From f1692380574d4ebb7ecacd0bfcaf1d5c9fd92452 Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 18 May 2026 11:51:38 +0100 Subject: [PATCH] Enhance viewer functionality with volume diagnostics and fullscreen toggle --- dicom-viewer/src/App.tsx | 305 ++++++++++++++++++++++++++++++++------- 1 file changed, 256 insertions(+), 49 deletions(-) diff --git a/dicom-viewer/src/App.tsx b/dicom-viewer/src/App.tsx index e4aeb8e..8c8a2c2 100644 --- a/dicom-viewer/src/App.tsx +++ b/dicom-viewer/src/App.tsx @@ -224,9 +224,11 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer const SIDE_MENU_WIDTH = 220; const SIDE_MENU_BUTTON_SIZE = 40; const SIDE_MENU_BUTTON_PEEK = 14; + const topControlsRef = useRef(null); const [fullscreenHoverIdx, setFullscreenHoverIdx] = useState(null); const [openDropdownIdx, setOpenDropdownIdx] = useState(null); + const [topControlsPeek, setTopControlsPeek] = useState(false); const MAX_VIEWPORTS = 9; const renderingEngineId = "renderingEngine_" + container_id; @@ -236,6 +238,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer const restoringStateRef = useRef(false); const [annotationNavEnabled, setAnnotationNavEnabled] = useState(true); + const [volumeDiagnosticsEnabled, setVolumeDiagnosticsEnabled] = useState(false); const [availableStacks, setAvailableStacks] = useState([]); const [activeViewport, setActiveViewport] = useState(0); @@ -992,6 +995,10 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer windowWidth: string, slicePosition: string, modality: string, + orientation: string, + resolution: string, + spacing: string, + focalPoint: string, }>>(() => Array(viewportGrid.rows * viewportGrid.cols).fill({ index: 0, @@ -1000,6 +1007,10 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer windowWidth: "", slicePosition: "", modality: "", + orientation: "", + resolution: "", + spacing: "", + focalPoint: "", }) ); @@ -1647,6 +1658,10 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer let windowWidth = ""; let slicePosition = ""; let modality = ""; + let orientation = ""; + let resolution = ""; + let spacing = ""; + let focalPoint = ""; if (viewportModes[viewportIdx] === "stack") { currentIndex = typeof viewport.getCurrentImageIdIndex === "function" @@ -1719,7 +1734,28 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer const firstImageId = viewportImageIds[viewportIdx]?.[0]; if (firstImageId) { const seriesMeta = metaData.get("generalSeriesModule", firstImageId) as any; + const planeMeta = metaData.get("imagePlaneModule", firstImageId) as any; + const pixelMeta = metaData.get("imagePixelModule", firstImageId) as any; modality = seriesMeta?.modality || ""; + orientation = String(viewport.viewportProperties?.orientation || "").toUpperCase(); + if (pixelMeta?.rows && pixelMeta?.columns) { + resolution = `${pixelMeta.columns} x ${pixelMeta.rows}`; + } + const spacingParts = [ + planeMeta?.columnPixelSpacing, + planeMeta?.rowPixelSpacing, + planeMeta?.sliceThickness, + ].filter((value) => value !== undefined && value !== null && value !== ""); + if (spacingParts.length > 0) { + spacing = spacingParts.join(" x "); + } + } + + if (typeof viewport.getCamera === "function") { + const camera = viewport.getCamera(); + if (camera?.focalPoint && Array.isArray(camera.focalPoint)) { + focalPoint = camera.focalPoint.map((value: number) => value.toFixed(1)).join(", "); + } } } @@ -1732,6 +1768,10 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer windowWidth, slicePosition, // add this field modality, + orientation, + resolution, + spacing, + focalPoint, }; return next; }); @@ -2301,6 +2341,21 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer } }, [updateOverlay, viewportImageIds]); + const toggleViewerFullscreen = useCallback(() => { + const root = appRootRef.current; + if (!root) return; + + if (document.fullscreenElement === root) { + document.exitFullscreen(); + } else if (root.requestFullscreen) { + root.requestFullscreen(); + } else if ((root as any).webkitRequestFullscreen) { + (root as any).webkitRequestFullscreen(); + } else if ((root as any).msRequestFullscreen) { + (root as any).msRequestFullscreen(); + } + }, []); + // Update overlay and preset state arrays when grid changes useEffect(() => { setImageInfos(Array(viewportGrid.rows * viewportGrid.cols).fill({ @@ -2310,6 +2365,10 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer windowWidth: "", slicePosition: "", modality: "", + orientation: "", + resolution: "", + spacing: "", + focalPoint: "", })); setPresetsOpenArr(Array(viewportGrid.rows * viewportGrid.cols).fill(false)); }, [viewportGrid]); @@ -2332,6 +2391,21 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer return () => document.removeEventListener("mousedown", handleClick); }, [presetsOpenArr]); + useEffect(() => { + if (!viewportMenuOpen) return; + + const handleOutsideClick = (e: MouseEvent) => { + const host = topControlsRef.current; + if (host && !host.contains(e.target as Node)) { + setViewportMenuOpen(false); + setTopControlsPeek(false); + } + }; + + document.addEventListener("mousedown", handleOutsideClick); + return () => document.removeEventListener("mousedown", handleOutsideClick); + }, [viewportMenuOpen]); + useEffect(() => { const preventExtraButtonDefault = (e: MouseEvent) => { // 4th button: button === 3, 5th button: button === 4 @@ -3725,6 +3799,30 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer )}
WC: {imageInfos[i]?.windowCenter}   WW: {imageInfos[i]?.windowWidth} + {volumeDiagnosticsEnabled && ( + <> +
+ Ori: {imageInfos[i]?.orientation || "N/A"} + {imageInfos[i]?.resolution && ( + <> +
+ Res: {imageInfos[i]?.resolution} + + )} + {imageInfos[i]?.spacing && ( + <> +
+ Spacing: {imageInfos[i]?.spacing} + + )} + {imageInfos[i]?.focalPoint && ( + <> +
+ Pos: {imageInfos[i]?.focalPoint} + + )} + + )} )} @@ -4582,6 +4680,23 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer > {annotationNavEnabled ? "Hide" : "Show"} Annotation Navigation + {/* Add more menu items here if needed */} @@ -4592,88 +4707,180 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
setTopControlsPeek(true)} + onMouseLeave={() => { + if (!viewportMenuOpen) setTopControlsPeek(false); }} > - {viewportMenuOpen && ( -
+ - ))} -
- )} + {Array.from({ length: viewportGrid.rows * viewportGrid.cols }).map((_, idx) => ( + + ))} + + {viewportGrid.rows}x{viewportGrid.cols} + + {viewportMenuOpen ? "▲" : "▼"} + + {viewportMenuOpen && ( +
e.stopPropagation()} + > + {gridOptions.map(opt => ( + + ))} +
+ )} +