Browse Source

添加动物专用测量工具--髋臼水平角DARA

sw 5 days ago
parent
commit
552ced557a

+ 1637 - 0
src/components/measures/DARAMeasurementTool.ts

@@ -0,0 +1,1637 @@
+import { Point3, Point2, fromPoint3ToPoint2, decomposeVectorOnAxes } 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,
+  drawPath: drawPathSvg,
+} = drawing;
+
+import {
+  calculateMidpoint,
+  vectorSubtract,
+  vectorAdd,
+  vectorScale,
+  getAngleBetweenLines,
+  degrees,
+  getLineIntersection,
+  fromPoint2ToPoint3,
+} from './mathUtils';
+import {
+  PublicToolProps,
+  ToolProps,
+  EventTypes,
+  SVGDrawingHelper,
+} from '@cornerstonejs/tools/dist/esm/types';
+
+// 注解初始配置接口
+interface AnnotationInitialConfig {
+  verticalDistance?: number;         // 脊柱线段之间的竖直距离,默认200px
+  lineAWidth?: number;               // 第一条脊柱线段的宽度,默认180px
+  lineBWidth?: number;               // 第二条脊柱线段的宽度,默认220px
+  leftTangentAngle?: number;         // 左侧髋臼切线的角度(度数),默认-30度
+  rightTangentAngle?: number;        // 右侧髋臼切线的角度(度数),默认30度
+  leftTangentLength?: number;        // 左侧髋臼切线长度,默认150px
+  rightTangentLength?: number;       // 右侧髋臼切线长度,默认150px
+
+  // 左侧髋臼垂线样式配置
+  leftPerpendicularLine?: {
+    length?: number;      // 垂线长度,默认100px
+    color?: string;       // 颜色,默认 'rgb(255, 255, 0)' (黄色)
+    width?: number;       // 线宽,默认1px
+    lineDash?: string;    // 虚线样式,默认'4,4'
+  };
+
+  // 右侧髋臼垂线样式配置
+  rightPerpendicularLine?: {
+    length?: number;      // 垂线长度,默认100px
+    color?: string;       // 颜色,默认 'rgb(255, 165, 0)' (橙色)
+    width?: number;       // 线宽,默认1px
+    lineDash?: string;    // 虚线样式,默认'4,4'
+  };
+}
+
+// 注解数据接口
+interface DARAMeasurementAnnotation extends Types.Annotation {
+  data: {
+    handles: {
+      points: CoreTypes.Point3[];
+      activeHandleIndex: number | null;
+      textBoxLeft?: CoreTypes.Point2;  // 左侧角度文本框位置
+      textBoxRight?: CoreTypes.Point2; // 右侧角度文本框位置
+    };
+    cachedStats?: {
+      [targetId: string]: {
+        angleLeft: number;              // 左侧髋臼角度,取锐角
+        angleLeftOrigin: number;// 左侧髋臼角度 有可能是钝角,会影响画圆弧
+        angleRight: number;             // 右侧髋臼角度
+        angleRightOrigin: number;// 右侧髋臼角度 有可能是钝角,,会影响画圆弧
+        midPointM1: CoreTypes.Point3;   // 第一条脊柱线中点
+        midPointM2: CoreTypes.Point3;   // 第二条脊柱线中点
+        spineAxisVector: CoreTypes.Point3;    // 脊柱轴心线向量
+        leftTangentVector: CoreTypes.Point3;  // 左侧髋臼切线向量
+        rightTangentVector: CoreTypes.Point3; // 右侧髋臼切线向量
+        /**
+         * 脊柱轴线的垂线向量
+         */
+        perpendicularVector: CoreTypes.Point3; // 脊柱轴线的垂线向量
+        leftIntersection?: CoreTypes.Point3;     // 左侧髋臼切线与脊柱轴心线的交点
+        rightIntersection?: CoreTypes.Point3;    // 右侧髋臼切线与脊柱轴心线的交点
+
+      };
+    };
+  };
+}
+
+// 导出的注解数据接口
+export interface ExportedAnnotationData {
+  points: CoreTypes.Point3[];           // 8个点的世界坐标
+  angles: {
+    angleLeft: number;                  // 左侧髋臼角度(度数)
+    angleRight: number;                 // 右侧髋臼角度(度数)
+  };
+  spineAxis: {
+    midPointM1: CoreTypes.Point3;       // 第一条脊柱线中点
+    midPointM2: CoreTypes.Point3;       // 第二条脊柱线中点
+    spineAxisVector: CoreTypes.Point3;  // 脊柱轴心线向量
+  };
+  tangentVectors: {
+    leftTangentVector: CoreTypes.Point3;   // 左侧髋臼切线向量
+    rightTangentVector: CoreTypes.Point3;  // 右侧髋臼切线向量
+  };
+  textBoxPositions?: {
+    textBoxLeft?: CoreTypes.Point2;     // 左侧文本框位置
+    textBoxRight?: CoreTypes.Point2;    // 右侧文本框位置
+  };
+  metadata: {
+    viewPlaneNormal: CoreTypes.Point3;
+    viewUp: CoreTypes.Point3;
+    FrameOfReferenceUID: string;
+    referencedImageId: string;
+  };
+}
+
+export default class DARAMeasurementTool extends AnnotationTool {
+  static toolName = 'DARAMeasurementTool';
+
+  editData: {
+    annotation: Types.Annotation;
+    viewportIdsToRender: string[];
+    handleIndex?: number;
+    newAnnotation?: boolean;
+    hasMoved?: boolean;
+    textBoxBeingMoved?: 'C' | 'D';
+    // 记录鼠标点击位置相对于文本框左上角的偏移量,用于防止拖拽时跳跃
+    textBoxOffset?: CoreTypes.Point2;
+  } | null = null;
+
+  isDrawing: boolean = false;
+
+  /**
+   * 创建一个预设的注解,带有8个默认点
+   * @param element HTML元素
+   * @param viewport Viewport实例
+   * @param config 可选的初始配置
+   */
+  static createDefaultAnnotation(
+    element: HTMLDivElement,
+    viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport,
+    config?: AnnotationInitialConfig
+  ): DARAMeasurementAnnotation {
+    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 verticalDistance = config?.verticalDistance ?? 200;
+    const lineAWidth = config?.lineAWidth ?? 180;
+    const lineBWidth = config?.lineBWidth ?? 220;
+    const lineCAngle = config?.leftTangentAngle ?? 30; // 左上到右下
+    const lineDAngle = config?.rightTangentAngle ?? 165;  // 右上到左下
+    const lineCLength = config?.leftTangentLength ?? 150;
+    const lineDLength = config?.rightTangentLength ?? 100;
+
+    // 创建8个canvas坐标点
+    // 线段A: 水平线,在中心上方 verticalDistance/2
+    const lineAY = centerY - verticalDistance / 2;
+    const lineAHalfWidth = lineAWidth / 2;
+
+    // 线段B: 水平线,在中心下方 verticalDistance/2
+    const lineBY = centerY + verticalDistance / 2;
+    const lineBHalfWidth = lineBWidth / 2;
+
+    // 线段C: 从左上斜向右下,角度为lineCAngle(相对于水平线)
+    const lineCAngleRad = (lineCAngle * Math.PI) / 180;
+    const lineCHalfLength = lineCLength / 2;
+    const lineCCenterX = centerX - 100; // C线段中心点在中心线左侧100px
+    const lineCCenterY = centerY;
+    const lineC1X = lineCCenterX - lineCHalfLength * Math.cos(lineCAngleRad);
+    const lineC1Y = lineCCenterY - lineCHalfLength * Math.sin(lineCAngleRad);
+    const lineC2X = lineCCenterX + lineCHalfLength * Math.cos(lineCAngleRad);
+    const lineC2Y = lineCCenterY + lineCHalfLength * Math.sin(lineCAngleRad);
+
+    // 线段D: 从右上斜向左下,角度为lineDAngle(相对于水平线)
+    const lineDAngleRad = (lineDAngle * Math.PI) / 180;
+    const lineDHalfLength = lineDLength / 2;
+    const lineDCenterX = centerX + 100; // D线段中心点在中心线右侧100px
+    const lineDCenterY = centerY;
+    const lineD1X = lineDCenterX - lineDHalfLength * Math.cos(lineDAngleRad);
+    const lineD1Y = lineDCenterY - lineDHalfLength * Math.sin(lineDAngleRad) - 50;//50表示向上移动一些
+    const lineD2X = lineDCenterX + lineDHalfLength * Math.cos(lineDAngleRad);
+    const lineD2Y = lineDCenterY + lineDHalfLength * Math.sin(lineDAngleRad) - 50;
+
+    const canvasPoints: CoreTypes.Point2[] = [
+      // 线段A: 水平线(点1, 点2)
+      [centerX - lineAHalfWidth, lineAY],
+      [centerX + lineAHalfWidth, lineAY],
+      // 线段B: 水平线(点3, 点4)
+      [centerX - lineBHalfWidth, lineBY],
+      [centerX + lineBHalfWidth, lineBY],
+      // 线段C: 斜线(点5, 点6)
+      [lineC1X, lineC1Y],
+      [lineC2X, lineC2Y],
+      // 线段D: 斜线(点7, 点8)
+      [lineD1X, lineD1Y],
+      [lineD2X, lineD2Y],
+    ];
+
+    // 转换为world坐标
+    const worldPoints = canvasPoints.map((canvasPoint) =>
+      viewport.canvasToWorld(canvasPoint)
+    );
+
+    const camera = viewport.getCamera();
+    const { viewPlaneNormal, viewUp } = camera;
+    if (viewPlaneNormal === undefined) {
+      throw new Error('----')
+    }
+    if (viewUp === undefined) {
+      throw new Error('----')
+    }
+    const annotationData = {
+      invalidated: true,
+      highlighted: false,
+      metadata: {
+        viewPlaneNormal: [...viewPlaneNormal] as CoreTypes.Point3,
+        viewUp: [...viewUp] as CoreTypes.Point3,
+        FrameOfReferenceUID: viewport.getFrameOfReferenceUID(),
+        referencedImageId: viewport.getCurrentImageId?.() || '',
+        toolName: DARAMeasurementTool.toolName,
+      },
+      data: {
+        label: '',
+        handles: {
+          points: worldPoints,
+          activeHandleIndex: null,
+        },
+        cachedStats: {},
+      },
+    } as DARAMeasurementAnnotation;
+
+    return annotationData;
+  }
+
+  /**
+   * 导出注解数据
+   * @param annotation 要导出的注解
+   * @returns 导出的数据对象
+   */
+  static exportAnnotationData(
+    annotation: DARAMeasurementAnnotation,
+    viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport
+  ): ExportedAnnotationData | null {
+    const targetId = `imageId:${viewport.getCurrentImageId?.() || ''}`;
+    const cachedStats = annotation.data.cachedStats?.[targetId];
+
+    if (!cachedStats || !cachedStats.midPointM1 || !cachedStats.midPointM2) {
+      return null;
+    }
+
+    return {
+      points: [...annotation.data.handles.points],
+      angles: {
+        angleLeft: cachedStats.angleLeft,
+        angleRight: cachedStats.angleRight,
+      },
+      spineAxis: {
+        midPointM1: cachedStats.midPointM1,
+        midPointM2: cachedStats.midPointM2,
+        spineAxisVector: cachedStats.spineAxisVector,
+      },
+      tangentVectors: {
+        leftTangentVector: cachedStats.leftTangentVector,
+        rightTangentVector: cachedStats.rightTangentVector,
+      },
+      textBoxPositions: {
+        textBoxLeft: annotation.data.handles.textBoxLeft,
+        textBoxRight: annotation.data.handles.textBoxRight,
+      },
+      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: ExportedAnnotationData,
+    element: HTMLDivElement,
+    viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport
+  ): DARAMeasurementAnnotation {
+    const enabledElement = getEnabledElement(element);
+    if (!enabledElement) {
+      throw new Error('Element is not enabled');
+    }
+
+    const annotationData = {
+      invalidated: true,
+      highlighted: false,
+      metadata: {
+        ...exportedData.metadata,
+        toolName: DARAMeasurementTool.toolName,
+      },
+      data: {
+        label: '',
+        handles: {
+          points: [...exportedData.points],
+          activeHandleIndex: null,
+          textBoxLeft: exportedData.textBoxPositions?.textBoxLeft,
+          textBoxRight: exportedData.textBoxPositions?.textBoxRight,
+        },
+        cachedStats: {},
+      },
+    } as DARAMeasurementAnnotation;
+
+    return annotationData;
+  }
+
+  constructor(
+    toolProps: PublicToolProps = {},
+    defaultToolProps: ToolProps = {
+      supportedInteractionTypes: ['Mouse', 'Touch'],
+      configuration: {
+        shadow: true,
+        preventHandleOutsideImage: false,
+      },
+    }
+  ) {
+    super(toolProps, defaultToolProps);
+  }
+
+  /**
+   * 添加新注解 - 禁用此功能,因为我们只使用预设的注解
+   */
+  addNewAnnotation(
+    evt: EventTypes.InteractionEventType
+  ): DARAMeasurementAnnotation {
+    // 不创建新注解,直接返回空对象
+    // 因为我们只使用预设的8点注解
+    evt.preventDefault();
+    return {} as DARAMeasurementAnnotation;
+  }
+
+  /**
+   * 检查点是否靠近工具
+   */
+  isPointNearTool(
+    element: HTMLDivElement,
+    annotation: DARAMeasurementAnnotation,
+    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;
+      }
+    }
+
+    // 检查是否靠近线段
+    if (points.length >= 2) {
+      for (let i = 0; i < points.length - 1; i += 2) {
+        if (i + 1 < points.length) {
+          const p1Canvas = viewport.worldToCanvas(points[i]);
+          const p2Canvas = viewport.worldToCanvas(points[i + 1]);
+
+          const dist = this._distanceToSegment(canvasCoords, p1Canvas, p2Canvas);
+          if (dist < 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;
+
+    // 扩大检测区域,包括文本框周围的区域
+    // drawLinkedTextBox会在textBoxPosition周围绘制,所以需要考虑偏移
+    const margin = 10;
+    const result = (
+      x >= tx - margin &&
+      x <= tx + textWidth + margin &&
+      y >= ty - margin &&
+      y <= ty + textHeight + margin
+    );
+
+    // 添加调试日志
+    if (result) {
+      console.log('文本框碰撞检测成功:', {
+        点击位置: canvasPoint,
+        文本框位置: textBoxPosition,
+        检测范围: {
+          x: [tx - margin, tx + textWidth + margin],
+          y: [ty - margin, ty + textHeight + margin]
+        }
+      });
+    }
+
+    return result;
+  }
+
+  /**
+   * 创建角度弧线的点数组
+   * @param center 弧线的圆心(交点)
+   * @param vector1 第一条线段的方向向量(中点线)
+   * @param vector2 第二条线段的方向向量(C或D线)
+   * @param radius 弧线半径
+   * @returns 弧线的点数组
+   */
+  private _createAngleArcPoints(
+    center: CoreTypes.Point2,
+    vector1: CoreTypes.Point2,
+    vector2: CoreTypes.Point2,
+    radius: number
+  ): CoreTypes.Point2[] {
+    // 计算两个向量的角度(相对于X轴正方向)
+    const angle1 = Math.atan2(vector1[1], vector1[0]);
+    const angle2 = Math.atan2(vector2[1], vector2[0]);
+
+    // 确定起始角度和结束角度
+    let startAngle = angle1;
+    let endAngle = angle2;
+
+    // 计算角度差,确保是正值且小于180度
+    let angleDiff = endAngle - startAngle;
+    if (angleDiff > Math.PI) {
+      angleDiff -= 2 * Math.PI;
+    } else if (angleDiff < -Math.PI) {
+      angleDiff += 2 * Math.PI;
+    }
+
+    // 如果角度差是负的,交换起始和结束角度
+    if (angleDiff < 0) {
+      [startAngle, endAngle] = [endAngle, startAngle];
+      angleDiff = -angleDiff;
+    }
+
+    // 生成弧线上的点
+    // 根据角度大小决定需要多少个点
+    const numPoints = Math.max(10, Math.ceil(Math.abs(angleDiff) / (Math.PI / 18))); // 至少10个点
+    const points: CoreTypes.Point2[] = [];
+
+    for (let i = 0; i <= numPoints; i++) {
+      const t = i / numPoints;
+      const angle = startAngle + angleDiff * t;
+      const x = center[0] + radius * Math.cos(angle);
+      const y = center[1] + radius * Math.sin(angle);
+      points.push([x, y]);
+    }
+
+    return points;
+  }
+
+  /**
+   * 计算点到线段的距离
+   */
+  private _distanceToSegment(
+    point: CoreTypes.Point2,
+    lineStart: CoreTypes.Point2,
+    lineEnd: CoreTypes.Point2
+  ): number {
+    const x = point[0];
+    const y = point[1];
+    const x1 = lineStart[0];
+    const y1 = lineStart[1];
+    const x2 = lineEnd[0];
+    const y2 = lineEnd[1];
+
+    const A = x - x1;
+    const B = y - y1;
+    const C = x2 - x1;
+    const D = y2 - y1;
+
+    const dot = A * C + B * D;
+    const lenSq = C * C + D * D;
+    let param = -1;
+
+    if (lenSq !== 0) {
+      param = dot / lenSq;
+    }
+
+    let xx, yy;
+
+    if (param < 0) {
+      xx = x1;
+      yy = y1;
+    } else if (param > 1) {
+      xx = x2;
+      yy = y2;
+    } else {
+      xx = x1 + param * C;
+      yy = y1 + param * D;
+    }
+
+    const dx = x - xx;
+    const dy = y - yy;
+
+    return Math.sqrt(dx * dx + dy * dy);
+  }
+
+  /**
+   * 取消操作
+   */
+  cancel(element: HTMLDivElement): string {
+    if (this.isDrawing) {
+      this.isDrawing = false;
+      this._deactivateDraw(element);
+      this._deactivateModify(element);
+
+      const enabledElement = getEnabledElement(element);
+      if (enabledElement) {
+        const { renderingEngine } = enabledElement;
+        const viewportIdsToRender =
+          utilities.viewportFilters.getViewportIdsWithToolToRender(
+            element,
+            this.getToolName()
+          );
+
+        utilities.triggerAnnotationRenderForViewportIds(
+          //renderingEngine,
+          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 => {
+    console.log('_mouseDownModifyCallback 被调用');
+
+    const eventDetail = evt.detail;
+    const { element, currentPoints } = eventDetail;
+    const canvasCoords = currentPoints.canvas;
+
+    console.log('点击位置:', canvasCoords);
+
+    const enabledElement = getEnabledElement(element);
+    if (!enabledElement) {
+      console.log('enabledElement 不存在');
+      return;
+    }
+    const { viewport } = enabledElement;
+
+    const annotations = annotation.state.getAnnotations(this.getToolName(), element);
+    console.log('找到的注解数量:', annotations?.length || 0);
+
+    if (!annotations || annotations.length === 0) {
+      return;
+    }
+
+    // 先检查是否点击在文本框上
+    for (const ann of annotations) {
+      const customAnn = ann as DARAMeasurementAnnotation;
+      const targetId = this.getTargetId(viewport);
+      const cachedStats = targetId ? customAnn.data.cachedStats?.[targetId] : undefined;
+
+      console.log('检查注解的cachedStats:', cachedStats);
+
+      if (cachedStats?.leftIntersection) {
+        const intersectionCanvas = viewport.worldToCanvas(cachedStats.leftIntersection);
+        const textBoxC: CoreTypes.Point2 = customAnn.data.handles.textBoxLeft || [
+          intersectionCanvas[0] + 10,
+          intersectionCanvas[1] - 10,
+        ];
+
+        console.log('C文本框位置:', textBoxC);
+
+        if (this._isPointInTextBox(canvasCoords, textBoxC)) {
+          // 点击在C文本框上
+          const viewportIdsToRender =
+            utilities.viewportFilters.getViewportIdsWithToolToRender(
+              element,
+              this.getToolName()
+            );
+
+          // 计算鼠标点击位置相对于文本框左上角的偏移量
+          // 这样在拖拽时可以保持鼠标和文本框的相对位置不变
+          const textBoxOffset: CoreTypes.Point2 = [
+            canvasCoords[0] - textBoxC[0],
+            canvasCoords[1] - textBoxC[1],
+          ];
+
+          this.editData = {
+            annotation: customAnn,
+            viewportIdsToRender,
+            hasMoved: false,
+            textBoxBeingMoved: 'C',
+            textBoxOffset: textBoxOffset, // 保存偏移量
+          };
+
+          customAnn.highlighted = true;
+          utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
+
+          evt.preventDefault();
+          evt.stopPropagation();
+          return;
+        }
+      }
+
+      if (cachedStats?.rightIntersection) {
+        const intersectionDCanvas = viewport.worldToCanvas(cachedStats.rightIntersection);
+        const textBoxD: CoreTypes.Point2 = customAnn.data.handles.textBoxRight || [
+          intersectionDCanvas[0] + 10,
+          intersectionDCanvas[1] + 20,
+        ];
+
+        if (this._isPointInTextBox(canvasCoords, textBoxD)) {
+          // 点击在D文本框上
+          const viewportIdsToRender =
+            utilities.viewportFilters.getViewportIdsWithToolToRender(
+              element,
+              this.getToolName()
+            );
+
+          // 计算鼠标点击位置相对于文本框左上角的偏移量
+          // 这样在拖拽时可以保持鼠标和文本框的相对位置不变
+          const textBoxOffset: CoreTypes.Point2 = [
+            canvasCoords[0] - textBoxD[0],
+            canvasCoords[1] - textBoxD[1],
+          ];
+
+          this.editData = {
+            annotation: customAnn,
+            viewportIdsToRender,
+            hasMoved: false,
+            textBoxBeingMoved: 'D',
+            textBoxOffset: textBoxOffset, // 保存偏移量
+          };
+
+          customAnn.highlighted = true;
+          utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
+
+          evt.preventDefault();
+          evt.stopPropagation();
+          return;
+        }
+      }
+    }
+
+    // 如果没有点击文本框,再查找最近的手柄
+    for (const ann of annotations) {
+      const customAnn = ann as DARAMeasurementAnnotation;
+      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 DARAMeasurementAnnotation;
+    const { data } = customAnn;
+
+    // 如果正在拖拽文本框
+    if (textBoxBeingMoved && this.editData.textBoxOffset) {
+      // 从当前鼠标位置减去偏移量,得到文本框左上角应该在的位置
+      // 这样可以保持鼠标和文本框的相对位置不变,防止跳跃
+      const newTextBoxPosition: CoreTypes.Point2 = [
+        canvasCoords[0] - this.editData.textBoxOffset[0],
+        canvasCoords[1] - this.editData.textBoxOffset[1],
+      ];
+
+      if (textBoxBeingMoved === 'C') {
+        data.handles.textBoxLeft = newTextBoxPosition;
+      } else if (textBoxBeingMoved === 'D') {
+        data.handles.textBoxRight = 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 DARAMeasurementAnnotation;
+
+    // 清除active状态
+    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: DARAMeasurementAnnotation
+  ): void {
+    const eventDetail = evt.detail;
+    const { element } = eventDetail;
+
+    annotation.highlighted = true;
+
+    const enabledElement = getEnabledElement(element);
+    if (!enabledElement) {
+      return;
+    }
+    const { renderingEngine } = enabledElement;
+    const viewportIdsToRender =
+      utilities.viewportFilters.getViewportIdsWithToolToRender(
+        element,
+        this.getToolName()
+      );
+
+    utilities.triggerAnnotationRenderForViewportIds(
+      //renderingEngine,
+      viewportIdsToRender
+    );
+
+    evt.preventDefault();
+  }
+
+  /**
+   * 工具选中回调
+   */
+  toolSelectedCallback(
+    evt: EventTypes.InteractionEventType,
+    annotation: DARAMeasurementAnnotation
+  ): 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 { renderingEngine } = enabledElement;
+
+    const { annotation, viewportIdsToRender } = this.editData;
+    const { data } = annotation as DARAMeasurementAnnotation;
+
+    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 DARAMeasurementAnnotation,
+          enabledElement
+        );
+
+        this.editData!.hasMoved = true;
+      } else if (this.editData!.newAnnotation) {
+        // 绘制新注解时的拖拽
+        const lastIndex = points.length - 1;
+        if (lastIndex >= 0) {
+          points[lastIndex] = worldPos;
+        }
+      }
+
+      utilities.triggerAnnotationRenderForViewportIds(
+        //renderingEngine,
+        viewportIdsToRender
+      );
+    }
+  };
+
+  /**
+   * 处理鼠标抬起/点击
+   */
+  _endCallback = (evt: EventTypes.InteractionEventType): void => {
+    const eventDetail = evt.detail;
+    const { element } = eventDetail;
+
+    if (!this.editData) {
+      return;
+    }
+
+    const { annotation, viewportIdsToRender, newAnnotation, hasMoved } =
+      this.editData;
+    const { data } = annotation as DARAMeasurementAnnotation;
+
+    if (newAnnotation && !hasMoved) {
+      // 添加新点
+      const points = data.handles.points;
+
+      if (points.length < 6) {
+        const { currentPoints } = eventDetail;
+        const worldPos = currentPoints.world;
+
+        if (points.length > 0) {
+          // 更新最后一个点的位置(如果已经在拖拽中添加了)
+          points[points.length - 1] = worldPos;
+        }
+
+        // 添加下一个点
+        if (points.length < 6) {
+          points.push(worldPos);
+          data.handles.activeHandleIndex = points.length - 1;
+        }
+
+        if (points.length === 6) {
+          // 完成注解创建
+          this.isDrawing = false;
+          data.handles.activeHandleIndex = null;
+          annotation.highlighted = false;
+          this.editData.newAnnotation = false;
+          this._deactivateDraw(element);
+        }
+
+        const enabledElement = getEnabledElement(element);
+        if (enabledElement) {
+          this._updateCachedStats(
+            annotation as DARAMeasurementAnnotation,
+            enabledElement
+          );
+
+          const { renderingEngine } = enabledElement;
+          const newViewportIdsToRender =
+            utilities.viewportFilters.getViewportIdsWithToolToRender(
+              element,
+              this.getToolName()
+            );
+          utilities.triggerAnnotationRenderForViewportIds(
+            //renderingEngine,
+            newViewportIdsToRender
+          );
+        }
+      }
+    } else if (hasMoved) {
+      // 拖拽结束
+      this.editData.hasMoved = false;
+    }
+  };
+
+  /**
+   * 获取靠近图像点的手柄
+   */
+  getHandleNearImagePoint(
+    element: HTMLDivElement,
+    annotation: DARAMeasurementAnnotation,
+    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;
+
+    // 使用更大的proximity来更容易选中手柄
+    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) {
+        // 设置activeHandleIndex以便拖拽
+        annotation.data.handles.activeHandleIndex = i;
+        return {
+          worldPosition: point,
+        } as Types.ToolHandle;
+      }
+    }
+
+    // 如果没有找到手柄,重置activeHandleIndex
+    annotation.data.handles.activeHandleIndex = null;
+    return undefined;
+  }
+
+  /**
+   * 更新缓存的统计数据
+   */
+  _updateCachedStats(
+    annotation: DARAMeasurementAnnotation,
+    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]) {
+      console.log(`完全变0`);
+      data.cachedStats[targetId] = {
+        angleLeft: 0,
+        angleLeftOrigin: 0,
+        angleRight: 0,
+        angleRightOrigin: 0,
+        midPointM1: [0, 0, 0] as CoreTypes.Point3,
+        midPointM2: [0, 0, 0] as CoreTypes.Point3,
+        spineAxisVector: [0, 0, 0] as CoreTypes.Point3,
+        leftTangentVector: [0, 0, 0] as CoreTypes.Point3,
+        rightTangentVector: [0, 0, 0] as CoreTypes.Point3,
+        perpendicularVector: [0, 0, 0] as CoreTypes.Point3,
+      };
+    }
+
+    const stats = data.cachedStats[targetId];
+
+    if (points.length >= 8) {
+      // 提取点: 脊柱线A = [0,1], 脊柱线B=[2,3], 左侧髋臼=[4,5], 右侧髋臼=[6,7]
+      const spineA1 = points[0],
+        spineA2 = points[1];
+      const spineB1 = points[2],
+        spineB2 = points[3];
+      const leftHip1 = points[4],
+        leftHip2 = points[5];
+      const rightHip1 = points[6],
+        rightHip2 = points[7];
+
+      // 计算脊柱线中点
+      const midPointM1 = calculateMidpoint(spineA1, spineA2);
+      const midPointM2 = calculateMidpoint(spineB1, spineB2);
+      console.log(`计算两个中点:${midPointM1} = ${midPointM2}`);
+      // 脊柱轴心线向量
+      const spineAxisVector = vectorSubtract(midPointM2, midPointM1);
+
+      // 髋臼切线向量
+      const leftTangentVector = vectorSubtract(leftHip2, leftHip1);
+      const rightTangentVector = vectorSubtract(rightHip2, rightHip1);
+
+      // 计算脊柱轴线的垂线向量(用于角度计算)
+      // 垂线向量可以通过叉积计算得到
+      const perpendicularVector = this._calculatePerpendicularVector(spineAxisVector);
+      console.log(`计算出垂线向量:${perpendicularVector}`)
+
+      // 计算左髋臼切线与脊柱轴心的交点
+      const leftIntersection = getLineIntersection(midPointM1, spineAxisVector, leftHip1, leftTangentVector) || undefined;
+
+      // 计算右髋臼切线与脊柱轴心的交点
+      const rightIntersection = getLineIntersection(midPointM1, spineAxisVector, rightHip1, rightTangentVector) || undefined;
+
+      // 存储计算结果
+      stats.midPointM1 = midPointM1;
+      stats.midPointM2 = midPointM2;
+      stats.spineAxisVector = spineAxisVector;
+      stats.leftTangentVector = leftTangentVector;
+      stats.rightTangentVector = rightTangentVector;
+      stats.perpendicularVector = perpendicularVector;
+      stats.leftIntersection = leftIntersection;
+      stats.rightIntersection = rightIntersection;
+
+      // 计算左侧髋臼角度(切线与脊柱轴线垂线的夹角)
+      const leftAngleRad = getAngleBetweenLines(leftTangentVector, perpendicularVector);
+      stats.angleLeftOrigin = Math.round(degrees(leftAngleRad));
+      // 约束:使用切线的向量或者反向向量,分别计算和垂线的夹角,取那个锐角
+      if (stats.angleLeftOrigin < 90) {
+        stats.angleLeft = stats.angleLeftOrigin;
+      } else {
+        stats.angleLeft = 180 - stats.angleLeftOrigin;
+      }
+
+      // 计算右侧髋臼角度(切线与脊柱轴线垂线的夹角)
+      const rightAngleRad = getAngleBetweenLines(rightTangentVector, perpendicularVector);
+      stats.angleRightOrigin = Math.round(degrees(rightAngleRad));
+      if (stats.angleRightOrigin < 90) {
+        stats.angleRight = stats.angleRightOrigin;
+      } else {
+        stats.angleRight = 180 - stats.angleRightOrigin;
+      }
+      // 兼容性字段已移除,不再存储向后兼容的数据
+    }
+  }
+
+  // private _calculatePerpendicularVector(vector: CoreTypes.Point3): CoreTypes.Point3 {
+  //   const vx = vector[0], vy = vector[1], vz = vector[2];
+  //   // 正确叉积:与 Y 轴 [0,1,0]
+  //   const perpendicular: CoreTypes.Point3 = [-vz, 0, vx];
+
+  //   // 计算长度
+  //   const length = Math.sqrt(perpendicular[0] ** 2 + perpendicular[1] ** 2 + perpendicular[2] ** 2);
+  //   // 使用 epsilon 避免浮点误差
+  //   if (length < 1e-10) {
+  //     // 输入平行于 Y 轴,使用 X 轴作为辅助
+  //     return [0, vz, -vy];
+  //   }
+
+  //   return perpendicular;
+  // }
+
+  /**
+   * 计算向量的垂线向量(2D 平面,忽略 z 轴)
+   * @param vector 输入向量 [x, y, z],但只使用 [x, y]
+   * @returns 垂直于输入向量的向量 [px, py, 0]
+   */
+  private _calculatePerpendicularVector(vector: CoreTypes.Point3): CoreTypes.Point3 {
+    const vx = vector[0];
+    const vy = vector[1];
+    // 2D 垂直向量:[-vy, vx, 0](顺时针 90° 旋转)
+    // 或者 [vy, -vx, 0](逆时针),取决于您需要的方向
+    return [-vy, vx, 0];
+  }
+
+  /**
+   * 渲染注解
+   */
+  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 DARAMeasurementAnnotation;
+      const { annotationUID, data } = annotation;
+      const points = data.handles.points;
+      const activeHandleIndex = data.handles.activeHandleIndex;
+
+      if (points.length < 1) {
+        continue;
+      }
+
+      const targetId = this.getTargetId(viewport);
+      const cachedStats = targetId ? data.cachedStats?.[targetId] : undefined;
+
+      // 转换所有点为 canvas 坐标
+      const canvasPoints = points.map((p) => viewport.worldToCanvas(p));
+
+      // 绘制线段 A (红色)
+      if (points.length >= 2 && annotationUID) {
+        const lineUID = `${annotationUID}-lineA`;
+        const lineOptions = {
+          color: 'rgb(255, 0, 0)',
+          width: 2,
+        };
+
+        drawLineSvg(
+          svgDrawingHelper,
+          annotationUID,
+          lineUID,
+          canvasPoints[0],
+          canvasPoints[1],
+          lineOptions
+        );
+      }
+
+      // 绘制线段 B (蓝色)
+      if (points.length >= 4 && annotationUID) {
+        const lineUID = `${annotationUID}-lineB`;
+        const lineOptions = {
+          color: 'rgb(0, 0, 255)',
+          width: 2,
+        };
+
+        drawLineSvg(
+          svgDrawingHelper,
+          annotationUID,
+          lineUID,
+          canvasPoints[2],
+          canvasPoints[3],
+          lineOptions
+        );
+      }
+
+      // 绘制中点线 (绿色)
+      if (points.length >= 4 && annotationUID) {
+        const midA = calculateMidpoint(points[0], points[1]);
+        const midB = calculateMidpoint(points[2], points[3]);
+        const midACanvas = viewport.worldToCanvas(midA);
+        const midBCanvas = viewport.worldToCanvas(midB);
+        const lineUID = `${annotationUID}-midLine`;
+        const lineOptions = {
+          color: 'rgb(0, 255, 0)',
+          width: 2,
+        };
+
+        drawLineSvg(
+          svgDrawingHelper,
+          annotationUID,
+          lineUID,
+          midACanvas,
+          midBCanvas,
+          lineOptions
+        );
+      }
+
+      // 绘制线段 C (黄色)
+      if (points.length >= 6 && annotationUID) {
+        const lineUID = `${annotationUID}-lineC`;
+        const lineOptions = {
+          color: 'rgb(255, 255, 0)',
+          width: 2,
+        };
+
+        drawLineSvg(
+          svgDrawingHelper,
+          annotationUID,
+          lineUID,
+          canvasPoints[4],
+          canvasPoints[5],
+          lineOptions
+        );
+
+        // 绘制 C 线段延长线 (黄色虚线)
+        const CVector = vectorSubtract(points[5], points[4]);
+        const extendEnd = vectorAdd(points[5], vectorScale(CVector, 2));
+        const extendEndCanvas = viewport.worldToCanvas(extendEnd);
+        const extendLineUID = `${annotationUID}-lineC-extend`;
+        const extendLineOptions = {
+          color: 'rgb(255, 255, 0)',
+          width: 1,
+          lineDash: '4,4',
+        };
+
+        drawLineSvg(
+          svgDrawingHelper,
+          annotationUID,
+          extendLineUID,
+          canvasPoints[5],
+          extendEndCanvas,
+          extendLineOptions
+        );
+      }
+
+      // 绘制线段 D (橙色)
+      if (points.length >= 8 && annotationUID) {
+        const lineUID = `${annotationUID}-lineD`;
+        const lineOptions = {
+          color: 'rgb(255, 165, 0)',
+          width: 2,
+        };
+
+        drawLineSvg(
+          svgDrawingHelper,
+          annotationUID,
+          lineUID,
+          canvasPoints[6],
+          canvasPoints[7],
+          lineOptions
+        );
+
+        // 绘制 D 线段延长线 (橙色虚线)
+        const DVector = vectorSubtract(points[7], points[6]);
+        const extendEndD = vectorAdd(points[7], vectorScale(DVector, 2));
+        const extendEndDCanvas = viewport.worldToCanvas(extendEndD);
+        const extendLineDUID = `${annotationUID}-lineD-extend`;
+        const extendLineDOptions = {
+          color: 'rgb(255, 165, 0)',
+          width: 1,
+          lineDash: '4,4',
+        };
+
+        drawLineSvg(
+          svgDrawingHelper,
+          annotationUID,
+          extendLineDUID,
+          canvasPoints[7],
+          extendEndDCanvas,
+          extendLineDOptions
+        );
+      }
+
+      // 绘制左侧髋臼的交点、垂线、角度弧线和文本
+      if (cachedStats?.leftIntersection && cachedStats.angleLeft !== undefined && annotationUID && cachedStats.spineAxisVector && cachedStats.perpendicularVector) {
+        const intersection = cachedStats.leftIntersection;
+        const intersectionCanvas = viewport.worldToCanvas(intersection);
+
+        // 绘制交点标记(小圆点)
+        const circleUID = `${annotationUID}-intersection-left`;
+        drawCircleSvg(
+          svgDrawingHelper,
+          annotationUID,
+          circleUID,
+          intersectionCanvas,
+          3,
+          {
+            color: 'rgb(255, 255, 0)',
+            fill: 'rgb(255, 255, 0)',
+          }
+        );
+
+        // 绘制脊柱轴线垂线(虚线,从交点出发)
+        // 定义左侧垂线样式配置
+        const DEFAULT_LEFT_PERPENDICULAR = {
+          length: 100,
+          color: 'rgb(255, 255, 0)',
+          width: 1,
+          lineDash: '4,4'
+        };
+
+        // 从配置中读取左侧垂线样式(如果有的话)
+        const leftPerpConfig = { ...DEFAULT_LEFT_PERPENDICULAR };
+
+        const perpendicularLength = leftPerpConfig.length;
+        // console.log(`脊柱轴线的垂线向量 ${cachedStats.perpendicularVector}`)
+        const perpendicularEnd = vectorAdd(intersection, decomposeVectorOnAxes(cachedStats.perpendicularVector, perpendicularLength));
+        // console.log(`垂线终点的向量 : ${perpendicularEnd}`)
+        const perpendicularEndCanvas = viewport.worldToCanvas(perpendicularEnd);
+        // console.log(`垂线终点转换为世界坐标 : ${perpendicularEndCanvas}`)
+        // console.log(`垂线的起点,也就是交点 : ${intersectionCanvas}`)
+        const perpendicularLineUID = `${annotationUID}-perpendicular-left`;
+        const perpendicularLineOptions = {
+          color: leftPerpConfig.color,
+          width: leftPerpConfig.width,
+          lineDash: leftPerpConfig.lineDash,
+        };
+
+        drawLineSvg(
+          svgDrawingHelper,
+          annotationUID,
+          perpendicularLineUID,
+          intersectionCanvas,
+          perpendicularEndCanvas,
+          perpendicularLineOptions
+        );
+
+        // 绘制角度弧线(垂线与左髋臼切线之间的夹角)
+        // 将3D向量转换为2D向量用于绘制
+        const perpendicularVector2D: CoreTypes.Point2 = [cachedStats.perpendicularVector[0], cachedStats.perpendicularVector[1]];
+        let leftTangentVector2D: CoreTypes.Point2 = [cachedStats.leftTangentVector[0], cachedStats.leftTangentVector[1]];
+        //如果钝角,使用反向向量,为了画锐角的弧线
+        if (cachedStats.angleLeftOrigin > 90) {
+          leftTangentVector2D = [-cachedStats.leftTangentVector[0], -cachedStats.leftTangentVector[1]];
+        }
+
+
+        // 创建弧线点数组
+        const arcRadius = 30; // 弧线半径
+        const arcPoints = this._createAngleArcPoints(
+          intersectionCanvas,
+          perpendicularVector2D,
+          leftTangentVector2D,
+          arcRadius
+        );
+
+        // 绘制弧线
+        const arcUID = `${annotationUID}-arc-left`;
+        drawPathSvg(
+          svgDrawingHelper,
+          annotationUID,
+          arcUID,
+          arcPoints,
+          {
+            color: 'rgb(255, 255, 0)',
+            width: 2,
+          }
+        );
+
+        // 绘制角度文本 (左侧髋臼)
+        const textLines = [`左侧: ${cachedStats.angleLeft}°`];
+        const textUID = `${annotationUID}-angle-text-left`;
+        const textBoxPosition: CoreTypes.Point2 = data.handles.textBoxLeft || [
+          intersectionCanvas[0] + 10,
+          intersectionCanvas[1] - 10,
+        ];
+
+        drawLinkedTextBox(
+          svgDrawingHelper,
+          annotationUID,
+          textUID,
+          textLines,
+          textBoxPosition,
+          [intersectionCanvas],
+          {},
+          {
+            color: 'rgb(255, 255, 0)',
+          }
+        );
+      }
+
+      // 绘制右侧髋臼的交点、垂线、角度弧线和文本
+      if (cachedStats?.rightIntersection && cachedStats.angleRight !== undefined && annotationUID && cachedStats.spineAxisVector && cachedStats.perpendicularVector) {
+        const intersectionD = cachedStats.rightIntersection;
+        const intersectionDCanvas = viewport.worldToCanvas(intersectionD);
+
+        // 绘制交点标记(小圆点)
+        const circleDUID = `${annotationUID}-intersection-right`;
+        drawCircleSvg(
+          svgDrawingHelper,
+          annotationUID,
+          circleDUID,
+          intersectionDCanvas,
+          3,
+          {
+            color: 'rgb(255, 165, 0)',
+            fill: 'rgb(255, 165, 0)',
+          }
+        );
+
+        // 绘制脊柱轴线垂线(虚线,从交点出发)
+        // 定义右侧垂线样式配置
+        const DEFAULT_RIGHT_PERPENDICULAR = {
+          length: 100,
+          color: 'rgb(255, 165, 0)',
+          width: 1,
+          lineDash: '4,4'
+        };
+
+        // 从配置中读取右侧垂线样式(如果有的话)
+        const rightPerpConfig = { ...DEFAULT_RIGHT_PERPENDICULAR };
+
+        const perpendicularLengthD = rightPerpConfig.length;
+        const perpendicularEndD = vectorAdd(intersectionD, decomposeVectorOnAxes(cachedStats.perpendicularVector, perpendicularLengthD));
+        const perpendicularEndDCanvas = viewport.worldToCanvas(perpendicularEndD);
+        const perpendicularLineDUID = `${annotationUID}-perpendicular-right`;
+        const perpendicularLineDOptions = {
+          color: rightPerpConfig.color,
+          width: rightPerpConfig.width,
+          lineDash: rightPerpConfig.lineDash,
+        };
+
+        drawLineSvg(
+          svgDrawingHelper,
+          annotationUID,
+          perpendicularLineDUID,
+          intersectionDCanvas,
+          perpendicularEndDCanvas,
+          perpendicularLineDOptions
+        );
+
+        // 绘制角度弧线(垂线与右髋臼切线之间的夹角)
+        // 将3D向量转换为2D向量用于绘制
+        const perpendicularVector2D: CoreTypes.Point2 = [cachedStats.perpendicularVector[0], cachedStats.perpendicularVector[1]];
+        let rightTangentVector2D: CoreTypes.Point2 = [cachedStats.rightTangentVector[0], cachedStats.rightTangentVector[1]];
+        //如果钝角,使用反向向量,为了画锐角的弧线
+        if (cachedStats.angleRightOrigin > 90) {
+          rightTangentVector2D = [-cachedStats.rightTangentVector[0], -cachedStats.rightTangentVector[1]];
+        }
+        // 创建弧线点数组
+        const arcRadiusD = 30; // 弧线半径
+        const arcPointsD = this._createAngleArcPoints(
+          intersectionDCanvas,
+          perpendicularVector2D,
+          rightTangentVector2D,
+          arcRadiusD
+        );
+
+        // 绘制弧线
+        const arcDUID = `${annotationUID}-arc-right`;
+        drawPathSvg(
+          svgDrawingHelper,
+          annotationUID,
+          arcDUID,
+          arcPointsD,
+          {
+            color: 'rgb(255, 165, 0)',
+            width: 2,
+          }
+        );
+
+        // 绘制角度文本 (右侧髋臼)
+        const textLinesD = [`右侧: ${cachedStats.angleRight}°`];
+        const textDUID = `${annotationUID}-angleD-text-right`;
+        const textBoxPositionD: CoreTypes.Point2 = data.handles.textBoxRight || [
+          intersectionDCanvas[0] + 10,
+          intersectionDCanvas[1] + 20,
+        ];
+
+        drawLinkedTextBox(
+          svgDrawingHelper,
+          annotationUID,
+          textDUID,
+          textLinesD,
+          textBoxPositionD,
+          [intersectionDCanvas],
+          {},
+          {
+            color: 'rgb(255, 165, 0)',
+          }
+        );
+      }
+
+      // 绘制手柄点
+      if (annotationUID) {
+        const handleGroupUID = '0';
+        drawHandles(
+          svgDrawingHelper,
+          annotationUID,
+          handleGroupUID,
+          canvasPoints,
+          {
+            color: 'rgb(255, 255, 255)',
+            handleRadius: 8,
+          }
+        );
+      }
+
+      renderStatus = true;
+    }
+
+    return renderStatus;
+  };
+}

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

@@ -27,6 +27,9 @@ import StackViewer, {
   activateTibialPlateauAngleMeasurement,
   deactivateTibialPlateauAngleMeasurement,
   clearTibialPlateauAngleMeasurements,
+  activateDARAMeasurement,
+  deactivateDARAMeasurement,
+  clearDARAMeasurements,
 } from './viewers/stack.image.viewer';
 import { useSelector, useDispatch } from 'react-redux';
 import store, { RootState } from '@/states/store';
@@ -405,6 +408,7 @@ const ViewerContainer: React.FC<ViewerContainerProps> = ({ imageUrls }) => {
             MeasurementToolManager.clearLengthMeasurementsForViewports(selectedViewportIds);
             MeasurementToolManager.clearAngleMeasurementsForViewports(selectedViewportIds);
             MeasurementToolManager.clearTibialPlateauAngleMeasurementsForViewports(selectedViewportIds);
+            MeasurementToolManager.clearDARAMeasurementsForViewports(selectedViewportIds);
           }
           console.log('Clearing All Measurements from MeasurementPanel');
           break;
@@ -479,6 +483,40 @@ const ViewerContainer: React.FC<ViewerContainerProps> = ({ imageUrls }) => {
           console.log('Activating TibialPlateauAngle Measurement from MeasurementPanel');
           break;
 
+        case '髋臼水平角':
+          console.log(`开始髋臼水平角测量`);
+          if (selectedViewportIds.length > 0) {
+            selectedViewportIds.forEach((viewportId) => {
+              const success = activateDARAMeasurement(viewportId);
+              if (success) {
+                console.log(`激活髋臼水平角测量工具成功`);
+                dispatch(
+                  setToolActive({
+                    toolName: 'DARAMeasurementTool',
+                    viewportId: viewportId,
+                  })
+                );
+              }
+            });
+          } else {
+            // 如果没有选中的 viewport,为所有可见的 viewport 激活
+            const visibleViewportCount = getVisibleViewportCount();
+            for (let i = 0; i < visibleViewportCount; i++) {
+              const viewportId = `viewport-${i}`;
+              const success = activateDARAMeasurement(viewportId);
+              if (success) {
+                dispatch(
+                  setToolActive({
+                    toolName: 'DARAMeasurementTool',
+                    viewportId: viewportId,
+                  })
+                );
+              }
+            }
+          }
+          console.log('Activating DARA Measurement from MeasurementPanel');
+          break;
+
         case '测量校正':
           console.log('Measurement Calibration - 功能待实现');
           break;

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

@@ -7,6 +7,7 @@ import { eventTarget } from '@cornerstonejs/core';
 import { registerGlobalTools } from '@/utils/cornerstoneToolsSetup';
 import { MeasurementToolManager } from '@/utils/measurementToolManager';
 import TibialPlateauAngleTool from '@/components/measures/TibialPlateauAngleTool';
+import DARAMeasurementTool from '@/components/measures/DARAMeasurementTool';
 import { boolean } from 'zod';
 
 const {
@@ -119,6 +120,7 @@ function registerTools(viewportId, renderingEngineId) {
   toolGroup.addTool(LengthTool.toolName); // 添加线段测量工具
   toolGroup.addTool(AngleTool.toolName); // 添加角度测量工具
   toolGroup.addTool(TibialPlateauAngleTool.toolName); // 添加胫骨平台夹角测量工具
+  toolGroup.addTool(DARAMeasurementTool.toolName); // 添加髋臼水平角测量工具
 
   // 设置默认工具状态
   setupDefaultToolStates(toolGroup);
@@ -168,6 +170,7 @@ function setupDefaultToolStates(toolGroup: cornerstoneTools.Types.IToolGroup) {
   toolGroup.setToolPassive(LengthTool.toolName);
   toolGroup.setToolPassive(AngleTool.toolName);
   toolGroup.setToolPassive(TibialPlateauAngleTool.toolName);
+  toolGroup.setToolPassive(DARAMeasurementTool.toolName);
 }
 export function addLMark(currentViewportId: string): void {
   // Implement the logic to add an L mark
@@ -674,6 +677,66 @@ export function isTibialPlateauAngleMeasurementActive(viewportId: string): boole
   return MeasurementToolManager.isTibialPlateauAngleToolActive(viewportId);
 }
 
+// ==================== 髋臼水平角测量相关函数 ====================
+
+/**
+ * 激活髋臼水平角测量工具
+ */
+export function activateDARAMeasurement(viewportId: string): boolean {
+  console.log(
+    `[activateDARAMeasurement] Activating DARA measurement for viewport: ${viewportId}`
+  );
+  return MeasurementToolManager.activateDARAMeasurementTool(viewportId);
+}
+
+/**
+ * 停用髋臼水平角测量工具
+ */
+export function deactivateDARAMeasurement(viewportId: string): boolean {
+  console.log(
+    `[deactivateDARAMeasurement] Deactivating DARA measurement for viewport: ${viewportId}`
+  );
+  return MeasurementToolManager.deactivateDARAMeasurementTool(viewportId);
+}
+
+/**
+ * 切换髋臼水平角测量工具状态
+ */
+export function toggleDARAMeasurement(viewportId: string): boolean {
+  console.log(
+    `[toggleDARAMeasurement] Toggling DARA measurement for viewport: ${viewportId}`
+  );
+  return MeasurementToolManager.toggleDARAMeasurementTool(viewportId);
+}
+
+/**
+ * 清除髋臼水平角测量标注
+ */
+export function clearDARAMeasurements(viewportId: string): boolean {
+  console.log(
+    `[clearDARAMeasurements] Clearing DARA measurements for viewport: ${viewportId}`
+  );
+  return MeasurementToolManager.clearDARAMeasurements(viewportId);
+}
+
+/**
+ * 获取髋臼水平角测量结果
+ */
+// eslint-disable-next-line
+export function getDARAMeasurements(viewportId: string): any[] {
+  console.log(
+    `[getDARAMeasurements] Getting DARA measurements for viewport: ${viewportId}`
+  );
+  return MeasurementToolManager.getDARAMeasurements(viewportId);
+}
+
+/**
+ * 检查髋臼水平角测量工具是否激活
+ */
+export function isDARAMeasurementActive(viewportId: string): boolean {
+  return MeasurementToolManager.isDARAMeasurementToolActive(viewportId);
+}
+
 const StackViewer = ({
   imageIndex = 0,
   imageUrls = [],

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

@@ -8,6 +8,7 @@ export type MeasurementAction =
   | '测量校正'
   | '停用线段测量'
   | '胫骨平台夹角'
+  | '髋臼水平角'
   | null;
 
 // 测量结果类型

+ 2 - 0
src/utils/cornerstoneToolsSetup.ts

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

+ 259 - 0
src/utils/measurementToolManager.ts

@@ -1,6 +1,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';
 
 const {
   ToolGroupManager,
@@ -689,4 +690,262 @@ export class MeasurementToolManager {
       this.clearTibialPlateauAngleMeasurements(viewportId)
     );
   }
+
+  // ==================== 髋臼水平角测量工具 ====================
+
+  /**
+   * 激活髋臼水平角测量工具
+   */
+  static activateDARAMeasurementTool(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.setToolActive(DARAMeasurementTool.toolName, {
+        bindings: [{ mouseButton: MouseBindings.Primary }],
+      });
+      // 获取工具实例并激活修改模式
+      const toolInstance = toolGroup.getToolInstance(
+        DARAMeasurementTool.toolName
+      ) as DARAMeasurementTool;
+      // 使用参数中的 viewportId,而不是硬编码
+      const viewport = cornerstone.getEnabledElementByViewportId(viewportId)?.viewport;
+      if (toolInstance && viewport.element) {
+        toolInstance._activateModify(viewport.element);
+      }
+      // 自动创建一个预设的注解
+      try {
+        const viewport = cornerstone.getEnabledElementByViewportId(viewportId)?.viewport;
+
+        if (viewport && viewport.element) {
+          // 创建预设注解
+          const defaultAnnotation = DARAMeasurementTool.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(
+              DARAMeasurementTool.toolName
+            ) as DARAMeasurementTool;
+
+            if (toolInstance && '_updateCachedStats' in toolInstance) {
+              (toolInstance as any)._updateCachedStats(defaultAnnotation, enabledElement);
+            }
+          }
+
+          // 触发渲染更新
+          viewport.render();
+
+          console.log('[MeasurementToolManager] Default DARA annotation created successfully');
+        }
+      } catch (error) {
+        console.error('[MeasurementToolManager] Failed to create default DARA annotation:', error);
+        // 注解创建失败不影响工具激活
+      }
+
+      console.log(
+        `[MeasurementToolManager] DARA tool activated for viewport: ${viewportId}`
+      );
+      return true;
+    } catch (error) {
+      console.error(
+        `[MeasurementToolManager] Error activating DARA tool:`,
+        error
+      );
+      return false;
+    }
+  }
+
+  /**
+   * 停用髋臼水平角测量工具
+   */
+  static deactivateDARAMeasurementTool(viewportId: string): boolean {
+    const toolGroup = this.getToolGroup(viewportId);
+    if (!toolGroup) return false;
+
+    try {
+      toolGroup.setToolPassive(DARAMeasurementTool.toolName, {
+        removeAllBindings: true,
+      });
+      console.log(
+        `[MeasurementToolManager] DARA tool deactivated for viewport: ${viewportId}`
+      );
+      return true;
+    } catch (error) {
+      console.error(
+        `[MeasurementToolManager] Error deactivating DARA tool:`,
+        error
+      );
+      return false;
+    }
+  }
+
+  /**
+   * 检查髋臼水平角测量工具是否处于激活状态
+   */
+  static isDARAMeasurementToolActive(viewportId: string): boolean {
+    const toolGroup = this.getToolGroup(viewportId);
+    if (!toolGroup) return false;
+
+    try {
+      const activeTool = toolGroup.getActivePrimaryMouseButtonTool();
+      return activeTool === DARAMeasurementTool.toolName;
+    } catch (error) {
+      console.error(
+        `[MeasurementToolManager] Error checking DARA tool state:`,
+        error
+      );
+      return false;
+    }
+  }
+
+  /**
+   * 切换髋臼水平角测量工具状态
+   */
+  static toggleDARAMeasurementTool(viewportId: string): boolean {
+    const isActive = this.isDARAMeasurementToolActive(viewportId);
+
+    if (isActive) {
+      return this.deactivateDARAMeasurementTool(viewportId);
+    } else {
+      return this.activateDARAMeasurementTool(viewportId);
+    }
+  }
+
+  /**
+   * 清除指定 viewport 的所有髋臼水平角测量标注
+   */
+  static clearDARAMeasurements(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(
+        DARAMeasurementTool.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} DARA measurements for viewport: ${viewportId}`
+      );
+      return true;
+    } catch (error) {
+      console.error(
+        `[MeasurementToolManager] Error clearing DARA measurements:`,
+        error
+      );
+      return false;
+    }
+  }
+
+  /**
+   * 获取指定 viewport 的所有髋臼水平角测量结果
+   */
+  // eslint-disable-next-line
+  static getDARAMeasurements(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(
+        DARAMeasurementTool.toolName,
+        viewport.element
+      );
+
+      return annotations.map((annotation) => ({
+        annotationUID: annotation.annotationUID,
+        angleLeft: annotation.data?.cachedStats?.angleLeft || 0,
+        angleRight: annotation.data?.cachedStats?.angleRight || 0,
+        unit: 'degrees',
+        points: annotation.data?.handles?.points || [],
+      }));
+    } catch (error) {
+      console.error(
+        `[MeasurementToolManager] Error getting DARA measurements:`,
+        error
+      );
+      return [];
+    }
+  }
+
+  /**
+   * 为多个 viewport 批量激活髋臼水平角测量工具
+   */
+  static activateDARAMeasurementToolForViewports(
+    viewportIds: string[]
+  ): boolean[] {
+    return viewportIds.map((viewportId) =>
+      this.activateDARAMeasurementTool(viewportId)
+    );
+  }
+
+  /**
+   * 为多个 viewport 批量停用髋臼水平角测量工具
+   */
+  static deactivateDARAMeasurementToolForViewports(
+    viewportIds: string[]
+  ): boolean[] {
+    return viewportIds.map((viewportId) =>
+      this.deactivateDARAMeasurementTool(viewportId)
+    );
+  }
+
+  /**
+   * 为多个 viewport 批量清除髋臼水平角测量
+   */
+  static clearDARAMeasurementsForViewports(viewportIds: string[]): boolean[] {
+    return viewportIds.map((viewportId) =>
+      this.clearDARAMeasurements(viewportId)
+    );
+  }
 }