<!-- 04_VISUAL/xwui/xwui-lib/xwui-lib-basic/src/media/XWUISceneCanvas/XWUISceneCanvas.md -->
# XWUISceneCanvas

The host of an xw3d scene (plnx REF_11 U-2). It gives scene composers one API over the two backends of the xw3d engine family: **webgl** (primitive `parts` on a context leased from the page-wide manager) and **css3d** (live DOM `layers` in xw3d's CSS 3D stage). It owns no CSS 3D of its own, resolves part colours from XWUI tokens, keeps at most 2 live contexts on a phone and 4 on desktop, and shows a flat plan face when webgl cannot run.

It composes `_shared/engine3d` (lease, caps, eviction to a still, off-screen pause), `@exonware/xw3d/css3d` (`engineCamera`, `createCss3dStage`, the shared clock) and the viz tokens.

## Basic Usage

```example
file: examples/BasicUsage.ts
html: examples/BasicUsage.html
title: Basic Usage
description: Five parts on a leased webgl context, labels on the top ranks.
```

## Data

`parts[]` (webgl) or `layers[]` (css3d). With `backend: 'auto'` parts draw on webgl and layers on css3d; parts are never drawn by css3d. With both, parts win.

World model: x right, y DOWN, z toward the viewer, in px. Rotation is in degrees about the css axes, x then y then z. The host feeds the engine `(x, -y, z)` inside the group chain `engineCamera` describes.

| Part | Fields | Notes |
|---|---|---|
| `box` | `size {x,y,z}` | |
| `sphere` | `radius`, `segments?` | |
| `cylinder` | `radiusBottom`, `radiusTop?`, `height`, `segments?`, `axis?` | Axis `'z'` by default: an upright tower on a floor |
| `torus` | `radius`, `tube`, `radial?`, `tubular?`, `axis?` | Axis `'z'` by default: a ring lying flat on a floor |
| `plane` | `size {x,y}` | Faces z, double sided |
| `grid` | `size {x,y}`, `divisions? {x,y}` | A subdivided plane facing z |
| `line` | `points[]`, `width?` | Points are world px, `position` offsets all of them; rotation and scale are ignored |
| `instanced` | `shape`, `count`, `transforms[]`, `colours?`, plus the shape's dimensions (`size`, `radius`, `tube`, `height`) | One hidden template cloned `count` times; the part's own `position`, `rotation` and `scale` are ignored |

Every part also takes `id` (required, the diff key), `position`, `rotation`, `scale`, `tone`, `color`, `opacity`, `unlit`, `visible`, `label` and `labelRank`.

- **Colour** is by token. `tone: 'teal'` reads `--xwui-hue-teal`; `color` is a custom property (`'--xwui-scene-accent'`, `'var(--x)'`), a CSS colour or a number. Unset: `--xwui-scene-accent`. Tokens are resolved once per theme change and ride the node tint, so parts that differ only in colour share one material and batch into one draw.
- **Opacity and unlit** are material state: changing either rebuilds that node. A move, a recolour or a hide updates in place.
- `layers[]`: `{ id, z?, items: [{ id, element, point, billboard?, align? }] }`. `element` is a live DOM element the host owns; it is moved into the world, never cloned. An item that leaves the data leaves the world.

`setParts` diffs by id: the same list again sends nothing to the engine, a changed part touches only its node (an instanced part only the instances that changed).

## Options

| Option | Default | What it does |
|---|---|---|
| `backend` | `'auto'` | `'auto'`, `'webgl'` or `'css3d'` |
| `engine` | `'webgl'` | `'webgpu'` is opt-in and falls back to webgl |
| `modules` | `[]` | xw3d boot modules (`['trail', 'wideline']`); the default is the lean core |
| `camera` | identity | The starting logical camera; afterwards `setCamera` is the authority |
| `projection` | see below | `{ perspective: 1500, plane: 'wall', origin, anchor, fit, design }` |
| `quality` | `'full'` | `'balanced'` drops the fill light, `'low'` keeps ambient light only and halves the label cap |
| `priority` | `0` | Context priority under pressure; a higher one is never evicted for a lower one |
| `explicit` | `false` | The reader asked for 3D (a tap): allowed on a low-power device |
| `lowPower` | `false` | Host flag: treat the device as low-power for webgl |
| `reducedMotion` | the page | Override the page signal |
| `mirrored` | the page direction | Override the right-to-left mirroring |
| `face` | `'3d'` | `'2d'` shows only the flat face and releases the context |
| `flatFace` | `true` | Draw the flat plan face when webgl is unsupported or `face` is `'2d'` |
| `labels` | `{ max }` | Unset: 12 on phones, 40 on desktop (8 low-power), halved at `quality: 'low'` |
| `lighting` | `'lit'` | `'flat'` makes every part unlit |

`projection.origin` and `anchor` are percents of the stage from the visual top-left, authored left to right. `design` is the size the world is authored at; the scene scales to fit it (the C12 `scaleToFit`).

## Backend selection

| Condition | Result |
|---|---|
| parts, WebGL2 present, not low-power (or `explicit`) | webgl |
| parts, no WebGL2 | `unsupported` `no-webgl2` |
| parts, low-power device or `lowPower`, not `explicit` | `unsupported` `low-power` |
| parts, the cap is full and nothing may be evicted | `unsupported` `cap` |
| parts, the engine failed to boot | `unsupported` `boot-failed` |
| parts, the context was lost and did not come back | `unsupported` `context-lost` |
| layers only | css3d (no GPU context, always available with a document) |
| `face: '2d'` | the flat face alone |
| nothing to draw | idle |

Reduced motion changes motion, never the backend. Phones: antialiasing is off (4x MSAA cost 84 MB against 48 MB in the WP0 phone class), the device pixel ratio is capped at 1.5 by the manager, labels are capped at 12.

When webgl is unsupported the component sets `data-state="unsupported"`, fires `unsupported { reason }` and draws the **flat face**: the same parts seen straight on as plain SVG (a front view on a wall, a top-down map on a floor, yaw spins the map on a floor, pitch and perspective are ignored), painted with the same tokens, with the capped labels. It contains no CSS 3D. A composer shows this face or its own twin.

## Camera

The camera is the logical css camera `{ yaw, pitch, zoom, roll?, focusX?, focusY? }`. A right-to-left page is drawn mirrored (yaw, roll, origin, anchor) while the camera you pass stays logical.

On webgl the camera is `engineCamera`'s: a group chain reproducing `scale(zoom * fit) rotateZ(roll) rotateX(pitch) Ryaw translate(-focus)`, the eye `perspective` px over the origin, the field of view from the canvas height. Every point lands within 0.5 px of the stage's `project()`. With the perspective origin off the stage centre the canvas is **larger** than the stage and centred on that origin; the surface clips it. The host sets the canvas rect and the engine size itself.

## Events

`ready { backend }`, `unsupported { reason }`, `contextLost`, `evicted { still }`, `restored { backend }`. Subscribe with `instance.logic.on(type, (e) => e.detail)`. State decisions taken inside the constructor announce on a microtask, so a listener attached right after `new` hears them.

Eviction: when another scene needs the context this one becomes a captured still (same box, no layout shift), the engine is disposed with `loseContext`, and the phase is `evicted`. When the scene is visible again it takes a fresh context, rebuilds from the data and fires `restored`. A context the browser drops (`contextLost`) is retried once the same way.

## Methods

| Method | What it does |
|---|---|
| `setParts(parts)`, `setLayers(layers)` | Replace the data (diffed by id) |
| `updateConfig(patch)`, `updateData(patch)` | Standard; a change of backend, engine, modules, priority, quality, lighting, face or explicit builds a new surface, anything else updates in place |
| `setCamera(camera, { animate?, easing?, fit? })` | Move the camera. Returns a promise that resolves when it arrives or is superseded. `animate` eases on the shared clock; reduced motion snaps |
| `getCamera()` | The logical camera now |
| `render()` | Draw one frame on the next animation frame (webgl). Idle scenes draw nothing |
| `onFrame((dtSeconds, nowMs) => void)` | Run a handler every frame while on screen; returns the unsubscribe. Never runs under reduced motion |
| `pick(x, y)` | What is under a surface point (px from its top-left): `{ id, instance?, backend }` or null. webgl casts a ray (occlusion exact); css3d reads the element |
| `project(point)` | Where a world point is drawn now (px from the surface top-left), on both backends |
| `pin(el, point)`, `unpin(el)` | Pin a 2D element on a world point; it follows every camera move |
| `capture()` | The last frame as RGBA8 (webgl; an evicted scene returns its still); null on css3d |
| `refreshTheme()` | Resolve every colour again (also runs on a theme change of the page root) |
| `getBackend()`, `getPhase()` | `'webgl' | 'css3d' | null`; `'idle' | 'loading' | 'ready' | 'evicted' | 'lost' | 'unsupported'` |
| `getStage()`, `getLayerElement(id)` | The xw3d css3d stage and a world layer (css3d backend only; null on webgl, whose camera is the engine's) |
| `getCanvas()`, `getSurfaceElement()`, `getElement()` | The leased canvas, the clipped box, the root |
| `destroy()` | Releases the lease, the stage, the clock subscriptions; idempotent |

## Labels

Parts with a `label` are labelled in the overlay, but only the top n by `labelRank` (then authoring order). The set depends on data only, so a camera move re-projects the same elements and creates and removes none.

The overlay (`pin`, labels) is this component's own `PinLayer`, not the stage's `pin()`. A pinned element stays a child of `.xwui-scene-canvas__pins` and is written once (`slot` attribute, static `transform: translate(-50%, -50%)`); the per-frame position is one 2D `transform` on a carrier in the layer's shadow root that the element is slotted into, so a camera change never restyles a page-styled element. A z-index (the depth rank, on the carrier) is rewritten only when two visible pins that may overlap are stacked against their depth; the behind class only when it flips. Measured on the WP0 phone class (Chrome, CPU 4x), C12 stage with 12 pins on the full XWUI sheets: 9.3 ms median, 11.5 ms p95 per camera change with inline writes on the pins, 2.0 ms median, 2.7 ms p95 with the carriers. Read a pin's drawn spot with `project(point)`, never from the element's own style.

## Accessibility and RTL

The surface and the flat face are `aria-hidden`; the composer owns the accessible twin, the focusable group and its summary. Nothing in the component takes focus. Direction comes from the parent `dir`; the surface is pinned to `ltr` because stage pixels are physical, and xw3d mirrors the scene itself. Label text is model text (`textContent`, `dir="auto"`), never HTML.

## Packaging

Imports `@exonware/xw3d/css3d`, which xwui basic and super bundle at build time (view projection is synchronous). The component vitest run and the type-check resolve it from the xw3d checkout; the vite aliases belong to WP7. The webgl engine is a runtime load from `<xwui base>vendor/xw3d/build/`.

```api
```
