dwarves-gui/tests/robot_funnel_dag_3d.rs
//! **robot_funnel_dag_3d** — the RED-when-broken proof that the funnel pane renders its
//! Plan-DAG as a **3D graph**, with a camera-only rotate and a clickable drill.
//!
//! The funnel is nornir's persistent Plan-DAG (ideas → plans → nodes, with dependency
//! + decomposition edges). Beside the `facett-plandag` card lane and the flat
//! `facett-graphview` node-link graph, the pane now offers a **3D layered graph**: the
//! ranks are stacked as planes in space and projected through the world graph's
//! orthographic 3D camera (a CPU port of `facett_graphview::world_still`'s projection,
//! which is `gpu`-gated). A robot mounts the embedded pane over a real seeded funnel
//! (ideas → a write→fix→test plan chain + a triaged error) and drives it entirely
//! through the `Facet` state/update seam.
//!
//! The invariants proven here:
//! 1. switching to the 3D view renders the whole DAG in 3D (node + edge counts, lane
//! histogram, `graph3d_on`) — nothing is capped or dropped;
//! 2. clicking a node (the headless `graph3d_select` twin of a pointer click) drills to
//! the card detail — the SAME `facett-plandag` view's selection lands on that node;
//! 3. **3D rotating spins the CAMERA, not the LAYOUT** — over 24 repaint frames the
//! projection azimuth sweeps while `graph3d_build_calls()` stays EXACTLY 0 (the
//! cached layout is re-projected, never rebuilt); turning rotation off freezes it.
//!
//! Own test binary so the process-wide `graph3d_build_calls()` counter reflects only
//! this test (each integration file is a separate process — the same isolation the
//! world graph's `robot_world_3d_rotate` uses). Rotation calls `ctx.request_repaint()`
//! every frame, so the spin frames use `run_steps(N)` (bounded), never `run()`.
//!
//! **No crate-level `#![cfg(feature = ...)]`** (the silenced-tests trap): the scenario
//! runs unconditionally; only the test-matrix emit is feature-gated.
#![cfg(not(target_arch = "wasm32"))]
use std::cell::RefCell;
use std::rc::Rc;
use egui::vec2;
use egui_kittest::kittest::Queryable;
use egui_kittest::Harness;
use facett_core::Facet;
use nornir_funnel_embed::{graph3d_build_calls, FunnelPanel};
#[test]
fn funnel_plan_dag_renders_in_3d_rotates_camera_only_and_drills() {
// The embedded funnel seeds a real nordisk Plan-DAG (offline, no server): idea
// i-001 decomposed into n-001 write → n-002 fix → n-003 test, plus error i-002 →
// n-004 triage — a few ideas → plan → nodes with edges.
let panel = Rc::new(RefCell::new(FunnelPanel::new_embedded_seeded("nordisk")));
let p = panel.clone();
let mut h = Harness::builder()
.with_size(vec2(1400.0, 1000.0))
.build_ui(move |ui| {
Facet::ui(&mut *p.borrow_mut(), ui);
});
// Pause the live auto-poll so the frame accounting is clean (the embedded DAG is
// static, so a poll would not rebuild anyway — this just removes repaint noise).
panel.borrow_mut().update_json(r#"{"auto_poll":false}"#);
// Switch to the 3D layered render. Rotation is ON BY DEFAULT (the spinning
// showpiece), which keeps repaint requests coming — turn it off explicitly so
// run() settles; section (3) below turns it back on and measures the spin.
panel.borrow_mut().update_json(r#"{"dag_view":"graph3d"}"#);
panel
.borrow_mut()
.update_json(r#"{"graph3d_rotate":false}"#);
h.run();
// ── (1) The whole DAG renders in 3D ──────────────────────────────────────────
let s = Facet::state_json(&*panel.borrow());
assert_eq!(s["dag_view"], "graph3d", "the 3D view is active: {s}");
assert_eq!(s["graph3d_on"], true, "state reports 3D on: {s}");
let g = &s["graph3d"];
let nodes = g["nodes"].as_u64().unwrap();
let edges = g["edges"].as_u64().unwrap();
assert!(
nodes >= 6,
"every funnel node lowered into 3D (>=6, got {nodes}): {s}"
);
assert!(
edges >= 4,
"every DAG edge lowered into 3D (>=4, got {edges}): {s}"
);
assert!(
g["node_cap"].is_null(),
"no MAX_NODES ceiling in the 3D render: {s}"
);
// Coloured by lane/state (idea / plan / node / done / failed) — as DATA.
let lanes = &g["lanes"];
assert!(
lanes["idea"].as_u64().unwrap() >= 1,
"idea lane present: {s}"
);
assert!(
lanes["done"].as_u64().unwrap() >= 1,
"a done node present: {s}"
);
let distinct = ["idea", "plan", "node", "done", "failed"]
.iter()
.filter(|k| lanes[**k].as_u64().unwrap_or(0) > 0)
.count();
assert!(
distinct >= 3,
"the DAG spans several lanes (got {distinct}): {s}"
);
// The "3D rotating" checkbox is really rendered in the pane (wired into ui()).
assert!(
h.query_by_label("3D rotating").is_some(),
"the '3D rotating' checkbox renders in the 3D view controls"
);
// The idea WORDS are attached in 3D: labels default ON and the painted texts
// carry the seeded idea's own words (asserted as TEXT, not a count).
assert_eq!(g["labels"], true, "labels default ON in the 3D funnel: {s}");
let words: Vec<&str> = g["label_texts"]
.as_array()
.unwrap()
.iter()
.filter_map(|v| v.as_str())
.collect();
assert!(
words.iter().any(|w| w.contains("release engine")),
"the 3D labels carry the idea's words: {words:?}"
);
assert!(
h.query_by_label("labels").is_some(),
"the 'labels' checkbox renders in the 3D view controls"
);
// ── (2) Clicking a node drills to the card detail (same plandag selection) ────
// The 3D node id is the raw funnel card id (`n-002`); the drill maps it to the card
// view's `node:<id>` resolved key.
panel
.borrow_mut()
.update_json(r#"{"graph3d_select":"n-002"}"#);
h.run();
let s = Facet::state_json(&*panel.borrow());
assert_eq!(
s["graph3d"]["selected"], "n-002",
"the 3D node is selected: {s}"
);
assert!(
s["graph3d"]["selected_title"].is_string(),
"the drilled node carries its title: {s}"
);
// The drill reuses the EXISTING card detail: the same `facett-plandag` view now has
// that node selected, so switching to Cards shows its detail line.
assert_eq!(
s["plandag"]["selected"], "node:n-002",
"the drill lands on the card view's selection: {s}"
);
// ── (3) 3D rotating spins the CAMERA, not the LAYOUT ──────────────────────────
panel.borrow_mut().update_json(r#"{"graph3d_rotate":true}"#);
let s = Facet::state_json(&*panel.borrow());
assert_eq!(
s["graph3d"]["rotate"], true,
"the rotate toggle reads on: {s}"
);
let angle0 = s["graph3d"]["rotate_angle"]
.as_f64()
.expect("rotate_angle reported");
let base_builds = graph3d_build_calls();
const FRAMES: usize = 24;
h.run_steps(FRAMES); // EXACTLY N frames (run() would spin forever while rotating)
let builds_delta = graph3d_build_calls() - base_builds;
let s = Facet::state_json(&*panel.borrow());
let angle1 = s["graph3d"]["rotate_angle"].as_f64().unwrap();
assert_eq!(
builds_delta, 0,
"rotation is projection-only: {FRAMES} spin frames rebuilt the layout {builds_delta} times (must be 0)"
);
let moved = (angle1 - angle0).abs();
assert!(
moved > 1e-3,
"the camera azimuth advanced over {FRAMES} frames (Δ={moved:.4} rad)"
);
// Turning rotation OFF freezes the angle — no drift, still 0 rebuilds.
panel
.borrow_mut()
.update_json(r#"{"graph3d_rotate":false}"#);
let steady = graph3d_build_calls();
h.run_steps(6);
let s = Facet::state_json(&*panel.borrow());
let angle2 = s["graph3d"]["rotate_angle"].as_f64().unwrap();
assert_eq!(
graph3d_build_calls() - steady,
0,
"a static 3D view does not rebuild either"
);
assert!(
(angle2 - angle1).abs() < 1e-4,
"the azimuth is frozen once rotation is off"
);
let ok = nodes >= 6
&& edges >= 4
&& distinct >= 3
&& s["plandag"]["selected"] == "node:n-002"
&& builds_delta == 0
&& moved > 1e-3
&& (angle2 - angle1).abs() < 1e-4;
// A SHIPPED surface (the funnel pane's 3D Plan-DAG render) owes the coverage board a
// row — reuse edda's testmatrix rather than hand-roll a harness.
#[cfg(feature = "testmatrix")]
nornir_testmatrix::functional_status(
"nornir-funnel-embed",
"funnel_plan_dag_renders_in_3d_rotates_camera_only_and_drills",
ok,
"the funnel pane renders its Plan-DAG as a 3D layered graph (ranks stacked in space, \
projected through the world graph's orthographic 3D camera, coloured by lane/state): \
switching to the 3D view lowers every node + edge (no cap, multi-lane), a node click \
drills to the SAME facett-plandag card selection, and over 24 repaint frames the '3D \
rotating' azimuth sweeps (Δ>1e-3 rad) while graph3d_build_calls() stays EXACTLY 0 — \
proving the layout is reused from cache, never rebuilt per frame; rotation off freezes it",
);
assert!(ok);
}