Enhance debugging capabilities by adding detailed logging for thumbnail generation and viewer mounting processes
This commit is contained in:
+331
-9
@@ -30,6 +30,7 @@ declare global {
|
||||
cornerstone3dTools: typeof cornerstoneTools;
|
||||
cornerstone3d: typeof cornerstone3d;
|
||||
cornerstoneDICOMImageLoader: typeof cornerstoneDICOMImageLoader;
|
||||
__DV3D_THUMB_VERBOSE__?: boolean;
|
||||
|
||||
}
|
||||
}
|
||||
@@ -160,6 +161,17 @@ const TOOL_OPTIONS = [
|
||||
{ label: "Reference Cursors", value: ReferenceCursors.toolName, icon: "⦿" },
|
||||
];
|
||||
|
||||
const THUMB_DEBUG_BUFFER_MAX = 500;
|
||||
const THUMB_CAPTURE_MAX_RETRIES = 3;
|
||||
const THUMB_CAPTURE_RETRY_BASE_MS = 220;
|
||||
const THUMB_DEBUG_HIGH_FREQ_EVENTS = new Set([
|
||||
'stackThumbnails-state',
|
||||
'thumbnail-generation-check',
|
||||
'thumbnail-candidate',
|
||||
'thumbnail-render-source',
|
||||
'thumbnail-img-load',
|
||||
]);
|
||||
|
||||
function getToolIcon(toolName?: string) {
|
||||
if (!toolName) return '🔖';
|
||||
const found = TOOL_OPTIONS.find(o => o.value === toolName || o.label === toolName);
|
||||
@@ -172,6 +184,66 @@ function stripWadouriPrefix(imageId?: string): string {
|
||||
return imageId.replace(/^wadouri:+/, "").replace(/^:+/, "");
|
||||
}
|
||||
|
||||
function truncateForDebug(value: string, maxLen = 180): string {
|
||||
if (!value) return value;
|
||||
if (value.length <= maxLen) return value;
|
||||
return `${value.slice(0, maxLen)}...(${value.length} chars)`;
|
||||
}
|
||||
|
||||
function isLikelyDicomThumbnailSource(src?: string | null): boolean {
|
||||
if (!src) return false;
|
||||
const lower = src.toLowerCase();
|
||||
if (lower.startsWith('wadouri:')) return true;
|
||||
if (lower.includes('.dcm')) return true;
|
||||
if (lower.includes('/dicom') || lower.includes('/wado')) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function isCanvasLikelyReadyForThumbnail(canvas: HTMLCanvasElement): boolean {
|
||||
try {
|
||||
const ctx = canvas.getContext('2d', { willReadFrequently: true } as CanvasRenderingContext2DSettings);
|
||||
if (!ctx) return true;
|
||||
|
||||
const width = canvas.width || 160;
|
||||
const height = canvas.height || 160;
|
||||
const imageData = ctx.getImageData(0, 0, width, height);
|
||||
const data = imageData.data;
|
||||
|
||||
let minLum = 255;
|
||||
let maxLum = 0;
|
||||
let nonDarkCount = 0;
|
||||
let sampleCount = 0;
|
||||
|
||||
for (let y = 0; y < height; y += 8) {
|
||||
for (let x = 0; x < width; x += 8) {
|
||||
const i = (y * width + x) * 4;
|
||||
const r = data[i];
|
||||
const g = data[i + 1];
|
||||
const b = data[i + 2];
|
||||
const a = data[i + 3];
|
||||
const lum = (r + g + b) / 3;
|
||||
|
||||
if (a > 0 && lum > 12) {
|
||||
nonDarkCount += 1;
|
||||
}
|
||||
|
||||
minLum = Math.min(minLum, lum);
|
||||
maxLum = Math.max(maxLum, lum);
|
||||
sampleCount += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (sampleCount === 0) return true;
|
||||
|
||||
const nonDarkRatio = nonDarkCount / sampleCount;
|
||||
const dynamicRange = maxLum - minLum;
|
||||
return nonDarkRatio > 0.015 || dynamicRange > 8;
|
||||
} catch (e) {
|
||||
// If pixel inspection fails, do not block thumbnail generation.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Mouse button labels
|
||||
const MOUSE_BUTTONS = [
|
||||
{ label: "Left", value: "Primary" },
|
||||
@@ -281,10 +353,59 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
const [stackThumbnails, setStackThumbnails] = useState<Record<number, string>>({});
|
||||
const stackThumbnailsRef = useRef<Record<number, string>>({});
|
||||
const thumbRequestedRef = useRef<Record<number, boolean>>({});
|
||||
const thumbDebugPrefix = `[DV3D Thumb][${container_id || 'default'}]`;
|
||||
const thumbDebugBufferRef = useRef<Array<Record<string, unknown>>>([]);
|
||||
const thumbDebugLastLogRef = useRef<Record<string, number>>({});
|
||||
|
||||
const thumbDebug = useCallback((event: string, details?: Record<string, unknown>) => {
|
||||
try {
|
||||
const payload = details || {};
|
||||
const isHighFrequency = THUMB_DEBUG_HIGH_FREQ_EVENTS.has(event);
|
||||
const stackPart = typeof payload.stackIdx === 'number' ? String(payload.stackIdx) : (typeof payload.idx === 'number' ? String(payload.idx) : 'global');
|
||||
const key = `${event}:${stackPart}`;
|
||||
const now = Date.now();
|
||||
const last = thumbDebugLastLogRef.current[key] || 0;
|
||||
const verbose = typeof window !== 'undefined' && !!window.__DV3D_THUMB_VERBOSE__;
|
||||
|
||||
if (isHighFrequency && !verbose && now - last < 2500) {
|
||||
return;
|
||||
}
|
||||
|
||||
thumbDebugLastLogRef.current[key] = now;
|
||||
|
||||
const entry = {
|
||||
ts: new Date().toISOString(),
|
||||
event,
|
||||
container: container_id || 'default',
|
||||
path: typeof window !== 'undefined' ? window.location.pathname : '',
|
||||
details: payload,
|
||||
};
|
||||
|
||||
thumbDebugBufferRef.current.push(entry);
|
||||
if (thumbDebugBufferRef.current.length > THUMB_DEBUG_BUFFER_MAX) {
|
||||
thumbDebugBufferRef.current.shift();
|
||||
}
|
||||
|
||||
console.debug(thumbDebugPrefix, event, payload);
|
||||
} catch (e) {
|
||||
// ignore debug logging failures
|
||||
}
|
||||
}, [thumbDebugPrefix, container_id]);
|
||||
|
||||
const setStackThumbnail = useCallback((stackIdx: number, value: string) => {
|
||||
setStackThumbnails(prev => {
|
||||
const previous = prev[stackIdx];
|
||||
try {
|
||||
console.debug(thumbDebugPrefix, 'setStackThumbnail', {
|
||||
stackIdx,
|
||||
previous: previous ? truncateForDebug(previous, 120) : null,
|
||||
next: value ? truncateForDebug(value, 120) : null,
|
||||
previousWasBlob: !!(previous && previous.startsWith('blob:')),
|
||||
nextIsBlob: !!(value && value.startsWith('blob:')),
|
||||
});
|
||||
} catch (e) {
|
||||
// ignore debug logging failures
|
||||
}
|
||||
if (previous && previous !== value && previous.startsWith('blob:')) {
|
||||
try {
|
||||
URL.revokeObjectURL(previous);
|
||||
@@ -296,11 +417,18 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
stackThumbnailsRef.current = next;
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
}, [thumbDebugPrefix]);
|
||||
|
||||
useEffect(() => {
|
||||
stackThumbnailsRef.current = stackThumbnails;
|
||||
}, [stackThumbnails]);
|
||||
const entries = Object.entries(stackThumbnails).map(([idx, value]) => ({
|
||||
idx: Number(idx),
|
||||
value: truncateForDebug(value, 100),
|
||||
isBlob: value.startsWith('blob:'),
|
||||
isBroken: value === 'BROKEN',
|
||||
}));
|
||||
thumbDebug('stackThumbnails-state', { count: entries.length, entries });
|
||||
}, [stackThumbnails, thumbDebug]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
@@ -685,6 +813,23 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
})
|
||||
);
|
||||
|
||||
thumbDebug('imageStacks-loaded', {
|
||||
descriptorCount: stacksRaw.length,
|
||||
normalizedCount: normalized.length,
|
||||
summary: normalized.map((stack, idx) => {
|
||||
const ids = Array.isArray(stack.imageIds) ? stack.imageIds : [];
|
||||
const mid = ids[Math.floor(ids.length / 2)] || ids[0] || '';
|
||||
return {
|
||||
idx,
|
||||
name: stack.name || null,
|
||||
caseId: stack.caseId || null,
|
||||
studyId: stack.studyId || null,
|
||||
imageCount: ids.length,
|
||||
sampleImageId: truncateForDebug(mid, 180),
|
||||
};
|
||||
}),
|
||||
});
|
||||
|
||||
setAvailableStacks(normalized);
|
||||
|
||||
// Seed viewports with first stack if present (preserve old behavior)
|
||||
@@ -697,12 +842,15 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
})
|
||||
.catch(err => {
|
||||
console.error("Failed to load imageStacks:", err);
|
||||
thumbDebug('imageStacks-load-failed', {
|
||||
error: String((err as any)?.message || err),
|
||||
});
|
||||
setAvailableStacks([]);
|
||||
setViewportImageIds(Array(MAX_VIEWPORTS).fill([]));
|
||||
});
|
||||
|
||||
return () => { mounted = false; };
|
||||
}, []);
|
||||
}, [thumbDebug]);
|
||||
|
||||
const handleLoadStack = async (viewportIdx: number, stackIdx: number) => {
|
||||
const stackObj = availableStacks[stackIdx];
|
||||
@@ -784,24 +932,34 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
});
|
||||
};
|
||||
|
||||
const generateThumbnail = useCallback(async (stackIdx: number, chosenImageId: string | null) => {
|
||||
const generateThumbnail = useCallback(async (stackIdx: number, chosenImageId: string | null, retryAttempt = 0) => {
|
||||
if (!chosenImageId) {
|
||||
console.warn('generateThumbnail: no chosenImageId for stack', stackIdx);
|
||||
thumbDebug('generate-skip-no-image', { stackIdx });
|
||||
return;
|
||||
}
|
||||
// avoid regenerating
|
||||
if (stackThumbnails[stackIdx]) {
|
||||
console.debug('generateThumbnail: thumbnail already exists for', stackIdx);
|
||||
thumbDebug('generate-skip-existing', {
|
||||
stackIdx,
|
||||
existing: truncateForDebug(stackThumbnails[stackIdx], 120),
|
||||
});
|
||||
// already have it
|
||||
return;
|
||||
}
|
||||
if (thumbRequestedRef.current[stackIdx]) {
|
||||
console.debug('generateThumbnail: already requested for', stackIdx);
|
||||
thumbDebug('generate-skip-requested', { stackIdx });
|
||||
return;
|
||||
}
|
||||
|
||||
// If the chosen image is an external non-DICOM image, draw it to a canvas directly
|
||||
if (isExternalImageId(chosenImageId)) {
|
||||
thumbDebug('generate-external-start', {
|
||||
stackIdx,
|
||||
chosenImageId: truncateForDebug(chosenImageId, 180),
|
||||
});
|
||||
try {
|
||||
const img = new Image();
|
||||
img.crossOrigin = 'anonymous';
|
||||
@@ -833,6 +991,11 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('generateThumbnail: failed to generate thumbnail for external image', err);
|
||||
thumbDebug('generate-external-failed', {
|
||||
stackIdx,
|
||||
chosenImageId: truncateForDebug(chosenImageId, 180),
|
||||
error: String((err as any)?.message || err),
|
||||
});
|
||||
setStackThumbnail(stackIdx, 'BROKEN');
|
||||
}
|
||||
return;
|
||||
@@ -841,6 +1004,10 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
const renderingEngine = renderingEngineRef.current;
|
||||
if (!renderingEngine) {
|
||||
console.warn('generateThumbnail: no renderingEngine yet for', stackIdx);
|
||||
thumbDebug('generate-skip-no-engine', {
|
||||
stackIdx,
|
||||
chosenImageId: truncateForDebug(chosenImageId, 180),
|
||||
});
|
||||
return;
|
||||
}
|
||||
// mark requested once we are sure the rendering engine exists
|
||||
@@ -848,6 +1015,12 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
console.debug('generateThumbnail: starting for', stackIdx, chosenImageId);
|
||||
|
||||
const viewportId = `THUMB_${container_id || 'main'}_${stackIdx}`;
|
||||
thumbDebug('generate-start', {
|
||||
stackIdx,
|
||||
chosenImageId: truncateForDebug(chosenImageId, 180),
|
||||
viewportId,
|
||||
retryAttempt,
|
||||
});
|
||||
const el = document.createElement('div');
|
||||
el.style.width = '160px';
|
||||
el.style.height = '160px';
|
||||
@@ -861,6 +1034,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
const vp = renderingEngine.getViewport(viewportId);
|
||||
if (!vp) {
|
||||
console.warn('generateThumbnail: no viewport returned after enableElement for', viewportId);
|
||||
thumbDebug('generate-failed-no-viewport', { stackIdx, viewportId });
|
||||
return;
|
||||
}
|
||||
// Use single-image stack for thumbnail
|
||||
@@ -868,6 +1042,11 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
vp.setStack([chosenImageId]);
|
||||
} catch (err) {
|
||||
console.warn('generateThumbnail: vp.setStack failed', err);
|
||||
thumbDebug('generate-setStack-failed', {
|
||||
stackIdx,
|
||||
viewportId,
|
||||
error: String((err as any)?.message || err),
|
||||
});
|
||||
}
|
||||
|
||||
// Wait for IMAGE_RENDERED event on the temporary viewport element or timeout
|
||||
@@ -877,6 +1056,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
console.warn('generateThumbnail: IMAGE_RENDERED timeout for', viewportId);
|
||||
thumbDebug('generate-render-timeout', { stackIdx, viewportId });
|
||||
resolve(false);
|
||||
}
|
||||
}, 2000);
|
||||
@@ -886,6 +1066,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
resolved = true;
|
||||
clearTimeout(timeout);
|
||||
console.debug('generateThumbnail: IMAGE_RENDERED received for', viewportId);
|
||||
thumbDebug('generate-rendered', { stackIdx, viewportId });
|
||||
resolve(true);
|
||||
};
|
||||
|
||||
@@ -895,6 +1076,11 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
vp.element.addEventListener(IMAGE_RENDERED, onRendered, { once: true } as any);
|
||||
} catch (err) {
|
||||
console.warn('generateThumbnail: failed to add IMAGE_RENDERED listener', err);
|
||||
thumbDebug('generate-listener-failed', {
|
||||
stackIdx,
|
||||
viewportId,
|
||||
error: String((err as any)?.message || err),
|
||||
});
|
||||
}
|
||||
}
|
||||
// Trigger a render in case it doesn't auto-render.
|
||||
@@ -912,6 +1098,11 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
} catch (err) {
|
||||
clearTimeout(timeout);
|
||||
console.warn('generateThumbnail: error while waiting for render', err);
|
||||
thumbDebug('generate-render-wait-error', {
|
||||
stackIdx,
|
||||
viewportId,
|
||||
error: String((err as any)?.message || err),
|
||||
});
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
@@ -920,11 +1111,43 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
let canvas = findCanvasIn(el) || (vp && vp.element ? findCanvasIn(vp.element) : null);
|
||||
if (!canvas) {
|
||||
console.debug('generateThumbnail: no canvas found immediately, waiting briefly and retrying for', viewportId, 'canvasReady=', canvasReady);
|
||||
thumbDebug('generate-canvas-missing-initial', { stackIdx, viewportId, canvasReady });
|
||||
await new Promise(r => setTimeout(r, 150));
|
||||
canvas = findCanvasIn(el) || (vp && vp.element ? findCanvasIn(vp.element) : null);
|
||||
}
|
||||
|
||||
if (canvas) {
|
||||
thumbDebug('generate-canvas-found', {
|
||||
stackIdx,
|
||||
viewportId,
|
||||
canvasWidth: canvas.width,
|
||||
canvasHeight: canvas.height,
|
||||
});
|
||||
|
||||
const readyForCapture = isCanvasLikelyReadyForThumbnail(canvas);
|
||||
if (!readyForCapture) {
|
||||
if (retryAttempt < THUMB_CAPTURE_MAX_RETRIES) {
|
||||
const retryDelayMs = THUMB_CAPTURE_RETRY_BASE_MS * (retryAttempt + 1);
|
||||
thumbDebug('generate-canvas-not-ready-retry', {
|
||||
stackIdx,
|
||||
viewportId,
|
||||
retryAttempt,
|
||||
retryDelayMs,
|
||||
});
|
||||
thumbRequestedRef.current[stackIdx] = false;
|
||||
setTimeout(() => {
|
||||
generateThumbnail(stackIdx, chosenImageId, retryAttempt + 1);
|
||||
}, retryDelayMs);
|
||||
return;
|
||||
}
|
||||
|
||||
thumbDebug('generate-canvas-not-ready-final-attempt', {
|
||||
stackIdx,
|
||||
viewportId,
|
||||
retryAttempt,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
if (typeof canvas.toBlob === 'function') {
|
||||
const blobUrl = await new Promise<string | null>((resolve) => {
|
||||
@@ -937,50 +1160,87 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
}, 'image/png');
|
||||
});
|
||||
if (blobUrl) {
|
||||
thumbDebug('generate-toBlob-success', {
|
||||
stackIdx,
|
||||
viewportId,
|
||||
blobUrl: truncateForDebug(blobUrl, 120),
|
||||
});
|
||||
setStackThumbnail(stackIdx, blobUrl);
|
||||
} else {
|
||||
thumbDebug('generate-toBlob-empty-fallback-dataUrl', { stackIdx, viewportId });
|
||||
setStackThumbnail(stackIdx, canvas.toDataURL('image/png'));
|
||||
}
|
||||
} else {
|
||||
thumbDebug('generate-no-toBlob-fallback-dataUrl', { stackIdx, viewportId });
|
||||
setStackThumbnail(stackIdx, canvas.toDataURL('image/png'));
|
||||
}
|
||||
console.debug('generateThumbnail: thumbnail captured for stack', stackIdx);
|
||||
thumbDebug('generate-success', { stackIdx, viewportId });
|
||||
} catch (err) {
|
||||
console.warn('generateThumbnail: toDataURL failed', err);
|
||||
thumbDebug('generate-data-extract-failed', {
|
||||
stackIdx,
|
||||
viewportId,
|
||||
error: String((err as any)?.message || err),
|
||||
});
|
||||
// Mark as broken so UI shows placeholder instead of attempting to load raw DICOM URL
|
||||
setStackThumbnail(stackIdx, 'BROKEN');
|
||||
}
|
||||
} else {
|
||||
console.warn('generateThumbnail: no canvas found after render for', viewportId, 'canvasReady=', canvasReady, 'vp.element=', !!(vp && vp.element));
|
||||
thumbDebug('generate-failed-no-canvas', {
|
||||
stackIdx,
|
||||
viewportId,
|
||||
canvasReady,
|
||||
hasViewportElement: !!(vp && vp.element),
|
||||
});
|
||||
// Mark as broken so we don't attempt to use raw .dcm URL as <img> src
|
||||
setStackThumbnail(stackIdx, 'BROKEN');
|
||||
}
|
||||
} catch (err) {
|
||||
// log and mark broken so UI shows placeholder
|
||||
console.warn('Thumbnail generation failed for', chosenImageId, err);
|
||||
thumbDebug('generate-failed', {
|
||||
stackIdx,
|
||||
chosenImageId: truncateForDebug(chosenImageId || '', 180),
|
||||
error: String((err as any)?.message || err),
|
||||
});
|
||||
try { setStackThumbnail(stackIdx, 'BROKEN'); } catch (e) { /* ignore */ }
|
||||
} finally {
|
||||
try { renderingEngine.disableElement(viewportId); } catch (e) {
|
||||
console.warn('Failed to disable thumbnail element:', e);
|
||||
}
|
||||
thumbDebug('generate-cleanup', { stackIdx, viewportId });
|
||||
if (el && el.parentNode) el.parentNode.removeChild(el);
|
||||
}
|
||||
}, [container_id, stackThumbnails, setStackThumbnail]);
|
||||
}, [container_id, stackThumbnails, setStackThumbnail, thumbDebug]);
|
||||
|
||||
// Kick off thumbnail generation when stacks change
|
||||
useEffect(() => {
|
||||
if (!availableStacks || availableStacks.length === 0) return;
|
||||
thumbDebug('thumbnail-generation-check', {
|
||||
stackCount: availableStacks.length,
|
||||
requested: { ...thumbRequestedRef.current },
|
||||
thumbnailKeys: Object.keys(stackThumbnails),
|
||||
});
|
||||
const renderingEngine = renderingEngineRef.current;
|
||||
if (!renderingEngine) return; // wait until rendering engine exists
|
||||
availableStacks.forEach((stack, idx) => {
|
||||
const imageIds = Array.isArray(stack.imageIds) ? stack.imageIds : [];
|
||||
const midIndex = Math.floor(imageIds.length / 2);
|
||||
const chosen = imageIds[midIndex] || imageIds[0] || null;
|
||||
thumbDebug('thumbnail-candidate', {
|
||||
idx,
|
||||
imageCount: imageIds.length,
|
||||
chosen: truncateForDebug(chosen || '', 180),
|
||||
hasThumbnail: !!stackThumbnails[idx],
|
||||
requested: !!thumbRequestedRef.current[idx],
|
||||
});
|
||||
if (!stackThumbnails[idx] && !thumbRequestedRef.current[idx]) {
|
||||
generateThumbnail(idx, chosen as string | null);
|
||||
}
|
||||
});
|
||||
}, [availableStacks, generateThumbnail, stackThumbnails]);
|
||||
}, [availableStacks, generateThumbnail, stackThumbnails, thumbDebug]);
|
||||
|
||||
// Wrap setLoadedImageIds to also cache DICOM metadata
|
||||
const setLoadedImageIdsAndCacheMeta = async (imageIds: string[], options?: { force?: boolean }) => {
|
||||
@@ -3534,6 +3794,18 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
updateOverlay(activeViewport ?? 0, viewport);
|
||||
};
|
||||
|
||||
window[`exportThumbDebug_${apiKey}`] = () => {
|
||||
try {
|
||||
return JSON.stringify(thumbDebugBufferRef.current, null, 2);
|
||||
} catch (e) {
|
||||
return '[]';
|
||||
}
|
||||
};
|
||||
window[`clearThumbDebug_${apiKey}`] = () => {
|
||||
thumbDebugBufferRef.current = [];
|
||||
return true;
|
||||
};
|
||||
|
||||
|
||||
|
||||
// Cleanup
|
||||
@@ -3552,6 +3824,8 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
window[`loadAdditionalStack_${apiKey}`] = undefined;
|
||||
window[`getViewport_${apiKey}`] = undefined;
|
||||
window[`getAllViewports_${apiKey}`] = undefined;
|
||||
window[`exportThumbDebug_${apiKey}`] = undefined;
|
||||
window[`clearThumbDebug_${apiKey}`] = undefined;
|
||||
};
|
||||
}, [
|
||||
viewportGrid,
|
||||
@@ -5358,18 +5632,66 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
// If generation explicitly failed, treat as missing so placeholder is shown
|
||||
const generatedValid = generated && generated !== 'BROKEN' ? generated : null;
|
||||
const src = generatedValid || thumbnailUrl || null;
|
||||
thumbDebug('thumbnail-render-source', {
|
||||
idx,
|
||||
label,
|
||||
generated: generated ? truncateForDebug(generated, 120) : null,
|
||||
generatedValid: generatedValid ? truncateForDebug(generatedValid, 120) : null,
|
||||
fallback: thumbnailUrl ? truncateForDebug(thumbnailUrl, 180) : null,
|
||||
chosenImage: chosenImage ? truncateForDebug(chosenImage, 180) : null,
|
||||
src: src ? truncateForDebug(src, 180) : null,
|
||||
requested: !!thumbRequestedRef.current[idx],
|
||||
});
|
||||
if (!src) {
|
||||
return (<div style={{ width: 56, height: 56, borderRadius: 4, background: '#111', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#777' }}>No Img</div>);
|
||||
}
|
||||
return (
|
||||
<img
|
||||
data-thumb-stack={idx}
|
||||
src={src}
|
||||
alt={label}
|
||||
style={{ width: 56, height: 56, objectFit: 'cover', borderRadius: 4, background: '#111' }}
|
||||
onError={(e) => {
|
||||
onLoad={(e) => {
|
||||
const el = e.currentTarget;
|
||||
thumbDebug('thumbnail-img-load', {
|
||||
idx,
|
||||
label,
|
||||
src: truncateForDebug(src, 180),
|
||||
naturalWidth: el.naturalWidth,
|
||||
naturalHeight: el.naturalHeight,
|
||||
clientWidth: el.clientWidth,
|
||||
clientHeight: el.clientHeight,
|
||||
});
|
||||
}}
|
||||
onError={() => {
|
||||
console.warn('Thumbnail <img> failed to load for stack', idx, src);
|
||||
// Mark thumbnail as broken so we render the placeholder instead
|
||||
setStackThumbnail(idx, 'BROKEN');
|
||||
const usedGeneratedImage = !!generatedValid;
|
||||
const likelyDicomFallback = !usedGeneratedImage && isLikelyDicomThumbnailSource(src);
|
||||
thumbDebug('thumbnail-img-error', {
|
||||
idx,
|
||||
label,
|
||||
src: truncateForDebug(src, 180),
|
||||
usedGeneratedImage,
|
||||
likelyDicomFallback,
|
||||
requested: !!thumbRequestedRef.current[idx],
|
||||
chosenImage: chosenImage ? truncateForDebug(chosenImage, 180) : null,
|
||||
});
|
||||
|
||||
if (usedGeneratedImage) {
|
||||
// Generated image failed: mark as broken to avoid endless reload attempts.
|
||||
setStackThumbnail(idx, 'BROKEN');
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback URL failed (often raw DICOM). Do not mark BROKEN yet;
|
||||
// request generation and keep the thumbnail pipeline alive.
|
||||
if (!thumbRequestedRef.current[idx]) {
|
||||
thumbDebug('thumbnail-img-error-trigger-generate', {
|
||||
idx,
|
||||
chosenImage: chosenImage ? truncateForDebug(chosenImage, 180) : null,
|
||||
});
|
||||
generateThumbnail(idx, chosenImage as string | null);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -53,6 +53,7 @@ export function mountDicomViewers() {
|
||||
const containers = document.querySelectorAll(".dicom-viewer-root");
|
||||
containers.forEach((container) => {
|
||||
const id = container.id || '';
|
||||
const mountPrefix = `[DV3D Mount][${id || 'default'}]`;
|
||||
let imageStacks: () => Promise<any[]>;
|
||||
const isEmpty = container.getAttribute('data-empty') === 'true';
|
||||
|
||||
@@ -60,6 +61,20 @@ export function mountDicomViewers() {
|
||||
const dataNamed = container.getAttribute('data-named-stacks');
|
||||
const dataImages = container.getAttribute('data-images');
|
||||
|
||||
try {
|
||||
console.debug(mountPrefix, 'container-attrs', {
|
||||
path: window.location.pathname,
|
||||
isEmpty,
|
||||
hasDataNamedStacks: !!dataNamed,
|
||||
hasDataImages: !!dataImages,
|
||||
dataNamedLength: dataNamed ? dataNamed.length : 0,
|
||||
dataImagesLength: dataImages ? dataImages.length : 0,
|
||||
autoCacheStack: container.getAttribute('data-auto-cache-stack'),
|
||||
});
|
||||
} catch (e) {
|
||||
// ignore debug logging failures
|
||||
}
|
||||
|
||||
const parseToDescriptors = (parsed: any): Array<any> => {
|
||||
// Return array of descriptors: either simple string[] => { imageIds: string[] }
|
||||
// or objects: { imageIds: string[], name?, caseId?, studyId? }
|
||||
@@ -143,6 +158,25 @@ export function mountDicomViewers() {
|
||||
parsed = [];
|
||||
}
|
||||
const descriptors = parseToDescriptors(parsed);
|
||||
try {
|
||||
console.debug(mountPrefix, 'parsed-data-named-stacks', {
|
||||
descriptorCount: descriptors.length,
|
||||
summary: descriptors.map((d, idx) => {
|
||||
const imageIds = Array.isArray(d.imageIds) ? d.imageIds : [];
|
||||
const sample = imageIds[Math.floor(imageIds.length / 2)] || imageIds[0] || '';
|
||||
return {
|
||||
idx,
|
||||
name: d.name || null,
|
||||
caseId: d.caseId || null,
|
||||
studyId: d.studyId || null,
|
||||
imageCount: imageIds.length,
|
||||
sampleImageId: sample.length > 180 ? `${sample.slice(0, 180)}...(${sample.length} chars)` : sample,
|
||||
};
|
||||
}),
|
||||
});
|
||||
} catch (e) {
|
||||
// ignore debug logging failures
|
||||
}
|
||||
imageStacks = () => Promise.resolve(descriptors);
|
||||
} else if (dataImages) {
|
||||
let parsed;
|
||||
@@ -152,6 +186,14 @@ export function mountDicomViewers() {
|
||||
console.error("Invalid data-images JSON:", e);
|
||||
parsed = [];
|
||||
}
|
||||
try {
|
||||
console.debug(mountPrefix, 'parsed-data-images', {
|
||||
topLevelIsArray: Array.isArray(parsed),
|
||||
topLevelLength: Array.isArray(parsed) ? parsed.length : 0,
|
||||
});
|
||||
} catch (e) {
|
||||
// ignore debug logging failures
|
||||
}
|
||||
// Keep old behavior for data-images
|
||||
if (Array.isArray(parsed) && parsed.length > 0 && typeof parsed[0] === "string") {
|
||||
imageStacks = () => Promise.resolve([toWadouri(parsed)]);
|
||||
@@ -174,6 +216,17 @@ export function mountDicomViewers() {
|
||||
const viewerState = container.getAttribute('data-viewerstate');
|
||||
console.log("Mounting DICOM viewer:")
|
||||
console.log(viewerState, annotationJson, autoCacheStack);
|
||||
try {
|
||||
console.debug(mountPrefix, 'mount-props', {
|
||||
hasViewerState: !!viewerState,
|
||||
viewerStateLength: viewerState ? viewerState.length : 0,
|
||||
hasAnnotationJson: !!annotationJson,
|
||||
annotationJsonLength: annotationJson ? annotationJson.length : 0,
|
||||
autoCacheStack,
|
||||
});
|
||||
} catch (e) {
|
||||
// ignore debug logging failures
|
||||
}
|
||||
|
||||
createRoot(container).render(
|
||||
<App
|
||||
|
||||
Reference in New Issue
Block a user