diff --git a/dicom-viewer/src/App.tsx b/dicom-viewer/src/App.tsx index e92d739..8761a52 100644 --- a/dicom-viewer/src/App.tsx +++ b/dicom-viewer/src/App.tsx @@ -527,6 +527,12 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat const webSocketRef = useRef(null); + const logToPage = useCallback((msg: string, data?: any) => { + if (typeof (window as any).logDebug === "function") { + (window as any).logDebug(msg, data); + } + }, []); + const wsStateRef = useRef({ connected: false, isController: false, @@ -597,6 +603,7 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat const content = JSON.parse(messageEvent.data); const type = content.type; appLogger.info("Viewer WS event received (type=" + type + "):", content); + logToPage(`[Viewer WS] Received message (type=${type})`, content); if (type === "welcome") { setMyChannelName(content.channel_name); @@ -641,7 +648,7 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat setWsError(`Error initializing connection: ${e.message}`); setWsConnecting(false); } - }, [container_id]); + }, [container_id, logToPage]); const disconnectMultiuser = useCallback(() => { appLogger.info("Disconnecting viewer WS connection"); @@ -661,35 +668,43 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat const requestControl = useCallback(() => { if (webSocketRef.current && wsConnected) { appLogger.info("Sending request_control via WS"); - webSocketRef.current.send(JSON.stringify({ type: "request_control" })); + const msg = { type: "request_control" }; + webSocketRef.current.send(JSON.stringify(msg)); + logToPage("[Viewer WS] Sent message (type=request_control)", msg); } - }, [wsConnected]); + }, [wsConnected, logToPage]); const takeControl = useCallback(() => { if (webSocketRef.current && wsConnected && isStaff) { appLogger.info("Sending take_control via WS"); - webSocketRef.current.send(JSON.stringify({ type: "take_control" })); + const msg = { type: "take_control" }; + webSocketRef.current.send(JSON.stringify(msg)); + logToPage("[Viewer WS] Sent message (type=take_control)", msg); } - }, [wsConnected, isStaff]); + }, [wsConnected, isStaff, logToPage]); const grantControl = useCallback((targetChannel: string, targetUsername: string) => { if (webSocketRef.current && wsConnected) { appLogger.info("Sending grant_control via WS to:", targetUsername, targetChannel); - webSocketRef.current.send(JSON.stringify({ + const msg = { type: "grant_control", target_channel: targetChannel, target_username: targetUsername - })); + }; + webSocketRef.current.send(JSON.stringify(msg)); + logToPage("[Viewer WS] Sent message (type=grant_control)", msg); setControlRequestNotification(null); } - }, [wsConnected]); + }, [wsConnected, logToPage]); const releaseControl = useCallback(() => { if (webSocketRef.current && wsConnected) { appLogger.info("Sending release_control via WS"); - webSocketRef.current.send(JSON.stringify({ type: "release_control" })); + const msg = { type: "release_control" }; + webSocketRef.current.send(JSON.stringify(msg)); + logToPage("[Viewer WS] Sent message (type=release_control)", msg); } - }, [wsConnected]); + }, [wsConnected, logToPage]); useEffect(() => { if (multiuserRoom) { @@ -723,10 +738,12 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat lastStateSentRef.current = jsonState; if (webSocketRef.current && webSocketRef.current.readyState === WebSocket.OPEN) { appLogger.info("Sending viewer_state via WS:", jsonState); - webSocketRef.current.send(JSON.stringify({ + const msg = { type: "viewer_state", data: jsonState - })); + }; + webSocketRef.current.send(JSON.stringify(msg)); + logToPage("[Viewer WS] Sent message (type=viewer_state)", msg); } } } catch (err) { @@ -734,7 +751,7 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat } } }, 120); - }, [container_id]); + }, [container_id, logToPage]); // Throttled annotations send const lastAnnotationsSentRef = useRef(""); @@ -757,10 +774,12 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat lastAnnotationsSentRef.current = jsonAnn; if (webSocketRef.current && webSocketRef.current.readyState === WebSocket.OPEN) { appLogger.info("Sending annotations via WS:", jsonAnn); - webSocketRef.current.send(JSON.stringify({ + const msg = { type: "annotations", data: jsonAnn - })); + }; + webSocketRef.current.send(JSON.stringify(msg)); + logToPage("[Viewer WS] Sent message (type=annotations)", msg); } } } catch (err) { @@ -768,48 +787,8 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat } } }, 300); - }, [container_id]); + }, [container_id, logToPage]); - useEffect(() => { - const handleCornerstoneViewportEvent = () => { - const state = wsStateRef.current; - if (state.connected && state.isController && !restoringStateRef.current) { - throttleSendViewerState(); - } - }; - - const handleCornerstoneAnnotationEvent = () => { - const state = wsStateRef.current; - if (state.connected && state.isController && !restoringStateRef.current) { - throttleSendAnnotations(); - } - }; - - cornerstone3d.eventTarget.addEventListener(cornerstone3d.Enums.Events.CAMERA_MODIFIED, handleCornerstoneViewportEvent); - cornerstone3d.eventTarget.addEventListener(cornerstone3d.Enums.Events.VOI_MODIFIED, handleCornerstoneViewportEvent); - cornerstone3d.eventTarget.addEventListener(cornerstone3d.Enums.Events.IMAGE_RENDERED, handleCornerstoneViewportEvent); - cornerstone3d.eventTarget.addEventListener(cornerstone3d.Enums.Events.VOLUME_NEW_IMAGE, handleCornerstoneViewportEvent); - - cornerstone3d.eventTarget.addEventListener(cornerstoneTools.Enums.Events.ANNOTATION_ADDED, handleCornerstoneAnnotationEvent); - cornerstone3d.eventTarget.addEventListener(cornerstoneTools.Enums.Events.ANNOTATION_MODIFIED, handleCornerstoneAnnotationEvent); - cornerstone3d.eventTarget.addEventListener(cornerstoneTools.Enums.Events.ANNOTATION_REMOVED, handleCornerstoneAnnotationEvent); - cornerstone3d.eventTarget.addEventListener(cornerstoneTools.Enums.Events.ANNOTATION_COMPLETED, handleCornerstoneAnnotationEvent); - - return () => { - cornerstone3d.eventTarget.removeEventListener(cornerstone3d.Enums.Events.CAMERA_MODIFIED, handleCornerstoneViewportEvent); - cornerstone3d.eventTarget.removeEventListener(cornerstone3d.Enums.Events.VOI_MODIFIED, handleCornerstoneViewportEvent); - cornerstone3d.eventTarget.removeEventListener(cornerstone3d.Enums.Events.IMAGE_RENDERED, handleCornerstoneViewportEvent); - cornerstone3d.eventTarget.removeEventListener(cornerstone3d.Enums.Events.VOLUME_NEW_IMAGE, handleCornerstoneViewportEvent); - - cornerstone3d.eventTarget.removeEventListener(cornerstoneTools.Enums.Events.ANNOTATION_ADDED, handleCornerstoneAnnotationEvent); - cornerstone3d.eventTarget.removeEventListener(cornerstoneTools.Enums.Events.ANNOTATION_MODIFIED, handleCornerstoneAnnotationEvent); - cornerstone3d.eventTarget.removeEventListener(cornerstoneTools.Enums.Events.ANNOTATION_REMOVED, handleCornerstoneAnnotationEvent); - cornerstone3d.eventTarget.removeEventListener(cornerstoneTools.Enums.Events.ANNOTATION_COMPLETED, handleCornerstoneAnnotationEvent); - - if (stateThrottleTimeoutRef.current !== null) window.clearTimeout(stateThrottleTimeoutRef.current); - if (annotationsThrottleTimeoutRef.current !== null) window.clearTimeout(annotationsThrottleTimeoutRef.current); - }; - }, [throttleSendViewerState, throttleSendAnnotations]); const [annotationNavEnabled, setAnnotationNavEnabled] = useState(true); const [volumeDiagnosticsEnabled, setVolumeDiagnosticsEnabled] = useState(false); @@ -4464,6 +4443,64 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat cancelAnimationFrame(raf); }; }, [elementRef, updateOverlay, viewportGrid, viewportModes, viewportImageIds, crossHairsEnabled]); + + useEffect(() => { + const handleCornerstoneViewportEvent = (evt: any) => { + const state = wsStateRef.current; + appLogger.debug(`Viewport event received on element: ${evt?.type}, connected=${state.connected}, isController=${state.isController}, restoring=${restoringStateRef.current}`); + if (state.connected && state.isController && !restoringStateRef.current) { + throttleSendViewerState(); + } + }; + + const handleCornerstoneAnnotationEvent = () => { + const state = wsStateRef.current; + if (state.connected && state.isController && !restoringStateRef.current) { + throttleSendAnnotations(); + } + }; + + // Viewport events are triggered on individual viewport HTML elements (not on eventTarget globally) + const viewportEvents = [ + cornerstone3d.Enums.Events.CAMERA_MODIFIED, + cornerstone3d.Enums.Events.VOI_MODIFIED, + cornerstone3d.Enums.Events.IMAGE_RENDERED, + cornerstone3d.Enums.Events.VOLUME_NEW_IMAGE, + ]; + + const activeElements: HTMLDivElement[] = []; + viewportRefs.current.forEach((el) => { + if (el) { + viewportEvents.forEach(eventName => { + el.addEventListener(eventName, handleCornerstoneViewportEvent); + }); + activeElements.push(el); + } + }); + + // Annotation events trigger globally on the eventTarget + cornerstone3d.eventTarget.addEventListener(cornerstoneTools.Enums.Events.ANNOTATION_ADDED, handleCornerstoneAnnotationEvent); + cornerstone3d.eventTarget.addEventListener(cornerstoneTools.Enums.Events.ANNOTATION_MODIFIED, handleCornerstoneAnnotationEvent); + cornerstone3d.eventTarget.addEventListener(cornerstoneTools.Enums.Events.ANNOTATION_REMOVED, handleCornerstoneAnnotationEvent); + cornerstone3d.eventTarget.addEventListener(cornerstoneTools.Enums.Events.ANNOTATION_COMPLETED, handleCornerstoneAnnotationEvent); + + return () => { + activeElements.forEach((el) => { + viewportEvents.forEach(eventName => { + el.removeEventListener(eventName, handleCornerstoneViewportEvent); + }); + }); + + cornerstone3d.eventTarget.removeEventListener(cornerstoneTools.Enums.Events.ANNOTATION_ADDED, handleCornerstoneAnnotationEvent); + cornerstone3d.eventTarget.removeEventListener(cornerstoneTools.Enums.Events.ANNOTATION_MODIFIED, handleCornerstoneAnnotationEvent); + cornerstone3d.eventTarget.removeEventListener(cornerstoneTools.Enums.Events.ANNOTATION_REMOVED, handleCornerstoneAnnotationEvent); + cornerstone3d.eventTarget.removeEventListener(cornerstoneTools.Enums.Events.ANNOTATION_COMPLETED, handleCornerstoneAnnotationEvent); + + if (stateThrottleTimeoutRef.current !== null) window.clearTimeout(stateThrottleTimeoutRef.current); + if (annotationsThrottleTimeoutRef.current !== null) window.clearTimeout(annotationsThrottleTimeoutRef.current); + }; + }, [throttleSendViewerState, throttleSendAnnotations, viewportGrid, viewportModes, viewportImageIds]); + // Synchronize Crosshairs Projection Type useEffect(() => { if (!crossHairsEnabled) return; @@ -5644,22 +5681,6 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat appLogger.debug("Checking viewport", i); const viewport = renderingEngine.getViewport(`CT_${i}`); if (!viewport) { allReady = false; break; } - const stackIdx = state.stackIdx?.[i]; - const stackObj = state.stacks?.[stackIdx]; - const importedImageIds = (stackObj?.i || []).map(rewriteImageId); - if (!importedImageIds.length) continue; - try { - const viewportImageIds = viewport.getImageIds?.() || []; - if ( - viewportImageIds.length !== importedImageIds.length || - !viewportImageIds.every((id, idx) => id === importedImageIds[idx]) - ) { - allReady = false; - break; - } - } catch (e) { - allReady = false; - } } if (!allReady && attempts < maxAttempts) { attempts++; diff --git a/dicom-viewer/src/main.tsx b/dicom-viewer/src/main.tsx index 46c20c2..2059da0 100644 --- a/dicom-viewer/src/main.tsx +++ b/dicom-viewer/src/main.tsx @@ -49,7 +49,7 @@ type RootRecord = { signature: string; }; -const rootRegistry = new WeakMap(); +const rootRegistry = new Map(); const mainLogger = logger.child("main"); declare global { @@ -241,6 +241,19 @@ const mountContainer = (container: HTMLElement, imageStacks: () => Promise {