From e618f0d9173b8910ad8b364dd9ce51aa53eed34e Mon Sep 17 00:00:00 2001 From: Ross Date: Thu, 22 May 2025 23:15:39 +0100 Subject: [PATCH] fix some multiview stuff --- dicom-viewer/src/App.tsx | 265 ++++++++++++++++++++------------------- 1 file changed, 136 insertions(+), 129 deletions(-) diff --git a/dicom-viewer/src/App.tsx b/dicom-viewer/src/App.tsx index 115f724..96e3339 100644 --- a/dicom-viewer/src/App.tsx +++ b/dicom-viewer/src/App.tsx @@ -12,6 +12,7 @@ import * as cornerstoneTools from '@cornerstonejs/tools'; import { StackScrollOutOfBoundsEvent } from 'core/src/types/EventTypes'; import { segmentation, Enums as csEnums } from '@cornerstonejs/tools'; import { voi } from "@cornerstonejs/tools/utilities" +import { readDicomElementExplicit } from "dicom-parser" const { MouseBindings, KeyboardBindings } = csToolsEnums; @@ -73,6 +74,10 @@ function App() { windowWidth: "", }) + const MAX_VIEWPORTS = 9; + const renderingEngineId = "mainRenderingEngine"; + const renderingEngineRef = useRef(null); + // Store imageIds globally after loading so they can be reused const [loadedImageIds, setLoadedImageIds] = useState(null); @@ -82,7 +87,6 @@ function App() { // Store refs for each viewport const viewportRefs = useRef>([]) - const renderingEnginesRef = useRef>({}); // Store the properties and scroll position of existing viewports before grid change const prevViewportProps = useRef>({}); @@ -224,167 +228,163 @@ function App() { }); }, []); - useEffect(() => { - const setup = async () => { - // 1. Save properties and scroll position for only the existing viewports - const prevCount = Object.keys(renderingEnginesRef.current).length; - for (let i = 0; i < prevCount; i++) { - const renderingEngineId = `renderingEngine_${i}`; + function saveVisibleViewportState() { + for (let i = 0; i < viewportGrid.rows * viewportGrid.cols; i++) { const viewportId = `CT_${i}`; - const renderingEngine = renderingEnginesRef.current[renderingEngineId]; + const renderingEngine = renderingEngineRef.current; if (renderingEngine) { const viewport = renderingEngine.getViewport(viewportId); - if (viewport) { - if (typeof viewport.getProperties === "function") { - console.log("Saving previous state for viewport", i, viewport.getProperties()) - prevViewportProps.current[i] = viewport.getProperties(); - } - if (typeof viewport.getCurrentImageIdIndex === "function") { - prevScrollIndices.current[i] = viewport.getCurrentImageIdIndex(); - } + if (viewport && typeof viewport.getProperties === "function") { + prevViewportProps.current[i] = viewport.getProperties(); + } + if (viewport && typeof viewport.getCurrentImageIdIndex === "function") { + prevScrollIndices.current[i] = viewport.getCurrentImageIdIndex(); } } } + } - 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; - - let renderingEngine = renderingEnginesRef.current[renderingEngineId]; - if (!renderingEngine) { - renderingEngine = new RenderingEngine(renderingEngineId); - renderingEnginesRef.current[renderingEngineId] = renderingEngine; - } - const viewportInput = { - viewportId, - type: Enums.ViewportType.STACK, - element, - }; - renderingEngine.enableElement(viewportInput); - - const toolGroup = ToolGroupManager.getToolGroup(toolGroupId); - toolGroup?.addViewport(viewportId, renderingEngineId); - - const viewport = renderingEngine.getViewport(viewportId); - viewport.setStack(loadedImageIds); - - - renderingEngine.resize(true, true) - - cornerstoneTools.utilities.stackPrefetch.enable(viewport.element); - viewport.resize(); - viewport.render(); - - // Restore previous state for existing viewports only - if (prevViewportProps.current[i] && typeof viewport.setProperties === "function") { - console.log("Restoring properties for viewport", i, prevViewportProps.current[i]) - console.log(viewport) - viewport.setProperties(prevViewportProps.current[i]); - } - // Restore previous scroll position for existing viewports only - if ( - prevScrollIndices.current[i] !== undefined && - typeof viewport.setImageIdIndex === "function" - ) { - viewport.setImageIdIndex(prevScrollIndices.current[i]); - } - - updateOverlay(i, viewport); - - // 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; + useEffect(() => { + const setup = async () => { + // 1. Initialize Cornerstone3D and tools ONCE + if (!running.current) { + running.current = true; + await csRenderInit(); + await csToolsInit(); + dicomImageLoaderInit({ maxWebWorkers: 1 }); } - running.current = true; - await csRenderInit(); - await csToolsInit(); - dicomImageLoaderInit({ maxWebWorkers: 1 }); + // 2. Load image IDs if not already loaded + let imageIds = loadedImageIds; + if (!imageIds) { + imageIds = await createImageIdsAndCacheMetaData(); + setLoadedImageIds(imageIds); + } - const toolGroup = setupTools(); - const imageIds = await createImageIdsAndCacheMetaData(); - setLoadedImageIds(imageIds); + // 3. Get or create the rendering engine + let renderingEngine = renderingEngineRef.current; + if (!renderingEngine) { + renderingEngine = new RenderingEngine(renderingEngineId); + renderingEngineRef.current = renderingEngine; + } - for (let i = 0; i < viewportGrid.rows * viewportGrid.cols; i++) { - const renderingEngineId = `renderingEngine_${i}`; + // 4. Setup tool group + const toolGroup = ToolGroupManager.getToolGroup(toolGroupId) || setupTools(); + + // 6. Enable and configure each viewport + for (let i = 0; i < MAX_VIEWPORTS; 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); + // Enable element only once + if (!element._cornerstoneEnabled) { + if (!renderingEngine.getViewport(viewportId)) { + renderingEngine.enableElement({ + viewportId, + type: Enums.ViewportType.STACK, + element, + }); + element._cornerstoneEnabled = true; + } + // Add tool group only once + toolGroup.addViewport(viewportId, renderingEngineId); + } const viewport = renderingEngine.getViewport(viewportId); - viewport.setStack(imageIds); + if (!viewport) continue; -/* // Restore previous state for existing viewports only - if ( - prevViewportProps.current[i] && - typeof viewport.setProperties === "function" - ) { - viewport.setProperties(prevViewportProps.current[i]); + // Set stack if images are loaded + if (imageIds) { + viewport.setStack(imageIds); } + // Restore previous scroll position for existing viewports only if ( prevScrollIndices.current[i] !== undefined && typeof viewport.setImageIdIndex === "function" ) { viewport.setImageIdIndex(prevScrollIndices.current[i]); - } */ - - cornerstoneTools.utilities.stackPrefetch.enable(viewport.element); - - setTimeout(() => { - console.log("Resizing viewport", i); - renderingEngine.resize(false, false) - viewport.resetCameraForResize(); - viewport.render(); - }, 500); + } + // Restore previous state for existing viewports only + const prevProps = prevViewportProps.current[i]; + if (prevProps && typeof viewport.setProperties === "function") { + // Only restore safe properties + const { voiRange, invert, interpolationType, VOILUTFunction, colormap } = prevProps; + const propsToRestore: any = {}; + if (voiRange && voiRange.lower !== undefined && voiRange.upper !== undefined) propsToRestore.voiRange = voiRange; + if (invert !== undefined) propsToRestore.invert = invert; + if (interpolationType !== undefined) propsToRestore.interpolationType = interpolationType; + if (VOILUTFunction) propsToRestore.VOILUTFunction = VOILUTFunction; + if ( + colormap !== undefined && + colormap !== null && + (typeof colormap === "string" || (typeof colormap === "object" && !("actor" in colormap))) + ) { + propsToRestore.colormap = colormap; + } + setTimeout(() => { + if (viewportId === "CT_0") { + console.log("Restoring properties:", propsToRestore, viewport); + console.log("VOI range:", voiRange); + } + viewport.setProperties(propsToRestore); + if (viewportId === "CT_0") { + console.log("Restored:", propsToRestore, viewport); + console.log(viewport.getProperties()); + } + viewport.render() + }, 100); + } - console.log(viewport) + // Attach ResizeObserver to handle dynamic resizing (only once) + if (!element._resizeObserver) { + const resizeObserver = new window.ResizeObserver(() => { + if (element.offsetWidth > 0 && element.offsetHeight > 0) { + renderingEngine.resize(); + viewport.resize(); + viewport.render(); + } + }); + resizeObserver.observe(element); + element._resizeObserver = resizeObserver; + } - // Attach overlay update for each viewport - viewport.element.addEventListener(Enums.Events.IMAGE_RENDERED, () => updateOverlay(i, viewport)); - // Initial overlay update + // Attach overlay update for each viewport (only once) + if (!element._overlayListener) { + viewport.element.addEventListener(IMAGE_RENDERED, () => updateOverlay(i, viewport)); + element._overlayListener = true; + } updateOverlay(i, viewport); } + + // 7. Apply mouse tool bindings applyMouseToolBindings(); }; - // Use requestAnimationFrame to ensure DOM is painted before setup - const raf = requestAnimationFrame(() => { - setup(); - }); - - return () => cancelAnimationFrame(raf); + // Use requestAnimationFrame to ensure DOM is painted before setup + const raf = requestAnimationFrame(() => { + setup(); + }); + // Cleanup: disconnect ResizeObservers + return () => { + viewportRefs.current.forEach(el => { + if (el && el._resizeObserver) { + el._resizeObserver.disconnect(); + delete el._resizeObserver; + } + }); + cancelAnimationFrame(raf); + }; }, [elementRef, updateOverlay, viewportGrid, loadedImageIds]); // Handler to reset the view (camera and window/level) const handleDoubleClick = useCallback((viewportIdx: numebr) => { - const renderingEngineId = `renderingEngine_${viewportIdx}`; const viewportId = `CT_${viewportIdx}`; - const renderingEngine = renderingEnginesRef.current[renderingEngineId]; + const renderingEngine = renderingEngineRef.current; const viewport = renderingEngine.getViewport(viewportId); if (viewport && typeof viewport.resetCamera === "function") { viewport.resetCamera(); @@ -396,10 +396,9 @@ function App() { // 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]; + const renderingEngine = renderingEngineRef.current; console.log("Rendering engine:", renderingEngine); if (renderingEngine) { const viewport = renderingEngine.getViewport(viewportId); @@ -458,9 +457,12 @@ function App() { }; }, []); - // Render the grid of viewports + const viewports = []; - for (let i = 0; i < viewportGrid.rows * viewportGrid.cols; i++) { + for (let i = 0; i < MAX_VIEWPORTS; i++) { + // Determine if this viewport should be visible in the current grid + const isVisible = i < viewportGrid.rows * viewportGrid.cols; + viewports.push(
handleDoubleClick(i)} > @@ -574,7 +581,6 @@ function App() { opacity: 0.9, }} onClick={() => { - console.log("Preset clicked for viewport", i, preset.name); handleApplyPreset(i, preset.center, preset.width); setPresetsOpenArr(prev => { const next = [...prev]; @@ -596,6 +602,7 @@ function App() { + return (
{/* Viewport grid menu at top center */} @@ -657,6 +664,7 @@ function App() { fontSize: "15px", }} onClick={() => { + saveVisibleViewportState(viewportGrid, renderingEngineRef.current); setViewportGrid({ rows: opt.rows, cols: opt.cols }) setViewportMenuOpen(false) }} @@ -870,4 +878,3 @@ function setupTools() { }) return toolGroup } -