222 lines
7.0 KiB
Rust
222 lines
7.0 KiB
Rust
//! 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<Item = Rect>) -> Option<Rect> {
|
|
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());
|
|
}
|
|
}
|