facett-map3d/tests/minimap_proof.rs
//! **OSM-3D minimap overview proof** (headless, data-level A/B — no pixels).
//!
//! The car-navigator / flight-sim **minimap inset** is a pure top-down projection
//! of the live camera over the scene ground bounds ([`MinimapView::compute`]). This
//! asserts, as DATA (no framebuffer, no GPU):
//! 1. rotating the camera **bearing** by θ rotates the minimap **heading** by θ,
//! 2. the heading **compass bearing** tracks the camera bearing 1:1,
//! 3. **panning** the focus slides the "you are here" marker in the matching
//! overview direction (and off-extent focus reads as out-of-bounds),
//! 4. the projection is **deterministic** (pure function of bounds + camera).
//!
//! The overview reuses the same `OrbitCamera` math the 3D view flies, so a square
//! scene makes a world-heading rotation show up as the identical rotation on the
//! inset.
//!
//! Run: `cargo test -p facett-map3d --test minimap_proof [--features testmatrix]`.
use facett_map3d::{MinimapView, OrbitCamera, V3, WorldBounds};
/// A **square** ground extent centred on the origin — square so the two axes share
/// one scale and a world-heading rotation maps to the same rotation in overview uv.
const SQUARE: WorldBounds = [-1.0, -1.0, 1.0, 1.0];
/// A camera looking at `target`, bearing set, settled to its goals (pure pose).
fn cam(bearing_deg: f32, target: V3) -> OrbitCamera {
let mut c = OrbitCamera {
target,
goal_target: target,
goal_distance: 3.0,
..Default::default()
};
c.set_bearing_deg(bearing_deg);
c.set_tilt_deg(45.0);
c.settle();
c
}
#[test]
fn bearing_rotates_the_minimap_heading_and_pan_slides_the_marker() {
// (1) BEARING rotates the heading vector by the same angle. For each base
// bearing, a +90° change rotates the overview heading by ~90°.
let mut worst_rot_err = 0.0f32;
let mut rot_ok = true;
for &base in &[0.0f32, 41.0, 137.0, 223.0, 311.0] {
let a = MinimapView::compute(SQUARE, &cam(base, V3::new(0.0, 0.0, 0.0))).heading;
let b = MinimapView::compute(SQUARE, &cam(base + 90.0, V3::new(0.0, 0.0, 0.0))).heading;
let dot = (a[0] * b[0] + a[1] * b[1]).clamp(-1.0, 1.0);
let ang = dot.acos().to_degrees();
let err = (ang - 90.0).abs();
worst_rot_err = worst_rot_err.max(err);
rot_ok &= err < 1.0;
// Heading is a unit vector.
let len = (a[0] * a[0] + a[1] * a[1]).sqrt();
assert!((len - 1.0).abs() < 1e-4, "heading is unit (len={len})");
}
assert!(rot_ok, "a +90° bearing rotates the minimap heading ~90° (worst {worst_rot_err:.3}°)");
// (2) The compass bearing the inset reports rotates 1:1 with the camera bearing.
let mut worst_track = 0.0f32;
for &base in &[10.0f32, 80.0, 200.0, 350.0] {
let v0 = MinimapView::compute(SQUARE, &cam(base, V3::new(0.0, 0.0, 0.0)));
let v1 = MinimapView::compute(SQUARE, &cam(base + 30.0, V3::new(0.0, 0.0, 0.0)));
// heading_deg derives from the heading vector — check the DELTA is 30°.
let d = (v1.heading_deg() - v0.heading_deg()).rem_euclid(360.0);
let d = if d > 180.0 { 360.0 - d } else { d };
worst_track = worst_track.max((d - 30.0).abs());
}
assert!(worst_track < 1.0, "the reported compass bearing tracks the camera 1:1 (worst {worst_track:.3}°)");
// (3) PANNING the focus slides the marker. Centred target sits at uv (0.5, 0.5)
// and is in-bounds; moving the target +x moves focus_uv.u right; a far target
// falls out of bounds.
let centre = MinimapView::compute(SQUARE, &cam(0.0, V3::new(0.0, 0.0, 0.0)));
assert!((centre.focus_uv[0] - 0.5).abs() < 1e-4 && (centre.focus_uv[1] - 0.5).abs() < 1e-4, "centred focus is uv (0.5,0.5), got {:?}", centre.focus_uv);
assert!(centre.focus_in_bounds(), "a centred focus is on the inset");
let east = MinimapView::compute(SQUARE, &cam(0.0, V3::new(0.5, 0.0, 0.0)));
assert!(east.focus_uv[0] > centre.focus_uv[0] + 0.1, "moving the target +x slides the marker right ({} > {})", east.focus_uv[0], centre.focus_uv[0]);
let south = MinimapView::compute(SQUARE, &cam(0.0, V3::new(0.0, 0.0, 0.5)));
assert!(south.focus_uv[1] > centre.focus_uv[1] + 0.1, "moving the target +z slides the marker down ({} > {})", south.focus_uv[1], centre.focus_uv[1]);
let off = MinimapView::compute(SQUARE, &cam(0.0, V3::new(4.0, 0.0, 0.0)));
assert!(!off.focus_in_bounds(), "a focus panned way off the extent reads out-of-bounds (uv {:?})", off.focus_uv);
// (4) Cone half-angle is a positive HUD wedge; determinism: pure function.
assert!(centre.cone_half > 0.0, "the view cone has a positive half-angle");
let d1 = MinimapView::compute(SQUARE, &cam(118.0, V3::new(0.2, 0.0, -0.3)));
let d2 = MinimapView::compute(SQUARE, &cam(118.0, V3::new(0.2, 0.0, -0.3)));
assert_eq!(d1, d2, "the minimap projection is a pure function of (bounds, camera)");
// The reported compass bearing IS the heading vector's compass angle — clockwise
// from +z, i.e. atan2(du, dv) — with no drift (a pure derive from `heading`).
let compass = centre.heading[0].atan2(centre.heading[1]).to_degrees().rem_euclid(360.0);
assert!((compass - centre.heading_deg()).abs() < 1e-3, "heading_deg matches the heading vector compass angle");
#[cfg(feature = "testmatrix")]
{
let ok = rot_ok
&& worst_track < 1.0
&& (centre.focus_uv[0] - 0.5).abs() < 1e-4
&& east.focus_uv[0] > centre.focus_uv[0] + 0.1
&& south.focus_uv[1] > centre.focus_uv[1] + 0.1
&& !off.focus_in_bounds()
&& centre.cone_half > 0.0
&& d1 == d2;
facett_core::testmatrix::emit(
"facett-map3d",
"minimap_overview_heading_and_marker",
ok,
&format!(
"rot_err<1deg={rot_ok} worst_rot={worst_rot_err:.3} bearing_track_err={worst_track:.3} \
centre_uv={:?} east_u={:.3} south_v={:.3} off_bounds={} cone_half={:.3} det={}",
centre.focus_uv, east.focus_uv[0], south.focus_uv[1], !off.focus_in_bounds(), centre.cone_half, d1 == d2
),
);
}
}