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

nordisk/facett

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