HipDIMeasurementTool.ts 42 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484
  1. import { Point3, Point2, fitCircleFrom3Points, vectorAdd, calculateMidpoint, calculateMidpointFor2 } 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. movingWholeTool?: boolean;
  92. wholeToolOffset?: CoreTypes.Point2;
  93. } | null = null;
  94. isDrawing: boolean = false;
  95. /**
  96. * 创建一个预设的注解,带有6个默认点
  97. * @param element HTML元素
  98. * @param viewport Viewport实例
  99. * @param config 可选的初始配置
  100. */
  101. static createDefaultAnnotation(
  102. element: HTMLDivElement,
  103. viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport,
  104. config?: AnnotationInitialConfig
  105. ): HipDIAnnotation {
  106. const enabledElement = getEnabledElement(element);
  107. if (!enabledElement) {
  108. throw new Error('Element is not enabled');
  109. }
  110. // 获取viewport的尺寸
  111. const canvas = viewport.canvas;
  112. const { width, height } = canvas;
  113. const centerX = width / 2;
  114. const centerY = height / 2;
  115. // 应用默认值
  116. const acetabulumRadius = config?.acetabulumRadius ?? 80;
  117. const femurRadius = config?.femurRadius ?? 60;
  118. const circleDistance = config?.circleDistance ?? 100;
  119. // 创建髋臼圆的3个点(左侧圆,点1,2,3)
  120. // 在圆周上均匀分布3个点(0度、120度、240度)
  121. const acetabulumCenterX = centerX - circleDistance / 2;
  122. const acetabulumCenterY = centerY;
  123. const angle1 = 0;
  124. const angle2 = (2 * Math.PI) / 3; // 120度
  125. const angle3 = (4 * Math.PI) / 3; // 240度
  126. const acetabulumPoints: CoreTypes.Point2[] = [
  127. [
  128. acetabulumCenterX + acetabulumRadius * Math.cos(angle1),
  129. acetabulumCenterY + acetabulumRadius * Math.sin(angle1)
  130. ],
  131. [
  132. acetabulumCenterX + acetabulumRadius * Math.cos(angle2),
  133. acetabulumCenterY + acetabulumRadius * Math.sin(angle2)
  134. ],
  135. [
  136. acetabulumCenterX + acetabulumRadius * Math.cos(angle3),
  137. acetabulumCenterY + acetabulumRadius * Math.sin(angle3)
  138. ]
  139. ];
  140. // 创建股骨头圆的3个点(右侧圆,点4,5,6)
  141. const femurCenterX = centerX + circleDistance / 2;
  142. const femurCenterY = centerY;
  143. const femurPoints: CoreTypes.Point2[] = [
  144. [
  145. femurCenterX + femurRadius * Math.cos(angle1),
  146. femurCenterY + femurRadius * Math.sin(angle1)
  147. ],
  148. [
  149. femurCenterX + femurRadius * Math.cos(angle2),
  150. femurCenterY + femurRadius * Math.sin(angle2)
  151. ],
  152. [
  153. femurCenterX + femurRadius * Math.cos(angle3),
  154. femurCenterY + femurRadius * Math.sin(angle3)
  155. ]
  156. ];
  157. const canvasPoints: CoreTypes.Point2[] = [
  158. ...acetabulumPoints,
  159. ...femurPoints
  160. ];
  161. // 转换为world坐标
  162. const worldPoints = canvasPoints.map((canvasPoint) =>
  163. viewport.canvasToWorld(canvasPoint)
  164. );
  165. const camera = viewport.getCamera();
  166. const { viewPlaneNormal, viewUp } = camera;
  167. if (viewPlaneNormal === undefined) {
  168. throw new Error('viewPlaneNormal is undefined');
  169. }
  170. if (viewUp === undefined) {
  171. throw new Error('viewUp is undefined');
  172. }
  173. const annotationData = {
  174. invalidated: true,
  175. highlighted: false,
  176. metadata: {
  177. viewPlaneNormal: [...viewPlaneNormal] as CoreTypes.Point3,
  178. viewUp: [...viewUp] as CoreTypes.Point3,
  179. FrameOfReferenceUID: viewport.getFrameOfReferenceUID(),
  180. referencedImageId: viewport.getCurrentImageId?.() || '',
  181. toolName: HipDIMeasurementTool.toolName,
  182. },
  183. data: {
  184. label: '',
  185. handles: {
  186. points: worldPoints,
  187. activeHandleIndex: null,
  188. },
  189. cachedStats: {},
  190. },
  191. } as HipDIAnnotation;
  192. return annotationData;
  193. }
  194. /**
  195. * 导出注解数据
  196. * @param annotation 要导出的注解
  197. * @returns 导出的数据对象
  198. */
  199. static exportAnnotationData(
  200. annotation: HipDIAnnotation,
  201. viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport
  202. ): ExportedDIData | null {
  203. const targetId = `imageId:${viewport.getCurrentImageId?.() || ''}`;
  204. const cachedStats = annotation.data.cachedStats?.[targetId];
  205. if (!cachedStats || !cachedStats.acetabulumCircle || !cachedStats.femurCircle) {
  206. return null;
  207. }
  208. return {
  209. points: [...annotation.data.handles.points],
  210. circles: {
  211. acetabulum: {
  212. center: cachedStats.acetabulumCircle.center,
  213. radius: cachedStats.acetabulumCircle.radius,
  214. },
  215. femur: {
  216. center: cachedStats.femurCircle.center,
  217. radius: cachedStats.femurCircle.radius,
  218. },
  219. },
  220. measurements: {
  221. distance: cachedStats.distance,
  222. DI: cachedStats.DI,
  223. },
  224. textBoxPosition: annotation.data.textBox,
  225. metadata: {
  226. viewPlaneNormal: annotation.metadata?.viewPlaneNormal || [0, 0, 1],
  227. viewUp: annotation.metadata?.viewUp || [0, 1, 0],
  228. FrameOfReferenceUID: annotation.metadata?.FrameOfReferenceUID || '',
  229. referencedImageId: annotation.metadata?.referencedImageId || '',
  230. },
  231. };
  232. }
  233. /**
  234. * 从导出的数据恢复注解
  235. * @param exportedData 导出的数据
  236. * @param element HTML元素
  237. * @param viewport Viewport实例
  238. * @returns 恢复的注解
  239. */
  240. static restoreFromExportedData(
  241. exportedData: ExportedDIData,
  242. element: HTMLDivElement,
  243. viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport
  244. ): HipDIAnnotation {
  245. const enabledElement = getEnabledElement(element);
  246. if (!enabledElement) {
  247. throw new Error('Element is not enabled');
  248. }
  249. const annotationData = {
  250. invalidated: true,
  251. highlighted: false,
  252. metadata: {
  253. ...exportedData.metadata,
  254. toolName: HipDIMeasurementTool.toolName,
  255. },
  256. data: {
  257. label: '',
  258. handles: {
  259. points: [...exportedData.points],
  260. activeHandleIndex: null,
  261. },
  262. textBox: exportedData.textBoxPosition,
  263. cachedStats: {},
  264. },
  265. } as HipDIAnnotation;
  266. return annotationData;
  267. }
  268. constructor(
  269. toolProps: PublicToolProps = {},
  270. defaultToolProps: ToolProps = {
  271. supportedInteractionTypes: ['Mouse', 'Touch'],
  272. configuration: {
  273. shadow: true,
  274. preventHandleOutsideImage: false,
  275. },
  276. }
  277. ) {
  278. super(toolProps, defaultToolProps);
  279. }
  280. /**
  281. * 添加新注解 - 禁用此功能
  282. */
  283. addNewAnnotation(
  284. evt: EventTypes.InteractionEventType
  285. ): HipDIAnnotation {
  286. evt.preventDefault();
  287. return {} as HipDIAnnotation;
  288. }
  289. /**
  290. * 检查点是否靠近工具
  291. */
  292. isPointNearTool(
  293. element: HTMLDivElement,
  294. annotation: HipDIAnnotation,
  295. canvasCoords: CoreTypes.Point2,
  296. proximity: number
  297. ): boolean {
  298. const enabledElement = getEnabledElement(element);
  299. if (!enabledElement) {
  300. return false;
  301. }
  302. const { viewport } = enabledElement;
  303. const points = annotation.data.handles.points;
  304. // 检查是否靠近任意一个手柄点
  305. for (let i = 0; i < points.length; i++) {
  306. const point = points[i];
  307. const canvasPoint = viewport.worldToCanvas(point);
  308. const distance = Math.sqrt(
  309. Math.pow(canvasPoint[0] - canvasCoords[0], 2) +
  310. Math.pow(canvasPoint[1] - canvasCoords[1], 2)
  311. );
  312. if (distance < proximity) {
  313. return true;
  314. }
  315. }
  316. // 检查是否靠近圆心连线
  317. const targetId = this.getTargetId(viewport);
  318. const cachedStats = targetId ? annotation.data.cachedStats?.[targetId] : undefined;
  319. if (cachedStats?.acetabulumCircle && cachedStats?.femurCircle) {
  320. const M1 = cachedStats.acetabulumCircle.center;
  321. const M2 = cachedStats.femurCircle.center;
  322. const M1Canvas = viewport.worldToCanvas(M1);
  323. const M2Canvas = viewport.worldToCanvas(M2);
  324. const dist = this._distanceToSegment(canvasCoords, M1Canvas, M2Canvas);
  325. if (dist < proximity) {
  326. console.log('靠近圆心连线');
  327. return true;
  328. }
  329. }
  330. return false;
  331. }
  332. /**
  333. * 检查点击是否在文本框内
  334. */
  335. private _isPointInTextBox(
  336. canvasPoint: CoreTypes.Point2,
  337. textBoxPosition: CoreTypes.Point2,
  338. textWidth: number = 100,
  339. textHeight: number = 40
  340. ): boolean {
  341. const [x, y] = canvasPoint;
  342. const [tx, ty] = textBoxPosition;
  343. const margin = 10;
  344. return (
  345. x >= tx - margin &&
  346. x <= tx + textWidth + margin &&
  347. y >= ty - margin &&
  348. y <= ty + textHeight + margin
  349. );
  350. }
  351. /**
  352. * 计算点到线段的距离
  353. */
  354. private _distanceToSegment(
  355. point: CoreTypes.Point2,
  356. lineStart: CoreTypes.Point2,
  357. lineEnd: CoreTypes.Point2
  358. ): number {
  359. const x = point[0];
  360. const y = point[1];
  361. const x1 = lineStart[0];
  362. const y1 = lineStart[1];
  363. const x2 = lineEnd[0];
  364. const y2 = lineEnd[1];
  365. const A = x - x1;
  366. const B = y - y1;
  367. const C = x2 - x1;
  368. const D = y2 - y1;
  369. const dot = A * C + B * D;
  370. const lenSq = C * C + D * D;
  371. let param = -1;
  372. if (lenSq !== 0) {
  373. param = dot / lenSq;
  374. }
  375. let xx, yy;
  376. if (param < 0) {
  377. xx = x1;
  378. yy = y1;
  379. } else if (param > 1) {
  380. xx = x2;
  381. yy = y2;
  382. } else {
  383. xx = x1 + param * C;
  384. yy = y1 + param * D;
  385. }
  386. const dx = x - xx;
  387. const dy = y - yy;
  388. return Math.sqrt(dx * dx + dy * dy);
  389. }
  390. /**
  391. * 计算圆在canvas坐标系下的像素半径
  392. * @param circle 圆的几何信息
  393. * @param viewport 视口对象
  394. * @returns 像素半径
  395. */
  396. private _getCircleRadiusInPixels(
  397. circle: { center: CoreTypes.Point3; radius: number },
  398. viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport
  399. ): number {
  400. const { center, radius } = circle;
  401. // 沿X轴方向获取距离圆心radius的点
  402. const radiusVector: Point3 = [radius, 0, 0];
  403. const endPoint: Point3 = vectorAdd(center, radiusVector);
  404. // 分别转换圆心和圆周上的点到canvas坐标
  405. const centerCanvas = viewport.worldToCanvas(center);
  406. const endCanvas = viewport.worldToCanvas(endPoint);
  407. // 计算两个canvas点之间的距离就是像素半径
  408. const radiusPixels = Math.sqrt(
  409. Math.pow(endCanvas[0] - centerCanvas[0], 2) +
  410. Math.pow(endCanvas[1] - centerCanvas[1], 2)
  411. );
  412. return radiusPixels;
  413. }
  414. /**
  415. * 取消操作
  416. */
  417. cancel(element: HTMLDivElement): string {
  418. if (this.isDrawing) {
  419. this.isDrawing = false;
  420. this._deactivateDraw(element);
  421. this._deactivateModify(element);
  422. const enabledElement = getEnabledElement(element);
  423. if (enabledElement) {
  424. const viewportIdsToRender =
  425. utilities.viewportFilters.getViewportIdsWithToolToRender(
  426. element,
  427. this.getToolName()
  428. );
  429. utilities.triggerAnnotationRenderForViewportIds(
  430. viewportIdsToRender
  431. );
  432. }
  433. this.editData = null;
  434. return this.getToolName();
  435. }
  436. return '';
  437. }
  438. /**
  439. * 激活绘制模式
  440. */
  441. _activateDraw(element: HTMLDivElement): void {
  442. element.addEventListener(
  443. 'CORNERSTONE_TOOLS_MOUSE_DRAG',
  444. this._dragCallback as EventListener
  445. );
  446. element.addEventListener(
  447. 'CORNERSTONE_TOOLS_MOUSE_UP',
  448. this._endCallback as EventListener
  449. );
  450. element.addEventListener(
  451. 'CORNERSTONE_TOOLS_MOUSE_CLICK',
  452. this._endCallback as EventListener
  453. );
  454. }
  455. /**
  456. * 取消激活绘制模式
  457. */
  458. _deactivateDraw(element: HTMLDivElement): void {
  459. element.removeEventListener(
  460. 'CORNERSTONE_TOOLS_MOUSE_DRAG',
  461. this._dragCallback as EventListener
  462. );
  463. element.removeEventListener(
  464. 'CORNERSTONE_TOOLS_MOUSE_UP',
  465. this._endCallback as EventListener
  466. );
  467. element.removeEventListener(
  468. 'CORNERSTONE_TOOLS_MOUSE_CLICK',
  469. this._endCallback as EventListener
  470. );
  471. }
  472. /**
  473. * 激活修改模式
  474. */
  475. _activateModify(element: HTMLDivElement): void {
  476. element.addEventListener(
  477. 'CORNERSTONE_TOOLS_MOUSE_DOWN',
  478. this._mouseDownModifyCallback as EventListener
  479. );
  480. element.addEventListener(
  481. 'CORNERSTONE_TOOLS_MOUSE_DRAG',
  482. this._mouseDragModifyCallback as EventListener
  483. );
  484. element.addEventListener(
  485. 'CORNERSTONE_TOOLS_MOUSE_UP',
  486. this._mouseUpModifyCallback as EventListener
  487. );
  488. element.addEventListener(
  489. 'CORNERSTONE_TOOLS_MOUSE_MOVE',
  490. this._mouseMoveModifyCallback as EventListener
  491. );
  492. element.addEventListener(
  493. 'keydown',
  494. this._keyDownCallback as EventListener
  495. );
  496. }
  497. /**
  498. * 取消激活修改模式
  499. */
  500. _deactivateModify(element: HTMLDivElement): void {
  501. element.removeEventListener(
  502. 'CORNERSTONE_TOOLS_MOUSE_DOWN',
  503. this._mouseDownModifyCallback as EventListener
  504. );
  505. element.removeEventListener(
  506. 'CORNERSTONE_TOOLS_MOUSE_DRAG',
  507. this._mouseDragModifyCallback as EventListener
  508. );
  509. element.removeEventListener(
  510. 'CORNERSTONE_TOOLS_MOUSE_UP',
  511. this._mouseUpModifyCallback as EventListener
  512. );
  513. element.removeEventListener(
  514. 'CORNERSTONE_TOOLS_MOUSE_MOVE',
  515. this._mouseMoveModifyCallback as EventListener
  516. );
  517. element.removeEventListener(
  518. 'keydown',
  519. this._keyDownCallback as EventListener
  520. );
  521. }
  522. /**
  523. * 鼠标按下回调 - 用于修改模式
  524. */
  525. _mouseDownModifyCallback = (evt: EventTypes.InteractionEventType): void => {
  526. const eventDetail = evt.detail;
  527. const { element, currentPoints } = eventDetail;
  528. const canvasCoords = currentPoints.canvas;
  529. const enabledElement = getEnabledElement(element);
  530. if (!enabledElement) {
  531. return;
  532. }
  533. const { viewport } = enabledElement;
  534. const annotations = annotation.state.getAnnotations(this.getToolName(), element);
  535. if (!annotations || annotations.length === 0) {
  536. return;
  537. }
  538. // 先检查是否点击在文本框上
  539. for (const ann of annotations) {
  540. const customAnn = ann as HipDIAnnotation;
  541. const targetId = this.getTargetId(viewport);
  542. const cachedStats = targetId ? customAnn.data.cachedStats?.[targetId] : undefined;
  543. if (cachedStats?.femurCircle) {
  544. const femurCenterCanvas = viewport.worldToCanvas(cachedStats.femurCircle.center);
  545. const textBox: CoreTypes.Point2 = customAnn.data.textBox || [
  546. femurCenterCanvas[0] + 20,
  547. femurCenterCanvas[1] - 20,
  548. ];
  549. if (this._isPointInTextBox(canvasCoords, textBox)) {
  550. const viewportIdsToRender =
  551. utilities.viewportFilters.getViewportIdsWithToolToRender(
  552. element,
  553. this.getToolName()
  554. );
  555. const textBoxOffset: CoreTypes.Point2 = [
  556. canvasCoords[0] - textBox[0],
  557. canvasCoords[1] - textBox[1],
  558. ];
  559. this.editData = {
  560. annotation: customAnn,
  561. viewportIdsToRender,
  562. hasMoved: false,
  563. textBoxBeingMoved: true,
  564. textBoxOffset: textBoxOffset,
  565. };
  566. // 点击文本时也选中工具
  567. customAnn.isSelected = true;
  568. customAnn.highlighted = true;
  569. utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
  570. evt.preventDefault();
  571. evt.stopPropagation();
  572. return;
  573. }
  574. }
  575. }
  576. // 查找最近的手柄
  577. for (const ann of annotations) {
  578. const customAnn = ann as HipDIAnnotation;
  579. const handle = this.getHandleNearImagePoint(
  580. element,
  581. customAnn,
  582. canvasCoords,
  583. 6
  584. );
  585. if (handle) {
  586. const viewportIdsToRender =
  587. utilities.viewportFilters.getViewportIdsWithToolToRender(
  588. element,
  589. this.getToolName()
  590. );
  591. this.editData = {
  592. annotation: customAnn,
  593. viewportIdsToRender,
  594. handleIndex: customAnn.data.handles.activeHandleIndex || 0,
  595. hasMoved: false,
  596. };
  597. customAnn.isSelected = true; // 选中工具
  598. utilities.triggerAnnotationRenderForViewportIds(
  599. viewportIdsToRender
  600. );
  601. evt.preventDefault();
  602. evt.stopPropagation();
  603. return;
  604. }
  605. }
  606. // 如果没有找到手柄,检查是否点击在工具的其他位置(先检测圆周,再检测连线)
  607. for (const ann of annotations) {
  608. const customAnn = ann as HipDIAnnotation;
  609. const targetId = this.getTargetId(viewport);
  610. const cachedStats = targetId ? customAnn.data.cachedStats?.[targetId] : undefined;
  611. // 检查是否点击在圆周上
  612. if (cachedStats?.acetabulumCircle && cachedStats?.femurCircle) {
  613. const acetabulumCenterCanvas = viewport.worldToCanvas(cachedStats.acetabulumCircle.center);
  614. const femurCenterCanvas = viewport.worldToCanvas(cachedStats.femurCircle.center);
  615. // 计算鼠标点到髋臼圆心的距离
  616. const distanceToAcetabulum = Math.sqrt(
  617. Math.pow(canvasCoords[0] - acetabulumCenterCanvas[0], 2) +
  618. Math.pow(canvasCoords[1] - acetabulumCenterCanvas[1], 2)
  619. );
  620. // 计算鼠标点到股骨头圆心的距离
  621. const distanceToFemur = Math.sqrt(
  622. Math.pow(canvasCoords[0] - femurCenterCanvas[0], 2) +
  623. Math.pow(canvasCoords[1] - femurCenterCanvas[1], 2)
  624. );
  625. // 使用统一的函数计算像素半径
  626. const acetabulumRadiusPixels = this._getCircleRadiusInPixels(cachedStats.acetabulumCircle, viewport);
  627. const femurRadiusPixels = this._getCircleRadiusInPixels(cachedStats.femurCircle, viewport);
  628. // 定义圆周点击容差(像素)
  629. const circleTolerance = 8;
  630. // 检查是否点击在圆周上(允许一定的容差)
  631. const onAcetabulumCircle = Math.abs(distanceToAcetabulum - acetabulumRadiusPixels) <= circleTolerance;
  632. const onFemurCircle = Math.abs(distanceToFemur - femurRadiusPixels) <= circleTolerance;
  633. if (onAcetabulumCircle || onFemurCircle) {
  634. console.log(`在圆周附近 8 px 内`)
  635. const viewportIdsToRender =
  636. utilities.viewportFilters.getViewportIdsWithToolToRender(
  637. element,
  638. this.getToolName()
  639. );
  640. // 使用圆心连线中点作为偏移基准
  641. const midPointCanvas = calculateMidpointFor2(acetabulumCenterCanvas, femurCenterCanvas);
  642. const wholeToolOffset: CoreTypes.Point2 = [
  643. canvasCoords[0] - midPointCanvas[0],
  644. canvasCoords[1] - midPointCanvas[1],
  645. ];
  646. this.editData = {
  647. annotation: customAnn,
  648. viewportIdsToRender,
  649. hasMoved: false,
  650. movingWholeTool: true,
  651. wholeToolOffset: wholeToolOffset,
  652. };
  653. customAnn.isSelected = true; // 选中工具
  654. utilities.triggerAnnotationRenderForViewportIds(
  655. viewportIdsToRender
  656. );
  657. evt.preventDefault();
  658. evt.stopPropagation();
  659. return;
  660. }
  661. }
  662. // 检查是否点击在圆心连线上
  663. if (this.isPointNearTool(element, customAnn, canvasCoords, 10)) {
  664. const viewportIdsToRender =
  665. utilities.viewportFilters.getViewportIdsWithToolToRender(
  666. element,
  667. this.getToolName()
  668. );
  669. // 计算鼠标点击位置相对于连线中点的偏移量
  670. const targetId = this.getTargetId(viewport);
  671. const cachedStats = targetId ? customAnn.data.cachedStats?.[targetId] : undefined;
  672. if (cachedStats?.acetabulumCircle && cachedStats.femurCircle) {
  673. const M1Canvas = viewport.worldToCanvas(cachedStats.acetabulumCircle.center);
  674. const M2Canvas = viewport.worldToCanvas(cachedStats.femurCircle.center);
  675. const midPointCanvas = calculateMidpointFor2(M1Canvas, M2Canvas);
  676. const wholeToolOffset: CoreTypes.Point2 = [
  677. canvasCoords[0] - midPointCanvas[0],
  678. canvasCoords[1] - midPointCanvas[1],
  679. ];
  680. this.editData = {
  681. annotation: customAnn,
  682. viewportIdsToRender,
  683. hasMoved: false,
  684. movingWholeTool: true,
  685. wholeToolOffset: wholeToolOffset,
  686. };
  687. customAnn.isSelected = true; // 选中工具
  688. utilities.triggerAnnotationRenderForViewportIds(
  689. viewportIdsToRender
  690. );
  691. evt.preventDefault();
  692. evt.stopPropagation();
  693. return;
  694. }
  695. }
  696. }
  697. // 如果没有点击在工具上,取消所有工具的选中状态
  698. for (const ann of annotations) {
  699. const customAnn = ann as HipDIAnnotation;
  700. customAnn.isSelected = false;
  701. }
  702. };
  703. /**
  704. * 鼠标拖拽回调 - 用于修改模式
  705. */
  706. _mouseDragModifyCallback = (evt: EventTypes.InteractionEventType): void => {
  707. if (!this.editData) {
  708. return;
  709. }
  710. const eventDetail = evt.detail;
  711. const { currentPoints } = eventDetail;
  712. const canvasCoords = currentPoints.canvas;
  713. const enabledElement = getEnabledElement(eventDetail.element);
  714. if (!enabledElement) {
  715. return;
  716. }
  717. const { annotation: ann, viewportIdsToRender, textBoxBeingMoved } = this.editData;
  718. const customAnn = ann as HipDIAnnotation;
  719. const { data } = customAnn;
  720. // 如果正在拖拽文本框
  721. if (textBoxBeingMoved && this.editData.textBoxOffset) {
  722. const newTextBoxPosition: CoreTypes.Point2 = [
  723. canvasCoords[0] - this.editData.textBoxOffset[0],
  724. canvasCoords[1] - this.editData.textBoxOffset[1],
  725. ];
  726. data.textBox = newTextBoxPosition;
  727. this.editData.hasMoved = true;
  728. utilities.triggerAnnotationRenderForViewportIds(
  729. viewportIdsToRender
  730. );
  731. evt.preventDefault();
  732. evt.stopPropagation();
  733. return;
  734. }
  735. // 如果正在移动整个工具
  736. if (this.editData.movingWholeTool && this.editData.wholeToolOffset) {
  737. const { viewport } = enabledElement;
  738. // 计算新的中点位置
  739. let newMidCanvas: CoreTypes.Point2 = [
  740. canvasCoords[0] - this.editData.wholeToolOffset[0],
  741. canvasCoords[1] - this.editData.wholeToolOffset[1],
  742. ];
  743. const targetId = this.getTargetId(viewport);
  744. const cachedStats = targetId ? customAnn.data.cachedStats?.[targetId] : undefined;
  745. if (cachedStats?.acetabulumCircle && cachedStats.femurCircle) {
  746. // 获取当前的世界坐标中点
  747. const M1Canvas = viewport.worldToCanvas(cachedStats.acetabulumCircle.center);
  748. const M2Canvas = viewport.worldToCanvas(cachedStats.femurCircle.center);
  749. const oldMidCanvas = calculateMidpointFor2(M1Canvas, M2Canvas);
  750. // 计算偏移量(新的中点相对旧的中点)
  751. const offsetCanvas: CoreTypes.Point2 = [
  752. newMidCanvas[0] - oldMidCanvas[0],
  753. newMidCanvas[1] - oldMidCanvas[1],
  754. ];
  755. // 将canvas偏移转换为world偏移
  756. const offsetWorld = [
  757. viewport.canvasToWorld(offsetCanvas)[0] - viewport.canvasToWorld([0, 0])[0],
  758. viewport.canvasToWorld(offsetCanvas)[1] - viewport.canvasToWorld([0, 0])[1],
  759. 0, // z轴偏移为0,保持在2D平面上
  760. ] as CoreTypes.Point3;
  761. // 应用相同的偏移量给所有相关点
  762. for (let i = 0; i < data.handles.points.length; i++) {
  763. data.handles.points[i] = [
  764. data.handles.points[i][0] + offsetWorld[0],
  765. data.handles.points[i][1] + offsetWorld[1],
  766. data.handles.points[i][2],
  767. ];
  768. }
  769. // 更新文本框位置
  770. if (data.textBox) {
  771. data.textBox[0] += offsetCanvas[0];
  772. data.textBox[1] += offsetCanvas[1];
  773. }
  774. // 重新计算圆心和统计数据
  775. this._updateCachedStats(customAnn, enabledElement);
  776. this.editData.hasMoved = true;
  777. utilities.triggerAnnotationRenderForViewportIds(
  778. viewportIdsToRender
  779. );
  780. evt.preventDefault();
  781. evt.stopPropagation();
  782. return;
  783. }
  784. }
  785. // 处理手柄拖拽
  786. const worldPos = currentPoints.world;
  787. const activeHandleIndex = data.handles.activeHandleIndex;
  788. if (activeHandleIndex !== null && activeHandleIndex >= 0 && activeHandleIndex < data.handles.points.length) {
  789. data.handles.points[activeHandleIndex] = worldPos;
  790. this._updateCachedStats(customAnn, enabledElement);
  791. this.editData.hasMoved = true;
  792. utilities.triggerAnnotationRenderForViewportIds(
  793. viewportIdsToRender
  794. );
  795. evt.preventDefault();
  796. evt.stopPropagation();
  797. }
  798. };
  799. /**
  800. * 鼠标释放回调 - 用于修改模式
  801. */
  802. _mouseUpModifyCallback = (evt: EventTypes.InteractionEventType): void => {
  803. if (!this.editData) {
  804. return;
  805. }
  806. const { annotation: ann } = this.editData;
  807. const customAnn = ann as HipDIAnnotation;
  808. customAnn.data.handles.activeHandleIndex = null;
  809. customAnn.highlighted = false;
  810. const eventDetail = evt.detail;
  811. const { element } = eventDetail;
  812. const viewportIdsToRender =
  813. utilities.viewportFilters.getViewportIdsWithToolToRender(
  814. element,
  815. this.getToolName()
  816. );
  817. utilities.triggerAnnotationRenderForViewportIds(
  818. viewportIdsToRender
  819. );
  820. this.editData = null;
  821. evt.preventDefault();
  822. evt.stopPropagation();
  823. };
  824. /**
  825. * 鼠标移动回调 - 用于修改模式,处理悬停检测
  826. * 实现功能1和2:鼠标悬停在文本上时光标为手型,悬停在线段或手柄上时光标为十字准星
  827. */
  828. _mouseMoveModifyCallback = (evt: EventTypes.InteractionEventType): void => {
  829. const eventDetail = evt.detail;
  830. const { element, currentPoints } = eventDetail;
  831. if (!currentPoints || !currentPoints.canvas) {
  832. return;
  833. }
  834. const canvasCoords = currentPoints.canvas;
  835. const enabledElement = getEnabledElement(element);
  836. if (!enabledElement) {
  837. return;
  838. }
  839. const { viewport } = enabledElement;
  840. const annotations = annotation.state.getAnnotations(this.getToolName(), element);
  841. if (!annotations || annotations.length === 0) {
  842. element.style.cursor = 'default';
  843. return;
  844. }
  845. let isHovering = false;
  846. // 检查是否悬停在文本框上
  847. for (const ann of annotations) {
  848. const customAnn = ann as HipDIAnnotation;
  849. const targetId = this.getTargetId(viewport);
  850. const cachedStats = targetId ? customAnn.data.cachedStats?.[targetId] : undefined;
  851. if (cachedStats?.femurCircle) {
  852. const femurCenterCanvas = viewport.worldToCanvas(cachedStats.femurCircle.center);
  853. const textBox: CoreTypes.Point2 = customAnn.data.textBox || [
  854. femurCenterCanvas[0] + 20,
  855. femurCenterCanvas[1] - 20,
  856. ];
  857. if (this._isPointInTextBox(canvasCoords, textBox)) {
  858. element.style.cursor = 'pointer'; // 手型光标
  859. customAnn.highlighted = true;
  860. isHovering = true;
  861. break;
  862. }
  863. }
  864. }
  865. // 如果没有悬停在文本框上,检查是否悬停在手柄、线段或圆周上
  866. if (!isHovering) {
  867. for (const ann of annotations) {
  868. const customAnn = ann as HipDIAnnotation;
  869. const targetId = this.getTargetId(viewport);
  870. const cachedStats = targetId ? customAnn.data.cachedStats?.[targetId] : undefined;
  871. // 检查是否靠近手柄
  872. const handle = this.getHandleNearImagePoint(element, customAnn, canvasCoords, 6);
  873. if (handle) {
  874. element.style.cursor = 'crosshair'; // 十字准星光标
  875. customAnn.highlighted = true;
  876. isHovering = true;
  877. break;
  878. }
  879. // 检查是否靠近圆心连线(线段)
  880. if (this.isPointNearTool(element, customAnn, canvasCoords, 10)) {
  881. element.style.cursor = 'crosshair'; // 十字准星光标
  882. customAnn.highlighted = true;
  883. isHovering = true;
  884. break;
  885. }
  886. // 检查是否悬停在圆周上
  887. if (cachedStats?.acetabulumCircle && cachedStats?.femurCircle) {
  888. const acetabulumCenterCanvas = viewport.worldToCanvas(cachedStats.acetabulumCircle.center);
  889. const femurCenterCanvas = viewport.worldToCanvas(cachedStats.femurCircle.center);
  890. // 计算鼠标点到圆心的距离
  891. const distanceToAcetabulum = Math.sqrt(
  892. Math.pow(canvasCoords[0] - acetabulumCenterCanvas[0], 2) +
  893. Math.pow(canvasCoords[1] - acetabulumCenterCanvas[1], 2)
  894. );
  895. const distanceToFemur = Math.sqrt(
  896. Math.pow(canvasCoords[0] - femurCenterCanvas[0], 2) +
  897. Math.pow(canvasCoords[1] - femurCenterCanvas[1], 2)
  898. );
  899. // 使用统一的函数计算像素半径
  900. const acetabulumRadiusPixels = this._getCircleRadiusInPixels(cachedStats.acetabulumCircle, viewport);
  901. const femurRadiusPixels = this._getCircleRadiusInPixels(cachedStats.femurCircle, viewport);
  902. // 检查是否悬停在圆周附近(允许一定的容差)
  903. const hoverTolerance = 8;
  904. const onAcetabulumCircle = Math.abs(distanceToAcetabulum - acetabulumRadiusPixels) <= hoverTolerance;
  905. const onFemurCircle = Math.abs(distanceToFemur - femurRadiusPixels) <= hoverTolerance;
  906. if (onAcetabulumCircle || onFemurCircle) {
  907. element.style.cursor = 'crosshair'; // 十字准星光标
  908. customAnn.highlighted = true;
  909. isHovering = true;
  910. break;
  911. }
  912. }
  913. }
  914. }
  915. // 如果没有悬停在任何地方,重置高亮和光标
  916. if (!isHovering) {
  917. for (const ann of annotations) {
  918. const customAnn = ann as HipDIAnnotation;
  919. customAnn.highlighted = false;
  920. }
  921. element.style.cursor = 'default';
  922. }
  923. // 触发渲染以更新高亮状态
  924. const viewportIdsToRender = utilities.viewportFilters.getViewportIdsWithToolToRender(
  925. element,
  926. this.getToolName()
  927. );
  928. utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
  929. };
  930. /**
  931. * 键盘事件回调 - 处理delete键删除
  932. */
  933. _keyDownCallback = (evt: KeyboardEvent): void => {
  934. if (evt.key === 'Delete') {
  935. const element = document.activeElement as HTMLDivElement;
  936. if (!element) return;
  937. const annotations = annotation.state.getAnnotations(this.getToolName(), element);
  938. // 查找选中的注解
  939. const selectedAnnotation = annotations.find(ann => ann.isSelected);
  940. if (selectedAnnotation) {
  941. // 删除选中的注解
  942. annotation.state.removeAnnotation(selectedAnnotation.annotationUID!);
  943. // 触发视图更新
  944. const viewportIdsToRender =
  945. utilities.viewportFilters.getViewportIdsWithToolToRender(
  946. element,
  947. this.getToolName()
  948. );
  949. utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
  950. evt.preventDefault();
  951. evt.stopPropagation();
  952. }
  953. }
  954. };
  955. /**
  956. * 处理选中回调
  957. */
  958. handleSelectedCallback(
  959. evt: EventTypes.InteractionEventType,
  960. annotation: HipDIAnnotation
  961. ): void {
  962. const eventDetail = evt.detail;
  963. const { element } = eventDetail;
  964. annotation.highlighted = true;
  965. const enabledElement = getEnabledElement(element);
  966. if (!enabledElement) {
  967. return;
  968. }
  969. const viewportIdsToRender =
  970. utilities.viewportFilters.getViewportIdsWithToolToRender(
  971. element,
  972. this.getToolName()
  973. );
  974. utilities.triggerAnnotationRenderForViewportIds(
  975. viewportIdsToRender
  976. );
  977. evt.preventDefault();
  978. }
  979. /**
  980. * 工具选中回调
  981. */
  982. toolSelectedCallback(
  983. evt: EventTypes.InteractionEventType,
  984. annotation: HipDIAnnotation
  985. ): void {
  986. // 实现工具选中逻辑
  987. }
  988. /**
  989. * 处理鼠标拖拽
  990. */
  991. _dragCallback = (evt: EventTypes.InteractionEventType): void => {
  992. this.isDrawing = true;
  993. const eventDetail = evt.detail;
  994. const { element } = eventDetail;
  995. const enabledElement = getEnabledElement(element);
  996. if (!enabledElement || !this.editData) {
  997. return;
  998. }
  999. const { annotation, viewportIdsToRender } = this.editData;
  1000. const { data } = annotation as HipDIAnnotation;
  1001. if (annotation) {
  1002. const { currentPoints } = eventDetail;
  1003. const worldPos = currentPoints.world;
  1004. const points = data.handles.points;
  1005. const activeHandleIndex = data.handles.activeHandleIndex;
  1006. if (activeHandleIndex !== null && activeHandleIndex < points.length) {
  1007. points[activeHandleIndex] = worldPos;
  1008. this._updateCachedStats(
  1009. annotation as HipDIAnnotation,
  1010. enabledElement
  1011. );
  1012. this.editData!.hasMoved = true;
  1013. }
  1014. utilities.triggerAnnotationRenderForViewportIds(
  1015. viewportIdsToRender
  1016. );
  1017. }
  1018. };
  1019. /**
  1020. * 处理鼠标抬起/点击
  1021. */
  1022. _endCallback = (evt: EventTypes.InteractionEventType): void => {
  1023. const eventDetail = evt.detail;
  1024. const { element } = eventDetail;
  1025. if (!this.editData) {
  1026. return;
  1027. }
  1028. const { annotation, hasMoved } = this.editData;
  1029. const { data } = annotation as HipDIAnnotation;
  1030. if (hasMoved) {
  1031. this.editData.hasMoved = false;
  1032. }
  1033. };
  1034. /**
  1035. * 获取靠近图像点的手柄
  1036. */
  1037. getHandleNearImagePoint(
  1038. element: HTMLDivElement,
  1039. annotation: HipDIAnnotation,
  1040. canvasCoords: CoreTypes.Point2,
  1041. proximity: number
  1042. ): Types.ToolHandle | undefined {
  1043. const enabledElement = getEnabledElement(element);
  1044. if (!enabledElement) {
  1045. return undefined;
  1046. }
  1047. const { viewport } = enabledElement;
  1048. const points = annotation.data.handles.points;
  1049. const handleProximity = Math.max(proximity, 15);
  1050. for (let i = 0; i < points.length; i++) {
  1051. const point = points[i];
  1052. const canvasPoint = viewport.worldToCanvas(point);
  1053. const distance = Math.sqrt(
  1054. Math.pow(canvasPoint[0] - canvasCoords[0], 2) +
  1055. Math.pow(canvasPoint[1] - canvasCoords[1], 2)
  1056. );
  1057. if (distance < handleProximity) {
  1058. annotation.data.handles.activeHandleIndex = i;
  1059. return {
  1060. worldPosition: point,
  1061. } as Types.ToolHandle;
  1062. }
  1063. }
  1064. annotation.data.handles.activeHandleIndex = null;
  1065. return undefined;
  1066. }
  1067. /**
  1068. * 更新缓存的统计数据
  1069. */
  1070. _updateCachedStats(
  1071. annotation: HipDIAnnotation,
  1072. enabledElement: CoreTypes.IEnabledElement
  1073. ): void {
  1074. const { viewport } = enabledElement;
  1075. const { data } = annotation;
  1076. const points = data.handles.points;
  1077. const targetId = this.getTargetId(viewport);
  1078. if (!targetId) {
  1079. return;
  1080. }
  1081. if (!data.cachedStats) {
  1082. data.cachedStats = {};
  1083. }
  1084. if (!data.cachedStats[targetId]) {
  1085. data.cachedStats[targetId] = {
  1086. distance: 0,
  1087. DI: 0,
  1088. };
  1089. }
  1090. const stats = data.cachedStats[targetId];
  1091. if (points.length >= 6) {
  1092. // 拟合髋臼圆(点1,2,3)
  1093. const acetabulumCircle = fitCircleFrom3Points(
  1094. points[0],
  1095. points[1],
  1096. points[2]
  1097. );
  1098. // 拟合股骨头圆(点4,5,6)
  1099. const femurCircle = fitCircleFrom3Points(
  1100. points[3],
  1101. points[4],
  1102. points[5]
  1103. );
  1104. if (!acetabulumCircle || !femurCircle) {
  1105. console.warn('圆拟合失败,三点可能共线');
  1106. return;
  1107. }
  1108. // 存储圆数据
  1109. stats.acetabulumCircle = acetabulumCircle;
  1110. stats.femurCircle = femurCircle;
  1111. // 计算圆心距离d
  1112. const M1 = acetabulumCircle.center;
  1113. const M2 = femurCircle.center;
  1114. const distance = Math.sqrt(
  1115. Math.pow(M2[0] - M1[0], 2) +
  1116. Math.pow(M2[1] - M1[1], 2) +
  1117. Math.pow(M2[2] - M1[2], 2)
  1118. );
  1119. stats.distance = distance;
  1120. // 计算DI指数
  1121. const DI = distance / femurCircle.radius;
  1122. stats.DI = DI;
  1123. }
  1124. }
  1125. /**
  1126. * 渲染注解
  1127. */
  1128. renderAnnotation = (
  1129. enabledElement: CoreTypes.IEnabledElement,
  1130. svgDrawingHelper: SVGDrawingHelper
  1131. ): boolean => {
  1132. let renderStatus = false;
  1133. const { viewport } = enabledElement;
  1134. const { element } = viewport;
  1135. let annotations = annotation.state.getAnnotations(this.getToolName(), element);
  1136. if (!annotations?.length) {
  1137. return renderStatus;
  1138. }
  1139. for (let i = 0; i < annotations.length; i++) {
  1140. const annotation = annotations[i] as HipDIAnnotation;
  1141. const { annotationUID, data } = annotation;
  1142. const points = data.handles.points;
  1143. if (points.length < 1) {
  1144. continue;
  1145. }
  1146. const targetId = this.getTargetId(viewport);
  1147. const cachedStats = targetId ? data.cachedStats?.[targetId] : undefined;
  1148. // 确定颜色(选中状态为蓝色,非选中为白色)
  1149. const color = annotation.highlighted ? 'rgb(0, 0, 255)' : 'rgb(255, 255, 255)';
  1150. // 转换所有点为 canvas 坐标
  1151. const canvasPoints = points.map((p) => viewport.worldToCanvas(p));
  1152. // 绘制髋臼圆
  1153. if (cachedStats?.acetabulumCircle && annotationUID) {
  1154. const center = cachedStats.acetabulumCircle.center;
  1155. const centerCanvas = viewport.worldToCanvas(center);
  1156. // 计算半径的像素值
  1157. const radiusVector: Point3 = [cachedStats.acetabulumCircle.radius, 0, 0];
  1158. const endPoint = vectorAdd(center, radiusVector);
  1159. const endCanvas = viewport.worldToCanvas(endPoint);
  1160. const radiusPixels = Math.sqrt(
  1161. Math.pow(endCanvas[0] - centerCanvas[0], 2) +
  1162. Math.pow(endCanvas[1] - centerCanvas[1], 2)
  1163. );
  1164. const circleUID = `${annotationUID}-acetabulum-circle`;
  1165. drawCircleSvg(
  1166. svgDrawingHelper,
  1167. annotationUID,
  1168. circleUID,
  1169. centerCanvas,
  1170. radiusPixels,
  1171. {
  1172. color: color,
  1173. width: 2,
  1174. }
  1175. );
  1176. }
  1177. // 绘制股骨头圆
  1178. if (cachedStats?.femurCircle && annotationUID) {
  1179. const center = cachedStats.femurCircle.center;
  1180. const centerCanvas = viewport.worldToCanvas(center);
  1181. // 计算半径的像素值
  1182. const radiusVector: Point3 = [cachedStats.femurCircle.radius, 0, 0];
  1183. const endPoint = vectorAdd(center, radiusVector);
  1184. const endCanvas = viewport.worldToCanvas(endPoint);
  1185. const radiusPixels = Math.sqrt(
  1186. Math.pow(endCanvas[0] - centerCanvas[0], 2) +
  1187. Math.pow(endCanvas[1] - centerCanvas[1], 2)
  1188. );
  1189. const circleUID = `${annotationUID}-femur-circle`;
  1190. drawCircleSvg(
  1191. svgDrawingHelper,
  1192. annotationUID,
  1193. circleUID,
  1194. centerCanvas,
  1195. radiusPixels,
  1196. {
  1197. color: color,
  1198. width: 2,
  1199. }
  1200. );
  1201. }
  1202. // 绘制圆心连线
  1203. if (cachedStats?.acetabulumCircle && cachedStats?.femurCircle && annotationUID) {
  1204. const M1 = cachedStats.acetabulumCircle.center;
  1205. const M2 = cachedStats.femurCircle.center;
  1206. const M1Canvas = viewport.worldToCanvas(M1);
  1207. const M2Canvas = viewport.worldToCanvas(M2);
  1208. const lineUID = `${annotationUID}-center-line`;
  1209. drawLineSvg(
  1210. svgDrawingHelper,
  1211. annotationUID,
  1212. lineUID,
  1213. M1Canvas,
  1214. M2Canvas,
  1215. {
  1216. color: color,
  1217. width: 2,
  1218. }
  1219. );
  1220. }
  1221. // 绘制6个控制点
  1222. if (annotationUID) {
  1223. const handleGroupUID = '0';
  1224. // 根据工具选中状态调整手柄半径:选中时为12,否则为6
  1225. const isSelected = annotation.isSelected || annotation.highlighted;
  1226. const handleRadius = isSelected ? 12 : 6;
  1227. drawHandles(
  1228. svgDrawingHelper,
  1229. annotationUID,
  1230. handleGroupUID,
  1231. canvasPoints,
  1232. {
  1233. color: 'rgb(255, 255, 255)',
  1234. handleRadius: handleRadius,
  1235. }
  1236. );
  1237. }
  1238. // 绘制DI数值标注(黄色,可拖拽)
  1239. if (cachedStats?.femurCircle && cachedStats.DI !== undefined && annotationUID) {
  1240. const femurCenterCanvas = viewport.worldToCanvas(cachedStats.femurCircle.center);
  1241. const textLines = [`DI: ${cachedStats.DI.toFixed(2)}`];
  1242. const textUID = `${annotationUID}-DI-text`;
  1243. const textBoxPosition: CoreTypes.Point2 = data.textBox || [
  1244. femurCenterCanvas[0] + 20,
  1245. femurCenterCanvas[1] - 20,
  1246. ];
  1247. drawLinkedTextBox(
  1248. svgDrawingHelper,
  1249. annotationUID,
  1250. textUID,
  1251. textLines,
  1252. textBoxPosition,
  1253. [femurCenterCanvas],
  1254. {},
  1255. {
  1256. color: 'rgb(255, 255, 0)', // 黄色
  1257. }
  1258. );
  1259. }
  1260. renderStatus = true;
  1261. }
  1262. return renderStatus;
  1263. };
  1264. }