get tools working

This commit is contained in:
Ross
2025-05-19 21:47:52 +01:00
parent 63990c49ee
commit 8a04f91c1c
2 changed files with 282 additions and 146 deletions
+228 -92
View File
@@ -6,6 +6,7 @@ import {
} from "@cornerstonejs/core" } from "@cornerstonejs/core"
import { init as csRenderInit } 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 { 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 { init as dicomImageLoaderInit } from "@cornerstonejs/dicom-image-loader"
import * as cornerstoneTools from '@cornerstonejs/tools'; import * as cornerstoneTools from '@cornerstonejs/tools';
import { StackScrollOutOfBoundsEvent } from 'core/src/types/EventTypes'; import { StackScrollOutOfBoundsEvent } from 'core/src/types/EventTypes';
@@ -16,13 +17,6 @@ const { MouseBindings } = csToolsEnums;
const { IMAGE_RENDERED } = Enums.Events; const { IMAGE_RENDERED } = Enums.Events;
const toolGroupId = 'STACK_TOOL_GROUP_ID'; 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) // Common CT presets (add more as needed)
const WINDOW_LEVEL_PRESETS = [ const WINDOW_LEVEL_PRESETS = [
@@ -33,6 +27,26 @@ const WINDOW_LEVEL_PRESETS = [
{ name: "Abdomen", center: 60, width: 400 }, { 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() { function App() {
@@ -40,6 +54,11 @@ function App() {
const viewportRef = useRef<any>(null) // Store the viewport instance const viewportRef = useRef<any>(null) // Store the viewport instance
const running = useRef(false) 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 // State for overlay info
const [imageInfo, setImageInfo] = useState({ const [imageInfo, setImageInfo] = useState({
index: 0, index: 0,
@@ -48,11 +67,40 @@ function App() {
windowWidth: "", windowWidth: "",
}) })
// State for selected tool per mouse button
const [mouseToolBindings, setMouseToolBindings] = useState({
Primary: WindowLevelTool.toolName,
Auxiliary: PanTool.toolName,
Secondary: ZoomTool.toolName,
})
// 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 }))
}
const updateOverlay = useCallback(() => { const updateOverlay = useCallback(() => {
console.log("updateOverlay")
const viewport = viewportRef.current const viewport = viewportRef.current
if (!viewport) return if (!viewport) return
console.log(viewport)
// For StackViewport in Cornerstone3D v3: // For StackViewport in Cornerstone3D v3:
const currentIndex = typeof viewport.getCurrentImageIdIndex === "function" const currentIndex = typeof viewport.getCurrentImageIdIndex === "function"
@@ -94,46 +142,32 @@ const updateOverlay = useCallback(() => {
dicomImageLoaderInit({ maxWebWorkers: 1 }) dicomImageLoaderInit({ maxWebWorkers: 1 })
// Add tools to Cornerstone3D // Add tools to Cornerstone3D
cornerstoneTools.addTool(PanTool); cornerstoneTools.addTool(PanTool)
cornerstoneTools.addTool(WindowLevelTool); cornerstoneTools.addTool(WindowLevelTool)
cornerstoneTools.addTool(StackScrollTool); cornerstoneTools.addTool(StackScrollTool)
cornerstoneTools.addTool(ZoomTool); cornerstoneTools.addTool(ZoomTool)
cornerstoneTools.addTool(PlanarRotateTool); 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 // Create tool group ONCE
// Any viewport using the group let toolGroup = ToolGroupManager.getToolGroup(toolGroupId)
const toolGroup = ToolGroupManager.createToolGroup(toolGroupId); if (!toolGroup) {
toolGroup = ToolGroupManager.createToolGroup(toolGroupId)
// Add tools to the tool group toolGroup.addTool(WindowLevelTool.toolName)
toolGroup.addTool(WindowLevelTool.toolName); toolGroup.addTool(PanTool.toolName)
toolGroup.addTool(PanTool.toolName); toolGroup.addTool(ZoomTool.toolName)
toolGroup.addTool(ZoomTool.toolName); toolGroup.addTool(StackScrollTool.toolName, { loop: false })
toolGroup.addTool(StackScrollTool.toolName, { loop: false }); toolGroup.addTool(PlanarRotateTool.toolName)
toolGroup.addTool(PlanarRotateTool.toolName); toolGroup.addTool(LengthTool.toolName)
toolGroup.addTool(ProbeTool.toolName)
// Set the initial state of the tools, here all tools are active and bound to toolGroup.addTool(ArrowAnnotateTool.toolName)
// Different mouse inputs toolGroup.addTool(RectangleROITool.toolName)
toolGroup.setToolActive(defaultLeftClickTool, { toolGroup.addTool(EllipticalROITool.toolName)
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
},
],
});
// The Stack Scroll mouse wheel is a tool using the `mouseWheelCallback` // The Stack Scroll mouse wheel is a tool using the `mouseWheelCallback`
// and needs to be registered against the 'Wheel' binding. // 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() const imageIds = await createImageIdsAndCacheMetaData()
console.log("imageIds", imageIds)
// Instantiate a rendering engine // Instantiate a rendering engine
const renderingEngineId = "myRenderingEngine" const renderingEngineId = "myRenderingEngine"
@@ -187,10 +208,6 @@ const updateOverlay = useCallback(() => {
cornerstoneTools.utilities.stackPrefetch.enable(viewport.element); cornerstoneTools.utilities.stackPrefetch.enable(viewport.element);
function test(e) {
console.log("test", e)
}
// Render the image // Render the image
viewport.render() viewport.render()
viewport.element.addEventListener(Enums.Events.IMAGE_RENDERED, updateOverlay) viewport.element.addEventListener(Enums.Events.IMAGE_RENDERED, updateOverlay)
@@ -200,6 +217,9 @@ const updateOverlay = useCallback(() => {
// Initial overlay update // Initial overlay update
updateOverlay() updateOverlay()
// Apply mouse tool bindings after setup
applyMouseToolBindings()
} }
setup() setup()
@@ -235,10 +255,25 @@ const updateOverlay = useCallback(() => {
viewport.render() 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 ( return (
<div style={{ position: "relative", width: "512px", height: "512px" }}> <div style={{ position: "relative", width: "512px", height: "512px" }}>
{/* ...existing Reset View button and viewport div... */}
<button <button
style={{ style={{
position: "absolute", position: "absolute",
@@ -252,6 +287,7 @@ const updateOverlay = useCallback(() => {
borderRadius: "4px", borderRadius: "4px",
cursor: "pointer", cursor: "pointer",
opacity: 0.85, opacity: 0.85,
userSelect: "none", // <-- Prevents text selection
}} }}
onClick={handleResetView} onClick={handleResetView}
> >
@@ -260,6 +296,7 @@ const updateOverlay = useCallback(() => {
<div <div
ref={elementRef} ref={elementRef}
onDoubleClick={handleDoubleClick} onDoubleClick={handleDoubleClick}
onContextMenu={e => e.preventDefault()} // <-- Add this line
style={{ style={{
width: "100%", width: "100%",
height: "100%", height: "100%",
@@ -280,6 +317,7 @@ const updateOverlay = useCallback(() => {
zIndex: 10, zIndex: 10,
pointerEvents: "none", pointerEvents: "none",
minWidth: "120px", minWidth: "120px",
userSelect: "none", // <-- Prevents text selection
}} }}
> >
<div> <div>
@@ -289,7 +327,7 @@ const updateOverlay = useCallback(() => {
WC: {imageInfo.windowCenter} &nbsp; WW: {imageInfo.windowWidth} WC: {imageInfo.windowCenter} &nbsp; WW: {imageInfo.windowWidth}
</div> </div>
</div> </div>
{/* Preset menu at bottom right, collapsed by default, expands on hover */} {/* Preset menu at bottom right, expands on click */}
<div <div
style={{ style={{
position: "absolute", position: "absolute",
@@ -299,6 +337,7 @@ const updateOverlay = useCallback(() => {
}} }}
> >
<div <div
className="preset-menu"
style={{ style={{
background: "rgba(0,0,0,0.7)", background: "rgba(0,0,0,0.7)",
color: "#fff", color: "#fff",
@@ -306,30 +345,35 @@ const updateOverlay = useCallback(() => {
fontSize: "14px", fontSize: "14px",
minWidth: "120px", minWidth: "120px",
overflow: "hidden", overflow: "hidden",
transition: "height 0.2s",
height: "28px",
cursor: "pointer", cursor: "pointer",
position: "relative", position: "relative",
boxShadow: presetsOpen ? "0 2px 8px rgba(0,0,0,0.3)" : undefined,
}} }}
className="preset-menu"
> >
<div style={{ padding: "6px 12px", fontWeight: "bold" }}>
Presets
</div>
<div <div
style={{ style={{
position: "absolute", padding: "6px 12px",
left: 0, fontWeight: "bold",
right: 0, userSelect: "none",
bottom: 0, }}
onClick={() => setPresetsOpen((open) => !open)}
>
Presets
<span style={{ float: "right", fontWeight: "normal" }}>
{presetsOpen ? "▲" : "▼"}
</span>
</div>
{presetsOpen && (
<div
className="preset-menu-content"
style={{
background: "rgba(0,0,0,0.95)", background: "rgba(0,0,0,0.95)",
borderRadius: "4px", borderRadius: "0 0 4px 4px",
padding: "6px 0 6px 0", padding: "6px 0 6px 0",
display: "none", display: "flex",
flexDirection: "column", flexDirection: "column",
zIndex: 11, zIndex: 11,
}} }}
className="preset-menu-content"
> >
{WINDOW_LEVEL_PRESETS.map((preset) => ( {WINDOW_LEVEL_PRESETS.map((preset) => (
<button <button
@@ -347,25 +391,117 @@ const updateOverlay = useCallback(() => {
fontSize: "13px", fontSize: "13px",
opacity: 0.9, opacity: 0.9,
}} }}
onClick={() => handleApplyPreset(preset.center, preset.width)} onClick={() => {
handleApplyPreset(preset.center, preset.width);
setPresetsOpen(false);
}}
> >
{preset.name} {preset.name}
</button> </button>
))} ))}
</div> </div>
)}
</div> </div>
{/* Inline CSS for hover effect */}
<style>
{`
.preset-menu:hover .preset-menu-content {
display: flex;
}
.preset-menu:hover {
height: auto !important;
}
`}
</style>
</div> </div>
{/* Settings button at top right */}
<button
style={{
position: "absolute",
top: 8,
right: 8,
zIndex: 20,
padding: "6px 12px",
background: "#222",
color: "#fff",
border: "none",
borderRadius: "4px",
cursor: "pointer",
opacity: 0.85,
userSelect: "none",
}}
onClick={() => setSettingsOpen(true)}
>
Settings
</button>
{/* Settings Modal (viewport-local, not fixed to page) */}
{settingsOpen && (
<div
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: "100%",
background: "rgba(0,0,0,0.4)",
zIndex: 1000,
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
onClick={() => setSettingsOpen(false)}
>
<div
style={{
background: "#222",
color: "#fff",
borderRadius: "8px",
minWidth: "320px",
minHeight: "180px",
padding: "24px",
boxShadow: "0 4px 24px rgba(0,0,0,0.4)",
position: "relative",
}}
onClick={e => e.stopPropagation()}
>
<div style={{ fontSize: "20px", marginBottom: "16px" }}>
Settings
</div>
{/* Settings content: Tool dropdowns */}
<div style={{ marginBottom: "24px" }}>
<div style={{ marginBottom: 12, fontWeight: "bold" }}>Mouse Button Tool Bindings</div>
{MOUSE_BUTTONS.map((btn) => (
<div key={btn.value} style={{ marginBottom: 10 }}>
<label style={{ marginRight: 8 }}>{btn.label} Button:</label>
<select
value={mouseToolBindings[btn.value]}
onChange={e => handleToolChange(btn.value, e.target.value)}
style={{
background: "#333",
color: "#fff",
border: "1px solid #444",
borderRadius: "4px",
padding: "4px 8px",
fontSize: "14px",
}}
>
{TOOL_OPTIONS.map(opt => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
</select>
</div>
))}
</div>
<button
style={{
position: "absolute",
top: 12,
right: 12,
background: "transparent",
color: "#fff",
border: "none",
fontSize: "20px",
cursor: "pointer",
}}
onClick={() => setSettingsOpen(false)}
aria-label="Close"
>
×
</button>
</div>
</div>
)}
{/* ...rest of your UI... */}
</div> </div>
) )
} }
@@ -62,7 +62,7 @@ import cornerstoneDICOMImageLoader from "@cornerstonejs/dicom-image-loader"
return imageIds 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() { export default async function createImageIdsAndCacheMetaData() {
const imageIds = [] const imageIds = []