Skip to main content

plasma_prp/core/
transform.rs

1//! Transform propagation — parent-child coordinate hierarchy.
2//!
3//! plCoordinateInterface maintains local-to-parent and local-to-world matrices.
4//! When a parent moves, all children recompute their world transforms.
5//!
6//! C++ ref: pnSceneObject/plCoordinateInterface.h/.cpp
7
8use glam::{Mat4, Vec3};
9
10/// Multiply two 4x4 matrices stored as [f32; 16] in row-major order.
11/// hsMatrix44 is row-major: fMap[row][col], flat index = row*4+col.
12pub fn mat44_multiply(a: &[f32; 16], b: &[f32; 16]) -> [f32; 16] {
13    // glam::Mat4 is column-major, so we transpose on import/export
14    let ma = Mat4::from_cols_array(a).transpose();
15    let mb = Mat4::from_cols_array(b).transpose();
16    let result = ma * mb;
17    result.transpose().to_cols_array()
18}
19
20/// Extract translation from a row-major 4x4 matrix.
21/// Translation is at fMap[0][3]=m[3], fMap[1][3]=m[7], fMap[2][3]=m[11].
22pub fn mat44_translation(m: &[f32; 16]) -> Vec3 {
23    Vec3::new(m[3], m[7], m[11])
24}
25
26/// Create an identity 4x4 matrix.
27pub fn mat44_identity() -> [f32; 16] {
28    [
29        1.0, 0.0, 0.0, 0.0,
30        0.0, 1.0, 0.0, 0.0,
31        0.0, 0.0, 1.0, 0.0,
32        0.0, 0.0, 0.0, 1.0,
33    ]
34}
35
36/// Check if a matrix is identity.
37pub fn mat44_is_identity(m: &[f32; 16]) -> bool {
38    let id = mat44_identity();
39    m.iter()
40        .zip(id.iter())
41        .all(|(a, b)| (a - b).abs() < 1e-6)
42}
43
44/// Transform a point by a row-major 4x4 matrix.
45pub fn mat44_transform_point(m: &[f32; 16], p: Vec3) -> Vec3 {
46    Vec3::new(
47        p.x * m[0] + p.y * m[1] + p.z * m[2]  + m[3],
48        p.x * m[4] + p.y * m[5] + p.z * m[6]  + m[7],
49        p.x * m[8] + p.y * m[9] + p.z * m[10] + m[11],
50    )
51}
52
53/// Transform a direction (no translation) by a row-major 4x4 matrix.
54pub fn mat44_transform_direction(m: &[f32; 16], d: Vec3) -> Vec3 {
55    Vec3::new(
56        d.x * m[0] + d.y * m[1] + d.z * m[2],
57        d.x * m[4] + d.y * m[5] + d.z * m[6],
58        d.x * m[8] + d.y * m[9] + d.z * m[10],
59    )
60}
61
62/// Propagate transforms through a parent-child hierarchy.
63///
64/// Given a parent's local_to_world matrix and a child's local_to_parent matrix,
65/// compute the child's local_to_world.
66pub fn propagate_transform(
67    parent_local_to_world: &[f32; 16],
68    child_local_to_parent: &[f32; 16],
69) -> [f32; 16] {
70    mat44_multiply(parent_local_to_world, child_local_to_parent)
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76
77    #[test]
78    fn test_identity() {
79        let id = mat44_identity();
80        assert!(mat44_is_identity(&id));
81    }
82
83    #[test]
84    fn test_transform_point() {
85        // Row-major translation matrix: translate by (1, 2, 3)
86        // fMap[0][3]=1, fMap[1][3]=2, fMap[2][3]=3
87        let m = [
88            1.0, 0.0, 0.0, 1.0,  // row 0: m[3] = translate X
89            0.0, 1.0, 0.0, 2.0,  // row 1: m[7] = translate Y
90            0.0, 0.0, 1.0, 3.0,  // row 2: m[11] = translate Z
91            0.0, 0.0, 0.0, 1.0,  // row 3
92        ];
93        let p = Vec3::new(0.0, 0.0, 0.0);
94        let result = mat44_transform_point(&m, p);
95        assert!((result.x - 1.0).abs() < 0.01);
96        assert!((result.y - 2.0).abs() < 0.01);
97        assert!((result.z - 3.0).abs() < 0.01);
98    }
99
100    #[test]
101    fn test_propagate() {
102        // Row-major: translation in column 3 of each row
103        let parent = [
104            1.0, 0.0, 0.0, 10.0,  // translate X=10
105            0.0, 1.0, 0.0, 0.0,
106            0.0, 0.0, 1.0, 0.0,
107            0.0, 0.0, 0.0, 1.0,
108        ];
109        let child = [
110            1.0, 0.0, 0.0, 5.0,   // translate X=5
111            0.0, 1.0, 0.0, 0.0,
112            0.0, 0.0, 1.0, 0.0,
113            0.0, 0.0, 0.0, 1.0,
114        ];
115        let result = propagate_transform(&parent, &child);
116        let pos = mat44_translation(&result);
117        assert!((pos.x - 15.0).abs() < 0.01);
118    }
119}