Skip to main content

stipple_geometry/
affine.rs

1use crate::{Point, Vec2};
2
3/// A 2D affine transform stored as six coefficients `[a, b, c, d, e, f]`,
4/// row-major, mapping a point `(x, y)` to:
5///
6/// ```text
7/// x' = a·x + c·y + e
8/// y' = b·x + d·y + f
9/// ```
10///
11/// This matches the column convention used by SVG/PostScript `matrix(...)`
12/// and by `oxideav-core`'s `Transform2D`; `stipple-render` converts between the
13/// two at the render boundary.
14#[derive(Clone, Copy, Debug, PartialEq)]
15pub struct Affine([f64; 6]);
16
17impl Default for Affine {
18    #[inline]
19    fn default() -> Self {
20        Self::IDENTITY
21    }
22}
23
24impl Affine {
25    pub const IDENTITY: Self = Self([1.0, 0.0, 0.0, 1.0, 0.0, 0.0]);
26
27    #[inline]
28    pub const fn new(a: f64, b: f64, c: f64, d: f64, e: f64, f: f64) -> Self {
29        Self([a, b, c, d, e, f])
30    }
31
32    #[inline]
33    pub const fn translate(v: Vec2) -> Self {
34        Self([1.0, 0.0, 0.0, 1.0, v.dx, v.dy])
35    }
36
37    #[inline]
38    pub const fn scale(sx: f64, sy: f64) -> Self {
39        Self([sx, 0.0, 0.0, sy, 0.0, 0.0])
40    }
41
42    #[inline]
43    pub fn rotate(radians: f64) -> Self {
44        let (s, c) = radians.sin_cos();
45        Self([c, s, -s, c, 0.0, 0.0])
46    }
47
48    #[inline]
49    pub const fn as_array(self) -> [f64; 6] {
50        self.0
51    }
52
53    /// Compose so that `self` is applied **after** `inner`
54    /// (`result(p) = self(inner(p))`).
55    pub fn then(self, outer: Affine) -> Affine {
56        let [a1, b1, c1, d1, e1, f1] = self.0;
57        let [a2, b2, c2, d2, e2, f2] = outer.0;
58        Affine([
59            a1 * a2 + b1 * c2,
60            a1 * b2 + b1 * d2,
61            c1 * a2 + d1 * c2,
62            c1 * b2 + d1 * d2,
63            e1 * a2 + f1 * c2 + e2,
64            e1 * b2 + f1 * d2 + f2,
65        ])
66    }
67
68    /// Apply the transform to a point.
69    #[inline]
70    pub fn apply(self, p: Point) -> Point {
71        let [a, b, c, d, e, f] = self.0;
72        Point::new(a * p.x + c * p.y + e, b * p.x + d * p.y + f)
73    }
74
75    /// The determinant of the linear (2×2) part.
76    #[inline]
77    pub fn determinant(self) -> f64 {
78        let [a, b, c, d, ..] = self.0;
79        a * d - b * c
80    }
81
82    /// Matrix inverse, or `None` if the transform is singular.
83    pub fn inverse(self) -> Option<Affine> {
84        let det = self.determinant();
85        if det.abs() < f64::EPSILON {
86            return None;
87        }
88        let [a, b, c, d, e, f] = self.0;
89        let inv = 1.0 / det;
90        Some(Affine([
91            d * inv,
92            -b * inv,
93            -c * inv,
94            a * inv,
95            (c * f - d * e) * inv,
96            (b * e - a * f) * inv,
97        ]))
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    #[test]
106    fn translate_then_scale_order() {
107        // Translate by (10, 0), then scale 2x: point (1,0) -> (22, 0).
108        let t = Affine::translate(Vec2::new(10.0, 0.0)).then(Affine::scale(2.0, 2.0));
109        assert_eq!(t.apply(Point::new(1.0, 0.0)), Point::new(22.0, 0.0));
110    }
111
112    #[test]
113    fn inverse_roundtrip() {
114        let t = Affine::translate(Vec2::new(5.0, -3.0)).then(Affine::scale(2.0, 4.0));
115        let p = Point::new(7.0, 9.0);
116        let back = t.inverse().unwrap().apply(t.apply(p));
117        assert!((back.x - p.x).abs() < 1e-9 && (back.y - p.y).abs() < 1e-9);
118    }
119
120    #[test]
121    fn singular_has_no_inverse() {
122        assert!(Affine::scale(0.0, 1.0).inverse().is_none());
123    }
124}