PolygonLengthMeasurementTool.ts 44 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473
  1. import { Point3, Point2, fromPoint3ToPoint2, decomposeVectorOnAxes } 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. drawPath: drawPathSvg,
  20. } = drawing;
  21. import {
  22. calculateMidpoint,
  23. vectorSubtract,
  24. vectorAdd,
  25. vectorScale,
  26. getAngleBetweenLines,
  27. degrees,
  28. getLineIntersection,
  29. fromPoint2ToPoint3,
  30. } from './mathUtils';
  31. import {
  32. PublicToolProps,
  33. ToolProps,
  34. EventTypes,
  35. SVGDrawingHelper,
  36. } from '@cornerstonejs/tools/dist/esm/types';
  37. // 十字准星光标
  38. const cursorUrl = 'url("data:image/svg+xml;base64,PHN2ZyBpZD0ibW92ZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB3aWR0aD0iNDgiIGhlaWdodD0iNDgiIHZpZXdCb3g9IjAgMCA0OCA0OCI+CiAgPGRlZnM+CiAgICA8c3R5bGU+CiAgICAgIC52aS1wcmltYXJ5IHsKICAgICAgICBmaWxsOiAjRkY2RTZFOwogICAgICB9CgogICAgICAudmktcHJpbWFyeSwgLnZpLWFjY2VudCB7CiAgICAgICAgZmlsbC1ydWxlOiBldmVub2RkOwogICAgICB9CgogICAgICAudmktYWNjZW50IHsKICAgICAgICBmaWxsOiAjMEMwMDU4OwogICAgICB9CiAgICA8L3N0eWxlPgogIDwvZGVmcz4KICA8cGF0aCBjbGFzcz0idmktcHJpbWFyeSIgZD0iTTM0LDMwbC0yLTIsMi0ySDI2djhsMi0yLDIsMi01LDVIMjRsLTUtNSwyLTItMiwyaDhWMTVsLTIsMi0yLTIsNS01aDFsNSw1LTIsMi0yLTJ2OGg4bC0yLTIsMi0yLDUsNXYxWiIvPgogIDxwYXRoIGNsYXNzPSJ2aS1hY2NlbnQiIGQ9Ik0xOSwyM3YzaDR2NGgzVjI2aDRWMjNIMjZWMTlIMjN2NEgxOVoiLz4KPC9zdmc+Cg==") 24 24, crosshair';
  39. // 类型定义
  40. type PolygonLengthMeasurementAnnotation = Types.Annotation & {
  41. data: {
  42. handles: {
  43. points: CoreTypes.Point3[]; // 多边形的所有顶点(世界坐标)
  44. activeHandleIndex: number | null; // 当前激活的手柄索引
  45. textBox?: CoreTypes.Point2; // 文本框位置
  46. };
  47. cachedStats?: {
  48. [targetId: string]: {
  49. totalLength: number; // 总周长(像素)
  50. totalLengthMm: number; // 总周长(mm)
  51. segmentLengths: number[]; // 各边长度(包括闭合边)
  52. };
  53. };
  54. };
  55. };
  56. // 导出的注解数据接口
  57. export interface ExportedAnnotationData {
  58. points: CoreTypes.Point3[]; // 多边形的所有顶点
  59. totalLengthMm: number; // 总周长(mm)
  60. metadata: {
  61. viewPlaneNormal: CoreTypes.Point3;
  62. viewUp: CoreTypes.Point3;
  63. FrameOfReferenceUID: string;
  64. referencedImageId: string;
  65. };
  66. }
  67. export default class PolygonLengthMeasurementTool extends AnnotationTool {
  68. static toolName = 'PolygonLengthMeasurementTool';
  69. editData: {
  70. annotation: Types.Annotation;
  71. viewportIdsToRender: string[];
  72. handleIndex?: number;
  73. newAnnotation?: boolean;
  74. hasMoved?: boolean;
  75. textBoxBeingMoved?: boolean; // 是否正在拖拽文本框
  76. textBoxOffset?: CoreTypes.Point2; // 文本框拖拽偏移量
  77. movingWholeTool?: boolean; // 是否移动整个工具
  78. wholeToolOffset?: CoreTypes.Point2; // 整体移动偏移量
  79. } | null = null;
  80. currentAnnotation: PolygonLengthMeasurementAnnotation | null = null; // 当前正在绘制的注解
  81. isDrawing: boolean = false;
  82. drawingElement: HTMLDivElement | null = null; // 当前绘制模式的元素
  83. drawingCompleted: boolean = false; // 标记绘制是否已完成(双击后反激活)
  84. // 预览鼠标位置(canvas坐标)
  85. private previewMousePosition: CoreTypes.Point2 | null = null;
  86. constructor(
  87. toolProps: PublicToolProps = {},
  88. defaultToolProps: ToolProps = {
  89. supportedInteractionTypes: ['Mouse', 'Touch'],
  90. configuration: {
  91. shadow: true,
  92. preventHandleOutsideImage: false,
  93. },
  94. }
  95. ) {
  96. super(toolProps, defaultToolProps);
  97. }
  98. /**
  99. * 创建默认注解(用于预设模式)
  100. */
  101. static createDefaultAnnotation(
  102. element: HTMLDivElement,
  103. viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport,
  104. points: CoreTypes.Point3[] = []
  105. ): PolygonLengthMeasurementAnnotation {
  106. const enabledElement = getEnabledElement(element);
  107. if (!enabledElement) {
  108. throw new Error('Element is not enabled');
  109. }
  110. const camera = viewport.getCamera();
  111. const { viewPlaneNormal, viewUp } = camera;
  112. if (viewPlaneNormal === undefined) {
  113. throw new Error('viewPlaneNormal is undefined');
  114. }
  115. if (viewUp === undefined) {
  116. throw new Error('viewUp is undefined');
  117. }
  118. const annotationData = {
  119. invalidated: true,
  120. highlighted: false,
  121. metadata: {
  122. viewPlaneNormal: [...viewPlaneNormal] as CoreTypes.Point3,
  123. viewUp: [...viewUp] as CoreTypes.Point3,
  124. FrameOfReferenceUID: viewport.getFrameOfReferenceUID(),
  125. referencedImageId: viewport.getCurrentImageId?.() || '',
  126. toolName: PolygonLengthMeasurementTool.toolName,
  127. },
  128. data: {
  129. label: '',
  130. handles: {
  131. points: points,
  132. activeHandleIndex: null,
  133. },
  134. cachedStats: {},
  135. },
  136. } as PolygonLengthMeasurementAnnotation;
  137. return annotationData;
  138. }
  139. /**
  140. * 导出注解数据
  141. */
  142. static exportAnnotationData(
  143. annotation: PolygonLengthMeasurementAnnotation,
  144. viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport
  145. ): ExportedAnnotationData | null {
  146. const targetId = `imageId:${viewport.getCurrentImageId?.() || ''}`;
  147. const cachedStats = annotation.data.cachedStats?.[targetId];
  148. if (!cachedStats) {
  149. return null;
  150. }
  151. return {
  152. points: [...annotation.data.handles.points],
  153. totalLengthMm: cachedStats.totalLengthMm,
  154. metadata: {
  155. viewPlaneNormal: annotation.metadata?.viewPlaneNormal || [0, 0, 1],
  156. viewUp: annotation.metadata?.viewUp || [0, 1, 0],
  157. FrameOfReferenceUID: annotation.metadata?.FrameOfReferenceUID || '',
  158. referencedImageId: annotation.metadata?.referencedImageId || '',
  159. },
  160. };
  161. }
  162. /**
  163. * 添加新注解 - 创建空的闭合多边形注解用于绘制模式
  164. */
  165. addNewAnnotation(
  166. evt: EventTypes.InteractionEventType
  167. ): PolygonLengthMeasurementAnnotation {
  168. const eventDetail = evt.detail;
  169. const { element, currentPoints } = eventDetail;
  170. const worldPos = currentPoints.world;
  171. const enabledElement = getEnabledElement(element);
  172. if (!enabledElement) {
  173. throw new Error('Element is not enabled');
  174. }
  175. const camera = enabledElement.viewport.getCamera();
  176. const { viewPlaneNormal, viewUp } = camera;
  177. if (viewPlaneNormal === undefined) {
  178. throw new Error('viewPlaneNormal is undefined');
  179. }
  180. if (viewUp === undefined) {
  181. throw new Error('viewUp is undefined');
  182. }
  183. // 创建新的空注解
  184. const annotationData = {
  185. invalidated: true,
  186. highlighted: false,
  187. metadata: {
  188. viewPlaneNormal: [...viewPlaneNormal] as CoreTypes.Point3,
  189. viewUp: [...viewUp] as CoreTypes.Point3,
  190. FrameOfReferenceUID: enabledElement.viewport.getFrameOfReferenceUID(),
  191. referencedImageId: enabledElement.viewport.getCurrentImageId?.() || '',
  192. toolName: PolygonLengthMeasurementTool.toolName,
  193. },
  194. data: {
  195. label: '',
  196. handles: {
  197. points: [worldPos], // 添加第一个点
  198. activeHandleIndex: 0,
  199. },
  200. cachedStats: {},
  201. },
  202. } as PolygonLengthMeasurementAnnotation;
  203. // 设置编辑数据
  204. this.editData = {
  205. annotation: annotationData,
  206. viewportIdsToRender: [enabledElement.viewportId],
  207. newAnnotation: true,
  208. hasMoved: false,
  209. };
  210. // 激活绘制模式
  211. this._activateDraw(element);
  212. return annotationData;
  213. }
  214. /**
  215. * 取消操作
  216. */
  217. cancel(element: HTMLDivElement): string {
  218. if (this.isDrawing) {
  219. this.isDrawing = false;
  220. this._deactivateModify(element);
  221. const enabledElement = getEnabledElement(element);
  222. if (enabledElement) {
  223. const { renderingEngine } = enabledElement;
  224. const viewportIdsToRender =
  225. utilities.viewportFilters.getViewportIdsWithToolToRender(
  226. element,
  227. this.getToolName()
  228. );
  229. utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
  230. }
  231. this.editData = null;
  232. return this.getToolName();
  233. }
  234. return '';
  235. }
  236. /**
  237. * 处理 控制点/端点 选中回调
  238. */
  239. handleSelectedCallback(
  240. evt: EventTypes.InteractionEventType,
  241. annotation: PolygonLengthMeasurementAnnotation
  242. ): void {
  243. // 实现控制点选中逻辑
  244. annotation.isSelected = true;
  245. annotation.highlighted = true;
  246. const { element } = evt.detail;
  247. const viewportIdsToRender =
  248. utilities.viewportFilters.getViewportIdsWithToolToRender(
  249. element,
  250. this.getToolName()
  251. );
  252. utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
  253. }
  254. /**
  255. * 工具选中回调 - 当工具被激活时调用
  256. */
  257. toolSelectedCallback(
  258. evt: EventTypes.InteractionEventType,
  259. annotation: PolygonLengthMeasurementAnnotation
  260. ): void {
  261. const { element } = evt.detail;
  262. // 如果 annotation 不为空,说明是点击已有的注释,激活修改模式
  263. if (annotation) {
  264. console.log('PolygonLengthMeasurementTool selected existing annotation');
  265. this._activateModify(element);
  266. return;
  267. }
  268. // 工具被激活时,总是进入绘制模式,允许开始新的绘制
  269. // 完全重置所有绘制状态,确保全新的开始
  270. this._deactivateDraw(element); // 先清理之前的绘制状态
  271. this._deactivateModify(element); // 清理修改模式
  272. this.isDrawing = false; // 重置绘制状态
  273. this.drawingCompleted = false; // 允许新的点击操作
  274. this.currentAnnotation = null;
  275. this.drawingElement = null;
  276. // 激活绘制模式,等待用户开始绘制
  277. this._activateDraw(element);
  278. console.log('PolygonLengthMeasurementTool activated for new drawing');
  279. }
  280. /**
  281. * 激活时被调用
  282. */
  283. onSetToolActive(): void {
  284. // 示例:重置相关状态
  285. this.isDrawing = false;
  286. this.editData = null;
  287. this.drawingCompleted = false;//允许开始新的注解
  288. // 示例:添加工具特定的初始化
  289. // this.initializeCustomState();
  290. }
  291. /**
  292. * 检查点是否靠近工具
  293. */
  294. isPointNearTool(
  295. element: HTMLDivElement,
  296. annotation: PolygonLengthMeasurementAnnotation,
  297. canvasCoords: CoreTypes.Point2,
  298. proximity: number
  299. ): boolean {
  300. const enabledElement = getEnabledElement(element);
  301. if (!enabledElement) {
  302. return false;
  303. }
  304. const { viewport } = enabledElement;
  305. const points = annotation.data.handles.points;
  306. // 检查是否靠近文本框
  307. const textBox = annotation.data.handles.textBox;
  308. if (textBox && Array.isArray(textBox) && textBox.length >= 2) {
  309. if (this._isPointInTextBox(canvasCoords, textBox as CoreTypes.Point2)) {
  310. return true;
  311. }
  312. }
  313. // 检查是否靠近任意一个手柄点
  314. for (let i = 0; i < points.length; i++) {
  315. const point = points[i];
  316. const canvasPoint = viewport.worldToCanvas(point);
  317. const distance = Math.sqrt(
  318. Math.pow(canvasPoint[0] - canvasCoords[0], 2) +
  319. Math.pow(canvasPoint[1] - canvasCoords[1], 2)
  320. );
  321. if (distance < proximity) {
  322. return true;
  323. }
  324. }
  325. // 检查是否靠近线段(包括闭合线段)
  326. if (points.length >= 2) {
  327. for (let i = 0; i < points.length - 1; i++) {
  328. const p1Canvas = viewport.worldToCanvas(points[i]);
  329. const p2Canvas = viewport.worldToCanvas(points[i + 1]);
  330. const dist = this._distanceToSegment(canvasCoords, p1Canvas, p2Canvas);
  331. if (dist < proximity) {
  332. return true;
  333. }
  334. }
  335. }
  336. // 检查是否靠近闭合线段
  337. if (points.length >= 3) {
  338. if (this._isPointNearClosingSegment(canvasCoords, points, viewport, proximity)) {
  339. return true;
  340. }
  341. }
  342. return false;
  343. }
  344. /**
  345. * 检查是否靠近闭合线段
  346. */
  347. private _isPointNearClosingSegment(
  348. canvasCoords: CoreTypes.Point2,
  349. points: CoreTypes.Point3[],
  350. viewport: CoreTypes.IViewport,
  351. proximity: number
  352. ): boolean {
  353. if (points.length < 3) return false;
  354. const lastPoint = points[points.length - 1];
  355. const firstPoint = points[0];
  356. const lastCanvas = viewport.worldToCanvas(lastPoint);
  357. const firstCanvas = viewport.worldToCanvas(firstPoint);
  358. const dist = this._distanceToSegment(canvasCoords, lastCanvas, firstCanvas);
  359. return dist < proximity;
  360. }
  361. /**
  362. * 检查点击是否在文本框内
  363. */
  364. private _isPointInTextBox(
  365. canvasPoint: CoreTypes.Point2,
  366. textBoxPosition: CoreTypes.Point2,
  367. textWidth: number = 100,
  368. textHeight: number = 40
  369. ): boolean {
  370. // 确保参数有效
  371. if (!Array.isArray(canvasPoint) || canvasPoint.length < 2 ||
  372. !Array.isArray(textBoxPosition) || textBoxPosition.length < 2) {
  373. return false;
  374. }
  375. const [x, y] = canvasPoint;
  376. const [tx, ty] = textBoxPosition;
  377. const margin = 10;
  378. return (
  379. x >= tx - margin &&
  380. x <= tx + textWidth + margin &&
  381. y >= ty - margin &&
  382. y <= ty + textHeight + margin
  383. );
  384. }
  385. /**
  386. * 计算点到线段的距离
  387. */
  388. private _distanceToSegment(
  389. point: CoreTypes.Point2,
  390. lineStart: CoreTypes.Point2,
  391. lineEnd: CoreTypes.Point2
  392. ): number {
  393. const x = point[0];
  394. const y = point[1];
  395. const x1 = lineStart[0];
  396. const y1 = lineStart[1];
  397. const x2 = lineEnd[0];
  398. const y2 = lineEnd[1];
  399. const A = x - x1;
  400. const B = y - y1;
  401. const C = x2 - x1;
  402. const D = y2 - y1;
  403. const dot = A * C + B * D;
  404. const lenSq = C * C + D * D;
  405. let param = -1;
  406. if (lenSq !== 0) {
  407. param = dot / lenSq;
  408. }
  409. let xx, yy;
  410. if (param < 0) {
  411. xx = x1;
  412. yy = y1;
  413. } else if (param > 1) {
  414. xx = x2;
  415. yy = y2;
  416. } else {
  417. xx = x1 + param * C;
  418. yy = y1 + param * D;
  419. }
  420. const dx = x - xx;
  421. const dy = y - yy;
  422. return Math.sqrt(dx * dx + dy * dy);
  423. }
  424. /**
  425. * 计算两点间距离(像素)
  426. */
  427. private _calculateDistance(point1: CoreTypes.Point3, point2: CoreTypes.Point3, viewport: CoreTypes.IViewport): number {
  428. const canvas1 = viewport.worldToCanvas(point1);
  429. const canvas2 = viewport.worldToCanvas(point2);
  430. return Math.sqrt(
  431. Math.pow(canvas2[0] - canvas1[0], 2) + Math.pow(canvas2[1] - canvas1[1], 2)
  432. );
  433. }
  434. /**
  435. * 将像素距离转换为mm
  436. */
  437. private _convertPixelsToMm(pixels: number, viewport: CoreTypes.IViewport): number {
  438. // 这里需要根据DICOM元数据获取像素间距
  439. // 暂时使用固定值,实际应该从图像元数据中获取
  440. const pixelSpacing = 0.5; // mm per pixel,示例值
  441. return pixels * pixelSpacing;
  442. }
  443. /**
  444. * 更新缓存的统计数据(包含闭合边计算)
  445. */
  446. _updateCachedStats(
  447. annotation: PolygonLengthMeasurementAnnotation,
  448. enabledElement: CoreTypes.IEnabledElement
  449. ): void {
  450. const { viewport } = enabledElement;
  451. const { data } = annotation;
  452. const points = data.handles.points;
  453. const targetId = this.getTargetId(viewport);
  454. if (!targetId) {
  455. return;
  456. }
  457. if (!data.cachedStats) {
  458. data.cachedStats = {};
  459. }
  460. if (!data.cachedStats[targetId]) {
  461. data.cachedStats[targetId] = {
  462. totalLength: 0,
  463. totalLengthMm: 0,
  464. segmentLengths: []
  465. };
  466. }
  467. const stats = data.cachedStats[targetId];
  468. stats.segmentLengths = [];
  469. let totalPixels = 0;
  470. // 计算所有相邻顶点间的边长
  471. for (let i = 0; i < points.length - 1; i++) {
  472. const segmentLength = this._calculateDistance(points[i], points[i + 1], viewport);
  473. stats.segmentLengths.push(segmentLength);
  474. totalPixels += segmentLength;
  475. }
  476. // 计算闭合边(最后一个点到第一个点)
  477. if (points.length >= 3) {
  478. const closingSegmentLength = this._calculateDistance(points[points.length - 1], points[0], viewport);
  479. stats.segmentLengths.push(closingSegmentLength);
  480. totalPixels += closingSegmentLength;
  481. }
  482. stats.totalLength = totalPixels;
  483. stats.totalLengthMm = this._convertPixelsToMm(totalPixels, viewport);
  484. }
  485. /**
  486. * 激活绘制模式
  487. */
  488. _activateDraw(element: HTMLDivElement): void {
  489. this.drawingElement = element; // 存储当前绘制模式的元素
  490. this.isDrawing = true;
  491. element.addEventListener(
  492. 'CORNERSTONE_TOOLS_MOUSE_CLICK',
  493. this._mouseClickDrawCallback as EventListener
  494. );
  495. element.addEventListener(
  496. 'dblclick',
  497. this._mouseDoubleClickDrawCallback as EventListener
  498. );
  499. // 添加鼠标移动监听用于预览
  500. element.addEventListener(
  501. 'CORNERSTONE_TOOLS_MOUSE_MOVE',
  502. this._mouseMoveDrawCallback as EventListener
  503. );
  504. }
  505. /**
  506. * 取消激活绘制模式
  507. */
  508. _deactivateDraw(element?: HTMLDivElement): void {
  509. const targetElement = element || this.drawingElement;
  510. if (!targetElement) return;
  511. this.isDrawing = false;
  512. targetElement.removeEventListener(
  513. 'CORNERSTONE_TOOLS_MOUSE_CLICK',
  514. this._mouseClickDrawCallback as EventListener
  515. );
  516. targetElement.removeEventListener(
  517. 'dblclick',
  518. this._mouseDoubleClickDrawCallback as EventListener
  519. );
  520. // 清理鼠标移动监听器
  521. targetElement.removeEventListener(
  522. 'CORNERSTONE_TOOLS_MOUSE_MOVE',
  523. this._mouseMoveDrawCallback as EventListener
  524. );
  525. this.drawingElement = null; // 清理
  526. this.previewMousePosition = null; // 清理预览位置
  527. }
  528. /**
  529. * 激活修改模式
  530. */
  531. _activateModify(element: HTMLDivElement): void {
  532. element.addEventListener(
  533. 'CORNERSTONE_TOOLS_MOUSE_DOWN',
  534. this._mouseDownModifyCallback as EventListener
  535. );
  536. element.addEventListener(
  537. 'CORNERSTONE_TOOLS_MOUSE_DRAG',
  538. this._mouseDragModifyCallback as EventListener
  539. );
  540. element.addEventListener(
  541. 'CORNERSTONE_TOOLS_MOUSE_UP',
  542. this._mouseUpModifyCallback as EventListener
  543. );
  544. element.addEventListener(
  545. 'CORNERSTONE_TOOLS_MOUSE_MOVE',
  546. this._mouseMoveModifyCallback as EventListener
  547. );
  548. element.addEventListener(
  549. 'keydown',
  550. this._keyDownCallback as EventListener
  551. );
  552. }
  553. /**
  554. * 取消激活修改模式
  555. */
  556. _deactivateModify(element: HTMLDivElement): void {
  557. element.removeEventListener(
  558. 'CORNERSTONE_TOOLS_MOUSE_DOWN',
  559. this._mouseDownModifyCallback as EventListener
  560. );
  561. element.removeEventListener(
  562. 'CORNERSTONE_TOOLS_MOUSE_DRAG',
  563. this._mouseDragModifyCallback as EventListener
  564. );
  565. element.removeEventListener(
  566. 'CORNERSTONE_TOOLS_MOUSE_UP',
  567. this._mouseUpModifyCallback as EventListener
  568. );
  569. element.removeEventListener(
  570. 'CORNERSTONE_TOOLS_MOUSE_MOVE',
  571. this._mouseMoveModifyCallback as EventListener
  572. );
  573. element.removeEventListener(
  574. 'keydown',
  575. this._keyDownCallback as EventListener
  576. );
  577. }
  578. /**
  579. * 鼠标点击回调 - 用于绘制模式,添加新的点
  580. */
  581. _mouseClickDrawCallback = (evt: EventTypes.InteractionEventType): void => {
  582. if (!this.isDrawing || this.drawingCompleted) {
  583. return;
  584. }
  585. const eventDetail = evt.detail;
  586. const { element, currentPoints } = eventDetail;
  587. const worldPos = currentPoints.world;
  588. const enabledElement = getEnabledElement(element);
  589. if (!enabledElement) {
  590. return;
  591. }
  592. // 如果还没有当前注解,创建第一个
  593. if (!this.currentAnnotation) {
  594. // 创建新的注解
  595. const camera = enabledElement.viewport.getCamera();
  596. const { viewPlaneNormal, viewUp } = camera;
  597. if (viewPlaneNormal === undefined || viewUp === undefined) {
  598. return;
  599. }
  600. this.currentAnnotation = {
  601. invalidated: true,
  602. highlighted: false,
  603. metadata: {
  604. viewPlaneNormal: [...viewPlaneNormal] as CoreTypes.Point3,
  605. viewUp: [...viewUp] as CoreTypes.Point3,
  606. FrameOfReferenceUID: enabledElement.viewport.getFrameOfReferenceUID(),
  607. referencedImageId: enabledElement.viewport.getCurrentImageId?.() || '',
  608. toolName: PolygonLengthMeasurementTool.toolName,
  609. },
  610. data: {
  611. label: '',
  612. handles: {
  613. points: [worldPos],
  614. activeHandleIndex: 0,
  615. },
  616. cachedStats: {},
  617. },
  618. } as PolygonLengthMeasurementAnnotation;
  619. // 添加到临时状态以便渲染
  620. annotation.state.addAnnotation(this.currentAnnotation, element);
  621. console.log('Created first point for polygon');
  622. } else {
  623. // 添加新点到现有注解
  624. this.currentAnnotation.data.handles.points.push(worldPos);
  625. this.currentAnnotation.data.handles.activeHandleIndex = this.currentAnnotation.data.handles.points.length - 1;
  626. // 更新统计数据
  627. this._updateCachedStats(this.currentAnnotation, enabledElement);
  628. console.log(`Added point ${this.currentAnnotation.data.handles.points.length} to polygon`);
  629. }
  630. // 触发渲染更新
  631. const viewportIdsToRender = utilities.viewportFilters.getViewportIdsWithToolToRender(
  632. element,
  633. this.getToolName()
  634. );
  635. utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
  636. evt.preventDefault();
  637. evt.stopPropagation();
  638. };
  639. /**
  640. * 鼠标移动回调 - 用于绘制模式,更新预览位置
  641. */
  642. _mouseMoveDrawCallback = (evt: EventTypes.InteractionEventType): void => {
  643. if (!this.isDrawing) {
  644. return;
  645. }
  646. const eventDetail = evt.detail;
  647. const canvasCoords = eventDetail.currentPoints?.canvas;
  648. if (canvasCoords) {
  649. this.previewMousePosition = [...canvasCoords];
  650. // 触发重新渲染以显示预览线
  651. const viewportIdsToRender = utilities.viewportFilters.getViewportIdsWithToolToRender(
  652. eventDetail.element,
  653. this.getToolName()
  654. );
  655. utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
  656. }
  657. };
  658. /**
  659. * 鼠标双击回调 - 用于绘制模式,在双击位置添加最后一个点并结束绘制
  660. */
  661. _mouseDoubleClickDrawCallback = (evt: EventTypes.InteractionEventType): void => {
  662. if (!this.isDrawing || !this.currentAnnotation) {
  663. return;
  664. }
  665. const eventDetail = evt.detail;
  666. const element = eventDetail.element || this.drawingElement;
  667. const currentPoints = eventDetail.currentPoints;
  668. if (!element) {
  669. return;
  670. }
  671. // 获取双击位置,支持多种情况
  672. let worldPos: CoreTypes.Point3 | undefined;
  673. if (currentPoints?.world) {
  674. // 优先使用 world 坐标
  675. worldPos = currentPoints.world;
  676. } else if (currentPoints?.canvas) {
  677. // 如果没有 world 坐标但有 canvas 坐标,转换得到 world 坐标
  678. const enabledElement = getEnabledElement(element);
  679. if (enabledElement) {
  680. worldPos = enabledElement.viewport.canvasToWorld(currentPoints.canvas);
  681. }
  682. } else {
  683. // 如果 CornerstoneJS 没有提供坐标,使用原生 DOM 事件坐标
  684. // __类型转换__:将 `evt` 转换为 `MouseEvent`
  685. const mouseEvent = evt as unknown as MouseEvent;
  686. // __获取鼠标坐标__:使用 `clientX`, `clientY`
  687. const clientX = mouseEvent.clientX;
  688. const clientY = mouseEvent.clientY;
  689. // __计算 canvas 坐标__:减去 canvas 的偏移量
  690. const rect = element.getBoundingClientRect();
  691. const canvasX = clientX - rect.left;
  692. const canvasY = clientY - rect.top;
  693. const canvasPos: CoreTypes.Point2 = [canvasX, canvasY];
  694. // __转换为 world 坐标__:使用 `viewport.canvasToWorld()`
  695. const enabledElement = getEnabledElement(element);
  696. if (enabledElement) {
  697. worldPos = enabledElement.viewport.canvasToWorld(canvasPos);
  698. }
  699. }
  700. if (!worldPos) {
  701. // 如果仍然没有位置信息,记录警告但不中断流程
  702. console.warn('Unable to get double-click position for polygon');
  703. return;
  704. }
  705. // 先在双击位置添加最后一个点
  706. this.currentAnnotation.data.handles.points.push(worldPos);
  707. this.currentAnnotation.data.handles.activeHandleIndex = this.currentAnnotation.data.handles.points.length - 1;
  708. // 更新统计数据
  709. const enabledElement = getEnabledElement(element);
  710. if (enabledElement) {
  711. this._updateCachedStats(this.currentAnnotation, enabledElement);
  712. }
  713. // 然后检查点数决定是否结束绘制
  714. if (this.currentAnnotation.data.handles.points.length >= 3) {
  715. // 结束绘制模式,激活修改模式
  716. this._deactivateDraw(element);
  717. this._activateModify(element);
  718. this.isDrawing = false;
  719. this.drawingCompleted = true; // 标记绘制已完成,防止再次添加点
  720. this.currentAnnotation = null; // 完全清理当前注解引用,确保下次激活时全新的开始
  721. // 取消所有注解的选中状态,确保下次激活时 annotation 参数为 null
  722. const annotations = annotation.state.getAnnotations(this.getToolName(), element);
  723. if (annotations) {
  724. for (const ann of annotations) {
  725. const customAnn = ann as PolygonLengthMeasurementAnnotation;
  726. customAnn.isSelected = false;
  727. customAnn.highlighted = false;
  728. }
  729. }
  730. console.log('Finished drawing polygon');
  731. } else {
  732. // 如果点数仍然少于3个,删除这个注解
  733. annotation.state.removeAnnotation(this.currentAnnotation.annotationUID!);
  734. this.currentAnnotation = null;
  735. }
  736. // 触发渲染更新
  737. const viewportIdsToRender = utilities.viewportFilters.getViewportIdsWithToolToRender(
  738. element,
  739. this.getToolName()
  740. );
  741. utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
  742. evt.preventDefault();
  743. evt.stopPropagation();
  744. };
  745. /**
  746. * 键盘按下回调 - 用于删除功能
  747. */
  748. _keyDownCallback = (evt: KeyboardEvent): void => {
  749. const element = evt.currentTarget as HTMLDivElement;
  750. if (evt.key === 'Delete' || evt.key === 'Backspace') {
  751. const enabledElement = getEnabledElement(element);
  752. if (!enabledElement) {
  753. return;
  754. }
  755. const annotations = annotation.state.getAnnotations(this.getToolName(), element);
  756. if (!annotations || annotations.length === 0) {
  757. return;
  758. }
  759. const selectedAnnotations = annotations.filter(
  760. (ann) => (ann as PolygonLengthMeasurementAnnotation).isSelected
  761. );
  762. if (selectedAnnotations.length === 0) {
  763. return;
  764. }
  765. for (const ann of selectedAnnotations) {
  766. annotation.state.removeAnnotation(ann.annotationUID!);
  767. }
  768. const viewportIdsToRender =
  769. utilities.viewportFilters.getViewportIdsWithToolToRender(
  770. element,
  771. this.getToolName()
  772. );
  773. utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
  774. evt.preventDefault();
  775. evt.stopPropagation();
  776. }
  777. };
  778. /**
  779. * 鼠标移动回调 - 用于修改模式,处理悬停检测
  780. */
  781. _mouseMoveModifyCallback = (evt: EventTypes.InteractionEventType): void => {
  782. const eventDetail = evt.detail;
  783. const { element, currentPoints } = eventDetail;
  784. if (!currentPoints || !currentPoints.canvas) {
  785. return;
  786. }
  787. const canvasCoords = currentPoints.canvas;
  788. const enabledElement = getEnabledElement(element);
  789. if (!enabledElement) {
  790. return;
  791. }
  792. const { viewport } = enabledElement;
  793. const annotations = annotation.state.getAnnotations(this.getToolName(), element);
  794. if (!annotations || annotations.length === 0) {
  795. element.style.cursor = 'default';
  796. return;
  797. }
  798. let isHovering = false;
  799. // 检查是否悬停在文本框上
  800. for (const ann of annotations) {
  801. const customAnn = ann as PolygonLengthMeasurementAnnotation;
  802. const textBoxPosition = customAnn.data.handles.textBox;
  803. if (textBoxPosition && this._isPointInTextBox(canvasCoords, textBoxPosition)) {
  804. element.style.cursor = 'pointer';
  805. customAnn.highlighted = true;
  806. isHovering = true;
  807. break;
  808. }
  809. }
  810. // 如果没有悬停在文本框上,检查是否悬停在手柄或线段上
  811. if (!isHovering) {
  812. for (const ann of annotations) {
  813. const customAnn = ann as PolygonLengthMeasurementAnnotation;
  814. // 检查是否靠近手柄
  815. const handle = this.getHandleNearImagePoint(element, customAnn, canvasCoords, 6);
  816. if (handle) {
  817. element.style.cursor = cursorUrl;
  818. customAnn.highlighted = true;
  819. isHovering = true;
  820. break;
  821. }
  822. // 检查是否靠近线段(包括闭合线段)
  823. if (this.isPointNearTool(element, customAnn, canvasCoords, 10)) {
  824. element.style.cursor = cursorUrl;
  825. customAnn.highlighted = true;
  826. isHovering = true;
  827. break;
  828. }
  829. }
  830. }
  831. // 如果没有悬停在任何地方,重置高亮
  832. if (!isHovering) {
  833. for (const ann of annotations) {
  834. const customAnn = ann as PolygonLengthMeasurementAnnotation;
  835. if (!customAnn.isSelected) {
  836. customAnn.highlighted = false;
  837. }
  838. }
  839. element.style.cursor = 'default';
  840. }
  841. const viewportIdsToRender = utilities.viewportFilters.getViewportIdsWithToolToRender(
  842. element,
  843. this.getToolName()
  844. );
  845. utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
  846. };
  847. /**
  848. * 鼠标按下回调 - 用于修改模式
  849. */
  850. _mouseDownModifyCallback = (evt: EventTypes.InteractionEventType): void => {
  851. const eventDetail = evt.detail;
  852. const { element, currentPoints } = eventDetail;
  853. const canvasCoords = currentPoints.canvas;
  854. const enabledElement = getEnabledElement(element);
  855. if (!enabledElement) {
  856. return;
  857. }
  858. const { viewport } = enabledElement;
  859. const annotations = annotation.state.getAnnotations(this.getToolName(), element);
  860. if (!annotations || annotations.length === 0) {
  861. return;
  862. }
  863. // 先检查是否点击在文本框上
  864. for (const ann of annotations) {
  865. const customAnn = ann as PolygonLengthMeasurementAnnotation;
  866. const textBoxPosition = customAnn.data.handles.textBox;
  867. if (textBoxPosition && this._isPointInTextBox(canvasCoords, textBoxPosition)) {
  868. const viewportIdsToRender =
  869. utilities.viewportFilters.getViewportIdsWithToolToRender(
  870. element,
  871. this.getToolName()
  872. );
  873. const textBoxOffset: CoreTypes.Point2 = [
  874. canvasCoords[0] - textBoxPosition[0],
  875. canvasCoords[1] - textBoxPosition[1],
  876. ];
  877. this.editData = {
  878. annotation: customAnn,
  879. viewportIdsToRender,
  880. hasMoved: false,
  881. textBoxBeingMoved: true,
  882. textBoxOffset: textBoxOffset,
  883. };
  884. customAnn.isSelected = true;
  885. customAnn.highlighted = true;
  886. utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
  887. evt.preventDefault();
  888. evt.stopPropagation();
  889. return;
  890. }
  891. }
  892. // 如果没有点击文本框,再查找最近的手柄
  893. for (const ann of annotations) {
  894. const customAnn = ann as PolygonLengthMeasurementAnnotation;
  895. const handle = this.getHandleNearImagePoint(
  896. element,
  897. customAnn,
  898. canvasCoords,
  899. 6
  900. );
  901. if (handle) {
  902. const viewportIdsToRender =
  903. utilities.viewportFilters.getViewportIdsWithToolToRender(
  904. element,
  905. this.getToolName()
  906. );
  907. this.editData = {
  908. annotation: customAnn,
  909. viewportIdsToRender,
  910. handleIndex: customAnn.data.handles.activeHandleIndex || 0,
  911. hasMoved: false,
  912. };
  913. customAnn.isSelected = true;
  914. customAnn.highlighted = true;
  915. utilities.triggerAnnotationRenderForViewportIds(
  916. viewportIdsToRender
  917. );
  918. evt.preventDefault();
  919. evt.stopPropagation();
  920. return;
  921. }
  922. }
  923. // 如果没有找到手柄,检查是否点击在工具的其他位置(线段上,包括闭合线段)
  924. for (const ann of annotations) {
  925. const customAnn = ann as PolygonLengthMeasurementAnnotation;
  926. if (this.isPointNearTool(element, customAnn, canvasCoords, 10)) {
  927. const viewportIdsToRender =
  928. utilities.viewportFilters.getViewportIdsWithToolToRender(
  929. element,
  930. this.getToolName()
  931. );
  932. const points = customAnn.data.handles.points;
  933. if (points.length >= 2) {
  934. const midPoint = calculateMidpoint(points[0], points[1]);
  935. const midCanvas = viewport.worldToCanvas(midPoint);
  936. const wholeToolOffset: CoreTypes.Point2 = [
  937. canvasCoords[0] - midCanvas[0],
  938. canvasCoords[1] - midCanvas[1],
  939. ];
  940. this.editData = {
  941. annotation: customAnn,
  942. viewportIdsToRender,
  943. hasMoved: false,
  944. movingWholeTool: true,
  945. wholeToolOffset: wholeToolOffset,
  946. };
  947. customAnn.isSelected = true;
  948. customAnn.highlighted = true;
  949. utilities.triggerAnnotationRenderForViewportIds(
  950. viewportIdsToRender
  951. );
  952. evt.preventDefault();
  953. evt.stopPropagation();
  954. return;
  955. }
  956. }
  957. }
  958. // 如果没有点击在工具上,取消所有工具的选中状态
  959. for (const ann of annotations) {
  960. const customAnn = ann as PolygonLengthMeasurementAnnotation;
  961. customAnn.isSelected = false;
  962. customAnn.highlighted = false;
  963. }
  964. const viewportIdsToRender =
  965. utilities.viewportFilters.getViewportIdsWithToolToRender(
  966. element,
  967. this.getToolName()
  968. );
  969. utilities.triggerAnnotationRenderForViewportIds(
  970. viewportIdsToRender
  971. );
  972. };
  973. /**
  974. * 鼠标拖拽回调 - 用于修改模式
  975. */
  976. _mouseDragModifyCallback = (evt: EventTypes.InteractionEventType): void => {
  977. if (!this.editData) {
  978. return;
  979. }
  980. const eventDetail = evt.detail;
  981. const { currentPoints } = eventDetail;
  982. const canvasCoords = currentPoints.canvas;
  983. const enabledElement = getEnabledElement(eventDetail.element);
  984. if (!enabledElement) {
  985. return;
  986. }
  987. const { annotation: ann, viewportIdsToRender, textBoxBeingMoved, movingWholeTool } = this.editData;
  988. const customAnn = ann as PolygonLengthMeasurementAnnotation;
  989. const { data } = customAnn;
  990. // 如果正在拖拽文本框
  991. if (textBoxBeingMoved && this.editData.textBoxOffset) {
  992. const newTextBoxPosition: CoreTypes.Point2 = [
  993. canvasCoords[0] - this.editData.textBoxOffset[0],
  994. canvasCoords[1] - this.editData.textBoxOffset[1],
  995. ];
  996. data.handles.textBox = newTextBoxPosition;
  997. this.editData.hasMoved = true;
  998. utilities.triggerAnnotationRenderForViewportIds(
  999. viewportIdsToRender
  1000. );
  1001. evt.preventDefault();
  1002. evt.stopPropagation();
  1003. return;
  1004. }
  1005. // 如果正在移动整个工具
  1006. if (movingWholeTool && this.editData.wholeToolOffset) {
  1007. const newMidCanvas: CoreTypes.Point2 = [
  1008. canvasCoords[0] - this.editData.wholeToolOffset[0],
  1009. canvasCoords[1] - this.editData.wholeToolOffset[1],
  1010. ];
  1011. const newMidWorld = enabledElement.viewport.canvasToWorld(newMidCanvas);
  1012. const points = data.handles.points;
  1013. if (points.length >= 2) {
  1014. const oldMid = calculateMidpoint(points[0], points[1]);
  1015. const oldMidCanvas = enabledElement.viewport.worldToCanvas(oldMid);
  1016. const offset = vectorSubtract(newMidWorld, oldMid);
  1017. const canvasOffset: CoreTypes.Point2 = [
  1018. newMidCanvas[0] - oldMidCanvas[0],
  1019. newMidCanvas[1] - oldMidCanvas[1],
  1020. ];
  1021. // 移动所有点
  1022. for (let i = 0; i < points.length; i++) {
  1023. points[i] = vectorAdd(points[i], offset);
  1024. }
  1025. // 移动文本框位置 (使用canvas坐标偏移)
  1026. if (data.handles.textBox && Array.isArray(data.handles.textBox) && data.handles.textBox.length >= 2) {
  1027. data.handles.textBox[0] += canvasOffset[0];
  1028. data.handles.textBox[1] += canvasOffset[1];
  1029. }
  1030. this._updateCachedStats(customAnn, enabledElement);
  1031. this.editData.hasMoved = true;
  1032. utilities.triggerAnnotationRenderForViewportIds(
  1033. viewportIdsToRender
  1034. );
  1035. evt.preventDefault();
  1036. evt.stopPropagation();
  1037. return;
  1038. }
  1039. }
  1040. // 否则处理手柄拖拽
  1041. const worldPos = currentPoints.world;
  1042. const activeHandleIndex = data.handles.activeHandleIndex;
  1043. if (activeHandleIndex !== null && activeHandleIndex >= 0 && activeHandleIndex < data.handles.points.length) {
  1044. data.handles.points[activeHandleIndex] = worldPos;
  1045. this._updateCachedStats(customAnn, enabledElement);
  1046. this.editData.hasMoved = true;
  1047. utilities.triggerAnnotationRenderForViewportIds(
  1048. viewportIdsToRender
  1049. );
  1050. evt.preventDefault();
  1051. evt.stopPropagation();
  1052. }
  1053. };
  1054. /**
  1055. * 鼠标释放回调 - 用于修改模式
  1056. */
  1057. _mouseUpModifyCallback = (evt: EventTypes.InteractionEventType): void => {
  1058. if (!this.editData) {
  1059. return;
  1060. }
  1061. const { annotation: ann, hasMoved, movingWholeTool } = this.editData;
  1062. const customAnn = ann as PolygonLengthMeasurementAnnotation;
  1063. customAnn.data.handles.activeHandleIndex = null;
  1064. if (!hasMoved && movingWholeTool) {
  1065. customAnn.isSelected = true;
  1066. customAnn.highlighted = true;
  1067. }
  1068. const eventDetail = evt.detail;
  1069. const { element } = eventDetail;
  1070. const viewportIdsToRender =
  1071. utilities.viewportFilters.getViewportIdsWithToolToRender(
  1072. element,
  1073. this.getToolName()
  1074. );
  1075. utilities.triggerAnnotationRenderForViewportIds(
  1076. viewportIdsToRender
  1077. );
  1078. this.editData = null;
  1079. evt.preventDefault();
  1080. evt.stopPropagation();
  1081. };
  1082. /**
  1083. * 获取靠近图像点的手柄
  1084. */
  1085. getHandleNearImagePoint(
  1086. element: HTMLDivElement,
  1087. annotation: PolygonLengthMeasurementAnnotation,
  1088. canvasCoords: CoreTypes.Point2,
  1089. proximity: number
  1090. ): Types.ToolHandle | undefined {
  1091. const enabledElement = getEnabledElement(element);
  1092. if (!enabledElement) {
  1093. return undefined;
  1094. }
  1095. const { viewport } = enabledElement;
  1096. const points = annotation.data.handles.points;
  1097. const handleProximity = Math.max(proximity, 15);
  1098. for (let i = 0; i < points.length; i++) {
  1099. const point = points[i];
  1100. const canvasPoint = viewport.worldToCanvas(point);
  1101. const distance = Math.sqrt(
  1102. Math.pow(canvasPoint[0] - canvasCoords[0], 2) +
  1103. Math.pow(canvasPoint[1] - canvasCoords[1], 2)
  1104. );
  1105. if (distance < handleProximity) {
  1106. annotation.data.handles.activeHandleIndex = i;
  1107. return {
  1108. worldPosition: point,
  1109. } as Types.ToolHandle;
  1110. }
  1111. }
  1112. annotation.data.handles.activeHandleIndex = null;
  1113. return undefined;
  1114. }
  1115. /**
  1116. * 渲染注解
  1117. */
  1118. renderAnnotation = (
  1119. enabledElement: CoreTypes.IEnabledElement,
  1120. svgDrawingHelper: SVGDrawingHelper
  1121. ): boolean => {
  1122. let renderStatus = false;
  1123. const { viewport } = enabledElement;
  1124. const { element } = viewport;
  1125. let annotations = annotation.state.getAnnotations(this.getToolName(), element);
  1126. if (!annotations?.length) {
  1127. return renderStatus;
  1128. }
  1129. for (let i = 0; i < annotations.length; i++) {
  1130. const annotation = annotations[i] as PolygonLengthMeasurementAnnotation;
  1131. const { annotationUID, data } = annotation;
  1132. const points = data.handles.points;
  1133. // 如果点数为0,跳过
  1134. if (points.length === 0) {
  1135. continue;
  1136. }
  1137. const targetId = this.getTargetId(viewport);
  1138. const cachedStats = targetId ? data.cachedStats?.[targetId] : undefined;
  1139. // 转换所有点为 canvas 坐标
  1140. const canvasPoints = points.map((p) => viewport.worldToCanvas(p));
  1141. const lineOptions = {
  1142. color: 'rgb(0, 255, 0)',
  1143. width: 2,
  1144. };
  1145. // 绘制多边形线段(需要至少2个点)
  1146. if (annotationUID && points.length >= 2) {
  1147. for (let j = 0; j < canvasPoints.length - 1; j++) {
  1148. const lineUID = `${annotationUID}-line-${j}`;
  1149. drawLineSvg(
  1150. svgDrawingHelper,
  1151. annotationUID,
  1152. lineUID,
  1153. canvasPoints[j],
  1154. canvasPoints[j + 1],
  1155. lineOptions
  1156. );
  1157. }
  1158. // 绘制闭合线段(从最后一个点到第一个点)
  1159. if (points.length >= 3) {
  1160. const closingLineUID = `${annotationUID}-closing-line`;
  1161. drawLineSvg(
  1162. svgDrawingHelper,
  1163. annotationUID,
  1164. closingLineUID,
  1165. canvasPoints[canvasPoints.length - 1],
  1166. canvasPoints[0],
  1167. lineOptions
  1168. );
  1169. }
  1170. }
  1171. // 绘制周长文本(需要至少3个点且有统计数据)
  1172. if (cachedStats?.totalLengthMm !== undefined && annotationUID && points.length >= 3) {
  1173. const textLines = [`周长: ${cachedStats.totalLengthMm.toFixed(2)} mm`];
  1174. const textUID = `${annotationUID}-length-text`;
  1175. const textBoxPosition: CoreTypes.Point2 =
  1176. (data.handles.textBox &&
  1177. Array.isArray(data.handles.textBox) &&
  1178. data.handles.textBox.length >= 2)
  1179. ? data.handles.textBox
  1180. : [
  1181. canvasPoints[0][0] + 10,
  1182. canvasPoints[0][1] - 10,
  1183. ];
  1184. console.log('textBoxPosition:', textBoxPosition);
  1185. // 保存文本框位置到注解数据中
  1186. data.handles.textBox = textBoxPosition;
  1187. drawLinkedTextBox(
  1188. svgDrawingHelper,
  1189. annotationUID,
  1190. textUID,
  1191. textLines,
  1192. textBoxPosition,
  1193. [canvasPoints[0] as CoreTypes.Point2],
  1194. {},
  1195. {
  1196. color: 'rgb(0, 255, 0)',
  1197. }
  1198. );
  1199. }
  1200. // 绘制手柄点(至少有1个点就绘制)
  1201. if (annotationUID && points.length >= 1) {
  1202. const handleGroupUID = '0';
  1203. const isSelected = annotation.isSelected || annotation.highlighted;
  1204. const handleRadius = isSelected ? 12 : 8;
  1205. drawHandles(
  1206. svgDrawingHelper,
  1207. annotationUID,
  1208. handleGroupUID,
  1209. canvasPoints,
  1210. {
  1211. color: 'rgb(255, 255, 255)',
  1212. handleRadius: handleRadius,
  1213. }
  1214. );
  1215. }
  1216. renderStatus = true;
  1217. }
  1218. // 绘制预览线(只在绘制模式且有鼠标位置时)
  1219. if (this.isDrawing && this.previewMousePosition && this.currentAnnotation &&
  1220. this.currentAnnotation.data.handles.points.length > 0) {
  1221. const points = this.currentAnnotation.data.handles.points;
  1222. const lastPoint = points[points.length - 1];
  1223. const lastCanvasPoint = enabledElement.viewport.worldToCanvas(lastPoint);
  1224. // 总是绘制从最后一个点到鼠标位置的预览线(用于显示即将添加的下一个线段)
  1225. const previewLineUID = 'preview-line-to-mouse';
  1226. drawLineSvg(
  1227. svgDrawingHelper,
  1228. 'preview', // 使用固定UID
  1229. previewLineUID,
  1230. lastCanvasPoint,
  1231. this.previewMousePosition,
  1232. {
  1233. color: 'rgba(0, 255, 0, 0.7)', // 半透明绿色
  1234. width: 2,
  1235. lineDash: [8, 4], // 虚线样式:8px实线,4px空白
  1236. }
  1237. );
  1238. // 当至少有两个点时,额外绘制从鼠标位置到起始点的预览线(用于预览闭合形状)
  1239. if (points.length >= 2) {
  1240. const firstPoint = points[0];
  1241. const firstCanvasPoint = enabledElement.viewport.worldToCanvas(firstPoint);
  1242. const closingPreviewLineUID = 'preview-line-closing';
  1243. drawLineSvg(
  1244. svgDrawingHelper,
  1245. 'preview', // 使用固定UID
  1246. closingPreviewLineUID,
  1247. this.previewMousePosition,
  1248. firstCanvasPoint,
  1249. {
  1250. color: 'rgba(255, 255, 0, 0.7)', // 半透明黄色,用于区分
  1251. width: 2,
  1252. lineDash: [8, 4], // 虚线样式:8px实线,4px空白
  1253. }
  1254. );
  1255. }
  1256. }
  1257. return renderStatus;
  1258. };
  1259. }