facett-map3d/tests/route_robot.rs
//! **ROBOT-UI — route overlay + turn-by-turn follow** (live egui_kittest render + data drive).
//!
//! Drives the LIVE `Map3D` Facet the way the web demo does (CPU painter) and proves
//! BOTH halves of the car-nav route feature on the live surface:
//! • RENDER: with the minimap inset on, attaching a route paints the "your route"
//! line + waypoint dots into the **bottom-right inset** AND now draws the flat
//! ground-plane route **ribbon into the main 3-D view** — both the inset and the
//! main scene differ from the no-route frame. (Before the osm-3d-gaps §F fix the
//! 3-D ribbon was back-face-culled from above, so only the inset changed; the
//! ribbon now pixel-draws, so the main-view change is asserted too.)
//! • DRIVE: turning turn-by-turn FOLLOW on and running real frames advances
//! `route_progress` in `state_json` and MOVES the reported camera target — the
//! camera chases the route.
//!
//! Run: `cargo test -p facett-map3d --features testmatrix --test route_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
}
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 (route on/off) headless at a pinned clock, minimap inset ON, and
/// return the frame.
fn render(route: 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);
if route {
m.set_route(&ways, &pts);
}
m.spin = false;
m.set_clock(0.0); // pin → deterministic, no auto-motion
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")
}
/// Count pixels that differ between two frames inside a fractional sub-rect
/// (x0..x1, y0..y1 in [0,1]).
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 route_paints_on_the_minimap_and_follow_camera_advances() {
// ── RENDER: the route HUD line changes the bottom-right minimap inset ─────────
let plain = render(false);
let routed = render(true);
// The inset lives in the bottom-right ~30% of the frame; the route line/dots
// paint there. The upper-left is the main 3-D scene, where the flat ground-plane
// route ribbon now draws too (osm-3d-gaps §F: it was back-face-culled before).
let inset_diff = diff_in_region(&plain, &routed, 0.6, 0.6, 1.0, 1.0);
let main_diff = diff_in_region(&plain, &routed, 0.0, 0.0, 0.6, 0.6);
let painted = inset_diff > 20;
let ribbon_in_3d = main_diff > 20;
assert!(painted, "the route paints into the minimap inset (diff={inset_diff} px)");
assert!(
ribbon_in_3d,
"the ground-plane route ribbon now draws in the main 3-D view (upper-left diff={main_diff}); \
a 0 here is the invisible-roads regression (osm-3d-gaps §F)"
);
// ── DRIVE: follow mode + running real frames advances progress + moves camera ─
use std::cell::RefCell;
use std::rc::Rc;
let ways = city();
let pts = l_route_points();
let view = Rc::new(RefCell::new(
// clock UNPINNED → the per-frame tick auto-advances the follow.
Map3D::from_geometry("OSM 3D", &ways).with_route(&ways, &pts).with_route_follow(true),
));
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);
});
let target_of = |m: &Map3D| {
let j = m.state_json();
let t = j["camera"]["target"].as_array().unwrap().clone();
[t[0].as_f64().unwrap() as f32, t[2].as_f64().unwrap() as f32]
};
harness.run_steps(2);
let p0 = view.borrow().route_progress();
let t0 = target_of(&view.borrow());
harness.run_steps(30);
let p1 = view.borrow().route_progress();
let t1 = target_of(&view.borrow());
let advanced = p1 > p0 + 1e-3;
let moved = ((t1[0] - t0[0]).powi(2) + (t1[1] - t0[1]).powi(2)).sqrt() > 1e-3;
let following = view.borrow().route_follow_active();
assert!(following, "state_json reports the camera is following the route");
assert!(advanced, "running frames advances route_progress ({p0} → {p1})");
assert!(moved, "the follow camera target moves as it drives the route");
#[cfg(feature = "testmatrix")]
{
let ok = painted && ribbon_in_3d && advanced && moved && following;
facett_core::testmatrix::emit(
"facett-map3d",
"route_robot_minimap_paints_and_follows",
ok,
&format!(
"inset_diff={inset_diff} main_diff={main_diff} painted={painted} ribbon_in_3d={ribbon_in_3d} \
progress {p0:.3}->{p1:.3} advanced={advanced} moved={moved} following={following}"
),
);
}
}