Skip to main content

maple_engine/components/
node_transform.rs

1//! represents the current transform of a given node. each node has a transform that can be manipulated to move, rotate, and scale the node in 3D space.
2
3use glam::{Mat4, Quat, Vec3};
4
5/// Represents a nodes transform data in 3d space with position, rotation, and scale as well as a precalculated model matrix.
6#[derive(Clone, Copy)]
7pub struct NodeTransform {
8    /// position in 3D space with y as up.
9    pub position: Vec3,
10    /// rotation in quaternion form.
11    pub rotation: Quat,
12    /// scale in 3D space.
13    pub scale: Vec3,
14    /// precalculated model matrix.
15    pub matrix: Mat4,
16    /// readonly field that stores the nodes position in world space
17    world_transform: WorldTransform,
18}
19
20/// represents a position in worldspace
21#[derive(Clone, Copy)]
22pub struct WorldTransform {
23    /// position in worldspace
24    pub position: Vec3,
25    /// rotation in worldspace
26    pub rotation: Quat,
27    /// scale in worldspace
28    pub scale: Vec3,
29    /// matrix of the world position
30    pub matrix: Mat4,
31}
32
33impl Default for WorldTransform {
34    fn default() -> Self {
35        let mut out = Self {
36            position: Vec3::ZERO,
37            rotation: Quat::IDENTITY,
38            scale: Vec3::ONE,
39            matrix: Mat4::IDENTITY,
40        };
41
42        out.update_matrix();
43        out
44    }
45}
46
47impl std::ops::Add for NodeTransform {
48    type Output = NodeTransform;
49
50    fn add(self, rhs: Self) -> Self::Output {
51        let rotated_position = self.rotation * rhs.position; // position relative to parent space
52        let position = self.position + rotated_position * self.scale; // scale relative to parent space scale
53        let rotation = (self.rotation * rhs.rotation).normalize();
54        let scale = self.scale * rhs.scale;
55
56        Self::new(position, rotation, scale)
57    }
58}
59
60// same thing but for world transform
61impl std::ops::Add for WorldTransform {
62    type Output = WorldTransform;
63
64    fn add(self, rhs: Self) -> Self::Output {
65        let rotated_position = self.rotation * rhs.position; // position relative to parent space
66        let position = self.position + rotated_position * self.scale; // scale relative to parent space scale
67        let rotation = (self.rotation * rhs.rotation).normalize();
68        let scale = self.scale * rhs.scale;
69
70        let mut result = Self {
71            position,
72            rotation,
73            scale,
74            matrix: Mat4::IDENTITY,
75        };
76
77        result.update_matrix();
78        result
79    }
80}
81
82impl WorldTransform {
83    /// updates the model matrix based on the position, rotation, and scale.
84    fn update_matrix(&mut self) {
85        self.matrix =
86            Mat4::from_scale_rotation_translation(self.scale, self.rotation, self.position);
87    }
88}
89
90impl Default for NodeTransform {
91    /// the default constructor for NodeTransform sets the position to (0, 0, 0), rotation to identity, scale to (1, 1, 1), and matrix to identity.
92    fn default() -> Self {
93        let mut transform = Self {
94            position: Vec3::ZERO,
95            rotation: Quat::IDENTITY,
96            scale: Vec3::ONE,
97            matrix: Mat4::IDENTITY,
98            world_transform: WorldTransform::default(),
99        };
100        transform.update_matrix();
101        transform
102    }
103}
104
105impl PartialEq for NodeTransform {
106    /// compares two NodeTransforms by their position, rotation, scale, and matrix.
107    fn eq(&self, other: &Self) -> bool {
108        self.position == other.position
109            && self.rotation == other.rotation
110            && self.scale == other.scale
111            && self.matrix == other.matrix
112    }
113}
114
115impl std::fmt::Debug for NodeTransform {
116    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
117        write!(
118            f,
119            "Local => Position: {:?}, Rotation: {:?}, Scale: {:?}, Matrix: {:?}\n\
120             World => Position: {:?}, Rotation: {:?}, Scale: {:?}",
121            self.position,
122            self.rotation,
123            self.scale,
124            self.matrix,
125            self.world_transform.position,
126            self.world_transform.rotation,
127            self.world_transform.scale
128        )
129    }
130}
131
132impl NodeTransform {
133    /// constructs a new NodeTransform with the given position, rotation, and scale.
134    ///
135    /// # Arguments
136    /// - `position` - the position in 3D space.
137    /// - `rotation` - the rotation in quaternion form.
138    /// - `scale` - the scale in 3D space.
139    ///
140    /// # Returns
141    /// a new NodeTransform with the given position, rotation, and scale.
142    pub fn new(position: impl Into<Vec3>, rotation: Quat, scale: impl Into<Vec3>) -> Self {
143        let mut transform = Self {
144            position: position.into(),
145            rotation,
146            scale: scale.into(),
147            matrix: Mat4::IDENTITY,
148            world_transform: WorldTransform::default(),
149        };
150        transform.update_matrix();
151        transform
152    }
153
154    /// updates the model matrix based on the position, rotation, and scale.
155    fn update_matrix(&mut self) {
156        self.matrix =
157            Mat4::from_scale_rotation_translation(self.scale, self.rotation, self.position);
158    }
159
160    /// returns the world space of the object
161    ///
162    /// this is not meant to be modified and will not update when you modify localspace
163    pub fn world_space(&self) -> &WorldTransform {
164        &self.world_transform
165    }
166
167    /// get the world space transform of the transform
168    ///
169    /// useful if you need to know where a node is in the world
170    pub fn get_world_space(&mut self, parent_space: WorldTransform) {
171        // we need to add self to the worldspace to get the current objects worldspace
172        // the current worldspace is considered dirty so we cant use self.worldspace as this is
173        // called after localspace has been modified
174        let local_world_space = WorldTransform {
175            position: self.position,
176            rotation: self.rotation,
177            scale: self.scale,
178            matrix: self.matrix,
179        };
180
181        self.world_transform = parent_space + local_world_space;
182        self.world_transform.update_matrix();
183    }
184
185    /// gets the position of the transform.
186    ///
187    /// # Returns
188    /// the position in 3D space.
189    pub fn get_position(&self) -> &Vec3 {
190        &self.position
191    }
192
193    /// gets a mutible position
194    pub fn get_position_mut(&mut self) -> &mut Vec3 {
195        &mut self.position
196    }
197
198    /// linarly interpolate the transform between 2 transforms and a t value
199    pub fn lerp(a: &Self, b: &Self, t: f32) -> Self {
200        let position = a.position.lerp(b.position, t);
201        let rotation = a.rotation.slerp(b.rotation, t);
202        let scale = a.scale.lerp(b.scale, t);
203
204        Self::new(position, rotation, scale)
205    }
206
207    /// sets the position of the transform.
208    ///
209    /// # Arguments
210    /// - `position` - the new position in 3D space.
211    ///
212    /// # Returns
213    /// a mutable reference to the NodeTransform.
214    pub fn set_position(&mut self, position: impl Into<Vec3>) -> &mut Self {
215        self.position = position.into();
216        self.update_matrix();
217        self
218    }
219
220    /// gets the rotation of the transform.
221    ///
222    /// # Returns
223    /// the rotation in quaternion form.
224    pub fn get_rotation(&self) -> &Quat {
225        &self.rotation
226    }
227
228    /// returns a mutible refrence to the rotation quat
229    pub fn get_rotation_mut(&mut self) -> &mut Quat {
230        &mut self.rotation
231    }
232
233    /// gets the rotation of the transform as euler angles in degrees.
234    ///
235    /// # Returns
236    /// the rotation as euler angles in degrees.
237    pub fn get_rotation_euler_xyz(&self) -> Vec3 {
238        let (x, y, z) = self.rotation.to_euler(glam::EulerRot::XYZ);
239        Vec3::new(x.to_degrees(), y.to_degrees(), z.to_degrees())
240    }
241
242    /// sets the rotation of the transform.
243    ///
244    /// # Arguments
245    /// - `rotation` - the new rotation in quaternion form.
246    ///
247    /// # Returns
248    /// a mutable reference to the NodeTransform.
249    pub fn set_rotation(&mut self, rotation: Quat) -> &mut Self {
250        self.rotation = rotation;
251        self.update_matrix();
252        self
253    }
254
255    /// sets the rotation of the transform as euler angles in degrees in xyz order.
256    ///
257    /// # Arguments
258    /// - `degrees` - the new rotation as euler angles in degrees.
259    ///
260    /// # Returns
261    /// a mutable reference to the NodeTransform.
262    pub fn set_euler_xyz(&mut self, degrees: impl Into<Vec3>) -> &mut Self {
263        let degrees = degrees.into();
264
265        self.rotation = Quat::from_euler(
266            glam::EulerRot::XYZ,
267            degrees.x.to_radians(),
268            degrees.y.to_radians(),
269            degrees.z.to_radians(),
270        );
271        self.update_matrix();
272        self
273    }
274
275    /// gets the scale of the transform.
276    ///
277    /// # Returns
278    /// the scale in 3D space.
279    pub fn get_scale(&self) -> &Vec3 {
280        &self.scale
281    }
282
283    /// get a mutible refrence to the scale
284    pub fn get_scale_mut(&mut self) -> &mut Vec3 {
285        &mut self.scale
286    }
287
288    /// sets the scale of the transform.
289    /// # Arguments
290    /// - `scale` - the new scale in 3D space.
291    ///
292    /// # Returns
293    /// a mutable reference to the NodeTransform.
294    pub fn set_scale(&mut self, scale: impl Into<Vec3>) -> &mut Self {
295        self.scale = scale.into();
296        self.update_matrix();
297        self
298    }
299
300    /// gets the forward vector of the transform.
301    ///
302    /// # Returns
303    /// the forward vector of the transform.
304    pub fn get_forward_vector(&self) -> Vec3 {
305        self.rotation * Vec3::Z
306    }
307
308    /// gets the right vector of the transform.
309    ///
310    /// # Returns
311    /// the right vector of the transform.
312    pub fn get_right_vector(&self) -> Vec3 {
313        self.rotation * Vec3::X
314    }
315
316    /// gets the up vector of the transform.
317    ///
318    /// # Returns
319    /// the up vector of the transform.
320    pub fn get_up_vector(&self) -> Vec3 {
321        self.rotation * Vec3::Y
322    }
323
324    /// scales the transform by the given scale.
325    ///
326    /// # Arguments
327    /// - `scale` - the scale to multiply the current scale by.
328    ///
329    /// # Returns
330    /// a mutable reference to the NodeTransform.
331    pub fn scale(&mut self, scale: impl Into<Vec3>) -> &mut Self {
332        self.scale *= scale.into();
333        self.update_matrix();
334        self
335    }
336
337    /// translates the position of the transform by the given translation.
338    ///
339    /// # Arguments
340    /// - `translation` - the translation to add to the current position.
341    ///
342    /// # Returns
343    /// a mutable reference to the NodeTransform.
344    pub fn translate(&mut self, translation: impl Into<Vec3>) -> &mut Self {
345        self.position += translation.into();
346        self.update_matrix();
347        self
348    }
349
350    /// translates the position of the transform by the given translation in world space.
351    /// This ignores the objects rotation when moving,
352    ///
353    /// # Arguments
354    /// - `translation` - the translation to add to the current position.
355    pub fn translate_world_space(&mut self, translation: impl Into<Vec3>) -> &mut Self {
356        self.position += self.rotation * translation.into();
357        self.update_matrix();
358        self
359    }
360
361    /// rotates the transform by the given axis and degrees.
362    ///
363    /// # Arguments
364    /// - `axis` - the axis to rotate around.
365    /// - `degrees` - the degrees to rotate by.
366    ///
367    /// # Returns
368    /// a mutable reference to the NodeTransform.
369    pub fn rotate(&mut self, axis: impl Into<Vec3>, degrees: f32) -> &mut Self {
370        let angle_quat = Quat::from_axis_angle(axis.into(), degrees.to_radians());
371        self.rotation = angle_quat * self.rotation;
372        self.update_matrix();
373        self
374    }
375
376    /// rotates the transform by the given euler angles in degrees in xyz order.
377    ///
378    /// # Arguments
379    /// - `degrees` - the euler angles in degrees to rotate by.
380    ///
381    /// # Returns
382    /// a mutable reference to the NodeTransform.
383    pub fn rotate_euler_xyz(&mut self, degrees: impl Into<Vec3>) -> &mut Self {
384        let degrees = degrees.into();
385
386        let euler_quat = Quat::from_euler(
387            glam::EulerRot::XYZ,
388            degrees.x.to_radians(),
389            degrees.y.to_radians(),
390            degrees.z.to_radians(),
391        );
392        self.rotation = euler_quat * self.rotation;
393        self.update_matrix();
394        self
395    }
396}
397
398impl From<NodeTransform> for WorldTransform {
399    fn from(value: NodeTransform) -> Self {
400        let mut out = Self {
401            position: value.position,
402            rotation: value.rotation,
403            scale: value.scale,
404            matrix: Mat4::IDENTITY,
405        };
406        out.update_matrix();
407        out
408    }
409}
410
411#[cfg(test)]
412mod tests {
413    use super::*;
414    use glam::{Mat4, Quat, Vec3};
415
416    #[test]
417    fn test_default_transform() {
418        let transform = NodeTransform::default();
419        assert_eq!(transform.position, Vec3::ZERO);
420        assert_eq!(transform.rotation, Quat::IDENTITY);
421        assert_eq!(transform.scale, Vec3::ONE);
422        assert_eq!(transform.matrix, Mat4::IDENTITY);
423    }
424
425    #[test]
426    fn test_translation() {
427        let mut transform = NodeTransform::default();
428        transform.translate(Vec3::new(1.0, 2.0, 3.0));
429        assert_eq!(transform.position, Vec3::new(1.0, 2.0, 3.0));
430    }
431
432    #[test]
433    fn test_rotation() {
434        let mut transform = NodeTransform::default();
435        transform.rotate(Vec3::Y, 90.0);
436        let expected_rotation = Quat::from_axis_angle(Vec3::Y, 90.0_f32.to_radians());
437        assert_eq!(transform.rotation, expected_rotation);
438    }
439
440    #[test]
441    fn test_scaling() {
442        let mut transform = NodeTransform::default();
443        transform.scale(Vec3::new(2.0, 3.0, 4.0));
444        assert_eq!(transform.scale, Vec3::new(2.0, 3.0, 4.0));
445    }
446
447    #[test]
448    fn test_model_matrix_update() {
449        let mut transform = NodeTransform::default();
450        transform.set_position(Vec3::new(1.0, 2.0, 3.0));
451        transform.set_scale(Vec3::new(2.0, 2.0, 2.0));
452        transform.set_rotation(Quat::from_axis_angle(Vec3::Y, 45.0_f32.to_radians()));
453
454        let expected_matrix = Mat4::from_scale_rotation_translation(
455            transform.scale,
456            transform.rotation,
457            transform.position,
458        );
459        assert_eq!(transform.matrix, expected_matrix);
460    }
461
462    #[test]
463    fn test_add_transform() {
464        const EPSILON: f32 = 1e-5;
465
466        fn approx_eq(v1: &Vec3, v2: &Vec3) -> bool {
467            (*v1 - *v2).length() < EPSILON
468        }
469
470        fn approx_eq_quat(q1: &Quat, q2: &Quat) -> bool {
471            q1.dot(*q2).abs() > 1.0 - EPSILON
472        }
473
474        let transform1 = NodeTransform::new(
475            Vec3::new(1.0, 0.0, 0.0),
476            Quat::from_axis_angle(Vec3::Y, 90.0_f32.to_radians()),
477            Vec3::new(2.0, 2.0, 2.0),
478        );
479
480        let transform2 = NodeTransform::new(
481            Vec3::new(0.0, 1.0, 0.0),
482            Quat::from_axis_angle(Vec3::X, 90.0_f32.to_radians()),
483            Vec3::new(0.5, 0.5, 0.5),
484        );
485
486        let result = transform1 + transform2;
487
488        let expected_position = Vec3::new(1.0, 2.0, 0.0);
489        let expected_rotation = (transform1.rotation * transform2.rotation).normalize();
490        let expected_scale = Vec3::new(1.0, 1.0, 1.0);
491
492        assert!(
493            approx_eq(&result.position, &expected_position),
494            "position: {:?} != {:?}",
495            result.position,
496            expected_position
497        );
498        assert!(
499            approx_eq_quat(&result.rotation, &expected_rotation),
500            "rotation: {:?} != {:?}",
501            result.rotation,
502            expected_rotation
503        );
504        assert!(
505            approx_eq(&result.scale, &expected_scale),
506            "scale: {:?} != {:?}",
507            result.scale,
508            expected_scale
509        );
510    }
511
512    #[test]
513    fn test_euler_rotation() {
514        let mut transform = NodeTransform::default();
515        transform.set_euler_xyz(Vec3::new(90.0, 0.0, 0.0));
516
517        let expected_rotation = Quat::from_axis_angle(Vec3::X, 90.0_f32.to_radians());
518        assert_eq!(transform.rotation, expected_rotation);
519    }
520
521    #[test]
522    fn test_get_euler() {
523        let mut transform = NodeTransform::default();
524        transform.set_euler_xyz(Vec3::new(90.0, 0.0, 0.0));
525
526        let result = transform.get_rotation_euler_xyz();
527        let expected = Vec3::new(90.0, 0.0, 0.0);
528
529        // Compare with epsilon
530        const EPSILON: f32 = 0.001; // Slightly larger epsilon for euler angle conversion
531        assert!(
532            (result.x - expected.x).abs() < EPSILON
533                && (result.y - expected.y).abs() < EPSILON
534                && (result.z - expected.z).abs() < EPSILON,
535            "Expected approximately {:?}, got {:?}",
536            expected,
537            result
538        );
539    }
540}