maple_engine 0.3.0

Engine implementation of maple engine
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
//! 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.

use glam::{Mat4, Quat, Vec3};

/// Represents a nodes transform data in 3d space with position, rotation, and scale as well as a precalculated model matrix.
#[derive(Clone, Copy)]
pub struct NodeTransform {
    /// position in 3D space with y as up.
    pub position: Vec3,
    /// rotation in quaternion form.
    pub rotation: Quat,
    /// scale in 3D space.
    pub scale: Vec3,
    /// precalculated model matrix.
    pub matrix: Mat4,
    /// readonly field that stores the nodes position in world space
    world_transform: WorldTransform,
}

/// represents a position in worldspace
#[derive(Clone, Copy)]
pub struct WorldTransform {
    /// position in worldspace
    pub position: Vec3,
    /// rotation in worldspace
    pub rotation: Quat,
    /// scale in worldspace
    pub scale: Vec3,
    /// matrix of the world position
    pub matrix: Mat4,
}

impl Default for WorldTransform {
    fn default() -> Self {
        let mut out = Self {
            position: Vec3::ZERO,
            rotation: Quat::IDENTITY,
            scale: Vec3::ONE,
            matrix: Mat4::IDENTITY,
        };

        out.update_matrix();
        out
    }
}

impl std::ops::Add for NodeTransform {
    type Output = NodeTransform;

    fn add(self, rhs: Self) -> Self::Output {
        let rotated_position = self.rotation * rhs.position; // position relative to parent space
        let position = self.position + rotated_position * self.scale; // scale relative to parent space scale
        let rotation = (self.rotation * rhs.rotation).normalize();
        let scale = self.scale * rhs.scale;

        Self::new(position, rotation, scale)
    }
}

// same thing but for world transform
impl std::ops::Add for WorldTransform {
    type Output = WorldTransform;

    fn add(self, rhs: Self) -> Self::Output {
        let rotated_position = self.rotation * rhs.position; // position relative to parent space
        let position = self.position + rotated_position * self.scale; // scale relative to parent space scale
        let rotation = (self.rotation * rhs.rotation).normalize();
        let scale = self.scale * rhs.scale;

        let mut result = Self {
            position,
            rotation,
            scale,
            matrix: Mat4::IDENTITY,
        };

        result.update_matrix();
        result
    }
}

impl WorldTransform {
    /// updates the model matrix based on the position, rotation, and scale.
    fn update_matrix(&mut self) {
        self.matrix =
            Mat4::from_scale_rotation_translation(self.scale, self.rotation, self.position);
    }
}

impl Default for NodeTransform {
    /// the default constructor for NodeTransform sets the position to (0, 0, 0), rotation to identity, scale to (1, 1, 1), and matrix to identity.
    fn default() -> Self {
        let mut transform = Self {
            position: Vec3::ZERO,
            rotation: Quat::IDENTITY,
            scale: Vec3::ONE,
            matrix: Mat4::IDENTITY,
            world_transform: WorldTransform::default(),
        };
        transform.update_matrix();
        transform
    }
}

impl PartialEq for NodeTransform {
    /// compares two NodeTransforms by their position, rotation, scale, and matrix.
    fn eq(&self, other: &Self) -> bool {
        self.position == other.position
            && self.rotation == other.rotation
            && self.scale == other.scale
            && self.matrix == other.matrix
    }
}

impl std::fmt::Debug for NodeTransform {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "Local => Position: {:?}, Rotation: {:?}, Scale: {:?}, Matrix: {:?}\n\
             World => Position: {:?}, Rotation: {:?}, Scale: {:?}",
            self.position,
            self.rotation,
            self.scale,
            self.matrix,
            self.world_transform.position,
            self.world_transform.rotation,
            self.world_transform.scale
        )
    }
}

impl NodeTransform {
    /// constructs a new NodeTransform with the given position, rotation, and scale.
    ///
    /// # Arguments
    /// - `position` - the position in 3D space.
    /// - `rotation` - the rotation in quaternion form.
    /// - `scale` - the scale in 3D space.
    ///
    /// # Returns
    /// a new NodeTransform with the given position, rotation, and scale.
    pub fn new(position: impl Into<Vec3>, rotation: Quat, scale: impl Into<Vec3>) -> Self {
        let mut transform = Self {
            position: position.into(),
            rotation,
            scale: scale.into(),
            matrix: Mat4::IDENTITY,
            world_transform: WorldTransform::default(),
        };
        transform.update_matrix();
        transform
    }

    /// updates the model matrix based on the position, rotation, and scale.
    fn update_matrix(&mut self) {
        self.matrix =
            Mat4::from_scale_rotation_translation(self.scale, self.rotation, self.position);
    }

    /// returns the world space of the object
    ///
    /// this is not meant to be modified and will not update when you modify localspace
    pub fn world_space(&self) -> &WorldTransform {
        &self.world_transform
    }

    /// get the world space transform of the transform
    ///
    /// useful if you need to know where a node is in the world
    pub fn get_world_space(&mut self, parent_space: WorldTransform) {
        // we need to add self to the worldspace to get the current objects worldspace
        // the current worldspace is considered dirty so we cant use self.worldspace as this is
        // called after localspace has been modified
        let local_world_space = WorldTransform {
            position: self.position,
            rotation: self.rotation,
            scale: self.scale,
            matrix: self.matrix,
        };

        self.world_transform = parent_space + local_world_space;
        self.world_transform.update_matrix();
    }

    /// gets the position of the transform.
    ///
    /// # Returns
    /// the position in 3D space.
    pub fn get_position(&self) -> &Vec3 {
        &self.position
    }

    /// gets a mutible position
    pub fn get_position_mut(&mut self) -> &mut Vec3 {
        &mut self.position
    }

    /// linarly interpolate the transform between 2 transforms and a t value
    pub fn lerp(a: &Self, b: &Self, t: f32) -> Self {
        let position = a.position.lerp(b.position, t);
        let rotation = a.rotation.slerp(b.rotation, t);
        let scale = a.scale.lerp(b.scale, t);

        Self::new(position, rotation, scale)
    }

    /// sets the position of the transform.
    ///
    /// # Arguments
    /// - `position` - the new position in 3D space.
    ///
    /// # Returns
    /// a mutable reference to the NodeTransform.
    pub fn set_position(&mut self, position: impl Into<Vec3>) -> &mut Self {
        self.position = position.into();
        self.update_matrix();
        self
    }

    /// gets the rotation of the transform.
    ///
    /// # Returns
    /// the rotation in quaternion form.
    pub fn get_rotation(&self) -> &Quat {
        &self.rotation
    }

    /// returns a mutible refrence to the rotation quat
    pub fn get_rotation_mut(&mut self) -> &mut Quat {
        &mut self.rotation
    }

    /// gets the rotation of the transform as euler angles in degrees.
    ///
    /// # Returns
    /// the rotation as euler angles in degrees.
    pub fn get_rotation_euler_xyz(&self) -> Vec3 {
        let (x, y, z) = self.rotation.to_euler(glam::EulerRot::XYZ);
        Vec3::new(x.to_degrees(), y.to_degrees(), z.to_degrees())
    }

    /// sets the rotation of the transform.
    ///
    /// # Arguments
    /// - `rotation` - the new rotation in quaternion form.
    ///
    /// # Returns
    /// a mutable reference to the NodeTransform.
    pub fn set_rotation(&mut self, rotation: Quat) -> &mut Self {
        self.rotation = rotation;
        self.update_matrix();
        self
    }

    /// sets the rotation of the transform as euler angles in degrees in xyz order.
    ///
    /// # Arguments
    /// - `degrees` - the new rotation as euler angles in degrees.
    ///
    /// # Returns
    /// a mutable reference to the NodeTransform.
    pub fn set_euler_xyz(&mut self, degrees: impl Into<Vec3>) -> &mut Self {
        let degrees = degrees.into();

        self.rotation = Quat::from_euler(
            glam::EulerRot::XYZ,
            degrees.x.to_radians(),
            degrees.y.to_radians(),
            degrees.z.to_radians(),
        );
        self.update_matrix();
        self
    }

    /// gets the scale of the transform.
    ///
    /// # Returns
    /// the scale in 3D space.
    pub fn get_scale(&self) -> &Vec3 {
        &self.scale
    }

    /// get a mutible refrence to the scale
    pub fn get_scale_mut(&mut self) -> &mut Vec3 {
        &mut self.scale
    }

    /// sets the scale of the transform.
    /// # Arguments
    /// - `scale` - the new scale in 3D space.
    ///
    /// # Returns
    /// a mutable reference to the NodeTransform.
    pub fn set_scale(&mut self, scale: impl Into<Vec3>) -> &mut Self {
        self.scale = scale.into();
        self.update_matrix();
        self
    }

    /// gets the forward vector of the transform.
    ///
    /// # Returns
    /// the forward vector of the transform.
    pub fn get_forward_vector(&self) -> Vec3 {
        self.rotation * Vec3::Z
    }

    /// gets the right vector of the transform.
    ///
    /// # Returns
    /// the right vector of the transform.
    pub fn get_right_vector(&self) -> Vec3 {
        self.rotation * Vec3::X
    }

    /// gets the up vector of the transform.
    ///
    /// # Returns
    /// the up vector of the transform.
    pub fn get_up_vector(&self) -> Vec3 {
        self.rotation * Vec3::Y
    }

    /// scales the transform by the given scale.
    ///
    /// # Arguments
    /// - `scale` - the scale to multiply the current scale by.
    ///
    /// # Returns
    /// a mutable reference to the NodeTransform.
    pub fn scale(&mut self, scale: impl Into<Vec3>) -> &mut Self {
        self.scale *= scale.into();
        self.update_matrix();
        self
    }

    /// translates the position of the transform by the given translation.
    ///
    /// # Arguments
    /// - `translation` - the translation to add to the current position.
    ///
    /// # Returns
    /// a mutable reference to the NodeTransform.
    pub fn translate(&mut self, translation: impl Into<Vec3>) -> &mut Self {
        self.position += translation.into();
        self.update_matrix();
        self
    }

    /// translates the position of the transform by the given translation in world space.
    /// This ignores the objects rotation when moving,
    ///
    /// # Arguments
    /// - `translation` - the translation to add to the current position.
    pub fn translate_world_space(&mut self, translation: impl Into<Vec3>) -> &mut Self {
        self.position += self.rotation * translation.into();
        self.update_matrix();
        self
    }

    /// rotates the transform by the given axis and degrees.
    ///
    /// # Arguments
    /// - `axis` - the axis to rotate around.
    /// - `degrees` - the degrees to rotate by.
    ///
    /// # Returns
    /// a mutable reference to the NodeTransform.
    pub fn rotate(&mut self, axis: impl Into<Vec3>, degrees: f32) -> &mut Self {
        let angle_quat = Quat::from_axis_angle(axis.into(), degrees.to_radians());
        self.rotation = angle_quat * self.rotation;
        self.update_matrix();
        self
    }

    /// rotates the transform by the given euler angles in degrees in xyz order.
    ///
    /// # Arguments
    /// - `degrees` - the euler angles in degrees to rotate by.
    ///
    /// # Returns
    /// a mutable reference to the NodeTransform.
    pub fn rotate_euler_xyz(&mut self, degrees: impl Into<Vec3>) -> &mut Self {
        let degrees = degrees.into();

        let euler_quat = Quat::from_euler(
            glam::EulerRot::XYZ,
            degrees.x.to_radians(),
            degrees.y.to_radians(),
            degrees.z.to_radians(),
        );
        self.rotation = euler_quat * self.rotation;
        self.update_matrix();
        self
    }
}

impl From<NodeTransform> for WorldTransform {
    fn from(value: NodeTransform) -> Self {
        let mut out = Self {
            position: value.position,
            rotation: value.rotation,
            scale: value.scale,
            matrix: Mat4::IDENTITY,
        };
        out.update_matrix();
        out
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use glam::{Mat4, Quat, Vec3};

    #[test]
    fn test_default_transform() {
        let transform = NodeTransform::default();
        assert_eq!(transform.position, Vec3::ZERO);
        assert_eq!(transform.rotation, Quat::IDENTITY);
        assert_eq!(transform.scale, Vec3::ONE);
        assert_eq!(transform.matrix, Mat4::IDENTITY);
    }

    #[test]
    fn test_translation() {
        let mut transform = NodeTransform::default();
        transform.translate(Vec3::new(1.0, 2.0, 3.0));
        assert_eq!(transform.position, Vec3::new(1.0, 2.0, 3.0));
    }

    #[test]
    fn test_rotation() {
        let mut transform = NodeTransform::default();
        transform.rotate(Vec3::Y, 90.0);
        let expected_rotation = Quat::from_axis_angle(Vec3::Y, 90.0_f32.to_radians());
        assert_eq!(transform.rotation, expected_rotation);
    }

    #[test]
    fn test_scaling() {
        let mut transform = NodeTransform::default();
        transform.scale(Vec3::new(2.0, 3.0, 4.0));
        assert_eq!(transform.scale, Vec3::new(2.0, 3.0, 4.0));
    }

    #[test]
    fn test_model_matrix_update() {
        let mut transform = NodeTransform::default();
        transform.set_position(Vec3::new(1.0, 2.0, 3.0));
        transform.set_scale(Vec3::new(2.0, 2.0, 2.0));
        transform.set_rotation(Quat::from_axis_angle(Vec3::Y, 45.0_f32.to_radians()));

        let expected_matrix = Mat4::from_scale_rotation_translation(
            transform.scale,
            transform.rotation,
            transform.position,
        );
        assert_eq!(transform.matrix, expected_matrix);
    }

    #[test]
    fn test_add_transform() {
        const EPSILON: f32 = 1e-5;

        fn approx_eq(v1: &Vec3, v2: &Vec3) -> bool {
            (*v1 - *v2).length() < EPSILON
        }

        fn approx_eq_quat(q1: &Quat, q2: &Quat) -> bool {
            q1.dot(*q2).abs() > 1.0 - EPSILON
        }

        let transform1 = NodeTransform::new(
            Vec3::new(1.0, 0.0, 0.0),
            Quat::from_axis_angle(Vec3::Y, 90.0_f32.to_radians()),
            Vec3::new(2.0, 2.0, 2.0),
        );

        let transform2 = NodeTransform::new(
            Vec3::new(0.0, 1.0, 0.0),
            Quat::from_axis_angle(Vec3::X, 90.0_f32.to_radians()),
            Vec3::new(0.5, 0.5, 0.5),
        );

        let result = transform1 + transform2;

        let expected_position = Vec3::new(1.0, 2.0, 0.0);
        let expected_rotation = (transform1.rotation * transform2.rotation).normalize();
        let expected_scale = Vec3::new(1.0, 1.0, 1.0);

        assert!(
            approx_eq(&result.position, &expected_position),
            "position: {:?} != {:?}",
            result.position,
            expected_position
        );
        assert!(
            approx_eq_quat(&result.rotation, &expected_rotation),
            "rotation: {:?} != {:?}",
            result.rotation,
            expected_rotation
        );
        assert!(
            approx_eq(&result.scale, &expected_scale),
            "scale: {:?} != {:?}",
            result.scale,
            expected_scale
        );
    }

    #[test]
    fn test_euler_rotation() {
        let mut transform = NodeTransform::default();
        transform.set_euler_xyz(Vec3::new(90.0, 0.0, 0.0));

        let expected_rotation = Quat::from_axis_angle(Vec3::X, 90.0_f32.to_radians());
        assert_eq!(transform.rotation, expected_rotation);
    }

    #[test]
    fn test_get_euler() {
        let mut transform = NodeTransform::default();
        transform.set_euler_xyz(Vec3::new(90.0, 0.0, 0.0));

        let result = transform.get_rotation_euler_xyz();
        let expected = Vec3::new(90.0, 0.0, 0.0);

        // Compare with epsilon
        const EPSILON: f32 = 0.001; // Slightly larger epsilon for euler angle conversion
        assert!(
            (result.x - expected.x).abs() < EPSILON
                && (result.y - expected.y).abs() < EPSILON
                && (result.z - expected.z).abs() < EPSILON,
            "Expected approximately {:?}, got {:?}",
            expected,
            result
        );
    }
}