facett-demo/src/bin/facett_robot.rs
//! # `facett_robot` — drive the facett-demo main window with a robot, headless OR watch
//!
//! ONE [`Scenario`](nornir_robotui::Scenario), ONE demo, two modes — the choosable
//! entry point the brief asks for:
//!
//! ```text
//! facett_robot run # HEADLESS egui_kittest drive → prints the matrix rows (CI)
//! facett_robot watch # a REAL eframe window, paced + visible — you WATCH the robot
//! facett_robot # mode from the env: ROBOTUI_WATCH=1 → watch, else headless
//! ```
//!
//! Pacing / window env (read by [`WatchConfig::from_env`]): `ROBOTUI_WATCH_DELAY_MS`
//! (per-step), `ROBOTUI_WATCH_LINGER_MS` (hold at end), `ROBOTUI_WATCH_HOLD=1` (keep
//! the window open until closed by hand), `ROBOTUI_WATCH_RENDERER=wgpu|glow` (the GPU
//! backend; defaults to wgpu on this `watch-wgpu` build so the OSM 3D city paints on
//! the GPU instead of the glow→CPU fallback).
//!
//! Built only with the `robotui` feature (see its `required-features`), which pulls
//! `nornir-robotui` 0.2.6 with its `watch-wgpu` backend so BOTH modes are available
//! AND the visible window renders GPU panes natively.
//!
//! ## The standalone OSM bins are NOT driven here — by design
//!
//! `osm3d_demo`, `osm_fly_demo`, `osm2d_demo` and `sigtuna_demo` are separate,
//! **non-interactive camera-tour / render bins**: each opens a fixed fly-through or
//! screenshot of ONE map viewer, with no deck, tab bar or control toolbar for a
//! robot to click. Their correctness is proven by their OWN image/render tests
//! (`tests/osm3d_*.rs`, `tests/osm2d_holes.rs`, `tests/sigtuna_tour.rs`) — the
//! pixel-oracle path, not the AccessKit-drive path. The map SURFACES they showcase
//! are the SAME `OsmView` / `Map3D` facets this robot drives live through the deck's
//! `OSM 2D` / `OSM 3D` tabs (with the loaded-count + navigable value asserts), so the
//! interactive coverage lives here and the tour/render coverage lives in those tests.
use std::process::ExitCode;
use facett_demo::robot;
use nornir_robotui::{run_scenario_mode, status, RunMode, RunOutcome, TestResultRow, WatchConfig, WatchRenderer};
fn usage() -> &'static str {
"usage: facett_robot <run|watch>\n\
\n\
run HEADLESS egui_kittest drive of the facett-demo main window → matrix rows (CI)\n\
watch open a REAL eframe window and replay the scenario, paced + visible\n\
\n\
With no subcommand the mode comes from the env: ROBOTUI_WATCH=1 → watch, else headless.\n\
env: ROBOTUI_WATCH_DELAY_MS (per-step pace), ROBOTUI_WATCH_LINGER_MS (end hold),\n\
\x20 ROBOTUI_WATCH_HOLD=1 (keep the window open until you close it)"
}
fn main() -> ExitCode {
let args: Vec<String> = std::env::args().skip(1).collect();
let mode = match args.first().map(|s| s.as_str()) {
Some("run") | Some("headless") => RunMode::Headless,
Some("watch") => RunMode::Watch,
Some("-h") | Some("--help") | Some("help") => {
println!("{}", usage());
return ExitCode::SUCCESS;
}
Some(other) => {
eprintln!("facett_robot: unknown subcommand {other:?}\n\n{}", usage());
return ExitCode::from(2);
}
// No subcommand → the env toggle chooses (ROBOTUI_WATCH=1 ⇒ watch).
None => RunMode::from_env(),
};
let scenario = robot::facett_scenario();
// ── HEADLESS: drive through the correctly-SIZED shared session (`run_headless`),
// NOT `run_scenario_mode`'s default-canvas harness — the ~70-tab deck bar clips
// the later tabs (OSM 3D / icebergview) on the narrow default, so those tab
// switches can't land. The bin's matrix must drive the same deck the tests prove.
if mode == RunMode::Headless {
let rows = robot::run_headless();
print_matrix(&scenario.suite, &rows);
let failed = rows.iter().filter(|r| r.status == status::FAIL).count();
return if failed > 0 { ExitCode::FAILURE } else { ExitCode::SUCCESS };
}
// ── WATCH: the visible, paced eframe window. A generous canvas so the whole deck
// + the OSM map views are watchable, and the wgpu renderer so the OSM 3D city
// paints on the GPU (the env toggle `ROBOTUI_WATCH_RENDERER` overrides this via
// `from_env`; the earlier glow pass fell back to the CPU painter).
let mut cfg = WatchConfig::from_env()
.with_title("facett-demo — robot drive")
.with_step_delay(std::time::Duration::from_millis(1200));
cfg.size = egui::vec2(robot::CANVAS[0], robot::CANVAS[1]);
if WatchRenderer::from_env().is_none() {
cfg.renderer = WatchRenderer::Wgpu;
}
let outcome = run_scenario_mode(
robot::demo_app(),
robot::facett_robot_draw,
&scenario,
"facett",
mode,
cfg,
);
match outcome {
Ok(RunOutcome::Headless(rows)) => {
// Unreachable in practice (headless handled above), but keep it honest.
print_matrix(&scenario.suite, &rows);
let failed = rows.iter().filter(|r| r.status == status::FAIL).count();
if failed > 0 { ExitCode::FAILURE } else { ExitCode::SUCCESS }
}
Ok(RunOutcome::Watched) => {
eprintln!("facett_robot: watch window closed");
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("facett_robot: {e}");
ExitCode::FAILURE
}
}
}
/// Print the headless matrix rows as a small pass/fail table.
fn print_matrix(suite: &str, rows: &[TestResultRow]) {
println!("facett-demo robot-ui matrix — suite `{suite}` ({} rows)", rows.len());
for r in rows {
let mark = if r.status == status::PASS { "PASS" } else { "FAIL" };
print!(" [{mark}] {}", r.test_name);
if !r.message.is_empty() {
print!(" — {}", r.message);
}
println!();
}
let passed = rows.iter().filter(|r| r.status == status::PASS).count();
println!(" {passed}/{} passed", rows.len());
}