@realitycollective/xrblocks-uiextensions 0.1.0 → 0.1.1-preview.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
@@ -1,9 +1,60 @@
1
1
  # Changelog
2
2
 
3
- Change log for the Reality Collective WebXR UI Extensions packages. All four packages are versioned and released together; the version below is the one carried by the `v<version>` release tag.
3
+ Change log for the Reality Collective WebXR UI Extensions packages. All five packages are versioned and released together; the version below is the one carried by the `v<version>` release tag.
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]
8
+
9
+ ### Added
10
+
11
+ - `@realitycollective/threejs-uiextensions` - a plain three.js / WebXR binding. `UixWindowHost`, `UixPanelDocument`, `configureRendererForUikit` and `fitScale` moved here from `@realitycollective/xrblocks-uiextensions` unchanged (the XR Blocks scene graph is plain three.js already, so its host had no XR-Blocks-specific code in it), plus a new `pointerBridgeFactory` option on `UixWindowHost` so a platform's own interaction source can drive the same host - `@realitycollective/xrblocks-uiextensions`'s `UixWindowHost` is now a thin subclass that supplies `XrBlocksPointerBridge` in place of this package's own bridge, and every other export keeps its old import path through a re-export. New in this package: `ScenePointerBridge` (`pointer-bridge.ts`), which resolves a hit-tested ray, a proximity-tested grab or touch, into the same `pointerdown`/`pointerup`/`click`/`pointerenter`/`pointerleave` events and `TouchPress` samples `XrBlocksPointerBridge` produces from XR Blocks' own callbacks; `connectWebXrPointerInput` (`webxr-input.ts`), which wires that bridge straight to a live WebXR session through `renderer.xr` - `selectstart`/`selectend`/`squeezestart`/`squeezeend` on three's own controller objects for ray and near/grab, and a per-frame `index-finger-tip` joint poll for touch - with no engine SDK beyond three.js itself; and `connectUIExtensions` (`setup.ts`), the package's one setup entry point, mirroring `xrblocks-uiextensions`'s own `connectUIExtensions` for a scene with no XR Blocks Script. `webxrRayInputAccess` (`ray-input.ts`) reads a title-bar ray drag's live ray from three's own `getController(i)` objects, so a drag rides the ray at a fixed grab distance (`beginDrag`/`dragPosition`), the same laser-distance math every other platform uses. Runs the shared `windowHostContractCases`, `sceneTargetContractCases` and `uixElementContractCases`, a core-parity suite (follow equals `stepFollow`, region and hand-menu defaults, title default, control upgrades), and a rule-by-rule suite mirroring `xrblocks-uiextensions/test/pointer-bridge.test.ts` for touch press, controller poke, ray click on release, hover, hold-to-drag with a per-window `dragDelay`, near drag, laser-distance ray drag, billboard while dragging, drop capture and focus bias including a docked window. `CursorVisual` (`cursor-visual.ts`) closes `ui/pointer-cursor` for this binding: a disc drawn at each ray's current hit on a panel, oriented to the hit surface (or the ray's own orientation with no surface normal to read), shown and hidden only by whether the ray currently hits something - never by hover or focus state. XR Blocks is excused from this rule (its SDK draws its own reticle with no seam to observe) but plain three.js owns rendering, so nothing drew one here until now. `ScenePointerBridge.rayCast` reports the world-space hit point and surface normal a cursor needs, alongside the existing `rayHit` (now a thin projection of it); `connectWebXrPointerInput` draws one `CursorVisual` per controller slot from the same per-frame ray cast hover already runs, given a `scene` (`connectUIExtensions` passes its own automatically).
12
+ - `@realitycollective/webxr-uiextensions` - `core/cursor.ts`'s `cursorPlacement` and `DEFAULT_CURSOR_OFFSET` (0.004 m): the one placement rule `ui/pointer-cursor` states as pure logic - a cursor disc's hit point nudged a fixed distance off the surface along its own facing, so it never z-fights that surface, restated from IWSDK's `@iwsdk/xr-input` `CursorVisual.updateFromIntersection`'s `zOffset`. The orientation itself reuses the core's existing `faceViewer`/`rotate` primitives rather than adding a new one. `@realitycollective/threejs-uiextensions`'s `CursorVisual` is the first binding to run it.
13
+ - `stepFollow()`, the follow rule every following window and region uses, as pure logic in the core: IWSDK's `FollowSystem` in its `PivotY` behaviour restated step for step, with `enterFollow()`, `followOffsetFromPose()`, `resolveFollow()`, the yaw helpers (`yawOf`, `yawQuaternion`, `rotateByYaw`, `yawToward`, `strictFollowTarget`) and the defaults `DEFAULT_WINDOW_FOLLOW` (offset `[0, -0.15, -1.2]`, speed 3, tolerance 0.35 m, 30 degrees) and `DEFAULT_REGION_FOLLOW` (IWSDK's `Follower` defaults: offset `[0, -0.2, -1.4]`, speed 1, tolerance 0.4 m). A new IWSDK test drives IWSDK's own `FollowSystem` headlessly beside `stepFollow` over the same head motion and requires the same position and facing every frame, so the core rule cannot drift from the reference.
14
+ - Placement rules as pure logic: `focusBiasAmount()` and `applyFocusBias()` (the focused window is drawn 0.02 m nearer per window behind it), `regionSlotPose()`, and the window defaults `DEFAULT_FOCUS_BIAS` (0.02 m), `DEFAULT_DRAG_DELAY` (0.3 s) and `DEFAULT_BILLBOARD_WHILE_DRAGGING` (true). `RayTuple` is re-exported with the other geometry names.
15
+ - `@realitycollective/native-uiextensions` - `nativeUiHostConformanceCases()`, the host conformance kit: ten runner-free host cases, named `ui/<master row>`, that a native app runs on its device against its real `ui` slice. They check that the host draws a window at the pose it is handed, follows the core rule in `body-follow` and `head-locked`, hides a hand menu with no tracked hand, applies the title, chrome and labels, hides and restores, applies focus bias, and measures a fingertip's signed distance positive in front. The host provides the test readbacks of the new `NativeUiTestHost` type (`windowPose`, `windowHidden`, `elementProperties`, `measureTouch`) in a test build. Against the reference fake every case passes, and the kit is shown to fail a host that misplaces a window or drops the sign of a distance.
16
+ - `uixElementContractCases()`, a shared conformance suite for `UixElement`, the interface the core's controls drive panel elements through, with `CONTRACT_PANEL_MARKUP` and the `UixElementContractSubject`, `UixElementContractDriver` and `UixElementContractCase` types. Every platform builds the same small panel from the markup its own way, and six cases check it: children follow the markup in order, `tagOf` reads a `uix-*` tag and nothing for a built-in one, `data-uix-*` attributes arrive camelCased in `userData`, `setProperties` reaches what the platform shows, one event reaches every listener on its element and no other, and a core stepper upgraded from the markup follows clicks to its maximum. It runs against real uikit elements on IWSDK and on XR Blocks and three.js, built headlessly the way each platform builds them, and against `NativeWindowHost` over the fake native app. It found the native defect under Fixed.
17
+ - `dataAttributeKey()`, the one rule for where a `data-*` attribute lands in `userData`: `data-uix-min` becomes `uixMin`, and an already camelCased `dataUixMin` gives the same key. The IWSDK component set and the native proxy elements both use it; the IWSDK copy was private.
18
+ - `sceneTargetContractCases()`, a shared conformance suite for `SceneTarget`, shipped as data beside `windowHostContractCases()`, with `CONTRACT_SCENE` and the `SceneTargetContractSetup` and `SceneTargetContractCase` types. A client builds its UI with the core's `applyScene()` and never asks which platform draws it, so every host must turn the same descriptor into the same windows. The suite checks that through the host's `WindowManager`: every descriptor window is opened under its id and title, a window named into a region is in that region, a window keeps its dock mode and chrome, and a window given both a region and a dock mode is world-locked in that region. The IWSDK, XR Blocks and native hosts all pass it.
19
+ - `MemoryWindowHost`, `MemoryWindowHandle`, `MemoryPanel` and `createMemoryWindowHostSetup()` in `@realitycollective/webxr-uiextensions`, an in-memory `WindowHost` and the family's mock. The package shipped `windowHostContractCases()` but nothing to run them against without an engine, so every headless consumer and every new adapter had to write a host first. The mock opens windows on a real `WindowManager`. A window's panel attaches when the test calls `attach(id)`, as on IWSDK, and closing a window through the manager disposes its panel and drops it from `onPanelReady` replay. `dispose()` closes every window it opened and forgets its listeners, as every platform host does. `createMemoryWindowHostSetup()` returns a fresh `WindowHostContractSetup`, so the suite runs in one loop. It passes every contract case, which `test/memory-window-host.test.ts` checks, and it is inside the 100% coverage gate. It came from a team building a native host, whose version passed the suite under Node and Hermes.
20
+ - `dispose()` on `WindowHost`, implemented by every platform host: `IwsdkSceneHost`, `UixWindowHost` (XR Blocks and three.js) and `NativeWindowHost`. It means the same thing everywhere: close every window the host opened through its `WindowManager`, so each goes down the normal close path, then release the host's own subscriptions and listeners, so `onPanelReady` replays nothing afterwards. It is safe to call twice. The XR Blocks host also removes the region groups it added to the scene. The IWSDK host silences its readiness system, which stays registered on the world, and forgets itself, so a later `createSceneHost(world)` builds a fresh host. No host could be torn down before, so an app that rebuilt its UI leaked listeners and scene objects on every platform. `windowHostContractCases()` gains a seventh case that holds every host to it. This adds a required member to the `WindowHost` interface, so a host written outside this repository must implement it.
21
+ - `@realitycollective/native-uiextensions` - the native app adapter, for an OpenXR or visionOS app that embeds a JavaScript engine such as Hermes, renders panels itself and installs `globalThis.__rcHost`, per the Reality Collective native host contract shared across the WebXR family repositories. `NativeWindowHost` implements the core `WindowHost` and `SceneTarget`, as the IWSDK and XR Blocks hosts do, so `applyScene` builds the same scene on native; regions go to the host as `createRegion` and are removed on `dispose`. Bare panels and windows build a tree of internal proxy `UixElement`s over the host's `NativeElementNode` tree, so the existing `data-uix` control upgraders work unchanged; `setProperties` forwards to `host.setProperties`, and `addEventListener` is routed from the host's single `onElementEvent` callback by `(panelId, elementHandle)`. Every `WindowManager` state change (open, focus, minimize, hide, dock, chrome, hand menu) mirrors to the host as `applyWindow(record)`; closing a window disposes its panel (once attached) and calls `closeWindow`. No engine dependency - the package reads and writes only plain data across the host boundary. Ships with an in-memory fake of the `ui` slice under `test/helpers` and runs the shared `windowHostContractCases()` against it.
22
+ - `demos/webxr-multiplatform` - `?uix-engine=<engine>&uix-autostart=1` boots that pipeline at once instead of showing the launch screen. It exists for the post-deploy smoke test, which now loads the desktop and IWSDK pipelines of the deployed lab as well as its launch screen; before this a pipeline that failed to start was invisible to CI, because nothing pressed START. The XR Blocks pipeline is not smoked: xrblocks renders into `<body>` rather than the mount point and logs a `console.error` about the three.js revision it wants, both of which the smoke test counts as failures.
23
+ - `dragDelay` on `WindowOptionsBase`: seconds a title-bar ray press is held before it becomes a drag, per window, default `DEFAULT_DRAG_DELAY` (0.3 s). Every platform already had the constant; only IWSDK's `UIWindow` component read it per window, and even there `createUIWindow` never passed the option through, so it was unreachable everywhere. Native and XR Blocks now take it too.
24
+ - `@realitycollective/webxr-uiextensions` - two core modules extracted from `NativeWindowHost`, the only binding that had this orchestration before: `pointer-events.ts` (`EdgePress`, the press/release/click state machine for a boolean-active ray or grab pointer, and `dispatchTouchUpdate`, which turns a `TouchPress` update into the same three events) and `titlebar-drag.ts` (`TitlebarDragController`, hold-to-drag timing, drag math, billboard-while-dragging and drop capture as one class a binding drives per window). `NativeWindowHost` now runs both with no behaviour change; `@realitycollective/xrblocks-uiextensions` runs `TitlebarDragController` too (see below). `EdgePress` is not needed by XR Blocks: its own select/grab callbacks are already edge-triggered, one start/end pair per gesture, so there is no boolean to edge-detect there.
25
+ - `@realitycollective/xrblocks-uiextensions` - `pointer-bridge.ts`'s `XrBlocksPointerBridge`, attached automatically to every panel `UixWindowHost` creates. It runs the IWSDK pointer rules: a poke (touch) and a controller tip both press and release through the core `TouchPress`, from `onObjectTouching`/`onObjectTouchEnd`; a ray clicks on `onSelectEnd` (release), never on `onSelectStart` (intersection); hover forwards `onHoverEnter`/`onHoverExit` to `pointerenter`/`pointerleave`; a title-bar press runs the core `TitlebarDragController` - a ray waits out `dragDelay`, a grab (near drag, gated to the title bar) starts at once, both billboard while dragging by default (`billboardWhileDragging`), and a drop within a region's snap radius docks the window into it. The demo pipeline's `onSelectStart` override that raycast and clicked on intersection is gone; `pointer-forward.ts` stays for a caller driving its own raycaster outside the host.
26
+ - `@realitycollective/xrblocks-uiextensions` - `ray-input.ts`'s `rayOf`/`XrBlocksRayInputAccess`, read from a new `rayInput` option (`input` on `connectUIExtensions`, mirroring `input: xb.input` on `@realitycollective/xrblocks-interactions`' own provider). A title-bar RAY drag now rides the controller's live ray at a fixed grab distance - the core `beginDrag`/`dragPosition` laser math IWSDK and native use, via a new core helper, `intersectRayPlane` (`drag-math.ts`), which seeds the drag session from where the ray meets the title bar's own plane, since XR Blocks' `SelectEvent` carries no hit point of its own. With no `rayInput` wired (or no ray source for that controller yet), a ray drag falls back to the controller's own position delta - correct, but not laser-distance - the same fallback this host used before `rayInput` existed. A grab (near drag) is unaffected: it already rode the hand's own position, which is exactly how native's grab pointer works too.
27
+ - `@realitycollective/xrblocks-uiextensions` - focus bias (`applyFocusBias`): the focused window is now drawn 0.02 m nearer the viewer per window behind it, every frame, as IWSDK and native already do - including a DOCKED window. `layoutRegion` now uses the core `regionSlotPose` (rotating the slot offset by the region's own orientation, which the previous hand-written version did not) and runs every frame, not only on a dock change, so a docked window's pre-bias position is always fresh before bias nudges it - `update()`'s three passes (drag, then regions, then placement and bias) now match `NativeWindowHost`'s order.
28
+
29
+ ### Changed
30
+
31
+ - 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.
32
+ - `@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.
33
+ - **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:
34
+ - Implement `setWindowPose(windowId, pose, depthOrder)` and draw and hit-test each window exactly there. Never place a window yourself; the pose already carries the follow rule, hand-menu anchor, region slot, drag and focus bias.
35
+ - Implement `onPointerSample(cb)` and report what each pointer measures: `ray` (every frame, with `ray` and `active` for select), `touch` (while near a panel, with `signedDistance` along the panel normal, positive in front) and `grab` (every frame, with `point` and `active` for squeeze or pinch). Stop raising `pointerdown`, `pointerup` and `click` yourself; the binding raises them, and a click now fires on RELEASE for rays too, as on IWSDK. `onElementEvent` stays for hover and value changes.
36
+ - `applyWindow(record)` carries only two fields a host applies: `hidden`, which now means "not shown" (hidden, or a hand menu whose palm gate is closed), and `dockMode`. Title, chrome button `display`, the PIN and MIN labels and the minimized content arrive through `setProperties`.
37
+ - `createRegion` and `removeRegion` are gone from the `ui` slice: regions are the binding's.
38
+ - Give the binding the `input` slice (`getHeadPose()`, and `sample()` for grip poses) and frames (`update(dt)`, or `attachToHost: true` over `__rcHost.onFrame`), as `createNativeInteractions` takes them.
39
+ - `@realitycollective/native-uiextensions` - a window's `WindowManager` record opens when its panel attaches, as IWSDK's `UIWindowSystem` opens it on adoption, not synchronously in `createWindow`. `manager.has(id)` is false until `onReady` fires. `NativeWindowHostOptions` gains the `registerUIExtensions` switches (`drag`, `nearDrag`, `regions`, `controls`, all on) and `touchPress` thresholds, and `CreateWindowOptions` gains `billboardWhileDragging`.
40
+ - `head-locked` is defined: inside an immersive session it follows exactly as `body-follow` does, as IWSDK does because its `ScreenSpaceUISystem` hands the panel back to world space while presenting. Only outside a session is it pinned to the screen. The core's dock-state comments said "rigidly attached to the view", which no platform does in a session.
41
+ - `@realitycollective/iwsdk-uiextensions` - the `UIWindow` and `UIDockRegion` component defaults and the `createUIWindow` and `createDockRegion` factory defaults now read the core constants (`DEFAULT_REGION`, `DEFAULT_WINDOW_FOLLOW`, `DEFAULT_REGION_FOLLOW`, `DEFAULT_FOCUS_BIAS`, `DEFAULT_DRAG_DELAY`, `DEFAULT_BILLBOARD_WHILE_DRAGGING`), so there is one set of defaults, in the core, for every platform. No value changed: the region defaults stay `column`, pitch 0.35 m, 2 columns, capacity 0, snap radius 0.5 m, as published in 0.1.1-preview.0.
42
+ - `tagOf()` reads nothing for a built-in element on every platform, as its doc always said. The XR Blocks and three.js parser records a built-in tag such as `span` the same way as a custom one, so `tagOf` read `"span"` there and nothing on IWSDK. A custom element name always holds a hyphen, so `tagOf` now returns a name only when it has one.
43
+ - `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.
44
+ - `@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.
45
+ - **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.
46
+
47
+ ### Fixed
48
+
49
+ - `@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.
50
+ - `@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.
51
+ - `@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.
52
+
53
+ - `demos/webxr-multiplatform` - the IWSDK pipeline failed to start on the deployed lab, for every visitor, with `Failed to resolve module specifier "three-mesh-bvh"`. The Vite config externalised `three-mesh-bvh` alongside the optional integrations xrblocks imports lazily, but `@iwsdk/core` imports it for real, so the IWSDK chunk shipped a bare import the browser cannot resolve. It resolves in the workspace through `@iwsdk/core`'s own dependency and is now bundled like `three-pathfinding`. Found while photographing the live lab for the Reality Collective site.
54
+ - `@realitycollective/iwsdk-uiextensions` - the README and `Examples/controls` still showed the `data-uix="stepper"` / `data-uix-role` attribute form that the core stopped upgrading when controls became custom elements, so the shipped example silently produced no controls. Both now use `<uix-stepper>`, `<uix-toggle>`, `<uix-expandable-label>` and `<uix-log-view>` with their part elements, and both register `uixComponentSet` when creating the world, which IWSDK 0.5 needs before it will parse a panel that uses a control. The README says why.
55
+ - `docs/developer-cycle.md` - the publishing section described GitHub Packages ("no npmjs.com for now"); the packages publish to npmjs.com under the `@realitycollective` scope through `publish-npm.yml`, and the section now describes that workflow, its two dist-tags and the consumer install commands.
56
+ - `@realitycollective/iwsdk-uiextensions` - `createUIWindow` never read `options.dragDelay`: the `UIWindow` component always opened with `DEFAULT_DRAG_DELAY` regardless of what a caller asked for, even though the component field and `UIDragSystem` already honoured a per-window value. It is now read through, alongside the new `WindowOptionsBase.dragDelay`.
57
+
7
58
  ## [0.1.0] - 2026-09-17
8
59
 
9
60
  ### Added
@@ -52,6 +103,7 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
52
103
  - `@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.
53
104
  - `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.
54
105
 
106
+ [0.1.1]: https://github.com/realitycollective/WebXR-UIExtensions/compare/v0.1.0...development
55
107
  [0.1.0]: https://github.com/realitycollective/WebXR-UIExtensions/releases/tag/v0.1.0
56
108
 
57
109
  ### Fixed
package/README.md CHANGED
@@ -14,18 +14,23 @@
14
14
  | Portable scene descriptors (`applyScene`) | ✅ | ✅ implements `SceneTarget` |
15
15
  | Panel-ready wiring (`onPanelReady`) | ✅ | ✅ implements `WindowHost` |
16
16
  | Follow mode (body-follow, yaw-only, eased) | ✅ | ✅ pure `follow-math` |
17
- | Hand menus (`hand-locked`, palm gate, anchors) | ✅ from the player rig | ✅ from a `HandPoseSource`; `webxrHandPoseSource(renderer.xr)` reads the session's input sources, and without hands the menu follows the body |
17
+ | Hand menus (`hand-locked`, palm gate, anchors) | ✅ from the player rig | ✅ from a `HandPoseSource`; `webxrHandPoseSource(renderer.xr)` reads the session's input sources. No tracked hand hides the menu, exactly as IWSDK and native - it never falls back to some other placement |
18
18
  | Dock regions (wall/belt, slots, follow) | ✅ | ✅ `createRegion`, `manager.dockTo` (`host.dock` forwards) |
19
19
  | Desktop mouse input (hover, click, drag-to-look) | ✅ | ✅ via `@pmndrs/pointer-events` |
20
20
  | Desktop locomotion (WASD, jump, crouch, sprint) | n/a | ✅ `DesktopControls` |
21
- | XR select-ray click forwarding | ✅ | ✅ minimal (`forwardClick`) |
21
+ | Ray click on release, poke (touch), controller-tip poke | ✅ | ✅ `pointer-bridge.ts`'s `XrBlocksPointerBridge`, attached to every panel automatically - runs the core `TouchPress` and clicks on `onSelectEnd`, not on intersection |
22
+ | Hover styles (`pointerenter`/`pointerleave`) | ✅ | ✅ relayed from `onHoverEnter`/`onHoverExit` |
22
23
  | Bare panels (`createPanel`) | ⬜ ECS owns the lifecycle | ✅ `supportsStandalonePanels` is `true` |
23
- | Title-bar ray drag (`@pmndrs/handle`) | ✅ | ⬜ roadmap (`movable` is accepted and ignored) |
24
- | Title-bar near grab (squeeze / pinch) | ✅ | ⬜ roadmap (needs drag) |
25
- | Guarded poke (one press per touch, front only) | ✅ `UITouchGuardSystem` over IWSDK's touch pointers | ⬜ no near touch here yet; the core `TouchPress` is ready for it |
26
- | Drop-to-dock by dragging | ✅ | ⬜ roadmap (needs drag) |
24
+ | Title-bar ray drag, with a per-window `dragDelay` | ✅ (`@pmndrs/handle`) | ✅ `TitlebarDragController`, from `onSelectStart`/`onSelectEnd` on the title bar |
25
+ | Title-bar near grab (squeeze / pinch), starts at once | ✅ | ✅ from `onObjectGrabStart`/`onObjectGrabEnd`, gated to the title bar as on native |
26
+ | Billboard while dragging | ✅ | ✅ `billboardWhileDragging` (default on) |
27
+ | Drop-to-dock by dragging | ✅ | ✅ the core `RegionRegistry.capture` |
28
+ | 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
+ | Guarded poke (one press per touch, front only) | ✅ `UITouchGuardSystem` over IWSDK's touch pointers | ✅ the same core `TouchPress`, fed from `onObjectTouching` |
27
30
  | System keyboard text input | ✅ | ⬜ untested on Android XR |
28
31
 
32
+ 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
+
29
34
  ## Required renderer setup (read this first)
30
35
 
31
36
  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.
@@ -45,15 +50,11 @@ IWSDK does this internally, which is why panels look right there with no setup.
45
50
 
46
51
  ```ts
47
52
  import * as xb from 'xrblocks';
48
- import {
49
- DockMode,
50
- connectUIExtensions,
51
- forwardClick,
52
- } from '@realitycollective/xrblocks-uiextensions';
53
+ import { DockMode, connectUIExtensions } from '@realitycollective/xrblocks-uiextensions';
53
54
 
54
55
  class MyScript extends xb.Script {
55
56
  async init() {
56
- this.uix = connectUIExtensions({ scene: this, camera: xb.camera });
57
+ this.uix = connectUIExtensions({ scene: this, camera: xb.camera, xr: xb.core.renderer.xr, input: xb.input });
57
58
  const config = await fetch('./ui/my-window.json').then((r) => r.json());
58
59
  this.uix.createWindow({
59
60
  id: 'status',
@@ -65,26 +66,23 @@ class MyScript extends xb.Script {
65
66
  update() {
66
67
  this.uix.update(xb.getDeltaTime());
67
68
  }
68
- onSelectStart(event) {
69
- /* raycast from event.target, then forwardClick(intersections) -
70
- see demos/webxr-multiplatform for the complete wiring */
71
- }
72
69
  }
73
70
 
74
71
  xb.add(new MyScript());
75
72
  await xb.init();
76
73
  ```
77
74
 
78
- Nothing here imports `xrblocks` - the glue binds to plain three.js shapes (`scene: Object3D`, `camera`), so the same host works in a hand-rolled three.js WebXR app.
75
+ Nothing here imports `xrblocks` at the type level - the glue binds to plain three.js shapes (`scene: Object3D`, `camera`), so the same host works in a hand-rolled three.js WebXR app. Press, poke, drag and hover need no wiring in your own `Script`: `createWindow`/`createPanel` attach the pointer bridge to every panel automatically, driven by whichever of XR Blocks' `onSelectStart`/`onSelectEnd`, `onObjectTouchStart`/`onObjectTouching`/`onObjectTouchEnd`, `onObjectGrabStart`/`onObjectGrabEnd` and `onHoverEnter`/`onHoverExit` your scene calls.
79
76
 
80
77
  ### Window options and handles
81
78
 
82
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:
83
80
 
84
81
  - `id` is optional. Omit it and the window is named `uix-window-<n>`.
85
- - `movable` is accepted and recorded, but nothing acts on it yet: this host has no title-bar drag of its own, so there is no gate to close. It is in the options so a scene descriptor written for IWSDK loads here unchanged.
82
+ - `movable` (default `true`) gates the title-bar drag: `false` never wires a press listener onto the title bar at all.
83
+ - `dragDelay` (default `DEFAULT_DRAG_DELAY`, 0.3 s) is how long a ray press on the title bar is held before it becomes a drag; a grab (near drag) always starts at once. `billboardWhileDragging` (default `true`) keeps the window yawed toward the viewer while it is dragged, and once more settling at the drop. Pass `input: xb.input` to `connectUIExtensions` (or `rayInput` to the host) so a ray drag rides the actual controller ray at a fixed distance, as IWSDK and native do; without it, a ray drag falls back to the controller's own position delta.
86
84
  - The four chrome flags (`closable`, `minimizable`, `pinnable`, `dockable`) are off unless set, as on IWSDK; `host.manager.setChrome(id, {...})` changes them later. `host.manager.hide/show`, `dockTo/undock/returnHome` and `close` all take effect here, so a menu written against the manager needs no host-specific code.
87
- - `handMenu` and `dockMode: 'hand-locked'` make a hand menu. Pass `xr: renderer.xr` to `connectUIExtensions` (or `handPose` to the host) so it rides the session's tracked hands; on a page that also serves a desktop the source reports no hands outside a session and the menu follows the body until one starts.
85
+ - `handMenu` and `dockMode: 'hand-locked'` make a hand menu. Pass `xr: renderer.xr` to `connectUIExtensions` (or `handPose` to the host) so it rides the session's tracked hands. With no hand tracked - no source at all, or a session with neither palm raised - the menu is hidden, exactly as IWSDK and native; it never falls back to some other placement.
88
86
 
89
87
  The handle it returns satisfies the core `WindowHandle` and adds the three.js specifics:
90
88
 
package/dist/host.d.ts CHANGED
@@ -1,163 +1,20 @@
1
1
  /**
2
- * UixWindowHost - the engine binding for plain three.js / XR Blocks scenes.
2
+ * UixWindowHost - the XR Blocks binding.
3
3
  *
4
- * Owns a core `WindowManager` + `RegionRegistry` and applies their decisions
5
- * to the scene graph: spawn UIKitML windows and dock regions, wire chrome
6
- * buttons (focus / PIN / DOCK / MIN / X), collapse content while minimized,
7
- * hide and show, place docked windows into their region's slots, return a
8
- * window to where it spawned, ease `body-follow` windows (and body-locked
9
- * regions) toward the viewer each frame, and ride `hand-locked` windows
10
- * (hand menus) on a tracked hand behind the palm gate.
11
- *
12
- * The manager is the API app code drives; every manager event is applied
13
- * here, so `host.manager.hide(id)` or `.dockTo(id, region)` from a hand menu
14
- * is all a caller needs.
15
- *
16
- * It implements the core's `WindowHost` (so app code can wire behaviour
17
- * through `onPanelReady` with no engine knowledge) and `SceneTarget` (so a
18
- * portable `SceneDescriptor` builds the same playground here as on IWSDK).
19
- */
20
- import { Group, type Object3D } from 'three';
21
- import { type Kit } from '@pmndrs/uikitml';
22
- import { RegionRegistry, WindowManager, type HandPoseSource, type HeadPoseSource, type PanelHandle, type PanelReadyEvent, type RegionDefinition, type SceneRegion, type SceneTarget, type SceneWindow, type Vec3Tuple, type WindowHandle as CoreWindowHandle, type WindowHost, type WindowOptionsBase } from '@realitycollective/webxr-uiextensions';
23
- import { UixPanelDocument } from './panel-document.js';
24
- /**
25
- * Options for {@link UixWindowHost.createWindow}.
26
- *
27
- * Everything but `config` comes from the portable {@link WindowOptionsBase},
28
- * so an option means the same thing here as on the IWSDK adapter. `id` is
29
- * optional: leave it out and the host names the window `uix-window-<n>`.
30
- *
31
- * One option is accepted but not acted on: `movable`. This host has no
32
- * title-bar drag of its own yet - dragging comes from the XR Blocks / desktop
33
- * input layer above it - so the flag is recorded and otherwise ignored.
34
- */
35
- export interface CreateWindowOptions extends WindowOptionsBase {
36
- /** Compiled UIKitML JSON (the `{ element, classes }` shape). */
37
- config: unknown;
38
- }
39
- export interface CreateRegionOptions extends Partial<RegionDefinition> {
40
- id: string;
41
- position?: Vec3Tuple;
42
- /** Body-lock the region so it follows the viewer. */
43
- follow?: boolean;
44
- followOffset?: Vec3Tuple;
45
- }
46
- /**
47
- * A window spawned by {@link UixWindowHost.createWindow}.
48
- *
49
- * Satisfies the core {@link CoreWindowHandle} and adds the three.js specifics.
50
- * `panel` is the document itself, which already implements `PanelHandle`, and
51
- * is never `undefined` here: uikitml interprets the markup synchronously, so
52
- * the panel exists the moment `createWindow` returns.
4
+ * Every window/panel/region rule (spawn, chrome, follow, dock regions, hand
5
+ * menus, drag, focus bias) lives in
6
+ * `@realitycollective/threejs-uiextensions`'s own `UixWindowHost`, which this
7
+ * class extends unchanged - "one behaviour, every platform": XR Blocks IS a
8
+ * three.js WebXR scene, so it runs the same host as the plain three.js
9
+ * binding. The only thing added here is XR Blocks' own interaction surface:
10
+ * `XrBlocksPointerBridge` (`pointer-bridge.ts`) turns its select/touch/grab/
11
+ * hover callbacks into the pointer events and touch samples the shared host
12
+ * already knows how to apply, in place of `threejs-uiextensions`'s own
13
+ * WebXR-session raycasting bridge (`ScenePointerBridge`), which XR Blocks
14
+ * does not need - its own interaction manager already resolves hit-testing.
53
15
  */
54
- export interface XrBlocksWindowHandle extends CoreWindowHandle {
55
- readonly id: string;
56
- /** The scene-graph node - position/rotate freely. */
57
- group: Group;
58
- document: UixPanelDocument;
59
- /** Same object as {@link document}, under the portable name. */
60
- readonly panel: UixPanelDocument;
61
- onReady(listener: (panel: PanelHandle) => void): () => void;
62
- }
63
- /** Back-compatible name for {@link XrBlocksWindowHandle}. */
64
- export type WindowHandle = XrBlocksWindowHandle;
65
- export interface RegionHandle {
66
- id: string;
67
- group: Group;
68
- }
69
- export interface UixWindowHostOptions {
70
- /** Parent for spawned windows (the scene, or any group inside it). */
71
- scene: Object3D;
72
- /** Viewer pose provider - camera on desktop, HMD pose in XR. */
73
- headPose: HeadPoseSource;
74
- /**
75
- * Tracked-hand pose provider, for `hand-locked` windows (hand menus). See
76
- * `webxrHandPoseSource` for one backed by a WebXR session. Leave it out
77
- * where there are no hands (a desktop) and hand-locked windows fall back
78
- * to body-follow placement, so the same scene still shows its menus.
79
- */
80
- handPose?: HandPoseSource;
81
- /** Optional UIKitML component kit(s) (e.g. horizon kit). */
82
- kit?: Kit;
83
- /**
84
- * Resolver for a window's `config` when it arrives as a string path (as
85
- * portable `SceneDescriptor`s carry it).
86
- *
87
- * Defaults to fetching the `.uikitml` source and parsing it, which is the
88
- * same artefact IWSDK loads - so one markup file serves every adapter and
89
- * no build step is required. Override only for an unusual transport; you do
90
- * not need to supply this to load a normal panel.
91
- */
92
- loadConfig?: (path: string) => Promise<unknown>;
93
- }
94
- export declare class UixWindowHost implements WindowHost, SceneTarget {
95
- /** Bare panels work here - the host does not own a panel lifecycle. */
96
- readonly supportsStandalonePanels: boolean;
97
- readonly manager: WindowManager;
98
- readonly regions: RegionRegistry;
99
- private readonly scene;
100
- private readonly headPose;
101
- private readonly handPose;
102
- private readonly kit;
103
- private readonly loadConfig;
104
- private readonly states;
105
- private readonly regionStates;
106
- private readonly readyListeners;
107
- /** Panels already live - replayed to late `onPanelReady` subscribers. */
108
- private readonly ready;
109
- /** Feeds `uix-window-<n>` ids to windows created without one. */
110
- private windowSequence;
16
+ import { UixWindowHost as ThreeJsWindowHost, type CreateRegionOptions, type CreateWindowOptions, type RegionHandle, type UixWindowHostOptions, type WindowHandle, type XrBlocksWindowHandle } from '@realitycollective/threejs-uiextensions';
17
+ export type { CreateRegionOptions, CreateWindowOptions, RegionHandle, UixWindowHostOptions, WindowHandle, XrBlocksWindowHandle };
18
+ export declare class UixWindowHost extends ThreeJsWindowHost {
111
19
  constructor(options: UixWindowHostOptions);
112
- /**
113
- * PanelHost - bare panel, unmanaged (used by devtools and tests). Always
114
- * available here, which is what `supportsStandalonePanels` reports.
115
- */
116
- createPanel(configJson: unknown): UixPanelDocument;
117
- /**
118
- * Observe panel readiness. Windows already live are replayed immediately,
119
- * so wiring code never races window creation.
120
- */
121
- onPanelReady(listener: (event: PanelReadyEvent) => void): () => void;
122
- spawnRegion(region: SceneRegion): void;
123
- /**
124
- * Scene descriptors carry config PATHS; this resolves the path and spawns
125
- * the window when it arrives. Fire-and-forget by design - subscribe with
126
- * {@link onPanelReady} to wire behaviour once the panel exists.
127
- */
128
- spawnWindow(window: SceneWindow): void;
129
- /** Create a dock region anchored in the scene. */
130
- createRegion(options: CreateRegionOptions): RegionHandle;
131
- region(id: string): RegionHandle | undefined;
132
- /**
133
- * Dock a window into a region (or undock it with `undefined`). A thin
134
- * forwarder to the manager, kept so existing callers read the same;
135
- * `manager.dockTo` / `manager.undock` are the portable calls.
136
- */
137
- dock(windowId: string, regionId: string | undefined): void;
138
- /** Make the registry (and the layout) agree with the record's region. */
139
- private applyRegion;
140
- /** Put a window back where it spawned: its region, or its placement and mode. */
141
- private returnHome;
142
- /** Spawn a managed window from compiled UIKitML JSON. */
143
- createWindow(options: CreateWindowOptions): XrBlocksWindowHandle;
144
- window(id: string): XrBlocksWindowHandle | undefined;
145
- /** Drive per-frame from the engine loop (delta in SECONDS). */
146
- update(deltaSeconds: number): void;
147
- /** This frame's tracked hands from the source, absent ones left out. */
148
- private readHands;
149
- private placeOnHand;
150
- private applyFollow;
151
- /** Re-place every docked window into its region's slot. */
152
- private layoutRegions;
153
- private layoutRegion;
154
- private wireChrome;
155
- /** Show exactly the enabled buttons. */
156
- private applyChrome;
157
- /** Drawn exactly when not hidden and (for a hand menu) the palm gate is open. */
158
- private applyPresentation;
159
- private setContentCollapsed;
160
- /** MIN when open, MAX when minimized - the label names the next action. */
161
- private syncMinimizeLabel;
162
- private syncPinLabel;
163
20
  }