facett-map3d/tests/garmin_overlays_robot.rs
//! **ROBOT-UI — Garmin nav overlays** (live egui_kittest render + data drive).
//!
//! Drives the LIVE `Map3D` navig Facet the way the web demo does (CPU painter) and
//! proves the three Garmin overlays on the real surface — both as PIXELS (they paint
//! into their expected screen regions) and as DATA (`state_json` exposes the bearing,
//! scale, and POI count):
//! • NORTH ROSE — turning the overlays on paints the compass into the **top-left**;
//! rotating the camera bearing changes `compass_bearing_deg`.
//! • SCALE BAR — the scale bar paints into the **bottom-left** and `state_json`
//! carries a metres value + a `"… m"/"… km"` label; dollying out grows it.
//! • POI LAYER — turning the POI layer on paints markers (the frame changes) and
//! `poi_count` reports how many draw; hiding a category drops the count.
//! • ROADS — a route ribbon draws in the main 3-D view (the ground-plane fix), so a
//! Garmin-style route is visible (guards the invisible-roads regression).
//!
//! Run: `cargo test -p facett-map3d --features testmatrix --test garmin_overlays_robot`
use egui_kittest::Harness;
use facett_core::{Facet, Theme};
use facett_map::{LayerCategory, Way};
use facett_map3d::{Map3D, Msg, PoiNode};
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 pois() -> Vec<PoiNode> {
vec![
PoiNode::new(17.7018, 59.6118, &[("amenity", "bar")], "Loki's"),
PoiNode::new(17.7030, 59.6122, &[("shop", "kiosk")], ""),
PoiNode::new(17.7024, 59.6130, &[("amenity", "restaurant")], "Grill"),
PoiNode::new(17.7036, 59.6126, &[("amenity", "fuel")], "Q8"),
]
}
fn 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 a map builder's output headless at a pinned clock; return the frame.
fn render(build: impl FnOnce(&[Way]) -> Map3D) -> image::RgbaImage {
use std::cell::RefCell;
use std::rc::Rc;
let ways = city();
let view = Rc::new(RefCell::new({
let mut m = build(&ways);
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 garmin_overlays_paint_and_expose_data() {
let ways = city();
let pts = route_points();
// ── PIXELS: overlays-off vs overlays-on differ in their expected regions ──────
let plain = render(|w| Map3D::from_geometry("navig", w));
let garmin = render(|w| {
Map3D::from_geometry("navig", w)
.with_route(w, &route_points())
.with_pois(w, &pois())
.show_pois(true)
.with_compass(true)
.with_scale_bar(true)
});
let compass_diff = diff_in_region(&plain, &garmin, 0.0, 0.0, 0.24, 0.24); // top-left rose
let scale_diff = diff_in_region(&plain, &garmin, 0.0, 0.86, 0.4, 0.99); // bottom-left bar
let scene_diff = diff_in_region(&plain, &garmin, 0.1, 0.2, 0.9, 0.8); // POI pins + road ribbon
let compass_painted = compass_diff > 20;
let scale_painted = scale_diff > 12;
let scene_painted = scene_diff > 20;
assert!(compass_painted, "the north rose paints into the top-left (diff={compass_diff} px)");
assert!(scale_painted, "the scale bar paints into the bottom-left (diff={scale_diff} px)");
assert!(
scene_painted,
"POI pins + the road route ribbon paint into the main 3-D view (diff={scene_diff}); \
a 0 here is the invisible-roads / no-POI regression"
);
// ── DATA: state_json exposes bearing + scale + poi_count, and they respond ────
use std::cell::RefCell;
use std::rc::Rc;
let view = Rc::new(RefCell::new(
Map3D::from_geometry("navig", &ways)
.with_route(&ways, &pts)
.with_pois(&ways, &pois())
.show_pois(true)
.with_compass(true)
.with_scale_bar(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);
});
harness.run_steps(2);
let j0 = view.borrow().state_json();
let bearing0 = j0["compass_bearing_deg"].as_f64().unwrap();
let scale_m0 = j0["scale_metres"].as_f64().unwrap();
let scale_label = j0["scale_label"].as_str().unwrap().to_string();
let poi_total = j0["poi_total"].as_u64().unwrap();
let poi_on = j0["poi_count"].as_u64().unwrap();
assert_eq!(poi_total, 4, "all 4 POIs attached");
assert_eq!(poi_on, 4, "the POI layer reports all 4 drawing");
assert!(scale_m0 > 0.0, "the scale bar reports a positive ground distance");
assert!(
scale_label.ends_with('m') || scale_label.ends_with("km"),
"the scale label carries a unit ({scale_label:?})"
);
assert!(j0["compass"].as_bool().unwrap() && j0["scale_bar"].as_bool().unwrap() && j0["show_pois"].as_bool().unwrap());
// Rotate the bearing → compass_bearing_deg tracks it.
view.borrow_mut().update(Msg::SetCameraPose {
target: None,
azimuth: Some(bearing0 as f32 + 1.2),
elevation: None,
distance: None,
fov_y: None,
});
harness.run_steps(2);
let bearing1 = view.borrow().state_json()["compass_bearing_deg"].as_f64().unwrap();
let bearing_moved = (bearing1 - bearing0).abs() > 1.0;
assert!(bearing_moved, "rotating the camera moves the compass bearing ({bearing0:.1}° → {bearing1:.1}°)");
// Dolly OUT → the scale distance grows.
let far_distance = view.borrow().render_camera().distance * 6.0;
view.borrow_mut().update(Msg::SetCameraPose {
target: None,
azimuth: None,
elevation: None,
distance: Some(far_distance),
fov_y: None,
});
harness.run_steps(2);
let scale_m1 = view.borrow().state_json()["scale_metres"].as_f64().unwrap();
let scale_grew = scale_m1 > scale_m0;
assert!(scale_grew, "dollying out grows the scale distance ({scale_m0} → {scale_m1} m)");
// Hide the Bars category → poi_count drops.
view.borrow_mut().set_poi_layer(LayerCategory::Bars, false);
harness.run_steps(1);
let poi_hidden = view.borrow().state_json()["poi_count"].as_u64().unwrap();
let poi_dropped = poi_hidden < poi_on;
assert!(poi_dropped, "hiding a POI category drops the count ({poi_on} → {poi_hidden})");
#[cfg(feature = "testmatrix")]
{
let ok = compass_painted
&& scale_painted
&& scene_painted
&& poi_total == 4
&& poi_on == 4
&& bearing_moved
&& scale_grew
&& poi_dropped;
facett_core::testmatrix::emit(
"facett-map3d",
"garmin_overlays_paint_and_expose_data",
ok,
&format!(
"compass_diff={compass_diff} scale_diff={scale_diff} scene_diff={scene_diff} \
poi_total={poi_total} poi_on={poi_on} poi_hidden={poi_hidden} \
bearing {bearing0:.1}->{bearing1:.1} scale {scale_m0}->{scale_m1} label={scale_label:?}"
),
);
}
}