@realitycollective/webxr-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 +33 -2
- package/README.md +16 -2
- package/dist/behaviours/behaviour.d.ts +17 -1
- package/dist/behaviours/behaviour.js +12 -2
- package/dist/behaviours/behaviour.js.map +1 -1
- package/dist/behaviours/grab.d.ts +1 -0
- package/dist/behaviours/grab.js +10 -4
- package/dist/behaviours/grab.js.map +1 -1
- package/dist/contract-cases.js +15 -0
- package/dist/contract-cases.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/math.d.ts +8 -0
- package/dist/math.js +17 -0
- package/dist/math.js.map +1 -1
- package/dist/memory-ports.d.ts +55 -0
- package/dist/memory-ports.js +128 -0
- package/dist/memory-ports.js.map +1 -0
- package/dist/near-pointer-contract-cases.js +48 -0
- package/dist/near-pointer-contract-cases.js.map +1 -1
- package/dist/near-pointer.d.ts +9 -52
- package/dist/near-pointer.js +9 -23
- package/dist/near-pointer.js.map +1 -1
- package/dist/release-velocity.d.ts +58 -0
- package/dist/release-velocity.js +89 -0
- package/dist/release-velocity.js.map +1 -0
- package/dist/runtime.d.ts +38 -2
- package/dist/runtime.js +75 -15
- package/dist/runtime.js.map +1 -1
- package/dist/trace-synthetic.d.ts +12 -0
- package/dist/trace-synthetic.js +194 -0
- package/dist/trace-synthetic.js.map +1 -0
- package/dist/trace.d.ts +215 -0
- package/dist/trace.js +350 -0
- package/dist/trace.js.map +1 -0
- package/package.json +8 -4
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.
|
|
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/
|
|
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
|
@@ -8,7 +8,7 @@ Two words are used throughout. An **interactable** is an object that can be inte
|
|
|
8
8
|
npm install @realitycollective/webxr-interactions
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
> You usually do **not** install this directly. Install an adapter for your engine instead. Each adapter re-exports everything here, so your app depends on one package: [`threejs-interactions`](https://www.npmjs.com/package/@realitycollective/threejs-interactions) · [`iwsdk-interactions`](https://www.npmjs.com/package/@realitycollective/iwsdk-interactions) · [`xrblocks-interactions`](https://www.npmjs.com/package/@realitycollective/xrblocks-interactions)
|
|
11
|
+
> You usually do **not** install this directly. Install an adapter for your engine instead. Each adapter re-exports everything here, so your app depends on one package: [`threejs-interactions`](https://www.npmjs.com/package/@realitycollective/threejs-interactions) · [`iwsdk-interactions`](https://www.npmjs.com/package/@realitycollective/iwsdk-interactions) · [`xrblocks-interactions`](https://www.npmjs.com/package/@realitycollective/xrblocks-interactions) · [`babylon-interactions`](https://www.npmjs.com/package/@realitycollective/babylon-interactions) · [`native-interactions`](https://www.npmjs.com/package/@realitycollective/native-interactions)
|
|
12
12
|
|
|
13
13
|
## What it provides
|
|
14
14
|
|
|
@@ -21,6 +21,14 @@ npm install @realitycollective/webxr-interactions
|
|
|
21
21
|
| **Capability checks** | If the headset cannot do what a behaviour needs, the behaviour switches itself off and reports `behaviourDisabled`, rather than silently doing nothing |
|
|
22
22
|
| **Events** | The core never calls into your code. It emits events and you subscribe |
|
|
23
23
|
| **Feedback** | The core asks for a haptic pulse or a sound. Playing it is your app's job. `routeHapticsToProvider` is an opt-in helper that sends those requests straight to the controller |
|
|
24
|
+
| **Pointer arbitration** | One decision per hand across interactables and UI panels: pass a `PointerArbiter` from `@realitycollective/webxr-input` as `pointers` to this setup and to the UI Extensions host. `INTERACTIONS_POINTER_SET`, `NEAR_POINTER_DEFAULTS`, `nearPointerContractCases()` |
|
|
25
|
+
| **Pointer display** | The ray line and cursor disc as app configuration, IWSDK's look by default: the `pointerDisplay` option, `getPointerDisplay()`, `onPointerDrawing()`. Every binding draws the same drawing |
|
|
26
|
+
| **Physics** | `PhysicsFacility`, one contract for the platform physics behind grab, throw and rest, with IWSDK's defaults (`PHYSICS_DEFAULTS`) and `physicsFacilityContractCases()`. Every binding ships one: Rapier (three.js and XR Blocks), IWSDK's own, Babylon Physics V2, the native host's |
|
|
27
|
+
| **Held pose and throws** | `TransformPort.beginHold()` / `endHold(release)`: a grabbed object follows exactly while held and leaves with the object's own velocity over IWSDK's physics step (`ReleaseVelocityTracker`, `RELEASE_WINDOW_SECONDS`). `holderPoint` is the grip first, so hands drive a dial, slide or hinge as controllers do |
|
|
28
|
+
| **Recorded-trace parity** | `TraceRecorder`, `TraceInputProvider`, `replayTrace`, `compareOutcomes`, `traceParityCases()` and `TRACE_TOLERANCES`: an input session recorded on the reference replays through every binding with the same outcome. `synthesizeTrace` builds a synthetic one |
|
|
29
|
+
| **In-memory ports** | `MemoryHitTester`, `MemoryTransformPort` and `MemoryPhysicsFacility`: a complete engine-free host for tests, tools and the trace suite |
|
|
30
|
+
| **Conformance suites** | `hitTesterContractCases()`, `transformPortContractCases()`, `nearPointerContractCases()`, `physicsFacilityContractCases()`, plus `inputProviderContractCases()` from `@realitycollective/webxr-input`, shipped as data so any binding proves itself in its own test runner |
|
|
31
|
+
| **Platforms** | three.js (raw WebXR), Meta IWSDK (the reference), Google XR Blocks, Babylon.js and native hosts (OpenXR on Quest, visionOS). IWSDK is the reference behaviour; every other binding runs the same core rules |
|
|
24
32
|
|
|
25
33
|
All input arrives through the shared [`@realitycollective/webxr-input`](https://www.npmjs.com/package/@realitycollective/webxr-input) types, so the same core runs on any runtime that has an adapter.
|
|
26
34
|
|
|
@@ -30,6 +38,12 @@ All input arrives through the shared [`@realitycollective/webxr-input`](https://
|
|
|
30
38
|
- **Events out, never callbacks in.** The core never reaches into your objects.
|
|
31
39
|
- **Fail loudly, not silently.** A behaviour the hardware cannot support reports `behaviourDisabled` instead of quietly doing nothing.
|
|
32
40
|
|
|
41
|
+
## One pointer decision, and the pointer's look
|
|
42
|
+
|
|
43
|
+
Which pointer owns a hand (touch, grab or ray) is decided ONCE per source across interactables and UI panels, by the `PointerArbiter` from `@realitycollective/webxr-input` (IWSDK's `MultiPointer` over every pointer-event object). Pass one arbiter to the Interactions setup (`pointers`) and to the UI Extensions host, so a fingertip on a panel retires the ray over an object and the cursor is reported on panels too. What is drawn for a pointer is the app's `pointerDisplay` (`PointerDisplay`, IWSDK's look by default: the ray while hitting, drawn as a stub to 0.3 m; cursors on objects and panels), settable at run time; the runtime publishes `onPointerDrawing`, which every binding's renderer draws and the native host is handed.
|
|
44
|
+
|
|
45
|
+
Throws follow the object: the release velocity is the held object's velocity over IWSDK's physics step (`release-velocity.ts`). Parity with the web is measured by replaying recorded input traces (`trace.ts`) through every binding.
|
|
46
|
+
|
|
33
47
|
## Conformance suites
|
|
34
48
|
|
|
35
49
|
Every adapter implements `HitTester` and `TransformPort` (see `ports.ts`) against its own scene graph, so this package ships the checks as data rather than as tests: `hitTesterContractCases()` and `transformPortContractCases()`, alongside `inputProviderContractCases()` from [`@realitycollective/webxr-input`](https://www.npmjs.com/package/@realitycollective/webxr-input) for `InputProvider`. Each case throws a plain `Error` naming the rule a platform broke, so any test runner can host it - an adapter written outside this repository runs the same suite its own way. All three run against every platform in `packages/iwsdk-interactions/test/`, next to each other: `provider-parity.test.ts` for `InputProvider`, `port-parity.test.ts` for `HitTester` and `TransformPort`.
|
|
@@ -38,7 +52,7 @@ Every adapter implements `HitTester` and `TransformPort` (see `ports.ts`) agains
|
|
|
38
52
|
|
|
39
53
|
## Live demo
|
|
40
54
|
|
|
41
|
-
The interaction playground - the full station set, mouse-capable on desktop, VR button for headsets: **[webxr-interactions.pages.dev](https://webxr-interactions.pages.dev)**
|
|
55
|
+
The interaction playground - the full station set on every platform (IWSDK, three.js, Babylon.js and XR Blocks), mouse-capable on desktop, VR button for headsets: **[webxr-interactions.pages.dev](https://webxr-interactions.pages.dev)**
|
|
42
56
|
|
|
43
57
|
## Documentation
|
|
44
58
|
|
|
@@ -36,6 +36,12 @@ export interface InteractorInfo {
|
|
|
36
36
|
angularVelocity?: Vec3Tuple;
|
|
37
37
|
select: number;
|
|
38
38
|
squeeze: number;
|
|
39
|
+
/**
|
|
40
|
+
* True on a release the runtime synthesised because the source was lost,
|
|
41
|
+
* the target unregistered or disabled, or the runtime disposed: no hand
|
|
42
|
+
* let go. A grab behaviour then hands its port zeros instead of a throw.
|
|
43
|
+
*/
|
|
44
|
+
synthesized?: true;
|
|
39
45
|
}
|
|
40
46
|
/** Everything a behaviour can reach during a tick or a routed moment. */
|
|
41
47
|
export interface BehaviourContext {
|
|
@@ -68,5 +74,15 @@ export interface Behaviour {
|
|
|
68
74
|
/** Current logical 0..1 value (state snapshots, HUDs). */
|
|
69
75
|
getValue(): number;
|
|
70
76
|
}
|
|
71
|
-
/**
|
|
77
|
+
/**
|
|
78
|
+
* The point a hand-driven behaviour (dial, slide, hinge) tracks while a
|
|
79
|
+
* holder grabs it: the GRIP, as IWSDK moves a held object with its grab
|
|
80
|
+
* pointer, which is built on `xrOrigin.gripSpaces[side]` (`@iwsdk/xr-input`
|
|
81
|
+
* `grab-pointer.js`; `@pmndrs/handle` then moves the object with that
|
|
82
|
+
* pointer). Never the index fingertip: with a closed hand the fingertip
|
|
83
|
+
* curls into the palm and barely moves around a wheel's axis, which is why
|
|
84
|
+
* the native handwheel would not turn (Pale Signal handover, G2). A source
|
|
85
|
+
* with no grip (a desktop pointer) falls back to its fingertip, then its
|
|
86
|
+
* ray origin.
|
|
87
|
+
*/
|
|
72
88
|
export declare function holderPoint(holder: InteractorInfo): Vec3Tuple | undefined;
|
|
@@ -1,5 +1,15 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* The point a hand-driven behaviour (dial, slide, hinge) tracks while a
|
|
3
|
+
* holder grabs it: the GRIP, as IWSDK moves a held object with its grab
|
|
4
|
+
* pointer, which is built on `xrOrigin.gripSpaces[side]` (`@iwsdk/xr-input`
|
|
5
|
+
* `grab-pointer.js`; `@pmndrs/handle` then moves the object with that
|
|
6
|
+
* pointer). Never the index fingertip: with a closed hand the fingertip
|
|
7
|
+
* curls into the palm and barely moves around a wheel's axis, which is why
|
|
8
|
+
* the native handwheel would not turn (Pale Signal handover, G2). A source
|
|
9
|
+
* with no grip (a desktop pointer) falls back to its fingertip, then its
|
|
10
|
+
* ray origin.
|
|
11
|
+
*/
|
|
2
12
|
export function holderPoint(holder) {
|
|
3
|
-
return holder.
|
|
13
|
+
return holder.gripPose?.position ?? holder.indexTip ?? holder.ray?.origin;
|
|
4
14
|
}
|
|
5
15
|
//# sourceMappingURL=behaviour.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"behaviour.js","sourceRoot":"","sources":["../../src/behaviours/behaviour.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"behaviour.js","sourceRoot":"","sources":["../../src/behaviours/behaviour.ts"],"names":[],"mappings":"AA0FA;;;;;;;;;;GAUG;AACH,MAAM,UAAU,WAAW,CAAC,MAAsB;IAChD,OAAO,MAAM,CAAC,QAAQ,EAAE,QAAQ,IAAI,MAAM,CAAC,QAAQ,IAAI,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC;AAC5E,CAAC","sourcesContent":["/**\n * The behaviour contract - a mechanism attached to an interactable.\n *\n * Behaviours are pure state machines: they receive routed input moments\n * (press/grab transitions), tick from `update(dt)`, and write their output\n * through the interactable's {@link TransformPort}. They declare:\n *\n * - `ownership` - who owns the transform (the formalised pale-signal\n * driven / dragged / handDriven / native models), so a binder knows\n * without reading code;\n * - `requires` / `requiresAnyOf` - the input capabilities they need;\n * capability negotiation disables unsatisfiable behaviours VISIBLY\n * (a `behaviourDisabled` event with the unmet list), never silently.\n */\nimport type {\n InputCapabilityRequirement,\n InputSourceKind,\n PoseTuple,\n RayTuple,\n Vec3Tuple,\n} from \"@realitycollective/webxr-input\";\nimport type { InteractionEvent } from \"../events.js\";\nimport type { FeedbackIntent } from \"../feedback.js\";\nimport type { TransformPort } from \"../ports.js\";\n\n/** Who owns the interactable's transform while this behaviour is active. */\nexport type TransformOwnership = \"driven\" | \"dragged\" | \"handDriven\" | \"native\";\n\n/** The slice of a live interactor a behaviour may consume. */\nexport interface InteractorInfo {\n id: string;\n kind: InputSourceKind;\n ray?: RayTuple;\n gripPose?: PoseTuple;\n indexTip?: Vec3Tuple;\n /**\n * Grip velocity, present on the same terms as `InputSourceSnapshot`'s -\n * the runtime's `VelocityTracker` fills it when the provider does not.\n * `onGrabEnd`'s release throw reads these; absent on a synthesized\n * release (source lost, target unregistered, runtime disposed), which\n * behaves as zero.\n */\n linearVelocity?: Vec3Tuple;\n angularVelocity?: Vec3Tuple;\n select: number;\n squeeze: number;\n /**\n * True on a release the runtime synthesised because the source was lost,\n * the target unregistered or disabled, or the runtime disposed: no hand\n * let go. A grab behaviour then hands its port zeros instead of a throw.\n */\n synthesized?: true;\n}\n\n/** Everything a behaviour can reach during a tick or a routed moment. */\nexport interface BehaviourContext {\n dt: number;\n interactableId: string;\n transform: TransformPort | undefined;\n emit(event: Omit<InteractionEvent, \"interactableId\">): void;\n feedback(intent: Omit<FeedbackIntent, \"interactableId\">): void;\n /** LIVE world pose of another registered interactable (toss scoring). */\n getWorldPose(interactableId: string): PoseTuple | undefined;\n}\n\nexport interface Behaviour {\n readonly kind: string;\n readonly ownership: TransformOwnership;\n /** ALL of these must be satisfied. */\n readonly requires: readonly InputCapabilityRequirement[];\n /** For each group, AT LEAST ONE must be satisfied. */\n readonly requiresAnyOf?: readonly (readonly InputCapabilityRequirement[])[];\n /** True when this behaviour makes the interactable a grab target. */\n readonly grabbable: boolean;\n\n onPressStart?(ctx: BehaviourContext, interactor: InteractorInfo): void;\n onPressEnd?(ctx: BehaviourContext, interactor: InteractorInfo): void;\n onGrabStart?(ctx: BehaviourContext, interactor: InteractorInfo): void;\n onGrabEnd?(ctx: BehaviourContext, interactor: InteractorInfo): void;\n\n /**\n * Per-frame tick. `holder` is set while a grab interactor holds this\n * interactable (grab-family behaviours consume it; others ignore it).\n */\n update(ctx: BehaviourContext, holder?: InteractorInfo): void;\n\n /** Current logical 0..1 value (state snapshots, HUDs). */\n getValue(): number;\n}\n\n/**\n * The point a hand-driven behaviour (dial, slide, hinge) tracks while a\n * holder grabs it: the GRIP, as IWSDK moves a held object with its grab\n * pointer, which is built on `xrOrigin.gripSpaces[side]` (`@iwsdk/xr-input`\n * `grab-pointer.js`; `@pmndrs/handle` then moves the object with that\n * pointer). Never the index fingertip: with a closed hand the fingertip\n * curls into the palm and barely moves around a wheel's axis, which is why\n * the native handwheel would not turn (Pale Signal handover, G2). A source\n * with no grip (a desktop pointer) falls back to its fingertip, then its\n * ray origin.\n */\nexport function holderPoint(holder: InteractorInfo): Vec3Tuple | undefined {\n return holder.gripPose?.position ?? holder.indexTip ?? holder.ray?.origin;\n}\n"]}
|
|
@@ -12,6 +12,7 @@ export declare class GrabBehaviour implements Behaviour {
|
|
|
12
12
|
private held;
|
|
13
13
|
private offsetPosition;
|
|
14
14
|
private offsetQuaternion;
|
|
15
|
+
private readonly release;
|
|
15
16
|
constructor(_config?: Omit<GrabConfig, "kind">, fulfilment?: GrabFulfilment);
|
|
16
17
|
setFulfilment(fulfilment: GrabFulfilment): void;
|
|
17
18
|
onGrabStart(ctx: BehaviourContext, interactor: InteractorInfo): void;
|
package/dist/behaviours/grab.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { quatConjugate, quatMultiply, vAdd, vApplyQuat, vSub } from "../math.js";
|
|
2
|
+
import { ReleaseVelocityTracker } from "../release-velocity.js";
|
|
2
3
|
export class GrabBehaviour {
|
|
3
4
|
kind = "grab";
|
|
4
5
|
ownership;
|
|
@@ -8,6 +9,7 @@ export class GrabBehaviour {
|
|
|
8
9
|
held = false;
|
|
9
10
|
offsetPosition = [0, 0, 0];
|
|
10
11
|
offsetQuaternion = [0, 0, 0, 1];
|
|
12
|
+
release = new ReleaseVelocityTracker();
|
|
11
13
|
constructor(_config = {}, fulfilment = "poseOnly") {
|
|
12
14
|
this.fulfilment = fulfilment;
|
|
13
15
|
this.ownership = fulfilment === "native" ? "native" : "handDriven";
|
|
@@ -17,6 +19,7 @@ export class GrabBehaviour {
|
|
|
17
19
|
}
|
|
18
20
|
onGrabStart(ctx, interactor) {
|
|
19
21
|
this.held = true;
|
|
22
|
+
this.release.reset();
|
|
20
23
|
if (this.fulfilment === "poseOnly") {
|
|
21
24
|
ctx.transform?.beginHold?.();
|
|
22
25
|
if (interactor.gripPose && ctx.transform) {
|
|
@@ -25,6 +28,8 @@ export class GrabBehaviour {
|
|
|
25
28
|
const invGrip = quatConjugate(grip.quaternion);
|
|
26
29
|
this.offsetPosition = vApplyQuat(vSub(object.position, grip.position), invGrip);
|
|
27
30
|
this.offsetQuaternion = quatMultiply(invGrip, object.quaternion);
|
|
31
|
+
// The pose at pick-up is the first sample the throw is measured from.
|
|
32
|
+
this.release.record(object, 0);
|
|
28
33
|
}
|
|
29
34
|
}
|
|
30
35
|
ctx.emit({ type: "grabStart", behaviourKind: this.kind, interactorId: interactor.id });
|
|
@@ -43,11 +48,11 @@ export class GrabBehaviour {
|
|
|
43
48
|
// runtime disposed) - see the file comment and `InteractorInfo`'s.
|
|
44
49
|
// The object then resumes at rest rather than flying off on a
|
|
45
50
|
// disconnect it had nothing to do with.
|
|
46
|
-
ctx.transform?.endHold?.(
|
|
47
|
-
linearVelocity:
|
|
48
|
-
|
|
49
|
-
});
|
|
51
|
+
ctx.transform?.endHold?.(interactor.synthesized
|
|
52
|
+
? { linearVelocity: [0, 0, 0], angularVelocity: [0, 0, 0] }
|
|
53
|
+
: this.release.release());
|
|
50
54
|
}
|
|
55
|
+
this.release.reset();
|
|
51
56
|
ctx.emit({ type: "grabEnd", behaviourKind: this.kind, interactorId: interactor.id });
|
|
52
57
|
ctx.feedback({
|
|
53
58
|
cue: "drop",
|
|
@@ -69,6 +74,7 @@ export class GrabBehaviour {
|
|
|
69
74
|
quaternion: quatMultiply(grip.quaternion, this.offsetQuaternion),
|
|
70
75
|
};
|
|
71
76
|
transform.setWorldPose(pose);
|
|
77
|
+
this.release.record(pose, ctx.dt);
|
|
72
78
|
}
|
|
73
79
|
getValue() {
|
|
74
80
|
return this.held ? 1 : 0;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"grab.js","sourceRoot":"","sources":["../../src/behaviours/grab.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"grab.js","sourceRoot":"","sources":["../../src/behaviours/grab.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Grab - pick up and carry an object.\n *\n * Two fulfilments, chosen by capability negotiation at bind time:\n * - `\"poseOnly\"` - the behaviour owns the carry: on grab it captures the\n * offset between the holder's grip and the object, then follows the grip\n * each frame via `setWorldPose`. On a host with no physics the object\n * stays where it is dropped. On one with physics the engine still owns\n * the simulation; this behaviour only suspends and resumes it (see\n * below).\n * - `\"native\"` - the engine owns carry/throw (e.g. IWSDK grabbables +\n * physics); the behaviour only mirrors transitions into core events.\n *\n * On a `poseOnly` host whose object has physics, \"the behaviour owns the\n * carry\" also means the behaviour owns suspending and resuming it:\n * `onGrabStart` calls `ctx.transform.beginHold()` so the follow above does\n * not fight the solver, and `onGrabEnd` calls `endHold()` with the release\n * velocity so a throw carries through. Both are optional on `TransformPort`\n * and simply do not exist on a host with no physics, so this costs a host\n * without one nothing.\n *\n * The release velocity is the core throw rule (`release-velocity.ts`): the\n * velocity of the OBJECT the carry wrote, over IWSDK's physics step of\n * 1/60 s, angular from the orientation delta over the same step. This is\n * what IWSDK's grab system and Havok hand a thrown body, so a throw on a\n * pose-only platform leaves the hand at the same speed as on the web. A\n * source that reports its own grip velocity does not override it: the rule\n * is on the object, on every platform alike. A synthesised release (the\n * source was lost, the target unregistered, the runtime disposed) throws\n * nothing: zeros, and the object resumes at rest.\n */\nimport type { PoseTuple } from \"@realitycollective/webxr-input\";\nimport { quatConjugate, quatMultiply, vAdd, vApplyQuat, vSub } from \"../math.js\";\nimport { ReleaseVelocityTracker } from \"../release-velocity.js\";\nimport type { Behaviour, BehaviourContext, InteractorInfo } from \"./behaviour.js\";\n\nexport interface GrabConfig {\n kind: \"grab\";\n}\n\nexport type GrabFulfilment = \"poseOnly\" | \"native\";\n\nexport class GrabBehaviour implements Behaviour {\n readonly kind = \"grab\";\n readonly ownership: \"handDriven\" | \"native\";\n readonly requires = [\"grabs\"] as const;\n readonly grabbable = true;\n\n private fulfilment: GrabFulfilment;\n private held = false;\n private offsetPosition = [0, 0, 0] as [number, number, number];\n private offsetQuaternion = [0, 0, 0, 1] as [number, number, number, number];\n private readonly release = new ReleaseVelocityTracker();\n\n constructor(_config: Omit<GrabConfig, \"kind\"> = {}, fulfilment: GrabFulfilment = \"poseOnly\") {\n this.fulfilment = fulfilment;\n this.ownership = fulfilment === \"native\" ? \"native\" : \"handDriven\";\n }\n\n setFulfilment(fulfilment: GrabFulfilment): void {\n this.fulfilment = fulfilment;\n }\n\n onGrabStart(ctx: BehaviourContext, interactor: InteractorInfo): void {\n this.held = true;\n this.release.reset();\n if (this.fulfilment === \"poseOnly\") {\n ctx.transform?.beginHold?.();\n if (interactor.gripPose && ctx.transform) {\n const object = ctx.transform.getWorldPose();\n const grip = interactor.gripPose;\n const invGrip = quatConjugate(grip.quaternion);\n this.offsetPosition = vApplyQuat(vSub(object.position, grip.position), invGrip);\n this.offsetQuaternion = quatMultiply(invGrip, object.quaternion);\n // The pose at pick-up is the first sample the throw is measured from.\n this.release.record(object, 0);\n }\n }\n ctx.emit({ type: \"grabStart\", behaviourKind: this.kind, interactorId: interactor.id });\n ctx.feedback({\n cue: \"grab\",\n behaviourKind: this.kind,\n sourceId: interactor.id,\n intensity: 0.5,\n durationMs: 30,\n });\n }\n\n onGrabEnd(ctx: BehaviourContext, interactor: InteractorInfo): void {\n this.held = false;\n if (this.fulfilment === \"poseOnly\") {\n // Zero on a synthesized release (source lost, target unregistered,\n // runtime disposed) - see the file comment and `InteractorInfo`'s.\n // The object then resumes at rest rather than flying off on a\n // disconnect it had nothing to do with.\n ctx.transform?.endHold?.(\n interactor.synthesized\n ? { linearVelocity: [0, 0, 0], angularVelocity: [0, 0, 0] }\n : this.release.release(),\n );\n }\n this.release.reset();\n ctx.emit({ type: \"grabEnd\", behaviourKind: this.kind, interactorId: interactor.id });\n ctx.feedback({\n cue: \"drop\",\n behaviourKind: this.kind,\n sourceId: interactor.id,\n intensity: 0.25,\n durationMs: 20,\n });\n }\n\n update(ctx: BehaviourContext, holder?: InteractorInfo): void {\n if (this.fulfilment !== \"poseOnly\" || !this.held || !holder) return;\n const grip = holder.gripPose;\n const transform = ctx.transform;\n if (!grip || !transform?.setWorldPose) return;\n const pose: PoseTuple = {\n position: vAdd(grip.position, vApplyQuat(this.offsetPosition, grip.quaternion)),\n quaternion: quatMultiply(grip.quaternion, this.offsetQuaternion),\n };\n transform.setWorldPose(pose);\n this.release.record(pose, ctx.dt);\n }\n\n getValue(): number {\n return this.held ? 1 : 0;\n }\n}\n"]}
|
package/dist/contract-cases.js
CHANGED
|
@@ -84,6 +84,21 @@ const HIT_TESTER_CASES = [
|
|
|
84
84
|
assert(inside !== null && inside.distance === 0, `a point inside the target is at distance 0, got ${inside?.distance}`);
|
|
85
85
|
},
|
|
86
86
|
},
|
|
87
|
+
{
|
|
88
|
+
name: "hitProximity's point is the surface point nearest the query point, never the centre",
|
|
89
|
+
run({ hitTester, driver }) {
|
|
90
|
+
// The touch cursor sits at this point (IWSDK's sphere intersector
|
|
91
|
+
// reports the point on the mesh). A host that answered with the centre
|
|
92
|
+
// put the cursor inside the object (Pale Signal handover, G7).
|
|
93
|
+
driver.place("target", [0, 0, -2], 0.1);
|
|
94
|
+
const hit = hitTester.hitProximity([0.13, 0, -2], 0.5);
|
|
95
|
+
assert(hit !== null, "a point 3 cm outside a 10 cm target must be found within 0.5 m");
|
|
96
|
+
assertVec3Close(hit.point, [0.1, 0, -2], 0.02, "the proximity point");
|
|
97
|
+
const above = hitTester.hitProximity([0, 0.15, -2], 0.5);
|
|
98
|
+
assert(above !== null, "a point 5 cm above the target must be found");
|
|
99
|
+
assertVec3Close(above.point, [0, 0.1, -2], 0.02, "the proximity point from above");
|
|
100
|
+
},
|
|
101
|
+
},
|
|
87
102
|
{
|
|
88
103
|
name: "hitProximity misses a target whose surface is farther than the query radius",
|
|
89
104
|
run({ hitTester, driver }) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"contract-cases.js","sourceRoot":"","sources":["../src/contract-cases.ts"],"names":[],"mappings":"AAqBA,OAAO,EAAE,YAAY,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAkD3D;;;;;;;;GAQG;AACH,MAAM,UAAU,sBAAsB;IACpC,OAAO,gBAAgB,CAAC;AAC1B,CAAC;AAED,MAAM,QAAQ,GAAa,EAAE,MAAM,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;AACxE,MAAM,QAAQ,GAAa,EAAE,MAAM,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;AAEvE;;;;;;;;GAQG;AACH,mFAAmF;AACnF,MAAM,WAAW,GAAG,IAAI,CAAC;AAEzB,SAAS,OAAO,CAAC,KAAa;IAC5B,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC;AACjC,CAAC;AAED,SAAS,KAAK,CAAC,MAAc;IAC3B,OAAO,MAAM,GAAG,IAAI,CAAC;AACvB,CAAC;AAED,MAAM,gBAAgB,GAAqC;IACzD;QACE,IAAI,EAAE,wCAAwC;QAC9C,GAAG,CAAC,EAAE,SAAS,EAAE;YACf,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,IAAI,EAAE,6CAA6C,CAAC,CAAC;QAC7F,CAAC;KACF;IACD;QACE,IAAI,EAAE,4FAA4F;QAClG,GAAG,CAAC,EAAE,SAAS,EAAE,MAAM,EAAE;YACvB,MAAM,MAAM,GAAG,IAAI,CAAC;YACpB,MAAM,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;YAC3C,MAAM,GAAG,GAAG,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;YACvC,MAAM,CAAC,GAAG,KAAK,IAAI,EAAE,6CAA6C,CAAC,CAAC;YACpE,MAAM,CAAC,GAAG,CAAC,cAAc,KAAK,QAAQ,EAAE,0CAA0C,GAAG,CAAC,cAAc,GAAG,CAAC,CAAC;YACzG,MAAM,CAAC,GAAG,CAAC,QAAQ,GAAG,CAAC,EAAE,kCAAkC,GAAG,CAAC,QAAQ,EAAE,CAAC,CAAC;YAC3E,MAAM,GAAG,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC;YAC1B,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC,EAAE,GAAG,CAAC,EAAE,oCAAoC,GAAG,CAAC,QAAQ,EAAE,CAAC,CAAC;YAC1F,eAAe,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,OAAO,CAAC,CAAC;QACvD,CAAC;KACF;IACD;QACE,IAAI,EAAE,kDAAkD;QACxD,GAAG,CAAC,EAAE,SAAS,EAAE,MAAM,EAAE;YACvB,MAAM,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;YACzC,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,IAAI,EAAE,wDAAwD,CAAC,CAAC;QACxG,CAAC;KACF;IACD;QACE,IAAI,EAAE,qEAAqE;QAC3E,GAAG,CAAC,EAAE,SAAS,EAAE,MAAM,EAAE;YACvB,MAAM,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;YACxC,MAAM,MAAM,GAAG,SAAS,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;YACvD,MAAM,CAAC,MAAM,KAAK,IAAI,EAAE,wDAAwD,CAAC,CAAC;YAClF,MAAM,CACJ,MAAM,CAAC,cAAc,KAAK,QAAQ,EAClC,0CAA0C,MAAM,CAAC,cAAc,GAAG,CACnE,CAAC;YACF,MAAM,OAAO,GAAG,SAAS,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;YACxD,MAAM,CAAC,OAAO,KAAK,IAAI,EAAE,qDAAqD,CAAC,CAAC;QAClF,CAAC;KACF;IACD;QACE,IAAI,EAAE,4EAA4E;QAClF,GAAG,CAAC,EAAE,SAAS,EAAE,MAAM,EAAE;YACvB,wEAAwE;YACxE,uEAAuE;YACvE,kEAAkE;YAClE,mBAAmB;YACnB,MAAM,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;YACxC,MAAM,OAAO,GAAG,SAAS,CAAC,YAAY,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;YAC3D,MAAM,CAAC,OAAO,KAAK,IAAI,EAAE,gEAAgE,CAAC,CAAC;YAC3F,MAAM,CACJ,OAAO,CAAC,OAAO,CAAC,QAAQ,EAAE,IAAI,EAAE,WAAW,CAAC,EAC5C,0DAA0D,OAAO,CAAC,QAAQ,EAAE,CAC7E,CAAC;YACF,MAAM,MAAM,GAAG,SAAS,CAAC,YAAY,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;YAC1D,MAAM,CAAC,MAAM,KAAK,IAAI,IAAI,MAAM,CAAC,QAAQ,KAAK,CAAC,EAAE,mDAAmD,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAC;QAC1H,CAAC;KACF;IACD;QACE,IAAI,EAAE,6EAA6E;QACnF,GAAG,CAAC,EAAE,SAAS,EAAE,MAAM,EAAE;YACvB,MAAM,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;YACxC,MAAM,CACJ,SAAS,CAAC,YAAY,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,IAAI,EACpD,mEAAmE,CACpE,CAAC;YACF,MAAM,CACJ,SAAS,CAAC,YAAY,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,IAAI,EACpD,8DAA8D,CAC/D,CAAC;QACJ,CAAC;KACF;IACD;QACE,IAAI,EAAE,8DAA8D;QACpE,GAAG,CAAC,EAAE,SAAS,EAAE,MAAM,EAAE;YACvB,MAAM,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;YAC1C,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,IAAI,EAAE,8CAA8C,CAAC,CAAC;QAC9F,CAAC;KACF;IACD;QACE,IAAI,EAAE,kDAAkD;QACxD,GAAG,CAAC,EAAE,SAAS,EAAE,MAAM,EAAE;YACvB,MAAM,MAAM,GAAG,GAAG,CAAC;YACnB,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;YACxC,MAAM,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;YACzC,MAAM,GAAG,GAAG,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;YACvC,MAAM,CAAC,GAAG,KAAK,IAAI,EAAE,yCAAyC,CAAC,CAAC;YAChE,MAAM,CAAC,GAAG,CAAC,cAAc,KAAK,MAAM,EAAE,oCAAoC,GAAG,CAAC,cAAc,GAAG,CAAC,CAAC;YACjG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,EAAE,oCAAoC,GAAG,CAAC,QAAQ,EAAE,CAAC,CAAC;QACtG,CAAC;KACF;IACD;QACE,IAAI,EAAE,0GAA0G;QAChH,GAAG,CAAC,EAAE,SAAS,EAAE,MAAM,EAAE;YACvB,IAAI,CAAC,MAAM,CAAC,SAAS;gBAAE,OAAO;YAC9B,MAAM,CAAC,SAAS,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;YACrC,oEAAoE;YACpE,gEAAgE;YAChE,wBAAwB;YACxB,MAAM,KAAK,GAAG,SAAS,CAAC,YAAY,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;YAC1D,MAAM,CACJ,KAAK,KAAK,IAAI,EACd,gHAAgH,CACjH,CAAC;YACF,MAAM,CACJ,OAAO,CAAC,KAAK,CAAC,QAAQ,EAAE,IAAI,EAAE,WAAW,CAAC,EAC1C,wEAAwE,KAAK,CAAC,QAAQ,EAAE,CACzF,CAAC;QACJ,CAAC;KACF;IACD;QACE,IAAI,EAAE,+EAA+E;QACrF,GAAG,CAAC,EAAE,SAAS,EAAE,MAAM,EAAE;YACvB,IAAI,CAAC,MAAM,CAAC,UAAU;gBAAE,OAAO;YAC/B,MAAM,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;YACzC,MAAM,CAAC,UAAU,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;YACnC,MAAM,CACJ,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,IAAI,EACnC,qDAAqD,CACtD,CAAC;YACF,MAAM,CACJ,SAAS,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,KAAK,IAAI,EAChD,6DAA6D,CAC9D,CAAC;YACF,MAAM,CAAC,UAAU,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;YAClC,MAAM,CACJ,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,IAAI,EACnC,+CAA+C,CAChD,CAAC;QACJ,CAAC;KACF;IACD;QACE,IAAI,EAAE,yDAAyD;QAC/D,GAAG,CAAC,EAAE,SAAS,EAAE,MAAM,EAAE;YACvB,MAAM,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;YACzC,KAAK,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI;gBAC3B,CAAC,QAAQ,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;gBAC5C,CAAC,cAAc,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;aACvD,EAAE,CAAC;gBACX,MAAM,KAAK,GAAG,KAAK,EAAE,CAAC;gBACtB,MAAM,MAAM,GAAG,KAAK,EAAE,CAAC;gBACvB,MAAM,CAAC,KAAK,KAAK,IAAI,IAAI,MAAM,KAAK,IAAI,EAAE,GAAG,KAAK,8BAA8B,CAAC,CAAC;gBAClF,MAAM,CAAC,MAAM,KAAK,KAAK,IAAI,MAAM,CAAC,KAAK,KAAK,KAAK,CAAC,KAAK,EAAE,GAAG,KAAK,4CAA4C,CAAC,CAAC;gBAC/G,MAAM,QAAQ,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;gBAChC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC;gBACrB,MAAM,KAAK,GAAG,KAAK,EAAE,CAAC;gBACtB,MAAM,CACJ,KAAK,KAAK,IAAI,IAAI,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,QAAQ,CAAC,EAC7D,wDAAwD,KAAK,SAAS,CACvE,CAAC;YACJ,CAAC;QACH,CAAC;KACF;IACD;QACE,IAAI,EAAE,sGAAsG;QAC5G,GAAG,CAAC,EAAE,SAAS,EAAE,MAAM,EAAE;YACvB,IAAI,CAAC,SAAS,CAAC,OAAO;gBAAE,OAAO;YAC/B,MAAM,CAAC,KAAK,CAAC,aAAa,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;YAC7C,MAAM,GAAG,GAAG,SAAS,CAAC,OAAO,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YACxD,MAAM,CAAC,GAAG,KAAK,IAAI,EAAE,+CAA+C,CAAC,CAAC;YACtE,MAAM,CAAC,GAAG,CAAC,cAAc,KAAK,aAAa,EAAE,kBAAkB,GAAG,CAAC,cAAc,2BAA2B,CAAC,CAAC;YAC9G,MAAM,CAAC,GAAG,CAAC,QAAQ,GAAG,CAAC,IAAI,GAAG,CAAC,QAAQ,IAAI,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,EAAE,6BAA6B,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,oBAAoB,CAAC,CAAC;YAClI,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,GAAG,CAAC,EAAE,sCAAsC,CAAC,CAAC;QAC5F,CAAC;KACF;IACD;QACE,IAAI,EAAE,kFAAkF;QACxF,GAAG,CAAC,EAAE,SAAS,EAAE,MAAM,EAAE;YACvB,IAAI,CAAC,SAAS,CAAC,OAAO;gBAAE,OAAO;YAC/B,wEAAwE;YACxE,iEAAiE;YACjE,MAAM,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;YAC7C,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,IAAI,EAAE,4CAA4C,CAAC,CAAC;YAC1F,MAAM,GAAG,GAAG,SAAS,CAAC,OAAO,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YACxD,MAAM,CAAC,GAAG,KAAK,IAAI,IAAI,GAAG,CAAC,cAAc,KAAK,WAAW,EAAE,iDAAiD,CAAC,CAAC;YAC9G,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,GAAG,CAAC,EAAE,sCAAsC,CAAC,CAAC;QAC9F,CAAC;KACF;IACD;QACE,IAAI,EAAE,oFAAoF;QAC1F,GAAG,CAAC,EAAE,SAAS,EAAE,MAAM,EAAE;YACvB,IAAI,CAAC,SAAS,CAAC,OAAO;gBAAE,OAAO;YAC/B,MAAM,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;YAC7C,MAAM,CAAC,KAAK,CAAC,UAAU,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,CAAC;YAC3C,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,IAAI,EAAE,sEAAsE,CAAC,CAAC;QACvI,CAAC;KACF;IACD;QACE,IAAI,EAAE,8FAA8F;QACpG,GAAG,CAAC,EAAE,SAAS,EAAE,MAAM,EAAE;YACvB,IAAI,CAAC,SAAS,CAAC,OAAO;gBAAE,OAAO;YAC/B,MAAM,CAAC,KAAK,CAAC,UAAU,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;YAC9C,MAAM,CAAC,KAAK,CAAC,SAAS,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;YAC7C,MAAM,OAAO,GAAG,SAAS,CAAC,OAAO,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YAC5D,MAAM,CAAC,OAAO,KAAK,IAAI,IAAI,OAAO,CAAC,cAAc,KAAK,SAAS,EAAE,mDAAmD,CAAC,CAAC;YACtH,MAAM,CAAC,KAAK,CAAC,aAAa,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;YAC9C,MAAM,MAAM,GAAG,SAAS,CAAC,OAAO,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YAC3D,MAAM,CAAC,MAAM,KAAK,IAAI,IAAI,MAAM,CAAC,cAAc,KAAK,aAAa,EAAE,iEAAiE,CAAC,CAAC;QACxI,CAAC;KACF;IACD;QACE,IAAI,EAAE,2GAA2G;QACjH,GAAG,CAAC,EAAE,SAAS,EAAE,MAAM,EAAE;YACvB,IAAI,CAAC,SAAS,CAAC,OAAO,IAAI,CAAC,MAAM,CAAC,UAAU;gBAAE,OAAO;YACrD,MAAM,CAAC,KAAK,CAAC,aAAa,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;YAC7C,MAAM,CAAC,UAAU,CAAC,aAAa,EAAE,KAAK,CAAC,CAAC;YACxC,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,IAAI,EAAE,uCAAuC,CAAC,CAAC;YACtG,MAAM,CAAC,UAAU,CAAC,aAAa,EAAE,IAAI,CAAC,CAAC;YACvC,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,cAAc,KAAK,aAAa,EAAE,kDAAkD,CAAC,CAAC;QAC5I,CAAC;KACF;CACF,CAAC;AAuCF;;;GAGG;AACH,MAAM,UAAU,0BAA0B;IACxC,OAAO,oBAAoB,CAAC;AAC9B,CAAC;AAED,MAAM,QAAQ,GAAG,IAAI,CAAC;AACtB,uEAAuE;AACvE,+DAA+D;AAC/D,MAAM,cAAc,GAAc,CAAC,CAAC,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;AAErE,8EAA8E;AAC9E,wEAAwE;AACxE,qEAAqE;AACrE,0EAA0E;AAC1E,yEAAyE;AACzE,yEAAyE;AACzE,8BAA8B;AAC9B,8EAA8E;AAE9E,MAAM,UAAU,GAAG,CAAC,GAAG,EAAE,CAAC;AAC1B,MAAM,UAAU,GAAG,CAAC,CAAC;AACrB,4HAA4H;AAC5H,MAAM,aAAa,GAAG,CAAC,CAAC;AACxB,wHAAwH;AACxH,MAAM,iBAAiB,GAAG,EAAE,CAAC;AAC7B;;;;GAIG;AACH,MAAM,gBAAgB,GAAG,IAAI,CAAC;AAE9B,SAAS,sBAAsB,CAAC,KAAgB,EAAE,QAAmB,EAAE,KAAa;IAClF,eAAe,CAAC,KAAK,CAAC,QAAQ,EAAE,QAAQ,CAAC,QAAQ,EAAE,gBAAgB,EAAE,GAAG,KAAK,WAAW,CAAC,CAAC;IAC1F,eAAe,CAAC,KAAK,CAAC,UAAU,EAAE,QAAQ,CAAC,UAAU,EAAE,GAAG,KAAK,aAAa,CAAC,CAAC;AAChF,CAAC;AAED,MAAM,oBAAoB,GAAyC;IACjE;QACE,IAAI,EAAE,4DAA4D;QAClE,GAAG,CAAC,EAAE,IAAI,EAAE;YACV,MAAM,MAAM,GAAc,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;YAC5C,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC;YAC5B,eAAe,CAAC,IAAI,CAAC,cAAc,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,kBAAkB,CAAC,CAAC;QAC/E,CAAC;KACF;IACD;QACE,IAAI,EAAE,2DAA2D;QACjE,GAAG,CAAC,EAAE,IAAI,EAAE;YACV,MAAM,IAAI,GAAG,IAAI,CAAC,YAAY,EAAE,CAAC;YACjC,KAAK,MAAM,KAAK,IAAI,CAAC,GAAG,IAAI,CAAC,QAAQ,EAAE,GAAG,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;gBAC3D,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,kDAAkD,KAAK,EAAE,CAAC,CAAC;YAC5F,CAAC;YACD,MAAM,MAAM,GAAG,UAAU,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;YAC3C,MAAM,CACJ,OAAO,CAAC,MAAM,EAAE,CAAC,EAAE,QAAQ,CAAC,EAC5B,mEAAmE,MAAM,EAAE,CAC5E,CAAC;QACJ,CAAC;KACF;IACD;QACE,IAAI,EAAE,wDAAwD;QAC9D,GAAG,CAAC,EAAE,IAAI,EAAE;YACV,IAAI,CAAC;gBACH,IAAI,CAAC,gBAAgB,CAAC,cAAc,CAAC,CAAC;YACxC,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,MAAM,IAAI,KAAK,CAAC,+DAA+D,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;YAClG,CAAC;QACH,CAAC;KACF;IACD;QACE,IAAI,EAAE,4DAA4D;QAClE,GAAG,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE;YAChB,eAAe,CAAC,IAAI,CAAC,gBAAgB,EAAE,EAAE,IAAI,EAAE,oBAAoB,CAAC,CAAC;QACvE,CAAC;KACF;IACD;QACE,IAAI,EAAE,kDAAkD;QACxD,GAAG,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE;YAChB,eAAe,CAAC,IAAI,CAAC,YAAY,EAAE,EAAE,IAAI,EAAE,gBAAgB,CAAC,CAAC;QAC/D,CAAC;KACF;IACD;QACE,IAAI,EAAE,sEAAsE;QAC5E,GAAG,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE;YAChB,MAAM,MAAM,GAAc,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;YAC5C,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC;YAC5B,eAAe,CACb,IAAI,CAAC,YAAY,EAAE,CAAC,QAAQ,EAC5B,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,UAAU,CAAC,MAAM,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC,EACxD,QAAQ,EACR,8CAA8C,CAC/C,CAAC;YACF,eAAe,CAAC,IAAI,CAAC,gBAAgB,EAAE,EAAE,IAAI,EAAE,yCAAyC,CAAC,CAAC;QAC5F,CAAC;KACF;IACD;QACE,IAAI,EAAE,wEAAwE;QAC9E,GAAG,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE;YAChB,IAAI,CAAC,gBAAgB,CAAC,cAAc,CAAC,CAAC;YACtC,eAAe,CACb,IAAI,CAAC,YAAY,EAAE,CAAC,UAAU,EAC9B,YAAY,CAAC,IAAI,CAAC,UAAU,EAAE,cAAc,CAAC,EAC7C,kDAAkD,CACnD,CAAC;YACF,eAAe,CAAC,IAAI,CAAC,gBAAgB,EAAE,EAAE,IAAI,EAAE,2CAA2C,CAAC,CAAC;QAC9F,CAAC;KACF;IACD;QACE,IAAI,EAAE,6DAA6D;QACnE,GAAG,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE;YAChB,IAAI,CAAC,IAAI,CAAC,YAAY;gBAAE,OAAO;YAC/B,MAAM,MAAM,GAAc,EAAE,QAAQ,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,EAAE,UAAU,EAAE,cAAc,EAAE,CAAC;YACrF,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC;YAC1B,eAAe,CAAC,IAAI,CAAC,YAAY,EAAE,EAAE,MAAM,EAAE,mCAAmC,CAAC,CAAC;YAClF,eAAe,CAAC,IAAI,CAAC,gBAAgB,EAAE,EAAE,IAAI,EAAE,uCAAuC,CAAC,CAAC;QAC1F,CAAC;KACF;IACD;QACE,IAAI,EAAE,uDAAuD;QAC7D,GAAG,CAAC,EAAE,IAAI,EAAE;YACV,MAAM,IAAI,GAAG,IAAI,CAAC,YAAY,EAAE,CAAC;YACjC,MAAM,QAAQ,GAAG,IAAI,CAAC,gBAAgB,EAAE,CAAC;YACzC,MAAM,MAAM,GAAG,IAAI,CAAC,cAAc,EAAE,CAAC;YACrC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC;YACvB,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC;YACzB,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC;YAC3B,MAAM,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC;YAChB,MAAM,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,GAAG,EAAE,qEAAqE,CAAC,CAAC;YACvH,MAAM,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC,KAAK,GAAG,EAAE,0EAA0E,CAAC,CAAC;YAC9H,MAAM,CAAC,IAAI,CAAC,gBAAgB,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,GAAG,EAAE,yEAAyE,CAAC,CAAC;YAC/H,MAAM,CAAC,IAAI,CAAC,cAAc,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,EAAE,uEAAuE,CAAC,CAAC;QACpH,CAAC;KACF;IACD;QACE,IAAI,EAAE,+BAA+B;QACrC,GAAG,CAAC,EAAE,IAAI,EAAE;YACV,MAAM,MAAM,GAAc,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;YAC5C,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC;YAC5B,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;YACd,eAAe,CAAC,IAAI,CAAC,cAAc,EAAE,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,IAAI,CAAC,EAAE,QAAQ,EAAE,oDAAoD,CAAC,CAAC;YAC1H,IAAI,CAAC,IAAI,CAAC,YAAY;gBAAE,OAAO;YAC/B,MAAM,MAAM,GAAc,EAAE,QAAQ,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,EAAE,UAAU,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;YACnF,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC;YAC1B,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;YACvB,eAAe,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC,QAAQ,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,EAAE,QAAQ,EAAE,kDAAkD,CAAC,CAAC;QAChI,CAAC;KACF;IACD;QACE,IAAI,EAAE,sEAAsE;QAC5E,GAAG,CAAC,EAAE,IAAI,EAAE;YACV,IAAI,CAAC,IAAI,CAAC,SAAS;gBAAE,OAAO;YAC5B,IAAI,CAAC;gBACH,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,CAAC,CAAC;YAChD,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,MAAM,IAAI,KAAK,CAAC,4DAA4D,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;YAC/F,CAAC;QACH,CAAC;KACF;IACD;QACE,IAAI,EAAE,0FAA0F;QAChG,GAAG,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE;YACzB,IAAI,CAAC,OAAO,IAAI,CAAC,IAAI,CAAC,YAAY,IAAI,CAAC,IAAI,CAAC,SAAS;gBAAE,OAAO;YAC9D,MAAM,IAAI,GAAc;gBACtB,QAAQ,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;gBACpE,UAAU,EAAE,IAAI,CAAC,UAAU;aAC5B,CAAC;YACF,IAAI,CAAC,SAAS,EAAE,CAAC;YACjB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,UAAU,EAAE,CAAC,EAAE,EAAE,CAAC;gBACpC,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;gBACxB,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;YAC3B,CAAC;YACD,sEAAsE;YACtE,mEAAmE;YACnE,iEAAiE;YACjE,qEAAqE;YACrE,kEAAkE;YAClE,iEAAiE;YACjE,eAAe,CAAC,IAAI,CAAC,YAAY,EAAE,EAAE,IAAI,EAAE,wBAAwB,UAAU,aAAa,CAAC,CAAC;QAC9F,CAAC;KACF;IACD;QACE,IAAI,EAAE,+FAA+F;QACrG,GAAG,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE;YACzB,IAAI,CAAC,OAAO,IAAI,CAAC,IAAI,CAAC,SAAS,IAAI,CAAC,IAAI,CAAC,OAAO;gBAAE,OAAO;YACzD,IAAI,CAAC,SAAS,EAAE,CAAC;YACjB,MAAM,OAAO,GAAgB,EAAE,cAAc,EAAE,CAAC,aAAa,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,eAAe,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;YACnG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;YACtB,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;YACzB,MAAM,KAAK,GAAG,IAAI,CAAC,YAAY,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;YACjE,MAAM,CACJ,KAAK,GAAG,aAAa,GAAG,UAAU,GAAG,GAAG,EACxC,8BAA8B,aAAa,iEAAiE,KAAK,EAAE,CACpH,CAAC;QACJ,CAAC;KACF;IACD;QACE,IAAI,EAAE,4GAA4G;QAClH,GAAG,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE;YACzB,IAAI,CAAC,OAAO,IAAI,CAAC,IAAI,CAAC,YAAY,IAAI,CAAC,IAAI,CAAC,SAAS,IAAI,CAAC,IAAI,CAAC,OAAO;gBAAE,OAAO;YAC/E,qEAAqE;YACrE,mEAAmE;YACnE,2DAA2D;YAC3D,IAAI,CAAC,SAAS,EAAE,CAAC;YACjB,MAAM,YAAY,GAAgB,EAAE,cAAc,EAAE,CAAC,iBAAiB,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,eAAe,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;YAC5G,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC;YAC3B,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;YACzB,MAAM,MAAM,GAAc;gBACxB,QAAQ,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC;gBAC9E,UAAU,EAAE,IAAI,CAAC,UAAU;aAC5B,CAAC;YACF,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC;YAC1B,mEAAmE;YACnE,oEAAoE;YACpE,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;YACzB,sBAAsB,CAAC,IAAI,CAAC,YAAY,EAAE,EAAE,MAAM,EAAE,oDAAoD,CAAC,CAAC;QAC5G,CAAC;KACF;CACF,CAAC;AAEF,8EAA8E;AAC9E,2BAA2B;AAC3B,8EAA8E;AAE9E,SAAS,MAAM,CAAC,SAAkB,EAAE,OAAe;IACjD,IAAI,CAAC,SAAS;QAAE,MAAM,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC;AAC3C,CAAC;AAED,SAAS,IAAI,CAAC,CAAY,EAAE,CAAY;IACtC,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAC3D,CAAC;AAED,SAAS,OAAO,CAAC,CAAS,EAAE,CAAS,EAAE,GAAW;IAChD,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,GAAG,CAAC;AAChC,CAAC;AAED,6EAA6E;AAC7E,2EAA2E;AAC3E,mCAAmC;AACnC,SAAS,eAAe,CAAC,KAAgB,EAAE,QAAmB,EAAE,GAAW,EAAE,KAAa;IACxF,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,GAAG,KAAK,wBAAwB,QAAQ,CAAC,CAAC,CAAC,SAAS,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IAC5G,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,GAAG,KAAK,wBAAwB,QAAQ,CAAC,CAAC,CAAC,SAAS,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IAC5G,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,GAAG,KAAK,wBAAwB,QAAQ,CAAC,CAAC,CAAC,SAAS,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;AAC9G,CAAC;AAED,6EAA6E;AAC7E,SAAS,eAAe,CAAC,KAAgB,EAAE,QAAmB,EAAE,KAAa;IAC3E,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;IAC9G,MAAM,CACJ,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,QAAQ,CAAC,EACnC,GAAG,KAAK,sBAAsB,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAChF,CAAC;AACJ,CAAC;AAED,SAAS,eAAe,CAAC,KAAgB,EAAE,QAAmB,EAAE,KAAa;IAC3E,eAAe,CAAC,KAAK,CAAC,QAAQ,EAAE,QAAQ,CAAC,QAAQ,EAAE,QAAQ,EAAE,GAAG,KAAK,WAAW,CAAC,CAAC;IAClF,eAAe,CAAC,KAAK,CAAC,UAAU,EAAE,QAAQ,CAAC,UAAU,EAAE,GAAG,KAAK,aAAa,CAAC,CAAC;AAChF,CAAC;AAED,SAAS,UAAU,CAAC,CAAY;IAC9B,OAAO,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAC1E,CAAC","sourcesContent":["/**\n * Shared `HitTester` and `TransformPort` conformance suites, shipped as data\n * rather than as tests - the same idea as `windowHostContractCases()` in\n * WebXR-UIExtensions and `inputProviderContractCases()` in\n * `@realitycollective/webxr-input`. Five adapters implement these two ports\n * against five different scene graphs; running the same checks against every\n * one is what stops an adapter quietly dropping a documented rule.\n *\n * The suites are runner-free on purpose. Every adapter repository already has\n * its own test runner, and an adapter written outside this repository cannot\n * reach into this one's `test/` folder, so the checks ship as plain objects\n * that throw an `Error` on failure and the adapter iterates them.\n *\n * Both suites assert only what `ports.ts` documents, including two rules\n * every platform now shares:\n * - `getWorldPose()` is the LIVE pose and `getRestWorldPose()` the rest pose\n * offsets are measured from.\n * - Every tuple a port returns is fresh and owned by the caller, and a tuple\n * passed in is not kept.\n */\nimport type { PoseTuple, QuatTuple, RayTuple, Vec3Tuple } from \"@realitycollective/webxr-input\";\nimport { quatMultiply, vAdd, vApplyQuat } from \"./math.js\";\nimport type { HitTester, HoldRelease, TransformPort } from \"./ports.js\";\n\n// ---------------------------------------------------------------------------\n// HitTester\n// ---------------------------------------------------------------------------\n\n/**\n * Places (or replaces) one target for a case to query against. Build a FRESH\n * subject per case: cases place targets of their own and do not clean up.\n */\nexport interface HitTesterContractDriver {\n /**\n * Register a target at `position` with an interaction radius of `radius`,\n * however the platform registers a target - a mesh, a node, an entity.\n */\n place(id: string, position: Vec3Tuple, radius: number): void;\n /**\n * Register a target at `position` with NO declared radius - a bare node\n * with no geometry of its own - on a platform that can register one\n * without a radius (three.js, XR Blocks, Babylon, IWSDK). `hitProximity`\n * must then treat it as a 0.1 m sphere, `ports.ts`'s documented default.\n * Leave this out on a platform whose registration always takes an\n * explicit radius (native); the case that needs it skips without running.\n */\n placeBare?(id: string, position: Vec3Tuple): void;\n /**\n * Hide, or show again, a placed target using the platform's own ordinary\n * visibility flag alone - three.js `Object3D.visible`, Babylon\n * `isVisible`/`setEnabled`, and so on - never by unregistering it. A hit\n * tester must never answer with a target hidden this way, even on an\n * engine whose own raycast ignores that flag by design (three.js's\n * `Raycaster`; see `ThreeHitTester.hitRay`'s own comment). Leave this out\n * on a platform with no such flag to toggle; the case that needs it skips.\n */\n setVisible?(id: string, visible: boolean): void;\n}\n\n/** The tester under test, plus the means of putting targets into its scene. */\nexport interface HitTesterContractSubject {\n hitTester: HitTester;\n driver: HitTesterContractDriver;\n}\n\n/** One check a {@link HitTester} implementation must pass. */\nexport interface HitTesterContractCase {\n name: string;\n run(subject: HitTesterContractSubject): void;\n}\n\n/**\n * The shared `HitTester` conformance suite. An adapter's test file is a loop:\n *\n * ```ts\n * for (const contractCase of hitTesterContractCases()) {\n * it(contractCase.name, () => contractCase.run(makeSubject()));\n * }\n * ```\n */\nexport function hitTesterContractCases(): readonly HitTesterContractCase[] {\n return HIT_TESTER_CASES;\n}\n\nconst DOWN_RAY: RayTuple = { origin: [0, 0, 0], direction: [0, 0, -1] };\nconst AWAY_RAY: RayTuple = { origin: [0, 0, 0], direction: [0, 0, 1] };\n\n/**\n * A ray-based query only has to land WITHIN the target's own radius:\n * `ports.ts` documents `point` as \"world-space hit or closest point\", so a\n * mesh-accurate ray tester (three.js) reporting the surface it actually hit\n * is as conforming as a sphere tester (Babylon, IWSDK) reporting the target's\n * centre. Placing the target exactly on the ray's line makes both answers\n * the same point when a tester has no radius-shaped slack, and this is the\n * tolerance for testers that do.\n */\n/** A surface distance is exact up to 1 mm, the tolerance the master row states. */\nconst SURFACE_EPS = 1e-3;\n\nfunction degrees(value: number): number {\n return (value * Math.PI) / 180;\n}\n\nfunction slack(radius: number): number {\n return radius + 0.05;\n}\n\nconst HIT_TESTER_CASES: readonly HitTesterContractCase[] = [\n {\n name: \"an empty scene answers a ray with null\",\n run({ hitTester }) {\n assert(hitTester.hitRay(DOWN_RAY) === null, \"hitRay over an empty scene must return null\");\n },\n },\n {\n name: \"a ray through a placed target returns its id, a positive distance and a point near the ray\",\n run({ hitTester, driver }) {\n const radius = 0.15;\n driver.place(\"target\", [0, 0, -2], radius);\n const hit = hitTester.hitRay(DOWN_RAY);\n assert(hit !== null, \"a ray through a placed target must not miss\");\n assert(hit.interactableId === \"target\", `expected interactableId \"target\", got \"${hit.interactableId}\"`);\n assert(hit.distance > 0, `distance must be positive, got ${hit.distance}`);\n const eps = slack(radius);\n assert(closeTo(hit.distance, 2, eps), `distance must be close to 2, got ${hit.distance}`);\n assertVec3Close(hit.point, [0, 0, -2], eps, \"point\");\n },\n },\n {\n name: \"a ray pointing away from the target returns null\",\n run({ hitTester, driver }) {\n driver.place(\"target\", [0, 0, -2], 0.15);\n assert(hitTester.hitRay(AWAY_RAY) === null, \"a ray pointing away from every target must return null\");\n },\n },\n {\n name: \"hitProximity finds a target inside its radius and misses outside it\",\n run({ hitTester, driver }) {\n driver.place(\"target\", [0, 0, -2], 0.1);\n const inside = hitTester.hitProximity([0, 0, -2], 0.5);\n assert(inside !== null, \"hitProximity at the target's own position must find it\");\n assert(\n inside.interactableId === \"target\",\n `expected interactableId \"target\", got \"${inside.interactableId}\"`,\n );\n const outside = hitTester.hitProximity([5, 5, 5], 0.05);\n assert(outside === null, \"hitProximity far from every target must return null\");\n },\n },\n {\n name: \"hitProximity reports the distance to the target's SURFACE, clamped at zero\",\n run({ hitTester, driver }) {\n // The master row: a point 3 cm outside a 10 cm target reports 0.03, not\n // 0.13. A tester that reports the distance to the centre removes every\n // near interaction, because no fingertip is ever within 5 cm of a\n // target's centre.\n driver.place(\"target\", [0, 0, -2], 0.1);\n const outside = hitTester.hitProximity([0.13, 0, -2], 0.5);\n assert(outside !== null, \"a point 3 cm outside a 10 cm target must be found within 0.5 m\");\n assert(\n closeTo(outside.distance, 0.03, SURFACE_EPS),\n `distance must be to the surface, 0.03 within 1 mm, got ${outside.distance}`,\n );\n const inside = hitTester.hitProximity([0.02, 0, -2], 0.5);\n assert(inside !== null && inside.distance === 0, `a point inside the target is at distance 0, got ${inside?.distance}`);\n },\n },\n {\n name: \"hitProximity misses a target whose surface is farther than the query radius\",\n run({ hitTester, driver }) {\n driver.place(\"target\", [0, 0, -2], 0.1);\n assert(\n hitTester.hitProximity([0.13, 0, -2], 0.02) === null,\n \"a surface 3 cm away is outside a 2 cm query and must not be found\",\n );\n assert(\n hitTester.hitProximity([0.13, 0, -2], 0.04) !== null,\n \"a surface 3 cm away is inside a 4 cm query and must be found\",\n );\n },\n },\n {\n name: \"a ray that passes a target farther than its radius misses it\",\n run({ hitTester, driver }) {\n driver.place(\"target\", [0.5, 0, -2], 0.1);\n assert(hitTester.hitRay(DOWN_RAY) === null, \"a ray 0.5 m from a 0.1 m target must miss it\");\n },\n },\n {\n name: \"with two targets on one ray, the nearer one wins\",\n run({ hitTester, driver }) {\n const radius = 0.1;\n driver.place(\"far\", [0, 0, -3], radius);\n driver.place(\"near\", [0, 0, -1], radius);\n const hit = hitTester.hitRay(DOWN_RAY);\n assert(hit !== null, \"a ray through two targets must not miss\");\n assert(hit.interactableId === \"near\", `the nearer target must win, got \"${hit.interactableId}\"`);\n assert(closeTo(hit.distance, 1, slack(radius)), `distance must be close to 1, got ${hit.distance}`);\n },\n },\n {\n name: \"hitProximity treats a target with no declared radius as a 0.1 m sphere, when the driver can register one\",\n run({ hitTester, driver }) {\n if (!driver.placeBare) return;\n driver.placeBare(\"bare\", [0, 0, -2]);\n // 0.12 m from the centre is 0.02 m outside a 0.1 m default radius -\n // found within a 0.05 m query, and NOT the 0.12 m a zero-radius\n // reading would report.\n const found = hitTester.hitProximity([0.12, 0, -2], 0.05);\n assert(\n found !== null,\n \"a point 0.12 m from a bare target's centre must be found within a 0.05 m query, given the 0.1 m default radius\",\n );\n assert(\n closeTo(found.distance, 0.02, SURFACE_EPS),\n `distance must be to the default 0.1 m surface, 0.02 within 1 mm, got ${found.distance}`,\n );\n },\n },\n {\n name: \"an object hidden by visible alone is never hit, when the driver can toggle it\",\n run({ hitTester, driver }) {\n if (!driver.setVisible) return;\n driver.place(\"target\", [0, 0, -2], 0.15);\n driver.setVisible(\"target\", false);\n assert(\n hitTester.hitRay(DOWN_RAY) === null,\n \"a ray must not hit a target hidden by visible alone\",\n );\n assert(\n hitTester.hitProximity([0, 0, -2], 0.5) === null,\n \"hitProximity must not find a target hidden by visible alone\",\n );\n driver.setVisible(\"target\", true);\n assert(\n hitTester.hitRay(DOWN_RAY) !== null,\n \"showing the target again must restore the hit\",\n );\n },\n },\n {\n name: \"each hit and its point are fresh values the caller owns\",\n run({ hitTester, driver }) {\n driver.place(\"target\", [0, 0, -2], 0.15);\n for (const [label, query] of [\n [\"hitRay\", () => hitTester.hitRay(DOWN_RAY)],\n [\"hitProximity\", () => hitTester.hitProximity([0, 0, -2], 0.5)],\n ] as const) {\n const first = query();\n const second = query();\n assert(first !== null && second !== null, `${label} must find the placed target`);\n assert(second !== first && second.point !== first.point, `${label} must return a new hit and point each call`);\n const expected = first.point[2];\n first.point[2] = 1e6;\n const third = query();\n assert(\n third !== null && closeTo(third.point[2], expected, POSE_EPS),\n `writing to a returned point must not change the next ${label} answer`,\n );\n }\n },\n },\n {\n name: \"hitCone, when present, answers a ray that reaches a target with that target at angular distance zero\",\n run({ hitTester, driver }) {\n if (!hitTester.hitCone) return;\n driver.place(\"cone-direct\", [0, 0, -2], 0.1);\n const hit = hitTester.hitCone(DOWN_RAY, degrees(5), 30);\n assert(hit !== null, \"hitCone answered null for a target on the ray\");\n assert(hit.interactableId === \"cone-direct\", `hitCone named \"${hit.interactableId}\", expected \"cone-direct\"`);\n assert(hit.distance > 0 && hit.distance <= 2 + slack(0.1), `hitCone reported distance ${String(hit.distance)}, expected about 2`);\n assert(dist(hit.point, [0, 0, -2]) <= slack(0.1), \"hitCone's point is not on the target\");\n },\n },\n {\n name: \"hitCone, when present, finds a target inside the cone that the ray itself misses\",\n run({ hitTester, driver }) {\n if (!hitTester.hitCone) return;\n // 2 m ahead and 0.2 m to the side: its silhouette (radius 0.1) is about\n // 2.9 degrees off axis, inside a 5 degree cone, outside the ray.\n driver.place(\"cone-near\", [0.2, 0, -2], 0.1);\n assert(hitTester.hitRay(DOWN_RAY) === null, \"the ray itself must miss the offset target\");\n const hit = hitTester.hitCone(DOWN_RAY, degrees(5), 30);\n assert(hit !== null && hit.interactableId === \"cone-near\", \"hitCone did not find the target inside the cone\");\n assert(dist(hit.point, [0.2, 0, -2]) <= slack(0.1), \"hitCone's point is not on the target\");\n },\n },\n {\n name: \"hitCone, when present, ignores a target outside the cone and one beyond the length\",\n run({ hitTester, driver }) {\n if (!hitTester.hitCone) return;\n driver.place(\"cone-wide\", [0.5, 0, -2], 0.1);\n driver.place(\"cone-far\", [0, 0, -40], 0.1);\n assert(hitTester.hitCone(DOWN_RAY, degrees(5), 30) === null, \"hitCone answered with a target outside the cone or beyond the length\");\n },\n },\n {\n name: \"hitCone, when present, prefers the target nearer the axis, and the nearer of two on the axis\",\n run({ hitTester, driver }) {\n if (!hitTester.hitCone) return;\n driver.place(\"cone-off\", [0.15, 0, -2], 0.05);\n driver.place(\"cone-on\", [0.05, 0, -3], 0.05);\n const byAngle = hitTester.hitCone(DOWN_RAY, degrees(8), 30);\n assert(byAngle !== null && byAngle.interactableId === \"cone-on\", \"hitCone did not prefer the target nearer the axis\");\n driver.place(\"cone-nearer\", [0, 0, -1], 0.05);\n const nearer = hitTester.hitCone(DOWN_RAY, degrees(8), 30);\n assert(nearer !== null && nearer.interactableId === \"cone-nearer\", \"with two targets on the axis, hitCone did not prefer the nearer\");\n },\n },\n {\n name: \"hitCone, when present, never answers with a target hidden by visible alone, when the driver can toggle it\",\n run({ hitTester, driver }) {\n if (!hitTester.hitCone || !driver.setVisible) return;\n driver.place(\"cone-hidden\", [0, 0, -2], 0.1);\n driver.setVisible(\"cone-hidden\", false);\n assert(hitTester.hitCone(DOWN_RAY, degrees(5), 30) === null, \"hitCone answered with a hidden target\");\n driver.setVisible(\"cone-hidden\", true);\n assert(hitTester.hitCone(DOWN_RAY, degrees(5), 30)?.interactableId === \"cone-hidden\", \"hitCone lost the target after it was shown again\");\n },\n },\n];\n\n// ---------------------------------------------------------------------------\n// TransformPort\n// ---------------------------------------------------------------------------\n\n/**\n * Advances a subject's physics simulation - a real engine step where a\n * platform's physics can run in a test, otherwise a fake that faithfully\n * reproduces the one behaviour these cases check: gravity moves the object\n * unless held, and a release velocity carries it.\n */\nexport interface TransformPortPhysicsDriver {\n step(dtSeconds: number): void;\n}\n\n/** The port under test, over one object. Build a FRESH one per case. */\nexport interface TransformPortContractSubject {\n port: TransformPort;\n /**\n * The world pose the object sat at when the port captured its rest. Place\n * it away from the origin and turned, under no parent or an unscaled one,\n * so a port that ignores its rest frame cannot pass by accident.\n */\n rest: PoseTuple;\n /**\n * Present only when the object has a physics body - the same condition\n * that gives the port `beginHold`/`endHold`. The held/released/reset\n * cases below skip without one, since there is nothing for them to check.\n */\n physics?: TransformPortPhysicsDriver;\n}\n\n/** One check a {@link TransformPort} implementation must pass. */\nexport interface TransformPortContractCase {\n name: string;\n run(subject: TransformPortContractSubject): void;\n}\n\n/**\n * The shared `TransformPort` conformance suite. An adapter's test file is a\n * loop, the same shape as {@link hitTesterContractCases}.\n */\nexport function transformPortContractCases(): readonly TransformPortContractCase[] {\n return TRANSFORM_PORT_CASES;\n}\n\nconst POSE_EPS = 1e-3;\n// A quarter turn about Y - a non-identity unit quaternion, so the case\n// cannot pass by accident on a port that ignores its argument.\nconst QUARTER_TURN_Y: QuatTuple = [0, Math.SQRT1_2, 0, Math.SQRT1_2];\n\n// ---------------------------------------------------------------------------\n// Held / released / reset - the \"held pose\" rule, checked only when the\n// subject carries a physics driver. One fixed step size and a looser\n// position tolerance than POSE_EPS: real engines differ slightly in their\n// integrator, and a held/reset case allows for up to one step of gravity\n// (see PHYSICS_POSE_EPS's own comment), where the plain pose cases above\n// never touch physics at all.\n// ---------------------------------------------------------------------------\n\nconst PHYSICS_DT = 1 / 60;\nconst HELD_STEPS = 5;\n/** metres/second - small enough that one step's drift is easy to read, large enough not to vanish into PHYSICS_POSE_EPS. */\nconst RELEASE_SPEED = 2;\n/** metres/second - about 25x RELEASE_SPEED, so \"kept the old velocity\" and \"one step of gravity\" cannot be confused. */\nconst LARGE_PRIOR_SPEED = 50;\n/**\n * 2 cm. One step of gravity at PHYSICS_DT droops roughly\n * `0.5 * 9.8 * PHYSICS_DT^2` ≈ 1.4 mm, well inside this; a full step of\n * LARGE_PRIOR_SPEED (≈ 0.83 m) or RELEASE_SPEED (≈ 33 mm) is well outside it.\n */\nconst PHYSICS_POSE_EPS = 0.02;\n\nfunction assertPhysicsPoseClose(value: PoseTuple, expected: PoseTuple, label: string): void {\n assertVec3Close(value.position, expected.position, PHYSICS_POSE_EPS, `${label}.position`);\n assertQuatClose(value.quaternion, expected.quaternion, `${label}.quaternion`);\n}\n\nconst TRANSFORM_PORT_CASES: readonly TransformPortContractCase[] = [\n {\n name: \"setLocalOffset then getLocalOffset returns the same offset\",\n run({ port }) {\n const offset: Vec3Tuple = [0.3, -0.1, 0.05];\n port.setLocalOffset(offset);\n assertVec3Close(port.getLocalOffset(), offset, POSE_EPS, \"getLocalOffset()\");\n },\n },\n {\n name: \"getWorldPose returns finite numbers and a unit quaternion\",\n run({ port }) {\n const pose = port.getWorldPose();\n for (const value of [...pose.position, ...pose.quaternion]) {\n assert(Number.isFinite(value), `getWorldPose() must return finite numbers, got ${value}`);\n }\n const length = quatLength(pose.quaternion);\n assert(\n closeTo(length, 1, POSE_EPS),\n `getWorldPose().quaternion must be a unit quaternion, length was ${length}`,\n );\n },\n },\n {\n name: \"setLocalRotation with a unit quaternion does not throw\",\n run({ port }) {\n try {\n port.setLocalRotation(QUARTER_TURN_Y);\n } catch (error) {\n throw new Error(`setLocalRotation(unit quaternion) must not throw, it threw: ${String(error)}`);\n }\n },\n },\n {\n name: \"getRestWorldPose returns the pose captured at registration\",\n run({ port, rest }) {\n assertPoseClose(port.getRestWorldPose(), rest, \"getRestWorldPose()\");\n },\n },\n {\n name: \"before any write, the live pose is the rest pose\",\n run({ port, rest }) {\n assertPoseClose(port.getWorldPose(), rest, \"getWorldPose()\");\n },\n },\n {\n name: \"getWorldPose follows setLocalOffset while getRestWorldPose stays put\",\n run({ port, rest }) {\n const offset: Vec3Tuple = [0.3, -0.1, 0.05];\n port.setLocalOffset(offset);\n assertVec3Close(\n port.getWorldPose().position,\n vAdd(rest.position, vApplyQuat(offset, rest.quaternion)),\n POSE_EPS,\n \"getWorldPose().position after setLocalOffset\",\n );\n assertPoseClose(port.getRestWorldPose(), rest, \"getRestWorldPose() after setLocalOffset\");\n },\n },\n {\n name: \"getWorldPose follows setLocalRotation while getRestWorldPose stays put\",\n run({ port, rest }) {\n port.setLocalRotation(QUARTER_TURN_Y);\n assertQuatClose(\n port.getWorldPose().quaternion,\n quatMultiply(rest.quaternion, QUARTER_TURN_Y),\n \"getWorldPose().quaternion after setLocalRotation\",\n );\n assertPoseClose(port.getRestWorldPose(), rest, \"getRestWorldPose() after setLocalRotation\");\n },\n },\n {\n name: \"setWorldPose, when present, is what getWorldPose reads back\",\n run({ port, rest }) {\n if (!port.setWorldPose) return;\n const target: PoseTuple = { position: [0.5, 1.2, -0.4], quaternion: QUARTER_TURN_Y };\n port.setWorldPose(target);\n assertPoseClose(port.getWorldPose(), target, \"getWorldPose() after setWorldPose\");\n assertPoseClose(port.getRestWorldPose(), rest, \"getRestWorldPose() after setWorldPose\");\n },\n },\n {\n name: \"every returned tuple is a fresh value the caller owns\",\n run({ port }) {\n const pose = port.getWorldPose();\n const restPose = port.getRestWorldPose();\n const offset = port.getLocalOffset();\n pose.position[0] = 1e6;\n pose.quaternion[3] = 1e6;\n restPose.position[0] = 1e6;\n offset[0] = 1e6;\n assert(port.getWorldPose().position[0] !== 1e6, \"writing to a returned getWorldPose() tuple must not change the port\");\n assert(port.getWorldPose().quaternion[3] !== 1e6, \"writing to a returned getWorldPose() quaternion must not change the port\");\n assert(port.getRestWorldPose().position[0] !== 1e6, \"writing to a returned getRestWorldPose() tuple must not change the port\");\n assert(port.getLocalOffset()[0] !== 1e6, \"writing to a returned getLocalOffset() tuple must not change the port\");\n },\n },\n {\n name: \"a tuple passed in is not kept\",\n run({ port }) {\n const offset: Vec3Tuple = [0.3, -0.1, 0.05];\n port.setLocalOffset(offset);\n offset[0] = 9;\n assertVec3Close(port.getLocalOffset(), [0.3, -0.1, 0.05], POSE_EPS, \"getLocalOffset() after the caller reused its tuple\");\n if (!port.setWorldPose) return;\n const target: PoseTuple = { position: [0.5, 1.2, -0.4], quaternion: [0, 0, 0, 1] };\n port.setWorldPose(target);\n target.position[0] = 9;\n assertVec3Close(port.getWorldPose().position, [0.5, 1.2, -0.4], POSE_EPS, \"getWorldPose() after the caller reused its tuple\");\n },\n },\n {\n name: \"setEffect, when present, accepts scale and emissive without throwing\",\n run({ port }) {\n if (!port.setEffect) return;\n try {\n port.setEffect({ scale: 1.2, emissive: 0.3 });\n } catch (error) {\n throw new Error(`setEffect({ scale, emissive }) must not throw, it threw: ${String(error)}`);\n }\n },\n },\n {\n name: \"Held: beginHold holds a fixed setWorldPose without falling, when the subject has physics\",\n run({ port, rest, physics }) {\n if (!physics || !port.setWorldPose || !port.beginHold) return;\n const held: PoseTuple = {\n position: [rest.position[0], rest.position[1] + 1, rest.position[2]],\n quaternion: rest.quaternion,\n };\n port.beginHold();\n for (let i = 0; i < HELD_STEPS; i++) {\n port.setWorldPose(held);\n physics.step(PHYSICS_DT);\n }\n // A tight tolerance, not PHYSICS_POSE_EPS: a genuinely held body sees\n // no simulation at all, so it matches the written pose exactly bar\n // floating point noise - the same POSE_EPS the non-physics cases\n // above use. One step of unwanted gravity (~2.7 mm at PHYSICS_DT) is\n // what this case exists to catch, and it must not hide inside the\n // tolerance the way it deliberately may in the Reset case below.\n assertPoseClose(port.getWorldPose(), held, `getWorldPose() after ${HELD_STEPS} held steps`);\n },\n },\n {\n name: \"Released: endHold sends the body away with the release velocity, when the subject has physics\",\n run({ port, rest, physics }) {\n if (!physics || !port.beginHold || !port.endHold) return;\n port.beginHold();\n const release: HoldRelease = { linearVelocity: [RELEASE_SPEED, 0, 0], angularVelocity: [0, 0, 0] };\n port.endHold(release);\n physics.step(PHYSICS_DT);\n const moved = port.getWorldPose().position[0] - rest.position[0];\n assert(\n moved > RELEASE_SPEED * PHYSICS_DT * 0.5,\n `endHold({ linearVelocity: [${RELEASE_SPEED}, 0, 0] }) must move the body that way within one step, moved ${moved}`,\n );\n },\n },\n {\n name: \"Reset: setWorldPose while not held teleports to the pose and clears velocity, when the subject has physics\",\n run({ port, rest, physics }) {\n if (!physics || !port.setWorldPose || !port.beginHold || !port.endHold) return;\n // Give the body a large prior velocity and let it run for a step, so\n // carrying that velocity through the reset below is unmistakable -\n // one step of it (~0.83 m) dwarfs PHYSICS_POSE_EPS (2 cm).\n port.beginHold();\n const priorRelease: HoldRelease = { linearVelocity: [LARGE_PRIOR_SPEED, 0, 0], angularVelocity: [0, 0, 0] };\n port.endHold(priorRelease);\n physics.step(PHYSICS_DT);\n const target: PoseTuple = {\n position: [rest.position[0] - 1, rest.position[1] + 2, rest.position[2] + 0.5],\n quaternion: rest.quaternion,\n };\n port.setWorldPose(target);\n // One further step: it must be at `target`, not carrying the prior\n // velocity forward - allowing for this one step's worth of gravity.\n physics.step(PHYSICS_DT);\n assertPhysicsPoseClose(port.getWorldPose(), target, \"getWorldPose() one step after a reset setWorldPose\");\n },\n },\n];\n\n// ---------------------------------------------------------------------------\n// Shared assertion helpers\n// ---------------------------------------------------------------------------\n\nfunction assert(condition: boolean, message: string): asserts condition {\n if (!condition) throw new Error(message);\n}\n\nfunction dist(a: Vec3Tuple, b: Vec3Tuple): number {\n return Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]);\n}\n\nfunction closeTo(a: number, b: number, eps: number): boolean {\n return Math.abs(a - b) <= eps;\n}\n\n// Indexed one component at a time, rather than in a loop, so each index is a\n// literal: `noUncheckedIndexedAccess` cannot narrow a tuple read through a\n// variable index back to `number`.\nfunction assertVec3Close(value: Vec3Tuple, expected: Vec3Tuple, eps: number, label: string): void {\n assert(closeTo(value[0], expected[0], eps), `${label}[0] must be close to ${expected[0]}, got ${value[0]}`);\n assert(closeTo(value[1], expected[1], eps), `${label}[1] must be close to ${expected[1]}, got ${value[1]}`);\n assert(closeTo(value[2], expected[2], eps), `${label}[2] must be close to ${expected[2]}, got ${value[2]}`);\n}\n\n// q and -q are the same rotation, so compare by the size of the dot product.\nfunction assertQuatClose(value: QuatTuple, expected: QuatTuple, label: string): void {\n const dot = value[0] * expected[0] + value[1] * expected[1] + value[2] * expected[2] + value[3] * expected[3];\n assert(\n closeTo(Math.abs(dot), 1, POSE_EPS),\n `${label} must be close to [${expected.join(\", \")}], got [${value.join(\", \")}]`,\n );\n}\n\nfunction assertPoseClose(value: PoseTuple, expected: PoseTuple, label: string): void {\n assertVec3Close(value.position, expected.position, POSE_EPS, `${label}.position`);\n assertQuatClose(value.quaternion, expected.quaternion, `${label}.quaternion`);\n}\n\nfunction quatLength(q: QuatTuple): number {\n return Math.sqrt(q[0] * q[0] + q[1] * q[1] + q[2] * q[2] + q[3] * q[3]);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"contract-cases.js","sourceRoot":"","sources":["../src/contract-cases.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Shared `HitTester` and `TransformPort` conformance suites, shipped as data\n * rather than as tests - the same idea as `windowHostContractCases()` in\n * WebXR-UIExtensions and `inputProviderContractCases()` in\n * `@realitycollective/webxr-input`. Five adapters implement these two ports\n * against five different scene graphs; running the same checks against every\n * one is what stops an adapter quietly dropping a documented rule.\n *\n * The suites are runner-free on purpose. Every adapter repository already has\n * its own test runner, and an adapter written outside this repository cannot\n * reach into this one's `test/` folder, so the checks ship as plain objects\n * that throw an `Error` on failure and the adapter iterates them.\n *\n * Both suites assert only what `ports.ts` documents, including two rules\n * every platform now shares:\n * - `getWorldPose()` is the LIVE pose and `getRestWorldPose()` the rest pose\n * offsets are measured from.\n * - Every tuple a port returns is fresh and owned by the caller, and a tuple\n * passed in is not kept.\n */\nimport type { PoseTuple, QuatTuple, RayTuple, Vec3Tuple } from \"@realitycollective/webxr-input\";\nimport { quatMultiply, vAdd, vApplyQuat } from \"./math.js\";\nimport type { HitTester, HoldRelease, TransformPort } from \"./ports.js\";\n\n// ---------------------------------------------------------------------------\n// HitTester\n// ---------------------------------------------------------------------------\n\n/**\n * Places (or replaces) one target for a case to query against. Build a FRESH\n * subject per case: cases place targets of their own and do not clean up.\n */\nexport interface HitTesterContractDriver {\n /**\n * Register a target at `position` with an interaction radius of `radius`,\n * however the platform registers a target - a mesh, a node, an entity.\n */\n place(id: string, position: Vec3Tuple, radius: number): void;\n /**\n * Register a target at `position` with NO declared radius - a bare node\n * with no geometry of its own - on a platform that can register one\n * without a radius (three.js, XR Blocks, Babylon, IWSDK). `hitProximity`\n * must then treat it as a 0.1 m sphere, `ports.ts`'s documented default.\n * Leave this out on a platform whose registration always takes an\n * explicit radius (native); the case that needs it skips without running.\n */\n placeBare?(id: string, position: Vec3Tuple): void;\n /**\n * Hide, or show again, a placed target using the platform's own ordinary\n * visibility flag alone - three.js `Object3D.visible`, Babylon\n * `isVisible`/`setEnabled`, and so on - never by unregistering it. A hit\n * tester must never answer with a target hidden this way, even on an\n * engine whose own raycast ignores that flag by design (three.js's\n * `Raycaster`; see `ThreeHitTester.hitRay`'s own comment). Leave this out\n * on a platform with no such flag to toggle; the case that needs it skips.\n */\n setVisible?(id: string, visible: boolean): void;\n}\n\n/** The tester under test, plus the means of putting targets into its scene. */\nexport interface HitTesterContractSubject {\n hitTester: HitTester;\n driver: HitTesterContractDriver;\n}\n\n/** One check a {@link HitTester} implementation must pass. */\nexport interface HitTesterContractCase {\n name: string;\n run(subject: HitTesterContractSubject): void;\n}\n\n/**\n * The shared `HitTester` conformance suite. An adapter's test file is a loop:\n *\n * ```ts\n * for (const contractCase of hitTesterContractCases()) {\n * it(contractCase.name, () => contractCase.run(makeSubject()));\n * }\n * ```\n */\nexport function hitTesterContractCases(): readonly HitTesterContractCase[] {\n return HIT_TESTER_CASES;\n}\n\nconst DOWN_RAY: RayTuple = { origin: [0, 0, 0], direction: [0, 0, -1] };\nconst AWAY_RAY: RayTuple = { origin: [0, 0, 0], direction: [0, 0, 1] };\n\n/**\n * A ray-based query only has to land WITHIN the target's own radius:\n * `ports.ts` documents `point` as \"world-space hit or closest point\", so a\n * mesh-accurate ray tester (three.js) reporting the surface it actually hit\n * is as conforming as a sphere tester (Babylon, IWSDK) reporting the target's\n * centre. Placing the target exactly on the ray's line makes both answers\n * the same point when a tester has no radius-shaped slack, and this is the\n * tolerance for testers that do.\n */\n/** A surface distance is exact up to 1 mm, the tolerance the master row states. */\nconst SURFACE_EPS = 1e-3;\n\nfunction degrees(value: number): number {\n return (value * Math.PI) / 180;\n}\n\nfunction slack(radius: number): number {\n return radius + 0.05;\n}\n\nconst HIT_TESTER_CASES: readonly HitTesterContractCase[] = [\n {\n name: \"an empty scene answers a ray with null\",\n run({ hitTester }) {\n assert(hitTester.hitRay(DOWN_RAY) === null, \"hitRay over an empty scene must return null\");\n },\n },\n {\n name: \"a ray through a placed target returns its id, a positive distance and a point near the ray\",\n run({ hitTester, driver }) {\n const radius = 0.15;\n driver.place(\"target\", [0, 0, -2], radius);\n const hit = hitTester.hitRay(DOWN_RAY);\n assert(hit !== null, \"a ray through a placed target must not miss\");\n assert(hit.interactableId === \"target\", `expected interactableId \"target\", got \"${hit.interactableId}\"`);\n assert(hit.distance > 0, `distance must be positive, got ${hit.distance}`);\n const eps = slack(radius);\n assert(closeTo(hit.distance, 2, eps), `distance must be close to 2, got ${hit.distance}`);\n assertVec3Close(hit.point, [0, 0, -2], eps, \"point\");\n },\n },\n {\n name: \"a ray pointing away from the target returns null\",\n run({ hitTester, driver }) {\n driver.place(\"target\", [0, 0, -2], 0.15);\n assert(hitTester.hitRay(AWAY_RAY) === null, \"a ray pointing away from every target must return null\");\n },\n },\n {\n name: \"hitProximity finds a target inside its radius and misses outside it\",\n run({ hitTester, driver }) {\n driver.place(\"target\", [0, 0, -2], 0.1);\n const inside = hitTester.hitProximity([0, 0, -2], 0.5);\n assert(inside !== null, \"hitProximity at the target's own position must find it\");\n assert(\n inside.interactableId === \"target\",\n `expected interactableId \"target\", got \"${inside.interactableId}\"`,\n );\n const outside = hitTester.hitProximity([5, 5, 5], 0.05);\n assert(outside === null, \"hitProximity far from every target must return null\");\n },\n },\n {\n name: \"hitProximity reports the distance to the target's SURFACE, clamped at zero\",\n run({ hitTester, driver }) {\n // The master row: a point 3 cm outside a 10 cm target reports 0.03, not\n // 0.13. A tester that reports the distance to the centre removes every\n // near interaction, because no fingertip is ever within 5 cm of a\n // target's centre.\n driver.place(\"target\", [0, 0, -2], 0.1);\n const outside = hitTester.hitProximity([0.13, 0, -2], 0.5);\n assert(outside !== null, \"a point 3 cm outside a 10 cm target must be found within 0.5 m\");\n assert(\n closeTo(outside.distance, 0.03, SURFACE_EPS),\n `distance must be to the surface, 0.03 within 1 mm, got ${outside.distance}`,\n );\n const inside = hitTester.hitProximity([0.02, 0, -2], 0.5);\n assert(inside !== null && inside.distance === 0, `a point inside the target is at distance 0, got ${inside?.distance}`);\n },\n },\n {\n name: \"hitProximity's point is the surface point nearest the query point, never the centre\",\n run({ hitTester, driver }) {\n // The touch cursor sits at this point (IWSDK's sphere intersector\n // reports the point on the mesh). A host that answered with the centre\n // put the cursor inside the object (Pale Signal handover, G7).\n driver.place(\"target\", [0, 0, -2], 0.1);\n const hit = hitTester.hitProximity([0.13, 0, -2], 0.5);\n assert(hit !== null, \"a point 3 cm outside a 10 cm target must be found within 0.5 m\");\n assertVec3Close(hit.point, [0.1, 0, -2], 0.02, \"the proximity point\");\n const above = hitTester.hitProximity([0, 0.15, -2], 0.5);\n assert(above !== null, \"a point 5 cm above the target must be found\");\n assertVec3Close(above.point, [0, 0.1, -2], 0.02, \"the proximity point from above\");\n },\n },\n {\n name: \"hitProximity misses a target whose surface is farther than the query radius\",\n run({ hitTester, driver }) {\n driver.place(\"target\", [0, 0, -2], 0.1);\n assert(\n hitTester.hitProximity([0.13, 0, -2], 0.02) === null,\n \"a surface 3 cm away is outside a 2 cm query and must not be found\",\n );\n assert(\n hitTester.hitProximity([0.13, 0, -2], 0.04) !== null,\n \"a surface 3 cm away is inside a 4 cm query and must be found\",\n );\n },\n },\n {\n name: \"a ray that passes a target farther than its radius misses it\",\n run({ hitTester, driver }) {\n driver.place(\"target\", [0.5, 0, -2], 0.1);\n assert(hitTester.hitRay(DOWN_RAY) === null, \"a ray 0.5 m from a 0.1 m target must miss it\");\n },\n },\n {\n name: \"with two targets on one ray, the nearer one wins\",\n run({ hitTester, driver }) {\n const radius = 0.1;\n driver.place(\"far\", [0, 0, -3], radius);\n driver.place(\"near\", [0, 0, -1], radius);\n const hit = hitTester.hitRay(DOWN_RAY);\n assert(hit !== null, \"a ray through two targets must not miss\");\n assert(hit.interactableId === \"near\", `the nearer target must win, got \"${hit.interactableId}\"`);\n assert(closeTo(hit.distance, 1, slack(radius)), `distance must be close to 1, got ${hit.distance}`);\n },\n },\n {\n name: \"hitProximity treats a target with no declared radius as a 0.1 m sphere, when the driver can register one\",\n run({ hitTester, driver }) {\n if (!driver.placeBare) return;\n driver.placeBare(\"bare\", [0, 0, -2]);\n // 0.12 m from the centre is 0.02 m outside a 0.1 m default radius -\n // found within a 0.05 m query, and NOT the 0.12 m a zero-radius\n // reading would report.\n const found = hitTester.hitProximity([0.12, 0, -2], 0.05);\n assert(\n found !== null,\n \"a point 0.12 m from a bare target's centre must be found within a 0.05 m query, given the 0.1 m default radius\",\n );\n assert(\n closeTo(found.distance, 0.02, SURFACE_EPS),\n `distance must be to the default 0.1 m surface, 0.02 within 1 mm, got ${found.distance}`,\n );\n },\n },\n {\n name: \"an object hidden by visible alone is never hit, when the driver can toggle it\",\n run({ hitTester, driver }) {\n if (!driver.setVisible) return;\n driver.place(\"target\", [0, 0, -2], 0.15);\n driver.setVisible(\"target\", false);\n assert(\n hitTester.hitRay(DOWN_RAY) === null,\n \"a ray must not hit a target hidden by visible alone\",\n );\n assert(\n hitTester.hitProximity([0, 0, -2], 0.5) === null,\n \"hitProximity must not find a target hidden by visible alone\",\n );\n driver.setVisible(\"target\", true);\n assert(\n hitTester.hitRay(DOWN_RAY) !== null,\n \"showing the target again must restore the hit\",\n );\n },\n },\n {\n name: \"each hit and its point are fresh values the caller owns\",\n run({ hitTester, driver }) {\n driver.place(\"target\", [0, 0, -2], 0.15);\n for (const [label, query] of [\n [\"hitRay\", () => hitTester.hitRay(DOWN_RAY)],\n [\"hitProximity\", () => hitTester.hitProximity([0, 0, -2], 0.5)],\n ] as const) {\n const first = query();\n const second = query();\n assert(first !== null && second !== null, `${label} must find the placed target`);\n assert(second !== first && second.point !== first.point, `${label} must return a new hit and point each call`);\n const expected = first.point[2];\n first.point[2] = 1e6;\n const third = query();\n assert(\n third !== null && closeTo(third.point[2], expected, POSE_EPS),\n `writing to a returned point must not change the next ${label} answer`,\n );\n }\n },\n },\n {\n name: \"hitCone, when present, answers a ray that reaches a target with that target at angular distance zero\",\n run({ hitTester, driver }) {\n if (!hitTester.hitCone) return;\n driver.place(\"cone-direct\", [0, 0, -2], 0.1);\n const hit = hitTester.hitCone(DOWN_RAY, degrees(5), 30);\n assert(hit !== null, \"hitCone answered null for a target on the ray\");\n assert(hit.interactableId === \"cone-direct\", `hitCone named \"${hit.interactableId}\", expected \"cone-direct\"`);\n assert(hit.distance > 0 && hit.distance <= 2 + slack(0.1), `hitCone reported distance ${String(hit.distance)}, expected about 2`);\n assert(dist(hit.point, [0, 0, -2]) <= slack(0.1), \"hitCone's point is not on the target\");\n },\n },\n {\n name: \"hitCone, when present, finds a target inside the cone that the ray itself misses\",\n run({ hitTester, driver }) {\n if (!hitTester.hitCone) return;\n // 2 m ahead and 0.2 m to the side: its silhouette (radius 0.1) is about\n // 2.9 degrees off axis, inside a 5 degree cone, outside the ray.\n driver.place(\"cone-near\", [0.2, 0, -2], 0.1);\n assert(hitTester.hitRay(DOWN_RAY) === null, \"the ray itself must miss the offset target\");\n const hit = hitTester.hitCone(DOWN_RAY, degrees(5), 30);\n assert(hit !== null && hit.interactableId === \"cone-near\", \"hitCone did not find the target inside the cone\");\n assert(dist(hit.point, [0.2, 0, -2]) <= slack(0.1), \"hitCone's point is not on the target\");\n },\n },\n {\n name: \"hitCone, when present, ignores a target outside the cone and one beyond the length\",\n run({ hitTester, driver }) {\n if (!hitTester.hitCone) return;\n driver.place(\"cone-wide\", [0.5, 0, -2], 0.1);\n driver.place(\"cone-far\", [0, 0, -40], 0.1);\n assert(hitTester.hitCone(DOWN_RAY, degrees(5), 30) === null, \"hitCone answered with a target outside the cone or beyond the length\");\n },\n },\n {\n name: \"hitCone, when present, prefers the target nearer the axis, and the nearer of two on the axis\",\n run({ hitTester, driver }) {\n if (!hitTester.hitCone) return;\n driver.place(\"cone-off\", [0.15, 0, -2], 0.05);\n driver.place(\"cone-on\", [0.05, 0, -3], 0.05);\n const byAngle = hitTester.hitCone(DOWN_RAY, degrees(8), 30);\n assert(byAngle !== null && byAngle.interactableId === \"cone-on\", \"hitCone did not prefer the target nearer the axis\");\n driver.place(\"cone-nearer\", [0, 0, -1], 0.05);\n const nearer = hitTester.hitCone(DOWN_RAY, degrees(8), 30);\n assert(nearer !== null && nearer.interactableId === \"cone-nearer\", \"with two targets on the axis, hitCone did not prefer the nearer\");\n },\n },\n {\n name: \"hitCone, when present, never answers with a target hidden by visible alone, when the driver can toggle it\",\n run({ hitTester, driver }) {\n if (!hitTester.hitCone || !driver.setVisible) return;\n driver.place(\"cone-hidden\", [0, 0, -2], 0.1);\n driver.setVisible(\"cone-hidden\", false);\n assert(hitTester.hitCone(DOWN_RAY, degrees(5), 30) === null, \"hitCone answered with a hidden target\");\n driver.setVisible(\"cone-hidden\", true);\n assert(hitTester.hitCone(DOWN_RAY, degrees(5), 30)?.interactableId === \"cone-hidden\", \"hitCone lost the target after it was shown again\");\n },\n },\n];\n\n// ---------------------------------------------------------------------------\n// TransformPort\n// ---------------------------------------------------------------------------\n\n/**\n * Advances a subject's physics simulation - a real engine step where a\n * platform's physics can run in a test, otherwise a fake that faithfully\n * reproduces the one behaviour these cases check: gravity moves the object\n * unless held, and a release velocity carries it.\n */\nexport interface TransformPortPhysicsDriver {\n step(dtSeconds: number): void;\n}\n\n/** The port under test, over one object. Build a FRESH one per case. */\nexport interface TransformPortContractSubject {\n port: TransformPort;\n /**\n * The world pose the object sat at when the port captured its rest. Place\n * it away from the origin and turned, under no parent or an unscaled one,\n * so a port that ignores its rest frame cannot pass by accident.\n */\n rest: PoseTuple;\n /**\n * Present only when the object has a physics body - the same condition\n * that gives the port `beginHold`/`endHold`. The held/released/reset\n * cases below skip without one, since there is nothing for them to check.\n */\n physics?: TransformPortPhysicsDriver;\n}\n\n/** One check a {@link TransformPort} implementation must pass. */\nexport interface TransformPortContractCase {\n name: string;\n run(subject: TransformPortContractSubject): void;\n}\n\n/**\n * The shared `TransformPort` conformance suite. An adapter's test file is a\n * loop, the same shape as {@link hitTesterContractCases}.\n */\nexport function transformPortContractCases(): readonly TransformPortContractCase[] {\n return TRANSFORM_PORT_CASES;\n}\n\nconst POSE_EPS = 1e-3;\n// A quarter turn about Y - a non-identity unit quaternion, so the case\n// cannot pass by accident on a port that ignores its argument.\nconst QUARTER_TURN_Y: QuatTuple = [0, Math.SQRT1_2, 0, Math.SQRT1_2];\n\n// ---------------------------------------------------------------------------\n// Held / released / reset - the \"held pose\" rule, checked only when the\n// subject carries a physics driver. One fixed step size and a looser\n// position tolerance than POSE_EPS: real engines differ slightly in their\n// integrator, and a held/reset case allows for up to one step of gravity\n// (see PHYSICS_POSE_EPS's own comment), where the plain pose cases above\n// never touch physics at all.\n// ---------------------------------------------------------------------------\n\nconst PHYSICS_DT = 1 / 60;\nconst HELD_STEPS = 5;\n/** metres/second - small enough that one step's drift is easy to read, large enough not to vanish into PHYSICS_POSE_EPS. */\nconst RELEASE_SPEED = 2;\n/** metres/second - about 25x RELEASE_SPEED, so \"kept the old velocity\" and \"one step of gravity\" cannot be confused. */\nconst LARGE_PRIOR_SPEED = 50;\n/**\n * 2 cm. One step of gravity at PHYSICS_DT droops roughly\n * `0.5 * 9.8 * PHYSICS_DT^2` ≈ 1.4 mm, well inside this; a full step of\n * LARGE_PRIOR_SPEED (≈ 0.83 m) or RELEASE_SPEED (≈ 33 mm) is well outside it.\n */\nconst PHYSICS_POSE_EPS = 0.02;\n\nfunction assertPhysicsPoseClose(value: PoseTuple, expected: PoseTuple, label: string): void {\n assertVec3Close(value.position, expected.position, PHYSICS_POSE_EPS, `${label}.position`);\n assertQuatClose(value.quaternion, expected.quaternion, `${label}.quaternion`);\n}\n\nconst TRANSFORM_PORT_CASES: readonly TransformPortContractCase[] = [\n {\n name: \"setLocalOffset then getLocalOffset returns the same offset\",\n run({ port }) {\n const offset: Vec3Tuple = [0.3, -0.1, 0.05];\n port.setLocalOffset(offset);\n assertVec3Close(port.getLocalOffset(), offset, POSE_EPS, \"getLocalOffset()\");\n },\n },\n {\n name: \"getWorldPose returns finite numbers and a unit quaternion\",\n run({ port }) {\n const pose = port.getWorldPose();\n for (const value of [...pose.position, ...pose.quaternion]) {\n assert(Number.isFinite(value), `getWorldPose() must return finite numbers, got ${value}`);\n }\n const length = quatLength(pose.quaternion);\n assert(\n closeTo(length, 1, POSE_EPS),\n `getWorldPose().quaternion must be a unit quaternion, length was ${length}`,\n );\n },\n },\n {\n name: \"setLocalRotation with a unit quaternion does not throw\",\n run({ port }) {\n try {\n port.setLocalRotation(QUARTER_TURN_Y);\n } catch (error) {\n throw new Error(`setLocalRotation(unit quaternion) must not throw, it threw: ${String(error)}`);\n }\n },\n },\n {\n name: \"getRestWorldPose returns the pose captured at registration\",\n run({ port, rest }) {\n assertPoseClose(port.getRestWorldPose(), rest, \"getRestWorldPose()\");\n },\n },\n {\n name: \"before any write, the live pose is the rest pose\",\n run({ port, rest }) {\n assertPoseClose(port.getWorldPose(), rest, \"getWorldPose()\");\n },\n },\n {\n name: \"getWorldPose follows setLocalOffset while getRestWorldPose stays put\",\n run({ port, rest }) {\n const offset: Vec3Tuple = [0.3, -0.1, 0.05];\n port.setLocalOffset(offset);\n assertVec3Close(\n port.getWorldPose().position,\n vAdd(rest.position, vApplyQuat(offset, rest.quaternion)),\n POSE_EPS,\n \"getWorldPose().position after setLocalOffset\",\n );\n assertPoseClose(port.getRestWorldPose(), rest, \"getRestWorldPose() after setLocalOffset\");\n },\n },\n {\n name: \"getWorldPose follows setLocalRotation while getRestWorldPose stays put\",\n run({ port, rest }) {\n port.setLocalRotation(QUARTER_TURN_Y);\n assertQuatClose(\n port.getWorldPose().quaternion,\n quatMultiply(rest.quaternion, QUARTER_TURN_Y),\n \"getWorldPose().quaternion after setLocalRotation\",\n );\n assertPoseClose(port.getRestWorldPose(), rest, \"getRestWorldPose() after setLocalRotation\");\n },\n },\n {\n name: \"setWorldPose, when present, is what getWorldPose reads back\",\n run({ port, rest }) {\n if (!port.setWorldPose) return;\n const target: PoseTuple = { position: [0.5, 1.2, -0.4], quaternion: QUARTER_TURN_Y };\n port.setWorldPose(target);\n assertPoseClose(port.getWorldPose(), target, \"getWorldPose() after setWorldPose\");\n assertPoseClose(port.getRestWorldPose(), rest, \"getRestWorldPose() after setWorldPose\");\n },\n },\n {\n name: \"every returned tuple is a fresh value the caller owns\",\n run({ port }) {\n const pose = port.getWorldPose();\n const restPose = port.getRestWorldPose();\n const offset = port.getLocalOffset();\n pose.position[0] = 1e6;\n pose.quaternion[3] = 1e6;\n restPose.position[0] = 1e6;\n offset[0] = 1e6;\n assert(port.getWorldPose().position[0] !== 1e6, \"writing to a returned getWorldPose() tuple must not change the port\");\n assert(port.getWorldPose().quaternion[3] !== 1e6, \"writing to a returned getWorldPose() quaternion must not change the port\");\n assert(port.getRestWorldPose().position[0] !== 1e6, \"writing to a returned getRestWorldPose() tuple must not change the port\");\n assert(port.getLocalOffset()[0] !== 1e6, \"writing to a returned getLocalOffset() tuple must not change the port\");\n },\n },\n {\n name: \"a tuple passed in is not kept\",\n run({ port }) {\n const offset: Vec3Tuple = [0.3, -0.1, 0.05];\n port.setLocalOffset(offset);\n offset[0] = 9;\n assertVec3Close(port.getLocalOffset(), [0.3, -0.1, 0.05], POSE_EPS, \"getLocalOffset() after the caller reused its tuple\");\n if (!port.setWorldPose) return;\n const target: PoseTuple = { position: [0.5, 1.2, -0.4], quaternion: [0, 0, 0, 1] };\n port.setWorldPose(target);\n target.position[0] = 9;\n assertVec3Close(port.getWorldPose().position, [0.5, 1.2, -0.4], POSE_EPS, \"getWorldPose() after the caller reused its tuple\");\n },\n },\n {\n name: \"setEffect, when present, accepts scale and emissive without throwing\",\n run({ port }) {\n if (!port.setEffect) return;\n try {\n port.setEffect({ scale: 1.2, emissive: 0.3 });\n } catch (error) {\n throw new Error(`setEffect({ scale, emissive }) must not throw, it threw: ${String(error)}`);\n }\n },\n },\n {\n name: \"Held: beginHold holds a fixed setWorldPose without falling, when the subject has physics\",\n run({ port, rest, physics }) {\n if (!physics || !port.setWorldPose || !port.beginHold) return;\n const held: PoseTuple = {\n position: [rest.position[0], rest.position[1] + 1, rest.position[2]],\n quaternion: rest.quaternion,\n };\n port.beginHold();\n for (let i = 0; i < HELD_STEPS; i++) {\n port.setWorldPose(held);\n physics.step(PHYSICS_DT);\n }\n // A tight tolerance, not PHYSICS_POSE_EPS: a genuinely held body sees\n // no simulation at all, so it matches the written pose exactly bar\n // floating point noise - the same POSE_EPS the non-physics cases\n // above use. One step of unwanted gravity (~2.7 mm at PHYSICS_DT) is\n // what this case exists to catch, and it must not hide inside the\n // tolerance the way it deliberately may in the Reset case below.\n assertPoseClose(port.getWorldPose(), held, `getWorldPose() after ${HELD_STEPS} held steps`);\n },\n },\n {\n name: \"Released: endHold sends the body away with the release velocity, when the subject has physics\",\n run({ port, rest, physics }) {\n if (!physics || !port.beginHold || !port.endHold) return;\n port.beginHold();\n const release: HoldRelease = { linearVelocity: [RELEASE_SPEED, 0, 0], angularVelocity: [0, 0, 0] };\n port.endHold(release);\n physics.step(PHYSICS_DT);\n const moved = port.getWorldPose().position[0] - rest.position[0];\n assert(\n moved > RELEASE_SPEED * PHYSICS_DT * 0.5,\n `endHold({ linearVelocity: [${RELEASE_SPEED}, 0, 0] }) must move the body that way within one step, moved ${moved}`,\n );\n },\n },\n {\n name: \"Reset: setWorldPose while not held teleports to the pose and clears velocity, when the subject has physics\",\n run({ port, rest, physics }) {\n if (!physics || !port.setWorldPose || !port.beginHold || !port.endHold) return;\n // Give the body a large prior velocity and let it run for a step, so\n // carrying that velocity through the reset below is unmistakable -\n // one step of it (~0.83 m) dwarfs PHYSICS_POSE_EPS (2 cm).\n port.beginHold();\n const priorRelease: HoldRelease = { linearVelocity: [LARGE_PRIOR_SPEED, 0, 0], angularVelocity: [0, 0, 0] };\n port.endHold(priorRelease);\n physics.step(PHYSICS_DT);\n const target: PoseTuple = {\n position: [rest.position[0] - 1, rest.position[1] + 2, rest.position[2] + 0.5],\n quaternion: rest.quaternion,\n };\n port.setWorldPose(target);\n // One further step: it must be at `target`, not carrying the prior\n // velocity forward - allowing for this one step's worth of gravity.\n physics.step(PHYSICS_DT);\n assertPhysicsPoseClose(port.getWorldPose(), target, \"getWorldPose() one step after a reset setWorldPose\");\n },\n },\n];\n\n// ---------------------------------------------------------------------------\n// Shared assertion helpers\n// ---------------------------------------------------------------------------\n\nfunction assert(condition: boolean, message: string): asserts condition {\n if (!condition) throw new Error(message);\n}\n\nfunction dist(a: Vec3Tuple, b: Vec3Tuple): number {\n return Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]);\n}\n\nfunction closeTo(a: number, b: number, eps: number): boolean {\n return Math.abs(a - b) <= eps;\n}\n\n// Indexed one component at a time, rather than in a loop, so each index is a\n// literal: `noUncheckedIndexedAccess` cannot narrow a tuple read through a\n// variable index back to `number`.\nfunction assertVec3Close(value: Vec3Tuple, expected: Vec3Tuple, eps: number, label: string): void {\n assert(closeTo(value[0], expected[0], eps), `${label}[0] must be close to ${expected[0]}, got ${value[0]}`);\n assert(closeTo(value[1], expected[1], eps), `${label}[1] must be close to ${expected[1]}, got ${value[1]}`);\n assert(closeTo(value[2], expected[2], eps), `${label}[2] must be close to ${expected[2]}, got ${value[2]}`);\n}\n\n// q and -q are the same rotation, so compare by the size of the dot product.\nfunction assertQuatClose(value: QuatTuple, expected: QuatTuple, label: string): void {\n const dot = value[0] * expected[0] + value[1] * expected[1] + value[2] * expected[2] + value[3] * expected[3];\n assert(\n closeTo(Math.abs(dot), 1, POSE_EPS),\n `${label} must be close to [${expected.join(\", \")}], got [${value.join(\", \")}]`,\n );\n}\n\nfunction assertPoseClose(value: PoseTuple, expected: PoseTuple, label: string): void {\n assertVec3Close(value.position, expected.position, POSE_EPS, `${label}.position`);\n assertQuatClose(value.quaternion, expected.quaternion, `${label}.quaternion`);\n}\n\nfunction quatLength(q: QuatTuple): number {\n return Math.sqrt(q[0] * q[0] + q[1] * q[1] + q[2] * q[2] + q[3] * q[3]);\n}\n"]}
|