Ver Fonte

feat(processing): implement multi-selection of viewers on image processing page

sw há 1 mês atrás
pai
commit
93dc895662

+ 137 - 57
src/pages/view/components/ViewerContainer.tsx

@@ -1,8 +1,49 @@
-import React, { useState, useEffect } from 'react';
+import React, { useState, useEffect, ReactElement } from 'react';
 import StackViewer from './viewers/stack.image.viewer';
 import { useSelector, useDispatch } from 'react-redux';
-import { RootState } from '@/states/store';
+import store, { RootState } from '@/states/store';
 import { clearAction } from '@/states/view/functionAreaSlice';
+import * as cornerstone from '@cornerstonejs/core';
+import * as cornerstoneTools from '@cornerstonejs/tools';
+import { SystemMode } from '@/states/systemModeSlice';
+import * as cornerstoneDICOMImageLoader from '@cornerstonejs/dicom-image-loader';
+
+const renderingEngineId = 'myRenderingEngine';
+const setup = () => {
+  // 初始化 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',
+      };
+    },
+  });
+  // 创建渲染引擎
+  new cornerstone.RenderingEngine(renderingEngineId);
+};
+setup();
 
 interface ViewerContainerProps {
   imageUrls: string[];
@@ -10,31 +51,49 @@ interface ViewerContainerProps {
 
 const ViewerContainer: React.FC<ViewerContainerProps> = ({ imageUrls }) => {
   const [selectedViewers, setSelectedViewers] = useState<number[]>([]);
-  const [gridLayout, setGridLayout] = useState<'1x1' | '1x2' | '2x1' | '2x2'>(
-    '1x1'
-  );
+  const [gridLayout, setGridLayout] = useState<string>('1x1');
   const action = useSelector((state: RootState) => state.functionArea.action);
   const dispatch = useDispatch();
+  // eslint-disable-next-line
+  const [viewersState, setViewersState] = useState<ReactElement[]>([]);
+  console.log(`[ViewerContainer] rerendered]`);
 
-  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
+  const renderViewers = (start: number, end: number) => {
+    return imageUrls.slice(start, end).map((url, index) => (
+      <div
         key={index}
-        imageIndex={0}
-        imageUrls={[url]}
-        viewportId={`viewport-${index}`}
-      />
-    </div>
-  ));
+        onClick={(event) => handleSelectViewer(index, event)}
+        style={{
+          border: selectedViewers.includes(index)
+            ? '2px solid blue'
+            : '1px solid gray',
+          cursor: 'pointer',
+        }}
+      >
+        <input
+          type="checkbox"
+          checked={selectedViewers.includes(index)}
+          onChange={(event) => handleCheckboxChange(index, event)}
+          style={{ marginRight: '8px' }}
+        />
+        <StackViewer
+          key={index}
+          imageIndex={0}
+          imageUrls={[url]}
+          viewportId={`viewport-${index}`}
+          renderingEngineId={renderingEngineId}
+        />
+      </div>
+    ));
+  };
+
+  // useEffect(() => {
+
+  // }, [imageUrls]);
+
+  useEffect(() => {
+    renderGrid();
+  }, [viewersState]);
 
   useEffect(() => {
     if (action) {
@@ -42,96 +101,97 @@ const ViewerContainer: React.FC<ViewerContainerProps> = ({ imageUrls }) => {
       switch (action) {
         case 'Add L Mark':
           selectedViewers.forEach((index) => {
-            viewers[index].props.addLMark();
+            viewersState[index].props.addLMark();
           });
           break;
         case 'Add R Mark':
           // Implement the logic to add an R mark
           selectedViewers.forEach((index) => {
-            viewers[index].props.addRLabel();
+            viewersState[index].props.addRLabel();
           });
           console.log('Adding R Mark');
           break;
         case 'Delete Selected Mark': {
           selectedViewers.forEach((index) => {
-            viewers[index].props.deleteSelectedMark();
+            viewersState[index].props.deleteSelectedMark();
           });
           break;
         }
         case 'Horizontal Flip': {
           selectedViewers.forEach((index) => {
-            viewers[index].props.HorizontalFlip();
+            viewersState[index].props.HorizontalFlip();
           });
           break;
         }
         case 'Vertical Flip': {
           selectedViewers.forEach((index) => {
-            viewers[index].props.VerticalFlip();
+            viewersState[index].props.VerticalFlip();
           });
           break;
         }
         case 'Rotate Counterclockwise 90': {
           selectedViewers.forEach((index) => {
-            viewers[index].props.RotateCounterclockwise90();
+            viewersState[index].props.RotateCounterclockwise90();
           });
           break;
         }
         case 'Rotate Clockwise 90':
           selectedViewers.forEach((index) => {
-            viewers[index].props.RotateClockwise90();
+            viewersState[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();
+            viewersState[index].props.rotateAnyAngle();
           });
           break;
         case 'AddMask':
           selectedViewers.forEach((index) => {
-            viewers[index].props.addMask();
+            viewersState[index].props.addMask();
           });
           break;
         case 'Delete Digital Mask':
           selectedViewers.forEach((index) => {
-            viewers[index].props.remoteMask();
+            viewersState[index].props.remoteMask();
           });
           break;
         case 'Adjust Brightness and Contrast':
           selectedViewers.forEach((index) => {
-            viewers[index].props.adjustBrightnessAndContrast();
+            viewersState[index].props.adjustBrightnessAndContrast();
           });
           break;
         case 'Crop Selected Area':
           // Implement the logic to crop the selected area
           selectedViewers.forEach((index) => {
-            viewers[index].props.cropSelectedArea();
+            viewersState[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();
+            viewersState[index].props.deleteMask();
           });
           console.log('Deleting Mask');
           break;
         case 'Image Comparison':
           // Implement the logic for image comparison
           selectedViewers.forEach((index) => {
-            viewers[index].props.imageComparison();
+            viewersState[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();
+            viewersState[index].props.invertContrast();
           });
           console.log('Inverting Contrast');
           break;
         case '1x1 Layout':
           setGridLayout('1x1');
+          // renderGrid('1x1');
           console.log(`1x1 Layout`);
           break;
         case '1x2 Layout':
@@ -146,14 +206,14 @@ const ViewerContainer: React.FC<ViewerContainerProps> = ({ imageUrls }) => {
         case 'Magnifier': {
           // Implement the logic for magnifier
           selectedViewers.forEach((index) => {
-            viewers[index].props.activateMagnifier();
+            viewersState[index].props.activateMagnifier();
           });
           break;
         }
         case 'Fit Size': {
           // Implement the logic to fit the image size
           selectedViewers.forEach((index) => {
-            viewers[index].props.fitImageSize();
+            viewersState[index].props.fitImageSize();
           });
           console.log('Fitting Image Size');
           break;
@@ -162,79 +222,79 @@ const ViewerContainer: React.FC<ViewerContainerProps> = ({ imageUrls }) => {
           // Implement the logic to set the image to original size
           console.log('Setting Image to Original Size');
           selectedViewers.forEach((index) => {
-            viewers[index].props.setOriginalSize();
+            viewersState[index].props.setOriginalSize();
           });
           break;
         }
         case 'Zoom Image':
           // Implement the logic to zoom the image
           selectedViewers.forEach((index) => {
-            viewers[index].props.zoomImage();
+            viewersState[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();
+            viewersState[index].props.resetCursor();
           });
           console.log('Resetting Cursor');
           break;
         case 'Pan':
           // Implement the logic to pan the image
           selectedViewers.forEach((index) => {
-            viewers[index].props.panImage();
+            viewersState[index].props.panImage();
           });
           console.log('Panning Image');
           break;
         case 'Invert Image':
           selectedViewers.forEach((index) => {
-            viewers[index].props.InvertImage();
+            viewersState[index].props.InvertImage();
           });
           break;
         case 'Reset Image':
           selectedViewers.forEach((index) => {
-            viewers[index].props.ResetImage();
+            viewersState[index].props.ResetImage();
           });
           break;
         case 'Snapshot':
           // Implement the logic to take a snapshot
           selectedViewers.forEach((index) => {
-            viewers[index].props.takeSnapshot();
+            viewersState[index].props.takeSnapshot();
           });
           console.log('Taking Snapshot');
           break;
         case 'Advanced Processing':
           // Implement the logic for advanced processing
           selectedViewers.forEach((index) => {
-            viewers[index].props.advancedProcessing();
+            viewersState[index].props.advancedProcessing();
           });
           console.log('Performing Advanced Processing');
           break;
         case 'Musician':
           // Implement the logic for musician
           selectedViewers.forEach((index) => {
-            viewers[index].props.activateMusician();
+            viewersState[index].props.activateMusician();
           });
           console.log('Activating Musician');
           break;
         case 'Image Measurement':
           // Implement the logic for image measurement
           selectedViewers.forEach((index) => {
-            viewers[index].props.imageMeasurement();
+            viewersState[index].props.imageMeasurement();
           });
           console.log('Measuring Image');
           break;
         case 'More':
           // Implement the logic for more options
           selectedViewers.forEach((index) => {
-            viewers[index].props.moreOptions();
+            viewersState[index].props.moreOptions();
           });
           console.log('Showing More Options');
           break;
         case 'Apply Colormap':
           selectedViewers.forEach((index) => {
-            viewers[index].props.ApplyColormap();
+            viewersState[index].props.ApplyColormap();
           });
           break;
         default:
@@ -244,13 +304,33 @@ const ViewerContainer: React.FC<ViewerContainerProps> = ({ imageUrls }) => {
     }
   }, [action]);
 
-  const handleSelectViewer = (index: number) => {
+  const handleSelectViewer = (index: number, event) => {
     setSelectedViewers((prev) =>
       prev.includes(index) ? prev.filter((i) => i !== index) : [...prev, index]
     );
+    console.log(`handleSelectViewer : ${index}`);
+
+    // Directly update the div style using a ref
+    const viewerElement = event.currentTarget;
+    console.log(`viewerElement:`, viewerElement);
+    console.log(`selectedViewers:`, selectedViewers);
+    if (viewerElement) {
+      viewerElement.style.border = selectedViewers.includes(index)
+        ? '1px solid gray'
+        : '2px solid blue';
+    }
+  };
+
+  const handleCheckboxChange = (index: number, event) => {
+    setSelectedViewers((prev) =>
+      event.target.checked ? [...prev, index] : prev.filter((i) => i !== index)
+    );
+
+    event.target.checked = !event.target.checked;
   };
 
   const renderGrid = () => {
+    console.log('Rendering  layout', gridLayout);
     switch (gridLayout) {
       case '1x1':
         return (
@@ -258,7 +338,7 @@ const ViewerContainer: React.FC<ViewerContainerProps> = ({ imageUrls }) => {
             className="h-full w-full"
             style={{ display: 'grid', gridTemplateColumns: '1fr' }}
           >
-            {viewers[0]}
+            {renderViewers(0, 1)}
           </div>
         );
       case '1x2':
@@ -267,13 +347,13 @@ const ViewerContainer: React.FC<ViewerContainerProps> = ({ imageUrls }) => {
             className="h-full w-full"
             style={{ display: 'grid', gridTemplateColumns: '1fr 1fr' }}
           >
-            {viewers.slice(0, 2)}
+            {renderViewers(0, 2)}
           </div>
         );
       case '2x1':
         return (
           <div className="h-full w-full grid grid-cols-1 grid-rows-2">
-            {viewers.slice(0, 2)}
+            {renderViewers(0, 2)}
           </div>
         );
       case '2x2':
@@ -286,7 +366,7 @@ const ViewerContainer: React.FC<ViewerContainerProps> = ({ imageUrls }) => {
               gridTemplateRows: '1fr 1fr',
             }}
           >
-            {viewers.slice(0, 4)}
+            {renderViewers(0, 4)}
           </div>
         );
       default:

+ 42 - 43
src/pages/view/components/viewers/stack.image.viewer.tsx

@@ -2,9 +2,6 @@ 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 { SystemMode } from '@/states/systemModeSlice';
-import store from '@/states/store';
 import { annotation, SplineROITool } from '@cornerstonejs/tools';
 import { eventTarget } from '@cornerstonejs/core';
 
@@ -487,10 +484,12 @@ const StackViewer = ({
   imageIndex = 0,
   imageUrls = [],
   viewportId,
+  renderingEngineId,
 }: {
   imageIndex?: number;
   imageUrls?: string[];
   viewportId: string;
+  renderingEngineId: string;
 }) => {
   const elementRef = useRef<HTMLDivElement>(null);
   // const action = useSelector((state: RootState) => state.functionArea.action);
@@ -498,52 +497,52 @@ const StackViewer = ({
 
   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
-      );
-
+      // // 初始化 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',
+      //     };
+      //   },
+      // });
       currentViewportId = viewportId;
       const viewportInput: cornerstone.Types.PublicViewportInput = {
         viewportId,
         element: elementRef.current!,
         type: cornerstone.Enums.ViewportType.STACK,
       };
-
+      const renderingEngine = cornerstone.getRenderingEngine(renderingEngineId);
+      if (!renderingEngine) {
+        console.error(
+          `[stack.image.viewer] No rendering engine with id ${renderingEngineId} found`
+        );
+        return;
+      }
+      // Enable the element for use with Cornerstone
       renderingEngine.enableElement(viewportInput);
-      registerTools(viewportId, renderingEngineId);
+      registerTools(viewportId, renderingEngine.id);
 
       // Get the stack viewport that was created
       const viewport = renderingEngine.getViewport(
@@ -567,7 +566,7 @@ const StackViewer = ({
     };
 
     setup();
-  }, [elementRef, imageIndex, viewportId]);
+  }, [elementRef, imageIndex, viewportId, renderingEngineId]);
 
   return (
     <div