stack.image.viewer.tsx 58 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687
  1. import React, { useEffect, useRef } from 'react';
  2. import * as cornerstone from '@cornerstonejs/core';
  3. import type { Types } from '@cornerstonejs/core';
  4. import * as cornerstoneTools from '@cornerstonejs/tools';
  5. import { annotation, SplineROITool } from '@cornerstonejs/tools';
  6. import { eventTarget } from '@cornerstonejs/core';
  7. import { registerGlobalTools } from '@/utils/cornerstoneToolsSetup';
  8. import { MeasurementToolManager } from '@/utils/measurementToolManager';
  9. import TibialPlateauAngleTool from '@/components/measures/TibialPlateauAngleTool';
  10. import DARAMeasurementTool from '@/components/measures/DARAMeasurementTool';
  11. import HipDIMeasurementTool from '@/components/measures/HipDIMeasurementTool';
  12. import HipNHAAngleMeasurementTool from '@/components/measures/HipNHAAngleMeasurementTool';
  13. import VHSMeasurementTool from '@/components/measures/VHSMeasurementTool';
  14. import TPLOMeasurementTool from '@/components/measures/TPLOMeasurementTool';
  15. import TTAMeasurementTool from '@/components/measures/TTAMeasurementTool';
  16. import CBLOMeasurementTool from '@/components/measures/CBLOMeasurementTool';
  17. import HipCoverageMeasurementTool from '@/components/measures/HipCoverageMeasurementTool';
  18. import HipDorsalCoverageTool from '@/components/measures/HipDorsalCoverageTool';
  19. import CircleCenterMeasurementTool from '@/components/measures/CircleCenterMeasurementTool';
  20. import MidlineMeasurementTool from '@/components/measures/MidlineMeasurementTool';
  21. import FindMidpointMeasurementTool from '@/components/measures/FindMidpointMeasurementTool';
  22. import VerticalTiltMeasurementTool from '@/components/measures/VerticalTiltMeasurementTool';
  23. import HorizontalTiltMeasurementTool from '@/components/measures/HorizontalTiltMeasurementTool';
  24. import DicomOverlayTool from '@/components/overlay/DicomOverlayTool';
  25. import ImageViewerErrorBoundary from './ImageViewerErrorBoundary';
  26. import { boolean } from 'zod';
  27. import { EVENTS } from '@cornerstonejs/core';
  28. import { useSelector } from 'react-redux';
  29. import { selectOverlayEnabled } from '@/states/view/dicomOverlaySlice';
  30. import PolygonLengthMeasurementTool from '@/components/measures/PolygonLengthMeasurementTool';
  31. import PolylineLengthMeasurementTool from '@/components/measures/PolylineLengthMeasurementTool';
  32. const {
  33. MagnifyTool,
  34. PanTool,
  35. WindowLevelTool,
  36. StackScrollTool,
  37. ZoomTool,
  38. LabelTool,
  39. LengthTool,
  40. AngleTool,//角度测量工具
  41. ToolGroupManager,
  42. Enums: csToolsEnums,
  43. PlanarRotateTool,
  44. } = cornerstoneTools;
  45. const { MouseBindings } = csToolsEnums;
  46. // let toolGroup: cornerstoneTools.Types.IToolGroup;
  47. // let currentViewportId: string;
  48. /**
  49. * 增强错误信息,添加更多上下文
  50. */
  51. function enhanceError(
  52. error: unknown,
  53. context: {
  54. imageUrls: string[];
  55. imageIndex: number;
  56. viewportId: string;
  57. }
  58. ): Error {
  59. const baseError = error instanceof Error ? error : new Error(String(error));
  60. const { imageUrls, imageIndex, viewportId } = context;
  61. const contextInfo = {
  62. imageUrls,
  63. imageIndex,
  64. viewportId,
  65. timestamp: new Date().toISOString(),
  66. userAgent: navigator.userAgent
  67. };
  68. // 创建增强的错误消息
  69. const enhancedMessage = `[${context.viewportId}] 图像加载失败: ${baseError.message}
  70. 上下文信息: ${JSON.stringify(contextInfo, null, 2)}`;
  71. const enhancedError = new Error(enhancedMessage);
  72. enhancedError.name = `ImageLoadError`;
  73. enhancedError.stack = `${enhancedError.name}: ${enhancedMessage}\n原错误堆栈: ${baseError.stack}`;
  74. return enhancedError;
  75. }
  76. /**
  77. * 判断是否为严重错误,需要抛出给 Error Boundary
  78. */
  79. function isCriticalError(error: Error): boolean {
  80. const message = error.message.toLowerCase();
  81. // 以下类型的错误被认为是严重的,需要用户干预
  82. const criticalErrors = [
  83. '图像url列表为空',
  84. '图像索引无效',
  85. '401', // 认证失败
  86. '403', // 权限不足
  87. '404', // 文件不存在
  88. '500', // 服务器错误
  89. '认证失败',
  90. '权限不足',
  91. 'unauthorized',
  92. 'forbidden',
  93. 'not found',
  94. 'internal server error'
  95. ];
  96. return criticalErrors.some(keyword => message.includes(keyword));
  97. }
  98. function getToolgroupByViewportId(currentViewportId: string) {
  99. const toolGroup = ToolGroupManager.getToolGroup(
  100. `STACK_TOOL_GROUP_ID_${currentViewportId}`
  101. );
  102. if (!toolGroup) {
  103. console.log('toolGroup not found');
  104. throw new Error('Tool group not found');
  105. }
  106. return toolGroup;
  107. }
  108. function overlayRedRectangle(currentViewportId: string) {
  109. const viewport = cornerstone.getEnabledElementByViewportId(currentViewportId)
  110. .viewport as cornerstone.StackViewport;
  111. const viewportElement = viewport.element;
  112. const annotations = cornerstoneTools.annotation.state.getAnnotations(
  113. 'LinearSplineROI',
  114. viewportElement
  115. );
  116. if (!annotations || annotations.length === 0) {
  117. console.log('No ROI annotations found');
  118. return;
  119. }
  120. const annotation = annotations[annotations.length - 1];
  121. const points = annotation?.data?.handles?.points;
  122. // 创建一个覆盖 Canvas
  123. const canvas = document.createElement('canvas');
  124. canvas.style.position = 'absolute';
  125. canvas.width = viewportElement.clientWidth;
  126. canvas.height = viewportElement.clientHeight;
  127. viewportElement.firstChild?.appendChild(canvas);
  128. const ctx = canvas.getContext('2d');
  129. if (!ctx) {
  130. throw new Error('Failed to get 2D context from canvas');
  131. }
  132. ctx.fillStyle = 'rgba(0, 0, 0, 1)';
  133. // 将 ROI 坐标转换为 Canvas 坐标
  134. if (!points) {
  135. console.log('No points found in handles');
  136. return;
  137. }
  138. // Convert all points to canvas coordinates
  139. const z = viewport.getCurrentImageIdIndex() || 0;
  140. const canvasPoints = points.map((point: Types.Point2 | Types.Point3) => {
  141. const point3D: Types.Point3 =
  142. point.length === 2 ? [point[0], point[1], z] : point;
  143. return viewport.worldToCanvas(point3D);
  144. });
  145. // Log for debugging
  146. console.log('Canvas Points:', canvasPoints);
  147. // Draw the polygon
  148. ctx.beginPath();
  149. ctx.rect(0, 0, canvas.width, canvas.height); // Full canvas for evenodd rule
  150. ctx.moveTo(canvasPoints[0][0], canvasPoints[0][1]); // Start at the first point
  151. for (let i = 1; i < canvasPoints.length; i++) {
  152. ctx.lineTo(canvasPoints[i][0], canvasPoints[i][1]); // Connect to subsequent points
  153. }
  154. ctx.closePath(); // Close the polygon
  155. ctx.fill('evenodd'); // Fill with red
  156. }
  157. function registerTools(viewportId, renderingEngineId) {
  158. // 确保全局工具已注册(只会执行一次)
  159. registerGlobalTools();
  160. // 创建该 viewport 的工具组
  161. const toolGroupId = `STACK_TOOL_GROUP_ID_${viewportId}`;
  162. const toolGroupTmp = ToolGroupManager.createToolGroup(toolGroupId);
  163. if (!toolGroupTmp) {
  164. console.error(
  165. `[registerTools] Failed to create tool group for viewport: ${viewportId}`
  166. );
  167. return;
  168. }
  169. const toolGroup = toolGroupTmp;
  170. // 添加工具到工具组
  171. toolGroup.addTool(MagnifyTool.toolName);
  172. toolGroup.addTool(PanTool.toolName);
  173. toolGroup.addTool(WindowLevelTool.toolName);
  174. toolGroup.addTool(StackScrollTool.toolName);
  175. toolGroup.addTool(ZoomTool.toolName);
  176. toolGroup.addTool(LabelTool.toolName);
  177. toolGroup.addTool(PlanarRotateTool.toolName);
  178. toolGroup.addTool(LengthTool.toolName); // 添加线段测量工具
  179. toolGroup.addTool(AngleTool.toolName); // 添加角度测量工具
  180. toolGroup.addTool(TibialPlateauAngleTool.toolName); // 添加胫骨平台夹角测量工具
  181. toolGroup.addTool(DARAMeasurementTool.toolName); // 添加髋臼水平角测量工具
  182. toolGroup.addTool(HipDIMeasurementTool.toolName); // 添加髋关节牵引指数测量工具
  183. toolGroup.addTool(HipNHAAngleMeasurementTool.toolName); // 添加髋关节水平角测量工具
  184. toolGroup.addTool(VHSMeasurementTool.toolName); // 添加心锥比测量工具
  185. toolGroup.addTool(TPLOMeasurementTool.toolName); // 添加TPLO测量工具
  186. toolGroup.addTool(TTAMeasurementTool.toolName); // 添加TTA测量工具
  187. toolGroup.addTool(CBLOMeasurementTool.toolName); // 添加CBLO测量工具
  188. toolGroup.addTool(HipCoverageMeasurementTool.toolName); // 添加股骨头覆盖率测量工具
  189. toolGroup.addTool(HipDorsalCoverageTool.toolName); // 添加髋臼背覆盖测量工具
  190. toolGroup.addTool(CircleCenterMeasurementTool.toolName); // 添加找圆心测量工具
  191. toolGroup.addTool(MidlineMeasurementTool.toolName); // 添加找中线测量工具
  192. toolGroup.addTool(FindMidpointMeasurementTool.toolName); // 添加找中点测量工具
  193. toolGroup.addTool(VerticalTiltMeasurementTool.toolName); // 添加直线垂直倾斜度测量工具
  194. toolGroup.addTool(HorizontalTiltMeasurementTool.toolName); // 添加直线水平倾斜度测量工具
  195. toolGroup.addTool(DicomOverlayTool.toolName); // 添加DICOM四角信息显示工具
  196. toolGroup.addTool(PolygonLengthMeasurementTool.toolName); // 添加多边形长度测量工具
  197. toolGroup.addTool(PolylineLengthMeasurementTool.toolName); // 添加拆线长度测量工具
  198. // 设置默认工具状态
  199. setupDefaultToolStates(toolGroup);
  200. // 添加 viewport 到工具组
  201. toolGroup.addViewport(viewportId, renderingEngineId);
  202. console.log(`[registerTools] Tools registered for viewport: ${viewportId}`);
  203. }
  204. /**
  205. * 设置默认工具状态
  206. */
  207. function setupDefaultToolStates(toolGroup: cornerstoneTools.Types.IToolGroup) {
  208. // 设置平移工具(中键)
  209. toolGroup.setToolActive(PanTool.toolName, {
  210. bindings: [
  211. {
  212. mouseButton: MouseBindings.Auxiliary, // Middle Click
  213. },
  214. ],
  215. });
  216. // 设置缩放工具(右键)
  217. toolGroup.setToolActive(ZoomTool.toolName, {
  218. bindings: [
  219. {
  220. mouseButton: MouseBindings.Secondary, // Right Click
  221. },
  222. ],
  223. });
  224. // 设置滚轮滚动工具
  225. toolGroup.setToolActive(StackScrollTool.toolName, {
  226. bindings: [
  227. {
  228. mouseButton: MouseBindings.Wheel, // Mouse Wheel
  229. },
  230. ],
  231. });
  232. // 其他工具默认为被动状态
  233. toolGroup.setToolPassive(MagnifyTool.toolName);
  234. toolGroup.setToolPassive(WindowLevelTool.toolName);
  235. toolGroup.setToolPassive(LabelTool.toolName);
  236. toolGroup.setToolPassive(PlanarRotateTool.toolName);
  237. toolGroup.setToolPassive(LengthTool.toolName);
  238. toolGroup.setToolPassive(AngleTool.toolName);
  239. toolGroup.setToolPassive(TibialPlateauAngleTool.toolName);
  240. toolGroup.setToolPassive(DARAMeasurementTool.toolName);
  241. toolGroup.setToolPassive(HipDIMeasurementTool.toolName);
  242. toolGroup.setToolPassive(HipNHAAngleMeasurementTool.toolName);
  243. toolGroup.setToolPassive(VHSMeasurementTool.toolName);
  244. toolGroup.setToolPassive(TPLOMeasurementTool.toolName);
  245. toolGroup.setToolPassive(TTAMeasurementTool.toolName);
  246. toolGroup.setToolPassive(CBLOMeasurementTool.toolName);
  247. toolGroup.setToolPassive(HipCoverageMeasurementTool.toolName);
  248. toolGroup.setToolPassive(HipDorsalCoverageTool.toolName);
  249. toolGroup.setToolPassive(CircleCenterMeasurementTool.toolName);
  250. toolGroup.setToolPassive(MidlineMeasurementTool.toolName);
  251. toolGroup.setToolPassive(FindMidpointMeasurementTool.toolName);
  252. toolGroup.setToolPassive(VerticalTiltMeasurementTool.toolName);
  253. toolGroup.setToolPassive(HorizontalTiltMeasurementTool.toolName);
  254. toolGroup.setToolPassive(PolygonLengthMeasurementTool.toolName);
  255. toolGroup.setToolPassive(PolylineLengthMeasurementTool.toolName);
  256. }
  257. export function addLMark(currentViewportId: string): void {
  258. // Implement the logic to add an L mark
  259. console.log('Adding L Mark viewport id : ', currentViewportId);
  260. const toolGroup = getToolgroupByViewportId(currentViewportId);
  261. // currentViewportId = viewportId;
  262. toolGroup.setToolActive(LabelTool.toolName, {
  263. bindings: [
  264. // {
  265. // mouseButton: MouseBindings.Primary, // Left Click
  266. // },
  267. ],
  268. });
  269. const position: Types.Point3 = [100, 100, 0]; // Example position
  270. const text = 'L'; // Predefined text
  271. LabelTool.hydrate(currentViewportId, position, text);
  272. toolGroup.setToolPassive(LabelTool.toolName, {
  273. removeAllBindings: true,
  274. });
  275. // const enabledElement = cornerstone.getEnabledElementByViewportId(currentViewportId);
  276. // cursors.elementCursor.resetElementCursor(elementRef.current as HTMLDivElement);
  277. }
  278. export function addRLabel(viewportId) {
  279. const toolGroup = getToolgroupByViewportId(viewportId);
  280. toolGroup.setToolActive(LabelTool.toolName, {
  281. bindings: [],
  282. });
  283. const element = document.getElementById(viewportId);
  284. const elementHeight = element ? element.getBoundingClientRect().height : 0;
  285. const position: Types.Point3 = [100, elementHeight / 2, 0]; // Example position
  286. const text = 'R'; // Predefined text
  287. LabelTool.hydrate(viewportId, position, text);
  288. toolGroup.setToolPassive(LabelTool.toolName, { removeAllBindings: true });
  289. }
  290. export function adjustBrightnessAndContrast(currentViewportId: string) {
  291. const toolGroup = getToolgroupByViewportId(currentViewportId);
  292. const planar = toolGroup.getToolInstance(WindowLevelTool.toolName); // Reset rotation angle
  293. const isActive = planar.mode === csToolsEnums.ToolModes.Active;
  294. if (isActive) {
  295. toolGroup.setToolPassive(WindowLevelTool.toolName, {
  296. removeAllBindings: true,
  297. });
  298. } else {
  299. toolGroup.setToolActive(WindowLevelTool.toolName, {
  300. bindings: [
  301. {
  302. mouseButton: MouseBindings.Primary, // Left Click
  303. },
  304. ],
  305. });
  306. }
  307. }
  308. /**
  309. * 防抖工具函数
  310. * @param func 要防抖的函数
  311. * @param wait 等待时间(毫秒)
  312. * @returns 防抖后的函数
  313. */
  314. function debounce<T extends (...args: any[]) => any>(func: T, wait: number): (...args: Parameters<T>) => void {
  315. let timeout: NodeJS.Timeout | null = null;
  316. return (...args: Parameters<T>) => {
  317. if (timeout) clearTimeout(timeout);
  318. timeout = setTimeout(() => func(...args), wait);
  319. };
  320. }
  321. function containImage(viewport:cornerstone.StackViewport) {
  322. viewport.getRenderingEngine().resize(/* immediate */ true);
  323. console.log(`使用 resetCamera方式`)
  324. viewport.resetCamera({ resetPan: true, resetZoom: true }); // 重置 pan/zoom 到 contain
  325. viewport.render();
  326. }
  327. /**
  328. * 保持宽高比的图像适应函数
  329. * 确保图像完全显示在容器内,同时保持原始宽高比(可能会有黑边)
  330. * @param currentViewportId 当前视口ID
  331. */
  332. export function fitImageWithAspectRatio(currentViewportId: string): void {
  333. try {
  334. console.log(`[fitImageWithAspectRatio] 重新适配大小`)
  335. const viewport = cornerstone.getEnabledElementByViewportId(currentViewportId)
  336. .viewport as cornerstone.StackViewport;
  337. if (!viewport) {
  338. console.warn(`[fitImageWithAspectRatio] Viewport not found: ${currentViewportId}`);
  339. return;
  340. }
  341. containImage(viewport)
  342. } catch (error) {
  343. console.error(`[fitImageWithAspectRatio] Error fitting image with aspect ratio:`, error);
  344. }
  345. }
  346. export function fitImageSize(currentViewportId: string) {
  347. // 使用新的保持宽高比的适应函数
  348. fitImageWithAspectRatio(currentViewportId);
  349. }
  350. export function deleteSelectedMark(currentViewportId: string): void {
  351. const viewport =
  352. cornerstone.getEnabledElementByViewportId(currentViewportId).viewport;
  353. const allAnnotations = cornerstoneTools.annotation.state.getAllAnnotations();
  354. // 删除所有 LabelTool 创建的标记(包括 L/R 标记、预定义标记、自定义标记、时间戳标记)
  355. for (const annotation of allAnnotations) {
  356. if (annotation.metadata?.toolName === LabelTool.toolName) {
  357. cornerstoneTools.annotation.state.removeAnnotation(
  358. annotation.annotationUID!
  359. );
  360. }
  361. }
  362. viewport.render();
  363. }
  364. export function addMark(currentViewportId: string): void {
  365. // Implement the logic to add a mask
  366. console.log('Adding Mask');
  367. // Add the specific logic to add a mask here
  368. cornerstoneTools.addTool(SplineROITool);
  369. const toolGroup = getToolgroupByViewportId(currentViewportId);
  370. toolGroup.addTool(SplineROITool.toolName);
  371. toolGroup.addToolInstance('LinearSplineROI', SplineROITool.toolName, {
  372. spline: {
  373. type: SplineROITool.SplineTypes.Linear,
  374. },
  375. });
  376. toolGroup.setToolActive('LinearSplineROI', {
  377. bindings: [{ mouseButton: MouseBindings.Primary }],
  378. });
  379. }
  380. /**
  381. * 添加自定义文本标记到图像
  382. */
  383. export function addCustomMark(currentViewportId: string, text: string): void {
  384. console.log(`Adding custom mark: ${text} to viewport: ${currentViewportId}`);
  385. const toolGroup = getToolgroupByViewportId(currentViewportId);
  386. // 激活标签工具
  387. toolGroup.setToolActive(LabelTool.toolName, {
  388. bindings: [],
  389. });
  390. // 获取视口元素和位置
  391. const element = document.getElementById(currentViewportId);
  392. if (!element) {
  393. console.error(`Element not found for viewport: ${currentViewportId}`);
  394. return;
  395. }
  396. // 计算标记位置(图像中心位置)
  397. const elementHeight = element.getBoundingClientRect().height;
  398. const elementWidth = element.getBoundingClientRect().width;
  399. const position: Types.Point3 = [elementWidth / 2, elementHeight / 2, 0];
  400. // 添加标签
  401. LabelTool.hydrate(currentViewportId, position, text);
  402. // 恢复工具状态
  403. toolGroup.setToolPassive(LabelTool.toolName, {
  404. removeAllBindings: true,
  405. });
  406. console.log(`Custom mark "${text}" added successfully`);
  407. }
  408. export function remoteMask(currentViewportId: string): void {
  409. // 1. 获取所有 annotation
  410. const all = annotation.state.getAllAnnotations();
  411. // 2. 过滤出 LinearSplineROI 产生的
  412. const toRemove = all.filter(
  413. (a) => a.metadata?.toolName === 'LinearSplineROI'
  414. );
  415. // 3. 逐条删掉
  416. toRemove.forEach((a) => {
  417. if (a.annotationUID) {
  418. annotation.state.removeAnnotation(a.annotationUID);
  419. }
  420. });
  421. const viewport = cornerstone.getEnabledElementByViewportId(currentViewportId)
  422. .viewport as cornerstone.StackViewport;
  423. viewport.render();
  424. console.log('Deleting Digital Mask');
  425. const viewportElement = viewport.element;
  426. const firstChild = viewportElement.firstChild;
  427. if (firstChild) {
  428. const canvasElements = Array.from(firstChild.childNodes).filter(
  429. (child): child is Element =>
  430. child instanceof Element && child.tagName === 'CANVAS'
  431. );
  432. canvasElements.slice(1).forEach((canvas) => {
  433. firstChild.removeChild(canvas);
  434. });
  435. }
  436. }
  437. export function HorizontalFlip(currentViewportId: string): void {
  438. const viewport = cornerstone.getEnabledElementByViewportId(currentViewportId)
  439. .viewport as cornerstone.StackViewport;
  440. // 切换水平翻转状态
  441. const { flipHorizontal } = viewport.getCamera();
  442. viewport.setCamera({ flipHorizontal: !flipHorizontal });
  443. console.log('Flipping Image Horizontally');
  444. }
  445. export function VerticalFlip(currentViewportId: string): void {
  446. const viewport = cornerstone.getEnabledElementByViewportId(currentViewportId)
  447. .viewport as cornerstone.StackViewport;
  448. // 切换竖直翻转状态
  449. const { flipVertical } = viewport.getCamera();
  450. viewport.setCamera({ flipVertical: !flipVertical });
  451. }
  452. export function RotateCounterclockwise90(currentViewportId: string): void {
  453. const viewport = cornerstone.getEnabledElementByViewportId(currentViewportId)
  454. .viewport as cornerstone.StackViewport;
  455. // 获取当前相机
  456. const camera = viewport.getCamera();
  457. // 计算新的旋转角度(当前角度 + 90度)
  458. const newRotation = (camera.rotation ?? 0) + 90;
  459. // 但计算viewUp向量时,我们应该使用90度的弧度,而不是新角度的弧度!
  460. const ninetyDegreesRadians = (90 * Math.PI) / 180;
  461. // 获取当前的viewUp向量
  462. const currentViewUp = camera.viewUp || [0, 1, 0];
  463. // 计算旋转后的viewUp向量(这才是正确的相对旋转)
  464. const newViewUp: [number, number, number] = [
  465. currentViewUp[0] * Math.cos(ninetyDegreesRadians) -
  466. currentViewUp[1] * Math.sin(ninetyDegreesRadians),
  467. currentViewUp[0] * Math.sin(ninetyDegreesRadians) +
  468. currentViewUp[1] * Math.cos(ninetyDegreesRadians),
  469. 0,
  470. ];
  471. // 设置新的相机参数
  472. viewport.setCamera({
  473. ...camera,
  474. viewUp: newViewUp,
  475. rotation: newRotation % 360, // 确保角度在0-359范围内
  476. });
  477. viewport.render();
  478. console.log('Rotating Image Counterclockwise 90°');
  479. }
  480. export function RotateClockwise90(currentViewportId: string): void {
  481. const viewport = cornerstone.getEnabledElementByViewportId(currentViewportId)
  482. .viewport as cornerstone.StackViewport;
  483. const camera = viewport.getCamera();
  484. // 计算新的旋转角度(当前角度 + 90度)
  485. const newRotation = (camera.rotation ?? 0) - 90;
  486. // 但计算viewUp向量时,我们应该使用90度的弧度,而不是新角度的弧度!
  487. const ninetyDegreesRadians = (90 * Math.PI) / 180;
  488. // 获取当前的viewUp向量
  489. const currentViewUp = camera.viewUp || [0, 1, 0];
  490. // 计算旋转后的viewUp向量(这才是正确的相对旋转)
  491. const newViewUp: [number, number, number] = [
  492. currentViewUp[0] * Math.cos(ninetyDegreesRadians) -
  493. currentViewUp[1] * Math.sin(ninetyDegreesRadians),
  494. currentViewUp[0] * Math.sin(ninetyDegreesRadians) +
  495. currentViewUp[1] * Math.cos(ninetyDegreesRadians),
  496. 0,
  497. ];
  498. // 设置新的相机参数
  499. viewport.setCamera({
  500. ...camera,
  501. viewUp: newViewUp,
  502. rotation: newRotation % 360, // 确保角度在0-359范围内
  503. });
  504. viewport.render();
  505. console.log('Rotating Image Clockwise 90°');
  506. }
  507. export function ResetImage(currentViewportId: string): void {
  508. const viewport = cornerstone.getEnabledElementByViewportId(currentViewportId)
  509. .viewport as cornerstone.StackViewport;
  510. // Implement the logic to reset the image
  511. // Resets the viewport's camera
  512. viewport.resetCamera();
  513. // Resets the viewport's properties
  514. viewport.resetProperties();
  515. viewport.render();
  516. console.log('Resetting Image');
  517. }
  518. export function InvertImage(currentViewportId: string): void {
  519. const viewport = cornerstone.getEnabledElementByViewportId(currentViewportId)
  520. .viewport as cornerstone.StackViewport;
  521. // Implement the logic to invert the image
  522. const invert = !viewport.getProperties().invert;
  523. viewport.setProperties({ invert });
  524. viewport.render();
  525. console.log('Inverting Image');
  526. }
  527. export function setOriginalSize(currentViewportId: string) {
  528. const viewport = cornerstone.getEnabledElementByViewportId(currentViewportId)
  529. .viewport as cornerstone.StackViewport;
  530. // 1) 先正常 fit(或本来就是 fit 状态)
  531. viewport.resetCamera();
  532. // 2) 计算“fit → 1:1”的放大倍数
  533. const { dimensions, spacing } = viewport.getImageData();
  534. console.log(`dimensions:${dimensions}, spacing:${spacing}`);
  535. const canvas = viewport.canvas;
  536. // 水平方向 1:1 需要的倍率
  537. const cssPixelsPerDicomPx = canvas.clientWidth / (dimensions[0] * spacing[0]);
  538. // 垂直方向 1:1 需要的倍率
  539. const cssPixelsPerDicomPy =
  540. canvas.clientHeight / (dimensions[1] * spacing[1]);
  541. // 取两者最小值,保证整张图不会被裁剪
  542. const zoomFactor = Math.min(cssPixelsPerDicomPx, cssPixelsPerDicomPy);
  543. console.log(`zoomFactor:${zoomFactor}`);
  544. console.log(
  545. `canvas.clientWidth:${canvas.clientWidth}, dimensions[0]:${dimensions[0]}, spacing[0]:${spacing[0]}`
  546. );
  547. console.log(
  548. `canvas.clientHeight:${canvas.clientHeight}, dimensions[1]:${dimensions[1]}, spacing[1]:${spacing[1]}`
  549. );
  550. // 3) 直接放大
  551. const zoom = viewport.getZoom();
  552. viewport.setZoom((zoom * 1) / zoomFactor);
  553. viewport.render();
  554. }
  555. export function activateMagnifier(currentViewportId: string) {
  556. console.log('Activating Magnifier');
  557. const toolGroup = getToolgroupByViewportId(currentViewportId);
  558. const isActive =
  559. toolGroup.getActivePrimaryMouseButtonTool() === MagnifyTool.toolName;
  560. if (isActive) {
  561. toolGroup.setToolPassive(MagnifyTool.toolName, {
  562. removeAllBindings: true,
  563. });
  564. } else {
  565. toolGroup.setToolActive(MagnifyTool.toolName, {
  566. bindings: [
  567. {
  568. mouseButton: MouseBindings.Primary, // Left Click
  569. },
  570. ],
  571. });
  572. }
  573. }
  574. export function invertContrast(currentViewportId: string) {
  575. const viewport =
  576. cornerstone.getEnabledElementByViewportId(currentViewportId).viewport;
  577. const targetBool = !viewport.getProperties().invert;
  578. viewport.setProperties({
  579. invert: targetBool,
  580. });
  581. viewport.render();
  582. }
  583. export function rotateAnyAngle(currentViewportId: string) {
  584. const toolGroup = getToolgroupByViewportId(currentViewportId);
  585. const planar = toolGroup.getToolInstance(PlanarRotateTool.toolName); // Reset rotation angle
  586. const isActive = planar.mode === csToolsEnums.ToolModes.Active;
  587. console.log(
  588. `PlanarRotateTool is currently ${isActive ? 'active' : 'inactive'}`
  589. );
  590. if (isActive) {
  591. toolGroup.setToolPassive(PlanarRotateTool.toolName, {
  592. removeAllBindings: true,
  593. });
  594. } else {
  595. toolGroup.setToolActive(PlanarRotateTool.toolName, {
  596. bindings: [
  597. {
  598. mouseButton: MouseBindings.Primary, // Left Click
  599. },
  600. ],
  601. });
  602. }
  603. console.log('Rotating Image by Any Angle');
  604. }
  605. // ==================== 线段测量相关函数 ====================
  606. /**
  607. * 激活线段测量工具
  608. */
  609. export function activateLengthMeasurement(viewportId: string): boolean {
  610. console.log(
  611. `[activateLengthMeasurement] Activating length measurement for viewport: ${viewportId}`
  612. );
  613. return MeasurementToolManager.activateLengthTool(viewportId);
  614. }
  615. /**
  616. * 停用线段测量工具
  617. */
  618. export function deactivateLengthMeasurement(viewportId: string): boolean {
  619. console.log(
  620. `[deactivateLengthMeasurement] Deactivating length measurement for viewport: ${viewportId}`
  621. );
  622. return MeasurementToolManager.deactivateLengthTool(viewportId);
  623. }
  624. /**
  625. * 切换线段测量工具状态
  626. */
  627. export function toggleLengthMeasurement(viewportId: string): boolean {
  628. console.log(
  629. `[toggleLengthMeasurement] Toggling length measurement for viewport: ${viewportId}`
  630. );
  631. return MeasurementToolManager.toggleLengthTool(viewportId);
  632. }
  633. /**
  634. * 清除线段测量标注
  635. */
  636. export function clearLengthMeasurements(viewportId: string): boolean {
  637. console.log(
  638. `[clearLengthMeasurements] Clearing length measurements for viewport: ${viewportId}`
  639. );
  640. return MeasurementToolManager.clearLengthMeasurements(viewportId);
  641. }
  642. /**
  643. * 获取线段测量结果
  644. */
  645. // eslint-disable-next-line
  646. export function getLengthMeasurements(viewportId: string): any[] {
  647. console.log(
  648. `[getLengthMeasurements] Getting length measurements for viewport: ${viewportId}`
  649. );
  650. return MeasurementToolManager.getLengthMeasurements(viewportId);
  651. }
  652. /**
  653. * 检查线段测量工具是否激活
  654. */
  655. export function isLengthMeasurementActive(viewportId: string): boolean {
  656. return MeasurementToolManager.isLengthToolActive(viewportId);
  657. }
  658. // ==================== 角度测量相关函数 ====================
  659. /**
  660. * 激活角度测量工具
  661. */
  662. export function activateAngleMeasurement(viewportId: string): boolean {
  663. console.log(
  664. `[activateAngleMeasurement] Activating angle measurement for viewport: ${viewportId}`
  665. );
  666. return MeasurementToolManager.activateAngleTool(viewportId);
  667. }
  668. /**
  669. * 停用角度测量工具
  670. */
  671. export function deactivateAngleMeasurement(viewportId: string): boolean {
  672. console.log(
  673. `[deactivateAngleMeasurement] Deactivating angle measurement for viewport: ${viewportId}`
  674. );
  675. return MeasurementToolManager.deactivateAngleTool(viewportId);
  676. }
  677. /**
  678. * 切换角度测量工具状态
  679. */
  680. export function toggleAngleMeasurement(viewportId: string): boolean {
  681. console.log(
  682. `[toggleAngleMeasurement] Toggling angle measurement for viewport: ${viewportId}`
  683. );
  684. return MeasurementToolManager.toggleAngleTool(viewportId);
  685. }
  686. /**
  687. * 清除角度测量标注
  688. */
  689. export function clearAngleMeasurements(viewportId: string): boolean {
  690. console.log(
  691. `[clearAngleMeasurements] Clearing angle measurements for viewport: ${viewportId}`
  692. );
  693. return MeasurementToolManager.clearAngleMeasurements(viewportId);
  694. }
  695. /**
  696. * 获取角度测量结果
  697. */
  698. // eslint-disable-next-line
  699. export function getAngleMeasurements(viewportId: string): any[] {
  700. console.log(
  701. `[getAngleMeasurements] Getting angle measurements for viewport: ${viewportId}`
  702. );
  703. return MeasurementToolManager.getAngleMeasurements(viewportId);
  704. }
  705. /**
  706. * 检查角度测量工具是否激活
  707. */
  708. export function isAngleMeasurementActive(viewportId: string): boolean {
  709. return MeasurementToolManager.isAngleToolActive(viewportId);
  710. }
  711. // ==================== 胫骨平台夹角测量相关函数 ====================
  712. /**
  713. * 激活胫骨平台夹角测量工具
  714. */
  715. export function activateTibialPlateauAngleMeasurement(viewportId: string): boolean {
  716. console.log(
  717. `[activateTibialPlateauAngleMeasurement] Activating TPA measurement for viewport: ${viewportId}`
  718. );
  719. return MeasurementToolManager.activateTibialPlateauAngleTool(viewportId);
  720. }
  721. /**
  722. * 停用胫骨平台夹角测量工具
  723. */
  724. export function deactivateTibialPlateauAngleMeasurement(viewportId: string): boolean {
  725. console.log(
  726. `[deactivateTibialPlateauAngleMeasurement] Deactivating TPA measurement for viewport: ${viewportId}`
  727. );
  728. return MeasurementToolManager.deactivateTibialPlateauAngleTool(viewportId);
  729. }
  730. /**
  731. * 切换胫骨平台夹角测量工具状态
  732. */
  733. export function toggleTibialPlateauAngleMeasurement(viewportId: string): boolean {
  734. console.log(
  735. `[toggleTibialPlateauAngleMeasurement] Toggling TPA measurement for viewport: ${viewportId}`
  736. );
  737. return MeasurementToolManager.toggleTibialPlateauAngleTool(viewportId);
  738. }
  739. /**
  740. * 清除胫骨平台夹角测量标注
  741. */
  742. export function clearTibialPlateauAngleMeasurements(viewportId: string): boolean {
  743. console.log(
  744. `[clearTibialPlateauAngleMeasurements] Clearing TPA measurements for viewport: ${viewportId}`
  745. );
  746. return MeasurementToolManager.clearTibialPlateauAngleMeasurements(viewportId);
  747. }
  748. /**
  749. * 获取胫骨平台夹角测量结果
  750. */
  751. // eslint-disable-next-line
  752. export function getTibialPlateauAngleMeasurements(viewportId: string): any[] {
  753. console.log(
  754. `[getTibialPlateauAngleMeasurements] Getting TPA measurements for viewport: ${viewportId}`
  755. );
  756. return MeasurementToolManager.getTibialPlateauAngleMeasurements(viewportId);
  757. }
  758. /**
  759. * 检查胫骨平台夹角测量工具是否激活
  760. */
  761. export function isTibialPlateauAngleMeasurementActive(viewportId: string): boolean {
  762. return MeasurementToolManager.isTibialPlateauAngleToolActive(viewportId);
  763. }
  764. // ==================== 髋臼水平角测量相关函数 ====================
  765. /**
  766. * 激活髋臼水平角测量工具
  767. */
  768. export function activateDARAMeasurement(viewportId: string): boolean {
  769. console.log(
  770. `[activateDARAMeasurement] Activating DARA measurement for viewport: ${viewportId}`
  771. );
  772. return MeasurementToolManager.activateDARAMeasurementTool(viewportId);
  773. }
  774. /**
  775. * 停用髋臼水平角测量工具
  776. */
  777. export function deactivateDARAMeasurement(viewportId: string): boolean {
  778. console.log(
  779. `[deactivateDARAMeasurement] Deactivating DARA measurement for viewport: ${viewportId}`
  780. );
  781. return MeasurementToolManager.deactivateDARAMeasurementTool(viewportId);
  782. }
  783. /**
  784. * 切换髋臼水平角测量工具状态
  785. */
  786. export function toggleDARAMeasurement(viewportId: string): boolean {
  787. console.log(
  788. `[toggleDARAMeasurement] Toggling DARA measurement for viewport: ${viewportId}`
  789. );
  790. return MeasurementToolManager.toggleDARAMeasurementTool(viewportId);
  791. }
  792. /**
  793. * 清除髋臼水平角测量标注
  794. */
  795. export function clearDARAMeasurements(viewportId: string): boolean {
  796. console.log(
  797. `[clearDARAMeasurements] Clearing DARA measurements for viewport: ${viewportId}`
  798. );
  799. return MeasurementToolManager.clearDARAMeasurements(viewportId);
  800. }
  801. /**
  802. * 获取髋臼水平角测量结果
  803. */
  804. // eslint-disable-next-line
  805. export function getDARAMeasurements(viewportId: string): any[] {
  806. console.log(
  807. `[getDARAMeasurements] Getting DARA measurements for viewport: ${viewportId}`
  808. );
  809. return MeasurementToolManager.getDARAMeasurements(viewportId);
  810. }
  811. /**
  812. * 检查髋臼水平角测量工具是否激活
  813. */
  814. export function isDARAMeasurementActive(viewportId: string): boolean {
  815. return MeasurementToolManager.isDARAMeasurementToolActive(viewportId);
  816. }
  817. // ==================== 髋关节牵引指数测量相关函数 ====================
  818. /**
  819. * 激活髋关节牵引指数测量工具
  820. */
  821. export function activateHipDIMeasurement(viewportId: string): boolean {
  822. console.log(
  823. `[activateHipDIMeasurement] Activating HipDI measurement for viewport: ${viewportId}`
  824. );
  825. return MeasurementToolManager.activateHipDIMeasurementTool(viewportId);
  826. }
  827. /**
  828. * 停用髋关节牵引指数测量工具
  829. */
  830. export function deactivateHipDIMeasurement(viewportId: string): boolean {
  831. console.log(
  832. `[deactivateHipDIMeasurement] Deactivating HipDI measurement for viewport: ${viewportId}`
  833. );
  834. return MeasurementToolManager.deactivateHipDIMeasurementTool(viewportId);
  835. }
  836. /**
  837. * 切换髋关节牵引指数测量工具状态
  838. */
  839. export function toggleHipDIMeasurement(viewportId: string): boolean {
  840. console.log(
  841. `[toggleHipDIMeasurement] Toggling HipDI measurement for viewport: ${viewportId}`
  842. );
  843. return MeasurementToolManager.toggleHipDIMeasurementTool(viewportId);
  844. }
  845. /**
  846. * 清除髋关节牵引指数测量标注
  847. */
  848. export function clearHipDIMeasurements(viewportId: string): boolean {
  849. console.log(
  850. `[clearHipDIMeasurements] Clearing HipDI measurements for viewport: ${viewportId}`
  851. );
  852. return MeasurementToolManager.clearHipDIMeasurements(viewportId);
  853. }
  854. /**
  855. * 获取髋关节牵引指数测量结果
  856. */
  857. // eslint-disable-next-line
  858. export function getHipDIMeasurements(viewportId: string): any[] {
  859. console.log(
  860. `[getHipDIMeasurements] Getting HipDI measurements for viewport: ${viewportId}`
  861. );
  862. return MeasurementToolManager.getHipDIMeasurements(viewportId);
  863. }
  864. /**
  865. * 检查髋关节牵引指数测量工具是否激活
  866. */
  867. export function isHipDIMeasurementActive(viewportId: string): boolean {
  868. return MeasurementToolManager.isHipDIMeasurementToolActive(viewportId);
  869. }
  870. // ==================== 髋关节水平角测量相关函数 ====================
  871. /**
  872. * 激活髋关节水平角测量工具
  873. */
  874. export function activateHipNHAAngleMeasurement(viewportId: string): boolean {
  875. console.log(
  876. `[activateHipNHAAngleMeasurement] Activating HipNHA measurement for viewport: ${viewportId}`
  877. );
  878. return MeasurementToolManager.activateHipNHAAngleMeasurementTool(viewportId);
  879. }
  880. /**
  881. * 停用髋关节水平角测量工具
  882. */
  883. export function deactivateHipNHAAngleMeasurement(viewportId: string): boolean {
  884. console.log(
  885. `[deactivateHipNHAAngleMeasurement] Deactivating HipNHA measurement for viewport: ${viewportId}`
  886. );
  887. return MeasurementToolManager.deactivateHipNHAAngleMeasurementTool(viewportId);
  888. }
  889. /**
  890. * 切换髋关节水平角测量工具状态
  891. */
  892. export function toggleHipNHAAngleMeasurement(viewportId: string): boolean {
  893. console.log(
  894. `[toggleHipNHAAngleMeasurement] Toggling HipNHA measurement for viewport: ${viewportId}`
  895. );
  896. return MeasurementToolManager.toggleHipNHAAngleMeasurementTool(viewportId);
  897. }
  898. /**
  899. * 清除髋关节水平角测量标注
  900. */
  901. export function clearHipNHAAngleMeasurements(viewportId: string): boolean {
  902. console.log(
  903. `[clearHipNHAAngleMeasurements] Clearing HipNHA measurements for viewport: ${viewportId}`
  904. );
  905. return MeasurementToolManager.clearHipNHAAngleMeasurements(viewportId);
  906. }
  907. /**
  908. * 获取髋关节水平角测量结果
  909. */
  910. // eslint-disable-next-line
  911. export function getHipNHAAngleMeasurements(viewportId: string): any[] {
  912. console.log(
  913. `[getHipNHAAngleMeasurements] Getting HipNHA measurements for viewport: ${viewportId}`
  914. );
  915. return MeasurementToolManager.getHipNHAAngleMeasurements(viewportId);
  916. }
  917. /**
  918. * 检查髋关节水平角测量工具是否激活
  919. */
  920. export function isHipNHAAngleMeasurementActive(viewportId: string): boolean {
  921. return MeasurementToolManager.isHipNHAAngleMeasurementToolActive(viewportId);
  922. }
  923. // ==================== TPLO测量相关函数 ====================
  924. /**
  925. * 激活TPLO测量工具
  926. */
  927. export function activateTPLOMeasurement(viewportId: string): boolean {
  928. console.log(
  929. `[activateTPLOMeasurement] Activating TPLO measurement for viewport: ${viewportId}`
  930. );
  931. return MeasurementToolManager.activateTPLOMeasurementTool(viewportId);
  932. }
  933. /**
  934. * 停用TPLO测量工具
  935. */
  936. export function deactivateTPLOMeasurement(viewportId: string): boolean {
  937. console.log(
  938. `[deactivateTPLOMeasurement] Deactivating TPLO measurement for viewport: ${viewportId}`
  939. );
  940. return MeasurementToolManager.deactivateTPLOMeasurementTool(viewportId);
  941. }
  942. /**
  943. * 切换TPLO测量工具状态
  944. */
  945. export function toggleTPLOMeasurement(viewportId: string): boolean {
  946. console.log(
  947. `[toggleTPLOMeasurement] Toggling TPLO measurement for viewport: ${viewportId}`
  948. );
  949. return MeasurementToolManager.toggleTPLOMeasurementTool(viewportId);
  950. }
  951. /**
  952. * 清除TPLO测量标注
  953. */
  954. export function clearTPLOMeasurements(viewportId: string): boolean {
  955. console.log(
  956. `[clearTPLOMeasurements] Clearing TPLO measurements for viewport: ${viewportId}`
  957. );
  958. return MeasurementToolManager.clearTPLOMeasurements(viewportId);
  959. }
  960. /**
  961. * 获取TPLO测量结果
  962. */
  963. // eslint-disable-next-line
  964. export function getTPLOMeasurements(viewportId: string): any[] {
  965. console.log(
  966. `[getTPLOMeasurements] Getting TPLO measurements for viewport: ${viewportId}`
  967. );
  968. return MeasurementToolManager.getTPLOMeasurements(viewportId);
  969. }
  970. /**
  971. * 检查TPLO测量工具是否激活
  972. */
  973. export function isTPLOMeasurementActive(viewportId: string): boolean {
  974. return MeasurementToolManager.isTPLOMeasurementToolActive(viewportId);
  975. }
  976. // ==================== TTA测量相关函数 ====================
  977. /**
  978. * 激活TTA测量工具
  979. */
  980. export function activateTTAMeasurement(viewportId: string): boolean {
  981. console.log(
  982. `[activateTTAMeasurement] Activating TTA measurement for viewport: ${viewportId}`
  983. );
  984. return MeasurementToolManager.activateTTAMeasurementTool(viewportId);
  985. }
  986. /**
  987. * 停用TTA测量工具
  988. */
  989. export function deactivateTTAMeasurement(viewportId: string): boolean {
  990. console.log(
  991. `[deactivateTTAMeasurement] Deactivating TTA measurement for viewport: ${viewportId}`
  992. );
  993. return MeasurementToolManager.deactivateTTAMeasurementTool(viewportId);
  994. }
  995. /**
  996. * 切换TTA测量工具状态
  997. */
  998. export function toggleTTAMeasurement(viewportId: string): boolean {
  999. console.log(
  1000. `[toggleTTAMeasurement] Toggling TTA measurement for viewport: ${viewportId}`
  1001. );
  1002. return MeasurementToolManager.toggleTTAMeasurementTool(viewportId);
  1003. }
  1004. /**
  1005. * 清除TTA测量标注
  1006. */
  1007. export function clearTTAMeasurements(viewportId: string): boolean {
  1008. console.log(
  1009. `[clearTTAMeasurements] Clearing TTA measurements for viewport: ${viewportId}`
  1010. );
  1011. return MeasurementToolManager.clearTTAMeasurements(viewportId);
  1012. }
  1013. /**
  1014. * 获取TTA测量结果
  1015. */
  1016. // eslint-disable-next-line
  1017. export function getTTAMeasurements(viewportId: string): any[] {
  1018. console.log(
  1019. `[getTTAMeasurements] Getting TTA measurements for viewport: ${viewportId}`
  1020. );
  1021. return MeasurementToolManager.getTTAMeasurements(viewportId);
  1022. }
  1023. /**
  1024. * 检查TTA测量工具是否激活
  1025. */
  1026. export function isTTAMeasurementActive(viewportId: string): boolean {
  1027. return MeasurementToolManager.isTTAMeasurementToolActive(viewportId);
  1028. }
  1029. // ==================== CBLO测量相关函数 ====================
  1030. export function activateCBLOMeasurement(viewportId: string): boolean {
  1031. console.log(`[activateCBLOMeasurement] Activating CBLO measurement for viewport: ${viewportId}`);
  1032. return MeasurementToolManager.activateCBLOMeasurementTool(viewportId);
  1033. }
  1034. export function deactivateCBLOMeasurement(viewportId: string): boolean {
  1035. console.log(`[deactivateCBLOMeasurement] Deactivating CBLO measurement for viewport: ${viewportId}`);
  1036. return MeasurementToolManager.deactivateCBLOMeasurementTool(viewportId);
  1037. }
  1038. export function clearCBLOMeasurements(viewportId: string): boolean {
  1039. console.log(`[clearCBLOMeasurements] Clearing CBLO measurements for viewport: ${viewportId}`);
  1040. return MeasurementToolManager.clearCBLOMeasurements(viewportId);
  1041. }
  1042. // ==================== HipCoverage测量相关函数 ====================
  1043. export function activateHipCoverageMeasurement(viewportId: string): boolean {
  1044. console.log(`[activateHipCoverageMeasurement] Activating HipCoverage measurement for viewport: ${viewportId}`);
  1045. return MeasurementToolManager.activateHipCoverageMeasurementTool(viewportId);
  1046. }
  1047. export function deactivateHipCoverageMeasurement(viewportId: string): boolean {
  1048. console.log(`[deactivateHipCoverageMeasurement] Deactivating HipCoverage measurement for viewport: ${viewportId}`);
  1049. return MeasurementToolManager.deactivateHipCoverageMeasurementTool(viewportId);
  1050. }
  1051. export function clearHipCoverageMeasurements(viewportId: string): boolean {
  1052. console.log(`[clearHipCoverageMeasurements] Clearing HipCoverage measurements for viewport: ${viewportId}`);
  1053. return MeasurementToolManager.clearHipCoverageMeasurements(viewportId);
  1054. }
  1055. // ==================== HipDorsalCoverage测量相关函数 ====================
  1056. export function activateHipDorsalCoverageMeasurement(viewportId: string): boolean {
  1057. console.log(`[activateHipDorsalCoverageMeasurement] Activating HipDorsalCoverage measurement for viewport: ${viewportId}`);
  1058. return MeasurementToolManager.activateHipDorsalCoverageTool(viewportId);
  1059. }
  1060. export function deactivateHipDorsalCoverageMeasurement(viewportId: string): boolean {
  1061. console.log(`[deactivateHipDorsalCoverageMeasurement] Deactivating HipDorsalCoverage measurement for viewport: ${viewportId}`);
  1062. return MeasurementToolManager.deactivateHipDorsalCoverageTool(viewportId);
  1063. }
  1064. export function clearHipDorsalCoverageMeasurements(viewportId: string): boolean {
  1065. console.log(`[clearHipDorsalCoverageMeasurements] Clearing HipDorsalCoverage measurements for viewport: ${viewportId}`);
  1066. return MeasurementToolManager.clearHipDorsalCoverageMeasurements(viewportId);
  1067. }
  1068. // ==================== 找圆心测量相关函数 ====================
  1069. export function activateCircleCenterMeasurement(viewportId: string): boolean {
  1070. console.log(`[activateCircleCenterMeasurement] Activating CircleCenter measurement for viewport: ${viewportId}`);
  1071. return MeasurementToolManager.activateCircleCenterMeasurementTool(viewportId);
  1072. }
  1073. export function deactivateCircleCenterMeasurement(viewportId: string): boolean {
  1074. console.log(`[deactivateCircleCenterMeasurement] Deactivating CircleCenter measurement for viewport: ${viewportId}`);
  1075. return MeasurementToolManager.deactivateCircleCenterMeasurementTool(viewportId);
  1076. }
  1077. export function clearCircleCenterMeasurements(viewportId: string): boolean {
  1078. console.log(`[clearCircleCenterMeasurements] Clearing CircleCenter measurements for viewport: ${viewportId}`);
  1079. return MeasurementToolManager.clearCircleCenterMeasurements(viewportId);
  1080. }
  1081. // ==================== 找中线测量相关函数 ====================
  1082. export function activateMidlineMeasurement(viewportId: string): boolean {
  1083. console.log(`[activateMidlineMeasurement] Activating Midline measurement for viewport: ${viewportId}`);
  1084. return MeasurementToolManager.activateMidlineMeasurementTool(viewportId);
  1085. }
  1086. export function deactivateMidlineMeasurement(viewportId: string): boolean {
  1087. console.log(`[deactivateMidlineMeasurement] Deactivating Midline measurement for viewport: ${viewportId}`);
  1088. return MeasurementToolManager.deactivateMidlineMeasurementTool(viewportId);
  1089. }
  1090. export function clearMidlineMeasurements(viewportId: string): boolean {
  1091. console.log(`[clearMidlineMeasurements] Clearing Midline measurements for viewport: ${viewportId}`);
  1092. return MeasurementToolManager.clearMidlineMeasurements(viewportId);
  1093. }
  1094. // ==================== 找中点测量相关函数 ====================
  1095. export function activateFindMidpointMeasurement(viewportId: string): boolean {
  1096. console.log(`[activateFindMidpointMeasurement] Activating FindMidpoint measurement for viewport: ${viewportId}`);
  1097. return MeasurementToolManager.activateFindMidpointMeasurementTool(viewportId);
  1098. }
  1099. export function deactivateFindMidpointMeasurement(viewportId: string): boolean {
  1100. console.log(`[deactivateFindMidpointMeasurement] Deactivating FindMidpoint measurement for viewport: ${viewportId}`);
  1101. return MeasurementToolManager.deactivateFindMidpointMeasurementTool(viewportId);
  1102. }
  1103. export function clearFindMidpointMeasurements(viewportId: string): boolean {
  1104. console.log(`[clearFindMidpointMeasurements] Clearing FindMidpoint measurements for viewport: ${viewportId}`);
  1105. return MeasurementToolManager.clearFindMidpointMeasurements(viewportId);
  1106. }
  1107. // ==================== 直线垂直倾斜度测量相关函数 ====================
  1108. export function activateVerticalTiltMeasurement(viewportId: string): boolean {
  1109. console.log(`[activateVerticalTiltMeasurement] Activating VerticalTilt measurement for viewport: ${viewportId}`);
  1110. return MeasurementToolManager.activateVerticalTiltMeasurementTool(viewportId);
  1111. }
  1112. export function deactivateVerticalTiltMeasurement(viewportId: string): boolean {
  1113. console.log(`[deactivateVerticalTiltMeasurement] Deactivating VerticalTilt measurement for viewport: ${viewportId}`);
  1114. return MeasurementToolManager.deactivateVerticalTiltMeasurementTool(viewportId);
  1115. }
  1116. export function clearVerticalTiltMeasurements(viewportId: string): boolean {
  1117. console.log(`[clearVerticalTiltMeasurements] Clearing VerticalTilt measurements for viewport: ${viewportId}`);
  1118. return MeasurementToolManager.clearVerticalTiltMeasurements(viewportId);
  1119. }
  1120. // ==================== 直线水平倾斜度测量相关函数 ====================
  1121. export function activateHorizontalTiltMeasurement(viewportId: string): boolean {
  1122. console.log(`[activateHorizontalTiltMeasurement] Activating HorizontalTilt measurement for viewport: ${viewportId}`);
  1123. return MeasurementToolManager.activateHorizontalTiltMeasurementTool(viewportId);
  1124. }
  1125. export function deactivateHorizontalTiltMeasurement(viewportId: string): boolean {
  1126. console.log(`[deactivateHorizontalTiltMeasurement] Deactivating HorizontalTilt measurement for viewport: ${viewportId}`);
  1127. return MeasurementToolManager.deactivateHorizontalTiltMeasurementTool(viewportId);
  1128. }
  1129. export function clearHorizontalTiltMeasurements(viewportId: string): boolean {
  1130. console.log(`[clearHorizontalTiltMeasurements] Clearing HorizontalTilt measurements for viewport: ${viewportId}`);
  1131. return MeasurementToolManager.clearHorizontalTiltMeasurements(viewportId);
  1132. }
  1133. // ==================== 多边形长度测量相关函数 ====================
  1134. /**
  1135. * 激活多边形长度测量工具
  1136. */
  1137. export function activatePolygonLengthMeasurement(viewportId: string): boolean {
  1138. console.log(`[activatePolygonLengthMeasurement] Activating PolygonLength measurement for viewport: ${viewportId}`);
  1139. return MeasurementToolManager.activatePolygonLengthMeasurementTool(viewportId);
  1140. }
  1141. /**
  1142. * 停用多边形长度测量工具
  1143. */
  1144. export function deactivatePolygonLengthMeasurement(viewportId: string): boolean {
  1145. console.log(`[deactivatePolygonLengthMeasurement] Deactivating PolygonLength measurement for viewport: ${viewportId}`);
  1146. return MeasurementToolManager.deactivatePolygonLengthMeasurementTool(viewportId);
  1147. }
  1148. /**
  1149. * 清除多边形长度测量标注
  1150. */
  1151. export function clearPolygonLengthMeasurements(viewportId: string): boolean {
  1152. console.log(`[clearPolygonLengthMeasurements] Clearing PolygonLength measurements for viewport: ${viewportId}`);
  1153. return MeasurementToolManager.clearPolygonLengthMeasurements(viewportId);
  1154. }
  1155. // ==================== 拆线长度测量相关函数 ====================
  1156. /**
  1157. * 激活拆线长度测量工具
  1158. */
  1159. export function activatePolylineLengthMeasurement(viewportId: string): boolean {
  1160. console.log(`[activatePolylineLengthMeasurement] Activating PolylineLength measurement for viewport: ${viewportId}`);
  1161. return MeasurementToolManager.activatePolylineLengthMeasurementTool(viewportId);
  1162. }
  1163. /**
  1164. * 停用拆线长度测量工具
  1165. */
  1166. export function deactivatePolylineLengthMeasurement(viewportId: string): boolean {
  1167. console.log(`[deactivatePolylineLengthMeasurement] Deactivating PolylineLength measurement for viewport: ${viewportId}`);
  1168. return MeasurementToolManager.deactivatePolylineLengthMeasurementTool(viewportId);
  1169. }
  1170. /**
  1171. * 清除拆线长度测量标注
  1172. */
  1173. export function clearPolylineLengthMeasurements(viewportId: string): boolean {
  1174. console.log(`[clearPolylineLengthMeasurements] Clearing PolylineLength measurements for viewport: ${viewportId}`);
  1175. return MeasurementToolManager.clearPolylineLengthMeasurements(viewportId);
  1176. }
  1177. export class ImageLoadError extends Error {
  1178. constructor(message: string, failedImageIds: string[]) {
  1179. super(message);
  1180. this.name = 'ImageLoadError';
  1181. this.failedImageIds = failedImageIds; // 附加失败 ID 列表
  1182. }
  1183. failedImageIds: string[];
  1184. }
  1185. /**
  1186. * 安全的图像栈设置函数,带超时保护和错误捕获
  1187. * @param viewport - Cornerstone viewport 实例
  1188. * @param imageIds - 图像 ID 数组
  1189. * @param imageIndex - 当前图像索引
  1190. * @param timeout - 超时时间(毫秒),默认30秒
  1191. */
  1192. export async function safeSetStack(
  1193. viewport: any,
  1194. imageIds: string[],
  1195. imageIndex: number,
  1196. timeout: number = 30000
  1197. ): Promise<void> {
  1198. const errors: string[] = [];
  1199. // 错误事件处理器
  1200. const handler = (evt: any) => {
  1201. console.error(`捕获到图像加载错误: ${evt.detail.imageId}`);
  1202. errors.push(evt.detail.imageId);
  1203. };
  1204. // 添加错误监听器
  1205. eventTarget.addEventListener(EVENTS.IMAGE_LOAD_ERROR, handler);
  1206. try {
  1207. // 创建超时 Promise
  1208. const timeoutPromise = new Promise<never>((_, reject) => {
  1209. setTimeout(() => {
  1210. reject(new Error(`图像加载超时 (${timeout}ms)`));
  1211. }, timeout);
  1212. });
  1213. // 等待 setStack 完成或超时
  1214. await Promise.race([
  1215. viewport.setStack(imageIds, imageIndex),
  1216. timeoutPromise
  1217. ]);
  1218. // setStack 完成后,检查是否有错误
  1219. if (errors.length > 0) {
  1220. throw new ImageLoadError(
  1221. `图像加载失败:共 ${errors.length} 张(${errors.join(', ')})`,
  1222. errors
  1223. );
  1224. }
  1225. console.log(`✅ 图像栈设置成功: ${imageIds.length} 张图像`);
  1226. } finally {
  1227. // 无论成功失败,都移除监听器(清理资源)
  1228. eventTarget.removeEventListener(EVENTS.IMAGE_LOAD_ERROR, handler);
  1229. }
  1230. }
  1231. const StackViewer = ({
  1232. imageIndex = 0,
  1233. imageUrls = [],
  1234. viewportId,
  1235. renderingEngineId,
  1236. selected
  1237. }: {
  1238. imageIndex?: number;
  1239. imageUrls?: string[];
  1240. viewportId: string;
  1241. renderingEngineId: string;
  1242. selected?: boolean;
  1243. }) => {
  1244. const elementRef = useRef<HTMLDivElement>(null);
  1245. // 用于捕获异步错误并在渲染时抛出,让 Error Boundary 能够捕获
  1246. const [renderError, setRenderError] = React.useState<Error | null>(null);
  1247. // 获取overlay启用状态
  1248. const overlayEnabled = useSelector(selectOverlayEnabled);
  1249. // const action = useSelector((state: RootState) => state.functionArea.action);
  1250. // const dispatch = useDispatch();
  1251. // 监听overlay状态变化,启用/禁用DicomOverlayTool
  1252. useEffect(() => {
  1253. try {
  1254. const toolGroup = MeasurementToolManager.getToolGroup(viewportId);
  1255. // const toolGroup = ToolGroupManager.getToolGroup(`STACK_TOOL_GROUP_ID_${viewportId}`);
  1256. if (!toolGroup) {
  1257. console.warn(`[StackViewer] Tool group not found for viewport: ${viewportId}`);
  1258. return;
  1259. }
  1260. if (overlayEnabled) {
  1261. console.log(`[StackViewer] Enabling DicomOverlay for viewport: ${viewportId}`);
  1262. toolGroup.setToolEnabled(DicomOverlayTool.toolName);
  1263. } else {
  1264. console.log(`[StackViewer] Disabling DicomOverlay for viewport: ${viewportId}`);
  1265. toolGroup.setToolDisabled(DicomOverlayTool.toolName);
  1266. //再次启用,表示关闭四角信息
  1267. toolGroup.setToolEnabled(DicomOverlayTool.toolName);
  1268. }
  1269. } catch (error) {
  1270. console.error(`[StackViewer] Error toggling DicomOverlay:`, error);
  1271. }
  1272. }, [overlayEnabled, viewportId]);
  1273. // // 监听imageUrls变化并重新加载图像
  1274. // useEffect(() => {
  1275. // const renderingEngine = cornerstone.getRenderingEngine(renderingEngineId);
  1276. // if (!renderingEngine) {
  1277. // return;
  1278. // }
  1279. // const viewport = renderingEngine.getViewport(viewportId) as cornerstone.Types.IStackViewport;
  1280. // if (viewport && imageUrls.length > 0) {
  1281. // console.log(`[StackViewer] imageUrls changed for viewport ${viewportId}, reloading...`);
  1282. // viewport.setStack(imageUrls, imageIndex).then(() => {
  1283. // viewport.render();
  1284. // }).catch((error) => {
  1285. // console.error('[StackViewer] Error reloading image stack:', error);
  1286. // });
  1287. // }
  1288. // }, [imageUrls, imageIndex, viewportId, renderingEngineId]);
  1289. useEffect(() => {
  1290. const setup = async () => {
  1291. // 清除之前的错误状态(如果有)
  1292. setRenderError(null);
  1293. // // 初始化 Cornerstone
  1294. // cornerstone.init();
  1295. // cornerstoneTools.init();
  1296. // const state = store.getState();
  1297. // console.log(`当前系统模式:${state.systemMode.mode}`);
  1298. // const token =
  1299. // state.systemMode.mode === SystemMode.Emergency
  1300. // ? state.product.guest
  1301. // : state.userInfo.token;
  1302. // console.log(`token stack.image.viewer: ${token}`);
  1303. // cornerstoneDICOMImageLoader.init({
  1304. // maxWebWorkers: navigator.hardwareConcurrency || 1,
  1305. // errorInterceptor: (error) => {
  1306. // if (error.status === 401) {
  1307. // console.error('Authentication failed. Please refresh the token.');
  1308. // }
  1309. // console.error(`请求dcm文件出错:${error}`);
  1310. // },
  1311. // beforeSend: (xhr, imageId, defaultHeaders) => {
  1312. // return {
  1313. // ...defaultHeaders,
  1314. // Authorization: `Bearer ${token}`,
  1315. // Language: 'en',
  1316. // Product: 'DROS',
  1317. // Source: 'Electron',
  1318. // };
  1319. // },
  1320. // });
  1321. // const currentViewportId = viewportId;
  1322. eventTarget.addEventListener(
  1323. cornerstoneTools.Enums.Events.ANNOTATION_COMPLETED,
  1324. (evt) => {
  1325. const { annotation } = evt.detail;
  1326. console.log('Annotation completed event:', annotation);
  1327. if (annotation.metadata.toolName === 'LinearSplineROI') {
  1328. console.log('SplineROITool annotation completed:', annotation);
  1329. overlayRedRectangle(viewportId);
  1330. //取消工具激活状态
  1331. const toolGroup = ToolGroupManager.getToolGroup(
  1332. 'STACK_TOOL_GROUP_ID'
  1333. );
  1334. if (!toolGroup) {
  1335. console.log('toolGroup not found');
  1336. }
  1337. toolGroup?.setToolPassive('LinearSplineROI', {
  1338. removeAllBindings: true,
  1339. });
  1340. }
  1341. }
  1342. );
  1343. const viewportInput: cornerstone.Types.PublicViewportInput = {
  1344. viewportId,
  1345. element: elementRef.current!,
  1346. type: cornerstone.Enums.ViewportType.STACK,
  1347. };
  1348. const renderingEngine = cornerstone.getRenderingEngine(renderingEngineId);
  1349. if (!renderingEngine) {
  1350. console.error(
  1351. `[stack.image.viewer] No rendering engine with id ${renderingEngineId} found`
  1352. );
  1353. return;
  1354. }
  1355. // Enable the element for use with Cornerstone
  1356. renderingEngine.enableElement(viewportInput);
  1357. registerTools(viewportId, renderingEngine.id);
  1358. // Get the stack viewport that was created
  1359. const viewport = renderingEngine.getViewport(
  1360. viewportId
  1361. ) as cornerstone.Types.IStackViewport;
  1362. // 增强的图像加载逻辑,包含完善的错误处理
  1363. try {
  1364. console.log(`重新加载图像----开始`);
  1365. console.log(`图像URLs:`, imageUrls);
  1366. console.log(`图像索引:`, imageIndex);
  1367. // 1. 验证图像URLs
  1368. if (!imageUrls || imageUrls.length === 0) {
  1369. throw new Error('图像URL列表为空');
  1370. }
  1371. // 2. 验证图像索引
  1372. if (imageIndex < 0 || imageIndex >= imageUrls.length) {
  1373. throw new Error(`图像索引无效: ${imageIndex}, 有效范围: 0-${imageUrls.length - 1}`);
  1374. }
  1375. // 3. 预检查图像可访问性(可选)
  1376. // await validateImageUrls(imageUrls);
  1377. // 4. 设置图像栈,包含超时保护
  1378. await safeSetStack(viewport, imageUrls, imageIndex);
  1379. // 5. 图像加载完成后,自动应用保持宽高比的适应
  1380. fitImageWithAspectRatio(viewportId);
  1381. console.log(`重新加载图像----结束`);
  1382. } catch (error) {
  1383. // 不直接抛出错误,而是保存到状态中
  1384. // 在下次渲染时抛出,这样 Error Boundary 才能捕获
  1385. const enhancedError = enhanceError(error, { imageUrls, imageIndex, viewportId });
  1386. setRenderError(enhancedError);
  1387. return; // 不再继续执行后续代码
  1388. // 根据错误严重程度决定处理策略
  1389. if (isCriticalError(enhancedError)) {
  1390. // 严重错误:保存到状态,让 Error Boundary 处理
  1391. console.error('🚨 严重图像加载错误,将在渲染时抛出给 Error Boundary:', enhancedError);
  1392. setRenderError(enhancedError);
  1393. return; // 不再继续执行后续代码
  1394. } else {
  1395. // 非严重错误:仅记录日志,继续运行
  1396. console.warn('⚠️ 图像加载警告:', enhancedError);
  1397. // 可以在这里尝试降级策略,比如显示占位符
  1398. }
  1399. }
  1400. viewport.render();
  1401. };
  1402. setup();
  1403. }, [elementRef, imageIndex, viewportId, renderingEngineId, imageUrls[0]]);
  1404. // 监听容器大小变化,自动保持宽高比
  1405. useEffect(() => {
  1406. if (!elementRef.current) return;
  1407. // 创建防抖的适应函数
  1408. const debouncedFit = debounce(() => {
  1409. // console.log(`容器大小变化了---------`)
  1410. requestAnimationFrame(() => {
  1411. fitImageWithAspectRatio(viewportId);
  1412. });
  1413. }, 100);
  1414. // 创建ResizeObserver监听容器大小变化
  1415. const resizeObserver = new ResizeObserver(debouncedFit);
  1416. resizeObserver.observe(elementRef.current);
  1417. console.log(`[StackViewer] ResizeObserver attached to viewport: ${viewportId}`);
  1418. // 清理函数
  1419. return () => {
  1420. resizeObserver.disconnect();
  1421. console.log(`[StackViewer] ResizeObserver disconnected from viewport: ${viewportId}`);
  1422. };
  1423. }, [viewportId]);
  1424. // 在渲染期间检查错误状态,如果有错误则抛出
  1425. // 这样 Error Boundary 就能捕获到异步错误了
  1426. if (renderError) {
  1427. throw renderError;
  1428. }
  1429. return (
  1430. <div
  1431. id={viewportId}
  1432. ref={elementRef}
  1433. onContextMenu={(e) => e.preventDefault()}
  1434. style={{
  1435. width: '100%', height: '100%', backgroundColor: '#000',
  1436. border: selected
  1437. ? '2px solid blue'
  1438. : '1px solid gray',
  1439. }}
  1440. />
  1441. );
  1442. };
  1443. /**
  1444. * 带错误边界的 StackViewer 包装组件
  1445. * 自动处理图像加载错误,提供优雅的错误恢复机制
  1446. */
  1447. export const StackViewerWithErrorBoundary = ({
  1448. imageIndex = 0,
  1449. imageUrls = [],
  1450. viewportId,
  1451. renderingEngineId,
  1452. selected,
  1453. maxRetries = 3,
  1454. onError
  1455. }: {
  1456. imageIndex?: number;
  1457. imageUrls?: string[];
  1458. viewportId: string;
  1459. renderingEngineId: string;
  1460. selected?: boolean;
  1461. maxRetries?: number;
  1462. onError?: (error: Error, errorInfo: any) => void;
  1463. }) => {
  1464. return (
  1465. <ImageViewerErrorBoundary
  1466. maxRetries={maxRetries}
  1467. onError={onError}
  1468. >
  1469. <StackViewer
  1470. imageIndex={imageIndex}
  1471. imageUrls={imageUrls}
  1472. viewportId={viewportId}
  1473. renderingEngineId={renderingEngineId}
  1474. selected={selected}
  1475. />
  1476. </ImageViewerErrorBoundary>
  1477. );
  1478. };
  1479. export default StackViewer;