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        // Use ZYX Euler order: Z(yaw), Y(pitch), X(roll)
80        // This matches UE's rotation application order
81        Quat::from_euler(glam::EulerRot::ZYX, yaw_rad, pitch_rad, roll_rad)
82    }
83
84    /// Create from quaternion
85    pub fn from_quaternion(quat: Quaternion) -> Self {
86        // To reverse the composition: yaw_quat * pitch_quat * roll_quat
87        // We need to extract in the reverse order
88        // Use ZYX order to match our composition order
89        let (z_rad, y_rad, x_rad) = quat.to_euler(glam::EulerRot::ZYX);
90        Self {
91            pitch: y_rad.to_degrees(),  // Y rotation = Pitch  
92            yaw: z_rad.to_degrees(),    // Z rotation = Yaw
93            roll: x_rad.to_degrees(),   // X rotation = Roll
94        }
95    }
96
97    /// Normalize angles to [-180, 180] range
98    pub fn normalize(mut self) -> Self {
99        self.pitch = normalize_angle(self.pitch);
100        self.yaw = normalize_angle(self.yaw);
101        self.roll = normalize_angle(self.roll);
102        self
103    }
104
105    /// Get the forward vector for this rotation
106    pub fn get_forward_vector(self) -> Vector {
107        self.to_quaternion() * VectorConstants::FORWARD
108    }
109
110    /// Get the right vector for this rotation
111    pub fn get_right_vector(self) -> Vector {
112        self.to_quaternion() * VectorConstants::RIGHT
113    }
114
115    /// Get the up vector for this rotation
116    pub fn get_up_vector(self) -> Vector {
117        self.to_quaternion() * VectorConstants::UP
118    }
119
120    /// Check if this rotator is nearly zero
121    pub fn is_nearly_zero(self, tolerance: f32) -> bool {
122        self.pitch.abs() <= tolerance 
123            && self.yaw.abs() <= tolerance 
124            && self.roll.abs() <= tolerance
125    }
126
127    /// Check if two rotators are nearly equal
128    pub fn is_nearly_equal(self, other: Rotator, tolerance: f32) -> bool {
129        (self.pitch - other.pitch).abs() <= tolerance
130            && (self.yaw - other.yaw).abs() <= tolerance
131            && (self.roll - other.roll).abs() <= tolerance
132    }
133
134    /// Add rotators component-wise
135    pub fn add(self, other: Rotator) -> Self {
136        Self {
137            pitch: self.pitch + other.pitch,
138            yaw: self.yaw + other.yaw,
139            roll: self.roll + other.roll,
140        }
141    }
142
143    /// Subtract rotators component-wise
144    pub fn sub(self, other: Rotator) -> Self {
145        Self {
146            pitch: self.pitch - other.pitch,
147            yaw: self.yaw - other.yaw,
148            roll: self.roll - other.roll,
149        }
150    }
151
152    /// Scale rotator by a factor
153    pub fn scale(self, factor: f32) -> Self {
154        Self {
155            pitch: self.pitch * factor,
156            yaw: self.yaw * factor,
157            roll: self.roll * factor,
158        }
159    }
160}
161
162impl Default for Rotator {
163    fn default() -> Self {
164        Self::ZERO
165    }
166}
167
168/// Normalize an angle to the range [-180, 180] degrees
169pub fn normalize_angle(angle: f32) -> f32 {
170    let mut result = angle % 360.0;
171    if result > 180.0 {
172        result -= 360.0;
173    } else if result < -180.0 {
174        result += 360.0;
175    }
176    result
177}
178
179/// Compute the angular difference between two angles (in degrees)
180/// Returns the shortest angular distance from angle1 to angle2
181pub fn angle_difference(angle1: f32, angle2: f32) -> f32 {
182    normalize_angle(angle2 - angle1)
183}
184
185/// Linearly interpolate between two rotators
186pub fn lerp_rotator(a: Rotator, b: Rotator, alpha: f32) -> Rotator {
187    Rotator {
188        pitch: a.pitch + alpha * angle_difference(a.pitch, b.pitch),
189        yaw: a.yaw + alpha * angle_difference(a.yaw, b.yaw),
190        roll: a.roll + alpha * angle_difference(a.roll, b.roll),
191    }
192}
193
194#[cfg(test)]
195mod tests {
196    use super::*;
197
198    #[test]
199    fn test_rotator_creation() {
200        let rot = Rotator::new(45.0, 90.0, 0.0);
201        assert_eq!(rot.pitch, 45.0);
202        assert_eq!(rot.yaw, 90.0);
203        assert_eq!(rot.roll, 0.0);
204    }
205
206    #[test]
207    fn test_normalize_angle() {
208        assert_eq!(normalize_angle(270.0), -90.0);
209        assert_eq!(normalize_angle(-270.0), 90.0);
210        assert_eq!(normalize_angle(180.0), 180.0);
211        assert_eq!(normalize_angle(-180.0), -180.0);
212    }
213
214    #[test]
215    fn test_quaternion_conversion() {
216        let rot = Rotator::new(0.0, 90.0, 0.0);
217        let quat = rot.to_quaternion();
218        let back_to_rot = Rotator::from_quaternion(quat);
219        
220        // Should be approximately equal (floating point precision)
221        assert!((rot.yaw - back_to_rot.yaw).abs() < 0.001);
222    }
223
224    #[test]
225    fn test_forward_vector() {
226        let rot = Rotator::from_yaw(90.0);
227        let forward = rot.get_forward_vector();
228        
229        // 90 degree yaw should point along positive Y axis
230        assert!((forward.y - 1.0).abs() < 0.001);
231        assert!(forward.x.abs() < 0.001);
232        assert!(forward.z.abs() < 0.001);
233    }
234
235    #[test]
236    fn test_rotation_vectors() {
237        // Test zero rotation gives expected vectors
238        let zero_rot = Rotator::ZERO;
239        let forward = zero_rot.get_forward_vector();
240        let right = zero_rot.get_right_vector();
241        let up = zero_rot.get_up_vector();
242        
243        // Forward should be X axis
244        assert!((forward.x - 1.0).abs() < 0.001);
245        assert!(forward.y.abs() < 0.001);
246        assert!(forward.z.abs() < 0.001);
247        
248        // Right should be Y axis  
249        assert!(right.x.abs() < 0.001);
250        assert!((right.y - 1.0).abs() < 0.001);
251        assert!(right.z.abs() < 0.001);
252        
253        // Up should be Z axis
254        assert!(up.x.abs() < 0.001);
255        assert!(up.y.abs() < 0.001);
256        assert!((up.z - 1.0).abs() < 0.001);
257    }
258
259    #[test]
260    fn test_pitch_rotation() {
261        let rot = Rotator::from_pitch(90.0);
262        let forward = rot.get_forward_vector();
263        
264        // In UE, positive pitch typically looks DOWN (negative Z direction)
265        // 90 degree pitch should point along negative Z axis (down)
266        assert!(forward.x.abs() < 0.001);
267        assert!(forward.y.abs() < 0.001);
268        assert!((forward.z + 1.0).abs() < 0.001);  // Changed to -1.0 (down)
269    }
270
271    #[test]
272    fn test_negative_pitch_rotation() {
273        let rot = Rotator::from_pitch(-90.0);
274        let forward = rot.get_forward_vector();
275        
276        // Negative pitch should look UP (positive Z direction)
277        assert!(forward.x.abs() < 0.001);
278        assert!(forward.y.abs() < 0.001);
279        assert!((forward.z - 1.0).abs() < 0.001);  // Should be +1.0 (up)
280    }
281
282    #[test]
283    fn test_quaternion_conversion_roundtrip() {
284        let original = Rotator::new(30.0, 45.0, 60.0);
285        let quat = original.to_quaternion();
286        let back_to_rot = Rotator::from_quaternion(quat);
287        
288        // Should be approximately equal (allowing for floating point precision)
289        assert!((original.pitch - back_to_rot.pitch).abs() < 0.01);
290        assert!((original.yaw - back_to_rot.yaw).abs() < 0.01);
291        assert!((original.roll - back_to_rot.roll).abs() < 0.01);
292    }
293
294    #[test]
295    fn test_rotator_display() {
296        let rot = Rotator::new(45.0, 90.0, -30.0);
297        let display_str = format!("{}", rot);
298        assert!(display_str.contains("P=45.00°"));
299        assert!(display_str.contains("Y=90.00°"));
300        assert!(display_str.contains("R=-30.00°"));
301    }
302
303    #[test]
304    fn test_rotator_json_serialization() {
305        let rot = Rotator::new(45.0, 90.0, -30.0);
306        
307        // Test JSON serialization
308        let json = serde_json::to_string(&rot).unwrap();
309        let deserialized: Rotator = serde_json::from_str(&json).unwrap();
310        
311        assert!(rot.is_nearly_equal(deserialized, 0.001));
312    }
313
314    #[test]
315    fn test_rotator_binary_serialization() {
316        let rot = Rotator::new(45.0, 90.0, -30.0);
317        
318        // Test binary serialization
319        let binary = rot.to_binary().unwrap();
320        let deserialized = Rotator::from_binary(&binary).unwrap();
321        
322        assert!(rot.is_nearly_equal(deserialized, 0.001));
323    }
324}