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
),
);
}
}