Skip to content

Rubber band selection #42

Open

Description

@lucasfelixs

Context

Today the only ways to select entities are clicking (replace) and modifier-clicking (Shift adds, Ctrl/⌘ toggles). Selecting many entities at once (a row of seats, every desk in a room) means clicking each one.

Rubber band selection (drag a rectangle on an empty area to select everything inside it) is a standard interaction in any 2D editor and a prerequisite for useful bulk operations such as moving or deleting a group.


What changes

Core: a pure query for entities inside a rectangle. Similar to hitTestPoint, it respects selectable and works in world space:

// @spatial-kit/core
export function hitTestBounds(
  scene: Scene,
  boundsWorld: Bounds,
  mode?: 'intersect' | 'contain', // default: 'intersect'
): EntityId[];

// exposed on SpatialCore
core.hitTestBounds(boundsWorld, mode?): EntityId[];

Selection itself reuses the existing core.setSelection(ids, mode), so there is no new selection logic.

SVG and React: a new drag mode. The pointer state machine in handlers.ts today has idle → pending → pan | entity | resize. Add a marquee mode:

  1. pointerdown on an empty area, with the configured trigger → pending, as today.
  2. Past clickThresholdPx → marquee. Draw a rectangle in overlaysG (screen-constant stroke, like the resize handles).
  3. pointermove → update the rectangle. Optionally preview the selection live.
  4. pointerup → core.setSelection(ids, mode). Shift = add, Ctrl/⌘ = toggle, otherwise replace, the same as modifierSelect for clicks. Remove the rectangle.

Note: the React adapter has its own copy of the handlers in packages/adapters/react/src/internal/attachHandlers.ts. Both need the change.


Open questions

Comment before starting:

  • Trigger. Dragging an empty area pans today. Options: (a) a new option emptyDrag: 'pan' | 'select' (default 'pan', so nothing breaks); (b) Shift+drag on an empty area starts a marquee; (c) both. Proposal: (a), with marquee styling options next to it.
  • Hit mode default. intersect (touching is enough, as in Figma) or contain (fully inside, as in many CAD tools)?

Affected files

File Change
packages/core/src/picking.ts Add hitTestBounds
packages/core/src/core.ts Expose core.hitTestBounds
packages/core/src/utils.ts Possibly an intersectsAABB / containsAABB helper
packages/svg/src/handlers.ts marquee drag mode
packages/svg/src/index.ts, renderer.ts New option(s) and passing them to the handlers
packages/adapters/react/src/internal/attachHandlers.ts Same marquee mode
packages/adapters/react/src/SpatialCanvas.tsx Prop(s) and rectangle rendering
READMEs (root, svg, react) Document the option

Invariants to preserve

  • The default behavior does not change: dragging an empty area still pans unless the new option is set.
  • Non-selectable entities are never selected.
  • Clicks below clickThresholdPx still behave as clicks.
  • Pinch-to-zoom and touch input are unaffected.
  • No production dependencies.

Implementation checklist

  • hitTestBounds in core, with tests (intersect/contain, selectable: false, empty scene, a rectangle drawn in any direction)
  • marquee mode in the SVG handlers, with tests (jsdom; mock getBoundingClientRect and pointer capture)
  • Same in the React adapter, with tests
  • Use the marquee in at least one example
  • Docs updated
  • Changeset (minor for core, svg and react)
  • Run pnpm typecheck && pnpm test && pnpm lint
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions