Skip to main content

libdqg/
transform.rs

1use glam::{Mat4, Quat, Vec3};
2
3/// Basic affine transformations for anything that carries a model matrix.
4///
5/// Implementors only supply [`Transformable::transform`] and [`Transformable::transform_mut`]
6/// (plus, optionally, a [`Transformable::pivot`]); every operation below is provided.
7///
8/// # Composition order
9///
10/// Each operation is post-multiplied onto the existing matrix, so it is applied in the object's
11/// *local* space — the frame established by the transforms already on it. Chaining therefore
12/// reads outside-in:
13///
14/// ```
15/// # use libdqg::transform::Transformable;
16/// # use libdqg::glam::{Mat4, Vec3};
17/// # struct Thing(Mat4);
18/// # impl Transformable for Thing {
19/// #     fn transform(&self) -> Mat4 { self.0 }
20/// #     fn transform_mut(&mut self) -> &mut Mat4 { &mut self.0 }
21/// # }
22/// # let mut thing = Thing(Mat4::IDENTITY);
23/// // Rotate a quarter turn, then move 10 units along the *rotated* X axis.
24/// thing.rotate(std::f32::consts::FRAC_PI_2).translate(10.0, 0.0);
25/// ```
26///
27/// Rotations and scales pivot around [`Transformable::pivot`], which defaults to the local
28/// origin. Translations are unaffected by the pivot. Use [`Transformable::set_transform`] when
29/// you want to drive the matrix directly instead of accumulating onto it.
30pub trait Transformable {
31    /// The current model matrix.
32    fn transform(&self) -> Mat4;
33
34    /// Mutable access to the model matrix.
35    fn transform_mut(&mut self) -> &mut Mat4;
36
37    /// Local-space point that rotations and scales turn around. Defaults to the local origin.
38    fn pivot(&self) -> Vec3 {
39        Vec3::ZERO
40    }
41
42    /// Replaces the model matrix outright, discarding any accumulated transforms.
43    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    /// Clears the model matrix back to the identity.
52    fn reset_transform(&mut self) -> &mut Self
53    where
54        Self: Sized,
55    {
56        self.set_transform(Mat4::IDENTITY)
57    }
58
59    /// Composes `transform` onto the current matrix, about the local origin.
60    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    /// Composes `transform` onto the current matrix, about [`Transformable::pivot`].
69    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    /// Moves along the local X and Y axes.
78    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    /// Moves by `offset` in local space.
86    fn translate3(&mut self, offset: Vec3) -> &mut Self
87    where
88        Self: Sized,
89    {
90        self.apply(Mat4::from_translation(offset))
91    }
92
93    /// Rotates around the Z axis — the in-plane spin for 2D content.
94    fn rotate(&mut self, radians: f32) -> &mut Self
95    where
96        Self: Sized,
97    {
98        self.rotate_z(radians)
99    }
100
101    /// [`Transformable::rotate`], taking degrees.
102    fn rotate_degrees(&mut self, degrees: f32) -> &mut Self
103    where
104        Self: Sized,
105    {
106        self.rotate(degrees.to_radians())
107    }
108
109    /// Rotates around the X axis.
110    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    /// Rotates around the Y axis.
118    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    /// Rotates around the Z axis.
126    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    /// Applies an arbitrary rotation.
134    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    /// Scales the X and Y axes independently.
142    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    /// Scales every axis by the same factor.
150    fn scale_uniform(&mut self, factor: f32) -> &mut Self
151    where
152        Self: Sized,
153    {
154        self.scale3(Vec3::splat(factor))
155    }
156
157    /// Scales by `scale` in local space.
158    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    /// Mirrors across the pivot's vertical axis.
166    fn flip_x(&mut self) -> &mut Self
167    where
168        Self: Sized,
169    {
170        self.scale(-1.0, 1.0)
171    }
172
173    /// Mirrors across the pivot's horizontal axis.
174    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        // A point one unit right of the pivot swings to one unit above it.
237        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        // The translation runs along the rotated X axis, which now points up.
266        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}