@realitycollective/native-interactions 0.1.1-preview.3 → 0.1.2-preview.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,10 +4,35 @@ Change log for the Reality Collective WebXR Interaction Extensions packages. All
4
4
 
5
5
  The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Preview builds are not listed separately. The entry for a version accumulates while its previews are published, and is dated when that version is released.
6
6
 
7
- ## [0.1.1]
7
+ ## [0.1.2]
8
+
9
+ ### Changed
10
+
11
+ - Development, tests and demos run on one shared copy of Meta's `super-three` fork at 0.185.0 (root `overrides`, with `@types/three` 0.185.4), the version every WebXR repository now uses, and on `@iwsdk/core` 1.0.1. IWSDK 1.0.1 changes no runtime code from 1.0.0. XR Blocks needs three r182 or later and logged an error on the r181 copy the demos used before. Published peer ranges are unchanged.
12
+ - The playground chooses its platform by asking the browser's WebXR runtime which immersive modes it can start (`demos/playground/src/platform-choice.ts`). The user agent now only tells a Meta browser from any other.
13
+
14
+ ### Added
15
+
16
+ - Playground diagnostics for framework testers, behind a hidden URL option: the on-device log and the report Pages Function of the UI Extensions lab, byte-identical (`demos/playground/src/diagnostics.ts`, `demos/playground/functions/api/report.ts`). `?log=1` records and sends by itself; `?log=local` keeps the log on the device; without the option nothing is recorded.
17
+
18
+ ### Fixed
19
+
20
+ - Playground, XR Blocks on a browser that can enter XR (Android XR, XREAL Aura): XR Blocks' Enter XR button was unstyled and below the visible page, and the 2D view sat at floor level. The button is pinned at the bottom centre and the view raised to IWSDK's 1.7 m (`demos/playground/src/platforms/xrblocks-page.ts`).
21
+
22
+ ## [0.1.1] - 2026-10-01
8
23
 
9
24
  ### Added
10
25
 
26
+ - Native parity round (Pale Signal handover of 29 September 2026): everything IWSDK delivers through its engine for the mechanics the Pale Signal web build uses, restated as core rules every binding runs, with the native contract stating what a host reports.
27
+ - `@realitycollective/webxr-interactions` - the grab lifecycle for hands is stated where it was implied: a hand's `select` is the runtime's binary pinch gesture and its `squeeze` 0 (the `@realitycollective/webxr-input` contract, IWSDK's `useHandPinchForGrab` over the browser's `selectstart`/`selectend`), so the core's 0.7/0.3 thresholds start a grab on the pinch and end it on the frame the pinch ends. `holderPoint` (the point a dial, slide or hinge tracks while held) is the GRIP first, as IWSDK's grab pointer is built on `xrOrigin.gripSpaces` and moves a held object with it, then the fingertip, then the ray origin; a closed hand's fingertip barely moves around a wheel's axis, which is why the native handwheel did not turn. `release-velocity.ts` (`ReleaseVelocityTracker`, `RELEASE_WINDOW_SECONDS` 1/60): the throw is the held OBJECT's velocity over IWSDK's physics step, angular from the orientation delta over the same step, which is what Havok gives a body IWSDK drives by target transform and releases; `GrabBehaviour` records the poses it writes and hands `endHold` that, zeros on a synthesised release (`InteractorInfo.synthesized`). `surfacePointOnSphere` and a `hitTesterContractCases()` case: `hitProximity`'s point is the surface point nearest the query, never the centre, so the touch cursor sits on the object; every platform's tester now reports it.
28
+ - `@realitycollective/webxr-interactions` - one pointer decision per source across interactables AND UI panels, as IWSDK's `MultiPointer` runs over every pointer-event object: `InteractionRuntime` takes `pointers` (a `PointerArbiter` from `@realitycollective/webxr-input`, shared with the UI Extensions hosts) and offers its touch, grab and ray candidates to it under `INTERACTIONS_POINTER_SET`; a panel nearer than an object takes the ray, a fingertip on a panel retires the ray over an object and the reverse, and `PointerVisuals` name the target kind, id and distance. `POINTER_PRIORITY`, `pickActivePointer`, `PointerCandidate`, `PointerVisuals` and `pointerVisualsFor` now live in `@realitycollective/webxr-input` and are re-exported unchanged. Two `nearPointerContractCases()` prove the arbitration on every platform's hit tester.
29
+ - `@realitycollective/webxr-interactions` - pointer display is app configuration through the core: `InteractionRuntime` takes `pointerDisplay` (`PointerDisplay` or a partial `PointerDisplayConfig`, IWSDK 1.0.0's look by default: ray while hitting, cursors on objects and panels), exposes `getPointerDisplay()` for run-time changes, and publishes `onPointerDrawing` / `getPointerDrawing`, the per-source `PointerDrawing` resolved from the arbiter's decision under the settings. `getPointerArbiter()` and `isSelecting()` are public.
30
+ - `@realitycollective/threejs-interactions` (reused by `@realitycollective/xrblocks-interactions`) - `ThreePointerVisuals` draws the drawing: IWSDK's ray shader and stub (3 cm from the hand to `min(0.3 m, the hit)`, 1 mm, white, blue while selecting) and the disc at the hit; `createThreeInteractions({ pointerVisuals: { scene }, pointers, pointerDisplay })` and `connectXRBlocksInteractions` take the same options.
31
+ - `@realitycollective/babylon-interactions` - `BabylonPointerVisuals` draws the same drawing through a structural kit of the app's own `MeshBuilder`, `StandardMaterial`, `Color3` and `Quaternion`; `createBabylonInteractions({ pointerVisuals: { kit }, pointers, pointerDisplay })`.
32
+ - `@realitycollective/iwsdk-interactions` - `IWSDKPointerVisuals` applies the app's settings to IWSDK's own `RayPointer` (`rayDisplayMode`, length) and `CursorVisual`, and enforces the core's drawing where a setting hides what IWSDK would show; `registerInteractions({ pointers, pointerDisplay, pointerVisuals })`.
33
+ - `@realitycollective/webxr-interactions` - the trace suite (G6): `trace.ts` (`rc-trace.v1`, `TraceRecorder`, `TraceInputProvider`, `replayTrace`, `compareOutcomes`, `traceParityCases`, `TRACE_TOLERANCES`) replays a recorded session's input through a binding's own ports and compares press and grab timing, values, poses and release velocity with the reference within stated tolerances; `memory-ports.ts` (`MemoryHitTester`, `MemoryTransformPort`) are the engine-free reference ports that pass both shared suites; `trace-synthetic.ts` scripts the playground mechanics through the core so the pipeline is proved on every platform before a person records on a Quest 3 (`iwsdk-interactions/test/trace-parity.test.ts` replays the synthetic traces on all five platforms, and recorded ones from the folder `RC_TRACES_DIR` names).
34
+ - The playground (`demos/playground`) runs on IWSDK, three.js, Babylon and XR Blocks from one launch screen with one descriptor, one pointer display and a Record button that downloads a trace; the native harness (`harness/native`) is a project for the WebXR-to-native conversion pipeline that runs every suite this family ships against a host on the device (`kits`), plays the same stations there (`play`) and records a native trace (`trace`), and `npm run harness:compile` typechecks it, bundles it through the pipeline's gates and compiles it to Hermes bytecode. CI compiles the harness and never runs it; native builds run only on a developer's machine or a headset.
35
+ - `@realitycollective/native-interactions` - `NativeInputHost.sample()` states what a hand and a controller report for `select` and `squeeze`, with the OpenXR source and resting values (a hand: binary pinch from `XR_FB_hand_tracking_aim` or `XR_EXT_hand_interaction`, squeeze 0; a controller: trigger and grip values), and the binding forces a hand's squeeze to 0. `applyPointerVisuals` hands the host `NativePointerVisuals`, the resolved drawing (stub from `rayFrom` to `rayTo`, radius, colour, disc radius, opacity, offset) plus the decision (`activePointer`, `targetKind`, `targetId`, `hitDistance`), so a panel cursor reaches the host through the same member and no shell hook decides a display mode; `applyPointerDisplay` carries the settings on every change. Every query considers registered interactables only, never scenery, and `hitProximity`'s point is the surface point nearest the query; the test host gains `placeScenery` and `pointerDisplay`. Kit cases: a hand's select is 0 or 1 and its squeeze 0; the pointer display is the app's and the host draws what it is handed; queries never answer with scenery enclosing the hand; the drawing case now covers a panel cursor and a ray told off. `createNativeInteractions({ pointers, pointerDisplay })`.
11
36
  - `@realitycollective/webxr-interactions` - the near-pointer rule, IWSDK 1.0.0's pointer stack restated as one core decision every binding runs (`near-pointer.ts`, `NEAR_POINTER_DEFAULTS`): a fingertip (or a controller's ray origin) within 0.15 m of a target's surface owns the source as the touch pointer (hysteresis out to 0.2 m), presses at 0.02 m arriving from outside and releases on the way out, never pressing from inside or behind; the grip is a 0.07 m grab sphere; per source the first of touch, grab, ray with a candidate is the active pointer, locked while it presses or grabs; and `InteractionRuntime` publishes per-source `PointerVisuals` (`onPointerVisuals`, `getPointerVisuals`, `getActivePointer`): the ray and its cursor show only while the ray owns the source or nothing does, with the cursor at the active pointer's hit. `InteractableDescriptor.pokeRadius` is now the touch hover enter distance for that target (default 0.15 m, was a 0.05 m proximity trigger). `nearPointerContractCases()` is the shared suite, run on every platform's hit tester.
12
37
  - `@realitycollective/webxr-interactions` - the physics contract (`physics.ts`): `PhysicsFacility`, the platform physics behind one set of semantics and IWSDK 1.0.0's defaults (`PHYSICS_DEFAULTS`: gravity `[0, -9.81, 0]`, 60 steps per second with interpolation, a dynamic body with no damping and gravity factor 1, an auto shape with density 1, restitution 0 and friction 0.5), static and kinematic states, suspend for a hold, resume with the release velocity, teleport with velocity cleared. `physicsFacilityContractCases()` proves a facility falls and rests, bounces, holds, throws, teleports and disposes; `MemoryPhysicsFacility` is the engine-free reference. Every binding names a default engine and takes an app's own facility instead: `createThreeInteractions({ physics: { rapier } })` (Rapier, `RapierPhysicsFacility`, proved against the real engine), `connectXRBlocksInteractions({ physics: { rapier } })` (the Rapier XR Blocks bundles), `registerInteractions` (Havok through IWSDK's components, `IWSDKPhysicsFacility`), `createBabylonInteractions({ physics: { kit } })` (Havok, `BabylonPhysicsFacility`, proved against a fake), and the native `physics` slice (`NativePhysicsHost`, Jolt on Quest and Android, RealityKit on visionOS, with every obligation stated in `native-types.ts` and the whole shared suite in the host conformance kit). A registered object with a body drives the held pose through its facility; `register` takes `body` and `shape` specs.
13
38
  - `@realitycollective/native-interactions` - `NativeInputHost.applyPointerVisuals(sourceId, visuals)`: the host is told each frame whether to draw a source's ray and where its cursor sits, and draws exactly that; the earlier rule that the host draws a cursor at every ray hit is withdrawn. The kit gains `input/the ray and cursor are drawn exactly as told`, the whole `nearPointerContractCases()` suite over the host's own queries, and the physics cases.
@@ -38,6 +63,8 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
38
63
 
39
64
  ### Changed
40
65
 
66
+ - Every package now requires `@realitycollective/webxr-input` at `^0.1.8-preview.0` rather than `^0.1.7`: the pointer arbiter and the pointer display the code above uses exist only from that release, so an older Input fails at install rather than at run time.
67
+
41
68
  - The reference is now IWSDK 1.0. `@realitycollective/iwsdk-interactions` peers on `@iwsdk/core` and `@iwsdk/xr-input` `>=1.0.0 <2.0.0` (was `>=0.5.0 <0.6.0`) and is built and tested against 1.0.0. The per-frame visibility write the presence code works around is unchanged in 1.0.
42
69
  - `@realitycollective/threejs-interactions`, `@realitycollective/babylon-interactions` and `@realitycollective/xrblocks-interactions` - a controller's `indexTip` is its ray origin, as IWSDK's input rig makes it, so a controller pokes buttons and interactables as a fingertip does. Before this only a tracked hand carried an index tip on these three platforms, and controllers could not poke at all.
43
70
  - **Breaking for native hosts:** `@realitycollective/native-interactions` now derives and decides what the IWSDK provider derives and decides, and a native host reports facts rather than conclusions. What a host must absorb:
@@ -48,9 +75,13 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
48
75
  - `@realitycollective/native-interactions` - `NativeInputProvider` samples no sources while the session lacks focus, as IWSDK's provider samples none unless its visibility state is `Visible`; clamps a pulse's intensity to 0..1, as IWSDK does; takes `nativeGrab` as IWSDK's provider does; and has a `dispose()`, which `NativeInteractions.dispose()` calls.
49
76
  - `@realitycollective/webxr-interactions` - `TransformPort.getWorldPose()` is the LIVE world pose on every platform, and the new required `getRestWorldPose()` returns the rest pose captured at registration, in world space. The core only said "current world pose", and the platforms disagreed: the three.js, IWSDK and XR Blocks ports returned the rest pose, while the Babylon.js port returned the live one. The same behaviour therefore acted differently by platform. The hinge, slide and dial behaviours measure the hand against the rest pose, so they now call `getRestWorldPose()`. Grab and toss scoring keep `getWorldPose()` and now see where the object really is. `ports.ts` also states tuple ownership for `TransformPort` and `HitTester`: every returned tuple is fresh and owned by the caller, and a tuple passed in is read during the call and not kept. Every platform already met the ownership rule; the suites now hold them to it. The native `interactions` slice gains `getRestWorldPose(targetId)`, which the native app must implement. This adds a required member to `TransformPort`, so a port written outside this repository must implement it.
50
77
  - `@realitycollective/xrblocks-interactions` - the structural slices `XBVec3Like`, `XBQuatLike`, `XBRayLike` and `XBControllerLike` are exported, under the same `XB` prefix as `XBRaySourceLike`, `XBDirectTouchLike` and `XBFrameLike` whose fields they type. They were module-private, so a consumer building a fake frame for a test had nothing to name, and TypeDoc reported each as referenced but undocumented.
78
+ - Dependencies on other Reality Collective repositories now follow one rule, checked by `scripts/rc-dependencies.mjs`. Going into `main`, each names the release npm tags `latest` and never a preview. Going into any other branch, each names the newest version on npm, release or preview. The rule covers every `package.json` in the repository (published packages, demos, harnesses and examples) and every copy `package-lock.json` records. A range passes only when its lowest version is the target, so `^0.1.8` passes for 0.1.8 and `^0.1.4` does not. CI runs it before the install, against the branch a pull request targets, and the publish workflow runs it before publishing. `release.mjs status` reports it. `release.mjs prepare` stops before cutting a release when a dependency names an unreleased preview of another repository, because that repository has to release first, and moves any range that is only behind. Other dependencies are out of scope. The script stays identical across the Reality Collective repositories.
51
79
 
52
80
  ### Fixed
53
81
 
82
+ - `@realitycollective/iwsdk-interactions` - `register` and the physics facility no longer throw in a world created with `features.physics` off. IWSDK registers `PhysicsBody` and `PhysicsShape` only when physics is on, and elics throws when an entity is asked about a component its world never registered, so the first registration in such an app failed with "Cannot read properties of null (reading 'words')". The binding now treats an unregistered component as absent; a test drives a real elics world with physics off.
83
+ - The playground's XR Blocks page drew nothing on a desktop: XR Blocks appends its canvas after the full-height container, below the visible page. The playground now moves it into the container.
84
+
54
85
  - `@realitycollective/threejs-interactions`, `@realitycollective/iwsdk-interactions` and `@realitycollective/xrblocks-interactions` - `getWorldPose()` returned the pose captured at registration, not where the object was. Toss scoring therefore read a thrown ball at its registered position until the app called `recaptureRest()`, and a second grab of an object moved without a recapture took its hold offset from the old position. `getWorldPose()` now returns the live pose, as the Babylon.js port already did.
55
86
  - `@realitycollective/webxr-interactions` - `InteractableDescriptor.pokeRadius` had no effect. The value was stored on the registration and documented as the poke trigger radius, but the proximity query always used the 5 cm default, so an interactable asking for a wider or narrower poke got the default. The runtime now queries once per source at the largest radius any enabled interactable registered, and accepts the nearest hit only when it lies inside that interactable's own radius. One query per source is kept deliberately; the trade is that a farther interactable with a larger radius is not found behind a nearer one with a smaller radius, because the hit tester returns only the nearest. Covered by a runtime test that failed before the change.
56
87
  - CI - the staging deploy published under `--branch=pr-<number>` while the `-test` Pages project's production branch is `staging`, so only `pr-<n>.webxr-interactions-test.pages.dev` aliases were ever created and the project's root URL was a 404. It now deploys as `staging` on every pull request and on every push to `development`, so `webxr-interactions-test.pages.dev` serves the newest preview build, the same arrangement WebXR-UIExtensions already had.
@@ -109,5 +140,5 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
109
140
  - All five packages depend on `@realitycollective/webxr-input`, released independently from the [WebXR-Input](https://github.com/realitycollective/WebXR-Input) repository. That package must be published before this one.
110
141
  - The interaction packages themselves carry no IWSDK coupling beyond the adapter: `webxr-interactions` is engine-free, and `threejs-interactions` and `xrblocks-interactions` peer only on three.js.
111
142
 
112
- [0.1.1]: https://github.com/realitycollective/WebXR-Interactions/compare/v0.1.0...development
143
+ [0.1.1]: https://github.com/realitycollective/WebXR-Interactions/releases/tag/v0.1.1
113
144
  [0.1.0]: https://github.com/realitycollective/WebXR-Interactions/releases/tag/v0.1.0
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @realitycollective/native-interactions
2
2
 
3
- The native host adapter for the Reality Collective Interaction Extensions. A native app (OpenXR on Quest, CompositorServices on visionOS, or any other shell embedding a JavaScript engine such as Hermes) installs `globalThis.__rcHost`, and this package reads its `input` and `interactions` slices into the [`@realitycollective/webxr-interactions`](https://www.npmjs.com/package/@realitycollective/webxr-interactions) core.
3
+ The native host adapter for the Reality Collective Interaction Extensions. A native app (OpenXR on Quest, CompositorServices on visionOS, or any other shell embedding a JavaScript engine such as Hermes) installs `globalThis.__rcHost`, and this package reads its `input` and `interactions` slices, and the optional `physics` slice, into the [`@realitycollective/webxr-interactions`](https://www.npmjs.com/package/@realitycollective/webxr-interactions) core.
4
4
 
5
5
  ```sh
6
6
  npm install @realitycollective/native-interactions
@@ -8,6 +8,12 @@ npm install @realitycollective/native-interactions
8
8
 
9
9
  It re-exports everything from the core, so this is the only interaction package your app needs.
10
10
 
11
+ ## Tested on a headset
12
+
13
+ A native app built with this package passed on a Meta Quest 3 on 1 October 2026, played by a person and checked by its conformance kit.
14
+
15
+ Known issue on native: the grab interaction does not work as it should yet. Pinch works.
16
+
11
17
  ## What it binds
12
18
 
13
19
  | Layer | Detail |
@@ -16,6 +22,10 @@ It re-exports everything from the core, so this is the only interaction package
16
22
  | **Hit-testing** | `__rcHost.interactions.hitRay` / `hitProximity` with IWSDK's semantics: the ray parameter to the closest point, and the distance to a target's SURFACE, clamped at zero. `setTargetRadius` tells the host each target's radius (0.1 m unless given) |
17
23
  | **Movement** | `__rcHost.interactions`, keyed by the target id you chose when you registered the object with the native scene |
18
24
  | **No engine dependency** | Assets and rendering stay in the native app; only numbers, strings, booleans and tuples cross the boundary |
25
+ | **Pointer drawing** | The host is handed the resolved drawing every frame (`applyPointerVisuals`) and the app's display settings (`applyPointerDisplay`); `createNativeInteractions({ pointers, pointerDisplay })` shares one arbiter with the UI binding |
26
+ | **Physics** | `NativePhysicsFacility` over the host's `physics` slice (the `physics` option): the host simulates, the binding decides held, released and rest as IWSDK does |
27
+ | **Eye gaze** | `getFacts().eyeTracking` and `getEyeGazePose()` on the input slice; the provider applies the shared gaze-and-pinch rule |
28
+ | **Proof on the device** | `nativeInteractionsHostConformanceCases()` (the kit, below) and the repository's native harness (`harness/native`), which the conversion pipeline builds into an app that runs every suite against the real host |
19
29
 
20
30
  ## Usage
21
31
 
@@ -56,6 +66,10 @@ A missing slice - neither passed in nor found on `globalThis.__rcHost` - throws
56
66
  - **One port per interactable.** `TransformPort` is per object; the native host is one object serving every registered interactable, so `NativeTransformPort` binds one `targetId` onto it, the same idea as an engine adapter's port binding to one scene node.
57
67
  - **`NativeHit.targetId`** is renamed to the core's `interactableId` on the way through `NativeHitTester` - the native host names the thing it hit, the core names the thing it manages.
58
68
 
69
+ ## What a host reports for a hand, and what it is handed to draw
70
+
71
+ `native-types.ts` states every input value a host reports, with its OpenXR source and resting value: a hand's `select` is the runtime's pinch gesture, 1 or 0, never a strength, and its `squeeze` is 0; a controller's are its trigger and grip values. Every hit query considers registered interactables only, never scenery, and a proximity hit's point is the surface point nearest the fingertip. The host is handed the resolved pointer drawing every frame (`applyPointerVisuals`: the ray stub's extent, radius and colour, the cursor's point, radius, opacity and offset, and which panel or object it sits on) and decides no display mode of its own. The native harness (`harness/native`) runs the whole kit on a device.
72
+
59
73
  ## Proving a host: the conformance kit
60
74
 
61
75
  `nativeInteractionsHostConformanceCases()` returns the host cases, as runner-free data named `interactions/<row>` and `input/<row>`. A native app runs them on its device, inside a live focused session, against its real slices and the test readbacks of `NativeInteractionsTestHost` (`placeTarget`, `clearTargets`, `presenceShown`, `lastRelease`, `cursors`). They include the core's whole `hitTesterContractCases()` run against the host itself, so a host that measures proximity to a target's centre fails on the device, not in the field.
@@ -15,12 +15,39 @@
15
15
  * Runner-free, like the core suites: a case resolves on success and rejects
16
16
  * with a plain `Error` naming its row otherwise.
17
17
  */
18
+ import { POINTER_DISPLAY_DEFAULTS } from "@realitycollective/webxr-input";
18
19
  import { hitTesterContractCases, nearPointerContractCases, physicsFacilityContractCases, } from "@realitycollective/webxr-interactions";
19
20
  import { NativeHitTester } from "./hit-tester.js";
20
21
  import { NativeInteractions } from "./host.js";
21
22
  import { NativePhysicsFacility } from "./physics-facility.js";
22
23
  import { NativeInputProvider } from "./provider.js";
23
24
  import { NativeTransformPort } from "./transform-port.js";
25
+ /** A drawing as the binding hands it: IWSDK's defaults resolved for one decision. */
26
+ function told(sourceId, decision) {
27
+ const d = POINTER_DISPLAY_DEFAULTS;
28
+ return {
29
+ sourceId,
30
+ ray: false,
31
+ rayFrom: d.rayHandFade,
32
+ raySolidTo: d.rayReach - d.rayFade,
33
+ rayTo: d.rayReach,
34
+ rayRadius: d.rayRadius,
35
+ rayColor: [d.rayColor[0], d.rayColor[1], d.rayColor[2]],
36
+ cursor: false,
37
+ cursorPoint: null,
38
+ cursorRadius: d.cursorRadius,
39
+ cursorOpacity: d.cursorOpacity,
40
+ cursorOffset: d.cursorOffset,
41
+ activePointer: null,
42
+ targetKind: null,
43
+ targetId: null,
44
+ hitDistance: null,
45
+ ...decision,
46
+ };
47
+ }
48
+ function samePoint(a, b) {
49
+ return JSON.stringify(a) === JSON.stringify(b);
50
+ }
24
51
  function fail(name, message) {
25
52
  throw new Error(`[${name}] ${message}`);
26
53
  }
@@ -113,34 +140,131 @@ export function nativeInteractionsHostConformanceCases() {
113
140
  }
114
141
  }),
115
142
  hostCase("input/the ray and cursor are drawn exactly as told", ({ input, testHost }, name) => {
116
- // The binding decides the ray and cursor per source (the near-pointer
117
- // rule) and hands the host the result; a host that draws its own ray
118
- // or a cursor at every ray hit fails here, as the Quest host did when
119
- // it kept the cursor on a touched object.
143
+ // The binding decides the ray and cursor per source (the arbiter under
144
+ // the app's pointer display) and hands the host the resolved drawing; a
145
+ // host that draws its own ray, a cursor at every ray hit, or applies a
146
+ // display mode of its own fails here, as the Quest host did when it kept
147
+ // the cursor on a touched object and later drew the ray by a shell hook.
120
148
  if (typeof input.applyPointerVisuals !== "function") {
121
149
  fail(name, "the host has no applyPointerVisuals, so it cannot hide the ray while a hand touches something");
122
150
  }
123
151
  if (typeof testHost.pointerVisuals !== "function") {
124
152
  fail(name, "the test host has no pointerVisuals readback");
125
153
  }
126
- const touched = { sourceId: "rc-kit-source", activePointer: "touch", ray: false, cursor: true, cursorPoint: [0, 1, -0.9] };
154
+ const touched = told("rc-kit-source", { activePointer: "touch", ray: false, cursor: true, cursorPoint: [0, 1, -0.9], targetKind: "object", targetId: "rc-kit-target", hitDistance: 0.01 });
127
155
  input.applyPointerVisuals("rc-kit-source", { ...touched });
128
156
  const drawn = testHost.pointerVisuals("rc-kit-source");
129
- if (!drawn || drawn.ray || !drawn.cursor || JSON.stringify(drawn.cursorPoint) !== JSON.stringify(touched.cursorPoint)) {
157
+ if (!drawn || drawn.ray || !drawn.cursor || !samePoint(drawn.cursorPoint, touched.cursorPoint)) {
130
158
  fail(name, `told ${JSON.stringify(touched)}, the host draws ${JSON.stringify(drawn)}`);
131
159
  }
132
- if (!testHost.cursors().some((point) => JSON.stringify(point) === JSON.stringify(touched.cursorPoint))) {
160
+ if (!testHost.cursors().some((point) => samePoint(point, touched.cursorPoint))) {
133
161
  fail(name, "the host draws no cursor disc at the point it was told");
134
162
  }
135
- const idle = { sourceId: "rc-kit-source", activePointer: null, ray: true, cursor: false, cursorPoint: null };
163
+ const idle = told("rc-kit-source", { ray: true });
136
164
  input.applyPointerVisuals("rc-kit-source", { ...idle });
137
165
  const after = testHost.pointerVisuals("rc-kit-source");
138
166
  if (!after || !after.ray || after.cursor) {
139
167
  fail(name, `told ${JSON.stringify(idle)}, the host draws ${JSON.stringify(after)}`);
140
168
  }
141
- if (testHost.cursors().some((point) => JSON.stringify(point) === JSON.stringify(touched.cursorPoint))) {
169
+ if (testHost.cursors().some((point) => samePoint(point, touched.cursorPoint))) {
142
170
  fail(name, "the host still draws the cursor disc after being told to stop");
143
171
  }
172
+ // A cursor on a PANEL arrives through the same member: the host draws
173
+ // it where told, and never needs a stopgap of its own for panels.
174
+ const onPanel = told("rc-kit-source", { activePointer: "ray", ray: true, rayTo: 0.3, cursor: true, cursorPoint: [0.2, 1.2, -0.8], targetKind: "panel", targetId: "rc-kit-panel", hitDistance: 0.8 });
175
+ input.applyPointerVisuals("rc-kit-source", { ...onPanel });
176
+ const panel = testHost.pointerVisuals("rc-kit-source");
177
+ if (!panel || !panel.cursor || !samePoint(panel.cursorPoint, onPanel.cursorPoint)) {
178
+ fail(name, `told a cursor on a panel ${JSON.stringify(onPanel)}, the host draws ${JSON.stringify(panel)}`);
179
+ }
180
+ if (!testHost.cursors().some((point) => samePoint(point, onPanel.cursorPoint))) {
181
+ fail(name, "the host draws no cursor disc on the panel it was told about");
182
+ }
183
+ // The ray told off stays off whatever the host might prefer to show.
184
+ const rayOff = told("rc-kit-source", { activePointer: "ray", ray: false, cursor: true, cursorPoint: [0, 1, -1.5], targetKind: "object", targetId: "rc-kit-target", hitDistance: 1.5 });
185
+ input.applyPointerVisuals("rc-kit-source", { ...rayOff });
186
+ const off = testHost.pointerVisuals("rc-kit-source");
187
+ if (!off || off.ray)
188
+ fail(name, "told to draw no ray while the ray hits an object (the app's display says never), the host still draws one");
189
+ input.applyPointerVisuals("rc-kit-source", { ...told("rc-kit-source", {}) });
190
+ }),
191
+ hostCase("input/the pointer display is the app's: the binding resolves it and the host draws what it is handed", ({ input, interactions, testHost }, name) => {
192
+ // Two setups with opposite ray settings over the same live sources:
193
+ // what the host draws for every source must be exactly the binding's
194
+ // drawing each time. A host with a display mode of its own (a shell
195
+ // hook, a default of "always") disagrees on one of the two.
196
+ if (typeof input.applyPointerVisuals !== "function" || typeof testHost.pointerVisuals !== "function") {
197
+ fail(name, "the host has no applyPointerVisuals or no pointerVisuals readback");
198
+ }
199
+ for (const ray of ["never", "always"]) {
200
+ const setup = new NativeInteractions({ input, interactions, pointerDisplay: { ray } });
201
+ try {
202
+ if (typeof testHost.pointerDisplay === "function") {
203
+ const received = testHost.pointerDisplay();
204
+ if (received?.ray !== ray)
205
+ fail(name, `the host received pointer display ${JSON.stringify(received)} after the binding was configured with ray "${ray}"`);
206
+ }
207
+ setup.update(1 / 72);
208
+ for (const source of input.sample()) {
209
+ const decided = setup.runtime.getPointerDrawing(source.id);
210
+ const drawn = testHost.pointerVisuals(source.id);
211
+ if (!decided || !drawn)
212
+ fail(name, `the binding decided ${String(JSON.stringify(decided))} for source "${source.id}" and the host draws ${String(JSON.stringify(drawn))}: a sampled source gets a drawing, and the host keeps it`);
213
+ // With ray "never" the binding decides no ray, so a host that still draws one differs here.
214
+ if (drawn.ray !== decided.ray || drawn.cursor !== decided.cursor || !samePoint(drawn.cursorPoint, decided.cursorPoint) || Math.abs(drawn.rayTo - decided.rayTo) > 1e-9) {
215
+ fail(name, `with ray "${ray}", the binding decided ${JSON.stringify(decided)} for "${source.id}" and the host draws ${JSON.stringify(drawn)}`);
216
+ }
217
+ }
218
+ }
219
+ finally {
220
+ setup.dispose();
221
+ }
222
+ }
223
+ }),
224
+ hostCase("input/a hand's select is 0 or 1 (the runtime's pinch gesture) and its squeeze is 0", ({ input }, name) => {
225
+ // The web gives IWSDK a binary pinch (the browser's selectstart and
226
+ // selectend); a host that reports the analog pinch strength, or the
227
+ // grasp as squeeze, holds a grab open on a relaxed hand. Run with hands
228
+ // tracked; a session with controllers only has nothing to check here.
229
+ for (const source of input.sample()) {
230
+ if (source.kind !== "hand")
231
+ continue;
232
+ if (source.select !== 0 && source.select !== 1) {
233
+ fail(name, `hand "${source.id}" reports select ${source.select}; a hand's select is 1 while the runtime reports its pinch gesture and 0 otherwise, never a strength`);
234
+ }
235
+ if (source.squeeze !== 0) {
236
+ fail(name, `hand "${source.id}" reports squeeze ${source.squeeze}; a hand has no squeeze (its grab is its pinch through select), and a grasp value here never releases a grab`);
237
+ }
238
+ }
239
+ }),
240
+ hostCase("interactions/queries consider registered interactables only: scenery enclosing the hand is never answered", ({ interactions, testHost }, name) => {
241
+ // A host that answered proximity with any shown mesh found the floor's
242
+ // bounds around every hand and no fingertip ever reached a target.
243
+ if (typeof testHost.placeScenery !== "function")
244
+ fail(name, "the test host has no placeScenery, so the query scope cannot be proved");
245
+ testHost.clearTargets();
246
+ try {
247
+ testHost.placeScenery("rc-kit-floor", [0, 0, -1], 3);
248
+ testHost.placeScenery("rc-kit-wall", [0, 1, -1.05], 0.02);
249
+ if (interactions.hitProximity([0, 1, -1], 0.5) !== null)
250
+ fail(name, "with only scenery placed, hitProximity answered; queries consider registered interactables only");
251
+ if (interactions.hitRay({ origin: [0, 1, 0], direction: [0, 0, -1] }) !== null)
252
+ fail(name, "with only scenery placed, hitRay answered");
253
+ if (interactions.hitCone && interactions.hitCone({ origin: [0, 1, 0], direction: [0, 0, -1] }, (5 * Math.PI) / 180, 30) !== null)
254
+ fail(name, "with only scenery placed, hitCone answered");
255
+ testHost.placeTarget("rc-kit-target", [0, 1, -1], 0.1);
256
+ const near = interactions.hitProximity([0, 1, -0.87], 0.5);
257
+ if (!near || near.targetId !== "rc-kit-target")
258
+ fail(name, `a fingertip 3 cm from a target, inside the floor's bounds, must reach the target; got ${JSON.stringify(near)}`);
259
+ if (Math.abs(near.distance - 0.03) > 0.001)
260
+ fail(name, `the distance is to the target's surface, 0.03 within 1 mm, got ${near.distance}`);
261
+ const along = interactions.hitRay({ origin: [0, 1, 0], direction: [0, 0, -1] });
262
+ if (!along || along.targetId !== "rc-kit-target")
263
+ fail(name, `a ray through scenery to a target must answer the target; got ${JSON.stringify(along)}`);
264
+ }
265
+ finally {
266
+ testHost.clearTargets();
267
+ }
144
268
  }),
145
269
  hostCase("interactions/a registered target with no radius is a 10 cm sphere", ({ input, interactions, testHost }, name) => {
146
270
  testHost.clearTargets();
@@ -1 +1 @@
1
- {"version":3,"file":"conformance.js","sourceRoot":"","sources":["../src/conformance.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The Interactions host conformance kit: host cases (the third kind in the\n * Masters' \"Validation\" section), shipped as data so a native app runs them\n * against its REAL `input` and `interactions` slices on the device.\n *\n * The contract cases a native package ran before this checked shapes against\n * fakes, and passed while the real host measured proximity to a target's\n * centre, which removed every near interaction on the headset. These run the\n * same checks against the host itself, through the test readbacks of\n * `NativeInteractionsTestHost`: the core's whole `hitTesterContractCases()`\n * (surface distance included) over targets the host places, the default\n * target radius, the release velocity reaching `endHold`, and what the host\n * draws for presence. Run them inside a live, focused session.\n *\n * Runner-free, like the core suites: a case resolves on success and rejects\n * with a plain `Error` naming its row otherwise.\n */\nimport {\n hitTesterContractCases,\n nearPointerContractCases,\n physicsFacilityContractCases,\n type HoldRelease,\n type PointerVisuals,\n} from \"@realitycollective/webxr-interactions\";\nimport { NativeHitTester } from \"./hit-tester.js\";\nimport { NativeInteractions } from \"./host.js\";\nimport { NativePhysicsFacility } from \"./physics-facility.js\";\nimport { NativeInputProvider } from \"./provider.js\";\nimport { NativeTransformPort } from \"./transform-port.js\";\nimport type { Vec3Tuple } from \"@realitycollective/webxr-input\";\nimport type {\n NativeInputHost,\n NativeInteractionHost,\n NativeInteractionsTestHost,\n NativePhysicsHost,\n NativePresenceShown,\n} from \"./native-types.js\";\n\n/** The real slices under test, and their readbacks. */\nexport interface NativeInteractionsHostConformanceSetup {\n input: NativeInputHost;\n interactions: NativeInteractionHost;\n testHost: NativeInteractionsTestHost;\n /** The `physics` slice. A host that installs none fails the physics cases. */\n physics?: NativePhysicsHost;\n}\n\n/** One check a native host must pass. `name` is `interactions/<row>`, `input/<row>` or `physics/<row>`. */\nexport interface NativeInteractionsHostConformanceCase {\n name: string;\n run(setup: NativeInteractionsHostConformanceSetup): Promise<void>;\n}\n\nfunction fail(name: string, message: string): never {\n throw new Error(`[${name}] ${message}`);\n}\n\nfunction hostCase(\n name: string,\n run: (setup: NativeInteractionsHostConformanceSetup, name: string) => void | Promise<void>,\n): NativeInteractionsHostConformanceCase {\n return { name, run: async (setup) => run(setup, name) };\n}\n\nfunction exactlyOneFamily(shown: NativePresenceShown | undefined): boolean {\n return shown !== undefined && shown.hand !== shown.controller;\n}\n\n/** The Interactions and Input host conformance cases. See the file comment. */\nexport function nativeInteractionsHostConformanceCases(): NativeInteractionsHostConformanceCase[] {\n const hitCases = hitTesterContractCases().map((contractCase) =>\n hostCase(`interactions/${contractCase.name}`, ({ interactions, testHost }, name) => {\n testHost.clearTargets();\n try {\n contractCase.run({\n hitTester: new NativeHitTester({ interactions }),\n driver: { place: (id, position, radius) => testHost.placeTarget(id, position, radius) },\n });\n } catch (error) {\n fail(name, (error as Error).message);\n } finally {\n testHost.clearTargets();\n }\n }),\n );\n\n // The near-pointer rule over the host's real queries: touch press, grip\n // grab, pointer priority and what the ray and cursor should show.\n const nearCases = nearPointerContractCases().map((contractCase) =>\n hostCase(`interactions/${contractCase.name}`, ({ interactions, testHost }, name) => {\n testHost.clearTargets();\n try {\n contractCase.run({\n hitTester: new NativeHitTester({ interactions }),\n driver: { place: (id, position, radius) => testHost.placeTarget(id, position, radius) },\n });\n } catch (error) {\n fail(name, (error as Error).message);\n } finally {\n testHost.clearTargets();\n }\n }),\n );\n\n // The platform's physics behind the core contract: the whole shared suite\n // over the host's engine, on the device.\n const physicsCases = physicsFacilityContractCases().map((contractCase) =>\n hostCase(`physics/${contractCase.name}`, ({ physics }, name) => {\n if (!physics) fail(name, \"the host installs no physics slice; every native platform names a default engine (Jolt on Quest and Android, RealityKit on visionOS)\");\n const facility = new NativePhysicsFacility({ physics });\n try {\n contractCase.run({ facility });\n } catch (error) {\n fail(name, (error as Error).message);\n } finally {\n facility.dispose();\n }\n }),\n );\n\n return [\n ...hitCases,\n ...nearCases,\n ...physicsCases,\n hostCase(\"physics/a registered target with a body is held and released through the physics slice\", ({ input, interactions, physics, testHost }, name) => {\n if (!physics) fail(name, \"the host installs no physics slice\");\n testHost.clearTargets();\n const setup = new NativeInteractions({ input, interactions, physics });\n try {\n testHost.placeTarget(\"rc-kit-body\", [0, 1, -1], 0.1);\n const port = setup.register({ id: \"rc-kit-body\", behaviours: [{ kind: \"grab\" }] }, { shape: { kind: \"sphere\", dimensions: [0.1, 0, 0] } });\n if (!physics.hasBody(\"rc-kit-body\")) fail(name, \"register with a shape must add a body to the physics slice\");\n port.beginHold();\n if (!physics.isSuspended(\"rc-kit-body\")) fail(name, \"beginHold on a target with a body must suspend it in the physics slice\");\n port.endHold({ linearVelocity: [1, 0, 0], angularVelocity: [0, 0, 0] });\n if (physics.isSuspended(\"rc-kit-body\")) fail(name, \"endHold must resume the body\");\n const velocity = physics.getVelocity(\"rc-kit-body\").linear;\n if (Math.abs(velocity[0] - 1) > 0.01) fail(name, `endHold must hand the body the release velocity, it has ${JSON.stringify(velocity)}`);\n setup.unregister(\"rc-kit-body\");\n if (physics.hasBody(\"rc-kit-body\")) fail(name, \"unregister must remove the body the binding added\");\n } finally {\n setup.dispose();\n testHost.clearTargets();\n }\n }),\n hostCase(\"input/the ray and cursor are drawn exactly as told\", ({ input, testHost }, name) => {\n // The binding decides the ray and cursor per source (the near-pointer\n // rule) and hands the host the result; a host that draws its own ray\n // or a cursor at every ray hit fails here, as the Quest host did when\n // it kept the cursor on a touched object.\n if (typeof input.applyPointerVisuals !== \"function\") {\n fail(name, \"the host has no applyPointerVisuals, so it cannot hide the ray while a hand touches something\");\n }\n if (typeof testHost.pointerVisuals !== \"function\") {\n fail(name, \"the test host has no pointerVisuals readback\");\n }\n const touched: PointerVisuals = { sourceId: \"rc-kit-source\", activePointer: \"touch\", ray: false, cursor: true, cursorPoint: [0, 1, -0.9] };\n input.applyPointerVisuals(\"rc-kit-source\", { ...touched });\n const drawn = testHost.pointerVisuals(\"rc-kit-source\");\n if (!drawn || drawn.ray || !drawn.cursor || JSON.stringify(drawn.cursorPoint) !== JSON.stringify(touched.cursorPoint)) {\n fail(name, `told ${JSON.stringify(touched)}, the host draws ${JSON.stringify(drawn)}`);\n }\n if (!testHost.cursors().some((point) => JSON.stringify(point) === JSON.stringify(touched.cursorPoint))) {\n fail(name, \"the host draws no cursor disc at the point it was told\");\n }\n const idle: PointerVisuals = { sourceId: \"rc-kit-source\", activePointer: null, ray: true, cursor: false, cursorPoint: null };\n input.applyPointerVisuals(\"rc-kit-source\", { ...idle });\n const after = testHost.pointerVisuals(\"rc-kit-source\");\n if (!after || !after.ray || after.cursor) {\n fail(name, `told ${JSON.stringify(idle)}, the host draws ${JSON.stringify(after)}`);\n }\n if (testHost.cursors().some((point) => JSON.stringify(point) === JSON.stringify(touched.cursorPoint))) {\n fail(name, \"the host still draws the cursor disc after being told to stop\");\n }\n }),\n hostCase(\"interactions/a registered target with no radius is a 10 cm sphere\", ({ input, interactions, testHost }, name) => {\n testHost.clearTargets();\n const setup = new NativeInteractions({ input, interactions });\n try {\n // Placed small, then registered with no radius: the registration\n // must make it a 0.1 m sphere, so a point 9 cm from its centre is\n // inside it.\n testHost.placeTarget(\"rc-kit-bare\", [0, 1, -1], 0.02);\n setup.register({ id: \"rc-kit-bare\", behaviours: [] });\n const hit = interactions.hitProximity([0.09, 1, -1], 0.001);\n if (!hit || hit.targetId !== \"rc-kit-bare\") {\n fail(name, \"a point 9 cm from a bare target's centre did not reach it; the host is not using the 0.1 m radius\");\n }\n } finally {\n setup.dispose();\n testHost.clearTargets();\n }\n }),\n hostCase(\"interactions/endHold hands the host the release velocity\", ({ interactions, testHost }, name) => {\n testHost.clearTargets();\n try {\n testHost.placeTarget(\"rc-kit-throw\", [0, 1, -1], 0.1);\n const port = new NativeTransformPort(\"rc-kit-throw\", { interactions });\n const release: HoldRelease = { linearVelocity: [1, 2, 3], angularVelocity: [0, 0.5, 0] };\n port.beginHold();\n port.endHold(release);\n const received = testHost.lastRelease(\"rc-kit-throw\");\n if (JSON.stringify(received) !== JSON.stringify(release)) {\n fail(name, `the host received ${JSON.stringify(received)}, expected ${JSON.stringify(release)}`);\n }\n } finally {\n testHost.clearTargets();\n }\n }),\n hostCase(\"input/capabilities in a live session: rays, pokes and pose-only grabs\", ({ input }, name) => {\n const provider = new NativeInputProvider({ input });\n try {\n const caps = provider.getCapabilities();\n // Pose-only grabs follow from the same fact, so rays and pokes carry it.\n if (!caps.rays || !caps.pokes || caps.grabs !== \"poseOnly\") {\n fail(name, `a live session must report rays, pokes and pose-only grabs, got ${JSON.stringify(caps)}; run the kit inside a live session`);\n }\n } finally {\n provider.dispose();\n }\n }),\n hostCase(\"input/presence hides the models only, per side\", ({ input, testHost }, name) => {\n const provider = new NativeInputProvider({ input });\n try {\n if (!provider.getCapabilities().presence || !provider.setPresenceVisible) {\n fail(name, \"the host has no applyPresence, so presence cannot be shown or hidden\");\n }\n provider.setPresenceVisible(\"left\", false);\n const left = testHost.presenceShown(\"left\");\n if (!left || left.hand || left.controller) fail(name, `a hidden left side still draws ${JSON.stringify(left)}`);\n if (!exactlyOneFamily(testHost.presenceShown(\"right\"))) {\n fail(name, `the right side must still draw exactly one family, drew ${JSON.stringify(testHost.presenceShown(\"right\"))}`);\n }\n } finally {\n provider.setPresenceVisible?.(\"all\", true);\n provider.dispose();\n }\n }),\n hostCase('input/\"auto\" shows the family the live session uses', ({ input, testHost }, name) => {\n const provider = new NativeInputProvider({ input });\n try {\n provider.setPresenceModality?.(\"auto\");\n const handJoints = provider.getCapabilities().handJoints;\n for (const side of [\"left\", \"right\"] as const) {\n const shown = testHost.presenceShown(side);\n if (!shown || shown.hand !== handJoints || shown.controller !== !handJoints) {\n fail(name, `${side} draws ${JSON.stringify(shown)} with hand joints ${handJoints ? \"live\" : \"off\"}`);\n }\n }\n } finally {\n provider.dispose();\n }\n }),\n hostCase(\"input/eye gaze: a host that reports eye tracking poses the gaze, and the binding takes far targeting\", ({ input }, name) => {\n // Runs when the host reports eye tracking; a host without it (visionOS,\n // a Quest without eye tracking) passes without running. With it, the\n // host must hand the binding a valid pose in a live, focused session,\n // and the binding then samples one gaze snapshot with a ray. That the\n // hand and controller far rays drop is the shared contract case.\n if (!input.getFacts().eyeTracking) return;\n if (typeof input.getEyeGazePose !== \"function\") {\n fail(name, \"the host reports eyeTracking but has no getEyeGazePose(); the binding cannot use a fact it cannot pose\");\n }\n const provider = new NativeInputProvider({ input });\n try {\n if (!provider.getCapabilities().eyeGaze) {\n fail(name, \"the host reports eyeTracking in a live session, so capabilities.eyeGaze must be true; is the session focused?\");\n }\n const pose = input.getEyeGazePose();\n if (!pose || [...pose.position, ...pose.quaternion].some((n) => !Number.isFinite(n))) {\n fail(name, `getEyeGazePose() returned ${JSON.stringify(pose)}; run the kit while the user's eyes are tracked`);\n }\n const gaze = provider.sample().find((source) => source.kind === \"gaze\");\n if (!gaze?.ray) fail(name, \"with a valid gaze pose the binding must sample one gaze snapshot with a ray\");\n } finally {\n provider.dispose();\n }\n }),\n hostCase(\"input/eye gaze: a cone query never answers with a hidden target\", ({ interactions, testHost }, name) => {\n if (!interactions.hitCone) return;\n testHost.clearTargets();\n try {\n testHost.placeTarget(\"rc-kit-cone\", [0.2, 1, -2], 0.1);\n const ray = { origin: [0, 1, 0] as Vec3Tuple, direction: [0, 0, -1] as Vec3Tuple };\n const shown = interactions.hitCone(ray, (5 * Math.PI) / 180, 30);\n if (!shown || shown.targetId !== \"rc-kit-cone\") {\n fail(name, \"a target 0.2 m beside a ray 2 m out, inside a 5 degree cone, was not found\");\n }\n testHost.setTargetVisible?.(\"rc-kit-cone\", false);\n if (testHost.setTargetVisible && interactions.hitCone(ray, (5 * Math.PI) / 180, 30) !== null) {\n fail(name, \"the cone query answered with a hidden target\");\n }\n } finally {\n testHost.clearTargets();\n }\n }),\n hostCase(\"input/presence never hides the cursors\", ({ input, testHost }, name) => {\n const provider = new NativeInputProvider({ input });\n try {\n const before = testHost.cursors().length;\n provider.setPresenceVisible?.(\"all\", false);\n const after = testHost.cursors().length;\n if (after !== before) fail(name, `hiding presence changed the cursors drawn from ${before} to ${after}`);\n } finally {\n provider.setPresenceVisible?.(\"all\", true);\n provider.dispose();\n }\n }),\n ];\n}\n"]}
1
+ {"version":3,"file":"conformance.js","sourceRoot":"","sources":["../src/conformance.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The Interactions host conformance kit: host cases (the third kind in the\n * Masters' \"Validation\" section), shipped as data so a native app runs them\n * against its REAL `input` and `interactions` slices on the device.\n *\n * The contract cases a native package ran before this checked shapes against\n * fakes, and passed while the real host measured proximity to a target's\n * centre, which removed every near interaction on the headset. These run the\n * same checks against the host itself, through the test readbacks of\n * `NativeInteractionsTestHost`: the core's whole `hitTesterContractCases()`\n * (surface distance included) over targets the host places, the default\n * target radius, the release velocity reaching `endHold`, and what the host\n * draws for presence. Run them inside a live, focused session.\n *\n * Runner-free, like the core suites: a case resolves on success and rejects\n * with a plain `Error` naming its row otherwise.\n */\nimport { POINTER_DISPLAY_DEFAULTS } from \"@realitycollective/webxr-input\";\nimport {\n hitTesterContractCases,\n nearPointerContractCases,\n physicsFacilityContractCases,\n type HoldRelease,\n} from \"@realitycollective/webxr-interactions\";\nimport { NativeHitTester } from \"./hit-tester.js\";\nimport { NativeInteractions } from \"./host.js\";\nimport { NativePhysicsFacility } from \"./physics-facility.js\";\nimport { NativeInputProvider } from \"./provider.js\";\nimport { NativeTransformPort } from \"./transform-port.js\";\nimport type { Vec3Tuple } from \"@realitycollective/webxr-input\";\nimport type {\n NativeInputHost,\n NativeInteractionHost,\n NativeInteractionsTestHost,\n NativePhysicsHost,\n NativePointerVisuals,\n NativePresenceShown,\n} from \"./native-types.js\";\n\n/** A drawing as the binding hands it: IWSDK's defaults resolved for one decision. */\nfunction told(sourceId: string, decision: Partial<NativePointerVisuals>): NativePointerVisuals {\n const d = POINTER_DISPLAY_DEFAULTS;\n return {\n sourceId,\n ray: false,\n rayFrom: d.rayHandFade,\n raySolidTo: d.rayReach - d.rayFade,\n rayTo: d.rayReach,\n rayRadius: d.rayRadius,\n rayColor: [d.rayColor[0], d.rayColor[1], d.rayColor[2]],\n cursor: false,\n cursorPoint: null,\n cursorRadius: d.cursorRadius,\n cursorOpacity: d.cursorOpacity,\n cursorOffset: d.cursorOffset,\n activePointer: null,\n targetKind: null,\n targetId: null,\n hitDistance: null,\n ...decision,\n };\n}\n\nfunction samePoint(a: readonly number[] | null | undefined, b: readonly number[] | null | undefined): boolean {\n return JSON.stringify(a) === JSON.stringify(b);\n}\n\n/** The real slices under test, and their readbacks. */\nexport interface NativeInteractionsHostConformanceSetup {\n input: NativeInputHost;\n interactions: NativeInteractionHost;\n testHost: NativeInteractionsTestHost;\n /** The `physics` slice. A host that installs none fails the physics cases. */\n physics?: NativePhysicsHost;\n}\n\n/** One check a native host must pass. `name` is `interactions/<row>`, `input/<row>` or `physics/<row>`. */\nexport interface NativeInteractionsHostConformanceCase {\n name: string;\n run(setup: NativeInteractionsHostConformanceSetup): Promise<void>;\n}\n\nfunction fail(name: string, message: string): never {\n throw new Error(`[${name}] ${message}`);\n}\n\nfunction hostCase(\n name: string,\n run: (setup: NativeInteractionsHostConformanceSetup, name: string) => void | Promise<void>,\n): NativeInteractionsHostConformanceCase {\n return { name, run: async (setup) => run(setup, name) };\n}\n\nfunction exactlyOneFamily(shown: NativePresenceShown | undefined): boolean {\n return shown !== undefined && shown.hand !== shown.controller;\n}\n\n/** The Interactions and Input host conformance cases. See the file comment. */\nexport function nativeInteractionsHostConformanceCases(): NativeInteractionsHostConformanceCase[] {\n const hitCases = hitTesterContractCases().map((contractCase) =>\n hostCase(`interactions/${contractCase.name}`, ({ interactions, testHost }, name) => {\n testHost.clearTargets();\n try {\n contractCase.run({\n hitTester: new NativeHitTester({ interactions }),\n driver: { place: (id, position, radius) => testHost.placeTarget(id, position, radius) },\n });\n } catch (error) {\n fail(name, (error as Error).message);\n } finally {\n testHost.clearTargets();\n }\n }),\n );\n\n // The near-pointer rule over the host's real queries: touch press, grip\n // grab, pointer priority and what the ray and cursor should show.\n const nearCases = nearPointerContractCases().map((contractCase) =>\n hostCase(`interactions/${contractCase.name}`, ({ interactions, testHost }, name) => {\n testHost.clearTargets();\n try {\n contractCase.run({\n hitTester: new NativeHitTester({ interactions }),\n driver: { place: (id, position, radius) => testHost.placeTarget(id, position, radius) },\n });\n } catch (error) {\n fail(name, (error as Error).message);\n } finally {\n testHost.clearTargets();\n }\n }),\n );\n\n // The platform's physics behind the core contract: the whole shared suite\n // over the host's engine, on the device.\n const physicsCases = physicsFacilityContractCases().map((contractCase) =>\n hostCase(`physics/${contractCase.name}`, ({ physics }, name) => {\n if (!physics) fail(name, \"the host installs no physics slice; every native platform names a default engine (Jolt on Quest and Android, RealityKit on visionOS)\");\n const facility = new NativePhysicsFacility({ physics });\n try {\n contractCase.run({ facility });\n } catch (error) {\n fail(name, (error as Error).message);\n } finally {\n facility.dispose();\n }\n }),\n );\n\n return [\n ...hitCases,\n ...nearCases,\n ...physicsCases,\n hostCase(\"physics/a registered target with a body is held and released through the physics slice\", ({ input, interactions, physics, testHost }, name) => {\n if (!physics) fail(name, \"the host installs no physics slice\");\n testHost.clearTargets();\n const setup = new NativeInteractions({ input, interactions, physics });\n try {\n testHost.placeTarget(\"rc-kit-body\", [0, 1, -1], 0.1);\n const port = setup.register({ id: \"rc-kit-body\", behaviours: [{ kind: \"grab\" }] }, { shape: { kind: \"sphere\", dimensions: [0.1, 0, 0] } });\n if (!physics.hasBody(\"rc-kit-body\")) fail(name, \"register with a shape must add a body to the physics slice\");\n port.beginHold();\n if (!physics.isSuspended(\"rc-kit-body\")) fail(name, \"beginHold on a target with a body must suspend it in the physics slice\");\n port.endHold({ linearVelocity: [1, 0, 0], angularVelocity: [0, 0, 0] });\n if (physics.isSuspended(\"rc-kit-body\")) fail(name, \"endHold must resume the body\");\n const velocity = physics.getVelocity(\"rc-kit-body\").linear;\n if (Math.abs(velocity[0] - 1) > 0.01) fail(name, `endHold must hand the body the release velocity, it has ${JSON.stringify(velocity)}`);\n setup.unregister(\"rc-kit-body\");\n if (physics.hasBody(\"rc-kit-body\")) fail(name, \"unregister must remove the body the binding added\");\n } finally {\n setup.dispose();\n testHost.clearTargets();\n }\n }),\n hostCase(\"input/the ray and cursor are drawn exactly as told\", ({ input, testHost }, name) => {\n // The binding decides the ray and cursor per source (the arbiter under\n // the app's pointer display) and hands the host the resolved drawing; a\n // host that draws its own ray, a cursor at every ray hit, or applies a\n // display mode of its own fails here, as the Quest host did when it kept\n // the cursor on a touched object and later drew the ray by a shell hook.\n if (typeof input.applyPointerVisuals !== \"function\") {\n fail(name, \"the host has no applyPointerVisuals, so it cannot hide the ray while a hand touches something\");\n }\n if (typeof testHost.pointerVisuals !== \"function\") {\n fail(name, \"the test host has no pointerVisuals readback\");\n }\n const touched = told(\"rc-kit-source\", { activePointer: \"touch\", ray: false, cursor: true, cursorPoint: [0, 1, -0.9], targetKind: \"object\", targetId: \"rc-kit-target\", hitDistance: 0.01 });\n input.applyPointerVisuals(\"rc-kit-source\", { ...touched });\n const drawn = testHost.pointerVisuals(\"rc-kit-source\");\n if (!drawn || drawn.ray || !drawn.cursor || !samePoint(drawn.cursorPoint, touched.cursorPoint)) {\n fail(name, `told ${JSON.stringify(touched)}, the host draws ${JSON.stringify(drawn)}`);\n }\n if (!testHost.cursors().some((point) => samePoint(point, touched.cursorPoint))) {\n fail(name, \"the host draws no cursor disc at the point it was told\");\n }\n const idle = told(\"rc-kit-source\", { ray: true });\n input.applyPointerVisuals(\"rc-kit-source\", { ...idle });\n const after = testHost.pointerVisuals(\"rc-kit-source\");\n if (!after || !after.ray || after.cursor) {\n fail(name, `told ${JSON.stringify(idle)}, the host draws ${JSON.stringify(after)}`);\n }\n if (testHost.cursors().some((point) => samePoint(point, touched.cursorPoint))) {\n fail(name, \"the host still draws the cursor disc after being told to stop\");\n }\n // A cursor on a PANEL arrives through the same member: the host draws\n // it where told, and never needs a stopgap of its own for panels.\n const onPanel = told(\"rc-kit-source\", { activePointer: \"ray\", ray: true, rayTo: 0.3, cursor: true, cursorPoint: [0.2, 1.2, -0.8], targetKind: \"panel\", targetId: \"rc-kit-panel\", hitDistance: 0.8 });\n input.applyPointerVisuals(\"rc-kit-source\", { ...onPanel });\n const panel = testHost.pointerVisuals(\"rc-kit-source\");\n if (!panel || !panel.cursor || !samePoint(panel.cursorPoint, onPanel.cursorPoint)) {\n fail(name, `told a cursor on a panel ${JSON.stringify(onPanel)}, the host draws ${JSON.stringify(panel)}`);\n }\n if (!testHost.cursors().some((point) => samePoint(point, onPanel.cursorPoint))) {\n fail(name, \"the host draws no cursor disc on the panel it was told about\");\n }\n // The ray told off stays off whatever the host might prefer to show.\n const rayOff = told(\"rc-kit-source\", { activePointer: \"ray\", ray: false, cursor: true, cursorPoint: [0, 1, -1.5], targetKind: \"object\", targetId: \"rc-kit-target\", hitDistance: 1.5 });\n input.applyPointerVisuals(\"rc-kit-source\", { ...rayOff });\n const off = testHost.pointerVisuals(\"rc-kit-source\");\n if (!off || off.ray) fail(name, \"told to draw no ray while the ray hits an object (the app's display says never), the host still draws one\");\n input.applyPointerVisuals(\"rc-kit-source\", { ...told(\"rc-kit-source\", {}) });\n }),\n hostCase(\"input/the pointer display is the app's: the binding resolves it and the host draws what it is handed\", ({ input, interactions, testHost }, name) => {\n // Two setups with opposite ray settings over the same live sources:\n // what the host draws for every source must be exactly the binding's\n // drawing each time. A host with a display mode of its own (a shell\n // hook, a default of \"always\") disagrees on one of the two.\n if (typeof input.applyPointerVisuals !== \"function\" || typeof testHost.pointerVisuals !== \"function\") {\n fail(name, \"the host has no applyPointerVisuals or no pointerVisuals readback\");\n }\n for (const ray of [\"never\", \"always\"] as const) {\n const setup = new NativeInteractions({ input, interactions, pointerDisplay: { ray } });\n try {\n if (typeof testHost.pointerDisplay === \"function\") {\n const received = testHost.pointerDisplay();\n if (received?.ray !== ray) fail(name, `the host received pointer display ${JSON.stringify(received)} after the binding was configured with ray \"${ray}\"`);\n }\n setup.update(1 / 72);\n for (const source of input.sample()) {\n const decided = setup.runtime.getPointerDrawing(source.id);\n const drawn = testHost.pointerVisuals(source.id);\n if (!decided || !drawn) fail(name, `the binding decided ${String(JSON.stringify(decided))} for source \"${source.id}\" and the host draws ${String(JSON.stringify(drawn))}: a sampled source gets a drawing, and the host keeps it`);\n // With ray \"never\" the binding decides no ray, so a host that still draws one differs here.\n if (drawn.ray !== decided.ray || drawn.cursor !== decided.cursor || !samePoint(drawn.cursorPoint, decided.cursorPoint) || Math.abs(drawn.rayTo - decided.rayTo) > 1e-9) {\n fail(name, `with ray \"${ray}\", the binding decided ${JSON.stringify(decided)} for \"${source.id}\" and the host draws ${JSON.stringify(drawn)}`);\n }\n }\n } finally {\n setup.dispose();\n }\n }\n }),\n hostCase(\"input/a hand's select is 0 or 1 (the runtime's pinch gesture) and its squeeze is 0\", ({ input }, name) => {\n // The web gives IWSDK a binary pinch (the browser's selectstart and\n // selectend); a host that reports the analog pinch strength, or the\n // grasp as squeeze, holds a grab open on a relaxed hand. Run with hands\n // tracked; a session with controllers only has nothing to check here.\n for (const source of input.sample()) {\n if (source.kind !== \"hand\") continue;\n if (source.select !== 0 && source.select !== 1) {\n fail(name, `hand \"${source.id}\" reports select ${source.select}; a hand's select is 1 while the runtime reports its pinch gesture and 0 otherwise, never a strength`);\n }\n if (source.squeeze !== 0) {\n fail(name, `hand \"${source.id}\" reports squeeze ${source.squeeze}; a hand has no squeeze (its grab is its pinch through select), and a grasp value here never releases a grab`);\n }\n }\n }),\n hostCase(\"interactions/queries consider registered interactables only: scenery enclosing the hand is never answered\", ({ interactions, testHost }, name) => {\n // A host that answered proximity with any shown mesh found the floor's\n // bounds around every hand and no fingertip ever reached a target.\n if (typeof testHost.placeScenery !== \"function\") fail(name, \"the test host has no placeScenery, so the query scope cannot be proved\");\n testHost.clearTargets();\n try {\n testHost.placeScenery(\"rc-kit-floor\", [0, 0, -1], 3);\n testHost.placeScenery(\"rc-kit-wall\", [0, 1, -1.05], 0.02);\n if (interactions.hitProximity([0, 1, -1], 0.5) !== null) fail(name, \"with only scenery placed, hitProximity answered; queries consider registered interactables only\");\n if (interactions.hitRay({ origin: [0, 1, 0], direction: [0, 0, -1] }) !== null) fail(name, \"with only scenery placed, hitRay answered\");\n if (interactions.hitCone && interactions.hitCone({ origin: [0, 1, 0], direction: [0, 0, -1] }, (5 * Math.PI) / 180, 30) !== null) fail(name, \"with only scenery placed, hitCone answered\");\n testHost.placeTarget(\"rc-kit-target\", [0, 1, -1], 0.1);\n const near = interactions.hitProximity([0, 1, -0.87], 0.5);\n if (!near || near.targetId !== \"rc-kit-target\") fail(name, `a fingertip 3 cm from a target, inside the floor's bounds, must reach the target; got ${JSON.stringify(near)}`);\n if (Math.abs(near.distance - 0.03) > 0.001) fail(name, `the distance is to the target's surface, 0.03 within 1 mm, got ${near.distance}`);\n const along = interactions.hitRay({ origin: [0, 1, 0], direction: [0, 0, -1] });\n if (!along || along.targetId !== \"rc-kit-target\") fail(name, `a ray through scenery to a target must answer the target; got ${JSON.stringify(along)}`);\n } finally {\n testHost.clearTargets();\n }\n }),\n hostCase(\"interactions/a registered target with no radius is a 10 cm sphere\", ({ input, interactions, testHost }, name) => {\n testHost.clearTargets();\n const setup = new NativeInteractions({ input, interactions });\n try {\n // Placed small, then registered with no radius: the registration\n // must make it a 0.1 m sphere, so a point 9 cm from its centre is\n // inside it.\n testHost.placeTarget(\"rc-kit-bare\", [0, 1, -1], 0.02);\n setup.register({ id: \"rc-kit-bare\", behaviours: [] });\n const hit = interactions.hitProximity([0.09, 1, -1], 0.001);\n if (!hit || hit.targetId !== \"rc-kit-bare\") {\n fail(name, \"a point 9 cm from a bare target's centre did not reach it; the host is not using the 0.1 m radius\");\n }\n } finally {\n setup.dispose();\n testHost.clearTargets();\n }\n }),\n hostCase(\"interactions/endHold hands the host the release velocity\", ({ interactions, testHost }, name) => {\n testHost.clearTargets();\n try {\n testHost.placeTarget(\"rc-kit-throw\", [0, 1, -1], 0.1);\n const port = new NativeTransformPort(\"rc-kit-throw\", { interactions });\n const release: HoldRelease = { linearVelocity: [1, 2, 3], angularVelocity: [0, 0.5, 0] };\n port.beginHold();\n port.endHold(release);\n const received = testHost.lastRelease(\"rc-kit-throw\");\n if (JSON.stringify(received) !== JSON.stringify(release)) {\n fail(name, `the host received ${JSON.stringify(received)}, expected ${JSON.stringify(release)}`);\n }\n } finally {\n testHost.clearTargets();\n }\n }),\n hostCase(\"input/capabilities in a live session: rays, pokes and pose-only grabs\", ({ input }, name) => {\n const provider = new NativeInputProvider({ input });\n try {\n const caps = provider.getCapabilities();\n // Pose-only grabs follow from the same fact, so rays and pokes carry it.\n if (!caps.rays || !caps.pokes || caps.grabs !== \"poseOnly\") {\n fail(name, `a live session must report rays, pokes and pose-only grabs, got ${JSON.stringify(caps)}; run the kit inside a live session`);\n }\n } finally {\n provider.dispose();\n }\n }),\n hostCase(\"input/presence hides the models only, per side\", ({ input, testHost }, name) => {\n const provider = new NativeInputProvider({ input });\n try {\n if (!provider.getCapabilities().presence || !provider.setPresenceVisible) {\n fail(name, \"the host has no applyPresence, so presence cannot be shown or hidden\");\n }\n provider.setPresenceVisible(\"left\", false);\n const left = testHost.presenceShown(\"left\");\n if (!left || left.hand || left.controller) fail(name, `a hidden left side still draws ${JSON.stringify(left)}`);\n if (!exactlyOneFamily(testHost.presenceShown(\"right\"))) {\n fail(name, `the right side must still draw exactly one family, drew ${JSON.stringify(testHost.presenceShown(\"right\"))}`);\n }\n } finally {\n provider.setPresenceVisible?.(\"all\", true);\n provider.dispose();\n }\n }),\n hostCase('input/\"auto\" shows the family the live session uses', ({ input, testHost }, name) => {\n const provider = new NativeInputProvider({ input });\n try {\n provider.setPresenceModality?.(\"auto\");\n const handJoints = provider.getCapabilities().handJoints;\n for (const side of [\"left\", \"right\"] as const) {\n const shown = testHost.presenceShown(side);\n if (!shown || shown.hand !== handJoints || shown.controller !== !handJoints) {\n fail(name, `${side} draws ${JSON.stringify(shown)} with hand joints ${handJoints ? \"live\" : \"off\"}`);\n }\n }\n } finally {\n provider.dispose();\n }\n }),\n hostCase(\"input/eye gaze: a host that reports eye tracking poses the gaze, and the binding takes far targeting\", ({ input }, name) => {\n // Runs when the host reports eye tracking; a host without it (visionOS,\n // a Quest without eye tracking) passes without running. With it, the\n // host must hand the binding a valid pose in a live, focused session,\n // and the binding then samples one gaze snapshot with a ray. That the\n // hand and controller far rays drop is the shared contract case.\n if (!input.getFacts().eyeTracking) return;\n if (typeof input.getEyeGazePose !== \"function\") {\n fail(name, \"the host reports eyeTracking but has no getEyeGazePose(); the binding cannot use a fact it cannot pose\");\n }\n const provider = new NativeInputProvider({ input });\n try {\n if (!provider.getCapabilities().eyeGaze) {\n fail(name, \"the host reports eyeTracking in a live session, so capabilities.eyeGaze must be true; is the session focused?\");\n }\n const pose = input.getEyeGazePose();\n if (!pose || [...pose.position, ...pose.quaternion].some((n) => !Number.isFinite(n))) {\n fail(name, `getEyeGazePose() returned ${JSON.stringify(pose)}; run the kit while the user's eyes are tracked`);\n }\n const gaze = provider.sample().find((source) => source.kind === \"gaze\");\n if (!gaze?.ray) fail(name, \"with a valid gaze pose the binding must sample one gaze snapshot with a ray\");\n } finally {\n provider.dispose();\n }\n }),\n hostCase(\"input/eye gaze: a cone query never answers with a hidden target\", ({ interactions, testHost }, name) => {\n if (!interactions.hitCone) return;\n testHost.clearTargets();\n try {\n testHost.placeTarget(\"rc-kit-cone\", [0.2, 1, -2], 0.1);\n const ray = { origin: [0, 1, 0] as Vec3Tuple, direction: [0, 0, -1] as Vec3Tuple };\n const shown = interactions.hitCone(ray, (5 * Math.PI) / 180, 30);\n if (!shown || shown.targetId !== \"rc-kit-cone\") {\n fail(name, \"a target 0.2 m beside a ray 2 m out, inside a 5 degree cone, was not found\");\n }\n testHost.setTargetVisible?.(\"rc-kit-cone\", false);\n if (testHost.setTargetVisible && interactions.hitCone(ray, (5 * Math.PI) / 180, 30) !== null) {\n fail(name, \"the cone query answered with a hidden target\");\n }\n } finally {\n testHost.clearTargets();\n }\n }),\n hostCase(\"input/presence never hides the cursors\", ({ input, testHost }, name) => {\n const provider = new NativeInputProvider({ input });\n try {\n const before = testHost.cursors().length;\n provider.setPresenceVisible?.(\"all\", false);\n const after = testHost.cursors().length;\n if (after !== before) fail(name, `hiding presence changed the cursors drawn from ${before} to ${after}`);\n } finally {\n provider.setPresenceVisible?.(\"all\", true);\n provider.dispose();\n }\n }),\n ];\n}\n"]}
package/dist/host.d.ts CHANGED
@@ -14,7 +14,7 @@
14
14
  * app's own frame callback. Without it, call `update(dt)` yourself with
15
15
  * seconds.
16
16
  */
17
- import { InteractionRuntime, type DwellConfig, type InteractableDescriptor, type NearPointerOptions, type PhysicsBodySpec, type PhysicsShapeSpec } from "@realitycollective/webxr-interactions";
17
+ import { InteractionRuntime, type DwellConfig, type InteractableDescriptor, type NearPointerOptions, type PhysicsBodySpec, type PhysicsShapeSpec, type PointerArbiter, type PointerDisplay, type PointerDisplayConfig } from "@realitycollective/webxr-interactions";
18
18
  import { NativeHitTester, type NativeHitTesterOptions } from "./hit-tester.js";
19
19
  import { NativePhysicsFacility } from "./physics-facility.js";
20
20
  import { NativeInputProvider, type NativeInputProviderOptions } from "./provider.js";
@@ -24,6 +24,19 @@ export interface NativeInteractionsOptions extends NativeInputProviderOptions, N
24
24
  dwellDefaults?: DwellConfig;
25
25
  /** The near-pointer distances (touch hover, touch press, grab radius). Defaults are IWSDK 1.0.0's. */
26
26
  nearPointer?: Partial<NearPointerOptions>;
27
+ /**
28
+ * The pointer arbiter shared with the UI Extensions native host, so one
29
+ * decision per source covers panels and interactables (the runtime's
30
+ * `pointers` option). Omit when the app has no panels.
31
+ */
32
+ pointers?: PointerArbiter;
33
+ /**
34
+ * The app's pointer display settings (the runtime's `pointerDisplay`
35
+ * option): ray never, always or while hitting; cursors on objects and on
36
+ * panels; the ray and cursor look. Defaults are IWSDK 1.0.0's. The host is
37
+ * handed the resolved drawing every frame and the settings on every change.
38
+ */
39
+ pointerDisplay?: PointerDisplay | Partial<PointerDisplayConfig>;
27
40
  /**
28
41
  * The `physics` slice: the host's default engine (Jolt on Quest and
29
42
  * Android, RealityKit on visionOS) behind the core contract, or an app's
@@ -72,6 +85,7 @@ export declare class NativeInteractions {
72
85
  private readonly interactionsHost;
73
86
  private detachHost;
74
87
  private readonly unsubscribeVisuals;
88
+ private readonly unsubscribeDisplay;
75
89
  constructor(options?: NativeInteractionsOptions);
76
90
  /**
77
91
  * Register an interactable. The native app knows it by `descriptor.id`,
package/dist/host.js CHANGED
@@ -37,6 +37,7 @@ export class NativeInteractions {
37
37
  interactionsHost;
38
38
  detachHost = null;
39
39
  unsubscribeVisuals;
40
+ unsubscribeDisplay;
40
41
  constructor(options = {}) {
41
42
  this.options = options;
42
43
  this.interactionsHost = resolveHostSlice("interactions", options.interactions);
@@ -47,19 +48,34 @@ export class NativeInteractions {
47
48
  hitTester: this.hitTester,
48
49
  ...(options.dwellDefaults ? { dwellDefaults: options.dwellDefaults } : {}),
49
50
  ...(options.nearPointer ? { nearPointer: options.nearPointer } : {}),
51
+ ...(options.pointers ? { pointers: options.pointers } : {}),
52
+ ...(options.pointerDisplay ? { pointerDisplay: options.pointerDisplay } : {}),
50
53
  });
51
54
  const physicsHost = findHostSlice("physics", options.physics);
52
55
  this.physics = physicsHost ? new NativePhysicsFacility({ physics: physicsHost }) : null;
53
56
  this.stepPhysics = options.stepPhysics ?? true;
54
- // The host is handed the ray and cursor decision every frame, per source
55
- // (`NativeInputHost.applyPointerVisuals`); it draws exactly that.
57
+ // The host is handed the ray and cursor DRAWING every frame, per source
58
+ // (`NativeInputHost.applyPointerVisuals`): the arbiter's decision under
59
+ // the app's pointer display settings, fully resolved. It draws exactly that.
56
60
  const input = resolveHostSlice("input", options.input);
57
- this.unsubscribeVisuals = this.runtime.onPointerVisuals((visuals) => {
61
+ this.unsubscribeVisuals = this.runtime.onPointerDrawing((drawings) => {
58
62
  if (!input.applyPointerVisuals)
59
63
  return;
60
- for (const entry of visuals)
61
- input.applyPointerVisuals(entry.sourceId, { ...entry });
64
+ for (const drawing of drawings) {
65
+ const visuals = this.runtime.getPointerVisuals(drawing.sourceId);
66
+ input.applyPointerVisuals(drawing.sourceId, {
67
+ ...drawing,
68
+ rayColor: [drawing.rayColor[0], drawing.rayColor[1], drawing.rayColor[2]],
69
+ activePointer: visuals?.activePointer ?? null,
70
+ targetKind: visuals?.targetKind ?? null,
71
+ targetId: visuals?.targetId ?? null,
72
+ hitDistance: visuals?.hitDistance ?? null,
73
+ });
74
+ }
62
75
  });
76
+ const display = this.runtime.getPointerDisplay();
77
+ input.applyPointerDisplay?.({ ...display.get() });
78
+ this.unsubscribeDisplay = display.onChange((config) => input.applyPointerDisplay?.({ ...config }));
63
79
  if (options.attachToHost)
64
80
  this.attachToHost(options);
65
81
  }
@@ -107,6 +123,7 @@ export class NativeInteractions {
107
123
  this.detachHost?.();
108
124
  this.detachHost = null;
109
125
  this.unsubscribeVisuals();
126
+ this.unsubscribeDisplay();
110
127
  this.runtime.dispose();
111
128
  this.provider.dispose();
112
129
  }
package/dist/host.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"host.js","sourceRoot":"","sources":["../src/host.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * createNativeInteractions - one-call setup for the native adapter, with the\n * same shape as the Babylon, three.js, IWSDK and XR Blocks setups.\n *\n * ```ts\n * const interactions = createNativeInteractions({ attachToHost: true });\n * interactions.register({ id: \"button\", behaviours: [{ kind: \"press\" }] });\n * interactions.runtime.onEvent((event) => console.log(event.type));\n * ```\n *\n * The native app owns the scene, so an interactable is registered by id\n * alone: the app answers hit queries and pose reads for that id through the\n * `interactions` slice. With `attachToHost` the loop drives itself from the\n * app's own frame callback. Without it, call `update(dt)` yourself with\n * seconds.\n */\nimport {\n InteractionRuntime,\n type DwellConfig,\n type InteractableDescriptor,\n type NearPointerOptions,\n type PhysicsBodySpec,\n type PhysicsShapeSpec,\n} from \"@realitycollective/webxr-interactions\";\nimport { NativeHitTester, type NativeHitTesterOptions } from \"./hit-tester.js\";\nimport { NativePhysicsFacility } from \"./physics-facility.js\";\nimport { NativeInputProvider, type NativeInputProviderOptions } from \"./provider.js\";\nimport { NativeTransformPort } from \"./transform-port.js\";\nimport {\n findHostSlice,\n installedHost,\n resolveHostSlice,\n type NativeFrameSource,\n type NativeInteractionHost,\n type NativePhysicsHost,\n} from \"./native-types.js\";\n\nexport interface NativeInteractionsOptions\n extends NativeInputProviderOptions,\n NativeHitTesterOptions {\n dwellDefaults?: DwellConfig;\n /** The near-pointer distances (touch hover, touch press, grab radius). Defaults are IWSDK 1.0.0's. */\n nearPointer?: Partial<NearPointerOptions>;\n /**\n * The `physics` slice: the host's default engine (Jolt on Quest and\n * Android, RealityKit on visionOS) behind the core contract, or an app's\n * own `PhysicsFacility` in its place. Omit to read\n * `globalThis.__rcHost.physics`. Without either, `physics` is null and no\n * target can carry a body; the host conformance kit fails such a host.\n */\n physics?: NativePhysicsHost;\n /** Step the physics from `update(dt)`. Default true; false when the host steps its engine itself. */\n stepPhysics?: boolean;\n /**\n * Drive `update(dt)` from the native app's frame callback. Default false -\n * the app calls `update` from its own loop.\n */\n attachToHost?: boolean;\n /** Where frames come from for `attachToHost`. Omit to use `globalThis.__rcHost`. */\n frames?: NativeFrameSource;\n}\n\n/** Per-registration options, as IWSDK's `register` takes them. */\nexport interface NativeRegisterOptions {\n /**\n * The radius, in metres, the host hit-tests this target with. Default\n * 0.1, the radius IWSDK gives a target that declares none.\n */\n targetRadius?: number;\n /**\n * Give the target a body in the `physics` slice at its current world pose\n * (core defaults: dynamic, no damping, gravity factor 1). A target whose\n * body the host already holds (`physics.hasBody(id)`) needs neither this\n * nor `shape`: its port is bound to that body as it is.\n */\n body?: PhysicsBodySpec;\n /** The body's collider (default `\"auto\"`, the host's collider for the object). Implies `body`. */\n shape?: PhysicsShapeSpec;\n}\n\n/** The radius a target is hit-tested with when it declares none: IWSDK's `targetRadius ?? 0.1`. */\nconst DEFAULT_TARGET_RADIUS = 0.1;\n\n/** Longest frame the host-attached loop will report, in seconds. */\nconst MAX_FRAME_SECONDS = 0.1;\n\nexport class NativeInteractions {\n readonly runtime: InteractionRuntime;\n readonly provider: NativeInputProvider;\n readonly hitTester: NativeHitTester;\n /** The platform's physics over the `physics` slice, or null when the host installs none. */\n readonly physics: NativePhysicsFacility | null;\n private readonly stepPhysics: boolean;\n private readonly ports = new Map<string, NativeTransformPort>();\n private readonly ownedBodies = new Set<string>();\n private readonly options: NativeInteractionsOptions;\n private readonly interactionsHost: NativeInteractionHost;\n private detachHost: (() => void) | null = null;\n private readonly unsubscribeVisuals: () => void;\n\n constructor(options: NativeInteractionsOptions = {}) {\n this.options = options;\n this.interactionsHost = resolveHostSlice(\"interactions\", options.interactions);\n this.provider = new NativeInputProvider(options);\n this.hitTester = new NativeHitTester(options);\n this.runtime = new InteractionRuntime({\n provider: this.provider,\n hitTester: this.hitTester,\n ...(options.dwellDefaults ? { dwellDefaults: options.dwellDefaults } : {}),\n ...(options.nearPointer ? { nearPointer: options.nearPointer } : {}),\n });\n const physicsHost = findHostSlice(\"physics\", options.physics);\n this.physics = physicsHost ? new NativePhysicsFacility({ physics: physicsHost }) : null;\n this.stepPhysics = options.stepPhysics ?? true;\n // The host is handed the ray and cursor decision every frame, per source\n // (`NativeInputHost.applyPointerVisuals`); it draws exactly that.\n const input = resolveHostSlice(\"input\", options.input);\n this.unsubscribeVisuals = this.runtime.onPointerVisuals((visuals) => {\n if (!input.applyPointerVisuals) return;\n for (const entry of visuals) input.applyPointerVisuals(entry.sourceId, { ...entry });\n });\n if (options.attachToHost) this.attachToHost(options);\n }\n\n /**\n * Register an interactable. The native app knows it by `descriptor.id`,\n * and is told the radius to hit-test it with: `options.targetRadius`, or\n * 0.1 m, so a bare target is a 10 cm sphere as on IWSDK.\n */\n register(descriptor: InteractableDescriptor, options: NativeRegisterOptions = {}): NativeTransformPort {\n const id = descriptor.id;\n if (options.body || options.shape) {\n if (!this.physics) {\n throw new Error(\n `@realitycollective/native-interactions: \"${id}\" asks for a physics body but no physics slice is installed; the host installs globalThis.__rcHost.physics, or pass \\`physics\\` to createNativeInteractions`,\n );\n }\n this.physics.addBody(id, this.interactionsHost.getWorldPose(id), options.body, options.shape);\n this.ownedBodies.add(id);\n }\n const bound = this.physics && this.physics.hasBody(id) ? this.physics : null;\n const port = new NativeTransformPort(id, {\n ...(this.options.interactions ? { interactions: this.options.interactions } : {}),\n ...(bound ? { physics: { facility: bound, bodyId: id } } : {}),\n });\n this.interactionsHost.setTargetRadius(descriptor.id, options.targetRadius ?? DEFAULT_TARGET_RADIUS);\n this.ports.set(descriptor.id, port);\n this.runtime.registerInteractable(descriptor, { transform: port });\n return port;\n }\n\n unregister(id: string): void {\n this.runtime.unregisterInteractable(id);\n this.ports.delete(id);\n if (this.ownedBodies.delete(id)) this.physics?.removeBody(id);\n }\n\n getPort(id: string): NativeTransformPort | undefined {\n return this.ports.get(id);\n }\n\n update(dt: number): void {\n // The provider's eye-gaze filter and grace integrate over the same step.\n this.provider.setFrameDelta(dt);\n this.runtime.update(dt);\n if (this.stepPhysics) this.physics?.step(dt);\n }\n\n dispose(): void {\n this.detachHost?.();\n this.detachHost = null;\n this.unsubscribeVisuals();\n this.runtime.dispose();\n this.provider.dispose();\n }\n\n private attachToHost(options: NativeInteractionsOptions): void {\n const frames = options.frames ?? installedHost();\n if (!frames?.onFrame) {\n throw new Error(\n \"@realitycollective/native-interactions: attachToHost needs a frame source, and globalThis.__rcHost.onFrame is not installed. Pass `frames`, or call update(dt) yourself.\",\n );\n }\n // The app reports the frame delta in seconds. A long frame is clamped so\n // behaviours integrate over a sane step rather than jumping.\n this.detachHost = frames.onFrame((_timestampMs, deltaS) => {\n this.update(Math.min(MAX_FRAME_SECONDS, Math.max(0, deltaS)));\n });\n }\n}\n\nexport function createNativeInteractions(\n options: NativeInteractionsOptions = {},\n): NativeInteractions {\n return new NativeInteractions(options);\n}\n"]}
1
+ {"version":3,"file":"host.js","sourceRoot":"","sources":["../src/host.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * createNativeInteractions - one-call setup for the native adapter, with the\n * same shape as the Babylon, three.js, IWSDK and XR Blocks setups.\n *\n * ```ts\n * const interactions = createNativeInteractions({ attachToHost: true });\n * interactions.register({ id: \"button\", behaviours: [{ kind: \"press\" }] });\n * interactions.runtime.onEvent((event) => console.log(event.type));\n * ```\n *\n * The native app owns the scene, so an interactable is registered by id\n * alone: the app answers hit queries and pose reads for that id through the\n * `interactions` slice. With `attachToHost` the loop drives itself from the\n * app's own frame callback. Without it, call `update(dt)` yourself with\n * seconds.\n */\nimport {\n InteractionRuntime,\n type DwellConfig,\n type InteractableDescriptor,\n type NearPointerOptions,\n type PhysicsBodySpec,\n type PhysicsShapeSpec,\n type PointerArbiter,\n type PointerDisplay,\n type PointerDisplayConfig,\n} from \"@realitycollective/webxr-interactions\";\nimport { NativeHitTester, type NativeHitTesterOptions } from \"./hit-tester.js\";\nimport { NativePhysicsFacility } from \"./physics-facility.js\";\nimport { NativeInputProvider, type NativeInputProviderOptions } from \"./provider.js\";\nimport { NativeTransformPort } from \"./transform-port.js\";\nimport {\n findHostSlice,\n installedHost,\n resolveHostSlice,\n type NativeFrameSource,\n type NativeInteractionHost,\n type NativePhysicsHost,\n} from \"./native-types.js\";\n\nexport interface NativeInteractionsOptions\n extends NativeInputProviderOptions,\n NativeHitTesterOptions {\n dwellDefaults?: DwellConfig;\n /** The near-pointer distances (touch hover, touch press, grab radius). Defaults are IWSDK 1.0.0's. */\n nearPointer?: Partial<NearPointerOptions>;\n /**\n * The pointer arbiter shared with the UI Extensions native host, so one\n * decision per source covers panels and interactables (the runtime's\n * `pointers` option). Omit when the app has no panels.\n */\n pointers?: PointerArbiter;\n /**\n * The app's pointer display settings (the runtime's `pointerDisplay`\n * option): ray never, always or while hitting; cursors on objects and on\n * panels; the ray and cursor look. Defaults are IWSDK 1.0.0's. The host is\n * handed the resolved drawing every frame and the settings on every change.\n */\n pointerDisplay?: PointerDisplay | Partial<PointerDisplayConfig>;\n /**\n * The `physics` slice: the host's default engine (Jolt on Quest and\n * Android, RealityKit on visionOS) behind the core contract, or an app's\n * own `PhysicsFacility` in its place. Omit to read\n * `globalThis.__rcHost.physics`. Without either, `physics` is null and no\n * target can carry a body; the host conformance kit fails such a host.\n */\n physics?: NativePhysicsHost;\n /** Step the physics from `update(dt)`. Default true; false when the host steps its engine itself. */\n stepPhysics?: boolean;\n /**\n * Drive `update(dt)` from the native app's frame callback. Default false -\n * the app calls `update` from its own loop.\n */\n attachToHost?: boolean;\n /** Where frames come from for `attachToHost`. Omit to use `globalThis.__rcHost`. */\n frames?: NativeFrameSource;\n}\n\n/** Per-registration options, as IWSDK's `register` takes them. */\nexport interface NativeRegisterOptions {\n /**\n * The radius, in metres, the host hit-tests this target with. Default\n * 0.1, the radius IWSDK gives a target that declares none.\n */\n targetRadius?: number;\n /**\n * Give the target a body in the `physics` slice at its current world pose\n * (core defaults: dynamic, no damping, gravity factor 1). A target whose\n * body the host already holds (`physics.hasBody(id)`) needs neither this\n * nor `shape`: its port is bound to that body as it is.\n */\n body?: PhysicsBodySpec;\n /** The body's collider (default `\"auto\"`, the host's collider for the object). Implies `body`. */\n shape?: PhysicsShapeSpec;\n}\n\n/** The radius a target is hit-tested with when it declares none: IWSDK's `targetRadius ?? 0.1`. */\nconst DEFAULT_TARGET_RADIUS = 0.1;\n\n/** Longest frame the host-attached loop will report, in seconds. */\nconst MAX_FRAME_SECONDS = 0.1;\n\nexport class NativeInteractions {\n readonly runtime: InteractionRuntime;\n readonly provider: NativeInputProvider;\n readonly hitTester: NativeHitTester;\n /** The platform's physics over the `physics` slice, or null when the host installs none. */\n readonly physics: NativePhysicsFacility | null;\n private readonly stepPhysics: boolean;\n private readonly ports = new Map<string, NativeTransformPort>();\n private readonly ownedBodies = new Set<string>();\n private readonly options: NativeInteractionsOptions;\n private readonly interactionsHost: NativeInteractionHost;\n private detachHost: (() => void) | null = null;\n private readonly unsubscribeVisuals: () => void;\n private readonly unsubscribeDisplay: () => void;\n\n constructor(options: NativeInteractionsOptions = {}) {\n this.options = options;\n this.interactionsHost = resolveHostSlice(\"interactions\", options.interactions);\n this.provider = new NativeInputProvider(options);\n this.hitTester = new NativeHitTester(options);\n this.runtime = new InteractionRuntime({\n provider: this.provider,\n hitTester: this.hitTester,\n ...(options.dwellDefaults ? { dwellDefaults: options.dwellDefaults } : {}),\n ...(options.nearPointer ? { nearPointer: options.nearPointer } : {}),\n ...(options.pointers ? { pointers: options.pointers } : {}),\n ...(options.pointerDisplay ? { pointerDisplay: options.pointerDisplay } : {}),\n });\n const physicsHost = findHostSlice(\"physics\", options.physics);\n this.physics = physicsHost ? new NativePhysicsFacility({ physics: physicsHost }) : null;\n this.stepPhysics = options.stepPhysics ?? true;\n // The host is handed the ray and cursor DRAWING every frame, per source\n // (`NativeInputHost.applyPointerVisuals`): the arbiter's decision under\n // the app's pointer display settings, fully resolved. It draws exactly that.\n const input = resolveHostSlice(\"input\", options.input);\n this.unsubscribeVisuals = this.runtime.onPointerDrawing((drawings) => {\n if (!input.applyPointerVisuals) return;\n for (const drawing of drawings) {\n const visuals = this.runtime.getPointerVisuals(drawing.sourceId);\n input.applyPointerVisuals(drawing.sourceId, {\n ...drawing,\n rayColor: [drawing.rayColor[0], drawing.rayColor[1], drawing.rayColor[2]],\n activePointer: visuals?.activePointer ?? null,\n targetKind: visuals?.targetKind ?? null,\n targetId: visuals?.targetId ?? null,\n hitDistance: visuals?.hitDistance ?? null,\n });\n }\n });\n const display = this.runtime.getPointerDisplay();\n input.applyPointerDisplay?.({ ...display.get() });\n this.unsubscribeDisplay = display.onChange((config) => input.applyPointerDisplay?.({ ...config }));\n if (options.attachToHost) this.attachToHost(options);\n }\n\n /**\n * Register an interactable. The native app knows it by `descriptor.id`,\n * and is told the radius to hit-test it with: `options.targetRadius`, or\n * 0.1 m, so a bare target is a 10 cm sphere as on IWSDK.\n */\n register(descriptor: InteractableDescriptor, options: NativeRegisterOptions = {}): NativeTransformPort {\n const id = descriptor.id;\n if (options.body || options.shape) {\n if (!this.physics) {\n throw new Error(\n `@realitycollective/native-interactions: \"${id}\" asks for a physics body but no physics slice is installed; the host installs globalThis.__rcHost.physics, or pass \\`physics\\` to createNativeInteractions`,\n );\n }\n this.physics.addBody(id, this.interactionsHost.getWorldPose(id), options.body, options.shape);\n this.ownedBodies.add(id);\n }\n const bound = this.physics && this.physics.hasBody(id) ? this.physics : null;\n const port = new NativeTransformPort(id, {\n ...(this.options.interactions ? { interactions: this.options.interactions } : {}),\n ...(bound ? { physics: { facility: bound, bodyId: id } } : {}),\n });\n this.interactionsHost.setTargetRadius(descriptor.id, options.targetRadius ?? DEFAULT_TARGET_RADIUS);\n this.ports.set(descriptor.id, port);\n this.runtime.registerInteractable(descriptor, { transform: port });\n return port;\n }\n\n unregister(id: string): void {\n this.runtime.unregisterInteractable(id);\n this.ports.delete(id);\n if (this.ownedBodies.delete(id)) this.physics?.removeBody(id);\n }\n\n getPort(id: string): NativeTransformPort | undefined {\n return this.ports.get(id);\n }\n\n update(dt: number): void {\n // The provider's eye-gaze filter and grace integrate over the same step.\n this.provider.setFrameDelta(dt);\n this.runtime.update(dt);\n if (this.stepPhysics) this.physics?.step(dt);\n }\n\n dispose(): void {\n this.detachHost?.();\n this.detachHost = null;\n this.unsubscribeVisuals();\n this.unsubscribeDisplay();\n this.runtime.dispose();\n this.provider.dispose();\n }\n\n private attachToHost(options: NativeInteractionsOptions): void {\n const frames = options.frames ?? installedHost();\n if (!frames?.onFrame) {\n throw new Error(\n \"@realitycollective/native-interactions: attachToHost needs a frame source, and globalThis.__rcHost.onFrame is not installed. Pass `frames`, or call update(dt) yourself.\",\n );\n }\n // The app reports the frame delta in seconds. A long frame is clamped so\n // behaviours integrate over a sane step rather than jumping.\n this.detachHost = frames.onFrame((_timestampMs, deltaS) => {\n this.update(Math.min(MAX_FRAME_SECONDS, Math.max(0, deltaS)));\n });\n }\n}\n\nexport function createNativeInteractions(\n options: NativeInteractionsOptions = {},\n): NativeInteractions {\n return new NativeInteractions(options);\n}\n"]}
package/dist/index.d.ts CHANGED
@@ -9,7 +9,7 @@
9
9
  * internal.
10
10
  */
11
11
  export * from "@realitycollective/webxr-interactions";
12
- export type { NativeFrameSource, NativeHit, NativeHostSlices, NativeInputFacts, NativeInputHost, NativeInteractionHost, NativeInteractionsTestHost, NativePhysicsHost, NativePresenceShown, } from "./native-types.js";
12
+ export type { NativeFrameSource, NativeHit, NativeHostSlices, NativeInputFacts, NativeInputHost, NativeInteractionHost, NativeInteractionsTestHost, NativePhysicsHost, NativePointerVisuals, NativePresenceShown, } from "./native-types.js";
13
13
  export * from "./provider.js";
14
14
  export * from "./hit-tester.js";
15
15
  export * from "./transform-port.js";
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,cAAc,uCAAuC,CAAC;AAYtD,cAAc,eAAe,CAAC;AAC9B,cAAc,iBAAiB,CAAC;AAChC,cAAc,qBAAqB,CAAC;AACpC,cAAc,uBAAuB,CAAC;AACtC,cAAc,WAAW,CAAC;AAC1B,OAAO,EAAE,sCAAsC,EAAE,MAAM,kBAAkB,CAAC","sourcesContent":["/**\n * Public entry point of the native adapter: the same kinds of export as the\n * Babylon, three.js, IWSDK and XR Blocks adapters. The core is re-exported;\n * the provider, hit tester and transform port implement its contracts; the\n * setup entry point wires them together; and the slice types describe what\n * the native app installs on `globalThis.__rcHost`. One addition the web\n * adapters do not need: the host conformance kit a native app runs on its\n * device against its real slices. Slice reading and tuple copying stay\n * internal.\n */\nexport * from \"@realitycollective/webxr-interactions\";\nexport type {\n NativeFrameSource,\n NativeHit,\n NativeHostSlices,\n NativeInputFacts,\n NativeInputHost,\n NativeInteractionHost,\n NativeInteractionsTestHost,\n NativePhysicsHost,\n NativePresenceShown,\n} from \"./native-types.js\";\nexport * from \"./provider.js\";\nexport * from \"./hit-tester.js\";\nexport * from \"./transform-port.js\";\nexport * from \"./physics-facility.js\";\nexport * from \"./host.js\";\nexport { nativeInteractionsHostConformanceCases } from \"./conformance.js\";\nexport type {\n NativeInteractionsHostConformanceCase,\n NativeInteractionsHostConformanceSetup,\n} from \"./conformance.js\";\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,cAAc,uCAAuC,CAAC;AAatD,cAAc,eAAe,CAAC;AAC9B,cAAc,iBAAiB,CAAC;AAChC,cAAc,qBAAqB,CAAC;AACpC,cAAc,uBAAuB,CAAC;AACtC,cAAc,WAAW,CAAC;AAC1B,OAAO,EAAE,sCAAsC,EAAE,MAAM,kBAAkB,CAAC","sourcesContent":["/**\n * Public entry point of the native adapter: the same kinds of export as the\n * Babylon, three.js, IWSDK and XR Blocks adapters. The core is re-exported;\n * the provider, hit tester and transform port implement its contracts; the\n * setup entry point wires them together; and the slice types describe what\n * the native app installs on `globalThis.__rcHost`. One addition the web\n * adapters do not need: the host conformance kit a native app runs on its\n * device against its real slices. Slice reading and tuple copying stay\n * internal.\n */\nexport * from \"@realitycollective/webxr-interactions\";\nexport type {\n NativeFrameSource,\n NativeHit,\n NativeHostSlices,\n NativeInputFacts,\n NativeInputHost,\n NativeInteractionHost,\n NativeInteractionsTestHost,\n NativePhysicsHost,\n NativePointerVisuals,\n NativePresenceShown,\n} from \"./native-types.js\";\nexport * from \"./provider.js\";\nexport * from \"./hit-tester.js\";\nexport * from \"./transform-port.js\";\nexport * from \"./physics-facility.js\";\nexport * from \"./host.js\";\nexport { nativeInteractionsHostConformanceCases } from \"./conformance.js\";\nexport type {\n NativeInteractionsHostConformanceCase,\n NativeInteractionsHostConformanceSetup,\n} from \"./conformance.js\";\n"]}
@@ -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, PhysicsBodySpec, PhysicsBodyState, PhysicsShapeSpec, PhysicsVelocity, PointerVisuals } 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`
@@ -86,8 +108,34 @@ export interface NativeInputHost {
86
108
  /**
87
109
  * This frame's tracked sources, in the `InputSourceSnapshot` shape. `kind`
88
110
  * is `"hand"` while hand joints are tracked, else `"controller"`.
89
- * `select` is the trigger value, or 1 while the runtime reports selecting
90
- * (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
+ *
91
139
  * `gripPose` is the WebXR GRIP frame, not a hand joint - see
92
140
  * `InputSourceSnapshot.gripPose`. `indexTip` is the index fingertip for a
93
141
  * hand and the ray origin for a controller. `hapticsAvailable` is true when
@@ -129,20 +177,35 @@ export interface NativeInputHost {
129
177
  applyPresence?(side: "left" | "right", shown: NativePresenceShown): void;
130
178
  /**
131
179
  * Draw, or stop drawing, one source's ray and cursor, exactly as told.
132
- * Decided here by the core near-pointer rule (`near-pointer.ts` in
133
- * `@realitycollective/webxr-interactions`, IWSDK's `MultiPointer`): the
134
- * ray shows only while the ray owns the source or no pointer does, never
135
- * while the fingertip or grip owns it, and never for a source with no ray;
136
- * the cursor disc shows exactly while the active pointer has a hit, at
137
- * `cursorPoint` (world metres), the ray's hit or the surface point under
138
- * the fingertip or grip. A host draws nothing for a source it was not told
139
- * about, and never draws "a cursor at every ray hit" on its own, which is
140
- * what this contract said before 28 September 2026 and what put a cursor
141
- * on a touched object. Called every frame for every sampled source, with
142
- * fresh objects the host may keep. IWSDK: `RayPointer.update` with
143
- * `forceHideRay` and `CursorVisual.setVisible`.
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.
144
207
  */
145
- applyPointerVisuals?(sourceId: string, visuals: PointerVisuals): void;
208
+ applyPointerDisplay?(config: PointerDisplayConfig): void;
146
209
  }
147
210
  /** What the host's ray or proximity query reports: the target it reached, if any. */
148
211
  export interface NativeHit {
@@ -150,13 +213,29 @@ export interface NativeHit {
150
213
  targetId: string;
151
214
  /** `hitRay`: the ray parameter t, metres. `hitProximity`: metres to the target's SURFACE, never negative. */
152
215
  distance: number;
153
- /** 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
+ */
154
223
  point: Vec3Tuple;
155
224
  }
156
225
  /**
157
226
  * The native host's `interactions` slice: `HitTester` with its semantics
158
227
  * stated, and `TransformPort` with every member keyed by the target id the
159
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.
160
239
  */
161
240
  export interface NativeInteractionHost {
162
241
  /**
@@ -325,7 +404,13 @@ export interface NativePhysicsHost {
325
404
  export interface NativeInteractionsTestHost {
326
405
  /** Put a shown, hit-testable target of `radius` metres at `position`, as the app's scene would. */
327
406
  placeTarget(targetId: string, position: Vec3Tuple, radius: number): void;
328
- /** 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. */
329
414
  clearTargets(): void;
330
415
  /** What the host draws for one side now. */
331
416
  presenceShown(side: "left" | "right"): NativePresenceShown | undefined;
@@ -334,7 +419,9 @@ export interface NativeInteractionsTestHost {
334
419
  /** Every cursor disc the host draws now, as world positions. */
335
420
  cursors(): Vec3Tuple[];
336
421
  /** What the host draws for one source now, as last told through `applyPointerVisuals`; undefined for a source never told. */
337
- pointerVisuals?(sourceId: string): PointerVisuals | undefined;
422
+ pointerVisuals?(sourceId: string): NativePointerVisuals | undefined;
423
+ /** The pointer display settings the host last received through `applyPointerDisplay`, or undefined. Optional. */
424
+ pointerDisplay?(): PointerDisplayConfig | undefined;
338
425
  /** Hide or show a placed target with the host's ordinary visibility flag, for the hidden-target cone case. Optional. */
339
426
  setTargetVisible?(targetId: string, visible: boolean): void;
340
427
  }
@@ -1 +1 @@
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 HeadPose,\n InputHitHint,\n InputSourceSnapshot,\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 PointerVisuals,\n} 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 * 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 * `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 * 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 by the core near-pointer rule (`near-pointer.ts` in\n * `@realitycollective/webxr-interactions`, IWSDK's `MultiPointer`): the\n * ray shows only while the ray owns the source or no pointer does, never\n * while the fingertip or grip owns it, and never for a source with no ray;\n * the cursor disc shows exactly while the active pointer has a hit, at\n * `cursorPoint` (world metres), the ray's hit or the surface point under\n * the fingertip or grip. A host draws nothing for a source it was not told\n * about, and never draws \"a cursor at every ray hit\" on its own, which is\n * what this contract said before 28 September 2026 and what put a cursor\n * on a touched object. Called every frame for every sampled source, with\n * fresh objects the host may keep. IWSDK: `RayPointer.update` with\n * `forceHideRay` and `CursorVisual.setVisible`.\n */\n applyPointerVisuals?(sourceId: string, visuals: PointerVisuals): 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 * 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 /** 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 /** What the host draws for one source now, as last told through `applyPointerVisuals`; undefined for a source never told. */\n pointerVisuals?(sourceId: string): PointerVisuals | 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"]}
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"]}
@@ -23,6 +23,10 @@
23
23
  * selects, and the pinching hand's ray pose travels as `selectorPose`. A
24
24
  * host reports only `eyeTracking` and `getEyeGazePose()`.
25
25
  * - A pulse's intensity is clamped to 0..1 before it reaches the host.
26
+ * - A hand's `squeeze` is 0 whatever the host reports (`NativeInputHost.sample`):
27
+ * a hand has no squeeze on the web, its grab is its pinch through
28
+ * `select`, and a grasp value left in `squeeze` held grabs open on a
29
+ * relaxed hand (Pale Signal handover, G1).
26
30
  *
27
31
  * Every snapshot, and every tuple inside it, is copied before it leaves this
28
32
  * class, so a host that reuses its own sample buffers still meets the
package/dist/provider.js CHANGED
@@ -23,6 +23,10 @@
23
23
  * selects, and the pinching hand's ray pose travels as `selectorPose`. A
24
24
  * host reports only `eyeTracking` and `getEyeGazePose()`.
25
25
  * - A pulse's intensity is clamped to 0..1 before it reaches the host.
26
+ * - A hand's `squeeze` is 0 whatever the host reports (`NativeInputHost.sample`):
27
+ * a hand has no squeeze on the web, its grab is its pinch through
28
+ * `select`, and a grasp value left in `squeeze` held grabs open on a
29
+ * relaxed hand (Pale Signal handover, G1).
26
30
  *
27
31
  * Every snapshot, and every tuple inside it, is copied before it leaves this
28
32
  * class, so a host that reuses its own sample buffers still meets the
@@ -110,7 +114,12 @@ export class NativeInputProvider {
110
114
  this.eyeGaze.reset();
111
115
  return [];
112
116
  }
113
- const sources = this.host.sample().map(copySnapshot);
117
+ const sources = this.host.sample().map((source) => {
118
+ const copy = copySnapshot(source);
119
+ if (copy.kind === "hand")
120
+ copy.squeeze = 0;
121
+ return copy;
122
+ });
114
123
  if (!this.capabilities.eyeGaze)
115
124
  return sources;
116
125
  return this.eyeGaze.update(this.eyeGazeFrame(sources), sources, this.frameSeconds);
@@ -1 +1 @@
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"]}
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 * - A hand's `squeeze` is 0 whatever the host reports (`NativeInputHost.sample`):\n * a hand has no squeeze on the web, its grab is its pinch through\n * `select`, and a grasp value left in `squeeze` held grabs open on a\n * relaxed hand (Pale Signal handover, G1).\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((source) => {\n const copy = copySnapshot(source);\n if (copy.kind === \"hand\") copy.squeeze = 0;\n return copy;\n });\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"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@realitycollective/native-interactions",
3
- "version": "0.1.1-preview.3",
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.",
3
+ "version": "0.1.2-preview.0",
4
+ "description": "Native host adapter for the Reality Collective Interaction Extensions - reads the `input`, `interactions` and `physics` slices a native app (OpenXR, visionOS) installs on `globalThis.__rcHost`, or an injected fake, maps them into the engine-free @realitycollective/webxr-interactions core, hands the host the resolved pointer drawing, and ships the host conformance kit a native app runs on its device. No engine dependency: assets and rendering stay in the native app. Re-exports the core.",
5
5
  "keywords": [
6
6
  "realitycollective",
7
7
  "native",
@@ -11,7 +11,11 @@
11
11
  "xr",
12
12
  "interaction",
13
13
  "input",
14
- "typescript"
14
+ "typescript",
15
+ "physics",
16
+ "pointer",
17
+ "conformance",
18
+ "hermes"
15
19
  ],
16
20
  "license": "MIT",
17
21
  "author": "Reality Collective",
@@ -34,8 +38,8 @@
34
38
  "typecheck": "tsc -p tsconfig.json --noEmit"
35
39
  },
36
40
  "dependencies": {
37
- "@realitycollective/webxr-input": "^0.1.7",
38
- "@realitycollective/webxr-interactions": "^0.1.1-preview.3"
41
+ "@realitycollective/webxr-input": "^0.1.8",
42
+ "@realitycollective/webxr-interactions": "^0.1.2-preview.0"
39
43
  },
40
44
  "repository": {
41
45
  "type": "git",