facett-map3d/tests/garmin_nav_robot.rs
//! **ROBOT-UI — Garmin turn-by-turn nav chrome** (live egui_kittest render + data drive).
//!
//! Drives the LIVE `Map3D` Facet headless and proves the new car-navigator gaps the
//! `feat-osm-garmin` work closed, as DATA (`state_json`) AND as pixels:
//! • MANEUVER MODEL: attaching an L-shaped route derives a discrete turn maneuver
//! (`maneuver_count == 1`) and a next-turn banner ("… Turn … in …").
//! • BANNER: turning the maneuver overlay on paints the top-of-screen banner (the
//! top-centre strip differs from the overlay-off frame).
//! • VEHICLE MARKER: the heading-oriented chevron reports on (`show_vehicle`) with a
//! live bearing.
//! • NIGHT MODE: toggling night flips the `night` state flag (the sun/sky preset swap).
//!
//! Run: `cargo test -p facett-map3d --features testmatrix --test garmin_nav_robot`
use egui_kittest::Harness;
use facett_core::{Facet, Theme};
use facett_map::Way;
use facett_map3d::Map3D;
fn city() -> Vec<Way> {
let mut ways = Vec::new();
let mut id = 1;
for r in 0..6 {
for c in 0..6 {
let bx = 17.70 + c as f64 * 0.0012;
let by = 59.61 + r as f64 * 0.0012;
let tags = vec![("building".to_string(), "yes".to_string())];
let ring = [(bx, by), (bx + 0.0007, by), (bx + 0.0007, by + 0.0007), (bx, by + 0.0007), (bx, by)];
ways.push(Way::from_lonlat(id, &ring, &tags, &Theme::default()).expect("a building way"));
id += 1;
}
}
ways
}
/// An L-shaped route (east, then north) — one 90° corner ⇒ exactly one turn maneuver.
fn l_route_points() -> Vec<(f32, f32)> {
let stops = vec![(17.7006, 59.6106), (17.7054, 59.6106), (17.7054, 59.6154)];
Way::line(9001, "road", stops, egui::Color32::WHITE, 2.0).points
}
const SIZE: f32 = 480.0;
/// Render the map headless (maneuver overlay on/off) at a pinned clock, route + minimap
/// on, and return the frame.
fn render(maneuvers: bool) -> image::RgbaImage {
use std::cell::RefCell;
use std::rc::Rc;
let ways = city();
let pts = l_route_points();
let view = Rc::new(RefCell::new({
let mut m = Map3D::from_geometry("OSM 3D", &ways).with_minimap(true);
m.set_route(&ways, &pts);
m.set_maneuvers(maneuvers);
m.set_vehicle(true);
m.spin = false;
m.set_clock(0.0);
m
}));
let v2 = view.clone();
let mut harness = Harness::builder().with_size(egui::vec2(SIZE, SIZE)).build_ui(move |ui| {
Facet::ui(&mut *v2.borrow_mut(), ui);
});
harness.run_steps(3);
harness.render().expect("headless render produced an image")
}
fn diff_in_region(a: &image::RgbaImage, b: &image::RgbaImage, x0: f32, y0: f32, x1: f32, y1: f32) -> u64 {
let (w, h) = a.dimensions();
let (px0, py0) = ((x0 * w as f32) as u32, (y0 * h as f32) as u32);
let (px1, py1) = ((x1 * w as f32) as u32, (y1 * h as f32) as u32);
let mut d = 0u64;
for y in py0..py1.min(h) {
for x in px0..px1.min(w) {
if a.get_pixel(x, y) != b.get_pixel(x, y) {
d += 1;
}
}
}
d
}
#[test]
fn maneuver_banner_vehicle_and_night_drive_the_state_and_paint() {
// ── MANEUVER MODEL + banner + vehicle as DATA ─────────────────────────────────
let ways = city();
let pts = l_route_points();
let mut m = Map3D::from_geometry("OSM 3D", &ways)
.with_minimap(true)
.with_maneuvers(true)
.with_vehicle(true);
m.set_route(&ways, &pts);
m.set_clock(0.0);
let plan = m.maneuver_plan().expect("a maneuver plan for the routed L");
let turns = plan.turn_count();
let banner = m.maneuver_banner().expect("a next-turn banner at the start");
let veh_bearing = m.vehicle_bearing_deg();
let j = m.state_json();
let count_json = j["maneuver_count"].as_u64().unwrap_or(0);
let banner_json = j["maneuver_banner"].as_str().unwrap_or("").to_string();
let show_man = j["show_maneuvers"].as_bool().unwrap_or(false);
let show_veh = j["show_vehicle"].as_bool().unwrap_or(false);
assert_eq!(turns, 1, "the L-route derives exactly one turn maneuver");
assert_eq!(count_json, 1, "state_json surfaces the turn count");
assert!(banner.contains(" in "), "the banner reads a distance: {banner}");
assert!(
banner.contains("Turn") || banner.contains("Bear") || banner.contains("Sharp") || banner.contains("U-turn"),
"the banner names a turn instruction: {banner}"
);
assert_eq!(banner, banner_json, "the typed banner matches state_json");
assert!(show_man, "state_json reports the maneuver overlay is on");
assert!(show_veh, "state_json reports the vehicle chevron is on");
assert!(veh_bearing.is_some(), "the vehicle has a live heading");
// ── NIGHT MODE flips the state flag (sun/sky preset swap) ──────────────────────
assert!(!m.night_on(), "day by default");
m.set_night(true);
assert!(m.night_on(), "night mode engages");
assert!(m.state_json()["night"].as_bool().unwrap_or(false), "state_json reports night");
// ── AUTO-ZOOM by speed is opt-in + moves the chase pose ────────────────────────
m.set_route_follow(true);
let dist_fixed = m.state_json()["camera"]["distance"].as_f64().unwrap();
m.set_auto_zoom(true);
let dist_auto = m.state_json()["camera"]["distance"].as_f64().unwrap();
let auto_zoomed = dist_auto > dist_fixed + 1e-4;
assert!(auto_zoomed, "auto-zoom dollies the follow camera out ({dist_fixed} → {dist_auto})");
// ── BANNER PAINTS: the top-centre strip differs with the overlay on vs off ─────
let plain = render(false);
let banded = render(true);
let banner_diff = diff_in_region(&plain, &banded, 0.2, 0.0, 0.8, 0.12);
let painted = banner_diff > 20;
assert!(painted, "the next-turn banner paints across the top strip (diff={banner_diff} px)");
#[cfg(feature = "testmatrix")]
{
let ok = turns == 1 && count_json == 1 && show_man && show_veh && veh_bearing.is_some() && auto_zoomed && painted;
facett_core::testmatrix::emit(
"facett-map3d",
"garmin_nav_robot_maneuver_banner_vehicle_night",
ok,
&format!(
"turns={turns} banner={banner:?} show_man={show_man} show_veh={show_veh} \
auto_zoomed={auto_zoomed} banner_diff={banner_diff} painted={painted}"
),
);
}
}
/// The two remaining Garmin gaps as DATA (`state_json`):
/// • CHEVRON-FLOW (F2): opt-in direction arrows march along the ribbon — the glyphs
/// bake extra triangles into the SAME overlay mesh and count in `state_json`
/// (off ⇒ 0, the red-when-broken oracle).
/// • PREDICTIVE LOOK-AHEAD (F1): the follow camera aims a look-ahead point AHEAD of the
/// vehicle — with it on the camera target LEADS the vehicle (anticipating the road);
/// off ⇒ the target sits on the vehicle.
#[test]
fn chevron_flow_bakes_into_the_mesh_and_lookahead_leads_the_vehicle() {
let ways = city();
let pts = l_route_points();
// ── CHEVRON-FLOW (F2) ─────────────────────────────────────────────────────────
let mut plain = Map3D::from_geometry("OSM 3D", &ways);
plain.set_route(&ways, &pts);
let mut chevroned = Map3D::from_geometry("OSM 3D", &ways).with_chevrons(true);
chevroned.set_route(&ways, &pts);
let base_tris = plain.state_json()["triangles"].as_u64().unwrap();
let cj = chevroned.state_json();
let chev_tris = cj["triangles"].as_u64().unwrap();
let chev_count = cj["chevron_count"].as_u64().unwrap_or(0);
let show_chev = cj["show_chevrons"].as_bool().unwrap_or(false);
assert!(show_chev, "state_json reports the chevron flow is on");
assert!(chev_count > 0, "the routed L carries a chevron train (got {chev_count})");
assert!(chev_tris > base_tris, "chevrons bake extra triangles into the overlay mesh ({base_tris} → {chev_tris})");
assert_eq!(plain.state_json()["chevron_count"].as_u64().unwrap_or(99), 0, "no chevrons when off");
// ── PREDICTIVE LOOK-AHEAD (F1) ────────────────────────────────────────────────
let mut m = Map3D::from_geometry("OSM 3D", &ways);
m.set_route(&ways, &pts);
m.set_clock(0.0);
m.set_route_follow(true);
m.update(facett_map3d::Msg::RouteProgress(0.5)); // mid-route, moving forward
let veh = m.route().unwrap().point_at(0.5);
let xz = |t: &serde_json::Value| {
let a = t.as_array().unwrap();
(a[0].as_f64().unwrap(), a[2].as_f64().unwrap())
};
let lead = |tx: f64, tz: f64| ((tx - veh[0] as f64).powi(2) + (tz - veh[2] as f64).powi(2)).sqrt();
// Look-ahead OFF: the follow target sits ON the vehicle.
let (ox, oz) = xz(&m.state_json()["camera"]["target"]);
let lead_off = lead(ox, oz);
m.set_lookahead(true);
let lj = m.state_json();
let (lx, lz) = xz(&lj["camera"]["target"]);
let lead_on = lead(lx, lz);
let look_on = lj["lookahead"].as_bool().unwrap_or(false);
let look_brg = lj["lookahead_bearing_deg"].as_f64();
assert!(look_on, "state_json reports look-ahead is on");
assert!(look_brg.is_some(), "state_json surfaces the anticipatory bearing");
assert!(lead_off < 1e-4, "plain follow locks the target on the vehicle (lead {lead_off})");
assert!(lead_on > 1e-3, "look-ahead makes the target LEAD the vehicle (lead {lead_on})");
// Toggling look-ahead back off returns the target to the vehicle.
m.set_lookahead(false);
let (rx, rz) = xz(&m.state_json()["camera"]["target"]);
assert!(lead(rx, rz) < 1e-4, "look-ahead off re-locks the target on the vehicle");
#[cfg(feature = "testmatrix")]
{
let ok = show_chev && chev_count > 0 && chev_tris > base_tris && look_on && lead_off < 1e-4 && lead_on > 1e-3;
facett_core::testmatrix::emit(
"facett-map3d",
"garmin_nav_robot_chevron_flow_and_lookahead",
ok,
&format!("chev_count={chev_count} base_tris={base_tris} chev_tris={chev_tris} lead_off={lead_off:.5} lead_on={lead_on:.5}"),
);
}
}