use facett_graphview::{
Camera, Color, Decorations, GraphClipReport, GraphEdge, GraphModel, GraphNode,
NodeDecoration, Pos, WidgetRect, pick, render_graph_clipped,
};
fn blob_dir() -> std::path::PathBuf {
match std::env::var_os("NORNIR_WAREHOUSE_ROOT") {
Some(root) => std::path::PathBuf::from(root).join("facett-graphview"),
None => std::env::temp_dir().join("facett-graphview"),
}
}
#[allow(unused)]
fn emit(check: &str, ok: bool, detail: &str) {
#[cfg(feature = "testmatrix")]
facett_core::testmatrix::emit("facett-graphview::graph_clip", check, ok, detail);
}
fn grid(cols: usize, rows: usize, spacing: f32) -> GraphModel {
let mut nodes = Vec::new();
for r in 0..rows {
for c in 0..cols {
let i = r * cols + c;
nodes.push(GraphNode {
id: format!("n{i}"),
label: format!("node {i}"),
fill: Color::rgb((40 + (i * 37) % 200) as u8, (60 + (i * 53) % 180) as u8, 160),
stroke: Color::WHITE,
pos: Pos::new(c as f32 * spacing, r as f32 * spacing),
});
}
}
let mut edges = Vec::new();
for r in 0..rows {
for c in 0..cols.saturating_sub(1) {
let i = r * cols + c;
edges.push(GraphEdge {
from: format!("n{i}"),
to: format!("n{}", i + 1),
color: Color::rgb(210, 210, 230),
dashed: false,
label: None,
});
}
}
GraphModel { nodes, edges }
}
#[test]
fn layout_cull_nodes_edges_counts_are_exact() {
let m = grid(4, 3, 240.0);
assert_eq!(m.nodes.len(), 12);
assert_eq!(m.edges.len(), 9);
let rect = WidgetRect::new(20.0, 20.0, 760.0, 560.0);
let cam = Camera::fit(&m, rect.w, rect.h, 60.0);
let (mnx, mny, mxx, mxy) = m.world_bounds().unwrap();
let (wcx, wcy) = ((mnx + mxx) * 0.5, (mny + mxy) * 0.5);
let cam = Camera {
zoom: cam.zoom,
pan_x: rect.x + rect.w * 0.5 - wcx * cam.zoom,
pan_y: rect.y + rect.h * 0.5 - wcy * cam.zoom,
};
let (_frame, rep) =
render_graph_clipped(&m, &Decorations::default(), &cam, 800, 600, rect, Color::rgb(18, 20, 28), true);
let s = rep.state_json();
assert_eq!(s["layout"]["nodes_in"], 12);
assert_eq!(s["layout"]["projected"], 12);
assert_eq!(s["layout"]["nan"], 0);
assert_eq!(rep.nodes_culled, 0, "nothing culled when the graph fits the rect");
assert_eq!(rep.nodes_drawn, 12, "nodes.drawn == expected");
assert_eq!(rep.edges_drawn, 9, "edges.drawn == expected");
assert_eq!(rep.nodes_drawn + rep.nodes_culled, m.nodes.len());
emit("counts_exact", true, &format!("nodes_drawn={} edges_drawn={}", rep.nodes_drawn, rep.edges_drawn));
}
#[test]
fn cull_drops_offscreen_nodes_monotonically() {
let m = grid(8, 2, 220.0); let rect = WidgetRect::new(0.0, 0.0, 300.0, 460.0);
let cam = Camera { pan_x: 60.0, pan_y: 60.0, zoom: 1.0 };
let (_f, rep) =
render_graph_clipped(&m, &Decorations::default(), &cam, 1200, 480, rect, Color::rgb(18, 20, 28), true);
assert!(rep.nodes_drawn > 0 && rep.nodes_drawn < m.nodes.len(), "a real partition: {} of {}", rep.nodes_drawn, m.nodes.len());
assert_eq!(rep.nodes_drawn + rep.nodes_culled, m.nodes.len(), "cull is a partition (monotone)");
assert!(rep.edges_drawn <= m.edges.len(), "edges monotone (drawn ≤ in)");
emit("cull_monotone", true, &format!("drawn={} culled={} in={}", rep.nodes_drawn, rep.nodes_culled, m.nodes.len()));
}
#[test]
fn picking_resolves_chip_under_probe_and_respects_rect() {
let m = grid(3, 3, 240.0); let rect = WidgetRect::new(0.0, 0.0, 800.0, 800.0);
let cam = Camera { pan_x: 120.0, pan_y: 120.0, zoom: 1.0 };
let picked = pick(&m, &cam, rect, 360.0, 360.0);
assert_eq!(picked, Some("n4"), "probe at a chip centre picks that node");
assert_eq!(pick(&m, &cam, rect, 360.0, 240.0), None, "probe in a gap picks nothing");
let tight = WidgetRect::new(0.0, 0.0, 200.0, 200.0);
assert_eq!(pick(&m, &cam, tight, 360.0, 360.0), None, "outside the rect picks nothing");
emit("picking_resolves", true, "probe→chip inside rect; gap/outside→None");
}
fn sprawling() -> (GraphModel, Camera, WidgetRect, u32, u32) {
let m = grid(5, 5, 150.0); let (canvas_w, canvas_h) = (900u32, 900u32);
let rect = WidgetRect::new(210.0, 210.0, 480.0, 480.0);
let (mnx, mny, mxx, mxy) = m.world_bounds().unwrap();
let (wcx, wcy) = ((mnx + mxx) * 0.5, (mny + mxy) * 0.5);
let cam = Camera {
zoom: 1.0,
pan_x: rect.x + rect.w * 0.5 - wcx,
pan_y: rect.y + rect.h * 0.5 - wcy,
};
(m, cam, rect, canvas_w, canvas_h)
}
#[test]
fn composite_clipped_has_no_ink_outside_rect_and_writes_a_nonblank_png() {
let (m, cam, rect, cw, ch) = sprawling();
let bg = Color::rgb(18, 20, 28);
let (frame, rep) = render_graph_clipped(&m, &Decorations::default(), &cam, cw, ch, rect, bg, true);
assert_eq!(rep.ink_outside_rect, 0, "FIX: no pixel ink outside the widget rect");
assert_eq!(rep.geom_ink_outside_rect, 0, "FIX: no chip-geometry vertex escapes the rect");
assert!(rep.lit_px > 0, "the clipped graph is non-blank (lit_px={})", rep.lit_px);
assert!(rep.nodes_drawn > 0, "at least one chip is in-rect ({} drawn)", rep.nodes_drawn);
let dir = blob_dir();
std::fs::create_dir_all(&dir).expect("blob dir");
let png_path = dir.join("graph_clip_final_picture.png");
let img = image::RgbaImage::from_raw(cw, ch, frame.rgba.clone()).expect("rgba→image");
img.save(&png_path).expect("write final-picture PNG blob");
eprintln!("[composite] FINAL PICTURE blob written: {}", png_path.display());
let loaded = image::open(&png_path).expect("reload blob").to_rgba8();
assert_eq!(loaded.dimensions(), (cw, ch), "blob sized to the canvas");
let (mut lit, mut outside) = (0u64, 0u64);
let bg3 = [bg.r as i32, bg.g as i32, bg.b as i32];
for (x, y, p) in loaded.enumerate_pixels() {
let d = (p.0[0] as i32 - bg3[0]).abs() + (p.0[1] as i32 - bg3[1]).abs() + (p.0[2] as i32 - bg3[2]).abs();
let painted = p.0[3] != 0 && d > 6;
if painted {
lit += 1;
let (px, py) = (x as f32 + 0.5, y as f32 + 0.5);
if !rect.contains(px, py) {
outside += 1;
}
}
}
assert!(lit > 0, "the reloaded blob is non-blank (lit={lit})");
assert_eq!(outside, 0, "the reloaded blob has NO painted pixel outside the rect (outside={outside})");
emit("composite_clipped_clean", true, &format!("lit_px={} ink_outside=0 nodes_drawn={}", rep.lit_px, rep.nodes_drawn));
}
#[test]
fn unclipped_variant_leaks_ink_the_clipped_one_does_not() {
let (m, cam, rect, cw, ch) = sprawling();
let bg = Color::rgb(18, 20, 28);
let (_bf, bug) = render_graph_clipped(&m, &Decorations::default(), &cam, cw, ch, rect, bg, false);
let (_ff, fixed) = render_graph_clipped(&m, &Decorations::default(), &cam, cw, ch, rect, bg, true);
assert!(bug.ink_outside_rect > 0, "BUG: the unclipped variant paints outside the rect (ink={})", bug.ink_outside_rect);
assert!(bug.geom_ink_outside_rect > 0, "BUG: the unclipped variant's geometry escapes the rect");
assert_eq!(fixed.ink_outside_rect, 0, "FIX: the clipped variant paints nothing outside the rect");
assert_eq!(fixed.geom_ink_outside_rect, 0, "FIX: the clipped geometry is confined");
assert!(fixed.lit_px > 0 && bug.lit_px > 0, "both variants draw the in-rect graph");
assert_eq!(fixed.nodes_drawn, bug.nodes_drawn, "the cull is identical; only the scissor differs");
emit(
"sensitivity_bug_red_fix_green",
bug.ink_outside_rect > 0 && fixed.ink_outside_rect == 0,
&format!("bug_ink={} fix_ink={}", bug.ink_outside_rect, fixed.ink_outside_rect),
);
}
#[test]
fn decorated_clipped_render_is_clean_and_nonblank() {
let m = grid(3, 3, 240.0);
let rect = WidgetRect::new(10.0, 10.0, 780.0, 580.0);
let cam = Camera::fit(&m, rect.w, rect.h, 60.0);
let (mnx, mny, mxx, mxy) = m.world_bounds().unwrap();
let (wcx, wcy) = ((mnx + mxx) * 0.5, (mny + mxy) * 0.5);
let cam = Camera { zoom: cam.zoom, pan_x: rect.x + rect.w * 0.5 - wcx * cam.zoom, pan_y: rect.y + rect.h * 0.5 - wcy * cam.zoom };
let mut deco = Decorations::default();
deco.nodes.insert("n0".into(), NodeDecoration { scale: None, ring: Some(Color::rgb(240, 80, 80)), badge: None, badge_color: None });
deco.edges.push(GraphEdge { from: "n0".into(), to: "n8".into(), color: Color::rgb(255, 200, 60), dashed: false, label: None });
let (_f, rep) = render_graph_clipped(&m, &deco, &cam, 800, 600, rect, Color::rgb(18, 20, 28), true);
assert_eq!(rep.ink_outside_rect, 0, "decorated render stays inside the rect");
assert!(rep.lit_px > 0, "decorated render is non-blank");
assert_eq!(rep.edges_drawn, 7, "base + emphasis edges drawn");
emit("decorated_clean", true, &format!("edges_drawn={} lit_px={}", rep.edges_drawn, rep.lit_px));
}
#[cfg(feature = "gpu")]
#[test]
fn gpu_lane_clipped_render_has_no_ink_outside_rect() {
use facett_graphview::clip::render_graph_clipped_gpu;
let has_gpu = facett_graphview::gpu::probe_usable_gpu();
eprintln!("[gpu] probe_usable_gpu = {has_gpu}");
let (m, cam, rect, cw, ch) = sprawling();
let bg = Color::rgb(18, 20, 28);
let (frame, fixed) = render_graph_clipped_gpu(&m, &Decorations::default(), &cam, cw, ch, rect, bg, true);
assert_eq!(fixed.ink_outside_rect, 0, "GPU lane: no ink outside the rect (clipped)");
assert_eq!(fixed.geom_ink_outside_rect, 0, "GPU lane: clipped geometry confined");
assert!(fixed.lit_px > 0 && fixed.nodes_drawn > 0, "GPU lane drew the in-rect graph");
let (_bf, bug) = render_graph_clipped_gpu(&m, &Decorations::default(), &cam, cw, ch, rect, bg, false);
assert!(bug.ink_outside_rect > 0, "GPU lane sensitivity: unclipped variant leaks (ink={})", bug.ink_outside_rect);
let dir = blob_dir();
std::fs::create_dir_all(&dir).expect("blob dir");
let png_path = dir.join("graph_clip_final_picture_gpu.png");
image::RgbaImage::from_raw(cw, ch, frame.rgba.clone())
.expect("rgba→image")
.save(&png_path)
.expect("write GPU final-picture blob");
eprintln!("[gpu] FINAL PICTURE blob written: {}", png_path.display());
emit("gpu_lane_clipped_clean", true, &format!("has_gpu={has_gpu} fix_ink=0 bug_ink={}", bug.ink_outside_rect));
}
#[allow(unused_imports)]
use facett_graphview as _gv;
type _R = GraphClipReport;