Skip to content

feat(tools): touch interaction support with dispatcher fallback to mouse callbacks#2815

Open
sedghi wants to merge 2 commits into
mainfrom
cornerstoneTouch
Open

feat(tools): touch interaction support with dispatcher fallback to mouse callbacks#2815
sedghi wants to merge 2 commits into
mainfrom
cornerstoneTouch

Conversation

@sedghi

@sedghi sedghi commented Jul 17, 2026

Copy link
Copy Markdown
Member

Context

An audit of all viewport tools across the clinical modes found that touch failures were architectural, not per-tool: tools entering through preMouseDownCallback / mouseClickCallback were unreachable by touch because the touch pipeline never calls those handlers, hover-dependent affordances have no touch equivalent, and in-draw precision targets stayed at mouse sizes. This PR addresses the tool-framework side of that audit in one pass.

Foundation: dispatcher fallback

  • New eventDispatchers/shared/getTouchCallbackWithMouseFallback.ts: touch dispatchers resolve the explicit touch callback when a tool implements it, otherwise fall back to the mouse counterpart (touchDragCallback -> mouseDragCallback, preTouchStartCallback -> preMouseDownCallback, postTouchStartCallback -> postMouseDownCallback, touchTapCallback -> mouseClickCallback).
  • The fallback (only the fallback) is gated on the tool declaring 'Touch' in supportedInteractionTypes - the first runtime consumer of that field. Explicit touch callbacks keep working regardless of declaration.
  • A payload adapter additively synthesizes the mouse-detail fields touch events lack (mouseButton, startPoints/lastPoints/deltaPoints on tap details).
  • touchTap.ts is now a dedicated handler (first-Active-tool-wins semantics preserved); touchStartActivate gained the same try/catch as its mouse twin; the TOUCH_TAP listener leak in touchToolEventDispatcher.disable is fixed.
  • Deferred-tap guard in touchStartListener: a pending TOUCH_TAP timer no longer fires into a new in-progress gesture (it used to end brush/scissors strokes mid-drag).
  • Declaration corrections: Crosshairs ['Mouse'] -> ['Mouse','Touch']; hover-only SegmentSelect/SegmentLabel narrowed to ['Mouse']; VolumeCropping and RectangleROIStartEndThreshold gained declarations.
  • Companion safety edits for tools the fallback newly reaches: TOUCH listener triples in RegionSegment/WholeBodySegment draw loops, TrackballRotate touchend cleanup beside its mouseup-only sample-distance restore.

Engine

  • The rendering engine sets touch-action: none on every enabled viewport element (restored on disable) via RenderingEngine/helpers/elementTouchAction.ts, so browsers no longer consume viewport gestures (scroll, pinch zoom, double-tap zoom). Apps no longer need per-app workarounds.

Per-tool touch work

  • Brush family: TOUCH listeners in the draw loop; strokes paint by finger. When a second finger joins mid-stroke (pinch zoom), the stroke cancels and rolls back its voxel writes via the memo system without entering undo history.
  • Scissors x3: same multi-finger cancellation - the rubber band is dropped instead of resizing at the mean touch point and applying on release.
  • Spline/Livewire: double-tap closes the contour; the first-point close target widens on touch (touchCloseCurveDistance); Livewire's live path preview tracks the finger during a drag.
  • Probe/DragProbe/Magnify: render offset above the finger on touch so the finger does not occlude the readout/loupe (touchCanvasOffset, tunable, {x:0,y:0} disables).
  • AdvancedMagnify: touches inside the loupe no longer leak to the underlying viewport; long-press on the loupe opens the zoom-factor picker (mouse binding unchanged); on touch, dragging anywhere on the glass moves the loupe (ring handles still resize) and interior taps no longer stack a second magnifier.
  • VolumeRotate: additive drag-to-rotate for both mouse and touch (rotateDragDegreesPerPixel); wheel behavior unchanged.
  • ClickSegment: on touch, the probe runs at the tap point instead of requiring hover-arming; the desktop hover flow is untouched.
  • Crosshairs: isPointNearTool honors the dispatcher-provided proximity (36px on touch instead of the hardcoded 6); the mobile config block defaults on for coarse-pointer platforms (same (any-pointer:coarse) check the examples use; configuration.mobile.enabled = false opts out).
  • ArrowAnnotate: double-tap label re-edit uses the touch proximity.

Fixes found during device testing

  • ToolGroup.clone() crashed on tools added to a group without ever receiving a mode (undefined toolOptions read) - hit by AdvancedMagnify's tool-group cloning outside OHIF-style setups.
  • getCurrentVolumeViewportSlice error message named Livewire but the utility is shared with WindowLevelRegion; generalized.

Example

  • New touchAllTools example: sagittal volume + stack viewport, every touch-capable tool selectable onto Primary (1-finger), brush size/segment index controls, an on-page log console for device debugging, and mobile-safe init (isMobile, capability-gated preferSizeOverAccuracy). The example runner gained an env-gated LAN mode (CS3D_ALLOW_LAN=1) for phone testing.

Behavior notes for reviewers

  • Tap routing: a tool with only mouseClickCallback that declares 'Touch' (e.g. MIPJumpToClick) now receives taps; in groups where such a tool precedes another tap-consumer in add order, tap delivery can change.
  • SegmentSelect/SegmentLabel default declarations narrowed to ['Mouse'] (they are hover-only; consumers can still pass their own supportedInteractionTypes).
  • Mouse behavior is unchanged throughout; new interactions were added to mouse only where they are strictly additive (VolumeRotate drag).

Testing

  • tsc --noEmit clean on core and tools; oxlint clean (no new warnings).
  • Verified interactively with synthetic touch sequences (Chromium touch emulation) and on-device iOS WebKit (Simulator via safaridriver + iPhone hardware): brush paint/cancel-on-pinch rollback, scissors cancel, spline/livewire double-tap close, probe/magnify offsets, AdvancedMagnify move/resize/press-picker, Crosshairs handle visibility, MIPJumpToClick tap.

Summary by CodeRabbit

  • New Features
    • Added mobile/touch capabilities across viewing and key tools, including a Touch: All Tools demo.
    • Introduced mobile detection and optional LAN development-server access.
  • Bug Fixes
    • Prevented browser touch gestures from interfering with custom viewports by routing touch input to tools and restoring prior behavior afterward.
    • Improved touch event handling with touch-to-mouse callback fallback, refined tap behavior, and safer touch-start annotation flows.
    • Enhanced touch interaction reliability: multi-touch cancellation, loupe positioning offsets, and expanded touch end/cancel cleanup; clarified rotate/oblique error messaging.

…use callbacks

Touch dispatchers now fall back to the mouse counterpart callback when a tool has no touch-specific handler (touchDragCallback to mouseDragCallback, pre/postTouchStartCallback to pre/postMouseDownCallback, touchTapCallback to mouseClickCallback), gated by the tool declaring Touch in supportedInteractionTypes. This makes the previously mouse-only entry tools (Brush, Scissors, PaintFill, MIPJumpToClick, ClickSegment, Sculptor, RegionSegment, WholeBodySegment) reachable by touch, and the rendering engine now sets touch-action none on enabled viewport elements so the browser no longer consumes viewport gestures.
@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 9cdaf598-d77f-44ed-8076-0d46a8e213df

📥 Commits

Reviewing files that changed from the base of the PR and between aec4ad1 and 81be05d.

📒 Files selected for processing (6)
  • packages/tools/examples/touchAllTools/index.ts
  • packages/tools/src/tools/CrosshairsTool.ts
  • packages/tools/src/tools/TrackballRotateTool.ts
  • packages/tools/src/tools/VolumeCroppingTool.ts
  • packages/tools/src/tools/segmentation/BrushTool.ts
  • utils/ExampleRunner/template-config.js
🚧 Files skipped from review as they are similar to previous changes (3)
  • utils/ExampleRunner/template-config.js
  • packages/tools/src/tools/segmentation/BrushTool.ts
  • packages/tools/examples/touchAllTools/index.ts

📝 Walkthrough

Walkthrough

Touch support is expanded across viewport touch-action handling, event callback fallback, gesture-aware tools, segmentation drawing lifecycles, a touch-focused example, and optional LAN development-server configuration.

Changes

Touch interaction support

Layer / File(s) Summary
Viewport touch-action management
packages/core/src/RenderingEngine/*, packages/core/src/RenderingEngine/helpers/elementTouchAction.ts
Active viewports preserve prior inline touch-action, disable browser touch handling, and restore it during reset.
Touch event routing and fallback callbacks
packages/tools/src/eventDispatchers/*, packages/tools/src/eventListeners/*, packages/tools/src/store/ToolGroupManager/ToolGroup.ts, packages/tools/src/utilities/touch/index.ts
Touch dispatch resolves touch callbacks or augmented mouse fallbacks, suppresses stale taps, and cleans up listeners.
Touch-aware tool behavior
packages/tools/src/tools/AdvancedMagnifyTool.ts, packages/tools/src/tools/MagnifyTool.ts, packages/tools/src/tools/CrosshairsTool.ts, packages/tools/src/tools/VolumeRotateTool.ts, packages/tools/src/tools/annotation/*
Tools add touch positioning, proximity, rotation, annotation, probe, magnify-picker, and interaction-type handling.
Touch drawing lifecycles
packages/tools/src/tools/annotation/*, packages/tools/src/tools/segmentation/*
Drawing tools register touch handlers, suppress tap echoes, cancel multi-touch edits, clear temporary state, and update supported interaction types.
Touch demo and development configuration
packages/tools/examples/touchAllTools/index.ts, utils/ExampleRunner/template-config.js, packages/core/src/utilities/getCurrentVolumeViewportSlice.ts
A touch-tools demo is added with segmentation, diagnostics, and logging UI; example dev servers optionally allow LAN access; one error message is corrected.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Browser
  participant TouchListener
  participant Dispatcher
  participant Tool
  Browser->>TouchListener: emit touch start, drag, or tap
  TouchListener->>Dispatcher: dispatch touch event
  Dispatcher->>Tool: resolve touch callback
  Dispatcher->>Tool: invoke mouse fallback when applicable
  Tool->>Browser: update interaction state and rendering
Loading

Suggested reviewers: wayfarer3130, mbellehumeur, wayfarer3130

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description covers context, changes, and testing, but the checklist and tested environment sections are left incomplete, which the template requires. Fill in the checklist by marking applicable boxes, and add the OS, Node, and browser versions used for testing.
Docstring Coverage ⚠️ Warning Docstring coverage is 53.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise, semantic-release compliant, and accurately summarizes the main change: touch interaction support with mouse-callback fallback.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cornerstoneTouch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
putComment timed out

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/tools/src/tools/annotation/WholeBodySegmentTool.ts (1)

129-135: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Guarantee cleanup when grow-cut fails.

If runGrowCut() rejects, growCutData remains populated and the cursor stays hidden. Move state, cursor, and render cleanup into finally.

Proposed fix
     this._deactivateDraw(element);
-    await this.runGrowCut();
-
-    this.growCutData = null;
-
-    resetElementCursor(element);
-    triggerAnnotationRenderForViewportUIDs([viewport.id]);
+    try {
+      await this.runGrowCut();
+    } finally {
+      this.growCutData = null;
+      resetElementCursor(element);
+      triggerAnnotationRenderForViewportUIDs([viewport.id]);
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/tools/src/tools/annotation/WholeBodySegmentTool.ts` around lines 129
- 135, Update the flow around runGrowCut in WholeBodySegmentTool so cleanup
always executes when it resolves or rejects: place growCutData reset,
resetElementCursor(element), and
triggerAnnotationRenderForViewportUIDs([viewport.id]) in a finally block.
Preserve the existing grow-cut error propagation while ensuring state, cursor,
and render cleanup are performed on failure.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/tools/examples/touchAllTools/index.ts`:
- Around line 179-189: Add ClickSegmentTool to every required demo setup:
register it, include it in the volume tool group, and append its toolName to the
selectable segmentation list alongside the existing segmentation tools. Use the
existing ClickSegmentTool symbol and preserve the current ordering and
configuration conventions.
- Around line 151-189: Add CrosshairsTool.toolName to the selectableToolNames
and selection setup, and update the volume tool group configuration to create
and register a second orthographic volume viewport. Ensure both volume viewports
participate in the Crosshairs linking/deactivation flow so Crosshairs is
reachable and functional with two viewports.

In `@packages/tools/src/tools/annotation/ClickSegmentTool.ts`:
- Around line 820-836: Update the touch path around runHoverProbe and
preMouseDownCallback so it awaits the runConfinementDryRun verdict before
invoking segmentation. Propagate or explicitly await the dry-run promise,
ensuring lastProbe reflects the completed 3D confinement check and rejected
regions return through notifySegmentationError without starting segmentation.

In `@packages/tools/src/tools/CrosshairsTool.ts`:
- Line 223: Update the interaction-mode selection around the enabled setting in
CrosshairsTool so coarse secondary pointers on hybrid devices do not force
mobile behavior for mouse input. Use the primary-pointer capability or determine
mobile behavior from the current interaction, while preserving mobile behavior
for genuinely primary touch interactions and ensuring mouse gestures set
state.isInteractingWithTool.

In `@packages/tools/src/tools/segmentation/BrushTool.ts`:
- Around line 464-470: Update BrushTool._clearCursor to trigger annotation
rendering for every viewport recorded by _refreshCursor in viewportIdsToRender,
rather than only the source enabledElement viewport. Clear or preserve
_hoverData as needed so the stored viewport list remains available until all
affected viewports are scheduled for rerender, preventing stale cursors in
linked viewports.

In `@packages/tools/src/tools/TrackballRotateTool.ts`:
- Around line 73-90: Complete touch termination cleanup in TrackballRotateTool
and VolumeCroppingTool: update TrackballRotateTool’s cleanup registration and
removal to include touchcancel alongside touchend and mouseup, and add touchend
and touchcancel cleanup in VolumeCroppingTool before enabling touch fallback.

In `@utils/ExampleRunner/template-config.js`:
- Around line 119-129: Update the CS3D_ALLOW_LAN condition in the template
configuration so LAN exposure is enabled only when process.env.CS3D_ALLOW_LAN
explicitly equals the string '1'. Preserve the existing restricted allowedHosts
configuration for all other values, including '0' and 'false'.

---

Outside diff comments:
In `@packages/tools/src/tools/annotation/WholeBodySegmentTool.ts`:
- Around line 129-135: Update the flow around runGrowCut in WholeBodySegmentTool
so cleanup always executes when it resolves or rejects: place growCutData reset,
resetElementCursor(element), and
triggerAnnotationRenderForViewportUIDs([viewport.id]) in a finally block.
Preserve the existing grow-cut error propagation while ensuring state, cursor,
and render cleanup are performed on failure.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7044fb68-29bd-464b-878f-0d090f99224c

📥 Commits

Reviewing files that changed from the base of the PR and between a0bb6c5 and aec4ad1.

📒 Files selected for processing (37)
  • packages/core/src/RenderingEngine/BaseRenderingEngine.ts
  • packages/core/src/RenderingEngine/ContextPoolRenderingEngine.ts
  • packages/core/src/RenderingEngine/TiledRenderingEngine.ts
  • packages/core/src/RenderingEngine/helpers/elementTouchAction.ts
  • packages/core/src/utilities/getCurrentVolumeViewportSlice.ts
  • packages/tools/examples/touchAllTools/index.ts
  • packages/tools/src/eventDispatchers/shared/getTouchCallbackWithMouseFallback.ts
  • packages/tools/src/eventDispatchers/touchEventHandlers/touchDrag.ts
  • packages/tools/src/eventDispatchers/touchEventHandlers/touchStart.ts
  • packages/tools/src/eventDispatchers/touchEventHandlers/touchStartActivate.ts
  • packages/tools/src/eventDispatchers/touchEventHandlers/touchTap.ts
  • packages/tools/src/eventDispatchers/touchToolEventDispatcher.ts
  • packages/tools/src/eventListeners/touch/touchStartListener.ts
  • packages/tools/src/store/ToolGroupManager/ToolGroup.ts
  • packages/tools/src/tools/AdvancedMagnifyTool.ts
  • packages/tools/src/tools/CrosshairsTool.ts
  • packages/tools/src/tools/MagnifyTool.ts
  • packages/tools/src/tools/TrackballRotateTool.ts
  • packages/tools/src/tools/VolumeCroppingTool.ts
  • packages/tools/src/tools/VolumeRotateTool.ts
  • packages/tools/src/tools/annotation/ArrowAnnotateTool.ts
  • packages/tools/src/tools/annotation/ClickSegmentTool.ts
  • packages/tools/src/tools/annotation/DragProbeTool.ts
  • packages/tools/src/tools/annotation/LivewireContourTool.ts
  • packages/tools/src/tools/annotation/ProbeTool.ts
  • packages/tools/src/tools/annotation/RegionSegmentTool.ts
  • packages/tools/src/tools/annotation/SplineROITool.ts
  • packages/tools/src/tools/annotation/WholeBodySegmentTool.ts
  • packages/tools/src/tools/segmentation/BrushTool.ts
  • packages/tools/src/tools/segmentation/CircleScissorsTool.ts
  • packages/tools/src/tools/segmentation/RectangleROIStartEndThresholdTool.ts
  • packages/tools/src/tools/segmentation/RectangleScissorsTool.ts
  • packages/tools/src/tools/segmentation/SegmentLabelTool.ts
  • packages/tools/src/tools/segmentation/SegmentSelectTool.ts
  • packages/tools/src/tools/segmentation/SphereScissorsTool.ts
  • packages/tools/src/utilities/touch/index.ts
  • utils/ExampleRunner/template-config.js

Comment thread packages/tools/examples/touchAllTools/index.ts
Comment thread packages/tools/examples/touchAllTools/index.ts
Comment thread packages/tools/src/tools/annotation/ClickSegmentTool.ts
Comment thread packages/tools/src/tools/CrosshairsTool.ts
Comment thread packages/tools/src/tools/segmentation/BrushTool.ts
Comment thread packages/tools/src/tools/TrackballRotateTool.ts
Comment thread utils/ExampleRunner/template-config.js
Crosshairs skips the global interaction lock per touch gesture instead of per device capability so mouse drags on hybrid hardware keep the lock; brush cursor clearing rerenders all linked viewports; TrackballRotate and VolumeCropping clean up on touchend/touchcancel; touchAllTools example description matches the two-viewport roster and drops vestigial Crosshairs wiring; example runner LAN flag requires CS3D_ALLOW_LAN=1 exactly.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant