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

nordisk/facett

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");
}