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

nordisk/dwarves

crates/nornir-warehouse-nav/tests/robot_world_shot.rs

//! **robot_world_shot** — the image-backed proof for the owner's report ("typ 1000 noder
//! är gråa och läggs över varandra"). A screenshot-capturing egui_kittest robot test that
//! renders the SHIPPED `WarehouseNav` world graph at ~1000 nodes on the GPU
//! (`Harness::render()` → RgbaImage), saves it, and asserts — QUANTITATIVELY, not by
//! eyeballing — that the two failure modes are gone:
//!
//!   1. **Stacked nodes** — the fraction of nodes whose nearest neighbour is closer than a
//!      dot diameter (in projected px, using the panel's real fit camera) is small. The
//!      "before" look (184-px chips at zoom 1) trips the same guard.
//!   2. **All grey** — the rendered node fills have real variance (many distinct colours
//!      keyed by repo/crate). A single-grey model trips the guard.
//!
//! Saves `world-1000.png` (before: chips, zoom-1, greyed) and `world-1000-fixed.png`
//! (after: the real dot-cloud panel) under `workspace_nordisk/scratch/` for visual review.
//!
//! Needs a GPU adapter (egui_kittest `wgpu` feature); on a box without one the render step
//! is skipped honestly and the data-level guards still run.

#![cfg(not(target_arch = "wasm32"))]

use std::cell::RefCell;
use std::collections::BTreeSet;
use std::rc::Rc;

use egui::{vec2, Color32};
use egui_kittest::Harness;
use facett_core::Facet;
use facett_graphpan::{draw_graph, Decorations, GraphModel, GraphView, BOX_H, BOX_W};
use nornir_warehouse_nav::WarehouseNav;

fn shot_dir() -> String {
    format!(
        "{}/git/workspace_nordisk/scratch",
        std::env::var("HOME").unwrap_or_default()
    )
}
const N_TARGET: u64 = 1000;

/// The panel's own dot radius formula (kept in sync) — for the projected-overlap guard.
fn dot_radius(n: usize) -> f32 {
    (9000.0 / n.max(1) as f32).sqrt().clamp(1.8, 6.0)
}

/// Distinct node fill colours in a model (the COLOUR proxy — grey blobs ⇒ ~1).
fn distinct_fills(m: &GraphModel) -> usize {
    m.nodes
        .iter()
        .map(|n| (n.fill.r(), n.fill.g(), n.fill.b()))
        .collect::<BTreeSet<_>>()
        .len()
}

/// Fraction of nodes whose nearest neighbour is closer than `min_px` in PROJECTED pixels
/// (world distance × zoom) — the "stacked on top of each other" proxy.
fn stacked_fraction(m: &GraphModel, zoom: f32, min_px: f32) -> f32 {
    let n = m.nodes.len();
    if n < 2 {
        return 0.0;
    }
    let min2 = (min_px / zoom.max(1e-6)).powi(2); // compare in world units
    let mut stacked = 0usize;
    for (i, a) in m.nodes.iter().enumerate() {
        let mut near = false;
        for (j, b) in m.nodes.iter().enumerate() {
            if i == j {
                continue;
            }
            let dx = a.pos.x - b.pos.x;
            let dy = a.pos.y - b.pos.y;
            if dx * dx + dy * dy < min2 {
                near = true;
                break;
            }
        }
        if near {
            stacked += 1;
        }
    }
    stacked as f32 / n as f32
}

/// Fraction of nodes whose CHIP (184×34 world units at zoom 1 — the pre-fix render)
/// overlaps another node's chip. This is the stacking the old chip renderer produced.
fn chip_overlap_fraction(m: &GraphModel) -> f32 {
    let n = m.nodes.len();
    if n < 2 {
        return 0.0;
    }
    let mut over = 0usize;
    for (i, a) in m.nodes.iter().enumerate() {
        let mut hit = false;
        for (j, b) in m.nodes.iter().enumerate() {
            if i == j {
                continue;
            }
            if (a.pos.x - b.pos.x).abs() < BOX_W && (a.pos.y - b.pos.y).abs() < BOX_H {
                hit = true;
                break;
            }
        }
        if hit {
            over += 1;
        }
    }
    over as f32 / n as f32
}

fn nav_1000() -> WarehouseNav {
    let mut nav = WarehouseNav::from_fixture_with_functions(N_TARGET as usize);
    nav.update_json(&format!(r#"{{"world_target":{N_TARGET}}}"#));
    nav.update_json(r#"{"world":true}"#);
    nav
}

#[test]
fn robot_world_1000_is_spread_and_coloured_with_screenshot() {
    let _ = std::fs::create_dir_all(shot_dir());

    // ── the SHIPPED panel at ~1000 nodes, rendered on the GPU ──────────────────────
    let nav = Rc::new(RefCell::new(nav_1000()));
    let n = nav.clone();
    let mut h = Harness::builder()
        .with_size(vec2(1400.0, 1000.0))
        .wgpu()
        .build_ui(move |ui| {
            n.borrow_mut().ui(ui);
        });
    h.run();
    h.run(); // second frame: the fit has been applied, dots are placed

    let s = Facet::state_json(&*nav.borrow());
    let nodes = s["preview_nodes"].as_u64().unwrap();
    assert!(
        nodes >= 700,
        "world previews ~1000 nodes (got {nodes}): {s}"
    );

    // ── AFTER guards (data-level, deterministic) ───────────────────────────────────
    let model = nav.borrow().world_preview_model();
    let view = nav.borrow().world_view_snapshot();
    let dot_r = dot_radius(model.nodes.len());

    // COLOUR: many distinct fills (repo/crate/function hues), not one grey.
    let fills = distinct_fills(&model);
    assert!(
        fills > 12,
        "world is multi-coloured ({fills} distinct fills), not grey blobs"
    );

    // SPREAD: with the real fit camera + dot size, few nodes stack (centres within a dot
    // diameter). The pre-fix collapse put >50% here.
    let stacked = stacked_fraction(&model, view.zoom, 2.0 * dot_r);
    assert!(
        stacked < 0.20,
        "nodes are spread — only {:.1}% stack within a dot diameter (fit zoom {:.4}, dot {:.1}px)",
        stacked * 100.0,
        view.zoom,
        dot_r
    );
    // Positions are distinct (no exact pile-up).
    let distinct_pos = model
        .nodes
        .iter()
        .map(|n| (n.pos.x.to_bits(), n.pos.y.to_bits()))
        .collect::<BTreeSet<_>>()
        .len();
    assert_eq!(
        distinct_pos,
        model.nodes.len(),
        "every node has a distinct position"
    );

    // ── save the AFTER screenshot (the real rendered panel) ────────────────────────
    match h.render() {
        Ok(img) => {
            let p = format!("{}/world-1000-fixed.png", shot_dir());
            img.save(&p).expect("save fixed screenshot");
            eprintln!(
                "[shot] AFTER  → {p}  ({} nodes, {fills} fills, {:.1}% stacked)",
                model.nodes.len(),
                stacked * 100.0
            );
        }
        Err(e) => eprintln!("[shot] AFTER render skipped (no GPU?): {e}"),
    }

    // ── BEFORE: the pre-fix look (184-px chips, zoom 1, greyed) — the red-when-broken
    // contrast. Same node positions, rendered the old way, trips BOTH guards. ────────
    let mut grey = model.clone();
    for nd in &mut grey.nodes {
        nd.fill = Color32::from_gray(140);
        nd.stroke = Color32::from_gray(90);
    }
    // colour guard FAILS on the greyed model:
    assert_eq!(
        distinct_fills(&grey),
        1,
        "the greyed before-model trips the colour guard"
    );
    // stacking guard FAILS on the chip render (184-px chips overlap heavily):
    let chip_over = chip_overlap_fraction(&grey);
    assert!(
        chip_over > 0.40,
        "the before (chip) render is stacked — {:.1}% of chips overlap (guard is red-when-broken)",
        chip_over * 100.0
    );

    let gm = grey.clone();
    let gv = Rc::new(RefCell::new(GraphView::default())); // zoom 1, dot_px None → chips
    let gvc = gv.clone();
    let mut h2 = Harness::builder()
        .with_size(vec2(1400.0, 1000.0))
        .wgpu()
        .build_ui(move |ui| {
            let bg = Color32::from_gray(14);
            let text = Color32::from_gray(235);
            let ring = Color32::from_rgb(0x39, 0x87, 0xe5);
            let dim = Color32::from_gray(120);
            let _ = draw_graph(
                ui,
                &gm,
                &Decorations::default(),
                &mut gvc.borrow_mut(),
                bg,
                text,
                ring,
                dim,
            );
        });
    h2.run();
    match h2.render() {
        Ok(img) => {
            let p = format!("{}/world-1000.png", shot_dir());
            img.save(&p).expect("save before screenshot");
            eprintln!(
                "[shot] BEFORE → {p}  (greyed chips, {:.1}% overlap)",
                chip_over * 100.0
            );
        }
        Err(e) => eprintln!("[shot] BEFORE render skipped (no GPU?): {e}"),
    }

    // testmatrix cell — the world graph is spread + multi-coloured (not grey
    // stacked blobs). Reaching here means every guard above passed; this row
    // surfaces the visual proof in the dwarves testmatrix (`--features testmatrix`).
    #[cfg(feature = "testmatrix")]
    nornir_testmatrix::functional_status(
        "dwarves/world-graph",
        "world_1000_is_spread_and_coloured",
        true,
        &format!(
            "world previews {nodes} nodes with {fills} distinct fills and only \
             {:.1}% stacked within a dot diameter (was grey + >50% stacked)",
            stacked * 100.0
        ),
    );
}