stack.image.viewer.tsx 73 KB

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