diff --git a/dicom-viewer/package.json b/dicom-viewer/package.json index 4b7cfce..b83d8dc 100644 --- a/dicom-viewer/package.json +++ b/dicom-viewer/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "dev": "vite", - "build": "tsc && vite build", + "build": "tsc && vite build && cp dist/index.js /home/ross/rad/rad/rad/static/dv3d/index.js", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "preview": "vite preview" }, diff --git a/dicom-viewer/src/App.tsx b/dicom-viewer/src/App.tsx index 5616d17..5826c20 100644 --- a/dicom-viewer/src/App.tsx +++ b/dicom-viewer/src/App.tsx @@ -1,5 +1,4 @@ import { useEffect, useRef, useCallback, useState } from "react" -import cornerstoneDICOMImageLoader from "@cornerstonejs/dicom-image-loader" import { RenderingEngine, Enums, @@ -15,6 +14,7 @@ import { readDicomElementExplicit } from "dicom-parser" import { volumeLoader } from '@cornerstonejs/core'; import { metaData } from '@cornerstonejs/core'; import * as dicomParser from "dicom-parser"; +import cornerstoneDICOMImageLoader from "@cornerstonejs/dicom-image-loader" import { NoLabelArrowAnnotateTool } from "./NoLabelArrowAnnotateTool"; import { render } from "@cornerstonejs/tools/tools/displayTools/Labelmap/labelmapDisplay" @@ -176,47 +176,48 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer () => Array(MAX_VIEWPORTS).fill(0) ); + const [stackOrderModified, setStackOrderModified] = useState(false); const [loadingState, setLoadingState] = useState(null); // Load annotations and viewer state from props if provided -useEffect(() => { - let didCancel = false; - // Wait until availableStacks and viewportImageIds are loaded (i.e., app is ready) - if (annotationJson || viewerState) { + useEffect(() => { + let didCancel = false; + // Wait until availableStacks and viewportImageIds are loaded (i.e., app is ready) + if (annotationJson || viewerState) { setLoadingState(annotationJson ? "annotations" : "viewerState"); - // Use a short delay to ensure Cornerstone is initialized and viewports are ready - const timeout = setTimeout(() => { - if (didCancel) return; - const apiKey = container_id || "default"; - if (annotationJson) { - try { - const fn = window[`importAnnotations_${apiKey}`] || window.importAnnotations; - if (typeof fn === "function") { - fn(annotationJson); + // Use a short delay to ensure Cornerstone is initialized and viewports are ready + const timeout = setTimeout(() => { + if (didCancel) return; + const apiKey = container_id || "default"; + if (annotationJson) { + try { + const fn = window[`importAnnotations_${apiKey}`] || window.importAnnotations; + if (typeof fn === "function") { + fn(annotationJson); + } + } catch (e) { + console.error("Failed to import annotations from prop:", e); } - } catch (e) { - console.error("Failed to import annotations from prop:", e); } - } - if (viewerState) { - try { - const fn = window[`importViewerState_${apiKey}`] || window.importViewerState; - if (typeof fn === "function") { - fn(viewerState); + if (viewerState) { + try { + const fn = window[`importViewerState_${apiKey}`] || window.importViewerState; + if (typeof fn === "function") { + fn(viewerState); + } + } catch (e) { + console.error("Failed to import viewer state from prop:", e); } - } catch (e) { - console.error("Failed to import viewer state from prop:", e); } - } setLoadingState(null); - }, 800); // 500ms delay, adjust as needed + }, 2000); // 500ms delay, adjust as needed - return () => { - didCancel = true; - clearTimeout(timeout); - setLoadingState(null); - }; - } -}, [annotationJson, viewerState, container_id]); + return () => { + didCancel = true; + clearTimeout(timeout); + setLoadingState(null); + }; + } + }, [annotationJson, viewerState, container_id]); // Load available stacks on mount useEffect(() => { @@ -318,6 +319,7 @@ useEffect(() => { } return sortedIds; }); + setStackOrderModified(true); return next; }); }; @@ -371,7 +373,7 @@ useEffect(() => { const prevViewportProps = useRef>({}); const prevScrollIndices = useRef>({}); - const [orderBySliceLocation, setOrderBySliceLocation] = useState(true); + const [orderBySliceLocation, setOrderBySliceLocation] = useState(false); const [altEnablesReferenceCursors, setAltEnablesReferenceCursors] = useState(true); @@ -420,7 +422,7 @@ useEffect(() => { const [ctrlMouseToolBindings, setCtrlMouseToolBindings] = useState({ Primary: LengthTool.toolName, Auxiliary: ProbeTool.toolName, - Secondary: ArrowAnnotateTool.toolName, + Secondary: NoLabelArrowAnnotateTool.toolName, }) // Track if Ctrl is pressed @@ -780,9 +782,8 @@ useEffect(() => { running.current = true; await csRenderInit(); await csToolsInit( - { showSVGCursors: true, } ); - dicomImageLoaderInit({ maxWebWorkers: 1 }); + dicomImageLoaderInit(); } @@ -1077,6 +1078,25 @@ useEffect(() => { return () => document.removeEventListener("mousedown", handleClick); }, [presetsOpen]); +useEffect(() => { + const preventExtraButtonDefault = (e: MouseEvent) => { + // 4th button: button === 3, 5th button: button === 4 + if (e.button === 3 || e.button === 4) { + e.preventDefault(); + //e.stopPropagation(); + return false; + } + }; + window.addEventListener("mousedown", preventExtraButtonDefault, true); + window.addEventListener("mouseup", preventExtraButtonDefault, true); + window.addEventListener("auxclick", preventExtraButtonDefault, true); + return () => { + window.removeEventListener("mousedown", preventExtraButtonDefault, true); + window.removeEventListener("mouseup", preventExtraButtonDefault, true); + window.removeEventListener("auxclick", preventExtraButtonDefault, true); + }; +}, []); + useEffect(() => { const handleContextMenu = (e: MouseEvent) => { e.preventDefault(); @@ -1144,45 +1164,47 @@ useEffect(() => { }; }, [setAvailableStacks, setViewportImageIds, setSelectedStackIdx, setLoadedImageIdsAndCacheMeta]); - useEffect(() => { - if (!orderBySliceLocation) return; +useEffect(() => { + if (!orderBySliceLocation) return; - // Only sort if all metadata is loaded for all viewports - const allLoaded = viewportImageIds.every(imageIds => - !imageIds || imageIds.length === 0 || - imageIds.every(id => !!metaData.get("imagePlaneModule", id)) - ); - if (!allLoaded) return; + // Only sort if all metadata is loaded for all viewports + const allLoaded = viewportImageIds.every(imageIds => + !imageIds || imageIds.length === 0 || + imageIds.every(id => !!metaData.get("imagePlaneModule", id)) + ); + if (!allLoaded) return; - setViewportImageIds(prev => { - const next = prev.map(imageIds => { - if (!imageIds || imageIds.length === 0) return imageIds; + setViewportImageIds(prev => { + let modified = false; + const next = prev.map(imageIds => { + if (!imageIds || imageIds.length === 0) return imageIds; - const withSliceLoc = imageIds.map(id => ({ - imageId: id, - sliceLoc: (() => { - const meta = metaData.get('imagePlaneModule', id); - return meta && meta.sliceLocation !== undefined ? Number(meta.sliceLocation) : null; - })(), - })); + const withSliceLoc = imageIds.map(id => ({ + imageId: id, + sliceLoc: (() => { + const meta = metaData.get('imagePlaneModule', id); + return meta && meta.sliceLocation !== undefined ? Number(meta.sliceLocation) : null; + })(), + })); - const sorted = [...withSliceLoc].sort((a, b) => { - if (a.sliceLoc !== null && b.sliceLoc !== null) { - return a.sliceLoc - b.sliceLoc; - } - return a.imageId.localeCompare(b.imageId); - }); - - const sortedIds = sorted.map(obj => obj.imageId); - if (JSON.stringify(sortedIds) === JSON.stringify(imageIds)) { - return imageIds; + const sorted = [...withSliceLoc].sort((a, b) => { + if (a.sliceLoc !== null && b.sliceLoc !== null) { + return a.sliceLoc - b.sliceLoc; } - return sortedIds; + return a.imageId.localeCompare(b.imageId); }); - return next; - }); - }, [orderBySliceLocation, availableStacks]); + const sortedIds = sorted.map(obj => obj.imageId); + if (JSON.stringify(sortedIds) !== JSON.stringify(imageIds)) { + modified = true; + return sortedIds; + } + return imageIds; + }); + if (modified) setStackOrderModified(true); + return next; + }); +}, [orderBySliceLocation, availableStacks]); function updateActiveReferenceLineViewport(mainToolGroup: cornerstoneTools.Types.IToolGroup) { // Ensure the main tool group is defined (it won't be during setup) @@ -1197,26 +1219,106 @@ useEffect(() => { mainToolGroup.setToolDisabled(ReferenceLinesTool.toolName) } } + + + // Inside App component + function resetViewer() { + const renderingEngine = renderingEngineRef.current; + if (renderingEngine) { + for (let i = 0; i < MAX_VIEWPORTS; i++) { + const viewport = renderingEngine.getViewport(`CT_${i}`); + if (viewport) { + viewport.resetCamera?.(); + viewport.resetProperties?.(); + viewport.render?.(); + } + } + } + if (cornerstoneTools?.annotation?.state?.removeAllAnnotations) { + cornerstoneTools.annotation.state.removeAllAnnotations(); + } + } + + function truncateStack(lower: number, upper: number) { + // Only allow when 1 viewport and 1 stack is loaded + console.log("Truncating stack from", lower, "to", upper); + console.log(stackOrderModified) + if ( + viewportGrid.rows !== 1 || + viewportGrid.cols !== 1 || + availableStacks.length !== 1 || + !viewportImageIds[0] || + viewportImageIds[0].length === 0 || + stackOrderModified + ) { + alert("Truncation is only allowed when a single stack is loaded in a single viewport (and the order has not been modified in the viewer)."); + return false; + } + + const stack = availableStacks[0]; + const imageIds = stack.imageIds; + + // Validate bounds + if ( + lower < 0 || + upper >= imageIds.length || + lower >= upper + ) { + alert("Invalid truncation bounds."); + return false; + } + + // Truncate the stack and update state + const truncatedImageIds = imageIds.slice(lower, upper + 1); + setAvailableStacks([{ ...stack, imageIds: truncatedImageIds }]); + setViewportImageIds([truncatedImageIds]); + setSelectedStackIdx([0]); + // Optionally reset scroll position, overlays, etc. + + // Optionally, force a re-render or reset the viewport + const renderingEngine = renderingEngineRef.current; + if (renderingEngine) { + const viewport = renderingEngine.getViewport("CT_0"); + if (viewport && typeof viewport.setStack === "function") { + viewport.setStack(truncatedImageIds); + viewport.setImageIdIndex?.(0); + viewport.render?.(); + } + } + + return true; + } + function getCurrentStackPosition(viewportIdx: number = 0): number | null { + const renderingEngine = renderingEngineRef.current; + if (!renderingEngine) return null; + const viewport = renderingEngine.getViewport(`CT_${viewportIdx}`); + if (!viewport || typeof viewport.getCurrentImageIdIndex !== "function") return null; + return viewport.getCurrentImageIdIndex(); + } + useEffect(() => { const apiKey = container_id || "default"; - function jumpToSliceByImageId(viewportIdx: number, imageId: string) { - const renderingEngine = renderingEngineRef.current; - if (!renderingEngine) return; - const viewport = renderingEngine.getViewport(`CT_${viewportIdx}`); - if (!viewport || typeof viewport.getImageIds !== "function") return; - const imageIds = viewport.getImageIds(); - // Some stacks may have imageIds with or without "wadouri:" prefix, so normalize for comparison - const normImageIds = imageIds.map(id => id.replace(/^wadouri:/, "")); - const normTarget = imageId.replace(/^wadouri:/, ""); - const idx = normImageIds.indexOf(normTarget); - if (idx !== -1 && typeof viewport.setImageIdIndex === "function") { - csUtilities.jumpToSlice(viewport.element, { imageIndex: idx }); - updateOverlay(viewportIdx, viewport); + function jumpToSliceByImageId(viewportIdx: number, imageId: string) { + const renderingEngine = renderingEngineRef.current; + if (!renderingEngine) return; + const viewport = renderingEngine.getViewport(`CT_${viewportIdx}`); + if (!viewport || typeof viewport.getImageIds !== "function") return; + const imageIds = viewport.getImageIds(); + // Some stacks may have imageIds with or without "wadouri:" prefix, so normalize for comparison + const normImageIds = imageIds.map(id => id.replace(/^wadouri:/, "")); + const normTarget = imageId.replace(/^wadouri:/, ""); + const idx = normImageIds.indexOf(normTarget); + if (idx !== -1 && typeof viewport.setImageIdIndex === "function") { + csUtilities.jumpToSlice(viewport.element, { imageIndex: idx }); + updateOverlay(viewportIdx, viewport); + } } - } // Attach to window with a unique name window[`jumpToSliceByImageId_${apiKey}`] = jumpToSliceByImageId; + window[`resetViewer_${apiKey}`] = resetViewer; + window[`truncateStack_${apiKey}`] = truncateStack; + window[`getCurrentStackPosition_${apiKey}`] = getCurrentStackPosition; // Export the current viewer state as JSON @@ -1564,6 +1666,9 @@ useEffect(() => { window[`importLegacyAnnotations_${apiKey}`] = undefined; window[`importLegacyViewport_${apiKey}`] = undefined; window[`jumpToSliceByImageId_${apiKey}`] = undefined; + window[`resetViewer_${apiKey}`] = undefined; + window[`truncateStack_${apiKey}`] = undefined; + window[`getCurrentStackPosition_${apiKey}`] = undefined; }; }, [ viewportGrid, @@ -1778,7 +1883,6 @@ useEffect(() => { )} - // Annotations buttons {viewportModes[i] === "stack" && (() => { // Get all annotations for this stack const imageIds = viewportImageIds[i]; @@ -1959,7 +2063,6 @@ useEffect(() => { ); })()} -// ...existing code... {/* Overlay info (bottom left of viewport) */}
{
)} - {isVisible && availableStacks.length > 1 && ( + {isVisible && (availableStacks.length > 1 || !viewportImageIds[i] || viewportImageIds[i].length === 0) && (
({ }, }, base: command === "build" ? "/static/dv3d/" : "/", + build: { + rollupOptions: { + output: { + entryFileNames: `index.js`, + chunkFileNames: `[name].js`, + assetFileNames: `[name][extname]`, + }, + }, + outDir: "dist", + emptyOutDir: true, + }, })) \ No newline at end of file