@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
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Client opt-in: forward the haptic half of feedback intents to the input
3
+ * provider's `pulse` (which no-ops on sources without actuators - hands).
4
+ * Returns the unsubscribe. Audio remains entirely the client's concern.
5
+ */
6
+ export function routeHapticsToProvider(onFeedback, provider, scale = 1) {
7
+ return onFeedback((intent) => {
8
+ if (!intent.sourceId)
9
+ return;
10
+ if (!provider.getCapabilities().haptics)
11
+ return;
12
+ provider.pulse?.(intent.sourceId, Math.min(1, intent.intensity * scale), intent.durationMs);
13
+ });
14
+ }
15
+ //# sourceMappingURL=feedback.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"feedback.js","sourceRoot":"","sources":["../src/feedback.ts"],"names":[],"mappings":"AAsCA;;;;GAIG;AACH,MAAM,UAAU,sBAAsB,CACpC,UAAuD,EACvD,QAAuB,EACvB,KAAK,GAAG,CAAC;IAET,OAAO,UAAU,CAAC,CAAC,MAAM,EAAE,EAAE;QAC3B,IAAI,CAAC,MAAM,CAAC,QAAQ;YAAE,OAAO;QAC7B,IAAI,CAAC,QAAQ,CAAC,eAAe,EAAE,CAAC,OAAO;YAAE,OAAO;QAChD,QAAQ,CAAC,KAAK,EAAE,CAAC,MAAM,CAAC,QAAQ,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,SAAS,GAAG,KAAK,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC;IAC9F,CAAC,CAAC,CAAC;AACL,CAAC","sourcesContent":["/**\n * Feedback hooks - haptics/audio stay OUT of the framework; the client\n * implements them. Behaviours emit *intents* describing the physical\n * moment (\"this press just actuated, a short strong pulse would fit\");\n * the client subscribes and realises them however it likes (haptic pulse\n * on controllers, an audio cue, both, neither).\n *\n * `routeHapticsToProvider` is the one convenience the framework offers -\n * an explicit opt-in the client calls, never an automatic behaviour.\n */\nimport type { InputProvider, Unsubscribe } from \"@realitycollective/webxr-input\";\n\n/** A named moment a client may want to sonify/hapticise. */\nexport type FeedbackCue =\n | \"hover\"\n | \"press\"\n | \"actuate\"\n | \"release\"\n | \"grab\"\n | \"drop\"\n | \"score\"\n | \"dwellComplete\"\n | \"valueTick\"; // discrete steps on analog controls (dial detents etc.)\n\nexport interface FeedbackIntent {\n cue: FeedbackCue;\n interactableId: string;\n behaviourKind?: string;\n /** The input source that caused the moment - haptics target this. */\n sourceId?: string;\n /** Suggested haptic intensity 0..1 (clients may remap freely). */\n intensity: number;\n /** Suggested haptic duration in ms. */\n durationMs: number;\n}\n\nexport type FeedbackListener = (intent: FeedbackIntent) => void;\n\n/**\n * Client opt-in: forward the haptic half of feedback intents to the input\n * provider's `pulse` (which no-ops on sources without actuators - hands).\n * Returns the unsubscribe. Audio remains entirely the client's concern.\n */\nexport function routeHapticsToProvider(\n onFeedback: (listener: FeedbackListener) => Unsubscribe,\n provider: InputProvider,\n scale = 1,\n): Unsubscribe {\n return onFeedback((intent) => {\n if (!intent.sourceId) return;\n if (!provider.getCapabilities().haptics) return;\n provider.pulse?.(intent.sourceId, Math.min(1, intent.intensity * scale), intent.durationMs);\n });\n}\n"]}
package/dist/gaze.d.ts ADDED
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Gaze support - first-class, per the design decision that gaze serves two
3
+ * roles:
4
+ *
5
+ * 1. **Combination gating** (`gaze.required`): input on an interactable only
6
+ * counts while the viewer is looking at it.
7
+ * 2. **Accessibility** (`gaze.dwell`): looking at an interactable fills a
8
+ * dwell meter; a full meter fires a synthesized press - no hands needed.
9
+ * Consumers render the meter from `dwellProgress` events.
10
+ *
11
+ * The dwell state machine is pure and headless-tested. The runtime feeds it
12
+ * hover booleans; it never sees the engine.
13
+ */
14
+ export interface DwellConfig {
15
+ /** Seconds of sustained gaze to fire. */
16
+ holdSeconds?: number;
17
+ /** Drain speed multiplier when looking away. */
18
+ decayFactor?: number;
19
+ /** After firing, progress must fall below this before it can re-arm. */
20
+ rearmBelow?: number;
21
+ }
22
+ export interface GazeConfig {
23
+ /** Non-gaze input on this interactable only counts while gazed at. */
24
+ required?: boolean;
25
+ /** Enable dwell-to-press. `true` uses defaults. */
26
+ dwell?: boolean | DwellConfig;
27
+ }
28
+ export declare const DWELL_DEFAULTS: Required<DwellConfig>;
29
+ export declare function resolveDwellConfig(config: boolean | DwellConfig | undefined, defaults?: Required<DwellConfig>): Required<DwellConfig> | null;
30
+ export interface DwellTick {
31
+ /** Dwell meter 0..1. */
32
+ progress: number;
33
+ /** Progress moved enough to be worth re-rendering (≥ 1%). */
34
+ changed: boolean;
35
+ /** The meter just completed - fire the synthesized press. */
36
+ fired: boolean;
37
+ }
38
+ export declare class DwellState {
39
+ private progress;
40
+ private lastReported;
41
+ private armed;
42
+ update(hovered: boolean, dt: number, config: Required<DwellConfig>): DwellTick;
43
+ getProgress(): number;
44
+ }
package/dist/gaze.js ADDED
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Gaze support - first-class, per the design decision that gaze serves two
3
+ * roles:
4
+ *
5
+ * 1. **Combination gating** (`gaze.required`): input on an interactable only
6
+ * counts while the viewer is looking at it.
7
+ * 2. **Accessibility** (`gaze.dwell`): looking at an interactable fills a
8
+ * dwell meter; a full meter fires a synthesized press - no hands needed.
9
+ * Consumers render the meter from `dwellProgress` events.
10
+ *
11
+ * The dwell state machine is pure and headless-tested. The runtime feeds it
12
+ * hover booleans; it never sees the engine.
13
+ */
14
+ export const DWELL_DEFAULTS = {
15
+ holdSeconds: 1.8,
16
+ decayFactor: 2.5,
17
+ rearmBelow: 0.1,
18
+ };
19
+ export function resolveDwellConfig(config, defaults = DWELL_DEFAULTS) {
20
+ if (!config)
21
+ return null;
22
+ if (config === true)
23
+ return defaults;
24
+ return {
25
+ holdSeconds: config.holdSeconds ?? defaults.holdSeconds,
26
+ decayFactor: config.decayFactor ?? defaults.decayFactor,
27
+ rearmBelow: config.rearmBelow ?? defaults.rearmBelow,
28
+ };
29
+ }
30
+ export class DwellState {
31
+ progress = 0;
32
+ lastReported = 0;
33
+ armed = true;
34
+ update(hovered, dt, config) {
35
+ const before = this.progress;
36
+ if (hovered) {
37
+ this.progress = Math.min(1, this.progress + dt / config.holdSeconds);
38
+ }
39
+ else {
40
+ this.progress = Math.max(0, this.progress - (dt * config.decayFactor) / config.holdSeconds);
41
+ }
42
+ let fired = false;
43
+ if (this.armed && this.progress >= 1) {
44
+ fired = true;
45
+ this.armed = false;
46
+ this.progress = 0;
47
+ }
48
+ else if (!this.armed && this.progress <= config.rearmBelow) {
49
+ this.armed = true;
50
+ }
51
+ const changed = Math.abs(this.progress - this.lastReported) >= 0.01 ||
52
+ (this.progress === 0 && before !== 0);
53
+ if (changed)
54
+ this.lastReported = this.progress;
55
+ return { progress: this.progress, changed, fired };
56
+ }
57
+ getProgress() {
58
+ return this.progress;
59
+ }
60
+ }
61
+ //# sourceMappingURL=gaze.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gaze.js","sourceRoot":"","sources":["../src/gaze.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAkBH,MAAM,CAAC,MAAM,cAAc,GAA0B;IACnD,WAAW,EAAE,GAAG;IAChB,WAAW,EAAE,GAAG;IAChB,UAAU,EAAE,GAAG;CAChB,CAAC;AAEF,MAAM,UAAU,kBAAkB,CAChC,MAAyC,EACzC,WAAkC,cAAc;IAEhD,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAC;IACzB,IAAI,MAAM,KAAK,IAAI;QAAE,OAAO,QAAQ,CAAC;IACrC,OAAO;QACL,WAAW,EAAE,MAAM,CAAC,WAAW,IAAI,QAAQ,CAAC,WAAW;QACvD,WAAW,EAAE,MAAM,CAAC,WAAW,IAAI,QAAQ,CAAC,WAAW;QACvD,UAAU,EAAE,MAAM,CAAC,UAAU,IAAI,QAAQ,CAAC,UAAU;KACrD,CAAC;AACJ,CAAC;AAWD,MAAM,OAAO,UAAU;IACb,QAAQ,GAAG,CAAC,CAAC;IACb,YAAY,GAAG,CAAC,CAAC;IACjB,KAAK,GAAG,IAAI,CAAC;IAErB,MAAM,CAAC,OAAgB,EAAE,EAAU,EAAE,MAA6B;QAChE,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC;QAC7B,IAAI,OAAO,EAAE,CAAC;YACZ,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,QAAQ,GAAG,EAAE,GAAG,MAAM,CAAC,WAAW,CAAC,CAAC;QACvE,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,GAAG,CACtB,CAAC,EACD,IAAI,CAAC,QAAQ,GAAG,CAAC,EAAE,GAAG,MAAM,CAAC,WAAW,CAAC,GAAG,MAAM,CAAC,WAAW,CAC/D,CAAC;QACJ,CAAC;QAED,IAAI,KAAK,GAAG,KAAK,CAAC;QAClB,IAAI,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,QAAQ,IAAI,CAAC,EAAE,CAAC;YACrC,KAAK,GAAG,IAAI,CAAC;YACb,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;YACnB,IAAI,CAAC,QAAQ,GAAG,CAAC,CAAC;QACpB,CAAC;aAAM,IAAI,CAAC,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,QAAQ,IAAI,MAAM,CAAC,UAAU,EAAE,CAAC;YAC7D,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;QACpB,CAAC;QAED,MAAM,OAAO,GACX,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,YAAY,CAAC,IAAI,IAAI;YACnD,CAAC,IAAI,CAAC,QAAQ,KAAK,CAAC,IAAI,MAAM,KAAK,CAAC,CAAC,CAAC;QACxC,IAAI,OAAO;YAAE,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC,QAAQ,CAAC;QAC/C,OAAO,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IACrD,CAAC;IAED,WAAW;QACT,OAAO,IAAI,CAAC,QAAQ,CAAC;IACvB,CAAC;CACF","sourcesContent":["/**\n * Gaze support - first-class, per the design decision that gaze serves two\n * roles:\n *\n * 1. **Combination gating** (`gaze.required`): input on an interactable only\n * counts while the viewer is looking at it.\n * 2. **Accessibility** (`gaze.dwell`): looking at an interactable fills a\n * dwell meter; a full meter fires a synthesized press - no hands needed.\n * Consumers render the meter from `dwellProgress` events.\n *\n * The dwell state machine is pure and headless-tested. The runtime feeds it\n * hover booleans; it never sees the engine.\n */\n\nexport interface DwellConfig {\n /** Seconds of sustained gaze to fire. */\n holdSeconds?: number;\n /** Drain speed multiplier when looking away. */\n decayFactor?: number;\n /** After firing, progress must fall below this before it can re-arm. */\n rearmBelow?: number;\n}\n\nexport interface GazeConfig {\n /** Non-gaze input on this interactable only counts while gazed at. */\n required?: boolean;\n /** Enable dwell-to-press. `true` uses defaults. */\n dwell?: boolean | DwellConfig;\n}\n\nexport const DWELL_DEFAULTS: Required<DwellConfig> = {\n holdSeconds: 1.8,\n decayFactor: 2.5,\n rearmBelow: 0.1,\n};\n\nexport function resolveDwellConfig(\n config: boolean | DwellConfig | undefined,\n defaults: Required<DwellConfig> = DWELL_DEFAULTS,\n): Required<DwellConfig> | null {\n if (!config) return null;\n if (config === true) return defaults;\n return {\n holdSeconds: config.holdSeconds ?? defaults.holdSeconds,\n decayFactor: config.decayFactor ?? defaults.decayFactor,\n rearmBelow: config.rearmBelow ?? defaults.rearmBelow,\n };\n}\n\nexport interface DwellTick {\n /** Dwell meter 0..1. */\n progress: number;\n /** Progress moved enough to be worth re-rendering (≥ 1%). */\n changed: boolean;\n /** The meter just completed - fire the synthesized press. */\n fired: boolean;\n}\n\nexport class DwellState {\n private progress = 0;\n private lastReported = 0;\n private armed = true;\n\n update(hovered: boolean, dt: number, config: Required<DwellConfig>): DwellTick {\n const before = this.progress;\n if (hovered) {\n this.progress = Math.min(1, this.progress + dt / config.holdSeconds);\n } else {\n this.progress = Math.max(\n 0,\n this.progress - (dt * config.decayFactor) / config.holdSeconds,\n );\n }\n\n let fired = false;\n if (this.armed && this.progress >= 1) {\n fired = true;\n this.armed = false;\n this.progress = 0;\n } else if (!this.armed && this.progress <= config.rearmBelow) {\n this.armed = true;\n }\n\n const changed =\n Math.abs(this.progress - this.lastReported) >= 0.01 ||\n (this.progress === 0 && before !== 0);\n if (changed) this.lastReported = this.progress;\n return { progress: this.progress, changed, fired };\n }\n\n getProgress(): number {\n return this.progress;\n }\n}\n"]}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * @realitycollective/webxr-interactions - the engine-free interaction core.
3
+ *
4
+ * Layers (each strictly on the one below):
5
+ * - behaviours (press, pulse, hinge, dial, slide, grab, tossScore) -
6
+ * pure state machines driven by `update(dt)`;
7
+ * - the runtime/binder - targeting, press/grab transitions, gaze gating
8
+ * + dwell, capability negotiation, events, feedback intents;
9
+ * - the shared input contracts, re-exported from
10
+ * `@realitycollective/webxr-input`.
11
+ *
12
+ * Pair with an engine adapter: `@realitycollective/threejs-interactions`
13
+ * (standalone WebXR + three.js), `@realitycollective/iwsdk-interactions`
14
+ * (Meta IWSDK) or `@realitycollective/xrblocks-interactions` (Google XR
15
+ * Blocks, experimental).
16
+ */
17
+ export * from "@realitycollective/webxr-input";
18
+ export * from "./math.js";
19
+ export * from "./events.js";
20
+ export * from "./feedback.js";
21
+ export * from "./ports.js";
22
+ export * from "./gaze.js";
23
+ export * from "./descriptor.js";
24
+ export * from "./runtime.js";
25
+ export * from "./pointer-bridge.js";
26
+ export * from "./behaviours/behaviour.js";
27
+ export * from "./behaviours/press.js";
28
+ export * from "./behaviours/pulse.js";
29
+ export * from "./behaviours/hinge.js";
30
+ export * from "./behaviours/dial.js";
31
+ export * from "./behaviours/slide.js";
32
+ export * from "./behaviours/grab.js";
33
+ export * from "./behaviours/toss-score.js";
34
+ export * from "./behaviours/factory.js";
package/dist/index.js ADDED
@@ -0,0 +1,35 @@
1
+ /**
2
+ * @realitycollective/webxr-interactions - the engine-free interaction core.
3
+ *
4
+ * Layers (each strictly on the one below):
5
+ * - behaviours (press, pulse, hinge, dial, slide, grab, tossScore) -
6
+ * pure state machines driven by `update(dt)`;
7
+ * - the runtime/binder - targeting, press/grab transitions, gaze gating
8
+ * + dwell, capability negotiation, events, feedback intents;
9
+ * - the shared input contracts, re-exported from
10
+ * `@realitycollective/webxr-input`.
11
+ *
12
+ * Pair with an engine adapter: `@realitycollective/threejs-interactions`
13
+ * (standalone WebXR + three.js), `@realitycollective/iwsdk-interactions`
14
+ * (Meta IWSDK) or `@realitycollective/xrblocks-interactions` (Google XR
15
+ * Blocks, experimental).
16
+ */
17
+ export * from "@realitycollective/webxr-input";
18
+ export * from "./math.js";
19
+ export * from "./events.js";
20
+ export * from "./feedback.js";
21
+ export * from "./ports.js";
22
+ export * from "./gaze.js";
23
+ export * from "./descriptor.js";
24
+ export * from "./runtime.js";
25
+ export * from "./pointer-bridge.js";
26
+ export * from "./behaviours/behaviour.js";
27
+ export * from "./behaviours/press.js";
28
+ export * from "./behaviours/pulse.js";
29
+ export * from "./behaviours/hinge.js";
30
+ export * from "./behaviours/dial.js";
31
+ export * from "./behaviours/slide.js";
32
+ export * from "./behaviours/grab.js";
33
+ export * from "./behaviours/toss-score.js";
34
+ export * from "./behaviours/factory.js";
35
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,cAAc,gCAAgC,CAAC;AAE/C,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,iBAAiB,CAAC;AAChC,cAAc,cAAc,CAAC;AAC7B,cAAc,qBAAqB,CAAC;AAEpC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,uBAAuB,CAAC;AACtC,cAAc,uBAAuB,CAAC;AACtC,cAAc,uBAAuB,CAAC;AACtC,cAAc,sBAAsB,CAAC;AACrC,cAAc,uBAAuB,CAAC;AACtC,cAAc,sBAAsB,CAAC;AACrC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,yBAAyB,CAAC","sourcesContent":["/**\n * @realitycollective/webxr-interactions - the engine-free interaction core.\n *\n * Layers (each strictly on the one below):\n * - behaviours (press, pulse, hinge, dial, slide, grab, tossScore) -\n * pure state machines driven by `update(dt)`;\n * - the runtime/binder - targeting, press/grab transitions, gaze gating\n * + dwell, capability negotiation, events, feedback intents;\n * - the shared input contracts, re-exported from\n * `@realitycollective/webxr-input`.\n *\n * Pair with an engine adapter: `@realitycollective/threejs-interactions`\n * (standalone WebXR + three.js), `@realitycollective/iwsdk-interactions`\n * (Meta IWSDK) or `@realitycollective/xrblocks-interactions` (Google XR\n * Blocks, experimental).\n */\nexport * from \"@realitycollective/webxr-input\";\n\nexport * from \"./math.js\";\nexport * from \"./events.js\";\nexport * from \"./feedback.js\";\nexport * from \"./ports.js\";\nexport * from \"./gaze.js\";\nexport * from \"./descriptor.js\";\nexport * from \"./runtime.js\";\nexport * from \"./pointer-bridge.js\";\n\nexport * from \"./behaviours/behaviour.js\";\nexport * from \"./behaviours/press.js\";\nexport * from \"./behaviours/pulse.js\";\nexport * from \"./behaviours/hinge.js\";\nexport * from \"./behaviours/dial.js\";\nexport * from \"./behaviours/slide.js\";\nexport * from \"./behaviours/grab.js\";\nexport * from \"./behaviours/toss-score.js\";\nexport * from \"./behaviours/factory.js\";\n"]}
package/dist/math.d.ts ADDED
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Pure interaction math - the proven motion functions extracted from the
3
+ * Pale Signal research (springs, hinge/dial normalisation, hoop scoring),
4
+ * plus the tuple vector/quaternion helpers the binder and behaviours need.
5
+ * No engine imports; everything is unit-tested headless.
6
+ */
7
+ import type { QuatTuple, RayTuple, Vec3Tuple } from "@realitycollective/webxr-input";
8
+ /** Clamp to the unit interval. */
9
+ export declare function clamp01(t: number): number;
10
+ /**
11
+ * Normalize a measured displacement/angle against a signed range to 0..1.
12
+ * Sign-agnostic: a range of `-π/2` maps `-π/2 → 1`.
13
+ */
14
+ export declare function normalize01(value: number, range: number): number;
15
+ /**
16
+ * Did a tossed object pass *through* a hoop this frame? True when it crossed
17
+ * the hoop plane in the scoring direction (downward - `prev` above, `curr`
18
+ * at/below) while inside the ring (`radial <= radius`).
19
+ */
20
+ export declare function passedThroughHoop(prevAlong: number, currAlong: number, planeAlong: number, radial: number, radius: number): boolean;
21
+ /**
22
+ * One step of a damped harmonic oscillator (semi-implicit Euler).
23
+ * `out[0]` = next value, `out[1]` = next velocity. An under-damped spring
24
+ * (damping ratio ≈ 0.3-0.5) is what gives a pressed button its bounce.
25
+ * `dt` is clamped to 1/30 s so a long frame can't blow the integrator up.
26
+ */
27
+ export declare function springStep(value: number, velocity: number, target: number, stiffness: number, damping: number, dt: number, out?: Float32Array): Float32Array;
28
+ /** Exponential decay of `value` toward 0 over `dt` at `ratePerSec`. */
29
+ export declare function decay(value: number, ratePerSec: number, dt: number): number;
30
+ /** Penner's ease-out-bounce on `t ∈ [0,1]`. */
31
+ export declare function easeOutBounce(t: number): number;
32
+ /** Clamp a value to the symmetric range `[-|mag|, +|mag|]`. */
33
+ export declare function clampSym(x: number, mag: number): number;
34
+ /**
35
+ * Hinge angle for a position-driven lever: given the hand's offset from the
36
+ * pivot resolved into the hinge plane - `alongRest` (toward the arm's rest
37
+ * direction) and `alongSwing` (the in-plane perpendicular) - the angle the
38
+ * arm should take to point at the hand, clamped to `±|maxMag|`.
39
+ */
40
+ export declare function hingeAngleToHand(alongRest: number, alongSwing: number, maxMag: number): number;
41
+ /**
42
+ * Map a centered hinge angle in `[-|mag|, +|mag|]` to a 0..1 analog value
43
+ * with the rest (0) at 0.5 - a bidirectional lever as one normalized value.
44
+ */
45
+ export declare function centeredUnit(angle: number, mag: number): number;
46
+ export declare function vSub(a: Vec3Tuple, b: Vec3Tuple): Vec3Tuple;
47
+ export declare function vAdd(a: Vec3Tuple, b: Vec3Tuple): Vec3Tuple;
48
+ export declare function vScale(a: Vec3Tuple, s: number): Vec3Tuple;
49
+ export declare function vDot(a: Vec3Tuple, b: Vec3Tuple): number;
50
+ export declare function vCross(a: Vec3Tuple, b: Vec3Tuple): Vec3Tuple;
51
+ export declare function vLength(a: Vec3Tuple): number;
52
+ export declare function vDistance(a: Vec3Tuple, b: Vec3Tuple): number;
53
+ export declare function vNormalize(a: Vec3Tuple): Vec3Tuple;
54
+ /** Quaternion from an axis (normalised) and an angle in radians. */
55
+ export declare function quatFromAxisAngle(axis: Vec3Tuple, angle: number): QuatTuple;
56
+ export declare function quatMultiply(a: QuatTuple, b: QuatTuple): QuatTuple;
57
+ export declare function quatConjugate(q: QuatTuple): QuatTuple;
58
+ /** Rotate a vector by a quaternion. */
59
+ export declare function vApplyQuat(v: Vec3Tuple, q: QuatTuple): Vec3Tuple;
60
+ /** Transform a world-space point into the local frame of `pose`. */
61
+ export declare function worldToLocal(point: Vec3Tuple, posePosition: Vec3Tuple, poseQuaternion: QuatTuple): Vec3Tuple;
62
+ /** Closest-approach distance from a ray to a point, and the along-ray t. */
63
+ export declare function rayPointDistance(ray: RayTuple, point: Vec3Tuple): {
64
+ distance: number;
65
+ t: number;
66
+ };
67
+ /** Angular offset (radians) between a ray direction and the direction to a point. */
68
+ export declare function rayAngleTo(ray: RayTuple, point: Vec3Tuple): number;
package/dist/math.js ADDED
@@ -0,0 +1,164 @@
1
+ /** Clamp to the unit interval. */
2
+ export function clamp01(t) {
3
+ return t < 0 ? 0 : t > 1 ? 1 : t;
4
+ }
5
+ /**
6
+ * Normalize a measured displacement/angle against a signed range to 0..1.
7
+ * Sign-agnostic: a range of `-π/2` maps `-π/2 → 1`.
8
+ */
9
+ export function normalize01(value, range) {
10
+ if (range === 0)
11
+ return 0;
12
+ return clamp01(value / range);
13
+ }
14
+ /**
15
+ * Did a tossed object pass *through* a hoop this frame? True when it crossed
16
+ * the hoop plane in the scoring direction (downward - `prev` above, `curr`
17
+ * at/below) while inside the ring (`radial <= radius`).
18
+ */
19
+ export function passedThroughHoop(prevAlong, currAlong, planeAlong, radial, radius) {
20
+ const crossedDown = prevAlong > planeAlong && currAlong <= planeAlong;
21
+ return crossedDown && radial <= radius;
22
+ }
23
+ /**
24
+ * One step of a damped harmonic oscillator (semi-implicit Euler).
25
+ * `out[0]` = next value, `out[1]` = next velocity. An under-damped spring
26
+ * (damping ratio ≈ 0.3-0.5) is what gives a pressed button its bounce.
27
+ * `dt` is clamped to 1/30 s so a long frame can't blow the integrator up.
28
+ */
29
+ export function springStep(value, velocity, target, stiffness, damping, dt, out = new Float32Array(2)) {
30
+ const h = Math.min(dt, 1 / 30);
31
+ const accel = -stiffness * (value - target) - damping * velocity;
32
+ const nextVelocity = velocity + accel * h;
33
+ out[0] = value + nextVelocity * h;
34
+ out[1] = nextVelocity;
35
+ return out;
36
+ }
37
+ /** Exponential decay of `value` toward 0 over `dt` at `ratePerSec`. */
38
+ export function decay(value, ratePerSec, dt) {
39
+ const next = value * Math.exp(-ratePerSec * Math.max(dt, 0));
40
+ return next < 1e-4 ? 0 : next;
41
+ }
42
+ /** Penner's ease-out-bounce on `t ∈ [0,1]`. */
43
+ export function easeOutBounce(t) {
44
+ const n1 = 7.5625;
45
+ const d1 = 2.75;
46
+ let x = clamp01(t);
47
+ if (x < 1 / d1)
48
+ return n1 * x * x;
49
+ if (x < 2 / d1)
50
+ return n1 * (x -= 1.5 / d1) * x + 0.75;
51
+ if (x < 2.5 / d1)
52
+ return n1 * (x -= 2.25 / d1) * x + 0.9375;
53
+ return n1 * (x -= 2.625 / d1) * x + 0.984375;
54
+ }
55
+ /** Clamp a value to the symmetric range `[-|mag|, +|mag|]`. */
56
+ export function clampSym(x, mag) {
57
+ const m = Math.abs(mag);
58
+ return x < -m ? -m : x > m ? m : x;
59
+ }
60
+ /**
61
+ * Hinge angle for a position-driven lever: given the hand's offset from the
62
+ * pivot resolved into the hinge plane - `alongRest` (toward the arm's rest
63
+ * direction) and `alongSwing` (the in-plane perpendicular) - the angle the
64
+ * arm should take to point at the hand, clamped to `±|maxMag|`.
65
+ */
66
+ export function hingeAngleToHand(alongRest, alongSwing, maxMag) {
67
+ return clampSym(Math.atan2(alongSwing, alongRest), maxMag);
68
+ }
69
+ /**
70
+ * Map a centered hinge angle in `[-|mag|, +|mag|]` to a 0..1 analog value
71
+ * with the rest (0) at 0.5 - a bidirectional lever as one normalized value.
72
+ */
73
+ export function centeredUnit(angle, mag) {
74
+ const m = Math.abs(mag);
75
+ if (m < 1e-6)
76
+ return 0.5;
77
+ return clamp01(0.5 + (0.5 * angle) / m);
78
+ }
79
+ // ---------------------------------------------------------------------------
80
+ // Tuple vector / quaternion helpers (allocation-light, no engine types).
81
+ // ---------------------------------------------------------------------------
82
+ export function vSub(a, b) {
83
+ return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
84
+ }
85
+ export function vAdd(a, b) {
86
+ return [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
87
+ }
88
+ export function vScale(a, s) {
89
+ return [a[0] * s, a[1] * s, a[2] * s];
90
+ }
91
+ export function vDot(a, b) {
92
+ return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
93
+ }
94
+ export function vCross(a, b) {
95
+ return [
96
+ a[1] * b[2] - a[2] * b[1],
97
+ a[2] * b[0] - a[0] * b[2],
98
+ a[0] * b[1] - a[1] * b[0],
99
+ ];
100
+ }
101
+ export function vLength(a) {
102
+ return Math.hypot(a[0], a[1], a[2]);
103
+ }
104
+ export function vDistance(a, b) {
105
+ return Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]);
106
+ }
107
+ export function vNormalize(a) {
108
+ const l = vLength(a);
109
+ return l < 1e-9 ? [0, 0, 0] : [a[0] / l, a[1] / l, a[2] / l];
110
+ }
111
+ /** Quaternion from an axis (normalised) and an angle in radians. */
112
+ export function quatFromAxisAngle(axis, angle) {
113
+ const half = angle / 2;
114
+ const s = Math.sin(half);
115
+ return [axis[0] * s, axis[1] * s, axis[2] * s, Math.cos(half)];
116
+ }
117
+ export function quatMultiply(a, b) {
118
+ const [ax, ay, az, aw] = a;
119
+ const [bx, by, bz, bw] = b;
120
+ return [
121
+ aw * bx + ax * bw + ay * bz - az * by,
122
+ aw * by - ax * bz + ay * bw + az * bx,
123
+ aw * bz + ax * by - ay * bx + az * bw,
124
+ aw * bw - ax * bx - ay * by - az * bz,
125
+ ];
126
+ }
127
+ export function quatConjugate(q) {
128
+ return [-q[0], -q[1], -q[2], q[3]];
129
+ }
130
+ /** Rotate a vector by a quaternion. */
131
+ export function vApplyQuat(v, q) {
132
+ const [x, y, z] = v;
133
+ const [qx, qy, qz, qw] = q;
134
+ // t = 2 * cross(q.xyz, v)
135
+ const tx = 2 * (qy * z - qz * y);
136
+ const ty = 2 * (qz * x - qx * z);
137
+ const tz = 2 * (qx * y - qy * x);
138
+ // v + qw * t + cross(q.xyz, t)
139
+ return [
140
+ x + qw * tx + qy * tz - qz * ty,
141
+ y + qw * ty + qz * tx - qx * tz,
142
+ z + qw * tz + qx * ty - qy * tx,
143
+ ];
144
+ }
145
+ /** Transform a world-space point into the local frame of `pose`. */
146
+ export function worldToLocal(point, posePosition, poseQuaternion) {
147
+ return vApplyQuat(vSub(point, posePosition), quatConjugate(poseQuaternion));
148
+ }
149
+ /** Closest-approach distance from a ray to a point, and the along-ray t. */
150
+ export function rayPointDistance(ray, point) {
151
+ const toPoint = vSub(point, ray.origin);
152
+ const t = vDot(toPoint, ray.direction);
153
+ if (t <= 0)
154
+ return { distance: vLength(toPoint), t: 0 };
155
+ const closest = vAdd(ray.origin, vScale(ray.direction, t));
156
+ return { distance: vDistance(closest, point), t };
157
+ }
158
+ /** Angular offset (radians) between a ray direction and the direction to a point. */
159
+ export function rayAngleTo(ray, point) {
160
+ const dir = vNormalize(vSub(point, ray.origin));
161
+ const d = vDot(vNormalize(ray.direction), dir);
162
+ return Math.acos(Math.min(1, Math.max(-1, d)));
163
+ }
164
+ //# sourceMappingURL=math.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"math.js","sourceRoot":"","sources":["../src/math.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Pure interaction math - the proven motion functions extracted from the\n * Pale Signal research (springs, hinge/dial normalisation, hoop scoring),\n * plus the tuple vector/quaternion helpers the binder and behaviours need.\n * No engine imports; everything is unit-tested headless.\n */\nimport type { QuatTuple, RayTuple, Vec3Tuple } from \"@realitycollective/webxr-input\";\n\n/** Clamp to the unit interval. */\nexport function clamp01(t: number): number {\n return t < 0 ? 0 : t > 1 ? 1 : t;\n}\n\n/**\n * Normalize a measured displacement/angle against a signed range to 0..1.\n * Sign-agnostic: a range of `-π/2` maps `-π/2 → 1`.\n */\nexport function normalize01(value: number, range: number): number {\n if (range === 0) return 0;\n return clamp01(value / range);\n}\n\n/**\n * Did a tossed object pass *through* a hoop this frame? True when it crossed\n * the hoop plane in the scoring direction (downward - `prev` above, `curr`\n * at/below) while inside the ring (`radial <= radius`).\n */\nexport function passedThroughHoop(\n prevAlong: number,\n currAlong: number,\n planeAlong: number,\n radial: number,\n radius: number,\n): boolean {\n const crossedDown = prevAlong > planeAlong && currAlong <= planeAlong;\n return crossedDown && radial <= radius;\n}\n\n/**\n * One step of a damped harmonic oscillator (semi-implicit Euler).\n * `out[0]` = next value, `out[1]` = next velocity. An under-damped spring\n * (damping ratio ≈ 0.3-0.5) is what gives a pressed button its bounce.\n * `dt` is clamped to 1/30 s so a long frame can't blow the integrator up.\n */\nexport function springStep(\n value: number,\n velocity: number,\n target: number,\n stiffness: number,\n damping: number,\n dt: number,\n out: Float32Array = new Float32Array(2),\n): Float32Array {\n const h = Math.min(dt, 1 / 30);\n const accel = -stiffness * (value - target) - damping * velocity;\n const nextVelocity = velocity + accel * h;\n out[0] = value + nextVelocity * h;\n out[1] = nextVelocity;\n return out;\n}\n\n/** Exponential decay of `value` toward 0 over `dt` at `ratePerSec`. */\nexport function decay(value: number, ratePerSec: number, dt: number): number {\n const next = value * Math.exp(-ratePerSec * Math.max(dt, 0));\n return next < 1e-4 ? 0 : next;\n}\n\n/** Penner's ease-out-bounce on `t ∈ [0,1]`. */\nexport function easeOutBounce(t: number): number {\n const n1 = 7.5625;\n const d1 = 2.75;\n let x = clamp01(t);\n if (x < 1 / d1) return n1 * x * x;\n if (x < 2 / d1) return n1 * (x -= 1.5 / d1) * x + 0.75;\n if (x < 2.5 / d1) return n1 * (x -= 2.25 / d1) * x + 0.9375;\n return n1 * (x -= 2.625 / d1) * x + 0.984375;\n}\n\n/** Clamp a value to the symmetric range `[-|mag|, +|mag|]`. */\nexport function clampSym(x: number, mag: number): number {\n const m = Math.abs(mag);\n return x < -m ? -m : x > m ? m : x;\n}\n\n/**\n * Hinge angle for a position-driven lever: given the hand's offset from the\n * pivot resolved into the hinge plane - `alongRest` (toward the arm's rest\n * direction) and `alongSwing` (the in-plane perpendicular) - the angle the\n * arm should take to point at the hand, clamped to `±|maxMag|`.\n */\nexport function hingeAngleToHand(alongRest: number, alongSwing: number, maxMag: number): number {\n return clampSym(Math.atan2(alongSwing, alongRest), maxMag);\n}\n\n/**\n * Map a centered hinge angle in `[-|mag|, +|mag|]` to a 0..1 analog value\n * with the rest (0) at 0.5 - a bidirectional lever as one normalized value.\n */\nexport function centeredUnit(angle: number, mag: number): number {\n const m = Math.abs(mag);\n if (m < 1e-6) return 0.5;\n return clamp01(0.5 + (0.5 * angle) / m);\n}\n\n// ---------------------------------------------------------------------------\n// Tuple vector / quaternion helpers (allocation-light, no engine types).\n// ---------------------------------------------------------------------------\n\nexport function vSub(a: Vec3Tuple, b: Vec3Tuple): Vec3Tuple {\n return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];\n}\n\nexport function vAdd(a: Vec3Tuple, b: Vec3Tuple): Vec3Tuple {\n return [a[0] + b[0], a[1] + b[1], a[2] + b[2]];\n}\n\nexport function vScale(a: Vec3Tuple, s: number): Vec3Tuple {\n return [a[0] * s, a[1] * s, a[2] * s];\n}\n\nexport function vDot(a: Vec3Tuple, b: Vec3Tuple): number {\n return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];\n}\n\nexport function vCross(a: Vec3Tuple, b: Vec3Tuple): Vec3Tuple {\n return [\n a[1] * b[2] - a[2] * b[1],\n a[2] * b[0] - a[0] * b[2],\n a[0] * b[1] - a[1] * b[0],\n ];\n}\n\nexport function vLength(a: Vec3Tuple): number {\n return Math.hypot(a[0], a[1], a[2]);\n}\n\nexport function vDistance(a: Vec3Tuple, b: Vec3Tuple): number {\n return Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]);\n}\n\nexport function vNormalize(a: Vec3Tuple): Vec3Tuple {\n const l = vLength(a);\n return l < 1e-9 ? [0, 0, 0] : [a[0] / l, a[1] / l, a[2] / l];\n}\n\n/** Quaternion from an axis (normalised) and an angle in radians. */\nexport function quatFromAxisAngle(axis: Vec3Tuple, angle: number): QuatTuple {\n const half = angle / 2;\n const s = Math.sin(half);\n return [axis[0] * s, axis[1] * s, axis[2] * s, Math.cos(half)];\n}\n\nexport function quatMultiply(a: QuatTuple, b: QuatTuple): QuatTuple {\n const [ax, ay, az, aw] = a;\n const [bx, by, bz, bw] = b;\n return [\n aw * bx + ax * bw + ay * bz - az * by,\n aw * by - ax * bz + ay * bw + az * bx,\n aw * bz + ax * by - ay * bx + az * bw,\n aw * bw - ax * bx - ay * by - az * bz,\n ];\n}\n\nexport function quatConjugate(q: QuatTuple): QuatTuple {\n return [-q[0], -q[1], -q[2], q[3]];\n}\n\n/** Rotate a vector by a quaternion. */\nexport function vApplyQuat(v: Vec3Tuple, q: QuatTuple): Vec3Tuple {\n const [x, y, z] = v;\n const [qx, qy, qz, qw] = q;\n // t = 2 * cross(q.xyz, v)\n const tx = 2 * (qy * z - qz * y);\n const ty = 2 * (qz * x - qx * z);\n const tz = 2 * (qx * y - qy * x);\n // v + qw * t + cross(q.xyz, t)\n return [\n x + qw * tx + qy * tz - qz * ty,\n y + qw * ty + qz * tx - qx * tz,\n z + qw * tz + qx * ty - qy * tx,\n ];\n}\n\n/** Transform a world-space point into the local frame of `pose`. */\nexport function worldToLocal(\n point: Vec3Tuple,\n posePosition: Vec3Tuple,\n poseQuaternion: QuatTuple,\n): Vec3Tuple {\n return vApplyQuat(vSub(point, posePosition), quatConjugate(poseQuaternion));\n}\n\n/** Closest-approach distance from a ray to a point, and the along-ray t. */\nexport function rayPointDistance(ray: RayTuple, point: Vec3Tuple): { distance: number; t: number } {\n const toPoint = vSub(point, ray.origin);\n const t = vDot(toPoint, ray.direction);\n if (t <= 0) return { distance: vLength(toPoint), t: 0 };\n const closest = vAdd(ray.origin, vScale(ray.direction, t));\n return { distance: vDistance(closest, point), t };\n}\n\n/** Angular offset (radians) between a ray direction and the direction to a point. */\nexport function rayAngleTo(ray: RayTuple, point: Vec3Tuple): number {\n const dir = vNormalize(vSub(point, ray.origin));\n const d = vDot(vNormalize(ray.direction), dir);\n return Math.acos(Math.min(1, Math.max(-1, d)));\n}\n"]}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Pointer bridge - drive the UI Extensions (or anything speaking the
3
+ * shared pointer contract) from the SAME input stack that drives the
4
+ * interactions. One provider, both families: this is the coexistence
5
+ * contract in code.
6
+ *
7
+ * The returned object implements `PointerInputSource` (structurally
8
+ * identical in `@realitycollective/webxr-input` and
9
+ * `@realitycollective/webxr-uiextensions`): press-move-release derived
10
+ * from a source's ray + select signal, fed by the runtime's per-frame
11
+ * sampling.
12
+ */
13
+ import { type PointerInputSource } from "@realitycollective/webxr-input";
14
+ import type { InteractionRuntime } from "./runtime.js";
15
+ export interface PointerBridge {
16
+ source: PointerInputSource;
17
+ dispose(): void;
18
+ }
19
+ /**
20
+ * Create a pointer stream from the runtime's sampled sources.
21
+ * `match` picks which source drives it (default: any source with a ray -
22
+ * first pressed wins and holds until release).
23
+ */
24
+ export declare function createPointerBridge(runtime: InteractionRuntime, match?: (sourceId: string) => boolean): PointerBridge;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Pointer bridge - drive the UI Extensions (or anything speaking the
3
+ * shared pointer contract) from the SAME input stack that drives the
4
+ * interactions. One provider, both families: this is the coexistence
5
+ * contract in code.
6
+ *
7
+ * The returned object implements `PointerInputSource` (structurally
8
+ * identical in `@realitycollective/webxr-input` and
9
+ * `@realitycollective/webxr-uiextensions`): press-move-release derived
10
+ * from a source's ray + select signal, fed by the runtime's per-frame
11
+ * sampling.
12
+ */
13
+ import { SELECT_PRESS_THRESHOLD, SELECT_RELEASE_THRESHOLD, } from "@realitycollective/webxr-input";
14
+ import { Emitter } from "./events.js";
15
+ /**
16
+ * Create a pointer stream from the runtime's sampled sources.
17
+ * `match` picks which source drives it (default: any source with a ray -
18
+ * first pressed wins and holds until release).
19
+ */
20
+ export function createPointerBridge(runtime, match = () => true) {
21
+ const press = new Emitter();
22
+ const move = new Emitter();
23
+ const release = new Emitter();
24
+ let activeSourceId = null;
25
+ const unsubscribe = runtime.onSourcesSampled((sources) => {
26
+ if (activeSourceId === null) {
27
+ for (const source of sources) {
28
+ if (!source.ray || !match(source.id))
29
+ continue;
30
+ if (source.select >= SELECT_PRESS_THRESHOLD) {
31
+ activeSourceId = source.id;
32
+ press.emit({ origin: source.ray.origin, direction: source.ray.direction });
33
+ break;
34
+ }
35
+ }
36
+ return;
37
+ }
38
+ const active = sources.find((s) => s.id === activeSourceId);
39
+ if (!active || !active.ray || active.select <= SELECT_RELEASE_THRESHOLD) {
40
+ if (active?.ray) {
41
+ release.emit({ origin: active.ray.origin, direction: active.ray.direction });
42
+ }
43
+ else if (activeSourceId !== null) {
44
+ release.emit({ origin: [0, 0, 0], direction: [0, 0, -1] });
45
+ }
46
+ activeSourceId = null;
47
+ return;
48
+ }
49
+ move.emit({ origin: active.ray.origin, direction: active.ray.direction });
50
+ });
51
+ return {
52
+ source: {
53
+ onPress: (l) => press.subscribe(l),
54
+ onMove: (l) => move.subscribe(l),
55
+ onRelease: (l) => release.subscribe(l),
56
+ },
57
+ dispose: unsubscribe,
58
+ };
59
+ }
60
+ //# sourceMappingURL=pointer-bridge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pointer-bridge.js","sourceRoot":"","sources":["../src/pointer-bridge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EACL,sBAAsB,EACtB,wBAAwB,GAIzB,MAAM,gCAAgC,CAAC;AACxC,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAQtC;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CACjC,OAA2B,EAC3B,QAAuC,GAAG,EAAE,CAAC,IAAI;IAEjD,MAAM,KAAK,GAAG,IAAI,OAAO,EAAiB,CAAC;IAC3C,MAAM,IAAI,GAAG,IAAI,OAAO,EAAiB,CAAC;IAC1C,MAAM,OAAO,GAAG,IAAI,OAAO,EAAiB,CAAC;IAE7C,IAAI,cAAc,GAAkB,IAAI,CAAC;IAEzC,MAAM,WAAW,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC,OAAO,EAAE,EAAE;QACvD,IAAI,cAAc,KAAK,IAAI,EAAE,CAAC;YAC5B,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;gBAC7B,IAAI,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC;oBAAE,SAAS;gBAC/C,IAAI,MAAM,CAAC,MAAM,IAAI,sBAAsB,EAAE,CAAC;oBAC5C,cAAc,GAAG,MAAM,CAAC,EAAE,CAAC;oBAC3B,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,CAAC,GAAG,CAAC,SAAS,EAAE,CAAC,CAAC;oBAC3E,MAAM;gBACR,CAAC;YACH,CAAC;YACD,OAAO;QACT,CAAC;QACD,MAAM,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,cAAc,CAAC,CAAC;QAC5D,IAAI,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,GAAG,IAAI,MAAM,CAAC,MAAM,IAAI,wBAAwB,EAAE,CAAC;YACxE,IAAI,MAAM,EAAE,GAAG,EAAE,CAAC;gBAChB,OAAO,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,CAAC,GAAG,CAAC,SAAS,EAAE,CAAC,CAAC;YAC/E,CAAC;iBAAM,IAAI,cAAc,KAAK,IAAI,EAAE,CAAC;gBACnC,OAAO,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;YAC7D,CAAC;YACD,cAAc,GAAG,IAAI,CAAC;YACtB,OAAO;QACT,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,CAAC,GAAG,CAAC,SAAS,EAAE,CAAC,CAAC;IAC5E,CAAC,CAAC,CAAC;IAEH,OAAO;QACL,MAAM,EAAE;YACN,OAAO,EAAE,CAAC,CAAC,EAAe,EAAE,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC;YAC/C,MAAM,EAAE,CAAC,CAAC,EAAe,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC;YAC7C,SAAS,EAAE,CAAC,CAAC,EAAe,EAAE,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC;SACpD;QACD,OAAO,EAAE,WAAW;KACrB,CAAC;AACJ,CAAC","sourcesContent":["/**\n * Pointer bridge - drive the UI Extensions (or anything speaking the\n * shared pointer contract) from the SAME input stack that drives the\n * interactions. One provider, both families: this is the coexistence\n * contract in code.\n *\n * The returned object implements `PointerInputSource` (structurally\n * identical in `@realitycollective/webxr-input` and\n * `@realitycollective/webxr-uiextensions`): press-move-release derived\n * from a source's ray + select signal, fed by the runtime's per-frame\n * sampling.\n */\nimport {\n SELECT_PRESS_THRESHOLD,\n SELECT_RELEASE_THRESHOLD,\n type PointerInputSource,\n type PointerSample,\n type Unsubscribe,\n} from \"@realitycollective/webxr-input\";\nimport { Emitter } from \"./events.js\";\nimport type { InteractionRuntime } from \"./runtime.js\";\n\nexport interface PointerBridge {\n source: PointerInputSource;\n dispose(): void;\n}\n\n/**\n * Create a pointer stream from the runtime's sampled sources.\n * `match` picks which source drives it (default: any source with a ray -\n * first pressed wins and holds until release).\n */\nexport function createPointerBridge(\n runtime: InteractionRuntime,\n match: (sourceId: string) => boolean = () => true,\n): PointerBridge {\n const press = new Emitter<PointerSample>();\n const move = new Emitter<PointerSample>();\n const release = new Emitter<PointerSample>();\n\n let activeSourceId: string | null = null;\n\n const unsubscribe = runtime.onSourcesSampled((sources) => {\n if (activeSourceId === null) {\n for (const source of sources) {\n if (!source.ray || !match(source.id)) continue;\n if (source.select >= SELECT_PRESS_THRESHOLD) {\n activeSourceId = source.id;\n press.emit({ origin: source.ray.origin, direction: source.ray.direction });\n break;\n }\n }\n return;\n }\n const active = sources.find((s) => s.id === activeSourceId);\n if (!active || !active.ray || active.select <= SELECT_RELEASE_THRESHOLD) {\n if (active?.ray) {\n release.emit({ origin: active.ray.origin, direction: active.ray.direction });\n } else if (activeSourceId !== null) {\n release.emit({ origin: [0, 0, 0], direction: [0, 0, -1] });\n }\n activeSourceId = null;\n return;\n }\n move.emit({ origin: active.ray.origin, direction: active.ray.direction });\n });\n\n return {\n source: {\n onPress: (l): Unsubscribe => press.subscribe(l),\n onMove: (l): Unsubscribe => move.subscribe(l),\n onRelease: (l): Unsubscribe => release.subscribe(l),\n },\n dispose: unsubscribe,\n };\n}\n"]}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Engine-side ports - the space queries and transform writes the core
3
+ * cannot perform itself. Adapters implement these against their scene
4
+ * graph; the core only ever speaks tuples and interactable ids.
5
+ */
6
+ import type { PoseTuple, QuatTuple, RayTuple, Vec3Tuple } from "@realitycollective/webxr-input";
7
+ export interface InteractableHit {
8
+ interactableId: string;
9
+ /** Distance from the query origin (ray origin / probe point). */
10
+ distance: number;
11
+ /** World-space hit or closest point. */
12
+ point: Vec3Tuple;
13
+ }
14
+ /**
15
+ * Resolves which interactable a ray or a proximity probe touches.
16
+ * Implemented by the adapter (three.js Raycaster, engine BVH, …). A
17
+ * provider that pre-resolves targeting (IWSDK) may make this redundant -
18
+ * provider hints always take precedence over hit-tester results.
19
+ */
20
+ export interface HitTester {
21
+ hitRay(ray: RayTuple): InteractableHit | null;
22
+ hitProximity(point: Vec3Tuple, radius: number): InteractableHit | null;
23
+ }
24
+ /**
25
+ * The transform/effect surface of ONE interactable's scene object.
26
+ * Behaviours write through this; the adapter owns how writes land
27
+ * (Object3D fields, ECS components, …).
28
+ *
29
+ * Offsets/rotations are FROM REST: the adapter captures the rest pose at
30
+ * registration and applies these relative to it, so behaviours compose
31
+ * with anything else animating the same object (the pale-signal
32
+ * non-destructive-offset rule).
33
+ */
34
+ export interface TransformPort {
35
+ /** Current world pose. */
36
+ getWorldPose(): PoseTuple;
37
+ /** Local-space pose relative to the captured rest (dragged observation). */
38
+ getLocalOffset(): Vec3Tuple;
39
+ /** Additive local-space offset from rest (driven press / slide). */
40
+ setLocalOffset(offset: Vec3Tuple): void;
41
+ /** Local rotation about the rest orientation (hinge / dial output). */
42
+ setLocalRotation(quaternion: QuatTuple): void;
43
+ /** Follow a world pose while grabbed (poseOnly grab fulfilment). */
44
+ setWorldPose?(pose: PoseTuple): void;
45
+ /** Additive presentation effect (pulse): scale multiplier & emissive boost. */
46
+ setEffect?(effect: {
47
+ scale?: number;
48
+ emissive?: number;
49
+ }): void;
50
+ }
package/dist/ports.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=ports.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ports.js","sourceRoot":"","sources":["../src/ports.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * Engine-side ports - the space queries and transform writes the core\n * cannot perform itself. Adapters implement these against their scene\n * graph; the core only ever speaks tuples and interactable ids.\n */\nimport type { PoseTuple, QuatTuple, RayTuple, Vec3Tuple } from \"@realitycollective/webxr-input\";\n\nexport interface InteractableHit {\n interactableId: string;\n /** Distance from the query origin (ray origin / probe point). */\n distance: number;\n /** World-space hit or closest point. */\n point: Vec3Tuple;\n}\n\n/**\n * Resolves which interactable a ray or a proximity probe touches.\n * Implemented by the adapter (three.js Raycaster, engine BVH, …). A\n * provider that pre-resolves targeting (IWSDK) may make this redundant -\n * provider hints always take precedence over hit-tester results.\n */\nexport interface HitTester {\n hitRay(ray: RayTuple): InteractableHit | null;\n hitProximity(point: Vec3Tuple, radius: number): InteractableHit | null;\n}\n\n/**\n * The transform/effect surface of ONE interactable's scene object.\n * Behaviours write through this; the adapter owns how writes land\n * (Object3D fields, ECS components, …).\n *\n * Offsets/rotations are FROM REST: the adapter captures the rest pose at\n * registration and applies these relative to it, so behaviours compose\n * with anything else animating the same object (the pale-signal\n * non-destructive-offset rule).\n */\nexport interface TransformPort {\n /** Current world pose. */\n getWorldPose(): PoseTuple;\n /** Local-space pose relative to the captured rest (dragged observation). */\n getLocalOffset(): Vec3Tuple;\n /** Additive local-space offset from rest (driven press / slide). */\n setLocalOffset(offset: Vec3Tuple): void;\n /** Local rotation about the rest orientation (hinge / dial output). */\n setLocalRotation(quaternion: QuatTuple): void;\n /** Follow a world pose while grabbed (poseOnly grab fulfilment). */\n setWorldPose?(pose: PoseTuple): void;\n /** Additive presentation effect (pulse): scale multiplier & emissive boost. */\n setEffect?(effect: { scale?: number; emissive?: number }): void;\n}\n"]}