@realitycollective/babylon-interactions 0.1.0-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 ADDED
@@ -0,0 +1,35 @@
1
+ # Changelog
2
+
3
+ Change log for the Reality Collective WebXR Interaction Extensions packages. All five packages are versioned and released together; the version below is the one carried by the `v<version>` release tag.
4
+
5
+ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Preview builds are not listed separately. The entry for a version accumulates while its previews are published, and is dated when that version is released.
6
+
7
+ ## [0.1.0]
8
+
9
+ ### Added
10
+
11
+ - `@realitycollective/webxr-interactions` - engine-free core: interactables and interactors, the behaviour set (`press` including latching, `pulse`, `hinge`, `dial`, `slide`, `grab` in poseOnly and native modes, `tossScore`), gaze (`required` gating and dwell-to-press), the runtime/binder with hints > poke > ray targeting, hysteresis and lifecycle, capability negotiation with a visible `behaviourDisabled` outcome, events as the only outbound pathway, and feedback intents (haptics and audio remain the client's, with `routeHapticsToProvider` as an explicit opt-in).
12
+ - `@realitycollective/threejs-interactions` - the default standalone adapter: raw WebXR (`XRSession` sources, hand joints, select/squeeze plus gamepad analog, haptic pulse), three.js hit-testing and transform ports, and a desktop mouse fallback. No framework required.
13
+ - `@realitycollective/iwsdk-interactions` - Meta IWSDK adapter: player-rig poses and stateful gamepads as the provider, IWSDK's own targeting (`Pressed` / `Grabbed` tags) forwarded as pre-resolved hints, and native grab fulfilment when the app enables IWSDK grabbing/physics. One-call `registerInteractions(world)` setup.
14
+ - `@realitycollective/xrblocks-interactions` - EXPERIMENTAL Google XR Blocks adapter, structurally typed against xrblocks v0.20.0 (`Input.getFrame()` ray sources and direct touches, pooled structs copied, no haptics upstream).
15
+ - `demos/playground` - the standalone interaction playground: the full station set from one portable `InteractionDescriptor`, with client-side audio cues, opt-in controller haptics and client-side toss ballistics.
16
+ - `@realitycollective/webxr-interactions` - per-source velocity. `VelocityTracker` differentiates the grip poses the runtime already samples into linear and angular velocity, and the runtime runs one by default (`velocity: false` turns it off, `{ smoothing }` averages it). A source on its first frame, or one that reappears after dropping out, reports no velocity rather than a jump from its last known pose, and a provider that supplies its own velocity keeps it. Read it from `runtime.onSample(...)` or `runtime.getSource(id)`. `poseVelocity` and `clampDeadzone` join the math helpers.
17
+ - `@realitycollective/webxr-interactions` - `routeAudioToSink`, the audio counterpart to `routeHapticsToProvider`: an opt-in that plays the cues named in a map through a client-supplied sink. Cues absent from the map are ignored, so an app sonifies only the moments it has sounds for.
18
+ - `@realitycollective/iwsdk-interactions` and `@realitycollective/threejs-interactions` - presence control. `setPresenceVisible(target, visible)` shows and hides the hand and controller visuals per side; the IWSDK adapter adds `setPresenceModality(mode)` to force hands or controllers over the automatic choice, and the three.js adapter takes the app's own models through `registerVisual(handedness, root)`. Each provider reports `supportsPresence`; the XR Blocks adapter reports false, since XR Blocks exposes no way to hide its visuals.
19
+ - `@realitycollective/threejs-interactions` - native WebXR pose velocity. Where the browser reports `linearVelocity` or `angularVelocity` with a grip pose (or with the wrist joint a hand falls back to), the provider passes it through on the snapshot and the core's tracker leaves it alone; where it does not, the tracker derives velocity from consecutive poses as before. The desktop mouse source synthesises nothing: its grip rides the camera ray, so the derived velocity is camera motion, and hand mechanics should be gated on `handedness !== "none"`.
20
+ - `@realitycollective/babylon-interactions` - a Babylon.js adapter, the fourth engine. It reads a `WebXRDefaultExperience` (controllers, motion controller trigger and squeeze, hand-tracking joints, session manager) for input, with `scene.onPointerObservable` as the desktop fallback, haptics through the motion controller's `pulse`, and presence over the visuals Babylon builds - motion controller root meshes and hand meshes. Babylon picks the visual per input source, so there is no modality switch. Hit-testing is a sphere test over registered nodes, with an optional `pickWithRay` hook for mesh-accurate targeting; the transform port moves, rotates and scales Babylon nodes from rest. Structurally typed against the Babylon API rather than importing `@babylonjs/core`, as the XR Blocks adapter is, so an upstream release cannot break the install - and so the whole adapter is covered headlessly by structural fakes. Written against the documented Babylon 7 API and not yet exercised against a real Babylon runtime.
21
+ - Tests for `@realitycollective/iwsdk-interactions`, which had none. `@iwsdk/core` imports headlessly in node, so the provider is covered against a structural fake world with no module mocking, and a parity suite instantiates all three providers and checks each one implements the whole `InputProvider` contract. The package's coverage ratchet moves off zero, and the other three ratchets rise to the floor they now measure.
22
+
23
+ ### Changed
24
+
25
+ - `@realitycollective/iwsdk-interactions` - `pulse` resolves the side from the handedness recorded while sampling, rather than parsing the source id. Parsing the id remains the fallback for an id the provider did not produce.
26
+
27
+ - `@realitycollective/iwsdk-interactions` targets **IWSDK 0.5.x**. Its peer range is now `>=0.5.0 <0.6.0` and it is developed and tested against `@iwsdk/core` 0.5.3. The adapter is source-compatible with 0.4.x and needed no code change, but 0.5 is the only line exercised, so it is the only line supported. Every symbol and runtime member the provider uses is present in both, and it already reads `world.input.xr.gamepads` rather than the accessor 0.5 deprecates.
28
+ - `demos/playground` no longer compiles UIKitML at build time. `@iwsdk/vite-plugin-uikitml` was discontinued at 0.4.2 and only ever did `JSON.stringify(parse(source))`, so the panel is served as `.uikitml` from `public/ui/` and parsed on load. The demo drops that plugin, and its station markup uses `rgba()` rather than `background-opacity`, which the 0.5 parser removed.
29
+
30
+ ### Notes
31
+
32
+ - All five packages depend on `@realitycollective/webxr-input`, released independently from the [WebXR-Input](https://github.com/realitycollective/WebXR-Input) repository. That package must be published before this one.
33
+ - The interaction packages themselves carry no IWSDK coupling beyond the adapter: `webxr-interactions` is engine-free, and `threejs-interactions` and `xrblocks-interactions` peer only on three.js.
34
+
35
+ [0.1.0]: https://github.com/realitycollective/WebXR-Interactions/commits/main
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Reality Collective
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,70 @@
1
+ # @realitycollective/babylon-interactions
2
+
3
+ The Babylon.js adapter for the Reality Collective Interaction Extensions. It feeds a Babylon WebXR experience into the [`@realitycollective/webxr-interactions`](https://www.npmjs.com/package/@realitycollective/webxr-interactions) core.
4
+
5
+ ```sh
6
+ npm install @realitycollective/babylon-interactions
7
+ ```
8
+
9
+ It re-exports everything from the core, so this is the only interaction package your app needs.
10
+
11
+ > **Not yet exercised against a real Babylon runtime.** The adapter is written against the documented Babylon 7 API and covered by structural fakes. Treat it as a preview until it has run in a Babylon app.
12
+
13
+ ## What it binds
14
+
15
+ | Layer | Detail |
16
+ | --- | --- |
17
+ | **Input** | `WebXRDefaultExperience`: controllers, motion controller components (trigger, squeeze), hand-tracking joints, and the session manager for what is live |
18
+ | **Hit-testing** | A sphere test over registered nodes, or your own `scene.pickWithRay` through the `pickWithRay` hook |
19
+ | **Movement** | Moves, rotates and scales Babylon nodes for grab, hinge, dial and slide |
20
+ | **Desktop** | `scene.onPointerObservable` as a pointer fallback, so the same scene is testable without a headset |
21
+ | **Haptics** | Through the motion controller's `pulse` |
22
+ | **No @babylonjs/core dependency** | It matches the shape of the Babylon API in TypeScript rather than importing Babylon, so an upstream release cannot break your install |
23
+
24
+ ## Usage
25
+
26
+ ```ts
27
+ import { createBabylonInteractions } from "@realitycollective/babylon-interactions";
28
+
29
+ const xr = await scene.createDefaultXRExperienceAsync();
30
+ const interactions = createBabylonInteractions({ scene, xr, attachToScene: true });
31
+
32
+ interactions.register({ id: "button", behaviours: [{ kind: "press" }] }, buttonMesh);
33
+ interactions.runtime.onEvent((event) => console.log(event.type));
34
+ ```
35
+
36
+ `attachToScene` drives the update loop from `scene.onBeforeRenderObservable` and the engine's frame delta. Leave it off and call `interactions.update(dtSeconds)` from your own loop.
37
+
38
+ For mesh-accurate targeting, hand the adapter your own pick:
39
+
40
+ ```ts
41
+ interactions.setPickWithRay((origin, direction, maxDistance) => {
42
+ const info = scene.pickWithRay(new Ray(Vector3.FromArray(origin), Vector3.FromArray(direction), maxDistance));
43
+ return info?.hit && info.pickedPoint
44
+ ? { mesh: info.pickedMesh, distance: info.distance, point: info.pickedPoint.asArray() as [number, number, number] }
45
+ : null;
46
+ });
47
+ ```
48
+
49
+ ## Things to know
50
+
51
+ - **Forward is +Z.** Babylon is left-handed, so a controller ray points down +Z where three.js and raw WebXR point down -Z. A scene that sets `useRightHandedSystem` faces -Z instead; the adapter reads that flag at construction and flips. The adapter handles this; it matters if you compare rays with another adapter's.
52
+ - **Rotations need a quaternion.** A node whose `rotationQuaternion` is null is still driven by Euler angles. Set `node.rotationQuaternion = Quaternion.Identity()` before registering it, or pass `createQuaternion` to `register`, otherwise the first rotation write stores a plain object that Babylon cannot use.
53
+ - **Presence** shows and hides what Babylon built: motion controller root meshes and hand meshes. Babylon picks the visual per input source, so there is no hands/controllers switch - `setPresenceModality` always returns false.
54
+ - **Desktop grip.** The pointer fallback puts its grip one metre along the pointer ray, matching the three.js adapter, so grab, hinge, dial and slide follow the cursor on desktop. Set `desktopGripDistance` near the distance of the things being manipulated; at 0 the grip sits on the camera and a drag reports camera motion only.
55
+
56
+ ## Peer dependency
57
+
58
+ None. Babylon is matched structurally, not imported, so any Babylon version whose objects carry these members works.
59
+
60
+ ## Live demo
61
+
62
+ The interaction playground - the three.js build of the same station set: **[webxr-interactions.pages.dev](https://webxr-interactions.pages.dev)**
63
+
64
+ ## Documentation
65
+
66
+ See the [repository README](https://github.com/realitycollective/WebXR-Interactions#readme).
67
+
68
+ ## License
69
+
70
+ MIT - see [LICENSE](./LICENSE).
@@ -0,0 +1,220 @@
1
+ /**
2
+ * The shape of the Babylon.js API this adapter reads, written out here
3
+ * rather than imported.
4
+ *
5
+ * `@babylonjs/core` is NOT a dependency of this package, in the same way
6
+ * the XR Blocks adapter does not depend on `xrblocks`. Babylon ships one
7
+ * large package on a fast release train, and an adapter that imported it
8
+ * would drag a version choice into every consumer and break on an upstream
9
+ * rename. Matching the shape instead means an app installs whatever Babylon
10
+ * it already uses and passes its objects straight in.
11
+ *
12
+ * The trade for that is honesty about provenance: these declarations were
13
+ * written from the Babylon 7 documentation, not verified against an
14
+ * installed package, so members a version might not carry are optional and
15
+ * read defensively. Nothing here is required to be a Babylon object - a
16
+ * plain object with the same members works, which is what the tests use.
17
+ *
18
+ * Coordinates: Babylon is LEFT-handed and a node's forward is +Z, where
19
+ * three.js and raw WebXR use -Z. That difference is applied in one place
20
+ * (`nodeForward`) so it is stated once.
21
+ */
22
+ import type { PoseTuple, QuatTuple, Vec3Tuple } from "@realitycollective/webxr-input";
23
+ /** Structural slice of Babylon's `Vector3`. */
24
+ export interface BabylonVector3Like {
25
+ x: number;
26
+ y: number;
27
+ z: number;
28
+ }
29
+ /** Structural slice of Babylon's `Quaternion`. */
30
+ export interface BabylonQuaternionLike {
31
+ x: number;
32
+ y: number;
33
+ z: number;
34
+ w: number;
35
+ }
36
+ /** Structural slice of Babylon's `Ray`. */
37
+ export interface BabylonRayLike {
38
+ origin: BabylonVector3Like;
39
+ direction: BabylonVector3Like;
40
+ }
41
+ /**
42
+ * Structural slice of Babylon's `Observable<T>`. The observer handle is
43
+ * opaque - it is only ever handed straight back to `remove`.
44
+ */
45
+ export interface BabylonObservableLike<T> {
46
+ add(callback: (eventData: T) => void): unknown;
47
+ remove(observer: unknown): boolean;
48
+ }
49
+ /**
50
+ * Structural slice of Babylon's `TransformNode`, plus the one member
51
+ * `AbstractMesh` adds that this adapter reads (`isVisible`).
52
+ *
53
+ * `parent` is `unknown` on purpose: Babylon types it as `Nullable<Node>`,
54
+ * and `Node` carries none of the transform members, so anything narrower
55
+ * would refuse a real Babylon node. Read it through `parentOf`.
56
+ */
57
+ export interface BabylonTransformNodeLike {
58
+ position: BabylonVector3Like;
59
+ rotationQuaternion?: BabylonQuaternionLike | null;
60
+ scaling?: BabylonVector3Like;
61
+ parent?: unknown;
62
+ getAbsolutePosition(): BabylonVector3Like;
63
+ absoluteRotationQuaternion?: BabylonQuaternionLike;
64
+ computeWorldMatrix?(force?: boolean): unknown;
65
+ setEnabled?(value: boolean): void;
66
+ isEnabled?(checkAncestors?: boolean): boolean;
67
+ isVisible?: boolean;
68
+ }
69
+ /** Structural slice of Babylon's `Camera`. */
70
+ export interface BabylonCameraLike {
71
+ globalPosition?: BabylonVector3Like;
72
+ absoluteRotation?: BabylonQuaternionLike;
73
+ position?: BabylonVector3Like;
74
+ getForwardRay?(length?: number): BabylonRayLike;
75
+ }
76
+ /** Structural slice of Babylon's `Engine` - only the frame delta is read. */
77
+ export interface BabylonEngineLike {
78
+ getDeltaTime(): number;
79
+ }
80
+ /** Structural slice of Babylon's `PickingInfo`. */
81
+ export interface BabylonPickingInfoLike {
82
+ hit?: boolean;
83
+ distance?: number;
84
+ pickedPoint?: BabylonVector3Like | null;
85
+ pickedMesh?: BabylonTransformNodeLike | null;
86
+ ray?: BabylonRayLike | null;
87
+ }
88
+ /**
89
+ * Structural slice of Babylon's `PointerInfo`. `type` is one of the
90
+ * `PointerEventTypes` constants - see {@link POINTER_EVENT_TYPES}.
91
+ */
92
+ export interface BabylonPointerInfoLike {
93
+ type: number;
94
+ event?: {
95
+ clientX?: number;
96
+ clientY?: number;
97
+ button?: number;
98
+ };
99
+ pickInfo?: BabylonPickingInfoLike | null;
100
+ }
101
+ /**
102
+ * The `PointerEventTypes` values this adapter reacts to. Babylon defines
103
+ * them as one bit per event; these three are unchanged across 5, 6 and 7.
104
+ */
105
+ export declare const POINTER_EVENT_TYPES: {
106
+ readonly down: 1;
107
+ readonly up: 2;
108
+ readonly move: 4;
109
+ };
110
+ /** Structural slice of Babylon's `Scene`. */
111
+ export interface BabylonSceneLike {
112
+ /**
113
+ * Babylon defaults to a left-handed system with forward +Z. A scene that
114
+ * sets this flag is right-handed and its nodes face -Z; the adapter reads
115
+ * it once at construction.
116
+ */
117
+ useRightHandedSystem?: boolean;
118
+ onBeforeRenderObservable?: BabylonObservableLike<unknown>;
119
+ onPointerObservable?: BabylonObservableLike<BabylonPointerInfoLike>;
120
+ pick?(x: number, y: number): BabylonPickingInfoLike | null;
121
+ activeCamera?: BabylonCameraLike | null;
122
+ getEngine?(): BabylonEngineLike;
123
+ }
124
+ /** Structural slice of one `WebXRControllerComponent` reading. */
125
+ export interface BabylonMotionControllerComponentLike {
126
+ value?: number;
127
+ pressed?: boolean;
128
+ }
129
+ /** Structural slice of Babylon's `WebXRAbstractMotionController`. */
130
+ export interface BabylonMotionControllerLike {
131
+ getComponentOfType?(type: string): BabylonMotionControllerComponentLike | null;
132
+ getMainComponent?(): BabylonMotionControllerComponentLike | null;
133
+ pulse?(value: number, duration: number): Promise<unknown>;
134
+ rootMesh?: BabylonTransformNodeLike | null;
135
+ }
136
+ /** Structural slice of Babylon's `WebXRInputSource`. */
137
+ export interface BabylonXRControllerLike {
138
+ uniqueId: string;
139
+ inputSource: {
140
+ handedness?: string;
141
+ hand?: unknown;
142
+ gamepad?: {
143
+ hapticActuators?: readonly unknown[];
144
+ } | null;
145
+ };
146
+ pointer: BabylonTransformNodeLike;
147
+ grip?: BabylonTransformNodeLike | null;
148
+ motionController?: BabylonMotionControllerLike | null;
149
+ onMotionControllerInitObservable?: BabylonObservableLike<unknown>;
150
+ }
151
+ /** Structural slice of Babylon's `WebXRInput`. */
152
+ export interface BabylonXRInputLike {
153
+ controllers: readonly BabylonXRControllerLike[];
154
+ onControllerAddedObservable?: BabylonObservableLike<BabylonXRControllerLike>;
155
+ onControllerRemovedObservable?: BabylonObservableLike<BabylonXRControllerLike>;
156
+ }
157
+ /** Structural slice of one tracked hand from the hand-tracking feature. */
158
+ export interface BabylonXRHandLike {
159
+ getJointMesh?(jointName: string): BabylonTransformNodeLike | null | undefined;
160
+ handMesh?: BabylonTransformNodeLike | null;
161
+ }
162
+ /**
163
+ * Structural slice of `WebXRHandTracking`, the feature the features manager
164
+ * registers under `"xr-hand-tracking"`.
165
+ */
166
+ export interface BabylonHandTrackingLike {
167
+ getHandByControllerId(id: string): BabylonXRHandLike | null | undefined;
168
+ }
169
+ /** Structural slice of Babylon's `WebXRDefaultExperience`. */
170
+ export interface BabylonXRExperienceLike {
171
+ baseExperience?: {
172
+ sessionManager?: {
173
+ session?: unknown;
174
+ onXRSessionInit?: BabylonObservableLike<unknown>;
175
+ onXRSessionEnded?: BabylonObservableLike<unknown>;
176
+ };
177
+ featuresManager?: {
178
+ getEnabledFeature(featureName: string): unknown;
179
+ };
180
+ };
181
+ input?: BabylonXRInputLike;
182
+ }
183
+ /** The name Babylon registers hand tracking under in the features manager. */
184
+ export declare const HAND_TRACKING_FEATURE = "xr-hand-tracking";
185
+ /** Index fingertip joint, as WebXR and Babylon both spell it. */
186
+ export declare const INDEX_TIP_JOINT = "index-finger-tip";
187
+ /** Copy a Babylon vector into a tuple. */
188
+ export declare function toVec3(v: BabylonVector3Like | null | undefined): Vec3Tuple | null;
189
+ /** Copy a Babylon quaternion into a tuple, defaulting to identity. */
190
+ export declare function toQuat(q: BabylonQuaternionLike | null | undefined): QuatTuple;
191
+ /** Write a tuple into an existing Babylon vector, in place. */
192
+ export declare function writeVec3(target: BabylonVector3Like, value: Vec3Tuple): void;
193
+ /** Write a tuple into an existing Babylon quaternion, in place. */
194
+ export declare function writeQuat(target: BabylonQuaternionLike, value: QuatTuple): void;
195
+ /** A node's world pose: absolute position and absolute rotation. */
196
+ export declare function nodeWorldPose(node: BabylonTransformNodeLike): PoseTuple;
197
+ /**
198
+ * The world-space forward axis for a scene: +Z in Babylon's default
199
+ * left-handed system, the opposite of three.js and of a raw WebXR target
200
+ * ray, and -Z when the scene sets `useRightHandedSystem`.
201
+ */
202
+ export declare function defaultForward(rightHanded?: boolean): Vec3Tuple;
203
+ /**
204
+ * A node's forward direction in world space, honouring the scene's
205
+ * handedness (see {@link defaultForward}).
206
+ */
207
+ export declare function nodeForward(node: BabylonTransformNodeLike, rightHanded?: boolean): Vec3Tuple;
208
+ /**
209
+ * The parent of a node, when it is one this adapter can read a world pose
210
+ * from. Babylon types `parent` as `Node`, which has no transform, so a
211
+ * parent that is a bone or a bare node reports null and the caller treats
212
+ * the node as unparented.
213
+ */
214
+ export declare function parentOf(node: BabylonTransformNodeLike): BabylonTransformNodeLike | null;
215
+ /**
216
+ * Is this node currently showing? A node the app disabled or hid is not a
217
+ * hit-test candidate. Absent members mean yes - a fake, or a build that does
218
+ * not carry them, should not silently drop out of targeting.
219
+ */
220
+ export declare function nodeShowing(node: BabylonTransformNodeLike): boolean;
@@ -0,0 +1,92 @@
1
+ import { vApplyQuat } from "@realitycollective/webxr-interactions";
2
+ /**
3
+ * The `PointerEventTypes` values this adapter reacts to. Babylon defines
4
+ * them as one bit per event; these three are unchanged across 5, 6 and 7.
5
+ */
6
+ export const POINTER_EVENT_TYPES = {
7
+ down: 1,
8
+ up: 2,
9
+ move: 4,
10
+ };
11
+ /** The name Babylon registers hand tracking under in the features manager. */
12
+ export const HAND_TRACKING_FEATURE = "xr-hand-tracking";
13
+ /** Index fingertip joint, as WebXR and Babylon both spell it. */
14
+ export const INDEX_TIP_JOINT = "index-finger-tip";
15
+ // ---------------------------------------------------------------------------
16
+ // Conversions. Everything below turns Babylon-shaped objects into the core's
17
+ // tuples, or writes tuples back IN PLACE.
18
+ //
19
+ // In place is deliberate. Babylon caches the previous position/rotation and
20
+ // recomputes the world matrix when the live values differ, so mutating
21
+ // `node.position.x` is seen. Replacing `node.position` with a plain object is
22
+ // not just missed, it breaks Babylon, which calls Vector3 methods on it.
23
+ // ---------------------------------------------------------------------------
24
+ /** Copy a Babylon vector into a tuple. */
25
+ export function toVec3(v) {
26
+ return v ? [v.x, v.y, v.z] : null;
27
+ }
28
+ /** Copy a Babylon quaternion into a tuple, defaulting to identity. */
29
+ export function toQuat(q) {
30
+ return q ? [q.x, q.y, q.z, q.w] : [0, 0, 0, 1];
31
+ }
32
+ /** Write a tuple into an existing Babylon vector, in place. */
33
+ export function writeVec3(target, value) {
34
+ target.x = value[0];
35
+ target.y = value[1];
36
+ target.z = value[2];
37
+ }
38
+ /** Write a tuple into an existing Babylon quaternion, in place. */
39
+ export function writeQuat(target, value) {
40
+ target.x = value[0];
41
+ target.y = value[1];
42
+ target.z = value[2];
43
+ target.w = value[3];
44
+ }
45
+ /** A node's world pose: absolute position and absolute rotation. */
46
+ export function nodeWorldPose(node) {
47
+ return {
48
+ position: toVec3(node.getAbsolutePosition()) ?? [0, 0, 0],
49
+ quaternion: toQuat(node.absoluteRotationQuaternion),
50
+ };
51
+ }
52
+ /**
53
+ * The world-space forward axis for a scene: +Z in Babylon's default
54
+ * left-handed system, the opposite of three.js and of a raw WebXR target
55
+ * ray, and -Z when the scene sets `useRightHandedSystem`.
56
+ */
57
+ export function defaultForward(rightHanded = false) {
58
+ return [0, 0, rightHanded ? -1 : 1];
59
+ }
60
+ /**
61
+ * A node's forward direction in world space, honouring the scene's
62
+ * handedness (see {@link defaultForward}).
63
+ */
64
+ export function nodeForward(node, rightHanded = false) {
65
+ return vApplyQuat(defaultForward(rightHanded), toQuat(node.absoluteRotationQuaternion));
66
+ }
67
+ /**
68
+ * The parent of a node, when it is one this adapter can read a world pose
69
+ * from. Babylon types `parent` as `Node`, which has no transform, so a
70
+ * parent that is a bone or a bare node reports null and the caller treats
71
+ * the node as unparented.
72
+ */
73
+ export function parentOf(node) {
74
+ const parent = node.parent;
75
+ if (!parent || typeof parent !== "object")
76
+ return null;
77
+ const candidate = parent;
78
+ return typeof candidate.getAbsolutePosition === "function"
79
+ ? parent
80
+ : null;
81
+ }
82
+ /**
83
+ * Is this node currently showing? A node the app disabled or hid is not a
84
+ * hit-test candidate. Absent members mean yes - a fake, or a build that does
85
+ * not carry them, should not silently drop out of targeting.
86
+ */
87
+ export function nodeShowing(node) {
88
+ if (node.isVisible === false)
89
+ return false;
90
+ return node.isEnabled ? node.isEnabled() !== false : true;
91
+ }
92
+ //# sourceMappingURL=babylon-types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"babylon-types.js","sourceRoot":"","sources":["../src/babylon-types.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The shape of the Babylon.js API this adapter reads, written out here\n * rather than imported.\n *\n * `@babylonjs/core` is NOT a dependency of this package, in the same way\n * the XR Blocks adapter does not depend on `xrblocks`. Babylon ships one\n * large package on a fast release train, and an adapter that imported it\n * would drag a version choice into every consumer and break on an upstream\n * rename. Matching the shape instead means an app installs whatever Babylon\n * it already uses and passes its objects straight in.\n *\n * The trade for that is honesty about provenance: these declarations were\n * written from the Babylon 7 documentation, not verified against an\n * installed package, so members a version might not carry are optional and\n * read defensively. Nothing here is required to be a Babylon object - a\n * plain object with the same members works, which is what the tests use.\n *\n * Coordinates: Babylon is LEFT-handed and a node's forward is +Z, where\n * three.js and raw WebXR use -Z. That difference is applied in one place\n * (`nodeForward`) so it is stated once.\n */\nimport type { PoseTuple, QuatTuple, Vec3Tuple } from \"@realitycollective/webxr-input\";\nimport { vApplyQuat } from \"@realitycollective/webxr-interactions\";\n\n/** Structural slice of Babylon's `Vector3`. */\nexport interface BabylonVector3Like {\n x: number;\n y: number;\n z: number;\n}\n\n/** Structural slice of Babylon's `Quaternion`. */\nexport interface BabylonQuaternionLike {\n x: number;\n y: number;\n z: number;\n w: number;\n}\n\n/** Structural slice of Babylon's `Ray`. */\nexport interface BabylonRayLike {\n origin: BabylonVector3Like;\n direction: BabylonVector3Like;\n}\n\n/**\n * Structural slice of Babylon's `Observable<T>`. The observer handle is\n * opaque - it is only ever handed straight back to `remove`.\n */\nexport interface BabylonObservableLike<T> {\n add(callback: (eventData: T) => void): unknown;\n remove(observer: unknown): boolean;\n}\n\n/**\n * Structural slice of Babylon's `TransformNode`, plus the one member\n * `AbstractMesh` adds that this adapter reads (`isVisible`).\n *\n * `parent` is `unknown` on purpose: Babylon types it as `Nullable<Node>`,\n * and `Node` carries none of the transform members, so anything narrower\n * would refuse a real Babylon node. Read it through `parentOf`.\n */\nexport interface BabylonTransformNodeLike {\n position: BabylonVector3Like;\n rotationQuaternion?: BabylonQuaternionLike | null;\n scaling?: BabylonVector3Like;\n parent?: unknown;\n getAbsolutePosition(): BabylonVector3Like;\n absoluteRotationQuaternion?: BabylonQuaternionLike;\n computeWorldMatrix?(force?: boolean): unknown;\n setEnabled?(value: boolean): void;\n isEnabled?(checkAncestors?: boolean): boolean;\n isVisible?: boolean;\n}\n\n/** Structural slice of Babylon's `Camera`. */\nexport interface BabylonCameraLike {\n globalPosition?: BabylonVector3Like;\n absoluteRotation?: BabylonQuaternionLike;\n position?: BabylonVector3Like;\n getForwardRay?(length?: number): BabylonRayLike;\n}\n\n/** Structural slice of Babylon's `Engine` - only the frame delta is read. */\nexport interface BabylonEngineLike {\n getDeltaTime(): number;\n}\n\n/** Structural slice of Babylon's `PickingInfo`. */\nexport interface BabylonPickingInfoLike {\n hit?: boolean;\n distance?: number;\n pickedPoint?: BabylonVector3Like | null;\n pickedMesh?: BabylonTransformNodeLike | null;\n ray?: BabylonRayLike | null;\n}\n\n/**\n * Structural slice of Babylon's `PointerInfo`. `type` is one of the\n * `PointerEventTypes` constants - see {@link POINTER_EVENT_TYPES}.\n */\nexport interface BabylonPointerInfoLike {\n type: number;\n event?: { clientX?: number; clientY?: number; button?: number };\n pickInfo?: BabylonPickingInfoLike | null;\n}\n\n/**\n * The `PointerEventTypes` values this adapter reacts to. Babylon defines\n * them as one bit per event; these three are unchanged across 5, 6 and 7.\n */\nexport const POINTER_EVENT_TYPES = {\n down: 1,\n up: 2,\n move: 4,\n} as const;\n\n/** Structural slice of Babylon's `Scene`. */\nexport interface BabylonSceneLike {\n /**\n * Babylon defaults to a left-handed system with forward +Z. A scene that\n * sets this flag is right-handed and its nodes face -Z; the adapter reads\n * it once at construction.\n */\n useRightHandedSystem?: boolean;\n onBeforeRenderObservable?: BabylonObservableLike<unknown>;\n onPointerObservable?: BabylonObservableLike<BabylonPointerInfoLike>;\n pick?(x: number, y: number): BabylonPickingInfoLike | null;\n activeCamera?: BabylonCameraLike | null;\n getEngine?(): BabylonEngineLike;\n}\n\n/** Structural slice of one `WebXRControllerComponent` reading. */\nexport interface BabylonMotionControllerComponentLike {\n value?: number;\n pressed?: boolean;\n}\n\n/** Structural slice of Babylon's `WebXRAbstractMotionController`. */\nexport interface BabylonMotionControllerLike {\n getComponentOfType?(type: string): BabylonMotionControllerComponentLike | null;\n getMainComponent?(): BabylonMotionControllerComponentLike | null;\n pulse?(value: number, duration: number): Promise<unknown>;\n rootMesh?: BabylonTransformNodeLike | null;\n}\n\n/** Structural slice of Babylon's `WebXRInputSource`. */\nexport interface BabylonXRControllerLike {\n uniqueId: string;\n inputSource: {\n handedness?: string;\n hand?: unknown;\n gamepad?: { hapticActuators?: readonly unknown[] } | null;\n };\n pointer: BabylonTransformNodeLike;\n grip?: BabylonTransformNodeLike | null;\n motionController?: BabylonMotionControllerLike | null;\n onMotionControllerInitObservable?: BabylonObservableLike<unknown>;\n}\n\n/** Structural slice of Babylon's `WebXRInput`. */\nexport interface BabylonXRInputLike {\n controllers: readonly BabylonXRControllerLike[];\n onControllerAddedObservable?: BabylonObservableLike<BabylonXRControllerLike>;\n onControllerRemovedObservable?: BabylonObservableLike<BabylonXRControllerLike>;\n}\n\n/** Structural slice of one tracked hand from the hand-tracking feature. */\nexport interface BabylonXRHandLike {\n getJointMesh?(jointName: string): BabylonTransformNodeLike | null | undefined;\n handMesh?: BabylonTransformNodeLike | null;\n}\n\n/**\n * Structural slice of `WebXRHandTracking`, the feature the features manager\n * registers under `\"xr-hand-tracking\"`.\n */\nexport interface BabylonHandTrackingLike {\n getHandByControllerId(id: string): BabylonXRHandLike | null | undefined;\n}\n\n/** Structural slice of Babylon's `WebXRDefaultExperience`. */\nexport interface BabylonXRExperienceLike {\n baseExperience?: {\n sessionManager?: {\n session?: unknown;\n onXRSessionInit?: BabylonObservableLike<unknown>;\n onXRSessionEnded?: BabylonObservableLike<unknown>;\n };\n featuresManager?: { getEnabledFeature(featureName: string): unknown };\n };\n input?: BabylonXRInputLike;\n}\n\n/** The name Babylon registers hand tracking under in the features manager. */\nexport const HAND_TRACKING_FEATURE = \"xr-hand-tracking\";\n\n/** Index fingertip joint, as WebXR and Babylon both spell it. */\nexport const INDEX_TIP_JOINT = \"index-finger-tip\";\n\n// ---------------------------------------------------------------------------\n// Conversions. Everything below turns Babylon-shaped objects into the core's\n// tuples, or writes tuples back IN PLACE.\n//\n// In place is deliberate. Babylon caches the previous position/rotation and\n// recomputes the world matrix when the live values differ, so mutating\n// `node.position.x` is seen. Replacing `node.position` with a plain object is\n// not just missed, it breaks Babylon, which calls Vector3 methods on it.\n// ---------------------------------------------------------------------------\n\n/** Copy a Babylon vector into a tuple. */\nexport function toVec3(v: BabylonVector3Like | null | undefined): Vec3Tuple | null {\n return v ? [v.x, v.y, v.z] : null;\n}\n\n/** Copy a Babylon quaternion into a tuple, defaulting to identity. */\nexport function toQuat(q: BabylonQuaternionLike | null | undefined): QuatTuple {\n return q ? [q.x, q.y, q.z, q.w] : [0, 0, 0, 1];\n}\n\n/** Write a tuple into an existing Babylon vector, in place. */\nexport function writeVec3(target: BabylonVector3Like, value: Vec3Tuple): void {\n target.x = value[0];\n target.y = value[1];\n target.z = value[2];\n}\n\n/** Write a tuple into an existing Babylon quaternion, in place. */\nexport function writeQuat(target: BabylonQuaternionLike, value: QuatTuple): void {\n target.x = value[0];\n target.y = value[1];\n target.z = value[2];\n target.w = value[3];\n}\n\n/** A node's world pose: absolute position and absolute rotation. */\nexport function nodeWorldPose(node: BabylonTransformNodeLike): PoseTuple {\n return {\n position: toVec3(node.getAbsolutePosition()) ?? [0, 0, 0],\n quaternion: toQuat(node.absoluteRotationQuaternion),\n };\n}\n\n/**\n * The world-space forward axis for a scene: +Z in Babylon's default\n * left-handed system, the opposite of three.js and of a raw WebXR target\n * ray, and -Z when the scene sets `useRightHandedSystem`.\n */\nexport function defaultForward(rightHanded = false): Vec3Tuple {\n return [0, 0, rightHanded ? -1 : 1];\n}\n\n/**\n * A node's forward direction in world space, honouring the scene's\n * handedness (see {@link defaultForward}).\n */\nexport function nodeForward(node: BabylonTransformNodeLike, rightHanded = false): Vec3Tuple {\n return vApplyQuat(defaultForward(rightHanded), toQuat(node.absoluteRotationQuaternion));\n}\n\n/**\n * The parent of a node, when it is one this adapter can read a world pose\n * from. Babylon types `parent` as `Node`, which has no transform, so a\n * parent that is a bone or a bare node reports null and the caller treats\n * the node as unparented.\n */\nexport function parentOf(node: BabylonTransformNodeLike): BabylonTransformNodeLike | null {\n const parent = node.parent;\n if (!parent || typeof parent !== \"object\") return null;\n const candidate = parent as Partial<BabylonTransformNodeLike>;\n return typeof candidate.getAbsolutePosition === \"function\"\n ? (parent as BabylonTransformNodeLike)\n : null;\n}\n\n/**\n * Is this node currently showing? A node the app disabled or hid is not a\n * hit-test candidate. Absent members mean yes - a fake, or a build that does\n * not carry them, should not silently drop out of targeting.\n */\nexport function nodeShowing(node: BabylonTransformNodeLike): boolean {\n if (node.isVisible === false) return false;\n return node.isEnabled ? node.isEnabled() !== false : true;\n}\n"]}
@@ -0,0 +1,57 @@
1
+ /**
2
+ * BabylonHitTester - resolves the core's ray and proximity queries against
3
+ * registered Babylon nodes.
4
+ *
5
+ * The default test needs no Babylon raycaster: each registered node carries
6
+ * a radius, and a query is a sphere test against the node's absolute
7
+ * position. That is the same model the IWSDK adapter uses, and it is enough
8
+ * for buttons, levers, dials and grabbables, which are small next to the
9
+ * distance they are pointed at from.
10
+ *
11
+ * An app that wants mesh-accurate picking supplies `pickWithRay`, typically
12
+ * a one-line wrapper over `scene.pickWithRay`. Whatever mesh it returns is
13
+ * mapped back to an interactable id through the registration map, walking up
14
+ * parents so a pick on a child mesh still resolves to the registered root.
15
+ */
16
+ import type { RayTuple, Vec3Tuple } from "@realitycollective/webxr-input";
17
+ import { type HitTester, type InteractableHit } from "@realitycollective/webxr-interactions";
18
+ import { type BabylonTransformNodeLike } from "./babylon-types.js";
19
+ /** What an app's mesh-accurate pick reports back. */
20
+ export interface BabylonPickResult {
21
+ /** The node that was hit. Mapped to an interactable through registration. */
22
+ mesh: BabylonTransformNodeLike | null | undefined;
23
+ /** Distance from the ray origin, in metres. */
24
+ distance: number;
25
+ /** World-space hit point. */
26
+ point: Vec3Tuple;
27
+ }
28
+ /**
29
+ * A mesh-accurate pick supplied by the app - `scene.pickWithRay` with a Ray
30
+ * built from these arguments. Return null for a miss.
31
+ */
32
+ export type BabylonPickWithRay = (origin: Vec3Tuple, direction: Vec3Tuple, maxDistance: number) => BabylonPickResult | null;
33
+ export interface BabylonHitTesterOptions {
34
+ /** Targeting radius for a node registered without one. Default 0.1 m. */
35
+ defaultRadius?: number;
36
+ /** Ray length handed to `pickWithRay`, in metres. Default 100. */
37
+ maxRayDistance?: number;
38
+ /** Optional mesh-accurate pick. Absent, the sphere test is used. */
39
+ pickWithRay?: BabylonPickWithRay;
40
+ }
41
+ export declare class BabylonHitTester implements HitTester {
42
+ private readonly entries;
43
+ private readonly byNode;
44
+ private readonly defaultRadius;
45
+ private readonly maxRayDistance;
46
+ private pickWithRay;
47
+ constructor(options?: BabylonHitTesterOptions);
48
+ /** Supply or replace the mesh-accurate pick after construction. */
49
+ setPickWithRay(pick: BabylonPickWithRay | null): void;
50
+ register(id: string, node: BabylonTransformNodeLike, radius?: number): void;
51
+ unregister(id: string): void;
52
+ getNode(id: string): BabylonTransformNodeLike | undefined;
53
+ hitRay(ray: RayTuple): InteractableHit | null;
54
+ hitProximity(point: Vec3Tuple, radius: number): InteractableHit | null;
55
+ /** The registered interactable a picked node belongs to, if any. */
56
+ private ownerOf;
57
+ }
@@ -0,0 +1,90 @@
1
+ import { rayPointDistance, } from "@realitycollective/webxr-interactions";
2
+ import { nodeShowing, parentOf, toVec3 } from "./babylon-types.js";
3
+ const DEFAULT_RADIUS = 0.1;
4
+ const DEFAULT_MAX_RAY_DISTANCE = 100;
5
+ export class BabylonHitTester {
6
+ entries = new Map();
7
+ byNode = new Map();
8
+ defaultRadius;
9
+ maxRayDistance;
10
+ pickWithRay;
11
+ constructor(options = {}) {
12
+ this.defaultRadius = options.defaultRadius ?? DEFAULT_RADIUS;
13
+ this.maxRayDistance = options.maxRayDistance ?? DEFAULT_MAX_RAY_DISTANCE;
14
+ this.pickWithRay = options.pickWithRay ?? null;
15
+ }
16
+ /** Supply or replace the mesh-accurate pick after construction. */
17
+ setPickWithRay(pick) {
18
+ this.pickWithRay = pick;
19
+ }
20
+ register(id, node, radius) {
21
+ this.unregister(id);
22
+ this.entries.set(id, { node, radius: radius ?? this.defaultRadius });
23
+ this.byNode.set(node, id);
24
+ }
25
+ unregister(id) {
26
+ const entry = this.entries.get(id);
27
+ if (entry)
28
+ this.byNode.delete(entry.node);
29
+ this.entries.delete(id);
30
+ }
31
+ getNode(id) {
32
+ return this.entries.get(id)?.node;
33
+ }
34
+ hitRay(ray) {
35
+ // An app-supplied pick REPLACES the sphere test for rays - it does not
36
+ // sit in front of it. A miss, or a hit on scenery, is the answer: the
37
+ // app's own pick already said what the ray reached first, and testing
38
+ // spheres behind it would target something the ray never got to.
39
+ // Proximity queries stay on the sphere test either way.
40
+ if (this.pickWithRay) {
41
+ const picked = this.pickWithRay(ray.origin, ray.direction, this.maxRayDistance);
42
+ if (!picked?.mesh)
43
+ return null;
44
+ const id = this.ownerOf(picked.mesh);
45
+ return id === null
46
+ ? null
47
+ : { interactableId: id, distance: picked.distance, point: picked.point };
48
+ }
49
+ let best = null;
50
+ for (const [id, { node, radius }] of this.entries) {
51
+ if (!nodeShowing(node))
52
+ continue;
53
+ const point = toVec3(node.getAbsolutePosition());
54
+ if (!point)
55
+ continue;
56
+ const { distance, t } = rayPointDistance(ray, point);
57
+ if (t > 0 && distance <= radius && (best === null || t < best.distance)) {
58
+ best = { interactableId: id, distance: t, point };
59
+ }
60
+ }
61
+ return best;
62
+ }
63
+ hitProximity(point, radius) {
64
+ let best = null;
65
+ for (const [id, { node, radius: targetRadius }] of this.entries) {
66
+ if (!nodeShowing(node))
67
+ continue;
68
+ const at = toVec3(node.getAbsolutePosition());
69
+ if (!at)
70
+ continue;
71
+ const distance = Math.hypot(at[0] - point[0], at[1] - point[1], at[2] - point[2]) - targetRadius;
72
+ if (distance <= radius && (best === null || distance < best.distance)) {
73
+ best = { interactableId: id, distance: Math.max(0, distance), point: at };
74
+ }
75
+ }
76
+ return best;
77
+ }
78
+ /** The registered interactable a picked node belongs to, if any. */
79
+ ownerOf(node) {
80
+ let current = node;
81
+ while (current) {
82
+ const id = this.byNode.get(current);
83
+ if (id !== undefined)
84
+ return id;
85
+ current = parentOf(current);
86
+ }
87
+ return null;
88
+ }
89
+ }
90
+ //# sourceMappingURL=hit-tester.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hit-tester.js","sourceRoot":"","sources":["../src/hit-tester.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * BabylonHitTester - resolves the core's ray and proximity queries against\n * registered Babylon nodes.\n *\n * The default test needs no Babylon raycaster: each registered node carries\n * a radius, and a query is a sphere test against the node's absolute\n * position. That is the same model the IWSDK adapter uses, and it is enough\n * for buttons, levers, dials and grabbables, which are small next to the\n * distance they are pointed at from.\n *\n * An app that wants mesh-accurate picking supplies `pickWithRay`, typically\n * a one-line wrapper over `scene.pickWithRay`. Whatever mesh it returns is\n * mapped back to an interactable id through the registration map, walking up\n * parents so a pick on a child mesh still resolves to the registered root.\n */\nimport type { RayTuple, Vec3Tuple } from \"@realitycollective/webxr-input\";\nimport {\n rayPointDistance,\n type HitTester,\n type InteractableHit,\n} from \"@realitycollective/webxr-interactions\";\nimport { nodeShowing, parentOf, toVec3, type BabylonTransformNodeLike } from \"./babylon-types.js\";\n\n/** What an app's mesh-accurate pick reports back. */\nexport interface BabylonPickResult {\n /** The node that was hit. Mapped to an interactable through registration. */\n mesh: BabylonTransformNodeLike | null | undefined;\n /** Distance from the ray origin, in metres. */\n distance: number;\n /** World-space hit point. */\n point: Vec3Tuple;\n}\n\n/**\n * A mesh-accurate pick supplied by the app - `scene.pickWithRay` with a Ray\n * built from these arguments. Return null for a miss.\n */\nexport type BabylonPickWithRay = (\n origin: Vec3Tuple,\n direction: Vec3Tuple,\n maxDistance: number,\n) => BabylonPickResult | null;\n\nexport interface BabylonHitTesterOptions {\n /** Targeting radius for a node registered without one. Default 0.1 m. */\n defaultRadius?: number;\n /** Ray length handed to `pickWithRay`, in metres. Default 100. */\n maxRayDistance?: number;\n /** Optional mesh-accurate pick. Absent, the sphere test is used. */\n pickWithRay?: BabylonPickWithRay;\n}\n\nconst DEFAULT_RADIUS = 0.1;\nconst DEFAULT_MAX_RAY_DISTANCE = 100;\n\nexport class BabylonHitTester implements HitTester {\n private readonly entries = new Map<string, { node: BabylonTransformNodeLike; radius: number }>();\n private readonly byNode = new Map<BabylonTransformNodeLike, string>();\n private readonly defaultRadius: number;\n private readonly maxRayDistance: number;\n private pickWithRay: BabylonPickWithRay | null;\n\n constructor(options: BabylonHitTesterOptions = {}) {\n this.defaultRadius = options.defaultRadius ?? DEFAULT_RADIUS;\n this.maxRayDistance = options.maxRayDistance ?? DEFAULT_MAX_RAY_DISTANCE;\n this.pickWithRay = options.pickWithRay ?? null;\n }\n\n /** Supply or replace the mesh-accurate pick after construction. */\n setPickWithRay(pick: BabylonPickWithRay | null): void {\n this.pickWithRay = pick;\n }\n\n register(id: string, node: BabylonTransformNodeLike, radius?: number): void {\n this.unregister(id);\n this.entries.set(id, { node, radius: radius ?? this.defaultRadius });\n this.byNode.set(node, id);\n }\n\n unregister(id: string): void {\n const entry = this.entries.get(id);\n if (entry) this.byNode.delete(entry.node);\n this.entries.delete(id);\n }\n\n getNode(id: string): BabylonTransformNodeLike | undefined {\n return this.entries.get(id)?.node;\n }\n\n hitRay(ray: RayTuple): InteractableHit | null {\n // An app-supplied pick REPLACES the sphere test for rays - it does not\n // sit in front of it. A miss, or a hit on scenery, is the answer: the\n // app's own pick already said what the ray reached first, and testing\n // spheres behind it would target something the ray never got to.\n // Proximity queries stay on the sphere test either way.\n if (this.pickWithRay) {\n const picked = this.pickWithRay(ray.origin, ray.direction, this.maxRayDistance);\n if (!picked?.mesh) return null;\n const id = this.ownerOf(picked.mesh);\n return id === null\n ? null\n : { interactableId: id, distance: picked.distance, point: picked.point };\n }\n\n let best: InteractableHit | null = null;\n for (const [id, { node, radius }] of this.entries) {\n if (!nodeShowing(node)) continue;\n const point = toVec3(node.getAbsolutePosition());\n if (!point) continue;\n const { distance, t } = rayPointDistance(ray, point);\n if (t > 0 && distance <= radius && (best === null || t < best.distance)) {\n best = { interactableId: id, distance: t, point };\n }\n }\n return best;\n }\n\n hitProximity(point: Vec3Tuple, radius: number): InteractableHit | null {\n let best: InteractableHit | null = null;\n for (const [id, { node, radius: targetRadius }] of this.entries) {\n if (!nodeShowing(node)) continue;\n const at = toVec3(node.getAbsolutePosition());\n if (!at) continue;\n const distance =\n Math.hypot(at[0] - point[0], at[1] - point[1], at[2] - point[2]) - targetRadius;\n if (distance <= radius && (best === null || distance < best.distance)) {\n best = { interactableId: id, distance: Math.max(0, distance), point: at };\n }\n }\n return best;\n }\n\n /** The registered interactable a picked node belongs to, if any. */\n private ownerOf(node: BabylonTransformNodeLike): string | null {\n let current: BabylonTransformNodeLike | null = node;\n while (current) {\n const id = this.byNode.get(current);\n if (id !== undefined) return id;\n current = parentOf(current);\n }\n return null;\n }\n}\n"]}