XR Blocks SDK
XR Blocks (import * as xb from 'xrblocks') is a cross-platform JavaScript/TypeScript
SDK for rapidly prototyping AI + XR apps. It is built on three.js,
targets Chrome 136+ with WebXR on Android XR, and ships a desktop simulator so the
same code runs in a normal browser. The whole SDK is re-exported from
src/xrblocks.ts — that barrel is the public API surface; if a symbol is
not exported there it is internal.
The single most important rule when generating code: only call APIs that exist.
The framework's own evaluation found that hallucinated/inconsistent APIs are the #1
cause of broken generated apps. When unsure, grep src/xrblocks.ts and the relevant
subfolder, or copy a pattern from a real file in samples/, demos/, or templates/.
Canonical app skeleton
Every app is one or more xb.Script subclasses added before xb.init(). This is the
minimal, verified pattern (see templates/ and the README for full HTML):
import * as THREE from 'three';
import * as xb from 'xrblocks';
class MainScript extends xb.Script {
init() {
// called once; may be async
this.add(new THREE.HemisphereLight(0xffffff, 0x666666, 3));
const geometry = new THREE.CylinderGeometry(0.2, 0.2, 0.4, 32);
const material = new THREE.MeshPhongMaterial({color: 0xffffff});
this.player = new THREE.Mesh(geometry, material);
// Place it in front of the user at a comfortable height/distance.
this.player.position.set(0, xb.user.height - 0.5, -xb.user.objectDistance);
this.add(this.player); // `this` is itself a THREE.Object3D
}
onSelectEnd() {
// desktop click OR XR pinch
this.player.material.color.set(Math.random() * 0xffffff);
}
}
document.addEventListener('DOMContentLoaded', () => {
xb.add(new MainScript()); // register the script
xb.init(new xb.Options()); // boot the engine + render loop
});
Script is a THREE.Object3D, so this.add(obj) puts things in the scene under it.
Do not manage the render loop, WebXR session, or camera yourself — Core owns them.
Mental model
Core is a singleton (core/Core.ts). xb.init() builds the
renderer, camera, WebXR session, and every subsystem, then drives the frame loop.
Core is also exposed as xb.core, with convenience aliases:
xb.scene, xb.user, xb.world, xb.ai, xb.depth, xb.sound, xb.input,
xb.camera, plus functions xb.add(), xb.init(), xb.getDeltaTime(),
xb.getElapsedTime().
Script is your extension point (core/Script.ts) — Unity
MonoBehaviour-style lifecycle hooks (below).
Options configures everything (core/Options.ts) — one
object with chainable enable*() methods and per-subsystem sub-options.
- The conceptual model from the papers is the Reality Model:
user, world,
and AI agents as first-class primitives, with an interaction grammar that
separates explicit events (onSelectStart, click, pinch) from implicit intent
(gesture, gaze, voice).
Enabling features (xb.Options)
These chainable methods exist — verified in core/Options.ts:
const options = new xb.Options();
options.enableUI(); // spatial UI + reticles
options.enableReticles(); // pointing cursor
options.enableControllers(); // tracked controllers
options.enableHands(); // hand tracking (joints, pinch)
options.enableHandRays(); // visible rays from hands/controllers
options.enableGestures(); // pinch/fist/point/etc. (see input/gestures)
options.enableStrokes(); // $1 unistroke recognition
options.enableDepth(); // WebXR depth sensing + depth mesh
options.enablePlaneDetection(); // detected planes in xb.world
options.enableObjectDetection(); // object detection (also sets camera permission)
options.enableCamera('environment'); // passthrough device camera ('environment'|'user')
options.enableAI(); // Gemini/OpenAI via xb.ai
options.enableXRTransitions(); // fade transitions
options.enableVR(); // immersive-vr instead of immersive-ar
xb.init(options);
There is no enablePhysics() or enableLighting() — these are configured directly:
import RAPIER from '@dimforge/rapier3d-simd-compat'; // physics engine
options.physics.RAPIER = RAPIER; // assigning RAPIER enables physics
// ...then implement initPhysics(physics) / physicsStep() in your Script.
options.formFactor = 'desktop' autostarts the simulator; ?formFactor=desktop in the
URL does the same. options.catchScriptExceptions (default true) keeps one buggy
script from crashing the app.
Script lifecycle hooks
Override only what you need (all verified against core/Script.ts and core/User.ts):
| Hook |
When |
init(deps?) |
once, after registration; may return a Promise; receives injected deps |
update(time?, frame?) |
every frame |
initPhysics(physics) / physicsStep() |
physics setup / per physics step |
onSelectStart(e) / onSelectEnd(e) |
pinch (XR) or click (desktop) |
onSqueezeStart(e) / onSqueezeEnd(e) |
grip button |
onKeyDown(e) / onKeyUp(e) |
keyboard (e.code) |
onXRSessionStarted(session?) / onXRSessionEnded() |
entering/leaving XR |
onSimulatorStarted() |
desktop simulator booted |
Object-targeted hooks fire on the Script whose subtree was hit. Return true to mark
the event handled and stop it propagating to ancestors:
onObjectSelectStart/End, onObjectTouchStart/Touching/End,
onObjectGrabStart/Grabbing/End, onHoverEnter/Hovering/Exit.
Talking to the user, world, and AI
// User (xb.user — see core/User.ts)
xb.user.height;
xb.user.objectDistance;
xb.user.panelDistance;
xb.user.handedness;
xb.user.isSelecting(); // any controller pinching/clicking? (id optional)
xb.user.isSelectingAt(object); // is the user selecting this object/subtree?
xb.user.isPointingAt(object); // hover test
xb.user.getReticleTarget(0); // object under controller 0's reticle
xb.user.hands; // hand joints when enableHands()
// AI (xb.ai — see ai/AI.ts). Requires a key (?key=... or keys.json) — guard it.
if (xb.ai.isAvailable()) {
const res = await xb.ai.query({prompt: 'Write a haiku about dust.'});
// multimodal: xb.ai.query({type: 'multiPart', parts: [{text}, {inlineData:{data,mimeType}}]})
// res.text holds the answer; xb.ai.startLiveSession(config) for Gemini Live.
}
// World (xb.world) — detected planes / objects / meshes after enabling detection.
// Depth (xb.depth), Sound (xb.sound: spatial audio, speech recog/synth).
Spatial UI (core)
The core UI is a declarative grid built from xb.SpatialPanel (see
ui/layouts/SpatialPanel.ts and templates/1_ui):
const panel = new xb.SpatialPanel({
backgroundColor: '#2b2b2baa',
width: 2.5,
height: 1.5,
});
panel.position.set(0, xb.user.height, -xb.user.panelDistance);
this.add(panel);
const grid = panel.addGrid();
grid
.addRow({weight: 0.7})
.addText({text: 'Hello XR', fontColor: '#fff', fontSize: 0.08});
const button = grid
.addRow({weight: 0.3})
.addCol({weight: 1})
.addIconButton({text: 'check_circle', fontSize: 0.5}); // `text` = Material icon name
button.onTriggered = () => console.log('clicked/pinched/touched'); // unified select
onTriggered unifies click / pinch / touch on buttons. For flexbox-rich cards,
gradients, and shadows, use the uiblocks addon instead — see
addons/uiblocks/SKILL.md and "two UI systems" below.
Directory map (read deeper on demand)
| Path |
What lives there |
core/ |
Core singleton, Script, Options, User, DI Registry, XRButton, WebXR session mgmt |
input/ |
controllers, hands, gaze, mouse, gamepad; gestures/; strokes/ |
world/ |
World + planes/, mesh/, objects/ (Gemini & MediaPipe backends), sounds/ |
depth/ |
depth sensing, depth mesh, occlusion/ shaders & passes |
ai/ |
AI facade over Gemini + OpenAI (query / live / image gen) |
agent/ |
agent framework: tools, memory, context (WIP — see agent/README.md) |
ui/ |
core spatial UI: SpatialPanel, Grid/Row/Col, views, ModelViewer, Reticle |
ux/ |
DragManager, reusable interaction behaviors |
simulator/ |
desktop XR simulator (virtual user/hands/depth/planes, control modes) |
sound/ |
spatial audio, speech recognizer/synthesizer (see sound/README.md) |
physics/ |
Rapier3D integration |
lighting/, camera/, video/, stereo/ |
light estimation, device camera, video streams, stereo utils |
utils/ |
ModelLoader, dependency injection, helpers |
addons/ |
opt-in modules, each often with its own README/skills: uiblocks, netblocks, testing, glasses, volumes, virtualkeyboard, simulator UI, ... |
Two UI systems — pick deliberately
- Core UI (
xb.SpatialPanel + .addGrid()/.addRow()/.addCol()/.addText()/.add*Button())
— lightweight, no extra deps, good for HUDs, menus, and quick panels.
- uiblocks addon (
UICard, UIPanel, UIText, UIImage, UIIcon) — full flexbox
layout (@pmndrs/uikit), gradients, strokes, drop/inner shadows, and spatial behaviors.
Import from xrblocks/addons/uiblocks/src and call options.uikit.enable(uikit).
See addons/uiblocks/SKILL.md.
Don't mix the two on the same panel, and don't import UIPanel/UICard from xrblocks
core — they only exist in the uiblocks addon.
Common hallucinated-API mistakes to avoid
| ❌ Don't |
✅ Do |
options.enablePhysics() |
options.physics.RAPIER = RAPIER + implement initPhysics() |
Use xb.core.renderer / xb.core.physics in a constructor |
They're created during xb.init(); use them in/after init() |
new xb.UIPanel(...) / new xb.UICard(...) |
Those are the uiblocks addon; core uses xb.SpatialPanel().addGrid() |
xb.ai.query('text') (bare string) |
xb.ai.query({prompt: 'text'}), and guard with xb.ai.isAvailable() |
| Assume AI works with no key |
Provide ?key=... or keys.json; handle the unavailable case |
rgba()/hsla() colors in UI |
hex strings ('#ffffff') or THREE.Color |
Drive your own requestAnimationFrame loop |
Put per-frame logic in update(time, frame) |
Forget xb.add(script) before xb.init() |
Register every Script first |
Import bare three without the pinned importmap |
Use the importmap from the README / a template |
Design principles (honor these when contributing)
- Simplicity & readability — a
Script should read like a high-level description of
the experience. Simple things stay simple; complex logic stays explicit.
- Creator experience first — absorb incidental complexity (sensor fusion, AI, cross-
platform input) behind ready-to-use primitives.
- Pragmatism over completeness — "worse is better": small, modular, adaptable.
- Legible to AI — favor high-level, semantic, hard-to-misuse APIs; consistent naming;
export through
src/xrblocks.ts. The SDK is meant to ground LLM code generation.
Contributing conventions
- TypeScript throughout; new public symbols must be re-exported from
src/xrblocks.ts.
- Tests are colocated
*.test.ts (Vitest): npm test.
npm run lint (ESLint) and npm run format (Prettier) before a PR.
- Local dev:
npm run dev (Rollup watch + http-server on :8080); npm run serve to just
serve. Build: npm run build. Addons build separately into build/addons/*.
Source: google/xrblocks — distributed by TomeVault.
1---2name: google-xrblocks-xrblocks3description: XR Blocks SDK4---56# XR Blocks SDK78XR Blocks (`import * as xb from 'xrblocks'`) is a cross-platform JavaScript/TypeScript9SDK for rapidly prototyping **AI + XR** apps. It is built on [three.js](https://threejs.org),10targets Chrome 136+ with WebXR on Android XR, and ships a **desktop simulator** so the11same code runs in a normal browser. The whole SDK is re-exported from12[`src/xrblocks.ts`](xrblocks.ts) — that barrel is the public API surface; if a symbol is13not exported there it is internal.1415> The single most important rule when generating code: **only call APIs that exist.**16> The framework's own evaluation found that hallucinated/inconsistent APIs are the #117> cause of broken generated apps. When unsure, grep `src/xrblocks.ts` and the relevant18> subfolder, or copy a pattern from a real file in `samples/`, `demos/`, or `templates/`.1920## Canonical app skeleton2122Every app is one or more `xb.Script` subclasses added before `xb.init()`. This is the23minimal, verified pattern (see [`templates/`](../templates) and the README for full HTML):2425```js26import * as THREE from 'three';27import * as xb from 'xrblocks';2829class MainScript extends xb.Script {30 init() {31 // called once; may be async32 this.add(new THREE.HemisphereLight(0xffffff, 0x666666, 3));33 const geometry = new THREE.CylinderGeometry(0.2, 0.2, 0.4, 32);34 const material = new THREE.MeshPhongMaterial({color: 0xffffff});35 this.player = new THREE.Mesh(geometry, material);36 // Place it in front of the user at a comfortable height/distance.37 this.player.position.set(0, xb.user.height - 0.5, -xb.user.objectDistance);38 this.add(this.player); // `this` is itself a THREE.Object3D39 }4041 onSelectEnd() {42 // desktop click OR XR pinch43 this.player.material.color.set(Math.random() * 0xffffff);44 }45}4647document.addEventListener('DOMContentLoaded', () => {48 xb.add(new MainScript()); // register the script49 xb.init(new xb.Options()); // boot the engine + render loop50});51```5253`Script` is a `THREE.Object3D`, so `this.add(obj)` puts things in the scene under it.54Do not manage the render loop, WebXR session, or camera yourself — `Core` owns them.5556## Mental model5758- **`Core` is a singleton** ([`core/Core.ts`](core/Core.ts)). `xb.init()` builds the59 renderer, camera, WebXR session, and every subsystem, then drives the frame loop.60 `Core` is also exposed as `xb.core`, with convenience aliases:61 `xb.scene`, `xb.user`, `xb.world`, `xb.ai`, `xb.depth`, `xb.sound`, `xb.input`,62 `xb.camera`, plus functions `xb.add()`, `xb.init()`, `xb.getDeltaTime()`,63 `xb.getElapsedTime()`.64- **`Script` is your extension point** ([`core/Script.ts`](core/Script.ts)) — Unity65 `MonoBehaviour`-style lifecycle hooks (below).66- **`Options` configures everything** ([`core/Options.ts`](core/Options.ts)) — one67 object with chainable `enable*()` methods and per-subsystem sub-options.68- The conceptual model from the papers is the **Reality Model**: `user`, `world`,69 and AI `agents` as first-class primitives, with an _interaction grammar_ that70 separates explicit events (`onSelectStart`, click, pinch) from implicit intent71 (gesture, gaze, voice).7273## Enabling features (`xb.Options`)7475These chainable methods **exist** — verified in [`core/Options.ts`](core/Options.ts):7677```js78const options = new xb.Options();79options.enableUI(); // spatial UI + reticles80options.enableReticles(); // pointing cursor81options.enableControllers(); // tracked controllers82options.enableHands(); // hand tracking (joints, pinch)83options.enableHandRays(); // visible rays from hands/controllers84options.enableGestures(); // pinch/fist/point/etc. (see input/gestures)85options.enableStrokes(); // $1 unistroke recognition86options.enableDepth(); // WebXR depth sensing + depth mesh87options.enablePlaneDetection(); // detected planes in xb.world88options.enableObjectDetection(); // object detection (also sets camera permission)89options.enableCamera('environment'); // passthrough device camera ('environment'|'user')90options.enableAI(); // Gemini/OpenAI via xb.ai91options.enableXRTransitions(); // fade transitions92options.enableVR(); // immersive-vr instead of immersive-ar93xb.init(options);94```9596**There is no `enablePhysics()` or `enableLighting()`** — these are configured directly:9798```js99import RAPIER from '@dimforge/rapier3d-simd-compat'; // physics engine100options.physics.RAPIER = RAPIER; // assigning RAPIER enables physics101// ...then implement initPhysics(physics) / physicsStep() in your Script.102```103104`options.formFactor = 'desktop'` autostarts the simulator; `?formFactor=desktop` in the105URL does the same. `options.catchScriptExceptions` (default `true`) keeps one buggy106script from crashing the app.107108## Script lifecycle hooks109110Override only what you need (all verified against `core/Script.ts` and `core/User.ts`):111112| Hook | When |113| ----------------------------------------------------- | ---------------------------------------------------------------------- |114| `init(deps?)` | once, after registration; may return a Promise; receives injected deps |115| `update(time?, frame?)` | every frame |116| `initPhysics(physics)` / `physicsStep()` | physics setup / per physics step |117| `onSelectStart(e)` / `onSelectEnd(e)` | pinch (XR) or click (desktop) |118| `onSqueezeStart(e)` / `onSqueezeEnd(e)` | grip button |119| `onKeyDown(e)` / `onKeyUp(e)` | keyboard (`e.code`) |120| `onXRSessionStarted(session?)` / `onXRSessionEnded()` | entering/leaving XR |121| `onSimulatorStarted()` | desktop simulator booted |122123**Object-targeted hooks** fire on the Script whose subtree was hit. Return `true` to mark124the event handled and stop it propagating to ancestors:125`onObjectSelectStart/End`, `onObjectTouchStart/Touching/End`,126`onObjectGrabStart/Grabbing/End`, `onHoverEnter/Hovering/Exit`.127128## Talking to the user, world, and AI129130```js131// User (xb.user — see core/User.ts)132xb.user.height;133xb.user.objectDistance;134xb.user.panelDistance;135xb.user.handedness;136xb.user.isSelecting(); // any controller pinching/clicking? (id optional)137xb.user.isSelectingAt(object); // is the user selecting this object/subtree?138xb.user.isPointingAt(object); // hover test139xb.user.getReticleTarget(0); // object under controller 0's reticle140xb.user.hands; // hand joints when enableHands()141142// AI (xb.ai — see ai/AI.ts). Requires a key (?key=... or keys.json) — guard it.143if (xb.ai.isAvailable()) {144 const res = await xb.ai.query({prompt: 'Write a haiku about dust.'});145 // multimodal: xb.ai.query({type: 'multiPart', parts: [{text}, {inlineData:{data,mimeType}}]})146 // res.text holds the answer; xb.ai.startLiveSession(config) for Gemini Live.147}148149// World (xb.world) — detected planes / objects / meshes after enabling detection.150// Depth (xb.depth), Sound (xb.sound: spatial audio, speech recog/synth).151```152153## Spatial UI (core)154155The core UI is a declarative grid built from `xb.SpatialPanel` (see156[`ui/layouts/SpatialPanel.ts`](ui/layouts/SpatialPanel.ts) and `templates/1_ui`):157158```js159const panel = new xb.SpatialPanel({160 backgroundColor: '#2b2b2baa',161 width: 2.5,162 height: 1.5,163});164panel.position.set(0, xb.user.height, -xb.user.panelDistance);165this.add(panel);166167const grid = panel.addGrid();168grid169 .addRow({weight: 0.7})170 .addText({text: 'Hello XR', fontColor: '#fff', fontSize: 0.08});171const button = grid172 .addRow({weight: 0.3})173 .addCol({weight: 1})174 .addIconButton({text: 'check_circle', fontSize: 0.5}); // `text` = Material icon name175button.onTriggered = () => console.log('clicked/pinched/touched'); // unified select176```177178`onTriggered` unifies click / pinch / touch on buttons. For flexbox-rich cards,179gradients, and shadows, use the **uiblocks addon** instead — see180[`addons/uiblocks/SKILL.md`](addons/uiblocks/SKILL.md) and "two UI systems" below.181182## Directory map (read deeper on demand)183184| Path | What lives there |185| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- |186| [`core/`](core) | `Core` singleton, `Script`, `Options`, `User`, DI `Registry`, `XRButton`, WebXR session mgmt |187| [`input/`](input) | controllers, hands, gaze, mouse, gamepad; `gestures/`; `strokes/` |188| [`world/`](world) | `World` + `planes/`, `mesh/`, `objects/` (Gemini & MediaPipe backends), `sounds/` |189| [`depth/`](depth) | depth sensing, depth mesh, `occlusion/` shaders & passes |190| [`ai/`](ai) | `AI` facade over `Gemini` + `OpenAI` (query / live / image gen) |191| [`agent/`](agent) | agent framework: tools, memory, context (WIP — see `agent/README.md`) |192| [`ui/`](ui) | core spatial UI: `SpatialPanel`, `Grid`/`Row`/`Col`, views, `ModelViewer`, `Reticle` |193| [`ux/`](ux) | `DragManager`, reusable interaction behaviors |194| [`simulator/`](simulator) | desktop XR simulator (virtual user/hands/depth/planes, control modes) |195| [`sound/`](sound) | spatial audio, speech recognizer/synthesizer (see `sound/README.md`) |196| [`physics/`](physics) | Rapier3D integration |197| [`lighting/`](lighting), [`camera/`](camera), [`video/`](video), [`stereo/`](stereo) | light estimation, device camera, video streams, stereo utils |198| [`utils/`](utils) | `ModelLoader`, dependency injection, helpers |199| [`addons/`](addons) | opt-in modules, each often with its own README/skills: `uiblocks`, `netblocks`, `testing`, `glasses`, `volumes`, `virtualkeyboard`, simulator UI, ... |200201## Two UI systems — pick deliberately202203- **Core UI** (`xb.SpatialPanel` + `.addGrid()/.addRow()/.addCol()/.addText()/.add*Button()`)204 — lightweight, no extra deps, good for HUDs, menus, and quick panels.205- **uiblocks addon** (`UICard`, `UIPanel`, `UIText`, `UIImage`, `UIIcon`) — full flexbox206 layout (`@pmndrs/uikit`), gradients, strokes, drop/inner shadows, and spatial behaviors.207 Import from `xrblocks/addons/uiblocks/src` and call `options.uikit.enable(uikit)`.208 See [`addons/uiblocks/SKILL.md`](addons/uiblocks/SKILL.md).209210Don't mix the two on the same panel, and don't import `UIPanel`/`UICard` from `xrblocks`211core — they only exist in the uiblocks addon.212213## Common hallucinated-API mistakes to avoid214215| ❌ Don't | ✅ Do |216| ----------------------------------------------------------- | ------------------------------------------------------------------------- |217| `options.enablePhysics()` | `options.physics.RAPIER = RAPIER` + implement `initPhysics()` |218| Use `xb.core.renderer` / `xb.core.physics` in a constructor | They're created during `xb.init()`; use them in/after `init()` |219| `new xb.UIPanel(...)` / `new xb.UICard(...)` | Those are the **uiblocks addon**; core uses `xb.SpatialPanel().addGrid()` |220| `xb.ai.query('text')` (bare string) | `xb.ai.query({prompt: 'text'})`, and guard with `xb.ai.isAvailable()` |221| Assume AI works with no key | Provide `?key=...` or `keys.json`; handle the unavailable case |222| `rgba()`/`hsla()` colors in UI | hex strings (`'#ffffff'`) or `THREE.Color` |223| Drive your own `requestAnimationFrame` loop | Put per-frame logic in `update(time, frame)` |224| Forget `xb.add(script)` before `xb.init()` | Register every Script first |225| Import bare `three` without the pinned importmap | Use the importmap from the README / a template |226227## Design principles (honor these when contributing)2282291. **Simplicity & readability** — a `Script` should read like a high-level description of230 the experience. Simple things stay simple; complex logic stays explicit.2312. **Creator experience first** — absorb incidental complexity (sensor fusion, AI, cross-232 platform input) behind ready-to-use primitives.2333. **Pragmatism over completeness** — "worse is better": small, modular, adaptable.2344. **Legible to AI** — favor high-level, semantic, hard-to-misuse APIs; consistent naming;235 export through `src/xrblocks.ts`. The SDK is meant to ground LLM code generation.236237## Contributing conventions238239- TypeScript throughout; new public symbols must be re-exported from `src/xrblocks.ts`.240- Tests are colocated `*.test.ts` (Vitest): `npm test`.241- `npm run lint` (ESLint) and `npm run format` (Prettier) before a PR.242- Local dev: `npm run dev` (Rollup watch + `http-server` on :8080); `npm run serve` to just243 serve. Build: `npm run build`. Addons build separately into `build/addons/*`.244245---246> Source: [google/xrblocks](https://github.com/google/xrblocks) — distributed by [TomeVault](https://tomevault.io).247<!-- tomevault:4.0:skill_md:2026-06-29 -->