Sfoglia il codice sorgente

feat (1.66.2 -> 1.67.0): 实现文本标记工具功能,支持在图像上添加、编辑、旋转和缩放文本标注

- 在 stack.image.viewer.tsx 中导入并注册 TextAnnotationTool 和 TextAnnotationToolManager
- 在 cornerstoneToolsSetup.ts 中注册全局文本标记工具
- 新增 TextAnnotationTool 组件,实现完整的文本标注功能包括拖拽、旋转、缩放等
- 新增 TextAnnotationToolManager 工具管理器,提供工具激活、停用和清理功能

改动文件:
- src/pages/view/components/viewers/stack.image.viewer.tsx
- src/utils/cornerstoneToolsSetup.ts
- src/components/measures/TextAnnotationTool.ts
- src/utils/textAnnotationToolManager.ts
dengdx 4 giorni fa
parent
commit
22a20d6f55

+ 1716 - 0
src/components/measures/TextAnnotationTool.ts

@@ -0,0 +1,1716 @@
+import { Point3, Point2, fromPoint3ToPoint2 } 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,
+  drawTextBox,
+  drawRect: drawRectSvg,
+  drawPath: drawPathSvg,
+  drawLine,
+} = drawing;
+
+import {
+  calculateMidpoint,
+  vectorSubtract,
+  vectorAdd,
+  fromPoint2ToPoint3,
+} from './mathUtils';
+import {
+  PublicToolProps,
+  ToolProps,
+  EventTypes,
+  SVGDrawingHelper,
+} from '@cornerstonejs/tools/dist/esm/types';
+import { s } from 'msw/lib/core/HttpResponse-CCdkF1fJ';
+import { style } from '@cornerstonejs/tools/dist/esm/stateManagement/annotation/config';
+// import { drawLine } from '@cornerstonejs/tools/dist/esm/drawingSvg';
+
+// 十字准星光标
+const cursorUrl = 'url("data:image/svg+xml;base64,PHN2ZyBpZD0ibW92ZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB3aWR0aD0iNDgiIGhlaWdodD0iNDgiIHZpZXdCb3g9IjAgMCA0OCA0OCI+CiAgPGRlZnM+CiAgICA8c3R5bGU+CiAgICAgIC52aS1wcmltYXJ5IHsKICAgICAgICBmaWxsOiAjRkY2RTZFOwogICAgICB9CgogICAgICAudmktcHJpbWFyeSwgLnZpLWFjY2VudCB7CiAgICAgICAgZmlsbC1ydWxlOiBldmVub2RkOwogICAgICB9CgogICAgICAudmktYWNjZW50IHsKICAgICAgICBmaWxsOiAjMEMwMDU4OwogICAgICB9CiAgICA8L3N0eWxlPgogIDwvZGVmcz4KICA8cGF0aCBjbGFzcz0idmktcHJpbWFyeSIgZD0iTTM0LDMwbC0yLTIsMi0ySDI2djhsMi0yLDIsMi01LDVIMjRsLTUtNSwyLTItMiwyaDhWMTVsLTIsMi0yLTIsNS01aDFsNSw1LTIsMi0yLTJ2OGg4bC0yLTIsMi0yLDUsNXYxWiIvPgogIDxwYXRoIGNsYXNzPSJ2aS1hY2NlbnQiIGQ9Ik0xOSwyM3YzaDR2NGgzVjI2aDRWMjNIMjZWMTlIMjN2NEgxOVoiLz4KPC9zdmc+Cg==") 24 24, crosshair';
+
+// 文本注解数据接口
+interface TextAnnotation extends Types.Annotation {
+  data: {
+    handles: {
+      points: CoreTypes.Point3[]; // 四个角点 [左上, 右上, 右下, 左下]
+      activeHandleIndex: number | null;
+      textData: {
+        position: CoreTypes.Point2; // 文本框左上角位置
+        text: string; // 文本内容
+        width: number; // 文本框宽度
+        height: number; // 文本框高度
+        rotation?: number; // 旋转角度(度数)
+      };
+      rotationHandle?: CoreTypes.Point3; // 旋转手柄位置
+    };
+    cachedStats?: {};
+  };
+  // 选中状态
+  isSelected?: boolean;
+  // 高亮状态
+  highlighted?: boolean;
+}
+
+// 导出的注解数据接口
+export interface ExportedTextAnnotationData {
+  points: CoreTypes.Point3[]; // 四个角点的世界坐标
+  text: string; // 文本内容
+  rotation?: number; // 旋转角度
+  metadata: {
+    viewPlaneNormal: CoreTypes.Point3;
+    viewUp: CoreTypes.Point3;
+    FrameOfReferenceUID: string;
+    referencedImageId: string;
+  };
+}
+
+export default class TextAnnotationTool extends AnnotationTool {
+  static toolName = 'TextAnnotationTool';
+
+  editData: {
+    annotation: Types.Annotation;
+    viewportIdsToRender: string[];
+    handleIndex?: number;
+    newAnnotation?: boolean;
+    hasMoved?: boolean;
+    // 是否拖拽文本框
+    movingTextBox?: boolean;
+    // 是否移动整个工具
+    movingWholeTool?: boolean;
+    // 是否正在旋转
+    rotatingText?: boolean;
+    // 移动文本框时的初始偏移量
+    textBoxOffset?: CoreTypes.Point2;
+    // 移动整个工具时的初始偏移量
+    wholeToolOffset?: CoreTypes.Point2;
+    // 保存拖拽开始时的原始点坐标
+    originalPoints?: CoreTypes.Point3[];
+    // 旋转开始时的角度
+    initialRotation?: number;
+    // 鼠标初始角度
+    initialMouseAngle?: number;
+    // 是否为选中状态
+    selected?: boolean;
+  } | null = null;
+
+  isDrawing: boolean = false;
+
+  /**
+   * 获取工具名称
+   */
+  getToolName(): string {
+    return TextAnnotationTool.toolName;
+  }
+
+  /**
+   * 创建一个预设的文本注解,带有默认文本框
+   * @param element HTML元素
+   * @param viewport Viewport实例
+   */
+  static createDefaultAnnotation(
+    element: HTMLDivElement,
+    viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport,
+    text: string
+  ): TextAnnotation {
+    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;
+
+    // 创建默认文本框:中心位置,宽100,高40
+    const textBoxWidth = 100;
+    const textBoxHeight = 40;
+    const textBoxLeft = centerX - textBoxWidth / 2;
+    const textBoxTop = centerY - textBoxHeight / 2;
+
+    // 创建四个角点的canvas坐标
+    const canvasPoints: CoreTypes.Point2[] = [
+      [textBoxLeft, textBoxTop], // 左上
+      [textBoxLeft + textBoxWidth, textBoxTop], // 右上
+      [textBoxLeft + textBoxWidth, textBoxTop + textBoxHeight], // 右下
+      [textBoxLeft, textBoxTop + textBoxHeight], // 左下
+    ];
+
+    // 转换为world坐标
+    const worldPoints = canvasPoints.map((canvasPoint) =>
+      viewport.canvasToWorld(canvasPoint)
+    );
+
+    const camera = viewport.getCamera();
+    const { viewPlaneNormal, viewUp } = camera;
+    if (viewPlaneNormal === undefined) {
+      throw new Error('viewPlaneNormal is undefined');
+    }
+    if (viewUp === undefined) {
+      throw new Error('viewUp is undefined');
+    }
+
+    const annotationData = {
+      invalidated: true,
+      highlighted: false,
+      metadata: {
+        viewPlaneNormal: [...viewPlaneNormal] as CoreTypes.Point3,
+        viewUp: [...viewUp] as CoreTypes.Point3,
+        FrameOfReferenceUID: viewport.getFrameOfReferenceUID(),
+        referencedImageId: viewport.getCurrentImageId?.() || '',
+        toolName: TextAnnotationTool.toolName,
+      },
+      data: {
+        handles: {
+          points: worldPoints,
+          activeHandleIndex: null,
+          textData: {
+            position: [textBoxLeft, textBoxTop],
+            text: text,
+            width: textBoxWidth,
+            height: textBoxHeight,
+          },
+        },
+        cachedStats: {},
+      },
+    } as TextAnnotation;
+
+    return annotationData;
+  }
+
+  /**
+   * 导出注解数据
+   * @param annotation 要导出的注解
+   * @returns 导出的数据对象
+   */
+  static exportAnnotationData(
+    annotation: TextAnnotation,
+    viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport
+  ): ExportedTextAnnotationData | null {
+    return {
+      points: [...annotation.data.handles.points],
+      text: annotation.data.handles.textData.text,
+      rotation: annotation.data.handles.textData.rotation,
+      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: ExportedTextAnnotationData,
+    element: HTMLDivElement,
+    viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport
+  ): TextAnnotation {
+    const enabledElement = getEnabledElement(element);
+    if (!enabledElement) {
+      throw new Error('Element is not enabled');
+    }
+
+    const annotationData = {
+      invalidated: true,
+      highlighted: false,
+      metadata: {
+        ...exportedData.metadata,
+        toolName: TextAnnotationTool.toolName,
+      },
+      data: {
+        handles: {
+          points: [...exportedData.points],
+          activeHandleIndex: null,
+          textData: {
+            position: [0, 0], // 将在render时计算
+            text: exportedData.text,
+            width: 100, // 默认值,将在render时根据points计算
+            height: 40,
+            rotation: exportedData.rotation || 0,
+          },
+        },
+        cachedStats: {},
+      },
+    } as TextAnnotation;
+
+    return annotationData;
+  }
+
+  constructor(
+    toolProps: PublicToolProps = {},
+    defaultToolProps: ToolProps = {
+      supportedInteractionTypes: ['Mouse', 'Touch'],
+      configuration: {
+        shadow: true,
+        preventHandleOutsideImage: false,
+      },
+    }
+  ) {
+    super(toolProps, defaultToolProps);
+  }
+
+  /**
+   * 添加新注解 - 禁用此功能,因为我们只使用预设的注解
+   */
+  addNewAnnotation(
+    evt: EventTypes.InteractionEventType,
+    interactionType: string
+  ): Types.Annotation {
+    // 不创建新注解,直接返回空对象
+    // 因为我们只使用预设的注解
+    evt.preventDefault();
+    return {} as Types.Annotation;
+  }
+
+  /**
+   * 检查点是否靠近工具
+   */
+  isPointNearTool(
+    element: HTMLDivElement,
+    annotation: Types.Annotation | TextAnnotation,
+    canvasCoords: CoreTypes.Point2,
+    proximity: number,
+    interactionType: string
+  ): boolean {
+    const enabledElement = getEnabledElement(element);
+    if (!enabledElement) {
+      return false;
+    }
+    const { viewport } = enabledElement;
+
+    const customAnn = annotation as TextAnnotation;
+    const handles = customAnn.data.handles;
+    if (!handles || !handles.points) {
+      return false;
+    }
+
+    const points = handles.points;
+
+    // 检查是否在文本框内
+    if (this._isPointInTextBox(canvasCoords, handles.textData)) {
+      return true;
+    }
+
+    // 检查是否靠近任意一个手柄点
+    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;
+      }
+    }
+
+    // 检查是否在线段上(文本框边框)
+    const edges = [
+      [points[0], points[1]], // 上边
+      [points[1], points[2]], // 右边
+      [points[2], points[3]], // 下边
+      [points[3], points[0]], // 左边
+    ];
+
+    for (const [p1, p2] of edges) {
+      const canvasP1 = viewport.worldToCanvas(p1);
+      const canvasP2 = viewport.worldToCanvas(p2);
+      const dist = this._distanceToSegment(canvasCoords, canvasP1, canvasP2);
+      if (dist < proximity) {
+        return true;
+      }
+    }
+
+    return false;
+  }
+
+  /**
+   * 检查点击是否在文本区域内(只检测文本中心的小区域,不检测整个矩形框)
+   */
+  private _isPointInTextBox(
+    canvasPoint: CoreTypes.Point2,
+    textBox: { position: CoreTypes.Point2; width: number; height: number }
+  ): boolean {
+    const [x, y] = canvasPoint;
+    const [tx, ty] = textBox.position;
+    const { width, height } = textBox;
+
+    // 计算文本中心位置
+    const centerX = tx + width / 2;
+    const centerY = ty + height / 2;
+
+    // 只检测文本中心的小区域(假设文本大小约为矩形的 60%)
+    const textWidth = width * 0.6;
+    const textHeight = height * 0.6;
+
+    const margin = 5;
+    return (
+      x >= centerX - textWidth / 2 - margin &&
+      x <= centerX + textWidth / 2 + margin &&
+      y >= centerY - textHeight / 2 - margin &&
+      y <= centerY + textHeight / 2 + margin
+    );
+  }
+
+  /**
+   * 计算点到线段的距离
+   */
+  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 _isPointNearLine(
+    canvasPoint: CoreTypes.Point2,
+    points: CoreTypes.Point3[],
+    viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport,
+    proximity: number = 6
+  ): boolean {
+    if (points.length < 4) {
+      return false;
+    }
+
+    const canvasPoints = points.map(p => viewport.worldToCanvas(p));
+
+    // 检查四条边
+    const edges = [
+      [canvasPoints[0], canvasPoints[1]], // 上边
+      [canvasPoints[1], canvasPoints[2]], // 右边
+      [canvasPoints[2], canvasPoints[3]], // 下边
+      [canvasPoints[3], canvasPoints[0]], // 左边
+    ];
+
+    for (const [start, end] of edges) {
+      const dist = this._distanceToSegment(canvasPoint, start, end);
+      if (dist < proximity) {
+        return true;
+      }
+    }
+
+    return false;
+  }
+
+  /**
+   * 更新文本框位置和尺寸基于四个角点
+   */
+  private _updateTextBoxFromPoints(annotation: TextAnnotation, viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport): void {
+    const points = annotation.data.handles.points;
+    if (points.length < 4) return;
+
+    const canvasPoints = points.map(p => viewport.worldToCanvas(p));
+
+    // 计算边界框
+    const minX = Math.min(...canvasPoints.map(p => p[0]));
+    const minY = Math.min(...canvasPoints.map(p => p[1]));
+    const maxX = Math.max(...canvasPoints.map(p => p[0]));
+    const maxY = Math.max(...canvasPoints.map(p => p[1]));
+
+    annotation.data.handles.textData.position = [minX, minY];
+    annotation.data.handles.textData.width = maxX - minX;
+    annotation.data.handles.textData.height = maxY - minY;
+  }
+
+  /**
+   * 计算鼠标相对于矩形中心的角度
+   */
+  private _calculateAngle(
+    mouseCanvas: CoreTypes.Point2,
+    centerCanvas: CoreTypes.Point2
+  ): number {
+    const dx = mouseCanvas[0] - centerCanvas[0];
+    const dy = mouseCanvas[1] - centerCanvas[1];
+    return Math.atan2(dy, dx) * (180 / Math.PI);
+  }
+
+  /**
+   * 获取旋转手柄的位置(跟随矩形旋转,始终在逻辑顶部边上方30像素)
+   * 使用向量计算:从矩形中心点沿旋转角度方向移动"高度/2 + 30"距离
+   */
+  private _getRotationHandlePosition(
+    annotation: TextAnnotation,
+    viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport
+  ): CoreTypes.Point2 | null {
+    const points = annotation.data.handles.points;
+    if (points.length < 4) return null;
+
+    const canvasPoints = points.map(p => viewport.worldToCanvas(p));
+    const rotation = annotation.data.handles.textData.rotation || 0;
+
+    // 1️⃣ 计算矩形中心点
+    const centerX = (canvasPoints[0][0] + canvasPoints[2][0]) / 2;
+    const centerY = (canvasPoints[0][1] + canvasPoints[2][1]) / 2;
+
+    // 2️⃣ 计算矩形实际高度(欧几里得距离)
+    const heightEdge = Math.sqrt(
+      Math.pow(canvasPoints[3][0] - canvasPoints[0][0], 2) +
+      Math.pow(canvasPoints[3][1] - canvasPoints[0][1], 2)
+    );
+
+    // 3️⃣ 计算旋转方向向量(单位向量) 减少90度是为了让0度时手柄在上方
+    const angleRad = (rotation-90) * Math.PI / 180;
+    const directionX = Math.cos(angleRad);
+    const directionY = Math.sin(angleRad);
+
+    // 4️⃣ 计算移动距离:矩形高度的一半 + 70像素
+    const moveDistance = heightEdge / 2 + 70;
+
+    // 5️⃣ 沿着旋转方向移动距离
+    const handleX = centerX + directionX * moveDistance;
+    const handleY = centerY + directionY * moveDistance;
+
+    return [handleX, handleY];
+  }
+
+  /**
+   * 检查点是否靠近旋转手柄
+   */
+  private _isPointNearRotationHandle(
+    canvasPoint: CoreTypes.Point2,
+    annotation: TextAnnotation,
+    viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport,
+    proximity: number = 15
+  ): boolean {
+    const rotationHandlePos = this._getRotationHandlePosition(annotation, viewport);
+    if (!rotationHandlePos) return false;
+
+    const distance = Math.sqrt(
+      Math.pow(canvasPoint[0] - rotationHandlePos[0], 2) +
+      Math.pow(canvasPoint[1] - rotationHandlePos[1], 2)
+    );
+
+    return distance < proximity;
+  }
+
+  /**
+   * 取消操作
+   */
+  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(
+          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
+    );
+  }
+
+  /**
+   * 键盘按下回调 - 用于删除功能
+   */
+  _keyDownCallback = (evt: KeyboardEvent): void => {
+    const element = evt.currentTarget as HTMLDivElement;
+
+    // 检查是否按下 Delete 或 Backspace 键
+    if (evt.key === 'Delete' || evt.key === 'Backspace') {
+      const enabledElement = getEnabledElement(element);
+      if (!enabledElement) {
+        return;
+      }
+
+      const annotations = annotation.state.getAnnotations(this.getToolName(), element);
+      if (!annotations || annotations.length === 0) {
+        return;
+      }
+
+      // 查找被选中的注解
+      const selectedAnnotations = annotations.filter(
+        (ann) => (ann as TextAnnotation).isSelected
+      );
+
+      if (selectedAnnotations.length === 0) {
+        return;
+      }
+
+      // 删除所有选中的注解
+      for (const ann of selectedAnnotations) {
+        annotation.state.removeAnnotation(ann.annotationUID!);
+      }
+
+      // 触发渲染更新
+      const viewportIdsToRender =
+        utilities.viewportFilters.getViewportIdsWithToolToRender(
+          element,
+          this.getToolName()
+        );
+
+      utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
+
+      evt.preventDefault();
+      evt.stopPropagation();
+    }
+  };
+
+  /**
+   * 鼠标移动回调 - 用于修改模式,处理悬停检测
+   * 实现功能1和2:鼠标悬停在文本上时光标为手型,悬停在线段或手柄上时光标为十字准星
+   */
+  _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) {
+      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 TextAnnotation;
+      if (customAnn.isSelected && this._isPointNearRotationHandle(canvasCoords, customAnn, viewport)) {
+        element.style.cursor = 'grab'; // 抓手光标表示可以旋转
+        customAnn.highlighted = true;
+        isHovering = true;
+        break;
+      }
+    }
+
+    // 检查是否悬停在文本框上
+    if (!isHovering) {
+      for (const ann of annotations) {
+        const customAnn = ann as TextAnnotation;
+        if (this._isPointInTextBox(canvasCoords, customAnn.data.handles.textData)) {
+          element.style.cursor = 'pointer'; // 手型光标
+          customAnn.highlighted = true;
+          isHovering = true;
+          break;
+        }
+      }
+    }
+
+    // 如果没有悬停在文本框上,检查是否悬停在手柄或线段上
+    if (!isHovering) {
+      for (const ann of annotations) {
+        const customAnn = ann as TextAnnotation;
+
+        // 检查是否靠近手柄
+        const handle = this.getHandleNearImagePoint(element, customAnn, canvasCoords, 6);
+        if (handle) {
+          element.style.cursor = cursorUrl; // 十字准星光标
+          customAnn.highlighted = true;
+          isHovering = true;
+          break;
+        }
+
+        // 使用专门的线段检测方法
+        if (this._isPointNearLine(canvasCoords, customAnn.data.handles.points, viewport, 6)) {
+          element.style.cursor = cursorUrl; // 十字准星光标
+          customAnn.highlighted = true;
+          isHovering = true;
+          break;
+        }
+      }
+    }
+
+    // 如果没有悬停在任何地方,重置高亮(但保留选中状态)
+    if (!isHovering) {
+      for (const ann of annotations) {
+        const customAnn = ann as TextAnnotation;
+        // 只有未选中的工具才重置 highlighted
+        // 已选中的工具保持 highlighted = true
+        if (!customAnn.isSelected) {
+          customAnn.highlighted = false;
+        }
+      }
+      element.style.cursor = 'default';
+    }
+
+    // 触发渲染以更新高亮状态
+    const viewportIdsToRender = utilities.viewportFilters.getViewportIdsWithToolToRender(
+      element,
+      this.getToolName()
+    );
+    utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
+  };
+
+  /**
+   * 鼠标按下回调 - 用于修改模式
+   */
+  _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 TextAnnotation;
+      if (this._isPointInTextBox(canvasCoords, customAnn.data.handles.textData)) {
+        // 点击在文本框上
+        const viewportIdsToRender =
+          utilities.viewportFilters.getViewportIdsWithToolToRender(
+            element,
+            this.getToolName()
+          );
+
+        // 计算鼠标点击位置相对于文本框左上角的偏移量
+        const textBoxOffset: CoreTypes.Point2 = [
+          canvasCoords[0] - customAnn.data.handles.textData.position[0],
+          canvasCoords[1] - customAnn.data.handles.textData.position[1],
+        ];
+
+        this.editData = {
+          annotation: customAnn,
+          viewportIdsToRender,
+          hasMoved: false,
+          movingTextBox: true,
+          textBoxOffset: textBoxOffset,
+        };
+
+        // 设置选中状态(不设置 highlighted,因为选中状态已经足够)
+        customAnn.isSelected = true;
+        utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
+
+        evt.preventDefault();
+        evt.stopPropagation();
+        return;
+      }
+    }
+
+    // 如果没有点击文本框,再查找最近的手柄
+    for (const ann of annotations) {
+      const customAnn = ann as TextAnnotation;
+      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,
+          originalPoints: [...customAnn.data.handles.points], // 保存拖拽开始时的原始点坐标
+        };
+
+        // 设置选中状态
+        customAnn.isSelected = true;
+
+        utilities.triggerAnnotationRenderForViewportIds(
+          viewportIdsToRender
+        );
+
+        evt.preventDefault();
+        evt.stopPropagation();
+        return;
+      }
+    }
+
+    // 其次检查是否点击在旋转手柄上(对于选中的注解)
+    for (const ann of annotations) {
+      const customAnn = ann as TextAnnotation;
+      if (customAnn.isSelected && this._isPointNearRotationHandle(canvasCoords, customAnn, viewport)) {
+        // 点击在旋转手柄上,开始旋转
+        const viewportIdsToRender =
+          utilities.viewportFilters.getViewportIdsWithToolToRender(
+            element,
+            this.getToolName()
+          );
+
+        // 计算矩形中心点
+        const points = customAnn.data.handles.points;
+        const center = calculateMidpoint(points[0], points[2]); // 左上和右下中点
+        const centerCanvas = viewport.worldToCanvas(center);
+
+        // 保存初始旋转角度
+        const initialRotation = customAnn.data.handles.textData.rotation || 0;
+        const initialMouseAngle = this._calculateAngle(canvasCoords, centerCanvas);
+
+        this.editData = {
+          annotation: customAnn,
+          viewportIdsToRender,
+          hasMoved: false,
+          rotatingText: true,
+          initialRotation: initialRotation,
+          initialMouseAngle: initialMouseAngle,
+        };
+
+        // 设置选中状态
+        customAnn.isSelected = true;
+
+        utilities.triggerAnnotationRenderForViewportIds(
+          viewportIdsToRender
+        );
+
+        evt.preventDefault();
+        evt.stopPropagation();
+        return;
+      }
+    }
+
+    // 如果没有找到手柄,检查是否点击在线段上(移动整个工具)
+    for (const ann of annotations) {
+      const customAnn = ann as TextAnnotation;
+      if (this.isPointNearTool(element, customAnn as Types.Annotation, canvasCoords, 10, 'mouse')) {
+        // 点击在线段上,开始移动整个工具
+        const viewportIdsToRender =
+          utilities.viewportFilters.getViewportIdsWithToolToRender(
+            element,
+            this.getToolName()
+          );
+
+        // 计算鼠标点击位置相对于工具中心的偏移量
+        const points = customAnn.data.handles.points;
+        const center = calculateMidpoint(points[0], points[2]); // 左上和右下中点
+        const centerCanvas = viewport.worldToCanvas(center);
+        const wholeToolOffset: CoreTypes.Point2 = [
+          canvasCoords[0] - centerCanvas[0],
+          canvasCoords[1] - centerCanvas[1],
+        ];
+
+        this.editData = {
+          annotation: customAnn,
+          viewportIdsToRender,
+          hasMoved: false,
+          movingWholeTool: true,
+          wholeToolOffset: wholeToolOffset,
+        };
+
+        // 设置选中状态
+        customAnn.isSelected = true;
+        customAnn.highlighted = true;
+
+        utilities.triggerAnnotationRenderForViewportIds(
+          viewportIdsToRender
+        );
+
+        evt.preventDefault();
+        evt.stopPropagation();
+        return;
+      }
+    }
+
+    // 如果没有点击在工具上,取消所有工具的选中状态
+    for (const ann of annotations) {
+      const customAnn = ann as TextAnnotation;
+      customAnn.isSelected = false;
+      customAnn.highlighted = false;
+    }
+
+    const viewportIdsToRender =
+      utilities.viewportFilters.getViewportIdsWithToolToRender(
+        element,
+        this.getToolName()
+      );
+
+    utilities.triggerAnnotationRenderForViewportIds(
+      viewportIdsToRender
+    );
+  };
+
+  /**
+   * 鼠标拖拽回调 - 用于修改模式
+   */
+  _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, movingTextBox, movingWholeTool } = this.editData;
+    const customAnn = ann as TextAnnotation;
+
+    // 如果正在拖拽文本框
+    if (movingTextBox && this.editData.textBoxOffset) {
+      // 从当前鼠标位置减去偏移量,得到文本框左上角应该在的位置
+      const newTextBoxPosition: CoreTypes.Point2 = [
+        canvasCoords[0] - this.editData.textBoxOffset[0],
+        canvasCoords[1] - this.editData.textBoxOffset[1],
+      ];
+
+      // 更新四个角点的位置
+      const { width, height } = customAnn.data.handles.textData;
+      customAnn.data.handles.points[0] = enabledElement.viewport.canvasToWorld([newTextBoxPosition[0], newTextBoxPosition[1]]);
+      customAnn.data.handles.points[1] = enabledElement.viewport.canvasToWorld([newTextBoxPosition[0] + width, newTextBoxPosition[1]]);
+      customAnn.data.handles.points[2] = enabledElement.viewport.canvasToWorld([newTextBoxPosition[0] + width, newTextBoxPosition[1] + height]);
+      customAnn.data.handles.points[3] = enabledElement.viewport.canvasToWorld([newTextBoxPosition[0], newTextBoxPosition[1] + height]);
+
+      customAnn.data.handles.textData.position = newTextBoxPosition;
+
+      this.editData.hasMoved = true;
+
+      // 触发渲染更新
+      utilities.triggerAnnotationRenderForViewportIds(
+        viewportIdsToRender
+      );
+
+      evt.preventDefault();
+      evt.stopPropagation();
+      return;
+    }
+
+    // 如果正在移动整个工具
+    if (movingWholeTool && this.editData.wholeToolOffset) {
+      // 计算新的中心位置
+      const newCenterCanvas: CoreTypes.Point2 = [
+        canvasCoords[0] - this.editData.wholeToolOffset[0],
+        canvasCoords[1] - this.editData.wholeToolOffset[1],
+      ];
+      const newCenterWorld = enabledElement.viewport.canvasToWorld(newCenterCanvas);
+
+      // 计算所有点的偏移量
+      const points = customAnn.data.handles.points;
+      const oldCenter = calculateMidpoint(points[0], points[2]);
+      const offset = vectorSubtract(newCenterWorld, oldCenter);
+
+      // 移动所有点
+      for (let i = 0; i < points.length; i++) {
+        points[i] = vectorAdd(points[i], offset);
+      }
+
+      // 更新文本框位置
+      this._updateTextBoxFromPoints(customAnn, enabledElement.viewport);
+
+      this.editData.hasMoved = true;
+
+      // 触发渲染更新
+      utilities.triggerAnnotationRenderForViewportIds(
+        viewportIdsToRender
+      );
+
+      evt.preventDefault();
+      evt.stopPropagation();
+      return;
+    }
+
+    // 如果正在旋转
+    if (this.editData.rotatingText && this.editData.initialRotation !== undefined && this.editData.initialMouseAngle !== undefined) {
+      // 计算矩形中心点
+      const points = customAnn.data.handles.points;
+      const center = calculateMidpoint(points[0], points[2]); // 左上和右下中点
+      const centerCanvas = enabledElement.viewport.worldToCanvas(center);
+
+      // 计算当前鼠标角度
+      const currentMouseAngle = this._calculateAngle(canvasCoords, centerCanvas);
+
+      // 计算角度差
+      const angleDelta = currentMouseAngle - this.editData.initialMouseAngle;
+
+      // 更新旋转角度
+      const newRotation = (this.editData.initialRotation + angleDelta) % 360;
+      customAnn.data.handles.textData.rotation = newRotation;
+
+      this.editData.hasMoved = true;
+
+      // 触发渲染更新
+      utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
+
+      evt.preventDefault();
+      evt.stopPropagation();
+      return;
+    }
+
+    // 处理单个手柄拖拽 - 实现矩形的自由伸缩(考虑旋转状态)
+    const worldPos = currentPoints.world;
+    const activeHandleIndex = customAnn.data.handles.activeHandleIndex;
+
+    if (activeHandleIndex !== null && activeHandleIndex >= 0 && activeHandleIndex < customAnn.data.handles.points.length && this.editData.originalPoints) {
+      // 获取当前旋转角度
+      const rotation = customAnn.data.handles.textData.rotation || 0;
+
+      // 根据被拖拽的角点索引,重新计算所有四个顶点的位置
+      // 保持矩形几何关系:直角和平行边
+      const originalPoints = this.editData.originalPoints;
+
+      // 计算矩形中心点
+      const center = calculateMidpoint(originalPoints[0], originalPoints[2]);
+      const centerCanvas = enabledElement.viewport.worldToCanvas(center);
+
+      // 将鼠标世界坐标转换为canvas坐标
+      const mouseCanvas = enabledElement.viewport.worldToCanvas(worldPos);
+
+      // 如果有旋转,需要将鼠标位置转换到未旋转的坐标系中进行计算
+      let effectiveMousePos = worldPos;
+      if (rotation !== 0) {
+        // 计算鼠标在旋转坐标系中的位置
+        const dx = mouseCanvas[0] - centerCanvas[0];
+        const dy = mouseCanvas[1] - centerCanvas[1];
+        const cos = Math.cos(-rotation * Math.PI / 180); // 反向旋转
+        const sin = Math.sin(-rotation * Math.PI / 180);
+
+        const unrotatedX = centerCanvas[0] + dx * cos - dy * sin;
+        const unrotatedY = centerCanvas[1] + dx * sin + dy * cos;
+
+        // 转换回世界坐标
+        effectiveMousePos = enabledElement.viewport.canvasToWorld([unrotatedX, unrotatedY]);
+      }
+
+      // 使用转换后的鼠标位置进行几何计算
+      const effectiveWorldPos = effectiveMousePos;
+
+      // 确定矩形的变换规则(在未旋转坐标系中进行)
+      switch (activeHandleIndex) {
+        case 0: // 拖拽左上角
+          customAnn.data.handles.points[0] = effectiveWorldPos;
+          // 固定右下角(2)
+          customAnn.data.handles.points[2] = [...originalPoints[2]] as CoreTypes.Point3;
+          // 更新右上角(1): x同右下角, y同左上角
+          customAnn.data.handles.points[1] = [originalPoints[2][0], effectiveWorldPos[1], effectiveWorldPos[2]] as CoreTypes.Point3;
+          // 更新左下角(3): x同左上角, y同右下角
+          customAnn.data.handles.points[3] = [effectiveWorldPos[0], originalPoints[2][1], effectiveWorldPos[2]] as CoreTypes.Point3;
+          break;
+
+        case 1: // 拖拽右上角
+          customAnn.data.handles.points[1] = effectiveWorldPos;
+          // 固定左下角(3)
+          customAnn.data.handles.points[3] = [...originalPoints[3]] as CoreTypes.Point3;
+          // 更新左上角(0): x同左下角, y同右上角
+          customAnn.data.handles.points[0] = [originalPoints[3][0], effectiveWorldPos[1], effectiveWorldPos[2]] as CoreTypes.Point3;
+          // 更新右下角(2): x同右上角, y同左下角
+          customAnn.data.handles.points[2] = [effectiveWorldPos[0], originalPoints[3][1], effectiveWorldPos[2]] as CoreTypes.Point3;
+          break;
+
+        case 2: // 拖拽右下角
+          customAnn.data.handles.points[2] = effectiveWorldPos;
+          // 固定左上角(0)
+          customAnn.data.handles.points[0] = [...originalPoints[0]] as CoreTypes.Point3;
+          // 更新右上角(1): x同右下角, y同左上角
+          customAnn.data.handles.points[1] = [effectiveWorldPos[0], originalPoints[0][1], effectiveWorldPos[2]] as CoreTypes.Point3;
+          // 更新左下角(3): x同左上角, y同右下角
+          customAnn.data.handles.points[3] = [originalPoints[0][0], effectiveWorldPos[1], effectiveWorldPos[2]] as CoreTypes.Point3;
+          break;
+
+        case 3: // 拖拽左下角
+          customAnn.data.handles.points[3] = effectiveWorldPos;
+          // 固定右上角(1)
+          customAnn.data.handles.points[1] = [...originalPoints[1]] as CoreTypes.Point3;
+          // 更新左上角(0): x同左下角, y同右上角
+          customAnn.data.handles.points[0] = [effectiveWorldPos[0], originalPoints[1][1], effectiveWorldPos[2]] as CoreTypes.Point3;
+          // 更新右下角(2): x同右上角, y同左下角
+          customAnn.data.handles.points[2] = [originalPoints[1][0], effectiveWorldPos[1], effectiveWorldPos[2]] as CoreTypes.Point3;
+          break;
+      }
+
+      this.editData.hasMoved = true;
+
+      utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
+      evt.preventDefault();
+      evt.stopPropagation();
+    }
+  };
+
+  /**
+   * 鼠标释放回调 - 用于修改模式
+   */
+  _mouseUpModifyCallback = (evt: EventTypes.InteractionEventType): void => {
+    if (!this.editData) {
+      return;
+    }
+
+    const { annotation: ann, hasMoved, movingWholeTool } = this.editData;
+    const customAnn = ann as TextAnnotation;
+
+    // 清除active状态
+    customAnn.data.handles.activeHandleIndex = null;
+
+    // 如果没有拖拽且点击在线段上(movingWholeTool为true),则进入选中状态
+    if (!hasMoved && movingWholeTool) {
+      customAnn.isSelected = true;
+      customAnn.highlighted = true; // 保持高亮以显示选中
+    }
+
+    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();
+  };
+
+  /**
+   * 处理鼠标拖拽
+   */
+  _dragCallback = (evt: EventTypes.InteractionEventType): void => {
+    this.isDrawing = true;
+    const eventDetail = evt.detail;
+    const { element } = eventDetail;
+
+    const enabledElement = getEnabledElement(element);
+    if (!enabledElement || !this.editData) {
+      return;
+    }
+
+    const { annotation, viewportIdsToRender } = this.editData;
+    const { data } = annotation as TextAnnotation;
+
+    if (annotation) {
+      const { currentPoints } = eventDetail;
+      const worldPos = currentPoints.world;
+
+      const points = data.handles.points;
+      const activeHandleIndex = data.handles.activeHandleIndex;
+
+      if (activeHandleIndex !== null && activeHandleIndex >= 0 && activeHandleIndex < points.length) {
+        // 更新手柄位置
+        points[activeHandleIndex] = worldPos;
+
+        this._updateTextBoxFromPoints(annotation as TextAnnotation, enabledElement.viewport);
+
+        this.editData!.hasMoved = true;
+      }
+
+      utilities.triggerAnnotationRenderForViewportIds(
+        viewportIdsToRender
+      );
+    }
+  };
+
+  /**
+   * 处理鼠标抬起/点击
+   */
+  _endCallback = (evt: EventTypes.InteractionEventType): void => {
+    const eventDetail = evt.detail;
+    const { element } = eventDetail;
+
+    if (!this.editData) {
+      return;
+    }
+
+    const { annotation, viewportIdsToRender, newAnnotation, hasMoved } =
+      this.editData;
+    const { data } = annotation as TextAnnotation;
+
+    if (newAnnotation && !hasMoved) {
+      // 添加新点
+      const points = data.handles.points;
+
+      if (points.length < 4) {
+        const { currentPoints } = eventDetail;
+        const worldPos = currentPoints.world;
+
+        // 添加下一个点
+        if (points.length < 4) {
+          points.push(worldPos);
+          data.handles.activeHandleIndex = points.length - 1;
+        }
+
+        if (points.length === 4) {
+          // 完成注解创建
+          this.isDrawing = false;
+          data.handles.activeHandleIndex = null;
+          annotation.highlighted = false;
+          this.editData.newAnnotation = false;
+        }
+
+        const enabledElement = getEnabledElement(element);
+        if (enabledElement) {
+          this._updateTextBoxFromPoints(
+            annotation as TextAnnotation,
+            enabledElement.viewport
+          );
+
+          const { renderingEngine } = enabledElement;
+          const newViewportIdsToRender =
+            utilities.viewportFilters.getViewportIdsWithToolToRender(
+              element,
+              this.getToolName()
+            );
+          utilities.triggerAnnotationRenderForViewportIds(
+            newViewportIdsToRender
+          );
+        }
+      }
+    } else if (hasMoved) {
+      // 拖拽结束
+      this.editData.hasMoved = false;
+    }
+  };
+
+  /**
+   * 获取靠近图像点的手柄
+   */
+  getHandleNearImagePoint(
+    element: HTMLDivElement,
+    annotation: Types.Annotation,
+    canvasCoords: CoreTypes.Point2,
+    proximity: number
+  ): Types.ToolHandle | undefined {
+    const enabledElement = getEnabledElement(element);
+    if (!enabledElement) {
+      return undefined;
+    }
+    const { viewport } = enabledElement;
+
+    const customAnn = annotation as TextAnnotation;
+    const points = customAnn.data.handles.points;
+
+    // 使用更大的proximity来更容易选中手柄
+    const handleProximity = Math.max(proximity, 15);
+
+    // 获取旋转角度
+    const rotation = customAnn.data.handles.textData.rotation || 0;
+
+    for (let i = 0; i < points.length; i++) {
+      const point = points[i];
+      let canvasPoint = viewport.worldToCanvas(point);
+
+      // 如果有旋转,计算旋转后的手柄位置
+      if (rotation !== 0) {
+        const canvasPoints = points.map(p => viewport.worldToCanvas(p));
+        const centerX = (canvasPoints[0][0] + canvasPoints[2][0]) / 2;
+        const centerY = (canvasPoints[0][1] + canvasPoints[2][1]) / 2;
+
+        const dx = canvasPoint[0] - centerX;
+        const dy = canvasPoint[1] - centerY;
+        const cos = Math.cos(rotation * Math.PI / 180);
+        const sin = Math.sin(rotation * Math.PI / 180);
+
+        canvasPoint = [
+          centerX + dx * cos - dy * sin,
+          centerY + dx * sin + dy * cos
+        ] as CoreTypes.Point2;
+      }
+
+      const distance = Math.sqrt(
+        Math.pow(canvasPoint[0] - canvasCoords[0], 2) +
+        Math.pow(canvasPoint[1] - canvasCoords[1], 2)
+      );
+
+      if (distance < handleProximity) {
+        // 设置activeHandleIndex以便拖拽
+        customAnn.data.handles.activeHandleIndex = i;
+        return {
+          worldPosition: point,
+        } as Types.ToolHandle;
+      }
+    }
+
+    // 如果没有找到手柄,重置activeHandleIndex
+    customAnn.data.handles.activeHandleIndex = null;
+    return undefined;
+  }
+
+  /**
+   * 处理注解选中
+   */
+  handleSelectedCallback(
+    evt: EventTypes.InteractionEventType,
+    annotation: Types.Annotation
+  ): void {
+    const eventDetail = evt.detail;
+    const { element } = eventDetail;
+
+    annotation.highlighted = true;
+
+    const viewportIdsToRender =
+      utilities.viewportFilters.getViewportIdsWithToolToRender(
+        element,
+        this.getToolName()
+      );
+
+    utilities.triggerAnnotationRenderForViewportIds(
+      viewportIdsToRender
+    );
+
+    evt.preventDefault();
+  }
+
+  /**
+   * 处理工具选中
+   */
+  toolSelectedCallback(
+    evt: EventTypes.InteractionEventType,
+    annotation: Types.Annotation
+  ): void {
+    // 实现工具选中逻辑
+  }
+
+  /**
+   * 渲染注解
+   */
+  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 TextAnnotation;
+      const { annotationUID, data } = annotation;
+      const points = data.handles.points;
+
+      if (points.length < 4) {
+        continue;
+      }
+
+      // 更新文本框位置和尺寸
+      this._updateTextBoxFromPoints(annotation, viewport);
+
+      // 转换所有点为 canvas 坐标
+      const canvasPoints = points.map((p) => viewport.worldToCanvas(p));
+
+      // 获取样式
+      const styleSpecifier: any = {
+        toolGroupId: this.toolGroupId,
+        toolName: this.getToolName(),
+        viewportId: viewport.id,
+        annotationUID: annotationUID,
+      };
+
+      const lineWidth = this.getStyle('lineWidth', styleSpecifier, annotation);
+      const color = this.getStyle('color', styleSpecifier, annotation);
+
+      // 绘制文本框边框 - 使用旋转后的手柄位置绘制
+      if (annotationUID && canvasPoints.length >= 4) {
+        const rectUID = `${annotationUID}-rect`;
+
+        // 获取旋转角度
+        const rotation = annotation.data.handles.textData.rotation || 0;
+
+        // 如果有旋转,计算旋转后的矩形路径
+        let rectanglePath = canvasPoints;
+        if (rotation !== 0) {
+          // 计算矩形中心点
+          const centerX = (canvasPoints[0][0] + canvasPoints[2][0]) / 2;
+          const centerY = (canvasPoints[0][1] + canvasPoints[2][1]) / 2;
+
+          // 对每个点应用旋转变换
+          rectanglePath = canvasPoints.map(point => {
+            const dx = point[0] - centerX;
+            const dy = point[1] - centerY;
+            const cos = Math.cos(rotation * Math.PI / 180);
+            const sin = Math.sin(rotation * Math.PI / 180);
+
+            return [
+              centerX + dx * cos - dy * sin,
+              centerY + dx * sin + dy * cos
+            ] as CoreTypes.Point2;
+          });
+        }
+
+        // 创建封闭的矩形路径:从第一个点开始,顺时针遍历所有点,最后回到起点
+        const closedRectanglePath = [...rectanglePath, rectanglePath[0]];
+
+        drawPathSvg(
+          svgDrawingHelper,
+          annotationUID,
+          rectUID,
+          closedRectanglePath,
+          {
+            color: 'rgb(0, 255, 0)',
+            width: (lineWidth as number) || 1,
+          }
+        );
+      }
+
+      // 直接在矩形左上角绘制文本,不使用 drawLinkedTextBox(它会画连线)
+      if (annotationUID && canvasPoints.length >= 4) {
+        const textUID = `${annotationUID}-text`;
+
+        // 计算矩形中心位置
+        // const centerX = (canvasPoints[0][0] + canvasPoints[2][0]) / 2;
+        // const centerY = (canvasPoints[0][1] + canvasPoints[2][1]) / 2;
+        const centerX = canvasPoints[0][0];//(canvasPoints[0][0] + canvasPoints[2][0]) / 2;
+        const centerY = canvasPoints[0][1];//(canvasPoints[0][1] + canvasPoints[2][1]) / 2;
+
+        // // 以下两行用于调试,查看文本左上角位置
+        //         drawLine(svgDrawingHelper, annotationUID, 'cx', [centerX - 10, centerY], [centerX + 10, centerY]);
+        //         drawLine(svgDrawingHelper, annotationUID, 'cy', [centerX, centerY - 10], [centerX, centerY + 10], { color: 'rgb(255, 0, 0)', style: 'dashed', lineWidth: '5px' });
+        // 2️⃣ 文本参数
+        const fontSize = 14;
+        const correctedX = centerX;
+        const correctedY = centerY;
+        const textContent = annotation.data.handles.textData.text.split('\n');
+        // 3️⃣ 绘制文本
+        drawTextBox(
+          svgDrawingHelper,
+          annotationUID,
+          textUID,
+          textContent,
+          [correctedX, correctedY],
+          {
+            fontSize,
+            fontFamily: 'Arial, sans-serif',
+            centerX: false,
+            //centerY: false,
+            // textAnchor: 'left',
+            // dominantBaseline: 'hanging',
+            padding: 0,//覆盖内部实现的padding 25
+            margin: 0,
+            color: color as string || 'rgb(0, 0, 0)',
+          }
+        );
+        //拉伸变形
+        const textGroup = svgDrawingHelper.getSvgNode(`${annotationUID}::text::${textUID}`);
+        if (textGroup) {
+          //计算scalex和scaley
+          const textEl = textGroup;
+          //先调整tspan
+          // 获取 textEl 的所有 tspan 子元素
+          const tspans = textEl.querySelectorAll('tspan');
+          // 遍历所有 tspan 元素,设置 dy 属性
+          tspans.forEach((tspan: SVGElement) => {
+            tspan.setAttribute('dy', '1em');
+          });
+
+          const bbox = textEl.getBBox();
+
+          const textWidth = bbox.width;
+          const textHeight = bbox.height;
+          const targetWidth = Math.abs(canvasPoints[0][0] - canvasPoints[2][0]);    // 矩形宽度(canvas/SVG 坐标)
+          const targetHeight = Math.abs(canvasPoints[0][1] - canvasPoints[2][1]);  // 矩形高度
+          const scaleX = targetWidth / textWidth;
+          const scaleY = targetHeight / textHeight;
+          // 以文本左上角为缩放中心,保持左上角位置不变
+          const cx = bbox.x;
+          const cy = bbox.y;
+          // 统一使用矩形中心作为旋转中心,确保所有元素同步旋转
+          const centerX = cx + targetWidth / 2;
+          const centerY = cy + targetHeight / 2;
+          // 获取旋转角度(度数)
+          const rotation = annotation.data.handles.textData.rotation || 0;
+          this.acculateTransform(textEl, scaleX, scaleY, cx, cy, rotation, centerX, centerY,);
+        }
+      }
+
+      // 绘制手柄点
+      if (annotationUID) {
+        const handleGroupUID = '0';
+        // 根据选中状态调整手柄半径:选中时为12,否则为8
+        // 如果悬停(highlighted)但未选中,则使用10
+        const isSelected = annotation.isSelected;
+        const isHighlighted = annotation.highlighted;
+        let handleRadius = 8;
+        if (isSelected) {
+          handleRadius = 12;
+        } else if (isHighlighted) {
+          handleRadius = 10;
+        }
+
+        // 获取旋转角度
+        const rotation = annotation.data.handles.textData.rotation || 0;
+
+        // 如果有旋转,计算旋转后的手柄位置
+        let rotatedHandlePoints = canvasPoints;
+        if (rotation !== 0) {
+          // 计算矩形中心点
+          const centerX = (canvasPoints[0][0] + canvasPoints[2][0]) / 2;
+          const centerY = (canvasPoints[0][1] + canvasPoints[2][1]) / 2;
+
+          // 对每个手柄点应用旋转变换
+          rotatedHandlePoints = canvasPoints.map(point => {
+            const dx = point[0] - centerX;
+            const dy = point[1] - centerY;
+            const cos = Math.cos(rotation * Math.PI / 180);
+            const sin = Math.sin(rotation * Math.PI / 180);
+
+            return [
+              centerX + dx * cos - dy * sin,
+              centerY + dx * sin + dy * cos
+            ] as CoreTypes.Point2;
+          });
+        }
+
+        drawHandles(
+          svgDrawingHelper,
+          annotationUID,
+          handleGroupUID,
+          rotatedHandlePoints,
+          {
+            color: 'rgb(255, 255, 255)',
+            handleRadius: handleRadius,
+          }
+        );
+      }
+
+      // 绘制旋转手柄(仅对选中的注解)
+      if (annotationUID && annotation.isSelected) {
+        const rotationHandlePos = this._getRotationHandlePosition(annotation, viewport);
+        if (rotationHandlePos) {
+          const rotationHandleGroupUID = 'rotation-handle';
+
+          drawHandles(
+            svgDrawingHelper,
+            annotationUID,
+            rotationHandleGroupUID,
+            [rotationHandlePos],
+            {
+              color: 'rgb(0, 150, 255)',
+              handleRadius: 8,
+            }
+          );
+        }
+      }
+
+      renderStatus = true;
+    }
+
+    return renderStatus;
+  };
+
+  private acculateTransform(
+    textEl: SVGGElement,
+    scaleX: number,
+    scaleY: number,
+    scaleCenterX: number,    // 缩放中心X
+    scaleCenterY: number,    // 缩放中心Y
+    rotation: number = 0,
+    rotationCenterX?: number, // 旋转中心X(可选,默认使用缩放中心)
+    rotationCenterY?: number  // 旋转中心Y(可选,默认使用缩放中心)
+  ): void {
+    const svg = textEl.ownerSVGElement!;
+    const baseCTM = textEl.getCTM();
+
+    if (!baseCTM) {
+      return;
+    }
+
+    // 如果没有指定旋转中心,使用缩放中心
+    const rotCenterX = rotationCenterX ?? scaleCenterX;
+    const rotCenterY = rotationCenterY ?? scaleCenterY;
+
+    // 1. 构造变换矩阵(顺序:先旋转,再缩放)
+    const transformMatrix = svg
+      .createSVGMatrix()
+      .translate(rotCenterX, rotCenterY)     // 移动到旋转中心
+      .rotate(rotation)                      // 应用旋转
+      .translate(-rotCenterX, -rotCenterY)  // 移回原位
+      .translate(scaleCenterX, scaleCenterY) // 移动到缩放中心
+      .scaleNonUniform(scaleX, scaleY)       // 应用缩放
+      .translate(-scaleCenterX, -scaleCenterY); // 移回原位
+
+    // 2. 合成最终矩阵
+    const finalMatrix = baseCTM.multiply(transformMatrix);
+
+    // 3. 清空 transform(避免叠加)
+    textEl.transform.baseVal.clear();
+
+    // 4. 设置最终矩阵
+    const finalTransform = svg.createSVGTransform();
+    finalTransform.setMatrix(finalMatrix);
+
+    textEl.transform.baseVal.appendItem(finalTransform);
+  }
+}

+ 5 - 19
src/pages/view/components/viewers/stack.image.viewer.tsx

@@ -6,6 +6,7 @@ import { annotation, SplineROITool } from '@cornerstonejs/tools';
 import { eventTarget } from '@cornerstonejs/core';
 import { registerGlobalTools } from '@/utils/cornerstoneToolsSetup';
 import { MeasurementToolManager } from '@/utils/measurementToolManager';
+import { TextAnnotationToolManager } from '@/utils/textAnnotationToolManager';
 import TibialPlateauAngleTool from '@/components/measures/TibialPlateauAngleTool';
 import DARAMeasurementTool from '@/components/measures/DARAMeasurementTool';
 import HipDIMeasurementTool from '@/components/measures/HipDIMeasurementTool';
@@ -21,6 +22,7 @@ import MidlineMeasurementTool from '@/components/measures/MidlineMeasurementTool
 import FindMidpointMeasurementTool from '@/components/measures/FindMidpointMeasurementTool';
 import VerticalTiltMeasurementTool from '@/components/measures/VerticalTiltMeasurementTool';
 import HorizontalTiltMeasurementTool from '@/components/measures/HorizontalTiltMeasurementTool';
+import TextAnnotationTool from '@/components/measures/TextAnnotationTool';
 import LineGrayscaleMeasurementTool from '@/components/measures/LineGrayscaleMeasurementTool';
 import RectangleGrayscaleMeasurementTool from '@/components/measures/RectangleGrayscaleMeasurementTool';
 import MaskTool from '@/components/tools/MaskTool';
@@ -262,6 +264,7 @@ function registerTools(viewportId, renderingEngineId) {
   toolGroup.addTool(LineGrayscaleMeasurementTool.toolName); // 添加直线灰度测量工具
   toolGroup.addTool(RectangleGrayscaleMeasurementTool.toolName); // 添加矩形区域灰度测量工具
   toolGroup.addTool(MaskTool.toolName); // 添加掩码工具
+  toolGroup.addTool(TextAnnotationTool.toolName);
 
   // 设置默认工具状态
   setupDefaultToolStates(toolGroup);
@@ -369,25 +372,8 @@ function setupDefaultToolStates(toolGroup: cornerstoneTools.Types.IToolGroup) {
   toolGroup.setToolPassive(MaskTool.toolName);
 }
 export function addLMark(currentViewportId: string): void {
-  // Implement the logic to add an L mark
-  console.log('Adding L Mark viewport id : ', currentViewportId);
-  const toolGroup = getToolgroupByViewportId(currentViewportId);
-  // currentViewportId = viewportId;
-  toolGroup.setToolActive(LabelTool.toolName, {
-    bindings: [
-      // {
-      //   mouseButton: MouseBindings.Primary, // Left Click
-      // },
-    ],
-  });
-  const position: Types.Point3 = [100, 100, 0]; // Example position
-  const text = 'L'; // Predefined text
-  LabelTool.hydrate(currentViewportId, position, text);
-  toolGroup.setToolPassive(LabelTool.toolName, {
-    removeAllBindings: true,
-  });
-  // const enabledElement = cornerstone.getEnabledElementByViewportId(currentViewportId);
-  // cursors.elementCursor.resetElementCursor(elementRef.current as HTMLDivElement);
+  // 使用专门的文本标记工具管理器
+  TextAnnotationToolManager.activateTextAnnotationTool(currentViewportId, 'L');
 }
 export function addRLabel(currentViewportId: string): void {
   console.log('Adding R Mark viewport id : ', currentViewportId);

+ 3 - 0
src/utils/cornerstoneToolsSetup.ts

@@ -14,6 +14,7 @@ import MidlineMeasurementTool from '@/components/measures/MidlineMeasurementTool
 import FindMidpointMeasurementTool from '@/components/measures/FindMidpointMeasurementTool';
 import VerticalTiltMeasurementTool from '@/components/measures/VerticalTiltMeasurementTool';
 import HorizontalTiltMeasurementTool from '@/components/measures/HorizontalTiltMeasurementTool';
+import TextAnnotationTool from '@/components/measures/TextAnnotationTool';
 import PolygonLengthMeasurementTool from '@/components/measures/PolygonLengthMeasurementTool';
 import DicomOverlayTool from '@/components/overlay/DicomOverlayTool';
 import PolylineLengthMeasurementTool from '@/components/measures/PolylineLengthMeasurementTool';
@@ -74,12 +75,14 @@ export function registerGlobalTools(): void {
     cornerstoneTools.addTool(FindMidpointMeasurementTool); // 添加找中点测量工具
     cornerstoneTools.addTool(VerticalTiltMeasurementTool); // 添加直线垂直倾斜度测量工具
     cornerstoneTools.addTool(HorizontalTiltMeasurementTool); // 添加直线水平倾斜度测量工具
+    cornerstoneTools.addTool(TextAnnotationTool); // 添加文本标记工具
     cornerstoneTools.addTool(PolygonLengthMeasurementTool); // 添加多边形长度测量工具
     cornerstoneTools.addTool(PolylineLengthMeasurementTool); // 添加拆线长度测量工具
     cornerstoneTools.addTool(LineGrayscaleMeasurementTool); // 添加直线灰度测量工具
     cornerstoneTools.addTool(RectangleGrayscaleMeasurementTool); // 添加矩形区域灰度测量工具
     cornerstoneTools.addTool(DicomOverlayTool); // 添加DICOM四角信息显示工具
     cornerstoneTools.addTool(MaskTool); // 添加矩形ROI工具
+    cornerstoneTools.addTool(TextAnnotationTool); // 添加文本标记工具
 
     toolsRegistered = true;
     console.log('[cornerstoneToolsSetup] All tools registered successfully');

+ 168 - 0
src/utils/textAnnotationToolManager.ts

@@ -0,0 +1,168 @@
+import * as cornerstone from '@cornerstonejs/core';
+import * as cornerstoneTools from '@cornerstonejs/tools';
+import TextAnnotationTool from '@/components/measures/TextAnnotationTool';
+
+const { MouseBindings } = cornerstoneTools.Enums;
+
+/**
+ * 文本标记工具管理器
+ * 专门管理文本标记工具的激活、停用等操作
+ */
+export class TextAnnotationToolManager {
+  /**
+   * 激活文本标记工具
+   */
+  static activateTextAnnotationTool(viewportId: string,text:string): boolean {
+    try {
+      // 获取工具组
+      const toolGroup = this.getToolGroup(viewportId);
+      if (!toolGroup) {
+        console.error(`[TextAnnotationToolManager] Tool group not found for viewport: ${viewportId}`);
+        return false;
+      }
+
+      // 停用其他可能冲突的工具
+      toolGroup.setToolPassive('MagnifyTool', { removeAllBindings: true });
+      toolGroup.setToolPassive('PanTool', { removeAllBindings: true });
+      toolGroup.setToolPassive('WindowLevelTool', { removeAllBindings: true });
+      toolGroup.setToolPassive('ZoomTool', { removeAllBindings: true });
+      toolGroup.setToolPassive('LengthTool', { removeAllBindings: true });
+      toolGroup.setToolPassive('AngleTool', { removeAllBindings: true });
+      toolGroup.setToolPassive('StackScrollTool', { removeAllBindings: true });
+      toolGroup.setToolPassive('PlanarRotateTool', { removeAllBindings: true });
+
+      // 激活文本标记工具
+      toolGroup.setToolActive(TextAnnotationTool.toolName, {
+        bindings: [{ mouseButton: MouseBindings.Primary }],
+      });
+
+      // 获取工具实例并激活修改模式
+      const toolInstance = toolGroup.getToolInstance(TextAnnotationTool.toolName) as TextAnnotationTool;
+      const viewport = cornerstone.getEnabledElementByViewportId(viewportId)?.viewport;
+      if (toolInstance && viewport.element) {
+        toolInstance._activateModify(viewport.element);
+      }
+
+      // 自动创建一个预设的注解
+      this.createDefaultAnnotation(viewportId, text);
+
+      console.log(`[TextAnnotationToolManager] TextAnnotationTool activated for viewport: ${viewportId}`);
+      return true;
+    } catch (error) {
+      console.error(`[TextAnnotationToolManager] Error activating TextAnnotationTool:`, error);
+      return false;
+    }
+  }
+
+  /**
+   * 停用文本标记工具
+   */
+  static deactivateTextAnnotationTool(viewportId: string): boolean {
+    try {
+      const toolGroup = this.getToolGroup(viewportId);
+      if (!toolGroup) return false;
+
+      toolGroup.setToolPassive(TextAnnotationTool.toolName, { removeAllBindings: true });
+      console.log(`[TextAnnotationToolManager] TextAnnotationTool deactivated for viewport: ${viewportId}`);
+      return true;
+    } catch (error) {
+      console.error(`[TextAnnotationToolManager] Error deactivating TextAnnotationTool:`, error);
+      return false;
+    }
+  }
+
+  /**
+   * 清除指定 viewport 的所有文本标记标注
+   */
+  static clearTextAnnotations(viewportId: string): boolean {
+    try {
+      const viewport = cornerstone.getEnabledElementByViewportId(viewportId)?.viewport;
+      if (!viewport) return false;
+
+      const annotations = cornerstoneTools.annotation.state.getAnnotations(
+        TextAnnotationTool.toolName,
+        viewport.element
+      );
+
+      let removedCount = 0;
+      annotations.forEach((annotation) => {
+        if (annotation.annotationUID) {
+          cornerstoneTools.annotation.state.removeAnnotation(annotation.annotationUID);
+          removedCount++;
+        }
+      });
+
+      viewport.render();
+      console.log(`[TextAnnotationToolManager] Cleared ${removedCount} TextAnnotation annotations for viewport: ${viewportId}`);
+      return true;
+    } catch (error) {
+      console.error(`[TextAnnotationToolManager] Error clearing TextAnnotation annotations:`, error);
+      return false;
+    }
+  }
+
+  /**
+   * 为多个 viewport 批量清除文本标记标注
+   */
+  static clearTextAnnotationsForViewports(viewportIds: string[]): boolean[] {
+    return viewportIds.map((viewportId) => this.clearTextAnnotations(viewportId));
+  }
+
+  /**
+   * 获取工具组
+   */
+  private static getToolGroup(viewportId: string): cornerstoneTools.Types.IToolGroup | undefined {
+    const toolGroupId = `STACK_TOOL_GROUP_ID_${viewportId}`;
+    const toolGroup = cornerstoneTools.ToolGroupManager.getToolGroup(toolGroupId);
+
+    if (!toolGroup) {
+      console.warn(`[TextAnnotationToolManager] Tool group not found for viewport: ${viewportId}`);
+    }
+
+    return toolGroup;
+  }
+
+  /**
+   * 创建默认注解
+   */
+  private static createDefaultAnnotation(viewportId: string,text:string): void {
+    try {
+      const toolGroup = this.getToolGroup(viewportId);
+      if (!toolGroup) return;
+      const viewport = cornerstone.getEnabledElementByViewportId(viewportId)?.viewport;
+      if (!viewport) return;
+
+      const defaultAnnotation = TextAnnotationTool.createDefaultAnnotation(
+        viewport.element as HTMLDivElement,
+        viewport as cornerstone.Types.IStackViewport,
+        text
+      );
+
+      cornerstoneTools.annotation.state.addAnnotation(defaultAnnotation, viewport.element);
+
+      // // 文本框位置会在 renderAnnotation 时自动更新
+
+      // const viewportIdsToRender = cornerstoneTools.utilities.viewportFilters.getViewportIdsWithToolToRender(
+      //   viewport.element,
+      //   TextAnnotationTool.toolName
+      // );
+
+      // cornerstoneTools.utilities.triggerAnnotationRenderForViewportIds(
+      //   viewportIdsToRender
+      // );
+      const enabledElement = cornerstone.getEnabledElement(viewport.element);
+      if (enabledElement) {
+        const toolInstance = toolGroup.getToolInstance(TextAnnotationTool.toolName) as TextAnnotationTool;
+        if (toolInstance && '_updateCachedStats' in toolInstance) {
+          (toolInstance as any)._updateCachedStats(defaultAnnotation, enabledElement);
+        }
+      }
+
+      viewport.render();
+
+      console.log('[TextAnnotationToolManager] Default TextAnnotation annotation created successfully');
+    } catch (error) {
+      console.error('[TextAnnotationToolManager] Failed to create default TextAnnotation annotation:', error);
+    }
+  }
+}