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
+126 -45
View File
@@ -487,7 +487,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
}
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
const [graphicsErrorVisible, setGraphicsErrorVisible] = useState(false);
const [graphicsErrorMessage, setGraphicsErrorMessage] = useState<string | null>(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<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 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 (
<button
key={preset.name}
title={hotkeyHint ? `Shortcut: ${hotkeyHint}` : undefined}
style={{
display: "block",
width: "100%",
@@ -4005,14 +4089,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
}}
onClick={e => {
e.stopPropagation(); // <-- prevent bubbling to viewport
if (preset.action === "reset") {
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);
}
applyPresetOption(i, preset);
setPresetsOpenArr(prev => {
const next = [...prev];
next[i] = false;
@@ -4020,9 +4097,11 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
});
}}
>
{preset.name}
{preset.name}{hotkeyHint ? ` (${hotkeyHint})` : ""}
</button>
))}
);
});
})()}
</div>
)}
</div>
@@ -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..."}
</div>
)}