1use std::ops::{Add, Div, Mul, Sub};
4
5pub type Id = String;
7
8#[derive(Clone, Copy, PartialEq, Debug, Default)]
10#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
11pub struct Point {
12 pub x: f64,
13 pub y: f64,
14}
15
16impl Point {
17 pub const ZERO: Point = Point { x: 0.0, y: 0.0 };
18
19 pub const fn new(x: f64, y: f64) -> Self {
20 Self { x, y }
21 }
22
23 pub fn distance_sq(&self, other: Point) -> f64 {
24 let d = *self - other;
25 d.x * d.x + d.y * d.y
26 }
27
28 pub fn distance(&self, other: Point) -> f64 {
29 self.distance_sq(other).sqrt()
30 }
31
32 pub fn lerp(&self, other: Point, t: f64) -> Point {
33 Point::new(
34 self.x + (other.x - self.x) * t,
35 self.y + (other.y - self.y) * t,
36 )
37 }
38}
39
40impl From<(f64, f64)> for Point {
41 fn from((x, y): (f64, f64)) -> Self {
42 Point::new(x, y)
43 }
44}
45
46impl Add for Point {
47 type Output = Point;
48 fn add(self, rhs: Point) -> Point {
49 Point::new(self.x + rhs.x, self.y + rhs.y)
50 }
51}
52
53impl Sub for Point {
54 type Output = Point;
55 fn sub(self, rhs: Point) -> Point {
56 Point::new(self.x - rhs.x, self.y - rhs.y)
57 }
58}
59
60impl Mul<f64> for Point {
61 type Output = Point;
62 fn mul(self, rhs: f64) -> Point {
63 Point::new(self.x * rhs, self.y * rhs)
64 }
65}
66
67impl Div<f64> for Point {
68 type Output = Point;
69 fn div(self, rhs: f64) -> Point {
70 Point::new(self.x / rhs, self.y / rhs)
71 }
72}
73
74#[derive(Clone, Copy, PartialEq, Debug, Default)]
76pub struct Size {
77 pub width: f64,
78 pub height: f64,
79}
80
81impl Size {
82 pub fn new(width: f64, height: f64) -> Self {
83 Self { width, height }
84 }
85}
86
87impl From<(f64, f64)> for Size {
88 fn from((width, height): (f64, f64)) -> Self {
89 Size::new(width, height)
90 }
91}
92
93#[derive(Clone, Copy, PartialEq, Debug, Default)]
95#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
96pub struct Rect {
97 pub x: f64,
98 pub y: f64,
99 pub width: f64,
100 pub height: f64,
101}
102
103impl Rect {
104 pub const ZERO: Rect = Rect {
105 x: 0.0,
106 y: 0.0,
107 width: 0.0,
108 height: 0.0,
109 };
110
111 pub const fn new(x: f64, y: f64, width: f64, height: f64) -> Self {
112 Self {
113 x,
114 y,
115 width,
116 height,
117 }
118 }
119
120 pub fn from_points(origin: Point, size: Size) -> Self {
121 Self::new(origin.x, origin.y, size.width, size.height)
122 }
123
124 pub fn between(a: Point, b: Point) -> Self {
127 Self {
128 x: a.x.min(b.x),
129 y: a.y.min(b.y),
130 width: (a.x - b.x).abs(),
131 height: (a.y - b.y).abs(),
132 }
133 }
134
135 pub fn origin(&self) -> Point {
136 Point::new(self.x, self.y)
137 }
138
139 pub fn size(&self) -> Size {
140 Size::new(self.width, self.height)
141 }
142
143 pub fn center(&self) -> Point {
144 Point::new(self.x + self.width / 2.0, self.y + self.height / 2.0)
145 }
146
147 pub fn max_x(&self) -> f64 {
148 self.x + self.width
149 }
150
151 pub fn max_y(&self) -> f64 {
152 self.y + self.height
153 }
154
155 pub fn union(&self, other: &Rect) -> Rect {
156 let x = self.x.min(other.x);
157 let y = self.y.min(other.y);
158 Rect::new(
159 x,
160 y,
161 self.max_x().max(other.max_x()) - x,
162 self.max_y().max(other.max_y()) - y,
163 )
164 }
165
166 pub fn contains(&self, p: Point) -> bool {
167 p.x >= self.x && p.x <= self.max_x() && p.y >= self.y && p.y <= self.max_y()
168 }
169
170 pub fn intersects(&self, other: Rect) -> bool {
172 self.x <= other.max_x()
173 && self.max_x() >= other.x
174 && self.y <= other.max_y()
175 && self.max_y() >= other.y
176 }
177
178 pub fn expanded(&self, by: f64) -> Rect {
180 Rect::new(
181 self.x - by,
182 self.y - by,
183 self.width + by * 2.0,
184 self.height + by * 2.0,
185 )
186 }
187
188 pub fn distance_to_edge(&self, point: Point) -> f64 {
195 let dx = (self.x - point.x).max(point.x - self.max_x());
196 let dy = (self.y - point.y).max(point.y - self.max_y());
197 if dx > 0.0 || dy > 0.0 {
198 dx.max(0.0).hypot(dy.max(0.0))
199 } else {
200 dx.max(dy)
201 }
202 }
203
204 pub fn bounds(rects: impl IntoIterator<Item = Self>) -> Option<Self> {
206 let mut min_x = f64::INFINITY;
207 let mut min_y = f64::INFINITY;
208 let mut max_x = f64::NEG_INFINITY;
209 let mut max_y = f64::NEG_INFINITY;
210 for rect in rects {
211 min_x = min_x.min(rect.x);
212 min_y = min_y.min(rect.y);
213 max_x = max_x.max(rect.max_x());
214 max_y = max_y.max(rect.max_y());
215 }
216 min_x
217 .is_finite()
218 .then_some(Self::new(min_x, min_y, max_x - min_x, max_y - min_y))
219 }
220}
221
222#[derive(Clone, Copy, Debug, PartialEq)]
226pub struct Grid {
227 size: f64,
228}
229
230impl Grid {
231 pub const fn new(size: f64) -> Self {
234 Self {
235 size: if size.is_finite() && size > 0.0 {
236 size
237 } else {
238 1.0
239 },
240 }
241 }
242
243 pub const fn size(self) -> f64 {
244 self.size
245 }
246
247 pub fn snap(self, value: f64) -> f64 {
248 (value / self.size).round() * self.size
249 }
250
251 pub fn snap_up(self, value: f64) -> f64 {
254 (value / self.size).ceil() * self.size
255 }
256
257 pub fn snap_point(self, point: Point) -> Point {
258 Point::new(self.snap(point.x), self.snap(point.y))
259 }
260
261 pub fn snap_rect(self, rect: Rect) -> Rect {
262 Rect::new(
263 self.snap(rect.x),
264 self.snap(rect.y),
265 self.snap(rect.width),
266 self.snap(rect.height),
267 )
268 }
269}
270
271#[derive(Clone, Copy, PartialEq, Debug)]
278#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
279pub struct Viewport {
280 pub x: f64,
282 pub y: f64,
284 pub zoom: f64,
286}
287
288impl Default for Viewport {
289 fn default() -> Self {
290 Self {
291 x: 0.0,
292 y: 0.0,
293 zoom: 1.0,
294 }
295 }
296}
297
298impl Viewport {
299 pub const fn new(x: f64, y: f64, zoom: f64) -> Self {
300 Self { x, y, zoom }
301 }
302
303 pub const fn offset(&self) -> Point {
305 Point::new(self.x, self.y)
306 }
307
308 pub const fn with_offset(self, offset: Point) -> Self {
310 Self {
311 x: offset.x,
312 y: offset.y,
313 zoom: self.zoom,
314 }
315 }
316
317 pub fn screen_to_flow(&self, p: Point) -> Point {
319 (p - self.offset()) / self.zoom
320 }
321
322 pub fn flow_to_screen(&self, p: Point) -> Point {
324 p * self.zoom + self.offset()
325 }
326
327 pub fn zoom_about(self, zoom: f64, screen: Point, min_zoom: f64, max_zoom: f64) -> Self {
333 if !zoom.is_finite() {
334 return self;
335 }
336 let zoom = zoom.clamp(min_zoom, max_zoom);
337 let flow = self.screen_to_flow(screen);
338 Self {
339 x: screen.x - flow.x * zoom,
340 y: screen.y - flow.y * zoom,
341 zoom,
342 }
343 }
344
345 pub fn panned(self, by: Point) -> Self {
347 Self {
348 x: self.x + by.x,
349 y: self.y + by.y,
350 ..self
351 }
352 }
353
354 pub fn is_sane(self, min_zoom: f64, max_zoom: f64) -> bool {
358 self.x.is_finite() && self.y.is_finite() && (min_zoom..=max_zoom).contains(&self.zoom)
359 }
360
361 pub fn fit(drawing: Option<Rect>, safe: Rect, min_zoom: f64, max_zoom: f64) -> Self {
368 let Some(drawing) = drawing else {
369 return Self {
370 x: safe.x + safe.width / 2.0,
371 y: safe.y + safe.height / 2.0,
372 zoom: 1.0,
373 };
374 };
375 let width = drawing.width.max(1.0);
376 let height = drawing.height.max(1.0);
377 let wanted = (safe.width / width).min(safe.height / height);
378 let zoom = if wanted.is_finite() {
379 wanted.clamp(min_zoom, max_zoom)
380 } else {
381 1.0
382 };
383 Self {
384 x: safe.x + (safe.width - width * zoom) / 2.0 - drawing.x * zoom,
385 y: safe.y + (safe.height - height * zoom) / 2.0 - drawing.y * zoom,
386 zoom,
387 }
388 }
389
390 pub fn lerp(&self, other: &Viewport, t: f64) -> Viewport {
391 Viewport {
392 x: self.x + (other.x - self.x) * t,
393 y: self.y + (other.y - self.y) * t,
394 zoom: self.zoom + (other.zoom - self.zoom) * t,
395 }
396 }
397}
398
399#[derive(Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd, Debug)]
406#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
407#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
408pub enum Side {
409 Top,
410 Right,
411 Bottom,
412 Left,
413}
414
415impl Side {
416 pub fn normal(&self) -> Point {
418 match self {
419 Side::Top => Point::new(0.0, -1.0),
420 Side::Right => Point::new(1.0, 0.0),
421 Side::Bottom => Point::new(0.0, 1.0),
422 Side::Left => Point::new(-1.0, 0.0),
423 }
424 }
425
426 pub fn opposite(&self) -> Side {
427 match self {
428 Side::Top => Side::Bottom,
429 Side::Right => Side::Left,
430 Side::Bottom => Side::Top,
431 Side::Left => Side::Right,
432 }
433 }
434
435 pub fn is_horizontal(&self) -> bool {
436 matches!(self, Side::Left | Side::Right)
437 }
438
439 pub(crate) fn class_name(&self) -> &'static str {
440 match self {
441 Side::Top => "top",
442 Side::Right => "right",
443 Side::Bottom => "bottom",
444 Side::Left => "left",
445 }
446 }
447}
448
449pub fn side_point(rect: &Rect, side: Side, frac: f64) -> Point {
451 match side {
452 Side::Top => Point::new(rect.x + rect.width * frac, rect.y),
453 Side::Bottom => Point::new(rect.x + rect.width * frac, rect.max_y()),
454 Side::Left => Point::new(rect.x, rect.y + rect.height * frac),
455 Side::Right => Point::new(rect.max_x(), rect.y + rect.height * frac),
456 }
457}
458
459pub(crate) const DEFAULT_NODE_SIZE: Size = Size {
461 width: 150.0,
462 height: 40.0,
463};
464
465#[derive(Clone, PartialEq, Debug)]
469pub struct Node<T = ()> {
470 pub id: Id,
471 pub position: Point,
473 pub label: String,
475 pub data: T,
477 pub node_type: Option<String>,
481 pub source_side: Side,
484 pub target_side: Side,
486 pub draggable: bool,
487 pub selectable: bool,
488 pub selected: bool,
489 pub size: Option<Size>,
492 pub measured: Option<Size>,
494 pub class: Option<String>,
496 pub style: Option<String>,
498}
499
500impl Node<()> {
501 pub fn new(id: impl Into<Id>, label: impl Into<String>, position: impl Into<Point>) -> Self {
503 Self::with_data(id, label, position, ())
504 }
505}
506
507impl<T> Node<T> {
508 pub fn with_data(
510 id: impl Into<Id>,
511 label: impl Into<String>,
512 position: impl Into<Point>,
513 data: T,
514 ) -> Self {
515 Self {
516 id: id.into(),
517 position: position.into(),
518 label: label.into(),
519 data,
520 node_type: None,
521 source_side: Side::Bottom,
522 target_side: Side::Top,
523 draggable: true,
524 selectable: true,
525 selected: false,
526 size: None,
527 measured: None,
528 class: None,
529 style: None,
530 }
531 }
532
533 pub fn node_type(mut self, ty: impl Into<String>) -> Self {
534 self.node_type = Some(ty.into());
535 self
536 }
537
538 pub fn size(mut self, size: impl Into<Size>) -> Self {
539 self.size = Some(size.into());
540 self
541 }
542
543 pub fn class(mut self, class: impl Into<String>) -> Self {
544 self.class = Some(class.into());
545 self
546 }
547
548 pub fn style(mut self, style: impl Into<String>) -> Self {
549 self.style = Some(style.into());
550 self
551 }
552
553 pub fn draggable(mut self, draggable: bool) -> Self {
554 self.draggable = draggable;
555 self
556 }
557
558 pub fn selectable(mut self, selectable: bool) -> Self {
559 self.selectable = selectable;
560 self
561 }
562
563 pub fn sides(mut self, target: Side, source: Side) -> Self {
566 self.target_side = target;
567 self.source_side = source;
568 self
569 }
570
571 pub fn rect(&self) -> Rect {
574 let size = self.size.or(self.measured).unwrap_or(DEFAULT_NODE_SIZE);
575 Rect::from_points(self.position, size)
576 }
577}
578
579#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
581pub enum EdgeKind {
582 #[default]
584 Bezier,
585 Straight,
587 SmoothStep,
589}
590
591#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
593pub enum MarkerKind {
594 #[default]
595 ArrowClosed,
596 Arrow,
597 None,
598}
599
600#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
602pub enum AnchorMode {
603 #[default]
606 Handles,
607 Seats,
613}
614
615#[derive(Clone, PartialEq, Debug)]
617pub struct Edge {
618 pub id: Id,
619 pub source: Id,
620 pub target: Id,
621 pub source_handle: Option<Id>,
623 pub target_handle: Option<Id>,
625 pub source_seat: Option<crate::ports::PortSeat>,
628 pub target_seat: Option<crate::ports::PortSeat>,
630 pub label: Option<String>,
631 pub label_position: f64,
635 pub weight: u8,
639 pub kind: EdgeKind,
640 pub animated: bool,
642 pub selected: bool,
643 pub selectable: bool,
644 pub marker_start: MarkerKind,
645 pub marker_end: MarkerKind,
646 pub class: Option<String>,
648 pub style: Option<String>,
650}
651
652impl Edge {
653 pub fn new(source: impl Into<Id>, target: impl Into<Id>) -> Self {
655 let source = source.into();
656 let target = target.into();
657 Self {
658 id: format!("{source}->{target}"),
659 source,
660 target,
661 source_handle: None,
662 target_handle: None,
663 source_seat: None,
664 target_seat: None,
665 label: None,
666 label_position: 0.5,
667 weight: 2,
668 kind: EdgeKind::default(),
669 animated: false,
670 selected: false,
671 selectable: true,
672 marker_start: MarkerKind::None,
673 marker_end: MarkerKind::default(),
674 class: None,
675 style: None,
676 }
677 }
678
679 pub fn id(mut self, id: impl Into<Id>) -> Self {
680 self.id = id.into();
681 self
682 }
683
684 pub fn label(mut self, label: impl Into<String>) -> Self {
685 self.label = Some(label.into());
686 self
687 }
688
689 pub fn label_position(mut self, position: f64) -> Self {
691 self.label_position = position;
692 self
693 }
694
695 pub fn weight(mut self, weight: u8) -> Self {
697 self.weight = weight;
698 self
699 }
700
701 pub fn kind(mut self, kind: EdgeKind) -> Self {
702 self.kind = kind;
703 self
704 }
705
706 pub fn animated(mut self, animated: bool) -> Self {
707 self.animated = animated;
708 self
709 }
710
711 pub fn marker_start(mut self, marker: MarkerKind) -> Self {
712 self.marker_start = marker;
713 self
714 }
715
716 pub fn marker_end(mut self, marker: MarkerKind) -> Self {
717 self.marker_end = marker;
718 self
719 }
720
721 pub fn source_seat(mut self, seat: crate::ports::PortSeat) -> Self {
723 self.source_seat = Some(seat);
724 self
725 }
726
727 pub fn target_seat(mut self, seat: crate::ports::PortSeat) -> Self {
729 self.target_seat = Some(seat);
730 self
731 }
732
733 pub fn source_handle(mut self, id: impl Into<Id>) -> Self {
734 self.source_handle = Some(id.into());
735 self
736 }
737
738 pub fn target_handle(mut self, id: impl Into<Id>) -> Self {
739 self.target_handle = Some(id.into());
740 self
741 }
742
743 pub fn class(mut self, class: impl Into<String>) -> Self {
744 self.class = Some(class.into());
745 self
746 }
747
748 pub fn style(mut self, style: impl Into<String>) -> Self {
749 self.style = Some(style.into());
750 self
751 }
752}
753
754#[derive(Clone, PartialEq, Debug)]
756pub struct Connection {
757 pub source: Id,
758 pub target: Id,
759 pub source_handle: Option<Id>,
760 pub target_handle: Option<Id>,
761}
762
763#[derive(Clone, PartialEq, Debug)]
767pub struct ConnectEnd {
768 pub point: Point,
770 pub connection: Option<Connection>,
773}
774
775#[derive(Clone, PartialEq, Debug)]
779pub struct DeleteRequest {
780 pub nodes: Vec<Id>,
781 pub edges: Vec<Id>,
782}
783
784impl Connection {
785 pub fn into_edge(self) -> Edge {
787 let mut edge = Edge::new(self.source, self.target);
788 edge.source_handle = self.source_handle;
789 edge.target_handle = self.target_handle;
790 if let Some(h) = &edge.source_handle {
792 edge.id = format!("{}#{}", edge.id, h);
793 }
794 if let Some(h) = &edge.target_handle {
795 edge.id = format!("{}#{}", edge.id, h);
796 }
797 edge
798 }
799}
800
801#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
804pub enum HandleKind {
805 Source,
806 Target,
807}
808
809#[derive(Clone, PartialEq, Eq, Hash, Debug)]
811pub struct HandleKey {
812 pub node: Id,
813 pub kind: HandleKind,
814 pub id: Id,
817}
818
819#[derive(Clone, Copy, PartialEq, Debug)]
821pub struct HandleGeom {
822 pub side: Side,
823 pub offset: f64,
825}
826
827#[derive(Clone, PartialEq, Debug)]
830pub struct NodeGeom {
831 pub id: Id,
832 pub rect: Rect,
833 pub selected: bool,
834 pub source_side: Side,
835 pub target_side: Side,
836 pub measured: bool,
838}
839
840#[cfg(test)]
841mod tests {
842 use super::*;
843
844 #[test]
845 fn viewport_roundtrip() {
846 let vp = Viewport::new(13.0, -7.0, 1.7);
847 let p = Point::new(100.0, 250.0);
848 let q = vp.screen_to_flow(vp.flow_to_screen(p));
849 assert!(p.distance(q) < 1e-9);
850 }
851
852 #[test]
855 fn zooming_holds_the_point_it_is_given() {
856 let vp = Viewport::new(137.0, -42.0, 1.4);
857 let at = Point::new(400.0, 300.0);
858 for zoom in [0.2, 0.5, 1.0, 1.7, 3.0, 12.0] {
859 let before = vp.screen_to_flow(at);
860 let after = vp.zoom_about(zoom, at, 0.2, 3.0).screen_to_flow(at);
861 assert!(
862 before.distance(after) < 1e-9,
863 "zoom {zoom} moved the canvas under the pointer",
864 );
865 }
866 }
867
868 #[test]
869 fn zoom_stays_within_its_limits() {
870 let vp = Viewport::new(137.0, -42.0, 1.4);
871 let at = Point::ZERO;
872 assert_eq!(vp.zoom_about(99.0, at, 0.2, 3.0).zoom, 3.0);
873 assert_eq!(vp.zoom_about(0.0, at, 0.2, 3.0).zoom, 0.2);
874 assert_eq!(vp.zoom_about(f64::NAN, at, 0.2, 3.0), vp);
876 assert_eq!(vp.zoom_about(f64::INFINITY, at, 0.2, 3.0), vp);
877 }
878
879 #[test]
880 fn a_viewport_from_outside_is_only_trusted_when_it_makes_sense() {
881 assert!(Viewport::new(137.0, -42.0, 1.4).is_sane(0.2, 3.0));
882 assert!(Viewport::default().is_sane(0.2, 3.0));
883 for broken in [
884 Viewport::new(f64::NAN, 0.0, 1.0),
885 Viewport::new(0.0, f64::INFINITY, 1.0),
886 Viewport::new(0.0, 0.0, 0.0),
887 Viewport::new(0.0, 0.0, 6.0),
888 Viewport::new(0.0, 0.0, f64::NAN),
889 ] {
890 assert!(!broken.is_sane(0.2, 3.0), "{broken:?}");
891 }
892 }
893
894 #[test]
895 fn fitting_puts_the_whole_drawing_inside_the_clear_area() {
896 let safe = Rect::new(32.0, 32.0, 1216.0, 560.0);
897 for drawing in [
898 Rect::new(-400.0, -300.0, 900.0, 600.0),
899 Rect::new(0.0, 0.0, 60.0, 40.0),
900 Rect::new(1000.0, 1000.0, 4000.0, 200.0),
901 ] {
902 let fitted = Viewport::fit(Some(drawing), safe, 0.2, 1.35);
903 let top_left = fitted.flow_to_screen(drawing.origin());
904 let bottom_right = fitted.flow_to_screen(Point::new(drawing.max_x(), drawing.max_y()));
905 let slack = 1e-6;
906 assert!(
907 top_left.x >= safe.x - slack && top_left.y >= safe.y - slack,
908 "{drawing:?} starts outside the clear area",
909 );
910 assert!(
911 bottom_right.x <= safe.max_x() + slack && bottom_right.y <= safe.max_y() + slack,
912 "{drawing:?} runs past the clear area",
913 );
914 assert!((0.2..=1.35).contains(&fitted.zoom));
915 }
916 }
917
918 #[test]
919 fn fitting_a_nonsense_drawing_still_gives_a_usable_view() {
920 let safe = Rect::new(0.0, 0.0, 800.0, 600.0);
921 for drawing in [
922 Rect::new(0.0, 0.0, f64::NAN, 10.0),
923 Rect::new(0.0, 0.0, 0.0, 0.0),
924 Rect::new(f64::NAN, 0.0, 10.0, 10.0),
925 ] {
926 let fitted = Viewport::fit(Some(drawing), safe, 0.2, 1.35);
927 assert!(fitted.zoom.is_finite() && fitted.zoom > 0.0, "{drawing:?}");
928 }
929 }
930
931 #[test]
932 fn fitting_nothing_centres_the_origin() {
933 let safe = Rect::new(0.0, 0.0, 800.0, 600.0);
934 let fitted = Viewport::fit(None, safe, 0.2, 1.35);
935 assert_eq!(fitted.flow_to_screen(Point::ZERO), Point::new(400.0, 300.0));
936 assert_eq!(fitted.zoom, 1.0);
937 }
938
939 #[test]
940 fn a_grid_snaps_both_ways_and_survives_a_nonsense_size() {
941 let grid = Grid::new(12.0);
942 assert_eq!(grid.snap(17.0), 12.0);
943 assert_eq!(grid.snap(19.0), 24.0);
944 assert_eq!(grid.snap(-17.0), -12.0);
945 assert_eq!(grid.snap_up(13.0), 24.0);
946 assert_eq!(grid.snap_up(24.0), 24.0);
947 for size in [0.0, -12.0, f64::NAN, f64::INFINITY] {
948 assert_eq!(Grid::new(size).size(), 1.0, "size {size}");
949 }
950 }
951
952 #[test]
953 fn a_rectangle_between_two_corners_is_the_same_whichever_corner_comes_first() {
954 let a = Point::new(30.0, -10.0);
955 let b = Point::new(-6.0, 22.0);
956 assert_eq!(Rect::between(a, b), Rect::between(b, a));
957 assert_eq!(Rect::between(a, b), Rect::new(-6.0, -10.0, 36.0, 32.0));
958 }
959
960 #[test]
961 fn bounds_covers_every_rect_or_nothing_at_all() {
962 assert_eq!(Rect::bounds([]), None);
963 let rects = [
964 Rect::new(0.0, 0.0, 10.0, 10.0),
965 Rect::new(-5.0, 20.0, 5.0, 5.0),
966 ];
967 assert_eq!(Rect::bounds(rects), Some(Rect::new(-5.0, 0.0, 15.0, 25.0)));
968 }
969
970 #[test]
971 fn distance_to_edge_is_signed() {
972 let r = Rect::new(0.0, 0.0, 100.0, 50.0);
973 assert!(r.distance_to_edge(Point::new(50.0, 25.0)) < 0.0);
974 assert_eq!(r.distance_to_edge(Point::new(0.0, 25.0)), 0.0);
975 assert!(r.distance_to_edge(Point::new(110.0, 25.0)) > 0.0);
976 }
977
978 #[test]
979 fn side_points() {
980 let r = Rect::new(10.0, 20.0, 100.0, 50.0);
981 assert_eq!(side_point(&r, Side::Top, 0.5), Point::new(60.0, 20.0));
982 assert_eq!(side_point(&r, Side::Bottom, 0.5), Point::new(60.0, 70.0));
983 assert_eq!(side_point(&r, Side::Left, 0.5), Point::new(10.0, 45.0));
984 assert_eq!(side_point(&r, Side::Right, 0.25), Point::new(110.0, 32.5));
985 }
986
987 #[test]
988 fn rect_union() {
989 let a = Rect::new(0.0, 0.0, 10.0, 10.0);
990 let b = Rect::new(20.0, -5.0, 10.0, 10.0);
991 let u = a.union(&b);
992 assert_eq!(u, Rect::new(0.0, -5.0, 30.0, 15.0));
993 }
994}