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/// \return True if the boxes (defined by the bounding box) intersect.
301pub fn do_boxes_intersect(p1: (Point, Point), p2: (Point, Point)) -> bool {
302    let overlap_x = p2.0.x < p1.1.x && p1.0.x < p2.1.x;
303    let overlap_y = p2.0.y < p1.1.y && p1.0.y < p2.1.y;
304    overlap_x && overlap_y
305}
306
307/// Return the weighted median for \p vec.
308/// This is the method that's described in
309/// "DAG - A Program that Draws Directed Graphs"
310/// Gansner, North, Vo 1989. Pg 10.
311pub fn weighted_median(vec: &[f64]) -> f64 {
312    assert!(!vec.is_empty(), "array can't be empty");
313
314    let mut vec = vec.to_vec();
315    vec.sort_by(|a, b| a.partial_cmp(b).unwrap());
316
317    if vec.len() == 1 {
318        return vec[0];
319    }
320
321    if vec.len() == 2 {
322        return (vec[0] + vec[1]) / 2.;
323    }
324    let mid = vec.len() / 2;
325
326    if vec.len() % 2 == 1 {
327        return vec[mid];
328    }
329
330    (vec[mid] + vec[mid - 1]) / 2.
331}
332
333/// Represents the size, location and centerpoint of a shape. We align shapes
334/// along their center points, and have edges directed at the center. Shapes
335/// like Box and Circle have their center point in the middle, but labels have
336/// their center point in one of the sides to make sure that edges don't
337/// obscure the text. The halo is the gap around the shape where nothing can be
338/// placed and it is applied symmetrically to the sides.
339///
340/// This struct has fields that represent the following points:
341///   ____________________
342///  |    _____________   |
343///  |  |             |   |
344///  |  |             |   |
345///  |  |      M <----|---|--the middle of the shape, in absolute coordinates.
346///  |  |         C <-|---|--the center point, saved as delta, relative to M.
347///  |  |_____________|   |
348///  |                ^---|--- the size of the shape.
349///  |____________________| <----- size + halo.
350///
351#[derive(Debug, Clone, Copy)]
352pub struct Position {
353    middle: Point, // The middle of the shape, in absolute coordinates.
354    size: Point,   // Height and width of the shape.
355    center: Point, // Delta from the middle point.
356    halo: Point,   // The boundary around the shape, applied symmetrically.
357}
358
359impl Position {
360    pub fn new(middle: Point, size: Point, center: Point, halo: Point) -> Self {
361        Self {
362            middle,
363            size,
364            center,
365            halo,
366        }
367    }
368
369    pub fn distance_to_left(&self, with_halo: bool) -> f64 {
370        self.center().x - self.bbox(with_halo).0.x
371    }
372    pub fn distance_to_right(&self, with_halo: bool) -> f64 {
373        self.bbox(with_halo).1.x - self.center().x
374    }
375    pub fn left(&self, with_halo: bool) -> f64 {
376        self.bbox(with_halo).0.x
377    }
378    pub fn right(&self, with_halo: bool) -> f64 {
379        self.bbox(with_halo).1.x
380    }
381    pub fn top(&self, with_halo: bool) -> f64 {
382        self.bbox(with_halo).0.y
383    }
384    pub fn bottom(&self, with_halo: bool) -> f64 {
385        self.bbox(with_halo).1.y
386    }
387    // Returns the bounding box of the shape.
388    // Include the size of the halo, if \p with_halo is set.
389    pub fn bbox(&self, with_halo: bool) -> (Point, Point) {
390        let size = self.size(with_halo);
391        let top_left = self.middle.sub(size.scale(0.5));
392        let bottom_right = top_left.add(size);
393        (top_left, bottom_right)
394    }
395
396    /// Returns the center of the shape in absolute coordinates.
397    pub fn center(&self) -> Point {
398        self.middle.add(self.center)
399    }
400
401    /// Returns the middle of the shape. (not center!)
402    pub fn middle(&self) -> Point {
403        self.middle
404    }
405
406    pub fn size(&self, with_halo: bool) -> Point {
407        if with_halo {
408            self.size.add(self.halo)
409        } else {
410            self.size
411        }
412    }
413
414    /// \return True if the box fits within the x ranges of \p range.
415    pub fn in_x_range(&self, range: (f64, f64), with_halo: bool) -> bool {
416        self.left(with_halo) >= range.0 && self.right(with_halo) <= range.1
417    }
418
419    pub fn set_size(&mut self, size: Point) {
420        self.size = size;
421    }
422
423    /// Update the center point for the shape. This is expressed as the delta
424    /// from the center of mass (middle-point).
425    pub fn set_new_center_point(&mut self, center: Point) {
426        self.center = center;
427        assert!(self.center.x.abs() < self.size.x);
428        assert!(self.center.y.abs() < self.size.y);
429    }
430
431    // Move the shape to a new location. The coordinate \p p is the absolute
432    // coordinates for new center of the shape.
433    pub fn move_to(&mut self, p: Point) {
434        let delta = p.sub(self.center());
435        self.middle = self.middle.add(delta);
436    }
437
438    pub fn align_to_top(&mut self, y: f64) {
439        self.middle.y = y + self.size.y / 2. + self.halo.y / 2.
440    }
441    pub fn align_to_left(&mut self, x: f64) {
442        self.middle.x = x + self.size.x / 2. + self.halo.x / 2.
443    }
444    pub fn align_to_right(&mut self, x: f64) {
445        self.middle.x = x - self.size.x / 2. - self.halo.x / 2.;
446    }
447    // Move the shape in the direction of \p d.
448    pub fn translate(&mut self, d: Point) {
449        self.middle = self.middle.add(d);
450    }
451
452    /// Align the shape to the line \p x, to the right or the left, depending on
453    ///  \p to_left.
454    pub fn align_x(&mut self, x: f64, to_left: bool) {
455        let half_box = self.size.x / 2. + self.halo.x / 2.;
456        if to_left {
457            self.middle.x = x + half_box;
458        } else {
459            self.middle.x = x - half_box;
460        }
461    }
462
463    // Align the center of the shape to \p x.
464    pub fn set_x(&mut self, x: f64) {
465        self.middle.x = x - self.center.x;
466    }
467
468    // Align the center of the shape to \p y.
469    pub fn set_y(&mut self, y: f64) {
470        self.middle.y = y - self.center.y;
471    }
472
473    pub fn transpose(&mut self) {
474        self.middle = self.middle.transpose();
475        self.size = self.size.transpose();
476        self.center = self.center.transpose();
477        self.halo = self.halo.transpose();
478    }
479}
480
481/// \return True if the segment intersects the rect.
482pub fn segment_rect_intersection(
483    seg: (Point, Point),
484    rect: (Point, Point),
485) -> bool {
486    // Check that the rect is normalized.
487    assert!(rect.0.x <= rect.1.x);
488    assert!(rect.0.y <= rect.1.y);
489
490    // Check the case of vertical segment:
491    if seg.0.x == seg.1.x {
492        return seg.1.x >= rect.0.x && seg.1.x <= rect.1.x;
493    }
494
495    // Check if the lives are outside of the x range.
496    let above = seg.0.x < rect.0.x && seg.1.x < rect.0.x;
497    let below = seg.0.x > rect.1.x && seg.1.x > rect.1.x;
498    if above || below {
499        return false;
500    }
501
502    // Check if the lives are outside of the y range.
503    let above = seg.0.y < rect.0.y && seg.1.y < rect.0.y;
504    let below = seg.0.y > rect.1.y && seg.1.y > rect.1.y;
505    if above || below {
506        return false;
507    }
508
509    // Find the intersection point with the edge of the box.
510    //    | o
511    //    |/
512    //    o  <----- y
513    //   /|
514    //  / |
515    // o  x
516    let dx = seg.1.x - seg.0.x; // Can't be zero.
517    let dy = seg.1.y - seg.0.y;
518    let a = dy / dx;
519    // y = a x + b
520    // b = y - a * x;
521    let b = seg.0.y - a * seg.0.x;
522
523    // Intersect the segment with the two vertical lines of the box.
524    let y0 = a * rect.0.x + b;
525    let y1 = a * rect.1.x + b;
526
527    // There is no intersection if both hits are on the same side of the box.
528    let above = y0 < rect.0.y && y1 < rect.0.y;
529    let below = y0 > rect.1.y && y1 > rect.1.y;
530    !(above || below)
531}
532
533#[test]
534fn segment_rect_intersection_test() {
535    // Check intersection:
536    let v0 = (
537        Point::new(-48., -27.),
538        Point::new(-196., -55.),
539        Point::new(-50., -50.),
540        Point::new(50., 50.),
541    );
542    let v1 = (
543        Point::new(-70., -156.),
544        Point::new(57., 41.),
545        Point::new(-50., -50.),
546        Point::new(50., 50.),
547    );
548    let v2 = (
549        Point::new(70., -11.),
550        Point::new(-20., -119.),
551        Point::new(-50., -50.),
552        Point::new(50., 50.),
553    );
554    assert!(segment_rect_intersection((v0.0, v0.1), (v0.2, v0.3)));
555    assert!(segment_rect_intersection((v1.0, v1.1), (v1.2, v1.3)));
556    assert!(segment_rect_intersection((v2.0, v2.1), (v2.2, v2.3)));
557
558    // Check no intersection:
559    let v0 = (
560        Point::new(190., -55.),
561        Point::new(173., 199.),
562        Point::new(-50., -50.),
563        Point::new(50., 50.),
564    );
565    let v1 = (
566        Point::new(142., -19.),
567        Point::new(-108., -133.),
568        Point::new(-50., -50.),
569        Point::new(50., 50.),
570    );
571    let v2 = (
572        Point::new(151., 80.),
573        Point::new(17., 124.),
574        Point::new(-50., -50.),
575        Point::new(50., 50.),
576    );
577    assert!(!segment_rect_intersection((v0.0, v0.1), (v0.2, v0.3)));
578    assert!(!segment_rect_intersection((v1.0, v1.1), (v1.2, v1.3)));
579    assert!(!segment_rect_intersection((v2.0, v2.1), (v2.2, v2.3)));
580}