@realitycollective/native-interactions 0.1.1-preview.2 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -25,8 +25,30 @@
25
25
  * Units are metres, seconds and radians; poses are world space, quaternions
26
26
  * `[x, y, z, w]`, right handed, +Y up.
27
27
  */
28
- import type { HeadPose, InputHitHint, InputSourceSnapshot, PoseTuple, QuatTuple, RayTuple, Unsubscribe, Vec3Tuple } from "@realitycollective/webxr-input";
29
- import type { HoldRelease } from "@realitycollective/webxr-interactions";
28
+ import type { ActivePointerKind, HeadPose, InputHitHint, InputSourceSnapshot, PointerDisplayConfig, PointerDrawing, PointerTargetKind, PoseTuple, QuatTuple, RayTuple, Unsubscribe, Vec3Tuple } from "@realitycollective/webxr-input";
29
+ import type { HoldRelease, PhysicsBodySpec, PhysicsBodyState, PhysicsShapeSpec, PhysicsVelocity } from "@realitycollective/webxr-interactions";
30
+ /**
31
+ * What the host draws for one source this frame: the core's pointer drawing
32
+ * (`pointerDrawing` in `@realitycollective/webxr-input`: the arbiter's
33
+ * decision under the app's pointer display settings), plus the decision it
34
+ * came from, so a host can tell a panel cursor from an object cursor in its
35
+ * logs. Every field is resolved here. The host draws `ray` and `cursor`
36
+ * exactly as they are, at the sizes, colours and offsets given, and decides
37
+ * nothing: not the display mode, not whether a panel gets a cursor, not the
38
+ * stub's length. Before 29 September 2026 a host had to reach these through
39
+ * a shell hook of its own (`__rcShell.setPointerDisplay`), which is exactly
40
+ * the kind of host-side rule this contract forbids.
41
+ */
42
+ export interface NativePointerVisuals extends PointerDrawing {
43
+ /** The pointer owning the source, or null when none has a candidate. */
44
+ activePointer: ActivePointerKind | null;
45
+ /** What the cursor sits on: a registered interactable, a UI panel, or null. */
46
+ targetKind: PointerTargetKind | null;
47
+ /** The interactable id or the panel id the cursor sits on, or null. */
48
+ targetId: string | null;
49
+ /** The hit's distance (ray parameter, or surface distance), or null. */
50
+ hitDistance: number | null;
51
+ }
30
52
  /**
31
53
  * What the host knows about its session, from which this package derives
32
54
  * `InputCapabilities` exactly as `IWSDKInputProvider.refreshCapabilities`
@@ -47,6 +69,18 @@ export interface NativeInputFacts {
47
69
  * `"hand-tracking"`. A tracked hand source also counts, without this.
48
70
  */
49
71
  handTracking: boolean;
72
+ /**
73
+ * An eye-gaze source is present: OpenXR `XR_EXT_eye_gaze_interaction` is
74
+ * bound and its action is active (`isActive`), on a device with eye
75
+ * tracking and the eye-tracking permission granted; visionOS never
76
+ * reports one (gaze reaches an app only at the moment of a pinch). IWSDK:
77
+ * an `XRInputSource` with `targetRayMode === "gaze"`. While true the
78
+ * binding applies the eye-gaze rule (`@realitycollective/webxr-input`
79
+ * `eye-gaze.ts`): `capabilities.eyeGaze` is true, hand and controller far
80
+ * rays are dropped once a valid gaze pose has been seen, and a pinch
81
+ * selects what is gazed at. The host draws none of this; it reports.
82
+ */
83
+ eyeTracking: boolean;
50
84
  }
51
85
  /**
52
86
  * What one side's presence visuals should show, handed to the host. The host
@@ -74,8 +108,34 @@ export interface NativeInputHost {
74
108
  /**
75
109
  * This frame's tracked sources, in the `InputSourceSnapshot` shape. `kind`
76
110
  * is `"hand"` while hand joints are tracked, else `"controller"`.
77
- * `select` is the trigger value, or 1 while the runtime reports selecting
78
- * (a hand pinch), 0..1; `squeeze` is the grip value, 0 for hands.
111
+ *
112
+ * What a source reports for `select` and `squeeze` is fixed per kind, so
113
+ * the core's grab lifecycle (start at 0.7, end below 0.3, the same
114
+ * thresholds IWSDK's provider feeds) sees on this host what it sees on the
115
+ * web on the same headset:
116
+ *
117
+ * - A CONTROLLER: `select` is the trigger's analog value, OpenXR
118
+ * `/input/trigger/value` (WebXR `gamepad.buttons[0].value`), and
119
+ * `squeeze` the grip's, `/input/squeeze/value` (`buttons[1].value`).
120
+ * Both rest at 0.
121
+ * - A HAND: `select` is BINARY, 1 while the runtime reports the hand's
122
+ * pinch gesture and 0 otherwise, NEVER the analog pinch strength. This is
123
+ * what the web gives IWSDK: the browser fires `selectstart` and
124
+ * `selectend` from the runtime's own pinch recogniser and IWSDK reads
125
+ * `getSelecting() ? 1 : 0` (`@iwsdk/xr-input` `xr-input-manager.js`).
126
+ * The source on Quest is `XR_FB_hand_tracking_aim`'s
127
+ * `XR_HAND_TRACKING_AIM_INDEX_PINCHING_BIT_FB` (the same bit the Quest
128
+ * Browser turns into `selectstart`), on a runtime without it
129
+ * `XR_EXT_hand_interaction` `pinch_ext/ready_ext` and `pinch_ext/value`
130
+ * through the runtime's own threshold. `squeeze` is 0 ALWAYS: a hand has
131
+ * no squeeze on the web (IWSDK reads a gamepad squeeze button a hand
132
+ * does not have), and its grab is its pinch through `select`. OpenXR's
133
+ * `grasp_ext` is not a hand's squeeze; a relaxed hand keeps it above the
134
+ * release threshold, so a grab never ends, which is what held the Pale
135
+ * Signal handwheel for 7 s after the hand opened. A relaxed, open hand
136
+ * reads `select` 0 and `squeeze` 0. The binding forces a hand's `squeeze`
137
+ * to 0 whatever the host says; the kit checks a hand's `select` is 0 or 1.
138
+ *
79
139
  * `gripPose` is the WebXR GRIP frame, not a hand joint - see
80
140
  * `InputSourceSnapshot.gripPose`. `indexTip` is the index fingertip for a
81
141
  * hand and the ray origin for a controller. `hapticsAvailable` is true when
@@ -85,6 +145,16 @@ export interface NativeInputHost {
85
145
  sample(): readonly InputSourceSnapshot[];
86
146
  /** The viewer's head pose this frame. Present on any host that tracks a head; capabilities `gaze` and `headPose` follow it. */
87
147
  getHeadPose?(): HeadPose;
148
+ /**
149
+ * This frame's gaze target-ray pose, world space (`-Z` along the gaze),
150
+ * or null when the runtime has no valid pose this frame (a blink, an
151
+ * uncalibrated headset: OpenXR `XrEyeGazeSampleTimeEXT` not current, or
152
+ * the pose's `XR_SPACE_LOCATION_ORIENTATION_TRACKED_BIT` clear). Raw: the
153
+ * binding filters it, as IWSDK's `GazePointer` filters
154
+ * `xrOrigin.eyeSpace`. Read every frame while `eyeTracking` is true.
155
+ * Required when `eyeTracking` can be true; without it the fact is ignored.
156
+ */
157
+ getEyeGazePose?(): PoseTuple | null;
88
158
  /**
89
159
  * Pre-resolved targeting hints, for a host with its own targeting. Frame
90
160
  * fresh: the hints for the frame `sample()` just reported. A hint beats the
@@ -99,13 +169,43 @@ export interface NativeInputHost {
99
169
  pulse?(sourceId: string, intensity: number, durationMs: number): boolean;
100
170
  /**
101
171
  * Show or hide one side's hand mesh and controller model, as decided here
102
- * (`IWSDKInputProvider.applyPresence`). Presence is the MODELS only: the
103
- * host never draws a ray for a hand, and it draws a cursor disc at every
104
- * ray's hit on a panel or an interactable whatever presence says, as
105
- * IWSDK's `CursorVisual` is. Called only when a side's result changed.
106
- * Without this member `capabilities.presence` is false.
172
+ * (`IWSDKInputProvider.applyPresence`). Presence is the MODELS only: it
173
+ * never touches the ray or the cursor, which `applyPointerVisuals` owns.
174
+ * Called only when a side's result changed. Without this member
175
+ * `capabilities.presence` is false.
107
176
  */
108
177
  applyPresence?(side: "left" | "right", shown: NativePresenceShown): void;
178
+ /**
179
+ * Draw, or stop drawing, one source's ray and cursor, exactly as told.
180
+ * Decided here: the pointer arbiter (`pointer-arbiter.ts` in
181
+ * `@realitycollective/webxr-input`, IWSDK's `MultiPointer`) picks the
182
+ * pointer owning the source across interactables AND UI panels, so a
183
+ * cursor on a panel arrives here too (`targetKind: "panel"`) and a touch on
184
+ * a panel hides the ray over an object; then the app's pointer display
185
+ * settings (`pointer-display.ts`, IWSDK's `RayPointer` and `CursorVisual`
186
+ * defaults) resolve what is drawn: `ray` with its stub from `rayFrom` to
187
+ * `rayTo` metres along the source's ray (fully visible to `raySolidTo`,
188
+ * fading after), `rayRadius` and `rayColor`; `cursor` at `cursorPoint`
189
+ * (world metres, the ray's hit or the surface point under the fingertip or
190
+ * grip), a disc of `cursorRadius`, `cursorOpacity`, sitting `cursorOffset`
191
+ * off the surface along its normal. A host draws nothing for a source it
192
+ * was not told about, never draws "a cursor at every ray hit" on its own
193
+ * (what this contract said before 28 September 2026), and never applies a
194
+ * display mode of its own (what the Pale Signal host did through a shell
195
+ * hook until 29 September 2026). Called every frame for every sampled
196
+ * source, with fresh objects the host may keep. IWSDK: `RayPointer.update`
197
+ * with `forceHideRay`, `rayDisplayMode` and its shader, and
198
+ * `CursorVisual.setVisible` and `updateFromIntersection`.
199
+ */
200
+ applyPointerVisuals?(sourceId: string, visuals: NativePointerVisuals): void;
201
+ /**
202
+ * The app's pointer display settings, handed over at construction and on
203
+ * every change (`PointerDisplay.set`), so a host can size its meshes or
204
+ * log the configuration. Informational: every per-frame decision already
205
+ * arrives resolved in `applyPointerVisuals`, so a host needs nothing from
206
+ * here to draw correctly. Optional.
207
+ */
208
+ applyPointerDisplay?(config: PointerDisplayConfig): void;
109
209
  }
110
210
  /** What the host's ray or proximity query reports: the target it reached, if any. */
111
211
  export interface NativeHit {
@@ -113,13 +213,29 @@ export interface NativeHit {
113
213
  targetId: string;
114
214
  /** `hitRay`: the ray parameter t, metres. `hitProximity`: metres to the target's SURFACE, never negative. */
115
215
  distance: number;
116
- /** World-space hit point, or the target's centre. */
216
+ /**
217
+ * World-space point. `hitRay`: where the ray enters the target. `hitProximity`:
218
+ * the point on the target's SURFACE nearest the query point, never the
219
+ * centre, because the touch cursor is drawn there (IWSDK's sphere
220
+ * intersector reports the point on the mesh). A host that answered with
221
+ * the centre put the cursor inside the object.
222
+ */
117
223
  point: Vec3Tuple;
118
224
  }
119
225
  /**
120
226
  * The native host's `interactions` slice: `HitTester` with its semantics
121
227
  * stated, and `TransformPort` with every member keyed by the target id the
122
228
  * app chose when it registered the object with the native scene.
229
+ *
230
+ * SCOPE OF EVERY QUERY: `hitRay`, `hitProximity` and `hitCone` consider
231
+ * REGISTERED INTERACTABLES ONLY, the ids this binding handed to
232
+ * `setTargetRadius`, and among them only the ones shown. Never scenery, a
233
+ * floor, a wall, a panel or any other mesh, however near. IWSDK's
234
+ * `EntityHitTester` tests the entities `register` gave it and nothing else.
235
+ * A host that answered a proximity query with the floor's bounds (which
236
+ * contain the hand) passed every earlier case and left no fingertip able to
237
+ * reach a target on the device; the kit now surrounds the query with
238
+ * scenery and expects the target.
123
239
  */
124
240
  export interface NativeInteractionHost {
125
241
  /**
@@ -139,6 +255,22 @@ export interface NativeInteractionHost {
139
255
  * Never the distance to the centre. IWSDK: `EntityHitTester.hitProximity`.
140
256
  */
141
257
  hitProximity(point: Vec3Tuple, radius: number): NativeHit | null;
258
+ /**
259
+ * Eye-gaze targeting: the best shown target inside a cone of `halfAngle`
260
+ * radians about `ray`, no farther than `maxLength` metres, or null. A
261
+ * target the ray reaches (as `hitRay`) wins outright with the point where
262
+ * the ray enters it; otherwise the target whose silhouette is nearest the
263
+ * ray in angle, and within half a degree the nearer one, with `point` the
264
+ * point of the target nearest the ray and `distance` metres to it. For a
265
+ * sphere target this is `coneHitForSpheres` in
266
+ * `@realitycollective/webxr-interactions`; a host that tests meshes
267
+ * measures to the closest point on the mesh's bounds, as IWSDK's
268
+ * `GazeConecaster` does with an oriented bounding box. Optional: without
269
+ * it the binding targets eye gaze with `hitRay` alone, so a glance that
270
+ * misses a small target by a degree finds nothing. IWSDK:
271
+ * `GazeConecaster.findFrameBest`.
272
+ */
273
+ hitCone?(ray: RayTuple, halfAngle: number, maxLength: number): NativeHit | null;
142
274
  /**
143
275
  * The radius, in metres, the host hit-tests a registered target with.
144
276
  * Called once per registration with the app's `targetRadius`, or 0.1 when
@@ -168,22 +300,102 @@ export interface NativeInteractionHost {
168
300
  emissive?: number;
169
301
  }): void;
170
302
  /**
171
- * A pose-only grab started: suspend this target's physics body, if it has
172
- * one, so it follows `setWorldPose` exactly. REQUIRED when grabs are
173
- * pose-only (`nativeGrab` off). A target with no body needs nothing.
174
- * IWSDK: `beginHold` removes the `PhysicsBody` (`register.ts`,
175
- * `physicsBindingFor`).
303
+ * A pose-only grab started on a target that has NO body in the `physics`
304
+ * slice: the host lets the object rest where the hold leaves it. A target
305
+ * that has a body is held through the `physics` slice instead
306
+ * (`NativePhysicsHost.suspend`), and this member is not called for it.
307
+ * REQUIRED when grabs are pose-only (`nativeGrab` off). IWSDK: `beginHold`
308
+ * removes the `PhysicsBody`.
176
309
  */
177
310
  beginHold(targetId: string): void;
178
311
  /**
179
- * The grab ended: resume the body with `release` as its velocity (linear
180
- * m/s and angular rad/s, world space; zeros for a synthesized release,
181
- * which rests). A target with NO physics body rests where it was released,
182
- * as on IWSDK where there is no body to re-add. IWSDK: `endHold` re-adds
183
- * the `PhysicsBody` and, for a non-zero velocity, a `PhysicsManipulation`.
312
+ * The grab ended on a target with no body in the `physics` slice: the
313
+ * object rests where it was released, as on IWSDK where there is no body
314
+ * to re-add. `release` is the velocity it would have carried (linear m/s
315
+ * and angular rad/s, world space; zeros for a synthesized release). A
316
+ * target with a body is resumed through `NativePhysicsHost.resume` instead.
184
317
  */
185
318
  endHold(targetId: string, release: HoldRelease): void;
186
319
  }
320
+ /**
321
+ * The native host's `physics` slice: the platform's physics engine behind
322
+ * the core `PhysicsFacility` contract (`physics.ts` in
323
+ * `@realitycollective/webxr-interactions`), one body per string id, the same
324
+ * ids the app registers interactables with. The host runs the platform's
325
+ * default engine, Jolt Physics on Quest and Android and RealityKit physics
326
+ * on visionOS, and applies these defaults exactly (IWSDK 1.0.0's, from
327
+ * `@iwsdk/core` `dist/physics/`): gravity `[0, -9.81, 0]` m/s², 60 steps
328
+ * per second with render interpolation, a body dynamic with linear and
329
+ * angular damping 0 and gravity factor 1, a shape "auto" (from the object's
330
+ * geometry) with density 1 kg/m³, restitution 0 and friction 0.5. An app
331
+ * may install its own `PhysicsFacility` here to replace the engine.
332
+ *
333
+ * Units: metres, seconds, radians. Poses are world space, quaternions
334
+ * `[x, y, z, w]`, +Y up. The binding copies every tuple it hands over and
335
+ * every tuple it reads, so the host may reuse its buffers.
336
+ *
337
+ * The host conformance kit runs the whole shared suite
338
+ * (`physicsFacilityContractCases()`) against this slice on the device.
339
+ */
340
+ export interface NativePhysicsHost {
341
+ /** The engine behind the slice, for reports: `"jolt"`, `"realitykit"`, or an app's own name. Never empty. */
342
+ readonly engine: string;
343
+ /** World gravity, m/s². Starts at `[0, -9.81, 0]`. */
344
+ getGravity(): Vec3Tuple;
345
+ /** Set world gravity, m/s²; every dynamic body, sleeping ones included, sees it from the next step. */
346
+ setGravity(gravity: Vec3Tuple): void;
347
+ /**
348
+ * Add a body for `id` at `pose` with the specs given; a missing field takes
349
+ * the default above. `state`: `"dynamic"` responds to forces, collisions and
350
+ * gravity, `"static"` never moves, `"kinematic"` moves only by `setBodyPose`
351
+ * and pushes dynamic bodies. `shape.kind` `"auto"` is the host's collider
352
+ * for the object's geometry; `"box"` takes full extents in `dimensions`,
353
+ * `"sphere"` its radius in `dimensions[0]`, `"capsule"` radius and height.
354
+ * Adding an id that exists replaces it. IWSDK: `PhysicsBody` and `PhysicsShape`.
355
+ */
356
+ addBody(id: string, pose: PoseTuple, body?: PhysicsBodySpec, shape?: PhysicsShapeSpec): void;
357
+ /** Remove the body; a missing id is ignored. */
358
+ removeBody(id: string): void;
359
+ hasBody(id: string): boolean;
360
+ /** Change how the body moves; a suspended body takes the new state when it resumes. */
361
+ setBodyState(id: string, state: PhysicsBodyState): void;
362
+ getBodyState(id: string): PhysicsBodyState;
363
+ /** Where the body is now, world space. Throws an error whose message contains `no physics body "<id>"` for an unknown id. */
364
+ getBodyPose(id: string): PoseTuple;
365
+ /**
366
+ * Teleport: the body is at `pose` from the next step with its velocity
367
+ * cleared, so it rests there rather than carrying what it did. While
368
+ * suspended the write is exact and carries nothing. IWSDK:
369
+ * `PhysicsSystem.setBodyTransform`.
370
+ */
371
+ setBodyPose(id: string, pose: PoseTuple): void;
372
+ /** Linear m/s and angular rad/s (axis scaled), world space. */
373
+ getVelocity(id: string): PhysicsVelocity;
374
+ /** Set both velocities. IWSDK: `PhysicsManipulation`. */
375
+ setVelocity(id: string, velocity: PhysicsVelocity): void;
376
+ /**
377
+ * A hold began: from here until `resume` the body is not simulated (no
378
+ * gravity, no collision response) and `setBodyPose` writes are exact. A
379
+ * second `suspend` changes nothing. IWSDK: `beginHold` removes the body.
380
+ */
381
+ suspend(id: string): void;
382
+ /**
383
+ * The hold ended: simulate the body again in the state it had, with
384
+ * `release` as its velocity so a throw carries through (zeros rest it).
385
+ * Resuming a body that is not suspended changes nothing. IWSDK:
386
+ * `endHold` re-adds the body with a `PhysicsManipulation`.
387
+ */
388
+ resume(id: string, release: HoldRelease): void;
389
+ isSuspended(id: string): boolean;
390
+ /**
391
+ * Advance the world by `dtSeconds`. A host whose engine steps itself from
392
+ * its own loop may take this as a hint and return; the kit then reads the
393
+ * poses the engine wrote. The binding calls it once per `update(dt)`.
394
+ */
395
+ step(dtSeconds: number): void;
396
+ /** Release the world and every body. */
397
+ dispose(): void;
398
+ }
187
399
  /**
188
400
  * Test-only readbacks a host provides so the host conformance kit
189
401
  * (`nativeInteractionsHostConformanceCases`) can check what the host
@@ -192,7 +404,13 @@ export interface NativeInteractionHost {
192
404
  export interface NativeInteractionsTestHost {
193
405
  /** Put a shown, hit-testable target of `radius` metres at `position`, as the app's scene would. */
194
406
  placeTarget(targetId: string, position: Vec3Tuple, radius: number): void;
195
- /** Remove every target `placeTarget` put in. */
407
+ /**
408
+ * Put a shown mesh of `radius` metres at `position` that is NOT a
409
+ * registered interactable (a floor, a wall, a prop), through the host's
410
+ * ordinary scene, so the kit can prove the queries never answer with it.
411
+ */
412
+ placeScenery(id: string, position: Vec3Tuple, radius: number): void;
413
+ /** Remove every target and every piece of scenery placed by the kit. */
196
414
  clearTargets(): void;
197
415
  /** What the host draws for one side now. */
198
416
  presenceShown(side: "left" | "right"): NativePresenceShown | undefined;
@@ -200,6 +418,12 @@ export interface NativeInteractionsTestHost {
200
418
  lastRelease(targetId: string): HoldRelease | undefined;
201
419
  /** Every cursor disc the host draws now, as world positions. */
202
420
  cursors(): Vec3Tuple[];
421
+ /** What the host draws for one source now, as last told through `applyPointerVisuals`; undefined for a source never told. */
422
+ pointerVisuals?(sourceId: string): NativePointerVisuals | undefined;
423
+ /** The pointer display settings the host last received through `applyPointerDisplay`, or undefined. Optional. */
424
+ pointerDisplay?(): PointerDisplayConfig | undefined;
425
+ /** Hide or show a placed target with the host's ordinary visibility flag, for the hidden-target cone case. Optional. */
426
+ setTargetVisible?(targetId: string, visible: boolean): void;
203
427
  }
204
428
  /**
205
429
  * The native app's frame callback, the root member of `__rcHost` that
@@ -208,10 +432,15 @@ export interface NativeInteractionsTestHost {
208
432
  export interface NativeFrameSource {
209
433
  onFrame(callback: (timestampMs: number, deltaS: number) => void): () => void;
210
434
  }
211
- /** The two slices this package reads off `globalThis.__rcHost`. */
435
+ /**
436
+ * The slices this package reads off `globalThis.__rcHost`. `physics` is
437
+ * part of the contract on every native platform: a host without it fails
438
+ * the conformance kit, and a target cannot carry a body until it is there.
439
+ */
212
440
  export interface NativeHostSlices {
213
441
  input: NativeInputHost;
214
442
  interactions: NativeInteractionHost;
443
+ physics: NativePhysicsHost;
215
444
  }
216
445
  /**
217
446
  * `globalThis.__rcHost`, read defensively: the root `NativeHost` interface
@@ -227,6 +456,8 @@ export declare function installedHost(): (Partial<NativeHostSlices> & Partial<Na
227
456
  * time something calls a method that is not there.
228
457
  */
229
458
  export declare function resolveHostSlice<K extends keyof NativeHostSlices>(name: K, injected: NativeHostSlices[K] | undefined): NativeHostSlices[K];
459
+ /** The value passed in, or `globalThis.__rcHost`'s slice of that name, or undefined. */
460
+ export declare function findHostSlice<K extends keyof NativeHostSlices>(name: K, injected: NativeHostSlices[K] | undefined): NativeHostSlices[K] | undefined;
230
461
  /** Copy a position tuple. */
231
462
  export declare function copyVec3(v: Vec3Tuple): Vec3Tuple;
232
463
  /** Copy an orientation tuple. */
@@ -14,12 +14,16 @@ export function installedHost() {
14
14
  * time something calls a method that is not there.
15
15
  */
16
16
  export function resolveHostSlice(name, injected) {
17
- const slice = injected ?? installedHost()?.[name];
17
+ const slice = findHostSlice(name, injected);
18
18
  if (!slice) {
19
19
  throw new Error(`@realitycollective/native-interactions: no "${name}" slice was supplied and globalThis.__rcHost.${name} is not installed. Pass one directly, or have the native app install it before this package is constructed.`);
20
20
  }
21
21
  return slice;
22
22
  }
23
+ /** The value passed in, or `globalThis.__rcHost`'s slice of that name, or undefined. */
24
+ export function findHostSlice(name, injected) {
25
+ return injected ?? installedHost()?.[name];
26
+ }
23
27
  // ---------------------------------------------------------------------------
24
28
  // Copies. A host may reuse its own buffers across calls, so every tuple that
25
29
  // crosses back into this package is copied on the way in, never referenced.
@@ -68,6 +72,8 @@ export function copySnapshot(source) {
68
72
  copy.nativeGrabbing = source.nativeGrabbing;
69
73
  if (source.hapticsAvailable !== undefined)
70
74
  copy.hapticsAvailable = source.hapticsAvailable;
75
+ if (source.selectorPose)
76
+ copy.selectorPose = copyPose(source.selectorPose);
71
77
  return copy;
72
78
  }
73
79
  //# sourceMappingURL=native-types.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"native-types.js","sourceRoot":"","sources":["../src/native-types.ts"],"names":[],"mappings":"AAsOA;;;;;GAKG;AACH,MAAM,UAAU,aAAa;IAC3B,OAAQ,UAAoF,CAAC,QAAQ,CAAC;AACxG,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAC9B,IAAO,EACP,QAAyC;IAEzC,MAAM,KAAK,GAAG,QAAQ,IAAK,aAAa,EAA4C,EAAE,CAAC,IAAI,CAAC,CAAC;IAC7F,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CACb,+CAA+C,IAAI,gDAAgD,IAAI,6GAA6G,CACrN,CAAC;IACJ,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,8EAA8E;AAC9E,6EAA6E;AAC7E,4EAA4E;AAC5E,8EAA8E;AAE9E,6BAA6B;AAC7B,MAAM,UAAU,QAAQ,CAAC,CAAY;IACnC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAC5B,CAAC;AAED,iCAAiC;AACjC,MAAM,UAAU,QAAQ,CAAC,CAAY;IACnC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAClC,CAAC;AAED,4CAA4C;AAC5C,MAAM,UAAU,QAAQ,CAAC,IAAe;IACtC,OAAO,EAAE,QAAQ,EAAE,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,UAAU,EAAE,QAAQ,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;AACtF,CAAC;AAED,uCAAuC;AACvC,MAAM,UAAU,OAAO,CAAC,GAAa;IACnC,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,SAAS,EAAE,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC;AAC9E,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAAC,MAA2B;IACtD,MAAM,IAAI,GAAwB;QAChC,EAAE,EAAE,MAAM,CAAC,EAAE;QACb,IAAI,EAAE,MAAM,CAAC,IAAI;QACjB,UAAU,EAAE,MAAM,CAAC,UAAU;QAC7B,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,OAAO,EAAE,MAAM,CAAC,OAAO;KACxB,CAAC;IACF,IAAI,MAAM,CAAC,GAAG;QAAE,IAAI,CAAC,GAAG,GAAG,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAC/C,IAAI,MAAM,CAAC,QAAQ;QAAE,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IAC/D,IAAI,MAAM,CAAC,QAAQ;QAAE,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IAC/D,IAAI,MAAM,CAAC,cAAc;QAAE,IAAI,CAAC,cAAc,GAAG,QAAQ,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC;IACjF,IAAI,MAAM,CAAC,eAAe;QAAE,IAAI,CAAC,eAAe,GAAG,QAAQ,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC;IACpF,IAAI,MAAM,CAAC,cAAc,KAAK,SAAS;QAAE,IAAI,CAAC,cAAc,GAAG,MAAM,CAAC,cAAc,CAAC;IACrF,IAAI,MAAM,CAAC,gBAAgB,KAAK,SAAS;QAAE,IAAI,CAAC,gBAAgB,GAAG,MAAM,CAAC,gBAAgB,CAAC;IAC3F,OAAO,IAAI,CAAC;AACd,CAAC","sourcesContent":["/**\n * The `input` and `interactions` slices a native host (OpenXR on Quest,\n * CompositorServices on visionOS, or any other shell embedding a JavaScript\n * engine such as Hermes) installs on `globalThis.__rcHost`.\n *\n * A HOST IS HANDED RESULTS, NOT RULES. Every rule the IWSDK binding applies\n * in its provider runs in this package from the same inputs: capabilities\n * are derived here from the facts the host reports, the presence modality\n * is decided here and handed to the host per side, and a target's radius is\n * handed to the host at registration. The host measures, renders and\n * reports. Each member below states what the host does, in what units and\n * with what sign, and which IWSDK line it stands in for.\n *\n * `NativeInteractionHost` is `HitTester` plus `TransformPort` from\n * `@realitycollective/webxr-interactions`, with a `targetId` added to every\n * `TransformPort` member because that port is per object and the host is one\n * object serving every registered interactable. Declaring the shapes here,\n * rather than reusing the originals by reference, is deliberate: this file is\n * the one place that states what crosses the native boundary, in the same\n * structural-typing style as `babylon-types.ts` and the XR Blocks `XB*Like`\n * types.\n *\n * Values that cross are plain: numbers, strings, booleans and tuples. No\n * engine objects in either direction, and assets stay on the native side.\n * Units are metres, seconds and radians; poses are world space, quaternions\n * `[x, y, z, w]`, right handed, +Y up.\n */\nimport type {\n HeadPose,\n InputHitHint,\n InputSourceSnapshot,\n PoseTuple,\n QuatTuple,\n RayTuple,\n Unsubscribe,\n Vec3Tuple,\n} from \"@realitycollective/webxr-input\";\nimport type { HoldRelease } from \"@realitycollective/webxr-interactions\";\n\n/**\n * What the host knows about its session, from which this package derives\n * `InputCapabilities` exactly as `IWSDKInputProvider.refreshCapabilities`\n * does from the WebXR session.\n */\nexport interface NativeInputFacts {\n /** A session is presenting: OpenXR `SYNCHRONIZED`, `VISIBLE` or `FOCUSED`. IWSDK: `world.session` exists. */\n immersive: boolean;\n /**\n * The session has input focus: OpenXR `FOCUSED`. While false no sources are\n * sampled, as IWSDK's provider returns none unless the visibility state is\n * `Visible`.\n */\n focused: boolean;\n /**\n * Hand tracking is enabled on the session (`XR_EXT_hand_tracking` and the\n * system supports it). IWSDK: `session.enabledFeatures` includes\n * `\"hand-tracking\"`. A tracked hand source also counts, without this.\n */\n handTracking: boolean;\n}\n\n/**\n * What one side's presence visuals should show, handed to the host. The host\n * draws exactly this and decides nothing: which family is shown for\n * `\"auto\"`, and which side a request targets, are this package's.\n */\nexport interface NativePresenceShown {\n /** Draw this side's hand mesh. */\n hand: boolean;\n /** Draw this side's controller model. */\n controller: boolean;\n}\n\n/**\n * The native host's `input` slice. The host reports facts, sources and\n * signals; `NativeInputProvider` turns them into the `InputProvider`\n * contract.\n */\nexport interface NativeInputHost {\n /** This moment's session facts. Read at construction and on every signal. */\n getFacts(): NativeInputFacts;\n /** The facts changed: session start or end, focus gained or lost. */\n onFactsChanged(listener: () => void): Unsubscribe;\n /** A source connected or disconnected (WebXR `inputsourceschange`). Capabilities re-derive on it. */\n onSourcesChanged(listener: () => void): Unsubscribe;\n /**\n * This frame's tracked sources, in the `InputSourceSnapshot` shape. `kind`\n * is `\"hand\"` while hand joints are tracked, else `\"controller\"`.\n * `select` is the trigger value, or 1 while the runtime reports selecting\n * (a hand pinch), 0..1; `squeeze` is the grip value, 0 for hands.\n * `gripPose` is the WebXR GRIP frame, not a hand joint - see\n * `InputSourceSnapshot.gripPose`. `indexTip` is the index fingertip for a\n * hand and the ray origin for a controller. `hapticsAvailable` is true when\n * the source has an actuator. The host may reuse its buffers: this package\n * copies every snapshot.\n */\n sample(): readonly InputSourceSnapshot[];\n /** The viewer's head pose this frame. Present on any host that tracks a head; capabilities `gaze` and `headPose` follow it. */\n getHeadPose?(): HeadPose;\n /**\n * Pre-resolved targeting hints, for a host with its own targeting. Frame\n * fresh: the hints for the frame `sample()` just reported. A hint beats the\n * core's own hit tests, and is equivalent to `nativeGrabbing` for a grab.\n */\n sampleHints?(): readonly InputHitHint[];\n /**\n * Fire a haptic pulse on a source. `intensity` 0..1 (already clamped),\n * `durationMs` in milliseconds. Returns false when it could not be\n * delivered. IWSDK: `actuator.pulse(intensity, durationMs)`.\n */\n pulse?(sourceId: string, intensity: number, durationMs: number): boolean;\n /**\n * Show or hide one side's hand mesh and controller model, as decided here\n * (`IWSDKInputProvider.applyPresence`). Presence is the MODELS only: the\n * host never draws a ray for a hand, and it draws a cursor disc at every\n * ray's hit on a panel or an interactable whatever presence says, as\n * IWSDK's `CursorVisual` is. Called only when a side's result changed.\n * Without this member `capabilities.presence` is false.\n */\n applyPresence?(side: \"left\" | \"right\", shown: NativePresenceShown): void;\n}\n\n/** What the host's ray or proximity query reports: the target it reached, if any. */\nexport interface NativeHit {\n /** The target id the app registered with the native scene. */\n targetId: string;\n /** `hitRay`: the ray parameter t, metres. `hitProximity`: metres to the target's SURFACE, never negative. */\n distance: number;\n /** World-space hit point, or the target's centre. */\n point: Vec3Tuple;\n}\n\n/**\n * The native host's `interactions` slice: `HitTester` with its semantics\n * stated, and `TransformPort` with every member keyed by the target id the\n * app chose when it registered the object with the native scene.\n */\nexport interface NativeInteractionHost {\n /**\n * The nearest shown target along `ray` (origin in metres, direction\n * normalised). A target counts when its centre is within its radius of the\n * ray line and in front of the origin; `distance` is the ray parameter of\n * the closest point, and `t <= 0` never hits. A host that tests triangle\n * meshes instead may report the surface it hit; the contract cases accept\n * any answer within the target's radius plus 0.05 m of the sphere answer.\n * IWSDK: `EntityHitTester.hitRay`.\n */\n hitRay(ray: RayTuple): NativeHit | null;\n /**\n * The nearest shown target whose SURFACE is within `radius` metres of\n * `point`. `distance = max(0, |centre - point| - targetRadius)`: a point\n * 3 cm outside a 10 cm target reports 0.03, a point inside reports 0.\n * Never the distance to the centre. IWSDK: `EntityHitTester.hitProximity`.\n */\n hitProximity(point: Vec3Tuple, radius: number): NativeHit | null;\n /**\n * The radius, in metres, the host hit-tests a registered target with.\n * Called once per registration with the app's `targetRadius`, or 0.1 when\n * it gave none, as IWSDK registers a bare target as a 10 cm sphere\n * (`register.ts`, `options.targetRadius ?? 0.1`). A host never excludes a\n * target from hit testing because of its radius.\n */\n setTargetRadius(targetId: string, radius: number): void;\n /** Where the object is now. */\n getWorldPose(targetId: string): PoseTuple;\n /** The rest pose captured at registration, in world space. */\n getRestWorldPose(targetId: string): PoseTuple;\n /** The offset from rest last written, metres, in the rest frame. */\n getLocalOffset(targetId: string): Vec3Tuple;\n /** Offset the object from its rest pose, metres, in the rest frame. */\n setLocalOffset(targetId: string, offset: Vec3Tuple): void;\n /** Rotate the object from its rest orientation. */\n setLocalRotation(targetId: string, quaternion: QuatTuple): void;\n /** Place the object at a world pose (the pose-only grab carry). */\n setWorldPose?(targetId: string, pose: PoseTuple): void;\n /**\n * The pulse effect: `scale` multiplies the rest scale, `emissive` is added\n * to the base emissive intensity. Last write wins per field.\n */\n setEffect?(targetId: string, effect: { scale?: number; emissive?: number }): void;\n /**\n * A pose-only grab started: suspend this target's physics body, if it has\n * one, so it follows `setWorldPose` exactly. REQUIRED when grabs are\n * pose-only (`nativeGrab` off). A target with no body needs nothing.\n * IWSDK: `beginHold` removes the `PhysicsBody` (`register.ts`,\n * `physicsBindingFor`).\n */\n beginHold(targetId: string): void;\n /**\n * The grab ended: resume the body with `release` as its velocity (linear\n * m/s and angular rad/s, world space; zeros for a synthesized release,\n * which rests). A target with NO physics body rests where it was released,\n * as on IWSDK where there is no body to re-add. IWSDK: `endHold` re-adds\n * the `PhysicsBody` and, for a non-zero velocity, a `PhysicsManipulation`.\n */\n endHold(targetId: string, release: HoldRelease): void;\n}\n\n/**\n * Test-only readbacks a host provides so the host conformance kit\n * (`nativeInteractionsHostConformanceCases`) can check what the host\n * actually did. A shipping host may omit them.\n */\nexport interface NativeInteractionsTestHost {\n /** Put a shown, hit-testable target of `radius` metres at `position`, as the app's scene would. */\n placeTarget(targetId: string, position: Vec3Tuple, radius: number): void;\n /** Remove every target `placeTarget` put in. */\n clearTargets(): void;\n /** What the host draws for one side now. */\n presenceShown(side: \"left\" | \"right\"): NativePresenceShown | undefined;\n /** The last release the host received for a target through `endHold`. */\n lastRelease(targetId: string): HoldRelease | undefined;\n /** Every cursor disc the host draws now, as world positions. */\n cursors(): Vec3Tuple[];\n}\n\n/**\n * The native app's frame callback, the root member of `__rcHost` that\n * `NativeInteractions` attaches to when `attachToHost` is set.\n */\nexport interface NativeFrameSource {\n onFrame(callback: (timestampMs: number, deltaS: number) => void): () => void;\n}\n\n/** The two slices this package reads off `globalThis.__rcHost`. */\nexport interface NativeHostSlices {\n input: NativeInputHost;\n interactions: NativeInteractionHost;\n}\n\n/**\n * `globalThis.__rcHost`, read defensively: the root `NativeHost` interface\n * belongs to `service-framework-native`, which this package does not depend\n * on, so the global is read as an unknown bag of optional slices rather than\n * imported.\n */\nexport function installedHost(): (Partial<NativeHostSlices> & Partial<NativeFrameSource>) | undefined {\n return (globalThis as { __rcHost?: Partial<NativeHostSlices> & Partial<NativeFrameSource> }).__rcHost;\n}\n\n/**\n * Resolve one slice: the value passed in, or `globalThis.__rcHost`'s slice\n * of the same name. Throws one clear error naming the missing slice, so a\n * native-interactions class fails at construction rather than the first\n * time something calls a method that is not there.\n */\nexport function resolveHostSlice<K extends keyof NativeHostSlices>(\n name: K,\n injected: NativeHostSlices[K] | undefined,\n): NativeHostSlices[K] {\n const slice = injected ?? (installedHost() as Partial<NativeHostSlices> | undefined)?.[name];\n if (!slice) {\n throw new Error(\n `@realitycollective/native-interactions: no \"${name}\" slice was supplied and globalThis.__rcHost.${name} is not installed. Pass one directly, or have the native app install it before this package is constructed.`,\n );\n }\n return slice;\n}\n\n// ---------------------------------------------------------------------------\n// Copies. A host may reuse its own buffers across calls, so every tuple that\n// crosses back into this package is copied on the way in, never referenced.\n// ---------------------------------------------------------------------------\n\n/** Copy a position tuple. */\nexport function copyVec3(v: Vec3Tuple): Vec3Tuple {\n return [v[0], v[1], v[2]];\n}\n\n/** Copy an orientation tuple. */\nexport function copyQuat(q: QuatTuple): QuatTuple {\n return [q[0], q[1], q[2], q[3]];\n}\n\n/** Copy a pose (position + orientation). */\nexport function copyPose(pose: PoseTuple): PoseTuple {\n return { position: copyVec3(pose.position), quaternion: copyQuat(pose.quaternion) };\n}\n\n/** Copy a ray (origin + direction). */\nexport function copyRay(ray: RayTuple): RayTuple {\n return { origin: copyVec3(ray.origin), direction: copyVec3(ray.direction) };\n}\n\n/**\n * Copy one `InputSourceSnapshot` field by field, including every tuple it\n * carries, so a host that pools and refills its own snapshot objects still\n * meets the ownership rule: a snapshot handed to `sample()`'s caller is\n * never written to again.\n */\nexport function copySnapshot(source: InputSourceSnapshot): InputSourceSnapshot {\n const copy: InputSourceSnapshot = {\n id: source.id,\n kind: source.kind,\n handedness: source.handedness,\n select: source.select,\n squeeze: source.squeeze,\n };\n if (source.ray) copy.ray = copyRay(source.ray);\n if (source.gripPose) copy.gripPose = copyPose(source.gripPose);\n if (source.indexTip) copy.indexTip = copyVec3(source.indexTip);\n if (source.linearVelocity) copy.linearVelocity = copyVec3(source.linearVelocity);\n if (source.angularVelocity) copy.angularVelocity = copyVec3(source.angularVelocity);\n if (source.nativeGrabbing !== undefined) copy.nativeGrabbing = source.nativeGrabbing;\n if (source.hapticsAvailable !== undefined) copy.hapticsAvailable = source.hapticsAvailable;\n return copy;\n}\n"]}
1
+ {"version":3,"file":"native-types.js","sourceRoot":"","sources":["../src/native-types.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The `input` and `interactions` slices a native host (OpenXR on Quest,\n * CompositorServices on visionOS, or any other shell embedding a JavaScript\n * engine such as Hermes) installs on `globalThis.__rcHost`.\n *\n * A HOST IS HANDED RESULTS, NOT RULES. Every rule the IWSDK binding applies\n * in its provider runs in this package from the same inputs: capabilities\n * are derived here from the facts the host reports, the presence modality\n * is decided here and handed to the host per side, and a target's radius is\n * handed to the host at registration. The host measures, renders and\n * reports. Each member below states what the host does, in what units and\n * with what sign, and which IWSDK line it stands in for.\n *\n * `NativeInteractionHost` is `HitTester` plus `TransformPort` from\n * `@realitycollective/webxr-interactions`, with a `targetId` added to every\n * `TransformPort` member because that port is per object and the host is one\n * object serving every registered interactable. Declaring the shapes here,\n * rather than reusing the originals by reference, is deliberate: this file is\n * the one place that states what crosses the native boundary, in the same\n * structural-typing style as `babylon-types.ts` and the XR Blocks `XB*Like`\n * types.\n *\n * Values that cross are plain: numbers, strings, booleans and tuples. No\n * engine objects in either direction, and assets stay on the native side.\n * Units are metres, seconds and radians; poses are world space, quaternions\n * `[x, y, z, w]`, right handed, +Y up.\n */\nimport type {\n ActivePointerKind,\n HeadPose,\n InputHitHint,\n InputSourceSnapshot,\n PointerDisplayConfig,\n PointerDrawing,\n PointerTargetKind,\n PoseTuple,\n QuatTuple,\n RayTuple,\n Unsubscribe,\n Vec3Tuple,\n} from \"@realitycollective/webxr-input\";\nimport type {\n HoldRelease,\n PhysicsBodySpec,\n PhysicsBodyState,\n PhysicsShapeSpec,\n PhysicsVelocity,\n} from \"@realitycollective/webxr-interactions\";\n\n/**\n * What the host draws for one source this frame: the core's pointer drawing\n * (`pointerDrawing` in `@realitycollective/webxr-input`: the arbiter's\n * decision under the app's pointer display settings), plus the decision it\n * came from, so a host can tell a panel cursor from an object cursor in its\n * logs. Every field is resolved here. The host draws `ray` and `cursor`\n * exactly as they are, at the sizes, colours and offsets given, and decides\n * nothing: not the display mode, not whether a panel gets a cursor, not the\n * stub's length. Before 29 September 2026 a host had to reach these through\n * a shell hook of its own (`__rcShell.setPointerDisplay`), which is exactly\n * the kind of host-side rule this contract forbids.\n */\nexport interface NativePointerVisuals extends PointerDrawing {\n /** The pointer owning the source, or null when none has a candidate. */\n activePointer: ActivePointerKind | null;\n /** What the cursor sits on: a registered interactable, a UI panel, or null. */\n targetKind: PointerTargetKind | null;\n /** The interactable id or the panel id the cursor sits on, or null. */\n targetId: string | null;\n /** The hit's distance (ray parameter, or surface distance), or null. */\n hitDistance: number | null;\n}\n\n/**\n * What the host knows about its session, from which this package derives\n * `InputCapabilities` exactly as `IWSDKInputProvider.refreshCapabilities`\n * does from the WebXR session.\n */\nexport interface NativeInputFacts {\n /** A session is presenting: OpenXR `SYNCHRONIZED`, `VISIBLE` or `FOCUSED`. IWSDK: `world.session` exists. */\n immersive: boolean;\n /**\n * The session has input focus: OpenXR `FOCUSED`. While false no sources are\n * sampled, as IWSDK's provider returns none unless the visibility state is\n * `Visible`.\n */\n focused: boolean;\n /**\n * Hand tracking is enabled on the session (`XR_EXT_hand_tracking` and the\n * system supports it). IWSDK: `session.enabledFeatures` includes\n * `\"hand-tracking\"`. A tracked hand source also counts, without this.\n */\n handTracking: boolean;\n /**\n * An eye-gaze source is present: OpenXR `XR_EXT_eye_gaze_interaction` is\n * bound and its action is active (`isActive`), on a device with eye\n * tracking and the eye-tracking permission granted; visionOS never\n * reports one (gaze reaches an app only at the moment of a pinch). IWSDK:\n * an `XRInputSource` with `targetRayMode === \"gaze\"`. While true the\n * binding applies the eye-gaze rule (`@realitycollective/webxr-input`\n * `eye-gaze.ts`): `capabilities.eyeGaze` is true, hand and controller far\n * rays are dropped once a valid gaze pose has been seen, and a pinch\n * selects what is gazed at. The host draws none of this; it reports.\n */\n eyeTracking: boolean;\n}\n\n/**\n * What one side's presence visuals should show, handed to the host. The host\n * draws exactly this and decides nothing: which family is shown for\n * `\"auto\"`, and which side a request targets, are this package's.\n */\nexport interface NativePresenceShown {\n /** Draw this side's hand mesh. */\n hand: boolean;\n /** Draw this side's controller model. */\n controller: boolean;\n}\n\n/**\n * The native host's `input` slice. The host reports facts, sources and\n * signals; `NativeInputProvider` turns them into the `InputProvider`\n * contract.\n */\nexport interface NativeInputHost {\n /** This moment's session facts. Read at construction and on every signal. */\n getFacts(): NativeInputFacts;\n /** The facts changed: session start or end, focus gained or lost. */\n onFactsChanged(listener: () => void): Unsubscribe;\n /** A source connected or disconnected (WebXR `inputsourceschange`). Capabilities re-derive on it. */\n onSourcesChanged(listener: () => void): Unsubscribe;\n /**\n * This frame's tracked sources, in the `InputSourceSnapshot` shape. `kind`\n * is `\"hand\"` while hand joints are tracked, else `\"controller\"`.\n *\n * What a source reports for `select` and `squeeze` is fixed per kind, so\n * the core's grab lifecycle (start at 0.7, end below 0.3, the same\n * thresholds IWSDK's provider feeds) sees on this host what it sees on the\n * web on the same headset:\n *\n * - A CONTROLLER: `select` is the trigger's analog value, OpenXR\n * `/input/trigger/value` (WebXR `gamepad.buttons[0].value`), and\n * `squeeze` the grip's, `/input/squeeze/value` (`buttons[1].value`).\n * Both rest at 0.\n * - A HAND: `select` is BINARY, 1 while the runtime reports the hand's\n * pinch gesture and 0 otherwise, NEVER the analog pinch strength. This is\n * what the web gives IWSDK: the browser fires `selectstart` and\n * `selectend` from the runtime's own pinch recogniser and IWSDK reads\n * `getSelecting() ? 1 : 0` (`@iwsdk/xr-input` `xr-input-manager.js`).\n * The source on Quest is `XR_FB_hand_tracking_aim`'s\n * `XR_HAND_TRACKING_AIM_INDEX_PINCHING_BIT_FB` (the same bit the Quest\n * Browser turns into `selectstart`), on a runtime without it\n * `XR_EXT_hand_interaction` `pinch_ext/ready_ext` and `pinch_ext/value`\n * through the runtime's own threshold. `squeeze` is 0 ALWAYS: a hand has\n * no squeeze on the web (IWSDK reads a gamepad squeeze button a hand\n * does not have), and its grab is its pinch through `select`. OpenXR's\n * `grasp_ext` is not a hand's squeeze; a relaxed hand keeps it above the\n * release threshold, so a grab never ends, which is what held the Pale\n * Signal handwheel for 7 s after the hand opened. A relaxed, open hand\n * reads `select` 0 and `squeeze` 0. The binding forces a hand's `squeeze`\n * to 0 whatever the host says; the kit checks a hand's `select` is 0 or 1.\n *\n * `gripPose` is the WebXR GRIP frame, not a hand joint - see\n * `InputSourceSnapshot.gripPose`. `indexTip` is the index fingertip for a\n * hand and the ray origin for a controller. `hapticsAvailable` is true when\n * the source has an actuator. The host may reuse its buffers: this package\n * copies every snapshot.\n */\n sample(): readonly InputSourceSnapshot[];\n /** The viewer's head pose this frame. Present on any host that tracks a head; capabilities `gaze` and `headPose` follow it. */\n getHeadPose?(): HeadPose;\n /**\n * This frame's gaze target-ray pose, world space (`-Z` along the gaze),\n * or null when the runtime has no valid pose this frame (a blink, an\n * uncalibrated headset: OpenXR `XrEyeGazeSampleTimeEXT` not current, or\n * the pose's `XR_SPACE_LOCATION_ORIENTATION_TRACKED_BIT` clear). Raw: the\n * binding filters it, as IWSDK's `GazePointer` filters\n * `xrOrigin.eyeSpace`. Read every frame while `eyeTracking` is true.\n * Required when `eyeTracking` can be true; without it the fact is ignored.\n */\n getEyeGazePose?(): PoseTuple | null;\n /**\n * Pre-resolved targeting hints, for a host with its own targeting. Frame\n * fresh: the hints for the frame `sample()` just reported. A hint beats the\n * core's own hit tests, and is equivalent to `nativeGrabbing` for a grab.\n */\n sampleHints?(): readonly InputHitHint[];\n /**\n * Fire a haptic pulse on a source. `intensity` 0..1 (already clamped),\n * `durationMs` in milliseconds. Returns false when it could not be\n * delivered. IWSDK: `actuator.pulse(intensity, durationMs)`.\n */\n pulse?(sourceId: string, intensity: number, durationMs: number): boolean;\n /**\n * Show or hide one side's hand mesh and controller model, as decided here\n * (`IWSDKInputProvider.applyPresence`). Presence is the MODELS only: it\n * never touches the ray or the cursor, which `applyPointerVisuals` owns.\n * Called only when a side's result changed. Without this member\n * `capabilities.presence` is false.\n */\n applyPresence?(side: \"left\" | \"right\", shown: NativePresenceShown): void;\n /**\n * Draw, or stop drawing, one source's ray and cursor, exactly as told.\n * Decided here: the pointer arbiter (`pointer-arbiter.ts` in\n * `@realitycollective/webxr-input`, IWSDK's `MultiPointer`) picks the\n * pointer owning the source across interactables AND UI panels, so a\n * cursor on a panel arrives here too (`targetKind: \"panel\"`) and a touch on\n * a panel hides the ray over an object; then the app's pointer display\n * settings (`pointer-display.ts`, IWSDK's `RayPointer` and `CursorVisual`\n * defaults) resolve what is drawn: `ray` with its stub from `rayFrom` to\n * `rayTo` metres along the source's ray (fully visible to `raySolidTo`,\n * fading after), `rayRadius` and `rayColor`; `cursor` at `cursorPoint`\n * (world metres, the ray's hit or the surface point under the fingertip or\n * grip), a disc of `cursorRadius`, `cursorOpacity`, sitting `cursorOffset`\n * off the surface along its normal. A host draws nothing for a source it\n * was not told about, never draws \"a cursor at every ray hit\" on its own\n * (what this contract said before 28 September 2026), and never applies a\n * display mode of its own (what the Pale Signal host did through a shell\n * hook until 29 September 2026). Called every frame for every sampled\n * source, with fresh objects the host may keep. IWSDK: `RayPointer.update`\n * with `forceHideRay`, `rayDisplayMode` and its shader, and\n * `CursorVisual.setVisible` and `updateFromIntersection`.\n */\n applyPointerVisuals?(sourceId: string, visuals: NativePointerVisuals): void;\n /**\n * The app's pointer display settings, handed over at construction and on\n * every change (`PointerDisplay.set`), so a host can size its meshes or\n * log the configuration. Informational: every per-frame decision already\n * arrives resolved in `applyPointerVisuals`, so a host needs nothing from\n * here to draw correctly. Optional.\n */\n applyPointerDisplay?(config: PointerDisplayConfig): void;\n}\n\n/** What the host's ray or proximity query reports: the target it reached, if any. */\nexport interface NativeHit {\n /** The target id the app registered with the native scene. */\n targetId: string;\n /** `hitRay`: the ray parameter t, metres. `hitProximity`: metres to the target's SURFACE, never negative. */\n distance: number;\n /**\n * World-space point. `hitRay`: where the ray enters the target. `hitProximity`:\n * the point on the target's SURFACE nearest the query point, never the\n * centre, because the touch cursor is drawn there (IWSDK's sphere\n * intersector reports the point on the mesh). A host that answered with\n * the centre put the cursor inside the object.\n */\n point: Vec3Tuple;\n}\n\n/**\n * The native host's `interactions` slice: `HitTester` with its semantics\n * stated, and `TransformPort` with every member keyed by the target id the\n * app chose when it registered the object with the native scene.\n *\n * SCOPE OF EVERY QUERY: `hitRay`, `hitProximity` and `hitCone` consider\n * REGISTERED INTERACTABLES ONLY, the ids this binding handed to\n * `setTargetRadius`, and among them only the ones shown. Never scenery, a\n * floor, a wall, a panel or any other mesh, however near. IWSDK's\n * `EntityHitTester` tests the entities `register` gave it and nothing else.\n * A host that answered a proximity query with the floor's bounds (which\n * contain the hand) passed every earlier case and left no fingertip able to\n * reach a target on the device; the kit now surrounds the query with\n * scenery and expects the target.\n */\nexport interface NativeInteractionHost {\n /**\n * The nearest shown target along `ray` (origin in metres, direction\n * normalised). A target counts when its centre is within its radius of the\n * ray line and in front of the origin; `distance` is the ray parameter of\n * the closest point, and `t <= 0` never hits. A host that tests triangle\n * meshes instead may report the surface it hit; the contract cases accept\n * any answer within the target's radius plus 0.05 m of the sphere answer.\n * IWSDK: `EntityHitTester.hitRay`.\n */\n hitRay(ray: RayTuple): NativeHit | null;\n /**\n * The nearest shown target whose SURFACE is within `radius` metres of\n * `point`. `distance = max(0, |centre - point| - targetRadius)`: a point\n * 3 cm outside a 10 cm target reports 0.03, a point inside reports 0.\n * Never the distance to the centre. IWSDK: `EntityHitTester.hitProximity`.\n */\n hitProximity(point: Vec3Tuple, radius: number): NativeHit | null;\n /**\n * Eye-gaze targeting: the best shown target inside a cone of `halfAngle`\n * radians about `ray`, no farther than `maxLength` metres, or null. A\n * target the ray reaches (as `hitRay`) wins outright with the point where\n * the ray enters it; otherwise the target whose silhouette is nearest the\n * ray in angle, and within half a degree the nearer one, with `point` the\n * point of the target nearest the ray and `distance` metres to it. For a\n * sphere target this is `coneHitForSpheres` in\n * `@realitycollective/webxr-interactions`; a host that tests meshes\n * measures to the closest point on the mesh's bounds, as IWSDK's\n * `GazeConecaster` does with an oriented bounding box. Optional: without\n * it the binding targets eye gaze with `hitRay` alone, so a glance that\n * misses a small target by a degree finds nothing. IWSDK:\n * `GazeConecaster.findFrameBest`.\n */\n hitCone?(ray: RayTuple, halfAngle: number, maxLength: number): NativeHit | null;\n /**\n * The radius, in metres, the host hit-tests a registered target with.\n * Called once per registration with the app's `targetRadius`, or 0.1 when\n * it gave none, as IWSDK registers a bare target as a 10 cm sphere\n * (`register.ts`, `options.targetRadius ?? 0.1`). A host never excludes a\n * target from hit testing because of its radius.\n */\n setTargetRadius(targetId: string, radius: number): void;\n /** Where the object is now. */\n getWorldPose(targetId: string): PoseTuple;\n /** The rest pose captured at registration, in world space. */\n getRestWorldPose(targetId: string): PoseTuple;\n /** The offset from rest last written, metres, in the rest frame. */\n getLocalOffset(targetId: string): Vec3Tuple;\n /** Offset the object from its rest pose, metres, in the rest frame. */\n setLocalOffset(targetId: string, offset: Vec3Tuple): void;\n /** Rotate the object from its rest orientation. */\n setLocalRotation(targetId: string, quaternion: QuatTuple): void;\n /** Place the object at a world pose (the pose-only grab carry). */\n setWorldPose?(targetId: string, pose: PoseTuple): void;\n /**\n * The pulse effect: `scale` multiplies the rest scale, `emissive` is added\n * to the base emissive intensity. Last write wins per field.\n */\n setEffect?(targetId: string, effect: { scale?: number; emissive?: number }): void;\n /**\n * A pose-only grab started on a target that has NO body in the `physics`\n * slice: the host lets the object rest where the hold leaves it. A target\n * that has a body is held through the `physics` slice instead\n * (`NativePhysicsHost.suspend`), and this member is not called for it.\n * REQUIRED when grabs are pose-only (`nativeGrab` off). IWSDK: `beginHold`\n * removes the `PhysicsBody`.\n */\n beginHold(targetId: string): void;\n /**\n * The grab ended on a target with no body in the `physics` slice: the\n * object rests where it was released, as on IWSDK where there is no body\n * to re-add. `release` is the velocity it would have carried (linear m/s\n * and angular rad/s, world space; zeros for a synthesized release). A\n * target with a body is resumed through `NativePhysicsHost.resume` instead.\n */\n endHold(targetId: string, release: HoldRelease): void;\n}\n\n/**\n * The native host's `physics` slice: the platform's physics engine behind\n * the core `PhysicsFacility` contract (`physics.ts` in\n * `@realitycollective/webxr-interactions`), one body per string id, the same\n * ids the app registers interactables with. The host runs the platform's\n * default engine, Jolt Physics on Quest and Android and RealityKit physics\n * on visionOS, and applies these defaults exactly (IWSDK 1.0.0's, from\n * `@iwsdk/core` `dist/physics/`): gravity `[0, -9.81, 0]` m/s², 60 steps\n * per second with render interpolation, a body dynamic with linear and\n * angular damping 0 and gravity factor 1, a shape \"auto\" (from the object's\n * geometry) with density 1 kg/m³, restitution 0 and friction 0.5. An app\n * may install its own `PhysicsFacility` here to replace the engine.\n *\n * Units: metres, seconds, radians. Poses are world space, quaternions\n * `[x, y, z, w]`, +Y up. The binding copies every tuple it hands over and\n * every tuple it reads, so the host may reuse its buffers.\n *\n * The host conformance kit runs the whole shared suite\n * (`physicsFacilityContractCases()`) against this slice on the device.\n */\nexport interface NativePhysicsHost {\n /** The engine behind the slice, for reports: `\"jolt\"`, `\"realitykit\"`, or an app's own name. Never empty. */\n readonly engine: string;\n /** World gravity, m/s². Starts at `[0, -9.81, 0]`. */\n getGravity(): Vec3Tuple;\n /** Set world gravity, m/s²; every dynamic body, sleeping ones included, sees it from the next step. */\n setGravity(gravity: Vec3Tuple): void;\n /**\n * Add a body for `id` at `pose` with the specs given; a missing field takes\n * the default above. `state`: `\"dynamic\"` responds to forces, collisions and\n * gravity, `\"static\"` never moves, `\"kinematic\"` moves only by `setBodyPose`\n * and pushes dynamic bodies. `shape.kind` `\"auto\"` is the host's collider\n * for the object's geometry; `\"box\"` takes full extents in `dimensions`,\n * `\"sphere\"` its radius in `dimensions[0]`, `\"capsule\"` radius and height.\n * Adding an id that exists replaces it. IWSDK: `PhysicsBody` and `PhysicsShape`.\n */\n addBody(id: string, pose: PoseTuple, body?: PhysicsBodySpec, shape?: PhysicsShapeSpec): void;\n /** Remove the body; a missing id is ignored. */\n removeBody(id: string): void;\n hasBody(id: string): boolean;\n /** Change how the body moves; a suspended body takes the new state when it resumes. */\n setBodyState(id: string, state: PhysicsBodyState): void;\n getBodyState(id: string): PhysicsBodyState;\n /** Where the body is now, world space. Throws an error whose message contains `no physics body \"<id>\"` for an unknown id. */\n getBodyPose(id: string): PoseTuple;\n /**\n * Teleport: the body is at `pose` from the next step with its velocity\n * cleared, so it rests there rather than carrying what it did. While\n * suspended the write is exact and carries nothing. IWSDK:\n * `PhysicsSystem.setBodyTransform`.\n */\n setBodyPose(id: string, pose: PoseTuple): void;\n /** Linear m/s and angular rad/s (axis scaled), world space. */\n getVelocity(id: string): PhysicsVelocity;\n /** Set both velocities. IWSDK: `PhysicsManipulation`. */\n setVelocity(id: string, velocity: PhysicsVelocity): void;\n /**\n * A hold began: from here until `resume` the body is not simulated (no\n * gravity, no collision response) and `setBodyPose` writes are exact. A\n * second `suspend` changes nothing. IWSDK: `beginHold` removes the body.\n */\n suspend(id: string): void;\n /**\n * The hold ended: simulate the body again in the state it had, with\n * `release` as its velocity so a throw carries through (zeros rest it).\n * Resuming a body that is not suspended changes nothing. IWSDK:\n * `endHold` re-adds the body with a `PhysicsManipulation`.\n */\n resume(id: string, release: HoldRelease): void;\n isSuspended(id: string): boolean;\n /**\n * Advance the world by `dtSeconds`. A host whose engine steps itself from\n * its own loop may take this as a hint and return; the kit then reads the\n * poses the engine wrote. The binding calls it once per `update(dt)`.\n */\n step(dtSeconds: number): void;\n /** Release the world and every body. */\n dispose(): void;\n}\n\n/**\n * Test-only readbacks a host provides so the host conformance kit\n * (`nativeInteractionsHostConformanceCases`) can check what the host\n * actually did. A shipping host may omit them.\n */\nexport interface NativeInteractionsTestHost {\n /** Put a shown, hit-testable target of `radius` metres at `position`, as the app's scene would. */\n placeTarget(targetId: string, position: Vec3Tuple, radius: number): void;\n /**\n * Put a shown mesh of `radius` metres at `position` that is NOT a\n * registered interactable (a floor, a wall, a prop), through the host's\n * ordinary scene, so the kit can prove the queries never answer with it.\n */\n placeScenery(id: string, position: Vec3Tuple, radius: number): void;\n /** Remove every target and every piece of scenery placed by the kit. */\n clearTargets(): void;\n /** What the host draws for one side now. */\n presenceShown(side: \"left\" | \"right\"): NativePresenceShown | undefined;\n /** The last release the host received for a target through `endHold`. */\n lastRelease(targetId: string): HoldRelease | undefined;\n /** Every cursor disc the host draws now, as world positions. */\n cursors(): Vec3Tuple[];\n /** What the host draws for one source now, as last told through `applyPointerVisuals`; undefined for a source never told. */\n pointerVisuals?(sourceId: string): NativePointerVisuals | undefined;\n /** The pointer display settings the host last received through `applyPointerDisplay`, or undefined. Optional. */\n pointerDisplay?(): PointerDisplayConfig | undefined;\n /** Hide or show a placed target with the host's ordinary visibility flag, for the hidden-target cone case. Optional. */\n setTargetVisible?(targetId: string, visible: boolean): void;\n}\n\n/**\n * The native app's frame callback, the root member of `__rcHost` that\n * `NativeInteractions` attaches to when `attachToHost` is set.\n */\nexport interface NativeFrameSource {\n onFrame(callback: (timestampMs: number, deltaS: number) => void): () => void;\n}\n\n/**\n * The slices this package reads off `globalThis.__rcHost`. `physics` is\n * part of the contract on every native platform: a host without it fails\n * the conformance kit, and a target cannot carry a body until it is there.\n */\nexport interface NativeHostSlices {\n input: NativeInputHost;\n interactions: NativeInteractionHost;\n physics: NativePhysicsHost;\n}\n\n/**\n * `globalThis.__rcHost`, read defensively: the root `NativeHost` interface\n * belongs to `service-framework-native`, which this package does not depend\n * on, so the global is read as an unknown bag of optional slices rather than\n * imported.\n */\nexport function installedHost(): (Partial<NativeHostSlices> & Partial<NativeFrameSource>) | undefined {\n return (globalThis as { __rcHost?: Partial<NativeHostSlices> & Partial<NativeFrameSource> }).__rcHost;\n}\n\n/**\n * Resolve one slice: the value passed in, or `globalThis.__rcHost`'s slice\n * of the same name. Throws one clear error naming the missing slice, so a\n * native-interactions class fails at construction rather than the first\n * time something calls a method that is not there.\n */\nexport function resolveHostSlice<K extends keyof NativeHostSlices>(\n name: K,\n injected: NativeHostSlices[K] | undefined,\n): NativeHostSlices[K] {\n const slice = findHostSlice(name, injected);\n if (!slice) {\n throw new Error(\n `@realitycollective/native-interactions: no \"${name}\" slice was supplied and globalThis.__rcHost.${name} is not installed. Pass one directly, or have the native app install it before this package is constructed.`,\n );\n }\n return slice;\n}\n\n/** The value passed in, or `globalThis.__rcHost`'s slice of that name, or undefined. */\nexport function findHostSlice<K extends keyof NativeHostSlices>(\n name: K,\n injected: NativeHostSlices[K] | undefined,\n): NativeHostSlices[K] | undefined {\n return injected ?? (installedHost() as Partial<NativeHostSlices> | undefined)?.[name];\n}\n\n// ---------------------------------------------------------------------------\n// Copies. A host may reuse its own buffers across calls, so every tuple that\n// crosses back into this package is copied on the way in, never referenced.\n// ---------------------------------------------------------------------------\n\n/** Copy a position tuple. */\nexport function copyVec3(v: Vec3Tuple): Vec3Tuple {\n return [v[0], v[1], v[2]];\n}\n\n/** Copy an orientation tuple. */\nexport function copyQuat(q: QuatTuple): QuatTuple {\n return [q[0], q[1], q[2], q[3]];\n}\n\n/** Copy a pose (position + orientation). */\nexport function copyPose(pose: PoseTuple): PoseTuple {\n return { position: copyVec3(pose.position), quaternion: copyQuat(pose.quaternion) };\n}\n\n/** Copy a ray (origin + direction). */\nexport function copyRay(ray: RayTuple): RayTuple {\n return { origin: copyVec3(ray.origin), direction: copyVec3(ray.direction) };\n}\n\n/**\n * Copy one `InputSourceSnapshot` field by field, including every tuple it\n * carries, so a host that pools and refills its own snapshot objects still\n * meets the ownership rule: a snapshot handed to `sample()`'s caller is\n * never written to again.\n */\nexport function copySnapshot(source: InputSourceSnapshot): InputSourceSnapshot {\n const copy: InputSourceSnapshot = {\n id: source.id,\n kind: source.kind,\n handedness: source.handedness,\n select: source.select,\n squeeze: source.squeeze,\n };\n if (source.ray) copy.ray = copyRay(source.ray);\n if (source.gripPose) copy.gripPose = copyPose(source.gripPose);\n if (source.indexTip) copy.indexTip = copyVec3(source.indexTip);\n if (source.linearVelocity) copy.linearVelocity = copyVec3(source.linearVelocity);\n if (source.angularVelocity) copy.angularVelocity = copyVec3(source.angularVelocity);\n if (source.nativeGrabbing !== undefined) copy.nativeGrabbing = source.nativeGrabbing;\n if (source.hapticsAvailable !== undefined) copy.hapticsAvailable = source.hapticsAvailable;\n if (source.selectorPose) copy.selectorPose = copyPose(source.selectorPose);\n return copy;\n}\n"]}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * NativePhysicsFacility - the core `PhysicsFacility` over the native host's
3
+ * `physics` slice. The host runs the platform's default engine (Jolt on
4
+ * Quest and Android, RealityKit on visionOS) behind exactly the contract's
5
+ * members; this class is a thin binding that copies every tuple across the
6
+ * boundary in both directions, so a host that reuses its own buffers still
7
+ * meets the ownership rule the shared suite checks, and a tuple the core
8
+ * hands over is never kept by the host.
9
+ *
10
+ * An app that replaces the default engine passes its own `PhysicsFacility`
11
+ * as the slice: the shapes are identical, so the override is the same path.
12
+ */
13
+ import type { PoseTuple, Vec3Tuple } from "@realitycollective/webxr-input";
14
+ import type { HoldRelease, PhysicsBodySpec, PhysicsBodyState, PhysicsFacility, PhysicsShapeSpec, PhysicsVelocity } from "@realitycollective/webxr-interactions";
15
+ import { type NativePhysicsHost } from "./native-types.js";
16
+ export interface NativePhysicsFacilityOptions {
17
+ /** The `physics` slice, or an app's own facility. Omit to read `globalThis.__rcHost.physics`. */
18
+ physics?: NativePhysicsHost;
19
+ }
20
+ export declare class NativePhysicsFacility implements PhysicsFacility {
21
+ private readonly host;
22
+ constructor(options?: NativePhysicsFacilityOptions);
23
+ /** The engine the host runs, as it names it: `"jolt"`, `"realitykit"`, or an app's own. */
24
+ get engine(): string;
25
+ getGravity(): Vec3Tuple;
26
+ setGravity(gravity: Vec3Tuple): void;
27
+ addBody(id: string, pose: PoseTuple, body?: PhysicsBodySpec, shape?: PhysicsShapeSpec): void;
28
+ removeBody(id: string): void;
29
+ hasBody(id: string): boolean;
30
+ setBodyState(id: string, state: PhysicsBodyState): void;
31
+ getBodyState(id: string): PhysicsBodyState;
32
+ getBodyPose(id: string): PoseTuple;
33
+ setBodyPose(id: string, pose: PoseTuple): void;
34
+ getVelocity(id: string): PhysicsVelocity;
35
+ setVelocity(id: string, velocity: PhysicsVelocity): void;
36
+ suspend(id: string): void;
37
+ resume(id: string, release: HoldRelease): void;
38
+ isSuspended(id: string): boolean;
39
+ step(dtSeconds: number): void;
40
+ dispose(): void;
41
+ }
@@ -0,0 +1,63 @@
1
+ import { copyPose, copyVec3, resolveHostSlice } from "./native-types.js";
2
+ function copyVelocity(velocity) {
3
+ return { linear: copyVec3(velocity.linear), angular: copyVec3(velocity.angular) };
4
+ }
5
+ export class NativePhysicsFacility {
6
+ host;
7
+ constructor(options = {}) {
8
+ this.host = resolveHostSlice("physics", options.physics);
9
+ }
10
+ /** The engine the host runs, as it names it: `"jolt"`, `"realitykit"`, or an app's own. */
11
+ get engine() {
12
+ return this.host.engine;
13
+ }
14
+ getGravity() {
15
+ return copyVec3(this.host.getGravity());
16
+ }
17
+ setGravity(gravity) {
18
+ this.host.setGravity(copyVec3(gravity));
19
+ }
20
+ addBody(id, pose, body, shape) {
21
+ this.host.addBody(id, copyPose(pose), body ? { ...body } : undefined, shape ? { ...shape, ...(shape.dimensions ? { dimensions: copyVec3(shape.dimensions) } : {}) } : undefined);
22
+ }
23
+ removeBody(id) {
24
+ this.host.removeBody(id);
25
+ }
26
+ hasBody(id) {
27
+ return this.host.hasBody(id);
28
+ }
29
+ setBodyState(id, state) {
30
+ this.host.setBodyState(id, state);
31
+ }
32
+ getBodyState(id) {
33
+ return this.host.getBodyState(id);
34
+ }
35
+ getBodyPose(id) {
36
+ return copyPose(this.host.getBodyPose(id));
37
+ }
38
+ setBodyPose(id, pose) {
39
+ this.host.setBodyPose(id, copyPose(pose));
40
+ }
41
+ getVelocity(id) {
42
+ return copyVelocity(this.host.getVelocity(id));
43
+ }
44
+ setVelocity(id, velocity) {
45
+ this.host.setVelocity(id, copyVelocity(velocity));
46
+ }
47
+ suspend(id) {
48
+ this.host.suspend(id);
49
+ }
50
+ resume(id, release) {
51
+ this.host.resume(id, { linearVelocity: copyVec3(release.linearVelocity), angularVelocity: copyVec3(release.angularVelocity) });
52
+ }
53
+ isSuspended(id) {
54
+ return this.host.isSuspended(id);
55
+ }
56
+ step(dtSeconds) {
57
+ this.host.step(dtSeconds);
58
+ }
59
+ dispose() {
60
+ this.host.dispose();
61
+ }
62
+ }
63
+ //# sourceMappingURL=physics-facility.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"physics-facility.js","sourceRoot":"","sources":["../src/physics-facility.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * NativePhysicsFacility - the core `PhysicsFacility` over the native host's\n * `physics` slice. The host runs the platform's default engine (Jolt on\n * Quest and Android, RealityKit on visionOS) behind exactly the contract's\n * members; this class is a thin binding that copies every tuple across the\n * boundary in both directions, so a host that reuses its own buffers still\n * meets the ownership rule the shared suite checks, and a tuple the core\n * hands over is never kept by the host.\n *\n * An app that replaces the default engine passes its own `PhysicsFacility`\n * as the slice: the shapes are identical, so the override is the same path.\n */\nimport type { PoseTuple, Vec3Tuple } from \"@realitycollective/webxr-input\";\nimport type {\n HoldRelease,\n PhysicsBodySpec,\n PhysicsBodyState,\n PhysicsFacility,\n PhysicsShapeSpec,\n PhysicsVelocity,\n} from \"@realitycollective/webxr-interactions\";\nimport { copyPose, copyVec3, resolveHostSlice, type NativePhysicsHost } from \"./native-types.js\";\n\nexport interface NativePhysicsFacilityOptions {\n /** The `physics` slice, or an app's own facility. Omit to read `globalThis.__rcHost.physics`. */\n physics?: NativePhysicsHost;\n}\n\nfunction copyVelocity(velocity: PhysicsVelocity): PhysicsVelocity {\n return { linear: copyVec3(velocity.linear), angular: copyVec3(velocity.angular) };\n}\n\nexport class NativePhysicsFacility implements PhysicsFacility {\n private readonly host: NativePhysicsHost;\n\n constructor(options: NativePhysicsFacilityOptions = {}) {\n this.host = resolveHostSlice(\"physics\", options.physics);\n }\n\n /** The engine the host runs, as it names it: `\"jolt\"`, `\"realitykit\"`, or an app's own. */\n get engine(): string {\n return this.host.engine;\n }\n\n getGravity(): Vec3Tuple {\n return copyVec3(this.host.getGravity());\n }\n\n setGravity(gravity: Vec3Tuple): void {\n this.host.setGravity(copyVec3(gravity));\n }\n\n addBody(id: string, pose: PoseTuple, body?: PhysicsBodySpec, shape?: PhysicsShapeSpec): void {\n this.host.addBody(\n id,\n copyPose(pose),\n body ? { ...body } : undefined,\n shape ? { ...shape, ...(shape.dimensions ? { dimensions: copyVec3(shape.dimensions) } : {}) } : undefined,\n );\n }\n\n removeBody(id: string): void {\n this.host.removeBody(id);\n }\n\n hasBody(id: string): boolean {\n return this.host.hasBody(id);\n }\n\n setBodyState(id: string, state: PhysicsBodyState): void {\n this.host.setBodyState(id, state);\n }\n\n getBodyState(id: string): PhysicsBodyState {\n return this.host.getBodyState(id);\n }\n\n getBodyPose(id: string): PoseTuple {\n return copyPose(this.host.getBodyPose(id));\n }\n\n setBodyPose(id: string, pose: PoseTuple): void {\n this.host.setBodyPose(id, copyPose(pose));\n }\n\n getVelocity(id: string): PhysicsVelocity {\n return copyVelocity(this.host.getVelocity(id));\n }\n\n setVelocity(id: string, velocity: PhysicsVelocity): void {\n this.host.setVelocity(id, copyVelocity(velocity));\n }\n\n suspend(id: string): void {\n this.host.suspend(id);\n }\n\n resume(id: string, release: HoldRelease): void {\n this.host.resume(id, { linearVelocity: copyVec3(release.linearVelocity), angularVelocity: copyVec3(release.angularVelocity) });\n }\n\n isSuspended(id: string): boolean {\n return this.host.isSuspended(id);\n }\n\n step(dtSeconds: number): void {\n this.host.step(dtSeconds);\n }\n\n dispose(): void {\n this.host.dispose();\n }\n}\n"]}