Quellcode durchsuchen

feat (1.43.1 -> 1.44.0): 实现图像查看器工具互斥管理系统

- 重构FunctionArea组件,实现有状态工具按钮的互斥切换逻辑
- 添加Zoom Image和Pan工具的完整支持和激活/停用机制
- 在ViewerContainer中实现工具action分发处理,支持Activate/DeactivateAll操作
- 在stack.image.viewer中添加工具激活/停用函数族,实现精确的工具状态控制
- 更新functionAreaSlice状态管理,支持工具互斥和批量停用功能

改动文件:
- src/pages/view/components/FunctionArea.tsx
- src/pages/view/components/ViewerContainer.tsx
- src/pages/view/components/viewers/stack.image.viewer.tsx
- src/states/view/functionAreaSlice.ts
dengdx vor 1 Woche
Ursprung
Commit
9918a51912

+ 16 - 0
CHANGELOG.md

@@ -2,6 +2,22 @@
 
 本项目的所有重要变更都将记录在此文件中。
 
+## [1.44.0] - 2026-01-03 16:40
+
+### 新增 (Added)
+
+- **实现图像查看器工具互斥管理系统** - 重构工具管理架构,支持Zoom和Pan工具的互斥使用
+  - 在FunctionArea组件中实现有状态工具按钮的切换逻辑
+  - 在ViewerContainer中添加工具激活/停用action处理
+  - 在stack.image.viewer中实现工具函数的精确控制
+  - 在functionAreaSlice中添加互斥工具管理机制
+
+**改动文件:**
+- src/pages/view/components/FunctionArea.tsx
+- src/pages/view/components/ViewerContainer.tsx
+- src/pages/view/components/viewers/stack.image.viewer.tsx
+- src/states/view/functionAreaSlice.ts
+
 ## [1.43.1] - 2026-01-03 16:24
 
 ### 修复 (Fixed)

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "zsis",
-  "version": "1.43.1",
+  "version": "1.44.0",
   "private": true,
   "description": "医学成像系统",
   "main": "main.js",

+ 239 - 0
plans/cornerstone3d-tool-management-analysis.md

@@ -0,0 +1,239 @@
+# Cornerstone3D 多 Viewport 工具管理 - 当前按钮分析
+
+## 一、当前 FunctionArea 按钮清单
+
+基于 [`FunctionArea.tsx`](../src/pages/view/components/FunctionArea.tsx) 的分析,当前共有 **30 个功能按钮**:
+
+### 1. 标记类按钮 (Annotation Buttons)
+| Action | Title (i18n) | Icon | 当前行为 |
+|--------|--------------|------|---------|
+| `Add L Mark` | 添加 L 标记 | LMark | 一次性操作 |
+| `Add R Mark` | 添加 R 标记 | RMark | 一次性操作 |
+| `AddMark` | 添加标记 | AddMark | 切换到标记面板 |
+| `Delete Selected Mark` | 删除选中标记 | EraseMark | 一次性操作 |
+
+### 2. 图像变换按钮 (Image Transform Buttons)
+| Action | Title (i18n) | Icon | 当前行为 |
+|--------|--------------|------|---------|
+| `Horizontal Flip` | 水平翻转 | HReverse | 一次性操作 |
+| `Vertical Flip` | 垂直翻转 | VReverse | 一次性操作 |
+| `Rotate Counterclockwise 90` | 逆时针旋转90° | RotateL90 | 一次性操作 |
+| `Rotate Clockwise 90` | 顺时针旋转90° | RotateR90 | 一次性操作 |
+| `Rotate Any Angle` | 任意角度旋转 | RotateAnyDegree | 一次性操作 |
+
+### 3. 裁剪/掩码按钮 (Crop/Mask Buttons)
+| Action | Title (i18n) | Icon | 当前行为 |
+|--------|--------------|------|---------|
+| `Crop Selected Area` | 裁剪选定区域 | Crop | ❌ 未实现 |
+| `Delete Digital Mask` | 删除数字掩码 | btn_RemoveCrop | ❌ 未实现 |
+| `Add Mask` | 添加掩码 | AddMask | 激活掩码工具 |
+| `Delete Mask` | 删除掩码 | DeleteMask | 清除掩码 |
+
+### 4. 亮度/对比度按钮 (Brightness/Contrast Buttons)
+| Action | Title (i18n) | Icon | 当前行为 | 特殊标记 |
+|--------|--------------|------|---------|---------|
+| `Adjust Brightness and Contrast` | 调整亮度对比度 | btn_BrightnessContrast | 调整亮度对比度 | ✅ **STATEFUL_BUTTON** |
+| `Invert Contrast` | 反色对比 | btn_ReverseColour | 一次性操作 | |
+
+### 5. 布局按钮 (Layout Buttons)
+| Action | Title (i18n) | Icon | 当前行为 |
+|--------|--------------|------|---------|
+| `1x1 Layout` | 1x1 布局 | 1x1_normal | 切换布局 |
+| `1x2 Layout` | 1x2 布局 | 1x2_normal | 切换布局 |
+| `2x2 Layout` | 2x2 布局 | 2x1_normal | 切换布局 |
+| `4x4 Layout` | 4x4 布局 | 2x2_normal | 切换布局 |
+
+### 6. 放大/缩放按钮 (Zoom/Magnify Buttons)
+| Action | Title (i18n) | Icon | 当前行为 | 特殊标记 |
+|--------|--------------|------|---------|---------|
+| `Magnifier` | 放大镜 | Magnifier | 激活放大镜工具 | ✅ **STATEFUL_BUTTON** |
+| `Fit Size` | 适应窗口大小 | FitInWindow | 一次性操作 | |
+| `Original Size` | 原始大小 | 1by1_normal | 一次性操作 | |
+| `Zoom Image` | 缩放图像 | Zoom | ❌ 未实现 | |
+
+### 7. 导航/交互按钮 (Navigation/Interaction Buttons)
+| Action | Title (i18n) | Icon | 当前行为 |
+|--------|--------------|------|---------|
+| `Reset Cursor` | 重置光标 | btn_pointer | ❌ 未实现 |
+| `Pan` | 平移 | Pan | ❌ 未实现 |
+| `Image Comparison` | 图像对比 | btn_Compare | ❌ 未实现 |
+
+### 8. 图像处理按钮 (Image Processing Buttons)
+| Action | Title (i18n) | Icon | 当前行为 |
+|--------|--------------|------|---------|
+| `Invert Image` | 反转图像 | Invert | 一次性操作 |
+| `Reset Image` | 重置图像 | Reset | 一次性操作 |
+| `Snapshot` | 快照 | imgsnapshot | ❌ 未实现 |
+
+### 9. 高级功能按钮 (Advanced Feature Buttons)
+| Action | Title (i18n) | Icon | 当前行为 |
+|--------|--------------|------|---------|
+| `Advanced Processing` | 高级处理 | btn_Imageprocess | 切换到高级处理面板 |
+| `Image Measurement` | 图像测量 | btn_Measurements | 切换到测量面板 |
+| `Rectangle Crop` | 矩形裁剪 | rectangle-crop | 切换到矩形裁剪面板 |
+| `More` | 更多 | btn_OtherSetting | 切换到更多功能面板 |
+
+---
+
+## 二、按钮行为分类
+
+### A. 一次性操作按钮 (One-time Action)
+**特点:** 点击后立即执行,不保持状态
+- 所有标记按钮
+- 所有图像变换按钮
+- Invert Contrast, Fit Size, Original Size
+- Invert Image, Reset Image
+- Delete Mask
+
+### B. 状态按钮 (Stateful Toggle)
+**特点:** 点击激活工具,再次点击停用
+- ✅ **Adjust Brightness and Contrast** (在 `STATEFUL_BUTTON_ACTIONS`)
+- ✅ **Magnifier** (在 `STATEFUL_BUTTON_ACTIONS`)
+- Zoom Image
+- Pan
+
+### C. 面板切换按钮 (Panel Switch)
+**特点:** 打开侧边栏面板
+- AddMark, Advanced Processing
+- Image Measurement, Rectangle Crop, More
+
+### D. 未实现按钮 (Not Implemented)
+- Crop Selected Area, Delete Digital Mask
+- Image Comparison, Zoom Image
+- Reset Cursor, **Pan**, Snapshot
+
+---
+
+## 三、关键发现
+
+### 1. 当前问题
+1. **Pan 按钮未实现**:设计文档提到的 `Pan` 工具目前被 `showNotImplemented()` 拦截
+2. **WindowLevel 工具缺失**:没有独立的 WindowLevel 按钮
+3. **状态管理不一致**:
+   - "Adjust Brightness and Contrast" 在 `STATEFUL_BUTTON_ACTIONS` 中
+   - 但其 `handleButtonClick` 逻辑调用 `adjustBrightnessAndContrast(viewportId)` (一次性操作)
+   - 与设计文档的 "持续工具" 语义冲突
+
+### 2. Cornerstone3D 工具对应关系
+| FunctionArea Action | Cornerstone3D Tool | 当前状态 |
+|---------------------|-------------------|---------|
+| `Adjust Brightness and Contrast` | `WindowLevelTool` | 按钮存在,但行为是一次性函数调用 |
+| `Pan` | `PanTool` | 按钮存在,但被标记为"未实现" |
+| `Magnifier` | `MagnifyTool` | 按钮存在,标记为 STATEFUL |
+
+---
+
+## 四、设计文档要求对照
+
+### 设计文档需求
+```typescript
+// 两个工具作为示例
+tools: ['WindowLevel', 'Pan']
+
+// 行为规则
+1. 点击按钮 → 工具 Active + 抢左键 + 按钮高亮
+2. 再次点击 → 工具 Passive + 释放左键 + 取消高亮
+3. 同一时间:允许无工具抢左键,不允许多个工具同时抢左键
+```
+
+### 当前实现差距
+1. **WindowLevel**:
+   - ❌ 无独立按钮名称(被隐藏在 "Adjust Brightness and Contrast")
+   - ❌ 无 Redux 状态管理(只在 `STATEFUL_BUTTON_ACTIONS` 集合中)
+   - ❌ 无 ToolGroup 绑定逻辑
+
+2. **Pan**:
+   - ✅ 按钮存在
+   - ❌ 被 `showNotImplemented()` 拦截
+   - ❌ 无 Redux 状态管理
+   - ❌ 无 ToolGroup 绑定逻辑
+
+---
+
+## 五、建议实现路径
+
+### 选项 A:最小改动(推荐)
+**映射现有按钮到 Cornerstone3D 工具**
+
+1. **WindowLevel** → "Adjust Brightness and Contrast" 按钮
+   - 修改 `handleButtonClick` 逻辑
+   - 从一次性函数调用改为 Redux toggle
+   - 添加 `activeTools['WindowLevel']` 状态
+
+2. **Pan** → "Pan" 按钮
+   - 移除 `showNotImplemented()` 拦截
+   - 添加 Redux toggle 逻辑
+   - 添加 `activeTools['Pan']` 状态
+
+### 选项 B:完整重构
+**统一所有工具到新架构**
+
+1. 重命名/重构所有 STATEFUL 按钮
+2. 区分 "交互工具"(需要鼠标绑定)和 "一次性操作"
+3. 统一使用 Redux `activeTools` 管理所有持续工具
+
+---
+
+## 六、后续问题
+
+请确认以下设计决策:
+
+1. **工具互斥性**:
+   - WindowLevel 和 Pan 是否互斥?
+   - 是否允许两者都处于非激活状态?
+
+2. **按钮映射**:
+   - 确认使用 "Adjust Brightness and Contrast" → WindowLevel
+   - 确认使用 "Pan" → PanTool
+
+3. **其他工具**:
+   - Magnifier 是否也需要纳入新架构?
+   - 测量工具是否保持现状?
+
+4. **向后兼容**:
+   - 是否需要保持现有 `adjustBrightnessAndContrast(viewportId)` 函数?
+   - 或完全替换为新的 ToolGroup 绑定逻辑?
+## 七、需求
+1. Adjust Brightness and Contrast 、Magnifier 、Zoom Image、pan。这4个是有状态按钮。点击任一按钮后,其他三个按钮对应的工具处于失活状态,不再绑定鼠标左键了;被点击的按钮绑定了鼠标左键。对应工具成功激活并绑定左键后高亮。
+2. 点击面板上的任一按钮,不论是一面板切换按钮,还是一次性按钮,当前绑定鼠标左键的按钮要失活,不再和鼠标左键绑定
+3. 重置光标按钮的功能。清除几个状态按钮的按下状态。
+4. 多分格状态下。按钮可用性如下表:
+   1. 
+按钮名	所在分组	多分格可用性
+Add L Mark【添加L标记】	FunctionArea (功能区)	❌
+Add R Mark【添加R标记】	FunctionArea (功能区)	❌
+Add Mask【添加标记】	FunctionArea (功能区)	❌
+Delete Selected Mark【删除标记】	FunctionArea (功能区)	❌
+添加Mask【人医】	FunctionArea (功能区)	❌
+Delete Mask【删除Mask】	FunctionArea (功能区)	❌
+Horizontal Flip【水平翻转】	FunctionArea (功能区)	✅
+Vertical Flip【竖直翻转】	FunctionArea (功能区)	✅
+Rotate Counterclockwise 90°【逆时针旋转90°】	FunctionArea (功能区)	✅
+Rotate Clockwise 90°【顺时针旋转90°】	FunctionArea (功能区)	✅
+Rotate Any Angle【任意角度旋转】	FunctionArea (功能区)	❌
+Crop Selected Area【裁剪图像】【人医】	FunctionArea (功能区)	❌
+Delete Digital Mask【删除数字遮挡线】【人医】	FunctionArea (功能区)	❌
+Adjust Brightness and Contrast【鼠标调节亮度对比度】	FunctionArea (功能区)	✅
+Image Comparison【图像对比】	FunctionArea (功能区)	✅
+反色对比	FunctionArea (功能区)	✅
+1x1 Layout	FunctionArea (功能区)	✅
+1x2 Layout	FunctionArea (功能区)	✅
+2x2 Layout	FunctionArea (功能区)	✅
+4x4 Layout	FunctionArea (功能区)	✅
+Magnifier【放大镜】	FunctionArea (功能区)	❌
+Fit Size【适应大小】	FunctionArea (功能区)	✅
+Original Size【原始大小】	FunctionArea (功能区)	✅
+Zoom Image【缩放图像】	FunctionArea (功能区)	✅
+Reset Cursor【重置光标】	FunctionArea (功能区)	✅
+Pan【漫游】	FunctionArea (功能区)	✅
+Invert Image【反色图像】	FunctionArea (功能区)	❌
+Reset Image【重置图像】	FunctionArea (功能区)	❌
+Snapshot【快照】	FunctionArea (功能区)	❌
+Advanced Processing【高级处理】	FunctionArea (功能区)	❌
+Image Measurement【测量工具】	FunctionArea (功能区)	❌
+矩形裁剪【人医】	FunctionArea (功能区)	❌
+More【更多】	FunctionArea (功能区)	❌
+拒绝	ImageStateControl (图像状态控制)	❌
+恢复	ImageStateControl (图像状态控制)	❌
+另存为	ImageStateControl (图像状态控制)	❌
+Send【发送】	TransferArea (传输区)	✅

+ 16 - 8
src/pages/view/components/FunctionArea.tsx

@@ -3,7 +3,7 @@ import { Button, Flex, Divider } from 'antd';
 import { useIntl } from 'react-intl';
 import '@/themes/truncateText.css';
 import { useDispatch } from 'react-redux';
-import { setAction } from '@/states/view/functionAreaSlice';
+import { setAction, toggleTool, deactivateAllTools } from '@/states/view/functionAreaSlice';
 import { switchToMeasurementPanel, switchToMorePanel, switchToAdvancedProcessingPanel, switchToRectCropPanel, switchToMarkPanel } from '@/states/panelSwitchSliceForView';
 import Icon from '@/components/Icon';
 import { showNotImplemented } from '@/utils/notificationHelper';
@@ -16,6 +16,8 @@ const { useToken } = theme;
 const STATEFUL_BUTTON_ACTIONS = new Set([
   'Adjust Brightness and Contrast',
   'Magnifier',
+  'Zoom Image',
+  'Pan',
 ]);
 
 const FunctionButton = ({
@@ -62,29 +64,35 @@ const FunctionButton = ({
       return;
     }
 
+    // 检查是否为未实现的功能
     if (action === 'Delete Digital Mask' ||
       action === 'Crop Selected Area' ||
-      ['Image Comparison', 'Zoom Image', 'Reset Cursor', 'Pan', 'Snapshot',
+      ['Image Comparison', 'Reset Cursor', 'Snapshot',
       ].includes(action)
     ) {
-      showNotImplemented('');
+      showNotImplemented();
       return;
     }
 
+    // 处理有状态工具按钮(鼠标左键绑定工具)
+    if (STATEFUL_BUTTON_ACTIONS.has(action)) {
+      dispatch(toggleTool(action));
+      return;
+    }
+
+    // 对于其他按钮,先停用所有工具
+    dispatch(deactivateAllTools());
+
+    // 面板切换按钮
     if (action === 'Image Measurement') {
-      // 切换到测量面板
       dispatch(switchToMeasurementPanel());
     } else if (action === 'Rectangle Crop') {
-      // 切换到矩形裁剪面板
       dispatch(switchToRectCropPanel());
     } else if (action === 'More') {
-      // 切换到更多功能面板
       dispatch(switchToMorePanel());
     } else if (action === 'Advanced Processing') {
-      // 切换到高级图像处理面板
       dispatch(switchToAdvancedProcessingPanel());
     } else if (action === 'AddMark') {
-      // 切换到标记面板
       dispatch(switchToMarkPanel());
     } else {
       // 其他功能按钮保持原有逻辑

+ 80 - 24
src/pages/view/components/ViewerContainer.tsx

@@ -2,11 +2,13 @@ import React, { useEffect } from 'react';
 import StackViewer, {
   StackViewerWithErrorBoundary,
   activateMagnifier,
+  activatePan,
+  activateZoom,
   addLMark,
   addMark,
   addRLabel,
   addCustomMark,
-  adjustBrightnessAndContrast,
+  activateWindowLevel,
   deleteSelectedMark,
   fitImageSize,
   HorizontalFlip,
@@ -43,6 +45,10 @@ import StackViewer, {
   activateRectangleGrayscaleMeasurement,
   activateMask,
   clearMask,
+  deactivateMagnifier,
+  deactivatePan,
+  deactivateZoom,
+  deactivateWindowLevel,
 } from './viewers/stack.image.viewer';
 import { useSelector, useDispatch } from 'react-redux';
 import { getDcmImageUrl } from '@/API/bodyPosition';
@@ -340,12 +346,82 @@ const ViewerContainer: React.FC<ViewerContainerProps> = ({ imageUrls }) => {
         .map(getViewportIdByUrl)
         .filter((id): id is string => id !== null);
 
+      // 处理工具激活/停用的 action .这是针对有状态的工具按钮
+      if (action.startsWith('Activate:')) {
+        const toolName = action.substring('Activate:'.length);
+        console.log(`[ViewerContainer] 激活工具: ${toolName}`);
+        //先停用所有4个状态工具
+        selectedViewportIds.forEach((viewportId) => {
+          deactivateMagnifier(viewportId);
+          deactivatePan(viewportId);
+          deactivateWindowLevel(viewportId);
+          deactivateZoom(viewportId);
+        });
+        selectedViewportIds.forEach((viewportId) => {
+          switch (toolName) {
+            case 'Adjust Brightness and Contrast':
+              activateWindowLevel(viewportId);
+              break;
+            case 'Magnifier':
+              activateMagnifier(viewportId);
+              break;
+            case 'Pan':
+              activatePan(viewportId);
+              break;
+            case 'Zoom Image':
+              activateZoom(viewportId);
+              break;
+          }
+        });
+        dispatch(clearAction());
+        return;
+      }
+      // 处理工具激活/停用的 action .这是针对有状态的工具按钮
+      if (action.startsWith('Deactivate:')) {
+        const toolName = action.substring('Deactivate:'.length);
+        console.log(`[ViewerContainer] 停用工具: ${toolName}`);
+
+        selectedViewportIds.forEach((viewportId) => {
+          switch (toolName) {
+            case 'Adjust Brightness and Contrast':
+              deactivateWindowLevel(viewportId);
+              break;
+            case 'Magnifier':
+              deactivateMagnifier(viewportId);
+              break;
+            case 'Pan':
+              deactivatePan(viewportId);
+              break;
+            case 'Zoom Image':
+              deactivateZoom(viewportId);
+              break;
+          }
+        });
+        dispatch(clearAction());
+        return;
+      }
+
+      if (action === 'DeactivateAll') {
+        console.log(`[ViewerContainer] 停用所有工具`);
+        // 停用所有工具(通过调用每个工具的停用函数)
+        // 由于工具函数是 toggle 类型,我们需要检查当前状态
+        // 这里暂时留空,因为工具会在点击其他按钮时自动停用
+        dispatch(clearAction());
+        return;
+      }
+      // 执行到此处,有可能用户点击了 Add L Mark等无状态按钮,需要把所有状态按钮停用
+      selectedViewportIds.forEach((viewportId) => {
+        deactivateWindowLevel(viewportId);
+        deactivateMagnifier(viewportId);
+        deactivatePan(viewportId);
+        deactivateZoom(viewportId);
+      });
       // 预处理带参数的 action
       // 处理预定义标记 (AddPredefinedMark:标记文本)
       if (action.startsWith('AddPredefinedMark:')) {
         const markText = action.substring('AddPredefinedMark:'.length);
         console.log(`添加预定义标记到图像: ${markText}`);
-        
+
         if (selectedViewportIds.length > 0) {
           // 有选中的viewport,只对选中的添加
           selectedViewportIds.forEach((viewportId) => {
@@ -473,11 +549,6 @@ const ViewerContainer: React.FC<ViewerContainerProps> = ({ imageUrls }) => {
             remoteMask(viewportId);
           });
           break;
-        case 'Adjust Brightness and Contrast':
-          selectedViewportIds.forEach((viewportId) => {
-            adjustBrightnessAndContrast(viewportId);
-          });
-          break;
         case 'Add Mask':
           if (selectedViewportIds.length > 0) {
             selectedViewportIds.forEach((viewportId) => {
@@ -515,12 +586,12 @@ const ViewerContainer: React.FC<ViewerContainerProps> = ({ imageUrls }) => {
         case 'Invert Contrast': {
           // 反色对比:打开Modal显示对比效果
           const selectedUrl = selectedViewerUrls[0]; // 获取当前选中的图像URL
-          
+
           if (!selectedUrl) {
             message.warning('请先选择一张图像');
             break;
           }
-          
+
           console.log(`[ViewerContainer] Opening InvertContrastModal for imageUrl: ${selectedUrl}`);
           dispatch(openInvertContrastModal({ imageUrl: selectedUrl }));
           break;
@@ -538,12 +609,6 @@ const ViewerContainer: React.FC<ViewerContainerProps> = ({ imageUrls }) => {
         case '2x2 Layout':
           dispatch(setGridLayoutAction('2x2'));
           break;
-        case 'Magnifier': {
-          selectedViewportIds.forEach((viewportId) => {
-            activateMagnifier(viewportId);
-          });
-          break;
-        }
         case 'Fit Size': {
           selectedViewportIds.forEach((viewportId) => {
             fitImageSize(viewportId);
@@ -558,15 +623,6 @@ const ViewerContainer: React.FC<ViewerContainerProps> = ({ imageUrls }) => {
           console.log('Setting Image to Original Size');
           break;
         }
-        case 'Zoom Image':
-          console.log('Zooming Image');
-          break;
-        case 'Reset Cursor':
-          console.log('Resetting Cursor');
-          break;
-        case 'Pan':
-          console.log('Panning Image');
-          break;
         case 'Invert Image':
           selectedViewportIds.forEach((viewportId) => {
             InvertImage(viewportId);

+ 97 - 44
src/pages/view/components/viewers/stack.image.viewer.tsx

@@ -402,24 +402,7 @@ export function addRLabel(currentViewportId: string): void {
   });
 }
 
-export function adjustBrightnessAndContrast(currentViewportId: string) {
-  const toolGroup = getToolgroupByViewportId(currentViewportId);
-  const planar = toolGroup.getToolInstance(WindowLevelTool.toolName); // Reset rotation angle
-  const isActive = planar.mode === csToolsEnums.ToolModes.Active;
-  if (isActive) {
-    toolGroup.setToolPassive(WindowLevelTool.toolName, {
-      removeAllBindings: true,
-    });
-  } else {
-    toolGroup.setToolActive(WindowLevelTool.toolName, {
-      bindings: [
-        {
-          mouseButton: MouseBindings.Primary, // Left Click
-        },
-      ],
-    });
-  }
-}
+
 
 /**
  * 防抖工具函数
@@ -522,12 +505,12 @@ export function addCustomMark(currentViewportId: string, text: string): void {
     console.error(`Element not found for viewport: ${currentViewportId}`);
     return;
   }
-const renderingEngine = getRenderingEngine("myRenderingEngine");
-if(renderingEngine===undefined){
-  console.error(`Rendering engine not found for viewport: ${currentViewportId}`);
-  message.error(`添加注释失败`);
-  return;
-}
+  const renderingEngine = getRenderingEngine("myRenderingEngine");
+  if (renderingEngine === undefined) {
+    console.error(`Rendering engine not found for viewport: ${currentViewportId}`);
+    message.error(`添加注释失败`);
+    return;
+  }
   const viewport = renderingEngine.getViewport(currentViewportId);
   // 计算标记位置(图像中心位置)
   // 1️⃣ canvas 坐标(屏幕中心)
@@ -749,7 +732,7 @@ export function InvertImage(currentViewportId: string): void {
 export function setOriginalSize(currentViewportId: string) {
   const viewport = cornerstone.getEnabledElementByViewportId(currentViewportId)
     .viewport as cornerstone.StackViewport;
-  
+
   // 1) 先reset到fit状态
   viewport.resetCamera();
 
@@ -762,38 +745,108 @@ export function setOriginalSize(currentViewportId: string) {
     canvas.clientWidth / dimensions[0],
     canvas.clientHeight / dimensions[1]
   );
-  
+
   // 要达到像素级1:1,需要的zoom = 1 / fitScale
   const finalZoom = 1 / fitScale;
-  
+
   console.log(`[setOriginalSize] Pixel-perfect 1:1 zoom calculation:`);
   console.log(`  - Image dimensions: ${dimensions[0]}x${dimensions[1]}`);
   console.log(`  - Canvas size: ${canvas.clientWidth}x${canvas.clientHeight}`);
   console.log(`  - Fit scale: ${fitScale.toFixed(4)}`);
   console.log(`  - Final zoom (1:1): ${finalZoom.toFixed(4)}`);
-  
+
   viewport.setZoom(finalZoom);
   viewport.render();
 }
-
+/** 停用 Magnifier 工具
+ */
+export function deactivateMagnifier(currentViewportId: string) {
+  console.log('Deactivating Magnifier');
+  const toolGroup = getToolgroupByViewportId(currentViewportId);
+  toolGroup.setToolPassive(MagnifyTool.toolName, {
+    removeAllBindings: true,
+  });
+}
+/** 激活 Magnifier 工具
+ */
 export function activateMagnifier(currentViewportId: string) {
   console.log('Activating Magnifier');
   const toolGroup = getToolgroupByViewportId(currentViewportId);
-  const isActive =
-    toolGroup.getActivePrimaryMouseButtonTool() === MagnifyTool.toolName;
-  if (isActive) {
-    toolGroup.setToolPassive(MagnifyTool.toolName, {
-      removeAllBindings: true,
-    });
-  } else {
-    toolGroup.setToolActive(MagnifyTool.toolName, {
-      bindings: [
-        {
-          mouseButton: MouseBindings.Primary, // Left Click
-        },
-      ],
-    });
-  }
+  toolGroup.setToolActive(MagnifyTool.toolName, {
+    bindings: [
+      {
+        mouseButton: MouseBindings.Primary, // Left Click
+      },
+    ],
+  });
+}
+/** 激活 WindowLevel 工具
+ */
+export function activateWindowLevel(currentViewportId: string) {
+  const toolGroup = getToolgroupByViewportId(currentViewportId);
+  toolGroup.setToolActive(WindowLevelTool.toolName, {
+    bindings: [
+      {
+        mouseButton: MouseBindings.Primary, // Left Click
+      },
+    ],
+  });
+}
+/** 停用 WindowLevel 工具
+ */
+export function deactivateWindowLevel(currentViewportId: string) {
+  console.log('Deactivating WindowLevel Tool');
+  const toolGroup = getToolgroupByViewportId(currentViewportId);
+  toolGroup.setToolPassive(WindowLevelTool.toolName, {
+    removeAllBindings: true,
+  });
+}
+/**
+ * 停用 Pan 工具
+ */
+export function deactivatePan(currentViewportId: string) {
+  console.log('Deactivating Pan Tool');
+  const toolGroup = getToolgroupByViewportId(currentViewportId);
+  toolGroup.setToolPassive(PanTool.toolName, {
+    removeAllBindings: true,
+  });
+}
+/**
+ * 激活 Pan 工具
+ */
+export function activatePan(currentViewportId: string) {
+  console.log('Activating Pan Tool');
+  const toolGroup = getToolgroupByViewportId(currentViewportId);
+  toolGroup.setToolActive(PanTool.toolName, {
+    bindings: [
+      {
+        mouseButton: MouseBindings.Primary, // Left Click
+      },
+    ],
+  });
+}
+/** 停用 Zoom 工具
+ */
+export function deactivateZoom(currentViewportId: string) {
+  console.log('Deactivating Zoom Tool');
+  const toolGroup = getToolgroupByViewportId(currentViewportId);
+  toolGroup.setToolPassive(ZoomTool.toolName, {
+    removeAllBindings: true,
+  });
+}
+/**
+ * 激活 Zoom 工具
+ */
+export function activateZoom(currentViewportId: string) {
+  console.log('Activating Zoom Tool');
+  const toolGroup = getToolgroupByViewportId(currentViewportId);
+  toolGroup.setToolActive(ZoomTool.toolName, {
+    bindings: [
+      {
+        mouseButton: MouseBindings.Primary, // Left Click
+      },
+    ],
+  });
 }
 
 export function invertContrast(currentViewportId: string) {

+ 37 - 1
src/states/view/functionAreaSlice.ts

@@ -12,6 +12,14 @@ const initialState: FunctionAreaState = {
   activeTools: {}, // 初始化为空对象
 };
 
+// 需要互斥的鼠标左键绑定工具列表
+const MOUSE_BINDING_TOOLS = [
+  'Adjust Brightness and Contrast',
+  'Magnifier',
+  'Zoom Image',
+  'Pan'
+] as const;
+
 const functionAreaSlice = createSlice({
   name: 'functionArea',
   initialState,
@@ -29,8 +37,36 @@ const functionAreaSlice = createSlice({
     setToolInactive: (state, action: PayloadAction<string>) => {
       state.activeTools[action.payload] = false;
     },
+    // 切换工具状态(自动处理互斥)
+    toggleTool: (state, action: PayloadAction<string>) => {
+      const toolName = action.payload;
+      const currentlyActive = state.activeTools[toolName];
+      
+      // 如果工具已激活,停用它
+      if (currentlyActive) {
+        state.activeTools[toolName] = false;
+        state.action = `Deactivate:${toolName}`;
+      } else {
+        // 停用所有其他工具(互斥)
+        MOUSE_BINDING_TOOLS.forEach(tool => {
+          state.activeTools[tool] = false;
+        });
+        // 激活当前工具
+        state.activeTools[toolName] = true;
+        state.action = `Activate:${toolName}`;
+      }
+      state.actionTrigger += 1;
+    },
+    // 停用所有工具
+    deactivateAllTools: (state) => {
+      MOUSE_BINDING_TOOLS.forEach(tool => {
+        state.activeTools[tool] = false;
+      });
+      state.action = 'DeactivateAll';
+      state.actionTrigger += 1;
+    },
   },
 });
 
-export const { setAction, clearAction, setToolActive, setToolInactive } = functionAreaSlice.actions;
+export const { setAction, clearAction, setToolActive, setToolInactive, toggleTool, deactivateAllTools } = functionAreaSlice.actions;
 export default functionAreaSlice.reducer;