carry
Picking a thing up and putting it somewhere, as a model with no surface in it: what is being carried, where it came from, which target the pointer is over, whether that target will take it and, if not, WHY. Drag and drop, and its two-tap twin, on every surface (a page, a panel in a flat scene, a panel in a headset, a socket on a model in the world) is this model with a view on top.
Why it is its own thing: a drag that crosses from one panel to another cannot belong to either. Each panel only sees its own pointer events (a page listener, or the ray that hits its plane), so they report to one carry, and the carry decides.
import { createCarry } from 'tosijs-3d'
const carry = createCarry()
const holster = {
id: 'holster',
accepts: (thing) => (thing.kind === 'pistol' ? [] : ['only a pistol fits']),
receive: (thing, from) => console.log('holstered', thing, 'from', from),
}
carry.pickUp({ kind: 'pistol' }, 'hand')
carry.over(holster) // carry.verdict → { ok: true, reasons: [] }
carry.drop() // holstered { kind: 'pistol' } from hand
Two ways, one model
- Drag:
pickUpwhen the press has moved far enough to be a drag (DRAG_THRESHOLDin the surface's units decides that, in the view),overas the pointer moves,dropon release (on nothing: the carry is cancelled and the thing stays where it was). - Two taps:
pickUpon the first tap,dropon the tap on a target. Tapping the thing again, or anything that is not a target, puts it down. This is the path for a gamepad and the kinder one in a headset, where holding a trigger while you aim at arm's length is tiring.
Targets say why
A target's accepts(thing, from) returns REASONS, an empty list meaning yes.
A view lights the targets that would take what is held and dims the rest,
and shows the reason on the one the pointer is over: "two-handed: both hands
must be free" is an answer, a red outline is not.
drop is all or nothing: it asks the target again (the world may have
changed since the pointer arrived) and only then calls receive. If
receive throws or returns reasons, the thing stays where it was.
Watching it
carry.subscribe(fn) calls fn with the state after every change: what is
held, the target under the pointer and its verdict. Every view of every
surface draws from that one state, so they cannot disagree.