//! Geometry shared by the renderer and the interaction code: node rectangles, //! edge anchor points and the cubic Bézier curves edges are drawn with. use eframe::egui::{Pos2, Rect, Vec2, pos2, vec2}; use crate::model::{Node, Side}; /// The node's box in canvas coordinates. pub fn node_rect(node: &Node) -> Rect { Rect::from_min_size( pos2(node.x as f32, node.y as f32), vec2(node.width as f32, node.height as f32), ) } /// The point on `rect` where an edge attached to `side` starts or ends. pub fn anchor(rect: Rect, side: Side) -> Pos2 { match side { Side::Top => pos2(rect.center().x, rect.top()), Side::Bottom => pos2(rect.center().x, rect.bottom()), Side::Left => pos2(rect.left(), rect.center().y), Side::Right => pos2(rect.right(), rect.center().y), } } /// The outward pointing unit normal of a side. pub fn normal(side: Side) -> Vec2 { match side { Side::Top => vec2(0.0, -1.0), Side::Bottom => vec2(0.0, 1.0), Side::Left => vec2(-1.0, 0.0), Side::Right => vec2(1.0, 0.0), } } /// Picks the sides for an edge whose `fromSide`/`toSide` the file leaves out. /// /// The dominant axis between the two node centres wins, which is what the /// reference implementations do. pub fn auto_sides(from: Rect, to: Rect) -> (Side, Side) { let delta = to.center() - from.center(); // Compare the gaps rather than the raw centre distance so that a wide node // sitting just above a narrow one still connects top-to-bottom. let gap_x = (to.left() - from.right()).max(from.left() - to.right()); let gap_y = (to.top() - from.bottom()).max(from.top() - to.bottom()); let horizontal = if gap_x >= 0.0 && gap_y >= 0.0 { gap_x >= gap_y } else if gap_x >= 0.0 { true } else if gap_y >= 0.0 { false } else { delta.x.abs() >= delta.y.abs() }; if horizontal { if delta.x >= 0.0 { (Side::Right, Side::Left) } else { (Side::Left, Side::Right) } } else if delta.y >= 0.0 { (Side::Bottom, Side::Top) } else { (Side::Top, Side::Bottom) } } /// The four control points of the cubic Bézier used to draw an edge. pub fn edge_curve(from: Rect, from_side: Side, to: Rect, to_side: Side) -> [Pos2; 4] { let start = anchor(from, from_side); let end = anchor(to, to_side); curve_between(start, from_side, end, to_side) } /// Same as [`edge_curve`] but for free endpoints (used while dragging a new edge). pub fn curve_between(start: Pos2, from_side: Side, end: Pos2, to_side: Side) -> [Pos2; 4] { let distance = (end - start).length(); let strength = (distance * 0.45).clamp(30.0, 250.0); [ start, start + normal(from_side) * strength, end + normal(to_side) * strength, end, ] } /// Point on a cubic Bézier at `t` in `0..=1`. pub fn bezier_point(p: [Pos2; 4], t: f32) -> Pos2 { let u = 1.0 - t; let (a, b, c, d) = (u * u * u, 3.0 * u * u * t, 3.0 * u * t * t, t * t * t); pos2( a * p[0].x + b * p[1].x + c * p[2].x + d * p[3].x, a * p[0].y + b * p[1].y + c * p[2].y + d * p[3].y, ) } /// Derivative of a cubic Bézier at `t`; the direction the curve travels in. pub fn bezier_tangent(p: [Pos2; 4], t: f32) -> Vec2 { let u = 1.0 - t; let d = (p[1] - p[0]) * (3.0 * u * u) + (p[2] - p[1]) * (6.0 * u * t) + (p[3] - p[2]) * (3.0 * t * t); if d.length() > f32::EPSILON { d.normalized() } else { (p[3] - p[0]).normalized() } } /// How many samples are used when approximating a curve by a polyline. const SAMPLES: usize = 24; /// Shortest distance from `point` to the curve, used for hit testing edges. pub fn distance_to_curve(p: [Pos2; 4], point: Pos2) -> f32 { let mut best = f32::INFINITY; let mut previous = p[0]; for i in 1..=SAMPLES { let current = bezier_point(p, i as f32 / SAMPLES as f32); best = best.min(distance_to_segment(point, previous, current)); previous = current; } best } /// Axis aligned bounds of the curve, approximated from samples. pub fn curve_bounds(p: [Pos2; 4]) -> Rect { let mut rect = Rect::from_points(&[p[0], p[3]]); for i in 1..SAMPLES { rect = rect.union(Rect::from_points(&[bezier_point( p, i as f32 / SAMPLES as f32, )])); } rect } fn distance_to_segment(point: Pos2, a: Pos2, b: Pos2) -> f32 { let ab = b - a; let len_sq = ab.length_sq(); if len_sq <= f32::EPSILON { return (point - a).length(); } let t = ((point - a).dot(ab) / len_sq).clamp(0.0, 1.0); (point - (a + ab * t)).length() } /// The bounding box of everything in `rects`, or `None` when there is nothing. pub fn bounds_of(rects: impl IntoIterator) -> Option { rects.into_iter().reduce(|acc, r| acc.union(r)) } #[cfg(test)] mod tests { use super::*; fn rect(x: f32, y: f32, w: f32, h: f32) -> Rect { Rect::from_min_size(pos2(x, y), vec2(w, h)) } #[test] fn anchors_sit_on_the_middle_of_each_side() { let r = rect(0.0, 0.0, 100.0, 50.0); assert_eq!(anchor(r, Side::Top), pos2(50.0, 0.0)); assert_eq!(anchor(r, Side::Bottom), pos2(50.0, 50.0)); assert_eq!(anchor(r, Side::Left), pos2(0.0, 25.0)); assert_eq!(anchor(r, Side::Right), pos2(100.0, 25.0)); } #[test] fn auto_sides_follow_the_dominant_axis() { let a = rect(0.0, 0.0, 100.0, 100.0); assert_eq!( auto_sides(a, rect(400.0, 0.0, 100.0, 100.0)), (Side::Right, Side::Left) ); assert_eq!( auto_sides(a, rect(-400.0, 0.0, 100.0, 100.0)), (Side::Left, Side::Right) ); assert_eq!( auto_sides(a, rect(0.0, 400.0, 100.0, 100.0)), (Side::Bottom, Side::Top) ); assert_eq!( auto_sides(a, rect(0.0, -400.0, 100.0, 100.0)), (Side::Top, Side::Bottom) ); } #[test] fn a_curve_starts_and_ends_on_its_anchors() { let from = rect(0.0, 0.0, 100.0, 100.0); let to = rect(300.0, 0.0, 100.0, 100.0); let curve = edge_curve(from, Side::Right, to, Side::Left); assert_eq!(bezier_point(curve, 0.0), anchor(from, Side::Right)); assert_eq!(bezier_point(curve, 1.0), anchor(to, Side::Left)); // Leaving a right side means travelling to the right. assert!(bezier_tangent(curve, 0.0).x > 0.9); } #[test] fn hit_testing_measures_distance_to_the_curve() { let curve = [ pos2(0.0, 0.0), pos2(50.0, 0.0), pos2(50.0, 0.0), pos2(100.0, 0.0), ]; assert!(distance_to_curve(curve, pos2(50.0, 0.0)) < 0.5); assert!((distance_to_curve(curve, pos2(50.0, 20.0)) - 20.0).abs() < 0.5); } #[test] fn bounds_cover_every_rect() { let all = bounds_of([rect(0.0, 0.0, 10.0, 10.0), rect(90.0, 40.0, 10.0, 10.0)]).unwrap(); assert_eq!(all, rect(0.0, 0.0, 100.0, 50.0)); assert!(bounds_of([]).is_none()); } }