diff --git a/dicom-viewer/public/test_images/stack2/IMG_098_0oQ6qq4.dcm b/dicom-viewer/public/test_images/stack2/IMG1.dcm similarity index 100% rename from dicom-viewer/public/test_images/stack2/IMG_098_0oQ6qq4.dcm rename to dicom-viewer/public/test_images/stack2/IMG1.dcm diff --git a/dicom-viewer/public/test_images/stack2/IMG_107_AA1OnoC.dcm b/dicom-viewer/public/test_images/stack2/IMG10.dcm similarity index 100% rename from dicom-viewer/public/test_images/stack2/IMG_107_AA1OnoC.dcm rename to dicom-viewer/public/test_images/stack2/IMG10.dcm diff --git a/dicom-viewer/public/test_images/stack2/IMG_108_lqSEYEH.dcm b/dicom-viewer/public/test_images/stack2/IMG11.dcm similarity index 100% rename from dicom-viewer/public/test_images/stack2/IMG_108_lqSEYEH.dcm rename to dicom-viewer/public/test_images/stack2/IMG11.dcm diff --git a/dicom-viewer/public/test_images/stack2/IMG_109_oHWP1w2.dcm b/dicom-viewer/public/test_images/stack2/IMG12.dcm similarity index 100% rename from dicom-viewer/public/test_images/stack2/IMG_109_oHWP1w2.dcm rename to dicom-viewer/public/test_images/stack2/IMG12.dcm diff --git a/dicom-viewer/public/test_images/stack2/IMG_110_f2ofJri.dcm b/dicom-viewer/public/test_images/stack2/IMG13.dcm similarity index 100% rename from dicom-viewer/public/test_images/stack2/IMG_110_f2ofJri.dcm rename to dicom-viewer/public/test_images/stack2/IMG13.dcm diff --git a/dicom-viewer/public/test_images/stack2/IMG_111_RBIHQnF.dcm b/dicom-viewer/public/test_images/stack2/IMG14.dcm similarity index 100% rename from dicom-viewer/public/test_images/stack2/IMG_111_RBIHQnF.dcm rename to dicom-viewer/public/test_images/stack2/IMG14.dcm diff --git a/dicom-viewer/public/test_images/stack2/IMG_112_67pUQi5.dcm b/dicom-viewer/public/test_images/stack2/IMG15.dcm similarity index 100% rename from dicom-viewer/public/test_images/stack2/IMG_112_67pUQi5.dcm rename to dicom-viewer/public/test_images/stack2/IMG15.dcm diff --git a/dicom-viewer/public/test_images/stack2/IMG_099_68DHkBt.dcm b/dicom-viewer/public/test_images/stack2/IMG2.dcm similarity index 100% rename from dicom-viewer/public/test_images/stack2/IMG_099_68DHkBt.dcm rename to dicom-viewer/public/test_images/stack2/IMG2.dcm diff --git a/dicom-viewer/public/test_images/stack2/IMG_100_ZWxOgvd.dcm b/dicom-viewer/public/test_images/stack2/IMG3.dcm similarity index 100% rename from dicom-viewer/public/test_images/stack2/IMG_100_ZWxOgvd.dcm rename to dicom-viewer/public/test_images/stack2/IMG3.dcm diff --git a/dicom-viewer/public/test_images/stack2/IMG_101_qjkip11.dcm b/dicom-viewer/public/test_images/stack2/IMG4.dcm similarity index 100% rename from dicom-viewer/public/test_images/stack2/IMG_101_qjkip11.dcm rename to dicom-viewer/public/test_images/stack2/IMG4.dcm diff --git a/dicom-viewer/public/test_images/stack2/IMG_102_NtkccoA.dcm b/dicom-viewer/public/test_images/stack2/IMG5.dcm similarity index 100% rename from dicom-viewer/public/test_images/stack2/IMG_102_NtkccoA.dcm rename to dicom-viewer/public/test_images/stack2/IMG5.dcm diff --git a/dicom-viewer/public/test_images/stack2/IMG_103_lteBAs3.dcm b/dicom-viewer/public/test_images/stack2/IMG6.dcm similarity index 100% rename from dicom-viewer/public/test_images/stack2/IMG_103_lteBAs3.dcm rename to dicom-viewer/public/test_images/stack2/IMG6.dcm diff --git a/dicom-viewer/public/test_images/stack2/IMG_104_Y9nkN3z.dcm b/dicom-viewer/public/test_images/stack2/IMG7.dcm similarity index 100% rename from dicom-viewer/public/test_images/stack2/IMG_104_Y9nkN3z.dcm rename to dicom-viewer/public/test_images/stack2/IMG7.dcm diff --git a/dicom-viewer/public/test_images/stack2/IMG_105_5NSbmJu.dcm b/dicom-viewer/public/test_images/stack2/IMG8.dcm similarity index 100% rename from dicom-viewer/public/test_images/stack2/IMG_105_5NSbmJu.dcm rename to dicom-viewer/public/test_images/stack2/IMG8.dcm diff --git a/dicom-viewer/public/test_images/stack2/IMG_106_tZLEq9Y.dcm b/dicom-viewer/public/test_images/stack2/IMG9.dcm similarity index 100% rename from dicom-viewer/public/test_images/stack2/IMG_106_tZLEq9Y.dcm rename to dicom-viewer/public/test_images/stack2/IMG9.dcm diff --git a/dicom-viewer/public/test_images/stack5/IMG1.dcm b/dicom-viewer/public/test_images/stack5/IMG1.dcm new file mode 100644 index 0000000..1aa62c8 Binary files /dev/null and b/dicom-viewer/public/test_images/stack5/IMG1.dcm differ diff --git a/dicom-viewer/public/test_images/stack5/IMG10.dcm b/dicom-viewer/public/test_images/stack5/IMG10.dcm new file mode 100644 index 0000000..1e7f923 Binary files /dev/null and b/dicom-viewer/public/test_images/stack5/IMG10.dcm differ diff --git a/dicom-viewer/public/test_images/stack5/IMG11.dcm b/dicom-viewer/public/test_images/stack5/IMG11.dcm new file mode 100644 index 0000000..69a618e Binary files /dev/null and b/dicom-viewer/public/test_images/stack5/IMG11.dcm differ diff --git a/dicom-viewer/public/test_images/stack5/IMG12.dcm b/dicom-viewer/public/test_images/stack5/IMG12.dcm new file mode 100644 index 0000000..64f2028 Binary files /dev/null and b/dicom-viewer/public/test_images/stack5/IMG12.dcm differ diff --git a/dicom-viewer/public/test_images/stack5/IMG13.dcm b/dicom-viewer/public/test_images/stack5/IMG13.dcm new file mode 100644 index 0000000..ae72f82 Binary files /dev/null and b/dicom-viewer/public/test_images/stack5/IMG13.dcm differ diff --git a/dicom-viewer/public/test_images/stack5/IMG14.dcm b/dicom-viewer/public/test_images/stack5/IMG14.dcm new file mode 100644 index 0000000..6085a4f Binary files /dev/null and b/dicom-viewer/public/test_images/stack5/IMG14.dcm differ diff --git a/dicom-viewer/public/test_images/stack5/IMG15.dcm b/dicom-viewer/public/test_images/stack5/IMG15.dcm new file mode 100644 index 0000000..decd7f9 Binary files /dev/null and b/dicom-viewer/public/test_images/stack5/IMG15.dcm differ diff --git a/dicom-viewer/public/test_images/stack5/IMG16.dcm b/dicom-viewer/public/test_images/stack5/IMG16.dcm new file mode 100644 index 0000000..88b3851 Binary files /dev/null and b/dicom-viewer/public/test_images/stack5/IMG16.dcm differ diff --git a/dicom-viewer/public/test_images/stack5/IMG17.dcm b/dicom-viewer/public/test_images/stack5/IMG17.dcm new file mode 100644 index 0000000..b2c5c09 Binary files /dev/null and b/dicom-viewer/public/test_images/stack5/IMG17.dcm differ diff --git a/dicom-viewer/public/test_images/stack5/IMG18.dcm b/dicom-viewer/public/test_images/stack5/IMG18.dcm new file mode 100644 index 0000000..32c01c3 Binary files /dev/null and b/dicom-viewer/public/test_images/stack5/IMG18.dcm differ diff --git a/dicom-viewer/public/test_images/stack5/IMG19.dcm b/dicom-viewer/public/test_images/stack5/IMG19.dcm new file mode 100644 index 0000000..ff1ce75 Binary files /dev/null and b/dicom-viewer/public/test_images/stack5/IMG19.dcm differ diff --git a/dicom-viewer/public/test_images/stack5/IMG2.dcm b/dicom-viewer/public/test_images/stack5/IMG2.dcm new file mode 100644 index 0000000..4fe1b5f Binary files /dev/null and b/dicom-viewer/public/test_images/stack5/IMG2.dcm differ diff --git a/dicom-viewer/public/test_images/stack5/IMG20.dcm b/dicom-viewer/public/test_images/stack5/IMG20.dcm new file mode 100644 index 0000000..67592ce Binary files /dev/null and b/dicom-viewer/public/test_images/stack5/IMG20.dcm differ diff --git a/dicom-viewer/public/test_images/stack5/IMG21.dcm b/dicom-viewer/public/test_images/stack5/IMG21.dcm new file mode 100644 index 0000000..381b823 Binary files /dev/null and b/dicom-viewer/public/test_images/stack5/IMG21.dcm differ diff --git a/dicom-viewer/public/test_images/stack5/IMG22.dcm b/dicom-viewer/public/test_images/stack5/IMG22.dcm new file mode 100644 index 0000000..7615ceb Binary files /dev/null and b/dicom-viewer/public/test_images/stack5/IMG22.dcm differ diff --git a/dicom-viewer/public/test_images/stack5/IMG23.dcm b/dicom-viewer/public/test_images/stack5/IMG23.dcm new file mode 100644 index 0000000..cb60970 Binary files /dev/null and b/dicom-viewer/public/test_images/stack5/IMG23.dcm differ diff --git a/dicom-viewer/public/test_images/stack5/IMG24.dcm b/dicom-viewer/public/test_images/stack5/IMG24.dcm new file mode 100644 index 0000000..151ae9e Binary files /dev/null and b/dicom-viewer/public/test_images/stack5/IMG24.dcm differ diff --git a/dicom-viewer/public/test_images/stack5/IMG25.dcm b/dicom-viewer/public/test_images/stack5/IMG25.dcm new file mode 100644 index 0000000..f32672e Binary files /dev/null and b/dicom-viewer/public/test_images/stack5/IMG25.dcm differ diff --git a/dicom-viewer/public/test_images/stack5/IMG3.dcm b/dicom-viewer/public/test_images/stack5/IMG3.dcm new file mode 100644 index 0000000..0504f0d Binary files /dev/null and b/dicom-viewer/public/test_images/stack5/IMG3.dcm differ diff --git a/dicom-viewer/public/test_images/stack5/IMG4.dcm b/dicom-viewer/public/test_images/stack5/IMG4.dcm new file mode 100644 index 0000000..9497bd1 Binary files /dev/null and b/dicom-viewer/public/test_images/stack5/IMG4.dcm differ diff --git a/dicom-viewer/public/test_images/stack5/IMG5.dcm b/dicom-viewer/public/test_images/stack5/IMG5.dcm new file mode 100644 index 0000000..a26528e Binary files /dev/null and b/dicom-viewer/public/test_images/stack5/IMG5.dcm differ diff --git a/dicom-viewer/public/test_images/stack5/IMG6.dcm b/dicom-viewer/public/test_images/stack5/IMG6.dcm new file mode 100644 index 0000000..717c56c Binary files /dev/null and b/dicom-viewer/public/test_images/stack5/IMG6.dcm differ diff --git a/dicom-viewer/public/test_images/stack5/IMG7.dcm b/dicom-viewer/public/test_images/stack5/IMG7.dcm new file mode 100644 index 0000000..5ecf8b6 Binary files /dev/null and b/dicom-viewer/public/test_images/stack5/IMG7.dcm differ diff --git a/dicom-viewer/public/test_images/stack5/IMG8.dcm b/dicom-viewer/public/test_images/stack5/IMG8.dcm new file mode 100644 index 0000000..be0c14e Binary files /dev/null and b/dicom-viewer/public/test_images/stack5/IMG8.dcm differ diff --git a/dicom-viewer/public/test_images/stack5/IMG9.dcm b/dicom-viewer/public/test_images/stack5/IMG9.dcm new file mode 100644 index 0000000..06ebbc3 Binary files /dev/null and b/dicom-viewer/public/test_images/stack5/IMG9.dcm differ diff --git a/dicom-viewer/src/App.tsx b/dicom-viewer/src/App.tsx index 1d4a6fe..bb44444 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 } from "@cornerstonejs/tools" +import { LengthTool, ProbeTool, ArrowAnnotateTool, RectangleROITool, EllipticalROITool, PlanarFreehandROITool, PlanarFreehandContourSegmentationTool, SculptorTool, CrosshairsTool, ReferenceLinesTool } 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,6 @@ const { IMAGE_RENDERED } = Enums.Events; const STACK_TOOL_GROUP_ID = 'STACK_TOOL_GROUP_ID'; const VOLUME_TOOL_GROUP_ID = 'VOLUME_TOOL_GROUP_ID'; -const toolGroupId = 'STACK_TOOL_GROUP_ID'; // Common CT presets (add more as needed) const WINDOW_LEVEL_PRESETS = [ @@ -87,6 +86,7 @@ function App() { const [loadedImageIds, setLoadedImageIds] = useState(null); const [crossReferenceEnabled, setCrossReferenceEnabled] = useState(true); + const [referenceLinesEnabled, setReferenceLinesEnabled] = useState(false); // Wrap setLoadedImageIds to also cache DICOM metadata const setLoadedImageIdsAndCacheMeta = async (imageIds: string[]) => { @@ -98,9 +98,9 @@ function App() { 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)); + //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); @@ -251,32 +251,36 @@ function App() { }; // Helper to apply all mouse tool bindings (normal or ctrl) - const applyMouseToolBindings = useCallback(() => { - const toolGroup = cornerstoneTools.ToolGroupManager.getToolGroup(toolGroupId) +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); + + [stackToolGroup, volumeToolGroup].forEach(toolGroup => { if (toolGroup) { // Set all tools to passive first, removing all bindings TOOL_OPTIONS.forEach(opt => { - toolGroup.setToolPassive(opt.value, { removeAllBindings: true }) - }) + toolGroup.setToolPassive(opt.value, { removeAllBindings: true }); + }); // Collect all bindings for each tool - const toolBindings: Record> = {} + const toolBindings: Record> = {}; // Normal bindings Object.entries(mouseToolBindings).forEach(([btn, tool]) => { - if (!toolBindings[tool]) toolBindings[tool] = [] - toolBindings[tool].push({ mouseButton: MouseBindings[btn] }) - }) + if (!toolBindings[tool]) toolBindings[tool] = []; + toolBindings[tool].push({ mouseButton: MouseBindings[btn] }); + }); // Ctrl bindings Object.entries(ctrlMouseToolBindings).forEach(([btn, tool]) => { - if (!toolBindings[tool]) toolBindings[tool] = [] - toolBindings[tool].push({ mouseButton: MouseBindings[btn], modifierKey: KeyboardBindings.Ctrl }) - }) + if (!toolBindings[tool]) toolBindings[tool] = []; + toolBindings[tool].push({ mouseButton: MouseBindings[btn], modifierKey: KeyboardBindings.Ctrl }); + }); // Apply all bindings for each tool in a single call Object.entries(toolBindings).forEach(([tool, bindings]) => { - toolGroup.setToolActive(tool, { bindings }) - }) + toolGroup.setToolActive(tool, { bindings }); + }); // Always re-apply the mouse wheel binding for StackScrollTool toolGroup.setToolActive(StackScrollTool.toolName, { @@ -285,9 +289,10 @@ function App() { mouseButton: MouseBindings.Wheel, // Wheel Mouse }, ], - }) + }); } - }, [mouseToolBindings, ctrlMouseToolBindings]) + }); +}, [mouseToolBindings, ctrlMouseToolBindings]); const [sideMenuOpen, setSideMenuOpen] = useState(false); @@ -353,6 +358,8 @@ function App() { }); }, []); + const [gridBtnActive, setGridBtnActive] = useState(false); + function saveVisibleViewportState() { for (let i = 0; i < viewportGrid.rows * viewportGrid.cols; i++) { const viewportId = `CT_${i}`; @@ -375,7 +382,9 @@ function App() { if (!running.current) { running.current = true; await csRenderInit(); - await csToolsInit(); + await csToolsInit( + { showSVGCursors: true, } + ); dicomImageLoaderInit({ maxWebWorkers: 1 }); } @@ -393,10 +402,51 @@ function App() { renderingEngineRef.current = renderingEngine; } - // 4. Setup tool group - const toolGroup = ToolGroupManager.getToolGroup(toolGroupId) || setupTools(); + // --- In your setup function, use the correct tool group per viewport --- + const { stackToolGroup, volumeToolGroup } = ToolGroupManager.getToolGroup(STACK_TOOL_GROUP_ID) + ? { stackToolGroup: ToolGroupManager.getToolGroup(STACK_TOOL_GROUP_ID), volumeToolGroup: ToolGroupManager.getToolGroup(VOLUME_TOOL_GROUP_ID) } + : setupTools(); + const visibleVolumeViewports = viewportModes + .slice(0, viewportGrid.rows * viewportGrid.cols) + .filter(mode => mode === 'volume').length; + + // After the viewport setup loop, outside the for-loop: + if ( + crossReferenceEnabled && + visibleVolumeViewports >= 2 + ) { + if (!volumeToolGroup.hasTool(CrosshairsTool.toolName)) { + volumeToolGroup.addTool(CrosshairsTool.toolName); + } + volumeToolGroup.setToolEnabled(CrosshairsTool.toolName); + volumeToolGroup.setToolActive(CrosshairsTool.toolName, { + bindings: [{ mouseButton: MouseBindings.Primary }], + }); + } else if (volumeToolGroup.hasTool(CrosshairsTool.toolName)) { + volumeToolGroup.setToolDisabled(CrosshairsTool.toolName); + } + + // --- In your setup function, after tool group creation, enable/disable ReferenceLinesTool for each tool group --- + if (referenceLinesEnabled) { + if (!stackToolGroup.hasTool(ReferenceLinesTool.toolName)) { + stackToolGroup.addTool(ReferenceLinesTool.toolName); + } + if (!volumeToolGroup.hasTool(ReferenceLinesTool.toolName)) { + volumeToolGroup.addTool(ReferenceLinesTool.toolName); + } + stackToolGroup.setToolEnabled(ReferenceLinesTool.toolName); + volumeToolGroup.setToolEnabled(ReferenceLinesTool.toolName); + } else { + if (stackToolGroup.hasTool(ReferenceLinesTool.toolName)) { + stackToolGroup.setToolDisabled(ReferenceLinesTool.toolName); + } + if (volumeToolGroup.hasTool(ReferenceLinesTool.toolName)) { + volumeToolGroup.setToolDisabled(ReferenceLinesTool.toolName); + } + } + // 6. Enable and configure each viewport for (let i = 0; i < MAX_VIEWPORTS; i++) { const viewportId = `CT_${i}`; @@ -408,6 +458,11 @@ function App() { ? Enums.ViewportType.ORTHOGRAPHIC // or Enums.ViewportType.VOLUME if supported : Enums.ViewportType.STACK; + // Choose tool group based on viewport type + const toolGroup = viewportModes[i] === 'volume' ? volumeToolGroup : stackToolGroup; + + + if (!element._cornerstoneEnabled || element._lastViewportType !== viewportType) { if (renderingEngine.getViewport(viewportId)) { renderingEngine.disableElement(viewportId); @@ -425,25 +480,13 @@ function App() { // Add tool group only once toolGroup.addViewport(viewportId, renderingEngineId); - // Inside your setup loop for each viewport - if (viewportModes[i] === 'volume' && crossReferenceEnabled) { - if (!toolGroup.hasTool(CrosshairsTool.toolName)) { - toolGroup.addTool(CrosshairsTool.toolName); - } - toolGroup.setToolEnabled(CrosshairsTool.toolName); - toolGroup.setToolActive(CrosshairsTool.toolName, { - bindings: [{ mouseButton: MouseBindings.Primary }], - }); - } else if (toolGroup.hasTool(CrosshairsTool.toolName)) { - toolGroup.setToolDisabled(CrosshairsTool.toolName); - } + const viewport = renderingEngine.getViewport(viewportId); if (!viewport) continue; if (!element._resizeObserver) { const resizeObserver = new window.ResizeObserver(() => { - console.log("Resize observer triggered for viewport", i); if (element.offsetWidth > 0 && element.offsetHeight > 0) { renderingEngine.resize(); // Always get the latest viewport instance @@ -467,6 +510,17 @@ 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; + }); + } + // Example: create and set a volume (you may need to adapt this for your data) const volumeId = 'myVolumeId'; const volume = await volumeLoader.createAndCacheVolume(volumeId, { imageIds }); @@ -635,79 +689,51 @@ function App() { if (!loadedImageIds || loadedImageIds.length === 0) return; if (!orderBySliceLocation) return; - // Helper to get slice location from metadata - const getSliceLocation = (imageId: string) => { - const meta = metaData.get('imagePlaneModule', imageId); - return meta && meta.sliceLocation !== undefined ? Number(meta.sliceLocation) : null; + let cancelled = false; + + 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; + } + + // 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 = 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 sortedIds = sorted.map(obj => obj.imageId); + const isSame = + loadedImageIds.length === sortedIds.length && + loadedImageIds.every((id, idx) => id === sortedIds[idx]); + + if (!isSame && !cancelled) { + setLoadedImageIds(sortedIds); + } }; - // Build array of { imageId, sliceLocation } - const withSliceLoc = loadedImageIds.map(id => ({ - imageId: id, - sliceLoc: getSliceLocation(id), - })); + checkAndSort(); - // Sort by slice location, fallback to name if missing - const sorted = [...withSliceLoc].sort((a, b) => { - if (a.sliceLoc !== null && b.sliceLoc !== null) { - return a.sliceLoc - b.sliceLoc; - } - return a.imageId.localeCompare(b.imageId); - }); - - // Only update if the order actually changed - const sortedIds = sorted.map(obj => obj.imageId); - const isSame = - loadedImageIds.length === sortedIds.length && - loadedImageIds.every((id, idx) => id === sortedIds[idx]); - - if (!isSame) { - setLoadedImageIds(sortedIds); - } - // eslint-disable-next-line - }, [orderBySliceLocation]); - - useEffect(() => { - if (!loadedImageIds || loadedImageIds.length === 0) return; - // Only trigger if orderBySliceLocation is enabled - if (!orderBySliceLocation) return; - - // Check if all images have metadata loaded - const allLoaded = loadedImageIds.every( - id => !!metaData.get("imagePlaneModule", id) - ); - if (!allLoaded) return; - - // Helper to get slice location from metadata - const getSliceLocation = (imageId: string) => { - const meta = metaData.get('imagePlaneModule', imageId); - return meta && meta.sliceLocation !== undefined ? Number(meta.sliceLocation) : null; - }; - - // Build array of { imageId, sliceLocation } - const withSliceLoc = loadedImageIds.map(id => ({ - imageId: id, - sliceLoc: getSliceLocation(id), - })); - - // Sort by slice location, fallback to name if missing - const sorted = [...withSliceLoc].sort((a, b) => { - if (a.sliceLoc !== null && b.sliceLoc !== null) { - return a.sliceLoc - b.sliceLoc; - } - return a.imageId.localeCompare(b.imageId); - }); - - // Only update if the order actually changed - const sortedIds = sorted.map(obj => obj.imageId); - const isSame = - loadedImageIds.length === sortedIds.length && - loadedImageIds.every((id, idx) => id === sortedIds[idx]); - - if (!isSame) { - setLoadedImageIds(sortedIds); - } - }, [orderBySliceLocation, loadedImageIds]); + return () => { cancelled = true; }; + }, [loadedImageIds, orderBySliceLocation]); const viewports = []; @@ -1215,28 +1241,70 @@ function App() { > {crossReferenceEnabled ? "Disable" : "Enable"} Cross Reference Lines + {/* Add more menu items here if needed */} {/* Viewport grid menu at top center */} + +
+ {/* Transparent hit area to catch hover/focus */} +
setGridBtnActive(true)} + onFocus={() => setGridBtnActive(true)} + tabIndex={-1} + /> {viewportMenuOpen && ( - // ...your grid menu dropdown...
{gridOptions.map(opt => ( @@ -1297,6 +1371,7 @@ function App() { )}
+ {/* Viewport grid (inside main div) */}
{/* Settings Modal (viewport-local, not fixed to page) */} - {settingsOpen && ( -
setSettingsOpen(false)} - > + { + settingsOpen && (
e.stopPropagation()} + onClick={() => setSettingsOpen(false)} > -
- Settings -
- {/* Settings content: Tool dropdowns */} -
-
Mouse Button Tool Bindings
- {MOUSE_BUTTONS.map((btn) => ( -
- - -
- ))} -
Ctrl + Mouse Button Tool Bindings
- {MOUSE_BUTTONS.map((btn) => ( -
- - -
- ))} -
- +
+ Settings +
+ {/* Settings content: Tool dropdowns */} +
+
Mouse Button Tool Bindings
+ {MOUSE_BUTTONS.map((btn) => ( +
+ + +
+ ))} +
Ctrl + Mouse Button Tool Bindings
+ {MOUSE_BUTTONS.map((btn) => ( +
+ + +
+ ))} +
+ +
-
- )} - {metaModalOpen && ( -
setMetaModalOpen(false)} - > + ) + } + { + metaModalOpen && (
e.stopPropagation()} + onClick={() => setMetaModalOpen(false)} > -
- DICOM Metadata -
- {metaModalContent} - +
+ DICOM Metadata +
+ {metaModalContent} + +
-
- )} + ) + } {/* ...rest of your UI... */} -
+ ) } @@ -1515,45 +1594,51 @@ function setupTools() { cornerstoneTools.addTool(PlanarFreehandContourSegmentationTool) cornerstoneTools.addTool(SculptorTool) cornerstoneTools.addTool(CrosshairsTool) + cornerstoneTools.addTool(ReferenceLinesTool); - // Create tool group ONCE - let toolGroup = ToolGroupManager.getToolGroup(toolGroupId) - if (!toolGroup) { - toolGroup = ToolGroupManager.createToolGroup(toolGroupId) - toolGroup.addTool(WindowLevelTool.toolName) - toolGroup.addTool(PanTool.toolName) - toolGroup.addTool(ZoomTool.toolName) - toolGroup.addTool(StackScrollTool.toolName, { loop: false }) - toolGroup.addTool(PlanarRotateTool.toolName) - toolGroup.addTool(LengthTool.toolName) - toolGroup.addTool(ArrowAnnotateTool.toolName, { - getTextCallback: () => '', - }) - toolGroup.addTool(ProbeTool.toolName, { - statsCalculator: () => null, - }) - toolGroup.addTool(RectangleROITool.toolName, { - calculateStats: false, - getTextCallback: () => '', - }) - toolGroup.addTool(EllipticalROITool.toolName, { - getTextCallback: () => '', - }) - toolGroup.addTool(PlanarFreehandROITool.toolName, { - calculateStats: false, - }) - toolGroup.addTool(PlanarFreehandContourSegmentationTool.toolName, {}) - toolGroup.addTool(SculptorTool.toolName, {}) + + // 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, {}); + // StackScrollTool mouse wheel binding + stackToolGroup.setToolActive(StackScrollTool.toolName, { + bindings: [{ mouseButton: MouseBindings.Wheel }], + }); } - // The Stack Scroll mouse wheel is a tool using the `mouseWheelCallback` - // and needs to be registered against the 'Wheel' binding. - toolGroup.setToolActive(StackScrollTool.toolName, { - bindings: [ - { - mouseButton: MouseBindings.Wheel, // Wheel Mouse - }, - ], - }) - return toolGroup + // 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(CrosshairsTool.toolName, {}); + } + + return { stackToolGroup, volumeToolGroup }; } diff --git a/dicom-viewer/src/index.css b/dicom-viewer/src/index.css index 9e15fb6..35181e9 100644 --- a/dicom-viewer/src/index.css +++ b/dicom-viewer/src/index.css @@ -9,7 +9,7 @@ html, body, #root { box-sizing: border-box; } -.grid-menu-hover-container .grid-menu-btn { +/* .grid-menu-hover-container .grid-menu-btn { opacity: 0; pointer-events: none; transition: opacity 0.2s; @@ -18,4 +18,5 @@ html, body, #root { .grid-menu-btn:focus { opacity: 1 !important; pointer-events: auto; -} \ No newline at end of file +} + */ \ No newline at end of file diff --git a/dicom-viewer/src/lib/createImageIds.ts b/dicom-viewer/src/lib/createImageIds.ts index 173af41..c23463a 100644 --- a/dicom-viewer/src/lib/createImageIds.ts +++ b/dicom-viewer/src/lib/createImageIds.ts @@ -63,13 +63,13 @@ import { readFile } from "dicom-parser" return imageIds } */ -const NUM_IMAGES = 152 // set to however many images you have +const NUM_IMAGES = 15 // set to however many images you have export default async function createImageIds() { const imageIds = [] for (let i = 1; i <= NUM_IMAGES; i++) { const fileName = `IMG${String(i)}.dcm` - imageIds.push(`wadouri:/test_images/stack4/${fileName}`) + imageIds.push(`wadouri:/test_images/stack2/${fileName}`) } return imageIds } \ No newline at end of file