Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/facett

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
            ),
        );
    }
}