facett-map3d/tests/minimap_robot.rs
//! **ROBOT-UI — minimap inset** (live egui_kittest render + data drive).
//!
//! Drives the LIVE `Map3D` Facet the way the web demo does (CPU painter), with the
//! **minimap inset** enabled, and proves BOTH halves of the feature on the live
//! surface:
//! • RENDER: turning the minimap ON paints extra ink in the **bottom-right inset
//! region** vs the minimap-OFF baseline (the inset is really drawn), while the
//! rest of the frame is unchanged (the overlay is confined to its corner).
//! • DRIVE: orbiting the camera (a real bearing change) moves the reported
//! `minimap_heading_deg` in `state_json` — the overview swings with the view.
//!
//! Run: `cargo test -p facett-map3d --features testmatrix --test minimap_robot`
use egui_kittest::Harness;
use facett_core::{Facet, Theme};
use facett_map::Way;
use facett_map3d::Map3D;
fn building(id: i64, ring: &[(f64, f64)]) -> Way {
let tags = vec![("building".to_string(), "yes".to_string())];
Way::from_lonlat(id, ring, &tags, &Theme::default()).expect("a building way")
}
fn city() -> Vec<Way> {
let mut ways = Vec::new();
let mut id = 1;
for r in 0..8 {
for c in 0..8 {
let bx = 17.70 + c as f64 * 0.0010;
let by = 59.61 + r as f64 * 0.0010;
ways.push(building(id, &[(bx, by), (bx + 0.0006, by), (bx + 0.0006, by + 0.0006), (bx, by + 0.0006), (bx, by)]));
id += 1;
}
}
ways
}
const SIZE: f32 = 480.0;
/// Render the map (minimap on/off) headless and return the frame + its state_json.
fn render(minimap: bool) -> (image::RgbaImage, serde_json::Value) {
use std::cell::RefCell;
use std::rc::Rc;
let view = Rc::new(RefCell::new({
let mut m = Map3D::from_geometry("OSM 3D", &city()).with_minimap(minimap);
m.spin = false;
m.set_clock(0.0); // pin the clock → 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);
let img = harness.render().expect("headless render produced an image");
let state = view.borrow().state_json();
(img, state)
}
/// Count pixels that differ between two frames inside the given fractional sub-rect
/// (x0..x1, y0..y1 in [0,1]) — the region the caller expects to change (or not).
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 minimap_inset_paints_in_its_corner_and_swings_with_the_camera() {
let (on, state_on) = render(true);
let (off, _state_off) = render(false);
assert_eq!(on.dimensions(), off.dimensions());
// state_json exposes the feature as data.
assert_eq!(state_on["minimap"], serde_json::json!(true));
assert!(state_on["minimap_heading_deg"].is_number(), "an active minimap reports a heading");
// RENDER: the bottom-right inset region gains ink when the minimap is on…
let inset_diff = diff_in_region(&on, &off, 0.66, 0.66, 1.0, 1.0);
assert!(inset_diff > 200, "the minimap paints its bottom-right inset (changed_px={inset_diff})");
// …and the top-left quadrant (away from the inset) is untouched — the overlay
// is confined to its corner.
let elsewhere_diff = diff_in_region(&on, &off, 0.0, 0.0, 0.5, 0.5);
assert_eq!(elsewhere_diff, 0, "the minimap overlay stays in its corner (top-left changed_px={elsewhere_diff})");
// DRIVE: orbit the camera to a new bearing → the reported heading moves.
use std::cell::RefCell;
use std::rc::Rc;
let view = Rc::new(RefCell::new({
let mut m = Map3D::from_geometry("OSM 3D", &city()).with_minimap(true);
m.spin = false;
m.set_clock(0.0);
m
}));
let h0 = view.borrow().minimap_view().unwrap().heading_deg();
{
let mut m = view.borrow_mut();
m.cam.orbit(500.0, 0.0); // swing the azimuth (a real bearing change)
m.cam.settle();
}
let h1 = view.borrow().minimap_view().unwrap().heading_deg();
let moved = (h1 - h0).rem_euclid(360.0);
let moved = if moved > 180.0 { 360.0 - moved } else { moved };
assert!(moved > 10.0, "orbiting swings the minimap heading (Δ={moved:.1}°, {h0:.1}→{h1:.1})");
// Write the on-frame so an agent can eyeball the inset.
let dir = std::env::var_os("NORNIR_WAREHOUSE_ROOT")
.map(|r| std::path::PathBuf::from(r).join("facett-map3d"))
.unwrap_or_else(|| std::env::temp_dir().join("facett-map3d"));
std::fs::create_dir_all(&dir).ok();
let png = dir.join("minimap_inset.png");
on.save(&png).ok();
eprintln!("[minimap_robot] inset_diff={inset_diff} elsewhere={elsewhere_diff} heading Δ={moved:.1}° png={}", png.display());
#[cfg(feature = "testmatrix")]
facett_core::testmatrix::emit(
"facett-map3d",
"minimap_robot_render_and_swing",
inset_diff > 200 && elsewhere_diff == 0 && moved > 10.0,
&format!("inset_diff={inset_diff} elsewhere={elsewhere_diff} heading_delta_deg={moved:.1} png={}", png.display()),
);
}