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