From eb5219b35563197d051a5b20f576d8f6a2c8ae37 Mon Sep 17 00:00:00 2001 From: Ross Date: Tue, 20 May 2025 07:57:28 +0100 Subject: [PATCH] start grid setup --- dicom-viewer/src/App.tsx | 267 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 248 insertions(+), 19 deletions(-) diff --git a/dicom-viewer/src/App.tsx b/dicom-viewer/src/App.tsx index 63d1856..f38e34d 100644 --- a/dicom-viewer/src/App.tsx +++ b/dicom-viewer/src/App.tsx @@ -6,13 +6,14 @@ import { } from "@cornerstonejs/core" import { init as csRenderInit } from "@cornerstonejs/core" import { init as csToolsInit, addTool, ToolGroupManager, Enums as csToolsEnums, PanTool, WindowLevelTool, StackScrollTool, ZoomTool, PlanarRotateTool } from "@cornerstonejs/tools" -import { LengthTool, ProbeTool, ArrowAnnotateTool, RectangleROITool, EllipticalROITool } from "@cornerstonejs/tools" +import { LengthTool, ProbeTool, ArrowAnnotateTool, RectangleROITool, EllipticalROITool, PlanarFreehandROITool, PlanarFreehandContourSegmentationTool, SculptorTool } from "@cornerstonejs/tools" import { init as dicomImageLoaderInit } from "@cornerstonejs/dicom-image-loader" import * as cornerstoneTools from '@cornerstonejs/tools'; import { StackScrollOutOfBoundsEvent } from 'core/src/types/EventTypes'; +import { segmentation, Enums as csEnums } from '@cornerstonejs/tools'; -const { MouseBindings } = csToolsEnums; +const { MouseBindings, KeyboardBindings } = csToolsEnums; const { IMAGE_RENDERED } = Enums.Events; @@ -39,6 +40,9 @@ const TOOL_OPTIONS = [ { label: "Arrow Annotate", value: ArrowAnnotateTool.toolName }, { label: "Rectangle ROI", value: RectangleROITool.toolName }, { label: "Ellipse ROI", value: EllipticalROITool.toolName }, + { label: "Freehand ROI", value: PlanarFreehandROITool.toolName }, + { label: "Freehand Contour Segmentation", value: PlanarFreehandContourSegmentationTool.toolName }, + { label: "Sculptor", value: SculptorTool.toolName }, ] // Mouse button labels @@ -67,32 +71,90 @@ function App() { windowWidth: "", }) + + const [viewportGrid, setViewportGrid] = useState({ rows: 1, cols: 1 }) + const [viewportMenuOpen, setViewportMenuOpen] = useState(false) + + // Store refs for each viewport + const viewportRefs = useRef>([]) + + // Helper to generate grid options + const gridOptions = [] + for (let rows = 1; rows <= 3; rows++) { + for (let cols = 1; cols <= 3; cols++) { + gridOptions.push({ rows, cols }) + } + } + // State for selected tool per mouse button const [mouseToolBindings, setMouseToolBindings] = useState({ Primary: WindowLevelTool.toolName, Auxiliary: PanTool.toolName, Secondary: ZoomTool.toolName, }) + const [ctrlMouseToolBindings, setCtrlMouseToolBindings] = useState({ + Primary: LengthTool.toolName, + Auxiliary: ProbeTool.toolName, + Secondary: ArrowAnnotateTool.toolName, + }) - // Helper to apply all mouse tool bindings + // Track if Ctrl is pressed + const [ctrlPressed, setCtrlPressed] = useState(false) + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.ctrlKey) setCtrlPressed(true) + } + const handleKeyUp = (e: KeyboardEvent) => { + if (!e.ctrlKey) setCtrlPressed(false) + } + window.addEventListener("keydown", handleKeyDown) + window.addEventListener("keyup", handleKeyUp) + return () => { + window.removeEventListener("keydown", handleKeyDown) + window.removeEventListener("keyup", handleKeyUp) + } + }, []) + + // Helper to apply all mouse tool bindings (normal or ctrl) const applyMouseToolBindings = useCallback(() => { - console.log("applyMouseToolBindings", mouseToolBindings) const toolGroup = cornerstoneTools.ToolGroupManager.getToolGroup(toolGroupId) if (toolGroup) { - // Set all tools to passive first + // Set all tools to passive first, removing all bindings TOOL_OPTIONS.forEach(opt => { toolGroup.setToolPassive(opt.value, { removeAllBindings: true }) }) - // Re-apply all bindings for all mouse buttons (Primary, Auxiliary, Secondary) + + // Collect all bindings for each tool + const toolBindings: Record> = {} + + // Normal bindings Object.entries(mouseToolBindings).forEach(([btn, tool]) => { - console.log("button", btn, "setToolActive", tool) - toolGroup.setToolActive(tool, { - bindings: [{ mouseButton: csToolsEnums.MouseBindings[btn] }], - }) + if (!toolBindings[tool]) toolBindings[tool] = [] + toolBindings[tool].push({ mouseButton: MouseBindings[btn] }) + }) + // Ctrl bindings + Object.entries(ctrlMouseToolBindings).forEach(([btn, tool]) => { + if (!toolBindings[tool]) toolBindings[tool] = [] + toolBindings[tool].push({ mouseButton: MouseBindings[btn], modifierKey: KeyboardBindings.Ctrl }) + }) + + // Apply all bindings for each tool in a single call + Object.entries(toolBindings).forEach(([tool, bindings]) => { + toolGroup.setToolActive(tool, { bindings }) + }) + + // Always re-apply the mouse wheel binding for StackScrollTool + toolGroup.setToolActive(StackScrollTool.toolName, { + bindings: [ + { + mouseButton: MouseBindings.Wheel, // Wheel Mouse + }, + ], }) - // Do NOT set wheel bindings here! } - }, [mouseToolBindings]) + }, [mouseToolBindings, ctrlMouseToolBindings]) + + const handleToolChange = (mouseButton: string, toolName: string) => { setMouseToolBindings(prev => ({ ...prev, [mouseButton]: toolName })) @@ -152,6 +214,9 @@ function App() { cornerstoneTools.addTool(ArrowAnnotateTool) cornerstoneTools.addTool(RectangleROITool) cornerstoneTools.addTool(EllipticalROITool) + cornerstoneTools.addTool(PlanarFreehandROITool) + cornerstoneTools.addTool(PlanarFreehandContourSegmentationTool) + cornerstoneTools.addTool(SculptorTool) // Create tool group ONCE let toolGroup = ToolGroupManager.getToolGroup(toolGroupId) @@ -163,10 +228,26 @@ function App() { toolGroup.addTool(StackScrollTool.toolName, { loop: false }) toolGroup.addTool(PlanarRotateTool.toolName) toolGroup.addTool(LengthTool.toolName) - toolGroup.addTool(ProbeTool.toolName) - toolGroup.addTool(ArrowAnnotateTool.toolName) - toolGroup.addTool(RectangleROITool.toolName) - toolGroup.addTool(EllipticalROITool.toolName) + toolGroup.addTool(ArrowAnnotateTool.toolName, { + getTextCallback: () => '', + }) + toolGroup.addTool(ProbeTool.toolName, { + statsCalculator: () => null, + }) + toolGroup.addTool(RectangleROITool.toolName, { + calculateStats: false, + getTextCallback: () => '', + }) + toolGroup.addTool(EllipticalROITool.toolName, { + getTextCallback: () => '', + }) + toolGroup.addTool(PlanarFreehandROITool.toolName, { + calculateStats: false, + }) + toolGroup.addTool(PlanarFreehandContourSegmentationTool.toolName, { + }) + toolGroup.addTool(SculptorTool.toolName, { + }) } // The Stack Scroll mouse wheel is a tool using the `mouseWheelCallback` @@ -206,6 +287,25 @@ function App() { // Create a stack volume viewport.setStack(imageIds) + const segmentationId = 'my-segmentation-id'; + + // Create a segmentation representation for the stack + segmentation.addSegmentations([{ + segmentationId, + representation: { + type: csEnums.SegmentationRepresentations.Contour + } + }]); + //segmentation.activeSegmentation.setActiveSegmentationRepresentations(segmentationId); + // Create a segmentation representation associated to the viewportId + await segmentation.addSegmentationRepresentations(viewportId, [ + { + segmentationId, + type: csToolsEnums.SegmentationRepresentations.Contour, + }, + ]); + + segmentation.segmentIndex.setActiveSegmentIndex(segmentationId, 1); cornerstoneTools.utilities.stackPrefetch.enable(viewport.element); // Render the image @@ -255,9 +355,11 @@ function App() { viewport.render() } }, []) + + // Re-apply bindings when tool sets or ctrl changes useEffect(() => { - applyMouseToolBindings(); - }, [mouseToolBindings, applyMouseToolBindings]); + applyMouseToolBindings() + }, [mouseToolBindings, ctrlMouseToolBindings, ctrlPressed, applyMouseToolBindings]) // Close the menu when clicking outside useEffect(() => { @@ -272,6 +374,28 @@ function App() { return () => document.removeEventListener("mousedown", handleClick); }, [presetsOpen]); + + // Render the grid of viewports + const viewports = [] + for (let i = 0; i < viewportGrid.rows * viewportGrid.cols; i++) { + viewports.push( +
(viewportRefs.current[i] = el)} + style={{ + flex: 1, + border: "1px solid #222", + background: "#000", + minWidth: 0, + minHeight: 0, + position: "relative", + }} + > + {/* You can render overlays or controls per-viewport here */} +
+ ) + } + return (
+ {/* Viewport grid menu at top center */} +
+ + {viewportMenuOpen && ( +
+ {gridOptions.map(opt => ( + + ))} +
+ )} +
+ + {/* Viewport grid */} +
+ {viewports} +
{/* Overlay at bottom left */}
{btn.label} Button: +
+ ))} +
Ctrl + Mouse Button Tool Bindings
+ {MOUSE_BUTTONS.map((btn) => ( +
+ +