diff --git a/dicom-viewer/src/App.tsx b/dicom-viewer/src/App.tsx index 74c1a82..cfb9b46 100644 --- a/dicom-viewer/src/App.tsx +++ b/dicom-viewer/src/App.tsx @@ -6,7 +6,7 @@ import { } from "@cornerstonejs/core" import { init as csRenderInit } from "@cornerstonejs/core" import { init as csToolsInit, addTool, ToolGroupManager, Enums as csToolsEnums, PanTool, WindowLevelTool, StackScrollTool, ZoomTool, PlanarRotateTool } from "@cornerstonejs/tools" -import { LengthTool, ProbeTool, ArrowAnnotateTool, RectangleROITool, EllipticalROITool, PlanarFreehandROITool, PlanarFreehandContourSegmentationTool, SculptorTool, CrosshairsTool, ReferenceLinesTool } from "@cornerstonejs/tools" +import { LengthTool, ProbeTool, ArrowAnnotateTool, RectangleROITool, EllipticalROITool, PlanarFreehandROITool, PlanarFreehandContourSegmentationTool, SculptorTool, CrosshairsTool, ReferenceLinesTool, ReferenceCursors } from "@cornerstonejs/tools" import { init as dicomImageLoaderInit } from "@cornerstonejs/dicom-image-loader" import * as cornerstoneTools from '@cornerstonejs/tools'; import { StackScrollOutOfBoundsEvent } from 'core/src/types/EventTypes'; @@ -24,7 +24,7 @@ const { IMAGE_RENDERED } = Enums.Events; const STACK_TOOL_GROUP_ID = 'STACK_TOOL_GROUP_ID'; const VOLUME_TOOL_GROUP_ID = 'VOLUME_TOOL_GROUP_ID'; -const REFERENCE_TOOL_GROUP_ID = 'REFERENCE_TOOL_GROUP_ID'; +const MAIN_TOOL_GROUP_ID = 'MAIN_TOOL_GROUP_ID'; // Common CT presets (add more as needed) const WINDOW_LEVEL_PRESETS = [ @@ -50,6 +50,7 @@ const TOOL_OPTIONS = [ { label: "Freehand ROI", value: PlanarFreehandROITool.toolName }, { label: "Freehand Contour Segmentation", value: PlanarFreehandContourSegmentationTool.toolName }, { label: "Sculptor", value: SculptorTool.toolName }, + { label: "Reference Cursors", value: ReferenceCursors.toolName }, ] // Mouse button labels @@ -122,7 +123,7 @@ function App() { await setLoadedImageIdsAndCacheMeta(stack); }; - const [crossReferenceEnabled, setCrossReferenceEnabled] = useState(true); + const [crossHairsEnabled, setCrossHairsEnabled] = useState(false); const [referenceLinesEnabled, setReferenceLinesEnabled] = useState(true); const sortViewportImageIdsBySliceLocation = () => { @@ -156,9 +157,6 @@ function App() { 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; } @@ -170,7 +168,6 @@ function App() { // Wrap setLoadedImageIds to also cache DICOM metadata const setLoadedImageIdsAndCacheMeta = async (imageIds: string[]) => { - console.log("Caching loaded image IDs:", imageIds); const loadPromises: Promise[] = []; for (const imageId of imageIds) { @@ -196,7 +193,6 @@ function App() { // Wait for all metadata loads to finish await Promise.all(loadPromises); - console.log("Image IDs cached:", imageIds); // Trigger sort if enabled if (orderBySliceLocation) { @@ -217,6 +213,7 @@ function App() { const [orderBySliceLocation, setOrderBySliceLocation] = useState(true); + const [altEnablesReferenceCursors, setAltEnablesReferenceCursors] = useState(true); // Helper to generate grid options const gridOptions = [] @@ -340,19 +337,56 @@ function App() { setMetaModalOpen(true); }; + const [altPressed, setAltPressed] = useState(false); + + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.altKey) setAltPressed(true); + }; + const handleKeyUp = (e: KeyboardEvent) => { + if (!e.altKey) setAltPressed(false); + }; + window.addEventListener("keydown", handleKeyDown); + window.addEventListener("keyup", handleKeyUp); + return () => { + window.removeEventListener("keydown", handleKeyDown); + window.removeEventListener("keyup", handleKeyUp); + }; + }, []); + + useEffect(() => { + if (!altEnablesReferenceCursors) return; + const mainToolGroup = ToolGroupManager.getToolGroup(MAIN_TOOL_GROUP_ID); + if (!mainToolGroup) return; + if (altPressed) { + if (!mainToolGroup.hasTool(ReferenceCursors.toolName)) { + mainToolGroup.addTool(ReferenceCursors.toolName); + } + mainToolGroup.setToolEnabled(ReferenceCursors.toolName); + mainToolGroup.setToolActive(ReferenceCursors.toolName); + } else { + if (mainToolGroup.hasTool(ReferenceCursors.toolName)) { + mainToolGroup.setToolDisabled(ReferenceCursors.toolName); + } + } + }, [altPressed, altEnablesReferenceCursors]); + // Helper to apply all mouse tool bindings (normal or ctrl) const applyMouseToolBindings = useCallback(() => { // Apply to both stack and volume tool groups const stackToolGroup = cornerstoneTools.ToolGroupManager.getToolGroup(STACK_TOOL_GROUP_ID); const volumeToolGroup = cornerstoneTools.ToolGroupManager.getToolGroup(VOLUME_TOOL_GROUP_ID); + const mainToolGroup = cornerstoneTools.ToolGroupManager.getToolGroup(MAIN_TOOL_GROUP_ID); - [stackToolGroup, volumeToolGroup].forEach(toolGroup => { + [stackToolGroup, volumeToolGroup, mainToolGroup].forEach(toolGroup => { if (toolGroup) { // Set all tools to passive first, removing all bindings TOOL_OPTIONS.forEach(opt => { toolGroup.setToolPassive(opt.value, { removeAllBindings: true }); }); + toolGroup.setToolDisabled(ReferenceCursors.toolName); + // Collect all bindings for each tool const toolBindings: Record> = {}; @@ -468,7 +502,6 @@ function App() { useEffect(() => { const setup = async () => { - console.log("Setting up.."); // 1. Initialize Cornerstone3D and tools ONCE if (!running.current) { running.current = true; @@ -488,9 +521,10 @@ function App() { } // --- In your setup function, use the correct tool group per viewport --- - const { stackToolGroup, volumeToolGroup, referenceToolGroup } = ToolGroupManager.getToolGroup(STACK_TOOL_GROUP_ID) - ? { stackToolGroup: ToolGroupManager.getToolGroup(STACK_TOOL_GROUP_ID), volumeToolGroup: ToolGroupManager.getToolGroup(VOLUME_TOOL_GROUP_ID), referenceToolGroup: ToolGroupManager.getToolGroup(REFERENCE_TOOL_GROUP_ID) } - : setupTools(); + let mainToolGroup = ToolGroupManager.getToolGroup(MAIN_TOOL_GROUP_ID); + if (!mainToolGroup) { + mainToolGroup = setupTools().mainToolGroup; + } const visibleVolumeViewports = viewportModes @@ -498,16 +532,7 @@ function App() { .filter(mode => mode === 'volume').length; // --- In your setup function, after tool group creation, enable/disable ReferenceLinesTool for each tool group --- - if (referenceLinesEnabled) { - const sourceViewportId = `CT_${activeViewport}`; - referenceToolGroup.setToolEnabled(ReferenceLinesTool.toolName); - referenceToolGroup.setToolConfiguration(ReferenceLinesTool.toolName, { - sourceViewportId, - }); - } else { - referenceToolGroup.setToolDisabled(ReferenceLinesTool.toolName); - - } + updateActiveReferenceLineViewport(mainToolGroup) // 6. Enable and configure each viewport for (let i = 0; i < MAX_VIEWPORTS; i++) { @@ -525,7 +550,7 @@ function App() { : Enums.ViewportType.STACK; // Choose tool group based on viewport type - const toolGroup = viewportModes[i] === 'volume' ? volumeToolGroup : stackToolGroup; + //const toolGroup = viewportModes[i] === 'volume' ? volumeToolGroup : stackToolGroup; @@ -540,14 +565,10 @@ function App() { }); element._cornerstoneEnabled = true; element._lastViewportType = viewportType; - toolGroup.addViewport(viewportId, renderingEngineId); + //mainToolGroup.addViewport(viewportId, renderingEngineId); + //toolGroup.addViewport(viewportId, renderingEngineId); } - // Add tool group only once - toolGroup.addViewport(viewportId, renderingEngineId); - - referenceToolGroup.addViewport(viewportId, renderingEngineId); - const viewport = renderingEngine.getViewport(viewportId); if (!viewport) continue; @@ -574,8 +595,16 @@ function App() { if (imageIds) { viewport.setStack(imageIds); } + // CrosshairsTool is not applicable for stack viewports + if (!crossHairsEnabled) { + mainToolGroup.addViewport(viewportId, renderingEngineId); + } else { + mainToolGroup.removeViewports(renderingEngineId, viewportId); + } + // Restore scroll, properties, etc. } else if (viewportModes[i] === 'volume') { + mainToolGroup.addViewport(viewportId, renderingEngineId); //let sortedImageIds = imageIds; //if (imageIds && imageIds.length > 1) { @@ -625,15 +654,7 @@ function App() { propsToRestore.colormap = colormap; } setTimeout(() => { - if (viewportId === "CT_0") { - console.log("Restoring properties:", propsToRestore, viewport); - console.log("VOI range:", voiRange); - } viewport.setProperties(propsToRestore); - if (viewportId === "CT_0") { - console.log("Restored:", propsToRestore, viewport); - console.log(viewport.getProperties()); - } viewport.render() }, 100); } @@ -659,20 +680,35 @@ function App() { activeVolumeViewports.push(viewport); } } - - if ( - crossReferenceEnabled && - activeVolumeViewports.length >= 2 - ) { - if (!volumeToolGroup.hasTool(CrosshairsTool.toolName)) { - volumeToolGroup.addTool(CrosshairsTool.toolName); + // Find all active volume viewports + const activeVolumeViewportIds = []; + for (let i = 0; i < viewportGrid.rows * viewportGrid.cols; i++) { + if (viewportModes[i] === 'volume') { + activeVolumeViewportIds.push(`CT_${i}`); + } + } + + if (mainToolGroup && crossHairsEnabled) { + // Only enable CrosshairsTool if at least two volume viewports are active + if (activeVolumeViewportIds.length >= 2) { + // Make sure CrosshairsTool is added to the tool group + if (!mainToolGroup.hasTool(CrosshairsTool.toolName)) { + mainToolGroup.addTool(CrosshairsTool.toolName); + } + mainToolGroup.setToolEnabled(CrosshairsTool.toolName); + mainToolGroup.setToolConfiguration(CrosshairsTool.toolName, { + // Example: color: '#ffeb3b', lineWidth: 2 + }); + mainToolGroup.setToolActive(CrosshairsTool.toolName, { + bindings: [{ mouseButton: MouseBindings.Primary }], + }); + + } else { + // Disable CrosshairsTool if not enough volume viewports + if (mainToolGroup.hasTool(CrosshairsTool.toolName)) { + mainToolGroup.setToolDisabled(CrosshairsTool.toolName); + } } - volumeToolGroup.setToolEnabled(CrosshairsTool.toolName); - volumeToolGroup.setToolActive(CrosshairsTool.toolName, { - bindings: [{ mouseButton: MouseBindings.Primary }], - }); - } else if (volumeToolGroup.hasTool(CrosshairsTool.toolName)) { - volumeToolGroup.setToolDisabled(CrosshairsTool.toolName); } // 7. Apply mouse tool bindings @@ -711,11 +747,9 @@ function App() { // Handler to apply a preset for a specific viewport const handleApplyPreset = useCallback((viewportIdx: number, center: number, width: number) => { - console.log("Applying preset:", center, width); const viewportId = `CT_${viewportIdx}`; try { const renderingEngine = renderingEngineRef.current; - console.log("Rendering engine:", renderingEngine); if (renderingEngine) { const viewport = renderingEngine.getViewport(viewportId); if (viewport && typeof viewport.setProperties === "function") { @@ -775,15 +809,8 @@ function App() { useEffect(() => { 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 }); - } + const mainToolGroup = ToolGroupManager.getToolGroup(MAIN_TOOL_GROUP_ID); + updateActiveReferenceLineViewport(mainToolGroup) }, [activeViewport, referenceLinesEnabled]); useEffect(() => { @@ -826,6 +853,19 @@ function App() { }, [orderBySliceLocation, availableStacks]); + function updateActiveReferenceLineViewport(mainToolGroup: cornerstoneTools.Types.IToolGroup) { + // Ensure the main tool group is defined (it won't be during setup) + if (!mainToolGroup) return; + const sourceViewportId = `CT_${activeViewport}` + if (referenceLinesEnabled) { + mainToolGroup.setToolEnabled(ReferenceLinesTool.toolName) + mainToolGroup.setToolConfiguration(ReferenceLinesTool.toolName, { + sourceViewportId, + }) + } else { + mainToolGroup.setToolDisabled(ReferenceLinesTool.toolName) + } + } const viewports = []; for (let i = 0; i < MAX_VIEWPORTS; i++) { @@ -851,11 +891,11 @@ function App() { 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, + 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)} @@ -1242,16 +1282,16 @@ zIndex: activeViewport === i ? 10 : 1, return ( -
+
{/* Side menu toggle button */} + {/* Add more menu items here if needed */}
@@ -1543,7 +1602,7 @@ zIndex: activeViewport === i ? 10 : 1, margin: 3, gridTemplateRows: `repeat(${viewportGrid.rows}, 1fr)`, gridTemplateColumns: `repeat(${viewportGrid.cols}, 1fr)`, - gap: "5px", // Increase gap for better visibility + gap: "5px", // Increase gap for better visibility zIndex: 1, }} > @@ -1735,6 +1794,7 @@ zIndex: activeViewport === i ? 10 : 1,
) + } export default App @@ -1753,60 +1813,33 @@ function setupTools() { cornerstoneTools.addTool(PlanarFreehandROITool) cornerstoneTools.addTool(PlanarFreehandContourSegmentationTool) cornerstoneTools.addTool(SculptorTool) + cornerstoneTools.addTool(ReferenceCursors) cornerstoneTools.addTool(CrosshairsTool) cornerstoneTools.addTool(ReferenceLinesTool); - // Stack tool group (for stack viewports) - 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(ArrowAnnotateTool.toolName, { getTextCallback: () => '' }); - 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(PlanarFreehandContourSegmentationTool.toolName, {}); - stackToolGroup.addTool(SculptorTool.toolName, {}); - //stackToolGroup.addTool(ReferenceLinesTool.toolName); - // StackScrollTool mouse wheel binding - stackToolGroup.setToolActive(StackScrollTool.toolName, { - bindings: [{ mouseButton: MouseBindings.Wheel }], - }); + + let mainToolGroup = ToolGroupManager.getToolGroup(MAIN_TOOL_GROUP_ID); + if (!mainToolGroup) { + mainToolGroup = ToolGroupManager.createToolGroup(MAIN_TOOL_GROUP_ID); + // Add all tools you need for both stack and volume + mainToolGroup.addTool(WindowLevelTool.toolName); + mainToolGroup.addTool(PanTool.toolName); + mainToolGroup.addTool(ZoomTool.toolName); + mainToolGroup.addTool(StackScrollTool.toolName, { loop: false }); + mainToolGroup.addTool(PlanarRotateTool.toolName); + mainToolGroup.addTool(LengthTool.toolName); + mainToolGroup.addTool(ArrowAnnotateTool.toolName, { getTextCallback: () => '' }); + mainToolGroup.addTool(ProbeTool.toolName, { statsCalculator: () => null }); + mainToolGroup.addTool(RectangleROITool.toolName, { calculateStats: false, getTextCallback: () => '' }); + mainToolGroup.addTool(EllipticalROITool.toolName, { getTextCallback: () => '' }); + mainToolGroup.addTool(PlanarFreehandROITool.toolName, { calculateStats: false }); + mainToolGroup.addTool(PlanarFreehandContourSegmentationTool.toolName, {}); + mainToolGroup.addTool(SculptorTool.toolName, {}); + //mainToolGroup.addTool(CrosshairsTool.toolName); + mainToolGroup.addTool(ReferenceLinesTool.toolName); + mainToolGroup.addTool(ReferenceCursors.toolName); } - // Volume tool group (for volume/orthographic viewports) - let volumeToolGroup = ToolGroupManager.getToolGroup(VOLUME_TOOL_GROUP_ID); - if (!volumeToolGroup) { - volumeToolGroup = ToolGroupManager.createToolGroup(VOLUME_TOOL_GROUP_ID); - volumeToolGroup.addTool(StackScrollTool.toolName, { loop: false }); - volumeToolGroup.addTool(WindowLevelTool.toolName); - volumeToolGroup.addTool(PanTool.toolName); - volumeToolGroup.addTool(ZoomTool.toolName); - volumeToolGroup.addTool(PlanarRotateTool.toolName); - volumeToolGroup.addTool(LengthTool.toolName); - volumeToolGroup.addTool(ArrowAnnotateTool.toolName, { getTextCallback: () => '' }); - volumeToolGroup.addTool(ProbeTool.toolName, { statsCalculator: () => null }); - volumeToolGroup.addTool(RectangleROITool.toolName, { calculateStats: false, getTextCallback: () => '' }); - volumeToolGroup.addTool(EllipticalROITool.toolName, { getTextCallback: () => '' }); - volumeToolGroup.addTool(PlanarFreehandROITool.toolName, { calculateStats: false }); - volumeToolGroup.addTool(PlanarFreehandContourSegmentationTool.toolName, {}); - volumeToolGroup.addTool(SculptorTool.toolName, {}); - //volumeToolGroup.addTool(ReferenceLinesTool.toolName); - //volumeToolGroup.addTool(CrosshairsTool.toolName, {}); - } - - let referenceToolGroup = ToolGroupManager.getToolGroup(REFERENCE_TOOL_GROUP_ID); - if (!referenceToolGroup) { - referenceToolGroup = ToolGroupManager.createToolGroup(REFERENCE_TOOL_GROUP_ID); - referenceToolGroup.addTool(ReferenceLinesTool.toolName); - } - - return { stackToolGroup, volumeToolGroup, referenceToolGroup }; + return { mainToolGroup }; }