Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/facett

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