Skip to main content

use_geometry/
line.rs

1use crate::{
2    error::GeometryError, orientation::signed_twice_area_2d, point::Point2, vector::Vector2,
3};
4
5/// An infinite 2D line represented by two sample points.
6#[derive(Debug, Clone, Copy, PartialEq)]
7pub struct Line2 {
8    /// The first point on the line.
9    a: Point2,
10    /// The second point on the line.
11    b: Point2,
12}
13
14impl Line2 {
15    /// Creates a line from two sample points.
16    #[must_use]
17    pub const fn new(a: Point2, b: Point2) -> Self {
18        Self { a, b }
19    }
20
21    /// Creates a validated line from two distinct sample points with finite coordinates.
22    ///
23    /// # Errors
24    ///
25    /// Returns [`GeometryError::NonFiniteComponent`] when either input point
26    /// contains a non-finite coordinate.
27    ///
28    /// Returns [`GeometryError::IdenticalPoints`] when `a == b`.
29    pub fn try_new(a: Point2, b: Point2) -> Result<Self, GeometryError> {
30        Self::try_from_points(a, b)
31    }
32
33    /// Creates a validated line from two distinct finite sample points.
34    ///
35    /// # Errors
36    ///
37    /// Returns [`GeometryError::NonFiniteComponent`] when either input point
38    /// contains a non-finite coordinate.
39    ///
40    /// Returns [`GeometryError::IdenticalPoints`] when `a == b`.
41    ///
42    /// # Examples
43    ///
44    /// ```
45    /// use use_geometry::{GeometryError, Line2, Point2};
46    ///
47    /// let line = Line2::try_from_points(Point2::new(0.0, 0.0), Point2::new(2.0, 2.0))?;
48    /// assert!(line.contains_point(Point2::new(4.0, 4.0)));
49    ///
50    /// assert!(matches!(
51    ///     Line2::try_from_points(Point2::new(1.0, 1.0), Point2::new(1.0, 1.0)),
52    ///     Err(GeometryError::IdenticalPoints)
53    /// ));
54    /// # Ok::<(), GeometryError>(())
55    /// ```
56    pub fn try_from_points(a: Point2, b: Point2) -> Result<Self, GeometryError> {
57        let a = a.validate()?;
58        let b = b.validate()?;
59
60        if a == b {
61            return Err(GeometryError::IdenticalPoints);
62        }
63
64        Ok(Self::new(a, b))
65    }
66
67    /// Creates a validated line from a point and non-zero direction vector.
68    ///
69    /// # Errors
70    ///
71    /// Returns [`GeometryError::NonFiniteComponent`] when `point` or `direction`
72    /// contains a non-finite value.
73    ///
74    /// Returns [`GeometryError::ZeroDirectionVector`] when `direction` is zero.
75    ///
76    /// # Examples
77    ///
78    /// ```
79    /// use use_geometry::{GeometryError, Line2, Point2, Vector2};
80    ///
81    /// let line = Line2::try_from_point_direction(Point2::new(1.0, 2.0), Vector2::new(3.0, 4.0))?;
82    /// assert_eq!(line.b(), Point2::new(4.0, 6.0));
83    /// # Ok::<(), GeometryError>(())
84    /// ```
85    pub fn try_from_point_direction(
86        point: Point2,
87        direction: Vector2,
88    ) -> Result<Self, GeometryError> {
89        let point = point.validate()?;
90        let direction = direction.validate()?;
91
92        if direction.length_squared() == 0.0 {
93            return Err(GeometryError::ZeroDirectionVector);
94        }
95
96        Ok(Self::new(point, point + direction))
97    }
98
99    /// Returns the first sample point on the line.
100    #[must_use]
101    pub const fn a(self) -> Point2 {
102        self.a
103    }
104
105    /// Returns the second sample point on the line.
106    #[must_use]
107    pub const fn b(self) -> Point2 {
108        self.b
109    }
110
111    /// Returns one sample point on the line.
112    #[must_use]
113    pub const fn point(self) -> Point2 {
114        self.a()
115    }
116
117    /// Returns the line direction from `a` to `b`.
118    #[must_use]
119    pub const fn direction(self) -> Vector2 {
120        Vector2::from_points(self.a(), self.b())
121    }
122
123    /// Returns `true` when `point` lies on the infinite line.
124    #[must_use]
125    pub fn contains_point(self, point: Point2) -> bool {
126        signed_twice_area_2d(self.a(), self.b(), point) == 0.0
127    }
128
129    /// Returns `true` when `point` lies within `tolerance` of the line.
130    ///
131    /// # Errors
132    ///
133    /// Returns [`GeometryError::NonFiniteTolerance`] when `tolerance` is `NaN`
134    /// or infinite.
135    ///
136    /// Returns [`GeometryError::NegativeTolerance`] when `tolerance` is negative.
137    pub fn contains_point_with_tolerance(
138        self,
139        point: Point2,
140        tolerance: f64,
141    ) -> Result<bool, GeometryError> {
142        let tolerance = GeometryError::validate_tolerance(tolerance)?;
143        let direction_length = self.direction().length();
144
145        if direction_length == 0.0 {
146            return Ok(self.a().distance_to(point) <= tolerance);
147        }
148
149        Ok(signed_twice_area_2d(self.a(), self.b(), point).abs() <= tolerance * direction_length)
150    }
151
152    /// Returns the slope, or `None` for a vertical line.
153    #[must_use]
154    pub fn slope(self) -> Option<f64> {
155        slope(self.a(), self.b())
156    }
157
158    /// Returns the slope when both line points contain only finite coordinates.
159    ///
160    /// # Errors
161    ///
162    /// Returns [`GeometryError::NonFiniteComponent`] when either point contains
163    /// a non-finite coordinate.
164    ///
165    /// # Examples
166    ///
167    /// ```
168    /// use use_geometry::{GeometryError, Line2, Point2};
169    ///
170    /// let diagonal = Line2::try_new(Point2::new(1.0, 1.0), Point2::new(3.0, 5.0))?;
171    /// let vertical = Line2::try_new(Point2::new(2.0, 1.0), Point2::new(2.0, 5.0))?;
172    ///
173    /// assert_eq!(diagonal.try_slope()?, Some(2.0));
174    /// assert_eq!(vertical.try_slope()?, None);
175    /// # Ok::<(), GeometryError>(())
176    /// ```
177    pub fn try_slope(self) -> Result<Option<f64>, GeometryError> {
178        try_slope(self.a(), self.b())
179    }
180}
181
182/// Returns the slope between two points, or `None` for a vertical line.
183#[must_use]
184pub fn slope(left: Point2, right: Point2) -> Option<f64> {
185    let delta_x = right.x() - left.x();
186    if delta_x == 0.0 {
187        None
188    } else {
189        Some((right.y() - left.y()) / delta_x)
190    }
191}
192
193/// Returns the slope between two points when both points contain only finite coordinates.
194///
195/// # Errors
196///
197/// Returns [`GeometryError::NonFiniteComponent`] when either point contains a
198/// non-finite coordinate.
199pub fn try_slope(left: Point2, right: Point2) -> Result<Option<f64>, GeometryError> {
200    let left = left.validate()?;
201    let right = right.validate()?;
202
203    Ok(slope(left, right))
204}
205
206#[cfg(test)]
207mod tests {
208    use super::{Line2, slope, try_slope};
209    use crate::{error::GeometryError, point::Point2, vector::Vector2};
210
211    #[test]
212    fn constructs_lines() {
213        let a = Point2::new(0.0, 0.0);
214        let b = Point2::new(1.0, 1.0);
215
216        assert_eq!(Line2::new(a, b).a(), a);
217        assert_eq!(Line2::new(a, b).b(), b);
218    }
219
220    #[test]
221    fn constructs_lines_with_try_new() {
222        let a = Point2::new(0.0, 0.0);
223        let b = Point2::new(1.0, 1.0);
224
225        assert_eq!(Line2::try_new(a, b), Ok(Line2::new(a, b)));
226        assert_eq!(Line2::try_from_points(a, b), Ok(Line2::new(a, b)));
227    }
228
229    #[test]
230    fn computes_direction() {
231        let line = Line2::new(Point2::new(0.0, 0.0), Point2::new(3.0, 4.0));
232
233        assert_eq!(line.direction(), Vector2::new(3.0, 4.0));
234        assert_eq!(line.point(), Point2::new(0.0, 0.0));
235    }
236
237    #[test]
238    fn computes_slope() {
239        let line = Line2::new(Point2::new(1.0, 1.0), Point2::new(3.0, 5.0));
240
241        assert_eq!(line.slope(), Some(2.0));
242        assert_eq!(slope(line.a(), line.b()), Some(2.0));
243    }
244
245    #[test]
246    fn vertical_lines_have_no_slope() {
247        let line = Line2::new(Point2::new(2.0, 1.0), Point2::new(2.0, 5.0));
248
249        assert_eq!(line.slope(), None);
250    }
251
252    #[test]
253    fn computes_try_slope_for_finite_lines() {
254        let line = Line2::new(Point2::new(1.0, 1.0), Point2::new(3.0, 5.0));
255
256        assert_eq!(line.try_slope(), Ok(Some(2.0)));
257        assert_eq!(try_slope(line.a(), line.b()), Ok(Some(2.0)));
258    }
259
260    #[test]
261    fn rejects_try_slope_for_non_finite_points() {
262        assert!(matches!(
263            try_slope(Point2::new(f64::NAN, 1.0), Point2::new(3.0, 5.0)),
264            Err(GeometryError::NonFiniteComponent {
265                type_name: "Point2",
266                component: "x",
267                value,
268            }) if value.is_nan()
269        ));
270    }
271
272    #[test]
273    fn rejects_identical_points_for_validated_lines() {
274        assert_eq!(
275            Line2::try_new(Point2::new(1.0, 1.0), Point2::new(1.0, 1.0)),
276            Err(GeometryError::IdenticalPoints)
277        );
278    }
279
280    #[test]
281    fn constructs_lines_from_point_and_direction() {
282        let line = Line2::try_from_point_direction(Point2::new(1.0, 2.0), Vector2::new(3.0, 4.0))
283            .expect("valid line");
284
285        assert_eq!(
286            line,
287            Line2::new(Point2::new(1.0, 2.0), Point2::new(4.0, 6.0))
288        );
289    }
290
291    #[test]
292    fn rejects_zero_direction_vectors() {
293        assert_eq!(
294            Line2::try_from_point_direction(Point2::new(1.0, 2.0), Vector2::zero()),
295            Err(GeometryError::ZeroDirectionVector)
296        );
297    }
298
299    #[test]
300    fn checks_line_containment() {
301        let line =
302            Line2::try_new(Point2::new(0.0, 0.0), Point2::new(2.0, 2.0)).expect("valid line");
303
304        assert!(line.contains_point(Point2::new(4.0, 4.0)));
305        assert!(!line.contains_point(Point2::new(4.0, 4.1)));
306        assert_eq!(
307            line.contains_point_with_tolerance(Point2::new(4.0, 4.1), 0.1),
308            Ok(true)
309        );
310    }
311}