facett-geomap/tests/basemap_snapshot.rs
//! **Screenshot PROOF (LAW #4)** — the geomap **basemap** layer.
//!
//! `facett-geomap` used to be point-only (dots + hotspots on a flat themed fill,
//! no map under them). It now hosts an optional vector basemap ([`facett_map::Way`]),
//! drawn under the points + hotspots through the same Web-Mercator viewport. This
//! renders a populated `GeoMap` (the znippy-rabbit silhouette as the basemap +
//! Swedish points + two hotspots) through egui_kittest's wgpu backend, asserts the
//! framebuffer is **non-blank** with basemap geometry (not just sparse dots), and
//! commits the golden PNG so the map is visibly there.
//!
//! Run: `cargo test -p facett-geomap --test basemap_snapshot`
//! Golden: `facett-geomap/tests/snapshots/basemap.png`.
use egui_kittest::Harness;
use facett_core::Facet;
use facett_core::rabbit::Rabbit;
use facett_geomap::{GeoMap, Hotspot, Way};
/// Build a geomap with the znippy-rabbit silhouette as the basemap, placed in
/// lon/lat around a Swedish centre, plus a few points + two hotspots on top — so
/// every layer (basemap, points, hotspots) is exercised in one frame.
fn rabbit_geomap() -> GeoMap {
let (clon, clat) = (13.0_f64, 58.0_f64);
let (sx, sy) = (5.0_f64, 4.6_f64);
let to_lonlat = |(x, y): (f64, f64)| (clon + x * sx, clat + y * sy);
let fur = egui::Color32::from_rgb(196, 201, 214);
let outline = egui::Color32::from_rgb(28, 32, 44);
let eye = egui::Color32::from_rgb(20, 22, 30);
let r = Rabbit::default();
let kinds = ["body", "head", "ear", "ear"];
let mut ways: Vec<Way> = Vec::new();
for (i, lp) in r.silhouette().into_iter().enumerate() {
let lonlat: Vec<(f64, f64)> = lp.into_iter().map(to_lonlat).collect();
ways.push(Way::filled(i as i64, kinds.get(i).copied().unwrap_or("body"), lonlat, fur, outline, 1.8));
}
let eye_loop: Vec<(f64, f64)> = r.eye().into_iter().map(to_lonlat).collect();
ways.push(Way::filled(900, "eye", eye_loop, eye, eye, 0.8));
let points = vec![(clon, clat), (clon + 1.0, clat + 1.0), (clon - 1.0, clat - 0.5), (clon + 2.0, clat - 1.0)];
let hotspots = vec![Hotspot::new(clon + 1.0, clat + 1.0, 120, "north"), Hotspot::new(clon - 1.0, clat - 0.5, 80, "south")];
GeoMap::new("znippy rabbit (geomap basemap)", points, hotspots).with_ways(ways)
}
/// Fraction of pixels that differ from the modal (≈background) colour — a
/// generalizable "did real content draw?" measure. A blank pane scores ~0.
fn content_ratio(img: &image::RgbaImage) -> f64 {
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 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;
}
}
content as f64 / total as f64
}
#[test]
fn geomap_renders_non_blank_with_basemap_and_saves_golden() {
// Inject-assert the geometry first: the basemap is the rabbit (body + head +
// 2 ears + eye = 5 filled loops) AND the point layer is non-empty.
let map = rabbit_geomap();
let s = map.state_json();
assert_eq!(s["ways"], 5, "basemap = body + head + 2 ears + eye");
assert_eq!(s["filled_ways"], 5, "every rabbit loop is a filled silhouette");
assert_eq!(s["way_kinds"]["ear"], 2, "two upright ears");
assert!(s["points"].as_u64().unwrap() > 0, "the existing point layer is still populated");
let mut view = rabbit_geomap();
let mut harness = Harness::builder().with_size(egui::vec2(720.0, 720.0)).build_ui(move |ui| {
facett_core::set_theme(ui.ctx(), facett_core::Theme::sci_fi());
ui.heading("geomap — basemap layer (2D)");
view.ui(ui);
});
harness.run_steps(3);
let img_out = harness.render().expect("wgpu render produced an image");
let ratio = content_ratio(&img_out);
eprintln!("[geomap-basemap] content ratio {ratio:.4}");
// The filled basemap silhouette covers a healthy chunk of the pane — this is
// the proof it is a MAP under the points, not just sparse dots.
assert!(ratio > 0.05, "the basemap silhouette must fill a visible region (got {ratio:.4})");
let out_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/snapshots");
std::fs::create_dir_all(&out_dir).unwrap();
let path = out_dir.join("basemap.png");
image::DynamicImage::ImageRgba8(img_out).save(&path).expect("save PNG");
let bytes = std::fs::metadata(&path).unwrap().len();
assert!(bytes > 5_000, "golden PNG must be non-trivial (got {bytes} bytes)");
eprintln!("facett-geomap basemap snapshot → {} ({bytes} bytes)", path.display());
// Feed the matrix: the geomap basemap (ways) + points rendered non-blank.
// geometry = ways + points (a dead geomap with neither becomes a RED row).
#[cfg(feature = "testmatrix")]
{
let ways = s["ways"].as_u64().unwrap_or(0) as usize;
let points = s["points"].as_u64().unwrap_or(0) as usize;
facett_core::testmatrix::emit_non_blank("GeoMap.basemap", ratio, 0.05, ways + points);
}
}