layout/core/
geometry.rs

1//! Contains functions that are related to the geometry of shapes and their
2//! interaction. This includes things like intersection of shapes and length
3//! of vectors.
4
5// Stores a 2D coordinate, or a vector.
6#[derive(Debug, Clone, Copy, PartialEq)]
7pub struct Point {
8    pub x: f64,
9    pub y: f64,
10}
11
12impl Point {
13    pub fn zero() -> Point {
14        Self { x: 0., y: 0. }
15    }
16
17    pub fn new(x: f64, y: f64) -> Point {
18        Self { x, y }
19    }
20
21    pub fn splat(s: f64) -> Point {
22        Point::new(s, s)
23    }
24
25    pub fn neg(&self) -> Point {
26        Point::new(-self.x, -self.y)
27    }
28
29    pub fn add(&self, other: Point) -> Point {
30        Point::new(self.x + other.x, self.y + other.y)
31    }
32
33    pub fn sub(&self, other: Point) -> Point {
34        self.add(other.neg())
35    }
36
37    pub fn distance_to(&self, other: Point) -> f64 {
38        let d = self.sub(other);
39        (d.x * d.x + d.y * d.y).sqrt()
40    }
41
42    pub fn length(&self) -> f64 {
43        Point::zero().distance_to(*self)
44    }
45
46    pub fn scale(&self, s: f64) -> Point {
47        Point::new(self.x * s, self.y * s)
48    }
49
50    pub fn transpose(&self) -> Point {
51        Point::new(self.y, self.x)
52    }
53
54    pub fn rotate_around(&self, center: Point, angle: f64) -> Point {
55        let normalized = self.sub(center);
56        let rotated = normalized.rotate(angle);
57        rotated.add(center)
58    }
59    pub fn rotate(&self, angle: f64) -> Point {
60        let x = self.x;
61        let y = self.y;
62        Point::new(
63            x * angle.cos() - y * angle.sin(),
64            x * angle.sin() + y * angle.cos(),
65        )
66    }
67}
68
69impl std::fmt::Display for Point {
70    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
71        write!(f, "(x: {:.3}, y: {:.3})", self.x, self.y)
72    }
73}
74
75/// \returns the intersection point for a line with slope \p m with an ellipse
76/// with the formula. 1 = (x^2 / a^2) + (y^2 / b^2).
77/// Replace Y with the line equation and isolate x and solve to get the
78/// intersection point with the ellipse.
79/// Notice that a line has two intersection points with a circle, so users need
80/// to figure out which of the two values (+X, +Y) or (-X, -Y) is relevant.
81pub fn ellipse_line_intersection(a: f64, b: f64, m: f64) -> Point {
82    let x: f64 = ((a * a * b * b) / (b * b + a * a * m * m)).sqrt();
83    let y: f64 = m * x;
84    Point::new(x, y)
85}
86
87/// This is the implementation of get_connector_location for circle-like shapes.
88/// 'See get_connector_location' for details.
89pub fn get_connection_point_for_circle(
90    loc: Point,
91    size: Point,
92    from: Point,
93    force: f64,
94) -> (Point, Point) {
95    let loc = loc;
96    let dx = from.x - loc.x;
97    let dy = from.y - loc.y;
98
99    let a = size.x / 2.;
100    let b = size.y / 2.;
101    let m = dy / dx;
102
103    if dx == 0. {
104        let b = b * dy.signum();
105        let loc1 = Point::new(loc.x, loc.y + b);
106        return create_vector_of_length(loc1, from, force);
107    }
108
109    let mut v = ellipse_line_intersection(a, b, m);
110
111    // The intersection formula gives two solutions (for the sqrt). Figure out
112    // which solution is needed depending on the direction of the arrow (dx).
113    if dx < 0. {
114        v = v.neg();
115    }
116
117    let loc1 = loc.add(v);
118    create_vector_of_length(loc1, from, force)
119}
120
121/// Perform linear interpolation of the vectors v0 and v1, using the
122/// ratio w which is assumed to be between 0..1.
123pub fn interpolate(v0: Point, v1: Point, w: f64) -> Point {
124    v0.scale(w).add(v1.scale(1. - w))
125}
126
127/// Return the normalized vector \p v multiplied by the scalar \p s.
128pub fn normalize_scale_vector(v: Point, s: f64) -> Point {
129    let len = Point::zero().distance_to(v);
130    assert!(len > 0., "Can't normalize the unit vector");
131    v.scale(s / len)
132}
133// Returns a vector in a direction of \to target, of length \p s.
134pub fn create_vector_of_length(
135    from: Point,
136    to: Point,
137    s: f64,
138) -> (Point, Point) {
139    // We can't normalize the zero vectors, so just create a unit vector.
140    if from == to {
141        return (from, Point::new(from.x + s, from.y));
142    }
143    let t = to.sub(from);
144    let t = normalize_scale_vector(t, s);
145    (from, t.add(from))
146}
147
148/// This is the implementation of get_connector_location for box-like shapes.
149/// 'See get_connector_location' for details.
150pub fn get_connection_point_for_box(
151    loc: Point,
152    size: Point,
153    from: Point,
154    force: f64,
155) -> (Point, Point) {
156    let mut loc = loc;
157    let mut size = size;
158
159    // Try to cut the left or right part of the box, to make arrows connect to
160    // a region of the box that's closer.
161    //                                     Example:    |
162    //                                                 v
163    //                                              [------|------]
164    if from.x > loc.x + size.x / 2. {
165        // Cut the left half.
166        size.x /= 2.;
167        loc.x += size.x / 2.;
168    } else if from.x < loc.x - size.x / 2. {
169        // Cut the right half.
170        size.x /= 2.;
171        loc.x -= size.x / 2.;
172    }
173
174    let dx = loc.x - from.x;
175    let dy = loc.y - from.y;
176
177    let mut box_x = size.x / 2.;
178    let mut box_y = size.y / 2.;
179
180    // This is a vertical edge. Don't divide by zero.
181    if dx == 0. {
182        // Edge coming from the top. Connect on top.
183        if dy > 0. {
184            let loc = Point::new(loc.x, loc.y - box_y);
185            return create_vector_of_length(loc, from, force);
186        } else {
187            // Connect on the bottom.
188            let loc = Point::new(loc.x, loc.y + box_y);
189            return create_vector_of_length(loc, from, force);
190        }
191    }
192
193    let slope_from = dy / dx;
194    // How much y goes up or down as we progress along x, up to the edge.
195    let mut gain_y = box_x * slope_from;
196
197    // Need to connect from the side.
198    if gain_y.abs() < box_y {
199        if dx > 0. {
200            box_x = -box_x;
201            gain_y = -gain_y;
202        }
203
204        let con = Point::new(loc.x + box_x, loc.y + gain_y);
205        return create_vector_of_length(con, from, force);
206    }
207
208    // How much x gain as we move along y and hit the top or bottom.
209    // dx * s = y  => dx = y/s
210    let mut gain_x = box_y / slope_from;
211
212    if dy > 0. {
213        box_y = -box_y;
214        gain_x = -gain_x;
215    }
216
217    let con = Point::new(loc.x + gain_x, loc.y + box_y);
218    create_vector_of_length(con, from, force)
219}
220
221pub fn get_passthrough_path_invisible(
222    _size: Point,
223    center: Point,
224    from: Point,
225    to: Point,
226    force: f64,
227) -> (Point, Point) {
228    //  We are trying to figure out the vector that represents the bezier
229    //  control point from R to A. If R is close to A then we should to honor
230    //  the preference of A, and not B, to make sure that we don't overshoot
231    // and create an edge that wraps around. We interpolate the direction
232    // vectors in reverse proportion to make this happen.
233    //
234    //      (from)A-------->R (center)
235    //                       \
236    //                        \
237    //                         v
238    //                          B (to)
239
240    let ar = center.sub(from);
241    let rb = to.sub(center);
242
243    let a_outgoing_edge = normalize_scale_vector(ar.neg(), force);
244    let b_outgoing_edge = normalize_scale_vector(rb.neg(), force);
245
246    // If this is a self-edge then handle it in a special way. First check if
247    // the source and destination are identical. If they are then prevent the
248    // sharp-edge problem and give the middle part a bow by changing the angle
249    // by 90'.
250    let sum = a_outgoing_edge.add(b_outgoing_edge);
251    if sum.length() < 1. {
252        let edge = a_outgoing_edge.rotate(90_f64.to_radians());
253        return (center, edge.add(center));
254    }
255
256    let total = ar.length() + rb.length();
257    let mut a_ratio = ar.length() / total;
258
259    // If the edges are vertical or horizontal then make sure that they are
260    // perfectly aligned, because lines that are almost straight don't look
261    // good.
262    if center.x == to.x || center.y == to.y {
263        a_ratio = 1.;
264    } else if center.x == from.x || center.y == from.y {
265        a_ratio = 0.;
266    }
267
268    let res = interpolate(a_outgoing_edge, b_outgoing_edge, 1. - a_ratio);
269    (center, res.add(center))
270}
271
272/// Make the shape have the same X and Y values.
273pub fn make_size_square(sz: Point) -> Point {
274    let l = sz.x.max(sz.y);
275    Point::new(l, l)
276}
277
278/// Increase the size of X and Y by \p s.
279pub fn pad_shape_scalar(size: Point, s: f64) -> Point {
280    Point::new(size.x + s, size.y + s)
281}
282
283/// Estimate the bounding box of some rendered text.
284pub fn get_size_for_str(label: &str, font_size: usize) -> Point {
285    // Find the longest line.
286    let max_line_len = if !label.is_empty() {
287        label.lines().map(|x| x.chars().count()).max().unwrap()
288    } else {
289        0
290    };
291    let ts = (max_line_len.max(1), label.lines().count().max(1));
292    Point::new(ts.0 as f64, ts.1 as f64).scale(font_size as f64)
293}
294
295/// \return true if \p x is in the inclusive range P.x .. P.y.
296pub fn in_range(range: (f64, f64), x: f64) -> bool {
297    x >= range.0 && x <= range.1
298}
299
300/// trivial function for checking aproximate equality of f64, within epsion of f64
301fn approx_eq_f64(x: f64, y: f64) -> bool {
302    if x == 0. {
303        y.abs() < f64::EPSILON
304    } else if y == 0. {
305        x.abs() < f64::EPSILON
306    } else {
307        let abs_diff = (x - y).abs();
308        if abs_diff < f64::EPSILON {
309            true
310        } else {
311            abs_diff / x.abs().max(y.abs()) < f64::EPSILON
312        }
313    }
314}
315
316/// Similar to usual smaller than or equal to op, except for equal is withint f64 epsilon
317fn smaller_than_or_equal_to_f64(x: f64, y: f64) -> bool {
318    if x > y {
319        false
320    } else {
321        // stricter than usual approx_eq for f64, but works ok and stricter
322        approx_eq_f64(x, y)
323    }
324}
325
326/// \return True if the boxes (defined by the bounding box) intersect.
327pub fn do_boxes_intersect(p1: (Point, Point), p2: (Point, Point)) -> bool {
328    let overlap_x = smaller_than_or_equal_to_f64(p2.0.x, p1.1.x)
329        && smaller_than_or_equal_to_f64(p1.0.x, p2.1.x);
330    let overlap_y = smaller_than_or_equal_to_f64(p2.0.y, p1.1.y)
331        && smaller_than_or_equal_to_f64(p1.0.y, p2.1.y);
332    overlap_x && overlap_y
333}
334
335/// Return the weighted median for \p vec.
336/// This is the method that's described in
337/// "DAG - A Program that Draws Directed Graphs"
338/// Gansner, North, Vo 1989. Pg 10.
339pub fn weighted_median(vec: &[f64]) -> f64 {
340    assert!(!vec.is_empty(), "array can't be empty");
341
342    let mut vec = vec.to_vec();
343    vec.sort_by(|a, b| a.partial_cmp(b).unwrap());
344
345    if vec.len() == 1 {
346        return vec[0];
347    }
348
349    if vec.len() == 2 {
350        return (vec[0] + vec[1]) / 2.;
351    }
352    let mid = vec.len() / 2;
353
354    if vec.len() % 2 == 1 {
355        return vec[mid];
356    }
357
358    (vec[mid] + vec[mid - 1]) / 2.
359}
360
361/// Represents the size, location and centerpoint of a shape. We align shapes
362/// along their center points, and have edges directed at the center. Shapes
363/// like Box and Circle have their center point in the middle, but labels have
364/// their center point in one of the sides to make sure that edges don't
365/// obscure the text. The halo is the gap around the shape where nothing can be
366/// placed and it is applied symmetrically to the sides.
367///
368/// This struct has fields that represent the following points:
369///   ____________________
370///  |    _____________   |
371///  |  |             |   |
372///  |  |             |   |
373///  |  |      M <----|---|--the middle of the shape, in absolute coordinates.
374///  |  |         C <-|---|--the center point, saved as delta, relative to M.
375///  |  |_____________|   |
376///  |                ^---|--- the size of the shape.
377///  |____________________| <----- size + halo.
378///
379#[derive(Debug, Clone, Copy)]
380pub struct Position {
381    middle: Point, // The middle of the shape, in absolute coordinates.
382    size: Point,   // Height and width of the shape.
383    center: Point, // Delta from the middle point.
384    halo: Point,   // The boundary around the shape, applied symmetrically.
385}
386
387impl Position {
388    pub fn new(middle: Point, size: Point, center: Point, halo: Point) -> Self {
389        Self {
390            middle,
391            size,
392            center,
393            halo,
394        }
395    }
396
397    pub fn distance_to_left(&self, with_halo: bool) -> f64 {
398        self.center().x - self.bbox(with_halo).0.x
399    }
400    pub fn distance_to_right(&self, with_halo: bool) -> f64 {
401        self.bbox(with_halo).1.x - self.center().x
402    }
403    pub fn left(&self, with_halo: bool) -> f64 {
404        self.bbox(with_halo).0.x
405    }
406    pub fn right(&self, with_halo: bool) -> f64 {
407        self.bbox(with_halo).1.x
408    }
409    pub fn top(&self, with_halo: bool) -> f64 {
410        self.bbox(with_halo).0.y
411    }
412    pub fn bottom(&self, with_halo: bool) -> f64 {
413        self.bbox(with_halo).1.y
414    }
415    // Returns the bounding box of the shape.
416    // Include the size of the halo, if \p with_halo is set.
417    pub fn bbox(&self, with_halo: bool) -> (Point, Point) {
418        let size = self.size(with_halo);
419        let top_left = self.middle.sub(size.scale(0.5));
420        let bottom_right = top_left.add(size);
421        (top_left, bottom_right)
422    }
423
424    /// Returns the center of the shape in absolute coordinates.
425    pub fn center(&self) -> Point {
426        self.middle.add(self.center)
427    }
428
429    /// Returns the middle of the shape. (not center!)
430    pub fn middle(&self) -> Point {
431        self.middle
432    }
433
434    pub fn size(&self, with_halo: bool) -> Point {
435        if with_halo {
436            self.size.add(self.halo)
437        } else {
438            self.size
439        }
440    }
441
442    /// \return True if the box fits within the x ranges of \p range.
443    pub fn in_x_range(&self, range: (f64, f64), with_halo: bool) -> bool {
444        self.left(with_halo) >= range.0 && self.right(with_halo) <= range.1
445    }
446
447    pub fn set_size(&mut self, size: Point) {
448        self.size = size;
449    }
450
451    /// Update the center point for the shape. This is expressed as the delta
452    /// from the center of mass (middle-point).
453    pub fn set_new_center_point(&mut self, center: Point) {
454        self.center = center;
455        assert!(self.center.x.abs() < self.size.x);
456        assert!(self.center.y.abs() < self.size.y);
457    }
458
459    // Move the shape to a new location. The coordinate \p p is the absolute
460    // coordinates for new center of the shape.
461    pub fn move_to(&mut self, p: Point) {
462        let delta = p.sub(self.center());
463        self.middle = self.middle.add(delta);
464    }
465
466    pub fn align_to_top(&mut self, y: f64) {
467        self.middle.y = y + self.size.y / 2. + self.halo.y / 2.
468    }
469    pub fn align_to_left(&mut self, x: f64) {
470        self.middle.x = x + self.size.x / 2. + self.halo.x / 2.
471    }
472    pub fn align_to_right(&mut self, x: f64) {
473        self.middle.x = x - self.size.x / 2. - self.halo.x / 2.;
474    }
475    // Move the shape in the direction of \p d.
476    pub fn translate(&mut self, d: Point) {
477        self.middle = self.middle.add(d);
478    }
479
480    /// Align the shape to the line \p x, to the right or the left, depending on
481    ///  \p to_left.
482    pub fn align_x(&mut self, x: f64, to_left: bool) {
483        let half_box = self.size.x / 2. + self.halo.x / 2.;
484        if to_left {
485            self.middle.x = x + half_box;
486        } else {
487            self.middle.x = x - half_box;
488        }
489    }
490
491    // Align the center of the shape to \p x.
492    pub fn set_x(&mut self, x: f64) {
493        self.middle.x = x - self.center.x;
494    }
495
496    // Align the center of the shape to \p y.
497    pub fn set_y(&mut self, y: f64) {
498        self.middle.y = y - self.center.y;
499    }
500
501    pub fn transpose(&mut self) {
502        self.middle = self.middle.transpose();
503        self.size = self.size.transpose();
504        self.center = self.center.transpose();
505        self.halo = self.halo.transpose();
506    }
507}
508
509/// \return True if the segment intersects the rect.
510pub fn segment_rect_intersection(
511    seg: (Point, Point),
512    rect: (Point, Point),
513) -> bool {
514    // Check that the rect is normalized.
515    assert!(rect.0.x <= rect.1.x);
516    assert!(rect.0.y <= rect.1.y);
517
518    // Check the case of vertical segment:
519    if seg.0.x == seg.1.x {
520        return seg.1.x >= rect.0.x && seg.1.x <= rect.1.x;
521    }
522
523    // Check if the lives are outside of the x range.
524    let above = seg.0.x < rect.0.x && seg.1.x < rect.0.x;
525    let below = seg.0.x > rect.1.x && seg.1.x > rect.1.x;
526    if above || below {
527        return false;
528    }
529
530    // Check if the lives are outside of the y range.
531    let above = seg.0.y < rect.0.y && seg.1.y < rect.0.y;
532    let below = seg.0.y > rect.1.y && seg.1.y > rect.1.y;
533    if above || below {
534        return false;
535    }
536
537    // Find the intersection point with the edge of the box.
538    //    | o
539    //    |/
540    //    o  <----- y
541    //   /|
542    //  / |
543    // o  x
544    let dx = seg.1.x - seg.0.x; // Can't be zero.
545    let dy = seg.1.y - seg.0.y;
546    let a = dy / dx;
547    // y = a x + b
548    // b = y - a * x;
549    let b = seg.0.y - a * seg.0.x;
550
551    // Intersect the segment with the two vertical lines of the box.
552    let y0 = a * rect.0.x + b;
553    let y1 = a * rect.1.x + b;
554
555    // There is no intersection if both hits are on the same side of the box.
556    let above = y0 < rect.0.y && y1 < rect.0.y;
557    let below = y0 > rect.1.y && y1 > rect.1.y;
558    !(above || below)
559}
560
561#[test]
562fn segment_rect_intersection_test() {
563    // Check intersection:
564    let v0 = (
565        Point::new(-48., -27.),
566        Point::new(-196., -55.),
567        Point::new(-50., -50.),
568        Point::new(50., 50.),
569    );
570    let v1 = (
571        Point::new(-70., -156.),
572        Point::new(57., 41.),
573        Point::new(-50., -50.),
574        Point::new(50., 50.),
575    );
576    let v2 = (
577        Point::new(70., -11.),
578        Point::new(-20., -119.),
579        Point::new(-50., -50.),
580        Point::new(50., 50.),
581    );
582    assert!(segment_rect_intersection((v0.0, v0.1), (v0.2, v0.3)));
583    assert!(segment_rect_intersection((v1.0, v1.1), (v1.2, v1.3)));
584    assert!(segment_rect_intersection((v2.0, v2.1), (v2.2, v2.3)));
585
586    // Check no intersection:
587    let v0 = (
588        Point::new(190., -55.),
589        Point::new(173., 199.),
590        Point::new(-50., -50.),
591        Point::new(50., 50.),
592    );
593    let v1 = (
594        Point::new(142., -19.),
595        Point::new(-108., -133.),
596        Point::new(-50., -50.),
597        Point::new(50., 50.),
598    );
599    let v2 = (
600        Point::new(151., 80.),
601        Point::new(17., 124.),
602        Point::new(-50., -50.),
603        Point::new(50., 50.),
604    );
605    assert!(!segment_rect_intersection((v0.0, v0.1), (v0.2, v0.3)));
606    assert!(!segment_rect_intersection((v1.0, v1.1), (v1.2, v1.3)));
607    assert!(!segment_rect_intersection((v2.0, v2.1), (v2.2, v2.3)));
608}