Skip to main content

steel_math/
vector.rs

1//! Rotation helpers for `DVec3` using Steel trig lookup table.
2//!
3//! Port of `net.minecraft.world.phys.Vec3`'s axis rotations, with X and Z using
4//! a different rotation direction than `glam`'s builtin methods.
5
6use glam::DVec3;
7
8use crate::trig;
9
10/// Returns the table-backed cosine and sine vanilla's rotations index.
11fn cos_sin(radians: f32) -> (f64, f64) {
12    let angle = f64::from(radians);
13    (f64::from(trig::cos(angle)), f64::from(trig::sin(angle)))
14}
15
16/// Rotates the vector around the X axis and positive angles rotate +Y toward -Z
17#[inline]
18#[must_use]
19pub fn x_rot(vector: DVec3, radians: f32) -> DVec3 {
20    let (cos, sin) = cos_sin(radians);
21    DVec3::new(
22        vector.x,
23        vector.y * cos + vector.z * sin,
24        vector.z * cos - vector.y * sin,
25    )
26}
27
28/// Rotates the vector around the Y axis and positive angles rotate +X toward -Z
29#[inline]
30#[must_use]
31pub fn y_rot(vector: DVec3, radians: f32) -> DVec3 {
32    let (cos, sin) = cos_sin(radians);
33    DVec3::new(
34        vector.x * cos + vector.z * sin,
35        vector.y,
36        vector.z * cos - vector.x * sin,
37    )
38}
39
40/// Rotates the vector around the Z axis and positive angles rotate +X toward -Y
41#[inline]
42#[must_use]
43pub fn z_rot(vector: DVec3, radians: f32) -> DVec3 {
44    let (cos, sin) = cos_sin(radians);
45    DVec3::new(
46        vector.x * cos + vector.y * sin,
47        vector.y * cos - vector.x * sin,
48        vector.z,
49    )
50}
51
52#[cfg(test)]
53mod tests {
54    use super::*;
55    use std::f32::consts::FRAC_PI_2;
56
57    /// The sine table steps by 2π / 65536, bounding the error on a unit vector.
58    const TABLE_EPSILON: f64 = 1e-4;
59
60    fn assert_close(actual: DVec3, expected: DVec3) {
61        assert!(
62            (actual - expected).length() < TABLE_EPSILON,
63            "expected {expected:?}, got {actual:?}"
64        );
65    }
66
67    #[test]
68    fn rotating_by_zero_returns_the_input() {
69        let vector = DVec3::new(1.0, 2.0, 3.0);
70
71        assert_eq!(x_rot(vector, 0.0).to_array(), vector.to_array());
72        assert_eq!(y_rot(vector, 0.0).to_array(), vector.to_array());
73        assert_eq!(z_rot(vector, 0.0).to_array(), vector.to_array());
74    }
75
76    #[test]
77    fn quarter_turns_match_vanilla_axis_mapping() {
78        assert_close(x_rot(DVec3::Y, FRAC_PI_2), DVec3::new(0.0, 0.0, -1.0));
79        assert_close(y_rot(DVec3::X, FRAC_PI_2), DVec3::new(0.0, 0.0, -1.0));
80        assert_close(z_rot(DVec3::X, FRAC_PI_2), DVec3::new(0.0, -1.0, 0.0));
81    }
82
83    /// Pins the sign convention that reaching for `DVec3::rotate_x` breaks.
84    #[test]
85    fn x_rot_opposes_glam_rotate_x_while_y_rot_agrees() {
86        let down = DVec3::new(0.0, -1.0, 0.0);
87        let radians = 30.0_f32.to_radians();
88
89        assert_close(x_rot(down, radians), DVec3::new(0.0, -0.866_025, 0.5));
90        assert_close(
91            down.rotate_x(f64::from(radians)),
92            DVec3::new(0.0, -0.866_025, -0.5),
93        );
94
95        let vector = DVec3::new(1.0, 0.0, 0.5);
96        assert_close(y_rot(vector, radians), vector.rotate_y(f64::from(radians)));
97    }
98}