添加测量工具完整指南.md 27 KB

添加新测量工具完整指南 - 以TPA为例

📋 概述

本文档详细说明如何在系统中添加一个新的Cornerstone3D测量工具。以胫骨平台夹角(TPA)测量工具为例,展示从工具类实现到UI集成的完整流程。


🎯 实现步骤总览

添加一个新的测量工具需要修改以下7个文件:

  1. 创建工具类 - src/components/measures/YourTool.ts
  2. 扩展Redux状态 - src/states/view/measurementPanelSlice.ts
  3. 全局注册工具 - src/utils/cornerstoneToolsSetup.ts
  4. 扩展工具管理器 - src/utils/measurementToolManager.ts
  5. 集成到Viewer - src/pages/view/components/viewers/stack.image.viewer.tsx
  6. 集成到容器 - src/pages/view/components/ViewerContainer.tsx
  7. UI按钮配置 - src/pages/view/components/MeasurementPanel.tsx(通常已存在)

📝 详细步骤

步骤1: 创建工具类

文件: src/components/measures/TibialPlateauAngleTool.ts

这是最核心的一步,需要创建继承自AnnotationTool的工具类。

关键要点:

import { AnnotationTool, Types } from '@cornerstonejs/tools';

export default class TibialPlateauAngleTool extends AnnotationTool {
  // 1. 定义工具名称(必须唯一)
  static toolName = 'TibialPlateauAngleTool';

  // 2. 创建默认标注
  static createDefaultAnnotation(
    element: HTMLDivElement,
    viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport,
    config?: AnnotationInitialConfig
  ): TibialPlateauAngleAnnotation {
    // 在这里定义标注的初始位置和数据结构
    // 返回包含控制点、元数据的标注对象
  }

  // 3. 渲染标注
  renderAnnotation = (
    enabledElement: CoreTypes.IEnabledElement,
    svgDrawingHelper: SVGDrawingHelper
  ): boolean => {
    // 使用Cornerstone3D的drawing工具绘制标注
    // drawLine, drawHandles, drawLinkedTextBox等
  }

  // 4. 处理鼠标事件
  _mouseDownModifyCallback = (evt: EventTypes.InteractionEventType): void => {
    // 处理鼠标按下:选中控制点或文本框
  }

  _mouseDragModifyCallback = (evt: EventTypes.InteractionEventType): void => {
    // 处理拖动:更新控制点或文本框位置
  }

  _mouseUpModifyCallback = (evt: EventTypes.InteractionEventType): void => {
    // 处理鼠标释放:完成编辑
  }

  // 5. 计算测量值
  _updateCachedStats(
    annotation: TibialPlateauAngleAnnotation,
    enabledElement: CoreTypes.IEnabledElement
  ): void {
    // 根据控制点计算测量结果(如角度、长度等)
    // 存储到annotation.data.cachedStats中
  }

  // 6. 可选:导出/导入数据
  static exportAnnotationData(...): ExportedAnnotationData { }
  static restoreFromExportedData(...): Annotation { }
}

完整代码结构:

// 导入依赖
import { AnnotationTool } from '@cornerstonejs/tools';
import { Types as CoreTypes } from '@cornerstonejs/core';

// 定义数据接口
interface YourToolAnnotation extends Types.Annotation {
  data: {
    handles: {
      points: CoreTypes.Point3[];
      activeHandleIndex: number | null;
      textBox?: CoreTypes.Point2;
    };
    cachedStats?: {
      [targetId: string]: {
        measurementValue: number;
        // 其他计算结果
      };
    };
  };
}

// 实现工具类
export default class YourTool extends AnnotationTool {
  static toolName = 'YourTool';
  
  editData: EditData | null = null;
  isDrawing: boolean = false;

  // ... 实现所有必需方法
}

步骤2: 扩展Redux状态

文件: src/states/view/measurementPanelSlice.ts

目的: 让Redux知道新的测量操作类型

修改内容:

// 找到 MeasurementAction 类型定义
export type MeasurementAction =
  | '线段测量'
  | '角度测量'
  | '清除测量'
  | '测量校正'
  | '停用线段测量'
  | '胫骨平台夹角'  // ⭐ 添加这一行
  | null;

位置定位:

  • 在文件顶部,导出的MeasurementAction类型定义中
  • 添加新的字符串字面量类型

步骤3: 全局注册工具

文件: src/utils/cornerstoneToolsSetup.ts

目的: 在Cornerstone3D中全局注册工具,使其可被使用

修改内容:

// 1. 导入工具类
import TibialPlateauAngleTool from '@/components/measures/TibialPlateauAngleTool';

// 2. 在registerGlobalTools函数中注册
export function registerGlobalTools(): void {
  if (toolsRegistered) {
    console.log('[cornerstoneToolsSetup] Tools already registered, skipping...');
    return;
  }

  console.log('[cornerstoneToolsSetup] Registering global tools...');

  try {
    // ... 其他工具注册
    cornerstoneTools.addTool(LengthTool);
    cornerstoneTools.addTool(cornerstoneTools.AngleTool);
    cornerstoneTools.addTool(TibialPlateauAngleTool); // ⭐ 添加这一行

    toolsRegistered = true;
    console.log('[cornerstoneToolsSetup] All tools registered successfully');
  } catch (error) {
    console.error('[cornerstoneToolsSetup] Error registering tools:', error);
    throw error;
  }
}

步骤4: 扩展工具管理器

文件: src/utils/measurementToolManager.ts

目的: 提供统一的API来激活、停用、清除工具

修改内容:

// 1. 导入工具类
import TibialPlateauAngleTool from '@/components/measures/TibialPlateauAngleTool';

// 2. 添加工具管理方法(在类中添加)
export class MeasurementToolManager {
  // ... 现有方法

  // ==================== 胫骨平台夹角测量工具 ====================

  /**
   * 激活工具
   */
  static activateTibialPlateauAngleTool(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.setToolActive(TibialPlateauAngleTool.toolName, {
        bindings: [{ mouseButton: MouseBindings.Primary }],
      });

      console.log(
        `[MeasurementToolManager] TibialPlateauAngle tool activated for viewport: ${viewportId}`
      );
      return true;
    } catch (error) {
      console.error(
        `[MeasurementToolManager] Error activating TibialPlateauAngle tool:`,
        error
      );
      return false;
    }
  }

  /**
   * 停用工具
   */
  static deactivateTibialPlateauAngleTool(viewportId: string): boolean {
    const toolGroup = this.getToolGroup(viewportId);
    if (!toolGroup) return false;

    try {
      toolGroup.setToolPassive(TibialPlateauAngleTool.toolName, {
        removeAllBindings: true,
      });
      console.log(
        `[MeasurementToolManager] TibialPlateauAngle tool deactivated for viewport: ${viewportId}`
      );
      return true;
    } catch (error) {
      console.error(
        `[MeasurementToolManager] Error deactivating TibialPlateauAngle tool:`,
        error
      );
      return false;
    }
  }

  /**
   * 检查工具是否激活
   */
  static isTibialPlateauAngleToolActive(viewportId: string): boolean {
    const toolGroup = this.getToolGroup(viewportId);
    if (!toolGroup) return false;

    try {
      const activeTool = toolGroup.getActivePrimaryMouseButtonTool();
      return activeTool === TibialPlateauAngleTool.toolName;
    } catch (error) {
      console.error(
        `[MeasurementToolManager] Error checking TibialPlateauAngle tool state:`,
        error
      );
      return false;
    }
  }

  /**
   * 切换工具状态
   */
  static toggleTibialPlateauAngleTool(viewportId: string): boolean {
    const isActive = this.isTibialPlateauAngleToolActive(viewportId);
    if (isActive) {
      return this.deactivateTibialPlateauAngleTool(viewportId);
    } else {
      return this.activateTibialPlateauAngleTool(viewportId);
    }
  }

  /**
   * 清除标注
   */
  static clearTibialPlateauAngleMeasurements(viewportId: string): boolean {
    try {
      const viewport =
        cornerstone.getEnabledElementByViewportId(viewportId)?.viewport;
      if (!viewport) {
        console.warn(
          `[MeasurementToolManager] Viewport not found: ${viewportId}`
        );
        return false;
      }

      const annotations = cornerstoneTools.annotation.state.getAnnotations(
        TibialPlateauAngleTool.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} TibialPlateauAngle measurements for viewport: ${viewportId}`
      );
      return true;
    } catch (error) {
      console.error(
        `[MeasurementToolManager] Error clearing TibialPlateauAngle measurements:`,
        error
      );
      return false;
    }
  }

  /**
   * 获取测量结果
   */
  static getTibialPlateauAngleMeasurements(viewportId: string): any[] {
    try {
      const viewport =
        cornerstone.getEnabledElementByViewportId(viewportId)?.viewport;
      if (!viewport) {
        console.warn(
          `[MeasurementToolManager] Viewport not found: ${viewportId}`
        );
        return [];
      }

      const annotations = cornerstoneTools.annotation.state.getAnnotations(
        TibialPlateauAngleTool.toolName,
        viewport.element
      );

      return annotations.map((annotation) => ({
        annotationUID: annotation.annotationUID,
        TPA: annotation.data?.cachedStats?.TPA || 0,
        unit: 'degrees',
        points: annotation.data?.handles?.points || [],
      }));
    } catch (error) {
      console.error(
        `[MeasurementToolManager] Error getting TibialPlateauAngle measurements:`,
        error
      );
      return [];
    }
  }

  /**
   * 批量操作方法
   */
  static activateTibialPlateauAngleToolForViewports(
    viewportIds: string[]
  ): boolean[] {
    return viewportIds.map((viewportId) =>
      this.activateTibialPlateauAngleTool(viewportId)
    );
  }

  static deactivateTibialPlateauAngleToolForViewports(
    viewportIds: string[]
  ): boolean[] {
    return viewportIds.map((viewportId) =>
      this.deactivateTibialPlateauAngleTool(viewportId)
    );
  }

  static clearTibialPlateauAngleMeasurementsForViewports(
    viewportIds: string[]
  ): boolean[] {
    return viewportIds.map((viewportId) =>
      this.clearTibialPlateauAngleMeasurements(viewportId)
    );
  }
}

方法命名规范:

  • activate{ToolName}Tool - 激活工具
  • deactivate{ToolName}Tool - 停用工具
  • is{ToolName}ToolActive - 检查状态
  • toggle{ToolName}Tool - 切换状态
  • clear{ToolName}Measurements - 清除标注
  • get{ToolName}Measurements - 获取结果
  • {action}{ToolName}ToolForViewports - 批量操作

步骤5: 集成到StackViewer

文件: src/pages/view/components/viewers/stack.image.viewer.tsx

目的: 将工具添加到每个viewport的工具组,并导出便捷函数

修改内容:

// 1. 导入工具类
import TibialPlateauAngleTool from '@/components/measures/TibialPlateauAngleTool';

// 2. 在registerTools函数中添加工具到工具组
function registerTools(viewportId, renderingEngineId) {
  // ... 现有代码
  
  // 添加工具到工具组
  toolGroup.addTool(MagnifyTool.toolName);
  toolGroup.addTool(PanTool.toolName);
  // ... 其他工具
  toolGroup.addTool(LengthTool.toolName);
  toolGroup.addTool(AngleTool.toolName);
  toolGroup.addTool(TibialPlateauAngleTool.toolName); // ⭐ 添加这一行

  // 设置默认工具状态
  setupDefaultToolStates(toolGroup);
  // ...
}

// 3. 在setupDefaultToolStates中设置为被动状态
function setupDefaultToolStates(toolGroup: cornerstoneTools.Types.IToolGroup) {
  // ... 其他工具状态设置
  
  toolGroup.setToolPassive(LengthTool.toolName);
  toolGroup.setToolPassive(AngleTool.toolName);
  toolGroup.setToolPassive(TibialPlateauAngleTool.toolName); // ⭐ 添加这一行
}

// 4. 导出便捷函数(在文件末尾,StackViewer组件定义之前)
// ==================== 胫骨平台夹角测量相关函数 ====================

/**
 * 激活胫骨平台夹角测量工具
 */
export function activateTibialPlateauAngleMeasurement(viewportId: string): boolean {
  console.log(
    `[activateTibialPlateauAngleMeasurement] Activating TPA measurement for viewport: ${viewportId}`
  );
  return MeasurementToolManager.activateTibialPlateauAngleTool(viewportId);
}

/**
 * 停用胫骨平台夹角测量工具
 */
export function deactivateTibialPlateauAngleMeasurement(viewportId: string): boolean {
  console.log(
    `[deactivateTibialPlateauAngleMeasurement] Deactivating TPA measurement for viewport: ${viewportId}`
  );
  return MeasurementToolManager.deactivateTibialPlateauAngleTool(viewportId);
}

/**
 * 切换胫骨平台夹角测量工具状态
 */
export function toggleTibialPlateauAngleMeasurement(viewportId: string): boolean {
  console.log(
    `[toggleTibialPlateauAngleMeasurement] Toggling TPA measurement for viewport: ${viewportId}`
  );
  return MeasurementToolManager.toggleTibialPlateauAngleTool(viewportId);
}

/**
 * 清除胫骨平台夹角测量标注
 */
export function clearTibialPlateauAngleMeasurements(viewportId: string): boolean {
  console.log(
    `[clearTibialPlateauAngleMeasurements] Clearing TPA measurements for viewport: ${viewportId}`
  );
  return MeasurementToolManager.clearTibialPlateauAngleMeasurements(viewportId);
}

/**
 * 获取胫骨平台夹角测量结果
 */
export function getTibialPlateauAngleMeasurements(viewportId: string): any[] {
  console.log(
    `[getTibialPlateauAngleMeasurements] Getting TPA measurements for viewport: ${viewportId}`
  );
  return MeasurementToolManager.getTibialPlateauAngleMeasurements(viewportId);
}

/**
 * 检查胫骨平台夹角测量工具是否激活
 */
export function isTibialPlateauAngleMeasurementActive(viewportId: string): boolean {
  return MeasurementToolManager.isTibialPlateauAngleToolActive(viewportId);
}

步骤6: 集成到ViewerContainer

文件: src/pages/view/components/ViewerContainer.tsx

目的: 响应Redux action,协调工具的激活和状态更新

修改内容:

// 1. 导入函数
import StackViewer, {
  // ... 其他导入
  activateLengthMeasurement,
  deactivateLengthMeasurement,
  clearLengthMeasurements,
  activateAngleMeasurement,
  deactivateAngleMeasurement,
  clearAngleMeasurements,
  activateTibialPlateauAngleMeasurement,     // ⭐ 添加
  deactivateTibialPlateauAngleMeasurement,   // ⭐ 添加
  clearTibialPlateauAngleMeasurements,       // ⭐ 添加
} from './viewers/stack.image.viewer';

// 2. 在测量面板Action处理的useEffect中添加case
useEffect(() => {
  if (measurementAction) {
    console.log(`[ViewerContainer] 处理测量操作: ${measurementAction}`);

    const selectedViewportIds = selectedViewerUrls
      .map(getViewportIdByUrl)
      .filter((id): id is string => id !== null);

    switch (measurementAction) {
      // ... 其他case
      
      case '胫骨平台夹角':  // ⭐ 添加这个case
        console.log(`开始胫骨平台夹角测量`);
        if (selectedViewportIds.length > 0) {
          selectedViewportIds.forEach((viewportId) => {
            const success = activateTibialPlateauAngleMeasurement(viewportId);
            if (success) {
              console.log(`激活胫骨平台夹角测量工具成功`);
              dispatch(
                setToolActive({
                  toolName: 'TibialPlateauAngleTool',
                  viewportId: viewportId,
                })
              );
            }
          });
        } else {
          // 如果没有选中的 viewport,为所有可见的 viewport 激活
          const visibleViewportCount = getVisibleViewportCount();
          for (let i = 0; i < visibleViewportCount; i++) {
            const viewportId = `viewport-${i}`;
            const success = activateTibialPlateauAngleMeasurement(viewportId);
            if (success) {
              dispatch(
                setToolActive({
                  toolName: 'TibialPlateauAngleTool',
                  viewportId: viewportId,
                })
              );
            }
          }
        }
        console.log('Activating TibialPlateauAngle Measurement from MeasurementPanel');
        break;

      // ... 其他case
    }

    dispatch(clearMeasurementAction());
  }
}, [measurementAction, selectedViewerUrls, gridLayout, dispatch]);

// 3. 更新清除测量的逻辑
case '清除测量':
  if (selectedViewportIds.length > 0) {
    MeasurementToolManager.clearLengthMeasurementsForViewports(selectedViewportIds);
    MeasurementToolManager.clearAngleMeasurementsForViewports(selectedViewportIds);
    MeasurementToolManager.clearTibialPlateauAngleMeasurementsForViewports(selectedViewportIds); // ⭐ 添加
  }
  console.log('Clearing All Measurements from MeasurementPanel');
  break;

步骤7: UI按钮配置

文件: src/pages/view/components/MeasurementPanel.tsx

说明: 这一步通常已经完成,按钮配置如下:

<FunctionButton
  title="胫骨平台夹角"
  action="胫骨平台夹角"  // ⭐ 这个值必须与Redux中的MeasurementAction类型匹配
  iconName="btn_TPA"
  productId="animal"
/>

FunctionButton组件的handleMeasurementAction函数会自动分发Redux action:

const handleMeasurementAction = (action: string) => {
  console.log(`执行测量操作: ${action}`);
  dispatch(setMeasurementAction(action as MeasurementAction));
};

🔄 数据流图

用户点击按钮
    ↓
FunctionButton.handleMeasurementAction()
    ↓
dispatch(setMeasurementAction('胫骨平台夹角'))
    ↓
Redux State更新: measurementAction = '胫骨平台夹角'
    ↓
ViewerContainer监听到measurementAction变化
    ↓
执行switch case: '胫骨平台夹角'
    ↓
调用activateTibialPlateauAngleMeasurement(viewportId)
    ↓
MeasurementToolManager.activateTibialPlateauAngleTool(viewportId)
    ↓
ToolGroup.setToolActive(TibialPlateauAngleTool)
    ↓
工具激活,等待用户在图像上操作
    ↓
用户拖动控制点 → _updateCachedStats() → renderAnnotation()
    ↓
显示测量结果

📋 快速检查清单

添加新工具时,按以下清单逐一检查:

  • [ ] 步骤1: 创建工具类 src/components/measures/{ToolName}.ts

    • 继承 AnnotationTool
    • 定义 static toolName
    • 实现 createDefaultAnnotation
    • 实现 renderAnnotation
    • 实现鼠标事件处理
    • 实现 _updateCachedStats
  • [ ] 步骤2: 扩展Redux类型 src/states/view/measurementPanelSlice.ts

    • MeasurementAction 类型中添加新的操作名
  • [ ] 步骤3: 全局注册 src/utils/cornerstoneToolsSetup.ts

    • 导入工具类
    • registerGlobalTools 中调用 cornerstoneTools.addTool
  • [ ] 步骤4: 扩展工具管理器 src/utils/measurementToolManager.ts

    • 导入工具类
    • 添加 activate{ToolName}Tool
    • 添加 deactivate{ToolName}Tool
    • 添加 is{ToolName}ToolActive
    • 添加 toggle{ToolName}Tool
    • 添加 clear{ToolName}Measurements
    • 添加 get{ToolName}Measurements
    • 添加批量操作方法
  • [ ] 步骤5: 集成到Viewer src/pages/view/components/viewers/stack.image.viewer.tsx

    • 导入工具类
    • registerTools 中添加工具到工具组
    • setupDefaultToolStates 中设置为被动状态
    • 导出6个便捷函数
  • [ ] 步骤6: 集成到容器 src/pages/view/components/ViewerContainer.tsx

    • 导入便捷函数
    • 在测量action处理中添加case
    • 在清除测量中添加清除逻辑
  • [ ] 步骤7: 确认UI按钮 src/pages/view/components/MeasurementPanel.tsx

    • 确认 FunctionButtonaction 属性与Redux类型匹配

🎯 模板代码

工具管理器方法模板

复制此模板,替换{ToolName}为你的工具名:

// ==================== {工具中文名}测量工具 ====================

/**
 * 激活{工具中文名}测量工具
 */
static activate{ToolName}Tool(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.setToolActive({ToolName}Tool.toolName, {
      bindings: [{ mouseButton: MouseBindings.Primary }],
    });

    console.log(`[MeasurementToolManager] {ToolName} tool activated for viewport: ${viewportId}`);
    return true;
  } catch (error) {
    console.error(`[MeasurementToolManager] Error activating {ToolName} tool:`, error);
    return false;
  }
}

/**
 * 停用{工具中文名}测量工具
 */
static deactivate{ToolName}Tool(viewportId: string): boolean {
  const toolGroup = this.getToolGroup(viewportId);
  if (!toolGroup) return false;

  try {
    toolGroup.setToolPassive({ToolName}Tool.toolName, { removeAllBindings: true });
    console.log(`[MeasurementToolManager] {ToolName} tool deactivated for viewport: ${viewportId}`);
    return true;
  } catch (error) {
    console.error(`[MeasurementToolManager] Error deactivating {ToolName} tool:`, error);
    return false;
  }
}

/**
 * 检查{工具中文名}测量工具是否激活
 */
static is{ToolName}ToolActive(viewportId: string): boolean {
  const toolGroup = this.getToolGroup(viewportId);
  if (!toolGroup) return false;

  try {
    const activeTool = toolGroup.getActivePrimaryMouseButtonTool();
    return activeTool === {ToolName}Tool.toolName;
  } catch (error) {
    console.error(`[MeasurementToolManager] Error checking {ToolName} tool state:`, error);
    return false;
  }
}

/**
 * 切换{工具中文名}测量工具状态
 */
static toggle{ToolName}Tool(viewportId: string): boolean {
  const isActive = this.is{ToolName}ToolActive(viewportId);
  if (isActive) {
    return this.deactivate{ToolName}Tool(viewportId);
  } else {
    return this.activate{ToolName}Tool(viewportId);
  }
}

/**
 * 清除{工具中文名}测量标注
 */
static clear{ToolName}Measurements(viewportId: string): boolean {
  try {
    const viewport = cornerstone.getEnabledElementByViewportId(viewportId)?.viewport;
    if (!viewport) {
      console.warn(`[MeasurementToolManager] Viewport not found: ${viewportId}`);
      return false;
    }

    const annotations = cornerstoneTools.annotation.state.getAnnotations(
      {ToolName}Tool.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} {ToolName} measurements for viewport: ${viewportId}`);
    return true;
  } catch (error) {
    console.error(`[MeasurementToolManager] Error clearing {ToolName} measurements:`, error);
    return false;
  }
}

/**
 * 获取{工具中文名}测量结果
 */
static get{ToolName}Measurements(viewportId: string): any[] {
  try {
    const viewport = cornerstone.getEnabledElementByViewportId(viewportId)?.viewport;
    if (!viewport) {
      console.warn(`[MeasurementToolManager] Viewport not found: ${viewportId}`);
      return [];
    }

    const annotations = cornerstoneTools.annotation.state.getAnnotations(
      {ToolName}Tool.toolName,
      viewport.element
    );

    return annotations.map((annotation) => ({
      annotationUID: annotation.annotationUID,
      // 根据你的工具返回相应的测量值
      value: annotation.data?.cachedStats?.value || 0,
      unit: 'unit',
      points: annotation.data?.handles?.points || [],
    }));
  } catch (error) {
    console.error(`[MeasurementToolManager] Error getting {ToolName} measurements:`, error);
    return [];
  }
}

/**
 * 批量操作方法
 */
static activate{ToolName}ToolForViewports(viewportIds: string[]): boolean[] {
  return viewportIds.map((viewportId) => this.activate{ToolName}Tool(viewportId));
}

static deactivate{ToolName}ToolForViewports(viewportIds: string[]): boolean[] {
  return viewportIds.map((viewportId) => this.deactivate{ToolName}Tool(viewportId));
}

static clear{ToolName}MeasurementsForViewports(viewportIds: string[]): boolean[] {
  return viewportIds.map((viewportId) => this.clear{ToolName}Measurements(viewportId));
}

💡 常见问题

Q1: 工具激活后没有反应?

A: 检查以下几点:

  1. 工具是否正确注册到全局(步骤3)
  2. 工具是否添加到ToolGroup(步骤5)
  3. 控制台是否有错误信息
  4. toolName是否在所有地方保持一致

Q2: 如何调试工具?

A:

  1. 在浏览器控制台查看日志
  2. 检查Redux DevTools中的状态
  3. 在工具类的关键方法中添加console.log
  4. 使用Chrome DevTools断点调试

Q3: 标注渲染不出来?

A:

  1. 检查renderAnnotation方法返回值是否为true
  2. 确认annotationUID存在
  3. 检查SVG元素是否正确创建
  4. 验证坐标转换是否正确(world ↔ canvas)

Q4: 多个viewport的标注混乱?

A:

  1. 确保使用正确的targetId区分不同viewport的数据
  2. 清除标注时使用viewport.element作为参数
  3. 检查工具状态是否正确绑定到各自的ToolGroup

Q5: 如何实现标注的持久化?

A:

  1. 实现exportAnnotationData方法导出数据
  2. 实现restoreFromExportedData方法恢复数据
  3. 在适当时机保存到localStorage或服务器
  4. 页面加载时恢复标注

📚 参考资源

Cornerstone3D文档

项目内参考

  • LengthTool: 线段测量工具,参考基础测量实现
  • AngleTool: 角度测量工具,参考角度计算
  • TibialPlateauAngleTool: 完整的自定义工具实现示例

🎉 总结

通过以上7个步骤,你已经成功添加了一个新的测量工具!

关键要点回顾

  1. ✅ 工具类继承AnnotationTool并实现所有必要方法
  2. ✅ Redux状态类型必须包含新的操作名
  3. ✅ 全局注册工具才能使用
  4. ✅ 工具管理器提供统一的API
  5. ✅ 每个viewport需要独立的ToolGroup
  6. ✅ ViewerContainer协调Redux和工具系统
  7. ✅ UI按钮通过Redux action触发整个流程

下次添加新工具时,只需按照本文档的7个步骤,复制模板代码并替换工具名即可!


📞 需要帮助?

如果在实现过程中遇到问题:

  1. 检查控制台错误信息
  2. 对照本文档的检查清单
  3. 参考TibialPlateauAngleTool的完整实现
  4. 查看Cornerstone3D官方文档

祝你成功添加新的测量工具!🚀