Skip to main content

irox_geometry/
line.rs

1// SPDX-License-Identifier: MIT
2// Copyright 2025 IROX Contributors
3//
4
5use crate::geometry::{Centroid, Geometry};
6use crate::rectangle::Rectangle;
7use crate::{Point, Point2D, Vector, Vector2D};
8use irox_tools::FloatIsh;
9
10#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Default)]
11pub struct LineSegment<T: FloatIsh> {
12    pub start: Point<T>,
13    pub end: Point<T>,
14}
15
16impl<T: FloatIsh> LineSegment<T> {
17    pub fn slope(&self) -> Option<T> {
18        let dx = self.end.x - self.start.x;
19        if dx == T::ZERO {
20            return None;
21        }
22        Some((self.end.y - self.start.y) / dx)
23    }
24
25    pub fn intercept(&self) -> T {
26        let Some(slope) = self.slope() else {
27            return self.start.x;
28        };
29        self.start.y - slope * self.start.x
30    }
31
32    pub fn intersect(&self, other: &Self) -> Option<Point<T>> {
33        let m1 = self.slope();
34        let m2 = other.slope();
35        let b1 = self.intercept();
36        let b2 = other.intercept();
37
38        match (m1, m2) {
39            (Some(m1), Some(m2)) => {
40                let x = (b2 - b1) / (m2 - m1);
41                let y = m1 * x + b1;
42                Some(Point::new_point(x, y))
43            }
44            (Some(m1), None) => {
45                let x = other.start.x;
46                let y = m1 * x + b1;
47                Some(Point::new_point(x, y))
48            }
49            (None, Some(m2)) => {
50                let x = self.start.x;
51                let y = m2 * x + b1;
52                Some(Point::new_point(x, y))
53            }
54            (None, None) => None,
55        }
56    }
57
58    pub fn is_clockwise(&self, point: &Point<T>) -> bool {
59        let dx = self.end.x - self.start.x;
60        let dy = self.end.y - self.start.y;
61        let px = point.x - self.start.x;
62        let py = point.y - self.start.y;
63        (dx * py - dy * px) <= T::ZERO
64    }
65
66    pub fn length(&self) -> T {
67        let dx = self.end.x - self.start.x;
68        let dy = self.end.y - self.start.y;
69        (dx * dx + dy * dy).sqrt()
70    }
71
72    pub fn point_along_length(&self, pct: T) -> Point<T> {
73        let dx = self.end.x - self.start.x;
74        let dy = self.end.y - self.start.y;
75        let proj = Vector::new(dx * pct, dy * pct);
76        self.start + proj
77    }
78
79    pub fn distance_to(&self, point: &Point<T>) -> T {
80        let dx = self.end.x - self.start.x;
81        let dy = self.end.y - self.start.y;
82        let px = point.x - self.start.x;
83        let py = point.y - self.start.y;
84        let len = self.length();
85        let pct = ((px * dx) + (py * dy)) / (len * len);
86        let pct = pct.clamp(T::ZERO, T::ONE);
87        let point_on_segment = self.point_along_length(pct);
88        let v = *point - point_on_segment;
89        v.magnitude()
90    }
91}
92
93impl<T: FloatIsh> Centroid<T> for LineSegment<T> {
94    fn centroid(&self) -> Point<T> {
95        self.point_along_length(T::from_f64(0.5))
96    }
97}
98
99impl<T: FloatIsh> Geometry<T> for LineSegment<T> {
100    fn contains(&self, _point: &Point<T>) -> bool {
101        todo!()
102    }
103
104    fn distance_to(&self, point: &Point<T>) -> T {
105        LineSegment::distance_to(self, point)
106    }
107
108    fn intersects(&self, _point: &Point<T>) -> bool {
109        todo!()
110    }
111
112    fn bounding_rectangle(&self) -> Rectangle<T> {
113        todo!()
114    }
115}
116
117#[cfg(test)]
118mod tests {
119    use crate::{LineSegment, Point, Point2D};
120    use irox_tools::assert_eq_eps;
121
122    #[test]
123    pub fn test1() {
124        let line = LineSegment {
125            start: Point::new_point(5., 5.),
126            end: Point::new_point(5., 20.),
127        };
128        assert_eq!(None, line.slope());
129        assert!(line.is_clockwise(&Point::new_point(5., 10.)));
130    }
131    #[test]
132    pub fn test2() {
133        let line = LineSegment {
134            start: Point::new_point(1., 1.),
135            end: Point::new_point(10., 10.),
136        };
137        assert_eq!(Some(1.), line.slope());
138        assert!(!line.is_clockwise(&Point::new_point(5., 10.)));
139        assert!(line.is_clockwise(&Point::new_point(5., 5.)));
140        assert!(line.is_clockwise(&Point::new_point(10., 5.)));
141    }
142
143    #[test]
144    pub fn test_distance() {
145        let line = LineSegment {
146            start: Point::new_point(50., 80.),
147            end: Point::new_point(50., -800.),
148        };
149        let d = line.distance_to(&Point::new_point(20., 1000.));
150        assert_eq_eps!(920.4890004280828f64, d, f64::EPSILON);
151
152        let line = LineSegment {
153            start: Point::new_point(0., 0.),
154            end: Point::new_point(10., 0.),
155        };
156        let pnt = line.point_along_length(0.5);
157        assert_eq!(pnt, Point::new_point(5.0, 0.0));
158        let d = line.distance_to(&Point::new_point(0., 10.));
159        assert_eq_eps!(10f64, d, 1e-13);
160        let d = line.distance_to(&Point::new_point(5., 10.));
161        assert_eq_eps!(10f64, d, 1e-13);
162        let d = line.distance_to(&Point::new_point(10., 10.));
163        assert_eq_eps!(10f64, d, 1e-13);
164        let d = line.distance_to(&Point::new_point(10., -10.));
165        assert_eq_eps!(10f64, d, 1e-13);
166        let d = line.distance_to(&Point::new_point(5., -10.));
167        assert_eq_eps!(10f64, d, 1e-13);
168        let d = line.distance_to(&Point::new_point(0., -10.));
169        assert_eq_eps!(10f64, d, 1e-13);
170        let d = line.distance_to(&Point::new_point(-10., 0.));
171        assert_eq_eps!(10f64, d, 1e-13);
172        let d = line.distance_to(&Point::new_point(20., 0.));
173        assert_eq_eps!(10f64, d, 1e-13);
174    }
175}