Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

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:

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:

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:

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:

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::writes 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 Facets), 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

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:

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.