π Introduction & Background
Hi, I'm a developer based in South Korea building a web-based PACS (Picture Archiving and Communication System) viewer currently deployed across multiple medical institutions. We work in close contact with clinical end-users and have found cornerstone-tools to be an invaluable part of our stack.
π§ The Problem
Recently, we've been hearing consistent feedback from clinicians about the inefficiency of the current annotation control and deletion workflow.
In complex diagnostic cases with many ROIs drawn, the existing approach creates a significant bottleneck:
- Too many steps to erase: To delete an annotation, users must switch tool modes (e.g., to
EraseTool) and then precisely click on the target object's outline.
- Repetitive and fatiguing: When dozens of annotations need to be cleared, this one-by-one approach wastes an enormous amount of mouse movement β a real burden for busy clinicians.
π‘ The Inspiration
This feedback led me to ask: "What if users could drag to multi-select annotations, move them as a group, or delete them all at once with the Delete key β just like in PowerPoint or Figma?"
I went ahead and implemented this: a globally managed multi-selection state with support for group move and batch deletion.
This feature has received overwhelmingly positive feedback from clinical staff and has meaningfully improved their workflow efficiency. I'd like to propose that this UX be considered for integration into Cornerstone3DTools as a core feature or official Tool, rather than remaining a custom workaround.
π Suggested Solution
β οΈ Note on Codebase & Implementation: Our product currently runs on a deprecated legacy version of cornerstone-tools, and the implementation described below is built as custom React hooks in a TypeScript frontend viewer.
That said, the core logic β event guards, state separation, coordinate math β can reasonably be ported into a pure TypeScript Cornerstone3D Tool with React dependencies removed. Despite the version gap, I hope the UX concept and logical direction of this proposal can be evaluated on its own merits.
Here are the three core implementation patterns I've developed and validated in production:
Core Implementation Logic 1: Event Guard β Preventing Conflicts with Existing Tools
When capturing drag events on the canvas background, we need strict guard conditions at mousedown to avoid conflicting with other active tools such as Window/Level adjustment or annotation drawing.
What happens without proper guard conditions
const handleMouseDown = (e) => {
if (e.button !== 0) return; // Left click only
if (choiceTool !== 'default') return; // Only allow drag-box in default/select mode
// If the user is clicking on an existing annotation to move it individually,
// do NOT initiate a drag-selection box
let isClickingOnAnnotation = false;
const tools = cornerstoneTools.store.state.tools;
tools.forEach((tool) => {
const toolState = cornerstoneTools.getToolState(element, tool.name);
if (toolState && toolState.data) {
if (toolState.data.find(anno => anno.active)) isClickingOnAnnotation = true;
}
});
if (isClickingOnAnnotation) return;
isMouseDownRef.current = true;
};
Visual Feedback: Rubber Band Selection UI
The drag selection area is visualized as a "rubber band" box rendered as a separate DOM overlay. The useDragSelection hook returns a dragBox state object (top, left, width, height), which the viewer component renders conditionally.
{dragBox && (
<div
style={{
position: 'fixed', // Absolute screen coordinates based on e.clientX/Y
backgroundColor: 'rgba(0, 120, 215, 0.3)', // Semi-transparent blue fill
border: '1px solid #0078D7',
top: dragBox.top,
left: dragBox.left,
width: dragBox.width,
height: dragBox.height,
pointerEvents: 'none', // β οΈ Critical: prevents the overlay from intercepting mouse events
zIndex: 9999,
}}
/>
)}
pointerEvents: 'none' is not merely a style choice β it is functionally critical. Without it, the overlay div would intercept mouse events during drag, causing Cornerstone's mousemove and mouseup events to fire incorrectly.
Core Implementation Logic 2: The selected State β Foundation for Group Move and Batch Delete
The most important design decision in this implementation is introducing a custom selected property instead of reusing the existing active property.
In the current Cornerstone ecosystem, simply hovering over an annotation sets its active state to true. If active were used as the multi-selection flag, pressing Delete would inadvertently remove any annotation the cursor happened to be over β an annoying and confusing bug. To prevent this, we strictly separate the hover state (active) from the explicit selection state (selected).
This selected property serves as the shared foundation for both Group Move and Batch Delete described below.
if (isFullyContained) {
anno.selected = true; // Use 'selected' instead of 'active'
anno.color = '#00FF00';
needsUpdate = true;
} else {
if (!isMultiSelect && anno.selected) {
anno.selected = false;
delete anno.color;
needsUpdate = true;
}
}
β Group Move
Drags all selected annotations together as a unit. At mousedown, we detect whether the user clicked on an already-selected annotation, and if so, switch into group-move mode. Two key design points:
- Handle click guard: The
isHandleClicked flag ensures that clicking directly on an individual handle vertex switches to single-handle editing, not group move.
- Immutable Origin pattern: Handle coordinates at drag-start are deep-copied into
initialStates. On each mousemove, positions are computed by adding delta(dx, dy) to the original coordinates β eliminating accumulated floating-point drift.
// Inside mousedown handler
// Condition: clicked on an already-selected annotation, not on a handle directly
if (clickedAnnotation?.selected && allSelectedAnnos.length > 1 && !isMultiSelect && !isHandleClicked) {
e.stopImmediatePropagation();
e.preventDefault();
const startImagePt = cornerstone.pageToPixel(element, e.pageX, e.pageY);
let hasMoved = false;
// Deep-copy handle coordinates at drag-start to preserve the immutable origin
const initialStates = allSelectedAnnos.map(anno => ({
anno,
origHandles: JSON.parse(JSON.stringify(anno.handles))
}));
// Recursively traverses handle structure and applies the delta offset
// Handles both array-type points (e.g. FreehandRoi) and single-object points (e.g. start/end)
const shiftHandles = (orig, target, dx, dy) => {
Object.keys(orig).forEach(key => {
if (key === 'boundingBox') return;
if (Array.isArray(orig[key])) {
orig[key].forEach((pt, i) => {
if (typeof pt.x === 'number') {
target[key][i].x = pt.x + dx;
target[key][i].y = pt.y + dy;
}
});
} else if (orig[key] && typeof orig[key] === 'object') {
if (typeof orig[key].x === 'number') {
target[key].x = orig[key].x + dx;
target[key].y = orig[key].y + dy;
}
shiftHandles(orig[key], target[key], dx, dy);
}
});
};
const handleGroupMove = (moveEvent) => {
hasMoved = true;
const currentImagePt = cornerstone.pageToPixel(element, moveEvent.pageX, moveEvent.pageY);
const dx = currentImagePt.x - startImagePt.x;
const dy = currentImagePt.y - startImagePt.y;
// Apply delta on top of the preserved origin coordinates
initialStates.forEach(({ anno, origHandles }) => {
shiftHandles(origHandles, anno.handles, dx, dy);
});
cornerstone.updateImage(element);
};
const handleGroupUp = () => {
window.removeEventListener('mousemove', handleGroupMove);
window.removeEventListener('mouseup', handleGroupUp);
// If no movement occurred β treat as a click and deselect all others
if (!hasMoved) {
allSelectedAnnos.forEach(anno => {
if (anno !== clickedAnnotation) {
anno.selected = false;
delete anno.color;
}
});
cornerstone.updateImage(element);
}
};
window.addEventListener('mousemove', handleGroupMove);
window.addEventListener('mouseup', handleGroupUp);
return; // Prevent single-selection logic from running after this
}
β‘ Batch Delete via Delete Key
Deletes all selected annotations in one shot. Bind this function to a global keydown listener for the Delete key to achieve intuitive PPT-style group deletion. To avoid index shifting bugs during multi-deletion, the array is iterated in reverse before calling splice.
const deleteSelectedAnnotations = () => {
const wadoElements = _getWadoElements();
wadoElements.forEach((value) => {
const element = value.element;
const tools = cornerstoneTools.store.state.tools;
let isDeleted = false;
tools.forEach((tool) => {
const toolState = cornerstoneTools.getToolState(element, tool.name);
if (toolState && toolState.data) {
// Iterate in reverse to avoid index shifting on splice
for (let i = toolState.data.length - 1; i >= 0; i--) {
if (toolState.data[i].selected) {
toolState.data.splice(i, 1);
isDeleted = true;
}
}
}
});
if (isDeleted) cornerstone.updateImage(element);
});
};
// Bind this to a global keydown listener for the 'Delete' key
β¨ Expected Impact
- Efficiency: Dramatically reduces the number of mouse actions required, shortening diagnostic and reporting workflows for clinicians.
- Desktop-class UX: Provides a familiar and intuitive UX that many users already know from tools like PowerPoint and Figma.
- Extensibility: Once multi-selection is part of the core, it naturally unlocks further capabilities β batch property/color changes, group move (already implemented and validated), and more.
π¨ Additional: PPT-style Selection Handle UI
Taking further inspiration from PowerPoint, we also implemented a visual overlay that renders handle dots and a dashed bounding box on selected annotations as a separate DOM layer.
I've kept the implementation details out of this post to avoid making it too long. If there's positive interest, I'm happy to share the full breakdown in a follow-up comment.
β
Next Steps
This entire feature set has been successfully validated in a production clinical environment. If the maintainers are open to this UX direction, I would love to discuss contributing a proper Cornerstone3D-native implementation as a PR β porting the logic out of our React-specific hooks and into the official Tool architecture. Looking forward to your feedback!
Why should we prioritize this feature?
1. Why prioritize this feature? (Clinical & UX Impact)
This feature addresses a critical bottleneck frequently reported by our clinical end-users. Currently, managing (moving or deleting) multiple annotations requires repetitive, one-by-one interactions, which causes significant fatigue when dealing with complex diagnostic cases.
Implementing a DragSelectionTool brings a familiar, desktop-class UX (similar to Figma or PowerPoint) to Cornerstone. By drastically reducing unnecessary mouse movements and resolving these inefficient workflows, it directly answers the real-world feedback from medical professionals.
2. Interaction with existing features (Coexistence & Enhancement)
This feature acts as a global management layer that smoothly interacts with all existing annotation tools (e.g., Rectangle, Ellipse, Freehand, Probe) without breaking them.
Crucially, it is designed to strictly coexist without conflicts:
- Event Guard: The drag-selection box only activates in the 'default' mode. It explicitly checks for
active annotations on mousedown to prevent any interference with existing W/L adjustments, panning, or active drawing tools.
- State Separation: By introducing a dedicated
selected state instead of reusing the hover-based active state, it ensures that users don't accidentally delete an annotation just because their cursor was hovering over it when pressing the Delete key.
- It enhances the existing tools by unlocking batch operations (Group Move, Batch Delete) on them without modifying the core drawing logic of individual tools.
π Introduction & Background
Hi, I'm a developer based in South Korea building a web-based PACS (Picture Archiving and Communication System) viewer currently deployed across multiple medical institutions. We work in close contact with clinical end-users and have found
cornerstone-toolsto be an invaluable part of our stack.π§ The Problem
Recently, we've been hearing consistent feedback from clinicians about the inefficiency of the current annotation control and deletion workflow.
In complex diagnostic cases with many ROIs drawn, the existing approach creates a significant bottleneck:
EraseTool) and then precisely click on the target object's outline.π‘ The Inspiration
This feedback led me to ask: "What if users could drag to multi-select annotations, move them as a group, or delete them all at once with the
Deletekey β just like in PowerPoint or Figma?"I went ahead and implemented this: a globally managed multi-selection state with support for group move and batch deletion.
This feature has received overwhelmingly positive feedback from clinical staff and has meaningfully improved their workflow efficiency. I'd like to propose that this UX be considered for integration into
Cornerstone3DToolsas a core feature or official Tool, rather than remaining a custom workaround.π Suggested Solution
Here are the three core implementation patterns I've developed and validated in production:
Core Implementation Logic 1: Event Guard β Preventing Conflicts with Existing Tools
When capturing drag events on the canvas background, we need strict guard conditions at
mousedownto avoid conflicting with other active tools such as Window/Level adjustment or annotation drawing.What happens without proper guard conditions
Visual Feedback: Rubber Band Selection UI
The drag selection area is visualized as a "rubber band" box rendered as a separate DOM overlay. The
useDragSelectionhook returns adragBoxstate object (top, left, width, height), which the viewer component renders conditionally.pointerEvents: 'none'is not merely a style choice β it is functionally critical. Without it, the overlaydivwould intercept mouse events during drag, causing Cornerstone'smousemoveandmouseupevents to fire incorrectly.Core Implementation Logic 2: The
selectedState β Foundation for Group Move and Batch DeleteThe most important design decision in this implementation is introducing a custom
selectedproperty instead of reusing the existingactiveproperty.In the current Cornerstone ecosystem, simply hovering over an annotation sets its
activestate totrue. Ifactivewere used as the multi-selection flag, pressingDeletewould inadvertently remove any annotation the cursor happened to be over β an annoying and confusing bug. To prevent this, we strictly separate the hover state (active) from the explicit selection state (selected).This
selectedproperty serves as the shared foundation for both Group Move and Batch Delete described below.β Group Move
Drags all
selectedannotations together as a unit. Atmousedown, we detect whether the user clicked on an already-selected annotation, and if so, switch into group-move mode. Two key design points:isHandleClickedflag ensures that clicking directly on an individual handle vertex switches to single-handle editing, not group move.initialStates. On eachmousemove, positions are computed by addingdelta(dx, dy)to the original coordinates β eliminating accumulated floating-point drift.β‘ Batch Delete via
DeleteKeyDeletes all
selectedannotations in one shot. Bind this function to a globalkeydownlistener for theDeletekey to achieve intuitive PPT-style group deletion. To avoid index shifting bugs during multi-deletion, the array is iterated in reverse before callingsplice.β¨ Expected Impact
π¨ Additional: PPT-style Selection Handle UI
Taking further inspiration from PowerPoint, we also implemented a visual overlay that renders handle dots and a dashed bounding box on selected annotations as a separate DOM layer.
I've kept the implementation details out of this post to avoid making it too long. If there's positive interest, I'm happy to share the full breakdown in a follow-up comment.
β Next Steps
This entire feature set has been successfully validated in a production clinical environment. If the maintainers are open to this UX direction, I would love to discuss contributing a proper Cornerstone3D-native implementation as a PR β porting the logic out of our React-specific hooks and into the official Tool architecture. Looking forward to your feedback!
Why should we prioritize this feature?
1. Why prioritize this feature? (Clinical & UX Impact)
This feature addresses a critical bottleneck frequently reported by our clinical end-users. Currently, managing (moving or deleting) multiple annotations requires repetitive, one-by-one interactions, which causes significant fatigue when dealing with complex diagnostic cases.
Implementing a
DragSelectionToolbrings a familiar, desktop-class UX (similar to Figma or PowerPoint) to Cornerstone. By drastically reducing unnecessary mouse movements and resolving these inefficient workflows, it directly answers the real-world feedback from medical professionals.2. Interaction with existing features (Coexistence & Enhancement)
This feature acts as a global management layer that smoothly interacts with all existing annotation tools (e.g., Rectangle, Ellipse, Freehand, Probe) without breaking them.
Crucially, it is designed to strictly coexist without conflicts:
activeannotations onmousedownto prevent any interference with existing W/L adjustments, panning, or active drawing tools.selectedstate instead of reusing the hover-basedactivestate, it ensures that users don't accidentally delete an annotation just because their cursor was hovering over it when pressing theDeletekey.