basic reference line functions

This commit is contained in:
Ross
2025-05-25 21:13:45 +01:00
parent a803e351c3
commit 81edcea5ea
+255 -86
View File
@@ -60,6 +60,7 @@ const MOUSE_BUTTONS = [
// App definintion
function App() {
const elementRef = useRef<HTMLDivElement>(null)
const viewportRef = useRef<any>(null) // Store the viewport instance
@@ -82,28 +83,106 @@ function App() {
const renderingEngineId = "mainRenderingEngine";
const renderingEngineRef = useRef<any>(null);
// Store imageIds globally after loading so they can be reused
const [loadedImageIds, setLoadedImageIds] = useState<string[] | null>(null);
const [availableStacks, setAvailableStacks] = useState<string[][]>([]);
const [activeViewport, setActiveViewport] = useState<number | null>(0);
const [viewportImageIds, setViewportImageIds] = useState<string[][]>(
() => Array(MAX_VIEWPORTS).fill([])
);
const [selectedStackIdx, setSelectedStackIdx] = useState<number[]>(
() => Array(MAX_VIEWPORTS).fill(0)
);
// Load available stacks on mount
useEffect(() => {
availableImageIds().then(stacks => {
setAvailableStacks(stacks);
// Optionally initialize each viewport with the first stack
setLoadedImageIdsAndCacheMeta(stacks[0]);
setViewportImageIds(Array(MAX_VIEWPORTS).fill(stacks[0] || []));
});
}, []);
const handleLoadStack = async (viewportIdx: number, stackIdx: number) => {
const stack = availableStacks[stackIdx];
if (!stack) return;
setViewportImageIds(prev => {
const next = [...prev];
next[viewportIdx] = stack;
return next;
});
setSelectedStackIdx(prev => {
const next = [...prev];
next[viewportIdx] = stackIdx;
return next;
});
// Cache imageIds and load metadata for this stack
await setLoadedImageIdsAndCacheMeta(stack);
};
const [crossReferenceEnabled, setCrossReferenceEnabled] = useState(true);
const [referenceLinesEnabled, setReferenceLinesEnabled] = useState(false);
const [referenceLinesEnabled, setReferenceLinesEnabled] = useState(true);
const sortViewportImageIdsBySliceLocation = () => {
setViewportImageIds(prev => {
const next = prev.map((imageIds, idx) => {
if (!imageIds || imageIds.length === 0) return imageIds;
const withSortKey = imageIds.map(id => {
const plane = metaData.get('imagePlaneModule', id);
let sortKey = null;
if (plane) {
if (plane.sliceLocation !== undefined) {
sortKey = Number(plane.sliceLocation);
} else if (
plane.imagePositionPatient &&
Array.isArray(plane.imagePositionPatient) &&
plane.imagePositionPatient.length === 3
) {
sortKey = Number(plane.imagePositionPatient[2]);
}
}
return { imageId: id, sortKey };
});
const sorted = [...withSortKey].sort((a, b) => {
if (a.sortKey !== null && b.sortKey !== null) {
return a.sortKey - b.sortKey;
}
return a.imageId.localeCompare(b.imageId);
});
const sortedIds = sorted.map(obj => obj.imageId);
// Log the sorted result for this viewport
console.log(`Viewport ${idx} sorted imageIds:`, sortedIds);
if (JSON.stringify(sortedIds) === JSON.stringify(imageIds)) {
return imageIds;
}
return sortedIds;
});
return next;
});
};
// Wrap setLoadedImageIds to also cache DICOM metadata
const setLoadedImageIdsAndCacheMeta = async (imageIds: string[]) => {
console.log("Caching loaded image IDs:", imageIds);
const loadPromises: Promise<any>[] = [];
for (const imageId of imageIds) {
// Only cache if it's a local file (wadouri:blob:) or a file URL
if (imageId.startsWith("wadouri:")) {
try {
// Extract the URL after "wadouri:"
const url = imageId.slice(8);
// Only cache if it's a blob or file URL
if (url.startsWith("blob:") || url.startsWith("/")) {
//const response = await fetch(url);
//const arrayBuffer = await response.arrayBuffer();
//const dataSet = dicomParser.parseDicom(new Uint8Array(arrayBuffer));
if (cornerstoneDICOMImageLoader.wadouri.dataSetCacheManager) {
cornerstoneDICOMImageLoader.wadouri.dataSetCacheManager.load(url);
// If load returns a promise, collect it
const result = cornerstoneDICOMImageLoader.wadouri.dataSetCacheManager.load(url);
if (result && typeof result.then === "function") {
loadPromises.push(result);
}
} else {
console.warn("MetaDataManager not found. Unable to cache metadata.");
}
@@ -113,7 +192,15 @@ function App() {
}
}
}
setLoadedImageIds(imageIds);
// Wait for all metadata loads to finish
await Promise.all(loadPromises);
console.log("Image IDs cached:", imageIds);
// Trigger sort if enabled
if (orderBySliceLocation) {
sortViewportImageIdsBySliceLocation();
}
};
@@ -196,12 +283,14 @@ function App() {
// Add this function to gather and format metadata:
const handleShowMetadata = () => {
if (!loadedImageIds || loadedImageIds.length === 0) {
// Find the first visible viewport with images
const firstIdx = viewportImageIds.findIndex(ids => ids && ids.length > 0);
if (firstIdx === -1) {
setMetaModalContent("No images loaded.");
setMetaModalOpen(true);
return;
}
const imageId = loadedImageIds[0];
const imageId = viewportImageIds[firstIdx][0];
// Gather all available metadata modules for this imageId
const modules = [
@@ -378,6 +467,7 @@ function App() {
useEffect(() => {
const setup = async () => {
console.log("Setting up..");
// 1. Initialize Cornerstone3D and tools ONCE
if (!running.current) {
running.current = true;
@@ -388,12 +478,6 @@ function App() {
dicomImageLoaderInit({ maxWebWorkers: 1 });
}
// 2. Load image IDs if not already loaded
let imageIds = loadedImageIds;
if (!imageIds) {
imageIds = await createImageIds();
setLoadedImageIdsAndCacheMeta(imageIds);
}
// 3. Get or create the rendering engine
let renderingEngine = renderingEngineRef.current;
@@ -420,6 +504,14 @@ function App() {
if (!volumeToolGroup.hasTool(ReferenceLinesTool.toolName)) {
volumeToolGroup.addTool(ReferenceLinesTool.toolName);
}
// Set the active viewport as the source for reference lines
const sourceViewportId = `CT_${activeViewport}`;
stackToolGroup.setToolConfiguration(ReferenceLinesTool.toolName, {
sourceViewportId,
});
volumeToolGroup.setToolConfiguration(ReferenceLinesTool.toolName, {
sourceViewportId,
});
stackToolGroup.setToolEnabled(ReferenceLinesTool.toolName);
volumeToolGroup.setToolEnabled(ReferenceLinesTool.toolName);
} else {
@@ -433,6 +525,10 @@ function App() {
// 6. Enable and configure each viewport
for (let i = 0; i < MAX_VIEWPORTS; i++) {
let imageIds = viewportImageIds[i];
if (!imageIds || imageIds.length === 0) continue;
const viewportId = `CT_${i}`;
const element = viewportRefs.current[i];
if (!element) continue;
@@ -494,20 +590,20 @@ function App() {
// Restore scroll, properties, etc.
} else if (viewportModes[i] === 'volume') {
let sortedImageIds = imageIds;
if (imageIds && imageIds.length > 1) {
sortedImageIds = [...imageIds].sort((a, b) => {
const metaA = metaData.get('imagePlaneModule', a);
const metaB = metaData.get('imagePlaneModule', b);
const sliceA = metaA && metaA.sliceLocation !== undefined ? Number(metaA.sliceLocation) : 0;
const sliceB = metaB && metaB.sliceLocation !== undefined ? Number(metaB.sliceLocation) : 0;
return sliceA - sliceB;
});
}
//let sortedImageIds = imageIds;
//if (imageIds && imageIds.length > 1) {
// sortedImageIds = [...imageIds].sort((a, b) => {
// const metaA = metaData.get('imagePlaneModule', a);
// const metaB = metaData.get('imagePlaneModule', b);
// const sliceA = metaA && metaA.sliceLocation !== undefined ? Number(metaA.sliceLocation) : 0;
// const sliceB = metaB && metaB.sliceLocation !== undefined ? Number(metaB.sliceLocation) : 0;
// return sliceA - sliceB;
// });
//}
// Use a unique volumeId per viewport
const volumeId = `myVolumeId_${i}`;
const volume = await volumeLoader.createAndCacheVolume(volumeId, { imageIds: sortedImageIds });
const volume = await volumeLoader.createAndCacheVolume(volumeId, { imageIds: imageIds });
volume.load();
viewport.setVolumes([{ volumeId }]);
viewport.render();
@@ -611,11 +707,11 @@ function App() {
});
cancelAnimationFrame(raf);
};
}, [elementRef, updateOverlay, viewportGrid, loadedImageIds, viewportModes]);
}, [elementRef, updateOverlay, viewportGrid, viewportModes, viewportImageIds]);
// Handler to reset the view (camera and window/level)
const handleDoubleClick = useCallback((viewportIdx: numebr) => {
const handleDoubleClick = useCallback((viewportIdx: number) => {
const viewportId = `CT_${viewportIdx}`;
const renderingEngine = renderingEngineRef.current;
const viewport = renderingEngine.getViewport(viewportId);
@@ -690,56 +786,58 @@ function App() {
};
}, []);
// Add this effect to re-order images by Slice Location after images are loaded and when the option changes:
useEffect(() => {
if (!loadedImageIds || loadedImageIds.length === 0) return;
if (!referenceLinesEnabled) return;
const stackToolGroup = ToolGroupManager.getToolGroup(STACK_TOOL_GROUP_ID);
const volumeToolGroup = ToolGroupManager.getToolGroup(VOLUME_TOOL_GROUP_ID);
const sourceViewportId = `CT_${activeViewport}`;
if (stackToolGroup?.hasTool(ReferenceLinesTool.toolName)) {
stackToolGroup.setToolConfiguration(ReferenceLinesTool.toolName, { sourceViewportId });
}
if (volumeToolGroup?.hasTool(ReferenceLinesTool.toolName)) {
volumeToolGroup.setToolConfiguration(ReferenceLinesTool.toolName, { sourceViewportId });
}
}, [activeViewport, referenceLinesEnabled]);
useEffect(() => {
if (!orderBySliceLocation) return;
let cancelled = false;
// 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;
const checkAndSort = () => {
// Check if all metadata is loaded
const allLoaded = loadedImageIds.every(
id => !!metaData.get("imagePlaneModule", id)
);
if (!allLoaded) {
// Try again soon
setTimeout(checkAndSort, 200);
return;
}
setViewportImageIds(prev => {
const next = prev.map(imageIds => {
if (!imageIds || imageIds.length === 0) return imageIds;
// All metadata loaded, sort by slice location
const getSliceLocation = (imageId: string) => {
const meta = metaData.get('imagePlaneModule', imageId);
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 withSliceLoc = loadedImageIds.map(id => ({
imageId: id,
sliceLoc: getSliceLocation(id),
}));
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 sorted = [...withSliceLoc].sort((a, b) => {
if (a.sliceLoc !== null && b.sliceLoc !== null) {
return a.sliceLoc - b.sliceLoc;
const sortedIds = sorted.map(obj => obj.imageId);
if (JSON.stringify(sortedIds) === JSON.stringify(imageIds)) {
return imageIds;
}
return a.imageId.localeCompare(b.imageId);
return sortedIds;
});
return next;
});
}, [orderBySliceLocation, availableStacks]);
const sortedIds = sorted.map(obj => obj.imageId);
const isSame =
loadedImageIds.length === sortedIds.length &&
loadedImageIds.every((id, idx) => id === sortedIds[idx]);
if (!isSame && !cancelled) {
setLoadedImageIds(sortedIds);
}
};
checkAndSort();
return () => { cancelled = true; };
}, [loadedImageIds, orderBySliceLocation]);
const viewports = [];
@@ -762,11 +860,17 @@ function App() {
flexDirection: "column",
gridRow: Math.floor(i / viewportGrid.cols) + 1,
gridColumn: (i % viewportGrid.cols) + 1,
height: "100%", // <-- Add this line
// Use visibility instead of display to hide but keep size/layout
height: "100%",
visibility: isVisible ? "visible" : "hidden",
pointerEvents: isVisible ? "auto" : "none",
// Remove outline, use only boxShadow for indicator
boxShadow: activeViewport === i
? "0 0 0 4px #1976d2, 0 0 16px 4px #1976d2cc"
: undefined,
cursor: "pointer",
zIndex: activeViewport === i ? 10 : 1,
}}
onClick={() => setActiveViewport(i)}
onDoubleClick={() => handleDoubleClick(i)}
>
<button
@@ -997,7 +1101,7 @@ function App() {
</div>
</div>
{/* Position bar inside each viewport */}
{viewportModes[i] === "stack" && loadedImageIds && (
{viewportModes[i] === "stack" && viewportImageIds[i] && (
<div
style={{
position: "absolute",
@@ -1008,7 +1112,7 @@ function App() {
maxHeight: "100%",
minHeight: 0,
zIndex: 30,
display: loadedImageIds.length > 0 ? "flex" : "none",
display: viewportImageIds[i].length > 0 ? "flex" : "none",
alignItems: "flex-start",
justifyContent: "center",
pointerEvents: "none",
@@ -1032,7 +1136,7 @@ function App() {
overflow: "hidden",
}}
>
{loadedImageIds.map((id, idx) => {
{viewportImageIds[i].map((id, idx) => {
const loaded = !!metaData.get("imagePlaneModule", id);
const renderingEngine = renderingEngineRef.current;
let currentIdx = -1;
@@ -1051,7 +1155,7 @@ function App() {
key={id}
style={{
width: "100%",
height: `${100 / loadedImageIds.length}%`,
height: `${100 / viewportImageIds[i].length}%`,
background: isCurrent
? "linear-gradient(to right, #ffeb3b 60%, #ff9800 100%)"
: loaded
@@ -1060,7 +1164,7 @@ function App() {
opacity: loaded ? 0.95 : 0.4,
transition: "background 0.3s, opacity 0.3s",
borderBottom:
idx < loadedImageIds.length - 1
idx < viewportImageIds[i].length - 1
? "1px solid #222"
: "none",
cursor: loaded ? "pointer" : "default",
@@ -1092,6 +1196,57 @@ function App() {
</div>
</div>
)}
<div
style={{
position: "absolute",
left: 8,
right: 8,
bottom: 8,
zIndex: 20,
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
gap: "8px",
background: "rgba(34,34,34,0.85)",
borderRadius: "4px",
padding: "2px 8px",
}}
>
<select
value={selectedStackIdx[i] || 0}
onChange={e => handleLoadStack(i, Number(e.target.value))}
style={{
background: "#222",
color: "#fff",
border: "1px solid #444",
borderRadius: "4px",
padding: "2px 8px",
fontSize: "13px",
marginRight: "6px",
}}
>
{availableStacks.map((stack, idx) => (
<option key={idx} value={idx}>
Stack {idx + 1} ({stack.length} images)
</option>
))}
</select>
<button
style={{
background: "#1976d2",
color: "#fff",
border: "none",
borderRadius: "3px",
padding: "2px 12px",
cursor: "pointer",
fontSize: "13px",
}}
onClick={() => handleLoadStack(i, selectedStackIdx[i] || 0)}
disabled={!availableStacks.length}
>
Load Stack
</button>
</div>
</div>
);
}
@@ -1100,11 +1255,16 @@ function App() {
return (
<div style={{
position: "relative", width: "99vw", height: "99vh", background: "#111",
margin: 0, padding: 0,
overflow: "hidden", // Add this line
}}>
<div style={{
position: "relative",
width: "99vw",
height: "99vh",
background: "#111",
padding: "12px", // <-- Add this line
boxSizing: "border-box", // <-- Ensure padding doesn't shrink content
background: "#222",
// overflow: "hidden",
}}>
<div>
{/* Side menu toggle button */}
<button
@@ -1264,6 +1424,12 @@ function App() {
>
{referenceLinesEnabled ? "Disable" : "Enable"} Reference Lines
</button>
<button
onClick={sortViewportImageIdsBySliceLocation}
style={{ margin: 8, padding: 8 }}
>
Sort All Viewports by Slice Location
</button>
{/* Add more menu items here if needed */}
</div>
@@ -1387,9 +1553,10 @@ function App() {
width: "100%",
height: "100%",
display: "grid",
margin: 3,
gridTemplateRows: `repeat(${viewportGrid.rows}, 1fr)`,
gridTemplateColumns: `repeat(${viewportGrid.cols}, 1fr)`,
gap: "2px",
gap: "5px", // Increase gap for better visibility
zIndex: 1,
}}
>
@@ -1620,6 +1787,7 @@ function setupTools() {
stackToolGroup.addTool(PlanarFreehandROITool.toolName, { calculateStats: false });
stackToolGroup.addTool(PlanarFreehandContourSegmentationTool.toolName, {});
stackToolGroup.addTool(SculptorTool.toolName, {});
stackToolGroup.addTool(ReferenceLinesTool.toolName);
// StackScrollTool mouse wheel binding
stackToolGroup.setToolActive(StackScrollTool.toolName, {
bindings: [{ mouseButton: MouseBindings.Wheel }],
@@ -1643,6 +1811,7 @@ function setupTools() {
volumeToolGroup.addTool(PlanarFreehandROITool.toolName, { calculateStats: false });
volumeToolGroup.addTool(PlanarFreehandContourSegmentationTool.toolName, {});
volumeToolGroup.addTool(SculptorTool.toolName, {});
volumeToolGroup.addTool(ReferenceLinesTool.toolName);
//volumeToolGroup.addTool(CrosshairsTool.toolName, {});
}