Skip to main content

rscad_csg_bsp/
rotation.rs

1//! Euler angle rotation utilities.
2
3use crate::Number;
4use nalgebra::Matrix3;
5
6/// Build a 3×3 rotation matrix from Euler angles (degrees), applied as Z × Y × X.
7///
8/// This is the extrinsic rotation convention matching OpenSCAD's `rotate([x, y, z])`.
9///
10/// # Mathematical derivation
11///
12/// Given angles (α, β, γ) for (X, Y, Z) in radians:
13///
14/// ```text
15/// Rx = ⎡ 1    0      0   ⎤    Ry = ⎡ cos β   0   sin β ⎤    Rz = ⎡ cos γ  -sin γ   0 ⎤
16///      ⎢ 0   cos α  -sin α⎥         ⎢  0      1    0    ⎥         ⎢ sin γ   cos γ   0 ⎥
17///      ⎣ 0   sin α   cos α⎦         ⎣-sin β   0   cos β ⎦         ⎣  0       0      1 ⎦
18///
19/// R = Rz · Ry · Rx
20/// ```
21pub fn euler_rotation_matrix<T: Number>(angles_deg: nalgebra::Vector3<T>) -> Matrix3<T> {
22    use num_traits::Float;
23    let xr = Float::to_radians(angles_deg[0]);
24    let yr = Float::to_radians(angles_deg[1]);
25    let zr = Float::to_radians(angles_deg[2]);
26    let (sx, cx) = (Float::sin(xr), Float::cos(xr));
27    let (sy, cy) = (Float::sin(yr), Float::cos(yr));
28    let (sz, cz) = (Float::sin(zr), Float::cos(zr));
29
30    // Rz * Ry * Rx
31    Matrix3::new(
32        cy * cz,
33        cz * sx * sy - cx * sz,
34        sx * sz + cx * cz * sy,
35        cy * sz,
36        cx * cz + sx * sy * sz,
37        cx * sy * sz - cz * sx,
38        -sy,
39        cy * sx,
40        cx * cy,
41    )
42}
43
44#[cfg(test)]
45mod tests {
46    use super::*;
47    use nalgebra::Vector3;
48
49    fn approx_eq(a: &Matrix3<f64>, b: &Matrix3<f64>, eps: f64) -> bool {
50        (a - b).iter().all(|x| x.abs() < eps)
51    }
52
53    #[test]
54    fn identity_for_zero_angles() {
55        let m = euler_rotation_matrix(Vector3::new(0.0_f64, 0.0, 0.0));
56        assert!(approx_eq(&m, &Matrix3::identity(), 1e-12));
57    }
58
59    #[test]
60    fn rotate_90_around_z() {
61        let m = euler_rotation_matrix(Vector3::new(0.0_f64, 0.0, 90.0));
62        let v = m * Vector3::new(1.0, 0.0, 0.0);
63        assert!((v[0]).abs() < 1e-12);
64        assert!((v[1] - 1.0).abs() < 1e-12);
65        assert!((v[2]).abs() < 1e-12);
66    }
67
68    #[test]
69    fn rotate_90_around_x() {
70        let m = euler_rotation_matrix(Vector3::new(90.0_f64, 0.0, 0.0));
71        let v = m * Vector3::new(0.0, 1.0, 0.0);
72        assert!((v[0]).abs() < 1e-12);
73        assert!((v[1]).abs() < 1e-12);
74        assert!((v[2] - 1.0).abs() < 1e-12);
75    }
76
77    #[test]
78    fn rotate_90_around_y() {
79        let m = euler_rotation_matrix(Vector3::new(0.0_f64, 90.0, 0.0));
80        let v = m * Vector3::new(1.0, 0.0, 0.0);
81        assert!((v[0]).abs() < 1e-12);
82        assert!((v[1]).abs() < 1e-12);
83        assert!((v[2] + 1.0).abs() < 1e-12);
84    }
85
86    #[test]
87    fn composed_rotation_is_orthogonal() {
88        let m = euler_rotation_matrix(Vector3::new(30.0_f64, 45.0, 60.0));
89        let mmt = m * m.transpose();
90        assert!(approx_eq(&mmt, &Matrix3::identity(), 1e-12));
91    }
92}