# 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.