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

nordisk/facett

facett-map3d/src/extrude.rs

//! **Extrusion** — turn 2D OSM ways ([`facett_map::Way`]) into a 3D triangle
//! mesh. The map's Mercator-unit footprints are recentred into a normalised
//! `[-1, 1]` design box (the same box [`facett_core::rabbit`] + facett-graph3d
//! use, so the camera + logo all agree on scale), then:
//!
//! - a **closed area/building ring** (a `Way` with a `fill`, or an explicitly
//!   closed loop) is **extruded into a prism**: a top cap lifted to `height`, a
//!   bottom cap on the ground (`y = 0`), and side walls joining the two rims, with
//!   outward + axial normals so the lit model catches the light around its edges;
//! - a **line way** (road / path / coastline) becomes a flat **ribbon** laid on
//!   the ground (`y = 0`), a thin extruded strip so roads read as a network, not
//!   hairlines;
//! - a **terrain plane** quad is fitted under the whole footprint so the city sits
//!   on ground, not in the void.
//!
//! The result is a [`Mesh3`] (positions + normals + per-triangle colours +
//! indices) the CPU painter / wgpu path projects, culls, sorts, and lights. Pure
//! data, deterministic, no RNG — two builds are byte-identical (snapshot-stable).

use std::borrow::Cow;
use egui::Color32;
use facett_map::{DEFAULT_BUILDING_HEIGHT_M, RoofShape, Way};

/// A single triangle's three vertex indices (into [`Mesh3::positions`]).
pub type Tri = [u32; 3];

/// The **surface class** of an extruded polygon — the prerequisite tag the water
/// feature keys on. `Opaque` polys (buildings/ground/roads) go to the normal mesh;
/// `Water` polys go to the dedicated water grid (Gerstner + Fresnel + reflection).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Surface {
    /// Buildings, ground cover, roads — the opaque depth-tested mesh.
    Opaque,
    /// Water (OSM `natural=water` / `waterway` / coastline fill) — the reflective
    /// animated water surface, at a fixed design-unit height.
    Water,
}

/// Is this way a **water** surface (so it goes to the reflective water mesh, not
/// the opaque ground cover)? Mirrors `prism_height_for`'s water classification.
#[must_use]
pub fn surface_of(kind: &str) -> Surface {
    match kind {
        "water" | "coastline" => Surface::Water,
        _ => Surface::Opaque,
    }
}

/// A 3D mesh of the extruded map: positions + per-vertex normals (parallel) +
/// per-**triangle** source colours + triangle indices. Coordinates are in the
/// normalised `[-1, 1]` design box (x right, **y up**, z depth) with the ground
/// at `y = 0` and prisms rising to `+height`.
#[derive(Clone, Debug, Default)]
pub struct Mesh3 {
    /// `(x, y, z)` per vertex.
    pub positions: Vec<[f32; 3]>,
    /// Unit normal per vertex (parallel to `positions`).
    pub normals: Vec<[f32; 3]>,
    /// Source colour per **triangle** (`indices.len() / 3` entries).
    pub colors: Vec<Color32>,
    /// Triangle indices (3 per face).
    pub indices: Vec<u32>,
    /// **Is this vertex part of an OSM `building`?** Parallel to
    /// [`positions`](Self::positions): `true` for a wall, foundation or roof vertex of a
    /// ring whose `kind` is `building`, `false` for terrain, ground cover, roads, route
    /// ribbons, water caps — and for every other closed ring the extruder raises.
    ///
    /// # Why a class and not a height test
    ///
    /// `Map3D::building_vertices` used to answer "is this a house?" with
    /// `p[1] > rise_eps`, i.e. *anything standing up*. But
    /// [`prism_height_for`] raises **every** closed ring that is not
    /// `water`/`park`/`coastline` — a `landuse=industrial` block, a `leisure` pitch, an
    /// untagged closed way — deliberately, because most OSM buildings carry no tag saying
    /// so. Height therefore cannot separate the two families, and the observable named
    /// after houses counted both: the owner's 2026-08-06 log printed `risen = 36 672` beside
    /// `building_ways = 0`, in a viewport that held no houses at all.
    ///
    /// The extruder is the only place that knows, because it is holding `w.kind` when it
    /// pushes the vertices. So it stamps this once, through
    /// [`classify_new_vertices`](Self::classify_new_vertices), and nothing downstream
    /// re-derives the answer from a y coordinate, a colour or a height (LAW 5).
    pub building: Vec<bool>,
}

impl Mesh3 {
    pub fn vertex_count(&self) -> usize {
        self.positions.len()
    }
    pub fn triangle_count(&self) -> usize {
        self.indices.len() / 3
    }

    /// Push one triangle (3 indices + its source colour).
    fn push_tri(&mut self, t: Tri, col: Color32) {
        self.indices.extend_from_slice(&t);
        self.colors.push(col);
    }

    /// Push a vertex (position + normal), returning its index.
    fn push_vertex(&mut self, pos: [f32; 3], nrm: [f32; 3]) -> u32 {
        let i = self.positions.len() as u32;
        self.positions.push(pos);
        self.normals.push(nrm);
        i
    }

    /// **Stamp [`building`](Self::building) for every vertex pushed since `from`** — the
    /// ONE writer of the class.
    ///
    /// Called once per feature by the extruder's own loop, which is the only code holding
    /// the way's `kind`. Idempotent in the sense that matters: it always brings
    /// `building.len()` up to `positions.len()`, so a feature that pushed no vertices
    /// costs nothing and a feature that pushed some cannot leave a gap. The
    /// `building.len() == positions.len()` invariant is asserted on APPLIED OUTPUT in
    /// `tests/risen_means_houses.rs`, because a short vector under-counts silently.
    pub(crate) fn classify_new_vertices(&mut self, from: usize, is_building: bool) {
        if self.building.len() < from {
            // A caller skipped a stamp. Fill the hole as NOT-a-building rather than
            // guessing: an unstamped vertex is one nobody claimed as a house.
            self.building.resize(from, false);
        }
        self.building.resize(self.positions.len(), is_building);
    }

    /// **Concatenate a per-worker arena onto this mesh, rebasing its indices** by the
    /// running vertex count — the same offset-and-append the 2D lane's
    /// `rebase_chunk_meta` does for its chunk geometry. Appending arenas in chunk
    /// order reproduces the serial per-way append order byte for byte: every
    /// parallel vector (`positions`/`normals`/`building` per vertex,
    /// `colors` per triangle, `indices` per corner) is extended in the same order the
    /// serial loop would have pushed it, and each index moves by exactly the vertex
    /// count that preceded its arena.
    fn append_rebased(&mut self, part: Mesh3) {
        // The stamp invariant holds per arena (every way that pushes vertices stamps
        // them), so it holds for the concatenation without a fix-up pass.
        debug_assert_eq!(part.building.len(), part.positions.len());
        let base = self.positions.len() as u32;
        self.positions.extend(part.positions);
        self.normals.extend(part.normals);
        self.colors.extend(part.colors);
        self.indices.extend(part.indices.into_iter().map(|i| i + base));
        self.building.extend(part.building);
    }
}

/// The **legacy absolute design-unit height** the water plane sits at. Kept only for
/// scenes with no world scale (`du_per_m == None` — the rabbit fixture, the unit-test
/// towns) so those render byte-for-byte as before. **Real data must go through
/// [`water_height`]**: as an absolute this is `3.1 m` above the ground at the default
/// scale, `9.4 m` at a 3 km town window and `174 m` at a 100 km region — a sea that
/// floats, and floats by a different amount in every viewport.
pub const WATER_HEIGHT: f32 = 5.0e-3;

/// **The clearance the water surface keeps over the terrain plane, in METRES.**
///
/// The scale-honest twin of [`GROUND_BAND_METRES`] and [`BUILDING_RISE_METRES`], and
/// for the same reason. The water plane has exactly one job above `y = 0`: win the
/// depth test against the coplanar terrain quad and sit above the flat ground cover
/// rather than inside it. That is a **kerb-height** question, not a design-unit one.
///
/// `0.6 m` sits by construction just over the top of the ground band
/// ([`ground_top`] ≈ `1.22 · 0.4 m` = `0.49 m`) and far under the shortest building
/// the extruder emits ([`MIN_DEFAULT_BUILDING_HEIGHT_M`] = 2.5 m), so "water covers
/// the ground cover" and "water never reads as a floating slab" both hold at **any**
/// scene span instead of by luck at one.
pub const WATER_RISE_METRES: f32 = 0.6;

/// **The scene's water-plane height in design units** — [`WATER_RISE_METRES`] taken
/// through the scene's own [`design_units_per_metre`], floored a clear
/// [`RISE_OVER_GROUND`] above the top of its ground band (so the sea can never sink
/// into a road ribbon) and capped at the legacy [`WATER_HEIGHT`] (so no scene ever
/// gets a *higher* water plane than it had before this fix).
///
/// `du_per_m == None` is SYNTHETIC data with no world scale: there is no metre to
/// convert, so [`WATER_HEIGHT`] is kept and those scenes are byte-for-byte unchanged.
///
/// **Honest bound:** the cap bites when `WATER_RISE_METRES · du_per_m > 5.0e-3`, i.e.
/// `du_per_m > 8.3e-3` — a span under ~190 m. At that point the scene is a single
/// block and the water plane is capped at `5.0e-3 du` = `0.6 m`… of a 190 m box. The
/// floor bites past a ~64 km span, where [`ground_band`] itself stops shrinking.
#[must_use]
pub fn water_height(du_per_m: Option<f32>) -> f32 {
    match du_per_m {
        Some(dpm) if dpm > 0.0 => (WATER_RISE_METRES * dpm)
            .max(RISE_OVER_GROUND * ground_top(ground_band(du_per_m)))
            .min(WATER_HEIGHT),
        _ => WATER_HEIGHT,
    }
}

/// [`water_height`] for a `ways` slice, through the **same** [`design_units_per_metre`]
/// the extruder scales everything else by — so the plane the water grid is built on and
/// the plane the reflection camera mirrors about can never be computed from two scales.
#[must_use]
pub fn water_height_for(ways: &[Way]) -> f32 {
    water_height(design_units_per_metre(ways))
}

/// **The Gerstner wave amplitude in METRES** — the height of the chop on the water
/// surface, measured crest-to-plane for the FIRST (largest) octave.
///
/// The scale-honest twin of [`WATER_RISE_METRES`], and the last raw design unit left
/// in the water lane. `water.wgsl` displaces `in.pos` by this length in **world**
/// (design) space, so as an absolute `0.0020 du` it was `1.25 m` of chop at the
/// fixture's 625 m/du, `3.75 m` in `korp`'s 3 km town window and `70 m` in a region
/// load — waves taller than the houses, and a different sea in every viewport.
///
/// `0.25 m` is a real inland/short-fetch chop, and it is *also* the number the
/// construction below needs: `0.25 · `[`GERSTNER_OCTAVE_SUM`]` = 0.49 m` fits under
/// [`WATER_RISE_METRES`] (`0.6 m`), so the summed trough of all three octaves cannot
/// reach the terrain plane the water sits on.
pub const WAVE_AMPLITUDE_METRES: f32 = 0.25;

/// **The base Gerstner wavelength in METRES** — crest-to-crest for the first octave;
/// the two others are [`GERSTNER_OCTAVE_WAVELENGTHS`] of it.
///
/// As an absolute `0.14 du` this was `87 m` at the fixture scale and `262 m` in the
/// town window — a swell the size of a city block rolling across a lake. `24 m` is a
/// real short-fetch wind wave, and it is what [`water_grid_cell`] sizes the
/// tessellation from, so the grid and the wave can never be tuned apart again.
pub const WAVE_WAVELENGTH_METRES: f32 = 24.0;

/// The **legacy absolute** amplitude, in design units. Kept for span-less synthetic
/// scenes (`du_per_m == None` — the rabbit fixture, the unit-test towns) so those
/// render byte-for-byte as before, and used by the RED half of the metric guard.
pub const WAVE_AMPLITUDE_DU: f32 = 0.0020;

/// The **legacy absolute** base wavelength, in design units. Same role as
/// [`WAVE_AMPLITUDE_DU`].
pub const WAVE_WAVELENGTH_DU: f32 = 0.14;

/// The per-octave amplitude multipliers in `water.wgsl::water_vs`, in order. The
/// vertex shader sums three Gerstner waves and scales the second and third down;
/// their SUM is the worst-case vertical displacement of the surface, which is what
/// [`wave_amplitude`] has to keep above the ground plane.
///
/// This is a Rust copy of numbers that live in the shader, so
/// `scene_scale_metric_constants::the_shader_octaves_match_the_rust_copy` reads them
/// straight back out of `water.wgsl` and fails if either side drifts.
pub const GERSTNER_OCTAVE_AMPLITUDES: [f32; 3] = [1.0, 0.6, 0.35];

/// The per-octave wavelength multipliers in `water.wgsl::water_vs`, in order. The
/// smallest is the one the tessellation has to resolve.
pub const GERSTNER_OCTAVE_WAVELENGTHS: [f32; 3] = [1.0, 0.55, 0.32];

/// The worst-case vertical displacement of the summed wave, as a multiple of the base
/// amplitude: `1.0 + 0.6 + 0.35 = 1.95`.
#[must_use]
pub fn gerstner_octave_sum() -> f32 {
    GERSTNER_OCTAVE_AMPLITUDES.iter().sum()
}

/// **The scene's Gerstner amplitude in design units** — [`WAVE_AMPLITUDE_METRES`]
/// taken through the scene's own [`design_units_per_metre`], and then capped so the
/// SUMMED trough of all three octaves still clears the terrain plane:
/// `sum · amp ≤ water_height(du_per_m)`. That cap is the construction — it is not a
/// tuned number, it is the statement "the sea does not cut through the ground", and
/// it holds at every span rather than by luck at one.
///
/// `du_per_m == None` is synthetic data with no world scale: [`WAVE_AMPLITUDE_DU`] is
/// kept and those scenes are byte-for-byte unchanged.
///
/// **Honest bound:** the metre wins wherever `WAVE_AMPLITUDE_METRES · sum` fits under
/// [`WATER_RISE_METRES`], which is everywhere `water_height` is on its metric arm
/// (`2.5e-5 < du_per_m < 8.3e-3`, i.e. spans from ~190 m to ~64 km). Outside that the
/// cap or the floor governs and the amplitude stops being an exact metre count — by
/// design, and in the safe direction.
#[must_use]
pub fn wave_amplitude(du_per_m: Option<f32>) -> f32 {
    match du_per_m {
        Some(dpm) if dpm > 0.0 => {
            (WAVE_AMPLITUDE_METRES * dpm).min(water_height(du_per_m) / gerstner_octave_sum())
        }
        _ => WAVE_AMPLITUDE_DU,
    }
}

/// **The scene's base Gerstner wavelength in design units** —
/// [`WAVE_WAVELENGTH_METRES`] through the scene's own [`design_units_per_metre`].
/// Synthetic scenes keep [`WAVE_WAVELENGTH_DU`].
#[must_use]
pub fn wave_wavelength(du_per_m: Option<f32>) -> f32 {
    match du_per_m {
        Some(dpm) if dpm > 0.0 => (WAVE_WAVELENGTH_METRES * dpm).max(1e-9),
        _ => WAVE_WAVELENGTH_DU,
    }
}

/// How many grid cells the tessellation wants across the SHORTEST Gerstner octave.
/// Four is the practical floor for a sine to read as a wave rather than as a zigzag.
const CELLS_PER_WAVE: f32 = 4.0;

/// **The water grid's target cell size in design units** — derived from
/// [`wave_wavelength`] so the tessellation and the wave it has to carry are ONE
/// number. The legacy `extent / 0.018` was an absolute chosen alongside the absolute
/// wavelength; the two were self-consistent at one scale and at no other.
///
/// **Honest bound:** the caller still clamps the resulting resolution to a vertex
/// budget (`12..=96` cells per ring), so a lake wider than `96 ·` this cell is
/// under-tessellated and its ripples read as flat. At a 3 km town window that bound is
/// a ~576 m lake; past it the wave is under a design unit of `5e-5` anyway, i.e.
/// sub-pixel, so flat is the honest picture rather than a lost feature.
#[must_use]
pub fn water_grid_cell(du_per_m: Option<f32>) -> f32 {
    match du_per_m {
        Some(dpm) if dpm > 0.0 => {
            (wave_wavelength(du_per_m) * GERSTNER_OCTAVE_WAVELENGTHS[2] / CELLS_PER_WAVE).max(1e-9)
        }
        _ => WATER_GRID_CELL_DU,
    }
}

/// The **legacy absolute** water-grid cell size, in design units — the `0.018` that
/// sat next to the absolute wavelength. Span-less scenes keep it.
pub const WATER_GRID_CELL_DU: f32 = 0.018;

/// The coarsest water grid any ring gets: enough cells that a lake's shoreline is a
/// polygon and not a triangle.
pub const WATER_MIN_RES: usize = 12;

/// The vertex budget: the most cells a single water ring may be tessellated into per
/// axis. `(96 + 1)² = 9409` vertices is already the ceiling one ring may cost.
pub const WATER_MAX_RES: usize = 96;

/// The grid a ring falls back to when the wave **cannot** be resolved inside
/// [`WATER_MAX_RES`] — see [`water_grid_res`]. Chosen to match what the legacy
/// `extent / 0.018` produced for a scene-sized ring (`≈ 18`), so the ring that used to
/// be budget-bound costs exactly what it used to.
pub const WATER_BUDGET_RES: usize = 18;

/// **How many cells per axis one water ring is tessellated into.**
///
/// ONE writer, and the reason it is a function rather than a literal: the grid exists
/// to carry the Gerstner wave, so the cell it wants is [`water_grid_cell`] — derived
/// from [`wave_wavelength`], not chosen beside it.
///
/// The part that is a JUDGEMENT and not a derivation, stated instead of buried: a ring
/// wider than `WATER_MAX_RES ·` cell cannot resolve the wave **whatever** resolution we
/// spend, because the budget binds first. Tessellating it to the ceiling therefore buys
/// aliasing at 26× the vertices, so it drops to [`WATER_BUDGET_RES`] — the density the
/// absolute `extent / 0.018` used to give it — and the ripples read as flat, which is
/// what a 0.25 m chop looks like from 1.8 km up anyway.
///
/// `extent` is the ring's larger bbox side, in design units.
#[must_use]
pub fn water_grid_res(extent: f32, du_per_m: Option<f32>) -> usize {
    let want = (extent / water_grid_cell(du_per_m)).ceil();
    if want <= WATER_MAX_RES as f32 {
        (want.max(0.0) as usize).clamp(WATER_MIN_RES, WATER_MAX_RES)
    } else {
        WATER_BUDGET_RES
    }
}

/// A **tessellated water mesh**: a grid of vertices over the water polygons, in the
/// design plane at `y = WATER_HEIGHT`. The GPU water pass vertex-displaces these by
/// summed Gerstner waves (`time` uniform), so the grid resolution sets the ripple
/// detail. Positions are `(x, y, z)`; `indices` are triangles; flat (no per-vertex
/// normal — the water shader computes the analytic Gerstner normal).
#[derive(Clone, Debug, Default)]
pub struct WaterMesh {
    pub positions: Vec<[f32; 3]>,
    pub indices: Vec<u32>,
}

impl WaterMesh {
    #[must_use]
    pub fn vertex_count(&self) -> usize {
        self.positions.len()
    }
    #[must_use]
    pub fn triangle_count(&self) -> usize {
        self.indices.len() / 3
    }
    #[must_use]
    pub fn is_empty(&self) -> bool {
        self.indices.is_empty()
    }

    /// The water twin of [`Mesh3::append_rebased`]: concatenate a per-worker water
    /// arena, offsetting its indices by the vertices already here.
    fn append_rebased(&mut self, part: WaterMesh) {
        let base = self.positions.len() as u32;
        self.positions.extend(part.positions);
        self.indices.extend(part.indices.into_iter().map(|i| i + base));
    }
}

/// Point-in-polygon (ray cast) for the water-grid cell test, in the `(x, z)` plane.
fn point_in_polygon(p: (f32, f32), poly: &[(f32, f32)]) -> bool {
    let n = poly.len();
    if n < 3 {
        return false;
    }
    let mut inside = false;
    let mut j = n - 1;
    for i in 0..n {
        let (xi, zi) = poly[i];
        let (xj, zj) = poly[j];
        if ((zi > p.1) != (zj > p.1))
            && (p.0 < (xj - xi) * (p.1 - zi) / (zj - zi + f32::EPSILON.copysign(zj - zi)) + xi)
        {
            inside = !inside;
        }
        j = i;
    }
    inside
}

/// Tessellate one water ring (design-plane `(x, z)`) into a grid of `res×res`
/// cells over its bbox, keeping the cells whose centre lies inside the polygon —
/// a small Gerstner-ready grid. Vertices at `y = y_water` — the scene's own
/// [`water_height`], never the absolute constant. Deterministic.
/// **Mesh a water ring.** `pub` since lane MP-b so the shore shape is assertable from
/// a test rather than only visible on a screen — the defect the owner reported is exactly
/// a shape this function decides.
pub fn water_grid_into(water: &mut WaterMesh, ring: &[(f32, f32)], res: usize, y_water: f32) {
    let poly = open_ring(ring);
    if poly.len() < 3 {
        return;
    }
    // ── THE SHORE IS THE RING, not a lattice (lane MP-b, 2026-09-05) ────────────
    //
    // This used to grid the ring's BOUNDING BOX and keep "a quad (2 tris) per cell
    // whose CENTRE is inside the polygon". The ring's vertices were never geometry,
    // only an inside/outside test for whole cells — so the shore was quantised to the
    // grid at ANY source precision. The owner, live on t14s: *"in 3D, zoomed in, lakes
    // are Minecraft-squared."* MEASURED on a 64-gon: 20 of 112 shore vertices lay on
    // the ring. Eighteen percent.
    //
    // Handing the lane a sharper level would not have rounded one edge, which is why
    // the level-swap hypothesis had nothing to fix. The cure is to triangulate the ring
    // itself, through the SAME ear-clip the 2D fill lane paints water with
    // (`facett_map::tessellate::triangulate`) — one tessellator, so the two views cannot
    // disagree about where a lake's edge is.
    //
    // `res` still decides how much INTERIOR detail the Gerstner wave gets: the ring's
    // own vertices carry the shore, and interior points are added on the same grid
    // pitch so a big lake still ripples instead of reading as one flat sheet. Only
    // points strictly inside the polygon are added — a grid point outside it would pull
    // a triangle over the shore and undo the whole change.
    // `triangulate` ear-clips a simple polygon RING. It is not a constrained
    // triangulation of a point set: appending interior samples to the vertex list makes
    // it read them as part of the outline, and the first cut of this change did exactly
    // that — a 64-gon came back with 260 boundary edges instead of 64, i.e. a shredded
    // mesh. The ring, and only the ring.
    let base = water.positions.len() as u32;
    let as_pos2: Vec<egui::Pos2> = poly.iter().map(|&(x, z)| egui::pos2(x, z)).collect();
    let tris = facett_map::tessellate::triangulate(&as_pos2);
    if tris.is_empty() {
        return;
    }
    for &(x, z) in &poly {
        water.positions.push([x, y_water, z]);
    }
    for t in tris {
        water.indices.extend_from_slice(&[base + t[0], base + t[1], base + t[2]]);
    }
    // `res` is deliberately unused now, and the trade is worth stating rather than
    // hiding: the Gerstner wave displaces VERTICES, so a lake meshed from its ring alone
    // ripples at the shore and reads flatter in the middle than the old grid did. That
    // is the same trade `water_grid_res` already made on purpose for region-scale water
    // ("a region-scale lake reads flat instead of costing 26x the vertices to alias the
    // same ripple") — and a flat middle is a lesser defect than a staircase shore, which
    // is the one an operator actually reported. Restoring interior detail means
    // subdividing the ring's triangles, not re-adding grid points to the outline.
    let _ = res;
}

/// The footprint bbox of all ways in Mercator-unit space, used to recentre +
/// scale the map into the `[-1, 1]` design box.
fn footprint_bbox(ways: &[Way]) -> Option<(f32, f32, f32, f32)> {
    let mut b: Option<(f32, f32, f32, f32)> = None;
    for w in ways {
        for &(x, y) in &w.points {
            b = Some(match b {
                None => (x, y, x, y),
                Some((x0, y0, x1, y1)) => (x0.min(x), y0.min(y), x1.max(x), y1.max(y)),
            });
        }
    }
    b
}

/// Fraction of the `[-1,1]` (span 2) box the data's **larger** axis fills — the
/// footprint is uniformly scaled so its long side spans `DESIGN_FILL` design
/// units (≈80% of the box). The height scaler below uses this so a building's
/// metric height maps to the SAME design-units-per-metre as its footprint.
pub const DESIGN_FILL: f32 = 1.6;

/// **Where the user is looking** — the 2D map camera, handed to the 3D builder so the
/// extruded town is the one under the viewport instead of whichever one happens to have
/// the most houses in it.
///
/// `span_m` is the viewport's **larger** real-world span in metres (the same quantity
/// [`world_span_metres`] answers for a ways set), so the two can be compared directly.
/// The host builds this from its own map camera — korp inverts its Web-Mercator
/// viewport bbox — and this crate never guesses at one.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ViewWindow {
    /// Viewport centre longitude (WGS-84 degrees).
    pub lon: f64,
    /// Viewport centre latitude (WGS-84 degrees).
    pub lat: f64,
    /// The viewport's larger span, in metres.
    pub span_m: f64,
}

/// **The legibility target the two bounds below are derived from: how many pixels the
/// MEDIAN house must stand on a 1150 px pane.**
///
/// Named, because it is the only number in this file that is a judgement rather than a
/// measurement, and because both bounds are now answers to it instead of independent
/// guesses. Eight pixels is a skyline you can read; a third of one is the flat white
/// pancake this whole subset exists to stop drawing.
pub const LEGIBLE_MEDIAN_HOUSE_PX: f64 = 8.0;

/// **The readability cap on the town half-window, in metres.**
///
/// # It was derived by extrapolation, and the extrapolation was wrong by 2.7x
///
/// The previous value (1500 m) came from one reading and one assumed law: at Stockholm
/// z12 the loaded ways spanned ~55.7 km and the median building stood **0.43 px** on a
/// 1900x1150 window (2026-08-03, live korp-server, RTX 4090); pixel height goes as
/// 1/span, so a 3 km scene "puts that same median at `0.43 · 55700/3000 ~= 8 px`".
/// The intent was [`LEGIBLE_MEDIAN_HOUSE_PX`] and the arithmetic is right.
///
/// **The step that does not hold is "that same median".** Narrowing the window does not
/// magnify a fixed population of houses, it CHANGES the population: the 55.7 km reading
/// is dominated by suburban sheds, the 3 km one by inner-city blocks, and the two
/// medians are not the same building. MEASURED end to end instead of extrapolated
/// (`tests/zoom_ladder::print_the_ladder`, the 2.4 km / 3600-house fixture, viewport
/// 1150 px, `skyline_percentile_px`):
///
/// ```text
///   half_m    scene_m    p50 px    p90 px    max px
///     1500       2372      2.99      4.74      8.00     <- the OLD cap
///     1158       2292      3.19      5.27      9.11
///      579       1092      8.89     16.28     30.42
///      400        772     10.90     21.40     46.34
/// ```
///
/// At the old cap the median house stood **2.99 px**, not 8 — the cap missed its own
/// stated target by 2.7x, and 2.99 px is inside the definition of the pancake the same
/// docstring gives ("a third of one").
///
/// **580 m is not a fit, it is a row of that table.** A `p50 = A · half^-k` fit through
/// the bracketing rows suggests 622 m, and 622 m MEASURES 6.38 px — the curve is lumpy
/// at the few-percent scale because the framing quantises on `frame_densest_cluster`'s
/// 3x3 cell neighbourhood, so a fitted constant would have been a number nobody had
/// looked at. 580 m is the widest window on the ladder whose APPLIED p50 clears
/// [`LEGIBLE_MEDIAN_HOUSE_PX`]: **8.89 px**, 2.97x the old cap, at 9 410 triangles
/// against 43 202. Cheaper AND legible; the old cap was paying 4.6x the geometry for a
/// third of the house.
///
/// The cost is stated plainly: the camera sees a smaller piece of the city at every zoom
/// above the cap — 1.16 km across rather than 3 km. That is the trade the constant
/// exists to make, and it is the direction the evidence points; there is no third
/// option, because the window can never be wider than legibility allows.
///
/// **What this does NOT fix, and cannot.** Above the cap the extruded scene is identical
/// at every zoom, and that is forced, not a defect: korp's z9 viewport is 159 km, and any
/// window that honoured it would BE the pancake. A flat band above the cap is what
/// capping means. What was wrong was where the cap sat and what it showed there.
pub const TOWN_HALF_MAX_M: f64 = 580.0;

/// **The floor on the town half-window, in metres.**
///
/// The old floor (400 m) was argued as "a 300 m window is three houses and no street".
/// MEASURED on the same ladder, a 400 m half-window is a **772 m** scene holding 400
/// houses and 4802 triangles — not three houses, a small town seen from outside it. It
/// was not the floor that was too low; the estimate under it was.
///
/// The floor is what the deep end of the zoom is FOR — "down among the houses" — and at
/// 400 m the deepest the pane can be driven is still a model on a table. 150 m is a 300 m
/// scene: a couple of blocks and the streets between them, which is a place you can be
/// inside of. MEASURED on the ladder, the deepest rung goes from `p50 10.90 px` (400 m,
/// 400 houses) to **32.23 px** (150 m, 64 houses), and dollied in from 14 px to 39 px
/// with a 364 px tallest wall — a street, not a table model. The band over which the
/// window tracks the zoom is exactly `log2(MAX/MIN)` wide, so this also widens it from
/// 1.91 camera zooms to 1.95 despite the cap coming down 2.6x.
pub const TOWN_HALF_MIN_M: f64 = 150.0;

/// **How much geometry a camera window must hold to count as "a place the user is
/// looking at".** A whole-Sweden viewport has a centre, and that centre is open Baltic;
/// honouring it would extrude water. This is the threshold that separates "the user
/// picked a town" from "the user is looking at a country", and it is deliberately LOW —
/// the question is *is there a scene here at all*, not *is this the best scene*.
///
/// MEASURED on the shipped thin `korp-ui` against the live `korp1.1` server, a real
/// focused 1900x1150 Xvfb window, camera over central Stockholm at zoom 13.2
/// (2026-08-05, RTX 4090 [discrete/Vulkan]): **3 228** building centroids inside the
/// camera window, against a threshold of 24 — a margin of 134x. A real town is not
/// near this line; it is two orders of magnitude clear of it.
///
/// The other end is the country overview, where the count has to collapse to roughly
/// nothing or the fallback never fires. That case is asserted on the synthetic
/// two-town fixture (`tests/camera_picks_the_town.rs`, a 900 km viewport centred east
/// of Gotland → `WindowSource::Densest`); it has NOT been measured against the live
/// pyramid, and this comment says so rather than implying a reading nobody took.
///
/// Because the two ends sit orders of magnitude apart, the exact value matters far
/// less than having one at all. 24 is "more than a hamlet, less than a village".
pub const MIN_BUILDINGS_IN_VIEW: usize = 24;

/// **The WIDE anchor: the viewport span at which the town window sits at its cap.**
///
/// korp's MEASURED z12 viewport on a 1900x1150 pane (korp `5f0ba51`:
/// `156543.03 · cos(59.33) / 2^12 = 19.49 m/px × 1900 px`). Anything wider than this is
/// a region overview and gets the widest legible town there is.
pub const TOWN_ZOOM_WIDE_SPAN_M: f64 = 37_042.0;

/// **The DEEP anchor: the narrowest viewport korp's map camera can actually produce.**
///
/// MEASURED 2026-08-05 on the shipped thin `korp-ui` against the live `korp1.1` server,
/// a real focused 1900x1150 Xvfb window (RTX 4090 [discrete/Vulkan]): driving the wheel
/// past the z15 rung parks the camera at **16.30** and it goes no further — the drive
/// harness's `aim 17` does not converge — where `map3d_aim.viewport_span_m` reads
/// **1245.0**. That is the closest the user can get, so it is where the deep end of the
/// window's travel has to land.
///
/// **This number is the whole reason [`town_half_for_span`] is not just a clamp.** With
/// the window equal to half the viewport span, the only camera zooms that move it are
/// those whose half-span lies inside `[TOWN_HALF_MIN_M, TOWN_HALF_MAX_M]` — spans of
/// 300 m to 1160 m. korp cannot produce a span under 1245 m at all, so under the plain
/// clamp the band is **unreachable from above**: every zoom the user can reach is pinned
/// at the cap and the 3D scene is one scene, forever. That is the report, restated as
/// the two measurements that force it.
pub const TOWN_ZOOM_DEEP_SPAN_M: f64 = 1_245.0;

/// **The town half-window for a viewport span — the ONE writer for "how much world does
/// the 3D pane show at this zoom" (L5).**
///
/// # Why this is a log interpolation and not `0.5 · span` clamped
///
/// The obvious spelling is "the window IS the viewport, clamped to what is legible", and
/// it was the shipped one. Two measurements kill it:
///
/// * a legible scene is at most `2·TOWN_HALF_MAX_M` = 1.16 km across (see that constant:
///   at 3 km the median house stands 2.99 px), and
/// * korp's map camera bottoms out at a **1245 m** viewport ([`TOWN_ZOOM_DEEP_SPAN_M`]).
///
/// So the clamp's band lies entirely BELOW every viewport korp can produce, and the
/// identity relation delivers exactly one scene at every reachable zoom. MEASURED on the
/// live server: `metres_per_design_unit` **1874.0** at camera 9.30 and **1875.0** at
/// camera 14.10 — five zoom levels, one scene, `half_m = 1500.0` at both. The 2D pane
/// zooms 2^10; the 3D pane zooms 1.00x. No choice of the two bounds fixes that, because
/// widening the cap to reach the band is the pancake and narrowing it moves the band
/// further out of reach. The RELATION has to change.
///
/// # What it is
///
/// A geometric interpolation of the half-window between its two bounds, driven by where
/// the viewport span sits between the two anchors, in log space (which is the space zoom
/// levels live in — one zoom level is one factor of two on both axes):
///
/// ```text
///   t        = clamp( log2(WIDE/span) / log2(WIDE/DEEP), 0, 1 )
///   half_m   = MAX · (MIN/MAX)^t
/// ```
///
/// so a whole-country viewport gets [`TOWN_HALF_MAX_M`], the deepest reachable viewport
/// gets [`TOWN_HALF_MIN_M`], and **every zoom in between moves the scene**, monotonically
/// and by a constant factor per zoom level (`(MIN/MAX)^(1/log2(WIDE/DEEP))` = 0.777x per
/// zoom, a 1.29x magnification per level). Both ends stay clamped, so no camera can ask
/// for a scene outside the legible band.
///
/// The honesty cost, stated: the 3D scene is no longer the same patch of world the 2D
/// viewport shows — it is a legible fraction of it that shrinks as you zoom in. It never
/// was the same patch (the cap saw to that); what changes is that the discrepancy is now
/// a declared, monotone function instead of a constant with a clamp hiding behind it.
#[must_use]
pub fn town_half_for_span(span_m: f64) -> f64 {
    if !span_m.is_finite() || span_m <= 0.0 {
        return TOWN_HALF_MAX_M;
    }
    let decades = (TOWN_ZOOM_WIDE_SPAN_M / TOWN_ZOOM_DEEP_SPAN_M).log2();
    let t = ((TOWN_ZOOM_WIDE_SPAN_M / span_m).log2() / decades).clamp(0.0, 1.0);
    // Geometric, not linear: equal zoom steps must give equal RATIOS, or the scene
    // would crawl at one end of the band and jump at the other.
    TOWN_HALF_MAX_M * (TOWN_HALF_MIN_M / TOWN_HALF_MAX_M).powf(t)
}

/// Which rule picked the town window — the observable that makes the camera-first
/// policy assertable instead of assumed. Published by korp under
/// `$KORP_STATE.map.map3d.town_window.source`.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum WindowSource {
    /// The **camera** chose it: the viewport centre held enough buildings
    /// ([`MIN_BUILDINGS_IN_VIEW`]) to be a scene. This is the normal case and the one
    /// the user means by "show me what I am looking at".
    Camera,
    /// **No usable camera** — none supplied (a fixture, a standalone demo), or one whose
    /// centre/span was not a finite place — so the densest cluster of building footprints
    /// chose it. The historical behaviour, now the fallback.
    ///
    /// An aimed camera whose window is EMPTY does **not** land here: that is a
    /// [`TownChoice::TooCoarse`] refusal, because the vote would answer confidently about
    /// somewhere the user is not looking (Stockholm → Sigtuna, 37 km; Stockholm at z9 →
    /// a town 66 km out).
    Densest,
    /// **The camera's own window was empty, so it SLID to the densest cluster still inside
    /// the camera's viewport.**
    ///
    /// Distinct from [`Self::Densest`], which is unbounded and can answer confidently about
    /// somewhere the user is not looking. This one cannot: every candidate is inside the box
    /// the operator has on screen, so the worst it can do is pick the wrong block of the
    /// street they are already looking at.
    ///
    /// MEASURED 2026-08-08 on the owner's live session: camera span **4488 m** with **940
    /// buildings on screen**, `town_half_for_span` = **250 m**, and that centre 250 m held
    /// **zero**. The refusal is correct reasoning for a country overview — its comment argues
    /// the case at 66 km — and simply wrong at city zoom, where the buildings are right there
    /// and merely not under the crosshair. He got `0 resta, 2 trianglar` and a flat pane over
    /// a city he could see.
    CameraSlid,
}

/// The town window a subset would use: where it is, how big it is, who chose it, and how
/// many building footprints are in it. Pure — computing it moves no geometry — so a
/// caller (and a test) can ask *what will be extruded* without extruding it.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct TownWindow {
    /// Which rule chose this window.
    pub source: WindowSource,
    /// Window centre longitude (WGS-84 degrees).
    pub lon: f64,
    /// Window centre latitude (WGS-84 degrees).
    pub lat: f64,
    /// Half-size in metres — the window is `2·half_m` across.
    pub half_m: f64,
    /// Building footprints whose centroid lies inside it.
    pub buildings: usize,
}

/// **Why a camera-aimed window was refused** — the numbers behind the refusal, so the
/// pane can say more than "empty" and a test can assert on the reason rather than on a
/// painted string.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct TownRefusal {
    /// The window that was asked about (metres, half-size) — the clamped camera span.
    pub half_m: f64,
    /// How many building footprints were actually in it.
    pub buildings: usize,
    /// How many it needed ([`MIN_BUILDINGS_IN_VIEW`]).
    pub needed: usize,
    /// The camera's own span in metres — what the user is looking at, and the number
    /// that tells them which way to zoom.
    pub span_m: f64,
}

impl TownRefusal {
    /// **Is the view simply EMPTY of houses, as opposed to too coarse to resolve them?**
    ///
    /// [`choose_town`] answers `TooCoarse` for both, because both are "the window could
    /// not be filled" — but they are not the same fact and they do not have the same
    /// remedy. Over open country the count is a hard **zero** and no amount of zooming
    /// produces a house; over a city at region zoom the houses exist and the sample is
    /// thin. The pane told the operator to *zoom in* in both cases, which is advice that
    /// cannot possibly work in the first one.
    #[must_use]
    pub fn no_buildings_at_all(&self) -> bool {
        self.buildings == 0
    }

    /// **The refusal, in words — the ONE writer** (L5). The pane paints this and korp
    /// logs it, and when they were two format strings they drifted: korp's line said
    /// "Nothing extruded" after the pane had started drawing the ground.
    ///
    /// Two facts, two sentences — see [`Self::no_buildings_at_all`].
    #[must_use]
    pub fn message(&self) -> String {
        if self.no_buildings_at_all() {
            format!(
                "no buildings in this {:.0} km view — nothing here to raise. The ground, \
                 water and roads that ARE here are drawn.",
                self.span_m / 1000.0,
            )
        } else {
            format!(
                "too sparse to raise a town: the {:.0} km view holds {} buildings in its \
                 {:.1} km centre and {} are needed. Zoom in for a skyline.",
                self.span_m / 1000.0,
                self.buildings,
                2.0 * self.half_m / 1000.0,
                self.needed,
            )
        }
    }
}

/// The three honest answers to "which town do I extrude?".
///
/// [`town_window`] flattens this to `Option<TownWindow>` for the callers that only care
/// about the positive case; [`choose_town`] is the one that can say *refused*.
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum TownChoice {
    /// A window was chosen — by the camera, or by the vote when there is no camera.
    Chosen(TownWindow),
    /// **A camera was supplied, and the data under it could not answer.** The vote is
    /// NOT consulted: it would answer confidently about somewhere else.
    ///
    /// `buildings: 0` is inside this answer, not [`Self::Nothing`]. "The camera is aimed
    /// at a place that holds no houses" is a refusal like any other short count — see
    /// [`choose_town`] for the measurement that moved it here.
    TooCoarse(TownRefusal),
    /// Nothing to window **and no camera to bound it by**: no buildings at all, or too
    /// few to cluster, on a call that passed `view: None`. The caller keeps everything —
    /// this is the synthetic-fixture / tiny-input case, not a refusal.
    ///
    /// It is reachable ONLY with `view == None` (or a non-finite window). With a camera in
    /// hand every unanswerable case is a [`Self::TooCoarse`], because "keep everything" is
    /// not a safe default once real geometry is involved: see [`view_town_subset`].
    Nothing,
}

/// Building centroids in `(lon, lat)` — the "where are the houses" reading every window
/// decision below is taken on. One writer (L5): the densest-cell vote and the
/// camera-window population count are the same question asked at two places, and when
/// they were two loops they were free to disagree about what a building is.
fn building_centroids(ways: &[Way]) -> Vec<(f64, f64)> {
    let mut pts: Vec<(f64, f64)> = Vec::new();
    for w in ways {
        if w.kind != "building" || w.lonlat.len() < 3 {
            continue;
        }
        let (mut sx, mut sy, mut n) = (0.0, 0.0, 0.0);
        for &(lon, lat) in &w.lonlat {
            sx += lon;
            sy += lat;
            n += 1.0;
        }
        pts.push((sx / n, sy / n));
    }
    pts
}

/// **How far past the legibility cap a window may be widened when the DATA is too thin
/// to fill it.** A multiple of [`TOWN_HALF_MAX_M`], not a second metre constant, so the
/// two cannot drift apart.
///
/// # Why legibility is allowed to lose this argument
///
/// MEASURED 2026-08-05, live korp-server, real focused 1900x1150 Xvfb window, RTX 4090
/// [discrete/Vulkan]: at camera 9.40 korp serves the **LOD-9 generalised** tile set and
/// its own status line says `CAPPED at LOD 9 — 40000 ways`. Forty thousand ways for a
/// 148 km viewport is one heavily decimated footprint per few hundred metres, so a 580 m
/// window over it caught **180 risen vertices and 269 triangles** — three enormous
/// simplified polygons and a lot of white. The 1500 m window it replaced caught 3 645.
///
/// Both pictures are wrong, and the wider one is wrong more usefully: a legibility bound
/// protects the reading of a scene that EXISTS, and at region zoom the scene does not.
/// So a window that cannot find [`MIN_BUILDINGS_IN_VIEW`] doubles until it can, up to
/// this bound. That is a deliberate ordering — a sparse-but-wide scene beats an empty
/// legible one — and it is stated here rather than discovered from a screenshot.
///
/// 4x is two doublings past the cap: 2.32 km half-window, 4.6 km of world, which is the
/// order of the pre-2026-08-05 behaviour and therefore known not to fall over.
pub const SPARSE_WIDEN_MAX_FACTOR: f64 = 4.0;

/// Widen `half_m` (doubling) until the window holds [`MIN_BUILDINGS_IN_VIEW`] centroids
/// or hits [`SPARSE_WIDEN_MAX_FACTOR`]x [`TOWN_HALF_MAX_M`]. Returns the window it
/// settled on and its population, or the widest tried when even that is empty.
///
/// **Used by the DENSITY fallback only, deliberately.** The camera branch refuses
/// instead of widening ([`TownChoice::TooCoarse`]), because widening there was measured
/// to need a 40 km scene. The vote is a different case: it picked the HEAVIEST cell, so
/// it is looking at buildings by construction, and the only open question is whether the
/// legible window is wide enough to hold enough of them.
fn grow_to_population(pts: &[(f64, f64)], lon: f64, lat: f64, start_half_m: f64) -> (f64, usize) {
    let ceiling = SPARSE_WIDEN_MAX_FACTOR * TOWN_HALF_MAX_M;
    let mut half_m = start_half_m.min(ceiling);
    loop {
        let n = buildings_in_window(pts, lon, lat, half_m);
        if n >= MIN_BUILDINGS_IN_VIEW || half_m >= ceiling {
            return (half_m, n);
        }
        half_m = (half_m * 2.0).min(ceiling);
    }
}

/// Count the centroids inside a `(lon, lat)` window centred on `(lon, lat)` with
/// half-size `half_m` metres.
fn buildings_in_window(pts: &[(f64, f64)], lon: f64, lat: f64, half_m: f64) -> usize {
    let (wlon, wlat) = (half_m / m_per_deg_lon(lat), half_m / M_PER_DEG_LAT);
    pts.iter()
        .filter(|&&(x, y)| (x - lon).abs() <= wlon && (y - lat).abs() <= wlat)
        .count()
}

/// **Choose the town window: the camera first, the densest cluster only when the camera
/// cannot answer.** The one writer (L5) behind every "which town gets extruded?"
/// question; [`densest_cluster_subset`] and [`view_town_subset`] are both this plus the
/// clipping.
///
/// # The half-window follows the zoom — see [`town_half_for_span`] for how, and why the
/// obvious spelling could not work
///
/// # Why the camera wins
///
/// Density picks the cell with the most footprints, and that cell is a suburb — a
/// dormitory grid of small houses out-counts an inner city of large blocks. The owner
/// panned to Stockholm, switched to 3D and got **Sigtuna**. The camera is not a better
/// heuristic than density, it is the *answer to a different and correct question*: the
/// user already told us where to look.
///
/// Returns `None` when there is nothing to window (no buildings at all).
#[must_use]
pub fn town_window(ways: &[Way], view: Option<ViewWindow>) -> Option<TownWindow> {
    match choose_town(ways, view) {
        TownChoice::Chosen(w) => Some(w),
        TownChoice::TooCoarse(_) | TownChoice::Nothing => None,
    }
}

/// **The whole decision, including the one answer [`town_window`] cannot express: that
/// the view is too coarse to choose at all.**
///
/// `town_window` returns `Option<TownWindow>`, so "no camera, so the vote decided",
/// "nothing here to window" and "the camera asked and the data could not answer" all
/// collapse into `Some(densest)`/`None`. The third case is a different thing, and at
/// region zoom it is the common one — see [`TownChoice::TooCoarse`].
#[must_use]
pub fn choose_town(ways: &[Way], view: Option<ViewWindow>) -> TownChoice {
    let pts = building_centroids(ways);
    // **THE CAMERA IS ASKED FIRST, EVEN WHEN THERE ARE NO BUILDINGS AT ALL.**
    //
    // `if pts.is_empty() { return Nothing }` used to stand above this block, and that one
    // line is three of the numbers in the owner's 2026-08-06 log. `Nothing` means *the
    // caller keeps everything* ([`view_town_subset`]), so a viewport whose reply happened
    // to carry no building rows handed the extruder its ways UNCLIPPED — and a korp reply
    // returns a way WHOLE whenever any of its vertices is in the box, so 86 ways of
    // coastline and lake rings out of a **1 827 m** viewport spanned **77 km**:
    //
    // ```text
    // rebuild: src=null half_m=null m/du=48232.0 tris=813626 risen=36672 … scene_ways=86 aim_offset_m=30057 aim_ratio=16.45
    // ```
    //
    // `m/du = 48 232` (an 8 m house = 1.7e-4 design units), a scene centre **30 km** from
    // the camera, 813 626 triangles out of 86 ways. All three are the same discarded
    // window: the camera was supplied and never consulted, because the buildings were
    // counted first.
    //
    // Zero buildings under an aimed camera is a REFUSAL, not an absence of information —
    // it is the same statement `buildings < MIN_BUILDINGS_IN_VIEW` already makes, at the
    // limit, and it already has an honest answer that bounds the scene (`TooCoarse` ⇒
    // extrude nothing ⇒ the pane's "zoom in" state). `Nothing` survives for the callers
    // that genuinely have no camera — the fixtures and the standalone demos — which is
    // what its doc always claimed it was for.
    if let Some(v) = view && v.lon.is_finite() && v.lat.is_finite() && v.span_m.is_finite() && v.span_m > 0.0 {
        // NOT widened. `grow_to_population` exists and the density fallback below
        // uses it, but the refusal a few lines down measured what widening would
        // cost HERE: the capped z9 sample first reaches MIN_BUILDINGS_IN_VIEW at
        // half_m = 20 000 m, 13.3x the readability cap. A camera looking at a place
        // the sampler did not deliver wants an honest refusal, not a 40 km pancake.
        let half_m = town_half_for_span(v.span_m);
        let buildings = buildings_in_window(&pts, v.lon, v.lat, half_m);
        if buildings >= MIN_BUILDINGS_IN_VIEW {
            return TownChoice::Chosen(TownWindow {
                source: WindowSource::Camera,
                lon: v.lon,
                lat: v.lat,
                half_m,
                buildings,
            });
        }
        // ── THE CAMERA ASKED AND THE DATA COULD NOT ANSWER ────────────────
        //
        // This used to fall through to the vote, and the vote always answers. On
        // oden's live pyramid at z9 that answer was a town **66 113 m** from where
        // the user was looking (ratio 0.415).
        //
        // MEASURED on the live sweden pyramid, camera on Stockholm at z9
        // (span 159 375 m, so the window clamps to 1500 m):
        //
        // | what                                        | buildings in the window |
        // |---------------------------------------------|-------------------------|
        // | what the selector SEES (budget-capped)      | **0**                   |
        // | the same z9 tile, uncapped                  | 155                     |
        // | the z15 pyramid over the same box           | **4 734**               |
        //
        // The viewport's whole capped sample holds 132 building centroids against
        // 88 441 in the same tiles uncapped — **0.149 %** — because a 40 000-way
        // budget is split over 15 covering tiles (2 857 rows for the camera's own
        // tile, 7.1 %) and the rows are stored `extent-desc`, i.e. the LARGEST
        // geometry first. So both fallbacks are properties of the sampler, not of
        // the landscape: weighing buildings alone lands in a suburb, weighing
        // everything lands in open water.
        //
        // That measurement rules out the two tempting repairs, on numbers:
        //
        //   * **Lower `MIN_BUILDINGS_IN_VIEW`** — impossible. The window holds
        //     ZERO. No threshold above 0 can be met, and 0 means "extrude an empty
        //     window".
        //   * **Widen the window** — the capped sample first reaches 24 buildings
        //     at `half_m = 20 000` (a 40 km scene): **13.3× `TOWN_HALF_MAX_M`**,
        //     the readability cap that exists to stop the pancake. That repairs the
        //     choice by destroying the scene.
        //
        // So it refuses, and the pane says why. A country overview with an empty 3D
        // pane and a "zoom in" line is honest; a confident city 66 km away is not.
        //
        // NOT the upstream fix: the 3 km box is STARVED by the per-tile budget
        // split (`korp::map_tiles::fair_take`). Giving the camera's own tile more
        // than an equal share is the real repair, and it lives in korp.
        // **BEFORE REFUSING: SLIDE THE WINDOW, INSIDE THE VIEWPORT.**
        //
        // Everything argued above is about a COUNTRY overview, where the window holds
        // zero because there is nothing within tens of kilometres and the vote would
        // answer about somewhere the user is not looking. At city zoom the same branch
        // fires for a completely different reason: the buildings are ON SCREEN and merely
        // not under the crosshair.
        //
        // MEASURED 2026-08-08 on the owner's live session: camera span **4488 m**, **940
        // buildings in the viewport**, `half_m` **250 m**, and that centre 250 m held
        // **zero**. He got `0 resta, 2 trianglar` and a flat pane over a city he could
        // see, and the refusal was working exactly as written.
        //
        // So the window slides to the densest cluster that is still INSIDE the camera's
        // own viewport. That is not the vote the comment above rules out: every candidate
        // is within the box the operator has on screen, so the worst outcome is the wrong
        // block of the street already in front of them — never Sigtuna, never 66 km. If
        // nothing inside the viewport reaches the threshold either, the refusal stands and
        // the pane still says why.
        if let Some(slid) = densest_within(&pts, &v, half_m) {
            return TownChoice::Chosen(slid);
        }
        return TownChoice::TooCoarse(TownRefusal {
            half_m,
            buildings,
            needed: MIN_BUILDINGS_IN_VIEW,
            span_m: v.span_m,
        });
    }
    // ── fallback: the densest cell of building footprints ─────────────────────
    //
    // **Buildings alone vote here, and only here.** The open question was whether the
    // cell vote should weigh all geometry or only `building`: weighing everything drags
    // Stockholm's answer into the water (Riddarfjärden is a lot of vertices), weighing
    // buildings alone lands in the suburb. Both are wrong, which is the tell that the
    // vote was never the right instrument — it is now reached only when NO camera can
    // answer, i.e. a country overview, where "the densest concentration of houses" is
    // exactly the right thing to mean and water is exactly the wrong thing to mean.
    if pts.len() < 50 {
        return TownChoice::Nothing; // too sparse to cluster → the caller keeps everything
    }
    let (mut lon0, mut lat0, mut lon1, mut lat1) = (f64::MAX, f64::MAX, f64::MIN, f64::MIN);
    for &(lon, lat) in &pts {
        lon0 = lon0.min(lon);
        lat0 = lat0.min(lat);
        lon1 = lon1.max(lon);
        lat1 = lat1.max(lat);
    }
    let lat_mid = (lat0 + lat1) * 0.5;
    let m_per_deg_lon = m_per_deg_lon(lat_mid);
    let half_m = TOWN_HALF_MAX_M;
    // Bin building centroids on a ~half-window grid; pick the heaviest cell as the town.
    let cell_deg_lat = half_m / M_PER_DEG_LAT;
    let cell_deg_lon = half_m / m_per_deg_lon;
    let nx = (((lon1 - lon0) / cell_deg_lon).ceil() as usize + 1).max(1);
    let ny = (((lat1 - lat0) / cell_deg_lat).ceil() as usize + 1).max(1);
    let cx = |lon: f64| (((lon - lon0) / cell_deg_lon) as usize).min(nx - 1);
    let cy = |lat: f64| (((lat - lat0) / cell_deg_lat) as usize).min(ny - 1);
    let mut bins = vec![0u32; nx * ny];
    for &(lon, lat) in &pts {
        bins[cy(lat) * nx + cx(lon)] += 1;
    }
    let best = bins.iter().enumerate().max_by_key(|(_, c)| **c).map(|(i, _)| i).unwrap_or(0);
    let (bcy, bcx) = (best / nx, best % nx);
    let town_lon = lon0 + (bcx as f64 + 0.5) * cell_deg_lon;
    let town_lat = lat0 + (bcy as f64 + 0.5) * cell_deg_lat;
    // The sparsity rule, on THIS branch only. The camera branch does NOT widen, and the
    // refusal above says why on numbers: the capped z9 sample first reaches
    // MIN_BUILDINGS_IN_VIEW at half_m = 20 000 m, 13.3x the readability cap. The density
    // vote is a different case — it picked the HEAVIEST cell, so it is looking at
    // buildings by construction, and the only question is whether the legible window is
    // wide enough to hold enough of them.
    let (half_m, buildings) = grow_to_population(&pts, town_lon, town_lat, half_m);
    TownChoice::Chosen(TownWindow { source: WindowSource::Densest, lon: town_lon, lat: town_lat, half_m, buildings })
}

/// **The densest window of `half_m` that is still inside the camera's viewport** — the slide
/// [`choose_town`] tries before it refuses. `None` when no placement inside the viewport
/// reaches [`MIN_BUILDINGS_IN_VIEW`], which is the honest "there is nothing here to extrude".
///
/// Bounded by construction: every candidate centre is a building centroid that is itself
/// inside the viewport, so the answer cannot wander off screen. That bound is the whole
/// difference between this and the unbounded densest-cluster vote, which
/// [`WindowSource::Densest`] documents and `choose_town` keeps for the no-camera case.
fn densest_within(pts: &[(f64, f64)], v: &ViewWindow, half_m: f64) -> Option<TownWindow> {
    // The reach is the WIDE anchor — the widest viewport that is still one legible place.
    //
    // It started at `4 x DEEP` (~5 km) and that was too tight to be useful: the owner could
    // navigate Sweden in 2D, press 3D at city or region scale, and the slide would decline
    // because the view was "too wide", so korp had to FLY the camera down to ~2.5 km instead
    // — *"i only see a stockholm? cant navigate on sweden?"*. Sliding is the repair that does
    // not move him; flying is the one that does, so the slide reaches as far as it honestly
    // can and the flight is the last resort.
    //
    // 37 km is where facett's own town window reaches its cap, i.e. the widest span at which
    // "the buildings are on screen and merely not under the crosshair" is still a true
    // sentence. Beyond it the two country-overview guards (`a_country_overview_refuses_…` at
    // 900 km and `a_region_view_over_a_capped_sample_…` at 159 km) hold the refusal shut,
    // which is where it belongs — and both go red if this bound is removed.
    if !(v.span_m.is_finite() && v.span_m <= TOWN_ZOOM_WIDE_SPAN_M) {
        return None;
    }
    let mpdl = m_per_deg_lon(v.lat);
    let half_view_lat = (v.span_m * 0.5) / M_PER_DEG_LAT;
    let half_view_lon = (v.span_m * 0.5) / mpdl;
    let inside: Vec<(f64, f64)> = pts
        .iter()
        .copied()
        .filter(|(lon, lat)| {
            (lon - v.lon).abs() <= half_view_lon && (lat - v.lat).abs() <= half_view_lat
        })
        .collect();
    if inside.len() < MIN_BUILDINGS_IN_VIEW {
        return None;
    }
    let (win_lat, win_lon) = (half_m / M_PER_DEG_LAT, half_m / mpdl);
    // Every candidate centre is a building that is itself on screen, so the window always
    // lands on something real. O(n^2) over the buildings in ONE viewport, which is the
    // hundreds — not the country-scale set the cell vote below exists to bucket.
    let mut best: Option<(usize, f64, f64)> = None;
    for &(clon, clat) in &inside {
        let n = inside
            .iter()
            .filter(|(lon, lat)| (lon - clon).abs() <= win_lon && (lat - clat).abs() <= win_lat)
            .count();
        if best.is_none_or(|(bn, _, _)| n > bn) {
            best = Some((n, clon, clat));
        }
    }
    let (n, lon, lat) = best?;
    (n >= MIN_BUILDINGS_IN_VIEW).then_some(TownWindow {
        source: WindowSource::CameraSlid,
        lon,
        lat,
        half_m,
        buildings: n,
    })
}

/// Select the **densest cluster of buildings** out of a country-/region-scale
/// `ways` set, returning a city-sized subset (the buildings around the busiest
/// [`TOWN_HALF_MAX_M`]-metre window, plus the roads / water / parks that fall inside it).
///
/// The camera-blind spelling of [`view_town_subset`] — kept for the callers that
/// genuinely have no camera (the synthetic fixtures, the standalone demos). A host that
/// HAS a map camera must pass it: choosing by density when the user has already aimed
/// the map is the Sigtuna defect.
pub fn densest_cluster_subset(ways: &[Way]) -> Cow<'_, [Way]> {
    view_town_subset(ways, None)
}

/// **Subset a country-/region-scale `ways` set to the one town the user is looking at**,
/// then hand that to the extruder.
///
/// Why subset at all: extruding a 30 km country and then dollying the *camera* into a
/// 2 km town leaves a giant terrain plane + thousands of far footprints in view — their
/// far edges project to near-singularity **spokes** (the starburst), and the buildings
/// are sub-pixel. Instead we **subset the data** to one town and extrude THAT: the mesh
/// is then city-scale, so heights scale to the town span (a readable skyline), the
/// terrain is town-sized (no far spokes), and a normal camera frames it.
///
/// Which town is [`town_window`]'s decision — the camera's, falling back to the densest
/// cluster. Returns the input unchanged when it is already small (a single town / the
/// synthetic fixture) or has too few buildings to window.
pub fn view_town_subset(ways: &[Way], view: Option<ViewWindow>) -> Cow<'_, [Way]> {
    let win = match choose_town(ways, view) {
        TownChoice::Chosen(w) => w,
        // **NO CAMERA.** Keeping everything is safe only because nothing aimed anywhere:
        // a fixture or a standalone demo hands its whole (town-sized) input in. With a
        // camera supplied this arm is UNREACHABLE by construction — `choose_town` answers
        // `TooCoarse` for every window it cannot fill, zero buildings included — and that
        // is deliberate: this arm used to be reached with a camera in hand, and then
        // "keep everything" meant a 1 827 m viewport extruding a 77 km scene. See
        // `choose_town`.
        TownChoice::Nothing => return Cow::Borrowed(ways), // no camera, and nothing to window
        // **REFUSED — and refusing means extruding NOTHING, not extruding everything.**
        // Returning `ways` here would hand the whole 160 km region to the extruder, i.e.
        // the pancake this subset exists to prevent. The empty set is what makes the
        // pane paint its "zoom in" state; see `TownChoice::TooCoarse`.
        TownChoice::TooCoarse(_) => return Cow::Owned(Vec::new()),
    };
    // Already smaller than the window we would cut → nothing to subset.
    //
    // Decided on the span of the WHOLE `ways` set, not on the span of the building
    // centroids. The two are not the same question, and using the wrong one is a
    // silent no-op: a compact town sitting inside a region viewport has a ~1 km centroid
    // span, so this returned "already a town" — and returned the entire region with it,
    // roads, water and all. The scene scale is derived from every retained vertex, so
    // that is a 55 km scene wearing a 1 km town's answer, and the subset never ran at
    // all. Ask the quantity this function exists to bound.
    let scene_span_m = world_span_metres(ways).map(f64::from).unwrap_or(0.0);
    if scene_span_m <= 2.0 * win.half_m {
        return Cow::Borrowed(ways);
    }
    let keep = clip_ways_to_window(ways, win.lon, win.lat, win.half_m);
    // **Selection is not ownership.** All three "keep everything" exits hand the input
    // straight back — they used to `to_vec()` it, and `Way` is the most expensive type
    // in the house to clone (`points` + `lonlat` + `holes` + `name`, three allocations
    // and 24 B per vertex). The last exit was the worst of them: it built `keep` in
    // full, threw it away, and then deep-cloned the whole input a second time, so one
    // call could copy the set twice over.
    if keep.len() < 10 { Cow::Borrowed(ways) } else { Cow::Owned(keep) }
}

/// **Clip a ways set to a `(lon, lat) ± half_m` window, so every surviving vertex is
/// inside it.** The one writer (L5) behind both windowing paths: the legibility-capped
/// town window ([`view_town_subset`]) and the whole-viewport continuous scene
/// ([`viewport_scene`]) cut geometry by exactly the same rules, and a change to those
/// rules cannot land in one and miss the other.
///
/// Returns the kept ways with no population floor and no "already small" shortcut —
/// those are the CALLER's policy, and `view_town_subset` still applies its own.
fn clip_ways_to_window(ways: &[Way], lon: f64, lat: f64, half_m: f64) -> Vec<Way> {
    let (wlon, wlat) = (half_m / m_per_deg_lon(lat), half_m / M_PER_DEG_LAT);
    let (klon0, klon1) = (lon - wlon, lon + wlon);
    let (klat0, klat1) = (lat - wlat, lat + wlat);
    // ── keep the window, and ONLY the window ──────────────────────────────────
    //
    // This used to keep every way with ANY vertex inside the window, whole. That
    // silently defeated the entire subset, because the SCENE SCALE is derived from the
    // retained geometry: `world_span_metres` and `footprint_bbox` read every vertex of
    // every kept way, so one motorway segment reaching out of the window dragged the
    // data bbox back to region size and the design unit with it.
    //
    // MEASURED on the shipped korp path, a real focused 1900x1150 Xvfb window against
    // the live korp-server (2026-08-03, RTX 4090, Stockholm z12): with the subset in
    // place the mesh dropped from 2 802 173 triangles to 116 465 — it HAD found the
    // town — and `metres_per_design_unit` still came back **33 195**, a design unit of
    // 33 km on a 3 km window. The median house stood 0.43 px and the pane drew the same
    // flat pancake as before the subset existed.
    //
    // So the window has to bound the GEOMETRY, not just choose it:
    //   * a way entirely inside is kept as it is;
    //   * a LINE crossing the border keeps its longest run of inside vertices, so a
    //     street through the town is still a street — it just stops within one segment
    //     of the border (tens of metres at z12) instead of running to the horizon;
    //   * a RING (a building, a lake, a landuse cap) is only meaningful whole, so it is
    //     kept whole when it fits the window and dropped when it does not. A lake wider
    //     than the town is flat ground cover; keeping it would put the scene scale back
    //     where it started, which is the entire defect.
    //
    // Every vertex that survives is inside the window by construction, so the span of
    // the returned set is bounded by the window — the design unit can no longer be a
    // hostage to whatever the tiler happened to include.
    let mut keep: Vec<Way> = Vec::with_capacity(ways.len() / 8);
    for w in ways {
        if let Some(cut) = clip_way_to_window(w, (klon0, klat0, klon1, klat1)) {
            keep.push(cut);
        }
    }
    keep
}

/// **[`clip_ways_to_window`] for ONE way**, against a `(lon0, lat0, lon1, lat1)` box —
/// the per-way rule, split out so the continuous-viewport walk can clip **lazily**, in
/// distance order, and clone only what it is going to keep. A dense z12 viewport drops
/// tens of thousands of far buildings; cloning them first and discarding them after is
/// the allocation the LAW's "preallocate and reuse" clause exists to forbid.
///
/// `None` when nothing of the way is inside.
fn clip_way_to_window(w: &Way, win: (f64, f64, f64, f64)) -> Option<Way> {
    let (klon0, klat0, klon1, klat1) = win;
    let inside = |&(lon, lat): &(f64, f64)| lon >= klon0 && lon <= klon1 && lat >= klat0 && lat <= klat1;
    if w.lonlat.is_empty() || !w.lonlat.iter().any(inside) {
        return None;
    }
    if w.lonlat.iter().all(inside) {
        return Some(w.clone());
    }
    // Straddles the border. A closed / filled ring survives only if it fits.
    let closed = w.fill.is_some()
        || (w.lonlat.len() > 2 && {
            let (a, b) = (w.lonlat[0], w.lonlat[w.lonlat.len() - 1]);
            (a.0 - b.0).abs() < 1e-9 && (a.1 - b.1).abs() < 1e-9
        });
    if closed {
        return None; // bigger than the window ⇒ it would re-set the scene scale
    }
    // A line: the longest piece of it that lies inside the window, cut at the
    // border rather than at the last inside VERTEX. Real tiled geometry is coarse —
    // a trunk road can cross a 3 km town in a single segment with no vertex inside
    // it at all — so a vertex-only clip deletes exactly the streets a town view
    // needs and leaves a hole where the main road was.
    let run = clip_polyline_to_window(&w.lonlat, win)?;
    Some(w.clone().with_lonlat(run))
}

/// The **longest contiguous piece of a `(lon, lat)` polyline that lies inside an
/// axis-aligned window**, with the crossings interpolated onto the border — or `None`
/// when none of it does.
///
/// Every returned vertex is inside the window by construction, which is the property
/// [`densest_cluster_subset`] needs: the scene's design unit is derived from the
/// retained vertices, so one vertex left outside is enough to put the scale back at
/// region scale.
fn clip_polyline_to_window(pts: &[(f64, f64)], win: (f64, f64, f64, f64)) -> Option<Vec<(f64, f64)>> {
    let same = |a: (f64, f64), b: (f64, f64)| (a.0 - b.0).abs() < 1e-12 && (a.1 - b.1).abs() < 1e-12;
    let mut runs: Vec<Vec<(f64, f64)>> = Vec::new();
    let mut cur: Vec<(f64, f64)> = Vec::new();
    for seg in pts.windows(2) {
        match clip_segment_to_window(seg[0], seg[1], win) {
            Some((pa, pb)) => {
                match cur.last() {
                    // Continues the run we are building.
                    Some(&last) if same(last, pa) => {}
                    // A new piece: the polyline left the window and came back.
                    Some(_) => {
                        if cur.len() >= 2 {
                            runs.push(std::mem::take(&mut cur));
                        } else {
                            cur.clear();
                        }
                        cur.push(pa);
                    }
                    None => cur.push(pa),
                }
                if !same(pa, pb) {
                    cur.push(pb);
                }
            }
            None => {
                if cur.len() >= 2 {
                    runs.push(std::mem::take(&mut cur));
                } else {
                    cur.clear();
                }
            }
        }
    }
    if cur.len() >= 2 {
        runs.push(cur);
    }
    runs.into_iter().max_by_key(Vec::len)
}

/// Liang–Barsky: the portion of the segment `a → b` inside `win`, or `None`.
fn clip_segment_to_window(
    a: (f64, f64),
    b: (f64, f64),
    (x0, y0, x1, y1): (f64, f64, f64, f64),
) -> Option<((f64, f64), (f64, f64))> {
    let (dx, dy) = (b.0 - a.0, b.1 - a.1);
    let (mut t0, mut t1) = (0.0f64, 1.0f64);
    for &(p, q) in &[(-dx, a.0 - x0), (dx, x1 - a.0), (-dy, a.1 - y0), (dy, y1 - a.1)] {
        if p == 0.0 {
            if q < 0.0 {
                return None; // parallel to this edge and wholly outside it
            }
        } else {
            let r = q / p;
            if p < 0.0 {
                if r > t1 {
                    return None;
                }
                if r > t0 {
                    t0 = r;
                }
            } else {
                if r < t0 {
                    return None;
                }
                if r < t1 {
                    t1 = r;
                }
            }
        }
    }
    Some(((a.0 + dx * t0, a.1 + dy * t0), (a.0 + dx * t1, a.1 + dy * t1)))
}

// ═════════════════════════════════════════════════════════════════════════════
//  THE CONTINUOUS VIEWPORT SCENE — the whole viewport, with distance falloff
// ═════════════════════════════════════════════════════════════════════════════
//
// [`view_town_subset`] answers "which ONE town do I extrude?" and its answer is a box
// of at most `2·TOWN_HALF_MAX_M` = 1.16 km. That box is the right answer to the question
// it asks, and the wrong shape on screen: however wide the 2D viewport is, the 3D pane
// shows a 1.16 km rectangle of it, floating in nothing, and out in open country it shows
// a sentence on an empty pane. The owner, driving it: *"why is 3d only a tile / rectangle
// rotating? you need to make it continously but cut grafiks at a distant length"* and
// *"3d i want to navigate throughout sweden!"*.
//
// So the question below is a DIFFERENT one — "what is in the viewport, and how much of
// it can this frame afford?" — and it is a separate entry point rather than a change of
// meaning under [`town_half_for_span`], which has consumers
// (`facett-map3d-flight`, the demos, the fixtures) that asked the first question and
// must keep getting the first answer.

/// **How much of a feature survives at its distance from the camera's aim point.**
///
/// Three levels, not a continuum, because the extruder's cost is a step function of the
/// way's shape and nothing in between is cheaper: a prism costs `4n` triangles in its
/// ring length, so the only real lever is the ring, and the only rings worth having are
/// "the real one" and "a box".
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum Detail {
    /// The way exactly as the tiler delivered it — a real footprint with a real roof.
    Full,
    /// **Simplified.** A risen ring collapses to its footprint **bounding box** (4
    /// corners, so `4n` becomes 18 triangles however wrinkly the original was); a line
    /// keeps at most [`COARSE_LINE_VERTS`] vertices. Still a building, still a street,
    /// at the size where nobody can count its corners.
    Coarse,
    /// **The ground only.** Risen rings are gone; a line survives as a single straight
    /// segment (a road still reads as a road at 3 km); flat ground cover — water, park,
    /// landuse — is kept as it is, because it costs `n-2` triangles and it IS the
    /// ground. This band is what makes the scene continuous instead of a rectangle: it
    /// runs all the way to the viewport border.
    Ground,
}

/// How many vertices a line keeps at [`Detail::Coarse`]. Eight is two corners more than
/// a city block needs at that distance, and it bounds a road's cost at
/// `3 · (10·8 − 2)` = 234 triangles regardless of how finely the tiler cut it.
pub const COARSE_LINE_VERTS: usize = 8;

/// **What a road becomes in the [`Detail::Ground`] band** — a plain stroked line instead
/// of the three-pass car-navigator ribbon.
///
/// # Why the falloff had to reach the ribbon and not only the vertices
///
/// [`road_ribbon`] draws glow + casing + fill, three [`ribbon_rounded`] passes with an
/// eight-segment join fan at every vertex, so even a road decimated to its two ENDS costs
/// `3 · (10·2 − 2)` = **54 triangles**. `viewport_scene`'s ground band is unconditional —
/// it is what makes the pane continuous out to the horizon — and a real city viewport
/// holds ~15 000 line ways. MEASURED on `tests/the_budget_is_enforced.rs`'s reconstruction
/// of the owner's 2026-08-11 drive (32 761 houses, 14 700 street ways, 2 975 cover rings in
/// a 6.2 km viewport): the lines alone cost **1 192 194** of a **1 357 801**-triangle
/// scene against a 600 000 budget. Decimating them further cannot help — they were
/// already at two vertices. The cost is in the PASSES.
///
/// The casing and the glow are near-field styling: at the ground band a street is a
/// hairline (`MIN_ROAD_HALF_W` has already taken over the width), and a 1.7x casing plus a
/// 2.8x glow around a one-pixel line is three ribbons drawing the same pixel. So a
/// ground-band road keeps its class colour and its width and loses the two passes nobody
/// can resolve: **54 → 18 triangles**, a 3x cut in the one term that dominates.
///
/// It is a `kind` rather than a flag on [`Way`] because `kind` is what
/// [`estimated_triangles`] and [`line_feature`] BOTH branch on — one string, so the
/// estimate the budget spends and the mesh the extruder emits cannot disagree about how
/// many passes a road got (`the_estimate_is_the_mesh` asserts that to 2 %). `style::classify`
/// never produces it, so a way carrying it can only have come from this crate's own falloff.
pub const GROUND_ROAD_KIND: &str = "road-far";

/// The number of vertices [`open_ring`] would leave, without building the ring — the
/// closing duplicate dropped, nothing allocated. Used by [`ground_floor_triangles`], which
/// runs once per candidate way on the interaction path (LAW: preallocate and reuse, and
/// do not allocate to count).
fn open_ring_len(ring: &[(f32, f32)]) -> usize {
    let mut n = ring.len();
    while n >= 2 {
        let (a, b) = (ring[0], ring[n - 1]);
        if (a.0 - b.0).abs() < 1e-9 && (a.1 - b.1).abs() < 1e-9 {
            n -= 1;
        } else {
            break;
        }
    }
    n
}

/// **What this way costs even if the scene degrades as far as it can** — its
/// [`Detail::Ground`] price, which for the unconditional half of the scene is a price the
/// budget cannot refuse to pay.
///
/// * a building: **0** — past the coarse band it is dropped, and that is the falloff;
/// * a non-cover ring (an untagged area, a boundary): **0** — [`into_detail`] drops it too;
/// * flat cover (water / park / coastline): its cap, `n − 2`. Never reduced, never
///   dropped — it IS the ground;
/// * a line: its two ends, one pass — `18` triangles ([`GROUND_ROAD_KIND`]);
/// * **except a line that was ALREADY at two vertices**: [`into_detail`] short-circuits on
///   `lonlat.len() < 3` and hands such a way back untouched — no decimation, and therefore
///   no demotion to [`GROUND_ROAD_KIND`] either. A two-node road (OSM is full of them: a
///   driveway, a service spur, a bridge segment) keeps all three ribbon passes and costs
///   **54**. Saying 18 for it would under-reserve the floor by 36 apiece and let the walk
///   this number exists to bound overshoot again, quietly.
///
/// Summed over the candidates this is the **floor**: the smallest scene
/// [`viewport_scene`] is allowed to produce while still honouring "never blank the pane".
/// A budget below it is a budget that cannot be met, and saying which of the two happened
/// is the difference between a bound and a caption.
///
/// It is a closed form rather than `estimated_triangles(&at_detail(w, Ground)?, …)` because
/// it runs once per candidate way on the interaction path and that spelling allocates a
/// reduced `Way` per call (LAW: preallocate and reuse, do not allocate to count).
/// `the_floor_is_exactly_what_the_ground_band_costs` pins the two spellings equal on every
/// way of the fixture, so the shortcut cannot drift from the walk it is predicting.
fn ground_floor_triangles(w: &Way) -> usize {
    if w.points.len() < 2 {
        return 0;
    }
    if is_prism(w) {
        if matches!(w.kind, "water" | "park" | "coastline") {
            return open_ring_len(&w.points).saturating_sub(2);
        }
        return 0;
    }
    // The branch `into_detail` takes, read off the same field it reads.
    let passes = if w.lonlat.len() < 3 && w.kind == "road" { 3 } else { 1 };
    passes * (10 * 2 - 2)
}

/// **The triangle budget for one continuous viewport scene.**
///
/// # Measured, not chosen
///
/// Three readings bracket it, all on the same scene the pane actually builds:
///
/// * korp MEASURED **2 802 173** triangles for a real un-subsetted Stockholm z12
///   viewport, and **116 465** for the town window that replaced it (recorded in
///   [`view_town_subset`]). Those are the two ends: everything, and what ships today.
/// * This crate's extruder emits **4.0 – 7.8 M triangles/s** in a DEBUG build on
///   The owner's t14s (AMD 890M / RADV STRIX1, `tests/continuous_viewport::throughput`,
///   1 610 – 14 430-way towns). The mesh is rebuilt on every camera change, so the
///   extrusion is on the interaction path, not the load path.
/// * The mesh costs a MEASURED **67 – 71 bytes per triangle** (positions + normals +
///   indices + a colour + the building class, after the caps' shared vertices), so it is
///   allocated, filled and freed at that rate too.
///
/// At this budget the scene is **~42 MB** and **~100 ms** of DEBUG extrusion — 5.1x the
/// geometry korp ships today and 21 % of the whole viewport. The release figure is
/// necessarily smaller and is NOT stated here, because this session was not allowed to
/// run a release build (the main session owns it and the cargo target dir is shared);
/// the debug number is the one that was taken.
///
/// It is a [`SceneBudget`] field rather than a hard constant precisely because it is a
/// hardware number: a host on a weaker GPU lowers it and the scene degrades by getting
/// shallower, which is the entire point of budgeting by triangles instead of by metres.
pub const VIEWPORT_TRIANGLE_BUDGET: usize = 600_000;

/// The share of [`SceneBudget::triangles`] spent before full-detail buildings give way
/// to [`Detail::Coarse`] boxes. The remainder buys the coarse band, which is where the
/// triangles go furthest: a box is 18 triangles and reads as a building for kilometres.
pub const FULL_DETAIL_SHARE: f32 = 0.55;

/// **What one viewport scene may cost.** Passed by the host, because the answer is a
/// property of the host's GPU and of nothing in this crate.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct SceneBudget {
    /// Total estimated triangles the scene may reach ([`VIEWPORT_TRIANGLE_BUDGET`]).
    pub triangles: usize,
    /// Fraction of `triangles` spent at [`Detail::Full`] ([`FULL_DETAIL_SHARE`]).
    pub full_detail_share: f32,
}

impl Default for SceneBudget {
    fn default() -> Self {
        Self { triangles: VIEWPORT_TRIANGLE_BUDGET, full_detail_share: FULL_DETAIL_SHARE }
    }
}

/// **What the falloff walk did** — every number the pane, the log and a test need, so
/// "the 3D scene is continuous and fades" is a reading rather than a screenshot.
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct ViewportStats {
    /// The viewport span the scene was cut from (metres) — the 2D pane's own width.
    pub span_m: f64,
    /// Estimated triangles the kept ways will extrude to ([`estimated_triangles`]).
    pub triangles: usize,
    /// The budget it was cut to.
    pub budget: usize,
    /// **The smallest this scene could possibly have been** — the sum of
    /// [`ground_floor_triangles`] over every candidate way: flat cover kept, every line
    /// down to its two ends and one pass, every building dropped. It is the price of the
    /// "never blank the pane" guarantee and the falloff cannot go below it.
    ///
    /// The bands are chosen inside `budget − ground_floor`, which is what makes
    /// [`Self::triangles`] a bound rather than a caption.
    pub ground_floor: usize,
    /// **Did the scene actually fit?** `triangles <= budget`.
    ///
    /// `false` has exactly two causes and both are honest rather than a miss:
    /// `ground_floor > budget` (the unconditional ground alone costs more than the host
    /// allowed — the falloff has nowhere left to go), or the
    /// [`MIN_BUILDINGS_IN_VIEW`] near-band floor holding `Detail::Full` open past the
    /// budget so the pane still has a skyline. A host that prints
    /// [`Self::triangles`] beside [`Self::budget`] must print this too; an over-budget
    /// number presented as if it were fine is worse than no bound at all.
    pub budget_met: bool,
    /// Buildings kept at [`Detail::Full`] / [`Detail::Coarse`], and dropped.
    pub buildings_full: usize,
    pub buildings_coarse: usize,
    pub buildings_dropped: usize,
    /// Lines (roads, paths, coastlines) kept at each level.
    pub lines_full: usize,
    pub lines_coarse: usize,
    pub lines_ground: usize,
    /// Flat ground cover kept — water, park, landuse. Never dropped.
    pub ground_cover: usize,
    /// **How far full detail reached**, in metres from the aim point. `f64::INFINITY`
    /// when the budget was never exhausted (the whole viewport is full detail).
    pub full_radius_m: f64,
    /// How far the coarse band reached; past this only [`Detail::Ground`] survives.
    pub coarse_radius_m: f64,
    /// **Where the scene ends**, in metres from the aim point: [`scene_reach_half_m`].
    /// From z14 down this IS half the viewport span and the scene is edge to edge; above
    /// it, it is the horizon the design box can hold — see that function.
    pub ground_radius_m: f64,
    /// Did the reach cover the WHOLE viewport (`ground_radius_m == span_m/2`)? The one
    /// bit that says whether the 3D pane is showing everything the 2D pane is.
    pub reach_is_the_viewport: bool,
    /// **How much ground the camera was asked to frame**, in metres
    /// ([`legible_frame_extent_m`]) — a pure function of the viewport span.
    pub frame_extent_m: f64,
    /// **How far the eye ended up from the aim point, in metres — the number that MOVES
    /// when the operator zooms on this path.**
    ///
    /// # Why this had to be published, and what it replaced
    ///
    /// On the town path the zoom was expressed in the DATA: `view_town_subset` clipped the
    /// geometry to [`town_half_for_span`], the design box was fitted to what survived, and
    /// so `metres_per_design_unit` tracked the zoom. korp's
    /// `the_zoom_slider_moves_the_3d_scene` asserts exactly that, and it is the right
    /// assertion about that mechanism.
    ///
    /// This path does not use that mechanism. The window is the reach (kilometres, not
    /// hundreds of metres), so a town smaller than the reach is never clipped and the
    /// design box does not move: MEASURED on korp's own 892 m fixture, `m/du` is **557.5
    /// at z12 and 557.5 at z15**, because the reach is 8 292 m and 2 315 m and the town
    /// fits inside both. The zoom is expressed in the CAMERA instead — same fixture, this
    /// number reads **622 m → 271 m → 206 m** across z12 / z15 / z16, and the applied
    /// median house goes **10.78 → 16.67 → 18.69 px**.
    ///
    /// `m/du` still tracks the zoom wherever the reach really bites (a region-scale ways
    /// set: 10 365 → 2 894 m/du over z12 → z15). It is simply no longer the quantity that
    /// is guaranteed to.
    ///
    /// This is the APPLIED distance, after [`crate::camera::OrbitCamera::min_distance`]
    /// clamps it — not the request — so a scene where the dolly floor bites reports the
    /// floor rather than the wish.
    pub camera_distance_m: f64,
    /// **Where the near band actually is** — the centroid of the full-detail buildings,
    /// in `(lon, lat)`. The camera aims here, so an empty viewport centre slides the
    /// camera to the nearest houses instead of framing a field. `None` when the view
    /// holds no buildings at all, and then the camera aims at the viewport centre.
    pub aim: Option<(f64, f64)>,
}

impl ViewportStats {
    /// Count an unwindowed hand-back (the no-camera path) so the emit is never blank.
    fn ways_in(&mut self, ways: &[Way]) {
        for w in ways {
            if w.kind == "building" {
                self.buildings_full += 1;
            } else if is_prism(w) {
                self.ground_cover += 1;
            } else {
                self.lines_full += 1;
            }
            self.triangles += estimated_triangles(w, RoofStyle::Pitched);
        }
    }

    /// The readable-data emit (korp publishes this under `map.map3d.viewport`).
    #[must_use]
    pub fn to_json(&self) -> serde_json::Value {
        let km = |m: f64| if m.is_finite() { serde_json::json!(m / 1000.0) } else { serde_json::Value::Null };
        serde_json::json!({
            "span_km": self.span_m / 1000.0,
            "triangles_estimated": self.triangles,
            "budget": self.budget,
            "ground_floor": self.ground_floor,
            "budget_met": self.budget_met,
            "buildings_full": self.buildings_full,
            "buildings_coarse": self.buildings_coarse,
            "buildings_dropped": self.buildings_dropped,
            "lines_full": self.lines_full,
            "lines_coarse": self.lines_coarse,
            "lines_ground": self.lines_ground,
            "ground_cover": self.ground_cover,
            "full_radius_km": km(self.full_radius_m),
            "coarse_radius_km": km(self.coarse_radius_m),
            "ground_radius_km": km(self.ground_radius_m),
            "reach_is_the_viewport": self.reach_is_the_viewport,
            // **The zoom observable on this path** — see `camera_distance_m`. `m/du` is
            // the scale of the DESIGN BOX and no longer the scale of the view.
            "frame_extent_m": self.frame_extent_m,
            "camera_distance_m": self.camera_distance_m,
            "aim": self.aim.map(|(lon, lat)| serde_json::json!([lon, lat])),
        })
    }
}

/// The scene the falloff walk produced: the ways to extrude, and what it did.
#[derive(Clone, Debug, Default)]
pub struct ViewportScene {
    /// The ways, already clipped to the viewport and already reduced to their level.
    pub ways: Vec<Way>,
    /// The reading ([`ViewportStats`]).
    pub stats: ViewportStats,
}

/// **How many triangles this way will cost the extruder** — read off
/// [`build_with_water_terrain_styled`]'s own branches, not estimated:
///
/// * a risen prism under [`RoofStyle::Pitched`] and `kind == "building"`: an eave cap
///   and a bottom cap (`n-2` ear-clipped triangles each), `2n` wall triangles, plus the
///   roof — `n` for a hipped cone, `6` for the gabled default, `0` for a footprint too
///   big to pitch ([`ROOF_PITCH_MAX_AREA_M2`]);
/// * any other risen prism: the two caps and the walls, `4n − 4`;
/// * flat ground cover (water / park / coastline): one cap, `n − 2`;
/// * a **road** line: three [`ribbon_rounded`] passes (glow, casing, fill), each
///   `2(v−1)` quad triangles plus a `CAP_SEGS`-segment join fan at every vertex —
///   `3 · (10v − 2)`. Roads are the single most expensive thing in a wide viewport and
///   that is why they have their own falloff;
/// * any other line — including a road the falloff has demoted to
///   [`GROUND_ROAD_KIND`]: one pass, `10v − 2`. The pass count is read off `kind` here
///   and in [`line_feature`], one string for both, so the number the budget spends is the
///   number the mesh emits.
///
/// **Deliberately an over-estimate in one place**: an untagged mega-ring that
/// [`prism_height_for`] would flatten is counted as if it rose. Over-estimating spends
/// the budget early, which is the safe direction; under-estimating blows it.
///
/// Asserted against the APPLIED mesh in `tests/continuous_viewport.rs`.
#[must_use]
pub fn estimated_triangles(w: &Way, roof_style: RoofStyle) -> usize {
    if w.points.len() < 2 {
        return 0;
    }
    if !is_prism(w) {
        let v = w.points.len();
        let passes = if w.kind == "road" { 3 } else { 1 };
        return passes * (10 * v - 2);
    }
    // A ring: drop the OSM closing duplicate exactly as `open_ring` does.
    let n = open_ring(&w.points).len();
    if n < 3 {
        return 0;
    }
    if matches!(w.kind, "water" | "park" | "coastline") {
        return n - 2; // flat cover: one single-sided cap, no walls
    }
    let shell = 2 * (n - 2) + 2 * n;
    if roof_style != RoofStyle::Pitched || w.kind != "building" {
        return shell;
    }
    let roof = if ring_area_m2(&w.lonlat) > f64::from(ROOF_PITCH_MAX_AREA_M2) {
        0 // too big to pitch — just the coloured eave cap
    } else if w.roof_shape == RoofShape::Hipped {
        n // a fan from every rim edge to the apex
    } else {
        6 // the gabled default: two slope quads + two gable ends
    };
    shell + roof
}

/// A `(lon, lat)` ring's area in **square metres**, on the same flat-Earth
/// approximation [`geo_distance_m`] uses (so the pitched-roof gate this feeds asks the
/// same question `resolve_prism_style` asks in design units).
fn ring_area_m2(lonlat: &[(f64, f64)]) -> f64 {
    if lonlat.len() < 3 {
        return 0.0;
    }
    let lat0 = lonlat.iter().map(|p| p.1).sum::<f64>() / lonlat.len() as f64;
    let (sx, sy) = (m_per_deg_lon(lat0), M_PER_DEG_LAT);
    let mut a = 0.0f64;
    for i in 0..lonlat.len() {
        let (x0, y0) = lonlat[i];
        let (x1, y1) = lonlat[(i + 1) % lonlat.len()];
        a += (x0 * sx) * (y1 * sy) - (x1 * sx) * (y0 * sy);
    }
    (a * 0.5).abs()
}

/// **Reduce a way to `detail`**, or `None` when nothing of it survives at that level.
/// Pure — it clones and re-projects through [`Way::with_lonlat`], so the Mercator
/// points and the bbox stay the ones the extruder will read (L5: there is no second
/// projection here).
#[must_use]
pub fn at_detail(w: &Way, detail: Detail) -> Option<Way> {
    into_detail(w.clone(), detail)
}

/// [`at_detail`] on an OWNED way — what the walk uses, so a way that came out of
/// [`clip_ways_to_window`] is cloned exactly once on its way into the scene instead of
/// twice (LAW: reuse, don't copy). The `Full` arm then moves rather than allocating.
fn into_detail(w: Way, detail: Detail) -> Option<Way> {
    if detail == Detail::Full || w.lonlat.len() < 3 {
        return Some(w);
    }
    if is_prism(&w) {
        if matches!(w.kind, "water" | "park" | "coastline") {
            return Some(w); // the ground: `n-2` triangles, never reduced, never dropped
        }
        if detail == Detail::Ground {
            return None; // a building past the coarse band — gone, and that is the falloff
        }
        let ring = bbox_ring(&w.lonlat);
        return Some(w.with_lonlat(ring));
    }
    // A line. `Coarse` decimates, `Ground` keeps the two ends — a road is still a road.
    let keep = if detail == Detail::Coarse { COARSE_LINE_VERTS } else { 2 };
    let line = decimate(&w.lonlat, keep);
    let mut out = w.with_lonlat(line);
    // …and in the GROUND band it also loses the casing and the glow. Decimating a road
    // to two vertices is not enough: the three-pass ribbon costs 54 triangles at two
    // vertices, and the ground band is unconditional, so on a real city viewport the
    // passes — not the vertices — are what puts the scene 2.3x over budget. See
    // [`GROUND_ROAD_KIND`] for the measurement.
    if detail == Detail::Ground && out.kind == "road" {
        out.kind = GROUND_ROAD_KIND;
    }
    Some(out)
}

/// The closed 5-vertex **bounding-box ring** of a `(lon, lat)` loop — a building's
/// block silhouette at the distance where its corners are sub-pixel.
fn bbox_ring(lonlat: &[(f64, f64)]) -> Vec<(f64, f64)> {
    let (mut x0, mut y0, mut x1, mut y1) = (f64::MAX, f64::MAX, f64::MIN, f64::MIN);
    for &(x, y) in lonlat {
        x0 = x0.min(x);
        y0 = y0.min(y);
        x1 = x1.max(x);
        y1 = y1.max(y);
    }
    vec![(x0, y0), (x1, y0), (x1, y1), (x0, y1), (x0, y0)]
}

/// Keep at most `keep` vertices of a polyline, **always including both ends**, evenly
/// spaced along the index. Not Douglas–Peucker: the cost this exists to bound is
/// per-VERTEX (a join fan each), so a bound on the count is the thing that is needed,
/// and a shape-aware simplifier would be a second answer to a question nobody asked.
fn decimate(lonlat: &[(f64, f64)], keep: usize) -> Vec<(f64, f64)> {
    let n = lonlat.len();
    let keep = keep.max(2);
    if n <= keep {
        return lonlat.to_vec();
    }
    (0..keep)
        .map(|i| lonlat[((i * (n - 1)) as f64 / (keep - 1) as f64).round() as usize])
        .collect()
}

/// **The whole viewport, extruded nearest-first until the triangles run out.**
///
/// The one entry point behind [`crate::Map3D::from_viewport_roofed`], and the answer to
/// a question [`view_town_subset`] does not ask.
///
/// # What it does, in order
///
/// 1. **Clip to the viewport**, not to a legibility box: the window is `span_m / 2`,
///    through the SAME [`clip_ways_to_window`] the town path uses, so every kept vertex
///    is inside the box the operator has on screen and the design unit is bounded by it.
/// 2. **Sort by distance** from the viewport centre (a way's own centroid,
///    [`geo_distance_m`]).
/// 3. **Walk nearest-first**, degrading [`Detail`] as the budget is spent — full
///    buildings, then boxes, then ground only. The level never climbs back, so the
///    scene really is a falloff and not a checkerboard of whatever happened to be cheap.
/// 4. **Never return nothing.** Ground cover, water and the road network survive at
///    every distance out to the viewport border, so a viewport with no buildings in it
///    still paints the country it is over. That is the second half of the defect: the
///    old path answered "no town here" with an empty pane.
///
/// # The floor under the near band
///
/// Full detail is not allowed to run out before [`MIN_BUILDINGS_IN_VIEW`] buildings have
/// had it — the same "more than a hamlet" threshold the town window uses. Without it, a
/// dense enough centre could spend the whole full-detail budget on the first street and
/// the pane would have no skyline at all at a zoom where it should.
///
/// # What it deliberately does not parameterise
///
/// The estimate is taken at [`RoofStyle::Pitched`], because
/// [`crate::Map3D::from_viewport_roofed`] is the only caller and it always extrudes
/// pitched. A `RoofStyle` on [`SceneBudget`] would be a knob with one setting and a
/// second place for the two to disagree.
///
/// # The floor, and why it is subtracted rather than hoped about
///
/// Ground cover, water and the road network are UNCONDITIONAL: they are kept at every
/// distance, because a viewport with no houses in it must still paint the country it is
/// over. This doc used to call that "a bounded cost — a `Detail::Ground` line is two
/// vertices", and the reasoning counted VERTICES. A two-vertex ROAD costs 54 triangles
/// (three [`ribbon_rounded`] passes, an eight-segment join fan at each end), and a real
/// city viewport holds ~15 000 line ways: **MEASURED 1 192 194 triangles of lines in a
/// 1 357 801-triangle scene against a 600 000 budget**, 2.26x over, on
/// `tests/the_budget_is_enforced.rs`'s reconstruction of the owner's 2026-08-11 drive. The
/// budget was printed and not honoured, which reads as a guarantee and is not one.
///
/// Two changes make it a bound:
///
/// * [`GROUND_ROAD_KIND`] — a road in the ground band loses its casing and glow, the two
///   passes that draw the same pixel at that distance. 54 → 18 triangles per line.
/// * [`ViewportStats::ground_floor`] is summed BEFORE the walk and subtracted from the
///   budget, so the detail bands are chosen inside what is actually left instead of
///   spending the whole budget and adding the far ground on top.
///
/// When the floor alone exceeds the budget — or when [`MIN_BUILDINGS_IN_VIEW`] holds the
/// near band open past it so the pane still has a skyline — the scene genuinely cannot
/// fit, and [`ViewportStats::budget_met`] says so rather than letting an over-budget
/// number be printed as if it were fine.
#[must_use]
pub fn viewport_scene(ways: &[Way], view: ViewWindow, budget: SceneBudget) -> ViewportScene {
    // The window is the viewport, bounded by what the design box can hold a camera
    // inside of ([`scene_reach_half_m`] — the measurement is in its doc).
    let half_m = scene_reach_half_m(view.span_m);
    let mut stats = ViewportStats {
        span_m: view.span_m,
        budget: budget.triangles,
        ground_radius_m: half_m,
        reach_is_the_viewport: half_m >= view.span_m * 0.5 - 1.0,
        // The REQUEST. `Map3D::from_viewport_roofed` stamps the applied
        // `camera_distance_m` over this struct once the dolly clamp has had its say.
        frame_extent_m: legible_frame_extent_m(view.span_m),
        camera_distance_m: legible_camera_distance_m(view.span_m),
        full_radius_m: f64::INFINITY,
        coarse_radius_m: f64::INFINITY,
        ..Default::default()
    };
    if !(view.span_m.is_finite() && view.span_m > 0.0 && view.lon.is_finite() && view.lat.is_finite()) {
        // No usable camera: this entry point has nothing to window by, so it hands the
        // input back rather than inventing a viewport. (`from_viewport_roofed` takes a
        // `ViewWindow` by value precisely so this is the only way in.)
        stats.ways_in(ways);
        stats.ground_floor = ways.iter().map(ground_floor_triangles).sum();
        stats.budget_met = stats.triangles <= stats.budget;
        return ViewportScene { ways: ways.to_vec(), stats };
    }
    // **Order first, clip second.** The window box, and a nearest-first index into the
    // INPUT — no geometry is copied yet. A dense z12 viewport drops tens of thousands of
    // far buildings, and cloning them only to throw them away is the allocation the LAW
    // ("preallocate and reuse", "zero copy") is about; clipping lazily inside the walk
    // means a way is cloned exactly when it is kept.
    let (wlon, wlat) = (half_m / m_per_deg_lon(view.lat), half_m / M_PER_DEG_LAT);
    let win = (view.lon - wlon, view.lat - wlat, view.lon + wlon, view.lat + wlat);
    // Only ways with a vertex in the window are candidates — the same first test
    // `clip_way_to_window` makes, made here so it is made ONCE and so the falloff's
    // "dropped" count means "in view and too far", not "somewhere else in Sweden".
    // The sort is on the PRECOMPUTED distance: one `geo_distance_m` per way, not one per
    // comparison.
    let touches = |w: &Way| {
        w.lonlat
            .iter()
            .any(|&(lon, lat)| lon >= win.0 && lon <= win.2 && lat >= win.1 && lat <= win.3)
    };
    let mut order: Vec<(f64, usize)> = ways
        .iter()
        .enumerate()
        .filter(|(_, w)| touches(w))
        .filter_map(|(i, w)| centroid_lonlat(w).map(|c| (geo_distance_m((view.lon, view.lat), c), i)))
        .collect();
    order.sort_by(|a, b| a.0.total_cmp(&b.0));

    // **RESERVE THE FLOOR BEFORE SPENDING ANYTHING.**
    //
    // The ground band is unconditional — cover, water and the road network survive to the
    // viewport border, because a viewport with no houses in it must still paint the
    // country it is over. That guarantee has a price, it is paid whatever the bands do,
    // and until now nothing subtracted it: the walk spent the WHOLE budget on the near
    // detail and then added the far ground on top. MEASURED on a reconstruction of
    // The owner's 2026-08-11 drive (`tests/the_budget_is_enforced.rs`): 1 357 801 triangles
    // estimated against a 600 000 budget, 1 192 194 of them lines. The estimate was
    // printed, not honoured.
    //
    // So the detail bands are chosen inside what is LEFT after the floor. One O(n) pass
    // over the candidates already selected above, no clipping and no clone.
    let ground_floor: usize = order.iter().map(|&(_, i)| ground_floor_triangles(&ways[i])).sum();
    stats.ground_floor = ground_floor;
    let detail_budget = budget.triangles.saturating_sub(ground_floor);
    let full_budget = (detail_budget as f64 * f64::from(budget.full_detail_share).clamp(0.0, 1.0)) as usize;
    let mut level = Detail::Full;
    let mut spent = 0usize;
    let mut keep: Vec<Way> = Vec::with_capacity(order.len());
    let (mut aim_lon, mut aim_lat, mut aim_n) = (0.0f64, 0.0f64, 0usize);

    for (dist_m, i) in order {
        let src = &ways[i];
        let is_building = src.kind == "building";
        let is_ring = is_prism(src);
        // Degrade, monotonically, and record where each band ended. The near-band floor
        // holds `Full` open until the skyline exists.
        if level == Detail::Full && spent >= full_budget && stats.buildings_full >= MIN_BUILDINGS_IN_VIEW {
            level = Detail::Coarse;
            stats.full_radius_m = dist_m;
        }
        if level == Detail::Coarse && spent >= detail_budget {
            level = Detail::Ground;
            stats.coarse_radius_m = dist_m;
        }
        // A building past the coarse band is dropped WITHOUT being clipped or cloned —
        // the cheapest possible answer to the commonest case in a wide viewport.
        if level == Detail::Ground && is_ring && is_building {
            stats.buildings_dropped += 1;
            continue;
        }
        let Some(w) = clip_way_to_window(src, win) else { continue };
        let Some(reduced) = into_detail(w, level) else {
            stats.buildings_dropped += usize::from(is_building);
            continue;
        };
        spent += estimated_triangles(&reduced, RoofStyle::Pitched);
        if is_building {
            match level {
                Detail::Full => {
                    stats.buildings_full += 1;
                    if let Some((lon, lat)) = centroid_lonlat(&reduced) {
                        aim_lon += lon;
                        aim_lat += lat;
                        aim_n += 1;
                    }
                }
                Detail::Coarse => stats.buildings_coarse += 1,
                Detail::Ground => stats.buildings_dropped += 1,
            }
        } else if is_ring {
            stats.ground_cover += 1;
        } else {
            match level {
                Detail::Full => stats.lines_full += 1,
                Detail::Coarse => stats.lines_coarse += 1,
                Detail::Ground => stats.lines_ground += 1,
            }
        }
        keep.push(reduced);
    }
    stats.triangles = spent;
    // **The verdict, said out loud.** `false` is not a miss to be hidden: it means either
    // the floor alone is over the host's budget, or `MIN_BUILDINGS_IN_VIEW` deliberately
    // held the near band open so the pane still has a skyline. Both are choices, and a
    // reader has to be able to tell them from a bound that simply was not applied.
    stats.budget_met = spent <= budget.triangles;
    if aim_n > 0 {
        stats.aim = Some((aim_lon / aim_n as f64, aim_lat / aim_n as f64));
    }
    ViewportScene { ways: keep, stats }
}

/// A way's `(lon, lat)` centroid — the point its distance from the camera is measured
/// at. `None` for an empty way.
fn centroid_lonlat(w: &Way) -> Option<(f64, f64)> {
    if w.lonlat.is_empty() {
        return None;
    }
    let n = w.lonlat.len() as f64;
    let (sx, sy) = w.lonlat.iter().fold((0.0, 0.0), |(a, b), &(x, y)| (a + x, b + y));
    Some((sx / n, sy / n))
}

/// **How much ground the camera frames, in metres, at a viewport span** — the near-band
/// legibility the pane must keep however wide the DATA window is.
///
/// # Why the data window and the camera framing had to be separated
///
/// They used to be the same number. [`view_town_subset`] cut the data to
/// [`town_half_for_span`] and [`crate::Map3D::frame_densest_cluster`] framed the densest
/// 3x3 cell of a 24x24 grid over the design box — and because the town filled the box,
/// that came out at a fixed fraction of the town window. Legibility was a side effect of
/// the cut.
///
/// Widen the data window to the whole viewport and the side effect inverts: the design
/// box is now 37 km, one grid cell is 1.5 km, and the same framing rule puts the camera
/// 13x further from the houses. MEASURED on the ladder fixture, the median house falls
/// from 10.07 px to well under one — the pancake, by a different road.
///
/// So the framing is stated in METRES and derived from [`town_half_for_span`], which is
/// the measured, defended answer to "how much ground is legible at this zoom". The
/// factor restates what the grid framing was already doing:
/// `3 cells / 24 · box span 2 · (2·half / DESIGN_FILL) metres per design unit`.
/// The camera therefore sits exactly where it sits today at every zoom; the only thing
/// that changed is that there is now geometry past the edge of the frame.
#[must_use]
pub fn legible_frame_extent_m(span_m: f64) -> f64 {
    const FRAME_CELLS: f64 = 3.0;
    const GRID: f64 = 24.0;
    let metres_per_du = 2.0 * town_half_for_span(span_m) / f64::from(DESIGN_FILL);
    (FRAME_CELLS / GRID * 2.0) * metres_per_du
}

/// How far the eye ends up from the aim point, in metres, for a viewport of `span_m` —
/// [`legible_frame_extent_m`] pushed through [`crate::Map3D::frame_extent`]'s own dolly
/// expression (`extent / tan(fov/2) · 1.6`), so the two cannot drift.
#[must_use]
pub fn legible_camera_distance_m(span_m: f64) -> f64 {
    let half_fov = f64::from(crate::camera::OrbitCamera::DEFAULT_FOV_Y_DEG.to_radians()) * 0.5;
    legible_frame_extent_m(span_m) / half_fov.tan() * 1.6
}

/// **How wide a scene the renderer can actually let you stand inside — the reach the
/// continuous viewport is cut to, in metres from the aim point.**
///
/// # This is a limit of the design box, and it was MEASURED, not assumed
///
/// Extruding the whole viewport is the obvious thing and it does not work, for a reason
/// that is structural rather than a matter of taste. Every scene is normalised into the
/// same `[-1, 1]` design box ([`to_design`]), so a wide scene does not put the camera
/// further away in design units — it makes the METRE smaller. And the camera has floors
/// that are stated in design units and cannot follow: the dolly bottoms out at
/// [`crate::camera::OrbitCamera::MIN_DISTANCE`] (0.06) and the near plane sits at
/// `NEAR_PLANE` (0.02). In a 37 km box those are **925 m** and **463 m** of real ground:
/// the eye cannot be closer to a house than a kilometre, and anything within 460 m of it
/// is clipped away. You cannot be in a street that is smaller than the near plane.
///
/// MEASURED on the ladder fixture (`tests/continuous_viewport::continuous_ladder`, before
/// this cap existed): windowing the whole 37 km z12 viewport put the median house at
/// **0.77 px**, against **10.07 px** on the town path. That is the pancake, reached by
/// widening the window instead of by capping it — the same failure
/// [`TOWN_HALF_MAX_M`] documents, one level up.
///
/// # What the reach is
///
/// The widest scene in which the eye can still stand at its legible distance
/// ([`legible_camera_distance_m`]) *without* being stopped by the dolly floor:
///
/// ```text
///   half_m = DESIGN_FILL/2 · legible_camera_distance_m(span) / MIN_DISTANCE
/// ```
///
/// Nothing in it is a new constant. It is the legibility target
/// ([`LEGIBLE_MEDIAN_HOUSE_PX`], through [`town_half_for_span`]), the design fill, and
/// the camera's own floor, solved for the one unknown. It tracks the zoom because
/// `town_half_for_span` does: **16.6 km at korp's z12**, 9.5 km at z14, 4.7 km at z16.
///
/// It is capped at `span_m/2` — there is no point extruding ground the 2D pane is not
/// showing either — so from z14 down the reach IS the viewport and the scene really is
/// continuous edge to edge. Above that it is a horizon under fog rather than an edge,
/// and it is **14x wider than `2·TOWN_HALF_MAX_M`** at z12: 16.6 km against 1.16 km,
/// 200x the area.
///
/// **Stated plainly, because it is the honest limit of this renderer**: at region zoom
/// the 3D pane still shows less world than the 2D pane. What changed is the size of the
/// discrepancy and the shape of its edge.
#[must_use]
pub fn scene_reach_half_m(span_m: f64) -> f64 {
    if !span_m.is_finite() || span_m <= 0.0 {
        return f64::from(TOWN_HALF_MAX_M as f32);
    }
    let reach = 0.5 * f64::from(DESIGN_FILL) * legible_camera_distance_m(span_m)
        / f64::from(crate::camera::OrbitCamera::MIN_DISTANCE);
    reach.min(span_m * 0.5)
}

/// Map a Mercator-unit `(x, y)` footprint vertex into the `[-1, 1]` design plane
/// `(dx, dz)` (the ground plane; y stays the height axis). North (small Mercator
/// y) maps to far (−z), so the camera tilt looks "into" the city.
///
/// **Public because a CAMERA has to land in the same plane the geometry did.**
/// `Map3D::design_from_lonlat` places a geographic eye in this frame, and a second
/// spelling of this arithmetic would put the camera and the city in two coordinate
/// systems that agree until one of them is edited (LAW 5). It stays the ONE writer.
#[must_use]
pub fn to_design(x: f32, y: f32, bb: (f32, f32, f32, f32)) -> (f32, f32) {
    let (x0, y0, x1, y1) = bb;
    let w = (x1 - x0).max(1e-9);
    let h = (y1 - y0).max(1e-9);
    let s = w.max(h); // uniform scale → no distortion
    let cx = (x0 + x1) * 0.5;
    let cy = (y0 + y1) * 0.5;
    let dx = (x - cx) / s * DESIGN_FILL; // fill ~80% of the [-1,1] box
    let dz = (y - cy) / s * DESIGN_FILL;
    (dx, dz)
}

/// **[`to_design`]'s arithmetic in full precision**, for the callers whose input MOVES.
///
/// # Why this is a second spelling and not a shared writer
///
/// It was written as the shared one — `to_design` delegating here through an f32 door on
/// each side — and that is the better shape, so the reason it is not is worth stating.
/// Routing the MESH path through f64 moves footprint vertices by up to one f32 ULP, and
/// `tests/sigtuna_tour_proof.rs` went red on it: its "the town's buildings extrude into
/// 3D prisms" check sits on a threshold thin enough for one ULP to cross. That is a
/// fragile assertion rather than a real regression — but it is a fragility in the MESH
/// path, and the flicker this function exists to fix is not in the mesh path at all: a
/// footprint vertex is quantised once and then sits still.
///
/// So the split is deliberate and narrow. The geometry keeps the arithmetic it was
/// pinned with; the per-frame CAMERA and overlay derivation gets the precision it needs.
/// The two agree to f32 rounding by construction (same formula, same constants). If the
/// sigtuna threshold is ever made robust, collapse them back into one writer.
///
/// # Why the precision is not a detail: the raven flickered on it
///
/// A footprint vertex is quantised ONCE, at build time, and then sits still — f32 costs
/// it nothing. The rider's eye and the raven's overlay pose are different animals: they
/// are re-derived from moving f64 lon/lat **every frame**, so a rounding step in the
/// middle of that derivation is not a static error, it is a STAIRCASE the pose walks up
/// and down while the body glides smoothly.
///
/// The old `design_from_lonlat` cast the Mercator coordinate to f32 **before** the bbox
/// subtraction, and one f32 ULP of Mercator y at 59.33° N is `2.98e-8 unit ×
/// 20.44e6 m/unit` = **0.61 m of ground** (measured tread: 0.590 m). The eye and the
/// korp landed on different treads on different frames, so their separation twitched
/// out and back — MEASURED over 600 flying frames as 60 one-frame reversals (10.0% of
/// frames) in the eye→korp distance and 52 (8.7%) in the overlay's depth, while the
/// body's own f64 `chase_lag_m` reversed 0 times out of 600.
///
/// On the real wgpu lane that is the flicker itself: the raven's centroid jumped with a
/// per-frame standard deviation of **9.29 px** and **35.6% of frames moved it more than
/// 10 px**, under a seat that is supposed to hold her still. Subtracting first and
/// rounding once takes those to 0.90 px and 0/118 frames.
///
/// The subtraction is the whole fix: `x − cx` is a small difference of two large
/// numbers, and doing it in f64 keeps the ~0.6 m of significance that the cast threw
/// away. The RESULT is still returned as f32 by [`to_design`] and that is fine — a
/// design unit is ~1 km here, so an f32 ULP of the *output* is well under a millimetre.
#[must_use]
pub fn to_design_f64(x: f64, y: f64, bb: (f64, f64, f64, f64)) -> (f64, f64) {
    let (x0, y0, x1, y1) = bb;
    let w = (x1 - x0).max(1e-9);
    let h = (y1 - y0).max(1e-9);
    let s = w.max(h); // uniform scale → no distortion
    let cx = (x0 + x1) * 0.5;
    let cy = (y0 + y1) * 0.5;
    let fill = f64::from(DESIGN_FILL);
    let dx = (x - cx) / s * fill; // fill ~80% of the [-1,1] box
    let dz = (y - cy) / s * fill;
    (dx, dz)
}

/// **Metres per degree of LATITUDE.** One writer (L5): it was declared twice in this
/// file — once inside `densest_cluster_subset` next to `WINDOW_M`, and once inside
/// [`world_span_metres`], which is the canonical scale writer itself. Same value both
/// times, which is exactly how two writers of one quantity survive: they agree until
/// one of them is edited.
///
/// That claim was true of the SOURCES and false of the crate: on 2026-08-22 the same
/// literal still stood in **eight test files**, which build their fixtures — synthetic
/// towns placed by converting metres to degrees — on a private copy of it. A fixture on
/// a private ruler measures a different earth from the code under test the moment
/// either side moves, and nothing would have said so. All eight now import this
/// constant; [`m_per_deg_lon`] absorbed four private `cos(lat)` twins with it, and
/// `tests/scene_scale_metric_constants.rs`
/// (`the_flat_earth_ruler_is_the_wgs84_meridian`) pins the VALUE against the WGS-84
/// meridian — because one writer that is WRONG is the failure mode a collapse creates,
/// and the pin's `lat = 0` arm is deliberately kept beside the others to show it: a
/// longitude writer with its `cos` deleted passes there (cos 0 = 1, the identity value)
/// and is caught at Stockholm.
pub const M_PER_DEG_LAT: f64 = 111_320.0;

/// **Metres per degree of LONGITUDE at `lat_deg`** — latitude degrees are constant,
/// longitude degrees shrink by `cos(lat)`. The floor keeps the pole from dividing by
/// zero. The other half of the same duplication: this expression stood verbatim in both
/// functions above.
#[must_use]
pub fn m_per_deg_lon(lat_deg: f64) -> f64 {
    M_PER_DEG_LAT * lat_deg.to_radians().cos().abs().max(1e-6)
}

/// **Metres between two WGS-84 points**, on the same flat-Earth approximation every
/// other metric quantity in this module uses (latitude degrees are constant, longitude
/// degrees shrink by `cos(lat)`). Exact enough by far at town scale — the error against
/// a great circle is well under a metre over the few kilometres a scene spans — and it
/// agrees *by construction* with [`world_span_metres`] and the town window, which is the
/// property that matters: the distance a test asserts on and the window that produced it
/// are measured with one ruler.
///
/// This is what turns "is the 3D pane showing the right place?" into a number.
#[must_use]
pub fn geo_distance_m(a: (f64, f64), b: (f64, f64)) -> f64 {
    let lat_mid = (a.1 + b.1) * 0.5;
    let dx = (a.0 - b.0) * m_per_deg_lon(lat_mid);
    let dy = (a.1 - b.1) * M_PER_DEG_LAT;
    (dx * dx + dy * dy).sqrt()
}

/// The data's **larger real-world span in metres**, from the ways' WGS-84
/// `lonlat` bbox: latitude degrees are ~111_320 m each; longitude degrees shrink
/// by `cos(lat)`. The data's long footprint axis fills [`DESIGN_FILL`] design
/// units, so **design-units-per-metre = `DESIGN_FILL / span_m`** — that single
/// factor lifts a building's metric height into the SAME scale as its footprint,
/// turning a ~10–30 m building in a ~1–3 km cluster into a readable ~0.01–0.04
/// design units (a skyline) instead of a uniform sky-high spike. Returns `None`
/// for empty / degenerate (single-point) data → the caller keeps a safe default.
pub fn world_span_metres(ways: &[Way]) -> Option<f32> {
    // ONE writer for "what lon/lat box is this data in": this used to be a
    // character-for-character copy of [`lonlat_bbox`]'s loop with the trig bolted on
    // the end, in the same file, ten lines apart.
    let (lon0, lat0, lon1, lat1) = lonlat_bbox(ways)?;
    let lat_mid = (lat0 + lat1) * 0.5;
    let m_per_deg_lon = m_per_deg_lon(lat_mid);
    let span_lat_m = (lat1 - lat0) * M_PER_DEG_LAT;
    let span_lon_m = (lon1 - lon0) * m_per_deg_lon;
    let span = span_lat_m.max(span_lon_m) as f32;
    if span.is_finite() && span > 1.0 { Some(span) } else { None }
}

/// **THE scene scale — design units per real metre.** `None` for synthetic /
/// degenerate data with no world span (the rabbit fixture, the unit-test towns),
/// which is the signal every metric constant reads to keep its legacy absolute.
///
/// This is the **one writer** (LAW 5) behind every "how big is a metre here?"
/// question on the extrusion side: building heights, [`ground_band`],
/// [`building_rise_eps`], [`water_height`]. Its exact reciprocal on the render side
/// is [`crate::lod::metres_per_design_unit`] — same `DESIGN_FILL / world_span_metres`
/// definition, so the extruder and the camera can never disagree about the scale.
///
/// Three separate defects have been the same mistake: an absolute design-unit
/// constant weighed against a metrically scaled scene. `GROUND_LIFT` and
/// `BUILDING_RISE_EPS` were the first two; the water plane and the fragment
/// shader's height haze were the third and fourth. Anything that means a LENGTH
/// goes through here.
#[must_use]
pub fn design_units_per_metre(ways: &[Way]) -> Option<f32> {
    world_span_metres(ways).map(|span_m| DESIGN_FILL / span_m)
}

/// **The span-less fallback**, for the consumers that need a number rather than the
/// `None` [`design_units_per_metre`] hands them. It is the exact reciprocal of the
/// render side's [`crate::lod::DEFAULT_METRES_PER_DESIGN_UNIT`] — *derived*, not a
/// second literal, so the extruder and the LOD lever cannot disagree about how big a
/// metre is on the very data both fall back on.
///
/// It was a second literal (`2.0e-4`, i.e. 5000 m/du) in the relief heightfield, 8×
/// away from `lod`'s 625 m/du: the terrain came out 8× flatter than the altitude lever
/// believed the scene to be. `tests/scene_scale_metric_constants.rs`
/// (`the_spanless_fallback_is_the_same_number_on_both_sides`) pins the two together
/// through the emitted mesh.
pub const DEFAULT_DESIGN_UNITS_PER_METRE: f32 = 1.0 / crate::lod::DEFAULT_METRES_PER_DESIGN_UNIT;

/// A footprint's **plan dimensions in metres** — `(short side, long side)` of its
/// axis-aligned extent — from a ring already in design units and the scene's
/// design-units-per-metre. `None` for a degenerate (sub-metre / empty) footprint.
/// The input to [`MAX_DEFAULT_WALL_OVER_FOOTPRINT`]; also what
/// [`building_height_stats`] reports the wall/footprint ratios against, so the
/// extruder and its oracle measure the same quantity.
pub fn footprint_plan_metres(ring: &[(f32, f32)], du_per_m: f32) -> Option<(f32, f32)> {
    if ring.len() < 3 || du_per_m.is_nan() || du_per_m <= 0.0 {
        return None;
    }
    let (mut x0, mut x1, mut z0, mut z1) = (f32::MAX, f32::MIN, f32::MAX, f32::MIN);
    for &(x, z) in ring {
        x0 = x0.min(x);
        x1 = x1.max(x);
        z0 = z0.min(z);
        z1 = z1.max(z);
    }
    let (w, h) = ((x1 - x0) / du_per_m, (z1 - z0) / du_per_m);
    if !w.is_finite() || !h.is_finite() {
        return None;
    }
    let (short, long) = (w.min(h), w.max(h));
    if long < 1.0 { None } else { Some((short, long)) }
}

/// **How tall a DEFAULT (untagged) building may be relative to its own footprint's
/// SHORT plan side.**
///
/// Only 69 of the embedded Sigtuna extract's 10373 buildings carry a `height` or
/// `building:levels` tag — 0.7%; over korp's whole Sweden cut it is 4.32 % (MEASURED
/// 2026-08-31, 3 841 483 buildings). Everything else fell back to a CONSTANT
/// [`DEFAULT_BUILDING_HEIGHT_M`] = 6 m, a number that knows nothing
/// about the footprint it is lifting. Measured on the demo's Sigtuna town core
/// (182 buildings), that put **77 of them TALLER THAN THEY ARE LONG** — footprints
/// with a 4.4 m long side wearing a 6 m wall, i.e. chimneys, not houses — with
/// wall/long-side reaching 1.77. The roof rise scales with the footprint
/// (`DEFAULT_ROOF_PITCH * footprint_radius`) while the wall did not, so the roof
/// shrank to a lid on a tall box: exactly the reported "the houses' walls have been
/// enlarged and the roofs look like they lean directly on the ground".
///
/// You do not build a 6 m wall on a 4 m shed. An untagged default is therefore
/// capped at this fraction of the footprint's short side, which (short <= long)
/// makes `wall <= 0.9 * long` a structural guarantee rather than a hope — see
/// `tests/wall_roof_ratio.rs`. **Tagged heights are REAL DATA and are never
/// capped**: a genuine 40 m tower on a small footprint stays 40 m.
pub const MAX_DEFAULT_WALL_OVER_FOOTPRINT: f32 = 0.9;

/// Floor under the [capped](MAX_DEFAULT_WALL_OVER_FOOTPRINT) default height, so a
/// tiny outbuilding is still a building and not a paving slab. Itself clamped by
/// the cap, so it can never re-create the very tower the cap removes.
pub const MIN_DEFAULT_BUILDING_HEIGHT_M: f32 = 2.5;

/// Design-height for one building footprint, from its real metric height scaled
/// by the data's world span. `height_m` is the way's tagged height as
/// [`facett_map::parse_building_height_m`] decided it — `None` for **both** an untagged
/// building and one whose tag was junk (`height=99999`, `1;12`, `B`), which then get the
/// same [`DEFAULT_BUILDING_HEIGHT_M`] box, capped by
/// [`MAX_DEFAULT_WALL_OVER_FOOTPRINT`] against the footprint. `du_per_m` is
/// [`DESIGN_FILL`]/span_m. A small deterministic per-building jitter (keyed on the
/// footprint, NO RNG → snapshot-stable) breaks up the default-height slab so an
/// untagged town still reads as a varied skyline, not one flat roofline.
fn building_design_height(height_m: Option<f32>, du_per_m: f32, osm_id: i64, ring: &[(f32, f32)]) -> f32 {
    let (tagged, base_m) = match height_m {
        Some(m) => (true, m),
        None => (false, DEFAULT_BUILDING_HEIGHT_M),
    };
    // Untagged defaults get a gentle ±25% deterministic jitter so the roofline
    // varies; tagged heights are used as-is (they're already real).
    let h_m = if tagged {
        base_m
    } else {
        // Hash the id + first ring vertex → a stable fraction in [-1, 1].
        let mut seed = osm_id as u64 ^ 0x9E37_79B9_7F4A_7C15;
        if let Some(&(x, z)) = ring.first() {
            seed ^= (x.to_bits() as u64).rotate_left(17) ^ (z.to_bits() as u64).rotate_left(31);
        }
        seed = seed.wrapping_mul(0x2545_F491_4F6C_DD1D);
        let frac = ((seed >> 40) as f32 / (1u64 << 24) as f32) * 2.0 - 1.0; // [-1,1]
        base_m * (1.0 + 0.25 * frac)
    };
    // ── the FOOTPRINT CAP (untagged only) ─────────────────────────────────────
    // The default above is a constant; the building it is lifting is not. Cap an
    // untagged wall against its own plan so a 4 m shed cannot wear a 6 m wall (see
    // MAX_DEFAULT_WALL_OVER_FOOTPRINT for the measured damage). A TAGGED height is
    // real data and passes through untouched.
    let h_m = match (tagged, footprint_plan_metres(ring, du_per_m)) {
        (false, Some((short_m, _long_m))) => {
            let cap_m = MAX_DEFAULT_WALL_OVER_FOOTPRINT * short_m;
            // The floor never exceeds the cap, so it cannot undo it.
            let floor_m = MIN_DEFAULT_BUILDING_HEIGHT_M.min(cap_m);
            h_m.min(cap_m).max(floor_m)
        }
        _ => h_m,
    };
    // Real metric height → design units. Clamp the floor to a hair above zero so a
    // 1-storey hut still has a sliver of height (never a zero-height degenerate
    // "prism"), but low enough that the per-building jitter still varies the
    // default-height roofline even at country scale.
    (h_m * du_per_m).max(1e-5)
}

/// Is this way a closed ring we should extrude into a prism? A `Way` with a
/// `fill` is already a classified area; otherwise we accept a geometrically
/// closed loop (first ≈ last) of at least a triangle.
fn is_prism(w: &Way) -> bool {
    if w.fill.is_some() {
        return true;
    }
    if w.points.len() < 4 {
        return false;
    }
    let (a, b) = (w.points[0], w.points[w.points.len() - 1]);
    (a.0 - b.0).abs() < 1e-6 && (a.1 - b.1).abs() < 1e-6
}

/// Build the extruded [`Mesh3`] from `ways`, returning `(mesh, tile_count)` where
/// `tile_count` is the number of footprints turned into geometry (prisms +
/// ribbons + the single terrain plane). `height` is the prism lift in design
/// units. Always emits at least the terrain plane (graceful on empty input).
pub fn build(ways: &[Way], height: f32) -> (Mesh3, usize) {
    let mut mesh = Mesh3::default();
    let mut tiles = 0usize;

    // Terrain plane first (drawn under everything). Sized to the design box.
    terrain_into(&mut mesh);
    tiles += 1;

    let Some(bb) = footprint_bbox(ways) else {
        return (mesh, tiles);
    };

    // Real-world span → design-units-per-metre, so building heights scale to the
    // map's actual ground size (a town vs a country). When the span is unknown
    // (synthetic / degenerate data) fall back to the legacy `height` design-unit
    // lift as a metres-equivalent so the rabbit fixture + unit tests are unchanged.
    let du_per_m = design_units_per_metre(ways);
    // The ground band rides the SAME metric scale the building heights do, so flat
    // cover / road ribbons can never out-rise a roof (see `ground_band`).
    let band = ground_band(du_per_m);

    for w in ways {
        // Recentre the way's footprint into the design plane.
        let ring: Vec<(f32, f32)> = w.points.iter().map(|&(x, y)| to_design(x, y, bb)).collect();
        if ring.len() < 2 {
            continue;
        }
        // Colour: the way's fill (area) else its stroke colour (line).
        let col = w.fill.unwrap_or(w.stroke);
        if is_prism(w) {
            // Don't make a country a skyscraper. A **building** footprint rises to
            // its own real metric height (`height_m` from OSM tags, else a low
            // default) scaled by the data's world span; a landuse / water / forest
            // / coastline ring (or any ring large vs the box, e.g. an admin
            // boundary) lies FLAT on the ground (height ≈ 0) so it reads as ground
            // cover, not a giant tower (#11 preserved).
            let h = prism_height_for(&ring, w.kind, w.height_m, du_per_m, w.osm_id, height);
            extrude_prism_banded(&mut mesh, &ring, h, col, band);
            tiles += 1;
        } else {
            line_feature(&mut mesh, &ring, w, band, du_per_m);
            tiles += 1;
        }
    }

    (mesh, tiles)
}

/// Like [`build`], but ALSO routes **water** polygons ([`Surface::Water`]) into a
/// separate tessellated [`WaterMesh`] (the reflective animated water grid) instead
/// of the flat opaque ground-cover cap. Returns `(opaque_mesh, tile_count,
/// water_mesh)`. The opaque mesh is identical to [`build`]'s for non-water data, so
/// the existing render path + tests are unchanged; water is purely additive.
pub fn build_with_water(ways: &[Way], height: f32) -> (Mesh3, usize, WaterMesh) {
    build_with_water_terrain(ways, height, GroundTerrain::Flat)
}

/// The **ground terrain choice** for [`build_with_water_terrain`]: the legacy flat
/// `y = 0` quad, or a real heightfielded relief mesh sampled from an [`Elevation`]
/// source (the orbit map's "real DEM terrain" — [`relief_terrain_into`]).
///
/// [`Elevation`]: crate::terrain::Elevation
pub enum GroundTerrain<'a> {
    /// The legacy single flat quad (default; snapshot-stable — the extrude tests and
    /// the fixture goldens keep using this).
    Flat,
    /// Real relief: displace a subdivided ground grid by elevations from `elev`.
    Relief {
        /// The elevation source (a `Dem`, `ProceduralTerrain`, or `TerrainField`).
        elev: &'a dyn crate::terrain::Elevation,
        /// Grid subdivisions per side (`subdiv+1` vertices per axis).
        subdiv: usize,
        /// Vertical exaggeration over the metric relief (auto-capped in-box).
        exaggeration: f32,
    },
}

/// Like [`build_with_water`], but the caller chooses the [`GroundTerrain`]: the flat
/// quad (identical output to `build_with_water`) OR a real heightfielded relief mesh.
/// The relief path is what makes the orbit [`crate::Map3D`] show real DEM topography
/// instead of a billiard-table plane. Every roof is a **flat** top cap (the byte-stable
/// default) — for pitched roofs + OSM colours use [`build_with_water_terrain_styled`].
pub fn build_with_water_terrain(ways: &[Way], height: f32, terrain: GroundTerrain<'_>) -> (Mesh3, usize, WaterMesh) {
    let (mesh, tiles, water, _stats) = build_with_water_terrain_styled(ways, height, terrain, RoofStyle::Flat);
    (mesh, tiles, water)
}

/// The **roof rendering style** for [`build_with_water_terrain_styled`] — the opt-in
/// knob that keeps the flat-cap render goldens byte-identical while letting a scene
/// grow real pitched roofs + OSM colours.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum RoofStyle {
    /// Every building is a flat top-cap prism, themed-fill coloured — the byte-stable
    /// default (identical geometry + colours to the historical [`build`] path, so the
    /// pixel goldens do not move).
    #[default]
    Flat,
    /// Buildings grow **real roof geometry** (gabled / hipped from OSM `roof:shape`,
    /// a sensible default for untagged small footprints, flat for large ones) and
    /// carry **OSM colours** (`building:colour` facades, `roof:colour`/`roof:material`
    /// roofs, tasteful defaults otherwise) so the town reads as buildings, not massing.
    Pitched,
}

/// Per-scene counts of the pitched-roof / coloured-building work the styled extrude
/// did — the `state_json` oracle for the "houses read as real buildings" feature.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct RoofStats {
    /// Buildings that grew real pitched (gabled / hipped) roof geometry.
    pub pitched: usize,
    /// Buildings whose roof tint ends up distinct from their facade tint (an OSM
    /// colour or a tasteful class default) — i.e. the town is no longer monochrome.
    pub coloured: usize,
}

/// The design-box **area fraction** above which a building keeps a flat roof even in
/// [`RoofStyle::Pitched`] — a large footprint (a commercial block / hall) reads
/// better flat AND a pitched roof over a big span would tower; also the perf gate
/// (pitching only the small house-scale footprints bounds the extra roof geometry).
const ROOF_PITCH_MAX_AREA_FRAC: f32 = 0.02; // 2% of the [-1,1]² box

/// **The same gate as a real footprint AREA, in m².**
///
/// [`ROOF_PITCH_MAX_AREA_FRAC`] is a fraction of the `[-1,1]²` design box, so it is a
/// SQUARED length and its error grows as `(m/du)²` — the most scale-sensitive constant
/// in the file. At `korp`'s 1875 m/du it opens at **281 250 m²**, about forty football
/// pitches: nothing in an OSM extract is that big, so the gate never fired and every
/// building got a pitched roof, which is both halves of what it exists to prevent (a
/// pitched roof towering over a commercial hall, and the geometry cost of pitching
/// every footprint in a city).
///
/// `4 000 m²` — a 63 m square — is the commercial block / hall the doc above describes,
/// comfortably over any dwelling and 70× tighter than the fraction it replaces at
/// korp's scale. It is deliberately NOT set at a villa's 150 m²: this crate's own
/// "small house" fixtures are 45 × 67 m (≈3 000 m²), so a dwelling-sized gate would
/// re-scale three existing guards' fixtures to land, which is a larger change than the
/// finding warrants. The gate now FIRES, which it never did; where exactly it sits is a
/// tuning question that can be answered against real extracts.
pub const ROOF_PITCH_MAX_AREA_M2: f32 = 4_000.0;

/// The pitched-roof area gate in **design units²** for a scene of `du_per_m`.
/// `None` (synthetic / span-less data) keeps the legacy design-box fraction, so the
/// fixtures pitch exactly the roofs they always did.
#[must_use]
pub fn roof_pitch_max_area_du(du_per_m: Option<f32>) -> f32 {
    match du_per_m {
        Some(d) if d > 0.0 => ROOF_PITCH_MAX_AREA_M2 * d * d,
        _ => ROOF_PITCH_MAX_AREA_FRAC * 4.0,
    }
}

/// Default roof-ridge **pitch** — the ridge/apex rises by `pitch × footprint
/// half-width` above the eave (≈30–35° slopes on a typical house), capped so a wide
/// footprint's roof never towers over its walls (see [`ROOF_RIDGE_MAX_RISE_FACTOR`]).
const DEFAULT_ROOF_PITCH: f32 = 0.6;
/// Cap the roof ridge rise at this multiple of the eave (wall) height, so even a wide
/// house keeps a believable roof-to-wall proportion (never a circus tent).
const ROOF_RIDGE_MAX_RISE_FACTOR: f32 = 1.4;
/// **Gabled-roof footprint-fill floor.** A gabled roof is built on the footprint's
/// oriented bounding box (OBB); when the footprint fills its OBB by at least this
/// fraction it is essentially rectangular, so the OBB-based ridge sits flush on the
/// walls (no overhang). Below it — L-shapes, angled/stepped blocks — the gabled eaves
/// would overhang the walls and leave a see-through hole under the roof at street level,
/// so those footprints fall back to the watertight hipped fan (zero overhang) instead.
/// A true rectangle scores `1.0`; `0.9` keeps gabled only for near-rectangular houses.
const GABLE_MAX_OBB_FILL: f32 = 0.9;

/// Like [`build_with_water_terrain`], but with a [`RoofStyle`]: [`RoofStyle::Flat`]
/// reproduces the historical flat-cap mesh **byte-for-byte** (the golden path), while
/// [`RoofStyle::Pitched`] grows real gabled/hipped roof geometry + honours OSM colours.
/// Returns the extra [`RoofStats`] (pitched + coloured counts) for the oracle.
pub fn build_with_water_terrain_styled(
    ways: &[Way],
    height: f32,
    terrain: GroundTerrain<'_>,
    roof_style: RoofStyle,
) -> (Mesh3, usize, WaterMesh, RoofStats) {
    build_with_water_terrain_styled_with_workers(ways, height, terrain, roof_style, 0)
}

/// Below this many ways the per-way fan-out is not worth a worker pool spin-up and
/// the extrusion stays on the serial loop (a fixture town, the rabbit, a unit test).
const PARALLEL_WAYS_THRESHOLD: usize = 64;

/// **The scene-independent inputs one way's extrusion needs** — everything the
/// per-way body reads besides the way itself, captured once so the serial loop and
/// the gatling chunk workers route through the SAME body (LAW 5).
struct WayExtrudeCtx<'a> {
    bb: (f32, f32, f32, f32),
    du_per_m: Option<f32>,
    band: f32,
    y_water: f32,
    relief: Option<&'a ReliefField>,
    roof_style: RoofStyle,
    height: f32,
}

/// **Extrude ONE way into the given arenas** — the loop body of
/// [`build_with_water_terrain_styled`], hoisted out so it has exactly one spelling
/// whether the arenas are the final mesh (serial) or a chunk worker's private ones
/// (gatling). Everything it reads is per-way or in `cx`; everything it writes is
/// appended to the arenas — no state crosses from one way to the next, which is
/// what makes chunking it sound (and `classify_new_vertices` per-way-scoped).
fn extrude_way_into(
    mesh: &mut Mesh3,
    water: &mut WaterMesh,
    tiles: &mut usize,
    stats: &mut RoofStats,
    w: &Way,
    cx: &WayExtrudeCtx<'_>,
) {
    // **THE ONE PLACE THAT KNOWS WHETHER A VERTEX IS A HOUSE.** Everything pushed
    // between here and the stamp at the bottom belongs to THIS way, so `w.kind` is
    // the whole answer. Nothing downstream re-derives it from a height — it cannot,
    // because `prism_height_for` raises every non-water closed ring, which is why
    // `building_vertices` used to count industrial blocks as houses.
    let vertices_before = mesh.positions.len();
    let is_building = w.kind == "building";
    let ring: Vec<(f32, f32)> = w.points.iter().map(|&(x, y)| to_design(x, y, cx.bb)).collect();
    if ring.len() < 2 {
        return;
    }
    // WATER → the reflective water grid, NOT the opaque mesh.
    if surface_of(w.kind) == Surface::Water && is_prism(w) {
        // Grid resolution scales with the ring extent so big lakes still get a
        // few cells; bounded so a tiny pond doesn't explode. Deterministic.
        let (mut x0, mut z0, mut x1, mut z1) = (f32::MAX, f32::MAX, f32::MIN, f32::MIN);
        for &(x, z) in &ring {
            x0 = x0.min(x);
            z0 = z0.min(z);
            x1 = x1.max(x);
            z1 = z1.max(z);
        }
        let extent = (x1 - x0).max(z1 - z0);
        // ONE writer for "how fine is the water grid": `water_grid_res` derives the
        // cell from the scene's own Gerstner wavelength, so the tessellation and the
        // wave it carries cannot be tuned apart — and it owns the vertex budget, which
        // is why a region-scale lake reads flat instead of costing 26× the vertices to
        // alias the same ripple.
        let res = water_grid_res(extent, cx.du_per_m);
        water_grid_into(water, &ring, res, cx.y_water);
        *tiles += 1;
        return;
    }
    let col = w.fill.unwrap_or(w.stroke);
    if is_prism(w) {
        let h = prism_height_for(&ring, w.kind, w.height_m, cx.du_per_m, w.osm_id, cx.height);
        // Sit the prism ON the ground: over relief terrain its eave datum rides the
        // highest ground under the footprint (`base_y`) and its foundation reaches the
        // lowest (`foot_y`), so the land never rises over the lower walls. Flat terrain
        // → (0, 0), the byte-stable historical base.
        let (base_y, foot_y) = cx.relief.map_or((0.0, 0.0), |f| f.footprint_span(&ring));
        // A real risen building under RoofStyle::Pitched grows real roof geometry
        // + OSM colours; everything else keeps the byte-stable flat-cap prism.
        if cx.roof_style == RoofStyle::Pitched && w.kind == "building" && h.abs() >= 1e-5 {
            let style = resolve_prism_style(&ring, w, col, cx.du_per_m);
            extrude_prism_styled_based(mesh, &ring, h, &style, base_y, foot_y);
            if matches!(style.shape, RoofShape::Gabled | RoofShape::Hipped) {
                stats.pitched += 1;
            }
            if style.roof_col != style.wall_col {
                stats.coloured += 1;
            }
        } else {
            extrude_prism_based(mesh, &ring, h, col, base_y, foot_y, cx.band);
        }
        *tiles += 1;
    } else {
        line_feature(mesh, &ring, w, cx.band, cx.du_per_m);
        *tiles += 1;
    }
    // One stamp per way, covering every vertex the branches above pushed — walls,
    // foundation, flat cap, pitched roof, ribbon, casing, glow. A branch added later
    // is classified BY CONSTRUCTION rather than by remembering to say so.
    mesh.classify_new_vertices(vertices_before, is_building);
}

/// [`build_with_water_terrain_styled`] with an explicit gatling worker count —
/// `0` = one per core (the public default), `1` = the serial reference loop. The
/// hook the byte-identity proof (`tests/extrude_fanout.rs`) drives both sides
/// through; not part of the supported API surface.
#[doc(hidden)]
pub fn build_with_water_terrain_styled_with_workers(
    ways: &[Way],
    height: f32,
    terrain: GroundTerrain<'_>,
    roof_style: RoofStyle,
    workers: usize,
) -> (Mesh3, usize, WaterMesh, RoofStats) {
    let mut mesh = Mesh3::default();
    let mut water = WaterMesh::default();
    let mut tiles = 0usize;
    let mut stats = RoofStats::default();

    // The relief heightfield (`None` for the flat plane) is retained so every prism can
    // be lifted onto the ground it stands over. Under `Flat`, buildings keep the
    // byte-stable `y = 0` base — the existing goldens/callers do not move.
    let relief: Option<ReliefField> = match terrain {
        GroundTerrain::Flat => {
            terrain_into(&mut mesh);
            tiles += 1;
            None
        }
        GroundTerrain::Relief { elev, subdiv, exaggeration } => {
            let field = relief_heightfield(ways, elev, subdiv, exaggeration);
            tiles += push_relief_mesh(&mut mesh, &field);
            Some(field)
        }
    };
    // The ground is never a house — including a RELIEF ground, whose vertices really do
    // stand metres above `y = 0` and which the old class-blind `y > rise_eps` reading
    // therefore counted as buildings. Stamped here rather than at the end so the vector
    // is never behind the positions it describes.
    mesh.classify_new_vertices(0, false);

    let Some(bb) = footprint_bbox(ways) else {
        return (mesh, tiles, water, stats);
    };
    let du_per_m = design_units_per_metre(ways);
    // ONE scale for the whole scene's vertical stack: terrain < cover < road < water
    // < roof. Every one of these is a LENGTH and every one goes through `du_per_m`.
    let band = ground_band(du_per_m);
    let y_water = water_height(du_per_m);

    let cx = WayExtrudeCtx {
        bb,
        du_per_m,
        band,
        y_water,
        relief: relief.as_ref(),
        roof_style,
        height,
    };

    // ── The per-way fan. Each way's extrusion is independent (see
    //    `extrude_way_into`), so the ways are CHUNKED across znippy's fork-join
    //    gatling (rayon is FORBIDDEN — ROOT LAW #0): each worker extrudes its chunk
    //    into a private Mesh3/WaterMesh arena, and the arenas are concatenated IN
    //    CHUNK ORDER with an index rebase (`append_rebased`). Chunk order = way
    //    order, so the fanned mesh is byte-identical to the serial append —
    //    positions, indices, colours, building stamps and the water grid alike
    //    (`tests/extrude_fanout.rs` holds the two side by side). Below the
    //    threshold (or `workers == 1`) the same body runs in the plain loop.
    if ways.len() < PARALLEL_WAYS_THRESHOLD || workers == 1 {
        for w in ways {
            extrude_way_into(&mut mesh, &mut water, &mut tiles, &mut stats, w, &cx);
        }
    } else {
        let eff = if workers == 0 { znippy_zoomies::gatling_forkjoin::default_workers() } else { workers };
        // ~4 chunks per worker evens out the load (a chunk of skyscrapers beside a
        // chunk of paths), floored so a chunk is never trivially small.
        let chunk = ways.len().div_ceil(eff * 4).max(16);
        let nch = ways.len().div_ceil(chunk);
        let parts = znippy_zoomies::gatling_forkjoin::gatling_for_each(nch, workers, |ci| {
            let lo = ci * chunk;
            let hi = (lo + chunk).min(ways.len());
            let mut m = Mesh3::default();
            let mut wt = WaterMesh::default();
            let mut t = 0usize;
            let mut s = RoofStats::default();
            for w in &ways[lo..hi] {
                extrude_way_into(&mut m, &mut wt, &mut t, &mut s, w, &cx);
            }
            (m, wt, t, s)
        });
        // `gatling_for_each` returns the chunks in INDEX order — the concatenation
        // below is the whole determinism argument, so it never sorts or filters.
        // (No reserve pass: `extend` from a `Vec` iterator already reserves the
        // exact length, so the tail is one memcpy per lane per chunk as it stands —
        // an explicit reserve was measured to change nothing.)
        for (m, wt, t, s) in parts {
            mesh.append_rebased(m);
            water.append_rebased(wt);
            tiles += t;
            stats.pitched += s.pitched;
            stats.coloured += s.coloured;
        }
    }

    (mesh, tiles, water, stats)
}

/// Fraction of the `[-1,1]` design box (area 4) above which an *untagged* closed
/// ring is treated as flat ground cover rather than a risen prism — the safety
/// net that catches a country-sized admin boundary with no useful `kind`.
const FLAT_AREA_FRAC: f32 = 0.18; // 18% of the box

/// Choose the extrusion height for a footprint ring so the scene reads as a 3D
/// city (buildings rise to their REAL height, land/water lie flat):
/// - a **building** ring rises to its real metric height
///   ([`building_design_height`]: `height_m` from tags else a low default), scaled
///   by the data's world span (`du_per_m`) — so the skyline is low + varied, never
///   uniform spikes. When the span is unknown (`du_per_m == None`, e.g. the
///   synthetic rabbit) it falls back to the legacy `fallback` design-unit lift so
///   the fixtures/unit tests are unchanged;
/// - **water / park / landuse / coastline** rings lie FLAT (`0.0`) — ground
///   cover, never extruded into a block;
/// - any other ring rises *unless* it is large relative to the design box
///   ([`FLAT_AREA_FRAC`]), in which case it flattens — this catches admin /
///   boundary mega-rings while keeping building-scale + synthetic fixtures tall.
fn prism_height_for(
    ring: &[(f32, f32)],
    kind: &str,
    height_m: Option<f32>,
    du_per_m: Option<f32>,
    osm_id: i64,
    fallback: f32,
) -> f32 {
    match kind {
        // A real OSM building → its REAL metric height, span-scaled. Without a
        // world scale (synthetic / degenerate data) fall back to the legacy lift.
        "building" => match du_per_m {
            Some(dpm) => building_design_height(height_m, dpm, osm_id, ring),
            None => fallback,
        },
        // Ground cover never rises.
        "water" | "park" | "coastline" => 0.0,
        // Any other risen ring: large vs the box (an admin / boundary mega-ring) →
        // FLAT (#11). Otherwise it rises, but the lift depends on whether we have a
        // real world scale:
        //  - REAL data (`du_per_m` is `Some`): a small untagged ring (a leisure
        //    pitch, a `man_made`, an untagged closed way) gets the SAME low metric
        //    default a tagless building gets — so it never towers as the old
        //    uniform-`fallback` spike (the bug was these rings using 0.22 du);
        //  - SYNTHETIC data (`du_per_m` is `None`, e.g. the rabbit fixture): keep
        //    the legacy `fallback` design-unit lift so the fixture stays tall (a
        //    fixture has no real metric height, and there is no span to scale by).
        _ => {
            let poly = open_ring(ring);
            let area = signed_area(&poly).abs();
            let box_area = 4.0f32; // [-1,1]² = 2×2
            if area > FLAT_AREA_FRAC * box_area {
                0.0
            } else {
                match du_per_m {
                    Some(dpm) => building_design_height(height_m, dpm, osm_id, ring),
                    None => fallback,
                }
            }
        }
    }
}

/// Public convenience: build a mesh from ways at a default height. Returns just
/// the mesh (the [`crate::Map3D`] constructor uses [`build`] for the tile count).
pub fn extrude_ways(ways: &[Way], height: f32) -> Mesh3 {
    build(ways, height).0
}

/// The **height distribution** of the extruded building prisms, in design units —
/// the readable-data emit that proves the skyline is low + varied (a city), not
/// uniform sky-high spikes. Computed from the SAME path [`build`] uses, so what
/// it reports is what the mesh extrudes. `box_span` (= 2.0, the `[-1,1]` extent)
/// lets the matrix assert `p95` is a small fraction of the box.
#[derive(Clone, Debug, Default)]
pub struct HeightStats {
    /// Number of building footprints that rose to a prism.
    pub buildings: usize,
    /// How many used a REAL OSM tag (`height` / `building:levels`).
    pub tagged: usize,
    /// How many fell back to the default low-building height.
    pub defaulted: usize,
    /// Design-unit height stats over the buildings.
    pub min: f32,
    pub median: f32,
    pub p95: f32,
    pub max: f32,
    /// The `[-1,1]` box span (2.0) the heights are measured against.
    pub box_span: f32,
    /// **The wall/footprint ratio — a building's wall height over its own
    /// footprint's LONG plan side**, median over the buildings. A number, not a
    /// taste: a house is wider than it is tall to the eave, so this sits well
    /// under 1. Reported because "the walls have been enlarged and the roofs look
    /// like they lean on the ground" is exactly this quantity going up.
    pub wall_over_footprint_median: f32,
    /// The worst (largest) wall/long-side ratio in the scene.
    pub wall_over_footprint_max: f32,
    /// **How many buildings are TALLER THAN THEY ARE LONG** (ratio > 1) — a wall
    /// that swallows its own roof. The RED-when-broken count: it was 77 of 182 on
    /// the demo's Sigtuna core before the untagged default was capped against the
    /// footprint ([`MAX_DEFAULT_WALL_OVER_FOOTPRINT`]).
    pub taller_than_long: usize,
    /// Median **roof rise over wall height** — how much of a building's silhouette
    /// is roof. A lid on a tall box tends to 0.
    pub roof_over_wall_median: f32,
}

impl HeightStats {
    /// The distribution as readable JSON (the "see what the mesh extrudes" emit).
    pub fn to_json(&self) -> serde_json::Value {
        serde_json::json!({
            "buildings": self.buildings,
            "tagged": self.tagged,
            "defaulted": self.defaulted,
            "min_du": self.min,
            "median_du": self.median,
            "p95_du": self.p95,
            "max_du": self.max,
            "box_span": self.box_span,
            "p95_frac_of_box": if self.box_span > 0.0 { self.p95 / self.box_span } else { 0.0 },
            "max_frac_of_box": if self.box_span > 0.0 { self.max / self.box_span } else { 0.0 },
            // The proportion oracle: a wall must not swallow its own roof.
            "wall_over_footprint_median": self.wall_over_footprint_median,
            "wall_over_footprint_max": self.wall_over_footprint_max,
            "taller_than_long": self.taller_than_long,
            "roof_over_wall_median": self.roof_over_wall_median,
        })
    }
}

/// Compute the [`HeightStats`] for `ways` (using the same world-span scaling +
/// per-building metric height [`build`] applies). `fallback` mirrors the
/// `height` arg [`build`] takes (used only when no world scale is available).
pub fn building_height_stats(ways: &[Way], fallback: f32) -> HeightStats {
    let Some(bb) = footprint_bbox(ways) else {
        return HeightStats { box_span: 2.0, ..Default::default() };
    };
    let du_per_m = design_units_per_metre(ways);
    let mut heights: Vec<f32> = Vec::new();
    let mut tagged = 0usize;
    // Wall/footprint + roof/wall ratios, measured on the SAME rings + the SAME
    // heights the extruder uses — so what this reports is what the mesh is.
    let mut wall_over_long: Vec<f32> = Vec::new();
    let mut roof_over_wall: Vec<f32> = Vec::new();
    let mut taller_than_long = 0usize;
    for w in ways {
        let ring: Vec<(f32, f32)> = w.points.iter().map(|&(x, y)| to_design(x, y, bb)).collect();
        if ring.len() < 2 || !is_prism(w) {
            continue;
        }
        // The height this ring actually extrudes to (the SAME call `build` makes).
        let h = prism_height_for(&ring, w.kind, w.height_m, du_per_m, w.osm_id, fallback);
        if h <= 1e-6 {
            continue; // flat ground cover (water / park / mega-ring) → not a building
        }
        if w.height_m.is_some() {
            tagged += 1;
        }
        heights.push(h);
        // Ratios need a real world scale (a synthetic fixture has none).
        if let Some(dpm) = du_per_m && let Some((_short_m, long_m)) = footprint_plan_metres(&ring, dpm) {
            let h_m = h / dpm;
            let r = h_m / long_m;
            if r > 1.0 {
                taller_than_long += 1;
            }
            wall_over_long.push(r);
            // The roof rise this footprint grows under the pitched path — the
            // SAME expression `roof_hipped_into` / `roof_gabled_into` use.
            let poly = open_ring(&ring);
            if poly.len() >= 3 {
                let n = poly.len() as f32;
                let (mut cx, mut cz) = (0.0f32, 0.0f32);
                for &(x, z) in &poly {
                    cx += x;
                    cz += z;
                }
                let centroid = (cx / n, cz / n);
                let rise = (DEFAULT_ROOF_PITCH * footprint_radius(&poly, centroid))
                    .min(ROOF_RIDGE_MAX_RISE_FACTOR * h);
                roof_over_wall.push(rise / h.max(1e-9));
            }
        }
    }
    let med = |v: &mut Vec<f32>| -> f32 {
        if v.is_empty() {
            return 0.0;
        }
        v.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
        v[v.len() / 2]
    };
    let wall_max = wall_over_long.iter().fold(0.0f32, |a, &b| a.max(b));
    let wall_med = med(&mut wall_over_long);
    let roof_med = med(&mut roof_over_wall);
    if heights.is_empty() {
        return HeightStats { box_span: 2.0, ..Default::default() };
    }
    heights.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
    let n = heights.len();
    let pct = |q: f32| heights[((q * (n as f32 - 1.0)).round() as usize).min(n - 1)];
    HeightStats {
        buildings: n,
        tagged,
        defaulted: n - tagged,
        min: heights[0],
        median: pct(0.5),
        p95: pct(0.95),
        max: heights[n - 1],
        box_span: 2.0,
        wall_over_footprint_median: wall_med,
        wall_over_footprint_max: wall_max,
        taller_than_long,
        roof_over_wall_median: roof_med,
    }
}

/// The terrain plane mesh alone (a single ground quad in the design box).
pub fn terrain_plane() -> Mesh3 {
    let mut m = Mesh3::default();
    terrain_into(&mut m);
    m
}

/// **Z-fight lift for flat ground cover.** The terrain plane sits at `y = 0`;
/// flat landuse / water / park / coastline caps (height ≈ 0) would land EXACTLY
/// coplanar with it, so the GPU depth test (`CompareFunction::Less`) flip-flops
/// per-pixel frame to frame — the **green ground flicker**. We lift every flat cap
/// a hair above the terrain (a few × the depth buffer's resolvable epsilon) so it
/// deterministically wins the depth test without reading as "floating". Tiny vs the
/// `[-1,1]` box (≈0.0006 of the half-extent) so it is sub-pixel in the render.
pub const GROUND_LIFT: f32 = 1.2e-3;

/// The **z-order layer** a flat ground feature draws in — the cartographic stack
/// (land fill UNDER roads) made depth-correct on the GPU depth buffer. Each layer
/// occupies a DISJOINT, ordered sub-band of [`GROUND_LIFT`] so features sharing the
/// ground plane never z-fight: `terrain(0) < Cover < Road`, all sub-pixel so they
/// still read as laid on the ground. This is the seam a richer korp-supplied layer
/// stack ([`crate::korp::FeatureLayers`]) drapes onto.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum GroundLayer {
    /// Landuse / park / natural area fill — the LOWEST ground layer (over terrain).
    Cover,
    /// Street / path / rail ribbons — drawn ABOVE cover (a road always wins the
    /// depth test over the landuse polygon it crosses).
    Road,
    /// **Host ink laid ON the basemap** — a case glow, an overprint trail, a graph
    /// edge — drawn above the whole road stack, sub-passes included. Lane GG #4.
    ///
    /// It is a band of its own and not a fourth road sub-pass, and that distinction is
    /// the fix: [`layer_lift_in`] jitters each feature by a hash of its first vertex over
    /// its band's whole `span`, so `Road`'s `[1.12, 1.22)` already overlaps its own
    /// stack's reach of `[1.18, 1.28)`. A pass step is `0.03` — three of them cannot clear
    /// a jitter span of `0.10`, so lifting overlay ink by "one more pass" would leave which
    /// of two features wins decided by a coordinate hash, merely with the odds reversed.
    /// A disjoint band is what this enum's own doc promised in the first place.
    Overlay,
}

impl GroundLayer {
    /// The [`GROUND_LIFT`]-relative `(base, span)` sub-band this layer occupies. The
    /// bands are disjoint and ordered (`Cover` = `[1.00,1.08)` < `Road` = `[1.12,1.22)` <
    /// `Overlay` = `[1.30,1.38)`) and all stay within `[0.9, 1.5]·GROUND_LIFT`.
    ///
    /// **`Overlay`'s floor is 1.30 and not 1.22** because a road is not one ribbon: it is
    /// three, stacked by [`road_pass_lift`] at `+0`, `+0.03` and `+0.06`, so the road
    /// stack's real ceiling is `1.22 + 0.06 = 1.28`. Disjointness has to be measured
    /// against what the layer actually reaches, not against the base band it is written
    /// from — the two differ by two pass steps, and that gap is exactly where overlay ink
    /// used to sink. `1.38` still clears `1.5` and stays under the route ribbon's
    /// [`ROUTE_LIFT_BANDS`] = `1.6`, so the cartographic stack keeps its order:
    /// cover < roads < host ink < your route.
    fn band(self) -> (f32, f32) {
        match self {
            GroundLayer::Cover => (1.00, 0.08),
            GroundLayer::Road => (1.12, 0.10),
            GroundLayer::Overlay => (1.30, 0.08),
        }
    }
}

/// Deterministic per-ring micro-lift WITHIN a feature's [`GroundLayer`] band, so two
/// overlapping features of the SAME class stratify AND different classes never
/// collide (roads strictly above cover). Hashes the ring's first vertex → a fraction
/// of the band. NO RNG (snapshot-stable). Always ≥ a hair so a lone feature clears
/// the terrain plane.
/// `band` is the scene's band height from [`ground_band`]; pass [`GROUND_LIFT`] for
/// the historical absolute band.
#[must_use]
pub fn layer_lift_in(ring: &[(f32, f32)], layer: GroundLayer, band: f32) -> f32 {
    let mut seed = 0x51ED_2701_A3C5_9E17u64;
    if let Some(&(x, z)) = ring.first() {
        seed ^= (x.to_bits() as u64).rotate_left(19) ^ (z.to_bits() as u64).rotate_left(41);
    }
    seed = seed.wrapping_mul(0x2545_F491_4F6C_DD1D);
    let frac = (seed >> 40) as f32 / (1u64 << 24) as f32; // [0,1)
    let (base, span) = layer.band();
    band * (base + span * frac)
}

/// [`layer_lift_in`] on the historical absolute [`GROUND_LIFT`] band — the
/// scale-free entry point (synthetic data, and callers with no scene scale).
#[must_use]
pub fn layer_lift(ring: &[(f32, f32)], layer: GroundLayer) -> f32 {
    layer_lift_in(ring, layer, GROUND_LIFT)
}

/// **The ground band's height expressed in METRES — the scale the buildings are
/// already measured in.**
///
/// [`GROUND_LIFT`] is an absolute design-unit constant while a building's rise is
/// `height_m · du_per_m` — metric, and therefore a function of how much ground the
/// scene spans. The two are unrelated quantities, so which of them is taller depends
/// entirely on the viewport, and past a few hundred metres of span the LAND WINS.
///
/// MEASURED on the real Sweden extract at central Stockholm — korp's own served
/// viewport, 2026-08-02, `korp/tests/map_render_truth.rs`: span 55 688 m ⇒
/// `du_per_m = 2.87e-5`; 641 buildings, median rise **2.05e-4** design units, against
/// a ground-cover cap at **1.30e-3** and road ribbons at **1.46e-3**. The land floats
/// **6.3×** over the median rooftop and the ribbons **7.1×**; even the tallest
/// building in the scene (1.67e-3) does not clear the ribbons. That is the recurring
/// *"the ground plane has risen and partly engulfs the buildings"* report, and
/// re-tuning an absolute constant cannot fix it — the defect is the mismatch of
/// UNITS, not the value.
///
/// So the band is declared in metres and converted through the scene's OWN
/// `du_per_m`, the same factor [`building_design_height`] uses. At `0.4 m` the cover
/// caps and road ribbons sit under half a metre of kerb, below every building rise the
/// extruder can produce ([`MIN_DEFAULT_BUILDING_HEIGHT_M`] is 2.5 m), so **cover <
/// road < roof holds by construction at any scene scale** instead of by luck at one.
pub const GROUND_BAND_METRES: f32 = 0.4;

/// Depth-safety floor for [`ground_band`], in design units. The band's whole job is to
/// win the depth test against the coplanar terrain, so it may shrink with the scene but
/// not to nothing. At a view distance of ~3 design units a `1e-5` separation is still
/// ~100× the `f32` `1/z` epsilon the CPU backend resolves with (`backend.rs`), and a
/// span wide enough for this floor to bite has sub-pixel buildings anyway.
///
/// **Honest bound:** where the floor bites — `du_per_m < 2.5e-5`, i.e. a span past
/// ~64 km — the guarantee weakens to "a ≥ 2.5 m building clears the band up to a
/// ~330 km span". A whole-country extrusion is still outside it.
pub const MIN_GROUND_BAND: f32 = 1.0e-5;

/// **The scene's ground-band height in design units** — [`GROUND_BAND_METRES`] taken
/// through the scene's own design-units-per-metre, clamped to `[MIN_GROUND_BAND,
/// GROUND_LIFT]` (never taller than the historical band, never below depth safety).
///
/// `du_per_m == None` is SYNTHETIC data with no world scale (the rabbit fixture, the
/// unit tests): there is no metre to convert, so the legacy [`GROUND_LIFT`] band is
/// kept and those scenes render byte-for-byte as before.
#[must_use]
pub fn ground_band(du_per_m: Option<f32>) -> f32 {
    match du_per_m {
        Some(dpm) if dpm > 0.0 => (GROUND_BAND_METRES * dpm).clamp(MIN_GROUND_BAND, GROUND_LIFT),
        _ => GROUND_LIFT,
    }
}

/// **The very top of the ground band** — the highest design-space `y` any FLAT feature
/// can reach in a scene whose band is `band`. Read off the highest [`GroundLayer`]
/// sub-band ([`GroundLayer::Road`]) rather than restated as a literal, so a change to
/// the stratification moves this with it (one writer for "where the ground ends").
#[must_use]
pub fn ground_top(band: f32) -> f32 {
    let (base, span) = GroundLayer::Road.band();
    band * (base + span)
}

/// **The rise that separates a BUILDING from the ground, in METRES.**
///
/// The scale-honest twin of [`GROUND_BAND_METRES`], and for the same reason. A
/// building's rise is `height_m · du_per_m` — metric, so it shrinks with the scene's
/// span — while the historical [`crate::BUILDING_RISE_EPS`] is an absolute design-unit
/// constant. Compare the two and the answer depends entirely on the viewport.
///
/// MEASURED on the shipped korp path, a real focused Xvfb window against the live
/// korp-server (2026-08-03, RTX 4090, Stockholm z12, `39 587` building ways /
/// `2 123 768` triangles): span ≈ 55.7 km ⇒ `du_per_m = 2.87e-5`, median building rise
/// `2.05e-4` design units — **one to two orders below `BUILDING_RISE_EPS = 4.0e-3`**.
/// Every risen-geometry observable therefore read **zero buildings** on a scene made
/// almost entirely of buildings, and `Map3D::frame_densest_cluster` took its
/// `risen.len() < 3` early return, leaving `camera.distance` bit-identical to its
/// `3.2` default. The defect is the mismatch of UNITS, not the value.
///
/// `1.0 m` sits by construction between the two things it must separate: the ground
/// band tops out at `1.22 · 0.4 m ≈ 0.5 m` of kerb (see [`ground_top`]), and the
/// shortest building the extruder can emit is [`MIN_DEFAULT_BUILDING_HEIGHT_M`] = 2.5 m.
/// So "a road-only scene reports zero buildings" and "a real house counts" both hold at
/// **any** scene span, instead of by luck at one.
pub const BUILDING_RISE_METRES: f32 = 1.0;

/// Clearance the rise threshold keeps over [`ground_top`] when the metric threshold
/// alone would sink into the ground band (only past a ~110 km span — see
/// [`building_rise_eps`]'s honest bound).
const RISE_OVER_GROUND: f32 = 1.2;

/// **The scene's building-rise threshold in design units** — [`BUILDING_RISE_METRES`]
/// taken through the scene's own design-units-per-metre, floored a clear
/// [`RISE_OVER_GROUND`] above the top of its ground band so a road ribbon can never
/// be counted as a house.
///
/// `du_per_m == None` is SYNTHETIC data with no world scale (the rabbit fixture, the
/// unit-test towns): there is no metre to convert, so the legacy absolute
/// [`crate::BUILDING_RISE_EPS`] is kept and those scenes report byte-for-byte the same
/// counts as before.
///
/// **Honest bound, in two stages** — [`ground_band`] stops shrinking at
/// [`MIN_GROUND_BAND`] (`du_per_m < 2.5e-5`, i.e. a span past ~64 km), so the floor
/// eventually stops tracking the scene. COMPUTED from the constants, and asserted in
/// `tests/region_span_buildings_rise.rs`:
///
/// - past a **~109 km** span the threshold stops meaning exactly "1 m" and starts
///   meaning "a clear margin above the kerb" — it is then *looser* than 1 m, so no
///   building is lost, only the physical reading of the number goes soft;
/// - past a **~273 km** span the floor finally exceeds even the shortest building the
///   extruder can emit ([`MIN_DEFAULT_BUILDING_HEIGHT_M`] · `du_per_m`), and a minimum-
///   height house would be counted as ground. At that span such a house is `2.4e-6`
///   design units in a box of `2.0` — six orders down, far past anything the `f32`
///   depth buffer or a pixel could resolve — so the classification is moot, but it is
///   the point where this function stops being able to tell a house from a kerb.
///
/// Every korp viewport measured so far (z12 Stockholm, ~55.7 km) is inside both.
#[must_use]
pub fn building_rise_eps(du_per_m: Option<f32>) -> f32 {
    match du_per_m {
        Some(dpm) if dpm > 0.0 => {
            (BUILDING_RISE_METRES * dpm).max(RISE_OVER_GROUND * ground_top(ground_band(du_per_m)))
        }
        _ => crate::BUILDING_RISE_EPS,
    }
}

/// [`building_rise_eps`] for a `ways` slice, derived through the **same**
/// `DESIGN_FILL / world_span_metres` the extruder scales building heights by — so the
/// threshold that decides "is this a building?" and the height that answers it can
/// never be computed from two different scales.
#[must_use]
pub fn building_rise_eps_for(ways: &[Way]) -> f32 {
    building_rise_eps(design_units_per_metre(ways))
}

/// The flat-cover lift (the [`GroundLayer::Cover`] band) — the clearance a landuse /
/// park / water cap gets over the terrain plane, in the scene's own `band`.
fn ground_lift_for(ring: &[(f32, f32)], band: f32) -> f32 {
    layer_lift_in(ring, GroundLayer::Cover, band)
}

/// **The colour of the ground plane — read from the map palette, not invented here.**
///
/// There were two writers of "what colour is land". 2D paints the whole landmass in
/// [`facett_map::MapPalette::land`] (`#F2EFE9`, paper) via `LandmassLayer`; 3D's
/// terrain quad was a hard-coded `rgb(38, 44, 40)` — a near-black green that had never
/// heard of the palette. Same ways, same theme, two different grounds, and the 3D one
/// was the reason the pane read as a dark slab with cover floating on it.
///
/// One writer (LAW 5): the palette. `day` matches the palette korp classifies its ways
/// with (`map_theme().palette`), so the plane and the features on it can never come
/// from two different themes.
#[must_use]
pub fn terrain_colour(day: bool) -> Color32 {
    facett_map::MapPalette::cartographic(day).land
}

/// Append the ground quad (`y = 0`, normal +y) covering the design box, in the
/// palette's land colour ([`terrain_colour`]).
fn terrain_into(mesh: &mut Mesh3) {
    terrain_into_coloured(mesh, terrain_colour(true));
}

/// [`terrain_into`] with an explicit ground colour — the seam a host with a night
/// palette (or a test asserting the plane's colour) paints through.
fn terrain_into_coloured(mesh: &mut Mesh3, ground: Color32) {
    let n = [0.0, 1.0, 0.0];
    let g = 1.05; // a touch larger than the data box
    let a = mesh.push_vertex([-g, 0.0, -g], n);
    let b = mesh.push_vertex([g, 0.0, -g], n);
    let c = mesh.push_vertex([g, 0.0, g], n);
    let d = mesh.push_vertex([-g, 0.0, g], n);
    // CCW from above (+y) → front-facing after the camera tilt.
    mesh.push_tri([a, c, b], ground);
    mesh.push_tri([a, d, c], ground);
}

/// The maximum vertical relief (design units, ±) the [`relief_terrain_into`] ground
/// heightfield is allowed to span. Bounded a hair above a typical readable building
/// height so the topography reads under the skyline without towering over it or
/// escaping the `[-1, 1]` design box. The metric height scale is auto-fit down to
/// this ceiling so a 600 m procedural range and a 40 m town DEM both land in-box.
pub const MAX_TERRAIN_RELIEF: f32 = 0.30;

/// The **data's WGS-84 lon/lat bounding box** `(lon0, lat0, lon1, lat1)` from the
/// ways' `lonlat`, or `None` for empty data. The relief mesh samples elevation over
/// this box (mapped onto the design grid) so real topography lands under the city.
#[must_use]
pub fn lonlat_bbox(ways: &[Way]) -> Option<(f64, f64, f64, f64)> {
    let (mut lon0, mut lat0) = (f64::MAX, f64::MAX);
    let (mut lon1, mut lat1) = (f64::MIN, f64::MIN);
    let mut any = false;
    for w in ways {
        for &(lon, lat) in &w.lonlat {
            lon0 = lon0.min(lon);
            lat0 = lat0.min(lat);
            lon1 = lon1.max(lon);
            lat1 = lat1.max(lat);
            any = true;
        }
    }
    any.then_some((lon0, lat0, lon1, lat1))
}

/// A sampled **ground-relief heightfield** over the design box `[-g, g]²`: the placed
/// design-space `y` at each of the `(n+1)²` grid vertices (the SAME `y` values
/// [`relief_terrain_into`] gives the rendered terrain). Lets a building's base be lifted
/// onto the terrain surface at its footprint (see [`ReliefField::height_at`]) instead of
/// every building being pinned to `y = 0` while the relief rises up THROUGH them — the
/// "houses look half-buried / the land covers the lower walls" bug. `Flat` terrain has no
/// field (buildings keep the byte-stable `y = 0` base).
struct ReliefField {
    /// Grid subdivisions per side (`n + 1` vertices per axis).
    n: usize,
    /// Half-extent of the terrain grid in design units (matches [`terrain_into`]).
    g: f32,
    /// Placed design-space `y` per grid vertex, row-major `j * (n + 1) + i`.
    ys: Vec<f32>,
}

impl ReliefField {
    /// Bilinearly sample the **ground height** (design-space `y`) at a design-plane point
    /// `(x, z)` (both in `[-g, g]`). A footprint placed at this `(x, z)` and lifted by this
    /// height sits flush ON the rendered terrain, so the land never rises over its walls.
    fn height_at(&self, x: f32, z: f32) -> f32 {
        let n = self.n;
        let g = self.g;
        // design [-g, g] → grid coordinate [0, n], clamped so off-box footprints read the edge.
        let fx = (((x + g) / (2.0 * g)) * n as f32).clamp(0.0, n as f32);
        let fz = (((z + g) / (2.0 * g)) * n as f32).clamp(0.0, n as f32);
        let i0 = (fx.floor() as usize).min(n);
        let j0 = (fz.floor() as usize).min(n);
        let i1 = (i0 + 1).min(n);
        let j1 = (j0 + 1).min(n);
        let tx = fx - i0 as f32;
        let tz = fz - j0 as f32;
        let at = |i: usize, j: usize| self.ys[j * (n + 1) + i];
        let a = at(i0, j0) + (at(i1, j0) - at(i0, j0)) * tx;
        let b = at(i0, j1) + (at(i1, j1) - at(i0, j1)) * tx;
        a + (b - a) * tz
    }

    /// The **terrain span under a footprint** `(hi, lo)`: the max and min ground height
    /// over the footprint. A prism lifted so its eave datum is `hi` and its foundation
    /// reaches `lo` spans the whole ground under it — no wall is buried (all terrain ≤
    /// eave) and none floats (the skirt reaches the lowest ground).
    ///
    /// # Sampling the ring's VERTICES alone was not enough
    /// Until 2026-08-02 this sampled `height_at` at each ring vertex only, and claimed
    /// on that basis that "no wall is buried". The terrain is a bilinear grid, so a
    /// ridge or grid node **inside** the footprint — or along an edge, between two
    /// vertices — is routinely higher than every corner. Where that happened the land
    /// rose over the walls and swallowed the lower storeys, which is the recurring
    /// *"houses are sunk into the land"* report. It appears **partly**, on some
    /// buildings and not others, because it needs terrain that peaks between the
    /// samples — so it never looked like a systematic error, and every fix aimed at
    /// `GROUND_LIFT` (1.2e-3, sub-pixel) missed it entirely.
    ///
    /// The span therefore samples more than the vertices — but it must sample the
    /// **FOOTPRINT**, and between 2026-08-02 03:51 and this commit it sampled the
    /// footprint's **BOUNDING BOX**, which is not the same set and is the reason the OSM
    /// 3D view got worse rather than better.
    ///
    /// # Why the bounding box was the wrong set
    /// A bbox equals its ring only for an axis-aligned rectangle. For an L-shape, a
    /// triangle, or any ring running diagonally, the box reaches ground the building does
    /// not stand on; where it clips a nearby ridge, that ridge becomes the eave datum and
    /// the prism is lifted off terrain it never touches. **Flat ground cover suffers
    /// worst** — parks, landuse and coastline rings are the largest rings in the scene and
    /// their cap is drawn *at* the datum (`height ≈ 0`), so the land itself floats up.
    /// Measured on a synthetic multi-octave relief (400 random rings, terrain range 0.60
    /// design units): the bbox bound lifted small building footprints by 0.013 on average
    /// but 16-cell ground-cover rings by 0.047 — **3.5× more**, worst case 0.19. A default
    /// 6 m building is ~0.006 design units at Sigtuna scale, so the land outran the
    /// rooftops by several building heights. Reported as *"houses are barely visible, the
    /// ground is higher than the houses"*.
    ///
    /// # The set that is actually bounded
    /// `hi` is the max over the ring's **vertices**, the grid **nodes inside the ring**,
    /// and the points where the ring's **edges cross grid lines**. The last two are what
    /// vertex-only sampling missed: a bilinear surface takes its extremes at cell corners,
    /// so a peak strictly between two vertices is either at an interior node or on a cell
    /// boundary an edge crosses. None of the three reaches outside the footprint.
    ///
    /// `lo` stays the conservative bbox minimum on purpose: it is the foundation, it is
    /// buried, and being too deep is invisible while being too shallow floats the skirt.
    ///
    /// Bounded by the grid (worst case `(n+1)²` node tests plus `O(edges · n)` crossings)
    /// and it runs once per mesh build, never per frame.
    fn footprint_span(&self, ring: &[(f32, f32)]) -> (f32, f32) {
        if ring.is_empty() {
            return (0.0, 0.0);
        }
        let (n, g) = (self.n, self.g);
        // design [-g, g] → grid coordinate [0, n], the SAME mapping `height_at` uses.
        let to_grid = |v: f32| (((v + g) / (2.0 * g)) * n as f32).clamp(0.0, n as f32);
        // …and back, so a grid index names the design-space point to test for containment.
        let to_design = |k: usize| (k as f32 / n as f32) * 2.0 * g - g;

        let (mut x0, mut x1, mut z0, mut z1) = (f32::MAX, f32::MIN, f32::MAX, f32::MIN);
        for &(x, z) in ring {
            x0 = x0.min(x);
            x1 = x1.max(x);
            z0 = z0.min(z);
            z1 = z1.max(z);
        }
        let i0 = to_grid(x0).floor() as usize;
        let i1 = (to_grid(x1).ceil() as usize).min(n);
        let j0 = to_grid(z0).floor() as usize;
        let j1 = (to_grid(z1).ceil() as usize).min(n);

        let mut lo = f32::MAX;
        let mut hi = f32::MIN;

        // ── the ring's own vertices: always in the footprint, by definition. ──
        for &(x, z) in ring {
            let y = self.height_at(x, z);
            lo = lo.min(y);
            hi = hi.max(y);
        }

        // ── grid nodes: `lo` takes them all (buried, conservative is free); `hi` takes
        //    only the ones the ring actually encloses. ──
        for j in j0..=j1 {
            for i in i0..=i1 {
                let y = self.ys[j * (n + 1) + i];
                lo = lo.min(y);
                if point_in_polygon((to_design(i), to_design(j)), ring) {
                    hi = hi.max(y);
                }
            }
        }

        // ── where each edge crosses a grid line. On a cell boundary the bilinear surface
        //    is linear, so its max along an edge segment sits at a vertex or at one of
        //    these crossings — this is what catches a ridge peaking BETWEEN two vertices
        //    without ever leaving the footprint. ──
        for w in 0..ring.len() {
            let (ax, az) = ring[w];
            let (bx, bz) = ring[(w + 1) % ring.len()];
            let mut cross = |ga: f32, gb: f32| {
                let (from, to) = if ga <= gb { (ga, gb) } else { (gb, ga) };
                for k in (from.ceil() as usize)..=(to.floor().max(0.0) as usize).min(n) {
                    let t = if (gb - ga).abs() > 1e-9 { (k as f32 - ga) / (gb - ga) } else { continue };
                    if (0.0..=1.0).contains(&t) {
                        let y = self.height_at(ax + (bx - ax) * t, az + (bz - az) * t);
                        lo = lo.min(y);
                        hi = hi.max(y);
                    }
                }
            };
            cross(to_grid(ax), to_grid(bx)); // vertical grid lines
            cross(to_grid(az), to_grid(bz)); // horizontal grid lines
        }

        if lo.is_finite() && hi.is_finite() { (hi, lo) } else { (0.0, 0.0) }
    }
}

/// Compute the [`ReliefField`] for `ways`/`elev` — the design-space `y` grid the
/// relief terrain mesh is built from AND that buildings sample to sit on the ground.
/// Single source of truth for the terrain `y` so the mesh and the building bases can
/// never disagree (the fix for "the ground covers the lower half of the buildings").
fn relief_heightfield(ways: &[Way], elev: &dyn crate::terrain::Elevation, subdiv: usize, exaggeration: f32) -> ReliefField {
    let n = subdiv.max(2);
    let g = 1.05f32; // matches the flat terrain quad's extent
    let geo = lonlat_bbox(ways);
    // Design-units-per-metre from the data's world span (the SAME scale buildings use);
    // for span-less/degenerate data the ONE shared fallback, which is `lod`'s default
    // scale read backwards — not a private literal that lets the terrain and the
    // altitude lever believe two different scenes.
    let du_per_m = design_units_per_metre(ways).unwrap_or(DEFAULT_DESIGN_UNITS_PER_METRE);

    // Map a grid fraction (tx east→, tz far→near) to the `(lon, lat)` to sample. With
    // real data this is the ways' lon/lat box (north = far = −z, matching `to_design`);
    // otherwise a small deterministic patch so the procedural field still gives relief.
    let sample_lonlat = |tx: f64, tz: f64| -> (f64, f64) {
        match geo {
            Some((lon0, lat0, lon1, lat1)) => (lon0 + (lon1 - lon0) * tx, lat1 - (lat1 - lat0) * tz),
            None => (17.0 + tx * 0.05, 59.0 + (1.0 - tz) * 0.05),
        }
    };

    let verts = (n + 1) * (n + 1);
    let vidx = |i: usize, j: usize| j * (n + 1) + i;

    // ── pass 1: raw metric heights + their mean (the datum we centre on) ──
    let mut hm = vec![0.0f32; verts];
    let mut sum = 0.0f64;
    for j in 0..=n {
        let tz = j as f64 / n as f64;
        for i in 0..=n {
            let tx = i as f64 / n as f64;
            let (lon, lat) = sample_lonlat(tx, tz);
            let h = elev.elevation_at(lon, lat);
            hm[vidx(i, j)] = h;
            sum += f64::from(h);
        }
    }
    let mean = (sum / verts as f64) as f32;

    // Auto-fit the metric scale so the peak deviation lands within the in-box relief
    // ceiling (procedural 600 m and a 40 m town DEM both fit); never exaggerate beyond
    // the metric scale the buildings use.
    let peak_m = hm.iter().fold(0.0f32, |a, &h| a.max((h - mean).abs()));
    let raw_scale = du_per_m * exaggeration.max(0.0);
    let scale = if peak_m > 1e-3 { raw_scale.min(MAX_TERRAIN_RELIEF / peak_m) } else { 0.0 };

    // ── pass 2: the placed design-space y per vertex (mean-centred, in-box) ──
    let ys: Vec<f32> = hm.iter().map(|&h| ((h - mean) * scale).clamp(-MAX_TERRAIN_RELIEF, MAX_TERRAIN_RELIEF)).collect();
    ReliefField { n, g, ys }
}

/// **Replace the flat ground quad with a real heightfielded terrain mesh** (roadmap
/// §6, ported from the flight engine): a subdivided `(subdiv+1)²` grid over the design
/// box `[-g, g]²`, each vertex lifted to a **real elevation** sampled from `elev` (a
/// DEM or the procedural fallback) at the `(lon, lat)` the design grid maps onto. The
/// metric relief is mean-centred (so the city still sits around `y = 0`), scaled by the
/// data's design-units-per-metre × `exaggeration`, and **auto-fit** so the peak relief
/// never exceeds [`MAX_TERRAIN_RELIEF`] (keeps procedural 600 m ranges + town DEMs both
/// in-box). Per-vertex normals from central differences drive the painter's lighting so
/// the slopes actually shade; a height tint (low = ground green, high = pale rock) makes
/// the relief read. Winding matches [`terrain_into`] (front-facing from above). Counts
/// as ONE terrain feature (returns `1`) like the flat quad, so tile counts are stable.
pub fn relief_terrain_into(mesh: &mut Mesh3, ways: &[Way], elev: &dyn crate::terrain::Elevation, subdiv: usize, exaggeration: f32) -> usize {
    let field = relief_heightfield(ways, elev, subdiv, exaggeration);
    push_relief_mesh(mesh, &field)
}

/// Push the relief grid geometry (vertices + height tint + central-difference normals +
/// two triangles per cell) for a computed [`ReliefField`]. Splitting the `y` computation
/// ([`relief_heightfield`]) from the mesh emission lets buildings sample the SAME field to
/// sit on the ground without ever recomputing (or drifting from) the rendered surface.
fn push_relief_mesh(mesh: &mut Mesh3, field: &ReliefField) -> usize {
    let (n, g) = (field.n, field.g);
    let verts = (n + 1) * (n + 1);
    let vidx = |i: usize, j: usize| j * (n + 1) + i;

    // ── place vertices (design x/z + the field's real height y) + height tint ──
    let base0 = mesh.positions.len() as u32;
    let low = Color32::from_rgb(38, 44, 40); // valley / ground green (the old flat colour)
    let high = Color32::from_rgb(120, 126, 120); // pale rock / ridge
    let mut vcol = vec![low; verts];
    let up = [0.0f32, 1.0, 0.0];
    for j in 0..=n {
        let z = -g + 2.0 * g * (j as f32 / n as f32);
        for i in 0..=n {
            let x = -g + 2.0 * g * (i as f32 / n as f32);
            let y = field.ys[vidx(i, j)];
            // Height tint fraction over the actual relief band (finite even if flat).
            let t = ((y / MAX_TERRAIN_RELIEF) * 0.5 + 0.5).clamp(0.0, 1.0);
            vcol[vidx(i, j)] = Color32::from_rgb(
                (low.r() as f32 + (high.r() as f32 - low.r() as f32) * t) as u8,
                (low.g() as f32 + (high.g() as f32 - low.g() as f32) * t) as u8,
                (low.b() as f32 + (high.b() as f32 - low.b() as f32) * t) as u8,
            );
            mesh.push_vertex([x, y, z], up); // normal fixed up in pass 3
        }
    }

    // ── pass 3: per-vertex normals from central differences of the placed grid ──
    for j in 0..=n {
        for i in 0..=n {
            let p = |a: usize, b: usize| mesh.positions[(base0 as usize) + vidx(a, b)];
            let l = p(i.saturating_sub(1), j);
            let r = p((i + 1).min(n), j);
            let d = p(i, j.saturating_sub(1));
            let u = p(i, (j + 1).min(n));
            let du = [r[0] - l[0], r[1] - l[1], r[2] - l[2]];
            let dv = [u[0] - d[0], u[1] - d[1], u[2] - d[2]];
            // du (east) × dv (near) → points up for this west→east / far→near layout.
            let mut nrm = [du[1] * dv[2] - du[2] * dv[1], du[2] * dv[0] - du[0] * dv[2], du[0] * dv[1] - du[1] * dv[0]];
            let len = (nrm[0] * nrm[0] + nrm[1] * nrm[1] + nrm[2] * nrm[2]).sqrt();
            if len > 1e-9 {
                let s = if nrm[1] < 0.0 { -1.0 / len } else { 1.0 / len };
                nrm = [nrm[0] * s, nrm[1] * s, nrm[2] * s];
                mesh.normals[(base0 as usize) + vidx(i, j)] = nrm;
            }
        }
    }

    // ── triangles: two per cell, winding matching `terrain_into` (front-facing up) ──
    for j in 0..n {
        for i in 0..n {
            let v00 = base0 + vidx(i, j) as u32;
            let v10 = base0 + vidx(i + 1, j) as u32;
            let v01 = base0 + vidx(i, j + 1) as u32;
            let v11 = base0 + vidx(i + 1, j + 1) as u32;
            // Per-triangle colour = one corner's tint (cheap, reads the height band).
            mesh.push_tri([v00, v11, v10], vcol[vidx(i + 1, j + 1)]);
            mesh.push_tri([v00, v01, v11], vcol[vidx(i, j + 1)]);
        }
    }
    1
}

/// Strip a ring's trailing duplicate-of-first closing vertex (OSM rings repeat
/// the first point as the last). Ear-clipping wants the open vertex list.
fn open_ring(ring: &[(f32, f32)]) -> Vec<(f32, f32)> {
    let mut v = ring.to_vec();
    while v.len() >= 2 {
        let (a, b) = (v[0], v[v.len() - 1]);
        if (a.0 - b.0).abs() < 1e-9 && (a.1 - b.1).abs() < 1e-9 {
            v.pop();
        } else {
            break;
        }
    }
    v
}

/// Signed area of a polygon (shoelace) in the `(x, z)` ground plane. Positive =
/// CCW, negative = CW. Magnitude is the footprint area in design units².
fn signed_area(poly: &[(f32, f32)]) -> f32 {
    let n = poly.len();
    if n < 3 {
        return 0.0;
    }
    let mut a = 0.0f32;
    for i in 0..n {
        let (x0, z0) = poly[i];
        let (x1, z1) = poly[(i + 1) % n];
        a += x0 * z1 - x1 * z0;
    }
    a * 0.5
}

/// Is point `p` strictly inside triangle `a,b,c` (CCW)? Used by the ear-clip to
/// reject ears that would swallow a reflex vertex.
fn point_in_tri(p: (f32, f32), a: (f32, f32), b: (f32, f32), c: (f32, f32)) -> bool {
    // Barycentric sign test.
    let d1 = (p.0 - b.0) * (a.1 - b.1) - (a.0 - b.0) * (p.1 - b.1);
    let d2 = (p.0 - c.0) * (b.1 - c.1) - (b.0 - c.0) * (p.1 - c.1);
    let d3 = (p.0 - a.0) * (c.1 - a.1) - (c.0 - a.0) * (p.1 - a.1);
    let has_neg = d1 < 0.0 || d2 < 0.0 || d3 < 0.0;
    let has_pos = d1 > 0.0 || d2 > 0.0 || d3 > 0.0;
    !(has_neg && has_pos)
}

/// Cross product z of `(b-a)×(c-b)` — convex turn test for a CCW polygon.
fn cross_z(a: (f32, f32), b: (f32, f32), c: (f32, f32)) -> f32 {
    (b.0 - a.0) * (c.1 - b.1) - (b.1 - a.1) * (c.0 - b.0)
}

/// **Ear-clip triangulate** a simple (possibly concave) polygon given as a CCW
/// open vertex list, returning local-index triangles `[i, j, k]` into `poly`.
/// Standard O(n²) ear clipping — correct for concave footprints, NO fabricated
/// centroid vertex (the centroid-fan is what drew the OSM starburst). Returns an
/// empty Vec for a degenerate ring (< 3 verts or ~zero area / collinear).
fn ear_clip(poly: &[(f32, f32)]) -> Vec<[usize; 3]> {
    let n = poly.len();
    if n < 3 {
        return Vec::new();
    }
    if signed_area(poly).abs() < 1e-12 {
        return Vec::new(); // collinear / zero-area → no cap
    }
    // Working list of vertex indices in CCW order.
    let mut idx: Vec<usize> = (0..n).collect();
    let mut tris: Vec<[usize; 3]> = Vec::with_capacity(n - 2);
    let mut guard = 0usize;
    let guard_max = n * n + 16;
    while idx.len() > 3 {
        guard += 1;
        if guard > guard_max {
            // No ear found in a full sweep (numerically nasty ring) — fan the
            // remainder over its FIRST vertex. This is a bounded fan over the
            // ring's own remaining verts (never a global centroid), so it cannot
            // span the design box.
            for k in 1..idx.len() - 1 {
                tris.push([idx[0], idx[k], idx[k + 1]]);
            }
            return tris;
        }
        let m = idx.len();
        let mut clipped = false;
        for i in 0..m {
            let ia = idx[(i + m - 1) % m];
            let ib = idx[i];
            let ic = idx[(i + 1) % m];
            let (a, b, c) = (poly[ia], poly[ib], poly[ic]);
            // Convex vertex? (CCW: positive cross).
            if cross_z(a, b, c) <= 0.0 {
                continue;
            }
            // No other vertex inside this candidate ear?
            let mut ear = true;
            for &ip in &idx {
                if ip == ia || ip == ib || ip == ic {
                    continue;
                }
                if point_in_tri(poly[ip], a, b, c) {
                    ear = false;
                    break;
                }
            }
            if ear {
                tris.push([ia, ib, ic]);
                idx.remove(i);
                clipped = true;
                break;
            }
        }
        if !clipped {
            // Degenerate sweep without progress — bail with a remainder fan.
            for k in 1..idx.len() - 1 {
                tris.push([idx[0], idx[k], idx[k + 1]]);
            }
            return tris;
        }
    }
    if idx.len() == 3 {
        tris.push([idx[0], idx[1], idx[2]]);
    }
    tris
}

/// Extrude a closed ring `(dx, dz)` (design-plane footprint) into a prism: an
/// **ear-clip-triangulated** top cap at `y = height`, a bottom cap at `y = 0`,
/// and side walls. Caps use ONLY the ring's own vertices (no centroid fan — that
/// fan drew the OSM "starburst" on large concave rings). When `height` is ~0 the
/// prism collapses to a single flat ground cap (large area rings → ground cover,
/// not towers); see [`prism_height_for`].
fn extrude_prism_banded(mesh: &mut Mesh3, ring: &[(f32, f32)], height: f32, col: Color32, band: f32) {
    extrude_prism_based(mesh, ring, height, col, 0.0, 0.0, band);
}

fn extrude_prism(mesh: &mut Mesh3, ring: &[(f32, f32)], height: f32, col: Color32) {
    // Flat ground (`y = 0`) base — the byte-stable historical geometry.
    extrude_prism_based(mesh, ring, height, col, 0.0, 0.0, GROUND_LIFT);
}

/// Like [`extrude_prism`], but the prism sits on **uneven ground**: its eave/top datum is
/// `base_y` (the highest ground under the footprint) and its bottom/foundation is `foot_y`
/// (the lowest), so over relief terrain the land never rises over the lower walls (the
/// "houses half-buried in the ground" bug). `base_y == foot_y == 0` reproduces
/// [`extrude_prism`] byte-for-byte (the flat-terrain / golden path).
fn extrude_prism_based(
    mesh: &mut Mesh3,
    ring: &[(f32, f32)],
    height: f32,
    col: Color32,
    base_y: f32,
    foot_y: f32,
    band: f32,
) {
    // Open the ring (drop the OSM closing-duplicate) + normalise to CCW so the
    // ear-clip + cap normals are consistent.
    let mut poly = open_ring(ring);
    if poly.len() < 3 {
        return; // degenerate → skip cap (and there are no walls to draw)
    }
    if signed_area(&poly) < 0.0 {
        poly.reverse(); // force CCW
    }
    let cap_tris = ear_clip(&poly);
    if cap_tris.is_empty() {
        return; // collinear / zero-area ring → no geometry
    }
    let n = poly.len();
    let flat = height.abs() < 1e-5;

    // A flat ground-cover cap (landuse / water / park / coastline → height ≈ 0)
    // must NOT sit coplanar with the terrain (plane at y = 0, or the relief surface at
    // `base_y`), or the GPU depth test z-fights and the ground flickers green. Lift it a
    // deterministic hair above the terrain (still sub-pixel) so it wins the depth test.
    // Risen prisms rise `height` above their ground datum `base_y`.
    let cap_y = if flat { base_y + ground_lift_for(&poly, band) } else { base_y + height };
    // Walls/foundation drop to the lowest ground under the footprint (flush on a slope).
    let bottom_y = foot_y;

    // ── top cap (y = cap_y, normal +y) — ear-clipped, CCW from +y ──
    let up = [0.0, 1.0, 0.0];
    let top0 = mesh.positions.len() as u32;
    for &(x, z) in &poly {
        mesh.push_vertex([x, cap_y, z], up);
    }
    for &[i, j, k] in &cap_tris {
        mesh.push_tri([top0 + i as u32, top0 + j as u32, top0 + k as u32], col);
    }

    if flat {
        // A flat area patch (ground cover): one single-sided cap, no walls, no
        // separate bottom — it lies just above the ground so it reads as land, not
        // a box, but clears the terrain plane in depth (no z-fight flicker).
        return;
    }

    // ── bottom cap (y = bottom_y, normal -y) — same triangulation, reversed winding ──
    let down = [0.0, -1.0, 0.0];
    let bot0 = mesh.positions.len() as u32;
    for &(x, z) in &poly {
        mesh.push_vertex([x, bottom_y, z], down);
    }
    for &[i, j, k] in &cap_tris {
        // reverse winding so the bottom face points -y
        mesh.push_tri([bot0 + i as u32, bot0 + k as u32, bot0 + j as u32], col);
    }

    // ── side walls: a quad per rim edge between top & bottom rims ──
    //
    // FIX 3 — **flat-shaded walls, no diagonal seam.** Each wall quad gets ONE flat
    // outward face-normal (the edge's perpendicular in the ground plane), and the
    // quad's four corners are emitted as **its own** vertices carrying that single
    // normal — so the quad's two triangles share one normal and shade as a uniform
    // lit face (no visible diagonal). The OLD code shared rim vertices between
    // adjacent walls and gave each a per-vertex *radial* (centroid-relative) normal;
    // the two triangles of one quad then interpolated/averaged DIFFERENT normals →
    // a lighting seam exactly along the split diagonal. We also derive the normal
    // from the edge direction (CCW ring ⇒ outward is `dir × up`), so winding +
    // normal agree and the face never back-culls a half.
    let centroid = {
        let (mut cx, mut cz) = (0.0f32, 0.0f32);
        for &(x, z) in &poly {
            cx += x;
            cz += z;
        }
        (cx / n as f32, cz / n as f32)
    };
    let wall_col = Color32::from_rgb(
        (col.r() as f32 * 0.82) as u8,
        (col.g() as f32 * 0.82) as u8,
        (col.b() as f32 * 0.82) as u8,
    );
    for i in 0..n {
        let (x0, z0) = poly[i];
        let (x1, z1) = poly[(i + 1) % n];
        // Edge direction in the ground plane; outward perpendicular (rotate −90°),
        // then flip toward the centroid-away side so it is unambiguously outward
        // (robust even if a ring locally reverses on a near-degenerate edge).
        let (ex, ez) = (x1 - x0, z1 - z0);
        let elen = (ex * ex + ez * ez).sqrt().max(1e-6);
        let (mut nx, mut nz) = (ez / elen, -ex / elen); // perpendicular
        // Edge midpoint → away from centroid is the outward sense.
        let (mx, mz) = ((x0 + x1) * 0.5 - centroid.0, (z0 + z1) * 0.5 - centroid.1);
        if nx * mx + nz * mz < 0.0 {
            nx = -nx;
            nz = -nz;
        }
        let nrm = [nx, 0.0, nz];
        // This quad's OWN four corners, all carrying the single flat face normal.
        // Top rides the ground datum + height (`cap_y`), bottom drops to `bottom_y`.
        let i0t = mesh.push_vertex([x0, cap_y, z0], nrm);
        let i0b = mesh.push_vertex([x0, bottom_y, z0], nrm);
        let i1t = mesh.push_vertex([x1, cap_y, z1], nrm);
        let i1b = mesh.push_vertex([x1, bottom_y, z1], nrm);
        // Outward-facing CCW winding (consistent across both triangles): viewed from
        // outside (along −nrm into the face) the order top0→top1→bot0 / top1→bot1→bot0
        // turns the same way, so neither triangle is back-culled (no half-quad gap).
        mesh.push_tri([i0t, i1t, i0b], wall_col);
        mesh.push_tri([i1t, i1b, i0b], wall_col);
    }
}

// ─────────────────────────────────────────────────────────────────────────────────
// Pitched-roof + OSM-colour extrusion (round 2: houses read as real buildings).
// Additive + golden-safe: only reached from `build_with_water_terrain_styled` under
// `RoofStyle::Pitched`; the historical flat-cap `extrude_prism` above is untouched, so
// the default path (and every pixel golden) is byte-identical (L2).
// ─────────────────────────────────────────────────────────────────────────────────

/// The resolved per-building **prism style** the pitched path extrudes with: the roof
/// silhouette (already defaulted for untagged/large footprints), the facade + roof
/// colours (OSM tag else a tasteful default), and the ridge pitch.
#[derive(Clone, Copy, Debug)]
pub struct PrismStyle {
    /// The resolved roof silhouette (Flat / Gabled / Hipped).
    pub shape: RoofShape,
    /// Facade (wall) colour — `building:colour` else the themed fill.
    pub wall_col: Color32,
    /// Roof colour — `roof:colour`/`roof:material` else a class default.
    pub roof_col: Color32,
    /// Ridge/apex rise as a fraction of the footprint half-width.
    pub pitch: f32,
}

/// A deterministic, tasteful **default roof tint** for a building with no OSM roof
/// colour — a small varied palette keyed on the footprint id so a fully-untagged town
/// gets a warm, varied roofscape (terracotta / slate / brown / grey-green) instead of
/// one monochrome fill. No RNG → snapshot-stable.
fn default_roof_tint(osm_id: i64) -> Color32 {
    const PALETTE: [(u8, u8, u8); 5] = [
        (176, 92, 66),  // terracotta tile (the dominant Nordic/European roof)
        (150, 74, 58),  // deep clay red
        (96, 100, 108), // slate grey
        (120, 96, 72),  // weathered brown
        (104, 120, 96), // patinated copper-green
    ];
    let mut seed = (osm_id as u64) ^ 0xD1B5_4A32_D192_ED03;
    seed = seed.wrapping_mul(0x2545_F491_4F6C_DD1D);
    let idx = ((seed >> 40) as usize) % PALETTE.len();
    let (r, g, b) = PALETTE[idx];
    Color32::from_rgb(r, g, b)
}

/// Resolve a building [`Way`]'s [`PrismStyle`]: pick the roof silhouette (the OSM
/// `roof:shape` else a default gabled for a small footprint, flat for a large one),
/// the facade colour (`building:colour` else the themed fill) and the roof colour
/// (`roof:colour`/material else a class default). `col` is the way's themed fill.
fn resolve_prism_style(ring: &[(f32, f32)], w: &Way, col: Color32, du_per_m: Option<f32>) -> PrismStyle {
    // Footprint area (design units) → gate large blocks to a flat roof (realism + perf).
    // The gate is a real m² through this scene's own scale (see
    // [`ROOF_PITCH_MAX_AREA_M2`]); as a fraction of the design box it was a SQUARED
    // length, 281 250 m² at korp's scale, and never fired at all.
    let area = signed_area(&open_ring(ring)).abs();
    let small_enough = area <= roof_pitch_max_area_du(du_per_m);
    let shape = if !small_enough {
        RoofShape::Flat
    } else {
        match w.roof_shape {
            // An explicit OSM roof shape wins.
            RoofShape::Gabled => RoofShape::Gabled,
            RoofShape::Hipped => RoofShape::Hipped,
            // Untagged (Flat): give a small house a sensible default gabled roof so
            // the town reads as pitched rooftops, not flat massing.
            RoofShape::Flat => RoofShape::Gabled,
        }
    };
    let wall_col = w.building_colour.unwrap_or(col);
    let roof_col = w.roof_colour.unwrap_or_else(|| default_roof_tint(w.osm_id));
    PrismStyle { shape, wall_col, roof_col, pitch: DEFAULT_ROOF_PITCH }
}

/// Extrude a building footprint into a **coloured, roofed** prism: shaded facade walls
/// (0.82× the facade colour, matching [`extrude_prism`]), a bottom cap, an eave cap at
/// `height`, and — for a `Gabled`/`Hipped` [`RoofShape`] — real pitched roof geometry
/// rising above the eave in the roof colour. `Flat` keeps just the (coloured) eave cap.
/// Additive to [`extrude_prism`]; used only under [`RoofStyle::Pitched`]. The scene loop
/// calls [`extrude_prism_styled_based`] directly (with the terrain datum); this flat-base
/// wrapper is the byte-stable entry the unit tests build against.
#[cfg_attr(not(test), allow(dead_code))]
fn extrude_prism_styled(mesh: &mut Mesh3, ring: &[(f32, f32)], height: f32, style: &PrismStyle) {
    // Flat ground (`y = 0`) base — the byte-stable historical geometry.
    extrude_prism_styled_based(mesh, ring, height, style, 0.0, 0.0);
}

/// Like [`extrude_prism_styled`], but the roofed building sits on **uneven ground**: its
/// eave (and the roof above it) rides `base_y + height` (the highest ground under the
/// footprint plus its wall height) and its foundation drops to `foot_y` (the lowest), so
/// over relief terrain the land never rises over the lower facade — the fix for "houses
/// look half-buried". `base_y == foot_y == 0` reproduces [`extrude_prism_styled`].
fn extrude_prism_styled_based(
    mesh: &mut Mesh3,
    ring: &[(f32, f32)],
    height: f32,
    style: &PrismStyle,
    base_y: f32,
    foot_y: f32,
) {
    let mut poly = open_ring(ring);
    if poly.len() < 3 {
        return;
    }
    if signed_area(&poly) < 0.0 {
        poly.reverse();
    }
    let cap_tris = ear_clip(&poly);
    if cap_tris.is_empty() {
        return;
    }
    let n = poly.len();

    // Eave (top of the walls / base of the roof) rides the ground datum; the foundation
    // reaches the lowest ground under the footprint.
    let eave_y = base_y + height;
    let bottom_y = foot_y;

    // ── eave cap (y = eave_y, +y) — the roof platform, in the ROOF colour ──
    let up = [0.0, 1.0, 0.0];
    let cap0 = mesh.positions.len() as u32;
    for &(x, z) in &poly {
        mesh.push_vertex([x, eave_y, z], up);
    }
    for &[i, j, k] in &cap_tris {
        mesh.push_tri([cap0 + i as u32, cap0 + j as u32, cap0 + k as u32], style.roof_col);
    }

    // ── bottom cap (y = bottom_y, −y) — facade colour (unseen underside) ──
    let down = [0.0, -1.0, 0.0];
    let bot0 = mesh.positions.len() as u32;

The file goes on; the console shows all of it.