facett-map/tests/paint_order_pixels.rs
//! **THE declared paint order, asserted on PIXELS** (`facett_map::paint_order`).
//!
//! # What the owner reported, 2026-08-22
//!
//! On the live 2D map: the basemap "draws in the order things were read in, and paints
//! over what should be on top". Four mechanisms produced that (see the
//! `facett_map::paint_order` module docs); this file is the guard for the cure, and it
//! is deliberately written against the **rendered framebuffer** rather than against the
//! sorted `Vec<DrawFeature>`.
//!
//! That distinction is the whole point (LAW 2). A `feats[0].order < feats[1].order`
//! assertion is a state round-trip: it re-reads the very key the sort was given and can
//! pass on a painter that ignores the sort entirely, on a GPU lane that re-orders the
//! stream behind it, and on a split that drops the callback in the wrong slot. Only the
//! pixels know which polygon ended up on top.
//!
//! # The three claims
//!
//! 1. **`input_order_cannot_change_a_pixel`** — the same two overlapping features fed
//! in the two possible orders must render byte-identically. This is the reported
//! symptom stated as a property.
//! 2. **`a_bridge_paints_over_the_water_and_a_tunnel_under_it`** — OSM `bridge` /
//! `tunnel` / `layer`, which were read NOWHERE before this change, decide the stack
//! at the crossing pixel.
//! 3. **`the_gpu_slot_lands_in_the_declared_stack`** — the CPU/GPU seam: when the lane
//! owns one class's fills and the CPU painter keeps another's, the callback slot
//! goes at the lane's own rank FLOOR, so a CPU-painted building still lands over
//! GPU-painted water. It used to go at a hardcoded `LayerKind::Line.default_z()`,
//! which put every GPU fill above every CPU fill whatever the class.
//!
//! # Proven RED (2026-08-22, oden, RTX 4090)
//!
//! Each claim was watched fail before it was trusted — see the `RED:` note above each
//! test for the exact edit that reddens it.
//!
//! Run: `cargo test -p facett-map --features wgpu --test paint_order_pixels`
use egui_kittest::Harness;
use facett_map::layer::{LatLon, MapFacet, MapTheme, MapView, WaysLayer};
use facett_map::{Way, WayKind};
/// The ONE GPU gate for this crate's device-backed tests: a missing adapter becomes a
/// SKIP row in the matrix and a panic under CI, never a silent `ok`.
mod common;
/// A closed axis-aligned ring at `(lon, lat)` of half-width `w`, tagged `tags`.
///
/// Built through [`Way::from_lonlat_with`] — the SAME constructor korp's served and
/// tiled paths use — so the tags this test writes are parsed by the same code the live
/// map runs, including `paint_order::parse_osm_z`.
fn ring(id: i64, lon: f64, lat: f64, w: f64, tags: &[(&str, &str)]) -> Way {
let t: Vec<(String, String)> = tags.iter().map(|(k, v)| ((*k).to_string(), (*v).to_string())).collect();
let r = vec![
(lon - w, lat - w),
(lon + w, lat - w),
(lon + w, lat + w),
(lon - w, lat + w),
(lon - w, lat - w),
];
Way::from_lonlat_with(id, &r, &t, &facett_map::MapPalette::cartographic(false))
.expect("a 5-vertex closed ring is a way")
}
/// A straight two-point line at latitude `lat`, tagged `tags`.
fn line(id: i64, lon0: f64, lon1: f64, lat: f64, tags: &[(&str, &str)]) -> Way {
let t: Vec<(String, String)> = tags.iter().map(|(k, v)| ((*k).to_string(), (*v).to_string())).collect();
Way::from_lonlat_with(id, &[(lon0, lat), (lon1, lat)], &t, &facett_map::MapPalette::cartographic(false))
.expect("a 2-vertex line is a way")
}
fn facet(ways: Vec<Way>) -> MapFacet {
let view = MapView::new(LatLon::new(59.33, 18.06), 13.0, (520.0, 400.0));
MapFacet::new("paint-order", view, MapTheme::default())
.with_layer(Box::new(WaysLayer::new("osm-ways", ways)))
}
// **The one-device-at-a-time turn lives in `common`** — `common/one_gpu.rs`, reached as
// `common::one_gpu` because this file already says `mod common;`. Declaring a second
// `#[path]` copy at the crate root of this test binary would compile two DIFFERENT
// statics out of one file, and two mutexes over one device serialise nothing.
/// Render a facet through the real wgpu backend and hand back the framebuffer as
/// `(width, height, rgba)`.
fn render(mut f: MapFacet) -> (u32, u32, Vec<u8>) {
let _turn = common::one_gpu::hold();
let mut harness = Harness::builder().with_size(egui::vec2(520.0, 400.0)).build_ui(move |ui| {
f.ui(ui);
});
harness.run_steps(3);
let img = harness.render().expect("wgpu render produced an image");
(img.width(), img.height(), img.as_raw().clone())
}
/// The rendered colour at a fraction of the pane (`0.0..1.0` in each axis).
fn px_at(w: u32, h: u32, buf: &[u8], fx: f32, fy: f32) -> [u8; 4] {
let x = ((fx * w as f32) as u32).min(w - 1);
let y = ((fy * h as f32) as u32).min(h - 1);
let i = ((y * w + x) * 4) as usize;
[buf[i], buf[i + 1], buf[i + 2], buf[i + 3]]
}
fn near(a: [u8; 4], b: [u8; 4], tol: i32) -> bool {
(0..3).all(|i| (i32::from(a[i]) - i32::from(b[i])).abs() <= tol)
}
// ─────────────────────────────────────────────────────────────────────────────
/// **THE reported bug, as a property.** Two overlapping features, fed in the two
/// possible input orders, must produce the SAME framebuffer.
///
/// The fixture is chosen so the old code genuinely disagreed with itself: a `landuse`
/// ring and a `water` ring of the same size, overlapping. Both are `DrawGeom::Polygon`
/// with the identical old `z` (`LayerKind::Polygon.default_z()`), so the old stable
/// sort left the winner of the overlap to whichever was pushed second — i.e. to the
/// order the tile merge happened to append them in.
///
/// RED: revert `features_for_cpu` to `feats.sort_by_key(|f| f.z)` with the per-kind
/// `LayerKind::default_z()` — the two renders then differ on ~7 800 of 208 000 pixels
/// (measured on oden, 2026-08-22), because the overlap paints in whichever colour came
/// last.
#[test]
fn input_order_cannot_change_a_pixel() {
// Through `device_or_skip`, not a bare `headless_device(..).is_none()`
// (lane ST, map2d-S9/S10). The bare form returned silently: no stderr line,
// no matrix row, and a host that DECLARES it has a GPU did not fail. A skip
// that leaves no mark is indistinguishable from a pass in every readout the
// suite produces.
if common::device_or_skip(
"input_order_cannot_change_a_pixel",
"that two feature orderings rasterise to the SAME pixels — the paint order is the declared lane stack, not the order features arrived in",
)
.is_none()
{
return;
}
let mk = || {
(
ring(1, 18.055, 59.330, 0.010, &[("landuse", "forest")]),
ring(2, 18.065, 59.330, 0.010, &[("natural", "water")]),
)
};
let (a, b) = mk();
let (w0, h0, forward) = render(facet(vec![a, b]));
let (a, b) = mk();
let (w1, h1, reversed) = render(facet(vec![b, a]));
assert_eq!((w0, h0), (w1, h1), "same pane size both ways");
// NON-VACUOUS: the fixture must actually draw two colours that overlap, or
// "identical" is the identity trap — two blank panes are also identical.
let distinct: std::collections::HashSet<[u8; 4]> =
forward.as_chunks::<4>().0.iter().map(|c| [c[0], c[1], c[2], c[3]]).collect();
assert!(
distinct.len() >= 3,
"the fixture must render background + BOTH fills ({} distinct colours found) — \
otherwise an identical pair proves nothing",
distinct.len()
);
let diff = forward.as_chunks::<4>().0.iter().zip(reversed.as_chunks::<4>().0).filter(|(a, b)| a != b).count();
assert_eq!(
diff,
0,
"swapping the INPUT order changed {diff} of {} pixels — the stack is still decided \
by the order the ways were read in, which is exactly what the owner reported",
(w0 * h0) as usize
);
nornir_testmatrix::functional_status(
"facett-map",
"input_order_cannot_change_a_pixel",
diff == 0,
&format!("{diff} differing pixels of {} over {} distinct colours", w0 * h0, distinct.len()),
);
}
/// **OSM `bridge` / `tunnel` decide the crossing pixel.** They were read nowhere in
/// `facett-map`, `facett-osm` or `korp/src/map_paint.rs` before 2026-08-22, so a
/// viaduct and the river under it had nothing but their classes to go on — and a road
/// is a LINE, so it went over the water whether it was a bridge or a tunnel.
///
/// RED: drop the `w.z_order.effective_layer() * LAYER_STEP` term from
/// `paint_order::way_rank` — the tunnel then paints over the water exactly as the
/// bridge does and the second assertion fails.
#[test]
fn a_bridge_paints_over_the_water_and_a_tunnel_under_it() {
// Through `device_or_skip`, not a bare `headless_device(..).is_none()`
// (lane ST, map2d-S9/S10). The bare form returned silently: no stderr line,
// no matrix row, and a host that DECLARES it has a GPU did not fail. A skip
// that leaves no mark is indistinguishable from a pass in every readout the
// suite produces.
if common::device_or_skip(
"a_bridge_paints_over_the_water_and_a_tunnel_under_it",
"that a bridge's pixels land ON TOP of the water it crosses and a tunnel's land UNDER it — the layer= tag reaching the raster, not just the sort",
)
.is_none()
{
return;
}
// A wide lake with a road straight across its middle.
let lake = || ring(1, 18.060, 59.330, 0.020, &[("natural", "water")]);
let road = |tags: &[(&str, &str)]| line(2, 18.040, 18.080, 59.330, tags);
let bridge_tags: &[(&str, &str)] = &[("highway", "motorway"), ("bridge", "yes")];
let tunnel_tags: &[(&str, &str)] = &[("highway", "motorway"), ("tunnel", "yes")];
// Water must be painted LAST in the input in both cases, so that "whoever was read
// last wins" — the old behaviour — would put the lake over the road every time.
let (w, h, with_bridge) = render(facet(vec![road(bridge_tags), lake()]));
let (_, _, with_tunnel) = render(facet(vec![road(tunnel_tags), lake()]));
// The centre of the pane is the middle of the lake, on the road's line.
let bridge_px = px_at(w, h, &with_bridge, 0.5, 0.5);
let tunnel_px = px_at(w, h, &with_tunnel, 0.5, 0.5);
// A point inside the lake but well off the road — the water reference colour.
let water_px = px_at(w, h, &with_tunnel, 0.5, 0.30);
assert!(
near(tunnel_px, water_px, 6),
"a TUNNEL under the lake must leave the water's own colour on the crossing pixel: \
got {tunnel_px:?} at the crossing vs {water_px:?} in open water"
);
assert!(
!near(bridge_px, water_px, 6),
"a BRIDGE over the lake must show the ROAD on the crossing pixel, not the water: \
got {bridge_px:?}, water is {water_px:?}"
);
assert!(
bridge_px != tunnel_px,
"bridge and tunnel must not render the same crossing pixel ({bridge_px:?}) — \
that is the state the map was in when the `layer` tag was read nowhere"
);
nornir_testmatrix::functional_status(
"facett-map",
"bridge_over_water_tunnel_under",
!near(bridge_px, water_px, 6) && near(tunnel_px, water_px, 6),
&format!("crossing px: bridge {bridge_px:?}, tunnel {tunnel_px:?}, open water {water_px:?}"),
);
}
/// **The CPU/GPU seam.** The GPU callback is ONE opaque slot, so where it goes in the
/// CPU list decides the whole cross-lane stack.
///
/// It went at a hardcoded `LayerKind::Line.default_z()` — "the lane draws lines" —
/// which stopped being true when the fill lane landed: a lake the GPU took was drawn in
/// the LINE slot, **above every CPU-painted fill whatever its class**, and
/// `way_lane_plan_scan` decides per viewport which layers the lane takes, so the
/// stacking changed with what had been loaded.
///
/// The applied output here is the SPLIT INDEX the frame computes — the number that
/// says how many CPU features paint before the callback — taken from the same code
/// path `MapFacet::ui` runs (`gpu_slot_split_for_test`), over the real feature list.
///
/// RED: put the split back to `feats.partition_point(|f| f.order.rank < LINE_BAND)`
/// (the old `Line.default_z()` behaviour) — the CPU-painted building then falls BELOW
/// the callback and the last assertion fires.
///
/// **Gated on `wgpu`, and it has to be.** `MapFacet::gpu_slot_split_for_test` is itself
/// `#[cfg(feature = "wgpu")]` (layer.rs:2883), and facett-map's `default` closure is
/// empty — so before this attribute existed, a plain `cargo test -p facett-map` did not
/// COMPILE at all: `E0599: no method named gpu_slot_split_for_test`. Every other test in
/// this file went down with it, including the CPU-only pixel proofs, which is worse than
/// the ordering bug the file was written for. The rest of the file stays in the default
/// green; only the arm that reaches the wgpu-gated hook is gated.
#[cfg(feature = "wgpu")]
#[test]
fn the_gpu_slot_lands_in_the_declared_stack() {
// Layer 0: water — the lane will own its fills.
// Layer 1: a building — the CPU painter keeps it.
let water = ring(1, 18.060, 59.330, 0.020, &[("natural", "water")]);
let building = ring(2, 18.060, 59.330, 0.004, &[("building", "yes")]);
assert_eq!(water.style.kind, WayKind::Water, "fixture: layer 0 really is water");
assert_eq!(building.style.kind, WayKind::Building, "fixture: layer 1 really is a building");
let view = MapView::new(LatLon::new(59.33, 18.06), 13.0, (520.0, 400.0));
let f = MapFacet::new("seam", view, MapTheme::default())
.with_layer(Box::new(WaysLayer::new("osm-water", vec![water])))
.with_layer(Box::new(WaysLayer::new("osm-buildings", vec![building])));
// The lane takes layer 0's FILLS only. `features_for_cpu` therefore drops the water
// polygon and keeps the building — the exact configuration the item names.
let feats = f.features_for_cpu_for_test(&[0], false, true);
assert_eq!(feats.len(), 1, "the CPU keeps exactly the building (the lane took the water)");
assert_eq!(feats[0].id, 2, "and it is the BUILDING that stayed on the CPU");
let (floor, split) = f.gpu_slot_split_for_test(&[0], false, true, &feats);
let floor = floor.expect("the lane owns the water fill, so it has a rank floor");
assert_eq!(
floor,
facett_map::way_rank(&ring(1, 18.060, 59.330, 0.020, &[("natural", "water")])),
"the slot floor is the WATER's declared rank — the lowest thing the lane draws"
);
assert!(
floor < feats[0].order.rank,
"sanity: water ranks under buildings ({floor} < {})",
feats[0].order.rank
);
assert_eq!(
split, 0,
"ZERO CPU features may paint before the callback: the building ranks ABOVE the \
water the lane owns, so it must land in the tail — a split of {split} would put \
the CPU building UNDER the GPU water, which is the tangle on the live map"
);
nornir_testmatrix::functional_status(
"facett-map",
"gpu_slot_lands_in_the_declared_stack",
split == 0,
&format!("lane floor {floor}, building rank {}, split {split}", feats[0].order.rank),
);
}
/// **The GPU lane's own stream order.** The three pixel claims above exercise the CPU
/// painter (a headless `Harness` installs no `RenderState`, so `renderer_installed()`
/// is false and the frame never places a callback). The device lane has to be pinned
/// separately, and its applied output is the **vertex stream** — the exact bytes
/// `draw_indirect` pulls, in the exact order the rasteriser consumes them.
///
/// Two things are asserted, both on the baked bytes:
///
/// 1. swapping the input order does not move a single byte of `tri_vertices`;
/// 2. the WATER's triangles come before the BUILDING's, i.e. the declared class order
/// survives into the stream.
///
/// This is where the tile lane's tangle lived: `add_gpu_tiled_callback` groups by
/// `Way::tile_key`, and the renderer draws all tiles' fills, then all tiles' outlines,
/// then all tiles' roads — so ordering *inside* the stream is the only class order the
/// tile lane has.
///
/// RED: delete the `fills.sort_unstable_by_key(..)` in `build_gpu_fill_geometry` — the
/// two bakes then differ on every vertex from the first way onwards and claim 2 fails
/// outright.
#[cfg(feature = "wgpu")]
#[test]
fn the_gpu_bake_emits_the_declared_order_whatever_the_input_order() {
use facett_map::gpu::{build_gpu_fill_geometry, ways_origin};
use facett_map::tessellate::FillTessCache;
let water = ring(1, 18.060, 59.330, 0.020, &[("natural", "water")]);
let building = ring(2, 18.060, 59.330, 0.004, &[("building", "yes")]);
let origin = ways_origin([&water, &building].into_iter()).expect("non-empty fixture");
// One cache per way so the ear clip is the same triangulation in both bakes.
let bake = |ws: [&Way; 2]| {
let cache = FillTessCache::new(2);
let items: Vec<_> = ws
.iter()
.enumerate()
.filter_map(|(i, w)| cache.tris(i, &w.lonlat, &w.holes).map(|t| (*w, w.style.lod as u32, t)))
.collect();
build_gpu_fill_geometry(items.into_iter(), origin)
};
let forward = bake([&water, &building]);
let reversed = bake([&building, &water]);
assert!(!forward.tri_vertices.is_empty(), "the fixture must actually bake triangles");
assert_eq!(forward.tri_meta.len(), 2, "both rings reached the stream");
assert_eq!(
forward.tri_vertices, reversed.tri_vertices,
"the baked fill stream must not depend on the order the ways were handed over — \
that dependence is what makes the tile lane's stacking change with what was loaded"
);
assert_eq!(forward.outline_vertices, reversed.outline_vertices, "and neither must the outlines");
// And the order is the DECLARED one, not just a stable one: water under buildings.
// `tri_meta` spans are emitted in stream order, so the first span is what draws first.
//
// The discriminator is the vertex COLOUR, not the vertex count: both fixtures are
// 5-point squares and therefore bake the SAME number of triangles, so a
// count-based check would read `true` whichever way round the stream came out —
// a guard that could never go red (LAW 2, the identity-value trap).
let want = |c: egui::Color32| {
let [r, g, b, a] = c.to_array();
[f32::from(r) / 255.0, f32::from(g) / 255.0, f32::from(b) / 255.0, f32::from(a) / 255.0]
};
let water_col = want(water.fill.expect("the lake is filled"));
let building_col = want(building.fill.expect("the footprint is filled"));
assert_ne!(water_col, building_col, "fixture: the two fills must be DIFFERENT colours");
let first_col = forward.tri_vertices[0].col;
let water_first = first_col == water_col;
assert!(
forward.tri_meta[0].vert_start < forward.tri_meta[1].vert_start,
"spans must tile the stream in draw order"
);
assert!(
water_first,
"the WATER's triangles must be the first span in the stream (drawn first = under \
the building); the stream opens with {first_col:?}, water is {water_col:?} and the \
building is {building_col:?}"
);
nornir_testmatrix::functional_status(
"facett-map",
"gpu_bake_emits_the_declared_order",
forward.tri_vertices == reversed.tri_vertices && water_first,
&format!(
"{} tri verts, spans {:?}",
forward.tri_vertices.len(),
forward.tri_meta.iter().map(|m| (m.vert_start, m.vert_count)).collect::<Vec<_>>()
),
);
}
/// **A ditch is not a river.** `style.rs` classified every `waterway=*` through ONE
/// branch — river, stream, ditch, drain, canal, weir, dam all got width 1.0, the same
/// blue, `is_area: true` and `Lod::Region` — while the `highway` classes directly above
/// have had a four-rung ladder all along. `Lod::Region` starts at camera **6**
/// (`LOD_TIER_ZOOMS = [0, 6, 12, 15]`), so every agricultural ditch in the extract drew
/// from a regional overview outwards, at a zoom where it is far under a pixel.
///
/// The applied output is the PIXELS at a Region-tier camera: the river must be on the
/// canvas and the ditch must not. A `feats.len()` or an `assert_eq!(lod, Detail)` would
/// be a state round-trip — it would re-read the constant the classifier was just given.
///
/// RED: put the classifier back to the single `if has("waterway")` arm at
/// `Lod::Region` — the ditch then draws at camera 8 and the second assertion fires.
#[test]
fn a_ditch_is_not_drawn_at_a_region_zoom_and_a_river_is() {
// Through `device_or_skip`, not a bare `headless_device(..).is_none()`
// (lane ST, map2d-S9/S10). The bare form returned silently: no stderr line,
// no matrix row, and a host that DECLARES it has a GPU did not fail. A skip
// that leaves no mark is indistinguishable from a pass in every readout the
// suite produces.
if common::device_or_skip(
"a_ditch_is_not_drawn_at_a_region_zoom_and_a_river_is",
"that the zoom filter drops a ditch and keeps a river at a region zoom — the min-zoom gate reaching the raster",
)
.is_none()
{
return;
}
// Camera 8.0 → `view_lod_tier` = Region (the `[0, 6, 12, 15]` ladder). One water
// line straight across the pane, drawn alone so the canvas is background + it.
let across = |id: i64, tags: &[(&str, &str)]| line(id, 17.6, 18.5, 59.330, tags);
let at_region = |ways: Vec<Way>| {
let view = MapView::new(LatLon::new(59.33, 18.06), 8.0, (520.0, 400.0));
render(MapFacet::new("water", view, MapTheme::default()).with_layer(Box::new(WaysLayer::new("osm-water", ways))))
};
let river = across(1, &[("waterway", "river")]);
let ditch = across(2, &[("waterway", "ditch")]);
assert_eq!(river.style.kind, WayKind::Water, "fixture: a river is water");
assert_eq!(ditch.style.kind, WayKind::Water, "fixture: so is a ditch");
let (w, h, empty) = at_region(vec![]);
let (_, _, with_river) = at_region(vec![across(1, &[("waterway", "river")])]);
let (_, _, with_ditch) = at_region(vec![across(2, &[("waterway", "ditch")])]);
let ink = |buf: &[u8]| buf.as_chunks::<4>().0.iter().zip(empty.as_chunks::<4>().0).filter(|(a, b)| a != b).count();
let river_px = ink(&with_river);
let ditch_px = ink(&with_ditch);
assert!(
river_px > 0,
"a RIVER must be on the canvas at a regional zoom — it painted {river_px} pixels of {}",
(w * h) as usize
);
assert_eq!(
ditch_px, 0,
"a DITCH must NOT be on the canvas at a regional zoom — it painted {ditch_px} pixels. \
Every field drain in the extract drawing from camera 6 outwards is the blue hairball."
);
// And the same ditch DOES draw once the camera earns it, so this is a tier and not
// a way of never drawing ditches at all.
let deep = MapView::new(LatLon::new(59.33, 18.06), 15.5, (520.0, 400.0));
let (_, _, blank_deep) = render(MapFacet::new("water", deep, MapTheme::default()));
let deep = MapView::new(LatLon::new(59.33, 18.06), 15.5, (520.0, 400.0));
let (_, _, ditch_deep) = render(
MapFacet::new("water", deep, MapTheme::default())
.with_layer(Box::new(WaysLayer::new("osm-water", vec![across(2, &[("waterway", "ditch")])]))),
);
let deep_px = ditch_deep.as_chunks::<4>().0.iter().zip(blank_deep.as_chunks::<4>().0).filter(|(a, b)| a != b).count();
assert!(
deep_px > 0,
"the ditch must appear once the camera reaches its tier (zoom 15.5) — otherwise this \
test passes on a classifier that simply lost the ditch"
);
nornir_testmatrix::functional_status(
"facett-map",
"waterway_tiering_river_vs_ditch",
river_px > 0 && ditch_px == 0 && deep_px > 0,
&format!("at z8: river {river_px} px, ditch {ditch_px} px; at z15.5: ditch {deep_px} px"),
);
}
// ─────────────────────────────────────────────────────────────────────────────
// LANE GG #6 — a bare `layer` on an AREA buried the road network
// ─────────────────────────────────────────────────────────────────────────────
/// **A `landuse=grass layer=1` RING MUST NOT COVER A GROUND ROAD.**
///
/// # The defect
///
/// `way_rank`'s dominant term is `effective_layer · LAYER_STEP`, and `LAYER_STEP` is
/// `100_000` — chosen so no combination of band, class and tier can climb out of its own
/// layer. That is right for the thing `layer` exists to express: a deck over a river, a
/// tunnel under a street. It is a disaster for a **mis-tag**, because a single careless
/// `layer=1` on an opaque polygon lifts it a hundred thousand rank units — past every
/// road, every path, every label — and OSM is full of them.
///
/// So `landuse=grass layer=1` paints over the road that crosses it, and there is nothing
/// the class order can do about it. That is one of the "terrain over roads" symptoms
/// directly, and it is the one that survives every fix to the GPU depth stack, because it
/// is not a depth bug at all: the declared order genuinely says the grass is on top.
///
/// # The rule, and why it is not "ignore `layer` on areas"
///
/// An area may legitimately be off the ground — a `man_made=bridge` deck, a building
/// passage, an underground car park — and OSM says so with `bridge=*` or `tunnel=*`
/// **alongside** the `layer`. A bare `layer` on an area says only *"put me somewhere
/// else"* without saying what it is, and that is the shape of the mis-tag.
///
/// So: **an AREA honours `layer` only when it also says why** (`bridge` or `tunnel`).
/// A bare `layer` on an area is ignored. LINES are untouched — a `layer=1` road really is
/// a flyover, and that is exactly the case
/// `a_bridge_paints_over_the_water_and_a_tunnel_under_it` guards.
#[test]
fn a_bare_layer_on_a_landuse_ring_cannot_bury_the_road_crossing_it() {
let grass = ring(1, 18.060, 59.330, 0.020, &[("landuse", "grass"), ("layer", "1")]);
let road = line(2, 18.040, 18.080, 59.330, &[("highway", "motorway")]);
assert!(grass.fill.is_some(), "fixture: the landuse ring must be an AREA");
assert!(road.fill.is_none(), "fixture: the road must be a LINE");
assert_eq!(grass.z_order.layer, 1, "fixture: the layer tag really parsed");
assert!(
!grass.z_order.bridge && !grass.z_order.tunnel,
"fixture: a BARE layer — the ring says 'put me elsewhere' without saying what it is"
);
let road_colour = road.stroke.to_array();
let grass_colour = grass.fill.expect("an area fill").to_array();
assert!(
!near(road_colour, grass_colour, 24),
"fixture: the two inks must be distinguishable ({road_colour:?} vs {grass_colour:?})"
);
// The grass is fed LAST, so "whoever was read last wins" also hands it the pixel.
let (w, h, buf) = render(facet(vec![road, grass]));
let crossing = px_at(w, h, &buf, 0.5, 0.5);
let open_grass = px_at(w, h, &buf, 0.5, 0.35);
assert!(
near(open_grass, grass_colour, 24),
"the grass did not paint where no road crosses it ({open_grass:?} vs \
{grass_colour:?}) — nothing is being covered, so nothing is proved"
);
assert!(
near(crossing, road_colour, 40),
"a bare `layer=1` on a landuse ring must not bury the motorway crossing it — got \
{crossing:?}, the road is {road_colour:?}, the grass is {grass_colour:?}. \
`effective_layer · LAYER_STEP` is 100 000 rank units, so ONE careless tag \
outranks the whole road network and no class order can answer it."
);
}
/// **A `man_made=bridge` AREA KEEPS its layer** — the rule above must not throw the real
/// decks out with the mis-tags.
///
/// Without this the fix is "areas have no layer", which is a different and wrong claim: a
/// bridge deck is a polygon that genuinely IS above the water it spans, and burying it
/// would trade one visible defect for another one nobody is looking at.
#[test]
fn an_area_that_says_why_it_is_raised_keeps_its_layer() {
// `man_made=bridge` alone does not classify to a drawable way (`from_lonlat_with`
// answers `None`), so the deck is tagged the way a mapped bridge surface actually is:
// a real polygon class PLUS the `bridge` that says why it is raised. The rule under
// test reads `OsmZ`, so that pair is exactly the condition.
let deck = ring(1, 18.060, 59.330, 0.020, &[("landuse", "grass"), ("bridge", "yes"), ("layer", "1")]);
let ground = ring(2, 18.060, 59.330, 0.030, &[("landuse", "grass")]);
assert!(deck.z_order.bridge, "fixture: the deck says WHY it is raised");
assert!(
facett_map::way_rank(&deck) > facett_map::way_rank(&ground),
"a bridge deck AREA must still outrank the ground it spans — deck {} vs ground {}. \
The GG #6 rule ignores a BARE layer on an area; an area that declares a bridge or \
a tunnel is saying what it is, and that is the case `layer` exists for.",
facett_map::way_rank(&deck),
facett_map::way_rank(&ground),
);
}