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}