Browse Source

feat (1.70.6 -> 1.71.0): 实现Cobb角测量工具功能,支持脊柱侧弯角测量

- 新增CobbAngleMeasurementTool.ts测量工具类,实现脊柱侧弯角计算
- 在ViewerContainer.tsx中集成Cobb角测量工具配置和激活逻辑
- 在stack.image.viewer.tsx中添加工具注册和激活函数
- 在measurementPanelSlice.ts中添加Cobb角测量动作类型
- 在cornerstoneToolsSetup.ts中注册全局Cobb角测量工具
- 在measurementToolManager.ts中实现Cobb角测量工具的管理方法

改动文件:
- src/components/measures/CobbAngleMeasurementTool.ts
- src/pages/view/components/ViewerContainer.tsx
- src/pages/view/components/viewers/stack.image.viewer.tsx
- src/states/view/measurementPanelSlice.ts
- src/utils/cornerstoneToolsSetup.ts
- src/utils/measurementToolManager.ts
dengdx 6 hours ago
parent
commit
b34630bcb1

+ 19 - 0
CHANGELOG.md

@@ -2,6 +2,25 @@
 
 本项目的所有重要变更都将记录在此文件中.
 
+## [1.71.0] - 2026-01-21 09:44
+
+feat (1.70.6 -> 1.71.0): 实现Cobb角测量工具功能,支持脊柱侧弯角测量
+
+- 新增CobbAngleMeasurementTool.ts测量工具类,实现脊柱侧弯角计算
+- 在ViewerContainer.tsx中集成Cobb角测量工具配置和激活逻辑
+- 在stack.image.viewer.tsx中添加工具注册和激活函数
+- 在measurementPanelSlice.ts中添加Cobb角测量动作类型
+- 在cornerstoneToolsSetup.ts中注册全局Cobb角测量工具
+- 在measurementToolManager.ts中实现Cobb角测量工具的管理方法
+
+改动文件:
+- src/components/measures/CobbAngleMeasurementTool.ts
+- src/pages/view/components/ViewerContainer.tsx
+- src/pages/view/components/viewers/stack.image.viewer.tsx
+- src/states/view/measurementPanelSlice.ts
+- src/utils/cornerstoneToolsSetup.ts
+- src/utils/measurementToolManager.ts
+
 ## [1.70.6] - 2026-01-20 19:08
 
 refactor (1.70.5 -> 1.70.6): 更新高压发生器和探测器设备URI从DIOS更改为CCOS

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "zsis",
-  "version": "1.70.6",
+  "version": "1.71.0",
   "private": true,
   "description": "医学成像系统",
   "main": "main.js",

+ 1332 - 0
src/components/measures/CobbAngleMeasurementTool.ts

@@ -0,0 +1,1332 @@
+import { Point3, Point2 } 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,
+  drawPath: drawPathSvg,
+} = drawing;
+
+import {
+  calculateMidpoint,
+  vectorSubtract,
+  vectorAdd,
+  vectorScale,
+  getAngleBetweenLines,
+  degrees,
+} from './mathUtils';
+import {
+  PublicToolProps,
+  ToolProps,
+  EventTypes,
+  SVGDrawingHelper,
+} from '@cornerstonejs/tools/dist/esm/types';
+
+// Cobb角测量注解接口
+interface CobbAngleAnnotation extends Types.Annotation {
+  data: {
+    handles: {
+      points: CoreTypes.Point3[];
+      activeHandleIndex: number | null;
+      textBoxCobb?: CoreTypes.Point2;
+    };
+    cachedStats?: {
+      [targetId: string]: {
+        cobbAngle: number;
+        upperVertebraLine: CoreTypes.Point3;
+        lowerVertebraLine: CoreTypes.Point3;
+        angleCenter: CoreTypes.Point3;
+      };
+    };
+  };
+}
+
+// 导出的注解数据接口
+export interface ExportedCobbAnnotationData {
+  points: CoreTypes.Point3[];
+  cobbAngle: number;
+  textBoxPosition?: CoreTypes.Point2;
+  metadata: {
+    viewPlaneNormal: CoreTypes.Point3;
+    viewUp: CoreTypes.Point3;
+    FrameOfReferenceUID: string;
+    referencedImageId: string;
+  };
+}
+
+export default class CobbAngleMeasurementTool extends AnnotationTool {
+  static toolName = 'CobbAngleMeasurementTool';
+
+  editData: {
+    annotation: Types.Annotation;
+    viewportIdsToRender: string[];
+    handleIndex?: number;
+    newAnnotation?: boolean;
+    hasMoved?: boolean;
+    textBoxBeingMoved?: boolean;
+    textBoxOffset?: CoreTypes.Point2;
+    movingWholeTool?: boolean;
+    wholeToolOffset?: CoreTypes.Point2;
+  } | null = null;
+
+  isDrawing: boolean = false;
+
+  constructor(
+    toolProps: PublicToolProps = {},
+    defaultToolProps: ToolProps = {
+      supportedInteractionTypes: ['Mouse', 'Touch'],
+      configuration: {
+        shadow: true,
+        preventHandleOutsideImage: false,
+      },
+    }
+  ) {
+    super(toolProps, defaultToolProps);
+  }
+
+  /**
+   * 创建默认的Cobb角测量注解
+   * 需要8个点:上椎体端板线(4点)+ 下椎体端板线(4点)
+   */
+  static createDefaultAnnotation(
+    element: HTMLDivElement,
+    viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport,
+    config?: {
+      upperVertebraCenterX?: number;
+      upperVertebraCenterY?: number;
+      lowerVertebraCenterX?: number;
+      lowerVertebraCenterY?: number;
+      lineLength?: number;
+    }
+  ): CobbAngleAnnotation {
+    const enabledElement = getEnabledElement(element);
+    if (!enabledElement) {
+      throw new Error('Element is not enabled');
+    }
+
+    const canvas = viewport.canvas;
+    const { width, height } = canvas;
+
+    // 默认配置
+    const upperCenterX = config?.upperVertebraCenterX ?? width / 2 - 50;
+    const upperCenterY = config?.upperVertebraCenterY ?? height / 2 - 100;
+    const lowerCenterX = config?.lowerVertebraCenterX ?? width / 2 + 50;
+    const lowerCenterY = config?.lowerVertebraCenterY ?? height / 2 + 100;
+    const lineLength = config?.lineLength ?? 80;
+
+    // 上椎体端板线(水平线,模拟端板方向)
+    const upperHalfLength = lineLength / 2;
+    const upperPoint1: CoreTypes.Point2 = [
+      upperCenterX - upperHalfLength,
+      upperCenterY
+    ];
+    const upperPoint2: CoreTypes.Point2 = [
+      upperCenterX + upperHalfLength,
+      upperCenterY
+    ];
+    // 上椎体端板线的延长点(用于方向计算)
+    const upperPoint3: CoreTypes.Point2 = [
+      upperCenterX - upperHalfLength - 20,
+      upperCenterY
+    ];
+    const upperPoint4: CoreTypes.Point2 = [
+      upperCenterX + upperHalfLength + 20,
+      upperCenterY
+    ];
+
+    // 下椎体端板线(有一定倾斜角度)
+    const lowerHalfLength = lineLength / 2;
+    const lowerPoint1: CoreTypes.Point2 = [
+      lowerCenterX - lowerHalfLength,
+      lowerCenterY
+    ];
+    const lowerPoint2: CoreTypes.Point2 = [
+      lowerCenterX + lowerHalfLength,
+      lowerCenterY + 30 // 模拟侧弯
+    ];
+    // 下椎体端板线的延长点
+    const lowerPoint3: CoreTypes.Point2 = [
+      lowerCenterX - lowerHalfLength - 20,
+      lowerCenterY - 10
+    ];
+    const lowerPoint4: CoreTypes.Point2 = [
+      lowerCenterX + lowerHalfLength + 20,
+      lowerCenterY + 40
+    ];
+
+    const canvasPoints: CoreTypes.Point2[] = [
+      upperPoint1, upperPoint2, upperPoint3, upperPoint4, // 上椎体端板线
+      lowerPoint1, lowerPoint2, lowerPoint3, lowerPoint4, // 下椎体端板线
+    ];
+
+    const worldPoints = canvasPoints.map((canvasPoint) =>
+      viewport.canvasToWorld(canvasPoint)
+    );
+
+    const camera = viewport.getCamera();
+    const { viewPlaneNormal, viewUp } = camera;
+    if (viewPlaneNormal === undefined || viewUp === undefined) {
+      throw new Error('Camera parameters undefined');
+    }
+
+    return {
+      invalidated: true,
+      highlighted: false,
+      metadata: {
+        viewPlaneNormal: [...viewPlaneNormal] as CoreTypes.Point3,
+        viewUp: [...viewUp] as CoreTypes.Point3,
+        FrameOfReferenceUID: viewport.getFrameOfReferenceUID(),
+        referencedImageId: viewport.getCurrentImageId?.() || '',
+        toolName: CobbAngleMeasurementTool.toolName,
+      },
+      data: {
+        label: '',
+        handles: {
+          points: worldPoints,
+          activeHandleIndex: null,
+        },
+        cachedStats: {},
+      },
+    } as CobbAngleAnnotation;
+  }
+
+  /**
+   * 导出Cobb角测量数据
+   */
+  static exportAnnotationData(
+    annotation: CobbAngleAnnotation,
+    viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport
+  ): ExportedCobbAnnotationData | null {
+    const targetId = `imageId:${viewport.getCurrentImageId?.() || ''}`;
+    const cachedStats = annotation.data.cachedStats?.[targetId];
+
+    if (!cachedStats || cachedStats.cobbAngle === undefined) {
+      return null;
+    }
+
+    return {
+      points: [...annotation.data.handles.points],
+      cobbAngle: cachedStats.cobbAngle,
+      textBoxPosition: annotation.data.handles.textBoxCobb,
+      metadata: {
+        viewPlaneNormal: annotation.metadata?.viewPlaneNormal || [0, 0, 1],
+        viewUp: annotation.metadata?.viewUp || [0, 1, 0],
+        FrameOfReferenceUID: annotation.metadata?.FrameOfReferenceUID || '',
+        referencedImageId: annotation.metadata?.referencedImageId || '',
+      },
+    };
+  }
+
+  /**
+   * 从导出数据恢复注解
+   */
+  static restoreFromExportedData(
+    exportedData: ExportedCobbAnnotationData,
+    element: HTMLDivElement,
+    viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport
+  ): CobbAngleAnnotation {
+    const enabledElement = getEnabledElement(element);
+    if (!enabledElement) {
+      throw new Error('Element is not enabled');
+    }
+
+    return {
+      invalidated: true,
+      highlighted: false,
+      metadata: {
+        ...exportedData.metadata,
+        toolName: CobbAngleMeasurementTool.toolName,
+      },
+      data: {
+        label: '',
+        handles: {
+          points: [...exportedData.points],
+          activeHandleIndex: null,
+          textBoxCobb: exportedData.textBoxPosition,
+        },
+        cachedStats: {},
+      },
+    } as CobbAngleAnnotation;
+  }
+
+  addNewAnnotation(evt: EventTypes.InteractionEventType): CobbAngleAnnotation {
+    evt.preventDefault();
+    return {} as CobbAngleAnnotation;
+  }
+
+  /**
+   * 检查点是否在工具附近
+   */
+  isPointNearTool(
+    element: HTMLDivElement,
+    annotation: CobbAngleAnnotation,
+    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 canvasPoint = viewport.worldToCanvas(points[i]);
+      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 >= 8) {
+      const canvasPoints = points.map((p) => viewport.worldToCanvas(p));
+
+      // 检查上椎体端板线
+      const distToUpperLine = this._distanceToSegment(canvasCoords, canvasPoints[0], canvasPoints[1]);
+      if (distToUpperLine < proximity) return true;
+
+      // 检查下椎体端板线
+      const distToLowerLine = this._distanceToSegment(canvasCoords, canvasPoints[4], canvasPoints[5]);
+      if (distToLowerLine < proximity) return true;
+    }
+
+    return false;
+  }
+
+  /**
+   * 计算点到线段的距离
+   */
+  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);
+  }
+
+  /**
+   * 创建角度弧线点
+   */
+  private _createAngleArcPoints(
+    center: CoreTypes.Point2,
+    vector1: CoreTypes.Point2,
+    vector2: CoreTypes.Point2,
+    radius: number
+  ): CoreTypes.Point2[] {
+    // 计算两个向量的角度
+    const angle1 = Math.atan2(vector1[1], vector1[0]);
+    const angle2 = Math.atan2(vector2[1], vector2[0]);
+
+    let angleDiff = angle2 - angle1;
+
+    // 归一化角度差到 [-π, π] 范围
+    while (angleDiff > Math.PI) {
+      angleDiff -= 2 * Math.PI;
+    }
+    while (angleDiff < -Math.PI) {
+      angleDiff += 2 * Math.PI;
+    }
+
+    // 计算两个向量之间的夹角(0-π)
+    const crossProduct = vector1[0] * vector2[1] - vector1[1] * vector2[0];
+    const isClockwise = crossProduct < 0; // 叉积为负表示顺时针
+
+    // 确保使用锐角(最小角度),但保持正确的方向
+    let startAngle: number;
+    let endAngle: number;
+
+    if (Math.abs(angleDiff) <= Math.PI / 2) {
+      // 锐角情况:直接使用当前角度
+      startAngle = angle1;
+      endAngle = angle2;
+    } else {
+      // 钝角情况:使用补角,但要选择正确的内侧
+      if (angleDiff > 0) {
+        // angle2 在 angle1 的顺时针方向
+        if (isClockwise) {
+          // 顺时针方向,使用补角的逆时针弧
+          startAngle = angle1;
+          endAngle = angle1 + (angleDiff - Math.PI);
+        } else {
+          // 逆时针方向,使用补角的顺时针弧
+          startAngle = angle2;
+          endAngle = angle2 - (angleDiff - Math.PI);
+        }
+      } else {
+        // angle2 在 angle1 的逆时针方向
+        if (isClockwise) {
+          // 顺时针方向,使用补角的逆时针弧
+          startAngle = angle2;
+          endAngle = angle2 + (Math.abs(angleDiff) - Math.PI);
+        } else {
+          // 逆时针方向,使用补角的顺时针弧
+          startAngle = angle1;
+          endAngle = angle1 - (Math.abs(angleDiff) - Math.PI);
+        }
+      }
+    }
+
+    // 归一化角度到 [0, 2π] 范围
+    const normalizeAngle = (angle: number): number => {
+      while (angle < 0) angle += 2 * Math.PI;
+      while (angle >= 2 * Math.PI) angle -= 2 * Math.PI;
+      return angle;
+    };
+
+    startAngle = normalizeAngle(startAngle);
+    endAngle = normalizeAngle(endAngle);
+
+    // 计算弧线长度
+    let arcLength = endAngle - startAngle;
+    if (arcLength < 0) arcLength += 2 * Math.PI;
+
+    // 确保弧线长度不超过180度(锐角)
+    if (arcLength > Math.PI) {
+      [startAngle, endAngle] = [endAngle, startAngle];
+      arcLength = 2 * Math.PI - arcLength;
+    }
+
+    const numPoints = Math.max(10, Math.ceil(arcLength / (Math.PI / 18)));
+    const points: CoreTypes.Point2[] = [];
+
+    for (let i = 0; i <= numPoints; i++) {
+      const t = i / numPoints;
+      const angle = startAngle + arcLength * t;
+      const x = center[0] + radius * Math.cos(angle);
+      const y = center[1] + radius * Math.sin(angle);
+      points.push([x, y]);
+    }
+
+    return points;
+  }
+
+  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);
+    element.addEventListener('CORNERSTONE_TOOLS_MOUSE_MOVE', this._mouseMoveModifyCallback as EventListener);
+    element.addEventListener('keydown', this._keyDownCallback 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);
+    element.removeEventListener('CORNERSTONE_TOOLS_MOUSE_MOVE', this._mouseMoveModifyCallback as EventListener);
+    element.removeEventListener('keydown', this._keyDownCallback 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 CobbAngleAnnotation;
+      const targetId = this.getTargetId(viewport);
+      const cachedStats = targetId ? customAnn.data.cachedStats?.[targetId] : undefined;
+
+      if (cachedStats?.angleCenter) {
+        // 计算默认文本框位置(与renderAnnotation中的逻辑一致)
+        let defaultTextBoxPosition: CoreTypes.Point2;
+        
+        if (customAnn.data.handles.points.length >= 8) {
+          const canvasPoints = customAnn.data.handles.points.map(p => viewport.worldToCanvas(p));
+          
+          // 计算两条线段的中点
+          const upperMidCanvas: CoreTypes.Point2 = [
+            (canvasPoints[0][0] + canvasPoints[1][0]) / 2,
+            (canvasPoints[0][1] + canvasPoints[1][1]) / 2
+          ];
+          const lowerMidCanvas: CoreTypes.Point2 = [
+            (canvasPoints[4][0] + canvasPoints[5][0]) / 2,
+            (canvasPoints[4][1] + canvasPoints[5][1]) / 2
+          ];
+
+          // 计算连接线的中点
+          const midlineCenter: CoreTypes.Point2 = [
+            (upperMidCanvas[0] + lowerMidCanvas[0]) / 2,
+            (upperMidCanvas[1] + lowerMidCanvas[1]) / 2
+          ];
+
+          // 计算连接线的方向向量
+          const midlineVector: CoreTypes.Point2 = [
+            lowerMidCanvas[0] - upperMidCanvas[0],
+            lowerMidCanvas[1] - upperMidCanvas[1]
+          ];
+          const midlineLength = Math.sqrt(
+            midlineVector[0] * midlineVector[0] + midlineVector[1] * midlineVector[1]
+          );
+
+          // 计算垂直方向
+          const perpVector: CoreTypes.Point2 = [
+            -midlineVector[1] / midlineLength,
+            midlineVector[0] / midlineLength
+          ];
+
+          // 角度标识线的末端
+          const angleLineLength = 50;
+          const angleLineEnd: CoreTypes.Point2 = [
+            midlineCenter[0] + perpVector[0] * angleLineLength,
+            midlineCenter[1] + perpVector[1] * angleLineLength
+          ];
+
+          defaultTextBoxPosition = [
+            angleLineEnd[0] + 10,
+            angleLineEnd[1] - 10,
+          ];
+        } else {
+          const angleCenterCanvas = viewport.worldToCanvas(cachedStats.angleCenter);
+          defaultTextBoxPosition = [
+            angleCenterCanvas[0] + 40,
+            angleCenterCanvas[1] - 10,
+          ];
+        }
+
+        // 获取文本框位置,如果不存在则使用默认位置
+        const textBoxCobb: CoreTypes.Point2 = customAnn.data.handles.textBoxCobb || defaultTextBoxPosition;
+
+        if (this._isPointInTextBox(canvasCoords, textBoxCobb)) {
+          const viewportIdsToRender =
+            utilities.viewportFilters.getViewportIdsWithToolToRender(element, this.getToolName());
+
+          // 计算鼠标点击位置相对于文本框左上角的偏移量
+          const textBoxOffset: CoreTypes.Point2 = [
+            canvasCoords[0] - textBoxCobb[0],
+            canvasCoords[1] - textBoxCobb[1],
+          ];
+
+          this.editData = {
+            annotation: customAnn,
+            viewportIdsToRender,
+            hasMoved: false,
+            textBoxBeingMoved: true,
+            textBoxOffset: textBoxOffset,
+          };
+
+          customAnn.highlighted = true;
+          customAnn.isSelected = true;  // 点击文本时设置选中状态
+          utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
+          evt.preventDefault();
+          evt.stopPropagation();
+          return;
+        }
+      }
+    }
+
+    // 如果没有点击文本框,再检查是否点击在手柄上
+    for (const ann of annotations) {
+      const customAnn = ann as CobbAngleAnnotation;
+      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;
+        customAnn.isSelected = true;  // 点击手柄时设置选中状态
+        utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
+        evt.preventDefault();
+        evt.stopPropagation();
+        return;
+      }
+    }
+
+    // 如果没有点击手柄,再检查是否点击在线段上(整体移动)
+    for (const ann of annotations) {
+      const customAnn = ann as CobbAngleAnnotation;
+      if (this.isPointNearTool(element, customAnn, canvasCoords, 10)) {
+        const viewportIdsToRender =
+          utilities.viewportFilters.getViewportIdsWithToolToRender(element, this.getToolName());
+
+        // 使用两条测量线的中心作为拖拽基准点
+        const upperMid = calculateMidpoint(customAnn.data.handles.points[0], customAnn.data.handles.points[1]);
+        const lowerMid = calculateMidpoint(customAnn.data.handles.points[4], customAnn.data.handles.points[5]);
+        const toolCenter = calculateMidpoint(upperMid, lowerMid);
+        const toolCenterCanvas = viewport.worldToCanvas(toolCenter);
+
+        // 计算鼠标点击位置相对工具中心的偏移
+        const wholeToolOffset: CoreTypes.Point2 = [
+          canvasCoords[0] - toolCenterCanvas[0],
+          canvasCoords[1] - toolCenterCanvas[1],
+        ];
+
+        this.editData = {
+          annotation: customAnn,
+          viewportIdsToRender,
+          hasMoved: false,
+          movingWholeTool: true,
+          wholeToolOffset: wholeToolOffset,
+        };
+
+        customAnn.isSelected = true;  // 点击线段时设置选中状态
+        utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
+        evt.preventDefault();
+        evt.stopPropagation();
+        return;
+      }
+    }
+
+    // 如果没有点击在工具上,取消所有工具的选中状态
+    for (const ann of annotations) {
+      const customAnn = ann as CobbAngleAnnotation;
+      customAnn.isSelected = false;
+    }
+
+    // 触发渲染更新
+    const viewportIdsToRender =
+      utilities.viewportFilters.getViewportIdsWithToolToRender(element, this.getToolName());
+    utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
+  };
+
+  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
+    );
+  }
+
+  _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 CobbAngleAnnotation;
+    const { data } = customAnn;
+
+    if (textBoxBeingMoved && this.editData.textBoxOffset) {
+      data.handles.textBoxCobb = [
+        canvasCoords[0] - this.editData.textBoxOffset[0],
+        canvasCoords[1] - this.editData.textBoxOffset[1],
+      ];
+      this.editData.hasMoved = true;
+      utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
+      evt.preventDefault();
+      evt.stopPropagation();
+      return;
+    }
+
+    // 如果正在移动整个工具
+    if (this.editData.movingWholeTool && this.editData.wholeToolOffset) {
+      const { viewport } = enabledElement;
+
+      // 计算新的工具中心位置
+      const newCenterCanvas: CoreTypes.Point2 = [
+        canvasCoords[0] - this.editData.wholeToolOffset[0],
+        canvasCoords[1] - this.editData.wholeToolOffset[1],
+      ];
+
+      // 获取当前工具中心位置
+      const upperMid = calculateMidpoint(customAnn.data.handles.points[0], customAnn.data.handles.points[1]);
+      const lowerMid = calculateMidpoint(customAnn.data.handles.points[4], customAnn.data.handles.points[5]);
+      const currentCenter = calculateMidpoint(upperMid, lowerMid);
+      const currentCenterCanvas = viewport.worldToCanvas(currentCenter);
+
+      // 计算偏移量(canvas坐标)
+      const offsetCanvas: CoreTypes.Point2 = [
+        newCenterCanvas[0] - currentCenterCanvas[0],
+        newCenterCanvas[1] - currentCenterCanvas[1],
+      ];
+
+      // 将canvas偏移转换为world偏移
+      const offsetWorld = [
+        viewport.canvasToWorld(offsetCanvas)[0] - viewport.canvasToWorld([0, 0])[0],
+        viewport.canvasToWorld(offsetCanvas)[1] - viewport.canvasToWorld([0, 0])[1],
+        0, // z轴偏移为0,保持在2D平面上
+      ] as CoreTypes.Point3;
+
+      // 应用相同的偏移量给所有控制点
+      for (let i = 0; i < data.handles.points.length; i++) {
+        data.handles.points[i] = [
+          data.handles.points[i][0] + offsetWorld[0],
+          data.handles.points[i][1] + offsetWorld[1],
+          data.handles.points[i][2],
+        ];
+      }
+
+      // 更新文本框位置
+      if (data.handles.textBoxCobb) {
+        data.handles.textBoxCobb[0] += offsetCanvas[0];
+        data.handles.textBoxCobb[1] += offsetCanvas[1];
+      }
+
+      // 重新计算几何属性(角度等)
+      this._updateCachedStats(customAnn, enabledElement);
+
+      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 CobbAngleAnnotation;
+
+    customAnn.data.handles.activeHandleIndex = null;
+    customAnn.highlighted = false;
+
+    const viewportIdsToRender =
+      utilities.viewportFilters.getViewportIdsWithToolToRender(
+        evt.detail.element,
+        this.getToolName()
+      );
+
+    utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
+    this.editData = null;
+    evt.preventDefault();
+    evt.stopPropagation();
+  };
+
+  _mouseMoveModifyCallback = (evt: EventTypes.InteractionEventType): void => {
+    const eventDetail = evt.detail;
+    const { element, currentPoints } = eventDetail;
+    if (!currentPoints || !currentPoints.canvas) {
+      return;
+    }
+    const canvasCoords = currentPoints.canvas;
+
+    const enabledElement = getEnabledElement(element);
+    if (!enabledElement) {
+      element.style.cursor = 'default';
+      return;
+    }
+    const { viewport } = enabledElement;
+
+    const annotations = annotation.state.getAnnotations(this.getToolName(), element);
+    if (!annotations || annotations.length === 0) {
+      element.style.cursor = 'default';
+      return;
+    }
+
+    let isHovering = false;
+
+    // 检查是否悬停在文本框上
+    for (const ann of annotations) {
+      const customAnn = ann as CobbAngleAnnotation;
+      const targetId = this.getTargetId(viewport);
+      const cachedStats = targetId ? customAnn.data.cachedStats?.[targetId] : undefined;
+
+      if (cachedStats?.angleCenter) {
+        // 计算默认文本框位置(与renderAnnotation中的逻辑一致)
+        let defaultTextBoxPosition: CoreTypes.Point2;
+        
+        if (customAnn.data.handles.points.length >= 8) {
+          const canvasPoints = customAnn.data.handles.points.map(p => viewport.worldToCanvas(p));
+          
+          // 计算两条线段的中点
+          const upperMidCanvas: CoreTypes.Point2 = [
+            (canvasPoints[0][0] + canvasPoints[1][0]) / 2,
+            (canvasPoints[0][1] + canvasPoints[1][1]) / 2
+          ];
+          const lowerMidCanvas: CoreTypes.Point2 = [
+            (canvasPoints[4][0] + canvasPoints[5][0]) / 2,
+            (canvasPoints[4][1] + canvasPoints[5][1]) / 2
+          ];
+
+          // 计算连接线的中点
+          const midlineCenter: CoreTypes.Point2 = [
+            (upperMidCanvas[0] + lowerMidCanvas[0]) / 2,
+            (upperMidCanvas[1] + lowerMidCanvas[1]) / 2
+          ];
+
+          // 计算连接线的方向向量
+          const midlineVector: CoreTypes.Point2 = [
+            lowerMidCanvas[0] - upperMidCanvas[0],
+            lowerMidCanvas[1] - upperMidCanvas[1]
+          ];
+          const midlineLength = Math.sqrt(
+            midlineVector[0] * midlineVector[0] + midlineVector[1] * midlineVector[1]
+          );
+
+          // 计算垂直方向
+          const perpVector: CoreTypes.Point2 = [
+            -midlineVector[1] / midlineLength,
+            midlineVector[0] / midlineLength
+          ];
+
+          // 角度标识线的末端
+          const angleLineLength = 50;
+          const angleLineEnd: CoreTypes.Point2 = [
+            midlineCenter[0] + perpVector[0] * angleLineLength,
+            midlineCenter[1] + perpVector[1] * angleLineLength
+          ];
+
+          defaultTextBoxPosition = [
+            angleLineEnd[0] + 10,
+            angleLineEnd[1] - 10,
+          ];
+        } else {
+          const angleCenterCanvas = viewport.worldToCanvas(cachedStats.angleCenter);
+          defaultTextBoxPosition = [
+            angleCenterCanvas[0] + 40,
+            angleCenterCanvas[1] - 10,
+          ];
+        }
+
+        const textBoxCobb: CoreTypes.Point2 = customAnn.data.handles.textBoxCobb || defaultTextBoxPosition;
+
+        if (this._isPointInTextBox(canvasCoords, textBoxCobb)) {
+          element.style.cursor = 'pointer'; // 手型光标
+          customAnn.highlighted = true;
+          isHovering = true;
+          break;
+        }
+      }
+    }
+
+    // 如果没有悬停在文本框上,检查是否悬停在手柄、线段上
+    if (!isHovering) {
+      for (const ann of annotations) {
+        const customAnn = ann as CobbAngleAnnotation;
+        const targetId = this.getTargetId(viewport);
+        const cachedStats = targetId ? customAnn.data.cachedStats?.[targetId] : undefined;
+
+        // 检查是否靠近手柄
+        const handle = this.getHandleNearImagePoint(element, customAnn, canvasCoords, 6);
+        if (handle) {
+          element.style.cursor = 'crosshair'; // 十字准星光标
+          customAnn.highlighted = true;
+          isHovering = true;
+          break;
+        }
+
+        // 检查是否靠近线段
+        if (this.isPointNearTool(element, customAnn, canvasCoords, 10)) {
+          element.style.cursor = 'crosshair'; // 十字准星光标
+          customAnn.highlighted = true;
+          isHovering = true;
+          break;
+        }
+      }
+    }
+
+    // 如果没有悬停在任何地方,重置高亮和光标
+    if (!isHovering) {
+      for (const ann of annotations) {
+        const customAnn = ann as CobbAngleAnnotation;
+        customAnn.highlighted = false;
+      }
+      element.style.cursor = 'default';
+    }
+
+    // 触发渲染以更新高亮状态
+    const viewportIdsToRender = utilities.viewportFilters.getViewportIdsWithToolToRender(
+      element,
+      this.getToolName()
+    );
+    utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
+  };
+
+  _keyDownCallback = (evt: KeyboardEvent): void => {
+    if (evt.key === 'Delete') {
+      const element = document.activeElement as HTMLDivElement;
+      if (!element) return;
+
+      const annotations = annotation.state.getAnnotations(this.getToolName(), element);
+
+      // 查找选中的注解
+      const selectedAnnotation = annotations.find(ann => ann.isSelected);
+      if (selectedAnnotation) {
+        // 删除选中的注解
+        annotation.state.removeAnnotation(selectedAnnotation.annotationUID!);
+
+        // 触发视图更新
+        const viewportIdsToRender =
+          utilities.viewportFilters.getViewportIdsWithToolToRender(
+            element,
+            this.getToolName()
+          );
+        utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
+
+        evt.preventDefault();
+        evt.stopPropagation();
+      }
+    }
+  };
+
+  handleSelectedCallback(
+    evt: EventTypes.InteractionEventType,
+    annotation: CobbAngleAnnotation
+  ): void {
+    annotation.highlighted = true;
+    const viewportIdsToRender =
+      utilities.viewportFilters.getViewportIdsWithToolToRender(
+        evt.detail.element,
+        this.getToolName()
+      );
+    utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
+    evt.preventDefault();
+  }
+
+  toolSelectedCallback(
+    evt: EventTypes.InteractionEventType,
+    annotation: CobbAngleAnnotation
+  ): void {
+    // 实现工具选中逻辑
+  }
+
+  _dragCallback = (evt: EventTypes.InteractionEventType): void => {
+    this.isDrawing = true;
+    const enabledElement = getEnabledElement(evt.detail.element);
+    if (!enabledElement || !this.editData) return;
+
+    const { annotation, viewportIdsToRender } = this.editData;
+    const { data } = annotation as CobbAngleAnnotation;
+
+    if (annotation) {
+      const worldPos = evt.detail.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 CobbAngleAnnotation, enabledElement);
+        this.editData!.hasMoved = true;
+      }
+
+      utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
+    }
+  };
+
+  _endCallback = (evt: EventTypes.InteractionEventType): void => {
+    if (!this.editData) return;
+    if (this.editData.hasMoved) {
+      this.editData.hasMoved = false;
+    }
+  };
+
+  getHandleNearImagePoint(
+    element: HTMLDivElement,
+    annotation: CobbAngleAnnotation,
+    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 canvasPoint = viewport.worldToCanvas(points[i]);
+      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: points[i] } as Types.ToolHandle;
+      }
+    }
+
+    annotation.data.handles.activeHandleIndex = null;
+    return undefined;
+  }
+
+  _updateCachedStats(
+    annotation: CobbAngleAnnotation,
+    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] = {
+        cobbAngle: 0,
+        upperVertebraLine: [0, 0, 0] as CoreTypes.Point3,
+        lowerVertebraLine: [0, 0, 0] as CoreTypes.Point3,
+        angleCenter: [0, 0, 0] as CoreTypes.Point3,
+      };
+    }
+
+    const stats = data.cachedStats[targetId];
+
+    if (points.length >= 8) {
+      // 上椎体端板线:points[0] -> points[1]
+      const upperPoint1 = points[0];
+      const upperPoint2 = points[1];
+      const upperVertebraVector = vectorSubtract(upperPoint2, upperPoint1);
+      stats.upperVertebraLine = upperVertebraVector;
+
+      // 下椎体端板线:points[4] -> points[5]
+      const lowerPoint1 = points[4];
+      const lowerPoint2 = points[5];
+      const lowerVertebraVector = vectorSubtract(lowerPoint2, lowerPoint1);
+      stats.lowerVertebraLine = lowerVertebraVector;
+
+      // 计算两条平行线的夹角
+      const angleRad = getAngleBetweenLines(upperVertebraVector, lowerVertebraVector);
+      // Cobb角通常取锐角
+      const acuteAngleRad = Math.min(angleRad, Math.PI - angleRad);
+      stats.cobbAngle = Math.round(degrees(acuteAngleRad));
+
+      // 计算两条延长线的交汇点作为角度中心点
+      const intersectionPoint = this._calculateExtensionLinesIntersection(
+        upperPoint1, upperPoint2, lowerPoint1, lowerPoint2
+      );
+      stats.angleCenter = intersectionPoint;
+    }
+  }
+
+  /**
+   * 计算两条延长线的交汇点
+   */
+  private _calculateExtensionLinesIntersection(
+    upperPoint1: CoreTypes.Point3,
+    upperPoint2: CoreTypes.Point3,
+    lowerPoint1: CoreTypes.Point3,
+    lowerPoint2: CoreTypes.Point3
+  ): CoreTypes.Point3 {
+    // 将3D点转换为2D点(忽略z坐标)
+    const upperP1: CoreTypes.Point2 = [upperPoint1[0], upperPoint1[1]];
+    const upperP2: CoreTypes.Point2 = [upperPoint2[0], upperPoint2[1]];
+    const lowerP1: CoreTypes.Point2 = [lowerPoint1[0], lowerPoint1[1]];
+    const lowerP2: CoreTypes.Point2 = [lowerPoint2[0], lowerPoint2[1]];
+
+    // 计算两条线的方向向量
+    const upperVector: CoreTypes.Point2 = [
+      upperP2[0] - upperP1[0],
+      upperP2[1] - upperP1[1]
+    ];
+    const lowerVector: CoreTypes.Point2 = [
+      lowerP2[0] - lowerP1[0],
+      lowerP2[1] - lowerP1[1]
+    ];
+
+    // 计算两条线的交汇点
+    const intersection = this._calculateLineIntersection(upperP1, upperVector, lowerP1, lowerVector);
+
+    // 如果两条线平行或几乎平行,返回两条线的中点
+    if (!intersection) {
+      const upperMid = calculateMidpoint(upperPoint1, upperPoint2);
+      const lowerMid = calculateMidpoint(lowerPoint1, lowerPoint2);
+      return calculateMidpoint(upperMid, lowerMid);
+    }
+
+    // 返回交汇点,保持z坐标为0
+    return [intersection[0], intersection[1], 0];
+  }
+
+  /**
+   * 计算两条线的交汇点
+   */
+  private _calculateLineIntersection(
+    point1: CoreTypes.Point2,
+    vector1: CoreTypes.Point2,
+    point2: CoreTypes.Point2,
+    vector2: CoreTypes.Point2
+  ): CoreTypes.Point2 | null {
+    const x1 = point1[0], y1 = point1[1];
+    const dx1 = vector1[0], dy1 = vector1[1];
+    const x2 = point2[0], y2 = point2[1];
+    const dx2 = vector2[0], dy2 = vector2[1];
+
+    // 计算行列式
+    const denominator = dx1 * dy2 - dy1 * dx2;
+
+    // 如果行列式接近0,两条线平行或重合
+    if (Math.abs(denominator) < 1e-10) {
+      return null;
+    }
+
+    // 计算参数t
+    const t = ((x2 - x1) * dy2 - (y2 - y1) * dx2) / denominator;
+
+    // 计算交汇点
+    const intersectionX = x1 + t * dx1;
+    const intersectionY = y1 + t * dy1;
+
+    return [intersectionX, intersectionY];
+  }
+
+  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 CobbAngleAnnotation;
+      const { annotationUID, data } = annotation;
+      const points = data.handles.points;
+
+      if (points.length < 8) continue;
+
+      const targetId = this.getTargetId(viewport);
+      const cachedStats = targetId ? data.cachedStats?.[targetId] : undefined;
+      const canvasPoints = points.map((p) => viewport.worldToCanvas(p));
+      const lineColor = annotation.highlighted ? 'rgb(0, 150, 255)' : 'rgb(255, 255, 255)';
+
+      // 绘制上椎体端板线
+      if (annotationUID) {
+        drawLineSvg(
+          svgDrawingHelper,
+          annotationUID,
+          `${annotationUID}-upperVertebra`,
+          canvasPoints[0],
+          canvasPoints[1],
+          { color: lineColor, width: 3 }
+        );
+
+        // 绘制下椎体端板线
+        drawLineSvg(
+          svgDrawingHelper,
+          annotationUID,
+          `${annotationUID}-lowerVertebra`,
+          canvasPoints[4],
+          canvasPoints[5],
+          { color: lineColor, width: 3 }
+        );
+
+        // 绘制角度标识线和文本
+        if (cachedStats && cachedStats.angleCenter) {
+          // 计算两条线段的中点
+          const upperMidCanvas: CoreTypes.Point2 = [
+            (canvasPoints[0][0] + canvasPoints[1][0]) / 2,
+            (canvasPoints[0][1] + canvasPoints[1][1]) / 2
+          ];
+          const lowerMidCanvas: CoreTypes.Point2 = [
+            (canvasPoints[4][0] + canvasPoints[5][0]) / 2,
+            (canvasPoints[4][1] + canvasPoints[5][1]) / 2
+          ];
+
+          // 绘制连接两个中点的虚线
+          drawLineSvg(
+            svgDrawingHelper,
+            annotationUID,
+            `${annotationUID}-midline`,
+            upperMidCanvas,
+            lowerMidCanvas,
+            { color: 'rgb(255, 200, 0)', width: 1, lineDash: '4,4' }
+          );
+
+          // 计算连接线的中点
+          const midlineCenter: CoreTypes.Point2 = [
+            (upperMidCanvas[0] + lowerMidCanvas[0]) / 2,
+            (upperMidCanvas[1] + lowerMidCanvas[1]) / 2
+          ];
+
+          // 计算连接线的方向向量(归一化)
+          const midlineVector: CoreTypes.Point2 = [
+            lowerMidCanvas[0] - upperMidCanvas[0],
+            lowerMidCanvas[1] - upperMidCanvas[1]
+          ];
+          const midlineLength = Math.sqrt(
+            midlineVector[0] * midlineVector[0] + midlineVector[1] * midlineVector[1]
+          );
+
+          // 计算垂直于连接线的方向(向右侧延伸)
+          const perpVector: CoreTypes.Point2 = [
+            -midlineVector[1] / midlineLength,
+            midlineVector[0] / midlineLength
+          ];
+
+          // 角度标识线的长度
+          const angleLineLength = 50;
+          const angleLineEnd: CoreTypes.Point2 = [
+            midlineCenter[0] + perpVector[0] * angleLineLength,
+            midlineCenter[1] + perpVector[1] * angleLineLength
+          ];
+
+          // 绘制角度标识线(从连接线中点伸出的虚线)
+          drawLineSvg(
+            svgDrawingHelper,
+            annotationUID,
+            `${annotationUID}-angleline`,
+            midlineCenter,
+            angleLineEnd,
+            { color: 'rgb(255, 0, 0)', width: 2, lineDash: '4,4' }
+          );
+
+          // 文本位置:在角度标识线的末端
+          const textLines = [`Cobb角: ${cachedStats.cobbAngle}°`];
+          const textBoxPosition: CoreTypes.Point2 = data.handles.textBoxCobb || [
+            angleLineEnd[0] + 10,
+            angleLineEnd[1] - 10,
+          ];
+
+          // 文本连接到角度标识线末端
+          drawLinkedTextBox(
+            svgDrawingHelper,
+            annotationUID,
+            `${annotationUID}-text`,
+            textLines,
+            textBoxPosition,
+            [angleLineEnd],
+            {},
+            { color: 'rgb(255, 0, 0)' }
+          );
+        }
+
+        // 绘制控制点(只绘制前4个点:上下两条线的端点)
+        const isSelected = annotation.isSelected || false;
+        const handleRadius = isSelected ? 12 : 6;
+
+        // 只绘制上椎体端板线的两个端点和下椎体端板线的两个端点
+        const visibleHandles = [
+          canvasPoints[0], // 上椎体端板线起点
+          canvasPoints[1], // 上椎体端板线终点
+          canvasPoints[4], // 下椎体端板线起点
+          canvasPoints[5], // 下椎体端板线终点
+        ];
+
+        drawHandles(
+          svgDrawingHelper,
+          annotationUID,
+          '0',
+          visibleHandles,
+          { color: 'rgb(255, 255, 255)', handleRadius: handleRadius }
+        );
+      }
+
+      renderStatus = true;
+    }
+
+    return renderStatus;
+  };
+}

+ 9 - 1
src/pages/view/components/ViewerContainer.tsx

@@ -44,6 +44,7 @@ import StackViewer, {
   activatePolylineLengthMeasurement,
   activateLineGrayscaleMeasurement,
   activateRectangleGrayscaleMeasurement,
+  activateCobbAngleMeasurement,
   activateMask,
   clearMask,
   deactivateMagnifier,
@@ -185,6 +186,11 @@ const MEASUREMENT_TOOL_CONFIGS: Record<string, MeasurementToolConfig> = {
     activateFunction: MeasurementToolManager.activateRectangleGrayscaleMeasurementTool,
     logPrefix: 'RectangleGrayscale',
   },
+  'Cobb角': {
+    toolName: 'CobbAngleMeasurementTool',
+    activateFunction: activateCobbAngleMeasurement,
+    logPrefix: 'CobbAngle',
+  },
 };
 
 const setup = () => {
@@ -785,6 +791,7 @@ const ViewerContainer: React.FC<ViewerContainerProps> = ({ imageUrls }) => {
             MeasurementToolManager.clearPolylineLengthMeasurementsForViewports(selectedViewportIds);
             MeasurementToolManager.clearLineGrayscaleMeasurementsForViewports(selectedViewportIds);
             MeasurementToolManager.clearRectangleGrayscaleMeasurementsForViewports(selectedViewportIds);
+            MeasurementToolManager.clearCobbAngleMeasurementsForViewports(selectedViewportIds);
           }
           console.log('Clearing All Measurements from MeasurementPanel');
           break;
@@ -843,7 +850,8 @@ const ViewerContainer: React.FC<ViewerContainerProps> = ({ imageUrls }) => {
         case '多边形长度测量':
         case '拆线长度测量':
         case '直线灰度':
-        case '矩形区域灰度': {
+        case '矩形区域灰度':
+        case 'Cobb角': {
           const config = MEASUREMENT_TOOL_CONFIGS[measurementAction];
           if (config) {
             activateMeasurementTool(config, selectedViewportIds);

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

@@ -23,6 +23,7 @@ import VerticalTiltMeasurementTool from '@/components/measures/VerticalTiltMeasu
 import HorizontalTiltMeasurementTool from '@/components/measures/HorizontalTiltMeasurementTool';
 import LineGrayscaleMeasurementTool from '@/components/measures/LineGrayscaleMeasurementTool';
 import RectangleGrayscaleMeasurementTool from '@/components/measures/RectangleGrayscaleMeasurementTool';
+import CobbAngleMeasurementTool from '@/components/measures/CobbAngleMeasurementTool';
 import MaskTool from '@/components/tools/MaskTool';
 import DicomOverlayTool from '@/components/overlay/DicomOverlayTool';
 import ImageViewerErrorBoundary from './ImageViewerErrorBoundary';
@@ -261,6 +262,7 @@ function registerTools(viewportId, renderingEngineId) {
   toolGroup.addTool(PolylineLengthMeasurementTool.toolName); // 添加拆线长度测量工具
   toolGroup.addTool(LineGrayscaleMeasurementTool.toolName); // 添加直线灰度测量工具
   toolGroup.addTool(RectangleGrayscaleMeasurementTool.toolName); // 添加矩形区域灰度测量工具
+  toolGroup.addTool(CobbAngleMeasurementTool.toolName); // 添加Cobb角测量工具
   toolGroup.addTool(MaskTool.toolName); // 添加掩码工具
 
   // 设置默认工具状态
@@ -366,6 +368,7 @@ function setupDefaultToolStates(toolGroup: cornerstoneTools.Types.IToolGroup) {
   toolGroup.setToolPassive(PolylineLengthMeasurementTool.toolName);
   toolGroup.setToolPassive(LineGrayscaleMeasurementTool.toolName);
   toolGroup.setToolPassive(RectangleGrayscaleMeasurementTool.toolName);
+  toolGroup.setToolPassive(CobbAngleMeasurementTool.toolName);
   toolGroup.setToolPassive(MaskTool.toolName);
 }
 export function addLMark(currentViewportId: string): void {
@@ -1613,6 +1616,14 @@ export function activateRectangleGrayscaleMeasurement(viewportId: string): boole
   return MeasurementToolManager.activateRectangleGrayscaleMeasurementTool(viewportId);
 }
 
+/**
+ * 激活Cobb角测量工具
+ */
+export function activateCobbAngleMeasurement(viewportId: string): boolean {
+  console.log(`[activateCobbAngleMeasurement] Activating CobbAngle measurement for viewport: ${viewportId}`);
+  return MeasurementToolManager.activateCobbAngleMeasurementTool(viewportId);
+}
+
 /**
  * 停用矩形区域灰度测量工具
  */

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

@@ -26,6 +26,7 @@ export type MeasurementAction =
   | '拆线长度测量'
   | '矩形区域灰度'
   | '直线灰度'
+  | 'Cobb角'
   | null;
 
 // 测量结果类型

+ 2 - 0
src/utils/cornerstoneToolsSetup.ts

@@ -20,6 +20,7 @@ import PolylineLengthMeasurementTool from '@/components/measures/PolylineLengthM
 import LineGrayscaleMeasurementTool from '@/components/measures/LineGrayscaleMeasurementTool';
 import RectangleGrayscaleMeasurementTool from '@/components/measures/RectangleGrayscaleMeasurementTool';
 import MaskTool from '@/components/tools/MaskTool';
+import CobbAngleMeasurementTool from '@/components/measures/CobbAngleMeasurementTool';
 
 const {
   MagnifyTool,
@@ -78,6 +79,7 @@ export function registerGlobalTools(): void {
     cornerstoneTools.addTool(PolylineLengthMeasurementTool); // 添加拆线长度测量工具
     cornerstoneTools.addTool(LineGrayscaleMeasurementTool); // 添加直线灰度测量工具
     cornerstoneTools.addTool(RectangleGrayscaleMeasurementTool); // 添加矩形区域灰度测量工具
+    cornerstoneTools.addTool(CobbAngleMeasurementTool);
     cornerstoneTools.addTool(DicomOverlayTool); // 添加DICOM四角信息显示工具
     cornerstoneTools.addTool(MaskTool); // 添加矩形ROI工具
 

+ 205 - 0
src/utils/measurementToolManager.ts

@@ -22,6 +22,7 @@ import PolygonLengthMeasurementTool from '@/components/measures/PolygonLengthMea
 import PolylineLengthMeasurementTool from '@/components/measures/PolylineLengthMeasurementTool';
 import LineGrayscaleMeasurementTool from '@/components/measures/LineGrayscaleMeasurementTool';
 import RectangleGrayscaleMeasurementTool from '@/components/measures/RectangleGrayscaleMeasurementTool';
+import CobbAngleMeasurementTool from '@/components/measures/CobbAngleMeasurementTool';
 import MaskTool from '@/components/tools/MaskTool';
 
 const {
@@ -3927,4 +3928,208 @@ viewport.render();
   static clearMaskAnnotationsForViewports(viewportIds: string[]): boolean[] {
     return viewportIds.map((viewportId) => this.clearMaskAnnotations(viewportId));
   }
+
+  // ==================== CobbAngleMeasurement(脊柱侧弯Cobb角)测量工具 ====================
+
+  /**
+   * 激活Cobb角测量工具
+   */
+  static activateCobbAngleMeasurementTool(viewportId: string): boolean {
+    const toolGroup = MeasurementToolManager.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.setToolPassive(HipDIMeasurementTool.toolName, { removeAllBindings: true });
+      toolGroup.setToolPassive(HipNHAAngleMeasurementTool.toolName, { removeAllBindings: true });
+      toolGroup.setToolPassive(VHSMeasurementTool.toolName, { removeAllBindings: true });
+      toolGroup.setToolPassive(TPLOMeasurementTool.toolName, { removeAllBindings: true });
+      toolGroup.setToolPassive(TTAMeasurementTool.toolName, { removeAllBindings: true });
+      toolGroup.setToolPassive(CBLOMeasurementTool.toolName, { removeAllBindings: true });
+      toolGroup.setToolPassive(HipCoverageMeasurementTool.toolName, { removeAllBindings: true });
+      toolGroup.setToolPassive(HipDorsalCoverageTool.toolName, { removeAllBindings: true });
+      toolGroup.setToolPassive(CircleCenterMeasurementTool.toolName, { removeAllBindings: true });
+      toolGroup.setToolPassive(MidlineMeasurementTool.toolName, { removeAllBindings: true });
+      toolGroup.setToolPassive(FindMidpointMeasurementTool.toolName, { removeAllBindings: true });
+      toolGroup.setToolPassive(VerticalTiltMeasurementTool.toolName, { removeAllBindings: true });
+      toolGroup.setToolPassive(HorizontalTiltMeasurementTool.toolName, { removeAllBindings: true });
+      toolGroup.setToolPassive(PolygonLengthMeasurementTool.toolName, { removeAllBindings: true });
+      toolGroup.setToolPassive(PolylineLengthMeasurementTool.toolName, { removeAllBindings: true });
+      toolGroup.setToolPassive(LineGrayscaleMeasurementTool.toolName, { removeAllBindings: true });
+      toolGroup.setToolPassive(RectangleGrayscaleMeasurementTool.toolName, { removeAllBindings: true });
+      toolGroup.setToolPassive(MaskTool.toolName, { removeAllBindings: true });
+
+      // 激活Cobb角测量工具
+      toolGroup.setToolActive(CobbAngleMeasurementTool.toolName, {
+        bindings: [{ mouseButton: MouseBindings.Primary }],
+      });
+
+      // 获取工具实例并激活修改模式
+      const toolInstance = toolGroup.getToolInstance(CobbAngleMeasurementTool.toolName) as CobbAngleMeasurementTool;
+      const viewport = cornerstone.getEnabledElementByViewportId(viewportId)?.viewport;
+      if (toolInstance && viewport.element) {
+        toolInstance._activateModify(viewport.element);
+      }
+
+      // 自动创建一个预设的注解
+      try {
+        if (viewport && viewport.element) {
+          const defaultAnnotation = CobbAngleMeasurementTool.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(CobbAngleMeasurementTool.toolName) as CobbAngleMeasurementTool;
+            if (toolInstance && '_updateCachedStats' in toolInstance) {
+              (toolInstance as any)._updateCachedStats(defaultAnnotation, enabledElement);
+            }
+          }
+
+          viewport.render();
+          console.log('[MeasurementToolManager] Default CobbAngle annotation created successfully');
+        }
+      } catch (error) {
+        console.error('[MeasurementToolManager] Failed to create default CobbAngle annotation:', error);
+      }
+
+      console.log(`[MeasurementToolManager] CobbAngle tool activated for viewport: ${viewportId}`);
+      return true;
+    } catch (error) {
+      console.error(`[MeasurementToolManager] Error activating CobbAngle tool:`, error);
+      return false;
+    }
+  }
+
+  /**
+   * 停用Cobb角测量工具
+   */
+  static deactivateCobbAngleMeasurementTool(viewportId: string): boolean {
+    const toolGroup = MeasurementToolManager.getToolGroup(viewportId);
+    if (!toolGroup) return false;
+
+    try {
+      toolGroup.setToolPassive(CobbAngleMeasurementTool.toolName, { removeAllBindings: true });
+      console.log(`[MeasurementToolManager] CobbAngle tool deactivated for viewport: ${viewportId}`);
+      return true;
+    } catch (error) {
+      console.error(`[MeasurementToolManager] Error deactivating CobbAngle tool:`, error);
+      return false;
+    }
+  }
+
+  /**
+   * 检查Cobb角测量工具是否处于激活状态
+   */
+  static isCobbAngleMeasurementToolActive(viewportId: string): boolean {
+    const toolGroup = MeasurementToolManager.getToolGroup(viewportId);
+    if (!toolGroup) return false;
+
+    try {
+      const activeTool = toolGroup.getActivePrimaryMouseButtonTool();
+      return activeTool === CobbAngleMeasurementTool.toolName;
+    } catch (error) {
+      console.error(`[MeasurementToolManager] Error checking CobbAngle tool state:`, error);
+      return false;
+    }
+  }
+
+  /**
+   * 切换Cobb角测量工具状态
+   */
+  static toggleCobbAngleMeasurementTool(viewportId: string): boolean {
+    const isActive = this.isCobbAngleMeasurementToolActive(viewportId);
+
+    if (isActive) {
+      return this.deactivateCobbAngleMeasurementTool(viewportId);
+    } else {
+      return this.activateCobbAngleMeasurementTool(viewportId);
+    }
+  }
+
+  /**
+   * 清除指定 viewport 的所有Cobb角测量标注
+   */
+  static clearCobbAngleMeasurements(viewportId: string): boolean {
+    try {
+      const viewport = cornerstone.getEnabledElementByViewportId(viewportId)?.viewport;
+      if (!viewport) return false;
+
+      const annotations = cornerstoneTools.annotation.state.getAnnotations(
+        CobbAngleMeasurementTool.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} CobbAngle measurements for viewport: ${viewportId}`);
+      return true;
+    } catch (error) {
+      console.error(`[MeasurementToolManager] Error clearing CobbAngle measurements:`, error);
+      return false;
+    }
+  }
+
+  /**
+   * 获取指定 viewport 的所有Cobb角测量结果
+   */
+  // eslint-disable-next-line
+  static getCobbAngleMeasurements(viewportId: string): any[] {
+    try {
+      const viewport = cornerstone.getEnabledElementByViewportId(viewportId)?.viewport;
+      if (!viewport) return [];
+
+      const annotations = cornerstoneTools.annotation.state.getAnnotations(
+        CobbAngleMeasurementTool.toolName,
+        viewport.element
+      );
+
+      return annotations.map((annotation) => ({
+        annotationUID: annotation.annotationUID,
+        cobbAngle: annotation.data?.cachedStats?.cobbAngle || 0,
+        unit: 'degrees',
+        points: annotation.data?.handles?.points || [],
+      }));
+    } catch (error) {
+      console.error(`[MeasurementToolManager] Error getting CobbAngle measurements:`, error);
+      return [];
+    }
+  }
+
+  /**
+   * 为多个 viewport 批量激活Cobb角测量工具
+   */
+  static activateCobbAngleMeasurementToolForViewports(viewportIds: string[]): boolean[] {
+    return viewportIds.map((viewportId) => this.activateCobbAngleMeasurementTool(viewportId));
+  }
+
+  /**
+   * 为多个 viewport 批量停用Cobb角测量工具
+   */
+  static deactivateCobbAngleMeasurementToolForViewports(viewportIds: string[]): boolean[] {
+    return viewportIds.map((viewportId) => this.deactivateCobbAngleMeasurementTool(viewportId));
+  }
+
+  /**
+   * 为多个 viewport 批量清除Cobb角测量
+   */
+  static clearCobbAngleMeasurementsForViewports(viewportIds: string[]): boolean[] {
+    return viewportIds.map((viewportId) => this.clearCobbAngleMeasurements(viewportId));
+  }
 }