@realitycollective/native-interactions 0.1.1-preview.0 → 0.1.1-preview.2
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 +23 -0
- package/README.md +15 -2
- package/dist/conformance.d.ts +14 -0
- package/dist/conformance.js +151 -0
- package/dist/conformance.js.map +1 -0
- package/dist/host.d.ts +15 -2
- package/dist/host.js +13 -3
- package/dist/host.js.map +1 -1
- package/dist/index.d.ts +7 -3
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/native-types.d.ts +164 -26
- package/dist/native-types.js.map +1 -1
- package/dist/provider.d.ts +48 -10
- package/dist/provider.js +133 -29
- package/dist/provider.js.map +1 -1
- package/dist/transform-port.d.ts +10 -1
- package/dist/transform-port.js +11 -0
- package/dist/transform-port.js.map +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -8,11 +8,33 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
|
|
|
8
8
|
|
|
9
9
|
### Added
|
|
10
10
|
|
|
11
|
+
- `@realitycollective/webxr-interactions` - three `hitTesterContractCases()` that hold every platform to IWSDK's hit-test semantics: `hitProximity` reports the distance to the target's SURFACE, clamped at zero (a point 3 cm outside a 10 cm target reports 0.03 within 1 mm, not 0.13); it misses a target whose surface is farther than the query radius; and a ray that passes a target farther than its radius misses it. Every platform in the repository already passed them. The `HitTester` and `TransformPort.setEffect` docs now state those semantics, and the effect rule: `scale` multiplies the rest scale, `emissive` adds to the base intensity, and the last write wins.
|
|
12
|
+
- `@realitycollective/native-interactions` - `nativeInteractionsHostConformanceCases()`, the host conformance kit a native app runs on its device against its real slices: the core's whole `hitTesterContractCases()` run against the host itself, the 0.1 m default target radius, the release velocity reaching `endHold`, live-session capabilities, presence hiding the models only per side, `"auto"` following the live modality, and presence never hiding the cursors. The host provides the test readbacks of the new `NativeInteractionsTestHost` type. The kit fails a host that measures proximity to a target's centre, ignores the radius it is handed, loses a release velocity, or draws the wrong presence.
|
|
11
13
|
- `@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
14
|
- `@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.
|
|
15
|
+
- `@realitycollective/webxr-interactions` - "held pose" for a grabbed object with physics: `TransformPort` gains two optional members, `beginHold()` and `endHold(release: HoldRelease)`, and the `HoldRelease` type they take (`linearVelocity`/`angularVelocity`, metres and radians per second, world space). A host whose object has physics uses them to give a `poseOnly` grab the same three behaviours native grab fulfilment already had: held (physics suspended, the object follows `setWorldPose` exactly, gravity and collisions do nothing), released (physics resumes with the grabbing hand's velocity, so a throw carries through), and reset (a `setWorldPose` while not held teleports and clears velocity - "back to the tee"). `setWorldPose`'s own doc states this split. A platform with no physics grows neither member and behaves exactly as before; a platform that fulfils grabs natively is expected to show the same three behaviours through its own engine, and the runtime never calls `beginHold`/`endHold` for one. `InteractorInfo` gains optional `linearVelocity`/`angularVelocity`, the grabbing source's tracked velocity `GrabBehaviour` reads for the release. `transformPortContractCases()` gains three cases - Held, Released, Reset - that run only when the contract subject carries an optional `physics` driver, and skip otherwise.
|
|
16
|
+
- `@realitycollective/native-interactions` - the `interactions` slice gains the matching optional `beginHold(targetId)`/`endHold(targetId, release)`, keyed by target id like every other member. `NativeTransformPort` grows the two only when the host slice carries them, the same rule as `setWorldPose`/`setEffect`. The README states the rule for a native app that fulfils grabs itself: it is responsible for showing held/released/reset through its own engine, and `beginHold`/`endHold` are never called for a grab that app already owns.
|
|
17
|
+
- `@realitycollective/babylon-interactions` - `BabylonTransformPort` implements the held pose over Physics V2's `PhysicsBody`, through a new `physicsMotionTypes` construction option (Babylon's own `PhysicsMotionType.ANIMATED`/`.DYNAMIC` values - this package holds no Babylon values, only shapes). A held node switches to `ANIMATED`; `endHold` switches it back to `DYNAMIC` with the release velocity. A node with no `physicsBody`, or a construction with no `physicsMotionTypes`, grows neither member. Written from the Babylon Physics V2 documentation - this package has no `@babylonjs/core`/`@babylonjs/havok` dependency to verify it against - and not yet exercised against a live scene.
|
|
18
|
+
- `@realitycollective/iwsdk-interactions` - `IWSDKTransformPort` takes an optional `physics: IWSDKPhysicsBinding` (`beginHold`/`endHold`/`teleport`); `IWSDKInteractions.register()` builds one from `@iwsdk/core`'s `PhysicsBody`/`PhysicsShape`/`PhysicsManipulation` components and `PhysicsSystem.setBodyTransform` whenever the entity has a physics body. A hold removes `PhysicsBody` (keeping `PhysicsShape`), which tears down the entity's Havok body so gravity, collisions and the physics-to-object3D sync all stop while the port's own `setWorldPose` writes take over; release re-adds `PhysicsBody` at the state it had and, for a nonzero velocity, a `PhysicsManipulation`. Never the `Grabbed` tag `GrabSystem` owns - its own doc says not to add or remove it by hand - and never the private Havok handle `PhysicsSystem` keeps to itself.
|
|
19
|
+
- `@realitycollective/threejs-interactions` (reused by `@realitycollective/xrblocks-interactions`) - `ThreeHitTester.register(id, object, targetRadius?)` takes an optional radius, default 0.1 m, used by `hitProximity` for a target with no geometry of its own (a bare `Group`), matching IWSDK's default for a target registered without one. A target with a mesh keeps using its own bounding-sphere radius. `hitTesterContractCases()` gains a case proving the default, run wherever a driver's new optional `placeBare(id, position)` can register one.
|
|
20
|
+
- `@realitycollective/threejs-interactions` (reused by `@realitycollective/xrblocks-interactions`) - `ThreeHitTester.hitRay`/`hitProximity` now drop a hit whose object, or any ancestor, has `visible === false`. three.js's own `Raycaster` ignores `visible` by design (r186, issue 14700), so the rule is re-applied by the adapter. `hitTesterContractCases()` gains a case proving it, run wherever a driver's new optional `setVisible(id, visible)` can toggle one; `HitTester`'s doc states the rule for every platform.
|
|
21
|
+
- `@realitycollective/threejs-interactions`, `@realitycollective/babylon-interactions` and `@realitycollective/xrblocks-interactions` - `sample()` reports no sources while a live session exists but is not `"visible"` (backgrounded, or the browser's own "content is obscured" state), the rule `IWSDKInputProvider` already applied to `world.visibilityState`. Proven per platform, and through `provider-parity.test.ts`'s shared suite where a fake session can express visibility.
|
|
22
|
+
- `@realitycollective/xrblocks-interactions` - `XRBlocksInputProvider` reads the real grip pose from `controller.inputSource.gripSpace` (falling back to `targetRaySpace`), through a new optional `xr` context on `XRBlocksContext` - the same `WebXRManager`-shaped access `webxr-uiextensions`' xrblocks integration already reads `gripSpace` through. Angular velocity, differentiated from consecutive grip poses by the core's `VelocityTracker`, is now meaningful; before this the grip pose was the ray origin with an identity orientation, and every rotation reads were zero. `xr` also gates sampling on session visibility. Falls back to the previous ray-pose behaviour with no `xr` context supplied.
|
|
23
|
+
- `@realitycollective/xrblocks-interactions` - `XRBlocksInputProvider.pulse()` plays a haptic pulse through the controller's WebXR Gamepad `hapticActuators` (`Controller.gamepad`, verified against `input/Controller.ts`), clamping intensity to 0..1 exactly as the other providers do, and reports `capabilities.haptics`/`hapticsAvailable` accordingly. xrblocks 0.20.0 had no such surface; 0.21.1 does.
|
|
24
|
+
- `@realitycollective/xrblocks-interactions` - a new `nativeGrab` provider option (mirroring `IWSDKInputProvider`'s) switches `capabilities.grabs` to `"native"`, and `XRBlocksInteractions.onObjectGrabStart`/`onObjectGrabEnd` forward XR Blocks' `ManipulationManager`-driven `ObjectGrabEvent` Script hooks into `setNativeGrabbing`, the same hint `IWSDKInputProvider` raises for its own native grab. xrblocks 0.20.0 had no engine-owned grab; 0.21.1's `ManipulationManager` does.
|
|
25
|
+
- `@realitycollective/xrblocks-interactions` - a new `XRBlocksTransformPort`, replacing the reused `ThreeTransformPort` in the platform's own registration path (`XRBlocksInteractions.register()`) and in `port-parity.test.ts`'s platform list. It implements `beginHold`/`endHold` when the target's object carries a RAPIER rigid body (`register(descriptor, object, { rigidBody })`, plus a `rigidBodyTypes` construction option - the same `physicsMotionTypes` idea `BabylonTransformPort` uses for Havok): held switches the body kinematic and keeps it at the object's written pose; released switches it back to dynamic with the release velocity. With no body, or no `rigidBodyTypes`, it behaves exactly like `ThreeTransformPort`. xrblocks 0.20.0 bundled no physics; 0.21.1 bundles RAPIER (`PhysicsOptions.RAPIER`).
|
|
26
|
+
- `@realitycollective/babylon-interactions` - `BabylonTransformPort.setEffect`'s emissive pulse now lights the target: `material.emissiveColor` (`StandardMaterial` and `PBRMaterial` both carry it) is scaled by `1 + effect.emissive` about its own captured base, and on a `PBRMaterial`, `emissiveIntensity` is additionally set to its base plus `effect.emissive`, the same additive rule three.js's `emissiveIntensity` already follows. Before this the pulse only scaled the mesh. A node with no material, or a material with no `emissiveColor`, is left alone, as before.
|
|
13
27
|
|
|
14
28
|
### Changed
|
|
15
29
|
|
|
30
|
+
- The reference is now IWSDK 1.0. `@realitycollective/iwsdk-interactions` peers on `@iwsdk/core` and `@iwsdk/xr-input` `>=1.0.0 <2.0.0` (was `>=0.5.0 <0.6.0`) and is built and tested against 1.0.0. The per-frame visibility write the presence code works around is unchanged in 1.0.
|
|
31
|
+
- `@realitycollective/threejs-interactions`, `@realitycollective/babylon-interactions` and `@realitycollective/xrblocks-interactions` - a controller's `indexTip` is its ray origin, as IWSDK's input rig makes it, so a controller pokes buttons and interactables as a fingertip does. Before this only a tracked hand carried an index tip on these three platforms, and controllers could not poke at all.
|
|
32
|
+
- **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:
|
|
33
|
+
- The `input` slice reports `getFacts()` (`immersive`, `focused`, `handTracking`) and `onFactsChanged(cb)` in place of `getCapabilities()` and `onCapabilitiesChanged(cb)`. `NativeInputProvider` derives every capability as `IWSDKInputProvider.refreshCapabilities` does: `rays`, `pokes` and pose-only grabs from immersion (a controller pokes with its ray origin, so `pokes` no longer waits for hand tracking), `handJoints` and `pinch` from the hand-tracking fact or a tracked hand, `buttonsAxes` from a controller source, `haptics` from a source with an actuator on a host that can pulse, `gaze` and `headPose` from a head pose, and `presence` from `applyPresence`. It re-derives and fires `onCapabilitiesChanged` on every facts or source change; before this a native host hard-coded them and the event never fired.
|
|
34
|
+
- `setPresenceVisible` and `setPresenceModality` are no longer host members. The provider implements them, decides per side what to show as `IWSDKInputProvider.applyPresence` does (`"auto"` shows hands while hand joints are live), and calls the host's new `applyPresence(side, { hand, controller })` with the result, only when a side changed.
|
|
35
|
+
- `beginHold(targetId)` and `endHold(targetId, release)` are required members of the `interactions` slice, and `NativeTransformPort` throws at construction without them. A pose-only grab calls both, and a host without them silently dropped every release velocity. A target with no physics body rests where released, as on IWSDK.
|
|
36
|
+
- The `interactions` slice gains `setTargetRadius(targetId, radius)`. `NativeInteractions.register(descriptor, { targetRadius })` calls it with the given radius or 0.1 m, the radius IWSDK gives a bare target, so a target is never excluded from hit testing for want of one.
|
|
37
|
+
- `@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.
|
|
16
38
|
- `@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
39
|
- `@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
40
|
|
|
@@ -21,6 +43,7 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
|
|
|
21
43
|
- `@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
44
|
- `@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
45
|
- 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.
|
|
46
|
+
- `@realitycollective/webxr-interactions` - a `poseOnly` grab on a host with physics fought its own physics solver. `GrabBehaviour` wrote `setWorldPose` every frame while held and told the port nothing at grab end; the solver kept simulating over the top of that write, physics never resumed on release, and a throw dropped instead of carrying velocity. `GrabBehaviour` now calls the port's `beginHold()` on grab start and `endHold(release)` on grab end, with the grabbing source's tracked velocity (zero on a synthesized release - source lost, target unregistered, runtime disposed, or the interactable disabled mid-hold - which carries none). Every way a grab can end now calls `endHold` exactly once, including `InteractionRuntime.dispose()`, which previously left a held grab's physics suspended forever with no `endHold` at all.
|
|
24
47
|
|
|
25
48
|
## [0.1.0] - 2026-09-17
|
|
26
49
|
|
package/README.md
CHANGED
|
@@ -12,8 +12,8 @@ It re-exports everything from the core, so this is the only interaction package
|
|
|
12
12
|
|
|
13
13
|
| Layer | Detail |
|
|
14
14
|
| --- | --- |
|
|
15
|
-
| **Input** | `__rcHost.input` -
|
|
16
|
-
| **Hit-testing** | `__rcHost.interactions.hitRay` / `hitProximity`
|
|
15
|
+
| **Input** | `__rcHost.input` - session facts (`getFacts`, `onFactsChanged`), sources (`sample`, `onSourcesChanged`), and optional head pose, hints, haptics and presence drawing (`applyPresence`). The provider derives the capabilities and decides presence itself, as the IWSDK provider does |
|
|
16
|
+
| **Hit-testing** | `__rcHost.interactions.hitRay` / `hitProximity` with IWSDK's semantics: the ray parameter to the closest point, and the distance to a target's SURFACE, clamped at zero. `setTargetRadius` tells the host each target's radius (0.1 m unless given) |
|
|
17
17
|
| **Movement** | `__rcHost.interactions`, keyed by the target id you chose when you registered the object with the native scene |
|
|
18
18
|
| **No engine dependency** | Assets and rendering stay in the native app; only numbers, strings, booleans and tuples cross the boundary |
|
|
19
19
|
|
|
@@ -49,10 +49,23 @@ A missing slice - neither passed in nor found on `globalThis.__rcHost` - throws
|
|
|
49
49
|
## Things to know
|
|
50
50
|
|
|
51
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
|
+
- **A host is handed results, not rules.** The host reports facts (`immersive`, `focused`, `handTracking`) and sources; `NativeInputProvider` derives every capability from them exactly as `IWSDKInputProvider.refreshCapabilities` does, re-derives on every facts or source change, and fires `onCapabilitiesChanged`. Presence is decided here too: the requested visibility per side and the modality, with `"auto"` showing hands while hand joints are live and controllers otherwise, handed to the host as `applyPresence(side, { hand, controller })`. Presence hides the MODELS only; the host never draws a hand ray and always draws a cursor disc at a ray's hit. No sources are sampled while the session lacks focus, and a pulse's intensity is clamped to 0..1, as on IWSDK.
|
|
52
53
|
- **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.
|
|
54
|
+
- **Held pose is required.** `beginHold(targetId)`/`endHold(targetId, release)` are required members of the `interactions` slice, and `NativeTransformPort` throws at construction without them. A pose-only grab calls both on every grab and release; a host that lacked them used to drop every release velocity silently. While held, the object follows `setWorldPose` writes exactly; on release, a target with a physics body resumes with `release`'s velocities so a throw carries through, and a target with no body rests where it was released, as on IWSDK. A `setWorldPose` while NOT held teleports and clears velocity. A native app that fulfils grabs itself (`nativeGrab: true`) implements both as no-ops and shows the same three behaviours (held, released, reset) through its own engine.
|
|
55
|
+
- **Haptics are routed by the app, as on IWSDK.** Press and grab feedback reach `pulse` when the app routes them, `routeHapticsToProvider((listener) => interactions.runtime.onFeedback(listener), interactions.provider)`, exactly as a web client does with `registerInteractions`. Neither setup routes them by itself.
|
|
53
56
|
- **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
57
|
- **`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
58
|
|
|
59
|
+
## Proving a host: the conformance kit
|
|
60
|
+
|
|
61
|
+
`nativeInteractionsHostConformanceCases()` returns the host cases, as runner-free data named `interactions/<row>` and `input/<row>`. A native app runs them on its device, inside a live focused session, against its real slices and the test readbacks of `NativeInteractionsTestHost` (`placeTarget`, `clearTargets`, `presenceShown`, `lastRelease`, `cursors`). They include the core's whole `hitTesterContractCases()` run against the host itself, so a host that measures proximity to a target's centre fails on the device, not in the field.
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
for (const hostCase of nativeInteractionsHostConformanceCases()) {
|
|
65
|
+
await hostCase.run({ input: __rcHost.input, interactions: __rcHost.interactions, testHost: __rcShell.testHost });
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
56
69
|
## Peer dependency
|
|
57
70
|
|
|
58
71
|
None. No engine object crosses the boundary in either direction.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { NativeInputHost, NativeInteractionHost, NativeInteractionsTestHost } from "./native-types.js";
|
|
2
|
+
/** The real slices under test, and their readbacks. */
|
|
3
|
+
export interface NativeInteractionsHostConformanceSetup {
|
|
4
|
+
input: NativeInputHost;
|
|
5
|
+
interactions: NativeInteractionHost;
|
|
6
|
+
testHost: NativeInteractionsTestHost;
|
|
7
|
+
}
|
|
8
|
+
/** One check a native host must pass. `name` is `interactions/<row>` or `input/<row>`. */
|
|
9
|
+
export interface NativeInteractionsHostConformanceCase {
|
|
10
|
+
name: string;
|
|
11
|
+
run(setup: NativeInteractionsHostConformanceSetup): Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
/** The Interactions and Input host conformance cases. See the file comment. */
|
|
14
|
+
export declare function nativeInteractionsHostConformanceCases(): NativeInteractionsHostConformanceCase[];
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Interactions host conformance kit: host cases (the third kind in the
|
|
3
|
+
* Masters' "Validation" section), shipped as data so a native app runs them
|
|
4
|
+
* against its REAL `input` and `interactions` slices on the device.
|
|
5
|
+
*
|
|
6
|
+
* The contract cases a native package ran before this checked shapes against
|
|
7
|
+
* fakes, and passed while the real host measured proximity to a target's
|
|
8
|
+
* centre, which removed every near interaction on the headset. These run the
|
|
9
|
+
* same checks against the host itself, through the test readbacks of
|
|
10
|
+
* `NativeInteractionsTestHost`: the core's whole `hitTesterContractCases()`
|
|
11
|
+
* (surface distance included) over targets the host places, the default
|
|
12
|
+
* target radius, the release velocity reaching `endHold`, and what the host
|
|
13
|
+
* draws for presence. Run them inside a live, focused session.
|
|
14
|
+
*
|
|
15
|
+
* Runner-free, like the core suites: a case resolves on success and rejects
|
|
16
|
+
* with a plain `Error` naming its row otherwise.
|
|
17
|
+
*/
|
|
18
|
+
import { hitTesterContractCases, } from "@realitycollective/webxr-interactions";
|
|
19
|
+
import { NativeHitTester } from "./hit-tester.js";
|
|
20
|
+
import { NativeInteractions } from "./host.js";
|
|
21
|
+
import { NativeInputProvider } from "./provider.js";
|
|
22
|
+
import { NativeTransformPort } from "./transform-port.js";
|
|
23
|
+
function fail(name, message) {
|
|
24
|
+
throw new Error(`[${name}] ${message}`);
|
|
25
|
+
}
|
|
26
|
+
function hostCase(name, run) {
|
|
27
|
+
return { name, run: async (setup) => run(setup, name) };
|
|
28
|
+
}
|
|
29
|
+
function exactlyOneFamily(shown) {
|
|
30
|
+
return shown !== undefined && shown.hand !== shown.controller;
|
|
31
|
+
}
|
|
32
|
+
/** The Interactions and Input host conformance cases. See the file comment. */
|
|
33
|
+
export function nativeInteractionsHostConformanceCases() {
|
|
34
|
+
const hitCases = hitTesterContractCases().map((contractCase) => hostCase(`interactions/${contractCase.name}`, ({ interactions, testHost }, name) => {
|
|
35
|
+
testHost.clearTargets();
|
|
36
|
+
try {
|
|
37
|
+
contractCase.run({
|
|
38
|
+
hitTester: new NativeHitTester({ interactions }),
|
|
39
|
+
driver: { place: (id, position, radius) => testHost.placeTarget(id, position, radius) },
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
fail(name, error.message);
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
testHost.clearTargets();
|
|
47
|
+
}
|
|
48
|
+
}));
|
|
49
|
+
return [
|
|
50
|
+
...hitCases,
|
|
51
|
+
hostCase("interactions/a registered target with no radius is a 10 cm sphere", ({ input, interactions, testHost }, name) => {
|
|
52
|
+
testHost.clearTargets();
|
|
53
|
+
const setup = new NativeInteractions({ input, interactions });
|
|
54
|
+
try {
|
|
55
|
+
// Placed small, then registered with no radius: the registration
|
|
56
|
+
// must make it a 0.1 m sphere, so a point 9 cm from its centre is
|
|
57
|
+
// inside it.
|
|
58
|
+
testHost.placeTarget("rc-kit-bare", [0, 1, -1], 0.02);
|
|
59
|
+
setup.register({ id: "rc-kit-bare", behaviours: [] });
|
|
60
|
+
const hit = interactions.hitProximity([0.09, 1, -1], 0.001);
|
|
61
|
+
if (!hit || hit.targetId !== "rc-kit-bare") {
|
|
62
|
+
fail(name, "a point 9 cm from a bare target's centre did not reach it; the host is not using the 0.1 m radius");
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
setup.dispose();
|
|
67
|
+
testHost.clearTargets();
|
|
68
|
+
}
|
|
69
|
+
}),
|
|
70
|
+
hostCase("interactions/endHold hands the host the release velocity", ({ interactions, testHost }, name) => {
|
|
71
|
+
testHost.clearTargets();
|
|
72
|
+
try {
|
|
73
|
+
testHost.placeTarget("rc-kit-throw", [0, 1, -1], 0.1);
|
|
74
|
+
const port = new NativeTransformPort("rc-kit-throw", { interactions });
|
|
75
|
+
const release = { linearVelocity: [1, 2, 3], angularVelocity: [0, 0.5, 0] };
|
|
76
|
+
port.beginHold();
|
|
77
|
+
port.endHold(release);
|
|
78
|
+
const received = testHost.lastRelease("rc-kit-throw");
|
|
79
|
+
if (JSON.stringify(received) !== JSON.stringify(release)) {
|
|
80
|
+
fail(name, `the host received ${JSON.stringify(received)}, expected ${JSON.stringify(release)}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
testHost.clearTargets();
|
|
85
|
+
}
|
|
86
|
+
}),
|
|
87
|
+
hostCase("input/capabilities in a live session: rays, pokes and pose-only grabs", ({ input }, name) => {
|
|
88
|
+
const provider = new NativeInputProvider({ input });
|
|
89
|
+
try {
|
|
90
|
+
const caps = provider.getCapabilities();
|
|
91
|
+
// Pose-only grabs follow from the same fact, so rays and pokes carry it.
|
|
92
|
+
if (!caps.rays || !caps.pokes || caps.grabs !== "poseOnly") {
|
|
93
|
+
fail(name, `a live session must report rays, pokes and pose-only grabs, got ${JSON.stringify(caps)}; run the kit inside a live session`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
provider.dispose();
|
|
98
|
+
}
|
|
99
|
+
}),
|
|
100
|
+
hostCase("input/presence hides the models only, per side", ({ input, testHost }, name) => {
|
|
101
|
+
const provider = new NativeInputProvider({ input });
|
|
102
|
+
try {
|
|
103
|
+
if (!provider.getCapabilities().presence || !provider.setPresenceVisible) {
|
|
104
|
+
fail(name, "the host has no applyPresence, so presence cannot be shown or hidden");
|
|
105
|
+
}
|
|
106
|
+
provider.setPresenceVisible("left", false);
|
|
107
|
+
const left = testHost.presenceShown("left");
|
|
108
|
+
if (!left || left.hand || left.controller)
|
|
109
|
+
fail(name, `a hidden left side still draws ${JSON.stringify(left)}`);
|
|
110
|
+
if (!exactlyOneFamily(testHost.presenceShown("right"))) {
|
|
111
|
+
fail(name, `the right side must still draw exactly one family, drew ${JSON.stringify(testHost.presenceShown("right"))}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
provider.setPresenceVisible?.("all", true);
|
|
116
|
+
provider.dispose();
|
|
117
|
+
}
|
|
118
|
+
}),
|
|
119
|
+
hostCase('input/"auto" shows the family the live session uses', ({ input, testHost }, name) => {
|
|
120
|
+
const provider = new NativeInputProvider({ input });
|
|
121
|
+
try {
|
|
122
|
+
provider.setPresenceModality?.("auto");
|
|
123
|
+
const handJoints = provider.getCapabilities().handJoints;
|
|
124
|
+
for (const side of ["left", "right"]) {
|
|
125
|
+
const shown = testHost.presenceShown(side);
|
|
126
|
+
if (!shown || shown.hand !== handJoints || shown.controller !== !handJoints) {
|
|
127
|
+
fail(name, `${side} draws ${JSON.stringify(shown)} with hand joints ${handJoints ? "live" : "off"}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
provider.dispose();
|
|
133
|
+
}
|
|
134
|
+
}),
|
|
135
|
+
hostCase("input/presence never hides the cursors", ({ input, testHost }, name) => {
|
|
136
|
+
const provider = new NativeInputProvider({ input });
|
|
137
|
+
try {
|
|
138
|
+
const before = testHost.cursors().length;
|
|
139
|
+
provider.setPresenceVisible?.("all", false);
|
|
140
|
+
const after = testHost.cursors().length;
|
|
141
|
+
if (after !== before)
|
|
142
|
+
fail(name, `hiding presence changed the cursors drawn from ${before} to ${after}`);
|
|
143
|
+
}
|
|
144
|
+
finally {
|
|
145
|
+
provider.setPresenceVisible?.("all", true);
|
|
146
|
+
provider.dispose();
|
|
147
|
+
}
|
|
148
|
+
}),
|
|
149
|
+
];
|
|
150
|
+
}
|
|
151
|
+
//# sourceMappingURL=conformance.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conformance.js","sourceRoot":"","sources":["../src/conformance.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The Interactions host conformance kit: host cases (the third kind in the\n * Masters' \"Validation\" section), shipped as data so a native app runs them\n * against its REAL `input` and `interactions` slices on the device.\n *\n * The contract cases a native package ran before this checked shapes against\n * fakes, and passed while the real host measured proximity to a target's\n * centre, which removed every near interaction on the headset. These run the\n * same checks against the host itself, through the test readbacks of\n * `NativeInteractionsTestHost`: the core's whole `hitTesterContractCases()`\n * (surface distance included) over targets the host places, the default\n * target radius, the release velocity reaching `endHold`, and what the host\n * draws for presence. Run them inside a live, focused session.\n *\n * Runner-free, like the core suites: a case resolves on success and rejects\n * with a plain `Error` naming its row otherwise.\n */\nimport {\n hitTesterContractCases,\n type HoldRelease,\n} from \"@realitycollective/webxr-interactions\";\nimport { NativeHitTester } from \"./hit-tester.js\";\nimport { NativeInteractions } from \"./host.js\";\nimport { NativeInputProvider } from \"./provider.js\";\nimport { NativeTransformPort } from \"./transform-port.js\";\nimport type {\n NativeInputHost,\n NativeInteractionHost,\n NativeInteractionsTestHost,\n NativePresenceShown,\n} from \"./native-types.js\";\n\n/** The real slices under test, and their readbacks. */\nexport interface NativeInteractionsHostConformanceSetup {\n input: NativeInputHost;\n interactions: NativeInteractionHost;\n testHost: NativeInteractionsTestHost;\n}\n\n/** One check a native host must pass. `name` is `interactions/<row>` or `input/<row>`. */\nexport interface NativeInteractionsHostConformanceCase {\n name: string;\n run(setup: NativeInteractionsHostConformanceSetup): Promise<void>;\n}\n\nfunction fail(name: string, message: string): never {\n throw new Error(`[${name}] ${message}`);\n}\n\nfunction hostCase(\n name: string,\n run: (setup: NativeInteractionsHostConformanceSetup, name: string) => void | Promise<void>,\n): NativeInteractionsHostConformanceCase {\n return { name, run: async (setup) => run(setup, name) };\n}\n\nfunction exactlyOneFamily(shown: NativePresenceShown | undefined): boolean {\n return shown !== undefined && shown.hand !== shown.controller;\n}\n\n/** The Interactions and Input host conformance cases. See the file comment. */\nexport function nativeInteractionsHostConformanceCases(): NativeInteractionsHostConformanceCase[] {\n const hitCases = hitTesterContractCases().map((contractCase) =>\n hostCase(`interactions/${contractCase.name}`, ({ interactions, testHost }, name) => {\n testHost.clearTargets();\n try {\n contractCase.run({\n hitTester: new NativeHitTester({ interactions }),\n driver: { place: (id, position, radius) => testHost.placeTarget(id, position, radius) },\n });\n } catch (error) {\n fail(name, (error as Error).message);\n } finally {\n testHost.clearTargets();\n }\n }),\n );\n\n return [\n ...hitCases,\n hostCase(\"interactions/a registered target with no radius is a 10 cm sphere\", ({ input, interactions, testHost }, name) => {\n testHost.clearTargets();\n const setup = new NativeInteractions({ input, interactions });\n try {\n // Placed small, then registered with no radius: the registration\n // must make it a 0.1 m sphere, so a point 9 cm from its centre is\n // inside it.\n testHost.placeTarget(\"rc-kit-bare\", [0, 1, -1], 0.02);\n setup.register({ id: \"rc-kit-bare\", behaviours: [] });\n const hit = interactions.hitProximity([0.09, 1, -1], 0.001);\n if (!hit || hit.targetId !== \"rc-kit-bare\") {\n fail(name, \"a point 9 cm from a bare target's centre did not reach it; the host is not using the 0.1 m radius\");\n }\n } finally {\n setup.dispose();\n testHost.clearTargets();\n }\n }),\n hostCase(\"interactions/endHold hands the host the release velocity\", ({ interactions, testHost }, name) => {\n testHost.clearTargets();\n try {\n testHost.placeTarget(\"rc-kit-throw\", [0, 1, -1], 0.1);\n const port = new NativeTransformPort(\"rc-kit-throw\", { interactions });\n const release: HoldRelease = { linearVelocity: [1, 2, 3], angularVelocity: [0, 0.5, 0] };\n port.beginHold();\n port.endHold(release);\n const received = testHost.lastRelease(\"rc-kit-throw\");\n if (JSON.stringify(received) !== JSON.stringify(release)) {\n fail(name, `the host received ${JSON.stringify(received)}, expected ${JSON.stringify(release)}`);\n }\n } finally {\n testHost.clearTargets();\n }\n }),\n hostCase(\"input/capabilities in a live session: rays, pokes and pose-only grabs\", ({ input }, name) => {\n const provider = new NativeInputProvider({ input });\n try {\n const caps = provider.getCapabilities();\n // Pose-only grabs follow from the same fact, so rays and pokes carry it.\n if (!caps.rays || !caps.pokes || caps.grabs !== \"poseOnly\") {\n fail(name, `a live session must report rays, pokes and pose-only grabs, got ${JSON.stringify(caps)}; run the kit inside a live session`);\n }\n } finally {\n provider.dispose();\n }\n }),\n hostCase(\"input/presence hides the models only, per side\", ({ input, testHost }, name) => {\n const provider = new NativeInputProvider({ input });\n try {\n if (!provider.getCapabilities().presence || !provider.setPresenceVisible) {\n fail(name, \"the host has no applyPresence, so presence cannot be shown or hidden\");\n }\n provider.setPresenceVisible(\"left\", false);\n const left = testHost.presenceShown(\"left\");\n if (!left || left.hand || left.controller) fail(name, `a hidden left side still draws ${JSON.stringify(left)}`);\n if (!exactlyOneFamily(testHost.presenceShown(\"right\"))) {\n fail(name, `the right side must still draw exactly one family, drew ${JSON.stringify(testHost.presenceShown(\"right\"))}`);\n }\n } finally {\n provider.setPresenceVisible?.(\"all\", true);\n provider.dispose();\n }\n }),\n hostCase('input/\"auto\" shows the family the live session uses', ({ input, testHost }, name) => {\n const provider = new NativeInputProvider({ input });\n try {\n provider.setPresenceModality?.(\"auto\");\n const handJoints = provider.getCapabilities().handJoints;\n for (const side of [\"left\", \"right\"] as const) {\n const shown = testHost.presenceShown(side);\n if (!shown || shown.hand !== handJoints || shown.controller !== !handJoints) {\n fail(name, `${side} draws ${JSON.stringify(shown)} with hand joints ${handJoints ? \"live\" : \"off\"}`);\n }\n }\n } finally {\n provider.dispose();\n }\n }),\n hostCase(\"input/presence never hides the cursors\", ({ input, testHost }, name) => {\n const provider = new NativeInputProvider({ input });\n try {\n const before = testHost.cursors().length;\n provider.setPresenceVisible?.(\"all\", false);\n const after = testHost.cursors().length;\n if (after !== before) fail(name, `hiding presence changed the cursors drawn from ${before} to ${after}`);\n } finally {\n provider.setPresenceVisible?.(\"all\", true);\n provider.dispose();\n }\n }),\n ];\n}\n"]}
|
package/dist/host.d.ts
CHANGED
|
@@ -29,16 +29,29 @@ export interface NativeInteractionsOptions extends NativeInputProviderOptions, N
|
|
|
29
29
|
/** Where frames come from for `attachToHost`. Omit to use `globalThis.__rcHost`. */
|
|
30
30
|
frames?: NativeFrameSource;
|
|
31
31
|
}
|
|
32
|
+
/** Per-registration options, as IWSDK's `register` takes them. */
|
|
33
|
+
export interface NativeRegisterOptions {
|
|
34
|
+
/**
|
|
35
|
+
* The radius, in metres, the host hit-tests this target with. Default
|
|
36
|
+
* 0.1, the radius IWSDK gives a target that declares none.
|
|
37
|
+
*/
|
|
38
|
+
targetRadius?: number;
|
|
39
|
+
}
|
|
32
40
|
export declare class NativeInteractions {
|
|
33
41
|
readonly runtime: InteractionRuntime;
|
|
34
42
|
readonly provider: NativeInputProvider;
|
|
35
43
|
readonly hitTester: NativeHitTester;
|
|
36
44
|
private readonly ports;
|
|
37
45
|
private readonly options;
|
|
46
|
+
private readonly interactionsHost;
|
|
38
47
|
private detachHost;
|
|
39
48
|
constructor(options?: NativeInteractionsOptions);
|
|
40
|
-
/**
|
|
41
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Register an interactable. The native app knows it by `descriptor.id`,
|
|
51
|
+
* and is told the radius to hit-test it with: `options.targetRadius`, or
|
|
52
|
+
* 0.1 m, so a bare target is a 10 cm sphere as on IWSDK.
|
|
53
|
+
*/
|
|
54
|
+
register(descriptor: InteractableDescriptor, options?: NativeRegisterOptions): NativeTransformPort;
|
|
42
55
|
unregister(id: string): void;
|
|
43
56
|
getPort(id: string): NativeTransformPort | undefined;
|
|
44
57
|
update(dt: number): void;
|
package/dist/host.js
CHANGED
|
@@ -18,7 +18,9 @@ import { InteractionRuntime, } from "@realitycollective/webxr-interactions";
|
|
|
18
18
|
import { NativeHitTester } from "./hit-tester.js";
|
|
19
19
|
import { NativeInputProvider } from "./provider.js";
|
|
20
20
|
import { NativeTransformPort } from "./transform-port.js";
|
|
21
|
-
import { installedHost } from "./native-types.js";
|
|
21
|
+
import { installedHost, resolveHostSlice, } from "./native-types.js";
|
|
22
|
+
/** The radius a target is hit-tested with when it declares none: IWSDK's `targetRadius ?? 0.1`. */
|
|
23
|
+
const DEFAULT_TARGET_RADIUS = 0.1;
|
|
22
24
|
/** Longest frame the host-attached loop will report, in seconds. */
|
|
23
25
|
const MAX_FRAME_SECONDS = 0.1;
|
|
24
26
|
export class NativeInteractions {
|
|
@@ -27,9 +29,11 @@ export class NativeInteractions {
|
|
|
27
29
|
hitTester;
|
|
28
30
|
ports = new Map();
|
|
29
31
|
options;
|
|
32
|
+
interactionsHost;
|
|
30
33
|
detachHost = null;
|
|
31
34
|
constructor(options = {}) {
|
|
32
35
|
this.options = options;
|
|
36
|
+
this.interactionsHost = resolveHostSlice("interactions", options.interactions);
|
|
33
37
|
this.provider = new NativeInputProvider(options);
|
|
34
38
|
this.hitTester = new NativeHitTester(options);
|
|
35
39
|
this.runtime = new InteractionRuntime({
|
|
@@ -40,9 +44,14 @@ export class NativeInteractions {
|
|
|
40
44
|
if (options.attachToHost)
|
|
41
45
|
this.attachToHost(options);
|
|
42
46
|
}
|
|
43
|
-
/**
|
|
44
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Register an interactable. The native app knows it by `descriptor.id`,
|
|
49
|
+
* and is told the radius to hit-test it with: `options.targetRadius`, or
|
|
50
|
+
* 0.1 m, so a bare target is a 10 cm sphere as on IWSDK.
|
|
51
|
+
*/
|
|
52
|
+
register(descriptor, options = {}) {
|
|
45
53
|
const port = new NativeTransformPort(descriptor.id, this.options.interactions ? { interactions: this.options.interactions } : {});
|
|
54
|
+
this.interactionsHost.setTargetRadius(descriptor.id, options.targetRadius ?? DEFAULT_TARGET_RADIUS);
|
|
46
55
|
this.ports.set(descriptor.id, port);
|
|
47
56
|
this.runtime.registerInteractable(descriptor, { transform: port });
|
|
48
57
|
return port;
|
|
@@ -61,6 +70,7 @@ export class NativeInteractions {
|
|
|
61
70
|
this.detachHost?.();
|
|
62
71
|
this.detachHost = null;
|
|
63
72
|
this.runtime.dispose();
|
|
73
|
+
this.provider.dispose();
|
|
64
74
|
}
|
|
65
75
|
attachToHost(options) {
|
|
66
76
|
const frames = options.frames ?? installedHost();
|
package/dist/host.js.map
CHANGED
|
@@ -1 +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,
|
|
1
|
+
{"version":3,"file":"host.js","sourceRoot":"","sources":["../src/host.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * createNativeInteractions - one-call setup for the native adapter, with the\n * same shape as the Babylon, three.js, IWSDK and XR Blocks setups.\n *\n * ```ts\n * const interactions = createNativeInteractions({ attachToHost: true });\n * interactions.register({ id: \"button\", behaviours: [{ kind: \"press\" }] });\n * interactions.runtime.onEvent((event) => console.log(event.type));\n * ```\n *\n * The native app owns the scene, so an interactable is registered by id\n * alone: the app answers hit queries and pose reads for that id through the\n * `interactions` slice. With `attachToHost` the loop drives itself from the\n * app's own frame callback. Without it, call `update(dt)` yourself with\n * seconds.\n */\nimport {\n InteractionRuntime,\n type DwellConfig,\n type InteractableDescriptor,\n} 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 {\n installedHost,\n resolveHostSlice,\n type NativeFrameSource,\n type NativeInteractionHost,\n} 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/** Per-registration options, as IWSDK's `register` takes them. */\nexport interface NativeRegisterOptions {\n /**\n * The radius, in metres, the host hit-tests this target with. Default\n * 0.1, the radius IWSDK gives a target that declares none.\n */\n targetRadius?: number;\n}\n\n/** The radius a target is hit-tested with when it declares none: IWSDK's `targetRadius ?? 0.1`. */\nconst DEFAULT_TARGET_RADIUS = 0.1;\n\n/** Longest frame the host-attached loop will report, in seconds. */\nconst MAX_FRAME_SECONDS = 0.1;\n\nexport class NativeInteractions {\n readonly runtime: InteractionRuntime;\n readonly provider: NativeInputProvider;\n readonly hitTester: NativeHitTester;\n private readonly ports = new Map<string, NativeTransformPort>();\n private readonly options: NativeInteractionsOptions;\n private readonly interactionsHost: NativeInteractionHost;\n private detachHost: (() => void) | null = null;\n\n constructor(options: NativeInteractionsOptions = {}) {\n this.options = options;\n this.interactionsHost = resolveHostSlice(\"interactions\", options.interactions);\n this.provider = new NativeInputProvider(options);\n this.hitTester = new NativeHitTester(options);\n this.runtime = new InteractionRuntime({\n provider: this.provider,\n hitTester: this.hitTester,\n ...(options.dwellDefaults ? { dwellDefaults: options.dwellDefaults } : {}),\n });\n if (options.attachToHost) this.attachToHost(options);\n }\n\n /**\n * Register an interactable. The native app knows it by `descriptor.id`,\n * and is told the radius to hit-test it with: `options.targetRadius`, or\n * 0.1 m, so a bare target is a 10 cm sphere as on IWSDK.\n */\n register(descriptor: InteractableDescriptor, options: NativeRegisterOptions = {}): NativeTransformPort {\n const port = new NativeTransformPort(\n descriptor.id,\n this.options.interactions ? { interactions: this.options.interactions } : {},\n );\n this.interactionsHost.setTargetRadius(descriptor.id, options.targetRadius ?? DEFAULT_TARGET_RADIUS);\n this.ports.set(descriptor.id, port);\n this.runtime.registerInteractable(descriptor, { transform: port });\n return port;\n }\n\n unregister(id: string): void {\n this.runtime.unregisterInteractable(id);\n this.ports.delete(id);\n }\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 this.provider.dispose();\n }\n\n private attachToHost(options: NativeInteractionsOptions): void {\n const frames = options.frames ?? installedHost();\n if (!frames?.onFrame) {\n throw new Error(\n \"@realitycollective/native-interactions: attachToHost needs a frame source, and globalThis.__rcHost.onFrame is not installed. Pass `frames`, or call update(dt) yourself.\",\n );\n }\n // The app reports the frame delta in seconds. A long frame is clamped so\n // behaviours integrate over a sane step rather than jumping.\n this.detachHost = frames.onFrame((_timestampMs, deltaS) => {\n this.update(Math.min(MAX_FRAME_SECONDS, Math.max(0, deltaS)));\n });\n }\n}\n\nexport function createNativeInteractions(\n options: NativeInteractionsOptions = {},\n): NativeInteractions {\n return new NativeInteractions(options);\n}\n"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -3,12 +3,16 @@
|
|
|
3
3
|
* Babylon, three.js, IWSDK and XR Blocks adapters. The core is re-exported;
|
|
4
4
|
* the provider, hit tester and transform port implement its contracts; the
|
|
5
5
|
* setup entry point wires them together; and the slice types describe what
|
|
6
|
-
* the native app installs on `globalThis.__rcHost`.
|
|
7
|
-
*
|
|
6
|
+
* the native app installs on `globalThis.__rcHost`. One addition the web
|
|
7
|
+
* adapters do not need: the host conformance kit a native app runs on its
|
|
8
|
+
* device against its real slices. Slice reading and tuple copying stay
|
|
9
|
+
* internal.
|
|
8
10
|
*/
|
|
9
11
|
export * from "@realitycollective/webxr-interactions";
|
|
10
|
-
export type { NativeFrameSource, NativeHit, NativeHostSlices, NativeInputHost, NativeInteractionHost, } from "./native-types.js";
|
|
12
|
+
export type { NativeFrameSource, NativeHit, NativeHostSlices, NativeInputFacts, NativeInputHost, NativeInteractionHost, NativeInteractionsTestHost, NativePresenceShown, } from "./native-types.js";
|
|
11
13
|
export * from "./provider.js";
|
|
12
14
|
export * from "./hit-tester.js";
|
|
13
15
|
export * from "./transform-port.js";
|
|
14
16
|
export * from "./host.js";
|
|
17
|
+
export { nativeInteractionsHostConformanceCases } from "./conformance.js";
|
|
18
|
+
export type { NativeInteractionsHostConformanceCase, NativeInteractionsHostConformanceSetup, } from "./conformance.js";
|
package/dist/index.js
CHANGED
|
@@ -3,12 +3,15 @@
|
|
|
3
3
|
* Babylon, three.js, IWSDK and XR Blocks adapters. The core is re-exported;
|
|
4
4
|
* the provider, hit tester and transform port implement its contracts; the
|
|
5
5
|
* setup entry point wires them together; and the slice types describe what
|
|
6
|
-
* the native app installs on `globalThis.__rcHost`.
|
|
7
|
-
*
|
|
6
|
+
* the native app installs on `globalThis.__rcHost`. One addition the web
|
|
7
|
+
* adapters do not need: the host conformance kit a native app runs on its
|
|
8
|
+
* device against its real slices. Slice reading and tuple copying stay
|
|
9
|
+
* internal.
|
|
8
10
|
*/
|
|
9
11
|
export * from "@realitycollective/webxr-interactions";
|
|
10
12
|
export * from "./provider.js";
|
|
11
13
|
export * from "./hit-tester.js";
|
|
12
14
|
export * from "./transform-port.js";
|
|
13
15
|
export * from "./host.js";
|
|
16
|
+
export { nativeInteractionsHostConformanceCases } from "./conformance.js";
|
|
14
17
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,cAAc,uCAAuC,CAAC;AAWtD,cAAc,eAAe,CAAC;AAC9B,cAAc,iBAAiB,CAAC;AAChC,cAAc,qBAAqB,CAAC;AACpC,cAAc,WAAW,CAAC;AAC1B,OAAO,EAAE,sCAAsC,EAAE,MAAM,kBAAkB,CAAC","sourcesContent":["/**\n * Public entry point of the native adapter: the same kinds of export as the\n * Babylon, three.js, IWSDK and XR Blocks adapters. The core is re-exported;\n * the provider, hit tester and transform port implement its contracts; the\n * setup entry point wires them together; and the slice types describe what\n * the native app installs on `globalThis.__rcHost`. One addition the web\n * adapters do not need: the host conformance kit a native app runs on its\n * device against its real slices. Slice reading and tuple copying stay\n * internal.\n */\nexport * from \"@realitycollective/webxr-interactions\";\nexport type {\n NativeFrameSource,\n NativeHit,\n NativeHostSlices,\n NativeInputFacts,\n NativeInputHost,\n NativeInteractionHost,\n NativeInteractionsTestHost,\n NativePresenceShown,\n} from \"./native-types.js\";\nexport * from \"./provider.js\";\nexport * from \"./hit-tester.js\";\nexport * from \"./transform-port.js\";\nexport * from \"./host.js\";\nexport { nativeInteractionsHostConformanceCases } from \"./conformance.js\";\nexport type {\n NativeInteractionsHostConformanceCase,\n NativeInteractionsHostConformanceSetup,\n} from \"./conformance.js\";\n"]}
|
package/dist/native-types.d.ts
CHANGED
|
@@ -3,65 +3,203 @@
|
|
|
3
3
|
* CompositorServices on visionOS, or any other shell embedding a JavaScript
|
|
4
4
|
* engine such as Hermes) installs on `globalThis.__rcHost`.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* A HOST IS HANDED RESULTS, NOT RULES. Every rule the IWSDK binding applies
|
|
7
|
+
* in its provider runs in this package from the same inputs: capabilities
|
|
8
|
+
* are derived here from the facts the host reports, the presence modality
|
|
9
|
+
* is decided here and handed to the host per side, and a target's radius is
|
|
10
|
+
* handed to the host at registration. The host measures, renders and
|
|
11
|
+
* reports. Each member below states what the host does, in what units and
|
|
12
|
+
* with what sign, and which IWSDK line it stands in for.
|
|
13
|
+
*
|
|
8
14
|
* `NativeInteractionHost` is `HitTester` plus `TransformPort` from
|
|
9
|
-
* `@realitycollective/webxr-interactions
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* packages open.
|
|
15
|
+
* `@realitycollective/webxr-interactions`, with a `targetId` added to every
|
|
16
|
+
* `TransformPort` member because that port is per object and the host is one
|
|
17
|
+
* object serving every registered interactable. Declaring the shapes here,
|
|
18
|
+
* rather than reusing the originals by reference, is deliberate: this file is
|
|
19
|
+
* the one place that states what crosses the native boundary, in the same
|
|
20
|
+
* structural-typing style as `babylon-types.ts` and the XR Blocks `XB*Like`
|
|
21
|
+
* types.
|
|
17
22
|
*
|
|
18
23
|
* Values that cross are plain: numbers, strings, booleans and tuples. No
|
|
19
|
-
* engine objects in either direction, and assets stay
|
|
20
|
-
*
|
|
24
|
+
* engine objects in either direction, and assets stay on the native side.
|
|
25
|
+
* Units are metres, seconds and radians; poses are world space, quaternions
|
|
26
|
+
* `[x, y, z, w]`, right handed, +Y up.
|
|
27
|
+
*/
|
|
28
|
+
import type { HeadPose, InputHitHint, InputSourceSnapshot, PoseTuple, QuatTuple, RayTuple, Unsubscribe, Vec3Tuple } from "@realitycollective/webxr-input";
|
|
29
|
+
import type { HoldRelease } from "@realitycollective/webxr-interactions";
|
|
30
|
+
/**
|
|
31
|
+
* What the host knows about its session, from which this package derives
|
|
32
|
+
* `InputCapabilities` exactly as `IWSDKInputProvider.refreshCapabilities`
|
|
33
|
+
* does from the WebXR session.
|
|
34
|
+
*/
|
|
35
|
+
export interface NativeInputFacts {
|
|
36
|
+
/** A session is presenting: OpenXR `SYNCHRONIZED`, `VISIBLE` or `FOCUSED`. IWSDK: `world.session` exists. */
|
|
37
|
+
immersive: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* The session has input focus: OpenXR `FOCUSED`. While false no sources are
|
|
40
|
+
* sampled, as IWSDK's provider returns none unless the visibility state is
|
|
41
|
+
* `Visible`.
|
|
42
|
+
*/
|
|
43
|
+
focused: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Hand tracking is enabled on the session (`XR_EXT_hand_tracking` and the
|
|
46
|
+
* system supports it). IWSDK: `session.enabledFeatures` includes
|
|
47
|
+
* `"hand-tracking"`. A tracked hand source also counts, without this.
|
|
48
|
+
*/
|
|
49
|
+
handTracking: boolean;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* What one side's presence visuals should show, handed to the host. The host
|
|
53
|
+
* draws exactly this and decides nothing: which family is shown for
|
|
54
|
+
* `"auto"`, and which side a request targets, are this package's.
|
|
55
|
+
*/
|
|
56
|
+
export interface NativePresenceShown {
|
|
57
|
+
/** Draw this side's hand mesh. */
|
|
58
|
+
hand: boolean;
|
|
59
|
+
/** Draw this side's controller model. */
|
|
60
|
+
controller: boolean;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The native host's `input` slice. The host reports facts, sources and
|
|
64
|
+
* signals; `NativeInputProvider` turns them into the `InputProvider`
|
|
65
|
+
* contract.
|
|
21
66
|
*/
|
|
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
67
|
export interface NativeInputHost {
|
|
25
|
-
|
|
26
|
-
|
|
68
|
+
/** This moment's session facts. Read at construction and on every signal. */
|
|
69
|
+
getFacts(): NativeInputFacts;
|
|
70
|
+
/** The facts changed: session start or end, focus gained or lost. */
|
|
71
|
+
onFactsChanged(listener: () => void): Unsubscribe;
|
|
72
|
+
/** A source connected or disconnected (WebXR `inputsourceschange`). Capabilities re-derive on it. */
|
|
27
73
|
onSourcesChanged(listener: () => void): Unsubscribe;
|
|
74
|
+
/**
|
|
75
|
+
* This frame's tracked sources, in the `InputSourceSnapshot` shape. `kind`
|
|
76
|
+
* is `"hand"` while hand joints are tracked, else `"controller"`.
|
|
77
|
+
* `select` is the trigger value, or 1 while the runtime reports selecting
|
|
78
|
+
* (a hand pinch), 0..1; `squeeze` is the grip value, 0 for hands.
|
|
79
|
+
* `gripPose` is the WebXR GRIP frame, not a hand joint - see
|
|
80
|
+
* `InputSourceSnapshot.gripPose`. `indexTip` is the index fingertip for a
|
|
81
|
+
* hand and the ray origin for a controller. `hapticsAvailable` is true when
|
|
82
|
+
* the source has an actuator. The host may reuse its buffers: this package
|
|
83
|
+
* copies every snapshot.
|
|
84
|
+
*/
|
|
28
85
|
sample(): readonly InputSourceSnapshot[];
|
|
86
|
+
/** The viewer's head pose this frame. Present on any host that tracks a head; capabilities `gaze` and `headPose` follow it. */
|
|
29
87
|
getHeadPose?(): HeadPose;
|
|
88
|
+
/**
|
|
89
|
+
* Pre-resolved targeting hints, for a host with its own targeting. Frame
|
|
90
|
+
* fresh: the hints for the frame `sample()` just reported. A hint beats the
|
|
91
|
+
* core's own hit tests, and is equivalent to `nativeGrabbing` for a grab.
|
|
92
|
+
*/
|
|
30
93
|
sampleHints?(): readonly InputHitHint[];
|
|
94
|
+
/**
|
|
95
|
+
* Fire a haptic pulse on a source. `intensity` 0..1 (already clamped),
|
|
96
|
+
* `durationMs` in milliseconds. Returns false when it could not be
|
|
97
|
+
* delivered. IWSDK: `actuator.pulse(intensity, durationMs)`.
|
|
98
|
+
*/
|
|
31
99
|
pulse?(sourceId: string, intensity: number, durationMs: number): boolean;
|
|
32
|
-
|
|
33
|
-
|
|
100
|
+
/**
|
|
101
|
+
* Show or hide one side's hand mesh and controller model, as decided here
|
|
102
|
+
* (`IWSDKInputProvider.applyPresence`). Presence is the MODELS only: the
|
|
103
|
+
* host never draws a ray for a hand, and it draws a cursor disc at every
|
|
104
|
+
* ray's hit on a panel or an interactable whatever presence says, as
|
|
105
|
+
* IWSDK's `CursorVisual` is. Called only when a side's result changed.
|
|
106
|
+
* Without this member `capabilities.presence` is false.
|
|
107
|
+
*/
|
|
108
|
+
applyPresence?(side: "left" | "right", shown: NativePresenceShown): void;
|
|
34
109
|
}
|
|
35
|
-
/** What the host's ray
|
|
110
|
+
/** What the host's ray or proximity query reports: the target it reached, if any. */
|
|
36
111
|
export interface NativeHit {
|
|
37
|
-
/** The target id the app registered with
|
|
112
|
+
/** The target id the app registered with the native scene. */
|
|
38
113
|
targetId: string;
|
|
39
|
-
/**
|
|
114
|
+
/** `hitRay`: the ray parameter t, metres. `hitProximity`: metres to the target's SURFACE, never negative. */
|
|
40
115
|
distance: number;
|
|
41
|
-
/** World-space hit or
|
|
116
|
+
/** World-space hit point, or the target's centre. */
|
|
42
117
|
point: Vec3Tuple;
|
|
43
118
|
}
|
|
44
119
|
/**
|
|
45
|
-
* The native host's `interactions` slice: `HitTester`
|
|
46
|
-
* `TransformPort` with every member keyed by the target id the
|
|
47
|
-
* when it registered the object with the native scene
|
|
48
|
-
* object serves every interactable rather than one port per object.
|
|
120
|
+
* The native host's `interactions` slice: `HitTester` with its semantics
|
|
121
|
+
* stated, and `TransformPort` with every member keyed by the target id the
|
|
122
|
+
* app chose when it registered the object with the native scene.
|
|
49
123
|
*/
|
|
50
124
|
export interface NativeInteractionHost {
|
|
125
|
+
/**
|
|
126
|
+
* The nearest shown target along `ray` (origin in metres, direction
|
|
127
|
+
* normalised). A target counts when its centre is within its radius of the
|
|
128
|
+
* ray line and in front of the origin; `distance` is the ray parameter of
|
|
129
|
+
* the closest point, and `t <= 0` never hits. A host that tests triangle
|
|
130
|
+
* meshes instead may report the surface it hit; the contract cases accept
|
|
131
|
+
* any answer within the target's radius plus 0.05 m of the sphere answer.
|
|
132
|
+
* IWSDK: `EntityHitTester.hitRay`.
|
|
133
|
+
*/
|
|
51
134
|
hitRay(ray: RayTuple): NativeHit | null;
|
|
135
|
+
/**
|
|
136
|
+
* The nearest shown target whose SURFACE is within `radius` metres of
|
|
137
|
+
* `point`. `distance = max(0, |centre - point| - targetRadius)`: a point
|
|
138
|
+
* 3 cm outside a 10 cm target reports 0.03, a point inside reports 0.
|
|
139
|
+
* Never the distance to the centre. IWSDK: `EntityHitTester.hitProximity`.
|
|
140
|
+
*/
|
|
52
141
|
hitProximity(point: Vec3Tuple, radius: number): NativeHit | null;
|
|
142
|
+
/**
|
|
143
|
+
* The radius, in metres, the host hit-tests a registered target with.
|
|
144
|
+
* Called once per registration with the app's `targetRadius`, or 0.1 when
|
|
145
|
+
* it gave none, as IWSDK registers a bare target as a 10 cm sphere
|
|
146
|
+
* (`register.ts`, `options.targetRadius ?? 0.1`). A host never excludes a
|
|
147
|
+
* target from hit testing because of its radius.
|
|
148
|
+
*/
|
|
149
|
+
setTargetRadius(targetId: string, radius: number): void;
|
|
53
150
|
/** Where the object is now. */
|
|
54
151
|
getWorldPose(targetId: string): PoseTuple;
|
|
55
152
|
/** The rest pose captured at registration, in world space. */
|
|
56
153
|
getRestWorldPose(targetId: string): PoseTuple;
|
|
154
|
+
/** The offset from rest last written, metres, in the rest frame. */
|
|
57
155
|
getLocalOffset(targetId: string): Vec3Tuple;
|
|
156
|
+
/** Offset the object from its rest pose, metres, in the rest frame. */
|
|
58
157
|
setLocalOffset(targetId: string, offset: Vec3Tuple): void;
|
|
158
|
+
/** Rotate the object from its rest orientation. */
|
|
59
159
|
setLocalRotation(targetId: string, quaternion: QuatTuple): void;
|
|
160
|
+
/** Place the object at a world pose (the pose-only grab carry). */
|
|
60
161
|
setWorldPose?(targetId: string, pose: PoseTuple): void;
|
|
162
|
+
/**
|
|
163
|
+
* The pulse effect: `scale` multiplies the rest scale, `emissive` is added
|
|
164
|
+
* to the base emissive intensity. Last write wins per field.
|
|
165
|
+
*/
|
|
61
166
|
setEffect?(targetId: string, effect: {
|
|
62
167
|
scale?: number;
|
|
63
168
|
emissive?: number;
|
|
64
169
|
}): void;
|
|
170
|
+
/**
|
|
171
|
+
* A pose-only grab started: suspend this target's physics body, if it has
|
|
172
|
+
* one, so it follows `setWorldPose` exactly. REQUIRED when grabs are
|
|
173
|
+
* pose-only (`nativeGrab` off). A target with no body needs nothing.
|
|
174
|
+
* IWSDK: `beginHold` removes the `PhysicsBody` (`register.ts`,
|
|
175
|
+
* `physicsBindingFor`).
|
|
176
|
+
*/
|
|
177
|
+
beginHold(targetId: string): void;
|
|
178
|
+
/**
|
|
179
|
+
* The grab ended: resume the body with `release` as its velocity (linear
|
|
180
|
+
* m/s and angular rad/s, world space; zeros for a synthesized release,
|
|
181
|
+
* which rests). A target with NO physics body rests where it was released,
|
|
182
|
+
* as on IWSDK where there is no body to re-add. IWSDK: `endHold` re-adds
|
|
183
|
+
* the `PhysicsBody` and, for a non-zero velocity, a `PhysicsManipulation`.
|
|
184
|
+
*/
|
|
185
|
+
endHold(targetId: string, release: HoldRelease): void;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Test-only readbacks a host provides so the host conformance kit
|
|
189
|
+
* (`nativeInteractionsHostConformanceCases`) can check what the host
|
|
190
|
+
* actually did. A shipping host may omit them.
|
|
191
|
+
*/
|
|
192
|
+
export interface NativeInteractionsTestHost {
|
|
193
|
+
/** Put a shown, hit-testable target of `radius` metres at `position`, as the app's scene would. */
|
|
194
|
+
placeTarget(targetId: string, position: Vec3Tuple, radius: number): void;
|
|
195
|
+
/** Remove every target `placeTarget` put in. */
|
|
196
|
+
clearTargets(): void;
|
|
197
|
+
/** What the host draws for one side now. */
|
|
198
|
+
presenceShown(side: "left" | "right"): NativePresenceShown | undefined;
|
|
199
|
+
/** The last release the host received for a target through `endHold`. */
|
|
200
|
+
lastRelease(targetId: string): HoldRelease | undefined;
|
|
201
|
+
/** Every cursor disc the host draws now, as world positions. */
|
|
202
|
+
cursors(): Vec3Tuple[];
|
|
65
203
|
}
|
|
66
204
|
/**
|
|
67
205
|
* The native app's frame callback, the root member of `__rcHost` that
|
package/dist/native-types.js.map
CHANGED
|
@@ -1 +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"]}
|
|
1
|
+
{"version":3,"file":"native-types.js","sourceRoot":"","sources":["../src/native-types.ts"],"names":[],"mappings":"AAsOA;;;;;GAKG;AACH,MAAM,UAAU,aAAa;IAC3B,OAAQ,UAAoF,CAAC,QAAQ,CAAC;AACxG,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAC9B,IAAO,EACP,QAAyC;IAEzC,MAAM,KAAK,GAAG,QAAQ,IAAK,aAAa,EAA4C,EAAE,CAAC,IAAI,CAAC,CAAC;IAC7F,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CACb,+CAA+C,IAAI,gDAAgD,IAAI,6GAA6G,CACrN,CAAC;IACJ,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,8EAA8E;AAC9E,6EAA6E;AAC7E,4EAA4E;AAC5E,8EAA8E;AAE9E,6BAA6B;AAC7B,MAAM,UAAU,QAAQ,CAAC,CAAY;IACnC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAC5B,CAAC;AAED,iCAAiC;AACjC,MAAM,UAAU,QAAQ,CAAC,CAAY;IACnC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAClC,CAAC;AAED,4CAA4C;AAC5C,MAAM,UAAU,QAAQ,CAAC,IAAe;IACtC,OAAO,EAAE,QAAQ,EAAE,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,UAAU,EAAE,QAAQ,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;AACtF,CAAC;AAED,uCAAuC;AACvC,MAAM,UAAU,OAAO,CAAC,GAAa;IACnC,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,SAAS,EAAE,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC;AAC9E,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAAC,MAA2B;IACtD,MAAM,IAAI,GAAwB;QAChC,EAAE,EAAE,MAAM,CAAC,EAAE;QACb,IAAI,EAAE,MAAM,CAAC,IAAI;QACjB,UAAU,EAAE,MAAM,CAAC,UAAU;QAC7B,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,OAAO,EAAE,MAAM,CAAC,OAAO;KACxB,CAAC;IACF,IAAI,MAAM,CAAC,GAAG;QAAE,IAAI,CAAC,GAAG,GAAG,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAC/C,IAAI,MAAM,CAAC,QAAQ;QAAE,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IAC/D,IAAI,MAAM,CAAC,QAAQ;QAAE,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IAC/D,IAAI,MAAM,CAAC,cAAc;QAAE,IAAI,CAAC,cAAc,GAAG,QAAQ,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC;IACjF,IAAI,MAAM,CAAC,eAAe;QAAE,IAAI,CAAC,eAAe,GAAG,QAAQ,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC;IACpF,IAAI,MAAM,CAAC,cAAc,KAAK,SAAS;QAAE,IAAI,CAAC,cAAc,GAAG,MAAM,CAAC,cAAc,CAAC;IACrF,IAAI,MAAM,CAAC,gBAAgB,KAAK,SAAS;QAAE,IAAI,CAAC,gBAAgB,GAAG,MAAM,CAAC,gBAAgB,CAAC;IAC3F,OAAO,IAAI,CAAC;AACd,CAAC","sourcesContent":["/**\n * The `input` and `interactions` slices a native host (OpenXR on Quest,\n * CompositorServices on visionOS, or any other shell embedding a JavaScript\n * engine such as Hermes) installs on `globalThis.__rcHost`.\n *\n * A HOST IS HANDED RESULTS, NOT RULES. Every rule the IWSDK binding applies\n * in its provider runs in this package from the same inputs: capabilities\n * are derived here from the facts the host reports, the presence modality\n * is decided here and handed to the host per side, and a target's radius is\n * handed to the host at registration. The host measures, renders and\n * reports. Each member below states what the host does, in what units and\n * with what sign, and which IWSDK line it stands in for.\n *\n * `NativeInteractionHost` is `HitTester` plus `TransformPort` from\n * `@realitycollective/webxr-interactions`, with a `targetId` added to every\n * `TransformPort` member because that port is per object and the host is one\n * object serving every registered interactable. Declaring the shapes here,\n * rather than reusing the originals by reference, is deliberate: this file is\n * the one place that states what crosses the native boundary, in the same\n * structural-typing style as `babylon-types.ts` and the XR Blocks `XB*Like`\n * types.\n *\n * Values that cross are plain: numbers, strings, booleans and tuples. No\n * engine objects in either direction, and assets stay on the native side.\n * Units are metres, seconds and radians; poses are world space, quaternions\n * `[x, y, z, w]`, right handed, +Y up.\n */\nimport type {\n HeadPose,\n InputHitHint,\n InputSourceSnapshot,\n PoseTuple,\n QuatTuple,\n RayTuple,\n Unsubscribe,\n Vec3Tuple,\n} from \"@realitycollective/webxr-input\";\nimport type { HoldRelease } from \"@realitycollective/webxr-interactions\";\n\n/**\n * What the host knows about its session, from which this package derives\n * `InputCapabilities` exactly as `IWSDKInputProvider.refreshCapabilities`\n * does from the WebXR session.\n */\nexport interface NativeInputFacts {\n /** A session is presenting: OpenXR `SYNCHRONIZED`, `VISIBLE` or `FOCUSED`. IWSDK: `world.session` exists. */\n immersive: boolean;\n /**\n * The session has input focus: OpenXR `FOCUSED`. While false no sources are\n * sampled, as IWSDK's provider returns none unless the visibility state is\n * `Visible`.\n */\n focused: boolean;\n /**\n * Hand tracking is enabled on the session (`XR_EXT_hand_tracking` and the\n * system supports it). IWSDK: `session.enabledFeatures` includes\n * `\"hand-tracking\"`. A tracked hand source also counts, without this.\n */\n handTracking: boolean;\n}\n\n/**\n * What one side's presence visuals should show, handed to the host. The host\n * draws exactly this and decides nothing: which family is shown for\n * `\"auto\"`, and which side a request targets, are this package's.\n */\nexport interface NativePresenceShown {\n /** Draw this side's hand mesh. */\n hand: boolean;\n /** Draw this side's controller model. */\n controller: boolean;\n}\n\n/**\n * The native host's `input` slice. The host reports facts, sources and\n * signals; `NativeInputProvider` turns them into the `InputProvider`\n * contract.\n */\nexport interface NativeInputHost {\n /** This moment's session facts. Read at construction and on every signal. */\n getFacts(): NativeInputFacts;\n /** The facts changed: session start or end, focus gained or lost. */\n onFactsChanged(listener: () => void): Unsubscribe;\n /** A source connected or disconnected (WebXR `inputsourceschange`). Capabilities re-derive on it. */\n onSourcesChanged(listener: () => void): Unsubscribe;\n /**\n * This frame's tracked sources, in the `InputSourceSnapshot` shape. `kind`\n * is `\"hand\"` while hand joints are tracked, else `\"controller\"`.\n * `select` is the trigger value, or 1 while the runtime reports selecting\n * (a hand pinch), 0..1; `squeeze` is the grip value, 0 for hands.\n * `gripPose` is the WebXR GRIP frame, not a hand joint - see\n * `InputSourceSnapshot.gripPose`. `indexTip` is the index fingertip for a\n * hand and the ray origin for a controller. `hapticsAvailable` is true when\n * the source has an actuator. The host may reuse its buffers: this package\n * copies every snapshot.\n */\n sample(): readonly InputSourceSnapshot[];\n /** The viewer's head pose this frame. Present on any host that tracks a head; capabilities `gaze` and `headPose` follow it. */\n getHeadPose?(): HeadPose;\n /**\n * Pre-resolved targeting hints, for a host with its own targeting. Frame\n * fresh: the hints for the frame `sample()` just reported. A hint beats the\n * core's own hit tests, and is equivalent to `nativeGrabbing` for a grab.\n */\n sampleHints?(): readonly InputHitHint[];\n /**\n * Fire a haptic pulse on a source. `intensity` 0..1 (already clamped),\n * `durationMs` in milliseconds. Returns false when it could not be\n * delivered. IWSDK: `actuator.pulse(intensity, durationMs)`.\n */\n pulse?(sourceId: string, intensity: number, durationMs: number): boolean;\n /**\n * Show or hide one side's hand mesh and controller model, as decided here\n * (`IWSDKInputProvider.applyPresence`). Presence is the MODELS only: the\n * host never draws a ray for a hand, and it draws a cursor disc at every\n * ray's hit on a panel or an interactable whatever presence says, as\n * IWSDK's `CursorVisual` is. Called only when a side's result changed.\n * Without this member `capabilities.presence` is false.\n */\n applyPresence?(side: \"left\" | \"right\", shown: NativePresenceShown): void;\n}\n\n/** What the host's ray or proximity query reports: the target it reached, if any. */\nexport interface NativeHit {\n /** The target id the app registered with the native scene. */\n targetId: string;\n /** `hitRay`: the ray parameter t, metres. `hitProximity`: metres to the target's SURFACE, never negative. */\n distance: number;\n /** World-space hit point, or the target's centre. */\n point: Vec3Tuple;\n}\n\n/**\n * The native host's `interactions` slice: `HitTester` with its semantics\n * stated, and `TransformPort` with every member keyed by the target id the\n * app chose when it registered the object with the native scene.\n */\nexport interface NativeInteractionHost {\n /**\n * The nearest shown target along `ray` (origin in metres, direction\n * normalised). A target counts when its centre is within its radius of the\n * ray line and in front of the origin; `distance` is the ray parameter of\n * the closest point, and `t <= 0` never hits. A host that tests triangle\n * meshes instead may report the surface it hit; the contract cases accept\n * any answer within the target's radius plus 0.05 m of the sphere answer.\n * IWSDK: `EntityHitTester.hitRay`.\n */\n hitRay(ray: RayTuple): NativeHit | null;\n /**\n * The nearest shown target whose SURFACE is within `radius` metres of\n * `point`. `distance = max(0, |centre - point| - targetRadius)`: a point\n * 3 cm outside a 10 cm target reports 0.03, a point inside reports 0.\n * Never the distance to the centre. IWSDK: `EntityHitTester.hitProximity`.\n */\n hitProximity(point: Vec3Tuple, radius: number): NativeHit | null;\n /**\n * The radius, in metres, the host hit-tests a registered target with.\n * Called once per registration with the app's `targetRadius`, or 0.1 when\n * it gave none, as IWSDK registers a bare target as a 10 cm sphere\n * (`register.ts`, `options.targetRadius ?? 0.1`). A host never excludes a\n * target from hit testing because of its radius.\n */\n setTargetRadius(targetId: string, radius: number): void;\n /** Where the object is now. */\n getWorldPose(targetId: string): PoseTuple;\n /** The rest pose captured at registration, in world space. */\n getRestWorldPose(targetId: string): PoseTuple;\n /** The offset from rest last written, metres, in the rest frame. */\n getLocalOffset(targetId: string): Vec3Tuple;\n /** Offset the object from its rest pose, metres, in the rest frame. */\n setLocalOffset(targetId: string, offset: Vec3Tuple): void;\n /** Rotate the object from its rest orientation. */\n setLocalRotation(targetId: string, quaternion: QuatTuple): void;\n /** Place the object at a world pose (the pose-only grab carry). */\n setWorldPose?(targetId: string, pose: PoseTuple): void;\n /**\n * The pulse effect: `scale` multiplies the rest scale, `emissive` is added\n * to the base emissive intensity. Last write wins per field.\n */\n setEffect?(targetId: string, effect: { scale?: number; emissive?: number }): void;\n /**\n * A pose-only grab started: suspend this target's physics body, if it has\n * one, so it follows `setWorldPose` exactly. REQUIRED when grabs are\n * pose-only (`nativeGrab` off). A target with no body needs nothing.\n * IWSDK: `beginHold` removes the `PhysicsBody` (`register.ts`,\n * `physicsBindingFor`).\n */\n beginHold(targetId: string): void;\n /**\n * The grab ended: resume the body with `release` as its velocity (linear\n * m/s and angular rad/s, world space; zeros for a synthesized release,\n * which rests). A target with NO physics body rests where it was released,\n * as on IWSDK where there is no body to re-add. IWSDK: `endHold` re-adds\n * the `PhysicsBody` and, for a non-zero velocity, a `PhysicsManipulation`.\n */\n endHold(targetId: string, release: HoldRelease): void;\n}\n\n/**\n * Test-only readbacks a host provides so the host conformance kit\n * (`nativeInteractionsHostConformanceCases`) can check what the host\n * actually did. A shipping host may omit them.\n */\nexport interface NativeInteractionsTestHost {\n /** Put a shown, hit-testable target of `radius` metres at `position`, as the app's scene would. */\n placeTarget(targetId: string, position: Vec3Tuple, radius: number): void;\n /** Remove every target `placeTarget` put in. */\n clearTargets(): void;\n /** What the host draws for one side now. */\n presenceShown(side: \"left\" | \"right\"): NativePresenceShown | undefined;\n /** The last release the host received for a target through `endHold`. */\n lastRelease(targetId: string): HoldRelease | undefined;\n /** Every cursor disc the host draws now, as world positions. */\n cursors(): Vec3Tuple[];\n}\n\n/**\n * The native app's frame callback, the root member of `__rcHost` that\n * `NativeInteractions` attaches to when `attachToHost` is set.\n */\nexport interface NativeFrameSource {\n onFrame(callback: (timestampMs: number, deltaS: number) => void): () => void;\n}\n\n/** The two slices this package reads off `globalThis.__rcHost`. */\nexport interface NativeHostSlices {\n input: NativeInputHost;\n interactions: NativeInteractionHost;\n}\n\n/**\n * `globalThis.__rcHost`, read defensively: the root `NativeHost` interface\n * belongs to `service-framework-native`, which this package does not depend\n * on, so the global is read as an unknown bag of optional slices rather than\n * imported.\n */\nexport function installedHost(): (Partial<NativeHostSlices> & Partial<NativeFrameSource>) | undefined {\n return (globalThis as { __rcHost?: Partial<NativeHostSlices> & Partial<NativeFrameSource> }).__rcHost;\n}\n\n/**\n * Resolve one slice: the value passed in, or `globalThis.__rcHost`'s slice\n * of the same name. Throws one clear error naming the missing slice, so a\n * native-interactions class fails at construction rather than the first\n * time something calls a method that is not there.\n */\nexport function resolveHostSlice<K extends keyof NativeHostSlices>(\n name: K,\n injected: NativeHostSlices[K] | undefined,\n): NativeHostSlices[K] {\n const slice = injected ?? (installedHost() as Partial<NativeHostSlices> | undefined)?.[name];\n if (!slice) {\n throw new Error(\n `@realitycollective/native-interactions: no \"${name}\" slice was supplied and globalThis.__rcHost.${name} is not installed. Pass one directly, or have the native app install it before this package is constructed.`,\n );\n }\n return slice;\n}\n\n// ---------------------------------------------------------------------------\n// Copies. A host may reuse its own buffers across calls, so every tuple that\n// crosses back into this package is copied on the way in, never referenced.\n// ---------------------------------------------------------------------------\n\n/** Copy a position tuple. */\nexport function copyVec3(v: Vec3Tuple): Vec3Tuple {\n return [v[0], v[1], v[2]];\n}\n\n/** Copy an orientation tuple. */\nexport function copyQuat(q: QuatTuple): QuatTuple {\n return [q[0], q[1], q[2], q[3]];\n}\n\n/** Copy a pose (position + orientation). */\nexport function copyPose(pose: PoseTuple): PoseTuple {\n return { position: copyVec3(pose.position), quaternion: copyQuat(pose.quaternion) };\n}\n\n/** Copy a ray (origin + direction). */\nexport function copyRay(ray: RayTuple): RayTuple {\n return { origin: copyVec3(ray.origin), direction: copyVec3(ray.direction) };\n}\n\n/**\n * Copy one `InputSourceSnapshot` field by field, including every tuple it\n * carries, so a host that pools and refills its own snapshot objects still\n * meets the ownership rule: a snapshot handed to `sample()`'s caller is\n * never written to again.\n */\nexport function copySnapshot(source: InputSourceSnapshot): InputSourceSnapshot {\n const copy: InputSourceSnapshot = {\n id: source.id,\n kind: source.kind,\n handedness: source.handedness,\n select: source.select,\n squeeze: source.squeeze,\n };\n if (source.ray) copy.ray = copyRay(source.ray);\n if (source.gripPose) copy.gripPose = copyPose(source.gripPose);\n if (source.indexTip) copy.indexTip = copyVec3(source.indexTip);\n if (source.linearVelocity) copy.linearVelocity = copyVec3(source.linearVelocity);\n if (source.angularVelocity) copy.angularVelocity = copyVec3(source.angularVelocity);\n if (source.nativeGrabbing !== undefined) copy.nativeGrabbing = source.nativeGrabbing;\n if (source.hapticsAvailable !== undefined) copy.hapticsAvailable = source.hapticsAvailable;\n return copy;\n}\n"]}
|
package/dist/provider.d.ts
CHANGED
|
@@ -1,26 +1,51 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* NativeInputProvider - the native host's input provider.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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.
|
|
4
|
+
* It applies the rules `IWSDKInputProvider` applies, over the facts the
|
|
5
|
+
* native app's `input` slice reports, so the host is handed results:
|
|
9
6
|
*
|
|
10
|
-
*
|
|
11
|
-
* `
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
7
|
+
* - Capabilities are DERIVED here, as `refreshCapabilities` derives them from
|
|
8
|
+
* the WebXR session: `rays`, `pokes` and grabs from immersion, `handJoints`
|
|
9
|
+
* and `pinch` from hand tracking or a tracked hand, `buttonsAxes` and
|
|
10
|
+
* `haptics` from the sources present, `gaze` and `headPose` from a head
|
|
11
|
+
* pose, `presence` from the host's presence member. They re-derive, and
|
|
12
|
+
* `onCapabilitiesChanged` fires, whenever the facts or the sources change.
|
|
13
|
+
* - Presence is decided here, as `applyPresence` decides it: the requested
|
|
14
|
+
* visibility per side, and the modality, with `"auto"` showing hands while
|
|
15
|
+
* hand joints are live and controllers otherwise. The host is told what to
|
|
16
|
+
* draw per side.
|
|
17
|
+
* - No sources are sampled while the session lacks focus, as IWSDK samples
|
|
18
|
+
* none unless its visibility state is `Visible`.
|
|
19
|
+
* - A pulse's intensity is clamped to 0..1 before it reaches the host.
|
|
20
|
+
*
|
|
21
|
+
* Every snapshot, and every tuple inside it, is copied before it leaves this
|
|
22
|
+
* class, so a host that reuses its own sample buffers still meets the
|
|
23
|
+
* ownership rule the shared contract suite checks.
|
|
15
24
|
*/
|
|
16
25
|
import { type Handedness, type HeadPose, type InputCapabilities, type InputHitHint, type InputProvider, type InputSourceSnapshot, type PresenceModality, type Unsubscribe } from "@realitycollective/webxr-input";
|
|
17
26
|
import { type NativeInputHost } from "./native-types.js";
|
|
18
27
|
export interface NativeInputProviderOptions {
|
|
19
28
|
/** The `input` slice. Omit to read `globalThis.__rcHost.input`. */
|
|
20
29
|
input?: NativeInputHost;
|
|
30
|
+
/**
|
|
31
|
+
* The native app fulfils grabs with its own physics, so grabs report
|
|
32
|
+
* `"native"` and the runtime never carries the object. Default `false`:
|
|
33
|
+
* `"poseOnly"`, the runtime carries it. The same option as IWSDK's
|
|
34
|
+
* `IWSDKProviderOptions.nativeGrab`.
|
|
35
|
+
*/
|
|
36
|
+
nativeGrab?: boolean;
|
|
21
37
|
}
|
|
22
38
|
export declare class NativeInputProvider implements InputProvider {
|
|
23
39
|
private readonly host;
|
|
40
|
+
private readonly nativeGrab;
|
|
41
|
+
private capabilities;
|
|
42
|
+
private facts;
|
|
43
|
+
private readonly capsListeners;
|
|
44
|
+
private readonly sourceListeners;
|
|
45
|
+
private readonly disposers;
|
|
46
|
+
private readonly presenceVisible;
|
|
47
|
+
private presenceModality;
|
|
48
|
+
private readonly presenceApplied;
|
|
24
49
|
readonly getHeadPose?: () => HeadPose;
|
|
25
50
|
readonly sampleHints?: () => readonly InputHitHint[];
|
|
26
51
|
readonly pulse?: (sourceId: string, intensity: number, durationMs: number) => boolean;
|
|
@@ -31,4 +56,17 @@ export declare class NativeInputProvider implements InputProvider {
|
|
|
31
56
|
onCapabilitiesChanged(listener: (capabilities: InputCapabilities) => void): Unsubscribe;
|
|
32
57
|
onSourcesChanged(listener: () => void): Unsubscribe;
|
|
33
58
|
sample(): readonly InputSourceSnapshot[];
|
|
59
|
+
/** Release the host subscriptions and every listener. */
|
|
60
|
+
dispose(): void;
|
|
61
|
+
/** Re-read the facts and re-derive; publish and re-apply presence on a change. */
|
|
62
|
+
private refresh;
|
|
63
|
+
/** `IWSDKInputProvider.refreshCapabilities`, over the host's facts and sources. */
|
|
64
|
+
private derive;
|
|
65
|
+
/**
|
|
66
|
+
* `IWSDKInputProvider.applyPresence`: each side shows the family the
|
|
67
|
+
* modality picks, while that side is visible. Only a changed side is sent.
|
|
68
|
+
* Reports whether presence reached a live session, as IWSDK reports false
|
|
69
|
+
* with no session to draw in; the request is kept and applied on entry.
|
|
70
|
+
*/
|
|
71
|
+
private applyPresence;
|
|
34
72
|
}
|
package/dist/provider.js
CHANGED
|
@@ -1,22 +1,44 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* NativeInputProvider - the native host's input provider.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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.
|
|
4
|
+
* It applies the rules `IWSDKInputProvider` applies, over the facts the
|
|
5
|
+
* native app's `input` slice reports, so the host is handed results:
|
|
9
6
|
*
|
|
10
|
-
*
|
|
11
|
-
* `
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
7
|
+
* - Capabilities are DERIVED here, as `refreshCapabilities` derives them from
|
|
8
|
+
* the WebXR session: `rays`, `pokes` and grabs from immersion, `handJoints`
|
|
9
|
+
* and `pinch` from hand tracking or a tracked hand, `buttonsAxes` and
|
|
10
|
+
* `haptics` from the sources present, `gaze` and `headPose` from a head
|
|
11
|
+
* pose, `presence` from the host's presence member. They re-derive, and
|
|
12
|
+
* `onCapabilitiesChanged` fires, whenever the facts or the sources change.
|
|
13
|
+
* - Presence is decided here, as `applyPresence` decides it: the requested
|
|
14
|
+
* visibility per side, and the modality, with `"auto"` showing hands while
|
|
15
|
+
* hand joints are live and controllers otherwise. The host is told what to
|
|
16
|
+
* draw per side.
|
|
17
|
+
* - No sources are sampled while the session lacks focus, as IWSDK samples
|
|
18
|
+
* none unless its visibility state is `Visible`.
|
|
19
|
+
* - A pulse's intensity is clamped to 0..1 before it reaches the host.
|
|
20
|
+
*
|
|
21
|
+
* Every snapshot, and every tuple inside it, is copied before it leaves this
|
|
22
|
+
* class, so a host that reuses its own sample buffers still meets the
|
|
23
|
+
* ownership rule the shared contract suite checks.
|
|
15
24
|
*/
|
|
16
|
-
import {} from "@realitycollective/webxr-input";
|
|
17
|
-
import { copyPose, copySnapshot, resolveHostSlice } from "./native-types.js";
|
|
25
|
+
import { NO_CAPABILITIES, } from "@realitycollective/webxr-input";
|
|
26
|
+
import { copyPose, copySnapshot, resolveHostSlice, } from "./native-types.js";
|
|
27
|
+
const SIDES = ["left", "right"];
|
|
28
|
+
function clampUnit(value) {
|
|
29
|
+
return Math.min(1, Math.max(0, value));
|
|
30
|
+
}
|
|
18
31
|
export class NativeInputProvider {
|
|
19
32
|
host;
|
|
33
|
+
nativeGrab;
|
|
34
|
+
capabilities;
|
|
35
|
+
facts;
|
|
36
|
+
capsListeners = new Set();
|
|
37
|
+
sourceListeners = new Set();
|
|
38
|
+
disposers = [];
|
|
39
|
+
presenceVisible = { left: true, right: true };
|
|
40
|
+
presenceModality = "auto";
|
|
41
|
+
presenceApplied = { left: null, right: null };
|
|
20
42
|
getHeadPose;
|
|
21
43
|
sampleHints;
|
|
22
44
|
pulse;
|
|
@@ -24,38 +46,120 @@ export class NativeInputProvider {
|
|
|
24
46
|
setPresenceModality;
|
|
25
47
|
constructor(options = {}) {
|
|
26
48
|
this.host = resolveHostSlice("input", options.input);
|
|
27
|
-
|
|
28
|
-
|
|
49
|
+
this.nativeGrab = options.nativeGrab ?? false;
|
|
50
|
+
this.facts = { ...this.host.getFacts() };
|
|
51
|
+
this.capabilities = this.derive();
|
|
52
|
+
const host = this.host;
|
|
53
|
+
if (host.getHeadPose) {
|
|
29
54
|
this.getHeadPose = () => copyPose(host.getHeadPose());
|
|
30
55
|
}
|
|
31
|
-
if (
|
|
32
|
-
const host = this.host;
|
|
56
|
+
if (host.sampleHints) {
|
|
33
57
|
this.sampleHints = () => host.sampleHints().map((hint) => ({ ...hint }));
|
|
34
58
|
}
|
|
35
|
-
if (
|
|
36
|
-
|
|
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);
|
|
59
|
+
if (host.pulse) {
|
|
60
|
+
this.pulse = (sourceId, intensity, durationMs) => host.pulse(sourceId, clampUnit(intensity), durationMs);
|
|
42
61
|
}
|
|
43
|
-
if (
|
|
44
|
-
|
|
45
|
-
|
|
62
|
+
if (host.applyPresence) {
|
|
63
|
+
this.setPresenceVisible = (target, visible) => {
|
|
64
|
+
for (const side of SIDES) {
|
|
65
|
+
if (target === "all" || target === side)
|
|
66
|
+
this.presenceVisible[side] = visible;
|
|
67
|
+
}
|
|
68
|
+
return this.applyPresence();
|
|
69
|
+
};
|
|
70
|
+
this.setPresenceModality = (mode) => {
|
|
71
|
+
this.presenceModality = mode;
|
|
72
|
+
return this.applyPresence();
|
|
73
|
+
};
|
|
46
74
|
}
|
|
75
|
+
this.disposers.push(host.onFactsChanged(() => this.refresh()), host.onSourcesChanged(() => {
|
|
76
|
+
this.refresh();
|
|
77
|
+
for (const listener of [...this.sourceListeners])
|
|
78
|
+
listener();
|
|
79
|
+
}));
|
|
80
|
+
this.applyPresence();
|
|
47
81
|
}
|
|
48
82
|
getCapabilities() {
|
|
49
|
-
return this.
|
|
83
|
+
return this.capabilities;
|
|
50
84
|
}
|
|
51
85
|
onCapabilitiesChanged(listener) {
|
|
52
|
-
|
|
86
|
+
this.capsListeners.add(listener);
|
|
87
|
+
return () => this.capsListeners.delete(listener);
|
|
53
88
|
}
|
|
54
89
|
onSourcesChanged(listener) {
|
|
55
|
-
|
|
90
|
+
this.sourceListeners.add(listener);
|
|
91
|
+
return () => this.sourceListeners.delete(listener);
|
|
56
92
|
}
|
|
57
93
|
sample() {
|
|
94
|
+
if (!this.facts.focused)
|
|
95
|
+
return [];
|
|
58
96
|
return this.host.sample().map(copySnapshot);
|
|
59
97
|
}
|
|
98
|
+
/** Release the host subscriptions and every listener. */
|
|
99
|
+
dispose() {
|
|
100
|
+
for (const dispose of this.disposers)
|
|
101
|
+
dispose();
|
|
102
|
+
this.disposers.length = 0;
|
|
103
|
+
this.capsListeners.clear();
|
|
104
|
+
this.sourceListeners.clear();
|
|
105
|
+
}
|
|
106
|
+
/** Re-read the facts and re-derive; publish and re-apply presence on a change. */
|
|
107
|
+
refresh() {
|
|
108
|
+
this.facts = { ...this.host.getFacts() };
|
|
109
|
+
const next = this.derive();
|
|
110
|
+
if (JSON.stringify(next) !== JSON.stringify(this.capabilities)) {
|
|
111
|
+
this.capabilities = next;
|
|
112
|
+
for (const listener of [...this.capsListeners])
|
|
113
|
+
listener(next);
|
|
114
|
+
}
|
|
115
|
+
this.applyPresence();
|
|
116
|
+
}
|
|
117
|
+
/** `IWSDKInputProvider.refreshCapabilities`, over the host's facts and sources. */
|
|
118
|
+
derive() {
|
|
119
|
+
const { immersive, handTracking } = this.facts;
|
|
120
|
+
const sources = immersive ? this.host.sample() : [];
|
|
121
|
+
const handTracked = sources.some((source) => source.kind === "hand");
|
|
122
|
+
const hands = immersive && (handTracking || handTracked);
|
|
123
|
+
const head = typeof this.host.getHeadPose === "function";
|
|
124
|
+
return {
|
|
125
|
+
...NO_CAPABILITIES,
|
|
126
|
+
rays: immersive,
|
|
127
|
+
// A controller's index tip is its ray origin, so pokes need no hand.
|
|
128
|
+
pokes: immersive,
|
|
129
|
+
grabs: immersive ? (this.nativeGrab ? "native" : "poseOnly") : "none",
|
|
130
|
+
handJoints: hands,
|
|
131
|
+
pinch: hands,
|
|
132
|
+
buttonsAxes: sources.some((source) => source.kind === "controller"),
|
|
133
|
+
gaze: head,
|
|
134
|
+
headPose: head,
|
|
135
|
+
haptics: typeof this.host.pulse === "function" && sources.some((source) => source.hapticsAvailable === true),
|
|
136
|
+
presence: typeof this.host.applyPresence === "function",
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* `IWSDKInputProvider.applyPresence`: each side shows the family the
|
|
141
|
+
* modality picks, while that side is visible. Only a changed side is sent.
|
|
142
|
+
* Reports whether presence reached a live session, as IWSDK reports false
|
|
143
|
+
* with no session to draw in; the request is kept and applied on entry.
|
|
144
|
+
*/
|
|
145
|
+
applyPresence() {
|
|
146
|
+
const apply = this.host.applyPresence;
|
|
147
|
+
if (!apply)
|
|
148
|
+
return false;
|
|
149
|
+
const handJoints = this.capabilities.handJoints;
|
|
150
|
+
const modality = this.presenceModality;
|
|
151
|
+
const hands = modality === "hands" || (modality === "auto" && handJoints);
|
|
152
|
+
const controllers = modality === "controllers" || (modality === "auto" && !handJoints);
|
|
153
|
+
for (const side of SIDES) {
|
|
154
|
+
const wanted = this.presenceVisible[side];
|
|
155
|
+
const shown = { hand: wanted && hands, controller: wanted && controllers };
|
|
156
|
+
const last = this.presenceApplied[side];
|
|
157
|
+
if (last && last.hand === shown.hand && last.controller === shown.controller)
|
|
158
|
+
continue;
|
|
159
|
+
this.presenceApplied[side] = shown;
|
|
160
|
+
apply.call(this.host, side, { ...shown });
|
|
161
|
+
}
|
|
162
|
+
return this.facts.immersive;
|
|
163
|
+
}
|
|
60
164
|
}
|
|
61
165
|
//# sourceMappingURL=provider.js.map
|
package/dist/provider.js.map
CHANGED
|
@@ -1 +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"]}
|
|
1
|
+
{"version":3,"file":"provider.js","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * NativeInputProvider - the native host's input provider.\n *\n * It applies the rules `IWSDKInputProvider` applies, over the facts the\n * native app's `input` slice reports, so the host is handed results:\n *\n * - Capabilities are DERIVED here, as `refreshCapabilities` derives them from\n * the WebXR session: `rays`, `pokes` and grabs from immersion, `handJoints`\n * and `pinch` from hand tracking or a tracked hand, `buttonsAxes` and\n * `haptics` from the sources present, `gaze` and `headPose` from a head\n * pose, `presence` from the host's presence member. They re-derive, and\n * `onCapabilitiesChanged` fires, whenever the facts or the sources change.\n * - Presence is decided here, as `applyPresence` decides it: the requested\n * visibility per side, and the modality, with `\"auto\"` showing hands while\n * hand joints are live and controllers otherwise. The host is told what to\n * draw per side.\n * - No sources are sampled while the session lacks focus, as IWSDK samples\n * none unless its visibility state is `Visible`.\n * - A pulse's intensity is clamped to 0..1 before it reaches the host.\n *\n * Every snapshot, and every tuple inside it, is copied before it leaves this\n * class, so a host that reuses its own sample buffers still meets the\n * ownership rule the shared contract suite checks.\n */\nimport {\n NO_CAPABILITIES,\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 {\n copyPose,\n copySnapshot,\n resolveHostSlice,\n type NativeInputFacts,\n type NativeInputHost,\n type NativePresenceShown,\n} from \"./native-types.js\";\n\nexport interface NativeInputProviderOptions {\n /** The `input` slice. Omit to read `globalThis.__rcHost.input`. */\n input?: NativeInputHost;\n /**\n * The native app fulfils grabs with its own physics, so grabs report\n * `\"native\"` and the runtime never carries the object. Default `false`:\n * `\"poseOnly\"`, the runtime carries it. The same option as IWSDK's\n * `IWSDKProviderOptions.nativeGrab`.\n */\n nativeGrab?: boolean;\n}\n\ntype Side = \"left\" | \"right\";\nconst SIDES: readonly Side[] = [\"left\", \"right\"];\n\nfunction clampUnit(value: number): number {\n return Math.min(1, Math.max(0, value));\n}\n\nexport class NativeInputProvider implements InputProvider {\n private readonly host: NativeInputHost;\n private readonly nativeGrab: boolean;\n private capabilities: InputCapabilities;\n private facts: NativeInputFacts;\n private readonly capsListeners = new Set<(c: InputCapabilities) => void>();\n private readonly sourceListeners = new Set<() => void>();\n private readonly disposers: Unsubscribe[] = [];\n private readonly presenceVisible: Record<Side, boolean> = { left: true, right: true };\n private presenceModality: PresenceModality = \"auto\";\n private readonly presenceApplied: Record<Side, NativePresenceShown | null> = { left: null, right: null };\n\n readonly getHeadPose?: () => HeadPose;\n readonly sampleHints?: () => readonly InputHitHint[];\n readonly pulse?: (sourceId: string, intensity: number, durationMs: number) => boolean;\n readonly setPresenceVisible?: (target: Handedness | \"all\", visible: boolean) => boolean;\n readonly setPresenceModality?: (mode: PresenceModality) => boolean;\n\n constructor(options: NativeInputProviderOptions = {}) {\n this.host = resolveHostSlice(\"input\", options.input);\n this.nativeGrab = options.nativeGrab ?? false;\n this.facts = { ...this.host.getFacts() };\n this.capabilities = this.derive();\n\n const host = this.host;\n if (host.getHeadPose) {\n this.getHeadPose = () => copyPose(host.getHeadPose!());\n }\n if (host.sampleHints) {\n this.sampleHints = () => host.sampleHints!().map((hint) => ({ ...hint }));\n }\n if (host.pulse) {\n this.pulse = (sourceId, intensity, durationMs) =>\n host.pulse!(sourceId, clampUnit(intensity), durationMs);\n }\n if (host.applyPresence) {\n this.setPresenceVisible = (target, visible) => {\n for (const side of SIDES) {\n if (target === \"all\" || target === side) this.presenceVisible[side] = visible;\n }\n return this.applyPresence();\n };\n this.setPresenceModality = (mode) => {\n this.presenceModality = mode;\n return this.applyPresence();\n };\n }\n\n this.disposers.push(\n host.onFactsChanged(() => this.refresh()),\n host.onSourcesChanged(() => {\n this.refresh();\n for (const listener of [...this.sourceListeners]) listener();\n }),\n );\n this.applyPresence();\n }\n\n getCapabilities(): InputCapabilities {\n return this.capabilities;\n }\n\n onCapabilitiesChanged(listener: (capabilities: InputCapabilities) => void): Unsubscribe {\n this.capsListeners.add(listener);\n return () => this.capsListeners.delete(listener);\n }\n\n onSourcesChanged(listener: () => void): Unsubscribe {\n this.sourceListeners.add(listener);\n return () => this.sourceListeners.delete(listener);\n }\n\n sample(): readonly InputSourceSnapshot[] {\n if (!this.facts.focused) return [];\n return this.host.sample().map(copySnapshot);\n }\n\n /** Release the host subscriptions and every listener. */\n dispose(): void {\n for (const dispose of this.disposers) dispose();\n this.disposers.length = 0;\n this.capsListeners.clear();\n this.sourceListeners.clear();\n }\n\n /** Re-read the facts and re-derive; publish and re-apply presence on a change. */\n private refresh(): void {\n this.facts = { ...this.host.getFacts() };\n const next = this.derive();\n if (JSON.stringify(next) !== JSON.stringify(this.capabilities)) {\n this.capabilities = next;\n for (const listener of [...this.capsListeners]) listener(next);\n }\n this.applyPresence();\n }\n\n /** `IWSDKInputProvider.refreshCapabilities`, over the host's facts and sources. */\n private derive(): InputCapabilities {\n const { immersive, handTracking } = this.facts;\n const sources = immersive ? this.host.sample() : [];\n const handTracked = sources.some((source) => source.kind === \"hand\");\n const hands = immersive && (handTracking || handTracked);\n const head = typeof this.host.getHeadPose === \"function\";\n return {\n ...NO_CAPABILITIES,\n rays: immersive,\n // A controller's index tip is its ray origin, so pokes need no hand.\n pokes: immersive,\n grabs: immersive ? (this.nativeGrab ? \"native\" : \"poseOnly\") : \"none\",\n handJoints: hands,\n pinch: hands,\n buttonsAxes: sources.some((source) => source.kind === \"controller\"),\n gaze: head,\n headPose: head,\n haptics: typeof this.host.pulse === \"function\" && sources.some((source) => source.hapticsAvailable === true),\n presence: typeof this.host.applyPresence === \"function\",\n };\n }\n\n /**\n * `IWSDKInputProvider.applyPresence`: each side shows the family the\n * modality picks, while that side is visible. Only a changed side is sent.\n * Reports whether presence reached a live session, as IWSDK reports false\n * with no session to draw in; the request is kept and applied on entry.\n */\n private applyPresence(): boolean {\n const apply = this.host.applyPresence;\n if (!apply) return false;\n const handJoints = this.capabilities.handJoints;\n const modality = this.presenceModality;\n const hands = modality === \"hands\" || (modality === \"auto\" && handJoints);\n const controllers = modality === \"controllers\" || (modality === \"auto\" && !handJoints);\n for (const side of SIDES) {\n const wanted = this.presenceVisible[side];\n const shown: NativePresenceShown = { hand: wanted && hands, controller: wanted && controllers };\n const last = this.presenceApplied[side];\n if (last && last.hand === shown.hand && last.controller === shown.controller) continue;\n this.presenceApplied[side] = shown;\n apply.call(this.host, side, { ...shown });\n }\n return this.facts.immersive;\n }\n}\n"]}
|
package/dist/transform-port.d.ts
CHANGED
|
@@ -9,9 +9,16 @@
|
|
|
9
9
|
* only ever assigned here when the host itself carries the matching member,
|
|
10
10
|
* so an app whose native host cannot follow a world pose, or has no effect
|
|
11
11
|
* hook, sees a port without one rather than a method that silently no-ops.
|
|
12
|
+
*
|
|
13
|
+
* `beginHold` and `endHold` are always present. A pose-only grab calls them
|
|
14
|
+
* on every grab and release, and a host that silently lacked them used to
|
|
15
|
+
* drop every throw on the floor (the release velocity never reached it). A
|
|
16
|
+
* host without them now fails here, at construction, naming both. A target
|
|
17
|
+
* with no physics body simply rests where it is released; the host decides
|
|
18
|
+
* that from its own scene, as IWSDK does.
|
|
12
19
|
*/
|
|
13
20
|
import type { PoseTuple, QuatTuple, Vec3Tuple } from "@realitycollective/webxr-input";
|
|
14
|
-
import type { TransformPort } from "@realitycollective/webxr-interactions";
|
|
21
|
+
import type { HoldRelease, TransformPort } from "@realitycollective/webxr-interactions";
|
|
15
22
|
import { type NativeInteractionHost } from "./native-types.js";
|
|
16
23
|
export interface NativeTransformPortOptions {
|
|
17
24
|
/** The `interactions` slice. Omit to read `globalThis.__rcHost.interactions`. */
|
|
@@ -25,6 +32,8 @@ export declare class NativeTransformPort implements TransformPort {
|
|
|
25
32
|
scale?: number;
|
|
26
33
|
emissive?: number;
|
|
27
34
|
}) => void;
|
|
35
|
+
readonly beginHold: () => void;
|
|
36
|
+
readonly endHold: (release: HoldRelease) => void;
|
|
28
37
|
constructor(targetId: string, options?: NativeTransformPortOptions);
|
|
29
38
|
getWorldPose(): PoseTuple;
|
|
30
39
|
getRestWorldPose(): PoseTuple;
|
package/dist/transform-port.js
CHANGED
|
@@ -4,6 +4,8 @@ export class NativeTransformPort {
|
|
|
4
4
|
targetId;
|
|
5
5
|
setWorldPose;
|
|
6
6
|
setEffect;
|
|
7
|
+
beginHold;
|
|
8
|
+
endHold;
|
|
7
9
|
constructor(targetId, options = {}) {
|
|
8
10
|
this.targetId = targetId;
|
|
9
11
|
this.host = resolveHostSlice("interactions", options.interactions);
|
|
@@ -15,6 +17,15 @@ export class NativeTransformPort {
|
|
|
15
17
|
const host = this.host;
|
|
16
18
|
this.setEffect = (effect) => host.setEffect(targetId, effect);
|
|
17
19
|
}
|
|
20
|
+
if (typeof this.host.beginHold !== "function" || typeof this.host.endHold !== "function") {
|
|
21
|
+
throw new Error("@realitycollective/native-interactions: the interactions slice must implement beginHold(targetId) and endHold(targetId, release). A pose-only grab calls both, and a host without them drops every release velocity. A host with no physics implements them and lets the object rest.");
|
|
22
|
+
}
|
|
23
|
+
const host = this.host;
|
|
24
|
+
this.beginHold = () => host.beginHold(targetId);
|
|
25
|
+
this.endHold = (release) => host.endHold(targetId, {
|
|
26
|
+
linearVelocity: copyVec3(release.linearVelocity),
|
|
27
|
+
angularVelocity: copyVec3(release.angularVelocity),
|
|
28
|
+
});
|
|
18
29
|
}
|
|
19
30
|
getWorldPose() {
|
|
20
31
|
return copyPose(this.host.getWorldPose(this.targetId));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transform-port.js","sourceRoot":"","sources":["../src/transform-port.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"transform-port.js","sourceRoot":"","sources":["../src/transform-port.ts"],"names":[],"mappings":"AAqBA,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;IACpE,SAAS,CAAa;IACtB,OAAO,CAAiC;IAEjD,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;QACD,IAAI,OAAO,IAAI,CAAC,IAAI,CAAC,SAAS,KAAK,UAAU,IAAI,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,KAAK,UAAU,EAAE,CAAC;YACzF,MAAM,IAAI,KAAK,CACb,uRAAuR,CACxR,CAAC;QACJ,CAAC;QACD,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;QACvB,IAAI,CAAC,SAAS,GAAG,GAAG,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;QAChD,IAAI,CAAC,OAAO,GAAG,CAAC,OAAO,EAAE,EAAE,CACzB,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE;YACrB,cAAc,EAAE,QAAQ,CAAC,OAAO,CAAC,cAAc,CAAC;YAChD,eAAe,EAAE,QAAQ,CAAC,OAAO,CAAC,eAAe,CAAC;SACnD,CAAC,CAAC;IACP,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 *\n * `beginHold` and `endHold` are always present. A pose-only grab calls them\n * on every grab and release, and a host that silently lacked them used to\n * drop every throw on the floor (the release velocity never reached it). A\n * host without them now fails here, at construction, naming both. A target\n * with no physics body simply rests where it is released; the host decides\n * that from its own scene, as IWSDK does.\n */\nimport type { PoseTuple, QuatTuple, Vec3Tuple } from \"@realitycollective/webxr-input\";\nimport type { HoldRelease, 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 readonly beginHold: () => void;\n readonly endHold: (release: HoldRelease) => 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 if (typeof this.host.beginHold !== \"function\" || typeof this.host.endHold !== \"function\") {\n throw new Error(\n \"@realitycollective/native-interactions: the interactions slice must implement beginHold(targetId) and endHold(targetId, release). A pose-only grab calls both, and a host without them drops every release velocity. A host with no physics implements them and lets the object rest.\",\n );\n }\n const host = this.host;\n this.beginHold = () => host.beginHold(targetId);\n this.endHold = (release) =>\n host.endHold(targetId, {\n linearVelocity: copyVec3(release.linearVelocity),\n angularVelocity: copyVec3(release.angularVelocity),\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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@realitycollective/native-interactions",
|
|
3
|
-
"version": "0.1.1-preview.
|
|
3
|
+
"version": "0.1.1-preview.2",
|
|
4
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
5
|
"keywords": [
|
|
6
6
|
"realitycollective",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"@realitycollective/webxr-input": "^0.1.4",
|
|
38
|
-
"@realitycollective/webxr-interactions": "^0.1.1-preview.
|
|
38
|
+
"@realitycollective/webxr-interactions": "^0.1.1-preview.2"
|
|
39
39
|
},
|
|
40
40
|
"repository": {
|
|
41
41
|
"type": "git",
|