@realitycollective/native-interactions 0.1.1-preview.1 → 0.1.1-preview.3

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.
@@ -1,26 +1,70 @@
1
1
  /**
2
2
  * NativeInputProvider - the native host's input provider.
3
3
  *
4
- * A thin, copying pass-through over the `input` slice a native app (OpenXR,
5
- * visionOS) installs on `globalThis.__rcHost`, or hands in directly for
6
- * tests. Every snapshot, and every tuple inside it, is copied before it
7
- * leaves this class, so a host that reuses its own sample buffers still
8
- * meets the ownership rule the shared contract suite checks.
4
+ * It applies the rules `IWSDKInputProvider` applies, over the facts the
5
+ * native app's `input` slice reports, so the host is handed results:
9
6
  *
10
- * Optional members - `getHeadPose`, `sampleHints`, `pulse`,
11
- * `setPresenceVisible`, `setPresenceModality` - are only ever assigned when
12
- * the host itself carries them, so `typeof provider.pulse` answers honestly
13
- * for a host that cannot pulse rather than a method that is present but
14
- * always returns false.
7
+ * - Capabilities are DERIVED here, as `refreshCapabilities` derives them from
8
+ * the WebXR session: `rays`, `pokes` and grabs from immersion, `handJoints`
9
+ * and `pinch` from hand tracking or a tracked hand, `buttonsAxes` and
10
+ * `haptics` from the sources present, `gaze` and `headPose` from a head
11
+ * pose, `eyeGaze` from the eye-tracking fact and a host that can pose the
12
+ * gaze, `presence` from the host's presence member. They re-derive, and
13
+ * `onCapabilitiesChanged` fires, whenever the facts or the sources change.
14
+ * - Presence is decided here, as `applyPresence` decides it: the requested
15
+ * visibility per side, and the modality, with `"auto"` showing hands while
16
+ * hand joints are live and controllers otherwise. The host is told what to
17
+ * draw per side.
18
+ * - No sources are sampled while the session lacks focus, as IWSDK samples
19
+ * none unless its visibility state is `Visible`.
20
+ * - The eye-gaze rule (`@realitycollective/webxr-input` `eye-gaze.ts`) runs
21
+ * here over the host's raw snapshots and its gaze pose, as IWSDK's
22
+ * `GazePointer` runs it: far rays drop once gaze has a valid pose, a pinch
23
+ * selects, and the pinching hand's ray pose travels as `selectorPose`. A
24
+ * host reports only `eyeTracking` and `getEyeGazePose()`.
25
+ * - A pulse's intensity is clamped to 0..1 before it reaches the host.
26
+ *
27
+ * Every snapshot, and every tuple inside it, is copied before it leaves this
28
+ * class, so a host that reuses its own sample buffers still meets the
29
+ * ownership rule the shared contract suite checks.
15
30
  */
16
- import { type Handedness, type HeadPose, type InputCapabilities, type InputHitHint, type InputProvider, type InputSourceSnapshot, type PresenceModality, type Unsubscribe } from "@realitycollective/webxr-input";
31
+ import { type EyeGazeOptions, type Handedness, type HeadPose, type InputCapabilities, type InputHitHint, type InputProvider, type InputSourceSnapshot, type PresenceModality, type Unsubscribe } from "@realitycollective/webxr-input";
17
32
  import { type NativeInputHost } from "./native-types.js";
18
33
  export interface NativeInputProviderOptions {
19
34
  /** The `input` slice. Omit to read `globalThis.__rcHost.input`. */
20
35
  input?: NativeInputHost;
36
+ /**
37
+ * The native app fulfils grabs with its own physics, so grabs report
38
+ * `"native"` and the runtime never carries the object. Default `false`:
39
+ * `"poseOnly"`, the runtime carries it. The same option as IWSDK's
40
+ * `IWSDKProviderOptions.nativeGrab`.
41
+ */
42
+ nativeGrab?: boolean;
43
+ /**
44
+ * Tuning for the eye-gaze rule: the filter and the tracking-loss grace.
45
+ * Defaults are IWSDK 1.0.0's (`EYE_GAZE_DEFAULTS`).
46
+ */
47
+ eyeGaze?: EyeGazeOptions;
48
+ /**
49
+ * Seconds between two `sample()` calls, for the eye-gaze filter and grace,
50
+ * when the host cannot say. Default 1/60. `NativeInteractions` passes the
51
+ * real frame delta through `setFrameDelta`.
52
+ */
53
+ frameSeconds?: number;
21
54
  }
22
55
  export declare class NativeInputProvider implements InputProvider {
23
56
  private readonly host;
57
+ private readonly nativeGrab;
58
+ private capabilities;
59
+ private facts;
60
+ private readonly capsListeners;
61
+ private readonly sourceListeners;
62
+ private readonly disposers;
63
+ private readonly presenceVisible;
64
+ private presenceModality;
65
+ private readonly presenceApplied;
66
+ private readonly eyeGaze;
67
+ private frameSeconds;
24
68
  readonly getHeadPose?: () => HeadPose;
25
69
  readonly sampleHints?: () => readonly InputHitHint[];
26
70
  readonly pulse?: (sourceId: string, intensity: number, durationMs: number) => boolean;
@@ -30,5 +74,28 @@ export declare class NativeInputProvider implements InputProvider {
30
74
  getCapabilities(): InputCapabilities;
31
75
  onCapabilitiesChanged(listener: (capabilities: InputCapabilities) => void): Unsubscribe;
32
76
  onSourcesChanged(listener: () => void): Unsubscribe;
77
+ /** The seconds the next `sample()` integrates over; `NativeInteractions.update(dt)` sets it each frame. */
78
+ setFrameDelta(seconds: number): void;
33
79
  sample(): readonly InputSourceSnapshot[];
80
+ /** Release the host subscriptions and every listener. */
81
+ dispose(): void;
82
+ /**
83
+ * The eye-gaze reading for this frame: the host's raw pose (copied), and
84
+ * each hand's ray-space pose. A host reports a hand's ray, not its
85
+ * ray-space orientation, so the pose is built from the ray with world up
86
+ * (`rayPoseFromRay`): a gaze-started drag follows the hand's position and
87
+ * direction, not its roll about the ray.
88
+ */
89
+ private eyeGazeFrame;
90
+ /** Re-read the facts and re-derive; publish and re-apply presence on a change. */
91
+ private refresh;
92
+ /** `IWSDKInputProvider.refreshCapabilities`, over the host's facts and sources. */
93
+ private derive;
94
+ /**
95
+ * `IWSDKInputProvider.applyPresence`: each side shows the family the
96
+ * modality picks, while that side is visible. Only a changed side is sent.
97
+ * Reports whether presence reached a live session, as IWSDK reports false
98
+ * with no session to draw in; the request is kept and applied on entry.
99
+ */
100
+ private applyPresence;
34
101
  }
package/dist/provider.js CHANGED
@@ -1,22 +1,53 @@
1
1
  /**
2
2
  * NativeInputProvider - the native host's input provider.
3
3
  *
4
- * A thin, copying pass-through over the `input` slice a native app (OpenXR,
5
- * visionOS) installs on `globalThis.__rcHost`, or hands in directly for
6
- * tests. Every snapshot, and every tuple inside it, is copied before it
7
- * leaves this class, so a host that reuses its own sample buffers still
8
- * meets the ownership rule the shared contract suite checks.
4
+ * It applies the rules `IWSDKInputProvider` applies, over the facts the
5
+ * native app's `input` slice reports, so the host is handed results:
9
6
  *
10
- * Optional members - `getHeadPose`, `sampleHints`, `pulse`,
11
- * `setPresenceVisible`, `setPresenceModality` - are only ever assigned when
12
- * the host itself carries them, so `typeof provider.pulse` answers honestly
13
- * for a host that cannot pulse rather than a method that is present but
14
- * always returns false.
7
+ * - Capabilities are DERIVED here, as `refreshCapabilities` derives them from
8
+ * the WebXR session: `rays`, `pokes` and grabs from immersion, `handJoints`
9
+ * and `pinch` from hand tracking or a tracked hand, `buttonsAxes` and
10
+ * `haptics` from the sources present, `gaze` and `headPose` from a head
11
+ * pose, `eyeGaze` from the eye-tracking fact and a host that can pose the
12
+ * gaze, `presence` from the host's presence member. They re-derive, and
13
+ * `onCapabilitiesChanged` fires, whenever the facts or the sources change.
14
+ * - Presence is decided here, as `applyPresence` decides it: the requested
15
+ * visibility per side, and the modality, with `"auto"` showing hands while
16
+ * hand joints are live and controllers otherwise. The host is told what to
17
+ * draw per side.
18
+ * - No sources are sampled while the session lacks focus, as IWSDK samples
19
+ * none unless its visibility state is `Visible`.
20
+ * - The eye-gaze rule (`@realitycollective/webxr-input` `eye-gaze.ts`) runs
21
+ * here over the host's raw snapshots and its gaze pose, as IWSDK's
22
+ * `GazePointer` runs it: far rays drop once gaze has a valid pose, a pinch
23
+ * selects, and the pinching hand's ray pose travels as `selectorPose`. A
24
+ * host reports only `eyeTracking` and `getEyeGazePose()`.
25
+ * - A pulse's intensity is clamped to 0..1 before it reaches the host.
26
+ *
27
+ * Every snapshot, and every tuple inside it, is copied before it leaves this
28
+ * class, so a host that reuses its own sample buffers still meets the
29
+ * ownership rule the shared contract suite checks.
15
30
  */
16
- import {} from "@realitycollective/webxr-input";
17
- import { copyPose, copySnapshot, resolveHostSlice } from "./native-types.js";
31
+ import { EyeGazeInput, NO_CAPABILITIES, rayPoseFromRay, } from "@realitycollective/webxr-input";
32
+ import { copyPose, copySnapshot, resolveHostSlice, } from "./native-types.js";
33
+ const SIDES = ["left", "right"];
34
+ const DEFAULT_FRAME_SECONDS = 1 / 60;
35
+ function clampUnit(value) {
36
+ return Math.min(1, Math.max(0, value));
37
+ }
18
38
  export class NativeInputProvider {
19
39
  host;
40
+ nativeGrab;
41
+ capabilities;
42
+ facts;
43
+ capsListeners = new Set();
44
+ sourceListeners = new Set();
45
+ disposers = [];
46
+ presenceVisible = { left: true, right: true };
47
+ presenceModality = "auto";
48
+ presenceApplied = { left: null, right: null };
49
+ eyeGaze;
50
+ frameSeconds;
20
51
  getHeadPose;
21
52
  sampleHints;
22
53
  pulse;
@@ -24,38 +55,154 @@ export class NativeInputProvider {
24
55
  setPresenceModality;
25
56
  constructor(options = {}) {
26
57
  this.host = resolveHostSlice("input", options.input);
27
- if (this.host.getHeadPose) {
28
- const host = this.host;
58
+ this.nativeGrab = options.nativeGrab ?? false;
59
+ this.eyeGaze = new EyeGazeInput(options.eyeGaze);
60
+ this.frameSeconds = options.frameSeconds ?? DEFAULT_FRAME_SECONDS;
61
+ this.facts = { ...this.host.getFacts() };
62
+ this.capabilities = this.derive();
63
+ const host = this.host;
64
+ if (host.getHeadPose) {
29
65
  this.getHeadPose = () => copyPose(host.getHeadPose());
30
66
  }
31
- if (this.host.sampleHints) {
32
- const host = this.host;
67
+ if (host.sampleHints) {
33
68
  this.sampleHints = () => host.sampleHints().map((hint) => ({ ...hint }));
34
69
  }
35
- if (this.host.pulse) {
36
- const host = this.host;
37
- this.pulse = (sourceId, intensity, durationMs) => host.pulse(sourceId, intensity, durationMs);
70
+ if (host.pulse) {
71
+ this.pulse = (sourceId, intensity, durationMs) => host.pulse(sourceId, clampUnit(intensity), durationMs);
38
72
  }
39
- if (this.host.setPresenceVisible) {
40
- const host = this.host;
41
- this.setPresenceVisible = (target, visible) => host.setPresenceVisible(target, visible);
42
- }
43
- if (this.host.setPresenceModality) {
44
- const host = this.host;
45
- this.setPresenceModality = (mode) => host.setPresenceModality(mode);
73
+ if (host.applyPresence) {
74
+ this.setPresenceVisible = (target, visible) => {
75
+ for (const side of SIDES) {
76
+ if (target === "all" || target === side)
77
+ this.presenceVisible[side] = visible;
78
+ }
79
+ return this.applyPresence();
80
+ };
81
+ this.setPresenceModality = (mode) => {
82
+ this.presenceModality = mode;
83
+ return this.applyPresence();
84
+ };
46
85
  }
86
+ this.disposers.push(host.onFactsChanged(() => this.refresh()), host.onSourcesChanged(() => {
87
+ this.refresh();
88
+ for (const listener of [...this.sourceListeners])
89
+ listener();
90
+ }));
91
+ this.applyPresence();
47
92
  }
48
93
  getCapabilities() {
49
- return this.host.getCapabilities();
94
+ return this.capabilities;
50
95
  }
51
96
  onCapabilitiesChanged(listener) {
52
- return this.host.onCapabilitiesChanged(listener);
97
+ this.capsListeners.add(listener);
98
+ return () => this.capsListeners.delete(listener);
53
99
  }
54
100
  onSourcesChanged(listener) {
55
- return this.host.onSourcesChanged(listener);
101
+ this.sourceListeners.add(listener);
102
+ return () => this.sourceListeners.delete(listener);
103
+ }
104
+ /** The seconds the next `sample()` integrates over; `NativeInteractions.update(dt)` sets it each frame. */
105
+ setFrameDelta(seconds) {
106
+ this.frameSeconds = seconds;
56
107
  }
57
108
  sample() {
58
- return this.host.sample().map(copySnapshot);
109
+ if (!this.facts.focused) {
110
+ this.eyeGaze.reset();
111
+ return [];
112
+ }
113
+ const sources = this.host.sample().map(copySnapshot);
114
+ if (!this.capabilities.eyeGaze)
115
+ return sources;
116
+ return this.eyeGaze.update(this.eyeGazeFrame(sources), sources, this.frameSeconds);
117
+ }
118
+ /** Release the host subscriptions and every listener. */
119
+ dispose() {
120
+ for (const dispose of this.disposers)
121
+ dispose();
122
+ this.disposers.length = 0;
123
+ this.capsListeners.clear();
124
+ this.sourceListeners.clear();
125
+ this.eyeGaze.reset();
126
+ }
127
+ /**
128
+ * The eye-gaze reading for this frame: the host's raw pose (copied), and
129
+ * each hand's ray-space pose. A host reports a hand's ray, not its
130
+ * ray-space orientation, so the pose is built from the ray with world up
131
+ * (`rayPoseFromRay`): a gaze-started drag follows the hand's position and
132
+ * direction, not its roll about the ray.
133
+ */
134
+ eyeGazeFrame(sources) {
135
+ const raw = this.host.getEyeGazePose?.() ?? null;
136
+ const rayPoses = {};
137
+ for (const source of sources) {
138
+ if ((source.handedness === "left" || source.handedness === "right") && source.ray && !rayPoses[source.handedness]) {
139
+ rayPoses[source.handedness] = rayPoseFromRay(source.ray);
140
+ }
141
+ }
142
+ return { present: this.facts.eyeTracking, pose: raw ? copyPose(raw) : null, rayPoses };
143
+ }
144
+ /** Re-read the facts and re-derive; publish and re-apply presence on a change. */
145
+ refresh() {
146
+ this.facts = { ...this.host.getFacts() };
147
+ const next = this.derive();
148
+ if (JSON.stringify(next) !== JSON.stringify(this.capabilities)) {
149
+ this.capabilities = next;
150
+ if (!next.eyeGaze)
151
+ this.eyeGaze.reset();
152
+ for (const listener of [...this.capsListeners])
153
+ listener(next);
154
+ }
155
+ this.applyPresence();
156
+ }
157
+ /** `IWSDKInputProvider.refreshCapabilities`, over the host's facts and sources. */
158
+ derive() {
159
+ const { immersive, handTracking, eyeTracking } = this.facts;
160
+ const sources = immersive ? this.host.sample() : [];
161
+ const handTracked = sources.some((source) => source.kind === "hand");
162
+ const hands = immersive && (handTracking || handTracked);
163
+ const head = typeof this.host.getHeadPose === "function";
164
+ return {
165
+ ...NO_CAPABILITIES,
166
+ rays: immersive,
167
+ // A controller's index tip is its ray origin, so pokes need no hand.
168
+ pokes: immersive,
169
+ grabs: immersive ? (this.nativeGrab ? "native" : "poseOnly") : "none",
170
+ handJoints: hands,
171
+ pinch: hands,
172
+ buttonsAxes: sources.some((source) => source.kind === "controller"),
173
+ gaze: head,
174
+ // IWSDK: the session reports a gaze input source. The host must also be
175
+ // able to pose it, or the fact is worth nothing.
176
+ eyeGaze: immersive && eyeTracking === true && typeof this.host.getEyeGazePose === "function",
177
+ headPose: head,
178
+ haptics: typeof this.host.pulse === "function" && sources.some((source) => source.hapticsAvailable === true),
179
+ presence: typeof this.host.applyPresence === "function",
180
+ };
181
+ }
182
+ /**
183
+ * `IWSDKInputProvider.applyPresence`: each side shows the family the
184
+ * modality picks, while that side is visible. Only a changed side is sent.
185
+ * Reports whether presence reached a live session, as IWSDK reports false
186
+ * with no session to draw in; the request is kept and applied on entry.
187
+ */
188
+ applyPresence() {
189
+ const apply = this.host.applyPresence;
190
+ if (!apply)
191
+ return false;
192
+ const handJoints = this.capabilities.handJoints;
193
+ const modality = this.presenceModality;
194
+ const hands = modality === "hands" || (modality === "auto" && handJoints);
195
+ const controllers = modality === "controllers" || (modality === "auto" && !handJoints);
196
+ for (const side of SIDES) {
197
+ const wanted = this.presenceVisible[side];
198
+ const shown = { hand: wanted && hands, controller: wanted && controllers };
199
+ const last = this.presenceApplied[side];
200
+ if (last && last.hand === shown.hand && last.controller === shown.controller)
201
+ continue;
202
+ this.presenceApplied[side] = shown;
203
+ apply.call(this.host, side, { ...shown });
204
+ }
205
+ return this.facts.immersive;
59
206
  }
60
207
  }
61
208
  //# sourceMappingURL=provider.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"provider.js","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,EASN,MAAM,gCAAgC,CAAC;AACxC,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,gBAAgB,EAAwB,MAAM,mBAAmB,CAAC;AAOnG,MAAM,OAAO,mBAAmB;IACb,IAAI,CAAkB;IAE9B,WAAW,CAAkB;IAC7B,WAAW,CAAiC;IAC5C,KAAK,CAAwE;IAC7E,kBAAkB,CAA6D;IAC/E,mBAAmB,CAAuC;IAEnE,YAAY,UAAsC,EAAE;QAClD,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC,OAAO,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;QAErD,IAAI,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC;YAC1B,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;YACvB,IAAI,CAAC,WAAW,GAAG,GAAG,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,WAAY,EAAE,CAAC,CAAC;QACzD,CAAC;QACD,IAAI,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC;YAC1B,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;YACvB,IAAI,CAAC,WAAW,GAAG,GAAG,EAAE,CAAC,IAAI,CAAC,WAAY,EAAE,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,CAAC,CAAC,CAAC;QAC5E,CAAC;QACD,IAAI,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;YACpB,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;YACvB,IAAI,CAAC,KAAK,GAAG,CAAC,QAAQ,EAAE,SAAS,EAAE,UAAU,EAAE,EAAE,CAAC,IAAI,CAAC,KAAM,CAAC,QAAQ,EAAE,SAAS,EAAE,UAAU,CAAC,CAAC;QACjG,CAAC;QACD,IAAI,IAAI,CAAC,IAAI,CAAC,kBAAkB,EAAE,CAAC;YACjC,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;YACvB,IAAI,CAAC,kBAAkB,GAAG,CAAC,MAAM,EAAE,OAAO,EAAE,EAAE,CAAC,IAAI,CAAC,kBAAmB,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAC3F,CAAC;QACD,IAAI,IAAI,CAAC,IAAI,CAAC,mBAAmB,EAAE,CAAC;YAClC,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;YACvB,IAAI,CAAC,mBAAmB,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,mBAAoB,CAAC,IAAI,CAAC,CAAC;QACvE,CAAC;IACH,CAAC;IAED,eAAe;QACb,OAAO,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,CAAC;IACrC,CAAC;IAED,qBAAqB,CAAC,QAAmD;QACvE,OAAO,IAAI,CAAC,IAAI,CAAC,qBAAqB,CAAC,QAAQ,CAAC,CAAC;IACnD,CAAC;IAED,gBAAgB,CAAC,QAAoB;QACnC,OAAO,IAAI,CAAC,IAAI,CAAC,gBAAgB,CAAC,QAAQ,CAAC,CAAC;IAC9C,CAAC;IAED,MAAM;QACJ,OAAO,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;IAC9C,CAAC;CACF","sourcesContent":["/**\n * NativeInputProvider - the native host's input provider.\n *\n * A thin, copying pass-through over the `input` slice a native app (OpenXR,\n * visionOS) installs on `globalThis.__rcHost`, or hands in directly for\n * tests. Every snapshot, and every tuple inside it, is copied before it\n * leaves this class, so a host that reuses its own sample buffers still\n * meets the ownership rule the shared contract suite checks.\n *\n * Optional members - `getHeadPose`, `sampleHints`, `pulse`,\n * `setPresenceVisible`, `setPresenceModality` - are only ever assigned when\n * the host itself carries them, so `typeof provider.pulse` answers honestly\n * for a host that cannot pulse rather than a method that is present but\n * always returns false.\n */\nimport {\n type Handedness,\n type HeadPose,\n type InputCapabilities,\n type InputHitHint,\n type InputProvider,\n type InputSourceSnapshot,\n type PresenceModality,\n type Unsubscribe,\n} from \"@realitycollective/webxr-input\";\nimport { copyPose, copySnapshot, resolveHostSlice, type NativeInputHost } from \"./native-types.js\";\n\nexport interface NativeInputProviderOptions {\n /** The `input` slice. Omit to read `globalThis.__rcHost.input`. */\n input?: NativeInputHost;\n}\n\nexport class NativeInputProvider implements InputProvider {\n private readonly host: NativeInputHost;\n\n readonly getHeadPose?: () => HeadPose;\n readonly sampleHints?: () => readonly InputHitHint[];\n readonly pulse?: (sourceId: string, intensity: number, durationMs: number) => boolean;\n readonly setPresenceVisible?: (target: Handedness | \"all\", visible: boolean) => boolean;\n readonly setPresenceModality?: (mode: PresenceModality) => boolean;\n\n constructor(options: NativeInputProviderOptions = {}) {\n this.host = resolveHostSlice(\"input\", options.input);\n\n if (this.host.getHeadPose) {\n const host = this.host;\n this.getHeadPose = () => copyPose(host.getHeadPose!());\n }\n if (this.host.sampleHints) {\n const host = this.host;\n this.sampleHints = () => host.sampleHints!().map((hint) => ({ ...hint }));\n }\n if (this.host.pulse) {\n const host = this.host;\n this.pulse = (sourceId, intensity, durationMs) => host.pulse!(sourceId, intensity, durationMs);\n }\n if (this.host.setPresenceVisible) {\n const host = this.host;\n this.setPresenceVisible = (target, visible) => host.setPresenceVisible!(target, visible);\n }\n if (this.host.setPresenceModality) {\n const host = this.host;\n this.setPresenceModality = (mode) => host.setPresenceModality!(mode);\n }\n }\n\n getCapabilities(): InputCapabilities {\n return this.host.getCapabilities();\n }\n\n onCapabilitiesChanged(listener: (capabilities: InputCapabilities) => void): Unsubscribe {\n return this.host.onCapabilitiesChanged(listener);\n }\n\n onSourcesChanged(listener: () => void): Unsubscribe {\n return this.host.onSourcesChanged(listener);\n }\n\n sample(): readonly InputSourceSnapshot[] {\n return this.host.sample().map(copySnapshot);\n }\n}\n"]}
1
+ {"version":3,"file":"provider.js","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * NativeInputProvider - the native host's input provider.\n *\n * It applies the rules `IWSDKInputProvider` applies, over the facts the\n * native app's `input` slice reports, so the host is handed results:\n *\n * - Capabilities are DERIVED here, as `refreshCapabilities` derives them from\n * the WebXR session: `rays`, `pokes` and grabs from immersion, `handJoints`\n * and `pinch` from hand tracking or a tracked hand, `buttonsAxes` and\n * `haptics` from the sources present, `gaze` and `headPose` from a head\n * pose, `eyeGaze` from the eye-tracking fact and a host that can pose the\n * gaze, `presence` from the host's presence member. They re-derive, and\n * `onCapabilitiesChanged` fires, whenever the facts or the sources change.\n * - Presence is decided here, as `applyPresence` decides it: the requested\n * visibility per side, and the modality, with `\"auto\"` showing hands while\n * hand joints are live and controllers otherwise. The host is told what to\n * draw per side.\n * - No sources are sampled while the session lacks focus, as IWSDK samples\n * none unless its visibility state is `Visible`.\n * - The eye-gaze rule (`@realitycollective/webxr-input` `eye-gaze.ts`) runs\n * here over the host's raw snapshots and its gaze pose, as IWSDK's\n * `GazePointer` runs it: far rays drop once gaze has a valid pose, a pinch\n * selects, and the pinching hand's ray pose travels as `selectorPose`. A\n * host reports only `eyeTracking` and `getEyeGazePose()`.\n * - A pulse's intensity is clamped to 0..1 before it reaches the host.\n *\n * Every snapshot, and every tuple inside it, is copied before it leaves this\n * class, so a host that reuses its own sample buffers still meets the\n * ownership rule the shared contract suite checks.\n */\nimport {\n EyeGazeInput,\n NO_CAPABILITIES,\n rayPoseFromRay,\n type EyeGazeFrame,\n type EyeGazeOptions,\n type Handedness,\n type HeadPose,\n type InputCapabilities,\n type InputHitHint,\n type InputProvider,\n type InputSourceSnapshot,\n type PoseTuple,\n type PresenceModality,\n type Unsubscribe,\n} from \"@realitycollective/webxr-input\";\nimport {\n copyPose,\n copySnapshot,\n resolveHostSlice,\n type NativeInputFacts,\n type NativeInputHost,\n type NativePresenceShown,\n} from \"./native-types.js\";\n\nexport interface NativeInputProviderOptions {\n /** The `input` slice. Omit to read `globalThis.__rcHost.input`. */\n input?: NativeInputHost;\n /**\n * The native app fulfils grabs with its own physics, so grabs report\n * `\"native\"` and the runtime never carries the object. Default `false`:\n * `\"poseOnly\"`, the runtime carries it. The same option as IWSDK's\n * `IWSDKProviderOptions.nativeGrab`.\n */\n nativeGrab?: boolean;\n /**\n * Tuning for the eye-gaze rule: the filter and the tracking-loss grace.\n * Defaults are IWSDK 1.0.0's (`EYE_GAZE_DEFAULTS`).\n */\n eyeGaze?: EyeGazeOptions;\n /**\n * Seconds between two `sample()` calls, for the eye-gaze filter and grace,\n * when the host cannot say. Default 1/60. `NativeInteractions` passes the\n * real frame delta through `setFrameDelta`.\n */\n frameSeconds?: number;\n}\n\ntype Side = \"left\" | \"right\";\nconst SIDES: readonly Side[] = [\"left\", \"right\"];\nconst DEFAULT_FRAME_SECONDS = 1 / 60;\n\nfunction clampUnit(value: number): number {\n return Math.min(1, Math.max(0, value));\n}\n\nexport class NativeInputProvider implements InputProvider {\n private readonly host: NativeInputHost;\n private readonly nativeGrab: boolean;\n private capabilities: InputCapabilities;\n private facts: NativeInputFacts;\n private readonly capsListeners = new Set<(c: InputCapabilities) => void>();\n private readonly sourceListeners = new Set<() => void>();\n private readonly disposers: Unsubscribe[] = [];\n private readonly presenceVisible: Record<Side, boolean> = { left: true, right: true };\n private presenceModality: PresenceModality = \"auto\";\n private readonly presenceApplied: Record<Side, NativePresenceShown | null> = { left: null, right: null };\n private readonly eyeGaze: EyeGazeInput;\n private frameSeconds: number;\n\n readonly getHeadPose?: () => HeadPose;\n readonly sampleHints?: () => readonly InputHitHint[];\n readonly pulse?: (sourceId: string, intensity: number, durationMs: number) => boolean;\n readonly setPresenceVisible?: (target: Handedness | \"all\", visible: boolean) => boolean;\n readonly setPresenceModality?: (mode: PresenceModality) => boolean;\n\n constructor(options: NativeInputProviderOptions = {}) {\n this.host = resolveHostSlice(\"input\", options.input);\n this.nativeGrab = options.nativeGrab ?? false;\n this.eyeGaze = new EyeGazeInput(options.eyeGaze);\n this.frameSeconds = options.frameSeconds ?? DEFAULT_FRAME_SECONDS;\n this.facts = { ...this.host.getFacts() };\n this.capabilities = this.derive();\n\n const host = this.host;\n if (host.getHeadPose) {\n this.getHeadPose = () => copyPose(host.getHeadPose!());\n }\n if (host.sampleHints) {\n this.sampleHints = () => host.sampleHints!().map((hint) => ({ ...hint }));\n }\n if (host.pulse) {\n this.pulse = (sourceId, intensity, durationMs) =>\n host.pulse!(sourceId, clampUnit(intensity), durationMs);\n }\n if (host.applyPresence) {\n this.setPresenceVisible = (target, visible) => {\n for (const side of SIDES) {\n if (target === \"all\" || target === side) this.presenceVisible[side] = visible;\n }\n return this.applyPresence();\n };\n this.setPresenceModality = (mode) => {\n this.presenceModality = mode;\n return this.applyPresence();\n };\n }\n\n this.disposers.push(\n host.onFactsChanged(() => this.refresh()),\n host.onSourcesChanged(() => {\n this.refresh();\n for (const listener of [...this.sourceListeners]) listener();\n }),\n );\n this.applyPresence();\n }\n\n getCapabilities(): InputCapabilities {\n return this.capabilities;\n }\n\n onCapabilitiesChanged(listener: (capabilities: InputCapabilities) => void): Unsubscribe {\n this.capsListeners.add(listener);\n return () => this.capsListeners.delete(listener);\n }\n\n onSourcesChanged(listener: () => void): Unsubscribe {\n this.sourceListeners.add(listener);\n return () => this.sourceListeners.delete(listener);\n }\n\n /** The seconds the next `sample()` integrates over; `NativeInteractions.update(dt)` sets it each frame. */\n setFrameDelta(seconds: number): void {\n this.frameSeconds = seconds;\n }\n\n sample(): readonly InputSourceSnapshot[] {\n if (!this.facts.focused) {\n this.eyeGaze.reset();\n return [];\n }\n const sources = this.host.sample().map(copySnapshot);\n if (!this.capabilities.eyeGaze) return sources;\n return this.eyeGaze.update(this.eyeGazeFrame(sources), sources, this.frameSeconds);\n }\n\n /** Release the host subscriptions and every listener. */\n dispose(): void {\n for (const dispose of this.disposers) dispose();\n this.disposers.length = 0;\n this.capsListeners.clear();\n this.sourceListeners.clear();\n this.eyeGaze.reset();\n }\n\n /**\n * The eye-gaze reading for this frame: the host's raw pose (copied), and\n * each hand's ray-space pose. A host reports a hand's ray, not its\n * ray-space orientation, so the pose is built from the ray with world up\n * (`rayPoseFromRay`): a gaze-started drag follows the hand's position and\n * direction, not its roll about the ray.\n */\n private eyeGazeFrame(sources: readonly InputSourceSnapshot[]): EyeGazeFrame {\n const raw = this.host.getEyeGazePose?.() ?? null;\n const rayPoses: Partial<Record<Side, PoseTuple>> = {};\n for (const source of sources) {\n if ((source.handedness === \"left\" || source.handedness === \"right\") && source.ray && !rayPoses[source.handedness]) {\n rayPoses[source.handedness] = rayPoseFromRay(source.ray);\n }\n }\n return { present: this.facts.eyeTracking, pose: raw ? copyPose(raw) : null, rayPoses };\n }\n\n /** Re-read the facts and re-derive; publish and re-apply presence on a change. */\n private refresh(): void {\n this.facts = { ...this.host.getFacts() };\n const next = this.derive();\n if (JSON.stringify(next) !== JSON.stringify(this.capabilities)) {\n this.capabilities = next;\n if (!next.eyeGaze) this.eyeGaze.reset();\n for (const listener of [...this.capsListeners]) listener(next);\n }\n this.applyPresence();\n }\n\n /** `IWSDKInputProvider.refreshCapabilities`, over the host's facts and sources. */\n private derive(): InputCapabilities {\n const { immersive, handTracking, eyeTracking } = this.facts;\n const sources = immersive ? this.host.sample() : [];\n const handTracked = sources.some((source) => source.kind === \"hand\");\n const hands = immersive && (handTracking || handTracked);\n const head = typeof this.host.getHeadPose === \"function\";\n return {\n ...NO_CAPABILITIES,\n rays: immersive,\n // A controller's index tip is its ray origin, so pokes need no hand.\n pokes: immersive,\n grabs: immersive ? (this.nativeGrab ? \"native\" : \"poseOnly\") : \"none\",\n handJoints: hands,\n pinch: hands,\n buttonsAxes: sources.some((source) => source.kind === \"controller\"),\n gaze: head,\n // IWSDK: the session reports a gaze input source. The host must also be\n // able to pose it, or the fact is worth nothing.\n eyeGaze: immersive && eyeTracking === true && typeof this.host.getEyeGazePose === \"function\",\n headPose: head,\n haptics: typeof this.host.pulse === \"function\" && sources.some((source) => source.hapticsAvailable === true),\n presence: typeof this.host.applyPresence === \"function\",\n };\n }\n\n /**\n * `IWSDKInputProvider.applyPresence`: each side shows the family the\n * modality picks, while that side is visible. Only a changed side is sent.\n * Reports whether presence reached a live session, as IWSDK reports false\n * with no session to draw in; the request is kept and applied on entry.\n */\n private applyPresence(): boolean {\n const apply = this.host.applyPresence;\n if (!apply) return false;\n const handJoints = this.capabilities.handJoints;\n const modality = this.presenceModality;\n const hands = modality === \"hands\" || (modality === \"auto\" && handJoints);\n const controllers = modality === \"controllers\" || (modality === \"auto\" && !handJoints);\n for (const side of SIDES) {\n const wanted = this.presenceVisible[side];\n const shown: NativePresenceShown = { hand: wanted && hands, controller: wanted && controllers };\n const last = this.presenceApplied[side];\n if (last && last.hand === shown.hand && last.controller === shown.controller) continue;\n this.presenceApplied[side] = shown;\n apply.call(this.host, side, { ...shown });\n }\n return this.facts.immersive;\n }\n}\n"]}
@@ -9,13 +9,31 @@
9
9
  * only ever assigned here when the host itself carries the matching member,
10
10
  * so an app whose native host cannot follow a world pose, or has no effect
11
11
  * hook, sees a port without one rather than a method that silently no-ops.
12
+ *
13
+ * `beginHold` and `endHold` are always present. A pose-only grab calls them
14
+ * on every grab and release, and a host that silently lacked them used to
15
+ * drop every throw on the floor (the release velocity never reached it). A
16
+ * host without them now fails here, at construction, naming both.
17
+ *
18
+ * With `options.physics` (the platform facility over the host's `physics`
19
+ * slice, and the target's body id in it), the hold goes through the core
20
+ * physics contract: `beginHold` suspends the body, `endHold` resumes it with
21
+ * the release velocity, and `setWorldPose` teleports it with velocity
22
+ * cleared while not held. The `interactions` slice's own `beginHold` and
23
+ * `endHold` are then not called for this target. Without it, a target with
24
+ * no body rests where it is released, as the `interactions` slice states.
12
25
  */
13
26
  import type { PoseTuple, QuatTuple, Vec3Tuple } from "@realitycollective/webxr-input";
14
- import type { HoldRelease, TransformPort } from "@realitycollective/webxr-interactions";
27
+ import type { HoldRelease, PhysicsFacility, TransformPort } from "@realitycollective/webxr-interactions";
15
28
  import { type NativeInteractionHost } from "./native-types.js";
16
29
  export interface NativeTransformPortOptions {
17
30
  /** The `interactions` slice. Omit to read `globalThis.__rcHost.interactions`. */
18
31
  interactions?: NativeInteractionHost;
32
+ /** The target's physics body: the facility over the `physics` slice, and the body's id. */
33
+ physics?: {
34
+ facility: PhysicsFacility;
35
+ bodyId: string;
36
+ };
19
37
  }
20
38
  export declare class NativeTransformPort implements TransformPort {
21
39
  private readonly host;
@@ -25,8 +43,8 @@ export declare class NativeTransformPort implements TransformPort {
25
43
  scale?: number;
26
44
  emissive?: number;
27
45
  }) => void;
28
- readonly beginHold?: () => void;
29
- readonly endHold?: (release: HoldRelease) => void;
46
+ readonly beginHold: () => void;
47
+ readonly endHold: (release: HoldRelease) => void;
30
48
  constructor(targetId: string, options?: NativeTransformPortOptions);
31
49
  getWorldPose(): PoseTuple;
32
50
  getRestWorldPose(): PoseTuple;
@@ -9,7 +9,17 @@ export class NativeTransformPort {
9
9
  constructor(targetId, options = {}) {
10
10
  this.targetId = targetId;
11
11
  this.host = resolveHostSlice("interactions", options.interactions);
12
- if (this.host.setWorldPose) {
12
+ const physics = options.physics;
13
+ if (physics) {
14
+ // The facility owns the body's pose and velocity; the host's own
15
+ // world-pose write, when it has one, keeps the drawn object in step.
16
+ const host = this.host;
17
+ this.setWorldPose = (pose) => {
18
+ physics.facility.setBodyPose(physics.bodyId, pose);
19
+ host.setWorldPose?.(targetId, copyPose(pose));
20
+ };
21
+ }
22
+ else if (this.host.setWorldPose) {
13
23
  const host = this.host;
14
24
  this.setWorldPose = (pose) => host.setWorldPose(targetId, copyPose(pose));
15
25
  }
@@ -17,17 +27,20 @@ export class NativeTransformPort {
17
27
  const host = this.host;
18
28
  this.setEffect = (effect) => host.setEffect(targetId, effect);
19
29
  }
20
- if (this.host.beginHold) {
21
- const host = this.host;
22
- this.beginHold = () => host.beginHold(targetId);
30
+ if (typeof this.host.beginHold !== "function" || typeof this.host.endHold !== "function") {
31
+ throw new Error("@realitycollective/native-interactions: the interactions slice must implement beginHold(targetId) and endHold(targetId, release). A pose-only grab calls both, and a host without them drops every release velocity. A host with no physics implements them and lets the object rest.");
23
32
  }
24
- if (this.host.endHold) {
25
- const host = this.host;
26
- this.endHold = (release) => host.endHold(targetId, {
27
- linearVelocity: copyVec3(release.linearVelocity),
28
- angularVelocity: copyVec3(release.angularVelocity),
29
- });
33
+ const host = this.host;
34
+ if (physics) {
35
+ this.beginHold = () => physics.facility.suspend(physics.bodyId);
36
+ this.endHold = (release) => physics.facility.resume(physics.bodyId, release);
37
+ return;
30
38
  }
39
+ this.beginHold = () => host.beginHold(targetId);
40
+ this.endHold = (release) => host.endHold(targetId, {
41
+ linearVelocity: copyVec3(release.linearVelocity),
42
+ angularVelocity: copyVec3(release.angularVelocity),
43
+ });
31
44
  }
32
45
  getWorldPose() {
33
46
  return copyPose(this.host.getWorldPose(this.targetId));
@@ -1 +1 @@
1
- {"version":3,"file":"transform-port.js","sourceRoot":"","sources":["../src/transform-port.ts"],"names":[],"mappings":"AAcA,OAAO,EACL,QAAQ,EACR,QAAQ,EACR,QAAQ,EACR,gBAAgB,GAEjB,MAAM,mBAAmB,CAAC;AAO3B,MAAM,OAAO,mBAAmB;IACb,IAAI,CAAwB;IAC5B,QAAQ,CAAS;IAEzB,YAAY,CAA6B;IACzC,SAAS,CAA2D;IACpE,SAAS,CAAc;IACvB,OAAO,CAAkC;IAElD,YAAY,QAAgB,EAAE,UAAsC,EAAE;QACpE,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC,cAAc,EAAE,OAAO,CAAC,YAAY,CAAC,CAAC;QAEnE,IAAI,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC;YAC3B,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;YACvB,IAAI,CAAC,YAAY,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,YAAa,CAAC,QAAQ,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC;QAC7E,CAAC;QACD,IAAI,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;YACxB,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;YACvB,IAAI,CAAC,SAAS,GAAG,CAAC,MAAM,EAAE,EAAE,CAAC,IAAI,CAAC,SAAU,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;QACjE,CAAC;QACD,IAAI,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;YACxB,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;YACvB,IAAI,CAAC,SAAS,GAAG,GAAG,EAAE,CAAC,IAAI,CAAC,SAAU,CAAC,QAAQ,CAAC,CAAC;QACnD,CAAC;QACD,IAAI,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;YACtB,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;YACvB,IAAI,CAAC,OAAO,GAAG,CAAC,OAAO,EAAE,EAAE,CACzB,IAAI,CAAC,OAAQ,CAAC,QAAQ,EAAE;gBACtB,cAAc,EAAE,QAAQ,CAAC,OAAO,CAAC,cAAc,CAAC;gBAChD,eAAe,EAAE,QAAQ,CAAC,OAAO,CAAC,eAAe,CAAC;aACnD,CAAC,CAAC;QACP,CAAC;IACH,CAAC;IAED,YAAY;QACV,OAAO,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC;IACzD,CAAC;IAED,gBAAgB;QACd,OAAO,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC;IAC7D,CAAC;IAED,cAAc;QACZ,OAAO,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC;IAC3D,CAAC;IAED,cAAc,CAAC,MAAiB;QAC9B,IAAI,CAAC,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;IAC5D,CAAC;IAED,gBAAgB,CAAC,UAAqB;QACpC,IAAI,CAAC,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,QAAQ,EAAE,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC;IAClE,CAAC;CACF","sourcesContent":["/**\n * NativeTransformPort - the read/write surface of ONE interactable, over the\n * native host's `interactions` slice. The host serves every registered\n * object through one set of methods, so this port is a thin binding of one\n * `targetId` onto them - the same idea as `BabylonTransformPort` binding to\n * one node, but the object itself lives on the native side.\n *\n * `setWorldPose` and `setEffect` are optional on `TransformPort`, and are\n * only ever assigned here when the host itself carries the matching member,\n * so an app whose native host cannot follow a world pose, or has no effect\n * hook, sees a port without one rather than a method that silently no-ops.\n */\nimport type { PoseTuple, QuatTuple, Vec3Tuple } from \"@realitycollective/webxr-input\";\nimport type { HoldRelease, TransformPort } from \"@realitycollective/webxr-interactions\";\nimport {\n copyPose,\n copyQuat,\n copyVec3,\n resolveHostSlice,\n type NativeInteractionHost,\n} from \"./native-types.js\";\n\nexport interface NativeTransformPortOptions {\n /** The `interactions` slice. Omit to read `globalThis.__rcHost.interactions`. */\n interactions?: NativeInteractionHost;\n}\n\nexport class NativeTransformPort implements TransformPort {\n private readonly host: NativeInteractionHost;\n private readonly targetId: string;\n\n readonly setWorldPose?: (pose: PoseTuple) => void;\n readonly setEffect?: (effect: { scale?: number; emissive?: number }) => void;\n readonly beginHold?: () => void;\n readonly endHold?: (release: HoldRelease) => void;\n\n constructor(targetId: string, options: NativeTransformPortOptions = {}) {\n this.targetId = targetId;\n this.host = resolveHostSlice(\"interactions\", options.interactions);\n\n if (this.host.setWorldPose) {\n const host = this.host;\n this.setWorldPose = (pose) => host.setWorldPose!(targetId, copyPose(pose));\n }\n if (this.host.setEffect) {\n const host = this.host;\n this.setEffect = (effect) => host.setEffect!(targetId, effect);\n }\n if (this.host.beginHold) {\n const host = this.host;\n this.beginHold = () => host.beginHold!(targetId);\n }\n if (this.host.endHold) {\n const host = this.host;\n this.endHold = (release) =>\n host.endHold!(targetId, {\n linearVelocity: copyVec3(release.linearVelocity),\n angularVelocity: copyVec3(release.angularVelocity),\n });\n }\n }\n\n getWorldPose(): PoseTuple {\n return copyPose(this.host.getWorldPose(this.targetId));\n }\n\n getRestWorldPose(): PoseTuple {\n return copyPose(this.host.getRestWorldPose(this.targetId));\n }\n\n getLocalOffset(): Vec3Tuple {\n return copyVec3(this.host.getLocalOffset(this.targetId));\n }\n\n setLocalOffset(offset: Vec3Tuple): void {\n this.host.setLocalOffset(this.targetId, copyVec3(offset));\n }\n\n setLocalRotation(quaternion: QuatTuple): void {\n this.host.setLocalRotation(this.targetId, copyQuat(quaternion));\n }\n}\n"]}
1
+ {"version":3,"file":"transform-port.js","sourceRoot":"","sources":["../src/transform-port.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * NativeTransformPort - the read/write surface of ONE interactable, over the\n * native host's `interactions` slice. The host serves every registered\n * object through one set of methods, so this port is a thin binding of one\n * `targetId` onto them - the same idea as `BabylonTransformPort` binding to\n * one node, but the object itself lives on the native side.\n *\n * `setWorldPose` and `setEffect` are optional on `TransformPort`, and are\n * only ever assigned here when the host itself carries the matching member,\n * so an app whose native host cannot follow a world pose, or has no effect\n * hook, sees a port without one rather than a method that silently no-ops.\n *\n * `beginHold` and `endHold` are always present. A pose-only grab calls them\n * on every grab and release, and a host that silently lacked them used to\n * drop every throw on the floor (the release velocity never reached it). A\n * host without them now fails here, at construction, naming both.\n *\n * With `options.physics` (the platform facility over the host's `physics`\n * slice, and the target's body id in it), the hold goes through the core\n * physics contract: `beginHold` suspends the body, `endHold` resumes it with\n * the release velocity, and `setWorldPose` teleports it with velocity\n * cleared while not held. The `interactions` slice's own `beginHold` and\n * `endHold` are then not called for this target. Without it, a target with\n * no body rests where it is released, as the `interactions` slice states.\n */\nimport type { PoseTuple, QuatTuple, Vec3Tuple } from \"@realitycollective/webxr-input\";\nimport type { HoldRelease, PhysicsFacility, TransformPort } from \"@realitycollective/webxr-interactions\";\nimport {\n copyPose,\n copyQuat,\n copyVec3,\n resolveHostSlice,\n type NativeInteractionHost,\n} from \"./native-types.js\";\n\nexport interface NativeTransformPortOptions {\n /** The `interactions` slice. Omit to read `globalThis.__rcHost.interactions`. */\n interactions?: NativeInteractionHost;\n /** The target's physics body: the facility over the `physics` slice, and the body's id. */\n physics?: { facility: PhysicsFacility; bodyId: string };\n}\n\nexport class NativeTransformPort implements TransformPort {\n private readonly host: NativeInteractionHost;\n private readonly targetId: string;\n\n readonly setWorldPose?: (pose: PoseTuple) => void;\n readonly setEffect?: (effect: { scale?: number; emissive?: number }) => void;\n readonly beginHold: () => void;\n readonly endHold: (release: HoldRelease) => void;\n\n constructor(targetId: string, options: NativeTransformPortOptions = {}) {\n this.targetId = targetId;\n this.host = resolveHostSlice(\"interactions\", options.interactions);\n\n const physics = options.physics;\n if (physics) {\n // The facility owns the body's pose and velocity; the host's own\n // world-pose write, when it has one, keeps the drawn object in step.\n const host = this.host;\n this.setWorldPose = (pose) => {\n physics.facility.setBodyPose(physics.bodyId, pose);\n host.setWorldPose?.(targetId, copyPose(pose));\n };\n } else if (this.host.setWorldPose) {\n const host = this.host;\n this.setWorldPose = (pose) => host.setWorldPose!(targetId, copyPose(pose));\n }\n if (this.host.setEffect) {\n const host = this.host;\n this.setEffect = (effect) => host.setEffect!(targetId, effect);\n }\n if (typeof this.host.beginHold !== \"function\" || typeof this.host.endHold !== \"function\") {\n throw new Error(\n \"@realitycollective/native-interactions: the interactions slice must implement beginHold(targetId) and endHold(targetId, release). A pose-only grab calls both, and a host without them drops every release velocity. A host with no physics implements them and lets the object rest.\",\n );\n }\n const host = this.host;\n if (physics) {\n this.beginHold = () => physics.facility.suspend(physics.bodyId);\n this.endHold = (release) => physics.facility.resume(physics.bodyId, release);\n return;\n }\n this.beginHold = () => host.beginHold(targetId);\n this.endHold = (release) =>\n host.endHold(targetId, {\n linearVelocity: copyVec3(release.linearVelocity),\n angularVelocity: copyVec3(release.angularVelocity),\n });\n }\n\n getWorldPose(): PoseTuple {\n return copyPose(this.host.getWorldPose(this.targetId));\n }\n\n getRestWorldPose(): PoseTuple {\n return copyPose(this.host.getRestWorldPose(this.targetId));\n }\n\n getLocalOffset(): Vec3Tuple {\n return copyVec3(this.host.getLocalOffset(this.targetId));\n }\n\n setLocalOffset(offset: Vec3Tuple): void {\n this.host.setLocalOffset(this.targetId, copyVec3(offset));\n }\n\n setLocalRotation(quaternion: QuatTuple): void {\n this.host.setLocalRotation(this.targetId, copyQuat(quaternion));\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@realitycollective/native-interactions",
3
- "version": "0.1.1-preview.1",
3
+ "version": "0.1.1-preview.3",
4
4
  "description": "Native host adapter for the Reality Collective Interaction Extensions - reads the `input` and `interactions` slices a native app (OpenXR, visionOS) installs on `globalThis.__rcHost`, or an injected fake, and maps them into the engine-free @realitycollective/webxr-interactions core. No engine dependency: assets and rendering stay in the native app. Re-exports the core.",
5
5
  "keywords": [
6
6
  "realitycollective",
@@ -34,8 +34,8 @@
34
34
  "typecheck": "tsc -p tsconfig.json --noEmit"
35
35
  },
36
36
  "dependencies": {
37
- "@realitycollective/webxr-input": "^0.1.4",
38
- "@realitycollective/webxr-interactions": "^0.1.1-preview.1"
37
+ "@realitycollective/webxr-input": "^0.1.7",
38
+ "@realitycollective/webxr-interactions": "^0.1.1-preview.3"
39
39
  },
40
40
  "repository": {
41
41
  "type": "git",