facett-map/tests/lane_width_is_one_formula.rs
//! **Both lanes draw a road at the width the ONE formula says** — lane GG #27.
//!
//! `facett-map/src/style.rs` declares the width law: a way's on-screen width is
//! `line_width_px(style.width, zoom) = clamp(style.width × line_width_scale(zoom), 1, 24)`,
//! "the single formula both the CPU painter and the GPU line lane use, so the two never
//! disagree". `gpu/line.wgsl` repeats the claim at its `width_scale` uniform ("matches
//! the CPU painter's `facett_map::style::line_width_px` exactly (ONE formula, both
//! lanes)"), and `MapPaintCallback::width_scale` is documented as "deck.gl `widthScale`
//! for this frame's zoom".
//!
//! None of that was what `MapFacet` did. Its CPU closure resolved
//! `line_width_px_scaled(w, GPU_LINE_WIDTH_SCALE)` and both GPU callbacks passed the
//! same `GPU_LINE_WIDTH_SCALE` — a `const 1.0`. So the two lanes agreed with *each
//! other* and both ignored the zoom ramp the law is built on, while `facett-geomap`'s
//! `osm2d` pane, drawing the same `Way`s through the same shader, passed the live
//! `line_width_scale(cam.zoom)` in both of ITS lanes. Same road, same zoom, two panes,
//! widths apart by the whole `[0.5, 3.0]` ramp.
//!
//! This test holds three numbers side by side for one way under one camera, at a
//! regional zoom (8) and a street zoom (14): what the CPU painter strokes, what the GPU
//! vertex path resolves from the facet's uniform, and what the law says. All three must
//! be **bit-equal** — not "close": the law's whole point is that there is one
//! arithmetic, and `line_width_px_scaled` IS the shader's arithmetic.
//!
//! Run: `cargo test -p facett-map --features wgpu --test lane_width_is_one_formula -- --test-threads=1`
#![cfg(feature = "wgpu")]
use facett_map::Way;
use facett_map::gpu::line::resolved_width_px;
use facett_map::gpu::{MapLineUniforms, MapTransform, build_gpu_geometry};
use facett_map::layer::{LatLon, MapFacet, MapTheme, MapView, WaysLayer};
const CANVAS: (f32, f32) = (900.0, 640.0);
/// A trunk road's own themed width — the width class the zoom ramp moves most visibly.
const STYLE_W: f32 = 3.0;
/// One straight east-west trunk through the camera's centre.
fn road(theme: &facett_core::Theme) -> Way {
let pts: Vec<(f64, f64)> = (0..=8).map(|i| (18.00 + i as f64 * 0.02, 59.32)).collect();
let tags = vec![("highway".to_owned(), "trunk".to_owned())];
let mut w = Way::from_lonlat(1, &pts, &tags, theme).expect("a straight road");
w.style.width = STYLE_W;
w
}
/// The facet exactly as a host mounts it, at `zoom`.
fn facet_at(zoom: f32, theme: &facett_core::Theme, way: Way) -> (MapView, MapFacet) {
let view = MapView::new(LatLon::new(59.32, 18.08), zoom, CANVAS);
let mut f = MapFacet::new("gg27", view, MapTheme::day(theme));
f.push_layer(Box::new(WaysLayer::new("osm-roads", vec![way])));
(view, f)
}
/// The width the GPU lane draws `way` at: the lane's own bake (`build_gpu_geometry`, so
/// `half_px` is what the vertex buffer really holds) pushed through the vertex path's
/// width resolution with the uniform the facet hands the callback. No second formula —
/// `resolved_width_px` is the line `applied_screen_pos` itself runs.
fn gpu_width_px(way: &Way, width_scale: f32) -> f32 {
let origin = [0.0, 0.0];
let geo = build_gpu_geometry(std::iter::once((way, way.style.lod as u32)), origin);
let v = geo
.line_vertices
.first()
.expect("the trunk bakes at least one pretty-line quad");
let rect = egui::Rect::from_min_size(egui::pos2(0.0, 0.0), egui::vec2(CANVAS.0, CANVAS.1));
let t = MapTransform::new(origin, [0.0, 0.0], 1.0, 1.0, rect);
let u =
MapLineUniforms::new(&t, CANVAS.0, CANVAS.1, 0.0, 0.0, None).with_width_scale(width_scale);
resolved_width_px(v, &u)
}
#[test]
fn cpu_and_gpu_lanes_draw_the_same_road_at_the_laws_width_at_zoom_8_and_14() {
let theme = facett_core::Theme::default();
let mut seen = Vec::new();
for zoom in [8.0f32, 14.0] {
let way = road(&theme);
let (view, f) = facet_at(zoom, &theme, way.clone());
let cpu = f.cpu_line_width_px_for_test(STYLE_W);
let gpu = gpu_width_px(&way, f.gpu_width_scale_for_test());
let law = facett_map::style::line_width_px(STYLE_W, view.world_px());
let scale = facett_map::style::line_width_scale(view.world_px());
eprintln!(
"[gg27] zoom {zoom}: style {STYLE_W} px, law scale {scale:.4} -> CPU {cpu} px, GPU {gpu} px, law {law} px"
);
assert_eq!(
cpu.to_bits(),
gpu.to_bits(),
"zoom {zoom}: the CPU painter strokes {cpu} px and the GPU lane draws {gpu} px for the same road"
);
assert_eq!(
cpu.to_bits(),
law.to_bits(),
"zoom {zoom}: both lanes draw {cpu} px but the ONE formula (style::line_width_px) says {law} px — \
the facet is not passing the live line_width_scale ({scale:.4}) to its lanes"
);
seen.push(law);
}
// The premise: the two zooms sit on different rungs of the ramp, so the equalities
// above are not all landing on the same number. Zoom 14 is past the 3.0x clamp and
// zoom 8 is not — a road really is drawn at two widths across this span.
assert_ne!(
seen[0].to_bits(),
seen[1].to_bits(),
"the law gives the same width at zoom 8 and 14 ({} px) — this test would prove nothing",
seen[0]
);
}
// ─────────────────────────────────────────────────────────────────────────────────
// HO-T7 — THE STANDALONE `MapView` WIDGET PASSES `width_scale: 1.0` AGAINST ITS OWN FIT.
// ─────────────────────────────────────────────────────────────────────────────────
//
// GG #27 above closed the disagreement inside `MapFacet`. It did not reach the OTHER
// widget in this crate: `facett_map::MapView` (the `Facet` in `src/lib.rs`, not the
// slippy camera in `layer.rs` that shares its name) fits the data's Mercator bbox into
// its pane and draws the same `Way`s through the same shader. Its two lanes:
//
// CPU (`MapView::view`) `fill_outline_width_px(style.width · zoom_w)`
// = max(w · zoom_w, 0.5) — the AREA-outline law, applied to a
// road: no zoom ramp, no [1, 24] pixel band, and against the
// widget's own `zoom_w` fit;
// GPU (`draw_ways_gpu`) `width_scale: 1.0`, literal, commented "a fit-to-rect static
// surface … the shared formula's 1× reference" — so the shader
// draws clamp(w · 1.0, 1, 24).
//
// Two formulas, one way, and neither is the law. The fit IS a camera: `mercator()`
// returns normalised 0..1, so pixels-per-Mercator-unit is pixels-per-world — the exact
// `world_px` `style::line_width_scale` takes, and the exact thing
// `layer::MapView::world_px()` (`256 · 2^zoom`) is. `draw_ways_gpu` already resolves the
// fit to `zoom_x`/`zoom_y` in those units; the fit is non-uniform and the law wants one
// scalar, so `MapView::fit_world_px` takes the SMALLER axis — the axis that limits how
// much ground a pixel covers, and the one the CPU painter's own `zoom_w` already picked
// with its `.min()`.
/// The widget's pane. Two of them, so the fit really moves between the two cases.
const WIDE: egui::Rect = egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1200.0, 800.0));
/// A twentieth of the area: the same data fitted into a thumbnail is a different camera.
const TINY: egui::Rect = egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(160.0, 120.0));
/// The trunk this widget fits to. It spans real ground in BOTH axes (≈7° of longitude,
/// 4° of latitude) so the fit lands on the interesting part of the law's ramp: the wide
/// pane comes out inside `[0.5, 3.0]` and the thumbnail on the `0.5` floor. `road` above
/// is east-west only, which pins `merc_h` to the `1e-12` guard and both fits to the `3.0`
/// ceiling — the same width twice, proving nothing.
fn fit_road(theme: &facett_core::Theme) -> Way {
let pts: Vec<(f64, f64)> = (0..=8).map(|i| (10.0 + i as f64 * 0.8875, 55.0 + i as f64 * 0.5)).collect();
let tags = vec![("highway".to_owned(), "trunk".to_owned())];
let mut w = Way::from_lonlat(1, &pts, &tags, theme).expect("a long diagonal road");
w.style.width = STYLE_W;
w
}
/// The widget, holding one trunk road, GPU lane opted in.
fn widget(theme: &facett_core::Theme) -> facett_map::MapView {
facett_map::MapView::new("ho-t7", Vec::new()).with_ways(vec![fit_road(theme)]).with_gpu(true)
}
/// **Both of the standalone widget's lanes draw a road at the width the ONE formula
/// says, at two different fits** — lane HO-T7.
#[test]
fn the_map_view_widgets_cpu_and_gpu_lanes_draw_one_width_at_two_fits() {
let theme = facett_core::Theme::default();
let v = widget(&theme);
let way = fit_road(&theme);
let mut seen = Vec::new();
for (name, rect) in [("wide", WIDE), ("tiny", TINY)] {
let world_px = v.fit_world_px_for_test(rect).expect("the fixture has ways to fit");
let cpu = v.cpu_line_width_px_for_test(STYLE_W, rect).expect("the fixture has ways to fit");
let scale = v.gpu_width_scale_for_test(rect).expect("the fixture has ways to fit");
let gpu = gpu_width_px(&way, scale);
let law = facett_map::style::line_width_px(STYLE_W, world_px);
let law_scale = facett_map::style::line_width_scale(world_px);
eprintln!(
"[ho-t7] {name} {}x{} px: fit world_px {world_px:.0}, law scale {law_scale:.4} -> \
CPU {cpu} px, GPU {gpu} px (uniform {scale}), law {law} px",
rect.width(),
rect.height()
);
assert_eq!(
cpu.to_bits(),
gpu.to_bits(),
"{name} fit: the widget's CPU painter strokes {cpu} px and its GPU lane draws {gpu} px \
for the same road. `MapView::view` uses the AREA-outline law against its own `zoom_w` \
while `draw_ways_gpu` hands the shader `width_scale: {scale}`"
);
assert_eq!(
cpu.to_bits(),
law.to_bits(),
"{name} fit: both of the widget's lanes would have to draw {law} px — the ONE formula \
(style::line_width_px) at this fit's world_px {world_px:.0}, scale {law_scale:.4} — \
and the CPU lane draws {cpu} px"
);
seen.push(law);
}
// The premise: the two fits really are two cameras, so the equalities above are not
// both landing on the same number.
assert_ne!(
seen[0].to_bits(),
seen[1].to_bits(),
"the law gives the same width at both fits ({} px) — this test would prove nothing",
seen[0]
);
}