1use crate::point::Point;
2
3#[derive(Debug, Clone, Copy, PartialEq)]
9pub struct Transform {
10 pub a: f32,
11 pub b: f32,
12 pub c: f32,
13 pub d: f32,
14 pub e: f32,
15 pub f: f32,
16}
17
18impl Default for Transform {
19 fn default() -> Self {
20 Self::IDENTITY
21 }
22}
23
24impl Transform {
25 pub const IDENTITY: Transform = Transform {
26 a: 1.0,
27 b: 0.0,
28 c: 0.0,
29 d: 1.0,
30 e: 0.0,
31 f: 0.0,
32 };
33
34 pub fn translate(tx: f32, ty: f32) -> Self {
36 Self {
37 a: 1.0,
38 b: 0.0,
39 c: 0.0,
40 d: 1.0,
41 e: tx,
42 f: ty,
43 }
44 }
45
46 pub fn scale_around(sx: f32, sy: f32, cx: f32, cy: f32) -> Self {
48 Self {
49 a: sx,
50 b: 0.0,
51 c: 0.0,
52 d: sy,
53 e: cx - sx * cx,
54 f: cy - sy * cy,
55 }
56 }
57
58 pub fn rotate_around(angle_deg: f32, cx: f32, cy: f32) -> Self {
60 let a = angle_deg.to_radians();
61 let cos = a.cos();
62 let sin = a.sin();
63 Self {
64 a: cos,
65 b: sin,
66 c: -sin,
67 d: cos,
68 e: cx - cx * cos + cy * sin,
69 f: cy - cx * sin - cy * cos,
70 }
71 }
72
73 pub fn then(self, next: Transform) -> Transform {
76 Transform {
77 a: next.a * self.a + next.c * self.b,
78 b: next.b * self.a + next.d * self.b,
79 c: next.a * self.c + next.c * self.d,
80 d: next.b * self.c + next.d * self.d,
81 e: next.a * self.e + next.c * self.f + next.e,
82 f: next.b * self.e + next.d * self.f + next.f,
83 }
84 }
85
86 pub fn apply(&self, p: Point) -> Point {
87 Point::new(
88 self.a * p.x + self.c * p.y + self.e,
89 self.b * p.x + self.d * p.y + self.f,
90 )
91 }
92
93 pub fn to_array(&self) -> [f32; 6] {
94 [self.a, self.b, self.c, self.d, self.e, self.f]
95 }
96
97 pub fn from_array(m: [f32; 6]) -> Transform {
99 Transform {
100 a: m[0],
101 b: m[1],
102 c: m[2],
103 d: m[3],
104 e: m[4],
105 f: m[5],
106 }
107 }
108
109 pub fn invert(&self) -> Option<Transform> {
111 let det = self.a * self.d - self.b * self.c;
112 if det.abs() < 1e-6 {
113 return None;
114 }
115 Some(Transform {
116 a: self.d / det,
117 b: -self.b / det,
118 c: -self.c / det,
119 d: self.a / det,
120 e: (self.c * self.f - self.d * self.e) / det,
121 f: (self.b * self.e - self.a * self.f) / det,
122 })
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129
130 #[test]
131 fn identity_is_noop() {
132 let p = Point::new(3.0, 4.0);
133 assert_eq!(Transform::IDENTITY.apply(p), p);
134 }
135
136 #[test]
137 fn then_composes_in_application_order() {
138 let translate = Transform {
139 e: 10.0,
140 ..Transform::IDENTITY
141 };
142 let scale = Transform {
143 a: 2.0,
144 d: 2.0,
145 ..Transform::IDENTITY
146 };
147 let t = translate.then(scale);
148 assert_eq!(t.apply(Point::new(0.0, 0.0)), Point::new(20.0, 0.0));
149 }
150
151 #[test]
152 fn rotate_around_keeps_center_fixed() {
153 let c = Point::new(5.0, 5.0);
154 let r = Transform::rotate_around(90.0, c.x, c.y).apply(c);
155 assert!((r.x - c.x).abs() < 1e-4 && (r.y - c.y).abs() < 1e-4);
156 }
157
158 #[test]
159 fn scale_around_keeps_center_fixed() {
160 let c = Point::new(7.0, 2.0);
161 let s = Transform::scale_around(3.0, 3.0, c.x, c.y).apply(c);
162 assert!((s.x - c.x).abs() < 1e-4 && (s.y - c.y).abs() < 1e-4);
163 }
164
165 #[test]
166 fn from_array_round_trips_to_array() {
167 let t = Transform {
168 a: 1.5,
169 b: 0.5,
170 c: -0.25,
171 d: 2.0,
172 e: 3.0,
173 f: -4.0,
174 };
175 assert_eq!(Transform::from_array(t.to_array()), t);
176 }
177
178 #[test]
179 fn then_invert_is_identity() {
180 let t = Transform {
181 a: 1.5,
182 b: 0.5,
183 c: -0.25,
184 d: 2.0,
185 e: 3.0,
186 f: -4.0,
187 };
188 let id = t.then(t.invert().unwrap());
189 let approx = |x: f32, y: f32| (x - y).abs() < 1e-4;
190 assert!(approx(id.a, 1.0));
191 assert!(approx(id.b, 0.0));
192 assert!(approx(id.c, 0.0));
193 assert!(approx(id.d, 1.0));
194 assert!(approx(id.e, 0.0));
195 assert!(approx(id.f, 0.0));
196 }
197
198 #[test]
199 fn invert_singular_returns_none() {
200 let singular = Transform {
201 a: 0.0,
202 b: 0.0,
203 c: 0.0,
204 d: 0.0,
205 e: 5.0,
206 f: 5.0,
207 };
208 assert!(singular.invert().is_none());
209 }
210}