Repository navigation
Rubber band selection #42
Description
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:
pointerdownon an empty area, with the configured trigger →pending, as today.- Past
clickThresholdPx→marquee. Draw a rectangle inoverlaysG(screen-constant stroke, like the resize handles). pointermove→ update the rectangle. Optionally preview the selection live.pointerup→core.setSelection(ids, mode). Shift =add, Ctrl/⌘ =toggle, otherwisereplace, the same asmodifierSelectfor 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), withmarqueestyling options next to it. - Hit mode default.
intersect(touching is enough, as in Figma) orcontain(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-
selectableentities are never selected. - Clicks below
clickThresholdPxstill behave as clicks. - Pinch-to-zoom and touch input are unaffected.
- No production dependencies.
Implementation checklist
-
hitTestBoundsin core, with tests (intersect/contain,selectable: false, empty scene, a rectangle drawn in any direction) -
marqueemode in the SVG handlers, with tests (jsdom; mockgetBoundingClientRectand pointer capture) - Same in the React adapter, with tests
- Use the marquee in at least one example
- Docs updated
- Changeset (
minorfor core, svg and react) - Run
pnpm typecheck && pnpm test && pnpm lint
Activity