Kaynağa Gözat

feat: 集成 cornerstone3D 线段测量功能并重构状态管理架构

新增文件:
- src/states/view/measurementPanelSlice.ts: 专门的测量面板状态管理,支持工具状态跟踪和测量结果管理
- src/utils/cornerstoneToolsSetup.ts: 全局工具注册模块,避免重复注册问题
- src/utils/measurementToolManager.ts: 测量工具管理器,统一管理各viewport的测量工具

修改文件:
- config/dev.ts: 更新开环境配置
- src/pages/view/components/MeasurementPanel.tsx: 重构使用专门的measurementPanelSlice action creator
- src/pages/view/components/ViewerContainer.tsx: 分离测量和功能区域的action处理逻辑
- src/pages/view/components/viewers/stack.image.viewer.tsx: 重构工具注册流程,添加线段测量相关函数
- src/states/store.ts: 集成measurementPanel reducer到Redux store

功能特性:
- 支持线段测量工具的激活、停用、清除操作
- 为每个viewport独立管理工具状态
- 提供完整的测量结果跟踪和管理
- 架构清晰,职责分离,便于扩展其他测量工具
sw 2 hafta önce
ebeveyn
işleme
a6328ef874

+ 2 - 2
config/dev.ts

@@ -11,7 +11,7 @@ export default {
     stats: true,
   },
   defineConstants: {
-    MQTT_BROKER_URL_FROM_WEBPACK: '"ws://192.168.110.112:8083/mqtt"',
+    MQTT_BROKER_URL_FROM_WEBPACK: '"ws://192.168.110.13:8083/mqtt"',
   },
   mini: {},
   h5: {
@@ -20,7 +20,7 @@ export default {
     devServer: {
       proxy: {
         '/dr': {
-          target: 'http://192.168.110.112:6001', // 你的后端服务地址
+          target: 'http://192.168.110.13:6001', // 你的后端服务地址
           changeOrigin: true, // 允许跨域
           // pathRewrite: {
           //   '^/dr/api': '' // 可选,用于重写路径

+ 8 - 1
src/pages/view/components/MeasurementPanel.tsx

@@ -3,6 +3,10 @@ import { Layout, Button, Typography, Divider, Flex } from 'antd';
 import { ArrowLeftOutlined } from '@ant-design/icons';
 import { useDispatch } from 'react-redux';
 import { switchToOperationPanel } from '../../../states/panelSwitchSliceForView';
+import {
+  setMeasurementAction,
+  type MeasurementAction,
+} from '../../../states/view/measurementPanelSlice';
 import Icon from '@/components/Icon';
 import '@/themes/truncateText.css';
 
@@ -20,9 +24,12 @@ const FunctionButton = ({
   iconName: string;
   productId?: string;
 }) => {
+  const dispatch = useDispatch();
+
   const handleMeasurementAction = (action: string) => {
     console.log(`执行测量操作: ${action}`);
-    // 这里可以添加具体的测量逻辑
+    // 通过 Redux 分发 action 到 ViewerContainer
+    dispatch(setMeasurementAction(action as MeasurementAction));
   };
 
   return (

+ 172 - 0
src/pages/view/components/ViewerContainer.tsx

@@ -18,10 +18,19 @@ import StackViewer, {
   RotateCounterclockwise90,
   setOriginalSize,
   VerticalFlip,
+  activateLengthMeasurement,
+  deactivateLengthMeasurement,
+  clearLengthMeasurements,
 } from './viewers/stack.image.viewer';
 import { useSelector, useDispatch } from 'react-redux';
 import store, { RootState } from '@/states/store';
 import { clearAction } from '@/states/view/functionAreaSlice';
+import {
+  clearMeasurementAction,
+  selectCurrentMeasurementAction,
+  setToolActive,
+  setToolInactive,
+} from '@/states/view/measurementPanelSlice';
 import * as cornerstone from '@cornerstonejs/core';
 import * as cornerstoneTools from '@cornerstonejs/tools';
 import { SystemMode } from '@/states/systemModeSlice';
@@ -73,6 +82,7 @@ const ViewerContainer: React.FC<ViewerContainerProps> = ({ imageUrls }) => {
   const [selectedViewers, setSelectedViewers] = useState<number[]>([]);
   const [gridLayout, setGridLayout] = useState<string>('1x1');
   const action = useSelector((state: RootState) => state.functionArea.action);
+  const measurementAction = useSelector(selectCurrentMeasurementAction);
   const dispatch = useDispatch();
   const selectedBodyPosition = useSelector(
     (state: RootState) => state.bodyPositionList.selectedBodyPosition
@@ -309,6 +319,49 @@ const ViewerContainer: React.FC<ViewerContainerProps> = ({ imageUrls }) => {
             ApplyColormap(`viewport-${index}`);
           });
           break;
+        // ==================== 线段测量相关操作 ====================
+        case '线段测量':
+          if (selectedViewers.length > 0) {
+            selectedViewers.forEach((index) => {
+              activateLengthMeasurement(`viewport-${index}`);
+            });
+          } else {
+            // 如果没有选中的 viewport,为所有可见的 viewport 激活
+            const visibleViewportCount = getVisibleViewportCount();
+            for (let i = 0; i < visibleViewportCount; i++) {
+              activateLengthMeasurement(`viewport-${i}`);
+            }
+          }
+          console.log('Activating Length Measurement');
+          break;
+        case '清除测量':
+          if (selectedViewers.length > 0) {
+            selectedViewers.forEach((index) => {
+              clearLengthMeasurements(`viewport-${index}`);
+            });
+          } else {
+            // 如果没有选中的 viewport,清除所有可见 viewport 的测量
+            const visibleViewportCount = getVisibleViewportCount();
+            for (let i = 0; i < visibleViewportCount; i++) {
+              clearLengthMeasurements(`viewport-${i}`);
+            }
+          }
+          console.log('Clearing Length Measurements');
+          break;
+        case '停用线段测量':
+          if (selectedViewers.length > 0) {
+            selectedViewers.forEach((index) => {
+              deactivateLengthMeasurement(`viewport-${index}`);
+            });
+          } else {
+            // 如果没有选中的 viewport,停用所有可见 viewport 的测量工具
+            const visibleViewportCount = getVisibleViewportCount();
+            for (let i = 0; i < visibleViewportCount; i++) {
+              deactivateLengthMeasurement(`viewport-${i}`);
+            }
+          }
+          console.log('Deactivating Length Measurement');
+          break;
         default:
           break;
       }
@@ -316,6 +369,125 @@ const ViewerContainer: React.FC<ViewerContainerProps> = ({ imageUrls }) => {
     }
   }, [action]);
 
+  // ==================== 测量面板 Action 处理 ====================
+  useEffect(() => {
+    if (measurementAction) {
+      console.log(`[ViewerContainer] 处理测量操作: ${measurementAction}`);
+
+      // 处理测量相关操作
+      switch (measurementAction) {
+        case '线段测量':
+          if (selectedViewers.length > 0) {
+            selectedViewers.forEach((index) => {
+              const success = activateLengthMeasurement(`viewport-${index}`);
+              if (success) {
+                dispatch(
+                  setToolActive({
+                    toolName: 'LengthTool',
+                    viewportId: `viewport-${index}`,
+                  })
+                );
+              }
+            });
+          } else {
+            // 如果没有选中的 viewport,为所有可见的 viewport 激活
+            const visibleViewportCount = getVisibleViewportCount();
+            for (let i = 0; i < visibleViewportCount; i++) {
+              const success = activateLengthMeasurement(`viewport-${i}`);
+              if (success) {
+                dispatch(
+                  setToolActive({
+                    toolName: 'LengthTool',
+                    viewportId: `viewport-${i}`,
+                  })
+                );
+              }
+            }
+          }
+          console.log('Activating Length Measurement from MeasurementPanel');
+          break;
+
+        case '清除测量':
+          if (selectedViewers.length > 0) {
+            selectedViewers.forEach((index) => {
+              clearLengthMeasurements(`viewport-${index}`);
+            });
+          } else {
+            // 如果没有选中的 viewport,清除所有可见 viewport 的测量
+            const visibleViewportCount = getVisibleViewportCount();
+            for (let i = 0; i < visibleViewportCount; i++) {
+              clearLengthMeasurements(`viewport-${i}`);
+            }
+          }
+          console.log('Clearing Length Measurements from MeasurementPanel');
+          break;
+
+        case '停用线段测量':
+          if (selectedViewers.length > 0) {
+            selectedViewers.forEach((index) => {
+              const success = deactivateLengthMeasurement(`viewport-${index}`);
+              if (success) {
+                dispatch(
+                  setToolInactive({
+                    toolName: 'LengthTool',
+                    viewportId: `viewport-${index}`,
+                  })
+                );
+              }
+            });
+          } else {
+            // 如果没有选中的 viewport,停用所有可见 viewport 的测量工具
+            const visibleViewportCount = getVisibleViewportCount();
+            for (let i = 0; i < visibleViewportCount; i++) {
+              const success = deactivateLengthMeasurement(`viewport-${i}`);
+              if (success) {
+                dispatch(
+                  setToolInactive({
+                    toolName: 'LengthTool',
+                    viewportId: `viewport-${i}`,
+                  })
+                );
+              }
+            }
+          }
+          console.log('Deactivating Length Measurement from MeasurementPanel');
+          break;
+
+        case '角度测量':
+          console.log('Angle Measurement - 功能待实现');
+          break;
+
+        case '测量校正':
+          console.log('Measurement Calibration - 功能待实现');
+          break;
+
+        default:
+          console.log(`未处理的测量操作: ${measurementAction}`);
+          break;
+      }
+
+      // 清理测量 action
+      dispatch(clearMeasurementAction());
+    }
+  }, [measurementAction, selectedViewers, gridLayout, dispatch]);
+
+  /**
+   * 获取当前可见的 viewport 数量
+   */
+  const getVisibleViewportCount = (): number => {
+    switch (gridLayout) {
+      case '1x1':
+        return 1;
+      case '1x2':
+      case '2x1':
+        return Math.min(2, imageUrls.length);
+      case '2x2':
+        return Math.min(4, imageUrls.length);
+      default:
+        return 1;
+    }
+  };
+
   const handleSelectViewer = (index: number, event) => {
     setSelectedViewers((prev) =>
       prev.includes(index) ? prev.filter((i) => i !== index) : [...prev, index]

+ 94 - 27
src/pages/view/components/viewers/stack.image.viewer.tsx

@@ -4,6 +4,8 @@ import type { Types } from '@cornerstonejs/core';
 import * as cornerstoneTools from '@cornerstonejs/tools';
 import { annotation, SplineROITool } from '@cornerstonejs/tools';
 import { eventTarget } from '@cornerstonejs/core';
+import { registerGlobalTools } from '@/utils/cornerstoneToolsSetup';
+import { MeasurementToolManager } from '@/utils/measurementToolManager';
 
 const {
   MagnifyTool,
@@ -12,6 +14,7 @@ const {
   StackScrollTool,
   ZoomTool,
   LabelTool,
+  LengthTool,
   ToolGroupManager,
   Enums: csToolsEnums,
   PlanarRotateTool,
@@ -88,22 +91,21 @@ function overlayRedRectangle(currentViewportId: string) {
 }
 
 function registerTools(viewportId, renderingEngineId) {
-  // Add tools to Cornerstone3D
-  cornerstoneTools.addTool(MagnifyTool);
-  cornerstoneTools.addTool(PanTool);
-  cornerstoneTools.addTool(WindowLevelTool);
-  cornerstoneTools.addTool(StackScrollTool);
-  cornerstoneTools.addTool(ZoomTool);
-  cornerstoneTools.addTool(LabelTool);
-  cornerstoneTools.addTool(PlanarRotateTool);
-  // Define a tool group
+  // 确保全局工具已注册(只会执行一次)
+  registerGlobalTools();
+
+  // 创建该 viewport 的工具组
   const toolGroupId = `STACK_TOOL_GROUP_ID_${viewportId}`;
   const toolGroupTmp = ToolGroupManager.createToolGroup(toolGroupId);
   if (!toolGroupTmp) {
+    console.error(
+      `[registerTools] Failed to create tool group for viewport: ${viewportId}`
+    );
     return;
   }
   const toolGroup = toolGroupTmp;
-  // Add tools to the tool group
+
+  // 添加工具到工具组
   toolGroup.addTool(MagnifyTool.toolName);
   toolGroup.addTool(PanTool.toolName);
   toolGroup.addTool(WindowLevelTool.toolName);
@@ -111,24 +113,22 @@ function registerTools(viewportId, renderingEngineId) {
   toolGroup.addTool(ZoomTool.toolName);
   toolGroup.addTool(LabelTool.toolName);
   toolGroup.addTool(PlanarRotateTool.toolName);
+  toolGroup.addTool(LengthTool.toolName); // 添加线段测量工具
+
+  // 设置默认工具状态
+  setupDefaultToolStates(toolGroup);
 
-  // Set the LabelTool as active
-  // toolGroup.setToolActive(LabelTool.toolName, {
-  //   bindings: [
-  //     {
-  //       mouseButton: MouseBindings.Primary, // Left Click
-  //     },
-  //   ],
-  // });
-  // Set the initial state of the tools
-  // toolGroup.setToolActive(WindowLevelTool.toolName, {
-  //   bindings: [
-  //     {
-  //       mouseButton: MouseBindings.Primary, // Left Click
-  //     },
-  //   ],
-  // });
+  // 添加 viewport 到工具组
+  toolGroup.addViewport(viewportId, renderingEngineId);
+
+  console.log(`[registerTools] Tools registered for viewport: ${viewportId}`);
+}
 
+/**
+ * 设置默认工具状态
+ */
+function setupDefaultToolStates(toolGroup: cornerstoneTools.Types.IToolGroup) {
+  // 设置平移工具(中键)
   toolGroup.setToolActive(PanTool.toolName, {
     bindings: [
       {
@@ -137,6 +137,7 @@ function registerTools(viewportId, renderingEngineId) {
     ],
   });
 
+  // 设置缩放工具(右键)
   toolGroup.setToolActive(ZoomTool.toolName, {
     bindings: [
       {
@@ -145,6 +146,7 @@ function registerTools(viewportId, renderingEngineId) {
     ],
   });
 
+  // 设置滚轮滚动工具
   toolGroup.setToolActive(StackScrollTool.toolName, {
     bindings: [
       {
@@ -153,7 +155,12 @@ function registerTools(viewportId, renderingEngineId) {
     ],
   });
 
-  toolGroup.addViewport(viewportId, renderingEngineId);
+  // 其他工具默认为被动状态
+  toolGroup.setToolPassive(MagnifyTool.toolName);
+  toolGroup.setToolPassive(WindowLevelTool.toolName);
+  toolGroup.setToolPassive(LabelTool.toolName);
+  toolGroup.setToolPassive(PlanarRotateTool.toolName);
+  toolGroup.setToolPassive(LengthTool.toolName);
 }
 export function addLMark(currentViewportId: string): void {
   // Implement the logic to add an L mark
@@ -480,6 +487,66 @@ export function rotateAnyAngle(currentViewportId: string) {
   console.log('Rotating Image by Any Angle');
 }
 
+// ==================== 线段测量相关函数 ====================
+
+/**
+ * 激活线段测量工具
+ */
+export function activateLengthMeasurement(viewportId: string): boolean {
+  console.log(
+    `[activateLengthMeasurement] Activating length measurement for viewport: ${viewportId}`
+  );
+  return MeasurementToolManager.activateLengthTool(viewportId);
+}
+
+/**
+ * 停用线段测量工具
+ */
+export function deactivateLengthMeasurement(viewportId: string): boolean {
+  console.log(
+    `[deactivateLengthMeasurement] Deactivating length measurement for viewport: ${viewportId}`
+  );
+  return MeasurementToolManager.deactivateLengthTool(viewportId);
+}
+
+/**
+ * 切换线段测量工具状态
+ */
+export function toggleLengthMeasurement(viewportId: string): boolean {
+  console.log(
+    `[toggleLengthMeasurement] Toggling length measurement for viewport: ${viewportId}`
+  );
+  return MeasurementToolManager.toggleLengthTool(viewportId);
+}
+
+/**
+ * 清除线段测量标注
+ */
+export function clearLengthMeasurements(viewportId: string): boolean {
+  console.log(
+    `[clearLengthMeasurements] Clearing length measurements for viewport: ${viewportId}`
+  );
+  return MeasurementToolManager.clearLengthMeasurements(viewportId);
+}
+
+/**
+ * 获取线段测量结果
+ */
+// eslint-disable-next-line
+export function getLengthMeasurements(viewportId: string): any[] {
+  console.log(
+    `[getLengthMeasurements] Getting length measurements for viewport: ${viewportId}`
+  );
+  return MeasurementToolManager.getLengthMeasurements(viewportId);
+}
+
+/**
+ * 检查线段测量工具是否激活
+ */
+export function isLengthMeasurementActive(viewportId: string): boolean {
+  return MeasurementToolManager.isLengthToolActive(viewportId);
+}
+
 const StackViewer = ({
   imageIndex = 0,
   imageUrls = [],

+ 2 - 0
src/states/store.ts

@@ -17,6 +17,7 @@ import aprReducer from './exam/aprSlice';
 import bodyPositionListenerMiddleware from './exam/bodyPositionListener';
 import { aprMiddleware } from './exam/aprSlice';
 import functionAreaReducer from './view/functionAreaSlice';
+import measurementPanelReducer from './view/measurementPanelSlice';
 import searchReducer from './patient/worklist/slices/searchSlice';
 import businessFlowMiddleware from './businessFlowMiddleware';
 import leavingRegisterMonitor from './leavingRegisterMonitor';
@@ -72,6 +73,7 @@ const store = configureStore({
     bodyPositionDetail: bodyPositionDetailReducer,
     apr: aprReducer,
     functionArea: functionAreaReducer,
+    measurementPanel: measurementPanelReducer,
     workEntities: workEntitiesSlice.reducer,
     workFilters: workFiltersSlice.reducer,
     workPagination: workPaginationSlice.reducer,

+ 221 - 0
src/states/view/measurementPanelSlice.ts

@@ -0,0 +1,221 @@
+import { createSlice, PayloadAction } from '@reduxjs/toolkit';
+
+// 测量操作类型
+export type MeasurementAction =
+  | '线段测量'
+  | '角度测量'
+  | '清除测量'
+  | '测量校正'
+  | '停用线段测量'
+  | null;
+
+// 测量结果类型
+export interface MeasurementResult {
+  annotationUID: string;
+  toolName: string;
+  viewportId: string;
+  value: number;
+  unit: string;
+  points: number[][];
+  timestamp: number;
+}
+
+// 工具状态类型
+export interface ToolState {
+  toolName: string;
+  viewportId: string;
+  isActive: boolean;
+}
+
+// 测量面板状态接口
+interface MeasurementPanelState {
+  currentAction: MeasurementAction;
+  activeViewports: string[];
+  measurementResults: MeasurementResult[];
+  toolStates: Record<string, boolean>; // key: `${toolName}-${viewportId}`, value: isActive
+  isProcessing: boolean;
+}
+
+const initialState: MeasurementPanelState = {
+  currentAction: null,
+  activeViewports: [],
+  measurementResults: [],
+  toolStates: {},
+  isProcessing: false,
+};
+
+const measurementPanelSlice = createSlice({
+  name: 'measurementPanel',
+  initialState,
+  reducers: {
+    // ==================== 测量操作相关 ====================
+    setMeasurementAction: (state, action: PayloadAction<MeasurementAction>) => {
+      state.currentAction = action.payload;
+      state.isProcessing = action.payload !== null;
+    },
+
+    clearMeasurementAction: (state) => {
+      state.currentAction = null;
+      state.isProcessing = false;
+    },
+
+    // ==================== 工具状态管理 ====================
+    setToolActive: (
+      state,
+      action: PayloadAction<{ toolName: string; viewportId: string }>
+    ) => {
+      const { toolName, viewportId } = action.payload;
+      const key = `${toolName}-${viewportId}`;
+      state.toolStates[key] = true;
+    },
+
+    setToolInactive: (
+      state,
+      action: PayloadAction<{ toolName: string; viewportId: string }>
+    ) => {
+      const { toolName, viewportId } = action.payload;
+      const key = `${toolName}-${viewportId}`;
+      state.toolStates[key] = false;
+    },
+
+    // 批量设置工具状态
+    setToolStatesForViewports: (
+      state,
+      action: PayloadAction<{
+        toolName: string;
+        viewportIds: string[];
+        isActive: boolean;
+      }>
+    ) => {
+      const { toolName, viewportIds, isActive } = action.payload;
+      viewportIds.forEach((viewportId) => {
+        const key = `${toolName}-${viewportId}`;
+        state.toolStates[key] = isActive;
+      });
+    },
+
+    // ==================== 测量结果管理 ====================
+    addMeasurementResult: (state, action: PayloadAction<MeasurementResult>) => {
+      state.measurementResults.push(action.payload);
+    },
+
+    removeMeasurementResult: (state, action: PayloadAction<string>) => {
+      const annotationUID = action.payload;
+      state.measurementResults = state.measurementResults.filter(
+        (result) => result.annotationUID !== annotationUID
+      );
+    },
+
+    updateMeasurementResult: (
+      state,
+      action: PayloadAction<MeasurementResult>
+    ) => {
+      const updatedResult = action.payload;
+      const index = state.measurementResults.findIndex(
+        (result) => result.annotationUID === updatedResult.annotationUID
+      );
+      if (index !== -1) {
+        state.measurementResults[index] = updatedResult;
+      }
+    },
+
+    clearMeasurementResults: (
+      state,
+      action: PayloadAction<string | undefined>
+    ) => {
+      const viewportId = action.payload;
+      if (viewportId) {
+        // 清除指定 viewport 的测量结果
+        state.measurementResults = state.measurementResults.filter(
+          (result) => result.viewportId !== viewportId
+        );
+      } else {
+        // 清除所有测量结果
+        state.measurementResults = [];
+      }
+    },
+
+    // ==================== Viewport 管理 ====================
+    setActiveViewports: (state, action: PayloadAction<string[]>) => {
+      state.activeViewports = action.payload;
+    },
+
+    addActiveViewport: (state, action: PayloadAction<string>) => {
+      const viewportId = action.payload;
+      if (!state.activeViewports.includes(viewportId)) {
+        state.activeViewports.push(viewportId);
+      }
+    },
+
+    removeActiveViewport: (state, action: PayloadAction<string>) => {
+      const viewportId = action.payload;
+      state.activeViewports = state.activeViewports.filter(
+        (id) => id !== viewportId
+      );
+    },
+
+    // ==================== 重置状态 ====================
+    resetMeasurementPanel: (state) => {
+      state.currentAction = null;
+      state.activeViewports = [];
+      state.measurementResults = [];
+      state.toolStates = {};
+      state.isProcessing = false;
+    },
+  },
+});
+
+// 导出 action creators
+export const {
+  setMeasurementAction,
+  clearMeasurementAction,
+  setToolActive,
+  setToolInactive,
+  setToolStatesForViewports,
+  addMeasurementResult,
+  removeMeasurementResult,
+  updateMeasurementResult,
+  clearMeasurementResults,
+  setActiveViewports,
+  addActiveViewport,
+  removeActiveViewport,
+  resetMeasurementPanel,
+} = measurementPanelSlice.actions;
+
+// 导出 reducer
+export default measurementPanelSlice.reducer;
+
+// ==================== Selectors ====================
+export const selectMeasurementPanel = (state: {
+  measurementPanel: MeasurementPanelState;
+}) => state.measurementPanel;
+
+export const selectCurrentMeasurementAction = (state: {
+  measurementPanel: MeasurementPanelState;
+}) => state.measurementPanel.currentAction;
+
+export const selectMeasurementResults = (state: {
+  measurementPanel: MeasurementPanelState;
+}) => state.measurementPanel.measurementResults;
+
+export const selectMeasurementResultsByViewport =
+  (viewportId: string) =>
+  (state: { measurementPanel: MeasurementPanelState }) =>
+    state.measurementPanel.measurementResults.filter(
+      (result) => result.viewportId === viewportId
+    );
+
+export const selectToolState =
+  (toolName: string, viewportId: string) =>
+  (state: { measurementPanel: MeasurementPanelState }) => {
+    const key = `${toolName}-${viewportId}`;
+    return state.measurementPanel.toolStates[key] || false;
+  };
+
+export const selectActiveViewports = (state: {
+  measurementPanel: MeasurementPanelState;
+}) => state.measurementPanel.activeViewports;
+
+export const selectIsProcessing = (state: {
+  measurementPanel: MeasurementPanelState;
+}) => state.measurementPanel.isProcessing;

+ 61 - 0
src/utils/cornerstoneToolsSetup.ts

@@ -0,0 +1,61 @@
+import * as cornerstoneTools from '@cornerstonejs/tools';
+
+const {
+  MagnifyTool,
+  PanTool,
+  WindowLevelTool,
+  StackScrollTool,
+  ZoomTool,
+  LabelTool,
+  PlanarRotateTool,
+  LengthTool,
+} = cornerstoneTools;
+
+let toolsRegistered = false;
+
+/**
+ * 全局注册所有 Cornerstone3D 工具
+ * 这个函数只会执行一次,避免重复注册
+ */
+export function registerGlobalTools(): void {
+  if (toolsRegistered) {
+    console.log(
+      '[cornerstoneToolsSetup] Tools already registered, skipping...'
+    );
+    return;
+  }
+
+  console.log('[cornerstoneToolsSetup] Registering global tools...');
+
+  try {
+    // 注册所有工具
+    cornerstoneTools.addTool(MagnifyTool);
+    cornerstoneTools.addTool(PanTool);
+    cornerstoneTools.addTool(WindowLevelTool);
+    cornerstoneTools.addTool(StackScrollTool);
+    cornerstoneTools.addTool(ZoomTool);
+    cornerstoneTools.addTool(LabelTool);
+    cornerstoneTools.addTool(PlanarRotateTool);
+    cornerstoneTools.addTool(LengthTool); // 添加线段测量工具
+
+    toolsRegistered = true;
+    console.log('[cornerstoneToolsSetup] All tools registered successfully');
+  } catch (error) {
+    console.error('[cornerstoneToolsSetup] Error registering tools:', error);
+    throw error;
+  }
+}
+
+/**
+ * 检查工具是否已注册
+ */
+export function areToolsRegistered(): boolean {
+  return toolsRegistered;
+}
+
+/**
+ * 重置工具注册状态(主要用于测试)
+ */
+export function resetToolsRegistration(): void {
+  toolsRegistered = false;
+}

+ 229 - 0
src/utils/measurementToolManager.ts

@@ -0,0 +1,229 @@
+import * as cornerstone from '@cornerstonejs/core';
+import * as cornerstoneTools from '@cornerstonejs/tools';
+
+const {
+  ToolGroupManager,
+  LengthTool,
+  WindowLevelTool,
+  MagnifyTool,
+  Enums: csToolsEnums,
+} = cornerstoneTools;
+
+const { MouseBindings } = csToolsEnums;
+
+/**
+ * 测量工具管理器
+ * 统一管理所有测量相关的工具操作
+ */
+export class MeasurementToolManager {
+  /**
+   * 根据 viewportId 获取对应的工具组
+   */
+  static getToolGroup(
+    viewportId: string
+  ): cornerstoneTools.Types.IToolGroup | undefined {
+    const toolGroupId = `STACK_TOOL_GROUP_ID_${viewportId}`;
+    const toolGroup = ToolGroupManager.getToolGroup(toolGroupId);
+
+    if (!toolGroup) {
+      console.warn(
+        `[MeasurementToolManager] Tool group not found for viewport: ${viewportId}`
+      );
+    }
+
+    return toolGroup;
+  }
+
+  /**
+   * 激活线段测量工具
+   */
+  static activateLengthTool(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.setToolActive(LengthTool.toolName, {
+        bindings: [{ mouseButton: MouseBindings.Primary }],
+      });
+
+      console.log(
+        `[MeasurementToolManager] Length tool activated for viewport: ${viewportId}`
+      );
+      return true;
+    } catch (error) {
+      console.error(
+        `[MeasurementToolManager] Error activating length tool:`,
+        error
+      );
+      return false;
+    }
+  }
+
+  /**
+   * 停用线段测量工具
+   */
+  static deactivateLengthTool(viewportId: string): boolean {
+    const toolGroup = this.getToolGroup(viewportId);
+    if (!toolGroup) return false;
+
+    try {
+      toolGroup.setToolPassive(LengthTool.toolName, {
+        removeAllBindings: true,
+      });
+      console.log(
+        `[MeasurementToolManager] Length tool deactivated for viewport: ${viewportId}`
+      );
+      return true;
+    } catch (error) {
+      console.error(
+        `[MeasurementToolManager] Error deactivating length tool:`,
+        error
+      );
+      return false;
+    }
+  }
+
+  /**
+   * 检查线段测量工具是否处于激活状态
+   */
+  static isLengthToolActive(viewportId: string): boolean {
+    const toolGroup = this.getToolGroup(viewportId);
+    if (!toolGroup) return false;
+
+    try {
+      const activeTool = toolGroup.getActivePrimaryMouseButtonTool();
+      return activeTool === LengthTool.toolName;
+    } catch (error) {
+      console.error(
+        `[MeasurementToolManager] Error checking tool state:`,
+        error
+      );
+      return false;
+    }
+  }
+
+  /**
+   * 切换线段测量工具状态
+   */
+  static toggleLengthTool(viewportId: string): boolean {
+    const isActive = this.isLengthToolActive(viewportId);
+
+    if (isActive) {
+      return this.deactivateLengthTool(viewportId);
+    } else {
+      return this.activateLengthTool(viewportId);
+    }
+  }
+
+  /**
+   * 清除指定 viewport 的所有线段测量标注
+   */
+  static clearLengthMeasurements(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(
+        LengthTool.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} length measurements for viewport: ${viewportId}`
+      );
+      return true;
+    } catch (error) {
+      console.error(
+        `[MeasurementToolManager] Error clearing measurements:`,
+        error
+      );
+      return false;
+    }
+  }
+
+  /**
+   * 获取指定 viewport 的所有线段测量结果
+   */
+  // eslint-disable-next-line
+  static getLengthMeasurements(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(
+        LengthTool.toolName,
+        viewport.element
+      );
+
+      return annotations.map((annotation) => ({
+        annotationUID: annotation.annotationUID,
+        length: annotation.data?.cachedStats?.length || 0,
+        unit: annotation.data?.cachedStats?.unit || 'mm',
+        points: annotation.data?.handles?.points || [],
+      }));
+    } catch (error) {
+      console.error(
+        `[MeasurementToolManager] Error getting measurements:`,
+        error
+      );
+      return [];
+    }
+  }
+
+  /**
+   * 为多个 viewport 批量激活线段测量工具
+   */
+  static activateLengthToolForViewports(viewportIds: string[]): boolean[] {
+    return viewportIds.map((viewportId) => this.activateLengthTool(viewportId));
+  }
+
+  /**
+   * 为多个 viewport 批量停用线段测量工具
+   */
+  static deactivateLengthToolForViewports(viewportIds: string[]): boolean[] {
+    return viewportIds.map((viewportId) =>
+      this.deactivateLengthTool(viewportId)
+    );
+  }
+
+  /**
+   * 为多个 viewport 批量清除线段测量
+   */
+  static clearLengthMeasurementsForViewports(viewportIds: string[]): boolean[] {
+    return viewportIds.map((viewportId) =>
+      this.clearLengthMeasurements(viewportId)
+    );
+  }
+}