Skip to main content

use_segment/
lib.rs

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