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));
}
}
}
}