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

nordisk/facett

facett-demo/src/gfx_v2/scene.rs

//! **The shared GFX v2 scene** — one set of real Swedish geography that every lane
//! in the [`GfxV2Tab`](super::GfxV2Tab) consumes: the 2D projection, the 3D
//! projection, the GPU label collision lane and the GPU pick pass.
//!
//! ## Why real data
//!
//! GFX_V2's recurring finding was features that "worked" against a fixture too
//! small to exercise them. A 3-element scene cannot make labels collide, cannot
//! produce a repeated place name, and cannot tell a working pick tie-break from a
//! broken one. So this scene is built from **real WGS-84 Swedish geography**,
//! reused rather than invented (LAW: reuse, do not twin):
//!
//! * [`knut_popsim::geo::CITIES`] — 14 real population centres (real coordinates,
//!   real relative population weights). These are the high-priority labels.
//! * [`knut_popsim::geo::address_for`] — the deterministic address generator the
//!   Sverige tab already uses, which scatters people around those centres with
//!   real per-city spreads. These are the low-priority labels and the bulk of the
//!   pick targets.
//!
//! Two properties of that data are load-bearing for the lanes under test, and
//! neither is arrangeable with a small fixture:
//!
//! 1. **Genuine collision.** Stockholm's addresses scatter within 0.28°, so at a
//!    Sweden-wide zoom dozens of labels land inside a handful of grid cells. The
//!    collision pass has real work to do; most candidates must lose.
//! 2. **Genuine repeated names.** Every address carries its *kommun* name, so
//!    "Stockholm" appears hundreds of times. That is what exercises the label
//!    lane's name-repeat de-duplication (the `name_hash` → repeat-cell slots),
//!    which a scene of distinct names would leave entirely untouched.
//!
//! ## Projection
//!
//! Geography enters as lon/lat, is turned into **Web-Mercator world units** once
//! ([`Place::world`]), and from there *every* screen position — 2D, 3D, label
//! anchor and pick quad alike — is produced by
//! [`facett_core::render::View`]. This module contains no projection maths of its
//! own; that is item 6's whole point, and it is what lets the parity check in
//! [`super`] be meaningful rather than circular.

use facett_core::engine::pick::PickId;
use facett_core::render::View;

/// Pick layer for the 14 named population centres. Layer 0 is reserved by
/// [`PickId::NOTHING`], so a miss can never be mistaken for feature 0 of layer 0.
pub const LAYER_CITY: u8 = 1;
/// Pick layer for the generated addresses.
pub const LAYER_PLACE: u8 = 2;

/// How many addresses the scene generates. Chosen so that the Sweden-wide view
/// puts many candidates in one collision cell (see the module docs) while the
/// whole scene still projects in well under a frame on the CPU.
pub const PLACE_COUNT: usize = 600;

/// One thing in the scene: a named point with a stable pick identity.
#[derive(Clone, Debug, PartialEq)]
pub struct Place {
    /// The label text AND the pick answer. For a city its own name; for an
    /// address its kommun — which is why names repeat.
    pub name: &'static str,
    pub lon: f64,
    pub lat: f64,
    /// Web-Mercator world units, derived once from lon/lat.
    pub world: [f32; 2],
    /// Relative importance — drives the label priority rank and the drawn radius.
    /// For cities the real population weight; for addresses a small constant.
    pub weight: f32,
    /// The pick identity. Stable across frames and across zoom, and derived from
    /// the domain (layer + index into that layer), never from a draw-order index.
    pub id: PickId,
}

impl Place {
    /// Whether this is one of the 14 real population centres.
    pub fn is_city(&self) -> bool {
        self.id.layer() == LAYER_CITY
    }
}

/// Web-Mercator, in the same world units [`View::ortho_2d_map`] expects: x east,
/// y **south** (screen-down), both normalised so the whole world spans 1.0.
///
/// This is the ONE lon/lat → world writer in the demo's GFX v2 lane. Latitude is
/// clamped just inside the poles because the Mercator `tan` diverges there — with
/// Sweden's bbox topping out at 69°N that clamp never actually engages, but a
/// projection that returns an infinity for a legal input is a trap for the next
/// caller, not a saved branch.
pub fn mercator(lon: f64, lat: f64) -> [f32; 2] {
    let lat = lat.clamp(-89.9, 89.9);
    let x = (lon + 180.0) / 360.0;
    let s = (lat.to_radians()).sin();
    let y = 0.5 - (((1.0 + s) / (1.0 - s)).ln()) / (4.0 * std::f64::consts::PI);
    [x as f32, y as f32]
}

/// The shared scene. Built once; every lane reads it.
#[derive(Clone, Debug)]
pub struct Scene {
    pub places: Vec<Place>,
    /// World-space bounds of everything in the scene, `[min_x, min_y, max_x, max_y]`.
    pub bounds: [f32; 4],
}

impl Scene {
    /// Build the Sweden scene: the 14 real cities, then [`PLACE_COUNT`]
    /// deterministic addresses around them.
    ///
    /// Deterministic by construction — `address_for` is seeded off the id alone —
    /// so a golden, a parity assertion and a pick assertion all see the same scene
    /// on every run and on every machine.
    pub fn sweden() -> Self {
        use knut_popsim::geo;

        let mut places: Vec<Place> = Vec::with_capacity(geo::CITIES.len() + PLACE_COUNT);

        for (i, c) in geo::CITIES.iter().enumerate() {
            places.push(Place {
                name: c.name,
                lon: c.lon,
                lat: c.lat,
                world: mercator(c.lon, c.lat),
                weight: c.weight as f32,
                id: PickId::new(LAYER_CITY, i as u32),
            });
        }

        for i in 0..PLACE_COUNT {
            // `address_for` is seeded by the absolute id, so place `i` is always the
            // same point. Offset the ids so they do not alias the city indices in
            // anyone's mental model; the pick layer already keeps them distinct.
            let a = geo::address_for(10_000 + i as i64);
            places.push(Place {
                name: a.kommun,
                lon: a.lon,
                lat: a.lat,
                world: mercator(a.lon, a.lat),
                weight: 1.0,
                id: PickId::new(LAYER_PLACE, i as u32),
            });
        }

        let mut bounds = [f32::MAX, f32::MAX, f32::MIN, f32::MIN];
        for p in &places {
            bounds[0] = bounds[0].min(p.world[0]);
            bounds[1] = bounds[1].min(p.world[1]);
            bounds[2] = bounds[2].max(p.world[0]);
            bounds[3] = bounds[3].max(p.world[1]);
        }

        Self { places, bounds }
    }

    /// The scene's world-space centre — the point both views target, so the 2D and
    /// 3D lanes frame the same thing and the parity check compares like with like.
    pub fn centre(&self) -> [f32; 2] {
        [(self.bounds[0] + self.bounds[2]) * 0.5, (self.bounds[1] + self.bounds[3]) * 0.5]
    }

    /// World-space extent, `(width, height)`.
    pub fn extent(&self) -> (f32, f32) {
        (self.bounds[2] - self.bounds[0], self.bounds[3] - self.bounds[1])
    }

    /// Look up a place by its [`PickId`] — how a pick result becomes a name.
    pub fn by_id(&self, id: PickId) -> Option<&Place> {
        if id.is_nothing() {
            return None;
        }
        self.places.iter().find(|p| p.id == id)
    }

    /// How many distinct names the scene contains. The gap between this and
    /// `places.len()` IS the repeated-name pressure the label lane must survive:
    /// with 614 places and ~14 distinct names, name-repeat de-duplication is doing
    /// real work rather than being exercised zero times.
    pub fn distinct_names(&self) -> usize {
        let mut names: Vec<&str> = self.places.iter().map(|p| p.name).collect();
        names.sort_unstable();
        names.dedup();
        names.len()
    }

    /// Project every place to screen pixels **through the view core**.
    ///
    /// `centre_px` is where the view's target lands on screen (the pane centre, not
    /// the render target's centre) and `half_h` the pane's half-height in pixels —
    /// the two parameters [`View::project_ground`] needs to be able to answer in
    /// perspective. A 2D caller's `half_h` is ignored by the ortho branch, but it
    /// is passed anyway so that the SAME call shape serves both lanes; a 2D-only
    /// overload is how the two lanes drift apart.
    pub fn project(&self, view: &View, centre_px: (f32, f32), half_h: f32) -> Vec<Projected> {
        self.places
            .iter()
            .map(|p| {
                let pr = view.project_ground(p.world, centre_px, half_h);
                Projected { x: pr.x, y: pr.y, depth: pr.depth, visible: pr.visible }
            })
            .collect()
    }
}

/// A projected place — deliberately a plain struct rather than
/// `facett_core::render::camera::Projected`, so this module can hand the same
/// shape to the painter, the label lane and the pick lane without re-projecting
/// (three lanes projecting independently is how a label ends up 32 px from the
/// dot it names).
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Projected {
    pub x: f32,
    pub y: f32,
    pub depth: f32,
    pub visible: bool,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn the_scene_is_real_swedish_geography_not_a_fixture() {
        let s = Scene::sweden();
        assert_eq!(s.places.len(), 14 + PLACE_COUNT, "14 cities + the generated addresses");

        // The 14 cities are the real ones, with their real coordinates.
        let sthlm = s.places.iter().find(|p| p.name == "Stockholm" && p.is_city()).expect("Stockholm");
        assert!((sthlm.lon - 18.0686).abs() < 1e-6, "Stockholm keeps its real longitude");
        assert!((sthlm.lat - 59.3293).abs() < 1e-6, "Stockholm keeps its real latitude");
        assert!(sthlm.weight > 20.0, "Stockholm carries the largest population weight");

        // Every place is inside Sweden — the generator is not scattering into the sea.
        let (lo_lon, lo_lat, hi_lon, hi_lat) = knut_popsim::geo::SWEDEN_BBOX;
        for p in &s.places {
            assert!(p.lon >= lo_lon && p.lon <= hi_lon, "{} lon {} inside Sweden", p.name, p.lon);
            assert!(p.lat >= lo_lat && p.lat <= hi_lat, "{} lat {} inside Sweden", p.name, p.lat);
        }
    }

    #[test]
    fn names_repeat_hard_enough_to_exercise_name_dedup() {
        let s = Scene::sweden();
        let distinct = s.distinct_names();
        // The whole point: far more places than names.
        assert!(distinct <= 20, "at most ~14 kommun names, got {distinct}");
        assert!(
            s.places.len() > distinct * 20,
            "{} places over only {distinct} names — that is the repeat pressure the label \
             lane's name slots exist for",
            s.places.len(),
        );
    }

    #[test]
    fn pick_ids_are_unique_and_never_collide_with_a_miss() {
        let s = Scene::sweden();
        let mut ids: Vec<u32> = s.places.iter().map(|p| p.id.0).collect();
        ids.sort_unstable();
        let n = ids.len();
        ids.dedup();
        assert_eq!(ids.len(), n, "every place has a distinct PickId");
        for p in &s.places {
            assert!(!p.id.is_nothing(), "{} must not encode as a miss", p.name);
        }
        // Round-trip through the wire encoding the GPU pass actually uses.
        for p in &s.places {
            assert_eq!(PickId::from_rgba(p.id.to_rgba()), p.id, "{} survives the rgba round-trip", p.name);
        }
    }

    #[test]
    fn by_id_resolves_a_pick_back_to_a_name_and_a_miss_to_nothing() {
        let s = Scene::sweden();
        let want = &s.places[7];
        assert_eq!(s.by_id(want.id).map(|p| p.name), Some(want.name));
        assert!(s.by_id(PickId::NOTHING).is_none(), "a miss resolves to no place, not to place 0");
    }

    #[test]
    fn mercator_is_monotonic_and_flips_latitude_to_screen_down() {
        // East increases x.
        assert!(mercator(11.0, 59.0)[0] < mercator(18.0, 59.0)[0]);
        // North DECREASES y (world y is screen-down).
        assert!(mercator(18.0, 65.0)[1] < mercator(18.0, 55.0)[1]);
        // The poles are finite, not an infinity waiting for the next caller.
        assert!(mercator(0.0, 90.0)[1].is_finite());
        assert!(mercator(0.0, -90.0)[1].is_finite());
    }
}