iron_shapes/
edge_rational.rs

1// Copyright (c) 2018-2020 Thomas Kramer.
2// SPDX-FileCopyrightText: 2018-2022 Thomas Kramer
3//
4// SPDX-License-Identifier: AGPL-3.0-or-later
5
6//! Edge intersection functions for rational coordinates.
7//!
8//! Some computations on edges can be performed exactly when the coordinates
9//! have rational type. For example the intersection of two edges with rational coordinates
10//! can be expressed exactly as a point with rational coordinates.
11//!
12//! This modules contains implementations of such computations that take edges with rational coordinates
13//! and produce an output in rational coordinates.
14//!
15
16use crate::point::Point;
17
18use num_rational::Ratio;
19use num_traits::Zero;
20
21use std::cmp::Ordering;
22
23pub use crate::edge::{Edge, EdgeIntersection, LineIntersection};
24use crate::traits::BoundingBox;
25use crate::CoordinateType;
26
27impl<T: CoordinateType + num_integer::Integer> Edge<Ratio<T>> {
28    /// Compute the intersection point of the lines defined by the two edges.
29    ///
30    /// Degenerate lines don't intersect by definition.
31    ///
32    /// Returns `LineIntersection::None` iff the two lines don't intersect.
33    /// Returns `LineIntersection::Collinear` iff both lines are equal.
34    /// Returns `LineIntersection::Point(p,(a,b,c))` iff the lines intersect in exactly one point `p`.
35    /// `f` is a value such that `self.start + self.vector()*a/c == p` and
36    /// `other.start + other.vector()*b/c == p`.
37    ///
38    /// # Examples
39    ///
40    /// ```
41    /// extern crate num_rational;
42    /// use num_rational::Ratio;
43    /// use iron_shapes::point::Point;
44    /// use iron_shapes::edge_rational::*;
45    ///
46    /// let r = |i| Ratio::from_integer(i);
47    ///
48    /// let e1 = Edge::new((r(0), r(0)), (r(2), r(2)));
49    /// let e2 = Edge::new((r(0), r(2)), (r(2), r(0)));
50    ///
51    /// assert_eq!(e1.line_intersection_rational(e2),
52    ///     LineIntersection::Point(Point::new(r(1), r(1)), (r(4), r(4), r(8))));
53    ///
54    /// ```
55    pub fn line_intersection_rational(
56        &self,
57        other: Edge<Ratio<T>>,
58    ) -> LineIntersection<Ratio<T>, Ratio<T>> {
59        if self.is_degenerate() || other.is_degenerate() {
60            LineIntersection::None
61        } else {
62            // TODO: faster implementation if both lines are orthogonal
63
64            let ab = self.vector();
65            let cd = other.vector();
66
67            // Assert that the vectors have a non-zero length. This should already be the case
68            // because the degenerate cases are handled before.
69            debug_assert!(ab.norm2_squared() > Ratio::zero());
70            debug_assert!(cd.norm2_squared() > Ratio::zero());
71
72            let s = ab.cross_prod(cd);
73
74            // TODO: What if approximate zero due to rounding error?
75            if s.is_zero() {
76                // Lines are parallel
77                debug_assert!(self.is_parallel(&other));
78
79                // TODO: check more efficiently for collinear lines.
80                if self.line_contains_point(other.start) {
81                    // If the line defined by `self` contains at least one point of `other` then they are equal.
82                    debug_assert!(self.is_collinear(&other));
83                    LineIntersection::Collinear
84                } else {
85                    LineIntersection::None
86                }
87            } else {
88                let ac = other.start - self.start;
89                let ac_cross_cd = ac.cross_prod(cd);
90                let i = ac_cross_cd / s;
91
92                let p: Point<Ratio<T>> = self.start + ab * i;
93
94                let ca_cross_ab = ac.cross_prod(ab);
95
96                // Check that the intersection point lies on the lines indeed.
97                // TODO: uncomment this checks
98                //                debug_assert!(self.cast().line_contains_point_approx(p, 1e-4));
99                //                debug_assert!(other.cast().line_contains_point_approx(p, 1e-2));
100
101                // debug_assert!({
102                //     let j = ca_cross_ab / s;
103                //     let p2: Point<Ratio<T>> = other.start + cd * j;
104                //     (p - p2).norm2_squared() < Ratio::new(T::one(), 1_000_000)
105                // });
106
107                let positions = if s < Ratio::zero() {
108                    (
109                        Ratio::zero() - ac_cross_cd,
110                        Ratio::zero() - ca_cross_ab,
111                        Ratio::zero() - s,
112                    )
113                } else {
114                    (ac_cross_cd, ca_cross_ab, s)
115                };
116
117                LineIntersection::Point(p, positions)
118            }
119        }
120    }
121
122    /// Compute the intersection with another edge.
123    pub fn edge_intersection_rational(
124        &self,
125        other: &Edge<Ratio<T>>,
126    ) -> EdgeIntersection<Ratio<T>, Ratio<T>, Edge<Ratio<T>>> {
127        //        debug_assert!(tolerance >= Ratio::zero(), "Tolerance cannot be negative.");
128
129        // Swap direction of other edge such that both have the same direction.
130        let other = if (self.start < self.end) != (other.start < other.end) {
131            other.reversed()
132        } else {
133            *other
134        };
135
136        // Check endpoints for coincidence.
137        // This must be handled separately because equality of the intersection point and endpoints
138        // will not necessarily be detected due to rounding errors.
139        let same_start_start = self.start == other.start;
140        let same_start_end = self.start == other.end;
141
142        let same_end_start = self.end == other.start;
143        let same_end_end = self.end == other.end;
144
145        // TODO: optimize for chained edges (start1 == end2 ^ start2 == end1)
146
147        // Are the edges equal but not degenerate?
148        let fully_coincident =
149            (same_start_start & same_end_end) ^ (same_start_end & same_end_start);
150
151        let result = if self.is_degenerate() {
152            // First degenerate case
153            if other.contains_point(self.start).inclusive_bounds() {
154                EdgeIntersection::EndPoint(self.start)
155            } else {
156                EdgeIntersection::None
157            }
158        } else if other.is_degenerate() {
159            // Second degenerate case
160            if self.contains_point(other.start).inclusive_bounds() {
161                EdgeIntersection::EndPoint(other.start)
162            } else {
163                EdgeIntersection::None
164            }
165        } else if fully_coincident {
166            EdgeIntersection::Overlap(*self)
167        } else if !self.bounding_box().touches(&other.bounding_box()) {
168            // If bounding boxes do not touch, then intersection is impossible.
169            EdgeIntersection::None
170        } else {
171            // Compute the intersection of the lines defined by the two edges.
172            let line_intersection = self.line_intersection_rational(other);
173
174            // Then check if the intersection point is on both edges
175            // or find the intersection if the edges overlap.
176            match line_intersection {
177                LineIntersection::None => EdgeIntersection::None,
178
179                // Intersection in one point:
180                LineIntersection::Point(p, (pos1, pos2, len)) => {
181                    if pos1 >= Ratio::zero() && pos1 <= len && pos2 >= Ratio::zero() && pos2 <= len
182                    {
183                        if pos1 == Ratio::zero()
184                            || pos1 == len
185                            || pos2 == Ratio::zero()
186                            || pos2 == len
187                        {
188                            EdgeIntersection::EndPoint(p)
189                        } else {
190                            EdgeIntersection::Point(p)
191                        }
192                    } else {
193                        EdgeIntersection::None
194                    }
195                }
196                LineIntersection::Collinear => {
197                    debug_assert!(self.is_collinear(&other));
198
199                    // Project all points of the two edges on the line defined by the first edge
200                    // (scaled by the length of the first edge).
201                    // This allows to calculate the interval of overlap in one dimension.
202
203                    let (pa, pb) = self.into();
204                    let (pc, pd) = other.into();
205
206                    let b = pb - pa;
207                    let c = pc - pa;
208                    let d = pd - pa;
209
210                    let dist_a = Ratio::zero();
211                    let dist_b = b.dot(b);
212
213                    let dist_c = b.dot(c);
214                    let dist_d = b.dot(d);
215
216                    let start1 = (dist_a, pa);
217                    let end1 = (dist_b, pb);
218
219                    // Sort end points of other edge.
220                    let (start2, end2) = if dist_c < dist_d {
221                        ((dist_c, pc), (dist_d, pd))
222                    } else {
223                        ((dist_d, pd), (dist_c, pc))
224                    };
225
226                    // Find maximum by distance.
227                    let start = if start1.0 < start2.0 { start2 } else { start1 };
228
229                    // Find minimum by distance.
230                    let end = if end1.0 < end2.0 { end1 } else { end2 };
231
232                    // Check if the edges overlap in more than one point, in exactly one point or
233                    // in zero points.
234                    match start.0.cmp(&end.0) {
235                        Ordering::Less => EdgeIntersection::Overlap(Edge::new(start.1, end.1)),
236                        Ordering::Equal => EdgeIntersection::EndPoint(start.1),
237                        Ordering::Greater => EdgeIntersection::None,
238                    }
239                }
240            }
241        };
242
243        // Sanity check for the result.
244        debug_assert!({
245            match result {
246                EdgeIntersection::Point(p) => {
247                    self.contains_point(p).is_within_bounds()
248                        && other.contains_point(p).is_within_bounds()
249                }
250                EdgeIntersection::EndPoint(p) => {
251                    self.contains_point(p).on_bounds() || other.contains_point(p).on_bounds()
252                }
253                EdgeIntersection::None => self.edges_intersect(&other).is_no(),
254                EdgeIntersection::Overlap(_) => true,
255            }
256        });
257
258        // Check that the result is consistent with the edge intersection test.
259
260        debug_assert_eq!(
261            result == EdgeIntersection::None,
262            self.edges_intersect(&other).is_no()
263        );
264
265        result
266    }
267}
268
269#[cfg(test)]
270mod tests {
271    use super::*;
272    use crate::traits::Scale;
273    use num_rational::Rational64;
274
275    #[test]
276    fn test_rational_edge() {
277        let e = Edge::new(
278            (Rational64::from(0), Rational64::from(0)),
279            (Rational64::from(1), Rational64::from(1)),
280        );
281        let v = e.vector();
282        assert!(v.norm2_squared() == Rational64::from(2));
283        assert!(!e.is_degenerate());
284    }
285
286    #[test]
287    fn test_line_intersection_rational() {
288        // Helper constructors.
289        let rp = |a: i64, b: i64| Point::new(Rational64::from(a), Rational64::from(b));
290        let r = Rational64::new;
291        let i = Rational64::from;
292
293        let e1 = Edge::new(rp(0, 0), rp(2, 2));
294        let e2 = Edge::new(rp(1, 0), rp(0, 1));
295        let e3 = Edge::new(rp(1, 0), rp(3, 2));
296
297        assert_eq!(
298            e1.line_intersection_rational(e2),
299            LineIntersection::Point(Point::new(r(1, 2), r(1, 2)), (i(1), i(2), i(4)))
300        );
301
302        // Parallel lines should not intersect
303        assert_eq!(e1.line_intersection_rational(e3), LineIntersection::None);
304
305        let e4 = Edge::new(rp(-320, 2394), rp(94, -4482));
306        let e5 = Edge::new(rp(71, 133), rp(-1373, 13847));
307
308        if let LineIntersection::Point(intersection, _) = e4.line_intersection_rational(e5) {
309            let a = e4.vector();
310            let b = intersection - e4.start;
311            let area = b.cross_prod(a);
312            assert!(area.is_zero());
313
314            let a = e5.vector();
315            let b = intersection - e5.start;
316            let area = b.cross_prod(a);
317            assert!(area.is_zero());
318        } else {
319            assert!(false);
320        }
321
322        // Collinear lines.
323        let scale = Rational64::new(1, 3);
324        let e1 = Edge::new(rp(0, 0), rp(2, 2)).scale(scale);
325        let e2 = Edge::new(rp(4, 4), rp(8, 8)).scale(scale);
326        assert!(!e1.is_coincident(&e2));
327        assert!(e1.is_parallel(&e2));
328        assert_eq!(
329            e1.line_intersection_rational(e2),
330            LineIntersection::Collinear
331        );
332    }
333
334    #[test]
335    fn test_edge_intersection_rational() {}
336}