facett-demo/tests/osm2d_holes.rs
//! **Multipolygon-with-holes fill proof + before/after render** for the 2D OSM
//! viewer. Drives the REAL pipeline (WKB → `facett_osm::ways_from_wkb` → `OsmView`):
//! a lake (`natural=water`) carrying an **island hole**, plus a 2-member
//! `landuse=forest` **multipolygon**. It renders the scene twice through the wgpu
//! harness and saves both PNGs to the workspace scratch dir so the fill can be
//! eyeballed:
//! * `osm2d-before.png` — the hole NOT punched (island filled solid as water),
//! the pre-fix symptom ("islands filled in"), reproduced by clearing the ways'
//! hole rings; and
//! * `osm2d-after.png` — the hole PUNCHED (the island shows as land), the fix.
//!
//! It also asserts, on the fixed pixels, that the island centre is NOT water — a
//! red-when-broken end-to-end guard for the hole-punch.
//!
//! Run: `cargo test -p facett-demo --test osm2d_holes -- --nocapture`
use egui_kittest::Harness;
use facett::Facet;
use facett::geomap::OsmView;
// ── tiny WKB builders (little-endian) ─────────────────────────────────────────
fn wkb_polygon_rings(rings: &[&[(f64, f64)]]) -> Vec<u8> {
let mut b = vec![1u8];
b.extend_from_slice(&3u32.to_le_bytes()); // Polygon
b.extend_from_slice(&(rings.len() as u32).to_le_bytes());
for ring in rings {
b.extend_from_slice(&(ring.len() as u32).to_le_bytes());
for &(x, y) in *ring {
b.extend_from_slice(&x.to_le_bytes());
b.extend_from_slice(&y.to_le_bytes());
}
}
b
}
fn wkb_multipolygon(polys: &[Vec<u8>]) -> Vec<u8> {
let mut b = vec![1u8];
b.extend_from_slice(&6u32.to_le_bytes()); // MultiPolygon
b.extend_from_slice(&(polys.len() as u32).to_le_bytes());
for p in polys {
b.extend_from_slice(p);
}
b
}
fn sq(x0: f64, y0: f64, x1: f64, y1: f64) -> Vec<(f64, f64)> {
vec![(x0, y0), (x1, y0), (x1, y1), (x0, y1), (x0, y0)]
}
/// The scene, decoded through the real WKB → ways pipeline: a big lake with an
/// island hole + a 2-member forest multipolygon. Centred on a Sigtuna-ish bbox.
fn scene_ways() -> Vec<facett::geomap::Way> {
let th = facett::Theme::default();
// Lake covering most of the bbox, with a rectangular island hole in the middle.
let lake_outer = sq(17.60, 59.55, 17.80, 59.65);
let island = sq(17.66, 59.585, 17.74, 59.615);
let lake = wkb_polygon_rings(&[&lake_outer, &island]);
// A 2-member forest multipolygon in a corner.
let f_a = sq(17.605, 59.555, 17.63, 59.57);
let f_b = sq(17.77, 59.555, 17.795, 59.57);
let forest = wkb_multipolygon(&[wkb_polygon_rings(&[&f_a]), wkb_polygon_rings(&[&f_b])]);
facett::osm::ways_from_wkb(
[(lake.as_slice(), r#"{"natural":"water"}"#), (forest.as_slice(), r#"{"landuse":"forest"}"#)],
&th,
)
}
fn render(view: OsmView, name: &str) -> image::RgbaImage {
let mut v = view;
let mut h = Harness::builder().with_size(egui::vec2(560.0, 420.0)).build_ui(move |ui| {
v.ui(ui);
});
h.run_steps(3);
let img = h.render().expect("wgpu render produced an image");
let dir = &std::path::PathBuf::from(std::env::var_os("HOME").unwrap_or_default()).join("git/workspace_nordisk/scratch");
std::fs::create_dir_all(dir).ok();
let path = dir.join(format!("{name}.png"));
image::DynamicImage::ImageRgba8(img.clone()).save(&path).expect("save PNG");
eprintln!("[osm2d-holes] wrote {}", path.display());
img
}
/// Is the pixel at (fx,fy) fraction of the image "watery" (clearly blue-dominant)?
fn is_water(img: &image::RgbaImage, fx: f32, fy: f32) -> bool {
let x = (fx * img.width() as f32) as u32;
let y = (fy * img.height() as f32) as u32;
let p = img.get_pixel(x.min(img.width() - 1), y.min(img.height() - 1));
// Water in the default theme is blue-dominant; land ("paper") is not.
p.0[2] as i32 > p.0[0] as i32 + 25 && p.0[2] as i32 > p.0[1] as i32 + 10
}
#[test]
fn multipolygon_hole_is_punched_and_before_after_rendered() {
let ways = scene_ways();
// Pipeline sanity: lake (1 way, 1 hole) + forest multipolygon (2 ways).
assert_eq!(ways.len(), 3, "lake + two forest members");
let lake = ways.iter().find(|w| w.kind == "water").expect("a water way");
assert_eq!(lake.holes.len(), 1, "the lake carries its island hole");
// AFTER: the real fixed pipeline (holes punched).
let after = render(OsmView::from_geometry("OSM 2D — holes fixed", Vec::new(), ways.clone()), "osm2d-after");
// BEFORE: the pre-fix symptom — clear the hole rings so the island fills solid
// (islands filled in, hole not punched).
let broken: Vec<_> = ways
.iter()
.cloned()
.map(|mut w| {
w.holes.clear();
w
})
.collect();
let before = render(OsmView::from_geometry("OSM 2D — holes broken", Vec::new(), broken), "osm2d-before");
// The island centre is at the middle of the pane. AFTER: not water (land shows
// through the punched hole). BEFORE: water (the island was filled solid).
assert!(is_water(&before, 0.5, 0.5), "BEFORE: the island is wrongly filled as water");
assert!(!is_water(&after, 0.5, 0.5), "AFTER: the punched hole shows land, not water");
}