stack.image.viewer.tsx 75 KB

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