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