fenris_geometry/primitives/
line.rs

1use crate::{ConvexPolygon, Disk, HalfPlane, Plane};
2use fenris_traits::Real;
3use nalgebra::allocator::Allocator;
4use nalgebra::{clamp, DefaultAllocator, DimName, Matrix2, OPoint, OVector, Vector2, U2, U3};
5use nalgebra::{Point2, Point3, Scalar};
6use numeric_literals::replace_float_literals;
7use std::fmt::Debug;
8
9pub type LineSegment3d<T> = LineSegment<T, U3>;
10
11impl<T: Real> LineSegment3d<T> {
12    #[allow(non_snake_case)]
13    pub fn closest_point_to_plane_parametric(&self, plane: &Plane<T>) -> T {
14        let n = plane.normal();
15        let x0 = plane.point();
16        let [a, b] = [self.start(), self.end()];
17        let d = &b.coords - &a.coords;
18        let y = &x0.coords - &a.coords;
19
20        let nTd = n.dot(&d);
21        let nTy = n.dot(&y);
22
23        // The parameter t is generally given by the equation
24        //  dot(n, d) * t = dot(n, y)
25        // but we must be careful, since dot(n, d) can get arbitrarily close to 0,
26        // which causes some challenges.
27        let t = if nTd.signum() == nTy.signum() {
28            // Sign is the same, thus t >= 0
29            if nTy.abs() >= nTd.abs() {
30                T::one()
31            } else {
32                nTy / nTd
33            }
34        } else {
35            // t must be negative, directly clamp to zero
36            T::zero()
37        };
38
39        t
40    }
41
42    pub fn closest_point_to_plane(&self, plane: &Plane<T>) -> Point3<T> {
43        let t = self.closest_point_to_plane_parametric(plane);
44        self.point_from_parameter(t)
45    }
46}
47
48#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct LineSegment<T, D>
50where
51    T: Scalar,
52    D: DimName,
53    DefaultAllocator: Allocator<T, D>,
54{
55    start: OPoint<T, D>,
56    end: OPoint<T, D>,
57}
58
59pub type LineSegment2d<T> = LineSegment<T, U2>;
60
61impl<T, D> LineSegment<T, D>
62where
63    T: Scalar,
64    D: DimName,
65    DefaultAllocator: Allocator<T, D>,
66{
67    pub fn from_end_points(start: OPoint<T, D>, end: OPoint<T, D>) -> Self {
68        Self { start, end }
69    }
70
71    pub fn start(&self) -> &OPoint<T, D> {
72        &self.start
73    }
74
75    pub fn end(&self) -> &OPoint<T, D> {
76        &self.end
77    }
78
79    pub fn reverse(&self) -> Self {
80        Self {
81            start: self.end.clone(),
82            end: self.start.clone(),
83        }
84    }
85}
86
87impl<T, D> LineSegment<T, D>
88where
89    T: Real,
90    D: DimName,
91    DefaultAllocator: Allocator<T, D>,
92{
93    pub fn to_line(&self) -> Line<T, D> {
94        let dir = &self.end - &self.start;
95        Line::from_point_and_dir(self.start.clone(), dir)
96    }
97
98    /// Returns the vector tangent to the line, pointing from start to end.
99    ///
100    /// Note that the vector is **not** normalized.
101    pub fn tangent_dir(&self) -> OVector<T, D> {
102        &self.end().coords - &self.start().coords
103    }
104
105    pub fn length(&self) -> T {
106        self.tangent_dir().norm()
107    }
108
109    pub fn midpoint(&self) -> OPoint<T, D> {
110        OPoint::from((&self.start().coords + &self.end().coords) / (T::one() + T::one()))
111    }
112
113    /// Compute the closest point on the segment to the given point, represented in
114    /// the parametric form x = a + t * (b - a).
115    pub fn closest_point_parametric(&self, point: &OPoint<T, D>) -> T {
116        let t = self.to_line().project_point_parametric(point);
117        clamp(t, T::zero(), T::one())
118    }
119
120    /// Computes the closest point on the line to the given point.
121    pub fn closest_point(&self, point: &OPoint<T, D>) -> OPoint<T, D> {
122        let t = self.closest_point_parametric(point);
123        self.point_from_parameter(t)
124    }
125
126    pub fn point_from_parameter(&self, t: T) -> OPoint<T, D> {
127        OPoint::from(&self.start().coords + self.tangent_dir() * t)
128    }
129}
130
131impl<T> LineSegment2d<T>
132where
133    T: Real,
134{
135    /// Returns a vector normal to the line segment, in the direction consistent with a
136    /// counter-clockwise winding order when the edge is part of a polygon.
137    ///
138    /// In other words, the normal direction points "to the right" when following the segment
139    /// from its first endpoint to its second.
140    ///
141    /// Note that the vector is **not** normalized.
142    pub fn normal_dir(&self) -> Vector2<T> {
143        let tangent = self.tangent_dir();
144        Vector2::new(tangent.y, -tangent.x)
145    }
146
147    pub fn intersect_line_parametric(&self, line: &Line2d<T>) -> Option<T> {
148        self.to_line()
149            .intersect_line_parametric(line)
150            .map(|(t1, _)| t1)
151    }
152
153    #[replace_float_literals(T::from_f64(literal).unwrap())]
154    pub fn intersect_disk_parametric(&self, disk: &Disk<T>) -> Option<[T; 2]> {
155        let [t1, t2] = self.to_line().intersect_disk_parametric(disk)?;
156        let t1 = clamp(t1, 0.0, 1.0);
157        let t2 = clamp(t2, 0.0, 1.0);
158        Some([t1, t2])
159    }
160
161    #[replace_float_literals(T::from_f64(literal).unwrap())]
162    pub fn intersect_disk(&self, disk: &Disk<T>) -> Option<Self> {
163        self.intersect_disk_parametric(disk)
164            .map(|[t1, t2]| self.segment_from_parameters(&t1, &t2))
165    }
166
167    pub fn segment_from_parameters(&self, t_begin: &T, t_end: &T) -> Self {
168        let begin = self.point_from_parameter(t_begin.clone());
169        let end = self.point_from_parameter(t_end.clone());
170        Self::from_end_points(begin, end)
171    }
172
173    /// Computes the intersection of two line segments (if any), but returns the result as a parameter.
174    ///
175    /// Let all points on this line segment be defined by the relation x = a + t * (b - a)
176    /// for 0 <= t <= 1. Then, if the two line segments intersect, t is returned. Otherwise,
177    /// `None` is returned.
178    pub fn intersect_segment_parametric(&self, other: &LineSegment2d<T>) -> Option<T> {
179        // Represent the two lines as:
180        //  x1 = a1 + t1 * d1
181        //  x2 = a2 + t2 * d2
182        // where di = bi - ai. This gives the linear system
183        //  [ d1  -d2 ] t = a2 - a1,
184        // where t = [t1, t2].
185
186        let d1 = &self.end - &self.start;
187        let d2 = &other.end - &other.start;
188
189        let line1 = Line2d::from_point_and_dir(self.start.clone(), d1);
190        let line2 = Line2d::from_point_and_dir(other.start.clone(), d2);
191
192        line1
193            .intersect_line_parametric(&line2)
194            .and_then(|(t1, t2)| {
195                // TODO: This may go very wrong if we're talking "exact" intersection
196                // e.g. when a line segment intersects another segment only at a point,
197                // in which case we might discard the intersection entirely.
198                // I suppose the only way to deal with this is either arbitrary precision
199                // or using epsilons? Also, keep in mind that the `from` and `to`
200                // points may already be suffering from imprecision!
201                if t2 < T::zero() || t2 > T::one() {
202                    None
203                } else if t1 < T::zero() || t1 > T::one() {
204                    None
205                } else {
206                    Some(t1)
207                }
208            })
209    }
210
211    /// Compute the intersection between the line segment and a half-plane.
212    ///
213    /// Returns `None` if the segment and the half-plane do not intersect, otherwise
214    /// returns `Some([t1, t2])` with `t1 <= t2`, and `t1` and `t2` correspond to the start and end parameters
215    /// relative to the current line segment.
216    #[replace_float_literals(T::from_f64(literal).unwrap())]
217    pub fn intersect_half_plane_parametric(&self, half_plane: &HalfPlane<T>) -> Option<[T; 2]> {
218        let contains_start = half_plane.contains_point(self.start());
219        let contains_end = half_plane.contains_point(self.end());
220
221        match (contains_start, contains_end) {
222            (true, true) => Some([0.0, 1.0]),
223            (false, false) => None,
224            (true, false) | (false, true) => {
225                let t_intersect = self
226                    .intersect_line_parametric(&half_plane.surface())
227                    // Technically the intersection should be in the interval [0, 1] already,
228                    // but numerical errors may lead to values that are slightly outside, or, in the case of
229                    // very nearly parallel lines, far outside.
230                    .map(|t| clamp(t, 0.0, 1.0));
231
232                let (t_start, t_end);
233                if contains_start {
234                    // The only case when the intersection returns None is when the half-plane line and the
235                    // line segment are parallel, which we *technically* have excluded already.
236                    // But due to floating-point imprecision we might still find ourselves in this situation.
237                    // In this case the result may be more or less arbitrary, so we pick a reasonable default
238                    // to fall back on
239                    t_start = 0.0;
240                    t_end = t_intersect.unwrap_or(1.0);
241                } else {
242                    t_start = t_intersect.unwrap_or(0.0);
243                    t_end = 1.0;
244                }
245
246                debug_assert!(t_start <= t_end);
247                Some([t_start, t_end])
248            }
249        }
250    }
251
252    #[replace_float_literals(T::from_f64(literal).unwrap())]
253    pub fn intersect_half_plane(&self, half_plane: &HalfPlane<T>) -> Option<Self> {
254        self.intersect_half_plane_parametric(half_plane)
255            .map(|[t1, t2]| self.segment_from_parameters(&t1, &t2))
256    }
257
258    pub fn intersect_polygon(&self, other: &ConvexPolygon<T>) -> Option<LineSegment2d<T>> {
259        let mut result = self.clone();
260        for half_plane in other.half_planes() {
261            result = result.intersect_half_plane(&half_plane)?;
262        }
263        Some(result)
264    }
265}
266
267impl<T: Real> LineSegment3d<T> {
268    pub fn intersect_plane_parametric(&self, plane: &Plane<T>) -> Option<T> {
269        self.to_line()
270            .intersect_plane_parametric(plane)
271            .filter(|t| t >= &T::zero() && t <= &T::one())
272    }
273}
274
275#[derive(Debug, Clone)]
276pub struct Line<T, D>
277where
278    T: Scalar,
279    D: DimName,
280    DefaultAllocator: Allocator<T, D>,
281{
282    point: OPoint<T, D>,
283    dir: OVector<T, D>,
284}
285
286pub type Line2d<T> = Line<T, U2>;
287pub type Line3d<T> = Line<T, U3>;
288
289impl<T, D> Line<T, D>
290where
291    T: Scalar,
292    D: DimName,
293    DefaultAllocator: Allocator<T, D>,
294{
295    pub fn from_point_and_dir(point: OPoint<T, D>, dir: OVector<T, D>) -> Self {
296        // TODO: Make dir Unit?
297        Self { point, dir }
298    }
299
300    pub fn point(&self) -> &OPoint<T, D> {
301        &self.point
302    }
303
304    pub fn dir(&self) -> &OVector<T, D> {
305        &self.dir
306    }
307}
308
309impl<T, D> Line<T, D>
310where
311    T: Real,
312    D: DimName,
313    DefaultAllocator: Allocator<T, D>,
314{
315    /// A normalized vector tangent to the line.
316    pub fn tangent(&self) -> OVector<T, D> {
317        self.dir.normalize()
318    }
319
320    pub fn from_point_through_point(point: OPoint<T, D>, through: &OPoint<T, D>) -> Self {
321        let dir = through - &point;
322        Self::from_point_and_dir(point, dir)
323    }
324
325    /// Computes the projection of the given point onto the line, representing the point
326    /// in parametric form.
327    pub fn project_point_parametric(&self, point: &OPoint<T, D>) -> T {
328        let d2 = self.dir.magnitude_squared();
329        if d2 == T::zero() {
330            // TODO: Is this the correct way to handle it? If the line degenerate to a point
331            // it's no longer even a line! (It is a line *segment* though ...)
332            // Line degenerates to a point, just return 0 as it will give *a* correct solution
333            T::zero()
334        } else {
335            (point - &self.point).dot(&self.dir) / d2
336        }
337    }
338
339    /// Computes the projection of the given point onto the line.
340    pub fn project_point(&self, point: &OPoint<T, D>) -> OPoint<T, D> {
341        let t = self.project_point_parametric(point);
342        self.point_from_parameter(t)
343    }
344
345    pub fn point_from_parameter(&self, t: T) -> OPoint<T, D> {
346        &self.point + &self.dir * t
347    }
348}
349
350impl<T> Line2d<T>
351where
352    T: Real,
353{
354    pub fn intersect(&self, other: &Line2d<T>) -> Option<Point2<T>> {
355        self.intersect_line_parametric(other)
356            .map(|(t1, _)| self.point_from_parameter(t1))
357    }
358
359    /// Computes the intersection of two lines, if any.
360    ///
361    /// Let all points on each line segment be defined by the relation `x1 = a1 + t1 * d1`
362    /// for `0 <= t1 <= 1`, and similarly for `t2`. Here, `t1` is the parameter associated with
363    /// `self`, and `t2` is the parameter associated with `other`.
364    pub fn intersect_line_parametric(&self, other: &Line2d<T>) -> Option<(T, T)> {
365        // Represent the two lines as:
366        //  x1 = a1 + t1 * d1
367        //  x2 = a2 + t2 * d2
368        // where di = bi - ai. This gives the linear system
369        //  [ d1  -d2 ] t = a2 - a1,
370        // where t = [t1, t2].
371
372        let rhs = &other.point - &self.point;
373        let matrix = Matrix2::from_columns(&[self.dir, -other.dir]);
374
375        // TODO: Rewrite to use LU decomposition?
376        matrix
377            .try_inverse()
378            .map(|inv| inv * rhs)
379            // Inverse returns vector, split it up into its components
380            .map(|t| (t.x, t.y))
381    }
382
383    pub fn intersect_disk(&self, disk: &Disk<T>) -> Option<LineSegment2d<T>> {
384        let [t1, t2] = self.intersect_disk_parametric(disk)?;
385        let p1 = self.point_from_parameter(t1);
386        let p2 = self.point_from_parameter(t2);
387        Some(LineSegment2d::from_end_points(p1, p2))
388    }
389
390    #[replace_float_literals(T::from_f64(literal).unwrap())]
391    pub fn intersect_disk_parametric(&self, disk: &Disk<T>) -> Option<[T; 2]> {
392        let a = self.point();
393        let d = self.dir();
394        let r = disk.radius();
395        let a0 = a - disk.center();
396
397        // The solutions are given by the solutions to the quadratic equation
398        // alpha * t^2 + beta * t + gamma = 0
399        let alpha = d.dot(&d);
400        let beta = 2.0 * d.dot(&a0);
401        let gamma = a0.dot(&a0) - r * r;
402
403        let discriminant = beta * beta - 4.0 * alpha * gamma;
404        if discriminant >= 0.0 {
405            // Non-negative discriminant means that we have two (possible identical) real solutions that correspond
406            // to intersection points
407            let disc_sqrt = discriminant.sqrt();
408            let t1 = (-beta - disc_sqrt) / (2.0 * alpha);
409            let t2 = (-beta + disc_sqrt) / (2.0 * alpha);
410            debug_assert!(t1 <= t2);
411            Some([t1, t2])
412        } else {
413            // No real solutions, so no intersection
414            None
415        }
416    }
417}
418
419impl<T> Line3d<T>
420where
421    T: Real,
422{
423    pub fn intersect_plane_parametric(&self, plane: &Plane<T>) -> Option<T> {
424        let n = plane.normal();
425        let d = self.dir();
426        let b = self.point() - plane.point();
427        let d_dot_n = d.dot(&n);
428        // TODO: This will actually *never* return a non-empty intersection in the case
429        // that the line is entirely contained in the plane. However, this is so extremely
430        // unlikely to be the case in the presence of floating-point arithmetic, that we
431        // consider it never to be the case
432        (d_dot_n != T::zero()).then(|| -b.dot(&n) / d_dot_n)
433    }
434}