From 8a04f91c1c48217965f21e4d7e202cc28fc91c4c Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 19 May 2025 21:47:52 +0100 Subject: [PATCH] get tools working --- dicom-viewer/src/App.tsx | 426 ++++++++++++------ .../src/lib/createImageIdsAndCacheMetaData.ts | 2 +- 2 files changed, 282 insertions(+), 146 deletions(-) diff --git a/dicom-viewer/src/App.tsx b/dicom-viewer/src/App.tsx index 12b0072..63d1856 100644 --- a/dicom-viewer/src/App.tsx +++ b/dicom-viewer/src/App.tsx @@ -6,6 +6,7 @@ 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 { init as dicomImageLoaderInit } from "@cornerstonejs/dicom-image-loader" import * as cornerstoneTools from '@cornerstonejs/tools'; import { StackScrollOutOfBoundsEvent } from 'core/src/types/EventTypes'; @@ -16,13 +17,6 @@ const { MouseBindings } = csToolsEnums; const { IMAGE_RENDERED } = Enums.Events; const toolGroupId = 'STACK_TOOL_GROUP_ID'; -const leftClickTools = [ - WindowLevelTool.toolName, - PlanarRotateTool.toolName, - StackScrollTool.toolName, -]; -const defaultLeftClickTool = leftClickTools[0]; -let currentLeftClickTool = leftClickTools[0]; // Common CT presets (add more as needed) const WINDOW_LEVEL_PRESETS = [ @@ -33,6 +27,26 @@ const WINDOW_LEVEL_PRESETS = [ { name: "Abdomen", center: 60, width: 400 }, ] +// Tool options for dropdowns +const TOOL_OPTIONS = [ + { label: "Window/Level", value: WindowLevelTool.toolName }, + { label: "Pan", value: PanTool.toolName }, + { label: "Zoom", value: ZoomTool.toolName }, + { label: "Stack Scroll", value: StackScrollTool.toolName }, + { label: "Rotate", value: PlanarRotateTool.toolName }, + { label: "Length", value: LengthTool.toolName }, + { label: "Probe", value: ProbeTool.toolName }, + { label: "Arrow Annotate", value: ArrowAnnotateTool.toolName }, + { label: "Rectangle ROI", value: RectangleROITool.toolName }, + { label: "Ellipse ROI", value: EllipticalROITool.toolName }, +] + +// Mouse button labels +const MOUSE_BUTTONS = [ + { label: "Left", value: "Primary" }, + { label: "Middle", value: "Auxiliary" }, + { label: "Right", value: "Secondary" }, +] function App() { @@ -40,6 +54,11 @@ function App() { const viewportRef = useRef(null) // Store the viewport instance const running = useRef(false) + // State to control preset menu open/close + const [presetsOpen, setPresetsOpen] = useState(false); + + const [settingsOpen, setSettingsOpen] = useState(false); + // State for overlay info const [imageInfo, setImageInfo] = useState({ index: 0, @@ -48,40 +67,69 @@ function App() { windowWidth: "", }) -const updateOverlay = useCallback(() => { - console.log("updateOverlay") - const viewport = viewportRef.current - if (!viewport) return - console.log(viewport) + // State for selected tool per mouse button + const [mouseToolBindings, setMouseToolBindings] = useState({ + Primary: WindowLevelTool.toolName, + Auxiliary: PanTool.toolName, + Secondary: ZoomTool.toolName, + }) - // For StackViewport in Cornerstone3D v3: - const currentIndex = typeof viewport.getCurrentImageIdIndex === "function" - ? viewport.getCurrentImageIdIndex() - : 0 - const imageIds = typeof viewport.getImageIds === "function" - ? viewport.getImageIds() - : [] - const total = imageIds.length - - // Use getVOI() for current window center/width - let windowCenter = "" - let windowWidth = "" - if (typeof viewport.getProperties === "function") { - const props = viewport.getProperties() - if (props.voiRange) { - const { lower, upper } = props.voiRange - windowCenter = ((upper + lower) / 2).toFixed(0) - windowWidth = (upper - lower).toFixed(0) + // Helper to apply all mouse tool bindings + const applyMouseToolBindings = useCallback(() => { + console.log("applyMouseToolBindings", mouseToolBindings) + const toolGroup = cornerstoneTools.ToolGroupManager.getToolGroup(toolGroupId) + if (toolGroup) { + // Set all tools to passive first + TOOL_OPTIONS.forEach(opt => { + toolGroup.setToolPassive(opt.value, { removeAllBindings: true }) + }) + // Re-apply all bindings for all mouse buttons (Primary, Auxiliary, Secondary) + Object.entries(mouseToolBindings).forEach(([btn, tool]) => { + console.log("button", btn, "setToolActive", tool) + toolGroup.setToolActive(tool, { + bindings: [{ mouseButton: csToolsEnums.MouseBindings[btn] }], + }) + }) + // Do NOT set wheel bindings here! } + }, [mouseToolBindings]) + + const handleToolChange = (mouseButton: string, toolName: string) => { + setMouseToolBindings(prev => ({ ...prev, [mouseButton]: toolName })) } - setImageInfo({ - index: currentIndex + 1, - total, - windowCenter, - windowWidth, - }) -}, []) + const updateOverlay = useCallback(() => { + const viewport = viewportRef.current + if (!viewport) return + + // For StackViewport in Cornerstone3D v3: + const currentIndex = typeof viewport.getCurrentImageIdIndex === "function" + ? viewport.getCurrentImageIdIndex() + : 0 + const imageIds = typeof viewport.getImageIds === "function" + ? viewport.getImageIds() + : [] + const total = imageIds.length + + // Use getVOI() for current window center/width + let windowCenter = "" + let windowWidth = "" + if (typeof viewport.getProperties === "function") { + const props = viewport.getProperties() + if (props.voiRange) { + const { lower, upper } = props.voiRange + windowCenter = ((upper + lower) / 2).toFixed(0) + windowWidth = (upper - lower).toFixed(0) + } + } + + setImageInfo({ + index: currentIndex + 1, + total, + windowCenter, + windowWidth, + }) + }, []) useEffect(() => { const setup = async () => { if (running.current) { @@ -94,46 +142,32 @@ const updateOverlay = useCallback(() => { dicomImageLoaderInit({ maxWebWorkers: 1 }) // Add tools to Cornerstone3D - cornerstoneTools.addTool(PanTool); - cornerstoneTools.addTool(WindowLevelTool); - cornerstoneTools.addTool(StackScrollTool); - cornerstoneTools.addTool(ZoomTool); - cornerstoneTools.addTool(PlanarRotateTool); + cornerstoneTools.addTool(PanTool) + cornerstoneTools.addTool(WindowLevelTool) + cornerstoneTools.addTool(StackScrollTool) + cornerstoneTools.addTool(ZoomTool) + cornerstoneTools.addTool(PlanarRotateTool) + cornerstoneTools.addTool(LengthTool) + cornerstoneTools.addTool(ProbeTool) + cornerstoneTools.addTool(ArrowAnnotateTool) + cornerstoneTools.addTool(RectangleROITool) + cornerstoneTools.addTool(EllipticalROITool) - // Define a tool group, which defines how mouse events map to tool commands for - // Any viewport using the group - const toolGroup = ToolGroupManager.createToolGroup(toolGroupId); - - // Add tools to the tool group - toolGroup.addTool(WindowLevelTool.toolName); - toolGroup.addTool(PanTool.toolName); - toolGroup.addTool(ZoomTool.toolName); - toolGroup.addTool(StackScrollTool.toolName, { loop: false }); - toolGroup.addTool(PlanarRotateTool.toolName); - - // Set the initial state of the tools, here all tools are active and bound to - // Different mouse inputs - toolGroup.setToolActive(defaultLeftClickTool, { - bindings: [ - { - mouseButton: MouseBindings.Primary, // Left Click - }, - ], - }); - toolGroup.setToolActive(PanTool.toolName, { - bindings: [ - { - mouseButton: MouseBindings.Auxiliary, // Middle Click - }, - ], - }); - toolGroup.setToolActive(ZoomTool.toolName, { - bindings: [ - { - mouseButton: MouseBindings.Secondary, // Right Click - }, - ], - }); + // Create tool group ONCE + let toolGroup = ToolGroupManager.getToolGroup(toolGroupId) + if (!toolGroup) { + toolGroup = ToolGroupManager.createToolGroup(toolGroupId) + toolGroup.addTool(WindowLevelTool.toolName) + toolGroup.addTool(PanTool.toolName) + toolGroup.addTool(ZoomTool.toolName) + 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) + } // The Stack Scroll mouse wheel is a tool using the `mouseWheelCallback` // and needs to be registered against the 'Wheel' binding. @@ -144,22 +178,9 @@ const updateOverlay = useCallback(() => { }, ], }); - toolGroup.setToolActive(PlanarRotateTool.toolName, { - bindings: [ - { - mouseButton: MouseBindings.Wheel, // Shift Wheel Mouse - //modifierKey: KeyboardBindings.Shift, - }, - { - mouseButton: MouseBindings.Wheel_Primary, // Left Click+Wheel Mouse - }, - ], - }); - const imageIds = await createImageIdsAndCacheMetaData() - console.log("imageIds", imageIds) // Instantiate a rendering engine const renderingEngineId = "myRenderingEngine" @@ -187,10 +208,6 @@ const updateOverlay = useCallback(() => { cornerstoneTools.utilities.stackPrefetch.enable(viewport.element); - function test(e) { - console.log("test", e) - } - // Render the image viewport.render() viewport.element.addEventListener(Enums.Events.IMAGE_RENDERED, updateOverlay) @@ -200,6 +217,9 @@ const updateOverlay = useCallback(() => { // Initial overlay update updateOverlay() + + // Apply mouse tool bindings after setup + applyMouseToolBindings() } setup() @@ -235,10 +255,25 @@ const updateOverlay = useCallback(() => { viewport.render() } }, []) + useEffect(() => { + applyMouseToolBindings(); + }, [mouseToolBindings, applyMouseToolBindings]); + + // Close the menu when clicking outside + useEffect(() => { + if (!presetsOpen) return; + const handleClick = (e: MouseEvent) => { + const menu = document.querySelector(".preset-menu"); + if (menu && !menu.contains(e.target as Node)) { + setPresetsOpen(false); + } + }; + document.addEventListener("mousedown", handleClick); + return () => document.removeEventListener("mousedown", handleClick); + }, [presetsOpen]); return (
- {/* ...existing Reset View button and viewport div... */} - ))} + Presets + + {presetsOpen ? "▲" : "▼"} + +
+ {presetsOpen && ( +
+ {WINDOW_LEVEL_PRESETS.map((preset) => ( + + ))} +
+ )} + + + {/* Settings button at top right */} + + + {/* Settings Modal (viewport-local, not fixed to page) */} + {settingsOpen && ( +
setSettingsOpen(false)} + > +
e.stopPropagation()} + > +
+ Settings +
+ {/* Settings content: Tool dropdowns */} +
+
Mouse Button Tool Bindings
+ {MOUSE_BUTTONS.map((btn) => ( +
+ + +
+ ))} +
+
- {/* Inline CSS for hover effect */} - - + )} + {/* ...rest of your UI... */} ) } diff --git a/dicom-viewer/src/lib/createImageIdsAndCacheMetaData.ts b/dicom-viewer/src/lib/createImageIdsAndCacheMetaData.ts index 1d58d6e..71ea35c 100644 --- a/dicom-viewer/src/lib/createImageIdsAndCacheMetaData.ts +++ b/dicom-viewer/src/lib/createImageIdsAndCacheMetaData.ts @@ -62,7 +62,7 @@ import cornerstoneDICOMImageLoader from "@cornerstonejs/dicom-image-loader" return imageIds } */ -const NUM_IMAGES = 10 // set to however many images you have +const NUM_IMAGES = 20 // set to however many images you have export default async function createImageIdsAndCacheMetaData() { const imageIds = []