facett-graphpan/tests/focus_flight.rs
//! **📌 Stay / 🎯 Focus on the graphpan canvas** (the universe star-field and every
//! `draw_graph` board), rendered: real frames, a real click, the UI clock at 60 fps.
use egui::{pos2, Color32, Pos2, Rect, Vec2};
use facett_core::fly::NavMode;
use facett_graphpan::{draw_graph, Decorations, GraphEdge, GraphModel, GraphNode, GraphView, FOCUS_ZOOM};
const SCREEN: Vec2 = Vec2::new(1200.0, 800.0);
fn model() -> GraphModel {
let n = |id: &str, x: f32, y: f32| GraphNode { id: id.into(), label: id.into(), fill: Color32::from_rgb(120, 200, 255), stroke: Color32::BLACK, pos: pos2(x, y) };
GraphModel {
nodes: vec![n("a", 0.0, 0.0), n("b", 900.0, 500.0), n("c", -400.0, 300.0)],
edges: vec![GraphEdge { from: "a".into(), to: "b".into(), color: Color32::GRAY, dashed: false, label: None }],
}
}
struct Rig {
ctx: egui::Context,
t: f64,
canvas: Rect,
view: GraphView,
}
impl Rig {
fn new(mode: NavMode) -> Self {
let ctx = egui::Context::default();
ctx.set_pixels_per_point(1.0);
mode.remember(&ctx);
Self { ctx, t: 1.0, canvas: Rect::NOTHING, view: GraphView { zoom: 0.5, pan: Vec2::new(-100.0, -40.0), ..Default::default() } }
}
fn frame(&mut self, events: Vec<egui::Event>) {
self.t += 1.0 / 60.0;
let input = egui::RawInput { screen_rect: Some(Rect::from_min_size(Pos2::ZERO, SCREEN)), time: Some(self.t), events, ..Default::default() };
let (m, d) = (model(), Decorations::default());
let view = &mut self.view;
let mut canvas = Rect::NOTHING;
let mut out = self.ctx.run_ui(input, |ui| {
egui::CentralPanel::default().show(ui, |ui| {
canvas = ui.available_rect_before_wrap();
let _ = draw_graph(ui, &m, &d, view, Color32::BLACK, Color32::WHITE, Color32::YELLOW, Color32::GRAY);
});
});
out.textures_delta.clear();
self.canvas = canvas;
}
fn screen_of(&self, id: &str) -> Pos2 {
let p = model().nodes.into_iter().find(|n| n.id == id).unwrap().pos;
self.view.project(self.canvas.center() + self.view.pan, p)
}
fn click(&mut self, p: Pos2) {
self.frame(vec![egui::Event::PointerMoved(p)]);
let b = |pressed| egui::Event::PointerButton { pos: p, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() };
self.frame(vec![b(true)]);
self.frame(vec![b(false)]);
}
}
/// 🎯 Focus: the clicked node flies to the canvas centre (±2 px) at the focus zoom.
#[test]
fn focus_flies_the_clicked_node_to_the_centre() {
let mut rig = Rig::new(NavMode::Focus);
rig.frame(Vec::new());
let start = rig.screen_of("b");
rig.click(start);
let mut frames = 0;
let mut prev = rig.screen_of("b");
let total = (rig.canvas.center() - start).length();
for _ in 0..120 {
rig.frame(Vec::new());
frames += 1;
let p = rig.screen_of("b");
assert!((p - prev).length() <= 0.25 * total, "a frame jumped {}", (p - prev).length());
prev = p;
if (p - rig.canvas.center()).length() < 0.01 && (rig.view.zoom - FOCUS_ZOOM).abs() < 1e-5 {
break;
}
}
assert!(frames >= 20, "a flight, not a jump: {frames}");
assert!((rig.screen_of("b") - rig.canvas.center()).length() <= 2.0, "b at the centre: {:?} vs {:?}", rig.screen_of("b"), rig.canvas.center());
assert!((rig.view.zoom - FOCUS_ZOOM).abs() < 1e-4, "at the focus zoom: {}", rig.view.zoom);
}
/// 📌 Stay: the click selects, the camera never moves.
#[test]
fn stay_a_click_never_moves_the_camera() {
let mut rig = Rig::new(NavMode::Stay);
rig.frame(Vec::new());
let start = rig.screen_of("c");
rig.click(start);
for _ in 0..40 {
rig.frame(Vec::new());
}
assert_eq!(rig.view.selected.as_deref(), Some("c"));
assert_eq!((rig.view.zoom, rig.view.pan), (0.5, Vec2::new(-100.0, -40.0)));
assert_eq!(rig.screen_of("c"), start);
}