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/neon_snapshot.rs

//! **Screenshot PROOF (LAW #4) for the orgasmic neon decor.**
//!
//! Renders [`Graph3D`] through egui_kittest's wgpu backend at a **pinned clock**
//! (so every neon effect — breathing, particle flow, shockwave — is a pure function
//! of the clock and the framebuffer is byte-stable) and asserts, exactly the
//! `pop_tab` pattern:
//!   1. **lit + many-shade** — `Full` decor draws a luminous, multi-hue frame (the
//!      neon palette + glow orbs), not the old flat fill, and
//!   2. **the dim is real** — a **shockwave-active** frame (a node selected, its
//!      subgraph lit, the rest dimmed to ~0.1α) emits **LESS total light** than the
//!      unselected frame.
//!
//! Reused decor is `facett_core::render::decor` (glow/AO/particles/shockwave); the
//! CPU painter lane is wasm-clean, egui's wgpu backend rasterises it here for the PNG.
//!
//! Run: `cargo test -p facett-graph3d --test neon_snapshot`
//! Goldens: `facett-graph3d/tests/snapshots/neon_*.png`.

use egui::Color32;
use egui_kittest::Harness;

use facett_core::Facet;
use facett_graph3d::{DecorPolicy, Graph3D, Layout3D};

/// A deterministic medium graph: a ring + a few chords, distinct hues per node.
fn graph() -> Graph3D {
    let nodes: Vec<(String, Color32)> =
        (0..14).map(|i| (format!("n{i}"), Color32::from_rgb(0, 200, 220))).collect();
    let mut edges: Vec<(usize, usize)> = (0..14).map(|i| (i, (i + 1) % 14)).collect();
    edges.extend([(0, 7), (3, 10), (5, 12), (0, 5)]);
    Graph3D::new(nodes, edges, Layout3D::Sphere)
}

/// `(distinct_shades, total_light)` of a frame. `distinct_shades` = number of
/// non-background colour buckets (a flat fill → tiny; a lit, neon-shaded scene →
/// many). `total_light` = Σ(r+g+b) over every pixel — the "how much light" oracle.
fn measure(img: &image::RgbaImage) -> (usize, u64) {
    let mut hist = std::collections::HashMap::<u32, u32>::new();
    for p in img.pixels() {
        let key = ((p.0[0] as u32 >> 3) << 10) | ((p.0[1] as u32 >> 3) << 5) | (p.0[2] as u32 >> 3);
        *hist.entry(key).or_default() += 1;
    }
    let (modal, _) = hist.iter().max_by_key(|(_, c)| **c).map(|(k, c)| (*k, *c)).unwrap();
    let (mr, mg, mb) = (((modal >> 10) & 0x1f) as i32, ((modal >> 5) & 0x1f) as i32, (modal & 0x1f) as i32);
    let mut shades = std::collections::HashSet::<u32>::new();
    let mut light = 0u64;
    for p in img.pixels() {
        light += p.0[0] as u64 + p.0[1] as u64 + p.0[2] as u64;
        let (r, g, b) = ((p.0[0] >> 3) as i32, (p.0[1] >> 3) as i32, (p.0[2] >> 3) as i32);
        if (r - mr).abs() + (g - mg).abs() + (b - mb).abs() > 3 {
            shades.insert(((r as u32) << 10) | ((g as u32) << 5) | b as u32);
        }
    }
    (shades.len(), light)
}

fn render(g: Graph3D, name: &str) -> image::RgbaImage {
    let mut view = g;
    let mut harness = Harness::builder().with_size(egui::vec2(480.0, 480.0)).build_ui(move |ui| view.ui(ui));
    harness.run_steps(3);
    let img = harness.render().expect("wgpu render produced an image");
    let out_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/snapshots");
    std::fs::create_dir_all(&out_dir).unwrap();
    image::DynamicImage::ImageRgba8(img.clone()).save(out_dir.join(format!("neon_{name}.png"))).ok();
    img
}

#[test]
fn full_decor_is_lit_and_the_shockwave_dim_is_real() {
    const CLOCK: f64 = 2.0; // pinned → deterministic neon (breathing/particles/wave)

    // (a) Full decor, no selection: a lit, many-shade neon frame.
    let unselected = render(graph().with_clock(CLOCK).with_decor(DecorPolicy::Full), "full_unselected");
    let (shades, light_unsel) = measure(&unselected);
    eprintln!("[neon] unselected: {shades} shades, {light_unsel} total light");
    assert!(shades >= 8, "Full decor must paint a many-shade neon frame, got {shades}");

    // (b) Same clock, node 0 selected → shockwave + lit subgraph, the rest dimmed.
    let selected =
        render(graph().with_clock(CLOCK).with_decor(DecorPolicy::Full).with_selected(0), "full_shockwave");
    let (_, light_sel) = measure(&selected);
    eprintln!("[neon] shockwave: {light_sel} total light");
    assert!(
        light_sel < light_unsel,
        "the shockwave/dim must emit LESS total light ({light_sel}) than unselected ({light_unsel})"
    );

    // (c) Flat reference (decor Off) ⇒ far fewer shades than the neon frame: the
    //     neon palette genuinely lit the scene up.
    let flat = render(graph().with_clock(CLOCK).with_decor(DecorPolicy::Off), "flat_reference");
    let (flat_shades, _) = measure(&flat);
    eprintln!("[neon] flat: {flat_shades} shades");
    assert!(shades > flat_shades, "neon ({shades}) is richer than flat ({flat_shades})");
}