@realitycollective/webxr-interactions 0.1.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
@@ -4,6 +4,29 @@ Change log for the Reality Collective WebXR Interaction Extensions packages. All
4
4
 
5
5
  The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Preview builds are not listed separately. The entry for a version accumulates while its previews are published, and is dated when that version is released.
6
6
 
7
+ ## [0.1.1]
8
+
9
+ ### Added
10
+
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
+ - `@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.
17
+
18
+ ### Changed
19
+
20
+ - `@realitycollective/webxr-interactions` - `TransformPort.getWorldPose()` is the LIVE world pose on every platform, and the new required `getRestWorldPose()` returns the rest pose captured at registration, in world space. The core only said "current world pose", and the platforms disagreed: the three.js, IWSDK and XR Blocks ports returned the rest pose, while the Babylon.js port returned the live one. The same behaviour therefore acted differently by platform. The hinge, slide and dial behaviours measure the hand against the rest pose, so they now call `getRestWorldPose()`. Grab and toss scoring keep `getWorldPose()` and now see where the object really is. `ports.ts` also states tuple ownership for `TransformPort` and `HitTester`: every returned tuple is fresh and owned by the caller, and a tuple passed in is read during the call and not kept. Every platform already met the ownership rule; the suites now hold them to it. The native `interactions` slice gains `getRestWorldPose(targetId)`, which the native app must implement. This adds a required member to `TransformPort`, so a port written outside this repository must implement it.
21
+ - `@realitycollective/xrblocks-interactions` - the structural slices `XBVec3Like`, `XBQuatLike`, `XBRayLike` and `XBControllerLike` are exported, under the same `XB` prefix as `XBRaySourceLike`, `XBDirectTouchLike` and `XBFrameLike` whose fields they type. They were module-private, so a consumer building a fake frame for a test had nothing to name, and TypeDoc reported each as referenced but undocumented.
22
+
23
+ ### Fixed
24
+
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.
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.
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.
29
+
7
30
  ## [0.1.0] - 2026-09-17
8
31
 
9
32
  ### Added
@@ -57,4 +80,5 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
57
80
  - All five packages depend on `@realitycollective/webxr-input`, released independently from the [WebXR-Input](https://github.com/realitycollective/WebXR-Input) repository. That package must be published before this one.
58
81
  - The interaction packages themselves carry no IWSDK coupling beyond the adapter: `webxr-interactions` is engine-free, and `threejs-interactions` and `xrblocks-interactions` peer only on three.js.
59
82
 
83
+ [0.1.1]: https://github.com/realitycollective/WebXR-Interactions/compare/v0.1.0...development
60
84
  [0.1.0]: https://github.com/realitycollective/WebXR-Interactions/releases/tag/v0.1.0
package/README.md CHANGED
@@ -29,6 +29,10 @@ All input arrives through the shared [`@realitycollective/webxr-input`](https://
29
29
  - **Events out, never callbacks in.** The core never reaches into your objects.
30
30
  - **Fail loudly, not silently.** A behaviour the hardware cannot support reports `behaviourDisabled` instead of quietly doing nothing.
31
31
 
32
+ ## Conformance suites
33
+
34
+ Every adapter implements `HitTester` and `TransformPort` (see `ports.ts`) against its own scene graph, so this package ships the checks as data rather than as tests: `hitTesterContractCases()` and `transformPortContractCases()`, alongside `inputProviderContractCases()` from [`@realitycollective/webxr-input`](https://www.npmjs.com/package/@realitycollective/webxr-input) for `InputProvider`. Each case throws a plain `Error` naming the rule a platform broke, so any test runner can host it - an adapter written outside this repository runs the same suite its own way. All three run against every platform in `packages/iwsdk-interactions/test/`, next to each other: `provider-parity.test.ts` for `InputProvider`, `port-parity.test.ts` for `HitTester` and `TransformPort`.
35
+
32
36
  ## Live demo
33
37
 
34
38
  The interaction playground - the full station set, mouse-capable on desktop, VR button for headsets: **[webxr-interactions.pages.dev](https://webxr-interactions.pages.dev)**
@@ -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
  }
@@ -35,7 +44,7 @@ export interface BehaviourContext {
35
44
  transform: TransformPort | undefined;
36
45
  emit(event: Omit<InteractionEvent, "interactableId">): void;
37
46
  feedback(intent: Omit<FeedbackIntent, "interactableId">): void;
38
- /** REST world pose of another registered interactable (toss scoring). */
47
+ /** LIVE world pose of another registered interactable (toss scoring). */
39
48
  getWorldPose(interactableId: string): PoseTuple | undefined;
40
49
  }
41
50
  export interface Behaviour {
@@ -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 /** REST 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"]}
@@ -21,7 +21,7 @@ export class DialBehaviour {
21
21
  const transform = ctx.transform;
22
22
  if (!hand || !transform)
23
23
  return null;
24
- const rest = transform.getWorldPose();
24
+ const rest = transform.getRestWorldPose();
25
25
  const local = worldToLocal(hand, rest.position, rest.quaternion);
26
26
  // Project onto the plane perpendicular to the axis.
27
27
  const along = vDot(local, this.axis);
@@ -1 +1 @@
1
- {"version":3,"file":"dial.js","sourceRoot":"","sources":["../../src/behaviours/dial.ts"],"names":[],"mappings":"AAQA,OAAO,EACL,WAAW,EACX,iBAAiB,EACjB,MAAM,EACN,IAAI,EACJ,UAAU,EACV,IAAI,EACJ,YAAY,GACb,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,WAAW,EAA8D,MAAM,gBAAgB,CAAC;AAUzG,MAAM,aAAa,GAAG,IAAI,CAAC;AAE3B,MAAM,OAAO,aAAa;IACf,IAAI,GAAG,MAAM,CAAC;IACd,SAAS,GAAG,YAAqB,CAAC;IAClC,QAAQ,GAAG,CAAC,OAAO,CAAU,CAAC;IAC9B,SAAS,GAAG,IAAI,CAAC;IAET,IAAI,CAAY;IACzB,QAAQ,CAAS;IACjB,KAAK,GAAG,CAAC,CAAC;IACV,WAAW,GAAG,CAAC,CAAC;IAChB,UAAU,GAAqB,IAAI,CAAC;IACpC,WAAW,GAAG,CAAC,CAAC,CAAC;IAEzB,YAAY,SAAmC,EAAE;QAC/C,IAAI,CAAC,IAAI,GAAG,MAAM,CAAC,IAAI,IAAI,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;QACrC,IAAI,CAAC,QAAQ,GAAG,MAAM,CAAC,QAAQ,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC;IACvD,CAAC;IAEO,SAAS,CAAC,GAAqB,EAAE,MAAsB;QAC7D,MAAM,IAAI,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC;QACjC,MAAM,SAAS,GAAG,GAAG,CAAC,SAAS,CAAC;QAChC,IAAI,CAAC,IAAI,IAAI,CAAC,SAAS;YAAE,OAAO,IAAI,CAAC;QACrC,MAAM,IAAI,GAAG,SAAS,CAAC,YAAY,EAAE,CAAC;QACtC,MAAM,KAAK,GAAG,YAAY,CAAC,IAAI,EAAE,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;QACjE,oDAAoD;QACpD,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;QACrC,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,EAAE;YACzB,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,KAAK;YACpB,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,KAAK;YACpB,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,KAAK;SACrB,CAAC,CAAC;QACH,MAAM,CAAC,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC;QAC7B,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;IAC3D,CAAC;IAED,WAAW,CAAC,GAAqB,EAAE,UAA0B;QAC3D,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,UAAU,CAAC,CAAC;QAClD,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,KAAK,CAAC;QAC9B,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,aAAa,EAAE,IAAI,CAAC,IAAI,EAAE,YAAY,EAAE,UAAU,CAAC,EAAE,EAAE,CAAC,CAAC;QACvF,GAAG,CAAC,QAAQ,CAAC;YACX,GAAG,EAAE,MAAM;YACX,aAAa,EAAE,IAAI,CAAC,IAAI;YACxB,QAAQ,EAAE,UAAU,CAAC,EAAE;YACvB,SAAS,EAAE,GAAG;YACd,UAAU,EAAE,EAAE;SACf,CAAC,CAAC;IACL,CAAC;IAED,SAAS,CAAC,GAAqB,EAAE,UAA0B;QACzD,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC;QACvB,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,aAAa,EAAE,IAAI,CAAC,IAAI,EAAE,YAAY,EAAE,UAAU,CAAC,EAAE,EAAE,CAAC,CAAC;QACrF,GAAG,CAAC,QAAQ,CAAC;YACX,GAAG,EAAE,MAAM;YACX,aAAa,EAAE,IAAI,CAAC,IAAI;YACxB,QAAQ,EAAE,UAAU,CAAC,EAAE;YACvB,SAAS,EAAE,GAAG;YACd,UAAU,EAAE,EAAE;SACf,CAAC,CAAC;IACL,CAAC;IAED,MAAM,CAAC,GAAqB,EAAE,MAAuB;QACnD,IAAI,MAAM,IAAI,IAAI,CAAC,UAAU,IAAI,GAAG,CAAC,SAAS,EAAE,CAAC;YAC/C,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;YAC5C,IAAI,OAAO,EAAE,CAAC;gBACZ,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;gBAC/C,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,CAAC;gBACjF,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,WAAW,GAAG,KAAK,CAAC,CAAC,CAAC;gBAC5E,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;gBAClB,GAAG,CAAC,SAAS,CAAC,gBAAgB,CAAC,iBAAiB,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;YAC3E,CAAC;QACH,CAAC;QACD,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC;QAC9B,IAAI,IAAI,CAAC,GAAG,CAAC,KAAK,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,aAAa,EAAE,CAAC;YACvD,IAAI,CAAC,WAAW,GAAG,KAAK,CAAC;YACzB,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,cAAc,EAAE,aAAa,EAAE,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QACtE,CAAC;IACH,CAAC;IAED,QAAQ;QACN,OAAO,WAAW,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;IAChD,CAAC;IAED,WAAW,CAAC,QAAgB;QAC1B,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,IAAI,CAAC,KAAK,GAAG,QAAQ;YAAE,IAAI,CAAC,KAAK,GAAG,QAAQ,CAAC;IACnD,CAAC;IAED,WAAW;QACT,OAAO,IAAI,CAAC,QAAQ,CAAC;IACvB,CAAC;CACF","sourcesContent":["/**\n * Dial - a twistable knob. `handDriven`: while held, the hand's bearing\n * around the dial axis accumulates a clamped rotation `[0, maxAngle]`;\n * publishes `normalize01(angle, maxAngle)`. Works from any provider that\n * can grab and report a hand/grip position (the research prototype's\n * engine-dragged variant becomes an adapter option later).\n */\nimport type { Vec3Tuple } from \"@realitycollective/webxr-input\";\nimport {\n normalize01,\n quatFromAxisAngle,\n vCross,\n vDot,\n vNormalize,\n vSub,\n worldToLocal,\n} from \"../math.js\";\nimport { holderPoint, type Behaviour, type BehaviourContext, type InteractorInfo } from \"./behaviour.js\";\n\nexport interface DialConfig {\n kind: \"dial\";\n /** Local twist axis. */\n axis?: Vec3Tuple;\n /** Max rotation from rest (radians). */\n maxAngle?: number;\n}\n\nconst VALUE_EPSILON = 1e-3;\n\nexport class DialBehaviour implements Behaviour {\n readonly kind = \"dial\";\n readonly ownership = \"handDriven\" as const;\n readonly requires = [\"grabs\"] as const;\n readonly grabbable = true;\n\n private readonly axis: Vec3Tuple;\n private maxAngle: number;\n private angle = 0;\n private angleAtGrab = 0;\n private refBearing: Vec3Tuple | null = null;\n private lastEmitted = -1;\n\n constructor(config: Omit<DialConfig, \"kind\"> = {}) {\n this.axis = config.axis ?? [0, 1, 0];\n this.maxAngle = config.maxAngle ?? (3 * Math.PI) / 2;\n }\n\n private bearingOf(ctx: BehaviourContext, holder: InteractorInfo): Vec3Tuple | null {\n const hand = holderPoint(holder);\n const transform = ctx.transform;\n if (!hand || !transform) return null;\n const rest = transform.getWorldPose();\n const local = worldToLocal(hand, rest.position, rest.quaternion);\n // Project onto the plane perpendicular to the axis.\n const along = vDot(local, this.axis);\n const planar = vSub(local, [\n this.axis[0] * along,\n this.axis[1] * along,\n this.axis[2] * along,\n ]);\n const n = vNormalize(planar);\n return n[0] === 0 && n[1] === 0 && n[2] === 0 ? null : n;\n }\n\n onGrabStart(ctx: BehaviourContext, interactor: InteractorInfo): void {\n this.refBearing = this.bearingOf(ctx, interactor);\n this.angleAtGrab = this.angle;\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.4,\n durationMs: 25,\n });\n }\n\n onGrabEnd(ctx: BehaviourContext, interactor: InteractorInfo): void {\n this.refBearing = null;\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.2,\n durationMs: 15,\n });\n }\n\n update(ctx: BehaviourContext, holder?: InteractorInfo): void {\n if (holder && this.refBearing && ctx.transform) {\n const current = this.bearingOf(ctx, holder);\n if (current) {\n const cross = vCross(this.refBearing, current);\n const delta = Math.atan2(vDot(cross, this.axis), vDot(this.refBearing, current));\n const next = Math.min(this.maxAngle, Math.max(0, this.angleAtGrab + delta));\n this.angle = next;\n ctx.transform.setLocalRotation(quatFromAxisAngle(this.axis, this.angle));\n }\n }\n const value = this.getValue();\n if (Math.abs(value - this.lastEmitted) > VALUE_EPSILON) {\n this.lastEmitted = value;\n ctx.emit({ type: \"valueChanged\", behaviourKind: this.kind, value });\n }\n }\n\n getValue(): number {\n return normalize01(this.angle, this.maxAngle);\n }\n\n setMaxAngle(maxAngle: number): void {\n this.maxAngle = maxAngle;\n if (this.angle > maxAngle) this.angle = maxAngle;\n }\n\n getMaxAngle(): number {\n return this.maxAngle;\n }\n}\n"]}
1
+ {"version":3,"file":"dial.js","sourceRoot":"","sources":["../../src/behaviours/dial.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Dial - a twistable knob. `handDriven`: while held, the hand's bearing\n * around the dial axis accumulates a clamped rotation `[0, maxAngle]`;\n * publishes `normalize01(angle, maxAngle)`. Works from any provider that\n * can grab and report a hand/grip position (the research prototype's\n * engine-dragged variant becomes an adapter option later).\n */\nimport type { Vec3Tuple } from \"@realitycollective/webxr-input\";\nimport {\n normalize01,\n quatFromAxisAngle,\n vCross,\n vDot,\n vNormalize,\n vSub,\n worldToLocal,\n} from \"../math.js\";\nimport { holderPoint, type Behaviour, type BehaviourContext, type InteractorInfo } from \"./behaviour.js\";\n\nexport interface DialConfig {\n kind: \"dial\";\n /** Local twist axis. */\n axis?: Vec3Tuple;\n /** Max rotation from rest (radians). */\n maxAngle?: number;\n}\n\nconst VALUE_EPSILON = 1e-3;\n\nexport class DialBehaviour implements Behaviour {\n readonly kind = \"dial\";\n readonly ownership = \"handDriven\" as const;\n readonly requires = [\"grabs\"] as const;\n readonly grabbable = true;\n\n private readonly axis: Vec3Tuple;\n private maxAngle: number;\n private angle = 0;\n private angleAtGrab = 0;\n private refBearing: Vec3Tuple | null = null;\n private lastEmitted = -1;\n\n constructor(config: Omit<DialConfig, \"kind\"> = {}) {\n this.axis = config.axis ?? [0, 1, 0];\n this.maxAngle = config.maxAngle ?? (3 * Math.PI) / 2;\n }\n\n private bearingOf(ctx: BehaviourContext, holder: InteractorInfo): Vec3Tuple | null {\n const hand = holderPoint(holder);\n const transform = ctx.transform;\n if (!hand || !transform) return null;\n const rest = transform.getRestWorldPose();\n const local = worldToLocal(hand, rest.position, rest.quaternion);\n // Project onto the plane perpendicular to the axis.\n const along = vDot(local, this.axis);\n const planar = vSub(local, [\n this.axis[0] * along,\n this.axis[1] * along,\n this.axis[2] * along,\n ]);\n const n = vNormalize(planar);\n return n[0] === 0 && n[1] === 0 && n[2] === 0 ? null : n;\n }\n\n onGrabStart(ctx: BehaviourContext, interactor: InteractorInfo): void {\n this.refBearing = this.bearingOf(ctx, interactor);\n this.angleAtGrab = this.angle;\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.4,\n durationMs: 25,\n });\n }\n\n onGrabEnd(ctx: BehaviourContext, interactor: InteractorInfo): void {\n this.refBearing = null;\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.2,\n durationMs: 15,\n });\n }\n\n update(ctx: BehaviourContext, holder?: InteractorInfo): void {\n if (holder && this.refBearing && ctx.transform) {\n const current = this.bearingOf(ctx, holder);\n if (current) {\n const cross = vCross(this.refBearing, current);\n const delta = Math.atan2(vDot(cross, this.axis), vDot(this.refBearing, current));\n const next = Math.min(this.maxAngle, Math.max(0, this.angleAtGrab + delta));\n this.angle = next;\n ctx.transform.setLocalRotation(quatFromAxisAngle(this.axis, this.angle));\n }\n }\n const value = this.getValue();\n if (Math.abs(value - this.lastEmitted) > VALUE_EPSILON) {\n this.lastEmitted = value;\n ctx.emit({ type: \"valueChanged\", behaviourKind: this.kind, value });\n }\n }\n\n getValue(): number {\n return normalize01(this.angle, this.maxAngle);\n }\n\n setMaxAngle(maxAngle: number): void {\n this.maxAngle = maxAngle;\n if (this.angle > maxAngle) this.angle = maxAngle;\n }\n\n getMaxAngle(): number {\n return this.maxAngle;\n }\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"]}
@@ -42,7 +42,7 @@ export class HingeBehaviour {
42
42
  if (holder && ctx.transform) {
43
43
  const hand = holderPoint(holder);
44
44
  if (hand) {
45
- const rest = ctx.transform.getWorldPose();
45
+ const rest = ctx.transform.getRestWorldPose();
46
46
  const local = worldToLocal(hand, rest.position, rest.quaternion);
47
47
  const alongRest = vDot(local, this.restDir);
48
48
  const alongSwing = vDot(local, this.swingDir);
@@ -1 +1 @@
1
- {"version":3,"file":"hinge.js","sourceRoot":"","sources":["../../src/behaviours/hinge.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Hinge - a position-driven lever. `handDriven`: grabbing only marks the\n * hold; the behaviour reads the holder's hand position each frame and\n * swings the arm to point at it (movement, not wrist), bidirectional about\n * rest, clamped to ±maxAngle. Stays where left on release. Publishes a\n * centred 0..1 value (rest = 0.5).\n *\n * All math happens in the interactable's REST local frame, so the mount\n * (table / wall vertical / wall horizontal / any) is simply the parent\n * orientation of the object - no faceYaw plumbing needed.\n */\nimport type { Vec3Tuple } from \"@realitycollective/webxr-input\";\nimport {\n centeredUnit,\n hingeAngleToHand,\n quatFromAxisAngle,\n vCross,\n vDot,\n worldToLocal,\n} from \"../math.js\";\nimport { holderPoint, type Behaviour, type BehaviourContext, type InteractorInfo } from \"./behaviour.js\";\n\nexport interface HingeConfig {\n kind: \"hinge\";\n /** Local hinge axis. */\n axis?: Vec3Tuple;\n /** Local rest direction of the arm. */\n restDir?: Vec3Tuple;\n /** Max swing (radians) either side of rest. */\n maxAngle?: number;\n}\n\nconst VALUE_EPSILON = 1e-3;\n\nexport class HingeBehaviour implements Behaviour {\n readonly kind = \"hinge\";\n readonly ownership = \"handDriven\" as const;\n readonly requires = [\"grabs\"] as const;\n readonly grabbable = true;\n\n private readonly axis: Vec3Tuple;\n private readonly restDir: Vec3Tuple;\n private readonly swingDir: Vec3Tuple;\n private maxAngle: number;\n private angle = 0;\n private lastEmitted = -1;\n\n constructor(config: Omit<HingeConfig, \"kind\"> = {}) {\n this.axis = config.axis ?? [1, 0, 0];\n this.restDir = config.restDir ?? [0, 1, 0];\n this.swingDir = vCross(this.axis, this.restDir);\n this.maxAngle = config.maxAngle ?? Math.PI / 4;\n }\n\n onGrabStart(ctx: BehaviourContext, interactor: InteractorInfo): void {\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.4,\n durationMs: 25,\n });\n }\n\n onGrabEnd(ctx: BehaviourContext, interactor: InteractorInfo): void {\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.2,\n durationMs: 15,\n });\n }\n\n update(ctx: BehaviourContext, holder?: InteractorInfo): void {\n if (holder && ctx.transform) {\n const hand = holderPoint(holder);\n if (hand) {\n const rest = ctx.transform.getWorldPose();\n const local = worldToLocal(hand, rest.position, rest.quaternion);\n const alongRest = vDot(local, this.restDir);\n const alongSwing = vDot(local, this.swingDir);\n this.angle = hingeAngleToHand(alongRest, alongSwing, this.maxAngle);\n ctx.transform.setLocalRotation(quatFromAxisAngle(this.axis, this.angle));\n }\n }\n const value = this.getValue();\n if (Math.abs(value - this.lastEmitted) > VALUE_EPSILON) {\n this.lastEmitted = value;\n ctx.emit({ type: \"valueChanged\", behaviourKind: this.kind, value });\n }\n }\n\n getValue(): number {\n return centeredUnit(this.angle, this.maxAngle);\n }\n\n /** Live-tune the swing range (playground steppers). */\n setMaxAngle(maxAngle: number): void {\n this.maxAngle = maxAngle;\n }\n\n getMaxAngle(): number {\n return this.maxAngle;\n }\n}\n"]}
1
+ {"version":3,"file":"hinge.js","sourceRoot":"","sources":["../../src/behaviours/hinge.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Hinge - a position-driven lever. `handDriven`: grabbing only marks the\n * hold; the behaviour reads the holder's hand position each frame and\n * swings the arm to point at it (movement, not wrist), bidirectional about\n * rest, clamped to ±maxAngle. Stays where left on release. Publishes a\n * centred 0..1 value (rest = 0.5).\n *\n * All math happens in the interactable's REST local frame, so the mount\n * (table / wall vertical / wall horizontal / any) is simply the parent\n * orientation of the object - no faceYaw plumbing needed.\n */\nimport type { Vec3Tuple } from \"@realitycollective/webxr-input\";\nimport {\n centeredUnit,\n hingeAngleToHand,\n quatFromAxisAngle,\n vCross,\n vDot,\n worldToLocal,\n} from \"../math.js\";\nimport { holderPoint, type Behaviour, type BehaviourContext, type InteractorInfo } from \"./behaviour.js\";\n\nexport interface HingeConfig {\n kind: \"hinge\";\n /** Local hinge axis. */\n axis?: Vec3Tuple;\n /** Local rest direction of the arm. */\n restDir?: Vec3Tuple;\n /** Max swing (radians) either side of rest. */\n maxAngle?: number;\n}\n\nconst VALUE_EPSILON = 1e-3;\n\nexport class HingeBehaviour implements Behaviour {\n readonly kind = \"hinge\";\n readonly ownership = \"handDriven\" as const;\n readonly requires = [\"grabs\"] as const;\n readonly grabbable = true;\n\n private readonly axis: Vec3Tuple;\n private readonly restDir: Vec3Tuple;\n private readonly swingDir: Vec3Tuple;\n private maxAngle: number;\n private angle = 0;\n private lastEmitted = -1;\n\n constructor(config: Omit<HingeConfig, \"kind\"> = {}) {\n this.axis = config.axis ?? [1, 0, 0];\n this.restDir = config.restDir ?? [0, 1, 0];\n this.swingDir = vCross(this.axis, this.restDir);\n this.maxAngle = config.maxAngle ?? Math.PI / 4;\n }\n\n onGrabStart(ctx: BehaviourContext, interactor: InteractorInfo): void {\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.4,\n durationMs: 25,\n });\n }\n\n onGrabEnd(ctx: BehaviourContext, interactor: InteractorInfo): void {\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.2,\n durationMs: 15,\n });\n }\n\n update(ctx: BehaviourContext, holder?: InteractorInfo): void {\n if (holder && ctx.transform) {\n const hand = holderPoint(holder);\n if (hand) {\n const rest = ctx.transform.getRestWorldPose();\n const local = worldToLocal(hand, rest.position, rest.quaternion);\n const alongRest = vDot(local, this.restDir);\n const alongSwing = vDot(local, this.swingDir);\n this.angle = hingeAngleToHand(alongRest, alongSwing, this.maxAngle);\n ctx.transform.setLocalRotation(quatFromAxisAngle(this.axis, this.angle));\n }\n }\n const value = this.getValue();\n if (Math.abs(value - this.lastEmitted) > VALUE_EPSILON) {\n this.lastEmitted = value;\n ctx.emit({ type: \"valueChanged\", behaviourKind: this.kind, value });\n }\n }\n\n getValue(): number {\n return centeredUnit(this.angle, this.maxAngle);\n }\n\n /** Live-tune the swing range (playground steppers). */\n setMaxAngle(maxAngle: number): void {\n this.maxAngle = maxAngle;\n }\n\n getMaxAngle(): number {\n return this.maxAngle;\n }\n}\n"]}
@@ -28,7 +28,7 @@ export class SlideBehaviour {
28
28
  const transform = ctx.transform;
29
29
  if (!hand || !transform)
30
30
  return null;
31
- const rest = transform.getWorldPose();
31
+ const rest = transform.getRestWorldPose();
32
32
  const local = worldToLocal(hand, rest.position, rest.quaternion);
33
33
  // Pull is measured along −axis.
34
34
  return -vDot(local, this.axis);
@@ -1 +1 @@
1
- {"version":3,"file":"slide.js","sourceRoot":"","sources":["../../src/behaviours/slide.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AACpE,OAAO,EAAE,WAAW,EAA8D,MAAM,gBAAgB,CAAC;AAYzG,MAAM,aAAa,GAAG,IAAI,CAAC;AAE3B,MAAM,OAAO,cAAc;IAChB,IAAI,GAAG,OAAO,CAAC;IACf,SAAS,GAAG,YAAqB,CAAC;IAClC,QAAQ,GAAG,CAAC,OAAO,CAAU,CAAC;IAC9B,SAAS,GAAG,IAAI,CAAC;IAET,IAAI,CAAY;IACzB,MAAM,CAAS;IACf,SAAS,CAAS;IAClB,OAAO,CAAS;IAEhB,IAAI,GAAG,CAAC,CAAC,CAAC,gCAAgC;IAC1C,QAAQ,GAAG,CAAC,CAAC;IACb,UAAU,GAAG,CAAC,CAAC;IACf,eAAe,GAAG,CAAC,CAAC;IACpB,IAAI,GAAG,KAAK,CAAC;IACb,WAAW,GAAG,CAAC,CAAC,CAAC;IACR,MAAM,GAAG,IAAI,YAAY,CAAC,CAAC,CAAC,CAAC;IAE9C,YAAY,SAAoC,EAAE;QAChD,IAAI,CAAC,IAAI,GAAG,MAAM,CAAC,IAAI,IAAI,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;QACrC,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC,MAAM,IAAI,GAAG,CAAC;QACnC,IAAI,CAAC,SAAS,GAAG,MAAM,CAAC,SAAS,IAAI,GAAG,CAAC;QACzC,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC,OAAO,IAAI,EAAE,CAAC;IACtC,CAAC;IAEO,SAAS,CAAC,GAAqB,EAAE,MAAsB;QAC7D,MAAM,IAAI,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC;QACjC,MAAM,SAAS,GAAG,GAAG,CAAC,SAAS,CAAC;QAChC,IAAI,CAAC,IAAI,IAAI,CAAC,SAAS;YAAE,OAAO,IAAI,CAAC;QACrC,MAAM,IAAI,GAAG,SAAS,CAAC,YAAY,EAAE,CAAC;QACtC,MAAM,KAAK,GAAG,YAAY,CAAC,IAAI,EAAE,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;QACjE,gCAAgC;QAChC,OAAO,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;IACjC,CAAC;IAED,WAAW,CAAC,GAAqB,EAAE,UAA0B;QAC3D,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC;QAC5B,IAAI,CAAC,eAAe,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,UAAU,CAAC,IAAI,CAAC,CAAC;QAC5D,IAAI,CAAC,QAAQ,GAAG,CAAC,CAAC;QAClB,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,aAAa,EAAE,IAAI,CAAC,IAAI,EAAE,YAAY,EAAE,UAAU,CAAC,EAAE,EAAE,CAAC,CAAC;QACvF,GAAG,CAAC,QAAQ,CAAC;YACX,GAAG,EAAE,MAAM;YACX,aAAa,EAAE,IAAI,CAAC,IAAI;YACxB,QAAQ,EAAE,UAAU,CAAC,EAAE;YACvB,SAAS,EAAE,GAAG;YACd,UAAU,EAAE,EAAE;SACf,CAAC,CAAC;IACL,CAAC;IAED,SAAS,CAAC,GAAqB,EAAE,UAA0B;QACzD,IAAI,CAAC,IAAI,GAAG,KAAK,CAAC;QAClB,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,aAAa,EAAE,IAAI,CAAC,IAAI,EAAE,YAAY,EAAE,UAAU,CAAC,EAAE,EAAE,CAAC,CAAC;QACrF,GAAG,CAAC,QAAQ,CAAC;YACX,GAAG,EAAE,MAAM;YACX,aAAa,EAAE,IAAI,CAAC,IAAI;YACxB,QAAQ,EAAE,UAAU,CAAC,EAAE;YACvB,SAAS,EAAE,GAAG;YACd,UAAU,EAAE,EAAE;SACf,CAAC,CAAC;IACL,CAAC;IAED,MAAM,CAAC,GAAqB,EAAE,MAAuB;QACnD,IAAI,IAAI,CAAC,IAAI,IAAI,MAAM,EAAE,CAAC;YACxB,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;YAC1C,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;gBACnB,MAAM,IAAI,GAAG,IAAI,CAAC,UAAU,GAAG,CAAC,KAAK,GAAG,IAAI,CAAC,eAAe,CAAC,CAAC;gBAC9D,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC;YACvD,CAAC;QACH,CAAC;aAAM,IAAI,IAAI,CAAC,IAAI,GAAG,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,IAAI,EAAE,CAAC;YAC3D,UAAU,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,QAAQ,EAAE,CAAC,EAAE,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,OAAO,EAAE,GAAG,CAAC,EAAE,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;YAC3F,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;YAC7C,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;YACpC,IAAI,IAAI,CAAC,IAAI,GAAG,IAAI,IAAI,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,IAAI,EAAE,CAAC;gBACvD,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC;gBACd,IAAI,CAAC,QAAQ,GAAG,CAAC,CAAC;YACpB,CAAC;QACH,CAAC;QACD,GAAG,CAAC,SAAS,EAAE,cAAc,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;QAC7D,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC;QAC9B,IAAI,IAAI,CAAC,GAAG,CAAC,KAAK,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,aAAa,EAAE,CAAC;YACvD,IAAI,CAAC,WAAW,GAAG,KAAK,CAAC;YACzB,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,cAAc,EAAE,aAAa,EAAE,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QACtE,CAAC;IACH,CAAC;IAED,QAAQ;QACN,OAAO,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC;IACvE,CAAC;IAED,SAAS,CAAC,MAAc;QACtB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,IAAI,CAAC,IAAI,GAAG,MAAM;YAAE,IAAI,CAAC,IAAI,GAAG,MAAM,CAAC;IAC7C,CAAC;IAED,SAAS;QACP,OAAO,IAAI,CAAC,MAAM,CAAC;IACrB,CAAC;IAED,SAAS,CAAC,SAAiB,EAAE,OAAe;QAC1C,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;QAC3B,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACzB,CAAC;IAED,SAAS;QACP,OAAO,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC;IAC9D,CAAC;CACF","sourcesContent":["/**\n * Slide - a pulley/handle pulled along one local axis. `handDriven` while\n * held (the pull follows the hand's displacement along −axis), `driven`\n * on release: a spring returns the handle home while the value keeps\n * publishing. Value = pull / travel.\n */\nimport type { Vec3Tuple } from \"@realitycollective/webxr-input\";\nimport { springStep, vDot, vScale, worldToLocal } from \"../math.js\";\nimport { holderPoint, type Behaviour, type BehaviourContext, type InteractorInfo } from \"./behaviour.js\";\n\nexport interface SlideConfig {\n kind: \"slide\";\n /** Local rail axis; the handle is pulled along −axis. */\n axis?: Vec3Tuple;\n /** Full pull travel in meters. */\n travel?: number;\n stiffness?: number;\n damping?: number;\n}\n\nconst VALUE_EPSILON = 1e-3;\n\nexport class SlideBehaviour implements Behaviour {\n readonly kind = \"slide\";\n readonly ownership = \"handDriven\" as const;\n readonly requires = [\"grabs\"] as const;\n readonly grabbable = true;\n\n private readonly axis: Vec3Tuple;\n private travel: number;\n private stiffness: number;\n private damping: number;\n\n private pull = 0; // meters along −axis, 0..travel\n private velocity = 0;\n private pullAtGrab = 0;\n private handAlongAtGrab = 0;\n private held = false;\n private lastEmitted = -1;\n private readonly spring = new Float32Array(2);\n\n constructor(config: Omit<SlideConfig, \"kind\"> = {}) {\n this.axis = config.axis ?? [0, 1, 0];\n this.travel = config.travel ?? 0.3;\n this.stiffness = config.stiffness ?? 180;\n this.damping = config.damping ?? 14;\n }\n\n private handAlong(ctx: BehaviourContext, holder: InteractorInfo): number | null {\n const hand = holderPoint(holder);\n const transform = ctx.transform;\n if (!hand || !transform) return null;\n const rest = transform.getWorldPose();\n const local = worldToLocal(hand, rest.position, rest.quaternion);\n // Pull is measured along −axis.\n return -vDot(local, this.axis);\n }\n\n onGrabStart(ctx: BehaviourContext, interactor: InteractorInfo): void {\n this.held = true;\n this.pullAtGrab = this.pull;\n this.handAlongAtGrab = this.handAlong(ctx, interactor) ?? 0;\n this.velocity = 0;\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.4,\n durationMs: 25,\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.2,\n durationMs: 15,\n });\n }\n\n update(ctx: BehaviourContext, holder?: InteractorInfo): void {\n if (this.held && holder) {\n const along = this.handAlong(ctx, holder);\n if (along !== null) {\n const next = this.pullAtGrab + (along - this.handAlongAtGrab);\n this.pull = Math.min(this.travel, Math.max(0, next));\n }\n } else if (this.pull > 0 || Math.abs(this.velocity) > 1e-4) {\n springStep(this.pull, this.velocity, 0, this.stiffness, this.damping, ctx.dt, this.spring);\n this.pull = Math.max(0, this.spring[0] ?? 0);\n this.velocity = this.spring[1] ?? 0;\n if (this.pull < 1e-4 && Math.abs(this.velocity) < 1e-3) {\n this.pull = 0;\n this.velocity = 0;\n }\n }\n ctx.transform?.setLocalOffset(vScale(this.axis, -this.pull));\n const value = this.getValue();\n if (Math.abs(value - this.lastEmitted) > VALUE_EPSILON) {\n this.lastEmitted = value;\n ctx.emit({ type: \"valueChanged\", behaviourKind: this.kind, value });\n }\n }\n\n getValue(): number {\n return this.travel < 1e-9 ? 0 : Math.min(1, this.pull / this.travel);\n }\n\n setTravel(travel: number): void {\n this.travel = travel;\n if (this.pull > travel) this.pull = travel;\n }\n\n getTravel(): number {\n return this.travel;\n }\n\n setSpring(stiffness: number, damping: number): void {\n this.stiffness = stiffness;\n this.damping = damping;\n }\n\n getSpring(): { stiffness: number; damping: number } {\n return { stiffness: this.stiffness, damping: this.damping };\n }\n}\n"]}
1
+ {"version":3,"file":"slide.js","sourceRoot":"","sources":["../../src/behaviours/slide.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Slide - a pulley/handle pulled along one local axis. `handDriven` while\n * held (the pull follows the hand's displacement along −axis), `driven`\n * on release: a spring returns the handle home while the value keeps\n * publishing. Value = pull / travel.\n */\nimport type { Vec3Tuple } from \"@realitycollective/webxr-input\";\nimport { springStep, vDot, vScale, worldToLocal } from \"../math.js\";\nimport { holderPoint, type Behaviour, type BehaviourContext, type InteractorInfo } from \"./behaviour.js\";\n\nexport interface SlideConfig {\n kind: \"slide\";\n /** Local rail axis; the handle is pulled along −axis. */\n axis?: Vec3Tuple;\n /** Full pull travel in meters. */\n travel?: number;\n stiffness?: number;\n damping?: number;\n}\n\nconst VALUE_EPSILON = 1e-3;\n\nexport class SlideBehaviour implements Behaviour {\n readonly kind = \"slide\";\n readonly ownership = \"handDriven\" as const;\n readonly requires = [\"grabs\"] as const;\n readonly grabbable = true;\n\n private readonly axis: Vec3Tuple;\n private travel: number;\n private stiffness: number;\n private damping: number;\n\n private pull = 0; // meters along −axis, 0..travel\n private velocity = 0;\n private pullAtGrab = 0;\n private handAlongAtGrab = 0;\n private held = false;\n private lastEmitted = -1;\n private readonly spring = new Float32Array(2);\n\n constructor(config: Omit<SlideConfig, \"kind\"> = {}) {\n this.axis = config.axis ?? [0, 1, 0];\n this.travel = config.travel ?? 0.3;\n this.stiffness = config.stiffness ?? 180;\n this.damping = config.damping ?? 14;\n }\n\n private handAlong(ctx: BehaviourContext, holder: InteractorInfo): number | null {\n const hand = holderPoint(holder);\n const transform = ctx.transform;\n if (!hand || !transform) return null;\n const rest = transform.getRestWorldPose();\n const local = worldToLocal(hand, rest.position, rest.quaternion);\n // Pull is measured along −axis.\n return -vDot(local, this.axis);\n }\n\n onGrabStart(ctx: BehaviourContext, interactor: InteractorInfo): void {\n this.held = true;\n this.pullAtGrab = this.pull;\n this.handAlongAtGrab = this.handAlong(ctx, interactor) ?? 0;\n this.velocity = 0;\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.4,\n durationMs: 25,\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.2,\n durationMs: 15,\n });\n }\n\n update(ctx: BehaviourContext, holder?: InteractorInfo): void {\n if (this.held && holder) {\n const along = this.handAlong(ctx, holder);\n if (along !== null) {\n const next = this.pullAtGrab + (along - this.handAlongAtGrab);\n this.pull = Math.min(this.travel, Math.max(0, next));\n }\n } else if (this.pull > 0 || Math.abs(this.velocity) > 1e-4) {\n springStep(this.pull, this.velocity, 0, this.stiffness, this.damping, ctx.dt, this.spring);\n this.pull = Math.max(0, this.spring[0] ?? 0);\n this.velocity = this.spring[1] ?? 0;\n if (this.pull < 1e-4 && Math.abs(this.velocity) < 1e-3) {\n this.pull = 0;\n this.velocity = 0;\n }\n }\n ctx.transform?.setLocalOffset(vScale(this.axis, -this.pull));\n const value = this.getValue();\n if (Math.abs(value - this.lastEmitted) > VALUE_EPSILON) {\n this.lastEmitted = value;\n ctx.emit({ type: \"valueChanged\", behaviourKind: this.kind, value });\n }\n }\n\n getValue(): number {\n return this.travel < 1e-9 ? 0 : Math.min(1, this.pull / this.travel);\n }\n\n setTravel(travel: number): void {\n this.travel = travel;\n if (this.pull > travel) this.pull = travel;\n }\n\n getTravel(): number {\n return this.travel;\n }\n\n setSpring(stiffness: number, damping: number): void {\n this.stiffness = stiffness;\n this.damping = damping;\n }\n\n getSpring(): { stiffness: number; damping: number } {\n return { stiffness: this.stiffness, damping: this.damping };\n }\n}\n"]}
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Shared `HitTester` and `TransformPort` conformance suites, shipped as data
3
+ * rather than as tests - the same idea as `windowHostContractCases()` in
4
+ * WebXR-UIExtensions and `inputProviderContractCases()` in
5
+ * `@realitycollective/webxr-input`. Five adapters implement these two ports
6
+ * against five different scene graphs; running the same checks against every
7
+ * one is what stops an adapter quietly dropping a documented rule.
8
+ *
9
+ * The suites are runner-free on purpose. Every adapter repository already has
10
+ * its own test runner, and an adapter written outside this repository cannot
11
+ * reach into this one's `test/` folder, so the checks ship as plain objects
12
+ * that throw an `Error` on failure and the adapter iterates them.
13
+ *
14
+ * Both suites assert only what `ports.ts` documents, including two rules
15
+ * every platform now shares:
16
+ * - `getWorldPose()` is the LIVE pose and `getRestWorldPose()` the rest pose
17
+ * offsets are measured from.
18
+ * - Every tuple a port returns is fresh and owned by the caller, and a tuple
19
+ * passed in is not kept.
20
+ */
21
+ import type { PoseTuple, Vec3Tuple } from "@realitycollective/webxr-input";
22
+ import type { HitTester, TransformPort } from "./ports.js";
23
+ /**
24
+ * Places (or replaces) one target for a case to query against. Build a FRESH
25
+ * subject per case: cases place targets of their own and do not clean up.
26
+ */
27
+ export interface HitTesterContractDriver {
28
+ /**
29
+ * Register a target at `position` with an interaction radius of `radius`,
30
+ * however the platform registers a target - a mesh, a node, an entity.
31
+ */
32
+ place(id: string, position: Vec3Tuple, radius: number): void;
33
+ }
34
+ /** The tester under test, plus the means of putting targets into its scene. */
35
+ export interface HitTesterContractSubject {
36
+ hitTester: HitTester;
37
+ driver: HitTesterContractDriver;
38
+ }
39
+ /** One check a {@link HitTester} implementation must pass. */
40
+ export interface HitTesterContractCase {
41
+ name: string;
42
+ run(subject: HitTesterContractSubject): void;
43
+ }
44
+ /**
45
+ * The shared `HitTester` conformance suite. An adapter's test file is a loop:
46
+ *
47
+ * ```ts
48
+ * for (const contractCase of hitTesterContractCases()) {
49
+ * it(contractCase.name, () => contractCase.run(makeSubject()));
50
+ * }
51
+ * ```
52
+ */
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
+ }
63
+ /** The port under test, over one object. Build a FRESH one per case. */
64
+ export interface TransformPortContractSubject {
65
+ port: TransformPort;
66
+ /**
67
+ * The world pose the object sat at when the port captured its rest. Place
68
+ * it away from the origin and turned, under no parent or an unscaled one,
69
+ * so a port that ignores its rest frame cannot pass by accident.
70
+ */
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;
78
+ }
79
+ /** One check a {@link TransformPort} implementation must pass. */
80
+ export interface TransformPortContractCase {
81
+ name: string;
82
+ run(subject: TransformPortContractSubject): void;
83
+ }
84
+ /**
85
+ * The shared `TransformPort` conformance suite. An adapter's test file is a
86
+ * loop, the same shape as {@link hitTesterContractCases}.
87
+ */
88
+ export declare function transformPortContractCases(): readonly TransformPortContractCase[];