Просмотр исходного кода

添加折线测量工具和多边形测量工具

dengdx 2 месяцев назад
Родитель
Сommit
f66ba7893e

+ 1473 - 0
src/components/measures/PolygonLengthMeasurementTool.ts

@@ -0,0 +1,1473 @@
+import { Point3, Point2, fromPoint3ToPoint2, decomposeVectorOnAxes } from './mathUtils';
+import {
+  utilities as csUtils,
+  Types as CoreTypes,
+  getEnabledElement,
+} from '@cornerstonejs/core';
+
+import {
+  AnnotationTool,
+  utilities,
+  Types,
+  annotation,
+  drawing,
+} from '@cornerstonejs/tools';
+
+const {
+  drawHandles,
+  drawLinkedTextBox,
+  drawLine: drawLineSvg,
+  drawCircle: drawCircleSvg,
+  drawPath: drawPathSvg,
+} = drawing;
+
+import {
+  calculateMidpoint,
+  vectorSubtract,
+  vectorAdd,
+  vectorScale,
+  getAngleBetweenLines,
+  degrees,
+  getLineIntersection,
+  fromPoint2ToPoint3,
+} from './mathUtils';
+import {
+  PublicToolProps,
+  ToolProps,
+  EventTypes,
+  SVGDrawingHelper,
+} from '@cornerstonejs/tools/dist/esm/types';
+
+// 十字准星光标
+const cursorUrl = 'url("data:image/svg+xml;base64,PHN2ZyBpZD0ibW92ZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB3aWR0aD0iNDgiIGhlaWdodD0iNDgiIHZpZXdCb3g9IjAgMCA0OCA0OCI+CiAgPGRlZnM+CiAgICA8c3R5bGU+CiAgICAgIC52aS1wcmltYXJ5IHsKICAgICAgICBmaWxsOiAjRkY2RTZFOwogICAgICB9CgogICAgICAudmktcHJpbWFyeSwgLnZpLWFjY2VudCB7CiAgICAgICAgZmlsbC1ydWxlOiBldmVub2RkOwogICAgICB9CgogICAgICAudmktYWNjZW50IHsKICAgICAgICBmaWxsOiAjMEMwMDU4OwogICAgICB9CiAgICA8L3N0eWxlPgogIDwvZGVmcz4KICA8cGF0aCBjbGFzcz0idmktcHJpbWFyeSIgZD0iTTM0LDMwbC0yLTIsMi0ySDI2djhsMi0yLDIsMi01LDVIMjRsLTUtNSwyLTItMiwyaDhWMTVsLTIsMi0yLTIsNS01aDFsNSw1LTIsMi0yLTJ2OGg4bC0yLTIsMi0yLDUsNXYxWiIvPgogIDxwYXRoIGNsYXNzPSJ2aS1hY2NlbnQiIGQ9Ik0xOSwyM3YzaDR2NGgzVjI2aDRWMjNIMjZWMTlIMjN2NEgxOVoiLz4KPC9zdmc+Cg==") 24 24, crosshair';
+
+// 类型定义
+type PolygonLengthMeasurementAnnotation = Types.Annotation & {
+  data: {
+    handles: {
+      points: CoreTypes.Point3[];           // 多边形的所有顶点(世界坐标)
+      activeHandleIndex: number | null;     // 当前激活的手柄索引
+      textBox?: CoreTypes.Point2;           // 文本框位置
+    };
+    cachedStats?: {
+      [targetId: string]: {
+        totalLength: number;                // 总周长(像素)
+        totalLengthMm: number;              // 总周长(mm)
+        segmentLengths: number[];           // 各边长度(包括闭合边)
+      };
+    };
+  };
+};
+
+// 导出的注解数据接口
+export interface ExportedAnnotationData {
+  points: CoreTypes.Point3[];               // 多边形的所有顶点
+  totalLengthMm: number;                    // 总周长(mm)
+  metadata: {
+    viewPlaneNormal: CoreTypes.Point3;
+    viewUp: CoreTypes.Point3;
+    FrameOfReferenceUID: string;
+    referencedImageId: string;
+  };
+}
+
+export default class PolygonLengthMeasurementTool extends AnnotationTool {
+  static toolName = 'PolygonLengthMeasurementTool';
+
+  editData: {
+    annotation: Types.Annotation;
+    viewportIdsToRender: string[];
+    handleIndex?: number;
+    newAnnotation?: boolean;
+    hasMoved?: boolean;
+    textBoxBeingMoved?: boolean;            // 是否正在拖拽文本框
+    textBoxOffset?: CoreTypes.Point2;       // 文本框拖拽偏移量
+    movingWholeTool?: boolean;              // 是否移动整个工具
+    wholeToolOffset?: CoreTypes.Point2;     // 整体移动偏移量
+  } | null = null;
+
+  currentAnnotation: PolygonLengthMeasurementAnnotation | null = null; // 当前正在绘制的注解
+
+  isDrawing: boolean = false;
+
+  drawingElement: HTMLDivElement | null = null; // 当前绘制模式的元素
+
+  drawingCompleted: boolean = false; // 标记绘制是否已完成(双击后反激活)
+
+  // 预览鼠标位置(canvas坐标)
+  private previewMousePosition: CoreTypes.Point2 | null = null;
+
+  constructor(
+    toolProps: PublicToolProps = {},
+    defaultToolProps: ToolProps = {
+      supportedInteractionTypes: ['Mouse', 'Touch'],
+      configuration: {
+        shadow: true,
+        preventHandleOutsideImage: false,
+      },
+    }
+  ) {
+    super(toolProps, defaultToolProps);
+  }
+
+  /**
+   * 创建默认注解(用于预设模式)
+   */
+  static createDefaultAnnotation(
+    element: HTMLDivElement,
+    viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport,
+    points: CoreTypes.Point3[] = []
+  ): PolygonLengthMeasurementAnnotation {
+    const enabledElement = getEnabledElement(element);
+    if (!enabledElement) {
+      throw new Error('Element is not enabled');
+    }
+
+    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: PolygonLengthMeasurementTool.toolName,
+      },
+      data: {
+        label: '',
+        handles: {
+          points: points,
+          activeHandleIndex: null,
+        },
+        cachedStats: {},
+      },
+    } as PolygonLengthMeasurementAnnotation;
+
+    return annotationData;
+  }
+
+  /**
+   * 导出注解数据
+   */
+  static exportAnnotationData(
+    annotation: PolygonLengthMeasurementAnnotation,
+    viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport
+  ): ExportedAnnotationData | null {
+    const targetId = `imageId:${viewport.getCurrentImageId?.() || ''}`;
+    const cachedStats = annotation.data.cachedStats?.[targetId];
+
+    if (!cachedStats) {
+      return null;
+    }
+
+    return {
+      points: [...annotation.data.handles.points],
+      totalLengthMm: cachedStats.totalLengthMm,
+      metadata: {
+        viewPlaneNormal: annotation.metadata?.viewPlaneNormal || [0, 0, 1],
+        viewUp: annotation.metadata?.viewUp || [0, 1, 0],
+        FrameOfReferenceUID: annotation.metadata?.FrameOfReferenceUID || '',
+        referencedImageId: annotation.metadata?.referencedImageId || '',
+      },
+    };
+  }
+
+  /**
+   * 添加新注解 - 创建空的闭合多边形注解用于绘制模式
+   */
+  addNewAnnotation(
+    evt: EventTypes.InteractionEventType
+  ): PolygonLengthMeasurementAnnotation {
+    const eventDetail = evt.detail;
+    const { element, currentPoints } = eventDetail;
+    const worldPos = currentPoints.world;
+
+    const enabledElement = getEnabledElement(element);
+    if (!enabledElement) {
+      throw new Error('Element is not enabled');
+    }
+
+    const camera = enabledElement.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: enabledElement.viewport.getFrameOfReferenceUID(),
+        referencedImageId: enabledElement.viewport.getCurrentImageId?.() || '',
+        toolName: PolygonLengthMeasurementTool.toolName,
+      },
+      data: {
+        label: '',
+        handles: {
+          points: [worldPos], // 添加第一个点
+          activeHandleIndex: 0,
+        },
+        cachedStats: {},
+      },
+    } as PolygonLengthMeasurementAnnotation;
+
+    // 设置编辑数据
+    this.editData = {
+      annotation: annotationData,
+      viewportIdsToRender: [enabledElement.viewportId],
+      newAnnotation: true,
+      hasMoved: false,
+    };
+
+    // 激活绘制模式
+    this._activateDraw(element);
+
+    return annotationData;
+  }
+
+  /**
+   * 取消操作
+   */
+  cancel(element: HTMLDivElement): string {
+    if (this.isDrawing) {
+      this.isDrawing = false;
+      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 '';
+  }
+
+  /**
+   * 处理 控制点/端点 选中回调
+   */
+  handleSelectedCallback(
+    evt: EventTypes.InteractionEventType,
+    annotation: PolygonLengthMeasurementAnnotation
+  ): void {
+    // 实现控制点选中逻辑
+    annotation.isSelected = true;
+    annotation.highlighted = true;
+
+    const { element } = evt.detail;
+    const viewportIdsToRender =
+      utilities.viewportFilters.getViewportIdsWithToolToRender(
+        element,
+        this.getToolName()
+      );
+
+    utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
+  }
+
+  /**
+   * 工具选中回调 - 当工具被激活时调用
+   */
+  toolSelectedCallback(
+    evt: EventTypes.InteractionEventType,
+    annotation: PolygonLengthMeasurementAnnotation
+  ): void {
+    const { element } = evt.detail;
+
+    // 如果 annotation 不为空,说明是点击已有的注释,激活修改模式
+    if (annotation) {
+      console.log('PolygonLengthMeasurementTool selected existing annotation');
+      this._activateModify(element);
+      return;
+    }
+
+    // 工具被激活时,总是进入绘制模式,允许开始新的绘制
+    // 完全重置所有绘制状态,确保全新的开始
+    this._deactivateDraw(element); // 先清理之前的绘制状态
+    this._deactivateModify(element); // 清理修改模式
+
+    this.isDrawing = false; // 重置绘制状态
+    this.drawingCompleted = false; // 允许新的点击操作
+    this.currentAnnotation = null;
+    this.drawingElement = null;
+
+    // 激活绘制模式,等待用户开始绘制
+    this._activateDraw(element);
+
+    console.log('PolygonLengthMeasurementTool activated for new drawing');
+  }
+
+  /**
+   * 激活时被调用
+   */
+  onSetToolActive(): void {
+    // 示例:重置相关状态
+    this.isDrawing = false;
+    this.editData = null;
+    this.drawingCompleted = false;//允许开始新的注解
+    // 示例:添加工具特定的初始化
+    // this.initializeCustomState();
+  }
+
+  /**
+   * 检查点是否靠近工具
+   */
+  isPointNearTool(
+    element: HTMLDivElement,
+    annotation: PolygonLengthMeasurementAnnotation,
+    canvasCoords: CoreTypes.Point2,
+    proximity: number
+  ): boolean {
+    const enabledElement = getEnabledElement(element);
+    if (!enabledElement) {
+      return false;
+    }
+    const { viewport } = enabledElement;
+
+    const points = annotation.data.handles.points;
+
+    // 检查是否靠近文本框
+    const textBox = annotation.data.handles.textBox;
+    if (textBox && Array.isArray(textBox) && textBox.length >= 2) {
+      if (this._isPointInTextBox(canvasCoords, textBox as CoreTypes.Point2)) {
+        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;
+      }
+    }
+
+    // 检查是否靠近线段(包括闭合线段)
+    if (points.length >= 2) {
+      for (let i = 0; i < points.length - 1; i++) {
+        const p1Canvas = viewport.worldToCanvas(points[i]);
+        const p2Canvas = viewport.worldToCanvas(points[i + 1]);
+
+        const dist = this._distanceToSegment(canvasCoords, p1Canvas, p2Canvas);
+        if (dist < proximity) {
+          return true;
+        }
+      }
+    }
+
+    // 检查是否靠近闭合线段
+    if (points.length >= 3) {
+      if (this._isPointNearClosingSegment(canvasCoords, points, viewport, proximity)) {
+        return true;
+      }
+    }
+
+    return false;
+  }
+
+  /**
+   * 检查是否靠近闭合线段
+   */
+  private _isPointNearClosingSegment(
+    canvasCoords: CoreTypes.Point2,
+    points: CoreTypes.Point3[],
+    viewport: CoreTypes.IViewport,
+    proximity: number
+  ): boolean {
+    if (points.length < 3) return false;
+
+    const lastPoint = points[points.length - 1];
+    const firstPoint = points[0];
+
+    const lastCanvas = viewport.worldToCanvas(lastPoint);
+    const firstCanvas = viewport.worldToCanvas(firstPoint);
+
+    const dist = this._distanceToSegment(canvasCoords, lastCanvas, firstCanvas);
+    return dist < proximity;
+  }
+
+  /**
+   * 检查点击是否在文本框内
+   */
+  private _isPointInTextBox(
+    canvasPoint: CoreTypes.Point2,
+    textBoxPosition: CoreTypes.Point2,
+    textWidth: number = 100,
+    textHeight: number = 40
+  ): boolean {
+    // 确保参数有效
+    if (!Array.isArray(canvasPoint) || canvasPoint.length < 2 ||
+      !Array.isArray(textBoxPosition) || textBoxPosition.length < 2) {
+      return false;
+    }
+
+    const [x, y] = canvasPoint;
+    const [tx, ty] = textBoxPosition;
+
+    const margin = 10;
+    return (
+      x >= tx - margin &&
+      x <= tx + textWidth + margin &&
+      y >= ty - margin &&
+      y <= ty + textHeight + margin
+    );
+  }
+
+  /**
+   * 计算点到线段的距离
+   */
+  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 _calculateDistance(point1: CoreTypes.Point3, point2: CoreTypes.Point3, viewport: CoreTypes.IViewport): number {
+    const canvas1 = viewport.worldToCanvas(point1);
+    const canvas2 = viewport.worldToCanvas(point2);
+    return Math.sqrt(
+      Math.pow(canvas2[0] - canvas1[0], 2) + Math.pow(canvas2[1] - canvas1[1], 2)
+    );
+  }
+
+  /**
+   * 将像素距离转换为mm
+   */
+  private _convertPixelsToMm(pixels: number, viewport: CoreTypes.IViewport): number {
+    // 这里需要根据DICOM元数据获取像素间距
+    // 暂时使用固定值,实际应该从图像元数据中获取
+    const pixelSpacing = 0.5; // mm per pixel,示例值
+    return pixels * pixelSpacing;
+  }
+
+  /**
+   * 更新缓存的统计数据(包含闭合边计算)
+   */
+  _updateCachedStats(
+    annotation: PolygonLengthMeasurementAnnotation,
+    enabledElement: CoreTypes.IEnabledElement
+  ): void {
+    const { viewport } = enabledElement;
+    const { data } = annotation;
+    const points = data.handles.points;
+
+    const targetId = this.getTargetId(viewport);
+    if (!targetId) {
+      return;
+    }
+
+    if (!data.cachedStats) {
+      data.cachedStats = {};
+    }
+
+    if (!data.cachedStats[targetId]) {
+      data.cachedStats[targetId] = {
+        totalLength: 0,
+        totalLengthMm: 0,
+        segmentLengths: []
+      };
+    }
+
+    const stats = data.cachedStats[targetId];
+    stats.segmentLengths = [];
+
+    let totalPixels = 0;
+
+    // 计算所有相邻顶点间的边长
+    for (let i = 0; i < points.length - 1; i++) {
+      const segmentLength = this._calculateDistance(points[i], points[i + 1], viewport);
+      stats.segmentLengths.push(segmentLength);
+      totalPixels += segmentLength;
+    }
+
+    // 计算闭合边(最后一个点到第一个点)
+    if (points.length >= 3) {
+      const closingSegmentLength = this._calculateDistance(points[points.length - 1], points[0], viewport);
+      stats.segmentLengths.push(closingSegmentLength);
+      totalPixels += closingSegmentLength;
+    }
+
+    stats.totalLength = totalPixels;
+    stats.totalLengthMm = this._convertPixelsToMm(totalPixels, viewport);
+  }
+
+  /**
+   * 激活绘制模式
+   */
+  _activateDraw(element: HTMLDivElement): void {
+    this.drawingElement = element; // 存储当前绘制模式的元素
+    this.isDrawing = true;
+    element.addEventListener(
+      'CORNERSTONE_TOOLS_MOUSE_CLICK',
+      this._mouseClickDrawCallback as EventListener
+    );
+    element.addEventListener(
+      'dblclick',
+      this._mouseDoubleClickDrawCallback as EventListener
+    );
+    // 添加鼠标移动监听用于预览
+    element.addEventListener(
+      'CORNERSTONE_TOOLS_MOUSE_MOVE',
+      this._mouseMoveDrawCallback as EventListener
+    );
+  }
+
+  /**
+   * 取消激活绘制模式
+   */
+  _deactivateDraw(element?: HTMLDivElement): void {
+    const targetElement = element || this.drawingElement;
+    if (!targetElement) return;
+
+    this.isDrawing = false;
+    targetElement.removeEventListener(
+      'CORNERSTONE_TOOLS_MOUSE_CLICK',
+      this._mouseClickDrawCallback as EventListener
+    );
+    targetElement.removeEventListener(
+      'dblclick',
+      this._mouseDoubleClickDrawCallback as EventListener
+    );
+    // 清理鼠标移动监听器
+    targetElement.removeEventListener(
+      'CORNERSTONE_TOOLS_MOUSE_MOVE',
+      this._mouseMoveDrawCallback as EventListener
+    );
+    this.drawingElement = null; // 清理
+    this.previewMousePosition = null; // 清理预览位置
+  }
+
+  /**
+   * 激活修改模式
+   */
+  _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
+    );
+  }
+
+  /**
+   * 鼠标点击回调 - 用于绘制模式,添加新的点
+   */
+  _mouseClickDrawCallback = (evt: EventTypes.InteractionEventType): void => {
+    if (!this.isDrawing || this.drawingCompleted) {
+      return;
+    }
+
+    const eventDetail = evt.detail;
+    const { element, currentPoints } = eventDetail;
+    const worldPos = currentPoints.world;
+
+    const enabledElement = getEnabledElement(element);
+    if (!enabledElement) {
+      return;
+    }
+
+    // 如果还没有当前注解,创建第一个
+    if (!this.currentAnnotation) {
+      // 创建新的注解
+      const camera = enabledElement.viewport.getCamera();
+      const { viewPlaneNormal, viewUp } = camera;
+      if (viewPlaneNormal === undefined || viewUp === undefined) {
+        return;
+      }
+
+      this.currentAnnotation = {
+        invalidated: true,
+        highlighted: false,
+        metadata: {
+          viewPlaneNormal: [...viewPlaneNormal] as CoreTypes.Point3,
+          viewUp: [...viewUp] as CoreTypes.Point3,
+          FrameOfReferenceUID: enabledElement.viewport.getFrameOfReferenceUID(),
+          referencedImageId: enabledElement.viewport.getCurrentImageId?.() || '',
+          toolName: PolygonLengthMeasurementTool.toolName,
+        },
+        data: {
+          label: '',
+          handles: {
+            points: [worldPos],
+            activeHandleIndex: 0,
+          },
+          cachedStats: {},
+        },
+      } as PolygonLengthMeasurementAnnotation;
+
+      // 添加到临时状态以便渲染
+      annotation.state.addAnnotation(this.currentAnnotation, element);
+
+      console.log('Created first point for polygon');
+    } else {
+      // 添加新点到现有注解
+      this.currentAnnotation.data.handles.points.push(worldPos);
+      this.currentAnnotation.data.handles.activeHandleIndex = this.currentAnnotation.data.handles.points.length - 1;
+
+      // 更新统计数据
+      this._updateCachedStats(this.currentAnnotation, enabledElement);
+
+      console.log(`Added point ${this.currentAnnotation.data.handles.points.length} to polygon`);
+    }
+
+    // 触发渲染更新
+    const viewportIdsToRender = utilities.viewportFilters.getViewportIdsWithToolToRender(
+      element,
+      this.getToolName()
+    );
+    utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
+
+    evt.preventDefault();
+    evt.stopPropagation();
+  };
+
+  /**
+   * 鼠标移动回调 - 用于绘制模式,更新预览位置
+   */
+  _mouseMoveDrawCallback = (evt: EventTypes.InteractionEventType): void => {
+    if (!this.isDrawing) {
+      return;
+    }
+
+    const eventDetail = evt.detail;
+    const canvasCoords = eventDetail.currentPoints?.canvas;
+
+    if (canvasCoords) {
+      this.previewMousePosition = [...canvasCoords];
+
+      // 触发重新渲染以显示预览线
+      const viewportIdsToRender = utilities.viewportFilters.getViewportIdsWithToolToRender(
+        eventDetail.element,
+        this.getToolName()
+      );
+      utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
+    }
+  };
+
+  /**
+   * 鼠标双击回调 - 用于绘制模式,在双击位置添加最后一个点并结束绘制
+   */
+  _mouseDoubleClickDrawCallback = (evt: EventTypes.InteractionEventType): void => {
+    if (!this.isDrawing || !this.currentAnnotation) {
+      return;
+    }
+
+    const eventDetail = evt.detail;
+    const element = eventDetail.element || this.drawingElement;
+    const currentPoints = eventDetail.currentPoints;
+
+    if (!element) {
+      return;
+    }
+
+    // 获取双击位置,支持多种情况
+    let worldPos: CoreTypes.Point3 | undefined;
+
+    if (currentPoints?.world) {
+      // 优先使用 world 坐标
+      worldPos = currentPoints.world;
+    } else if (currentPoints?.canvas) {
+      // 如果没有 world 坐标但有 canvas 坐标,转换得到 world 坐标
+      const enabledElement = getEnabledElement(element);
+      if (enabledElement) {
+        worldPos = enabledElement.viewport.canvasToWorld(currentPoints.canvas);
+      }
+    } else {
+      // 如果 CornerstoneJS 没有提供坐标,使用原生 DOM 事件坐标
+      // __类型转换__:将 `evt` 转换为 `MouseEvent`
+      const mouseEvent = evt as unknown as MouseEvent;
+
+      // __获取鼠标坐标__:使用 `clientX`, `clientY`
+      const clientX = mouseEvent.clientX;
+      const clientY = mouseEvent.clientY;
+
+      // __计算 canvas 坐标__:减去 canvas 的偏移量
+      const rect = element.getBoundingClientRect();
+      const canvasX = clientX - rect.left;
+      const canvasY = clientY - rect.top;
+
+      const canvasPos: CoreTypes.Point2 = [canvasX, canvasY];
+
+      // __转换为 world 坐标__:使用 `viewport.canvasToWorld()`
+      const enabledElement = getEnabledElement(element);
+      if (enabledElement) {
+        worldPos = enabledElement.viewport.canvasToWorld(canvasPos);
+      }
+    }
+
+    if (!worldPos) {
+      // 如果仍然没有位置信息,记录警告但不中断流程
+      console.warn('Unable to get double-click position for polygon');
+      return;
+    }
+
+    // 先在双击位置添加最后一个点
+    this.currentAnnotation.data.handles.points.push(worldPos);
+    this.currentAnnotation.data.handles.activeHandleIndex = this.currentAnnotation.data.handles.points.length - 1;
+
+    // 更新统计数据
+    const enabledElement = getEnabledElement(element);
+    if (enabledElement) {
+      this._updateCachedStats(this.currentAnnotation, enabledElement);
+    }
+
+    // 然后检查点数决定是否结束绘制
+    if (this.currentAnnotation.data.handles.points.length >= 3) {
+      // 结束绘制模式,激活修改模式
+      this._deactivateDraw(element);
+      this._activateModify(element);
+      this.isDrawing = false;
+      this.drawingCompleted = true; // 标记绘制已完成,防止再次添加点
+      this.currentAnnotation = null; // 完全清理当前注解引用,确保下次激活时全新的开始
+
+      // 取消所有注解的选中状态,确保下次激活时 annotation 参数为 null
+      const annotations = annotation.state.getAnnotations(this.getToolName(), element);
+      if (annotations) {
+        for (const ann of annotations) {
+          const customAnn = ann as PolygonLengthMeasurementAnnotation;
+          customAnn.isSelected = false;
+          customAnn.highlighted = false;
+        }
+      }
+
+      console.log('Finished drawing polygon');
+    } else {
+      // 如果点数仍然少于3个,删除这个注解
+      annotation.state.removeAnnotation(this.currentAnnotation.annotationUID!);
+      this.currentAnnotation = null;
+    }
+
+    // 触发渲染更新
+    const viewportIdsToRender = utilities.viewportFilters.getViewportIdsWithToolToRender(
+      element,
+      this.getToolName()
+    );
+    utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
+
+    evt.preventDefault();
+    evt.stopPropagation();
+  };
+
+  /**
+   * 键盘按下回调 - 用于删除功能
+   */
+  _keyDownCallback = (evt: KeyboardEvent): void => {
+    const element = evt.currentTarget as HTMLDivElement;
+
+    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 PolygonLengthMeasurementAnnotation).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();
+    }
+  };
+
+  /**
+   * 鼠标移动回调 - 用于修改模式,处理悬停检测
+   */
+  _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 PolygonLengthMeasurementAnnotation;
+      const textBoxPosition = customAnn.data.handles.textBox;
+
+      if (textBoxPosition && this._isPointInTextBox(canvasCoords, textBoxPosition)) {
+        element.style.cursor = 'pointer';
+        customAnn.highlighted = true;
+        isHovering = true;
+        break;
+      }
+    }
+
+    // 如果没有悬停在文本框上,检查是否悬停在手柄或线段上
+    if (!isHovering) {
+      for (const ann of annotations) {
+        const customAnn = ann as PolygonLengthMeasurementAnnotation;
+
+        // 检查是否靠近手柄
+        const handle = this.getHandleNearImagePoint(element, customAnn, canvasCoords, 6);
+        if (handle) {
+          element.style.cursor = cursorUrl;
+          customAnn.highlighted = true;
+          isHovering = true;
+          break;
+        }
+
+        // 检查是否靠近线段(包括闭合线段)
+        if (this.isPointNearTool(element, customAnn, canvasCoords, 10)) {
+          element.style.cursor = cursorUrl;
+          customAnn.highlighted = true;
+          isHovering = true;
+          break;
+        }
+      }
+    }
+
+    // 如果没有悬停在任何地方,重置高亮
+    if (!isHovering) {
+      for (const ann of annotations) {
+        const customAnn = ann as PolygonLengthMeasurementAnnotation;
+        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 PolygonLengthMeasurementAnnotation;
+      const textBoxPosition = customAnn.data.handles.textBox;
+
+      if (textBoxPosition && this._isPointInTextBox(canvasCoords, textBoxPosition)) {
+        const viewportIdsToRender =
+          utilities.viewportFilters.getViewportIdsWithToolToRender(
+            element,
+            this.getToolName()
+          );
+
+        const textBoxOffset: CoreTypes.Point2 = [
+          canvasCoords[0] - textBoxPosition[0],
+          canvasCoords[1] - textBoxPosition[1],
+        ];
+
+        this.editData = {
+          annotation: customAnn,
+          viewportIdsToRender,
+          hasMoved: false,
+          textBoxBeingMoved: true,
+          textBoxOffset: textBoxOffset,
+        };
+
+        customAnn.isSelected = true;
+        customAnn.highlighted = true;
+        utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
+
+        evt.preventDefault();
+        evt.stopPropagation();
+        return;
+      }
+    }
+
+    // 如果没有点击文本框,再查找最近的手柄
+    for (const ann of annotations) {
+      const customAnn = ann as PolygonLengthMeasurementAnnotation;
+      const handle = this.getHandleNearImagePoint(
+        element,
+        customAnn,
+        canvasCoords,
+        6
+      );
+
+      if (handle) {
+        const viewportIdsToRender =
+          utilities.viewportFilters.getViewportIdsWithToolToRender(
+            element,
+            this.getToolName()
+          );
+
+        this.editData = {
+          annotation: customAnn,
+          viewportIdsToRender,
+          handleIndex: customAnn.data.handles.activeHandleIndex || 0,
+          hasMoved: false,
+        };
+
+        customAnn.isSelected = true;
+        customAnn.highlighted = true;
+
+        utilities.triggerAnnotationRenderForViewportIds(
+          viewportIdsToRender
+        );
+
+        evt.preventDefault();
+        evt.stopPropagation();
+        return;
+      }
+    }
+
+    // 如果没有找到手柄,检查是否点击在工具的其他位置(线段上,包括闭合线段)
+    for (const ann of annotations) {
+      const customAnn = ann as PolygonLengthMeasurementAnnotation;
+      if (this.isPointNearTool(element, customAnn, canvasCoords, 10)) {
+        const viewportIdsToRender =
+          utilities.viewportFilters.getViewportIdsWithToolToRender(
+            element,
+            this.getToolName()
+          );
+
+        const points = customAnn.data.handles.points;
+        if (points.length >= 2) {
+          const midPoint = calculateMidpoint(points[0], points[1]);
+          const midCanvas = viewport.worldToCanvas(midPoint);
+          const wholeToolOffset: CoreTypes.Point2 = [
+            canvasCoords[0] - midCanvas[0],
+            canvasCoords[1] - midCanvas[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 PolygonLengthMeasurementAnnotation;
+      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, textBoxBeingMoved, movingWholeTool } = this.editData;
+    const customAnn = ann as PolygonLengthMeasurementAnnotation;
+    const { data } = customAnn;
+
+    // 如果正在拖拽文本框
+    if (textBoxBeingMoved && this.editData.textBoxOffset) {
+      const newTextBoxPosition: CoreTypes.Point2 = [
+        canvasCoords[0] - this.editData.textBoxOffset[0],
+        canvasCoords[1] - this.editData.textBoxOffset[1],
+      ];
+
+      data.handles.textBox = newTextBoxPosition;
+      this.editData.hasMoved = true;
+
+      utilities.triggerAnnotationRenderForViewportIds(
+        viewportIdsToRender
+      );
+
+      evt.preventDefault();
+      evt.stopPropagation();
+      return;
+    }
+
+    // 如果正在移动整个工具
+    if (movingWholeTool && this.editData.wholeToolOffset) {
+      const newMidCanvas: CoreTypes.Point2 = [
+        canvasCoords[0] - this.editData.wholeToolOffset[0],
+        canvasCoords[1] - this.editData.wholeToolOffset[1],
+      ];
+      const newMidWorld = enabledElement.viewport.canvasToWorld(newMidCanvas);
+
+      const points = data.handles.points;
+      if (points.length >= 2) {
+        const oldMid = calculateMidpoint(points[0], points[1]);
+        const oldMidCanvas = enabledElement.viewport.worldToCanvas(oldMid);
+        const offset = vectorSubtract(newMidWorld, oldMid);
+        const canvasOffset: CoreTypes.Point2 = [
+          newMidCanvas[0] - oldMidCanvas[0],
+          newMidCanvas[1] - oldMidCanvas[1],
+        ];
+
+        // 移动所有点
+        for (let i = 0; i < points.length; i++) {
+          points[i] = vectorAdd(points[i], offset);
+        }
+
+        // 移动文本框位置 (使用canvas坐标偏移)
+        if (data.handles.textBox && Array.isArray(data.handles.textBox) && data.handles.textBox.length >= 2) {
+          data.handles.textBox[0] += canvasOffset[0];
+          data.handles.textBox[1] += canvasOffset[1];
+        }
+
+        this._updateCachedStats(customAnn, enabledElement);
+        this.editData.hasMoved = true;
+
+        utilities.triggerAnnotationRenderForViewportIds(
+          viewportIdsToRender
+        );
+
+        evt.preventDefault();
+        evt.stopPropagation();
+        return;
+      }
+    }
+
+    // 否则处理手柄拖拽
+    const worldPos = currentPoints.world;
+    const activeHandleIndex = data.handles.activeHandleIndex;
+
+    if (activeHandleIndex !== null && activeHandleIndex >= 0 && activeHandleIndex < data.handles.points.length) {
+      data.handles.points[activeHandleIndex] = worldPos;
+      this._updateCachedStats(customAnn, enabledElement);
+      this.editData.hasMoved = true;
+
+      utilities.triggerAnnotationRenderForViewportIds(
+        viewportIdsToRender
+      );
+
+      evt.preventDefault();
+      evt.stopPropagation();
+    }
+  };
+
+  /**
+   * 鼠标释放回调 - 用于修改模式
+   */
+  _mouseUpModifyCallback = (evt: EventTypes.InteractionEventType): void => {
+    if (!this.editData) {
+      return;
+    }
+
+    const { annotation: ann, hasMoved, movingWholeTool } = this.editData;
+    const customAnn = ann as PolygonLengthMeasurementAnnotation;
+
+    customAnn.data.handles.activeHandleIndex = null;
+
+    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();
+  };
+
+  /**
+   * 获取靠近图像点的手柄
+   */
+  getHandleNearImagePoint(
+    element: HTMLDivElement,
+    annotation: PolygonLengthMeasurementAnnotation,
+    canvasCoords: CoreTypes.Point2,
+    proximity: number
+  ): Types.ToolHandle | undefined {
+    const enabledElement = getEnabledElement(element);
+    if (!enabledElement) {
+      return undefined;
+    }
+    const { viewport } = enabledElement;
+
+    const points = annotation.data.handles.points;
+    const handleProximity = Math.max(proximity, 15);
+
+    for (let i = 0; i < points.length; i++) {
+      const point = points[i];
+      const canvasPoint = viewport.worldToCanvas(point);
+      const distance = Math.sqrt(
+        Math.pow(canvasPoint[0] - canvasCoords[0], 2) +
+        Math.pow(canvasPoint[1] - canvasCoords[1], 2)
+      );
+
+      if (distance < handleProximity) {
+        annotation.data.handles.activeHandleIndex = i;
+        return {
+          worldPosition: point,
+        } as Types.ToolHandle;
+      }
+    }
+
+    annotation.data.handles.activeHandleIndex = null;
+    return undefined;
+  }
+
+  /**
+   * 渲染注解
+   */
+  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 PolygonLengthMeasurementAnnotation;
+      const { annotationUID, data } = annotation;
+      const points = data.handles.points;
+
+      // 如果点数为0,跳过
+      if (points.length === 0) {
+        continue;
+      }
+
+      const targetId = this.getTargetId(viewport);
+      const cachedStats = targetId ? data.cachedStats?.[targetId] : undefined;
+
+      // 转换所有点为 canvas 坐标
+      const canvasPoints = points.map((p) => viewport.worldToCanvas(p));
+
+      const lineOptions = {
+        color: 'rgb(0, 255, 0)',
+        width: 2,
+      };
+      // 绘制多边形线段(需要至少2个点)
+      if (annotationUID && points.length >= 2) {
+        for (let j = 0; j < canvasPoints.length - 1; j++) {
+          const lineUID = `${annotationUID}-line-${j}`;
+          drawLineSvg(
+            svgDrawingHelper,
+            annotationUID,
+            lineUID,
+            canvasPoints[j],
+            canvasPoints[j + 1],
+            lineOptions
+          );
+        }
+
+        // 绘制闭合线段(从最后一个点到第一个点)
+        if (points.length >= 3) {
+          const closingLineUID = `${annotationUID}-closing-line`;
+          drawLineSvg(
+            svgDrawingHelper,
+            annotationUID,
+            closingLineUID,
+            canvasPoints[canvasPoints.length - 1],
+            canvasPoints[0],
+            lineOptions
+          );
+        }
+      }
+
+      // 绘制周长文本(需要至少3个点且有统计数据)
+      if (cachedStats?.totalLengthMm !== undefined && annotationUID && points.length >= 3) {
+        const textLines = [`周长: ${cachedStats.totalLengthMm.toFixed(2)} mm`];
+        const textUID = `${annotationUID}-length-text`;
+        const textBoxPosition: CoreTypes.Point2 =
+          (data.handles.textBox &&
+            Array.isArray(data.handles.textBox) &&
+            data.handles.textBox.length >= 2)
+            ? data.handles.textBox
+            : [
+              canvasPoints[0][0] + 10,
+              canvasPoints[0][1] - 10,
+            ];
+
+        console.log('textBoxPosition:', textBoxPosition);
+        // 保存文本框位置到注解数据中
+        data.handles.textBox = textBoxPosition;
+
+        drawLinkedTextBox(
+          svgDrawingHelper,
+          annotationUID,
+          textUID,
+          textLines,
+          textBoxPosition,
+          [canvasPoints[0] as CoreTypes.Point2],
+          {},
+          {
+            color: 'rgb(0, 255, 0)',
+          }
+        );
+      }
+
+      // 绘制手柄点(至少有1个点就绘制)
+      if (annotationUID && points.length >= 1) {
+        const handleGroupUID = '0';
+        const isSelected = annotation.isSelected || annotation.highlighted;
+        const handleRadius = isSelected ? 12 : 8;
+
+        drawHandles(
+          svgDrawingHelper,
+          annotationUID,
+          handleGroupUID,
+          canvasPoints,
+          {
+            color: 'rgb(255, 255, 255)',
+            handleRadius: handleRadius,
+          }
+        );
+      }
+
+      renderStatus = true;
+    }
+
+    // 绘制预览线(只在绘制模式且有鼠标位置时)
+    if (this.isDrawing && this.previewMousePosition && this.currentAnnotation &&
+      this.currentAnnotation.data.handles.points.length > 0) {
+
+      const points = this.currentAnnotation.data.handles.points;
+      const lastPoint = points[points.length - 1];
+      const lastCanvasPoint = enabledElement.viewport.worldToCanvas(lastPoint);
+
+      // 总是绘制从最后一个点到鼠标位置的预览线(用于显示即将添加的下一个线段)
+      const previewLineUID = 'preview-line-to-mouse';
+      drawLineSvg(
+        svgDrawingHelper,
+        'preview',  // 使用固定UID
+        previewLineUID,
+        lastCanvasPoint,
+        this.previewMousePosition,
+        {
+          color: 'rgba(0, 255, 0, 0.7)',  // 半透明绿色
+          width: 2,
+          lineDash: [8, 4],  // 虚线样式:8px实线,4px空白
+        }
+      );
+
+      // 当至少有两个点时,额外绘制从鼠标位置到起始点的预览线(用于预览闭合形状)
+      if (points.length >= 2) {
+        const firstPoint = points[0];
+        const firstCanvasPoint = enabledElement.viewport.worldToCanvas(firstPoint);
+
+        const closingPreviewLineUID = 'preview-line-closing';
+        drawLineSvg(
+          svgDrawingHelper,
+          'preview',  // 使用固定UID
+          closingPreviewLineUID,
+          this.previewMousePosition,
+          firstCanvasPoint,
+          {
+            color: 'rgba(255, 255, 0, 0.7)',  // 半透明黄色,用于区分
+            width: 2,
+            lineDash: [8, 4],  // 虚线样式:8px实线,4px空白
+          }
+        );
+      }
+    }
+
+    return renderStatus;
+  };
+}

+ 1411 - 0
src/components/measures/PolylineLengthMeasurementTool.ts

@@ -0,0 +1,1411 @@
+import { Point3, Point2, fromPoint3ToPoint2, decomposeVectorOnAxes } from './mathUtils';
+import {
+  utilities as csUtils,
+  Types as CoreTypes,
+  getEnabledElement,
+} from '@cornerstonejs/core';
+
+import {
+  AnnotationTool,
+  utilities,
+  Types,
+  annotation,
+  drawing,
+} from '@cornerstonejs/tools';
+
+const {
+  drawHandles,
+  drawLinkedTextBox,
+  drawLine: drawLineSvg,
+  drawCircle: drawCircleSvg,
+  drawPath: drawPathSvg,
+} = drawing;
+
+import {
+  calculateMidpoint,
+  vectorSubtract,
+  vectorAdd,
+  vectorScale,
+  getAngleBetweenLines,
+  degrees,
+  getLineIntersection,
+  fromPoint2ToPoint3,
+} from './mathUtils';
+import {
+  PublicToolProps,
+  ToolProps,
+  EventTypes,
+  SVGDrawingHelper,
+} from '@cornerstonejs/tools/dist/esm/types';
+
+// 十字准星光标
+const cursorUrl = 'url("data:image/svg+xml;base64,PHN2ZyBpZD0ibW92ZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB3aWR0aD0iNDgiIGhlaWdodD0iNDgiIHZpZXdCb3g9IjAgMCA0OCA0OCI+CiAgPGRlZnM+CiAgICA8c3R5bGU+CiAgICAgIC52aS1wcmltYXJ5IHsKICAgICAgICBmaWxsOiAjRkY2RTZFOwogICAgICB9CgogICAgICAudmktcHJpbWFyeSwgLnZpLWFjY2VudCB7CiAgICAgICAgZmlsbC1ydWxlOiBldmVub2RkOwogICAgICB9CgogICAgICAudmktYWNjZW50IHsKICAgICAgICBmaWxsOiAjMEMwMDU4OwogICAgICB9CiAgICA8L3N0eWxlPgogIDwvZGVmcz4KICA8cGF0aCBjbGFzcz0idmktcHJpbWFyeSIgZD0iTTM0LDMwbC0yLTIsMi0ySDI2djhsMi0yLDIsMi01LDVIMjRsLTUtNSwyLTIsMiwyVjI2SDE1bDIsMi0yLTItNS01VjI0bDUtNSwyLTItMiwyaDhWMTVsLTIsMi0yLTIsNS01aDFsNSw1LTIsMi0yLTJ2OGg4bC0yLTIsMi0yLDUsNXYxWiIvPgogIDxwYXRoIGNsYXNzPSJ2aS1hY2NlbnQiIGQ9Ik0xOSwyM3YzaDR2NGgzVjI2aDRWMjNIMjZWMTlIMjN2NEgxOVoiLz4KPC9zdmc+Cg==") 24 24, crosshair';
+
+// 类型定义
+type PolylineLengthMeasurementAnnotation = Types.Annotation & {
+  data: {
+    handles: {
+      points: CoreTypes.Point3[];           // 折线的所有点(世界坐标)
+      activeHandleIndex: number | null;     // 当前激活的手柄索引
+      textBox?: CoreTypes.Point2;           // 文本框位置
+    };
+    cachedStats?: {
+      [targetId: string]: {
+        totalLength: number;                // 总长度(像素)
+        totalLengthMm: number;              // 总长度(mm)
+        segmentLengths: number[];           // 各线段长度
+      };
+    };
+  };
+};
+
+// 导出的注解数据接口
+export interface ExportedAnnotationData {
+  points: CoreTypes.Point3[];               // 折线的所有点
+  totalLengthMm: number;                    // 总长度(mm)
+  metadata: {
+    viewPlaneNormal: CoreTypes.Point3;
+    viewUp: CoreTypes.Point3;
+    FrameOfReferenceUID: string;
+    referencedImageId: string;
+  };
+}
+
+export default class PolylineLengthMeasurementTool extends AnnotationTool {
+  static toolName = 'PolylineLengthMeasurementTool';
+
+  editData: {
+    annotation: Types.Annotation;
+    viewportIdsToRender: string[];
+    handleIndex?: number;
+    newAnnotation?: boolean;
+    hasMoved?: boolean;
+    textBoxBeingMoved?: boolean;            // 是否正在拖拽文本框
+    textBoxOffset?: CoreTypes.Point2;       // 文本框拖拽偏移量
+    movingWholeTool?: boolean;              // 是否移动整个工具
+    wholeToolOffset?: CoreTypes.Point2;     // 整体移动偏移量
+  } | null = null;
+
+  currentAnnotation: PolylineLengthMeasurementAnnotation | null = null; // 当前正在绘制的注解
+
+  isDrawing: boolean = false;
+
+  drawingElement: HTMLDivElement | null = null; // 当前绘制模式的元素
+
+  drawingCompleted: boolean = false; // 标记绘制是否已完成(双击后反激活)
+
+  // 预览鼠标位置(canvas坐标)
+  private previewMousePosition: CoreTypes.Point2 | null = null;
+
+  constructor(
+    toolProps: PublicToolProps = {},
+    defaultToolProps: ToolProps = {
+      supportedInteractionTypes: ['Mouse', 'Touch'],
+      configuration: {
+        shadow: true,
+        preventHandleOutsideImage: false,
+      },
+    }
+  ) {
+    super(toolProps, defaultToolProps);
+  }
+
+  /**
+   * 创建默认注解(用于预设模式)
+   */
+  static createDefaultAnnotation(
+    element: HTMLDivElement,
+    viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport,
+    points: CoreTypes.Point3[] = []
+  ): PolylineLengthMeasurementAnnotation {
+    const enabledElement = getEnabledElement(element);
+    if (!enabledElement) {
+      throw new Error('Element is not enabled');
+    }
+
+    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: PolylineLengthMeasurementTool.toolName,
+      },
+      data: {
+        label: '',
+        handles: {
+          points: points,
+          activeHandleIndex: null,
+        },
+        cachedStats: {},
+      },
+    } as PolylineLengthMeasurementAnnotation;
+
+    return annotationData;
+  }
+
+  /**
+   * 导出注解数据
+   */
+  static exportAnnotationData(
+    annotation: PolylineLengthMeasurementAnnotation,
+    viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport
+  ): ExportedAnnotationData | null {
+    const targetId = `imageId:${viewport.getCurrentImageId?.() || ''}`;
+    const cachedStats = annotation.data.cachedStats?.[targetId];
+
+    if (!cachedStats) {
+      return null;
+    }
+
+    return {
+      points: [...annotation.data.handles.points],
+      totalLengthMm: cachedStats.totalLengthMm,
+      metadata: {
+        viewPlaneNormal: annotation.metadata?.viewPlaneNormal || [0, 0, 1],
+        viewUp: annotation.metadata?.viewUp || [0, 1, 0],
+        FrameOfReferenceUID: annotation.metadata?.FrameOfReferenceUID || '',
+        referencedImageId: annotation.metadata?.referencedImageId || '',
+      },
+    };
+  }
+
+  /**
+   * 添加新注解 - 创建空的折线注解用于绘制模式
+   */
+  addNewAnnotation(
+    evt: EventTypes.InteractionEventType
+  ): PolylineLengthMeasurementAnnotation {
+    const eventDetail = evt.detail;
+    const { element, currentPoints } = eventDetail;
+    const worldPos = currentPoints.world;
+
+    const enabledElement = getEnabledElement(element);
+    if (!enabledElement) {
+      throw new Error('Element is not enabled');
+    }
+
+    const camera = enabledElement.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: enabledElement.viewport.getFrameOfReferenceUID(),
+        referencedImageId: enabledElement.viewport.getCurrentImageId?.() || '',
+        toolName: PolylineLengthMeasurementTool.toolName,
+      },
+      data: {
+        label: '',
+        handles: {
+          points: [worldPos], // 添加第一个点
+          activeHandleIndex: 0,
+        },
+        cachedStats: {},
+      },
+    } as PolylineLengthMeasurementAnnotation;
+
+    // 设置编辑数据
+    this.editData = {
+      annotation: annotationData,
+      viewportIdsToRender: [enabledElement.viewportId],
+      newAnnotation: true,
+      hasMoved: false,
+    };
+
+    // 激活绘制模式
+    this._activateDraw(element);
+
+    return annotationData;
+  }
+
+  /**
+   * 取消操作
+   */
+  cancel(element: HTMLDivElement): string {
+    if (this.isDrawing) {
+      this.isDrawing = false;
+      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 '';
+  }
+
+  /**
+   * 处理 控制点/端点 选中回调
+   */
+  handleSelectedCallback(
+    evt: EventTypes.InteractionEventType,
+    annotation: PolylineLengthMeasurementAnnotation
+  ): void {
+    // 实现控制点选中逻辑
+    annotation.isSelected = true;
+    annotation.highlighted = true;
+
+    const { element } = evt.detail;
+    const viewportIdsToRender =
+      utilities.viewportFilters.getViewportIdsWithToolToRender(
+        element,
+        this.getToolName()
+      );
+
+    utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
+  }
+
+  /**
+   * 工具选中回调 - 当工具被激活时调用
+   */
+  toolSelectedCallback(
+    evt: EventTypes.InteractionEventType,
+    annotation: PolylineLengthMeasurementAnnotation
+  ): void {
+    const { element } = evt.detail;
+
+    // 如果 annotation 不为空,说明是点击已有的注释,激活修改模式
+    if (annotation) {
+      console.log('PolylineLengthMeasurementTool selected existing annotation');
+      this._activateModify(element);
+      return;
+    }
+
+    // 工具被激活时,总是进入绘制模式,允许开始新的绘制
+    // 完全重置所有绘制状态,确保全新的开始
+    this._deactivateDraw(element); // 先清理之前的绘制状态
+    this._deactivateModify(element); // 清理修改模式
+
+    this.isDrawing = false; // 重置绘制状态
+    this.drawingCompleted = false; // 允许新的点击操作
+    this.currentAnnotation = null;
+    this.drawingElement = null;
+
+    // 激活绘制模式,等待用户开始绘制
+    this._activateDraw(element);
+
+    console.log('PolylineLengthMeasurementTool activated for new drawing');
+  }
+  /**
+   * 激活时被调用
+   */
+  onSetToolActive(): void {
+    // 示例:重置相关状态
+    this.isDrawing = false;
+    this.editData = null;
+    this.drawingCompleted = false;//允许开始新的注解
+    // 示例:添加工具特定的初始化
+    // this.initializeCustomState();
+  }
+
+
+  /**
+   * 检查点是否靠近工具
+   */
+  isPointNearTool(
+    element: HTMLDivElement,
+    annotation: PolylineLengthMeasurementAnnotation,
+    canvasCoords: CoreTypes.Point2,
+    proximity: number
+  ): boolean {
+    const enabledElement = getEnabledElement(element);
+    if (!enabledElement) {
+      return false;
+    }
+    const { viewport } = enabledElement;
+
+    const points = annotation.data.handles.points;
+
+    // 检查是否靠近文本框
+    const textBox = annotation.data.handles.textBox;
+    if (textBox && Array.isArray(textBox) && textBox.length >= 2) {
+      if (this._isPointInTextBox(canvasCoords, textBox as CoreTypes.Point2)) {
+        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;
+      }
+    }
+
+    // 检查是否靠近线段
+    if (points.length >= 2) {
+      for (let i = 0; i < points.length - 1; i++) {
+        const p1Canvas = viewport.worldToCanvas(points[i]);
+        const p2Canvas = viewport.worldToCanvas(points[i + 1]);
+
+        const dist = this._distanceToSegment(canvasCoords, p1Canvas, p2Canvas);
+        if (dist < proximity) {
+          return true;
+        }
+      }
+    }
+
+    return false;
+  }
+
+  /**
+   * 检查点击是否在文本框内
+   */
+  private _isPointInTextBox(
+    canvasPoint: CoreTypes.Point2,
+    textBoxPosition: CoreTypes.Point2,
+    textWidth: number = 100,
+    textHeight: number = 40
+  ): boolean {
+    // 确保参数有效
+    if (!Array.isArray(canvasPoint) || canvasPoint.length < 2 ||
+      !Array.isArray(textBoxPosition) || textBoxPosition.length < 2) {
+      return false;
+    }
+
+    const [x, y] = canvasPoint;
+    const [tx, ty] = textBoxPosition;
+
+    const margin = 10;
+    return (
+      x >= tx - margin &&
+      x <= tx + textWidth + margin &&
+      y >= ty - margin &&
+      y <= ty + textHeight + margin
+    );
+  }
+
+  /**
+   * 计算点到线段的距离
+   */
+  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 _calculateDistance(point1: CoreTypes.Point3, point2: CoreTypes.Point3, viewport: CoreTypes.IViewport): number {
+    const canvas1 = viewport.worldToCanvas(point1);
+    const canvas2 = viewport.worldToCanvas(point2);
+    return Math.sqrt(
+      Math.pow(canvas2[0] - canvas1[0], 2) + Math.pow(canvas2[1] - canvas1[1], 2)
+    );
+  }
+
+  /**
+   * 将像素距离转换为mm
+   */
+  private _convertPixelsToMm(pixels: number, viewport: CoreTypes.IViewport): number {
+    // 这里需要根据DICOM元数据获取像素间距
+    // 暂时使用固定值,实际应该从图像元数据中获取
+    const pixelSpacing = 0.5; // mm per pixel,示例值
+    return pixels * pixelSpacing;
+  }
+
+  /**
+   * 更新缓存的统计数据
+   */
+  _updateCachedStats(
+    annotation: PolylineLengthMeasurementAnnotation,
+    enabledElement: CoreTypes.IEnabledElement
+  ): void {
+    const { viewport } = enabledElement;
+    const { data } = annotation;
+    const points = data.handles.points;
+
+    const targetId = this.getTargetId(viewport);
+    if (!targetId) {
+      return;
+    }
+
+    if (!data.cachedStats) {
+      data.cachedStats = {};
+    }
+
+    if (!data.cachedStats[targetId]) {
+      data.cachedStats[targetId] = {
+        totalLength: 0,
+        totalLengthMm: 0,
+        segmentLengths: []
+      };
+    }
+
+    const stats = data.cachedStats[targetId];
+    stats.segmentLengths = [];
+
+    let totalPixels = 0;
+    for (let i = 0; i < points.length - 1; i++) {
+      const segmentLength = this._calculateDistance(points[i], points[i + 1], viewport);
+      stats.segmentLengths.push(segmentLength);
+      totalPixels += segmentLength;
+    }
+
+    stats.totalLength = totalPixels;
+    stats.totalLengthMm = this._convertPixelsToMm(totalPixels, viewport);
+  }
+
+  /**
+   * 激活绘制模式
+   */
+  _activateDraw(element: HTMLDivElement): void {
+    this.drawingElement = element; // 存储当前绘制模式的元素
+    this.isDrawing = true;
+    element.addEventListener(
+      'CORNERSTONE_TOOLS_MOUSE_CLICK',
+      this._mouseClickDrawCallback as EventListener
+    );
+    element.addEventListener(
+      'dblclick',
+      this._mouseDoubleClickDrawCallback as EventListener
+    );
+    // 添加鼠标移动监听用于预览
+    element.addEventListener(
+      'CORNERSTONE_TOOLS_MOUSE_MOVE',
+      this._mouseMoveDrawCallback as EventListener
+    );
+  }
+
+  /**
+   * 取消激活绘制模式
+   */
+  _deactivateDraw(element?: HTMLDivElement): void {
+    const targetElement = element || this.drawingElement;
+    if (!targetElement) return;
+
+    this.isDrawing = false;
+    targetElement.removeEventListener(
+      'CORNERSTONE_TOOLS_MOUSE_CLICK',
+      this._mouseClickDrawCallback as EventListener
+    );
+    targetElement.removeEventListener(
+      'dblclick',
+      this._mouseDoubleClickDrawCallback as EventListener
+    );
+    // 清理鼠标移动监听器
+    targetElement.removeEventListener(
+      'CORNERSTONE_TOOLS_MOUSE_MOVE',
+      this._mouseMoveDrawCallback as EventListener
+    );
+    this.drawingElement = null; // 清理
+    this.previewMousePosition = null; // 清理预览位置
+  }
+
+  /**
+   * 激活修改模式
+   */
+  _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
+    );
+  }
+
+  /**
+   * 鼠标点击回调 - 用于绘制模式,添加新的点
+   */
+  _mouseClickDrawCallback = (evt: EventTypes.InteractionEventType): void => {
+    if (!this.isDrawing || this.drawingCompleted) {
+      return;
+    }
+
+    const eventDetail = evt.detail;
+    const { element, currentPoints } = eventDetail;
+    const worldPos = currentPoints.world;
+
+    const enabledElement = getEnabledElement(element);
+    if (!enabledElement) {
+      return;
+    }
+
+    // 如果还没有当前注解,创建第一个
+    if (!this.currentAnnotation) {
+      // 创建新的注解
+      const camera = enabledElement.viewport.getCamera();
+      const { viewPlaneNormal, viewUp } = camera;
+      if (viewPlaneNormal === undefined || viewUp === undefined) {
+        return;
+      }
+
+      this.currentAnnotation = {
+        invalidated: true,
+        highlighted: false,
+        metadata: {
+          viewPlaneNormal: [...viewPlaneNormal] as CoreTypes.Point3,
+          viewUp: [...viewUp] as CoreTypes.Point3,
+          FrameOfReferenceUID: enabledElement.viewport.getFrameOfReferenceUID(),
+          referencedImageId: enabledElement.viewport.getCurrentImageId?.() || '',
+          toolName: PolylineLengthMeasurementTool.toolName,
+        },
+        data: {
+          label: '',
+          handles: {
+            points: [worldPos],
+            activeHandleIndex: 0,
+          },
+          cachedStats: {},
+        },
+      } as PolylineLengthMeasurementAnnotation;
+
+      // 添加到临时状态以便渲染
+      annotation.state.addAnnotation(this.currentAnnotation, element);
+
+      console.log('Created first point for polyline');
+    } else {
+      // 添加新点到现有注解
+      this.currentAnnotation.data.handles.points.push(worldPos);
+      this.currentAnnotation.data.handles.activeHandleIndex = this.currentAnnotation.data.handles.points.length - 1;
+
+      // 更新统计数据
+      this._updateCachedStats(this.currentAnnotation, enabledElement);
+
+      console.log(`Added point ${this.currentAnnotation.data.handles.points.length} to polyline`);
+    }
+
+    // 触发渲染更新
+    const viewportIdsToRender = utilities.viewportFilters.getViewportIdsWithToolToRender(
+      element,
+      this.getToolName()
+    );
+    utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
+
+    evt.preventDefault();
+    evt.stopPropagation();
+  };
+
+  /**
+   * 鼠标移动回调 - 用于绘制模式,更新预览位置
+   */
+  _mouseMoveDrawCallback = (evt: EventTypes.InteractionEventType): void => {
+    if (!this.isDrawing) {
+      return;
+    }
+
+    const eventDetail = evt.detail;
+    const canvasCoords = eventDetail.currentPoints?.canvas;
+
+    if (canvasCoords) {
+      this.previewMousePosition = [...canvasCoords];
+
+      // 触发重新渲染以显示预览线
+      const viewportIdsToRender = utilities.viewportFilters.getViewportIdsWithToolToRender(
+        eventDetail.element,
+        this.getToolName()
+      );
+      utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
+    }
+  };
+
+  /**
+   * 鼠标双击回调 - 用于绘制模式,在双击位置添加最后一个点并结束绘制
+   */
+  _mouseDoubleClickDrawCallback = (evt: EventTypes.InteractionEventType): void => {
+    if (!this.isDrawing || !this.currentAnnotation) {
+      return;
+    }
+
+    const eventDetail = evt.detail;
+    const element = eventDetail.element || this.drawingElement;
+    const currentPoints = eventDetail.currentPoints;
+
+    if (!element) {
+      return;
+    }
+
+    // 获取双击位置,支持多种情况
+    let worldPos: CoreTypes.Point3 | undefined;
+
+    if (currentPoints?.world) {
+      // 优先使用 world 坐标
+      worldPos = currentPoints.world;
+    } else if (currentPoints?.canvas) {
+      // 如果没有 world 坐标但有 canvas 坐标,转换得到 world 坐标
+      const enabledElement = getEnabledElement(element);
+      if (enabledElement) {
+        worldPos = enabledElement.viewport.canvasToWorld(currentPoints.canvas);
+      }
+    } else {
+      // 如果 CornerstoneJS 没有提供坐标,使用原生 DOM 事件坐标
+      // __类型转换__:将 `evt` 转换为 `MouseEvent`
+      const mouseEvent = evt as unknown as MouseEvent;
+
+      // __获取鼠标坐标__:使用 `clientX`, `clientY`
+      const clientX = mouseEvent.clientX;
+      const clientY = mouseEvent.clientY;
+
+      // __计算 canvas 坐标__:减去 canvas 的偏移量
+      const rect = element.getBoundingClientRect();
+      const canvasX = clientX - rect.left;
+      const canvasY = clientY - rect.top;
+
+      const canvasPos: CoreTypes.Point2 = [canvasX, canvasY];
+
+      // __转换为 world 坐标__:使用 `viewport.canvasToWorld()`
+      const enabledElement = getEnabledElement(element);
+      if (enabledElement) {
+        worldPos = enabledElement.viewport.canvasToWorld(canvasPos);
+      }
+    }
+
+    if (!worldPos) {
+      // 如果仍然没有位置信息,记录警告但不中断流程
+      console.warn('Unable to get double-click position for polyline');
+      return;
+    }
+
+    // 先在双击位置添加最后一个点
+    this.currentAnnotation.data.handles.points.push(worldPos);
+    this.currentAnnotation.data.handles.activeHandleIndex = this.currentAnnotation.data.handles.points.length - 1;
+
+    // 更新统计数据
+    const enabledElement = getEnabledElement(element);
+    if (enabledElement) {
+      this._updateCachedStats(this.currentAnnotation, enabledElement);
+    }
+
+    // 然后检查点数决定是否结束绘制
+    if (this.currentAnnotation.data.handles.points.length >= 2) {
+      // 结束绘制模式,激活修改模式
+      this._deactivateDraw(element);
+      this._activateModify(element);
+      this.isDrawing = false;
+      this.drawingCompleted = true; // 标记绘制已完成,防止再次添加点
+      this.currentAnnotation = null; // 完全清理当前注解引用,确保下次激活时全新的开始
+
+      // 取消所有注解的选中状态,确保下次激活时 annotation 参数为 null
+      const annotations = annotation.state.getAnnotations(this.getToolName(), element);
+      if (annotations) {
+        for (const ann of annotations) {
+          const customAnn = ann as PolylineLengthMeasurementAnnotation;
+          customAnn.isSelected = false;
+          customAnn.highlighted = false;
+        }
+      }
+
+      console.log('Finished drawing polyline');
+    } else {
+      // 如果点数仍然少于2个,删除这个注解
+      annotation.state.removeAnnotation(this.currentAnnotation.annotationUID!);
+      this.currentAnnotation = null;
+    }
+
+    // 触发渲染更新
+    const viewportIdsToRender = utilities.viewportFilters.getViewportIdsWithToolToRender(
+      element,
+      this.getToolName()
+    );
+    utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
+
+    evt.preventDefault();
+    evt.stopPropagation();
+  };
+
+  /**
+   * 键盘按下回调 - 用于删除功能
+   */
+  _keyDownCallback = (evt: KeyboardEvent): void => {
+    const element = evt.currentTarget as HTMLDivElement;
+
+    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 PolylineLengthMeasurementAnnotation).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();
+    }
+  };
+
+  /**
+   * 鼠标移动回调 - 用于修改模式,处理悬停检测
+   */
+  _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 PolylineLengthMeasurementAnnotation;
+      const textBoxPosition = customAnn.data.handles.textBox;
+
+      if (textBoxPosition && this._isPointInTextBox(canvasCoords, textBoxPosition)) {
+        element.style.cursor = 'pointer';
+        customAnn.highlighted = true;
+        isHovering = true;
+        break;
+      }
+    }
+
+    // 如果没有悬停在文本框上,检查是否悬停在手柄或线段上
+    if (!isHovering) {
+      for (const ann of annotations) {
+        const customAnn = ann as PolylineLengthMeasurementAnnotation;
+
+        // 检查是否靠近手柄
+        const handle = this.getHandleNearImagePoint(element, customAnn, canvasCoords, 6);
+        if (handle) {
+          element.style.cursor = cursorUrl;
+          customAnn.highlighted = true;
+          isHovering = true;
+          break;
+        }
+
+        // 检查是否靠近线段
+        if (this.isPointNearTool(element, customAnn, canvasCoords, 10)) {
+          element.style.cursor = cursorUrl;
+          customAnn.highlighted = true;
+          isHovering = true;
+          break;
+        }
+      }
+    }
+
+    // 如果没有悬停在任何地方,重置高亮
+    if (!isHovering) {
+      for (const ann of annotations) {
+        const customAnn = ann as PolylineLengthMeasurementAnnotation;
+        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 PolylineLengthMeasurementAnnotation;
+      const textBoxPosition = customAnn.data.handles.textBox;
+
+      if (textBoxPosition && this._isPointInTextBox(canvasCoords, textBoxPosition)) {
+        const viewportIdsToRender =
+          utilities.viewportFilters.getViewportIdsWithToolToRender(
+            element,
+            this.getToolName()
+          );
+
+        const textBoxOffset: CoreTypes.Point2 = [
+          canvasCoords[0] - textBoxPosition[0],
+          canvasCoords[1] - textBoxPosition[1],
+        ];
+
+        this.editData = {
+          annotation: customAnn,
+          viewportIdsToRender,
+          hasMoved: false,
+          textBoxBeingMoved: true,
+          textBoxOffset: textBoxOffset,
+        };
+
+        customAnn.isSelected = true;
+        customAnn.highlighted = true;
+        utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
+
+        evt.preventDefault();
+        evt.stopPropagation();
+        return;
+      }
+    }
+
+    // 如果没有点击文本框,再查找最近的手柄
+    for (const ann of annotations) {
+      const customAnn = ann as PolylineLengthMeasurementAnnotation;
+      const handle = this.getHandleNearImagePoint(
+        element,
+        customAnn,
+        canvasCoords,
+        6
+      );
+
+      if (handle) {
+        const viewportIdsToRender =
+          utilities.viewportFilters.getViewportIdsWithToolToRender(
+            element,
+            this.getToolName()
+          );
+
+        this.editData = {
+          annotation: customAnn,
+          viewportIdsToRender,
+          handleIndex: customAnn.data.handles.activeHandleIndex || 0,
+          hasMoved: false,
+        };
+
+        customAnn.isSelected = true;
+        customAnn.highlighted = true;
+
+        utilities.triggerAnnotationRenderForViewportIds(
+          viewportIdsToRender
+        );
+
+        evt.preventDefault();
+        evt.stopPropagation();
+        return;
+      }
+    }
+
+    // 如果没有找到手柄,检查是否点击在工具的其他位置(线段上)
+    for (const ann of annotations) {
+      const customAnn = ann as PolylineLengthMeasurementAnnotation;
+      if (this.isPointNearTool(element, customAnn, canvasCoords, 10)) {
+        const viewportIdsToRender =
+          utilities.viewportFilters.getViewportIdsWithToolToRender(
+            element,
+            this.getToolName()
+          );
+
+        const points = customAnn.data.handles.points;
+        if (points.length >= 2) {
+          const midPoint = calculateMidpoint(points[0], points[1]);
+          const midCanvas = viewport.worldToCanvas(midPoint);
+          const wholeToolOffset: CoreTypes.Point2 = [
+            canvasCoords[0] - midCanvas[0],
+            canvasCoords[1] - midCanvas[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 PolylineLengthMeasurementAnnotation;
+      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, textBoxBeingMoved, movingWholeTool } = this.editData;
+    const customAnn = ann as PolylineLengthMeasurementAnnotation;
+    const { data } = customAnn;
+
+    // 如果正在拖拽文本框
+    if (textBoxBeingMoved && this.editData.textBoxOffset) {
+      const newTextBoxPosition: CoreTypes.Point2 = [
+        canvasCoords[0] - this.editData.textBoxOffset[0],
+        canvasCoords[1] - this.editData.textBoxOffset[1],
+      ];
+
+      data.handles.textBox = newTextBoxPosition;
+      this.editData.hasMoved = true;
+
+      utilities.triggerAnnotationRenderForViewportIds(
+        viewportIdsToRender
+      );
+
+      evt.preventDefault();
+      evt.stopPropagation();
+      return;
+    }
+
+    // 如果正在移动整个工具
+    if (movingWholeTool && this.editData.wholeToolOffset) {
+      const newMidCanvas: CoreTypes.Point2 = [
+        canvasCoords[0] - this.editData.wholeToolOffset[0],
+        canvasCoords[1] - this.editData.wholeToolOffset[1],
+      ];
+      const newMidWorld = enabledElement.viewport.canvasToWorld(newMidCanvas);
+
+      const points = data.handles.points;
+      if (points.length >= 2) {
+        const oldMid = calculateMidpoint(points[0], points[1]);
+        const oldMidCanvas = enabledElement.viewport.worldToCanvas(oldMid);
+        const offset = vectorSubtract(newMidWorld, oldMid);
+        const canvasOffset: CoreTypes.Point2 = [
+          newMidCanvas[0] - oldMidCanvas[0],
+          newMidCanvas[1] - oldMidCanvas[1],
+        ];
+
+        // 移动所有点
+        for (let i = 0; i < points.length; i++) {
+          points[i] = vectorAdd(points[i], offset);
+        }
+
+        // 移动文本框位置 (使用canvas坐标偏移)
+        if (data.handles.textBox && Array.isArray(data.handles.textBox) && data.handles.textBox.length >= 2) {
+          data.handles.textBox[0] += canvasOffset[0];
+          data.handles.textBox[1] += canvasOffset[1];
+        }
+
+        this._updateCachedStats(customAnn, enabledElement);
+        this.editData.hasMoved = true;
+
+        utilities.triggerAnnotationRenderForViewportIds(
+          viewportIdsToRender
+        );
+
+        evt.preventDefault();
+        evt.stopPropagation();
+        return;
+      }
+    }
+
+    // 否则处理手柄拖拽
+    const worldPos = currentPoints.world;
+    const activeHandleIndex = data.handles.activeHandleIndex;
+
+    if (activeHandleIndex !== null && activeHandleIndex >= 0 && activeHandleIndex < data.handles.points.length) {
+      data.handles.points[activeHandleIndex] = worldPos;
+      this._updateCachedStats(customAnn, enabledElement);
+      this.editData.hasMoved = true;
+
+      utilities.triggerAnnotationRenderForViewportIds(
+        viewportIdsToRender
+      );
+
+      evt.preventDefault();
+      evt.stopPropagation();
+    }
+  };
+
+  /**
+   * 鼠标释放回调 - 用于修改模式
+   */
+  _mouseUpModifyCallback = (evt: EventTypes.InteractionEventType): void => {
+    if (!this.editData) {
+      return;
+    }
+
+    const { annotation: ann, hasMoved, movingWholeTool } = this.editData;
+    const customAnn = ann as PolylineLengthMeasurementAnnotation;
+
+    customAnn.data.handles.activeHandleIndex = null;
+
+    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();
+  };
+
+  /**
+   * 获取靠近图像点的手柄
+   */
+  getHandleNearImagePoint(
+    element: HTMLDivElement,
+    annotation: PolylineLengthMeasurementAnnotation,
+    canvasCoords: CoreTypes.Point2,
+    proximity: number
+  ): Types.ToolHandle | undefined {
+    const enabledElement = getEnabledElement(element);
+    if (!enabledElement) {
+      return undefined;
+    }
+    const { viewport } = enabledElement;
+
+    const points = annotation.data.handles.points;
+    const handleProximity = Math.max(proximity, 15);
+
+    for (let i = 0; i < points.length; i++) {
+      const point = points[i];
+      const canvasPoint = viewport.worldToCanvas(point);
+      const distance = Math.sqrt(
+        Math.pow(canvasPoint[0] - canvasCoords[0], 2) +
+        Math.pow(canvasPoint[1] - canvasCoords[1], 2)
+      );
+
+      if (distance < handleProximity) {
+        annotation.data.handles.activeHandleIndex = i;
+        return {
+          worldPosition: point,
+        } as Types.ToolHandle;
+      }
+    }
+
+    annotation.data.handles.activeHandleIndex = null;
+    return undefined;
+  }
+
+  /**
+   * 渲染注解
+   */
+  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 PolylineLengthMeasurementAnnotation;
+      const { annotationUID, data } = annotation;
+      const points = data.handles.points;
+
+      // 如果点数为0,跳过
+      if (points.length === 0) {
+        continue;
+      }
+
+      const targetId = this.getTargetId(viewport);
+      const cachedStats = targetId ? data.cachedStats?.[targetId] : undefined;
+
+      // 转换所有点为 canvas 坐标
+      const canvasPoints = points.map((p) => viewport.worldToCanvas(p));
+
+      // 绘制折线(需要至少2个点)
+      if (annotationUID && points.length >= 2) {
+        for (let j = 0; j < canvasPoints.length - 1; j++) {
+          const lineUID = `${annotationUID}-line-${j}`;
+          const lineOptions = {
+            color: 'rgb(0, 255, 0)',
+            width: 2,
+          };
+
+          drawLineSvg(
+            svgDrawingHelper,
+            annotationUID,
+            lineUID,
+            canvasPoints[j],
+            canvasPoints[j + 1],
+            lineOptions
+          );
+        }
+      }
+
+      // 绘制长度文本(需要至少2个点且有统计数据)
+      if (cachedStats?.totalLengthMm !== undefined && annotationUID && points.length >= 2) {
+        const textLines = [`长度: ${cachedStats.totalLengthMm.toFixed(2)} mm`];
+        const textUID = `${annotationUID}-length-text`;
+        const textBoxPosition: CoreTypes.Point2 =
+          (data.handles.textBox &&
+            Array.isArray(data.handles.textBox) &&
+            data.handles.textBox.length >= 2)
+            ? data.handles.textBox
+            : [
+              canvasPoints[0][0] + 10,
+              canvasPoints[0][1] - 10,
+            ];
+
+        console.log('textBoxPosition:', textBoxPosition);
+        // 保存文本框位置到注解数据中
+        data.handles.textBox = textBoxPosition;
+        // 在文本框绘制前添加
+        const CORACanvas = canvasPoints[0];
+        console.log('CORACanvas:', CORACanvas);
+        console.log('CORACanvas[0]:', CORACanvas[0]);
+        console.log('CORACanvas[1]:', CORACanvas[1]);
+        console.log('is NaN:', Number.isNaN(CORACanvas[0]), Number.isNaN(CORACanvas[1]));
+
+        drawLinkedTextBox(
+          svgDrawingHelper,
+          annotationUID,
+          textUID,
+          textLines,
+          textBoxPosition,
+          [canvasPoints[0] as CoreTypes.Point2],
+          {},
+          {
+            color: 'rgb(0, 255, 0)',
+          }
+        );
+      }
+
+      // 绘制手柄点(至少有1个点就绘制)
+      if (annotationUID && points.length >= 1) {
+        const handleGroupUID = '0';
+        const isSelected = annotation.isSelected || annotation.highlighted;
+        const handleRadius = isSelected ? 12 : 8;
+
+        drawHandles(
+          svgDrawingHelper,
+          annotationUID,
+          handleGroupUID,
+          canvasPoints,
+          {
+            color: 'rgb(255, 255, 255)',
+            handleRadius: handleRadius,
+          }
+        );
+      }
+
+      renderStatus = true;
+    }
+
+    // 绘制预览线(只在绘制模式且有鼠标位置时)
+    if (this.isDrawing && this.previewMousePosition && this.currentAnnotation &&
+        this.currentAnnotation.data.handles.points.length > 0) {
+
+      const lastPoint = this.currentAnnotation.data.handles.points[
+        this.currentAnnotation.data.handles.points.length - 1
+      ];
+      const lastCanvasPoint = enabledElement.viewport.worldToCanvas(lastPoint);
+
+      // 绘制虚线预览
+      const previewLineUID = 'preview-line';
+      drawLineSvg(
+        svgDrawingHelper,
+        'preview',  // 使用固定UID
+        previewLineUID,
+        lastCanvasPoint,
+        this.previewMousePosition,
+        {
+          color: 'rgba(0, 255, 0, 0.7)',  // 半透明绿色
+          width: 2,
+          lineDash: [8, 4],  // 虚线样式:8px实线,4px空白
+        }
+      );
+    }
+
+    return renderStatus;
+  };
+}

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

@@ -37,6 +37,8 @@ import StackViewer, {
   activateFindMidpointMeasurement,
   activateVerticalTiltMeasurement,
   activateHorizontalTiltMeasurement,
+  activatePolygonLengthMeasurement,
+  activatePolylineLengthMeasurement,
 } from './viewers/stack.image.viewer';
 import { useSelector, useDispatch } from 'react-redux';
 import { getDcmImageUrl } from '@/API/bodyPosition';
@@ -148,6 +150,16 @@ const MEASUREMENT_TOOL_CONFIGS: Record<string, MeasurementToolConfig> = {
     activateFunction: activateHorizontalTiltMeasurement,
     logPrefix: 'HorizontalTilt',
   },
+  '多边形长度测量': {
+    toolName: 'PolygonLengthMeasurementTool',
+    activateFunction: activatePolygonLengthMeasurement,
+    logPrefix: 'PolygonLength',
+  },
+  '拆线长度测量': {
+    toolName: 'PolylineLengthMeasurementTool',
+    activateFunction: activatePolylineLengthMeasurement,
+    logPrefix: 'PolylineLength',
+  },
 };
 
 const setup = () => {
@@ -604,6 +616,8 @@ const ViewerContainer: React.FC<ViewerContainerProps> = ({ imageUrls }) => {
             MeasurementToolManager.clearFindMidpointMeasurementsForViewports(selectedViewportIds);
             MeasurementToolManager.clearVerticalTiltMeasurementsForViewports(selectedViewportIds);
             MeasurementToolManager.clearHorizontalTiltMeasurementsForViewports(selectedViewportIds);
+            MeasurementToolManager.clearPolygonLengthMeasurementsForViewports(selectedViewportIds);
+            MeasurementToolManager.clearPolylineLengthMeasurementsForViewports(selectedViewportIds);
           }
           console.log('Clearing All Measurements from MeasurementPanel');
           break;
@@ -658,7 +672,9 @@ const ViewerContainer: React.FC<ViewerContainerProps> = ({ imageUrls }) => {
         case '找中线':
         case '找中点':
         case '直线垂直倾斜度':
-        case '直线水平倾斜度': {
+        case '直线水平倾斜度':
+        case '多边形长度测量':
+        case '拆线长度测量': {
           const config = MEASUREMENT_TOOL_CONFIGS[measurementAction];
           if (config) {
             activateMeasurementTool(config, selectedViewportIds);

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

@@ -27,6 +27,8 @@ import { boolean } from 'zod';
 import { EVENTS } from '@cornerstonejs/core';
 import { useSelector } from 'react-redux';
 import { selectOverlayEnabled } from '@/states/view/dicomOverlaySlice';
+import PolygonLengthMeasurementTool from '@/components/measures/PolygonLengthMeasurementTool';
+import PolylineLengthMeasurementTool from '@/components/measures/PolylineLengthMeasurementTool';
 
 const {
   MagnifyTool,
@@ -211,6 +213,8 @@ function registerTools(viewportId, renderingEngineId) {
   toolGroup.addTool(VerticalTiltMeasurementTool.toolName); // 添加直线垂直倾斜度测量工具
   toolGroup.addTool(HorizontalTiltMeasurementTool.toolName); // 添加直线水平倾斜度测量工具
   toolGroup.addTool(DicomOverlayTool.toolName); // 添加DICOM四角信息显示工具
+  toolGroup.addTool(PolygonLengthMeasurementTool.toolName); // 添加多边形长度测量工具
+  toolGroup.addTool(PolylineLengthMeasurementTool.toolName); // 添加拆线长度测量工具
 
   // 设置默认工具状态
   setupDefaultToolStates(toolGroup);
@@ -274,6 +278,8 @@ function setupDefaultToolStates(toolGroup: cornerstoneTools.Types.IToolGroup) {
   toolGroup.setToolPassive(FindMidpointMeasurementTool.toolName);
   toolGroup.setToolPassive(VerticalTiltMeasurementTool.toolName);
   toolGroup.setToolPassive(HorizontalTiltMeasurementTool.toolName);
+  toolGroup.setToolPassive(PolygonLengthMeasurementTool.toolName);
+  toolGroup.setToolPassive(PolylineLengthMeasurementTool.toolName);
 }
 export function addLMark(currentViewportId: string): void {
   // Implement the logic to add an L mark
@@ -1287,6 +1293,58 @@ export function clearHorizontalTiltMeasurements(viewportId: string): boolean {
   console.log(`[clearHorizontalTiltMeasurements] Clearing HorizontalTilt measurements for viewport: ${viewportId}`);
   return MeasurementToolManager.clearHorizontalTiltMeasurements(viewportId);
 }
+
+// ==================== 多边形长度测量相关函数 ====================
+
+/**
+ * 激活多边形长度测量工具
+ */
+export function activatePolygonLengthMeasurement(viewportId: string): boolean {
+  console.log(`[activatePolygonLengthMeasurement] Activating PolygonLength measurement for viewport: ${viewportId}`);
+  return MeasurementToolManager.activatePolygonLengthMeasurementTool(viewportId);
+}
+
+/**
+ * 停用多边形长度测量工具
+ */
+export function deactivatePolygonLengthMeasurement(viewportId: string): boolean {
+  console.log(`[deactivatePolygonLengthMeasurement] Deactivating PolygonLength measurement for viewport: ${viewportId}`);
+  return MeasurementToolManager.deactivatePolygonLengthMeasurementTool(viewportId);
+}
+
+/**
+ * 清除多边形长度测量标注
+ */
+export function clearPolygonLengthMeasurements(viewportId: string): boolean {
+  console.log(`[clearPolygonLengthMeasurements] Clearing PolygonLength measurements for viewport: ${viewportId}`);
+  return MeasurementToolManager.clearPolygonLengthMeasurements(viewportId);
+}
+
+// ==================== 拆线长度测量相关函数 ====================
+
+/**
+ * 激活拆线长度测量工具
+ */
+export function activatePolylineLengthMeasurement(viewportId: string): boolean {
+  console.log(`[activatePolylineLengthMeasurement] Activating PolylineLength measurement for viewport: ${viewportId}`);
+  return MeasurementToolManager.activatePolylineLengthMeasurementTool(viewportId);
+}
+
+/**
+ * 停用拆线长度测量工具
+ */
+export function deactivatePolylineLengthMeasurement(viewportId: string): boolean {
+  console.log(`[deactivatePolylineLengthMeasurement] Deactivating PolylineLength measurement for viewport: ${viewportId}`);
+  return MeasurementToolManager.deactivatePolylineLengthMeasurementTool(viewportId);
+}
+
+/**
+ * 清除拆线长度测量标注
+ */
+export function clearPolylineLengthMeasurements(viewportId: string): boolean {
+  console.log(`[clearPolylineLengthMeasurements] Clearing PolylineLength measurements for viewport: ${viewportId}`);
+  return MeasurementToolManager.clearPolylineLengthMeasurements(viewportId);
+}
 export class ImageLoadError extends Error {
   constructor(message: string, failedImageIds: string[]) {
     super(message);

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

@@ -22,6 +22,8 @@ export type MeasurementAction =
   | '找中点'
   | '直线垂直倾斜度'
   | '直线水平倾斜度'
+  | '多边形长度测量'
+  | '拆线长度测量'
   | null;
 
 // 测量结果类型

+ 4 - 0
src/utils/cornerstoneToolsSetup.ts

@@ -14,7 +14,9 @@ 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 PolygonLengthMeasurementTool from '@/components/measures/PolygonLengthMeasurementTool';
 import DicomOverlayTool from '@/components/overlay/DicomOverlayTool';
+import PolylineLengthMeasurementTool from '@/components/measures/PolylineLengthMeasurementTool';
 
 const {
   MagnifyTool,
@@ -69,6 +71,8 @@ export function registerGlobalTools(): void {
     cornerstoneTools.addTool(FindMidpointMeasurementTool); // 添加找中点测量工具
     cornerstoneTools.addTool(VerticalTiltMeasurementTool); // 添加直线垂直倾斜度测量工具
     cornerstoneTools.addTool(HorizontalTiltMeasurementTool); // 添加直线水平倾斜度测量工具
+    cornerstoneTools.addTool(PolygonLengthMeasurementTool); // 添加多边形长度测量工具
+    cornerstoneTools.addTool(PolylineLengthMeasurementTool); // 添加拆线长度测量工具
     cornerstoneTools.addTool(DicomOverlayTool); // 添加DICOM四角信息显示工具
 
     toolsRegistered = true;

+ 207 - 0
src/utils/measurementToolManager.ts

@@ -15,6 +15,8 @@ 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 PolygonLengthMeasurementTool from '@/components/measures/PolygonLengthMeasurementTool';
+import PolylineLengthMeasurementTool from '@/components/measures/PolylineLengthMeasurementTool';
 
 const {
   ToolGroupManager,
@@ -3211,4 +3213,209 @@ viewport.render();
   static clearHorizontalTiltMeasurementsForViewports(viewportIds: string[]): boolean[] {
     return viewportIds.map((viewportId) => this.clearHorizontalTiltMeasurements(viewportId));
   }
+
+  // ==================== PolygonLengthMeasurement(多边形长度测量)工具 ====================
+
+  /**
+   * 激活多边形长度测量工具
+   */
+  static activatePolygonLengthMeasurementTool(viewportId: string): boolean {
+    const toolGroup = this.getToolGroup(viewportId);
+    if (!toolGroup) return false;
+
+    try {
+      // 停用其他可能冲突的工具
+      toolGroup.setToolPassive(WindowLevelTool.toolName, { removeAllBindings: true });
+      toolGroup.setToolPassive(MagnifyTool.toolName, { removeAllBindings: true });
+      toolGroup.setToolPassive(LengthTool.toolName, { removeAllBindings: true });
+      toolGroup.setToolPassive(AngleTool.toolName, { removeAllBindings: true });
+      toolGroup.setToolPassive(TibialPlateauAngleTool.toolName, { removeAllBindings: true });
+      toolGroup.setToolPassive(DARAMeasurementTool.toolName, { removeAllBindings: true });
+      toolGroup.setToolPassive(HipDIMeasurementTool.toolName, { removeAllBindings: true });
+      toolGroup.setToolPassive(HipNHAAngleMeasurementTool.toolName, { removeAllBindings: true });
+      toolGroup.setToolPassive(VHSMeasurementTool.toolName, { removeAllBindings: true });
+      toolGroup.setToolPassive(TPLOMeasurementTool.toolName, { removeAllBindings: true });
+      toolGroup.setToolPassive(TTAMeasurementTool.toolName, { removeAllBindings: true });
+      toolGroup.setToolPassive(CBLOMeasurementTool.toolName, { removeAllBindings: true });
+      toolGroup.setToolPassive(HipCoverageMeasurementTool.toolName, { removeAllBindings: true });
+      toolGroup.setToolPassive(HipDorsalCoverageTool.toolName, { removeAllBindings: true });
+      toolGroup.setToolPassive(CircleCenterMeasurementTool.toolName, { removeAllBindings: true });
+      toolGroup.setToolPassive(MidlineMeasurementTool.toolName, { removeAllBindings: true });
+      toolGroup.setToolPassive(FindMidpointMeasurementTool.toolName, { removeAllBindings: true });
+      toolGroup.setToolPassive(VerticalTiltMeasurementTool.toolName, { removeAllBindings: true });
+      toolGroup.setToolPassive(HorizontalTiltMeasurementTool.toolName, { removeAllBindings: true });
+
+      // 激活多边形长度测量工具
+      toolGroup.setToolActive(PolygonLengthMeasurementTool.toolName, {
+        bindings: [{ mouseButton: MouseBindings.Primary }],
+      });
+
+      // 获取工具实例并激活绘制模式(因为多边形工具是绘制类型的)
+      const toolInstance = toolGroup.getToolInstance(PolygonLengthMeasurementTool.toolName) as PolygonLengthMeasurementTool;
+      const viewport = cornerstone.getEnabledElementByViewportId(viewportId)?.viewport;
+      if (toolInstance && viewport.element) {
+        toolInstance._activateDraw(viewport.element);
+      }
+
+      console.log(`[MeasurementToolManager] PolygonLengthMeasurement tool activated for viewport: ${viewportId}`);
+      return true;
+    } catch (error) {
+      console.error(`[MeasurementToolManager] Error activating PolygonLengthMeasurement tool:`, error);
+      return false;
+    }
+  }
+
+  /**
+   * 停用多边形长度测量工具
+   */
+  static deactivatePolygonLengthMeasurementTool(viewportId: string): boolean {
+    const toolGroup = this.getToolGroup(viewportId);
+    if (!toolGroup) return false;
+
+    try {
+      toolGroup.setToolPassive(PolygonLengthMeasurementTool.toolName, { removeAllBindings: true });
+      console.log(`[MeasurementToolManager] PolygonLengthMeasurement tool deactivated for viewport: ${viewportId}`);
+      return true;
+    } catch (error) {
+      console.error(`[MeasurementToolManager] Error deactivating PolygonLengthMeasurement tool:`, error);
+      return false;
+    }
+  }
+
+  /**
+   * 清除指定 viewport 的所有多边形长度测量标注
+   */
+  static clearPolygonLengthMeasurements(viewportId: string): boolean {
+    try {
+      const viewport = cornerstone.getEnabledElementByViewportId(viewportId)?.viewport;
+      if (!viewport) return false;
+
+      const annotations = cornerstoneTools.annotation.state.getAnnotations(
+        PolygonLengthMeasurementTool.toolName,
+        viewport.element
+      );
+
+      let removedCount = 0;
+      annotations.forEach((annotation) => {
+        if (annotation.annotationUID) {
+          cornerstoneTools.annotation.state.removeAnnotation(annotation.annotationUID);
+          removedCount++;
+        }
+      });
+
+      viewport.render();
+      console.log(`[MeasurementToolManager] Cleared ${removedCount} PolygonLength measurements for viewport: ${viewportId}`);
+      return true;
+    } catch (error) {
+      console.error(`[MeasurementToolManager] Error clearing PolygonLength measurements:`, error);
+      return false;
+    }
+  }
+
+  static clearPolygonLengthMeasurementsForViewports(viewportIds: string[]): boolean[] {
+    return viewportIds.map((viewportId) => this.clearPolygonLengthMeasurements(viewportId));
+  }
+
+  // ==================== PolylineLengthMeasurement(拆线长度测量)工具 ====================
+
+  /**
+   * 激活拆线长度测量工具
+   */
+  static activatePolylineLengthMeasurementTool(viewportId: string): boolean {
+    const toolGroup = this.getToolGroup(viewportId);
+    if (!toolGroup) return false;
+
+    try {
+      // 停用其他可能冲突的工具
+      toolGroup.setToolPassive(WindowLevelTool.toolName, { removeAllBindings: true });
+      toolGroup.setToolPassive(MagnifyTool.toolName, { removeAllBindings: true });
+      toolGroup.setToolPassive(LengthTool.toolName, { removeAllBindings: true });
+      toolGroup.setToolPassive(AngleTool.toolName, { removeAllBindings: true });
+      toolGroup.setToolPassive(TibialPlateauAngleTool.toolName, { removeAllBindings: true });
+      toolGroup.setToolPassive(DARAMeasurementTool.toolName, { removeAllBindings: true });
+      toolGroup.setToolPassive(HipDIMeasurementTool.toolName, { removeAllBindings: true });
+      toolGroup.setToolPassive(HipNHAAngleMeasurementTool.toolName, { removeAllBindings: true });
+      toolGroup.setToolPassive(VHSMeasurementTool.toolName, { removeAllBindings: true });
+      toolGroup.setToolPassive(TPLOMeasurementTool.toolName, { removeAllBindings: true });
+      toolGroup.setToolPassive(TTAMeasurementTool.toolName, { removeAllBindings: true });
+      toolGroup.setToolPassive(CBLOMeasurementTool.toolName, { removeAllBindings: true });
+      toolGroup.setToolPassive(HipCoverageMeasurementTool.toolName, { removeAllBindings: true });
+      toolGroup.setToolPassive(HipDorsalCoverageTool.toolName, { removeAllBindings: true });
+      toolGroup.setToolPassive(CircleCenterMeasurementTool.toolName, { removeAllBindings: true });
+      toolGroup.setToolPassive(MidlineMeasurementTool.toolName, { removeAllBindings: true });
+      toolGroup.setToolPassive(FindMidpointMeasurementTool.toolName, { removeAllBindings: true });
+      toolGroup.setToolPassive(VerticalTiltMeasurementTool.toolName, { removeAllBindings: true });
+      toolGroup.setToolPassive(HorizontalTiltMeasurementTool.toolName, { removeAllBindings: true });
+      toolGroup.setToolPassive(PolygonLengthMeasurementTool.toolName, { removeAllBindings: true });
+
+      // 激活拆线长度测量工具
+      toolGroup.setToolActive(PolylineLengthMeasurementTool.toolName, {
+        bindings: [{ mouseButton: MouseBindings.Primary }],
+      });
+
+      // 获取工具实例并激活绘制模式(因为折线工具是绘制类型的)
+      const toolInstance = toolGroup.getToolInstance(PolylineLengthMeasurementTool.toolName) as PolylineLengthMeasurementTool;
+      const viewport = cornerstone.getEnabledElementByViewportId(viewportId)?.viewport;
+      if (toolInstance && viewport.element) {
+        toolInstance._activateDraw(viewport.element);
+      }
+
+      console.log(`[MeasurementToolManager] PolylineLengthMeasurement tool activated for viewport: ${viewportId}`);
+      return true;
+    } catch (error) {
+      console.error(`[MeasurementToolManager] Error activating PolylineLengthMeasurement tool:`, error);
+      return false;
+    }
+  }
+
+  /**
+   * 停用拆线长度测量工具
+   */
+  static deactivatePolylineLengthMeasurementTool(viewportId: string): boolean {
+    const toolGroup = this.getToolGroup(viewportId);
+    if (!toolGroup) return false;
+
+    try {
+      toolGroup.setToolPassive(PolylineLengthMeasurementTool.toolName, { removeAllBindings: true });
+      console.log(`[MeasurementToolManager] PolylineLengthMeasurement tool deactivated for viewport: ${viewportId}`);
+      return true;
+    } catch (error) {
+      console.error(`[MeasurementToolManager] Error deactivating PolylineLengthMeasurement tool:`, error);
+      return false;
+    }
+  }
+
+  /**
+   * 清除指定 viewport 的所有拆线长度测量标注
+   */
+  static clearPolylineLengthMeasurements(viewportId: string): boolean {
+    try {
+      const viewport = cornerstone.getEnabledElementByViewportId(viewportId)?.viewport;
+      if (!viewport) return false;
+
+      const annotations = cornerstoneTools.annotation.state.getAnnotations(
+        PolylineLengthMeasurementTool.toolName,
+        viewport.element
+      );
+
+      let removedCount = 0;
+      annotations.forEach((annotation) => {
+        if (annotation.annotationUID) {
+          cornerstoneTools.annotation.state.removeAnnotation(annotation.annotationUID);
+          removedCount++;
+        }
+      });
+
+      viewport.render();
+      console.log(`[MeasurementToolManager] Cleared ${removedCount} PolylineLength measurements for viewport: ${viewportId}`);
+      return true;
+    } catch (error) {
+      console.error(`[MeasurementToolManager] Error clearing PolylineLength measurements:`, error);
+      return false;
+    }
+  }
+
+  static clearPolylineLengthMeasurementsForViewports(viewportIds: string[]): boolean[] {
+    return viewportIds.map((viewportId) => this.clearPolylineLengthMeasurements(viewportId));
+  }
 }