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

//! **Screenshot PROOF (LAW #4) for the TRAVELS ghost.**
//!
//! Renders [`Graph3D`] through egui_kittest's wgpu backend at a **pinned clock** (so the
//! ghost pulse + spline are a pure function of the clock and the framebuffer is byte
//! -stable) and asserts the guided-tour overlay is REAL:
//!   1. **the ghost paints non-blank** — a graph WITH a tour route emits strictly MORE
//!      light than the identical graph with NO tour (the glowing ghost marker + spline
//!      actually rasterise), and
//!   2. **the tour moves** — stepping the tour clock relocates the ghost, so a later
//!      frame differs from the arrival frame (RED-when-broken: a frozen tour is identical).
//!
//! The camera-follow is disabled here so the ONLY difference between frames is the ghost
//! overlay itself (an isolated, honest oracle).
//!
//! Run: `cargo test -p facett-graph3d --test travels_snapshot`.
//! Saves inspection PNGs to `facett-graph3d/tests/snapshots/travel_*.png`.

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

use facett_core::Facet;
use facett_graph3d::{Graph3D, Layout3D, Msg, Travel, TravelRoute};

/// A deterministic sphere graph with the spin frozen and the yaw pinned at 0.
fn graph() -> Graph3D {
    let nodes: Vec<(String, Color32)> = (0..12).map(|i| (format!("n{i}"), Color32::from_rgb(0, 200, 220))).collect();
    let edges: Vec<(usize, usize)> = (0..12).map(|i| (i, (i + 1) % 12)).collect();
    let mut g = Graph3D::new(nodes, edges, Layout3D::Sphere);
    let _ = g.update(Msg::SetSpin(false));
    let _ = g.update(Msg::SetYaw(0.0));
    g
}

/// Attach a tour with the camera-fly DISABLED (yaw stays pinned) and seek to `clock`.
fn with_tour(mut g: Graph3D, stops: Vec<usize>, clock: f64) -> Graph3D {
    let centers: Vec<[f32; 3]> = g.nodes.iter().map(|n| n.pos).collect();
    g.travel = Some(Travel::new(TravelRoute::new("t", stops), &centers).with_camera_follow(false));
    let _ = g.update(Msg::SetClock(clock));
    g
}

/// Total light Σ(r+g+b) over every pixel, plus the raw pixel bytes for frame diffing.
fn render(g: Graph3D, name: &str) -> (u64, Vec<u8>) {
    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!("travel_{name}.png"))).ok();
    let light: u64 = img.pixels().map(|p| p.0[0] as u64 + p.0[1] as u64 + p.0[2] as u64).sum();
    (light, img.into_raw())
}

#[test]
fn ghost_paints_non_blank_and_the_tour_moves() {
    const CLOCK: f64 = 2.0;

    // No tour — the baseline light at the same pinned yaw.
    let (light_plain, _) = render(graph().with_clock(CLOCK), "none");

    // A tour dwelling at a stop — the ghost marker + spline light up (same yaw).
    let (light_tour, buf_a) = render(with_tour(graph(), vec![0, 4, 8], 0.6), "arrival");
    assert!(
        light_tour > light_plain,
        "the glowing ghost paints non-blank: tour light {light_tour} > plain {light_plain}"
    );

    // Step the tour clock — the ghost relocates, so the frame changes (the tour moves).
    let (_light_later, buf_b) = render(with_tour(graph(), vec![0, 4, 8], 3.5), "later");
    let diff = buf_a.iter().zip(&buf_b).filter(|(a, b)| a != b).count();
    assert!(diff > 0, "advancing the tour clock changed the frame (the ghost moved): {diff} bytes differ");

    #[cfg(feature = "testmatrix")]
    facett_core::testmatrix::emit(
        "facett-graph3d::Graph3D::travel",
        "ghost_non_blank_and_moves",
        light_tour > light_plain && diff > 0,
        &format!("plain_light={light_plain} tour_light={light_tour} moved_bytes={diff}"),
    );
}