From b961a806d97985eff3dc3881653e1531d20ee450 Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 8 Jun 2026 12:51:08 +0100 Subject: [PATCH] start implmeentting proper crosshairs --- dicom-viewer/src/App.tsx | 199 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 197 insertions(+), 2 deletions(-) diff --git a/dicom-viewer/src/App.tsx b/dicom-viewer/src/App.tsx index 8d2394f..3eb26a2 100644 --- a/dicom-viewer/src/App.tsx +++ b/dicom-viewer/src/App.tsx @@ -1153,6 +1153,10 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat }; const [crossHairsEnabled, setCrossHairsEnabled] = useState(false); + const [crosshairsProjectionType, setCrosshairsProjectionType] = useState("None"); + const [syncSlabThickness, setSyncSlabThickness] = useState(true); + const [showCenterPoint, setShowCenterPoint] = useState(true); + const slabThicknessSynchronizerRef = useRef(null); const [referenceLinesEnabled, setReferenceLinesEnabled] = useState(true); const [showAdvancedMouseBindings, setShowAdvancedMouseBindings] = useState(false); @@ -3884,8 +3888,16 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat mainToolGroup.addTool(CrosshairsTool.toolName); } mainToolGroup.setToolEnabled(CrosshairsTool.toolName); + const blendModeMap: Record = { + "None": Enums.BlendModes.COMPOSITE, + "MIP": Enums.BlendModes.MAXIMUM_INTENSITY_BLEND, + "MinIP": Enums.BlendModes.MINIMUM_INTENSITY_BLEND, + "Average": Enums.BlendModes.AVERAGE_INTENSITY_BLEND, + }; + const initialBlendMode = blendModeMap[crosshairsProjectionType] ?? Enums.BlendModes.COMPOSITE; mainToolGroup.setToolConfiguration(CrosshairsTool.toolName, { - // Example: color: '#ffeb3b', lineWidth: 2 + referenceLinesCenterGapRadius: showCenterPoint ? 0 : 20, + slabThicknessBlendMode: initialBlendMode, }); mainToolGroup.setToolActive(CrosshairsTool.toolName, { bindings: [{ mouseButton: MouseBindings.Primary }], @@ -3918,7 +3930,114 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat }); cancelAnimationFrame(raf); }; - }, [elementRef, updateOverlay, viewportGrid, viewportModes, viewportImageIds]); + }, [elementRef, updateOverlay, viewportGrid, viewportModes, viewportImageIds, crossHairsEnabled]); + // Synchronize Crosshairs Projection Type + useEffect(() => { + if (!crossHairsEnabled) return; + const renderingEngine = renderingEngineRef.current; + if (!renderingEngine) return; + + const blendModeMap: Record = { + "None": Enums.BlendModes.COMPOSITE, + "MIP": Enums.BlendModes.MAXIMUM_INTENSITY_BLEND, + "MinIP": Enums.BlendModes.MINIMUM_INTENSITY_BLEND, + "Average": Enums.BlendModes.AVERAGE_INTENSITY_BLEND, + }; + const targetBlendMode = blendModeMap[crosshairsProjectionType] ?? Enums.BlendModes.COMPOSITE; + + // Update all active volume viewports + const visibleCount = viewportGrid.rows * viewportGrid.cols; + for (let i = 0; i < visibleCount; i++) { + if (viewportModes[i] === 'volume') { + const viewportId = `CT_${i}`; + const viewport = renderingEngine.getViewport(viewportId) as any; + if (viewport && typeof viewport.setBlendMode === "function") { + try { + viewport.setBlendMode(targetBlendMode); + viewport.render(); + } catch (e) { + appLogger.warn("Failed to set blend mode for viewport", viewportId, e); + } + } + } + } + + // Also update CrosshairsTool configuration + const mainToolGroup = ToolGroupManager.getToolGroup(MAIN_TOOL_GROUP_ID); + if (mainToolGroup && mainToolGroup.hasTool(CrosshairsTool.toolName)) { + mainToolGroup.setToolConfiguration(CrosshairsTool.toolName, { + slabThicknessBlendMode: targetBlendMode, + }); + renderingEngine.render(); + } + }, [crosshairsProjectionType, crossHairsEnabled, viewportGrid, viewportModes]); + + // Synchronize Crosshairs Center Point (Gap Radius) + useEffect(() => { + if (!crossHairsEnabled) return; + const mainToolGroup = ToolGroupManager.getToolGroup(MAIN_TOOL_GROUP_ID); + if (mainToolGroup && mainToolGroup.hasTool(CrosshairsTool.toolName)) { + mainToolGroup.setToolConfiguration(CrosshairsTool.toolName, { + referenceLinesCenterGapRadius: showCenterPoint ? 0 : 20, + }); + const renderingEngine = renderingEngineRef.current; + if (renderingEngine) { + renderingEngine.render(); + } + } + }, [showCenterPoint, crossHairsEnabled]); + + // Manage Slab Thickness Synchronizer + useEffect(() => { + // Clean up previous synchronizer + if (slabThicknessSynchronizerRef.current) { + try { + slabThicknessSynchronizerRef.current.destroy(); + } catch (e) { + appLogger.warn("Failed to destroy slab thickness synchronizer", e); + } + slabThicknessSynchronizerRef.current = null; + } + + if (!crossHairsEnabled || !syncSlabThickness) return; + + const renderingEngine = renderingEngineRef.current; + if (!renderingEngine) return; + + const activeVolumeViewportIds = []; + const visibleCount = viewportGrid.rows * viewportGrid.cols; + for (let i = 0; i < visibleCount; i++) { + if (viewportModes[i] === 'volume') { + activeVolumeViewportIds.push(`CT_${i}`); + } + } + + if (activeVolumeViewportIds.length >= 2) { + try { + const { createSlabThicknessSynchronizer } = cornerstoneTools.synchronizers; + const slabSync = createSlabThicknessSynchronizer("slabThicknessSync"); + slabThicknessSynchronizerRef.current = slabSync; + activeVolumeViewportIds.forEach((viewportId) => { + slabSync.add({ + renderingEngineId, + viewportId, + }); + }); + } catch (e) { + appLogger.error("Failed to create and add to slab thickness synchronizer", e); + } + } + + return () => { + if (slabThicknessSynchronizerRef.current) { + try { + slabThicknessSynchronizerRef.current.destroy(); + } catch (e) { /* ignore */ } + slabThicknessSynchronizerRef.current = null; + } + }; + }, [syncSlabThickness, crossHairsEnabled, viewportGrid, viewportModes]); + // Handler to apply a preset for a specific viewport const handleApplyPreset = useCallback((viewportIdx: number, center: number, width: number) => { const viewportId = `CT_${viewportIdx}`; @@ -7174,6 +7293,82 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat > {crossHairsEnabled ? "Disable" : "Enable"} Cross Reference Lines + {crossHairsEnabled && ( +
+
+ Crosshairs Options +
+ + {/* Projection Type Selector */} +
+ + +
+ + {/* Sync Slab Thickness */} +
+ setSyncSlabThickness(e.target.checked)} + style={{ cursor: "pointer", width: "16px", height: "16px" }} + /> + +
+ + {/* Show Center Point */} +
+ setShowCenterPoint(e.target.checked)} + style={{ cursor: "pointer", width: "16px", height: "16px" }} + /> + +
+
+ )}