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/risen_means_houses.rs

//! **`risen` must count houses, because that is what it is printed next to.**
//!
//! # The two numbers, and which one lied
//!
//! From the owner's `~/korp-ui.log`, `korp 5fd688b`, one rebuild:
//!
//! ```text
//! rebuild: … tris=813626 risen=36672 visible=36672 building_ways=0 scene_ways=86 …
//! ```
//!
//! `building_ways = 0` beside `risen = 36 672`. Both are printed on one line, under names
//! that imply they measure the same thing, and they cannot both be right about buildings.
//!
//! **`building_ways` was true.** It counts `LayerCategory::Buildings` over the loaded
//! basemap and the reply genuinely carried no building rows.
//!
//! **`risen` was the lie, and it is a lie by construction, not by accident.** It was
//! `mesh.positions.iter().filter(|p| p[1] > rise_eps).count()` — *every* vertex above the
//! scene's 1 m threshold — while `extrude::prism_height_for` raises **every closed ring
//! that is not `water`/`park`/`coastline`** through `building_design_height`. A
//! untagged closed ring, a `leisure` pitch, an untagged closed way: all of them
//! "rise", none of them is a house, and all of them were counted as
//! `building_vertices` / `buildings_visible`. So the pane could report tens of thousands
//! of "buildings" in a viewport that held none — which is exactly what it did.
//!
//! The extruder raising an untagged ring is deliberate (most OSM buildings carry no
//! `height`, and a ring that is a building without saying so should still stand up). What
//! was wrong is that nothing recorded WHICH rings were houses, so the observable named
//! after houses could not tell.
//!
//! # The mechanism, and why it is fix-by-construction
//!
//! [`extrude::Mesh3::building`] is stamped for every vertex by the ONE writer that knows —
//! the per-way loop in the extruder, which is holding `w.kind` at the time. Nothing
//! downstream re-derives it from a height, a colour or a y coordinate, so there is no
//! second definition to drift.
//!
//! # How it goes red
//!
//! `houses_and_industrial_blocks` is a camera over a real town of 36 houses **and** 20
//! raised untagged closed rings in the same window. Make `building_vertices` count
//! `p[1] > rise_eps` again (drop the class filter) and
//! `risen_counts_houses_not_every_raised_ring` fails on `class_blind=1266 houses=1266`,
//! and `a_scene_with_no_houses_reports_no_risen_houses` fails on 720 phantom houses.
//! SEEN RED both ways, 2026-08-06:
//!
//! | probe | `risen_counts_houses…` | `a_scene_with_no_houses…` |
//! |---|---|---|
//! | drop the class filter in `risen_house_positions` | RED (1266 = 1266) | RED (720 houses) |
//! | stamp every way `true` in the extruder | RED (1266 = 1266) | RED (720 houses) |
//!
//! Both controls (`the_fixture_holds_both_families`, `the_class_separates_the_two_families`)
//! stay green through both probes — the ground is stamped before the per-way loop, so
//! "stamp everything true" still leaves a `false` family and only the COUNTS move. That is
//! the honest reading: the separation guard is not the one that catches a mis-stamp; the
//! two counting guards are.

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, ViewWindow};
use facett_map3d::Map3D;

/// A ~1.8 km viewport — the rung the owner's second rebuild was on.
const SPAN_M: f64 = 1_827.0;
const HOUSES: usize = 36;
const BLOCKS: usize = 20;

/// A town the camera CAN choose, holding both families: `HOUSES` twelve-metre
/// `building=yes` footprints, and `BLOCKS` small non-building closed rings that the
/// extruder's any-other-ring branch also raises.
///
/// The blocks are `man_made=pier` rings (a closed pier AREA is common in harbour OSM).
/// They were `man_made=storage_tank` until 2026-09-29: since 69fdd5a (2026-09-03, "a way
/// whose tags say nothing is not a road") `Way::from_lonlat` REFUSES a way whose tags name
/// no classified key/pair, so every storage tank came back `None`, the fixture silently
/// held 0 blocks, and all three assertions on the two families went red. A pier is in
/// `CLASSIFIED_PAIRS`, classifies as a non-building `Path`, and still rises.
fn houses_and_industrial_blocks() -> Vec<Way> {
    let (clon, clat) = STOCKHOLM;
    let mpdl = m_per_deg_lon(clat);
    let theme = Theme::default();
    let mut ways: Vec<Way> = Vec::new();
    for i in 0..HOUSES {
        let (gx, gy) = (i % 6, i / 6);
        let dx = (gx as f64 - 2.5) * 40.0 / mpdl;
        let dy = (gy as f64 - 2.5) * 40.0 / M_PER_DEG_LAT;
        ways.extend(Way::from_lonlat(
            500 + i as i64,
            &ring(clon + dx, clat + dy, 6.0 / mpdl, 6.0 / M_PER_DEG_LAT),
            &tags(&[("building", "yes")]),
            &theme,
        ));
    }
    for i in 0..BLOCKS {
        let dx = (i as f64 - 9.5) * 22.0 / mpdl;
        let dy = 130.0 / M_PER_DEG_LAT;
        ways.extend(Way::from_lonlat(
            900 + i as i64,
            &ring(clon + dx, clat + dy, 9.0 / mpdl, 9.0 / M_PER_DEG_LAT),
            &tags(&[("man_made", "pier")]),
            &theme,
        ));
    }
    ways
}

fn camera() -> ViewWindow {
    ViewWindow { lon: STOCKHOLM.0, lat: STOCKHOLM.1, span_m: SPAN_M }
}

/// The control: the fixture really does hold both families, and the camera really does
/// choose a town off it (so nothing below passes on an empty scene).
#[test]
fn the_fixture_holds_both_families() {
    let ways = houses_and_industrial_blocks();
    assert_eq!(ways.iter().filter(|w| w.kind == "building").count(), HOUSES);
    assert_eq!(ways.iter().filter(|w| w.kind != "building").count(), BLOCKS);
    let m = Map3D::from_geometry_in_view_roofed("both", &ways, Some(camera()));
    let w = m.town_window().expect("the camera chose a town");
    assert_eq!(w.source, extrude::WindowSource::Camera);
    assert!(m.triangle_count() > 100, "a real mesh, got {} triangles", m.triangle_count());
}

/// **THE MEASUREMENT.** Every vertex the mesh carries is classified, and the two families
/// are separable — the property the old `y > rise_eps` filter could not have.
#[test]
fn the_class_separates_the_two_families() {
    let ways = houses_and_industrial_blocks();
    let m = Map3D::from_geometry_in_view_roofed("both", &ways, Some(camera()));
    let mesh = m.mesh();
    assert_eq!(
        mesh.building.len(),
        mesh.positions.len(),
        "EVERY vertex carries a class — a short class vector silently under-counts, \
         which is the failure mode this assert exists for"
    );
    let houses = mesh.building.iter().filter(|b| **b).count();
    let other = mesh.building.len() - houses;
    assert!(houses > 0, "the houses are stamped");
    assert!(other > 0, "…and the ground/terrain/industrial vertices are not");
}

/// **THE LIE, closed.** `building_vertices` counts HOUSES. The untagged rings rise —
/// they are in the mesh above the threshold — and they are not counted.
#[test]
fn risen_counts_houses_not_every_raised_ring() {
    let ways = houses_and_industrial_blocks();
    let m = Map3D::from_geometry_in_view_roofed("both", &ways, Some(camera()));
    let mesh = m.mesh();
    let eps = m.building_rise_eps();

    // What the OLD definition returned, computed here so the ratio is printed rather than
    // asserted from memory: every risen vertex, class-blind.
    let class_blind = mesh.positions.iter().filter(|p| p[1] > eps).count();
    let houses = m.building_vertices();
    assert!(class_blind > 0, "the fixture has risen geometry at all");
    assert!(houses > 0, "and some of it is houses");
    assert!(
        houses < class_blind,
        "the class-blind count folds the raised untagged rings into the house count: \
         class_blind={class_blind} houses={houses} — if these are equal the class filter \
         is not being applied"
    );
    // The number, for the record: what fraction of the old `risen` was actually houses.
    println!(
        "  risen: class-blind {class_blind}, houses {houses} \
         ({:.1} % of the old reading was a house)",
        100.0 * houses as f64 / class_blind as f64
    );

    // And the frustum half, which korp logs as `visible`, must agree with it — the two
    // read the same class through the same threshold, so `visible <= risen` always.
    assert!(
        m.buildings_visible() <= houses,
        "visible {} must be a subset of risen {houses}",
        m.buildings_visible()
    );
}

/// A scene of **nothing but raised non-buildings** reports ZERO houses — the owner's
/// `building_ways = 0` and an honest `risen` finally agreeing.
#[test]
fn a_scene_with_no_houses_reports_no_risen_houses() {
    let (clon, clat) = STOCKHOLM;
    let mpdl = m_per_deg_lon(clat);
    let theme = Theme::default();
    let mut ways: Vec<Way> = Vec::new();
    // 60 raised untagged rings, no `building` anywhere — enough that the camera window
    // is full of geometry and the scene is emphatically not empty.
    for i in 0..60 {
        let (gx, gy) = (i % 8, i / 8);
        let dx = (gx as f64 - 3.5) * 45.0 / mpdl;
        let dy = (gy as f64 - 3.5) * 45.0 / M_PER_DEG_LAT;
        ways.extend(Way::from_lonlat(
            700 + i as i64,
            &ring(clon + dx, clat + dy, 10.0 / mpdl, 10.0 / M_PER_DEG_LAT),
            &tags(&[("man_made", "pier")]),
            &theme,
        ));
    }
    // No camera: the scene is already town-sized, so this is the `Nothing` (keep
    // everything) path and the whole set is extruded — a genuinely full mesh with no
    // houses in it, which is the state that must report 0.
    let m = Map3D::from_geometry("no houses", &ways);
    let eps = m.building_rise_eps();
    let class_blind = m.mesh().positions.iter().filter(|p| p[1] > eps).count();
    assert!(
        class_blind > 100,
        "the fixture really does raise geometry — {class_blind} risen vertices, which is \
         what the old reading would have called buildings"
    );
    assert_eq!(
        m.building_vertices(),
        0,
        "a scene with no `building` ways has no risen HOUSES, however much else stands up"
    );
    assert_eq!(m.buildings_visible(), 0, "and none of them are in frame either");
    println!("  no-house scene: class-blind risen {class_blind}, houses 0");
}