Skip to main content

use_line/
lib.rs

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