start ortho automode

This commit is contained in:
Ross
2026-06-08 13:50:42 +01:00
parent b961a806d9
commit d0f3431ef4
+309 -35
View File
@@ -1152,11 +1152,119 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat
}
};
const handleLoadStackOrthogonal = async (stackIdx: number) => {
const stackObj = availableStacks[stackIdx];
if (!stackObj) return;
setLoadingState("displayset");
try {
const hasDeclaredGroups = stackObj.groupingSource === "declared" && (stackObj.seriesGroups || []).length > 0;
const shouldExpandMultiframe = !(hasDeclaredGroups && (stackObj.imageIds || []).length > 1);
const stack = shouldExpandMultiframe
? await expandMultiframeImageIds(stackObj.imageIds || [])
: dedupeImageIds((stackObj.imageIds || []).map(normalizeToWadouriOrExternal));
await setLoadedImageIdsAndCacheMeta(stack, { force: true });
const seriesGroups = hasDeclaredGroups
? (stackObj.seriesGroups || [])
: await deriveSeriesGroupsFromImageIds(stack);
const primarySeries = seriesGroups[0]?.imageIds || stack;
// 1. Set grid layout to 1x3 (1 row, 3 columns)
setViewportGrid({ rows: 1, cols: 3 });
// 2. Set viewport modes: first 3 viewports as 'volume', others default to 'stack'
setViewportModes(prev => {
const next = [...prev];
next[0] = 'volume';
next[1] = 'volume';
next[2] = 'volume';
return next;
});
// 3. Set viewport image IDs for first 3 viewports
setViewportImageIds(prev => {
const next = [...prev];
next[0] = primarySeries;
next[1] = primarySeries;
next[2] = primarySeries;
return next;
});
// 4. Set selected stack index for first 3 viewports
setSelectedStackIdx(prev => {
const next = [...prev];
next[0] = stackIdx;
next[1] = stackIdx;
next[2] = stackIdx;
return next;
});
// 5. Set series groups for first 3 viewports
setSeriesGroupsByViewport(prev => {
const next = [...prev];
next[0] = seriesGroups || [];
next[1] = seriesGroups || [];
next[2] = seriesGroups || [];
return next;
});
// 6. Set selected series index for first 3 viewports
setSelectedSeriesIdxByViewport(prev => {
const next = [...prev];
next[0] = 0;
next[1] = 0;
next[2] = 0;
return next;
});
// 7. Enable crosshairs
setCrossHairsEnabled(true);
// 8. Cache imageIds and load metadata for this stack
await setLoadedImageIdsAndCacheMeta(primarySeries, { force: true });
// 9. After a short delay, set orientations to Axial, Sagittal, Coronal
setTimeout(() => {
const renderingEngine = renderingEngineRef.current;
if (renderingEngine) {
const orientations = ["axial", "sagittal", "coronal"];
for (let i = 0; i < 3; i++) {
const vp = renderingEngine.getViewport(`CT_${i}`);
if (vp && typeof vp.setOrientation === "function") {
try {
vp.setOrientation(orientations[i]);
vp.render();
} catch (e) {
appLogger.warn("Failed to set orthogonal orientation for viewport", i, e);
}
}
}
}
}, 500);
} catch (err) {
appLogger.error("Failed to load stack orthogonally:", err);
} finally {
setLoadingState(null);
}
};
const [crossHairsEnabled, setCrossHairsEnabled] = useState(false);
const [crosshairsProjectionType, setCrosshairsProjectionType] = useState<string>("None");
const [syncSlabThickness, setSyncSlabThickness] = useState<boolean>(true);
const [showCenterPoint, setShowCenterPoint] = useState<boolean>(true);
const slabThicknessSynchronizerRef = useRef<any>(null);
const setupEpochRef = useRef(0);
const [setupInProgress, setSetupInProgress] = useState(false);
const setupInProgressRef = useRef(setupInProgress);
useEffect(() => {
setupInProgressRef.current = setupInProgress;
}, [setupInProgress]);
const syncSlabThicknessRef = useRef(syncSlabThickness);
useEffect(() => {
syncSlabThicknessRef.current = syncSlabThickness;
}, [syncSlabThickness]);
const [referenceLinesEnabled, setReferenceLinesEnabled] = useState(true);
const [showAdvancedMouseBindings, setShowAdvancedMouseBindings] = useState(false);
@@ -2510,7 +2618,11 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat
useEffect(() => {
const mainToolGroup = ToolGroupManager.getToolGroup(MAIN_TOOL_GROUP_ID);
if (!mainToolGroup) return;
const STACK_TOOL_GROUP_ID = "TOOL_GROUP_stack_" + container_id;
const stackToolGroup = ToolGroupManager.getToolGroup(STACK_TOOL_GROUP_ID);
[mainToolGroup, stackToolGroup].forEach(toolGroup => {
if (!toolGroup) return;
// List all annotation tools you use
[
@@ -2523,12 +2635,13 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat
'Sculptor',
// Add more if needed
].forEach(toolName => {
mainToolGroup.setToolConfiguration(toolName, {
toolGroup.setToolConfiguration(toolName, {
color: '#00e676', // normal color
selectedColor: '#ffeb3b', // yellow for selected
});
});
}, [MAIN_TOOL_GROUP_ID]);
});
}, [MAIN_TOOL_GROUP_ID, container_id]);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
@@ -2555,6 +2668,8 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat
useEffect(() => {
const mainToolGroup = ToolGroupManager.getToolGroup(MAIN_TOOL_GROUP_ID);
const STACK_TOOL_GROUP_ID = "TOOL_GROUP_stack_" + container_id;
const stackToolGroup = ToolGroupManager.getToolGroup(STACK_TOOL_GROUP_ID);
if (!mainToolGroup) return;
const modifierActive = shiftPressed || (altEnablesReferenceCursors && altPressed);
@@ -2572,15 +2687,21 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat
});
});
if (!mainToolGroup.hasTool(ReferenceCursors.toolName)) {
mainToolGroup.addTool(ReferenceCursors.toolName);
[mainToolGroup, stackToolGroup].forEach(toolGroup => {
if (!toolGroup) return;
if (!toolGroup.hasTool(ReferenceCursors.toolName)) {
toolGroup.addTool(ReferenceCursors.toolName);
}
mainToolGroup.setToolEnabled(ReferenceCursors.toolName);
mainToolGroup.setToolActive(ReferenceCursors.toolName);
toolGroup.setToolEnabled(ReferenceCursors.toolName);
toolGroup.setToolActive(ReferenceCursors.toolName);
});
} else {
if (mainToolGroup.hasTool(ReferenceCursors.toolName)) {
mainToolGroup.setToolDisabled(ReferenceCursors.toolName);
[mainToolGroup, stackToolGroup].forEach(toolGroup => {
if (!toolGroup) return;
if (toolGroup.hasTool(ReferenceCursors.toolName)) {
toolGroup.setToolDisabled(ReferenceCursors.toolName);
}
});
}
}, [
altPressed,
@@ -2591,14 +2712,17 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat
viewportImageIds,
viewportModes,
setLoadedImageIdsAndCacheMeta,
container_id,
]);
// Helper to apply all mouse tool bindings (normal or ctrl)
const applyMouseToolBindings = useCallback(() => {
const STACK_TOOL_GROUP_ID = "TOOL_GROUP_stack_" + container_id;
// Apply to both stack and volume tool groups
const mainToolGroup = cornerstoneTools.ToolGroupManager.getToolGroup(MAIN_TOOL_GROUP_ID);
const stackToolGroup = cornerstoneTools.ToolGroupManager.getToolGroup(STACK_TOOL_GROUP_ID);
[mainToolGroup].forEach(toolGroup => {
[mainToolGroup, stackToolGroup].forEach(toolGroup => {
if (toolGroup) {
// Set all tools to passive first, removing all bindings
TOOL_OPTIONS.forEach(opt => {
@@ -2699,7 +2823,7 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat
}
});
}, [mouseToolBindings, ctrlMouseToolBindings, windowLevelMouseSensitivity]);
}, [mouseToolBindings, ctrlMouseToolBindings, windowLevelMouseSensitivity, container_id]);
useEffect(() => {
applyMouseToolBindings();
@@ -3579,15 +3703,23 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat
}, [applyStackDrop, getDropZoneFromClientPoint, isPhoneLayout, mobileInteractionActive, resolveDropZone, viewportGrid]);
useEffect(() => {
const setup = async () => {
appLogger.debug("setup")
setupEpochRef.current++;
const currentEpoch = setupEpochRef.current;
setupInProgressRef.current = true;
setSetupInProgress(true);
const setup = async () => {
appLogger.debug("setup", currentEpoch);
if (currentEpoch !== setupEpochRef.current) return;
try {
// 1. Initialize Cornerstone3D and tools ONCE
if (!running.current) {
running.current = true;
await csRenderInit();
if (currentEpoch !== setupEpochRef.current) return;
await csToolsInit(
);
if (currentEpoch !== setupEpochRef.current) return;
dicomImageLoaderInit();
// Register client-side image loader for external images (JPEG/PNG/blob/data)
try {
@@ -3653,19 +3785,40 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat
// --- In your setup function, use the correct tool group per viewport ---
let mainToolGroup = ToolGroupManager.getToolGroup(MAIN_TOOL_GROUP_ID);
if (!mainToolGroup) {
mainToolGroup = setupTools(MAIN_TOOL_GROUP_ID)
mainToolGroup = setupTools(MAIN_TOOL_GROUP_ID);
}
const STACK_TOOL_GROUP_ID = "TOOL_GROUP_stack_" + container_id;
let stackToolGroup = ToolGroupManager.getToolGroup(STACK_TOOL_GROUP_ID);
if (!stackToolGroup) {
stackToolGroup = ToolGroupManager.createToolGroup(STACK_TOOL_GROUP_ID);
stackToolGroup.addTool(WindowLevelTool.toolName);
stackToolGroup.addTool(PanTool.toolName);
stackToolGroup.addTool(ZoomTool.toolName);
stackToolGroup.addTool(StackScrollTool.toolName, { loop: false });
stackToolGroup.addTool(PlanarRotateTool.toolName);
stackToolGroup.addTool(LengthTool.toolName);
stackToolGroup.addTool(NoLabelArrowAnnotateTool.toolName, { getTextCallback: () => '' });
stackToolGroup.addTool(ArrowAnnotateTool.toolName);
stackToolGroup.addTool(ProbeTool.toolName, { statsCalculator: () => null });
stackToolGroup.addTool(RectangleROITool.toolName, { calculateStats: false, getTextCallback: () => '' });
stackToolGroup.addTool(EllipticalROITool.toolName, { getTextCallback: () => '' });
stackToolGroup.addTool(PlanarFreehandROITool.toolName, { calculateStats: false });
stackToolGroup.addTool(SculptorTool.toolName, {});
stackToolGroup.addTool(ReferenceLinesTool.toolName);
stackToolGroup.addTool(ReferenceCursors.toolName);
}
const visibleVolumeViewports = viewportModes
.slice(0, viewportGrid.rows * viewportGrid.cols)
.filter(mode => mode === 'volume').length;
// --- In your setup function, after tool group creation, enable/disable ReferenceLinesTool for each tool group ---
updateActiveReferenceLineViewport(mainToolGroup)
updateActiveReferenceLineViewport(mainToolGroup, stackToolGroup);
// 6. Enable and configure each viewport
for (let i = 0; i < MAX_VIEWPORTS; i++) {
if (currentEpoch !== setupEpochRef.current) return;
let imageIds = viewportImageIds[i];
if (!imageIds || imageIds.length === 0) continue;
@@ -3703,8 +3856,44 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat
const viewport = renderingEngine.getViewport(viewportId);
if (!viewport) continue;
// Patch setSlabThickness to support robust synchronization
if (viewportModes[i] === 'volume' && typeof (viewport as any).setSlabThickness === 'function' && !(viewport as any)._setSlabThicknessPatched) {
const originalSetSlabThickness = (viewport as any).setSlabThickness;
(viewport as any).setSlabThickness = function (slabThickness: any, actorUIDs: any) {
originalSetSlabThickness.call(this, slabThickness, actorUIDs);
if (syncSlabThicknessRef.current && !setupInProgressRef.current) {
if ((this as any)._isSyncingSlabThickness) return;
(this as any)._isSyncingSlabThickness = true;
try {
const engine = renderingEngineRef.current;
if (engine) {
const otherViewports = engine.getViewports().filter(v => v.id !== this.id && (v as any).setSlabThickness);
otherViewports.forEach(otherVp => {
if ((otherVp as any).getSlabThickness() !== slabThickness) {
(otherVp as any)._isSyncingSlabThickness = true;
try {
(otherVp as any).setSlabThickness(slabThickness);
otherVp.render();
} catch (err) { /* ignore */ }
finally {
(otherVp as any)._isSyncingSlabThickness = false;
}
}
});
}
} finally {
(this as any)._isSyncingSlabThickness = false;
}
}
};
(viewport as any)._setSlabThicknessPatched = true;
}
if (!element._resizeObserver) {
const resizeObserver = new window.ResizeObserver(() => {
if (setupInProgressRef.current) {
return;
}
if (element.offsetWidth > 0 && element.offsetHeight > 0) {
renderingEngine.resize();
// Always get the latest viewport instance
@@ -3751,12 +3940,16 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat
// Do not attach DICOM-specific tools to external images
mainToolGroup.removeViewports(renderingEngineId, viewportId);
if (stackToolGroup) {
stackToolGroup.removeViewports(renderingEngineId, viewportId);
}
} catch (err) {
appLogger.warn('Failed to render external image in viewport', err);
}
} else {
try {
viewport.setStack(imageIds);
await viewport.setStack(imageIds);
if (currentEpoch !== setupEpochRef.current) return;
} catch (err) {
appLogger.warn('viewport.setStack failed', err);
}
@@ -3765,13 +3958,22 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat
// CrosshairsTool is not applicable for stack viewports
if (!crossHairsEnabled) {
mainToolGroup.addViewport(viewportId, renderingEngineId);
if (stackToolGroup) {
stackToolGroup.removeViewports(renderingEngineId, viewportId);
}
} else {
mainToolGroup.removeViewports(renderingEngineId, viewportId);
if (stackToolGroup) {
stackToolGroup.addViewport(viewportId, renderingEngineId);
}
}
// Restore scroll, properties, etc.
} else if (viewportModes[i] === 'volume') {
mainToolGroup.addViewport(viewportId, renderingEngineId);
if (stackToolGroup) {
stackToolGroup.removeViewports(renderingEngineId, viewportId);
}
//let sortedImageIds = imageIds;
//if (imageIds && imageIds.length > 1) {
@@ -3784,11 +3986,25 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat
// });
//}
// Force cache metadata for the volume to prevent undefined extent errors
await setLoadedImageIdsAndCacheMeta(imageIds, { force: true });
if (currentEpoch !== setupEpochRef.current) return;
// Use a unique volumeId per viewport
const volumeId = `myVolumeId_${i}`;
try {
if (viewport.getActorUIDs().includes(volumeId)) {
viewport.removeActors([volumeId]);
}
} catch (e) { /* ignore */ }
try {
cornerstone3d.cache.removeVolumeLoadObject(volumeId);
} catch (e) { /* ignore */ }
const volume = await volumeLoader.createAndCacheVolume(volumeId, { imageIds: imageIds });
if (currentEpoch !== setupEpochRef.current) return;
volume.load();
viewport.setVolumes([{ volumeId }]);
await viewport.setVolumes([{ volumeId }]);
if (currentEpoch !== setupEpochRef.current) return;
if (typeof viewport.getNumberOfSlices === "function") {
const totalSlices = viewport.getNumberOfSlices();
const pending = pendingSliceIndexByViewportRef.current[i] || 0;
@@ -3913,6 +4129,23 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat
// 7. Apply mouse tool bindings
applyMouseToolBindings();
if (currentEpoch === setupEpochRef.current) {
setupInProgressRef.current = false;
setSetupInProgress(false);
if (renderingEngineRef.current) {
renderingEngineRef.current.resize();
renderingEngineRef.current.render();
}
}
} catch (err) {
appLogger.error("setup failed", err);
} finally {
if (currentEpoch === setupEpochRef.current) {
setupInProgressRef.current = false;
setSetupInProgress(false);
}
}
};
// Use requestAnimationFrame to ensure DOM is painted before setup
@@ -3990,15 +4223,12 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat
// Manage Slab Thickness Synchronizer
useEffect(() => {
// Clean up previous synchronizer
if (slabThicknessSynchronizerRef.current) {
try {
slabThicknessSynchronizerRef.current.destroy();
} catch (e) {
appLogger.warn("Failed to destroy slab thickness synchronizer", e);
}
cornerstoneTools.SynchronizerManager.destroySynchronizer("slabThicknessSync");
} catch (e) { /* ignore */ }
slabThicknessSynchronizerRef.current = null;
}
if (setupInProgress) return;
if (!crossHairsEnabled || !syncSlabThickness) return;
const renderingEngine = renderingEngineRef.current;
@@ -4029,14 +4259,12 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat
}
return () => {
if (slabThicknessSynchronizerRef.current) {
try {
slabThicknessSynchronizerRef.current.destroy();
cornerstoneTools.SynchronizerManager.destroySynchronizer("slabThicknessSync");
} catch (e) { /* ignore */ }
slabThicknessSynchronizerRef.current = null;
}
};
}, [syncSlabThickness, crossHairsEnabled, viewportGrid, viewportModes]);
}, [syncSlabThickness, crossHairsEnabled, viewportGrid, viewportModes, setupInProgress]);
// Handler to apply a preset for a specific viewport
const handleApplyPreset = useCallback((viewportIdx: number, center: number, width: number) => {
@@ -4657,17 +4885,28 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat
});
}, [orderBySliceLocation, availableStacks]);
function updateActiveReferenceLineViewport(mainToolGroup: cornerstoneTools.Types.IToolGroup) {
function updateActiveReferenceLineViewport(
mainToolGroup: cornerstoneTools.Types.IToolGroup,
stackToolGroup?: cornerstoneTools.Types.IToolGroup
) {
// Ensure the main tool group is defined (it won't be during setup)
if (!mainToolGroup) return;
const STACK_TOOL_GROUP_ID = "TOOL_GROUP_stack_" + container_id;
const targetStackToolGroup = stackToolGroup || ToolGroupManager.getToolGroup(STACK_TOOL_GROUP_ID);
const sourceViewportId = `CT_${activeViewport}`
if (referenceLinesEnabled) {
mainToolGroup.setToolEnabled(ReferenceLinesTool.toolName)
mainToolGroup.setToolConfiguration(ReferenceLinesTool.toolName, {
[mainToolGroup, targetStackToolGroup].forEach(toolGroup => {
if (!toolGroup) return;
toolGroup.setToolEnabled(ReferenceLinesTool.toolName)
toolGroup.setToolConfiguration(ReferenceLinesTool.toolName, {
sourceViewportId,
})
});
} else {
mainToolGroup.setToolDisabled(ReferenceLinesTool.toolName)
[mainToolGroup, targetStackToolGroup].forEach(toolGroup => {
if (!toolGroup) return;
toolGroup.setToolDisabled(ReferenceLinesTool.toolName)
});
}
}
@@ -4754,7 +4993,7 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat
setActiveViewport(0);
}
function truncateStack(lower: number, upper: number) {
async function truncateStack(lower: number, upper: number) {
// Only allow when 1 viewport and 1 stack is loaded
appLogger.debug("Truncating stack from", lower, "to", upper);
appLogger.debug(stackOrderModified)
@@ -4795,7 +5034,7 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat
if (renderingEngine) {
const viewport = renderingEngine.getViewport("CT_0");
if (viewport && typeof viewport.setStack === "function") {
viewport.setStack(truncatedImageIds);
await viewport.setStack(truncatedImageIds);
viewport.setImageIdIndex?.(0);
viewport.render?.();
}
@@ -5127,10 +5366,21 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat
if (state.modes?.[i] === "volume") {
try {
// Force cache metadata for the volume to prevent undefined extent errors
await setLoadedImageIdsAndCacheMeta(importedImageIds, { force: true });
const volumeId = `myVolumeId_${i}`;
try {
if (viewport.getActorUIDs().includes(volumeId)) {
viewport.removeActors([volumeId]);
}
} catch (e) { /* ignore */ }
try {
cornerstone3d.cache.removeVolumeLoadObject(volumeId);
} catch (e) { /* ignore */ }
const volume = await volumeLoader.createAndCacheVolume(volumeId, { imageIds: importedImageIds });
volume.load();
viewport.setVolumes([{ volumeId }]);
await viewport.setVolumes([{ volumeId }]);
viewport.render();
} catch (e) {
appLogger.warn("Failed to create/load volume for viewport", i, e);
@@ -5143,7 +5393,7 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat
setTimeout(() => { try { csUtilities.jumpToSlice(viewport.element, { imageIndex: state.volumeSliceIndices[i] }); } catch (e) { } }, 100);
}
} else {
try { if (viewport.setStack) { viewport.setStack(importedImageIds); viewport.render(); } } catch (e) { appLogger.warn('Failed to set stack for viewport', i, e); }
try { if (viewport.setStack) { await viewport.setStack(importedImageIds); viewport.render(); } } catch (e) { appLogger.warn('Failed to set stack for viewport', i, e); }
if (state.stackPos && typeof state.stackPos[i] === "number" && typeof viewport.setImageIdIndex === "function") {
try { if (csUtilities && csUtilities.jumpToSlice) csUtilities.jumpToSlice(viewport.element, { imageIndex: state.stackPos[i] }); else viewport.setImageIdIndex(state.stackPos[i]); } catch (e) { /* ignore */ }
}
@@ -7925,6 +8175,30 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat
{isActiveViewportStack && (
<div style={{ fontSize: 11, color: '#bbdefb', fontWeight: 700 }}>ACTIVE</div>
)}
{!invalid && (
<button
onClick={(e) => {
e.stopPropagation();
handleLoadStackOrthogonal(idx);
}}
style={{
background: '#2e7d32',
color: '#fff',
border: 'none',
borderRadius: 4,
padding: '4px 8px',
fontSize: 11,
fontWeight: 'bold',
cursor: 'pointer',
marginTop: '4px',
transition: 'background 0.2s',
}}
onMouseEnter={(e) => { e.currentTarget.style.background = '#1b5e20'; }}
onMouseLeave={(e) => { e.currentTarget.style.background = '#2e7d32'; }}
>
Ortho (3D)
</button>
)}
{hideChromeForMobileInteraction && !invalid && (
<div style={{ display: 'flex', gap: 6 }}>
<button