Skip to main content

use_geometry/
point.rs

1use core::ops::{Add, Sub};
2
3use crate::{aabb::Aabb2, error::GeometryError, vector::Vector2};
4
5/// A 2D point represented with `f64` coordinates.
6#[derive(Debug, Clone, Copy, PartialEq)]
7pub struct Point2 {
8    /// The horizontal coordinate.
9    x: f64,
10    /// The vertical coordinate.
11    y: f64,
12}
13
14impl Point2 {
15    /// Creates a point from `x` and `y` coordinates.
16    #[must_use]
17    pub const fn new(x: f64, y: f64) -> Self {
18        Self { x, y }
19    }
20
21    /// Returns the horizontal coordinate.
22    #[must_use]
23    pub const fn x(&self) -> f64 {
24        self.x
25    }
26
27    /// Returns the vertical coordinate.
28    #[must_use]
29    pub const fn y(&self) -> f64 {
30        self.y
31    }
32
33    /// Creates a point from finite `x` and `y` coordinates.
34    ///
35    /// # Errors
36    ///
37    /// Returns [`GeometryError::NonFiniteComponent`] when `x` or `y` is `NaN`
38    /// or infinite.
39    ///
40    /// # Examples
41    ///
42    /// ```
43    /// use use_geometry::{GeometryError, Point2};
44    ///
45    /// let point = Point2::try_new(1.0, -2.0)?;
46    /// assert_eq!(point, Point2::new(1.0, -2.0));
47    ///
48    /// assert!(matches!(
49    ///     Point2::try_new(f64::NAN, 0.0),
50    ///     Err(GeometryError::NonFiniteComponent { component: "x", .. })
51    /// ));
52    /// # Ok::<(), GeometryError>(())
53    /// ```
54    pub const fn try_new(x: f64, y: f64) -> Result<Self, GeometryError> {
55        if !x.is_finite() {
56            return Err(GeometryError::non_finite_component("Point2", "x", x));
57        }
58
59        if !y.is_finite() {
60            return Err(GeometryError::non_finite_component("Point2", "y", y));
61        }
62
63        Ok(Self::new(x, y))
64    }
65
66    /// Validates that an existing point contains only finite coordinates.
67    ///
68    /// # Errors
69    ///
70    /// Returns [`GeometryError::NonFiniteComponent`] when `self.x` or
71    /// `self.y` is `NaN` or infinite.
72    ///
73    /// # Examples
74    ///
75    /// ```
76    /// use use_geometry::{GeometryError, Point2};
77    ///
78    /// let validated = Point2::new(3.0, 4.0).validate()?;
79    /// assert_eq!(validated, Point2::new(3.0, 4.0));
80    /// # Ok::<(), GeometryError>(())
81    /// ```
82    pub const fn validate(self) -> Result<Self, GeometryError> {
83        Self::try_new(self.x, self.y)
84    }
85
86    /// Returns `true` when both coordinates are finite.
87    #[must_use]
88    pub const fn is_finite(self) -> bool {
89        self.x.is_finite() && self.y.is_finite()
90    }
91
92    /// Returns the origin `(0, 0)`.
93    #[must_use]
94    pub const fn origin() -> Self {
95        Self::new(0.0, 0.0)
96    }
97
98    /// Returns the Euclidean distance to another point.
99    ///
100    /// # Examples
101    ///
102    /// ```
103    /// use use_geometry::Point2;
104    ///
105    /// let origin = Point2::new(0.0, 0.0);
106    /// let point = Point2::new(3.0, 4.0);
107    ///
108    /// assert_eq!(origin.distance_to(point), 5.0);
109    /// ```
110    #[must_use]
111    pub fn distance_to(self, other: Self) -> f64 {
112        self.distance_squared_to(other).sqrt()
113    }
114
115    /// Returns the squared Euclidean distance to another point.
116    ///
117    /// # Examples
118    ///
119    /// ```
120    /// use use_geometry::Point2;
121    ///
122    /// let left = Point2::new(0.0, 0.0);
123    /// let right = Point2::new(3.0, 4.0);
124    ///
125    /// assert_eq!(left.distance_squared_to(right), 25.0);
126    /// ```
127    #[must_use]
128    pub fn distance_squared_to(self, other: Self) -> f64 {
129        let delta_x = other.x - self.x;
130        let delta_y = other.y - self.y;
131
132        delta_x.mul_add(delta_x, delta_y * delta_y)
133    }
134
135    /// Returns the midpoint between this point and another point.
136    ///
137    /// # Examples
138    ///
139    /// ```
140    /// use use_geometry::Point2;
141    ///
142    /// let left = Point2::new(-2.0, 1.0);
143    /// let right = Point2::new(4.0, 5.0);
144    ///
145    /// assert_eq!(left.midpoint(right), Point2::new(1.0, 3.0));
146    /// ```
147    #[must_use]
148    pub const fn midpoint(self, other: Self) -> Self {
149        Self::new(self.x.midpoint(other.x), self.y.midpoint(other.y))
150    }
151
152    /// Returns a point interpolated between this point and `other`.
153    ///
154    /// # Examples
155    ///
156    /// ```
157    /// use use_geometry::Point2;
158    ///
159    /// let start = Point2::new(0.0, 0.0);
160    /// let end = Point2::new(8.0, 4.0);
161    ///
162    /// assert_eq!(start.lerp(end, 0.25), Point2::new(2.0, 1.0));
163    /// ```
164    #[must_use]
165    pub const fn lerp(self, other: Self, t: f64) -> Self {
166        Self::new(
167            self.x + ((other.x - self.x) * t),
168            self.y + ((other.y - self.y) * t),
169        )
170    }
171
172    /// Returns a point translated by a vector.
173    ///
174    /// # Examples
175    ///
176    /// ```
177    /// use use_geometry::{Point2, Vector2};
178    ///
179    /// let point = Point2::new(1.5, -2.0);
180    /// let offset = Vector2::new(2.0, 3.5);
181    ///
182    /// assert_eq!(point.translate(offset), Point2::new(3.5, 1.5));
183    /// ```
184    #[must_use]
185    pub const fn translate(self, vector: Vector2) -> Self {
186        Self::new(self.x + vector.x(), self.y + vector.y())
187    }
188
189    /// Returns the degenerate bounding box rooted at this point.
190    #[must_use]
191    pub const fn aabb(self) -> Aabb2 {
192        Aabb2::from_points(self, self)
193    }
194}
195
196impl Add<Vector2> for Point2 {
197    type Output = Self;
198
199    fn add(self, rhs: Vector2) -> Self::Output {
200        Self::new(self.x + rhs.x(), self.y + rhs.y())
201    }
202}
203
204impl Sub<Vector2> for Point2 {
205    type Output = Self;
206
207    fn sub(self, rhs: Vector2) -> Self::Output {
208        Self::new(self.x - rhs.x(), self.y - rhs.y())
209    }
210}
211
212impl Sub<Self> for Point2 {
213    type Output = Vector2;
214
215    fn sub(self, rhs: Self) -> Self::Output {
216        Vector2::new(self.x - rhs.x, self.y - rhs.y)
217    }
218}
219
220#[cfg(test)]
221mod tests {
222    use super::Point2;
223    use crate::{Aabb2, distance::midpoint_2d, error::GeometryError, vector::Vector2};
224
225    fn approx_eq(left: f64, right: f64) -> bool {
226        (left - right).abs() < 1.0e-10
227    }
228
229    #[test]
230    fn constructs_points() {
231        assert_eq!(
232            Point2::new(1.0, 2.0),
233            Point2::try_new(1.0, 2.0).expect("valid point")
234        );
235    }
236
237    #[test]
238    fn constructs_points_with_try_new() {
239        assert_eq!(Point2::try_new(1.0, 2.0), Ok(Point2::new(1.0, 2.0)));
240    }
241
242    #[test]
243    fn rejects_non_finite_x_coordinates() {
244        assert!(matches!(
245            Point2::try_new(f64::NAN, 2.0),
246            Err(GeometryError::NonFiniteComponent {
247                type_name: "Point2",
248                component: "x",
249                value,
250            }) if value.is_nan()
251        ));
252    }
253
254    #[test]
255    fn rejects_non_finite_y_coordinates() {
256        assert_eq!(
257            Point2::try_new(1.0, f64::INFINITY),
258            Err(GeometryError::NonFiniteComponent {
259                type_name: "Point2",
260                component: "y",
261                value: f64::INFINITY,
262            })
263        );
264    }
265
266    #[test]
267    fn validates_existing_points() {
268        assert_eq!(Point2::new(1.0, 2.0).validate(), Ok(Point2::new(1.0, 2.0)));
269    }
270
271    #[test]
272    fn returns_origin() {
273        assert_eq!(Point2::origin(), Point2::new(0.0, 0.0));
274    }
275
276    #[test]
277    fn computes_distance_to_other_point() {
278        let left = Point2::new(0.0, 0.0);
279        let right = Point2::new(3.0, 4.0);
280
281        assert!(approx_eq(left.distance_to(right), 5.0));
282        assert!(approx_eq(left.distance_squared_to(right), 25.0));
283    }
284
285    #[test]
286    fn computes_midpoints() {
287        let left = Point2::new(0.0, 0.0);
288        let right = Point2::new(4.0, 2.0);
289
290        assert_eq!(left.midpoint(right), Point2::new(2.0, 1.0));
291        assert_eq!(midpoint_2d(left, right), Point2::new(2.0, 1.0));
292        assert_eq!(left.lerp(right, 0.25), Point2::new(1.0, 0.5));
293    }
294
295    #[test]
296    fn translates_by_vector() {
297        let point = Point2::new(1.5, -2.0);
298        let offset = Vector2::new(2.0, 3.5);
299
300        assert_eq!(point.translate(offset), Point2::new(3.5, 1.5));
301        assert_eq!(point + offset, Point2::new(3.5, 1.5));
302        assert_eq!(point + offset - offset, point);
303    }
304
305    #[test]
306    fn exposes_accessors_and_finite_checks() {
307        let point = Point2::new(1.5, -2.0);
308
309        assert!(approx_eq(point.x(), 1.5));
310        assert!(approx_eq(point.y(), -2.0));
311        assert!(point.is_finite());
312        assert!(!Point2::new(f64::NAN, 0.0).is_finite());
313    }
314
315    #[test]
316    fn converts_points_to_vectors_and_bounds() {
317        let start = Point2::new(1.0, 2.0);
318        let end = Point2::new(4.0, 6.0);
319
320        assert_eq!(end - start, Vector2::new(3.0, 4.0));
321        assert_eq!(start.aabb(), Aabb2::from_points(start, start));
322    }
323}