.
This commit is contained in:
@@ -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
@@ -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",
|
||||||
|
|||||||
@@ -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,
|
||||||
|
},
|
||||||
}))
|
}))
|
||||||
Reference in New Issue
Block a user