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

nordisk/facett

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