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

nordisk/facett

facett-demo/src/pop_tab.rs

//! **The `Sverige · Population` tab** — the showstopper. The Swedish
//! 11-million-person population graph (the canonical `knut-popsim` model) rendered
//! as **three beautiful, linked graphs side by side**, driven by a live query: a
//! scale field (1M → 100M), per-label + per-relation toggles, and a colour-by mode.
//!
//! ## The three linked views (one population, three lenses)
//! - **Full** — the population as a force-clustered hub-and-spoke web (people grid
//!   by kommun, entities orbiting), LOD-sampled to a surfaced [`pop_model::RENDER_CAP`].
//! - **Debt** — the enforcement sub-graph: Person → OWES → Debt → ESCALATES_TO →
//!   KfmCase, flowing left→right.
//! - **Kommun** — the per-kommun aggregation (one node per kommun on its real
//!   geographic position, sized by population) — the **full-scale** meta view, what
//!   we show at 11M/100M instead of 11M dots.
//!
//! ## Beauty (the whole point)
//! Each view renders through the shared `facett_core::render` **L0 SDF kernel** (the
//! same kernel the maps + `graphview` draw through), but lowered with a **glow lane**:
//! a soft [`RingInstance`] halo behind every node (sized by the node's marker), so the
//! graph reads as luminous orbs over a dark ground — depth + glow with zero GPU
//! dependency (pure CPU SDF, so it lights up identically native AND in the wasm demo).
//! Colours come from the 12-label register palette / a per-kommun wheel / a cool→hot
//! debt-heat scale (`pop_model`).
//!
//! ## Honest about scale (LAW)
//! You cannot draw 100M nodes (≈400M with entities) — that OOMs, and wasm has a hard
//! memory ceiling. So we **generate** only [`pop_model::GEN_CAP`] people, **render**
//! at most [`pop_model::RENDER_CAP`] nodes, and report the requested-scale STATS
//! analytically. The visible-vs-total split is shown on the tab AND in `state_json`
//! ("rendering N of 11,000,000 — aggregated by kommun"). On wasm the same caps apply.
//!
//! ## Observable (LAW)
//! `state_json` carries the scale, the analytic stats, the active filters, the colour
//! mode, the visible/total split per view, and each view's lit-pixel oracle — so the
//! matrix + an agent see exactly what the user sees, as data. Every view also renders
//! to a straight-RGBA [`Frame`] a test reads back to assert a non-blank picture.

use facett::Facet;
use facett::core::FacetCaps;
use facett_core::render::Frame;
use facett_graphview::{Camera, Decorations, GraphModel};

use crate::pop_beauty::{self, PopClock, SLATE_BG, Selection};
use crate::pop_model::{
    self, CANONICAL_PERSONS, ColorMode, PopQuery, PopStats, PopView, SCALE_MAX, SCALE_MIN,
};
use knut_popsim::{LABELS, PopGraph};

/// Re-export of the LOD render cap so callers can assert the visible-node budget.
pub use crate::pop_model::RENDER_CAP;

/// The stable tab title (deck / discovery-contract key + legend key).
pub const TITLE: &str = "Sverige";

/// The matte-slate scene ground (`#0A0B0E`, the aesthetic spec) the neon population
/// orbs glow over — re-exported from the beauty layer so the test asserts the corner.
pub const BG: [u8; 4] = SLATE_BG;

/// Off-screen render size per linked view pane (kept modest so three panes stay
/// snappy on the CPU SDF lane, native + wasm).
pub const PANE_W: u32 = 460;
pub const PANE_H: u32 = 440;

/// One rendered linked view: its model, the camera that fit it, the per-node label
/// index (so the neon palette + animation can be chosen per node), and the oracles.
struct ViewRender {
    view: PopView,
    model: GraphModel,
    /// Caller-supplied node sizing / rings (population orbs, escalation flag).
    deco: Decorations,
    camera: Camera,
    /// node id → label index, for the neon palette / per-label animation.
    label_of: std::collections::HashMap<String, u8>,
    tex: Option<egui::TextureHandle>,
    lit_px: usize,
    visible_nodes: usize,
}

/// The `Sverige · Population` tab — a canonical [`Facet`] driving the three linked
/// population graphs through the L0 kernel + a glow lane, off one `knut-popsim` sample.
pub struct PopGraphTab {
    title: &'static str,
    /// The live query the toggles + scale field drive.
    pub query: PopQuery,
    /// The sampled `PopGraph` (regenerated when the scale / seed / kommun changes).
    sample: PopGraph,
    /// The three linked views, rebuilt when the query changes.
    views: [ViewRender; 3],
    /// Analytic stats at the requested scale (the honest 11M/100M numbers).
    stats: PopStats,
    /// The **injected animation clock** — drives every pulse/strobe/dash/particle/
    /// shockwave. The live app ticks it each frame; a test pins it for stable snapshots.
    pub clock: PopClock,
    /// The click-shockwave selection (selected Person + lit neighbour set). Applies
    /// to the **Full** view (the clickable population web).
    pub selection: Selection,
    /// Whether the CPU **bloom** post-pass runs on every pane (the spec's viewport
    /// bloom — intersecting neon lines bleed into mixed-colour light).
    pub bloom: bool,
    /// True until the first real `ui` (so we rebuild + render on first paint).
    dirty: bool,
}

impl PopGraphTab {
    /// Build the tab on the default query (the canonical 11M scale, all labels +
    /// relations on, coloured by label). Generates the first sample + the three views.
    pub fn new(title: &'static str) -> Self {
        let query = PopQuery::default();
        let sample = query.sample();
        let stats = PopStats::at_scale(query.n_persons);
        let mk = |view: PopView, sample: &PopGraph, q: &PopQuery| {
            let model = pop_model::view_model(view, q, sample);
            let deco = pop_model::view_decorations(view, q, sample, &model);
            let camera = Camera::fit(&model, PANE_W as f32, PANE_H as f32, 36.0);
            let label_of = pop_model::label_indices(&model);
            let visible_nodes = model.nodes.len();
            ViewRender { view, model, deco, camera, label_of, tex: None, lit_px: 0, visible_nodes }
        };
        let views = [
            mk(PopView::Full, &sample, &query),
            mk(PopView::Debt, &sample, &query),
            mk(PopView::Kommun, &sample, &query),
        ];
        Self {
            title,
            query,
            sample,
            views,
            stats,
            clock: PopClock::default(),
            selection: Selection::default(),
            bloom: true,
            dirty: true,
        }
    }

    /// **Live-feed mutator**: swap in a caller-supplied [`PopGraph`] as the current
    /// sample and rebuild the three views off IT (instead of re-running the query's
    /// own generator). The demo's in-memory [`Sim`](crate::sim::Sim) hands its ticking
    /// population in here each tick so the linked panes animate as the population grows
    /// — without touching the scale/filter query the user drives. Stats stay analytic
    /// at the current `query.n_persons`.
    pub fn set_sample(&mut self, sample: PopGraph) {
        self.sample = sample;
        self.stats = PopStats::at_scale(self.query.n_persons);
        for vr in &mut self.views {
            vr.model = pop_model::view_model(vr.view, &self.query, &self.sample);
            vr.deco = pop_model::view_decorations(vr.view, &self.query, &self.sample, &vr.model);
            vr.camera = Camera::fit(&vr.model, PANE_W as f32, PANE_H as f32, 36.0);
            vr.label_of = pop_model::label_indices(&vr.model);
            vr.visible_nodes = vr.model.nodes.len();
            vr.tex = None;
        }
        self.selection = Selection::default();
        self.dirty = true;
    }

    /// Re-sample + rebuild the three view models from the current query. Called after
    /// any query change (scale / seed / toggles / colour). Re-fits each camera.
    pub fn rebuild(&mut self) {
        self.sample = self.query.sample();
        self.stats = PopStats::at_scale(self.query.n_persons);
        for vr in &mut self.views {
            vr.model = pop_model::view_model(vr.view, &self.query, &self.sample);
            vr.deco = pop_model::view_decorations(vr.view, &self.query, &self.sample, &vr.model);
            vr.camera = Camera::fit(&vr.model, PANE_W as f32, PANE_H as f32, 36.0);
            vr.label_of = pop_model::label_indices(&vr.model);
            vr.visible_nodes = vr.model.nodes.len();
            vr.tex = None; // force a re-render
        }
        // A fresh sample re-indexes node ids, so any stale selection no longer maps.
        self.selection = Selection::default();
        self.dirty = true;
    }

    /// Select the first `Person` node in the Full view (the click-shockwave entry the
    /// UI + a test drive): lights it + its `OWES`/`SUBJECT_OF`/… connections, dims the
    /// rest. `pub` so the inject-assert test fires the exact same path as the click.
    pub fn select_first_person(&mut self) {
        if let Some(full) = self.views.iter().find(|v| v.view == PopView::Full)
            && let Some(n) = full.model.nodes.iter().find(|n| n.id.starts_with('p'))
        {
            self.selection = Selection::for_node(&n.id, &full.model);
        }
        self.dirty = true;
    }

    /// Clear the selection (un-dim the whole graph).
    pub fn clear_selection(&mut self) {
        self.selection = Selection::default();
        self.dirty = true;
    }

    /// Render one view's model to a straight-RGBA [`Frame`] through the **beauty
    /// path** ([`pop_beauty::render_scene`]): the L0 SDF kernel lowered + the
    /// cyber-toon decor layered on top — neon gradient fills (with the Debt/KfmCase
    /// pulse + PoliceRecord strobe), AO drop-shadows, glow halos, relation-styled
    /// edges (neon-blue ribbons · animated red debt-flow dash + particle tracers ·
    /// liquid flow), the click shockwave, the dimmed-unrelated subgraph, and the CPU
    /// bloom post-pass. Pure CPU, so it lights up identically native AND on wasm; all
    /// animation is driven by the injected [`PopClock`] so snapshots stay stable.
    /// The single render path shared by `ui`, `frame_for`, and the PNG-blob test.
    fn render_view(&self, vr: &ViewRender, w: u32, h: u32) -> Frame {
        // The click-shockwave selection only applies to the clickable Full view; the
        // Debt + Kommun panes always render fully lit.
        let empty = Selection::default();
        let selection = if vr.view == PopView::Full { &self.selection } else { &empty };
        let ctx = pop_beauty::SceneCtx {
            model: &vr.model,
            deco: &vr.deco,
            camera: &vr.camera,
            query: &self.query,
            clock: self.clock,
            selection,
            bloom: self.bloom,
            label_of: &vr.label_of,
        };
        pop_beauty::render_scene(&ctx, w, h)
    }

    /// Render every view into a cached egui texture + record its oracle. Shared by
    /// `ui` and the headless harness.
    fn render_all(&mut self, ctx: &egui::Context) {
        // `self.views` is borrowed mutably while we call `render_view` (which borrows
        // `self` immutably) — split the borrow by rendering into a scratch Vec first.
        let frames: Vec<Frame> =
            self.views.iter().map(|vr| self.render_view(vr, PANE_W, PANE_H)).collect();
        for (vr, frame) in self.views.iter_mut().zip(frames) {
            vr.lit_px = frame.lit_px();
            let img =
                egui::ColorImage::from_rgba_unmultiplied([PANE_W as usize, PANE_H as usize], &frame.rgba);
            vr.tex = Some(ctx.load_texture(
                format!("{}-{}", vr.view.label(), self.title),
                img,
                egui::TextureOptions::LINEAR,
            ));
        }
        self.dirty = false;
    }

    /// Public render-to-Frame for a [`PopView`] (the PNG-blob seam the test reads).
    pub fn frame_for(&self, view: PopView) -> Frame {
        let vr = self.views.iter().find(|v| v.view == view).expect("view present");
        self.render_view(vr, PANE_W, PANE_H)
    }

    /// The visible / total node split for a view ("rendering N of TOTAL"). `total` is
    /// the analytic full-scale node count; `visible` is what actually drew.
    pub fn visible_of_total(&self, view: PopView) -> (usize, u64) {
        let vr = self.views.iter().find(|v| v.view == view).expect("view present");
        let total = match view {
            // Full / Debt are instance views over the whole population.
            PopView::Full | PopView::Debt => self.stats.total_nodes,
            // Kommun aggregates to a fixed node-per-kommun count.
            PopView::Kommun => vr.model.nodes.len() as u64,
        };
        (vr.visible_nodes, total)
    }
}

impl Facet for PopGraphTab {
    fn title(&self) -> &str {
        self.title
    }

    fn ui(&mut self, ui: &mut egui::Ui) {
        let mut changed = false;

        // Render-trace: this tab's `ui()` RAN (the wasm-safe ledger). The proof the
        // Sverige showcase actually executes in the shipped wasm bundle — read back
        // via `state_json["trace"]["ran"]` / the `window.__facett_state()` JS hook.
        facett::core::runtrace::ran("PopGraphTab::ui");

        // The empty-query state carries the stable UI error code facet-population-1
        // (facett_core::errcode): when the query returned an empty sample the three
        // linked panes would draw NOTHING, so draw a human hint whose a11y atom
        // carries the CODE (+ Error severity) instead — tests + consumers react to
        // the CODE, not a matched string. (In the demo the sample is never empty, so
        // this branch is not hit; it is the honest empty-state the panes lacked.)
        if self.sample.nodes.is_empty() {
            ui.strong("Sverige · Population");
            ui.separator();
            let resp = ui.label("no people at this scale/filter");
            resp.widget_info(|| {
                facett_core::a11y::Semantics::error("no people at this scale/filter")
                    .severity(facett_core::Severity::Error)
                    .error_code("facet-population-1")
                    .widget_info()
            });
            return;
        }

        // ── Scale field: slider (1M→100M, log-ish via the numeric step) + entry. ──
        ui.horizontal_wrapped(|ui| {
            ui.strong("Sverige · Population");
            ui.separator();
            ui.label("people:");
            let mut n = self.query.n_persons as f64;
            let resp = ui.add(
                egui::Slider::new(&mut n, SCALE_MIN as f64..=SCALE_MAX as f64)
                    .logarithmic(true)
                    .custom_formatter(|v, _| pop_model::human(v as i64))
                    .text(""),
            );
            if resp.changed() {
                facett::core::runtrace::ran("PopGraphTab::ctl:scale-slider");
                self.query.n_persons = n.round() as u64;
                changed = true;
            }
            if ui.button("11M").on_hover_text("the canonical Swedish population").clicked() {
                facett::core::runtrace::ran("PopGraphTab::ctl:11M");
                self.query.n_persons = CANONICAL_PERSONS;
                changed = true;
            }
            ui.separator();
            ui.label("colour:");
            for (mode, name) in ColorMode::ALL {
                if ui.selectable_label(self.query.color == mode, name).clicked() {
                    facett::core::runtrace::ran(&format!("PopGraphTab::ctl:colour:{name}"));
                    self.query.color = mode;
                    changed = true;
                }
            }
            ui.separator();
            // ── Beauty controls: bloom toggle + the click-shockwave selection. ──
            if ui.selectable_label(self.bloom, "✨ bloom").on_hover_text("CPU bloom post-pass — neon lines bleed into mixed-colour light").clicked() {
                facett::core::runtrace::ran("PopGraphTab::ctl:bloom");
                self.bloom = !self.bloom;
                self.dirty = true;
            }
            if ui.button("⚡ select").on_hover_text("fire the click-shockwave on a Person — light its debt/enforcement ties, dim the rest").clicked() {
                facett::core::runtrace::ran("PopGraphTab::ctl:select");
                self.select_first_person();
            }
            if self.selection.node.is_some() && ui.button("clear").clicked() {
                facett::core::runtrace::ran("PopGraphTab::ctl:clear");
                self.clear_selection();
            }
        });

        // ── Label toggles (the 12 register labels). ──
        ui.horizontal_wrapped(|ui| {
            ui.label("labels:");
            for (i, (name, _)) in LABELS.iter().enumerate() {
                if ui.selectable_label(self.query.labels[i], *name).clicked() {
                    facett::core::runtrace::ran(&format!("PopGraphTab::ctl:label:{name}"));
                    self.query.labels[i] = !self.query.labels[i];
                    changed = true;
                }
            }
        });
        // ── Relation toggles. ──
        ui.horizontal_wrapped(|ui| {
            ui.label("relations:");
            let names = pop_model::relation_names();
            for (i, name) in names.iter().enumerate() {
                if ui.selectable_label(self.query.relations[i], *name).clicked() {
                    facett::core::runtrace::ran(&format!("PopGraphTab::ctl:relation:{name}"));
                    self.query.relations[i] = !self.query.relations[i];
                    changed = true;
                }
            }
        });

        if changed {
            self.rebuild();
        }
        // ── Advance the injected animation clock from egui's frame time, and ask for
        // a continuous repaint so the pulse / strobe / dash / particles / shockwave
        // stay alive. (A headless test pins `clock` and never enters this path.) ──
        let dt = ui.input(|i| i.stable_dt).clamp(0.0, 0.1);
        self.clock.tick(dt);
        ui.ctx().request_repaint();
        // Re-render every frame while animating (cheap CPU panes) so the motion shows.
        self.dirty = true;
        if self.dirty || self.views[0].tex.is_none() {
            let ctx = ui.ctx().clone();
            self.render_all(&ctx);
        }

        // ── The three linked panes, side by side. ──
        ui.separator();
        ui.horizontal_wrapped(|ui| {
            for vr in &self.views {
                let (vis, total) = match vr.view {
                    PopView::Full | PopView::Debt => (vr.visible_nodes as u64, self.stats.total_nodes),
                    PopView::Kommun => (vr.visible_nodes as u64, vr.model.nodes.len() as u64),
                };
                ui.vertical(|ui| {
                    let title = match vr.view {
                        PopView::Full => "Full population (LOD)",
                        PopView::Debt => "Debt → enforcement",
                        PopView::Kommun => "Per-kommun (aggregated)",
                    };
                    ui.strong(title);
                    if let Some(tex) = &vr.tex {
                        ui.image((tex.id(), egui::vec2(PANE_W as f32, PANE_H as f32)));
                    }
                    let agg = if vr.view == PopView::Kommun { " — aggregated by kommun" } else { "" };
                    ui.label(format!("rendering {} of {}{}", pop_model::human(vis as i64), pop_model::human(total as i64), agg));
                });
            }
        });

        ui.separator();
        ui.label(format!(
            "scale {} people → {} nodes / {} edges (analytic) · generated {} · render cap {}",
            pop_model::human(self.query.n_persons as i64),
            pop_model::human(self.stats.total_nodes as i64),
            pop_model::human(self.stats.total_edges as i64),
            pop_model::human(self.sample.count_label(0) as i64),
            RENDER_CAP,
        ));

        // ── render-lane emit: this Facet::ui path RAN; every view lit pixels and
        // honoured the render cap (LOD), the visible/total split is real. ──────────
        #[cfg(feature = "testmatrix")]
        {
            let all_lit = self.views.iter().all(|v| v.lit_px > 0);
            let capped = self.views.iter().all(|v| v.visible_nodes <= RENDER_CAP.max(LABELS.len() * 4));
            facett::core::testmatrix::emit(
                "facett-demo::PopGraphTab::ui",
                "ui_render",
                all_lit && capped,
                &format!(
                    "scale={} total_nodes={} full_vis={} debt_vis={} kommun_vis={} lit=[{},{},{}]",
                    self.query.n_persons,
                    self.stats.total_nodes,
                    self.views[0].visible_nodes,
                    self.views[1].visible_nodes,
                    self.views[2].visible_nodes,
                    self.views[0].lit_px,
                    self.views[1].lit_px,
                    self.views[2].lit_px,
                ),
            );
        }
    }

    fn state_json(&self) -> serde_json::Value {
        let active_labels: Vec<&str> = LABELS
            .iter()
            .enumerate()
            .filter(|(i, _)| self.query.labels[*i])
            .map(|(_, (n, _))| *n)
            .collect();
        let active_rels: Vec<&str> = pop_model::relation_names()
            .iter()
            .enumerate()
            .filter(|(i, _)| self.query.relations[*i])
            .map(|(_, n)| *n)
            .collect();
        let per_label: serde_json::Map<String, serde_json::Value> = LABELS
            .iter()
            .enumerate()
            .map(|(i, (n, _))| (n.to_string(), serde_json::json!(self.stats.per_label[i])))
            .collect();
        let views: Vec<serde_json::Value> = self
            .views
            .iter()
            .map(|vr| {
                let (vis, total) = self.visible_of_total(vr.view);
                serde_json::json!({
                    "view": vr.view.label(),
                    "visible_nodes": vis,
                    "total_nodes": total,
                    "edges": vr.model.edges.len(),
                    "lit_px": vr.lit_px,
                    "zoom": vr.camera.zoom,
                })
            })
            .collect();
        // The highlighted-relation set when a node is selected (the neon dashboard).
        let mut highlighted: Vec<String> = self.selection.rels.iter().cloned().collect();
        highlighted.sort();
        // Which render lane is doing the beauty: L1 vello when the feature is on,
        // else the always-available CPU decor lane (wasm-clean).
        #[cfg(feature = "l1-vello")]
        let beauty_lane = "L1 vello (gradients/glow) over the CPU L0 + decor base";
        #[cfg(not(feature = "l1-vello"))]
        let beauty_lane = "CPU L0 + decor (glow/AO/dash/particles/bloom) — wasm-clean";
        serde_json::json!({
            "component": "PopGraphTab",
            "source": "knut-popsim::simulate() (wasm-clean, no backend)",
            "render_path": "facett_core::render L0 SDF (CpuRenderer) + cyber-toon decor (decor::{glow_halo,ao_shadow,dash_line,particles_on_edge,shockwave_ring,bloom_composite})",
            // ── beauty (the Cyber-Toon Raytraced layer) ──
            "palette": "neon-12 (pop_beauty::NEON)",
            "beauty_lane": beauty_lane,
            "bloom": self.bloom,
            "animation_phase": self.clock.phase,
            "selected_node": self.selection.node,
            "highlighted_relations": highlighted,
            "dimmed_alpha": if self.selection.node.is_some() { pop_beauty::DIM_ALPHA } else { 1.0 },
            // ── threading split (the native vs wasm generation path) ──
            "gen_threaded_native": cfg!(not(target_arch = "wasm32")),
            "target_gen_cap": PopQuery::target_gen_cap(),
            // ── scale (honest) ──
            "n_persons": self.query.n_persons,
            "scale_min": SCALE_MIN,
            "scale_max": SCALE_MAX,
            "generated_persons": self.sample.count_label(0),
            "gen_cap": PopQuery::target_gen_cap(),
            "render_cap": RENDER_CAP,
            // ── analytic stats at the requested scale ──
            "stats": {
                "total_nodes": self.stats.total_nodes,
                "total_edges": self.stats.total_edges,
                "per_label": per_label,
            },
            // ── filters ──
            "color_mode": self.query.color.label(),
            "active_labels": active_labels,
            "active_relations": active_rels,
            "seed": self.query.seed,
            "n_kommun": self.query.n_kommun,
            // ── the three linked views ──
            "views": views,
            "navigable": false,
            // The stable UI ERROR CODE for the empty-query state (facett_core::errcode):
            // present so tests + consumers react to the CODE, not a matched string.
            "error_code": if self.sample.nodes.is_empty() {
                serde_json::json!("facet-population-1")
            } else {
                serde_json::Value::Null
            },
        })
    }

    /// STRUCTURAL severity (the Robot-UI gate signal): RED when the population query
    /// returned no people at this scale/filter (the facet-population-1 empty state —
    /// an empty sample), the green Info floor otherwise.
    fn severity(&self) -> facett_core::Severity {
        if self.sample.nodes.is_empty() {
            facett_core::Severity::Error
        } else {
            facett_core::Severity::Info
        }
    }

    fn caps(&self) -> FacetCaps {
        FacetCaps::NONE.themeable().resizable()
    }

    /// Opt into typed downcast so the demo's live [`Sim`](crate::sim::Sim) can push a
    /// fresh ticking [`PopGraph`] sample in via [`set_sample`](Self::set_sample) while
    /// this tab lives boxed inside the `FacetDeck`.
    fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
        Some(self)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use facett::core::harness;

    /// INJECT-ASSERT: the tab builds the three linked views off a REAL knut-popsim
    /// sample, every view lowers + renders a non-blank glowing frame through the L0
    /// kernel, and the visible-node cap is honoured (LOD) while the analytic stats
    /// report the canonical 11M scale — not "didn't panic".
    #[test]
    fn pop_tab_renders_three_linked_nonblank_views() {
        let mut tab = PopGraphTab::new(TITLE);
        assert!(tab.caps().themeable, "the pop tab is themeable");

        // The default scale is the canonical 11M, reported analytically.
        assert_eq!(tab.query.n_persons, CANONICAL_PERSONS);
        let s = tab.state_json();
        assert_eq!(s["n_persons"].as_u64(), Some(CANONICAL_PERSONS));
        assert!(s["stats"]["total_nodes"].as_u64().unwrap() > 50_000_000, "11M people fan out to tens of millions of nodes");
        assert_eq!(s["generated_persons"].as_u64(), Some(PopQuery::target_gen_cap() as u64), "generation capped, scale honoured analytically");

        // Drive a real headless render frame (the whole tab UI).
        let r = harness::headless_render(&mut tab);
        assert!(r.drew(), "the pop tab tessellated its panes");

        // Every linked view lit a non-blank frame through the L0 + glow kernel.
        for view in [PopView::Full, PopView::Debt, PopView::Kommun] {
            let f = tab.frame_for(view);
            assert_eq!(f.rgba.len(), (PANE_W * PANE_H * 4) as usize);
            assert!(f.lit_px() > 2_000, "{} view drew a non-blank glowing frame: {} lit", view.label(), f.lit_px());
            // Containment: a corner pixel is the untouched ground.
            assert_eq!(&f.rgba[0..4], &BG, "{} corner is the dark ground", view.label());
        }

        // The visible/total split is real + capped (LOD), surfaced in state_json.
        let views = s["views"].as_array().unwrap();
        assert_eq!(views.len(), 3, "three linked views");
        let full = views.iter().find(|v| v["view"] == "full").unwrap();
        assert!(full["visible_nodes"].as_u64().unwrap() <= RENDER_CAP as u64, "Full view honours the render cap");
        assert!(full["total_nodes"].as_u64().unwrap() > full["visible_nodes"].as_u64().unwrap(), "rendering a sample of the whole");
        let kommun = views.iter().find(|v| v["view"] == "kommun").unwrap();
        assert_eq!(kommun["visible_nodes"], 14, "kommun view = one node per kommun (the 14 cities)");
    }

    /// INJECT-ASSERT: the scale field really scales the analytic stats across the
    /// whole 1M→100M range, while generation stays capped (memory-safe) — the honest
    /// LOD contract, asserted as state values.
    #[test]
    fn scaling_field_drives_stats_but_caps_generation() {
        let mut tab = PopGraphTab::new(TITLE);
        tab.query.n_persons = SCALE_MAX;
        tab.rebuild();
        let s = tab.state_json();
        assert_eq!(s["n_persons"].as_u64(), Some(SCALE_MAX), "field set to 100M");
        assert_eq!(s["generated_persons"].as_u64(), Some(PopQuery::target_gen_cap() as u64), "still only the target cap generated at 100M");
        let big_total = s["stats"]["total_nodes"].as_u64().unwrap();

        tab.query.n_persons = SCALE_MIN;
        tab.rebuild();
        let small_total = tab.state_json()["stats"]["total_nodes"].as_u64().unwrap();
        assert!(big_total > small_total * 8, "100M stats ≫ 1M stats ({big_total} vs {small_total})");
    }

    /// INJECT-ASSERT: toggling a label off changes the rendered picture (fewer
    /// visible nodes + a different lit-pixel count) AND drops it from state_json's
    /// active set — the toggles really drive the render, not just a flag.
    #[test]
    fn label_toggle_changes_render_and_state() {
        let mut tab = PopGraphTab::new(TITLE);
        let before = tab.frame_for(PopView::Full);

        // Turn off the four heaviest entity labels. At the LOD cap the *count* stays
        // capped (by design), but the picture + the active set must change.
        for li in [1, 4, 10, 11] {
            tab.query.labels[li] = false;
        }
        tab.rebuild();
        let after = tab.frame_for(PopView::Full);

        // The rendered picture really changed (a substantial pixel diff, not a flag).
        let diff = before
            .rgba
            .as_chunks::<4>().0.iter()
            .zip(after.rgba.as_chunks::<4>().0)
            .filter(|(a, b)| a != b)
            .count();
        assert!(diff > 1_000, "disabling labels visibly changed the Full view: {diff} px");

        let active = tab.state_json()["active_labels"].as_array().unwrap().clone();
        assert!(!active.iter().any(|v| v == "Address"), "Address toggled out of the active set");
        assert!(!active.iter().any(|v| v == "Vehicle"), "Vehicle toggled out");
        assert_eq!(active.len(), 8, "8 of 12 labels remain active");
        assert!(active.iter().any(|v| v == "Person"), "Person still active");
    }

    /// INJECT-ASSERT: the colour-by mode actually recolours — switching to debt-heat
    /// changes the rendered frame vs by-label (real recolour, asserted via the frame
    /// diff, not a flag).
    #[test]
    fn color_mode_recolours_the_frame() {
        let mut tab = PopGraphTab::new(TITLE);
        let by_label = tab.frame_for(PopView::Full);
        tab.query.color = ColorMode::ByDebtHeat;
        tab.rebuild();
        let by_heat = tab.frame_for(PopView::Full);
        let diff = by_label
            .rgba
            .as_chunks::<4>().0.iter()
            .zip(by_heat.rgba.as_chunks::<4>().0)
            .filter(|(a, b)| a != b)
            .count();
        assert!(diff > 1_000, "debt-heat recoloured a substantial part of the frame: {diff} px");
        assert_eq!(tab.state_json()["color_mode"], "debt-heat");
    }

    /// Write a straight-RGBA8 [`Frame`] to a real PNG (the final-picture blob seam;
    /// `image` dev-dep). The test emits one per style state for human eyeballing.
    fn save_png(path: &str, f: &Frame) {
        image::save_buffer(path, &f.rgba, f.width, f.height, image::ColorType::Rgba8)
            .expect("write png blob");
    }

    /// Summed RGB luma over a rectangular region (sensitive to dimming where a bare
    /// lit-pixel count is not — a dimmed node still has non-zero alpha).
    fn region_luma(f: &Frame, x0: usize, x1: usize, y0: usize, y1: usize) -> u64 {
        let w = f.width as usize;
        let mut s = 0u64;
        for y in y0..y1 {
            for x in x0..x1 {
                let p = (y * w + x) * 4;
                s += f.rgba[p] as u64 + f.rgba[p + 1] as u64 + f.rgba[p + 2] as u64;
            }
        }
        s
    }

    /// INJECT-ASSERT (the BEAUTY proof + final-picture blobs): with the animation
    /// clock pinned (deterministic), render the Full view in three style states —
    /// by-label, debt-heat, and shockwave-active — emit each as a PNG blob, and
    /// assert the picture really changes per state. The headline oracle: the
    /// **shockwave-active** render emits LESS total light than the unselected one
    /// (the unrelated subgraph dimmed to 0.1), and `state_json` carries the selected
    /// node + the highlighted-relation set + the dimmed alpha. Bloom on/off is a real
    /// toggle (the frames differ). Not a flag — the pixels prove it.
    #[test]
    fn beauty_style_states_render_distinct_blobs_and_shockwave_dims() {
        let mut tab = PopGraphTab::new(TITLE);
        // Pin the injected clock so every blob is byte-stable across runs (the LAW).
        tab.clock = PopClock::at(0.5);

        // ── State 1: by-label neon (the default). ──
        let by_label = tab.frame_for(PopView::Full);
        assert!(by_label.lit_px() > 2_000, "by-label drew a luminous picture: {}", by_label.lit_px());
        assert_eq!(&by_label.rgba[0..4], &BG, "matte slate ground in the corner");
        let total_unselected = region_luma(&by_label, 0, PANE_W as usize, 0, PANE_H as usize);

        // ── State 2: debt-heat recolour (a different picture). ──
        tab.query.color = ColorMode::ByDebtHeat;
        tab.rebuild();
        tab.clock = PopClock::at(0.5);
        let debt_heat = tab.frame_for(PopView::Full);
        let heat_diff = by_label
            .rgba
            .as_chunks::<4>().0.iter()
            .zip(debt_heat.rgba.as_chunks::<4>().0)
            .filter(|(a, b)| a != b)
            .count();
        assert!(heat_diff > 1_000, "debt-heat is a distinct picture from by-label: {heat_diff} px");

        // ── State 3: shockwave-active (back to by-label, a Person selected). ──
        tab.query.color = ColorMode::ByLabel;
        tab.rebuild();
        tab.clock = PopClock::at(0.5);
        tab.select_first_person();
        assert!(tab.selection.node.is_some(), "a Person was selected");
        let shockwave = tab.frame_for(PopView::Full);
        let total_selected = region_luma(&shockwave, 0, PANE_W as usize, 0, PANE_H as usize);

        // The headline beauty oracle: with the shockwave active, the unrelated
        // subgraph dims, so the WHOLE picture emits less light than the unselected one
        // (and the shockwave-active frame differs from by-label, not a flag).
        assert!(
            total_selected < total_unselected,
            "shockwave-active dims the unrelated subgraph (less total light): {total_selected} < {total_unselected}"
        );
        let shock_diff = by_label
            .rgba
            .as_chunks::<4>().0.iter()
            .zip(shockwave.rgba.as_chunks::<4>().0)
            .filter(|(a, b)| a != b)
            .count();
        assert!(shock_diff > 1_000, "shockwave-active is a distinct picture: {shock_diff} px");

        // state_json carries the beauty + selection state (readable, not pixels).
        let s = tab.state_json();
        assert_eq!(s["palette"], "neon-12 (pop_beauty::NEON)");
        assert_eq!(s["bloom"], true, "bloom on by default");
        assert!(s["selected_node"].is_string(), "the selected node id is surfaced");
        let dimmed = s["dimmed_alpha"].as_f64().unwrap();
        assert!((dimmed - pop_beauty::DIM_ALPHA as f64).abs() < 1e-6, "dimmed alpha = 0.1 while selected");
        let hl = s["highlighted_relations"].as_array().unwrap();
        assert!(!hl.is_empty(), "the click highlighted at least one relation");

        // ── Bloom is a real toggle: turning it off changes the rendered frame. ──
        let bloom_on = tab.frame_for(PopView::Full);
        tab.bloom = false;
        let bloom_off = tab.frame_for(PopView::Full);
        let bloom_diff = bloom_on
            .rgba
            .as_chunks::<4>().0.iter()
            .zip(bloom_off.rgba.as_chunks::<4>().0)
            .filter(|(a, b)| a != b)
            .count();
        assert!(bloom_diff > 500, "bloom on/off is a real visual change: {bloom_diff} px");
        assert_eq!(tab.state_json()["bloom"], false, "bloom flag follows the toggle");
        tab.bloom = true;

        // ── Emit the final-picture blobs for human eyeballing. ──
        if let Ok(dir) = std::env::var("POP_BLOB_DIR") {
            save_png(&format!("{dir}/pop_by_label.png"), &by_label);
            save_png(&format!("{dir}/pop_debt_heat.png"), &debt_heat);
            save_png(&format!("{dir}/pop_shockwave.png"), &shockwave);
            // The hero shot: all three linked panes at the default style.
            let mut hero = PopGraphTab::new(TITLE);
            hero.clock = PopClock::at(0.35);
            for view in [PopView::Full, PopView::Debt, PopView::Kommun] {
                save_png(&format!("{dir}/pop_pane_{}.png", view.label()), &hero.frame_for(view));
            }
        }
    }
}