Skip to main content

goud_engine/ecs/components/transform2d/
ops.rs

1//! Operations and methods for [`Transform2D`].
2//!
3//! Provides mutation (translate, rotate, scale), direction queries,
4//! matrix generation, point/direction transforms, and interpolation.
5
6use std::f32::consts::PI;
7
8use crate::core::math::Vec2;
9
10use super::mat3x3::Mat3x3;
11use super::types::Transform2D;
12
13impl Transform2D {
14    // =========================================================================
15    // Position Methods
16    // =========================================================================
17
18    /// Translates the transform by the given offset in world space.
19    #[inline]
20    pub fn translate(&mut self, offset: Vec2) {
21        self.position = self.position + offset;
22    }
23
24    /// Translates the transform in local space.
25    ///
26    /// The offset is rotated by the transform's rotation before being applied.
27    #[inline]
28    pub fn translate_local(&mut self, offset: Vec2) {
29        let (sin, cos) = self.rotation.sin_cos();
30        let rotated = Vec2::new(
31            offset.x * cos - offset.y * sin,
32            offset.x * sin + offset.y * cos,
33        );
34        self.position = self.position + rotated;
35    }
36
37    /// Sets the position of the transform.
38    #[inline]
39    pub fn set_position(&mut self, position: Vec2) {
40        self.position = position;
41    }
42
43    // =========================================================================
44    // Rotation Methods
45    // =========================================================================
46
47    /// Rotates the transform by the given angle in radians.
48    #[inline]
49    pub fn rotate(&mut self, angle: f32) {
50        self.rotation = normalize_angle(self.rotation + angle);
51    }
52
53    /// Rotates the transform by the given angle in degrees.
54    #[inline]
55    pub fn rotate_degrees(&mut self, degrees: f32) {
56        self.rotate(degrees.to_radians());
57    }
58
59    /// Sets the rotation angle in radians.
60    #[inline]
61    pub fn set_rotation(&mut self, rotation: f32) {
62        self.rotation = normalize_angle(rotation);
63    }
64
65    /// Sets the rotation angle in degrees.
66    #[inline]
67    pub fn set_rotation_degrees(&mut self, degrees: f32) {
68        self.set_rotation(degrees.to_radians());
69    }
70
71    /// Returns the rotation angle in degrees.
72    #[inline]
73    pub fn rotation_degrees(&self) -> f32 {
74        self.rotation.to_degrees()
75    }
76
77    /// Makes the transform look at a target position.
78    #[inline]
79    pub fn look_at_target(&mut self, target: Vec2) {
80        let direction = target - self.position;
81        self.rotation = direction.y.atan2(direction.x);
82    }
83
84    // =========================================================================
85    // Scale Methods
86    // =========================================================================
87
88    /// Sets the scale of the transform.
89    #[inline]
90    pub fn set_scale(&mut self, scale: Vec2) {
91        self.scale = scale;
92    }
93
94    /// Sets uniform scale on both axes.
95    #[inline]
96    pub fn set_scale_uniform(&mut self, scale: f32) {
97        self.scale = Vec2::new(scale, scale);
98    }
99
100    /// Multiplies the current scale by the given factors.
101    #[inline]
102    pub fn scale_by(&mut self, factors: Vec2) {
103        self.scale = Vec2::new(self.scale.x * factors.x, self.scale.y * factors.y);
104    }
105
106    // =========================================================================
107    // Direction Vectors
108    // =========================================================================
109
110    /// Returns the forward direction vector (positive X axis after rotation).
111    #[inline]
112    pub fn forward(&self) -> Vec2 {
113        let (sin, cos) = self.rotation.sin_cos();
114        Vec2::new(cos, sin)
115    }
116
117    /// Returns the right direction vector (positive Y axis after rotation).
118    ///
119    /// This is perpendicular to forward, rotated 90 degrees counter-clockwise.
120    #[inline]
121    pub fn right(&self) -> Vec2 {
122        let (sin, cos) = self.rotation.sin_cos();
123        Vec2::new(-sin, cos)
124    }
125
126    /// Returns the backward direction vector (negative X axis after rotation).
127    #[inline]
128    pub fn backward(&self) -> Vec2 {
129        -self.forward()
130    }
131
132    /// Returns the left direction vector (negative Y axis after rotation).
133    #[inline]
134    pub fn left(&self) -> Vec2 {
135        -self.right()
136    }
137
138    // =========================================================================
139    // Matrix Generation
140    // =========================================================================
141
142    /// Computes the 3x3 transformation matrix.
143    ///
144    /// The matrix represents the combined transformation: Scale * Rotation * Translation
145    /// (applied in that order when transforming points).
146    #[inline]
147    pub fn matrix(&self) -> Mat3x3 {
148        let (sin, cos) = self.rotation.sin_cos();
149        let sx = self.scale.x;
150        let sy = self.scale.y;
151
152        // Combined SRT matrix in column-major order:
153        // | cos*sx  -sin*sy  tx |
154        // | sin*sx   cos*sy  ty |
155        // |   0        0      1 |
156        Mat3x3 {
157            m: [
158                cos * sx,
159                sin * sx,
160                0.0,
161                -sin * sy,
162                cos * sy,
163                0.0,
164                self.position.x,
165                self.position.y,
166                1.0,
167            ],
168        }
169    }
170
171    /// Computes the inverse transformation matrix.
172    ///
173    /// Useful for converting world-space to local-space.
174    #[inline]
175    pub fn matrix_inverse(&self) -> Mat3x3 {
176        let (sin, cos) = self.rotation.sin_cos();
177        let inv_sx = 1.0 / self.scale.x;
178        let inv_sy = 1.0 / self.scale.y;
179
180        // Inverse of TRS = S^-1 * R^-1 * T^-1
181        // R^-1 is rotation by -angle, which has cos unchanged and sin negated
182
183        // First, the inverse rotation matrix (transpose for orthogonal matrix)
184        // R^-1 = [[cos, sin], [-sin, cos]]  (note: sin is now positive in top row)
185
186        // The combined S^-1 * R^-1 matrix:
187        // | cos/sx   sin/sx |
188        // | -sin/sy  cos/sy |
189
190        // Translation part: -(S^-1 * R^-1 * t)
191        let inv_tx = -(cos * self.position.x + sin * self.position.y) * inv_sx;
192        let inv_ty = -(-sin * self.position.x + cos * self.position.y) * inv_sy;
193
194        Mat3x3 {
195            m: [
196                cos * inv_sx,  // m[0]
197                -sin * inv_sy, // m[1]
198                0.0,           // m[2]
199                sin * inv_sx,  // m[3]
200                cos * inv_sy,  // m[4]
201                0.0,           // m[5]
202                inv_tx,        // m[6]
203                inv_ty,        // m[7]
204                1.0,           // m[8]
205            ],
206        }
207    }
208
209    /// Converts to a 4x4 matrix for use with 3D rendering APIs.
210    ///
211    /// The result places the 2D transform in the XY plane at Z=0.
212    #[inline]
213    pub fn to_mat4(&self) -> [f32; 16] {
214        self.matrix().to_mat4()
215    }
216
217    // =========================================================================
218    // Point Transformation
219    // =========================================================================
220
221    /// Transforms a point from local space to world space.
222    #[inline]
223    pub fn transform_point(&self, point: Vec2) -> Vec2 {
224        let (sin, cos) = self.rotation.sin_cos();
225        let scaled = Vec2::new(point.x * self.scale.x, point.y * self.scale.y);
226        let rotated = Vec2::new(
227            scaled.x * cos - scaled.y * sin,
228            scaled.x * sin + scaled.y * cos,
229        );
230        rotated + self.position
231    }
232
233    /// Transforms a direction from local space to world space.
234    ///
235    /// Unlike points, directions are not affected by translation.
236    #[inline]
237    pub fn transform_direction(&self, direction: Vec2) -> Vec2 {
238        let (sin, cos) = self.rotation.sin_cos();
239        Vec2::new(
240            direction.x * cos - direction.y * sin,
241            direction.x * sin + direction.y * cos,
242        )
243    }
244
245    /// Transforms a point from world space to local space.
246    #[inline]
247    pub fn inverse_transform_point(&self, point: Vec2) -> Vec2 {
248        let translated = point - self.position;
249        let (sin, cos) = self.rotation.sin_cos();
250        let rotated = Vec2::new(
251            translated.x * cos + translated.y * sin,
252            -translated.x * sin + translated.y * cos,
253        );
254        Vec2::new(rotated.x / self.scale.x, rotated.y / self.scale.y)
255    }
256
257    /// Transforms a direction from world space to local space.
258    #[inline]
259    pub fn inverse_transform_direction(&self, direction: Vec2) -> Vec2 {
260        let (sin, cos) = self.rotation.sin_cos();
261        Vec2::new(
262            direction.x * cos + direction.y * sin,
263            -direction.x * sin + direction.y * cos,
264        )
265    }
266
267    // =========================================================================
268    // Interpolation
269    // =========================================================================
270
271    /// Linearly interpolates between two transforms.
272    ///
273    /// Position and scale are linearly interpolated, rotation uses
274    /// shortest-path angle interpolation.
275    #[inline]
276    pub fn lerp(self, other: Self, t: f32) -> Self {
277        Self {
278            position: self.position.lerp(other.position, t),
279            rotation: lerp_angle(self.rotation, other.rotation, t),
280            scale: self.scale.lerp(other.scale, t),
281        }
282    }
283}
284
285/// Normalizes an angle to the range [-PI, PI).
286#[inline]
287pub(super) fn normalize_angle(angle: f32) -> f32 {
288    let mut result = angle % (2.0 * PI);
289    if result >= PI {
290        result -= 2.0 * PI;
291    } else if result < -PI {
292        result += 2.0 * PI;
293    }
294    result
295}
296
297/// Linearly interpolates between two angles using shortest path.
298#[inline]
299pub(super) fn lerp_angle(from: f32, to: f32, t: f32) -> f32 {
300    let mut diff = to - from;
301
302    // Wrap to [-PI, PI]
303    while diff > PI {
304        diff -= 2.0 * PI;
305    }
306    while diff < -PI {
307        diff += 2.0 * PI;
308    }
309
310    normalize_angle(from + diff * t)
311}