1use glam::{Mat4, Quat, Vec3};
2
3pub trait Transformable {
31 fn transform(&self) -> Mat4;
33
34 fn transform_mut(&mut self) -> &mut Mat4;
36
37 fn pivot(&self) -> Vec3 {
39 Vec3::ZERO
40 }
41
42 fn set_transform(&mut self, transform: Mat4) -> &mut Self
44 where
45 Self: Sized,
46 {
47 *self.transform_mut() = transform;
48 self
49 }
50
51 fn reset_transform(&mut self) -> &mut Self
53 where
54 Self: Sized,
55 {
56 self.set_transform(Mat4::IDENTITY)
57 }
58
59 fn apply(&mut self, transform: Mat4) -> &mut Self
61 where
62 Self: Sized,
63 {
64 let current = self.transform();
65 self.set_transform(current * transform)
66 }
67
68 fn apply_about_pivot(&mut self, transform: Mat4) -> &mut Self
70 where
71 Self: Sized,
72 {
73 let pivot = self.pivot();
74 self.apply(Mat4::from_translation(pivot) * transform * Mat4::from_translation(-pivot))
75 }
76
77 fn translate(&mut self, x: f32, y: f32) -> &mut Self
79 where
80 Self: Sized,
81 {
82 self.translate3(Vec3::new(x, y, 0.0))
83 }
84
85 fn translate3(&mut self, offset: Vec3) -> &mut Self
87 where
88 Self: Sized,
89 {
90 self.apply(Mat4::from_translation(offset))
91 }
92
93 fn rotate(&mut self, radians: f32) -> &mut Self
95 where
96 Self: Sized,
97 {
98 self.rotate_z(radians)
99 }
100
101 fn rotate_degrees(&mut self, degrees: f32) -> &mut Self
103 where
104 Self: Sized,
105 {
106 self.rotate(degrees.to_radians())
107 }
108
109 fn rotate_x(&mut self, radians: f32) -> &mut Self
111 where
112 Self: Sized,
113 {
114 self.apply_about_pivot(Mat4::from_rotation_x(radians))
115 }
116
117 fn rotate_y(&mut self, radians: f32) -> &mut Self
119 where
120 Self: Sized,
121 {
122 self.apply_about_pivot(Mat4::from_rotation_y(radians))
123 }
124
125 fn rotate_z(&mut self, radians: f32) -> &mut Self
127 where
128 Self: Sized,
129 {
130 self.apply_about_pivot(Mat4::from_rotation_z(radians))
131 }
132
133 fn rotate_quat(&mut self, rotation: Quat) -> &mut Self
135 where
136 Self: Sized,
137 {
138 self.apply_about_pivot(Mat4::from_quat(rotation))
139 }
140
141 fn scale(&mut self, x: f32, y: f32) -> &mut Self
143 where
144 Self: Sized,
145 {
146 self.scale3(Vec3::new(x, y, 1.0))
147 }
148
149 fn scale_uniform(&mut self, factor: f32) -> &mut Self
151 where
152 Self: Sized,
153 {
154 self.scale3(Vec3::splat(factor))
155 }
156
157 fn scale3(&mut self, scale: Vec3) -> &mut Self
159 where
160 Self: Sized,
161 {
162 self.apply_about_pivot(Mat4::from_scale(scale))
163 }
164
165 fn flip_x(&mut self) -> &mut Self
167 where
168 Self: Sized,
169 {
170 self.scale(-1.0, 1.0)
171 }
172
173 fn flip_y(&mut self) -> &mut Self
175 where
176 Self: Sized,
177 {
178 self.scale(1.0, -1.0)
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185 use std::f32::consts::FRAC_PI_2;
186
187 const EPS: f32 = 1e-5;
188
189 struct Thing {
190 matrix: Mat4,
191 pivot: Vec3,
192 }
193
194 impl Thing {
195 fn new() -> Self {
196 Self { matrix: Mat4::IDENTITY, pivot: Vec3::ZERO }
197 }
198
199 fn with_pivot(pivot: Vec3) -> Self {
200 Self { matrix: Mat4::IDENTITY, pivot }
201 }
202
203 fn at(&self, point: Vec3) -> Vec3 {
204 self.transform().transform_point3(point)
205 }
206 }
207
208 impl Transformable for Thing {
209 fn transform(&self) -> Mat4 {
210 self.matrix
211 }
212
213 fn transform_mut(&mut self) -> &mut Mat4 {
214 &mut self.matrix
215 }
216
217 fn pivot(&self) -> Vec3 {
218 self.pivot
219 }
220 }
221
222 #[test]
223 fn translate_moves_the_origin() {
224 let mut thing = Thing::new();
225 thing.translate(3.0, -4.0);
226 assert!(thing.at(Vec3::ZERO).abs_diff_eq(Vec3::new(3.0, -4.0, 0.0), EPS));
227 }
228
229 #[test]
230 fn rotation_holds_the_pivot_fixed() {
231 let pivot = Vec3::new(8.0, 5.0, 0.0);
232 let mut thing = Thing::with_pivot(pivot);
233 thing.rotate(FRAC_PI_2);
234
235 assert!(thing.at(pivot).abs_diff_eq(pivot, EPS));
236 assert!(thing.at(pivot + Vec3::X).abs_diff_eq(pivot + Vec3::Y, EPS));
238 }
239
240 #[test]
241 fn scale_expands_away_from_the_pivot() {
242 let pivot = Vec3::new(2.0, 2.0, 0.0);
243 let mut thing = Thing::with_pivot(pivot);
244 thing.scale_uniform(3.0);
245
246 assert!(thing.at(pivot).abs_diff_eq(pivot, EPS));
247 assert!(thing.at(pivot + Vec3::new(1.0, 1.0, 0.0))
248 .abs_diff_eq(pivot + Vec3::new(3.0, 3.0, 0.0), EPS));
249 }
250
251 #[test]
252 fn flip_x_mirrors_across_the_pivot() {
253 let pivot = Vec3::new(10.0, 0.0, 0.0);
254 let mut thing = Thing::with_pivot(pivot);
255 thing.flip_x();
256
257 assert!(thing.at(Vec3::new(12.0, 4.0, 0.0)).abs_diff_eq(Vec3::new(8.0, 4.0, 0.0), EPS));
258 }
259
260 #[test]
261 fn chained_operations_apply_in_local_space() {
262 let mut thing = Thing::new();
263 thing.rotate(FRAC_PI_2).translate(10.0, 0.0);
264
265 assert!(thing.at(Vec3::ZERO).abs_diff_eq(Vec3::new(0.0, 10.0, 0.0), EPS));
267 }
268
269 #[test]
270 fn set_transform_discards_accumulated_state() {
271 let mut thing = Thing::new();
272 thing.translate(5.0, 5.0).set_transform(Mat4::from_translation(Vec3::X));
273 assert!(thing.at(Vec3::ZERO).abs_diff_eq(Vec3::X, EPS));
274
275 thing.reset_transform();
276 assert!(thing.at(Vec3::ZERO).abs_diff_eq(Vec3::ZERO, EPS));
277 }
278}