use egui::{Pos2, Rect, Vec2};
use facett_core::{Layout, Scene, harness, hash_color, layout_positions};
use facett_graph::{GraphView, scene_from_labeled_edges};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Topo {
Chain, Star, Bipartite, Cycle, Grid, Disconnected, }
const TOPOS: &[Topo] = &[Topo::Chain, Topo::Star, Topo::Bipartite, Topo::Cycle, Topo::Grid, Topo::Disconnected];
const SIZES: &[usize] = &[2, 3, 5, 8, 12, 20, 30, 50, 60, 61, 120, 400];
const CANVASES: &[(f32, f32)] = &[
(160.0, 120.0),
(320.0, 240.0),
(400.0, 300.0),
(640.0, 480.0),
(800.0, 600.0),
(1280.0, 720.0),
(240.0, 900.0),
(1000.0, 200.0),
];
const THEMES: &[fn() -> facett_core::Theme] = &[facett_core::Theme::default, facett_core::Theme::sci_fi];
const LABELS: &[&str] = &["Person", "Company", "Address", "Account", "Device"];
fn lbl(i: usize) -> String {
LABELS[i % LABELS.len()].to_string()
}
fn layout_name(l: Layout) -> &'static str {
match l {
Layout::Circular => "Circular",
Layout::Force => "Force",
}
}
fn build_scene(topo: Topo, n: usize) -> (Scene, usize, usize) {
let mut rows: Vec<(i64, i64, String, String)> = Vec::new();
match topo {
Topo::Chain => {
for i in 0..n.saturating_sub(1) {
rows.push((i as i64, (i + 1) as i64, lbl(i), lbl(i + 1)));
}
}
Topo::Star => {
for i in 1..n {
rows.push((0, i as i64, lbl(0), lbl(i)));
}
}
Topo::Bipartite => {
let h = n / 2;
for a in 0..h {
for b in h..n {
rows.push((a as i64, b as i64, lbl(a), lbl(b)));
}
}
}
Topo::Cycle => {
for i in 0..n {
rows.push((i as i64, ((i + 1) % n) as i64, lbl(i), lbl((i + 1) % n)));
}
}
Topo::Grid => {
let cols = (n as f64).sqrt().ceil() as usize;
let cols = cols.max(1);
for r in 0..n {
let (cx, cy) = (r % cols, r / cols);
if cx + 1 < cols && r + 1 < n {
rows.push((r as i64, (r + 1) as i64, lbl(r), lbl(r + 1)));
}
if r + cols < n {
rows.push((r as i64, (r + cols) as i64, lbl(r), lbl(r + cols)));
}
let _ = cy;
}
}
Topo::Disconnected => {
for k in 0..(n / 2) {
let a = (2 * k) as i64;
let b = (2 * k + 1) as i64;
rows.push((a, b, lbl(2 * k), lbl(2 * k + 1)));
}
}
}
let edge_count = rows.len();
let mut ids = std::collections::HashSet::new();
for (s, d, _, _) in &rows {
ids.insert(*s);
ids.insert(*d);
}
let node_count = ids.len();
let scene = scene_from_labeled_edges(rows);
(scene, node_count, edge_count)
}
#[test]
fn build_scene_returns_correct_counts_and_colours() {
let mut cases = 0usize;
let mut ok = 0usize;
for &topo in TOPOS {
for &n in SIZES {
let (scene, want_nodes, want_edges) = build_scene(topo, n);
assert_eq!(scene.nodes.len(), want_nodes, "{topo:?} n={n} node count");
assert_eq!(scene.edges.len(), want_edges, "{topo:?} n={n} edge count");
for e in &scene.edges {
assert!(e.src < scene.nodes.len() && e.dst < scene.nodes.len(), "{topo:?} edge in-bounds");
}
for node in &scene.nodes {
assert_eq!(node.color, hash_color(&node.label), "{topo:?} node colour == hash(label)");
}
cases += 1;
ok += 1;
}
}
assert_eq!(cases, TOPOS.len() * SIZES.len());
#[cfg(feature = "testmatrix")]
facett_core::testmatrix::emit(
"facett-graph.build",
"scene_counts_and_label_colours",
ok == cases,
&format!("{ok}/{cases} build cases over {} topologies", TOPOS.len()),
);
assert_eq!(ok, cases, "every build case passed ({ok}/{cases})");
}
#[test]
fn layout_positions_are_finite_in_rect_and_shaped() {
let mut cases = 0usize;
let mut ok = 0usize;
for &topo in TOPOS {
for &n in SIZES {
let (scene, _, _) = build_scene(topo, n);
let nn = scene.nodes.len();
if nn == 0 {
continue;
}
for &(cw, ch) in CANVASES {
let rect = Rect::from_min_size(Pos2::new(10.0, 20.0), Vec2::new(cw, ch));
for &layout in &[Layout::Circular, Layout::Force] {
let pos = layout_positions(layout, &scene, rect);
assert_eq!(pos.len(), nn, "{topo:?} one pos per node");
let grown = rect.expand(8.0);
let ln = layout_name(layout);
for p in &pos {
assert!(p.x.is_finite() && p.y.is_finite(), "{topo:?} {ln} finite pos");
assert!(grown.contains(*p), "{topo:?} {ln} pos {p:?} in rect {grown:?}");
}
match layout {
Layout::Circular => {
let c = rect.center();
let r0 = (pos[0] - c).length();
for p in &pos {
let r = (*p - c).length();
assert!((r - r0).abs() < 0.5, "{topo:?} circular radius uniform: {r} vs {r0}");
}
if nn >= 2 {
assert!(r0 > 0.0, "circular radius positive");
}
}
Layout::Force => {
if nn >= 2 && cw > 200.0 && ch > 200.0 {
let (mut mn, mut mx) = (pos[0], pos[0]);
for p in &pos {
mn = Pos2::new(mn.x.min(p.x), mn.y.min(p.y));
mx = Pos2::new(mx.x.max(p.x), mx.y.max(p.y));
}
assert!((mx.x - mn.x) + (mx.y - mn.y) > 1.0, "{topo:?} force layout spreads");
}
}
}
cases += 1;
ok += 1;
}
}
}
}
assert!(cases > 300, "layout sweep is broad, got {cases}");
#[cfg(feature = "testmatrix")]
facett_core::testmatrix::emit(
"facett-graph.layout",
"positions_finite_in_rect_circular_force",
ok == cases,
&format!("{ok}/{cases} layout cases (topo×size×canvas×layout)"),
);
assert_eq!(ok, cases, "every layout case passed ({ok}/{cases})");
}
#[test]
fn edge_endpoints_match_laid_out_node_centres() {
let rect = Rect::from_min_size(Pos2::ZERO, Vec2::new(800.0, 600.0));
let mut cases = 0usize;
let mut ok = 0usize;
for &topo in TOPOS {
for &n in SIZES {
let (scene, _, _) = build_scene(topo, n);
if scene.nodes.is_empty() {
continue;
}
for &layout in &[Layout::Circular, Layout::Force] {
let pos = layout_positions(layout, &scene, rect);
for e in &scene.edges {
assert!(e.src < pos.len() && e.dst < pos.len());
let a = pos[e.src];
let b = pos[e.dst];
assert!(a.x.is_finite() && b.x.is_finite(), "edge endpoints finite");
assert_eq!(a, pos[e.src]);
assert_eq!(b, pos[e.dst]);
}
cases += 1;
ok += 1;
}
}
}
#[cfg(feature = "testmatrix")]
facett_core::testmatrix::emit(
"facett-graph.edges",
"endpoints_are_node_centres",
ok == cases,
&format!("{ok}/{cases} edge-resolution cases"),
);
assert_eq!(ok, cases, "every edge case passed ({ok}/{cases})");
}
#[test]
fn render_matrix_topo_x_size_x_canvas_x_layout() {
let mut cases = 0usize;
let mut drew = 0usize;
let mut state_correct = 0usize;
for &topo in TOPOS {
for &n in SIZES {
let (scene, want_nodes, want_edges) = build_scene(topo, n);
if want_nodes == 0 {
continue;
}
let mut want_label_count = std::collections::BTreeMap::new();
for node in &scene.nodes {
*want_label_count.entry(node.label.clone()).or_insert(0usize) += 1;
}
for &(cw, ch) in CANVASES {
for &layout in &[Layout::Circular, Layout::Force] {
for &theme_fn in THEMES {
let mut view = GraphView::new(scene.clone());
view.set_layout(layout);
let r = harness::render_themed_sized(&mut view, theme_fn(), (cw, ch));
let st_nodes = r.state["nodes"].as_u64().unwrap() as usize;
let st_edges = r.state["edges"].as_u64().unwrap() as usize;
assert_eq!(st_nodes, want_nodes, "{topo:?} n={n} {cw}x{ch} {} node count", layout_name(layout));
assert_eq!(st_edges, want_edges, "{topo:?} edges");
let st_labels = r.state["labels"].as_object().unwrap();
assert_eq!(st_labels.len(), want_label_count.len(), "{topo:?} distinct-label count");
let state_ok = st_nodes == want_nodes && st_edges == want_edges && st_labels.len() == want_label_count.len();
if state_ok {
state_correct += 1;
}
if r.drew() {
drew += 1;
}
cases += 1;
}
}
}
}
}
assert!(cases >= 2304, "the graph matrix has >= 2304 cases, got {cases}");
assert_eq!(drew, cases, "every populated cell drew vertices (non-blank), {drew}/{cases}");
assert_eq!(state_correct, cases, "every cell's state_json matched the model, {state_correct}/{cases}");
eprintln!("[graph-matrix] {cases} cells, all drew, all state-correct");
#[cfg(feature = "testmatrix")]
facett_core::testmatrix::emit(
"facett-graph.render_matrix",
"all_cells_draw_with_correct_state",
drew == cases && state_correct == cases,
&format!("{cases} cells (topo×size×canvas×layout), {drew} drew, {state_correct} state-correct"),
);
}
#[test]
fn empty_scene_is_distinguishable_from_a_populated_one() {
let mut empty = GraphView::new(Scene::new());
let re = harness::headless_render(&mut empty);
assert_eq!(re.state["nodes"], 0);
assert_eq!(re.state["edges"], 0);
let (full_scene, _, _) = build_scene(Topo::Grid, 120);
let mut full = GraphView::new(full_scene);
let rf = harness::headless_render(&mut full);
assert!(rf.vertices > re.vertices * 2, "populated >> empty: {} vs {}", rf.vertices, re.vertices);
#[cfg(feature = "testmatrix")]
facett_core::testmatrix::emit(
"facett-graph.render_matrix",
"empty_vs_populated_separable",
rf.vertices > re.vertices * 2,
&format!("empty={} populated={} verts", re.vertices, rf.vertices),
);
}