ue_types/
rotator.rs

1//! Rotator type and rotation utilities
2
3use crate::vector::*;
4use crate::BinarySerializable;
5use glam::Quat;
6use serde::{Deserialize, Serialize};
7use std::fmt;
8
9/// Unreal Engine style Rotator (Pitch, Yaw, Roll in degrees)
10/// 
11/// Represents rotation using Euler angles in degrees:
12/// - Pitch: Rotation around Y axis (up/down)
13/// - Yaw: Rotation around Z axis (left/right)  
14/// - Roll: Rotation around X axis (banking)
15#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
16pub struct Rotator {
17    /// Rotation around Y axis (degrees)
18    pub pitch: f32,
19    /// Rotation around Z axis (degrees) 
20    pub yaw: f32,
21    /// Rotation around X axis (degrees)
22    pub roll: f32,
23}
24
25impl fmt::Display for Rotator {
26    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27        write!(f, "P={:.2}° Y={:.2}° R={:.2}°", self.pitch, self.yaw, self.roll)
28    }
29}
30
31impl BinarySerializable for Rotator {}
32
33impl Rotator {
34    /// Zero rotation constant
35    pub const ZERO: Self = Self { 
36        pitch: 0.0, 
37        yaw: 0.0, 
38        roll: 0.0 
39    };
40
41    /// Create a new rotator with the given pitch, yaw, and roll (in degrees)
42    pub fn new(pitch: f32, yaw: f32, roll: f32) -> Self {
43        Self { pitch, yaw, roll }
44    }
45
46    /// Create a rotator with only yaw rotation
47    pub fn from_yaw(yaw: f32) -> Self {
48        Self { 
49            pitch: 0.0, 
50            yaw, 
51            roll: 0.0 
52        }
53    }
54
55    /// Create a rotator with only pitch rotation
56    pub fn from_pitch(pitch: f32) -> Self {
57        Self { 
58            pitch, 
59            yaw: 0.0, 
60            roll: 0.0 
61        }
62    }
63
64    /// Create a rotator with only roll rotation
65    pub fn from_roll(roll: f32) -> Self {
66        Self { 
67            pitch: 0.0, 
68            yaw: 0.0, 
69            roll 
70        }
71    }
72
73    /// Convert to quaternion (preferred for math operations)
74    pub fn to_quaternion(self) -> Quaternion {
75        let pitch_rad = self.pitch.to_radians();
76        let yaw_rad = self.yaw.to_radians();
77        let roll_rad = self.roll.to_radians();
78        
79        // UE uses YXZ rotation order
80        Quat::from_euler(glam::EulerRot::YXZ, yaw_rad, pitch_rad, roll_rad)
81    }
82
83    /// Create from quaternion
84    pub fn from_quaternion(quat: Quaternion) -> Self {
85        let (yaw, pitch, roll) = quat.to_euler(glam::EulerRot::YXZ);
86        Self {
87            pitch: pitch.to_degrees(),
88            yaw: yaw.to_degrees(),
89            roll: roll.to_degrees(),
90        }
91    }
92
93    /// Normalize angles to [-180, 180] range
94    pub fn normalize(mut self) -> Self {
95        self.pitch = normalize_angle(self.pitch);
96        self.yaw = normalize_angle(self.yaw);
97        self.roll = normalize_angle(self.roll);
98        self
99    }
100
101    /// Get the forward vector for this rotation
102    pub fn get_forward_vector(self) -> Vector {
103        self.to_quaternion() * VectorConstants::FORWARD
104    }
105
106    /// Get the right vector for this rotation
107    pub fn get_right_vector(self) -> Vector {
108        self.to_quaternion() * VectorConstants::RIGHT
109    }
110
111    /// Get the up vector for this rotation
112    pub fn get_up_vector(self) -> Vector {
113        self.to_quaternion() * VectorConstants::UP
114    }
115
116    /// Check if this rotator is nearly zero
117    pub fn is_nearly_zero(self, tolerance: f32) -> bool {
118        self.pitch.abs() <= tolerance 
119            && self.yaw.abs() <= tolerance 
120            && self.roll.abs() <= tolerance
121    }
122
123    /// Check if two rotators are nearly equal
124    pub fn is_nearly_equal(self, other: Rotator, tolerance: f32) -> bool {
125        (self.pitch - other.pitch).abs() <= tolerance
126            && (self.yaw - other.yaw).abs() <= tolerance
127            && (self.roll - other.roll).abs() <= tolerance
128    }
129
130    /// Add rotators component-wise
131    pub fn add(self, other: Rotator) -> Self {
132        Self {
133            pitch: self.pitch + other.pitch,
134            yaw: self.yaw + other.yaw,
135            roll: self.roll + other.roll,
136        }
137    }
138
139    /// Subtract rotators component-wise
140    pub fn sub(self, other: Rotator) -> Self {
141        Self {
142            pitch: self.pitch - other.pitch,
143            yaw: self.yaw - other.yaw,
144            roll: self.roll - other.roll,
145        }
146    }
147
148    /// Scale rotator by a factor
149    pub fn scale(self, factor: f32) -> Self {
150        Self {
151            pitch: self.pitch * factor,
152            yaw: self.yaw * factor,
153            roll: self.roll * factor,
154        }
155    }
156}
157
158impl Default for Rotator {
159    fn default() -> Self {
160        Self::ZERO
161    }
162}
163
164/// Normalize an angle to the range [-180, 180] degrees
165pub fn normalize_angle(angle: f32) -> f32 {
166    let mut result = angle % 360.0;
167    if result > 180.0 {
168        result -= 360.0;
169    } else if result < -180.0 {
170        result += 360.0;
171    }
172    result
173}
174
175/// Compute the angular difference between two angles (in degrees)
176/// Returns the shortest angular distance from angle1 to angle2
177pub fn angle_difference(angle1: f32, angle2: f32) -> f32 {
178    normalize_angle(angle2 - angle1)
179}
180
181/// Linearly interpolate between two rotators
182pub fn lerp_rotator(a: Rotator, b: Rotator, alpha: f32) -> Rotator {
183    Rotator {
184        pitch: a.pitch + alpha * angle_difference(a.pitch, b.pitch),
185        yaw: a.yaw + alpha * angle_difference(a.yaw, b.yaw),
186        roll: a.roll + alpha * angle_difference(a.roll, b.roll),
187    }
188}
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193
194    #[test]
195    fn test_rotator_creation() {
196        let rot = Rotator::new(45.0, 90.0, 0.0);
197        assert_eq!(rot.pitch, 45.0);
198        assert_eq!(rot.yaw, 90.0);
199        assert_eq!(rot.roll, 0.0);
200    }
201
202    #[test]
203    fn test_normalize_angle() {
204        assert_eq!(normalize_angle(270.0), -90.0);
205        assert_eq!(normalize_angle(-270.0), 90.0);
206        assert_eq!(normalize_angle(180.0), 180.0);
207        assert_eq!(normalize_angle(-180.0), -180.0);
208    }
209
210    #[test]
211    fn test_quaternion_conversion() {
212        let rot = Rotator::new(0.0, 90.0, 0.0);
213        let quat = rot.to_quaternion();
214        let back_to_rot = Rotator::from_quaternion(quat);
215        
216        // Should be approximately equal (floating point precision)
217        assert!((rot.yaw - back_to_rot.yaw).abs() < 0.001);
218    }
219
220    #[test]
221    fn test_forward_vector() {
222        let rot = Rotator::from_yaw(90.0);
223        let forward = rot.get_forward_vector();
224        
225        // 90 degree yaw should point along positive Y axis
226        assert!((forward.y - 1.0).abs() < 0.001);
227        assert!(forward.x.abs() < 0.001);
228    }
229
230    #[test]
231    fn test_rotator_display() {
232        let rot = Rotator::new(45.0, 90.0, -30.0);
233        let display_str = format!("{}", rot);
234        assert!(display_str.contains("P=45.00°"));
235        assert!(display_str.contains("Y=90.00°"));
236        assert!(display_str.contains("R=-30.00°"));
237    }
238
239    #[test]
240    fn test_rotator_json_serialization() {
241        let rot = Rotator::new(45.0, 90.0, -30.0);
242        
243        // Test JSON serialization
244        let json = serde_json::to_string(&rot).unwrap();
245        let deserialized: Rotator = serde_json::from_str(&json).unwrap();
246        
247        assert!(rot.is_nearly_equal(deserialized, 0.001));
248    }
249
250    #[test]
251    fn test_rotator_binary_serialization() {
252        let rot = Rotator::new(45.0, 90.0, -30.0);
253        
254        // Test binary serialization
255        let binary = rot.to_binary().unwrap();
256        let deserialized = Rotator::from_binary(&binary).unwrap();
257        
258        assert!(rot.is_nearly_equal(deserialized, 0.001));
259    }
260}