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

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.