facett-map3d/tests/scalebar_proof.rs
//! **Garmin scale-bar proof** (headless, data-level — no pixels).
//!
//! The dynamic **scale bar** is a pure function of the ground metres-per-pixel (from
//! the camera dolly + the map's metres-per-design-unit calibration) plus a target bar
//! length ([`ScaleBar::compute`] / [`ScaleBar::from_camera`]). This asserts, as DATA:
//! 1. the chosen distance is always a **nice `1 / 2 / 5 × 10ⁿ`** value,
//! 2. the printed **label** reads metres below a km and km above (`"500 m"` / `"2 km"`),
//! 3. **dollying out grows** the bar's distance (zoom out ⇒ a longer scale),
//! 4. the bar's pixel width matches `metres / metres_per_pixel`,
//! 5. the projection is **deterministic**.
//!
//! RED-when-broken: hard-code the distance (ignore the zoom) and (3) fails.
//!
//! Run: `cargo test -p facett-map3d --test scalebar_proof [--features testmatrix]`.
use facett_map3d::{OrbitCamera, ScaleBar, V3};
use facett_map3d::garmin::nice_125;
/// Is `m` a nice `1 / 2 / 5 × 10ⁿ` value?
fn is_125(m: f32) -> bool {
if m.is_nan() || m <= 0.0 {
return false;
}
let base = 10f32.powf(m.log10().round());
// Try the exponent and its neighbours (log10 rounding wobble at 10ⁿ boundaries).
for e in [-1i32, 0, 1] {
let b = base * 10f32.powi(e);
for mult in [1.0f32, 2.0, 5.0] {
if (m - mult * b).abs() <= (mult * b) * 1e-3 + 1e-4 {
return true;
}
}
}
false
}
/// A camera dollied to `distance`, settled (pure pose).
fn cam(distance: f32) -> OrbitCamera {
let mut c = OrbitCamera::default();
c.target = V3::new(0.0, 0.0, 0.0);
c.goal_target = c.target;
c.set_bearing_deg(0.0);
c.set_tilt_deg(45.0);
c.distance = distance;
c.goal_distance = distance;
c.settle();
c.distance = distance; // settle() eases toward goal; pin it exactly for the proof
c
}
#[test]
fn scale_bar_is_nice_rounded_labelled_and_grows_with_zoom() {
// (1)+(2) nice rounding + labelling across a wide metres-per-pixel sweep.
let mut all_nice = true;
let mut labels_ok = true;
for &mpp in &[0.05f32, 0.2, 1.0, 4.0, 25.0, 120.0, 900.0] {
let bar = ScaleBar::compute(mpp, 96.0);
all_nice &= is_125(bar.metres);
let want = if bar.metres >= 1000.0 { "km" } else { "m" };
labels_ok &= bar.label.ends_with(want);
// The bar spans its metres at this scale.
let px_err = (bar.px_width - bar.metres / mpp).abs();
assert!(px_err < 1e-2, "px_width matches metres/mpp (err {px_err} at mpp={mpp})");
}
assert!(all_nice, "every scale distance is a nice 1/2/5×10ⁿ value");
assert!(labels_ok, "the label unit matches the distance (m < 1 km ≤ km)");
// nice_125 spot checks (RED if the rounding table is wrong).
assert_eq!(nice_125(1.0), 1.0);
assert_eq!(nice_125(3.0), 2.0);
assert_eq!(nice_125(4.0), 5.0);
assert_eq!(nice_125(8.0), 10.0);
assert_eq!(nice_125(430.0), 500.0);
assert!((nice_125(1800.0) - 2000.0).abs() < 1e-3);
// (3) Dollying OUT grows the bar's ground distance. metres_per_du = 50 m/unit.
let mpd = 50.0f32;
let h = 480.0f32;
let near = ScaleBar::from_camera(&cam(2.0), h, mpd, 96.0);
let mid = ScaleBar::from_camera(&cam(10.0), h, mpd, 96.0);
let far = ScaleBar::from_camera(&cam(60.0), h, mpd, 96.0);
let grows = near.metres <= mid.metres && mid.metres <= far.metres && near.metres < far.metres;
assert!(
grows,
"zooming out grows the scale distance ({} → {} → {} m); equal-across-zoom is the RED bug",
near.metres, mid.metres, far.metres
);
// (4) px_width ties out from the camera path too.
let px_err = (mid.px_width - mid.metres / (mid.metres / mid.px_width)).abs();
assert!(px_err < 1e-1, "px_width is self-consistent");
// (5) Determinism.
let d1 = ScaleBar::from_camera(&cam(12.0), h, mpd, 96.0);
let d2 = ScaleBar::from_camera(&cam(12.0), h, mpd, 96.0);
assert_eq!(d1, d2, "the scale bar is a pure function of (camera, viewport, calibration)");
#[cfg(feature = "testmatrix")]
{
let ok = all_nice && labels_ok && grows && d1 == d2;
facett_core::testmatrix::emit(
"facett-map3d",
"garmin_scale_bar_nice_and_grows_with_zoom",
ok,
&format!(
"all_nice={all_nice} labels_ok={labels_ok} grows={grows} \
near={} mid={} far={} near_label={:?} far_label={:?} det={}",
near.metres, mid.metres, far.metres, near.label, far.label, d1 == d2
),
);
}
}