Skip to main content

mittens_engine/engine/ecs/component/
transform.rs

1use super::Component;
2use crate::engine::ecs::{ComponentId, IntentValue, SignalEmitter};
3use crate::engine::graphics::primitives::Transform;
4use crate::utils::math::{
5    mat_to_quat, quat_normalize, shortest_arc_quat, vec3_cross, vec3_dot, vec3_len, vec3_normalize,
6    vec3_scale, vec3_sub,
7};
8
9#[derive(Debug, Clone, Copy)]
10pub struct TransformComponent {
11    /// Engine-wide transform type (also used by renderer/VisualWorld).
12    pub transform: Transform,
13
14    pub pending_look_at_target_world: Option<[f32; 3]>,
15
16    component: Option<ComponentId>,
17}
18
19impl TransformComponent {
20    pub fn new() -> Self {
21        let transform = Transform::default();
22        Self {
23            transform,
24            pending_look_at_target_world: None,
25            component: None,
26        }
27    }
28
29    fn recompute_model(&mut self) {
30        self.transform.recompute_model();
31    }
32
33    pub fn with_position(mut self, x: f32, y: f32, z: f32) -> Self {
34        self.transform.translation = [x, y, z];
35        self.recompute_model();
36        self
37    }
38
39    pub fn with_scale(mut self, x: f32, y: f32, z: f32) -> Self {
40        self.transform.scale = [x, y, z];
41        self.recompute_model();
42        self
43    }
44
45    /// Builder-style: set rotation from Euler angles (radians), returns Self.
46    pub fn with_rotation_euler(mut self, pitch_x: f32, yaw_y: f32, roll_z: f32) -> Self {
47        self.set_rotation_euler_internal(pitch_x, yaw_y, roll_z);
48        self
49    }
50
51    /// Builder-style: set rotation from a quaternion (xyzw), returns Self.
52    pub fn with_rotation_quat(mut self, quat_xyzw: [f32; 4]) -> Self {
53        self.set_rotation_quat_internal(quat_xyzw);
54        self
55    }
56
57    pub fn with_looking_at(mut self, target_world: [f32; 3]) -> Self {
58        self.pending_look_at_target_world = Some(target_world);
59        self
60    }
61
62    pub fn look_at_world_rotation(
63        world_position: [f32; 3],
64        target_world: [f32; 3],
65    ) -> Option<[f32; 4]> {
66        let forward = vec3_sub(target_world, world_position);
67        if vec3_len(forward) <= 1e-5 {
68            return None;
69        }
70
71        let z = vec3_normalize(forward);
72        let fallback_up = if z[1].abs() < 0.99 {
73            [0.0, 1.0, 0.0]
74        } else {
75            [1.0, 0.0, 0.0]
76        };
77        let projected_up = vec3_sub(fallback_up, vec3_scale(z, vec3_dot(fallback_up, z)));
78        let y = if vec3_len(projected_up) > 1e-5 {
79            vec3_normalize(projected_up)
80        } else {
81            fallback_up
82        };
83        let x = vec3_normalize(vec3_cross(y, z));
84        let y = vec3_normalize(vec3_cross(z, x));
85
86        let basis = [
87            [x[0], x[1], x[2], 0.0],
88            [y[0], y[1], y[2], 0.0],
89            [z[0], z[1], z[2], 0.0],
90            [0.0, 0.0, 0.0, 1.0],
91        ];
92        let quat = quat_normalize(mat_to_quat(basis));
93        Some(if quat == [0.0, 0.0, 0.0, 1.0] && z != [0.0, 0.0, 1.0] {
94            shortest_arc_quat([0.0, 0.0, 1.0], z)
95        } else {
96            quat
97        })
98    }
99
100    /// Private helper: computes and sets quaternion from euler angles, then recomputes model.
101    fn set_rotation_euler_internal(&mut self, pitch_x: f32, yaw_y: f32, roll_z: f32) {
102        // Minimal Euler->quat (XYZ intrinsic) implementation.
103        let (sx, cx) = (0.5 * pitch_x).sin_cos();
104        let (sy, cy) = (0.5 * yaw_y).sin_cos();
105        let (sz, cz) = (0.5 * roll_z).sin_cos();
106
107        // q = qx * qy * qz
108        let qx = [sx, 0.0, 0.0, cx];
109        let qy = [0.0, sy, 0.0, cy];
110        let qz = [0.0, 0.0, sz, cz];
111
112        fn quat_mul(a: [f32; 4], b: [f32; 4]) -> [f32; 4] {
113            let (ax, ay, az, aw) = (a[0], a[1], a[2], a[3]);
114            let (bx, by, bz, bw) = (b[0], b[1], b[2], b[3]);
115            [
116                aw * bx + ax * bw + ay * bz - az * by,
117                aw * by - ax * bz + ay * bw + az * bx,
118                aw * bz + ax * by - ay * bx + az * bw,
119                aw * bw - ax * bx - ay * by - az * bz,
120            ]
121        }
122
123        let qxy = quat_mul(qx, qy);
124        let q = quat_mul(qxy, qz);
125        self.transform.rotation = q;
126        self.recompute_model();
127    }
128
129    /// Private helper: sets quaternion directly, then recomputes model.
130    fn set_rotation_quat_internal(&mut self, quat_xyzw: [f32; 4]) {
131        self.transform.rotation = quat_xyzw;
132        self.recompute_model();
133    }
134
135    /// Set rotation from Euler angles (radians), XYZ order, and queue update.
136    pub fn set_rotation_euler(
137        &mut self,
138        emit: &mut dyn SignalEmitter,
139        pitch_x: f32,
140        yaw_y: f32,
141        roll_z: f32,
142    ) {
143        self.set_rotation_euler_internal(pitch_x, yaw_y, roll_z);
144
145        let Some(cid) = self.component else {
146            return;
147        };
148        emit.push_intent_now(
149            cid,
150            IntentValue::UpdateTransform {
151                component_ids: vec![cid],
152                translation: self.transform.translation,
153                rotation_quat_xyzw: self.transform.rotation,
154                scale: self.transform.scale,
155            },
156        );
157    }
158
159    /// Set rotation from a quaternion (xyzw) and queue update.
160    pub fn set_rotation_quat(&mut self, emit: &mut dyn SignalEmitter, quat_xyzw: [f32; 4]) {
161        self.set_rotation_quat_internal(quat_xyzw);
162
163        let Some(cid) = self.component else {
164            return;
165        };
166        emit.push_intent_now(
167            cid,
168            IntentValue::UpdateTransform {
169                component_ids: vec![cid],
170                translation: self.transform.translation,
171                rotation_quat_xyzw: self.transform.rotation,
172                scale: self.transform.scale,
173            },
174        );
175    }
176
177    /// Set translation and queue update.
178    pub fn set_position(&mut self, emit: &mut dyn SignalEmitter, x: f32, y: f32, z: f32) {
179        self.transform.translation = [x, y, z];
180        self.recompute_model();
181        let Some(cid) = self.component else {
182            return;
183        };
184        emit.push_intent_now(
185            cid,
186            IntentValue::UpdateTransform {
187                component_ids: vec![cid],
188                translation: self.transform.translation,
189                rotation_quat_xyzw: self.transform.rotation,
190                scale: self.transform.scale,
191            },
192        );
193    }
194
195    /// Set non-uniform scale and queue update.
196    pub fn set_scale(&mut self, emit: &mut dyn SignalEmitter, x: f32, y: f32, z: f32) {
197        self.transform.scale = [x, y, z];
198        self.recompute_model();
199        let Some(cid) = self.component else {
200            return;
201        };
202        emit.push_intent_now(
203            cid,
204            IntentValue::UpdateTransform {
205                component_ids: vec![cid],
206                translation: self.transform.translation,
207                rotation_quat_xyzw: self.transform.rotation,
208                scale: self.transform.scale,
209            },
210        );
211    }
212}
213
214impl Component for TransformComponent {
215    fn name(&self) -> &'static str {
216        "transform"
217    }
218
219    fn set_id(&mut self, component: ComponentId) {
220        self.component = Some(component);
221    }
222
223    fn as_any(&self) -> &dyn std::any::Any {
224        self
225    }
226
227    fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
228        self
229    }
230
231    fn init(&mut self, emit: &mut dyn crate::engine::ecs::SignalEmitter, component: ComponentId) {
232        emit.push_intent_now(
233            component,
234            crate::engine::ecs::IntentValue::RegisterTransform {
235                component_ids: vec![component],
236            },
237        );
238        if let Some(target_world) = self.pending_look_at_target_world.take() {
239            emit.push_intent_now(
240                component,
241                crate::engine::ecs::IntentValue::LookAt {
242                    component_ids: vec![component],
243                    target_world,
244                },
245            );
246        }
247    }
248
249    fn to_mms_ast(
250        &self,
251        _world: &crate::engine::ecs::World,
252    ) -> crate::scripting::ast::ComponentExpression {
253        use crate::engine::ecs::component::ce_helpers::*;
254        let t = &self.transform;
255        // Emit position, rotation_quat (lossless), scale — matches the
256        // builder vocabulary in `component_registry::apply_transform_builder`.
257        ce_call(
258            "Transform",
259            "position",
260            nums(t.translation.iter().map(|&v| v as f64)),
261        )
262        .with_call("rotation_quat", nums(t.rotation.iter().map(|&v| v as f64)))
263        .with_call("scale", nums(t.scale.iter().map(|&v| v as f64)))
264    }
265}
266
267impl Default for TransformComponent {
268    fn default() -> Self {
269        Self::new()
270    }
271}
272
273#[cfg(test)]
274mod tests {
275    use super::TransformComponent;
276    use crate::engine::ecs::system::TransformSystem;
277    use crate::engine::ecs::{CommandQueue, IntentValue, SignalEmitter, SystemWorld, World};
278    use crate::engine::graphics::{RenderAssets, VisualWorld};
279    use crate::utils::math::{mat_to_quat, quat_rotate_vec3, vec3_dot, vec3_len, vec3_normalize};
280
281    fn flush(
282        world: &mut World,
283        systems: &mut SystemWorld,
284        visuals: &mut VisualWorld,
285        render_assets: &mut RenderAssets,
286        queue: &mut CommandQueue,
287    ) {
288        queue.flush(world, systems, visuals, render_assets);
289    }
290
291    fn world_forward(world: &World, cid: crate::engine::ecs::ComponentId) -> [f32; 3] {
292        let world_rot = mat_to_quat(TransformSystem::world_model(world, cid).expect("world model"));
293        vec3_normalize(quat_rotate_vec3(world_rot, [0.0, 0.0, 1.0]))
294    }
295
296    fn assert_faces_target(world: &World, cid: crate::engine::ecs::ComponentId, target: [f32; 3]) {
297        let position = TransformSystem::world_position(world, cid).expect("world position");
298        let desired = vec3_normalize([
299            target[0] - position[0],
300            target[1] - position[1],
301            target[2] - position[2],
302        ]);
303        let actual = world_forward(world, cid);
304        assert!(
305            vec3_dot(actual, desired) > 0.999,
306            "expected forward {:?} to face {:?}",
307            actual,
308            desired
309        );
310    }
311
312    #[test]
313    fn look_at_executor_preserves_translation_and_scale() {
314        let mut world = World::default();
315        let mut systems = SystemWorld::default();
316        let mut visuals = VisualWorld::default();
317        let mut render_assets = RenderAssets::new();
318        let mut queue = CommandQueue::new();
319
320        let root = world.add_component(
321            TransformComponent::new()
322                .with_position(1.0, 2.0, 3.0)
323                .with_scale(2.0, 3.0, 4.0),
324        );
325        world.init_component_tree(root, &mut queue);
326        flush(
327            &mut world,
328            &mut systems,
329            &mut visuals,
330            &mut render_assets,
331            &mut queue,
332        );
333
334        queue.push_intent_now(
335            root,
336            IntentValue::LookAt {
337                component_ids: vec![root],
338                target_world: [1.0, 2.0, 8.0],
339            },
340        );
341        flush(
342            &mut world,
343            &mut systems,
344            &mut visuals,
345            &mut render_assets,
346            &mut queue,
347        );
348
349        let transform = world
350            .get_component_by_id_as::<TransformComponent>(root)
351            .expect("transform");
352        assert_eq!(transform.transform.translation, [1.0, 2.0, 3.0]);
353        assert_eq!(transform.transform.scale, [2.0, 3.0, 4.0]);
354        assert_faces_target(&world, root, [1.0, 2.0, 8.0]);
355    }
356
357    #[test]
358    fn look_at_executor_respects_rotated_parent_world_space_target() {
359        let mut world = World::default();
360        let mut systems = SystemWorld::default();
361        let mut visuals = VisualWorld::default();
362        let mut render_assets = RenderAssets::new();
363        let mut queue = CommandQueue::new();
364
365        let parent =
366            world.add_component(TransformComponent::new().with_rotation_euler(0.0, 1.1, 0.0));
367        let child = world.add_component(TransformComponent::new().with_position(0.0, 0.0, 2.0));
368        world.add_child(parent, child).unwrap();
369        world.init_component_tree(parent, &mut queue);
370        flush(
371            &mut world,
372            &mut systems,
373            &mut visuals,
374            &mut render_assets,
375            &mut queue,
376        );
377
378        let target = [4.0, 1.0, -3.0];
379        queue.push_intent_now(
380            child,
381            IntentValue::LookAt {
382                component_ids: vec![child],
383                target_world: target,
384            },
385        );
386        flush(
387            &mut world,
388            &mut systems,
389            &mut visuals,
390            &mut render_assets,
391            &mut queue,
392        );
393
394        assert_faces_target(&world, child, target);
395    }
396
397    #[test]
398    fn authored_pending_look_at_is_order_independent() {
399        let mut world = World::default();
400        let mut systems = SystemWorld::default();
401        let mut visuals = VisualWorld::default();
402        let mut render_assets = RenderAssets::new();
403        let mut queue = CommandQueue::new();
404
405        let a = world.add_component(
406            TransformComponent::new()
407                .with_position(1.0, 0.0, 0.0)
408                .with_looking_at([3.0, 0.5, 1.0]),
409        );
410        let b = world.add_component(
411            TransformComponent::new()
412                .with_looking_at([3.0, 0.5, 1.0])
413                .with_position(1.0, 0.0, 0.0),
414        );
415        world.init_component_tree(a, &mut queue);
416        world.init_component_tree(b, &mut queue);
417        flush(
418            &mut world,
419            &mut systems,
420            &mut visuals,
421            &mut render_assets,
422            &mut queue,
423        );
424
425        let rot_a = world
426            .get_component_by_id_as::<TransformComponent>(a)
427            .expect("a")
428            .transform
429            .rotation;
430        let rot_b = world
431            .get_component_by_id_as::<TransformComponent>(b)
432            .expect("b")
433            .transform
434            .rotation;
435        let dot =
436            rot_a[0] * rot_b[0] + rot_a[1] * rot_b[1] + rot_a[2] * rot_b[2] + rot_a[3] * rot_b[3];
437        assert!(
438            dot.abs() > 0.9999,
439            "expected equivalent rotations, got {:?} vs {:?}",
440            rot_a,
441            rot_b
442        );
443    }
444
445    #[test]
446    fn look_at_degenerate_and_collinear_cases_are_stable() {
447        let mut world = World::default();
448        let mut systems = SystemWorld::default();
449        let mut visuals = VisualWorld::default();
450        let mut render_assets = RenderAssets::new();
451        let mut queue = CommandQueue::new();
452
453        let same_target = world.add_component(
454            TransformComponent::new()
455                .with_position(2.0, 3.0, 4.0)
456                .with_rotation_quat([0.0, 0.70710677, 0.0, 0.70710677]),
457        );
458        let vertical_target = world.add_component(TransformComponent::new());
459        world.init_component_tree(same_target, &mut queue);
460        world.init_component_tree(vertical_target, &mut queue);
461        flush(
462            &mut world,
463            &mut systems,
464            &mut visuals,
465            &mut render_assets,
466            &mut queue,
467        );
468
469        let original = world
470            .get_component_by_id_as::<TransformComponent>(same_target)
471            .expect("same_target")
472            .transform
473            .rotation;
474        queue.push_intent_now(
475            same_target,
476            IntentValue::LookAt {
477                component_ids: vec![same_target],
478                target_world: [2.0, 3.0, 4.0],
479            },
480        );
481        queue.push_intent_now(
482            vertical_target,
483            IntentValue::LookAt {
484                component_ids: vec![vertical_target],
485                target_world: [0.0, 5.0, 0.0],
486            },
487        );
488        flush(
489            &mut world,
490            &mut systems,
491            &mut visuals,
492            &mut render_assets,
493            &mut queue,
494        );
495
496        let same_rotation = world
497            .get_component_by_id_as::<TransformComponent>(same_target)
498            .expect("same_target")
499            .transform
500            .rotation;
501        assert_eq!(same_rotation, original);
502
503        let vertical_rotation = world
504            .get_component_by_id_as::<TransformComponent>(vertical_target)
505            .expect("vertical_target")
506            .transform
507            .rotation;
508        assert!(vertical_rotation.iter().all(|v| v.is_finite()));
509        assert!(vec3_len(world_forward(&world, vertical_target)) > 0.99);
510        assert_faces_target(&world, vertical_target, [0.0, 5.0, 0.0]);
511    }
512}