Przeglądaj źródła

fix: 修复四角DICOM信息显示与隐藏问题

- 在 OverlayRenderer.ts 中添加 skipRender 标志控制渲染状态

- 新增 clearPreviousElements 方法清理旧元素但保留最后一个

- 为 SVG 元素添加 data-dicom-overlay 标记便于管理

- 新增 reset/clear 方法控制渲染模式切换

- 在 DicomOverlayTool.ts 中工具启用时切换渲染标志

- 在 stack.image.viewer.tsx 中通过再次启用实现关闭功能

- 在 LocalOverlayConfigAdapter.ts 中统一默认值为无
sw 1 miesiąc temu
rodzic
commit
a75cd5b6cc

+ 266 - 1
docs/实现/四角DICOM信息显示功能.md

@@ -1181,7 +1181,272 @@ class DicomOverlayTool {
 
 ---
 
-**文档版本**: 1.0.0  
+---
+
+## 🏊 13. 启用/停用工具流程图(泳道图)
+
+### 13.1 启用流程
+
+```mermaid
+sequenceDiagram
+    participant User as 👤 用户
+    participant MP as MorePanel
+    participant Redux as Redux Store
+    participant SV as StackViewer
+    participant TG as ToolGroup
+    participant Tool as DicomOverlayTool
+    participant CM as ConfigManager
+    participant CS as Cornerstone3D
+    
+    User->>MP: 点击"tag信息"按钮
+    Note over MP: handleTagInfo()
+    MP->>Redux: dispatch(toggleOverlay())
+    Note over Redux: enabled: false → true
+    
+    Redux-->>SV: 状态变化通知
+    Note over SV: useEffect监听overlayEnabled
+    
+    SV->>TG: getToolGroup(viewportId)
+    TG-->>SV: 返回toolGroup实例
+    
+    SV->>TG: setToolEnabled(DicomOverlayTool.toolName)
+    Note over TG: 工具状态: Passive → Enabled
+    
+    TG->>Tool: onSetToolEnabled()
+    Note over Tool: 工具被激活
+    
+    Tool->>CM: getConfig()
+    Note over CM: 尝试加载配置
+    
+    alt 远程配置可用
+        CM->>CM: RemoteAdapter.fetch()
+        CM-->>Tool: 返回远程配置
+    else 远程配置失败
+        CM->>CM: LocalAdapter.getConfig()
+        CM-->>Tool: 返回本地配置
+    end
+    
+    Note over Tool: 配置加载完成
+    Tool->>CS: 触发viewport.render()
+    
+    CS->>Tool: renderAnnotation(enabledElement, svgHelper)
+    Note over Tool: Cornerstone自动调用渲染
+    
+    Tool->>Tool: extractMetadata(imageId)
+    Tool->>Tool: prepareTextLines()
+    Tool->>Tool: renderToSVG()
+    
+    Tool->>CS: 创建SVG文本元素
+    Note over CS: SVG layer
+    CS-->>User: 显示四角信息 ✅
+```
+
+### 13.2 停用流程
+
+```mermaid
+sequenceDiagram
+    participant User as 👤 用户
+    participant MP as MorePanel
+    participant Redux as Redux Store
+    participant SV as StackViewer
+    participant TG as ToolGroup
+    participant Tool as DicomOverlayTool
+    participant CS as Cornerstone3D
+    
+    User->>MP: 再次点击"tag信息"按钮
+    Note over MP: handleTagInfo()
+    MP->>Redux: dispatch(toggleOverlay())
+    Note over Redux: enabled: true → false
+    
+    Redux-->>SV: 状态变化通知
+    Note over SV: useEffect监听overlayEnabled
+    
+    SV->>TG: getToolGroup(viewportId)
+    TG-->>SV: 返回toolGroup实例
+    
+    SV->>TG: setToolDisabled(DicomOverlayTool.toolName)
+    Note over TG: 工具状态: Enabled → Disabled
+    
+    TG->>Tool: onSetToolDisabled()
+    Note over Tool: 工具被停用
+    
+    Tool->>CS: 触发viewport.render()
+    Note over CS: 重新渲染viewport
+    
+    CS->>Tool: renderAnnotation(enabledElement, svgHelper)
+    Note over Tool: 由于工具disabled,<br/>Cornerstone不再调用渲染
+    
+    Note over CS: SVG元素被清除
+    CS-->>User: 四角信息消失 ✅
+```
+
+### 13.3 参与者说明
+
+#### 🎨 UI层
+1. **User (用户)**
+   - 角色: 触发操作的人
+   - 操作: 点击"tag信息"按钮
+
+2. **MorePanel** (`src/pages/view/components/MorePanel.tsx`)
+   - 角色: UI控制面板
+   - 关键方法:
+     - `handleTagInfo()`: 处理按钮点击
+   - 职责: 触发Redux action
+
+#### 📦 状态管理层
+3. **Redux Store** (`src/states/view/dicomOverlaySlice.ts`)
+   - 角色: 全局状态管理
+   - 关键状态:
+     - `enabled: boolean`: overlay开关状态
+   - 关键Action:
+     - `toggleOverlay()`: 切换enabled状态
+   - 职责: 存储和通知状态变化
+
+#### 🖼️ 视图层
+4. **StackViewer** (`src/pages/view/components/viewers/stack.image.viewer.tsx`)
+   - 角色: 图像查看器组件
+   - 关键Hook:
+     - `useSelector(selectOverlayEnabled)`: 监听状态
+     - `useEffect([overlayEnabled])`: 响应状态变化
+   - 关键逻辑:
+     ```typescript
+     useEffect(() => {
+       const toolGroup = ToolGroupManager.getToolGroup(`STACK_TOOL_GROUP_ID_${viewportId}`);
+       if (overlayEnabled) {
+         toolGroup.setToolEnabled(DicomOverlayTool.toolName);
+       } else {
+         toolGroup.setToolDisabled(DicomOverlayTool.toolName);
+       }
+     }, [overlayEnabled, viewportId]);
+     ```
+   - 职责: 连接Redux状态和Cornerstone工具
+
+#### 🛠️ 工具管理层
+5. **ToolGroup** (Cornerstone3D提供)
+   - 角色: 工具集合管理器
+   - 关键方法:
+     - `setToolEnabled(toolName)`: 启用工具
+     - `setToolDisabled(toolName)`: 停用工具
+   - 职责: 管理工具生命周期,触发工具回调
+
+#### 🎯 核心工具层
+6. **DicomOverlayTool** (`src/components/overlay/DicomOverlayTool.ts`)
+   - 角色: DICOM信息显示工具(继承AnnotationTool)
+   - 关键生命周期方法:
+     - `onSetToolEnabled()`: 工具启用回调
+       ```typescript
+       onSetToolEnabled(): void {
+         console.log('[DicomOverlayTool] Tool enabled');
+         this.loadConfig();  // 异步加载配置
+       }
+       ```
+     - `onSetToolDisabled()`: 工具停用回调
+       ```typescript
+       onSetToolDisabled(): void {
+         console.log('[DicomOverlayTool] Tool disabled');
+         // Cornerstone会自动停止调用renderAnnotation
+       }
+       ```
+     - `renderAnnotation(enabledElement, svgHelper)`: 渲染回调
+       ```typescript
+       renderAnnotation(enabledElement, svgHelper): boolean {
+         if (!this.config) return false;
+         // 提取metadata
+         const metadata = this.extractMetadata(imageId);
+         // 渲染到SVG
+         this.renderer.renderToSVG(svgHelper, viewport, metadata, ...);
+         return true;
+       }
+       ```
+   - 职责: 管理配置、提取数据、触发渲染
+
+#### ⚙️ 配置管理层
+7. **ConfigManager** (`src/config/overlayConfig/OverlayConfigManager.ts`)
+   - 角色: 配置提供者
+   - 关键方法:
+     - `getConfig()`: 获取配置
+       ```typescript
+       async getConfig(): Promise<OverlayConfig> {
+         // 优先远程,失败降级本地
+         try {
+           return await this.remoteAdapter.getOverlayConfig();
+         } catch {
+           return await this.localAdapter.getOverlayConfig();
+         }
+       }
+       ```
+   - 职责: 统一配置访问,处理降级
+
+#### 🎨 渲染引擎层
+8. **Cornerstone3D**
+   - 角色: 医学图像渲染引擎
+   - 关键职责:
+     - 在渲染循环中自动调用`renderAnnotation()`
+     - 管理SVG图层
+     - 响应工具状态变化
+   - 工作机制:
+     - 工具Enabled时: 渲染循环包含该工具
+     - 工具Disabled时: 渲染循环跳过该工具
+
+### 13.4 关键流程说明
+
+#### 启用时的关键步骤
+
+1. **状态更新** (MorePanel → Redux)
+   - 用户点击触发`toggleOverlay()`
+   - Redux将`enabled`从`false`改为`true`
+
+2. **状态监听** (Redux → StackViewer)
+   - `useSelector`检测到状态变化
+   - `useEffect`被触发
+
+3. **工具激活** (StackViewer → ToolGroup)
+   - 调用`toolGroup.setToolEnabled()`
+   - ToolGroup触发工具的`onSetToolEnabled()`
+
+4. **配置加载** (Tool → ConfigManager)
+   - 异步加载配置
+   - 加载完成后触发`viewport.render()`
+
+5. **自动渲染** (Cornerstone → Tool)
+   - Cornerstone在渲染循环中调用`renderAnnotation()`
+   - 工具创建SVG元素显示信息
+
+#### 停用时的关键步骤
+
+1. **状态更新** (MorePanel → Redux)
+   - 用户再次点击触发`toggleOverlay()`
+   - Redux将`enabled`从`true`改为`false`
+
+2. **状态监听** (Redux → StackViewer)
+   - `useSelector`检测到状态变化
+   - `useEffect`被触发
+
+3. **工具停用** (StackViewer → ToolGroup)
+   - 调用`toolGroup.setToolDisabled()`
+   - ToolGroup触发工具的`onSetToolDisabled()`
+
+4. **停止渲染** (Cornerstone自动处理)
+   - Cornerstone不再调用该工具的`renderAnnotation()`
+   - SVG元素被清除
+   - 四角信息消失
+
+### 13.5 核心优势
+
+✅ **自动化渲染**: Cornerstone3D自动管理渲染循环,工具无需手动监听事件
+
+✅ **声明式状态**: 使用Redux管理状态,React自动响应变化
+
+✅ **清晰的职责分离**: 
+- UI层只负责触发action
+- 工具层只负责渲染逻辑
+- Cornerstone负责渲染调度
+
+✅ **SVG渲染**: 不会被Canvas覆盖,永久显示
+
+---
+
+**文档版本**: 1.1.0  
 **创建日期**: 2025-01-25  
 **最后更新**: 2025-01-25  
 **作者**: Cline (AI Assistant)

+ 15 - 10
src/components/overlay/DicomOverlayTool.ts

@@ -12,7 +12,7 @@ import { OverlayRenderer } from './renderers/OverlayRenderer';
 
 export default class DicomOverlayTool extends AnnotationTool {
   static toolName = 'DicomOverlay';
-  
+
   private config: OverlayConfig | null = null;
   private renderer: OverlayRenderer;
 
@@ -23,6 +23,7 @@ export default class DicomOverlayTool extends AnnotationTool {
       configuration: {},
     }
   ) {
+    console.log(`【20251025】 重新构建 DicomOverlayTool`);
     super(toolProps, defaultToolProps);
     this.renderer = new OverlayRenderer();
   }
@@ -33,6 +34,9 @@ export default class DicomOverlayTool extends AnnotationTool {
   onSetToolEnabled(): void {
     console.log('[DicomOverlayTool] Tool enabled');
     
+    // 重置渲染器,恢复正常渲染
+      this.renderer.skipRender = !this.renderer.skipRender;
+    
     // 加载配置(异步)
     this.loadConfig();
   }
@@ -51,7 +55,7 @@ export default class DicomOverlayTool extends AnnotationTool {
     try {
       this.config = await overlayConfigManager.getConfig();
       console.log('[DicomOverlayTool] Config loaded:', this.config);
-      
+
       // 配置加载后触发重新渲染
       const renderingEngines = cornerstone.getRenderingEngines();
       if (renderingEngines) {
@@ -78,21 +82,21 @@ export default class DicomOverlayTool extends AnnotationTool {
         console.log('[DicomOverlayTool] Metadata found in image cache');
         return image.metadata;
       }
-      
+
       // 方法2: 使用metaData API获取各个模块
       const modules = [
         'patientModule',
-        'generalStudyModule', 
+        'generalStudyModule',
         'generalSeriesModule',
         'imagePixelModule',
         'imagePlaneModule',
         'modalityLutModule',
         'voiLutModule'
       ];
-      
+
       const metadata: any = {};
       let hasData = false;
-      
+
       for (const moduleName of modules) {
         const moduleData = cornerstone.metaData.get(moduleName, imageId);
         if (moduleData) {
@@ -100,12 +104,12 @@ export default class DicomOverlayTool extends AnnotationTool {
           hasData = true;
         }
       }
-      
+
       if (hasData) {
         console.log('[DicomOverlayTool] Metadata assembled from modules:', Object.keys(metadata));
         return metadata;
       }
-      
+
       console.warn('[DicomOverlayTool] No metadata found for imageId:', imageId);
       return null;
     } catch (error) {
@@ -119,13 +123,14 @@ export default class DicomOverlayTool extends AnnotationTool {
    * Cornerstone会自动调用此方法来渲染overlay
    */
   renderAnnotation(enabledElement: any, svgDrawingHelper: any): boolean {
+    console.log(`【20251025】 renderAnnotation 执行了`);
     if (!this.config) {
       return false;
     }
 
     try {
       const { viewport } = enabledElement;
-      
+
       // 获取当前图像ID
       const imageId = viewport.getCurrentImageId?.();
       if (!imageId) {
@@ -137,7 +142,7 @@ export default class DicomOverlayTool extends AnnotationTool {
       if (!metadata) {
         return false;
       }
-
+console.log(`【20251025】 renderAnnotation 调用了 renderToSVG`);
       // 使用SVG渲染overlay
       this.renderer.renderToSVG(
         svgDrawingHelper,

+ 72 - 4
src/components/overlay/renderers/OverlayRenderer.ts

@@ -21,6 +21,7 @@ export interface RendererContext {
 export class OverlayRenderer {
   private canvas: HTMLCanvasElement | null = null;
   private ctx: CanvasRenderingContext2D | null = null;
+  skipRender = true; // 跳过渲染标志
 
   /**
    * 使用SVG渲染四角信息(AnnotationTool方式)
@@ -37,7 +38,16 @@ export class OverlayRenderer {
     corners: CornerConfig[],
     style: OverlayStyle
   ): void {
-    console.log(`[OverlayRenderer] Rendering to SVG with ${corners.length} corners`);
+    console.log(`【20251025】 [OverlayRenderer] Rendering to SVG with ${corners.length} corners`);
+    
+    // 在渲染新元素前,先清除所有旧的 overlay 元素,避免累积
+    this.clearPreviousElements(viewport);
+    
+    // 如果是清理模式,清除元素后直接返回,不渲染新元素
+    if (this.skipRender) {
+      console.log('[OverlayRenderer] Cleared elements in skip render mode, not rendering new elements');
+      return;
+    }
     
     // 渲染每个角落
     for (const corner of corners) {
@@ -47,6 +57,45 @@ export class OverlayRenderer {
     }
   }
 
+  /**
+   * 清除之前渲染的 overlay 元素
+   * 通过 viewport 的 canvas 查找 SVG 层
+   */
+  clearPreviousElements(viewport: any): void {
+    try {
+      // 通过 viewport 的 canvas 查找 SVG
+      const canvas = viewport?.canvas;
+      if (!canvas || !canvas.parentElement) {
+        console.warn('[OverlayRenderer] Cannot find canvas element');
+        return;
+      }
+      
+      // SVG 通常是 canvas 的同级元素
+      const svg = canvas.parentElement.querySelector('svg');
+      if (!svg) {
+        console.warn('[OverlayRenderer] Cannot find SVG element in canvas parent');
+        return;
+      }
+      
+      // 删除所有带 data-dicom-overlay 标记的元素,但保留最后一个
+      const overlayElements = svg.querySelectorAll('[data-dicom-overlay="true"]');
+      const count = overlayElements.length;
+      
+      // 只删除前 count-1 个元素,保留最后一个
+      for (let i = 0; i < count - 1; i++) {
+        overlayElements[i].remove();
+      }
+      
+      if (count > 1) {
+        console.log(`[OverlayRenderer] Cleared ${count - 1} previous overlay elements, kept last one`);
+      } else if (count === 1) {
+        console.log(`[OverlayRenderer] Kept the only overlay element`);
+      }
+    } catch (error) {
+      console.error('[OverlayRenderer] Error clearing previous elements:', error);
+    }
+  }
+
   /**
    * 渲染单个角落到SVG
    */
@@ -117,7 +166,8 @@ export class OverlayRenderer {
       textElement.setAttribute('stroke-width', '3');
       textElement.setAttribute('paint-order', 'stroke');
     }
-    
+    // 标记为DICOM Overlay元素 在删除时会用它来查询四角信息相关的svg元素
+    textElement.setAttribute('data-dicom-overlay', 'true'); 
     textElement.textContent = text;
     
     // 添加到SVG层
@@ -387,17 +437,35 @@ export class OverlayRenderer {
   /**
    * 清除 Canvas
    */
-  clear(): void {
+  clearCanvas(): void {
     if (!this.ctx || !this.canvas) return;
     this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
   }
 
+  /**
+   * 设置清理模式(下次渲染时不调用 appendNode,让元素变为"未触碰")
+   */
+  clear(): void {
+    console.log(`【20251025】clear,使 this.skipRender = true;`)
+    this.skipRender = true;
+  }
+
+  /**
+   * 重置渲染模式(恢复正常渲染)
+   */
+  reset(): void {
+    console.log(`【20251025】reset,使 this.skipRender = false;`)
+    this.skipRender = false;
+  }
+
   /**
    * 销毁渲染器
    */
   destroy(): void {
-    this.clear();
+    this.clearCanvas();
     this.canvas = null;
     this.ctx = null;
+    console.log(`【20251025】 destroy ,使 this.skipRender = false;`)
+    this.skipRender = false;
   }
 }

+ 13 - 13
src/config/overlayConfig/adapters/LocalOverlayConfigAdapter.ts

@@ -55,21 +55,21 @@ export class LocalOverlayConfigAdapter implements IOverlayConfigProvider {
             label: '患者ID',
             showLabel: true,
             format: TagFormat.RAW,
-            defaultValue: '',
+            defaultValue: '',
           },
           {
             tag: '00100040', // Patient Sex
             label: '性别',
             showLabel: true,
             format: TagFormat.SEX,
-            defaultValue: '',
+            defaultValue: '',
           },
           {
             tag: '00101010', // Patient Age
             label: '年龄',
             showLabel: true,
             format: TagFormat.RAW,
-            defaultValue: '',
+            defaultValue: '',
           },
         ],
       },
@@ -85,28 +85,28 @@ export class LocalOverlayConfigAdapter implements IOverlayConfigProvider {
             label: '检查日期',
             showLabel: true,
             format: TagFormat.DATE,
-            defaultValue: '',
+            defaultValue: '',
           },
           {
             tag: '00080030', // Study Time
             label: '检查时间',
             showLabel: true,
             format: TagFormat.TIME,
-            defaultValue: '',
+            defaultValue: '',
           },
           {
             tag: '00081030', // Study Description
             label: '检查描述',
             showLabel: true,
             format: TagFormat.RAW,
-            defaultValue: '',
+            defaultValue: '',
           },
           {
             tag: '00080060', // Modality
             label: '设备类型',
             showLabel: true,
             format: TagFormat.RAW,
-            defaultValue: '',
+            defaultValue: '',
           },
         ],
       },
@@ -122,21 +122,21 @@ export class LocalOverlayConfigAdapter implements IOverlayConfigProvider {
             label: '图像编号',
             showLabel: true,
             format: TagFormat.INTEGER,
-            defaultValue: '',
+            defaultValue: '',
           },
           {
             tag: '00280010', // Rows
             label: '行数',
             showLabel: true,
             format: TagFormat.INTEGER,
-            defaultValue: '',
+            defaultValue: '',
           },
           {
             tag: '00280011', // Columns
             label: '列数',
             showLabel: true,
             format: TagFormat.INTEGER,
-            defaultValue: '',
+            defaultValue: '',
           },
         ],
       },
@@ -152,21 +152,21 @@ export class LocalOverlayConfigAdapter implements IOverlayConfigProvider {
             label: '机构',
             showLabel: true,
             format: TagFormat.RAW,
-            defaultValue: '',
+            defaultValue: '',
           },
           {
             tag: '00080070', // Manufacturer
             label: '制造商',
             showLabel: true,
             format: TagFormat.RAW,
-            defaultValue: '',
+            defaultValue: '',
           },
           {
             tag: '00181000', // Device Serial Number
             label: '设备序列号',
             showLabel: true,
             format: TagFormat.RAW,
-            defaultValue: '',
+            defaultValue: '',
           },
         ],
       },

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

@@ -1245,6 +1245,8 @@ const StackViewer = ({
       } else {
         console.log(`[StackViewer] Disabling DicomOverlay for viewport: ${viewportId}`);
         toolGroup.setToolDisabled(DicomOverlayTool.toolName);
+        //再次启用,表示关闭四角信息
+        toolGroup.setToolEnabled(DicomOverlayTool.toolName);
       }
     } catch (error) {
       console.error(`[StackViewer] Error toggling DicomOverlay:`, error);