finish reference cursors

This commit is contained in:
Ross
2025-05-27 22:09:45 +01:00
parent ffb478af59
commit a245c55216
+164 -131
View File
@@ -6,7 +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, PlanarFreehandROITool, PlanarFreehandContourSegmentationTool, SculptorTool, CrosshairsTool, ReferenceLinesTool } from "@cornerstonejs/tools"
import { LengthTool, ProbeTool, ArrowAnnotateTool, RectangleROITool, EllipticalROITool, PlanarFreehandROITool, PlanarFreehandContourSegmentationTool, SculptorTool, CrosshairsTool, ReferenceLinesTool, ReferenceCursors } 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';
@@ -24,7 +24,7 @@ const { IMAGE_RENDERED } = Enums.Events;
const STACK_TOOL_GROUP_ID = 'STACK_TOOL_GROUP_ID';
const VOLUME_TOOL_GROUP_ID = 'VOLUME_TOOL_GROUP_ID';
const REFERENCE_TOOL_GROUP_ID = 'REFERENCE_TOOL_GROUP_ID';
const MAIN_TOOL_GROUP_ID = 'MAIN_TOOL_GROUP_ID';
// Common CT presets (add more as needed)
const WINDOW_LEVEL_PRESETS = [
@@ -50,6 +50,7 @@ const TOOL_OPTIONS = [
{ label: "Freehand ROI", value: PlanarFreehandROITool.toolName },
{ label: "Freehand Contour Segmentation", value: PlanarFreehandContourSegmentationTool.toolName },
{ label: "Sculptor", value: SculptorTool.toolName },
{ label: "Reference Cursors", value: ReferenceCursors.toolName },
]
// Mouse button labels
@@ -122,7 +123,7 @@ function App() {
await setLoadedImageIdsAndCacheMeta(stack);
};
const [crossReferenceEnabled, setCrossReferenceEnabled] = useState(true);
const [crossHairsEnabled, setCrossHairsEnabled] = useState(false);
const [referenceLinesEnabled, setReferenceLinesEnabled] = useState(true);
const sortViewportImageIdsBySliceLocation = () => {
@@ -156,9 +157,6 @@ function App() {
const sortedIds = sorted.map(obj => obj.imageId);
// Log the sorted result for this viewport
console.log(`Viewport ${idx} sorted imageIds:`, sortedIds);
if (JSON.stringify(sortedIds) === JSON.stringify(imageIds)) {
return imageIds;
}
@@ -170,7 +168,6 @@ function App() {
// Wrap setLoadedImageIds to also cache DICOM metadata
const setLoadedImageIdsAndCacheMeta = async (imageIds: string[]) => {
console.log("Caching loaded image IDs:", imageIds);
const loadPromises: Promise<any>[] = [];
for (const imageId of imageIds) {
@@ -196,7 +193,6 @@ function App() {
// Wait for all metadata loads to finish
await Promise.all(loadPromises);
console.log("Image IDs cached:", imageIds);
// Trigger sort if enabled
if (orderBySliceLocation) {
@@ -217,6 +213,7 @@ function App() {
const [orderBySliceLocation, setOrderBySliceLocation] = useState(true);
const [altEnablesReferenceCursors, setAltEnablesReferenceCursors] = useState(true);
// Helper to generate grid options
const gridOptions = []
@@ -340,19 +337,56 @@ function App() {
setMetaModalOpen(true);
};
const [altPressed, setAltPressed] = useState(false);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.altKey) setAltPressed(true);
};
const handleKeyUp = (e: KeyboardEvent) => {
if (!e.altKey) setAltPressed(false);
};
window.addEventListener("keydown", handleKeyDown);
window.addEventListener("keyup", handleKeyUp);
return () => {
window.removeEventListener("keydown", handleKeyDown);
window.removeEventListener("keyup", handleKeyUp);
};
}, []);
useEffect(() => {
if (!altEnablesReferenceCursors) return;
const mainToolGroup = ToolGroupManager.getToolGroup(MAIN_TOOL_GROUP_ID);
if (!mainToolGroup) return;
if (altPressed) {
if (!mainToolGroup.hasTool(ReferenceCursors.toolName)) {
mainToolGroup.addTool(ReferenceCursors.toolName);
}
mainToolGroup.setToolEnabled(ReferenceCursors.toolName);
mainToolGroup.setToolActive(ReferenceCursors.toolName);
} else {
if (mainToolGroup.hasTool(ReferenceCursors.toolName)) {
mainToolGroup.setToolDisabled(ReferenceCursors.toolName);
}
}
}, [altPressed, altEnablesReferenceCursors]);
// Helper to apply all mouse tool bindings (normal or ctrl)
const applyMouseToolBindings = useCallback(() => {
// Apply to both stack and volume tool groups
const stackToolGroup = cornerstoneTools.ToolGroupManager.getToolGroup(STACK_TOOL_GROUP_ID);
const volumeToolGroup = cornerstoneTools.ToolGroupManager.getToolGroup(VOLUME_TOOL_GROUP_ID);
const mainToolGroup = cornerstoneTools.ToolGroupManager.getToolGroup(MAIN_TOOL_GROUP_ID);
[stackToolGroup, volumeToolGroup].forEach(toolGroup => {
[stackToolGroup, volumeToolGroup, mainToolGroup].forEach(toolGroup => {
if (toolGroup) {
// Set all tools to passive first, removing all bindings
TOOL_OPTIONS.forEach(opt => {
toolGroup.setToolPassive(opt.value, { removeAllBindings: true });
});
toolGroup.setToolDisabled(ReferenceCursors.toolName);
// Collect all bindings for each tool
const toolBindings: Record<string, Array<any>> = {};
@@ -468,7 +502,6 @@ function App() {
useEffect(() => {
const setup = async () => {
console.log("Setting up..");
// 1. Initialize Cornerstone3D and tools ONCE
if (!running.current) {
running.current = true;
@@ -488,9 +521,10 @@ function App() {
}
// --- In your setup function, use the correct tool group per viewport ---
const { stackToolGroup, volumeToolGroup, referenceToolGroup } = ToolGroupManager.getToolGroup(STACK_TOOL_GROUP_ID)
? { stackToolGroup: ToolGroupManager.getToolGroup(STACK_TOOL_GROUP_ID), volumeToolGroup: ToolGroupManager.getToolGroup(VOLUME_TOOL_GROUP_ID), referenceToolGroup: ToolGroupManager.getToolGroup(REFERENCE_TOOL_GROUP_ID) }
: setupTools();
let mainToolGroup = ToolGroupManager.getToolGroup(MAIN_TOOL_GROUP_ID);
if (!mainToolGroup) {
mainToolGroup = setupTools().mainToolGroup;
}
const visibleVolumeViewports = viewportModes
@@ -498,16 +532,7 @@ function App() {
.filter(mode => mode === 'volume').length;
// --- In your setup function, after tool group creation, enable/disable ReferenceLinesTool for each tool group ---
if (referenceLinesEnabled) {
const sourceViewportId = `CT_${activeViewport}`;
referenceToolGroup.setToolEnabled(ReferenceLinesTool.toolName);
referenceToolGroup.setToolConfiguration(ReferenceLinesTool.toolName, {
sourceViewportId,
});
} else {
referenceToolGroup.setToolDisabled(ReferenceLinesTool.toolName);
}
updateActiveReferenceLineViewport(mainToolGroup)
// 6. Enable and configure each viewport
for (let i = 0; i < MAX_VIEWPORTS; i++) {
@@ -525,7 +550,7 @@ function App() {
: Enums.ViewportType.STACK;
// Choose tool group based on viewport type
const toolGroup = viewportModes[i] === 'volume' ? volumeToolGroup : stackToolGroup;
//const toolGroup = viewportModes[i] === 'volume' ? volumeToolGroup : stackToolGroup;
@@ -540,14 +565,10 @@ function App() {
});
element._cornerstoneEnabled = true;
element._lastViewportType = viewportType;
toolGroup.addViewport(viewportId, renderingEngineId);
//mainToolGroup.addViewport(viewportId, renderingEngineId);
//toolGroup.addViewport(viewportId, renderingEngineId);
}
// Add tool group only once
toolGroup.addViewport(viewportId, renderingEngineId);
referenceToolGroup.addViewport(viewportId, renderingEngineId);
const viewport = renderingEngine.getViewport(viewportId);
if (!viewport) continue;
@@ -574,8 +595,16 @@ function App() {
if (imageIds) {
viewport.setStack(imageIds);
}
// CrosshairsTool is not applicable for stack viewports
if (!crossHairsEnabled) {
mainToolGroup.addViewport(viewportId, renderingEngineId);
} else {
mainToolGroup.removeViewports(renderingEngineId, viewportId);
}
// Restore scroll, properties, etc.
} else if (viewportModes[i] === 'volume') {
mainToolGroup.addViewport(viewportId, renderingEngineId);
//let sortedImageIds = imageIds;
//if (imageIds && imageIds.length > 1) {
@@ -625,15 +654,7 @@ function App() {
propsToRestore.colormap = colormap;
}
setTimeout(() => {
if (viewportId === "CT_0") {
console.log("Restoring properties:", propsToRestore, viewport);
console.log("VOI range:", voiRange);
}
viewport.setProperties(propsToRestore);
if (viewportId === "CT_0") {
console.log("Restored:", propsToRestore, viewport);
console.log(viewport.getProperties());
}
viewport.render()
}, 100);
}
@@ -659,20 +680,35 @@ function App() {
activeVolumeViewports.push(viewport);
}
}
if (
crossReferenceEnabled &&
activeVolumeViewports.length >= 2
) {
if (!volumeToolGroup.hasTool(CrosshairsTool.toolName)) {
volumeToolGroup.addTool(CrosshairsTool.toolName);
// Find all active volume viewports
const activeVolumeViewportIds = [];
for (let i = 0; i < viewportGrid.rows * viewportGrid.cols; i++) {
if (viewportModes[i] === 'volume') {
activeVolumeViewportIds.push(`CT_${i}`);
}
}
if (mainToolGroup && crossHairsEnabled) {
// Only enable CrosshairsTool if at least two volume viewports are active
if (activeVolumeViewportIds.length >= 2) {
// Make sure CrosshairsTool is added to the tool group
if (!mainToolGroup.hasTool(CrosshairsTool.toolName)) {
mainToolGroup.addTool(CrosshairsTool.toolName);
}
mainToolGroup.setToolEnabled(CrosshairsTool.toolName);
mainToolGroup.setToolConfiguration(CrosshairsTool.toolName, {
// Example: color: '#ffeb3b', lineWidth: 2
});
mainToolGroup.setToolActive(CrosshairsTool.toolName, {
bindings: [{ mouseButton: MouseBindings.Primary }],
});
} else {
// Disable CrosshairsTool if not enough volume viewports
if (mainToolGroup.hasTool(CrosshairsTool.toolName)) {
mainToolGroup.setToolDisabled(CrosshairsTool.toolName);
}
}
volumeToolGroup.setToolEnabled(CrosshairsTool.toolName);
volumeToolGroup.setToolActive(CrosshairsTool.toolName, {
bindings: [{ mouseButton: MouseBindings.Primary }],
});
} else if (volumeToolGroup.hasTool(CrosshairsTool.toolName)) {
volumeToolGroup.setToolDisabled(CrosshairsTool.toolName);
}
// 7. Apply mouse tool bindings
@@ -711,11 +747,9 @@ function App() {
// Handler to apply a preset for a specific viewport
const handleApplyPreset = useCallback((viewportIdx: number, center: number, width: number) => {
console.log("Applying preset:", center, width);
const viewportId = `CT_${viewportIdx}`;
try {
const renderingEngine = renderingEngineRef.current;
console.log("Rendering engine:", renderingEngine);
if (renderingEngine) {
const viewport = renderingEngine.getViewport(viewportId);
if (viewport && typeof viewport.setProperties === "function") {
@@ -775,15 +809,8 @@ function App() {
useEffect(() => {
if (!referenceLinesEnabled) return;
const stackToolGroup = ToolGroupManager.getToolGroup(STACK_TOOL_GROUP_ID);
const volumeToolGroup = ToolGroupManager.getToolGroup(VOLUME_TOOL_GROUP_ID);
const sourceViewportId = `CT_${activeViewport}`;
if (stackToolGroup?.hasTool(ReferenceLinesTool.toolName)) {
stackToolGroup.setToolConfiguration(ReferenceLinesTool.toolName, { sourceViewportId });
}
if (volumeToolGroup?.hasTool(ReferenceLinesTool.toolName)) {
volumeToolGroup.setToolConfiguration(ReferenceLinesTool.toolName, { sourceViewportId });
}
const mainToolGroup = ToolGroupManager.getToolGroup(MAIN_TOOL_GROUP_ID);
updateActiveReferenceLineViewport(mainToolGroup)
}, [activeViewport, referenceLinesEnabled]);
useEffect(() => {
@@ -826,6 +853,19 @@ function App() {
}, [orderBySliceLocation, availableStacks]);
function updateActiveReferenceLineViewport(mainToolGroup: cornerstoneTools.Types.IToolGroup) {
// Ensure the main tool group is defined (it won't be during setup)
if (!mainToolGroup) return;
const sourceViewportId = `CT_${activeViewport}`
if (referenceLinesEnabled) {
mainToolGroup.setToolEnabled(ReferenceLinesTool.toolName)
mainToolGroup.setToolConfiguration(ReferenceLinesTool.toolName, {
sourceViewportId,
})
} else {
mainToolGroup.setToolDisabled(ReferenceLinesTool.toolName)
}
}
const viewports = [];
for (let i = 0; i < MAX_VIEWPORTS; i++) {
@@ -851,11 +891,11 @@ function App() {
visibility: isVisible ? "visible" : "hidden",
pointerEvents: isVisible ? "auto" : "none",
// Remove outline, use only boxShadow for indicator
boxShadow: activeViewport === i
? "0 0 0 4px #1976d2, 0 0 16px 4px #1976d2cc"
: undefined,
cursor: "pointer",
zIndex: activeViewport === i ? 10 : 1,
boxShadow: activeViewport === i
? "0 0 0 4px #1976d2, 0 0 16px 4px #1976d2cc"
: undefined,
cursor: "pointer",
zIndex: activeViewport === i ? 10 : 1,
}}
onClick={() => setActiveViewport(i)}
onDoubleClick={() => handleDoubleClick(i)}
@@ -1242,16 +1282,16 @@ zIndex: activeViewport === i ? 10 : 1,
return (
<div style={{
position: "relative",
width: "99vw",
height: "99vh",
background: "#111",
padding: "12px", // <-- Add this line
boxSizing: "border-box", // <-- Ensure padding doesn't shrink content
background: "#222",
// overflow: "hidden",
}}>
<div style={{
position: "relative",
width: "99vw",
height: "99vh",
background: "#111",
padding: "12px", // <-- Add this line
boxSizing: "border-box", // <-- Ensure padding doesn't shrink content
background: "#222",
// overflow: "hidden",
}}>
<div>
{/* Side menu toggle button */}
<button
@@ -1380,7 +1420,7 @@ zIndex: activeViewport === i ? 10 : 1,
<button
style={{
padding: "10px 18px",
background: crossReferenceEnabled ? "#1976d2" : "#333",
background: crossHairsEnabled ? "#1976d2" : "#333",
color: "#fff",
border: "none",
borderRadius: "4px",
@@ -1390,9 +1430,9 @@ zIndex: activeViewport === i ? 10 : 1,
marginBottom: "18px",
width: "100%",
}}
onClick={() => setCrossReferenceEnabled((prev) => !prev)}
onClick={() => setCrossHairsEnabled((prev) => !prev)}
>
{crossReferenceEnabled ? "Disable" : "Enable"} Cross Reference Lines
{crossHairsEnabled ? "Disable" : "Enable"} Cross Reference Lines
</button>
<button
style={{
@@ -1417,6 +1457,25 @@ zIndex: activeViewport === i ? 10 : 1,
>
Sort All Viewports by Slice Location
</button>
<button
style={{
padding: "10px 18px",
background: altEnablesReferenceCursors ? "#1976d2" : "#333",
color: "#fff",
border: "none",
borderRadius: "4px",
fontWeight: "bold",
cursor: "pointer",
fontSize: "15px",
marginBottom: "18px",
width: "100%",
}}
onClick={() => setAltEnablesReferenceCursors((prev) => !prev)}
>
{altEnablesReferenceCursors
? "Disable Alt Key for Reference Cursors"
: "Enable Alt Key for Reference Cursors"}
</button>
{/* Add more menu items here if needed */}
</div>
@@ -1543,7 +1602,7 @@ zIndex: activeViewport === i ? 10 : 1,
margin: 3,
gridTemplateRows: `repeat(${viewportGrid.rows}, 1fr)`,
gridTemplateColumns: `repeat(${viewportGrid.cols}, 1fr)`,
gap: "5px", // Increase gap for better visibility
gap: "5px", // Increase gap for better visibility
zIndex: 1,
}}
>
@@ -1735,6 +1794,7 @@ zIndex: activeViewport === i ? 10 : 1,
</div >
)
}
export default App
@@ -1753,60 +1813,33 @@ function setupTools() {
cornerstoneTools.addTool(PlanarFreehandROITool)
cornerstoneTools.addTool(PlanarFreehandContourSegmentationTool)
cornerstoneTools.addTool(SculptorTool)
cornerstoneTools.addTool(ReferenceCursors)
cornerstoneTools.addTool(CrosshairsTool)
cornerstoneTools.addTool(ReferenceLinesTool);
// Stack tool group (for stack viewports)
let stackToolGroup = ToolGroupManager.getToolGroup(STACK_TOOL_GROUP_ID);
if (!stackToolGroup) {
stackToolGroup = ToolGroupManager.createToolGroup(STACK_TOOL_GROUP_ID);
stackToolGroup.addTool(WindowLevelTool.toolName);
stackToolGroup.addTool(PanTool.toolName);
stackToolGroup.addTool(ZoomTool.toolName);
stackToolGroup.addTool(StackScrollTool.toolName, { loop: false });
stackToolGroup.addTool(PlanarRotateTool.toolName);
stackToolGroup.addTool(LengthTool.toolName);
stackToolGroup.addTool(ArrowAnnotateTool.toolName, { getTextCallback: () => '' });
stackToolGroup.addTool(ProbeTool.toolName, { statsCalculator: () => null });
stackToolGroup.addTool(RectangleROITool.toolName, { calculateStats: false, getTextCallback: () => '' });
stackToolGroup.addTool(EllipticalROITool.toolName, { getTextCallback: () => '' });
stackToolGroup.addTool(PlanarFreehandROITool.toolName, { calculateStats: false });
stackToolGroup.addTool(PlanarFreehandContourSegmentationTool.toolName, {});
stackToolGroup.addTool(SculptorTool.toolName, {});
//stackToolGroup.addTool(ReferenceLinesTool.toolName);
// StackScrollTool mouse wheel binding
stackToolGroup.setToolActive(StackScrollTool.toolName, {
bindings: [{ mouseButton: MouseBindings.Wheel }],
});
let mainToolGroup = ToolGroupManager.getToolGroup(MAIN_TOOL_GROUP_ID);
if (!mainToolGroup) {
mainToolGroup = ToolGroupManager.createToolGroup(MAIN_TOOL_GROUP_ID);
// Add all tools you need for both stack and volume
mainToolGroup.addTool(WindowLevelTool.toolName);
mainToolGroup.addTool(PanTool.toolName);
mainToolGroup.addTool(ZoomTool.toolName);
mainToolGroup.addTool(StackScrollTool.toolName, { loop: false });
mainToolGroup.addTool(PlanarRotateTool.toolName);
mainToolGroup.addTool(LengthTool.toolName);
mainToolGroup.addTool(ArrowAnnotateTool.toolName, { getTextCallback: () => '' });
mainToolGroup.addTool(ProbeTool.toolName, { statsCalculator: () => null });
mainToolGroup.addTool(RectangleROITool.toolName, { calculateStats: false, getTextCallback: () => '' });
mainToolGroup.addTool(EllipticalROITool.toolName, { getTextCallback: () => '' });
mainToolGroup.addTool(PlanarFreehandROITool.toolName, { calculateStats: false });
mainToolGroup.addTool(PlanarFreehandContourSegmentationTool.toolName, {});
mainToolGroup.addTool(SculptorTool.toolName, {});
//mainToolGroup.addTool(CrosshairsTool.toolName);
mainToolGroup.addTool(ReferenceLinesTool.toolName);
mainToolGroup.addTool(ReferenceCursors.toolName);
}
// Volume tool group (for volume/orthographic viewports)
let volumeToolGroup = ToolGroupManager.getToolGroup(VOLUME_TOOL_GROUP_ID);
if (!volumeToolGroup) {
volumeToolGroup = ToolGroupManager.createToolGroup(VOLUME_TOOL_GROUP_ID);
volumeToolGroup.addTool(StackScrollTool.toolName, { loop: false });
volumeToolGroup.addTool(WindowLevelTool.toolName);
volumeToolGroup.addTool(PanTool.toolName);
volumeToolGroup.addTool(ZoomTool.toolName);
volumeToolGroup.addTool(PlanarRotateTool.toolName);
volumeToolGroup.addTool(LengthTool.toolName);
volumeToolGroup.addTool(ArrowAnnotateTool.toolName, { getTextCallback: () => '' });
volumeToolGroup.addTool(ProbeTool.toolName, { statsCalculator: () => null });
volumeToolGroup.addTool(RectangleROITool.toolName, { calculateStats: false, getTextCallback: () => '' });
volumeToolGroup.addTool(EllipticalROITool.toolName, { getTextCallback: () => '' });
volumeToolGroup.addTool(PlanarFreehandROITool.toolName, { calculateStats: false });
volumeToolGroup.addTool(PlanarFreehandContourSegmentationTool.toolName, {});
volumeToolGroup.addTool(SculptorTool.toolName, {});
//volumeToolGroup.addTool(ReferenceLinesTool.toolName);
//volumeToolGroup.addTool(CrosshairsTool.toolName, {});
}
let referenceToolGroup = ToolGroupManager.getToolGroup(REFERENCE_TOOL_GROUP_ID);
if (!referenceToolGroup) {
referenceToolGroup = ToolGroupManager.createToolGroup(REFERENCE_TOOL_GROUP_ID);
referenceToolGroup.addTool(ReferenceLinesTool.toolName);
}
return { stackToolGroup, volumeToolGroup, referenceToolGroup };
return { mainToolGroup };
}