@realitycollective/xrblocks-uiextensions
v0.1.0
Published
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, includin
Maintainers
Readme
@realitycollective/xrblocks-uiextensions
EXPERIMENTAL adapter for Google XR Blocks and plain three.js. It hosts the same @realitycollective/webxr-uiextensions 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.
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.
Feature matrix vs the IWSDK adapter
| Feature | IWSDK | XR Blocks / three.js (this package) |
| --- | --- | --- |
| UIKitML panel hosting (runtime interpret, scale-to-fit) | ✅ | ✅ UixPanelDocument |
| Window lifecycle + chrome (focus/PIN/DOCK/MIN/X, all opt-in, pin labels) | ✅ | ✅ UixWindowHost |
| Driving windows from code (WindowManager: hide/show, dockTo/undock/returnHome, setChrome, close) | ✅ | ✅ every manager event applied |
| Portable scene descriptors (applyScene) | ✅ | ✅ implements SceneTarget |
| Panel-ready wiring (onPanelReady) | ✅ | ✅ implements WindowHost |
| Follow mode (body-follow, yaw-only, eased) | ✅ | ✅ pure follow-math |
| 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 |
| Dock regions (wall/belt, slots, follow) | ✅ | ✅ createRegion, manager.dockTo (host.dock forwards) |
| Desktop mouse input (hover, click, drag-to-look) | ✅ | ✅ via @pmndrs/pointer-events |
| Desktop locomotion (WASD, jump, crouch, sprint) | n/a | ✅ DesktopControls |
| XR select-ray click forwarding | ✅ | ✅ minimal (forwardClick) |
| Bare panels (createPanel) | ⬜ ECS owns the lifecycle | ✅ supportsStandalonePanels is true |
| Title-bar ray drag (@pmndrs/handle) | ✅ | ⬜ roadmap (movable is accepted and ignored) |
| Title-bar near grab (squeeze / pinch) | ✅ | ⬜ roadmap (needs drag) |
| 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 |
| Drop-to-dock by dragging | ✅ | ⬜ roadmap (needs drag) |
| System keyboard text input | ✅ | ⬜ untested on Android XR |
Required renderer setup (read this first)
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.
Apply both settings to any renderer you create:
import { configureRendererForUikit } from '@realitycollective/xrblocks-uiextensions';
const renderer = new WebGLRenderer({ antialias: true });
configureRendererForUikit(renderer); // transparent sort + local clippingIWSDK does this internally, which is why panels look right there with no setup. A hand-rolled three.js host must do it explicitly, and under XR Blocks you should apply it to the renderer xb.init() creates.
Usage in an XR Blocks Script
import * as xb from 'xrblocks';
import {
DockMode,
connectUIExtensions,
forwardClick,
} from '@realitycollective/xrblocks-uiextensions';
class MyScript extends xb.Script {
async init() {
this.uix = connectUIExtensions({ scene: this, camera: xb.camera });
const config = await fetch('./ui/my-window.json').then((r) => r.json());
this.uix.createWindow({
id: 'status',
title: 'Status',
config,
dockMode: DockMode.BodyFollow,
});
}
update() {
this.uix.update(xb.getDeltaTime());
}
onSelectStart(event) {
/* raycast from event.target, then forwardClick(intersections) -
see demos/webxr-multiplatform for the complete wiring */
}
}
xb.add(new MyScript());
await xb.init();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.
Window options and handles
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:
idis optional. Omit it and the window is nameduix-window-<n>.movableis 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.- 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/returnHomeandcloseall take effect here, so a menu written against the manager needs no host-specific code. handMenuanddockMode: 'hand-locked'make a hand menu. Passxr: renderer.xrtoconnectUIExtensions(orhandPoseto 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.
The handle it returns satisfies the core WindowHandle and adds the three.js specifics:
const handle = host.createWindow({ title: 'Status', config });
handle.id; // 'uix-window-1'
handle.group; // the scene-graph node - position and rotate freely
handle.document; // the UixPanelDocument
handle.panel; // the same document, under the portable name
handle.onReady((panel) => wire(panel)); // fires straight away hereonReady fires synchronously because uikitml interprets the markup during createWindow; only LAYOUT is async. It still returns an unsubscribe function, so code that runs on both adapters has one shape. supportsStandalonePanels is true: createPanel(config) gives an unmanaged panel with no chrome and no window record.
Known constraint: three versions
[email protected] declares a peer of three@^0.184, while IWSDK mandates the [email protected] 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.
Testing
npm test # scale/follow/pointer math + a headless host lifecycle suiteDemo
demos/webxr-multiplatform - detects the platform and boots this adapter on Android XR (or via ?uix-engine=xrblocks anywhere, including XR Blocks' desktop simulator).
Live demos
- Showcase: webxr-uiextensions.pages.dev
- Multiplatform lab: webxr-uix-lab.pages.dev
License
MIT © Reality Collective
