@realitycollective/native-interactions 0.1.1-preview.2 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -2
- package/README.md +15 -1
- package/dist/conformance.d.ts +4 -2
- package/dist/conformance.js +267 -1
- package/dist/conformance.js.map +1 -1
- package/dist/hit-tester.d.ts +7 -5
- package/dist/hit-tester.js +8 -0
- package/dist/hit-tester.js.map +1 -1
- package/dist/host.d.ts +43 -2
- package/dist/host.js +57 -2
- package/dist/host.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/native-types.d.ts +253 -22
- package/dist/native-types.js +7 -1
- package/dist/native-types.js.map +1 -1
- package/dist/physics-facility.d.ts +41 -0
- package/dist/physics-facility.js +63 -0
- package/dist/physics-facility.js.map +1 -0
- package/dist/provider.d.ts +35 -2
- package/dist/provider.js +57 -5
- package/dist/provider.js.map +1 -1
- package/dist/transform-port.d.ts +15 -4
- package/dist/transform-port.js +16 -1
- package/dist/transform-port.js.map +1 -1
- package/package.json +9 -5
package/CHANGELOG.md
CHANGED
|
@@ -4,10 +4,31 @@ 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.1] - 2026-10-01
|
|
8
8
|
|
|
9
9
|
### Added
|
|
10
10
|
|
|
11
|
+
- 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.
|
|
12
|
+
- `@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.
|
|
13
|
+
- `@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.
|
|
14
|
+
- `@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.
|
|
15
|
+
- `@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.
|
|
16
|
+
- `@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 })`.
|
|
17
|
+
- `@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 })`.
|
|
18
|
+
- `@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).
|
|
19
|
+
- 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.
|
|
20
|
+
- `@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 })`.
|
|
21
|
+
- `@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.
|
|
22
|
+
- `@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.
|
|
23
|
+
- `@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.
|
|
24
|
+
- `@realitycollective/iwsdk-interactions` - `test/near-pointer-reference.test.ts` reads the touch and grab constants off IWSDK 1.0.0's own pointer classes and drives the core touch machine beside `@pmndrs/pointer-events`' press rule, recording the one frame they differ on by design (a fingertip first seen inside the press band).
|
|
25
|
+
- `@realitycollective/xrblocks-interactions` - the provider reads the XR Blocks camera's head pose through real three.js scratch objects; a plain object handed to `getWorldPosition` threw against the real camera.
|
|
26
|
+
- Eye gaze and pinch, on every platform, following Meta IWSDK 1.0.0's gaze-and-pinch interaction (the primary input on Meta VR Glasses). The provider half of the rule is `@realitycollective/webxr-input` 0.1.7's `EyeGazeInput`; this repository applies it in every provider and adds the targeting half to the core. `@realitycollective/webxr-interactions` - `HitTester` gains an optional `hitCone(ray, halfAngle, maxLength)`, IWSDK's `GazeConecaster` query (a target the ray reaches wins outright, otherwise the nearest to the axis within the cone, the nearer on a tie within half a degree), with `coneHitForSpheres` for sphere-based testers and `GazeConsensus`, the time-weighted dwell vote over IWSDK's 0.15 s window. `InteractionRuntime` takes `eyeGaze` options (`EYE_GAZE_DEFAULTS`: 5 degree cone, 30 m, 0.15 s, suppression on, follow the hand on) and, when the provider reports `capabilities.eyeGaze`, targets the gaze source through the cone and the consensus, targets nothing while a hand's near pointer is hovering or grabbing, satisfies `gaze.required` and dwell from the eye gaze, presses or grabs the gazed target when the owning hand pinches, and from the next frame drives the gaze source's ray from the pinching hand aimed at the gaze hit (`selectorPose`), so `onSample` listeners such as the pointer bridge and every behaviour see a drag that follows the hand. `hitTesterContractCases()` gains five `hitCone` cases that run when a tester has one. Without `eyeGaze` nothing changes: a gaze-kind source still targets by plain ray as head gaze does.
|
|
27
|
+
- `@realitycollective/iwsdk-interactions` - `IWSDKInputProvider` reports `eyeGaze` when the session carries a gaze input source, reads IWSDK's own sampled eye pose (`xrOrigin.eyeSpace` while `gazeOrigin` is `"tracked"`) and applies `EyeGazeInput`, so it hands far targeting to gaze exactly when IWSDK's `GazePointer` does; `EntityHitTester` answers `hitCone` over its spheres. `test/gaze-reference.test.ts` drives IWSDK's `FilteredEyeGaze` and `GazeConecaster` side by side with the core's filter and consensus.
|
|
28
|
+
- `@realitycollective/threejs-interactions` (reused by `@realitycollective/xrblocks-interactions`) - `WebXRInputProvider` finds the `targetRayMode === "gaze"` source, poses it against the reference space and applies `EyeGazeInput`; `ThreeHitTester.hitCone` tries the raycast first and then the cone over the registered roots' bounding spheres.
|
|
29
|
+
- `@realitycollective/babylon-interactions` - `BabylonInputProvider` reads the `xr-eye-tracking` feature (`WebXREyeTracking.getEyeGaze()`) when the app enabled it and applies `EyeGazeInput`; `BabylonHitTester.hitCone` runs the cone over its spheres.
|
|
30
|
+
- `@realitycollective/native-interactions` - `NativeInputFacts.eyeTracking` (an eye-gaze source is present: OpenXR `XR_EXT_eye_gaze_interaction` bound and active) and `NativeInputHost.getEyeGazePose()` (this frame's gaze target-ray pose, world space, or null) let a native host report the facts while the binding applies the rule: `eyeGaze` is derived, far rays are dropped, the pinch selects, and the selector pose is built from the hand's ray. The `interactions` slice gains an optional `hitCone` the host answers over its own scene, forwarded by `NativeHitTester`. The host conformance kit gains `input/eye gaze` cases: a host that reports eye tracking must pose the gaze, and a hidden target never answers a cone query.
|
|
31
|
+
|
|
11
32
|
- `@realitycollective/webxr-interactions` - three `hitTesterContractCases()` that hold every platform to IWSDK's hit-test semantics: `hitProximity` reports the distance to the target's SURFACE, clamped at zero (a point 3 cm outside a 10 cm target reports 0.03 within 1 mm, not 0.13); it misses a target whose surface is farther than the query radius; and a ray that passes a target farther than its radius misses it. Every platform in the repository already passed them. The `HitTester` and `TransformPort.setEffect` docs now state those semantics, and the effect rule: `scale` multiplies the rest scale, `emissive` adds to the base intensity, and the last write wins.
|
|
12
33
|
- `@realitycollective/native-interactions` - `nativeInteractionsHostConformanceCases()`, the host conformance kit a native app runs on its device against its real slices: the core's whole `hitTesterContractCases()` run against the host itself, the 0.1 m default target radius, the release velocity reaching `endHold`, live-session capabilities, presence hiding the models only per side, `"auto"` following the live modality, and presence never hiding the cursors. The host provides the test readbacks of the new `NativeInteractionsTestHost` type. The kit fails a host that measures proximity to a target's centre, ignores the radius it is handed, loses a release velocity, or draws the wrong presence.
|
|
13
34
|
- `@realitycollective/webxr-interactions` - shared conformance suites for `HitTester` and `TransformPort`, `hitTesterContractCases()` and `transformPortContractCases()`, alongside the existing `inputProviderContractCases()` from `@realitycollective/webxr-input`. Runner-free data, the same pattern as WebXR-UIExtensions' `windowHostContractCases()`: each case throws a plain `Error` naming what a platform's ray/proximity query or transform read/write broke, and asserts only what `ports.ts` documents - a ray or proximity query answers null or an id with a positive distance and a point near the query, the nearer of two targets wins, an offset round-trips, `getWorldPose()` is finite with a unit quaternion, `getRestWorldPose()` is the registration pose and stays put, `getWorldPose()` follows `setLocalOffset`, `setLocalRotation` and `setWorldPose` from that rest frame, every returned tuple is a fresh value the caller owns and no tuple passed in is kept, and the optional `setEffect` accepts its shape without throwing. Each platform's subject starts away from the origin and turned, so a port that ignores its rest frame fails. Run against all five platforms in `packages/iwsdk-interactions/test/port-parity.test.ts`, next to the provider parity suite; every platform passes every case.
|
|
@@ -27,6 +48,8 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
|
|
|
27
48
|
|
|
28
49
|
### Changed
|
|
29
50
|
|
|
51
|
+
- 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.
|
|
52
|
+
|
|
30
53
|
- 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.
|
|
31
54
|
- `@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.
|
|
32
55
|
- **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:
|
|
@@ -37,9 +60,13 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
|
|
|
37
60
|
- `@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.
|
|
38
61
|
- `@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.
|
|
39
62
|
- `@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.
|
|
63
|
+
- 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.
|
|
40
64
|
|
|
41
65
|
### Fixed
|
|
42
66
|
|
|
67
|
+
- `@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.
|
|
68
|
+
- 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.
|
|
69
|
+
|
|
43
70
|
- `@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.
|
|
44
71
|
- `@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.
|
|
45
72
|
- 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.
|
|
@@ -98,5 +125,5 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
|
|
|
98
125
|
- 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.
|
|
99
126
|
- 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.
|
|
100
127
|
|
|
101
|
-
[0.1.1]: https://github.com/realitycollective/WebXR-Interactions/
|
|
128
|
+
[0.1.1]: https://github.com/realitycollective/WebXR-Interactions/releases/tag/v0.1.1
|
|
102
129
|
[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.
|
package/dist/conformance.d.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import type { NativeInputHost, NativeInteractionHost, NativeInteractionsTestHost } from "./native-types.js";
|
|
1
|
+
import type { NativeInputHost, NativeInteractionHost, NativeInteractionsTestHost, NativePhysicsHost } from "./native-types.js";
|
|
2
2
|
/** The real slices under test, and their readbacks. */
|
|
3
3
|
export interface NativeInteractionsHostConformanceSetup {
|
|
4
4
|
input: NativeInputHost;
|
|
5
5
|
interactions: NativeInteractionHost;
|
|
6
6
|
testHost: NativeInteractionsTestHost;
|
|
7
|
+
/** The `physics` slice. A host that installs none fails the physics cases. */
|
|
8
|
+
physics?: NativePhysicsHost;
|
|
7
9
|
}
|
|
8
|
-
/** One check a native host must pass. `name` is `interactions/<row>` or `
|
|
10
|
+
/** One check a native host must pass. `name` is `interactions/<row>`, `input/<row>` or `physics/<row>`. */
|
|
9
11
|
export interface NativeInteractionsHostConformanceCase {
|
|
10
12
|
name: string;
|
|
11
13
|
run(setup: NativeInteractionsHostConformanceSetup): Promise<void>;
|
package/dist/conformance.js
CHANGED
|
@@ -15,11 +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 {
|
|
18
|
+
import { POINTER_DISPLAY_DEFAULTS } from "@realitycollective/webxr-input";
|
|
19
|
+
import { hitTesterContractCases, nearPointerContractCases, physicsFacilityContractCases, } from "@realitycollective/webxr-interactions";
|
|
19
20
|
import { NativeHitTester } from "./hit-tester.js";
|
|
20
21
|
import { NativeInteractions } from "./host.js";
|
|
22
|
+
import { NativePhysicsFacility } from "./physics-facility.js";
|
|
21
23
|
import { NativeInputProvider } from "./provider.js";
|
|
22
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
|
+
}
|
|
23
51
|
function fail(name, message) {
|
|
24
52
|
throw new Error(`[${name}] ${message}`);
|
|
25
53
|
}
|
|
@@ -46,8 +74,198 @@ export function nativeInteractionsHostConformanceCases() {
|
|
|
46
74
|
testHost.clearTargets();
|
|
47
75
|
}
|
|
48
76
|
}));
|
|
77
|
+
// The near-pointer rule over the host's real queries: touch press, grip
|
|
78
|
+
// grab, pointer priority and what the ray and cursor should show.
|
|
79
|
+
const nearCases = nearPointerContractCases().map((contractCase) => hostCase(`interactions/${contractCase.name}`, ({ interactions, testHost }, name) => {
|
|
80
|
+
testHost.clearTargets();
|
|
81
|
+
try {
|
|
82
|
+
contractCase.run({
|
|
83
|
+
hitTester: new NativeHitTester({ interactions }),
|
|
84
|
+
driver: { place: (id, position, radius) => testHost.placeTarget(id, position, radius) },
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
fail(name, error.message);
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
testHost.clearTargets();
|
|
92
|
+
}
|
|
93
|
+
}));
|
|
94
|
+
// The platform's physics behind the core contract: the whole shared suite
|
|
95
|
+
// over the host's engine, on the device.
|
|
96
|
+
const physicsCases = physicsFacilityContractCases().map((contractCase) => hostCase(`physics/${contractCase.name}`, ({ physics }, name) => {
|
|
97
|
+
if (!physics)
|
|
98
|
+
fail(name, "the host installs no physics slice; every native platform names a default engine (Jolt on Quest and Android, RealityKit on visionOS)");
|
|
99
|
+
const facility = new NativePhysicsFacility({ physics });
|
|
100
|
+
try {
|
|
101
|
+
contractCase.run({ facility });
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
fail(name, error.message);
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
facility.dispose();
|
|
108
|
+
}
|
|
109
|
+
}));
|
|
49
110
|
return [
|
|
50
111
|
...hitCases,
|
|
112
|
+
...nearCases,
|
|
113
|
+
...physicsCases,
|
|
114
|
+
hostCase("physics/a registered target with a body is held and released through the physics slice", ({ input, interactions, physics, testHost }, name) => {
|
|
115
|
+
if (!physics)
|
|
116
|
+
fail(name, "the host installs no physics slice");
|
|
117
|
+
testHost.clearTargets();
|
|
118
|
+
const setup = new NativeInteractions({ input, interactions, physics });
|
|
119
|
+
try {
|
|
120
|
+
testHost.placeTarget("rc-kit-body", [0, 1, -1], 0.1);
|
|
121
|
+
const port = setup.register({ id: "rc-kit-body", behaviours: [{ kind: "grab" }] }, { shape: { kind: "sphere", dimensions: [0.1, 0, 0] } });
|
|
122
|
+
if (!physics.hasBody("rc-kit-body"))
|
|
123
|
+
fail(name, "register with a shape must add a body to the physics slice");
|
|
124
|
+
port.beginHold();
|
|
125
|
+
if (!physics.isSuspended("rc-kit-body"))
|
|
126
|
+
fail(name, "beginHold on a target with a body must suspend it in the physics slice");
|
|
127
|
+
port.endHold({ linearVelocity: [1, 0, 0], angularVelocity: [0, 0, 0] });
|
|
128
|
+
if (physics.isSuspended("rc-kit-body"))
|
|
129
|
+
fail(name, "endHold must resume the body");
|
|
130
|
+
const velocity = physics.getVelocity("rc-kit-body").linear;
|
|
131
|
+
if (Math.abs(velocity[0] - 1) > 0.01)
|
|
132
|
+
fail(name, `endHold must hand the body the release velocity, it has ${JSON.stringify(velocity)}`);
|
|
133
|
+
setup.unregister("rc-kit-body");
|
|
134
|
+
if (physics.hasBody("rc-kit-body"))
|
|
135
|
+
fail(name, "unregister must remove the body the binding added");
|
|
136
|
+
}
|
|
137
|
+
finally {
|
|
138
|
+
setup.dispose();
|
|
139
|
+
testHost.clearTargets();
|
|
140
|
+
}
|
|
141
|
+
}),
|
|
142
|
+
hostCase("input/the ray and cursor are drawn exactly as told", ({ input, testHost }, name) => {
|
|
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.
|
|
148
|
+
if (typeof input.applyPointerVisuals !== "function") {
|
|
149
|
+
fail(name, "the host has no applyPointerVisuals, so it cannot hide the ray while a hand touches something");
|
|
150
|
+
}
|
|
151
|
+
if (typeof testHost.pointerVisuals !== "function") {
|
|
152
|
+
fail(name, "the test host has no pointerVisuals readback");
|
|
153
|
+
}
|
|
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 });
|
|
155
|
+
input.applyPointerVisuals("rc-kit-source", { ...touched });
|
|
156
|
+
const drawn = testHost.pointerVisuals("rc-kit-source");
|
|
157
|
+
if (!drawn || drawn.ray || !drawn.cursor || !samePoint(drawn.cursorPoint, touched.cursorPoint)) {
|
|
158
|
+
fail(name, `told ${JSON.stringify(touched)}, the host draws ${JSON.stringify(drawn)}`);
|
|
159
|
+
}
|
|
160
|
+
if (!testHost.cursors().some((point) => samePoint(point, touched.cursorPoint))) {
|
|
161
|
+
fail(name, "the host draws no cursor disc at the point it was told");
|
|
162
|
+
}
|
|
163
|
+
const idle = told("rc-kit-source", { ray: true });
|
|
164
|
+
input.applyPointerVisuals("rc-kit-source", { ...idle });
|
|
165
|
+
const after = testHost.pointerVisuals("rc-kit-source");
|
|
166
|
+
if (!after || !after.ray || after.cursor) {
|
|
167
|
+
fail(name, `told ${JSON.stringify(idle)}, the host draws ${JSON.stringify(after)}`);
|
|
168
|
+
}
|
|
169
|
+
if (testHost.cursors().some((point) => samePoint(point, touched.cursorPoint))) {
|
|
170
|
+
fail(name, "the host still draws the cursor disc after being told to stop");
|
|
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
|
+
}
|
|
268
|
+
}),
|
|
51
269
|
hostCase("interactions/a registered target with no radius is a 10 cm sphere", ({ input, interactions, testHost }, name) => {
|
|
52
270
|
testHost.clearTargets();
|
|
53
271
|
const setup = new NativeInteractions({ input, interactions });
|
|
@@ -132,6 +350,54 @@ export function nativeInteractionsHostConformanceCases() {
|
|
|
132
350
|
provider.dispose();
|
|
133
351
|
}
|
|
134
352
|
}),
|
|
353
|
+
hostCase("input/eye gaze: a host that reports eye tracking poses the gaze, and the binding takes far targeting", ({ input }, name) => {
|
|
354
|
+
// Runs when the host reports eye tracking; a host without it (visionOS,
|
|
355
|
+
// a Quest without eye tracking) passes without running. With it, the
|
|
356
|
+
// host must hand the binding a valid pose in a live, focused session,
|
|
357
|
+
// and the binding then samples one gaze snapshot with a ray. That the
|
|
358
|
+
// hand and controller far rays drop is the shared contract case.
|
|
359
|
+
if (!input.getFacts().eyeTracking)
|
|
360
|
+
return;
|
|
361
|
+
if (typeof input.getEyeGazePose !== "function") {
|
|
362
|
+
fail(name, "the host reports eyeTracking but has no getEyeGazePose(); the binding cannot use a fact it cannot pose");
|
|
363
|
+
}
|
|
364
|
+
const provider = new NativeInputProvider({ input });
|
|
365
|
+
try {
|
|
366
|
+
if (!provider.getCapabilities().eyeGaze) {
|
|
367
|
+
fail(name, "the host reports eyeTracking in a live session, so capabilities.eyeGaze must be true; is the session focused?");
|
|
368
|
+
}
|
|
369
|
+
const pose = input.getEyeGazePose();
|
|
370
|
+
if (!pose || [...pose.position, ...pose.quaternion].some((n) => !Number.isFinite(n))) {
|
|
371
|
+
fail(name, `getEyeGazePose() returned ${JSON.stringify(pose)}; run the kit while the user's eyes are tracked`);
|
|
372
|
+
}
|
|
373
|
+
const gaze = provider.sample().find((source) => source.kind === "gaze");
|
|
374
|
+
if (!gaze?.ray)
|
|
375
|
+
fail(name, "with a valid gaze pose the binding must sample one gaze snapshot with a ray");
|
|
376
|
+
}
|
|
377
|
+
finally {
|
|
378
|
+
provider.dispose();
|
|
379
|
+
}
|
|
380
|
+
}),
|
|
381
|
+
hostCase("input/eye gaze: a cone query never answers with a hidden target", ({ interactions, testHost }, name) => {
|
|
382
|
+
if (!interactions.hitCone)
|
|
383
|
+
return;
|
|
384
|
+
testHost.clearTargets();
|
|
385
|
+
try {
|
|
386
|
+
testHost.placeTarget("rc-kit-cone", [0.2, 1, -2], 0.1);
|
|
387
|
+
const ray = { origin: [0, 1, 0], direction: [0, 0, -1] };
|
|
388
|
+
const shown = interactions.hitCone(ray, (5 * Math.PI) / 180, 30);
|
|
389
|
+
if (!shown || shown.targetId !== "rc-kit-cone") {
|
|
390
|
+
fail(name, "a target 0.2 m beside a ray 2 m out, inside a 5 degree cone, was not found");
|
|
391
|
+
}
|
|
392
|
+
testHost.setTargetVisible?.("rc-kit-cone", false);
|
|
393
|
+
if (testHost.setTargetVisible && interactions.hitCone(ray, (5 * Math.PI) / 180, 30) !== null) {
|
|
394
|
+
fail(name, "the cone query answered with a hidden target");
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
finally {
|
|
398
|
+
testHost.clearTargets();
|
|
399
|
+
}
|
|
400
|
+
}),
|
|
135
401
|
hostCase("input/presence never hides the cursors", ({ input, testHost }, name) => {
|
|
136
402
|
const provider = new NativeInputProvider({ input });
|
|
137
403
|
try {
|
package/dist/conformance.js.map
CHANGED
|
@@ -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 type HoldRelease,\n} from \"@realitycollective/webxr-interactions\";\nimport { NativeHitTester } from \"./hit-tester.js\";\nimport { NativeInteractions } from \"./host.js\";\nimport { NativeInputProvider } from \"./provider.js\";\nimport { NativeTransformPort } from \"./transform-port.js\";\nimport type {\n NativeInputHost,\n NativeInteractionHost,\n NativeInteractionsTestHost,\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}\n\n/** One check a native host must pass. `name` is `interactions/<row>` or `input/<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 return [\n ...hitCases,\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/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/hit-tester.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* NativeHitTester - resolves the core's ray and
|
|
3
|
-
* the native host's `interactions` slice. The native app owns the
|
|
4
|
-
* physics, so targeting is entirely the host's answer; this class
|
|
5
|
-
* copies the result across and renames its `targetId` to the core's
|
|
6
|
-
* `interactableId`.
|
|
2
|
+
* NativeHitTester - resolves the core's ray, proximity and cone queries
|
|
3
|
+
* against the native host's `interactions` slice. The native app owns the
|
|
4
|
+
* scene and physics, so targeting is entirely the host's answer; this class
|
|
5
|
+
* only copies the result across and renames its `targetId` to the core's
|
|
6
|
+
* `interactableId`. `hitCone` is present only when the host slice carries
|
|
7
|
+
* one, so the runtime falls back to `hitRay` for eye gaze on a host without.
|
|
7
8
|
*/
|
|
8
9
|
import type { RayTuple, Vec3Tuple } from "@realitycollective/webxr-input";
|
|
9
10
|
import type { HitTester, InteractableHit } from "@realitycollective/webxr-interactions";
|
|
@@ -14,6 +15,7 @@ export interface NativeHitTesterOptions {
|
|
|
14
15
|
}
|
|
15
16
|
export declare class NativeHitTester implements HitTester {
|
|
16
17
|
private readonly host;
|
|
18
|
+
readonly hitCone?: (ray: RayTuple, halfAngle: number, maxLength: number) => InteractableHit | null;
|
|
17
19
|
constructor(options?: NativeHitTesterOptions);
|
|
18
20
|
hitRay(ray: RayTuple): InteractableHit | null;
|
|
19
21
|
hitProximity(point: Vec3Tuple, radius: number): InteractableHit | null;
|