stack.image.viewer.tsx 34 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057
  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 { boolean } from 'zod';
  16. const {
  17. MagnifyTool,
  18. PanTool,
  19. WindowLevelTool,
  20. StackScrollTool,
  21. ZoomTool,
  22. LabelTool,
  23. LengthTool,
  24. AngleTool,//角度测量工具
  25. ToolGroupManager,
  26. Enums: csToolsEnums,
  27. PlanarRotateTool,
  28. } = cornerstoneTools;
  29. const { MouseBindings } = csToolsEnums;
  30. // let toolGroup: cornerstoneTools.Types.IToolGroup;
  31. // let currentViewportId: string;
  32. function getToolgroupByViewportId(currentViewportId: string) {
  33. const toolGroup = ToolGroupManager.getToolGroup(
  34. `STACK_TOOL_GROUP_ID_${currentViewportId}`
  35. );
  36. if (!toolGroup) {
  37. console.log('toolGroup not found');
  38. throw new Error('Tool group not found');
  39. }
  40. return toolGroup;
  41. }
  42. function overlayRedRectangle(currentViewportId: string) {
  43. const viewport = cornerstone.getEnabledElementByViewportId(currentViewportId)
  44. .viewport as cornerstone.StackViewport;
  45. const viewportElement = viewport.element;
  46. const annotations = cornerstoneTools.annotation.state.getAnnotations(
  47. 'LinearSplineROI',
  48. viewportElement
  49. );
  50. if (!annotations || annotations.length === 0) {
  51. console.log('No ROI annotations found');
  52. return;
  53. }
  54. const annotation = annotations[annotations.length - 1];
  55. const points = annotation?.data?.handles?.points;
  56. // 创建一个覆盖 Canvas
  57. const canvas = document.createElement('canvas');
  58. canvas.style.position = 'absolute';
  59. canvas.width = viewportElement.clientWidth;
  60. canvas.height = viewportElement.clientHeight;
  61. viewportElement.firstChild?.appendChild(canvas);
  62. const ctx = canvas.getContext('2d');
  63. if (!ctx) {
  64. throw new Error('Failed to get 2D context from canvas');
  65. }
  66. ctx.fillStyle = 'rgba(0, 0, 0, 1)';
  67. // 将 ROI 坐标转换为 Canvas 坐标
  68. if (!points) {
  69. console.log('No points found in handles');
  70. return;
  71. }
  72. // Convert all points to canvas coordinates
  73. const z = viewport.getCurrentImageIdIndex() || 0;
  74. const canvasPoints = points.map((point: Types.Point2 | Types.Point3) => {
  75. const point3D: Types.Point3 =
  76. point.length === 2 ? [point[0], point[1], z] : point;
  77. return viewport.worldToCanvas(point3D);
  78. });
  79. // Log for debugging
  80. console.log('Canvas Points:', canvasPoints);
  81. // Draw the polygon
  82. ctx.beginPath();
  83. ctx.rect(0, 0, canvas.width, canvas.height); // Full canvas for evenodd rule
  84. ctx.moveTo(canvasPoints[0][0], canvasPoints[0][1]); // Start at the first point
  85. for (let i = 1; i < canvasPoints.length; i++) {
  86. ctx.lineTo(canvasPoints[i][0], canvasPoints[i][1]); // Connect to subsequent points
  87. }
  88. ctx.closePath(); // Close the polygon
  89. ctx.fill('evenodd'); // Fill with red
  90. }
  91. function registerTools(viewportId, renderingEngineId) {
  92. // 确保全局工具已注册(只会执行一次)
  93. registerGlobalTools();
  94. // 创建该 viewport 的工具组
  95. const toolGroupId = `STACK_TOOL_GROUP_ID_${viewportId}`;
  96. const toolGroupTmp = ToolGroupManager.createToolGroup(toolGroupId);
  97. if (!toolGroupTmp) {
  98. console.error(
  99. `[registerTools] Failed to create tool group for viewport: ${viewportId}`
  100. );
  101. return;
  102. }
  103. const toolGroup = toolGroupTmp;
  104. // 添加工具到工具组
  105. toolGroup.addTool(MagnifyTool.toolName);
  106. toolGroup.addTool(PanTool.toolName);
  107. toolGroup.addTool(WindowLevelTool.toolName);
  108. toolGroup.addTool(StackScrollTool.toolName);
  109. toolGroup.addTool(ZoomTool.toolName);
  110. toolGroup.addTool(LabelTool.toolName);
  111. toolGroup.addTool(PlanarRotateTool.toolName);
  112. toolGroup.addTool(LengthTool.toolName); // 添加线段测量工具
  113. toolGroup.addTool(AngleTool.toolName); // 添加角度测量工具
  114. toolGroup.addTool(TibialPlateauAngleTool.toolName); // 添加胫骨平台夹角测量工具
  115. toolGroup.addTool(DARAMeasurementTool.toolName); // 添加髋臼水平角测量工具
  116. toolGroup.addTool(HipDIMeasurementTool.toolName); // 添加髋关节牵引指数测量工具
  117. toolGroup.addTool(HipNHAAngleMeasurementTool.toolName); // 添加髋关节水平角测量工具
  118. toolGroup.addTool(VHSMeasurementTool.toolName); // 添加心锥比测量工具
  119. toolGroup.addTool(TPLOMeasurementTool.toolName); // 添加TPLO测量工具
  120. // 设置默认工具状态
  121. setupDefaultToolStates(toolGroup);
  122. // 添加 viewport 到工具组
  123. toolGroup.addViewport(viewportId, renderingEngineId);
  124. console.log(`[registerTools] Tools registered for viewport: ${viewportId}`);
  125. }
  126. /**
  127. * 设置默认工具状态
  128. */
  129. function setupDefaultToolStates(toolGroup: cornerstoneTools.Types.IToolGroup) {
  130. // 设置平移工具(中键)
  131. toolGroup.setToolActive(PanTool.toolName, {
  132. bindings: [
  133. {
  134. mouseButton: MouseBindings.Auxiliary, // Middle Click
  135. },
  136. ],
  137. });
  138. // 设置缩放工具(右键)
  139. toolGroup.setToolActive(ZoomTool.toolName, {
  140. bindings: [
  141. {
  142. mouseButton: MouseBindings.Secondary, // Right Click
  143. },
  144. ],
  145. });
  146. // 设置滚轮滚动工具
  147. toolGroup.setToolActive(StackScrollTool.toolName, {
  148. bindings: [
  149. {
  150. mouseButton: MouseBindings.Wheel, // Mouse Wheel
  151. },
  152. ],
  153. });
  154. // 其他工具默认为被动状态
  155. toolGroup.setToolPassive(MagnifyTool.toolName);
  156. toolGroup.setToolPassive(WindowLevelTool.toolName);
  157. toolGroup.setToolPassive(LabelTool.toolName);
  158. toolGroup.setToolPassive(PlanarRotateTool.toolName);
  159. toolGroup.setToolPassive(LengthTool.toolName);
  160. toolGroup.setToolPassive(AngleTool.toolName);
  161. toolGroup.setToolPassive(TibialPlateauAngleTool.toolName);
  162. toolGroup.setToolPassive(DARAMeasurementTool.toolName);
  163. toolGroup.setToolPassive(HipDIMeasurementTool.toolName);
  164. toolGroup.setToolPassive(HipNHAAngleMeasurementTool.toolName);
  165. toolGroup.setToolPassive(VHSMeasurementTool.toolName);
  166. toolGroup.setToolPassive(TPLOMeasurementTool.toolName);
  167. }
  168. export function addLMark(currentViewportId: string): void {
  169. // Implement the logic to add an L mark
  170. console.log('Adding L Mark viewport id : ', currentViewportId);
  171. const toolGroup = getToolgroupByViewportId(currentViewportId);
  172. // currentViewportId = viewportId;
  173. toolGroup.setToolActive(LabelTool.toolName, {
  174. bindings: [
  175. // {
  176. // mouseButton: MouseBindings.Primary, // Left Click
  177. // },
  178. ],
  179. });
  180. const position: Types.Point3 = [100, 100, 0]; // Example position
  181. const text = 'L'; // Predefined text
  182. LabelTool.hydrate(currentViewportId, position, text);
  183. toolGroup.setToolPassive(LabelTool.toolName, {
  184. removeAllBindings: true,
  185. });
  186. // const enabledElement = cornerstone.getEnabledElementByViewportId(currentViewportId);
  187. // cursors.elementCursor.resetElementCursor(elementRef.current as HTMLDivElement);
  188. }
  189. export function addRLabel(viewportId) {
  190. const toolGroup = getToolgroupByViewportId(viewportId);
  191. toolGroup.setToolActive(LabelTool.toolName, {
  192. bindings: [],
  193. });
  194. const element = document.getElementById(viewportId);
  195. const elementHeight = element ? element.getBoundingClientRect().height : 0;
  196. const position: Types.Point3 = [100, elementHeight / 2, 0]; // Example position
  197. const text = 'R'; // Predefined text
  198. LabelTool.hydrate(viewportId, position, text);
  199. toolGroup.setToolPassive(LabelTool.toolName, { removeAllBindings: true });
  200. }
  201. export function adjustBrightnessAndContrast(currentViewportId: string) {
  202. const toolGroup = getToolgroupByViewportId(currentViewportId);
  203. const planar = toolGroup.getToolInstance(WindowLevelTool.toolName); // Reset rotation angle
  204. const isActive = planar.mode === csToolsEnums.ToolModes.Active;
  205. if (isActive) {
  206. toolGroup.setToolPassive(WindowLevelTool.toolName, {
  207. removeAllBindings: true,
  208. });
  209. } else {
  210. toolGroup.setToolActive(WindowLevelTool.toolName, {
  211. bindings: [
  212. {
  213. mouseButton: MouseBindings.Primary, // Left Click
  214. },
  215. ],
  216. });
  217. }
  218. }
  219. export function fitImageSize(currentViewportId: string) {
  220. const viewport = cornerstone.getEnabledElementByViewportId(currentViewportId)
  221. .viewport as cornerstone.StackViewport;
  222. viewport.resetCamera();
  223. viewport.render();
  224. }
  225. export function deleteSelectedMark(currentViewportId: string): void {
  226. const viewport =
  227. cornerstone.getEnabledElementByViewportId(currentViewportId).viewport;
  228. const allAnnotations = cornerstoneTools.annotation.state.getAllAnnotations();
  229. for (const annotation of allAnnotations) {
  230. if (annotation.data.text === 'L' || annotation.data.text === 'R') {
  231. cornerstoneTools.annotation.state.removeAnnotation(
  232. annotation.annotationUID!
  233. );
  234. }
  235. }
  236. viewport.render();
  237. }
  238. export function addMask(currentViewportId: string): void {
  239. // Implement the logic to add a mask
  240. console.log('Adding Mask');
  241. // Add the specific logic to add a mask here
  242. cornerstoneTools.addTool(SplineROITool);
  243. const toolGroup = getToolgroupByViewportId(currentViewportId);
  244. toolGroup.addTool(SplineROITool.toolName);
  245. toolGroup.addToolInstance('LinearSplineROI', SplineROITool.toolName, {
  246. spline: {
  247. type: SplineROITool.SplineTypes.Linear,
  248. },
  249. });
  250. toolGroup.setToolActive('LinearSplineROI', {
  251. bindings: [{ mouseButton: MouseBindings.Primary }],
  252. });
  253. }
  254. export function remoteMask(currentViewportId: string): void {
  255. // 1. 获取所有 annotation
  256. const all = annotation.state.getAllAnnotations();
  257. // 2. 过滤出 LinearSplineROI 产生的
  258. const toRemove = all.filter(
  259. (a) => a.metadata?.toolName === 'LinearSplineROI'
  260. );
  261. // 3. 逐条删掉
  262. toRemove.forEach((a) => {
  263. if (a.annotationUID) {
  264. annotation.state.removeAnnotation(a.annotationUID);
  265. }
  266. });
  267. const viewport = cornerstone.getEnabledElementByViewportId(currentViewportId)
  268. .viewport as cornerstone.StackViewport;
  269. viewport.render();
  270. console.log('Deleting Digital Mask');
  271. const viewportElement = viewport.element;
  272. const firstChild = viewportElement.firstChild;
  273. if (firstChild) {
  274. const canvasElements = Array.from(firstChild.childNodes).filter(
  275. (child): child is Element =>
  276. child instanceof Element && child.tagName === 'CANVAS'
  277. );
  278. canvasElements.slice(1).forEach((canvas) => {
  279. firstChild.removeChild(canvas);
  280. });
  281. }
  282. }
  283. export function HorizontalFlip(currentViewportId: string): void {
  284. const viewport = cornerstone.getEnabledElementByViewportId(currentViewportId)
  285. .viewport as cornerstone.StackViewport;
  286. // 切换水平翻转状态
  287. const { flipHorizontal } = viewport.getCamera();
  288. viewport.setCamera({ flipHorizontal: !flipHorizontal });
  289. console.log('Flipping Image Horizontally');
  290. }
  291. export function VerticalFlip(currentViewportId: string): void {
  292. const viewport = cornerstone.getEnabledElementByViewportId(currentViewportId)
  293. .viewport as cornerstone.StackViewport;
  294. // 切换竖直翻转状态
  295. const { flipVertical } = viewport.getCamera();
  296. viewport.setCamera({ flipVertical: !flipVertical });
  297. }
  298. export function ApplyColormap(currentViewportId: string): void {
  299. const viewport = cornerstone.getEnabledElementByViewportId(currentViewportId)
  300. .viewport as cornerstone.StackViewport;
  301. // Implement the logic to apply colormap
  302. viewport.setProperties({ colormap: { name: 'hsv' } });
  303. viewport.render();
  304. console.log('Applying Colormap');
  305. }
  306. export function RotateCounterclockwise90(currentViewportId: string): void {
  307. const viewport = cornerstone.getEnabledElementByViewportId(currentViewportId)
  308. .viewport as cornerstone.StackViewport;
  309. // 获取当前相机
  310. const camera = viewport.getCamera();
  311. // 计算新的旋转角度(当前角度 + 90度)
  312. const newRotation = (camera.rotation ?? 0) + 90;
  313. // 但计算viewUp向量时,我们应该使用90度的弧度,而不是新角度的弧度!
  314. const ninetyDegreesRadians = (90 * Math.PI) / 180;
  315. // 获取当前的viewUp向量
  316. const currentViewUp = camera.viewUp || [0, 1, 0];
  317. // 计算旋转后的viewUp向量(这才是正确的相对旋转)
  318. const newViewUp: [number, number, number] = [
  319. currentViewUp[0] * Math.cos(ninetyDegreesRadians) -
  320. currentViewUp[1] * Math.sin(ninetyDegreesRadians),
  321. currentViewUp[0] * Math.sin(ninetyDegreesRadians) +
  322. currentViewUp[1] * Math.cos(ninetyDegreesRadians),
  323. 0,
  324. ];
  325. // 设置新的相机参数
  326. viewport.setCamera({
  327. ...camera,
  328. viewUp: newViewUp,
  329. rotation: newRotation % 360, // 确保角度在0-359范围内
  330. });
  331. viewport.render();
  332. console.log('Rotating Image Counterclockwise 90°');
  333. }
  334. export function RotateClockwise90(currentViewportId: string): void {
  335. const viewport = cornerstone.getEnabledElementByViewportId(currentViewportId)
  336. .viewport as cornerstone.StackViewport;
  337. const camera = viewport.getCamera();
  338. // 计算新的旋转角度(当前角度 + 90度)
  339. const newRotation = (camera.rotation ?? 0) - 90;
  340. // 但计算viewUp向量时,我们应该使用90度的弧度,而不是新角度的弧度!
  341. const ninetyDegreesRadians = (90 * Math.PI) / 180;
  342. // 获取当前的viewUp向量
  343. const currentViewUp = camera.viewUp || [0, 1, 0];
  344. // 计算旋转后的viewUp向量(这才是正确的相对旋转)
  345. const newViewUp: [number, number, number] = [
  346. currentViewUp[0] * Math.cos(ninetyDegreesRadians) -
  347. currentViewUp[1] * Math.sin(ninetyDegreesRadians),
  348. currentViewUp[0] * Math.sin(ninetyDegreesRadians) +
  349. currentViewUp[1] * Math.cos(ninetyDegreesRadians),
  350. 0,
  351. ];
  352. // 设置新的相机参数
  353. viewport.setCamera({
  354. ...camera,
  355. viewUp: newViewUp,
  356. rotation: newRotation % 360, // 确保角度在0-359范围内
  357. });
  358. viewport.render();
  359. console.log('Rotating Image Clockwise 90°');
  360. }
  361. export function ResetImage(currentViewportId: string): void {
  362. const viewport = cornerstone.getEnabledElementByViewportId(currentViewportId)
  363. .viewport as cornerstone.StackViewport;
  364. // Implement the logic to reset the image
  365. // Resets the viewport's camera
  366. viewport.resetCamera();
  367. // Resets the viewport's properties
  368. viewport.resetProperties();
  369. viewport.render();
  370. console.log('Resetting Image');
  371. }
  372. export function InvertImage(currentViewportId: string): void {
  373. const viewport = cornerstone.getEnabledElementByViewportId(currentViewportId)
  374. .viewport as cornerstone.StackViewport;
  375. // Implement the logic to invert the image
  376. const invert = !viewport.getProperties().invert;
  377. viewport.setProperties({ invert });
  378. viewport.render();
  379. console.log('Inverting Image');
  380. }
  381. export function setOriginalSize(currentViewportId: string) {
  382. const viewport = cornerstone.getEnabledElementByViewportId(currentViewportId)
  383. .viewport as cornerstone.StackViewport;
  384. // 1) 先正常 fit(或本来就是 fit 状态)
  385. viewport.resetCamera();
  386. // 2) 计算“fit → 1:1”的放大倍数
  387. const { dimensions, spacing } = viewport.getImageData();
  388. console.log(`dimensions:${dimensions}, spacing:${spacing}`);
  389. const canvas = viewport.canvas;
  390. // 水平方向 1:1 需要的倍率
  391. const cssPixelsPerDicomPx = canvas.clientWidth / (dimensions[0] * spacing[0]);
  392. // 垂直方向 1:1 需要的倍率
  393. const cssPixelsPerDicomPy =
  394. canvas.clientHeight / (dimensions[1] * spacing[1]);
  395. // 取两者最小值,保证整张图不会被裁剪
  396. const zoomFactor = Math.min(cssPixelsPerDicomPx, cssPixelsPerDicomPy);
  397. console.log(`zoomFactor:${zoomFactor}`);
  398. console.log(
  399. `canvas.clientWidth:${canvas.clientWidth}, dimensions[0]:${dimensions[0]}, spacing[0]:${spacing[0]}`
  400. );
  401. console.log(
  402. `canvas.clientHeight:${canvas.clientHeight}, dimensions[1]:${dimensions[1]}, spacing[1]:${spacing[1]}`
  403. );
  404. // 3) 直接放大
  405. const zoom = viewport.getZoom();
  406. viewport.setZoom((zoom * 1) / zoomFactor);
  407. viewport.render();
  408. }
  409. export function activateMagnifier(currentViewportId: string) {
  410. console.log('Activating Magnifier');
  411. const toolGroup = getToolgroupByViewportId(currentViewportId);
  412. const isActive =
  413. toolGroup.getActivePrimaryMouseButtonTool() === MagnifyTool.toolName;
  414. if (isActive) {
  415. toolGroup.setToolPassive(MagnifyTool.toolName, {
  416. removeAllBindings: true,
  417. });
  418. } else {
  419. toolGroup.setToolActive(MagnifyTool.toolName, {
  420. bindings: [
  421. {
  422. mouseButton: MouseBindings.Primary, // Left Click
  423. },
  424. ],
  425. });
  426. }
  427. }
  428. export function invertContrast(currentViewportId: string) {
  429. const viewport =
  430. cornerstone.getEnabledElementByViewportId(currentViewportId).viewport;
  431. const targetBool = !viewport.getProperties().invert;
  432. viewport.setProperties({
  433. invert: targetBool,
  434. });
  435. viewport.render();
  436. }
  437. export function rotateAnyAngle(currentViewportId: string) {
  438. const toolGroup = getToolgroupByViewportId(currentViewportId);
  439. const planar = toolGroup.getToolInstance(PlanarRotateTool.toolName); // Reset rotation angle
  440. const isActive = planar.mode === csToolsEnums.ToolModes.Active;
  441. console.log(
  442. `PlanarRotateTool is currently ${isActive ? 'active' : 'inactive'}`
  443. );
  444. if (isActive) {
  445. toolGroup.setToolPassive(PlanarRotateTool.toolName, {
  446. removeAllBindings: true,
  447. });
  448. } else {
  449. toolGroup.setToolActive(PlanarRotateTool.toolName, {
  450. bindings: [
  451. {
  452. mouseButton: MouseBindings.Primary, // Left Click
  453. },
  454. ],
  455. });
  456. }
  457. console.log('Rotating Image by Any Angle');
  458. }
  459. // ==================== 线段测量相关函数 ====================
  460. /**
  461. * 激活线段测量工具
  462. */
  463. export function activateLengthMeasurement(viewportId: string): boolean {
  464. console.log(
  465. `[activateLengthMeasurement] Activating length measurement for viewport: ${viewportId}`
  466. );
  467. return MeasurementToolManager.activateLengthTool(viewportId);
  468. }
  469. /**
  470. * 停用线段测量工具
  471. */
  472. export function deactivateLengthMeasurement(viewportId: string): boolean {
  473. console.log(
  474. `[deactivateLengthMeasurement] Deactivating length measurement for viewport: ${viewportId}`
  475. );
  476. return MeasurementToolManager.deactivateLengthTool(viewportId);
  477. }
  478. /**
  479. * 切换线段测量工具状态
  480. */
  481. export function toggleLengthMeasurement(viewportId: string): boolean {
  482. console.log(
  483. `[toggleLengthMeasurement] Toggling length measurement for viewport: ${viewportId}`
  484. );
  485. return MeasurementToolManager.toggleLengthTool(viewportId);
  486. }
  487. /**
  488. * 清除线段测量标注
  489. */
  490. export function clearLengthMeasurements(viewportId: string): boolean {
  491. console.log(
  492. `[clearLengthMeasurements] Clearing length measurements for viewport: ${viewportId}`
  493. );
  494. return MeasurementToolManager.clearLengthMeasurements(viewportId);
  495. }
  496. /**
  497. * 获取线段测量结果
  498. */
  499. // eslint-disable-next-line
  500. export function getLengthMeasurements(viewportId: string): any[] {
  501. console.log(
  502. `[getLengthMeasurements] Getting length measurements for viewport: ${viewportId}`
  503. );
  504. return MeasurementToolManager.getLengthMeasurements(viewportId);
  505. }
  506. /**
  507. * 检查线段测量工具是否激活
  508. */
  509. export function isLengthMeasurementActive(viewportId: string): boolean {
  510. return MeasurementToolManager.isLengthToolActive(viewportId);
  511. }
  512. // ==================== 角度测量相关函数 ====================
  513. /**
  514. * 激活角度测量工具
  515. */
  516. export function activateAngleMeasurement(viewportId: string): boolean {
  517. console.log(
  518. `[activateAngleMeasurement] Activating angle measurement for viewport: ${viewportId}`
  519. );
  520. return MeasurementToolManager.activateAngleTool(viewportId);
  521. }
  522. /**
  523. * 停用角度测量工具
  524. */
  525. export function deactivateAngleMeasurement(viewportId: string): boolean {
  526. console.log(
  527. `[deactivateAngleMeasurement] Deactivating angle measurement for viewport: ${viewportId}`
  528. );
  529. return MeasurementToolManager.deactivateAngleTool(viewportId);
  530. }
  531. /**
  532. * 切换角度测量工具状态
  533. */
  534. export function toggleAngleMeasurement(viewportId: string): boolean {
  535. console.log(
  536. `[toggleAngleMeasurement] Toggling angle measurement for viewport: ${viewportId}`
  537. );
  538. return MeasurementToolManager.toggleAngleTool(viewportId);
  539. }
  540. /**
  541. * 清除角度测量标注
  542. */
  543. export function clearAngleMeasurements(viewportId: string): boolean {
  544. console.log(
  545. `[clearAngleMeasurements] Clearing angle measurements for viewport: ${viewportId}`
  546. );
  547. return MeasurementToolManager.clearAngleMeasurements(viewportId);
  548. }
  549. /**
  550. * 获取角度测量结果
  551. */
  552. // eslint-disable-next-line
  553. export function getAngleMeasurements(viewportId: string): any[] {
  554. console.log(
  555. `[getAngleMeasurements] Getting angle measurements for viewport: ${viewportId}`
  556. );
  557. return MeasurementToolManager.getAngleMeasurements(viewportId);
  558. }
  559. /**
  560. * 检查角度测量工具是否激活
  561. */
  562. export function isAngleMeasurementActive(viewportId: string): boolean {
  563. return MeasurementToolManager.isAngleToolActive(viewportId);
  564. }
  565. // ==================== 胫骨平台夹角测量相关函数 ====================
  566. /**
  567. * 激活胫骨平台夹角测量工具
  568. */
  569. export function activateTibialPlateauAngleMeasurement(viewportId: string): boolean {
  570. console.log(
  571. `[activateTibialPlateauAngleMeasurement] Activating TPA measurement for viewport: ${viewportId}`
  572. );
  573. return MeasurementToolManager.activateTibialPlateauAngleTool(viewportId);
  574. }
  575. /**
  576. * 停用胫骨平台夹角测量工具
  577. */
  578. export function deactivateTibialPlateauAngleMeasurement(viewportId: string): boolean {
  579. console.log(
  580. `[deactivateTibialPlateauAngleMeasurement] Deactivating TPA measurement for viewport: ${viewportId}`
  581. );
  582. return MeasurementToolManager.deactivateTibialPlateauAngleTool(viewportId);
  583. }
  584. /**
  585. * 切换胫骨平台夹角测量工具状态
  586. */
  587. export function toggleTibialPlateauAngleMeasurement(viewportId: string): boolean {
  588. console.log(
  589. `[toggleTibialPlateauAngleMeasurement] Toggling TPA measurement for viewport: ${viewportId}`
  590. );
  591. return MeasurementToolManager.toggleTibialPlateauAngleTool(viewportId);
  592. }
  593. /**
  594. * 清除胫骨平台夹角测量标注
  595. */
  596. export function clearTibialPlateauAngleMeasurements(viewportId: string): boolean {
  597. console.log(
  598. `[clearTibialPlateauAngleMeasurements] Clearing TPA measurements for viewport: ${viewportId}`
  599. );
  600. return MeasurementToolManager.clearTibialPlateauAngleMeasurements(viewportId);
  601. }
  602. /**
  603. * 获取胫骨平台夹角测量结果
  604. */
  605. // eslint-disable-next-line
  606. export function getTibialPlateauAngleMeasurements(viewportId: string): any[] {
  607. console.log(
  608. `[getTibialPlateauAngleMeasurements] Getting TPA measurements for viewport: ${viewportId}`
  609. );
  610. return MeasurementToolManager.getTibialPlateauAngleMeasurements(viewportId);
  611. }
  612. /**
  613. * 检查胫骨平台夹角测量工具是否激活
  614. */
  615. export function isTibialPlateauAngleMeasurementActive(viewportId: string): boolean {
  616. return MeasurementToolManager.isTibialPlateauAngleToolActive(viewportId);
  617. }
  618. // ==================== 髋臼水平角测量相关函数 ====================
  619. /**
  620. * 激活髋臼水平角测量工具
  621. */
  622. export function activateDARAMeasurement(viewportId: string): boolean {
  623. console.log(
  624. `[activateDARAMeasurement] Activating DARA measurement for viewport: ${viewportId}`
  625. );
  626. return MeasurementToolManager.activateDARAMeasurementTool(viewportId);
  627. }
  628. /**
  629. * 停用髋臼水平角测量工具
  630. */
  631. export function deactivateDARAMeasurement(viewportId: string): boolean {
  632. console.log(
  633. `[deactivateDARAMeasurement] Deactivating DARA measurement for viewport: ${viewportId}`
  634. );
  635. return MeasurementToolManager.deactivateDARAMeasurementTool(viewportId);
  636. }
  637. /**
  638. * 切换髋臼水平角测量工具状态
  639. */
  640. export function toggleDARAMeasurement(viewportId: string): boolean {
  641. console.log(
  642. `[toggleDARAMeasurement] Toggling DARA measurement for viewport: ${viewportId}`
  643. );
  644. return MeasurementToolManager.toggleDARAMeasurementTool(viewportId);
  645. }
  646. /**
  647. * 清除髋臼水平角测量标注
  648. */
  649. export function clearDARAMeasurements(viewportId: string): boolean {
  650. console.log(
  651. `[clearDARAMeasurements] Clearing DARA measurements for viewport: ${viewportId}`
  652. );
  653. return MeasurementToolManager.clearDARAMeasurements(viewportId);
  654. }
  655. /**
  656. * 获取髋臼水平角测量结果
  657. */
  658. // eslint-disable-next-line
  659. export function getDARAMeasurements(viewportId: string): any[] {
  660. console.log(
  661. `[getDARAMeasurements] Getting DARA measurements for viewport: ${viewportId}`
  662. );
  663. return MeasurementToolManager.getDARAMeasurements(viewportId);
  664. }
  665. /**
  666. * 检查髋臼水平角测量工具是否激活
  667. */
  668. export function isDARAMeasurementActive(viewportId: string): boolean {
  669. return MeasurementToolManager.isDARAMeasurementToolActive(viewportId);
  670. }
  671. // ==================== 髋关节牵引指数测量相关函数 ====================
  672. /**
  673. * 激活髋关节牵引指数测量工具
  674. */
  675. export function activateHipDIMeasurement(viewportId: string): boolean {
  676. console.log(
  677. `[activateHipDIMeasurement] Activating HipDI measurement for viewport: ${viewportId}`
  678. );
  679. return MeasurementToolManager.activateHipDIMeasurementTool(viewportId);
  680. }
  681. /**
  682. * 停用髋关节牵引指数测量工具
  683. */
  684. export function deactivateHipDIMeasurement(viewportId: string): boolean {
  685. console.log(
  686. `[deactivateHipDIMeasurement] Deactivating HipDI measurement for viewport: ${viewportId}`
  687. );
  688. return MeasurementToolManager.deactivateHipDIMeasurementTool(viewportId);
  689. }
  690. /**
  691. * 切换髋关节牵引指数测量工具状态
  692. */
  693. export function toggleHipDIMeasurement(viewportId: string): boolean {
  694. console.log(
  695. `[toggleHipDIMeasurement] Toggling HipDI measurement for viewport: ${viewportId}`
  696. );
  697. return MeasurementToolManager.toggleHipDIMeasurementTool(viewportId);
  698. }
  699. /**
  700. * 清除髋关节牵引指数测量标注
  701. */
  702. export function clearHipDIMeasurements(viewportId: string): boolean {
  703. console.log(
  704. `[clearHipDIMeasurements] Clearing HipDI measurements for viewport: ${viewportId}`
  705. );
  706. return MeasurementToolManager.clearHipDIMeasurements(viewportId);
  707. }
  708. /**
  709. * 获取髋关节牵引指数测量结果
  710. */
  711. // eslint-disable-next-line
  712. export function getHipDIMeasurements(viewportId: string): any[] {
  713. console.log(
  714. `[getHipDIMeasurements] Getting HipDI measurements for viewport: ${viewportId}`
  715. );
  716. return MeasurementToolManager.getHipDIMeasurements(viewportId);
  717. }
  718. /**
  719. * 检查髋关节牵引指数测量工具是否激活
  720. */
  721. export function isHipDIMeasurementActive(viewportId: string): boolean {
  722. return MeasurementToolManager.isHipDIMeasurementToolActive(viewportId);
  723. }
  724. // ==================== 髋关节水平角测量相关函数 ====================
  725. /**
  726. * 激活髋关节水平角测量工具
  727. */
  728. export function activateHipNHAAngleMeasurement(viewportId: string): boolean {
  729. console.log(
  730. `[activateHipNHAAngleMeasurement] Activating HipNHA measurement for viewport: ${viewportId}`
  731. );
  732. return MeasurementToolManager.activateHipNHAAngleMeasurementTool(viewportId);
  733. }
  734. /**
  735. * 停用髋关节水平角测量工具
  736. */
  737. export function deactivateHipNHAAngleMeasurement(viewportId: string): boolean {
  738. console.log(
  739. `[deactivateHipNHAAngleMeasurement] Deactivating HipNHA measurement for viewport: ${viewportId}`
  740. );
  741. return MeasurementToolManager.deactivateHipNHAAngleMeasurementTool(viewportId);
  742. }
  743. /**
  744. * 切换髋关节水平角测量工具状态
  745. */
  746. export function toggleHipNHAAngleMeasurement(viewportId: string): boolean {
  747. console.log(
  748. `[toggleHipNHAAngleMeasurement] Toggling HipNHA measurement for viewport: ${viewportId}`
  749. );
  750. return MeasurementToolManager.toggleHipNHAAngleMeasurementTool(viewportId);
  751. }
  752. /**
  753. * 清除髋关节水平角测量标注
  754. */
  755. export function clearHipNHAAngleMeasurements(viewportId: string): boolean {
  756. console.log(
  757. `[clearHipNHAAngleMeasurements] Clearing HipNHA measurements for viewport: ${viewportId}`
  758. );
  759. return MeasurementToolManager.clearHipNHAAngleMeasurements(viewportId);
  760. }
  761. /**
  762. * 获取髋关节水平角测量结果
  763. */
  764. // eslint-disable-next-line
  765. export function getHipNHAAngleMeasurements(viewportId: string): any[] {
  766. console.log(
  767. `[getHipNHAAngleMeasurements] Getting HipNHA measurements for viewport: ${viewportId}`
  768. );
  769. return MeasurementToolManager.getHipNHAAngleMeasurements(viewportId);
  770. }
  771. /**
  772. * 检查髋关节水平角测量工具是否激活
  773. */
  774. export function isHipNHAAngleMeasurementActive(viewportId: string): boolean {
  775. return MeasurementToolManager.isHipNHAAngleMeasurementToolActive(viewportId);
  776. }
  777. // ==================== TPLO测量相关函数 ====================
  778. /**
  779. * 激活TPLO测量工具
  780. */
  781. export function activateTPLOMeasurement(viewportId: string): boolean {
  782. console.log(
  783. `[activateTPLOMeasurement] Activating TPLO measurement for viewport: ${viewportId}`
  784. );
  785. return MeasurementToolManager.activateTPLOMeasurementTool(viewportId);
  786. }
  787. /**
  788. * 停用TPLO测量工具
  789. */
  790. export function deactivateTPLOMeasurement(viewportId: string): boolean {
  791. console.log(
  792. `[deactivateTPLOMeasurement] Deactivating TPLO measurement for viewport: ${viewportId}`
  793. );
  794. return MeasurementToolManager.deactivateTPLOMeasurementTool(viewportId);
  795. }
  796. /**
  797. * 切换TPLO测量工具状态
  798. */
  799. export function toggleTPLOMeasurement(viewportId: string): boolean {
  800. console.log(
  801. `[toggleTPLOMeasurement] Toggling TPLO measurement for viewport: ${viewportId}`
  802. );
  803. return MeasurementToolManager.toggleTPLOMeasurementTool(viewportId);
  804. }
  805. /**
  806. * 清除TPLO测量标注
  807. */
  808. export function clearTPLOMeasurements(viewportId: string): boolean {
  809. console.log(
  810. `[clearTPLOMeasurements] Clearing TPLO measurements for viewport: ${viewportId}`
  811. );
  812. return MeasurementToolManager.clearTPLOMeasurements(viewportId);
  813. }
  814. /**
  815. * 获取TPLO测量结果
  816. */
  817. // eslint-disable-next-line
  818. export function getTPLOMeasurements(viewportId: string): any[] {
  819. console.log(
  820. `[getTPLOMeasurements] Getting TPLO measurements for viewport: ${viewportId}`
  821. );
  822. return MeasurementToolManager.getTPLOMeasurements(viewportId);
  823. }
  824. /**
  825. * 检查TPLO测量工具是否激活
  826. */
  827. export function isTPLOMeasurementActive(viewportId: string): boolean {
  828. return MeasurementToolManager.isTPLOMeasurementToolActive(viewportId);
  829. }
  830. const StackViewer = ({
  831. imageIndex = 0,
  832. imageUrls = [],
  833. viewportId,
  834. renderingEngineId,
  835. selected
  836. }: {
  837. imageIndex?: number;
  838. imageUrls?: string[];
  839. viewportId: string;
  840. renderingEngineId: string;
  841. selected?: boolean;
  842. }) => {
  843. const elementRef = useRef<HTMLDivElement>(null);
  844. // const action = useSelector((state: RootState) => state.functionArea.action);
  845. // const dispatch = useDispatch();
  846. useEffect(() => {
  847. const setup = async () => {
  848. // // 初始化 Cornerstone
  849. // cornerstone.init();
  850. // cornerstoneTools.init();
  851. // const state = store.getState();
  852. // console.log(`当前系统模式:${state.systemMode.mode}`);
  853. // const token =
  854. // state.systemMode.mode === SystemMode.Emergency
  855. // ? state.product.guest
  856. // : state.userInfo.token;
  857. // console.log(`token stack.image.viewer: ${token}`);
  858. // cornerstoneDICOMImageLoader.init({
  859. // maxWebWorkers: navigator.hardwareConcurrency || 1,
  860. // errorInterceptor: (error) => {
  861. // if (error.status === 401) {
  862. // console.error('Authentication failed. Please refresh the token.');
  863. // }
  864. // console.error(`请求dcm文件出错:${error}`);
  865. // },
  866. // beforeSend: (xhr, imageId, defaultHeaders) => {
  867. // return {
  868. // ...defaultHeaders,
  869. // Authorization: `Bearer ${token}`,
  870. // Language: 'en',
  871. // Product: 'DROS',
  872. // Source: 'Electron',
  873. // };
  874. // },
  875. // });
  876. // const currentViewportId = viewportId;
  877. eventTarget.addEventListener(
  878. cornerstoneTools.Enums.Events.ANNOTATION_COMPLETED,
  879. (evt) => {
  880. const { annotation } = evt.detail;
  881. console.log('Annotation completed event:', annotation);
  882. if (annotation.metadata.toolName === 'LinearSplineROI') {
  883. console.log('SplineROITool annotation completed:', annotation);
  884. overlayRedRectangle(viewportId);
  885. //取消工具激活状态
  886. const toolGroup = ToolGroupManager.getToolGroup(
  887. 'STACK_TOOL_GROUP_ID'
  888. );
  889. if (!toolGroup) {
  890. console.log('toolGroup not found');
  891. }
  892. toolGroup?.setToolPassive('LinearSplineROI', {
  893. removeAllBindings: true,
  894. });
  895. }
  896. }
  897. );
  898. const viewportInput: cornerstone.Types.PublicViewportInput = {
  899. viewportId,
  900. element: elementRef.current!,
  901. type: cornerstone.Enums.ViewportType.STACK,
  902. };
  903. const renderingEngine = cornerstone.getRenderingEngine(renderingEngineId);
  904. if (!renderingEngine) {
  905. console.error(
  906. `[stack.image.viewer] No rendering engine with id ${renderingEngineId} found`
  907. );
  908. return;
  909. }
  910. // Enable the element for use with Cornerstone
  911. renderingEngine.enableElement(viewportInput);
  912. registerTools(viewportId, renderingEngine.id);
  913. // Get the stack viewport that was created
  914. const viewport = renderingEngine.getViewport(
  915. viewportId
  916. ) as cornerstone.Types.IStackViewport;
  917. // 给定一个dcm文件路径,加载并显示出来
  918. try {
  919. await viewport.setStack(imageUrls, imageIndex);
  920. } catch (error) {
  921. if (error instanceof Error) {
  922. console.error(
  923. '[stack.image.viewer] Error setting image stack:',
  924. error
  925. );
  926. } else {
  927. console.error('[stack.image.viewer] Unknown error:', error);
  928. }
  929. }
  930. viewport.render();
  931. };
  932. setup();
  933. }, [elementRef, imageIndex, viewportId, renderingEngineId]);
  934. return (
  935. <div
  936. ref={elementRef}
  937. onContextMenu={(e) => e.preventDefault()}
  938. style={{
  939. width: '100%', height: '100%', backgroundColor: '#000',
  940. border: selected
  941. ? '2px solid blue'
  942. : '1px solid gray',
  943. }}
  944. />
  945. );
  946. };
  947. export default StackViewer;