Enhance viewer functionality with volume diagnostics and fullscreen toggle

This commit is contained in:
Ross
2026-05-18 11:51:38 +01:00
parent f0e0fe0ef6
commit f169238057
+256 -49
View File
@@ -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<HTMLDivElement | null>(null);
const [fullscreenHoverIdx, setFullscreenHoverIdx] = useState<number | null>(null);
const [openDropdownIdx, setOpenDropdownIdx] = useState<number | null>(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<StackEntry[]>([]);
const [activeViewport, setActiveViewport] = useState<number | null>(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
)}
<br />
WC: {imageInfos[i]?.windowCenter} &nbsp; WW: {imageInfos[i]?.windowWidth}
{volumeDiagnosticsEnabled && (
<>
<br />
Ori: {imageInfos[i]?.orientation || "N/A"}
{imageInfos[i]?.resolution && (
<>
<br />
Res: {imageInfos[i]?.resolution}
</>
)}
{imageInfos[i]?.spacing && (
<>
<br />
Spacing: {imageInfos[i]?.spacing}
</>
)}
{imageInfos[i]?.focalPoint && (
<>
<br />
Pos: {imageInfos[i]?.focalPoint}
</>
)}
</>
)}
</div>
)}
</div>
@@ -4582,6 +4680,23 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
>
{annotationNavEnabled ? "Hide" : "Show"} Annotation Navigation
</button>
<button
style={{
padding: "10px 18px",
background: volumeDiagnosticsEnabled ? "#1976d2" : "#333",
color: "#fff",
border: "none",
borderRadius: "4px",
fontWeight: "bold",
cursor: "pointer",
fontSize: "15px",
marginBottom: "18px",
width: "100%",
}}
onClick={() => setVolumeDiagnosticsEnabled((prev) => !prev)}
>
{volumeDiagnosticsEnabled ? "Disable" : "Enable"} Volume Diagnostics
</button>
{/* Add more menu items here if needed */}
</div>
@@ -4592,88 +4707,180 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
<div
ref={topControlsRef}
className="grid-menu-hover-container"
style={{
position: "absolute",
top: 8,
top: viewportMenuOpen || topControlsPeek ? 8 : -18,
left: "50%",
transform: "translateX(-50%)",
zIndex: 100,
display: "flex",
flexDirection: "row",
alignItems: "center",
alignItems: "flex-start",
gap: 8,
width: "auto",
height: "40px",
pointerEvents: "auto",
transition: "top 180ms ease",
}}
onMouseEnter={() => setTopControlsPeek(true)}
onMouseLeave={() => {
if (!viewportMenuOpen) setTopControlsPeek(false);
}}
>
<button
className="grid-menu-btn"
style={{
padding: "6px 16px",
height: 40,
padding: "6px 12px 6px 10px",
background: "#222",
color: "#fff",
border: "none",
borderRadius: "4px",
borderRadius: "0 0 4px 4px",
fontWeight: "bold",
cursor: "pointer",
fontSize: "15px",
marginRight: "8px",
fontSize: "14px",
boxShadow: "0 2px 8px rgba(0,0,0,0.2)",
pointerEvents: "auto",
zIndex: 2,
display: "inline-flex",
alignItems: "center",
gap: 8,
}}
title="Toggle viewer fullscreen"
onFocus={() => setTopControlsPeek(true)}
onClick={() => {
setTopControlsPeek(true);
toggleViewerFullscreen();
}}
onClick={() => setViewportMenuOpen(open => !open)}
tabIndex={0}
>
Grid: {viewportGrid.rows}x{viewportGrid.cols}
<span style={{ fontSize: 16, lineHeight: 1 }}></span>
<span>Viewer</span>
</button>
{viewportMenuOpen && (
<div
<div style={{ position: "relative", display: "flex", flexDirection: "column", alignItems: "stretch" }}>
<button
className="grid-menu-btn"
style={{
position: "absolute",
top: "110%",
left: "50%",
transform: "translateX(-50%)",
height: 40,
padding: "6px 14px 6px 10px",
background: "#222",
color: "#fff",
borderRadius: "6px",
boxShadow: "0 2px 12px rgba(0,0,0,0.4)",
padding: "12px",
display: "grid",
gridTemplateColumns: "repeat(3, 40px)",
gap: "8px",
zIndex: 200,
pointerEvents: "auto",
border: "none",
borderRadius: "0 0 4px 4px",
fontWeight: "bold",
cursor: "pointer",
fontSize: "14px",
boxShadow: "0 2px 8px rgba(0,0,0,0.2)",
display: "inline-flex",
alignItems: "center",
gap: 10,
justifyContent: "space-between",
minWidth: 136,
}}
onFocus={() => setTopControlsPeek(true)}
onClick={() => {
setTopControlsPeek(true);
setViewportMenuOpen(open => !open);
}}
tabIndex={0}
>
{gridOptions.map(opt => (
<button
key={`${opt.rows}x${opt.cols}`}
<span style={{ display: "inline-flex", alignItems: "center", gap: 8 }}>
<span
style={{
width: "40px",
height: "40px",
background: (opt.rows === viewportGrid.rows && opt.cols === viewportGrid.cols) ? "#444" : "#333",
color: "#fff",
border: "none",
borderRadius: "4px",
fontWeight: "bold",
cursor: "pointer",
fontSize: "15px",
}}
onClick={() => {
saveVisibleViewportState();
setViewportGrid({ rows: opt.rows, cols: opt.cols })
setViewportMenuOpen(false)
display: "grid",
gridTemplateColumns: `repeat(${viewportGrid.cols}, 5px)`,
gridTemplateRows: `repeat(${viewportGrid.rows}, 5px)`,
gap: 2,
padding: 1,
}}
>
{opt.rows}x{opt.cols}
</button>
))}
</div>
)}
{Array.from({ length: viewportGrid.rows * viewportGrid.cols }).map((_, idx) => (
<span
key={idx}
style={{
width: 5,
height: 5,
borderRadius: 1,
background: "#90caf9",
display: "block",
}}
/>
))}
</span>
<span>{viewportGrid.rows}x{viewportGrid.cols}</span>
</span>
<span style={{ opacity: 0.8 }}>{viewportMenuOpen ? "▲" : "▼"}</span>
</button>
{viewportMenuOpen && (
<div
style={{
position: "absolute",
top: "calc(100% + 8px)",
right: 0,
background: "#222",
color: "#fff",
borderRadius: "6px",
boxShadow: "0 2px 12px rgba(0,0,0,0.4)",
padding: "12px",
display: "grid",
gridTemplateColumns: "repeat(3, minmax(68px, 1fr))",
gap: "8px",
zIndex: 200,
pointerEvents: "auto",
}}
onMouseDown={(e) => e.stopPropagation()}
>
{gridOptions.map(opt => (
<button
key={`${opt.rows}x${opt.cols}`}
style={{
minWidth: "68px",
height: "54px",
background: (opt.rows === viewportGrid.rows && opt.cols === viewportGrid.cols) ? "#444" : "#333",
color: "#fff",
border: "none",
borderRadius: "4px",
fontWeight: "bold",
cursor: "pointer",
fontSize: "13px",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
gap: 6,
}}
onClick={() => {
saveVisibleViewportState();
setViewportGrid({ rows: opt.rows, cols: opt.cols })
setViewportMenuOpen(false)
setTopControlsPeek(false)
}}
>
<span
style={{
display: "grid",
gridTemplateColumns: `repeat(${opt.cols}, 6px)`,
gridTemplateRows: `repeat(${opt.rows}, 6px)`,
gap: 2,
}}
>
{Array.from({ length: opt.rows * opt.cols }).map((_, idx) => (
<span
key={idx}
style={{
width: 6,
height: 6,
borderRadius: 1,
background: "#90caf9",
display: "block",
}}
/>
))}
</span>
<span>{opt.rows}x{opt.cols}</span>
</button>
))}
</div>
)}
</div>
</div>