diff --git a/dicom-viewer/src/App.tsx b/dicom-viewer/src/App.tsx index 5d90324..527207a 100644 --- a/dicom-viewer/src/App.tsx +++ b/dicom-viewer/src/App.tsx @@ -1104,6 +1104,8 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer ); const viewportStackSignaturesRef = useRef(Array(MAX_VIEWPORTS).fill("")); const defaultVoiRangeByViewportRef = useRef>(Array(MAX_VIEWPORTS).fill(null)); + const activePresetByViewportRef = useRef>(Array(MAX_VIEWPORTS).fill(null)); + const lastRenderedImageIdByViewportRef = useRef>(Array(MAX_VIEWPORTS).fill(null)); const [viewportModes, setViewportModes] = useState>( () => Array(MAX_VIEWPORTS).fill('stack') @@ -1899,6 +1901,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer if (viewportStackSignaturesRef.current[i] !== signature) { viewportStackSignaturesRef.current[i] = signature; defaultVoiRangeByViewportRef.current[i] = null; + lastRenderedImageIdByViewportRef.current[i] = null; next[i] = []; changed = true; } @@ -2423,8 +2426,14 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer const imageId = ids[currentIndex] || ids[0]; const defaultVoi = defaultVoiRangeByViewportRef.current[viewportIdx]; const voiMeta = imageId ? (metaData.get("voiLutModule", imageId) || {}) as any : {}; + const pixelMeta = imageId ? (metaData.get("imagePixelModule", imageId) || {}) as any : {}; + const lutMeta = imageId ? (metaData.get("modalityLutModule", imageId) || {}) as any : {}; const centerMeta = Array.isArray(voiMeta.windowCenter) ? voiMeta.windowCenter[0] : voiMeta.windowCenter; const widthMeta = Array.isArray(voiMeta.windowWidth) ? voiMeta.windowWidth[0] : voiMeta.windowWidth; + const minRaw = Number(pixelMeta.smallestPixelValue ?? pixelMeta.smallestImagePixelValue); + const maxRaw = Number(pixelMeta.largestPixelValue ?? pixelMeta.largestImagePixelValue); + const slope = Number(lutMeta.rescaleSlope ?? 1); + const intercept = Number(lutMeta.rescaleIntercept ?? 0); if (defaultVoi) { viewport.setProperties({ voiRange: defaultVoi }); @@ -2437,6 +2446,13 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer upper: center + width / 2, }, }); + } else if (Number.isFinite(minRaw) && Number.isFinite(maxRaw) && maxRaw > minRaw) { + viewport.setProperties({ + voiRange: { + lower: minRaw * slope + intercept, + upper: maxRaw * slope + intercept, + }, + }); } else if (typeof viewport.resetProperties === "function") { viewport.resetProperties(); } @@ -2492,7 +2508,21 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer slope = Number((image as any)?.slope ?? slope); intercept = Number((image as any)?.intercept ?? intercept); } catch (err) { - // Keep metadata-derived fallbacks below. + const fallbackIds = ids.filter((id): id is string => !!id).slice(0, 6); + for (const fallbackId of fallbackIds) { + try { + const fallbackImage = await imageLoader.loadAndCacheImage(fallbackId); + minRaw = Number((fallbackImage as any)?.minPixelValue); + maxRaw = Number((fallbackImage as any)?.maxPixelValue); + slope = Number((fallbackImage as any)?.slope ?? slope); + intercept = Number((fallbackImage as any)?.intercept ?? intercept); + if (Number.isFinite(minRaw) && Number.isFinite(maxRaw) && maxRaw > minRaw) { + break; + } + } catch (innerErr) { + // Keep trying additional images in the stack. + } + } } } const centerMeta = Array.isArray(voiMeta.windowCenter) ? voiMeta.windowCenter[0] : voiMeta.windowCenter; @@ -2528,9 +2558,13 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer }); }, [applyDefaultWindowLevel, updateOverlay, viewportImageIds]); - const applyPresetOption = useCallback((viewportIdx: number, preset: PresetOption) => { + const applyPresetOption = useCallback((viewportIdx: number, preset: PresetOption, persistSelection = true, resetCameraOnReset = true) => { + if (persistSelection) { + activePresetByViewportRef.current[viewportIdx] = preset; + } + if (preset.action === "reset") { - applyDefaultWindowLevel(viewportIdx, true); + applyDefaultWindowLevel(viewportIdx, resetCameraOnReset); return; } if (preset.action === "autolevel") { @@ -2541,6 +2575,42 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer handleApplyPreset(viewportIdx, wlPreset.center, wlPreset.width); }, [applyDefaultWindowLevel, handleApplyPreset, handleAutoLevel]); + useEffect(() => { + const applyPresetOnImageChange = () => { + const renderingEngine = renderingEngineRef.current; + if (!renderingEngine) return; + + const visibleCount = viewportGrid.rows * viewportGrid.cols; + for (let i = 0; i < visibleCount; i++) { + if (viewportModes[i] !== "stack") continue; + const preset = activePresetByViewportRef.current[i]; + if (!preset) continue; + + const viewport = renderingEngine.getViewport(`CT_${i}`); + if (!viewport || typeof viewport.getImageIds !== "function") continue; + + const ids = viewport.getImageIds() || []; + if (!ids.length) continue; + + const idx = typeof viewport.getCurrentImageIdIndex === "function" + ? viewport.getCurrentImageIdIndex() + : 0; + const currentImageId = ids[idx] || ids[0]; + if (!currentImageId) continue; + + if (lastRenderedImageIdByViewportRef.current[i] === currentImageId) { + continue; + } + lastRenderedImageIdByViewportRef.current[i] = currentImageId; + + applyPresetOption(i, preset, false, false); + } + }; + + const timer = window.setTimeout(applyPresetOnImageChange, 0); + return () => window.clearTimeout(timer); + }, [applyPresetOption, imageInfos, viewportGrid, viewportModes]); + useEffect(() => { const handlePresetHotkey = (e: KeyboardEvent) => { const target = e.target as HTMLElement | null;