Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/facett

facett-graph3d/tests/nav_focus.rs

//! **3D 📌 Stay / 🎯 Focus**, rendered: real egui frames, a real click on a node's
//! projected spot, the UI clock at 60 fps.
//!
//! * Focus: the camera target glides to the node — after the flight its projection is
//!   the canvas centre (±2 px) — on a continuous path, the view direction (yaw) kept.
//! * Stay: a click never moves the 3D camera.

use egui::{Pos2, Rect, Vec2};
use facett_core::fly::NavMode;
use facett_core::Facet;
use facett_graph3d::{DecorPolicy, Graph3D, Layout3D, Msg};

const SCREEN: Vec2 = Vec2::new(1200.0, 800.0);
const DT: f64 = 1.0 / 60.0;

fn graph(mode: NavMode) -> Graph3D {
    let nodes: Vec<(String, egui::Color32)> = (0..5).map(|i| (format!("n{i}"), egui::Color32::from_rgb(90, 160, 255))).collect();
    let pos = vec![[0.0, 0.0, 0.0], [0.8, 0.6, -0.3], [0.95, 0.4, 0.1], [-0.7, -0.5, 0.4], [0.6, 0.75, -0.5]];
    let mut g = Graph3D::new(nodes, vec![(0, 1), (1, 2), (1, 4), (0, 3)], Layout3D::Given)
        .with_positions(pos)
        .with_decor(DecorPolicy::Reduced);
    let _ = g.update(Msg::SetSpin(false));
    let _ = g.update(Msg::SetNavMode(mode));
    g
}

struct Rig {
    ctx: egui::Context,
    t: f64,
    canvas: Rect,
}

impl Rig {
    fn new() -> Self {
        let ctx = egui::Context::default();
        ctx.set_pixels_per_point(1.0);
        Self { ctx, t: 1.0, canvas: Rect::NOTHING }
    }

    fn frame(&mut self, g: &mut Graph3D, events: Vec<egui::Event>) {
        self.t += DT;
        let input = egui::RawInput { screen_rect: Some(Rect::from_min_size(Pos2::ZERO, SCREEN)), time: Some(self.t), events, ..Default::default() };
        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();
                g.ui(ui);
            });
        });
        out.textures_delta.clear();
        self.canvas = canvas;
    }

    fn click(&mut self, g: &mut Graph3D, p: Pos2) {
        self.frame(g, vec![egui::Event::PointerMoved(p)]);
        let b = |pressed| egui::Event::PointerButton { pos: p, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() };
        self.frame(g, vec![b(true)]);
        self.frame(g, vec![b(false)]);
    }

    fn land(&mut self, g: &mut Graph3D) -> usize {
        let mut frames = 0;
        for _ in 0..120 {
            self.frame(g, Vec::new());
            frames += 1;
            if !g.flying() {
                break;
            }
        }
        frames
    }

    fn screen_of(&self, g: &Graph3D, i: usize) -> Pos2 {
        let scale = self.canvas.size().min_elem() * 0.40;
        g.project_all(self.canvas.center(), scale, g.render_yaw())[i].0
    }
}

/// 🎯 Focus: node 1 glides to the centre, the yaw is kept, the path is continuous.
#[test]
fn focus_flies_the_clicked_node_to_the_centre() {
    let mut rig = Rig::new();
    let mut g = graph(NavMode::Focus);
    rig.frame(&mut g, Vec::new());
    let yaw = g.render_yaw();
    let start = rig.screen_of(&g, 1);
    rig.click(&mut g, start);
    assert!(g.flying(), "the click started a flight");
    let mut path = Vec::new();
    for _ in 0..120 {
        rig.frame(&mut g, Vec::new());
        path.push(rig.screen_of(&g, 1));
        if !g.flying() {
            break;
        }
    }
    assert!(path.len() >= 20, "a flight, not a jump: {} frames", path.len());
    let end = rig.screen_of(&g, 1);
    let c = rig.canvas.center();
    assert!((end - c).length() <= 2.0, "node 1 lands at the canvas centre: {end:?} vs {c:?}");
    assert!((g.render_yaw() - yaw).abs() < 1e-6, "the view direction is kept (no spin)");
    assert!(g.state().zoom > 1.0, "zoomed in to frame it and its neighbours: {}", g.state().zoom);
    let total = (c - start).length();
    let mut prev = start;
    for p in &path {
        assert!((*p - prev).length() <= 0.25 * total.max(1.0), "a frame jumped {} of {total}", (*p - prev).length());
        prev = *p;
    }
    // the neighbours stay on the canvas
    for j in [0, 2, 4] {
        assert!(rig.canvas.contains(rig.screen_of(&g, j)), "neighbour {j} framed");
    }
}

/// 📌 Stay: the click selects, the 3D camera does not move.
#[test]
fn stay_a_click_never_moves_the_3d_camera() {
    let mut rig = Rig::new();
    let mut g = graph(NavMode::Stay);
    rig.frame(&mut g, Vec::new());
    let start = rig.screen_of(&g, 1);
    let cam = (g.state().focus, g.state().zoom, g.render_yaw());
    rig.click(&mut g, start);
    rig.land(&mut g);
    assert_eq!(g.state().selected, Some(1), "the click selected node 1");
    assert_eq!((g.state().focus, g.state().zoom, g.render_yaw()), cam, "the camera did not move");
    assert_eq!(rig.screen_of(&g, 1), start);
}

/// ⊙ fit flies back to the whole graph, and the remembered mode reaches a new pane.
#[test]
fn fit_flies_back_and_the_choice_is_remembered() {
    let mut rig = Rig::new();
    let mut g = graph(NavMode::Focus);
    rig.frame(&mut g, Vec::new());
    let p = rig.screen_of(&g, 2);
    rig.click(&mut g, p);
    rig.land(&mut g);
    let whole = g.frame_camera(rig.canvas.width(), rig.canvas.height());
    assert_ne!(g.state().zoom, whole.1);
    let _ = g.update(Msg::FitFly(rig.t));
    rig.land(&mut g);
    assert_eq!((g.state().focus, g.state().zoom), whole, "back to the whole graph");

    NavMode::Focus.remember(&rig.ctx);
    let nodes: Vec<(String, egui::Color32)> = (0..3).map(|i| (format!("m{i}"), egui::Color32::WHITE)).collect();
    let mut h = Graph3D::new(nodes, vec![(0, 1)], Layout3D::Sphere);
    rig.frame(&mut h, Vec::new());
    assert_eq!(h.state().nav_mode, NavMode::Focus, "a new 3D pane starts in the viewer's mode");
}

/// **The wheel zooms, toward the cursor** (the 🏛 3D room: "I can't zoom"): a real
/// mouse-wheel event over node 1 zooms in and node 1 stays under the cursor; ± on the
/// keyboard zooms too; and the range reaches a close-up of one node. RED before: the
/// pane had no zoom at all — the wheel did nothing.
#[test]
fn the_wheel_zooms_toward_the_cursor_and_reaches_a_close_up() {
    let mut rig = Rig::new();
    let mut g = graph(NavMode::Stay);
    rig.frame(&mut g, Vec::new());
    let p = rig.screen_of(&g, 1);
    let z0 = g.state().zoom;
    let wheel = |dy: f32| egui::Event::MouseWheel { unit: egui::MouseWheelUnit::Point, delta: Vec2::new(0.0, dy), phase: egui::TouchPhase::Move, modifiers: Default::default() };
    rig.frame(&mut g, vec![egui::Event::PointerMoved(p)]);
    rig.frame(&mut g, vec![wheel(120.0)]);
    for _ in 0..30 {
        rig.frame(&mut g, Vec::new());
    }
    let z1 = g.state().zoom;
    assert!(z1 > z0 * 1.1, "the wheel zoomed in: {z0} → {z1}");
    let q = rig.screen_of(&g, 1);
    assert!((q - p).length() <= 1.0, "the node under the cursor stayed under it: {p:?} → {q:?}");

    rig.frame(&mut g, vec![egui::Event::Key { key: egui::Key::Minus, physical_key: None, pressed: true, repeat: false, modifiers: Default::default() }]);
    rig.frame(&mut g, Vec::new());
    assert!(g.state().zoom < z1, "− zooms out: {}", g.state().zoom);

    for _ in 0..40 {
        rig.frame(&mut g, vec![wheel(400.0)]);
    }
    for _ in 0..30 {
        rig.frame(&mut g, Vec::new());
    }
    assert!(g.state().zoom >= 50.0, "a close-up of one node is reachable: zoom {}", g.state().zoom);
}

/// **A host-placed cloud opens fitted and centred** — positions far from the origin
/// (x in 3..5), in a canvas inside an over-wide layout: after the first frames the
/// projected box of the nodes is centred (±2 px) in the visible canvas and wholly inside
/// it. RED before: the camera looked at world (0, 0, 0), the cloud sat off to the side
/// (and past the window), at a zoom made for `[-1, 1]³`.
#[test]
fn a_host_placed_cloud_opens_centred_in_the_visible_canvas() {
    let nodes: Vec<(String, egui::Color32)> = (0..6).map(|i| (format!("p{i}"), egui::Color32::WHITE)).collect();
    let pos = vec![[3.0, 0.2, 0.0], [5.0, 0.9, 0.1], [4.2, -0.6, 0.3], [3.6, 1.4, -0.2], [4.8, -1.1, 0.0], [3.3, 0.0, 0.4]];
    let mut g = Graph3D::new(nodes, vec![(0, 1), (1, 2)], Layout3D::Given).with_positions(pos);
    let _ = g.update(Msg::SetSpin(false));
    let ctx = egui::Context::default();
    ctx.set_pixels_per_point(1.0);
    let mut canvas = Rect::NOTHING;
    for i in 0..4 {
        let input = egui::RawInput { screen_rect: Some(Rect::from_min_size(Pos2::ZERO, SCREEN)), time: Some(1.0 + i as f64 / 60.0), ..Default::default() };
        let mut out = ctx.run_ui(input, |ui| {
            egui::CentralPanel::default().show(ui, |ui| {
                let wide = Rect::from_min_size(ui.max_rect().min, Vec2::new(6000.0, ui.max_rect().height()));
                ui.scope_builder(egui::UiBuilder::new().max_rect(wide), |ui| {
                    canvas = Rect::from_min_size(ui.cursor().min, facett_core::nav::canvas_size(ui));
                    g.ui(ui);
                });
            });
        });
        out.textures_delta.clear();
    }
    let seen = canvas.intersect(Rect::from_min_size(Pos2::ZERO, SCREEN));
    let scale = canvas.size().min_elem() * 0.40;
    let pts: Vec<Pos2> = g.project_all(canvas.center(), scale, g.render_yaw()).into_iter().map(|(p, _)| p).collect();
    let (min, max) = pts.iter().fold((Pos2::new(f32::MAX, f32::MAX), Pos2::new(f32::MIN, f32::MIN)), |(a, b), p| (a.min(*p), b.max(*p)));
    let b = Rect::from_min_max(min, max);
    assert!(canvas.width() <= SCREEN.x, "the canvas is the visible width: {canvas:?}");
    assert!((b.center() - seen.center()).length() <= 2.0, "centred: box {b:?} in {seen:?}");
    assert!(seen.contains_rect(b), "wholly visible: box {b:?} in {seen:?}");
}