@realitycollective/xrblocks-uiextensions 0.1.1-preview.2 → 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,21 @@ Change log for the Reality Collective WebXR UI Extensions packages. All five pac
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
+ - Pointers for an app with windows and no interactables. IWSDK draws every hand's ray and cursor itself, whichever packages an app uses; a native host draws only what it is told, and nothing told it, so a native app with windows alone showed no ray and no cursor and could not be aimed.
12
+ - `@realitycollective/webxr-uiextensions` - `PanelPointerOffers.present(sourceKey, hasRay, selecting, display)` and `PresentedPointer`: the drawing for one source, the arbiter's visuals through the app's `PointerDisplay` settings, with what the cursor sits on. The rule it states: whoever owns the arbiter presents every source. With a shared arbiter that is the Interactions runtime; with its own, the window host.
13
+ - `@realitycollective/native-uiextensions` - `NativeWindowHost` presents while it owns the arbiter: once per frame for every source that reported a pointer sample it calls the `input` slice's `applyPointerVisuals`, and once more with nothing to draw when a source stops reporting. New option `pointerDisplay` (the settings, or a `PointerDisplay` the app keeps), `getPointerDisplay()`, and the optional `applyPointerVisuals` and `applyPointerDisplay` members on `NativeUiInputHost`, the same members `@realitycollective/native-interactions` defines on that slice. With `pointers` given nothing changes: the Interactions binding presents.
14
+ - `@realitycollective/webxr-uiextensions` - `UIX_CONTROL_TAGS`, `UIX_ROLE_TAGS` and `UIX_ELEMENT_TAGS`: the `<uix-*>` element vocabulary a panel loader must accept, published by the core. It was declared twice, privately, in the core's upgrader and in the IWSDK binding's component set; `uixComponentSet` now reads it, and the native contract states what a host's panel loader (a build-time cook or a run-time parser) must do with each tag.
15
+ - Native test harness (`harness/native`): a native app project for the WebXR-to-native conversion pipeline that runs `nativeUiHostConformanceCases()`, `windowHostContractCases()`, `sceneTargetContractCases()` and `uixElementContractCases()` against a native host's test slices (or this repository's reference fakes under Node), and plays the showcase scene over the live `__rcHost` with every window and control event logged as a JSON line. `npm run harness:ui` fills its panel folder from the showcase and the core's window chrome; `npm run harness:compile` typechecks it, bundles it through the engine and browser-global gates and compiles Hermes bytecode. CI compiles it and never runs it.
16
+ - Native parity round (Pale Signal handover of 29 September 2026, G4 and G7): one pointer decision per source across UI panels AND interactables, as Meta IWSDK's `MultiPointer` runs over every pointer-event object.
17
+ - `@realitycollective/webxr-uiextensions` - `PanelPointerOffers` (`core/pointer-offers.ts`, `UIX_POINTER_SET`, `PANEL_GRAB_RADIUS`): a window host registers a `"panel"` target set with the `PointerArbiter` from `@realitycollective/webxr-input`, offers per source and per pointer kind the nearest panel its ray, fingertip or grip reaches, and hovers, presses, touches and drags with a pointer kind only while that kind owns the source with the host's candidate. A host used on its own gets an arbiter of its own and behaves as before. Keys are the source's handedness or the input provider's id; the Interactions runtime aliases one to the other.
18
+ - `@realitycollective/native-uiextensions` - `NativeWindowHost` takes `pointers`; every `onPointerSample` is offered (a new optional `NativePointerSample.distance` is the distance IWSDK's pointer of that kind compares, else derived), the press machines run only while the pointer owns its source, and the panel cursor now reaches the host through `@realitycollective/native-interactions`' `applyPointerVisuals` (`targetKind: "panel"`), so a host stopgap for panel cursors is no longer needed. `showKeyboard` states what a host that cannot observe the keyboard's dismissal (an Android `NativeActivity`) reports as `"keyboardclosed"`: its own `hideKeyboard`, or a key event that ends entry.
19
+ - `@realitycollective/threejs-uiextensions` - `connectUIExtensions({ pointers })` and `connectWebXrPointerInput({ pointers })`: each slot's ray hit, fingertip panel and grip panel (within `PANEL_GRAB_RADIUS`) are offered under the session input source's handedness before hover, press, squeeze and touch act, and with a shared arbiter this host draws no cursor of its own (the Interactions binding draws every cursor, panels included). `ScenePointerBridge.rootOf` and `isTouchPressed` support it.
20
+ - `@realitycollective/xrblocks-uiextensions` - `connectUIExtensions({ pointers })` and `XrBlocksPointerBridge({ pointers })`: touch, grab and ray candidates are offered (the ray from this binding's own raycast of the wired panels each frame), presses and touches are gated on ownership, and hover is per pointer from that raycast rather than XR Blocks' per-element `onHoverEnter`/`onHoverExit` (which stay as no-ops), closing the "XR Blocks hover is not per pointer" item.
21
+ - `@realitycollective/iwsdk-uiextensions` - `registerUIExtensions(world, { pointers })` registers `UIPointerOfferSystem` (priority -3.95, after IWSDK's InputSystem), which offers each hand's touch, grab and ray intersections on this world's panels to the shared arbiter; IWSDK arbitrates its own pointers, so nothing is gated there, and the core's published visuals name panels on IWSDK as on every other platform. `pointerOffersFor(world)` reaches the offers.
11
22
  - `@realitycollective/webxr-uiextensions` - hover per element as one core decision (`core/hover.ts`, `HoverTracker`): each pointer raises `pointerenter` on the element it moves onto and `pointerleave` on the one it left, once per change, and an element's hover style holds while any pointer is over it, as IWSDK's pointer events drive uikit's `:hover`. The three.js binding raises hover from it, and the native binding decides hover from its pointer samples and tells the host through the new `NativeUiHost.setHover`; a `pointerenter` or `pointerleave` a native host raises itself is ignored.
12
23
  - `@realitycollective/webxr-uiextensions` - scrolling as a core rule (`core/scroll.ts`, `ScrollState`, `SCROLL_DEFAULTS`): uikit's drag, coast (0.9 damping per frame, stopping below 10 px/s), rubber band (100 px, 30 % return per frame) and wheel, restated so a native host only draws the offset it is handed (`NativeUiHost.setScroll`; a node declares `scroll` and a ray sample carries `localPoint`). Text entry as a core rule (`core/text-entry.ts`, `TextEntry`) over a platform keyboard facility (`TextInputFacility`): a click on a text field asks for the keyboard with the field's value, `multiline` and `type`; every reported value replaces the field's and raises `valueChanged`. The web default is uikit's `Input` over a hidden HTML input and the browser's keyboard; the native default is the system keyboard (`NativeUiHost.showKeyboard`/`hideKeyboard`, host events `input` and `keyboardclosed`). Kit cases: `ui/hover is decided by the binding...`, `ui/the host scrolls an element's content exactly as told`, `ui/the host shows its keyboard for a text field when told...`.
13
24
  - `@realitycollective/webxr-uiextensions` - `WindowManager.setFollow(id, options)` and `OpenWindowOptions.follow`: a following window's offset (in the viewer's yaw frame, metres), speed, dead zone and angle live on the `WindowRecord` (`follow`, `DEFAULT_WINDOW_FOLLOW` unless set) and can change after spawn; the `followChanged` event carries the previous values. Every binding steps the core follow rule from the record: the IWSDK dock system writes the live `Follower`, the three.js and native hosts re-aim at the new offset.
@@ -31,6 +42,8 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
31
42
 
32
43
  ### Changed
33
44
 
45
+ - Every package now requires `@realitycollective/webxr-input` at `^0.1.8-preview.0`: the shared pointer arbiter the hosts offer their panels to exists only from that release.
46
+
34
47
  - The reference is now IWSDK 1.0. `@realitycollective/iwsdk-uiextensions` peers on `@iwsdk/core >=1.0.0 <2.0.0` (was `>=0.5.0 <0.6.0`) and is built and tested against 1.0.0. IWSDK 1.0's FollowSystem honours the height of the follow offset, where 0.5 set a following window at head height, so the core `stepFollow` now does the same on every platform: a body-follow or head-locked window sits 0.15 m below eye level by default, and a following region 0.2 m below. `follow-reference.test.ts` proves the core equal to IWSDK 1.0's own FollowSystem frame by frame.
35
48
  - `@realitycollective/xrblocks-uiextensions` - follows by the core rule. `body-follow` and `head-locked` windows now move by `stepFollow`, the same lazy yaw follow IWSDK applies: a snap on entering the mode, the 0.35 m dead zone with a 30 degree angle limit, a lerp of `dt * speed`, and following from where the window was left when it is unpinned. Before this XR Blocks eased with its own exponential approach, had no angle limit or snap, and did not move a `head-locked` window at all. A following region now uses IWSDK's region defaults (speed 1, tolerance 0.4 m) rather than speed 3 and 0.2 m. A window with no title is titled with its id, as on every platform, and every panel's `data-uix` controls are upgraded automatically, as IWSDK's `UIControlsSystem` does (`controls: false` opts out). `follow-math.ts` stays exported and unchanged for existing callers.
36
49
  - **Breaking for native hosts:** `@realitycollective/native-uiextensions` now applies every UI rule itself, and a native host is handed results. Before this, the binding mirrored `WindowManager` records to the host and left the host to place windows, write chrome, run the touch press and decide clicks, and a native host that re-derived those rules drifted from the web on chrome, dock modes, touch press, click timing and more. Now the binding runs, from the core, what IWSDK's `UIWindowSystem`, `UIDockSystem`, `FollowSystem`, `UIDockRegionSystem`, `UIDragSystem`, `UITouchGuardSystem` and `UIControlsSystem` do. What a native host must absorb:
@@ -46,9 +59,12 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
46
59
  - `WindowManager` holds one rule for regions: a window in a region is always world-locked, because the region places it. Opening a window into a region, or `dockTo`, makes it world-locked whatever dock mode it asked for; `dockTo` emits `regionChanged` and then, if the mode changed, `dockChanged`. Setting a follow mode with `setDockMode` or `togglePin` takes a docked window out of its region first. The hosts disagreed before this: the XR Blocks host forced a docked window to world-locked on its own, while the IWSDK and native hosts left the manager's record in the requested follow mode. The same scene descriptor therefore laid out differently by platform. The XR Blocks host's own copy of the rule is gone. The native host now sends the native app the manager's dock mode for a new window, not the one the options asked for.
47
60
  - `@realitycollective/iwsdk-uiextensions` - `TouchPointerLike` (the parameter type of `sampleOf`) is exported, and `UITouchGuardSystem` uses the core's `Hand` type rather than a private duplicate. `@realitycollective/xrblocks-uiextensions` exports `UikitComponentLike` (`UixPanelDocument.rootComponent`) and `InteractiveLike` (the return type of `pickInteractive`); `@realitycollective/uix-devtools` exports `CompileOptions` (the options of `compilePanelSource`). Each was a module-private type that appeared in an exported signature, so a consumer had nothing to name and TypeDoc reported it as referenced but undocumented.
48
61
  - **Breaking:** `HandPoseSource.hasHands()` is gone. It existed only to let `@realitycollective/xrblocks-uiextensions` keep a hand-locked window shown, by body-follow, when no hand was tracked - a behaviour IWSDK and native never had. A hand-locked window with no tracked hand is now hidden on every platform, as `evaluateHandMenu` already decides: a platform never decides a behaviour the reference has not. `webxrHandPoseSource` no longer implements the method.
62
+ - 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.
49
63
 
50
64
  ### Fixed
51
65
 
66
+ - `@realitycollective/native-uiextensions` - a text field bound the way the web binds it, `setProperties({ onValueChange })`, never heard typed text on native: the binding handed the function to the host as element state and raised only a `valueChanged` event. A function-valued property now stays in the binding as a callback, only plain data crosses to the host, and the keyboard's text calls `onValueChange` as uikit's `Input` does, then raises `valueChanged`.
67
+ - `@realitycollective/xrblocks-uiextensions` - the host reads rays with `xb.input.getFrame()`, which xrblocks added in 0.20.0. Handed the input of an older xrblocks, the host now refuses it once at construction, naming the version it needs, instead of throwing from every frame's update. The multiplatform lab moves to xrblocks `^0.21.1` (it declared `^0.19.0`, whose input has no `getFrame`), and mounts XR Blocks' canvas inside `#scene-container` as its other two pipelines do.
52
68
  - `@realitycollective/native-uiextensions` - a window spawned without a title showed a blank title bar until a later record change. The plain options the host received defaulted the title to `''` while the record defaulted it to the window id. Both now use the id, and the binding writes it to `uix-title` itself.
53
69
  - `@realitycollective/native-uiextensions` - `data-uix` controls were never upgraded on native: nothing called `upgradePanel`, so steppers, toggles, expandable labels and log views rendered but did nothing. Every panel, window or bare, is now upgraded when it is built.
54
70
  - `@realitycollective/native-uiextensions` - the `data-uix-*` attributes of a native panel never reached the controls. `NativeElementNode` had no field for them, so on native every control ran on its defaults: a stepper lost its range, step and starting value, a toggle its labels and colours, and every control its `data-uix-id`, so `controls.stepper("count")` found nothing. `NativeElementNode` gains `attributes`, the element's `data-*` attributes as the markup wrote them, and the proxy lifts them into `userData` with `dataAttributeKey()`. The native app must send them.
@@ -106,7 +122,7 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
106
122
  - `@realitycollective/webxr-uiextensions` takes its geometry vocabulary from `@realitycollective/webxr-input` at `^0.1.1` rather than redeclaring it. `Vec3Tuple`, `QuatTuple`, `HeadPose`, `HeadPoseSource` and `PointerSample` are now that package's types, re-exported under the same names, so no import changes for a consumer. `PointerSample` is its `RayTuple`, which is what lets an input provider written against the shared contracts feed this contract unchanged. It is the core's only runtime dependency: the contracts package is engine-free and carries none of its own, and `test/architecture.test.ts` now allows exactly that one name and fails on any other.
107
123
  - `PointerSample` states its ownership rule: a delivered sample belongs to the listener and the source never writes to it again, so the core's hold-to-drag and drag maths may keep a press-time sample without copying. It mirrors the rule `@realitycollective/webxr-input` 0.1.3 writes on `InputSourceSnapshot`, so a provider feeding both contracts has one promise to keep.
108
124
 
109
- [0.1.1]: https://github.com/realitycollective/WebXR-UIExtensions/compare/v0.1.0...development
125
+ [0.1.1]: https://github.com/realitycollective/WebXR-UIExtensions/releases/tag/v0.1.1
110
126
  [0.1.0]: https://github.com/realitycollective/WebXR-UIExtensions/releases/tag/v0.1.0
111
127
 
112
128
  ### Fixed
package/README.md CHANGED
@@ -1,12 +1,18 @@
1
1
  # @realitycollective/xrblocks-uiextensions
2
2
 
3
- **EXPERIMENTAL** adapter for [Google XR Blocks](https://github.com/google/xrblocks) and plain three.js. It hosts the same [`@realitycollective/webxr-uiextensions`](../webxr-uiextensions/README.md) core as the IWSDK adapter, with the same UIKitML panels, window chrome and window manager, inside any three.js WebXR scene. An XR Blocks Script gives you exactly that kind of scene.
3
+ **EXPERIMENTAL** adapter for [Google XR Blocks](https://github.com/google/xrblocks). It builds on [`@realitycollective/threejs-uiextensions`](../threejs-uiextensions/README.md) and hosts the same [`@realitycollective/webxr-uiextensions`](../webxr-uiextensions/README.md) core as the IWSDK adapter, with the same UIKitML panels, window chrome and window manager, inside an XR Blocks Script's three.js scene. For plain three.js without XR Blocks, use `@realitycollective/threejs-uiextensions` directly.
4
4
 
5
- > **Maturity:** the IWSDK adapter is the most complete one, and this adapter is built to match it. It now has nearly all the same windowing features, and its desktop path is verified in a real browser (panels render, mouse clicks reach uikit controls, WASD/jump/ crouch move the camera). It has still had NO on-device pass on Android XR hardware - treat the XR Blocks path specifically as unverified.
5
+ ```sh
6
+ npm install @realitycollective/xrblocks-uiextensions three
7
+ ```
8
+
9
+ It re-exports everything from the core, so this is the only UI Extensions package your app needs. Peer dependency: `three >= 0.170.0`; `xrblocks` itself is not a dependency.
10
+
11
+ > **Maturity:** the IWSDK adapter is the most complete one, and this adapter is built to match it. It now has nearly all the same windowing features, and its desktop path is verified in a real browser (panels render, mouse clicks reach uikit controls, WASD, jump and crouch move the camera). It has still had NO on-device pass on Android XR hardware - treat the XR Blocks path specifically as unverified.
6
12
 
7
13
  ## Feature matrix vs the IWSDK adapter
8
14
 
9
- | Feature | IWSDK | XR Blocks / three.js (this package) |
15
+ | Feature | IWSDK | XR Blocks (this package) |
10
16
  | --- | --- | --- |
11
17
  | UIKitML panel hosting (runtime interpret, scale-to-fit) | ✅ | ✅ `UixPanelDocument` |
12
18
  | Window lifecycle + chrome (focus/PIN/DOCK/MIN/X, all opt-in, pin labels) | ✅ | ✅ `UixWindowHost` |
@@ -28,9 +34,14 @@
28
34
  | Focus bias (the focused window drawn nearer) | ✅ | ✅ `applyFocusBias`, every frame, for every window including a docked one - `regionSlotPose` re-places every docked window every frame, not only on a dock change |
29
35
  | Guarded poke (one press per touch, front only) | ✅ `UITouchGuardSystem` over IWSDK's touch pointers | ✅ the same core `TouchPress`, fed from `onObjectTouching` |
30
36
  | System keyboard text input | ✅ | ⬜ untested on Android XR |
37
+ | Shared pointers with the Interactions family | ✅ `UIPointerOfferSystem` | ✅ `connectUIExtensions({ pointers })`: touch, grab and ray candidates offered to the same `PointerArbiter` as `@realitycollective/xrblocks-interactions` |
38
+ | Hover per pointer | ✅ | ✅ the bridge raycasts panels itself; XR Blocks' per-element hover callbacks are not relied on |
39
+ | Scrolling (drag, coast, clamp) | ✅ uikit | ⬜ core `ScrollState` exists; no case yet drives uikit beside it |
31
40
 
32
41
  A ray drag rides the ray at a fixed grab distance, the same laser math IWSDK and native use, when `input: xb.input` is supplied to `connectUIExtensions` (or `rayInput` to the host directly) - `xb.input.getFrame()`'s `raySources` give the controller's live ray, since `SelectEvent` itself carries none. With no `rayInput` wired it falls back to the controller's own position delta instead: correct, but not laser-distance.
33
42
 
43
+ Requires `xrblocks` **0.20 or later** when `input: xb.input` is supplied: the host reads rays with `xb.input.getFrame()`, and refuses an older input once, at construction, by name. Verified against `xrblocks` 0.21.1.
44
+
34
45
  ## Required renderer setup (read this first)
35
46
 
36
47
  uikit draws panel backgrounds, borders and **text glyphs** all as transparent meshes, stacked by `renderOrder`. three.js sorts transparent objects by camera distance by default, which is meaningless for coplanar UI layers - at grazing angles or close range a panel background can sort in front of its own text and labels silently vanish. uikit also clips panel content with local clipping planes, which three.js ignores unless enabled.
@@ -76,7 +87,7 @@ Nothing here imports `xrblocks` at the type level - the glue binds to plain thre
76
87
 
77
88
  ### Window options and handles
78
89
 
79
- `createWindow` takes the portable `WindowOptionsBase` fields plus `config`, so an option means here what it means on the IWSDK adapter. Two notes specific to this host:
90
+ `createWindow` takes the portable `WindowOptionsBase` fields plus `config`, so an option means here what it means on the IWSDK adapter. Notes specific to this host:
80
91
 
81
92
  - `id` is optional. Omit it and the window is named `uix-window-<n>`.
82
93
  - `movable` (default `true`) gates the title-bar drag: `false` never wires a press listener onto the title bar at all.
@@ -99,7 +110,7 @@ handle.onReady((panel) => wire(panel)); // fires straight away here
99
110
 
100
111
  ## Known constraint: three versions
101
112
 
102
- `xrblocks@0.19` declares a peer of `three@^0.184`, while IWSDK mandates the `super-three@0.181` fork used workspace-wide. Vite resolves a single `three` per bundle so the pairing works in practice, but npm's peer check cannot express it - this workspace uses `legacy-peer-deps` (see the root `.npmrc`). Revisit when IWSDK's three catches up.
113
+ `xrblocks` declares a peer of `three@^0.184`, while IWSDK mandates the `super-three@0.181` fork used workspace-wide. Vite resolves a single `three` per bundle so the pairing works in practice, but npm's peer check cannot express it - this workspace uses `legacy-peer-deps` (see the root `.npmrc`). Revisit when IWSDK's three catches up.
103
114
 
104
115
  ## Testing
105
116
 
@@ -118,4 +129,4 @@ npm test # scale/follow/pointer math + a headless host lifecycle suite
118
129
 
119
130
  ## License
120
131
 
121
- MIT © Reality Collective
132
+ MIT - see [LICENSE](./LICENSE).
package/dist/host.d.ts CHANGED
@@ -14,7 +14,24 @@
14
14
  * does not need - its own interaction manager already resolves hit-testing.
15
15
  */
16
16
  import { UixWindowHost as ThreeJsWindowHost, type CreateRegionOptions, type CreateWindowOptions, type RegionHandle, type UixWindowHostOptions, type WindowHandle, type XrBlocksWindowHandle } from '@realitycollective/threejs-uiextensions';
17
+ import type { PointerArbiter } from '@realitycollective/webxr-input';
17
18
  export type { CreateRegionOptions, CreateWindowOptions, RegionHandle, UixWindowHostOptions, WindowHandle, XrBlocksWindowHandle };
19
+ /** Options for the XR Blocks {@link UixWindowHost}. */
20
+ export interface XrBlocksWindowHostOptions extends UixWindowHostOptions {
21
+ /**
22
+ * The app's shared pointer arbiter (the Interactions runtime's). Panels
23
+ * then offer their touch, grab and ray candidates to it and act only with
24
+ * a pointer kind it says owns the source. Omit it and the host arbitrates
25
+ * on its own.
26
+ */
27
+ pointers?: PointerArbiter;
28
+ }
18
29
  export declare class UixWindowHost extends ThreeJsWindowHost {
19
- constructor(options: UixWindowHostOptions);
30
+ /** The XR Blocks bridge, or `undefined` when the caller supplied its own `pointerBridgeFactory`. */
31
+ private readonly xrBridge;
32
+ /** `xb.input`, read each frame to raycast the panels along every ray source. */
33
+ private readonly xrRayInput;
34
+ constructor(options: XrBlocksWindowHostOptions);
35
+ /** Drive per-frame from the engine loop (delta in SECONDS): the bridge raycasts this frame's rays first, so hover and the arbiter are current when the windows step. */
36
+ update(deltaSeconds: number): void;
20
37
  }
package/dist/host.js CHANGED
@@ -16,14 +16,36 @@
16
16
  import { UixWindowHost as ThreeJsWindowHost, } from '@realitycollective/threejs-uiextensions';
17
17
  import { XrBlocksPointerBridge } from './pointer-bridge.js';
18
18
  export class UixWindowHost extends ThreeJsWindowHost {
19
+ /** The XR Blocks bridge, or `undefined` when the caller supplied its own `pointerBridgeFactory`. */
20
+ xrBridge;
21
+ /** `xb.input`, read each frame to raycast the panels along every ray source. */
22
+ xrRayInput;
19
23
  constructor(options) {
24
+ // xrblocks added `xb.input.getFrame()` in 0.20.0. Refuse an older input here, once, rather than
25
+ // throwing from every frame's update.
26
+ if (options.rayInput && typeof options.rayInput.getFrame !== 'function') {
27
+ throw new Error('@realitycollective/xrblocks-uiextensions needs xrblocks 0.20 or later: the rayInput it was handed has no getFrame()');
28
+ }
29
+ const built = {};
20
30
  super({
21
31
  ...options,
22
32
  // Only when the caller has not already supplied one (a bare
23
33
  // `UixWindowHost` still gets the XR Blocks bridge by default, which is
24
34
  // what makes this package's own class useful to construct directly).
25
- pointerBridgeFactory: options.pointerBridgeFactory ?? ((isLive) => new XrBlocksPointerBridge({ isLive })),
35
+ pointerBridgeFactory: options.pointerBridgeFactory ??
36
+ ((isLive) => {
37
+ built.bridge = new XrBlocksPointerBridge({ isLive, ...(options.pointers ? { pointers: options.pointers } : {}) });
38
+ return built.bridge;
39
+ }),
26
40
  });
41
+ this.xrBridge = built.bridge;
42
+ this.xrRayInput = options.rayInput;
43
+ }
44
+ /** Drive per-frame from the engine loop (delta in SECONDS): the bridge raycasts this frame's rays first, so hover and the arbiter are current when the windows step. */
45
+ update(deltaSeconds) {
46
+ if (this.xrBridge && this.xrRayInput)
47
+ this.xrBridge.updateRays(this.xrRayInput.getFrame());
48
+ super.update(deltaSeconds);
27
49
  }
28
50
  }
29
51
  //# sourceMappingURL=host.js.map
package/dist/host.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"host.js","sourceRoot":"","sources":["../src/host.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,EACL,aAAa,IAAI,iBAAiB,GAOnC,MAAM,yCAAyC,CAAC;AACjD,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAI5D,MAAM,OAAO,aAAc,SAAQ,iBAAiB;IAClD,YAAY,OAA6B;QACvC,KAAK,CAAC;YACJ,GAAG,OAAO;YACV,4DAA4D;YAC5D,uEAAuE;YACvE,qEAAqE;YACrE,oBAAoB,EAAE,OAAO,CAAC,oBAAoB,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,IAAI,qBAAqB,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC;SAC1G,CAAC,CAAC;IACL,CAAC;CACF","sourcesContent":["/**\n * UixWindowHost - the XR Blocks binding.\n *\n * Every window/panel/region rule (spawn, chrome, follow, dock regions, hand\n * menus, drag, focus bias) lives in\n * `@realitycollective/threejs-uiextensions`'s own `UixWindowHost`, which this\n * class extends unchanged - \"one behaviour, every platform\": XR Blocks IS a\n * three.js WebXR scene, so it runs the same host as the plain three.js\n * binding. The only thing added here is XR Blocks' own interaction surface:\n * `XrBlocksPointerBridge` (`pointer-bridge.ts`) turns its select/touch/grab/\n * hover callbacks into the pointer events and touch samples the shared host\n * already knows how to apply, in place of `threejs-uiextensions`'s own\n * WebXR-session raycasting bridge (`ScenePointerBridge`), which XR Blocks\n * does not need - its own interaction manager already resolves hit-testing.\n */\nimport {\n UixWindowHost as ThreeJsWindowHost,\n type CreateRegionOptions,\n type CreateWindowOptions,\n type RegionHandle,\n type UixWindowHostOptions,\n type WindowHandle,\n type XrBlocksWindowHandle,\n} from '@realitycollective/threejs-uiextensions';\nimport { XrBlocksPointerBridge } from './pointer-bridge.js';\n\nexport type { CreateRegionOptions, CreateWindowOptions, RegionHandle, UixWindowHostOptions, WindowHandle, XrBlocksWindowHandle };\n\nexport class UixWindowHost extends ThreeJsWindowHost {\n constructor(options: UixWindowHostOptions) {\n super({\n ...options,\n // Only when the caller has not already supplied one (a bare\n // `UixWindowHost` still gets the XR Blocks bridge by default, which is\n // what makes this package's own class useful to construct directly).\n pointerBridgeFactory: options.pointerBridgeFactory ?? ((isLive) => new XrBlocksPointerBridge({ isLive })),\n });\n }\n}\n"]}
1
+ {"version":3,"file":"host.js","sourceRoot":"","sources":["../src/host.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,EACL,aAAa,IAAI,iBAAiB,GAOnC,MAAM,yCAAyC,CAAC;AAEjD,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAgB5D,MAAM,OAAO,aAAc,SAAQ,iBAAiB;IAClD,oGAAoG;IACnF,QAAQ,CAAoC;IAC7D,gFAAgF;IAC/D,UAAU,CAAqC;IAEhE,YAAY,OAAkC;QAC5C,gGAAgG;QAChG,sCAAsC;QACtC,IAAI,OAAO,CAAC,QAAQ,IAAI,OAAQ,OAAO,CAAC,QAA4C,CAAC,QAAQ,KAAK,UAAU,EAAE,CAAC;YAC7G,MAAM,IAAI,KAAK,CAAC,qHAAqH,CAAC,CAAC;QACzI,CAAC;QACD,MAAM,KAAK,GAAuC,EAAE,CAAC;QACrD,KAAK,CAAC;YACJ,GAAG,OAAO;YACV,4DAA4D;YAC5D,uEAAuE;YACvE,qEAAqE;YACrE,oBAAoB,EAClB,OAAO,CAAC,oBAAoB;gBAC5B,CAAC,CAAC,MAAM,EAAE,EAAE;oBACV,KAAK,CAAC,MAAM,GAAG,IAAI,qBAAqB,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;oBAClH,OAAO,KAAK,CAAC,MAAM,CAAC;gBACtB,CAAC,CAAC;SACL,CAAC,CAAC;QACH,IAAI,CAAC,QAAQ,GAAG,KAAK,CAAC,MAAM,CAAC;QAC7B,IAAI,CAAC,UAAU,GAAG,OAAO,CAAC,QAAQ,CAAC;IACrC,CAAC;IAED,wKAAwK;IAC/J,MAAM,CAAC,YAAoB;QAClC,IAAI,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,UAAU;YAAE,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,EAAE,CAAC,CAAC;QAC3F,KAAK,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC;IAC7B,CAAC;CACF","sourcesContent":["/**\n * UixWindowHost - the XR Blocks binding.\n *\n * Every window/panel/region rule (spawn, chrome, follow, dock regions, hand\n * menus, drag, focus bias) lives in\n * `@realitycollective/threejs-uiextensions`'s own `UixWindowHost`, which this\n * class extends unchanged - \"one behaviour, every platform\": XR Blocks IS a\n * three.js WebXR scene, so it runs the same host as the plain three.js\n * binding. The only thing added here is XR Blocks' own interaction surface:\n * `XrBlocksPointerBridge` (`pointer-bridge.ts`) turns its select/touch/grab/\n * hover callbacks into the pointer events and touch samples the shared host\n * already knows how to apply, in place of `threejs-uiextensions`'s own\n * WebXR-session raycasting bridge (`ScenePointerBridge`), which XR Blocks\n * does not need - its own interaction manager already resolves hit-testing.\n */\nimport {\n UixWindowHost as ThreeJsWindowHost,\n type CreateRegionOptions,\n type CreateWindowOptions,\n type RegionHandle,\n type UixWindowHostOptions,\n type WindowHandle,\n type XrBlocksWindowHandle,\n} from '@realitycollective/threejs-uiextensions';\nimport type { PointerArbiter } from '@realitycollective/webxr-input';\nimport { XrBlocksPointerBridge } from './pointer-bridge.js';\nimport type { XrBlocksRayInputAccess } from './ray-input.js';\n\nexport type { CreateRegionOptions, CreateWindowOptions, RegionHandle, UixWindowHostOptions, WindowHandle, XrBlocksWindowHandle };\n\n/** Options for the XR Blocks {@link UixWindowHost}. */\nexport interface XrBlocksWindowHostOptions extends UixWindowHostOptions {\n /**\n * The app's shared pointer arbiter (the Interactions runtime's). Panels\n * then offer their touch, grab and ray candidates to it and act only with\n * a pointer kind it says owns the source. Omit it and the host arbitrates\n * on its own.\n */\n pointers?: PointerArbiter;\n}\n\nexport class UixWindowHost extends ThreeJsWindowHost {\n /** The XR Blocks bridge, or `undefined` when the caller supplied its own `pointerBridgeFactory`. */\n private readonly xrBridge: XrBlocksPointerBridge | undefined;\n /** `xb.input`, read each frame to raycast the panels along every ray source. */\n private readonly xrRayInput: XrBlocksRayInputAccess | undefined;\n\n constructor(options: XrBlocksWindowHostOptions) {\n // xrblocks added `xb.input.getFrame()` in 0.20.0. Refuse an older input here, once, rather than\n // throwing from every frame's update.\n if (options.rayInput && typeof (options.rayInput as Partial<XrBlocksRayInputAccess>).getFrame !== 'function') {\n throw new Error('@realitycollective/xrblocks-uiextensions needs xrblocks 0.20 or later: the rayInput it was handed has no getFrame()');\n }\n const built: { bridge?: XrBlocksPointerBridge } = {};\n super({\n ...options,\n // Only when the caller has not already supplied one (a bare\n // `UixWindowHost` still gets the XR Blocks bridge by default, which is\n // what makes this package's own class useful to construct directly).\n pointerBridgeFactory:\n options.pointerBridgeFactory ??\n ((isLive) => {\n built.bridge = new XrBlocksPointerBridge({ isLive, ...(options.pointers ? { pointers: options.pointers } : {}) });\n return built.bridge;\n }),\n });\n this.xrBridge = built.bridge;\n this.xrRayInput = options.rayInput;\n }\n\n /** Drive per-frame from the engine loop (delta in SECONDS): the bridge raycasts this frame's rays first, so hover and the arbiter are current when the windows step. */\n override update(deltaSeconds: number): void {\n if (this.xrBridge && this.xrRayInput) this.xrBridge.updateRays(this.xrRayInput.getFrame());\n super.update(deltaSeconds);\n }\n}\n"]}
@@ -32,12 +32,20 @@
32
32
  * `TouchPress` every frame exactly as native's touch pointers do, through
33
33
  * `dispatchTouchUpdate`.
34
34
  *
35
+ * Shared arbitration and hover. Each touch, grab and ray candidate is offered
36
+ * to the app's `PointerArbiter` (`PanelPointerOffers`), keyed by handedness
37
+ * (`handIndex` 0 is left), and a press acts only while its kind owns the
38
+ * source. Rays need a per-frame raycast of their own (`updateRays`), which
39
+ * also raises hover per source through the core `HoverTracker`.
40
+ *
35
41
  * A near (grab) press only ever begins on the title bar, exactly as native's
36
42
  * grab pointer: `onObjectGrabStart`/`End` do nothing unless the hit node is
37
43
  * inside the `uix-titlebar` element.
38
44
  */
39
45
  import { type Object3D } from 'three';
46
+ import type { PointerArbiter } from '@realitycollective/webxr-input';
40
47
  import { type TouchPressOptions, type Vec3Tuple } from '@realitycollective/webxr-uiextensions';
48
+ import type { XBInputFrameLike } from './ray-input.js';
41
49
  /** The slice of an XR Blocks `Controller` this bridge reads a live ray pose from. Structural - no dependency on the `xrblocks` package. */
42
50
  export type XrBlocksController = Object3D;
43
51
  /** `xrblocks.d.ts`'s `SelectEvent`. */
@@ -63,6 +71,13 @@ export interface XrBlocksPointerBridgeOptions {
63
71
  isLive?(element: Object3D): boolean;
64
72
  /** Touch press thresholds. Default `DEFAULT_TOUCH_PRESS`. */
65
73
  touchOptions?: Partial<TouchPressOptions>;
74
+ /**
75
+ * The app's shared pointer arbiter (the Interactions runtime's). The bridge
76
+ * offers its touch, grab and ray candidates to it and acts only with a
77
+ * pointer kind the arbiter says owns the source. Omit it and the bridge
78
+ * makes its own, which owns every source it offers for.
79
+ */
80
+ pointers?: PointerArbiter;
66
81
  }
67
82
  /** This object's current WORLD position, as a plain tuple. */
68
83
  export declare function worldPositionOf(object: Object3D): Vec3Tuple;
@@ -72,14 +87,56 @@ export declare class XrBlocksPointerBridge {
72
87
  private readonly touches;
73
88
  private readonly touchOptions;
74
89
  private readonly isLive;
90
+ private readonly offers;
91
+ private readonly hover;
92
+ private readonly roots;
93
+ /** Every wired element -> the panel root it was wired under, for the arbiter's panel id. */
94
+ private readonly rootOf;
95
+ private readonly raycaster;
96
+ /** Element -> the source key its select press was granted to, so the release ends the SAME press. */
97
+ private readonly selects;
98
+ /** Element -> the source key its grab was granted to. */
99
+ private readonly grabs;
100
+ /** The ray sources of the last {@link updateRays}, in order: a controller with no handedness is keyed by its position here. */
101
+ private raySources;
102
+ /** Source keys the last {@link updateRays} offered a ray for, so a source that goes away is forgotten. */
103
+ private rayKeys;
75
104
  constructor(options?: XrBlocksPointerBridgeOptions);
76
105
  /**
77
106
  * Attach every callback this bridge understands to `root` and every
78
107
  * descendant, recursively. Call once per panel, right after its
79
- * `data-uix` controls are upgraded (`upgradePanel`).
108
+ * `data-uix` controls are upgraded (`upgradePanel`). The root also joins the
109
+ * set {@link updateRays} raycasts.
80
110
  */
81
111
  wire(root: Object3D): void;
112
+ /** Stop raycasting `root` (its window closed). Its callbacks stay attached and harmless. */
113
+ unwire(root: Object3D): void;
114
+ /**
115
+ * Once per frame, with `xb.input.getFrame()`: raycast every wired panel
116
+ * along each ray source, offer the nearest hit to the shared arbiter as that
117
+ * source's ray candidate, resolve the source, and raise `pointerenter` /
118
+ * `pointerleave` for what each source's ray now owns. XR Blocks' own
119
+ * `onHoverEnter` / `onHoverExit` are not used: they are not per pointer and
120
+ * ignore the arbiter.
121
+ */
122
+ updateRays(frame: XBInputFrameLike): void;
82
123
  private attach;
124
+ /**
125
+ * Whether the ray of the controller in `key` may press `element`. When
126
+ * {@link updateRays} has offered for this source, its decision stands.
127
+ * Otherwise (no ray input wired) XR Blocks' own hit on `element` is offered
128
+ * here, at the controller's distance, so a host without ray input still
129
+ * presses.
130
+ */
131
+ private claimRay;
132
+ /** The arbiter's name for a controller: its handedness, else `controller:<position in the ray sources>`. */
133
+ private controllerKey;
134
+ /** The panel id for the arbiter: the uuid of the wired root `element` belongs to. */
135
+ private panelIdOf;
136
+ /** The nearest hit on any live wired panel along a world ray. */
137
+ private rayCast;
138
+ /** One pointer's hover target this frame: `pointerleave` on what it left, `pointerenter` on what it reached (live elements only). */
139
+ private setHover;
83
140
  /** Whether `element` sits on, or inside, a window's title bar - the only thing a grab pointer can hold. */
84
141
  private isOnTitlebar;
85
142
  private touch;
@@ -32,12 +32,18 @@
32
32
  * `TouchPress` every frame exactly as native's touch pointers do, through
33
33
  * `dispatchTouchUpdate`.
34
34
  *
35
+ * Shared arbitration and hover. Each touch, grab and ray candidate is offered
36
+ * to the app's `PointerArbiter` (`PanelPointerOffers`), keyed by handedness
37
+ * (`handIndex` 0 is left), and a press acts only while its kind owns the
38
+ * source. Rays need a per-frame raycast of their own (`updateRays`), which
39
+ * also raises hover per source through the core `HoverTracker`.
40
+ *
35
41
  * A near (grab) press only ever begins on the title bar, exactly as native's
36
42
  * grab pointer: `onObjectGrabStart`/`End` do nothing unless the hit node is
37
43
  * inside the `uix-titlebar` element.
38
44
  */
39
- import { Quaternion, Vector3 } from 'three';
40
- import { TouchPress, WINDOW_CHROME_IDS, dispatchTouchUpdate, resolveTouchPress, } from '@realitycollective/webxr-uiextensions';
45
+ import { Quaternion, Raycaster, Vector3 } from 'three';
46
+ import { HoverTracker, PanelPointerOffers, TouchPress, WINDOW_CHROME_IDS, dispatchTouchUpdate, resolveTouchPress, } from '@realitycollective/webxr-uiextensions';
41
47
  const scratchPosition = new Vector3();
42
48
  const scratchQuaternion = new Quaternion();
43
49
  /** This object's current WORLD position, as a plain tuple. */
@@ -60,31 +66,108 @@ function signedDistanceToPlane(element, point) {
60
66
  const dz = point[2] - position[2];
61
67
  return dx * normal[0] + dy * normal[1] + dz * normal[2];
62
68
  }
69
+ const scratchOrigin = new Vector3();
70
+ const scratchDirection = new Vector3();
71
+ /** `left` or `right` from a handedness string, else `undefined`. */
72
+ function sideOf(handedness) {
73
+ return handedness === 'left' || handedness === 'right' ? handedness : undefined;
74
+ }
75
+ /** XR Blocks' `handIndex`: 0 is the left hand, anything else the right (the mapping `xrblocks-interactions` uses). */
76
+ function handKey(handIndex) {
77
+ return handIndex === 0 ? 'left' : 'right';
78
+ }
79
+ /** Metres between two world points. */
80
+ function distanceBetween(a, b) {
81
+ return Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]);
82
+ }
63
83
  export class XrBlocksPointerBridge {
64
84
  touches = new Map();
65
85
  touchOptions;
66
86
  isLive;
87
+ offers;
88
+ hover = new HoverTracker();
89
+ roots = new Set();
90
+ /** Every wired element -> the panel root it was wired under, for the arbiter's panel id. */
91
+ rootOf = new WeakMap();
92
+ raycaster = new Raycaster();
93
+ /** Element -> the source key its select press was granted to, so the release ends the SAME press. */
94
+ selects = new Map();
95
+ /** Element -> the source key its grab was granted to. */
96
+ grabs = new Map();
97
+ /** The ray sources of the last {@link updateRays}, in order: a controller with no handedness is keyed by its position here. */
98
+ raySources = [];
99
+ /** Source keys the last {@link updateRays} offered a ray for, so a source that goes away is forgotten. */
100
+ rayKeys = new Set();
67
101
  constructor(options = {}) {
68
102
  this.touchOptions = resolveTouchPress(options.touchOptions);
69
103
  this.isLive = options.isLive ?? (() => true);
104
+ this.offers = new PanelPointerOffers(options.pointers);
70
105
  }
71
106
  /**
72
107
  * Attach every callback this bridge understands to `root` and every
73
108
  * descendant, recursively. Call once per panel, right after its
74
- * `data-uix` controls are upgraded (`upgradePanel`).
109
+ * `data-uix` controls are upgraded (`upgradePanel`). The root also joins the
110
+ * set {@link updateRays} raycasts.
75
111
  */
76
112
  wire(root) {
77
- root.traverse((element) => this.attach(element));
113
+ this.roots.add(root);
114
+ root.traverse((element) => {
115
+ this.rootOf.set(element, root);
116
+ this.attach(element);
117
+ });
118
+ }
119
+ /** Stop raycasting `root` (its window closed). Its callbacks stay attached and harmless. */
120
+ unwire(root) {
121
+ this.roots.delete(root);
122
+ }
123
+ /**
124
+ * Once per frame, with `xb.input.getFrame()`: raycast every wired panel
125
+ * along each ray source, offer the nearest hit to the shared arbiter as that
126
+ * source's ray candidate, resolve the source, and raise `pointerenter` /
127
+ * `pointerleave` for what each source's ray now owns. XR Blocks' own
128
+ * `onHoverEnter` / `onHoverExit` are not used: they are not per pointer and
129
+ * ignore the arbiter.
130
+ */
131
+ updateRays(frame) {
132
+ this.raySources = frame.raySources;
133
+ const seen = new Set();
134
+ frame.raySources.forEach((source, index) => {
135
+ const key = this.controllerKey(source.controller, index);
136
+ seen.add(key);
137
+ const { origin, direction } = source.ray;
138
+ const hit = this.rayCast([origin.x, origin.y, origin.z], [direction.x, direction.y, direction.z]);
139
+ this.offers.offer(key, 'ray', hit ? { panelId: hit.root.uuid, point: hit.point, distance: hit.distance } : undefined);
140
+ this.offers.resolve(key);
141
+ this.setHover(`ray:${key}`, hit && this.offers.owns(key, 'ray') ? hit.target : undefined);
142
+ });
143
+ for (const key of this.rayKeys) {
144
+ if (seen.has(key))
145
+ continue;
146
+ this.setHover(`ray:${key}`, undefined);
147
+ this.offers.forget(key);
148
+ }
149
+ this.rayKeys = seen;
78
150
  }
79
151
  attach(element) {
80
152
  const bridge = this;
81
153
  Object.assign(element, {
82
154
  onSelectStart(event) {
83
- if (bridge.isLive(element))
155
+ if (!bridge.isLive(element))
156
+ return true;
157
+ const key = bridge.controllerKey(event.target);
158
+ if (bridge.claimRay(key, element, event.target)) {
159
+ bridge.selects.set(element, key);
160
+ bridge.offers.setSelecting(key, 'ray', true);
84
161
  bridge.bubble(element, 'pointerdown', { controller: event.target });
162
+ }
85
163
  return true;
86
164
  },
87
165
  onSelectEnd() {
166
+ const key = bridge.selects.get(element);
167
+ if (key === undefined)
168
+ return true;
169
+ bridge.selects.delete(element);
170
+ bridge.offers.setSelecting(key, 'ray', false);
88
171
  if (bridge.isLive(element)) {
89
172
  bridge.bubble(element, 'pointerup');
90
173
  bridge.bubble(element, 'click');
@@ -92,12 +175,27 @@ export class XrBlocksPointerBridge {
92
175
  return true;
93
176
  },
94
177
  onObjectGrabStart(event) {
95
- if (bridge.isLive(element) && bridge.isOnTitlebar(element)) {
178
+ if (!bridge.isLive(element))
179
+ return true;
180
+ const key = handKey(event.handIndex);
181
+ bridge.offers.offer(key, 'grab', { panelId: bridge.panelIdOf(element), point: worldPositionOf(event.hand), distance: 0 });
182
+ bridge.offers.resolve(key);
183
+ if (!bridge.offers.owns(key, 'grab'))
184
+ return true;
185
+ bridge.grabs.set(element, key);
186
+ bridge.offers.setSelecting(key, 'grab', true);
187
+ if (bridge.isOnTitlebar(element))
96
188
  bridge.bubble(element, 'pointerdown', { hand: event.hand });
97
- }
98
189
  return true;
99
190
  },
100
191
  onObjectGrabEnd() {
192
+ const key = bridge.grabs.get(element);
193
+ if (key === undefined)
194
+ return true;
195
+ bridge.grabs.delete(element);
196
+ bridge.offers.setSelecting(key, 'grab', false);
197
+ bridge.offers.offer(key, 'grab', undefined);
198
+ bridge.offers.resolve(key);
101
199
  if (bridge.isLive(element) && bridge.isOnTitlebar(element)) {
102
200
  bridge.bubble(element, 'pointerup');
103
201
  bridge.bubble(element, 'click');
@@ -105,27 +203,87 @@ export class XrBlocksPointerBridge {
105
203
  return true;
106
204
  },
107
205
  onObjectTouchEnd(event) {
206
+ const key = handKey(event.handIndex);
207
+ bridge.offers.offer(key, 'touch', undefined);
208
+ bridge.offers.resolve(key);
108
209
  bridge.touch(event.handIndex, undefined);
109
210
  return true;
110
211
  },
111
212
  onObjectTouching(event) {
112
213
  if (!bridge.isLive(element))
113
214
  return true;
215
+ const key = handKey(event.handIndex);
114
216
  const point = [event.touchPosition.x, event.touchPosition.y, event.touchPosition.z];
115
- bridge.touch(event.handIndex, { signedDistance: signedDistanceToPlane(element, point), target: element });
217
+ const signedDistance = signedDistanceToPlane(element, point);
218
+ bridge.offers.offer(key, 'touch', { panelId: bridge.panelIdOf(element), point, distance: Math.abs(signedDistance) });
219
+ bridge.offers.resolve(key);
220
+ bridge.touch(event.handIndex, bridge.offers.owns(key, 'touch') ? { signedDistance, target: element } : undefined);
116
221
  return true;
117
222
  },
223
+ // Hover is raised per pointer from `updateRays`' own raycast, through the
224
+ // core `HoverTracker`. XR Blocks' hover callbacks are one flag per
225
+ // object, not per pointer, and know nothing of the shared arbiter, so
226
+ // they do nothing here. They return true ("handled") so the SDK does
227
+ // not walk on to the ancestors.
118
228
  onHoverEnter() {
119
- if (bridge.isLive(element))
120
- element.dispatchEvent({ type: 'pointerenter' });
121
229
  return true;
122
230
  },
123
231
  onHoverExit() {
124
- element.dispatchEvent({ type: 'pointerleave' });
125
232
  return true;
126
233
  },
127
234
  });
128
235
  }
236
+ /**
237
+ * Whether the ray of the controller in `key` may press `element`. When
238
+ * {@link updateRays} has offered for this source, its decision stands.
239
+ * Otherwise (no ray input wired) XR Blocks' own hit on `element` is offered
240
+ * here, at the controller's distance, so a host without ray input still
241
+ * presses.
242
+ */
243
+ claimRay(key, element, controller) {
244
+ if (!this.rayKeys.has(key)) {
245
+ const point = worldPositionOf(element);
246
+ this.offers.offer(key, 'ray', { panelId: this.panelIdOf(element), point, distance: distanceBetween(worldPositionOf(controller), point) });
247
+ this.offers.resolve(key);
248
+ }
249
+ return this.offers.owns(key, 'ray');
250
+ }
251
+ /** The arbiter's name for a controller: its handedness, else `controller:<position in the ray sources>`. */
252
+ controllerKey(controller, knownIndex) {
253
+ const side = sideOf(controller.inputSource?.handedness);
254
+ if (side)
255
+ return side;
256
+ return `controller:${knownIndex ?? this.raySources.findIndex((source) => source.controller === controller)}`;
257
+ }
258
+ /** The panel id for the arbiter: the uuid of the wired root `element` belongs to. */
259
+ panelIdOf(element) {
260
+ return this.rootOf.get(element).uuid;
261
+ }
262
+ /** The nearest hit on any live wired panel along a world ray. */
263
+ rayCast(origin, direction) {
264
+ this.raycaster.set(scratchOrigin.set(...origin), scratchDirection.set(...direction).normalize());
265
+ this.raycaster.far = Infinity;
266
+ let nearest;
267
+ for (const root of this.roots) {
268
+ if (!this.isLive(root))
269
+ continue;
270
+ const hit = this.raycaster.intersectObject(root, true)[0];
271
+ if (hit && (!nearest || hit.distance < nearest.hit.distance))
272
+ nearest = { root, hit };
273
+ }
274
+ if (!nearest)
275
+ return undefined;
276
+ const { hit } = nearest;
277
+ return { root: nearest.root, target: hit.object, point: [hit.point.x, hit.point.y, hit.point.z], distance: hit.distance };
278
+ }
279
+ /** One pointer's hover target this frame: `pointerleave` on what it left, `pointerenter` on what it reached (live elements only). */
280
+ setHover(pointerId, target) {
281
+ const change = this.hover.update(pointerId, target);
282
+ if (change.pointerLeave)
283
+ change.pointerLeave.dispatchEvent({ type: 'pointerleave' });
284
+ if (change.pointerEnter && this.isLive(change.pointerEnter))
285
+ change.pointerEnter.dispatchEvent({ type: 'pointerenter' });
286
+ }
129
287
  /** Whether `element` sits on, or inside, a window's title bar - the only thing a grab pointer can hold. */
130
288
  isOnTitlebar(element) {
131
289
  for (let at = element; at; at = at.parent) {
@@ -141,6 +299,7 @@ export class XrBlocksPointerBridge {
141
299
  this.touches.set(handIndex, press);
142
300
  }
143
301
  const update = press.update(sample);
302
+ this.offers.setSelecting(handKey(handIndex), 'touch', press.held);
144
303
  dispatchTouchUpdate(update, { dispatch: (type, target) => this.bubble(target, type) }, (a, b) => a === b);
145
304
  }
146
305
  bubble(hit, type, extra = {}) {
@@ -1 +1 @@
1
- {"version":3,"file":"pointer-bridge.js","sourceRoot":"","sources":["../src/pointer-bridge.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * XrBlocksPointerBridge - turns XR Blocks' own per-object interaction\n * callbacks into the portable `pointerdown` / `pointerup` / `click` /\n * `pointerenter` / `pointerleave` events every `data-uix` control and window\n * chrome listener already reads (`UixElement.addEventListener`), and into\n * per-frame samples for the core `TouchPress`.\n *\n * XR Blocks 0.21.1 resolves hit-testing itself and calls `onSelectStart`,\n * `onSelectEnd`, `onObjectTouchStart` / `onObjectTouching` / `onObjectTouchEnd`,\n * `onObjectGrabStart` / `onObjectGrabEnd` and `onHoverEnter` / `onHoverExit`\n * directly on the object it decided is involved (see the `InteractionManager`\n * doc comments in the vendored `xrblocks.d.ts`), recursing to ancestors only\n * while a handler returns a falsy \"not handled\". Rather than rely on that\n * ancestor walk - documented only as \"recursively ... until handled\", with no\n * stated order or `typeof`-guard behaviour to build on - this bridge attaches\n * to every node once, at panel-creation time, and does its OWN bubbling from\n * the node the SDK actually calls, up through `Object3D.parent`, exactly as\n * `NativeWindowHost.dispatch` bubbles from a `NativeElementNode`. Every\n * attached handler returns `true`, so an ancestor the SDK also asks (it has\n * the same handler) is told the gesture is already handled and does nothing\n * further.\n *\n * Click timing. XR Blocks' select is itself edge-triggered - one\n * `onSelectStart` / `onSelectEnd` pair per gesture, evidently on the same\n * object throughout (the SDK's own manipulation manager decides the target\n * once, at select start) - so a press is `pointerdown` at select start and\n * `pointerup` + `click` at select end, with no separate edge-detection\n * needed. This is unlike native, whose samples arrive already resolved but\n * as a per-frame active flag the binding must itself edge-detect (see\n * `EdgePress`); XR Blocks needs no such state here. A touch (poke) IS a\n * continuous per-frame signal (`onObjectTouching`), so it drives the core\n * `TouchPress` every frame exactly as native's touch pointers do, through\n * `dispatchTouchUpdate`.\n *\n * A near (grab) press only ever begins on the title bar, exactly as native's\n * grab pointer: `onObjectGrabStart`/`End` do nothing unless the hit node is\n * inside the `uix-titlebar` element.\n */\nimport { Quaternion, Vector3, type Object3D } from 'three';\nimport {\n TouchPress,\n WINDOW_CHROME_IDS,\n dispatchTouchUpdate,\n resolveTouchPress,\n type PointerEventType,\n type TouchPressOptions,\n type Vec3Tuple,\n} from '@realitycollective/webxr-uiextensions';\n\n/** The slice of an XR Blocks `Controller` this bridge reads a live ray pose from. Structural - no dependency on the `xrblocks` package. */\nexport type XrBlocksController = Object3D;\n\n/** `xrblocks.d.ts`'s `SelectEvent`. */\nexport interface XrBlocksSelectEvent {\n target: XrBlocksController;\n}\n\n/** `xrblocks.d.ts`'s `ObjectTouchEvent`. */\nexport interface XrBlocksTouchEvent {\n handIndex: number;\n touchPosition: { x: number; y: number; z: number };\n}\n\n/** `xrblocks.d.ts`'s `ObjectGrabEvent`. */\nexport interface XrBlocksGrabEvent {\n handIndex: number;\n hand: Object3D;\n}\n\n/** A node dispatch reaches - loosely typed like `pointer-forward.ts`'s `InteractiveLike`, so no `Object3D` cast is needed at call sites. */\ninterface Dispatchable {\n dispatchEvent(event: { type: string; [key: string]: unknown }): void;\n parent: Dispatchable | null;\n}\n\nexport interface XrBlocksPointerBridgeOptions {\n /** Whether `element` should currently receive pointer events (a hidden or gated window says no). Omit to allow everything - a bare, unmanaged panel. */\n isLive?(element: Object3D): boolean;\n /** Touch press thresholds. Default `DEFAULT_TOUCH_PRESS`. */\n touchOptions?: Partial<TouchPressOptions>;\n}\n\nconst scratchPosition = new Vector3();\nconst scratchQuaternion = new Quaternion();\n\n/** This object's current WORLD position, as a plain tuple. */\nexport function worldPositionOf(object: Object3D): Vec3Tuple {\n object.getWorldPosition(scratchPosition);\n return [scratchPosition.x, scratchPosition.y, scratchPosition.z];\n}\n\n/** This object's local +Z axis, rotated to world space - its \"front\" normal, unit length. Used both for a touch's signed distance and a title-bar ray drag's grab plane. */\nexport function worldForwardOf(object: Object3D): Vec3Tuple {\n object.getWorldQuaternion(scratchQuaternion);\n const forward = new Vector3(0, 0, 1).applyQuaternion(scratchQuaternion);\n return [forward.x, forward.y, forward.z];\n}\n\n/** This object's current world position projected onto ITS OWN local +Z plane, signed distance to `point`, positive in front - the same convention `NativePointerSample.signedDistance` uses. */\nfunction signedDistanceToPlane(element: Object3D, point: Vec3Tuple): number {\n const position = worldPositionOf(element);\n const normal = worldForwardOf(element);\n const dx = point[0] - position[0];\n const dy = point[1] - position[1];\n const dz = point[2] - position[2];\n return dx * normal[0] + dy * normal[1] + dz * normal[2];\n}\n\nexport class XrBlocksPointerBridge {\n private readonly touches = new Map<number, TouchPress<Object3D>>();\n private readonly touchOptions: TouchPressOptions;\n private readonly isLive: (element: Object3D) => boolean;\n\n constructor(options: XrBlocksPointerBridgeOptions = {}) {\n this.touchOptions = resolveTouchPress(options.touchOptions);\n this.isLive = options.isLive ?? ((): boolean => true);\n }\n\n /**\n * Attach every callback this bridge understands to `root` and every\n * descendant, recursively. Call once per panel, right after its\n * `data-uix` controls are upgraded (`upgradePanel`).\n */\n wire(root: Object3D): void {\n root.traverse((element) => this.attach(element));\n }\n\n private attach(element: Object3D): void {\n const bridge = this;\n Object.assign(element, {\n onSelectStart(event: XrBlocksSelectEvent): boolean {\n if (bridge.isLive(element)) bridge.bubble(element, 'pointerdown', { controller: event.target });\n return true;\n },\n onSelectEnd(): boolean {\n if (bridge.isLive(element)) {\n bridge.bubble(element, 'pointerup');\n bridge.bubble(element, 'click');\n }\n return true;\n },\n onObjectGrabStart(event: XrBlocksGrabEvent): boolean {\n if (bridge.isLive(element) && bridge.isOnTitlebar(element)) {\n bridge.bubble(element, 'pointerdown', { hand: event.hand });\n }\n return true;\n },\n onObjectGrabEnd(): boolean {\n if (bridge.isLive(element) && bridge.isOnTitlebar(element)) {\n bridge.bubble(element, 'pointerup');\n bridge.bubble(element, 'click');\n }\n return true;\n },\n onObjectTouchEnd(event: XrBlocksTouchEvent): boolean {\n bridge.touch(event.handIndex, undefined);\n return true;\n },\n onObjectTouching(event: XrBlocksTouchEvent): boolean {\n if (!bridge.isLive(element)) return true;\n const point: Vec3Tuple = [event.touchPosition.x, event.touchPosition.y, event.touchPosition.z];\n bridge.touch(event.handIndex, { signedDistance: signedDistanceToPlane(element, point), target: element });\n return true;\n },\n onHoverEnter(): boolean {\n if (bridge.isLive(element)) (element as unknown as Dispatchable).dispatchEvent({ type: 'pointerenter' });\n return true;\n },\n onHoverExit(): boolean {\n (element as unknown as Dispatchable).dispatchEvent({ type: 'pointerleave' });\n return true;\n },\n });\n }\n\n /** Whether `element` sits on, or inside, a window's title bar - the only thing a grab pointer can hold. */\n private isOnTitlebar(element: Object3D): boolean {\n for (let at: Object3D | null = element; at; at = at.parent) {\n if (at.userData?.['id'] === WINDOW_CHROME_IDS.titlebar) return true;\n }\n return false;\n }\n\n private touch(handIndex: number, sample: { signedDistance: number; target: Object3D } | undefined): void {\n let press = this.touches.get(handIndex);\n if (!press) {\n press = new TouchPress<Object3D>(this.touchOptions);\n this.touches.set(handIndex, press);\n }\n const update = press.update(sample);\n dispatchTouchUpdate(\n update,\n { dispatch: (type, target) => this.bubble(target, type) },\n (a, b) => a === b,\n );\n }\n\n private bubble(hit: Object3D, type: PointerEventType, extra: Record<string, unknown> = {}): void {\n let stopped = false;\n const event = { type, ...extra, stopPropagation: () => (stopped = true) };\n for (let at: Dispatchable | null = hit as unknown as Dispatchable; at && !stopped; at = at.parent) {\n at.dispatchEvent(event);\n }\n }\n}\n"]}
1
+ {"version":3,"file":"pointer-bridge.js","sourceRoot":"","sources":["../src/pointer-bridge.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * XrBlocksPointerBridge - turns XR Blocks' own per-object interaction\n * callbacks into the portable `pointerdown` / `pointerup` / `click` /\n * `pointerenter` / `pointerleave` events every `data-uix` control and window\n * chrome listener already reads (`UixElement.addEventListener`), and into\n * per-frame samples for the core `TouchPress`.\n *\n * XR Blocks 0.21.1 resolves hit-testing itself and calls `onSelectStart`,\n * `onSelectEnd`, `onObjectTouchStart` / `onObjectTouching` / `onObjectTouchEnd`,\n * `onObjectGrabStart` / `onObjectGrabEnd` and `onHoverEnter` / `onHoverExit`\n * directly on the object it decided is involved (see the `InteractionManager`\n * doc comments in the vendored `xrblocks.d.ts`), recursing to ancestors only\n * while a handler returns a falsy \"not handled\". Rather than rely on that\n * ancestor walk - documented only as \"recursively ... until handled\", with no\n * stated order or `typeof`-guard behaviour to build on - this bridge attaches\n * to every node once, at panel-creation time, and does its OWN bubbling from\n * the node the SDK actually calls, up through `Object3D.parent`, exactly as\n * `NativeWindowHost.dispatch` bubbles from a `NativeElementNode`. Every\n * attached handler returns `true`, so an ancestor the SDK also asks (it has\n * the same handler) is told the gesture is already handled and does nothing\n * further.\n *\n * Click timing. XR Blocks' select is itself edge-triggered - one\n * `onSelectStart` / `onSelectEnd` pair per gesture, evidently on the same\n * object throughout (the SDK's own manipulation manager decides the target\n * once, at select start) - so a press is `pointerdown` at select start and\n * `pointerup` + `click` at select end, with no separate edge-detection\n * needed. This is unlike native, whose samples arrive already resolved but\n * as a per-frame active flag the binding must itself edge-detect (see\n * `EdgePress`); XR Blocks needs no such state here. A touch (poke) IS a\n * continuous per-frame signal (`onObjectTouching`), so it drives the core\n * `TouchPress` every frame exactly as native's touch pointers do, through\n * `dispatchTouchUpdate`.\n *\n * Shared arbitration and hover. Each touch, grab and ray candidate is offered\n * to the app's `PointerArbiter` (`PanelPointerOffers`), keyed by handedness\n * (`handIndex` 0 is left), and a press acts only while its kind owns the\n * source. Rays need a per-frame raycast of their own (`updateRays`), which\n * also raises hover per source through the core `HoverTracker`.\n *\n * A near (grab) press only ever begins on the title bar, exactly as native's\n * grab pointer: `onObjectGrabStart`/`End` do nothing unless the hit node is\n * inside the `uix-titlebar` element.\n */\nimport { Quaternion, Raycaster, Vector3, type Intersection, type Object3D } from 'three';\nimport type { PointerArbiter } from '@realitycollective/webxr-input';\nimport {\n HoverTracker,\n PanelPointerOffers,\n TouchPress,\n WINDOW_CHROME_IDS,\n dispatchTouchUpdate,\n resolveTouchPress,\n type PointerEventType,\n type TouchPressOptions,\n type Vec3Tuple,\n} from '@realitycollective/webxr-uiextensions';\nimport type { XBInputFrameLike } from './ray-input.js';\n\n/** The slice of an XR Blocks `Controller` this bridge reads a live ray pose from. Structural - no dependency on the `xrblocks` package. */\nexport type XrBlocksController = Object3D;\n\n/** `xrblocks.d.ts`'s `SelectEvent`. */\nexport interface XrBlocksSelectEvent {\n target: XrBlocksController;\n}\n\n/** `xrblocks.d.ts`'s `ObjectTouchEvent`. */\nexport interface XrBlocksTouchEvent {\n handIndex: number;\n touchPosition: { x: number; y: number; z: number };\n}\n\n/** `xrblocks.d.ts`'s `ObjectGrabEvent`. */\nexport interface XrBlocksGrabEvent {\n handIndex: number;\n hand: Object3D;\n}\n\n/** A node dispatch reaches - loosely typed like `pointer-forward.ts`'s `InteractiveLike`, so no `Object3D` cast is needed at call sites. */\ninterface Dispatchable {\n dispatchEvent(event: { type: string; [key: string]: unknown }): void;\n parent: Dispatchable | null;\n}\n\nexport interface XrBlocksPointerBridgeOptions {\n /** Whether `element` should currently receive pointer events (a hidden or gated window says no). Omit to allow everything - a bare, unmanaged panel. */\n isLive?(element: Object3D): boolean;\n /** Touch press thresholds. Default `DEFAULT_TOUCH_PRESS`. */\n touchOptions?: Partial<TouchPressOptions>;\n /**\n * The app's shared pointer arbiter (the Interactions runtime's). The bridge\n * offers its touch, grab and ray candidates to it and acts only with a\n * pointer kind the arbiter says owns the source. Omit it and the bridge\n * makes its own, which owns every source it offers for.\n */\n pointers?: PointerArbiter;\n}\n\nconst scratchPosition = new Vector3();\nconst scratchQuaternion = new Quaternion();\n\n/** This object's current WORLD position, as a plain tuple. */\nexport function worldPositionOf(object: Object3D): Vec3Tuple {\n object.getWorldPosition(scratchPosition);\n return [scratchPosition.x, scratchPosition.y, scratchPosition.z];\n}\n\n/** This object's local +Z axis, rotated to world space - its \"front\" normal, unit length. Used both for a touch's signed distance and a title-bar ray drag's grab plane. */\nexport function worldForwardOf(object: Object3D): Vec3Tuple {\n object.getWorldQuaternion(scratchQuaternion);\n const forward = new Vector3(0, 0, 1).applyQuaternion(scratchQuaternion);\n return [forward.x, forward.y, forward.z];\n}\n\n/** This object's current world position projected onto ITS OWN local +Z plane, signed distance to `point`, positive in front - the same convention `NativePointerSample.signedDistance` uses. */\nfunction signedDistanceToPlane(element: Object3D, point: Vec3Tuple): number {\n const position = worldPositionOf(element);\n const normal = worldForwardOf(element);\n const dx = point[0] - position[0];\n const dy = point[1] - position[1];\n const dz = point[2] - position[2];\n return dx * normal[0] + dy * normal[1] + dz * normal[2];\n}\n\nconst scratchOrigin = new Vector3();\nconst scratchDirection = new Vector3();\n\n/** `left` or `right` from a handedness string, else `undefined`. */\nfunction sideOf(handedness: unknown): 'left' | 'right' | undefined {\n return handedness === 'left' || handedness === 'right' ? handedness : undefined;\n}\n\n/** XR Blocks' `handIndex`: 0 is the left hand, anything else the right (the mapping `xrblocks-interactions` uses). */\nfunction handKey(handIndex: number): string {\n return handIndex === 0 ? 'left' : 'right';\n}\n\n/** Metres between two world points. */\nfunction distanceBetween(a: Vec3Tuple, b: Vec3Tuple): number {\n return Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]);\n}\n\nexport class XrBlocksPointerBridge {\n private readonly touches = new Map<number, TouchPress<Object3D>>();\n private readonly touchOptions: TouchPressOptions;\n private readonly isLive: (element: Object3D) => boolean;\n private readonly offers: PanelPointerOffers;\n private readonly hover = new HoverTracker<Object3D>();\n private readonly roots = new Set<Object3D>();\n /** Every wired element -> the panel root it was wired under, for the arbiter's panel id. */\n private readonly rootOf = new WeakMap<Object3D, Object3D>();\n private readonly raycaster = new Raycaster();\n /** Element -> the source key its select press was granted to, so the release ends the SAME press. */\n private readonly selects = new Map<Object3D, string>();\n /** Element -> the source key its grab was granted to. */\n private readonly grabs = new Map<Object3D, string>();\n /** The ray sources of the last {@link updateRays}, in order: a controller with no handedness is keyed by its position here. */\n private raySources: XBInputFrameLike['raySources'] = [];\n /** Source keys the last {@link updateRays} offered a ray for, so a source that goes away is forgotten. */\n private rayKeys = new Set<string>();\n\n constructor(options: XrBlocksPointerBridgeOptions = {}) {\n this.touchOptions = resolveTouchPress(options.touchOptions);\n this.isLive = options.isLive ?? ((): boolean => true);\n this.offers = new PanelPointerOffers(options.pointers);\n }\n\n /**\n * Attach every callback this bridge understands to `root` and every\n * descendant, recursively. Call once per panel, right after its\n * `data-uix` controls are upgraded (`upgradePanel`). The root also joins the\n * set {@link updateRays} raycasts.\n */\n wire(root: Object3D): void {\n this.roots.add(root);\n root.traverse((element) => {\n this.rootOf.set(element, root);\n this.attach(element);\n });\n }\n\n /** Stop raycasting `root` (its window closed). Its callbacks stay attached and harmless. */\n unwire(root: Object3D): void {\n this.roots.delete(root);\n }\n\n /**\n * Once per frame, with `xb.input.getFrame()`: raycast every wired panel\n * along each ray source, offer the nearest hit to the shared arbiter as that\n * source's ray candidate, resolve the source, and raise `pointerenter` /\n * `pointerleave` for what each source's ray now owns. XR Blocks' own\n * `onHoverEnter` / `onHoverExit` are not used: they are not per pointer and\n * ignore the arbiter.\n */\n updateRays(frame: XBInputFrameLike): void {\n this.raySources = frame.raySources;\n const seen = new Set<string>();\n frame.raySources.forEach((source, index) => {\n const key = this.controllerKey(source.controller, index);\n seen.add(key);\n const { origin, direction } = source.ray;\n const hit = this.rayCast([origin.x, origin.y, origin.z], [direction.x, direction.y, direction.z]);\n this.offers.offer(key, 'ray', hit ? { panelId: hit.root.uuid, point: hit.point, distance: hit.distance } : undefined);\n this.offers.resolve(key);\n this.setHover(`ray:${key}`, hit && this.offers.owns(key, 'ray') ? hit.target : undefined);\n });\n for (const key of this.rayKeys) {\n if (seen.has(key)) continue;\n this.setHover(`ray:${key}`, undefined);\n this.offers.forget(key);\n }\n this.rayKeys = seen;\n }\n\n private attach(element: Object3D): void {\n const bridge = this;\n Object.assign(element, {\n onSelectStart(event: XrBlocksSelectEvent): boolean {\n if (!bridge.isLive(element)) return true;\n const key = bridge.controllerKey(event.target);\n if (bridge.claimRay(key, element, event.target)) {\n bridge.selects.set(element, key);\n bridge.offers.setSelecting(key, 'ray', true);\n bridge.bubble(element, 'pointerdown', { controller: event.target });\n }\n return true;\n },\n onSelectEnd(): boolean {\n const key = bridge.selects.get(element);\n if (key === undefined) return true;\n bridge.selects.delete(element);\n bridge.offers.setSelecting(key, 'ray', false);\n if (bridge.isLive(element)) {\n bridge.bubble(element, 'pointerup');\n bridge.bubble(element, 'click');\n }\n return true;\n },\n onObjectGrabStart(event: XrBlocksGrabEvent): boolean {\n if (!bridge.isLive(element)) return true;\n const key = handKey(event.handIndex);\n bridge.offers.offer(key, 'grab', { panelId: bridge.panelIdOf(element), point: worldPositionOf(event.hand), distance: 0 });\n bridge.offers.resolve(key);\n if (!bridge.offers.owns(key, 'grab')) return true;\n bridge.grabs.set(element, key);\n bridge.offers.setSelecting(key, 'grab', true);\n if (bridge.isOnTitlebar(element)) bridge.bubble(element, 'pointerdown', { hand: event.hand });\n return true;\n },\n onObjectGrabEnd(): boolean {\n const key = bridge.grabs.get(element);\n if (key === undefined) return true;\n bridge.grabs.delete(element);\n bridge.offers.setSelecting(key, 'grab', false);\n bridge.offers.offer(key, 'grab', undefined);\n bridge.offers.resolve(key);\n if (bridge.isLive(element) && bridge.isOnTitlebar(element)) {\n bridge.bubble(element, 'pointerup');\n bridge.bubble(element, 'click');\n }\n return true;\n },\n onObjectTouchEnd(event: XrBlocksTouchEvent): boolean {\n const key = handKey(event.handIndex);\n bridge.offers.offer(key, 'touch', undefined);\n bridge.offers.resolve(key);\n bridge.touch(event.handIndex, undefined);\n return true;\n },\n onObjectTouching(event: XrBlocksTouchEvent): boolean {\n if (!bridge.isLive(element)) return true;\n const key = handKey(event.handIndex);\n const point: Vec3Tuple = [event.touchPosition.x, event.touchPosition.y, event.touchPosition.z];\n const signedDistance = signedDistanceToPlane(element, point);\n bridge.offers.offer(key, 'touch', { panelId: bridge.panelIdOf(element), point, distance: Math.abs(signedDistance) });\n bridge.offers.resolve(key);\n bridge.touch(event.handIndex, bridge.offers.owns(key, 'touch') ? { signedDistance, target: element } : undefined);\n return true;\n },\n // Hover is raised per pointer from `updateRays`' own raycast, through the\n // core `HoverTracker`. XR Blocks' hover callbacks are one flag per\n // object, not per pointer, and know nothing of the shared arbiter, so\n // they do nothing here. They return true (\"handled\") so the SDK does\n // not walk on to the ancestors.\n onHoverEnter(): boolean {\n return true;\n },\n onHoverExit(): boolean {\n return true;\n },\n });\n }\n\n /**\n * Whether the ray of the controller in `key` may press `element`. When\n * {@link updateRays} has offered for this source, its decision stands.\n * Otherwise (no ray input wired) XR Blocks' own hit on `element` is offered\n * here, at the controller's distance, so a host without ray input still\n * presses.\n */\n private claimRay(key: string, element: Object3D, controller: XrBlocksController): boolean {\n if (!this.rayKeys.has(key)) {\n const point = worldPositionOf(element);\n this.offers.offer(key, 'ray', { panelId: this.panelIdOf(element), point, distance: distanceBetween(worldPositionOf(controller), point) });\n this.offers.resolve(key);\n }\n return this.offers.owns(key, 'ray');\n }\n\n /** The arbiter's name for a controller: its handedness, else `controller:<position in the ray sources>`. */\n private controllerKey(controller: object, knownIndex?: number): string {\n const side = sideOf((controller as { inputSource?: { handedness?: unknown } }).inputSource?.handedness);\n if (side) return side;\n return `controller:${knownIndex ?? this.raySources.findIndex((source) => source.controller === controller)}`;\n }\n\n /** The panel id for the arbiter: the uuid of the wired root `element` belongs to. */\n private panelIdOf(element: Object3D): string {\n return this.rootOf.get(element)!.uuid;\n }\n\n /** The nearest hit on any live wired panel along a world ray. */\n private rayCast(origin: Vec3Tuple, direction: Vec3Tuple): { root: Object3D; target: Object3D; point: Vec3Tuple; distance: number } | undefined {\n this.raycaster.set(scratchOrigin.set(...origin), scratchDirection.set(...direction).normalize());\n this.raycaster.far = Infinity;\n let nearest: { root: Object3D; hit: Intersection<Object3D> } | undefined;\n for (const root of this.roots) {\n if (!this.isLive(root)) continue;\n const hit = this.raycaster.intersectObject(root, true)[0];\n if (hit && (!nearest || hit.distance < nearest.hit.distance)) nearest = { root, hit };\n }\n if (!nearest) return undefined;\n const { hit } = nearest;\n return { root: nearest.root, target: hit.object, point: [hit.point.x, hit.point.y, hit.point.z], distance: hit.distance };\n }\n\n /** One pointer's hover target this frame: `pointerleave` on what it left, `pointerenter` on what it reached (live elements only). */\n private setHover(pointerId: string, target: Object3D | undefined): void {\n const change = this.hover.update(pointerId, target);\n if (change.pointerLeave) (change.pointerLeave as unknown as Dispatchable).dispatchEvent({ type: 'pointerleave' });\n if (change.pointerEnter && this.isLive(change.pointerEnter)) (change.pointerEnter as unknown as Dispatchable).dispatchEvent({ type: 'pointerenter' });\n }\n\n /** Whether `element` sits on, or inside, a window's title bar - the only thing a grab pointer can hold. */\n private isOnTitlebar(element: Object3D): boolean {\n for (let at: Object3D | null = element; at; at = at.parent) {\n if (at.userData?.['id'] === WINDOW_CHROME_IDS.titlebar) return true;\n }\n return false;\n }\n\n private touch(handIndex: number, sample: { signedDistance: number; target: Object3D } | undefined): void {\n let press = this.touches.get(handIndex);\n if (!press) {\n press = new TouchPress<Object3D>(this.touchOptions);\n this.touches.set(handIndex, press);\n }\n const update = press.update(sample);\n this.offers.setSelecting(handKey(handIndex), 'touch', press.held);\n dispatchTouchUpdate(\n update,\n { dispatch: (type, target) => this.bubble(target, type) },\n (a, b) => a === b,\n );\n }\n\n private bubble(hit: Object3D, type: PointerEventType, extra: Record<string, unknown> = {}): void {\n let stopped = false;\n const event = { type, ...extra, stopPropagation: () => (stopped = true) };\n for (let at: Dispatchable | null = hit as unknown as Dispatchable; at && !stopped; at = at.parent) {\n at.dispatchEvent(event);\n }\n }\n}\n"]}
@@ -21,6 +21,7 @@
21
21
  import { type Object3D, type PerspectiveCamera } from 'three';
22
22
  import type { HandPoseSource, HeadPoseSource } from '@realitycollective/webxr-uiextensions';
23
23
  import type { Kit } from '@pmndrs/uikitml';
24
+ import type { PointerArbiter } from '@realitycollective/webxr-input';
24
25
  import { UixWindowHost } from './host.js';
25
26
  import type { XrBlocksRayInputAccess } from './ray-input.js';
26
27
  /** The slice of an XR Blocks Script / three.js app the host binds to. */
@@ -44,6 +45,12 @@ export interface EngineContext {
44
45
  * point-delta - see `pointer-bridge.ts`'s `beginTitlebarDrag`.
45
46
  */
46
47
  input?: XrBlocksRayInputAccess;
48
+ /**
49
+ * The app's shared pointer arbiter (the Interactions runtime's). Panels
50
+ * offer their touch, grab and ray candidates to it and act only with a
51
+ * pointer kind it says owns the source. Also needs `input` for rays.
52
+ */
53
+ pointers?: PointerArbiter;
47
54
  }
48
55
  /**
49
56
  * The slice of three's `WebXRManager` (`renderer.xr`) a hand pose source
package/dist/xrblocks.js CHANGED
@@ -78,6 +78,7 @@ export function connectUIExtensions(context) {
78
78
  ...(context.xr ? { handPose: webxrHandPoseSource(context.xr) } : {}),
79
79
  ...(context.kit ? { kit: context.kit } : {}),
80
80
  ...(context.input ? { rayInput: context.input } : {}),
81
+ ...(context.pointers ? { pointers: context.pointers } : {}),
81
82
  });
82
83
  }
83
84
  //# sourceMappingURL=xrblocks.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"xrblocks.js","sourceRoot":"","sources":["../src/xrblocks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,EAAE,UAAU,EAAE,OAAO,EAAyC,MAAM,OAAO,CAAC;AAGnF,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAoC1C;;;;;;GAMG;AACH,MAAM,UAAU,mBAAmB,CAAC,EAAoB;IACtD,OAAO;QACL,WAAW,CAAC,IAAI;YACd,MAAM,KAAK,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC;YAC5B,MAAM,cAAc,GAAG,EAAE,CAAC,iBAAiB,EAAE,CAAC;YAC9C,MAAM,OAAO,GAAG,EAAE,CAAC,UAAU,EAAE,CAAC;YAChC,IAAI,CAAC,KAAK,IAAI,CAAC,cAAc,IAAI,CAAC,OAAO,EAAE,CAAC;gBAC1C,OAAO,SAAS,CAAC;YACnB,CAAC;YACD,KAAK,MAAM,MAAM,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC;gBAC1C,IAAI,MAAM,CAAC,UAAU,KAAK,IAAI,EAAE,CAAC;oBAC/B,SAAS;gBACX,CAAC;gBACD,MAAM,KAAK,GAAG,MAAM,CAAC,SAAS,IAAI,MAAM,CAAC,cAAc,CAAC;gBACxD,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,EAAE,cAAc,CAAC,CAAC;gBAClD,IAAI,CAAC,IAAI,EAAE,CAAC;oBACV,OAAO,SAAS,CAAC;gBACnB,CAAC;gBACD,MAAM,EAAE,QAAQ,EAAE,WAAW,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC;gBACjD,OAAO;oBACL,QAAQ,EAAE,CAAC,QAAQ,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAC;oBAC9C,UAAU,EAAE,CAAC,WAAW,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC,CAAC;iBACzE,CAAC;YACJ,CAAC;YACD,OAAO,SAAS,CAAC;QACnB,CAAC;KACF,CAAC;AACJ,CAAC;AAED,kDAAkD;AAClD,MAAM,UAAU,oBAAoB,CAClC,MAA+B;IAE/B,MAAM,QAAQ,GAAG,IAAI,OAAO,EAAE,CAAC;IAC/B,MAAM,UAAU,GAAG,IAAI,UAAU,EAAE,CAAC;IACpC,OAAO;QACL,WAAW;YACT,MAAM,CAAC,gBAAgB,CAAC,QAAQ,CAAC,CAAC;YAClC,MAAM,CAAC,kBAAkB,CAAC,UAAU,CAAC,CAAC;YACtC,OAAO;gBACL,QAAQ,EAAE,CAAC,QAAQ,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAC;gBAC9C,UAAU,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC,CAAC;aACrE,CAAC;QACJ,CAAC;KACF,CAAC;AACJ,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,mBAAmB,CAAC,OAAsB;IACxD,OAAO,IAAI,aAAa,CAAC;QACvB,KAAK,EAAE,OAAO,CAAC,KAAK;QACpB,QAAQ,EAAE,oBAAoB,CAAC,OAAO,CAAC,MAAM,CAAC;QAC9C,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,mBAAmB,CAAC,OAAO,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACpE,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC5C,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACtD,CAAC,CAAC;AACL,CAAC","sourcesContent":["/**\n * XR Blocks glue.\n *\n * XR Blocks (https://github.com/google/xrblocks) is three.js-based: a\n * Script's `scene`/`camera` are ordinary three.js objects, and `update()`\n * runs per frame. This module binds a {@link UixWindowHost} to that shape\n * WITHOUT importing `xrblocks` (loose structural types), so the package\n * works identically in an XR Blocks Script and in any hand-rolled three.js\n * WebXR app.\n *\n * Typical XR Blocks usage:\n *\n * import * as xb from 'xrblocks';\n * import { connectUIExtensions } from '@realitycollective/xrblocks-uiextensions';\n *\n * class MyScript extends xb.Script {\n * init() { this.uix = connectUIExtensions({ scene: this, camera: xb.core.camera }); }\n * update() { this.uix.update(xb.core.timer.getDelta()); }\n * }\n */\nimport { Quaternion, Vector3, type Object3D, type PerspectiveCamera } from 'three';\nimport type { HandPoseSource, HeadPose, HeadPoseSource } from '@realitycollective/webxr-uiextensions';\nimport type { Kit } from '@pmndrs/uikitml';\nimport { UixWindowHost } from './host.js';\nimport type { XrBlocksRayInputAccess } from './ray-input.js';\n\n/** The slice of an XR Blocks Script / three.js app the host binds to. */\nexport interface EngineContext {\n /** Where windows are parented - an XR Blocks Script IS an Object3D. */\n scene: Object3D;\n /** The rendering camera; in XR the engine keeps it at the viewer pose. */\n camera: Pick<PerspectiveCamera, 'getWorldPosition' | 'getWorldQuaternion'>;\n /** Optional UIKitML component kit(s). */\n kit?: Kit;\n /**\n * `renderer.xr`, so hand menus can ride the tracked hands in a session.\n * Leave it out on a desktop; a hand-locked window is then hidden, the same\n * as it is anywhere with no hand tracked (see `HandPoseSource`).\n */\n xr?: WebXRFrameAccess;\n /**\n * `xb.input`, so a title-bar RAY drag rides the actual controller ray\n * (laser-distance math, as IWSDK and native) rather than the controller's\n * own position (point-delta). Leave it out and a ray drag falls back to\n * point-delta - see `pointer-bridge.ts`'s `beginTitlebarDrag`.\n */\n input?: XrBlocksRayInputAccess;\n}\n\n/**\n * The slice of three's `WebXRManager` (`renderer.xr`) a hand pose source\n * needs. Structural, so anything with the same three methods will do.\n */\nexport interface WebXRFrameAccess {\n getFrame(): XRFrame | null | undefined;\n getReferenceSpace(): XRReferenceSpace | null;\n getSession(): XRSession | null;\n}\n\n/**\n * HandPoseSource backed by a live WebXR session: each hand's pose is its\n * input source's `gripSpace` (a controller's grip, or the tracked hand),\n * falling back to the target ray space when a runtime gives a hand none.\n * Returns `undefined` for a hand with no input source this frame, or with no\n * frame at all (outside a session), so hand menus stay hidden there.\n */\nexport function webxrHandPoseSource(xr: WebXRFrameAccess): HandPoseSource {\n return {\n getHandPose(hand) {\n const frame = xr.getFrame();\n const referenceSpace = xr.getReferenceSpace();\n const session = xr.getSession();\n if (!frame || !referenceSpace || !session) {\n return undefined;\n }\n for (const source of session.inputSources) {\n if (source.handedness !== hand) {\n continue;\n }\n const space = source.gripSpace ?? source.targetRaySpace;\n const pose = frame.getPose(space, referenceSpace);\n if (!pose) {\n return undefined;\n }\n const { position, orientation } = pose.transform;\n return {\n position: [position.x, position.y, position.z],\n quaternion: [orientation.x, orientation.y, orientation.z, orientation.w],\n };\n }\n return undefined;\n },\n };\n}\n\n/** HeadPoseSource backed by a three.js camera. */\nexport function cameraHeadPoseSource(\n camera: EngineContext['camera'],\n): HeadPoseSource {\n const position = new Vector3();\n const quaternion = new Quaternion();\n return {\n getHeadPose(): HeadPose {\n camera.getWorldPosition(position);\n camera.getWorldQuaternion(quaternion);\n return {\n position: [position.x, position.y, position.z],\n quaternion: [quaternion.x, quaternion.y, quaternion.z, quaternion.w],\n };\n },\n };\n}\n\n/** Create a window host bound to an XR Blocks Script / three.js context. */\nexport function connectUIExtensions(context: EngineContext): UixWindowHost {\n return new UixWindowHost({\n scene: context.scene,\n headPose: cameraHeadPoseSource(context.camera),\n ...(context.xr ? { handPose: webxrHandPoseSource(context.xr) } : {}),\n ...(context.kit ? { kit: context.kit } : {}),\n ...(context.input ? { rayInput: context.input } : {}),\n });\n}\n"]}
1
+ {"version":3,"file":"xrblocks.js","sourceRoot":"","sources":["../src/xrblocks.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * XR Blocks glue.\n *\n * XR Blocks (https://github.com/google/xrblocks) is three.js-based: a\n * Script's `scene`/`camera` are ordinary three.js objects, and `update()`\n * runs per frame. This module binds a {@link UixWindowHost} to that shape\n * WITHOUT importing `xrblocks` (loose structural types), so the package\n * works identically in an XR Blocks Script and in any hand-rolled three.js\n * WebXR app.\n *\n * Typical XR Blocks usage:\n *\n * import * as xb from 'xrblocks';\n * import { connectUIExtensions } from '@realitycollective/xrblocks-uiextensions';\n *\n * class MyScript extends xb.Script {\n * init() { this.uix = connectUIExtensions({ scene: this, camera: xb.core.camera }); }\n * update() { this.uix.update(xb.core.timer.getDelta()); }\n * }\n */\nimport { Quaternion, Vector3, type Object3D, type PerspectiveCamera } from 'three';\nimport type { HandPoseSource, HeadPose, HeadPoseSource } from '@realitycollective/webxr-uiextensions';\nimport type { Kit } from '@pmndrs/uikitml';\nimport type { PointerArbiter } from '@realitycollective/webxr-input';\nimport { UixWindowHost } from './host.js';\nimport type { XrBlocksRayInputAccess } from './ray-input.js';\n\n/** The slice of an XR Blocks Script / three.js app the host binds to. */\nexport interface EngineContext {\n /** Where windows are parented - an XR Blocks Script IS an Object3D. */\n scene: Object3D;\n /** The rendering camera; in XR the engine keeps it at the viewer pose. */\n camera: Pick<PerspectiveCamera, 'getWorldPosition' | 'getWorldQuaternion'>;\n /** Optional UIKitML component kit(s). */\n kit?: Kit;\n /**\n * `renderer.xr`, so hand menus can ride the tracked hands in a session.\n * Leave it out on a desktop; a hand-locked window is then hidden, the same\n * as it is anywhere with no hand tracked (see `HandPoseSource`).\n */\n xr?: WebXRFrameAccess;\n /**\n * `xb.input`, so a title-bar RAY drag rides the actual controller ray\n * (laser-distance math, as IWSDK and native) rather than the controller's\n * own position (point-delta). Leave it out and a ray drag falls back to\n * point-delta - see `pointer-bridge.ts`'s `beginTitlebarDrag`.\n */\n input?: XrBlocksRayInputAccess;\n /**\n * The app's shared pointer arbiter (the Interactions runtime's). Panels\n * offer their touch, grab and ray candidates to it and act only with a\n * pointer kind it says owns the source. Also needs `input` for rays.\n */\n pointers?: PointerArbiter;\n}\n\n/**\n * The slice of three's `WebXRManager` (`renderer.xr`) a hand pose source\n * needs. Structural, so anything with the same three methods will do.\n */\nexport interface WebXRFrameAccess {\n getFrame(): XRFrame | null | undefined;\n getReferenceSpace(): XRReferenceSpace | null;\n getSession(): XRSession | null;\n}\n\n/**\n * HandPoseSource backed by a live WebXR session: each hand's pose is its\n * input source's `gripSpace` (a controller's grip, or the tracked hand),\n * falling back to the target ray space when a runtime gives a hand none.\n * Returns `undefined` for a hand with no input source this frame, or with no\n * frame at all (outside a session), so hand menus stay hidden there.\n */\nexport function webxrHandPoseSource(xr: WebXRFrameAccess): HandPoseSource {\n return {\n getHandPose(hand) {\n const frame = xr.getFrame();\n const referenceSpace = xr.getReferenceSpace();\n const session = xr.getSession();\n if (!frame || !referenceSpace || !session) {\n return undefined;\n }\n for (const source of session.inputSources) {\n if (source.handedness !== hand) {\n continue;\n }\n const space = source.gripSpace ?? source.targetRaySpace;\n const pose = frame.getPose(space, referenceSpace);\n if (!pose) {\n return undefined;\n }\n const { position, orientation } = pose.transform;\n return {\n position: [position.x, position.y, position.z],\n quaternion: [orientation.x, orientation.y, orientation.z, orientation.w],\n };\n }\n return undefined;\n },\n };\n}\n\n/** HeadPoseSource backed by a three.js camera. */\nexport function cameraHeadPoseSource(\n camera: EngineContext['camera'],\n): HeadPoseSource {\n const position = new Vector3();\n const quaternion = new Quaternion();\n return {\n getHeadPose(): HeadPose {\n camera.getWorldPosition(position);\n camera.getWorldQuaternion(quaternion);\n return {\n position: [position.x, position.y, position.z],\n quaternion: [quaternion.x, quaternion.y, quaternion.z, quaternion.w],\n };\n },\n };\n}\n\n/** Create a window host bound to an XR Blocks Script / three.js context. */\nexport function connectUIExtensions(context: EngineContext): UixWindowHost {\n return new UixWindowHost({\n scene: context.scene,\n headPose: cameraHeadPoseSource(context.camera),\n ...(context.xr ? { handPose: webxrHandPoseSource(context.xr) } : {}),\n ...(context.kit ? { kit: context.kit } : {}),\n ...(context.input ? { rayInput: context.input } : {}),\n ...(context.pointers ? { pointers: context.pointers } : {}),\n });\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@realitycollective/xrblocks-uiextensions",
3
- "version": "0.1.1-preview.2",
3
+ "version": "0.1.1",
4
4
  "description": "EXPERIMENTAL Google XR Blocks / plain three.js adapter for the Reality Collective UI Extensions - hosts the engine-free @realitycollective/webxr-uiextensions windowing core (UIKitML panels, window chrome, follow mode) in any three.js WebXR scene, including XR Blocks Scripts.",
5
5
  "keywords": [
6
6
  "realitycollective",
@@ -12,7 +12,10 @@
12
12
  "windowing",
13
13
  "spatial-ui",
14
14
  "uikitml",
15
- "typescript"
15
+ "typescript",
16
+ "hand-menu",
17
+ "pointer",
18
+ "docking"
16
19
  ],
17
20
  "license": "MIT",
18
21
  "author": "Reality Collective",
@@ -37,8 +40,9 @@
37
40
  "dependencies": {
38
41
  "@pmndrs/uikit": "^1.0.76",
39
42
  "@pmndrs/uikitml": "^0.1.12",
40
- "@realitycollective/threejs-uiextensions": "^0.1.1-preview.2",
41
- "@realitycollective/webxr-uiextensions": "^0.1.1-preview.2"
43
+ "@realitycollective/threejs-uiextensions": "^0.1.1",
44
+ "@realitycollective/webxr-input": "^0.1.8",
45
+ "@realitycollective/webxr-uiextensions": "^0.1.1"
42
46
  },
43
47
  "peerDependencies": {
44
48
  "three": ">=0.170.0"