Skip to main content

use_point/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::ops::{Add, Sub};
5
6use use_coordinate::GeometryError;
7use use_vector::Vector2;
8
9/// A 2D point represented with `f64` coordinates.
10#[derive(Debug, Clone, Copy, PartialEq)]
11pub struct Point2 {
12    x: f64,
13    y: f64,
14}
15
16impl Point2 {
17    /// Creates a point from `x` and `y` coordinates.
18    #[must_use]
19    pub const fn new(x: f64, y: f64) -> Self {
20        Self { x, y }
21    }
22
23    /// Returns the horizontal coordinate.
24    #[must_use]
25    pub const fn x(&self) -> f64 {
26        self.x
27    }
28
29    /// Returns the vertical coordinate.
30    #[must_use]
31    pub const fn y(&self) -> f64 {
32        self.y
33    }
34
35    /// Creates a point from finite `x` and `y` coordinates.
36    ///
37    /// # Errors
38    ///
39    /// Returns [`GeometryError::NonFiniteComponent`] when `x` or `y` is `NaN`
40    /// or infinite.
41    pub const fn try_new(x: f64, y: f64) -> Result<Self, GeometryError> {
42        if !x.is_finite() {
43            return Err(GeometryError::non_finite_component("Point2", "x", x));
44        }
45
46        if !y.is_finite() {
47            return Err(GeometryError::non_finite_component("Point2", "y", y));
48        }
49
50        Ok(Self::new(x, y))
51    }
52
53    /// Validates that an existing point contains only finite coordinates.
54    ///
55    /// # Errors
56    ///
57    /// Returns [`GeometryError::NonFiniteComponent`] when `self.x` or
58    /// `self.y` is `NaN` or infinite.
59    pub const fn validate(self) -> Result<Self, GeometryError> {
60        Self::try_new(self.x, self.y)
61    }
62
63    /// Returns `true` when both coordinates are finite.
64    #[must_use]
65    pub const fn is_finite(self) -> bool {
66        self.x.is_finite() && self.y.is_finite()
67    }
68
69    /// Returns the origin `(0, 0)`.
70    #[must_use]
71    pub const fn origin() -> Self {
72        Self::new(0.0, 0.0)
73    }
74
75    /// Returns the Euclidean distance to another point.
76    #[must_use]
77    pub fn distance_to(self, other: Self) -> f64 {
78        self.distance_squared_to(other).sqrt()
79    }
80
81    /// Returns the squared Euclidean distance to another point.
82    #[must_use]
83    pub fn distance_squared_to(self, other: Self) -> f64 {
84        let delta_x = other.x - self.x;
85        let delta_y = other.y - self.y;
86
87        delta_x.mul_add(delta_x, delta_y * delta_y)
88    }
89
90    /// Returns the midpoint between this point and another point.
91    #[must_use]
92    pub const fn midpoint(self, other: Self) -> Self {
93        Self::new(self.x.midpoint(other.x), self.y.midpoint(other.y))
94    }
95
96    /// Returns a point interpolated between this point and `other`.
97    #[must_use]
98    pub const fn lerp(self, other: Self, t: f64) -> Self {
99        Self::new(
100            self.x + ((other.x - self.x) * t),
101            self.y + ((other.y - self.y) * t),
102        )
103    }
104
105    /// Returns a point translated by a vector.
106    #[must_use]
107    pub const fn translate(self, vector: Vector2) -> Self {
108        Self::new(self.x + vector.x, self.y + vector.y)
109    }
110}
111
112impl Add<Vector2> for Point2 {
113    type Output = Self;
114
115    fn add(self, rhs: Vector2) -> Self::Output {
116        Self::new(self.x + rhs.x, self.y + rhs.y)
117    }
118}
119
120impl Sub<Vector2> for Point2 {
121    type Output = Self;
122
123    fn sub(self, rhs: Vector2) -> Self::Output {
124        Self::new(self.x - rhs.x, self.y - rhs.y)
125    }
126}
127
128impl Sub<Self> for Point2 {
129    type Output = Vector2;
130
131    fn sub(self, rhs: Self) -> Self::Output {
132        Vector2::new(self.x - rhs.x, self.y - rhs.y)
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use super::Point2;
139    use use_coordinate::GeometryError;
140    use use_vector::Vector2;
141
142    fn approx_eq(left: f64, right: f64) -> bool {
143        (left - right).abs() < 1.0e-10
144    }
145
146    #[test]
147    fn constructs_points() {
148        assert_eq!(
149            Point2::new(1.0, 2.0),
150            Point2::try_new(1.0, 2.0).expect("valid point")
151        );
152    }
153
154    #[test]
155    fn rejects_non_finite_coordinates() {
156        assert!(matches!(
157            Point2::try_new(f64::NAN, 2.0),
158            Err(GeometryError::NonFiniteComponent {
159                type_name: "Point2",
160                component: "x",
161                value,
162            }) if value.is_nan()
163        ));
164        assert_eq!(
165            Point2::try_new(1.0, f64::INFINITY),
166            Err(GeometryError::NonFiniteComponent {
167                type_name: "Point2",
168                component: "y",
169                value: f64::INFINITY,
170            })
171        );
172    }
173
174    #[test]
175    fn computes_distance_midpoint_and_lerp() {
176        let left = Point2::new(0.0, 0.0);
177        let right = Point2::new(4.0, 2.0);
178
179        assert!(approx_eq(left.distance_to(Point2::new(3.0, 4.0)), 5.0));
180        assert!(approx_eq(
181            left.distance_squared_to(Point2::new(3.0, 4.0)),
182            25.0
183        ));
184        assert_eq!(left.midpoint(right), Point2::new(2.0, 1.0));
185        assert_eq!(left.lerp(right, 0.25), Point2::new(1.0, 0.5));
186    }
187
188    #[test]
189    fn translates_points_and_builds_differences() {
190        let point = Point2::new(1.5, -2.0);
191        let offset = Vector2::new(2.0, 3.5);
192
193        assert_eq!(point.translate(offset), Point2::new(3.5, 1.5));
194        assert_eq!(point + offset, Point2::new(3.5, 1.5));
195        assert_eq!(point + offset - offset, point);
196        assert_eq!(
197            Point2::new(4.0, 6.0) - Point2::new(1.0, 2.0),
198            Vector2::new(3.0, 4.0)
199        );
200    }
201
202    #[test]
203    fn exposes_accessors_and_origin() {
204        let point = Point2::new(1.5, -2.0);
205
206        assert!(approx_eq(point.x(), 1.5));
207        assert!(approx_eq(point.y(), -2.0));
208        assert!(point.is_finite());
209        assert!(!Point2::new(f64::NAN, 0.0).is_finite());
210        assert_eq!(Point2::origin(), Point2::new(0.0, 0.0));
211    }
212}