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}