Skip to main content

dotloom_geometry/
point.rs

1use core::ops::{Add, AddAssign, Div, Mul, Neg, Sub, SubAssign};
2
3use serde::{Deserialize, Serialize};
4
5/// A position in the plane (model units, `f64`). Serialized as `[x, y]`.
6#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)]
7#[serde(from = "[f64; 2]", into = "[f64; 2]")]
8pub struct Point {
9    /// X coordinate.
10    pub x: f64,
11    /// Y coordinate (Y grows "up" in model space; views flip it).
12    pub y: f64,
13}
14
15/// A displacement in the plane. Serialized as `[x, y]`.
16#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)]
17#[serde(from = "[f64; 2]", into = "[f64; 2]")]
18pub struct Vector {
19    /// X component.
20    pub x: f64,
21    /// Y component.
22    pub y: f64,
23}
24
25impl From<[f64; 2]> for Point {
26    fn from(v: [f64; 2]) -> Self {
27        Self { x: v[0], y: v[1] }
28    }
29}
30impl From<Point> for [f64; 2] {
31    fn from(p: Point) -> Self {
32        [p.x, p.y]
33    }
34}
35impl From<[f64; 2]> for Vector {
36    fn from(v: [f64; 2]) -> Self {
37        Self { x: v[0], y: v[1] }
38    }
39}
40impl From<Vector> for [f64; 2] {
41    fn from(p: Vector) -> Self {
42        [p.x, p.y]
43    }
44}
45
46impl Point {
47    /// The origin.
48    pub const ORIGIN: Self = Self { x: 0.0, y: 0.0 };
49
50    /// Create a point.
51    #[must_use]
52    pub const fn new(x: f64, y: f64) -> Self {
53        Self { x, y }
54    }
55
56    /// Both coordinates are finite.
57    #[must_use]
58    pub fn is_finite(self) -> bool {
59        self.x.is_finite() && self.y.is_finite()
60    }
61
62    /// Euclidean distance.
63    #[must_use]
64    pub fn distance(self, other: Self) -> f64 {
65        (other - self).length()
66    }
67
68    /// Squared Euclidean distance.
69    #[must_use]
70    pub fn distance_sq(self, other: Self) -> f64 {
71        (other - self).length_sq()
72    }
73
74    /// Linear interpolation, `t = 0` gives `self`.
75    #[must_use]
76    pub fn lerp(self, other: Self, t: f64) -> Self {
77        Self::new(self.x + (other.x - self.x) * t, self.y + (other.y - self.y) * t)
78    }
79
80    /// Midpoint between two points.
81    #[must_use]
82    pub fn midpoint(self, other: Self) -> Self {
83        Self::new((self.x + other.x) * 0.5, (self.y + other.y) * 0.5)
84    }
85
86    /// Vector from the origin to this point.
87    #[must_use]
88    pub const fn to_vector(self) -> Vector {
89        Vector::new(self.x, self.y)
90    }
91
92    pub(crate) fn coord(self) -> robust::Coord<f64> {
93        robust::Coord { x: self.x, y: self.y }
94    }
95}
96
97impl Vector {
98    /// Zero vector.
99    pub const ZERO: Self = Self { x: 0.0, y: 0.0 };
100
101    /// Create a vector.
102    #[must_use]
103    pub const fn new(x: f64, y: f64) -> Self {
104        Self { x, y }
105    }
106
107    /// Unit vector at `angle` radians from +X, counter-clockwise.
108    #[must_use]
109    pub fn from_angle(angle: f64) -> Self {
110        let (s, c) = crate::math::sin_cos(angle);
111        Self::new(c, s)
112    }
113
114    /// Both components are finite.
115    #[must_use]
116    pub fn is_finite(self) -> bool {
117        self.x.is_finite() && self.y.is_finite()
118    }
119
120    /// Euclidean length (overflow-safe).
121    #[must_use]
122    pub fn length(self) -> f64 {
123        crate::math::hypot(self.x, self.y)
124    }
125
126    /// Squared length.
127    #[must_use]
128    pub fn length_sq(self) -> f64 {
129        self.x * self.x + self.y * self.y
130    }
131
132    /// Dot product.
133    #[must_use]
134    pub fn dot(self, o: Self) -> f64 {
135        self.x * o.x + self.y * o.y
136    }
137
138    /// 2D cross product (z component of the 3D cross product).
139    #[must_use]
140    pub fn cross(self, o: Self) -> f64 {
141        self.x * o.y - self.y * o.x
142    }
143
144    /// Unit vector in the same direction, or `None` for a zero/non-finite vector.
145    #[must_use]
146    pub fn normalize(self) -> Option<Self> {
147        let len = self.length();
148        if len > 0.0 && len.is_finite() { Some(Self::new(self.x / len, self.y / len)) } else { None }
149    }
150
151    /// Rotate 90° counter-clockwise.
152    #[must_use]
153    pub const fn perp(self) -> Self {
154        Self::new(-self.y, self.x)
155    }
156
157    /// Angle from +X in radians, in `(-π, π]`.
158    #[must_use]
159    pub fn angle(self) -> f64 {
160        crate::math::atan2(self.y, self.x)
161    }
162
163    /// Rotate by `angle` radians counter-clockwise.
164    #[must_use]
165    pub fn rotate(self, angle: f64) -> Self {
166        let (s, c) = crate::math::sin_cos(angle);
167        Self::new(self.x * c - self.y * s, self.x * s + self.y * c)
168    }
169
170    /// Point at the tip of this vector from the origin.
171    #[must_use]
172    pub const fn to_point(self) -> Point {
173        Point::new(self.x, self.y)
174    }
175}
176
177impl Sub for Point {
178    type Output = Vector;
179    fn sub(self, o: Self) -> Vector {
180        Vector::new(self.x - o.x, self.y - o.y)
181    }
182}
183impl Add<Vector> for Point {
184    type Output = Self;
185    fn add(self, v: Vector) -> Self {
186        Self::new(self.x + v.x, self.y + v.y)
187    }
188}
189impl AddAssign<Vector> for Point {
190    fn add_assign(&mut self, v: Vector) {
191        self.x += v.x;
192        self.y += v.y;
193    }
194}
195impl Sub<Vector> for Point {
196    type Output = Self;
197    fn sub(self, v: Vector) -> Self {
198        Self::new(self.x - v.x, self.y - v.y)
199    }
200}
201impl SubAssign<Vector> for Point {
202    fn sub_assign(&mut self, v: Vector) {
203        self.x -= v.x;
204        self.y -= v.y;
205    }
206}
207impl Add for Vector {
208    type Output = Self;
209    fn add(self, o: Self) -> Self {
210        Self::new(self.x + o.x, self.y + o.y)
211    }
212}
213impl AddAssign for Vector {
214    fn add_assign(&mut self, o: Self) {
215        self.x += o.x;
216        self.y += o.y;
217    }
218}
219impl Sub for Vector {
220    type Output = Self;
221    fn sub(self, o: Self) -> Self {
222        Self::new(self.x - o.x, self.y - o.y)
223    }
224}
225impl Neg for Vector {
226    type Output = Self;
227    fn neg(self) -> Self {
228        Self::new(-self.x, -self.y)
229    }
230}
231impl Mul<f64> for Vector {
232    type Output = Self;
233    fn mul(self, s: f64) -> Self {
234        Self::new(self.x * s, self.y * s)
235    }
236}
237impl Mul<Vector> for f64 {
238    type Output = Vector;
239    fn mul(self, v: Vector) -> Vector {
240        Vector::new(self * v.x, self * v.y)
241    }
242}
243impl Div<f64> for Vector {
244    type Output = Self;
245    fn div(self, s: f64) -> Self {
246        Self::new(self.x / s, self.y / s)
247    }
248}
249
250/// Orientation of three points using an exact (adaptive-precision) predicate.
251#[derive(Debug, Clone, Copy, PartialEq, Eq)]
252pub enum Orientation {
253    /// `c` lies to the left of `a → b`.
254    CounterClockwise,
255    /// `c` lies to the right of `a → b`.
256    Clockwise,
257    /// The three points are exactly collinear.
258    Collinear,
259}
260
261/// Exact orientation test (Shewchuk's adaptive predicate via the `robust` crate).
262#[must_use]
263pub fn orientation(a: Point, b: Point, c: Point) -> Orientation {
264    let d = robust::orient2d(a.coord(), b.coord(), c.coord());
265    if d > 0.0 {
266        Orientation::CounterClockwise
267    } else if d < 0.0 {
268        Orientation::Clockwise
269    } else {
270        Orientation::Collinear
271    }
272}
273
274/// Normalize an angle into `[0, 2π)`.
275#[must_use]
276pub fn normalize_angle(a: f64) -> f64 {
277    let tau = core::f64::consts::TAU;
278    let r = a.rem_euclid(tau);
279    if r >= tau { 0.0 } else { r }
280}
281
282/// Normalize an angle into `(-π, π]`.
283#[must_use]
284pub fn normalize_angle_signed(a: f64) -> f64 {
285    let pi = core::f64::consts::PI;
286    let r = normalize_angle(a);
287    if r > pi { r - core::f64::consts::TAU } else { r }
288}
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293
294    #[test]
295    fn serde_as_array() {
296        let p = Point::new(1.5, -2.0);
297        let s = serde_json::to_string(&p).unwrap();
298        assert_eq!(s, "[1.5,-2.0]");
299        let back: Point = serde_json::from_str(&s).unwrap();
300        assert_eq!(back, p);
301    }
302
303    #[test]
304    fn orientation_is_exact_for_nearly_collinear_points() {
305        // Classic failure case of naive floating point orientation.
306        let a = Point::new(0.5, 0.5);
307        let b = Point::new(12.0, 12.0);
308        let c = Point::new(24.0, 24.0);
309        assert_eq!(orientation(a, b, c), Orientation::Collinear);
310        let c2 = Point::new(24.0, 24.000_000_000_000_004);
311        assert_eq!(orientation(a, b, c2), Orientation::CounterClockwise);
312    }
313
314    #[test]
315    fn angle_normalization() {
316        use core::f64::consts::{PI, TAU};
317        assert!((normalize_angle(-PI / 2.0) - 1.5 * PI).abs() < 1e-15);
318        assert!(normalize_angle(TAU) < 1e-15);
319        assert!((normalize_angle_signed(1.5 * PI) + PI / 2.0).abs() < 1e-15);
320    }
321
322    #[test]
323    fn normalize_zero_is_none() {
324        assert!(Vector::ZERO.normalize().is_none());
325        assert!(Vector::new(f64::NAN, 1.0).normalize().is_none());
326        assert!(Vector::new(f64::INFINITY, 1.0).normalize().is_none());
327    }
328}