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

nordisk/facett

facett-map/tests/render_proof.rs

//! **Render PROOF (LAW #4 — "non-blank render")** for `facett_map::MapView`.
//!
//! The bug class this catches: *data loaded into state, but the pane draws
//! BLACK* — `state_json()["points"] == N > 0`, yet the rendered framebuffer is a
//! single uniform colour (nothing actually drawn). A `state_json` assertion can
//! never see this; only the **pixels** prove it. So we render the populated
//! `MapView` through egui_kittest's wgpu backend and assert the framebuffer is
//! NOT blank — real drawn content, measured as the fraction of pixels that differ
//! from the dominant (background) colour.
//!
//! Run: `cargo test -p facett-map --test render_proof`
//! (needs a GPU/`lavapipe`; the wgpu backend is the same one `facett-demo`'s
//! golden snapshot uses.)

use egui_kittest::Harness;
use facett_core::Facet;
use facett_map::MapView;
/// **The shared one-device-at-a-time lock** — `common/one_gpu.rs`. `egui_kittest::Harness`
/// brings up its own wgpu device, libtest runs `#[test]` bodies on parallel threads, and
/// two device bring-ups at once take this binary out with a hard `signal: 11` — not a test
/// failure, a SIGSEGV whose output looks nothing like the assertion that was running.
/// MEASURED 2026-08-27 on oden by taking the lock back OUT of the two files that had it:
/// `rail_tier_pixels` 2/5 runs and `place_label_tier_pixels` 3/5 runs died that way. This
/// file rendered 0/8 without it, which is a LOWER rate and not a safe one — the hazard is
/// the concurrent bring-up, and every file that does one takes the same turn.
#[path = "common/one_gpu.rs"]
mod one_gpu;

/// **Generalizable non-blank assertion.** Render `build` into an `RgbaImage`, then
/// measure how "alive" it is: the fraction of pixels that differ noticeably from
/// the *modal* (most common ≈ background) colour. A black / uniform pane scores
/// ~0; a pane with real drawn content scores well above `min_ratio`.
///
/// Returns the measured ratio so callers can log it. Panics with a descriptive
/// message (naming `label`) if the render is blank — this is the proof.
pub fn assert_non_blank(label: &str, min_ratio: f64, mut build: impl FnMut(&mut egui::Ui)) -> f64 {
    let _gpu = one_gpu::hold();
    let mut harness = Harness::builder()
        .with_size(egui::vec2(900.0, 640.0))
        .build_ui(|ui| build(ui));
    // A few frames so layout + the custom painter settle.
    harness.run_steps(3);
    let img = harness.render().expect("wgpu render produced an image");

    // Modal colour via a coarse RGB histogram (5-bit/channel buckets) → the
    // background dominates; "content" = pixels far from that bucket.
    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_key, _) = hist.iter().max_by_key(|(_, c)| **c).map(|(k, c)| (*k, *c)).unwrap();
    let (mr, mg, mb) = (
        ((modal_key >> 10) & 0x1f) as i32,
        ((modal_key >> 5) & 0x1f) as i32,
        (modal_key & 0x1f) as i32,
    );
    let mut content = 0usize;
    let total = (img.width() * img.height()) as usize;
    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 {
            content += 1;
        }
    }
    let ratio = content as f64 / total as f64;
    eprintln!("[render-proof] {label}: content ratio {ratio:.4} (modal bucket = bg), min {min_ratio}");
    assert!(
        ratio >= min_ratio,
        "{label}: rendered pane is BLANK/uniform — content ratio {ratio:.4} < {min_ratio}. \
         Points are in state but nothing drew (the black-pane bug)."
    );
    ratio
}

/// A dense Liechtenstein-shaped lon/lat cluster — the shape the real
/// `nodes.parquet` carries (~9.5°E / 47.1°N), without needing the data file.
fn liechtenstein_like(n: usize) -> Vec<(f64, f64)> {
    (0..n)
        .map(|i| {
            let f = i as f64 / n as f64;
            let lon = 9.47 + f * 0.20 + (f * 31.0).sin() * 0.02;
            let lat = 47.05 + (f * 7.0).sin().abs() * 0.45 + (f * 53.0).cos() * 0.01;
            (lon, lat)
        })
        .collect()
}

/// **The proof.** A populated MapView, drawn the way korp's `draw_map` draws it
/// (sci-fi theme, inside a CentralPanel after the heading/controls), must produce
/// a non-blank framebuffer. Without the auto-fit + min-rect guard, the cluster
/// would project off-screen / into a degenerate rect → black, and this fails.
#[test]
fn populated_map_renders_non_blank() {
    let pts = liechtenstein_like(40_000);
    let n = pts.len();
    // Threshold 0.01: cleanly separates the empty pane (~0.0007) from a populated
    // one (~0.028) — a ~40x gap — so it rejects blank without being brittle.
    let ratio = assert_non_blank("MapView (40k Liechtenstein-like pts)", 0.01, |ui| {
        facett_core::set_theme(ui.ctx(), facett_core::Theme::sci_fi());
        egui::CentralPanel::default().show(ui, |ui| {
            ui.heading("🗺 Map — real OSM");
            ui.label("Web-Mercator plot.");
            ui.add(egui::TextEdit::singleline(&mut String::new()).desired_width(f32::INFINITY));
            ui.label(format!("{n} points"));
            ui.separator();
            MapView::new("map", pts.clone()).ui(ui);
        });
    });
    // A real cluster fills a healthy chunk of the pane.
    assert!(ratio > 0.01, "the cluster fills a visible region");
    // Feed the matrix: ok iff the populated map rendered non-blank with points.
    #[cfg(feature = "testmatrix")]
    facett_core::testmatrix::emit_non_blank("MapView.points", ratio, 0.01, n);
}

/// **Negative control** — proves the assertion actually distinguishes blank from
/// drawn (not "always passes"). An EMPTY MapView is genuinely blank (just the
/// background + a tiny "no points" hint), so `assert_non_blank` at the same
/// threshold MUST reject it.
#[test]
fn empty_map_is_detected_as_blank() {
    let blank = std::panic::catch_unwind(|| {
        assert_non_blank("MapView (empty)", 0.01, |ui| {
            facett_core::set_theme(ui.ctx(), facett_core::Theme::sci_fi());
            egui::CentralPanel::default().show(ui, |ui| {
                MapView::new("map", Vec::new()).ui(ui);
            });
        });
    });
    assert!(blank.is_err(), "an empty map MUST fail the non-blank proof (else the proof is vacuous)");
    // Feed the matrix: ok iff the proof correctly REJECTED the empty (blank) map
    // — proving the non-blank gate is not vacuous.
    #[cfg(feature = "testmatrix")]
    facett_core::testmatrix::emit(
        "MapView.empty",
        "blank_is_rejected",
        blank.is_err(),
        "empty map correctly fails the non-blank proof",
    );
}

/// **Vector basemap proof.** A `MapView` with a real ways layer (roads + water)
/// must render non-blank — the lines actually tessellate + paint. This is the
/// proof the ported basemap draws, not just dots.
#[test]
fn basemap_ways_render_non_blank() {
    use facett_map::Way;
    let theme = facett_core::Theme::sci_fi();
    let tag = |k: &str, v: &str| vec![(k.to_string(), v.to_string())];
    let mut ways: Vec<Way> = Vec::new();
    for i in 0..60 {
        let f = i as f64 / 60.0;
        let lon0 = 9.47 + f * 0.18;
        if let Some(w) = Way::from_lonlat(i, &[(lon0, 47.05), (lon0 + 0.01, 47.45)], &tag("highway", "primary"), &theme) {
            ways.push(w);
        }
        if let Some(w) = Way::from_lonlat(1000 + i, &[(9.47, 47.05 + f * 0.4), (9.65, 47.06 + f * 0.4)], &tag("natural", "water"), &theme) {
            ways.push(w);
        }
    }
    let n = ways.len();
    assert!(n > 50, "built a basemap of {n} ways");
    let ratio = assert_non_blank("MapView basemap (roads+water ways)", 0.01, move |ui| {
        facett_core::set_theme(ui.ctx(), facett_core::Theme::sci_fi());
        egui::CentralPanel::default().show(ui, |ui| {
            ui.heading("🗺 Map — real OSM basemap");
            MapView::new("map", vec![(9.55, 47.15)]).with_ways(ways.clone()).ui(ui);
        });
    });
    assert!(ratio > 0.01, "the basemap lines fill a visible region");
    // Feed the matrix: ok iff the vector basemap (ways) rendered non-blank.
    #[cfg(feature = "testmatrix")]
    facett_core::testmatrix::emit_non_blank("MapView.ways", ratio, 0.01, n);
}

/// Count the fraction of pixels whose colour is close to `target` (RGB L1 within
/// `tol`/channel). Used to prove a **filled area** of a given hue actually
/// covered pixels (a line basemap would have ~none of a fill colour).
fn color_coverage(img: &image::RgbaImage, target: egui::Color32, tol: i32) -> f64 {
    let (tr, tg, tb) = (target.r() as i32, target.g() as i32, target.b() as i32);
    let mut hits = 0usize;
    for p in img.pixels() {
        let (r, g, b) = (p.0[0] as i32, p.0[1] as i32, p.0[2] as i32);
        if (r - tr).abs() <= tol && (g - tg).abs() <= tol && (b - tb).abs() <= tol {
            hits += 1;
        }
    }
    hits as f64 / (img.width() * img.height()) as f64
}

/// **The "looks like a real map" proof.** A multi-feature fixture — a **water
/// polygon** (closed ring) + a **park polygon** + a few **roads** — must render
/// (a) non-blank AND (b) with genuinely **filled areas**, not just lines. The
/// content gate is a *filled-pixel ratio*: we measure how much of the pane is
/// covered by the themed water fill + green fill. A bare-lines basemap (the old
/// "looks like shit" behaviour) would score ~0 on fill coverage even while
/// scoring on non-blank — so this is the gate that the areas actually fill.
#[test]
fn multi_feature_basemap_has_filled_areas_not_just_lines() {
    use facett_map::{MapPalette, Way};
    let theme = facett_core::Theme::default();
    let pal = MapPalette::from_theme(&theme);
    let tag = |k: &str, v: &str| vec![(k.to_string(), v.to_string())];

    // A closed water ring (a lake), a closed park ring, and three roads crossing.
    let lake = [(9.48, 47.10), (9.58, 47.10), (9.58, 47.22), (9.48, 47.22), (9.48, 47.10)];
    let park = [(9.50, 47.12), (9.54, 47.12), (9.54, 47.16), (9.50, 47.16), (9.50, 47.12)];
    let mut ways: Vec<Way> = Vec::new();
    ways.push(Way::from_lonlat(1, &lake, &tag("natural", "water"), &theme).unwrap());
    ways.push(Way::from_lonlat(2, &park, &tag("leisure", "park"), &theme).unwrap());
    for i in 0..3 {
        let y = 47.11 + i as f64 * 0.03;
        ways.push(Way::from_lonlat(10 + i, &[(9.47, y), (9.60, y + 0.05)], &tag("highway", "primary"), &theme).unwrap());
    }
    // Inject-assert the classification BEFORE rendering: the lake + park filled,
    // the roads are lines.
    assert_eq!(ways.iter().filter(|w| w.fill.is_some()).count(), 2, "lake + park are filled areas");
    assert_eq!(ways.iter().filter(|w| w.fill.is_none()).count(), 3, "three road lines");

    // Render + measure the fill coverage of the water + green areas.
    let render_ways = ways.clone();
    let _gpu = one_gpu::hold();
    let mut harness = Harness::builder().with_size(egui::vec2(720.0, 640.0)).build_ui(move |ui| {
        facett_core::set_theme(ui.ctx(), theme);
        ui.heading("🗺 Map — real OSM basemap");
        MapView::new("map", vec![]).with_ways(render_ways.clone()).ui(ui);
    });
    harness.run_steps(3);
    let img = harness.render().expect("wgpu render produced an image");
    let water_cov = color_coverage(&img, pal.water, 14);
    let green_cov = color_coverage(&img, pal.green, 14);
    // Save a golden so the human can eyeball the map.
    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).save(out_dir.join("real_map_2d.png")).expect("save PNG");
    eprintln!("[real-map] water fill coverage {water_cov:.4}, green fill coverage {green_cov:.4}");
    // The water lake fills a real chunk of the pane (it is the biggest area).
    assert!(water_cov > 0.05, "the water polygon must FILL a visible region (got {water_cov:.4}) — not just a line");
    // The park (a smaller, nested green area) is also visibly filled.
    assert!(green_cov > 0.005, "the park polygon must show as a green FILL (got {green_cov:.4})");
    // Feed the matrix: ok iff the filled areas actually covered pixels (not just
    // lines). geometry = the 5 ways (2 filled + 3 roads).
    #[cfg(feature = "testmatrix")]
    facett_core::testmatrix::emit(
        "MapView.filled_areas",
        "water_and_park_fill",
        water_cov > 0.05 && green_cov > 0.005,
        &format!("water_cov={water_cov:.4} green_cov={green_cov:.4} ways={}", ways.len()),
    );
}

/// **Black-pane guard** — even when the host hands the map almost no vertical
/// room, the min-rect floor keeps it drawable, so a populated map never renders
/// black just because its column collapsed.
#[test]
fn tiny_room_still_renders_non_blank() {
    let pts = liechtenstein_like(20_000);
    // `_ratio` is consumed only under the `testmatrix` feature; the leading
    // underscore keeps the default (feature-off) build warning-clean while the
    // `assert_non_blank` call still runs its non-blank assertion every time.
    let _ratio = assert_non_blank("MapView (squeezed into a horizontal row)", 0.01, |ui| {
        facett_core::set_theme(ui.ctx(), facett_core::Theme::sci_fi());
        egui::CentralPanel::default().show(ui, |ui| {
            ui.horizontal_top(|ui| {
                ui.set_max_height(8.0); // host gives ~no vertical room
                MapView::new("map", pts.clone()).ui(ui);
            });
        });
    });
    // Feed the matrix: ok iff the squeezed map still rendered (min-rect floor).
    #[cfg(feature = "testmatrix")]
    facett_core::testmatrix::emit_non_blank("MapView.tiny_room", _ratio, 0.01, pts.len());
}

/// **The land-vs-sea PIXEL proof — `MapFacet`'s landmass must not be painted in the
/// colour of the pane behind it.**
///
/// This is the guard that could not exist before [`MapTheme::sea`] did. `MapTheme::day`
/// set `land: palette.land` and `MapFacet::ui`'s first act washed the whole pane with
/// `theme.land`, so a [`LandmassLayer`] filled land-colour over a land-colour pane. The
/// polygon really was tessellated and really was drawn — and changed **not one pixel**.
/// Measured on korp's shipped 🗺 Map: 95.63 % of the map canvas was the single value
/// `#1f1f26`, which is exactly `MapPalette::from_theme(&Theme::default()).land`, and the
/// entire 20 000-way payload had nothing to read against.
///
/// So this asserts the APPLIED OUTPUT, not the colour arithmetic: render a facet whose
/// only layer is a landmass ring and require **two** large-area colours in the
/// framebuffer — land inside the silhouette, sea outside it. On the old code the two
/// coverages were the same colour, so the sea coverage was ~0 and the dominant share
/// ~1.0: this test goes RED. It is also the guard against re-introducing the identity
/// from either side, since it never mentions which of the two moved.
/// **All FOUR presets, not just `day`.** MEASURED 2026-08-22: setting
/// `MapTheme::cartographic`'s `sea` to `palette.land` — the preset korp's shipped
/// map actually uses — left all 140 default facett-map tests green, because this
/// proof only ever built `MapTheme::day`. The same mutation on `day` failed it at
/// once. A guard that watches one of four constructors is a guard over a quarter of
/// the surface; the loop is what closes that.
#[test]
fn a_landmass_reads_against_the_sea_in_the_rendered_pixels() {
    use facett_map::layer::{LandmassLayer, LatLon, MapFacet, MapTheme, MapView as FacetView};

    let theme = facett_core::Theme::default();
    // Every constructor a host can reach. `cartographic` ships in korp's map lane.
    type Preset = (&'static str, fn(&facett_core::Theme) -> MapTheme);
    let presets: [Preset; 4] = [
        ("day", MapTheme::day),
        ("night", MapTheme::night),
        ("cartographic-day", |t| MapTheme::cartographic(t, true)),
        ("cartographic-night", |t| MapTheme::cartographic(t, false)),
    ];

    for (name, make) in presets {
        let map_theme = make(&theme);
        let land = map_theme.land;
        let sea = map_theme.sea;
        // The land and the sea must be two colours at all. (The cheap half of the
        // proof — the pixels below are the half that cannot be satisfied by
        // arithmetic alone.)
        assert_ne!(
            land, sea,
            "MapTheme::{name} paints the landmass and the pane in ONE colour ({land:?}) — a \
             coastline cannot exist, whatever the data says",
        );

        // A big rectangular "country" around the middle of the pane, so the
        // silhouette is a real shape and not the whole canvas.
        let country: Vec<(f64, f64)> =
            vec![(12.0, 56.0), (20.0, 56.0), (20.0, 63.0), (12.0, 63.0), (12.0, 56.0)];
        let view = FacetView::new(LatLon::new(59.5, 16.0), 6.0, (900.0, 640.0));
        let _gpu = one_gpu::hold();
        let mut harness =
            Harness::builder().with_size(egui::vec2(900.0, 640.0)).build_ui(move |ui| {
                facett_core::set_theme(ui.ctx(), theme);
                let mut facet = MapFacet::new("land vs sea", view, make(&theme));
                facet.push_layer(Box::new(LandmassLayer::from_rings(
                    "landmass",
                    vec![country.clone()],
                    land,
                )));
                facet.ui(ui);
            });
        harness.run_steps(3);
        let img = harness.render().expect("wgpu render produced an image");

        let land_cov = color_coverage(&img, land, 6);
        let sea_cov = color_coverage(&img, sea, 6);
        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)
            .save(out_dir.join(format!("land_vs_sea-{name}.png")))
            .expect("save PNG");
        eprintln!("[land-vs-sea] {name}: land coverage {land_cov:.4}, sea coverage {sea_cov:.4}");

        // BOTH must cover a real share. Either one at ~0 is the failure: land at 0
        // means the silhouette never drew, sea at 0 means it drew in the pane's own
        // colour.
        assert!(
            land_cov > 0.10,
            "{name}: the landmass silhouette must FILL a visible region (got {land_cov:.4} of \
             the pane)",
        );
        assert!(
            sea_cov > 0.10,
            "{name}: the pane outside the landmass must read as SEA, not as more land (got \
             {sea_cov:.4}) — this is the 95.63 %-one-colour map, where the country was painted \
             in the colour of the pane behind it",
        );
    }
}