facett-map3d/tests/continuous_viewport.rs
//! **The 3D pane draws the viewport, continuously, and fades — it is not a rectangle.**
//!
//! [`Map3D::from_geometry_in_view_roofed`] extrudes the town under the crosshair and
//! nothing else, a box of at most `2 ·`[`extrude::TOWN_HALF_MAX_M`] = **1.16 km**. On
//! korp's z12 viewport (37 km) that is 3 % of the width and 0.1 % of the area, and it is
//! what the operator was looking at: *"why is 3d only a tile / rectangle rotating? you
//! need to make it continously but cut grafiks at a distant nelgnht"*. Out in open
//! country it extrudes **nothing** and the pane paints a sentence over a viewport that
//! has roads and water in it: *"its ok to go 3d from 2d and there is no buildings, but
//! draw the rest"*.
//!
//! [`Map3D::from_viewport_roofed`] is the other question — *what is in the viewport, and
//! how much of it can this frame afford?* — and this file is the measurement under it.
//!
//! # The readings this file takes, on t14s (AMD 890M / RADV STRIX1)
//!
//! All DEBUG-profile: the release build was owned by another session and could not be
//! run, which is said here rather than implied by a number nobody took.
//!
//! ```text
//! the extruder emits 4.0 – 7.8 M tris/s (`throughput`)
//! the mesh costs 67 – 71 bytes/triangle (`throughput`)
//! z12, town path: 14 106 tris, p50 house 10.07 px, scene 1.16 km
//! z12, continuous path: 198 852 tris, p90 house 9.57 px, scene 16.58 km
//! dense city, 600 k budget: full detail to 2.29 km, boxes to 3.09 km,
//! 34 266 buildings dropped past that, p90 8.00 px
//! the triangle ESTIMATE tracks the applied mesh to within 0.5 %
//! ```
use facett_core::Theme;
use facett_map::Way;
use facett_map3d::{extrude, Map3D};
/// The ONE writer of the flat-Earth ruler these fixtures are built on: the extruder's
/// own [`facett_map3d::extrude::M_PER_DEG_LAT`]. A fixture built on a private copy of
/// 111_320 measures a DIFFERENT earth from the code under test the moment either side is
/// edited — and the copies agree until then, which is exactly how this survived in nine
/// files (`granska-20`).
use facett_map3d::extrude::M_PER_DEG_LAT;
const STOCKHOLM: (f64, f64) = (18.0686, 59.3293);
/// korp's MEASURED z12 viewport span on a 1900x1150 pane — the same anchor
/// `tests/zoom_ladder.rs` derives its ladder from.
const KORP_Z12_SPAN_M: f64 = 37_042.0;
use facett_map3d::extrude::m_per_deg_lon;
fn span_at(z: f64) -> f64 {
KORP_Z12_SPAN_M * 2f64.powf(12.0 - z)
}
fn ring(lon: f64, lat: f64, half_lon: f64, half_lat: f64) -> Vec<(f64, f64)> {
vec![
(lon - half_lon, lat - half_lat),
(lon + half_lon, lat - half_lat),
(lon + half_lon, lat + half_lat),
(lon - half_lon, lat + half_lat),
(lon - half_lon, lat - half_lat),
]
}
fn tags(kv: &[(&str, &str)]) -> Vec<(String, String)> {
kv.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect()
}
/// `n x n` 12 m houses on a `pitch_m` grid, centred on `centre`.
fn houses(id0: i64, centre: (f64, f64), n: usize, pitch_m: f64) -> Vec<Way> {
let (clon, clat) = centre;
let mpdl = m_per_deg_lon(clat);
let theme = Theme::default();
let (h_lon, h_lat) = (6.0 / mpdl, 6.0 / M_PER_DEG_LAT);
let mut ways = Vec::new();
let mut id = id0;
for j in 0..n {
for i in 0..n {
let dx = (i as f64 - (n as f64 - 1.0) * 0.5) * pitch_m;
let dy = (j as f64 - (n as f64 - 1.0) * 0.5) * pitch_m;
ways.extend(Way::from_lonlat(
id,
&ring(clon + dx / mpdl, clat + dy / M_PER_DEG_LAT, h_lon, h_lat),
&tags(&[("building", "yes")]),
&theme,
));
id += 1;
}
}
ways
}
/// `n x n` **hexagonal** 12 m houses — a footprint that is NOT its own bounding box, so
/// `Detail::Full` and `Detail::Coarse` are distinguishable by shape.
fn hex_houses(id0: i64, centre: (f64, f64), n: usize, pitch_m: f64) -> Vec<Way> {
let (clon, clat) = centre;
let mpdl = m_per_deg_lon(clat);
let theme = Theme::default();
let mut ways = Vec::new();
let mut id = id0;
for j in 0..n {
for i in 0..n {
let dx = (i as f64 - (n as f64 - 1.0) * 0.5) * pitch_m;
let dy = (j as f64 - (n as f64 - 1.0) * 0.5) * pitch_m;
let (cx, cy) = (clon + dx / mpdl, clat + dy / M_PER_DEG_LAT);
let mut hexagon: Vec<(f64, f64)> = (0..6)
.map(|k| {
let a = std::f64::consts::TAU * k as f64 / 6.0;
(cx + 6.0 * a.cos() / mpdl, cy + 6.0 * a.sin() / M_PER_DEG_LAT)
})
.collect();
hexagon.push(hexagon[0]);
ways.extend(Way::from_lonlat(id, &hexagon, &tags(&[("building", "yes")]), &theme));
id += 1;
}
}
ways
}
/// **A z12 viewport of real shape**: a dense inner city, three rings of suburbs out to
/// the viewport edge, a road grid spanning the whole viewport, and a lake. Deliberately
/// NOT one compact town — the whole point of the continuous path is what it does with the
/// geometry a town window throws away.
fn city_region() -> Vec<Way> {
let (clon, clat) = STOCKHOLM;
let mpdl = m_per_deg_lon(clat);
let theme = Theme::default();
let mut ways = houses(1_000, STOCKHOLM, 60, 40.0); // 2.4 km inner city
let mut id = 200_000i64;
for (r_km, count, side, pitch) in [(3.0, 6, 26, 45.0), (7.0, 8, 20, 55.0), (13.0, 8, 14, 60.0)] {
for k in 0..count {
let a = std::f64::consts::TAU * k as f64 / count as f64;
let (dx, dy) = (r_km * 1000.0 * a.cos(), r_km * 1000.0 * a.sin());
ways.extend(houses(id, (clon + dx / mpdl, clat + dy / M_PER_DEG_LAT), side, pitch));
id += 10_000;
}
}
let half_lat = 0.5 * KORP_Z12_SPAN_M / M_PER_DEG_LAT;
let half_lon = 0.5 * KORP_Z12_SPAN_M / mpdl;
for i in 0..24 {
let t = i as f64 / 23.0 - 0.5;
let h: Vec<(f64, f64)> = (0..12)
.map(|k| (clon - half_lon + 2.0 * half_lon * k as f64 / 11.0, clat + 2.0 * half_lat * t))
.collect();
let v: Vec<(f64, f64)> = (0..12)
.map(|k| (clon + 2.0 * half_lon * t, clat - half_lat + 2.0 * half_lat * k as f64 / 11.0))
.collect();
ways.push(Way::line(id, "road", h, egui::Color32::GRAY, 2.0));
ways.push(Way::line(id + 1, "road", v, egui::Color32::GRAY, 2.0));
id += 2;
}
ways.extend(Way::from_lonlat(
id,
&ring(clon - half_lon * 0.1, clat + half_lat * 0.05, half_lon * 0.06, half_lat * 0.06),
&tags(&[("natural", "water")]),
&theme,
));
ways
}
fn view(centre: (f64, f64), span_m: f64) -> extrude::ViewWindow {
extrude::ViewWindow { lon: centre.0, lat: centre.1, span_m }
}
// ─────────────────────────────────────────────────────────────────────────────
// THE CLAIMS
// ─────────────────────────────────────────────────────────────────────────────
/// **THE CLAIM: the scene is the viewport, not a 1.16 km box — and the near band is still
/// legible.**
///
/// The two halves are one assertion, because either alone is trivially satisfiable: a
/// wider scene is one line ([`extrude::view_town_subset`] with a bigger window) and it
/// costs the skyline — MEASURED before [`extrude::scene_reach_half_m`] existed, windowing
/// the whole 37 km viewport put the median house at **0.77 px** against the town path's
/// 10.07.
///
/// RED-WHEN-BROKEN: making `scene_reach_half_m` return `TOWN_HALF_MAX_M` fails (a);
/// making it return `span_m / 2` fails (b).
#[test]
fn the_scene_is_the_viewport_and_the_near_band_is_legible() {
let ways = city_region();
let budget = extrude::SceneBudget::default();
println!(
"{:>5} {:>9} | {:>8} {:>8} {:>7} | {:>8} {:>8} {:>7} {:>7} {:>9} {:>6}",
"zoom", "span_m", "OLDtris", "OLD m/du", "OLDp50", "NEWtris", "NEW m/du", "NEWp90",
"NEWmax", "reach_km", "whole",
);
for zi in 9..=16 {
let z = f64::from(zi);
let v = view(STOCKHOLM, span_at(z));
let old = Map3D::from_geometry_in_view_roofed("town", &ways, Some(v));
let new = Map3D::from_viewport_roofed("continuous", &ways, v, budget);
let s = new.viewport_scene().expect("the continuous path always reports");
let (p90, mx) = (new.skyline_percentile_px(1150.0, 0.9), new.skyline_percentile_px(1150.0, 1.0));
println!(
"{z:>5} {:>9.0} | {:>8} {:>8.1} {:>7.2} | {:>8} {:>8.1} {p90:>7.2} {mx:>7.2} {:>9.1} {:>6}",
v.span_m,
old.triangle_count(),
old.metres_per_design_unit(),
old.skyline_percentile_px(1150.0, 0.5),
new.triangle_count(),
new.metres_per_design_unit(),
s.ground_radius_m / 1000.0,
s.reach_is_the_viewport,
);
// (a) The scene really is wider. At the cap the town window is 1.16 km across; the
// continuous reach must be MULTIPLES of that at every zoom the operator can
// drive, or nothing has changed.
let town_across = 2.0 * extrude::town_half_for_span(v.span_m);
let scene_across = 2.0 * s.ground_radius_m;
assert!(
scene_across > 6.0 * town_across,
"z{z}: the continuous scene must be far wider than the town box \
({scene_across:.0} m vs {town_across:.0} m)",
);
// (b) …and the houses in the near band still stand up. `p50` is deliberately NOT
// the metric here: the population it is taken over is now the WHOLE visible
// scene out to the horizon, so most of it is far away by construction and a
// falling median is the falloff working, not a regression. The near band is
// the upper decile.
assert!(
f64::from(p90) >= extrude::LEGIBLE_MEDIAN_HOUSE_PX,
"z{z}: the near band must stay legible — p90 {p90:.2} px against a {:.0} px \
target (max {mx:.2})",
extrude::LEGIBLE_MEDIAN_HOUSE_PX,
);
}
}
/// **THE CLAIM: from the zoom where a viewport fits, the scene covers ALL of it.**
///
/// The reach is capped at `span_m / 2` — there is no point extruding ground the 2D pane
/// is not showing — so at street/town zoom the 3D pane and the 2D pane show the same box.
/// That is "continuously across the viewport" made assertable.
#[test]
fn at_town_zoom_the_scene_covers_the_whole_viewport() {
let ways = city_region();
let mut covered = 0;
for zi in 9..=17 {
let v = view(STOCKHOLM, span_at(f64::from(zi)));
let s = Map3D::from_viewport_roofed("c", &ways, v, extrude::SceneBudget::default())
.viewport_scene()
.expect("reports");
println!(
"z{zi}: viewport {:.2} km, scene {:.2} km, whole {}",
v.span_m / 1000.0,
2.0 * s.ground_radius_m / 1000.0,
s.reach_is_the_viewport,
);
if s.reach_is_the_viewport {
covered += 1;
assert!(
(2.0 * s.ground_radius_m - v.span_m).abs() < 2.0,
"z{zi}: 'whole viewport' must mean exactly the viewport",
);
}
}
assert!(covered >= 4, "the reach must cover the whole viewport at the deep end (only {covered} rungs)");
}
/// **THE CLAIM: the budget, not a metre constant, sets how deep the detail goes — and it
/// degrades smoothly.**
///
/// Four budgets over one 7.8 km solid city (67 600 footprints). Halving the budget must
/// halve the geometry and pull the bands IN; it must never empty the scene. That is what
/// "self-limiting, degrades on a weak GPU instead of falling off a cliff" means, stated
/// as something the build fails on.
#[test]
fn the_budget_sets_the_bands() {
let ways = houses(1, STOCKHOLM, 260, 30.0);
let v = view(STOCKHOLM, KORP_Z12_SPAN_M);
let mut last: Option<(usize, f64)> = None;
for tris in [150_000usize, 300_000, 600_000, 1_200_000] {
let budget = extrude::SceneBudget { triangles: tris, ..Default::default() };
let map = Map3D::from_viewport_roofed("dense", &ways, v, budget);
let s = map.viewport_scene().unwrap();
println!(
"budget {tris:>9}: mesh {:>9} tris (est {:>9}) full {:>6} houses to {:>6.0} m | \
boxes {:>6} to {:>6.0} m | {:>6} dropped p90 {:.2} px",
map.triangle_count(),
s.triangles,
s.buildings_full,
s.full_radius_m,
s.buildings_coarse,
s.coarse_radius_m,
s.buildings_dropped,
map.skyline_percentile_px(1150.0, 0.9),
);
// The budget is HONOURED: the applied mesh lands on it, not near it. (Ground cover
// and the road network are unconditional and can push past — see
// `extrude::viewport_scene` — but on a pure city they do not.)
assert!(
map.triangle_count() <= tris + tris / 10,
"the mesh must respect the budget: {} against {tris}",
map.triangle_count(),
);
// The bands are real: some buildings got boxes, some got dropped.
assert!(
s.buildings_full > 0 && s.buildings_coarse > 0 && s.buildings_dropped > 0,
"a budget under the scene must produce all three bands: {s:?}",
);
// …and MONOTONE in the budget: more triangles ⇒ the full band reaches further.
if let Some((prev_tris, prev_radius)) = last {
assert!(
s.full_radius_m > prev_radius,
"a bigger budget must reach further: {tris} reached {:.0} m, {prev_tris} \
reached {prev_radius:.0} m",
s.full_radius_m,
);
}
last = Some((tris, s.full_radius_m));
}
}
/// **THE CLAIM: the triangle estimate the budget spends is the mesh the extruder emits.**
///
/// A budget is only self-limiting if the number it counts is the number that arrives.
/// [`extrude::estimated_triangles`] is read off `build_with_water_terrain_styled`'s own
/// branches, and this is where that reading is checked against the applied mesh.
#[test]
fn the_estimate_is_the_mesh() {
let ways = city_region();
for zi in [10, 12, 14, 16] {
let scene = extrude::viewport_scene(
&ways,
view(STOCKHOLM, span_at(f64::from(zi))),
extrude::SceneBudget::default(),
);
let (mesh, ..) = extrude::build_with_water_terrain_styled(
&scene.ways,
0.22,
extrude::GroundTerrain::Flat,
extrude::RoofStyle::Pitched,
);
// The terrain quad is 2 triangles the estimator does not count (it is not a way).
let real = mesh.triangle_count().saturating_sub(2);
let est = scene.stats.triangles;
let err = (est as f64 - real as f64) / real.max(1) as f64;
println!("z{zi}: estimate {est} vs mesh {real} ({:+.2} %)", err * 100.0);
assert!(err.abs() < 0.02, "z{zi}: the estimate must be the mesh within 2 % — {est} vs {real}");
}
}
/// **THE CLAIM: a viewport with no buildings in it still draws the country it is over.**
///
/// The old path answered `TownChoice::TooCoarse` with an EMPTY ways set, and the pane
/// returned before it painted anything. So the operator got one sentence on a blank canvas
/// over a viewport holding six roads and a lake.
///
/// It also asserts the second half of that defect: the refusal used to say *"zoomed too
/// far out … Zoom in."* when the count was a hard **zero**, which is advice that cannot
/// possibly work. Two facts, two sentences ([`extrude::TownRefusal::message`]).
#[test]
fn open_country_still_draws_the_ground() {
let (clon, clat) = (16.5, 62.5); // korp's country centre — forest near Ånge, no houses
let mpdl = m_per_deg_lon(clat);
let theme = Theme::default();
let span = KORP_Z12_SPAN_M;
let (half_lat, half_lon) = (0.5 * span / M_PER_DEG_LAT, 0.5 * span / mpdl);
let mut ways = Vec::new();
for i in 0..6 {
let t = i as f64 / 5.0 - 0.5;
let line: Vec<(f64, f64)> = (0..10)
.map(|k| (clon - half_lon + 2.0 * half_lon * k as f64 / 9.0, clat + 2.0 * half_lat * t))
.collect();
ways.push(Way::line(i, "road", line, egui::Color32::GRAY, 2.0));
}
ways.extend(Way::from_lonlat(
99,
&ring(clon, clat, half_lon * 0.1, half_lat * 0.1),
&tags(&[("natural", "water")]),
&theme,
));
let v = view((clon, clat), span);
let old = Map3D::from_geometry_in_view_roofed("town", &ways, Some(v));
let new = Map3D::from_viewport_roofed("continuous", &ways, v, extrude::SceneBudget::default());
println!(
"open country — OLD: {} tiles, {} tris\n NEW: {} tiles, {} tris, {:?}",
old.tile_count(),
old.triangle_count(),
new.tile_count(),
new.triangle_count(),
new.viewport_scene(),
);
// The old path drew the terrain quad and nothing else — `tile_count <= 1` is
// `Map3D::view`'s own definition of "nothing to extrude".
assert_eq!(old.tile_count(), 1, "this fixture must reproduce the empty pane");
// The new one draws the roads and the water that ARE there.
assert!(new.tile_count() > 1, "the ground must be drawn: {} tiles", new.tile_count());
assert!(new.triangle_count() > 100, "…and be real geometry: {} tris", new.triangle_count());
let s = new.viewport_scene().unwrap();
assert!(s.lines_full + s.lines_coarse + s.lines_ground > 0, "the roads must survive: {s:?}");
assert_eq!(s.buildings_full, 0, "there are no houses here — that part was never in doubt");
// …and the refusal now says WHICH fact it is.
let r = new.town_refused().expect("the choice still reports");
assert!(r.no_buildings_at_all(), "zero buildings, not a thin sample");
let msg = r.message();
println!("refusal: {msg}");
assert!(!msg.contains("Zoom in"), "must not tell the operator to do the impossible: {msg}");
assert!(msg.contains("no buildings"), "must say which fact it is: {msg}");
// The other fact still reads the other way: a real city sampled too thinly.
let sparse = extrude::TownRefusal { half_m: 580.0, buildings: 7, needed: 24, span_m: 37_042.0 };
assert!(!sparse.no_buildings_at_all());
assert!(sparse.message().contains("Zoom in"), "{}", sparse.message());
}
/// **THE CLAIM: the falloff is a falloff.** Detail must be monotone in distance — a
/// nearer building can never be coarser than a farther one, or the "fade" is a
/// checkerboard of whatever happened to be cheap.
#[test]
fn detail_never_climbs_back_with_distance() {
// HEXAGONAL footprints, deliberately: a rectangular one IS its own bounding box, so a
// shape test over the `houses` grid cannot tell `Detail::Full` from `Detail::Coarse`
// and the assertion below would be vacuously true (it was, on the first run).
let ways = hex_houses(1, STOCKHOLM, 200, 30.0);
let v = view(STOCKHOLM, KORP_Z12_SPAN_M);
let scene =
extrude::viewport_scene(&ways, v, extrude::SceneBudget { triangles: 200_000, ..Default::default() });
let s = scene.stats;
assert!(s.full_radius_m < s.coarse_radius_m, "the bands must nest: {s:?}");
assert!(s.coarse_radius_m <= s.ground_radius_m, "…inside the reach: {s:?}");
// A `Detail::Coarse` building is its bounding BOX, so its ring has exactly two
// distinct longitudes and two distinct latitudes; a full-detail one is the tiler's
// own ring. Compared by shape rather than by vertex count, because the fixture's
// footprints are already rectangles and the counts are equal.
let boxy = |w: &Way| {
let mut lons: Vec<f64> = w.lonlat.iter().map(|p| p.0).collect();
let mut lats: Vec<f64> = w.lonlat.iter().map(|p| p.1).collect();
lons.sort_by(f64::total_cmp);
lats.sort_by(f64::total_cmp);
lons.dedup_by(|a, b| (*a - *b).abs() < 1e-12);
lats.dedup_by(|a, b| (*a - *b).abs() < 1e-12);
lons.len() <= 2 && lats.len() <= 2
};
// The distance is looked up on the SOURCE way by `osm_id`, not recomputed from the
// kept ring: a reduced ring has a slightly different centroid (a hexagon's bounding
// box is not centred on the hexagon's vertex-average), and comparing a walk ordered by
// one centroid against a test ordered by another produces a metre-scale false alarm at
// the band boundary — which is exactly what the first run of this test reported.
let dist_of: std::collections::HashMap<i64, f64> = ways
.iter()
.map(|w| {
let n = w.lonlat.len() as f64;
let c = w.lonlat.iter().fold((0.0, 0.0), |(a, b), &(x, y)| (a + x, b + y));
(w.osm_id, extrude::geo_distance_m((v.lon, v.lat), (c.0 / n, c.1 / n)))
})
.collect();
let mut nearest_box = f64::INFINITY;
let mut farthest_full = 0.0f64;
for w in scene.ways.iter().filter(|w| w.kind == "building") {
let d = dist_of[&w.osm_id];
if boxy(w) {
nearest_box = nearest_box.min(d);
} else {
farthest_full = farthest_full.max(d);
}
}
assert!(nearest_box.is_finite(), "the fixture must actually exhaust the full-detail budget");
println!("full detail out to {farthest_full:.0} m; first box at {nearest_box:.0} m; bands {s:?}");
assert!(
farthest_full <= nearest_box + 1.0,
"a full-detail house at {farthest_full:.0} m sits BEYOND a boxed one at \
{nearest_box:.0} m — the falloff is not monotone",
);
}
/// **The throughput reading [`extrude::VIEWPORT_TRIANGLE_BUDGET`] is derived from.** Not
/// an assertion with a threshold — a machine number, printed, so the constant beside it
/// can be re-derived rather than believed. DEBUG profile (see the module docs).
#[test]
fn throughput() {
for side in [40usize, 80, 120] {
let mut ways = houses(1, STOCKHOLM, side, 30.0);
let mpdl = m_per_deg_lon(STOCKHOLM.1);
for (n, r) in (0..side).step_by(4).enumerate() {
let dy = (r as f64 - side as f64 * 0.5) * 30.0;
let line: Vec<(f64, f64)> = (0..40)
.map(|k| {
let dx = (k as f64 / 39.0 - 0.5) * side as f64 * 30.0;
(STOCKHOLM.0 + dx / mpdl, STOCKHOLM.1 + dy / M_PER_DEG_LAT)
})
.collect();
ways.push(Way::line(900_000 + n as i64, "road", line, egui::Color32::GRAY, 2.0));
}
let t0 = std::time::Instant::now();
let (mesh, ..) = extrude::build_with_water_terrain_styled(
&ways,
0.22,
extrude::GroundTerrain::Flat,
extrude::RoofStyle::Pitched,
);
let ms = t0.elapsed().as_secs_f64() * 1e3;
let bytes =
mesh.positions.len() * 24 + mesh.indices.len() * 4 + mesh.colors.len() * 4 + mesh.building.len();
println!(
"side {side:>4}: {:>7} ways -> {:>9} tris in {ms:>7.1} ms = {:>5.2} M tris/s (DEBUG), \
{:.0} bytes/triangle",
ways.len(),
mesh.triangle_count(),
mesh.triangle_count() as f64 / (ms / 1e3) / 1e6,
bytes as f64 / mesh.triangle_count() as f64,
);
}
}
/// **THE CLAIM korp's `the_zoom_slider_moves_the_3d_scene` is really about: zooming in
/// brings the scene DOWN among the houses — and on this path that is the CAMERA, not the
/// design unit.**
///
/// # Why the mechanism moved, and what the honest reading is
///
/// On the town path the zoom is expressed in the **data**: [`extrude::view_town_subset`]
/// clips the geometry to [`extrude::town_half_for_span`] (580 m at z12, 253 m at z15),
/// the design box is fitted to whatever survives, and `metres_per_design_unit` therefore
/// tracks the zoom. That is a true statement about that mechanism and korp asserts it.
///
/// This path does not use it. The window is [`extrude::scene_reach_half_m`] — kilometres,
/// not hundreds of metres — so a town SMALLER THAN THE REACH is never clipped and the
/// design box does not move at all. Reproduced below on korp's own fixture (a 892 m town
/// plus one 37 km trunk road): the reach is 8 292 m at z12 and 2 315 m at z15, the 892 m
/// town fits inside both, and `m/du` reads **557.5 at both**. Nothing is broken; the
/// quantity simply stopped being the one that carries the zoom.
///
/// `m/du` still tracks wherever the reach really bites — a region-scale ways set goes
/// 10 365 → 2 894 m/du over the same two rungs
/// (`the_scene_is_the_viewport_and_the_near_band_is_legible`). It is no longer the
/// quantity that is *guaranteed* to.
///
/// What is guaranteed is [`extrude::ViewportStats::camera_distance_m`], published under
/// `$KORP_STATE.map.map3d.viewport.camera_distance_m`. This test asserts it on korp's
/// fixture, in korp's units, so korp's test has a reading to point at.
#[test]
fn zooming_in_brings_the_camera_down_among_the_houses() {
// korp's fixture verbatim: a DENSE 45x45 grid of 12 m houses on a 20 m pitch (a
// ~892 m town), plus one trunk road across the whole z12 viewport.
let (clon, clat) = STOCKHOLM;
let m = m_per_deg_lon(clat);
let theme = Theme::default();
let mut ways: Vec<Way> = Vec::new();
let mut id = 1_000i64;
for j in 0..45 {
for i in 0..45 {
let dx = (f64::from(i) - 22.0) * 20.0;
let dy = (f64::from(j) - 22.0) * 20.0;
ways.extend(Way::from_lonlat(
id,
&ring(clon + dx / m, clat + dy / M_PER_DEG_LAT, 6.0 / m, 6.0 / M_PER_DEG_LAT),
&tags(&[("building", "yes")]),
&theme,
));
id += 1;
}
}
let half_lon = 0.5 * KORP_Z12_SPAN_M / m;
ways.push(Way::line(
9,
"road",
vec![(clon - half_lon, clat), (clon + half_lon, clat)],
egui::Color32::GRAY,
2.0,
));
let town_span = extrude::world_span_metres(&ways[..ways.len() - 1]).expect("a town");
println!("korp's fixture: {} ways, town span {town_span:.0} m", ways.len());
let at = |z: f64| {
let v = view(STOCKHOLM, span_at(z));
let map = Map3D::from_viewport_roofed("korp fixture", &ways, v, extrude::SceneBudget::default());
let s = map.viewport_scene().unwrap();
let (mdu, p50) = (map.metres_per_design_unit(), map.skyline_percentile_px(1150.0, 0.5));
println!(
"z{z:>4}: reach {:>8.0} m, town_half {:>6.0} m | m/du {mdu:>7.1} tris {:>7} \
camera {:>7.0} m frame {:>6.0} m p50 {p50:>6.2} px",
s.ground_radius_m,
map.town_window().map(|w| w.half_m).unwrap_or(0.0),
map.triangle_count(),
s.camera_distance_m,
s.frame_extent_m,
);
(map, s, f64::from(mdu), f64::from(p50))
};
let (wide_map, wide, wide_mdu, wide_px) = at(12.0);
let (deep_map, deep, deep_mdu, deep_px) = at(15.0);
// The DIAGNOSIS, asserted so it cannot quietly stop being the explanation: the town is
// smaller than the reach at BOTH rungs, which is exactly why the design box is fixed.
assert!(
f64::from(town_span) < wide.ground_radius_m && f64::from(town_span) < deep.ground_radius_m,
"this fixture's town ({town_span:.0} m) must fit inside the reach at both rungs \
({:.0} m, {:.0} m) — that is the premise of everything below",
wide.ground_radius_m,
deep.ground_radius_m,
);
assert!(
(wide_mdu - deep_mdu).abs() < 1.0,
"…and therefore `metres_per_design_unit` is CONSTANT here by construction, not by \
accident: {wide_mdu:.1} → {deep_mdu:.1}. If this ever fails the diagnosis above is \
stale and korp's own assertion may be assertable again.",
);
// The town decision is still asked, still published, and still tracks the zoom — so a
// consumer reading `town_window.half_m` gets the same ladder it always did.
let (hw, hd) = (
wide_map.town_window().expect("the choice still reports").half_m,
deep_map.town_window().expect("the choice still reports").half_m,
);
assert!(
(hw - extrude::town_half_for_span(span_at(12.0))).abs() < 1.0
&& (hd - extrude::town_half_for_span(span_at(15.0))).abs() < 1.0,
"`town_window` must still be the ladder's answer: {hw:.1} / {hd:.1}",
);
assert!(hd < hw * 0.8, "…and it must still move with the zoom: {hw:.1} → {hd:.1}");
// **THE CLAIM.** The camera comes down, by the magnitude korp's test asks of the scene.
assert!(
deep.camera_distance_m < wide.camera_distance_m * 0.8,
"zooming in must bring the camera DOWN among the houses: {:.0} m → {:.0} m \
({:.3}x)",
wide.camera_distance_m,
deep.camera_distance_m,
deep.camera_distance_m / wide.camera_distance_m,
);
// …and it is not a number in a struct: the houses really do get bigger on the pane.
assert!(
deep_px > wide_px * 1.25,
"…and the applied skyline must grow with it: {wide_px:.2} px → {deep_px:.2} px",
);
}