facett-flow/tests/host.rs
#![cfg(feature = "host")]
use egui::{Rect, pos2, vec2};
use facett_flow::{Camera, CpuStyle, Edge, FlowScene, Graph, Hit, Node, PaintOpts, View};
fn graph() -> Graph {
Graph {
nodes: vec![
Node {
id: "a".into(),
label: "A".into(),
position: [0.0, 0.0],
group: "g".into(),
kind: 0,
},
Node {
id: "b".into(),
label: "B".into(),
position: [200.0, 100.0],
group: "g".into(),
kind: 2,
},
],
edges: vec![Edge {
from: "a".into(),
to: "b".into(),
}],
}
}
#[test]
fn mapping_matches_camera_and_roundtrips() {
let rect = Rect::from_min_size(pos2(31.0, 43.0), vec2(900.0, 640.0));
let bounds = Rect::from_min_size(pos2(-20.0, -30.0), vec2(500.0, 200.0));
for i in 0..50 {
let camera = Camera {
centre: [i as f32 * 1.37, -i as f32 * 3.2],
zoom: 0.1 + i as f32 * 0.25,
};
let view = camera.view(rect, bounds);
let point = pos2(41.0, 78.0);
let fit =
((rect.width() - 64.0) / bounds.width()).min((rect.height() - 64.0) / bounds.height());
let expected =
rect.center() + (point - pos2(camera.centre[0], camera.centre[1])) * fit * camera.zoom;
assert!(view.screen(point).distance(expected) < 0.001);
assert!(view.world(view.screen(point)).distance(point) < 0.001);
}
let nav = facett_core::nav::Navigable {
scale: 2.0,
offset: [51.0, -19.0],
..Default::default()
};
let v = View::from_navigable(&nav, rect);
assert_eq!(v.world(v.screen(pos2(12.0, 7.0))), pos2(12.0, 7.0));
}
#[test]
fn cache_identity_tracks_geometry_and_replacement_is_atomic() {
let g = graph();
let mut scene = FlowScene::with_key(g.clone(), 17).unwrap();
let generation = scene.generation();
assert_eq!(
generation,
FlowScene::with_key(g.clone(), 17).unwrap().generation()
);
assert_ne!(
generation,
FlowScene::with_key(g.clone(), 18).unwrap().generation()
);
scene.replace(g.clone()).unwrap();
assert_eq!(generation, scene.generation());
let mut moved = g.clone();
moved.nodes[0].position[0] += 1.0;
scene.replace(moved).unwrap();
assert_ne!(generation, scene.generation());
let before = scene.graph().clone();
let mut invalid = g;
invalid.edges[0].to = "missing".into();
assert!(scene.replace(invalid).is_err());
assert_eq!(&before, scene.graph());
}
#[test]
fn cubic_endpoints_and_edge_picking_match() {
let scene = FlowScene::new(graph()).unwrap();
assert_eq!(scene.edge_point(0, 0.0), Some(pos2(0.0, 0.0)));
assert_eq!(scene.edge_point(0, 1.0), Some(pos2(200.0, 100.0)));
assert_eq!(scene.edge_point(0, 0.5), Some(pos2(100.0, 50.0)));
assert_eq!(scene.hit(pos2(100.0, 50.0), 1.0), Some(Hit::Edge(0)));
assert_eq!(scene.hit(pos2(100.0, 75.0), 1.0), None);
assert!(
scene
.hits_in(Rect::from_center_size(pos2(100.0, 50.0), vec2(4.0, 4.0)))
.contains(&Hit::Edge(0))
);
}
#[test]
fn label_budget_does_not_limit_visible_nodes_or_cpu_paint() {
let scene = FlowScene::new(graph()).unwrap();
let ctx = egui::Context::default();
let rect = Rect::from_min_size(pos2(0.0, 0.0), vec2(400.0, 300.0));
let mut painted = None;
let mut output = ctx.run_ui(
egui::RawInput {
screen_rect: Some(rect),
..Default::default()
},
|ui| {
painted = Some(facett_flow::paint(
ui,
egui::Id::new("host"),
rect,
&scene,
&View {
scale: 1.0,
offset: vec2(50.0, 50.0),
},
&PaintOpts {
cpu: CpuStyle::Full { max_edges: 100 },
label_budget: 1,
..Default::default()
},
));
},
);
let painted = painted.unwrap();
assert_eq!(painted.on_screen.len(), 1);
assert_eq!(painted.visible_nodes.len(), 2);
assert!(painted.truncated);
assert!(!painted.gpu);
assert!(!output.shapes.is_empty());
output.textures_delta.clear();
}
#[cfg(feature = "boxes")]
#[test]
fn boxes_extent_picking_and_endpoint_clipping() {
let mut scene = FlowScene::new(graph()).unwrap();
scene
.set_sizes(facett_flow::attr::Sizes {
nodes: vec![[80.0, 40.0]; 2],
})
.unwrap();
assert_eq!(scene.hit(pos2(39.0, 19.0), 0.0), Some(Hit::Node(0)));
assert_eq!(scene.hit(pos2(-40.0, 0.0), 0.0), Some(Hit::Node(0)));
assert_eq!(scene.hit(pos2(-42.0, 0.0), 0.0), None);
assert_eq!(scene.edge_point(0, 0.0), Some(pos2(40.0, 0.0)));
assert_eq!(scene.edge_point(0, 1.0), Some(pos2(160.0, 100.0)));
let before = scene.attribute_generation();
assert!(
scene
.set_sizes(facett_flow::attr::Sizes {
nodes: vec![[f32::NAN, 1.0]; 2]
})
.is_err()
);
assert_eq!(before, scene.attribute_generation());
}
#[cfg(feature = "status")]
#[test]
fn status_is_atomic_and_absent_status_uses_plain_scene() {
use facett_core::Elm;
use facett_flow::{
FlowGraph, Msg,
attr::{Status, StatusTable},
};
use std::sync::Arc;
let mut scene = FlowScene::new(graph()).unwrap();
scene
.set_status(StatusTable {
nodes: vec![None; 2],
edges: vec![None],
})
.unwrap();
assert_eq!(scene.attribute_generation(), None);
let geom = scene.generation();
scene
.set_status(StatusTable {
nodes: vec![Some(Status::Error); 2],
edges: vec![None],
})
.unwrap();
assert_eq!(geom, scene.generation());
let attr = scene.attribute_generation();
assert!(
scene
.set_status(StatusTable {
nodes: vec![],
edges: vec![]
})
.is_err()
);
assert_eq!(attr, scene.attribute_generation());
let mut flow = FlowGraph::new("status", graph()).unwrap();
let before = flow.state().clone();
flow.update(Msg::Status(Arc::new(StatusTable {
nodes: vec![],
edges: vec![],
})));
assert_eq!(&before, flow.state());
let mut replacement = graph();
replacement.nodes.reverse();
scene.replace(replacement).unwrap();
assert!(scene.status().is_none());
}
#[cfg(feature = "rate")]
#[test]
fn rate_invalid_input_does_not_change_state_and_none_keeps_plain() {
use facett_flow::attr::Rates;
let mut scene = FlowScene::new(graph()).unwrap();
scene.set_rates(Rates { edges: vec![None] }).unwrap();
assert_eq!(scene.attribute_generation(), None);
scene
.set_rates(Rates {
edges: vec![Some(0.0)],
})
.unwrap();
let before = scene.attribute_generation();
assert!(
scene
.set_rates(Rates {
edges: vec![Some(-1.0)]
})
.is_err()
);
assert_eq!(before, scene.attribute_generation());
}
#[cfg(feature = "compact-state")]
#[test]
fn compact_is_explicit_and_full_elm_model_roundtrips() {
use facett_core::{Elm, Facet};
use facett_flow::FlowGraph;
let mut flow = FlowGraph::new("large", Graph::demo(100_000)).unwrap();
assert!(flow.state_json().get("graph").is_some());
flow.set_compact_state(true);
let json = flow.state_json();
assert!(serde_json::to_vec(&json).unwrap().len() < 2048);
assert_eq!(json["node_count"], 100_000);
assert!(json.get("graph_digest").is_some());
let restored = FlowGraph::from_model(
serde_json::from_value(serde_json::to_value(flow.state()).unwrap()).unwrap(),
)
.unwrap();
assert_eq!(flow.state(), restored.state());
}
#[cfg(feature = "layered")]
#[test]
fn layered_layout_deterministic_and_directed() {
let mut a = graph();
let mut b = graph();
a.layered(Default::default()).unwrap();
b.layered(Default::default()).unwrap();
assert_eq!(a, b);
assert!(a.nodes[0].position[0] < a.nodes[1].position[0]);
}
#[cfg(feature = "rate")]
#[test]
fn cpu_zero_rate_has_no_motion_and_live_rate_moves() {
use facett_flow::attr::Rates;
let mut scene = FlowScene::new(graph()).unwrap();
let render = |scene: &FlowScene, seconds: f64, min_packet_px: f32| {
let ctx = egui::Context::default();
let rect = Rect::from_min_size(pos2(0.0, 0.0), vec2(400.0, 300.0));
let mut output = ctx.run_ui(
egui::RawInput {
screen_rect: Some(rect),
..Default::default()
},
|ui| {
let opts = PaintOpts {
seconds,
cpu: CpuStyle::Full { max_edges: 100 },
#[cfg(feature = "lod")]
min_packet_px,
..Default::default()
};
let _ = min_packet_px;
facett_flow::paint(
ui,
egui::Id::new("cpu-rate"),
rect,
scene,
&View {
scale: 1.0,
offset: vec2(50.0, 50.0),
},
&opts,
);
},
);
output.textures_delta.clear();
output.shapes
};
scene
.set_rates(Rates {
edges: vec![Some(0.0)],
})
.unwrap();
assert_eq!(render(&scene, 0.0, 0.0), render(&scene, 0.43, 0.0));
scene
.set_rates(Rates {
edges: vec![Some(1.0)],
})
.unwrap();
assert_ne!(render(&scene, 0.0, 0.0), render(&scene, 0.43, 0.0));
#[cfg(feature = "lod")]
assert_eq!(render(&scene, 0.0, 500.0), render(&scene, 0.43, 500.0));
}