more improvements
This commit is contained in:
+210
-75
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user