<!-- 04_VISUAL/xwui/xwui-lib/xwui-lib-super/src/layout/XWUIPlaneStage/XWUIPlaneStage.md -->
# XWUIPlaneStage

Typed objects on one floor plane drawn by xw3d (plnx REF_06 C13, REF_11 WP3-B): extruded grid cells, scatter markers with stems, ghosts and
threshold planes, site boxes and zones, towers with health rings, districts and roads with a minimap and fly-to, category
columns whose series are solid, hatched, outline or glass against a reference plane, state tiles on a wall or floor,
billboard cards, and dots on a phyllotaxis disc. The camera, the 2D face and the motion policy are XWUISceneStage (C12):
this component composes it into one view tree, and its world is C12's xw3d surface: every kind is an instance of one
shape-and-look part on a leased WebGL context, so the floor costs 6 to 14 draws whatever the object count. Name tags are
placed without overlap every frame and capped by device.

## Basic Usage

```example
file: examples/BasicUsage.ts
html: examples/BasicUsage.html
title: Basic Usage
description: A 3 x 3 grid of extruded cells, one marker and a threshold plane.
```

```example
file: examples/PlanCity.ts
html: examples/PlanCity.html
title: Plan City
description: The PX-Space plan space: towers, health rings, districts, roads, minimap and Present.
```

## Data

| Field | Shape | Meaning |
| --- | --- | --- |
| `objects` | `XWUIPlaneObject[]` | In reading order. Every object: `id, kind, x, y` and optional `label, hue, selected, dimmed, ghost, badge { text, tone }, value, state, ariaLabel`. |
| `zones` | `{ id, x, y, w, d, label?, hue? }[]` | Floor plates (districts, site zones, units), in the arrangement's unit. |
| `links` | `{ from, to, style?, flow?, state? }[]` | `road`: a street route on the floor (PX-Space). `wire`: straight, at both ends' lift. `beam`: straight, top to top. `flow` adds moving dashes; `state` warn and error change the dash (never colour alone). |
| `planes` | `{ z, label?, tone? }[]` | Horizontal reference or threshold planes, in height units. Column series above the highest one glow. |
| `axes`, `captions`, `grid` | | Floor axis titles, floor captions at positions, the cell grid of `grid` objects. |
| `selectedId` | string or null | The selected object; set by a pick. |
| `summary` | string | One sentence for the scene (read with the 3D face, shown with the twin). Unset: a count. |
| `status`, `statusText` | | `loading`, `empty`, `error` (C12 states). No objects shows the empty state. |
| `view`, `camera` | | Face (`3d`, `2d`, unset = policy) and a camera to ease to. |

Kinds and their own fields: `cell { height }`, `marker { z, size, stem, ghostOf }`, `tower { height, ring, href, size }`,
`box { w, d, h, pattern }`, `column { series: { id, value, style, hue?, label? }[] }`, `tile { column, stateLabel }`,
`billboard { title, kicker, icon }`, `dot { size }`. Bad input is dropped, never thrown: an object with no id, a repeated
id (the first wins), an unknown state (neutral), a link to an unknown object or to itself; a hue that is not a token NAME
is ignored (it can never inject CSS); a tower `href` must be relative or http(s).

## Options

| Option | Default | What it does |
| --- | --- | --- |
| `arrangement` | `'grid'` | `grid`, `wall`, `floor`: x, y are cells, centred on the origin. `scatter`, `site`, `map`: world units times `unit`, absolute. `disc`: a phyllotaxis spiral by index (golden angle 2.39996, radius `sqrt((i + 0.5) / n) * discRadius`). Also picks the camera defaults below. |
| `unit` | 64 for cells, 96 on a wall or floor, else 1 | World px per data unit. |
| `heightScale` | tallest object, px by arrangement, linear | `{ max, px, curve: 'linear' or 'sqrt', minPx }`. The city: `{ max: 120, px: 264, curve: 'sqrt', minPx: 14 }`. |
| `footprint` | by arrangement | Footprint of a cell, tower, tile or column, px. |
| `roadOffset` | `75, 67` | Street offsets of road links (PX-Space grid). |
| `discRadius` | 254 | Radius of the disc. |
| `flatten` | false | Heights fall to the floor (tweened) and the camera looks as straight down as its range allows. |
| `minimap` | false | A minimap of the floor with the camera eye; a click picks the nearest object and flies there. |
| `labels`, `topN` | `'all'`, 8 | `all`, `selected`, `top-n` (the N tallest), `none`. Selected and hovered objects always keep their tag. |
| `present`, `presentControl`, `presentSpeed` | false, false, 3.6 | Present mode: an auto orbit (degrees per second) with a visible Stop; the toolbar switch. Off under reduced motion (the mode stays, the camera does not move). A drag hands the camera back. |
| `transitionMs` | 900 | Height tween; 0 under reduced motion. |
| `flyOnSelect` | true for `map`, `site` | Centre the camera on a picked object. |
| `floor` | true | The lit grid floor. |
| `tintStrength` | `'normal'` | How much of its hue a plane, zone or glass body shows. `'soft'`: washes at 0.06 (floor, zones, threshold planes) and glass bodies at 0.2. `'normal'`: the look the stage always had (0.12 and 0.32; the scene and markup are unchanged). `'deep'`: washes at 0.24 and glass bodies at 0.55. The dim and ghost bodies follow (0.12 and 0.22 soft, 0.32 and 0.55 deep). Solid bodies, lines, edges and the selection keep their look: a solid tower keeps its light to read as a form. The hue fill of the selected tag and the tile cards scales with it (x0.6, x1, x1.4; the scene ink keeps 4.5:1 on every hue). The draw budget is unchanged (same tiers). A bad value is `'normal'`. Example: `{ tintStrength: 'deep' }`. |
| `engine` | `'auto'` | The xw3d backend: `auto` is `webgl`. `css3d` is honoured for the `wall` arrangement when every object is a tile or a billboard (live cards, no GPU context); any other content stays on webgl. Replaces the old `renderer: 'css3d'`. When webgl cannot run (no WebGL2, the context cap, a failed boot, a lost context) the flat twin is the face and `onUnsupported(reason)` fires; a low-power device shows the twin first and a tap on the 3D switch asks for 3D. |
| `camera` | by arrangement | Any XWUISceneStage option over the defaults (`mode, plane, rest, range, perspective, origin, anchor, easing, keyboardStep, presets, scaleToFit, fit, minBlockSize, touch, twinDefault, toolbar, quality`). `camera: { fit: 'fill' }` makes the stage take its host's whole box (the floor keeps its ratio by scale). See XWUISceneStage.md, "Sizing: design and fill". |
| `ariaLabel`, `twinLabel`, `emptyText`, `locale`, `numberingSystem`, `className` | locale pack | Names, empty copy, digits. |

Camera defaults (prototype values): `map` PX-Space (drag orbit, yaw -30, pitch 56, zoom 0.66, perspective 1500 at 50% 28%,
pitch 28 to 78, zoom 0.5 to 1.9, full toolbar); `grid` PX-Risks; `scatter` PX-Portfolio (1400 at 50% 22%); `site` PX-Assets
(90 degree key steps); `wall` Web-Compare (pointer tilt on a standing wall); `floor`; `disc` Web-Pricing (pitch 62, yaw -16,
+/-17 and +/-8). The design box for scale-to-fit follows the arrangement's zoom, so a host `rest.zoom` really zooms.

## Events and methods

Events (`on<Event>(handler)` or `logic.on(name)`): `select { id }` (the selection changed), `activate { id, href? }` (every
pick, also of the selected object; `href` from a tower), `hover { id | null }`, `presentChange { present }`, plus the camera
through `onCameraChange(camera)` and `onViewChange(view)`.

Methods: `getCanvas()` (the XWUISceneCanvas: `pick`, `capture`, `project`), `onUnsupported(handler)`, `getPinElement(id)`, `select(id)`, `getSelectedId()`, `flyTo(id)`, `setCamera(camera, { animate? })`, `getCamera()`, `command(name)`,
`setPresent(on)`, `isPresenting()`, `setView(view)`, `toggleView()`, `getView()`, `updateData()`, `updateConfig()`,
`getElement()`, `getViewport()`, `destroy()`; static `projectView(data, config)` (a still picture at the rest pose).
`logic.scene` is the C12 camera logic.

## Accessibility and RTL

- The surface is a decorative picture (`aria-hidden`). The objects are a labelled `listbox` in the overlay; each is an `option`
  with a name in words ("Operating plan FY2027, At risk, SAR 86.0M, 4 dependents"), `aria-selected`, `aria-posinset` and one
  roving tab stop. The options are visually hidden focus proxies (clipped, not `display: none`): a keyboard focus is drawn as a
  ring pinned on the object, a pointer picks through xw3d. Arrows walk the objects in READING order (data order: the camera
  turns the floor, so screen directions lie), Home and End jump, Enter and Space pick, `+`, `-` and `0` move the camera from a
  focused object, Escape stops present mode. The focused stage viewport keeps the C12 camera keys.
- The flat twin (2D face; default under reduced motion, below 560 px and whenever webgl cannot run) lists every object as a
  pressed-state button grouped by zone (or wall column) with its status, value and badge, then the links in words. Objects exist
  only on the 3D face and the twin only on the 2D face.
- State is never colour alone: every name tag carries a state glyph, tiles a glyph and a word, links dash by state (warn long,
  error short), a ghost tower keeps a dashed outline, the selection is a dashed outline with a beam, series over the
  reference plane get a heavy rim, and the twin says the numbers. Series styles differ by shape: solid, glass, hatched (glass
  plus edges) and outline (edges only).
- Tint strength changes only how much hue fills a plane or a card, never a cue: the tag and card ink stays at 4.5:1 at every strength (measured on all 17 hues), and the shape cues above do not depend on it.
- Pointer: hover tilt and drag orbit per C12 (a finger scrolls the page); hover is the nearest projected footprint (xw3d
  `hitTest`), a click is the engine's ray (occlusion exact) with the footprints as the fallback; the click that ends a drag is
  not a pick.
- Reduced motion: no height tween, no present orbit, no easing; the twin is the default face.
- RTL: direction from the parent `dir`. A `map` keeps its authored view (AR-PX-Space; `camera.mirrorInRtl: 'none'`), the other
  arrangements mirror the camera (xw3d draws yaw -35 as 35); the surface never mirrors by itself; horizontal arrows swap so
  forward is to the left; text on the floor and the twin is `dir="auto"`; the minimap panel sits at the inline start and draws
  its map left to right.

## Performance

The floor is `plane/<shape>/<look>` parts (box, sphere, cylinder, torus, plane; solid, dim, ghost, glass, wash, lines, edge,
selection), each ONE instanced draw: 6 draws for 100 towers with rings and 60 roads, 6 for 560 mixed objects, at most 13 in
the worst mix (a test holds it at 14). The camera never re-projects the view: a frame goes to xw3d's engine camera and to the
tag placement. Hover, focus and a height tween step patch the scene state silently and reach the surface only. Tags are
capped by device (12 on a narrow page, 40 otherwise, halved at `quality: 'low'`); selected and hovered objects keep theirs.
The overlay costs what pinned DOM costs: measure with only the component sheets loaded, not components-all.css (which tripled
a phone-class camera frame in the tester). Numbers are in `docs/changes/CHANGE_20261023_120000_000_PLANE_STAGE_XW3D.md`.

## Provenance and limits

PROMOTED from the app-owned `03_BASE/xwflow/xwflow-web/src/components/XWUIIsoStage` (2,463 lines; tiles x, y, z, tone,
state, links with state, text twin, roving focus). Kept as the regression floor and ported into the calc tests: the
bad-input rules, reading-order navigation that stops at the ends, the summary template, the state-is-news-not-identity
split (`hue` vs `state`), the per-link tilted plane through both lifted ends (`planeLinkPlane`, now in px), and the
listbox of options with one tab stop. Replaced: the fixed `rotateX`/`rotateZ` plane by the C12 camera and then by xw3d;
tones by hue token names; `xwui-icon` glyphs by constant SVG; tooltips by projected name tags. Absorbs P-G04
`XWUIPlaneScene`, W:OrbitStage, C-G11 `XWUITowerMap`, C-G3 `XWUIDepthColumns`, W:TileWall.

Not drawn yet: the moving dashes of a `flow` link (xw3d `trail`) and the hatch texture of a `hatched` series or `pattern`
(WP1-C); dashes are real short bars today, so xw3d's `wideline` dash is not needed. The old CSS 3D view and sheet are set
aside beside the files (`*.set-aside-20261023`).

```api
```
