@realitycollective/webxr-interactions 0.1.0-preview.0

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.
Files changed (58) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/LICENSE +21 -0
  3. package/README.md +42 -0
  4. package/dist/behaviours/behaviour.d.ts +63 -0
  5. package/dist/behaviours/behaviour.js +5 -0
  6. package/dist/behaviours/behaviour.js.map +1 -0
  7. package/dist/behaviours/dial.d.ts +36 -0
  8. package/dist/behaviours/dial.js +88 -0
  9. package/dist/behaviours/dial.js.map +1 -0
  10. package/dist/behaviours/factory.d.ts +24 -0
  11. package/dist/behaviours/factory.js +29 -0
  12. package/dist/behaviours/factory.js.map +1 -0
  13. package/dist/behaviours/grab.d.ts +21 -0
  14. package/dist/behaviours/grab.js +64 -0
  15. package/dist/behaviours/grab.js.map +1 -0
  16. package/dist/behaviours/hinge.d.ts +42 -0
  17. package/dist/behaviours/hinge.js +70 -0
  18. package/dist/behaviours/hinge.js.map +1 -0
  19. package/dist/behaviours/press.d.ts +57 -0
  20. package/dist/behaviours/press.js +109 -0
  21. package/dist/behaviours/press.js.map +1 -0
  22. package/dist/behaviours/pulse.d.ts +21 -0
  23. package/dist/behaviours/pulse.js +37 -0
  24. package/dist/behaviours/pulse.js.map +1 -0
  25. package/dist/behaviours/slide.d.ts +47 -0
  26. package/dist/behaviours/slide.js +104 -0
  27. package/dist/behaviours/slide.js.map +1 -0
  28. package/dist/behaviours/toss-score.d.ts +33 -0
  29. package/dist/behaviours/toss-score.js +58 -0
  30. package/dist/behaviours/toss-score.js.map +1 -0
  31. package/dist/descriptor.d.ts +21 -0
  32. package/dist/descriptor.js +2 -0
  33. package/dist/descriptor.js.map +1 -0
  34. package/dist/events.d.ts +32 -0
  35. package/dist/events.js +16 -0
  36. package/dist/events.js.map +1 -0
  37. package/dist/feedback.d.ts +31 -0
  38. package/dist/feedback.js +15 -0
  39. package/dist/feedback.js.map +1 -0
  40. package/dist/gaze.d.ts +44 -0
  41. package/dist/gaze.js +61 -0
  42. package/dist/gaze.js.map +1 -0
  43. package/dist/index.d.ts +34 -0
  44. package/dist/index.js +35 -0
  45. package/dist/index.js.map +1 -0
  46. package/dist/math.d.ts +68 -0
  47. package/dist/math.js +164 -0
  48. package/dist/math.js.map +1 -0
  49. package/dist/pointer-bridge.d.ts +24 -0
  50. package/dist/pointer-bridge.js +60 -0
  51. package/dist/pointer-bridge.js.map +1 -0
  52. package/dist/ports.d.ts +50 -0
  53. package/dist/ports.js +2 -0
  54. package/dist/ports.js.map +1 -0
  55. package/dist/runtime.d.ts +97 -0
  56. package/dist/runtime.js +511 -0
  57. package/dist/runtime.js.map +1 -0
  58. package/package.json +51 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,21 @@
1
+ # Changelog
2
+
3
+ Change log for the Reality Collective WebXR Interaction Extensions packages. All four packages are versioned and released together; the version below is the one carried by the `v<version>` release tag.
4
+
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
+
7
+ ## [0.1.0]
8
+
9
+ ### Added
10
+
11
+ - `@realitycollective/webxr-interactions` - engine-free core: interactables and interactors, the behaviour set (`press` including latching, `pulse`, `hinge`, `dial`, `slide`, `grab` in poseOnly and native modes, `tossScore`), gaze (`required` gating and dwell-to-press), the runtime/binder with hints > poke > ray targeting, hysteresis and lifecycle, capability negotiation with a visible `behaviourDisabled` outcome, events as the only outbound pathway, and feedback intents (haptics and audio remain the client's, with `routeHapticsToProvider` as an explicit opt-in).
12
+ - `@realitycollective/threejs-interactions` - the default standalone adapter: raw WebXR (`XRSession` sources, hand joints, select/squeeze plus gamepad analog, haptic pulse), three.js hit-testing and transform ports, and a desktop mouse fallback. No framework required.
13
+ - `@realitycollective/iwsdk-interactions` - Meta IWSDK adapter: player-rig poses and stateful gamepads as the provider, IWSDK's own targeting (`Pressed` / `Grabbed` tags) forwarded as pre-resolved hints, and native grab fulfilment when the app enables IWSDK grabbing/physics. One-call `registerInteractions(world)` setup.
14
+ - `@realitycollective/xrblocks-interactions` - EXPERIMENTAL Google XR Blocks adapter, structurally typed against xrblocks v0.20.0 (`Input.getFrame()` ray sources and direct touches, pooled structs copied, no haptics upstream).
15
+ - `demos/playground` - the standalone interaction playground: the full station set from one portable `InteractionDescriptor`, with client-side audio cues, opt-in controller haptics and client-side toss ballistics.
16
+
17
+ ### Notes
18
+
19
+ - All four 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.
20
+
21
+ [0.1.0]: https://github.com/realitycollective/WebXR-Interactions/commits/main
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Reality Collective
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,42 @@
1
+ # @realitycollective/webxr-interactions
2
+
3
+ The core of the Reality Collective Interaction Extensions. It holds all the interaction logic for WebXR and contains no 3D engine code at all.
4
+
5
+ Two words are used throughout. An **interactable** is an object that can be interacted with, such as a button. An **interactor** is the thing doing the interacting: a hand, controller, ray or mouse.
6
+
7
+ ```sh
8
+ npm install @realitycollective/webxr-interactions
9
+ ```
10
+
11
+ > You usually do **not** install this directly. Install an adapter for your engine instead. Each adapter re-exports everything here, so your app depends on one package: [`threejs-interactions`](https://www.npmjs.com/package/@realitycollective/threejs-interactions) · [`iwsdk-interactions`](https://www.npmjs.com/package/@realitycollective/iwsdk-interactions) · [`xrblocks-interactions`](https://www.npmjs.com/package/@realitycollective/xrblocks-interactions)
12
+
13
+ ## What it provides
14
+
15
+ | Area | Detail |
16
+ | --- | --- |
17
+ | **Behaviours** | Ready-made interaction types: `press` (with an optional latching mode that stays down until pressed again), `pulse`, `hinge`, `dial`, `slide`, `grab`, and `tossScore` for throw-and-catch scoring |
18
+ | **Gaze** | Optional look-to-activate. Require the user to be looking before an object responds, or let a sustained look trigger the press by itself |
19
+ | **Targeting** | Picks one target per hand, trying the platform's own answer first, then a close-range touch, then a pointing ray. A short delay stops the target flickering between two objects on a boundary |
20
+ | **Capability checks** | If the headset cannot do what a behaviour needs, the behaviour switches itself off and reports `behaviourDisabled`, rather than silently doing nothing |
21
+ | **Events** | The core never calls into your code. It emits events and you subscribe |
22
+ | **Feedback** | The core asks for a haptic pulse or a sound. Playing it is your app's job. `routeHapticsToProvider` is an opt-in helper that sends those requests straight to the controller |
23
+
24
+ All input arrives through the shared [`@realitycollective/webxr-input`](https://www.npmjs.com/package/@realitycollective/webxr-input) types, so the same core runs on any runtime that has an adapter.
25
+
26
+ ## Design rules
27
+
28
+ - **No engine imports.** No `three`, no `@iwsdk/*`, no `xrblocks`. The adapter supplies the code that works out what a ray hits, and the code that moves an object.
29
+ - **Events out, never callbacks in.** The core never reaches into your objects.
30
+ - **Fail loudly, not silently.** A behaviour the hardware cannot support reports `behaviourDisabled` instead of quietly doing nothing.
31
+
32
+ ## Live demo
33
+
34
+ The interaction playground - the full station set, mouse-capable on desktop, VR button for headsets: **[webxr-interactions.pages.dev](https://webxr-interactions.pages.dev)**
35
+
36
+ ## Documentation
37
+
38
+ Full architecture notes, the behaviour catalogue and the playground demo live in the [repository README](https://github.com/realitycollective/WebXR-Interactions#readme).
39
+
40
+ ## License
41
+
42
+ MIT - see [LICENSE](./LICENSE).
@@ -0,0 +1,63 @@
1
+ /**
2
+ * The behaviour contract - a mechanism attached to an interactable.
3
+ *
4
+ * Behaviours are pure state machines: they receive routed input moments
5
+ * (press/grab transitions), tick from `update(dt)`, and write their output
6
+ * through the interactable's {@link TransformPort}. They declare:
7
+ *
8
+ * - `ownership` - who owns the transform (the formalised pale-signal
9
+ * driven / dragged / handDriven / native models), so a binder knows
10
+ * without reading code;
11
+ * - `requires` / `requiresAnyOf` - the input capabilities they need;
12
+ * capability negotiation disables unsatisfiable behaviours VISIBLY
13
+ * (a `behaviourDisabled` event with the unmet list), never silently.
14
+ */
15
+ import type { InputCapabilityRequirement, InputSourceKind, PoseTuple, RayTuple, Vec3Tuple } from "@realitycollective/webxr-input";
16
+ import type { InteractionEvent } from "../events.js";
17
+ import type { FeedbackIntent } from "../feedback.js";
18
+ import type { TransformPort } from "../ports.js";
19
+ /** Who owns the interactable's transform while this behaviour is active. */
20
+ export type TransformOwnership = "driven" | "dragged" | "handDriven" | "native";
21
+ /** The slice of a live interactor a behaviour may consume. */
22
+ export interface InteractorInfo {
23
+ id: string;
24
+ kind: InputSourceKind;
25
+ ray?: RayTuple;
26
+ gripPose?: PoseTuple;
27
+ indexTip?: Vec3Tuple;
28
+ select: number;
29
+ squeeze: number;
30
+ }
31
+ /** Everything a behaviour can reach during a tick or a routed moment. */
32
+ export interface BehaviourContext {
33
+ dt: number;
34
+ interactableId: string;
35
+ transform: TransformPort | undefined;
36
+ emit(event: Omit<InteractionEvent, "interactableId">): void;
37
+ feedback(intent: Omit<FeedbackIntent, "interactableId">): void;
38
+ /** REST world pose of another registered interactable (toss scoring). */
39
+ getWorldPose(interactableId: string): PoseTuple | undefined;
40
+ }
41
+ export interface Behaviour {
42
+ readonly kind: string;
43
+ readonly ownership: TransformOwnership;
44
+ /** ALL of these must be satisfied. */
45
+ readonly requires: readonly InputCapabilityRequirement[];
46
+ /** For each group, AT LEAST ONE must be satisfied. */
47
+ readonly requiresAnyOf?: readonly (readonly InputCapabilityRequirement[])[];
48
+ /** True when this behaviour makes the interactable a grab target. */
49
+ readonly grabbable: boolean;
50
+ onPressStart?(ctx: BehaviourContext, interactor: InteractorInfo): void;
51
+ onPressEnd?(ctx: BehaviourContext, interactor: InteractorInfo): void;
52
+ onGrabStart?(ctx: BehaviourContext, interactor: InteractorInfo): void;
53
+ onGrabEnd?(ctx: BehaviourContext, interactor: InteractorInfo): void;
54
+ /**
55
+ * Per-frame tick. `holder` is set while a grab interactor holds this
56
+ * interactable (grab-family behaviours consume it; others ignore it).
57
+ */
58
+ update(ctx: BehaviourContext, holder?: InteractorInfo): void;
59
+ /** Current logical 0..1 value (state snapshots, HUDs). */
60
+ getValue(): number;
61
+ }
62
+ /** Hand position a hand-driven behaviour should track, best available. */
63
+ export declare function holderPoint(holder: InteractorInfo): Vec3Tuple | undefined;
@@ -0,0 +1,5 @@
1
+ /** Hand position a hand-driven behaviour should track, best available. */
2
+ export function holderPoint(holder) {
3
+ return holder.indexTip ?? holder.gripPose?.position ?? holder.ray?.origin;
4
+ }
5
+ //# sourceMappingURL=behaviour.js.map
@@ -0,0 +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"]}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Dial - a twistable knob. `handDriven`: while held, the hand's bearing
3
+ * around the dial axis accumulates a clamped rotation `[0, maxAngle]`;
4
+ * publishes `normalize01(angle, maxAngle)`. Works from any provider that
5
+ * can grab and report a hand/grip position (the research prototype's
6
+ * engine-dragged variant becomes an adapter option later).
7
+ */
8
+ import type { Vec3Tuple } from "@realitycollective/webxr-input";
9
+ import { type Behaviour, type BehaviourContext, type InteractorInfo } from "./behaviour.js";
10
+ export interface DialConfig {
11
+ kind: "dial";
12
+ /** Local twist axis. */
13
+ axis?: Vec3Tuple;
14
+ /** Max rotation from rest (radians). */
15
+ maxAngle?: number;
16
+ }
17
+ export declare class DialBehaviour implements Behaviour {
18
+ readonly kind = "dial";
19
+ readonly ownership: "handDriven";
20
+ readonly requires: readonly ["grabs"];
21
+ readonly grabbable = true;
22
+ private readonly axis;
23
+ private maxAngle;
24
+ private angle;
25
+ private angleAtGrab;
26
+ private refBearing;
27
+ private lastEmitted;
28
+ constructor(config?: Omit<DialConfig, "kind">);
29
+ private bearingOf;
30
+ onGrabStart(ctx: BehaviourContext, interactor: InteractorInfo): void;
31
+ onGrabEnd(ctx: BehaviourContext, interactor: InteractorInfo): void;
32
+ update(ctx: BehaviourContext, holder?: InteractorInfo): void;
33
+ getValue(): number;
34
+ setMaxAngle(maxAngle: number): void;
35
+ getMaxAngle(): number;
36
+ }
@@ -0,0 +1,88 @@
1
+ import { normalize01, quatFromAxisAngle, vCross, vDot, vNormalize, vSub, worldToLocal, } from "../math.js";
2
+ import { holderPoint } from "./behaviour.js";
3
+ const VALUE_EPSILON = 1e-3;
4
+ export class DialBehaviour {
5
+ kind = "dial";
6
+ ownership = "handDriven";
7
+ requires = ["grabs"];
8
+ grabbable = true;
9
+ axis;
10
+ maxAngle;
11
+ angle = 0;
12
+ angleAtGrab = 0;
13
+ refBearing = null;
14
+ lastEmitted = -1;
15
+ constructor(config = {}) {
16
+ this.axis = config.axis ?? [0, 1, 0];
17
+ this.maxAngle = config.maxAngle ?? (3 * Math.PI) / 2;
18
+ }
19
+ bearingOf(ctx, holder) {
20
+ const hand = holderPoint(holder);
21
+ const transform = ctx.transform;
22
+ if (!hand || !transform)
23
+ return null;
24
+ const rest = transform.getWorldPose();
25
+ const local = worldToLocal(hand, rest.position, rest.quaternion);
26
+ // Project onto the plane perpendicular to the axis.
27
+ const along = vDot(local, this.axis);
28
+ const planar = vSub(local, [
29
+ this.axis[0] * along,
30
+ this.axis[1] * along,
31
+ this.axis[2] * along,
32
+ ]);
33
+ const n = vNormalize(planar);
34
+ return n[0] === 0 && n[1] === 0 && n[2] === 0 ? null : n;
35
+ }
36
+ onGrabStart(ctx, interactor) {
37
+ this.refBearing = this.bearingOf(ctx, interactor);
38
+ this.angleAtGrab = this.angle;
39
+ ctx.emit({ type: "grabStart", behaviourKind: this.kind, interactorId: interactor.id });
40
+ ctx.feedback({
41
+ cue: "grab",
42
+ behaviourKind: this.kind,
43
+ sourceId: interactor.id,
44
+ intensity: 0.4,
45
+ durationMs: 25,
46
+ });
47
+ }
48
+ onGrabEnd(ctx, interactor) {
49
+ this.refBearing = null;
50
+ ctx.emit({ type: "grabEnd", behaviourKind: this.kind, interactorId: interactor.id });
51
+ ctx.feedback({
52
+ cue: "drop",
53
+ behaviourKind: this.kind,
54
+ sourceId: interactor.id,
55
+ intensity: 0.2,
56
+ durationMs: 15,
57
+ });
58
+ }
59
+ update(ctx, holder) {
60
+ if (holder && this.refBearing && ctx.transform) {
61
+ const current = this.bearingOf(ctx, holder);
62
+ if (current) {
63
+ const cross = vCross(this.refBearing, current);
64
+ const delta = Math.atan2(vDot(cross, this.axis), vDot(this.refBearing, current));
65
+ const next = Math.min(this.maxAngle, Math.max(0, this.angleAtGrab + delta));
66
+ this.angle = next;
67
+ ctx.transform.setLocalRotation(quatFromAxisAngle(this.axis, this.angle));
68
+ }
69
+ }
70
+ const value = this.getValue();
71
+ if (Math.abs(value - this.lastEmitted) > VALUE_EPSILON) {
72
+ this.lastEmitted = value;
73
+ ctx.emit({ type: "valueChanged", behaviourKind: this.kind, value });
74
+ }
75
+ }
76
+ getValue() {
77
+ return normalize01(this.angle, this.maxAngle);
78
+ }
79
+ setMaxAngle(maxAngle) {
80
+ this.maxAngle = maxAngle;
81
+ if (this.angle > maxAngle)
82
+ this.angle = maxAngle;
83
+ }
84
+ getMaxAngle() {
85
+ return this.maxAngle;
86
+ }
87
+ }
88
+ //# sourceMappingURL=dial.js.map
@@ -0,0 +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"]}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Behaviour factory - turns portable config data into behaviour instances.
3
+ * Custom kinds register through {@link registerBehaviourKind}, so apps can
4
+ * add mechanisms without touching the core.
5
+ */
6
+ import type { Behaviour } from "./behaviour.js";
7
+ import { type DialConfig } from "./dial.js";
8
+ import { type GrabConfig, type GrabFulfilment } from "./grab.js";
9
+ import { type HingeConfig } from "./hinge.js";
10
+ import { type PressConfig } from "./press.js";
11
+ import { type PulseConfig } from "./pulse.js";
12
+ import { type SlideConfig } from "./slide.js";
13
+ import { type TossScoreConfig } from "./toss-score.js";
14
+ export type BehaviourConfig = PressConfig | PulseConfig | HingeConfig | DialConfig | SlideConfig | GrabConfig | TossScoreConfig | (Record<string, unknown> & {
15
+ kind: string;
16
+ });
17
+ export interface BehaviourFactoryContext {
18
+ /** Grab fulfilment negotiated from the provider's capabilities. */
19
+ grabFulfilment: GrabFulfilment;
20
+ }
21
+ type BehaviourFactory = (config: Record<string, unknown>, context: BehaviourFactoryContext) => Behaviour;
22
+ export declare function registerBehaviourKind(kind: string, factory: BehaviourFactory): void;
23
+ export declare function createBehaviour(config: BehaviourConfig, context: BehaviourFactoryContext): Behaviour;
24
+ export {};
@@ -0,0 +1,29 @@
1
+ import { DialBehaviour } from "./dial.js";
2
+ import { GrabBehaviour } from "./grab.js";
3
+ import { HingeBehaviour } from "./hinge.js";
4
+ import { PressBehaviour } from "./press.js";
5
+ import { PulseBehaviour } from "./pulse.js";
6
+ import { SlideBehaviour } from "./slide.js";
7
+ import { TossScoreBehaviour } from "./toss-score.js";
8
+ const registry = new Map();
9
+ export function registerBehaviourKind(kind, factory) {
10
+ registry.set(kind, factory);
11
+ }
12
+ export function createBehaviour(config, context) {
13
+ const factory = registry.get(config.kind);
14
+ if (!factory) {
15
+ throw new Error(`Unknown behaviour kind "${config.kind}"`);
16
+ }
17
+ return factory(config, context);
18
+ }
19
+ registerBehaviourKind("press", (c) => new PressBehaviour(c));
20
+ registerBehaviourKind("pulse", (c) => new PulseBehaviour(c));
21
+ registerBehaviourKind("hinge", (c) => new HingeBehaviour(c));
22
+ registerBehaviourKind("dial", (c) => new DialBehaviour(c));
23
+ registerBehaviourKind("slide", (c) => new SlideBehaviour(c));
24
+ registerBehaviourKind("grab", (c, ctx) => new GrabBehaviour(c, ctx.grabFulfilment));
25
+ registerBehaviourKind("tossScore", (c) => {
26
+ const config = c;
27
+ return new TossScoreBehaviour(config);
28
+ });
29
+ //# sourceMappingURL=factory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"factory.js","sourceRoot":"","sources":["../../src/behaviours/factory.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,aAAa,EAAmB,MAAM,WAAW,CAAC;AAC3D,OAAO,EAAE,aAAa,EAAwC,MAAM,WAAW,CAAC;AAChF,OAAO,EAAE,cAAc,EAAoB,MAAM,YAAY,CAAC;AAC9D,OAAO,EAAE,cAAc,EAAoB,MAAM,YAAY,CAAC;AAC9D,OAAO,EAAE,cAAc,EAAoB,MAAM,YAAY,CAAC;AAC9D,OAAO,EAAE,cAAc,EAAoB,MAAM,YAAY,CAAC;AAC9D,OAAO,EAAE,kBAAkB,EAAwB,MAAM,iBAAiB,CAAC;AAsB3E,MAAM,QAAQ,GAAG,IAAI,GAAG,EAA4B,CAAC;AAErD,MAAM,UAAU,qBAAqB,CAAC,IAAY,EAAE,OAAyB;IAC3E,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;AAC9B,CAAC;AAED,MAAM,UAAU,eAAe,CAC7B,MAAuB,EACvB,OAAgC;IAEhC,MAAM,OAAO,GAAG,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IAC1C,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CAAC,2BAA2B,MAAM,CAAC,IAAI,GAAG,CAAC,CAAC;IAC7D,CAAC;IACD,OAAO,OAAO,CAAC,MAAiC,EAAE,OAAO,CAAC,CAAC;AAC7D,CAAC;AAED,qBAAqB,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,cAAc,CAAC,CAA8B,CAAC,CAAC,CAAC;AAC1F,qBAAqB,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,cAAc,CAAC,CAA8B,CAAC,CAAC,CAAC;AAC1F,qBAAqB,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,cAAc,CAAC,CAA8B,CAAC,CAAC,CAAC;AAC1F,qBAAqB,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,aAAa,CAAC,CAA6B,CAAC,CAAC,CAAC;AACvF,qBAAqB,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,cAAc,CAAC,CAA8B,CAAC,CAAC,CAAC;AAC1F,qBAAqB,CACnB,MAAM,EACN,CAAC,CAAC,EAAE,GAAG,EAAE,EAAE,CAAC,IAAI,aAAa,CAAC,CAA6B,EAAE,GAAG,CAAC,cAAc,CAAC,CACjF,CAAC;AACF,qBAAqB,CAAC,WAAW,EAAE,CAAC,CAAC,EAAE,EAAE;IACvC,MAAM,MAAM,GAAG,CAA+B,CAAC;IAC/C,OAAO,IAAI,kBAAkB,CAAC,MAAM,CAAC,CAAC;AACxC,CAAC,CAAC,CAAC","sourcesContent":["/**\n * Behaviour factory - turns portable config data into behaviour instances.\n * Custom kinds register through {@link registerBehaviourKind}, so apps can\n * add mechanisms without touching the core.\n */\nimport type { Behaviour } from \"./behaviour.js\";\nimport { DialBehaviour, type DialConfig } from \"./dial.js\";\nimport { GrabBehaviour, type GrabConfig, type GrabFulfilment } from \"./grab.js\";\nimport { HingeBehaviour, type HingeConfig } from \"./hinge.js\";\nimport { PressBehaviour, type PressConfig } from \"./press.js\";\nimport { PulseBehaviour, type PulseConfig } from \"./pulse.js\";\nimport { SlideBehaviour, type SlideConfig } from \"./slide.js\";\nimport { TossScoreBehaviour, type TossScoreConfig } from \"./toss-score.js\";\n\nexport type BehaviourConfig =\n | PressConfig\n | PulseConfig\n | HingeConfig\n | DialConfig\n | SlideConfig\n | GrabConfig\n | TossScoreConfig\n | (Record<string, unknown> & { kind: string });\n\nexport interface BehaviourFactoryContext {\n /** Grab fulfilment negotiated from the provider's capabilities. */\n grabFulfilment: GrabFulfilment;\n}\n\ntype BehaviourFactory = (\n config: Record<string, unknown>,\n context: BehaviourFactoryContext,\n) => Behaviour;\n\nconst registry = new Map<string, BehaviourFactory>();\n\nexport function registerBehaviourKind(kind: string, factory: BehaviourFactory): void {\n registry.set(kind, factory);\n}\n\nexport function createBehaviour(\n config: BehaviourConfig,\n context: BehaviourFactoryContext,\n): Behaviour {\n const factory = registry.get(config.kind);\n if (!factory) {\n throw new Error(`Unknown behaviour kind \"${config.kind}\"`);\n }\n return factory(config as Record<string, unknown>, context);\n}\n\nregisterBehaviourKind(\"press\", (c) => new PressBehaviour(c as Omit<PressConfig, \"kind\">));\nregisterBehaviourKind(\"pulse\", (c) => new PulseBehaviour(c as Omit<PulseConfig, \"kind\">));\nregisterBehaviourKind(\"hinge\", (c) => new HingeBehaviour(c as Omit<HingeConfig, \"kind\">));\nregisterBehaviourKind(\"dial\", (c) => new DialBehaviour(c as Omit<DialConfig, \"kind\">));\nregisterBehaviourKind(\"slide\", (c) => new SlideBehaviour(c as Omit<SlideConfig, \"kind\">));\nregisterBehaviourKind(\n \"grab\",\n (c, ctx) => new GrabBehaviour(c as Omit<GrabConfig, \"kind\">, ctx.grabFulfilment),\n);\nregisterBehaviourKind(\"tossScore\", (c) => {\n const config = c as unknown as TossScoreConfig;\n return new TossScoreBehaviour(config);\n});\n"]}
@@ -0,0 +1,21 @@
1
+ import type { Behaviour, BehaviourContext, InteractorInfo } from "./behaviour.js";
2
+ export interface GrabConfig {
3
+ kind: "grab";
4
+ }
5
+ export type GrabFulfilment = "poseOnly" | "native";
6
+ export declare class GrabBehaviour implements Behaviour {
7
+ readonly kind = "grab";
8
+ readonly ownership: "handDriven" | "native";
9
+ readonly requires: readonly ["grabs"];
10
+ readonly grabbable = true;
11
+ private fulfilment;
12
+ private held;
13
+ private offsetPosition;
14
+ private offsetQuaternion;
15
+ constructor(_config?: Omit<GrabConfig, "kind">, fulfilment?: GrabFulfilment);
16
+ setFulfilment(fulfilment: GrabFulfilment): void;
17
+ onGrabStart(ctx: BehaviourContext, interactor: InteractorInfo): void;
18
+ onGrabEnd(ctx: BehaviourContext, interactor: InteractorInfo): void;
19
+ update(ctx: BehaviourContext, holder?: InteractorInfo): void;
20
+ getValue(): number;
21
+ }
@@ -0,0 +1,64 @@
1
+ import { quatConjugate, quatMultiply, vAdd, vApplyQuat, vSub } from "../math.js";
2
+ export class GrabBehaviour {
3
+ kind = "grab";
4
+ ownership;
5
+ requires = ["grabs"];
6
+ grabbable = true;
7
+ fulfilment;
8
+ held = false;
9
+ offsetPosition = [0, 0, 0];
10
+ offsetQuaternion = [0, 0, 0, 1];
11
+ constructor(_config = {}, fulfilment = "poseOnly") {
12
+ this.fulfilment = fulfilment;
13
+ this.ownership = fulfilment === "native" ? "native" : "handDriven";
14
+ }
15
+ setFulfilment(fulfilment) {
16
+ this.fulfilment = fulfilment;
17
+ }
18
+ onGrabStart(ctx, interactor) {
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);
26
+ }
27
+ ctx.emit({ type: "grabStart", behaviourKind: this.kind, interactorId: interactor.id });
28
+ ctx.feedback({
29
+ cue: "grab",
30
+ behaviourKind: this.kind,
31
+ sourceId: interactor.id,
32
+ intensity: 0.5,
33
+ durationMs: 30,
34
+ });
35
+ }
36
+ onGrabEnd(ctx, interactor) {
37
+ this.held = false;
38
+ ctx.emit({ type: "grabEnd", behaviourKind: this.kind, interactorId: interactor.id });
39
+ ctx.feedback({
40
+ cue: "drop",
41
+ behaviourKind: this.kind,
42
+ sourceId: interactor.id,
43
+ intensity: 0.25,
44
+ durationMs: 20,
45
+ });
46
+ }
47
+ update(ctx, holder) {
48
+ if (this.fulfilment !== "poseOnly" || !this.held || !holder)
49
+ return;
50
+ const grip = holder.gripPose;
51
+ const transform = ctx.transform;
52
+ if (!grip || !transform?.setWorldPose)
53
+ return;
54
+ const pose = {
55
+ position: vAdd(grip.position, vApplyQuat(this.offsetPosition, grip.quaternion)),
56
+ quaternion: quatMultiply(grip.quaternion, this.offsetQuaternion),
57
+ };
58
+ transform.setWorldPose(pose);
59
+ }
60
+ getValue() {
61
+ return this.held ? 1 : 0;
62
+ }
63
+ }
64
+ //# sourceMappingURL=grab.js.map
@@ -0,0 +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"]}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Hinge - a position-driven lever. `handDriven`: grabbing only marks the
3
+ * hold; the behaviour reads the holder's hand position each frame and
4
+ * swings the arm to point at it (movement, not wrist), bidirectional about
5
+ * rest, clamped to ±maxAngle. Stays where left on release. Publishes a
6
+ * centred 0..1 value (rest = 0.5).
7
+ *
8
+ * All math happens in the interactable's REST local frame, so the mount
9
+ * (table / wall vertical / wall horizontal / any) is simply the parent
10
+ * orientation of the object - no faceYaw plumbing needed.
11
+ */
12
+ import type { Vec3Tuple } from "@realitycollective/webxr-input";
13
+ import { type Behaviour, type BehaviourContext, type InteractorInfo } from "./behaviour.js";
14
+ export interface HingeConfig {
15
+ kind: "hinge";
16
+ /** Local hinge axis. */
17
+ axis?: Vec3Tuple;
18
+ /** Local rest direction of the arm. */
19
+ restDir?: Vec3Tuple;
20
+ /** Max swing (radians) either side of rest. */
21
+ maxAngle?: number;
22
+ }
23
+ export declare class HingeBehaviour implements Behaviour {
24
+ readonly kind = "hinge";
25
+ readonly ownership: "handDriven";
26
+ readonly requires: readonly ["grabs"];
27
+ readonly grabbable = true;
28
+ private readonly axis;
29
+ private readonly restDir;
30
+ private readonly swingDir;
31
+ private maxAngle;
32
+ private angle;
33
+ private lastEmitted;
34
+ constructor(config?: Omit<HingeConfig, "kind">);
35
+ onGrabStart(ctx: BehaviourContext, interactor: InteractorInfo): void;
36
+ onGrabEnd(ctx: BehaviourContext, interactor: InteractorInfo): void;
37
+ update(ctx: BehaviourContext, holder?: InteractorInfo): void;
38
+ getValue(): number;
39
+ /** Live-tune the swing range (playground steppers). */
40
+ setMaxAngle(maxAngle: number): void;
41
+ getMaxAngle(): number;
42
+ }
@@ -0,0 +1,70 @@
1
+ import { centeredUnit, hingeAngleToHand, quatFromAxisAngle, vCross, vDot, worldToLocal, } from "../math.js";
2
+ import { holderPoint } from "./behaviour.js";
3
+ const VALUE_EPSILON = 1e-3;
4
+ export class HingeBehaviour {
5
+ kind = "hinge";
6
+ ownership = "handDriven";
7
+ requires = ["grabs"];
8
+ grabbable = true;
9
+ axis;
10
+ restDir;
11
+ swingDir;
12
+ maxAngle;
13
+ angle = 0;
14
+ lastEmitted = -1;
15
+ constructor(config = {}) {
16
+ this.axis = config.axis ?? [1, 0, 0];
17
+ this.restDir = config.restDir ?? [0, 1, 0];
18
+ this.swingDir = vCross(this.axis, this.restDir);
19
+ this.maxAngle = config.maxAngle ?? Math.PI / 4;
20
+ }
21
+ onGrabStart(ctx, interactor) {
22
+ ctx.emit({ type: "grabStart", behaviourKind: this.kind, interactorId: interactor.id });
23
+ ctx.feedback({
24
+ cue: "grab",
25
+ behaviourKind: this.kind,
26
+ sourceId: interactor.id,
27
+ intensity: 0.4,
28
+ durationMs: 25,
29
+ });
30
+ }
31
+ onGrabEnd(ctx, interactor) {
32
+ ctx.emit({ type: "grabEnd", behaviourKind: this.kind, interactorId: interactor.id });
33
+ ctx.feedback({
34
+ cue: "drop",
35
+ behaviourKind: this.kind,
36
+ sourceId: interactor.id,
37
+ intensity: 0.2,
38
+ durationMs: 15,
39
+ });
40
+ }
41
+ update(ctx, holder) {
42
+ if (holder && ctx.transform) {
43
+ const hand = holderPoint(holder);
44
+ if (hand) {
45
+ const rest = ctx.transform.getWorldPose();
46
+ const local = worldToLocal(hand, rest.position, rest.quaternion);
47
+ const alongRest = vDot(local, this.restDir);
48
+ const alongSwing = vDot(local, this.swingDir);
49
+ this.angle = hingeAngleToHand(alongRest, alongSwing, this.maxAngle);
50
+ ctx.transform.setLocalRotation(quatFromAxisAngle(this.axis, this.angle));
51
+ }
52
+ }
53
+ const value = this.getValue();
54
+ if (Math.abs(value - this.lastEmitted) > VALUE_EPSILON) {
55
+ this.lastEmitted = value;
56
+ ctx.emit({ type: "valueChanged", behaviourKind: this.kind, value });
57
+ }
58
+ }
59
+ getValue() {
60
+ return centeredUnit(this.angle, this.maxAngle);
61
+ }
62
+ /** Live-tune the swing range (playground steppers). */
63
+ setMaxAngle(maxAngle) {
64
+ this.maxAngle = maxAngle;
65
+ }
66
+ getMaxAngle() {
67
+ return this.maxAngle;
68
+ }
69
+ }
70
+ //# sourceMappingURL=hinge.js.map
@@ -0,0 +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"]}