# 添加新测量工具完整指南 - 以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`的工具类。 #### 关键要点: ```typescript 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 { } } ``` #### 完整代码结构: ```typescript // 导入依赖 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知道新的测量操作类型 #### 修改内容: ```typescript // 找到 MeasurementAction 类型定义 export type MeasurementAction = | '线段测量' | '角度测量' | '清除测量' | '测量校正' | '停用线段测量' | '胫骨平台夹角' // ⭐ 添加这一行 | null; ``` #### 位置定位: - 在文件顶部,导出的`MeasurementAction`类型定义中 - 添加新的字符串字面量类型 --- ### 步骤3: 全局注册工具 **文件**: `src/utils/cornerstoneToolsSetup.ts` **目的**: 在Cornerstone3D中全局注册工具,使其可被使用 #### 修改内容: ```typescript // 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来激活、停用、清除工具 #### 修改内容: ```typescript // 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的工具组,并导出便捷函数 #### 修改内容: ```typescript // 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,协调工具的激活和状态更新 #### 修改内容: ```typescript // 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` **说明**: 这一步通常已经完成,按钮配置如下: ```typescript ``` `FunctionButton`组件的`handleMeasurementAction`函数会自动分发Redux action: ```typescript 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` - [ ] 确认 `FunctionButton` 的 `action` 属性与Redux类型匹配 --- ## 🎯 模板代码 ### 工具管理器方法模板 复制此模板,替换`{ToolName}`为你的工具名: ```typescript // ==================== {工具中文名}测量工具 ==================== /** * 激活{工具中文名}测量工具 */ 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文档 - [官方文档](https://www.cornerstonejs.org/) - [工具开发指南](https://www.cornerstonejs.org/docs/concepts/cornerstone-tools/tools) - [注解系统](https://www.cornerstonejs.org/docs/concepts/cornerstone-tools/annotation) ### 项目内参考 - **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官方文档 祝你成功添加新的测量工具!🚀