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

nordisk/facett

facett-map3d/tests/golden_248.rs

//! **GOLDEN-IMAGE ORACLE @ 248° — the flagged residual-green-wedge pose.**
//!
//! The owner flagged a specific camera pose (azimuth **248°**, street-level
//! elevation, dollied in) where a residual green terrain wedge could smear into
//! the frame. This test pins that exact pose as a committed **golden PNG** and,
//! on every later run, **perceptually diffs** a fresh render against it — so the
//! smear can never silently return at that pose.
//!
//! Pipeline (reusing the existing harnesses):
//!   * geometry — a small deterministic Sigtuna-ish town of `building` rings
//!     (the same 5×5 `from_ways` town the matrix grid sweeps), so the pose is
//!     reproducible without any external data;
//!   * pose — aim at `render_camera().target`, then force
//!     `azimuth = 248°`, `elevation = 0.05`, `distance = 0.4` (live + goal),
//!     `spin = false`, a fixed clock — a pure, reproducible camera;
//!   * pixels — drive `Map3D::ui` through `egui_kittest`'s wgpu backend at
//!     600×600, `run_steps(3)`, `harness.render()` → `RgbaImage` (the same path
//!     as `render_proof.rs`);
//!   * diff — first run **writes** the golden; later runs **load + compare** with
//!     a PERCEPTUAL tolerance diff (per-pixel max-channel delta + a budget on the
//!     fraction of differing pixels), NOT exact bytes (AA / glyph nondeterminism);
//!   * numeric oracles — assert `ink_outside_rect == 0` and `lit_px > 0` at this
//!     pose, so the golden image and the numeric oracles must agree;
//!   * sensitivity control — prove that a deliberately-broken render (the
//!     pre-fix pipeline with the view-space clip turned OFF) diffs ABOVE the
//!     budget, so the golden is a real tripwire and not a rubber stamp.
//!
//! Run: `cargo test -p facett-map3d --features wgpu golden_248`
//! Golden: `facett-map3d/tests/snapshots/map3d_248.png`.

use egui::{Color32, Pos2};
use facett_core::{Facet, Theme};
use facett_map::Way;
use facett_map3d::{clip_poly_to_rect, Map3D};
use image::RgbaImage;

// ── the flagged pose ──────────────────────────────────────────────────────────
const AZIMUTH_DEG: f32 = 248.0;
const ELEVATION: f32 = 0.05;
const DISTANCE: f32 = 0.4;
const CLOCK_S: f64 = 1.2;
const CANVAS: f32 = 600.0;

// ── perceptual-diff thresholds ───────────────────────────────────────────────
/// A pixel counts as "different" only if its largest channel delta exceeds this
/// (8-bit). Soaks up anti-aliasing + glyph-hinting jitter on the HUD text.
const CHANNEL_TOL: i32 = 24;
/// The render may differ on at most this fraction of pixels before it is a
/// regression. AA fringes around the town silhouette + the HUD baseline are a
/// few per-mille; a returned green wedge repaints a large block, far above this.
const MAX_DIFF_FRACTION: f64 = 0.02;

/// A deterministic `building=yes` ring (lon/lat) → a `facett_map::Way`.
fn building(id: i64, ring: &[(f64, f64)]) -> Way {
    let tags = vec![("building".to_string(), "yes".to_string())];
    Way::from_lonlat(id, ring, &tags, &Theme::default()).expect("a building way")
}

/// The deterministic Sigtuna-ish town — a 5×5 block of small building footprints,
/// byte-for-byte the geometry the matrix grid's `Geom::Town` sweeps.
fn town_rings() -> Vec<Way> {
    let mut ways = Vec::new();
    let mut id = 1;
    for r in 0..5 {
        for c in 0..5 {
            let bx = 17.70 + c as f64 * 0.0015;
            let by = 59.61 + r as f64 * 0.0015;
            ways.push(building(
                id,
                &[(bx, by), (bx + 0.0008, by), (bx + 0.0008, by + 0.0008), (bx, by + 0.0008), (bx, by)],
            ));
            id += 1;
        }
    }
    ways
}

/// The documented WORST-CASE smear-bait town — a 6×6 block spread across the
/// design box so a street-level dolled-in camera straddles the near plane (the
/// exact geometry the `clip_pipeline` bug-oracle uses to provoke the green wedge).
/// Used only by the sensitivity control, not the golden.
fn smear_bait_town() -> Vec<Way> {
    let mut ways = Vec::new();
    let mut id = 1;
    for r in 0..6 {
        for c in 0..6 {
            let bx = 9.50 + c as f64 * 0.0015;
            let by = 47.10 + r as f64 * 0.0015;
            ways.push(building(
                id,
                &[(bx, by), (bx + 0.0008, by), (bx + 0.0008, by + 0.0008), (bx, by + 0.0008), (bx, by)],
            ));
            id += 1;
        }
    }
    ways
}

/// Build the town and aim the camera at the flagged 248° pose (deterministic).
fn town_at_flagged_pose() -> Map3D {
    let mut m = Map3D::from_ways("golden-248", &town_rings(), 0.22).with_clock(CLOCK_S);
    m.spin = false; // hold the chosen angle; render_camera adds no spin
    let tgt = m.render_camera().target;
    m.cam.target = tgt;
    m.cam.goal_target = tgt;
    let az = AZIMUTH_DEG.to_radians();
    m.cam.azimuth = az;
    m.cam.goal_azimuth = az;
    m.cam.elevation = ELEVATION; // clamp_elev lifts any below-ground request
    m.cam.goal_elevation = ELEVATION;
    m.cam.distance = DISTANCE;
    m.cam.goal_distance = DISTANCE;
    m.cam.settle();
    m
}

/// `lit_px` — pixels far from the modal (≈background) colour. Same modal-distance
/// metric `render_proof` / the selftest use, as an absolute count.
fn lit_pixels(img: &RgbaImage) -> usize {
    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 lit = 0usize;
    for p in img.pixels() {
        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 {
            lit += 1;
        }
    }
    lit
}

/// The worst projected face extent (px) — the smear oracle. The fixed pipeline
/// keeps this ≤ a sane multiple of the viewport; the legacy path lets it explode.
fn max_extent(faces: &[(f32, [Pos2; 3], Color32)]) -> f32 {
    let mut worst = 0.0f32;
    for (_d, pts, _c) in faces {
        let ex = pts[0].x.max(pts[1].x).max(pts[2].x) - pts[0].x.min(pts[1].x).min(pts[2].x);
        let ey = pts[0].y.max(pts[1].y).max(pts[2].y) - pts[0].y.min(pts[1].y).min(pts[2].y);
        worst = worst.max(ex.max(ey));
    }
    worst
}

/// Perceptual diff: `(differing_pixels, total_pixels, max_channel_delta_seen)`.
/// A pixel is "different" iff its largest channel delta exceeds `CHANNEL_TOL`.
fn perceptual_diff(a: &RgbaImage, b: &RgbaImage) -> (usize, usize, i32) {
    assert_eq!(a.dimensions(), b.dimensions(), "golden + fresh render must share dimensions");
    let total = (a.width() * a.height()) as usize;
    let mut differing = 0usize;
    let mut worst = 0i32;
    for (pa, pb) in a.pixels().zip(b.pixels()) {
        let d = (pa.0[0] as i32 - pb.0[0] as i32)
            .abs()
            .max((pa.0[1] as i32 - pb.0[1] as i32).abs())
            .max((pa.0[2] as i32 - pb.0[2] as i32).abs());
        worst = worst.max(d);
        if d > CHANNEL_TOL {
            differing += 1;
        }
    }
    (differing, total, worst)
}

/// Render the flagged pose through the live `ui()` path on the wgpu backend.
fn render_flagged_pose() -> RgbaImage {
    let mut view = town_at_flagged_pose();
    let mut harness = egui_kittest::Harness::builder()
        .with_size(egui::vec2(CANVAS, CANVAS))
        .build_ui(move |ui| {
            facett_core::set_theme(ui.ctx(), facett_core::Theme::sci_fi());
            view.ui(ui);
        });
    harness.run_steps(3);
    harness.render().expect("wgpu render produced an image")
}

#[test]
fn golden_image_oracle_at_248_degrees() {
    // ── 0. inject-assert the geometry actually built (a real extruded town) ────
    let probe = town_at_flagged_pose();
    let s = probe.state_json();
    assert!(s["triangles"].as_u64().unwrap() > 20, "a real extruded town mesh ({s:?})");
    assert!(s["tiles"].as_u64().unwrap() >= 20, "the 5×5 town's footprints");
    let deg = (probe.render_camera().azimuth.to_degrees()).rem_euclid(360.0);
    assert!((deg - 248.0).abs() < 0.01, "the camera holds the flagged 248° pose (got {deg:.3}°)");

    // ── 1. render the flagged pose to pixels ──────────────────────────────────
    let fresh = render_flagged_pose();
    assert_eq!(fresh.dimensions(), (CANVAS as u32, CANVAS as u32), "600×600 framebuffer");

    // ── 2. numeric EMIT oracles at 248° (must agree with the golden) ───────────
    let rect = egui::Rect::from_min_max(Pos2::new(0.0, 0.0), Pos2::new(CANVAS, CANVAS));
    let center = rect.center();
    let scale = rect.height() * 0.5;
    // The bg the live `ui()` renderer uses (it reads `theme(ui)`, which is the
    // `set_theme`-published sci-fi theme) — so the headless oracle shades against
    // the same background colour the screenshot was painted on.
    let bg = Theme::sci_fi().bg;
    let faces_fixed = probe.shade_faces(center, scale, bg);
    let mut ink_outside_rect = 0usize;
    for (_d, pts, _c) in &faces_fixed {
        for p in &clip_poly_to_rect(pts, rect) {
            let over = (rect.left() - p.x).max(p.x - rect.right()).max(rect.top() - p.y).max(p.y - rect.bottom());
            if over > 1e-2 {
                ink_outside_rect += 1;
            }
        }
    }
    let lit_px = lit_pixels(&fresh);
    eprintln!("[golden_248] pose az={AZIMUTH_DEG}° elev={ELEVATION} dist={DISTANCE} → ink_outside_rect={ink_outside_rect} lit_px={lit_px} faces={}", faces_fixed.len());
    assert_eq!(ink_outside_rect, 0, "NO ink escapes the rect at the flagged pose (the wedge is clipped)");
    assert!(lit_px > 0, "the flagged pose renders visible lit geometry (got lit_px={lit_px})");

    // ── 3. golden compare (first run = generate + pass) ────────────────────────
    let out_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/snapshots");
    std::fs::create_dir_all(&out_dir).unwrap();
    let golden_path = out_dir.join("map3d_248.png");

    if !golden_path.exists() {
        image::DynamicImage::ImageRgba8(fresh.clone()).save(&golden_path).expect("save golden PNG");
        let bytes = std::fs::metadata(&golden_path).unwrap().len();
        assert!(bytes > 2_000, "golden PNG must be non-trivial (got {bytes} bytes)");
        eprintln!(
            "[golden_248] FIRST RUN — wrote golden {} ({bytes} bytes). Visually bless, then commit.",
            golden_path.display()
        );
    } else {
        let golden = image::open(&golden_path).expect("load golden PNG").to_rgba8();
        let (differing, total, worst) = perceptual_diff(&golden, &fresh);
        let frac = differing as f64 / total as f64;
        eprintln!(
            "[golden_248] perceptual diff vs golden: differing={differing}/{total} ({:.4}%) worst_channel_delta={worst} (tol={CHANNEL_TOL}, budget={:.2}%)",
            frac * 100.0,
            MAX_DIFF_FRACTION * 100.0
        );
        assert!(
            frac <= MAX_DIFF_FRACTION,
            "VISUAL REGRESSION at 248°: {:.4}% of pixels differ (> {:.2}% budget). The smear may be back — inspect {}.",
            frac * 100.0,
            MAX_DIFF_FRACTION * 100.0,
            golden_path.display()
        );
    }

    // ── 4. SENSITIVITY CONTROL — prove the diff method is not a rubber stamp ────
    // Rasterize the FIXED pipeline (near + screen-extent clip ON) and the BROKEN
    // pre-fix pipeline (clip OFF: the brittle drop-whole / unbounded-extent path
    // that produced the green wedge), then perceptually diff them with the SAME
    // metric the golden uses. They must diff ABOVE the budget — so if a regression
    // ever reintroduced that broken geometry into the live render, the golden diff
    // (step 3) would likewise fire.
    //
    // The wedge is a near-plane-straddle artifact, so the control uses the
    // documented WORST-CASE smear-bait pose (street-level, dollied all the way in,
    // looking across the town) rather than the flagged-but-already-safe 248°
    // pose — at 248° the fixed + broken geometry happen to coincide (nothing
    // straddles the eye), which is exactly why 248° is now safe. Driving the
    // control from the bait pose proves the perceptual diff *catches the bug
    // class* the golden is meant to guard.
    let mut bait = Map3D::from_ways("golden-248-control", &smear_bait_town(), 0.22);
    bait.spin = false;
    bait.cam.target = facett_map3d::V3::new(0.0, 0.02, 0.0);
    bait.cam.goal_target = bait.cam.target;
    bait.cam.distance = 0.42;
    bait.cam.goal_distance = 0.42;
    bait.view_preset(facett_map3d::ViewPreset::Front);
    bait.cam.elevation = 0.03;
    bait.cam.goal_elevation = 0.03;
    bait.cam.settle();
    let bait_fixed = bait.shade_faces(center, scale, bg);
    let bait_broken = bait.shade_faces_clip(center, scale, bg, false);

    // (4a) GEOMETRIC signal — the fixed path bounds every face to ≤ viewport·K
    // (the screen-extent guard), while the broken (clip-off) path lets a
    // grazing-near-plane face project to a strictly LARGER extent: the green wedge
    // the rect-scissor then smears across the component. This is the same decisive
    // anti-smear invariant the clip_pipeline bug-oracle asserts (legacy extent >
    // fixed extent), proving the control pose really exercises the bug class.
    // VIEWPORT-TIGHT cap (was `scale * 8.0`). `SCREEN_BOUND_K = 1.0` ⇒ a
    // fully-clipped face spans at most the viewport (2·scale); +1px float epsilon.
    let cap = scale * 2.0 + 1.0;
    let fixed_extent = max_extent(&bait_fixed);
    let broken_extent = max_extent(&bait_broken);
    // PASS-ON-FIX: the production pipeline never exceeds the viewport.
    assert!(fixed_extent <= cap, "the fixed pipeline stays bounded (ext={fixed_extent:.0} ≤ cap={cap:.0})");
    // FAIL-ON-BUG, against the CAP — not against a slack multiple of the fixed
    // extent. This is what makes the tightened cap a real oracle: the pre-fix
    // pipeline BREACHES it at the bait pose. (Was `broken > fixed * 1.4`, a
    // relative slack multiplier that never touched the cap under test.)
    assert!(
        broken_extent > cap,
        "sensitivity control: the pre-fix pipeline must BREACH the viewport cap \
         (broken_ext={broken_extent:.0}px, cap={cap:.0}px) — otherwise the cap cannot detect the smear"
    );
    // THE LOOPHOLE, demonstrated in one assertion: the very same smear sits UNDER
    // the historical `scale · 8` cap, so the OLD oracle stayed green on broken
    // code. Tightening the cap from 8× to 2× the half-viewport is what converts
    // this class of test from decorative to load-bearing.
    let historical_loose_cap = scale * 8.0;
    assert!(
        broken_extent <= historical_loose_cap,
        "the historical loose cap ({historical_loose_cap:.0}px) would have PASSED this \
         {broken_extent:.0}px smear — that is the loophole this round closed"
    );
    assert!(
        broken_extent > fixed_extent * 1.4,
        "sensitivity control: the broken pipeline must smear to a much larger extent than the fix (broken_ext={broken_extent:.0}, fixed_ext={fixed_extent:.0})"
    );

    let budget_pct = MAX_DIFF_FRACTION * 100.0;
    eprintln!(
        "[golden_248] sensitivity control (geometric) @ smear-bait pose: fixed_ext={fixed_extent:.0}px broken_ext={broken_extent:.0}px cap={cap:.0}px (broken/fixed={:.2}×) fixed_faces={} broken_faces={}",
        broken_extent / fixed_extent.max(1.0),
        bait_fixed.len(),
        bait_broken.len(),
    );

    // (4b) PERCEPTUAL signal — prove the EXACT diff method step 3 uses (the
    // `perceptual_diff` + budget) actually FIRES on a wedge. Paint the broken
    // smear's screen-space footprint as a solid green block into a copy of the
    // fresh render, then run the same compare: it must exceed the budget. The
    // wedge block is the screen extent the broken pipeline projected (`broken_ext`
    // wide) in the terrain-green the residual wedge was — so this is the literal
    // failure step 3 guards, fed through the real comparator.
    //
    // Size the wedge to the broken pipeline's EXTRA span over the fix (the smear
    // the screen-extent guard removes), not the whole frame — so the control
    // proves the comparator catches a realistic SUB-frame wedge, not a trivial
    // full repaint.
    let wedge_px = ((broken_extent - fixed_extent) as u32).clamp(1, CANVAS as u32);
    let mut corrupted = fresh.clone();
    let green = image::Rgba([60u8, 180, 90, 255]); // the residual terrain wedge hue
    for y in 0..wedge_px.min(corrupted.height()) {
        for x in 0..wedge_px.min(corrupted.width()) {
            corrupted.put_pixel(x, y, green);
        }
    }
    let (cdiff, ctotal, cworst) = perceptual_diff(&fresh, &corrupted);
    let cfrac = cdiff as f64 / ctotal as f64;
    let pct = cfrac * 100.0;
    let wedge_frac = (wedge_px * wedge_px) as f64 / ctotal as f64;
    eprintln!(
        "[golden_248] sensitivity control (perceptual): a {wedge_px}×{wedge_px}px green wedge ({:.2}% of frame) → diff {cdiff}/{ctotal} ({pct:.4}%) worst_delta={cworst} — must exceed {budget_pct:.2}% budget",
        wedge_frac * 100.0
    );
    assert!(
        broken_extent > fixed_extent * 1.4,
        "sensitivity control: the broken pipeline must smear to a much larger extent than the fix (broken_ext={broken_extent:.0}, fixed_ext={fixed_extent:.0})"
    );
    assert!(
        cfrac > MAX_DIFF_FRACTION,
        "the golden diff is NOT sensitive: a {wedge_px}px green wedge only moved the perceptual diff to {pct:.4}% (≤ budget) — the comparator would rubber-stamp a regression"
    );

    // ── EMIT one row so the warehouse records the golden oracle ────────────────
    #[cfg(feature = "testmatrix")]
    facett_core::testmatrix::emit(
        "facett-map3d",
        "golden_248",
        ink_outside_rect == 0 && lit_px > 0 && cfrac > MAX_DIFF_FRACTION,
        &format!(
            "az={AZIMUTH_DEG} elev={ELEVATION} dist={DISTANCE} ink_outside_rect={ink_outside_rect} lit_px={lit_px} control_diff_frac={:.4} budget={MAX_DIFF_FRACTION}",
            cfrac
        ),
    );
}