MidlineMeasurementTool.ts 30 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133
  1. import { Point3, Point2, fromPoint3ToPoint2 } from './mathUtils';
  2. import {
  3. utilities as csUtils,
  4. Types as CoreTypes,
  5. getEnabledElement,
  6. } from '@cornerstonejs/core';
  7. import {
  8. AnnotationTool,
  9. utilities,
  10. Types,
  11. annotation,
  12. drawing,
  13. } from '@cornerstonejs/tools';
  14. const {
  15. drawHandles,
  16. drawLinkedTextBox,
  17. drawLine: drawLineSvg,
  18. } = drawing;
  19. import {
  20. calculateMidpoint,
  21. vectorSubtract,
  22. vectorAdd,
  23. calculateDistance,
  24. } from './mathUtils';
  25. import {
  26. PublicToolProps,
  27. ToolProps,
  28. EventTypes,
  29. SVGDrawingHelper,
  30. } from '@cornerstonejs/tools/dist/esm/types';
  31. // 注解数据接口
  32. interface MidlineMeasurementAnnotation extends Types.Annotation {
  33. data: {
  34. handles: {
  35. points: CoreTypes.Point3[]; // [A, B, C, D] - 4个点
  36. activeHandleIndex: number | null;
  37. };
  38. cachedStats?: {
  39. [targetId: string]: {
  40. midPointM1: CoreTypes.Point3; // AB中点
  41. midPointM2: CoreTypes.Point3; // CD中点
  42. midlineLength: number; // 中线长度
  43. };
  44. };
  45. };
  46. // 选中状态
  47. isSelected?: boolean;
  48. }
  49. // 类型断言函数
  50. function isMidlineMeasurementAnnotation(annotation: Types.Annotation): annotation is MidlineMeasurementAnnotation {
  51. return annotation.metadata?.toolName === 'MidlineMeasurementTool';
  52. }
  53. // 导出的注解数据接口
  54. export interface ExportedMidlineData {
  55. points: CoreTypes.Point3[]; // [A, B, C, D] 4个点的世界坐标
  56. midline: {
  57. midPointM1: CoreTypes.Point3; // AB中点
  58. midPointM2: CoreTypes.Point3; // CD中点
  59. length: number; // 中线长度
  60. };
  61. metadata: {
  62. viewPlaneNormal: CoreTypes.Point3;
  63. viewUp: CoreTypes.Point3;
  64. FrameOfReferenceUID: string;
  65. referencedImageId: string;
  66. };
  67. }
  68. export default class MidlineMeasurementTool extends AnnotationTool {
  69. static toolName = 'MidlineMeasurementTool';
  70. editData: {
  71. annotation: Types.Annotation;
  72. viewportIdsToRender: string[];
  73. handleIndex?: number;
  74. newAnnotation?: boolean;
  75. hasMoved?: boolean;
  76. movingWholeTool?: boolean; // 是否移动整个工具
  77. wholeToolOffset?: CoreTypes.Point2; // 工具移动偏移量
  78. } | null = null;
  79. isDrawing: boolean = false;
  80. /**
  81. * 创建一个预设的注解,带有4个默认点
  82. * @param element HTML元素
  83. * @param viewport Viewport实例
  84. */
  85. static createDefaultAnnotation(
  86. element: HTMLDivElement,
  87. viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport
  88. ): MidlineMeasurementAnnotation {
  89. const enabledElement = getEnabledElement(element);
  90. if (!enabledElement) {
  91. throw new Error('Element is not enabled');
  92. }
  93. // 获取viewport的尺寸
  94. const canvas = viewport.canvas;
  95. const { width, height } = canvas;
  96. const centerX = width / 2;
  97. const centerY = height / 2;
  98. // 创建4个canvas坐标点
  99. // 线段AB: 水平线,在中心上方
  100. const lineAY = centerY - 100;
  101. const lineAStartX = centerX - 80;
  102. const lineAEndX = centerX + 80;
  103. // 线段CD: 水平线,在中心下方
  104. const lineCY = centerY + 100;
  105. const lineCStartX = centerX - 60;
  106. const lineCEndX = centerX + 60;
  107. const canvasPoints: CoreTypes.Point2[] = [
  108. // 线段AB: 点A, 点B
  109. [lineAStartX, lineAY],
  110. [lineAEndX, lineAY],
  111. // 线段CD: 点C, 点D
  112. [lineCStartX, lineCY],
  113. [lineCEndX, lineCY],
  114. ];
  115. // 转换为world坐标
  116. const worldPoints = canvasPoints.map((canvasPoint) =>
  117. viewport.canvasToWorld(canvasPoint)
  118. );
  119. const camera = viewport.getCamera();
  120. const { viewPlaneNormal, viewUp } = camera;
  121. if (viewPlaneNormal === undefined) {
  122. throw new Error('viewPlaneNormal is undefined');
  123. }
  124. if (viewUp === undefined) {
  125. throw new Error('viewUp is undefined');
  126. }
  127. const annotationData = {
  128. invalidated: true,
  129. highlighted: false,
  130. metadata: {
  131. viewPlaneNormal: [...viewPlaneNormal] as CoreTypes.Point3,
  132. viewUp: [...viewUp] as CoreTypes.Point3,
  133. FrameOfReferenceUID: viewport.getFrameOfReferenceUID(),
  134. referencedImageId: viewport.getCurrentImageId?.() || '',
  135. toolName: MidlineMeasurementTool.toolName,
  136. },
  137. data: {
  138. label: '',
  139. handles: {
  140. points: worldPoints,
  141. activeHandleIndex: null,
  142. },
  143. cachedStats: {},
  144. },
  145. } as MidlineMeasurementAnnotation;
  146. return annotationData;
  147. }
  148. /**
  149. * 导出注解数据
  150. * @param annotation 要导出的注解
  151. * @returns 导出的数据对象
  152. */
  153. static exportAnnotationData(
  154. annotation: MidlineMeasurementAnnotation,
  155. viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport
  156. ): ExportedMidlineData | null {
  157. const targetId = `imageId:${viewport.getCurrentImageId?.() || ''}`;
  158. const cachedStats = annotation.data.cachedStats?.[targetId];
  159. if (!cachedStats) {
  160. return null;
  161. }
  162. return {
  163. points: [...annotation.data.handles.points],
  164. midline: {
  165. midPointM1: cachedStats.midPointM1,
  166. midPointM2: cachedStats.midPointM2,
  167. length: cachedStats.midlineLength,
  168. },
  169. metadata: {
  170. viewPlaneNormal: annotation.metadata?.viewPlaneNormal || [0, 0, 1],
  171. viewUp: annotation.metadata?.viewUp || [0, 1, 0],
  172. FrameOfReferenceUID: annotation.metadata?.FrameOfReferenceUID || '',
  173. referencedImageId: annotation.metadata?.referencedImageId || '',
  174. },
  175. };
  176. }
  177. /**
  178. * 从导出的数据恢复注解
  179. * @param exportedData 导出的数据
  180. * @param element HTML元素
  181. * @param viewport Viewport实例
  182. * @returns 恢复的注解
  183. */
  184. static restoreFromExportedData(
  185. exportedData: ExportedMidlineData,
  186. element: HTMLDivElement,
  187. viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport
  188. ): MidlineMeasurementAnnotation {
  189. const enabledElement = getEnabledElement(element);
  190. if (!enabledElement) {
  191. throw new Error('Element is not enabled');
  192. }
  193. const annotationData = {
  194. invalidated: true,
  195. highlighted: false,
  196. metadata: {
  197. ...exportedData.metadata,
  198. toolName: MidlineMeasurementTool.toolName,
  199. },
  200. data: {
  201. label: '',
  202. handles: {
  203. points: [...exportedData.points],
  204. activeHandleIndex: null,
  205. },
  206. cachedStats: {},
  207. },
  208. } as MidlineMeasurementAnnotation;
  209. return annotationData;
  210. }
  211. constructor(
  212. toolProps: PublicToolProps = {},
  213. defaultToolProps: ToolProps = {
  214. supportedInteractionTypes: ['Mouse', 'Touch'],
  215. configuration: {
  216. shadow: true,
  217. preventHandleOutsideImage: false,
  218. },
  219. }
  220. ) {
  221. super(toolProps, defaultToolProps);
  222. }
  223. /**
  224. * 添加新注解 - 禁用此功能,因为我们只使用预设的注解
  225. */
  226. addNewAnnotation(
  227. evt: EventTypes.InteractionEventType
  228. ): Types.Annotation {
  229. // 不创建新注解,直接返回空对象
  230. evt.preventDefault();
  231. return {} as Types.Annotation;
  232. }
  233. /**
  234. * 处理 控制点/端点 选中回调
  235. */
  236. handleSelectedCallback(
  237. evt: EventTypes.InteractionEventType,
  238. annotation: Types.Annotation
  239. ): void {
  240. // 实现手柄选中逻辑
  241. if (isMidlineMeasurementAnnotation(annotation)) {
  242. annotation.highlighted = true;
  243. }
  244. }
  245. /**
  246. * 工具选中回调
  247. */
  248. toolSelectedCallback(
  249. evt: EventTypes.InteractionEventType,
  250. annotation: Types.Annotation
  251. ): void {
  252. // 实现工具选中逻辑
  253. if (isMidlineMeasurementAnnotation(annotation)) {
  254. annotation.highlighted = true;
  255. console.log(`选中了中线测量工具`);
  256. }
  257. }
  258. /**
  259. * 检查点是否靠近工具
  260. */
  261. isPointNearTool(
  262. element: HTMLDivElement,
  263. annotation: Types.Annotation,
  264. canvasCoords: CoreTypes.Point2,
  265. proximity: number
  266. ): boolean {
  267. if (!isMidlineMeasurementAnnotation(annotation)) {
  268. return false;
  269. }
  270. const enabledElement = getEnabledElement(element);
  271. if (!enabledElement) {
  272. return false;
  273. }
  274. const { viewport } = enabledElement;
  275. const points = annotation.data.handles.points;
  276. // 检查是否靠近任意一个手柄点
  277. for (let i = 0; i < points.length; i++) {
  278. const point = points[i];
  279. const canvasPoint = viewport.worldToCanvas(point);
  280. const distance = Math.sqrt(
  281. Math.pow(canvasPoint[0] - canvasCoords[0], 2) +
  282. Math.pow(canvasPoint[1] - canvasCoords[1], 2)
  283. );
  284. if (distance < proximity) {
  285. return true;
  286. }
  287. }
  288. // 检查是否靠近线段
  289. if (points.length >= 2) {
  290. for (let i = 0; i < points.length - 1; i += 2) {
  291. if (i + 1 < points.length) {
  292. const p1Canvas = viewport.worldToCanvas(points[i]);
  293. const p2Canvas = viewport.worldToCanvas(points[i + 1]);
  294. const dist = this._distanceToSegment(canvasCoords, p1Canvas, p2Canvas);
  295. if (dist < proximity) {
  296. return true;
  297. }
  298. }
  299. }
  300. }
  301. // 检查是否靠近中线
  302. if (points.length >= 4) {
  303. const midA = calculateMidpoint(points[0], points[1]);
  304. const midC = calculateMidpoint(points[2], points[3]);
  305. const midACanvas = viewport.worldToCanvas(midA);
  306. const midCCanvas = viewport.worldToCanvas(midC);
  307. const dist = this._distanceToSegment(canvasCoords, midACanvas, midCCanvas);
  308. if (dist < proximity) {
  309. return true;
  310. }
  311. }
  312. return false;
  313. }
  314. /**
  315. * 计算点到线段的距离
  316. */
  317. private _distanceToSegment(
  318. point: CoreTypes.Point2,
  319. lineStart: CoreTypes.Point2,
  320. lineEnd: CoreTypes.Point2
  321. ): number {
  322. const x = point[0];
  323. const y = point[1];
  324. const x1 = lineStart[0];
  325. const y1 = lineStart[1];
  326. const x2 = lineEnd[0];
  327. const y2 = lineEnd[1];
  328. const A = x - x1;
  329. const B = y - y1;
  330. const C = x2 - x1;
  331. const D = y2 - y1;
  332. const dot = A * C + B * D;
  333. const lenSq = C * C + D * D;
  334. let param = -1;
  335. if (lenSq !== 0) {
  336. param = dot / lenSq;
  337. }
  338. let xx, yy;
  339. if (param < 0) {
  340. xx = x1;
  341. yy = y1;
  342. } else if (param > 1) {
  343. xx = x2;
  344. yy = y2;
  345. } else {
  346. xx = x1 + param * C;
  347. yy = y1 + param * D;
  348. }
  349. const dx = x - xx;
  350. const dy = y - yy;
  351. return Math.sqrt(dx * dx + dy * dy);
  352. }
  353. /**
  354. * 取消操作
  355. */
  356. cancel(element: HTMLDivElement): string {
  357. if (this.isDrawing) {
  358. this.isDrawing = false;
  359. this._deactivateDraw(element);
  360. this._deactivateModify(element);
  361. const enabledElement = getEnabledElement(element);
  362. if (enabledElement) {
  363. const { renderingEngine } = enabledElement;
  364. const viewportIdsToRender =
  365. utilities.viewportFilters.getViewportIdsWithToolToRender(
  366. element,
  367. this.getToolName()
  368. );
  369. utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
  370. }
  371. this.editData = null;
  372. return this.getToolName();
  373. }
  374. return '';
  375. }
  376. /**
  377. * 激活绘制模式
  378. */
  379. _activateDraw(element: HTMLDivElement): void {
  380. element.addEventListener(
  381. 'CORNERSTONE_TOOLS_MOUSE_DRAG',
  382. this._dragCallback as EventListener
  383. );
  384. element.addEventListener(
  385. 'CORNERSTONE_TOOLS_MOUSE_UP',
  386. this._endCallback as EventListener
  387. );
  388. element.addEventListener(
  389. 'CORNERSTONE_TOOLS_MOUSE_CLICK',
  390. this._endCallback as EventListener
  391. );
  392. }
  393. /**
  394. * 取消激活绘制模式
  395. */
  396. _deactivateDraw(element: HTMLDivElement): void {
  397. element.removeEventListener(
  398. 'CORNERSTONE_TOOLS_MOUSE_DRAG',
  399. this._dragCallback as EventListener
  400. );
  401. element.removeEventListener(
  402. 'CORNERSTONE_TOOLS_MOUSE_UP',
  403. this._endCallback as EventListener
  404. );
  405. element.removeEventListener(
  406. 'CORNERSTONE_TOOLS_MOUSE_CLICK',
  407. this._endCallback as EventListener
  408. );
  409. }
  410. /**
  411. * 激活修改模式
  412. */
  413. _activateModify(element: HTMLDivElement): void {
  414. element.addEventListener(
  415. 'CORNERSTONE_TOOLS_MOUSE_DOWN',
  416. this._mouseDownModifyCallback as EventListener
  417. );
  418. element.addEventListener(
  419. 'CORNERSTONE_TOOLS_MOUSE_DRAG',
  420. this._mouseDragModifyCallback as EventListener
  421. );
  422. element.addEventListener(
  423. 'CORNERSTONE_TOOLS_MOUSE_UP',
  424. this._mouseUpModifyCallback as EventListener
  425. );
  426. element.addEventListener(
  427. 'CORNERSTONE_TOOLS_MOUSE_MOVE',
  428. this._mouseMoveModifyCallback as EventListener
  429. );
  430. // 添加键盘事件监听用于删除功能
  431. element.addEventListener(
  432. 'keydown',
  433. this._keyDownCallback as EventListener
  434. );
  435. }
  436. /**
  437. * 取消激活修改模式
  438. */
  439. _deactivateModify(element: HTMLDivElement): void {
  440. element.removeEventListener(
  441. 'CORNERSTONE_TOOLS_MOUSE_DOWN',
  442. this._mouseDownModifyCallback as EventListener
  443. );
  444. element.removeEventListener(
  445. 'CORNERSTONE_TOOLS_MOUSE_DRAG',
  446. this._mouseDragModifyCallback as EventListener
  447. );
  448. element.removeEventListener(
  449. 'CORNERSTONE_TOOLS_MOUSE_UP',
  450. this._mouseUpModifyCallback as EventListener
  451. );
  452. element.removeEventListener(
  453. 'CORNERSTONE_TOOLS_MOUSE_MOVE',
  454. this._mouseMoveModifyCallback as EventListener
  455. );
  456. // 移除键盘事件监听
  457. element.removeEventListener(
  458. 'keydown',
  459. this._keyDownCallback as EventListener
  460. );
  461. }
  462. /**
  463. * 键盘按下回调 - 用于删除功能
  464. */
  465. _keyDownCallback = (evt: KeyboardEvent): void => {
  466. const element = evt.currentTarget as HTMLDivElement;
  467. if (evt.key === 'Delete' || evt.key === 'Backspace') {
  468. const enabledElement = getEnabledElement(element);
  469. if (!enabledElement) {
  470. return;
  471. }
  472. const annotations = annotation.state.getAnnotations(this.getToolName(), element);
  473. if (!annotations || annotations.length === 0) {
  474. return;
  475. }
  476. // 查找被选中的注解
  477. const selectedAnnotations = annotations.filter(
  478. (ann) => isMidlineMeasurementAnnotation(ann) && ann.highlighted
  479. );
  480. if (selectedAnnotations.length === 0) {
  481. return;
  482. }
  483. // 删除所有选中的注解
  484. for (const ann of selectedAnnotations) {
  485. annotation.state.removeAnnotation(ann.annotationUID!);
  486. }
  487. // 触发渲染更新
  488. const viewportIdsToRender =
  489. utilities.viewportFilters.getViewportIdsWithToolToRender(
  490. element,
  491. this.getToolName()
  492. );
  493. utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
  494. evt.preventDefault();
  495. evt.stopPropagation();
  496. }
  497. };
  498. /**
  499. * 鼠标移动回调 - 用于修改模式,处理悬停检测
  500. */
  501. _mouseMoveModifyCallback = (evt: EventTypes.InteractionEventType): void => {
  502. const eventDetail = evt.detail;
  503. const { element, currentPoints } = eventDetail;
  504. if (!currentPoints || !currentPoints.canvas) {
  505. return;
  506. }
  507. const canvasCoords = currentPoints.canvas;
  508. const enabledElement = getEnabledElement(element);
  509. if (!enabledElement) {
  510. return;
  511. }
  512. const annotations = annotation.state.getAnnotations(this.getToolName(), element);
  513. if (!annotations || annotations.length === 0) {
  514. element.style.cursor = 'default';
  515. return;
  516. }
  517. let isHovering = false;
  518. // 检查是否悬停在手柄或线段上
  519. for (const ann of annotations) {
  520. if (!isMidlineMeasurementAnnotation(ann)) continue;
  521. // 检查是否靠近手柄
  522. const handle = this.getHandleNearImagePoint(element, ann, canvasCoords, 6);
  523. if (handle) {
  524. element.style.cursor = 'crosshair';
  525. ann.highlighted = true;
  526. isHovering = true;
  527. break;
  528. }
  529. // 检查是否靠近线段
  530. if (this.isPointNearTool(element, ann, canvasCoords, 10)) {
  531. element.style.cursor = 'crosshair';
  532. ann.highlighted = true;
  533. isHovering = true;
  534. break;
  535. }
  536. }
  537. // 如果没有悬停在任何地方,重置高亮
  538. if (!isHovering) {
  539. for (const ann of annotations) {
  540. if (isMidlineMeasurementAnnotation(ann)) {
  541. ann.highlighted = false;
  542. }
  543. }
  544. element.style.cursor = 'default';
  545. }
  546. // 触发渲染以更新高亮状态
  547. const viewportIdsToRender = utilities.viewportFilters.getViewportIdsWithToolToRender(
  548. element,
  549. this.getToolName()
  550. );
  551. utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
  552. };
  553. /**
  554. * 鼠标按下回调 - 用于修改模式
  555. */
  556. _mouseDownModifyCallback = (evt: EventTypes.InteractionEventType): void => {
  557. const eventDetail = evt.detail;
  558. const { element, currentPoints } = eventDetail;
  559. const canvasCoords = currentPoints.canvas;
  560. const enabledElement = getEnabledElement(element);
  561. if (!enabledElement) {
  562. return;
  563. }
  564. const annotations = annotation.state.getAnnotations(this.getToolName(), element);
  565. if (!annotations || annotations.length === 0) {
  566. return;
  567. }
  568. // 查找最近的手柄
  569. for (const ann of annotations) {
  570. if (!isMidlineMeasurementAnnotation(ann)) continue;
  571. const handle = this.getHandleNearImagePoint(
  572. element,
  573. ann,
  574. canvasCoords,
  575. 6
  576. );
  577. if (handle) {
  578. const viewportIdsToRender =
  579. utilities.viewportFilters.getViewportIdsWithToolToRender(
  580. element,
  581. this.getToolName()
  582. );
  583. this.editData = {
  584. annotation: ann,
  585. viewportIdsToRender,
  586. handleIndex: ann.data.handles.activeHandleIndex || 0,
  587. hasMoved: false,
  588. };
  589. ann.isSelected = true;
  590. ann.highlighted = true;
  591. utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
  592. evt.preventDefault();
  593. evt.stopPropagation();
  594. return;
  595. }
  596. }
  597. // 检查是否点击在工具的其他位置(线段上)
  598. for (const ann of annotations) {
  599. if (!isMidlineMeasurementAnnotation(ann)) continue;
  600. if (this.isPointNearTool(element, ann, canvasCoords, 10)) {
  601. const viewportIdsToRender =
  602. utilities.viewportFilters.getViewportIdsWithToolToRender(
  603. element,
  604. this.getToolName()
  605. );
  606. // 计算鼠标点击位置相对于工具的偏移量
  607. const points = ann.data.handles.points;
  608. if (points.length >= 4) {
  609. const midPoint = calculateMidpoint(points[0], points[1]);
  610. const midCanvas = enabledElement.viewport.worldToCanvas(midPoint);
  611. const wholeToolOffset: CoreTypes.Point2 = [
  612. canvasCoords[0] - midCanvas[0],
  613. canvasCoords[1] - midCanvas[1],
  614. ];
  615. this.editData = {
  616. annotation: ann,
  617. viewportIdsToRender,
  618. hasMoved: false,
  619. movingWholeTool: true,
  620. wholeToolOffset: wholeToolOffset,
  621. };
  622. ann.isSelected = true;
  623. ann.highlighted = true;
  624. utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
  625. evt.preventDefault();
  626. evt.stopPropagation();
  627. return;
  628. }
  629. }
  630. }
  631. // 如果没有点击在工具上,取消所有工具的选中状态
  632. for (const ann of annotations) {
  633. if (isMidlineMeasurementAnnotation(ann)) {
  634. ann.isSelected = false;
  635. ann.highlighted = false;
  636. }
  637. }
  638. const viewportIdsToRender =
  639. utilities.viewportFilters.getViewportIdsWithToolToRender(
  640. element,
  641. this.getToolName()
  642. );
  643. utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
  644. };
  645. /**
  646. * 鼠标拖拽回调 - 用于修改模式
  647. */
  648. _mouseDragModifyCallback = (evt: EventTypes.InteractionEventType): void => {
  649. if (!this.editData) {
  650. return;
  651. }
  652. const eventDetail = evt.detail;
  653. const { currentPoints } = eventDetail;
  654. const canvasCoords = currentPoints.canvas;
  655. const enabledElement = getEnabledElement(eventDetail.element);
  656. if (!enabledElement) {
  657. return;
  658. }
  659. const { annotation: ann, viewportIdsToRender, movingWholeTool } = this.editData;
  660. if (!isMidlineMeasurementAnnotation(ann)) return;
  661. const { data } = ann;
  662. // 如果正在移动整个工具
  663. if (movingWholeTool && this.editData.wholeToolOffset) {
  664. const newMidCanvas: CoreTypes.Point2 = [
  665. canvasCoords[0] - this.editData.wholeToolOffset[0],
  666. canvasCoords[1] - this.editData.wholeToolOffset[1],
  667. ];
  668. const newMidWorld = enabledElement.viewport.canvasToWorld(newMidCanvas);
  669. const points = data.handles.points;
  670. if (points.length >= 4) {
  671. const oldMid = calculateMidpoint(points[0], points[1]);
  672. const offset = vectorSubtract(newMidWorld, oldMid);
  673. // 移动所有点
  674. for (let i = 0; i < points.length; i++) {
  675. points[i] = vectorAdd(points[i], offset);
  676. }
  677. // 重新计算统计数据
  678. this._updateCachedStats(ann, enabledElement);
  679. this.editData.hasMoved = true;
  680. utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
  681. evt.preventDefault();
  682. evt.stopPropagation();
  683. return;
  684. }
  685. }
  686. // 手柄拖拽
  687. const worldPos = currentPoints.world;
  688. const activeHandleIndex = data.handles.activeHandleIndex;
  689. if (activeHandleIndex !== null && activeHandleIndex >= 0 && activeHandleIndex < data.handles.points.length) {
  690. data.handles.points[activeHandleIndex] = worldPos;
  691. this._updateCachedStats(ann, enabledElement);
  692. this.editData.hasMoved = true;
  693. utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
  694. evt.preventDefault();
  695. evt.stopPropagation();
  696. }
  697. };
  698. /**
  699. * 鼠标释放回调 - 用于修改模式
  700. */
  701. _mouseUpModifyCallback = (evt: EventTypes.InteractionEventType): void => {
  702. if (!this.editData) {
  703. return;
  704. }
  705. const { annotation: ann, hasMoved, movingWholeTool } = this.editData;
  706. if (!isMidlineMeasurementAnnotation(ann)) return;
  707. ann.data.handles.activeHandleIndex = null;
  708. // 如果没有拖拽且点击在线段上,则保持选中状态
  709. if (!hasMoved && movingWholeTool) {
  710. ann.highlighted = true;
  711. }
  712. const eventDetail = evt.detail;
  713. const { element } = eventDetail;
  714. const viewportIdsToRender =
  715. utilities.viewportFilters.getViewportIdsWithToolToRender(
  716. element,
  717. this.getToolName()
  718. );
  719. utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
  720. this.editData = null;
  721. evt.preventDefault();
  722. evt.stopPropagation();
  723. };
  724. /**
  725. * 处理鼠标拖拽
  726. */
  727. _dragCallback = (evt: EventTypes.InteractionEventType): void => {
  728. this.isDrawing = true;
  729. const eventDetail = evt.detail;
  730. const { element } = eventDetail;
  731. const enabledElement = getEnabledElement(element);
  732. if (!enabledElement || !this.editData) {
  733. return;
  734. }
  735. const { annotation, viewportIdsToRender } = this.editData;
  736. if (!isMidlineMeasurementAnnotation(annotation)) return;
  737. const { data } = annotation;
  738. if (annotation) {
  739. const { currentPoints } = eventDetail;
  740. const worldPos = currentPoints.world;
  741. const points = data.handles.points;
  742. const activeHandleIndex = data.handles.activeHandleIndex;
  743. if (activeHandleIndex !== null && activeHandleIndex < points.length) {
  744. points[activeHandleIndex] = worldPos;
  745. this._updateCachedStats(
  746. annotation,
  747. enabledElement
  748. );
  749. this.editData!.hasMoved = true;
  750. }
  751. utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
  752. }
  753. };
  754. /**
  755. * 处理鼠标抬起/点击
  756. */
  757. _endCallback = (evt: EventTypes.InteractionEventType): void => {
  758. const eventDetail = evt.detail;
  759. const { element } = eventDetail;
  760. if (!this.editData) {
  761. return;
  762. }
  763. const { annotation, viewportIdsToRender, newAnnotation, hasMoved } =
  764. this.editData;
  765. if (!isMidlineMeasurementAnnotation(annotation)) return;
  766. const { data } = annotation;
  767. if (newAnnotation && !hasMoved) {
  768. const points = data.handles.points;
  769. if (points.length < 4) {
  770. const { currentPoints } = eventDetail;
  771. const worldPos = currentPoints.world;
  772. if (points.length > 0) {
  773. points[points.length - 1] = worldPos;
  774. }
  775. if (points.length < 4) {
  776. points.push(worldPos);
  777. data.handles.activeHandleIndex = points.length - 1;
  778. }
  779. if (points.length === 4) {
  780. this.isDrawing = false;
  781. data.handles.activeHandleIndex = null;
  782. annotation.highlighted = false;
  783. this.editData.newAnnotation = false;
  784. this._deactivateDraw(element);
  785. }
  786. const enabledElement = getEnabledElement(element);
  787. if (enabledElement) {
  788. this._updateCachedStats(
  789. annotation,
  790. enabledElement
  791. );
  792. const newViewportIdsToRender =
  793. utilities.viewportFilters.getViewportIdsWithToolToRender(
  794. element,
  795. this.getToolName()
  796. );
  797. utilities.triggerAnnotationRenderForViewportIds(newViewportIdsToRender);
  798. }
  799. }
  800. } else if (hasMoved) {
  801. this.editData.hasMoved = false;
  802. }
  803. };
  804. /**
  805. * 获取靠近图像点的手柄
  806. */
  807. getHandleNearImagePoint(
  808. element: HTMLDivElement,
  809. annotation: Types.Annotation,
  810. canvasCoords: CoreTypes.Point2,
  811. proximity: number
  812. ): Types.ToolHandle | undefined {
  813. if (!isMidlineMeasurementAnnotation(annotation)) return undefined;
  814. const enabledElement = getEnabledElement(element);
  815. if (!enabledElement) {
  816. return undefined;
  817. }
  818. const { viewport } = enabledElement;
  819. const points = annotation.data.handles.points;
  820. const handleProximity = Math.max(proximity, 15);
  821. for (let i = 0; i < points.length; i++) {
  822. const point = points[i];
  823. const canvasPoint = viewport.worldToCanvas(point);
  824. const distance = Math.sqrt(
  825. Math.pow(canvasPoint[0] - canvasCoords[0], 2) +
  826. Math.pow(canvasPoint[1] - canvasCoords[1], 2)
  827. );
  828. if (distance < handleProximity) {
  829. annotation.data.handles.activeHandleIndex = i;
  830. return {
  831. worldPosition: point,
  832. } as Types.ToolHandle;
  833. }
  834. }
  835. annotation.data.handles.activeHandleIndex = null;
  836. return undefined;
  837. }
  838. /**
  839. * 更新缓存的统计数据
  840. */
  841. _updateCachedStats(
  842. annotation: MidlineMeasurementAnnotation,
  843. enabledElement: CoreTypes.IEnabledElement
  844. ): void {
  845. const { viewport } = enabledElement;
  846. const { data } = annotation;
  847. const points = data.handles.points;
  848. const targetId = this.getTargetId(viewport);
  849. if (!targetId) {
  850. return;
  851. }
  852. if (!data.cachedStats) {
  853. data.cachedStats = {};
  854. }
  855. if (!data.cachedStats[targetId]) {
  856. data.cachedStats[targetId] = {
  857. midPointM1: [0, 0, 0] as CoreTypes.Point3,
  858. midPointM2: [0, 0, 0] as CoreTypes.Point3,
  859. midlineLength: 0,
  860. };
  861. }
  862. const stats = data.cachedStats[targetId];
  863. if (points.length >= 4) {
  864. // 计算中点
  865. const midPointM1 = calculateMidpoint(points[0], points[1]);
  866. const midPointM2 = calculateMidpoint(points[2], points[3]);
  867. // 计算中线长度
  868. const midlineLength = calculateDistance(midPointM1, midPointM2);
  869. // 存储计算结果
  870. stats.midPointM1 = midPointM1;
  871. stats.midPointM2 = midPointM2;
  872. stats.midlineLength = midlineLength;
  873. }
  874. }
  875. /**
  876. * 渲染注解
  877. */
  878. renderAnnotation = (
  879. enabledElement: CoreTypes.IEnabledElement,
  880. svgDrawingHelper: SVGDrawingHelper
  881. ): boolean => {
  882. let renderStatus = false;
  883. const { viewport } = enabledElement;
  884. const { element } = viewport;
  885. let annotations = annotation.state.getAnnotations(this.getToolName(), element);
  886. if (!annotations?.length) {
  887. return renderStatus;
  888. }
  889. for (let i = 0; i < annotations.length; i++) {
  890. const annotation = annotations[i];
  891. if (!isMidlineMeasurementAnnotation(annotation)) continue;
  892. const { annotationUID, data } = annotation;
  893. const points = data.handles.points;
  894. if (points.length < 4) {
  895. continue;
  896. }
  897. const targetId = this.getTargetId(viewport);
  898. const cachedStats = targetId ? data.cachedStats?.[targetId] : undefined;
  899. // 转换所有点为canvas坐标
  900. const canvasPoints = points.map((p) => viewport.worldToCanvas(p));
  901. // 绘制线段AB (蓝色)
  902. if (annotationUID) {
  903. const lineABUID = `${annotationUID}-lineAB`;
  904. drawLineSvg(
  905. svgDrawingHelper,
  906. annotationUID,
  907. lineABUID,
  908. canvasPoints[0],
  909. canvasPoints[1],
  910. {
  911. color: 'rgb(0, 0, 255)',
  912. width: 2,
  913. }
  914. );
  915. }
  916. // 绘制线段CD (红色)
  917. if (annotationUID) {
  918. const lineCDUID = `${annotationUID}-lineCD`;
  919. drawLineSvg(
  920. svgDrawingHelper,
  921. annotationUID,
  922. lineCDUID,
  923. canvasPoints[2],
  924. canvasPoints[3],
  925. {
  926. color: 'rgb(255, 0, 0)',
  927. width: 2,
  928. }
  929. );
  930. }
  931. // 绘制中线 (绿色)
  932. if (cachedStats?.midPointM1 && cachedStats?.midPointM2 && annotationUID) {
  933. const midM1Canvas = viewport.worldToCanvas(cachedStats.midPointM1);
  934. const midM2Canvas = viewport.worldToCanvas(cachedStats.midPointM2);
  935. const midlineUID = `${annotationUID}-midline`;
  936. drawLineSvg(
  937. svgDrawingHelper,
  938. annotationUID,
  939. midlineUID,
  940. midM1Canvas,
  941. midM2Canvas,
  942. {
  943. color: 'rgb(0, 255, 0)',
  944. width: 2,
  945. }
  946. );
  947. }
  948. // 绘制手柄点
  949. if (annotationUID) {
  950. const handleGroupUID = '0';
  951. const handleRadius = annotation.highlighted ? 12 : 8;
  952. drawHandles(
  953. svgDrawingHelper,
  954. annotationUID,
  955. handleGroupUID,
  956. canvasPoints,
  957. {
  958. color: 'rgb(255, 255, 255)',
  959. handleRadius: handleRadius,
  960. }
  961. );
  962. }
  963. renderStatus = true;
  964. }
  965. return renderStatus;
  966. };
  967. }