import React, { useEffect, useRef } from 'react'; import * as cornerstone from '@cornerstonejs/core'; import type { Types } from '@cornerstonejs/core'; import * as cornerstoneTools from '@cornerstonejs/tools'; import * as cornerstoneDICOMImageLoader from '@cornerstonejs/dicom-image-loader'; import { useSelector } from 'react-redux'; import { RootState } from '@/states/store'; import { SystemMode } from '@/states/systemModeSlice'; import store from '@/states/store'; import { clearAction } from '@/states/view/functionAreaSlice'; import { useDispatch } from 'react-redux'; const { MagnifyTool, PanTool, WindowLevelTool, StackScrollTool, ZoomTool, LabelTool, ToolGroupManager, Enums: csToolsEnums, PlanarRotateTool, } = cornerstoneTools; const { MouseBindings } = csToolsEnums; let toolGroup: cornerstoneTools.Types.IToolGroup; let 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 const toolGroupId = 'STACK_TOOL_GROUP_ID'; const toolGroupTmp = ToolGroupManager.createToolGroup(toolGroupId); if (!toolGroupTmp) { return; } toolGroup = toolGroupTmp; // Add tools to the tool group 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); // 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 // }, // ], // }); toolGroup.setToolActive(PanTool.toolName, { bindings: [ { mouseButton: MouseBindings.Auxiliary, // Middle Click }, ], }); toolGroup.setToolActive(ZoomTool.toolName, { bindings: [ { mouseButton: MouseBindings.Secondary, // Right Click }, ], }); toolGroup.setToolActive(StackScrollTool.toolName, { bindings: [ { mouseButton: MouseBindings.Wheel, // Mouse Wheel }, ], }); toolGroup.addViewport(viewportId, renderingEngineId); } function addLMark(): void { // Implement the logic to add an L mark console.log('Adding L Mark'); toolGroup.setToolActive(LabelTool.toolName, { bindings: [ // { // mouseButton: MouseBindings.Primary, // Left Click // }, ], }); const position: Types.Point3 = [100, 100, 0]; // Example position const text = 'L'; // Predefined text LabelTool.hydrate(currentViewportId, position, text); toolGroup.setToolPassive(LabelTool.toolName, { removeAllBindings: true, }); // const enabledElement = cornerstone.getEnabledElementByViewportId(currentViewportId); // cursors.elementCursor.resetElementCursor(elementRef.current as HTMLDivElement); } function addRLabel(viewportId) { toolGroup.setToolActive(LabelTool.toolName, { bindings: [], }); const element = document.getElementById(viewportId); const elementHeight = element ? element.getBoundingClientRect().height : 0; const position: Types.Point3 = [100, elementHeight / 2, 0]; // Example position const text = 'R'; // Predefined text LabelTool.hydrate(viewportId, position, text); toolGroup.setToolPassive(LabelTool.toolName, { removeAllBindings: true }); } function adjustBrightnessAndContrast() { 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 }, ], }); } } function fitImageSize() { const viewport = cornerstone.getEnabledElementByViewportId(currentViewportId) .viewport as cornerstone.StackViewport; viewport.resetCamera(); viewport.render(); } function deleteSelectedMark(): void { const viewport = cornerstone.getEnabledElementByViewportId(currentViewportId).viewport; const allAnnotations = cornerstoneTools.annotation.state.getAllAnnotations(); for (const annotation of allAnnotations) { if (annotation.data.text === 'L' || annotation.data.text === 'R') { cornerstoneTools.annotation.state.removeAnnotation( annotation.annotationUID! ); } } viewport.render(); } function HorizontalFlip(): void { const viewport = cornerstone.getEnabledElementByViewportId(currentViewportId) .viewport as cornerstone.StackViewport; // 切换水平翻转状态 const { flipHorizontal } = viewport.getCamera(); viewport.setCamera({ flipHorizontal: !flipHorizontal }); console.log('Flipping Image Horizontally'); } function VerticalFlip(): void { const viewport = cornerstone.getEnabledElementByViewportId(currentViewportId) .viewport as cornerstone.StackViewport; // 切换竖直翻转状态 const { flipVertical } = viewport.getCamera(); viewport.setCamera({ flipVertical: !flipVertical }); } function ApplyColormap(): void { const viewport = cornerstone.getEnabledElementByViewportId(currentViewportId) .viewport as cornerstone.StackViewport; // Implement the logic to apply colormap viewport.setProperties({ colormap: { name: 'hsv' } }); viewport.render(); console.log('Applying Colormap'); } function RotateCounterclockwise90(): void { const viewport = cornerstone.getEnabledElementByViewportId(currentViewportId) .viewport as cornerstone.StackViewport; // let { rotation } = viewport.getViewPresentation(); // const currentRotataion = viewport.getRotation(); // viewport.rotation(22) // console.log(`rotation:${rotation}`) // if(!!rotation){ // rotation=0; // } // viewport.setViewPresentation({ rotation: rotation ?? 0 + 30 }); // // // Implement the logic to rotate the image counterclockwise // // viewport.setCamera({ rotation: -90 }); // viewport.render(); // // 1. 获取当前的视图表现状态(一个完整的对象) // const currentViewPresentation = viewport.getViewPresentation(); // // 2. 创建一个新的对象,基于当前状态,只更新你需要修改的属性 // // 使用展开运算符 (...) 来复制所有现有属性 // const newViewPresentation = { // ...currentViewPresentation, // 保留所有原有属性,如 scale, translation, flip 等 // rotation: currentViewPresentation.rotation ?? + 30, // 只覆盖 rotation 属性 // }; // // 3. 将完整的新状态对象设置回 viewport // viewport.setViewPresentation(newViewPresentation); // viewport.render(); //---------------------------------------- // // 获取当前的完整视图状态 // const currentViewState = viewport.getViewPresentation(); // // 创建新的视图状态对象,保持所有其他属性不变 // const newViewState = { // ...currentViewState, // 保留所有现有属性 // rotation: (currentViewState.rotation ?? 0 + 30) % 360 // 只修改rotation // }; // // 设置新的视图状态 // viewport.setViewPresentation(newViewState); // viewport.render(); //----------------------------------------- // 获取当前相机 const camera = viewport.getCamera(); // // 计算新的旋转角度(当前角度 + 90度) // const newRotation = (camera.rotation ?? 0) + 90; // // 转换为弧度 // const radians = newRotation * Math.PI / 180; // // 计算新的viewUp向量(顺时针旋转90度) // const newViewUp:[number, number, number] = [ // Math.cos(radians), // X分量 // Math.sin(radians), // Y分量 // 0 // Z分量 // ]; // // 设置新的相机参数 // viewport.setCamera({ // ...camera, // 保持其他相机参数不变 // viewUp: newViewUp, // 更新向上向量 // rotation: newRotation // 更新旋转角度(可选,但推荐) // }); // 计算新的旋转角度(当前角度 + 90度) const newRotation = (camera.rotation ?? 0) + 90; // 但计算viewUp向量时,我们应该使用90度的弧度,而不是新角度的弧度! const ninetyDegreesRadians = (90 * Math.PI) / 180; // 获取当前的viewUp向量 const currentViewUp = camera.viewUp || [0, 1, 0]; // 计算旋转后的viewUp向量(这才是正确的相对旋转) const newViewUp: [number, number, number] = [ currentViewUp[0] * Math.cos(ninetyDegreesRadians) - currentViewUp[1] * Math.sin(ninetyDegreesRadians), currentViewUp[0] * Math.sin(ninetyDegreesRadians) + currentViewUp[1] * Math.cos(ninetyDegreesRadians), 0, ]; // 设置新的相机参数 viewport.setCamera({ ...camera, viewUp: newViewUp, rotation: newRotation % 360, // 确保角度在0-359范围内 }); viewport.render(); console.log('Rotating Image Counterclockwise 90°'); } function RotateClockwise90(): void { const viewport = cornerstone.getEnabledElementByViewportId(currentViewportId) .viewport as cornerstone.StackViewport; const camera = viewport.getCamera(); // 计算新的旋转角度(当前角度 + 90度) const newRotation = (camera.rotation ?? 0) - 90; // 但计算viewUp向量时,我们应该使用90度的弧度,而不是新角度的弧度! const ninetyDegreesRadians = (90 * Math.PI) / 180; // 获取当前的viewUp向量 const currentViewUp = camera.viewUp || [0, 1, 0]; // 计算旋转后的viewUp向量(这才是正确的相对旋转) const newViewUp: [number, number, number] = [ currentViewUp[0] * Math.cos(ninetyDegreesRadians) - currentViewUp[1] * Math.sin(ninetyDegreesRadians), currentViewUp[0] * Math.sin(ninetyDegreesRadians) + currentViewUp[1] * Math.cos(ninetyDegreesRadians), 0, ]; // 设置新的相机参数 viewport.setCamera({ ...camera, viewUp: newViewUp, rotation: newRotation % 360, // 确保角度在0-359范围内 }); viewport.render(); console.log('Rotating Image Clockwise 90°'); } function ResetImage(): void { const viewport = cornerstone.getEnabledElementByViewportId(currentViewportId) .viewport as cornerstone.StackViewport; // Implement the logic to reset the image // Resets the viewport's camera viewport.resetCamera(); // Resets the viewport's properties viewport.resetProperties(); viewport.render(); console.log('Resetting Image'); } function InvertImage(): void { const viewport = cornerstone.getEnabledElementByViewportId(currentViewportId) .viewport as cornerstone.StackViewport; // Implement the logic to invert the image const invert = !viewport.getProperties().invert; viewport.setProperties({ invert }); viewport.render(); console.log('Inverting Image'); } function setOriginalSize(viewport) { // 1) 先正常 fit(或本来就是 fit 状态) viewport.resetCamera(); // 2) 计算“fit → 1:1”的放大倍数 const { dimensions, spacing } = viewport.getImageData(); console.log(`dimensions:${dimensions}, spacing:${spacing}`); const canvas = viewport.canvas; // 水平方向 1:1 需要的倍率 const cssPixelsPerDicomPx = canvas.clientWidth / (dimensions[0] * spacing[0]); // 垂直方向 1:1 需要的倍率 const cssPixelsPerDicomPy = canvas.clientHeight / (dimensions[1] * spacing[1]); // 取两者最小值,保证整张图不会被裁剪 const zoomFactor = Math.min(cssPixelsPerDicomPx, cssPixelsPerDicomPy); console.log(`zoomFactor:${zoomFactor}`); console.log( `canvas.clientWidth:${canvas.clientWidth}, dimensions[0]:${dimensions[0]}, spacing[0]:${spacing[0]}` ); console.log( `canvas.clientHeight:${canvas.clientHeight}, dimensions[1]:${dimensions[1]}, spacing[1]:${spacing[1]}` ); // 3) 直接放大 const zoom = viewport.getZoom(); viewport.setZoom((zoom * 1) / zoomFactor); viewport.render(); } const StackViewer = ({ imageIndex = 0, imageUrls = [], }: { imageIndex?: number; imageUrls?: string[]; }) => { const elementRef = useRef(null); const action = useSelector((state: RootState) => state.functionArea.action); const dispatch = useDispatch(); useEffect(() => { const setup = async () => { // 初始化 Cornerstone cornerstone.init(); cornerstoneTools.init(); const state = store.getState(); console.log(`当前系统模式:${state.systemMode.mode}`); const token = state.systemMode.mode === SystemMode.Emergency ? state.product.guest : state.userInfo.token; console.log(`token stack.image.viewer: ${token}`); cornerstoneDICOMImageLoader.init({ maxWebWorkers: navigator.hardwareConcurrency || 1, errorInterceptor: (error) => { if (error.status === 401) { console.error('Authentication failed. Please refresh the token.'); } console.error(`请求dcm文件出错:${error}`); }, beforeSend: (xhr, imageId, defaultHeaders) => { return { ...defaultHeaders, Authorization: `Bearer ${token}`, Language: 'en', Product: 'DROS', Source: 'Electron', }; }, }); // Instantiate a rendering engine const renderingEngineId = 'myRenderingEngine'; const renderingEngine = new cornerstone.RenderingEngine( renderingEngineId ); const viewportId = 'CT_AXIAL_STACK'; currentViewportId = viewportId; const viewportInput: cornerstone.Types.PublicViewportInput = { viewportId, element: elementRef.current!, type: cornerstone.Enums.ViewportType.STACK, }; renderingEngine.enableElement(viewportInput); registerTools(viewportId, renderingEngineId); // Get the stack viewport that was created const viewport = renderingEngine.getViewport( viewportId ) as cornerstone.Types.IStackViewport; // 给定一个dcm文件路径,加载并显示出来 try { await viewport.setStack(imageUrls, imageIndex); } catch (error) { if (error instanceof Error) { console.error( '[stack.image.viewer] Error setting image stack:', error.message ); } else { console.error('[stack.image.viewer] Unknown error:', error); } } viewport.render(); }; setup(); }, [elementRef, imageIndex]); useEffect(() => { if (action) { // Handle the action switch (action) { case 'Add L Mark': addLMark(); break; case 'Add R Mark': // Implement the logic to add an R mark addRLabel(currentViewportId); console.log('Adding R Mark'); break; case 'Delete Selected Mark': { deleteSelectedMark(); break; } case 'Horizontal Flip': { HorizontalFlip(); break; } case 'Vertical Flip': { VerticalFlip(); break; } case 'Rotate Counterclockwise 90': { RotateCounterclockwise90(); break; } case 'Rotate Clockwise 90': RotateClockwise90(); break; case 'Rotate Any Angle': // Implement the logic to rotate the image by any angle { const planar = toolGroup.getToolInstance(PlanarRotateTool.toolName); // Reset rotation angle const isActive = planar.mode === csToolsEnums.ToolModes.Active; console.log( `PlanarRotateTool is currently ${isActive ? 'active' : 'inactive'}` ); if (isActive) { toolGroup.setToolPassive(PlanarRotateTool.toolName, { removeAllBindings: true, }); } else { toolGroup.setToolActive(PlanarRotateTool.toolName, { bindings: [ { mouseButton: MouseBindings.Primary, // Left Click }, ], }); } console.log('Rotating Image by Any Angle'); } break; case 'Crop Image': // Implement the logic to crop the image console.log('Cropping Image'); break; case 'Delete Digital Mask': // Implement the logic to delete the digital mask console.log('Deleting Digital Mask'); break; case 'Adjust Brightness and Contrast': adjustBrightnessAndContrast(); break; case 'Crop Selected Area': // Implement the logic to crop the selected area console.log('Cropping Selected Area'); break; case 'Delete Mask': // Implement the logic to delete the mask console.log('Deleting Mask'); break; case 'Image Comparison': // Implement the logic for image comparison console.log('Comparing Images'); break; case 'Invert Contrast': // Implement the logic to invert the contrast { const viewport = cornerstone.getEnabledElementByViewportId( currentViewportId ).viewport; const targetBool = !viewport.getProperties().invert; viewport.setProperties({ invert: targetBool, }); viewport.render(); } console.log('Inverting Contrast'); break; case '1x1 Layout': // Implement the logic for 1x1 layout console.log('Setting 1x1 Layout'); break; case '1x2 Layout': // Implement the logic for 1x2 layout console.log('Setting 1x2 Layout'); break; case '2x2 Layout': // Implement the logic for 2x2 layout console.log('Setting 2x2 Layout'); break; case '4x4 Layout': // Implement the logic for 4x4 layout console.log('Setting 4x4 Layout'); break; case 'Magnifier': { // Implement the logic for magnifier console.log('Activating Magnifier'); 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 }, ], }); } break; } case 'Fit Size': { // Implement the logic to fit the image size fitImageSize(); console.log('Fitting Image Size'); break; } case 'Original Size': { // Implement the logic to set the image to original size console.log('Setting Image to Original Size'); setOriginalSize( cornerstone.getEnabledElementByViewportId(currentViewportId) .viewport as cornerstone.StackViewport ); break; } case 'Zoom Image': // Implement the logic to zoom the image console.log('Zooming Image'); break; case 'Reset Cursor': // Implement the logic to reset the cursor console.log('Resetting Cursor'); break; case 'Pan': // Implement the logic to pan the image console.log('Panning Image'); break; case 'Invert Image': InvertImage(); break; case 'Reset Image': ResetImage(); break; case 'Snapshot': // Implement the logic to take a snapshot console.log('Taking Snapshot'); break; case 'Advanced Processing': // Implement the logic for advanced processing console.log('Performing Advanced Processing'); break; case 'Musician': // Implement the logic for musician console.log('Activating Musician'); break; case 'Image Measurement': // Implement the logic for image measurement console.log('Measuring Image'); break; case 'More': // Implement the logic for more options console.log('Showing More Options'); break; case 'Apply Colormap': ApplyColormap(); break; default: break; } dispatch(clearAction()); //清理后可连续同一个action触发响应 } }, [action]); return (
); }; export default StackViewer;