diff --git a/dicom-viewer/src/App.tsx b/dicom-viewer/src/App.tsx index 6ab78b5..90f82fa 100644 --- a/dicom-viewer/src/App.tsx +++ b/dicom-viewer/src/App.tsx @@ -487,7 +487,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer } const [stackOrderModified, setStackOrderModified] = useState(false); - const [loadingState, setLoadingState] = useState(null); + const [loadingState, setLoadingState] = useState(null); // Show a warning when a WebGL/graphics resource error (e.g. releaseGraphicsResources) occurs const [graphicsErrorVisible, setGraphicsErrorVisible] = useState(false); const [graphicsErrorMessage, setGraphicsErrorMessage] = useState(null); @@ -651,26 +651,31 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer 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; - }); + setLoadingState("displayset"); + try { + // Always revert to stack mode when loading a new stack + setViewportModes(prev => { + const next = [...prev]; + next[viewportIdx] = 'stack'; + return next; + }); - setViewportImageIds(prev => { - const next = [...prev]; - next[viewportIdx] = stack; - return next; - }); - setSelectedStackIdx(prev => { - const next = [...prev]; - next[viewportIdx] = stackIdx; - return next; - }); + setViewportImageIds(prev => { + const next = [...prev]; + next[viewportIdx] = stack; + return next; + }); + setSelectedStackIdx(prev => { + const next = [...prev]; + next[viewportIdx] = stackIdx; + return next; + }); - // Cache imageIds and load metadata for this stack - await setLoadedImageIdsAndCacheMeta(stack); + // Cache imageIds and load metadata for this stack + await setLoadedImageIdsAndCacheMeta(stack); + } finally { + setLoadingState(null); + } }; const [crossHairsEnabled, setCrossHairsEnabled] = useState(false); @@ -1918,13 +1923,18 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer nextModes[insertedViewportIndex] = "stack"; nextSelected[insertedViewportIndex] = stackIdx; - saveVisibleViewportState(); - setViewportGrid({ rows: newRows, cols: newCols }); - setViewportImageIds(nextImageIds); - setViewportModes(nextModes); - setSelectedStackIdx(nextSelected); - setActiveViewport(insertedViewportIndex); - await setLoadedImageIdsAndCacheMeta(stackObj.imageIds); + setLoadingState("displayset"); + try { + saveVisibleViewportState(); + setViewportGrid({ rows: newRows, cols: newCols }); + setViewportImageIds(nextImageIds); + setViewportModes(nextModes); + setSelectedStackIdx(nextSelected); + setActiveViewport(insertedViewportIndex); + await setLoadedImageIdsAndCacheMeta(stackObj.imageIds); + } finally { + setLoadingState(null); + } }; useEffect(() => { @@ -2371,7 +2381,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer const handleAutoLevel = useCallback((viewportIdx: number) => { const viewportId = `CT_${viewportIdx}`; - try { + void (async () => { const renderingEngine = renderingEngineRef.current; if (!renderingEngine) return; const viewport = renderingEngine.getViewport(viewportId); @@ -2395,10 +2405,22 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer const voiMeta = (metaData.get("voiLutModule", imageId) || {}) as any; const defaultVoi = defaultVoiRangeByViewportRef.current[viewportIdx]; - 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); + let minRaw = Number(pixelMeta.smallestPixelValue ?? pixelMeta.smallestImagePixelValue); + let maxRaw = Number(pixelMeta.largestPixelValue ?? pixelMeta.largestImagePixelValue); + let slope = Number(lutMeta.rescaleSlope ?? 1); + let intercept = Number(lutMeta.rescaleIntercept ?? 0); + + if (!Number.isFinite(minRaw) || !Number.isFinite(maxRaw) || maxRaw <= minRaw) { + try { + const image = await imageLoader.loadAndCacheImage(imageId); + minRaw = Number((image as any)?.minPixelValue); + maxRaw = Number((image as any)?.maxPixelValue); + slope = Number((image as any)?.slope ?? slope); + intercept = Number((image as any)?.intercept ?? intercept); + } catch (err) { + // Keep metadata-derived fallbacks below. + } + } const centerMeta = Array.isArray(voiMeta.windowCenter) ? voiMeta.windowCenter[0] : voiMeta.windowCenter; const widthMeta = Array.isArray(voiMeta.windowWidth) ? voiMeta.windowWidth[0] : voiMeta.windowWidth; @@ -2427,11 +2449,60 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer viewport.render(); updateOverlay(viewportIdx, viewport); - } catch (e) { + })().catch(() => { applyDefaultWindowLevel(viewportIdx); - } + }); }, [applyDefaultWindowLevel, updateOverlay, viewportImageIds]); + const applyPresetOption = useCallback((viewportIdx: number, preset: PresetOption) => { + if (preset.action === "reset") { + applyDefaultWindowLevel(viewportIdx, true); + return; + } + if (preset.action === "autolevel") { + handleAutoLevel(viewportIdx); + return; + } + const wlPreset = preset as Extract; + handleApplyPreset(viewportIdx, wlPreset.center, wlPreset.width); + }, [applyDefaultWindowLevel, handleApplyPreset, handleAutoLevel]); + + useEffect(() => { + const handlePresetHotkey = (e: KeyboardEvent) => { + const target = e.target as HTMLElement | null; + if (target) { + const tag = target.tagName?.toLowerCase(); + const editable = target.getAttribute("contenteditable") === "true"; + if (tag === "input" || tag === "textarea" || tag === "select" || editable) return; + } + if (e.ctrlKey || e.altKey || e.metaKey) return; + if (activeViewport === null) return; + + const modality = imageInfos[activeViewport]?.modality; + const options = getPresetOptionsForModality(modality); + + if (e.key === "0") { + const resetOption = options.find((option) => option.action === "reset"); + if (resetOption) { + applyPresetOption(activeViewport, resetOption); + e.preventDefault(); + } + return; + } + + if (!/^[1-9]$/.test(e.key)) return; + const presetIndex = Number(e.key) - 1; + const properPresets = options.filter((option): option is Extract => option.action === "preset"); + if (presetIndex >= properPresets.length) return; + + applyPresetOption(activeViewport, properPresets[presetIndex]); + e.preventDefault(); + }; + + window.addEventListener("keydown", handlePresetHotkey); + return () => window.removeEventListener("keydown", handlePresetHotkey); + }, [activeViewport, applyPresetOption, imageInfos]); + const toggleViewerFullscreen = useCallback(() => { const root = appRootRef.current; if (!root) return; @@ -3986,9 +4057,22 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer }} onClick={e => e.stopPropagation()} // <-- prevent bubbling to viewport > - {getPresetOptionsForModality(imageInfos[i]?.modality).map((preset) => ( + {(() => { + const options = getPresetOptionsForModality(imageInfos[i]?.modality); + let presetOrdinal = 0; + return options.map((preset) => { + let hotkeyHint = ""; + if (preset.action === "reset") { + hotkeyHint = "0"; + } else if (preset.action === "preset") { + presetOrdinal += 1; + hotkeyHint = String(presetOrdinal); + } + + return ( - ))} + ); + }); + })()} )} @@ -4475,7 +4554,9 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer > {loadingState === "annotations" ? "Loading Annotations..." - : "Loading Viewer State..."} + : loadingState === "displayset" + ? "Loading Display Set..." + : "Loading Viewer State..."} )}