@realitycollective/babylon-interactions 0.1.1-preview.3 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,10 +4,20 @@ Change log for the Reality Collective WebXR Interaction Extensions packages. All
4
4
 
5
5
  The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Preview builds are not listed separately. The entry for a version accumulates while its previews are published, and is dated when that version is released.
6
6
 
7
- ## [0.1.1]
7
+ ## [0.1.1] - 2026-10-01
8
8
 
9
9
  ### Added
10
10
 
11
+ - Native parity round (Pale Signal handover of 29 September 2026): everything IWSDK delivers through its engine for the mechanics the Pale Signal web build uses, restated as core rules every binding runs, with the native contract stating what a host reports.
12
+ - `@realitycollective/webxr-interactions` - the grab lifecycle for hands is stated where it was implied: a hand's `select` is the runtime's binary pinch gesture and its `squeeze` 0 (the `@realitycollective/webxr-input` contract, IWSDK's `useHandPinchForGrab` over the browser's `selectstart`/`selectend`), so the core's 0.7/0.3 thresholds start a grab on the pinch and end it on the frame the pinch ends. `holderPoint` (the point a dial, slide or hinge tracks while held) is the GRIP first, as IWSDK's grab pointer is built on `xrOrigin.gripSpaces` and moves a held object with it, then the fingertip, then the ray origin; a closed hand's fingertip barely moves around a wheel's axis, which is why the native handwheel did not turn. `release-velocity.ts` (`ReleaseVelocityTracker`, `RELEASE_WINDOW_SECONDS` 1/60): the throw is the held OBJECT's velocity over IWSDK's physics step, angular from the orientation delta over the same step, which is what Havok gives a body IWSDK drives by target transform and releases; `GrabBehaviour` records the poses it writes and hands `endHold` that, zeros on a synthesised release (`InteractorInfo.synthesized`). `surfacePointOnSphere` and a `hitTesterContractCases()` case: `hitProximity`'s point is the surface point nearest the query, never the centre, so the touch cursor sits on the object; every platform's tester now reports it.
13
+ - `@realitycollective/webxr-interactions` - one pointer decision per source across interactables AND UI panels, as IWSDK's `MultiPointer` runs over every pointer-event object: `InteractionRuntime` takes `pointers` (a `PointerArbiter` from `@realitycollective/webxr-input`, shared with the UI Extensions hosts) and offers its touch, grab and ray candidates to it under `INTERACTIONS_POINTER_SET`; a panel nearer than an object takes the ray, a fingertip on a panel retires the ray over an object and the reverse, and `PointerVisuals` name the target kind, id and distance. `POINTER_PRIORITY`, `pickActivePointer`, `PointerCandidate`, `PointerVisuals` and `pointerVisualsFor` now live in `@realitycollective/webxr-input` and are re-exported unchanged. Two `nearPointerContractCases()` prove the arbitration on every platform's hit tester.
14
+ - `@realitycollective/webxr-interactions` - pointer display is app configuration through the core: `InteractionRuntime` takes `pointerDisplay` (`PointerDisplay` or a partial `PointerDisplayConfig`, IWSDK 1.0.0's look by default: ray while hitting, cursors on objects and panels), exposes `getPointerDisplay()` for run-time changes, and publishes `onPointerDrawing` / `getPointerDrawing`, the per-source `PointerDrawing` resolved from the arbiter's decision under the settings. `getPointerArbiter()` and `isSelecting()` are public.
15
+ - `@realitycollective/threejs-interactions` (reused by `@realitycollective/xrblocks-interactions`) - `ThreePointerVisuals` draws the drawing: IWSDK's ray shader and stub (3 cm from the hand to `min(0.3 m, the hit)`, 1 mm, white, blue while selecting) and the disc at the hit; `createThreeInteractions({ pointerVisuals: { scene }, pointers, pointerDisplay })` and `connectXRBlocksInteractions` take the same options.
16
+ - `@realitycollective/babylon-interactions` - `BabylonPointerVisuals` draws the same drawing through a structural kit of the app's own `MeshBuilder`, `StandardMaterial`, `Color3` and `Quaternion`; `createBabylonInteractions({ pointerVisuals: { kit }, pointers, pointerDisplay })`.
17
+ - `@realitycollective/iwsdk-interactions` - `IWSDKPointerVisuals` applies the app's settings to IWSDK's own `RayPointer` (`rayDisplayMode`, length) and `CursorVisual`, and enforces the core's drawing where a setting hides what IWSDK would show; `registerInteractions({ pointers, pointerDisplay, pointerVisuals })`.
18
+ - `@realitycollective/webxr-interactions` - the trace suite (G6): `trace.ts` (`rc-trace.v1`, `TraceRecorder`, `TraceInputProvider`, `replayTrace`, `compareOutcomes`, `traceParityCases`, `TRACE_TOLERANCES`) replays a recorded session's input through a binding's own ports and compares press and grab timing, values, poses and release velocity with the reference within stated tolerances; `memory-ports.ts` (`MemoryHitTester`, `MemoryTransformPort`) are the engine-free reference ports that pass both shared suites; `trace-synthetic.ts` scripts the playground mechanics through the core so the pipeline is proved on every platform before a person records on a Quest 3 (`iwsdk-interactions/test/trace-parity.test.ts` replays the synthetic traces on all five platforms, and recorded ones from the folder `RC_TRACES_DIR` names).
19
+ - The playground (`demos/playground`) runs on IWSDK, three.js, Babylon and XR Blocks from one launch screen with one descriptor, one pointer display and a Record button that downloads a trace; the native harness (`harness/native`) is a project for the WebXR-to-native conversion pipeline that runs every suite this family ships against a host on the device (`kits`), plays the same stations there (`play`) and records a native trace (`trace`), and `npm run harness:compile` typechecks it, bundles it through the pipeline's gates and compiles it to Hermes bytecode. CI compiles the harness and never runs it; native builds run only on a developer's machine or a headset.
20
+ - `@realitycollective/native-interactions` - `NativeInputHost.sample()` states what a hand and a controller report for `select` and `squeeze`, with the OpenXR source and resting values (a hand: binary pinch from `XR_FB_hand_tracking_aim` or `XR_EXT_hand_interaction`, squeeze 0; a controller: trigger and grip values), and the binding forces a hand's squeeze to 0. `applyPointerVisuals` hands the host `NativePointerVisuals`, the resolved drawing (stub from `rayFrom` to `rayTo`, radius, colour, disc radius, opacity, offset) plus the decision (`activePointer`, `targetKind`, `targetId`, `hitDistance`), so a panel cursor reaches the host through the same member and no shell hook decides a display mode; `applyPointerDisplay` carries the settings on every change. Every query considers registered interactables only, never scenery, and `hitProximity`'s point is the surface point nearest the query; the test host gains `placeScenery` and `pointerDisplay`. Kit cases: a hand's select is 0 or 1 and its squeeze 0; the pointer display is the app's and the host draws what it is handed; queries never answer with scenery enclosing the hand; the drawing case now covers a panel cursor and a ray told off. `createNativeInteractions({ pointers, pointerDisplay })`.
11
21
  - `@realitycollective/webxr-interactions` - the near-pointer rule, IWSDK 1.0.0's pointer stack restated as one core decision every binding runs (`near-pointer.ts`, `NEAR_POINTER_DEFAULTS`): a fingertip (or a controller's ray origin) within 0.15 m of a target's surface owns the source as the touch pointer (hysteresis out to 0.2 m), presses at 0.02 m arriving from outside and releases on the way out, never pressing from inside or behind; the grip is a 0.07 m grab sphere; per source the first of touch, grab, ray with a candidate is the active pointer, locked while it presses or grabs; and `InteractionRuntime` publishes per-source `PointerVisuals` (`onPointerVisuals`, `getPointerVisuals`, `getActivePointer`): the ray and its cursor show only while the ray owns the source or nothing does, with the cursor at the active pointer's hit. `InteractableDescriptor.pokeRadius` is now the touch hover enter distance for that target (default 0.15 m, was a 0.05 m proximity trigger). `nearPointerContractCases()` is the shared suite, run on every platform's hit tester.
12
22
  - `@realitycollective/webxr-interactions` - the physics contract (`physics.ts`): `PhysicsFacility`, the platform physics behind one set of semantics and IWSDK 1.0.0's defaults (`PHYSICS_DEFAULTS`: gravity `[0, -9.81, 0]`, 60 steps per second with interpolation, a dynamic body with no damping and gravity factor 1, an auto shape with density 1, restitution 0 and friction 0.5), static and kinematic states, suspend for a hold, resume with the release velocity, teleport with velocity cleared. `physicsFacilityContractCases()` proves a facility falls and rests, bounces, holds, throws, teleports and disposes; `MemoryPhysicsFacility` is the engine-free reference. Every binding names a default engine and takes an app's own facility instead: `createThreeInteractions({ physics: { rapier } })` (Rapier, `RapierPhysicsFacility`, proved against the real engine), `connectXRBlocksInteractions({ physics: { rapier } })` (the Rapier XR Blocks bundles), `registerInteractions` (Havok through IWSDK's components, `IWSDKPhysicsFacility`), `createBabylonInteractions({ physics: { kit } })` (Havok, `BabylonPhysicsFacility`, proved against a fake), and the native `physics` slice (`NativePhysicsHost`, Jolt on Quest and Android, RealityKit on visionOS, with every obligation stated in `native-types.ts` and the whole shared suite in the host conformance kit). A registered object with a body drives the held pose through its facility; `register` takes `body` and `shape` specs.
13
23
  - `@realitycollective/native-interactions` - `NativeInputHost.applyPointerVisuals(sourceId, visuals)`: the host is told each frame whether to draw a source's ray and where its cursor sits, and draws exactly that; the earlier rule that the host draws a cursor at every ray hit is withdrawn. The kit gains `input/the ray and cursor are drawn exactly as told`, the whole `nearPointerContractCases()` suite over the host's own queries, and the physics cases.
@@ -38,6 +48,8 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
38
48
 
39
49
  ### Changed
40
50
 
51
+ - Every package now requires `@realitycollective/webxr-input` at `^0.1.8-preview.0` rather than `^0.1.7`: the pointer arbiter and the pointer display the code above uses exist only from that release, so an older Input fails at install rather than at run time.
52
+
41
53
  - The reference is now IWSDK 1.0. `@realitycollective/iwsdk-interactions` peers on `@iwsdk/core` and `@iwsdk/xr-input` `>=1.0.0 <2.0.0` (was `>=0.5.0 <0.6.0`) and is built and tested against 1.0.0. The per-frame visibility write the presence code works around is unchanged in 1.0.
42
54
  - `@realitycollective/threejs-interactions`, `@realitycollective/babylon-interactions` and `@realitycollective/xrblocks-interactions` - a controller's `indexTip` is its ray origin, as IWSDK's input rig makes it, so a controller pokes buttons and interactables as a fingertip does. Before this only a tracked hand carried an index tip on these three platforms, and controllers could not poke at all.
43
55
  - **Breaking for native hosts:** `@realitycollective/native-interactions` now derives and decides what the IWSDK provider derives and decides, and a native host reports facts rather than conclusions. What a host must absorb:
@@ -48,9 +60,13 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
48
60
  - `@realitycollective/native-interactions` - `NativeInputProvider` samples no sources while the session lacks focus, as IWSDK's provider samples none unless its visibility state is `Visible`; clamps a pulse's intensity to 0..1, as IWSDK does; takes `nativeGrab` as IWSDK's provider does; and has a `dispose()`, which `NativeInteractions.dispose()` calls.
49
61
  - `@realitycollective/webxr-interactions` - `TransformPort.getWorldPose()` is the LIVE world pose on every platform, and the new required `getRestWorldPose()` returns the rest pose captured at registration, in world space. The core only said "current world pose", and the platforms disagreed: the three.js, IWSDK and XR Blocks ports returned the rest pose, while the Babylon.js port returned the live one. The same behaviour therefore acted differently by platform. The hinge, slide and dial behaviours measure the hand against the rest pose, so they now call `getRestWorldPose()`. Grab and toss scoring keep `getWorldPose()` and now see where the object really is. `ports.ts` also states tuple ownership for `TransformPort` and `HitTester`: every returned tuple is fresh and owned by the caller, and a tuple passed in is read during the call and not kept. Every platform already met the ownership rule; the suites now hold them to it. The native `interactions` slice gains `getRestWorldPose(targetId)`, which the native app must implement. This adds a required member to `TransformPort`, so a port written outside this repository must implement it.
50
62
  - `@realitycollective/xrblocks-interactions` - the structural slices `XBVec3Like`, `XBQuatLike`, `XBRayLike` and `XBControllerLike` are exported, under the same `XB` prefix as `XBRaySourceLike`, `XBDirectTouchLike` and `XBFrameLike` whose fields they type. They were module-private, so a consumer building a fake frame for a test had nothing to name, and TypeDoc reported each as referenced but undocumented.
63
+ - Dependencies on other Reality Collective repositories now follow one rule, checked by `scripts/rc-dependencies.mjs`. Going into `main`, each names the release npm tags `latest` and never a preview. Going into any other branch, each names the newest version on npm, release or preview. The rule covers every `package.json` in the repository (published packages, demos, harnesses and examples) and every copy `package-lock.json` records. A range passes only when its lowest version is the target, so `^0.1.8` passes for 0.1.8 and `^0.1.4` does not. CI runs it before the install, against the branch a pull request targets, and the publish workflow runs it before publishing. `release.mjs status` reports it. `release.mjs prepare` stops before cutting a release when a dependency names an unreleased preview of another repository, because that repository has to release first, and moves any range that is only behind. Other dependencies are out of scope. The script stays identical across the Reality Collective repositories.
51
64
 
52
65
  ### Fixed
53
66
 
67
+ - `@realitycollective/iwsdk-interactions` - `register` and the physics facility no longer throw in a world created with `features.physics` off. IWSDK registers `PhysicsBody` and `PhysicsShape` only when physics is on, and elics throws when an entity is asked about a component its world never registered, so the first registration in such an app failed with "Cannot read properties of null (reading 'words')". The binding now treats an unregistered component as absent; a test drives a real elics world with physics off.
68
+ - The playground's XR Blocks page drew nothing on a desktop: XR Blocks appends its canvas after the full-height container, below the visible page. The playground now moves it into the container.
69
+
54
70
  - `@realitycollective/threejs-interactions`, `@realitycollective/iwsdk-interactions` and `@realitycollective/xrblocks-interactions` - `getWorldPose()` returned the pose captured at registration, not where the object was. Toss scoring therefore read a thrown ball at its registered position until the app called `recaptureRest()`, and a second grab of an object moved without a recapture took its hold offset from the old position. `getWorldPose()` now returns the live pose, as the Babylon.js port already did.
55
71
  - `@realitycollective/webxr-interactions` - `InteractableDescriptor.pokeRadius` had no effect. The value was stored on the registration and documented as the poke trigger radius, but the proximity query always used the 5 cm default, so an interactable asking for a wider or narrower poke got the default. The runtime now queries once per source at the largest radius any enabled interactable registered, and accepts the nearest hit only when it lies inside that interactable's own radius. One query per source is kept deliberately; the trade is that a farther interactable with a larger radius is not found behind a nearer one with a smaller radius, because the hit tester returns only the nearest. Covered by a runtime test that failed before the change.
56
72
  - CI - the staging deploy published under `--branch=pr-<number>` while the `-test` Pages project's production branch is `staging`, so only `pr-<n>.webxr-interactions-test.pages.dev` aliases were ever created and the project's root URL was a 404. It now deploys as `staging` on every pull request and on every push to `development`, so `webxr-interactions-test.pages.dev` serves the newest preview build, the same arrangement WebXR-UIExtensions already had.
@@ -109,5 +125,5 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
109
125
  - All five packages depend on `@realitycollective/webxr-input`, released independently from the [WebXR-Input](https://github.com/realitycollective/WebXR-Input) repository. That package must be published before this one.
110
126
  - The interaction packages themselves carry no IWSDK coupling beyond the adapter: `webxr-interactions` is engine-free, and `threejs-interactions` and `xrblocks-interactions` peer only on three.js.
111
127
 
112
- [0.1.1]: https://github.com/realitycollective/WebXR-Interactions/compare/v0.1.0...development
128
+ [0.1.1]: https://github.com/realitycollective/WebXR-Interactions/releases/tag/v0.1.1
113
129
  [0.1.0]: https://github.com/realitycollective/WebXR-Interactions/releases/tag/v0.1.0
package/README.md CHANGED
@@ -8,7 +8,7 @@ npm install @realitycollective/babylon-interactions
8
8
 
9
9
  It re-exports everything from the core, so this is the only interaction package your app needs.
10
10
 
11
- > **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.
11
+ > **Maturity.** Written against the documented Babylon API and covered by structural fakes; the interaction playground runs it in a real Babylon scene on desktop. No headset pass yet. The physics facility fails 7 of the 10 shared physics cases against a real Havok plugin (`test/physics-facility-havok.test.ts`), so treat physics on Babylon as experimental.
12
12
 
13
13
  ## What it binds
14
14
 
@@ -20,6 +20,9 @@ It re-exports everything from the core, so this is the only interaction package
20
20
  | **Desktop** | `scene.onPointerObservable` as a pointer fallback, so the same scene is testable without a headset |
21
21
  | **Haptics** | Through the motion controller's `pulse` |
22
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
+ | **Pointer visuals** | `BabylonPointerVisuals` draws the app's `pointerDisplay` through a structural kit of your own `MeshBuilder`, `StandardMaterial`, `Color3` and `Quaternion`: `createBabylonInteractions({ pointerVisuals: { kit }, pointers, pointerDisplay })` |
24
+ | **Physics** | `BabylonPhysicsFacility` over Physics V2 (`physics: { kit }`), and `BabylonTransformPort`'s held pose over a node's `physicsBody` (`physicsMotionTypes`) |
25
+ | **Eye gaze** | `BabylonInputProvider` reads the `xr-eye-tracking` feature when the app enabled it and applies the shared gaze-and-pinch rule |
23
26
 
24
27
  ## Usage
25
28
 
@@ -52,7 +55,7 @@ interactions.setPickWithRay((origin, direction, maxDistance) => {
52
55
  - **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
56
  - **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
57
  - **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
- - **Held pose.** A node with a Physics V2 `physicsBody`, registered with `physicsMotionTypes` (Babylon's own `PhysicsMotionType.ANIMATED`/`.DYNAMIC` - this package holds no Babylon values, only shapes), grows `beginHold`/`endHold`: while held it switches to `ANIMATED` and follows `setWorldPose` exactly, ignoring gravity and collisions; on release it goes back to `DYNAMIC` with the grabbing hand's velocity, so a throw carries through; a `setWorldPose` while not held teleports and clears velocity. A node with no `physicsBody`, or a construction with no `physicsMotionTypes`, gets neither member and behaves exactly as before. This package has no `@babylonjs/core`/`@babylonjs/havok` dependency to verify the physics half against, so it is written from the Babylon Physics V2 documentation and untested against a live scene - confirm it there before relying on it.
58
+ - **Held pose.** A node with a Physics V2 `physicsBody`, registered with `physicsMotionTypes` (Babylon's own `PhysicsMotionType.ANIMATED`/`.DYNAMIC` - this package holds no Babylon values, only shapes), grows `beginHold`/`endHold`: while held it switches to `ANIMATED` and follows `setWorldPose` exactly, ignoring gravity and collisions; on release it goes back to `DYNAMIC` with the grabbing hand's velocity, so a throw carries through; a `setWorldPose` while not held teleports and clears velocity. A node with no `physicsBody`, or a construction with no `physicsMotionTypes`, gets neither member and behaves exactly as before. The physics half is written from the Babylon Physics V2 documentation. This repository installs neither `@babylonjs/core` nor `@babylonjs/havok`, so CI checks it against structural fakes only. With both installed, `test/physics-facility-havok.test.ts` runs the shared physics suite against real Havok; the maturity note above says where that stands. Confirm it in your own scene before relying on it.
56
59
 
57
60
  ## Peer dependency
58
61
 
@@ -60,7 +63,7 @@ None. Babylon is matched structurally, not imported, so any Babylon version whos
60
63
 
61
64
  ## Live demo
62
65
 
63
- The interaction playground - the three.js build of the same station set: **[webxr-interactions.pages.dev](https://webxr-interactions.pages.dev)**
66
+ The interaction playground - the same station set on every platform; add `?engine=babylon` for this adapter: **[webxr-interactions.pages.dev](https://webxr-interactions.pages.dev)**
64
67
 
65
68
  ## Documentation
66
69
 
@@ -1,4 +1,4 @@
1
- import { coneHitForSpheres, rayPointDistance, } from "@realitycollective/webxr-interactions";
1
+ import { surfacePointOnSphere, coneHitForSpheres, rayPointDistance, } from "@realitycollective/webxr-interactions";
2
2
  import { nodeShowing, parentOf, toVec3 } from "./babylon-types.js";
3
3
  const DEFAULT_RADIUS = 0.1;
4
4
  const DEFAULT_MAX_RAY_DISTANCE = 100;
@@ -70,7 +70,8 @@ export class BabylonHitTester {
70
70
  continue;
71
71
  const distance = Math.hypot(at[0] - point[0], at[1] - point[1], at[2] - point[2]) - targetRadius;
72
72
  if (distance <= radius && (best === null || distance < best.distance)) {
73
- best = { interactableId: id, distance: Math.max(0, distance), point: at };
73
+ // The surface point nearest the fingertip, where the touch cursor sits.
74
+ best = { interactableId: id, distance: Math.max(0, distance), point: surfacePointOnSphere(at, targetRadius, point) };
74
75
  }
75
76
  }
76
77
  return best;
@@ -1 +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 coneHitForSpheres,\n rayPointDistance,\n type HitTester,\n type InteractableHit,\n type SphereTarget,\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 /**\n * The eye-gaze cone (`ports.ts`, `hitCone`) over the registered spheres,\n * the core's `coneHitForSpheres`. With an app-supplied `pickWithRay`, a\n * mesh the ray itself reaches wins first, as it does for `hitRay`.\n */\n hitCone(ray: RayTuple, halfAngle: number, maxLength: number): InteractableHit | null {\n if (this.pickWithRay) {\n const direct = this.hitRay(ray);\n if (direct && direct.distance <= maxLength) return direct;\n }\n const spheres: SphereTarget[] = [];\n for (const [id, { node, radius }] of this.entries) {\n if (!nodeShowing(node)) continue;\n const center = toVec3(node.getAbsolutePosition());\n if (center) spheres.push({ id, center, radius });\n }\n const hit = coneHitForSpheres(ray, spheres, halfAngle, maxLength);\n return hit ? { interactableId: hit.interactableId, distance: hit.distance, point: hit.point } : null;\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"]}
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 surfacePointOnSphere,\n coneHitForSpheres,\n rayPointDistance,\n type HitTester,\n type InteractableHit,\n type SphereTarget,\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 // The surface point nearest the fingertip, where the touch cursor sits.\n best = { interactableId: id, distance: Math.max(0, distance), point: surfacePointOnSphere(at, targetRadius, point) };\n }\n }\n return best;\n }\n\n /**\n * The eye-gaze cone (`ports.ts`, `hitCone`) over the registered spheres,\n * the core's `coneHitForSpheres`. With an app-supplied `pickWithRay`, a\n * mesh the ray itself reaches wins first, as it does for `hitRay`.\n */\n hitCone(ray: RayTuple, halfAngle: number, maxLength: number): InteractableHit | null {\n if (this.pickWithRay) {\n const direct = this.hitRay(ray);\n if (direct && direct.distance <= maxLength) return direct;\n }\n const spheres: SphereTarget[] = [];\n for (const [id, { node, radius }] of this.entries) {\n if (!nodeShowing(node)) continue;\n const center = toVec3(node.getAbsolutePosition());\n if (center) spheres.push({ id, center, radius });\n }\n const hit = coneHitForSpheres(ray, spheres, halfAngle, maxLength);\n return hit ? { interactableId: hit.interactableId, distance: hit.distance, point: hit.point } : null;\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"]}
package/dist/host.d.ts CHANGED
@@ -11,9 +11,10 @@
11
11
  * `scene.onBeforeRenderObservable` and the engine's own frame delta.
12
12
  * Without it, call `update(dt)` yourself with seconds.
13
13
  */
14
- import { InteractionRuntime, type DwellConfig, type InteractableDescriptor, type PhysicsBodySpec, type PhysicsFacility, type PhysicsShapeSpec } from "@realitycollective/webxr-interactions";
14
+ import { InteractionRuntime, type DwellConfig, type InteractableDescriptor, type PhysicsBodySpec, type PhysicsFacility, type PhysicsShapeSpec, type PointerArbiter, type PointerDisplay, type PointerDisplayConfig } from "@realitycollective/webxr-interactions";
15
15
  import { type BabylonPhysicsKitLike, type BabylonTransformNodeLike } from "./babylon-types.js";
16
16
  import { BabylonHitTester, type BabylonHitTesterOptions, type BabylonPickWithRay } from "./hit-tester.js";
17
+ import { BabylonPointerVisuals, type BabylonPointerVisualsKit } from "./pointer-visuals.js";
17
18
  import { BabylonTransformPort, type BabylonTransformPortOptions } from "./transform-port.js";
18
19
  import { BabylonInputProvider, type BabylonProviderOptions } from "./provider.js";
19
20
  export interface BabylonInteractionsOptions extends BabylonProviderOptions, BabylonHitTesterOptions {
@@ -24,6 +25,30 @@ export interface BabylonInteractionsOptions extends BabylonProviderOptions, Baby
24
25
  * `update` from its own loop.
25
26
  */
26
27
  attachToScene?: boolean;
28
+ /**
29
+ * The pointer arbiter shared with the UI Extensions window host
30
+ * (`connectUIExtensions({ pointers })`), so one decision per source covers
31
+ * panels and interactables, as IWSDK's `MultiPointer` does. Omit when the
32
+ * app has no panels.
33
+ */
34
+ pointers?: PointerArbiter;
35
+ /**
36
+ * The app's pointer display settings: ray never, always or while hitting;
37
+ * cursors on objects and on panels; the ray and cursor look. Defaults are
38
+ * IWSDK 1.0.0's. Change them at run time through
39
+ * `runtime.getPointerDisplay().set(...)`.
40
+ */
41
+ pointerDisplay?: PointerDisplay | Partial<PointerDisplayConfig>;
42
+ /**
43
+ * Draw the ray stub and the cursor disc for every source, IWSDK's look,
44
+ * into `options.scene`, exactly as the core decides them each frame. Pass
45
+ * `{ kit }`, the Babylon constructors the renderer builds its meshes from.
46
+ * Omit to draw nothing (an app that draws its own reads
47
+ * `runtime.onPointerDrawing`).
48
+ */
49
+ pointerVisuals?: {
50
+ kit: BabylonPointerVisualsKit;
51
+ };
27
52
  /**
28
53
  * The platform's physics. Havok (Physics V2) is the named default engine
29
54
  * for Babylon: pass `{ kit }`, the Havok-era values from
@@ -60,6 +85,8 @@ export declare class BabylonInteractions {
60
85
  readonly hitTester: BabylonHitTester;
61
86
  /** The platform's physics, when the setup was given one. */
62
87
  readonly physics: PhysicsFacility | null;
88
+ /** The ray and cursor renderer, when the setup was given `pointerVisuals`. */
89
+ readonly pointerVisuals: BabylonPointerVisuals | null;
63
90
  private readonly stepPhysics;
64
91
  private readonly ports;
65
92
  private readonly nodes;
package/dist/host.js CHANGED
@@ -14,6 +14,7 @@
14
14
  import { InteractionRuntime, } from "@realitycollective/webxr-interactions";
15
15
  import { nodeWorldPose } from "./babylon-types.js";
16
16
  import { BabylonHitTester, } from "./hit-tester.js";
17
+ import { BabylonPointerVisuals } from "./pointer-visuals.js";
17
18
  import { BabylonPhysicsFacility } from "./physics-facility.js";
18
19
  import { BabylonTransformPort } from "./transform-port.js";
19
20
  import { BabylonInputProvider } from "./provider.js";
@@ -25,6 +26,8 @@ export class BabylonInteractions {
25
26
  hitTester;
26
27
  /** The platform's physics, when the setup was given one. */
27
28
  physics;
29
+ /** The ray and cursor renderer, when the setup was given `pointerVisuals`. */
30
+ pointerVisuals;
28
31
  stepPhysics;
29
32
  ports = new Map();
30
33
  nodes = new Map();
@@ -36,7 +39,12 @@ export class BabylonInteractions {
36
39
  provider: this.provider,
37
40
  hitTester: this.hitTester,
38
41
  ...(options.dwellDefaults ? { dwellDefaults: options.dwellDefaults } : {}),
42
+ ...(options.pointers ? { pointers: options.pointers } : {}),
43
+ ...(options.pointerDisplay ? { pointerDisplay: options.pointerDisplay } : {}),
39
44
  });
45
+ this.pointerVisuals = options.pointerVisuals
46
+ ? new BabylonPointerVisuals({ runtime: this.runtime, kit: options.pointerVisuals.kit, scene: options.scene })
47
+ : null;
40
48
  this.physics = options.physics
41
49
  ? "kit" in options.physics
42
50
  ? new BabylonPhysicsFacility(options.scene, options.physics.kit, { nodeFor: (id) => this.nodes.get(id) })
@@ -97,6 +105,7 @@ export class BabylonInteractions {
97
105
  dispose() {
98
106
  this.detachScene?.();
99
107
  this.detachScene = null;
108
+ this.pointerVisuals?.dispose();
100
109
  this.runtime.dispose();
101
110
  this.provider.dispose();
102
111
  this.physics?.dispose();
package/dist/host.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"host.js","sourceRoot":"","sources":["../src/host.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * createBabylonInteractions - one-call setup for the Babylon adapter.\n *\n * ```ts\n * const interactions = createBabylonInteractions({ scene, xr, attachToScene: true });\n * interactions.register({ id: \"button\", behaviours: [{ kind: \"press\" }] }, buttonMesh);\n * interactions.runtime.onEvent((event) => console.log(event.type));\n * ```\n *\n * With `attachToScene` the render loop drives itself from\n * `scene.onBeforeRenderObservable` and the engine's own frame delta.\n * Without it, call `update(dt)` yourself with seconds.\n */\nimport {\n InteractionRuntime,\n type DwellConfig,\n type InteractableDescriptor,\n type PhysicsBodySpec,\n type PhysicsFacility,\n type PhysicsShapeSpec,\n} from \"@realitycollective/webxr-interactions\";\nimport { nodeWorldPose, type BabylonPhysicsKitLike, type BabylonTransformNodeLike } from \"./babylon-types.js\";\nimport {\n BabylonHitTester,\n type BabylonHitTesterOptions,\n type BabylonPickWithRay,\n} from \"./hit-tester.js\";\nimport { BabylonPhysicsFacility } from \"./physics-facility.js\";\nimport { BabylonTransformPort, type BabylonTransformPortOptions } from \"./transform-port.js\";\nimport { BabylonInputProvider, type BabylonProviderOptions } from \"./provider.js\";\n\nexport interface BabylonInteractionsOptions\n extends BabylonProviderOptions,\n BabylonHitTesterOptions {\n dwellDefaults?: DwellConfig;\n /**\n * Drive `update(dt)` from `scene.onBeforeRenderObservable`, using the\n * engine's frame delta in seconds. Default false - the app calls\n * `update` from its own loop.\n */\n attachToScene?: boolean;\n /**\n * The platform's physics. Havok (Physics V2) is the named default engine\n * for Babylon: pass `{ kit }`, the Havok-era values from\n * `BabylonPhysicsKitLike`, and the binding builds a `BabylonPhysicsFacility`\n * over `options.scene`. Pass any `PhysicsFacility` instead to replace the\n * default engine with your own. Omit it and no node can carry a body:\n * `register` with a `body` or `shape` then throws.\n */\n physics?: PhysicsFacility | { kit: BabylonPhysicsKitLike };\n /**\n * Step the facility from `update(dt)`. Default true. Pass false when the\n * app steps its physics world itself - or lets Babylon's own render loop\n * do it, since Babylon normally steps physics from `scene.render()`.\n */\n stepPhysics?: boolean;\n}\n\nexport interface BabylonRegisterOptions extends BabylonTransformPortOptions {\n /** Targeting radius for the sphere hit-tester. Default 0.1 m. */\n targetRadius?: number;\n /**\n * Give the node a physics body with these settings (defaults are the\n * core's: dynamic, no damping, gravity factor 1). Needs `physics` on the\n * setup. With a body, the port applies the held-pose rule through it.\n */\n body?: PhysicsBodySpec;\n /** The body's collider (default `\"auto\"`: the node's own geometry). Implies `body`. */\n shape?: PhysicsShapeSpec;\n}\n\n/** Longest frame the scene-attached loop will report, in seconds. */\nconst MAX_FRAME_SECONDS = 0.1;\n\nexport class BabylonInteractions {\n readonly runtime: InteractionRuntime;\n readonly provider: BabylonInputProvider;\n readonly hitTester: BabylonHitTester;\n /** The platform's physics, when the setup was given one. */\n readonly physics: PhysicsFacility | null;\n private readonly stepPhysics: boolean;\n private readonly ports = new Map<string, BabylonTransformPort>();\n private readonly nodes = new Map<string, BabylonTransformNodeLike>();\n private detachScene: (() => void) | null = null;\n\n constructor(options: BabylonInteractionsOptions) {\n this.provider = new BabylonInputProvider(options);\n this.hitTester = new BabylonHitTester(options);\n this.runtime = new InteractionRuntime({\n provider: this.provider,\n hitTester: this.hitTester,\n ...(options.dwellDefaults ? { dwellDefaults: options.dwellDefaults } : {}),\n });\n this.physics = options.physics\n ? \"kit\" in options.physics\n ? new BabylonPhysicsFacility(options.scene, options.physics.kit, { nodeFor: (id) => this.nodes.get(id) })\n : options.physics\n : null;\n this.stepPhysics = options.stepPhysics ?? true;\n if (options.attachToScene) this.attachToScene(options);\n }\n\n /** Supply or replace the app's mesh-accurate pick. */\n setPickWithRay(pick: BabylonPickWithRay | null): void {\n this.hitTester.setPickWithRay(pick);\n }\n\n /**\n * Register an interactable with its Babylon node. With `body` or `shape`\n * the node gets a physics body in the facility at its current world pose,\n * and its port drives the held-pose rule through it.\n */\n register(\n descriptor: InteractableDescriptor,\n node: BabylonTransformNodeLike,\n options: BabylonRegisterOptions = {},\n ): BabylonTransformPort {\n this.nodes.set(descriptor.id, node);\n let port: BabylonTransformPort;\n if (options.body || options.shape) {\n if (!this.physics) {\n throw new Error(\n `[babylon-interactions] \"${descriptor.id}\" asks for a physics body but the setup has no physics; pass { kit } (the default engine) or a PhysicsFacility to createBabylonInteractions`,\n );\n }\n node.computeWorldMatrix?.(true);\n this.physics.addBody(descriptor.id, nodeWorldPose(node), options.body, options.shape);\n port = new BabylonTransformPort(node, {\n ...(options.createQuaternion ? { createQuaternion: options.createQuaternion } : {}),\n physics: { facility: this.physics, bodyId: descriptor.id },\n });\n } else {\n port = new BabylonTransformPort(\n node,\n options.createQuaternion ? { createQuaternion: options.createQuaternion } : {},\n );\n }\n this.ports.set(descriptor.id, port);\n this.hitTester.register(descriptor.id, node, options.targetRadius);\n this.runtime.registerInteractable(descriptor, { transform: port });\n return port;\n }\n\n unregister(id: string): void {\n this.runtime.unregisterInteractable(id);\n this.hitTester.unregister(id);\n this.ports.delete(id);\n this.nodes.delete(id);\n this.physics?.removeBody(id);\n }\n\n getPort(id: string): BabylonTransformPort | undefined {\n return this.ports.get(id);\n }\n\n update(dt: number): void {\n // The provider's eye-gaze filter and grace integrate over the same step.\n this.provider.setFrameDelta(dt);\n this.runtime.update(dt);\n if (this.stepPhysics) this.physics?.step(dt);\n }\n\n dispose(): void {\n this.detachScene?.();\n this.detachScene = null;\n this.runtime.dispose();\n this.provider.dispose();\n this.physics?.dispose();\n }\n\n private attachToScene(options: BabylonInteractionsOptions): void {\n const observable = options.scene.onBeforeRenderObservable;\n if (!observable) return;\n const engine = options.scene.getEngine?.();\n const observer = observable.add(() => {\n // Babylon reports the frame delta in milliseconds. A long frame - a tab\n // that was backgrounded, a slow first frame - is clamped so behaviours\n // integrate over a sane step rather than jumping.\n const ms = engine?.getDeltaTime() ?? 0;\n this.update(Math.min(MAX_FRAME_SECONDS, Math.max(0, ms / 1000)));\n });\n this.detachScene = () => {\n observable.remove(observer);\n };\n }\n}\n\nexport function createBabylonInteractions(\n options: BabylonInteractionsOptions,\n): BabylonInteractions {\n return new BabylonInteractions(options);\n}\n"]}
1
+ {"version":3,"file":"host.js","sourceRoot":"","sources":["../src/host.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * createBabylonInteractions - one-call setup for the Babylon adapter.\n *\n * ```ts\n * const interactions = createBabylonInteractions({ scene, xr, attachToScene: true });\n * interactions.register({ id: \"button\", behaviours: [{ kind: \"press\" }] }, buttonMesh);\n * interactions.runtime.onEvent((event) => console.log(event.type));\n * ```\n *\n * With `attachToScene` the render loop drives itself from\n * `scene.onBeforeRenderObservable` and the engine's own frame delta.\n * Without it, call `update(dt)` yourself with seconds.\n */\nimport {\n InteractionRuntime,\n type DwellConfig,\n type InteractableDescriptor,\n type PhysicsBodySpec,\n type PhysicsFacility,\n type PhysicsShapeSpec,\n type PointerArbiter,\n type PointerDisplay,\n type PointerDisplayConfig,\n} from \"@realitycollective/webxr-interactions\";\nimport { nodeWorldPose, type BabylonPhysicsKitLike, type BabylonTransformNodeLike } from \"./babylon-types.js\";\nimport {\n BabylonHitTester,\n type BabylonHitTesterOptions,\n type BabylonPickWithRay,\n} from \"./hit-tester.js\";\nimport { BabylonPointerVisuals, type BabylonPointerVisualsKit } from \"./pointer-visuals.js\";\nimport { BabylonPhysicsFacility } from \"./physics-facility.js\";\nimport { BabylonTransformPort, type BabylonTransformPortOptions } from \"./transform-port.js\";\nimport { BabylonInputProvider, type BabylonProviderOptions } from \"./provider.js\";\n\nexport interface BabylonInteractionsOptions\n extends BabylonProviderOptions,\n BabylonHitTesterOptions {\n dwellDefaults?: DwellConfig;\n /**\n * Drive `update(dt)` from `scene.onBeforeRenderObservable`, using the\n * engine's frame delta in seconds. Default false - the app calls\n * `update` from its own loop.\n */\n attachToScene?: boolean;\n /**\n * The pointer arbiter shared with the UI Extensions window host\n * (`connectUIExtensions({ pointers })`), so one decision per source covers\n * panels and interactables, as IWSDK's `MultiPointer` does. Omit when the\n * app has no panels.\n */\n pointers?: PointerArbiter;\n /**\n * The app's pointer display settings: ray never, always or while hitting;\n * cursors on objects and on panels; the ray and cursor look. Defaults are\n * IWSDK 1.0.0's. Change them at run time through\n * `runtime.getPointerDisplay().set(...)`.\n */\n pointerDisplay?: PointerDisplay | Partial<PointerDisplayConfig>;\n /**\n * Draw the ray stub and the cursor disc for every source, IWSDK's look,\n * into `options.scene`, exactly as the core decides them each frame. Pass\n * `{ kit }`, the Babylon constructors the renderer builds its meshes from.\n * Omit to draw nothing (an app that draws its own reads\n * `runtime.onPointerDrawing`).\n */\n pointerVisuals?: { kit: BabylonPointerVisualsKit };\n /**\n * The platform's physics. Havok (Physics V2) is the named default engine\n * for Babylon: pass `{ kit }`, the Havok-era values from\n * `BabylonPhysicsKitLike`, and the binding builds a `BabylonPhysicsFacility`\n * over `options.scene`. Pass any `PhysicsFacility` instead to replace the\n * default engine with your own. Omit it and no node can carry a body:\n * `register` with a `body` or `shape` then throws.\n */\n physics?: PhysicsFacility | { kit: BabylonPhysicsKitLike };\n /**\n * Step the facility from `update(dt)`. Default true. Pass false when the\n * app steps its physics world itself - or lets Babylon's own render loop\n * do it, since Babylon normally steps physics from `scene.render()`.\n */\n stepPhysics?: boolean;\n}\n\nexport interface BabylonRegisterOptions extends BabylonTransformPortOptions {\n /** Targeting radius for the sphere hit-tester. Default 0.1 m. */\n targetRadius?: number;\n /**\n * Give the node a physics body with these settings (defaults are the\n * core's: dynamic, no damping, gravity factor 1). Needs `physics` on the\n * setup. With a body, the port applies the held-pose rule through it.\n */\n body?: PhysicsBodySpec;\n /** The body's collider (default `\"auto\"`: the node's own geometry). Implies `body`. */\n shape?: PhysicsShapeSpec;\n}\n\n/** Longest frame the scene-attached loop will report, in seconds. */\nconst MAX_FRAME_SECONDS = 0.1;\n\nexport class BabylonInteractions {\n readonly runtime: InteractionRuntime;\n readonly provider: BabylonInputProvider;\n readonly hitTester: BabylonHitTester;\n /** The platform's physics, when the setup was given one. */\n readonly physics: PhysicsFacility | null;\n /** The ray and cursor renderer, when the setup was given `pointerVisuals`. */\n readonly pointerVisuals: BabylonPointerVisuals | null;\n private readonly stepPhysics: boolean;\n private readonly ports = new Map<string, BabylonTransformPort>();\n private readonly nodes = new Map<string, BabylonTransformNodeLike>();\n private detachScene: (() => void) | null = null;\n\n constructor(options: BabylonInteractionsOptions) {\n this.provider = new BabylonInputProvider(options);\n this.hitTester = new BabylonHitTester(options);\n this.runtime = new InteractionRuntime({\n provider: this.provider,\n hitTester: this.hitTester,\n ...(options.dwellDefaults ? { dwellDefaults: options.dwellDefaults } : {}),\n ...(options.pointers ? { pointers: options.pointers } : {}),\n ...(options.pointerDisplay ? { pointerDisplay: options.pointerDisplay } : {}),\n });\n this.pointerVisuals = options.pointerVisuals\n ? new BabylonPointerVisuals({ runtime: this.runtime, kit: options.pointerVisuals.kit, scene: options.scene })\n : null;\n this.physics = options.physics\n ? \"kit\" in options.physics\n ? new BabylonPhysicsFacility(options.scene, options.physics.kit, { nodeFor: (id) => this.nodes.get(id) })\n : options.physics\n : null;\n this.stepPhysics = options.stepPhysics ?? true;\n if (options.attachToScene) this.attachToScene(options);\n }\n\n /** Supply or replace the app's mesh-accurate pick. */\n setPickWithRay(pick: BabylonPickWithRay | null): void {\n this.hitTester.setPickWithRay(pick);\n }\n\n /**\n * Register an interactable with its Babylon node. With `body` or `shape`\n * the node gets a physics body in the facility at its current world pose,\n * and its port drives the held-pose rule through it.\n */\n register(\n descriptor: InteractableDescriptor,\n node: BabylonTransformNodeLike,\n options: BabylonRegisterOptions = {},\n ): BabylonTransformPort {\n this.nodes.set(descriptor.id, node);\n let port: BabylonTransformPort;\n if (options.body || options.shape) {\n if (!this.physics) {\n throw new Error(\n `[babylon-interactions] \"${descriptor.id}\" asks for a physics body but the setup has no physics; pass { kit } (the default engine) or a PhysicsFacility to createBabylonInteractions`,\n );\n }\n node.computeWorldMatrix?.(true);\n this.physics.addBody(descriptor.id, nodeWorldPose(node), options.body, options.shape);\n port = new BabylonTransformPort(node, {\n ...(options.createQuaternion ? { createQuaternion: options.createQuaternion } : {}),\n physics: { facility: this.physics, bodyId: descriptor.id },\n });\n } else {\n port = new BabylonTransformPort(\n node,\n options.createQuaternion ? { createQuaternion: options.createQuaternion } : {},\n );\n }\n this.ports.set(descriptor.id, port);\n this.hitTester.register(descriptor.id, node, options.targetRadius);\n this.runtime.registerInteractable(descriptor, { transform: port });\n return port;\n }\n\n unregister(id: string): void {\n this.runtime.unregisterInteractable(id);\n this.hitTester.unregister(id);\n this.ports.delete(id);\n this.nodes.delete(id);\n this.physics?.removeBody(id);\n }\n\n getPort(id: string): BabylonTransformPort | undefined {\n return this.ports.get(id);\n }\n\n update(dt: number): void {\n // The provider's eye-gaze filter and grace integrate over the same step.\n this.provider.setFrameDelta(dt);\n this.runtime.update(dt);\n if (this.stepPhysics) this.physics?.step(dt);\n }\n\n dispose(): void {\n this.detachScene?.();\n this.detachScene = null;\n this.pointerVisuals?.dispose();\n this.runtime.dispose();\n this.provider.dispose();\n this.physics?.dispose();\n }\n\n private attachToScene(options: BabylonInteractionsOptions): void {\n const observable = options.scene.onBeforeRenderObservable;\n if (!observable) return;\n const engine = options.scene.getEngine?.();\n const observer = observable.add(() => {\n // Babylon reports the frame delta in milliseconds. A long frame - a tab\n // that was backgrounded, a slow first frame - is clamped so behaviours\n // integrate over a sane step rather than jumping.\n const ms = engine?.getDeltaTime() ?? 0;\n this.update(Math.min(MAX_FRAME_SECONDS, Math.max(0, ms / 1000)));\n });\n this.detachScene = () => {\n observable.remove(observer);\n };\n }\n}\n\nexport function createBabylonInteractions(\n options: BabylonInteractionsOptions,\n): BabylonInteractions {\n return new BabylonInteractions(options);\n}\n"]}
package/dist/index.d.ts CHANGED
@@ -14,4 +14,5 @@ export * from "./provider.js";
14
14
  export * from "./hit-tester.js";
15
15
  export * from "./transform-port.js";
16
16
  export * from "./physics-facility.js";
17
+ export * from "./pointer-visuals.js";
17
18
  export * from "./host.js";
package/dist/index.js CHANGED
@@ -14,5 +14,6 @@ export * from "./provider.js";
14
14
  export * from "./hit-tester.js";
15
15
  export * from "./transform-port.js";
16
16
  export * from "./physics-facility.js";
17
+ export * from "./pointer-visuals.js";
17
18
  export * from "./host.js";
18
19
  //# 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;;;;;;;;;GASG;AACH,cAAc,uCAAuC,CAAC;AAEtD,cAAc,oBAAoB,CAAC;AACnC,cAAc,eAAe,CAAC;AAC9B,cAAc,iBAAiB,CAAC;AAChC,cAAc,qBAAqB,CAAC;AACpC,cAAc,uBAAuB,CAAC;AACtC,cAAc,WAAW,CAAC","sourcesContent":["/**\n * @realitycollective/babylon-interactions - the Babylon.js engine adapter:\n * Babylon WebXR input, sphere or app-supplied hit-testing, and transform\n * ports over Babylon nodes. The core is re-exported wholesale, so apps\n * depend on exactly this package.\n *\n * Structurally typed against the Babylon API rather than importing\n * `@babylonjs/core`, in the same way as the XR Blocks adapter, so an\n * upstream release cannot break the install.\n */\nexport * from \"@realitycollective/webxr-interactions\";\n\nexport * from \"./babylon-types.js\";\nexport * from \"./provider.js\";\nexport * from \"./hit-tester.js\";\nexport * from \"./transform-port.js\";\nexport * from \"./physics-facility.js\";\nexport * from \"./host.js\";\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,cAAc,uCAAuC,CAAC;AAEtD,cAAc,oBAAoB,CAAC;AACnC,cAAc,eAAe,CAAC;AAC9B,cAAc,iBAAiB,CAAC;AAChC,cAAc,qBAAqB,CAAC;AACpC,cAAc,uBAAuB,CAAC;AACtC,cAAc,sBAAsB,CAAC;AACrC,cAAc,WAAW,CAAC","sourcesContent":["/**\n * @realitycollective/babylon-interactions - the Babylon.js engine adapter:\n * Babylon WebXR input, sphere or app-supplied hit-testing, and transform\n * ports over Babylon nodes. The core is re-exported wholesale, so apps\n * depend on exactly this package.\n *\n * Structurally typed against the Babylon API rather than importing\n * `@babylonjs/core`, in the same way as the XR Blocks adapter, so an\n * upstream release cannot break the install.\n */\nexport * from \"@realitycollective/webxr-interactions\";\n\nexport * from \"./babylon-types.js\";\nexport * from \"./provider.js\";\nexport * from \"./hit-tester.js\";\nexport * from \"./transform-port.js\";\nexport * from \"./physics-facility.js\";\nexport * from \"./pointer-visuals.js\";\nexport * from \"./host.js\";\n"]}
@@ -0,0 +1,105 @@
1
+ /**
2
+ * BabylonPointerVisuals - draws the ray stub and the cursor disc for every
3
+ * source, exactly as the core decided them (`InteractionRuntime.onPointerDrawing`:
4
+ * the pointer arbiter's decision under the app's pointer display settings).
5
+ *
6
+ * This is IWSDK 1.0.0's look restated for Babylon (`@iwsdk/xr-input`
7
+ * `ray-pointer.js` and `cursor-visual.js`), so a Babylon app shows what a
8
+ * Quest shows on the web:
9
+ *
10
+ * - the ray is a thin cylinder along the source's ray, spanning `rayFrom` to
11
+ * `rayTo` metres from its origin, so with the defaults it is the 1 mm white
12
+ * stub from 3 cm in front of the hand to `min(0.3 m, the hit)`, blue while
13
+ * selecting;
14
+ * - the cursor is a disc at the hit, facing the viewer along the pointing
15
+ * direction (the ray's, or the line from the head for a near hit), nudged
16
+ * `cursorOffset` off the surface, white, with opacity and radius from the
17
+ * drawing.
18
+ *
19
+ * The renderer decides nothing: it reads each frame's `PointerDrawing` and
20
+ * the source's ray from the runtime and writes them onto its meshes. Meshes
21
+ * are created per source on first sight and disposed with the renderer.
22
+ * Positions and directions arrive in world space as the provider gives them,
23
+ * so nothing is flipped for Babylon's left-handed default. Like the rest of
24
+ * this package it never imports `@babylonjs/core`: the app hands over the
25
+ * constructors it uses as a {@link BabylonPointerVisualsKit}.
26
+ */
27
+ import type { InteractionRuntime, PointerDrawing } from "@realitycollective/webxr-interactions";
28
+ import type { BabylonQuaternionLike, BabylonVector3Like } from "./babylon-types.js";
29
+ /** Structural slice of Babylon's `Color3`. */
30
+ export interface BabylonColor3Like {
31
+ r: number;
32
+ g: number;
33
+ b: number;
34
+ }
35
+ /** Structural slice of the `StandardMaterial` members the renderer writes. */
36
+ export interface BabylonPointerMaterialLike {
37
+ emissiveColor: BabylonColor3Like;
38
+ diffuseColor: BabylonColor3Like;
39
+ /** 0 transparent to 1 opaque. */
40
+ alpha: number;
41
+ /** Unlit so the colour is the colour on screen. */
42
+ disableLighting?: boolean;
43
+ dispose(): void;
44
+ }
45
+ /** Structural slice of the `Mesh` members the renderer writes. */
46
+ export interface BabylonPointerMeshLike {
47
+ position: BabylonVector3Like;
48
+ rotationQuaternion: BabylonQuaternionLike | null;
49
+ scaling: BabylonVector3Like;
50
+ isVisible: boolean;
51
+ material: BabylonPointerMaterialLike | null;
52
+ /** Higher groups draw later; the cursor uses 1 so it draws over the scene. */
53
+ renderingGroupId?: number;
54
+ dispose(): void;
55
+ }
56
+ /** What the app imports from `@babylonjs/core` and hands to the renderer. */
57
+ export interface BabylonPointerVisualsKit {
58
+ MeshBuilder: {
59
+ /** A cylinder along the Y axis, centred on the origin. */
60
+ CreateCylinder(name: string, options: {
61
+ height: number;
62
+ diameter: number;
63
+ tessellation?: number;
64
+ }, scene: unknown): BabylonPointerMeshLike;
65
+ /** A disc in the XY plane whose front face looks down -Z. */
66
+ CreateDisc(name: string, options: {
67
+ radius: number;
68
+ tessellation?: number;
69
+ }, scene: unknown): BabylonPointerMeshLike;
70
+ };
71
+ StandardMaterial: new (name: string, scene: unknown) => BabylonPointerMaterialLike;
72
+ Color3: new (r: number, g: number, b: number) => BabylonColor3Like;
73
+ Quaternion: new (x: number, y: number, z: number, w: number) => BabylonQuaternionLike;
74
+ }
75
+ export interface BabylonPointerVisualsOptions {
76
+ /** The runtime whose drawings are applied. */
77
+ runtime: InteractionRuntime;
78
+ /** The Babylon constructors the renderer builds its meshes from. */
79
+ kit: BabylonPointerVisualsKit;
80
+ /** The scene the meshes are created in (handed to the kit as is). */
81
+ scene: unknown;
82
+ }
83
+ interface SourceMeshes {
84
+ ray: BabylonPointerMeshLike;
85
+ cursor: BabylonPointerMeshLike;
86
+ }
87
+ export declare class BabylonPointerVisuals {
88
+ private readonly runtime;
89
+ private readonly kit;
90
+ private readonly scene;
91
+ private readonly meshes;
92
+ private readonly unsubscribe;
93
+ private disposed;
94
+ constructor(options: BabylonPointerVisualsOptions);
95
+ /** The meshes drawn for one source, for a test or an app that restyles them. */
96
+ meshesFor(sourceId: string): SourceMeshes | undefined;
97
+ /** Apply one frame's drawings: create meshes for new sources, hide those of sources gone. */
98
+ apply(drawings: readonly PointerDrawing[]): void;
99
+ private create;
100
+ private material;
101
+ private applyRay;
102
+ private applyCursor;
103
+ dispose(): void;
104
+ }
105
+ export {};
@@ -0,0 +1,149 @@
1
+ /** The unit vector along (x, y, z); +Z for a zero vector. */
2
+ function unit(x, y, z) {
3
+ const length = Math.hypot(x, y, z);
4
+ return length < 1e-6 ? [0, 0, 1] : [x / length, y / length, z / length];
5
+ }
6
+ /**
7
+ * The quaternion turning the axis `from` (+Y or +Z) onto the unit vector `to`.
8
+ * Antiparallel vectors turn half a circle about X, which is square to both axes.
9
+ */
10
+ function rotationBetween(from, to) {
11
+ const dot = from[0] * to[0] + from[1] * to[1] + from[2] * to[2];
12
+ if (dot < -0.999999)
13
+ return [1, 0, 0, 0];
14
+ const x = from[1] * to[2] - from[2] * to[1];
15
+ const y = from[2] * to[0] - from[0] * to[2];
16
+ const z = from[0] * to[1] - from[1] * to[0];
17
+ const w = 1 + dot;
18
+ const length = Math.hypot(x, y, z, w);
19
+ return [x / length, y / length, z / length, w / length];
20
+ }
21
+ function setColour(target, rgb) {
22
+ target.r = rgb[0];
23
+ target.g = rgb[1];
24
+ target.b = rgb[2];
25
+ }
26
+ const Y_AXIS = [0, 1, 0];
27
+ const Z_AXIS = [0, 0, 1];
28
+ export class BabylonPointerVisuals {
29
+ runtime;
30
+ kit;
31
+ scene;
32
+ meshes = new Map();
33
+ unsubscribe;
34
+ disposed = false;
35
+ constructor(options) {
36
+ this.runtime = options.runtime;
37
+ this.kit = options.kit;
38
+ this.scene = options.scene;
39
+ this.unsubscribe = this.runtime.onPointerDrawing((drawings) => this.apply(drawings));
40
+ }
41
+ /** The meshes drawn for one source, for a test or an app that restyles them. */
42
+ meshesFor(sourceId) {
43
+ return this.meshes.get(sourceId);
44
+ }
45
+ /** Apply one frame's drawings: create meshes for new sources, hide those of sources gone. */
46
+ apply(drawings) {
47
+ if (this.disposed)
48
+ return;
49
+ const seen = new Set();
50
+ for (const drawing of drawings) {
51
+ seen.add(drawing.sourceId);
52
+ const meshes = this.meshes.get(drawing.sourceId) ?? this.create(drawing.sourceId);
53
+ this.applyRay(meshes.ray, drawing);
54
+ this.applyCursor(meshes.cursor, drawing);
55
+ }
56
+ for (const [id, meshes] of this.meshes) {
57
+ if (!seen.has(id)) {
58
+ meshes.ray.isVisible = false;
59
+ meshes.cursor.isVisible = false;
60
+ }
61
+ }
62
+ }
63
+ create(sourceId) {
64
+ const ray = this.kit.MeshBuilder.CreateCylinder(`pointer-ray-${sourceId}`, { height: 1, diameter: 1, tessellation: 6 }, this.scene);
65
+ ray.material = this.material(`pointer-ray-${sourceId}-material`);
66
+ ray.isVisible = false;
67
+ const cursor = this.kit.MeshBuilder.CreateDisc(`pointer-cursor-${sourceId}`, { radius: 1, tessellation: 32 }, this.scene);
68
+ cursor.material = this.material(`pointer-cursor-${sourceId}-material`);
69
+ cursor.renderingGroupId = 1;
70
+ cursor.isVisible = false;
71
+ const meshes = { ray, cursor };
72
+ this.meshes.set(sourceId, meshes);
73
+ return meshes;
74
+ }
75
+ material(name) {
76
+ const material = new this.kit.StandardMaterial(name, this.scene);
77
+ material.emissiveColor = new this.kit.Color3(1, 1, 1);
78
+ material.diffuseColor = new this.kit.Color3(1, 1, 1);
79
+ material.disableLighting = true;
80
+ material.alpha = 1;
81
+ return material;
82
+ }
83
+ applyRay(mesh, drawing) {
84
+ const ray = this.runtime.getSource(drawing.sourceId)?.ray;
85
+ const length = drawing.rayTo - drawing.rayFrom;
86
+ if (!drawing.ray || !ray || length <= 0) {
87
+ mesh.isVisible = false;
88
+ return;
89
+ }
90
+ const dir = unit(ray.direction[0], ray.direction[1], ray.direction[2]);
91
+ const centre = (drawing.rayFrom + drawing.rayTo) / 2;
92
+ mesh.position.x = ray.origin[0] + dir[0] * centre;
93
+ mesh.position.y = ray.origin[1] + dir[1] * centre;
94
+ mesh.position.z = ray.origin[2] + dir[2] * centre;
95
+ mesh.scaling.x = drawing.rayRadius * 2;
96
+ mesh.scaling.y = length;
97
+ mesh.scaling.z = drawing.rayRadius * 2;
98
+ const q = rotationBetween(Y_AXIS, dir);
99
+ mesh.rotationQuaternion = new this.kit.Quaternion(q[0], q[1], q[2], q[3]);
100
+ const material = mesh.material;
101
+ setColour(material.emissiveColor, drawing.rayColor);
102
+ setColour(material.diffuseColor, drawing.rayColor);
103
+ material.alpha = 1;
104
+ mesh.isVisible = true;
105
+ }
106
+ applyCursor(mesh, drawing) {
107
+ if (!drawing.cursor || !drawing.cursorPoint) {
108
+ mesh.isVisible = false;
109
+ return;
110
+ }
111
+ const point = drawing.cursorPoint;
112
+ const source = this.runtime.getSource(drawing.sourceId);
113
+ // Pointing direction: along the ray for a ray hit; from the head for a near hit.
114
+ let dir;
115
+ if (source?.ray && drawing.ray) {
116
+ dir = unit(source.ray.direction[0], source.ray.direction[1], source.ray.direction[2]);
117
+ }
118
+ else {
119
+ const head = this.runtime.getProvider().getHeadPose?.()?.position;
120
+ dir = head ? unit(point[0] - head[0], point[1] - head[1], point[2] - head[2]) : Z_AXIS;
121
+ }
122
+ // The disc's front face looks down -Z, so turning +Z onto the pointing
123
+ // direction makes it face back at the viewer. It sits `cursorOffset` toward the viewer.
124
+ const q = rotationBetween(Z_AXIS, dir);
125
+ mesh.rotationQuaternion = new this.kit.Quaternion(q[0], q[1], q[2], q[3]);
126
+ mesh.position.x = point[0] - dir[0] * drawing.cursorOffset;
127
+ mesh.position.y = point[1] - dir[1] * drawing.cursorOffset;
128
+ mesh.position.z = point[2] - dir[2] * drawing.cursorOffset;
129
+ mesh.scaling.x = drawing.cursorRadius;
130
+ mesh.scaling.y = drawing.cursorRadius;
131
+ mesh.scaling.z = drawing.cursorRadius;
132
+ mesh.material.alpha = drawing.cursorOpacity;
133
+ mesh.isVisible = true;
134
+ }
135
+ dispose() {
136
+ if (this.disposed)
137
+ return;
138
+ this.disposed = true;
139
+ this.unsubscribe();
140
+ for (const { ray, cursor } of this.meshes.values()) {
141
+ for (const mesh of [ray, cursor]) {
142
+ mesh.material.dispose();
143
+ mesh.dispose();
144
+ }
145
+ }
146
+ this.meshes.clear();
147
+ }
148
+ }
149
+ //# sourceMappingURL=pointer-visuals.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pointer-visuals.js","sourceRoot":"","sources":["../src/pointer-visuals.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * BabylonPointerVisuals - draws the ray stub and the cursor disc for every\n * source, exactly as the core decided them (`InteractionRuntime.onPointerDrawing`:\n * the pointer arbiter's decision under the app's pointer display settings).\n *\n * This is IWSDK 1.0.0's look restated for Babylon (`@iwsdk/xr-input`\n * `ray-pointer.js` and `cursor-visual.js`), so a Babylon app shows what a\n * Quest shows on the web:\n *\n * - the ray is a thin cylinder along the source's ray, spanning `rayFrom` to\n * `rayTo` metres from its origin, so with the defaults it is the 1 mm white\n * stub from 3 cm in front of the hand to `min(0.3 m, the hit)`, blue while\n * selecting;\n * - the cursor is a disc at the hit, facing the viewer along the pointing\n * direction (the ray's, or the line from the head for a near hit), nudged\n * `cursorOffset` off the surface, white, with opacity and radius from the\n * drawing.\n *\n * The renderer decides nothing: it reads each frame's `PointerDrawing` and\n * the source's ray from the runtime and writes them onto its meshes. Meshes\n * are created per source on first sight and disposed with the renderer.\n * Positions and directions arrive in world space as the provider gives them,\n * so nothing is flipped for Babylon's left-handed default. Like the rest of\n * this package it never imports `@babylonjs/core`: the app hands over the\n * constructors it uses as a {@link BabylonPointerVisualsKit}.\n */\nimport type { InteractionRuntime, PointerDrawing, Unsubscribe } from \"@realitycollective/webxr-interactions\";\nimport type { BabylonQuaternionLike, BabylonVector3Like } from \"./babylon-types.js\";\n\n/** Structural slice of Babylon's `Color3`. */\nexport interface BabylonColor3Like {\n r: number;\n g: number;\n b: number;\n}\n\n/** Structural slice of the `StandardMaterial` members the renderer writes. */\nexport interface BabylonPointerMaterialLike {\n emissiveColor: BabylonColor3Like;\n diffuseColor: BabylonColor3Like;\n /** 0 transparent to 1 opaque. */\n alpha: number;\n /** Unlit so the colour is the colour on screen. */\n disableLighting?: boolean;\n dispose(): void;\n}\n\n/** Structural slice of the `Mesh` members the renderer writes. */\nexport interface BabylonPointerMeshLike {\n position: BabylonVector3Like;\n rotationQuaternion: BabylonQuaternionLike | null;\n scaling: BabylonVector3Like;\n isVisible: boolean;\n material: BabylonPointerMaterialLike | null;\n /** Higher groups draw later; the cursor uses 1 so it draws over the scene. */\n renderingGroupId?: number;\n dispose(): void;\n}\n\n/** What the app imports from `@babylonjs/core` and hands to the renderer. */\nexport interface BabylonPointerVisualsKit {\n MeshBuilder: {\n /** A cylinder along the Y axis, centred on the origin. */\n CreateCylinder(\n name: string,\n options: { height: number; diameter: number; tessellation?: number },\n scene: unknown,\n ): BabylonPointerMeshLike;\n /** A disc in the XY plane whose front face looks down -Z. */\n CreateDisc(name: string, options: { radius: number; tessellation?: number }, scene: unknown): BabylonPointerMeshLike;\n };\n StandardMaterial: new (name: string, scene: unknown) => BabylonPointerMaterialLike;\n Color3: new (r: number, g: number, b: number) => BabylonColor3Like;\n Quaternion: new (x: number, y: number, z: number, w: number) => BabylonQuaternionLike;\n}\n\nexport interface BabylonPointerVisualsOptions {\n /** The runtime whose drawings are applied. */\n runtime: InteractionRuntime;\n /** The Babylon constructors the renderer builds its meshes from. */\n kit: BabylonPointerVisualsKit;\n /** The scene the meshes are created in (handed to the kit as is). */\n scene: unknown;\n}\n\ninterface SourceMeshes {\n ray: BabylonPointerMeshLike;\n cursor: BabylonPointerMeshLike;\n}\n\ntype Vec3 = readonly [number, number, number];\n\n/** The unit vector along (x, y, z); +Z for a zero vector. */\nfunction unit(x: number, y: number, z: number): Vec3 {\n const length = Math.hypot(x, y, z);\n return length < 1e-6 ? [0, 0, 1] : [x / length, y / length, z / length];\n}\n\n/**\n * The quaternion turning the axis `from` (+Y or +Z) onto the unit vector `to`.\n * Antiparallel vectors turn half a circle about X, which is square to both axes.\n */\nfunction rotationBetween(from: Vec3, to: Vec3): [number, number, number, number] {\n const dot = from[0] * to[0] + from[1] * to[1] + from[2] * to[2];\n if (dot < -0.999999) return [1, 0, 0, 0];\n const x = from[1] * to[2] - from[2] * to[1];\n const y = from[2] * to[0] - from[0] * to[2];\n const z = from[0] * to[1] - from[1] * to[0];\n const w = 1 + dot;\n const length = Math.hypot(x, y, z, w);\n return [x / length, y / length, z / length, w / length];\n}\n\nfunction setColour(target: BabylonColor3Like, rgb: readonly [number, number, number]): void {\n target.r = rgb[0];\n target.g = rgb[1];\n target.b = rgb[2];\n}\n\nconst Y_AXIS: Vec3 = [0, 1, 0];\nconst Z_AXIS: Vec3 = [0, 0, 1];\n\nexport class BabylonPointerVisuals {\n private readonly runtime: InteractionRuntime;\n private readonly kit: BabylonPointerVisualsKit;\n private readonly scene: unknown;\n private readonly meshes = new Map<string, SourceMeshes>();\n private readonly unsubscribe: Unsubscribe;\n private disposed = false;\n\n constructor(options: BabylonPointerVisualsOptions) {\n this.runtime = options.runtime;\n this.kit = options.kit;\n this.scene = options.scene;\n this.unsubscribe = this.runtime.onPointerDrawing((drawings) => this.apply(drawings));\n }\n\n /** The meshes drawn for one source, for a test or an app that restyles them. */\n meshesFor(sourceId: string): SourceMeshes | undefined {\n return this.meshes.get(sourceId);\n }\n\n /** Apply one frame's drawings: create meshes for new sources, hide those of sources gone. */\n apply(drawings: readonly PointerDrawing[]): void {\n if (this.disposed) return;\n const seen = new Set<string>();\n for (const drawing of drawings) {\n seen.add(drawing.sourceId);\n const meshes = this.meshes.get(drawing.sourceId) ?? this.create(drawing.sourceId);\n this.applyRay(meshes.ray, drawing);\n this.applyCursor(meshes.cursor, drawing);\n }\n for (const [id, meshes] of this.meshes) {\n if (!seen.has(id)) {\n meshes.ray.isVisible = false;\n meshes.cursor.isVisible = false;\n }\n }\n }\n\n private create(sourceId: string): SourceMeshes {\n const ray = this.kit.MeshBuilder.CreateCylinder(\n `pointer-ray-${sourceId}`,\n { height: 1, diameter: 1, tessellation: 6 },\n this.scene,\n );\n ray.material = this.material(`pointer-ray-${sourceId}-material`);\n ray.isVisible = false;\n const cursor = this.kit.MeshBuilder.CreateDisc(`pointer-cursor-${sourceId}`, { radius: 1, tessellation: 32 }, this.scene);\n cursor.material = this.material(`pointer-cursor-${sourceId}-material`);\n cursor.renderingGroupId = 1;\n cursor.isVisible = false;\n const meshes = { ray, cursor };\n this.meshes.set(sourceId, meshes);\n return meshes;\n }\n\n private material(name: string): BabylonPointerMaterialLike {\n const material = new this.kit.StandardMaterial(name, this.scene);\n material.emissiveColor = new this.kit.Color3(1, 1, 1);\n material.diffuseColor = new this.kit.Color3(1, 1, 1);\n material.disableLighting = true;\n material.alpha = 1;\n return material;\n }\n\n private applyRay(mesh: BabylonPointerMeshLike, drawing: PointerDrawing): void {\n const ray = this.runtime.getSource(drawing.sourceId)?.ray;\n const length = drawing.rayTo - drawing.rayFrom;\n if (!drawing.ray || !ray || length <= 0) {\n mesh.isVisible = false;\n return;\n }\n const dir = unit(ray.direction[0], ray.direction[1], ray.direction[2]);\n const centre = (drawing.rayFrom + drawing.rayTo) / 2;\n mesh.position.x = ray.origin[0] + dir[0] * centre;\n mesh.position.y = ray.origin[1] + dir[1] * centre;\n mesh.position.z = ray.origin[2] + dir[2] * centre;\n mesh.scaling.x = drawing.rayRadius * 2;\n mesh.scaling.y = length;\n mesh.scaling.z = drawing.rayRadius * 2;\n const q = rotationBetween(Y_AXIS, dir);\n mesh.rotationQuaternion = new this.kit.Quaternion(q[0], q[1], q[2], q[3]);\n const material = mesh.material!;\n setColour(material.emissiveColor, drawing.rayColor);\n setColour(material.diffuseColor, drawing.rayColor);\n material.alpha = 1;\n mesh.isVisible = true;\n }\n\n private applyCursor(mesh: BabylonPointerMeshLike, drawing: PointerDrawing): void {\n if (!drawing.cursor || !drawing.cursorPoint) {\n mesh.isVisible = false;\n return;\n }\n const point = drawing.cursorPoint;\n const source = this.runtime.getSource(drawing.sourceId);\n // Pointing direction: along the ray for a ray hit; from the head for a near hit.\n let dir: Vec3;\n if (source?.ray && drawing.ray) {\n dir = unit(source.ray.direction[0], source.ray.direction[1], source.ray.direction[2]);\n } else {\n const head = this.runtime.getProvider().getHeadPose?.()?.position;\n dir = head ? unit(point[0] - head[0], point[1] - head[1], point[2] - head[2]) : Z_AXIS;\n }\n // The disc's front face looks down -Z, so turning +Z onto the pointing\n // direction makes it face back at the viewer. It sits `cursorOffset` toward the viewer.\n const q = rotationBetween(Z_AXIS, dir);\n mesh.rotationQuaternion = new this.kit.Quaternion(q[0], q[1], q[2], q[3]);\n mesh.position.x = point[0] - dir[0] * drawing.cursorOffset;\n mesh.position.y = point[1] - dir[1] * drawing.cursorOffset;\n mesh.position.z = point[2] - dir[2] * drawing.cursorOffset;\n mesh.scaling.x = drawing.cursorRadius;\n mesh.scaling.y = drawing.cursorRadius;\n mesh.scaling.z = drawing.cursorRadius;\n mesh.material!.alpha = drawing.cursorOpacity;\n mesh.isVisible = true;\n }\n\n dispose(): void {\n if (this.disposed) return;\n this.disposed = true;\n this.unsubscribe();\n for (const { ray, cursor } of this.meshes.values()) {\n for (const mesh of [ray, cursor]) {\n mesh.material!.dispose();\n mesh.dispose();\n }\n }\n this.meshes.clear();\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@realitycollective/babylon-interactions",
3
- "version": "0.1.1-preview.3",
3
+ "version": "0.1.1",
4
4
  "description": "Babylon.js adapter for the Reality Collective Interaction Extensions - maps a Babylon WebXR experience (WebXRDefaultExperience controllers, motion controller components, hand-tracking joints) into the engine-free @realitycollective/webxr-interactions core, with a scene pointer fallback on desktop. Structurally typed: no @babylonjs/core import. Re-exports the core.",
5
5
  "keywords": [
6
6
  "realitycollective",
@@ -10,7 +10,10 @@
10
10
  "xr",
11
11
  "interaction",
12
12
  "input",
13
- "typescript"
13
+ "typescript",
14
+ "physics",
15
+ "pointer",
16
+ "eye-gaze"
14
17
  ],
15
18
  "license": "MIT",
16
19
  "author": "Reality Collective",
@@ -33,8 +36,8 @@
33
36
  "typecheck": "tsc -p tsconfig.json --noEmit"
34
37
  },
35
38
  "dependencies": {
36
- "@realitycollective/webxr-input": "^0.1.7",
37
- "@realitycollective/webxr-interactions": "^0.1.1-preview.3"
39
+ "@realitycollective/webxr-input": "^0.1.8",
40
+ "@realitycollective/webxr-interactions": "^0.1.1"
38
41
  },
39
42
  "repository": {
40
43
  "type": "git",