This commit is contained in:
Ross
2025-06-11 19:32:18 +01:00
parent 21dca7d4b5
commit 11d1dc4339
3 changed files with 201 additions and 87 deletions
+1 -1
View File
@@ -5,7 +5,7 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "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", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview" "preview": "vite preview"
}, },
+189 -86
View File
@@ -1,5 +1,4 @@
import { useEffect, useRef, useCallback, useState } from "react" import { useEffect, useRef, useCallback, useState } from "react"
import cornerstoneDICOMImageLoader from "@cornerstonejs/dicom-image-loader"
import { import {
RenderingEngine, RenderingEngine,
Enums, Enums,
@@ -15,6 +14,7 @@ import { readDicomElementExplicit } from "dicom-parser"
import { volumeLoader } from '@cornerstonejs/core'; import { volumeLoader } from '@cornerstonejs/core';
import { metaData } from '@cornerstonejs/core'; import { metaData } from '@cornerstonejs/core';
import * as dicomParser from "dicom-parser"; import * as dicomParser from "dicom-parser";
import cornerstoneDICOMImageLoader from "@cornerstonejs/dicom-image-loader"
import { NoLabelArrowAnnotateTool } from "./NoLabelArrowAnnotateTool"; import { NoLabelArrowAnnotateTool } from "./NoLabelArrowAnnotateTool";
import { render } from "@cornerstonejs/tools/tools/displayTools/Labelmap/labelmapDisplay" 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) () => Array(MAX_VIEWPORTS).fill(0)
); );
const [stackOrderModified, setStackOrderModified] = useState(false);
const [loadingState, setLoadingState] = useState<null | "annotations" | "viewerState">(null); const [loadingState, setLoadingState] = useState<null | "annotations" | "viewerState">(null);
// Load annotations and viewer state from props if provided // Load annotations and viewer state from props if provided
useEffect(() => { useEffect(() => {
let didCancel = false; let didCancel = false;
// Wait until availableStacks and viewportImageIds are loaded (i.e., app is ready) // Wait until availableStacks and viewportImageIds are loaded (i.e., app is ready)
if (annotationJson || viewerState) { if (annotationJson || viewerState) {
setLoadingState(annotationJson ? "annotations" : "viewerState"); setLoadingState(annotationJson ? "annotations" : "viewerState");
// Use a short delay to ensure Cornerstone is initialized and viewports are ready // Use a short delay to ensure Cornerstone is initialized and viewports are ready
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
if (didCancel) return; if (didCancel) return;
const apiKey = container_id || "default"; const apiKey = container_id || "default";
if (annotationJson) { if (annotationJson) {
try { try {
const fn = window[`importAnnotations_${apiKey}`] || window.importAnnotations; const fn = window[`importAnnotations_${apiKey}`] || window.importAnnotations;
if (typeof fn === "function") { if (typeof fn === "function") {
fn(annotationJson); 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) {
if (viewerState) { try {
try { const fn = window[`importViewerState_${apiKey}`] || window.importViewerState;
const fn = window[`importViewerState_${apiKey}`] || window.importViewerState; if (typeof fn === "function") {
if (typeof fn === "function") { fn(viewerState);
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); setLoadingState(null);
}, 800); // 500ms delay, adjust as needed }, 2000); // 500ms delay, adjust as needed
return () => { return () => {
didCancel = true; didCancel = true;
clearTimeout(timeout); clearTimeout(timeout);
setLoadingState(null); setLoadingState(null);
}; };
} }
}, [annotationJson, viewerState, container_id]); }, [annotationJson, viewerState, container_id]);
// Load available stacks on mount // Load available stacks on mount
useEffect(() => { useEffect(() => {
@@ -318,6 +319,7 @@ useEffect(() => {
} }
return sortedIds; return sortedIds;
}); });
setStackOrderModified(true);
return next; return next;
}); });
}; };
@@ -371,7 +373,7 @@ useEffect(() => {
const prevViewportProps = useRef<Record<number, any>>({}); const prevViewportProps = useRef<Record<number, any>>({});
const prevScrollIndices = useRef<Record<number, number>>({}); const prevScrollIndices = useRef<Record<number, number>>({});
const [orderBySliceLocation, setOrderBySliceLocation] = useState(true); const [orderBySliceLocation, setOrderBySliceLocation] = useState(false);
const [altEnablesReferenceCursors, setAltEnablesReferenceCursors] = useState(true); const [altEnablesReferenceCursors, setAltEnablesReferenceCursors] = useState(true);
@@ -420,7 +422,7 @@ useEffect(() => {
const [ctrlMouseToolBindings, setCtrlMouseToolBindings] = useState({ const [ctrlMouseToolBindings, setCtrlMouseToolBindings] = useState({
Primary: LengthTool.toolName, Primary: LengthTool.toolName,
Auxiliary: ProbeTool.toolName, Auxiliary: ProbeTool.toolName,
Secondary: ArrowAnnotateTool.toolName, Secondary: NoLabelArrowAnnotateTool.toolName,
}) })
// Track if Ctrl is pressed // Track if Ctrl is pressed
@@ -780,9 +782,8 @@ useEffect(() => {
running.current = true; running.current = true;
await csRenderInit(); await csRenderInit();
await csToolsInit( await csToolsInit(
{ showSVGCursors: true, }
); );
dicomImageLoaderInit({ maxWebWorkers: 1 }); dicomImageLoaderInit();
} }
@@ -1077,6 +1078,25 @@ useEffect(() => {
return () => document.removeEventListener("mousedown", handleClick); return () => document.removeEventListener("mousedown", handleClick);
}, [presetsOpen]); }, [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(() => { useEffect(() => {
const handleContextMenu = (e: MouseEvent) => { const handleContextMenu = (e: MouseEvent) => {
e.preventDefault(); e.preventDefault();
@@ -1144,45 +1164,47 @@ useEffect(() => {
}; };
}, [setAvailableStacks, setViewportImageIds, setSelectedStackIdx, setLoadedImageIdsAndCacheMeta]); }, [setAvailableStacks, setViewportImageIds, setSelectedStackIdx, setLoadedImageIdsAndCacheMeta]);
useEffect(() => { useEffect(() => {
if (!orderBySliceLocation) return; if (!orderBySliceLocation) return;
// Only sort if all metadata is loaded for all viewports // Only sort if all metadata is loaded for all viewports
const allLoaded = viewportImageIds.every(imageIds => const allLoaded = viewportImageIds.every(imageIds =>
!imageIds || imageIds.length === 0 || !imageIds || imageIds.length === 0 ||
imageIds.every(id => !!metaData.get("imagePlaneModule", id)) imageIds.every(id => !!metaData.get("imagePlaneModule", id))
); );
if (!allLoaded) return; if (!allLoaded) return;
setViewportImageIds(prev => { setViewportImageIds(prev => {
const next = prev.map(imageIds => { let modified = false;
if (!imageIds || imageIds.length === 0) return imageIds; const next = prev.map(imageIds => {
if (!imageIds || imageIds.length === 0) return imageIds;
const withSliceLoc = imageIds.map(id => ({ const withSliceLoc = imageIds.map(id => ({
imageId: id, imageId: id,
sliceLoc: (() => { sliceLoc: (() => {
const meta = metaData.get('imagePlaneModule', id); const meta = metaData.get('imagePlaneModule', id);
return meta && meta.sliceLocation !== undefined ? Number(meta.sliceLocation) : null; return meta && meta.sliceLocation !== undefined ? Number(meta.sliceLocation) : null;
})(), })(),
})); }));
const sorted = [...withSliceLoc].sort((a, b) => { const sorted = [...withSliceLoc].sort((a, b) => {
if (a.sliceLoc !== null && b.sliceLoc !== null) { if (a.sliceLoc !== null && b.sliceLoc !== null) {
return a.sliceLoc - b.sliceLoc; 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;
} }
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) { function updateActiveReferenceLineViewport(mainToolGroup: cornerstoneTools.Types.IToolGroup) {
// Ensure the main tool group is defined (it won't be during setup) // Ensure the main tool group is defined (it won't be during setup)
@@ -1197,26 +1219,106 @@ useEffect(() => {
mainToolGroup.setToolDisabled(ReferenceLinesTool.toolName) 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(() => { useEffect(() => {
const apiKey = container_id || "default"; const apiKey = container_id || "default";
function jumpToSliceByImageId(viewportIdx: number, imageId: string) { function jumpToSliceByImageId(viewportIdx: number, imageId: string) {
const renderingEngine = renderingEngineRef.current; const renderingEngine = renderingEngineRef.current;
if (!renderingEngine) return; if (!renderingEngine) return;
const viewport = renderingEngine.getViewport(`CT_${viewportIdx}`); const viewport = renderingEngine.getViewport(`CT_${viewportIdx}`);
if (!viewport || typeof viewport.getImageIds !== "function") return; if (!viewport || typeof viewport.getImageIds !== "function") return;
const imageIds = viewport.getImageIds(); const imageIds = viewport.getImageIds();
// Some stacks may have imageIds with or without "wadouri:" prefix, so normalize for comparison // Some stacks may have imageIds with or without "wadouri:" prefix, so normalize for comparison
const normImageIds = imageIds.map(id => id.replace(/^wadouri:/, "")); const normImageIds = imageIds.map(id => id.replace(/^wadouri:/, ""));
const normTarget = imageId.replace(/^wadouri:/, ""); const normTarget = imageId.replace(/^wadouri:/, "");
const idx = normImageIds.indexOf(normTarget); const idx = normImageIds.indexOf(normTarget);
if (idx !== -1 && typeof viewport.setImageIdIndex === "function") { if (idx !== -1 && typeof viewport.setImageIdIndex === "function") {
csUtilities.jumpToSlice(viewport.element, { imageIndex: idx }); csUtilities.jumpToSlice(viewport.element, { imageIndex: idx });
updateOverlay(viewportIdx, viewport); updateOverlay(viewportIdx, viewport);
}
} }
}
// Attach to window with a unique name // Attach to window with a unique name
window[`jumpToSliceByImageId_${apiKey}`] = jumpToSliceByImageId; window[`jumpToSliceByImageId_${apiKey}`] = jumpToSliceByImageId;
window[`resetViewer_${apiKey}`] = resetViewer;
window[`truncateStack_${apiKey}`] = truncateStack;
window[`getCurrentStackPosition_${apiKey}`] = getCurrentStackPosition;
// Export the current viewer state as JSON // Export the current viewer state as JSON
@@ -1564,6 +1666,9 @@ useEffect(() => {
window[`importLegacyAnnotations_${apiKey}`] = undefined; window[`importLegacyAnnotations_${apiKey}`] = undefined;
window[`importLegacyViewport_${apiKey}`] = undefined; window[`importLegacyViewport_${apiKey}`] = undefined;
window[`jumpToSliceByImageId_${apiKey}`] = undefined; window[`jumpToSliceByImageId_${apiKey}`] = undefined;
window[`resetViewer_${apiKey}`] = undefined;
window[`truncateStack_${apiKey}`] = undefined;
window[`getCurrentStackPosition_${apiKey}`] = undefined;
}; };
}, [ }, [
viewportGrid, viewportGrid,
@@ -1778,7 +1883,6 @@ useEffect(() => {
</span> </span>
)} )}
</button> </button>
// Annotations buttons
{viewportModes[i] === "stack" && (() => { {viewportModes[i] === "stack" && (() => {
// Get all annotations for this stack // Get all annotations for this stack
const imageIds = viewportImageIds[i]; const imageIds = viewportImageIds[i];
@@ -1959,7 +2063,6 @@ useEffect(() => {
</div> </div>
); );
})()} })()}
// ...existing code...
{/* Overlay info (bottom left of viewport) */} {/* Overlay info (bottom left of viewport) */}
<div <div
style={{ style={{
@@ -2179,7 +2282,7 @@ useEffect(() => {
</div> </div>
</div> </div>
)} )}
{isVisible && availableStacks.length > 1 && ( {isVisible && (availableStacks.length > 1 || !viewportImageIds[i] || viewportImageIds[i].length === 0) && (
<div <div
style={{ style={{
position: "absolute", position: "absolute",
+11
View File
@@ -45,4 +45,15 @@ export default defineConfig(({ command }) => ({
}, },
}, },
base: command === "build" ? "/static/dv3d/" : "/", base: command === "build" ? "/static/dv3d/" : "/",
build: {
rollupOptions: {
output: {
entryFileNames: `index.js`,
chunkFileNames: `[name].js`,
assetFileNames: `[name][extname]`,
},
},
outDir: "dist",
emptyOutDir: true,
},
})) }))