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/camera_nav_proof.rs

//! **OSM-3D garmin-nav tilt/bearing proof** (headless, data-level A/B — no pixels).
//!
//! The car-navigator feel is two controls over the shared `OrbitCamera`:
//! **bearing** (compass heading the view looks along) and **tilt** (pitch down from
//! straight-above). This asserts, as DATA:
//!   1. changing BEARING rotates the horizontal heading by the same angle,
//!   2. increasing TILT lifts the view toward the horizon (forward pitch shallows),
//!   3. both round-trip through `bearing_deg()`/`tilt_deg()`, and
//!   4. the pose is deterministic under `settle()`.
//!
//! No GPU: reuses the pure `basis()`/`eye()` math, so it runs anywhere.
//!
//! Run: `cargo test -p facett-map3d --test camera_nav_proof [--features testmatrix]`.

use facett_map3d::{OrbitCamera, V3};

/// The camera's horizontal (ground-plane) heading unit vector for the current pose.
fn heading_xz(c: &OrbitCamera) -> (f32, f32) {
    let (fwd, _r, _u) = c.basis();
    let (x, z) = (fwd.x, fwd.z);
    let l = (x * x + z * z).sqrt().max(1e-9);
    (x / l, z / l)
}

/// Forward pitch: the downward component of the look direction (more negative =
/// steeper down toward the map; ~0 = level toward the horizon).
fn forward_pitch_y(c: &OrbitCamera) -> f32 {
    c.basis().0.y
}

fn nav_cam(bearing_deg: f32, tilt_deg: f32) -> OrbitCamera {
    let mut c = OrbitCamera::default();
    c.target = V3::new(0.0, 0.1, 0.0);
    c.goal_target = c.target;
    c.goal_distance = 3.0;
    c.set_bearing_deg(bearing_deg);
    c.set_tilt_deg(tilt_deg);
    c.settle();
    c
}

#[test]
fn tilt_bearing_drive_the_garmin_nav_view() {
    // (1) BEARING rotates the heading. For each base bearing, a +90° bearing change
    //     rotates the horizontal heading by ~90° (angle between the two unit vecs).
    let mut bearing_ok = true;
    let mut worst_bearing_err = 0.0f32;
    for &base in &[0.0f32, 37.0, 118.0, 205.0, 300.0] {
        let a = heading_xz(&nav_cam(base, 45.0));
        let b = heading_xz(&nav_cam(base + 90.0, 45.0));
        let dot = (a.0 * b.0 + a.1 * b.1).clamp(-1.0, 1.0);
        let angle = dot.acos().to_degrees();
        let err = (angle - 90.0).abs();
        worst_bearing_err = worst_bearing_err.max(err);
        bearing_ok &= err < 1.0;
    }
    assert!(bearing_ok, "a +90° bearing rotates the heading ~90° (worst err {worst_bearing_err:.3}°)");

    // (2) TILT shallows the pitch. Near top-down (tilt 10°) the view looks steeply
    //     DOWN; near the horizon (tilt 80°) it's nearly level → the downward
    //     component rises toward 0. Monotone across a tilt sweep.
    let pitches: Vec<f32> = [10.0f32, 30.0, 50.0, 70.0, 85.0]
        .iter()
        .map(|&t| forward_pitch_y(&nav_cam(0.0, t)))
        .collect();
    let mut tilt_monotone = true;
    for w in pitches.windows(2) {
        // higher tilt ⇒ shallower ⇒ pitch_y strictly greater (less negative).
        tilt_monotone &= w[1] > w[0] + 1e-4;
    }
    assert!(tilt_monotone, "increasing tilt shallows the pitch (pitch_y: {pitches:?})");
    assert!(pitches[0] < -0.5, "low tilt looks steeply down (pitch_y {})", pitches[0]);
    assert!(pitches[4] > pitches[0], "level tilt is far shallower than top-down");

    // (3) Round-trip: bearing/tilt read back what was set (within the clamp band).
    let c = nav_cam(217.0, 35.0);
    let bearing_rt = (c.bearing_deg() - 217.0).abs();
    let tilt_rt = (c.tilt_deg() - 35.0).abs();
    assert!(bearing_rt < 1e-3, "bearing round-trips, got {} (Δ {bearing_rt})", c.bearing_deg());
    assert!(tilt_rt < 1e-3, "tilt round-trips, got {} (Δ {tilt_rt})", c.tilt_deg());

    // (4) Determinism: same nav inputs ⇒ bit-identical pose.
    let d1 = nav_cam(118.0, 55.0);
    let d2 = nav_cam(118.0, 55.0);
    let deterministic = d1.eye() == d2.eye() && d1.bearing_deg() == d2.bearing_deg() && d1.tilt_deg() == d2.tilt_deg();
    assert!(deterministic, "nav pose is a pure function of (bearing, tilt)");

    #[cfg(feature = "testmatrix")]
    {
        let ok = bearing_ok && tilt_monotone && pitches[0] < -0.5 && bearing_rt < 1e-3 && tilt_rt < 1e-3 && deterministic;
        facett_core::testmatrix::emit(
            "facett-map3d",
            "camera_tilt_bearing_nav",
            ok,
            &format!(
                "bearing_err<1deg={bearing_ok} worst={worst_bearing_err:.3} tilt_monotone={tilt_monotone} \
                 pitch_topdown={:.3} pitch_level={:.3} bearing_rt={bearing_rt:.1e} tilt_rt={tilt_rt:.1e} det={deterministic}",
                pitches[0], pitches[4]
            ),
        );
    }
}