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

nordisk/facett

facett-core/src/render/view.rs

//! **THE unified view core — a 2D map IS a 3D view under constraints.**
//!
//! GFX_V2 item 6. Before this module `facett-map` (2D ortho Mercator) and
//! `facett-map3d` (3D orbit terrain) each carried their own projection writer, so
//! every AA / miter / typography / precision fix had to be made **twice** and the
//! two copies drifted. This is the ONE writer both skins route through (LAW #5):
//!
//! ```text
//!                     [ View — this module ]
//!               ┌──────────────┴──────────────┐
//!      [ 3D Terrain Mode ]            [ 2D Ortho Mode ]
//!      • Terrain::Heightfield         • Terrain::Flat  (Z(x,y) ≡ 0)
//!      • tilt / azimuth free          • tilt = azimuth = 0
//!      • Projection::Perspective      • Projection::Ortho
//!      • PostFx::ALL (SSAO + CSM)     • PostFx::NONE (bypassed, so 2D is free)
//!               └──────────────┬──────────────┘
//!                  [ WGSL compute culling & draw ]
//! ```
//!
//! ## World axes — `X east, Y up, Z south`
//! Deliberately chosen so **Web-Mercator drops straight in**: Mercator `x` grows
//! east and Mercator `y` grows *south*, exactly matching `(X, Z)`, and screen `y`
//! also grows downward. The 2D reduction therefore needs **no axis flip and no
//! fudge factor** — see [`View::project_px`], which is *bit-exact* against
//! `facett-map`'s `MapTransform::project` (`gfx_v2_item6_parity.rs` proves it).
//!
//! ## Why the frame is closed-form and not a look-at
//! `facett-map3d::camera::OrbitCamera::basis` builds the view frame as
//! `right = normalize(fwd × world_up)`. That cross product **vanishes when the
//! camera looks straight down** — precisely the 2D constraint — which is why
//! `ViewPreset::Top` has to fudge its elevation to `PI/2 − 0.001`. A 2D mode built
//! on a look-at would be built on a gimbal lock.
//!
//! [`View::basis`] instead evaluates the frame in closed form from
//! `(azimuth, tilt)`. It is an **exact algebraic identity** with the orbit
//! cross-product basis wherever that basis is defined (`sin(tilt) > 0`), and it
//! stays perfectly conditioned *at* `tilt = 0`. Both facts are asserted in
//! `tests` below, and the identity is re-asserted cross-crate from `facett-map3d`
//! (where both types are visible) by `unified_view_matches_orbit_camera_math`.

use glam::{Mat4, Vec3};

use super::camera::{Projected, V3};

/// How the view maps view-space onto the screen after the view transform.
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Projection {
    /// **Orthographic** — no foreshortening at all; depth does not divide.
    /// `px_per_unit` is pixels per world unit **per axis** (a uniform map camera
    /// passes the same value twice; a non-uniform "fit this bbox" camera does not).
    /// This is the 2D map's projection: it is what makes `screen = world·zoom + c`.
    Ortho { px_per_unit: [f32; 2] },
    /// **Perspective** — vertical field of view in radians. The 3D terrain mode.
    /// A very long focal length (tiny `fov_y`) approaches `Ortho`, which is the
    /// other half of GFX_V2's "orthographic (or long focal length)".
    Perspective { fov_y: f32 },
}

impl Projection {
    /// Whether this is the orthographic (2D-mode) projection.
    #[inline]
    pub fn is_ortho(&self) -> bool {
        matches!(self, Projection::Ortho { .. })
    }
}

/// Whether the view reads the terrain heightfield or pins the ground perfectly flat.
///
/// `Flat` is GFX_V2's `Z(x,y) = 0` constraint, and it is enforced **by
/// construction** in [`View::terrain_height`] rather than by a caller remembering
/// to zero it — so "let Z through" is not something a 2D caller can do by accident.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Terrain {
    /// `Z(x,y) ≡ 0`. The 2D map plane.
    Flat,
    /// Sample the heightfield. The 3D terrain.
    Heightfield,
}

/// Which screen-space post passes the view runs.
///
/// 2D mode sets [`PostFx::NONE`], which is *why* GFX_V2 can say "2D costs nothing
/// extra": the unified pipeline is the same, the post passes are simply skipped.
///
/// The field names track what the renderer **actually has** rather than GFX_V2's
/// prose: the ambient-occlusion pass in `facett-map3d` is **SSAO**
/// (`ssao.wgsl`), not GTAO — there is no GTAO anywhere in the workspace. The
/// shadow pass is a real cascaded shadow map, so `csm` is accurate.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub struct PostFx {
    /// Screen-space ambient occlusion (GFX_V2 calls this "GTAO"; the pass is SSAO).
    pub ssao: bool,
    /// Cascaded shadow maps + the soft-shadow (PCSS) filter.
    pub csm: bool,
    /// HDR bloom (bright-pass → separable blur → composite).
    pub bloom: bool,
    /// Post-process antialiasing (the present/resolve FXAA).
    pub fxaa: bool,
}

impl PostFx {
    /// Every pass on — the 3D terrain default.
    pub const ALL: Self = Self { ssao: true, csm: true, bloom: true, fxaa: true };
    /// Every pass off — the 2D bypass.
    pub const NONE: Self = Self { ssao: false, csm: false, bloom: false, fxaa: false };

    /// Whether any post pass is enabled (i.e. whether an offscreen HDR target and
    /// the resolve chain are needed at all).
    #[inline]
    pub fn any(&self) -> bool {
        self.ssao || self.csm || self.bloom || self.fxaa
    }
}

/// **The ground-plane frame of an orthographic view**, at `f64` precision — the
/// rotate-then-foreshorten the 2D map skins apply to a Mercator pixel offset.
///
/// It exists because the 2D map does its Mercator arithmetic in `f64` while
/// [`View`] stores its angles as `f32` (matching `OrbitCamera`). Rather than force
/// the 2D path down to `f32`, or let it keep a second hand-written rotation, the
/// **trigonometry is evaluated once** in [`GroundFrame2d::from_azimuth_tilt_rad`]
/// and [`apply`](Self::apply) is the one writer that applies it.
///
/// ## Why `foreshorten` is separate and not folded into `rot`
/// Folding `cos(tilt)` into the matrix rows would mean computing
/// `(ct·s)·dx + (ct·c)·dz` instead of `(s·dx + c·dz)·ct` — distributing a multiply
/// over an addition, which is **not** exact in floating point. Keeping it a
/// separate post-multiply preserves the exact operation order the hand-rolled 2D
/// path used, so the unification is bit-exact rather than merely close.
///
/// Measured over 400k pseudo-random `(bearing, tilt, dx, dz)` draws, the folded
/// form differs from the separated one in **52.5%** of cases — always by a single
/// ULP, never visibly. That is precisely what makes it dangerous: it would break
/// the 2D skins' bit-equality guards for a reason no screenshot could explain.
/// `the_ground_frame_must_not_fold_the_foreshorten_into_the_rotation` pins the
/// ordering, and it exists because a mutation that folded the rows **passed**
/// `facett-map`'s six-pose `layer.rs` sweep (half those poses had `tilt = 0`, where
/// `ct = 1` makes folding exact).
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct GroundFrame2d {
    /// The pure rotation `[a, b, c, d]`, applied as
    /// `(a·dx + b·dz, c·dx + d·dz)`. No tilt in here.
    pub rot: [f64; 4],
    /// The vertical foreshortening `cos(tilt)`, applied to the rotated `y`
    /// **after** the rotation. `1.0` at tilt 0.
    pub foreshorten: f64,
}

impl GroundFrame2d {
    /// Build from a turntable `azimuth` and `tilt`, both in **radians**. This is the
    /// one place the 2D frame's trigonometry is evaluated.
    pub fn from_azimuth_tilt_rad(azimuth: f64, tilt: f64) -> Self {
        let (sa, ca) = azimuth.sin_cos();
        Self { rot: [ca, -sa, sa, ca], foreshorten: tilt.cos() }
    }

    /// Apply the frame to a ground offset `(dx, dz)` in pixels (east, south),
    /// yielding the screen offset from the pane centre. **The one writer** — a
    /// caller must not spell this arithmetic out itself.
    #[inline]
    pub fn apply(&self, dx: f64, dz: f64) -> (f64, f64) {
        let rx = self.rot[0] * dx + self.rot[1] * dz;
        let ry = self.rot[2] * dx + self.rot[3] * dz;
        (rx, ry * self.foreshorten)
    }

    /// **The exact inverse of [`apply`](Self::apply)** — a screen offset from the pane
    /// centre back to the ground offset `(dx, dz)` in pixels (east, south).
    ///
    /// This is what a *gesture* needs: a drag is measured in screen pixels and has to
    /// become a movement across the ground, and on a rotated or tilted map those are not
    /// the same vector. It lives here, beside `apply`, because the only other way to get
    /// it is to spell the trigonometry out a second time at the call site — which is the
    /// drift LAW #5 exists to stop, and `project`'s own doc already records what that
    /// cost once.
    ///
    /// `rot` is a pure rotation (orthonormal, `det = 1`), so its inverse is its
    /// transpose; `foreshorten` is `cos(tilt)` with tilt clamped well below 90°, so it is
    /// never zero — the guard against a hand-built degenerate frame just passes the
    /// vertical component through rather than producing an infinity.
    #[inline]
    pub fn unapply(&self, rx: f64, ry: f64) -> (f64, f64) {
        let ry = if self.foreshorten.abs() < f64::EPSILON { ry } else { ry / self.foreshorten };
        let dx = self.rot[0] * rx + self.rot[2] * ry;
        let dz = self.rot[1] * rx + self.rot[3] * ry;
        (dx, dz)
    }
}

/// **The unified view.** One description; two modes.
///
/// See the module docs. Build the 2D mode with [`View::ortho_2d`] and the 3D mode
/// with [`View::perspective_3d`] / [`View::from_orbit`]; both are the same type, so
/// a fix to [`View::basis`] / [`View::project_px`] / [`View::view_proj`] lands in
/// both at once.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct View {
    /// The world point the view centres on (the orbit target / the 2D camera
    /// centre). World axes are `X east, Y up, Z south`.
    pub target: Vec3,
    /// **Turntable yaw about +Y**, radians. This is `OrbitCamera::azimuth`'s exact
    /// sense: increasing it swings the *eye* counter-clockwise (toward the east),
    /// so the direction the camera *faces* rotates the opposite way.
    ///
    /// It is deliberately NOT called `bearing`. A compass bearing is a clockwise
    /// heading, and `azimuth` is its negation — see
    /// [`compass_bearing_deg`](View::compass_bearing_deg) and the
    /// `the_two_skins_disagree_about_which_way_bearing_turns` test, which measures
    /// a live disagreement between the two skins on exactly this point.
    pub azimuth: f32,
    /// **Tilt from top-down** in radians; `0` = plan / map view (straight down),
    /// `PI/2` = horizon. Matches `OrbitCamera::tilt_deg` (`= 90° − elevation`).
    pub tilt: f32,
    /// **Bank about the FORWARD axis**, radians, positive = right wing down. `0` for
    /// every camera that is not flying a stick, which is every camera but one.
    ///
    /// # Why the projection core grew a third angle
    ///
    /// `azimuth` and `tilt` fix where the camera LOOKS; they cannot express how it is
    /// rotated about that look. The basis below therefore always returned a `right` with
    /// `y == 0` — a horizon level by construction — and a rig with a bank axis had nowhere
    /// to put it. `facett-map3d-flight`'s `FlightCamera` has had `roll` since it was written
    /// (Q / E, the flight-stick aileron), and when `facett-demo`'s flight tab was moved onto
    /// the shared renderer that roll was silently LEVELLED: the stick moved, the state
    /// changed, and the horizon did not. A degree of freedom dropped without a word is the
    /// same defect class as a control that acks and does nothing.
    ///
    /// Additive by construction: it defaults to `0` in both constructors, and at `0` the
    /// rotation in [`View::basis`] is skipped outright, so every existing view projects
    /// bit-for-bit as it did (`a_zero_roll_is_the_identity_on_the_basis` is the guard).
    pub roll: f32,
    /// Eye-to-target distance. In `Ortho` mode it only sets where the near/far
    /// band sits; it cannot change the on-screen scale (that is the definition of
    /// orthographic, and the 2D parity test pins it).
    pub distance: f32,
    /// Ortho or perspective.
    pub projection: Projection,
    /// Flat (`Z ≡ 0`) or heightfield.
    pub terrain: Terrain,
    /// Which post passes run.
    pub post: PostFx,
    /// **Near-plane override**, view-space design units (V2.5 D.3 fix). `None` = the
    /// stock behaviour: [`View::NEAR_PLANE`] for the CPU projector and the tight
    /// `(distance − 2).max(0.05)` band for the GPU matrix.
    ///
    /// Why it exists: the korp ride parks a rider's-eye a few METRES behind an
    /// overlay body, and a few metres is `~1e-3` design units on a city scene —
    /// far inside both stock nears, so the avatar was clipped out of existence on
    /// every frame while every count stayed green. A host that knows it has close
    /// geometry (`Map3D` when an overlay rides) writes the one number both lanes
    /// then honour; nobody else ever sets it, so every existing view projects
    /// bit-for-bit as before ([`View::near_clip`] is the one reader).
    pub near_override: Option<f32>,
}

impl View {
    /// The **view-space near plane** (camera-space depth). Identical to
    /// `OrbitCamera::NEAR_PLANE` / `Camera::NEAR_PLANE` — geometry at or behind it
    /// sits on the eye and must be rejected before a perspective divide.
    pub const NEAR_PLANE: f32 = 0.02;

    /// The far-plane pad added to `distance` (mirrors `OrbitCamera::far_plane`).
    const FAR_PAD: f32 = 4.0;

    /// **The shortest orbit distance the far plane is budgeted for** (A-2a).
    ///
    /// The far plane used to be `distance + FAR_PAD` outright, which silently made the
    /// FRUSTUM a function of the DOLLY LENGTH. That was harmless while every pose used
    /// the same scene-independent dolly. It stopped being harmless when
    /// `Map3D::look_from` was made ground-aware — parking a nose-down flight target on
    /// the ground along the view ray instead of a fixed two units out, so it can never
    /// be dragged under the terrain — because a low, steep pose then has a dolly of
    /// centimetres and would have had a frustum to match.
    ///
    /// The floor is derived, not picked: the scene lives in the design box `[-1,1]³`,
    /// whose body diagonal is `2√3 ≈ 3.47`, so an eye anywhere inside it must be able
    /// to see the opposite corner however short its dolly is. `2 + 4 = 6` clears that
    /// and is exactly the budget every non-flight pose already had.
    pub const FAR_MIN_DISTANCE: f32 = 2.0;

    // ── the two constructors ─────────────────────────────────────────────────

    /// **2D ortho mode**, by construction. `target` is the ground point under the
    /// screen centre (`y` is ignored — the plane is at `Y = 0`), `px_per_unit` is
    /// the per-axis zoom.
    ///
    /// Every 2D constraint is applied here rather than left to the caller:
    /// `tilt = azimuth = 0`, [`Projection::Ortho`], [`Terrain::Flat`],
    /// [`PostFx::NONE`]. There is no way to construct a *nearly*-2D view by
    /// forgetting one of them.
    pub fn ortho_2d(target: Vec3, px_per_unit: [f32; 2]) -> Self {
        Self {
            target: Vec3::new(target.x, 0.0, target.z),
            azimuth: 0.0,
            // A plan view has no bank: a rolled map is not a mode, it is a mistake.
            roll: 0.0,
            tilt: 0.0,
            distance: 1.0,
            projection: Projection::Ortho { px_per_unit },
            terrain: Terrain::Flat,
            post: PostFx::NONE,
            near_override: None,
        }
    }

    /// **3D terrain mode.** `tilt`/`azimuth` in radians, heightfield on, full post
    /// stack on.
    pub fn perspective_3d(target: Vec3, azimuth: f32, tilt: f32, distance: f32, fov_y: f32) -> Self {
        Self {
            target,
            azimuth,
            tilt,
            roll: 0.0,
            distance,
            projection: Projection::Perspective { fov_y },
            terrain: Terrain::Heightfield,
            post: PostFx::ALL,
            near_override: None,
        }
    }

    /// **Bank this view about its forward axis.** Chainable, and additive: the default is
    /// `0`, at which [`View::basis`] returns exactly what it returned before roll existed.
    #[must_use]
    pub fn with_roll(mut self, roll: f32) -> Self {
        self.roll = roll;
        self
    }

    /// **Override the near plane** (chainable, additive — `None` is the stock band).
    /// See [`Self::near_override`] for why a rider's-eye pose needs one.
    #[must_use]
    pub fn with_near(mut self, near: Option<f32>) -> Self {
        self.near_override = near;
        self
    }

    /// The **effective CPU-projector near plane**: the override, else
    /// [`Self::NEAR_PLANE`]. The one reader every near-clip goes through, so the
    /// CPU clip and the reject in [`Self::project_px`] cannot disagree.
    #[must_use]
    pub fn near_clip(&self) -> f32 {
        self.near_override.unwrap_or(Self::NEAR_PLANE)
    }

    /// Build from `OrbitCamera`'s **spherical** parameters (`azimuth`, `elevation`)
    /// instead of nav (`compass bearing`, `tilt`). This is the delegation seam
    /// `facett-map3d` calls, so the orbit camera has no projection writer of its
    /// own: `tilt = PI/2 − elevation`, `azimuth = azimuth`.
    pub fn from_orbit(target: V3, azimuth: f32, elevation: f32, distance: f32, fov_y: f32) -> Self {
        Self::perspective_3d(
            Vec3::new(target.x, target.y, target.z),
            azimuth,
            std::f32::consts::FRAC_PI_2 - elevation,
            distance,
            fov_y,
        )
    }

    // ── the 2D-facing façade (no glam needed at the call site) ───────────────

    /// [`View::ortho_2d`] for a caller that thinks in **2D map coordinates**:
    /// `ref_xy` is the Mercator point under the pane centre and `px_per_unit` the
    /// per-axis zoom. Lifts `(x, y)` onto the ground plane as `(x, 0, y)` — the
    /// axis choice that makes the reduction exact (see the module docs).
    ///
    /// This exists so `facett-map` and the other 2D skins need no `glam`
    /// dependency of their own to route through the unified core.
    pub fn ortho_2d_map(ref_xy: [f32; 2], px_per_unit: [f32; 2]) -> Self {
        Self::ortho_2d(Vec3::new(ref_xy[0], 0.0, ref_xy[1]), px_per_unit)
    }

    /// Project a **ground-plane** `(x, y)` (2D map space) to screen pixels.
    /// `half_h` is only read in perspective mode.
    #[inline]
    pub fn project_ground(&self, xy: [f32; 2], centre: (f32, f32), half_h: f32) -> Projected {
        self.project_world(Vec3::new(xy[0], 0.0, xy[1]), centre, half_h)
    }

    /// The inverse of [`View::project_ground`]: screen pixel → ground `(x, y)` in
    /// 2D map space. `None` when the ray cannot reach the plane.
    #[inline]
    pub fn unproject_ground(&self, px: (f32, f32), centre: (f32, f32), half_h: f32) -> Option<[f32; 2]> {
        self.unproject_ground_px(px, centre, half_h).map(|w| [w.x, w.z])
    }

    /// **The ground-plane frame this view applies, in `f64`** — what the 2D skins
    /// need. See [`GroundFrame2d`].
    ///
    /// The angles come from this `View`'s `f32` fields, so this is the right call
    /// when the caller already holds a `View`. A 2D caller that holds its bearing
    /// and tilt in **degrees** should use
    /// [`ground_frame_2d_compass_deg`](Self::ground_frame_2d_compass_deg) instead:
    /// converting degrees → radians in `f32` and back up to `f64` costs an ULP,
    /// which is invisible on screen but enough to break a bit-equality guard.
    ///
    /// Orthographic only — with a perspective divide there is no ground-plane
    /// affine.
    pub fn ground_frame_2d(&self) -> GroundFrame2d {
        debug_assert!(self.projection.is_ortho(), "a ground frame needs an orthographic view");
        GroundFrame2d::from_azimuth_tilt_rad(self.azimuth as f64, self.tilt as f64)
    }

    /// [`View::ground_frame_2d`] from a **true compass bearing and tilt in
    /// degrees**, both `f64` — the full-precision entry point for the 2D map, whose
    /// Mercator arithmetic is `f64` (at slippy zoom 22 the world is 2^30 px wide, so
    /// an `f32` pixel offset there is coarse).
    pub fn ground_frame_2d_compass_deg(compass_bearing_deg: f64, tilt_deg: f64) -> GroundFrame2d {
        GroundFrame2d::from_azimuth_tilt_rad(-compass_bearing_deg.to_radians(), tilt_deg.to_radians())
    }

    // ── the constraint ───────────────────────────────────────────────────────

    /// Whether this view satisfies **every** 2D constraint GFX_V2 names: tilt 0,
    /// azimuth 0, orthographic, flat terrain. (Post-FX is a cost choice, not a
    /// correctness constraint, so it is not part of the predicate.)
    pub fn is_constrained_2d(&self) -> bool {
        self.tilt == 0.0
            && self.azimuth == 0.0
            && self.projection.is_ortho()
            && self.terrain == Terrain::Flat
    }

    /// Collapse a 3D view onto the 2D constraint surface, keeping the ground point
    /// and converting the perspective scale at the target into the equivalent
    /// ortho `px_per_unit` for `half_h` pixels of half-viewport-height — so
    /// "flatten this 3D view" lands on the same on-screen scale it had.
    pub fn constrain_2d(&mut self, half_h: f32) {
        let px_per_unit = match self.projection {
            Projection::Ortho { px_per_unit } => px_per_unit,
            // The perspective scale AT the target plane: focal / depth.
            Projection::Perspective { fov_y } => {
                let s = (half_h / (fov_y * 0.5).tan()) / self.distance.max(1e-6);
                [s, s]
            }
        };
        self.tilt = 0.0;
        self.azimuth = 0.0;
        self.target.y = 0.0;
        self.projection = Projection::Ortho { px_per_unit };
        self.terrain = Terrain::Flat;
        self.post = PostFx::NONE;
    }

    /// **The `Z(x,y) = 0` constraint, enforced by construction.** Callers hand in
    /// whatever the heightfield sampled; `Flat` throws it away. A 2D caller cannot
    /// leak terrain relief into the plane by forgetting to zero it, and the item-6
    /// mutation test drives exactly this seam.
    #[inline]
    pub fn terrain_height(&self, sampled: f32) -> f32 {
        match self.terrain {
            Terrain::Flat => 0.0,
            Terrain::Heightfield => sampled,
        }
    }

    // ── the frame (ONE writer, both modes) ───────────────────────────────────

    /// `(forward, right, up)` — the orthonormal view frame, in **closed form** from
    /// `(azimuth, tilt)`.
    ///
    /// At `azimuth = tilt = 0` this is exactly `fwd = -Y` (straight down),
    /// `right = +X` (east → screen right), `up = -Z` (north → screen up): the 2D
    /// map frame, with no degeneracy. See the module docs for why a look-at cannot
    /// do that.
    pub fn basis(&self) -> (Vec3, Vec3, Vec3) {
        let (sb, cb) = self.azimuth.sin_cos();
        let (st, ct) = self.tilt.sin_cos();
        // Frame at azimuth 0: right=(1,0,0), up=(0,st,-ct), fwd=(0,-ct,-st);
        // then rotated about +Y by `azimuth` (Ry(a)·v), which matches
        // OrbitCamera's azimuth sense exactly.
        let right = Vec3::new(cb, 0.0, -sb);
        let up = Vec3::new(-ct * sb, st, -ct * cb);
        let fwd = Vec3::new(-st * sb, -ct, -st * cb);
        if self.roll == 0.0 {
            return (fwd, right, up);
        }
        // **BANK about the forward axis.** `fwd`, `right` and `up` are orthonormal by
        // construction above, so a Rodrigues rotation about `fwd` reduces exactly to a
        // rotation within the `(right, up)` plane — no cross products, no renormalisation,
        // and `fwd` is untouched, which is what makes this a roll rather than a re-aim.
        //
        // The sign matches `facett_map3d_flight::FlightCamera::right`, which banks its own
        // right vector about forward by `+roll` (Rodrigues). Two spellings of a rotation are
        // free to disagree about handedness, and a mirrored horizon is a defect no count can
        // see — so the flight rig's convention is adopted here rather than chosen afresh.
        let (sr, cr) = self.roll.sin_cos();
        (
            fwd,
            Vec3::new(
                right.x * cr + up.x * sr,
                right.y * cr + up.y * sr,
                right.z * cr + up.z * sr,
            ),
            Vec3::new(
                up.x * cr - right.x * sr,
                up.y * cr - right.y * sr,
                up.z * cr - right.z * sr,
            ),
        )
    }

    /// The eye position: `target − distance · forward`.
    pub fn eye(&self) -> Vec3 {
        let (fwd, _, _) = self.basis();
        self.target - fwd * self.distance
    }

    /// The bounded far plane (`max(distance, FAR_MIN_DISTANCE) + FAR_PAD`), mirroring
    /// `OrbitCamera::far_plane`. See [`Self::FAR_MIN_DISTANCE`] for why the distance has
    /// a floor rather than going straight into the sum.
    pub fn far_plane(&self) -> f32 {
        self.distance.max(Self::FAR_MIN_DISTANCE) + Self::FAR_PAD
    }

    /// A world point in **view space** (`x = right, y = up, z = forward`, relative
    /// to the eye). The near-plane clip happens here, before any divide.
    pub fn view_space(&self, p: Vec3) -> Vec3 {
        let (fwd, right, up) = self.basis();
        let rel = p - self.eye();
        Vec3::new(rel.dot(right), rel.dot(up), rel.dot(fwd))
    }

    /// **The one projection writer.** An already-view-space point → screen pixels.
    ///
    /// * `Ortho` — `px = centre + (c.x·sx, −c.y·sy)`. No divide, so nothing is
    ///   near-plane-sensitive in x/y; only the depth band clips.
    /// * `Perspective` — `px = centre + (c.x/c.z, −c.y/c.z)·focal`, `focal =
    ///   half_h / tan(fov_y/2)`.
    ///
    /// `centre` is where [`View::target`] lands in pixels (the pane centre, which
    /// is *not* necessarily the render-target centre). `half_h` is half the
    /// viewport height in pixels and is only read in perspective mode.
    pub fn project_px(&self, c: Vec3, centre: (f32, f32), half_h: f32) -> Projected {
        match self.projection {
            Projection::Ortho { px_per_unit } => {
                // Orthographic: x/y do not depend on depth, so a point behind the
                // eye still has a well-defined screen position; only the depth
                // band decides visibility.
                let visible = c.z > 0.0;
                Projected {
                    x: centre.0 + c.x * px_per_unit[0],
                    y: centre.1 - c.y * px_per_unit[1],
                    depth: c.z,
                    visible,
                }
            }
            Projection::Perspective { fov_y } => {
                if c.z <= self.near_clip() || c.z >= self.far_plane() {
                    return Projected { x: centre.0, y: centre.1, depth: c.z, visible: false };
                }
                let focal = half_h / (fov_y * 0.5).tan();
                Projected {
                    x: centre.0 + (c.x / c.z) * focal,
                    y: centre.1 - (c.y / c.z) * focal,
                    depth: c.z,
                    visible: true,
                }
            }
        }
    }

    /// A **world** point → screen pixels (the `view_space` + `project_px` pair).
    pub fn project_world(&self, p: Vec3, centre: (f32, f32), half_h: f32) -> Projected {
        self.project_px(self.view_space(p), centre, half_h)
    }

    /// **The inverse** of [`View::project_world`] onto the ground plane — a screen
    /// pixel → the world `(x, z)` where its view ray meets `Y = target.y`.
    ///
    /// This matters far more than a convenience: the GPU **compute cull** works in
    /// this unprojected space while the **draw** works in the projected one, so if
    /// the two are not exact inverses the cull silently drops ways that are on
    /// screen. Routing both through this module is what makes them inverses by
    /// construction.
    ///
    /// In `Ortho` mode this is the exact affine inverse (`unproject∘project` is
    /// bit-stable). In `Perspective` mode it is a ray/plane intersection, and it
    /// returns `None` for a ray parallel to or pointing away from the plane.
    ///
    /// ## Why the Ortho arm goes through [`GroundFrame2d::unapply`]
    ///
    /// It used to spell the inverse out as `target + right·cx + up·cy` — which lands
    /// on the **image plane**, not the ground, whenever the view is tilted (`up` has a
    /// `+sin(tilt)` component and `unproject_ground` then just *drops* the `y`).
    /// Descending that point along `fwd` to `Y = target.y` contributes a second
    /// `sin²(tilt)/cos(tilt)` term, and the two together turn the forward's
    /// `·cos(tilt)` into an inverse that also **multiplies** by `cos(tilt)` where it
    /// must divide. The unprojected offset therefore came out `cos²(tilt)` too short:
    /// **2.42× at tilt 50°, 14.9× at `MAX_TILT` 75°**.
    ///
    /// That is not cosmetic. `facett_map::gpu::MapTransform::viewport_bbox` builds the
    /// **compute cull's** frustum box out of exactly this call while the draw projects
    /// through `GroundFrame2d::apply`, so a too-short box culls ways that are plainly
    /// on screen — and the CPU fills, which cull against the generous
    /// `MapView::merc_bbox` instead, stay put, so half the map vanishes and the other
    /// half does not. It was unreachable while the GPU lane refused every rotated
    /// camera; it went live the moment the lane grew its ground frame.
    ///
    /// `GroundFrame2d::unapply` is the ONE writer of this inverse (LAW #5) and it
    /// already divides. Dividing each pixel axis by its own `px_per_unit` first is
    /// exact for an anisotropic ortho too, because the forward scales each axis
    /// independently *after* the frame.
    pub fn unproject_ground_px(&self, px: (f32, f32), centre: (f32, f32), half_h: f32) -> Option<Vec3> {
        match self.projection {
            Projection::Ortho { px_per_unit } => {
                // Pixel offset → the frame's own output space (one divide per axis),
                // then the one inverse writer, which undoes the foreshorten and the
                // rotation in that order.
                let (dx, dz) = self.ground_frame_2d().unapply(
                    f64::from((px.0 - centre.0) / px_per_unit[0]),
                    f64::from((px.1 - centre.1) / px_per_unit[1]),
                );
                Some(self.target + Vec3::new(dx as f32, 0.0, dz as f32))
            }
            Projection::Perspective { .. } => self.unproject_plane_px(px, centre, half_h, self.target.y),
        }
    }

    /// [`Self::unproject_ground_px`]'s perspective ray, intersected with an
    /// **arbitrary horizontal plane** `Y = plane_y` — the flight case, where the
    /// target is parked one dolly-length down the view ray (`target.y` is mid-air)
    /// and the ground the click means is the terrain plane at `Y = 0`. One ray
    /// writer for both (LAW 5). `None` for an ortho view (no eye to shoot from
    /// that this seam should invent), a ray parallel to the plane, or a plane
    /// behind the eye.
    pub fn unproject_plane_px(
        &self,
        px: (f32, f32),
        centre: (f32, f32),
        half_h: f32,
        plane_y: f32,
    ) -> Option<Vec3> {
        let Projection::Perspective { fov_y } = self.projection else {
            return None;
        };
        let (fwd, right, up) = self.basis();
        let focal = half_h / (fov_y * 0.5).tan();
        // The view ray through the pixel, in world space.
        let dir = fwd + right * ((px.0 - centre.0) / focal) + up * (-(px.1 - centre.1) / focal);
        let eye = self.eye();
        let denom = dir.y;
        if denom.abs() < 1e-9 {
            return None; // parallel to the plane
        }
        let t = (plane_y - eye.y) / denom;
        if t <= 0.0 {
            return None; // the plane is behind the eye
        }
        Some(eye + dir * t)
    }

    /// The **view-projection matrix** for the GPU depth-tested path, as a `glam`
    /// [`Mat4`] (column-major; `to_cols_array()` is the wgpu uniform layout).
    ///
    /// `clip = M · [world, 1]`. Perspective mode reproduces
    /// `OrbitCamera::view_proj` (tight near/far around the design box so the depth
    /// test keeps real precision); ortho mode emits the affine the 2D draw shader
    /// already applies, with `w ≡ 1`.
    ///
    /// `aspect` is viewport width / height; it is only read in perspective mode
    /// (ortho carries its own per-axis pixel scale, so it needs the viewport in
    /// pixels instead — see [`View::view_proj_px`]).
    pub fn view_proj(&self, aspect: f32) -> Mat4 {
        match self.projection {
            Projection::Perspective { fov_y } => {
                let f = 1.0 / (fov_y * 0.5).tan();
                self.compose(f / aspect.max(1e-6), f, true, (0.0, 0.0))
            }
            // Without a viewport we cannot turn px_per_unit into NDC; assume the
            // canonical half-height of 1 unit so the matrix is still well-formed.
            Projection::Ortho { px_per_unit } => {
                let sy = px_per_unit[1];
                let sx = px_per_unit[0] / aspect.max(1e-6);
                self.compose(sx, sy, false, (0.0, 0.0))
            }
        }
    }

    /// The view-projection matrix in **pixel-anchored** form: `centre_px` is where
    /// [`View::target`] lands and `viewport` is the render target in pixels. This
    /// is the form the 2D map needs, because its pane centre is not the target
    /// centre; the perspective mode reduces to [`View::view_proj`] when
    /// `centre_px == viewport/2`.
    pub fn view_proj_px(&self, centre_px: (f32, f32), viewport: (f32, f32)) -> Mat4 {
        let (vw, vh) = (viewport.0.max(1e-6), viewport.1.max(1e-6));
        let half_w = vw * 0.5;
        let half_h = vh * 0.5;
        // Pixel offset of the projection centre from the NDC origin, in NDC.
        let off = ((centre_px.0 - half_w) / half_w, -(centre_px.1 - half_h) / half_h);
        match self.projection {
            Projection::Perspective { fov_y } => {
                let focal = half_h / (fov_y * 0.5).tan();
                self.compose(focal / half_w, focal / half_h, true, off)
            }
            Projection::Ortho { px_per_unit } => {
                self.compose(px_per_unit[0] / half_w, px_per_unit[1] / half_h, false, off)
            }
        }
    }

    /// Assemble `P · V`. `sx`/`sy` are the NDC scales applied to view-space x/y,
    /// `divide` selects perspective (`clip.w = c.z`) vs ortho (`clip.w = 1`), and
    /// `off` shifts the projection centre in NDC.
    fn compose(&self, sx: f32, sy: f32, divide: bool, off: (f32, f32)) -> Mat4 {
        let (fwd, right, up) = self.basis();
        let eye = self.eye();
        let (tx, ty, tz) = (-right.dot(eye), -up.dot(eye), -fwd.dot(eye));

        // The GPU depth band. Stock: a tight near around the design box (the orbit
        // dolly is ≥ MIN_DISTANCE, so `distance − 2` only goes tight when a flight
        // pose parks the dolly at 2.0 — where the old `.max(0.05)` floor was 94 m of
        // real ground on a 3 km scene and near-clipped the ride avatar wholesale).
        // An override wins because the host measured its closest geometry.
        // Ortho has no eye to be behind: the band is symmetric about the (nominal) eye so
        // geometry taller than `distance` above the target is not near-clipped.
        let far = self.far_plane();
        let near = if divide {
            self.near_override.unwrap_or_else(|| (self.distance - 2.0).max(0.05))
        } else {
            -far
        };
        // ── THE DEPTH MAP IS PROJECTION-SPECIFIC. ──
        // Perspective writes `clip.z = a·z + b` and divides by `w = z`, so
        // `a = f/(f−n), b = −f·n/(f−n)` puts `z = n → 0` and `z = f → 1`.
        // Ortho does NOT divide (`w ≡ 1`), so the same `a, b` gave `ndc.z = z` — a depth
        // band of exactly [0, 1] view units instead of [n, f]. The plan view's eye sits
        // `distance = 1` above the ground, so the ground landed on `ndc.z = 1.0` (the
        // clear value, so a `Less` test rejects it) and anything below it or above the eye fell
        // outside the band.
        // MEASURED by `facett-map3d/tests/postfx_bypass.rs` on an RTX 4090 (6.62) and a
        // Radeon 890M (6.65) alike — the plan frame's luma spread, calibrated at 14.70 —
        // a vendor-independent defect, present since the unified core (`21f411b`,
        // 2026-08-01). The linear map is `ndc.z = (z − n) / (f − n)`.
        let (a, b) = if divide {
            (far / (far - near), -far * near / (far - near))
        } else {
            (1.0 / (far - near), -near / (far - near))
        };

        // clip.x = sx·(right·p + tx) + off.x·w
        // clip.y = sy·(up·p    + ty) + off.y·w
        // clip.z = a ·(fwd·p   + tz) + b
        // clip.w = divide ? (fwd·p + tz) : 1
        let (wx, wy, wz, ww) = if divide { (fwd.x, fwd.y, fwd.z, tz) } else { (0.0, 0.0, 0.0, 1.0) };
        Mat4::from_cols_array(&[
            // column 0 (coefficients of p.x)
            sx * right.x + off.0 * wx,
            sy * up.x + off.1 * wx,
            a * fwd.x,
            wx,
            // column 1 (p.y)
            sx * right.y + off.0 * wy,
            sy * up.y + off.1 * wy,
            a * fwd.y,
            wy,
            // column 2 (p.z)
            sx * right.z + off.0 * wz,
            sy * up.z + off.1 * wz,
            a * fwd.z,
            wz,
            // column 3 (the w=1 translation column)
            sx * tx + off.0 * ww,
            sy * ty + off.1 * ww,
            a * tz + b,
            ww,
        ])
    }

    // ── nav-facing helpers (so callers need no trigonometry) ─────────────────

    /// Tilt from top-down in degrees (`0` = plan view).
    pub fn tilt_deg(&self) -> f32 {
        self.tilt.to_degrees()
    }
    /// The turntable yaw in degrees, `[0, 360)`. This is what
    /// `OrbitCamera::bearing_deg` returns today — see
    /// [`compass_bearing_deg`](Self::compass_bearing_deg) for why that name is a
    /// misnomer.
    pub fn azimuth_deg(&self) -> f32 {
        Self::wrap360(self.azimuth.to_degrees())
    }

    /// **The true compass heading the camera faces**, degrees clockwise from north,
    /// `[0, 360)`. This is `−azimuth`, and the sign is not a detail:
    ///
    /// at `azimuth = 90°` the eye sits due **east** of the target, so the camera is
    /// looking **west** — compass bearing 270°, not 90°. `OrbitCamera::bearing_deg`
    /// returns the azimuth unchanged and therefore reports the *opposite* heading
    /// from `facett-map::layer::MapView::bearing_deg`, which applies the correct
    /// clockwise sense. That divergence is MEASURED (a due-north point moves 576 px
    /// left in 2D and 198 px right in 3D at the same nominal `bearing_deg = 90`) by
    /// `facett-map3d/tests/gfx_v2_item6_parity.rs`.
    ///
    /// The unified core keeps `azimuth` as the storage field so the 3D delegation is
    /// bit-exact, and exposes the compass sense here so a 2D caller gets the
    /// heading it means. Which sense should become canonical across both skins is a
    /// product decision — flipping `OrbitCamera` would rotate a shipping car-nav
    /// compass overlay — so this records and pins it rather than silently picking.
    pub fn compass_bearing_deg(&self) -> f32 {
        Self::wrap360(-self.azimuth.to_degrees())
    }

    /// Set the turntable yaw from a **true compass heading** (clockwise from north).
    pub fn set_compass_bearing_deg(&mut self, deg: f32) {
        self.azimuth = -deg.to_radians();
    }

    /// [`View::perspective_3d`] from a **true compass heading** in degrees rather
    /// than a turntable azimuth in radians.
    pub fn perspective_3d_compass(
        target: Vec3,
        compass_bearing_deg: f32,
        tilt_deg: f32,
        distance: f32,
        fov_y: f32,
    ) -> Self {
        Self::perspective_3d(
            target,
            -compass_bearing_deg.to_radians(),
            tilt_deg.to_radians(),
            distance,
            fov_y,
        )
    }

    /// **2D ortho mode with a true compass bearing and a tilt** — the mode
    /// `facett-map::layer::MapView` is in. A *tilted orthographic* view is a real
    /// projection, not a fake: its vertical foreshortening is exactly
    /// `cos(tilt)` with no perspective convergence, which is precisely what
    /// `MapView::project` computes by hand today. Routing it here makes that a
    /// consequence of the shared frame instead of a second writer.
    pub fn ortho_2d_compass(
        target_xy: [f32; 2],
        px_per_unit: [f32; 2],
        compass_bearing_deg: f32,
        tilt_deg: f32,
    ) -> Self {
        let mut v = Self::ortho_2d_map(target_xy, px_per_unit);
        v.set_compass_bearing_deg(compass_bearing_deg);
        v.tilt = tilt_deg.to_radians();
        v
    }

    #[inline]
    fn wrap360(d: f32) -> f32 {
        let d = d % 360.0;
        if d < 0.0 { d + 360.0 } else { d }
    }
    /// The polar `elevation` an `OrbitCamera` would hold for this tilt.
    pub fn elevation(&self) -> f32 {
        std::f32::consts::FRAC_PI_2 - self.tilt
    }
}

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

    /// The orbit camera's cross-product frame, written out verbatim as the
    /// REFERENCE the closed form must reproduce (`facett-map3d::camera::
    /// OrbitCamera::{eye,basis}`, spherical `azimuth`/`elevation`).
    fn orbit_reference_basis(azimuth: f32, elevation: f32) -> (Vec3, Vec3, Vec3) {
        let (se, ce) = elevation.sin_cos();
        let (sa, ca) = azimuth.sin_cos();
        // eye = target + distance·(ce·sa, se, ce·ca); fwd = normalize(target − eye)
        let fwd = -Vec3::new(ce * sa, se, ce * ca).normalize();
        let world_up = Vec3::new(0.0, 1.0, 0.0);
        let right = fwd.cross(world_up).normalize();
        let up = right.cross(fwd).normalize();
        (fwd, right, up)
    }

    /// **The identity.** The closed-form frame reproduces the orbit cross-product
    /// frame at a spread of NON-trivial poses. Deliberately excludes the poles and
    /// bearing/tilt 0 — an identity test that only ever sees `(0, 0)` would pass
    /// with a frame that is right for one pose and wrong everywhere else.
    #[test]
    fn closed_form_frame_is_the_orbit_cross_product_frame() {
        let poses = [
            (37.0f32, 12.0f32),
            (-115.0, 62.0),
            (223.0, 85.0),
            (91.0, 3.5),
            (180.0, 45.0),
            (-44.0, 89.0),
        ];
        let mut worst = 0.0f32;
        for (bearing_deg, tilt_deg) in poses {
            let b = bearing_deg.to_radians();
            let t = tilt_deg.to_radians();
            let v = View::perspective_3d(Vec3::ZERO, b, t, 5.0, 50f32.to_radians());
            let (fwd, right, up) = v.basis();
            // OrbitCamera's elevation = 90° − tilt.
            let (rf, rr, ru) = orbit_reference_basis(b, std::f32::consts::FRAC_PI_2 - t);
            for (got, want, name) in
                [(fwd, rf, "fwd"), (right, rr, "right"), (up, ru, "up")]
            {
                let d = (got - want).length();
                worst = worst.max(d);
                assert!(
                    d < 1e-6,
                    "{name} drifted at bearing {bearing_deg}° tilt {tilt_deg}°: \
                     closed form {got:?} vs orbit reference {want:?} (|Δ| = {d:e})"
                );
            }
            // And the frame really is orthonormal (not just equal to a broken ref).
            assert!((fwd.length() - 1.0).abs() < 1e-6, "fwd unit");
            assert!(fwd.dot(right).abs() < 1e-6 && fwd.dot(up).abs() < 1e-6, "orthogonal");
            assert!(right.dot(up).abs() < 1e-6, "orthogonal");
        }
        // NON-VACUITY: the poses must actually have moved the frame around. If the
        // basis were a constant the loop above would still pass against a constant
        // reference, so pin that the frame genuinely varies.
        let a = View::perspective_3d(Vec3::ZERO, 0.6, 0.3, 5.0, 1.0).basis().0;
        let c = View::perspective_3d(Vec3::ZERO, 2.4, 1.1, 5.0, 1.0).basis().0;
        assert!((a - c).length() > 0.5, "the test poses must not all give one frame");
        assert!(worst > 0.0, "reference and closed form are separate computations");
    }

    /// **The gimbal lock the closed form escapes — and it is worse than a NaN.**
    ///
    /// At tilt 0 (the 2D constraint) `right = normalize(fwd × world_up)` is
    /// `normalize` of a vector whose every component is zero *in exact arithmetic*.
    /// In `f32` it is not zero, it is the rounding residue of `cos(PI/2)`
    /// (`-4.37e-8`), so the look-at basis does **not** blow up visibly — it returns
    /// a perfectly unit `right` whose SIGN is decided entirely by that residue, and
    /// at elevation exactly `PI/2` the residue is negative, so it hands back
    /// `(-1, 0, 0)`: **east and west are silently swapped and the map renders
    /// mirrored.**
    ///
    /// That is why `ViewPreset::Top` must fudge `PI/2 − 0.001` — the fudge is
    /// load-bearing, not cosmetic — and it is why a 2D mode cannot be built on a
    /// look-at. The closed form is exact and stable at the constraint.
    #[test]
    fn top_down_lookat_basis_silently_mirrors_east_and_the_closed_form_does_not() {
        const EAST: Vec3 = Vec3::new(1.0, 0.0, 0.0);
        let quarter = std::f32::consts::FRAC_PI_2;

        // 1. AT the constraint the look-at basis is unit-length (so no NaN guard
        //    would ever catch it) but points the WRONG WAY along east.
        let (_, r_at, _) = orbit_reference_basis(0.0, quarter);
        assert!(r_at.is_finite(), "it does not NaN — that is the whole problem");
        assert!((r_at.length() - 1.0).abs() < 1e-6, "and it is unit length: {r_at:?}");
        assert!(
            r_at.dot(EAST) < -0.5,
            "the look-at basis must be MIRRORED at exact top-down, got {r_at:?}"
        );

        // 2. It is ill-conditioned, not merely wrong at one point: perturbing the
        //    elevation by a ten-millionth of a radian FLIPS the east axis.
        let (_, r_below, _) = orbit_reference_basis(0.0, quarter - 1e-7);
        let (_, r_above, _) = orbit_reference_basis(0.0, quarter + 1e-7);
        assert!(
            r_below.dot(r_above) < -0.5,
            "1e-7 rad across top-down must flip the look-at east axis: {r_below:?} vs {r_above:?}"
        );

        // 3. The closed form is exact AT the constraint...
        let v = View::ortho_2d(Vec3::ZERO, [1.0, 1.0]);
        let (fwd, right, up) = v.basis();
        assert_eq!(fwd, Vec3::new(0.0, -1.0, 0.0), "straight down");
        assert_eq!(right, EAST, "east → screen +x");
        assert_eq!(up, Vec3::new(0.0, 0.0, -1.0), "north → screen up");

        // 4. ...and STABLE across it: the same ±1e-7 sweep moves it by nothing.
        let lo = View::perspective_3d(Vec3::ZERO, 0.0, -1e-7, 5.0, 1.0).basis().1;
        let hi = View::perspective_3d(Vec3::ZERO, 0.0, 1e-7, 5.0, 1.0).basis().1;
        assert!(
            lo.dot(hi) > 0.999_999 && lo.dot(EAST) > 0.999_999,
            "the closed form must not flip across the constraint: {lo:?} vs {hi:?}"
        );
    }

    /// The 2D ortho reduction: `screen = (world − target)·zoom + centre`, the
    /// affine `facett-map`'s `MapTransform::project` applies — asserted at
    /// NON-identity zoom, NON-zero target and an off-centre pane, and demanded
    /// **bit-exact** (the axis choice makes it a genuine identity, not an
    /// approximation).
    #[test]
    fn ortho_2d_is_bit_exactly_the_map_affine() {
        let (zx, zy) = (4096.0f32, 4096.0f32);
        let (rx, ry) = (0.361_25f32, 0.284_75f32);
        let centre = (517.5f32, 388.25f32);
        let v = View::ortho_2d(Vec3::new(rx, 0.0, ry), [zx, zy]);
        for pos in [[0.361_5f32, 0.284_2], [0.0, 0.0], [1.0, -0.5], [rx, ry]] {
            let p = v.project_world(Vec3::new(pos[0], 0.0, pos[1]), centre, 300.0);
            let want_x = (pos[0] - rx) * zx + centre.0;
            let want_y = (pos[1] - ry) * zy + centre.1;
            assert_eq!(p.x.to_bits(), want_x.to_bits(), "x bit-exact for {pos:?}");
            assert_eq!(p.y.to_bits(), want_y.to_bits(), "y bit-exact for {pos:?}");
        }
        // NON-IDENTITY GUARD: the zoom must actually be doing work. A dead
        // transform (scale 1, offset 0) would satisfy the loop above only if the
        // reference were equally dead, so pin the applied magnitude.
        let far = v.project_world(Vec3::new(rx + 0.01, 0.0, ry), centre, 300.0);
        assert!(
            (far.x - centre.0 - 40.96).abs() < 1e-3,
            "0.01 Mercator at zoom 4096 must move 40.96 px, moved {}",
            far.x - centre.0
        );
    }

    /// Orthographic really is orthographic: `distance` cannot change the on-screen
    /// scale, but in perspective mode it must.
    #[test]
    fn ortho_ignores_distance_and_perspective_does_not() {
        let p = Vec3::new(0.25, 0.0, 0.1);
        let mut a = View::ortho_2d(Vec3::ZERO, [1000.0, 1000.0]);
        let near = a.project_world(p, (0.0, 0.0), 300.0).x;
        a.distance = 97.0;
        let far = a.project_world(p, (0.0, 0.0), 300.0).x;
        assert_eq!(near.to_bits(), far.to_bits(), "ortho scale is distance-invariant");

        let mut q = View::perspective_3d(Vec3::ZERO, 0.0, 0.9, 3.0, 50f32.to_radians());
        let s1 = q.project_world(p, (0.0, 0.0), 300.0);
        q.distance = 6.0;
        let s2 = q.project_world(p, (0.0, 0.0), 300.0);
        assert!(s1.visible && s2.visible, "both poses see the point");
        assert!(
            (s1.x - s2.x).abs() > 1.0,
            "perspective MUST foreshorten with distance ({} vs {})",
            s1.x,
            s2.x
        );
    }

    /// `Terrain::Flat` is the `Z(x,y) = 0` constraint and it *discards* the sample;
    /// `Heightfield` passes it through. Driven with a NON-zero sample so a
    /// pass-through bug cannot hide.
    #[test]
    fn flat_terrain_discards_the_heightfield_sample() {
        let flat = View::ortho_2d(Vec3::ZERO, [1.0, 1.0]);
        let hf = View::perspective_3d(Vec3::ZERO, 0.0, 0.7, 3.0, 1.0);
        assert_eq!(flat.terrain_height(37.5), 0.0, "Flat pins Z to 0");
        assert_eq!(hf.terrain_height(37.5), 37.5, "Heightfield passes Z through");
        assert_eq!(flat.terrain_height(-9.25), 0.0);
    }

    /// The constraint predicate must reject a view that misses ANY one constraint —
    /// four separate reds, so it cannot be a `true` in disguise.
    #[test]
    fn the_2d_constraint_predicate_rejects_each_single_violation() {
        let base = View::ortho_2d(Vec3::new(0.3, 0.0, 0.2), [512.0, 512.0]);
        assert!(base.is_constrained_2d(), "the constructor is constrained");
        let mut a = base;
        a.tilt = 0.001;
        assert!(!a.is_constrained_2d(), "non-zero tilt must fail");
        let mut b = base;
        b.azimuth = 0.001;
        assert!(!b.is_constrained_2d(), "non-zero azimuth must fail");
        let mut c = base;
        c.projection = Projection::Perspective { fov_y: 0.9 };
        assert!(!c.is_constrained_2d(), "perspective must fail");
        let mut d = base;
        d.terrain = Terrain::Heightfield;
        assert!(!d.is_constrained_2d(), "heightfield must fail");
    }

    /// `constrain_2d` collapses a genuinely tilted, rotated, perspective view onto
    /// the constraint surface AND keeps the on-screen scale at the target plane.
    #[test]
    fn constrain_2d_flattens_a_non_trivial_3d_view() {
        let mut v = View::perspective_3d(
            Vec3::new(1.5, 3.0, -2.5),
            127f32.to_radians(),
            58f32.to_radians(),
            4.0,
            50f32.to_radians(),
        );
        assert!(!v.is_constrained_2d(), "starts unconstrained");
        v.constrain_2d(300.0);
        assert!(v.is_constrained_2d(), "collapsed onto the constraint surface");
        assert_eq!(v.target.y, 0.0, "ground plane");
        assert!(!v.post.any(), "post-FX bypassed in 2D");
        // The scale is the perspective scale at the target plane: focal/distance.
        let want = (300.0 / (50f32.to_radians() * 0.5).tan()) / 4.0;
        match v.projection {
            Projection::Ortho { px_per_unit } => {
                assert!((px_per_unit[0] - want).abs() < 1e-3, "{:?} vs {want}", px_per_unit)
            }
            _ => panic!("must be ortho"),
        }
    }

    /// The **matrix** agrees with the CPU projection writer in BOTH modes — the
    /// GPU and CPU lanes cannot drift because they are the same frame. Checked at
    /// non-identity poses and with an off-centre pane.
    #[test]
    fn view_proj_px_agrees_with_the_cpu_projection_in_both_modes() {
        let viewport = (800.0f32, 600.0f32);
        let cases = [
            View::ortho_2d(Vec3::new(0.36, 0.0, 0.28), [1024.0, 1024.0]),
            View::perspective_3d(
                Vec3::new(0.2, 0.0, -0.4),
                71f32.to_radians(),
                49f32.to_radians(),
                3.0,
                50f32.to_radians(),
            ),
        ];
        let centre = (417.0f32, 271.0f32);
        for v in cases {
            let m = v.view_proj_px(centre, viewport);
            let probes = [
                Vec3::new(0.30, 0.0, 0.25),
                Vec3::new(0.40, 0.0, 0.31),
                Vec3::new(0.36, 0.0, 0.28),
            ];
            let mut moved = 0;
            for p in probes {
                let cpu = v.project_px(v.view_space(p), centre, viewport.1 * 0.5);
                if !cpu.visible {
                    continue;
                }
                let clip = m * p.extend(1.0);
                assert!(clip.w.abs() > 1e-9, "w must be non-degenerate");
                let ndc = (clip.x / clip.w, clip.y / clip.w);
                let px = (
                    (ndc.0 * 0.5 + 0.5) * viewport.0,
                    (0.5 - ndc.1 * 0.5) * viewport.1,
                );
                assert!(
                    (px.0 - cpu.x).abs() < 0.01 && (px.1 - cpu.y).abs() < 0.01,
                    "matrix {px:?} vs cpu ({}, {}) for {p:?} in {:?}",
                    cpu.x,
                    cpu.y,
                    v.projection
                );
                if (px.0 - centre.0).abs() > 1.0 {
                    moved += 1;
                }
            }
            assert!(moved > 0, "at least one probe must land off the pane centre");
        }
    }

    /// **`GroundFrame2d::apply` must use the SEPARATED operation order**, i.e.
    /// rotate first and multiply by `cos(tilt)` afterwards — never fold the
    /// foreshortening into the rotation rows.
    ///
    /// The two are algebraically equal and numerically are not: folding computes
    /// `(ct·sa)·dx + (ct·ca)·dz`, which distributes a multiply over an addition.
    /// Measured over 400k pseudo-random `(bearing, tilt, dx, dz)` draws, the folded
    /// form differs from the separated one in **52.5%** of cases — always by an ULP,
    /// never visibly, which is exactly what makes it dangerous: the 2D skins'
    /// bit-equality guards would start failing for a reason no screenshot could
    /// explain.
    ///
    /// This test exists because a source mutation that folded the rows **passed** the
    /// six-pose sweep in `facett-map`'s `layer.rs` guard. That sweep's poses were too
    /// kind (half of them had `tilt = 0`, where `ct = 1` makes folding exact). So
    /// this pins the ordering directly and asserts the folded form is *reachably*
    /// different, which makes the mutation impossible to sneak past.
    #[test]
    fn the_ground_frame_must_not_fold_the_foreshorten_into_the_rotation() {
        // A deterministic LCG — no rand dep, and the same draws every run.
        let mut state = 0x2545_F491_4F6C_DD1Du64;
        let mut next = move || {
            state ^= state >> 12;
            state ^= state << 25;
            state ^= state >> 27;
            ((state.wrapping_mul(0x2545_F491_4F6C_DD1D) >> 11) as f64) / ((1u64 << 53) as f64)
        };

        let mut checked = 0u32;
        let mut folded_differs = 0u32;
        for _ in 0..20_000 {
            let bearing = next() * 720.0 - 360.0;
            let tilt = next() * 89.0;
            let dx = next() * 10_000.0 - 5_000.0;
            let dz = next() * 10_000.0 - 5_000.0;

            let f = View::ground_frame_2d_compass_deg(bearing, tilt);
            let (_, got_y) = f.apply(dx, dz);

            // The canonical (reference) order: rotate, THEN foreshorten.
            let separated = (f.rot[2] * dx + f.rot[3] * dz) * f.foreshorten;
            assert_eq!(
                got_y.to_bits(),
                separated.to_bits(),
                "apply must rotate then foreshorten, at bearing {bearing} tilt {tilt}"
            );

            // The folded order, which `apply` must NOT be using.
            let ct = f.foreshorten;
            let folded = (ct * f.rot[2]) * dx + (ct * f.rot[3]) * dz;
            if folded.to_bits() != separated.to_bits() {
                folded_differs += 1;
            }
            checked += 1;
        }
        let pct = 100.0 * f64::from(folded_differs) / f64::from(checked);
        println!("[item6] folded != separated in {folded_differs}/{checked} = {pct:.1}% of draws");
        assert_eq!(checked, 20_000, "the sweep must have run");
        // NON-VACUITY: if the two orders never differed, the assertion above would be
        // comparing a value to itself and this guard would be decoration.
        assert!(
            folded_differs > 1_000,
            "the folded form must be REACHABLY different or this guard is hollow \
             (differed in only {folded_differs} of {checked})"
        );
    }

    /// **Cull and draw must be exact inverses** or the compute cull drops ways that
    /// are on screen. In ortho mode the round-trip is bit-stable; in perspective
    /// mode (a real ray/plane intersect at a non-trivial tilt) it closes to well
    /// under a hundredth of a pixel.
    #[test]
    fn unproject_is_the_inverse_of_project_in_both_modes() {
        let centre = (417.0f32, 271.0f32);
        let half_h = 300.0f32;

        // ORTHO: bit-stable round-trip at zoom 4096 and a non-zero ref.
        let o = View::ortho_2d(Vec3::new(0.361_25, 0.0, 0.284_75), [4096.0, 4096.0]);
        for px in [(300.0f32, 200.0f32), (700.0, 512.0), (centre.0, centre.1)] {
            let w = o.unproject_ground_px(px, centre, half_h).expect("ortho always hits");
            let back = o.project_world(w, centre, half_h);
            assert_eq!(back.x.to_bits(), px.0.to_bits(), "ortho x round-trip for {px:?}");
            assert_eq!(back.y.to_bits(), px.1.to_bits(), "ortho y round-trip for {px:?}");
        }

        // ORTHO AT A REAL BEARING AND A REAL TILT — the arm the guard above could
        // never reach. `ortho_2d` is azimuth 0 / tilt 0, where `cos(tilt) == 1` and
        // the whole frame is the identity, so it cannot tell a working inverse from
        // one that multiplies by `cos(tilt)` where it should divide (LAW 2: the
        // identity value). That defect made the compute cull's box 2.42× too short at
        // tilt 50° and 14.9× at MAX_TILT 75°; the sweep below is red on it at every
        // non-zero tilt and stays bit-stable at tilt 0.
        for &(bearing, tilt) in &[(0.0f32, 50.0f32), (31.0, 50.0), (137.0, 75.0), (-44.0, 12.5), (73.0, 0.0)] {
            let o = View::ortho_2d_compass([0.361_25, 0.284_75], [4096.0, 2731.0], bearing, tilt);
            for px in [(300.0f32, 200.0f32), (700.0, 512.0), (120.0, 890.0), (centre.0, centre.1)] {
                let w = o.unproject_ground_px(px, centre, half_h).expect("ortho always hits");
                assert!(w.y.abs() < 1e-6, "a tilted ortho hit must be ON the ground plane, got y={}", w.y);
                let back = o.project_world(w, centre, half_h);
                assert!(
                    (back.x - px.0).abs() < 2e-3 && (back.y - px.1).abs() < 2e-3,
                    "ortho round-trip at bearing {bearing} tilt {tilt} for {px:?}: got ({}, {})",
                    back.x,
                    back.y
                );
            }
        }

        // …and the defect must be REACHABLE, or the sweep above is a green nobody has
        // seen red: the old expression (`target + right·cx + up·cy`, y dropped) is
        // written out here and must miss by pixels, not ULPs.
        let o = View::ortho_2d_compass([0.361_25, 0.284_75], [4096.0, 2731.0], 31.0, 50.0);
        let (_, right, up) = o.basis();
        let px = (700.0f32, 512.0f32);
        let old = o.target
            + right * ((px.0 - centre.0) / 4096.0)
            + up * (-(px.1 - centre.1) / 2731.0);
        let old_back = o.project_world(Vec3::new(old.x, 0.0, old.z), centre, half_h);
        let miss = ((old_back.x - px.0).powi(2) + (old_back.y - px.1).powi(2)).sqrt();
        assert!(miss > 10.0, "the pre-fix ortho inverse must be reachably wrong, missed by only {miss} px");

        // PERSPECTIVE at a real tilt/azimuth: ray/plane, so approximate but tight.
        let p = View::perspective_3d(
            Vec3::new(0.2, 0.0, -0.4),
            71f32.to_radians(),
            49f32.to_radians(),
            3.0,
            50f32.to_radians(),
        );
        let mut hits = 0;
        for px in [(417.0f32, 271.0f32), (500.0, 350.0), (330.0, 300.0)] {
            let Some(w) = p.unproject_ground_px(px, centre, half_h) else { continue };
            assert!(w.y.abs() < 1e-4, "the hit must be ON the ground plane, got y={}", w.y);
            let back = p.project_world(w, centre, half_h);
            if !back.visible {
                continue;
            }
            assert!(
                (back.x - px.0).abs() < 0.01 && (back.y - px.1).abs() < 0.01,
                "perspective round-trip {px:?} -> {w:?} -> ({}, {})",
                back.x,
                back.y
            );
            hits += 1;
        }
        assert!(hits >= 2, "the perspective sweep must actually hit the plane, hit {hits}");

        // NON-VACUITY: a ray that cannot reach the plane must say so, not hand back
        // a bogus point. At an 80-degree tilt the eye is genuinely LIFTED above the
        // plane (`eye.y = distance*cos(tilt) > 0`), so a ray through the top of the
        // screen points above the horizon and must miss.
        //
        // (Tilt exactly 90 degrees is NOT a valid probe here and it is worth
        // recording why: there the eye sits *on* the ground plane, so every ray
        // trivially "hits" it at t ~ 0. The first version of this assertion used
        // 90 degrees and failed for that reason — the miss case, not the code.)
        let low = View::perspective_3d(Vec3::ZERO, 0.0, 80f32.to_radians(), 3.0, 50f32.to_radians());
        assert!(low.eye().y > 0.4, "the probe camera must really be above the plane");
        assert!(
            low.unproject_ground_px((centre.0, 0.0), centre, half_h).is_none(),
            "a ray above the horizon must miss the ground plane"
        );
        // ...and the BOTTOM of the same screen must still hit, or the `None` above
        // would prove nothing (a function that always returns None would pass).
        assert!(
            low.unproject_ground_px((centre.0, 600.0), centre, half_h).is_some(),
            "the downward ray of the same camera must hit"
        );
    }

    /// The depth row must ORDER fragments in `[0,1]` in perspective mode (the wgpu
    /// convention the depth test needs), at a non-trivial pose.
    #[test]
    fn perspective_depth_row_orders_and_stays_normalised() {
        let v = View::perspective_3d(
            Vec3::ZERO,
            0.7,
            0.8,
            5.0,
            50f32.to_radians(),
        );
        let m = v.view_proj(800.0 / 600.0);
        let (fwd, _, _) = v.basis();
        let near_p = v.eye() + fwd * 3.5;
        let far_p = v.eye() + fwd * 6.5;
        let (a, b) = (m * near_p.extend(1.0), m * far_p.extend(1.0));
        assert!(a.w > 0.0 && b.w > 0.0, "both in front of the eye");
        let (za, zb) = (a.z / a.w, b.z / b.w);
        assert!(za < zb, "nearer must be smaller depth ({za} vs {zb})");
        assert!((0.0..=1.0).contains(&za) && (0.0..=1.0).contains(&zb), "{za}, {zb} in [0,1]");
    }

    /// **The ortho twin** (2026-09-29): the plan view's depth row. Ortho does not divide
    /// (`w ≡ 1`), so the depth map must be linear over the view's `[near, far]` band —
    /// before `14b1c6e` it reused perspective's `a, b` and gave `ndc.z = z`, which put the
    /// ground of a plan view (eye `distance = 1` above it) on exactly `1.0`, the depth
    /// clear value, so a `Less` test drew no ground (measured by the GPU-only
    /// `postfx_bypass` plan frame; this is the CPU guard for the same row). Every ortho
    /// constructor shares the row (`compose(.., divide = false, ..)`), and so does
    /// `view_proj_px`; each is checked.
    #[test]
    fn ortho_depth_row_puts_the_plan_ground_inside_the_band_and_orders_height() {
        let target = [0.361_25_f32, 0.284_75_f32];
        let views = [
            ("ortho_2d", View::ortho_2d(Vec3::new(target[0], 0.0, target[1]), [4096.0, 4096.0])),
            ("ortho_2d_map", View::ortho_2d_map(target, [4096.0, 2731.0])),
            ("ortho_2d_compass", View::ortho_2d_compass(target, [4096.0, 2731.0], 31.0, 0.0)),
            ("ortho_2d_compass tilted", View::ortho_2d_compass(target, [4096.0, 2731.0], 31.0, 50.0)),
        ];
        for (name, v) in views {
            assert!(v.projection.is_ortho(), "{name} is ortho");
            let ground = Vec3::new(target[0], 0.0, target[1]);
            let (fwd, _, _) = v.basis();
            for (how, m) in [("view_proj", v.view_proj(800.0 / 600.0)), ("view_proj_px", v.view_proj_px((417.0, 271.0), (800.0, 600.0)))] {
                let depth = |p: Vec3| {
                    let c = m * p.extend(1.0);
                    assert!((c.w - 1.0).abs() < 1e-6, "{name}/{how}: ortho does not divide (w = {})", c.w);
                    c.z / c.w
                };
                let z_ground = depth(ground);
                // THE PLAN-VIEW CASE: the eye is `distance` (= 1 here) above the ground.
                assert!(
                    z_ground > 0.0 && z_ground < 1.0,
                    "{name}/{how}: the ground at the target must land strictly inside (0,1), got {z_ground} \
                     (1.0 is the depth clear value — a Less test would draw no ground)"
                );
                // Along the view ray: toward the eye is nearer (smaller), away is farther.
                let above = depth(ground - fwd * 0.5);
                let below = depth(ground + fwd * 0.5);
                assert!(above < z_ground && z_ground < below, "{name}/{how}: {above} < {z_ground} < {below}");
                // Geometry taller than `distance` above the target is not near-clipped, and
                // geometry below the ground still lies inside the band.
                let tall = depth(ground - fwd * (v.distance + 0.5));
                for (what, z) in [("above", above), ("below", below), ("taller than the eye", tall)] {
                    assert!((0.0..=1.0).contains(&z), "{name}/{how}: {what} at {z} is outside [0,1]");
                }
            }
        }
    }
}