Преглед изворни кода

feat(processing): implement multi-grid layout using viewerContainer to manage multiple viewers

ddx пре 1 месец
родитељ
комит
0ec3b70163

+ 2 - 2
src/pages/view/components/ImageControl.tsx

@@ -1,4 +1,4 @@
-import StackViewer from './viewers/stack.image.viewer';
+import ViewerContainer from './ViewerContainer';
 import { useSelector } from 'react-redux';
 import { RootState } from '@/states/store';
 import {
@@ -31,7 +31,7 @@ const ImageControl = () => {
   return (
     <div className="h-full w-full">
       {isExposed ? (
-        <StackViewer imageIndex={0} imageUrls={dcmUrls} />
+        <ViewerContainer imageUrls={dcmUrls} />
       ) : (
         <Image
           src={imageUrl}

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

@@ -0,0 +1,296 @@
+import React, { useState, useEffect } from 'react';
+import StackViewer from './viewers/stack.image.viewer';
+import { useSelector, useDispatch } from 'react-redux';
+import { RootState } from '@/states/store';
+import { clearAction } from '@/states/view/functionAreaSlice';
+
+interface ViewerContainerProps {
+  imageUrls: string[];
+}
+
+const ViewerContainer: React.FC<ViewerContainerProps> = ({ imageUrls }) => {
+  const [selectedViewers, setSelectedViewers] = useState<number[]>([]);
+  const [gridLayout, setGridLayout] = useState<'1x1' | '1x2' | '2x1' | '2x2'>(
+    '1x1'
+  );
+  const action = useSelector((state: RootState) => state.functionArea.action);
+  const dispatch = useDispatch();
+
+  const viewers = imageUrls.map((url, index) => (
+    <StackViewer
+      key={index}
+      imageIndex={index}
+      imageUrls={[url]}
+      viewportId={`viewport-${index}`}
+    />
+  ));
+
+  useEffect(() => {
+    if (action) {
+      // Handle the action
+      switch (action) {
+        case 'Add L Mark':
+          selectedViewers.forEach((index) => {
+            viewers[index].props.addLMark();
+          });
+          break;
+        case 'Add R Mark':
+          // Implement the logic to add an R mark
+          selectedViewers.forEach((index) => {
+            viewers[index].props.addRLabel();
+          });
+          console.log('Adding R Mark');
+          break;
+        case 'Delete Selected Mark': {
+          selectedViewers.forEach((index) => {
+            viewers[index].props.deleteSelectedMark();
+          });
+          break;
+        }
+        case 'Horizontal Flip': {
+          selectedViewers.forEach((index) => {
+            viewers[index].props.HorizontalFlip();
+          });
+          break;
+        }
+        case 'Vertical Flip': {
+          selectedViewers.forEach((index) => {
+            viewers[index].props.VerticalFlip();
+          });
+          break;
+        }
+        case 'Rotate Counterclockwise 90': {
+          selectedViewers.forEach((index) => {
+            viewers[index].props.RotateCounterclockwise90();
+          });
+          break;
+        }
+        case 'Rotate Clockwise 90':
+          selectedViewers.forEach((index) => {
+            viewers[index].props.RotateClockwise90();
+          });
+          break;
+        case 'Rotate Any Angle':
+          // Implement the logic to rotate the image by any angle
+          selectedViewers.forEach((index) => {
+            viewers[index].props.rotateAnyAngle();
+          });
+          break;
+        case 'AddMask':
+          selectedViewers.forEach((index) => {
+            viewers[index].props.addMask();
+          });
+          break;
+        case 'Delete Digital Mask':
+          selectedViewers.forEach((index) => {
+            viewers[index].props.remoteMask();
+          });
+          break;
+        case 'Adjust Brightness and Contrast':
+          selectedViewers.forEach((index) => {
+            viewers[index].props.adjustBrightnessAndContrast();
+          });
+          break;
+        case 'Crop Selected Area':
+          // Implement the logic to crop the selected area
+          selectedViewers.forEach((index) => {
+            viewers[index].props.cropSelectedArea();
+          });
+          console.log('Cropping Selected Area');
+          break;
+        case 'Delete Mask':
+          // Implement the logic to delete the mask
+          selectedViewers.forEach((index) => {
+            viewers[index].props.deleteMask();
+          });
+          console.log('Deleting Mask');
+          break;
+        case 'Image Comparison':
+          // Implement the logic for image comparison
+          selectedViewers.forEach((index) => {
+            viewers[index].props.imageComparison();
+          });
+          console.log('Comparing Images');
+          break;
+        case 'Invert Contrast':
+          // Implement the logic to invert the contrast
+          selectedViewers.forEach((index) => {
+            viewers[index].props.invertContrast();
+          });
+          console.log('Inverting Contrast');
+          break;
+        case '1x1 Layout':
+          setGridLayout('1x1');
+          break;
+        case '1x2 Layout':
+          setGridLayout('1x2');
+          break;
+        case '2x1 Layout':
+          setGridLayout('2x1');
+          break;
+        case '2x2 Layout':
+          setGridLayout('2x2');
+          break;
+        case 'Magnifier': {
+          // Implement the logic for magnifier
+          selectedViewers.forEach((index) => {
+            viewers[index].props.activateMagnifier();
+          });
+          break;
+        }
+        case 'Fit Size': {
+          // Implement the logic to fit the image size
+          selectedViewers.forEach((index) => {
+            viewers[index].props.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');
+          selectedViewers.forEach((index) => {
+            viewers[index].props.setOriginalSize();
+          });
+          break;
+        }
+        case 'Zoom Image':
+          // Implement the logic to zoom the image
+          selectedViewers.forEach((index) => {
+            viewers[index].props.zoomImage();
+          });
+          console.log('Zooming Image');
+          break;
+        case 'Reset Cursor':
+          // Implement the logic to reset the cursor
+          selectedViewers.forEach((index) => {
+            viewers[index].props.resetCursor();
+          });
+          console.log('Resetting Cursor');
+          break;
+        case 'Pan':
+          // Implement the logic to pan the image
+          selectedViewers.forEach((index) => {
+            viewers[index].props.panImage();
+          });
+          console.log('Panning Image');
+          break;
+        case 'Invert Image':
+          selectedViewers.forEach((index) => {
+            viewers[index].props.InvertImage();
+          });
+          break;
+        case 'Reset Image':
+          selectedViewers.forEach((index) => {
+            viewers[index].props.ResetImage();
+          });
+          break;
+        case 'Snapshot':
+          // Implement the logic to take a snapshot
+          selectedViewers.forEach((index) => {
+            viewers[index].props.takeSnapshot();
+          });
+          console.log('Taking Snapshot');
+          break;
+        case 'Advanced Processing':
+          // Implement the logic for advanced processing
+          selectedViewers.forEach((index) => {
+            viewers[index].props.advancedProcessing();
+          });
+          console.log('Performing Advanced Processing');
+          break;
+        case 'Musician':
+          // Implement the logic for musician
+          selectedViewers.forEach((index) => {
+            viewers[index].props.activateMusician();
+          });
+          console.log('Activating Musician');
+          break;
+        case 'Image Measurement':
+          // Implement the logic for image measurement
+          selectedViewers.forEach((index) => {
+            viewers[index].props.imageMeasurement();
+          });
+          console.log('Measuring Image');
+          break;
+        case 'More':
+          // Implement the logic for more options
+          selectedViewers.forEach((index) => {
+            viewers[index].props.moreOptions();
+          });
+          console.log('Showing More Options');
+          break;
+        case 'Apply Colormap':
+          selectedViewers.forEach((index) => {
+            viewers[index].props.ApplyColormap();
+          });
+          break;
+        default:
+          break;
+      }
+      dispatch(clearAction()); //清理后可连续同一个action触发响应
+    }
+  }, [action]);
+
+  const handleSelectViewer = (index: number) => {
+    setSelectedViewers((prev) =>
+      prev.includes(index) ? prev.filter((i) => i !== index) : [...prev, index]
+    );
+  };
+
+  const renderGrid = () => {
+    const viewers = imageUrls.map((url, index) => (
+      <div
+        key={index}
+        onClick={() => handleSelectViewer(index)}
+        style={{
+          border: selectedViewers.includes(index)
+            ? '2px solid blue'
+            : '1px solid gray',
+          cursor: 'pointer',
+        }}
+      >
+        <StackViewer imageIndex={index} imageUrls={[url]} viewportId={''} />
+      </div>
+    ));
+
+    switch (gridLayout) {
+      case '1x1':
+        return (
+          <div style={{ display: 'grid', gridTemplateColumns: '1fr' }}>
+            {viewers[0]}
+          </div>
+        );
+      case '1x2':
+        return (
+          <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr' }}>
+            {viewers.slice(0, 2)}
+          </div>
+        );
+      case '2x1':
+        return (
+          <div style={{ display: 'grid', gridTemplateRows: '1fr 1fr' }}>
+            {viewers.slice(0, 2)}
+          </div>
+        );
+      case '2x2':
+        return (
+          <div
+            style={{
+              display: 'grid',
+              gridTemplateColumns: '1fr 1fr',
+              gridTemplateRows: '1fr 1fr',
+            }}
+          >
+            {viewers.slice(0, 4)}
+          </div>
+        );
+      default:
+        return null;
+    }
+  };
+
+  return <div>{renderGrid()}</div>;
+};
+
+export default ViewerContainer;

+ 23 - 171
src/pages/view/components/viewers/stack.image.viewer.tsx

@@ -3,12 +3,8 @@ 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';
 import { annotation, SplineROITool } from '@cornerstonejs/tools';
 import { eventTarget } from '@cornerstonejs/core';
 
@@ -171,7 +167,7 @@ function registerTools(viewportId, renderingEngineId) {
 
   toolGroup.addViewport(viewportId, renderingEngineId);
 }
-function addLMark(): void {
+export function addLMark(): void {
   // Implement the logic to add an L mark
   console.log('Adding L Mark');
   toolGroup.setToolActive(LabelTool.toolName, {
@@ -190,7 +186,7 @@ function addLMark(): void {
   // const enabledElement = cornerstone.getEnabledElementByViewportId(currentViewportId);
   // cursors.elementCursor.resetElementCursor(elementRef.current as HTMLDivElement);
 }
-function addRLabel(viewportId) {
+export function addRLabel(viewportId) {
   toolGroup.setToolActive(LabelTool.toolName, {
     bindings: [],
   });
@@ -202,7 +198,7 @@ function addRLabel(viewportId) {
   toolGroup.setToolPassive(LabelTool.toolName, { removeAllBindings: true });
 }
 
-function adjustBrightnessAndContrast() {
+export function adjustBrightnessAndContrast() {
   const planar = toolGroup.getToolInstance(WindowLevelTool.toolName); // Reset rotation angle
   const isActive = planar.mode === csToolsEnums.ToolModes.Active;
   if (isActive) {
@@ -220,14 +216,14 @@ function adjustBrightnessAndContrast() {
   }
 }
 
-function fitImageSize() {
+export function fitImageSize() {
   const viewport = cornerstone.getEnabledElementByViewportId(currentViewportId)
     .viewport as cornerstone.StackViewport;
   viewport.resetCamera();
   viewport.render();
 }
 
-function deleteSelectedMark(): void {
+export function deleteSelectedMark(): void {
   const viewport =
     cornerstone.getEnabledElementByViewportId(currentViewportId).viewport;
   const allAnnotations = cornerstoneTools.annotation.state.getAllAnnotations();
@@ -241,7 +237,7 @@ function deleteSelectedMark(): void {
   viewport.render();
 }
 
-function addMask(): void {
+export function addMask(): void {
   // Implement the logic to add a mask
   console.log('Adding Mask');
   // Add the specific logic to add a mask here
@@ -257,7 +253,7 @@ function addMask(): void {
   });
 }
 
-function remoteMask(): void {
+export function remoteMask(): void {
   // 1. 获取所有 annotation
   const all = annotation.state.getAllAnnotations();
 
@@ -290,7 +286,7 @@ function remoteMask(): void {
     });
   }
 }
-function HorizontalFlip(): void {
+export function HorizontalFlip(): void {
   const viewport = cornerstone.getEnabledElementByViewportId(currentViewportId)
     .viewport as cornerstone.StackViewport;
   // 切换水平翻转状态
@@ -299,7 +295,7 @@ function HorizontalFlip(): void {
   console.log('Flipping Image Horizontally');
 }
 
-function VerticalFlip(): void {
+export function VerticalFlip(): void {
   const viewport = cornerstone.getEnabledElementByViewportId(currentViewportId)
     .viewport as cornerstone.StackViewport;
   // 切换竖直翻转状态
@@ -307,7 +303,7 @@ function VerticalFlip(): void {
   viewport.setCamera({ flipVertical: !flipVertical });
 }
 
-function ApplyColormap(): void {
+export function ApplyColormap(): void {
   const viewport = cornerstone.getEnabledElementByViewportId(currentViewportId)
     .viewport as cornerstone.StackViewport;
   // Implement the logic to apply colormap
@@ -316,7 +312,7 @@ function ApplyColormap(): void {
   console.log('Applying Colormap');
 }
 
-function RotateCounterclockwise90(): void {
+export function RotateCounterclockwise90(): void {
   const viewport = cornerstone.getEnabledElementByViewportId(currentViewportId)
     .viewport as cornerstone.StackViewport;
   // 获取当前相机
@@ -352,7 +348,7 @@ function RotateCounterclockwise90(): void {
   console.log('Rotating Image Counterclockwise 90°');
 }
 
-function RotateClockwise90(): void {
+export function RotateClockwise90(): void {
   const viewport = cornerstone.getEnabledElementByViewportId(currentViewportId)
     .viewport as cornerstone.StackViewport;
   const camera = viewport.getCamera();
@@ -385,7 +381,7 @@ function RotateClockwise90(): void {
   console.log('Rotating Image Clockwise 90°');
 }
 
-function ResetImage(): void {
+export function ResetImage(): void {
   const viewport = cornerstone.getEnabledElementByViewportId(currentViewportId)
     .viewport as cornerstone.StackViewport;
   // Implement the logic to reset the image
@@ -397,7 +393,7 @@ function ResetImage(): void {
   console.log('Resetting Image');
 }
 
-function InvertImage(): void {
+export function InvertImage(): void {
   const viewport = cornerstone.getEnabledElementByViewportId(currentViewportId)
     .viewport as cornerstone.StackViewport;
   // Implement the logic to invert the image
@@ -407,7 +403,7 @@ function InvertImage(): void {
   console.log('Inverting Image');
 }
 
-function setOriginalSize(viewport) {
+export function setOriginalSize(viewport) {
   // 1) 先正常 fit(或本来就是 fit 状态)
   viewport.resetCamera();
 
@@ -436,7 +432,7 @@ function setOriginalSize(viewport) {
   viewport.render();
 }
 
-function activateMagnifier() {
+export function activateMagnifier() {
   console.log('Activating Magnifier');
   const isActive =
     toolGroup.getActivePrimaryMouseButtonTool() === MagnifyTool.toolName;
@@ -455,7 +451,7 @@ function activateMagnifier() {
   }
 }
 
-function invertContrast() {
+export function invertContrast() {
   const viewport =
     cornerstone.getEnabledElementByViewportId(currentViewportId).viewport;
   const targetBool = !viewport.getProperties().invert;
@@ -465,7 +461,7 @@ function invertContrast() {
   viewport.render();
 }
 
-function rotateAnyAngle() {
+export function rotateAnyAngle() {
   const planar = toolGroup.getToolInstance(PlanarRotateTool.toolName); // Reset rotation angle
   const isActive = planar.mode === csToolsEnums.ToolModes.Active;
   console.log(
@@ -490,13 +486,15 @@ function rotateAnyAngle() {
 const StackViewer = ({
   imageIndex = 0,
   imageUrls = [],
+  viewportId,
 }: {
   imageIndex?: number;
   imageUrls?: string[];
+  viewportId: string;
 }) => {
   const elementRef = useRef<HTMLDivElement>(null);
-  const action = useSelector((state: RootState) => state.functionArea.action);
-  const dispatch = useDispatch();
+  // const action = useSelector((state: RootState) => state.functionArea.action);
+  // const dispatch = useDispatch();
 
   useEffect(() => {
     const setup = async () => {
@@ -537,7 +535,6 @@ const StackViewer = ({
         renderingEngineId
       );
 
-      const viewportId = 'CT_AXIAL_STACK';
       currentViewportId = viewportId;
       const viewportInput: cornerstone.Types.PublicViewportInput = {
         viewportId,
@@ -570,152 +567,7 @@ const StackViewer = ({
     };
 
     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
-          rotateAnyAngle();
-          break;
-        case 'AddMask':
-          addMask();
-          break;
-        case 'Delete Digital Mask':
-          remoteMask();
-          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
-          invertContrast();
-          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
-          activateMagnifier();
-          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]);
+  }, [elementRef, imageIndex, viewportId]);
 
   return (
     <div