FindMidpointMeasurementTool.ts 31 KB

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