HipDIMeasurementTool.ts 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032
  1. import { Point3, Point2, fitCircleFrom3Points, vectorAdd } from './mathUtils';
  2. import {
  3. utilities as csUtils,
  4. Types as CoreTypes,
  5. getEnabledElement,
  6. } from '@cornerstonejs/core';
  7. import {
  8. AnnotationTool,
  9. utilities,
  10. Types,
  11. annotation,
  12. drawing,
  13. } from '@cornerstonejs/tools';
  14. const {
  15. drawHandles,
  16. drawLinkedTextBox,
  17. drawLine: drawLineSvg,
  18. drawCircle: drawCircleSvg,
  19. } = drawing;
  20. import {
  21. PublicToolProps,
  22. ToolProps,
  23. EventTypes,
  24. SVGDrawingHelper,
  25. } from '@cornerstonejs/tools/dist/esm/types';
  26. // 注解初始配置接口
  27. interface AnnotationInitialConfig {
  28. acetabulumRadius?: number; // 髋臼圆初始半径(像素)
  29. femurRadius?: number; // 股骨头圆初始半径(像素)
  30. circleDistance?: number; // 两圆心之间的初始距离(像素)
  31. }
  32. // 注解数据接口
  33. interface HipDIAnnotation extends Types.Annotation {
  34. data: {
  35. handles: {
  36. points: CoreTypes.Point3[];
  37. activeHandleIndex: number | null;
  38. };
  39. textBox?: CoreTypes.Point2; // DI文本框位置
  40. cachedStats?: {
  41. [targetId: string]: {
  42. acetabulumCircle?: {
  43. center: CoreTypes.Point3;
  44. radius: number;
  45. };
  46. femurCircle?: {
  47. center: CoreTypes.Point3;
  48. radius: number;
  49. };
  50. distance: number;
  51. DI: number;
  52. };
  53. };
  54. };
  55. }
  56. // 导出的注解数据接口
  57. export interface ExportedDIData {
  58. points: CoreTypes.Point3[]; // 6个点的世界坐标
  59. circles: {
  60. acetabulum: {
  61. center: CoreTypes.Point3;
  62. radius: number;
  63. };
  64. femur: {
  65. center: CoreTypes.Point3;
  66. radius: number;
  67. };
  68. };
  69. measurements: {
  70. distance: number;
  71. DI: number;
  72. };
  73. textBoxPosition?: CoreTypes.Point2;
  74. metadata: {
  75. viewPlaneNormal: CoreTypes.Point3;
  76. viewUp: CoreTypes.Point3;
  77. FrameOfReferenceUID: string;
  78. referencedImageId: string;
  79. };
  80. }
  81. export default class HipDIMeasurementTool extends AnnotationTool {
  82. static toolName = 'HipDIMeasurementTool';
  83. editData: {
  84. annotation: Types.Annotation;
  85. viewportIdsToRender: string[];
  86. handleIndex?: number;
  87. newAnnotation?: boolean;
  88. hasMoved?: boolean;
  89. textBoxBeingMoved?: boolean;
  90. textBoxOffset?: CoreTypes.Point2;
  91. } | null = null;
  92. isDrawing: boolean = false;
  93. /**
  94. * 创建一个预设的注解,带有6个默认点
  95. * @param element HTML元素
  96. * @param viewport Viewport实例
  97. * @param config 可选的初始配置
  98. */
  99. static createDefaultAnnotation(
  100. element: HTMLDivElement,
  101. viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport,
  102. config?: AnnotationInitialConfig
  103. ): HipDIAnnotation {
  104. const enabledElement = getEnabledElement(element);
  105. if (!enabledElement) {
  106. throw new Error('Element is not enabled');
  107. }
  108. // 获取viewport的尺寸
  109. const canvas = viewport.canvas;
  110. const { width, height } = canvas;
  111. const centerX = width / 2;
  112. const centerY = height / 2;
  113. // 应用默认值
  114. const acetabulumRadius = config?.acetabulumRadius ?? 80;
  115. const femurRadius = config?.femurRadius ?? 60;
  116. const circleDistance = config?.circleDistance ?? 100;
  117. // 创建髋臼圆的3个点(左侧圆,点1,2,3)
  118. // 在圆周上均匀分布3个点(0度、120度、240度)
  119. const acetabulumCenterX = centerX - circleDistance / 2;
  120. const acetabulumCenterY = centerY;
  121. const angle1 = 0;
  122. const angle2 = (2 * Math.PI) / 3; // 120度
  123. const angle3 = (4 * Math.PI) / 3; // 240度
  124. const acetabulumPoints: CoreTypes.Point2[] = [
  125. [
  126. acetabulumCenterX + acetabulumRadius * Math.cos(angle1),
  127. acetabulumCenterY + acetabulumRadius * Math.sin(angle1)
  128. ],
  129. [
  130. acetabulumCenterX + acetabulumRadius * Math.cos(angle2),
  131. acetabulumCenterY + acetabulumRadius * Math.sin(angle2)
  132. ],
  133. [
  134. acetabulumCenterX + acetabulumRadius * Math.cos(angle3),
  135. acetabulumCenterY + acetabulumRadius * Math.sin(angle3)
  136. ]
  137. ];
  138. // 创建股骨头圆的3个点(右侧圆,点4,5,6)
  139. const femurCenterX = centerX + circleDistance / 2;
  140. const femurCenterY = centerY;
  141. const femurPoints: CoreTypes.Point2[] = [
  142. [
  143. femurCenterX + femurRadius * Math.cos(angle1),
  144. femurCenterY + femurRadius * Math.sin(angle1)
  145. ],
  146. [
  147. femurCenterX + femurRadius * Math.cos(angle2),
  148. femurCenterY + femurRadius * Math.sin(angle2)
  149. ],
  150. [
  151. femurCenterX + femurRadius * Math.cos(angle3),
  152. femurCenterY + femurRadius * Math.sin(angle3)
  153. ]
  154. ];
  155. const canvasPoints: CoreTypes.Point2[] = [
  156. ...acetabulumPoints,
  157. ...femurPoints
  158. ];
  159. // 转换为world坐标
  160. const worldPoints = canvasPoints.map((canvasPoint) =>
  161. viewport.canvasToWorld(canvasPoint)
  162. );
  163. const camera = viewport.getCamera();
  164. const { viewPlaneNormal, viewUp } = camera;
  165. if (viewPlaneNormal === undefined) {
  166. throw new Error('viewPlaneNormal is undefined');
  167. }
  168. if (viewUp === undefined) {
  169. throw new Error('viewUp is undefined');
  170. }
  171. const annotationData = {
  172. invalidated: true,
  173. highlighted: false,
  174. metadata: {
  175. viewPlaneNormal: [...viewPlaneNormal] as CoreTypes.Point3,
  176. viewUp: [...viewUp] as CoreTypes.Point3,
  177. FrameOfReferenceUID: viewport.getFrameOfReferenceUID(),
  178. referencedImageId: viewport.getCurrentImageId?.() || '',
  179. toolName: HipDIMeasurementTool.toolName,
  180. },
  181. data: {
  182. label: '',
  183. handles: {
  184. points: worldPoints,
  185. activeHandleIndex: null,
  186. },
  187. cachedStats: {},
  188. },
  189. } as HipDIAnnotation;
  190. return annotationData;
  191. }
  192. /**
  193. * 导出注解数据
  194. * @param annotation 要导出的注解
  195. * @returns 导出的数据对象
  196. */
  197. static exportAnnotationData(
  198. annotation: HipDIAnnotation,
  199. viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport
  200. ): ExportedDIData | null {
  201. const targetId = `imageId:${viewport.getCurrentImageId?.() || ''}`;
  202. const cachedStats = annotation.data.cachedStats?.[targetId];
  203. if (!cachedStats || !cachedStats.acetabulumCircle || !cachedStats.femurCircle) {
  204. return null;
  205. }
  206. return {
  207. points: [...annotation.data.handles.points],
  208. circles: {
  209. acetabulum: {
  210. center: cachedStats.acetabulumCircle.center,
  211. radius: cachedStats.acetabulumCircle.radius,
  212. },
  213. femur: {
  214. center: cachedStats.femurCircle.center,
  215. radius: cachedStats.femurCircle.radius,
  216. },
  217. },
  218. measurements: {
  219. distance: cachedStats.distance,
  220. DI: cachedStats.DI,
  221. },
  222. textBoxPosition: annotation.data.textBox,
  223. metadata: {
  224. viewPlaneNormal: annotation.metadata?.viewPlaneNormal || [0, 0, 1],
  225. viewUp: annotation.metadata?.viewUp || [0, 1, 0],
  226. FrameOfReferenceUID: annotation.metadata?.FrameOfReferenceUID || '',
  227. referencedImageId: annotation.metadata?.referencedImageId || '',
  228. },
  229. };
  230. }
  231. /**
  232. * 从导出的数据恢复注解
  233. * @param exportedData 导出的数据
  234. * @param element HTML元素
  235. * @param viewport Viewport实例
  236. * @returns 恢复的注解
  237. */
  238. static restoreFromExportedData(
  239. exportedData: ExportedDIData,
  240. element: HTMLDivElement,
  241. viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport
  242. ): HipDIAnnotation {
  243. const enabledElement = getEnabledElement(element);
  244. if (!enabledElement) {
  245. throw new Error('Element is not enabled');
  246. }
  247. const annotationData = {
  248. invalidated: true,
  249. highlighted: false,
  250. metadata: {
  251. ...exportedData.metadata,
  252. toolName: HipDIMeasurementTool.toolName,
  253. },
  254. data: {
  255. label: '',
  256. handles: {
  257. points: [...exportedData.points],
  258. activeHandleIndex: null,
  259. },
  260. textBox: exportedData.textBoxPosition,
  261. cachedStats: {},
  262. },
  263. } as HipDIAnnotation;
  264. return annotationData;
  265. }
  266. constructor(
  267. toolProps: PublicToolProps = {},
  268. defaultToolProps: ToolProps = {
  269. supportedInteractionTypes: ['Mouse', 'Touch'],
  270. configuration: {
  271. shadow: true,
  272. preventHandleOutsideImage: false,
  273. },
  274. }
  275. ) {
  276. super(toolProps, defaultToolProps);
  277. }
  278. /**
  279. * 添加新注解 - 禁用此功能
  280. */
  281. addNewAnnotation(
  282. evt: EventTypes.InteractionEventType
  283. ): HipDIAnnotation {
  284. evt.preventDefault();
  285. return {} as HipDIAnnotation;
  286. }
  287. /**
  288. * 检查点是否靠近工具
  289. */
  290. isPointNearTool(
  291. element: HTMLDivElement,
  292. annotation: HipDIAnnotation,
  293. canvasCoords: CoreTypes.Point2,
  294. proximity: number
  295. ): boolean {
  296. const enabledElement = getEnabledElement(element);
  297. if (!enabledElement) {
  298. return false;
  299. }
  300. const { viewport } = enabledElement;
  301. const points = annotation.data.handles.points;
  302. // 检查是否靠近任意一个手柄点
  303. for (let i = 0; i < points.length; i++) {
  304. const point = points[i];
  305. const canvasPoint = viewport.worldToCanvas(point);
  306. const distance = Math.sqrt(
  307. Math.pow(canvasPoint[0] - canvasCoords[0], 2) +
  308. Math.pow(canvasPoint[1] - canvasCoords[1], 2)
  309. );
  310. if (distance < proximity) {
  311. return true;
  312. }
  313. }
  314. return false;
  315. }
  316. /**
  317. * 检查点击是否在文本框内
  318. */
  319. private _isPointInTextBox(
  320. canvasPoint: CoreTypes.Point2,
  321. textBoxPosition: CoreTypes.Point2,
  322. textWidth: number = 100,
  323. textHeight: number = 40
  324. ): boolean {
  325. const [x, y] = canvasPoint;
  326. const [tx, ty] = textBoxPosition;
  327. const margin = 10;
  328. return (
  329. x >= tx - margin &&
  330. x <= tx + textWidth + margin &&
  331. y >= ty - margin &&
  332. y <= ty + textHeight + margin
  333. );
  334. }
  335. /**
  336. * 取消操作
  337. */
  338. cancel(element: HTMLDivElement): string {
  339. if (this.isDrawing) {
  340. this.isDrawing = false;
  341. this._deactivateDraw(element);
  342. this._deactivateModify(element);
  343. const enabledElement = getEnabledElement(element);
  344. if (enabledElement) {
  345. const viewportIdsToRender =
  346. utilities.viewportFilters.getViewportIdsWithToolToRender(
  347. element,
  348. this.getToolName()
  349. );
  350. utilities.triggerAnnotationRenderForViewportIds(
  351. viewportIdsToRender
  352. );
  353. }
  354. this.editData = null;
  355. return this.getToolName();
  356. }
  357. return '';
  358. }
  359. /**
  360. * 激活绘制模式
  361. */
  362. _activateDraw(element: HTMLDivElement): void {
  363. element.addEventListener(
  364. 'CORNERSTONE_TOOLS_MOUSE_DRAG',
  365. this._dragCallback as EventListener
  366. );
  367. element.addEventListener(
  368. 'CORNERSTONE_TOOLS_MOUSE_UP',
  369. this._endCallback as EventListener
  370. );
  371. element.addEventListener(
  372. 'CORNERSTONE_TOOLS_MOUSE_CLICK',
  373. this._endCallback as EventListener
  374. );
  375. }
  376. /**
  377. * 取消激活绘制模式
  378. */
  379. _deactivateDraw(element: HTMLDivElement): void {
  380. element.removeEventListener(
  381. 'CORNERSTONE_TOOLS_MOUSE_DRAG',
  382. this._dragCallback as EventListener
  383. );
  384. element.removeEventListener(
  385. 'CORNERSTONE_TOOLS_MOUSE_UP',
  386. this._endCallback as EventListener
  387. );
  388. element.removeEventListener(
  389. 'CORNERSTONE_TOOLS_MOUSE_CLICK',
  390. this._endCallback as EventListener
  391. );
  392. }
  393. /**
  394. * 激活修改模式
  395. */
  396. _activateModify(element: HTMLDivElement): void {
  397. element.addEventListener(
  398. 'CORNERSTONE_TOOLS_MOUSE_DOWN',
  399. this._mouseDownModifyCallback as EventListener
  400. );
  401. element.addEventListener(
  402. 'CORNERSTONE_TOOLS_MOUSE_DRAG',
  403. this._mouseDragModifyCallback as EventListener
  404. );
  405. element.addEventListener(
  406. 'CORNERSTONE_TOOLS_MOUSE_UP',
  407. this._mouseUpModifyCallback as EventListener
  408. );
  409. }
  410. /**
  411. * 取消激活修改模式
  412. */
  413. _deactivateModify(element: HTMLDivElement): void {
  414. element.removeEventListener(
  415. 'CORNERSTONE_TOOLS_MOUSE_DOWN',
  416. this._mouseDownModifyCallback as EventListener
  417. );
  418. element.removeEventListener(
  419. 'CORNERSTONE_TOOLS_MOUSE_DRAG',
  420. this._mouseDragModifyCallback as EventListener
  421. );
  422. element.removeEventListener(
  423. 'CORNERSTONE_TOOLS_MOUSE_UP',
  424. this._mouseUpModifyCallback as EventListener
  425. );
  426. }
  427. /**
  428. * 鼠标按下回调 - 用于修改模式
  429. */
  430. _mouseDownModifyCallback = (evt: EventTypes.InteractionEventType): void => {
  431. const eventDetail = evt.detail;
  432. const { element, currentPoints } = eventDetail;
  433. const canvasCoords = currentPoints.canvas;
  434. const enabledElement = getEnabledElement(element);
  435. if (!enabledElement) {
  436. return;
  437. }
  438. const { viewport } = enabledElement;
  439. const annotations = annotation.state.getAnnotations(this.getToolName(), element);
  440. if (!annotations || annotations.length === 0) {
  441. return;
  442. }
  443. // 先检查是否点击在文本框上
  444. for (const ann of annotations) {
  445. const customAnn = ann as HipDIAnnotation;
  446. const targetId = this.getTargetId(viewport);
  447. const cachedStats = targetId ? customAnn.data.cachedStats?.[targetId] : undefined;
  448. if (cachedStats?.femurCircle) {
  449. const femurCenterCanvas = viewport.worldToCanvas(cachedStats.femurCircle.center);
  450. const textBox: CoreTypes.Point2 = customAnn.data.textBox || [
  451. femurCenterCanvas[0] + 20,
  452. femurCenterCanvas[1] - 20,
  453. ];
  454. if (this._isPointInTextBox(canvasCoords, textBox)) {
  455. const viewportIdsToRender =
  456. utilities.viewportFilters.getViewportIdsWithToolToRender(
  457. element,
  458. this.getToolName()
  459. );
  460. const textBoxOffset: CoreTypes.Point2 = [
  461. canvasCoords[0] - textBox[0],
  462. canvasCoords[1] - textBox[1],
  463. ];
  464. this.editData = {
  465. annotation: customAnn,
  466. viewportIdsToRender,
  467. hasMoved: false,
  468. textBoxBeingMoved: true,
  469. textBoxOffset: textBoxOffset,
  470. };
  471. customAnn.highlighted = true;
  472. utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
  473. evt.preventDefault();
  474. evt.stopPropagation();
  475. return;
  476. }
  477. }
  478. }
  479. // 查找最近的手柄
  480. for (const ann of annotations) {
  481. const customAnn = ann as HipDIAnnotation;
  482. const handle = this.getHandleNearImagePoint(
  483. element,
  484. customAnn,
  485. canvasCoords,
  486. 6
  487. );
  488. if (handle) {
  489. const viewportIdsToRender =
  490. utilities.viewportFilters.getViewportIdsWithToolToRender(
  491. element,
  492. this.getToolName()
  493. );
  494. this.editData = {
  495. annotation: customAnn,
  496. viewportIdsToRender,
  497. handleIndex: customAnn.data.handles.activeHandleIndex || 0,
  498. hasMoved: false,
  499. };
  500. customAnn.highlighted = true;
  501. utilities.triggerAnnotationRenderForViewportIds(
  502. viewportIdsToRender
  503. );
  504. evt.preventDefault();
  505. evt.stopPropagation();
  506. return;
  507. }
  508. }
  509. };
  510. /**
  511. * 鼠标拖拽回调 - 用于修改模式
  512. */
  513. _mouseDragModifyCallback = (evt: EventTypes.InteractionEventType): void => {
  514. if (!this.editData) {
  515. return;
  516. }
  517. const eventDetail = evt.detail;
  518. const { currentPoints } = eventDetail;
  519. const canvasCoords = currentPoints.canvas;
  520. const enabledElement = getEnabledElement(eventDetail.element);
  521. if (!enabledElement) {
  522. return;
  523. }
  524. const { annotation: ann, viewportIdsToRender, textBoxBeingMoved } = this.editData;
  525. const customAnn = ann as HipDIAnnotation;
  526. const { data } = customAnn;
  527. // 如果正在拖拽文本框
  528. if (textBoxBeingMoved && this.editData.textBoxOffset) {
  529. const newTextBoxPosition: CoreTypes.Point2 = [
  530. canvasCoords[0] - this.editData.textBoxOffset[0],
  531. canvasCoords[1] - this.editData.textBoxOffset[1],
  532. ];
  533. data.textBox = newTextBoxPosition;
  534. this.editData.hasMoved = true;
  535. utilities.triggerAnnotationRenderForViewportIds(
  536. viewportIdsToRender
  537. );
  538. evt.preventDefault();
  539. evt.stopPropagation();
  540. return;
  541. }
  542. // 处理手柄拖拽
  543. const worldPos = currentPoints.world;
  544. const activeHandleIndex = data.handles.activeHandleIndex;
  545. if (activeHandleIndex !== null && activeHandleIndex >= 0 && activeHandleIndex < data.handles.points.length) {
  546. data.handles.points[activeHandleIndex] = worldPos;
  547. this._updateCachedStats(customAnn, enabledElement);
  548. this.editData.hasMoved = true;
  549. utilities.triggerAnnotationRenderForViewportIds(
  550. viewportIdsToRender
  551. );
  552. evt.preventDefault();
  553. evt.stopPropagation();
  554. }
  555. };
  556. /**
  557. * 鼠标释放回调 - 用于修改模式
  558. */
  559. _mouseUpModifyCallback = (evt: EventTypes.InteractionEventType): void => {
  560. if (!this.editData) {
  561. return;
  562. }
  563. const { annotation: ann } = this.editData;
  564. const customAnn = ann as HipDIAnnotation;
  565. customAnn.data.handles.activeHandleIndex = null;
  566. customAnn.highlighted = false;
  567. const eventDetail = evt.detail;
  568. const { element } = eventDetail;
  569. const viewportIdsToRender =
  570. utilities.viewportFilters.getViewportIdsWithToolToRender(
  571. element,
  572. this.getToolName()
  573. );
  574. utilities.triggerAnnotationRenderForViewportIds(
  575. viewportIdsToRender
  576. );
  577. this.editData = null;
  578. evt.preventDefault();
  579. evt.stopPropagation();
  580. };
  581. /**
  582. * 处理选中回调
  583. */
  584. handleSelectedCallback(
  585. evt: EventTypes.InteractionEventType,
  586. annotation: HipDIAnnotation
  587. ): void {
  588. const eventDetail = evt.detail;
  589. const { element } = eventDetail;
  590. annotation.highlighted = true;
  591. const enabledElement = getEnabledElement(element);
  592. if (!enabledElement) {
  593. return;
  594. }
  595. const viewportIdsToRender =
  596. utilities.viewportFilters.getViewportIdsWithToolToRender(
  597. element,
  598. this.getToolName()
  599. );
  600. utilities.triggerAnnotationRenderForViewportIds(
  601. viewportIdsToRender
  602. );
  603. evt.preventDefault();
  604. }
  605. /**
  606. * 工具选中回调
  607. */
  608. toolSelectedCallback(
  609. evt: EventTypes.InteractionEventType,
  610. annotation: HipDIAnnotation
  611. ): void {
  612. // 实现工具选中逻辑
  613. }
  614. /**
  615. * 处理鼠标拖拽
  616. */
  617. _dragCallback = (evt: EventTypes.InteractionEventType): void => {
  618. this.isDrawing = true;
  619. const eventDetail = evt.detail;
  620. const { element } = eventDetail;
  621. const enabledElement = getEnabledElement(element);
  622. if (!enabledElement || !this.editData) {
  623. return;
  624. }
  625. const { annotation, viewportIdsToRender } = this.editData;
  626. const { data } = annotation as HipDIAnnotation;
  627. if (annotation) {
  628. const { currentPoints } = eventDetail;
  629. const worldPos = currentPoints.world;
  630. const points = data.handles.points;
  631. const activeHandleIndex = data.handles.activeHandleIndex;
  632. if (activeHandleIndex !== null && activeHandleIndex < points.length) {
  633. points[activeHandleIndex] = worldPos;
  634. this._updateCachedStats(
  635. annotation as HipDIAnnotation,
  636. enabledElement
  637. );
  638. this.editData!.hasMoved = true;
  639. }
  640. utilities.triggerAnnotationRenderForViewportIds(
  641. viewportIdsToRender
  642. );
  643. }
  644. };
  645. /**
  646. * 处理鼠标抬起/点击
  647. */
  648. _endCallback = (evt: EventTypes.InteractionEventType): void => {
  649. const eventDetail = evt.detail;
  650. const { element } = eventDetail;
  651. if (!this.editData) {
  652. return;
  653. }
  654. const { annotation, hasMoved } = this.editData;
  655. const { data } = annotation as HipDIAnnotation;
  656. if (hasMoved) {
  657. this.editData.hasMoved = false;
  658. }
  659. };
  660. /**
  661. * 获取靠近图像点的手柄
  662. */
  663. getHandleNearImagePoint(
  664. element: HTMLDivElement,
  665. annotation: HipDIAnnotation,
  666. canvasCoords: CoreTypes.Point2,
  667. proximity: number
  668. ): Types.ToolHandle | undefined {
  669. const enabledElement = getEnabledElement(element);
  670. if (!enabledElement) {
  671. return undefined;
  672. }
  673. const { viewport } = enabledElement;
  674. const points = annotation.data.handles.points;
  675. const handleProximity = Math.max(proximity, 15);
  676. for (let i = 0; i < points.length; i++) {
  677. const point = points[i];
  678. const canvasPoint = viewport.worldToCanvas(point);
  679. const distance = Math.sqrt(
  680. Math.pow(canvasPoint[0] - canvasCoords[0], 2) +
  681. Math.pow(canvasPoint[1] - canvasCoords[1], 2)
  682. );
  683. if (distance < handleProximity) {
  684. annotation.data.handles.activeHandleIndex = i;
  685. return {
  686. worldPosition: point,
  687. } as Types.ToolHandle;
  688. }
  689. }
  690. annotation.data.handles.activeHandleIndex = null;
  691. return undefined;
  692. }
  693. /**
  694. * 更新缓存的统计数据
  695. */
  696. _updateCachedStats(
  697. annotation: HipDIAnnotation,
  698. enabledElement: CoreTypes.IEnabledElement
  699. ): void {
  700. const { viewport } = enabledElement;
  701. const { data } = annotation;
  702. const points = data.handles.points;
  703. const targetId = this.getTargetId(viewport);
  704. if (!targetId) {
  705. return;
  706. }
  707. if (!data.cachedStats) {
  708. data.cachedStats = {};
  709. }
  710. if (!data.cachedStats[targetId]) {
  711. data.cachedStats[targetId] = {
  712. distance: 0,
  713. DI: 0,
  714. };
  715. }
  716. const stats = data.cachedStats[targetId];
  717. if (points.length >= 6) {
  718. // 拟合髋臼圆(点1,2,3)
  719. const acetabulumCircle = fitCircleFrom3Points(
  720. points[0],
  721. points[1],
  722. points[2]
  723. );
  724. // 拟合股骨头圆(点4,5,6)
  725. const femurCircle = fitCircleFrom3Points(
  726. points[3],
  727. points[4],
  728. points[5]
  729. );
  730. if (!acetabulumCircle || !femurCircle) {
  731. console.warn('圆拟合失败,三点可能共线');
  732. return;
  733. }
  734. // 存储圆数据
  735. stats.acetabulumCircle = acetabulumCircle;
  736. stats.femurCircle = femurCircle;
  737. // 计算圆心距离d
  738. const M1 = acetabulumCircle.center;
  739. const M2 = femurCircle.center;
  740. const distance = Math.sqrt(
  741. Math.pow(M2[0] - M1[0], 2) +
  742. Math.pow(M2[1] - M1[1], 2) +
  743. Math.pow(M2[2] - M1[2], 2)
  744. );
  745. stats.distance = distance;
  746. // 计算DI指数
  747. const DI = distance / femurCircle.radius;
  748. stats.DI = DI;
  749. }
  750. }
  751. /**
  752. * 渲染注解
  753. */
  754. renderAnnotation = (
  755. enabledElement: CoreTypes.IEnabledElement,
  756. svgDrawingHelper: SVGDrawingHelper
  757. ): boolean => {
  758. let renderStatus = false;
  759. const { viewport } = enabledElement;
  760. const { element } = viewport;
  761. let annotations = annotation.state.getAnnotations(this.getToolName(), element);
  762. if (!annotations?.length) {
  763. return renderStatus;
  764. }
  765. for (let i = 0; i < annotations.length; i++) {
  766. const annotation = annotations[i] as HipDIAnnotation;
  767. const { annotationUID, data } = annotation;
  768. const points = data.handles.points;
  769. if (points.length < 1) {
  770. continue;
  771. }
  772. const targetId = this.getTargetId(viewport);
  773. const cachedStats = targetId ? data.cachedStats?.[targetId] : undefined;
  774. // 确定颜色(选中状态为蓝色,非选中为白色)
  775. const color = annotation.highlighted ? 'rgb(0, 0, 255)' : 'rgb(255, 255, 255)';
  776. // 转换所有点为 canvas 坐标
  777. const canvasPoints = points.map((p) => viewport.worldToCanvas(p));
  778. // 绘制髋臼圆
  779. if (cachedStats?.acetabulumCircle && annotationUID) {
  780. const center = cachedStats.acetabulumCircle.center;
  781. const centerCanvas = viewport.worldToCanvas(center);
  782. // 计算半径的像素值
  783. const radiusVector: Point3 = [cachedStats.acetabulumCircle.radius, 0, 0];
  784. const endPoint = vectorAdd(center, radiusVector);
  785. const endCanvas = viewport.worldToCanvas(endPoint);
  786. const radiusPixels = Math.sqrt(
  787. Math.pow(endCanvas[0] - centerCanvas[0], 2) +
  788. Math.pow(endCanvas[1] - centerCanvas[1], 2)
  789. );
  790. const circleUID = `${annotationUID}-acetabulum-circle`;
  791. drawCircleSvg(
  792. svgDrawingHelper,
  793. annotationUID,
  794. circleUID,
  795. centerCanvas,
  796. radiusPixels,
  797. {
  798. color: color,
  799. width: 2,
  800. }
  801. );
  802. }
  803. // 绘制股骨头圆
  804. if (cachedStats?.femurCircle && annotationUID) {
  805. const center = cachedStats.femurCircle.center;
  806. const centerCanvas = viewport.worldToCanvas(center);
  807. // 计算半径的像素值
  808. const radiusVector: Point3 = [cachedStats.femurCircle.radius, 0, 0];
  809. const endPoint = vectorAdd(center, radiusVector);
  810. const endCanvas = viewport.worldToCanvas(endPoint);
  811. const radiusPixels = Math.sqrt(
  812. Math.pow(endCanvas[0] - centerCanvas[0], 2) +
  813. Math.pow(endCanvas[1] - centerCanvas[1], 2)
  814. );
  815. const circleUID = `${annotationUID}-femur-circle`;
  816. drawCircleSvg(
  817. svgDrawingHelper,
  818. annotationUID,
  819. circleUID,
  820. centerCanvas,
  821. radiusPixels,
  822. {
  823. color: color,
  824. width: 2,
  825. }
  826. );
  827. }
  828. // 绘制圆心连线
  829. if (cachedStats?.acetabulumCircle && cachedStats?.femurCircle && annotationUID) {
  830. const M1 = cachedStats.acetabulumCircle.center;
  831. const M2 = cachedStats.femurCircle.center;
  832. const M1Canvas = viewport.worldToCanvas(M1);
  833. const M2Canvas = viewport.worldToCanvas(M2);
  834. const lineUID = `${annotationUID}-center-line`;
  835. drawLineSvg(
  836. svgDrawingHelper,
  837. annotationUID,
  838. lineUID,
  839. M1Canvas,
  840. M2Canvas,
  841. {
  842. color: color,
  843. width: 2,
  844. }
  845. );
  846. }
  847. // 绘制6个控制点
  848. if (annotationUID) {
  849. const handleGroupUID = '0';
  850. drawHandles(
  851. svgDrawingHelper,
  852. annotationUID,
  853. handleGroupUID,
  854. canvasPoints,
  855. {
  856. color: 'rgb(255, 255, 255)',
  857. handleRadius: 6,
  858. }
  859. );
  860. }
  861. // 绘制DI数值标注(黄色,可拖拽)
  862. if (cachedStats?.femurCircle && cachedStats.DI !== undefined && annotationUID) {
  863. const femurCenterCanvas = viewport.worldToCanvas(cachedStats.femurCircle.center);
  864. const textLines = [`DI: ${cachedStats.DI.toFixed(2)}`];
  865. const textUID = `${annotationUID}-DI-text`;
  866. const textBoxPosition: CoreTypes.Point2 = data.textBox || [
  867. femurCenterCanvas[0] + 20,
  868. femurCenterCanvas[1] - 20,
  869. ];
  870. drawLinkedTextBox(
  871. svgDrawingHelper,
  872. annotationUID,
  873. textUID,
  874. textLines,
  875. textBoxPosition,
  876. [femurCenterCanvas],
  877. {},
  878. {
  879. color: 'rgb(255, 255, 0)', // 黄色
  880. }
  881. );
  882. }
  883. renderStatus = true;
  884. }
  885. return renderStatus;
  886. };
  887. }