Enhance loading state management by adding 'displayset' option and improve user feedback during data loading
This commit is contained in:
+126
-45
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user