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}