facett-map3d/tests/tour_robot.rs
//! **ROBOT-UI — the Sigtuna car-nav tour** (live egui_kittest render + data drive).
//!
//! Drives the LIVE `Map3D` Facet the way the demo does (CPU painter) and proves the
//! cinematic tour on the real render surface, not just as a pose function:
//! • DATA: with the tour attached, `state_json.tour_phase` walks circle → descend →
//! drive as the pinned clock steps across the loop (and `tour_active` is true).
//! • RENDER: the air-circle frame and the street-drive frame are substantially
//! DIFFERENT pixels — the camera really descended bird → street and repainted the
//! scene (both frames non-blank), so the UI surface shows the whole journey.
//!
//! Run: `cargo test -p facett-map3d --features testmatrix --test tour_robot`
use egui_kittest::Harness;
use facett_core::{Facet, Theme};
use facett_map::Way;
use facett_map3d::Map3D;
/// A small extruded town + a diagonal street the drive phase follows.
fn town() -> (Vec<Way>, Vec<(f32, f32)>) {
let mut ways = Vec::new();
let mut id = 1i64;
for r in 0..6_i32 {
for c in 0..6_i32 {
if (r - c).abs() < 1 {
continue; // clear the diagonal for the street
}
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;
}
}
let stops: Vec<(f64, f64)> = (0..6).map(|i| (17.70 + i as f64 * 0.0012, 59.61 + i as f64 * 0.0012)).collect();
let road = Way::line(9001, "road", stops, egui::Color32::WHITE, 2.0);
let waypoints = road.points.clone();
ways.push(road);
(ways, waypoints)
}
const SIZE: f32 = 480.0;
/// The live oracle read off a rendered tour frame: phase, houses-in-frame, and the
/// round-2 pitched-roof + coloured-building counts.
struct Frame {
img: image::RgbaImage,
phase: String,
buildings: u64,
pitched: u64,
coloured: u64,
}
/// Render the tour at a pinned clock, and read back the live oracle. Built with REAL
/// pitched roofs + OSM colours (`from_geometry_roofed`) so the robot proves the round-2
/// geometry renders + reports on the actual UI surface.
fn render_at(clock: f64) -> Frame {
use std::cell::RefCell;
use std::rc::Rc;
let (ways, wps) = town();
let view = Rc::new(RefCell::new({
let mut m = Map3D::from_geometry_roofed("Sigtuna tour", &ways).with_route(&ways, &wps).with_tour(true);
m.spin = false;
m.set_clock(clock); // pin → deterministic tour pose, 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 sj = view.borrow().state_json();
let phase = sj["tour_phase"].as_str().unwrap_or("none").to_string();
let buildings = sj["buildings_visible"].as_u64().unwrap_or(0);
let pitched = sj["pitched_roofs"].as_u64().unwrap_or(0);
let coloured = sj["coloured_buildings"].as_u64().unwrap_or(0);
let img = harness.render().expect("headless render produced an image");
Frame { img, phase, buildings, pitched, coloured }
}
/// Fraction of pixels that differ between two frames.
fn frac_diff(a: &image::RgbaImage, b: &image::RgbaImage) -> f32 {
let (w, h) = a.dimensions();
let mut d = 0u64;
for (pa, pb) in a.pixels().zip(b.pixels()) {
if pa != pb {
d += 1;
}
}
d as f32 / (w * h) as f32
}
/// Fraction of non-background (lit) pixels — a cheap "the scene rendered" check.
fn lit_frac(a: &image::RgbaImage) -> f32 {
let (w, h) = a.dimensions();
let bg = *a.get_pixel(0, 0); // corner = sky/background
let lit = a.pixels().filter(|&&p| p != bg).count();
lit as f32 / (w * h) as f32
}
#[test]
fn tour_walks_phases_and_bird_to_street_repaints_the_scene() {
// circle_secs=18, descend_secs=8 (18..26), drive_secs=26 (26..52).
let air = render_at(4.0); // mid air-circle
let desc = render_at(22.0); // mid descend
let street = render_at(40.0); // mid drive
// DATA: the live UI reports the three phases in order.
assert_eq!(air.phase, "circle", "the pinned early clock renders the air-circle phase");
assert_eq!(desc.phase, "descend", "the mid clock renders the descend phase");
assert_eq!(street.phase, "drive", "the late clock renders the street-drive phase");
// HOUSES: at the air-circle altitude the extruded houses are in frame on the REAL
// render surface — "we want to SEE THE HOUSES" as DATA off the live Facet.
assert!(air.buildings > 0, "houses are visible circling from the air ({} risen verts in frame)", air.buildings);
// ROUND-2 ORACLE: the live rendered Facet reports real pitched roofs + coloured
// buildings (the houses read as real buildings), read straight off `state_json`.
assert!(air.pitched > 0, "the rendered town grew real pitched roofs ({})", air.pitched);
assert!(air.coloured > 0, "the rendered town carries coloured buildings ({})", air.coloured);
// RENDER: both frames are non-blank scenes...
let air_lit = lit_frac(&air.img);
let street_lit = lit_frac(&street.img);
assert!(air_lit > 0.02, "the air-circle frame renders the extruded town (lit={air_lit:.3})");
assert!(street_lit > 0.02, "the street-drive frame renders the town (lit={street_lit:.3})");
// ...and the bird→street descent substantially changes the picture (the camera
// really moved from a high orbit to a ground-level chase).
let moved = frac_diff(&air.img, &street.img);
assert!(moved > 0.1, "bird→street repaints the scene (frame diff {moved:.3} of pixels)");
#[cfg(feature = "testmatrix")]
{
let ok = air.phase == "circle"
&& desc.phase == "descend"
&& street.phase == "drive"
&& air_lit > 0.02
&& street_lit > 0.02
&& moved > 0.1
&& air.buildings > 0
&& air.pitched > 0
&& air.coloured > 0;
facett_core::testmatrix::emit(
"facett-map3d",
"tour_robot_walks_phases_and_repaints",
ok,
&format!(
"air={} desc={} street={} air_lit={air_lit:.3} street_lit={street_lit:.3} moved={moved:.3} \
air_buildings={} pitched={} coloured={}",
air.phase, desc.phase, street.phase, air.buildings, air.pitched, air.coloured
),
);
}
}