khora_core/math/
affine_transform.rs

1// Copyright 2025 eraflo
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Affine transformations for 2D and 3D space.
16
17use crate::math::{Mat4, Quaternion, Vec3, Vec4};
18
19/// Represents a 3D affine transformation (translation, rotation, scale).
20///
21/// This is a semantic wrapper around a `Mat4` that guarantees the matrix
22/// represents a valid affine transform. It provides a dedicated API for
23/// creating and manipulating these transformations.
24#[derive(Debug, Clone, Copy, PartialEq)]
25#[repr(transparent)]
26pub struct AffineTransform(pub Mat4);
27
28impl AffineTransform {
29    /// The identity transform, which results in no change.
30    pub const IDENTITY: Self = Self(Mat4::IDENTITY);
31
32    // --- CONSTRUCTORS ---
33    /// Creates an `AffineTransform` from a translation vector.
34    ///
35    /// # Arguments
36    ///
37    /// * `v` - The translation vector to apply
38    ///
39    /// # Example
40    ///
41    /// ```rust
42    /// use khora_core::math::Vec3;
43    /// use khora_core::math::affine_transform::AffineTransform;
44    ///
45    /// let transform = AffineTransform::from_translation(Vec3::new(1.0, 2.0, 3.0));
46    /// assert_eq!(transform.translation(), Vec3::new(1.0, 2.0, 3.0));
47    /// ```
48    #[inline]
49    pub fn from_translation(v: Vec3) -> Self {
50        Self(Mat4::from_cols(
51            Vec4::new(1.0, 0.0, 0.0, 0.0),
52            Vec4::new(0.0, 1.0, 0.0, 0.0),
53            Vec4::new(0.0, 0.0, 1.0, 0.0),
54            Vec4::new(v.x, v.y, v.z, 1.0),
55        ))
56    }
57
58    /// Creates an `AffineTransform` from a non-uniform scale vector.
59    ///
60    /// # Arguments
61    ///
62    /// * `scale` - The scale vector to apply to each axis
63    ///
64    /// # Example
65    ///
66    /// ```rust
67    /// use khora_core::math::Vec3;
68    /// use khora_core::math::affine_transform::AffineTransform;
69    ///
70    /// let transform = AffineTransform::from_scale(Vec3::new(2.0, 1.5, 0.5));
71    /// ```
72    #[inline]
73    pub fn from_scale(scale: Vec3) -> Self {
74        Self(Mat4::from_cols(
75            Vec4::new(scale.x, 0.0, 0.0, 0.0),
76            Vec4::new(0.0, scale.y, 0.0, 0.0),
77            Vec4::new(0.0, 0.0, scale.z, 0.0),
78            Vec4::new(0.0, 0.0, 0.0, 1.0),
79        ))
80    }
81
82    /// Creates an `AffineTransform` from a rotation around the X axis.
83    ///
84    /// # Arguments
85    ///
86    /// * `angle` - The angle of rotation in radians
87    ///
88    /// # Example
89    ///
90    /// ```rust
91    /// use khora_core::math::Vec3;
92    /// use khora_core::math::affine_transform::AffineTransform;
93    /// use std::f32::consts::PI;
94    ///
95    /// let transform = AffineTransform::from_rotation_x(PI / 2.0);
96    /// ```
97    #[inline]
98    pub fn from_rotation_x(angle: f32) -> Self {
99        let c = angle.cos();
100        let s = angle.sin();
101        Self(Mat4::from_cols(
102            Vec4::new(1.0, 0.0, 0.0, 0.0),
103            Vec4::new(0.0, c, s, 0.0),
104            Vec4::new(0.0, -s, c, 0.0),
105            Vec4::new(0.0, 0.0, 0.0, 1.0),
106        ))
107    }
108
109    /// Creates an `AffineTransform` from a rotation around the Y axis.
110    ///
111    /// # Arguments
112    ///
113    /// * `angle` - The angle of rotation in radians
114    ///
115    /// # Example
116    ///
117    /// ```rust
118    /// use khora_core::math::Vec3;
119    /// use khora_core::math::affine_transform::AffineTransform;
120    /// use std::f32::consts::PI;
121    ///
122    /// let transform = AffineTransform::from_rotation_y(PI / 2.0);
123    /// ```
124    #[inline]
125    pub fn from_rotation_y(angle: f32) -> Self {
126        let c = angle.cos();
127        let s = angle.sin();
128        Self(Mat4::from_cols(
129            Vec4::new(c, 0.0, -s, 0.0),
130            Vec4::new(0.0, 1.0, 0.0, 0.0),
131            Vec4::new(s, 0.0, c, 0.0),
132            Vec4::new(0.0, 0.0, 0.0, 1.0),
133        ))
134    }
135
136    /// Creates an `AffineTransform` from a rotation around the Z axis.
137    ///
138    /// # Arguments
139    ///
140    /// * `angle` - The angle of rotation in radians
141    ///
142    /// # Example
143    ///
144    /// ```rust
145    /// use std::f32::consts::PI;
146    /// use khora_core::math::Vec3;
147    /// use khora_core::math::affine_transform::AffineTransform;
148    ///
149    /// let transform = AffineTransform::from_rotation_z(PI / 2.0);
150    /// ```
151    #[inline]
152    pub fn from_rotation_z(angle: f32) -> Self {
153        let c = angle.cos();
154        let s = angle.sin();
155        Self(Mat4::from_cols(
156            Vec4::new(c, s, 0.0, 0.0),
157            Vec4::new(-s, c, 0.0, 0.0),
158            Vec4::new(0.0, 0.0, 1.0, 0.0),
159            Vec4::new(0.0, 0.0, 0.0, 1.0),
160        ))
161    }
162
163    /// Creates an `AffineTransform` from a rotation around an arbitrary axis.
164    ///
165    /// Uses Rodrigues' rotation formula to create a rotation matrix.
166    ///
167    /// # Arguments
168    ///
169    /// * `axis` - The axis of rotation (will be normalized automatically)
170    /// * `angle` - The angle of rotation in radians
171    ///
172    /// # Example
173    ///
174    /// ```rust
175    /// use std::f32::consts::PI;
176    /// use khora_core::math::Vec3;
177    /// use khora_core::math::affine_transform::AffineTransform;
178    ///
179    /// let axis = Vec3::new(1.0, 1.0, 0.0);
180    /// let transform = AffineTransform::from_axis_angle(axis, PI / 4.0);
181    /// ```
182    #[inline]
183    pub fn from_axis_angle(axis: Vec3, angle: f32) -> Self {
184        let c = angle.cos();
185        let s = angle.sin();
186        let t = 1.0 - c;
187        let x = axis.x;
188        let y = axis.y;
189        let z = axis.z;
190
191        Self(Mat4::from_cols(
192            Vec4::new(t * x * x + c, t * x * y - s * z, t * x * z + s * y, 0.0),
193            Vec4::new(t * y * x + s * z, t * y * y + c, t * y * z - s * x, 0.0),
194            Vec4::new(t * z * x - s * y, t * z * y + s * x, t * z * z + c, 0.0),
195            Vec4::new(0.0, 0.0, 0.0, 1.0),
196        ))
197    }
198
199    /// Creates an `AffineTransform` from a quaternion representing a rotation.
200    ///
201    /// # Arguments
202    ///
203    /// * `q` - The quaternion representing the rotation
204    ///
205    /// # Example
206    ///
207    /// ```rust
208    /// use khora_core::math::{Quaternion, Vec3};
209    /// use khora_core::math::affine_transform::AffineTransform;
210    /// use std::f32::consts::PI;
211    ///
212    /// let q = Quaternion::from_axis_angle(Vec3::Y, PI / 2.0);
213    /// let transform = AffineTransform::from_quat(q);
214    /// ```
215    #[inline]
216    pub fn from_quat(q: Quaternion) -> Self {
217        Self(Mat4::from_quat(q))
218    }
219
220    // --- SEMANTIC ACCESSORS---
221    /// Converts the `AffineTransform` to a `Mat4`.
222    ///
223    /// This is useful when you need to pass the transformation matrix to shaders
224    /// or other systems that expect a raw matrix.
225    ///
226    /// # Example
227    ///
228    /// ```rust
229    /// use khora_core::math::Vec3;
230    /// use khora_core::math::affine_transform::AffineTransform;
231    ///
232    /// let transform = AffineTransform::IDENTITY;
233    /// let matrix = transform.to_matrix();
234    /// ```
235    #[inline]
236    pub fn to_matrix(&self) -> Mat4 {
237        self.0
238    }
239
240    /// Extracts the translation component from the affine transform.
241    ///
242    /// Returns the translation vector representing the position offset
243    /// applied by this transformation.
244    ///
245    /// # Example
246    ///
247    /// ```rust
248    /// use khora_core::math::Vec3;
249    /// use khora_core::math::affine_transform::AffineTransform;
250    ///
251    /// let transform = AffineTransform::from_translation(Vec3::new(1.0, 2.0, 3.0));
252    /// assert_eq!(transform.translation(), Vec3::new(1.0, 2.0, 3.0));
253    /// ```
254    #[inline]
255    pub fn translation(&self) -> Vec3 {
256        self.0.cols[3].truncate()
257    }
258
259    /// Extracts the right direction vector from the affine transform.
260    ///
261    /// This returns the first column of the transformation matrix (excluding the w component),
262    /// which represents the transformed positive X-axis direction.
263    ///
264    /// # Example
265    ///
266    /// ```rust
267    /// use khora_core::math::Vec3;
268    /// use khora_core::math::affine_transform::AffineTransform;
269    /// use std::f32::consts::PI;
270    ///
271    /// let transform = AffineTransform::from_rotation_z(PI / 2.0);
272    /// let right = transform.right();
273    /// // After 90° rotation around Z, right vector points in -Y direction
274    /// ```
275    #[inline]
276    pub fn right(&self) -> Vec3 {
277        self.0.cols[0].truncate()
278    }
279
280    /// Extracts the up direction vector from the affine transform.
281    ///
282    /// This returns the second column of the transformation matrix (excluding the w component),
283    /// which represents the transformed positive Y-axis direction.
284    ///
285    /// # Example
286    ///
287    /// ```rust
288    /// use std::f32::consts::PI;
289    /// use khora_core::math::Vec3;
290    /// use khora_core::math::affine_transform::AffineTransform;
291    ///
292    /// let transform = AffineTransform::from_rotation_x(PI / 2.0);
293    /// let up = transform.up();
294    /// // After 90° rotation around X, up vector points in -Z direction
295    /// ```
296    #[inline]
297    pub fn up(&self) -> Vec3 {
298        self.0.cols[1].truncate()
299    }
300
301    /// Extracts the forward direction vector from the affine transform.
302    ///
303    /// This returns the third column of the transformation matrix (excluding the w component),
304    /// which represents the transformed positive Z-axis direction.
305    ///
306    /// # Example
307    ///
308    /// ```rust
309    /// use std::f32::consts::PI;
310    /// use khora_core::math::Vec3;
311    /// use khora_core::math::affine_transform::AffineTransform;
312    ///
313    /// let transform = AffineTransform::from_rotation_y(PI / 2.0);
314    /// let forward = transform.forward();
315    /// // After 90° rotation around Y, forward vector points in X direction
316    /// ```
317    #[inline]
318    pub fn forward(&self) -> Vec3 {
319        self.0.cols[2].truncate()
320    }
321
322    /// Extracts the rotation component as a quaternion.
323    ///
324    /// This method extracts the rotation represented by the upper-left 3x3
325    /// portion of the transformation matrix.
326    ///
327    /// # Note
328    ///
329    /// This assumes the transform has uniform or no scale. For transforms
330    /// with non-uniform scale, the result may not represent a pure rotation.
331    /// In such cases, consider normalizing the direction vectors first.
332    ///
333    /// # Example
334    ///
335    /// ```rust
336    /// use khora_core::math::{Quaternion, Vec3};
337    /// use khora_core::math::affine_transform::AffineTransform;
338    /// use std::f32::consts::PI;
339    ///
340    /// let q = Quaternion::from_axis_angle(Vec3::Y, PI / 2.0);
341    /// let transform = AffineTransform::from_quat(q);
342    /// let extracted = transform.rotation();
343    /// // extracted should be approximately equal to q
344    /// ```
345    #[inline]
346    pub fn rotation(&self) -> Quaternion {
347        Quaternion::from_rotation_matrix(&self.0)
348    }
349
350    /// Computes the inverse of the affine transformation.
351    ///
352    /// This uses an optimized affine inverse algorithm that's more efficient than
353    /// a general matrix inverse, taking advantage of the affine transform structure.
354    /// Returns `None` if the transformation is not invertible (e.g., zero scale).
355    ///
356    /// # Returns
357    ///
358    /// `Some(AffineTransform)` if the inverse exists, `None` otherwise.
359    ///
360    /// # Example
361    ///
362    /// ```rust
363    /// use khora_core::math::Vec3;
364    /// use khora_core::math::affine_transform::AffineTransform;
365    ///
366    /// let transform = AffineTransform::from_translation(Vec3::new(1.0, 2.0, 3.0));
367    /// let inverse = transform.inverse().unwrap();
368    /// // The inverse should translate by (-1, -2, -3)
369    /// ```
370    #[inline]
371    pub fn inverse(&self) -> Option<Self> {
372        self.0.affine_inverse().map(Self)
373    }
374}
375
376impl Default for AffineTransform {
377    /// Returns the identity `AffineTransform`.
378    fn default() -> Self {
379        Self::IDENTITY
380    }
381}
382
383// Allow easy conversion to the underlying Mat4 for sending to the GPU, etc.
384impl From<AffineTransform> for Mat4 {
385    /// Converts the `AffineTransform` into its inner `Mat4`.
386    #[inline]
387    fn from(transform: AffineTransform) -> Self {
388        transform.0
389    }
390}
391
392impl From<Mat4> for AffineTransform {
393    /// Converts a `Mat4` into an `AffineTransform`.
394    ///
395    /// # Panics
396    ///
397    /// Panics if the matrix is not a valid affine transformation.
398    #[inline]
399    fn from(val: Mat4) -> Self {
400        // Validate that the matrix is affine (last row must be [0, 0, 0, 1])
401        let last_row = val.get_row(3);
402        assert!(
403            last_row == Vec4::new(0.0, 0.0, 0.0, 1.0),
404            "Matrix is not a valid affine transformation"
405        );
406        AffineTransform(val)
407    }
408}