use std::collections::{BTreeSet, HashSet};
use std::path::PathBuf;
use uzor::types::Rect;
use uzor_graph::camera::Camera2D;
use uzor_graph::cluster::ClusterRegistry;
use uzor_graph::graph::{Graph, NodeIndex};
use uzor_graph::label_grid::{LabelLodConfig, DEFAULT_LABEL_DENSITY};
use uzor_graph::particle::Particle;
use uzor_graph::render::{
draw_box_select_rect, draw_cluster_edges, draw_cluster_supernodes, draw_edges, draw_hover_card, draw_nodes, DrawContext, HoverCardInfo,
};
use uzor_graph::theme::GraphTheme;
use uzor_graph::FocusSet;
use uzor_proof_harness::{ChannelTolerance, MultiLegDiff, MultiLegRender, RenderContext};
type G = Graph<(), ()>;
const CANVAS_W: u32 = 900;
const CANVAS_H: u32 = 650;
const LABEL_HALO: &str = "#0d0f14";
const CANVAS_BG: &str = "#0d0f14";
fn identity_camera() -> Camera2D {
Camera2D { pan_x: 0.0, pan_y: 0.0, zoom: 1.0 }
}
fn viewport() -> Rect {
Rect::new(0.0, 0.0, CANVAS_W as f64, CANVAS_H as f64)
}
fn out_dir() -> PathBuf {
PathBuf::from(r"C:\Users\VA PC\CODING\ML_TRADING\nemo\uzor\out")
}
fn fill_background(ctx: &mut dyn RenderContext) {
ctx.set_fill_color(CANVAS_BG);
ctx.fill_rect(0.0, 0.0, CANVAS_W as f64, CANVAS_H as f64);
}
fn empty_focus() -> FocusSet {
FocusSet::empty()
}
fn empty_selection() -> BTreeSet<NodeIndex> {
BTreeSet::new()
}
fn empty_node_set() -> HashSet<NodeIndex> {
HashSet::new()
}
fn build_baseline_graph() -> (G, Vec<Particle>, Vec<NodeIndex>) {
let mut graph = G::new();
let mut particles = Vec::new();
let mut nodes = Vec::new();
const SPECS: [(&str, &str, f32, f32, f32); 11] = [
("root", "alpha", 10.0, 80.0, 120.0),
("atlas", "beta", 6.0, 220.0, 80.0),
("beacon", "gamma", 5.0, 380.0, 140.0),
("cobalt", "alpha", 7.0, 520.0, 90.0),
("delta", "delta", 4.0, 660.0, 160.0),
("ember", "beta", 8.0, 800.0, 260.0),
("fjord", "gamma", 5.0, 700.0, 420.0),
("granite", "alpha", 6.0, 560.0, 520.0),
("harbor", "delta", 9.0, 380.0, 560.0),
("ion", "beta", 4.0, 200.0, 500.0),
("juniper", "gamma", 7.0, 60.0, 380.0),
];
for &(label, category, radius, x, y) in &SPECS {
let id = graph.push_node((), label, category, radius);
nodes.push(id);
particles.push(Particle::at(x, y));
}
let hub = graph.push_node((), "hub", "delta", 10.0);
nodes.push(hub);
particles.push(Particle::at(430.0, 300.0));
for &n in &nodes[0..5] {
graph.push_edge(hub, n, 1.4, ());
}
let ring = [nodes[5], nodes[6], nodes[7], nodes[8], nodes[9], nodes[10]];
for i in 0..ring.len() {
graph.push_edge(ring[i], ring[(i + 1) % ring.len()], 1.0, ());
}
graph.push_edge(nodes[0], nodes[5], 0.8, ());
graph.push_edge(nodes[10], nodes[3], 0.8, ());
(graph, particles, nodes)
}
fn draw_baseline_scene(ctx: &mut dyn RenderContext, graph: &G, particles: &[Particle], camera: &Camera2D) {
fill_background(ctx);
let visible: Vec<NodeIndex> = graph.nodes().map(|(id, _)| id).collect();
let focus = empty_focus();
let selection = empty_selection();
let hidden = empty_node_set();
let forced = empty_node_set();
let lod = LabelLodConfig::default();
let theme = GraphTheme::dark();
let dctx = DrawContext {
camera,
viewport: viewport(),
visible: &visible,
focus: &focus,
selection: &selection,
hovered: None,
hidden: &hidden,
label_density: 0.0,
label_halo: LABEL_HALO,
forced_labels: &forced,
label_lod: &lod,
theme: &theme,
};
draw_edges(ctx, graph, particles, &dctx);
draw_nodes(ctx, graph, particles, &dctx);
}
fn run_proof(name: &str, render: &MultiLegRender) -> MultiLegDiff {
let diff = MultiLegDiff::compute(render, ChannelTolerance::default());
for line in diff.report_lines() {
println!("[{name}] {line}");
}
uzor_proof_harness::write_composite_png(render, &out_dir().join(format!("{name}_backends.png")))
.unwrap_or_else(|e| panic!("{name}: composite PNG should write: {e}"));
diff
}
#[test]
fn graph_nodes_and_edges() {
let (graph, particles, _nodes) = build_baseline_graph();
let camera = identity_camera();
let render = MultiLegRender::capture(CANVAS_W, CANVAS_H, |ctx| {
draw_baseline_scene(ctx, &graph, &particles, &camera);
});
let diff = run_proof("graph_nodes_and_edges", &render);
assert!(diff.all_within_budget(), "graph_nodes_and_edges: structural backend divergence detected");
}
#[test]
fn graph_focus_dimming() {
let (graph, particles, nodes) = build_baseline_graph();
let camera = identity_camera();
let hub = nodes[11];
let mut focus = FocusSet::empty();
focus.select_many(graph.neighborhood_focus_keys_depth(hub, 1));
let render = MultiLegRender::capture(CANVAS_W, CANVAS_H, |ctx| {
fill_background(ctx);
let visible: Vec<NodeIndex> = graph.nodes().map(|(id, _)| id).collect();
let selection = empty_selection();
let hidden = empty_node_set();
let forced = empty_node_set();
let lod = LabelLodConfig::default();
let theme = GraphTheme::dark();
let dctx = DrawContext {
camera: &camera,
viewport: viewport(),
visible: &visible,
focus: &focus,
selection: &selection,
hovered: Some(hub),
hidden: &hidden,
label_density: DEFAULT_LABEL_DENSITY,
label_halo: LABEL_HALO,
forced_labels: &forced,
label_lod: &lod,
theme: &theme,
};
draw_edges(ctx, &graph, &particles, &dctx);
draw_nodes(ctx, &graph, &particles, &dctx);
});
let diff = run_proof("graph_focus_dimming", &render);
assert!(diff.all_within_budget(), "graph_focus_dimming: structural backend divergence detected");
}
fn build_label_scene_graph() -> (G, Vec<Particle>) {
let mut graph = G::new();
let mut particles = Vec::new();
const SPECS: [(&str, &str, f32, f32, f32); 8] = [
("north-relay", "alpha", 8.0, 120.0, 100.0),
("south-bridge", "beta", 6.0, 120.0, 300.0),
("east-gate", "gamma", 7.0, 420.0, 100.0),
("west-vault", "delta", 5.0, 420.0, 300.0),
("core-index", "alpha", 9.0, 270.0, 200.0),
("edge-crosser", "beta", 6.0, 650.0, 150.0),
("outer-ring", "gamma", 5.0, 650.0, 350.0),
("deep-node", "delta", 4.0, 800.0, 250.0),
];
let mut ids = Vec::new();
for &(label, category, radius, x, y) in &SPECS {
let id = graph.push_node((), label, category, radius);
ids.push(id);
particles.push(Particle::at(x, y));
}
let core = ids[4];
for &target in &[ids[0], ids[1], ids[2], ids[3], ids[5]] {
graph.push_edge(core, target, 1.0, ());
}
graph.push_edge(ids[5], ids[6], 1.0, ());
graph.push_edge(ids[6], ids[7], 1.0, ());
let cross_a = graph.push_node((), "", "", 0.5);
let cross_b = graph.push_node((), "", "", 0.5);
particles.push(Particle::at(500.0, 154.0));
particles.push(Particle::at(820.0, 154.0));
graph.push_edge(cross_a, cross_b, 0.6, ());
(graph, particles)
}
#[test]
fn graph_labels() {
let (graph, particles) = build_label_scene_graph();
let camera = identity_camera();
let render = MultiLegRender::capture(CANVAS_W, CANVAS_H, |ctx| {
fill_background(ctx);
let visible: Vec<NodeIndex> = graph.nodes().map(|(id, _)| id).collect();
let focus = empty_focus();
let selection = empty_selection();
let hidden = empty_node_set();
let forced = empty_node_set();
let lod = LabelLodConfig::default();
let theme = GraphTheme::dark();
let dctx = DrawContext {
camera: &camera,
viewport: viewport(),
visible: &visible,
focus: &focus,
selection: &selection,
hovered: None,
hidden: &hidden,
label_density: 20.0,
label_halo: LABEL_HALO,
forced_labels: &forced,
label_lod: &lod,
theme: &theme,
};
draw_edges(ctx, &graph, &particles, &dctx);
draw_nodes(ctx, &graph, &particles, &dctx);
});
let diff = run_proof("graph_labels", &render);
assert!(diff.all_within_budget(), "graph_labels: structural backend divergence detected");
}
#[test]
fn graph_selection_and_box_select() {
let (graph, particles, nodes) = build_baseline_graph();
let camera = identity_camera();
let selection: BTreeSet<NodeIndex> = [nodes[2], nodes[7]].into_iter().collect();
let render = MultiLegRender::capture(CANVAS_W, CANVAS_H, |ctx| {
fill_background(ctx);
let visible: Vec<NodeIndex> = graph.nodes().map(|(id, _)| id).collect();
let focus = empty_focus();
let hidden = empty_node_set();
let forced = empty_node_set();
let lod = LabelLodConfig::default();
let theme = GraphTheme::dark();
let dctx = DrawContext {
camera: &camera,
viewport: viewport(),
visible: &visible,
focus: &focus,
selection: &selection,
hovered: None,
hidden: &hidden,
label_density: 0.0,
label_halo: LABEL_HALO,
forced_labels: &forced,
label_lod: &lod,
theme: &theme,
};
draw_edges(ctx, &graph, &particles, &dctx);
draw_nodes(ctx, &graph, &particles, &dctx);
draw_box_select_rect(ctx, Rect::new(150.0, 150.0, 320.0, 220.0), &theme);
});
let diff = run_proof("graph_selection_and_box_select", &render);
assert!(diff.all_within_budget(), "graph_selection_and_box_select: structural backend divergence detected");
}
#[test]
fn graph_hover_card() {
let (graph, particles, nodes) = build_baseline_graph();
let camera = identity_camera();
let hovered = nodes[3]; let node = graph.get_node(hovered).expect("hovered node exists in the baseline fixture");
let p = particles[hovered.index()];
let anchor = camera.world_to_screen((p.x as f64, p.y as f64), viewport());
let info =
HoverCardInfo { label: &node.label, category: &node.category, degree: graph.degree(hovered), pinned: false };
let render = MultiLegRender::capture(CANVAS_W, CANVAS_H, |ctx| {
fill_background(ctx);
let visible: Vec<NodeIndex> = graph.nodes().map(|(id, _)| id).collect();
let focus = empty_focus();
let selection = empty_selection();
let hidden = empty_node_set();
let forced = empty_node_set();
let lod = LabelLodConfig::default();
let theme = GraphTheme::dark();
let dctx = DrawContext {
camera: &camera,
viewport: viewport(),
visible: &visible,
focus: &focus,
selection: &selection,
hovered: Some(hovered),
hidden: &hidden,
label_density: 0.0,
label_halo: LABEL_HALO,
forced_labels: &forced,
label_lod: &lod,
theme: &theme,
};
draw_edges(ctx, &graph, &particles, &dctx);
draw_nodes(ctx, &graph, &particles, &dctx);
draw_hover_card(ctx, anchor, &info, viewport(), &theme.hover_card);
});
let diff = run_proof("graph_hover_card", &render);
assert!(diff.all_within_budget(), "graph_hover_card: structural backend divergence detected");
}
#[test]
fn graph_cluster_supernodes() {
let (mut graph, mut particles, nodes) = build_baseline_graph();
let camera = identity_camera();
let members = vec![nodes[6], nodes[7], nodes[8], nodes[9]];
let mut clusters = ClusterRegistry::default();
let group = clusters.define(&graph, members).expect("cluster over a non-empty member list");
assert!(clusters.collapse(group, &mut graph, &mut particles), "collapse should succeed on a freshly-defined cluster");
let hidden: HashSet<NodeIndex> = clusters.hidden_nodes().collect();
let forced: HashSet<NodeIndex> = clusters.collapsed_clusters().map(|c| c.representative).collect();
let render = MultiLegRender::capture(CANVAS_W, CANVAS_H, |ctx| {
fill_background(ctx);
let visible: Vec<NodeIndex> = graph.nodes().map(|(id, _)| id).filter(|id| !hidden.contains(id)).collect();
let focus = empty_focus();
let selection = empty_selection();
let lod = LabelLodConfig::default();
let theme = GraphTheme::dark();
let dctx = DrawContext {
camera: &camera,
viewport: viewport(),
visible: &visible,
focus: &focus,
selection: &selection,
hovered: None,
hidden: &hidden,
label_density: 6.0,
label_halo: LABEL_HALO,
forced_labels: &forced,
label_lod: &lod,
theme: &theme,
};
draw_edges(ctx, &graph, &particles, &dctx);
draw_cluster_edges(ctx, &particles, &dctx, &clusters);
draw_nodes(ctx, &graph, &particles, &dctx);
draw_cluster_supernodes(ctx, &graph, &particles, &dctx, &clusters);
});
let diff = run_proof("graph_cluster_supernodes", &render);
assert!(diff.all_within_budget(), "graph_cluster_supernodes: structural backend divergence detected");
}
#[test]
fn graph_camera_zoomed_and_panned() {
let (graph, particles, _nodes) = build_baseline_graph();
let camera = Camera2D { pan_x: -140.0, pan_y: 95.0, zoom: 1.6 };
let render = MultiLegRender::capture(CANVAS_W, CANVAS_H, |ctx| {
draw_baseline_scene(ctx, &graph, &particles, &camera);
});
let diff = run_proof("graph_camera_zoomed_and_panned", &render);
assert!(diff.all_within_budget(), "graph_camera_zoomed_and_panned: structural backend divergence detected");
}