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 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 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 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 fn set_rotation_euler_internal(&mut self, pitch_x: f32, yaw_y: f32, roll_z: f32) {
102 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 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 fn set_rotation_quat_internal(&mut self, quat_xyzw: [f32; 4]) {
131 self.transform.rotation = quat_xyzw;
132 self.recompute_model();
133 }
134
135 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 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 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 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 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}