Skip to main content

optic_render/util/transform/
trans3d.rs

1use cgmath::*;
2
3/// A 3D transform with position, rotation (Euler angles in degrees), and scale.
4///
5/// The transform maintains a 4×4 TRS matrix (translate × rotate × scale). Call
6/// [`calc_matrix`](Transform3D::calc_matrix) after mutating to recompute it.
7///
8/// # Operations
9///
10/// | Category | Methods |
11/// |---|---|
12/// | **Position** — getter | [`pos`](Transform3D::pos) |
13/// | **Position** — absolute setter | [`set_pos_all`](Transform3D::set_pos_all), [`set_pos_x`](Transform3D::set_pos_x), [`set_pos_y`](Transform3D::set_pos_y), [`set_pos_z`](Transform3D::set_pos_z) |
14/// | **Position** — relative move | [`move_all`](Transform3D::move_all), [`move_x`](Transform3D::move_x), [`move_y`](Transform3D::move_y), [`move_z`](Transform3D::move_z) |
15/// | **Rotation** — getter | [`rot`](Transform3D::rot) |
16/// | **Rotation** — absolute setter | [`set_rot_all`](Transform3D::set_rot_all), [`set_rot_x`](Transform3D::set_rot_x), [`set_rot_y`](Transform3D::set_rot_y), [`set_rot_z`](Transform3D::set_rot_z) |
17/// | **Rotation** — relative add | [`rotate_all`](Transform3D::rotate_all), [`rotate_x`](Transform3D::rotate_x), [`rotate_y`](Transform3D::rotate_y), [`rotate_z`](Transform3D::rotate_z) |
18/// | **Scale** — getter | [`scale`](Transform3D::scale) |
19/// | **Scale** — absolute setter | [`set_scale_all`](Transform3D::set_scale_all), [`set_scale_same`](Transform3D::set_scale_same), [`set_scale_x`](Transform3D::set_scale_x), [`set_scale_y`](Transform3D::set_scale_y), [`set_scale_z`](Transform3D::set_scale_z) |
20/// | **Scale** — relative add | [`scale_all`](Transform3D::scale_all), [`scale_same`](Transform3D::scale_same), [`scale_x`](Transform3D::scale_x), [`scale_y`](Transform3D::scale_y), [`scale_z`](Transform3D::scale_z) |
21/// | **Matrix** | [`matrix`](Transform3D::matrix), [`calc_matrix`](Transform3D::calc_matrix) |
22///
23/// # Example
24///
25/// ```ignore
26/// use optic_render::Transform3D;
27///
28/// let mut t = Transform3D::default();
29/// t.set_pos_all(10.0, 0.0, 0.0);
30/// t.rotate_y(90.0);
31/// t.calc_matrix();
32/// ```
33#[derive(Clone, Debug)]
34pub struct Transform3D {
35    matrix: Matrix4<f32>,
36    pos: Vector3<f32>,
37    rot: Vector3<f32>,
38    scale: Vector3<f32>,
39}
40
41impl Default for Transform3D {
42    fn default() -> Self {
43        Self {
44            matrix: Matrix4::identity(),
45            pos: Vector3::new(0.0, 0.0, 0.0),
46            rot: Vector3::new(0.0, 0.0, 0.0),
47            scale: Vector3::new(1.0, 1.0, 1.0),
48        }
49    }
50}
51
52impl Transform3D {
53    fn calc_pos_matrix(&self) -> Matrix4<f32> {
54        Matrix4::from_translation(self.pos)
55    }
56
57    fn calc_rot_matrix(&self) -> Matrix4<f32> {
58        let x = Matrix4::from_angle_x(Rad::from(Deg(self.rot.x)));
59        let y = Matrix4::from_angle_y(Rad::from(Deg(self.rot.y)));
60        let z = Matrix4::from_angle_z(Rad::from(Deg(self.rot.z)));
61        x * y * z
62    }
63
64    fn calc_scale_matrix(&self) -> Matrix4<f32> {
65        Matrix4::from_nonuniform_scale(self.scale.x, self.scale.y, self.scale.z)
66    }
67
68    /// Recomputes the transformation matrix from the current pos/rot/scale.
69    pub fn calc_matrix(&mut self) {
70        self.matrix = self.calc_pos_matrix() * self.calc_rot_matrix() * self.calc_scale_matrix();
71    }
72
73    /// Returns the position.
74    pub fn pos(&self) -> Vector3<f32> { self.pos }
75    /// Returns the rotation (Euler angles in degrees).
76    pub fn rot(&self) -> Vector3<f32> { self.rot }
77    /// Returns the scale.
78    pub fn scale(&self) -> Vector3<f32> { self.scale }
79    /// Returns the cached 4×4 transformation matrix.
80    pub fn matrix(&self) -> Matrix4<f32> { self.matrix }
81
82    /// Translates by `(x, y, z)`.
83    pub fn move_all(&mut self, x: f32, y: f32, z: f32) { self.pos += vec3(x, y, z); }
84    /// Translates along the X axis.
85    pub fn move_x(&mut self, x: f32) { self.pos.x += x; }
86    /// Translates along the Y axis.
87    pub fn move_y(&mut self, y: f32) { self.pos.y += y; }
88    /// Translates along the Z axis.
89    pub fn move_z(&mut self, z: f32) { self.pos.z += z; }
90    /// Sets the position to `(x, y, z)`.
91    pub fn set_pos_all(&mut self, x: f32, y: f32, z: f32) { self.pos = vec3(x, y, z); }
92    /// Sets the X coordinate.
93    pub fn set_pos_x(&mut self, x: f32) { self.pos.x = x; }
94    /// Sets the Y coordinate.
95    pub fn set_pos_y(&mut self, y: f32) { self.pos.y = y; }
96    /// Sets the Z coordinate.
97    pub fn set_pos_z(&mut self, z: f32) { self.pos.z = z; }
98
99    /// Rotates by `(x, y, z)` degrees.
100    pub fn rotate_all(&mut self, x: f32, y: f32, z: f32) { self.rot += vec3(x, y, z); }
101    /// Rotates around the X axis.
102    pub fn rotate_x(&mut self, x: f32) { self.rot.x += x; }
103    /// Rotates around the Y axis.
104    pub fn rotate_y(&mut self, y: f32) { self.rot.y += y; }
105    /// Rotates around the Z axis.
106    pub fn rotate_z(&mut self, z: f32) { self.rot.z += z; }
107    /// Sets the rotation to `(x, y, z)` degrees.
108    pub fn set_rot_all(&mut self, x: f32, y: f32, z: f32) { self.rot = vec3(x, y, z); }
109    /// Sets the X rotation.
110    pub fn set_rot_x(&mut self, x: f32) { self.rot.x = x; }
111    /// Sets the Y rotation.
112    pub fn set_rot_y(&mut self, y: f32) { self.rot.y = y; }
113    /// Sets the Z rotation.
114    pub fn set_rot_z(&mut self, z: f32) { self.rot.z = z; }
115
116    /// Adds `(x, y, z)` to the scale.
117    pub fn scale_all(&mut self, x: f32, y: f32, z: f32) { self.scale += vec3(x, y, z); }
118    /// Adds `xyz` to all three scale components.
119    pub fn scale_same(&mut self, xyz: f32) { self.scale_all(xyz, xyz, xyz); }
120    /// Adds `x` to the scale X component.
121    pub fn scale_x(&mut self, x: f32) { self.scale.x += x; }
122    /// Adds `y` to the scale Y component.
123    pub fn scale_y(&mut self, y: f32) { self.scale.y += y; }
124    /// Adds `z` to the scale Z component.
125    pub fn scale_z(&mut self, z: f32) { self.scale.z += z; }
126    /// Sets the scale to `(x, y, z)`.
127    pub fn set_scale_all(&mut self, x: f32, y: f32, z: f32) { self.scale = vec3(x, y, z); }
128    /// Sets all three scale components to `xyz`.
129    pub fn set_scale_same(&mut self, xyz: f32) { self.set_scale_all(xyz, xyz, xyz); }
130    /// Sets the scale X component.
131    pub fn set_scale_x(&mut self, x: f32) { self.scale.x = x; }
132    /// Sets the scale Y component.
133    pub fn set_scale_y(&mut self, y: f32) { self.scale.y = y; }
134    /// Sets the scale Z component.
135    pub fn set_scale_z(&mut self, z: f32) { self.scale.z = z; }
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141
142    fn approx_eq(a: f32, b: f32) -> bool {
143        (a - b).abs() < 1e-5
144    }
145
146    fn mat_approx_eq(m1: &Matrix4<f32>, m2: &Matrix4<f32>) -> bool {
147        for c in 0..4 {
148            for r in 0..4 {
149                if !approx_eq(m1[c][r], m2[c][r]) {
150                    return false;
151                }
152            }
153        }
154        true
155    }
156
157    fn is_identity(m: &Matrix4<f32>) -> bool {
158        mat_approx_eq(m, &Matrix4::identity())
159    }
160
161    #[test]
162    fn transform3d_default() {
163        let t = Transform3D::default();
164        assert_eq!(t.pos(), vec3(0.0, 0.0, 0.0));
165        assert_eq!(t.rot(), vec3(0.0, 0.0, 0.0));
166        assert_eq!(t.scale(), vec3(1.0, 1.0, 1.0));
167        assert!(is_identity(&t.matrix()));
168    }
169
170    #[test]
171    fn transform3d_set_pos() {
172        let mut t = Transform3D::default();
173        t.set_pos_all(10.0, 20.0, 30.0);
174        assert_eq!(t.pos(), vec3(10.0, 20.0, 30.0));
175    }
176
177    #[test]
178    fn transform3d_move() {
179        let mut t = Transform3D::default();
180        t.move_all(1.0, 2.0, 3.0);
181        assert_eq!(t.pos(), vec3(1.0, 2.0, 3.0));
182        t.move_x(10.0);
183        assert_eq!(t.pos().x, 11.0);
184        t.move_y(20.0);
185        assert_eq!(t.pos().y, 22.0);
186        t.move_z(30.0);
187        assert_eq!(t.pos().z, 33.0);
188    }
189
190    #[test]
191    fn transform3d_rotate() {
192        let mut t = Transform3D::default();
193        t.rotate_all(90.0, 0.0, 0.0);
194        assert_eq!(t.rot(), vec3(90.0, 0.0, 0.0));
195        t.rotate_x(45.0);
196        assert!(approx_eq(t.rot().x, 135.0));
197        t.rotate_y(30.0);
198        assert!(approx_eq(t.rot().y, 30.0));
199        t.rotate_z(60.0);
200        assert!(approx_eq(t.rot().z, 60.0));
201    }
202
203    #[test]
204    fn transform3d_set_rot() {
205        let mut t = Transform3D::default();
206        t.set_rot_all(45.0, 90.0, 180.0);
207        assert_eq!(t.rot(), vec3(45.0, 90.0, 180.0));
208        t.set_rot_x(10.0);
209        t.set_rot_y(20.0);
210        t.set_rot_z(30.0);
211        assert_eq!(t.rot(), vec3(10.0, 20.0, 30.0));
212    }
213
214    #[test]
215    fn transform3d_scale() {
216        let mut t = Transform3D::default();
217        t.set_scale_all(2.0, 3.0, 4.0);
218        assert_eq!(t.scale(), vec3(2.0, 3.0, 4.0));
219    }
220
221    #[test]
222    fn transform3d_scale_operations() {
223        let mut t = Transform3D::default();
224        t.scale_all(1.0, 2.0, 3.0);
225        assert_eq!(t.scale(), vec3(2.0, 3.0, 4.0));
226        t.scale_same(5.0);
227        assert_eq!(t.scale(), vec3(7.0, 8.0, 9.0));
228        t.scale_x(1.0);
229        t.scale_y(1.0);
230        t.scale_z(1.0);
231        assert_eq!(t.scale(), vec3(8.0, 9.0, 10.0));
232    }
233
234    #[test]
235    fn transform3d_set_scale_individual() {
236        let mut t = Transform3D::default();
237        t.set_scale_x(5.0);
238        t.set_scale_y(6.0);
239        t.set_scale_z(7.0);
240        assert_eq!(t.scale(), vec3(5.0, 6.0, 7.0));
241    }
242
243    #[test]
244    fn transform3d_calc_matrix() {
245        let mut t = Transform3D::default();
246        // identity -> identity
247        t.calc_matrix();
248        assert!(is_identity(&t.matrix()));
249
250        // translation
251        t.set_pos_all(1.0, 2.0, 3.0);
252        t.calc_matrix();
253        let m = t.matrix();
254        assert!(approx_eq(m[3][0], 1.0));
255        assert!(approx_eq(m[3][1], 2.0));
256        assert!(approx_eq(m[3][2], 3.0));
257    }
258
259    #[test]
260    fn transform3d_matrix_combines() {
261        let mut t = Transform3D::default();
262        t.set_pos_all(10.0, 0.0, 0.0);
263        t.set_scale_all(2.0, 1.0, 1.0);
264        t.calc_matrix();
265        let m = t.matrix();
266        // translation x is 10, scale x is 2
267        assert!(approx_eq(m[0][0], 2.0));
268        assert!(approx_eq(m[3][0], 10.0));
269    }
270
271    #[test]
272    fn transform3d_set_scale_same() {
273        let mut t = Transform3D::default();
274        t.set_scale_same(3.0);
275        assert_eq!(t.scale(), vec3(3.0, 3.0, 3.0));
276    }
277}