Skip to content

[Feature Request] New Tool Proposal: DragSelectionTool for Group Move & Batch DeletionΒ #2684

@happysuwonboy

Description

@happysuwonboy

πŸ‘‹ 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

  1. Efficiency: Dramatically reduces the number of mouse actions required, shortening diagnostic and reporting workflows for clinicians.
  2. Desktop-class UX: Provides a familiar and intuitive UX that many users already know from tools like PowerPoint and Figma.
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions