功能名称:添加掩码(Add Mask)
功能描述:用户可以在医学图像上绘制一个封闭的多边形区域作为掩码,掩码内的图像区域保持可见,掩码外的区域变为黑色(不可见)。这类似于图像处理中的 Mask/遮罩功能。
启动功能:用户点击"添加Mask"按钮
FunctionArea.tsx - 主功能区RectCropPanel.tsx - 矩形裁剪面板
绘制掩码:
完成绘制:
修改掩码:
删除掩码:
本功能参考 PolygonLengthMeasurementTool.ts(多边形长度测量工具)的实现方式,但主要区别在于:
┌─────────────────────────────────────────────────────────────┐
│ UI 层 (React) │
│ ┌──────────────────┐ ┌─────────────────────────┐ │
│ │ FunctionArea.tsx │ │ RectCropPanel.tsx │ │
│ │ "Add Mask" 按钮 │ │ "添加Mask" 按钮 │ │
│ └────────┬─────────┘ └───────────┬─────────────┘ │
└───────────┼───────────────────────────────┼─────────────────┘
│ │
└───────────┬───────────────────┘
▼
┌─────────────────────────────────────────────────────────────┐
│ 状态管理层 (Redux) │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ rectCropPanelSlice / functionAreaSlice │ │
│ │ action: "添加Mask" | "删除Mask" │ │
│ └────────────────────────┬─────────────────────────────┘ │
└───────────────────────────┼─────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────┐
│ 控制层 (ViewerContainer) │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ 监听 Redux action 变化 │ │
│ │ 调用工具激活/停用函数 │ │
│ └────────────────────────┬─────────────────────────────┘ │
└───────────────────────────┼─────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────┐
│ 工具管理层 (Manager) │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ MeasurementToolManager.activateMaskTool() │ │
│ │ MeasurementToolManager.clearMaskAnnotations() │ │
│ └────────────────────────┬─────────────────────────────┘ │
└───────────────────────────┼─────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────┐
│ 核心工具层 (MaskTool) │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ MaskTool extends AnnotationTool │ │
│ │ - 绘制模式 (_activateDraw) │ │
│ │ - 修改模式 (_activateModify) │ │
│ │ - 渲染逻辑 (renderAnnotation) │ │
│ │ - 掩码效果 (applyMaskEffect) │ │
│ └────────────────────────┬─────────────────────────────┘ │
└───────────────────────────┼─────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────┐
│ Cornerstone3D 框架层 │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ - annotation.state (注解状态管理) │ │
│ │ - viewport.render() (视口渲染) │ │
│ │ - SVGDrawingHelper (SVG绘制) │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
FunctionArea.tsx
RectCropPanel.tsx
rectCropPanelSlice.ts
currentAction: CropAction | nullfunctionAreaSlice.ts
action: string | nullrectCropPanel.currentActionfunctionArea.actionMeasurementToolManager.ts
activateMaskTool(viewportId: string): booleandeactivateMaskTool(viewportId: string): booleanclearMaskAnnotations(viewportId: string): booleanstack.image.viewer.tsx
activateMask(viewportId: string): booleandeactivateMask(viewportId: string): booleanclearMask(viewportId: string): booleanAnnotationTool_activateDraw() - 激活绘制模式_deactivateDraw() - 停用绘制模式_activateModify() - 激活修改模式_deactivateModify() - 停用修改模式renderAnnotation() - 渲染掩码applyMaskEffect() - 应用掩码效果到图像_mouseClickDrawCallback() - 鼠标点击添加顶点_mouseDoubleClickDrawCallback() - 双击完成绘制_mouseMoveDrawCallback() - 鼠标移动预览_mouseDragModifyCallback() - 拖拽修改sequenceDiagram
autonumber
participant User as 用户
participant UI as FunctionArea/RectCropPanel
participant Redux as Redux Store
participant Container as ViewerContainer
participant Manager as MeasurementToolManager
participant Tool as MaskTool
participant CS as Cornerstone3D
participant Canvas as 渲染画布
User->>UI: 点击"添加Mask"按钮
UI->>Redux: dispatch(setAction("Add Mask"))
Redux-->>Container: action 状态变化
Container->>Container: useEffect 监听到变化
Container->>Manager: activateMaskTool(viewportId)
Manager->>Tool: setToolActive()
Tool->>Tool: _activateDraw(element)
Tool-->>User: 鼠标变为十字准星
User->>Tool: 点击添加第1个顶点
Tool->>CS: annotation.state.addAnnotation()
Tool->>Canvas: 触发渲染
Canvas-->>User: 显示第1个点
User->>Tool: 点击添加第2个顶点
Tool->>Tool: _mouseClickDrawCallback()
Tool->>Canvas: 触发渲染
Canvas-->>User: 显示线段 1-2
User->>Tool: 点击添加第3个顶点
Tool->>Tool: _mouseClickDrawCallback()
Tool->>Canvas: 触发渲染
Canvas-->>User: 显示线段 2-3 + 预览闭合线
User->>Tool: 双击完成绘制
Tool->>Tool: _mouseDoubleClickDrawCallback()
Tool->>Tool: _deactivateDraw()
Tool->>Tool: _activateModify()
Tool->>Tool: applyMaskEffect()
Tool->>Canvas: 渲染掩码效果
Canvas-->>User: 显示完整掩码(内部可见,外部变黑)
flowchart TD
Start([用户点击按钮]) --> Dispatch[Dispatch Redux Action]
Dispatch --> Listen{ViewerContainer<br/>监听到变化?}
Listen -->|是| GetViewports[获取选中的 viewportIds]
Listen -->|否| End([结束])
GetViewports --> HasSelection{有选中的<br/>viewport?}
HasSelection -->|是| LoopSelected[遍历选中的 viewportIds]
HasSelection -->|否| LoopAll[遍历所有可见 viewportIds]
LoopSelected --> CallActivate[调用 activateMask(viewportId)]
LoopAll --> CallActivate
CallActivate --> GetToolGroup[获取 ToolGroup]
GetToolGroup --> GroupExists{ToolGroup<br/>存在?}
GroupExists -->|否| ReturnFalse[返回 false]
GroupExists -->|是| DeactivateOthers[停用其他冲突工具]
DeactivateOthers --> ActivateTool[setToolActive(MaskTool)]
ActivateTool --> GetInstance[获取工具实例]
GetInstance --> ActivateDraw[调用 _activateDraw()]
ActivateDraw --> AddListeners[添加事件监听器]
AddListeners --> SetCursor[设置十字准星光标]
SetCursor --> ReturnTrue[返回 true]
ReturnFalse --> End
ReturnTrue --> End
classDiagram
class AnnotationTool {
<<abstract>>
+getToolName() string
+renderAnnotation() boolean
+cancel() string
}
class MaskTool {
-currentAnnotation: MaskAnnotation | null
-isDrawing: boolean
-drawingElement: HTMLDivElement | null
-previewMousePosition: Point2 | null
-editData: EditData | null
+static toolName: string
+static createDefaultAnnotation() MaskAnnotation
+addNewAnnotation() MaskAnnotation
+cancel() string
+isPointNearTool() boolean
+getHandleNearImagePoint() ToolHandle
+renderAnnotation() boolean
#_activateDraw(element: HTMLDivElement) void
#_deactivateDraw(element: HTMLDivElement) void
#_activateModify(element: HTMLDivElement) void
#_deactivateModify(element: HTMLDivElement) void
-_mouseClickDrawCallback(evt) void
-_mouseDoubleClickDrawCallback(evt) void
-_mouseMoveDrawCallback(evt) void
-_mouseDownModifyCallback(evt) void
-_mouseDragModifyCallback(evt) void
-_mouseUpModifyCallback(evt) void
-_keyDownCallback(evt) void
-applyMaskEffect(viewport, points) void
-clearMaskEffect(viewport) void
-_updateCachedStats(annotation, enabledElement) void
-_distanceToSegment(point, lineStart, lineEnd) number
}
class MaskAnnotation {
+annotationUID: string
+highlighted: boolean
+isSelected: boolean
+metadata: AnnotationMetadata
+data: MaskAnnotationData
}
class MaskAnnotationData {
+handles: MaskHandles
+cachedStats: object
+maskApplied: boolean
}
class MaskHandles {
+points: Point3[]
+activeHandleIndex: number | null
+textBox: Point2
}
AnnotationTool <|-- MaskTool : 继承
MaskTool --> MaskAnnotation : 创建/管理
MaskAnnotation --> MaskAnnotationData : 包含
MaskAnnotationData --> MaskHandles : 包含
stateDiagram-v2
[*] --> Inactive: 初始状态
Inactive --> Drawing: 用户点击"添加Mask"
Drawing --> Drawing: 单击添加顶点
Drawing --> Modifying: 双击完成绘制<br/>(点数≥3)
Drawing --> Inactive: 双击取消<br/>(点数<3)
Drawing --> Inactive: 按 Esc 取消
Modifying --> Modifying: 拖拽顶点
Modifying --> Modifying: 移动整体
Modifying --> Inactive: 点击"删除Mask"
Modifying --> Inactive: 按 Delete 键
Inactive --> [*]: 工具停用
import { Types as CoreTypes } from '@cornerstonejs/core';
import { Types } from '@cornerstonejs/tools';
/**
* 掩码注解数据结构
*/
type MaskAnnotation = Types.Annotation & {
data: {
handles: {
points: CoreTypes.Point3[]; // 多边形顶点(世界坐标)
activeHandleIndex: number | null; // 当前激活的顶点索引
textBox?: CoreTypes.Point2; // 文本框位置(可选)
};
maskApplied: boolean; // 掩码是否已应用
cachedStats?: {
[targetId: string]: {
vertexCount: number; // 顶点数量
area: number; // 多边形面积(像素)
};
};
};
};
/**
* 编辑数据结构(工具内部使用)
*/
interface EditData {
annotation: Types.Annotation;
viewportIdsToRender: string[];
handleIndex?: number;
newAnnotation?: boolean;
hasMoved?: boolean;
textBoxBeingMoved?: boolean;
textBoxOffset?: CoreTypes.Point2;
movingWholeTool?: boolean;
wholeToolOffset?: CoreTypes.Point2;
}
/**
* 导出的注解数据接口
*/
export interface ExportedMaskData {
points: CoreTypes.Point3[]; // 多边形顶点
maskApplied: boolean; // 掩码应用状态
metadata: {
viewPlaneNormal: CoreTypes.Point3;
viewUp: CoreTypes.Point3;
FrameOfReferenceUID: string;
referencedImageId: string;
};
}
// rectCropPanelSlice.ts
interface RectCropPanelState {
currentAction: CropAction | null; // 已包含 '添加Mask' | '删除Mask'
selectedRatio: CropRatio | null;
}
// functionAreaSlice.ts
interface FunctionAreaState {
action: string | null; // 可以是 'Add Mask' | 'Delete Mask'
}
用户点击按钮
↓
UI 组件触发事件 (onClick)
↓
Dispatch Redux Action
↓
Redux Store 状态更新
↓
ViewerContainer useEffect 监听状态变化
↓
调用 MeasurementToolManager.activateMaskTool()
↓
获取 ToolGroup,激活 MaskTool
↓
MaskTool._activateDraw() 激活绘制模式
↓
添加事件监听器
↓
用户点击 → _mouseClickDrawCallback
↓
创建/更新 annotation
↓
cornerstoneTools.annotation.state.addAnnotation()
↓
触发 viewport.render()
↓
renderAnnotation() 渲染掩码图形
↓
Canvas 更新显示
用户双击完成绘制
↓
_mouseDoubleClickDrawCallback()
↓
验证顶点数量 (≥3)
↓
调用 applyMaskEffect(viewport, points)
↓
获取 viewport 的 canvas context
↓
创建裁剪路径 (clip path)
↓
使用 canvas.clip() 设置裁剪区域
↓
填充外部区域为黑色
↓
设置 annotation.data.maskApplied = true
↓
触发 viewport.render()
↓
Canvas 显示掩码效果
用户点击"删除Mask"按钮
↓
Dispatch Redux Action
↓
ViewerContainer 监听到 action
↓
调用 MeasurementToolManager.clearMaskAnnotations()
↓
获取所有 MaskTool 注解
↓
遍历删除:annotation.state.removeAnnotation()
↓
调用 clearMaskEffect(viewport) 清除掩码效果
↓
恢复原始渲染
↓
触发 viewport.render()
↓
Canvas 恢复正常显示
src/components/tools/MaskTool.ts
src/pages/view/components/viewers/stack.image.viewer.tsx[ ] 导入 MaskTool 类
import MaskTool from '@/components/tools/MaskTool';
addToolsToToolGroup 函数中注册工具
typescript
toolGroup.addTool(MaskTool.toolName);
[ ] 导出激活函数 ```typescript export const activateMask = (viewportId: string): boolean => { return MeasurementToolManager.activateMaskTool(viewportId); };
export const deactivateMask = (viewportId: string): boolean => {
return MeasurementToolManager.deactivateMaskTool(viewportId);
};
export const clearMask = (viewportId: string): boolean => {
return MeasurementToolManager.clearMaskAnnotations(viewportId);
};
#### 2. `src/utils/measurementToolManager.ts`
- [ ] 导入 MaskTool
```typescript
import MaskTool from '@/components/tools/MaskTool';
[ ] 添加激活方法
static activateMaskTool(viewportId: string): boolean
typescript
static deactivateMaskTool(viewportId: string): boolean
[ ] 添加清除方法
static clearMaskAnnotations(viewportId: string): boolean
src/pages/view/components/ViewerContainer.tsxtypescript
import { activateMask, deactivateMask, clearMask } from './viewers/stack.image.viewer';
[ ] 在 useEffect 的 switch (action) 中添加 case
```typescript
case 'Add Mask':
if (selectedViewportIds.length > 0) {
selectedViewportIds.forEach((viewportId) => {
activateMask(viewportId);
}); } else { const visibleViewportCount = getVisibleViewportCount(); for (let i = 0; i < visibleViewportCount; i++) {
const viewportId = getViewportIdByUrl(imageUrls[i]);
if (viewportId) {
activateMask(viewportId);
}
} } break;
case 'Delete Mask':
if (selectedViewportIds.length > 0) {
selectedViewportIds.forEach((viewportId) => {
clearMask(viewportId);
});
} else {
const visibleViewportCount = getVisibleViewportCount();
for (let i = 0; i < visibleViewportCount; i++) {
const viewportId = getViewportIdByUrl(imageUrls[i]);
if (viewportId) {
clearMask(viewportId);
}
}
}
break;
#### 4. `src/pages/view/components/FunctionArea.tsx`
- [ ] 移除 'Delete Mask' 从未实现列表
```typescript
// 修改前
['Delete Mask', 'Image Comparison', ...].includes(action)
// 修改后
['Image Comparison', ...].includes(action)
| 测试场景 | 预期结果 |
|---|---|
| 工具激活 | MaskTool 成功激活,光标变为十字准星 |
| 添加顶点(少于3个) | 可以添加顶点,但不能应用掩码 |
| 添加顶点(3个及以上) | 双击后成功应用掩码效果 |
| 拖拽顶点 | 顶点位置更新,掩码效果实时更新 |
| 移动整个掩码 | 掩码整体移动,效果保持 |
| 删除掩码 | 掩码效果消失,图像恢复正常 |
| 多分格独立掩码 | 每个分格可以独立设置掩码 |
| Esc 取消绘制 | 绘制被取消,工具停用 |
步骤:
预期结果:
前置条件:已有一个掩码 步骤:
预期结果:
前置条件:已有一个掩码 步骤:
预期结果:
步骤:
预期结果:
步骤:
预期结果:
| 测试场景 | 输入 | 预期结果 |
|---|---|---|
| 最少顶点数 | 只点击 2 个点后双击 | 掩码不创建,提示至少需要3个点 |
| 大量顶点 | 点击 100 个点 | 掩码正常创建,性能可接受 |
| 超出图像边界 | 某些顶点在图像外 | 掩码仍然创建,但效果仅应用于图像内 |
| 没有选中 viewport | 未选中任何 viewport 时点击按钮 | 为所有可见 viewport 激活工具 |
| 工具冲突 | 掩码工具激活时点击其他测量工具 | 掩码工具停用,新工具激活 |
分析: PolygonLengthMeasurementTool 只是绘制 SVG 图形,但掩码需要实际影响图像渲染。
解决方案:
使用 Canvas API 的 clip() 方法:
applyMaskEffect(viewport: Viewport, points: Point3[]) {
const canvas = viewport.canvas;
const ctx = canvas.getContext('2d');
// 创建裁剪路径
ctx.beginPath();
const canvasPoints = points.map(p => viewport.worldToCanvas(p));
ctx.moveTo(canvasPoints[0][0], canvasPoints[0][1]);
for (let i = 1; i < canvasPoints.length; i++) {
ctx.lineTo(canvasPoints[i][0], canvasPoints[i][1]);
}
ctx.closePath();
// 应用裁剪
ctx.save();
ctx.clip();
// 填充外部区域为黑色(使用反向裁剪)
ctx.fillStyle = 'black';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.restore();
}
备选方案: 如果 Canvas clip 方案不可行,可以考虑:
分析: 如果允许多个掩码,它们的效果如何叠加?
解决方案:
方案 1:限制每个 viewport 只能有一个掩码(推荐)
方案 2:支持多个掩码,使用交集
推荐:方案 1,简单可靠
分析: 如何在 Cornerstone3D 中实现多边形区域可见,外部变黑的遮罩效果?
解决方案:
基于项目现有 overlayRedRectangle 函数,采用 Canvas 覆盖层 + 动态更新 方案:
evenodd 填充规则✅ 实时效果:用户可以立即看到遮罩效果变化 ✅ 性能良好:Canvas 2D 操作轻量级 ✅ 兼容性好:不依赖复杂 WebGL 特性 ✅ 易于管理:遮罩生命周期与工具生命周期绑定 ✅ 响应式:自动适应窗口大小变化
分析: 用户刷新页面或切换图像后,掩码效果是否保留?
解决方案:
annotation.state 中onSetToolActive(): void {
// 检查是否有已存在的掩码
const annotations = annotation.state.getAnnotations(MaskTool.toolName, element);
if (annotations && annotations.length > 0) {
// 重新应用掩码效果
const maskAnnotation = annotations[0] as MaskAnnotation;
if (maskAnnotation.data.maskApplied) {
this.applyMaskEffect(viewport, maskAnnotation.data.handles.points);
}
}
}
分析: 掩码工具激活时,其他工具(如测量、标记)是否受影响?
解决方案:
参考 MeasurementToolManager 中的模式:
toolGroup.setToolPassive(WindowLevelTool.toolName, { removeAllBindings: true });
toolGroup.setToolPassive(MagnifyTool.toolName, { removeAllBindings: true });
// ... 停用其他工具
分析: 多边形边缘可能产生锯齿或不精确的裁剪。
解决方案:
ctx.imageSmoothingEnabled = true分析: 频繁的掩码效果应用可能导致性能下降。
解决方案:
requestAnimationFrame 控制渲染频率分析: Canvas clip 行为在不同浏览器可能有差异。
解决方案:
export default class MaskTool extends AnnotationTool {
static toolName = 'MaskTool';
currentAnnotation: MaskAnnotation | null = null;
isDrawing: boolean = false;
drawingElement: HTMLDivElement | null = null;
previewMousePosition: Point2 | null = null;
// 创建默认注解
static createDefaultAnnotation(element, viewport): MaskAnnotation {
// 类似 PolygonLengthMeasurementTool.createDefaultAnnotation
}
// 添加新注解(绘制模式入口)
addNewAnnotation(evt): MaskAnnotation {
// 创建空注解
// 激活绘制模式
}
// 激活绘制模式
_activateDraw(element: HTMLDivElement): void {
this.drawingElement = element;
this.isDrawing = true;
element.addEventListener('CORNERSTONE_TOOLS_MOUSE_CLICK', this._mouseClickDrawCallback);
element.addEventListener('dblclick', this._mouseDoubleClickDrawCallback);
element.addEventListener('CORNERSTONE_TOOLS_MOUSE_MOVE', this._mouseMoveDrawCallback);
}
// 鼠标点击添加顶点
_mouseClickDrawCallback = (evt): void => {
if (!this.isDrawing) return;
const worldPos = evt.detail.currentPoints.world;
if (!this.currentAnnotation) {
// 创建第一个注解
this.currentAnnotation = this.createDefaultAnnotation(...);
this.currentAnnotation.data.handles.points = [worldPos];
annotation.state.addAnnotation(this.currentAnnotation, element);
} else {
// 添加新顶点
this.currentAnnotation.data.handles.points.push(worldPos);
}
// 触发渲染
viewport.render();
};
// 双击完成绘制
_mouseDoubleClickDrawCallback = (evt): void => {
if (this.currentAnnotation.data.handles.points.length >= 3) {
// 完成绘制
this._deactivateDraw(element);
this._activateModify(element);
// 应用掩码效果
this.applyMaskEffect(viewport, this.currentAnnotation.data.handles.points);
this.currentAnnotation.data.maskApplied = true;
// 触发渲染
viewport.render();
} else {
// 顶点不足,删除注解
annotation.state.removeAnnotation(this.currentAnnotation.annotationUID);
this.currentAnnotation = null;
}
};
// 应用掩码效果
applyMaskEffect(viewport, points: Point3[]): void {
const canvas = viewport.canvas;
const ctx = canvas.getContext('2d');
// 转换为 canvas 坐标
const canvasPoints = points.map(p => viewport.worldToCanvas(p));
// 创建裁剪路径
ctx.save();
ctx.beginPath();
ctx.moveTo(canvasPoints[0][0], canvasPoints[0][1]);
for (let i = 1; i < canvasPoints.length; i++) {
ctx.lineTo(canvasPoints[i][0], canvasPoints[i][1]);
}
ctx.closePath();
// 反向裁剪:填充外部区域为黑色
ctx.globalCompositeOperation = 'destination-out';
ctx.clip('evenodd');
ctx.fillStyle = 'black';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.restore();
}
// 渲染注解(绘制多边形轮廓和手柄)
renderAnnotation = (enabledElement, svgDrawingHelper): boolean => {
// 类似 PolygonLengthMeasurementTool.renderAnnotation
// 绘制多边形线段
// 绘制手柄点
// 绘制预览线(如果在绘制模式)
};
}
// measurementToolManager.ts
import MaskTool from '@/components/tools/MaskTool';
export class MeasurementToolManager {
// ... 其他方法
/**
* 激活掩码工具
*/
static activateMaskTool(viewportId: string): boolean {
const toolGroup = MeasurementToolManager.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.setToolActive(MaskTool.toolName, {
bindings: [{ mouseButton: MouseBindings.Primary }],
});
// 获取工具实例并激活绘制模式
const toolInstance = toolGroup.getToolInstance(MaskTool.toolName) as MaskTool;
const viewport = cornerstone.getEnabledElementByViewportId(viewportId)?.viewport;
if (toolInstance && viewport.element) {
toolInstance._activateDraw(viewport.element);
}
console.log(`[MeasurementToolManager] MaskTool activated for viewport: ${viewportId}`);
return true;
} catch (error) {
console.error(`[MeasurementToolManager] Error activating MaskTool:`, error);
return false;
}
}
/**
* 停用掩码工具
*/
static deactivateMaskTool(viewportId: string): boolean {
const toolGroup = MeasurementToolManager.getToolGroup(viewportId);
if (!toolGroup) return false;
try {
toolGroup.setToolPassive(MaskTool.toolName, { removeAllBindings: true });
console.log(`[MeasurementToolManager] MaskTool deactivated for viewport: ${viewportId}`);
return true;
} catch (error) {
console.error(`[MeasurementToolManager] Error deactivating MaskTool:`, error);
return false;
}
}
/**
* 清除指定 viewport 的所有掩码注解
*/
static clearMaskAnnotations(viewportId: string): boolean {
try {
const viewport = cornerstone.getEnabledElementByViewportId(viewportId)?.viewport;
if (!viewport) return false;
const annotations = cornerstoneTools.annotation.state.getAnnotations(
MaskTool.toolName,
viewport.element
);
let removedCount = 0;
annotations.forEach((annotation) => {
if (annotation.annotationUID) {
cornerstoneTools.annotation.state.removeAnnotation(annotation.annotationUID);
removedCount++;
}
});
// 清除掩码效果
const toolGroup = MeasurementToolManager.getToolGroup(viewportId);
const toolInstance = toolGroup?.getToolInstance(MaskTool.toolName) as MaskTool;
if (toolInstance) {
toolInstance.clearMaskEffect(viewport);
}
viewport.render();
console.log(`[MeasurementToolManager] Cleared ${removedCount} mask annotations for viewport: ${viewportId}`);
return true;
} catch (error) {
console.error(`[MeasurementToolManager] Error clearing mask annotations:`, error);
return false;
}
}
}
高优先级(核心功能)
中优先级(交互优化)
低优先级(增强功能)
第一阶段:核心工具开发
PolygonLengthMeasurementTool.ts 作为模板MaskTool.tsapplyMaskEffect() 和 clearMaskEffect() 方法第二阶段:集成到系统
stack.image.viewer.tsx 中注册工具MeasurementToolManager.ts 中添加管理方法ViewerContainer.tsx 中添加 action 处理第三阶段:测试和优化
⚠️ 掩码效果实现:Canvas clip 可能与 Cornerstone 的渲染流程冲突
⚠️ 渲染性能:掩码效果可能影响渲染帧率
⚠️ 跨 viewport 同步:多分格模式下的掩码独立性
| 文件路径 | 作用 |
|---|---|
src/components/tools/MaskTool.ts |
核心工具类(新建) |
src/utils/measurementToolManager.ts |
工具管理器(修改) |
src/pages/view/components/viewers/stack.image.viewer.tsx |
工具注册(修改) |
src/pages/view/components/ViewerContainer.tsx |
用户操作处理(修改) |
src/pages/view/components/FunctionArea.tsx |
主功能按钮(修改) |
src/pages/view/components/RectCropPanel.tsx |
裁剪面板按钮(无需修改) |
src/states/view/rectCropPanelSlice.ts |
状态管理(无需修改) |
文档版本:v1.0
创建日期:2025-12-20
最后更新:2025-12-20
作者:Cline AI
审核状态:待审核