more improvements

This commit is contained in:
Ross
2025-05-31 15:14:46 +01:00
parent c7728bf1a2
commit 4875b95ab2
+210 -75
View File
@@ -24,6 +24,7 @@ declare global {
}
}
type StackEntry = { imageIds: string[], studyInstanceUID?: string };
// Add this type for clarity
declare global {
@@ -109,6 +110,7 @@ function App() {
const [settingsOpen, setSettingsOpen] = useState(false);
const [openDropdownIdx, setOpenDropdownIdx] = useState<number | null>(null);
// State for overlay info
const [imageInfo, setImageInfo] = useState({
index: 0,
@@ -121,7 +123,9 @@ function App() {
const renderingEngineId = "mainRenderingEngine";
const renderingEngineRef = useRef<any>(null);
const [availableStacks, setAvailableStacks] = useState<string[][]>([]);
const [availableStacks, setAvailableStacks] = useState<StackEntry[]>([]);
const [activeViewport, setActiveViewport] = useState<number | null>(0);
const [viewportImageIds, setViewportImageIds] = useState<string[][]>(
() => Array(MAX_VIEWPORTS).fill([])
@@ -131,19 +135,47 @@ function App() {
);
// Load available stacks on mount
useEffect(() => {
availableImageIds().then(stacks => {
setAvailableStacks(stacks);
// Optionally initialize each viewport with the first stack
setLoadedImageIdsAndCacheMeta(stacks[0]);
setViewportImageIds(Array(MAX_VIEWPORTS).fill(stacks[0] || []));
});
}, []);
useEffect(() => {
availableImageIds().then(async stacks => {
// Try to extract StudyInstanceUID for each stack
const stackEntries: StackEntry[] = await Promise.all(
stacks.map(async imageIds => {
let studyInstanceUID: string | undefined = undefined;
try {
// Try to fetch the first image as a blob and parse it
const firstId = imageIds[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
}
return { imageIds, studyInstanceUID };
})
);
setAvailableStacks(stackEntries);
//setAvailableStacks(stacks.map(imageIds => ({ imageIds, studyInstanceUID: undefined })));
setLoadedImageIdsAndCacheMeta(stacks[0]);
setViewportImageIds(Array(MAX_VIEWPORTS).fill(stacks[0] || []));
});
}, []);
const handleLoadStack = async (viewportIdx: number, stackIdx: number) => {
const stack = availableStacks[stackIdx];
const stackObj = availableStacks[stackIdx];
if (!stackObj) return;
const stack = stackObj.imageIds;
// Always revert to stack mode when loading a new stack
setViewportModes(prev => {
const next = [...prev];
next[viewportIdx] = 'stack';
return next;
});
if (!stack) return;
setViewportImageIds(prev => {
const next = [...prev];
next[viewportIdx] = stack;
@@ -383,20 +415,20 @@ function App() {
setMetaModalOpen(true);
};
// Inside your App component:
useEffect(() => {
window.loadDicomStackFromFiles = (files: FileList | File[]) => {
// Convert FileList to array if needed
const fileArray = Array.from(files);
// Your logic to handle the files and load them as a stack
// For example, call your existing handler:
handleFilesSelected({ target: { files: fileArray } } as any);
};
// Cleanup on unmount
return () => {
window.loadDicomStackFromFiles = undefined;
};
}, []);
// Inside your App component:
useEffect(() => {
window.loadDicomStackFromFiles = (files: FileList | File[]) => {
// Convert FileList to array if needed
const fileArray = Array.from(files);
// Your logic to handle the files and load them as a stack
// For example, call your existing handler:
handleFilesSelected({ target: { files: fileArray } } as any);
};
// Cleanup on unmount
return () => {
window.loadDicomStackFromFiles = undefined;
};
}, []);
const [altPressed, setAltPressed] = useState(false);
@@ -509,18 +541,19 @@ const handleFilesSelected = async (event: React.ChangeEvent<HTMLInputElement> |
setOrderBySliceLocation(false);
// Convert FileList to Array and sort by name (optional)
const fileArray = Array.from(files).sort((a, b) => a.name.localeCompare(b.name));
let studyInstanceUID: string | undefined = undefined;
const arrayBuffer = await fileArray[0].arrayBuffer();
const dataSet = dicomParser.parseDicom(new Uint8Array(arrayBuffer));
studyInstanceUID = dataSet.string('x0020000d');
console.log("StudyInstanceUID:", studyInstanceUID);
// Create imageIds for each file
const imageIds = fileArray.map(
file => `wadouri:${URL.createObjectURL(file)}`
);
// Add as a new stack
setAvailableStacks(prev => {
const next = [...prev, imageIds];
// Optionally, set the new stack as active in the first viewport:
const next = [...prev, { imageIds, studyInstanceUID }];
setViewportImageIds(vpPrev => {
const vpNext = [...vpPrev];
vpNext[0] = imageIds;
@@ -531,7 +564,6 @@ const handleFilesSelected = async (event: React.ChangeEvent<HTMLInputElement> |
selNext[0] = next.length - 1;
return selNext;
});
// Cache imageIds and load metadata for this stack
setLoadedImageIdsAndCacheMeta(imageIds);
return next;
});
@@ -969,6 +1001,11 @@ const handleFilesSelected = async (event: React.ChangeEvent<HTMLInputElement> |
}
}
// 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 = [];
for (let i = 0; i < MAX_VIEWPORTS; i++) {
// Determine if this viewport should be visible in the current grid
@@ -1109,6 +1146,40 @@ const handleFilesSelected = async (event: React.ChangeEvent<HTMLInputElement> |
</button>
</div>
)}
{/* Fullscreen button below the Volume/Stack toggle */}
<button
style={{
position: "absolute",
top: 40, // below the toggle button (which is at top: 8, height: ~32)
left: 8,
zIndex: 20,
background: "#111",
color: "#fff",
border: "none",
borderRadius: "4px",
padding: "4px 8px",
cursor: "pointer",
fontSize: "13px",
opacity: 0.85,
marginTop: "4px",
}}
title="Toggle Fullscreen"
onClick={e => {
e.stopPropagation();
const el = viewportRefs.current[i];
if (document.fullscreenElement === el) {
document.exitFullscreen();
} else if (el?.requestFullscreen) {
el.requestFullscreen();
} else if ((el as any)?.webkitRequestFullscreen) {
(el as any).webkitRequestFullscreen();
} else if ((el as any)?.msRequestFullscreen) {
(el as any).msRequestFullscreen();
}
}}
>
{document.fullscreenElement === viewportRefs.current[i] ? "Exit Fullscreen" : "⛶ Fullscreen"}
</button>
{/* Overlay info (bottom left of viewport) */}
<div
style={{
@@ -1328,57 +1399,121 @@ const handleFilesSelected = async (event: React.ChangeEvent<HTMLInputElement> |
</div>
</div>
)}
<div
style={{
position: "absolute",
left: 8,
right: 8,
bottom: 8,
zIndex: 20,
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
gap: "8px",
background: "rgba(34,34,34,0.85)",
borderRadius: "4px",
padding: "2px 8px",
}}
>
{/* Custom Stack Dropdown */}
<div style={{ position: "relative" }}>
<button
style={{
background: "#222",
color: "#fff",
border: "1px solid #444",
borderRadius: "4px",
padding: "2px 8px",
fontSize: "13px",
marginRight: "6px",
minWidth: 120,
textAlign: "left",
cursor: "pointer",
}}
onClick={() => setOpenDropdownIdx(openDropdownIdx === i ? null : i)}
onBlur={() => setTimeout(() => setOpenDropdownIdx(null), 150)}
aria-haspopup="listbox"
aria-expanded={openDropdownIdx === i}
>
{/* Show current stack with indicator */}
{(() => {
const stackObj = availableStacks[selectedStackIdx[i] || 0];
const thisUID = stackObj?.studyInstanceUID;
const sameStudy = referenceUID && thisUID && referenceUID === thisUID;
const dotColor = sameStudy ? "#4caf50" : "#888";
return (
<span>
<span style={{
color: dotColor,
fontWeight: "bold",
marginRight: 6,
fontSize: "16px",
verticalAlign: "middle",
}}></span>
Stack {selectedStackIdx[i] + 1} ({stackObj?.imageIds.length || 0} images)
</span>
);
})()}
<span style={{ float: "right", fontSize: "12px" }}></span>
</button>
{openDropdownIdx === i && (
<div
style={{
position: "absolute",
left: 0,
bottom: "110%", // <-- changed from top: "110%" to bottom: "110%"
background: "#222",
border: "1px solid #444",
borderRadius: "4px",
boxShadow: "0 2px 8px rgba(0,0,0,0.3)",
zIndex: 100,
minWidth: 180,
maxHeight: 220,
overflowY: "auto",
}}
tabIndex={-1}
role="listbox"
>
{availableStacks.map((stackObj, idx) => {
const thisUID = stackObj.studyInstanceUID;
const sameStudy = referenceUID && thisUID && referenceUID === thisUID;
const dotColor = sameStudy ? "#4caf50" : "#888";
return (
<div
key={idx}
role="option"
aria-selected={selectedStackIdx[i] === idx}
style={{
position: "absolute",
left: 8,
right: 8,
bottom: 8,
zIndex: 20,
padding: "6px 12px",
background: selectedStackIdx[i] === idx ? "#444" : "#222",
color: "#fff",
cursor: "pointer",
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
gap: "8px",
background: "rgba(34,34,34,0.85)",
borderRadius: "4px",
padding: "2px 8px",
fontWeight: selectedStackIdx[i] === idx ? "bold" : "normal",
borderBottom: "1px solid #333",
}}
onMouseDown={e => {
e.preventDefault();
handleLoadStack(i, idx);
setOpenDropdownIdx(null);
}}
>
<select
value={selectedStackIdx[i] || 0}
onChange={e => handleLoadStack(i, Number(e.target.value))}
style={{
background: "#222",
color: "#fff",
border: "1px solid #444",
borderRadius: "4px",
padding: "2px 8px",
fontSize: "13px",
marginRight: "6px",
}}
>
{availableStacks.map((stack, idx) => (
<option key={idx} value={idx}>
Stack {idx + 1} ({stack.length} images)
</option>
))}
</select>
<button
style={{
background: "#1976d2",
color: "#fff",
border: "none",
borderRadius: "3px",
padding: "2px 12px",
cursor: "pointer",
fontSize: "13px",
}}
onClick={() => handleLoadStack(i, selectedStackIdx[i] || 0)}
disabled={!availableStacks.length}
>
Load Stack
</button>
<span style={{
color: dotColor,
fontWeight: "bold",
marginRight: 8,
fontSize: "16px",
verticalAlign: "middle",
}}></span>
Stack {idx + 1} ({stackObj.imageIds.length} images)
</div>
);
})}
</div>
)}
</div>
</div>
</div>
);
}