Refactor stack thumbnail management with dedicated setter and localStorage integration for stack panel width
This commit is contained in:
+94
-10
@@ -218,6 +218,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
// Right-side stack panel open state (toggled by button)
|
||||
const [stackPanelOpen, setStackPanelOpen] = useState(false);
|
||||
const [stackPanelWidth, setStackPanelWidth] = useState(320);
|
||||
const STACK_PANEL_WIDTH_KEY = `dv3d_stackPanelWidth_${container_id || 'default'}`;
|
||||
const stackPanelResizeRef = useRef<{ resizing: boolean; startX: number; startWidth: number }>({
|
||||
resizing: false,
|
||||
startX: 0,
|
||||
@@ -278,8 +279,65 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
|
||||
// Client-side generated thumbnails cache: stackIndex -> dataURL
|
||||
const [stackThumbnails, setStackThumbnails] = useState<Record<number, string>>({});
|
||||
const stackThumbnailsRef = useRef<Record<number, string>>({});
|
||||
const thumbRequestedRef = useRef<Record<number, boolean>>({});
|
||||
|
||||
const setStackThumbnail = useCallback((stackIdx: number, value: string) => {
|
||||
setStackThumbnails(prev => {
|
||||
const previous = prev[stackIdx];
|
||||
if (previous && previous !== value && previous.startsWith('blob:')) {
|
||||
try {
|
||||
URL.revokeObjectURL(previous);
|
||||
} catch (e) {
|
||||
// ignore revoke failures
|
||||
}
|
||||
}
|
||||
const next = { ...prev, [stackIdx]: value };
|
||||
stackThumbnailsRef.current = next;
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
stackThumbnailsRef.current = stackThumbnails;
|
||||
}, [stackThumbnails]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
Object.values(stackThumbnailsRef.current).forEach((thumbnail) => {
|
||||
if (typeof thumbnail === 'string' && thumbnail.startsWith('blob:')) {
|
||||
try {
|
||||
URL.revokeObjectURL(thumbnail);
|
||||
} catch (e) {
|
||||
// ignore revoke failures
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
const saved = localStorage.getItem(STACK_PANEL_WIDTH_KEY);
|
||||
if (saved) {
|
||||
const parsed = Number(saved);
|
||||
if (!Number.isNaN(parsed)) {
|
||||
setStackPanelWidth(Math.max(STACK_PANEL_MIN_WIDTH, Math.min(STACK_PANEL_MAX_WIDTH, Math.round(parsed))));
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore storage access errors
|
||||
}
|
||||
}, [STACK_PANEL_WIDTH_KEY]);
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
localStorage.setItem(STACK_PANEL_WIDTH_KEY, String(Math.round(stackPanelWidth)));
|
||||
} catch (e) {
|
||||
// ignore storage access errors
|
||||
}
|
||||
}, [STACK_PANEL_WIDTH_KEY, stackPanelWidth]);
|
||||
|
||||
// Helper to find a canvas element under a given node (recursive)
|
||||
function findCanvasIn(node: ParentNode | null): HTMLCanvasElement | null {
|
||||
if (!node) return null;
|
||||
@@ -769,13 +827,13 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
ctx.fillRect(0, 0, size, size);
|
||||
ctx.drawImage(img, x, y, w, h);
|
||||
const dataUrl = canvas.toDataURL('image/png');
|
||||
setStackThumbnails(prev => ({ ...prev, [stackIdx]: dataUrl }));
|
||||
setStackThumbnail(stackIdx, dataUrl);
|
||||
} else {
|
||||
setStackThumbnails(prev => ({ ...prev, [stackIdx]: 'BROKEN' }));
|
||||
setStackThumbnail(stackIdx, 'BROKEN');
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('generateThumbnail: failed to generate thumbnail for external image', err);
|
||||
setStackThumbnails(prev => ({ ...prev, [stackIdx]: 'BROKEN' }));
|
||||
setStackThumbnail(stackIdx, 'BROKEN');
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -868,30 +926,46 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
|
||||
if (canvas) {
|
||||
try {
|
||||
const dataUrl = canvas.toDataURL('image/png');
|
||||
setStackThumbnails(prev => ({ ...prev, [stackIdx]: dataUrl }));
|
||||
if (typeof canvas.toBlob === 'function') {
|
||||
const blobUrl = await new Promise<string | null>((resolve) => {
|
||||
canvas.toBlob((blob) => {
|
||||
if (!blob) {
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
resolve(URL.createObjectURL(blob));
|
||||
}, 'image/png');
|
||||
});
|
||||
if (blobUrl) {
|
||||
setStackThumbnail(stackIdx, blobUrl);
|
||||
} else {
|
||||
setStackThumbnail(stackIdx, canvas.toDataURL('image/png'));
|
||||
}
|
||||
} else {
|
||||
setStackThumbnail(stackIdx, canvas.toDataURL('image/png'));
|
||||
}
|
||||
console.debug('generateThumbnail: thumbnail captured for stack', stackIdx);
|
||||
} catch (err) {
|
||||
console.warn('generateThumbnail: toDataURL failed', err);
|
||||
// Mark as broken so UI shows placeholder instead of attempting to load raw DICOM URL
|
||||
setStackThumbnails(prev => ({ ...prev, [stackIdx]: 'BROKEN' }));
|
||||
setStackThumbnail(stackIdx, 'BROKEN');
|
||||
}
|
||||
} else {
|
||||
console.warn('generateThumbnail: no canvas found after render for', viewportId, 'canvasReady=', canvasReady, 'vp.element=', !!(vp && vp.element));
|
||||
// Mark as broken so we don't attempt to use raw .dcm URL as <img> src
|
||||
setStackThumbnails(prev => ({ ...prev, [stackIdx]: 'BROKEN' }));
|
||||
setStackThumbnail(stackIdx, 'BROKEN');
|
||||
}
|
||||
} catch (err) {
|
||||
// log and mark broken so UI shows placeholder
|
||||
console.warn('Thumbnail generation failed for', chosenImageId, err);
|
||||
try { setStackThumbnails(prev => ({ ...prev, [stackIdx]: 'BROKEN' })); } catch (e) { /* ignore */ }
|
||||
try { setStackThumbnail(stackIdx, 'BROKEN'); } catch (e) { /* ignore */ }
|
||||
} finally {
|
||||
try { renderingEngine.disableElement(viewportId); } catch (e) {
|
||||
console.warn('Failed to disable thumbnail element:', e);
|
||||
}
|
||||
if (el && el.parentNode) el.parentNode.removeChild(el);
|
||||
}
|
||||
}, [container_id, stackThumbnails]);
|
||||
}, [container_id, stackThumbnails, setStackThumbnail]);
|
||||
|
||||
// Kick off thumbnail generation when stacks change
|
||||
useEffect(() => {
|
||||
@@ -2775,6 +2849,16 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
setViewportImageIds(Array(MAX_VIEWPORTS).fill([]));
|
||||
setSelectedStackIdx(Array(MAX_VIEWPORTS).fill(0));
|
||||
setViewportModes(Array(MAX_VIEWPORTS).fill('stack'));
|
||||
Object.values(stackThumbnailsRef.current).forEach((thumbnail) => {
|
||||
if (typeof thumbnail === 'string' && thumbnail.startsWith('blob:')) {
|
||||
try {
|
||||
URL.revokeObjectURL(thumbnail);
|
||||
} catch (e) {
|
||||
// ignore revoke failures
|
||||
}
|
||||
}
|
||||
});
|
||||
stackThumbnailsRef.current = {};
|
||||
setStackThumbnails({});
|
||||
thumbRequestedRef.current = {};
|
||||
setStackOrderModified(false);
|
||||
@@ -5215,7 +5299,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
onError={(e) => {
|
||||
console.warn('Thumbnail <img> failed to load for stack', idx, src);
|
||||
// Mark thumbnail as broken so we render the placeholder instead
|
||||
setStackThumbnails(prev => ({ ...prev, [idx]: 'BROKEN' }));
|
||||
setStackThumbnail(idx, 'BROKEN');
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user