Enhance viewer functionality with volume diagnostics and fullscreen toggle
This commit is contained in:
+256
-49
@@ -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} 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>
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user