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