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

nordisk/facett

facett-map3d/tests/flight_altitude_is_continuous.rs

//! **A-2: THE FLIGHT CAMERA MUST NOT TELEPORT, AND MUST NOT BE SILENTLY PITCHED.**
//!
//! The owner's ruling, delegated and taken: **make the camera continuous.** A camera that
//! jumps breaks the flight illusion, which is the one thing flight mode is for.
//!
//! Two defects at the same two call sites — `OrbitCamera::update` and
//! `OrbitCamera::settle`, which both end in `clamp_above_ground` and both start with
//! `clamp_elev`. Neither is a bug in those functions: both are ORBIT constraints being
//! applied to a pose a 6DOF rig owns.
//!
//! ## A-2a — the ground clamp teleports the eye, then PINS it
//!
//! `clamp_above_ground` step 1 raises `target.y` to `surface + SURFACE_EPS` when it is
//! below. The eye is DERIVED (`eye = target − fwd·distance`, `fwd.y = −sin(elevation)`),
//! so raising the target by Δ raises the eye by exactly Δ.
//!
//! `Map3D::look_from` parks the target one dolly-length down the view ray
//! (`DOLLY_DU = 2.0`), so nose-down flight puts the target BELOW the eye:
//! `target.y = altitude_m·du_per_m + DOLLY_DU·sin(pitch)`, `sin(pitch) < 0`. The clamp
//! therefore fires whenever
//!
//! ```text
//! altitude_m · du_per_m  <  SURFACE_EPS − DOLLY_DU · sin(pitch)
//! ```
//!
//! and below that threshold it writes `goal_target.y` too, so
//! `eye.y = SURFACE_EPS − DOLLY_DU·sin(pitch)` **regardless of `altitude_m`** — the eye
//! stops following the commanded height entirely. korp's rig calls `look_from` every
//! frame, so it re-fires every frame.
//!
//! ## A-2b — a second, unconditional jump
//!
//! `clamp_elev(e) = e.clamp(MIN_ELEV = 0.02, ELEV_LIMIT)`. `look_from` writes
//! `elevation = −pitch` unclamped, so **any level or climbing pose is pitched ~1.15°
//! nose-down on the very next frame**, moving the derived eye by `distance·sin(0.02)`.
//!
//! ## The measurement
//!
//! Headless, no GPU, no adapter: sweep the commanded altitude down in 1 m steps at a
//! fixed nose-down pitch and watch where the renderer's own camera actually puts its
//! eye. Three claims, each able to fail on its own:
//!
//! 1. **monotone** — a lower commanded altitude is a lower eye, always;
//! 2. **no teleport** — no step-to-step move larger than 1.05× the commanded step;
//! 3. **tracking** — the eye is at the commanded altitude, to `SURFACE_EPS`.
//!
//! Run: `cargo test -p facett-map3d --test flight_altitude_is_continuous -- --nocapture`

use facett_map3d::{camera::OrbitCamera, Map3D};

mod common;

/// Central Stockholm — the centre `common::town` builds around.
const STOCKHOLM: (f64, f64) = (18.0686, 59.3293);
/// A scene a kilometre across.
const SPAN_M: f64 = 1_000.0;
/// Nose-down, hard enough that the dolly target is well below the eye. This is the
/// regime the 2026-08-22 measurement caught (506 m -> 637 m in ONE frame).
const PITCH: f64 = -0.25;
/// The sweep: 2000 m down to 1 m, one metre at a time.
const TOP_M: i32 = 2_000;

fn scene() -> Map3D {
    let m = Map3D::from_geometry("flight-altitude", &common::town(SPAN_M));
    assert!(m.geo_bbox().is_some(), "precondition: the scene has a geographic span");
    m
}

/// `(commanded altitude in m, the eye height the renderer's camera reports, in m)`.
fn sweep(m: &mut Map3D, pitch: f64) -> Vec<(f64, f64)> {
    let m_per_du = f64::from(m.metres_per_design_unit());
    let mut out = Vec::with_capacity(TOP_M as usize);
    for a in (1..=TOP_M).rev() {
        let alt = f64::from(a);
        assert!(
            m.look_from_lonlat(STOCKHOLM.0, STOCKHOLM.1, alt, 0.0, pitch, 0.0),
            "a scene with a geo bbox must accept a geographic pose"
        );
        // The rig commands a pose every frame and the renderer settles it every frame.
        // `settle` is `update` with the damping removed, which is the same two clamps.
        m.cam.settle();
        out.push((alt, f64::from(m.render_camera().eye().y) * m_per_du));
    }
    out
}

#[test]
fn descending_flight_lowers_the_eye_by_exactly_what_was_commanded() {
    let mut m = scene();
    let m_per_du = f64::from(m.metres_per_design_unit());
    let du_per_m = 1.0 / m_per_du;
    // The predicted threshold, from the geometry above — printed so the sweep's own
    // crossing can be compared against the algebra rather than merely observed.
    let threshold_m =
        (f64::from(OrbitCamera::SURFACE_EPS) - f64::from(Map3D::DOLLY_DU) * PITCH.sin()) * m_per_du;
    eprintln!(
        "[A-2a] scene: {m_per_du:.1} m/du ({du_per_m:.6} du/m), pitch {PITCH} rad, \
         predicted clamp threshold {threshold_m:.1} m"
    );

    let s = sweep(&mut m, PITCH);

    // ── (iii) TRACKING: the eye is where it was told to be.
    let (worst_alt, worst_err) = s
        .iter()
        .map(|(a, e)| (*a, (e - a).abs()))
        .fold((0.0, 0.0), |acc, x| if x.1 > acc.1 { x } else { acc });
    // The clamp's own epsilon is the only slack a correct implementation needs.
    let tol_m = f64::from(OrbitCamera::SURFACE_EPS) * m_per_du;
    eprintln!(
        "[A-2a] worst tracking error {worst_err:.1} m at a commanded {worst_alt:.0} m \
         (tolerance {tol_m:.3} m)"
    );

    // ── (ii) NO TELEPORT: consecutive commanded metres move the eye a metre.
    let mut worst_step = (0.0f64, 0.0f64);
    for w in s.windows(2) {
        let (a0, e0) = w[0];
        let (a1, e1) = w[1];
        let commanded = (a0 - a1).abs();
        let got = (e0 - e1).abs();
        if got > worst_step.1 {
            worst_step = (a1, got);
        }
        let _ = commanded;
    }
    eprintln!(
        "[A-2a] largest single-step eye move {:.1} m (commanded 1 m) at {:.0} m",
        worst_step.1, worst_step.0
    );

    // ── (i) MONOTONE: lower command, lower eye — no flat stretch, no reversal.
    let mut flat = Vec::new();
    for w in s.windows(2) {
        if w[1].1 >= w[0].1 - 1e-9 {
            flat.push(w[1].0);
        }
    }
    eprintln!(
        "[A-2a] {} of {} steps failed to lower the eye at all (first at {:?} m)",
        flat.len(),
        s.len() - 1,
        flat.first()
    );

    assert!(
        flat.is_empty(),
        "(i) the eye must fall whenever the commanded altitude does — {} of {} one-metre \
         steps moved it by nothing or upward, starting at {:?} m. Below the clamp \
         threshold the eye stops following the command entirely.",
        flat.len(),
        s.len() - 1,
        flat.first()
    );
    assert!(
        worst_step.1 <= 1.05,
        "(ii) a one-metre command must not move the eye by {:.1} m — that is a TELEPORT, \
         at a commanded altitude of {:.0} m (predicted clamp threshold {threshold_m:.1} m)",
        worst_step.1,
        worst_step.0
    );
    assert!(
        worst_err <= tol_m,
        "(iii) the eye must sit at the altitude it was given: worst error {worst_err:.1} m \
         at a commanded {worst_alt:.0} m (tolerance {tol_m:.3} m)"
    );
}

/// **A-2b: a level pose must stay level.**
///
/// `look_from` writes `elevation = −pitch`. For `pitch = 0` that is `0.0`, and the orbit
/// band's `MIN_ELEV = 0.02` then pushes it to 1.15° nose-down on the next `settle`. The
/// eye moves by `distance·sin(MIN_ELEV)` for a pose nobody changed.
#[test]
fn a_level_flight_pose_is_not_silently_pitched_nose_down() {
    let mut m = scene();
    let m_per_du = f64::from(m.metres_per_design_unit());
    let alt_m = 800.0;
    assert!(m.look_from_lonlat(STOCKHOLM.0, STOCKHOLM.1, alt_m, 0.0, 0.0, 0.0));
    let before = m.render_camera();
    let (el_before, eye_before) = (before.elevation, before.eye().y);
    m.cam.settle();
    let after = m.render_camera();
    let (el_after, eye_after) = (after.elevation, after.eye().y);
    let moved_m = f64::from(eye_after - eye_before).abs() * m_per_du;
    eprintln!(
        "[A-2b] level pose: elevation {el_before} -> {el_after} rad, eye moved {moved_m:.1} m \
         ({} m/du)",
        m_per_du as i64
    );

    assert!(
        el_after.abs() < 1e-6,
        "a level flight pose must stay level — `settle` pitched elevation from \
         {el_before} to {el_after} rad ({:.2} degrees nose-down), because the ORBIT \
         band's MIN_ELEV was applied to a pose the 6DOF rig owns",
        el_after.to_degrees()
    );
    assert!(
        moved_m < 1.0,
        "…and the derived eye must not move for a pose nobody changed: it jumped \
         {moved_m:.1} m"
    );
}

/// **A climbing pose too** — the same clamp, the other side of level.
#[test]
fn a_climbing_flight_pose_keeps_its_nose_up() {
    let mut m = scene();
    let pitch = 0.20_f64; // nose up
    assert!(m.look_from_lonlat(STOCKHOLM.0, STOCKHOLM.1, 800.0, 0.0, pitch, 0.0));
    m.cam.settle();
    let el = m.render_camera().elevation;
    assert!(
        (f64::from(el) + pitch).abs() < 1e-5,
        "a climbing pose commands elevation {} rad; the camera settled at {el} rad",
        -pitch
    );
}