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