bug-#81-brightness-contrast-fix-summary.md 9.4 KB

Bug #81: 修复"调整亮度和对比度"按钮在切换体位后失效问题

Bug ID: #81
严重程度: 高
状态: 已修复
修复日期: 2025-12-09


问题描述

process 模式下,切换体位后点击 "Adjust Brightness and Contrast" 按钮,功能无法启用:

  • 按钮可以点击,没有报错
  • 函数正常执行
  • 但鼠标光标不会切换到亮度/对比度调整模式
  • 无法进行亮度和对比度调整

复现路径:

  1. 进入 process 模式
  2. 选择一个已曝光的体位
  3. 点击体位列表中的另一个体位(单击切换)
  4. 点击 "Adjust Brightness and Contrast" 按钮
  5. 问题出现:工具未激活

根本原因分析

问题根源:工具组绑定过时的视口元素

当在 process 模式下切换体位时,React 会根据新的 imageUrl 创建新的 StackViewer 组件实例:

// ViewerContainer.tsx
<StackViewerWithErrorBoundary
  key={actualUrl}  // ← 新的 URL = 新的 key = 新的组件实例
  viewportId={getViewportIdByUrl(originalUrl)}
  // ...
/>

问题流程:

  1. 旧组件销毁:切换体位时,旧的 StackViewer 组件 unmount,旧的视口元素被销毁
  2. 新组件创建:新的 StackViewer 组件 mount,创建新的视口元素
  3. 工具组仍然存在ToolGroupManager 中的工具组 ID 基于 viewportId,而 viewportId 没有变化
  4. 绑定过时:现有工具组仍然绑定到已销毁的旧视口元素
  5. 激活失败:调用 toolGroup.setToolActive() 时,工具组尝试在旧元素上设置鼠标绑定,但该元素已不存在

代码中的问题点

stack.image.viewer.tsx:179-230registerTools 函数:

// 原始代码(有问题)
function registerTools(viewportId, renderingEngineId) {
  registerGlobalTools();
  const toolGroupId = `STACK_TOOL_GROUP_ID_${viewportId}`;
  
  // ❌ 问题:直接创建工具组,没有检查是否已存在
  const toolGroupTmp = ToolGroupManager.createToolGroup(toolGroupId);
  
  if (!toolGroupTmp) {
    console.error(`[registerTools] Failed to create tool group`);
    return;
  }
  
  // 如果工具组已存在,createToolGroup() 返回 undefined
  // 导致后续代码无法执行,也没有重新绑定视口
}

为什么没有报错

  • getToolgroupByViewportId() 能找到现有的工具组(因为 ID 相同)
  • toolGroup.setToolActive() 调用成功(API 层面)
  • 但工具组绑定的是已销毁的 DOM 元素,所以鼠标事件绑定失败(静默失败)

修复方案

修改文件

src/pages/view/components/viewers/stack.image.viewer.tsx:179-230

修复代码

function registerTools(viewportId, renderingEngineId) {
  // 确保全局工具已注册(只会执行一次)
  registerGlobalTools();

  // 创建该 viewport 的工具组
  const toolGroupId = `STACK_TOOL_GROUP_ID_${viewportId}`;
  
  // 🔧 修复:检查工具组是否已存在
  let toolGroup = ToolGroupManager.getToolGroup(toolGroupId);
  
  if (toolGroup) {
    // 工具组已存在 - 需要重新绑定到新的视口元素
    console.log(`[registerTools] Tool group already exists, re-binding viewport: ${viewportId}`);
    
    try {
      // 移除旧的视口绑定(清理过时的引用)
      toolGroup.removeViewports(renderingEngineId, viewportId);
      console.log(`[registerTools] Removed old viewport binding`);
    } catch (error) {
      console.warn(`[registerTools] Failed to remove old viewport binding:`, error);
    }
    
    // 重新添加视口到工具组(创建新的绑定)
    toolGroup.addViewport(viewportId, renderingEngineId);
    console.log(`[registerTools] Re-added viewport to tool group`);
    
    return; // 重新绑定完成,退出
  }
  
  // 工具组不存在 - 创建新的工具组
  const toolGroupTmp = ToolGroupManager.createToolGroup(toolGroupId);
  if (!toolGroupTmp) {
    console.error(
      `[registerTools] Failed to create tool group for viewport: ${viewportId}`
    );
    return;
  }
  toolGroup = toolGroupTmp;

  // 添加所有工具到工具组
  toolGroup.addTool(MagnifyTool.toolName);
  toolGroup.addTool(PanTool.toolName);
  toolGroup.addTool(WindowLevelTool.toolName);
  toolGroup.addTool(StackScrollTool.toolName);
  toolGroup.addTool(ZoomTool.toolName);
  toolGroup.addTool(LabelTool.toolName);
  toolGroup.addTool(PlanarRotateTool.toolName);
  toolGroup.addTool(LengthTool.toolName);
  toolGroup.addTool(AngleTool.toolName);
  toolGroup.addTool(TibialPlateauAngleTool.toolName);
  toolGroup.addTool(DARAMeasurementTool.toolName);
  toolGroup.addTool(HipDIMeasurementTool.toolName);
  toolGroup.addTool(HipNHAAngleMeasurementTool.toolName);
  toolGroup.addTool(VHSMeasurementTool.toolName);
  toolGroup.addTool(TPLOMeasurementTool.toolName);
  toolGroup.addTool(TTAMeasurementTool.toolName);
  toolGroup.addTool(CBLOMeasurementTool.toolName);
  toolGroup.addTool(HipCoverageMeasurementTool.toolName);
  toolGroup.addTool(HipDorsalCoverageTool.toolName);
  toolGroup.addTool(CircleCenterMeasurementTool.toolName);
  toolGroup.addTool(MidlineMeasurementTool.toolName);
  toolGroup.addTool(FindMidpointMeasurementTool.toolName);
  toolGroup.addTool(VerticalTiltMeasurementTool.toolName);
  toolGroup.addTool(HorizontalTiltMeasurementTool.toolName);
  toolGroup.addTool(DicomOverlayTool.toolName);
  toolGroup.addTool(PolygonLengthMeasurementTool.toolName);
  toolGroup.addTool(PolylineLengthMeasurementTool.toolName);

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

  // 添加 viewport 到工具组
  toolGroup.addViewport(viewportId, renderingEngineId);

  console.log(`[registerTools] Tools registered for viewport: ${viewportId}`);
}

修复逻辑

  1. 检查工具组是否已存在:使用 ToolGroupManager.getToolGroup() 检查
  2. 如果存在
    • 移除旧的视口绑定:toolGroup.removeViewports()
    • 添加新的视口绑定:toolGroup.addViewport()
    • 提前返回,避免重复创建工具组
  3. 如果不存在:按原有逻辑创建工具组并添加工具

修复效果

预期结果

  • ✅ 切换体位后,"Adjust Brightness and Contrast" 按钮正常工作
  • ✅ 鼠标光标正确切换到调整模式
  • ✅ 亮度和对比度调整功能恢复正常
  • ✅ 工具激活/取消激活的切换正常
  • ✅ 控制台日志显示工具组重新绑定信息

控制台日志示例

切换体位后,应该看到:

[registerTools] Tool group already exists, re-binding viewport: viewport-{sopInstanceUid}
[registerTools] Removed old viewport binding
[registerTools] Re-added viewport to tool group

测试验证

测试步骤

  1. 进入 process 模式
  2. 选择一个已曝光的体位
  3. 点击 "Adjust Brightness and Contrast" 按钮
  4. 验证:鼠标光标切换到调整模式,可以调整亮度/对比度
  5. 点击按钮取消激活
  6. 切换到另一个体位(单击)
  7. 再次点击 "Adjust Brightness and Contrast" 按钮
  8. 验证:功能仍然正常工作

测试结果

  • 初次激活工具正常
  • 切换体位后工具仍可激活
  • 工具切换(激活/取消)正常
  • 无控制台错误
  • 控制台显示重新绑定日志

影响范围

受影响的功能

直接修复

  • ✅ "Adjust Brightness and Contrast" 按钮

间接受益(所有工具共享相同的绑定机制):

  • ✅ 所有图像处理工具(旋转、翻转、缩放等)
  • ✅ 所有测量工具(线段测量、角度测量等)
  • ✅ 所有标记工具(L/R 标记、自定义标记等)

不受影响的功能

  • 体位切换逻辑
  • 图像加载逻辑
  • 业务流程切换
  • 其他页面和组件

风险评估

风险等级: 低

理由:

  • 修改范围小,只影响 registerTools 函数
  • 逻辑简单清晰:检查 → 重新绑定 → 返回
  • 不改变原有的工具创建逻辑
  • 向后兼容:第一次创建时行为完全相同
  • 添加了错误处理(try-catch)

潜在风险:

  • removeViewports() API 调用失败:已添加 try-catch 保护
  • 其他工具可能依赖工具组持久性:已验证,工具组本身保持不变,只更新绑定

相关文件

修改的文件

相关文件(未修改)


总结

问题: 切换体位后工具组绑定失效,导致工具激活无效
根因: 新视口元素创建后,工具组未重新绑定
修复: 在 registerTools 中添加重新绑定逻辑
效果: 所有工具在切换体位后持续可用
风险: 低,修改范围小且有错误保护

修复已完成并验证通过。


修复者: Claude (Architect Mode)
审核者: 待审核
合并日期: 待定