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

//! **Garmin north-rose / compass proof** (headless, data-level — no pixels).
//!
//! The car-navigator **north rose** is a pure function of the camera **bearing**
//! ([`CompassRose::from_camera`]). This asserts, as DATA (no framebuffer, no GPU):
//!   1. looking **north** (bearing 0) puts the N needle straight **up** (screen angle 0),
//!   2. rotating the camera bearing by +θ rotates the north needle by **−θ** (the rose
//!      counter-rotates rigidly — the world stays put while you turn),
//!   3. the four cardinals stay **90° apart** and rotate together,
//!   4. the projection is **deterministic** (a pure function of the camera).
//!
//! RED-when-broken: pin the needle to a constant (ignore the bearing) and (1)+(2) fail.
//!
//! Run: `cargo test -p facett-map3d --test compass_proof [--features testmatrix]`.

use facett_map3d::{CompassRose, OrbitCamera, V3};

/// A camera with a given compass bearing, settled to its goals (pure pose).
fn cam(bearing_deg: f32) -> OrbitCamera {
    let mut c = OrbitCamera::default();
    c.target = V3::new(0.0, 0.0, 0.0);
    c.goal_target = c.target;
    c.goal_distance = 3.0;
    c.set_bearing_deg(bearing_deg);
    c.set_tilt_deg(45.0);
    c.settle();
    c
}

/// Signed smallest angle between two screen angles (radians), in `(−π, π]`.
fn ang_delta(a: f32, b: f32) -> f32 {
    let mut d = (b - a).rem_euclid(std::f32::consts::TAU);
    if d > std::f32::consts::PI {
        d -= std::f32::consts::TAU;
    }
    d
}

#[test]
fn north_needle_points_up_at_bearing_zero_and_counter_rotates_with_bearing() {
    // (1) Looking north ⇒ N is up (screen angle 0).
    let n0 = CompassRose::from_camera(&cam(0.0)).north_screen_angle();
    assert!(n0.abs() < 1e-3, "north points up at bearing 0 (got {n0} rad)");

    // (2) A +θ bearing rotates the north needle by −θ (counter-rotation). Sweep bases.
    let mut worst_counter = 0.0f32;
    let mut counter_ok = true;
    for &base in &[0.0f32, 37.0, 130.0, 214.0, 305.0] {
        for &dtheta in &[15.0f32, 45.0, 90.0] {
            let a = CompassRose::from_camera(&cam(base)).north_screen_angle();
            let b = CompassRose::from_camera(&cam(base + dtheta)).north_screen_angle();
            let got = ang_delta(a, b).to_degrees(); // screen rotation
            let err = (got - (-dtheta)).abs();
            worst_counter = worst_counter.max(err);
            counter_ok &= err < 1.0;
        }
    }
    assert!(counter_ok, "a +θ bearing rotates the north needle −θ (worst {worst_counter:.3}°)");

    // (3) The cardinals sit 90° apart and rotate together with the bearing.
    let rose = CompassRose::from_camera(&cam(63.0));
    let (n, e, s, w) = (
        rose.cardinal_screen_angle(0.0),
        rose.cardinal_screen_angle(90.0),
        rose.cardinal_screen_angle(180.0),
        rose.cardinal_screen_angle(270.0),
    );
    let mut worst_quad = 0.0f32;
    for (a, b) in [(n, e), (e, s), (s, w), (w, n)] {
        worst_quad = worst_quad.max((ang_delta(a, b).to_degrees().abs() - 90.0).abs());
    }
    assert!(worst_quad < 1e-2, "the four cardinals stay 90° apart (worst {worst_quad:.4}°)");

    // North's unit screen dir is up (0, −1) at bearing 0; the vector is unit length.
    let [dx, dy] = CompassRose::from_camera(&cam(0.0)).cardinal_screen_dir(0.0);
    assert!((dx).abs() < 1e-3 && (dy + 1.0).abs() < 1e-3, "north dir is up (0,−1), got ({dx},{dy})");
    let [ex, ey] = CompassRose::from_camera(&cam(0.0)).cardinal_screen_dir(90.0);
    assert!((ex - 1.0).abs() < 1e-3 && ey.abs() < 1e-3, "east dir is right (1,0), got ({ex},{ey})");
    let len = (ex * ex + ey * ey).sqrt();
    assert!((len - 1.0).abs() < 1e-4, "cardinal dir is a unit vector (len={len})");

    // (4) Determinism: a pure function of the camera.
    let d1 = CompassRose::from_camera(&cam(118.0));
    let d2 = CompassRose::from_camera(&cam(118.0));
    assert_eq!(d1, d2, "the compass is a pure function of the camera bearing");
    assert!((d1.bearing_deg - 118.0).abs() < 1e-2, "the reported bearing is the camera bearing");

    #[cfg(feature = "testmatrix")]
    {
        let ok = n0.abs() < 1e-3 && counter_ok && worst_quad < 1e-2 && d1 == d2;
        facett_core::testmatrix::emit(
            "facett-map3d",
            "garmin_compass_north_rose_counter_rotates",
            ok,
            &format!(
                "north0={n0:.4} counter_ok={counter_ok} worst_counter={worst_counter:.3} \
                 worst_quad={worst_quad:.4} bearing={:.2} det={}",
                d1.bearing_deg,
                d1 == d2
            ),
        );
    }
}