use std::ops::{Add, Div, Mul, Sub};
pub type Id = String;
#[derive(Clone, Copy, PartialEq, Debug, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Point {
pub x: f64,
pub y: f64,
}
impl Point {
pub const ZERO: Point = Point { x: 0.0, y: 0.0 };
pub const fn new(x: f64, y: f64) -> Self {
Self { x, y }
}
pub fn distance_sq(&self, other: Point) -> f64 {
let d = *self - other;
d.x * d.x + d.y * d.y
}
pub fn distance(&self, other: Point) -> f64 {
self.distance_sq(other).sqrt()
}
pub fn lerp(&self, other: Point, t: f64) -> Point {
Point::new(
self.x + (other.x - self.x) * t,
self.y + (other.y - self.y) * t,
)
}
}
impl From<(f64, f64)> for Point {
fn from((x, y): (f64, f64)) -> Self {
Point::new(x, y)
}
}
impl Add for Point {
type Output = Point;
fn add(self, rhs: Point) -> Point {
Point::new(self.x + rhs.x, self.y + rhs.y)
}
}
impl Sub for Point {
type Output = Point;
fn sub(self, rhs: Point) -> Point {
Point::new(self.x - rhs.x, self.y - rhs.y)
}
}
impl Mul<f64> for Point {
type Output = Point;
fn mul(self, rhs: f64) -> Point {
Point::new(self.x * rhs, self.y * rhs)
}
}
impl Div<f64> for Point {
type Output = Point;
fn div(self, rhs: f64) -> Point {
Point::new(self.x / rhs, self.y / rhs)
}
}
#[derive(Clone, Copy, PartialEq, Debug, Default)]
pub struct Size {
pub width: f64,
pub height: f64,
}
impl Size {
pub fn new(width: f64, height: f64) -> Self {
Self { width, height }
}
}
impl From<(f64, f64)> for Size {
fn from((width, height): (f64, f64)) -> Self {
Size::new(width, height)
}
}
#[derive(Clone, Copy, PartialEq, Debug, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Rect {
pub x: f64,
pub y: f64,
pub width: f64,
pub height: f64,
}
impl Rect {
pub const ZERO: Rect = Rect {
x: 0.0,
y: 0.0,
width: 0.0,
height: 0.0,
};
pub const fn new(x: f64, y: f64, width: f64, height: f64) -> Self {
Self {
x,
y,
width,
height,
}
}
pub fn from_points(origin: Point, size: Size) -> Self {
Self::new(origin.x, origin.y, size.width, size.height)
}
pub fn between(a: Point, b: Point) -> Self {
Self {
x: a.x.min(b.x),
y: a.y.min(b.y),
width: (a.x - b.x).abs(),
height: (a.y - b.y).abs(),
}
}
pub fn origin(&self) -> Point {
Point::new(self.x, self.y)
}
pub fn size(&self) -> Size {
Size::new(self.width, self.height)
}
pub fn center(&self) -> Point {
Point::new(self.x + self.width / 2.0, self.y + self.height / 2.0)
}
pub fn max_x(&self) -> f64 {
self.x + self.width
}
pub fn max_y(&self) -> f64 {
self.y + self.height
}
pub fn union(&self, other: &Rect) -> Rect {
let x = self.x.min(other.x);
let y = self.y.min(other.y);
Rect::new(
x,
y,
self.max_x().max(other.max_x()) - x,
self.max_y().max(other.max_y()) - y,
)
}
pub fn contains(&self, p: Point) -> bool {
p.x >= self.x && p.x <= self.max_x() && p.y >= self.y && p.y <= self.max_y()
}
pub fn intersects(&self, other: Rect) -> bool {
self.x <= other.max_x()
&& self.max_x() >= other.x
&& self.y <= other.max_y()
&& self.max_y() >= other.y
}
pub fn expanded(&self, by: f64) -> Rect {
Rect::new(
self.x - by,
self.y - by,
self.width + by * 2.0,
self.height + by * 2.0,
)
}
pub fn distance_to_edge(&self, point: Point) -> f64 {
let dx = (self.x - point.x).max(point.x - self.max_x());
let dy = (self.y - point.y).max(point.y - self.max_y());
if dx > 0.0 || dy > 0.0 {
dx.max(0.0).hypot(dy.max(0.0))
} else {
dx.max(dy)
}
}
pub fn bounds(rects: impl IntoIterator<Item = Self>) -> Option<Self> {
let mut min_x = f64::INFINITY;
let mut min_y = f64::INFINITY;
let mut max_x = f64::NEG_INFINITY;
let mut max_y = f64::NEG_INFINITY;
for rect in rects {
min_x = min_x.min(rect.x);
min_y = min_y.min(rect.y);
max_x = max_x.max(rect.max_x());
max_y = max_y.max(rect.max_y());
}
min_x
.is_finite()
.then_some(Self::new(min_x, min_y, max_x - min_x, max_y - min_y))
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Grid {
size: f64,
}
impl Grid {
pub const fn new(size: f64) -> Self {
Self {
size: if size.is_finite() && size > 0.0 {
size
} else {
1.0
},
}
}
pub const fn size(self) -> f64 {
self.size
}
pub fn snap(self, value: f64) -> f64 {
(value / self.size).round() * self.size
}
pub fn snap_up(self, value: f64) -> f64 {
(value / self.size).ceil() * self.size
}
pub fn snap_point(self, point: Point) -> Point {
Point::new(self.snap(point.x), self.snap(point.y))
}
pub fn snap_rect(self, rect: Rect) -> Rect {
Rect::new(
self.snap(rect.x),
self.snap(rect.y),
self.snap(rect.width),
self.snap(rect.height),
)
}
}
#[derive(Clone, Copy, PartialEq, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Viewport {
pub x: f64,
pub y: f64,
pub zoom: f64,
}
impl Default for Viewport {
fn default() -> Self {
Self {
x: 0.0,
y: 0.0,
zoom: 1.0,
}
}
}
impl Viewport {
pub const fn new(x: f64, y: f64, zoom: f64) -> Self {
Self { x, y, zoom }
}
pub const fn offset(&self) -> Point {
Point::new(self.x, self.y)
}
pub const fn with_offset(self, offset: Point) -> Self {
Self {
x: offset.x,
y: offset.y,
zoom: self.zoom,
}
}
pub fn screen_to_flow(&self, p: Point) -> Point {
(p - self.offset()) / self.zoom
}
pub fn flow_to_screen(&self, p: Point) -> Point {
p * self.zoom + self.offset()
}
pub fn zoom_about(self, zoom: f64, screen: Point, min_zoom: f64, max_zoom: f64) -> Self {
if !zoom.is_finite() {
return self;
}
let zoom = zoom.clamp(min_zoom, max_zoom);
let flow = self.screen_to_flow(screen);
Self {
x: screen.x - flow.x * zoom,
y: screen.y - flow.y * zoom,
zoom,
}
}
pub fn panned(self, by: Point) -> Self {
Self {
x: self.x + by.x,
y: self.y + by.y,
..self
}
}
pub fn is_sane(self, min_zoom: f64, max_zoom: f64) -> bool {
self.x.is_finite() && self.y.is_finite() && (min_zoom..=max_zoom).contains(&self.zoom)
}
pub fn fit(drawing: Option<Rect>, safe: Rect, min_zoom: f64, max_zoom: f64) -> Self {
let Some(drawing) = drawing else {
return Self {
x: safe.x + safe.width / 2.0,
y: safe.y + safe.height / 2.0,
zoom: 1.0,
};
};
let width = drawing.width.max(1.0);
let height = drawing.height.max(1.0);
let wanted = (safe.width / width).min(safe.height / height);
let zoom = if wanted.is_finite() {
wanted.clamp(min_zoom, max_zoom)
} else {
1.0
};
Self {
x: safe.x + (safe.width - width * zoom) / 2.0 - drawing.x * zoom,
y: safe.y + (safe.height - height * zoom) / 2.0 - drawing.y * zoom,
zoom,
}
}
pub fn lerp(&self, other: &Viewport, t: f64) -> Viewport {
Viewport {
x: self.x + (other.x - self.x) * t,
y: self.y + (other.y - self.y) * t,
zoom: self.zoom + (other.zoom - self.zoom) * t,
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
pub enum Side {
Top,
Right,
Bottom,
Left,
}
impl Side {
pub fn normal(&self) -> Point {
match self {
Side::Top => Point::new(0.0, -1.0),
Side::Right => Point::new(1.0, 0.0),
Side::Bottom => Point::new(0.0, 1.0),
Side::Left => Point::new(-1.0, 0.0),
}
}
pub fn opposite(&self) -> Side {
match self {
Side::Top => Side::Bottom,
Side::Right => Side::Left,
Side::Bottom => Side::Top,
Side::Left => Side::Right,
}
}
pub fn is_horizontal(&self) -> bool {
matches!(self, Side::Left | Side::Right)
}
pub(crate) fn class_name(&self) -> &'static str {
match self {
Side::Top => "top",
Side::Right => "right",
Side::Bottom => "bottom",
Side::Left => "left",
}
}
}
pub fn side_point(rect: &Rect, side: Side, frac: f64) -> Point {
match side {
Side::Top => Point::new(rect.x + rect.width * frac, rect.y),
Side::Bottom => Point::new(rect.x + rect.width * frac, rect.max_y()),
Side::Left => Point::new(rect.x, rect.y + rect.height * frac),
Side::Right => Point::new(rect.max_x(), rect.y + rect.height * frac),
}
}
pub(crate) const DEFAULT_NODE_SIZE: Size = Size {
width: 150.0,
height: 40.0,
};
#[derive(Clone, PartialEq, Debug)]
pub struct Node<T = ()> {
pub id: Id,
pub position: Point,
pub label: String,
pub data: T,
pub node_type: Option<String>,
pub source_side: Side,
pub target_side: Side,
pub draggable: bool,
pub selectable: bool,
pub selected: bool,
pub size: Option<Size>,
pub measured: Option<Size>,
pub class: Option<String>,
pub style: Option<String>,
}
impl Node<()> {
pub fn new(id: impl Into<Id>, label: impl Into<String>, position: impl Into<Point>) -> Self {
Self::with_data(id, label, position, ())
}
}
impl<T> Node<T> {
pub fn with_data(
id: impl Into<Id>,
label: impl Into<String>,
position: impl Into<Point>,
data: T,
) -> Self {
Self {
id: id.into(),
position: position.into(),
label: label.into(),
data,
node_type: None,
source_side: Side::Bottom,
target_side: Side::Top,
draggable: true,
selectable: true,
selected: false,
size: None,
measured: None,
class: None,
style: None,
}
}
pub fn node_type(mut self, ty: impl Into<String>) -> Self {
self.node_type = Some(ty.into());
self
}
pub fn size(mut self, size: impl Into<Size>) -> Self {
self.size = Some(size.into());
self
}
pub fn class(mut self, class: impl Into<String>) -> Self {
self.class = Some(class.into());
self
}
pub fn style(mut self, style: impl Into<String>) -> Self {
self.style = Some(style.into());
self
}
pub fn draggable(mut self, draggable: bool) -> Self {
self.draggable = draggable;
self
}
pub fn selectable(mut self, selectable: bool) -> Self {
self.selectable = selectable;
self
}
pub fn sides(mut self, target: Side, source: Side) -> Self {
self.target_side = target;
self.source_side = source;
self
}
pub fn rect(&self) -> Rect {
let size = self.size.or(self.measured).unwrap_or(DEFAULT_NODE_SIZE);
Rect::from_points(self.position, size)
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum EdgeKind {
#[default]
Bezier,
Straight,
SmoothStep,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum MarkerKind {
#[default]
ArrowClosed,
Arrow,
None,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum AnchorMode {
#[default]
Handles,
Seats,
}
#[derive(Clone, PartialEq, Debug)]
pub struct Edge {
pub id: Id,
pub source: Id,
pub target: Id,
pub source_handle: Option<Id>,
pub target_handle: Option<Id>,
pub source_seat: Option<crate::ports::PortSeat>,
pub target_seat: Option<crate::ports::PortSeat>,
pub label: Option<String>,
pub label_position: f64,
pub weight: u8,
pub kind: EdgeKind,
pub animated: bool,
pub selected: bool,
pub selectable: bool,
pub marker_start: MarkerKind,
pub marker_end: MarkerKind,
pub class: Option<String>,
pub style: Option<String>,
}
impl Edge {
pub fn new(source: impl Into<Id>, target: impl Into<Id>) -> Self {
let source = source.into();
let target = target.into();
Self {
id: format!("{source}->{target}"),
source,
target,
source_handle: None,
target_handle: None,
source_seat: None,
target_seat: None,
label: None,
label_position: 0.5,
weight: 2,
kind: EdgeKind::default(),
animated: false,
selected: false,
selectable: true,
marker_start: MarkerKind::None,
marker_end: MarkerKind::default(),
class: None,
style: None,
}
}
pub fn id(mut self, id: impl Into<Id>) -> Self {
self.id = id.into();
self
}
pub fn label(mut self, label: impl Into<String>) -> Self {
self.label = Some(label.into());
self
}
pub fn label_position(mut self, position: f64) -> Self {
self.label_position = position;
self
}
pub fn weight(mut self, weight: u8) -> Self {
self.weight = weight;
self
}
pub fn kind(mut self, kind: EdgeKind) -> Self {
self.kind = kind;
self
}
pub fn animated(mut self, animated: bool) -> Self {
self.animated = animated;
self
}
pub fn marker_start(mut self, marker: MarkerKind) -> Self {
self.marker_start = marker;
self
}
pub fn marker_end(mut self, marker: MarkerKind) -> Self {
self.marker_end = marker;
self
}
pub fn source_seat(mut self, seat: crate::ports::PortSeat) -> Self {
self.source_seat = Some(seat);
self
}
pub fn target_seat(mut self, seat: crate::ports::PortSeat) -> Self {
self.target_seat = Some(seat);
self
}
pub fn source_handle(mut self, id: impl Into<Id>) -> Self {
self.source_handle = Some(id.into());
self
}
pub fn target_handle(mut self, id: impl Into<Id>) -> Self {
self.target_handle = Some(id.into());
self
}
pub fn class(mut self, class: impl Into<String>) -> Self {
self.class = Some(class.into());
self
}
pub fn style(mut self, style: impl Into<String>) -> Self {
self.style = Some(style.into());
self
}
}
#[derive(Clone, PartialEq, Debug)]
pub struct Connection {
pub source: Id,
pub target: Id,
pub source_handle: Option<Id>,
pub target_handle: Option<Id>,
}
#[derive(Clone, PartialEq, Debug)]
pub struct ConnectEnd {
pub point: Point,
pub connection: Option<Connection>,
}
#[derive(Clone, PartialEq, Debug)]
pub struct DeleteRequest {
pub nodes: Vec<Id>,
pub edges: Vec<Id>,
}
impl Connection {
pub fn into_edge(self) -> Edge {
let mut edge = Edge::new(self.source, self.target);
edge.source_handle = self.source_handle;
edge.target_handle = self.target_handle;
if let Some(h) = &edge.source_handle {
edge.id = format!("{}#{}", edge.id, h);
}
if let Some(h) = &edge.target_handle {
edge.id = format!("{}#{}", edge.id, h);
}
edge
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub enum HandleKind {
Source,
Target,
}
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
pub struct HandleKey {
pub node: Id,
pub kind: HandleKind,
pub id: Id,
}
#[derive(Clone, Copy, PartialEq, Debug)]
pub struct HandleGeom {
pub side: Side,
pub offset: f64,
}
#[derive(Clone, PartialEq, Debug)]
pub struct NodeGeom {
pub id: Id,
pub rect: Rect,
pub selected: bool,
pub source_side: Side,
pub target_side: Side,
pub measured: bool,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn viewport_roundtrip() {
let vp = Viewport::new(13.0, -7.0, 1.7);
let p = Point::new(100.0, 250.0);
let q = vp.screen_to_flow(vp.flow_to_screen(p));
assert!(p.distance(q) < 1e-9);
}
#[test]
fn zooming_holds_the_point_it_is_given() {
let vp = Viewport::new(137.0, -42.0, 1.4);
let at = Point::new(400.0, 300.0);
for zoom in [0.2, 0.5, 1.0, 1.7, 3.0, 12.0] {
let before = vp.screen_to_flow(at);
let after = vp.zoom_about(zoom, at, 0.2, 3.0).screen_to_flow(at);
assert!(
before.distance(after) < 1e-9,
"zoom {zoom} moved the canvas under the pointer",
);
}
}
#[test]
fn zoom_stays_within_its_limits() {
let vp = Viewport::new(137.0, -42.0, 1.4);
let at = Point::ZERO;
assert_eq!(vp.zoom_about(99.0, at, 0.2, 3.0).zoom, 3.0);
assert_eq!(vp.zoom_about(0.0, at, 0.2, 3.0).zoom, 0.2);
assert_eq!(vp.zoom_about(f64::NAN, at, 0.2, 3.0), vp);
assert_eq!(vp.zoom_about(f64::INFINITY, at, 0.2, 3.0), vp);
}
#[test]
fn a_viewport_from_outside_is_only_trusted_when_it_makes_sense() {
assert!(Viewport::new(137.0, -42.0, 1.4).is_sane(0.2, 3.0));
assert!(Viewport::default().is_sane(0.2, 3.0));
for broken in [
Viewport::new(f64::NAN, 0.0, 1.0),
Viewport::new(0.0, f64::INFINITY, 1.0),
Viewport::new(0.0, 0.0, 0.0),
Viewport::new(0.0, 0.0, 6.0),
Viewport::new(0.0, 0.0, f64::NAN),
] {
assert!(!broken.is_sane(0.2, 3.0), "{broken:?}");
}
}
#[test]
fn fitting_puts_the_whole_drawing_inside_the_clear_area() {
let safe = Rect::new(32.0, 32.0, 1216.0, 560.0);
for drawing in [
Rect::new(-400.0, -300.0, 900.0, 600.0),
Rect::new(0.0, 0.0, 60.0, 40.0),
Rect::new(1000.0, 1000.0, 4000.0, 200.0),
] {
let fitted = Viewport::fit(Some(drawing), safe, 0.2, 1.35);
let top_left = fitted.flow_to_screen(drawing.origin());
let bottom_right = fitted.flow_to_screen(Point::new(drawing.max_x(), drawing.max_y()));
let slack = 1e-6;
assert!(
top_left.x >= safe.x - slack && top_left.y >= safe.y - slack,
"{drawing:?} starts outside the clear area",
);
assert!(
bottom_right.x <= safe.max_x() + slack && bottom_right.y <= safe.max_y() + slack,
"{drawing:?} runs past the clear area",
);
assert!((0.2..=1.35).contains(&fitted.zoom));
}
}
#[test]
fn fitting_a_nonsense_drawing_still_gives_a_usable_view() {
let safe = Rect::new(0.0, 0.0, 800.0, 600.0);
for drawing in [
Rect::new(0.0, 0.0, f64::NAN, 10.0),
Rect::new(0.0, 0.0, 0.0, 0.0),
Rect::new(f64::NAN, 0.0, 10.0, 10.0),
] {
let fitted = Viewport::fit(Some(drawing), safe, 0.2, 1.35);
assert!(fitted.zoom.is_finite() && fitted.zoom > 0.0, "{drawing:?}");
}
}
#[test]
fn fitting_nothing_centres_the_origin() {
let safe = Rect::new(0.0, 0.0, 800.0, 600.0);
let fitted = Viewport::fit(None, safe, 0.2, 1.35);
assert_eq!(fitted.flow_to_screen(Point::ZERO), Point::new(400.0, 300.0));
assert_eq!(fitted.zoom, 1.0);
}
#[test]
fn a_grid_snaps_both_ways_and_survives_a_nonsense_size() {
let grid = Grid::new(12.0);
assert_eq!(grid.snap(17.0), 12.0);
assert_eq!(grid.snap(19.0), 24.0);
assert_eq!(grid.snap(-17.0), -12.0);
assert_eq!(grid.snap_up(13.0), 24.0);
assert_eq!(grid.snap_up(24.0), 24.0);
for size in [0.0, -12.0, f64::NAN, f64::INFINITY] {
assert_eq!(Grid::new(size).size(), 1.0, "size {size}");
}
}
#[test]
fn a_rectangle_between_two_corners_is_the_same_whichever_corner_comes_first() {
let a = Point::new(30.0, -10.0);
let b = Point::new(-6.0, 22.0);
assert_eq!(Rect::between(a, b), Rect::between(b, a));
assert_eq!(Rect::between(a, b), Rect::new(-6.0, -10.0, 36.0, 32.0));
}
#[test]
fn bounds_covers_every_rect_or_nothing_at_all() {
assert_eq!(Rect::bounds([]), None);
let rects = [
Rect::new(0.0, 0.0, 10.0, 10.0),
Rect::new(-5.0, 20.0, 5.0, 5.0),
];
assert_eq!(Rect::bounds(rects), Some(Rect::new(-5.0, 0.0, 15.0, 25.0)));
}
#[test]
fn distance_to_edge_is_signed() {
let r = Rect::new(0.0, 0.0, 100.0, 50.0);
assert!(r.distance_to_edge(Point::new(50.0, 25.0)) < 0.0);
assert_eq!(r.distance_to_edge(Point::new(0.0, 25.0)), 0.0);
assert!(r.distance_to_edge(Point::new(110.0, 25.0)) > 0.0);
}
#[test]
fn side_points() {
let r = Rect::new(10.0, 20.0, 100.0, 50.0);
assert_eq!(side_point(&r, Side::Top, 0.5), Point::new(60.0, 20.0));
assert_eq!(side_point(&r, Side::Bottom, 0.5), Point::new(60.0, 70.0));
assert_eq!(side_point(&r, Side::Left, 0.5), Point::new(10.0, 45.0));
assert_eq!(side_point(&r, Side::Right, 0.25), Point::new(110.0, 32.5));
}
#[test]
fn rect_union() {
let a = Rect::new(0.0, 0.0, 10.0, 10.0);
let b = Rect::new(20.0, -5.0, 10.0, 10.0);
let u = a.union(&b);
assert_eq!(u, Rect::new(0.0, -5.0, 30.0, 15.0));
}
}