@realitycollective/native-interactions 0.1.1-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 ADDED
@@ -0,0 +1,79 @@
1
+ # Changelog
2
+
3
+ Change log for the Reality Collective WebXR Interaction Extensions packages. All five packages are versioned and released together; the version below is the one carried by the `v<version>` release tag.
4
+
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
+
7
+ ## [0.1.1]
8
+
9
+ ### Added
10
+
11
+ - `@realitycollective/webxr-interactions` - shared conformance suites for `HitTester` and `TransformPort`, `hitTesterContractCases()` and `transformPortContractCases()`, alongside the existing `inputProviderContractCases()` from `@realitycollective/webxr-input`. Runner-free data, the same pattern as WebXR-UIExtensions' `windowHostContractCases()`: each case throws a plain `Error` naming what a platform's ray/proximity query or transform read/write broke, and asserts only what `ports.ts` documents - a ray or proximity query answers null or an id with a positive distance and a point near the query, the nearer of two targets wins, an offset round-trips, `getWorldPose()` is finite with a unit quaternion, `getRestWorldPose()` is the registration pose and stays put, `getWorldPose()` follows `setLocalOffset`, `setLocalRotation` and `setWorldPose` from that rest frame, every returned tuple is a fresh value the caller owns and no tuple passed in is kept, and the optional `setEffect` accepts its shape without throwing. Each platform's subject starts away from the origin and turned, so a port that ignores its rest frame fails. Run against all five platforms in `packages/iwsdk-interactions/test/port-parity.test.ts`, next to the provider parity suite; every platform passes every case.
12
+ - `@realitycollective/native-interactions` - a native host adapter, the fifth platform. It reads the `input` and `interactions` slices a native app (OpenXR on Quest, CompositorServices on visionOS) installs on `globalThis.__rcHost`, or hands in directly, and maps them onto `InputProvider`, `HitTester` and `TransformPort` with no engine dependency at all - assets and rendering stay in the native app, and only tuples and target ids cross the boundary. Every snapshot, hit and pose read across that boundary is copied, so a host that reuses its own buffers cannot reach an object the app is still holding, and optional members (`getHeadPose`, `sampleHints`, `pulse`, `setPresenceVisible`, `setPresenceModality`, `setWorldPose`, `setEffect`) are present only when the host slice itself carries them. `createNativeInteractions()` is the one-call setup, with the same shape as the other adapters' setups; `attachToHost` drives updates from the app's frame callback. The public surface matches the other adapters, and slice reading and tuple copying stay internal. Covered headlessly by an in-memory fake of both slices, including the shared `InputProvider` contract suite.
13
+
14
+ ### Changed
15
+
16
+ - `@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.
17
+ - `@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.
18
+
19
+ ### Fixed
20
+
21
+ - `@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.
22
+ - `@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.
23
+ - 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.
24
+
25
+ ## [0.1.0] - 2026-09-17
26
+
27
+ ### Added
28
+
29
+ - `verify:pack` now lints the shape of what ships: publint over every package directory, with warnings counted as errors, and attw (Are The Types Wrong) over every packed tarball, resolving the published types under node10, node16 and bundler resolution. `cjs-resolves-to-esm` is ignored by design, because every package is ESM-only and a require() caller is expected to use a dynamic import. Both run offline on the tarballs the script already builds; `publint` and `@arethetypeswrong/cli` are dev dependencies. The script stays identical across the Reality Collective repositories.
30
+ - `verify:pack` now also type-checks the published declarations themselves, with library checking on, through `scripts/declaration-check.mjs`, runnable on its own as `node scripts/declaration-check.mjs`. Each package's declaration entry is compiled as a strict consumer would compile it, with `skipLibCheck: false`, under nodenext and then bundler resolution; a diagnostic inside the package fails the run, and diagnostics inside upstream declaration files are counted and ignored, because they are not ours to fix and would drown the signal. attw proves the published types resolve; this proves they compile, which is what a consumer with library checking on, or a package emitting declarations on top of ours, needs. The check is opt-in per repository, through `declarationCheck` in `scripts/release.config.json`, because only a foreign declaration can put a name into our emitted types that the build did not already check. This repository reaches around 950 of them, from the IWSDK and three.js typings the adapter is built against, and every run prints that count so the opt-in stays measured rather than habitual. It is the check that would have caught the bare `World` under Fixed.
31
+ - `@realitycollective/webxr-interactions` - engine-free core: interactables and interactors, the behaviour set (`press` including latching, `pulse`, `hinge`, `dial`, `slide`, `grab` in poseOnly and native modes, `tossScore`), gaze (`required` gating and dwell-to-press), the runtime/binder with hints > poke > ray targeting, hysteresis and lifecycle, capability negotiation with a visible `behaviourDisabled` outcome, events as the only outbound pathway, and feedback intents (haptics and audio remain the client's, with `routeHapticsToProvider` as an explicit opt-in).
32
+ - `@realitycollective/threejs-interactions` - the default standalone adapter: raw WebXR (`XRSession` sources, hand joints, select/squeeze plus gamepad analog, haptic pulse), three.js hit-testing and transform ports, and a desktop mouse fallback. No framework required.
33
+ - `@realitycollective/iwsdk-interactions` - Meta IWSDK adapter: player-rig poses and stateful gamepads as the provider, IWSDK's own targeting (`Pressed` / `Grabbed` tags) forwarded as pre-resolved hints, and native grab fulfilment when the app enables IWSDK grabbing/physics. One-call `registerInteractions(world)` setup.
34
+ - `@realitycollective/xrblocks-interactions` - EXPERIMENTAL Google XR Blocks adapter, structurally typed against xrblocks v0.20.0 (`Input.getFrame()` ray sources and direct touches, pooled structs copied, no haptics upstream).
35
+ - `demos/playground` - the standalone interaction playground: the full station set from one portable `InteractionDescriptor`, with client-side audio cues, opt-in controller haptics and client-side toss ballistics.
36
+ - `@realitycollective/webxr-interactions` - per-source velocity. `VelocityTracker` differentiates the grip poses the runtime already samples into linear and angular velocity, and the runtime runs one by default (`velocity: false` turns it off, `{ smoothing }` averages it). A source on its first frame, or one that reappears after dropping out, reports no velocity rather than a jump from its last known pose, and a provider that supplies its own velocity keeps it. Read it from `runtime.onSample(...)` or `runtime.getSource(id)`. The differentiation itself is `velocityBetween` from `@realitycollective/webxr-input`, re-exported here; `clampDeadzone` joins the math helpers.
37
+ - `@realitycollective/webxr-interactions` - `routeAudioToSink`, the audio counterpart to `routeHapticsToProvider`: an opt-in that plays the cues named in a map through a client-supplied sink. Cues absent from the map are ignored, so an app sonifies only the moments it has sounds for.
38
+ - `@realitycollective/iwsdk-interactions` and `@realitycollective/threejs-interactions` - presence control. `setPresenceVisible(target, visible)` shows and hides the hand and controller visuals per side; the IWSDK adapter adds `setPresenceModality(mode)` to force hands or controllers over the automatic choice, and the three.js adapter takes the app's own models through `registerVisual(handedness, root)`. Every provider reports whether it can do this through `capabilities.presence`; the XR Blocks adapter reports false, since XR Blocks exposes no way to hide its visuals.
39
+ - `@realitycollective/threejs-interactions` - native WebXR pose velocity. Where the browser reports `linearVelocity` or `angularVelocity` with a grip pose (or with the wrist joint a hand falls back to), the provider passes it through on the snapshot and the core's tracker leaves it alone; where it does not, the tracker derives velocity from consecutive poses as before. The desktop mouse source synthesises nothing: its grip rides the camera ray, so the derived velocity is camera motion, and hand mechanics should be gated on `handedness !== "none"`.
40
+ - `@realitycollective/babylon-interactions` - a Babylon.js adapter, the fourth engine. It reads a `WebXRDefaultExperience` (controllers, motion controller trigger and squeeze, hand-tracking joints, session manager) for input, with `scene.onPointerObservable` as the desktop fallback, haptics through the motion controller's `pulse`, and presence over the visuals Babylon builds - motion controller root meshes and hand meshes. Babylon picks the visual per input source, so there is no modality switch. Hit-testing is a sphere test over registered nodes, with an optional `pickWithRay` hook for mesh-accurate targeting; the transform port moves, rotates and scales Babylon nodes from rest. Structurally typed against the Babylon API rather than importing `@babylonjs/core`, as the XR Blocks adapter is, so an upstream release cannot break the install - and so the whole adapter is covered headlessly by structural fakes. Written against the documented Babylon 7 API and not yet exercised against a real Babylon runtime.
41
+ - Tests for `@realitycollective/iwsdk-interactions`, which had none. `@iwsdk/core` imports headlessly in node, so the provider is covered against a structural fake world with no module mocking, and a parity suite instantiates all four providers and runs the shared contract suite against each one. The package's coverage ratchet moves off zero, and the other three ratchets rise to the floor they now measure.
42
+
43
+ ### Changed
44
+
45
+ - `@types/three` is pinned to `0.181.0` across the workspace (root `overrides`), the typings for the super-three 0.181 the workspace runs. `@iwsdk/core` carries its own `@types/three` dependency, and without the pin the tree held two copies, whose structurally identical classes are mutually unassignable in TypeScript. One copy, matching the runtime, is the rule in every Reality Collective repository that hosts IWSDK.
46
+ - Every provider reports presence through `capabilities.presence`, the required key `@realitycollective/webxr-input` 0.1.1 adds to `InputCapabilities`. The class-level `supportsPresence` field the four adapters carried while that key was unpublished is gone. This fixes a client-visible defect on the IWSDK adapter, which built its capabilities without a `presence` key at all: an app that read `capabilities.presence` saw false and hid a feature that worked. The value each provider reports is the one its own field computed - always true on IWSDK, true once Babylon has built a visual to hide, true on three.js once a model is registered, and always false on XR Blocks.
47
+ - `@realitycollective/threejs-interactions` - `registerVisual` and the new `unregisterVisual` now re-derive capabilities and notify `onCapabilitiesChanged`. Presence on this adapter is genuinely conditional: your app builds its own hand and controller models, so `capabilities.presence` is false until you hand the first one over and false again once you take the last one back.
48
+ - The Input 0.1.1 types replace the local stand-ins that were waiting on them. `InputSourceSnapshot` now carries `linearVelocity` and `angularVelocity` itself, so the `InputSourceSnapshotWithVelocity` alias is gone; the local `poseVelocity` is gone in favour of the shipped `velocityBetween`; and the per-adapter `PresenceTarget` and `PresenceModality` declarations are gone. `PresenceModality` now comes from the contracts, and a presence target is written as the contract writes it, `Handedness | "all"`. All three names were re-exported from these packages and are removed from that surface.
49
+ - Every package now requires `@realitycollective/webxr-input` at `^0.1.1` rather than `^0.1.0`, because the code above uses members that only exist from 0.1.1. A fresh install resolved to 0.1.1 either way; the range says so now, so a consumer holding an older lockfile fails at install rather than at run time.
50
+ - The provider parity suite is now three lines over `inputProviderContractCases()` from `@realitycollective/webxr-input`, run against all four adapters, instead of a hand-written copy of the same checks. The shared cases are stricter: they assert the capability record has exactly the contract's keys and that every snapshot is well formed. Two adapter-specific checks stay on top of them.
51
+ - `@realitycollective/iwsdk-interactions` - `setPresenceVisible` and `setPresenceModality` write nothing when nothing changed. The provider holds what it last applied per side - visibility, modality and the hand-tracking capability presence is derived from - and skips a side whose three inputs are unchanged. An app pushing presence from a state subscription, which is the documented pattern, no longer walks every descendant of both visual families on every unrelated state change. The per-frame re-apply during `sample()` stays unconditional, because IWSDK re-asserts its own visuals from its update loop and a capability refresh can change the modality underneath the adapter.
52
+
53
+ - `@realitycollective/iwsdk-interactions` - `pulse` resolves the side from the handedness recorded while sampling, rather than parsing the source id. Parsing the id remains the fallback for an id the provider did not produce.
54
+
55
+ - `@realitycollective/iwsdk-interactions` targets **IWSDK 0.5.x**. Its peer range is now `>=0.5.0 <0.6.0` and it is developed and tested against `@iwsdk/core` 0.5.3. The adapter is source-compatible with 0.4.x and needed no code change, but 0.5 is the only line exercised, so it is the only line supported. Every symbol and runtime member the provider uses is present in both, and it already reads `world.input.xr.gamepads` rather than the accessor 0.5 deprecates.
56
+ - `demos/playground` no longer compiles UIKitML at build time. `@iwsdk/vite-plugin-uikitml` was discontinued at 0.4.2 and only ever did `JSON.stringify(parse(source))`, so the panel is served as `.uikitml` from `public/ui/` and parsed on load. The demo drops that plugin, and its station markup uses `rgba()` rather than `background-opacity`, which the 0.5 parser removed.
57
+ - `@realitycollective/iwsdk-interactions` - the approximate hit tester the bridge keeps for gaze and poke targeting resolves each registered entity's world position once per frame instead of on every query. The core asks it five questions a frame (a poke and a ray test per side, plus the gaze ray) and each one walked every entity's parent chain again through `getWorldPosition`; the bridge now calls `beginFrame()` before the runtime ticks and the five answers come from one resolution. Measured 3.6x to 4x faster on the tester in the gate-1 benchmarks, 32 us to 9 us a frame at 50 entities on desktop, with the same answers. Its ray test also no longer allocates a tuple per entity per query.
58
+ - `@realitycollective/threejs-interactions` - `ThreeHitTester` uses three-mesh-bvh when the app has installed its prototype hooks, which IWSDK does by default: geometries are given a bounds tree as they are registered and the raycaster asks for the first hit only, so a ray against a detailed mesh costs microseconds instead of the near-millisecond a plain triangle walk takes. `three-mesh-bvh` is declared as an optional peer; without it the plain raycast is used and the README explains collider proxies. The roots list handed to the raycaster and the intersections array are now reused between calls rather than built per call, and `hitProximity` reads each object's position and scale straight off its world matrix after one matrix update instead of two updates and a decompose, measured 4x faster at 50 registered objects.
59
+ - `@realitycollective/webxr-interactions` - `onSample`, `getSource` and the pointer bridge document the snapshot ownership rule `@realitycollective/webxr-input` 0.1.3 states: a snapshot handed over is the receiver's to keep and is never written to again. The velocity tracker and the pointer bridge already relied on it.
60
+
61
+ ### Deprecated
62
+
63
+ - `@realitycollective/webxr-interactions` - `InteractionRuntime.onSourcesSampled` is deprecated in favour of `onSample`. They are the same stream, and since `InputSourceSnapshot` carries the velocity fields itself the two signatures are now identical, so only one name is needed. Nothing is removed: existing callers keep working and the method will go in a later major release.
64
+
65
+ ### Fixed
66
+
67
+ - `@realitycollective/iwsdk-interactions` emitted the same bare `World` in `InteractionBridgeSystem`'s base type that broke a consumer of the UI Extensions adapter on 9 September 2026. The cause is upstream: `@iwsdk/core` 0.5.3's `dist/ecs/system.d.ts` imports `World` from `'./world'` with no extension, the only such import in the package, and under the NodeNext resolution this repository builds with that import does not resolve (TS2835), so `World` was an unresolved name inside the host's own declaration and TypeScript's declaration emitter preserved it verbatim. `skipLibCheck` hid the error on both sides. It compiled for consumers only because `register.d.ts` happened to import `World` for another signature. The adapter now takes `createSystem` from its own `src/create-system.ts`, a wrapper whose return type names `World` through `@iwsdk/core`'s barrel, so the emitted base type no longer depends on that accident. The wrapper goes the day IWSDK ships `./world.js` there.
68
+ - `@realitycollective/iwsdk-interactions` imported three.js's `Vector3`, `Quaternion` and `Matrix4`, and IWSDK's `InputComponent`, through `@iwsdk/core`, which only provides them by `export * from 'three'` and `export * from '@iwsdk/xr-input'`. That resolves in node and in every demo, and fails a consumer whose Vite config excludes `three` from dependency optimization, which any app that transforms three's source does: esbuild cannot enumerate a star re-export it is not bundling, and the dev server stops with `No matching export in "@iwsdk/core" for import "Vector3"`, a message that names neither the package nor the cause. The provider, the transform port and `registerInteractions` now import from `three` and `@iwsdk/xr-input`, both declared as peer dependencies (`three >=0.170.0`, the range the three.js adapter carries; `@iwsdk/xr-input >=0.5.0 <0.6.0`, the range of `@iwsdk/core`, which installs it). The Anatomy Atlas XR client reported the failure against the UI Extensions adapter; the estate sweep found the same import here.
69
+ - `@realitycollective/xrblocks-interactions` imported `InteractionRuntime` from `@realitycollective/threejs-interactions`, which only re-exports it from `@realitycollective/webxr-interactions`. It now imports from the package that defines it, which it already depended on. Found by the new import-surface gate.
70
+ - Two gates, shared with every Reality Collective repository. `packages/webxr-interactions/test/import-surface.test.ts` runs `scripts/import-surface.mjs` over every published package and fails an import of a name that a dependency only re-exports from another package, and any bare import of a package the manifest does not declare. `packages/iwsdk-interactions/test/prebundle.test.ts` runs the dependency optimizer of every bundler `scripts/release.config.json` names (`scripts/prebundle-check.mjs`) over the adapter next to `@iwsdk/core`, once with defaults and once with every package `@iwsdk/core` re-exports wholesale excluded, so the consumer path is exercised on every `npm test`. It runs on **Vite 7 and Vite 8**, because this fault belongs to a bundler rather than to bundling: Vite 7 optimizes with esbuild, which requires every named import to be statically enumerable and so rejects a name behind an external `export *`, while Vite 8 optimizes with rolldown, which resolves the same import lazily through a namespace object and accepts it. Testing only one of them would say nothing about consumers on the other. The versions under test are ordinary devDependencies installed under npm aliases, `vite-7` and `vite-8`, the same mechanism the workspace already uses for `three`; add an entry to `prebundle.bundlers` to support another. Each run also feeds the optimizer a **deliberately broken copy** of the adapter, carrying one extra module that imports a name the host only re-exports wholesale, and asserts the bundler's verdict matches that bundler's `detectsStarHops` flag. The probed name is discovered from the host's own surface rather than hard-coded, so it stays valid as that surface changes. This is what stops the check becoming decoration: on a permissive bundler a gate that can only pass reports safety it never verified, and the suite now fails if a bundler's strictness moves in either direction, or if no supported bundler can detect the fault at all. Transpiling uses the TypeScript compiler the repository already builds with, so no bundler is a hidden requirement of the harness itself. Both were red before this fix.
71
+ - `@realitycollective/threejs-interactions` - `ThreeHitTester.hitProximity` reported the object's world scale as the hit `point`. The bounding-radius lookup reused the scratch vector holding the world position, so a box at (2, 1, -3) came back with `point: [1, 1, 1]`. The core only reads the id, so targeting was unaffected; anything reading `point` from a proximity hit on three.js or XR Blocks got the wrong answer.
72
+
73
+ ### Notes
74
+
75
+ - 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.
76
+ - 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.
77
+
78
+ [0.1.1]: https://github.com/realitycollective/WebXR-Interactions/compare/v0.1.0...development
79
+ [0.1.0]: https://github.com/realitycollective/WebXR-Interactions/releases/tag/v0.1.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Reality Collective
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,66 @@
1
+ # @realitycollective/native-interactions
2
+
3
+ The native host adapter for the Reality Collective Interaction Extensions. A native app (OpenXR on Quest, CompositorServices on visionOS, or any other shell embedding a JavaScript engine such as Hermes) installs `globalThis.__rcHost`, and this package reads its `input` and `interactions` slices into the [`@realitycollective/webxr-interactions`](https://www.npmjs.com/package/@realitycollective/webxr-interactions) core.
4
+
5
+ ```sh
6
+ npm install @realitycollective/native-interactions
7
+ ```
8
+
9
+ It re-exports everything from the core, so this is the only interaction package your app needs.
10
+
11
+ ## What it binds
12
+
13
+ | Layer | Detail |
14
+ | --- | --- |
15
+ | **Input** | `__rcHost.input` - the same members as `InputProvider`: capabilities, source sampling, optional head pose, hints, haptics and presence |
16
+ | **Hit-testing** | `__rcHost.interactions.hitRay` / `hitProximity` - the native app owns the scene and physics, so targeting is entirely its answer |
17
+ | **Movement** | `__rcHost.interactions`, keyed by the target id you chose when you registered the object with the native scene |
18
+ | **No engine dependency** | Assets and rendering stay in the native app; only numbers, strings, booleans and tuples cross the boundary |
19
+
20
+ ## Usage
21
+
22
+ One call, the same shape as `createBabylonInteractions` and `createThreeInteractions`:
23
+
24
+ ```ts
25
+ import { createNativeInteractions } from "@realitycollective/native-interactions";
26
+
27
+ // Reads globalThis.__rcHost.input / .interactions when no slice is passed in,
28
+ // and drives update(dt) from the app's own frame callback, __rcHost.onFrame.
29
+ const interactions = createNativeInteractions({ attachToHost: true });
30
+
31
+ interactions.register({ id: "button", behaviours: [{ kind: "press" }] });
32
+ interactions.runtime.onEvent((event) => console.log(event.type));
33
+ ```
34
+
35
+ The native app owns the scene, so an interactable is registered by id alone. The app answers hit queries and pose reads for that id through the `interactions` slice. Without `attachToHost`, call `interactions.update(dtSeconds)` from your own loop. `dispose()` detaches from the frame callback and disposes the runtime.
36
+
37
+ The provider, hit tester and transform port are exported too, as on every adapter, for an app that composes the runtime itself.
38
+
39
+ Pass the slices directly, typically in a test or when your host object is not on `globalThis`:
40
+
41
+ ```ts
42
+ const provider = new NativeInputProvider({ input: myInputSlice });
43
+ const hitTester = new NativeHitTester({ interactions: myInteractionSlice });
44
+ const port = new NativeTransformPort("button", { interactions: myInteractionSlice });
45
+ ```
46
+
47
+ A missing slice - neither passed in nor found on `globalThis.__rcHost` - throws one clear error naming it, at construction, rather than failing on the first call that needed it.
48
+
49
+ ## Things to know
50
+
51
+ - **Copies, not references.** Every snapshot `sample()` returns, and every tuple inside it, is copied before it leaves `NativeInputProvider`, so a host that reuses its own sample buffers cannot change a snapshot your app is still holding. The same applies to hit results and to poses read through `NativeTransformPort`.
52
+ - **Optional members are honest.** `getHeadPose`, `sampleHints`, `pulse`, `setPresenceVisible` and `setPresenceModality` on `NativeInputProvider`, and `setWorldPose`/`setEffect` on `NativeTransformPort`, are only ever present when the host slice itself carries the matching member - never a method that silently no-ops.
53
+ - **One port per interactable.** `TransformPort` is per object; the native host is one object serving every registered interactable, so `NativeTransformPort` binds one `targetId` onto it, the same idea as an engine adapter's port binding to one scene node.
54
+ - **`NativeHit.targetId`** is renamed to the core's `interactableId` on the way through `NativeHitTester` - the native host names the thing it hit, the core names the thing it manages.
55
+
56
+ ## Peer dependency
57
+
58
+ None. No engine object crosses the boundary in either direction.
59
+
60
+ ## Documentation
61
+
62
+ See the [repository README](https://github.com/realitycollective/WebXR-Interactions#readme).
63
+
64
+ ## License
65
+
66
+ MIT - see [LICENSE](./LICENSE).
@@ -0,0 +1,20 @@
1
+ /**
2
+ * NativeHitTester - resolves the core's ray and proximity queries against
3
+ * the native host's `interactions` slice. The native app owns the scene and
4
+ * physics, so targeting is entirely the host's answer; this class only
5
+ * copies the result across and renames its `targetId` to the core's
6
+ * `interactableId`.
7
+ */
8
+ import type { RayTuple, Vec3Tuple } from "@realitycollective/webxr-input";
9
+ import type { HitTester, InteractableHit } from "@realitycollective/webxr-interactions";
10
+ import { type NativeInteractionHost } from "./native-types.js";
11
+ export interface NativeHitTesterOptions {
12
+ /** The `interactions` slice. Omit to read `globalThis.__rcHost.interactions`. */
13
+ interactions?: NativeInteractionHost;
14
+ }
15
+ export declare class NativeHitTester implements HitTester {
16
+ private readonly host;
17
+ constructor(options?: NativeHitTesterOptions);
18
+ hitRay(ray: RayTuple): InteractableHit | null;
19
+ hitProximity(point: Vec3Tuple, radius: number): InteractableHit | null;
20
+ }
@@ -0,0 +1,19 @@
1
+ import { copyRay, copyVec3, resolveHostSlice, } from "./native-types.js";
2
+ export class NativeHitTester {
3
+ host;
4
+ constructor(options = {}) {
5
+ this.host = resolveHostSlice("interactions", options.interactions);
6
+ }
7
+ hitRay(ray) {
8
+ const hit = this.host.hitRay(copyRay(ray));
9
+ return hit ? toInteractableHit(hit) : null;
10
+ }
11
+ hitProximity(point, radius) {
12
+ const hit = this.host.hitProximity(copyVec3(point), radius);
13
+ return hit ? toInteractableHit(hit) : null;
14
+ }
15
+ }
16
+ function toInteractableHit(hit) {
17
+ return { interactableId: hit.targetId, distance: hit.distance, point: copyVec3(hit.point) };
18
+ }
19
+ //# sourceMappingURL=hit-tester.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hit-tester.js","sourceRoot":"","sources":["../src/hit-tester.ts"],"names":[],"mappings":"AASA,OAAO,EACL,OAAO,EACP,QAAQ,EACR,gBAAgB,GAGjB,MAAM,mBAAmB,CAAC;AAO3B,MAAM,OAAO,eAAe;IACT,IAAI,CAAwB;IAE7C,YAAY,UAAkC,EAAE;QAC9C,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC,cAAc,EAAE,OAAO,CAAC,YAAY,CAAC,CAAC;IACrE,CAAC;IAED,MAAM,CAAC,GAAa;QAClB,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC;QAC3C,OAAO,GAAG,CAAC,CAAC,CAAC,iBAAiB,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAC7C,CAAC;IAED,YAAY,CAAC,KAAgB,EAAE,MAAc;QAC3C,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,CAAC;QAC5D,OAAO,GAAG,CAAC,CAAC,CAAC,iBAAiB,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAC7C,CAAC;CACF;AAED,SAAS,iBAAiB,CAAC,GAAc;IACvC,OAAO,EAAE,cAAc,EAAE,GAAG,CAAC,QAAQ,EAAE,QAAQ,EAAE,GAAG,CAAC,QAAQ,EAAE,KAAK,EAAE,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;AAC9F,CAAC","sourcesContent":["/**\n * NativeHitTester - resolves the core's ray and proximity queries against\n * the native host's `interactions` slice. The native app owns the scene and\n * physics, so targeting is entirely the host's answer; this class only\n * copies the result across and renames its `targetId` to the core's\n * `interactableId`.\n */\nimport type { RayTuple, Vec3Tuple } from \"@realitycollective/webxr-input\";\nimport type { HitTester, InteractableHit } from \"@realitycollective/webxr-interactions\";\nimport {\n copyRay,\n copyVec3,\n resolveHostSlice,\n type NativeHit,\n type NativeInteractionHost,\n} from \"./native-types.js\";\n\nexport interface NativeHitTesterOptions {\n /** The `interactions` slice. Omit to read `globalThis.__rcHost.interactions`. */\n interactions?: NativeInteractionHost;\n}\n\nexport class NativeHitTester implements HitTester {\n private readonly host: NativeInteractionHost;\n\n constructor(options: NativeHitTesterOptions = {}) {\n this.host = resolveHostSlice(\"interactions\", options.interactions);\n }\n\n hitRay(ray: RayTuple): InteractableHit | null {\n const hit = this.host.hitRay(copyRay(ray));\n return hit ? toInteractableHit(hit) : null;\n }\n\n hitProximity(point: Vec3Tuple, radius: number): InteractableHit | null {\n const hit = this.host.hitProximity(copyVec3(point), radius);\n return hit ? toInteractableHit(hit) : null;\n }\n}\n\nfunction toInteractableHit(hit: NativeHit): InteractableHit {\n return { interactableId: hit.targetId, distance: hit.distance, point: copyVec3(hit.point) };\n}\n"]}
package/dist/host.d.ts ADDED
@@ -0,0 +1,48 @@
1
+ /**
2
+ * createNativeInteractions - one-call setup for the native adapter, with the
3
+ * same shape as the Babylon, three.js, IWSDK and XR Blocks setups.
4
+ *
5
+ * ```ts
6
+ * const interactions = createNativeInteractions({ attachToHost: true });
7
+ * interactions.register({ id: "button", behaviours: [{ kind: "press" }] });
8
+ * interactions.runtime.onEvent((event) => console.log(event.type));
9
+ * ```
10
+ *
11
+ * The native app owns the scene, so an interactable is registered by id
12
+ * alone: the app answers hit queries and pose reads for that id through the
13
+ * `interactions` slice. With `attachToHost` the loop drives itself from the
14
+ * app's own frame callback. Without it, call `update(dt)` yourself with
15
+ * seconds.
16
+ */
17
+ import { InteractionRuntime, type DwellConfig, type InteractableDescriptor } from "@realitycollective/webxr-interactions";
18
+ import { NativeHitTester, type NativeHitTesterOptions } from "./hit-tester.js";
19
+ import { NativeInputProvider, type NativeInputProviderOptions } from "./provider.js";
20
+ import { NativeTransformPort } from "./transform-port.js";
21
+ import { type NativeFrameSource } from "./native-types.js";
22
+ export interface NativeInteractionsOptions extends NativeInputProviderOptions, NativeHitTesterOptions {
23
+ dwellDefaults?: DwellConfig;
24
+ /**
25
+ * Drive `update(dt)` from the native app's frame callback. Default false -
26
+ * the app calls `update` from its own loop.
27
+ */
28
+ attachToHost?: boolean;
29
+ /** Where frames come from for `attachToHost`. Omit to use `globalThis.__rcHost`. */
30
+ frames?: NativeFrameSource;
31
+ }
32
+ export declare class NativeInteractions {
33
+ readonly runtime: InteractionRuntime;
34
+ readonly provider: NativeInputProvider;
35
+ readonly hitTester: NativeHitTester;
36
+ private readonly ports;
37
+ private readonly options;
38
+ private detachHost;
39
+ constructor(options?: NativeInteractionsOptions);
40
+ /** Register an interactable. The native app knows it by `descriptor.id`. */
41
+ register(descriptor: InteractableDescriptor): NativeTransformPort;
42
+ unregister(id: string): void;
43
+ getPort(id: string): NativeTransformPort | undefined;
44
+ update(dt: number): void;
45
+ dispose(): void;
46
+ private attachToHost;
47
+ }
48
+ export declare function createNativeInteractions(options?: NativeInteractionsOptions): NativeInteractions;
package/dist/host.js ADDED
@@ -0,0 +1,80 @@
1
+ /**
2
+ * createNativeInteractions - one-call setup for the native adapter, with the
3
+ * same shape as the Babylon, three.js, IWSDK and XR Blocks setups.
4
+ *
5
+ * ```ts
6
+ * const interactions = createNativeInteractions({ attachToHost: true });
7
+ * interactions.register({ id: "button", behaviours: [{ kind: "press" }] });
8
+ * interactions.runtime.onEvent((event) => console.log(event.type));
9
+ * ```
10
+ *
11
+ * The native app owns the scene, so an interactable is registered by id
12
+ * alone: the app answers hit queries and pose reads for that id through the
13
+ * `interactions` slice. With `attachToHost` the loop drives itself from the
14
+ * app's own frame callback. Without it, call `update(dt)` yourself with
15
+ * seconds.
16
+ */
17
+ import { InteractionRuntime, } from "@realitycollective/webxr-interactions";
18
+ import { NativeHitTester } from "./hit-tester.js";
19
+ import { NativeInputProvider } from "./provider.js";
20
+ import { NativeTransformPort } from "./transform-port.js";
21
+ import { installedHost } from "./native-types.js";
22
+ /** Longest frame the host-attached loop will report, in seconds. */
23
+ const MAX_FRAME_SECONDS = 0.1;
24
+ export class NativeInteractions {
25
+ runtime;
26
+ provider;
27
+ hitTester;
28
+ ports = new Map();
29
+ options;
30
+ detachHost = null;
31
+ constructor(options = {}) {
32
+ this.options = options;
33
+ this.provider = new NativeInputProvider(options);
34
+ this.hitTester = new NativeHitTester(options);
35
+ this.runtime = new InteractionRuntime({
36
+ provider: this.provider,
37
+ hitTester: this.hitTester,
38
+ ...(options.dwellDefaults ? { dwellDefaults: options.dwellDefaults } : {}),
39
+ });
40
+ if (options.attachToHost)
41
+ this.attachToHost(options);
42
+ }
43
+ /** Register an interactable. The native app knows it by `descriptor.id`. */
44
+ register(descriptor) {
45
+ const port = new NativeTransformPort(descriptor.id, this.options.interactions ? { interactions: this.options.interactions } : {});
46
+ this.ports.set(descriptor.id, port);
47
+ this.runtime.registerInteractable(descriptor, { transform: port });
48
+ return port;
49
+ }
50
+ unregister(id) {
51
+ this.runtime.unregisterInteractable(id);
52
+ this.ports.delete(id);
53
+ }
54
+ getPort(id) {
55
+ return this.ports.get(id);
56
+ }
57
+ update(dt) {
58
+ this.runtime.update(dt);
59
+ }
60
+ dispose() {
61
+ this.detachHost?.();
62
+ this.detachHost = null;
63
+ this.runtime.dispose();
64
+ }
65
+ attachToHost(options) {
66
+ const frames = options.frames ?? installedHost();
67
+ if (!frames?.onFrame) {
68
+ throw new Error("@realitycollective/native-interactions: attachToHost needs a frame source, and globalThis.__rcHost.onFrame is not installed. Pass `frames`, or call update(dt) yourself.");
69
+ }
70
+ // The app reports the frame delta in seconds. A long frame is clamped so
71
+ // behaviours integrate over a sane step rather than jumping.
72
+ this.detachHost = frames.onFrame((_timestampMs, deltaS) => {
73
+ this.update(Math.min(MAX_FRAME_SECONDS, Math.max(0, deltaS)));
74
+ });
75
+ }
76
+ }
77
+ export function createNativeInteractions(options = {}) {
78
+ return new NativeInteractions(options);
79
+ }
80
+ //# sourceMappingURL=host.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"host.js","sourceRoot":"","sources":["../src/host.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EACL,kBAAkB,GAGnB,MAAM,uCAAuC,CAAC;AAC/C,OAAO,EAAE,eAAe,EAA+B,MAAM,iBAAiB,CAAC;AAC/E,OAAO,EAAE,mBAAmB,EAAmC,MAAM,eAAe,CAAC;AACrF,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,EAAE,aAAa,EAA0B,MAAM,mBAAmB,CAAC;AAe1E,oEAAoE;AACpE,MAAM,iBAAiB,GAAG,GAAG,CAAC;AAE9B,MAAM,OAAO,kBAAkB;IACpB,OAAO,CAAqB;IAC5B,QAAQ,CAAsB;IAC9B,SAAS,CAAkB;IACnB,KAAK,GAAG,IAAI,GAAG,EAA+B,CAAC;IAC/C,OAAO,CAA4B;IAC5C,UAAU,GAAwB,IAAI,CAAC;IAE/C,YAAY,UAAqC,EAAE;QACjD,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,QAAQ,GAAG,IAAI,mBAAmB,CAAC,OAAO,CAAC,CAAC;QACjD,IAAI,CAAC,SAAS,GAAG,IAAI,eAAe,CAAC,OAAO,CAAC,CAAC;QAC9C,IAAI,CAAC,OAAO,GAAG,IAAI,kBAAkB,CAAC;YACpC,QAAQ,EAAE,IAAI,CAAC,QAAQ;YACvB,SAAS,EAAE,IAAI,CAAC,SAAS;YACzB,GAAG,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,OAAO,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAC3E,CAAC,CAAC;QACH,IAAI,OAAO,CAAC,YAAY;YAAE,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;IACvD,CAAC;IAED,4EAA4E;IAC5E,QAAQ,CAAC,UAAkC;QACzC,MAAM,IAAI,GAAG,IAAI,mBAAmB,CAClC,UAAU,CAAC,EAAE,EACb,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,YAAY,EAAE,IAAI,CAAC,OAAO,CAAC,YAAY,EAAE,CAAC,CAAC,CAAC,EAAE,CAC7E,CAAC;QACF,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;QACpC,IAAI,CAAC,OAAO,CAAC,oBAAoB,CAAC,UAAU,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACnE,OAAO,IAAI,CAAC;IACd,CAAC;IAED,UAAU,CAAC,EAAU;QACnB,IAAI,CAAC,OAAO,CAAC,sBAAsB,CAAC,EAAE,CAAC,CAAC;QACxC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IACxB,CAAC;IAED,OAAO,CAAC,EAAU;QAChB,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAC5B,CAAC;IAED,MAAM,CAAC,EAAU;QACf,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IAC1B,CAAC;IAED,OAAO;QACL,IAAI,CAAC,UAAU,EAAE,EAAE,CAAC;QACpB,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC;QACvB,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;IACzB,CAAC;IAEO,YAAY,CAAC,OAAkC;QACrD,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,aAAa,EAAE,CAAC;QACjD,IAAI,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC;YACrB,MAAM,IAAI,KAAK,CACb,0KAA0K,CAC3K,CAAC;QACJ,CAAC;QACD,yEAAyE;QACzE,6DAA6D;QAC7D,IAAI,CAAC,UAAU,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC,YAAY,EAAE,MAAM,EAAE,EAAE;YACxD,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,iBAAiB,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC;QAChE,CAAC,CAAC,CAAC;IACL,CAAC;CACF;AAED,MAAM,UAAU,wBAAwB,CACtC,UAAqC,EAAE;IAEvC,OAAO,IAAI,kBAAkB,CAAC,OAAO,CAAC,CAAC;AACzC,CAAC","sourcesContent":["/**\n * createNativeInteractions - one-call setup for the native adapter, with the\n * same shape as the Babylon, three.js, IWSDK and XR Blocks setups.\n *\n * ```ts\n * const interactions = createNativeInteractions({ attachToHost: true });\n * interactions.register({ id: \"button\", behaviours: [{ kind: \"press\" }] });\n * interactions.runtime.onEvent((event) => console.log(event.type));\n * ```\n *\n * The native app owns the scene, so an interactable is registered by id\n * alone: the app answers hit queries and pose reads for that id through the\n * `interactions` slice. With `attachToHost` the loop drives itself from the\n * app's own frame callback. Without it, call `update(dt)` yourself with\n * seconds.\n */\nimport {\n InteractionRuntime,\n type DwellConfig,\n type InteractableDescriptor,\n} from \"@realitycollective/webxr-interactions\";\nimport { NativeHitTester, type NativeHitTesterOptions } from \"./hit-tester.js\";\nimport { NativeInputProvider, type NativeInputProviderOptions } from \"./provider.js\";\nimport { NativeTransformPort } from \"./transform-port.js\";\nimport { installedHost, type NativeFrameSource } from \"./native-types.js\";\n\nexport interface NativeInteractionsOptions\n extends NativeInputProviderOptions,\n NativeHitTesterOptions {\n dwellDefaults?: DwellConfig;\n /**\n * Drive `update(dt)` from the native app's frame callback. Default false -\n * the app calls `update` from its own loop.\n */\n attachToHost?: boolean;\n /** Where frames come from for `attachToHost`. Omit to use `globalThis.__rcHost`. */\n frames?: NativeFrameSource;\n}\n\n/** Longest frame the host-attached loop will report, in seconds. */\nconst MAX_FRAME_SECONDS = 0.1;\n\nexport class NativeInteractions {\n readonly runtime: InteractionRuntime;\n readonly provider: NativeInputProvider;\n readonly hitTester: NativeHitTester;\n private readonly ports = new Map<string, NativeTransformPort>();\n private readonly options: NativeInteractionsOptions;\n private detachHost: (() => void) | null = null;\n\n constructor(options: NativeInteractionsOptions = {}) {\n this.options = options;\n this.provider = new NativeInputProvider(options);\n this.hitTester = new NativeHitTester(options);\n this.runtime = new InteractionRuntime({\n provider: this.provider,\n hitTester: this.hitTester,\n ...(options.dwellDefaults ? { dwellDefaults: options.dwellDefaults } : {}),\n });\n if (options.attachToHost) this.attachToHost(options);\n }\n\n /** Register an interactable. The native app knows it by `descriptor.id`. */\n register(descriptor: InteractableDescriptor): NativeTransformPort {\n const port = new NativeTransformPort(\n descriptor.id,\n this.options.interactions ? { interactions: this.options.interactions } : {},\n );\n this.ports.set(descriptor.id, port);\n this.runtime.registerInteractable(descriptor, { transform: port });\n return port;\n }\n\n unregister(id: string): void {\n this.runtime.unregisterInteractable(id);\n this.ports.delete(id);\n }\n\n getPort(id: string): NativeTransformPort | undefined {\n return this.ports.get(id);\n }\n\n update(dt: number): void {\n this.runtime.update(dt);\n }\n\n dispose(): void {\n this.detachHost?.();\n this.detachHost = null;\n this.runtime.dispose();\n }\n\n private attachToHost(options: NativeInteractionsOptions): void {\n const frames = options.frames ?? installedHost();\n if (!frames?.onFrame) {\n throw new Error(\n \"@realitycollective/native-interactions: attachToHost needs a frame source, and globalThis.__rcHost.onFrame is not installed. Pass `frames`, or call update(dt) yourself.\",\n );\n }\n // The app reports the frame delta in seconds. A long frame is clamped so\n // behaviours integrate over a sane step rather than jumping.\n this.detachHost = frames.onFrame((_timestampMs, deltaS) => {\n this.update(Math.min(MAX_FRAME_SECONDS, Math.max(0, deltaS)));\n });\n }\n}\n\nexport function createNativeInteractions(\n options: NativeInteractionsOptions = {},\n): NativeInteractions {\n return new NativeInteractions(options);\n}\n"]}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Public entry point of the native adapter: the same kinds of export as the
3
+ * Babylon, three.js, IWSDK and XR Blocks adapters. The core is re-exported;
4
+ * the provider, hit tester and transform port implement its contracts; the
5
+ * setup entry point wires them together; and the slice types describe what
6
+ * the native app installs on `globalThis.__rcHost`. Slice reading and tuple
7
+ * copying stay internal.
8
+ */
9
+ export * from "@realitycollective/webxr-interactions";
10
+ export type { NativeFrameSource, NativeHit, NativeHostSlices, NativeInputHost, NativeInteractionHost, } from "./native-types.js";
11
+ export * from "./provider.js";
12
+ export * from "./hit-tester.js";
13
+ export * from "./transform-port.js";
14
+ export * from "./host.js";
package/dist/index.js ADDED
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Public entry point of the native adapter: the same kinds of export as the
3
+ * Babylon, three.js, IWSDK and XR Blocks adapters. The core is re-exported;
4
+ * the provider, hit tester and transform port implement its contracts; the
5
+ * setup entry point wires them together; and the slice types describe what
6
+ * the native app installs on `globalThis.__rcHost`. Slice reading and tuple
7
+ * copying stay internal.
8
+ */
9
+ export * from "@realitycollective/webxr-interactions";
10
+ export * from "./provider.js";
11
+ export * from "./hit-tester.js";
12
+ export * from "./transform-port.js";
13
+ export * from "./host.js";
14
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,cAAc,uCAAuC,CAAC;AAQtD,cAAc,eAAe,CAAC;AAC9B,cAAc,iBAAiB,CAAC;AAChC,cAAc,qBAAqB,CAAC;AACpC,cAAc,WAAW,CAAC","sourcesContent":["/**\n * Public entry point of the native adapter: the same kinds of export as the\n * Babylon, three.js, IWSDK and XR Blocks adapters. The core is re-exported;\n * the provider, hit tester and transform port implement its contracts; the\n * setup entry point wires them together; and the slice types describe what\n * the native app installs on `globalThis.__rcHost`. Slice reading and tuple\n * copying stay internal.\n */\nexport * from \"@realitycollective/webxr-interactions\";\nexport type {\n NativeFrameSource,\n NativeHit,\n NativeHostSlices,\n NativeInputHost,\n NativeInteractionHost,\n} from \"./native-types.js\";\nexport * from \"./provider.js\";\nexport * from \"./hit-tester.js\";\nexport * from \"./transform-port.js\";\nexport * from \"./host.js\";\n"]}
@@ -0,0 +1,106 @@
1
+ /**
2
+ * The `input` and `interactions` slices a native host (OpenXR on Quest,
3
+ * CompositorServices on visionOS, or any other shell embedding a JavaScript
4
+ * engine such as Hermes) installs on `globalThis.__rcHost`.
5
+ *
6
+ * These are NOT new contracts: `NativeInputHost` is the exact shape of
7
+ * `InputProvider` from `@realitycollective/webxr-input`, and
8
+ * `NativeInteractionHost` is `HitTester` plus `TransformPort` from
9
+ * `@realitycollective/webxr-interactions/ports`, with a `targetId` added to
10
+ * every `TransformPort` member because that port is per object and the host
11
+ * is one object serving every registered interactable. Declaring them again
12
+ * here, rather than reusing the originals by reference, is deliberate: this
13
+ * file is the one place that states what crosses the native boundary, kept
14
+ * in the same structural-typing style as `babylon-types.ts` and the XR
15
+ * Blocks `XB*Like` types, so it reads correctly even without the other two
16
+ * packages open.
17
+ *
18
+ * Values that cross are plain: numbers, strings, booleans and tuples. No
19
+ * engine objects in either direction, and assets stay entirely on the
20
+ * native side.
21
+ */
22
+ import type { Handedness, HeadPose, InputCapabilities, InputHitHint, InputSourceSnapshot, PoseTuple, PresenceModality, QuatTuple, RayTuple, Unsubscribe, Vec3Tuple } from "@realitycollective/webxr-input";
23
+ /** The native host's `input` slice. Mirrors `InputProvider` member for member. */
24
+ export interface NativeInputHost {
25
+ getCapabilities(): InputCapabilities;
26
+ onCapabilitiesChanged(listener: (capabilities: InputCapabilities) => void): Unsubscribe;
27
+ onSourcesChanged(listener: () => void): Unsubscribe;
28
+ sample(): readonly InputSourceSnapshot[];
29
+ getHeadPose?(): HeadPose;
30
+ sampleHints?(): readonly InputHitHint[];
31
+ pulse?(sourceId: string, intensity: number, durationMs: number): boolean;
32
+ setPresenceVisible?(target: Handedness | "all", visible: boolean): boolean;
33
+ setPresenceModality?(mode: PresenceModality): boolean;
34
+ }
35
+ /** What the host's ray/proximity query reports: the target it reached, if any. */
36
+ export interface NativeHit {
37
+ /** The target id the app registered with `NativeTransformPort`/its own scene. */
38
+ targetId: string;
39
+ /** Distance from the query origin (ray origin / probe point). */
40
+ distance: number;
41
+ /** World-space hit or closest point. */
42
+ point: Vec3Tuple;
43
+ }
44
+ /**
45
+ * The native host's `interactions` slice: `HitTester` unchanged, and
46
+ * `TransformPort` with every member keyed by the target id the app chose
47
+ * when it registered the object with the native scene, because one host
48
+ * object serves every interactable rather than one port per object.
49
+ */
50
+ export interface NativeInteractionHost {
51
+ hitRay(ray: RayTuple): NativeHit | null;
52
+ hitProximity(point: Vec3Tuple, radius: number): NativeHit | null;
53
+ /** Where the object is now. */
54
+ getWorldPose(targetId: string): PoseTuple;
55
+ /** The rest pose captured at registration, in world space. */
56
+ getRestWorldPose(targetId: string): PoseTuple;
57
+ getLocalOffset(targetId: string): Vec3Tuple;
58
+ setLocalOffset(targetId: string, offset: Vec3Tuple): void;
59
+ setLocalRotation(targetId: string, quaternion: QuatTuple): void;
60
+ setWorldPose?(targetId: string, pose: PoseTuple): void;
61
+ setEffect?(targetId: string, effect: {
62
+ scale?: number;
63
+ emissive?: number;
64
+ }): void;
65
+ }
66
+ /**
67
+ * The native app's frame callback, the root member of `__rcHost` that
68
+ * `NativeInteractions` attaches to when `attachToHost` is set.
69
+ */
70
+ export interface NativeFrameSource {
71
+ onFrame(callback: (timestampMs: number, deltaS: number) => void): () => void;
72
+ }
73
+ /** The two slices this package reads off `globalThis.__rcHost`. */
74
+ export interface NativeHostSlices {
75
+ input: NativeInputHost;
76
+ interactions: NativeInteractionHost;
77
+ }
78
+ /**
79
+ * `globalThis.__rcHost`, read defensively: the root `NativeHost` interface
80
+ * belongs to `service-framework-native`, which this package does not depend
81
+ * on, so the global is read as an unknown bag of optional slices rather than
82
+ * imported.
83
+ */
84
+ export declare function installedHost(): (Partial<NativeHostSlices> & Partial<NativeFrameSource>) | undefined;
85
+ /**
86
+ * Resolve one slice: the value passed in, or `globalThis.__rcHost`'s slice
87
+ * of the same name. Throws one clear error naming the missing slice, so a
88
+ * native-interactions class fails at construction rather than the first
89
+ * time something calls a method that is not there.
90
+ */
91
+ export declare function resolveHostSlice<K extends keyof NativeHostSlices>(name: K, injected: NativeHostSlices[K] | undefined): NativeHostSlices[K];
92
+ /** Copy a position tuple. */
93
+ export declare function copyVec3(v: Vec3Tuple): Vec3Tuple;
94
+ /** Copy an orientation tuple. */
95
+ export declare function copyQuat(q: QuatTuple): QuatTuple;
96
+ /** Copy a pose (position + orientation). */
97
+ export declare function copyPose(pose: PoseTuple): PoseTuple;
98
+ /** Copy a ray (origin + direction). */
99
+ export declare function copyRay(ray: RayTuple): RayTuple;
100
+ /**
101
+ * Copy one `InputSourceSnapshot` field by field, including every tuple it
102
+ * carries, so a host that pools and refills its own snapshot objects still
103
+ * meets the ownership rule: a snapshot handed to `sample()`'s caller is
104
+ * never written to again.
105
+ */
106
+ export declare function copySnapshot(source: InputSourceSnapshot): InputSourceSnapshot;
@@ -0,0 +1,73 @@
1
+ /**
2
+ * `globalThis.__rcHost`, read defensively: the root `NativeHost` interface
3
+ * belongs to `service-framework-native`, which this package does not depend
4
+ * on, so the global is read as an unknown bag of optional slices rather than
5
+ * imported.
6
+ */
7
+ export function installedHost() {
8
+ return globalThis.__rcHost;
9
+ }
10
+ /**
11
+ * Resolve one slice: the value passed in, or `globalThis.__rcHost`'s slice
12
+ * of the same name. Throws one clear error naming the missing slice, so a
13
+ * native-interactions class fails at construction rather than the first
14
+ * time something calls a method that is not there.
15
+ */
16
+ export function resolveHostSlice(name, injected) {
17
+ const slice = injected ?? installedHost()?.[name];
18
+ if (!slice) {
19
+ throw new Error(`@realitycollective/native-interactions: no "${name}" slice was supplied and globalThis.__rcHost.${name} is not installed. Pass one directly, or have the native app install it before this package is constructed.`);
20
+ }
21
+ return slice;
22
+ }
23
+ // ---------------------------------------------------------------------------
24
+ // Copies. A host may reuse its own buffers across calls, so every tuple that
25
+ // crosses back into this package is copied on the way in, never referenced.
26
+ // ---------------------------------------------------------------------------
27
+ /** Copy a position tuple. */
28
+ export function copyVec3(v) {
29
+ return [v[0], v[1], v[2]];
30
+ }
31
+ /** Copy an orientation tuple. */
32
+ export function copyQuat(q) {
33
+ return [q[0], q[1], q[2], q[3]];
34
+ }
35
+ /** Copy a pose (position + orientation). */
36
+ export function copyPose(pose) {
37
+ return { position: copyVec3(pose.position), quaternion: copyQuat(pose.quaternion) };
38
+ }
39
+ /** Copy a ray (origin + direction). */
40
+ export function copyRay(ray) {
41
+ return { origin: copyVec3(ray.origin), direction: copyVec3(ray.direction) };
42
+ }
43
+ /**
44
+ * Copy one `InputSourceSnapshot` field by field, including every tuple it
45
+ * carries, so a host that pools and refills its own snapshot objects still
46
+ * meets the ownership rule: a snapshot handed to `sample()`'s caller is
47
+ * never written to again.
48
+ */
49
+ export function copySnapshot(source) {
50
+ const copy = {
51
+ id: source.id,
52
+ kind: source.kind,
53
+ handedness: source.handedness,
54
+ select: source.select,
55
+ squeeze: source.squeeze,
56
+ };
57
+ if (source.ray)
58
+ copy.ray = copyRay(source.ray);
59
+ if (source.gripPose)
60
+ copy.gripPose = copyPose(source.gripPose);
61
+ if (source.indexTip)
62
+ copy.indexTip = copyVec3(source.indexTip);
63
+ if (source.linearVelocity)
64
+ copy.linearVelocity = copyVec3(source.linearVelocity);
65
+ if (source.angularVelocity)
66
+ copy.angularVelocity = copyVec3(source.angularVelocity);
67
+ if (source.nativeGrabbing !== undefined)
68
+ copy.nativeGrabbing = source.nativeGrabbing;
69
+ if (source.hapticsAvailable !== undefined)
70
+ copy.hapticsAvailable = source.hapticsAvailable;
71
+ return copy;
72
+ }
73
+ //# sourceMappingURL=native-types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"native-types.js","sourceRoot":"","sources":["../src/native-types.ts"],"names":[],"mappings":"AA4FA;;;;;GAKG;AACH,MAAM,UAAU,aAAa;IAC3B,OAAQ,UAAoF,CAAC,QAAQ,CAAC;AACxG,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAC9B,IAAO,EACP,QAAyC;IAEzC,MAAM,KAAK,GAAG,QAAQ,IAAK,aAAa,EAA4C,EAAE,CAAC,IAAI,CAAC,CAAC;IAC7F,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CACb,+CAA+C,IAAI,gDAAgD,IAAI,6GAA6G,CACrN,CAAC;IACJ,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,8EAA8E;AAC9E,6EAA6E;AAC7E,4EAA4E;AAC5E,8EAA8E;AAE9E,6BAA6B;AAC7B,MAAM,UAAU,QAAQ,CAAC,CAAY;IACnC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAC5B,CAAC;AAED,iCAAiC;AACjC,MAAM,UAAU,QAAQ,CAAC,CAAY;IACnC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAClC,CAAC;AAED,4CAA4C;AAC5C,MAAM,UAAU,QAAQ,CAAC,IAAe;IACtC,OAAO,EAAE,QAAQ,EAAE,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,UAAU,EAAE,QAAQ,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;AACtF,CAAC;AAED,uCAAuC;AACvC,MAAM,UAAU,OAAO,CAAC,GAAa;IACnC,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,SAAS,EAAE,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC;AAC9E,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAAC,MAA2B;IACtD,MAAM,IAAI,GAAwB;QAChC,EAAE,EAAE,MAAM,CAAC,EAAE;QACb,IAAI,EAAE,MAAM,CAAC,IAAI;QACjB,UAAU,EAAE,MAAM,CAAC,UAAU;QAC7B,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,OAAO,EAAE,MAAM,CAAC,OAAO;KACxB,CAAC;IACF,IAAI,MAAM,CAAC,GAAG;QAAE,IAAI,CAAC,GAAG,GAAG,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAC/C,IAAI,MAAM,CAAC,QAAQ;QAAE,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IAC/D,IAAI,MAAM,CAAC,QAAQ;QAAE,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IAC/D,IAAI,MAAM,CAAC,cAAc;QAAE,IAAI,CAAC,cAAc,GAAG,QAAQ,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC;IACjF,IAAI,MAAM,CAAC,eAAe;QAAE,IAAI,CAAC,eAAe,GAAG,QAAQ,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC;IACpF,IAAI,MAAM,CAAC,cAAc,KAAK,SAAS;QAAE,IAAI,CAAC,cAAc,GAAG,MAAM,CAAC,cAAc,CAAC;IACrF,IAAI,MAAM,CAAC,gBAAgB,KAAK,SAAS;QAAE,IAAI,CAAC,gBAAgB,GAAG,MAAM,CAAC,gBAAgB,CAAC;IAC3F,OAAO,IAAI,CAAC;AACd,CAAC","sourcesContent":["/**\n * The `input` and `interactions` slices a native host (OpenXR on Quest,\n * CompositorServices on visionOS, or any other shell embedding a JavaScript\n * engine such as Hermes) installs on `globalThis.__rcHost`.\n *\n * These are NOT new contracts: `NativeInputHost` is the exact shape of\n * `InputProvider` from `@realitycollective/webxr-input`, and\n * `NativeInteractionHost` is `HitTester` plus `TransformPort` from\n * `@realitycollective/webxr-interactions/ports`, with a `targetId` added to\n * every `TransformPort` member because that port is per object and the host\n * is one object serving every registered interactable. Declaring them again\n * here, rather than reusing the originals by reference, is deliberate: this\n * file is the one place that states what crosses the native boundary, kept\n * in the same structural-typing style as `babylon-types.ts` and the XR\n * Blocks `XB*Like` types, so it reads correctly even without the other two\n * packages open.\n *\n * Values that cross are plain: numbers, strings, booleans and tuples. No\n * engine objects in either direction, and assets stay entirely on the\n * native side.\n */\nimport type {\n Handedness,\n HeadPose,\n InputCapabilities,\n InputHitHint,\n InputSourceSnapshot,\n PoseTuple,\n PresenceModality,\n QuatTuple,\n RayTuple,\n Unsubscribe,\n Vec3Tuple,\n} from \"@realitycollective/webxr-input\";\n\n/** The native host's `input` slice. Mirrors `InputProvider` member for member. */\nexport interface NativeInputHost {\n getCapabilities(): InputCapabilities;\n onCapabilitiesChanged(listener: (capabilities: InputCapabilities) => void): Unsubscribe;\n onSourcesChanged(listener: () => void): Unsubscribe;\n sample(): readonly InputSourceSnapshot[];\n getHeadPose?(): HeadPose;\n sampleHints?(): readonly InputHitHint[];\n pulse?(sourceId: string, intensity: number, durationMs: number): boolean;\n setPresenceVisible?(target: Handedness | \"all\", visible: boolean): boolean;\n setPresenceModality?(mode: PresenceModality): boolean;\n}\n\n/** What the host's ray/proximity query reports: the target it reached, if any. */\nexport interface NativeHit {\n /** The target id the app registered with `NativeTransformPort`/its own scene. */\n targetId: string;\n /** Distance from the query origin (ray origin / probe point). */\n distance: number;\n /** World-space hit or closest point. */\n point: Vec3Tuple;\n}\n\n/**\n * The native host's `interactions` slice: `HitTester` unchanged, and\n * `TransformPort` with every member keyed by the target id the app chose\n * when it registered the object with the native scene, because one host\n * object serves every interactable rather than one port per object.\n */\nexport interface NativeInteractionHost {\n hitRay(ray: RayTuple): NativeHit | null;\n hitProximity(point: Vec3Tuple, radius: number): NativeHit | null;\n /** Where the object is now. */\n getWorldPose(targetId: string): PoseTuple;\n /** The rest pose captured at registration, in world space. */\n getRestWorldPose(targetId: string): PoseTuple;\n getLocalOffset(targetId: string): Vec3Tuple;\n setLocalOffset(targetId: string, offset: Vec3Tuple): void;\n setLocalRotation(targetId: string, quaternion: QuatTuple): void;\n setWorldPose?(targetId: string, pose: PoseTuple): void;\n setEffect?(targetId: string, effect: { scale?: number; emissive?: number }): void;\n}\n\n/**\n * The native app's frame callback, the root member of `__rcHost` that\n * `NativeInteractions` attaches to when `attachToHost` is set.\n */\nexport interface NativeFrameSource {\n onFrame(callback: (timestampMs: number, deltaS: number) => void): () => void;\n}\n\n/** The two slices this package reads off `globalThis.__rcHost`. */\nexport interface NativeHostSlices {\n input: NativeInputHost;\n interactions: NativeInteractionHost;\n}\n\n/**\n * `globalThis.__rcHost`, read defensively: the root `NativeHost` interface\n * belongs to `service-framework-native`, which this package does not depend\n * on, so the global is read as an unknown bag of optional slices rather than\n * imported.\n */\nexport function installedHost(): (Partial<NativeHostSlices> & Partial<NativeFrameSource>) | undefined {\n return (globalThis as { __rcHost?: Partial<NativeHostSlices> & Partial<NativeFrameSource> }).__rcHost;\n}\n\n/**\n * Resolve one slice: the value passed in, or `globalThis.__rcHost`'s slice\n * of the same name. Throws one clear error naming the missing slice, so a\n * native-interactions class fails at construction rather than the first\n * time something calls a method that is not there.\n */\nexport function resolveHostSlice<K extends keyof NativeHostSlices>(\n name: K,\n injected: NativeHostSlices[K] | undefined,\n): NativeHostSlices[K] {\n const slice = injected ?? (installedHost() as Partial<NativeHostSlices> | undefined)?.[name];\n if (!slice) {\n throw new Error(\n `@realitycollective/native-interactions: no \"${name}\" slice was supplied and globalThis.__rcHost.${name} is not installed. Pass one directly, or have the native app install it before this package is constructed.`,\n );\n }\n return slice;\n}\n\n// ---------------------------------------------------------------------------\n// Copies. A host may reuse its own buffers across calls, so every tuple that\n// crosses back into this package is copied on the way in, never referenced.\n// ---------------------------------------------------------------------------\n\n/** Copy a position tuple. */\nexport function copyVec3(v: Vec3Tuple): Vec3Tuple {\n return [v[0], v[1], v[2]];\n}\n\n/** Copy an orientation tuple. */\nexport function copyQuat(q: QuatTuple): QuatTuple {\n return [q[0], q[1], q[2], q[3]];\n}\n\n/** Copy a pose (position + orientation). */\nexport function copyPose(pose: PoseTuple): PoseTuple {\n return { position: copyVec3(pose.position), quaternion: copyQuat(pose.quaternion) };\n}\n\n/** Copy a ray (origin + direction). */\nexport function copyRay(ray: RayTuple): RayTuple {\n return { origin: copyVec3(ray.origin), direction: copyVec3(ray.direction) };\n}\n\n/**\n * Copy one `InputSourceSnapshot` field by field, including every tuple it\n * carries, so a host that pools and refills its own snapshot objects still\n * meets the ownership rule: a snapshot handed to `sample()`'s caller is\n * never written to again.\n */\nexport function copySnapshot(source: InputSourceSnapshot): InputSourceSnapshot {\n const copy: InputSourceSnapshot = {\n id: source.id,\n kind: source.kind,\n handedness: source.handedness,\n select: source.select,\n squeeze: source.squeeze,\n };\n if (source.ray) copy.ray = copyRay(source.ray);\n if (source.gripPose) copy.gripPose = copyPose(source.gripPose);\n if (source.indexTip) copy.indexTip = copyVec3(source.indexTip);\n if (source.linearVelocity) copy.linearVelocity = copyVec3(source.linearVelocity);\n if (source.angularVelocity) copy.angularVelocity = copyVec3(source.angularVelocity);\n if (source.nativeGrabbing !== undefined) copy.nativeGrabbing = source.nativeGrabbing;\n if (source.hapticsAvailable !== undefined) copy.hapticsAvailable = source.hapticsAvailable;\n return copy;\n}\n"]}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * NativeInputProvider - the native host's input provider.
3
+ *
4
+ * A thin, copying pass-through over the `input` slice a native app (OpenXR,
5
+ * visionOS) installs on `globalThis.__rcHost`, or hands in directly for
6
+ * tests. Every snapshot, and every tuple inside it, is copied before it
7
+ * leaves this class, so a host that reuses its own sample buffers still
8
+ * meets the ownership rule the shared contract suite checks.
9
+ *
10
+ * Optional members - `getHeadPose`, `sampleHints`, `pulse`,
11
+ * `setPresenceVisible`, `setPresenceModality` - are only ever assigned when
12
+ * the host itself carries them, so `typeof provider.pulse` answers honestly
13
+ * for a host that cannot pulse rather than a method that is present but
14
+ * always returns false.
15
+ */
16
+ import { type Handedness, type HeadPose, type InputCapabilities, type InputHitHint, type InputProvider, type InputSourceSnapshot, type PresenceModality, type Unsubscribe } from "@realitycollective/webxr-input";
17
+ import { type NativeInputHost } from "./native-types.js";
18
+ export interface NativeInputProviderOptions {
19
+ /** The `input` slice. Omit to read `globalThis.__rcHost.input`. */
20
+ input?: NativeInputHost;
21
+ }
22
+ export declare class NativeInputProvider implements InputProvider {
23
+ private readonly host;
24
+ readonly getHeadPose?: () => HeadPose;
25
+ readonly sampleHints?: () => readonly InputHitHint[];
26
+ readonly pulse?: (sourceId: string, intensity: number, durationMs: number) => boolean;
27
+ readonly setPresenceVisible?: (target: Handedness | "all", visible: boolean) => boolean;
28
+ readonly setPresenceModality?: (mode: PresenceModality) => boolean;
29
+ constructor(options?: NativeInputProviderOptions);
30
+ getCapabilities(): InputCapabilities;
31
+ onCapabilitiesChanged(listener: (capabilities: InputCapabilities) => void): Unsubscribe;
32
+ onSourcesChanged(listener: () => void): Unsubscribe;
33
+ sample(): readonly InputSourceSnapshot[];
34
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * NativeInputProvider - the native host's input provider.
3
+ *
4
+ * A thin, copying pass-through over the `input` slice a native app (OpenXR,
5
+ * visionOS) installs on `globalThis.__rcHost`, or hands in directly for
6
+ * tests. Every snapshot, and every tuple inside it, is copied before it
7
+ * leaves this class, so a host that reuses its own sample buffers still
8
+ * meets the ownership rule the shared contract suite checks.
9
+ *
10
+ * Optional members - `getHeadPose`, `sampleHints`, `pulse`,
11
+ * `setPresenceVisible`, `setPresenceModality` - are only ever assigned when
12
+ * the host itself carries them, so `typeof provider.pulse` answers honestly
13
+ * for a host that cannot pulse rather than a method that is present but
14
+ * always returns false.
15
+ */
16
+ import {} from "@realitycollective/webxr-input";
17
+ import { copyPose, copySnapshot, resolveHostSlice } from "./native-types.js";
18
+ export class NativeInputProvider {
19
+ host;
20
+ getHeadPose;
21
+ sampleHints;
22
+ pulse;
23
+ setPresenceVisible;
24
+ setPresenceModality;
25
+ constructor(options = {}) {
26
+ this.host = resolveHostSlice("input", options.input);
27
+ if (this.host.getHeadPose) {
28
+ const host = this.host;
29
+ this.getHeadPose = () => copyPose(host.getHeadPose());
30
+ }
31
+ if (this.host.sampleHints) {
32
+ const host = this.host;
33
+ this.sampleHints = () => host.sampleHints().map((hint) => ({ ...hint }));
34
+ }
35
+ if (this.host.pulse) {
36
+ const host = this.host;
37
+ this.pulse = (sourceId, intensity, durationMs) => host.pulse(sourceId, intensity, durationMs);
38
+ }
39
+ if (this.host.setPresenceVisible) {
40
+ const host = this.host;
41
+ this.setPresenceVisible = (target, visible) => host.setPresenceVisible(target, visible);
42
+ }
43
+ if (this.host.setPresenceModality) {
44
+ const host = this.host;
45
+ this.setPresenceModality = (mode) => host.setPresenceModality(mode);
46
+ }
47
+ }
48
+ getCapabilities() {
49
+ return this.host.getCapabilities();
50
+ }
51
+ onCapabilitiesChanged(listener) {
52
+ return this.host.onCapabilitiesChanged(listener);
53
+ }
54
+ onSourcesChanged(listener) {
55
+ return this.host.onSourcesChanged(listener);
56
+ }
57
+ sample() {
58
+ return this.host.sample().map(copySnapshot);
59
+ }
60
+ }
61
+ //# sourceMappingURL=provider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider.js","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,EASN,MAAM,gCAAgC,CAAC;AACxC,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,gBAAgB,EAAwB,MAAM,mBAAmB,CAAC;AAOnG,MAAM,OAAO,mBAAmB;IACb,IAAI,CAAkB;IAE9B,WAAW,CAAkB;IAC7B,WAAW,CAAiC;IAC5C,KAAK,CAAwE;IAC7E,kBAAkB,CAA6D;IAC/E,mBAAmB,CAAuC;IAEnE,YAAY,UAAsC,EAAE;QAClD,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC,OAAO,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;QAErD,IAAI,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC;YAC1B,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;YACvB,IAAI,CAAC,WAAW,GAAG,GAAG,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,WAAY,EAAE,CAAC,CAAC;QACzD,CAAC;QACD,IAAI,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC;YAC1B,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;YACvB,IAAI,CAAC,WAAW,GAAG,GAAG,EAAE,CAAC,IAAI,CAAC,WAAY,EAAE,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,CAAC,CAAC,CAAC;QAC5E,CAAC;QACD,IAAI,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;YACpB,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;YACvB,IAAI,CAAC,KAAK,GAAG,CAAC,QAAQ,EAAE,SAAS,EAAE,UAAU,EAAE,EAAE,CAAC,IAAI,CAAC,KAAM,CAAC,QAAQ,EAAE,SAAS,EAAE,UAAU,CAAC,CAAC;QACjG,CAAC;QACD,IAAI,IAAI,CAAC,IAAI,CAAC,kBAAkB,EAAE,CAAC;YACjC,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;YACvB,IAAI,CAAC,kBAAkB,GAAG,CAAC,MAAM,EAAE,OAAO,EAAE,EAAE,CAAC,IAAI,CAAC,kBAAmB,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAC3F,CAAC;QACD,IAAI,IAAI,CAAC,IAAI,CAAC,mBAAmB,EAAE,CAAC;YAClC,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;YACvB,IAAI,CAAC,mBAAmB,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,mBAAoB,CAAC,IAAI,CAAC,CAAC;QACvE,CAAC;IACH,CAAC;IAED,eAAe;QACb,OAAO,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,CAAC;IACrC,CAAC;IAED,qBAAqB,CAAC,QAAmD;QACvE,OAAO,IAAI,CAAC,IAAI,CAAC,qBAAqB,CAAC,QAAQ,CAAC,CAAC;IACnD,CAAC;IAED,gBAAgB,CAAC,QAAoB;QACnC,OAAO,IAAI,CAAC,IAAI,CAAC,gBAAgB,CAAC,QAAQ,CAAC,CAAC;IAC9C,CAAC;IAED,MAAM;QACJ,OAAO,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;IAC9C,CAAC;CACF","sourcesContent":["/**\n * NativeInputProvider - the native host's input provider.\n *\n * A thin, copying pass-through over the `input` slice a native app (OpenXR,\n * visionOS) installs on `globalThis.__rcHost`, or hands in directly for\n * tests. Every snapshot, and every tuple inside it, is copied before it\n * leaves this class, so a host that reuses its own sample buffers still\n * meets the ownership rule the shared contract suite checks.\n *\n * Optional members - `getHeadPose`, `sampleHints`, `pulse`,\n * `setPresenceVisible`, `setPresenceModality` - are only ever assigned when\n * the host itself carries them, so `typeof provider.pulse` answers honestly\n * for a host that cannot pulse rather than a method that is present but\n * always returns false.\n */\nimport {\n type Handedness,\n type HeadPose,\n type InputCapabilities,\n type InputHitHint,\n type InputProvider,\n type InputSourceSnapshot,\n type PresenceModality,\n type Unsubscribe,\n} from \"@realitycollective/webxr-input\";\nimport { copyPose, copySnapshot, resolveHostSlice, type NativeInputHost } from \"./native-types.js\";\n\nexport interface NativeInputProviderOptions {\n /** The `input` slice. Omit to read `globalThis.__rcHost.input`. */\n input?: NativeInputHost;\n}\n\nexport class NativeInputProvider implements InputProvider {\n private readonly host: NativeInputHost;\n\n readonly getHeadPose?: () => HeadPose;\n readonly sampleHints?: () => readonly InputHitHint[];\n readonly pulse?: (sourceId: string, intensity: number, durationMs: number) => boolean;\n readonly setPresenceVisible?: (target: Handedness | \"all\", visible: boolean) => boolean;\n readonly setPresenceModality?: (mode: PresenceModality) => boolean;\n\n constructor(options: NativeInputProviderOptions = {}) {\n this.host = resolveHostSlice(\"input\", options.input);\n\n if (this.host.getHeadPose) {\n const host = this.host;\n this.getHeadPose = () => copyPose(host.getHeadPose!());\n }\n if (this.host.sampleHints) {\n const host = this.host;\n this.sampleHints = () => host.sampleHints!().map((hint) => ({ ...hint }));\n }\n if (this.host.pulse) {\n const host = this.host;\n this.pulse = (sourceId, intensity, durationMs) => host.pulse!(sourceId, intensity, durationMs);\n }\n if (this.host.setPresenceVisible) {\n const host = this.host;\n this.setPresenceVisible = (target, visible) => host.setPresenceVisible!(target, visible);\n }\n if (this.host.setPresenceModality) {\n const host = this.host;\n this.setPresenceModality = (mode) => host.setPresenceModality!(mode);\n }\n }\n\n getCapabilities(): InputCapabilities {\n return this.host.getCapabilities();\n }\n\n onCapabilitiesChanged(listener: (capabilities: InputCapabilities) => void): Unsubscribe {\n return this.host.onCapabilitiesChanged(listener);\n }\n\n onSourcesChanged(listener: () => void): Unsubscribe {\n return this.host.onSourcesChanged(listener);\n }\n\n sample(): readonly InputSourceSnapshot[] {\n return this.host.sample().map(copySnapshot);\n }\n}\n"]}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * NativeTransformPort - the read/write surface of ONE interactable, over the
3
+ * native host's `interactions` slice. The host serves every registered
4
+ * object through one set of methods, so this port is a thin binding of one
5
+ * `targetId` onto them - the same idea as `BabylonTransformPort` binding to
6
+ * one node, but the object itself lives on the native side.
7
+ *
8
+ * `setWorldPose` and `setEffect` are optional on `TransformPort`, and are
9
+ * only ever assigned here when the host itself carries the matching member,
10
+ * so an app whose native host cannot follow a world pose, or has no effect
11
+ * hook, sees a port without one rather than a method that silently no-ops.
12
+ */
13
+ import type { PoseTuple, QuatTuple, Vec3Tuple } from "@realitycollective/webxr-input";
14
+ import type { TransformPort } from "@realitycollective/webxr-interactions";
15
+ import { type NativeInteractionHost } from "./native-types.js";
16
+ export interface NativeTransformPortOptions {
17
+ /** The `interactions` slice. Omit to read `globalThis.__rcHost.interactions`. */
18
+ interactions?: NativeInteractionHost;
19
+ }
20
+ export declare class NativeTransformPort implements TransformPort {
21
+ private readonly host;
22
+ private readonly targetId;
23
+ readonly setWorldPose?: (pose: PoseTuple) => void;
24
+ readonly setEffect?: (effect: {
25
+ scale?: number;
26
+ emissive?: number;
27
+ }) => void;
28
+ constructor(targetId: string, options?: NativeTransformPortOptions);
29
+ getWorldPose(): PoseTuple;
30
+ getRestWorldPose(): PoseTuple;
31
+ getLocalOffset(): Vec3Tuple;
32
+ setLocalOffset(offset: Vec3Tuple): void;
33
+ setLocalRotation(quaternion: QuatTuple): void;
34
+ }
@@ -0,0 +1,35 @@
1
+ import { copyPose, copyQuat, copyVec3, resolveHostSlice, } from "./native-types.js";
2
+ export class NativeTransformPort {
3
+ host;
4
+ targetId;
5
+ setWorldPose;
6
+ setEffect;
7
+ constructor(targetId, options = {}) {
8
+ this.targetId = targetId;
9
+ this.host = resolveHostSlice("interactions", options.interactions);
10
+ if (this.host.setWorldPose) {
11
+ const host = this.host;
12
+ this.setWorldPose = (pose) => host.setWorldPose(targetId, copyPose(pose));
13
+ }
14
+ if (this.host.setEffect) {
15
+ const host = this.host;
16
+ this.setEffect = (effect) => host.setEffect(targetId, effect);
17
+ }
18
+ }
19
+ getWorldPose() {
20
+ return copyPose(this.host.getWorldPose(this.targetId));
21
+ }
22
+ getRestWorldPose() {
23
+ return copyPose(this.host.getRestWorldPose(this.targetId));
24
+ }
25
+ getLocalOffset() {
26
+ return copyVec3(this.host.getLocalOffset(this.targetId));
27
+ }
28
+ setLocalOffset(offset) {
29
+ this.host.setLocalOffset(this.targetId, copyVec3(offset));
30
+ }
31
+ setLocalRotation(quaternion) {
32
+ this.host.setLocalRotation(this.targetId, copyQuat(quaternion));
33
+ }
34
+ }
35
+ //# sourceMappingURL=transform-port.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transform-port.js","sourceRoot":"","sources":["../src/transform-port.ts"],"names":[],"mappings":"AAcA,OAAO,EACL,QAAQ,EACR,QAAQ,EACR,QAAQ,EACR,gBAAgB,GAEjB,MAAM,mBAAmB,CAAC;AAO3B,MAAM,OAAO,mBAAmB;IACb,IAAI,CAAwB;IAC5B,QAAQ,CAAS;IAEzB,YAAY,CAA6B;IACzC,SAAS,CAA2D;IAE7E,YAAY,QAAgB,EAAE,UAAsC,EAAE;QACpE,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC,cAAc,EAAE,OAAO,CAAC,YAAY,CAAC,CAAC;QAEnE,IAAI,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC;YAC3B,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;YACvB,IAAI,CAAC,YAAY,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,YAAa,CAAC,QAAQ,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC;QAC7E,CAAC;QACD,IAAI,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;YACxB,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;YACvB,IAAI,CAAC,SAAS,GAAG,CAAC,MAAM,EAAE,EAAE,CAAC,IAAI,CAAC,SAAU,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;QACjE,CAAC;IACH,CAAC;IAED,YAAY;QACV,OAAO,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC;IACzD,CAAC;IAED,gBAAgB;QACd,OAAO,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC;IAC7D,CAAC;IAED,cAAc;QACZ,OAAO,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC;IAC3D,CAAC;IAED,cAAc,CAAC,MAAiB;QAC9B,IAAI,CAAC,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;IAC5D,CAAC;IAED,gBAAgB,CAAC,UAAqB;QACpC,IAAI,CAAC,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,QAAQ,EAAE,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC;IAClE,CAAC;CACF","sourcesContent":["/**\n * NativeTransformPort - the read/write surface of ONE interactable, over the\n * native host's `interactions` slice. The host serves every registered\n * object through one set of methods, so this port is a thin binding of one\n * `targetId` onto them - the same idea as `BabylonTransformPort` binding to\n * one node, but the object itself lives on the native side.\n *\n * `setWorldPose` and `setEffect` are optional on `TransformPort`, and are\n * only ever assigned here when the host itself carries the matching member,\n * so an app whose native host cannot follow a world pose, or has no effect\n * hook, sees a port without one rather than a method that silently no-ops.\n */\nimport type { PoseTuple, QuatTuple, Vec3Tuple } from \"@realitycollective/webxr-input\";\nimport type { TransformPort } from \"@realitycollective/webxr-interactions\";\nimport {\n copyPose,\n copyQuat,\n copyVec3,\n resolveHostSlice,\n type NativeInteractionHost,\n} from \"./native-types.js\";\n\nexport interface NativeTransformPortOptions {\n /** The `interactions` slice. Omit to read `globalThis.__rcHost.interactions`. */\n interactions?: NativeInteractionHost;\n}\n\nexport class NativeTransformPort implements TransformPort {\n private readonly host: NativeInteractionHost;\n private readonly targetId: string;\n\n readonly setWorldPose?: (pose: PoseTuple) => void;\n readonly setEffect?: (effect: { scale?: number; emissive?: number }) => void;\n\n constructor(targetId: string, options: NativeTransformPortOptions = {}) {\n this.targetId = targetId;\n this.host = resolveHostSlice(\"interactions\", options.interactions);\n\n if (this.host.setWorldPose) {\n const host = this.host;\n this.setWorldPose = (pose) => host.setWorldPose!(targetId, copyPose(pose));\n }\n if (this.host.setEffect) {\n const host = this.host;\n this.setEffect = (effect) => host.setEffect!(targetId, effect);\n }\n }\n\n getWorldPose(): PoseTuple {\n return copyPose(this.host.getWorldPose(this.targetId));\n }\n\n getRestWorldPose(): PoseTuple {\n return copyPose(this.host.getRestWorldPose(this.targetId));\n }\n\n getLocalOffset(): Vec3Tuple {\n return copyVec3(this.host.getLocalOffset(this.targetId));\n }\n\n setLocalOffset(offset: Vec3Tuple): void {\n this.host.setLocalOffset(this.targetId, copyVec3(offset));\n }\n\n setLocalRotation(quaternion: QuatTuple): void {\n this.host.setLocalRotation(this.targetId, copyQuat(quaternion));\n }\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@realitycollective/native-interactions",
3
+ "version": "0.1.1-preview.0",
4
+ "description": "Native host adapter for the Reality Collective Interaction Extensions - reads the `input` and `interactions` slices a native app (OpenXR, visionOS) installs on `globalThis.__rcHost`, or an injected fake, and maps them into the engine-free @realitycollective/webxr-interactions core. No engine dependency: assets and rendering stay in the native app. Re-exports the core.",
5
+ "keywords": [
6
+ "realitycollective",
7
+ "native",
8
+ "openxr",
9
+ "visionos",
10
+ "webxr",
11
+ "xr",
12
+ "interaction",
13
+ "input",
14
+ "typescript"
15
+ ],
16
+ "license": "MIT",
17
+ "author": "Reality Collective",
18
+ "type": "module",
19
+ "main": "./dist/index.js",
20
+ "types": "./dist/index.d.ts",
21
+ "exports": {
22
+ ".": {
23
+ "types": "./dist/index.d.ts",
24
+ "default": "./dist/index.js"
25
+ }
26
+ },
27
+ "sideEffects": false,
28
+ "files": [
29
+ "dist",
30
+ "CHANGELOG.md"
31
+ ],
32
+ "scripts": {
33
+ "build": "tsc -p tsconfig.build.json",
34
+ "typecheck": "tsc -p tsconfig.json --noEmit"
35
+ },
36
+ "dependencies": {
37
+ "@realitycollective/webxr-input": "^0.1.4",
38
+ "@realitycollective/webxr-interactions": "^0.1.1-preview.0"
39
+ },
40
+ "repository": {
41
+ "type": "git",
42
+ "url": "git+https://github.com/realitycollective/WebXR-Interactions.git",
43
+ "directory": "packages/native-interactions"
44
+ },
45
+ "homepage": "https://github.com/realitycollective/WebXR-Interactions#readme",
46
+ "bugs": {
47
+ "url": "https://github.com/realitycollective/WebXR-Interactions/issues"
48
+ },
49
+ "publishConfig": {
50
+ "access": "public"
51
+ },
52
+ "engines": {
53
+ "node": ">=20.19.0 <21.0.0-0 || >=22.12.0 <23.0.0-0 || >=24.0.0"
54
+ }
55
+ }