stack.image.viewer.tsx 72 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055
  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. // 计算旋转后的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. }, [elementRef, imageIndex, viewportId, renderingEngineId, imageUrls[0]]);
  1696. // 监听容器大小变化,自动保持宽高比
  1697. useEffect(() => {
  1698. if (!elementRef.current) return;
  1699. // 创建防抖的适应函数
  1700. const debouncedFit = debounce(() => {
  1701. // console.log(`容器大小变化了---------`)
  1702. requestAnimationFrame(() => {
  1703. fitImageWithAspectRatio(viewportId);
  1704. });
  1705. }, 100);
  1706. // 创建ResizeObserver监听容器大小变化
  1707. const resizeObserver = new ResizeObserver(debouncedFit);
  1708. resizeObserver.observe(elementRef.current);
  1709. console.log(`[StackViewer] ResizeObserver attached to viewport: ${viewportId}`);
  1710. // 清理函数
  1711. return () => {
  1712. resizeObserver.disconnect();
  1713. console.log(`[StackViewer] ResizeObserver disconnected from viewport: ${viewportId}`);
  1714. };
  1715. }, [viewportId]);
  1716. // 在渲染期间检查错误状态,如果有错误则抛出
  1717. // 这样 Error Boundary 就能捕获到异步错误了
  1718. if (renderError) {
  1719. throw renderError;
  1720. }
  1721. return (
  1722. <div
  1723. id={viewportId}
  1724. ref={elementRef}
  1725. onContextMenu={(e) => e.preventDefault()}
  1726. style={{
  1727. width: '100%',
  1728. height: '100%',
  1729. backgroundColor: '#000',
  1730. border: selected ? '2px solid blue' : '1px solid gray',
  1731. position: 'relative', // 让浮动控制条能够正确定位
  1732. }}
  1733. >
  1734. {/* 播放控制器(逻辑组件,不渲染UI) */}
  1735. <PlaybackController
  1736. viewportId={viewportId}
  1737. renderingEngineId={renderingEngineId}
  1738. enabled={true}
  1739. />
  1740. {/* 浮动播放控制条(UI组件) */}
  1741. <FloatingPlaybackControls
  1742. viewportId={viewportId}
  1743. position="bottom"
  1744. autoHide={true}
  1745. showAdvancedControls={true}
  1746. />
  1747. </div>
  1748. );
  1749. };
  1750. /**
  1751. * 带错误边界的 StackViewer 包装组件
  1752. * 自动处理图像加载错误,提供优雅的错误恢复机制
  1753. */
  1754. export const StackViewerWithErrorBoundary = ({
  1755. imageIndex = 0,
  1756. imageUrls = [],
  1757. viewportId,
  1758. renderingEngineId,
  1759. selected,
  1760. maxRetries = 3,
  1761. onError
  1762. }: {
  1763. imageIndex?: number;
  1764. imageUrls?: string[];
  1765. viewportId: string;
  1766. renderingEngineId: string;
  1767. selected?: boolean;
  1768. maxRetries?: number;
  1769. onError?: (error: Error, errorInfo: any) => void;
  1770. }) => {
  1771. return (
  1772. <ImageViewerErrorBoundary
  1773. maxRetries={maxRetries}
  1774. onError={onError}
  1775. >
  1776. <StackViewer
  1777. imageIndex={imageIndex}
  1778. imageUrls={imageUrls}
  1779. viewportId={viewportId}
  1780. renderingEngineId={renderingEngineId}
  1781. selected={selected}
  1782. />
  1783. </ImageViewerErrorBoundary>
  1784. );
  1785. };
  1786. export default StackViewer;