Skip to main content

use_geometry/
segment.rs

1use crate::{aabb::Aabb2, error::GeometryError, point::Point2, vector::Vector2};
2
3/// A finite line segment between two 2D points.
4#[derive(Debug, Clone, Copy, PartialEq)]
5pub struct Segment2 {
6    /// The segment start point.
7    start: Point2,
8    /// The segment end point.
9    end: Point2,
10}
11
12impl Segment2 {
13    /// Creates a segment from `start` to `end`.
14    #[must_use]
15    pub const fn new(start: Point2, end: Point2) -> Self {
16        Self { start, end }
17    }
18
19    /// Creates a segment from `start` to `end` with finite point coordinates.
20    ///
21    /// # Errors
22    ///
23    /// Returns [`GeometryError::NonFiniteComponent`] when either input point
24    /// contains a non-finite coordinate.
25    ///
26    /// # Examples
27    ///
28    /// ```
29    /// use use_geometry::{GeometryError, Point2, Segment2};
30    ///
31    /// let segment = Segment2::try_new(Point2::new(0.0, 0.0), Point2::new(4.0, 2.0))?;
32    /// assert_eq!(segment.midpoint(), Point2::new(2.0, 1.0));
33    /// # Ok::<(), GeometryError>(())
34    /// ```
35    pub fn try_new(start: Point2, end: Point2) -> Result<Self, GeometryError> {
36        Ok(Self::new(start.validate()?, end.validate()?))
37    }
38
39    /// Returns the segment start point.
40    #[must_use]
41    pub const fn start(self) -> Point2 {
42        self.start
43    }
44
45    /// Returns the segment end point.
46    #[must_use]
47    pub const fn end(self) -> Point2 {
48        self.end
49    }
50
51    /// Returns the segment length.
52    #[must_use]
53    pub fn length(self) -> f64 {
54        self.start.distance_to(self.end)
55    }
56
57    /// Returns the squared segment length.
58    #[must_use]
59    pub fn length_squared(self) -> f64 {
60        self.start.distance_squared_to(self.end)
61    }
62
63    /// Returns the segment midpoint.
64    #[must_use]
65    pub const fn midpoint(self) -> Point2 {
66        self.start.midpoint(self.end)
67    }
68
69    /// Returns the segment vector from `start` to `end`.
70    #[must_use]
71    pub const fn vector(self) -> Vector2 {
72        Vector2::from_points(self.start, self.end)
73    }
74
75    /// Returns the point at parameter `t` along the segment.
76    ///
77    /// # Examples
78    ///
79    /// ```
80    /// use use_geometry::{Point2, Segment2};
81    ///
82    /// let segment = Segment2::new(Point2::new(0.0, 0.0), Point2::new(4.0, 2.0));
83    /// assert_eq!(segment.point_at(0.25), Point2::new(1.0, 0.5));
84    /// ```
85    #[must_use]
86    pub const fn point_at(self, t: f64) -> Point2 {
87        self.start.lerp(self.end, t)
88    }
89
90    /// Returns the segment with its endpoints reversed.
91    ///
92    /// # Examples
93    ///
94    /// ```
95    /// use use_geometry::{Point2, Segment2};
96    ///
97    /// let segment = Segment2::new(Point2::new(1.0, 2.0), Point2::new(4.0, 6.0));
98    ///
99    /// assert_eq!(segment.reversed().start(), Point2::new(4.0, 6.0));
100    /// assert_eq!(segment.reversed().end(), Point2::new(1.0, 2.0));
101    /// ```
102    #[must_use]
103    pub const fn reversed(self) -> Self {
104        Self::new(self.end, self.start)
105    }
106
107    /// Returns `true` when the segment collapses to a single point.
108    #[must_use]
109    pub fn is_degenerate(self) -> bool {
110        self.length_squared() == 0.0
111    }
112
113    /// Returns `true` when the segment length is within `tolerance` of zero.
114    ///
115    /// # Errors
116    ///
117    /// Returns [`GeometryError::NonFiniteTolerance`] when `tolerance` is `NaN`
118    /// or infinite.
119    ///
120    /// Returns [`GeometryError::NegativeTolerance`] when `tolerance` is negative.
121    ///
122    /// # Examples
123    ///
124    /// ```
125    /// use use_geometry::{GeometryError, Point2, Segment2};
126    ///
127    /// let segment = Segment2::new(Point2::new(2.0, 2.0), Point2::new(2.0, 2.0));
128    /// assert!(segment.is_degenerate_with_tolerance(0.0)?);
129    /// # Ok::<(), GeometryError>(())
130    /// ```
131    pub fn is_degenerate_with_tolerance(self, tolerance: f64) -> Result<bool, GeometryError> {
132        let tolerance = GeometryError::validate_tolerance(tolerance)?;
133
134        Ok(self.length_squared() <= tolerance * tolerance)
135    }
136
137    /// Returns the segment bounding box.
138    #[must_use]
139    pub const fn aabb(self) -> Aabb2 {
140        Aabb2::from_points(self.start, self.end)
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use super::Segment2;
147    use crate::{error::GeometryError, point::Point2, vector::Vector2};
148
149    fn approx_eq(left: f64, right: f64) -> bool {
150        (left - right).abs() < 1.0e-10
151    }
152
153    #[test]
154    fn constructs_segments() {
155        let start = Point2::new(0.0, 0.0);
156        let end = Point2::new(1.0, 1.0);
157
158        assert_eq!(Segment2::new(start, end).start(), start);
159        assert_eq!(Segment2::new(start, end).end(), end);
160    }
161
162    #[test]
163    fn constructs_segments_with_try_new() {
164        let start = Point2::new(0.0, 0.0);
165        let end = Point2::new(1.0, 1.0);
166
167        assert_eq!(Segment2::try_new(start, end), Ok(Segment2::new(start, end)));
168    }
169
170    #[test]
171    fn rejects_non_finite_segment_points() {
172        assert_eq!(
173            Segment2::try_new(Point2::new(0.0, 0.0), Point2::new(1.0, f64::INFINITY)),
174            Err(GeometryError::NonFiniteComponent {
175                type_name: "Point2",
176                component: "y",
177                value: f64::INFINITY,
178            })
179        );
180    }
181
182    #[test]
183    fn computes_length() {
184        let segment = Segment2::new(Point2::new(0.0, 0.0), Point2::new(3.0, 4.0));
185
186        assert!(approx_eq(segment.length(), 5.0));
187        assert!(approx_eq(segment.length_squared(), 25.0));
188    }
189
190    #[test]
191    fn computes_midpoint() {
192        let segment = Segment2::new(Point2::new(0.0, 0.0), Point2::new(4.0, 2.0));
193
194        assert_eq!(segment.midpoint(), Point2::new(2.0, 1.0));
195        assert_eq!(segment.point_at(0.25), Point2::new(1.0, 0.5));
196    }
197
198    #[test]
199    fn computes_vector() {
200        let segment = Segment2::new(Point2::new(1.0, 2.0), Point2::new(4.0, 6.0));
201
202        assert_eq!(segment.vector(), Vector2::new(3.0, 4.0));
203        assert_eq!(segment.start(), Point2::new(1.0, 2.0));
204        assert_eq!(segment.end(), Point2::new(4.0, 6.0));
205        assert_eq!(
206            segment.reversed(),
207            Segment2::new(Point2::new(4.0, 6.0), Point2::new(1.0, 2.0))
208        );
209    }
210
211    #[test]
212    fn detects_degenerate_segments() {
213        let segment = Segment2::new(Point2::new(2.0, 2.0), Point2::new(2.0, 2.0));
214
215        assert!(segment.is_degenerate());
216        assert_eq!(segment.is_degenerate_with_tolerance(0.0), Ok(true));
217        assert_eq!(
218            segment.is_degenerate_with_tolerance(-1.0),
219            Err(GeometryError::NegativeTolerance(-1.0))
220        );
221    }
222
223    #[test]
224    fn computes_segment_bounds() {
225        let segment = Segment2::new(Point2::new(4.0, 1.0), Point2::new(1.0, 3.0));
226
227        assert_eq!(segment.aabb().min(), Point2::new(1.0, 1.0));
228        assert_eq!(segment.aabb().max(), Point2::new(4.0, 3.0));
229    }
230}