facett-graph3d/tests/render_callchain.rs
//! **Render call-chain coverage** for `facett-graph3d` (the LAW: every function in
//! the input→pixel chain TRACKED + VERIFIED in the test matrix).
//!
//! # The call chains (start → pixel)
//! `Graph3D`:
//! ```text
//! Graph3D::new ─ build Node3 cloud
//! → Graph3D::layout {Sphere | Helix | Force} ── fill 3D positions
//! → Graph3D::project (private; via render) ── yaw-rotate + ortho → screen + depth
//! → Facet::ui ── edges faded by depth · nodes depth-sorted (painter's algo)
//! · gyro-stabilized labels · AccessKit yaw value
//! → Facet::state_json ── observable counts/yaw/layout
//! ```
//! `RabbitLogo3D` (the 3D test mascot):
//! ```text
//! RabbitLogo3D::with_depth → rabbit_mesh(depth) ── extruded solid (pos/nrm/idx)
//! → RabbitLogo3D::rotate (private; via render) ── yaw·Y then tilt·X
//! → Facet::ui (CPU painter) ── back-face cull · normal·light shade · depth sort
//! └ (feature wgpu) draw_gpu → build_gpu_geometry → upload → offscreen
//! colour+depth Z-buffer pass → blit (live GPU lane in gpu_offscreen.rs)
//! ```
//!
//! # Verification doctrine (return-vs-emit)
//! * **Assert the return**: `layout` produces finite positions on the right manifold
//! (Sphere → unit shell, Helix → unit-cube band, Force → finite unit cube, no NaN,
//! non-overlapping-ish = the cloud actually spreads); `state_json` reports the real
//! counts; the AccessKit yaw value is the exact injected-clock function.
//! * **EMIT where the return can't prove it** (EMIT-DOCTRINE): the *projected/lit*
//! pixels are opaque — the headless harness's tessellated **vertex count** ("it
//! actually drew") + the egui_kittest framebuffer non-blank ratio are reported via
//! `facett_core::testmatrix::emit` (testmatrix-gated) into the `nornir test` matrix.
//!
//! # Matrix
//! ~1000 cases = graph sizes {2 … 5000 nodes} × layouts {Sphere, Helix, Force} ×
//! fan/topologies {chain, fanout, fanin, star, complete} × clocks {0, 1, 2.5, 5}.
use egui::Color32;
use facett_core::Facet;
use facett_graph3d::{Graph3D, Layout3D, RabbitLogo3D};
/// testmatrix passthrough — a no-op unless built with `--features testmatrix`.
#[allow(unused)]
fn emit(check: &str, ok: bool, detail: &str) {
#[cfg(feature = "testmatrix")]
facett_core::testmatrix::emit("facett-graph3d", check, ok, detail);
#[cfg(not(feature = "testmatrix"))]
{
let _ = (check, ok, detail);
}
}
#[derive(Clone, Copy)]
enum Topo {
Chain,
FanOut,
FanIn,
Star,
Complete,
}
impl Topo {
const ALL: [Topo; 5] = [Topo::Chain, Topo::FanOut, Topo::FanIn, Topo::Star, Topo::Complete];
fn name(self) -> &'static str {
match self {
Topo::Chain => "chain",
Topo::FanOut => "fanout",
Topo::FanIn => "fanin",
Topo::Star => "star",
Topo::Complete => "complete",
}
}
fn edges(self, n: usize) -> Vec<(usize, usize)> {
if n < 2 {
return vec![];
}
match self {
Topo::Chain => (0..n - 1).map(|i| (i, i + 1)).collect(),
Topo::FanOut => (1..n).map(|i| (0, i)).collect(),
Topo::FanIn => (0..n - 1).map(|i| (i, n - 1)).collect(),
Topo::Star => (1..n).flat_map(|i| [(0, i), (i, 0)]).collect(),
// Complete is O(n²) — only used at small n in the matrix.
Topo::Complete => (0..n).flat_map(|i| (i + 1..n).map(move |j| (i, j))).collect(),
}
}
}
fn graph(n: usize, layout: Layout3D, topo: Topo) -> Graph3D {
let nodes: Vec<(String, Color32)> =
(0..n).map(|i| (format!("n{i}"), Color32::from_rgb((i * 7 % 255) as u8, 200, 220))).collect();
Graph3D::new(nodes, topo.edges(n), layout)
}
// ──────────────────────────────────────────────────────────────────────────
// 1. Graph3D::layout — RETURN proves the manifold + finiteness + spread.
// ──────────────────────────────────────────────────────────────────────────
#[test]
fn sphere_layout_lands_on_unit_shell() {
for &n in &[2usize, 50, 200, 1000] {
let g = graph(n, Layout3D::Sphere, Topo::Chain);
for node in &g.nodes {
let r = (node.pos[0].powi(2) + node.pos[1].powi(2) + node.pos[2].powi(2)).sqrt();
assert!((r - 1.0).abs() < 0.06, "on unit shell n={n}: r={r}");
}
}
emit("sphere_unit_shell", true, "all nodes within 0.06 of the unit sphere across n∈{2..1000}");
}
#[test]
fn helix_layout_is_finite_unit_band() {
for &n in &[2usize, 50, 500] {
let g = graph(n, Layout3D::Helix, Topo::Chain);
for node in &g.nodes {
assert!(node.pos.iter().all(|c| c.is_finite()), "finite helix");
// y sweeps the [-1,1] band; x,z ride the unit circle.
assert!(node.pos[1] >= -1.001 && node.pos[1] <= 1.001, "y band: {}", node.pos[1]);
let rxz = (node.pos[0].powi(2) + node.pos[2].powi(2)).sqrt();
assert!((rxz - 1.0).abs() < 1e-3, "x,z on unit circle: {rxz}");
}
}
emit("helix_unit_band", true, "helix: finite, y∈[-1,1], (x,z) on unit circle");
}
#[test]
fn force_layout_is_finite_unit_cube_and_spreads() {
for &n in &[2usize, 30, 60, 150] {
let g = graph(n, Layout3D::Force, Topo::Star);
let mut span = [0f32; 3];
let mut mn = [f32::MAX; 3];
let mut mx = [f32::MIN; 3];
for node in &g.nodes {
for c in 0..3 {
assert!(node.pos[c].is_finite(), "finite force pos");
assert!(node.pos[c] >= -1.05 && node.pos[c] <= 1.05, "in unit cube: {}", node.pos[c]);
mn[c] = mn[c].min(node.pos[c]);
mx[c] = mx[c].max(node.pos[c]);
}
}
for c in 0..3 {
span[c] = mx[c] - mn[c];
}
if n > 4 {
// The cloud actually spreads (non-overlapping-ish): the normaliser fills
// the cube, so at least one axis spans most of [-1,1].
assert!(span.iter().cloned().fold(0.0, f32::max) > 1.0, "force cloud spreads n={n}: {span:?}");
}
}
emit("force_unit_cube_spread", true, "force: finite, in unit cube, cloud spreads (span>1)");
}
// ──────────────────────────────────────────────────────────────────────────
// 2. Facet::state_json — RETURN reports the real model (counts/layout/yaw).
// ──────────────────────────────────────────────────────────────────────────
#[test]
fn state_json_reports_the_real_model() {
for layout in [Layout3D::Sphere, Layout3D::Helix, Layout3D::Force] {
for topo in Topo::ALL {
let n = 12;
let g = graph(n, layout, topo);
let j = g.state_json();
assert_eq!(j["nodes"], n);
assert_eq!(j["edges"], topo.edges(n).len());
assert_eq!(j["layout"], layout.label());
// Labels on for small graphs, off for big ones (the clutter gate).
assert_eq!(j["show_labels"], n <= 40);
}
}
// The big-graph label gate flips at >40.
assert_eq!(graph(200, Layout3D::Sphere, Topo::Chain).state_json()["show_labels"], false);
emit("state_json_real_model", true, "nodes/edges/layout/show_labels match the model");
}
// ──────────────────────────────────────────────────────────────────────────
// 3. Graph3D::project + Facet::ui — pixels are opaque ⇒ EMIT the drew-vertices
// oracle (the headless harness tessellates the projected/depth-sorted scene).
// ──────────────────────────────────────────────────────────────────────────
#[test]
fn ui_renders_drew_vertices_for_every_layout_and_topology() {
use facett_core::harness;
for layout in [Layout3D::Sphere, Layout3D::Helix, Layout3D::Force] {
for topo in Topo::ALL {
let mut g = graph(30, layout, topo).with_clock(1.0);
let r = harness::headless_render(&mut g);
let ok = r.drew() && r.vertices > 0;
assert!(ok, "graph3d drew vertices layout={} topo={} verts={}", layout.label(), topo.name(), r.vertices);
assert_eq!(r.state["nodes"], 30);
emit(
"graph3d_ui_drew",
ok,
&format!("layout={} topo={} vertices={}", layout.label(), topo.name(), r.vertices),
);
}
}
}
// ──────────────────────────────────────────────────────────────────────────
// 4. ROBOT / headless egui_kittest — drive the live graph surface, assert via the
// AccessKit yaw value (the injected-clock pure function) + a non-blank frame.
// ──────────────────────────────────────────────────────────────────────────
#[test]
fn robot_drives_graph_surface_and_reads_yaw() {
use egui_kittest::Harness;
use egui_kittest::kittest::{NodeT, Queryable};
// Same state + clock ⇒ deterministic yaw on the AccessKit Image node.
let mut g = graph(20, Layout3D::Sphere, Topo::Star).with_clock(2.0);
let mut h = Harness::builder().with_size(egui::vec2(420.0, 420.0)).build_ui(move |ui| g.ui(ui));
h.run();
let yaw = h
.get_all_by_role(egui::accesskit::Role::Image)
.filter_map(|n| n.accesskit_node().numeric_value())
.next()
.expect("the 3D canvas exposes an Image node carrying the yaw value");
// yaw = base(0.6) + clock(2.0)·spin_rate(0.4) = 1.4.
let ok = (yaw - 1.4).abs() < 1e-5;
assert!(ok, "robot reads the injected-clock yaw: {yaw}");
emit("robot_yaw_accesskit", ok, &format!("clock=2.0 → yaw={yaw} (expect 1.4)"));
}
#[test]
fn robot_graph_surface_renders_nonblank() {
use egui_kittest::Harness;
let mut g = graph(40, Layout3D::Force, Topo::Star).with_clock(1.0);
let mut h = Harness::builder().with_size(egui::vec2(400.0, 400.0)).build_ui(move |ui| {
facett_core::set_theme(ui.ctx(), facett_core::Theme::sci_fi());
g.ui(ui);
});
h.run();
// Tessellated output proves the scene drew (the headless framebuffer is the GPU
// backend's; here we assert via the harness state that real geometry exists).
// Re-render headlessly for the vertex proof (kittest's snapshot needs a GPU).
use facett_core::harness;
let mut g2 = graph(40, Layout3D::Force, Topo::Star).with_clock(1.0);
let r = harness::headless_render(&mut g2);
let ok = r.drew();
assert!(ok, "force graph surface is non-blank (drew {} verts)", r.vertices);
emit("robot_graph_nonblank", ok, &format!("force/star 40 nodes drew {} verts", r.vertices));
}
// ──────────────────────────────────────────────────────────────────────────
// 5. RabbitLogo3D — mesh build + rotate + CPU painter (cull/shade/depth-sort).
// Return proves the mesh; EMIT the lit-render vertex oracle.
// ──────────────────────────────────────────────────────────────────────────
#[test]
fn logo_mesh_is_a_real_extruded_solid() {
for &depth in &[0.1f32, 0.34, 0.8] {
let logo = RabbitLogo3D::with_depth("rabbit", depth);
let m = logo.mesh();
assert!(m.vertex_count() > 100, "real mesh @ depth {depth}");
assert!(m.triangle_count() > 50, "many tris @ depth {depth}");
// positions / normals are 1:1 and finite.
assert_eq!(m.positions.len(), m.normals.len(), "pos/normal parity");
assert!(m.positions.iter().all(|p| p.iter().all(|c| c.is_finite())), "finite positions");
assert!(m.normals.iter().all(|nrm| nrm.iter().all(|c| c.is_finite())), "finite normals");
// indices reference valid vertices.
assert!(m.indices.iter().all(|&i| (i as usize) < m.positions.len()), "indices in range");
assert_eq!(m.indices.len() % 3, 0, "triangle list");
}
emit("logo_mesh_real_solid", true, "extruded mesh: >100 verts, >50 tris, finite, valid indices");
}
#[test]
fn logo_cpu_painter_lights_and_depth_sorts() {
use facett_core::harness;
// Fixed clock → deterministic spin; the painter culls back-faces, shades by
// normal·light, and depth-sorts → tessellates to vertices (non-blank).
let mut logo = RabbitLogo3D::new("rabbit3d").with_clock(1.0);
let r = harness::headless_render(&mut logo);
let ok = r.drew() && r.state["triangles"].as_u64().unwrap() > 50;
assert!(ok, "the lit, depth-sorted logo tessellated ({} verts)", r.vertices);
assert_eq!(r.state["clock_s"], 1.0);
// Same clock ⇒ identical drawn geometry (pure spin function).
let mut a = RabbitLogo3D::new("r").with_clock(2.5);
let mut b = RabbitLogo3D::new("r").with_clock(2.5);
let (ra, rb) = (harness::headless_render(&mut a), harness::headless_render(&mut b));
assert_eq!(ra.vertices, rb.vertices, "same clock → same geometry");
emit("logo_cpu_lit_nonblank", ok, &format!("verts={} tris={}", r.vertices, r.state["triangles"]));
}
// ──────────────────────────────────────────────────────────────────────────
// 6. THE ~1000-CASE MATRIX — sizes × layouts × topologies × clocks, asserting the
// layout manifold + finiteness at each case and the headless render drew (on a
// sampled subset to bound runtime). One emit per stage.
// ──────────────────────────────────────────────────────────────────────────
#[test]
fn matrix_thousand_cases_assert_each_stage() {
use facett_core::harness;
let sizes: &[usize] =
&[2, 5, 10, 20, 40, 80, 120, 160, 240, 320, 480, 640, 960, 1280, 2560, 5000];
// `Given` is in the matrix WITHOUT a host placement on purpose: that is the
// degraded lane (`facet-graph3d-5`), and the matrix asserts it still produces a
// finite unit-cube cloud and still names its fallback — a silent half-placement
// would go red here as well as in `tests/given_positions.rs`.
let layouts =
[Layout3D::Sphere, Layout3D::Helix, Layout3D::Force, Layout3D::Grid, Layout3D::Given];
let clocks: &[f64] = &[0.0, 1.0, 2.5, 5.0, 7.5, 10.0];
let mut cases = 0usize;
let mut layout_ok_all = true;
let mut finite_ok_all = true;
let mut drew_ok_all = true;
for &n in sizes {
for layout in layouts {
// The Force layout is deterministic Fruchterman–Reingold: O(n²) per
// iteration × 100 iterations. It is proven at scale (n≤150) by
// `force_layout_is_finite_unit_cube_and_spreads`; in the matrix it is
// capped to keep the suite fast, while the O(n) Sphere/Helix layouts
// carry the large sizes (up to 5000).
if matches!(layout, Layout3D::Force) && n > 160 {
continue;
}
for topo in Topo::ALL {
// Complete topology is O(n²) edges — cap it so the matrix stays fast.
if matches!(topo, Topo::Complete) && n > 80 {
continue;
}
for &clock in clocks {
cases += 1;
let g = graph(n, layout, topo).with_clock(clock);
// Stage A: finiteness + manifold (every case, cheap).
for node in &g.nodes {
finite_ok_all &= node.pos.iter().all(|c| c.is_finite());
match layout {
Layout3D::Sphere => {
let r = (node.pos[0].powi(2) + node.pos[1].powi(2) + node.pos[2].powi(2)).sqrt();
layout_ok_all &= (r - 1.0).abs() < 0.07 || n == 1;
}
Layout3D::Helix => {
layout_ok_all &= node.pos[1].abs() <= 1.001;
}
Layout3D::Force => {
layout_ok_all &= node.pos.iter().all(|c| c.abs() <= 1.06);
}
Layout3D::Grid => {
// Row-major cube lattice normalised to the unit cube.
layout_ok_all &= node.pos.iter().all(|c| c.abs() <= 1.001);
}
Layout3D::Given => {
// No host placement was supplied here, so the GRID
// fallback drew it — same bound, plus the report.
layout_ok_all &= node.pos.iter().all(|c| c.abs() <= 1.001);
}
}
}
// Stage B: state_json reports the real counts.
let j = g.state_json();
layout_ok_all &= j["nodes"] == n && j["layout"] == layout.label();
// The degraded lane says WHICH layout actually drew, and the
// complete lanes say nothing — neither may be silent.
layout_ok_all &= if matches!(layout, Layout3D::Given) {
j["layout_fallback"] == "grid" && j["error_code"] == "facet-graph3d-5"
} else {
j["layout_fallback"].is_null()
};
// Stage C: headless render drew — sampled on the small graphs
// (rendering 5000 nodes × every clock is a bench, not a unit; the
// layout above already proved scale).
if n <= 40 && clock == 1.0 && matches!(topo, Topo::Star) {
let mut gg = graph(n, layout, topo).with_clock(clock);
let r = harness::headless_render(&mut gg);
drew_ok_all &= r.drew() || n < 2;
}
}
}
}
}
assert!(cases >= 1000, "matrix must run ≥1000 cases, ran {cases}");
assert!(finite_ok_all, "every layout position finite");
assert!(layout_ok_all, "every case on its manifold + state matches");
assert!(drew_ok_all, "every sampled headless render drew");
emit("matrix_cases", true, &format!("ran {cases} cases (12 sizes × 4 layouts × 5 topo × 4 clocks)"));
emit("matrix_finite_stage", finite_ok_all, "all layout positions finite every case");
emit("matrix_layout_stage", layout_ok_all, "all positions on manifold + state matches every case");
emit("matrix_render_stage", drew_ok_all, "sampled headless renders drew");
}
// ──────────────────────────────────────────────────────────────────────────
// 7. GPU geometry build (feature `wgpu`) — RETURN proves the upload payload
// mirrors the mesh 1:1 (no device needed). The LIVE upload→offscreen Z-buffer
// pass→blit path is driven on a real GPU by `tests/gpu_offscreen.rs`.
// ──────────────────────────────────────────────────────────────────────────
#[cfg(feature = "wgpu")]
#[test]
fn gpu_geometry_payload_mirrors_the_mesh() {
use facett_graph3d::logo3d::gpu::build_gpu_geometry;
let logo = RabbitLogo3D::new("rabbit3d");
let m = logo.mesh();
let geo = build_gpu_geometry(m);
assert_eq!(geo.vertices.len(), m.vertex_count(), "one GPU vertex per mesh vertex");
assert_eq!(geo.indices, m.indices, "indices carried verbatim");
// The first vertex carries the real position + normal.
assert_eq!(geo.vertices[0].pos, m.positions[0], "position carried");
assert_eq!(geo.vertices[0].nrm, m.normals[0], "normal carried");
emit(
"gpu_geometry_mirrors_mesh",
true,
&format!("verts={} indices={}", geo.vertices.len(), geo.indices.len()),
);
}