From 4213e3b6809ee374ee15d325d23da9fba6c457b3 Mon Sep 17 00:00:00 2001 From: Ross Date: Tue, 20 May 2025 09:31:04 +0100 Subject: [PATCH] work towards functional multiviewport --- dicom-viewer/src/App.tsx | 674 +++++++++++++++++++++++---------------- 1 file changed, 395 insertions(+), 279 deletions(-) diff --git a/dicom-viewer/src/App.tsx b/dicom-viewer/src/App.tsx index f38e34d..ae35feb 100644 --- a/dicom-viewer/src/App.tsx +++ b/dicom-viewer/src/App.tsx @@ -53,6 +53,7 @@ const MOUSE_BUTTONS = [ ] + function App() { const elementRef = useRef(null) const viewportRef = useRef(null) // Store the viewport instance @@ -71,12 +72,16 @@ function App() { windowWidth: "", }) + // Store imageIds globally after loading so they can be reused + const [loadedImageIds, setLoadedImageIds] = useState(null); + const [viewportGrid, setViewportGrid] = useState({ rows: 1, cols: 1 }) const [viewportMenuOpen, setViewportMenuOpen] = useState(false) // Store refs for each viewport const viewportRefs = useRef>([]) + const renderingEnginesRef = useRef>({}); // Helper to generate grid options const gridOptions = [] @@ -86,6 +91,26 @@ function App() { } } + // State for overlay info per viewport + const [imageInfos, setImageInfos] = useState>(() => + Array(viewportGrid.rows * viewportGrid.cols).fill({ + index: 0, + total: 0, + windowCenter: "", + windowWidth: "", + }) + ); + + // State for preset menu open/close per viewport + const [presetsOpenArr, setPresetsOpenArr] = useState( + () => Array(viewportGrid.rows * viewportGrid.cols).fill(false) + ); + // State for selected tool per mouse button const [mouseToolBindings, setMouseToolBindings] = useState({ Primary: WindowLevelTool.toolName, @@ -160,172 +185,161 @@ function App() { setMouseToolBindings(prev => ({ ...prev, [mouseButton]: toolName })) } - const updateOverlay = useCallback(() => { - const viewport = viewportRef.current - if (!viewport) return - - // For StackViewport in Cornerstone3D v3: + // Update overlay info for a specific viewport + const updateOverlay = useCallback((viewportIdx: number, viewport: any) => { + if (!viewport) return; const currentIndex = typeof viewport.getCurrentImageIdIndex === "function" ? viewport.getCurrentImageIdIndex() - : 0 + : 0; const imageIds = typeof viewport.getImageIds === "function" ? viewport.getImageIds() - : [] - const total = imageIds.length + : []; + const total = imageIds.length; - // Use getVOI() for current window center/width - let windowCenter = "" - let windowWidth = "" + let windowCenter = ""; + let windowWidth = ""; if (typeof viewport.getProperties === "function") { - const props = viewport.getProperties() + const props = viewport.getProperties(); if (props.voiRange) { - const { lower, upper } = props.voiRange - windowCenter = ((upper + lower) / 2).toFixed(0) - windowWidth = (upper - lower).toFixed(0) + const { lower, upper } = props.voiRange; + windowCenter = ((upper + lower) / 2).toFixed(0); + windowWidth = (upper - lower).toFixed(0); } } - setImageInfo({ - index: currentIndex + 1, - total, - windowCenter, - windowWidth, - }) - }, []) + setImageInfos(prev => { + const next = [...prev]; + next[viewportIdx] = { + index: currentIndex + 1, + total, + windowCenter, + windowWidth, + }; + return next; + }); + }, []); + + // Store the properties of existing viewports before grid change + const prevViewportProps = useRef>({}); + useEffect(() => { const setup = async () => { - if (running.current) { - return - } - running.current = true - - await csRenderInit() - await csToolsInit() - dicomImageLoaderInit({ maxWebWorkers: 1 }) - - // Add tools to Cornerstone3D - cornerstoneTools.addTool(PanTool) - cornerstoneTools.addTool(WindowLevelTool) - cornerstoneTools.addTool(StackScrollTool) - cornerstoneTools.addTool(ZoomTool) - cornerstoneTools.addTool(PlanarRotateTool) - cornerstoneTools.addTool(LengthTool) - cornerstoneTools.addTool(ProbeTool) - cornerstoneTools.addTool(ArrowAnnotateTool) - cornerstoneTools.addTool(RectangleROITool) - cornerstoneTools.addTool(EllipticalROITool) - cornerstoneTools.addTool(PlanarFreehandROITool) - cornerstoneTools.addTool(PlanarFreehandContourSegmentationTool) - cornerstoneTools.addTool(SculptorTool) - - // Create tool group ONCE - let toolGroup = ToolGroupManager.getToolGroup(toolGroupId) - if (!toolGroup) { - toolGroup = ToolGroupManager.createToolGroup(toolGroupId) - toolGroup.addTool(WindowLevelTool.toolName) - toolGroup.addTool(PanTool.toolName) - toolGroup.addTool(ZoomTool.toolName) - toolGroup.addTool(StackScrollTool.toolName, { loop: false }) - toolGroup.addTool(PlanarRotateTool.toolName) - toolGroup.addTool(LengthTool.toolName) - toolGroup.addTool(ArrowAnnotateTool.toolName, { - getTextCallback: () => '', - }) - toolGroup.addTool(ProbeTool.toolName, { - statsCalculator: () => null, - }) - toolGroup.addTool(RectangleROITool.toolName, { - calculateStats: false, - getTextCallback: () => '', - }) - toolGroup.addTool(EllipticalROITool.toolName, { - getTextCallback: () => '', - }) - toolGroup.addTool(PlanarFreehandROITool.toolName, { - calculateStats: false, - }) - toolGroup.addTool(PlanarFreehandContourSegmentationTool.toolName, { - }) - toolGroup.addTool(SculptorTool.toolName, { - }) - } - - // The Stack Scroll mouse wheel is a tool using the `mouseWheelCallback` - // and needs to be registered against the 'Wheel' binding. - toolGroup.setToolActive(StackScrollTool.toolName, { - bindings: [ - { - mouseButton: MouseBindings.Wheel, // Wheel Mouse - }, - ], - }); - - - const imageIds = await createImageIdsAndCacheMetaData() - - // Instantiate a rendering engine - const renderingEngineId = "myRenderingEngine" - const renderingEngine = new RenderingEngine(renderingEngineId) - const viewportId = "CT" - - - const viewportInput = { - viewportId, - type: Enums.ViewportType.STACK, - element: elementRef.current, - } - - renderingEngine.enableElement(viewportInput) - - // Set the tool group on the viewport - toolGroup.addViewport(viewportId, renderingEngineId); - - // Get the stack viewport that was created - const viewport = renderingEngine.getViewport(viewportId); - viewportRef.current = viewport; // Save for later use - - // Create a stack volume - viewport.setStack(imageIds) - - const segmentationId = 'my-segmentation-id'; - - // Create a segmentation representation for the stack - segmentation.addSegmentations([{ - segmentationId, - representation: { - type: csEnums.SegmentationRepresentations.Contour + // 1. Capture state of existing viewports before grid change + for (let i = 0; i < viewportGrid.rows * viewportGrid.cols; i++) { + const renderingEngineId = `renderingEngine_${i}`; + const viewportId = `CT_${i}`; + try { + const renderingEngine = renderingEnginesRef.current[renderingEngineId]; + if (renderingEngine) { + const viewport = renderingEngine.getViewport(viewportId); + if (viewport && typeof viewport.getProperties === "function") { + prevViewportProps.current[i] = viewport.getProperties(); + } + } + } catch (e) { + // Ignore if not found } - }]); - //segmentation.activeSegmentation.setActiveSegmentationRepresentations(segmentationId); - // Create a segmentation representation associated to the viewportId - await segmentation.addSegmentationRepresentations(viewportId, [ - { - segmentationId, - type: csToolsEnums.SegmentationRepresentations.Contour, - }, - ]); + } - segmentation.segmentIndex.setActiveSegmentIndex(segmentationId, 1); - cornerstoneTools.utilities.stackPrefetch.enable(viewport.element); + if (running.current) { + if (loadedImageIds) { + for (let i = 0; i < viewportGrid.rows * viewportGrid.cols; i++) { + const renderingEngineId = `renderingEngine_${i}`; + const viewportId = `CT_${i}`; + const element = viewportRefs.current[i]; + if (!element) continue; - // Render the image - viewport.render() - viewport.element.addEventListener(Enums.Events.IMAGE_RENDERED, updateOverlay) - ///viewport.element.addEventListener(Enums.Events.STACK_NEW_IMAGE, updateOverlay) - //viewport.element.addEventListener(Enums.Events.CAMERA_MODIFIED, updateOverlay) - //viewport.element.addEventListener(Enums.Events.PRESET_MODIFIED, updateOverlay) + const renderingEngine = new RenderingEngine(renderingEngineId); + renderingEnginesRef.current[renderingEngineId] = renderingEngine; + const viewportInput = { + viewportId, + type: Enums.ViewportType.STACK, + element, + }; + renderingEngine.enableElement(viewportInput); - // Initial overlay update - updateOverlay() + const toolGroup = ToolGroupManager.getToolGroup(toolGroupId); + toolGroup?.addViewport(viewportId, renderingEngineId); - // Apply mouse tool bindings after setup - applyMouseToolBindings() - } + const viewport = renderingEngine.getViewport(viewportId); + viewport.setStack(loadedImageIds); - setup() +/* // Restore previous state for existing viewports only + if (prevViewportProps.current[i] && typeof viewport.setProperties === "function") { + viewport.setProperties(prevViewportProps.current[i]); + } */ - // Create a stack viewport - }, [elementRef, updateOverlay]) + cornerstoneTools.utilities.stackPrefetch.enable(viewport.element); + console.log(i, viewport) + viewport.resetCameraForResize(); + viewport.render(); + + // Attach overlay update for each viewport + viewport.element.addEventListener(Enums.Events.IMAGE_RENDERED, () => updateOverlay(i, viewport)); + // Initial overlay update + updateOverlay(i, viewport); + } + applyMouseToolBindings(); + return; + } + return; + } + running.current = true; + + await csRenderInit(); + await csToolsInit(); + dicomImageLoaderInit({ maxWebWorkers: 1 }); + + const toolGroup = setupTools(); + const imageIds = await createImageIdsAndCacheMetaData(); + setLoadedImageIds(imageIds); + + for (let i = 0; i < viewportGrid.rows * viewportGrid.cols; i++) { + const renderingEngineId = `renderingEngine_${i}`; + const viewportId = `CT_${i}`; + const element = viewportRefs.current[i]; + if (!element) continue; + + const renderingEngine = new RenderingEngine(renderingEngineId); + renderingEnginesRef.current[renderingEngineId] = renderingEngine; + const viewportInput = { + viewportId, + type: Enums.ViewportType.STACK, + element, + }; + renderingEngine.enableElement(viewportInput); + + toolGroup.addViewport(viewportId, renderingEngineId); + + const viewport = renderingEngine.getViewport(viewportId); + viewport.setStack(imageIds); + + // Restore previous state for existing viewports only + if (prevViewportProps.current[i] && typeof viewport.setProperties === "function") { + viewport.setProperties(prevViewportProps.current[i]); + } + + cornerstoneTools.utilities.stackPrefetch.enable(viewport.element); + viewport.resize(); + viewport.render(); + + console.log(viewport) + + // Attach overlay update for each viewport + viewport.element.addEventListener(Enums.Events.IMAGE_RENDERED, () => updateOverlay(i, viewport)); + // Initial overlay update + updateOverlay(i, viewport); + } + applyMouseToolBindings(); + }; + + // Delay setup by 100ms (adjust as needed) + const timeout = setTimeout(() => { + setup(); + }, 300); + + return () => clearTimeout(timeout); + }, [elementRef, updateOverlay, viewportGrid, loadedImageIds]); // Handler to reset the view (camera and window/level) const handleResetView = useCallback(() => { @@ -340,21 +354,42 @@ function App() { // Double-click handler (reuse the same logic) const handleDoubleClick = handleResetView - // Handler to apply a preset - const handleApplyPreset = useCallback((center: number, width: number) => { - const viewport = viewportRef.current - if (viewport && typeof viewport.setProperties === "function") { - const props = viewport.getProperties() - viewport.setProperties({ - ...props, - voiRange: { - lower: center - width / 2, - upper: center + width / 2, - }, - }) - viewport.render() + // Handler to apply a preset for a specific viewport + const handleApplyPreset = useCallback((viewportIdx: number, center: number, width: number) => { + console.log("Applying preset:", center, width); + const renderingEngineId = `renderingEngine_${viewportIdx}`; + const viewportId = `CT_${viewportIdx}`; + try { +const renderingEngine = renderingEnginesRef.current[renderingEngineId]; + console.log("Rendering engine:", renderingEngine); + if (renderingEngine) { + const viewport = renderingEngine.getViewport(viewportId); + if (viewport && typeof viewport.setProperties === "function") { + viewport.setProperties({ + voiRange: { + lower: center - width / 2, + upper: center + width / 2, + }, + }); + viewport.render(); + updateOverlay(viewportIdx, viewport); + } + } + } catch (e) { + // Ignore if not found } - }, []) + }, [updateOverlay]); + + // Update overlay and preset state arrays when grid changes + useEffect(() => { + setImageInfos(Array(viewportGrid.rows * viewportGrid.cols).fill({ + index: 0, + total: 0, + windowCenter: "", + windowWidth: "", + })); + setPresetsOpenArr(Array(viewportGrid.rows * viewportGrid.cols).fill(false)); + }, [viewportGrid]); // Re-apply bindings when tool sets or ctrl changes useEffect(() => { @@ -374,9 +409,18 @@ function App() { return () => document.removeEventListener("mousedown", handleClick); }, [presetsOpen]); + useEffect(() => { + const handleContextMenu = (e: MouseEvent) => { + e.preventDefault(); + }; + document.addEventListener("contextmenu", handleContextMenu); + return () => { + document.removeEventListener("contextmenu", handleContextMenu); + }; + }, []); // Render the grid of viewports - const viewports = [] + const viewports = []; for (let i = 0; i < viewportGrid.rows * viewportGrid.cols; i++) { viewports.push(
- {/* You can render overlays or controls per-viewport here */} + {/* Overlay info (bottom left of viewport) */} +
+
+ Image: {imageInfos[i]?.index} / {imageInfos[i]?.total} +
+
+ WC: {imageInfos[i]?.windowCenter}   WW: {imageInfos[i]?.windowWidth} +
+
+ + {/* Preset menu (bottom right of viewport) */} +
+
+
+ setPresetsOpenArr(prev => { + const next = [...prev]; + next[i] = !next[i]; + return next; + }) + } + > + Presets + + {presetsOpenArr[i] ? "▲" : "▼"} + +
+ {presetsOpenArr[i] && ( +
+ {WINDOW_LEVEL_PRESETS.map((preset) => ( + + ))} +
+ )} +
+
- ) + ); } + + return ( -
+
+ {/* Reset View Button */} -
e.preventDefault()} // <-- Add this line - style={{ - width: "100%", - height: "100%", - backgroundColor: "#000", - }} - >
+ {/* Viewport grid menu at top center */}
- {/* Viewport grid */} + {/* Viewport grid (inside main div) */}
{viewports}
- {/* Overlay at bottom left */} -
-
- Image: {imageInfo.index} / {imageInfo.total} -
-
- WC: {imageInfo.windowCenter}   WW: {imageInfo.windowWidth} -
-
- {/* Preset menu at bottom right, expands on click */} -
-
-
setPresetsOpen((open) => !open)} - > - Presets - - {presetsOpen ? "▲" : "▼"} - -
- {presetsOpen && ( -
- {WINDOW_LEVEL_PRESETS.map((preset) => ( - - ))} -
- )} -
-
+ + {/* Settings button at top right */}