basic viewport save and restore fnuctiontality

This commit is contained in:
Ross
2025-05-31 21:29:37 +01:00
parent 40be13b51a
commit 313c7a9e9a
2 changed files with 749 additions and 431 deletions
+38
View File
@@ -12,6 +12,7 @@
<input type="file" id="dicomInput" multiple /> <input type="file" id="dicomInput" multiple />
<button onclick="sendToViewer()">Send to Viewer</button> <button onclick="sendToViewer()">Send to Viewer</button>
<button onclick="testLoadWadouriStack()">Test Load wadouri Stack</button>
<script> <script>
function sendToViewer() { function sendToViewer() {
const files = document.getElementById('dicomInput').files; const files = document.getElementById('dicomInput').files;
@@ -21,6 +22,43 @@ function sendToViewer() {
alert("Viewer API not ready"); alert("Viewer API not ready");
} }
} }
function testLoadWadouriStack() {
const urls = [
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_757_XQQyovo.dcm",
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_766_8fWI0Qc.dcm",
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_774_c8IpxUu.dcm",
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_758_6WKI7ws.dcm",
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_767_TduVOb8.dcm",
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_775_7zWneQ0.dcm",
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_759_BtKzgS5.dcm",
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_768_RdvtM8K.dcm",
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_776_LhVY3Ty.dcm",
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_760_a8CiR4J.dcm",
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_769_YHyJ1l6.dcm",
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_777_8IFYJmL.dcm",
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_761_vyvO1EN.dcm",
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_770_vQwi4LW.dcm",
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_778_BwGAMOR.dcm",
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_762_DhSgQsm.dcm",
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_771_xifitCl.dcm",
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_779_LyA1zYn.dcm",
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_763_aVjff3H.dcm",
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_772_cDQZtxQ.dcm",
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_780_qaZqsqg.dcm",
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_764_dDoeldL.dcm",
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_773_93Hvedg.dcm",
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_781_bEx9Owg.dcm",
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_765_qmd5mYJ.dcm"
];
const wadouriList = urls.map(url => "wadouri:" + url);
if (window.loadDicomStackFromWadouriList) {
window.loadDicomStackFromWadouriList(wadouriList);
} else {
alert("Viewer API not ready");
}
}
</script> </script>
</body> </body>
+312 -32
View File
@@ -24,6 +24,16 @@ declare global {
} }
} }
// Add to global types:
declare global {
interface Window {
exportViewerState?: () => string;
importViewerState?: (json: string) => void;
exportAnnotations?: () => string;
importAnnotations?: (json: string) => void;
}
}
type StackEntry = { imageIds: string[], studyInstanceUID?: string }; type StackEntry = { imageIds: string[], studyInstanceUID?: string };
// Add this type for clarity // Add this type for clarity
@@ -33,6 +43,12 @@ declare global {
} }
} }
declare global {
interface Window {
loadDicomStackFromWadouriList?: (wadouriImageIds: string[]) => void;
}
}
window.cornerstoneTools = cornerstoneTools; window.cornerstoneTools = cornerstoneTools;
@@ -110,8 +126,8 @@ function App() {
const [settingsOpen, setSettingsOpen] = useState(false); const [settingsOpen, setSettingsOpen] = useState(false);
const [fullscreenHoverIdx, setFullscreenHoverIdx] = useState<number | null>(null); const [fullscreenHoverIdx, setFullscreenHoverIdx] = useState<number | null>(null);
const [openDropdownIdx, setOpenDropdownIdx] = useState<number | null>(null); const [openDropdownIdx, setOpenDropdownIdx] = useState<number | null>(null);
// State for overlay info // State for overlay info
const [imageInfo, setImageInfo] = useState({ const [imageInfo, setImageInfo] = useState({
index: 0, index: 0,
@@ -124,9 +140,10 @@ const [openDropdownIdx, setOpenDropdownIdx] = useState<number | null>(null);
const renderingEngineId = "mainRenderingEngine"; const renderingEngineId = "mainRenderingEngine";
const renderingEngineRef = useRef<any>(null); const renderingEngineRef = useRef<any>(null);
const restoringStateRef = useRef(false);
const [availableStacks, setAvailableStacks] = useState<StackEntry[]>([]); const [availableStacks, setAvailableStacks] = useState<StackEntry[]>([]);
const [activeViewport, setActiveViewport] = useState<number | null>(0); const [activeViewport, setActiveViewport] = useState<number | null>(0);
const [viewportImageIds, setViewportImageIds] = useState<string[][]>( const [viewportImageIds, setViewportImageIds] = useState<string[][]>(
() => Array(MAX_VIEWPORTS).fill([]) () => Array(MAX_VIEWPORTS).fill([])
@@ -135,8 +152,9 @@ const [availableStacks, setAvailableStacks] = useState<StackEntry[]>([]);
() => Array(MAX_VIEWPORTS).fill(0) () => Array(MAX_VIEWPORTS).fill(0)
); );
// Load available stacks on mount // Load available stacks on mount
useEffect(() => { useEffect(() => {
availableImageIds().then(async stacks => { availableImageIds().then(async stacks => {
// Try to extract StudyInstanceUID for each stack // Try to extract StudyInstanceUID for each stack
const stackEntries: StackEntry[] = await Promise.all( const stackEntries: StackEntry[] = await Promise.all(
@@ -163,7 +181,7 @@ useEffect(() => {
setLoadedImageIdsAndCacheMeta(stacks[0]); setLoadedImageIdsAndCacheMeta(stacks[0]);
setViewportImageIds(Array(MAX_VIEWPORTS).fill(stacks[0] || [])); setViewportImageIds(Array(MAX_VIEWPORTS).fill(stacks[0] || []));
}); });
}, []); }, []);
const handleLoadStack = async (viewportIdx: number, stackIdx: number) => { const handleLoadStack = async (viewportIdx: number, stackIdx: number) => {
const stackObj = availableStacks[stackIdx]; const stackObj = availableStacks[stackIdx];
@@ -536,7 +554,7 @@ useEffect(() => {
const fileInputRef = useRef<HTMLInputElement | null>(null); const fileInputRef = useRef<HTMLInputElement | null>(null);
// Handler for file input change // Handler for file input change
const handleFilesSelected = async (event: React.ChangeEvent<HTMLInputElement> | { target: { files: File[] | FileList } }) => { const handleFilesSelected = async (event: React.ChangeEvent<HTMLInputElement> | { target: { files: File[] | FileList } }) => {
const files = event.target.files; const files = event.target.files;
if (!files || files.length === 0) return; if (!files || files.length === 0) return;
@@ -544,8 +562,8 @@ const handleFilesSelected = async (event: React.ChangeEvent<HTMLInputElement> |
const fileArray = Array.from(files).sort((a, b) => a.name.localeCompare(b.name)); const fileArray = Array.from(files).sort((a, b) => a.name.localeCompare(b.name));
let studyInstanceUID: string | undefined = undefined; let studyInstanceUID: string | undefined = undefined;
const arrayBuffer = await fileArray[0].arrayBuffer(); const arrayBuffer = await fileArray[0].arrayBuffer();
const dataSet = dicomParser.parseDicom(new Uint8Array(arrayBuffer)); const dataSet = dicomParser.parseDicom(new Uint8Array(arrayBuffer));
studyInstanceUID = dataSet.string('x0020000d'); studyInstanceUID = dataSet.string('x0020000d');
console.log("StudyInstanceUID:", studyInstanceUID); console.log("StudyInstanceUID:", studyInstanceUID);
@@ -568,7 +586,7 @@ const dataSet = dicomParser.parseDicom(new Uint8Array(arrayBuffer));
setLoadedImageIdsAndCacheMeta(imageIds); setLoadedImageIdsAndCacheMeta(imageIds);
return next; return next;
}); });
}; };
const handleToolChange = (mouseButton: string, toolName: string) => { const handleToolChange = (mouseButton: string, toolName: string) => {
@@ -629,6 +647,8 @@ const dataSet = dicomParser.parseDicom(new Uint8Array(arrayBuffer));
useEffect(() => { useEffect(() => {
const setup = async () => { const setup = async () => {
console.log("setup")
// 1. Initialize Cornerstone3D and tools ONCE // 1. Initialize Cornerstone3D and tools ONCE
if (!running.current) { if (!running.current) {
running.current = true; running.current = true;
@@ -719,7 +739,7 @@ const dataSet = dicomParser.parseDicom(new Uint8Array(arrayBuffer));
} }
if (viewportModes[i] === 'stack') { if (viewportModes[i] === 'stack') {
if (imageIds) { if (imageIds && !restoringStateRef.current) {
viewport.setStack(imageIds); viewport.setStack(imageIds);
} }
// CrosshairsTool is not applicable for stack viewports // CrosshairsTool is not applicable for stack viewports
@@ -754,6 +774,7 @@ const dataSet = dicomParser.parseDicom(new Uint8Array(arrayBuffer));
} }
viewport.render(); viewport.render();
if (!restoringStateRef.current) {
// Restore previous scroll position for existing viewports only // Restore previous scroll position for existing viewports only
if ( if (
prevScrollIndices.current[i] !== undefined && prevScrollIndices.current[i] !== undefined &&
@@ -782,11 +803,13 @@ const dataSet = dicomParser.parseDicom(new Uint8Array(arrayBuffer));
propsToRestore.colormap = colormap; propsToRestore.colormap = colormap;
} }
setTimeout(() => { setTimeout(() => {
console.log("Restoring properties (timeout) for viewport", i, propsToRestore);
viewport.setProperties(propsToRestore); viewport.setProperties(propsToRestore);
viewport.render() viewport.render()
}, 100); }, 100);
} }
}
// Always (re-)attach overlay update for each viewport // Always (re-)attach overlay update for each viewport
@@ -868,6 +891,8 @@ const dataSet = dicomParser.parseDicom(new Uint8Array(arrayBuffer));
const viewport = renderingEngine.getViewport(viewportId); const viewport = renderingEngine.getViewport(viewportId);
if (viewport && typeof viewport.resetCamera === "function") { if (viewport && typeof viewport.resetCamera === "function") {
viewport.resetCamera(); viewport.resetCamera();
console.log("Restoring viewport properties for viewport", viewportIdx);
viewport.resetProperties(); // Resets window/level and other properties viewport.resetProperties(); // Resets window/level and other properties
viewport.render(); viewport.render();
} }
@@ -881,6 +906,7 @@ const dataSet = dicomParser.parseDicom(new Uint8Array(arrayBuffer));
if (renderingEngine) { if (renderingEngine) {
const viewport = renderingEngine.getViewport(viewportId); const viewport = renderingEngine.getViewport(viewportId);
if (viewport && typeof viewport.setProperties === "function") { if (viewport && typeof viewport.setProperties === "function") {
console.log(`Applying preset to viewport ${viewportIdx}: center=${center}, width=${width}`);
viewport.setProperties({ viewport.setProperties({
voiRange: { voiRange: {
lower: center - width / 2, lower: center - width / 2,
@@ -948,6 +974,50 @@ const dataSet = dicomParser.parseDicom(new Uint8Array(arrayBuffer));
} }
}, []); }, []);
useEffect(() => {
// API: Load a stack from a list of wadouri imageIds (strings)
window.loadDicomStackFromWadouriList = async (wadouriImageIds: string[]) => {
if (!wadouriImageIds || wadouriImageIds.length === 0) return;
// Try to extract StudyInstanceUID from the first image if possible
let studyInstanceUID: string | undefined = undefined;
try {
const firstId = wadouriImageIds[0];
if (firstId && firstId.startsWith("wadouri:")) {
const url = firstId.slice(8);
const response = await fetch(url);
const arrayBuffer = await response.arrayBuffer();
const dataSet = dicomParser.parseDicom(new Uint8Array(arrayBuffer));
studyInstanceUID = dataSet.string('x0020000d');
}
} catch (e) {
// Ignore errors, fallback to undefined
}
setAvailableStacks(prev => {
const next = [...prev, { imageIds: wadouriImageIds, studyInstanceUID }];
setViewportImageIds(vpPrev => {
const vpNext = [...vpPrev];
vpNext[0] = wadouriImageIds;
return vpNext;
});
setSelectedStackIdx(selPrev => {
const selNext = [...selPrev];
selNext[0] = next.length - 1;
return selNext;
});
setLoadedImageIdsAndCacheMeta(wadouriImageIds);
return next;
});
};
// Cleanup
return () => {
window.loadDicomStackFromWadouriList = undefined;
};
}, [setAvailableStacks, setViewportImageIds, setSelectedStackIdx, setLoadedImageIdsAndCacheMeta]);
useEffect(() => { useEffect(() => {
if (!orderBySliceLocation) return; if (!orderBySliceLocation) return;
@@ -1001,11 +1071,187 @@ const dataSet = dicomParser.parseDicom(new Uint8Array(arrayBuffer));
mainToolGroup.setToolDisabled(ReferenceLinesTool.toolName) mainToolGroup.setToolDisabled(ReferenceLinesTool.toolName)
} }
} }
useEffect(() => {
// Export the current viewer state as JSON
window.exportViewerState = () => {
const numActive = viewportGrid.rows * viewportGrid.cols;
const activeViewportModes = viewportModes.slice(0, numActive);
const activeSelectedStackIdx = selectedStackIdx.slice(0, numActive);
const activeAvailableStacks = availableStacks;
const activeViewportImageIds = viewportImageIds.slice(0, numActive);
// Reference stack and UID for study comparison (use active viewport) // Collect viewport properties, stack positions, and camera for active viewports
const referenceStackIdx = activeViewport !== null ? selectedStackIdx[activeViewport] : 0; const renderingEngine = renderingEngineRef.current;
const referenceStackObj = availableStacks[referenceStackIdx]; const viewportProperties: any[] = [];
const referenceUID = referenceStackObj?.studyInstanceUID; const stackPositions: number[] = [];
const cameraProperties: any[] = [];
for (let i = 0; i < numActive; i++) {
let props = null;
let stackPos = null;
let camera = null;
if (renderingEngine) {
const viewportId = `CT_${i}`;
const viewport = renderingEngine.getViewport(viewportId);
if (viewport) {
if (typeof viewport.getProperties === "function") {
props = viewport.getProperties();
}
if (typeof viewport.getCurrentImageIdIndex === "function") {
stackPos = viewport.getCurrentImageIdIndex();
}
if (typeof viewport.getCamera === "function") {
camera = viewport.getCamera();
}
}
}
viewportProperties.push(props);
stackPositions.push(stackPos);
cameraProperties.push(camera);
}
const state = {
viewportGrid,
viewportModes: activeViewportModes,
selectedStackIdx: activeSelectedStackIdx,
availableStacks: activeAvailableStacks,
viewportImageIds: activeViewportImageIds,
viewportProperties,
stackPositions,
cameraProperties,
};
return JSON.stringify(state, null, 2);
};
window.importViewerState = (json: string) => {
try {
console.log("Importing viewer state:", json);
restoringStateRef.current = true;
const state = typeof json === "string" ? JSON.parse(json) : json;
if (state.viewportGrid) setViewportGrid(state.viewportGrid);
if (state.viewportModes) setViewportModes(state.viewportModes);
if (state.selectedStackIdx) setSelectedStackIdx(state.selectedStackIdx);
if (state.availableStacks) setAvailableStacks(state.availableStacks);
if (state.viewportImageIds) setViewportImageIds(state.viewportImageIds);
setTimeout(() => {
const renderingEngine = renderingEngineRef.current;
if (!renderingEngine) {
restoringStateRef.current = false;
return;
}
const numActive = state.viewportGrid?.rows * state.viewportGrid?.cols || 1;
for (let i = 0; i < numActive; i++) {
const viewportId = `CT_${i}`;
const viewport = renderingEngine.getViewport(viewportId);
if (!viewport) continue;
// Restore stack position (scroll)
if (
state.stackPositions &&
typeof state.stackPositions[i] === "number" &&
typeof viewport.setImageIdIndex === "function"
) {
if (csUtilities && csUtilities.jumpToSlice) {
csUtilities.jumpToSlice(viewport.element, { imageIndex: state.stackPositions[i] });
} else {
viewport.setImageIdIndex(state.stackPositions[i]);
}
}
// Restore viewport properties (VOI, invert, etc.)
if (
state.viewportProperties &&
state.viewportProperties[i] &&
typeof viewport.setProperties === "function"
) {
const props = state.viewportProperties[i];
const { voiRange, invert, interpolationType, VOILUTFunction, colormap } = props;
const propsToRestore: any = {};
if (voiRange && voiRange.lower !== undefined && voiRange.upper !== undefined) propsToRestore.voiRange = voiRange;
if (invert !== undefined) propsToRestore.invert = invert;
if (interpolationType !== undefined) propsToRestore.interpolationType = interpolationType;
if (VOILUTFunction) propsToRestore.VOILUTFunction = VOILUTFunction;
if (
colormap !== undefined &&
colormap !== null &&
(typeof colormap === "string" || (typeof colormap === "object" && !("actor" in colormap)))
) {
propsToRestore.colormap = colormap;
}
viewport.setProperties(propsToRestore);
viewport.render();
}
// Restore camera properties (zoom, pan, orientation)
if (
state.cameraProperties &&
state.cameraProperties[i] &&
typeof viewport.setCamera === "function"
) {
viewport.setCamera(state.cameraProperties[i]);
viewport.render();
}
}
restoringStateRef.current = false;
console.log("Viewer state restored successfully");
}, 300);
} catch (e) {
restoringStateRef.current = false;
alert("Failed to import viewer state: " + e);
}
};
// Export all annotations using cornerstoneTools.annotation.state.getAllAnnotations()
window.exportAnnotations = () => {
try {
const allAnnotations = cornerstoneTools.annotation.state.getAllAnnotations();
return JSON.stringify(allAnnotations, null, 2);
} catch (e) {
alert("Failed to export annotations: " + e);
return "{}";
}
};
// Import and restore all annotations using cornerstoneTools.annotation.state.restoreAnnotations()
window.importAnnotations = (json: string) => {
try {
const annotationData = typeof json === "string" ? JSON.parse(json) : json;
cornerstoneTools.annotation.state.restoreAnnotations(annotationData);
if (renderingEngineRef.current) {
renderingEngineRef.current.render();
}
} catch (e) {
alert("Failed to import annotations: " + e);
}
};
// Cleanup
return () => {
window.exportViewerState = undefined;
window.importViewerState = undefined;
window.exportAnnotations = undefined;
window.importAnnotations = undefined;
};
}, [
viewportGrid,
viewportModes,
selectedStackIdx,
availableStacks,
viewportImageIds,
setViewportGrid,
setViewportModes,
setSelectedStackIdx,
setAvailableStacks,
setViewportImageIds,
]);
// Reference stack and UID for study comparison (use active viewport)
const referenceStackIdx = activeViewport !== null ? selectedStackIdx[activeViewport] : 0;
const referenceStackObj = availableStacks[referenceStackIdx];
const referenceUID = referenceStackObj?.studyInstanceUID;
const viewports = []; const viewports = [];
for (let i = 0; i < MAX_VIEWPORTS; i++) { for (let i = 0; i < MAX_VIEWPORTS; i++) {
@@ -1148,7 +1394,7 @@ const referenceUID = referenceStackObj?.studyInstanceUID;
</div> </div>
)} )}
{/* Fullscreen button below the Volume/Stack toggle */} {/* Fullscreen button below the Volume/Stack toggle */}
<button <button
style={{ style={{
position: "absolute", position: "absolute",
top: 40, top: 40,
@@ -1183,7 +1429,7 @@ const referenceUID = referenceStackObj?.studyInstanceUID;
}} }}
onMouseEnter={() => setFullscreenHoverIdx(i)} onMouseEnter={() => setFullscreenHoverIdx(i)}
onMouseLeave={() => setFullscreenHoverIdx(null)} onMouseLeave={() => setFullscreenHoverIdx(null)}
> >
<span style={{ fontSize: "18px", lineHeight: 1 }}></span> <span style={{ fontSize: "18px", lineHeight: 1 }}></span>
{fullscreenHoverIdx === i && ( {fullscreenHoverIdx === i && (
<span style={{ marginLeft: 6 }}> <span style={{ marginLeft: 6 }}>
@@ -1192,7 +1438,7 @@ const referenceUID = referenceStackObj?.studyInstanceUID;
: "Fullscreen"} : "Fullscreen"}
</span> </span>
)} )}
</button> </button>
{/* Overlay info (bottom left of viewport) */} {/* Overlay info (bottom left of viewport) */}
<div <div
style={{ style={{
@@ -1412,7 +1658,7 @@ const referenceUID = referenceStackObj?.studyInstanceUID;
</div> </div>
</div> </div>
)} )}
<div <div
style={{ style={{
position: "absolute", position: "absolute",
left: 8, left: 8,
@@ -1427,7 +1673,7 @@ const referenceUID = referenceStackObj?.studyInstanceUID;
borderRadius: "4px", borderRadius: "4px",
padding: "2px 8px", padding: "2px 8px",
}} }}
> >
{/* Custom Stack Dropdown */} {/* Custom Stack Dropdown */}
<div style={{ position: "relative" }}> <div style={{ position: "relative" }}>
<button <button
@@ -1444,7 +1690,7 @@ const referenceUID = referenceStackObj?.studyInstanceUID;
cursor: "pointer", cursor: "pointer",
}} }}
onClick={() => setOpenDropdownIdx(openDropdownIdx === i ? null : i)} onClick={() => setOpenDropdownIdx(openDropdownIdx === i ? null : i)}
onBlur={() => setTimeout(() => setOpenDropdownIdx(null), 150)} //onBlur={() => setTimeout(() => setOpenDropdownIdx(null), 150)}
aria-haspopup="listbox" aria-haspopup="listbox"
aria-expanded={openDropdownIdx === i} aria-expanded={openDropdownIdx === i}
> >
@@ -1469,12 +1715,16 @@ const referenceUID = referenceStackObj?.studyInstanceUID;
})()} })()}
<span style={{ float: "right", fontSize: "12px" }}></span> <span style={{ float: "right", fontSize: "12px" }}></span>
</button> </button>
{openDropdownIdx === i && ( {openDropdownIdx === i && (
<div <div
ref={el => {
if (el) el.focus();
}}
tabIndex={0}
style={{ style={{
position: "absolute", position: "absolute",
left: 0, left: 0,
bottom: "110%", // <-- changed from top: "110%" to bottom: "110%" bottom: "110%",
background: "#222", background: "#222",
border: "1px solid #444", border: "1px solid #444",
borderRadius: "4px", borderRadius: "4px",
@@ -1483,9 +1733,37 @@ const referenceUID = referenceStackObj?.studyInstanceUID;
minWidth: 180, minWidth: 180,
maxHeight: 220, maxHeight: 220,
overflowY: "auto", overflowY: "auto",
overscrollBehavior: "contain",
scrollbarWidth: "thin",
}} }}
tabIndex={-1}
role="listbox" role="listbox"
onMouseDown={e => e.stopPropagation()}
onWheel={e => {
// Trap scroll inside the dropdown
const target = e.currentTarget as HTMLDivElement;
const { scrollTop, scrollHeight, clientHeight } = target;
const atTop = scrollTop === 0;
const atBottom = scrollTop + clientHeight >= scrollHeight - 1;
if (
(e.deltaY < 0 && atTop) ||
(e.deltaY > 0 && atBottom)
) {
// Let the event bubble to parent (viewport)
return;
}
// Otherwise, prevent scrolling the viewport
e.stopPropagation();
e.preventDefault();
// Manually scroll the menu
target.scrollTop += e.deltaY;
}}
onBlur={e => {
const related = e.relatedTarget as Node;
const parent = e.currentTarget.parentElement;
if (!parent?.contains(related)) {
setTimeout(() => setOpenDropdownIdx(null), 100);
}
}}
> >
{availableStacks.map((stackObj, idx) => { {availableStacks.map((stackObj, idx) => {
const thisUID = stackObj.studyInstanceUID; const thisUID = stackObj.studyInstanceUID;
@@ -1505,7 +1783,9 @@ const referenceUID = referenceStackObj?.studyInstanceUID;
alignItems: "center", alignItems: "center",
fontWeight: selectedStackIdx[i] === idx ? "bold" : "normal", fontWeight: selectedStackIdx[i] === idx ? "bold" : "normal",
borderBottom: "1px solid #333", borderBottom: "1px solid #333",
userSelect: "none",
}} }}
tabIndex={-1}
onMouseDown={e => { onMouseDown={e => {
e.preventDefault(); e.preventDefault();
handleLoadStack(i, idx); handleLoadStack(i, idx);
@@ -1524,9 +1804,9 @@ const referenceUID = referenceStackObj?.studyInstanceUID;
); );
})} })}
</div> </div>
)} )}
</div>
</div> </div>
</div>
</div> </div>
); );
} }
@@ -1547,7 +1827,7 @@ const referenceUID = referenceStackObj?.studyInstanceUID;
}}> }}>
<div> <div>
{/* Side menu toggle button */} {/* Side menu toggle button */}
<button <button
style={{ style={{
position: "absolute", position: "absolute",
top: "50%", top: "50%",
@@ -1568,12 +1848,12 @@ const referenceUID = referenceStackObj?.studyInstanceUID;
}} }}
onClick={() => setSideMenuOpen(open => !open)} onClick={() => setSideMenuOpen(open => !open)}
aria-label="Open menu" aria-label="Open menu"
> >
</button> </button>
{/* Side menu drawer */} {/* Side menu drawer */}
<div <div
style={{ style={{
position: "absolute", position: "absolute",
top: 0, top: 0,
@@ -1591,7 +1871,7 @@ const referenceUID = referenceStackObj?.studyInstanceUID;
overflow: "hidden", // <-- add this overflow: "hidden", // <-- add this
pointerEvents: sideMenuOpen ? "auto" : "none", // <-- add this pointerEvents: sideMenuOpen ? "auto" : "none", // <-- add this
}} }}
> >
<div <div
style={{ style={{
flex: 1, flex: 1,
@@ -1740,7 +2020,7 @@ const referenceUID = referenceStackObj?.studyInstanceUID;
</button> </button>
{/* Add more menu items here if needed */} {/* Add more menu items here if needed */}
</div> </div>
</div> </div>
</div> </div>
{/* Viewport grid menu at top center */} {/* Viewport grid menu at top center */}