facett-map3d-flight/tests/facet_robot.rs
//! **The FLIGHT viewer's robot scenario + matrix cell.** `facett-map3d-flight`
//! had NO `Facet` at all (`.nornir/osm-3d-gaps.md` row C:69), so its 6DOF rig,
//! floating origin, atmosphere and day cycle were unreachable from a host and
//! unobservable by the matrix. `src/facet.rs` closes that; this drives it.
//!
//! ONE call through the shared generic-`Facet` probe (L5): the pane renders
//! headlessly through the SHARED `facett_map3d` render core (so the pixel oracle
//! — real vertices + non-zero geometry — applies unchanged) and the JSON drive
//! seam is exercised end to end.
//!
//! It also pins the FLIGHT-owned pedestrian WALK mode (osm-3d-gaps row C:72,
//! previously the last open FLIGHT camera gap) with assertions that are each
//! individually RED when the corresponding constraint is removed.
use facett_map3d_flight::{FlightFacet, Locomotion, WALK_EYE_HEIGHT_M, WALK_SPEED_MPS};
#[test]
fn flight_facet_probe_emits_render_and_input_cells() {
let mut view = FlightFacet::local();
let script = [
r#"{"hour":6.5}"#, // dawn
r#"{"step":{"forward":120.0,"yaw":0.4,"dt":1.0}}"#, // fly
r#"{"ground_m":18.0}"#, // the host's DEM
r#"{"locomotion":"walk"}"#, // land and walk
r#"{"step":{"forward":50.0,"dt":2.0}}"#, // walk (capped)
];
let probe = facett_core::testmatrix::probe_and_emit("facett-map3d-flight", &mut view, &script);
// PIXEL oracle — the pane draws through the shared render core.
assert!(probe.drew(), "the FLIGHT pane must tessellate real vertices");
assert!(probe.cardinality() > 0, "state_json carries the render core's geometry");
assert!(probe.responded(), "update_json must drive state — a dead seam is a regression");
// STATE oracle — exact transitions, not "something moved".
assert_eq!(probe.initial_state["locomotion"], "fly");
assert!((probe.initial_state["altitude_m"].as_f64().unwrap() - 400.0).abs() < 1e-6);
assert!((probe.steps[0].state["hour"].as_f64().unwrap() - 6.5).abs() < 1e-5);
assert!(
probe.steps[0].state["sun_elevation_deg"].as_f64().unwrap()
< probe.initial_state["sun_elevation_deg"].as_f64().unwrap(),
"06:30 has a lower sun than midday"
);
assert_eq!(probe.steps[3].state["locomotion"], "walk");
// Landing on the walk lane pins the eye at pedestrian height above the
// host-supplied ground — the whole point of the mode.
let eye = probe.steps[3].state["eye_height_m"].as_f64().unwrap();
assert!((eye - WALK_EYE_HEIGHT_M).abs() < 1e-9, "walk plants the eye at {WALK_EYE_HEIGHT_M} m, got {eye}");
assert!((probe.steps[3].state["altitude_m"].as_f64().unwrap() - (18.0 + WALK_EYE_HEIGHT_M)).abs() < 1e-9);
// ...and 2 s of "50 m/s forward" is still only a walk.
let eye_after = probe.steps[4].state["eye_height_m"].as_f64().unwrap();
assert!((eye_after - WALK_EYE_HEIGHT_M).abs() < 1e-9, "walking does not take off");
}
/// RED-when-broken: the walk speed cap. Remove it and the pane covers 100 m in
/// the 2 s this asserts covers at most 2·`WALK_SPEED_MPS`.
#[test]
fn walking_two_seconds_covers_at_most_two_paces() {
let mut f = FlightFacet::local();
f.set_locomotion(Locomotion::Walk);
let start = f.camera().position;
facett_core::Facet::update_json(&mut f, r#"{"step":{"forward":50.0,"dt":2.0}}"#);
let moved = f.camera().position.ground_distance(start);
assert!(
moved <= 2.0 * WALK_SPEED_MPS + 1e-9,
"2 s of walking covers at most {:.2} m, covered {moved:.2} m",
2.0 * WALK_SPEED_MPS
);
assert!(moved > 0.0, "and it does move");
}
/// RED-when-broken: an unknown locomotion name must NOT silently leave the pane
/// in the previous lane while a robot script believes it switched.
#[test]
fn an_unknown_locomotion_name_is_rejected() {
let mut f = FlightFacet::local();
facett_core::Facet::update_json(&mut f, r#"{"locomotion":"teleport"}"#);
assert_eq!(f.locomotion(), Locomotion::Fly, "an unknown lane leaves the pane where it was");
assert_eq!(facett_core::Facet::state_json(&f)["locomotion"], "fly");
facett_core::Facet::update_json(&mut f, r#"{"locomotion":"orbit"}"#);
assert_eq!(f.locomotion(), Locomotion::Orbit);
}
/// The rig's world frame is the one `WorldPos` defines — climbing changes the
/// ALTITUDE the pane publishes, not its latitude. (This is the pane-level view of
/// the up-axis bug fixed in `camera_fly::forward`.)
#[test]
fn climbing_changes_the_published_altitude_not_the_latitude() {
let mut f = FlightFacet::local();
let before = facett_core::Facet::state_json(&f);
facett_core::Facet::update_json(&mut f, r#"{"step":{"up":250.0,"dt":1.0}}"#);
let after = facett_core::Facet::state_json(&f);
assert!(
(after["altitude_m"].as_f64().unwrap() - before["altitude_m"].as_f64().unwrap() - 250.0).abs() < 1e-6,
"a 250 m climb shows up as 250 m of altitude"
);
assert!(
(after["lat"].as_f64().unwrap() - before["lat"].as_f64().unwrap()).abs() < 1e-12,
"and moves the aircraft nowhere on the map"
);
}