diff --git a/dicom-viewer/src/App.tsx b/dicom-viewer/src/App.tsx index eec1674..1d4a6fe 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 } from "@cornerstonejs/tools" +import { LengthTool, ProbeTool, ArrowAnnotateTool, RectangleROITool, EllipticalROITool, PlanarFreehandROITool, PlanarFreehandContourSegmentationTool, SculptorTool, CrosshairsTool } 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'; @@ -22,6 +22,8 @@ const { MouseBindings, KeyboardBindings } = csToolsEnums; 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) @@ -84,6 +86,8 @@ function App() { // Store imageIds globally after loading so they can be reused const [loadedImageIds, setLoadedImageIds] = useState(null); + const [crossReferenceEnabled, setCrossReferenceEnabled] = useState(true); + // Wrap setLoadedImageIds to also cache DICOM metadata const setLoadedImageIdsAndCacheMeta = async (imageIds: string[]) => { for (const imageId of imageIds) { @@ -392,6 +396,7 @@ function App() { // 4. Setup tool group const toolGroup = ToolGroupManager.getToolGroup(toolGroupId) || setupTools(); + // 6. Enable and configure each viewport for (let i = 0; i < MAX_VIEWPORTS; i++) { const viewportId = `CT_${i}`; @@ -420,65 +425,48 @@ 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 + const vp = renderingEngine.getViewport(viewportId); + if (vp && typeof vp.resize === "function") { + vp.resize(); + } + if (vp && typeof vp.render === "function") { + vp.render(); + } + } + }); + resizeObserver.observe(element); + element._resizeObserver = resizeObserver; + } + if (viewportModes[i] === 'stack') { if (imageIds) { viewport.setStack(imageIds); } // Restore scroll, properties, etc. } else if (viewportModes[i] === 'volume') { - //// Ensure all images are loaded before proceeding - //const notLoaded = imageIds.filter(id => !metaData.get('imagePlaneModule', id)); - //if (notLoaded.length > 0) { - // // Force metadata loading for missing images - // console.log("Forcing metadata loading for missing images:", notLoaded); - // await Promise.all( - // notLoaded.map(async id => { - // try { - // // Remove "wadouri:" prefix to get the URL - // const url = id.startsWith("wadouri:") ? id.slice(8) : id; - // if (cornerstoneDICOMImageLoader.wadouri.dataSetCacheManager) { - // cornerstoneDICOMImageLoader.wadouri.dataSetCacheManager.load(url) - // } else { - // console.warn("MetaDataManager not found. Unable to cache metadata."); - // } - // } catch (e) { - // console.warn(`Failed to force-load metadata for ${id}:`, e); - // } - // }) - // ); - //} - //// After forcing, check again - //const stillNotLoaded = imageIds.filter(id => !metaData.get('imagePlaneModule', id)); - //if (stillNotLoaded.length > 0) { - // alert("Some images could not be loaded for volume rendering."); - // setViewportModes(prev => { - // const next = [...prev]; - // next[i] = 'stack'; - // return next; - // }); - // continue; - //} - //// Check metadata for all imageIds - //const missingMeta = imageIds.filter(id => { - // const meta = metaData.get('imagePlaneModule', id); - // return !(meta && meta.imagePositionPatient && meta.imageOrientationPatient && meta.pixelSpacing); - //}); - //if (missingMeta.length > 0) { - // console.warn( - // "The following imageIds are missing required DICOM metadata for volume rendering:", - // missingMeta - // ); - // setViewportModes(prev => { - // const next = [...prev]; - // next[i] = 'stack'; - // return next; - // }); - // alert("Some images are missing required DICOM metadata for volume rendering."); - // continue; - //} + // 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 }); @@ -531,18 +519,7 @@ function App() { }, 100); } - // Attach ResizeObserver to handle dynamic resizing (only once) - if (!element._resizeObserver) { - const resizeObserver = new window.ResizeObserver(() => { - if (element.offsetWidth > 0 && element.offsetHeight > 0) { - renderingEngine.resize(); - viewport.resize(); - viewport.render(); - } - }); - resizeObserver.observe(element); - element._resizeObserver = resizeObserver; - } + // Always (re-)attach overlay update for each viewport if (element._overlayListener) { @@ -884,12 +861,21 @@ function App() { userSelect: "none", }} > -
- Image: {imageInfos[i]?.index} / {imageInfos[i]?.total} -
-
- WC: {imageInfos[i]?.windowCenter}   WW: {imageInfos[i]?.windowWidth} -
+ {viewportModes[i] === "stack" ? ( + <> +
+ Image: {imageInfos[i]?.index} / {imageInfos[i]?.total} +
+
+ WC: {imageInfos[i]?.windowCenter}   WW: {imageInfos[i]?.windowWidth} +
+ + ) : ( + // Volume mode: do not show image position +
+ WC: {imageInfos[i]?.windowCenter}   WW: {imageInfos[i]?.windowWidth} +
+ )} {/* Preset menu (bottom right of viewport) */} @@ -978,6 +964,102 @@ function App() { )} + {/* Position bar inside each viewport */} + {viewportModes[i] === "stack" && loadedImageIds && ( +
0 ? "flex" : "none", + alignItems: "flex-start", + justifyContent: "center", + pointerEvents: "none", + flexDirection: "column", + padding: 0, + }} + > +
+ {loadedImageIds.map((id, idx) => { + const loaded = !!metaData.get("imagePlaneModule", id); + const renderingEngine = renderingEngineRef.current; + let currentIdx = -1; + if ( + renderingEngine && + typeof renderingEngine.getViewport === "function" + ) { + const viewport = renderingEngine.getViewport(`CT_${i}`); + if (viewport && typeof viewport.getCurrentImageIdIndex === "function") { + currentIdx = viewport.getCurrentImageIdIndex(); + } + } + const isCurrent = idx === currentIdx; + return ( +
{ + const renderingEngine = renderingEngineRef.current; + const viewport = renderingEngine?.getViewport(`CT_${i}`); + if ( + viewport && + typeof viewport.setImageIdIndex === "function" && + viewportModes[i] === "stack" + ) { + viewport.setImageIdIndex(idx); + viewport.render(); + updateOverlay(i, viewport); + } + }} + /> + ); + })} +
+
+ )}
); } @@ -1116,6 +1198,23 @@ function App() { > Show DICOM Metadata + {/* Add more menu items here if needed */} @@ -1215,112 +1314,7 @@ function App() { > {viewports} - {/* Per-viewport status bars, positioned inside each viewport */} - {Array.from({ length: viewportGrid.rows * viewportGrid.cols }).map((_, vpIdx) => { - // Only show for visible viewports - if (vpIdx >= loadedImageIds?.length) return null; - // Get imageIds for this viewport (all viewports share the same stack, adapt if needed) - const imageIds = loadedImageIds || []; - // Get current image index for this viewport - let currentIdx = -1; - const renderingEngine = renderingEngineRef.current; - if ( - renderingEngine && - typeof renderingEngine.getViewport === "function" && - viewportModes[vpIdx] === "stack" - ) { - const viewport = renderingEngine.getViewport(`CT_${vpIdx}`); - if (viewport && typeof viewport.getCurrentImageIdIndex === "function") { - currentIdx = viewport.getCurrentImageIdIndex(); - } - } - // Position the bar absolutely inside the viewport grid cell - // Calculate row and column for this viewport - const row = Math.floor(vpIdx / viewportGrid.cols) + 1; - const col = (vpIdx % viewportGrid.cols) + 1; - return ( -
0 && viewportModes[vpIdx] === "stack" ? "flex" : "none", - alignItems: "flex-start", - justifyContent: "center", - pointerEvents: "none", - flexDirection: "column", - padding: "8px 0", - }} - > -
- {imageIds.map((id, idx) => { - const loaded = !!metaData.get("imagePlaneModule", id); - const isCurrent = idx === currentIdx; - return ( -
{ - const renderingEngine = renderingEngineRef.current; - const viewport = renderingEngine?.getViewport(`CT_${vpIdx}`); - if ( - viewport && - typeof viewport.setImageIdIndex === "function" && - viewportModes[vpIdx] === "stack" - ) { - viewport.setImageIdIndex(idx); - viewport.render(); - updateOverlay(vpIdx, viewport); - } - }} - /> - ); - })} -
-
- ); - })} +
@@ -1520,6 +1514,7 @@ function setupTools() { cornerstoneTools.addTool(PlanarFreehandROITool) cornerstoneTools.addTool(PlanarFreehandContourSegmentationTool) cornerstoneTools.addTool(SculptorTool) + cornerstoneTools.addTool(CrosshairsTool) // Create tool group ONCE let toolGroup = ToolGroupManager.getToolGroup(toolGroupId)