diff --git a/dicom-viewer/src/App.tsx b/dicom-viewer/src/App.tsx index 3d63041..b7dd237 100644 --- a/dicom-viewer/src/App.tsx +++ b/dicom-viewer/src/App.tsx @@ -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(null); // State for overlay info const [imageInfo, setImageInfo] = useState({ index: 0, @@ -121,7 +123,9 @@ function App() { const renderingEngineId = "mainRenderingEngine"; const renderingEngineRef = useRef(null); - const [availableStacks, setAvailableStacks] = useState([]); + + +const [availableStacks, setAvailableStacks] = useState([]); const [activeViewport, setActiveViewport] = useState(0); const [viewportImageIds, setViewportImageIds] = useState( () => 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 | 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 | 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 | } } +// 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 | )} + {/* Fullscreen button below the Volume/Stack toggle */} + {/* Overlay info (bottom left of viewport) */}
|
)} +
+ {/* Custom Stack Dropdown */} +
+ +{openDropdownIdx === i && ( +
+ {availableStacks.map((stackObj, idx) => { + const thisUID = stackObj.studyInstanceUID; + const sameStudy = referenceUID && thisUID && referenceUID === thisUID; + const dotColor = sameStudy ? "#4caf50" : "#888"; + return (
{ + e.preventDefault(); + handleLoadStack(i, idx); + setOpenDropdownIdx(null); }} > - - + + Stack {idx + 1} ({stackObj.imageIds.length} images)
+ ); + })} +
+)} +
+
); }