facett-map3d/tests/no_town_still_bounds_the_camera.rs
//! **A viewport with no houses in it must not extrude the horizon.**
//!
//! # The three numbers this file is, and where they came from
//!
//! The owner's `~/korp-ui.log` on `korp 5fd688b`, second 3D rebuild after one click:
//!
//! ```text
//! rebuild: src=null half_m=null m/du=48232.0 tris=813626 risen=36672 visible=36672
//! gpu=false building_ways=0 scene_ways=86 aim_offset_m=30057 aim_ratio=16.45
//! ```
//!
//! * `m/du = 48 232` — one design unit is 48 km, so an 8 m house is `1.7e-4` units. No
//! camera can rescue that; the pane is a flat plane whatever else is right.
//! * `aim_ratio = 16.45` — with `aim_offset_m = 30 057` that puts the viewport span at
//! **1 827 m**, i.e. the extruded scene's centre lay **16 viewports** off screen.
//! * `scene_ways = 86` producing `tris = 813 626`.
//!
//! One line explains all three. `choose_town` counted building centroids BEFORE it looked
//! at the camera, so `building_ways = 0` returned `TownChoice::Nothing`, and
//! `view_town_subset`'s `Nothing` arm returns `ways.to_vec()` — the camera's `ViewWindow`
//! discarded. A korp map reply returns a way WHOLE when any of its vertices is inside the
//! box, so the 86 rows the 1.8 km box selected included coastline and lake mega-rings
//! reaching tens of kilometres out. The scene scale, the scene centre and the triangle
//! count are all read off the retained vertices, so all three followed the horizon.
//!
//! # The fixture
//!
//! [`lakes_and_no_houses`] is that shape, deliberately: a deep camera over ground that
//! holds **zero** buildings, and a handful of ways whose geometry runs far outside the
//! viewport. It is not a caricature — it is the measured one, scaled to the numbers above.
//!
//! # How each assertion goes red
//!
//! * Move the `pts.is_empty()` check back above the camera block in `choose_town` and
//! `an_aimed_camera_over_no_houses_refuses` fails on `TooCoarse` (it becomes `Nothing`),
//! `the_scene_scale_cannot_follow_the_horizon` fails with `m/du` back in the tens of
//! thousands, and `the_extruded_scene_stays_on_screen` fails with a ratio of ~16.
//! * Make `view_town_subset`'s `Nothing` arm return `Vec::new()` unconditionally and
//! `a_fixture_with_no_camera_still_keeps_everything` fails — the no-camera contract the
//! fixtures and the demos depend on is the reason `Nothing` still exists.
//!
//! Every assertion is on APPLIED OUTPUT: the retained way set's own world span, the
//! extruded mesh's own scale, and the scene's own geographic centre against the camera.
mod common;
use common::{m_per_deg_lon, ring, tags, M_PER_DEG_LAT, STOCKHOLM};
use facett_core::Theme;
use facett_map::Way;
use facett_map3d::extrude::{self, TownChoice, ViewWindow};
use facett_map3d::Map3D;
/// The viewport span implied by the owner's `aim_offset_m / aim_ratio` = 30 057 / 16.45.
const OWNER_SPAN_M: f64 = 1_827.0;
/// How far the mega-rings reach out of that viewport — `m/du · DESIGN_FILL` = the 77 km
/// world span the retained set actually had.
const MEGA_RING_SPAN_M: f64 = 77_000.0;
/// **The measured shape**: a deep camera over ground with NO buildings, and a few ways
/// (a coastline run, two lake rings, a trunk road) whose geometry reaches `MEGA_RING_SPAN_M`
/// out of the viewport — which is what a korp reply hands back when a way merely clips the
/// requested box.
fn lakes_and_no_houses() -> Vec<Way> {
let (clon, clat) = STOCKHOLM;
let mpdl = m_per_deg_lon(clat);
let theme = Theme::default();
// The geometry runs from the camera OUT (asymmetric), so a `MEGA_RING_SPAN_M`
// span means a full-span reach, not a half-span one.
let far_lon = MEGA_RING_SPAN_M / mpdl;
let far_lat = MEGA_RING_SPAN_M / M_PER_DEG_LAT;
let mut ways: Vec<Way> = Vec::new();
// **ASYMMETRIC ON PURPOSE.** The geometry reaches far in ONE direction, so the
// retained set's bbox centre is tens of km off the camera — which is what
// `aim_offset_m = 30 057` was. A symmetric fixture would put the centre back on the
// camera and the aim assertion would read 0.00 and pass for the wrong reason.
//
// A coastline running out of the viewport to the north-east, one vertex inside it.
ways.push(Way::line(
1,
"coastline",
vec![(clon, clat), (clon + far_lon * 0.5, clat + far_lat * 0.5), (clon + far_lon, clat + far_lat)],
egui::Color32::from_rgb(120, 160, 200),
2.0,
));
// Two lakes out along the same bearing, each far wider than the viewport.
for (i, (dx, dy)) in [(0.35, 0.30), (0.80, 0.72)].iter().enumerate() {
ways.extend(Way::from_lonlat(
10 + i as i64,
&ring(clon + dx * far_lon, clat + dy * far_lat, far_lon * 0.18, far_lat * 0.18),
&tags(&[("natural", "water")]),
&theme,
));
}
// Landuse rings small enough that `prism_height_for`'s untagged-ring branch RAISES
// them — this is where the 36 672 "risen" vertices came from in a scene with no
// houses at all.
for i in 0..8 {
let t = (f64::from(i) + 0.5) / 8.0;
ways.extend(Way::from_lonlat(
100 + i64::from(i),
&ring(clon + t * far_lon * 0.8, clat + t * far_lat * 0.8, 200.0 / mpdl, 200.0 / M_PER_DEG_LAT),
&tags(&[("landuse", "industrial")]),
&theme,
));
}
// A trunk road running out of the viewport, starting inside it.
ways.push(Way::line(
2,
"road",
vec![(clon, clat), (clon + far_lon, clat + far_lat * 0.2)],
egui::Color32::GRAY,
3.0,
));
ways
}
/// The camera the owner had: a ~1.8 km viewport centred on the same place.
fn deep_camera() -> ViewWindow {
ViewWindow { lon: STOCKHOLM.0, lat: STOCKHOLM.1, span_m: OWNER_SPAN_M }
}
/// The fixture really is the measured shape — a control, so nothing below passes on a
/// scene that would have worked anyway.
#[test]
fn the_fixture_is_the_measured_shape() {
let ways = lakes_and_no_houses();
let span = extrude::world_span_metres(&ways).expect("a real world span");
assert!(
(f64::from(span) / MEGA_RING_SPAN_M - 1.0).abs() < 0.15,
"the ways reach ~{MEGA_RING_SPAN_M:.0} m out of a {OWNER_SPAN_M:.0} m viewport, got {span:.0} m",
);
let houses = ways.iter().filter(|w| w.kind == "building").count();
assert_eq!(houses, 0, "and it holds NO buildings — that is the whole premise");
}
/// **THE ROOT.** A camera aimed at ground with no houses is a REFUSAL, not a licence to
/// keep the horizon.
#[test]
fn an_aimed_camera_over_no_houses_refuses() {
let ways = lakes_and_no_houses();
match extrude::choose_town(&ways, Some(deep_camera())) {
TownChoice::TooCoarse(r) => {
assert_eq!(r.buildings, 0, "the refusal reports the count it refused on");
assert_eq!(r.needed, extrude::MIN_BUILDINGS_IN_VIEW);
assert!(
(r.span_m - OWNER_SPAN_M).abs() < 1.0,
"and the camera span it was asked about: {} vs {OWNER_SPAN_M}",
r.span_m
);
}
other => panic!(
"an aimed camera over zero houses must refuse, not fall back to keeping \
everything — got {other:?}"
),
}
}
/// The scene the extruder is handed is bounded by the camera, so the design unit cannot
/// be a hostage to a lake on the horizon.
#[test]
fn the_scene_scale_cannot_follow_the_horizon() {
let ways = lakes_and_no_houses();
// APPLIED OUTPUT FIRST, because it is the owner's own number. `None` is the honest
// reading for an empty scene; what must never come back is a design unit of tens of
// kilometres. Asserted before the way count so the red run PRINTS the m/du it got.
let m = Map3D::from_geometry_in_view_roofed("no houses", &ways, Some(deep_camera()));
match m.design_units_per_metre() {
None => {}
Some(dpm) => panic!(
"an empty scene has no metric scale; got {:.1} m per design unit \
(the logged defect was 48 232), {} triangles",
1.0 / dpm,
m.triangle_count()
),
}
let subset = extrude::view_town_subset(&ways, Some(deep_camera()));
assert!(
subset.is_empty(),
"a refusal extrudes nothing (the pane's honest 'zoom in' state) — got {} ways",
subset.len()
);
// …and the triangle budget: only the flat terrain quad, not 813 626 triangles of
// horizon.
assert!(
m.triangle_count() <= 2,
"nothing but the ground quad may be extruded, got {} triangles",
m.triangle_count()
);
}
/// `aim_ratio` — the scale-free reading korp logs. There is no scene, so there is no
/// offset to report; the number that must never appear again is 16.
#[test]
fn the_extruded_scene_stays_on_screen() {
let ways = lakes_and_no_houses();
let cam = deep_camera();
let m = Map3D::from_geometry_in_view_roofed("no houses", &ways, Some(cam));
match m.geo_centre() {
None => {}
Some(c) => {
let offset = extrude::geo_distance_m((cam.lon, cam.lat), c);
let ratio = offset / cam.span_m;
panic!(
"the pane must not extrude a scene the user cannot see: centre {:.4},{:.4} \
is {offset:.0} m away, ratio {ratio:.2} (the logged defect was 16.45)",
c.0, c.1
);
}
}
// And the refusal is READABLE rather than merely empty — this is the surface a host
// paints "zoom in" from, and the one thing `town_window: null` could never say.
let r = m.town_refused().expect("the refusal reaches the scene");
assert_eq!(r.buildings, 0);
}
/// The contract `Nothing` still exists for: **no camera** ⇒ keep everything. Every
/// fixture and standalone demo in the crate depends on it, and narrowing `Nothing` must
/// not narrow this.
#[test]
fn a_fixture_with_no_camera_still_keeps_everything() {
let ways = lakes_and_no_houses();
assert_eq!(
extrude::choose_town(&ways, None),
TownChoice::Nothing,
"with no camera and no houses there is nothing to window"
);
let kept = extrude::view_town_subset(&ways, None);
assert_eq!(kept.len(), ways.len(), "…and the caller keeps its input unchanged");
}
/// A camera over ground that DOES hold houses is unaffected — the change is confined to
/// the arm that had no answer. Without this, "refuse everything" would pass every
/// assertion above.
#[test]
fn a_camera_over_real_houses_still_gets_its_town() {
let mut ways = lakes_and_no_houses();
let (clon, clat) = STOCKHOLM;
let mpdl = m_per_deg_lon(clat);
let theme = Theme::default();
// 36 twelve-metre houses on a 40 m pitch, centred on the camera — comfortably over
// MIN_BUILDINGS_IN_VIEW inside the clamped window.
for i in 0..36 {
let (gx, gy) = (i % 6, i / 6);
let dx = (f64::from(gx) - 2.5) * 40.0 / mpdl;
let dy = (f64::from(gy) - 2.5) * 40.0 / M_PER_DEG_LAT;
ways.extend(Way::from_lonlat(
500 + i64::from(i),
&ring(clon + dx, clat + dy, 6.0 / mpdl, 6.0 / M_PER_DEG_LAT),
&tags(&[("building", "yes")]),
&theme,
));
}
let cam = deep_camera();
let m = Map3D::from_geometry_in_view_roofed("a real town", &ways, Some(cam));
let w = m.town_window().expect("the camera chose the town");
assert_eq!(w.source, extrude::WindowSource::Camera);
assert!(w.buildings >= extrude::MIN_BUILDINGS_IN_VIEW, "{} buildings in the window", w.buildings);
// APPLIED OUTPUT: a legible scale and a scene on screen — the two things the defect
// destroyed, now asserted positively.
let mpdu = 1.0 / m.design_units_per_metre().expect("a real scene has a metric scale");
assert!(mpdu < 2_000.0, "a legible design unit, got {mpdu:.1} m");
let c = m.geo_centre().expect("a real scene has a place");
let ratio = extrude::geo_distance_m((cam.lon, cam.lat), c) / cam.span_m;
assert!(ratio < 0.5, "the town is on the user's screen, ratio {ratio:.3}");
}
/// **Two dolly floors, seven times apart — and now each one is asked where it binds.**
///
/// `hus3d-5`: `OrbitCamera::min_distance` (the FIELD, 0.4 du) is what an automatic fit
/// clamps against; `OrbitCamera::MIN_DISTANCE` (the CONST, 0.06 du) is what the
/// interactive dolly eases onto. Same shape as the `1e-3` / `BUILDING_RISE_EPS` split
/// `mapdrive-15` was about, and the item's own verdict was right: neither number is
/// wrong. What was wrong is that only one had a name, so the pair read as a duplicated
/// constant, and nothing asserted which one binds where — so a later "tidy-up"
/// collapsing them would look like a cleanup and silently change how close every
/// automatic framing in the crate parks.
///
/// This asserts the APPLIED distance at both ends, on a scene small enough that both
/// floors bind (a 40 m extent wants ~0.0004 du, far below either):
/// * the fitter settles at exactly `FRAME_MIN_DISTANCE`, not at `MIN_DISTANCE`; and
/// * the human dolly on that same camera then reaches ~`MIN_DISTANCE`, i.e. 6.7×
/// closer than the fit would ever park.
#[test]
fn the_two_dolly_floors_are_two_questions() {
use facett_map3d::camera::OrbitCamera;
// The gap is deliberate; if it ever closes, the two "questions" are one question
// and this test is measuring nothing.
let gap = OrbitCamera::FRAME_MIN_DISTANCE / OrbitCamera::MIN_DISTANCE;
println!(" frame floor {:.3} du / dolly floor {:.3} du = {gap:.2}x", OrbitCamera::FRAME_MIN_DISTANCE, OrbitCamera::MIN_DISTANCE);
assert!(gap > 2.0, "the two floors are {gap:.2}x apart — that is one floor with two names, not two questions");
let mut m = Map3D::synthetic("floors");
assert!(
(m.cam.min_distance - OrbitCamera::FRAME_MIN_DISTANCE).abs() < 1e-6,
"a fresh camera's framing floor is {} — the field must BE the named constant, not a second literal",
m.cam.min_distance
);
// A 40 m extent at the synthetic scene's scale: the geometrically-wanted framing
// distance is far below both floors, so whichever floor the fitter uses is the
// number that comes out.
let extent = 40.0 / facett_map3d::metres_per_design_unit(&[]); // 40 m at the span-less scale
m.frame_extent(facett_map3d::V3::new(0.0, 0.0, 0.0), extent, 0.0);
let fitted = m.cam.goal_distance;
println!(" fit of a {extent:.5} du extent settled at {fitted:.4} du");
assert!(
(fitted - OrbitCamera::FRAME_MIN_DISTANCE).abs() < 1e-4,
"the automatic fit settled at {fitted:.4} du — it must land on FRAME_MIN_DISTANCE ({:.4}), \
not on the interactive floor ({:.4})",
OrbitCamera::FRAME_MIN_DISTANCE,
OrbitCamera::MIN_DISTANCE
);
// …and the human can then walk in 6.7x closer than the fitter ever parks.
for _ in 0..400 {
m.cam.dolly(600.0, None);
}
m.cam.settle();
let dollied = m.cam.distance;
println!(" after 400 wheel-ins the dolly reached {dollied:.4} du");
assert!(
dollied < OrbitCamera::FRAME_MIN_DISTANCE * 0.5,
"the interactive dolly stopped at {dollied:.4} du — it must reach well past the FRAMING floor \
({:.4}); if it cannot, the fitter's floor is binding the human too",
OrbitCamera::FRAME_MIN_DISTANCE
);
assert!(
dollied >= OrbitCamera::MIN_DISTANCE,
"the dolly sank to {dollied:.4} du, below its own floor ({:.4})",
OrbitCamera::MIN_DISTANCE
);
}