Skip to main content

dinamika_cpu/
geometry.rs

1//! Geometric primitives: points, rectangles and affine transformations.
2
3use core::ops::{Add, Mul, Neg, Sub};
4
5/// A point (or vector) in the plane.
6#[derive(Copy, Clone, Debug, Default, PartialEq)]
7pub struct Point {
8    pub x: f32,
9    pub y: f32,
10}
11
12impl Point {
13    pub const ZERO: Point = Point { x: 0.0, y: 0.0 };
14
15    #[inline]
16    pub const fn new(x: f32, y: f32) -> Self {
17        Point { x, y }
18    }
19
20    /// Length of the vector.
21    #[inline]
22    pub fn length(self) -> f32 {
23        (self.x * self.x + self.y * self.y).sqrt()
24    }
25
26    /// Distance to another point.
27    #[inline]
28    pub fn distance(self, other: Point) -> f32 {
29        (self - other).length()
30    }
31
32    /// Dot product.
33    #[inline]
34    pub fn dot(self, other: Point) -> f32 {
35        self.x * other.x + self.y * other.y
36    }
37
38    /// Pseudo-scalar (z-component of the cross) product.
39    #[inline]
40    pub fn cross(self, other: Point) -> f32 {
41        self.x * other.y - self.y * other.x
42    }
43
44    /// Unit vector of the same direction. For the zero vector returns zero.
45    #[inline]
46    pub fn normalize(self) -> Point {
47        let len = self.length();
48        if len > 0.0 {
49            Point::new(self.x / len, self.y / len)
50        } else {
51            Point::ZERO
52        }
53    }
54
55    /// Perpendicular, rotated by +90° (left normal).
56    #[inline]
57    pub fn left_normal(self) -> Point {
58        Point::new(-self.y, self.x)
59    }
60
61    /// Linear interpolation between points.
62    #[inline]
63    pub fn lerp(self, other: Point, t: f32) -> Point {
64        Point::new(self.x + (other.x - self.x) * t, self.y + (other.y - self.y) * t)
65    }
66}
67
68impl Add for Point {
69    type Output = Point;
70    #[inline]
71    fn add(self, rhs: Point) -> Point {
72        Point::new(self.x + rhs.x, self.y + rhs.y)
73    }
74}
75
76impl Sub for Point {
77    type Output = Point;
78    #[inline]
79    fn sub(self, rhs: Point) -> Point {
80        Point::new(self.x - rhs.x, self.y - rhs.y)
81    }
82}
83
84impl Mul<f32> for Point {
85    type Output = Point;
86    #[inline]
87    fn mul(self, rhs: f32) -> Point {
88        Point::new(self.x * rhs, self.y * rhs)
89    }
90}
91
92impl Neg for Point {
93    type Output = Point;
94    #[inline]
95    fn neg(self) -> Point {
96        Point::new(-self.x, -self.y)
97    }
98}
99
100/// A rectangle defined by its bounds in floating-point coordinates.
101#[derive(Copy, Clone, Debug, PartialEq)]
102pub struct Rect {
103    pub left: f32,
104    pub top: f32,
105    pub right: f32,
106    pub bottom: f32,
107}
108
109impl Rect {
110    /// Creates a rectangle from a position and size. Returns `None` for
111    /// non-positive width/height or non-finite coordinates.
112    pub fn from_xywh(x: f32, y: f32, w: f32, h: f32) -> Option<Rect> {
113        let valid = w > 0.0 && h > 0.0 && x.is_finite() && y.is_finite();
114        if !valid {
115            return None;
116        }
117        Some(Rect { left: x, top: y, right: x + w, bottom: y + h })
118    }
119
120    /// Creates a rectangle from bounds, normalizing the order of the edges.
121    pub fn from_ltrb(left: f32, top: f32, right: f32, bottom: f32) -> Option<Rect> {
122        let (left, right) = if left <= right { (left, right) } else { (right, left) };
123        let (top, bottom) = if top <= bottom { (top, bottom) } else { (bottom, top) };
124        if right > left && bottom > top {
125            Some(Rect { left, top, right, bottom })
126        } else {
127            None
128        }
129    }
130
131    #[inline]
132    pub fn width(&self) -> f32 {
133        self.right - self.left
134    }
135
136    #[inline]
137    pub fn height(&self) -> f32 {
138        self.bottom - self.top
139    }
140
141    #[inline]
142    pub fn center(&self) -> Point {
143        Point::new((self.left + self.right) * 0.5, (self.top + self.bottom) * 0.5)
144    }
145
146    /// Whether the rectangle contains the point.
147    #[inline]
148    pub fn contains(&self, p: Point) -> bool {
149        p.x >= self.left && p.x < self.right && p.y >= self.top && p.y < self.bottom
150    }
151}
152
153/// A 2×3 affine transformation.
154///
155/// A point `(x, y)` maps to
156/// `(sx*x + kx*y + tx, ky*x + sy*y + ty)`. The order of the fields matches
157/// Skia (column-major).
158#[derive(Copy, Clone, Debug, PartialEq)]
159pub struct Transform {
160    pub sx: f32,
161    pub ky: f32,
162    pub kx: f32,
163    pub sy: f32,
164    pub tx: f32,
165    pub ty: f32,
166}
167
168impl Default for Transform {
169    fn default() -> Self {
170        Transform::identity()
171    }
172}
173
174impl Transform {
175    #[inline]
176    pub const fn identity() -> Self {
177        Transform { sx: 1.0, ky: 0.0, kx: 0.0, sy: 1.0, tx: 0.0, ty: 0.0 }
178    }
179
180    #[inline]
181    pub const fn from_row(sx: f32, ky: f32, kx: f32, sy: f32, tx: f32, ty: f32) -> Self {
182        Transform { sx, ky, kx, sy, tx, ty }
183    }
184
185    #[inline]
186    pub const fn from_translate(tx: f32, ty: f32) -> Self {
187        Transform { sx: 1.0, ky: 0.0, kx: 0.0, sy: 1.0, tx, ty }
188    }
189
190    #[inline]
191    pub const fn from_scale(sx: f32, sy: f32) -> Self {
192        Transform { sx, ky: 0.0, kx: 0.0, sy, tx: 0.0, ty: 0.0 }
193    }
194
195    /// Rotation around the origin by the given angle in degrees.
196    pub fn from_rotate(degrees: f32) -> Self {
197        let r = degrees.to_radians();
198        let (s, c) = r.sin_cos();
199        Transform { sx: c, ky: s, kx: -s, sy: c, tx: 0.0, ty: 0.0 }
200    }
201
202    /// Rotation around the point `(cx, cy)`.
203    pub fn from_rotate_at(degrees: f32, cx: f32, cy: f32) -> Self {
204        Transform::from_translate(cx, cy)
205            .pre_concat(Transform::from_rotate(degrees))
206            .pre_concat(Transform::from_translate(-cx, -cy))
207    }
208
209    #[inline]
210    pub fn is_identity(&self) -> bool {
211        *self == Transform::identity()
212    }
213
214    /// Composition: applies `other` first, then `self`.
215    pub fn pre_concat(&self, other: Transform) -> Transform {
216        Transform {
217            sx: self.sx * other.sx + self.kx * other.ky,
218            ky: self.ky * other.sx + self.sy * other.ky,
219            kx: self.sx * other.kx + self.kx * other.sy,
220            sy: self.ky * other.kx + self.sy * other.sy,
221            tx: self.sx * other.tx + self.kx * other.ty + self.tx,
222            ty: self.ky * other.tx + self.sy * other.ty + self.ty,
223        }
224    }
225
226    /// Composition: applies `self` first, then `other`.
227    pub fn post_concat(&self, other: Transform) -> Transform {
228        other.pre_concat(*self)
229    }
230
231    /// Maps a point.
232    #[inline]
233    pub fn map_point(&self, p: Point) -> Point {
234        Point::new(self.sx * p.x + self.kx * p.y + self.tx, self.ky * p.x + self.sy * p.y + self.ty)
235    }
236
237    /// Maps a slice of points in place.
238    pub fn map_points(&self, points: &mut [Point]) {
239        for p in points.iter_mut() {
240            *p = self.map_point(*p);
241        }
242    }
243
244    /// Inverse transformation. Returns `None` if the matrix is singular.
245    pub fn invert(&self) -> Option<Transform> {
246        let det = self.sx * self.sy - self.kx * self.ky;
247        // We take the singularity threshold relative to the squared matrix
248        // scale: the determinant has the dimension of "(element)²", so a fixed
249        // `f32::EPSILON` would wrongly reject invertible matrices with a small
250        // scale (for `scale(1e-4)` det ≈ 1e-8 < EPSILON, even though the matrix
251        // is non-singular). A relative threshold does not depend on the units.
252        let scale = self
253            .sx
254            .abs()
255            .max(self.ky.abs())
256            .max(self.kx.abs())
257            .max(self.sy.abs());
258        if !det.is_finite() || det.abs() <= f32::EPSILON * scale * scale {
259            return None;
260        }
261        let inv = 1.0 / det;
262        Some(Transform {
263            sx: self.sy * inv,
264            ky: -self.ky * inv,
265            kx: -self.kx * inv,
266            sy: self.sx * inv,
267            tx: (self.kx * self.ty - self.sy * self.tx) * inv,
268            ty: (self.ky * self.tx - self.sx * self.ty) * inv,
269        })
270    }
271
272    /// An estimate of the transformation's scale — useful for choosing the
273    /// curve flattening precision in screen coordinates.
274    pub fn max_scale(&self) -> f32 {
275        let sa = (self.sx * self.sx + self.ky * self.ky).sqrt();
276        let sb = (self.kx * self.kx + self.sy * self.sy).sqrt();
277        sa.max(sb)
278    }
279}
280
281#[cfg(test)]
282mod tests {
283    use super::*;
284
285    #[test]
286    fn transform_roundtrip() {
287        let t = Transform::from_translate(10.0, 5.0)
288            .pre_concat(Transform::from_rotate(33.0))
289            .pre_concat(Transform::from_scale(2.0, 3.0));
290        let inv = t.invert().unwrap();
291        let p = Point::new(7.0, -4.0);
292        let mapped = inv.map_point(t.map_point(p));
293        assert!((mapped.x - p.x).abs() < 1e-3, "{mapped:?}");
294        assert!((mapped.y - p.y).abs() < 1e-3, "{mapped:?}");
295    }
296
297    #[test]
298    fn invert_small_scale_is_not_degenerate() {
299        // A tiny but non-singular scale: det = 1e-8 < f32::EPSILON,
300        // yet the matrix is invertible — previously an absolute threshold rejected it.
301        let t = Transform::from_scale(1e-4, 1e-4);
302        let inv = t.invert().expect("a matrix with a small scale is invertible");
303        let p = Point::new(3.0, -5.0);
304        let mapped = inv.map_point(t.map_point(p));
305        assert!((mapped.x - p.x).abs() < 1e-2, "{mapped:?}");
306        assert!((mapped.y - p.y).abs() < 1e-2, "{mapped:?}");
307    }
308
309    #[test]
310    fn invert_singular_is_none() {
311        // Zero scale along one axis — the matrix is singular.
312        assert!(Transform::from_scale(1.0, 0.0).invert().is_none());
313    }
314
315    #[test]
316    fn translate_then_scale_order() {
317        // pre_concat applies the right argument first.
318        let t = Transform::from_scale(2.0, 2.0).pre_concat(Transform::from_translate(1.0, 1.0));
319        // first the translate (0,0)->(1,1), then the scale -> (2,2)
320        assert_eq!(t.map_point(Point::ZERO), Point::new(2.0, 2.0));
321    }
322}