Jelajahi Sumber

feat (1.72.0 -> 1.73.0): 实现文本注解工具条功能,支持选中注解的文本编辑、缩放和删除操作

- 增强TextAnnotationTool,支持配置文本框尺寸参数和导出TextAnnotation接口
- 新增TextAnnotationOperator类,提供注解的移动、缩放、旋转、文本设置等操作接口
- 新增TextAnnotationToolbar组件,实现文本输入框、缩放滑块和删除按钮
- 新增TextAnnotationToolbarOverlay覆盖层,监听注解选择事件并在底部显示工具条
- 更新堆栈图像查看器,集成工具条覆盖层组件
- 新增TextAnnotationOperatorExample使用示例文件

改动文件:
- src/components/measures/TextAnnotationTool.ts
- src/pages/view/components/viewers/stack.image.viewer.tsx
- src/utils/textAnnotationToolManager.ts
- src/components/measures/TextAnnotationToolbar.tsx
- src/components/measures/TextAnnotationToolbarOverlay.tsx
- src/utils/textAnnotationOperatorExample.ts
dengdx 1 hari lalu
induk
melakukan
e37216a85c

+ 19 - 0
CHANGELOG.md

@@ -2,6 +2,25 @@
 
 本项目的所有重要变更都将记录在此文件中.
 
+## [1.73.0] - 2026-01-16 12:50
+
+feat (1.72.0 -> 1.73.0): 实现文本注解工具条功能,支持选中注解的文本编辑、缩放和删除操作
+
+- 增强TextAnnotationTool,支持配置文本框尺寸参数和导出TextAnnotation接口
+- 新增TextAnnotationOperator类,提供注解的移动、缩放、旋转、文本设置等操作接口
+- 新增TextAnnotationToolbar组件,实现文本输入框、缩放滑块和删除按钮
+- 新增TextAnnotationToolbarOverlay覆盖层,监听注解选择事件并在底部显示工具条
+- 更新堆栈图像查看器,集成工具条覆盖层组件
+- 新增TextAnnotationOperatorExample使用示例文件
+
+改动文件:
+- src/components/measures/TextAnnotationTool.ts
+- src/pages/view/components/viewers/stack.image.viewer.tsx
+- src/utils/textAnnotationToolManager.ts
+- src/components/measures/TextAnnotationToolbar.tsx
+- src/components/measures/TextAnnotationToolbarOverlay.tsx
+- src/utils/textAnnotationOperatorExample.ts
+
 ## [1.72.0] - 2026-01-15 21:42
 
 feat (1.71.1 -> 1.72.0): 优化屏幕尺寸响应式布局,在xl屏幕尺寸下使用大屏组件提升用户体验

+ 1 - 1
package.json

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

+ 12 - 5
src/components/measures/TextAnnotationTool.ts

@@ -3,6 +3,7 @@ import {
   utilities as csUtils,
   Types as CoreTypes,
   getEnabledElement,
+  getRenderingEngine
 } from '@cornerstonejs/core';
 
 import {
@@ -42,7 +43,7 @@ import { style } from '@cornerstonejs/tools/dist/esm/stateManagement/annotation/
 const cursorUrl = 'url("data:image/svg+xml;base64,PHN2ZyBpZD0ibW92ZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB3aWR0aD0iNDgiIGhlaWdodD0iNDgiIHZpZXdCb3g9IjAgMCA0OCA0OCI+CiAgPGRlZnM+CiAgICA8c3R5bGU+CiAgICAgIC52aS1wcmltYXJ5IHsKICAgICAgICBmaWxsOiAjRkY2RTZFOwogICAgICB9CgogICAgICAudmktcHJpbWFyeSwgLnZpLWFjY2VudCB7CiAgICAgICAgZmlsbC1ydWxlOiBldmVub2RkOwogICAgICB9CgogICAgICAudmktYWNjZW50IHsKICAgICAgICBmaWxsOiAjMEMwMDU4OwogICAgICB9CiAgICA8L3N0eWxlPgogIDwvZGVmcz4KICA8cGF0aCBjbGFzcz0idmktcHJpbWFyeSIgZD0iTTM0LDMwbC0yLTIsMi0ySDI2djhsMi0yLDIsMi01LDVIMjRsLTUtNSwyLTItMiwyaDhWMTVsLTIsMi0yLTIsNS01aDFsNSw1LTIsMi0yLTJ2OGg4bC0yLTIsMi0yLDUsNXYxWiIvPgogIDxwYXRoIGNsYXNzPSJ2aS1hY2NlbnQiIGQ9Ik0xOSwyM3YzaDR2NGgzVjI2aDRWMjNIMjZWMTlIMjN2NEgxOVoiLz4KPC9zdmc+Cg==") 24 24, crosshair';
 
 // 文本注解数据接口
-interface TextAnnotation extends Types.Annotation {
+export interface TextAnnotation extends Types.Annotation {
   data: {
     handles: {
       points: CoreTypes.Point3[]; // 四个角点 [左上, 右上, 右下, 左下]
@@ -53,6 +54,8 @@ interface TextAnnotation extends Types.Annotation {
         width: number; // 文本框宽度
         height: number; // 文本框高度
         rotation?: number; // 旋转角度(度数)
+        originalWidth:number;//原始宽度
+        originalHeight:number;//原始高度
       };
       rotationHandle?: CoreTypes.Point3; // 旋转手柄位置
     };
@@ -126,7 +129,9 @@ export default class TextAnnotationTool extends AnnotationTool {
     element: HTMLDivElement,
     viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport,
     text: string,
-    position?: Point2
+    position?: Point2,
+    textBoxWidth:number=100,
+    textBoxHeight:number=40
   ): TextAnnotation {
     const enabledElement = getEnabledElement(element);
     if (!enabledElement) {
@@ -138,8 +143,8 @@ export default class TextAnnotationTool extends AnnotationTool {
     const { width, height } = canvas;
 
     // 创建默认文本框:宽100,高40
-    const textBoxWidth = 100;
-    const textBoxHeight = 40;
+    // const textBoxWidth = 100;
+    // const textBoxHeight = 40;
 
     // 如果提供了位置参数,使用该位置;否则使用图像中心
     let textBoxLeft: number;
@@ -198,6 +203,8 @@ export default class TextAnnotationTool extends AnnotationTool {
             text: text,
             width: textBoxWidth,
             height: textBoxHeight,
+            originalWidth:textBoxWidth,
+            originalHeight:textBoxHeight
           },
         },
         cachedStats: {},
@@ -526,7 +533,7 @@ export default class TextAnnotationTool extends AnnotationTool {
     );
 
     // 3️⃣ 计算旋转方向向量(单位向量) 减少90度是为了让0度时手柄在上方
-    const angleRad = (rotation-90) * Math.PI / 180;
+    const angleRad = (rotation - 90) * Math.PI / 180;
     const directionX = Math.cos(angleRad);
     const directionY = Math.sin(angleRad);
 

+ 165 - 0
src/components/measures/TextAnnotationToolbar.tsx

@@ -0,0 +1,165 @@
+import React, { useState, useEffect, useRef, CSSProperties } from 'react';
+import { Input, Slider, Button } from 'antd';
+import { DeleteOutlined } from '@ant-design/icons';
+import TextAnnotationTool, { TextAnnotation } from './TextAnnotationTool';
+import { Types as CoreTypes } from '@cornerstonejs/core';
+
+interface TextAnnotationToolbarProps {
+  annotation: TextAnnotation;
+  viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport;
+  onTextChange: (text: string) => void;
+  onScaleChange: (scale: number) => void;
+  onDelete: () => void;
+  position: { x: number; y: number };
+  onClose?: () => void;
+  className?: string | undefined;
+  style?: CSSProperties | undefined;
+}
+
+const TextAnnotationToolbar: React.FC<TextAnnotationToolbarProps> = ({
+  annotation,
+  viewport,
+  onTextChange,
+  onScaleChange,
+  onDelete,
+  position,
+  onClose
+}) => {
+  const [localText, setLocalText] = useState(annotation.data.handles.textData.text);
+  const [scale, setScale] = useState(1); // 默认缩放比例
+  const toolbarRef = useRef<HTMLDivElement>(null);
+
+  // 当annotation变化时更新本地状态
+  useEffect(() => {
+    setLocalText(annotation.data.handles.textData.text);
+  }, [annotation.data.handles.textData.text]);
+
+
+  // 处理文本输入
+  const handleTextChange = (value: string) => {
+    setLocalText(value);
+  };
+
+  // 处理文本输入确认(失去焦点或按Enter)
+  const handleTextConfirm = () => {
+    if (localText !== annotation.data.handles.textData.text) {
+      onTextChange(localText);
+    }
+  };
+
+  // 处理缩放变化
+  const handleScaleChange = (value: number) => {
+    setScale(value);
+    onScaleChange(value);
+  };
+
+  // 处理键盘事件
+  const handleKeyPress = (e: React.KeyboardEvent) => {
+    if (e.key === 'Enter') {
+      handleTextConfirm();
+      e.preventDefault();
+    } else if (e.key === 'Escape') {
+      setLocalText(annotation.data.handles.textData.text); // 恢复原始文本
+      onClose?.();
+      e.preventDefault();
+    }
+  };
+
+  return (
+    <div
+      ref={toolbarRef}
+      data-testid="text-annotation-toolbar"
+      style={{
+        position: 'absolute',
+        left: position.x,
+        top: position.y,
+        zIndex: 1000,
+        background: 'white',
+        border: '1px solid #d9d9d9',
+        borderRadius: '6px',
+        boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
+        padding: '8px 12px',
+        display: 'flex',
+        gap: '8px',
+        alignItems: 'center',
+        fontSize: '12px',
+        minWidth: '280px',
+        pointerEvents: 'auto'
+      }}
+    >
+      {/* 文本标签 */}
+      <span style={{ color: '#666', fontSize: '11px' }}>文本:</span>
+
+      {/* 文本输入框 */}
+      <Input
+        value={localText}
+        onChange={(e) => handleTextChange(e.target.value)}
+        onBlur={handleTextConfirm}
+        onPressEnter={handleTextConfirm}
+        onKeyDown={handleKeyPress}
+        style={{
+          width: 100,
+          fontSize: '12px',
+          borderRadius: '4px'
+        }}
+        size="small"
+        placeholder="输入文本"
+        autoFocus
+      />
+
+      {/* 缩放标签 */}
+      <span style={{ color: '#666', fontSize: '11px' }}>缩放:</span>
+
+      {/* 缩放滑杆 */}
+      <div style={{ display: 'flex', alignItems: 'center', gap: '4px', minWidth: '80px' }}>
+        <Slider
+          min={0.5}
+          max={3.0}
+          step={0.1}
+          value={scale}
+          onChange={handleScaleChange}
+          style={{ width: 60, margin: 0 }}
+          tooltip={{ formatter: (value) => `${value?.toFixed(1)}x` }}
+        />
+        <span style={{ fontSize: '11px', color: '#999', minWidth: '24px' }}>
+          {scale.toFixed(1)}x
+        </span>
+      </div>
+
+      {/* 删除按钮 */}
+      <Button
+        type="text"
+        danger
+        icon={<DeleteOutlined />}
+        onClick={onDelete}
+        size="small"
+        style={{
+          border: 'none',
+          padding: '2px 6px',
+          fontSize: '12px'
+        }}
+        title="删除注解"
+      />
+
+      {/* 关闭按钮(可选) */}
+      {onClose && (
+        <Button
+          type="text"
+          onClick={onClose}
+          size="small"
+          style={{
+            border: 'none',
+            padding: '2px 6px',
+            fontSize: '12px',
+            color: '#999'
+          }}
+          title="关闭工具条"
+        >
+          ×
+        </Button>
+      )}
+    </div>
+  );
+};
+
+export default TextAnnotationToolbar;

+ 98 - 0
src/components/measures/TextAnnotationToolbarOverlay.tsx

@@ -0,0 +1,98 @@
+import React, { useState, useEffect, CSSProperties } from 'react';
+import * as cornerstone from '@cornerstonejs/core';
+import * as cornerstoneTools from '@cornerstonejs/tools';
+import { annotation } from '@cornerstonejs/tools';
+import TextAnnotationToolbar from './TextAnnotationToolbar';
+import { TextAnnotation } from './TextAnnotationTool';
+import { TextAnnotationToolManager } from '@/utils/textAnnotationToolManager';
+
+interface TextAnnotationToolbarOverlayProps {
+  viewportId: string;
+  className?: string | undefined;
+  style?: CSSProperties | undefined;
+}
+
+const TextAnnotationToolbarOverlay: React.FC<TextAnnotationToolbarOverlayProps> = ({
+  viewportId,
+  className
+}) => {
+  const [selectedAnnotation, setSelectedAnnotation] = useState<TextAnnotation | null>(null);
+
+  useEffect(() => {
+    const handleAnnotationSelectionChanged = () => {
+      // 检查当前viewport中的文本注解选择状态
+      const element = document.getElementById(viewportId) as HTMLDivElement;
+      if (!element) return;
+
+      const annotations = cornerstoneTools.annotation.state.getAnnotations('TextAnnotationTool', element);
+      const selected = annotations.find((ann: any) => ann.isSelected) as TextAnnotation;
+      setSelectedAnnotation(selected || null);
+    };
+
+    // 监听注解渲染事件
+    const eventTarget = cornerstone.eventTarget;
+    eventTarget.addEventListener(
+      cornerstoneTools.Enums.Events.ANNOTATION_SELECTION_CHANGE,
+      handleAnnotationSelectionChanged
+    );
+
+    return () => {
+      eventTarget.removeEventListener(
+        cornerstoneTools.Enums.Events.ANNOTATION_SELECTION_CHANGE,
+        handleAnnotationSelectionChanged
+      );
+    };
+  }, [viewportId]);
+
+  // 处理文本更改
+  const handleTextAnnotationChange = (annotation: TextAnnotation, text: string) => {
+    const operator = TextAnnotationToolManager.createOperatorForAnnotation(viewportId, annotation);
+    operator.setText(text);
+  };
+
+  // 处理缩放更改
+  const handleTextAnnotationScale = (annotation: TextAnnotation, scale: number) => {
+    const operator = TextAnnotationToolManager.createOperatorForAnnotation(viewportId, annotation);
+    operator.scale(scale);
+  };
+
+  // 处理删除
+  const handleTextAnnotationDelete = (annotation: TextAnnotation) => {
+    const operator = TextAnnotationToolManager.createOperatorForAnnotation(viewportId, annotation);
+    operator.delete();
+    setSelectedAnnotation(null);
+  };
+
+  // 计算工具条位置(固定在底部居中)
+  const calculateToolbarPosition = () => {
+    const element = document.getElementById(viewportId);
+    if (!element) return { x: 0, y: 0 };
+
+    const rect = element.getBoundingClientRect();
+    return {
+      x: rect.width / 2 - 140, // 假设工具条宽度280px,居中显示
+      y: rect.height //- 120 // 距离底部80px
+    };
+  };
+
+  const toolbarPosition = calculateToolbarPosition();
+
+  return (
+    <>
+      {selectedAnnotation && (
+        <TextAnnotationToolbar
+          className={`${className} h-[120px]`}
+          annotation={selectedAnnotation}
+          viewport={cornerstone.getEnabledElementByViewportId(viewportId)?.viewport}
+          onTextChange={(text) => handleTextAnnotationChange(selectedAnnotation, text)}
+          onScaleChange={(scale) => handleTextAnnotationScale(selectedAnnotation, scale)}
+          onDelete={() => handleTextAnnotationDelete(selectedAnnotation)}
+          position={toolbarPosition}
+          onClose={() => setSelectedAnnotation(null)}
+        />
+      )}
+    </>
+  );
+};
+
+export default TextAnnotationToolbarOverlay;

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

@@ -41,6 +41,7 @@ import {
 } from '@/states/view/imageLoadingSlice';
 import PolygonLengthMeasurementTool from '@/components/measures/PolygonLengthMeasurementTool';
 import PolylineLengthMeasurementTool from '@/components/measures/PolylineLengthMeasurementTool';
+import TextAnnotationToolbarOverlay from '@/components/measures/TextAnnotationToolbarOverlay';
 import { PlaybackController } from '@/pages/view/components/playback/PlaybackController';
 import { FloatingPlaybackControls } from '@/pages/view/components/playback/FloatingPlaybackControls';
 import { getIpPort } from '@/API/config';
@@ -2122,63 +2123,70 @@ const StackViewer = ({
   }
 
   return (
-    <div
-      id={viewportId}
-      ref={elementRef}
-      onContextMenu={(e) => e.preventDefault()}
-      style={{
-        width: '100%',
-        height: '100%',
-        backgroundColor: '#000',
-        border: selected ? '2px solid blue' : '1px solid gray',
-        position: 'relative', // 让浮动控制条能够正确定位
-      }}
-    >
-      {/* 图像加载进度条 */}
-      {loadingInfo?.isLoading && (
-        <div
-          style={{
-            position: 'absolute',
-            top: '50%',
-            left: '50%',
-            transform: 'translate(-50%, -50%)',
-            zIndex: 1000,
-            textAlign: 'center',
-            backgroundColor: 'rgba(0, 0, 0, 0.7)',
-            padding: '20px',
-            borderRadius: '8px',
-          }}
-        >
-          <Progress
-            type="circle"
-            percent={undefined}
-            strokeColor="#1890ff"
-            trailColor="#333"
-            size={80}
-            strokeWidth={6}
-            format={() => ''}//暂时不显示百分比数字,目前不容易得到下载进度,后续再改进
-          />
-          <div style={{ marginTop: 8, color: '#fff', fontSize: 14 }}>
-            {loadingInfo.stage === 'preparing' ? '正在分析图像...' : '加载中...'}
+    <div className='flex flex-col h-full w-full'>
+      <div
+        id={viewportId}
+        ref={elementRef}
+        onContextMenu={(e) => e.preventDefault()}
+        className='flex-1'
+        data-testid={`stack-viewer-viewport-${viewportId}`}
+        style={{
+          width: '100%',
+          height: '100%',
+          backgroundColor: '#000',
+          border: selected ? '2px solid blue' : '1px solid gray',
+          //position: 'relative', // 让浮动控制条能够正确定位
+        }}
+      >
+        {/* 图像加载进度条 */}
+        {loadingInfo?.isLoading && (
+          <div
+            style={{
+              position: 'absolute',
+              top: '50%',
+              left: '50%',
+              transform: 'translate(-50%, -50%)',
+              zIndex: 1000,
+              textAlign: 'center',
+              backgroundColor: 'rgba(0, 0, 0, 0.7)',
+              padding: '20px',
+              borderRadius: '8px',
+            }}
+          >
+            <Progress
+              type="circle"
+              percent={undefined}
+              strokeColor="#1890ff"
+              trailColor="#333"
+              size={80}
+              strokeWidth={6}
+              format={() => ''}//暂时不显示百分比数字,目前不容易得到下载进度,后续再改进
+            />
+            <div style={{ marginTop: 8, color: '#fff', fontSize: 14 }}>
+              {loadingInfo.stage === 'preparing' ? '正在分析图像...' : '加载中...'}
+            </div>
           </div>
-        </div>
-      )}
-
-      {/* 播放控制器(逻辑组件,不渲染UI) */}
-      <PlaybackController
-        viewportId={viewportId}
-        renderingEngineId={renderingEngineId}
-        enabled={true}
-      />
-
-      {/* 浮动播放控制条(UI组件) */}
-      <FloatingPlaybackControls
-        viewportId={viewportId}
-        position="bottom"
-        autoHide={true}
-        showAdvancedControls={true}
-      />
+        )}
+
+        {/* 播放控制器(逻辑组件,不渲染UI) */}
+        <PlaybackController
+          viewportId={viewportId}
+          renderingEngineId={renderingEngineId}
+          enabled={true}
+        />
+
+        {/* 浮动播放控制条(UI组件) */}
+        <FloatingPlaybackControls
+          viewportId={viewportId}
+          position="bottom"
+          autoHide={true}
+          showAdvancedControls={true}
+        />
+      </div>
+            {/* 文本注解工具条覆盖层 */}
+      <TextAnnotationToolbarOverlay  className="absolute " viewportId={viewportId} />
     </div>
+
   );
 };
 

+ 107 - 0
src/utils/textAnnotationOperatorExample.ts

@@ -0,0 +1,107 @@
+import { TextAnnotationToolManager } from './textAnnotationToolManager';
+
+/**
+ * TextAnnotationOperator 使用示例
+ *
+ * 这个示例展示了如何基于选中的文本标记创建操作实例,
+ * 然后使用这个实例来操作选中的文本标记。
+ */
+export class TextAnnotationOperatorExample {
+
+  /**
+   * 示例:操作选中的文本注解
+   * @param viewportId 视口ID
+   */
+  static operateSelectedAnnotation(viewportId: string): void {
+    // 1. 基于选中的文本标记创建操作实例
+    const operator = TextAnnotationToolManager.createOperatorForSelectedAnnotation(viewportId);
+
+    if (!operator) {
+      console.log('没有找到选中的文本注解');
+      return;
+    }
+
+    console.log('创建了文本注解操作器');
+
+    // 2. 使用操作实例进行各种操作
+
+    // 获取当前信息
+    console.log('当前文本:', operator.getText());
+    console.log('当前边界:', operator.getBounds());
+    console.log('当前旋转角度:', operator.getRotation());
+
+    // 修改文本
+    operator.setText('修改后的文本内容');
+
+    // 移动注解 (向右移动50像素,向下移动30像素)
+    operator.move(50, 30);
+
+    // 缩放注解 (放大1.5倍)
+    operator.scale(1.5);
+
+    // 旋转注解 (旋转45度)
+    operator.rotate(45);
+
+    // 设置新位置 (canvas坐标)
+    operator.setPosition(200, 150);
+
+    console.log('操作完成');
+  }
+
+  /**
+   * 示例:批量操作多个选中的注解
+   * @param viewportIds 视口ID数组
+   */
+  static batchOperateSelectedAnnotations(viewportIds: string[]): void {
+    viewportIds.forEach(viewportId => {
+      const operator = TextAnnotationToolManager.createOperatorForSelectedAnnotation(viewportId);
+      if (operator) {
+        // 对每个选中的注解执行相同操作
+        operator.setText('批量修改的文本');
+        operator.move(10, 10);
+      }
+    });
+  }
+
+  /**
+   * 示例:条件操作
+   * @param viewportId 视口ID
+   */
+  static conditionalOperation(viewportId: string): void {
+    const operator = TextAnnotationToolManager.createOperatorForSelectedAnnotation(viewportId);
+
+    if (operator) {
+      const bounds = operator.getBounds();
+      if (bounds && bounds.width < 100) {
+        // 如果宽度小于100像素,则放大
+        operator.scale(1.5);
+      }
+
+      const text = operator.getText();
+      if (text.includes('临时')) {
+        // 如果文本包含"临时",则标记为删除
+        operator.setText(text.replace('临时', '永久'));
+      }
+    }
+  }
+
+  /**
+   * 示例:删除选中的注解
+   * @param viewportId 视口ID
+   */
+  static deleteSelectedAnnotation(viewportId: string): void {
+    const operator = TextAnnotationToolManager.createOperatorForSelectedAnnotation(viewportId);
+
+    if (operator) {
+      console.log('删除选中的文本注解');
+      operator.delete();
+    } else {
+      console.log('没有找到选中的文本注解');
+    }
+  }
+}
+
+// 使用示例:
+// 1. 首先在UI中选中一个文本注解
+// 2. 然后调用:
+TextAnnotationOperatorExample.operateSelectedAnnotation('viewport-1');

+ 303 - 1
src/utils/textAnnotationToolManager.ts

@@ -1,6 +1,7 @@
 import * as cornerstone from '@cornerstonejs/core';
 import * as cornerstoneTools from '@cornerstonejs/tools';
-import TextAnnotationTool from '@/components/measures/TextAnnotationTool';
+import TextAnnotationTool, { TextAnnotation } from '@/components/measures/TextAnnotationTool';
+import { calculateMidpoint, vectorSubtract, vectorAdd } from '@/components/measures/mathUtils';
 
 const { MouseBindings } = cornerstoneTools.Enums;
 
@@ -172,4 +173,305 @@ export class TextAnnotationToolManager {
       console.error('[TextAnnotationToolManager] Failed to create default TextAnnotation annotation:', error);
     }
   }
+
+  /**
+    * 创建用于操作指定注解的工具实例
+    * @param viewportId 视口ID
+    * @param annotation 要操作的注解
+    * @returns TextAnnotationOperator 实例
+    */
+   static createOperatorForAnnotation(viewportId: string, annotation: TextAnnotation): TextAnnotationOperator {
+     return new TextAnnotationOperator(viewportId, annotation);
+   }
+
+  /**
+    * 创建用于操作选中注解的工具实例
+    * @param viewportId 视口ID
+    * @returns TextAnnotationOperator 实例,如果没有选中的注解则返回 null
+    */
+   static createOperatorForSelectedAnnotation(viewportId: string): TextAnnotationOperator | null {
+    try {
+      const element = document.getElementById(viewportId) as HTMLDivElement;
+      if (!element) {
+        console.error(`[TextAnnotationToolManager] Element not found for viewport: ${viewportId}`);
+        return null;
+      }
+
+      const annotations = cornerstoneTools.annotation.state.getAnnotations(
+        TextAnnotationTool.toolName,
+        element
+      );
+      const selectedAnnotation = annotations.find((ann: any) => ann.isSelected) as TextAnnotation;
+      if (!selectedAnnotation) {
+        console.warn(`[TextAnnotationToolManager] No selected TextAnnotation found for viewport: ${viewportId}`);
+        return null;
+      }
+
+      return new TextAnnotationOperator(viewportId, selectedAnnotation);
+    } catch (error) {
+      console.error(`[TextAnnotationToolManager] Error creating operator for selected annotation:`, error);
+      return null;
+    }
+  }
+}
+
+/**
+ * 文本注解操作器类
+ * 提供对单个 TextAnnotation 的直接操作接口
+ */
+export class TextAnnotationOperator {
+  private viewportId: string;
+  private annotation: TextAnnotation;
+
+  constructor(viewportId: string, annotation: TextAnnotation) {
+    this.viewportId = viewportId;
+    this.annotation = annotation;
+  }
+
+  /**
+   * 移动注解
+   * @param deltaX X轴偏移量(canvas坐标)
+   * @param deltaY Y轴偏移量(canvas坐标)
+   */
+  move(deltaX: number, deltaY: number): boolean {
+    try {
+      const element = document.getElementById(this.viewportId) as HTMLDivElement;
+      if (!element) return false;
+
+      const enabledElement = cornerstone.getEnabledElement(element);
+      if (!enabledElement) return false;
+
+      // 计算新的中心位置
+      const points = this.annotation.data.handles.points;
+      const center = calculateMidpoint(points[0], points[2]);
+      const centerCanvas = enabledElement.viewport.worldToCanvas(center);
+      const newCenterCanvas: cornerstone.Types.Point2 = [
+        centerCanvas[0] + deltaX,
+        centerCanvas[1] + deltaY
+      ];
+      const newCenterWorld = enabledElement.viewport.canvasToWorld(newCenterCanvas);
+
+      // 计算所有点的偏移量
+      const offset = vectorSubtract(newCenterWorld, center);
+
+      // 移动所有点
+      for (let i = 0; i < points.length; i++) {
+        points[i] = vectorAdd(points[i], offset);
+      }
+
+      // 更新文本框位置
+      this.updateTextBoxFromPoints();
+
+      this.annotation.invalidated = true;
+      enabledElement.viewport.render();
+
+      return true;
+    } catch (error) {
+      console.error('[TextAnnotationOperator] Error moving annotation:', error);
+      return false;
+    }
+  }
+
+  /**
+   * 缩放注解
+   * @param scaleFactor 缩放因子(相对于当前尺寸)
+   */
+  scale(scaleFactor: number): boolean {
+    try {
+      const currentWidth = this.annotation.data.handles.textData.originalWidth;
+      const currentHeight = this.annotation.data.handles.textData.originalHeight;
+
+      // 计算新的尺寸
+      const newWidth = Math.max(50, currentWidth * scaleFactor);
+      const newHeight = Math.max(30, currentHeight * scaleFactor);
+
+      // 计算中心点
+      const centerX = this.annotation.data.handles.textData.position[0] + this.annotation.data.handles.textData.width / 2;
+      const centerY = this.annotation.data.handles.textData.position[1] + this.annotation.data.handles.textData.height / 2;
+
+      // 更新位置(保持中心点不变)
+      const newPosition: cornerstone.Types.Point2 = [
+        centerX - newWidth / 2,
+        centerY - newHeight / 2
+      ];
+
+      // 更新文本框数据
+      this.annotation.data.handles.textData.width = newWidth;
+      this.annotation.data.handles.textData.height = newHeight;
+      this.annotation.data.handles.textData.position = newPosition;
+
+      // 更新四个角点的位置
+      const element = document.getElementById(this.viewportId) as HTMLDivElement;
+      if (element) {
+        const enabledElement = cornerstone.getEnabledElement(element);
+        if (enabledElement) {
+          this.annotation.data.handles.points[0] = enabledElement.viewport.canvasToWorld([newPosition[0], newPosition[1]]);
+          this.annotation.data.handles.points[1] = enabledElement.viewport.canvasToWorld([newPosition[0] + newWidth, newPosition[1]]);
+          this.annotation.data.handles.points[2] = enabledElement.viewport.canvasToWorld([newPosition[0] + newWidth, newPosition[1] + newHeight]);
+          this.annotation.data.handles.points[3] = enabledElement.viewport.canvasToWorld([newPosition[0], newPosition[1] + newHeight]);
+        }
+      }
+
+      this.annotation.invalidated = true;
+      const viewport = cornerstone.getEnabledElementByViewportId(this.viewportId)?.viewport;
+      viewport?.render();
+
+      return true;
+    } catch (error) {
+      console.error('[TextAnnotationOperator] Error scaling annotation:', error);
+      return false;
+    }
+  }
+
+  /**
+   * 旋转注解
+   * @param angleDegrees 旋转角度(度数)
+   */
+  rotate(angleDegrees: number): boolean {
+    try {
+      // 更新旋转角度
+      const currentRotation = this.annotation.data.handles.textData.rotation || 0;
+      this.annotation.data.handles.textData.rotation = (currentRotation + angleDegrees) % 360;
+
+      this.annotation.invalidated = true;
+      const viewport = cornerstone.getEnabledElementByViewportId(this.viewportId)?.viewport;
+      viewport?.render();
+
+      return true;
+    } catch (error) {
+      console.error('[TextAnnotationOperator] Error rotating annotation:', error);
+      return false;
+    }
+  }
+
+  /**
+   * 设置文本内容
+   * @param text 新的文本内容
+   */
+  setText(text: string): boolean {
+    try {
+      this.annotation.data.handles.textData.text = text;
+      this.annotation.invalidated = true;
+
+      const viewport = cornerstone.getEnabledElementByViewportId(this.viewportId)?.viewport;
+      viewport?.render();
+
+      return true;
+    } catch (error) {
+      console.error('[TextAnnotationOperator] Error setting text:', error);
+      return false;
+    }
+  }
+
+  /**
+   * 设置位置
+   * @param x X坐标(canvas坐标)
+   * @param y Y坐标(canvas坐标)
+   */
+  setPosition(x: number, y: number): boolean {
+    try {
+      const element = document.getElementById(this.viewportId) as HTMLDivElement;
+      if (!element) return false;
+
+      const enabledElement = cornerstone.getEnabledElement(element);
+      if (!enabledElement) return false;
+
+      const width = this.annotation.data.handles.textData.width;
+      const height = this.annotation.data.handles.textData.height;
+
+      // 更新文本框位置
+      this.annotation.data.handles.textData.position = [x, y];
+
+      // 更新四个角点的位置
+      this.annotation.data.handles.points[0] = enabledElement.viewport.canvasToWorld([x, y]);
+      this.annotation.data.handles.points[1] = enabledElement.viewport.canvasToWorld([x + width, y]);
+      this.annotation.data.handles.points[2] = enabledElement.viewport.canvasToWorld([x + width, y + height]);
+      this.annotation.data.handles.points[3] = enabledElement.viewport.canvasToWorld([x, y + height]);
+
+      this.annotation.invalidated = true;
+      enabledElement.viewport.render();
+
+      return true;
+    } catch (error) {
+      console.error('[TextAnnotationOperator] Error setting position:', error);
+      return false;
+    }
+  }
+
+  /**
+   * 获取边界信息
+   */
+  getBounds(): { x: number; y: number; width: number; height: number } | null {
+    try {
+      return {
+        x: this.annotation.data.handles.textData.position[0],
+        y: this.annotation.data.handles.textData.position[1],
+        width: this.annotation.data.handles.textData.width,
+        height: this.annotation.data.handles.textData.height
+      };
+    } catch (error) {
+      console.error('[TextAnnotationOperator] Error getting bounds:', error);
+      return null;
+    }
+  }
+
+  /**
+   * 删除注解
+   */
+  delete(): boolean {
+    try {
+      if (this.annotation.annotationUID) {
+        cornerstoneTools.annotation.state.removeAnnotation(this.annotation.annotationUID);
+
+        const viewport = cornerstone.getEnabledElementByViewportId(this.viewportId)?.viewport;
+        viewport?.render();
+
+        return true;
+      }
+      return false;
+    } catch (error) {
+      console.error('[TextAnnotationOperator] Error deleting annotation:', error);
+      return false;
+    }
+  }
+
+  /**
+   * 获取当前文本内容
+   */
+  getText(): string {
+    return this.annotation.data.handles.textData.text;
+  }
+
+  /**
+   * 获取当前旋转角度
+   */
+  getRotation(): number {
+    return this.annotation.data.handles.textData.rotation || 0;
+  }
+
+  /**
+   * 更新文本框位置基于四个角点
+   */
+  private updateTextBoxFromPoints(): void {
+    const points = this.annotation.data.handles.points;
+    if (points.length < 4) return;
+
+    const element = document.getElementById(this.viewportId) as HTMLDivElement;
+    if (!element) return;
+
+    const enabledElement = cornerstone.getEnabledElement(element);
+    if (!enabledElement) return;
+
+    const canvasPoints = points.map(p => enabledElement.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]));
+
+    this.annotation.data.handles.textData.position = [minX, minY];
+    this.annotation.data.handles.textData.width = maxX - minX;
+    this.annotation.data.handles.textData.height = maxY - minY;
+  }
 }