HipNHAAngleMeasurementTool.ts 49 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687
  1. import { Point3, Point2, fitCircleFrom3Points, calculateOppositeVector } 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. vectorSubtract,
  23. vectorAdd,
  24. vectorScale,
  25. getAngleBetweenLines,
  26. degrees,
  27. } from './mathUtils';
  28. import {
  29. PublicToolProps,
  30. ToolProps,
  31. EventTypes,
  32. SVGDrawingHelper,
  33. } from '@cornerstonejs/tools/dist/esm/types';
  34. // 注解数据接口
  35. interface HipNHAAngleMeasurementAnnotation extends Types.Annotation {
  36. data: {
  37. handles: {
  38. points: CoreTypes.Point3[]; // 8个点的世界坐标
  39. activeHandleIndex: number | null; // 当前激活的手柄索引
  40. textBoxLeft?: CoreTypes.Point2; // 左侧角度文本框位置
  41. textBoxRight?: CoreTypes.Point2; // 右侧角度文本框位置
  42. };
  43. cachedStats?: {
  44. [targetId: string]: {
  45. // 左侧圆信息
  46. centerM1?: CoreTypes.Point3; // 左侧圆心M1
  47. radiusLeft?: number; // 左侧半径
  48. // 右侧圆信息
  49. centerM2?: CoreTypes.Point3; // 右侧圆心M2
  50. radiusRight?: number; // 右侧半径
  51. // 角度信息
  52. angleLeft?: number; // 左侧NHA角(度数)
  53. angleRight?: number; // 右侧NHA角(度数)
  54. // 辅助向量(用于绘制角度弧线)
  55. baselineVector?: CoreTypes.Point3; // M1-M2基准线向量
  56. leftTangentVector?: CoreTypes.Point3; // M1-4切线向量
  57. rightTangentVector?: CoreTypes.Point3; // M2-8切线向量
  58. };
  59. };
  60. };
  61. }
  62. // 导出的注解数据接口
  63. export interface ExportedHipNHAAnnotationData {
  64. points: CoreTypes.Point3[]; // 8个点的世界坐标
  65. circles: {
  66. centerM1: CoreTypes.Point3; // 左侧圆心
  67. radiusLeft: number; // 左侧半径
  68. centerM2: CoreTypes.Point3; // 右侧圆心
  69. radiusRight: number; // 右侧半径
  70. };
  71. angles: {
  72. angleLeft: number; // 左侧NHA角
  73. angleRight: number; // 右侧NHA角
  74. };
  75. textBoxPositions?: {
  76. textBoxLeft?: CoreTypes.Point2; // 左侧文本框位置
  77. textBoxRight?: CoreTypes.Point2; // 右侧文本框位置
  78. };
  79. metadata: {
  80. viewPlaneNormal: CoreTypes.Point3;
  81. viewUp: CoreTypes.Point3;
  82. FrameOfReferenceUID: string;
  83. referencedImageId: string;
  84. };
  85. }
  86. export default class HipNHAAngleMeasurementTool extends AnnotationTool {
  87. static toolName = 'HipNHAAngleMeasurementTool';
  88. editData: {
  89. annotation: Types.Annotation;
  90. viewportIdsToRender: string[];
  91. handleIndex?: number;
  92. newAnnotation?: boolean;
  93. hasMoved?: boolean;
  94. textBoxBeingMoved?: 'Left' | 'Right'; // 正在拖拽的文本框
  95. textBoxOffset?: CoreTypes.Point2; // 文本框拖拽时的偏移量
  96. movingWholeTool?: boolean; // 是否正在移动整个工具
  97. wholeToolOffset?: CoreTypes.Point2; // 移动整个工具时的偏移量
  98. selected?: boolean; // 工具选中状态
  99. } | null = null;
  100. isDrawing: boolean = false;
  101. /**
  102. * 创建一个预设的注解,带有8个默认点
  103. * @param element HTML元素
  104. * @param viewport Viewport实例
  105. */
  106. static createDefaultAnnotation(
  107. element: HTMLDivElement,
  108. viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport
  109. ): HipNHAAngleMeasurementAnnotation {
  110. const enabledElement = getEnabledElement(element);
  111. if (!enabledElement) {
  112. throw new Error('Element is not enabled');
  113. }
  114. // 获取viewport的尺寸
  115. const canvas = viewport.canvas;
  116. const { width, height } = canvas;
  117. const centerX = width / 2;
  118. const centerY = height / 2;
  119. // 左侧圆:圆心在中心左侧150px,半径60px
  120. const leftCircleCenter: CoreTypes.Point2 = [centerX - 150, centerY];
  121. const leftRadius = 60;
  122. // 右侧圆:圆心在中心右侧150px,半径60px
  123. const rightCircleCenter: CoreTypes.Point2 = [centerX + 150, centerY];
  124. const rightRadius = 60;
  125. // 生成左侧圆上的3个点(120度间隔)
  126. const leftPoint1: CoreTypes.Point2 = [
  127. leftCircleCenter[0] + leftRadius * Math.cos(0),
  128. leftCircleCenter[1] + leftRadius * Math.sin(0)
  129. ];
  130. const leftPoint2: CoreTypes.Point2 = [
  131. leftCircleCenter[0] + leftRadius * Math.cos(Math.PI * 2 / 3),
  132. leftCircleCenter[1] + leftRadius * Math.sin(Math.PI * 2 / 3)
  133. ];
  134. const leftPoint3: CoreTypes.Point2 = [
  135. leftCircleCenter[0] + leftRadius * Math.cos(Math.PI * 4 / 3),
  136. leftCircleCenter[1] + leftRadius * Math.sin(Math.PI * 4 / 3)
  137. ];
  138. // 左侧髋臼边缘点(在圆心上方)
  139. const leftAcetabularPoint: CoreTypes.Point2 = [
  140. leftCircleCenter[0] - 20,
  141. leftCircleCenter[1] - 80
  142. ];
  143. // 生成右侧圆上的3个点(120度间隔)
  144. const rightPoint1: CoreTypes.Point2 = [
  145. rightCircleCenter[0] + rightRadius * Math.cos(0),
  146. rightCircleCenter[1] + rightRadius * Math.sin(0)
  147. ];
  148. const rightPoint2: CoreTypes.Point2 = [
  149. rightCircleCenter[0] + rightRadius * Math.cos(Math.PI * 2 / 3),
  150. rightCircleCenter[1] + rightRadius * Math.sin(Math.PI * 2 / 3)
  151. ];
  152. const rightPoint3: CoreTypes.Point2 = [
  153. rightCircleCenter[0] + rightRadius * Math.cos(Math.PI * 4 / 3),
  154. rightCircleCenter[1] + rightRadius * Math.sin(Math.PI * 4 / 3)
  155. ];
  156. // 右侧髋臼边缘点(在圆心上方)
  157. const rightAcetabularPoint: CoreTypes.Point2 = [
  158. rightCircleCenter[0] + 20,
  159. rightCircleCenter[1] - 80
  160. ];
  161. const canvasPoints: CoreTypes.Point2[] = [
  162. leftPoint1, // 点0: 左圆点1
  163. leftPoint2, // 点1: 左圆点2
  164. leftPoint3, // 点2: 左圆点3
  165. leftAcetabularPoint, // 点3: 左侧髋臼边缘点
  166. rightPoint1, // 点4: 右圆点1
  167. rightPoint2, // 点5: 右圆点2
  168. rightPoint3, // 点6: 右圆点3
  169. rightAcetabularPoint, // 点7: 右侧髋臼边缘点
  170. ];
  171. // 转换为world坐标
  172. const worldPoints = canvasPoints.map((canvasPoint) =>
  173. viewport.canvasToWorld(canvasPoint)
  174. );
  175. const camera = 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. const annotationData = {
  184. invalidated: true,
  185. highlighted: false,
  186. metadata: {
  187. viewPlaneNormal: [...viewPlaneNormal] as CoreTypes.Point3,
  188. viewUp: [...viewUp] as CoreTypes.Point3,
  189. FrameOfReferenceUID: viewport.getFrameOfReferenceUID(),
  190. referencedImageId: viewport.getCurrentImageId?.() || '',
  191. toolName: HipNHAAngleMeasurementTool.toolName,
  192. },
  193. data: {
  194. label: '',
  195. handles: {
  196. points: worldPoints,
  197. activeHandleIndex: null,
  198. },
  199. cachedStats: {},
  200. },
  201. } as HipNHAAngleMeasurementAnnotation;
  202. return annotationData;
  203. }
  204. /**
  205. * 导出注解数据
  206. * @param annotation 要导出的注解
  207. * @param viewport Viewport实例
  208. * @returns 导出的数据对象
  209. */
  210. static exportAnnotationData(
  211. annotation: HipNHAAngleMeasurementAnnotation,
  212. viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport
  213. ): ExportedHipNHAAnnotationData | null {
  214. const targetId = `imageId:${viewport.getCurrentImageId?.() || ''}`;
  215. const cachedStats = annotation.data.cachedStats?.[targetId];
  216. if (!cachedStats || !cachedStats.centerM1 || !cachedStats.centerM2) {
  217. return null;
  218. }
  219. return {
  220. points: [...annotation.data.handles.points],
  221. circles: {
  222. centerM1: cachedStats.centerM1,
  223. radiusLeft: cachedStats.radiusLeft || 0,
  224. centerM2: cachedStats.centerM2,
  225. radiusRight: cachedStats.radiusRight || 0,
  226. },
  227. angles: {
  228. angleLeft: cachedStats.angleLeft || 0,
  229. angleRight: cachedStats.angleRight || 0,
  230. },
  231. textBoxPositions: {
  232. textBoxLeft: annotation.data.handles.textBoxLeft,
  233. textBoxRight: annotation.data.handles.textBoxRight,
  234. },
  235. metadata: {
  236. viewPlaneNormal: annotation.metadata?.viewPlaneNormal || [0, 0, 1],
  237. viewUp: annotation.metadata?.viewUp || [0, 1, 0],
  238. FrameOfReferenceUID: annotation.metadata?.FrameOfReferenceUID || '',
  239. referencedImageId: annotation.metadata?.referencedImageId || '',
  240. },
  241. };
  242. }
  243. /**
  244. * 从导出的数据恢复注解
  245. * @param exportedData 导出的数据
  246. * @param element HTML元素
  247. * @param viewport Viewport实例
  248. * @returns 恢复的注解
  249. */
  250. static restoreFromExportedData(
  251. exportedData: ExportedHipNHAAnnotationData,
  252. element: HTMLDivElement,
  253. viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport
  254. ): HipNHAAngleMeasurementAnnotation {
  255. const enabledElement = getEnabledElement(element);
  256. if (!enabledElement) {
  257. throw new Error('Element is not enabled');
  258. }
  259. const annotationData = {
  260. invalidated: true,
  261. highlighted: false,
  262. metadata: {
  263. ...exportedData.metadata,
  264. toolName: HipNHAAngleMeasurementTool.toolName,
  265. },
  266. data: {
  267. label: '',
  268. handles: {
  269. points: [...exportedData.points],
  270. activeHandleIndex: null,
  271. textBoxLeft: exportedData.textBoxPositions?.textBoxLeft,
  272. textBoxRight: exportedData.textBoxPositions?.textBoxRight,
  273. },
  274. cachedStats: {},
  275. },
  276. } as HipNHAAngleMeasurementAnnotation;
  277. return annotationData;
  278. }
  279. constructor(
  280. toolProps: PublicToolProps = {},
  281. defaultToolProps: ToolProps = {
  282. supportedInteractionTypes: ['Mouse', 'Touch'],
  283. configuration: {
  284. shadow: true,
  285. preventHandleOutsideImage: false,
  286. },
  287. }
  288. ) {
  289. super(toolProps, defaultToolProps);
  290. }
  291. /**
  292. * 设置注解选中状态
  293. * @param annotation 要设置的注解
  294. * @param selected 选中状态
  295. */
  296. private _setAnnotationSelected(annotation: HipNHAAngleMeasurementAnnotation, selected: boolean): void {
  297. annotation.isSelected = selected;
  298. if (selected) {
  299. annotation.highlighted = true;
  300. }
  301. }
  302. /**
  303. * 添加新注解 - 禁用此功能
  304. */
  305. addNewAnnotation(
  306. evt: EventTypes.InteractionEventType
  307. ): HipNHAAngleMeasurementAnnotation {
  308. evt.preventDefault();
  309. return {} as HipNHAAngleMeasurementAnnotation;
  310. }
  311. /**
  312. * 计算点到线段的距离
  313. */
  314. private _distanceToSegment(
  315. point: CoreTypes.Point2,
  316. lineStart: CoreTypes.Point2,
  317. lineEnd: CoreTypes.Point2
  318. ): number {
  319. const x = point[0];
  320. const y = point[1];
  321. const x1 = lineStart[0];
  322. const y1 = lineStart[1];
  323. const x2 = lineEnd[0];
  324. const y2 = lineEnd[1];
  325. const A = x - x1;
  326. const B = y - y1;
  327. const C = x2 - x1;
  328. const D = y2 - y1;
  329. const dot = A * C + B * D;
  330. const lenSq = C * C + D * D;
  331. let param = -1;
  332. if (lenSq !== 0) {
  333. param = dot / lenSq;
  334. }
  335. let xx, yy;
  336. if (param < 0) {
  337. xx = x1;
  338. yy = y1;
  339. } else if (param > 1) {
  340. xx = x2;
  341. yy = y2;
  342. } else {
  343. xx = x1 + param * C;
  344. yy = y1 + param * D;
  345. }
  346. const dx = x - xx;
  347. const dy = y - yy;
  348. return Math.sqrt(dx * dx + dy * dy);
  349. }
  350. /**
  351. * 检查点是否靠近工具
  352. */
  353. isPointNearTool(
  354. element: HTMLDivElement,
  355. annotation: HipNHAAngleMeasurementAnnotation,
  356. canvasCoords: CoreTypes.Point2,
  357. proximity: number
  358. ): boolean {
  359. const enabledElement = getEnabledElement(element);
  360. if (!enabledElement) {
  361. return false;
  362. }
  363. const { viewport } = enabledElement;
  364. const points = annotation.data.handles.points;
  365. //检查是否在文本框上
  366. if (this._isPointInTextBox(canvasCoords, annotation.data.handles.textBoxLeft || [0, 0]) ||
  367. this._isPointInTextBox(canvasCoords, annotation.data.handles.textBoxRight || [0, 0])) {
  368. return true;
  369. }
  370. // 检查是否靠近任意一个手柄点
  371. for (let i = 0; i < points.length; i++) {
  372. const point = points[i];
  373. const canvasPoint = viewport.worldToCanvas(point);
  374. const distance = Math.sqrt(
  375. Math.pow(canvasPoint[0] - canvasCoords[0], 2) +
  376. Math.pow(canvasPoint[1] - canvasCoords[1], 2)
  377. );
  378. if (distance < proximity) {
  379. return true;
  380. }
  381. }
  382. // 检查是否靠近线段
  383. if (points.length >= 8) {
  384. const targetId = this.getTargetId(viewport);
  385. const cachedStats = targetId ? annotation.data.cachedStats?.[targetId] : undefined;
  386. if (cachedStats) {
  387. // 检查基准线 M1-M2
  388. if (cachedStats.centerM1 && cachedStats.centerM2) {
  389. const centerM1Canvas = viewport.worldToCanvas(cachedStats.centerM1);
  390. const centerM2Canvas = viewport.worldToCanvas(cachedStats.centerM2);
  391. const dist = this._distanceToSegment(canvasCoords, centerM1Canvas, centerM2Canvas);
  392. if (dist < proximity) {
  393. return true;
  394. }
  395. }
  396. // 检查左侧切线 M1-point3
  397. if (cachedStats.centerM1 && points.length > 3) {
  398. const centerM1Canvas = viewport.worldToCanvas(cachedStats.centerM1);
  399. const point3Canvas = viewport.worldToCanvas(points[3]);
  400. const dist = this._distanceToSegment(canvasCoords, centerM1Canvas, point3Canvas);
  401. if (dist < proximity) {
  402. return true;
  403. }
  404. }
  405. // 检查右侧切线 M2-point7
  406. if (cachedStats.centerM2 && points.length > 7) {
  407. const centerM2Canvas = viewport.worldToCanvas(cachedStats.centerM2);
  408. const point7Canvas = viewport.worldToCanvas(points[7]);
  409. const dist = this._distanceToSegment(canvasCoords, centerM2Canvas, point7Canvas);
  410. if (dist < proximity) {
  411. return true;
  412. }
  413. }
  414. }
  415. }
  416. return false;
  417. }
  418. /**
  419. * 检查点击是否在文本框内
  420. */
  421. private _isPointInTextBox(
  422. canvasPoint: CoreTypes.Point2,
  423. textBoxPosition: CoreTypes.Point2,
  424. textWidth: number = 120,
  425. textHeight: number = 40
  426. ): boolean {
  427. const [x, y] = canvasPoint;
  428. const [tx, ty] = textBoxPosition;
  429. const margin = 10;
  430. return (
  431. x >= tx - margin &&
  432. x <= tx + textWidth + margin &&
  433. y >= ty - margin &&
  434. y <= ty + textHeight + margin
  435. );
  436. }
  437. /**
  438. * 创建角度弧线的点数组
  439. */
  440. private _createAngleArcPoints(
  441. center: CoreTypes.Point2,
  442. vector1: CoreTypes.Point2,
  443. vector2: CoreTypes.Point2,
  444. radius: number
  445. ): CoreTypes.Point2[] {
  446. const angle1 = Math.atan2(vector1[1], vector1[0]);
  447. const angle2 = Math.atan2(vector2[1], vector2[0]);
  448. let startAngle = angle1;
  449. let endAngle = angle2;
  450. let angleDiff = endAngle - startAngle;
  451. if (angleDiff > Math.PI) {
  452. angleDiff -= 2 * Math.PI;
  453. } else if (angleDiff < -Math.PI) {
  454. angleDiff += 2 * Math.PI;
  455. }
  456. if (angleDiff < 0) {
  457. [startAngle, endAngle] = [endAngle, startAngle];
  458. angleDiff = -angleDiff;
  459. }
  460. const numPoints = Math.max(10, Math.ceil(Math.abs(angleDiff) / (Math.PI / 18)));
  461. const points: CoreTypes.Point2[] = [];
  462. for (let i = 0; i <= numPoints; i++) {
  463. const t = i / numPoints;
  464. const angle = startAngle + angleDiff * t;
  465. const x = center[0] + radius * Math.cos(angle);
  466. const y = center[1] + radius * Math.sin(angle);
  467. points.push([x, y]);
  468. }
  469. return points;
  470. }
  471. /**
  472. * 取消操作
  473. */
  474. cancel(element: HTMLDivElement): string {
  475. if (this.isDrawing) {
  476. this.isDrawing = false;
  477. this._deactivateDraw(element);
  478. this._deactivateModify(element);
  479. const enabledElement = getEnabledElement(element);
  480. if (enabledElement) {
  481. const viewportIdsToRender =
  482. utilities.viewportFilters.getViewportIdsWithToolToRender(
  483. element,
  484. this.getToolName()
  485. );
  486. utilities.triggerAnnotationRenderForViewportIds(
  487. viewportIdsToRender
  488. );
  489. }
  490. this.editData = null;
  491. return this.getToolName();
  492. }
  493. return '';
  494. }
  495. /**
  496. * 激活绘制模式
  497. */
  498. _activateDraw(element: HTMLDivElement): void {
  499. element.addEventListener(
  500. 'CORNERSTONE_TOOLS_MOUSE_DRAG',
  501. this._dragCallback as EventListener
  502. );
  503. element.addEventListener(
  504. 'CORNERSTONE_TOOLS_MOUSE_UP',
  505. this._endCallback as EventListener
  506. );
  507. element.addEventListener(
  508. 'CORNERSTONE_TOOLS_MOUSE_CLICK',
  509. this._endCallback as EventListener
  510. );
  511. }
  512. /**
  513. * 取消激活绘制模式
  514. */
  515. _deactivateDraw(element: HTMLDivElement): void {
  516. element.removeEventListener(
  517. 'CORNERSTONE_TOOLS_MOUSE_DRAG',
  518. this._dragCallback as EventListener
  519. );
  520. element.removeEventListener(
  521. 'CORNERSTONE_TOOLS_MOUSE_UP',
  522. this._endCallback as EventListener
  523. );
  524. element.removeEventListener(
  525. 'CORNERSTONE_TOOLS_MOUSE_CLICK',
  526. this._endCallback as EventListener
  527. );
  528. }
  529. /**
  530. * 激活修改模式
  531. */
  532. _activateModify(element: HTMLDivElement): void {
  533. element.addEventListener(
  534. 'CORNERSTONE_TOOLS_MOUSE_DOWN',
  535. this._mouseDownModifyCallback as EventListener
  536. );
  537. element.addEventListener(
  538. 'CORNERSTONE_TOOLS_MOUSE_DRAG',
  539. this._mouseDragModifyCallback as EventListener
  540. );
  541. element.addEventListener(
  542. 'CORNERSTONE_TOOLS_MOUSE_UP',
  543. this._mouseUpModifyCallback as EventListener
  544. );
  545. element.addEventListener(
  546. 'CORNERSTONE_TOOLS_MOUSE_MOVE',
  547. this._mouseMoveModifyCallback as EventListener
  548. );
  549. element.addEventListener(
  550. 'keydown',
  551. this._keyDownCallback as EventListener
  552. );
  553. }
  554. /**
  555. * 取消激活修改模式
  556. */
  557. _deactivateModify(element: HTMLDivElement): void {
  558. element.removeEventListener(
  559. 'CORNERSTONE_TOOLS_MOUSE_DOWN',
  560. this._mouseDownModifyCallback as EventListener
  561. );
  562. element.removeEventListener(
  563. 'CORNERSTONE_TOOLS_MOUSE_DRAG',
  564. this._mouseDragModifyCallback as EventListener
  565. );
  566. element.removeEventListener(
  567. 'CORNERSTONE_TOOLS_MOUSE_UP',
  568. this._mouseUpModifyCallback as EventListener
  569. );
  570. element.removeEventListener(
  571. 'CORNERSTONE_TOOLS_MOUSE_MOVE',
  572. this._mouseMoveModifyCallback as EventListener
  573. );
  574. element.removeEventListener(
  575. 'keydown',
  576. this._keyDownCallback as EventListener
  577. );
  578. }
  579. /**
  580. * 鼠标按下回调 - 用于修改模式
  581. */
  582. _mouseDownModifyCallback = (evt: EventTypes.InteractionEventType): void => {
  583. const eventDetail = evt.detail;
  584. const { element, currentPoints } = eventDetail;
  585. const canvasCoords = currentPoints.canvas;
  586. const enabledElement = getEnabledElement(element);
  587. if (!enabledElement) {
  588. return;
  589. }
  590. const { viewport } = enabledElement;
  591. const annotations = annotation.state.getAnnotations(this.getToolName(), element);
  592. if (!annotations || annotations.length === 0) {
  593. return;
  594. }
  595. // 先检查是否点击在文本框上
  596. for (const ann of annotations) {
  597. const customAnn = ann as HipNHAAngleMeasurementAnnotation;
  598. const targetId = this.getTargetId(viewport);
  599. const cachedStats = targetId ? customAnn.data.cachedStats?.[targetId] : undefined;
  600. if (cachedStats?.centerM1) {
  601. const centerM1Canvas = viewport.worldToCanvas(cachedStats.centerM1);
  602. const textBoxLeft: CoreTypes.Point2 = customAnn.data.handles.textBoxLeft || [
  603. centerM1Canvas[0] - 60,
  604. centerM1Canvas[1] - 50,
  605. ];
  606. if (this._isPointInTextBox(canvasCoords, textBoxLeft)) {
  607. const viewportIdsToRender =
  608. utilities.viewportFilters.getViewportIdsWithToolToRender(
  609. element,
  610. this.getToolName()
  611. );
  612. const textBoxOffset: CoreTypes.Point2 = [
  613. canvasCoords[0] - textBoxLeft[0],
  614. canvasCoords[1] - textBoxLeft[1],
  615. ];
  616. this.editData = {
  617. annotation: customAnn,
  618. viewportIdsToRender,
  619. hasMoved: false,
  620. textBoxBeingMoved: 'Left',
  621. textBoxOffset: textBoxOffset,
  622. };
  623. customAnn.highlighted = true;
  624. utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
  625. evt.preventDefault();
  626. evt.stopPropagation();
  627. return;
  628. }
  629. }
  630. if (cachedStats?.centerM2) {
  631. const centerM2Canvas = viewport.worldToCanvas(cachedStats.centerM2);
  632. const textBoxRight: CoreTypes.Point2 = customAnn.data.handles.textBoxRight || [
  633. centerM2Canvas[0] + 20,
  634. centerM2Canvas[1] - 50,
  635. ];
  636. if (this._isPointInTextBox(canvasCoords, textBoxRight)) {
  637. const viewportIdsToRender =
  638. utilities.viewportFilters.getViewportIdsWithToolToRender(
  639. element,
  640. this.getToolName()
  641. );
  642. const textBoxOffset: CoreTypes.Point2 = [
  643. canvasCoords[0] - textBoxRight[0],
  644. canvasCoords[1] - textBoxRight[1],
  645. ];
  646. this.editData = {
  647. annotation: customAnn,
  648. viewportIdsToRender,
  649. hasMoved: false,
  650. textBoxBeingMoved: 'Right',
  651. textBoxOffset: textBoxOffset,
  652. };
  653. customAnn.highlighted = true;
  654. utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
  655. evt.preventDefault();
  656. evt.stopPropagation();
  657. return;
  658. }
  659. }
  660. }
  661. // 如果没有点击文本框,再查找最近的手柄
  662. for (const ann of annotations) {
  663. const customAnn = ann as HipNHAAngleMeasurementAnnotation;
  664. const handle = this.getHandleNearImagePoint(
  665. element,
  666. customAnn,
  667. canvasCoords,
  668. 6
  669. );
  670. if (handle) {
  671. const viewportIdsToRender =
  672. utilities.viewportFilters.getViewportIdsWithToolToRender(
  673. element,
  674. this.getToolName()
  675. );
  676. // 选中工具
  677. this._setAnnotationSelected(customAnn, true);
  678. this.editData = {
  679. annotation: customAnn,
  680. viewportIdsToRender,
  681. handleIndex: customAnn.data.handles.activeHandleIndex || 0,
  682. hasMoved: false,
  683. };
  684. customAnn.highlighted = true;
  685. utilities.triggerAnnotationRenderForViewportIds(
  686. viewportIdsToRender
  687. );
  688. evt.preventDefault();
  689. evt.stopPropagation();
  690. return;
  691. }
  692. }
  693. // 如果没有找到手柄,检查是否点击在线段上(开始移动整个工具)
  694. for (const ann of annotations) {
  695. const customAnn = ann as HipNHAAngleMeasurementAnnotation;
  696. if (this.isPointNearTool(element, customAnn, canvasCoords, 10)) {
  697. const viewportIdsToRender =
  698. utilities.viewportFilters.getViewportIdsWithToolToRender(
  699. element,
  700. this.getToolName()
  701. );
  702. // 选中工具
  703. this._setAnnotationSelected(customAnn, true);
  704. // 计算鼠标点击位置相对于工具的偏移量
  705. const points = customAnn.data.handles.points;
  706. if (points.length >= 8) {
  707. const midPoint = vectorAdd(points[0], vectorScale(vectorSubtract(points[1], points[0]), 0.5));
  708. const midCanvas = viewport.worldToCanvas(midPoint);
  709. const wholeToolOffset: CoreTypes.Point2 = [
  710. canvasCoords[0] - midCanvas[0],
  711. canvasCoords[1] - midCanvas[1],
  712. ];
  713. this.editData = {
  714. annotation: customAnn,
  715. viewportIdsToRender,
  716. hasMoved: false,
  717. movingWholeTool: true,
  718. wholeToolOffset: wholeToolOffset,
  719. };
  720. customAnn.highlighted = true;
  721. utilities.triggerAnnotationRenderForViewportIds(
  722. viewportIdsToRender
  723. );
  724. evt.preventDefault();
  725. evt.stopPropagation();
  726. return;
  727. }
  728. }
  729. }
  730. };
  731. /**
  732. * 鼠标拖拽回调 - 用于修改模式
  733. */
  734. _mouseDragModifyCallback = (evt: EventTypes.InteractionEventType): void => {
  735. if (!this.editData) {
  736. return;
  737. }
  738. const eventDetail = evt.detail;
  739. const { currentPoints } = eventDetail;
  740. const canvasCoords = currentPoints.canvas;
  741. const enabledElement = getEnabledElement(eventDetail.element);
  742. if (!enabledElement) {
  743. return;
  744. }
  745. const { annotation: ann, viewportIdsToRender, textBoxBeingMoved, movingWholeTool } = this.editData;
  746. const customAnn = ann as HipNHAAngleMeasurementAnnotation;
  747. const { data } = customAnn;
  748. // 如果正在拖拽文本框
  749. if (textBoxBeingMoved && this.editData.textBoxOffset) {
  750. const newTextBoxPosition: CoreTypes.Point2 = [
  751. canvasCoords[0] - this.editData.textBoxOffset[0],
  752. canvasCoords[1] - this.editData.textBoxOffset[1],
  753. ];
  754. if (textBoxBeingMoved === 'Left') {
  755. data.handles.textBoxLeft = newTextBoxPosition;
  756. } else if (textBoxBeingMoved === 'Right') {
  757. data.handles.textBoxRight = newTextBoxPosition;
  758. }
  759. this.editData.hasMoved = true;
  760. utilities.triggerAnnotationRenderForViewportIds(
  761. viewportIdsToRender
  762. );
  763. evt.preventDefault();
  764. evt.stopPropagation();
  765. return;
  766. }
  767. // 如果正在移动整个工具
  768. if (movingWholeTool && this.editData.wholeToolOffset) {
  769. // 计算新的中点位置
  770. const newMidCanvas: CoreTypes.Point2 = [
  771. canvasCoords[0] - this.editData.wholeToolOffset[0],
  772. canvasCoords[1] - this.editData.wholeToolOffset[1],
  773. ];
  774. const newMidWorld = enabledElement.viewport.canvasToWorld(newMidCanvas);
  775. // 计算所有点的偏移量
  776. const points = data.handles.points;
  777. if (points.length >= 8) {
  778. const oldMid = vectorAdd(points[0], vectorScale(vectorSubtract(points[1], points[0]), 0.5));
  779. const offset = vectorSubtract(newMidWorld, oldMid);
  780. // 移动所有点
  781. for (let i = 0; i < points.length; i++) {
  782. points[i] = vectorAdd(points[i], offset);
  783. }
  784. // 移动文本框位置(使用相同的偏移量)
  785. if (data.handles.textBoxLeft) {
  786. data.handles.textBoxLeft = [
  787. data.handles.textBoxLeft[0] + offset[0],
  788. data.handles.textBoxLeft[1] + offset[1]
  789. ];
  790. }
  791. if (data.handles.textBoxRight) {
  792. data.handles.textBoxRight = [
  793. data.handles.textBoxRight[0] + offset[0],
  794. data.handles.textBoxRight[1] + offset[1]
  795. ];
  796. }
  797. // 重新计算角度
  798. this._updateCachedStats(customAnn, enabledElement);
  799. this.editData.hasMoved = true;
  800. // 触发渲染更新
  801. utilities.triggerAnnotationRenderForViewportIds(
  802. viewportIdsToRender
  803. );
  804. evt.preventDefault();
  805. evt.stopPropagation();
  806. return;
  807. }
  808. }
  809. // 否则处理手柄拖拽
  810. const worldPos = currentPoints.world;
  811. const activeHandleIndex = data.handles.activeHandleIndex;
  812. if (activeHandleIndex !== null && activeHandleIndex >= 0 && activeHandleIndex < data.handles.points.length) {
  813. data.handles.points[activeHandleIndex] = worldPos;
  814. this._updateCachedStats(customAnn, enabledElement);
  815. this.editData.hasMoved = true;
  816. utilities.triggerAnnotationRenderForViewportIds(
  817. viewportIdsToRender
  818. );
  819. evt.preventDefault();
  820. evt.stopPropagation();
  821. }
  822. };
  823. /**
  824. * 鼠标释放回调 - 用于修改模式
  825. */
  826. _mouseUpModifyCallback = (evt: EventTypes.InteractionEventType): void => {
  827. if (!this.editData) {
  828. return;
  829. }
  830. const eventDetail = evt.detail;
  831. const { element, currentPoints } = eventDetail;
  832. const canvasCoords = currentPoints.canvas;
  833. const enabledElement = getEnabledElement(element);
  834. if (!enabledElement) {
  835. return;
  836. }
  837. const { viewport } = enabledElement;
  838. const { annotation: ann, hasMoved, movingWholeTool } = this.editData;
  839. const customAnn = ann as HipNHAAngleMeasurementAnnotation;
  840. customAnn.data.handles.activeHandleIndex = null;
  841. // 如果没有拖拽且点击在线段上(movingWholeTool为true),则进入选中状态
  842. if (!hasMoved && movingWholeTool) {
  843. // 设置选中状态:使用注解的selected字段
  844. this._setAnnotationSelected(customAnn, true);
  845. // 保持高亮以显示选中
  846. customAnn.highlighted = true;
  847. } else {
  848. customAnn.highlighted = false;
  849. }
  850. // 如果是简单点击(没有移动),检查是否点击到其他位置以取消选中
  851. if (!hasMoved) {
  852. const annotations = annotation.state.getAnnotations(this.getToolName(), element);
  853. // 检查是否点击到任何工具部分
  854. let clickedOnTool = false;
  855. for (const ann of annotations) {
  856. const customAnn = ann as HipNHAAngleMeasurementAnnotation;
  857. // 检查点击位置是否在工具范围内
  858. if (this.isPointNearTool(element, customAnn, canvasCoords, 25)) {
  859. clickedOnTool = true;
  860. break;
  861. }
  862. // 检查是否在文本框范围内
  863. const targetId = this.getTargetId(viewport);
  864. const cachedStats = targetId ? customAnn.data.cachedStats?.[targetId] : undefined;
  865. if (cachedStats?.centerM1) {
  866. const centerM1Canvas = viewport.worldToCanvas(cachedStats.centerM1);
  867. const textBoxLeft: CoreTypes.Point2 = customAnn.data.handles.textBoxLeft || [
  868. centerM1Canvas[0] - 60,
  869. centerM1Canvas[1] - 50,
  870. ];
  871. if (this._isPointInTextBox(canvasCoords, textBoxLeft)) {
  872. clickedOnTool = true;
  873. break;
  874. }
  875. }
  876. if (cachedStats?.centerM2) {
  877. const centerM2Canvas = viewport.worldToCanvas(cachedStats.centerM2);
  878. const textBoxRight: CoreTypes.Point2 = customAnn.data.handles.textBoxRight || [
  879. centerM2Canvas[0] + 20,
  880. centerM2Canvas[1] - 50,
  881. ];
  882. if (this._isPointInTextBox(canvasCoords, textBoxRight)) {
  883. clickedOnTool = true;
  884. break;
  885. }
  886. }
  887. }
  888. // 如果没有点击到任何工具,清除所有选中状态
  889. if (!clickedOnTool) {
  890. for (const ann of annotations) {
  891. const customAnn = ann as HipNHAAngleMeasurementAnnotation;
  892. this._setAnnotationSelected(customAnn, false);
  893. customAnn.highlighted = false;
  894. }
  895. }
  896. }
  897. const viewportIdsToRender =
  898. utilities.viewportFilters.getViewportIdsWithToolToRender(
  899. element,
  900. this.getToolName()
  901. );
  902. utilities.triggerAnnotationRenderForViewportIds(
  903. viewportIdsToRender
  904. );
  905. this.editData = null;
  906. evt.preventDefault();
  907. evt.stopPropagation();
  908. };
  909. /**
  910. * 鼠标移动回调 - 用于修改模式,处理悬停检测
  911. * 实现功能1和2:鼠标悬停在文本上时光标为手型,悬停在线段或手柄上时光标为十字准星
  912. */
  913. _mouseMoveModifyCallback = (evt: EventTypes.InteractionEventType): void => {
  914. const eventDetail = evt.detail;
  915. const { element, currentPoints } = eventDetail;
  916. // 添加空值检查
  917. if (!currentPoints || !currentPoints.canvas) {
  918. return;
  919. }
  920. const canvasCoords = currentPoints.canvas;
  921. const enabledElement = getEnabledElement(element);
  922. if (!enabledElement) {
  923. return;
  924. }
  925. const { viewport } = enabledElement;
  926. const annotations = annotation.state.getAnnotations(this.getToolName(), element);
  927. if (!annotations || annotations.length === 0) {
  928. // 没有注解时,重置光标
  929. element.style.cursor = 'default';
  930. return;
  931. }
  932. let isHovering = false;
  933. // 检查是否悬停在文本框上
  934. for (const ann of annotations) {
  935. const customAnn = ann as HipNHAAngleMeasurementAnnotation;
  936. const targetId = this.getTargetId(viewport);
  937. const cachedStats = targetId ? customAnn.data.cachedStats?.[targetId] : undefined;
  938. if (cachedStats?.centerM1) {
  939. const centerM1Canvas = viewport.worldToCanvas(cachedStats.centerM1);
  940. const textBoxLeft: CoreTypes.Point2 = customAnn.data.handles.textBoxLeft || [
  941. centerM1Canvas[0] - 60,
  942. centerM1Canvas[1] - 50,
  943. ];
  944. if (this._isPointInTextBox(canvasCoords, textBoxLeft)) {
  945. element.style.cursor = 'pointer'; // 手型光标
  946. customAnn.highlighted = true;
  947. isHovering = true;
  948. break;
  949. }
  950. }
  951. if (cachedStats?.centerM2) {
  952. const centerM2Canvas = viewport.worldToCanvas(cachedStats.centerM2);
  953. const textBoxRight: CoreTypes.Point2 = customAnn.data.handles.textBoxRight || [
  954. centerM2Canvas[0] + 20,
  955. centerM2Canvas[1] - 50,
  956. ];
  957. if (this._isPointInTextBox(canvasCoords, textBoxRight)) {
  958. element.style.cursor = 'pointer'; // 手型光标
  959. customAnn.highlighted = true;
  960. isHovering = true;
  961. break;
  962. }
  963. }
  964. }
  965. // 如果没有悬停在文本框上,检查是否悬停在手柄或线段上
  966. if (!isHovering) {
  967. for (const ann of annotations) {
  968. const customAnn = ann as HipNHAAngleMeasurementAnnotation;
  969. // 检查是否靠近手柄
  970. const handle = this.getHandleNearImagePoint(element, customAnn, canvasCoords, 6);
  971. if (handle) {
  972. element.style.cursor = 'crosshair'; // 十字准星光标
  973. customAnn.highlighted = true;
  974. isHovering = true;
  975. break;
  976. }
  977. // 检查是否靠近线段(圆弧路径)
  978. if (this.isPointNearTool(element, customAnn, canvasCoords, 10)) {
  979. element.style.cursor = 'crosshair'; // 十字准星光标
  980. customAnn.highlighted = true;
  981. isHovering = true;
  982. break;
  983. }
  984. }
  985. }
  986. // 如果没有悬停在任何地方,重置高亮(但保留选中状态)
  987. if (!isHovering) {
  988. for (const ann of annotations) {
  989. const customAnn = ann as HipNHAAngleMeasurementAnnotation;
  990. // 只有未选中的工具才重置 highlighted
  991. // 已选中的工具保持 highlighted = true
  992. if (!customAnn.isSelected) {
  993. customAnn.highlighted = false;
  994. }
  995. }
  996. element.style.cursor = 'default';
  997. }
  998. // 触发渲染以更新高亮状态
  999. const viewportIdsToRender = utilities.viewportFilters.getViewportIdsWithToolToRender(
  1000. element,
  1001. this.getToolName()
  1002. );
  1003. utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
  1004. };
  1005. /**
  1006. * 键盘按下回调 - 处理Delete键删除选中注解
  1007. */
  1008. _keyDownCallback = (evt: KeyboardEvent): void => {
  1009. const element = evt.currentTarget as HTMLDivElement;
  1010. // 检查是否按下 Delete 或 Backspace 键
  1011. if (evt.key === 'Delete' || evt.key === 'Backspace') {
  1012. const enabledElement = getEnabledElement(element);
  1013. if (!enabledElement) {
  1014. return;
  1015. }
  1016. const annotations = annotation.state.getAnnotations(this.getToolName(), element);
  1017. if (!annotations || annotations.length === 0) {
  1018. return;
  1019. }
  1020. // 查找被选中的注解
  1021. const selectedAnnotations = annotations.filter(
  1022. (ann) => (ann as HipNHAAngleMeasurementAnnotation).isSelected
  1023. );
  1024. if (selectedAnnotations.length === 0) {
  1025. return;
  1026. }
  1027. // 删除所有选中的注解
  1028. for (const ann of selectedAnnotations) {
  1029. annotation.state.removeAnnotation(ann.annotationUID!);
  1030. }
  1031. // 触发渲染更新
  1032. const viewportIdsToRender =
  1033. utilities.viewportFilters.getViewportIdsWithToolToRender(
  1034. element,
  1035. this.getToolName()
  1036. );
  1037. utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
  1038. evt.preventDefault();
  1039. evt.stopPropagation();
  1040. }
  1041. };
  1042. /**
  1043. * 处理选中回调
  1044. */
  1045. handleSelectedCallback(
  1046. evt: EventTypes.InteractionEventType,
  1047. annotation: HipNHAAngleMeasurementAnnotation
  1048. ): void {
  1049. const eventDetail = evt.detail;
  1050. const { element } = eventDetail;
  1051. annotation.highlighted = true;
  1052. const enabledElement = getEnabledElement(element);
  1053. if (!enabledElement) {
  1054. return;
  1055. }
  1056. const viewportIdsToRender =
  1057. utilities.viewportFilters.getViewportIdsWithToolToRender(
  1058. element,
  1059. this.getToolName()
  1060. );
  1061. utilities.triggerAnnotationRenderForViewportIds(
  1062. viewportIdsToRender
  1063. );
  1064. evt.preventDefault();
  1065. }
  1066. /**
  1067. * 工具选中回调
  1068. */
  1069. toolSelectedCallback(
  1070. evt: EventTypes.InteractionEventType,
  1071. annotation: HipNHAAngleMeasurementAnnotation
  1072. ): void {
  1073. // 实现工具选中逻辑
  1074. }
  1075. /**
  1076. * 处理鼠标拖拽
  1077. */
  1078. _dragCallback = (evt: EventTypes.InteractionEventType): void => {
  1079. this.isDrawing = true;
  1080. const eventDetail = evt.detail;
  1081. const { element } = eventDetail;
  1082. const enabledElement = getEnabledElement(element);
  1083. if (!enabledElement || !this.editData) {
  1084. return;
  1085. }
  1086. const { annotation, viewportIdsToRender } = this.editData;
  1087. const { data } = annotation as HipNHAAngleMeasurementAnnotation;
  1088. if (annotation) {
  1089. const { currentPoints } = eventDetail;
  1090. const worldPos = currentPoints.world;
  1091. const points = data.handles.points;
  1092. const activeHandleIndex = data.handles.activeHandleIndex;
  1093. if (activeHandleIndex !== null && activeHandleIndex < points.length) {
  1094. points[activeHandleIndex] = worldPos;
  1095. this._updateCachedStats(
  1096. annotation as HipNHAAngleMeasurementAnnotation,
  1097. enabledElement
  1098. );
  1099. this.editData!.hasMoved = true;
  1100. } else if (this.editData!.newAnnotation) {
  1101. const lastIndex = points.length - 1;
  1102. if (lastIndex >= 0) {
  1103. points[lastIndex] = worldPos;
  1104. }
  1105. }
  1106. utilities.triggerAnnotationRenderForViewportIds(
  1107. viewportIdsToRender
  1108. );
  1109. }
  1110. };
  1111. /**
  1112. * 处理鼠标抬起/点击
  1113. */
  1114. _endCallback = (evt: EventTypes.InteractionEventType): void => {
  1115. const eventDetail = evt.detail;
  1116. const { element } = eventDetail;
  1117. if (!this.editData) {
  1118. return;
  1119. }
  1120. const { annotation, hasMoved } = this.editData;
  1121. const { data } = annotation as HipNHAAngleMeasurementAnnotation;
  1122. if (hasMoved) {
  1123. this.editData.hasMoved = false;
  1124. }
  1125. data.handles.activeHandleIndex = null;
  1126. };
  1127. /**
  1128. * 获取靠近图像点的手柄
  1129. */
  1130. getHandleNearImagePoint(
  1131. element: HTMLDivElement,
  1132. annotation: HipNHAAngleMeasurementAnnotation,
  1133. canvasCoords: CoreTypes.Point2,
  1134. proximity: number
  1135. ): Types.ToolHandle | undefined {
  1136. const enabledElement = getEnabledElement(element);
  1137. if (!enabledElement) {
  1138. return undefined;
  1139. }
  1140. const { viewport } = enabledElement;
  1141. const points = annotation.data.handles.points;
  1142. const handleProximity = Math.max(proximity, 15);
  1143. for (let i = 0; i < points.length; i++) {
  1144. const point = points[i];
  1145. const canvasPoint = viewport.worldToCanvas(point);
  1146. const distance = Math.sqrt(
  1147. Math.pow(canvasPoint[0] - canvasCoords[0], 2) +
  1148. Math.pow(canvasPoint[1] - canvasCoords[1], 2)
  1149. );
  1150. if (distance < handleProximity) {
  1151. annotation.data.handles.activeHandleIndex = i;
  1152. return {
  1153. worldPosition: point,
  1154. } as Types.ToolHandle;
  1155. }
  1156. }
  1157. annotation.data.handles.activeHandleIndex = null;
  1158. return undefined;
  1159. }
  1160. /**
  1161. * 更新缓存的统计数据
  1162. */
  1163. _updateCachedStats(
  1164. annotation: HipNHAAngleMeasurementAnnotation,
  1165. enabledElement: CoreTypes.IEnabledElement
  1166. ): void {
  1167. const { viewport } = enabledElement;
  1168. const { data } = annotation;
  1169. const points = data.handles.points;
  1170. const targetId = this.getTargetId(viewport);
  1171. if (!targetId) {
  1172. return;
  1173. }
  1174. if (!data.cachedStats) {
  1175. data.cachedStats = {};
  1176. }
  1177. if (!data.cachedStats[targetId]) {
  1178. data.cachedStats[targetId] = {};
  1179. }
  1180. const stats = data.cachedStats[targetId];
  1181. if (points.length >= 8) {
  1182. // 拟合左侧圆(点0, 1, 2)
  1183. const leftCircleResult = fitCircleFrom3Points(points[0], points[1], points[2]);
  1184. if (leftCircleResult) {
  1185. stats.centerM1 = leftCircleResult.center;
  1186. stats.radiusLeft = leftCircleResult.radius;
  1187. }
  1188. // 拟合右侧圆(点4, 5, 6)
  1189. const rightCircleResult = fitCircleFrom3Points(points[4], points[5], points[6]);
  1190. if (rightCircleResult) {
  1191. stats.centerM2 = rightCircleResult.center;
  1192. stats.radiusRight = rightCircleResult.radius;
  1193. }
  1194. // 如果两个圆都拟合成功,计算角度
  1195. if (stats.centerM1 && stats.centerM2) {
  1196. // 基准线向量 M1 -> M2
  1197. stats.baselineVector = vectorSubtract(stats.centerM2, stats.centerM1);
  1198. // 左侧切线向量 M1 -> point3
  1199. stats.leftTangentVector = vectorSubtract(points[3], stats.centerM1);
  1200. // 右侧切线向量 M2 -> point7
  1201. stats.rightTangentVector = vectorSubtract(points[7], stats.centerM2);
  1202. // 计算左侧NHA角
  1203. const leftAngleRad = getAngleBetweenLines(
  1204. stats.baselineVector,
  1205. stats.leftTangentVector
  1206. );
  1207. stats.angleLeft = Math.round(degrees(leftAngleRad));
  1208. // 计算右侧NHA角
  1209. const rightAngleRad = getAngleBetweenLines(
  1210. stats.baselineVector,
  1211. calculateOppositeVector(stats.rightTangentVector)
  1212. );
  1213. stats.angleRight = Math.round(degrees(rightAngleRad));
  1214. }
  1215. }
  1216. }
  1217. /**
  1218. * 渲染注解
  1219. */
  1220. renderAnnotation = (
  1221. enabledElement: CoreTypes.IEnabledElement,
  1222. svgDrawingHelper: SVGDrawingHelper
  1223. ): boolean => {
  1224. let renderStatus = false;
  1225. const { viewport } = enabledElement;
  1226. const { element } = viewport;
  1227. let annotations = annotation.state.getAnnotations(this.getToolName(), element);
  1228. if (!annotations?.length) {
  1229. return renderStatus;
  1230. }
  1231. for (let i = 0; i < annotations.length; i++) {
  1232. const ann = annotations[i] as HipNHAAngleMeasurementAnnotation;
  1233. const { annotationUID, data } = ann;
  1234. const points = data.handles.points;
  1235. if (points.length < 1) {
  1236. continue;
  1237. }
  1238. const targetId = this.getTargetId(viewport);
  1239. const cachedStats = targetId ? data.cachedStats?.[targetId] : undefined;
  1240. // 转换所有点为 canvas 坐标
  1241. const canvasPoints = points.map((p) => viewport.worldToCanvas(p));
  1242. // 确定颜色(选中时蓝色,未选中时白色)
  1243. const mainColor = ann.highlighted ? 'rgb(0, 120, 255)' : 'rgb(255, 255, 255)';
  1244. // 根据选中或悬停状态调整手柄半径:选中或悬停时为12,否则为8
  1245. const isHighlighted = ann.isSelected || ann.highlighted;
  1246. const handleRadius = isHighlighted ? 12 : 8;
  1247. // 1. 绘制8个手柄点
  1248. if (annotationUID) {
  1249. const handleGroupUID = '0';
  1250. drawHandles(
  1251. svgDrawingHelper,
  1252. annotationUID,
  1253. handleGroupUID,
  1254. canvasPoints,
  1255. {
  1256. color: 'rgb(255, 255, 255)',
  1257. handleRadius: handleRadius,
  1258. }
  1259. );
  1260. }
  1261. // 如果缓存统计数据存在,绘制圆和线
  1262. if (cachedStats && annotationUID) {
  1263. // 2. 绘制左侧股骨头圆
  1264. if (cachedStats.centerM1 && cachedStats.radiusLeft) {
  1265. const centerM1Canvas = viewport.worldToCanvas(cachedStats.centerM1);
  1266. // 计算像素半径
  1267. const pointOnCircle: CoreTypes.Point3 = [
  1268. cachedStats.centerM1[0] + cachedStats.radiusLeft,
  1269. cachedStats.centerM1[1],
  1270. cachedStats.centerM1[2]
  1271. ];
  1272. const pointOnCircleCanvas = viewport.worldToCanvas(pointOnCircle);
  1273. const radiusPixels = Math.abs(pointOnCircleCanvas[0] - centerM1Canvas[0]);
  1274. drawCircleSvg(
  1275. svgDrawingHelper,
  1276. annotationUID,
  1277. 'leftCircle',
  1278. centerM1Canvas,
  1279. radiusPixels,
  1280. {
  1281. color: mainColor,
  1282. width: 2,
  1283. }
  1284. );
  1285. }
  1286. // 3. 绘制右侧股骨头圆
  1287. if (cachedStats.centerM2 && cachedStats.radiusRight) {
  1288. const centerM2Canvas = viewport.worldToCanvas(cachedStats.centerM2);
  1289. // 计算像素半径
  1290. const pointOnCircle: CoreTypes.Point3 = [
  1291. cachedStats.centerM2[0] + cachedStats.radiusRight,
  1292. cachedStats.centerM2[1],
  1293. cachedStats.centerM2[2]
  1294. ];
  1295. const pointOnCircleCanvas = viewport.worldToCanvas(pointOnCircle);
  1296. const radiusPixels = Math.abs(pointOnCircleCanvas[0] - centerM2Canvas[0]);
  1297. drawCircleSvg(
  1298. svgDrawingHelper,
  1299. annotationUID,
  1300. 'rightCircle',
  1301. centerM2Canvas,
  1302. radiusPixels,
  1303. {
  1304. color: mainColor,
  1305. width: 2,
  1306. }
  1307. );
  1308. }
  1309. // 4. 绘制基准线 M1-M2
  1310. if (cachedStats.centerM1 && cachedStats.centerM2) {
  1311. const centerM1Canvas = viewport.worldToCanvas(cachedStats.centerM1);
  1312. const centerM2Canvas = viewport.worldToCanvas(cachedStats.centerM2);
  1313. drawLineSvg(
  1314. svgDrawingHelper,
  1315. annotationUID,
  1316. 'baseline',
  1317. centerM1Canvas,
  1318. centerM2Canvas,
  1319. {
  1320. color: mainColor,
  1321. width: 2,
  1322. }
  1323. );
  1324. }
  1325. // 5. 绘制左侧切线 M1-point3
  1326. if (cachedStats.centerM1 && points.length > 3) {
  1327. const centerM1Canvas = viewport.worldToCanvas(cachedStats.centerM1);
  1328. const point3Canvas = viewport.worldToCanvas(points[3]);
  1329. drawLineSvg(
  1330. svgDrawingHelper,
  1331. annotationUID,
  1332. 'leftTangent',
  1333. centerM1Canvas,
  1334. point3Canvas,
  1335. {
  1336. color: mainColor,
  1337. width: 2,
  1338. }
  1339. );
  1340. }
  1341. // 6. 绘制右侧切线 M2-point7
  1342. if (cachedStats.centerM2 && points.length > 7) {
  1343. const centerM2Canvas = viewport.worldToCanvas(cachedStats.centerM2);
  1344. const point7Canvas = viewport.worldToCanvas(points[7]);
  1345. drawLineSvg(
  1346. svgDrawingHelper,
  1347. annotationUID,
  1348. 'rightTangent',
  1349. centerM2Canvas,
  1350. point7Canvas,
  1351. {
  1352. color: mainColor,
  1353. width: 2,
  1354. }
  1355. );
  1356. }
  1357. // 7. 绘制左侧角度弧线和文本
  1358. if (
  1359. cachedStats.centerM1 &&
  1360. cachedStats.baselineVector &&
  1361. cachedStats.leftTangentVector &&
  1362. cachedStats.angleLeft !== undefined
  1363. ) {
  1364. const centerM1Canvas = viewport.worldToCanvas(cachedStats.centerM1);
  1365. // 绘制角度弧线
  1366. const leftArcPoints = this._createAngleArcPoints(
  1367. centerM1Canvas,
  1368. [cachedStats.baselineVector[0], cachedStats.baselineVector[1]],
  1369. [cachedStats.leftTangentVector[0], cachedStats.leftTangentVector[1]],
  1370. 40
  1371. );
  1372. drawPathSvg(
  1373. svgDrawingHelper,
  1374. annotationUID,
  1375. 'leftArc',
  1376. leftArcPoints,
  1377. {
  1378. color: mainColor,
  1379. width: 1,
  1380. lineDash: '4,4',
  1381. }
  1382. );
  1383. // 绘制左侧角度文本框
  1384. const textBoxLeft: CoreTypes.Point2 = data.handles.textBoxLeft || [
  1385. centerM1Canvas[0] - 60,
  1386. centerM1Canvas[1] - 50,
  1387. ];
  1388. drawLinkedTextBox(
  1389. svgDrawingHelper,
  1390. annotationUID,
  1391. 'leftAngleText',
  1392. [`左侧NHA: ${cachedStats.angleLeft}°`],
  1393. textBoxLeft,
  1394. [centerM1Canvas],
  1395. {},
  1396. {
  1397. color: mainColor,
  1398. }
  1399. );
  1400. }
  1401. // 8. 绘制右侧角度弧线和文本
  1402. if (
  1403. cachedStats.centerM2 &&
  1404. cachedStats.baselineVector &&
  1405. cachedStats.rightTangentVector &&
  1406. cachedStats.angleRight !== undefined
  1407. ) {
  1408. const centerM2Canvas = viewport.worldToCanvas(cachedStats.centerM2);
  1409. // 绘制角度弧线
  1410. const rightArcPoints = this._createAngleArcPoints(
  1411. centerM2Canvas,
  1412. [-cachedStats.baselineVector[0], -cachedStats.baselineVector[1]],
  1413. [cachedStats.rightTangentVector[0], cachedStats.rightTangentVector[1]],
  1414. 40
  1415. );
  1416. drawPathSvg(
  1417. svgDrawingHelper,
  1418. annotationUID,
  1419. 'rightArc',
  1420. rightArcPoints,
  1421. {
  1422. color: mainColor,
  1423. width: 1,
  1424. lineDash: '4,4',
  1425. }
  1426. );
  1427. // 绘制右侧角度文本框
  1428. const textBoxRight: CoreTypes.Point2 = data.handles.textBoxRight || [
  1429. centerM2Canvas[0] + 20,
  1430. centerM2Canvas[1] - 50,
  1431. ];
  1432. drawLinkedTextBox(
  1433. svgDrawingHelper,
  1434. annotationUID,
  1435. 'rightAngleText',
  1436. [`右侧NHA: ${cachedStats.angleRight}°`],
  1437. textBoxRight,
  1438. [centerM2Canvas],
  1439. {},
  1440. {
  1441. color: mainColor,
  1442. }
  1443. );
  1444. }
  1445. }
  1446. renderStatus = true;
  1447. }
  1448. return renderStatus;
  1449. };
  1450. }