Enhance loading state management by adding 'displayset' option and improve user feedback during data loading

This commit is contained in:
Ross
2026-05-18 12:29:42 +01:00
parent 99df1cd1c1
commit c9902a2e15
+100 -19
View File
@@ -487,7 +487,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
} }
const [stackOrderModified, setStackOrderModified] = useState(false); const [stackOrderModified, setStackOrderModified] = useState(false);
const [loadingState, setLoadingState] = useState<null | "annotations" | "viewerState">(null); const [loadingState, setLoadingState] = useState<null | "annotations" | "viewerState" | "displayset">(null);
// Show a warning when a WebGL/graphics resource error (e.g. releaseGraphicsResources) occurs // Show a warning when a WebGL/graphics resource error (e.g. releaseGraphicsResources) occurs
const [graphicsErrorVisible, setGraphicsErrorVisible] = useState(false); const [graphicsErrorVisible, setGraphicsErrorVisible] = useState(false);
const [graphicsErrorMessage, setGraphicsErrorMessage] = useState<string | null>(null); const [graphicsErrorMessage, setGraphicsErrorMessage] = useState<string | null>(null);
@@ -651,6 +651,8 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
if (!stackObj) return; if (!stackObj) return;
const stack = stackObj.imageIds; const stack = stackObj.imageIds;
setLoadingState("displayset");
try {
// Always revert to stack mode when loading a new stack // Always revert to stack mode when loading a new stack
setViewportModes(prev => { setViewportModes(prev => {
const next = [...prev]; const next = [...prev];
@@ -671,6 +673,9 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
// Cache imageIds and load metadata for this stack // Cache imageIds and load metadata for this stack
await setLoadedImageIdsAndCacheMeta(stack); await setLoadedImageIdsAndCacheMeta(stack);
} finally {
setLoadingState(null);
}
}; };
const [crossHairsEnabled, setCrossHairsEnabled] = useState(false); const [crossHairsEnabled, setCrossHairsEnabled] = useState(false);
@@ -1918,6 +1923,8 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
nextModes[insertedViewportIndex] = "stack"; nextModes[insertedViewportIndex] = "stack";
nextSelected[insertedViewportIndex] = stackIdx; nextSelected[insertedViewportIndex] = stackIdx;
setLoadingState("displayset");
try {
saveVisibleViewportState(); saveVisibleViewportState();
setViewportGrid({ rows: newRows, cols: newCols }); setViewportGrid({ rows: newRows, cols: newCols });
setViewportImageIds(nextImageIds); setViewportImageIds(nextImageIds);
@@ -1925,6 +1932,9 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
setSelectedStackIdx(nextSelected); setSelectedStackIdx(nextSelected);
setActiveViewport(insertedViewportIndex); setActiveViewport(insertedViewportIndex);
await setLoadedImageIdsAndCacheMeta(stackObj.imageIds); await setLoadedImageIdsAndCacheMeta(stackObj.imageIds);
} finally {
setLoadingState(null);
}
}; };
useEffect(() => { useEffect(() => {
@@ -2371,7 +2381,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
const handleAutoLevel = useCallback((viewportIdx: number) => { const handleAutoLevel = useCallback((viewportIdx: number) => {
const viewportId = `CT_${viewportIdx}`; const viewportId = `CT_${viewportIdx}`;
try { void (async () => {
const renderingEngine = renderingEngineRef.current; const renderingEngine = renderingEngineRef.current;
if (!renderingEngine) return; if (!renderingEngine) return;
const viewport = renderingEngine.getViewport(viewportId); 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 voiMeta = (metaData.get("voiLutModule", imageId) || {}) as any;
const defaultVoi = defaultVoiRangeByViewportRef.current[viewportIdx]; const defaultVoi = defaultVoiRangeByViewportRef.current[viewportIdx];
const minRaw = Number(pixelMeta.smallestPixelValue ?? pixelMeta.smallestImagePixelValue); let minRaw = Number(pixelMeta.smallestPixelValue ?? pixelMeta.smallestImagePixelValue);
const maxRaw = Number(pixelMeta.largestPixelValue ?? pixelMeta.largestImagePixelValue); let maxRaw = Number(pixelMeta.largestPixelValue ?? pixelMeta.largestImagePixelValue);
const slope = Number(lutMeta.rescaleSlope ?? 1); let slope = Number(lutMeta.rescaleSlope ?? 1);
const intercept = Number(lutMeta.rescaleIntercept ?? 0); 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 centerMeta = Array.isArray(voiMeta.windowCenter) ? voiMeta.windowCenter[0] : voiMeta.windowCenter;
const widthMeta = Array.isArray(voiMeta.windowWidth) ? voiMeta.windowWidth[0] : voiMeta.windowWidth; 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(); viewport.render();
updateOverlay(viewportIdx, viewport); updateOverlay(viewportIdx, viewport);
} catch (e) { })().catch(() => {
applyDefaultWindowLevel(viewportIdx); applyDefaultWindowLevel(viewportIdx);
} });
}, [applyDefaultWindowLevel, updateOverlay, viewportImageIds]); }, [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<PresetOption, { action: "preset" }>;
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<PresetOption, { action: "preset" }> => 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 toggleViewerFullscreen = useCallback(() => {
const root = appRootRef.current; const root = appRootRef.current;
if (!root) return; if (!root) return;
@@ -3986,9 +4057,22 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
}} }}
onClick={e => e.stopPropagation()} // <-- prevent bubbling to viewport 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 (
<button <button
key={preset.name} key={preset.name}
title={hotkeyHint ? `Shortcut: ${hotkeyHint}` : undefined}
style={{ style={{
display: "block", display: "block",
width: "100%", width: "100%",
@@ -4005,14 +4089,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
}} }}
onClick={e => { onClick={e => {
e.stopPropagation(); // <-- prevent bubbling to viewport e.stopPropagation(); // <-- prevent bubbling to viewport
if (preset.action === "reset") { applyPresetOption(i, preset);
applyDefaultWindowLevel(i, true);
} else if (preset.action === "autolevel") {
handleAutoLevel(i);
} else {
const ctPreset = preset as Extract<PresetOption, { action: "preset" }>;
handleApplyPreset(i, ctPreset.center, ctPreset.width);
}
setPresetsOpenArr(prev => { setPresetsOpenArr(prev => {
const next = [...prev]; const next = [...prev];
next[i] = false; next[i] = false;
@@ -4020,9 +4097,11 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
}); });
}} }}
> >
{preset.name} {preset.name}{hotkeyHint ? ` (${hotkeyHint})` : ""}
</button> </button>
))} );
});
})()}
</div> </div>
)} )}
</div> </div>
@@ -4475,6 +4554,8 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
> >
{loadingState === "annotations" {loadingState === "annotations"
? "Loading Annotations..." ? "Loading Annotations..."
: loadingState === "displayset"
? "Loading Display Set..."
: "Loading Viewer State..."} : "Loading Viewer State..."}
</div> </div>
)} )}