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