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

nordisk/facett

facett-geomap/tests/osm_paint_order.rs

//! **The declared paint order, on PIXELS, for the SECOND 2D OSM painter** —
//! `facett_geomap::osm2d::OsmView`, the twin painter that ships in the same korp
//! binary as `facett_map`'s `MapFacet` and never heard of the
//! `facett_map::paint_order` cure.
//!
//! # The defects these tests were written RED against (2026-08-25)
//!
//! 1. **Fills were not sorted at all** — `OsmView::view` painted
//!    `visible_ways.iter().filter(fill.is_some())` in decode order, which is the
//!    reported bug verbatim ("draws in the order things were read in").
//! 2. **Lines sorted by a width bucket with a STABLE sort** —
//!    `lines.sort_by_key(|l| (l.is_road, l.rank))` — so two roads of the same
//!    width bucket fell back to decode order, and `layer`/`bridge`/`tunnel`
//!    decided nothing. The theme lane had no sort at all.
//!
//! # Determinism of the red
//!
//! A stable-sort tie bug passes by luck when the fixture happens to decode in the
//! right order. Every fixture here therefore feeds the winner FIRST in the ways
//! `Vec` — the one order that today's decode-order painter is GUARANTEED to get
//! wrong on every run — and the geometry is containment/colinear so the loser,
//! when wrongly painted last, covers the winner COMPLETELY (no anti-aliased
//! remnant can sneak a pass). No randomness anywhere; the same pixels every run.
//!
//! Run: `cargo test -p facett-geomap --test osm_paint_order`

use egui_kittest::Harness;
use facett_core::Facet;
use facett_geomap::Way;
use facett_geomap::osm2d::OsmView;
use facett_map::paint_order::OsmZ;

/// A closed square ring in (lon, lat).
fn square(x0: f64, y0: f64, x1: f64, y1: f64) -> Vec<(f64, f64)> {
    vec![(x0, y0), (x1, y0), (x1, y1), (x0, y1), (x0, y0)]
}

/// Count pixels within a per-channel band of `(r,g,b)`.
fn count_near(img: &image::RgbaImage, r: u8, g: u8, b: u8, tol: i32) -> usize {
    img.pixels()
        .filter(|p| {
            (p.0[0] as i32 - r as i32).abs() <= tol
                && (p.0[1] as i32 - g as i32).abs() <= tol
                && (p.0[2] as i32 - b as i32).abs() <= tol
        })
        .count()
}

/// One wgpu harness at a time (two coming up together SIGSEGV the driver —
/// same lesson as facett-map/tests/paint_order_pixels.rs `GPU_TURN`).
static GPU_TURN: std::sync::Mutex<()> = std::sync::Mutex::new(());

fn render(view: OsmView) -> image::RgbaImage {
    let _turn = GPU_TURN.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
    let mut view = view;
    let mut harness = Harness::builder().with_size(egui::vec2(400.0, 400.0)).build_ui(move |ui| {
        view.ui(ui);
    });
    harness.run_steps(3);
    harness.render().expect("wgpu render produced an image")
}

/// **Defect 1, the reported bug verbatim: fill draw order was decode order.**
///
/// A pure-RED bridge deck polygon is decoded BEFORE the pure-BLUE water it
/// crosses. Its class (unclassified area, 400) and its `bridge=yes` z-order both
/// rank it ABOVE the water fill (class 100) under
/// `facett_map::paint_order::way_paint_key` — but the unsorted fill loop paints
/// decode order, so the water (decoded second) buries the deck on every frame.
///
/// RED 2026-08-25 (before the fix): red(deck)=0 px — the deck is not on screen
/// at all. Deterministic: there is no sort, so the wrong order is certain.
#[test]
fn a_bridge_fill_decoded_before_the_water_still_paints_on_top() {
    let mut deck = Way::filled(
        2,
        "bridge",
        square(4.0, 4.0, 6.0, 6.0),
        egui::Color32::from_rgb(255, 0, 0),
        egui::Color32::from_rgb(200, 0, 0),
        1.0,
    );
    deck.z_order = OsmZ { layer: 0, bridge: true, tunnel: false };
    let water = Way::filled(
        1,
        "water",
        square(0.0, 0.0, 10.0, 10.0),
        egui::Color32::from_rgb(0, 0, 255),
        egui::Color32::from_rgb(0, 0, 200),
        1.0,
    );
    // Winner FIRST: decode order says the water (last) wins every overlap today.
    let img = render(OsmView::from_geometry("bridge-vs-water", Vec::new(), vec![deck, water]));

    let red = count_near(&img, 255, 0, 0, 40);
    let blue = count_near(&img, 0, 0, 255, 40);
    eprintln!("[paint-order fills] red(bridge deck)={red} blue(water)={blue}");
    assert!(blue > 5_000, "the water fill must cover a healthy area (got {blue})");
    assert!(
        red > 1_000,
        "the bridge deck must be visible OVER the water it crosses, not buried by \
         decode order (got {red} red px)"
    );
}

/// **The `layer` term must decide a fill-vs-fill overlap — against BOTH the
/// decode order AND the largest-first area fallback.**
///
/// Two unclassified fills of the SAME class: a small GREEN ground slab sits
/// entirely inside a LARGER MAGENTA deck tagged `bridge=yes layer=1`. Every fallback points
/// the wrong way on purpose — decode order (deck first), area (deck bigger, so
/// the area tie-break alone would paint it UNDER) — only the
/// `effective_layer * LAYER_STEP` term of the shared key puts the deck on top.
/// After the fix the green slab is completely covered; today it shows.
///
/// RED 2026-08-25 (before the fix): green(ground)=21 000+ px visible.
/// Also red against a half-fix: sorting fills by area alone still shows green.
///
/// **The deck says `bridge=yes` since 2026-09-29.** Lane GG #6a (`fbe9df2`, 2026-09-05)
/// made an AREA honour `layer` only when it also says why (`bridge`/`tunnel`): a bare
/// `layer=1` on a polygon is a mis-tag that must not lift it over the road network
/// (`facett-map/tests/paint_order_pixels.rs` holds that side). This fixture kept a bare
/// `layer=1` and so asserted the behaviour GG #6a removed: MEASURED on a Radeon 890M,
/// green(ground)=49 061 px, identical on main and every branch — a stale fixture, not a
/// vendor or ordering defect. With the deck declaring what it is, the layer term decides
/// again, and both fallbacks still point the wrong way (decode order: deck first; area:
/// deck larger), so an unsorted or area-only painter is still RED here.
#[test]
fn a_layer_tagged_fill_beats_area_and_decode_order() {
    let mut deck = Way::filled(
        7,
        "deck",
        square(2.0, 2.0, 8.0, 8.0),
        egui::Color32::from_rgb(255, 0, 255),
        egui::Color32::from_rgb(200, 0, 200),
        1.0,
    );
    deck.z_order = OsmZ { layer: 1, bridge: true, tunnel: false };
    let ground = Way::filled(
        8,
        "ground",
        square(3.0, 3.0, 7.0, 7.0),
        egui::Color32::from_rgb(0, 255, 0),
        egui::Color32::from_rgb(0, 200, 0),
        1.0,
    );
    let img = render(OsmView::from_geometry("layered-fill", Vec::new(), vec![deck, ground]));

    let magenta = count_near(&img, 255, 0, 255, 40);
    let green = count_near(&img, 0, 255, 0, 40);
    eprintln!("[paint-order layer-fill] magenta(deck layer=1)={magenta} green(ground)={green}");
    assert!(magenta > 5_000, "the layer=1 deck must be visible (got {magenta})");
    assert!(
        green < 50,
        "the ground slab sits UNDER the layer=1 deck that contains it; {green} green px \
         means the layer tag lost to decode order or to the area fallback"
    );
}

/// The tied-lines fixture both line-lane tests share: a GREEN ground road and a
/// MAGENTA `bridge=yes` road, **colinear, equal width** (same `road_rank` bucket
/// — the old sort key ties exactly), the ground run longer so it covers the
/// bridge completely whenever it is wrongly painted later. The bridge is decoded
/// FIRST, so the old stable sort's decode-order tie-break loses deterministically.
fn tied_lines() -> Vec<Way> {
    let mut bridge = Way::line(
        11,
        "road",
        vec![(2.0, 5.0), (8.0, 5.0)],
        egui::Color32::from_rgb(255, 0, 255),
        6.0,
    );
    bridge.z_order = OsmZ { layer: 0, bridge: true, tunnel: false };
    let ground = Way::line(
        12,
        "road",
        vec![(0.5, 5.0), (9.5, 5.0)],
        egui::Color32::from_rgb(0, 255, 0),
        6.0,
    );
    // Corner anchors so the fit shows the full 0..10 square (stable framing).
    let frame = Way::line(13, "frame", vec![(0.0, 0.0), (0.0, 10.0)], egui::Color32::from_rgb(0, 0, 255), 1.0);
    vec![bridge, ground, frame]
}

/// **Defect 2, theme lane: no line sort at all** — decode order painted the
/// ground road over the bridge that crosses it.
///
/// RED 2026-08-25 (before the fix): magenta(bridge)≈0 px — the wider/longer
/// ground fill, painted last, covers the colinear bridge entirely.
#[test]
fn a_bridge_line_decoded_first_paints_over_the_ground_road_theme_lane() {
    let img = render(OsmView::from_geometry("tied-lines-theme", Vec::new(), tied_lines()));
    let magenta = count_near(&img, 255, 0, 255, 60);
    let green = count_near(&img, 0, 255, 0, 60);
    eprintln!("[paint-order lines/theme] magenta(bridge)={magenta} green(ground)={green}");
    assert!(green > 100, "the ground road's uncovered ends must show (got {green})");
    assert!(
        magenta > 200,
        "the bridge=yes road must paint OVER the tied-width ground road, not lose \
         the stable-sort tie to decode order (got {magenta} magenta px)"
    );
}

/// **Defect 2, cartographic lane: the `(is_road, width-bucket)` sort at what was
/// `osm2d.rs:2093`** — `layer`/`bridge`/`tunnel` decided nothing there, and the
/// `is_road` half of the key put EVERY path under EVERY road unconditionally, so
/// a cycleway bridge over a street was buried on every single frame.
///
/// The fixture goes through `Way::from_lonlat_with` + the cartographic recolor —
/// the live pipeline — because the carto preset re-keys every hue by class/width,
/// which makes two same-bucket roads pixel-identical by construction; a PATH
/// (`pal.path` hue) over a ROAD is the same-tie defect with distinguishable ink.
/// Both ways are forced to the same painted width and LOD, so the ONLY term that
/// can put the bridge on top is `effective_layer * LAYER_STEP` in the shared key
/// — the old key orders this pair wrongly on every run (no coin flip).
///
/// RED 2026-08-25 (before the fix): path-hue px ≈ 0 — the bridge cycleway is not
/// on screen at all, buried under the street's casing + fill.
#[test]
fn a_bridge_path_paints_over_the_street_cartographic_lane() {
    let pal = facett_map::MapPalette::cartographic(true);
    let mk = |id: i64, ll: &[(f64, f64)], tags: &[(&str, &str)]| {
        let t: Vec<(String, String)> =
            tags.iter().map(|(k, v)| ((*k).to_string(), (*v).to_string())).collect();
        Way::from_lonlat_with(id, ll, &t, &pal).expect("a 2-vertex line is a way")
    };
    let mut bridge = mk(21, &[(2.0, 5.0), (8.0, 5.0)], &[("highway", "cycleway"), ("bridge", "yes")]);
    let mut ground = mk(22, &[(0.5, 5.0), (9.5, 5.0)], &[("highway", "residential")]);
    // Fixture hygiene: identical painted width (neither can hide by being thinner)
    // and Country LOD (the fit-zoom cull keeps both). Order-irrelevant: both ways
    // get the same treatment, so only class + z-order separate them.
    for w in [&mut bridge, &mut ground] {
        w.width = 6.0;
        w.style.width = 6.0;
        w.style.lod = facett_map::style::Lod::Country;
    }
    // A frame line so the fitted bbox stays the 0..10 square (colinear roads alone
    // would collapse the fit's vertical span).
    let frame = Way::line(23, "frame", vec![(0.0, 0.0), (0.0, 10.0)], egui::Color32::from_rgb(0, 0, 255), 1.0);
    let view = OsmView::from_geometry("bridge-path-carto", Vec::new(), vec![bridge, ground, frame])
        .with_palette(facett_map::PaletteMode::CartographicDay);
    let img = render(view);
    // The recolored inks, read off the SAME palette the painter used (no hardcoded
    // hex to drift): the cycleway paints `pal.path`, never the street's road hue.
    // Tolerance 10, not a lazy 30: the AA gradient where the street's orange fill
    // feathers into its grey casing passes within ~25/channel of `pal.path` (a
    // buried cycleway scored 526 false-positive px at tol 30), while no point on
    // that casing↔fill blend line comes within 10 of the path hue — only real
    // path ink does, and the visible cycleway's 3-px core is exact-hue.
    let path_px = count_near(&img, pal.path.r(), pal.path.g(), pal.path.b(), 10);
    eprintln!("[paint-order lines/carto] path-hue(bridge cycleway)={path_px}");
    assert!(
        path_px > 150,
        "cartographic lane: the bridge=yes cycleway must paint OVER the street it \
         crosses — the old (is_road, width-bucket) key buried it (got {path_px} \
         path-hue px)"
    );
}