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/manual_control.rs

//! **ROBOT-UI — seize the stick from the autopilot tour** (live egui_kittest render +
//! data drive).
//!
//! The owner reported that the cinematic tour plays but "I can't take control over the
//! flight". The fix: any manual gesture (orbit / pan / dolly / keyboard) routes its
//! `Msg` through `Map3D::update`, which now calls `take_manual_control()` first —
//! freezing the camera at the tour's current pose and dropping the tour so the user
//! flies freely — and the tour key (T → `Msg::SetTour(true)`) re-arms it.
//!
//! This drives the LIVE Facet the way the demo does (the SAME `Msg`s the drag/wheel/key
//! handlers emit, applied through the ELM `update` path, rendered on the CPU painter)
//! and proves, as DATA off `state_json` + the render surface:
//!   1. with the tour armed, `tour_active` is true and the camera flies the tour pose;
//!   2. a manual orbit gesture SEIZES control — `tour_active` flips false and the eye
//!      keeps moving as more manual input arrives (the user is flying, not the tour);
//!   3. the seize is JUMP-FREE — control is taken from exactly the tour's pose;
//!   4. re-arming (`SetTour(true)`) hands the stick back — `tour_active` true again.
//!
//! RED when broken: revert the `take_manual_control()` calls and step 2 fails (the tour
//! clobbers the camera every frame, so manual input can't move the eye / disarm it).
//!
//! Run: `cargo test -p facett-map3d --features testmatrix --test manual_control`

use egui_kittest::Harness;
use facett_core::{Facet, Theme};
use facett_map::Way;
use facett_map3d::{Map3D, Msg};

/// A small extruded town + a diagonal street the drive phase follows (same shape as
/// the tour robot fixture).
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;
            }
            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)
}

fn eye(m: &Map3D) -> [f32; 3] {
    let e = m.render_camera().eye();
    [e.x, e.y, e.z]
}

fn dist2(a: [f32; 3], b: [f32; 3]) -> f32 {
    (a[0] - b[0]).powi(2) + (a[1] - b[1]).powi(2) + (a[2] - b[2]).powi(2)
}

#[test]
fn manual_input_seizes_the_stick_and_t_rearms_the_tour() {
    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("seize", &ways).with_route(&ways, &wps).with_tour(true);
        m.spin = false;
        m.set_clock(40.0); // pin into the street-drive phase (drive_secs 26..52)
        m
    }));
    let v2 = view.clone();
    let mut harness = Harness::builder().with_size(egui::vec2(420.0, 420.0)).build_ui(move |ui| {
        Facet::ui(&mut *v2.borrow_mut(), ui);
    });
    harness.run_steps(2);

    // (1) The tour is armed and flying — the pose is the tour's, not manual.
    let tour_eye = eye(&view.borrow());
    assert!(view.borrow().tour_active(), "the tour is armed and flying the camera");
    let phase = view.borrow().state_json()["tour_phase"].as_str().unwrap_or("none").to_string();
    assert_eq!(phase, "drive", "pinned clock renders the street-drive phase");

    // (2) A manual orbit gesture (the exact Msg a left-drag emits) SEIZES control.
    view.borrow_mut().update(Msg::Orbit { dx: 60.0, dy: -25.0 });
    harness.run_steps(1);
    assert!(!view.borrow().tour_active(), "a manual drag disarms the autopilot (stick seized)");
    let seized_eye = eye(&view.borrow());

    // (3) The seize is jump-free: control was taken FROM the tour's pose (the first
    // manual orbit is a small nudge off it, not a teleport to some default framing).
    let jump = dist2(tour_eye, seized_eye).sqrt();
    assert!(jump < 1.5, "control is seized from the tour pose, no big jump (moved {jump:.3})");

    // …and MORE manual input keeps moving the eye — the user is flying now, the tour
    // is not clobbering the camera back every frame.
    view.borrow_mut().update(Msg::Orbit { dx: 120.0, dy: 0.0 });
    view.borrow_mut().update(Msg::Settle);
    harness.run_steps(1);
    let flown_eye = eye(&view.borrow());
    let moved = dist2(seized_eye, flown_eye).sqrt();
    assert!(moved > 1e-3, "manual input keeps flying the camera ({moved:.4}) — the tour no longer overrides it");

    // (4) Re-arm: T → SetTour(true) hands the stick back to the autopilot.
    view.borrow_mut().update(Msg::SetTour(true));
    harness.run_steps(1);
    assert!(view.borrow().tour_active(), "T re-arms the cinematic tour");

    #[cfg(feature = "testmatrix")]
    {
        let ok = phase == "drive" && jump < 1.5 && moved > 1e-3 && view.borrow().tour_active();
        facett_core::testmatrix::emit(
            "facett-map3d",
            "manual_control_seizes_and_rearms",
            ok,
            &format!("seize_jump={jump:.3} manual_move={moved:.4} rearmed={}", view.borrow().tour_active()),
        );
    }
}