Skip to main content

motion_core/
geometry.rs

1//! Double-precision authoring points, vectors, transforms and bounds.
2
3use std::ops::{Add, Div, Mul, Neg, Sub};
4
5pub mod bounds;
6pub mod flatten;
7mod transform;
8
9pub use transform::{Transform, TransformComponents};
10
11/// A position in two-dimensional design coordinates, without implicit narrowing.
12#[derive(Clone, Copy, Debug, Default, PartialEq)]
13pub struct Point {
14    /// Horizontal position, increasing toward the right.
15    pub x: f64,
16    /// Vertical position, increasing downward.
17    pub y: f64,
18}
19
20impl Point {
21    /// The coordinate origin.
22    pub const ORIGIN: Self = Self::new(0.0, 0.0);
23
24    /// Preserves the supplied coordinates; validation belongs to the consumer.
25    pub const fn new(horizontal: f64, vertical: f64) -> Self {
26        Self {
27            x: horizontal,
28            y: vertical,
29        }
30    }
31
32    /// Returns Euclidean distance using deterministic pure-Rust math.
33    pub fn distance(self, other: Self) -> f64 {
34        (self - other).length()
35    }
36
37    /// Linearly interpolates or extrapolates with double-precision progress.
38    pub fn interpolate(&self, target: &Self, progress: f64) -> Self {
39        Self::new(
40            self.x * (1.0 - progress) + target.x * progress,
41            self.y * (1.0 - progress) + target.y * progress,
42        )
43    }
44}
45
46impl Add<Vector2> for Point {
47    type Output = Self;
48
49    fn add(self, displacement: Vector2) -> Self {
50        Self::new(self.x + displacement.x, self.y + displacement.y)
51    }
52}
53
54impl Sub<Vector2> for Point {
55    type Output = Self;
56
57    fn sub(self, displacement: Vector2) -> Self {
58        Self::new(self.x - displacement.x, self.y - displacement.y)
59    }
60}
61
62impl Sub for Point {
63    type Output = Vector2;
64
65    fn sub(self, origin: Self) -> Vector2 {
66        Vector2::new(self.x - origin.x, self.y - origin.y)
67    }
68}
69
70/// A displacement or direction, distinct from a [`Point`] position.
71#[derive(Clone, Copy, Debug, Default, PartialEq)]
72pub struct Vector2 {
73    /// Horizontal displacement.
74    pub x: f64,
75    /// Vertical displacement.
76    pub y: f64,
77}
78
79impl Vector2 {
80    /// A displacement with no movement.
81    pub const ZERO: Self = Self::new(0.0, 0.0);
82
83    /// Preserves the supplied components; validation belongs to the consumer.
84    pub const fn new(horizontal: f64, vertical: f64) -> Self {
85        Self {
86            x: horizontal,
87            y: vertical,
88        }
89    }
90
91    /// Returns a unit vector at clockwise radians from the positive horizontal axis.
92    /// Principal quarter-turn angles preserve exact axis alignment.
93    pub fn from_angle(radians: f64) -> Self {
94        if radians == 0.0 {
95            Self::new(1.0, 0.0)
96        } else if radians.abs() == std::f64::consts::PI {
97            Self::new(-1.0, 0.0)
98        } else if radians == std::f64::consts::FRAC_PI_2 {
99            Self::new(0.0, 1.0)
100        } else if radians == -std::f64::consts::FRAC_PI_2 {
101            Self::new(0.0, -1.0)
102        } else {
103            Self::new(libm::cos(radians), libm::sin(radians))
104        }
105    }
106
107    /// Returns Euclidean length without squaring unscaled components.
108    pub fn length(self) -> f64 {
109        libm::hypot(self.x, self.y)
110    }
111
112    /// Returns a unit direction, or zero for a zero vector.
113    /// Finite very large or very small components are scaled before normalization.
114    /// Nonfinite input does not become a finite zero vector.
115    pub fn normalize(self) -> Self {
116        if self.x == 0.0 && self.y == 0.0 {
117            return Self::ZERO;
118        }
119        let magnitude = self.x.abs().max(self.y.abs());
120        let scaled = self / magnitude;
121        scaled / scaled.length()
122    }
123
124    /// Returns the scalar dot product.
125    pub fn dot(self, other: Self) -> f64 {
126        self.x * other.x + self.y * other.y
127    }
128
129    /// Returns the signed two-dimensional cross product.
130    pub fn cross(self, other: Self) -> f64 {
131        self.x * other.y - self.y * other.x
132    }
133
134    /// Rotates clockwise in the downward-positive coordinate system.
135    pub fn rotate(self, radians: f64) -> Self {
136        let direction = Self::from_angle(radians);
137        Self::new(
138            direction.x * self.x - direction.y * self.y,
139            direction.y * self.x + direction.x * self.y,
140        )
141    }
142
143    /// Linearly interpolates or extrapolates with double-precision progress.
144    pub fn interpolate(&self, target: &Self, progress: f64) -> Self {
145        Self::new(
146            self.x * (1.0 - progress) + target.x * progress,
147            self.y * (1.0 - progress) + target.y * progress,
148        )
149    }
150}
151
152impl Add for Vector2 {
153    type Output = Self;
154
155    fn add(self, other: Self) -> Self {
156        Self::new(self.x + other.x, self.y + other.y)
157    }
158}
159
160impl Sub for Vector2 {
161    type Output = Self;
162
163    fn sub(self, other: Self) -> Self {
164        Self::new(self.x - other.x, self.y - other.y)
165    }
166}
167
168impl Neg for Vector2 {
169    type Output = Self;
170
171    fn neg(self) -> Self {
172        Self::new(-self.x, -self.y)
173    }
174}
175
176impl Mul<f64> for Vector2 {
177    type Output = Self;
178
179    fn mul(self, factor: f64) -> Self {
180        Self::new(self.x * factor, self.y * factor)
181    }
182}
183
184impl Mul<Vector2> for f64 {
185    type Output = Vector2;
186
187    fn mul(self, vector: Vector2) -> Vector2 {
188        vector * self
189    }
190}
191
192impl Div<f64> for Vector2 {
193    type Output = Self;
194
195    fn div(self, divisor: f64) -> Self {
196        Self::new(self.x / divisor, self.y / divisor)
197    }
198}
199
200/// Readable angle literals returning double-precision radians.
201/// Nonfinite floating inputs propagate; consuming geometry validates them.
202pub trait AngleLiteral {
203    /// Converts degrees to radians, as in `45.degrees()`.
204    fn degrees(self) -> f64;
205
206    /// Converts complete revolutions to radians, as in `0.5.turns()`.
207    fn turns(self) -> f64;
208}
209
210impl AngleLiteral for f64 {
211    fn degrees(self) -> f64 {
212        self.to_radians()
213    }
214
215    fn turns(self) -> f64 {
216        self * std::f64::consts::TAU
217    }
218}
219
220impl AngleLiteral for i32 {
221    fn degrees(self) -> f64 {
222        f64::from(self).degrees()
223    }
224
225    fn turns(self) -> f64 {
226        f64::from(self).turns()
227    }
228}
229
230impl AngleLiteral for i64 {
231    fn degrees(self) -> f64 {
232        (self as f64).degrees()
233    }
234
235    fn turns(self) -> f64 {
236        (self as f64).turns()
237    }
238}
239
240#[cfg(test)]
241mod tests {
242    #[test]
243    fn design_points_preserve_precision_and_distinguish_displacements() {
244        use super::{Point, Vector2};
245
246        let origin = Point::new(16_777_217.125, -14.5);
247        let displacement = Vector2::new(3.0, 4.0);
248        let target = origin + displacement;
249        assert_eq!(target - origin, displacement);
250        assert_eq!(target - displacement, origin);
251        assert_eq!(origin.distance(target), 5.0);
252        assert_ne!(origin.x, f64::from(origin.x as f32));
253        let progress = 1.0 / 3.0;
254        assert_eq!(
255            Point::ORIGIN.interpolate(&Point::new(1.0, 1.0), progress).x,
256            progress
257        );
258        assert_eq!(
259            Point::ORIGIN.interpolate(&Point::new(2.0, 4.0), 1.5),
260            Point::new(3.0, 6.0)
261        );
262    }
263
264    #[test]
265    fn design_vectors_normalize_extreme_lengths_and_rotate_clockwise() {
266        use super::{AngleLiteral, Vector2};
267
268        assert_eq!(Vector2::ZERO.normalize(), Vector2::ZERO);
269        for component in [f64::MAX, 1e-300, f64::from_bits(1)] {
270            let unit = Vector2::new(component, component).normalize();
271            assert!((unit.length() - 1.0).abs() < 1e-15);
272            assert!((unit.x - std::f64::consts::FRAC_1_SQRT_2).abs() < 1e-15);
273        }
274        for component in [f64::NAN, f64::INFINITY] {
275            let invalid = Vector2::new(component, 0.0).normalize();
276            assert!(!invalid.x.is_finite() || !invalid.y.is_finite());
277        }
278        let rotated = Vector2::new(2.0, 0.0).rotate(90.degrees());
279        assert!(rotated.x.abs() < 1e-14);
280        assert!((rotated.y - 2.0).abs() < 1e-14);
281        assert_eq!(Vector2::from_angle(180.degrees()), Vector2::new(-1.0, 0.0));
282        assert_eq!(Vector2::from_angle(-90.degrees()), Vector2::new(0.0, -1.0));
283        assert_eq!(Vector2::new(1.0, 0.0).cross(Vector2::new(0.0, 1.0)), 1.0);
284        assert_eq!(Vector2::new(3.0, 4.0).dot(Vector2::new(-4.0, 3.0)), 0.0);
285        assert_eq!(0.5.turns(), 180.degrees());
286        assert_eq!(2_i64.turns(), 720.degrees());
287        assert_eq!(2.0 * Vector2::new(3.0, 4.0) / 2.0, Vector2::new(3.0, 4.0));
288    }
289}