start ortho automode
This commit is contained in:
+309
-35
@@ -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 [crossHairsEnabled, setCrossHairsEnabled] = useState(false);
|
||||||
const [crosshairsProjectionType, setCrosshairsProjectionType] = useState<string>("None");
|
const [crosshairsProjectionType, setCrosshairsProjectionType] = useState<string>("None");
|
||||||
const [syncSlabThickness, setSyncSlabThickness] = useState<boolean>(true);
|
const [syncSlabThickness, setSyncSlabThickness] = useState<boolean>(true);
|
||||||
const [showCenterPoint, setShowCenterPoint] = useState<boolean>(true);
|
const [showCenterPoint, setShowCenterPoint] = useState<boolean>(true);
|
||||||
const slabThicknessSynchronizerRef = useRef<any>(null);
|
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 [referenceLinesEnabled, setReferenceLinesEnabled] = useState(true);
|
||||||
|
|
||||||
const [showAdvancedMouseBindings, setShowAdvancedMouseBindings] = useState(false);
|
const [showAdvancedMouseBindings, setShowAdvancedMouseBindings] = useState(false);
|
||||||
@@ -2510,7 +2618,11 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const mainToolGroup = ToolGroupManager.getToolGroup(MAIN_TOOL_GROUP_ID);
|
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
|
// List all annotation tools you use
|
||||||
[
|
[
|
||||||
@@ -2523,12 +2635,13 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat
|
|||||||
'Sculptor',
|
'Sculptor',
|
||||||
// Add more if needed
|
// Add more if needed
|
||||||
].forEach(toolName => {
|
].forEach(toolName => {
|
||||||
mainToolGroup.setToolConfiguration(toolName, {
|
toolGroup.setToolConfiguration(toolName, {
|
||||||
color: '#00e676', // normal color
|
color: '#00e676', // normal color
|
||||||
selectedColor: '#ffeb3b', // yellow for selected
|
selectedColor: '#ffeb3b', // yellow for selected
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}, [MAIN_TOOL_GROUP_ID]);
|
});
|
||||||
|
}, [MAIN_TOOL_GROUP_ID, container_id]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
@@ -2555,6 +2668,8 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const mainToolGroup = ToolGroupManager.getToolGroup(MAIN_TOOL_GROUP_ID);
|
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;
|
if (!mainToolGroup) return;
|
||||||
const modifierActive = shiftPressed || (altEnablesReferenceCursors && altPressed);
|
const modifierActive = shiftPressed || (altEnablesReferenceCursors && altPressed);
|
||||||
|
|
||||||
@@ -2572,15 +2687,21 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!mainToolGroup.hasTool(ReferenceCursors.toolName)) {
|
[mainToolGroup, stackToolGroup].forEach(toolGroup => {
|
||||||
mainToolGroup.addTool(ReferenceCursors.toolName);
|
if (!toolGroup) return;
|
||||||
|
if (!toolGroup.hasTool(ReferenceCursors.toolName)) {
|
||||||
|
toolGroup.addTool(ReferenceCursors.toolName);
|
||||||
}
|
}
|
||||||
mainToolGroup.setToolEnabled(ReferenceCursors.toolName);
|
toolGroup.setToolEnabled(ReferenceCursors.toolName);
|
||||||
mainToolGroup.setToolActive(ReferenceCursors.toolName);
|
toolGroup.setToolActive(ReferenceCursors.toolName);
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
if (mainToolGroup.hasTool(ReferenceCursors.toolName)) {
|
[mainToolGroup, stackToolGroup].forEach(toolGroup => {
|
||||||
mainToolGroup.setToolDisabled(ReferenceCursors.toolName);
|
if (!toolGroup) return;
|
||||||
|
if (toolGroup.hasTool(ReferenceCursors.toolName)) {
|
||||||
|
toolGroup.setToolDisabled(ReferenceCursors.toolName);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
altPressed,
|
altPressed,
|
||||||
@@ -2591,14 +2712,17 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat
|
|||||||
viewportImageIds,
|
viewportImageIds,
|
||||||
viewportModes,
|
viewportModes,
|
||||||
setLoadedImageIdsAndCacheMeta,
|
setLoadedImageIdsAndCacheMeta,
|
||||||
|
container_id,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Helper to apply all mouse tool bindings (normal or ctrl)
|
// Helper to apply all mouse tool bindings (normal or ctrl)
|
||||||
const applyMouseToolBindings = useCallback(() => {
|
const applyMouseToolBindings = useCallback(() => {
|
||||||
|
const STACK_TOOL_GROUP_ID = "TOOL_GROUP_stack_" + container_id;
|
||||||
// Apply to both stack and volume tool groups
|
// Apply to both stack and volume tool groups
|
||||||
const mainToolGroup = cornerstoneTools.ToolGroupManager.getToolGroup(MAIN_TOOL_GROUP_ID);
|
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) {
|
if (toolGroup) {
|
||||||
// Set all tools to passive first, removing all bindings
|
// Set all tools to passive first, removing all bindings
|
||||||
TOOL_OPTIONS.forEach(opt => {
|
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(() => {
|
useEffect(() => {
|
||||||
applyMouseToolBindings();
|
applyMouseToolBindings();
|
||||||
@@ -3579,15 +3703,23 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat
|
|||||||
}, [applyStackDrop, getDropZoneFromClientPoint, isPhoneLayout, mobileInteractionActive, resolveDropZone, viewportGrid]);
|
}, [applyStackDrop, getDropZoneFromClientPoint, isPhoneLayout, mobileInteractionActive, resolveDropZone, viewportGrid]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const setup = async () => {
|
setupEpochRef.current++;
|
||||||
appLogger.debug("setup")
|
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
|
// 1. Initialize Cornerstone3D and tools ONCE
|
||||||
if (!running.current) {
|
if (!running.current) {
|
||||||
running.current = true;
|
running.current = true;
|
||||||
await csRenderInit();
|
await csRenderInit();
|
||||||
|
if (currentEpoch !== setupEpochRef.current) return;
|
||||||
await csToolsInit(
|
await csToolsInit(
|
||||||
);
|
);
|
||||||
|
if (currentEpoch !== setupEpochRef.current) return;
|
||||||
dicomImageLoaderInit();
|
dicomImageLoaderInit();
|
||||||
// Register client-side image loader for external images (JPEG/PNG/blob/data)
|
// Register client-side image loader for external images (JPEG/PNG/blob/data)
|
||||||
try {
|
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 ---
|
// --- In your setup function, use the correct tool group per viewport ---
|
||||||
let mainToolGroup = ToolGroupManager.getToolGroup(MAIN_TOOL_GROUP_ID);
|
let mainToolGroup = ToolGroupManager.getToolGroup(MAIN_TOOL_GROUP_ID);
|
||||||
if (!mainToolGroup) {
|
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
|
const visibleVolumeViewports = viewportModes
|
||||||
.slice(0, viewportGrid.rows * viewportGrid.cols)
|
.slice(0, viewportGrid.rows * viewportGrid.cols)
|
||||||
.filter(mode => mode === 'volume').length;
|
.filter(mode => mode === 'volume').length;
|
||||||
|
|
||||||
// --- In your setup function, after tool group creation, enable/disable ReferenceLinesTool for each tool group ---
|
// --- 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
|
// 6. Enable and configure each viewport
|
||||||
for (let i = 0; i < MAX_VIEWPORTS; i++) {
|
for (let i = 0; i < MAX_VIEWPORTS; i++) {
|
||||||
|
if (currentEpoch !== setupEpochRef.current) return;
|
||||||
|
|
||||||
let imageIds = viewportImageIds[i];
|
let imageIds = viewportImageIds[i];
|
||||||
if (!imageIds || imageIds.length === 0) continue;
|
if (!imageIds || imageIds.length === 0) continue;
|
||||||
@@ -3703,8 +3856,44 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat
|
|||||||
const viewport = renderingEngine.getViewport(viewportId);
|
const viewport = renderingEngine.getViewport(viewportId);
|
||||||
if (!viewport) continue;
|
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) {
|
if (!element._resizeObserver) {
|
||||||
const resizeObserver = new window.ResizeObserver(() => {
|
const resizeObserver = new window.ResizeObserver(() => {
|
||||||
|
if (setupInProgressRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (element.offsetWidth > 0 && element.offsetHeight > 0) {
|
if (element.offsetWidth > 0 && element.offsetHeight > 0) {
|
||||||
renderingEngine.resize();
|
renderingEngine.resize();
|
||||||
// Always get the latest viewport instance
|
// 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
|
// Do not attach DICOM-specific tools to external images
|
||||||
mainToolGroup.removeViewports(renderingEngineId, viewportId);
|
mainToolGroup.removeViewports(renderingEngineId, viewportId);
|
||||||
|
if (stackToolGroup) {
|
||||||
|
stackToolGroup.removeViewports(renderingEngineId, viewportId);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
appLogger.warn('Failed to render external image in viewport', err);
|
appLogger.warn('Failed to render external image in viewport', err);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
viewport.setStack(imageIds);
|
await viewport.setStack(imageIds);
|
||||||
|
if (currentEpoch !== setupEpochRef.current) return;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
appLogger.warn('viewport.setStack failed', 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
|
// CrosshairsTool is not applicable for stack viewports
|
||||||
if (!crossHairsEnabled) {
|
if (!crossHairsEnabled) {
|
||||||
mainToolGroup.addViewport(viewportId, renderingEngineId);
|
mainToolGroup.addViewport(viewportId, renderingEngineId);
|
||||||
|
if (stackToolGroup) {
|
||||||
|
stackToolGroup.removeViewports(renderingEngineId, viewportId);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
mainToolGroup.removeViewports(renderingEngineId, viewportId);
|
mainToolGroup.removeViewports(renderingEngineId, viewportId);
|
||||||
|
if (stackToolGroup) {
|
||||||
|
stackToolGroup.addViewport(viewportId, renderingEngineId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restore scroll, properties, etc.
|
// Restore scroll, properties, etc.
|
||||||
} else if (viewportModes[i] === 'volume') {
|
} else if (viewportModes[i] === 'volume') {
|
||||||
mainToolGroup.addViewport(viewportId, renderingEngineId);
|
mainToolGroup.addViewport(viewportId, renderingEngineId);
|
||||||
|
if (stackToolGroup) {
|
||||||
|
stackToolGroup.removeViewports(renderingEngineId, viewportId);
|
||||||
|
}
|
||||||
|
|
||||||
//let sortedImageIds = imageIds;
|
//let sortedImageIds = imageIds;
|
||||||
//if (imageIds && imageIds.length > 1) {
|
//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
|
// Use a unique volumeId per viewport
|
||||||
const volumeId = `myVolumeId_${i}`;
|
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 });
|
const volume = await volumeLoader.createAndCacheVolume(volumeId, { imageIds: imageIds });
|
||||||
|
if (currentEpoch !== setupEpochRef.current) return;
|
||||||
volume.load();
|
volume.load();
|
||||||
viewport.setVolumes([{ volumeId }]);
|
await viewport.setVolumes([{ volumeId }]);
|
||||||
|
if (currentEpoch !== setupEpochRef.current) return;
|
||||||
if (typeof viewport.getNumberOfSlices === "function") {
|
if (typeof viewport.getNumberOfSlices === "function") {
|
||||||
const totalSlices = viewport.getNumberOfSlices();
|
const totalSlices = viewport.getNumberOfSlices();
|
||||||
const pending = pendingSliceIndexByViewportRef.current[i] || 0;
|
const pending = pendingSliceIndexByViewportRef.current[i] || 0;
|
||||||
@@ -3913,6 +4129,23 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat
|
|||||||
|
|
||||||
// 7. Apply mouse tool bindings
|
// 7. Apply mouse tool bindings
|
||||||
applyMouseToolBindings();
|
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
|
// 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
|
// Manage Slab Thickness Synchronizer
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Clean up previous synchronizer
|
// Clean up previous synchronizer
|
||||||
if (slabThicknessSynchronizerRef.current) {
|
|
||||||
try {
|
try {
|
||||||
slabThicknessSynchronizerRef.current.destroy();
|
cornerstoneTools.SynchronizerManager.destroySynchronizer("slabThicknessSync");
|
||||||
} catch (e) {
|
} catch (e) { /* ignore */ }
|
||||||
appLogger.warn("Failed to destroy slab thickness synchronizer", e);
|
|
||||||
}
|
|
||||||
slabThicknessSynchronizerRef.current = null;
|
slabThicknessSynchronizerRef.current = null;
|
||||||
}
|
|
||||||
|
|
||||||
|
if (setupInProgress) return;
|
||||||
if (!crossHairsEnabled || !syncSlabThickness) return;
|
if (!crossHairsEnabled || !syncSlabThickness) return;
|
||||||
|
|
||||||
const renderingEngine = renderingEngineRef.current;
|
const renderingEngine = renderingEngineRef.current;
|
||||||
@@ -4029,14 +4259,12 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat
|
|||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (slabThicknessSynchronizerRef.current) {
|
|
||||||
try {
|
try {
|
||||||
slabThicknessSynchronizerRef.current.destroy();
|
cornerstoneTools.SynchronizerManager.destroySynchronizer("slabThicknessSync");
|
||||||
} catch (e) { /* ignore */ }
|
} catch (e) { /* ignore */ }
|
||||||
slabThicknessSynchronizerRef.current = null;
|
slabThicknessSynchronizerRef.current = null;
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}, [syncSlabThickness, crossHairsEnabled, viewportGrid, viewportModes]);
|
}, [syncSlabThickness, crossHairsEnabled, viewportGrid, viewportModes, setupInProgress]);
|
||||||
|
|
||||||
// Handler to apply a preset for a specific viewport
|
// Handler to apply a preset for a specific viewport
|
||||||
const handleApplyPreset = useCallback((viewportIdx: number, center: number, width: number) => {
|
const handleApplyPreset = useCallback((viewportIdx: number, center: number, width: number) => {
|
||||||
@@ -4657,17 +4885,28 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat
|
|||||||
});
|
});
|
||||||
}, [orderBySliceLocation, availableStacks]);
|
}, [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)
|
// Ensure the main tool group is defined (it won't be during setup)
|
||||||
if (!mainToolGroup) return;
|
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}`
|
const sourceViewportId = `CT_${activeViewport}`
|
||||||
if (referenceLinesEnabled) {
|
if (referenceLinesEnabled) {
|
||||||
mainToolGroup.setToolEnabled(ReferenceLinesTool.toolName)
|
[mainToolGroup, targetStackToolGroup].forEach(toolGroup => {
|
||||||
mainToolGroup.setToolConfiguration(ReferenceLinesTool.toolName, {
|
if (!toolGroup) return;
|
||||||
|
toolGroup.setToolEnabled(ReferenceLinesTool.toolName)
|
||||||
|
toolGroup.setToolConfiguration(ReferenceLinesTool.toolName, {
|
||||||
sourceViewportId,
|
sourceViewportId,
|
||||||
})
|
})
|
||||||
|
});
|
||||||
} else {
|
} 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);
|
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
|
// Only allow when 1 viewport and 1 stack is loaded
|
||||||
appLogger.debug("Truncating stack from", lower, "to", upper);
|
appLogger.debug("Truncating stack from", lower, "to", upper);
|
||||||
appLogger.debug(stackOrderModified)
|
appLogger.debug(stackOrderModified)
|
||||||
@@ -4795,7 +5034,7 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat
|
|||||||
if (renderingEngine) {
|
if (renderingEngine) {
|
||||||
const viewport = renderingEngine.getViewport("CT_0");
|
const viewport = renderingEngine.getViewport("CT_0");
|
||||||
if (viewport && typeof viewport.setStack === "function") {
|
if (viewport && typeof viewport.setStack === "function") {
|
||||||
viewport.setStack(truncatedImageIds);
|
await viewport.setStack(truncatedImageIds);
|
||||||
viewport.setImageIdIndex?.(0);
|
viewport.setImageIdIndex?.(0);
|
||||||
viewport.render?.();
|
viewport.render?.();
|
||||||
}
|
}
|
||||||
@@ -5127,10 +5366,21 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat
|
|||||||
|
|
||||||
if (state.modes?.[i] === "volume") {
|
if (state.modes?.[i] === "volume") {
|
||||||
try {
|
try {
|
||||||
|
// Force cache metadata for the volume to prevent undefined extent errors
|
||||||
|
await setLoadedImageIdsAndCacheMeta(importedImageIds, { force: true });
|
||||||
|
|
||||||
const volumeId = `myVolumeId_${i}`;
|
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 });
|
const volume = await volumeLoader.createAndCacheVolume(volumeId, { imageIds: importedImageIds });
|
||||||
volume.load();
|
volume.load();
|
||||||
viewport.setVolumes([{ volumeId }]);
|
await viewport.setVolumes([{ volumeId }]);
|
||||||
viewport.render();
|
viewport.render();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
appLogger.warn("Failed to create/load volume for viewport", i, 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);
|
setTimeout(() => { try { csUtilities.jumpToSlice(viewport.element, { imageIndex: state.volumeSliceIndices[i] }); } catch (e) { } }, 100);
|
||||||
}
|
}
|
||||||
} else {
|
} 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") {
|
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 */ }
|
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 && (
|
{isActiveViewportStack && (
|
||||||
<div style={{ fontSize: 11, color: '#bbdefb', fontWeight: 700 }}>ACTIVE</div>
|
<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 && (
|
{hideChromeForMobileInteraction && !invalid && (
|
||||||
<div style={{ display: 'flex', gap: 6 }}>
|
<div style={{ display: 'flex', gap: 6 }}>
|
||||||
<button
|
<button
|
||||||
|
|||||||
Reference in New Issue
Block a user