Browse Source

添加动物专用测量工具--髋关节牵引指数

sw 5 days ago
parent
commit
e2dee7162c

+ 1032 - 0
src/components/measures/HipDIMeasurementTool.ts

@@ -0,0 +1,1032 @@
+import { Point3, Point2, fitCircleFrom3Points, vectorAdd } from './mathUtils';
+import {
+  utilities as csUtils,
+  Types as CoreTypes,
+  getEnabledElement,
+} from '@cornerstonejs/core';
+
+import {
+  AnnotationTool,
+  utilities,
+  Types,
+  annotation,
+  drawing,
+} from '@cornerstonejs/tools';
+
+const {
+  drawHandles,
+  drawLinkedTextBox,
+  drawLine: drawLineSvg,
+  drawCircle: drawCircleSvg,
+} = drawing;
+
+import {
+  PublicToolProps,
+  ToolProps,
+  EventTypes,
+  SVGDrawingHelper,
+} from '@cornerstonejs/tools/dist/esm/types';
+
+// 注解初始配置接口
+interface AnnotationInitialConfig {
+  acetabulumRadius?: number;  // 髋臼圆初始半径(像素)
+  femurRadius?: number;       // 股骨头圆初始半径(像素)
+  circleDistance?: number;    // 两圆心之间的初始距离(像素)
+}
+
+// 注解数据接口
+interface HipDIAnnotation extends Types.Annotation {
+  data: {
+    handles: {
+      points: CoreTypes.Point3[];
+      activeHandleIndex: number | null;
+    };
+    textBox?: CoreTypes.Point2; // DI文本框位置
+    cachedStats?: {
+      [targetId: string]: {
+        acetabulumCircle?: {
+          center: CoreTypes.Point3;
+          radius: number;
+        };
+        femurCircle?: {
+          center: CoreTypes.Point3;
+          radius: number;
+        };
+        distance: number;
+        DI: number;
+      };
+    };
+  };
+}
+
+// 导出的注解数据接口
+export interface ExportedDIData {
+  points: CoreTypes.Point3[];           // 6个点的世界坐标
+  circles: {
+    acetabulum: {
+      center: CoreTypes.Point3;
+      radius: number;
+    };
+    femur: {
+      center: CoreTypes.Point3;
+      radius: number;
+    };
+  };
+  measurements: {
+    distance: number;
+    DI: number;
+  };
+  textBoxPosition?: CoreTypes.Point2;
+  metadata: {
+    viewPlaneNormal: CoreTypes.Point3;
+    viewUp: CoreTypes.Point3;
+    FrameOfReferenceUID: string;
+    referencedImageId: string;
+  };
+}
+
+export default class HipDIMeasurementTool extends AnnotationTool {
+  static toolName = 'HipDIMeasurementTool';
+
+  editData: {
+    annotation: Types.Annotation;
+    viewportIdsToRender: string[];
+    handleIndex?: number;
+    newAnnotation?: boolean;
+    hasMoved?: boolean;
+    textBoxBeingMoved?: boolean;
+    textBoxOffset?: CoreTypes.Point2;
+  } | null = null;
+
+  isDrawing: boolean = false;
+
+  /**
+   * 创建一个预设的注解,带有6个默认点
+   * @param element HTML元素
+   * @param viewport Viewport实例
+   * @param config 可选的初始配置
+   */
+  static createDefaultAnnotation(
+    element: HTMLDivElement,
+    viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport,
+    config?: AnnotationInitialConfig
+  ): HipDIAnnotation {
+    const enabledElement = getEnabledElement(element);
+    if (!enabledElement) {
+      throw new Error('Element is not enabled');
+    }
+
+    // 获取viewport的尺寸
+    const canvas = viewport.canvas;
+    const { width, height } = canvas;
+    const centerX = width / 2;
+    const centerY = height / 2;
+
+    // 应用默认值
+    const acetabulumRadius = config?.acetabulumRadius ?? 80;
+    const femurRadius = config?.femurRadius ?? 60;
+    const circleDistance = config?.circleDistance ?? 100;
+
+    // 创建髋臼圆的3个点(左侧圆,点1,2,3)
+    // 在圆周上均匀分布3个点(0度、120度、240度)
+    const acetabulumCenterX = centerX - circleDistance / 2;
+    const acetabulumCenterY = centerY;
+    
+    const angle1 = 0;
+    const angle2 = (2 * Math.PI) / 3;  // 120度
+    const angle3 = (4 * Math.PI) / 3;  // 240度
+
+    const acetabulumPoints: CoreTypes.Point2[] = [
+      [
+        acetabulumCenterX + acetabulumRadius * Math.cos(angle1),
+        acetabulumCenterY + acetabulumRadius * Math.sin(angle1)
+      ],
+      [
+        acetabulumCenterX + acetabulumRadius * Math.cos(angle2),
+        acetabulumCenterY + acetabulumRadius * Math.sin(angle2)
+      ],
+      [
+        acetabulumCenterX + acetabulumRadius * Math.cos(angle3),
+        acetabulumCenterY + acetabulumRadius * Math.sin(angle3)
+      ]
+    ];
+
+    // 创建股骨头圆的3个点(右侧圆,点4,5,6)
+    const femurCenterX = centerX + circleDistance / 2;
+    const femurCenterY = centerY;
+
+    const femurPoints: CoreTypes.Point2[] = [
+      [
+        femurCenterX + femurRadius * Math.cos(angle1),
+        femurCenterY + femurRadius * Math.sin(angle1)
+      ],
+      [
+        femurCenterX + femurRadius * Math.cos(angle2),
+        femurCenterY + femurRadius * Math.sin(angle2)
+      ],
+      [
+        femurCenterX + femurRadius * Math.cos(angle3),
+        femurCenterY + femurRadius * Math.sin(angle3)
+      ]
+    ];
+
+    const canvasPoints: CoreTypes.Point2[] = [
+      ...acetabulumPoints,
+      ...femurPoints
+    ];
+
+    // 转换为world坐标
+    const worldPoints = canvasPoints.map((canvasPoint) =>
+      viewport.canvasToWorld(canvasPoint)
+    );
+
+    const camera = viewport.getCamera();
+    const { viewPlaneNormal, viewUp } = camera;
+    if (viewPlaneNormal === undefined) {
+      throw new Error('viewPlaneNormal is undefined');
+    }
+    if (viewUp === undefined) {
+      throw new Error('viewUp is undefined');
+    }
+
+    const annotationData = {
+      invalidated: true,
+      highlighted: false,
+      metadata: {
+        viewPlaneNormal: [...viewPlaneNormal] as CoreTypes.Point3,
+        viewUp: [...viewUp] as CoreTypes.Point3,
+        FrameOfReferenceUID: viewport.getFrameOfReferenceUID(),
+        referencedImageId: viewport.getCurrentImageId?.() || '',
+        toolName: HipDIMeasurementTool.toolName,
+      },
+      data: {
+        label: '',
+        handles: {
+          points: worldPoints,
+          activeHandleIndex: null,
+        },
+        cachedStats: {},
+      },
+    } as HipDIAnnotation;
+
+    return annotationData;
+  }
+
+  /**
+   * 导出注解数据
+   * @param annotation 要导出的注解
+   * @returns 导出的数据对象
+   */
+  static exportAnnotationData(
+    annotation: HipDIAnnotation,
+    viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport
+  ): ExportedDIData | null {
+    const targetId = `imageId:${viewport.getCurrentImageId?.() || ''}`;
+    const cachedStats = annotation.data.cachedStats?.[targetId];
+
+    if (!cachedStats || !cachedStats.acetabulumCircle || !cachedStats.femurCircle) {
+      return null;
+    }
+
+    return {
+      points: [...annotation.data.handles.points],
+      circles: {
+        acetabulum: {
+          center: cachedStats.acetabulumCircle.center,
+          radius: cachedStats.acetabulumCircle.radius,
+        },
+        femur: {
+          center: cachedStats.femurCircle.center,
+          radius: cachedStats.femurCircle.radius,
+        },
+      },
+      measurements: {
+        distance: cachedStats.distance,
+        DI: cachedStats.DI,
+      },
+      textBoxPosition: annotation.data.textBox,
+      metadata: {
+        viewPlaneNormal: annotation.metadata?.viewPlaneNormal || [0, 0, 1],
+        viewUp: annotation.metadata?.viewUp || [0, 1, 0],
+        FrameOfReferenceUID: annotation.metadata?.FrameOfReferenceUID || '',
+        referencedImageId: annotation.metadata?.referencedImageId || '',
+      },
+    };
+  }
+
+  /**
+   * 从导出的数据恢复注解
+   * @param exportedData 导出的数据
+   * @param element HTML元素
+   * @param viewport Viewport实例
+   * @returns 恢复的注解
+   */
+  static restoreFromExportedData(
+    exportedData: ExportedDIData,
+    element: HTMLDivElement,
+    viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport
+  ): HipDIAnnotation {
+    const enabledElement = getEnabledElement(element);
+    if (!enabledElement) {
+      throw new Error('Element is not enabled');
+    }
+
+    const annotationData = {
+      invalidated: true,
+      highlighted: false,
+      metadata: {
+        ...exportedData.metadata,
+        toolName: HipDIMeasurementTool.toolName,
+      },
+      data: {
+        label: '',
+        handles: {
+          points: [...exportedData.points],
+          activeHandleIndex: null,
+        },
+        textBox: exportedData.textBoxPosition,
+        cachedStats: {},
+      },
+    } as HipDIAnnotation;
+
+    return annotationData;
+  }
+
+  constructor(
+    toolProps: PublicToolProps = {},
+    defaultToolProps: ToolProps = {
+      supportedInteractionTypes: ['Mouse', 'Touch'],
+      configuration: {
+        shadow: true,
+        preventHandleOutsideImage: false,
+      },
+    }
+  ) {
+    super(toolProps, defaultToolProps);
+  }
+
+  /**
+   * 添加新注解 - 禁用此功能
+   */
+  addNewAnnotation(
+    evt: EventTypes.InteractionEventType
+  ): HipDIAnnotation {
+    evt.preventDefault();
+    return {} as HipDIAnnotation;
+  }
+
+  /**
+   * 检查点是否靠近工具
+   */
+  isPointNearTool(
+    element: HTMLDivElement,
+    annotation: HipDIAnnotation,
+    canvasCoords: CoreTypes.Point2,
+    proximity: number
+  ): boolean {
+    const enabledElement = getEnabledElement(element);
+    if (!enabledElement) {
+      return false;
+    }
+    const { viewport } = enabledElement;
+
+    const points = annotation.data.handles.points;
+
+    // 检查是否靠近任意一个手柄点
+    for (let i = 0; i < points.length; i++) {
+      const point = points[i];
+      const canvasPoint = viewport.worldToCanvas(point);
+      const distance = Math.sqrt(
+        Math.pow(canvasPoint[0] - canvasCoords[0], 2) +
+        Math.pow(canvasPoint[1] - canvasCoords[1], 2)
+      );
+
+      if (distance < proximity) {
+        return true;
+      }
+    }
+
+    return false;
+  }
+
+  /**
+   * 检查点击是否在文本框内
+   */
+  private _isPointInTextBox(
+    canvasPoint: CoreTypes.Point2,
+    textBoxPosition: CoreTypes.Point2,
+    textWidth: number = 100,
+    textHeight: number = 40
+  ): boolean {
+    const [x, y] = canvasPoint;
+    const [tx, ty] = textBoxPosition;
+    
+    const margin = 10;
+    return (
+      x >= tx - margin &&
+      x <= tx + textWidth + margin &&
+      y >= ty - margin &&
+      y <= ty + textHeight + margin
+    );
+  }
+
+  /**
+   * 取消操作
+   */
+  cancel(element: HTMLDivElement): string {
+    if (this.isDrawing) {
+      this.isDrawing = false;
+      this._deactivateDraw(element);
+      this._deactivateModify(element);
+
+      const enabledElement = getEnabledElement(element);
+      if (enabledElement) {
+        const viewportIdsToRender =
+          utilities.viewportFilters.getViewportIdsWithToolToRender(
+            element,
+            this.getToolName()
+          );
+
+        utilities.triggerAnnotationRenderForViewportIds(
+          viewportIdsToRender
+        );
+      }
+
+      this.editData = null;
+      return this.getToolName();
+    }
+
+    return '';
+  }
+
+  /**
+   * 激活绘制模式
+   */
+  _activateDraw(element: HTMLDivElement): void {
+    element.addEventListener(
+      'CORNERSTONE_TOOLS_MOUSE_DRAG',
+      this._dragCallback as EventListener
+    );
+    element.addEventListener(
+      'CORNERSTONE_TOOLS_MOUSE_UP',
+      this._endCallback as EventListener
+    );
+    element.addEventListener(
+      'CORNERSTONE_TOOLS_MOUSE_CLICK',
+      this._endCallback as EventListener
+    );
+  }
+
+  /**
+   * 取消激活绘制模式
+   */
+  _deactivateDraw(element: HTMLDivElement): void {
+    element.removeEventListener(
+      'CORNERSTONE_TOOLS_MOUSE_DRAG',
+      this._dragCallback as EventListener
+    );
+    element.removeEventListener(
+      'CORNERSTONE_TOOLS_MOUSE_UP',
+      this._endCallback as EventListener
+    );
+    element.removeEventListener(
+      'CORNERSTONE_TOOLS_MOUSE_CLICK',
+      this._endCallback as EventListener
+    );
+  }
+
+  /**
+   * 激活修改模式
+   */
+  _activateModify(element: HTMLDivElement): void {
+    element.addEventListener(
+      'CORNERSTONE_TOOLS_MOUSE_DOWN',
+      this._mouseDownModifyCallback as EventListener
+    );
+    element.addEventListener(
+      'CORNERSTONE_TOOLS_MOUSE_DRAG',
+      this._mouseDragModifyCallback as EventListener
+    );
+    element.addEventListener(
+      'CORNERSTONE_TOOLS_MOUSE_UP',
+      this._mouseUpModifyCallback as EventListener
+    );
+  }
+
+  /**
+   * 取消激活修改模式
+   */
+  _deactivateModify(element: HTMLDivElement): void {
+    element.removeEventListener(
+      'CORNERSTONE_TOOLS_MOUSE_DOWN',
+      this._mouseDownModifyCallback as EventListener
+    );
+    element.removeEventListener(
+      'CORNERSTONE_TOOLS_MOUSE_DRAG',
+      this._mouseDragModifyCallback as EventListener
+    );
+    element.removeEventListener(
+      'CORNERSTONE_TOOLS_MOUSE_UP',
+      this._mouseUpModifyCallback as EventListener
+    );
+  }
+
+  /**
+   * 鼠标按下回调 - 用于修改模式
+   */
+  _mouseDownModifyCallback = (evt: EventTypes.InteractionEventType): void => {
+    const eventDetail = evt.detail;
+    const { element, currentPoints } = eventDetail;
+    const canvasCoords = currentPoints.canvas;
+
+    const enabledElement = getEnabledElement(element);
+    if (!enabledElement) {
+      return;
+    }
+    const { viewport } = enabledElement;
+
+    const annotations = annotation.state.getAnnotations(this.getToolName(), element);
+    
+    if (!annotations || annotations.length === 0) {
+      return;
+    }
+
+    // 先检查是否点击在文本框上
+    for (const ann of annotations) {
+      const customAnn = ann as HipDIAnnotation;
+      const targetId = this.getTargetId(viewport);
+      const cachedStats = targetId ? customAnn.data.cachedStats?.[targetId] : undefined;
+
+      if (cachedStats?.femurCircle) {
+        const femurCenterCanvas = viewport.worldToCanvas(cachedStats.femurCircle.center);
+        const textBox: CoreTypes.Point2 = customAnn.data.textBox || [
+          femurCenterCanvas[0] + 20,
+          femurCenterCanvas[1] - 20,
+        ];
+
+        if (this._isPointInTextBox(canvasCoords, textBox)) {
+          const viewportIdsToRender =
+            utilities.viewportFilters.getViewportIdsWithToolToRender(
+              element,
+              this.getToolName()
+            );
+
+          const textBoxOffset: CoreTypes.Point2 = [
+            canvasCoords[0] - textBox[0],
+            canvasCoords[1] - textBox[1],
+          ];
+
+          this.editData = {
+            annotation: customAnn,
+            viewportIdsToRender,
+            hasMoved: false,
+            textBoxBeingMoved: true,
+            textBoxOffset: textBoxOffset,
+          };
+
+          customAnn.highlighted = true;
+          utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
+
+          evt.preventDefault();
+          evt.stopPropagation();
+          return;
+        }
+      }
+    }
+
+    // 查找最近的手柄
+    for (const ann of annotations) {
+      const customAnn = ann as HipDIAnnotation;
+      const handle = this.getHandleNearImagePoint(
+        element,
+        customAnn,
+        canvasCoords,
+        6
+      );
+
+      if (handle) {
+        const viewportIdsToRender =
+          utilities.viewportFilters.getViewportIdsWithToolToRender(
+            element,
+            this.getToolName()
+          );
+
+        this.editData = {
+          annotation: customAnn,
+          viewportIdsToRender,
+          handleIndex: customAnn.data.handles.activeHandleIndex || 0,
+          hasMoved: false,
+        };
+
+        customAnn.highlighted = true;
+
+        utilities.triggerAnnotationRenderForViewportIds(
+          viewportIdsToRender
+        );
+
+        evt.preventDefault();
+        evt.stopPropagation();
+        return;
+      }
+    }
+  };
+
+  /**
+   * 鼠标拖拽回调 - 用于修改模式
+   */
+  _mouseDragModifyCallback = (evt: EventTypes.InteractionEventType): void => {
+    if (!this.editData) {
+      return;
+    }
+
+    const eventDetail = evt.detail;
+    const { currentPoints } = eventDetail;
+    const canvasCoords = currentPoints.canvas;
+
+    const enabledElement = getEnabledElement(eventDetail.element);
+    if (!enabledElement) {
+      return;
+    }
+
+    const { annotation: ann, viewportIdsToRender, textBoxBeingMoved } = this.editData;
+    const customAnn = ann as HipDIAnnotation;
+    const { data } = customAnn;
+
+    // 如果正在拖拽文本框
+    if (textBoxBeingMoved && this.editData.textBoxOffset) {
+      const newTextBoxPosition: CoreTypes.Point2 = [
+        canvasCoords[0] - this.editData.textBoxOffset[0],
+        canvasCoords[1] - this.editData.textBoxOffset[1],
+      ];
+
+      data.textBox = newTextBoxPosition;
+      this.editData.hasMoved = true;
+
+      utilities.triggerAnnotationRenderForViewportIds(
+        viewportIdsToRender
+      );
+
+      evt.preventDefault();
+      evt.stopPropagation();
+      return;
+    }
+
+    // 处理手柄拖拽
+    const worldPos = currentPoints.world;
+    const activeHandleIndex = data.handles.activeHandleIndex;
+
+    if (activeHandleIndex !== null && activeHandleIndex >= 0 && activeHandleIndex < data.handles.points.length) {
+      data.handles.points[activeHandleIndex] = worldPos;
+
+      this._updateCachedStats(customAnn, enabledElement);
+
+      this.editData.hasMoved = true;
+
+      utilities.triggerAnnotationRenderForViewportIds(
+        viewportIdsToRender
+      );
+
+      evt.preventDefault();
+      evt.stopPropagation();
+    }
+  };
+
+  /**
+   * 鼠标释放回调 - 用于修改模式
+   */
+  _mouseUpModifyCallback = (evt: EventTypes.InteractionEventType): void => {
+    if (!this.editData) {
+      return;
+    }
+
+    const { annotation: ann } = this.editData;
+    const customAnn = ann as HipDIAnnotation;
+
+    customAnn.data.handles.activeHandleIndex = null;
+    customAnn.highlighted = false;
+
+    const eventDetail = evt.detail;
+    const { element } = eventDetail;
+
+    const viewportIdsToRender =
+      utilities.viewportFilters.getViewportIdsWithToolToRender(
+        element,
+        this.getToolName()
+      );
+
+    utilities.triggerAnnotationRenderForViewportIds(
+      viewportIdsToRender
+    );
+
+    this.editData = null;
+
+    evt.preventDefault();
+    evt.stopPropagation();
+  };
+
+  /**
+   * 处理选中回调
+   */
+  handleSelectedCallback(
+    evt: EventTypes.InteractionEventType,
+    annotation: HipDIAnnotation
+  ): void {
+    const eventDetail = evt.detail;
+    const { element } = eventDetail;
+
+    annotation.highlighted = true;
+
+    const enabledElement = getEnabledElement(element);
+    if (!enabledElement) {
+      return;
+    }
+
+    const viewportIdsToRender =
+      utilities.viewportFilters.getViewportIdsWithToolToRender(
+        element,
+        this.getToolName()
+      );
+
+    utilities.triggerAnnotationRenderForViewportIds(
+      viewportIdsToRender
+    );
+
+    evt.preventDefault();
+  }
+
+  /**
+   * 工具选中回调
+   */
+  toolSelectedCallback(
+    evt: EventTypes.InteractionEventType,
+    annotation: HipDIAnnotation
+  ): void {
+    // 实现工具选中逻辑
+  }
+
+  /**
+   * 处理鼠标拖拽
+   */
+  _dragCallback = (evt: EventTypes.InteractionEventType): void => {
+    this.isDrawing = true;
+    const eventDetail = evt.detail;
+    const { element } = eventDetail;
+
+    const enabledElement = getEnabledElement(element);
+    if (!enabledElement || !this.editData) {
+      return;
+    }
+
+    const { annotation, viewportIdsToRender } = this.editData;
+    const { data } = annotation as HipDIAnnotation;
+
+    if (annotation) {
+      const { currentPoints } = eventDetail;
+      const worldPos = currentPoints.world;
+
+      const points = data.handles.points;
+      const activeHandleIndex = data.handles.activeHandleIndex;
+
+      if (activeHandleIndex !== null && activeHandleIndex < points.length) {
+        points[activeHandleIndex] = worldPos;
+
+        this._updateCachedStats(
+          annotation as HipDIAnnotation,
+          enabledElement
+        );
+
+        this.editData!.hasMoved = true;
+      }
+
+      utilities.triggerAnnotationRenderForViewportIds(
+        viewportIdsToRender
+      );
+    }
+  };
+
+  /**
+   * 处理鼠标抬起/点击
+   */
+  _endCallback = (evt: EventTypes.InteractionEventType): void => {
+    const eventDetail = evt.detail;
+    const { element } = eventDetail;
+
+    if (!this.editData) {
+      return;
+    }
+
+    const { annotation, hasMoved } = this.editData;
+    const { data } = annotation as HipDIAnnotation;
+
+    if (hasMoved) {
+      this.editData.hasMoved = false;
+    }
+  };
+
+  /**
+   * 获取靠近图像点的手柄
+   */
+  getHandleNearImagePoint(
+    element: HTMLDivElement,
+    annotation: HipDIAnnotation,
+    canvasCoords: CoreTypes.Point2,
+    proximity: number
+  ): Types.ToolHandle | undefined {
+    const enabledElement = getEnabledElement(element);
+    if (!enabledElement) {
+      return undefined;
+    }
+    const { viewport } = enabledElement;
+
+    const points = annotation.data.handles.points;
+
+    const handleProximity = Math.max(proximity, 15);
+
+    for (let i = 0; i < points.length; i++) {
+      const point = points[i];
+      const canvasPoint = viewport.worldToCanvas(point);
+      const distance = Math.sqrt(
+        Math.pow(canvasPoint[0] - canvasCoords[0], 2) +
+        Math.pow(canvasPoint[1] - canvasCoords[1], 2)
+      );
+
+      if (distance < handleProximity) {
+        annotation.data.handles.activeHandleIndex = i;
+        return {
+          worldPosition: point,
+        } as Types.ToolHandle;
+      }
+    }
+    
+    annotation.data.handles.activeHandleIndex = null;
+    return undefined;
+  }
+
+  /**
+   * 更新缓存的统计数据
+   */
+  _updateCachedStats(
+    annotation: HipDIAnnotation,
+    enabledElement: CoreTypes.IEnabledElement
+  ): void {
+    const { viewport } = enabledElement;
+    const { data } = annotation;
+    const points = data.handles.points;
+
+    const targetId = this.getTargetId(viewport);
+    if (!targetId) {
+      return;
+    }
+
+    if (!data.cachedStats) {
+      data.cachedStats = {};
+    }
+
+    if (!data.cachedStats[targetId]) {
+      data.cachedStats[targetId] = {
+        distance: 0,
+        DI: 0,
+      };
+    }
+
+    const stats = data.cachedStats[targetId];
+
+    if (points.length >= 6) {
+      // 拟合髋臼圆(点1,2,3)
+      const acetabulumCircle = fitCircleFrom3Points(
+        points[0],
+        points[1],
+        points[2]
+      );
+
+      // 拟合股骨头圆(点4,5,6)
+      const femurCircle = fitCircleFrom3Points(
+        points[3],
+        points[4],
+        points[5]
+      );
+
+      if (!acetabulumCircle || !femurCircle) {
+        console.warn('圆拟合失败,三点可能共线');
+        return;
+      }
+
+      // 存储圆数据
+      stats.acetabulumCircle = acetabulumCircle;
+      stats.femurCircle = femurCircle;
+
+      // 计算圆心距离d
+      const M1 = acetabulumCircle.center;
+      const M2 = femurCircle.center;
+
+      const distance = Math.sqrt(
+        Math.pow(M2[0] - M1[0], 2) +
+        Math.pow(M2[1] - M1[1], 2) +
+        Math.pow(M2[2] - M1[2], 2)
+      );
+
+      stats.distance = distance;
+
+      // 计算DI指数
+      const DI = distance / femurCircle.radius;
+      stats.DI = DI;
+    }
+  }
+
+  /**
+   * 渲染注解
+   */
+  renderAnnotation = (
+    enabledElement: CoreTypes.IEnabledElement,
+    svgDrawingHelper: SVGDrawingHelper
+  ): boolean => {
+    let renderStatus = false;
+    const { viewport } = enabledElement;
+    const { element } = viewport;
+
+    let annotations = annotation.state.getAnnotations(this.getToolName(), element);
+
+    if (!annotations?.length) {
+      return renderStatus;
+    }
+
+    for (let i = 0; i < annotations.length; i++) {
+      const annotation = annotations[i] as HipDIAnnotation;
+      const { annotationUID, data } = annotation;
+      const points = data.handles.points;
+
+      if (points.length < 1) {
+        continue;
+      }
+
+      const targetId = this.getTargetId(viewport);
+      const cachedStats = targetId ? data.cachedStats?.[targetId] : undefined;
+
+      // 确定颜色(选中状态为蓝色,非选中为白色)
+      const color = annotation.highlighted ? 'rgb(0, 0, 255)' : 'rgb(255, 255, 255)';
+
+      // 转换所有点为 canvas 坐标
+      const canvasPoints = points.map((p) => viewport.worldToCanvas(p));
+
+      // 绘制髋臼圆
+      if (cachedStats?.acetabulumCircle && annotationUID) {
+        const center = cachedStats.acetabulumCircle.center;
+        const centerCanvas = viewport.worldToCanvas(center);
+
+        // 计算半径的像素值
+        const radiusVector: Point3 = [cachedStats.acetabulumCircle.radius, 0, 0];
+        const endPoint = vectorAdd(center, radiusVector);
+        const endCanvas = viewport.worldToCanvas(endPoint);
+        const radiusPixels = Math.sqrt(
+          Math.pow(endCanvas[0] - centerCanvas[0], 2) +
+          Math.pow(endCanvas[1] - centerCanvas[1], 2)
+        );
+
+        const circleUID = `${annotationUID}-acetabulum-circle`;
+        drawCircleSvg(
+          svgDrawingHelper,
+          annotationUID,
+          circleUID,
+          centerCanvas,
+          radiusPixels,
+          {
+            color: color,
+            width: 2,
+          }
+        );
+      }
+
+      // 绘制股骨头圆
+      if (cachedStats?.femurCircle && annotationUID) {
+        const center = cachedStats.femurCircle.center;
+        const centerCanvas = viewport.worldToCanvas(center);
+
+        // 计算半径的像素值
+        const radiusVector: Point3 = [cachedStats.femurCircle.radius, 0, 0];
+        const endPoint = vectorAdd(center, radiusVector);
+        const endCanvas = viewport.worldToCanvas(endPoint);
+        const radiusPixels = Math.sqrt(
+          Math.pow(endCanvas[0] - centerCanvas[0], 2) +
+          Math.pow(endCanvas[1] - centerCanvas[1], 2)
+        );
+
+        const circleUID = `${annotationUID}-femur-circle`;
+        drawCircleSvg(
+          svgDrawingHelper,
+          annotationUID,
+          circleUID,
+          centerCanvas,
+          radiusPixels,
+          {
+            color: color,
+            width: 2,
+          }
+        );
+      }
+
+      // 绘制圆心连线
+      if (cachedStats?.acetabulumCircle && cachedStats?.femurCircle && annotationUID) {
+        const M1 = cachedStats.acetabulumCircle.center;
+        const M2 = cachedStats.femurCircle.center;
+        const M1Canvas = viewport.worldToCanvas(M1);
+        const M2Canvas = viewport.worldToCanvas(M2);
+
+        const lineUID = `${annotationUID}-center-line`;
+        drawLineSvg(
+          svgDrawingHelper,
+          annotationUID,
+          lineUID,
+          M1Canvas,
+          M2Canvas,
+          {
+            color: color,
+            width: 2,
+          }
+        );
+      }
+
+      // 绘制6个控制点
+      if (annotationUID) {
+        const handleGroupUID = '0';
+        drawHandles(
+          svgDrawingHelper,
+          annotationUID,
+          handleGroupUID,
+          canvasPoints,
+          {
+            color: 'rgb(255, 255, 255)',
+            handleRadius: 6,
+          }
+        );
+      }
+
+      // 绘制DI数值标注(黄色,可拖拽)
+      if (cachedStats?.femurCircle && cachedStats.DI !== undefined && annotationUID) {
+        const femurCenterCanvas = viewport.worldToCanvas(cachedStats.femurCircle.center);
+        const textLines = [`DI: ${cachedStats.DI.toFixed(2)}`];
+        const textUID = `${annotationUID}-DI-text`;
+        const textBoxPosition: CoreTypes.Point2 = data.textBox || [
+          femurCenterCanvas[0] + 20,
+          femurCenterCanvas[1] - 20,
+        ];
+
+        drawLinkedTextBox(
+          svgDrawingHelper,
+          annotationUID,
+          textUID,
+          textLines,
+          textBoxPosition,
+          [femurCenterCanvas],
+          {},
+          {
+            color: 'rgb(255, 255, 0)',  // 黄色
+          }
+        );
+      }
+
+      renderStatus = true;
+    }
+
+    return renderStatus;
+  };
+}

+ 38 - 0
src/pages/view/components/ViewerContainer.tsx

@@ -30,6 +30,9 @@ import StackViewer, {
   activateDARAMeasurement,
   deactivateDARAMeasurement,
   clearDARAMeasurements,
+  activateHipDIMeasurement,
+  deactivateHipDIMeasurement,
+  clearHipDIMeasurements,
 } from './viewers/stack.image.viewer';
 import { useSelector, useDispatch } from 'react-redux';
 import store, { RootState } from '@/states/store';
@@ -409,6 +412,7 @@ const ViewerContainer: React.FC<ViewerContainerProps> = ({ imageUrls }) => {
             MeasurementToolManager.clearAngleMeasurementsForViewports(selectedViewportIds);
             MeasurementToolManager.clearTibialPlateauAngleMeasurementsForViewports(selectedViewportIds);
             MeasurementToolManager.clearDARAMeasurementsForViewports(selectedViewportIds);
+            MeasurementToolManager.clearHipDIMeasurementsForViewports(selectedViewportIds);
           }
           console.log('Clearing All Measurements from MeasurementPanel');
           break;
@@ -517,6 +521,40 @@ const ViewerContainer: React.FC<ViewerContainerProps> = ({ imageUrls }) => {
           console.log('Activating DARA Measurement from MeasurementPanel');
           break;
 
+        case '髋关节牵引指数':
+          console.log(`开始髋关节牵引指数测量`);
+          if (selectedViewportIds.length > 0) {
+            selectedViewportIds.forEach((viewportId) => {
+              const success = activateHipDIMeasurement(viewportId);
+              if (success) {
+                console.log(`激活髋关节牵引指数测量工具成功`);
+                dispatch(
+                  setToolActive({
+                    toolName: 'HipDIMeasurementTool',
+                    viewportId: viewportId,
+                  })
+                );
+              }
+            });
+          } else {
+            // 如果没有选中的 viewport,为所有可见的 viewport 激活
+            const visibleViewportCount = getVisibleViewportCount();
+            for (let i = 0; i < visibleViewportCount; i++) {
+              const viewportId = `viewport-${i}`;
+              const success = activateHipDIMeasurement(viewportId);
+              if (success) {
+                dispatch(
+                  setToolActive({
+                    toolName: 'HipDIMeasurementTool',
+                    viewportId: viewportId,
+                  })
+                );
+              }
+            }
+          }
+          console.log('Activating HipDI Measurement from MeasurementPanel');
+          break;
+
         case '测量校正':
           console.log('Measurement Calibration - 功能待实现');
           break;

+ 63 - 0
src/pages/view/components/viewers/stack.image.viewer.tsx

@@ -8,6 +8,7 @@ import { registerGlobalTools } from '@/utils/cornerstoneToolsSetup';
 import { MeasurementToolManager } from '@/utils/measurementToolManager';
 import TibialPlateauAngleTool from '@/components/measures/TibialPlateauAngleTool';
 import DARAMeasurementTool from '@/components/measures/DARAMeasurementTool';
+import HipDIMeasurementTool from '@/components/measures/HipDIMeasurementTool';
 import { boolean } from 'zod';
 
 const {
@@ -121,6 +122,7 @@ function registerTools(viewportId, renderingEngineId) {
   toolGroup.addTool(AngleTool.toolName); // 添加角度测量工具
   toolGroup.addTool(TibialPlateauAngleTool.toolName); // 添加胫骨平台夹角测量工具
   toolGroup.addTool(DARAMeasurementTool.toolName); // 添加髋臼水平角测量工具
+  toolGroup.addTool(HipDIMeasurementTool.toolName); // 添加髋关节牵引指数测量工具
 
   // 设置默认工具状态
   setupDefaultToolStates(toolGroup);
@@ -171,6 +173,7 @@ function setupDefaultToolStates(toolGroup: cornerstoneTools.Types.IToolGroup) {
   toolGroup.setToolPassive(AngleTool.toolName);
   toolGroup.setToolPassive(TibialPlateauAngleTool.toolName);
   toolGroup.setToolPassive(DARAMeasurementTool.toolName);
+  toolGroup.setToolPassive(HipDIMeasurementTool.toolName);
 }
 export function addLMark(currentViewportId: string): void {
   // Implement the logic to add an L mark
@@ -737,6 +740,66 @@ export function isDARAMeasurementActive(viewportId: string): boolean {
   return MeasurementToolManager.isDARAMeasurementToolActive(viewportId);
 }
 
+// ==================== 髋关节牵引指数测量相关函数 ====================
+
+/**
+ * 激活髋关节牵引指数测量工具
+ */
+export function activateHipDIMeasurement(viewportId: string): boolean {
+  console.log(
+    `[activateHipDIMeasurement] Activating HipDI measurement for viewport: ${viewportId}`
+  );
+  return MeasurementToolManager.activateHipDIMeasurementTool(viewportId);
+}
+
+/**
+ * 停用髋关节牵引指数测量工具
+ */
+export function deactivateHipDIMeasurement(viewportId: string): boolean {
+  console.log(
+    `[deactivateHipDIMeasurement] Deactivating HipDI measurement for viewport: ${viewportId}`
+  );
+  return MeasurementToolManager.deactivateHipDIMeasurementTool(viewportId);
+}
+
+/**
+ * 切换髋关节牵引指数测量工具状态
+ */
+export function toggleHipDIMeasurement(viewportId: string): boolean {
+  console.log(
+    `[toggleHipDIMeasurement] Toggling HipDI measurement for viewport: ${viewportId}`
+  );
+  return MeasurementToolManager.toggleHipDIMeasurementTool(viewportId);
+}
+
+/**
+ * 清除髋关节牵引指数测量标注
+ */
+export function clearHipDIMeasurements(viewportId: string): boolean {
+  console.log(
+    `[clearHipDIMeasurements] Clearing HipDI measurements for viewport: ${viewportId}`
+  );
+  return MeasurementToolManager.clearHipDIMeasurements(viewportId);
+}
+
+/**
+ * 获取髋关节牵引指数测量结果
+ */
+// eslint-disable-next-line
+export function getHipDIMeasurements(viewportId: string): any[] {
+  console.log(
+    `[getHipDIMeasurements] Getting HipDI measurements for viewport: ${viewportId}`
+  );
+  return MeasurementToolManager.getHipDIMeasurements(viewportId);
+}
+
+/**
+ * 检查髋关节牵引指数测量工具是否激活
+ */
+export function isHipDIMeasurementActive(viewportId: string): boolean {
+  return MeasurementToolManager.isHipDIMeasurementToolActive(viewportId);
+}
+
 const StackViewer = ({
   imageIndex = 0,
   imageUrls = [],

+ 1 - 0
src/states/view/measurementPanelSlice.ts

@@ -9,6 +9,7 @@ export type MeasurementAction =
   | '停用线段测量'
   | '胫骨平台夹角'
   | '髋臼水平角'
+  | '髋关节牵引指数'
   | null;
 
 // 测量结果类型

+ 2 - 0
src/utils/cornerstoneToolsSetup.ts

@@ -1,6 +1,7 @@
 import * as cornerstoneTools from '@cornerstonejs/tools';
 import TibialPlateauAngleTool from '@/components/measures/TibialPlateauAngleTool';
 import DARAMeasurementTool from '@/components/measures/DARAMeasurementTool';
+import HipDIMeasurementTool from '@/components/measures/HipDIMeasurementTool';
 
 const {
   MagnifyTool,
@@ -42,6 +43,7 @@ export function registerGlobalTools(): void {
     cornerstoneTools.addTool(cornerstoneTools.AngleTool); // 添加角度测量工具
     cornerstoneTools.addTool(TibialPlateauAngleTool); // 添加胫骨平台夹角测量工具
     cornerstoneTools.addTool(DARAMeasurementTool); // 添加髋臼水平角测量工具
+    cornerstoneTools.addTool(HipDIMeasurementTool); // 添加髋关节牵引指数测量工具
 
     toolsRegistered = true;
     console.log('[cornerstoneToolsSetup] All tools registered successfully');

+ 262 - 0
src/utils/measurementToolManager.ts

@@ -2,6 +2,7 @@ import * as cornerstone from '@cornerstonejs/core';
 import * as cornerstoneTools from '@cornerstonejs/tools';
 import TibialPlateauAngleTool from '@/components/measures/TibialPlateauAngleTool';
 import DARAMeasurementTool from '@/components/measures/DARAMeasurementTool';
+import HipDIMeasurementTool from '@/components/measures/HipDIMeasurementTool';
 
 const {
   ToolGroupManager,
@@ -948,4 +949,265 @@ export class MeasurementToolManager {
       this.clearDARAMeasurements(viewportId)
     );
   }
+
+  // ==================== 髋关节牵引指数测量工具 ====================
+
+  /**
+   * 激活髋关节牵引指数测量工具
+   */
+  static activateHipDIMeasurementTool(viewportId: string): boolean {
+    const toolGroup = this.getToolGroup(viewportId);
+    if (!toolGroup) return false;
+
+    try {
+      // 停用其他可能冲突的工具
+      toolGroup.setToolPassive(WindowLevelTool.toolName, {
+        removeAllBindings: true,
+      });
+      toolGroup.setToolPassive(MagnifyTool.toolName, {
+        removeAllBindings: true,
+      });
+      toolGroup.setToolPassive(LengthTool.toolName, {
+        removeAllBindings: true,
+      });
+      toolGroup.setToolPassive(AngleTool.toolName, {
+        removeAllBindings: true,
+      });
+      toolGroup.setToolPassive(TibialPlateauAngleTool.toolName, {
+        removeAllBindings: true,
+      });
+      toolGroup.setToolPassive(DARAMeasurementTool.toolName, {
+        removeAllBindings: true,
+      });
+
+      // 激活髋关节牵引指数测量工具
+      toolGroup.setToolActive(HipDIMeasurementTool.toolName, {
+        bindings: [{ mouseButton: MouseBindings.Primary }],
+      });
+
+      // 获取工具实例并激活修改模式
+      const toolInstance = toolGroup.getToolInstance(
+        HipDIMeasurementTool.toolName
+      ) as HipDIMeasurementTool;
+
+      const viewport = cornerstone.getEnabledElementByViewportId(viewportId)?.viewport;
+      if (toolInstance && viewport.element) {
+        toolInstance._activateModify(viewport.element);
+      }
+
+      // 自动创建一个预设的注解
+      try {
+        if (viewport && viewport.element) {
+          // 创建预设注解
+          const defaultAnnotation = HipDIMeasurementTool.createDefaultAnnotation(
+            viewport.element as HTMLDivElement,
+            viewport as cornerstone.Types.IStackViewport
+          );
+
+          // 添加注解到状态管理
+          cornerstoneTools.annotation.state.addAnnotation(
+            defaultAnnotation,
+            viewport.element
+          );
+
+          // 获取工具实例并更新缓存统计数据
+          const enabledElement = cornerstone.getEnabledElement(viewport.element);
+          if (enabledElement) {
+            const toolInstance = toolGroup.getToolInstance(
+              HipDIMeasurementTool.toolName
+            ) as HipDIMeasurementTool;
+
+            if (toolInstance && '_updateCachedStats' in toolInstance) {
+              (toolInstance as any)._updateCachedStats(defaultAnnotation, enabledElement);
+            }
+          }
+
+          // 触发渲染更新
+          viewport.render();
+
+          console.log('[MeasurementToolManager] Default HipDI annotation created successfully');
+        }
+      } catch (error) {
+        console.error('[MeasurementToolManager] Failed to create default HipDI annotation:', error);
+        // 注解创建失败不影响工具激活
+      }
+
+      console.log(
+        `[MeasurementToolManager] HipDI tool activated for viewport: ${viewportId}`
+      );
+      return true;
+    } catch (error) {
+      console.error(
+        `[MeasurementToolManager] Error activating HipDI tool:`,
+        error
+      );
+      return false;
+    }
+  }
+
+  /**
+   * 停用髋关节牵引指数测量工具
+   */
+  static deactivateHipDIMeasurementTool(viewportId: string): boolean {
+    const toolGroup = this.getToolGroup(viewportId);
+    if (!toolGroup) return false;
+
+    try {
+      toolGroup.setToolPassive(HipDIMeasurementTool.toolName, {
+        removeAllBindings: true,
+      });
+      console.log(
+        `[MeasurementToolManager] HipDI tool deactivated for viewport: ${viewportId}`
+      );
+      return true;
+    } catch (error) {
+      console.error(
+        `[MeasurementToolManager] Error deactivating HipDI tool:`,
+        error
+      );
+      return false;
+    }
+  }
+
+  /**
+   * 检查髋关节牵引指数测量工具是否处于激活状态
+   */
+  static isHipDIMeasurementToolActive(viewportId: string): boolean {
+    const toolGroup = this.getToolGroup(viewportId);
+    if (!toolGroup) return false;
+
+    try {
+      const activeTool = toolGroup.getActivePrimaryMouseButtonTool();
+      return activeTool === HipDIMeasurementTool.toolName;
+    } catch (error) {
+      console.error(
+        `[MeasurementToolManager] Error checking HipDI tool state:`,
+        error
+      );
+      return false;
+    }
+  }
+
+  /**
+   * 切换髋关节牵引指数测量工具状态
+   */
+  static toggleHipDIMeasurementTool(viewportId: string): boolean {
+    const isActive = this.isHipDIMeasurementToolActive(viewportId);
+
+    if (isActive) {
+      return this.deactivateHipDIMeasurementTool(viewportId);
+    } else {
+      return this.activateHipDIMeasurementTool(viewportId);
+    }
+  }
+
+  /**
+   * 清除指定 viewport 的所有髋关节牵引指数测量标注
+   */
+  static clearHipDIMeasurements(viewportId: string): boolean {
+    try {
+      const viewport =
+        cornerstone.getEnabledElementByViewportId(viewportId)?.viewport;
+      if (!viewport) {
+        console.warn(
+          `[MeasurementToolManager] Viewport not found: ${viewportId}`
+        );
+        return false;
+      }
+
+      const annotations = cornerstoneTools.annotation.state.getAnnotations(
+        HipDIMeasurementTool.toolName,
+        viewport.element
+      );
+
+      let removedCount = 0;
+      annotations.forEach((annotation) => {
+        if (annotation.annotationUID) {
+          cornerstoneTools.annotation.state.removeAnnotation(
+            annotation.annotationUID
+          );
+          removedCount++;
+        }
+      });
+
+      viewport.render();
+      console.log(
+        `[MeasurementToolManager] Cleared ${removedCount} HipDI measurements for viewport: ${viewportId}`
+      );
+      return true;
+    } catch (error) {
+      console.error(
+        `[MeasurementToolManager] Error clearing HipDI measurements:`,
+        error
+      );
+      return false;
+    }
+  }
+
+  /**
+   * 获取指定 viewport 的所有髋关节牵引指数测量结果
+   */
+  // eslint-disable-next-line
+  static getHipDIMeasurements(viewportId: string): any[] {
+    try {
+      const viewport =
+        cornerstone.getEnabledElementByViewportId(viewportId)?.viewport;
+      if (!viewport) {
+        console.warn(
+          `[MeasurementToolManager] Viewport not found: ${viewportId}`
+        );
+        return [];
+      }
+
+      const annotations = cornerstoneTools.annotation.state.getAnnotations(
+        HipDIMeasurementTool.toolName,
+        viewport.element
+      );
+
+      return annotations.map((annotation) => ({
+        annotationUID: annotation.annotationUID,
+        DI: annotation.data?.cachedStats?.DI || 0,
+        distance: annotation.data?.cachedStats?.distance || 0,
+        unit: 'ratio',
+        points: annotation.data?.handles?.points || [],
+      }));
+    } catch (error) {
+      console.error(
+        `[MeasurementToolManager] Error getting HipDI measurements:`,
+        error
+      );
+      return [];
+    }
+  }
+
+  /**
+   * 为多个 viewport 批量激活髋关节牵引指数测量工具
+   */
+  static activateHipDIMeasurementToolForViewports(
+    viewportIds: string[]
+  ): boolean[] {
+    return viewportIds.map((viewportId) =>
+      this.activateHipDIMeasurementTool(viewportId)
+    );
+  }
+
+  /**
+   * 为多个 viewport 批量停用髋关节牵引指数测量工具
+   */
+  static deactivateHipDIMeasurementToolForViewports(
+    viewportIds: string[]
+  ): boolean[] {
+    return viewportIds.map((viewportId) =>
+      this.deactivateHipDIMeasurementTool(viewportId)
+    );
+  }
+
+  /**
+   * 为多个 viewport 批量清除髋关节牵引指数测量
+   */
+  static clearHipDIMeasurementsForViewports(viewportIds: string[]): boolean[] {
+    return viewportIds.map((viewportId) =>
+      this.clearHipDIMeasurements(viewportId)
+    );
+  }
 }