Enhance viewport preset management by adding active preset tracking and applying presets on image changes

This commit is contained in:
Ross
2026-05-18 12:39:08 +01:00
parent 7b786fcf9f
commit af6e22fd91
+73 -3
View File
@@ -1104,6 +1104,8 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
);
const viewportStackSignaturesRef = useRef<string[]>(Array(MAX_VIEWPORTS).fill(""));
const defaultVoiRangeByViewportRef = useRef<Array<{ lower: number; upper: number } | null>>(Array(MAX_VIEWPORTS).fill(null));
const activePresetByViewportRef = useRef<Array<PresetOption | null>>(Array(MAX_VIEWPORTS).fill(null));
const lastRenderedImageIdByViewportRef = useRef<Array<string | null>>(Array(MAX_VIEWPORTS).fill(null));
const [viewportModes, setViewportModes] = useState<Array<'stack' | 'volume'>>(
() => Array(MAX_VIEWPORTS).fill('stack')
@@ -1899,6 +1901,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
if (viewportStackSignaturesRef.current[i] !== signature) {
viewportStackSignaturesRef.current[i] = signature;
defaultVoiRangeByViewportRef.current[i] = null;
lastRenderedImageIdByViewportRef.current[i] = null;
next[i] = [];
changed = true;
}
@@ -2423,8 +2426,14 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
const imageId = ids[currentIndex] || ids[0];
const defaultVoi = defaultVoiRangeByViewportRef.current[viewportIdx];
const voiMeta = imageId ? (metaData.get("voiLutModule", imageId) || {}) as any : {};
const pixelMeta = imageId ? (metaData.get("imagePixelModule", imageId) || {}) as any : {};
const lutMeta = imageId ? (metaData.get("modalityLutModule", imageId) || {}) as any : {};
const centerMeta = Array.isArray(voiMeta.windowCenter) ? voiMeta.windowCenter[0] : voiMeta.windowCenter;
const widthMeta = Array.isArray(voiMeta.windowWidth) ? voiMeta.windowWidth[0] : voiMeta.windowWidth;
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);
if (defaultVoi) {
viewport.setProperties({ voiRange: defaultVoi });
@@ -2437,6 +2446,13 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
upper: center + width / 2,
},
});
} else if (Number.isFinite(minRaw) && Number.isFinite(maxRaw) && maxRaw > minRaw) {
viewport.setProperties({
voiRange: {
lower: minRaw * slope + intercept,
upper: maxRaw * slope + intercept,
},
});
} else if (typeof viewport.resetProperties === "function") {
viewport.resetProperties();
}
@@ -2492,7 +2508,21 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
slope = Number((image as any)?.slope ?? slope);
intercept = Number((image as any)?.intercept ?? intercept);
} catch (err) {
// Keep metadata-derived fallbacks below.
const fallbackIds = ids.filter((id): id is string => !!id).slice(0, 6);
for (const fallbackId of fallbackIds) {
try {
const fallbackImage = await imageLoader.loadAndCacheImage(fallbackId);
minRaw = Number((fallbackImage as any)?.minPixelValue);
maxRaw = Number((fallbackImage as any)?.maxPixelValue);
slope = Number((fallbackImage as any)?.slope ?? slope);
intercept = Number((fallbackImage as any)?.intercept ?? intercept);
if (Number.isFinite(minRaw) && Number.isFinite(maxRaw) && maxRaw > minRaw) {
break;
}
} catch (innerErr) {
// Keep trying additional images in the stack.
}
}
}
}
const centerMeta = Array.isArray(voiMeta.windowCenter) ? voiMeta.windowCenter[0] : voiMeta.windowCenter;
@@ -2528,9 +2558,13 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
});
}, [applyDefaultWindowLevel, updateOverlay, viewportImageIds]);
const applyPresetOption = useCallback((viewportIdx: number, preset: PresetOption) => {
const applyPresetOption = useCallback((viewportIdx: number, preset: PresetOption, persistSelection = true, resetCameraOnReset = true) => {
if (persistSelection) {
activePresetByViewportRef.current[viewportIdx] = preset;
}
if (preset.action === "reset") {
applyDefaultWindowLevel(viewportIdx, true);
applyDefaultWindowLevel(viewportIdx, resetCameraOnReset);
return;
}
if (preset.action === "autolevel") {
@@ -2541,6 +2575,42 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
handleApplyPreset(viewportIdx, wlPreset.center, wlPreset.width);
}, [applyDefaultWindowLevel, handleApplyPreset, handleAutoLevel]);
useEffect(() => {
const applyPresetOnImageChange = () => {
const renderingEngine = renderingEngineRef.current;
if (!renderingEngine) return;
const visibleCount = viewportGrid.rows * viewportGrid.cols;
for (let i = 0; i < visibleCount; i++) {
if (viewportModes[i] !== "stack") continue;
const preset = activePresetByViewportRef.current[i];
if (!preset) continue;
const viewport = renderingEngine.getViewport(`CT_${i}`);
if (!viewport || typeof viewport.getImageIds !== "function") continue;
const ids = viewport.getImageIds() || [];
if (!ids.length) continue;
const idx = typeof viewport.getCurrentImageIdIndex === "function"
? viewport.getCurrentImageIdIndex()
: 0;
const currentImageId = ids[idx] || ids[0];
if (!currentImageId) continue;
if (lastRenderedImageIdByViewportRef.current[i] === currentImageId) {
continue;
}
lastRenderedImageIdByViewportRef.current[i] = currentImageId;
applyPresetOption(i, preset, false, false);
}
};
const timer = window.setTimeout(applyPresetOnImageChange, 0);
return () => window.clearTimeout(timer);
}, [applyPresetOption, imageInfos, viewportGrid, viewportModes]);
useEffect(() => {
const handlePresetHotkey = (e: KeyboardEvent) => {
const target = e.target as HTMLElement | null;