@realitycollective/webxr-interactions 0.1.1-preview.0 → 0.1.1-preview.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -10,6 +10,10 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
10
10
 
11
11
  - `@realitycollective/webxr-interactions` - shared conformance suites for `HitTester` and `TransformPort`, `hitTesterContractCases()` and `transformPortContractCases()`, alongside the existing `inputProviderContractCases()` from `@realitycollective/webxr-input`. Runner-free data, the same pattern as WebXR-UIExtensions' `windowHostContractCases()`: each case throws a plain `Error` naming what a platform's ray/proximity query or transform read/write broke, and asserts only what `ports.ts` documents - a ray or proximity query answers null or an id with a positive distance and a point near the query, the nearer of two targets wins, an offset round-trips, `getWorldPose()` is finite with a unit quaternion, `getRestWorldPose()` is the registration pose and stays put, `getWorldPose()` follows `setLocalOffset`, `setLocalRotation` and `setWorldPose` from that rest frame, every returned tuple is a fresh value the caller owns and no tuple passed in is kept, and the optional `setEffect` accepts its shape without throwing. Each platform's subject starts away from the origin and turned, so a port that ignores its rest frame fails. Run against all five platforms in `packages/iwsdk-interactions/test/port-parity.test.ts`, next to the provider parity suite; every platform passes every case.
12
12
  - `@realitycollective/native-interactions` - a native host adapter, the fifth platform. It reads the `input` and `interactions` slices a native app (OpenXR on Quest, CompositorServices on visionOS) installs on `globalThis.__rcHost`, or hands in directly, and maps them onto `InputProvider`, `HitTester` and `TransformPort` with no engine dependency at all - assets and rendering stay in the native app, and only tuples and target ids cross the boundary. Every snapshot, hit and pose read across that boundary is copied, so a host that reuses its own buffers cannot reach an object the app is still holding, and optional members (`getHeadPose`, `sampleHints`, `pulse`, `setPresenceVisible`, `setPresenceModality`, `setWorldPose`, `setEffect`) are present only when the host slice itself carries them. `createNativeInteractions()` is the one-call setup, with the same shape as the other adapters' setups; `attachToHost` drives updates from the app's frame callback. The public surface matches the other adapters, and slice reading and tuple copying stay internal. Covered headlessly by an in-memory fake of both slices, including the shared `InputProvider` contract suite.
13
+ - `@realitycollective/webxr-interactions` - "held pose" for a grabbed object with physics: `TransformPort` gains two optional members, `beginHold()` and `endHold(release: HoldRelease)`, and the `HoldRelease` type they take (`linearVelocity`/`angularVelocity`, metres and radians per second, world space). A host whose object has physics uses them to give a `poseOnly` grab the same three behaviours native grab fulfilment already had: held (physics suspended, the object follows `setWorldPose` exactly, gravity and collisions do nothing), released (physics resumes with the grabbing hand's velocity, so a throw carries through), and reset (a `setWorldPose` while not held teleports and clears velocity - "back to the tee"). `setWorldPose`'s own doc states this split. A platform with no physics grows neither member and behaves exactly as before; a platform that fulfils grabs natively is expected to show the same three behaviours through its own engine, and the runtime never calls `beginHold`/`endHold` for one. `InteractorInfo` gains optional `linearVelocity`/`angularVelocity`, the grabbing source's tracked velocity `GrabBehaviour` reads for the release. `transformPortContractCases()` gains three cases - Held, Released, Reset - that run only when the contract subject carries an optional `physics` driver, and skip otherwise.
14
+ - `@realitycollective/native-interactions` - the `interactions` slice gains the matching optional `beginHold(targetId)`/`endHold(targetId, release)`, keyed by target id like every other member. `NativeTransformPort` grows the two only when the host slice carries them, the same rule as `setWorldPose`/`setEffect`. The README states the rule for a native app that fulfils grabs itself: it is responsible for showing held/released/reset through its own engine, and `beginHold`/`endHold` are never called for a grab that app already owns.
15
+ - `@realitycollective/babylon-interactions` - `BabylonTransformPort` implements the held pose over Physics V2's `PhysicsBody`, through a new `physicsMotionTypes` construction option (Babylon's own `PhysicsMotionType.ANIMATED`/`.DYNAMIC` values - this package holds no Babylon values, only shapes). A held node switches to `ANIMATED`; `endHold` switches it back to `DYNAMIC` with the release velocity. A node with no `physicsBody`, or a construction with no `physicsMotionTypes`, grows neither member. Written from the Babylon Physics V2 documentation - this package has no `@babylonjs/core`/`@babylonjs/havok` dependency to verify it against - and not yet exercised against a live scene.
16
+ - `@realitycollective/iwsdk-interactions` - `IWSDKTransformPort` takes an optional `physics: IWSDKPhysicsBinding` (`beginHold`/`endHold`/`teleport`); `IWSDKInteractions.register()` builds one from `@iwsdk/core`'s `PhysicsBody`/`PhysicsShape`/`PhysicsManipulation` components and `PhysicsSystem.setBodyTransform` whenever the entity has a physics body. A hold removes `PhysicsBody` (keeping `PhysicsShape`), which tears down the entity's Havok body so gravity, collisions and the physics-to-object3D sync all stop while the port's own `setWorldPose` writes take over; release re-adds `PhysicsBody` at the state it had and, for a nonzero velocity, a `PhysicsManipulation`. Never the `Grabbed` tag `GrabSystem` owns - its own doc says not to add or remove it by hand - and never the private Havok handle `PhysicsSystem` keeps to itself.
13
17
 
14
18
  ### Changed
15
19
 
@@ -21,6 +25,7 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
21
25
  - `@realitycollective/threejs-interactions`, `@realitycollective/iwsdk-interactions` and `@realitycollective/xrblocks-interactions` - `getWorldPose()` returned the pose captured at registration, not where the object was. Toss scoring therefore read a thrown ball at its registered position until the app called `recaptureRest()`, and a second grab of an object moved without a recapture took its hold offset from the old position. `getWorldPose()` now returns the live pose, as the Babylon.js port already did.
22
26
  - `@realitycollective/webxr-interactions` - `InteractableDescriptor.pokeRadius` had no effect. The value was stored on the registration and documented as the poke trigger radius, but the proximity query always used the 5 cm default, so an interactable asking for a wider or narrower poke got the default. The runtime now queries once per source at the largest radius any enabled interactable registered, and accepts the nearest hit only when it lies inside that interactable's own radius. One query per source is kept deliberately; the trade is that a farther interactable with a larger radius is not found behind a nearer one with a smaller radius, because the hit tester returns only the nearest. Covered by a runtime test that failed before the change.
23
27
  - CI - the staging deploy published under `--branch=pr-<number>` while the `-test` Pages project's production branch is `staging`, so only `pr-<n>.webxr-interactions-test.pages.dev` aliases were ever created and the project's root URL was a 404. It now deploys as `staging` on every pull request and on every push to `development`, so `webxr-interactions-test.pages.dev` serves the newest preview build, the same arrangement WebXR-UIExtensions already had.
28
+ - `@realitycollective/webxr-interactions` - a `poseOnly` grab on a host with physics fought its own physics solver. `GrabBehaviour` wrote `setWorldPose` every frame while held and told the port nothing at grab end; the solver kept simulating over the top of that write, physics never resumed on release, and a throw dropped instead of carrying velocity. `GrabBehaviour` now calls the port's `beginHold()` on grab start and `endHold(release)` on grab end, with the grabbing source's tracked velocity (zero on a synthesized release - source lost, target unregistered, runtime disposed, or the interactable disabled mid-hold - which carries none). Every way a grab can end now calls `endHold` exactly once, including `InteractionRuntime.dispose()`, which previously left a held grab's physics suspended forever with no `endHold` at all.
24
29
 
25
30
  ## [0.1.0] - 2026-09-17
26
31
 
@@ -25,6 +25,15 @@ export interface InteractorInfo {
25
25
  ray?: RayTuple;
26
26
  gripPose?: PoseTuple;
27
27
  indexTip?: Vec3Tuple;
28
+ /**
29
+ * Grip velocity, present on the same terms as `InputSourceSnapshot`'s -
30
+ * the runtime's `VelocityTracker` fills it when the provider does not.
31
+ * `onGrabEnd`'s release throw reads these; absent on a synthesized
32
+ * release (source lost, target unregistered, runtime disposed), which
33
+ * behaves as zero.
34
+ */
35
+ linearVelocity?: Vec3Tuple;
36
+ angularVelocity?: Vec3Tuple;
28
37
  select: number;
29
38
  squeeze: number;
30
39
  }
@@ -1 +1 @@
1
- {"version":3,"file":"behaviour.js","sourceRoot":"","sources":["../../src/behaviours/behaviour.ts"],"names":[],"mappings":"AA2EA,0EAA0E;AAC1E,MAAM,UAAU,WAAW,CAAC,MAAsB;IAChD,OAAO,MAAM,CAAC,QAAQ,IAAI,MAAM,CAAC,QAAQ,EAAE,QAAQ,IAAI,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC;AAC5E,CAAC","sourcesContent":["/**\n * The behaviour contract - a mechanism attached to an interactable.\n *\n * Behaviours are pure state machines: they receive routed input moments\n * (press/grab transitions), tick from `update(dt)`, and write their output\n * through the interactable's {@link TransformPort}. They declare:\n *\n * - `ownership` - who owns the transform (the formalised pale-signal\n * driven / dragged / handDriven / native models), so a binder knows\n * without reading code;\n * - `requires` / `requiresAnyOf` - the input capabilities they need;\n * capability negotiation disables unsatisfiable behaviours VISIBLY\n * (a `behaviourDisabled` event with the unmet list), never silently.\n */\nimport type {\n InputCapabilityRequirement,\n InputSourceKind,\n PoseTuple,\n RayTuple,\n Vec3Tuple,\n} from \"@realitycollective/webxr-input\";\nimport type { InteractionEvent } from \"../events.js\";\nimport type { FeedbackIntent } from \"../feedback.js\";\nimport type { TransformPort } from \"../ports.js\";\n\n/** Who owns the interactable's transform while this behaviour is active. */\nexport type TransformOwnership = \"driven\" | \"dragged\" | \"handDriven\" | \"native\";\n\n/** The slice of a live interactor a behaviour may consume. */\nexport interface InteractorInfo {\n id: string;\n kind: InputSourceKind;\n ray?: RayTuple;\n gripPose?: PoseTuple;\n indexTip?: Vec3Tuple;\n select: number;\n squeeze: number;\n}\n\n/** Everything a behaviour can reach during a tick or a routed moment. */\nexport interface BehaviourContext {\n dt: number;\n interactableId: string;\n transform: TransformPort | undefined;\n emit(event: Omit<InteractionEvent, \"interactableId\">): void;\n feedback(intent: Omit<FeedbackIntent, \"interactableId\">): void;\n /** LIVE world pose of another registered interactable (toss scoring). */\n getWorldPose(interactableId: string): PoseTuple | undefined;\n}\n\nexport interface Behaviour {\n readonly kind: string;\n readonly ownership: TransformOwnership;\n /** ALL of these must be satisfied. */\n readonly requires: readonly InputCapabilityRequirement[];\n /** For each group, AT LEAST ONE must be satisfied. */\n readonly requiresAnyOf?: readonly (readonly InputCapabilityRequirement[])[];\n /** True when this behaviour makes the interactable a grab target. */\n readonly grabbable: boolean;\n\n onPressStart?(ctx: BehaviourContext, interactor: InteractorInfo): void;\n onPressEnd?(ctx: BehaviourContext, interactor: InteractorInfo): void;\n onGrabStart?(ctx: BehaviourContext, interactor: InteractorInfo): void;\n onGrabEnd?(ctx: BehaviourContext, interactor: InteractorInfo): void;\n\n /**\n * Per-frame tick. `holder` is set while a grab interactor holds this\n * interactable (grab-family behaviours consume it; others ignore it).\n */\n update(ctx: BehaviourContext, holder?: InteractorInfo): void;\n\n /** Current logical 0..1 value (state snapshots, HUDs). */\n getValue(): number;\n}\n\n/** Hand position a hand-driven behaviour should track, best available. */\nexport function holderPoint(holder: InteractorInfo): Vec3Tuple | undefined {\n return holder.indexTip ?? holder.gripPose?.position ?? holder.ray?.origin;\n}\n"]}
1
+ {"version":3,"file":"behaviour.js","sourceRoot":"","sources":["../../src/behaviours/behaviour.ts"],"names":[],"mappings":"AAoFA,0EAA0E;AAC1E,MAAM,UAAU,WAAW,CAAC,MAAsB;IAChD,OAAO,MAAM,CAAC,QAAQ,IAAI,MAAM,CAAC,QAAQ,EAAE,QAAQ,IAAI,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC;AAC5E,CAAC","sourcesContent":["/**\n * The behaviour contract - a mechanism attached to an interactable.\n *\n * Behaviours are pure state machines: they receive routed input moments\n * (press/grab transitions), tick from `update(dt)`, and write their output\n * through the interactable's {@link TransformPort}. They declare:\n *\n * - `ownership` - who owns the transform (the formalised pale-signal\n * driven / dragged / handDriven / native models), so a binder knows\n * without reading code;\n * - `requires` / `requiresAnyOf` - the input capabilities they need;\n * capability negotiation disables unsatisfiable behaviours VISIBLY\n * (a `behaviourDisabled` event with the unmet list), never silently.\n */\nimport type {\n InputCapabilityRequirement,\n InputSourceKind,\n PoseTuple,\n RayTuple,\n Vec3Tuple,\n} from \"@realitycollective/webxr-input\";\nimport type { InteractionEvent } from \"../events.js\";\nimport type { FeedbackIntent } from \"../feedback.js\";\nimport type { TransformPort } from \"../ports.js\";\n\n/** Who owns the interactable's transform while this behaviour is active. */\nexport type TransformOwnership = \"driven\" | \"dragged\" | \"handDriven\" | \"native\";\n\n/** The slice of a live interactor a behaviour may consume. */\nexport interface InteractorInfo {\n id: string;\n kind: InputSourceKind;\n ray?: RayTuple;\n gripPose?: PoseTuple;\n indexTip?: Vec3Tuple;\n /**\n * Grip velocity, present on the same terms as `InputSourceSnapshot`'s -\n * the runtime's `VelocityTracker` fills it when the provider does not.\n * `onGrabEnd`'s release throw reads these; absent on a synthesized\n * release (source lost, target unregistered, runtime disposed), which\n * behaves as zero.\n */\n linearVelocity?: Vec3Tuple;\n angularVelocity?: Vec3Tuple;\n select: number;\n squeeze: number;\n}\n\n/** Everything a behaviour can reach during a tick or a routed moment. */\nexport interface BehaviourContext {\n dt: number;\n interactableId: string;\n transform: TransformPort | undefined;\n emit(event: Omit<InteractionEvent, \"interactableId\">): void;\n feedback(intent: Omit<FeedbackIntent, \"interactableId\">): void;\n /** LIVE world pose of another registered interactable (toss scoring). */\n getWorldPose(interactableId: string): PoseTuple | undefined;\n}\n\nexport interface Behaviour {\n readonly kind: string;\n readonly ownership: TransformOwnership;\n /** ALL of these must be satisfied. */\n readonly requires: readonly InputCapabilityRequirement[];\n /** For each group, AT LEAST ONE must be satisfied. */\n readonly requiresAnyOf?: readonly (readonly InputCapabilityRequirement[])[];\n /** True when this behaviour makes the interactable a grab target. */\n readonly grabbable: boolean;\n\n onPressStart?(ctx: BehaviourContext, interactor: InteractorInfo): void;\n onPressEnd?(ctx: BehaviourContext, interactor: InteractorInfo): void;\n onGrabStart?(ctx: BehaviourContext, interactor: InteractorInfo): void;\n onGrabEnd?(ctx: BehaviourContext, interactor: InteractorInfo): void;\n\n /**\n * Per-frame tick. `holder` is set while a grab interactor holds this\n * interactable (grab-family behaviours consume it; others ignore it).\n */\n update(ctx: BehaviourContext, holder?: InteractorInfo): void;\n\n /** Current logical 0..1 value (state snapshots, HUDs). */\n getValue(): number;\n}\n\n/** Hand position a hand-driven behaviour should track, best available. */\nexport function holderPoint(holder: InteractorInfo): Vec3Tuple | undefined {\n return holder.indexTip ?? holder.gripPose?.position ?? holder.ray?.origin;\n}\n"]}
@@ -17,12 +17,15 @@ export class GrabBehaviour {
17
17
  }
18
18
  onGrabStart(ctx, interactor) {
19
19
  this.held = true;
20
- if (this.fulfilment === "poseOnly" && interactor.gripPose && ctx.transform) {
21
- const object = ctx.transform.getWorldPose();
22
- const grip = interactor.gripPose;
23
- const invGrip = quatConjugate(grip.quaternion);
24
- this.offsetPosition = vApplyQuat(vSub(object.position, grip.position), invGrip);
25
- this.offsetQuaternion = quatMultiply(invGrip, object.quaternion);
20
+ if (this.fulfilment === "poseOnly") {
21
+ ctx.transform?.beginHold?.();
22
+ if (interactor.gripPose && ctx.transform) {
23
+ const object = ctx.transform.getWorldPose();
24
+ const grip = interactor.gripPose;
25
+ const invGrip = quatConjugate(grip.quaternion);
26
+ this.offsetPosition = vApplyQuat(vSub(object.position, grip.position), invGrip);
27
+ this.offsetQuaternion = quatMultiply(invGrip, object.quaternion);
28
+ }
26
29
  }
27
30
  ctx.emit({ type: "grabStart", behaviourKind: this.kind, interactorId: interactor.id });
28
31
  ctx.feedback({
@@ -35,6 +38,16 @@ export class GrabBehaviour {
35
38
  }
36
39
  onGrabEnd(ctx, interactor) {
37
40
  this.held = false;
41
+ if (this.fulfilment === "poseOnly") {
42
+ // Zero on a synthesized release (source lost, target unregistered,
43
+ // runtime disposed) - see the file comment and `InteractorInfo`'s.
44
+ // The object then resumes at rest rather than flying off on a
45
+ // disconnect it had nothing to do with.
46
+ ctx.transform?.endHold?.({
47
+ linearVelocity: interactor.linearVelocity ?? [0, 0, 0],
48
+ angularVelocity: interactor.angularVelocity ?? [0, 0, 0],
49
+ });
50
+ }
38
51
  ctx.emit({ type: "grabEnd", behaviourKind: this.kind, interactorId: interactor.id });
39
52
  ctx.feedback({
40
53
  cue: "drop",
@@ -1 +1 @@
1
- {"version":3,"file":"grab.js","sourceRoot":"","sources":["../../src/behaviours/grab.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Grab - pick up and carry an object.\n *\n * Two fulfilments, chosen by capability negotiation at bind time:\n * - `\"poseOnly\"` - the behaviour owns the carry: on grab it captures the\n * offset between the holder's grip and the object, then follows the grip\n * each frame via `setWorldPose`. On release the object stays where\n * dropped - ballistics/physics are deliberately the CLIENT's concern\n * (physics is a property of the object in space, not of the interaction\n * layer).\n * - `\"native\"` - the engine owns carry/throw (e.g. IWSDK grabbables +\n * physics); the behaviour only mirrors transitions into core events.\n */\nimport type { PoseTuple } from \"@realitycollective/webxr-input\";\nimport { quatConjugate, quatMultiply, vAdd, vApplyQuat, vSub } from \"../math.js\";\nimport type { Behaviour, BehaviourContext, InteractorInfo } from \"./behaviour.js\";\n\nexport interface GrabConfig {\n kind: \"grab\";\n}\n\nexport type GrabFulfilment = \"poseOnly\" | \"native\";\n\nexport class GrabBehaviour implements Behaviour {\n readonly kind = \"grab\";\n readonly ownership: \"handDriven\" | \"native\";\n readonly requires = [\"grabs\"] as const;\n readonly grabbable = true;\n\n private fulfilment: GrabFulfilment;\n private held = false;\n private offsetPosition = [0, 0, 0] as [number, number, number];\n private offsetQuaternion = [0, 0, 0, 1] as [number, number, number, number];\n\n constructor(_config: Omit<GrabConfig, \"kind\"> = {}, fulfilment: GrabFulfilment = \"poseOnly\") {\n this.fulfilment = fulfilment;\n this.ownership = fulfilment === \"native\" ? \"native\" : \"handDriven\";\n }\n\n setFulfilment(fulfilment: GrabFulfilment): void {\n this.fulfilment = fulfilment;\n }\n\n onGrabStart(ctx: BehaviourContext, interactor: InteractorInfo): void {\n this.held = true;\n if (this.fulfilment === \"poseOnly\" && interactor.gripPose && ctx.transform) {\n const object = ctx.transform.getWorldPose();\n const grip = interactor.gripPose;\n const invGrip = quatConjugate(grip.quaternion);\n this.offsetPosition = vApplyQuat(vSub(object.position, grip.position), invGrip);\n this.offsetQuaternion = quatMultiply(invGrip, object.quaternion);\n }\n ctx.emit({ type: \"grabStart\", behaviourKind: this.kind, interactorId: interactor.id });\n ctx.feedback({\n cue: \"grab\",\n behaviourKind: this.kind,\n sourceId: interactor.id,\n intensity: 0.5,\n durationMs: 30,\n });\n }\n\n onGrabEnd(ctx: BehaviourContext, interactor: InteractorInfo): void {\n this.held = false;\n ctx.emit({ type: \"grabEnd\", behaviourKind: this.kind, interactorId: interactor.id });\n ctx.feedback({\n cue: \"drop\",\n behaviourKind: this.kind,\n sourceId: interactor.id,\n intensity: 0.25,\n durationMs: 20,\n });\n }\n\n update(ctx: BehaviourContext, holder?: InteractorInfo): void {\n if (this.fulfilment !== \"poseOnly\" || !this.held || !holder) return;\n const grip = holder.gripPose;\n const transform = ctx.transform;\n if (!grip || !transform?.setWorldPose) return;\n const pose: PoseTuple = {\n position: vAdd(grip.position, vApplyQuat(this.offsetPosition, grip.quaternion)),\n quaternion: quatMultiply(grip.quaternion, this.offsetQuaternion),\n };\n transform.setWorldPose(pose);\n }\n\n getValue(): number {\n return this.held ? 1 : 0;\n }\n}\n"]}
1
+ {"version":3,"file":"grab.js","sourceRoot":"","sources":["../../src/behaviours/grab.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Grab - pick up and carry an object.\n *\n * Two fulfilments, chosen by capability negotiation at bind time:\n * - `\"poseOnly\"` - the behaviour owns the carry: on grab it captures the\n * offset between the holder's grip and the object, then follows the grip\n * each frame via `setWorldPose`. On release the object stays where\n * dropped - ballistics/physics are deliberately the CLIENT's concern\n * (physics is a property of the object in space, not of the interaction\n * layer).\n * - `\"native\"` - the engine owns carry/throw (e.g. IWSDK grabbables +\n * physics); the behaviour only mirrors transitions into core events.\n *\n * On a `poseOnly` host whose object has physics, \"the behaviour owns the\n * carry\" also means the behaviour owns suspending and resuming it:\n * `onGrabStart` calls `ctx.transform.beginHold()` so the follow above does\n * not fight the solver, and `onGrabEnd` calls `endHold()` with the\n * grabbing source's release velocity so a throw carries through. Both are\n * optional on `TransformPort` and simply do not exist on a host with no\n * physics, so this costs a host without one nothing.\n */\nimport type { PoseTuple } from \"@realitycollective/webxr-input\";\nimport { quatConjugate, quatMultiply, vAdd, vApplyQuat, vSub } from \"../math.js\";\nimport type { Behaviour, BehaviourContext, InteractorInfo } from \"./behaviour.js\";\n\nexport interface GrabConfig {\n kind: \"grab\";\n}\n\nexport type GrabFulfilment = \"poseOnly\" | \"native\";\n\nexport class GrabBehaviour implements Behaviour {\n readonly kind = \"grab\";\n readonly ownership: \"handDriven\" | \"native\";\n readonly requires = [\"grabs\"] as const;\n readonly grabbable = true;\n\n private fulfilment: GrabFulfilment;\n private held = false;\n private offsetPosition = [0, 0, 0] as [number, number, number];\n private offsetQuaternion = [0, 0, 0, 1] as [number, number, number, number];\n\n constructor(_config: Omit<GrabConfig, \"kind\"> = {}, fulfilment: GrabFulfilment = \"poseOnly\") {\n this.fulfilment = fulfilment;\n this.ownership = fulfilment === \"native\" ? \"native\" : \"handDriven\";\n }\n\n setFulfilment(fulfilment: GrabFulfilment): void {\n this.fulfilment = fulfilment;\n }\n\n onGrabStart(ctx: BehaviourContext, interactor: InteractorInfo): void {\n this.held = true;\n if (this.fulfilment === \"poseOnly\") {\n ctx.transform?.beginHold?.();\n if (interactor.gripPose && ctx.transform) {\n const object = ctx.transform.getWorldPose();\n const grip = interactor.gripPose;\n const invGrip = quatConjugate(grip.quaternion);\n this.offsetPosition = vApplyQuat(vSub(object.position, grip.position), invGrip);\n this.offsetQuaternion = quatMultiply(invGrip, object.quaternion);\n }\n }\n ctx.emit({ type: \"grabStart\", behaviourKind: this.kind, interactorId: interactor.id });\n ctx.feedback({\n cue: \"grab\",\n behaviourKind: this.kind,\n sourceId: interactor.id,\n intensity: 0.5,\n durationMs: 30,\n });\n }\n\n onGrabEnd(ctx: BehaviourContext, interactor: InteractorInfo): void {\n this.held = false;\n if (this.fulfilment === \"poseOnly\") {\n // Zero on a synthesized release (source lost, target unregistered,\n // runtime disposed) - see the file comment and `InteractorInfo`'s.\n // The object then resumes at rest rather than flying off on a\n // disconnect it had nothing to do with.\n ctx.transform?.endHold?.({\n linearVelocity: interactor.linearVelocity ?? [0, 0, 0],\n angularVelocity: interactor.angularVelocity ?? [0, 0, 0],\n });\n }\n ctx.emit({ type: \"grabEnd\", behaviourKind: this.kind, interactorId: interactor.id });\n ctx.feedback({\n cue: \"drop\",\n behaviourKind: this.kind,\n sourceId: interactor.id,\n intensity: 0.25,\n durationMs: 20,\n });\n }\n\n update(ctx: BehaviourContext, holder?: InteractorInfo): void {\n if (this.fulfilment !== \"poseOnly\" || !this.held || !holder) return;\n const grip = holder.gripPose;\n const transform = ctx.transform;\n if (!grip || !transform?.setWorldPose) return;\n const pose: PoseTuple = {\n position: vAdd(grip.position, vApplyQuat(this.offsetPosition, grip.quaternion)),\n quaternion: quatMultiply(grip.quaternion, this.offsetQuaternion),\n };\n transform.setWorldPose(pose);\n }\n\n getValue(): number {\n return this.held ? 1 : 0;\n }\n}\n"]}
@@ -51,6 +51,15 @@ export interface HitTesterContractCase {
51
51
  * ```
52
52
  */
53
53
  export declare function hitTesterContractCases(): readonly HitTesterContractCase[];
54
+ /**
55
+ * Advances a subject's physics simulation - a real engine step where a
56
+ * platform's physics can run in a test, otherwise a fake that faithfully
57
+ * reproduces the one behaviour these cases check: gravity moves the object
58
+ * unless held, and a release velocity carries it.
59
+ */
60
+ export interface TransformPortPhysicsDriver {
61
+ step(dtSeconds: number): void;
62
+ }
54
63
  /** The port under test, over one object. Build a FRESH one per case. */
55
64
  export interface TransformPortContractSubject {
56
65
  port: TransformPort;
@@ -60,6 +69,12 @@ export interface TransformPortContractSubject {
60
69
  * so a port that ignores its rest frame cannot pass by accident.
61
70
  */
62
71
  rest: PoseTuple;
72
+ /**
73
+ * Present only when the object has a physics body - the same condition
74
+ * that gives the port `beginHold`/`endHold`. The held/released/reset
75
+ * cases below skip without one, since there is nothing for them to check.
76
+ */
77
+ physics?: TransformPortPhysicsDriver;
63
78
  }
64
79
  /** One check a {@link TransformPort} implementation must pass. */
65
80
  export interface TransformPortContractCase {
@@ -107,6 +107,30 @@ const POSE_EPS = 1e-3;
107
107
  // A quarter turn about Y - a non-identity unit quaternion, so the case
108
108
  // cannot pass by accident on a port that ignores its argument.
109
109
  const QUARTER_TURN_Y = [0, Math.SQRT1_2, 0, Math.SQRT1_2];
110
+ // ---------------------------------------------------------------------------
111
+ // Held / released / reset - the "held pose" rule, checked only when the
112
+ // subject carries a physics driver. One fixed step size and a looser
113
+ // position tolerance than POSE_EPS: real engines differ slightly in their
114
+ // integrator, and a held/reset case allows for up to one step of gravity
115
+ // (see PHYSICS_POSE_EPS's own comment), where the plain pose cases above
116
+ // never touch physics at all.
117
+ // ---------------------------------------------------------------------------
118
+ const PHYSICS_DT = 1 / 60;
119
+ const HELD_STEPS = 5;
120
+ /** metres/second - small enough that one step's drift is easy to read, large enough not to vanish into PHYSICS_POSE_EPS. */
121
+ const RELEASE_SPEED = 2;
122
+ /** metres/second - about 25x RELEASE_SPEED, so "kept the old velocity" and "one step of gravity" cannot be confused. */
123
+ const LARGE_PRIOR_SPEED = 50;
124
+ /**
125
+ * 2 cm. One step of gravity at PHYSICS_DT droops roughly
126
+ * `0.5 * 9.8 * PHYSICS_DT^2` ≈ 1.4 mm, well inside this; a full step of
127
+ * LARGE_PRIOR_SPEED (≈ 0.83 m) or RELEASE_SPEED (≈ 33 mm) is well outside it.
128
+ */
129
+ const PHYSICS_POSE_EPS = 0.02;
130
+ function assertPhysicsPoseClose(value, expected, label) {
131
+ assertVec3Close(value.position, expected.position, PHYSICS_POSE_EPS, `${label}.position`);
132
+ assertQuatClose(value.quaternion, expected.quaternion, `${label}.quaternion`);
133
+ }
110
134
  const TRANSFORM_PORT_CASES = [
111
135
  {
112
136
  name: "setLocalOffset then getLocalOffset returns the same offset",
@@ -222,6 +246,65 @@ const TRANSFORM_PORT_CASES = [
222
246
  }
223
247
  },
224
248
  },
249
+ {
250
+ name: "Held: beginHold holds a fixed setWorldPose without falling, when the subject has physics",
251
+ run({ port, rest, physics }) {
252
+ if (!physics || !port.setWorldPose || !port.beginHold)
253
+ return;
254
+ const held = {
255
+ position: [rest.position[0], rest.position[1] + 1, rest.position[2]],
256
+ quaternion: rest.quaternion,
257
+ };
258
+ port.beginHold();
259
+ for (let i = 0; i < HELD_STEPS; i++) {
260
+ port.setWorldPose(held);
261
+ physics.step(PHYSICS_DT);
262
+ }
263
+ // A tight tolerance, not PHYSICS_POSE_EPS: a genuinely held body sees
264
+ // no simulation at all, so it matches the written pose exactly bar
265
+ // floating point noise - the same POSE_EPS the non-physics cases
266
+ // above use. One step of unwanted gravity (~2.7 mm at PHYSICS_DT) is
267
+ // what this case exists to catch, and it must not hide inside the
268
+ // tolerance the way it deliberately may in the Reset case below.
269
+ assertPoseClose(port.getWorldPose(), held, `getWorldPose() after ${HELD_STEPS} held steps`);
270
+ },
271
+ },
272
+ {
273
+ name: "Released: endHold sends the body away with the release velocity, when the subject has physics",
274
+ run({ port, rest, physics }) {
275
+ if (!physics || !port.beginHold || !port.endHold)
276
+ return;
277
+ port.beginHold();
278
+ const release = { linearVelocity: [RELEASE_SPEED, 0, 0], angularVelocity: [0, 0, 0] };
279
+ port.endHold(release);
280
+ physics.step(PHYSICS_DT);
281
+ const moved = port.getWorldPose().position[0] - rest.position[0];
282
+ assert(moved > RELEASE_SPEED * PHYSICS_DT * 0.5, `endHold({ linearVelocity: [${RELEASE_SPEED}, 0, 0] }) must move the body that way within one step, moved ${moved}`);
283
+ },
284
+ },
285
+ {
286
+ name: "Reset: setWorldPose while not held teleports to the pose and clears velocity, when the subject has physics",
287
+ run({ port, rest, physics }) {
288
+ if (!physics || !port.setWorldPose || !port.beginHold || !port.endHold)
289
+ return;
290
+ // Give the body a large prior velocity and let it run for a step, so
291
+ // carrying that velocity through the reset below is unmistakable -
292
+ // one step of it (~0.83 m) dwarfs PHYSICS_POSE_EPS (2 cm).
293
+ port.beginHold();
294
+ const priorRelease = { linearVelocity: [LARGE_PRIOR_SPEED, 0, 0], angularVelocity: [0, 0, 0] };
295
+ port.endHold(priorRelease);
296
+ physics.step(PHYSICS_DT);
297
+ const target = {
298
+ position: [rest.position[0] - 1, rest.position[1] + 2, rest.position[2] + 0.5],
299
+ quaternion: rest.quaternion,
300
+ };
301
+ port.setWorldPose(target);
302
+ // One further step: it must be at `target`, not carrying the prior
303
+ // velocity forward - allowing for this one step's worth of gravity.
304
+ physics.step(PHYSICS_DT);
305
+ assertPhysicsPoseClose(port.getWorldPose(), target, "getWorldPose() one step after a reset setWorldPose");
306
+ },
307
+ },
225
308
  ];
226
309
  // ---------------------------------------------------------------------------
227
310
  // Shared assertion helpers
@@ -1 +1 @@
1
- {"version":3,"file":"contract-cases.js","sourceRoot":"","sources":["../src/contract-cases.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Shared `HitTester` and `TransformPort` conformance suites, shipped as data\n * rather than as tests - the same idea as `windowHostContractCases()` in\n * WebXR-UIExtensions and `inputProviderContractCases()` in\n * `@realitycollective/webxr-input`. Five adapters implement these two ports\n * against five different scene graphs; running the same checks against every\n * one is what stops an adapter quietly dropping a documented rule.\n *\n * The suites are runner-free on purpose. Every adapter repository already has\n * its own test runner, and an adapter written outside this repository cannot\n * reach into this one's `test/` folder, so the checks ship as plain objects\n * that throw an `Error` on failure and the adapter iterates them.\n *\n * Both suites assert only what `ports.ts` documents, including two rules\n * every platform now shares:\n * - `getWorldPose()` is the LIVE pose and `getRestWorldPose()` the rest pose\n * offsets are measured from.\n * - Every tuple a port returns is fresh and owned by the caller, and a tuple\n * passed in is not kept.\n */\nimport type { PoseTuple, QuatTuple, RayTuple, Vec3Tuple } from \"@realitycollective/webxr-input\";\nimport { quatMultiply, vAdd, vApplyQuat } from \"./math.js\";\nimport type { HitTester, TransformPort } from \"./ports.js\";\n\n// ---------------------------------------------------------------------------\n// HitTester\n// ---------------------------------------------------------------------------\n\n/**\n * Places (or replaces) one target for a case to query against. Build a FRESH\n * subject per case: cases place targets of their own and do not clean up.\n */\nexport interface HitTesterContractDriver {\n /**\n * Register a target at `position` with an interaction radius of `radius`,\n * however the platform registers a target - a mesh, a node, an entity.\n */\n place(id: string, position: Vec3Tuple, radius: number): void;\n}\n\n/** The tester under test, plus the means of putting targets into its scene. */\nexport interface HitTesterContractSubject {\n hitTester: HitTester;\n driver: HitTesterContractDriver;\n}\n\n/** One check a {@link HitTester} implementation must pass. */\nexport interface HitTesterContractCase {\n name: string;\n run(subject: HitTesterContractSubject): void;\n}\n\n/**\n * The shared `HitTester` conformance suite. An adapter's test file is a loop:\n *\n * ```ts\n * for (const contractCase of hitTesterContractCases()) {\n * it(contractCase.name, () => contractCase.run(makeSubject()));\n * }\n * ```\n */\nexport function hitTesterContractCases(): readonly HitTesterContractCase[] {\n return HIT_TESTER_CASES;\n}\n\nconst DOWN_RAY: RayTuple = { origin: [0, 0, 0], direction: [0, 0, -1] };\nconst AWAY_RAY: RayTuple = { origin: [0, 0, 0], direction: [0, 0, 1] };\n\n/**\n * A ray-based query only has to land WITHIN the target's own radius:\n * `ports.ts` documents `point` as \"world-space hit or closest point\", so a\n * mesh-accurate ray tester (three.js) reporting the surface it actually hit\n * is as conforming as a sphere tester (Babylon, IWSDK) reporting the target's\n * centre. Placing the target exactly on the ray's line makes both answers\n * the same point when a tester has no radius-shaped slack, and this is the\n * tolerance for testers that do.\n */\nfunction slack(radius: number): number {\n return radius + 0.05;\n}\n\nconst HIT_TESTER_CASES: readonly HitTesterContractCase[] = [\n {\n name: \"an empty scene answers a ray with null\",\n run({ hitTester }) {\n assert(hitTester.hitRay(DOWN_RAY) === null, \"hitRay over an empty scene must return null\");\n },\n },\n {\n name: \"a ray through a placed target returns its id, a positive distance and a point near the ray\",\n run({ hitTester, driver }) {\n const radius = 0.15;\n driver.place(\"target\", [0, 0, -2], radius);\n const hit = hitTester.hitRay(DOWN_RAY);\n assert(hit !== null, \"a ray through a placed target must not miss\");\n assert(hit.interactableId === \"target\", `expected interactableId \"target\", got \"${hit.interactableId}\"`);\n assert(hit.distance > 0, `distance must be positive, got ${hit.distance}`);\n const eps = slack(radius);\n assert(closeTo(hit.distance, 2, eps), `distance must be close to 2, got ${hit.distance}`);\n assertVec3Close(hit.point, [0, 0, -2], eps, \"point\");\n },\n },\n {\n name: \"a ray pointing away from the target returns null\",\n run({ hitTester, driver }) {\n driver.place(\"target\", [0, 0, -2], 0.15);\n assert(hitTester.hitRay(AWAY_RAY) === null, \"a ray pointing away from every target must return null\");\n },\n },\n {\n name: \"hitProximity finds a target inside its radius and misses outside it\",\n run({ hitTester, driver }) {\n driver.place(\"target\", [0, 0, -2], 0.1);\n const inside = hitTester.hitProximity([0, 0, -2], 0.5);\n assert(inside !== null, \"hitProximity at the target's own position must find it\");\n assert(\n inside.interactableId === \"target\",\n `expected interactableId \"target\", got \"${inside.interactableId}\"`,\n );\n const outside = hitTester.hitProximity([5, 5, 5], 0.05);\n assert(outside === null, \"hitProximity far from every target must return null\");\n },\n },\n {\n name: \"with two targets on one ray, the nearer one wins\",\n run({ hitTester, driver }) {\n const radius = 0.1;\n driver.place(\"far\", [0, 0, -3], radius);\n driver.place(\"near\", [0, 0, -1], radius);\n const hit = hitTester.hitRay(DOWN_RAY);\n assert(hit !== null, \"a ray through two targets must not miss\");\n assert(hit.interactableId === \"near\", `the nearer target must win, got \"${hit.interactableId}\"`);\n assert(closeTo(hit.distance, 1, slack(radius)), `distance must be close to 1, got ${hit.distance}`);\n },\n },\n {\n name: \"each hit and its point are fresh values the caller owns\",\n run({ hitTester, driver }) {\n driver.place(\"target\", [0, 0, -2], 0.15);\n for (const [label, query] of [\n [\"hitRay\", () => hitTester.hitRay(DOWN_RAY)],\n [\"hitProximity\", () => hitTester.hitProximity([0, 0, -2], 0.5)],\n ] as const) {\n const first = query();\n const second = query();\n assert(first !== null && second !== null, `${label} must find the placed target`);\n assert(second !== first && second.point !== first.point, `${label} must return a new hit and point each call`);\n const expected = first.point[2];\n first.point[2] = 1e6;\n const third = query();\n assert(\n third !== null && closeTo(third.point[2], expected, POSE_EPS),\n `writing to a returned point must not change the next ${label} answer`,\n );\n }\n },\n },\n];\n\n// ---------------------------------------------------------------------------\n// TransformPort\n// ---------------------------------------------------------------------------\n\n/** The port under test, over one object. Build a FRESH one per case. */\nexport interface TransformPortContractSubject {\n port: TransformPort;\n /**\n * The world pose the object sat at when the port captured its rest. Place\n * it away from the origin and turned, under no parent or an unscaled one,\n * so a port that ignores its rest frame cannot pass by accident.\n */\n rest: PoseTuple;\n}\n\n/** One check a {@link TransformPort} implementation must pass. */\nexport interface TransformPortContractCase {\n name: string;\n run(subject: TransformPortContractSubject): void;\n}\n\n/**\n * The shared `TransformPort` conformance suite. An adapter's test file is a\n * loop, the same shape as {@link hitTesterContractCases}.\n */\nexport function transformPortContractCases(): readonly TransformPortContractCase[] {\n return TRANSFORM_PORT_CASES;\n}\n\nconst POSE_EPS = 1e-3;\n// A quarter turn about Y - a non-identity unit quaternion, so the case\n// cannot pass by accident on a port that ignores its argument.\nconst QUARTER_TURN_Y: QuatTuple = [0, Math.SQRT1_2, 0, Math.SQRT1_2];\n\nconst TRANSFORM_PORT_CASES: readonly TransformPortContractCase[] = [\n {\n name: \"setLocalOffset then getLocalOffset returns the same offset\",\n run({ port }) {\n const offset: Vec3Tuple = [0.3, -0.1, 0.05];\n port.setLocalOffset(offset);\n assertVec3Close(port.getLocalOffset(), offset, POSE_EPS, \"getLocalOffset()\");\n },\n },\n {\n name: \"getWorldPose returns finite numbers and a unit quaternion\",\n run({ port }) {\n const pose = port.getWorldPose();\n for (const value of [...pose.position, ...pose.quaternion]) {\n assert(Number.isFinite(value), `getWorldPose() must return finite numbers, got ${value}`);\n }\n const length = quatLength(pose.quaternion);\n assert(\n closeTo(length, 1, POSE_EPS),\n `getWorldPose().quaternion must be a unit quaternion, length was ${length}`,\n );\n },\n },\n {\n name: \"setLocalRotation with a unit quaternion does not throw\",\n run({ port }) {\n try {\n port.setLocalRotation(QUARTER_TURN_Y);\n } catch (error) {\n throw new Error(`setLocalRotation(unit quaternion) must not throw, it threw: ${String(error)}`);\n }\n },\n },\n {\n name: \"getRestWorldPose returns the pose captured at registration\",\n run({ port, rest }) {\n assertPoseClose(port.getRestWorldPose(), rest, \"getRestWorldPose()\");\n },\n },\n {\n name: \"before any write, the live pose is the rest pose\",\n run({ port, rest }) {\n assertPoseClose(port.getWorldPose(), rest, \"getWorldPose()\");\n },\n },\n {\n name: \"getWorldPose follows setLocalOffset while getRestWorldPose stays put\",\n run({ port, rest }) {\n const offset: Vec3Tuple = [0.3, -0.1, 0.05];\n port.setLocalOffset(offset);\n assertVec3Close(\n port.getWorldPose().position,\n vAdd(rest.position, vApplyQuat(offset, rest.quaternion)),\n POSE_EPS,\n \"getWorldPose().position after setLocalOffset\",\n );\n assertPoseClose(port.getRestWorldPose(), rest, \"getRestWorldPose() after setLocalOffset\");\n },\n },\n {\n name: \"getWorldPose follows setLocalRotation while getRestWorldPose stays put\",\n run({ port, rest }) {\n port.setLocalRotation(QUARTER_TURN_Y);\n assertQuatClose(\n port.getWorldPose().quaternion,\n quatMultiply(rest.quaternion, QUARTER_TURN_Y),\n \"getWorldPose().quaternion after setLocalRotation\",\n );\n assertPoseClose(port.getRestWorldPose(), rest, \"getRestWorldPose() after setLocalRotation\");\n },\n },\n {\n name: \"setWorldPose, when present, is what getWorldPose reads back\",\n run({ port, rest }) {\n if (!port.setWorldPose) return;\n const target: PoseTuple = { position: [0.5, 1.2, -0.4], quaternion: QUARTER_TURN_Y };\n port.setWorldPose(target);\n assertPoseClose(port.getWorldPose(), target, \"getWorldPose() after setWorldPose\");\n assertPoseClose(port.getRestWorldPose(), rest, \"getRestWorldPose() after setWorldPose\");\n },\n },\n {\n name: \"every returned tuple is a fresh value the caller owns\",\n run({ port }) {\n const pose = port.getWorldPose();\n const restPose = port.getRestWorldPose();\n const offset = port.getLocalOffset();\n pose.position[0] = 1e6;\n pose.quaternion[3] = 1e6;\n restPose.position[0] = 1e6;\n offset[0] = 1e6;\n assert(port.getWorldPose().position[0] !== 1e6, \"writing to a returned getWorldPose() tuple must not change the port\");\n assert(port.getWorldPose().quaternion[3] !== 1e6, \"writing to a returned getWorldPose() quaternion must not change the port\");\n assert(port.getRestWorldPose().position[0] !== 1e6, \"writing to a returned getRestWorldPose() tuple must not change the port\");\n assert(port.getLocalOffset()[0] !== 1e6, \"writing to a returned getLocalOffset() tuple must not change the port\");\n },\n },\n {\n name: \"a tuple passed in is not kept\",\n run({ port }) {\n const offset: Vec3Tuple = [0.3, -0.1, 0.05];\n port.setLocalOffset(offset);\n offset[0] = 9;\n assertVec3Close(port.getLocalOffset(), [0.3, -0.1, 0.05], POSE_EPS, \"getLocalOffset() after the caller reused its tuple\");\n if (!port.setWorldPose) return;\n const target: PoseTuple = { position: [0.5, 1.2, -0.4], quaternion: [0, 0, 0, 1] };\n port.setWorldPose(target);\n target.position[0] = 9;\n assertVec3Close(port.getWorldPose().position, [0.5, 1.2, -0.4], POSE_EPS, \"getWorldPose() after the caller reused its tuple\");\n },\n },\n {\n name: \"setEffect, when present, accepts scale and emissive without throwing\",\n run({ port }) {\n if (!port.setEffect) return;\n try {\n port.setEffect({ scale: 1.2, emissive: 0.3 });\n } catch (error) {\n throw new Error(`setEffect({ scale, emissive }) must not throw, it threw: ${String(error)}`);\n }\n },\n },\n];\n\n// ---------------------------------------------------------------------------\n// Shared assertion helpers\n// ---------------------------------------------------------------------------\n\nfunction assert(condition: boolean, message: string): asserts condition {\n if (!condition) throw new Error(message);\n}\n\nfunction closeTo(a: number, b: number, eps: number): boolean {\n return Math.abs(a - b) <= eps;\n}\n\n// Indexed one component at a time, rather than in a loop, so each index is a\n// literal: `noUncheckedIndexedAccess` cannot narrow a tuple read through a\n// variable index back to `number`.\nfunction assertVec3Close(value: Vec3Tuple, expected: Vec3Tuple, eps: number, label: string): void {\n assert(closeTo(value[0], expected[0], eps), `${label}[0] must be close to ${expected[0]}, got ${value[0]}`);\n assert(closeTo(value[1], expected[1], eps), `${label}[1] must be close to ${expected[1]}, got ${value[1]}`);\n assert(closeTo(value[2], expected[2], eps), `${label}[2] must be close to ${expected[2]}, got ${value[2]}`);\n}\n\n// q and -q are the same rotation, so compare by the size of the dot product.\nfunction assertQuatClose(value: QuatTuple, expected: QuatTuple, label: string): void {\n const dot = value[0] * expected[0] + value[1] * expected[1] + value[2] * expected[2] + value[3] * expected[3];\n assert(\n closeTo(Math.abs(dot), 1, POSE_EPS),\n `${label} must be close to [${expected.join(\", \")}], got [${value.join(\", \")}]`,\n );\n}\n\nfunction assertPoseClose(value: PoseTuple, expected: PoseTuple, label: string): void {\n assertVec3Close(value.position, expected.position, POSE_EPS, `${label}.position`);\n assertQuatClose(value.quaternion, expected.quaternion, `${label}.quaternion`);\n}\n\nfunction quatLength(q: QuatTuple): number {\n return Math.sqrt(q[0] * q[0] + q[1] * q[1] + q[2] * q[2] + q[3] * q[3]);\n}\n"]}
1
+ {"version":3,"file":"contract-cases.js","sourceRoot":"","sources":["../src/contract-cases.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Shared `HitTester` and `TransformPort` conformance suites, shipped as data\n * rather than as tests - the same idea as `windowHostContractCases()` in\n * WebXR-UIExtensions and `inputProviderContractCases()` in\n * `@realitycollective/webxr-input`. Five adapters implement these two ports\n * against five different scene graphs; running the same checks against every\n * one is what stops an adapter quietly dropping a documented rule.\n *\n * The suites are runner-free on purpose. Every adapter repository already has\n * its own test runner, and an adapter written outside this repository cannot\n * reach into this one's `test/` folder, so the checks ship as plain objects\n * that throw an `Error` on failure and the adapter iterates them.\n *\n * Both suites assert only what `ports.ts` documents, including two rules\n * every platform now shares:\n * - `getWorldPose()` is the LIVE pose and `getRestWorldPose()` the rest pose\n * offsets are measured from.\n * - Every tuple a port returns is fresh and owned by the caller, and a tuple\n * passed in is not kept.\n */\nimport type { PoseTuple, QuatTuple, RayTuple, Vec3Tuple } from \"@realitycollective/webxr-input\";\nimport { quatMultiply, vAdd, vApplyQuat } from \"./math.js\";\nimport type { HitTester, HoldRelease, TransformPort } from \"./ports.js\";\n\n// ---------------------------------------------------------------------------\n// HitTester\n// ---------------------------------------------------------------------------\n\n/**\n * Places (or replaces) one target for a case to query against. Build a FRESH\n * subject per case: cases place targets of their own and do not clean up.\n */\nexport interface HitTesterContractDriver {\n /**\n * Register a target at `position` with an interaction radius of `radius`,\n * however the platform registers a target - a mesh, a node, an entity.\n */\n place(id: string, position: Vec3Tuple, radius: number): void;\n}\n\n/** The tester under test, plus the means of putting targets into its scene. */\nexport interface HitTesterContractSubject {\n hitTester: HitTester;\n driver: HitTesterContractDriver;\n}\n\n/** One check a {@link HitTester} implementation must pass. */\nexport interface HitTesterContractCase {\n name: string;\n run(subject: HitTesterContractSubject): void;\n}\n\n/**\n * The shared `HitTester` conformance suite. An adapter's test file is a loop:\n *\n * ```ts\n * for (const contractCase of hitTesterContractCases()) {\n * it(contractCase.name, () => contractCase.run(makeSubject()));\n * }\n * ```\n */\nexport function hitTesterContractCases(): readonly HitTesterContractCase[] {\n return HIT_TESTER_CASES;\n}\n\nconst DOWN_RAY: RayTuple = { origin: [0, 0, 0], direction: [0, 0, -1] };\nconst AWAY_RAY: RayTuple = { origin: [0, 0, 0], direction: [0, 0, 1] };\n\n/**\n * A ray-based query only has to land WITHIN the target's own radius:\n * `ports.ts` documents `point` as \"world-space hit or closest point\", so a\n * mesh-accurate ray tester (three.js) reporting the surface it actually hit\n * is as conforming as a sphere tester (Babylon, IWSDK) reporting the target's\n * centre. Placing the target exactly on the ray's line makes both answers\n * the same point when a tester has no radius-shaped slack, and this is the\n * tolerance for testers that do.\n */\nfunction slack(radius: number): number {\n return radius + 0.05;\n}\n\nconst HIT_TESTER_CASES: readonly HitTesterContractCase[] = [\n {\n name: \"an empty scene answers a ray with null\",\n run({ hitTester }) {\n assert(hitTester.hitRay(DOWN_RAY) === null, \"hitRay over an empty scene must return null\");\n },\n },\n {\n name: \"a ray through a placed target returns its id, a positive distance and a point near the ray\",\n run({ hitTester, driver }) {\n const radius = 0.15;\n driver.place(\"target\", [0, 0, -2], radius);\n const hit = hitTester.hitRay(DOWN_RAY);\n assert(hit !== null, \"a ray through a placed target must not miss\");\n assert(hit.interactableId === \"target\", `expected interactableId \"target\", got \"${hit.interactableId}\"`);\n assert(hit.distance > 0, `distance must be positive, got ${hit.distance}`);\n const eps = slack(radius);\n assert(closeTo(hit.distance, 2, eps), `distance must be close to 2, got ${hit.distance}`);\n assertVec3Close(hit.point, [0, 0, -2], eps, \"point\");\n },\n },\n {\n name: \"a ray pointing away from the target returns null\",\n run({ hitTester, driver }) {\n driver.place(\"target\", [0, 0, -2], 0.15);\n assert(hitTester.hitRay(AWAY_RAY) === null, \"a ray pointing away from every target must return null\");\n },\n },\n {\n name: \"hitProximity finds a target inside its radius and misses outside it\",\n run({ hitTester, driver }) {\n driver.place(\"target\", [0, 0, -2], 0.1);\n const inside = hitTester.hitProximity([0, 0, -2], 0.5);\n assert(inside !== null, \"hitProximity at the target's own position must find it\");\n assert(\n inside.interactableId === \"target\",\n `expected interactableId \"target\", got \"${inside.interactableId}\"`,\n );\n const outside = hitTester.hitProximity([5, 5, 5], 0.05);\n assert(outside === null, \"hitProximity far from every target must return null\");\n },\n },\n {\n name: \"with two targets on one ray, the nearer one wins\",\n run({ hitTester, driver }) {\n const radius = 0.1;\n driver.place(\"far\", [0, 0, -3], radius);\n driver.place(\"near\", [0, 0, -1], radius);\n const hit = hitTester.hitRay(DOWN_RAY);\n assert(hit !== null, \"a ray through two targets must not miss\");\n assert(hit.interactableId === \"near\", `the nearer target must win, got \"${hit.interactableId}\"`);\n assert(closeTo(hit.distance, 1, slack(radius)), `distance must be close to 1, got ${hit.distance}`);\n },\n },\n {\n name: \"each hit and its point are fresh values the caller owns\",\n run({ hitTester, driver }) {\n driver.place(\"target\", [0, 0, -2], 0.15);\n for (const [label, query] of [\n [\"hitRay\", () => hitTester.hitRay(DOWN_RAY)],\n [\"hitProximity\", () => hitTester.hitProximity([0, 0, -2], 0.5)],\n ] as const) {\n const first = query();\n const second = query();\n assert(first !== null && second !== null, `${label} must find the placed target`);\n assert(second !== first && second.point !== first.point, `${label} must return a new hit and point each call`);\n const expected = first.point[2];\n first.point[2] = 1e6;\n const third = query();\n assert(\n third !== null && closeTo(third.point[2], expected, POSE_EPS),\n `writing to a returned point must not change the next ${label} answer`,\n );\n }\n },\n },\n];\n\n// ---------------------------------------------------------------------------\n// TransformPort\n// ---------------------------------------------------------------------------\n\n/**\n * Advances a subject's physics simulation - a real engine step where a\n * platform's physics can run in a test, otherwise a fake that faithfully\n * reproduces the one behaviour these cases check: gravity moves the object\n * unless held, and a release velocity carries it.\n */\nexport interface TransformPortPhysicsDriver {\n step(dtSeconds: number): void;\n}\n\n/** The port under test, over one object. Build a FRESH one per case. */\nexport interface TransformPortContractSubject {\n port: TransformPort;\n /**\n * The world pose the object sat at when the port captured its rest. Place\n * it away from the origin and turned, under no parent or an unscaled one,\n * so a port that ignores its rest frame cannot pass by accident.\n */\n rest: PoseTuple;\n /**\n * Present only when the object has a physics body - the same condition\n * that gives the port `beginHold`/`endHold`. The held/released/reset\n * cases below skip without one, since there is nothing for them to check.\n */\n physics?: TransformPortPhysicsDriver;\n}\n\n/** One check a {@link TransformPort} implementation must pass. */\nexport interface TransformPortContractCase {\n name: string;\n run(subject: TransformPortContractSubject): void;\n}\n\n/**\n * The shared `TransformPort` conformance suite. An adapter's test file is a\n * loop, the same shape as {@link hitTesterContractCases}.\n */\nexport function transformPortContractCases(): readonly TransformPortContractCase[] {\n return TRANSFORM_PORT_CASES;\n}\n\nconst POSE_EPS = 1e-3;\n// A quarter turn about Y - a non-identity unit quaternion, so the case\n// cannot pass by accident on a port that ignores its argument.\nconst QUARTER_TURN_Y: QuatTuple = [0, Math.SQRT1_2, 0, Math.SQRT1_2];\n\n// ---------------------------------------------------------------------------\n// Held / released / reset - the \"held pose\" rule, checked only when the\n// subject carries a physics driver. One fixed step size and a looser\n// position tolerance than POSE_EPS: real engines differ slightly in their\n// integrator, and a held/reset case allows for up to one step of gravity\n// (see PHYSICS_POSE_EPS's own comment), where the plain pose cases above\n// never touch physics at all.\n// ---------------------------------------------------------------------------\n\nconst PHYSICS_DT = 1 / 60;\nconst HELD_STEPS = 5;\n/** metres/second - small enough that one step's drift is easy to read, large enough not to vanish into PHYSICS_POSE_EPS. */\nconst RELEASE_SPEED = 2;\n/** metres/second - about 25x RELEASE_SPEED, so \"kept the old velocity\" and \"one step of gravity\" cannot be confused. */\nconst LARGE_PRIOR_SPEED = 50;\n/**\n * 2 cm. One step of gravity at PHYSICS_DT droops roughly\n * `0.5 * 9.8 * PHYSICS_DT^2` ≈ 1.4 mm, well inside this; a full step of\n * LARGE_PRIOR_SPEED (≈ 0.83 m) or RELEASE_SPEED (≈ 33 mm) is well outside it.\n */\nconst PHYSICS_POSE_EPS = 0.02;\n\nfunction assertPhysicsPoseClose(value: PoseTuple, expected: PoseTuple, label: string): void {\n assertVec3Close(value.position, expected.position, PHYSICS_POSE_EPS, `${label}.position`);\n assertQuatClose(value.quaternion, expected.quaternion, `${label}.quaternion`);\n}\n\nconst TRANSFORM_PORT_CASES: readonly TransformPortContractCase[] = [\n {\n name: \"setLocalOffset then getLocalOffset returns the same offset\",\n run({ port }) {\n const offset: Vec3Tuple = [0.3, -0.1, 0.05];\n port.setLocalOffset(offset);\n assertVec3Close(port.getLocalOffset(), offset, POSE_EPS, \"getLocalOffset()\");\n },\n },\n {\n name: \"getWorldPose returns finite numbers and a unit quaternion\",\n run({ port }) {\n const pose = port.getWorldPose();\n for (const value of [...pose.position, ...pose.quaternion]) {\n assert(Number.isFinite(value), `getWorldPose() must return finite numbers, got ${value}`);\n }\n const length = quatLength(pose.quaternion);\n assert(\n closeTo(length, 1, POSE_EPS),\n `getWorldPose().quaternion must be a unit quaternion, length was ${length}`,\n );\n },\n },\n {\n name: \"setLocalRotation with a unit quaternion does not throw\",\n run({ port }) {\n try {\n port.setLocalRotation(QUARTER_TURN_Y);\n } catch (error) {\n throw new Error(`setLocalRotation(unit quaternion) must not throw, it threw: ${String(error)}`);\n }\n },\n },\n {\n name: \"getRestWorldPose returns the pose captured at registration\",\n run({ port, rest }) {\n assertPoseClose(port.getRestWorldPose(), rest, \"getRestWorldPose()\");\n },\n },\n {\n name: \"before any write, the live pose is the rest pose\",\n run({ port, rest }) {\n assertPoseClose(port.getWorldPose(), rest, \"getWorldPose()\");\n },\n },\n {\n name: \"getWorldPose follows setLocalOffset while getRestWorldPose stays put\",\n run({ port, rest }) {\n const offset: Vec3Tuple = [0.3, -0.1, 0.05];\n port.setLocalOffset(offset);\n assertVec3Close(\n port.getWorldPose().position,\n vAdd(rest.position, vApplyQuat(offset, rest.quaternion)),\n POSE_EPS,\n \"getWorldPose().position after setLocalOffset\",\n );\n assertPoseClose(port.getRestWorldPose(), rest, \"getRestWorldPose() after setLocalOffset\");\n },\n },\n {\n name: \"getWorldPose follows setLocalRotation while getRestWorldPose stays put\",\n run({ port, rest }) {\n port.setLocalRotation(QUARTER_TURN_Y);\n assertQuatClose(\n port.getWorldPose().quaternion,\n quatMultiply(rest.quaternion, QUARTER_TURN_Y),\n \"getWorldPose().quaternion after setLocalRotation\",\n );\n assertPoseClose(port.getRestWorldPose(), rest, \"getRestWorldPose() after setLocalRotation\");\n },\n },\n {\n name: \"setWorldPose, when present, is what getWorldPose reads back\",\n run({ port, rest }) {\n if (!port.setWorldPose) return;\n const target: PoseTuple = { position: [0.5, 1.2, -0.4], quaternion: QUARTER_TURN_Y };\n port.setWorldPose(target);\n assertPoseClose(port.getWorldPose(), target, \"getWorldPose() after setWorldPose\");\n assertPoseClose(port.getRestWorldPose(), rest, \"getRestWorldPose() after setWorldPose\");\n },\n },\n {\n name: \"every returned tuple is a fresh value the caller owns\",\n run({ port }) {\n const pose = port.getWorldPose();\n const restPose = port.getRestWorldPose();\n const offset = port.getLocalOffset();\n pose.position[0] = 1e6;\n pose.quaternion[3] = 1e6;\n restPose.position[0] = 1e6;\n offset[0] = 1e6;\n assert(port.getWorldPose().position[0] !== 1e6, \"writing to a returned getWorldPose() tuple must not change the port\");\n assert(port.getWorldPose().quaternion[3] !== 1e6, \"writing to a returned getWorldPose() quaternion must not change the port\");\n assert(port.getRestWorldPose().position[0] !== 1e6, \"writing to a returned getRestWorldPose() tuple must not change the port\");\n assert(port.getLocalOffset()[0] !== 1e6, \"writing to a returned getLocalOffset() tuple must not change the port\");\n },\n },\n {\n name: \"a tuple passed in is not kept\",\n run({ port }) {\n const offset: Vec3Tuple = [0.3, -0.1, 0.05];\n port.setLocalOffset(offset);\n offset[0] = 9;\n assertVec3Close(port.getLocalOffset(), [0.3, -0.1, 0.05], POSE_EPS, \"getLocalOffset() after the caller reused its tuple\");\n if (!port.setWorldPose) return;\n const target: PoseTuple = { position: [0.5, 1.2, -0.4], quaternion: [0, 0, 0, 1] };\n port.setWorldPose(target);\n target.position[0] = 9;\n assertVec3Close(port.getWorldPose().position, [0.5, 1.2, -0.4], POSE_EPS, \"getWorldPose() after the caller reused its tuple\");\n },\n },\n {\n name: \"setEffect, when present, accepts scale and emissive without throwing\",\n run({ port }) {\n if (!port.setEffect) return;\n try {\n port.setEffect({ scale: 1.2, emissive: 0.3 });\n } catch (error) {\n throw new Error(`setEffect({ scale, emissive }) must not throw, it threw: ${String(error)}`);\n }\n },\n },\n {\n name: \"Held: beginHold holds a fixed setWorldPose without falling, when the subject has physics\",\n run({ port, rest, physics }) {\n if (!physics || !port.setWorldPose || !port.beginHold) return;\n const held: PoseTuple = {\n position: [rest.position[0], rest.position[1] + 1, rest.position[2]],\n quaternion: rest.quaternion,\n };\n port.beginHold();\n for (let i = 0; i < HELD_STEPS; i++) {\n port.setWorldPose(held);\n physics.step(PHYSICS_DT);\n }\n // A tight tolerance, not PHYSICS_POSE_EPS: a genuinely held body sees\n // no simulation at all, so it matches the written pose exactly bar\n // floating point noise - the same POSE_EPS the non-physics cases\n // above use. One step of unwanted gravity (~2.7 mm at PHYSICS_DT) is\n // what this case exists to catch, and it must not hide inside the\n // tolerance the way it deliberately may in the Reset case below.\n assertPoseClose(port.getWorldPose(), held, `getWorldPose() after ${HELD_STEPS} held steps`);\n },\n },\n {\n name: \"Released: endHold sends the body away with the release velocity, when the subject has physics\",\n run({ port, rest, physics }) {\n if (!physics || !port.beginHold || !port.endHold) return;\n port.beginHold();\n const release: HoldRelease = { linearVelocity: [RELEASE_SPEED, 0, 0], angularVelocity: [0, 0, 0] };\n port.endHold(release);\n physics.step(PHYSICS_DT);\n const moved = port.getWorldPose().position[0] - rest.position[0];\n assert(\n moved > RELEASE_SPEED * PHYSICS_DT * 0.5,\n `endHold({ linearVelocity: [${RELEASE_SPEED}, 0, 0] }) must move the body that way within one step, moved ${moved}`,\n );\n },\n },\n {\n name: \"Reset: setWorldPose while not held teleports to the pose and clears velocity, when the subject has physics\",\n run({ port, rest, physics }) {\n if (!physics || !port.setWorldPose || !port.beginHold || !port.endHold) return;\n // Give the body a large prior velocity and let it run for a step, so\n // carrying that velocity through the reset below is unmistakable -\n // one step of it (~0.83 m) dwarfs PHYSICS_POSE_EPS (2 cm).\n port.beginHold();\n const priorRelease: HoldRelease = { linearVelocity: [LARGE_PRIOR_SPEED, 0, 0], angularVelocity: [0, 0, 0] };\n port.endHold(priorRelease);\n physics.step(PHYSICS_DT);\n const target: PoseTuple = {\n position: [rest.position[0] - 1, rest.position[1] + 2, rest.position[2] + 0.5],\n quaternion: rest.quaternion,\n };\n port.setWorldPose(target);\n // One further step: it must be at `target`, not carrying the prior\n // velocity forward - allowing for this one step's worth of gravity.\n physics.step(PHYSICS_DT);\n assertPhysicsPoseClose(port.getWorldPose(), target, \"getWorldPose() one step after a reset setWorldPose\");\n },\n },\n];\n\n// ---------------------------------------------------------------------------\n// Shared assertion helpers\n// ---------------------------------------------------------------------------\n\nfunction assert(condition: boolean, message: string): asserts condition {\n if (!condition) throw new Error(message);\n}\n\nfunction closeTo(a: number, b: number, eps: number): boolean {\n return Math.abs(a - b) <= eps;\n}\n\n// Indexed one component at a time, rather than in a loop, so each index is a\n// literal: `noUncheckedIndexedAccess` cannot narrow a tuple read through a\n// variable index back to `number`.\nfunction assertVec3Close(value: Vec3Tuple, expected: Vec3Tuple, eps: number, label: string): void {\n assert(closeTo(value[0], expected[0], eps), `${label}[0] must be close to ${expected[0]}, got ${value[0]}`);\n assert(closeTo(value[1], expected[1], eps), `${label}[1] must be close to ${expected[1]}, got ${value[1]}`);\n assert(closeTo(value[2], expected[2], eps), `${label}[2] must be close to ${expected[2]}, got ${value[2]}`);\n}\n\n// q and -q are the same rotation, so compare by the size of the dot product.\nfunction assertQuatClose(value: QuatTuple, expected: QuatTuple, label: string): void {\n const dot = value[0] * expected[0] + value[1] * expected[1] + value[2] * expected[2] + value[3] * expected[3];\n assert(\n closeTo(Math.abs(dot), 1, POSE_EPS),\n `${label} must be close to [${expected.join(\", \")}], got [${value.join(\", \")}]`,\n );\n}\n\nfunction assertPoseClose(value: PoseTuple, expected: PoseTuple, label: string): void {\n assertVec3Close(value.position, expected.position, POSE_EPS, `${label}.position`);\n assertQuatClose(value.quaternion, expected.quaternion, `${label}.quaternion`);\n}\n\nfunction quatLength(q: QuatTuple): number {\n return Math.sqrt(q[0] * q[0] + q[1] * q[1] + q[2] * q[2] + q[3] * q[3]);\n}\n"]}
package/dist/ports.d.ts CHANGED
@@ -24,6 +24,17 @@ export interface HitTester {
24
24
  hitRay(ray: RayTuple): InteractableHit | null;
25
25
  hitProximity(point: Vec3Tuple, radius: number): InteractableHit | null;
26
26
  }
27
+ /**
28
+ * The release velocities handed to {@link TransformPort.endHold} - the
29
+ * grabbing source's velocity at the moment of release, so a throw carries
30
+ * through. Linear is metres per second, angular is radians per second,
31
+ * both in world space, both plain tuples: this crosses the same boundary
32
+ * `PoseTuple`/`Vec3Tuple` do, so it carries no engine object either.
33
+ */
34
+ export interface HoldRelease {
35
+ linearVelocity: Vec3Tuple;
36
+ angularVelocity: Vec3Tuple;
37
+ }
27
38
  /**
28
39
  * The transform/effect surface of ONE interactable's scene object.
29
40
  * Behaviours write through this; the adapter owns how writes land
@@ -56,11 +67,43 @@ export interface TransformPort {
56
67
  setLocalOffset(offset: Vec3Tuple): void;
57
68
  /** Local rotation about the rest orientation (hinge / dial output). */
58
69
  setLocalRotation(quaternion: QuatTuple): void;
59
- /** Follow a world pose while grabbed (poseOnly grab fulfilment). */
70
+ /**
71
+ * Follow a world pose while grabbed (poseOnly grab fulfilment), or place
72
+ * the object directly the rest of the time (reset / teleport).
73
+ *
74
+ * On a host whose object has physics, the two situations mean different
75
+ * things and a conforming port tells them apart with {@link beginHold}/
76
+ * {@link endHold}: WHILE HELD - between a `beginHold` and its matching
77
+ * `endHold` - the object follows every write exactly and its velocities
78
+ * are left untouched, because physics is suspended for the hold. NOT
79
+ * HELD, a write teleports the object there and clears its velocities, so
80
+ * it comes to rest at the pose ("back to the tee") rather than carrying
81
+ * whatever it was doing a moment before. A host with no physics has only
82
+ * the one meaning: the object is simply placed there.
83
+ */
60
84
  setWorldPose?(pose: PoseTuple): void;
61
85
  /** Additive presentation effect (pulse): scale multiplier & emissive boost. */
62
86
  setEffect?(effect: {
63
87
  scale?: number;
64
88
  emissive?: number;
65
89
  }): void;
90
+ /**
91
+ * Suspend physics for this object: from here until the matching
92
+ * `endHold`, it follows `setWorldPose` writes exactly and gravity /
93
+ * collisions stop moving it - see {@link setWorldPose}.
94
+ *
95
+ * Present only on a port whose object carries a physics body; a port
96
+ * with no physics never grows this member, so it behaves exactly as
97
+ * before. A platform that fulfils grabs natively through its own engine
98
+ * (`grabsNative`) must show the same three behaviours (held / released /
99
+ * reset) through that engine - the runtime never calls `beginHold` for
100
+ * one, because the engine, not `webxr-interactions`, already owns the
101
+ * hold.
102
+ */
103
+ beginHold?(): void;
104
+ /**
105
+ * Resume physics, applying `release` as the body's new velocity so a
106
+ * throw carries through. Present on the same terms as {@link beginHold}.
107
+ */
108
+ endHold?(release: HoldRelease): void;
66
109
  }
package/dist/ports.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"ports.js","sourceRoot":"","sources":["../src/ports.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * Engine-side ports - the space queries and transform writes the core\n * cannot perform itself. Adapters implement these against their scene\n * graph; the core only ever speaks tuples and interactable ids.\n */\nimport type { PoseTuple, QuatTuple, RayTuple, Vec3Tuple } from \"@realitycollective/webxr-input\";\n\nexport interface InteractableHit {\n interactableId: string;\n /** Distance from the query origin (ray origin / probe point). */\n distance: number;\n /** World-space hit or closest point. */\n point: Vec3Tuple;\n}\n\n/**\n * Resolves which interactable a ray or a proximity probe touches.\n * Implemented by the adapter (three.js Raycaster, engine BVH, …). A\n * provider that pre-resolves targeting (IWSDK) may make this redundant -\n * provider hints always take precedence over hit-tester results.\n *\n * Each hit, and the `point` tuple in it, is a fresh value the caller owns.\n * A ray or point passed in is read during the call and not kept.\n */\nexport interface HitTester {\n hitRay(ray: RayTuple): InteractableHit | null;\n hitProximity(point: Vec3Tuple, radius: number): InteractableHit | null;\n}\n\n/**\n * The transform/effect surface of ONE interactable's scene object.\n * Behaviours write through this; the adapter owns how writes land\n * (Object3D fields, ECS components, …).\n *\n * Offsets/rotations are FROM REST: the adapter captures the rest pose at\n * registration and applies these relative to it, so behaviours compose\n * with anything else animating the same object (the pale-signal\n * non-destructive-offset rule).\n *\n * Every tuple a port returns is a fresh value the caller owns. The port\n * keeps no reference to it and never writes to it again. A tuple passed in\n * is read during the call and not kept, so the caller may reuse it.\n */\nexport interface TransformPort {\n /**\n * The LIVE world pose: where the object is now, after every offset,\n * rotation and world-pose write, including a write made by anything else.\n */\n getWorldPose(): PoseTuple;\n /**\n * The rest pose captured at registration, in world space. Offsets and\n * rotations are measured from it, so it stays put while the port writes.\n * It follows a parent that moves.\n */\n getRestWorldPose(): PoseTuple;\n /** Local-space pose relative to the captured rest (dragged observation). */\n getLocalOffset(): Vec3Tuple;\n /** Additive local-space offset from rest (driven press / slide). */\n setLocalOffset(offset: Vec3Tuple): void;\n /** Local rotation about the rest orientation (hinge / dial output). */\n setLocalRotation(quaternion: QuatTuple): void;\n /** Follow a world pose while grabbed (poseOnly grab fulfilment). */\n setWorldPose?(pose: PoseTuple): void;\n /** Additive presentation effect (pulse): scale multiplier & emissive boost. */\n setEffect?(effect: { scale?: number; emissive?: number }): void;\n}\n"]}
1
+ {"version":3,"file":"ports.js","sourceRoot":"","sources":["../src/ports.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * Engine-side ports - the space queries and transform writes the core\n * cannot perform itself. Adapters implement these against their scene\n * graph; the core only ever speaks tuples and interactable ids.\n */\nimport type { PoseTuple, QuatTuple, RayTuple, Vec3Tuple } from \"@realitycollective/webxr-input\";\n\nexport interface InteractableHit {\n interactableId: string;\n /** Distance from the query origin (ray origin / probe point). */\n distance: number;\n /** World-space hit or closest point. */\n point: Vec3Tuple;\n}\n\n/**\n * Resolves which interactable a ray or a proximity probe touches.\n * Implemented by the adapter (three.js Raycaster, engine BVH, …). A\n * provider that pre-resolves targeting (IWSDK) may make this redundant -\n * provider hints always take precedence over hit-tester results.\n *\n * Each hit, and the `point` tuple in it, is a fresh value the caller owns.\n * A ray or point passed in is read during the call and not kept.\n */\nexport interface HitTester {\n hitRay(ray: RayTuple): InteractableHit | null;\n hitProximity(point: Vec3Tuple, radius: number): InteractableHit | null;\n}\n\n/**\n * The release velocities handed to {@link TransformPort.endHold} - the\n * grabbing source's velocity at the moment of release, so a throw carries\n * through. Linear is metres per second, angular is radians per second,\n * both in world space, both plain tuples: this crosses the same boundary\n * `PoseTuple`/`Vec3Tuple` do, so it carries no engine object either.\n */\nexport interface HoldRelease {\n linearVelocity: Vec3Tuple;\n angularVelocity: Vec3Tuple;\n}\n\n/**\n * The transform/effect surface of ONE interactable's scene object.\n * Behaviours write through this; the adapter owns how writes land\n * (Object3D fields, ECS components, …).\n *\n * Offsets/rotations are FROM REST: the adapter captures the rest pose at\n * registration and applies these relative to it, so behaviours compose\n * with anything else animating the same object (the pale-signal\n * non-destructive-offset rule).\n *\n * Every tuple a port returns is a fresh value the caller owns. The port\n * keeps no reference to it and never writes to it again. A tuple passed in\n * is read during the call and not kept, so the caller may reuse it.\n */\nexport interface TransformPort {\n /**\n * The LIVE world pose: where the object is now, after every offset,\n * rotation and world-pose write, including a write made by anything else.\n */\n getWorldPose(): PoseTuple;\n /**\n * The rest pose captured at registration, in world space. Offsets and\n * rotations are measured from it, so it stays put while the port writes.\n * It follows a parent that moves.\n */\n getRestWorldPose(): PoseTuple;\n /** Local-space pose relative to the captured rest (dragged observation). */\n getLocalOffset(): Vec3Tuple;\n /** Additive local-space offset from rest (driven press / slide). */\n setLocalOffset(offset: Vec3Tuple): void;\n /** Local rotation about the rest orientation (hinge / dial output). */\n setLocalRotation(quaternion: QuatTuple): void;\n /**\n * Follow a world pose while grabbed (poseOnly grab fulfilment), or place\n * the object directly the rest of the time (reset / teleport).\n *\n * On a host whose object has physics, the two situations mean different\n * things and a conforming port tells them apart with {@link beginHold}/\n * {@link endHold}: WHILE HELD - between a `beginHold` and its matching\n * `endHold` - the object follows every write exactly and its velocities\n * are left untouched, because physics is suspended for the hold. NOT\n * HELD, a write teleports the object there and clears its velocities, so\n * it comes to rest at the pose (\"back to the tee\") rather than carrying\n * whatever it was doing a moment before. A host with no physics has only\n * the one meaning: the object is simply placed there.\n */\n setWorldPose?(pose: PoseTuple): void;\n /** Additive presentation effect (pulse): scale multiplier & emissive boost. */\n setEffect?(effect: { scale?: number; emissive?: number }): void;\n /**\n * Suspend physics for this object: from here until the matching\n * `endHold`, it follows `setWorldPose` writes exactly and gravity /\n * collisions stop moving it - see {@link setWorldPose}.\n *\n * Present only on a port whose object carries a physics body; a port\n * with no physics never grows this member, so it behaves exactly as\n * before. A platform that fulfils grabs natively through its own engine\n * (`grabsNative`) must show the same three behaviours (held / released /\n * reset) through that engine - the runtime never calls `beginHold` for\n * one, because the engine, not `webxr-interactions`, already owns the\n * hold.\n */\n beginHold?(): void;\n /**\n * Resume physics, applying `release` as the body's new velocity so a\n * throw carries through. Present on the same terms as {@link beginHold}.\n */\n endHold?(release: HoldRelease): void;\n}\n"]}
package/dist/runtime.js CHANGED
@@ -210,7 +210,22 @@ export class InteractionRuntime {
210
210
  this.tickBehaviours(sources, dt);
211
211
  }
212
212
  dispose() {
213
+ if (this.disposed)
214
+ return;
213
215
  this.disposed = true;
216
+ // A disposed runtime still owes every held grab exactly one `endHold`
217
+ // (zero velocity - nothing threw it away, the scene just went down),
218
+ // so physics never stays suspended once nothing is left to resume it.
219
+ for (const [id, registered] of this.interactables) {
220
+ if (registered.grabbedBy) {
221
+ this.endGrab(id, registered.grabbedBy, {
222
+ id: registered.grabbedBy,
223
+ kind: "other",
224
+ select: 0,
225
+ squeeze: 0,
226
+ });
227
+ }
228
+ }
214
229
  this.unsubscribeCaps();
215
230
  this.interactables.clear();
216
231
  this.sourceStates.clear();
@@ -239,6 +254,10 @@ export class InteractionRuntime {
239
254
  info.gripPose = source.gripPose;
240
255
  if (source.indexTip)
241
256
  info.indexTip = source.indexTip;
257
+ if (source.linearVelocity)
258
+ info.linearVelocity = source.linearVelocity;
259
+ if (source.angularVelocity)
260
+ info.angularVelocity = source.angularVelocity;
242
261
  return info;
243
262
  }
244
263
  /** Resolve this source's target: hints beat poke beats ray. */
@@ -1 +1 @@
1
- {"version":3,"file":"runtime.js","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * InteractionRuntime - the engine-free orchestrator.\n *\n * Once per host frame the runtime pulls the input provider, resolves\n * targeting (provider hints first - provider power wins - then poke\n * proximity, then ray hits), applies press/grab transitions with\n * hysteresis, runs gaze gating + dwell, ticks every behaviour, and emits\n * interaction events and feedback intents. It never touches an engine:\n * space queries and transform writes go through the adapter-implemented\n * {@link HitTester} and {@link TransformPort}.\n *\n * Lifecycle is first-class (the pale-signal gap): interactables register,\n * unregister, enable and disable; behaviours disabled by capability\n * negotiation announce themselves with a `behaviourDisabled` event.\n */\nimport {\n SELECT_PRESS_THRESHOLD,\n SELECT_RELEASE_THRESHOLD,\n unmetRequirements,\n type InputCapabilities,\n type InputHitHint,\n type InputProvider,\n type InputSourceSnapshot,\n type PoseTuple,\n type RayTuple,\n type Unsubscribe,\n} from \"@realitycollective/webxr-input\";\nimport type { Behaviour, InteractorInfo } from \"./behaviours/behaviour.js\";\nimport {\n createBehaviour,\n type BehaviourConfig,\n type BehaviourFactoryContext,\n} from \"./behaviours/factory.js\";\nimport { GrabBehaviour } from \"./behaviours/grab.js\";\nimport type { InteractableDescriptor } from \"./descriptor.js\";\nimport { Emitter, type InteractionEvent, type InteractionEventListener } from \"./events.js\";\nimport type { FeedbackIntent, FeedbackListener } from \"./feedback.js\";\nimport { DwellState, resolveDwellConfig, DWELL_DEFAULTS, type DwellConfig } from \"./gaze.js\";\nimport { vApplyQuat } from \"./math.js\";\nimport type { HitTester, TransformPort } from \"./ports.js\";\nimport { VelocityTracker, type VelocityTrackerOptions } from \"./velocity-tracker.js\";\n\nconst DEFAULT_POKE_RADIUS = 0.05;\n/** Interactor id used when gaze is synthesized from the head pose. */\nexport const HEAD_GAZE_INTERACTOR_ID = \"head-gaze\";\n\nexport interface InteractableState {\n id: string;\n enabled: boolean;\n hovered: boolean;\n gazeHovered: boolean;\n pressed: boolean;\n grabbed: boolean;\n /** Primary behaviour's logical value. */\n value: number;\n /** Gaze-dwell meter 0..1 (0 when dwell is not enabled). */\n dwell: number;\n}\n\ninterface BehaviourSlot {\n instance: Behaviour;\n config: BehaviourConfig;\n disabled: boolean;\n disabledReason: string;\n}\n\ninterface Registered {\n def: InteractableDescriptor;\n behaviours: BehaviourSlot[];\n transform: TransformPort | undefined;\n enabled: boolean;\n pokeRadius: number;\n gazeRequired: boolean;\n dwellConfig: Required<DwellConfig> | null;\n dwell: DwellState;\n gazeHovered: boolean;\n hoveredBy: Set<string>;\n pressedBy: Set<string>;\n grabbedBy: string | null;\n}\n\ninterface SourceRuntimeState {\n hoverTarget: string | null;\n pressed: boolean;\n pressTarget: string | null;\n grabbed: boolean;\n grabTarget: string | null;\n}\n\nexport interface InteractionRuntimeOptions {\n provider: InputProvider;\n hitTester?: HitTester;\n dwellDefaults?: DwellConfig;\n /**\n * Per-source velocity tracking. On by default with no smoothing; pass\n * `false` to skip it, or options to smooth. The tracker only fills in\n * what the provider did not supply.\n */\n velocity?: VelocityTrackerOptions | false;\n}\n\nexport interface RegisterPorts {\n transform?: TransformPort;\n}\n\nexport class InteractionRuntime {\n private readonly provider: InputProvider;\n private hitTester: HitTester | null;\n private readonly dwellDefaults: Required<DwellConfig>;\n\n private readonly interactables = new Map<string, Registered>();\n private readonly sourceStates = new Map<string, SourceRuntimeState>();\n private readonly events = new Emitter<InteractionEvent>();\n private readonly feedbackEmitter = new Emitter<FeedbackIntent>();\n private readonly afterSample = new Emitter<readonly InputSourceSnapshot[]>();\n private readonly velocityTracker: VelocityTracker | null;\n private lastSources = new Map<string, InputSourceSnapshot>();\n private capabilities: InputCapabilities;\n private readonly unsubscribeCaps: Unsubscribe;\n private disposed = false;\n\n constructor(options: InteractionRuntimeOptions) {\n this.provider = options.provider;\n this.hitTester = options.hitTester ?? null;\n const d = options.dwellDefaults;\n this.dwellDefaults = {\n holdSeconds: d?.holdSeconds ?? DWELL_DEFAULTS.holdSeconds,\n decayFactor: d?.decayFactor ?? DWELL_DEFAULTS.decayFactor,\n rearmBelow: d?.rearmBelow ?? DWELL_DEFAULTS.rearmBelow,\n };\n this.velocityTracker =\n options.velocity === false ? null : new VelocityTracker(options.velocity ?? {});\n this.capabilities = this.provider.getCapabilities();\n this.unsubscribeCaps = this.provider.onCapabilitiesChanged((caps) => {\n this.capabilities = caps;\n this.renegotiateAll();\n });\n }\n\n // -- registry -------------------------------------------------------------\n\n registerInteractable(def: InteractableDescriptor, ports: RegisterPorts = {}): void {\n if (this.interactables.has(def.id)) {\n throw new Error(`Interactable \"${def.id}\" is already registered`);\n }\n const context: BehaviourFactoryContext = {\n grabFulfilment: this.capabilities.grabs === \"native\" ? \"native\" : \"poseOnly\",\n };\n const registered: Registered = {\n def,\n behaviours: def.behaviours.map((config) => ({\n instance: createBehaviour(config, context),\n config,\n disabled: false,\n disabledReason: \"\",\n })),\n transform: ports.transform,\n enabled: def.enabled !== false,\n pokeRadius: def.pokeRadius ?? DEFAULT_POKE_RADIUS,\n gazeRequired: def.gaze?.required === true,\n dwellConfig: resolveDwellConfig(def.gaze?.dwell, this.dwellDefaults),\n dwell: new DwellState(),\n gazeHovered: false,\n hoveredBy: new Set(),\n pressedBy: new Set(),\n grabbedBy: null,\n };\n this.interactables.set(def.id, registered);\n this.renegotiate(def.id, registered);\n }\n\n unregisterInteractable(id: string): void {\n const registered = this.interactables.get(id);\n if (!registered) return;\n this.releaseAllHolds(id);\n this.interactables.delete(id);\n }\n\n setInteractableEnabled(id: string, enabled: boolean): void {\n const registered = this.interactables.get(id);\n if (!registered || registered.enabled === enabled) return;\n registered.enabled = enabled;\n if (!enabled) this.releaseAllHolds(id);\n this.events.emit({\n type: enabled ? \"interactableEnabled\" : \"interactableDisabled\",\n interactableId: id,\n });\n }\n\n setHitTester(hitTester: HitTester | null): void {\n this.hitTester = hitTester;\n }\n\n /** Reach a live behaviour instance for tuning (playground steppers). */\n getBehaviour<T extends Behaviour = Behaviour>(interactableId: string, kind: string): T | undefined {\n const registered = this.interactables.get(interactableId);\n return registered?.behaviours.find((b) => b.instance.kind === kind)?.instance as T | undefined;\n }\n\n getState(id: string): InteractableState | undefined {\n const r = this.interactables.get(id);\n if (!r) return undefined;\n const primary = r.behaviours.find((b) => !b.disabled)?.instance;\n return {\n id,\n enabled: r.enabled,\n hovered: r.hoveredBy.size > 0 || r.gazeHovered,\n gazeHovered: r.gazeHovered,\n pressed: r.pressedBy.size > 0,\n grabbed: r.grabbedBy !== null,\n value: primary?.getValue() ?? 0,\n dwell: r.dwell.getProgress(),\n };\n }\n\n getInteractableIds(): string[] {\n return [...this.interactables.keys()];\n }\n\n // -- subscriptions ----------------------------------------------------------\n\n onEvent(listener: InteractionEventListener): Unsubscribe {\n return this.events.subscribe(listener);\n }\n\n onFeedback(listener: FeedbackListener): Unsubscribe {\n return this.feedbackEmitter.subscribe(listener);\n }\n\n /**\n * Sampled sources for this frame - the UI Extensions pointer bridge hook.\n *\n * @deprecated Use {@link onSample}. Both deliver the same stream: since\n * `@realitycollective/webxr-input` 0.1.1 carries `linearVelocity` and\n * `angularVelocity` on `InputSourceSnapshot`, the two signatures are\n * identical and only one name is needed. This one is kept so existing\n * callers keep working and will be removed in a later major release.\n */\n onSourcesSampled(listener: (sources: readonly InputSourceSnapshot[]) => void): Unsubscribe {\n return this.afterSample.subscribe(listener);\n }\n\n /**\n * This frame's sources, velocity included.\n *\n * The snapshots are the provider's own objects for this frame and are\n * never written to again once delivered (the ownership rule on\n * `InputSourceSnapshot`), so a listener may keep one, or a tuple inside\n * it, across frames without copying.\n */\n onSample(listener: (sources: readonly InputSourceSnapshot[]) => void): Unsubscribe {\n return this.afterSample.subscribe(listener);\n }\n\n /**\n * The last sample of one source, velocity included. Undefined before the\n * first update, and once the source stops reporting.\n *\n * The snapshot is the caller's to keep: it is never mutated after the\n * frame it was sampled in, and it is not refreshed in place, so call\n * again for a newer one.\n */\n getSource(id: string): InputSourceSnapshot | undefined {\n return this.lastSources.get(id);\n }\n\n getCapabilities(): InputCapabilities {\n return this.capabilities;\n }\n\n getProvider(): InputProvider {\n return this.provider;\n }\n\n // -- frame ------------------------------------------------------------------\n\n update(dt: number): void {\n if (this.disposed) return;\n const sampled = this.provider.sample();\n const sources = this.velocityTracker ? this.velocityTracker.update(sampled, dt) : sampled;\n const hints = this.provider.sampleHints?.() ?? [];\n this.lastSources = new Map(sources.map((source) => [source.id, source]));\n this.afterSample.emit(sources);\n\n const hintBySource = new Map<string, InputHitHint[]>();\n for (const hint of hints) {\n const list = hintBySource.get(hint.sourceId);\n if (list) list.push(hint);\n else hintBySource.set(hint.sourceId, [hint]);\n }\n\n const seen = new Set<string>();\n for (const source of sources) {\n seen.add(source.id);\n this.updateSource(source, hintBySource.get(source.id) ?? []);\n }\n // Sources that vanished mid-hold release everything they held.\n for (const [id, state] of this.sourceStates) {\n if (!seen.has(id)) {\n this.forceRelease(id, state);\n this.sourceStates.delete(id);\n }\n }\n\n this.updateGaze(sources, dt);\n this.tickBehaviours(sources, dt);\n }\n\n dispose(): void {\n this.disposed = true;\n this.unsubscribeCaps();\n this.interactables.clear();\n this.sourceStates.clear();\n this.lastSources.clear();\n this.velocityTracker?.reset();\n }\n\n // -- internals ---------------------------------------------------------------\n\n private sourceState(id: string): SourceRuntimeState {\n let state = this.sourceStates.get(id);\n if (!state) {\n state = { hoverTarget: null, pressed: false, pressTarget: null, grabbed: false, grabTarget: null };\n this.sourceStates.set(id, state);\n }\n return state;\n }\n\n private interactorInfo(source: InputSourceSnapshot): InteractorInfo {\n const info: InteractorInfo = {\n id: source.id,\n kind: source.kind,\n select: source.select,\n squeeze: source.squeeze,\n };\n if (source.ray) info.ray = source.ray;\n if (source.gripPose) info.gripPose = source.gripPose;\n if (source.indexTip) info.indexTip = source.indexTip;\n return info;\n }\n\n /** Resolve this source's target: hints beat poke beats ray. */\n private resolveTarget(source: InputSourceSnapshot, hints: InputHitHint[]): string | null {\n const hint =\n hints.find((h) => h.state === \"grab\") ??\n hints.find((h) => h.state === \"press\") ??\n hints.find((h) => h.state === \"hover\");\n if (hint && this.targetable(hint.targetId)) return hint.targetId;\n if (source.indexTip && this.hitTester) {\n // One proximity query at the largest radius any interactable asked for,\n // then the nearest hit must be inside ITS OWN radius. A per-interactable\n // query would be a hit test per registration per source per frame; this\n // keeps it to one. The trade: a farther interactable with a bigger radius\n // is not found behind a nearer one with a smaller radius, because the\n // tester returns only the nearest.\n const poke = this.hitTester.hitProximity(source.indexTip, this.maxPokeRadius());\n if (poke && this.targetable(poke.interactableId)) {\n const registered = this.interactables.get(poke.interactableId)!;\n if (poke.distance <= registered.pokeRadius) return poke.interactableId;\n }\n }\n if (source.ray && this.hitTester) {\n const hit = this.hitTester.hitRay(source.ray);\n if (hit && this.targetable(hit.interactableId)) return hit.interactableId;\n }\n return null;\n }\n\n private targetable(id: string): boolean {\n const registered = this.interactables.get(id);\n return registered !== undefined && registered.enabled;\n }\n\n /** The largest poke radius among enabled interactables; the default when none is registered. */\n private maxPokeRadius(): number {\n let radius = 0;\n for (const registered of this.interactables.values()) {\n if (registered.enabled && registered.pokeRadius > radius) radius = registered.pokeRadius;\n }\n return radius > 0 ? radius : DEFAULT_POKE_RADIUS;\n }\n\n private flags(registered: Registered): { pressable: boolean; grabbable: boolean } {\n let pressable = false;\n let grabbable = false;\n for (const slot of registered.behaviours) {\n if (slot.disabled) continue;\n if (slot.instance.onPressStart) pressable = true;\n if (slot.instance.grabbable) grabbable = true;\n }\n return { pressable, grabbable };\n }\n\n private updateSource(source: InputSourceSnapshot, hints: InputHitHint[]): void {\n const state = this.sourceState(source.id);\n const target = this.resolveTarget(source, hints);\n\n // Hover transitions.\n if (state.hoverTarget !== target) {\n if (state.hoverTarget) this.setHover(state.hoverTarget, source.id, false);\n if (target) this.setHover(target, source.id, true);\n state.hoverTarget = target;\n }\n\n const registered = target ? this.interactables.get(target) : undefined;\n const roles = registered ? this.flags(registered) : { pressable: false, grabbable: false };\n\n // Signal resolution. A pinching hand (or a 2D pointer click) acts as a\n // grab when the target is grab-only; controllers keep trigger=press,\n // squeeze=grab.\n const selectAsGrab = roles.grabbable && !roles.pressable && source.kind !== \"controller\";\n const pressSignal = selectAsGrab ? 0 : source.select;\n const grabSignal = Math.max(source.squeeze, selectAsGrab ? source.select : 0);\n\n const hintPress = hints.some((h) => h.state === \"press\");\n const hintGrab = hints.some((h) => h.state === \"grab\") || source.nativeGrabbing === true;\n\n // Press with hysteresis (hints override).\n const pressActive = hintPress || (state.pressed\n ? pressSignal > SELECT_RELEASE_THRESHOLD\n : pressSignal >= SELECT_PRESS_THRESHOLD);\n if (pressActive && !state.pressed) {\n state.pressed = true;\n if (target && registered && roles.pressable && this.gazeAllows(registered)) {\n state.pressTarget = target;\n this.routePressStart(target, registered, this.interactorInfo(source));\n }\n } else if (!pressActive && state.pressed) {\n state.pressed = false;\n if (state.pressTarget) {\n this.routePressEnd(state.pressTarget, source.id, this.interactorInfo(source));\n state.pressTarget = null;\n }\n }\n\n // Grab with hysteresis (hints / native grabbing override).\n const grabActive = hintGrab || (state.grabbed\n ? grabSignal > SELECT_RELEASE_THRESHOLD\n : grabSignal >= SELECT_PRESS_THRESHOLD);\n if (grabActive && !state.grabbed) {\n state.grabbed = true;\n const hintTargetId = hints.find((h) => h.state === \"grab\")?.targetId;\n const grabTargetId = hintTargetId && this.targetable(hintTargetId) ? hintTargetId : target;\n const grabTarget = grabTargetId ? this.interactables.get(grabTargetId) : undefined;\n if (\n grabTargetId &&\n grabTarget &&\n this.flags(grabTarget).grabbable &&\n grabTarget.grabbedBy === null &&\n this.gazeAllows(grabTarget)\n ) {\n state.grabTarget = grabTargetId;\n grabTarget.grabbedBy = source.id;\n const info = this.interactorInfo(source);\n for (const slot of grabTarget.behaviours) {\n if (!slot.disabled && slot.instance.grabbable) {\n slot.instance.onGrabStart?.(this.contextFor(grabTargetId, grabTarget, 0), info);\n }\n }\n }\n } else if (!grabActive && state.grabbed) {\n state.grabbed = false;\n if (state.grabTarget) {\n this.endGrab(state.grabTarget, source.id, this.interactorInfo(source));\n state.grabTarget = null;\n }\n }\n }\n\n private gazeAllows(registered: Registered): boolean {\n return !registered.gazeRequired || registered.gazeHovered;\n }\n\n private setHover(id: string, sourceId: string, hovered: boolean): void {\n const registered = this.interactables.get(id);\n if (!registered) return;\n if (hovered) {\n registered.hoveredBy.add(sourceId);\n if (registered.hoveredBy.size === 1) {\n this.events.emit({ type: \"hoverEnter\", interactableId: id, interactorId: sourceId });\n this.feedbackEmitter.emit({\n cue: \"hover\",\n interactableId: id,\n sourceId,\n intensity: 0.1,\n durationMs: 10,\n });\n }\n } else {\n registered.hoveredBy.delete(sourceId);\n if (registered.hoveredBy.size === 0) {\n this.events.emit({ type: \"hoverExit\", interactableId: id, interactorId: sourceId });\n }\n }\n }\n\n private routePressStart(id: string, registered: Registered, info: InteractorInfo): void {\n registered.pressedBy.add(info.id);\n this.events.emit({ type: \"pressStart\", interactableId: id, interactorId: info.id });\n const ctx = this.contextFor(id, registered, 0);\n for (const slot of registered.behaviours) {\n if (!slot.disabled) slot.instance.onPressStart?.(ctx, info);\n }\n }\n\n private routePressEnd(id: string, sourceId: string, info: InteractorInfo): void {\n const registered = this.interactables.get(id);\n if (!registered) return;\n registered.pressedBy.delete(sourceId);\n this.events.emit({ type: \"pressEnd\", interactableId: id, interactorId: sourceId });\n const ctx = this.contextFor(id, registered, 0);\n for (const slot of registered.behaviours) {\n if (!slot.disabled) slot.instance.onPressEnd?.(ctx, info);\n }\n }\n\n private endGrab(id: string, sourceId: string, info: InteractorInfo): void {\n const registered = this.interactables.get(id);\n if (!registered || registered.grabbedBy !== sourceId) return;\n registered.grabbedBy = null;\n const ctx = this.contextFor(id, registered, 0);\n for (const slot of registered.behaviours) {\n if (!slot.disabled && slot.instance.grabbable) slot.instance.onGrabEnd?.(ctx, info);\n }\n }\n\n private forceRelease(sourceId: string, state: SourceRuntimeState): void {\n const info: InteractorInfo = { id: sourceId, kind: \"other\", select: 0, squeeze: 0 };\n if (state.pressTarget) this.routePressEnd(state.pressTarget, sourceId, info);\n if (state.grabTarget) this.endGrab(state.grabTarget, sourceId, info);\n if (state.hoverTarget) this.setHover(state.hoverTarget, sourceId, false);\n }\n\n private releaseAllHolds(id: string): void {\n for (const [sourceId, state] of this.sourceStates) {\n if (state.pressTarget === id) {\n this.routePressEnd(id, sourceId, { id: sourceId, kind: \"other\", select: 0, squeeze: 0 });\n state.pressTarget = null;\n state.pressed = false;\n }\n if (state.grabTarget === id) {\n this.endGrab(id, sourceId, { id: sourceId, kind: \"other\", select: 0, squeeze: 0 });\n state.grabTarget = null;\n state.grabbed = false;\n }\n if (state.hoverTarget === id) {\n this.setHover(id, sourceId, false);\n state.hoverTarget = null;\n }\n }\n }\n\n private updateGaze(sources: readonly InputSourceSnapshot[], dt: number): void {\n let gazeRay: RayTuple | null = null;\n let gazeId = HEAD_GAZE_INTERACTOR_ID;\n const gazeSource = sources.find((s) => s.kind === \"gaze\");\n if (gazeSource?.ray) {\n gazeRay = gazeSource.ray;\n gazeId = gazeSource.id;\n } else if (this.capabilities.headPose && this.provider.getHeadPose) {\n const head: PoseTuple = this.provider.getHeadPose();\n gazeRay = {\n origin: head.position,\n direction: vApplyQuat([0, 0, -1], head.quaternion),\n };\n }\n\n const gazeTarget =\n gazeRay && this.hitTester ? this.hitTester.hitRay(gazeRay)?.interactableId ?? null : null;\n\n for (const [id, registered] of this.interactables) {\n const hovered = registered.enabled && gazeTarget === id;\n if (hovered !== registered.gazeHovered) {\n registered.gazeHovered = hovered;\n this.events.emit({\n type: hovered ? \"hoverEnter\" : \"hoverExit\",\n interactableId: id,\n interactorId: gazeId,\n });\n }\n if (registered.dwellConfig && registered.enabled) {\n const tick = registered.dwell.update(hovered, dt, registered.dwellConfig);\n if (tick.changed) {\n this.events.emit({\n type: \"dwellProgress\",\n interactableId: id,\n interactorId: gazeId,\n value: tick.progress,\n });\n }\n if (tick.fired) {\n // A completed dwell is a synthesized click: press + release.\n const info: InteractorInfo = { id: gazeId, kind: \"gaze\", select: 1, squeeze: 0 };\n this.routePressStart(id, registered, info);\n this.routePressEnd(id, gazeId, { ...info, select: 0 });\n this.feedbackEmitter.emit({\n cue: \"dwellComplete\",\n interactableId: id,\n intensity: 0.6,\n durationMs: 50,\n });\n }\n }\n }\n }\n\n private tickBehaviours(sources: readonly InputSourceSnapshot[], dt: number): void {\n const bySourceId = new Map(sources.map((s) => [s.id, s]));\n for (const [id, registered] of this.interactables) {\n if (!registered.enabled) continue;\n const holderSource = registered.grabbedBy ? bySourceId.get(registered.grabbedBy) : undefined;\n const holder = holderSource ? this.interactorInfo(holderSource) : undefined;\n const ctx = this.contextFor(id, registered, dt);\n for (const slot of registered.behaviours) {\n if (slot.disabled) continue;\n slot.instance.update(ctx, slot.instance.grabbable ? holder : undefined);\n }\n }\n }\n\n private contextFor(id: string, registered: Registered, dt: number) {\n return {\n dt,\n interactableId: id,\n transform: registered.transform,\n emit: (event: Omit<InteractionEvent, \"interactableId\">) =>\n this.events.emit({ ...event, interactableId: id }),\n feedback: (intent: Omit<FeedbackIntent, \"interactableId\">) =>\n this.feedbackEmitter.emit({ ...intent, interactableId: id }),\n getWorldPose: (otherId: string) =>\n this.interactables.get(otherId)?.transform?.getWorldPose(),\n };\n }\n\n private renegotiate(id: string, registered: Registered): void {\n const fulfilment = this.capabilities.grabs === \"native\" ? \"native\" : \"poseOnly\";\n for (const slot of registered.behaviours) {\n if (slot.instance instanceof GrabBehaviour) {\n slot.instance.setFulfilment(fulfilment);\n }\n const unmet = [...unmetRequirements(this.capabilities, slot.instance.requires)];\n for (const group of slot.instance.requiresAnyOf ?? []) {\n if (group.length > 0 && unmetRequirements(this.capabilities, group).length === group.length) {\n unmet.push(group[0]!);\n }\n }\n const disabled = unmet.length > 0;\n if (disabled !== slot.disabled) {\n slot.disabled = disabled;\n slot.disabledReason = disabled ? `unmet input capabilities: ${unmet.join(\", \")}` : \"\";\n this.events.emit({\n type: disabled ? \"behaviourDisabled\" : \"behaviourEnabled\",\n interactableId: id,\n behaviourKind: slot.instance.kind,\n ...(disabled ? { reason: slot.disabledReason } : {}),\n });\n }\n }\n }\n\n private renegotiateAll(): void {\n for (const [id, registered] of this.interactables) {\n this.renegotiate(id, registered);\n }\n }\n}\n\n/** Build a runtime's interactables from a portable descriptor. */\nexport function registerDescriptor(\n runtime: InteractionRuntime,\n descriptor: { interactables: InteractableDescriptor[] },\n portsFor: (id: string) => RegisterPorts = () => ({}),\n): void {\n for (const interactable of descriptor.interactables) {\n runtime.registerInteractable(interactable, portsFor(interactable.id));\n }\n}\n"]}
1
+ {"version":3,"file":"runtime.js","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * InteractionRuntime - the engine-free orchestrator.\n *\n * Once per host frame the runtime pulls the input provider, resolves\n * targeting (provider hints first - provider power wins - then poke\n * proximity, then ray hits), applies press/grab transitions with\n * hysteresis, runs gaze gating + dwell, ticks every behaviour, and emits\n * interaction events and feedback intents. It never touches an engine:\n * space queries and transform writes go through the adapter-implemented\n * {@link HitTester} and {@link TransformPort}.\n *\n * Lifecycle is first-class (the pale-signal gap): interactables register,\n * unregister, enable and disable; behaviours disabled by capability\n * negotiation announce themselves with a `behaviourDisabled` event.\n */\nimport {\n SELECT_PRESS_THRESHOLD,\n SELECT_RELEASE_THRESHOLD,\n unmetRequirements,\n type InputCapabilities,\n type InputHitHint,\n type InputProvider,\n type InputSourceSnapshot,\n type PoseTuple,\n type RayTuple,\n type Unsubscribe,\n} from \"@realitycollective/webxr-input\";\nimport type { Behaviour, InteractorInfo } from \"./behaviours/behaviour.js\";\nimport {\n createBehaviour,\n type BehaviourConfig,\n type BehaviourFactoryContext,\n} from \"./behaviours/factory.js\";\nimport { GrabBehaviour } from \"./behaviours/grab.js\";\nimport type { InteractableDescriptor } from \"./descriptor.js\";\nimport { Emitter, type InteractionEvent, type InteractionEventListener } from \"./events.js\";\nimport type { FeedbackIntent, FeedbackListener } from \"./feedback.js\";\nimport { DwellState, resolveDwellConfig, DWELL_DEFAULTS, type DwellConfig } from \"./gaze.js\";\nimport { vApplyQuat } from \"./math.js\";\nimport type { HitTester, TransformPort } from \"./ports.js\";\nimport { VelocityTracker, type VelocityTrackerOptions } from \"./velocity-tracker.js\";\n\nconst DEFAULT_POKE_RADIUS = 0.05;\n/** Interactor id used when gaze is synthesized from the head pose. */\nexport const HEAD_GAZE_INTERACTOR_ID = \"head-gaze\";\n\nexport interface InteractableState {\n id: string;\n enabled: boolean;\n hovered: boolean;\n gazeHovered: boolean;\n pressed: boolean;\n grabbed: boolean;\n /** Primary behaviour's logical value. */\n value: number;\n /** Gaze-dwell meter 0..1 (0 when dwell is not enabled). */\n dwell: number;\n}\n\ninterface BehaviourSlot {\n instance: Behaviour;\n config: BehaviourConfig;\n disabled: boolean;\n disabledReason: string;\n}\n\ninterface Registered {\n def: InteractableDescriptor;\n behaviours: BehaviourSlot[];\n transform: TransformPort | undefined;\n enabled: boolean;\n pokeRadius: number;\n gazeRequired: boolean;\n dwellConfig: Required<DwellConfig> | null;\n dwell: DwellState;\n gazeHovered: boolean;\n hoveredBy: Set<string>;\n pressedBy: Set<string>;\n grabbedBy: string | null;\n}\n\ninterface SourceRuntimeState {\n hoverTarget: string | null;\n pressed: boolean;\n pressTarget: string | null;\n grabbed: boolean;\n grabTarget: string | null;\n}\n\nexport interface InteractionRuntimeOptions {\n provider: InputProvider;\n hitTester?: HitTester;\n dwellDefaults?: DwellConfig;\n /**\n * Per-source velocity tracking. On by default with no smoothing; pass\n * `false` to skip it, or options to smooth. The tracker only fills in\n * what the provider did not supply.\n */\n velocity?: VelocityTrackerOptions | false;\n}\n\nexport interface RegisterPorts {\n transform?: TransformPort;\n}\n\nexport class InteractionRuntime {\n private readonly provider: InputProvider;\n private hitTester: HitTester | null;\n private readonly dwellDefaults: Required<DwellConfig>;\n\n private readonly interactables = new Map<string, Registered>();\n private readonly sourceStates = new Map<string, SourceRuntimeState>();\n private readonly events = new Emitter<InteractionEvent>();\n private readonly feedbackEmitter = new Emitter<FeedbackIntent>();\n private readonly afterSample = new Emitter<readonly InputSourceSnapshot[]>();\n private readonly velocityTracker: VelocityTracker | null;\n private lastSources = new Map<string, InputSourceSnapshot>();\n private capabilities: InputCapabilities;\n private readonly unsubscribeCaps: Unsubscribe;\n private disposed = false;\n\n constructor(options: InteractionRuntimeOptions) {\n this.provider = options.provider;\n this.hitTester = options.hitTester ?? null;\n const d = options.dwellDefaults;\n this.dwellDefaults = {\n holdSeconds: d?.holdSeconds ?? DWELL_DEFAULTS.holdSeconds,\n decayFactor: d?.decayFactor ?? DWELL_DEFAULTS.decayFactor,\n rearmBelow: d?.rearmBelow ?? DWELL_DEFAULTS.rearmBelow,\n };\n this.velocityTracker =\n options.velocity === false ? null : new VelocityTracker(options.velocity ?? {});\n this.capabilities = this.provider.getCapabilities();\n this.unsubscribeCaps = this.provider.onCapabilitiesChanged((caps) => {\n this.capabilities = caps;\n this.renegotiateAll();\n });\n }\n\n // -- registry -------------------------------------------------------------\n\n registerInteractable(def: InteractableDescriptor, ports: RegisterPorts = {}): void {\n if (this.interactables.has(def.id)) {\n throw new Error(`Interactable \"${def.id}\" is already registered`);\n }\n const context: BehaviourFactoryContext = {\n grabFulfilment: this.capabilities.grabs === \"native\" ? \"native\" : \"poseOnly\",\n };\n const registered: Registered = {\n def,\n behaviours: def.behaviours.map((config) => ({\n instance: createBehaviour(config, context),\n config,\n disabled: false,\n disabledReason: \"\",\n })),\n transform: ports.transform,\n enabled: def.enabled !== false,\n pokeRadius: def.pokeRadius ?? DEFAULT_POKE_RADIUS,\n gazeRequired: def.gaze?.required === true,\n dwellConfig: resolveDwellConfig(def.gaze?.dwell, this.dwellDefaults),\n dwell: new DwellState(),\n gazeHovered: false,\n hoveredBy: new Set(),\n pressedBy: new Set(),\n grabbedBy: null,\n };\n this.interactables.set(def.id, registered);\n this.renegotiate(def.id, registered);\n }\n\n unregisterInteractable(id: string): void {\n const registered = this.interactables.get(id);\n if (!registered) return;\n this.releaseAllHolds(id);\n this.interactables.delete(id);\n }\n\n setInteractableEnabled(id: string, enabled: boolean): void {\n const registered = this.interactables.get(id);\n if (!registered || registered.enabled === enabled) return;\n registered.enabled = enabled;\n if (!enabled) this.releaseAllHolds(id);\n this.events.emit({\n type: enabled ? \"interactableEnabled\" : \"interactableDisabled\",\n interactableId: id,\n });\n }\n\n setHitTester(hitTester: HitTester | null): void {\n this.hitTester = hitTester;\n }\n\n /** Reach a live behaviour instance for tuning (playground steppers). */\n getBehaviour<T extends Behaviour = Behaviour>(interactableId: string, kind: string): T | undefined {\n const registered = this.interactables.get(interactableId);\n return registered?.behaviours.find((b) => b.instance.kind === kind)?.instance as T | undefined;\n }\n\n getState(id: string): InteractableState | undefined {\n const r = this.interactables.get(id);\n if (!r) return undefined;\n const primary = r.behaviours.find((b) => !b.disabled)?.instance;\n return {\n id,\n enabled: r.enabled,\n hovered: r.hoveredBy.size > 0 || r.gazeHovered,\n gazeHovered: r.gazeHovered,\n pressed: r.pressedBy.size > 0,\n grabbed: r.grabbedBy !== null,\n value: primary?.getValue() ?? 0,\n dwell: r.dwell.getProgress(),\n };\n }\n\n getInteractableIds(): string[] {\n return [...this.interactables.keys()];\n }\n\n // -- subscriptions ----------------------------------------------------------\n\n onEvent(listener: InteractionEventListener): Unsubscribe {\n return this.events.subscribe(listener);\n }\n\n onFeedback(listener: FeedbackListener): Unsubscribe {\n return this.feedbackEmitter.subscribe(listener);\n }\n\n /**\n * Sampled sources for this frame - the UI Extensions pointer bridge hook.\n *\n * @deprecated Use {@link onSample}. Both deliver the same stream: since\n * `@realitycollective/webxr-input` 0.1.1 carries `linearVelocity` and\n * `angularVelocity` on `InputSourceSnapshot`, the two signatures are\n * identical and only one name is needed. This one is kept so existing\n * callers keep working and will be removed in a later major release.\n */\n onSourcesSampled(listener: (sources: readonly InputSourceSnapshot[]) => void): Unsubscribe {\n return this.afterSample.subscribe(listener);\n }\n\n /**\n * This frame's sources, velocity included.\n *\n * The snapshots are the provider's own objects for this frame and are\n * never written to again once delivered (the ownership rule on\n * `InputSourceSnapshot`), so a listener may keep one, or a tuple inside\n * it, across frames without copying.\n */\n onSample(listener: (sources: readonly InputSourceSnapshot[]) => void): Unsubscribe {\n return this.afterSample.subscribe(listener);\n }\n\n /**\n * The last sample of one source, velocity included. Undefined before the\n * first update, and once the source stops reporting.\n *\n * The snapshot is the caller's to keep: it is never mutated after the\n * frame it was sampled in, and it is not refreshed in place, so call\n * again for a newer one.\n */\n getSource(id: string): InputSourceSnapshot | undefined {\n return this.lastSources.get(id);\n }\n\n getCapabilities(): InputCapabilities {\n return this.capabilities;\n }\n\n getProvider(): InputProvider {\n return this.provider;\n }\n\n // -- frame ------------------------------------------------------------------\n\n update(dt: number): void {\n if (this.disposed) return;\n const sampled = this.provider.sample();\n const sources = this.velocityTracker ? this.velocityTracker.update(sampled, dt) : sampled;\n const hints = this.provider.sampleHints?.() ?? [];\n this.lastSources = new Map(sources.map((source) => [source.id, source]));\n this.afterSample.emit(sources);\n\n const hintBySource = new Map<string, InputHitHint[]>();\n for (const hint of hints) {\n const list = hintBySource.get(hint.sourceId);\n if (list) list.push(hint);\n else hintBySource.set(hint.sourceId, [hint]);\n }\n\n const seen = new Set<string>();\n for (const source of sources) {\n seen.add(source.id);\n this.updateSource(source, hintBySource.get(source.id) ?? []);\n }\n // Sources that vanished mid-hold release everything they held.\n for (const [id, state] of this.sourceStates) {\n if (!seen.has(id)) {\n this.forceRelease(id, state);\n this.sourceStates.delete(id);\n }\n }\n\n this.updateGaze(sources, dt);\n this.tickBehaviours(sources, dt);\n }\n\n dispose(): void {\n if (this.disposed) return;\n this.disposed = true;\n // A disposed runtime still owes every held grab exactly one `endHold`\n // (zero velocity - nothing threw it away, the scene just went down),\n // so physics never stays suspended once nothing is left to resume it.\n for (const [id, registered] of this.interactables) {\n if (registered.grabbedBy) {\n this.endGrab(id, registered.grabbedBy, {\n id: registered.grabbedBy,\n kind: \"other\",\n select: 0,\n squeeze: 0,\n });\n }\n }\n this.unsubscribeCaps();\n this.interactables.clear();\n this.sourceStates.clear();\n this.lastSources.clear();\n this.velocityTracker?.reset();\n }\n\n // -- internals ---------------------------------------------------------------\n\n private sourceState(id: string): SourceRuntimeState {\n let state = this.sourceStates.get(id);\n if (!state) {\n state = { hoverTarget: null, pressed: false, pressTarget: null, grabbed: false, grabTarget: null };\n this.sourceStates.set(id, state);\n }\n return state;\n }\n\n private interactorInfo(source: InputSourceSnapshot): InteractorInfo {\n const info: InteractorInfo = {\n id: source.id,\n kind: source.kind,\n select: source.select,\n squeeze: source.squeeze,\n };\n if (source.ray) info.ray = source.ray;\n if (source.gripPose) info.gripPose = source.gripPose;\n if (source.indexTip) info.indexTip = source.indexTip;\n if (source.linearVelocity) info.linearVelocity = source.linearVelocity;\n if (source.angularVelocity) info.angularVelocity = source.angularVelocity;\n return info;\n }\n\n /** Resolve this source's target: hints beat poke beats ray. */\n private resolveTarget(source: InputSourceSnapshot, hints: InputHitHint[]): string | null {\n const hint =\n hints.find((h) => h.state === \"grab\") ??\n hints.find((h) => h.state === \"press\") ??\n hints.find((h) => h.state === \"hover\");\n if (hint && this.targetable(hint.targetId)) return hint.targetId;\n if (source.indexTip && this.hitTester) {\n // One proximity query at the largest radius any interactable asked for,\n // then the nearest hit must be inside ITS OWN radius. A per-interactable\n // query would be a hit test per registration per source per frame; this\n // keeps it to one. The trade: a farther interactable with a bigger radius\n // is not found behind a nearer one with a smaller radius, because the\n // tester returns only the nearest.\n const poke = this.hitTester.hitProximity(source.indexTip, this.maxPokeRadius());\n if (poke && this.targetable(poke.interactableId)) {\n const registered = this.interactables.get(poke.interactableId)!;\n if (poke.distance <= registered.pokeRadius) return poke.interactableId;\n }\n }\n if (source.ray && this.hitTester) {\n const hit = this.hitTester.hitRay(source.ray);\n if (hit && this.targetable(hit.interactableId)) return hit.interactableId;\n }\n return null;\n }\n\n private targetable(id: string): boolean {\n const registered = this.interactables.get(id);\n return registered !== undefined && registered.enabled;\n }\n\n /** The largest poke radius among enabled interactables; the default when none is registered. */\n private maxPokeRadius(): number {\n let radius = 0;\n for (const registered of this.interactables.values()) {\n if (registered.enabled && registered.pokeRadius > radius) radius = registered.pokeRadius;\n }\n return radius > 0 ? radius : DEFAULT_POKE_RADIUS;\n }\n\n private flags(registered: Registered): { pressable: boolean; grabbable: boolean } {\n let pressable = false;\n let grabbable = false;\n for (const slot of registered.behaviours) {\n if (slot.disabled) continue;\n if (slot.instance.onPressStart) pressable = true;\n if (slot.instance.grabbable) grabbable = true;\n }\n return { pressable, grabbable };\n }\n\n private updateSource(source: InputSourceSnapshot, hints: InputHitHint[]): void {\n const state = this.sourceState(source.id);\n const target = this.resolveTarget(source, hints);\n\n // Hover transitions.\n if (state.hoverTarget !== target) {\n if (state.hoverTarget) this.setHover(state.hoverTarget, source.id, false);\n if (target) this.setHover(target, source.id, true);\n state.hoverTarget = target;\n }\n\n const registered = target ? this.interactables.get(target) : undefined;\n const roles = registered ? this.flags(registered) : { pressable: false, grabbable: false };\n\n // Signal resolution. A pinching hand (or a 2D pointer click) acts as a\n // grab when the target is grab-only; controllers keep trigger=press,\n // squeeze=grab.\n const selectAsGrab = roles.grabbable && !roles.pressable && source.kind !== \"controller\";\n const pressSignal = selectAsGrab ? 0 : source.select;\n const grabSignal = Math.max(source.squeeze, selectAsGrab ? source.select : 0);\n\n const hintPress = hints.some((h) => h.state === \"press\");\n const hintGrab = hints.some((h) => h.state === \"grab\") || source.nativeGrabbing === true;\n\n // Press with hysteresis (hints override).\n const pressActive = hintPress || (state.pressed\n ? pressSignal > SELECT_RELEASE_THRESHOLD\n : pressSignal >= SELECT_PRESS_THRESHOLD);\n if (pressActive && !state.pressed) {\n state.pressed = true;\n if (target && registered && roles.pressable && this.gazeAllows(registered)) {\n state.pressTarget = target;\n this.routePressStart(target, registered, this.interactorInfo(source));\n }\n } else if (!pressActive && state.pressed) {\n state.pressed = false;\n if (state.pressTarget) {\n this.routePressEnd(state.pressTarget, source.id, this.interactorInfo(source));\n state.pressTarget = null;\n }\n }\n\n // Grab with hysteresis (hints / native grabbing override).\n const grabActive = hintGrab || (state.grabbed\n ? grabSignal > SELECT_RELEASE_THRESHOLD\n : grabSignal >= SELECT_PRESS_THRESHOLD);\n if (grabActive && !state.grabbed) {\n state.grabbed = true;\n const hintTargetId = hints.find((h) => h.state === \"grab\")?.targetId;\n const grabTargetId = hintTargetId && this.targetable(hintTargetId) ? hintTargetId : target;\n const grabTarget = grabTargetId ? this.interactables.get(grabTargetId) : undefined;\n if (\n grabTargetId &&\n grabTarget &&\n this.flags(grabTarget).grabbable &&\n grabTarget.grabbedBy === null &&\n this.gazeAllows(grabTarget)\n ) {\n state.grabTarget = grabTargetId;\n grabTarget.grabbedBy = source.id;\n const info = this.interactorInfo(source);\n for (const slot of grabTarget.behaviours) {\n if (!slot.disabled && slot.instance.grabbable) {\n slot.instance.onGrabStart?.(this.contextFor(grabTargetId, grabTarget, 0), info);\n }\n }\n }\n } else if (!grabActive && state.grabbed) {\n state.grabbed = false;\n if (state.grabTarget) {\n this.endGrab(state.grabTarget, source.id, this.interactorInfo(source));\n state.grabTarget = null;\n }\n }\n }\n\n private gazeAllows(registered: Registered): boolean {\n return !registered.gazeRequired || registered.gazeHovered;\n }\n\n private setHover(id: string, sourceId: string, hovered: boolean): void {\n const registered = this.interactables.get(id);\n if (!registered) return;\n if (hovered) {\n registered.hoveredBy.add(sourceId);\n if (registered.hoveredBy.size === 1) {\n this.events.emit({ type: \"hoverEnter\", interactableId: id, interactorId: sourceId });\n this.feedbackEmitter.emit({\n cue: \"hover\",\n interactableId: id,\n sourceId,\n intensity: 0.1,\n durationMs: 10,\n });\n }\n } else {\n registered.hoveredBy.delete(sourceId);\n if (registered.hoveredBy.size === 0) {\n this.events.emit({ type: \"hoverExit\", interactableId: id, interactorId: sourceId });\n }\n }\n }\n\n private routePressStart(id: string, registered: Registered, info: InteractorInfo): void {\n registered.pressedBy.add(info.id);\n this.events.emit({ type: \"pressStart\", interactableId: id, interactorId: info.id });\n const ctx = this.contextFor(id, registered, 0);\n for (const slot of registered.behaviours) {\n if (!slot.disabled) slot.instance.onPressStart?.(ctx, info);\n }\n }\n\n private routePressEnd(id: string, sourceId: string, info: InteractorInfo): void {\n const registered = this.interactables.get(id);\n if (!registered) return;\n registered.pressedBy.delete(sourceId);\n this.events.emit({ type: \"pressEnd\", interactableId: id, interactorId: sourceId });\n const ctx = this.contextFor(id, registered, 0);\n for (const slot of registered.behaviours) {\n if (!slot.disabled) slot.instance.onPressEnd?.(ctx, info);\n }\n }\n\n private endGrab(id: string, sourceId: string, info: InteractorInfo): void {\n const registered = this.interactables.get(id);\n if (!registered || registered.grabbedBy !== sourceId) return;\n registered.grabbedBy = null;\n const ctx = this.contextFor(id, registered, 0);\n for (const slot of registered.behaviours) {\n if (!slot.disabled && slot.instance.grabbable) slot.instance.onGrabEnd?.(ctx, info);\n }\n }\n\n private forceRelease(sourceId: string, state: SourceRuntimeState): void {\n const info: InteractorInfo = { id: sourceId, kind: \"other\", select: 0, squeeze: 0 };\n if (state.pressTarget) this.routePressEnd(state.pressTarget, sourceId, info);\n if (state.grabTarget) this.endGrab(state.grabTarget, sourceId, info);\n if (state.hoverTarget) this.setHover(state.hoverTarget, sourceId, false);\n }\n\n private releaseAllHolds(id: string): void {\n for (const [sourceId, state] of this.sourceStates) {\n if (state.pressTarget === id) {\n this.routePressEnd(id, sourceId, { id: sourceId, kind: \"other\", select: 0, squeeze: 0 });\n state.pressTarget = null;\n state.pressed = false;\n }\n if (state.grabTarget === id) {\n this.endGrab(id, sourceId, { id: sourceId, kind: \"other\", select: 0, squeeze: 0 });\n state.grabTarget = null;\n state.grabbed = false;\n }\n if (state.hoverTarget === id) {\n this.setHover(id, sourceId, false);\n state.hoverTarget = null;\n }\n }\n }\n\n private updateGaze(sources: readonly InputSourceSnapshot[], dt: number): void {\n let gazeRay: RayTuple | null = null;\n let gazeId = HEAD_GAZE_INTERACTOR_ID;\n const gazeSource = sources.find((s) => s.kind === \"gaze\");\n if (gazeSource?.ray) {\n gazeRay = gazeSource.ray;\n gazeId = gazeSource.id;\n } else if (this.capabilities.headPose && this.provider.getHeadPose) {\n const head: PoseTuple = this.provider.getHeadPose();\n gazeRay = {\n origin: head.position,\n direction: vApplyQuat([0, 0, -1], head.quaternion),\n };\n }\n\n const gazeTarget =\n gazeRay && this.hitTester ? this.hitTester.hitRay(gazeRay)?.interactableId ?? null : null;\n\n for (const [id, registered] of this.interactables) {\n const hovered = registered.enabled && gazeTarget === id;\n if (hovered !== registered.gazeHovered) {\n registered.gazeHovered = hovered;\n this.events.emit({\n type: hovered ? \"hoverEnter\" : \"hoverExit\",\n interactableId: id,\n interactorId: gazeId,\n });\n }\n if (registered.dwellConfig && registered.enabled) {\n const tick = registered.dwell.update(hovered, dt, registered.dwellConfig);\n if (tick.changed) {\n this.events.emit({\n type: \"dwellProgress\",\n interactableId: id,\n interactorId: gazeId,\n value: tick.progress,\n });\n }\n if (tick.fired) {\n // A completed dwell is a synthesized click: press + release.\n const info: InteractorInfo = { id: gazeId, kind: \"gaze\", select: 1, squeeze: 0 };\n this.routePressStart(id, registered, info);\n this.routePressEnd(id, gazeId, { ...info, select: 0 });\n this.feedbackEmitter.emit({\n cue: \"dwellComplete\",\n interactableId: id,\n intensity: 0.6,\n durationMs: 50,\n });\n }\n }\n }\n }\n\n private tickBehaviours(sources: readonly InputSourceSnapshot[], dt: number): void {\n const bySourceId = new Map(sources.map((s) => [s.id, s]));\n for (const [id, registered] of this.interactables) {\n if (!registered.enabled) continue;\n const holderSource = registered.grabbedBy ? bySourceId.get(registered.grabbedBy) : undefined;\n const holder = holderSource ? this.interactorInfo(holderSource) : undefined;\n const ctx = this.contextFor(id, registered, dt);\n for (const slot of registered.behaviours) {\n if (slot.disabled) continue;\n slot.instance.update(ctx, slot.instance.grabbable ? holder : undefined);\n }\n }\n }\n\n private contextFor(id: string, registered: Registered, dt: number) {\n return {\n dt,\n interactableId: id,\n transform: registered.transform,\n emit: (event: Omit<InteractionEvent, \"interactableId\">) =>\n this.events.emit({ ...event, interactableId: id }),\n feedback: (intent: Omit<FeedbackIntent, \"interactableId\">) =>\n this.feedbackEmitter.emit({ ...intent, interactableId: id }),\n getWorldPose: (otherId: string) =>\n this.interactables.get(otherId)?.transform?.getWorldPose(),\n };\n }\n\n private renegotiate(id: string, registered: Registered): void {\n const fulfilment = this.capabilities.grabs === \"native\" ? \"native\" : \"poseOnly\";\n for (const slot of registered.behaviours) {\n if (slot.instance instanceof GrabBehaviour) {\n slot.instance.setFulfilment(fulfilment);\n }\n const unmet = [...unmetRequirements(this.capabilities, slot.instance.requires)];\n for (const group of slot.instance.requiresAnyOf ?? []) {\n if (group.length > 0 && unmetRequirements(this.capabilities, group).length === group.length) {\n unmet.push(group[0]!);\n }\n }\n const disabled = unmet.length > 0;\n if (disabled !== slot.disabled) {\n slot.disabled = disabled;\n slot.disabledReason = disabled ? `unmet input capabilities: ${unmet.join(\", \")}` : \"\";\n this.events.emit({\n type: disabled ? \"behaviourDisabled\" : \"behaviourEnabled\",\n interactableId: id,\n behaviourKind: slot.instance.kind,\n ...(disabled ? { reason: slot.disabledReason } : {}),\n });\n }\n }\n }\n\n private renegotiateAll(): void {\n for (const [id, registered] of this.interactables) {\n this.renegotiate(id, registered);\n }\n }\n}\n\n/** Build a runtime's interactables from a portable descriptor. */\nexport function registerDescriptor(\n runtime: InteractionRuntime,\n descriptor: { interactables: InteractableDescriptor[] },\n portsFor: (id: string) => RegisterPorts = () => ({}),\n): void {\n for (const interactable of descriptor.interactables) {\n runtime.registerInteractable(interactable, portsFor(interactable.id));\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@realitycollective/webxr-interactions",
3
- "version": "0.1.1-preview.0",
3
+ "version": "0.1.1-preview.1",
4
4
  "description": "Engine-free core of the Reality Collective Interaction Extensions: interactables, interactors, behaviours (press, hinge, dial, slide, grab, toss-scoring, pulse, gaze-dwell), capability negotiation and feedback hooks - driven through the shared @realitycollective/webxr-input contracts. Pair with an engine adapter: threejs-interactions, iwsdk-interactions or xrblocks-interactions.",
5
5
  "keywords": [
6
6
  "realitycollective",