Gunnar

nordisk/facett

.nornir/facett-demo-reasoning.md

# facett-demo — the showcase app

`facett-demo` is **one eframe (glow) binary that exercises every facett
component** with fake-but-realistic data, switchable across **every** palette in
`Theme::ALL`. It is the living "everything works together" proof and the D4
compliance check: if a component cannot be slotted into this one window, that is
a flag (see *Compliance* below), not a silent omission.

It is the gallery a reviewer opens to see the whole library at once, and the
acceptance artifact (`tests/snapshots/facett_demo.png`) a CI run produces.

### It is the reference driver for the look & feel (`look::Theme`)

facett-demo is now the **reference app that drives the unified `look::Theme`**
(`.nornir/look-and-feel.md`). The top toolbar carries a **preset switcher** over
`LookTheme::preset_names()` — Windows L/D, macOS L/D, Device — and `draw_app`
calls `DemoState::apply_look(ctx)` → `LookTheme::apply(ctx)` every frame:

- `src/main.rs` starts on `LookTheme::from_os(ctx.os())` (Linux → windows-dark).
- `DemoState { look_preset: usize }` + `look_theme()` / `set_look_preset(i)` /
  `apply_look(ctx)`.
- `state_json["look"] = { preset, preset_index, presets, mode }`.

`apply` installs the full `egui::Style` AND publishes the derived legacy palette,
so the legacy per-facet palette deck (`deck.palette_picker`) still works while the
new look re-themes every component coherently (COH-1). Inject-assert test:
`look_switcher_drives_every_preset_and_publishes_coherent_palette` drives every
preset and asserts the published palette name matches the active preset.

```
cargo run -p facett-demo           # the live window
cargo test -p facett-demo          # self-check + snapshot + mega matrix + robot drive
cargo test -p facett-demo --test robot_demo -- --nocapture   # D5b2: drive the whole app
```

## What it shows

A single tabbed `FacetDeck` holds **56 `Facet` components** (the deck vector in
`FacetDeck::new(...)` in `src/lib.rs` — that vector plus the coverage-gated
`LEGEND` registry are the **single source of truth**; the self-check asserts
`facets.len() == 56`), plus a non-`Facet` command menu overlay (`facett-menu`
`CommandPalette`, `Ctrl+P`) and an about/legend side panel.

Rather than duplicate the 56-row registry here (it would rot — `LEGEND` in
`src/lib.rs` carries the authoritative one-line blurb per tab), the deck spans
these families:

- **Graphs / DAGs** — `graph`, `graphview`, `Graphs` (the graph navigator over the
  Swedish 11M population graph), `metro`, `depgraph`, `graphpan`, `decograph`,
  `plandag` (Spark plan DAG), `pipeline` + `synthetic-sdp`, `SDP`/`brokkr` editors.
- **3-D** — `graph3d`, `rabbit3d`, `avocado3d` (glTF), `OSM 3D` (extruded city),
  `helix` / `dna` / `timehelix` (parametric time-axis / genome / vortex scenes).
- **Maps** — `map` (shared by `facett-osm`), `geomap`, `OSM 2D` (slippy GeoParquet),
  `OSM 3D` — all over the ONE `facett-map::MapLayer` trait + renderer.
- **Data** — `grid`, `table`, `warehouse`, `card`, `form`, `matrix`, `icebergview`
  (Iceberg time-travel), `warehousedeck` (multi-pane lakehouse WALL).
- **Charts** — `plot`, `boxplot`, `violin`, `heatmap`, `treemap`, `candles`, `stat`,
  `syschart`, `timeline` (swimlane Gantt), `pop` (ingest progress).
- **App-framework (facett-app)** — `nav`, `shell`, `overlay`, `widgets`,
  `primitives` (Fas 1–4 backbone: NavModel + router + AppShell + overlays + widgets).
- **Adopted panes** (lifted whole from nornir's viz) — `log`, `console`, `video`,
  `Git`, `Open File`, `docview`, `security`, `jobs`, `tabbar`, `Velo L1`.

`facett-arrow` is exercised as the RecordBatch builder that feeds grid/table; the
`effects` crate (RavenSprite / glow) is exercised through the deck fx (the **glow**
toggle and **send-raven** button).

## The one fake dataset (`src/data.rs`)

Everything is generated from a small, **deterministic** (no RNG, no files) "fleet
/ issues" world so the self-check test can assert exact counts and values:

- **48 vessels** (region, status, crew, score, progress, 7-point trend, position)
  → an Arrow `RecordBatch` feeding **grid** + **table**, a **card**/**form** for
  one vessel, and **pop** progress rows.
- an **8-node service DAG** (`Ingest → Decode → … → Audit`) → **graph**,
  **graph3d**, **depgraph**, **pipeline**.
- **telemetry series** (per-region throughput) → **plot** and the **plotters**
  family.
- the vessels' **lon/lat** → **map**, and (with per-region **hotspots**) the
  scrollable/zoomable **geomap**.
- the same 8-node DAG laid out as free-positioned **peers with event badges** →
  **syschart**.
- **two Iceberg snapshots** of the fleet table (an older half-fleet `#100`, the
  full-fleet `#200`) → **icebergview** time-travel + paging.

## Live UI (`src/main.rs` + `DemoState::draw_app`)

The entire toolbar + deck UI now lives in **`DemoState::draw_app(ui)`** (in
`src/lib.rs`), and `src/main.rs` is a thin eframe shell that just calls it. This
is deliberate: the **eframe binary and the D5b2 robot-UI test render the exact
same UI** (CLI/GUI parity), so the robot drives precisely what a human sees. The
shared UI offers:

- a **palette switcher** (`FacetDeck::palette_picker`) over `Theme::ALL` — the
  whole deck and every facet re-theme live;
- a **glow** toggle and **send-raven** button (deck fx);
- a **command palette** (facett-menu, `Ctrl+P`) whose invocations route through
  `DemoState::run_command` — the single command sink the toolbar buttons share;
- an **about / legend** toggle;
- a **node-select toolbar** (labelled `node:<id>`) shown when the **syschart** tab
  is active, so the chart's selection is robot-addressable (its own canvas
  click-select is position-based).

## Auto-demo / attract mode (#82) + the responsive hamburger menu (#39)

**Auto-demo (attract / kiosk loop).** `facett-demo/src/attract.rs` holds a pure,
`Copy` `AttractMode` state machine: after `IDLE_ARM_SECS` (5 s) of no interaction it
**arms**, then auto-advances through a `TOUR` of showcase tabs, dwelling `DWELL_SECS`
(5 s) each; **any** interaction cancels it and re-arms the idle timer. `draw_app` pumps
`update(dt, interacted) -> AttractAction` (gated on `attract_enabled && attract.running()`)
and draws an on-screen chip — *"auto-demo · move to take control · {tab}"*. Public API:
`new`/`running`/`idle_secs`/`dwell_secs`/`tour_index`/`current_tab`/`update`/`interrupt`/
`state_json`; the live state rides in `state_json.attract`. (Distinct from
`MapFacet::with_auto_rotate`, the 3D-camera idle spin in `.nornir/idle-auto-rotate.md`.)

**Hamburger main-menu drawer (responsive fold).** The deck's tab bar *is* the demo's
MAIN menu (60+ facets). On a **phone-class viewport** `draw_app` drives the device class
into the deck — `self.deck.set_menu_collapsed(phone)` — so the wide tab bar collapses to
a `≡` **hamburger + drawer** (canvas-first); tablet/desktop keep the inline wrapped bar.
The per-frame `ShellReport` (device class · `controls_folded` · canvas fractions) is the
observable data in `state_json.shell`, so the fold is robot-assertable. (This is the deck
menu's fold; the *scene* shell's own activity-rail → drawer fold is the separate
`facett-app` `scene` layer, `.nornir/scene-design.md`.)

## The per-frame lag fix (the "stutter on a 5080" bug)

`facett-demo` was reported to **lag / stutter** on a discrete GPU + 12 cores while
the sibling `sigtuna_demo` was buttery smooth. A 5080 has no business dropping
frames on a map demo, so the delta between the two binaries WAS the bug.

**Root cause — the per-frame full-deck state dump.** `main.rs` called
`DemoState::dump_state()` **every native frame**. `dump_state` builds the WHOLE
~27-facet deck's aggregated `state_json()` (a deep serde tree) and `fs::write`s it
to disk each frame. That tree is **not static**: the live `sim` accrues one Iceberg
snapshot (and grows the warehouse table) on every tick, so the per-frame serialise
grows without bound — the classic "starts okay, then stutters" shape. Measured (the
`demo_frame` test): the serialised tree is **≈ 112 KB and rising**, and building +
serialising it costs **≈ 160 ms/frame in a debug build** — *plus* a full-file
`fs::write` (fsync) — paid unconditionally every frame while the GPU idled. That is
CPU-side frame stall, not a GPU limit; `sigtuna_demo` never did it (it only reads
`map.state_json()` for the on-screen panel).

**Fix (L2/L4).** `dump_state` is a **robot-UI channel seam**, not interactive work.
`DemoApp` now only pays it when an external driver actually asked for the channel
(`$FACETT_DEMO_STATE` set); a human interactive run leaves it unset → **zero
per-frame serialise + disk write** → the frame is GPU-bound again. The channel (and
every `control_matrix` / robot test that drives it in-process) is unchanged.

**Measured smoothness, as data.** `DemoState` now carries a `FrameClock` (an EWMA of
egui's `stable_dt`), surfaced in the toolbar as `x.x ms · yyy fps` (green ≤ 16.6 ms)
and in `state_json.perf` (`ms_per_frame` / `fps` / `smooth_60fps`, testmatrix-gated).
The `facett.demo_frame` bench arm (dense town, street-level pose, the full CPU paint
pipeline) is the frame budget the Loki bencher re-runs every push to guard against a
regression back into per-frame stutter.

## The street-level tour (parity with `sigtuna_demo`)

The user also reported the OSM-3D tab "has a similar tour but the walk **doesn't go
down to street level**." It auto-played the framed **flythrough**, which orbits the
city HIGH and never lands. `osm3d_view` now flies the **same shared
`facett_map3d::SigtunaTour`** the `sigtuna_demo` bin does (air-circle so you SEE THE
HOUSES → descend → drive the streets), reusing the ONE street picker
(`osm_tabs::street_route`) and the town-core crop (`crop_to_town_core`) — L5, no
second tour, no forked picker. The crop is load-bearing: on the raw 16 km clip the
huge metres-per-design-unit pushes the camera's hard min-distance floor **~480 m**
over the road (you never get down among the houses); cropping to the dense core drops
metres-per-unit to **~481**, so the drive reaches a real **~30 m** street-level chase.
Proof (`osm3d_street_tour`, RED-when-broken): the tab walks circle → descend → drive,
descends bird → street (**air ≈ 514 m ≫ street ≈ 30 m**), and **2473 house verts are
in frame at the circle**. The camera-framing/​z-fight unit tests disable the
auto-pilot (`set_tour(false)`) exactly as they already did for the flythrough.

## Headless heart (`src/lib.rs`)

`DemoState::new()` builds the entire app — the populated `FacetDeck` (all 56
`Facet`s), the menu, and the fleet — **without a window**, so the self-check test
constructs it directly. `DemoState::state_json()` folds in the deck's `state_json`
(every facet + the caps map), the menu, and the legend.

Two host-driving helpers back the robot row: `run_command(id)` (the shared command
sink) and `select_syschart_node(id)` (forwards a selection to the boxed
`SystemChart` via the deck's typed `facet_mut::<T>(title)` — see *as_any_mut*
below). `facet_mut` works because `SystemChart` and `IcebergView` opt into the new
defaulted `Facet::as_any_mut` (everything else keeps the `None` default), giving a
host typed access to a concrete component inside a `FacetDeck`.

## Laws covered

- **(1 + 6) self-check** — `demo_state_has_every_component_populated` asserts
  every component is *present and populated* with the fake data via `state_json`
  (real counts/values: **56 facets** (asserted `facets.len() == 56`), 48 grid/table
  rows, per-region plot series, non-zero pop progress, one caps entry per facet,
  the demo command set, 8 syschart nodes, geomap points, 2 iceberg snapshots), not
  "didn't panic".
  `deck_cycles_through_every_palette` walks all of `Theme::ALL` and wraps.
- **(1) D5b1 static mega matrix** — `tests/mega_matrix.rs` crosses every component
  × data × palette (**58 components × 2 data × 7 palettes** — the 57 `MatrixCell`s
  plus the menu cell) and a coverage gate forces it to track the demo's `LEGEND`.
  See `.nornir/mega-matrix.md`.
- **(1) D5b2 robot-UI live drive** — `tests/robot_demo.rs` fires up the **whole
  demo headless** (over `DemoState::draw_app`, the same path as the binary) and
  **drives every tab + every component's controls** (theme picker; grid/table
  scroll; syschart node-select; geomap pan/zoom; icebergview pager + snapshot
  time-travel; menu open/Escape), asserting each component's `state_json` responds.
  Every step is one `robot-ui` matrix row (the run gates on **every** row green),
  printed as a grid and persisted to `target/robot-matrix.jsonl`. A fidelity test proves a
  wrong expectation is a red cell, not a panic. See `.nornir/mega-matrix.md` (D5b2).
- **(4) screenshot proof** — `tests/snapshot.rs` renders the populated, themed
  (nordic-aurora + glow) demo with `egui_kittest` (wgpu / lavapipe) to the golden
  `tests/snapshots/facett_demo.png`, asserting it is non-trivial (> 5 KB). Uses
  `run_steps` (not `run`) because the glow + 3D spin request a repaint every
  frame by design and never reach a steady state.
- **(2) this doc** — design + guide, in-repo.

## Compliance (D4 flags)

Every component slots in **without a flag** — including `syschart`, `geomap` and
`icebergview`, which were added as their own tabs so the demo is the **full
robot-drivable showcase** (D5b2). Notes on *how* a few are exercised rather than
re-implemented:

- **osm** now has its **own** tabs — `OSM 2D` (`facett-geomap::OsmView`, a slippy
  GeoParquet viewer) and `OSM 3D` (`facett-map3d::Map3D`, an extruded lit city) —
  in addition to `facett-osm` still yielding the same shared `MapView` as
  `facett-map` on the **map** tab. All maps ride the ONE `facett-map::MapLayer`
  trait + renderer, so this is layer selection, not re-implementation.
- **menu** (facett-menu `CommandPalette`) is an **overlay**, not a `Facet` — it is
  not tabbed into the deck but driven on top (toolbar button / `Ctrl+P`), which is
  its intended host integration.

No component had to be omitted.

### Robot-UI driver caveat

D5b2 is the documented `nornir-robotui` consumer, but **robotui 0.1.0 pins egui
0.33** (it serves nornir's egui-0.33 viz) while facett is **egui 0.34** — two egui
majors cannot share an `egui_kittest::Harness`. So `tests/robot_demo.rs` carries a
byte-faithful in-crate port of robotui's API over egui_kittest 0.34; swapping to
the published crate is a one-line `use` once robotui ships a 0.34 build (the dep is
listed, commented, in `facett-demo/Cargo.toml`). The only fix robotui needs is the
egui-0.34 `Event::MouseWheel { phase }` field.