1use crate::engine::ecs::component::{
2 GestureCoordType, GestureCoordTypeComponent, SignalRouteUpwardComponent,
3 TransformCameraSpecificComponent, TransformCameraSpecificMode, TransformComponent,
4 TransformDropComponent, TransformForkTRSComponent, TransformGizmoAxis, TransformGizmoComponent,
5 TransformGizmoRotateComponent, TransformGizmoScaleComponent, TransformGizmoTranslateComponent,
6 TransformMapRotationComponent, TransformMapScaleComponent, TransformMapTranslationComponent,
7};
8use crate::engine::ecs::system::GridSystem;
9use crate::engine::ecs::system::editor::context::EditorContextState;
10use crate::engine::ecs::{
11 ComponentId, EventSignal, IntentValue, RxWorld, SignalEmitter, SignalKind, World,
12};
13use crate::engine::user_input::InputState;
14use std::sync::OnceLock;
15use std::sync::atomic::{AtomicUsize, Ordering};
16use std::sync::{Arc, Mutex};
17
18#[derive(Debug, Clone, Copy)]
19enum TransformGizmoOp {
20 Translate(TransformGizmoAxis),
21 Rotate(TransformGizmoAxis),
22 Scale(TransformGizmoAxis),
23}
24
25#[derive(Debug, Default)]
26pub struct TransformGizmoSystem {
27 editor_context_state: Option<Arc<Mutex<EditorContextState>>>,
28 live_gizmos: std::collections::HashSet<ComponentId>,
29}
30
31impl TransformGizmoSystem {
32 pub fn new() -> Self {
33 Self::default()
34 }
35
36 pub fn set_editor_context_state(
37 &mut self,
38 editor_context_state: Arc<Mutex<EditorContextState>>,
39 ) {
40 self.editor_context_state = Some(editor_context_state);
41 }
42
43 pub fn update_camera_scales(
46 &mut self,
47 world: &mut World,
48 visuals: &crate::engine::graphics::VisualWorld,
49 has_window_camera: bool,
50 ) -> Vec<ComponentId> {
51 use crate::engine::graphics::CameraTarget;
52 const REFERENCE_DISTANCE: f32 = 4.0;
53 const MIN_SCALE: f32 = 0.02;
54 const MAX_SCALE: f32 = 20.0;
55
56 self.live_gizmos.retain(|id| {
57 world
58 .get_component_by_id_as::<TransformGizmoComponent>(*id)
59 .is_some()
60 });
61 let stereo_eyes = visuals
62 .visual_camera(CameraTarget::Xr)
63 .filter(|c| visuals.active_xr_camera().is_some() && !c.eyes.is_empty());
64 let stereo_active = stereo_eyes.is_some();
65 let mut changed = Vec::new();
66
67 for gizmo in self.live_gizmos.iter().copied().collect::<Vec<_>>() {
68 let Some(g) = world
69 .get_component_by_id_as::<TransformGizmoComponent>(gizmo)
70 .copied()
71 else {
72 continue;
73 };
74 let Some(anchor) = g.visual_root else {
75 continue;
76 };
77 let p = world
78 .get_component_by_id_as::<TransformComponent>(anchor)
79 .map(|t| t.transform.matrix_world[3])
80 .unwrap_or([0.0, 0.0, 0.0, 1.0]);
81 let views: Vec<[[f32; 4]; 4]> = if let Some(c) = stereo_eyes {
82 c.eyes.iter().map(|e| e.view).collect()
83 } else if has_window_camera {
84 visuals
85 .visual_camera(CameraTarget::Window)
86 .and_then(|c| c.eyes.first())
87 .map(|e| vec![e.view])
88 .unwrap_or_default()
89 } else {
90 Vec::new()
91 };
92 let depths: Vec<f32> = views
93 .iter()
94 .map(|v| {
95 let z = v[0][2] * p[0] + v[1][2] * p[1] + v[2][2] * p[2] + v[3][2];
96 -z
97 })
98 .collect();
99 if depths.is_empty() || depths.iter().any(|d| !d.is_finite() || *d <= 0.0) {
100 continue;
101 }
102 let depth = depths.iter().sum::<f32>() / depths.len() as f32;
103 let scale = (g.scale * depth / REFERENCE_DISTANCE).clamp(MIN_SCALE, MAX_SCALE);
104
105 let desired_mode = if stereo_active {
106 TransformCameraSpecificMode::Stereoscopic
107 } else {
108 TransformCameraSpecificMode::Monoscopic
109 };
110 let settings = world
111 .children_of(anchor)
112 .iter()
113 .copied()
114 .find_map(|marker| {
115 let c =
116 world.get_component_by_id_as::<TransformCameraSpecificComponent>(marker)?;
117 (c.mode == desired_mode)
118 .then(|| {
119 world.children_of(marker).iter().copied().find(|id| {
120 world
121 .get_component_by_id_as::<TransformComponent>(*id)
122 .is_some()
123 })
124 })
125 .flatten()
126 });
127 if let Some(settings) = settings {
128 if let Some(t) = world.get_component_by_id_as_mut::<TransformComponent>(settings) {
129 t.transform.scale = [scale; 3];
130 t.transform.recompute_model();
131 changed.push(anchor);
132 }
133 }
134 }
135 changed
136 }
137
138 pub fn install_scoped_handlers_for_gizmo(&mut self, rx: &mut RxWorld, gizmo_root: ComponentId) {
143 rx.add_handler(
144 SignalKind::ParentChanged,
145 gizmo_root,
146 Self::on_parent_changed,
147 );
148 rx.add_handler(SignalKind::DragStart, gizmo_root, Self::on_drag_start);
149 let editor_context_state = self.editor_context_state.clone();
150 rx.add_handler_closure_named(
151 SignalKind::DragMove,
152 gizmo_root,
153 None,
154 move |world, emit, env| {
155 Self::on_drag_move(world, emit, env, editor_context_state.clone());
156 },
157 );
158 rx.add_handler(SignalKind::DragEnd, gizmo_root, Self::on_drag_end);
159 }
160
161 fn debug_drag_plane_enabled() -> bool {
162 static ENABLED: OnceLock<bool> = OnceLock::new();
163 *ENABLED.get_or_init(|| {
164 let v = std::env::var("CAT_DEBUG_GIZMO_DRAG_PLANE").unwrap_or_default();
165 let v = v.trim().to_ascii_lowercase();
166 matches!(v.as_str(), "1" | "true" | "yes" | "on")
167 })
168 }
169
170 fn debug_enabled() -> bool {
171 static ENABLED: OnceLock<bool> = OnceLock::new();
172 *ENABLED.get_or_init(|| {
173 let v = std::env::var("CAT_DEBUG_GIZMO").unwrap_or_default();
174 let v = v.trim().to_ascii_lowercase();
175 matches!(v.as_str(), "1" | "true" | "yes" | "on")
176 })
177 }
178
179 fn debug_target_enabled() -> bool {
180 static ENABLED: OnceLock<bool> = OnceLock::new();
181 *ENABLED.get_or_init(|| {
182 let v = std::env::var("CAT_DEBUG_GIZMO_TARGET").unwrap_or_default();
183 let v = v.trim().to_ascii_lowercase();
184 matches!(v.as_str(), "1" | "true" | "yes" | "on")
185 })
186 }
187
188 fn debug_sanity_enabled() -> bool {
189 static ENABLED: OnceLock<bool> = OnceLock::new();
190 *ENABLED.get_or_init(|| {
191 let v = std::env::var("CAT_DEBUG_GIZMO_SANITY").unwrap_or_default();
192 let v = v.trim().to_ascii_lowercase();
193 matches!(v.as_str(), "1" | "true" | "yes" | "on")
194 })
195 }
196
197 fn debug_apply_enabled() -> bool {
198 static ENABLED: OnceLock<bool> = OnceLock::new();
199 *ENABLED.get_or_init(|| {
200 let v = std::env::var("CAT_DEBUG_GIZMO_APPLY").unwrap_or_default();
201 let v = v.trim().to_ascii_lowercase();
202 matches!(v.as_str(), "1" | "true" | "yes" | "on")
203 })
204 }
205
206 fn debug_hit_enabled() -> bool {
207 static ENABLED: OnceLock<bool> = OnceLock::new();
208 *ENABLED.get_or_init(|| {
209 let v = std::env::var("CAT_DEBUG_GIZMO_HIT").unwrap_or_default();
210 let v = v.trim().to_ascii_lowercase();
211 matches!(v.as_str(), "1" | "true" | "yes" | "on")
212 })
213 }
214
215 fn log_apply(world: &World, op: &str, target_transform: ComponentId, extra: &str) {
216 static LOG_COUNT: AtomicUsize = AtomicUsize::new(0);
217 let n = LOG_COUNT.fetch_add(1, Ordering::Relaxed);
218 if n >= 96 {
219 return;
220 }
221
222 let name = world
223 .get_component_record(target_transform)
224 .map(|n| {
225 if n.name.is_empty() {
226 n.component_type.clone()
227 } else {
228 format!("{}: {}", n.component_type, n.name)
229 }
230 })
231 .unwrap_or_else(|| "<missing>".to_string());
232
233 println!(
234 "[TransformGizmoSystem] APPLY op={} target={:?} '{}' {}",
235 op, target_transform, name, extra
236 );
237 }
238
239 fn sanity_check_transform_values(
240 world: &World,
241 target_transform: ComponentId,
242 translation: [f32; 3],
243 rotation_xyzw: [f32; 4],
244 scale: [f32; 3],
245 ) {
246 fn finite_f32(x: f32) -> bool {
247 x.is_finite()
248 }
249 fn finite3(v: [f32; 3]) -> bool {
250 finite_f32(v[0]) && finite_f32(v[1]) && finite_f32(v[2])
251 }
252 fn finite4(v: [f32; 4]) -> bool {
253 finite_f32(v[0]) && finite_f32(v[1]) && finite_f32(v[2]) && finite_f32(v[3])
254 }
255 fn too_large3(v: [f32; 3]) -> bool {
256 let lim = 1.0e6_f32;
257 v[0].abs() > lim || v[1].abs() > lim || v[2].abs() > lim
258 }
259
260 if finite3(translation)
261 && finite4(rotation_xyzw)
262 && finite3(scale)
263 && !too_large3(translation)
264 && !too_large3(scale)
265 {
266 return;
267 }
268
269 static LOG_COUNT: AtomicUsize = AtomicUsize::new(0);
270 let n = LOG_COUNT.fetch_add(1, Ordering::Relaxed);
271 if n >= 32 {
272 return;
273 }
274
275 let name = world
276 .get_component_record(target_transform)
277 .map(|n| {
278 if n.name.is_empty() {
279 n.component_type.clone()
280 } else {
281 format!("{}: {}", n.component_type, n.name)
282 }
283 })
284 .unwrap_or_else(|| "<missing>".to_string());
285
286 println!(
287 "[TransformGizmoSystem] SANITY target={:?} '{}' translation={:?} rotation={:?} scale={:?}",
288 target_transform, name, translation, rotation_xyzw, scale
289 );
290 }
291
292 fn apply_route_upward_if_present(
293 world: &World,
294 kind_name: &str,
295 start: ComponentId,
296 ) -> ComponentId {
297 let mut cur_target = start;
298
299 for &ch in world.children_of(start) {
302 let Some(op) = world.get_component_by_id_as::<SignalRouteUpwardComponent>(ch) else {
303 continue;
304 };
305
306 let want = op.intent_kind.trim();
307 let applies = want.is_empty() || want == "any" || want == kind_name;
308 if !applies {
309 continue;
310 }
311
312 let parent_type = op.parent_type.trim();
313 if parent_type.is_empty() {
314 continue;
315 }
316
317 let mut cur = world.parent_of(cur_target);
319 while let Some(cid) = cur {
320 let Some(node) = world.get_component_node(cid) else {
321 break;
322 };
323
324 if node.component.name() == parent_type {
325 cur_target = cid;
326 break;
327 }
328
329 cur = world.parent_of(cid);
330 }
331 }
332
333 cur_target
334 }
335
336 fn mat4_identity() -> crate::engine::graphics::primitives::TransformMatrix {
337 [
338 [1.0, 0.0, 0.0, 0.0],
339 [0.0, 1.0, 0.0, 0.0],
340 [0.0, 0.0, 1.0, 0.0],
341 [0.0, 0.0, 0.0, 1.0],
342 ]
343 }
344
345 fn mat4_mul_vec4(
346 m: crate::engine::graphics::primitives::TransformMatrix,
347 v: [f32; 4],
348 ) -> [f32; 4] {
349 [
350 m[0][0] * v[0] + m[1][0] * v[1] + m[2][0] * v[2] + m[3][0] * v[3],
351 m[0][1] * v[0] + m[1][1] * v[1] + m[2][1] * v[2] + m[3][1] * v[3],
352 m[0][2] * v[0] + m[1][2] * v[1] + m[2][2] * v[2] + m[3][2] * v[3],
353 m[0][3] * v[0] + m[1][3] * v[1] + m[2][3] * v[2] + m[3][3] * v[3],
354 ]
355 }
356
357 fn parent_transform_world_matrix(
358 world: &World,
359 transform_cid: ComponentId,
360 ) -> Option<crate::engine::graphics::primitives::TransformMatrix> {
361 let mut cur = transform_cid;
362 while let Some(parent) = world.parent_of(cur) {
363 if let Some(t) = world
364 .get_component_by_id_as::<crate::engine::ecs::component::TransformComponent>(parent)
365 {
366 return Some(t.transform.matrix_world);
367 }
368 cur = parent;
369 }
370 None
371 }
372
373 fn world_delta_to_target_local(
374 world: &World,
375 target_transform: ComponentId,
376 delta_world: [f32; 3],
377 ) -> [f32; 3] {
378 use crate::utils::math;
379
380 let parent_world = Self::parent_transform_world_matrix(world, target_transform)
381 .unwrap_or_else(Self::mat4_identity);
382 let inv_parent_world = math::mat4_inverse(parent_world).unwrap_or_else(Self::mat4_identity);
383
384 let v = Self::mat4_mul_vec4(
385 inv_parent_world,
386 [delta_world[0], delta_world[1], delta_world[2], 0.0],
387 );
388 [v[0], v[1], v[2]]
389 }
390
391 fn target_translation_local_to_world(
392 world: &World,
393 target_transform: ComponentId,
394 point_local: [f32; 3],
395 ) -> [f32; 3] {
396 let parent_world = Self::parent_transform_world_matrix(world, target_transform)
397 .unwrap_or_else(Self::mat4_identity);
398 let v = Self::mat4_mul_vec4(
399 parent_world,
400 [point_local[0], point_local[1], point_local[2], 1.0],
401 );
402 [v[0], v[1], v[2]]
403 }
404
405 fn world_point_to_target_translation_local(
406 world: &World,
407 target_transform: ComponentId,
408 point_world: [f32; 3],
409 ) -> [f32; 3] {
410 use crate::utils::math;
411
412 let parent_world = Self::parent_transform_world_matrix(world, target_transform)
413 .unwrap_or_else(Self::mat4_identity);
414 let inv_parent_world = math::mat4_inverse(parent_world).unwrap_or_else(Self::mat4_identity);
415 let v = Self::mat4_mul_vec4(
416 inv_parent_world,
417 [point_world[0], point_world[1], point_world[2], 1.0],
418 );
419 [v[0], v[1], v[2]]
420 }
421
422 fn active_snap_grid_for_translate(
423 world: &World,
424 editor_context_state: Option<Arc<Mutex<EditorContextState>>>,
425 ) -> Option<crate::engine::ecs::system::grid_system::ActiveGrid> {
426 let owner_transform = editor_context_state
427 .as_ref()
428 .and_then(|state| state.lock().ok())
429 .and_then(|state| state.active_grid_owner_transform)?;
430 let grids = GridSystem::new();
431 grids.active_grid_for_owner_transform(world, owner_transform)
432 }
433
434 fn world_dir_to_target_local(
435 world: &World,
436 target_transform: ComponentId,
437 dir_world: [f32; 3],
438 ) -> [f32; 3] {
439 use crate::utils::math;
440
441 let d = Self::world_delta_to_target_local(world, target_transform, dir_world);
442 math::vec3_normalize(d)
443 }
444
445 fn translation_drag_next_local(
446 world: &World,
447 target_transform: ComponentId,
448 axis_world: [f32; 3],
449 drag_start_hit_point_world: [f32; 3],
450 drag_start_target_translation: [f32; 3],
451 current_hit_point_world: [f32; 3],
452 ) -> [f32; 3] {
453 fn dot(a: [f32; 3], b: [f32; 3]) -> f32 {
454 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
455 }
456
457 fn add(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
458 [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
459 }
460
461 fn mul(v: [f32; 3], s: f32) -> [f32; 3] {
462 [v[0] * s, v[1] * s, v[2] * s]
463 }
464
465 fn sub(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
466 [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
467 }
468
469 let drag_world = sub(current_hit_point_world, drag_start_hit_point_world);
470 let axis_distance_world = dot(drag_world, axis_world);
471 let delta_world_axis = mul(axis_world, axis_distance_world);
472 let delta_local =
473 Self::world_delta_to_target_local(world, target_transform, delta_world_axis);
474 add(drag_start_target_translation, delta_local)
475 }
476
477 fn resolve_translation_space(
478 world: &World,
479 gizmo_cid: ComponentId,
480 ) -> crate::engine::ecs::component::TransformGizmoCoordSpace {
481 let mut translation_space = crate::engine::ecs::component::TransformGizmoCoordSpace::World;
482 let mut cur = Some(gizmo_cid);
483 while let Some(node) = cur {
484 if let Some(ed) =
485 world.get_component_by_id_as::<crate::engine::ecs::component::EditorComponent>(node)
486 {
487 translation_space = ed.transform_gizmo_translation_space;
488 break;
489 }
490 cur = world.parent_of(node);
491 }
492 translation_space
493 }
494
495 fn transform_direction(
496 m: crate::engine::graphics::primitives::TransformMatrix,
497 v: [f32; 3],
498 ) -> [f32; 3] {
499 [
500 m[0][0] * v[0] + m[1][0] * v[1] + m[2][0] * v[2],
501 m[0][1] * v[0] + m[1][1] * v[1] + m[2][1] * v[2],
502 m[0][2] * v[0] + m[1][2] * v[1] + m[2][2] * v[2],
503 ]
504 }
505
506 fn translation_axis_world(
507 world: &World,
508 target_transform: ComponentId,
509 space: crate::engine::ecs::component::TransformGizmoCoordSpace,
510 axis: TransformGizmoAxis,
511 ) -> [f32; 3] {
512 use crate::engine::ecs::system::transform_system::TransformSystem;
513 use crate::utils::math;
514
515 let axis_local = axis.unit_vec3();
516 match space {
517 crate::engine::ecs::component::TransformGizmoCoordSpace::World => axis_local,
518 crate::engine::ecs::component::TransformGizmoCoordSpace::Local => {
519 let target_world = TransformSystem::world_model(world, target_transform)
520 .unwrap_or_else(Self::mat4_identity);
521 math::vec3_normalize(Self::transform_direction(target_world, axis_local))
522 }
523 }
524 }
525
526 fn quat_from_z_to_dir(dir: [f32; 3]) -> [f32; 4] {
527 use crate::utils::math;
528
529 fn cross(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
530 [
531 a[1] * b[2] - a[2] * b[1],
532 a[2] * b[0] - a[0] * b[2],
533 a[0] * b[1] - a[1] * b[0],
534 ]
535 }
536
537 let z = [0.0f32, 0.0f32, 1.0f32];
539 let d = math::vec3_normalize(dir);
540 let dot_ = z[0] * d[0] + z[1] * d[1] + z[2] * d[2];
541
542 if dot_ >= 1.0 - 1e-6 {
543 return [0.0, 0.0, 0.0, 1.0];
544 }
545 if dot_ <= -1.0 + 1e-6 {
546 return math::quat_from_axis_angle([1.0, 0.0, 0.0], std::f32::consts::PI);
548 }
549
550 let axis = cross(z, d);
551 let axis_len = (axis[0] * axis[0] + axis[1] * axis[1] + axis[2] * axis[2]).sqrt();
552 if axis_len <= 1e-6 {
553 return [0.0, 0.0, 0.0, 1.0];
554 }
555 let axis_n = [axis[0] / axis_len, axis[1] / axis_len, axis[2] / axis_len];
556 let angle = dot_.clamp(-1.0, 1.0).acos();
557 math::quat_from_axis_angle(axis_n, angle)
558 }
559
560 fn spawn_debug_drag_plane(
561 world: &mut World,
562 emit: &mut dyn SignalEmitter,
563 hit_point: [f32; 3],
564 plane_normal: [f32; 3],
565 ) -> ComponentId {
566 use crate::engine::ecs::component::{
567 ColorComponent, EmissiveComponent, OpacityComponent, RenderableComponent,
568 TransformComponent,
569 };
570 use crate::engine::graphics::primitives::{CpuMeshHandle, MaterialHandle, Renderable};
571
572 let q = Self::quat_from_z_to_dir(plane_normal);
573
574 let size = 2.0_f32;
576 let thickness = 0.005_f32;
577
578 let t = world.add_component_boxed_named(
579 "gizmo_drag_plane_t",
580 Box::new(
581 TransformComponent::new()
582 .with_position(hit_point[0], hit_point[1], hit_point[2])
583 .with_rotation_quat(q)
584 .with_scale(size, size, thickness),
585 ),
586 );
587 let r = world.add_component_boxed_named(
588 "gizmo_drag_plane_r",
589 Box::new(RenderableComponent::new(Renderable::new(
590 CpuMeshHandle::CUBE,
591 MaterialHandle::UNLIT_MESH,
592 ))),
593 );
594 let c = world.add_component_boxed_named(
595 "gizmo_drag_plane_color",
596 Box::new(ColorComponent::rgba(1.0, 0.0, 1.0, 0.35)),
597 );
598 let o = world.add_component_boxed_named(
599 "gizmo_drag_plane_opacity",
600 Box::new(
601 OpacityComponent::new()
602 .with_opacity(0.35)
603 .with_multiple_layers(),
604 ),
605 );
606 let e = world.add_component_boxed_named(
607 "gizmo_drag_plane_emissive",
608 Box::new(EmissiveComponent::on()),
609 );
610
611 let _ = world.add_child(t, r);
612 let _ = world.add_child(r, c);
613 let _ = world.add_child(r, o);
614 let _ = world.add_child(r, e);
615
616 world.init_component_tree(t, emit);
617 t
618 }
619
620 fn on_parent_changed(
621 world: &mut World,
622 _emit: &mut dyn SignalEmitter,
623 env: &crate::engine::ecs::Signal,
624 ) {
625 let Some(EventSignal::ParentChanged {
626 child, new_parent, ..
627 }) = env.event.as_ref()
628 else {
629 return;
630 };
631
632 if world
633 .get_component_by_id_as::<TransformGizmoComponent>(*child)
634 .is_none()
635 {
636 return;
637 }
638
639 let mut target: Option<ComponentId> = None;
640 let mut cur = *new_parent;
641 while let Some(node) = cur {
642 if world
643 .get_component_by_id_as::<TransformComponent>(node)
644 .is_some()
645 {
646 target = Some(node);
647 break;
648 }
649 cur = world.parent_of(node);
650 }
651
652 let old_target = world
653 .get_component_by_id_as::<TransformGizmoComponent>(*child)
654 .and_then(|g| g.target_transform);
655
656 let routed_target =
659 target.map(|t| Self::apply_route_upward_if_present(world, "update_transform", t));
660
661 if Self::debug_target_enabled() {
662 if let (Some(orig), Some(routed)) = (target, routed_target) {
663 if orig != routed {
664 let orig_name = world
665 .get_component_record(orig)
666 .map(|n| {
667 if n.name.is_empty() {
668 n.component_type.clone()
669 } else {
670 format!("{}: {}", n.component_type, n.name)
671 }
672 })
673 .unwrap_or_else(|| "<missing>".to_string());
674 let routed_name = world
675 .get_component_record(routed)
676 .map(|n| {
677 if n.name.is_empty() {
678 n.component_type.clone()
679 } else {
680 format!("{}: {}", n.component_type, n.name)
681 }
682 })
683 .unwrap_or_else(|| "<missing>".to_string());
684 println!(
685 "[TransformGizmoSystem] routed target_transform {:?} '{}' -> {:?} '{}'",
686 orig, orig_name, routed, routed_name
687 );
688 }
689 }
690 }
691
692 if let Some(g) = world.get_component_by_id_as_mut::<TransformGizmoComponent>(*child) {
693 g.target_transform = routed_target;
694 g.active_raycaster = None;
695 }
696
697 if Self::debug_enabled() {
698 println!(
699 "[TransformGizmoSystem] ParentChanged gizmo={:?} new_parent={:?} old_target={:?} new_target={:?}",
700 child, new_parent, old_target, target
701 );
702 }
703 }
704
705 fn on_drag_start(
706 world: &mut World,
707 emit: &mut dyn SignalEmitter,
708 env: &crate::engine::ecs::Signal,
709 ) {
710 let Some(EventSignal::DragStart {
711 raycaster,
712 renderable,
713 hit_point,
714 ray_dir_world,
715 ..
716 }) = env.event.as_ref()
717 else {
718 return;
719 };
720
721 let Some((gizmo_cid, _op)) = Self::resolve_gizmo_op_for_renderable(world, *renderable)
722 else {
723 return;
724 };
725
726 let drag_start_target_translation = world
727 .get_component_by_id_as::<TransformGizmoComponent>(gizmo_cid)
728 .and_then(|g| g.target_transform)
729 .and_then(|target| world.get_component_by_id_as::<TransformComponent>(target))
730 .map(|t| t.transform.translation);
731
732 let mut old_debug_root: Option<ComponentId> = None;
733 if let Some(g) = world.get_component_by_id_as_mut::<TransformGizmoComponent>(gizmo_cid) {
734 g.active_raycaster = Some(*raycaster);
735 g.active_drag_slider_last_angle = 0.0;
736 g.active_drag_start_hit_point_world = Some(*hit_point);
737 g.active_drag_start_target_translation = drag_start_target_translation;
738 if Self::debug_drag_plane_enabled() {
739 old_debug_root = g.debug_drag_plane_root.take();
740 }
741 }
742
743 if let Some(root) = old_debug_root {
744 emit.push_intent_now(
745 root,
746 IntentValue::RemoveSubtree {
747 component_ids: vec![root],
748 },
749 );
750 }
751
752 if Self::debug_drag_plane_enabled() {
753 let plane_root = Self::spawn_debug_drag_plane(world, emit, *hit_point, *ray_dir_world);
754 if let Some(g) = world.get_component_by_id_as_mut::<TransformGizmoComponent>(gizmo_cid)
755 {
756 g.debug_drag_plane_root = Some(plane_root);
757 }
758 }
759 }
760
761 fn on_drag_move(
762 world: &mut World,
763 emit: &mut dyn SignalEmitter,
764 env: &crate::engine::ecs::Signal,
765 editor_context_state: Option<Arc<Mutex<EditorContextState>>>,
766 ) {
767 use crate::engine::ecs::system::transform_system::TransformSystem;
768 use crate::utils::math;
769
770 fn dot(a: [f32; 3], b: [f32; 3]) -> f32 {
771 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
772 }
773
774 fn sub(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
775 [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
776 }
777
778 fn mul(v: [f32; 3], s: f32) -> [f32; 3] {
779 [v[0] * s, v[1] * s, v[2] * s]
780 }
781
782 fn cross(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
783 [
784 a[1] * b[2] - a[2] * b[1],
785 a[2] * b[0] - a[0] * b[2],
786 a[0] * b[1] - a[1] * b[0],
787 ]
788 }
789
790 let Some(EventSignal::DragMove {
791 raycaster,
792 renderable,
793 delta_world,
794 hit_point,
795 screen_pos_px: _screen_pos_px,
796 screen_delta_px,
797 ..
798 }) = env.event.as_ref()
799 else {
800 return;
801 };
802
803 let Some((gizmo_cid, op)) = Self::resolve_gizmo_op_for_renderable(world, *renderable)
804 else {
805 return;
806 };
807
808 let Some((
810 target_transform,
811 active,
812 slider_last_angle,
813 drag_start_hit_point_world,
814 drag_start_target_translation,
815 )) = world
816 .get_component_by_id_as::<TransformGizmoComponent>(gizmo_cid)
817 .map(|g| {
818 (
819 g.target_transform,
820 g.active_raycaster,
821 g.active_drag_slider_last_angle,
822 g.active_drag_start_hit_point_world,
823 g.active_drag_start_target_translation,
824 )
825 })
826 else {
827 return;
828 };
829
830 let Some(target_transform) = target_transform else {
831 return;
832 };
833
834 if active != Some(*raycaster) {
835 return;
836 }
837
838 match op {
839 TransformGizmoOp::Translate(axis) => {
840 let translation_space = Self::resolve_translation_space(world, gizmo_cid);
841 let axis_v =
842 Self::translation_axis_world(world, target_transform, translation_space, axis);
843 let Some(drag_start_hit_point_world) = drag_start_hit_point_world else {
844 return;
845 };
846 let Some(drag_start_target_translation) = drag_start_target_translation else {
847 return;
848 };
849 let unsnapped_next = Self::translation_drag_next_local(
850 world,
851 target_transform,
852 axis_v,
853 drag_start_hit_point_world,
854 drag_start_target_translation,
855 *hit_point,
856 );
857 let next = if let Some(active_grid) =
858 Self::active_snap_grid_for_translate(world, editor_context_state.clone())
859 {
860 let candidate_world = Self::target_translation_local_to_world(
861 world,
862 target_transform,
863 unsnapped_next,
864 );
865 let snapped_world = GridSystem::snap_point_preserving_plane_offset(
866 &active_grid,
867 candidate_world,
868 )
869 .point_world;
870 Self::world_point_to_target_translation_local(
871 world,
872 target_transform,
873 snapped_world,
874 )
875 } else {
876 unsnapped_next
877 };
878
879 if Self::debug_apply_enabled() {
880 Self::log_apply(
881 world,
882 "translate",
883 target_transform,
884 &format!(
885 "translation_space={:?} hit_point={:?} drag_start_hit_point={:?} axis_world={:?} drag_start_t={:?} next_t={:?}",
886 translation_space,
887 *hit_point,
888 drag_start_hit_point_world,
889 axis_v,
890 drag_start_target_translation,
891 next,
892 ),
893 );
894 }
895
896 let Some(t_ro) =
897 world.get_component_by_id_as::<TransformComponent>(target_transform)
898 else {
899 return;
900 };
901 if Self::debug_sanity_enabled() {
902 Self::sanity_check_transform_values(
903 world,
904 target_transform,
905 next,
906 t_ro.transform.rotation,
907 t_ro.transform.scale,
908 );
909 }
910
911 let Some(t) =
912 world.get_component_by_id_as_mut::<TransformComponent>(target_transform)
913 else {
914 return;
915 };
916 t.set_position(emit, next[0], next[1], next[2]);
917 }
918 TransformGizmoOp::Rotate(axis) => {
919 let coord_type =
920 Self::resolve_gesture_coord_type_for_renderable(world, *renderable);
921
922 let mut rotation_space =
925 crate::engine::ecs::component::TransformGizmoCoordSpace::Local;
926 {
927 let mut cur = Some(gizmo_cid);
928 while let Some(node) = cur {
929 if let Some(ed) = world.get_component_by_id_as::<crate::engine::ecs::component::EditorComponent>(node) {
930 rotation_space = ed.transform_gizmo_rotation_space;
931 break;
932 }
933 cur = world.parent_of(node);
934 }
935 }
936
937 let axis_v = axis.unit_vec3();
938 let (angle, new_slider_last_angle) = match coord_type {
939 Some(GestureCoordType::ScreenSpace1DSlider) => {
940 match *screen_delta_px {
941 Some((dx, dy)) => {
942 let radians_per_px = 0.01_f32;
945 let delta_px = dx + dy;
946 let delta_angle = delta_px * radians_per_px;
947 (delta_angle, slider_last_angle + delta_angle)
948 }
949 None => {
950 (0.0_f32, slider_last_angle)
952 }
953 }
954 }
955 _ => {
956 let pivot = TransformSystem::world_position(world, target_transform)
957 .unwrap_or([0.0, 0.0, 0.0]);
958 let prev_hit = sub(*hit_point, *delta_world);
959
960 let mut v0 = sub(prev_hit, pivot);
961 let mut v1 = sub(*hit_point, pivot);
962
963 v0 = sub(v0, mul(axis_v, dot(v0, axis_v)));
965 v1 = sub(v1, mul(axis_v, dot(v1, axis_v)));
966 v0 = math::vec3_normalize(v0);
967 v1 = math::vec3_normalize(v1);
968
969 let c = cross(v0, v1);
971 let s = dot(axis_v, c);
972 let d = dot(v0, v1);
973 (s.atan2(d), slider_last_angle)
974 }
975 };
976
977 if angle != 0.0 {
978 let axis_local = match rotation_space {
979 crate::engine::ecs::component::TransformGizmoCoordSpace::Local => axis_v,
980 crate::engine::ecs::component::TransformGizmoCoordSpace::World => {
983 Self::world_dir_to_target_local(world, target_transform, axis_v)
984 }
985 };
986
987 let Some(t_ro) =
988 world.get_component_by_id_as::<TransformComponent>(target_transform)
989 else {
990 return;
991 };
992 let q_delta_local = math::quat_from_axis_angle(axis_local, angle);
993 let q_next = match rotation_space {
997 crate::engine::ecs::component::TransformGizmoCoordSpace::Local => {
998 math::quat_mul(t_ro.transform.rotation, q_delta_local)
999 }
1000 crate::engine::ecs::component::TransformGizmoCoordSpace::World => {
1001 math::quat_mul(q_delta_local, t_ro.transform.rotation)
1002 }
1003 };
1004
1005 if Self::debug_apply_enabled() {
1006 Self::log_apply(
1007 world,
1008 "rotate",
1009 target_transform,
1010 &format!(
1011 "delta_world={:?} axis_world={:?} axis_local={:?} angle={:.6} cur_q={:?} next_q={:?} pivot_world={:?}",
1012 *delta_world,
1013 axis_v,
1014 axis_local,
1015 angle,
1016 t_ro.transform.rotation,
1017 q_next,
1018 TransformSystem::world_position(world, target_transform)
1019 .unwrap_or([0.0, 0.0, 0.0]),
1020 ),
1021 );
1022 }
1023
1024 if Self::debug_sanity_enabled() {
1025 Self::sanity_check_transform_values(
1026 world,
1027 target_transform,
1028 t_ro.transform.translation,
1029 q_next,
1030 t_ro.transform.scale,
1031 );
1032 }
1033
1034 let Some(t) =
1035 world.get_component_by_id_as_mut::<TransformComponent>(target_transform)
1036 else {
1037 return;
1038 };
1039 t.set_rotation_quat(emit, q_next);
1040 }
1041
1042 if coord_type == Some(GestureCoordType::ScreenSpace1DSlider) {
1043 if let Some(g) =
1044 world.get_component_by_id_as_mut::<TransformGizmoComponent>(gizmo_cid)
1045 {
1046 g.active_drag_slider_last_angle = new_slider_last_angle;
1047 }
1048 }
1049 }
1050 TransformGizmoOp::Scale(axis) => {
1051 let d = dot(*delta_world, axis.unit_vec3());
1052
1053 let delta_world_axis = mul(axis.unit_vec3(), d);
1056 let delta_local_axis =
1057 Self::world_delta_to_target_local(world, target_transform, delta_world_axis);
1058 let axis_local_dir =
1059 Self::world_dir_to_target_local(world, target_transform, axis.unit_vec3());
1060 let d_local = dot(delta_local_axis, axis_local_dir);
1061
1062 let Some(t_ro) =
1063 world.get_component_by_id_as::<TransformComponent>(target_transform)
1064 else {
1065 return;
1066 };
1067 let mut s = t_ro.transform.scale;
1068 match axis {
1069 TransformGizmoAxis::X => s[0] = (s[0] + d_local).max(0.001),
1070 TransformGizmoAxis::Y => s[1] = (s[1] + d_local).max(0.001),
1071 TransformGizmoAxis::Z => s[2] = (s[2] + d_local).max(0.001),
1072 }
1073
1074 if Self::debug_apply_enabled() {
1075 Self::log_apply(
1076 world,
1077 "scale",
1078 target_transform,
1079 &format!(
1080 "delta_world={:?} axis_world={:?} d_world={:.6} delta_world_axis={:?} delta_local_axis={:?} axis_local_dir={:?} d_local={:.6} cur_s={:?} next_s={:?}",
1081 *delta_world,
1082 axis.unit_vec3(),
1083 d,
1084 delta_world_axis,
1085 delta_local_axis,
1086 axis_local_dir,
1087 d_local,
1088 t_ro.transform.scale,
1089 s,
1090 ),
1091 );
1092 }
1093
1094 if Self::debug_sanity_enabled() {
1095 Self::sanity_check_transform_values(
1096 world,
1097 target_transform,
1098 t_ro.transform.translation,
1099 t_ro.transform.rotation,
1100 s,
1101 );
1102 }
1103
1104 let Some(t) =
1105 world.get_component_by_id_as_mut::<TransformComponent>(target_transform)
1106 else {
1107 return;
1108 };
1109 t.set_scale(emit, s[0], s[1], s[2]);
1110 }
1111 }
1112 }
1113
1114 fn on_drag_end(
1115 world: &mut World,
1116 emit: &mut dyn SignalEmitter,
1117 env: &crate::engine::ecs::Signal,
1118 ) {
1119 let Some(EventSignal::DragEnd {
1120 raycaster,
1121 renderable,
1122 ..
1123 }) = env.event.as_ref()
1124 else {
1125 return;
1126 };
1127
1128 let Some((gizmo_cid, _op)) = Self::resolve_gizmo_op_for_renderable(world, *renderable)
1129 else {
1130 return;
1131 };
1132
1133 if let Some(g) = world.get_component_by_id_as_mut::<TransformGizmoComponent>(gizmo_cid) {
1134 if g.active_raycaster == Some(*raycaster) {
1135 g.active_raycaster = None;
1136 }
1137 g.active_drag_slider_last_angle = 0.0;
1138 g.active_drag_start_hit_point_world = None;
1139 g.active_drag_start_target_translation = None;
1140
1141 if Self::debug_drag_plane_enabled() {
1142 if let Some(root) = g.debug_drag_plane_root.take() {
1143 emit.push_intent_now(
1144 root,
1145 IntentValue::RemoveSubtree {
1146 component_ids: vec![root],
1147 },
1148 );
1149 }
1150 }
1151 }
1152 }
1153
1154 pub fn register_transform_gizmo(
1158 &mut self,
1159 world: &mut World,
1160 component: ComponentId,
1161 emit: &mut dyn SignalEmitter,
1162 ) {
1163 use crate::engine::ecs::component::{
1164 EditorComponent, OverlayComponent, TransformComponent, TransformGizmoAxis,
1165 TransformGizmoComponent, TransformGizmoCoordSpace, TransformGizmoRotateComponent,
1166 TransformGizmoTranslateComponent,
1167 };
1168 use crate::engine::graphics::primitives::CpuMeshHandle;
1169
1170 let Some(_) = world.get_component_by_id_as::<TransformGizmoComponent>(component) else {
1172 return;
1173 };
1174
1175 let mut cur = component;
1177 let mut parent_transform: Option<ComponentId> = None;
1178 while let Some(p) = world.parent_of(cur) {
1179 if world
1180 .get_component_by_id_as::<TransformComponent>(p)
1181 .is_some()
1182 {
1183 parent_transform = Some(p);
1184 break;
1185 }
1186 cur = p;
1187 }
1188 if parent_transform.is_none() {
1189 return;
1190 }
1191 let parent_transform = parent_transform.unwrap();
1192
1193 if let Some(g) = world.get_component_by_id_as_mut::<TransformGizmoComponent>(component) {
1196 g.target_transform = Some(parent_transform);
1197 }
1198
1199 if let Some(g) = world.get_component_by_id_as::<TransformGizmoComponent>(component) {
1201 if g.visual_root.is_some() {
1202 return;
1203 }
1204 }
1205
1206 let gizmo_scale = world
1207 .get_component_by_id_as::<TransformGizmoComponent>(component)
1208 .map(|g| g.scale)
1209 .unwrap_or(1.0);
1210
1211 fn mat4_identity() -> crate::engine::graphics::primitives::TransformMatrix {
1219 [
1220 [1.0, 0.0, 0.0, 0.0],
1221 [0.0, 1.0, 0.0, 0.0],
1222 [0.0, 0.0, 1.0, 0.0],
1223 [0.0, 0.0, 0.0, 1.0],
1224 ]
1225 }
1226
1227 fn mat4_mul(
1228 a: crate::engine::graphics::primitives::TransformMatrix,
1229 b: crate::engine::graphics::primitives::TransformMatrix,
1230 ) -> crate::engine::graphics::primitives::TransformMatrix {
1231 let mut out = [[0.0f32; 4]; 4];
1232 for c in 0..4 {
1233 for r in 0..4 {
1234 out[c][r] = a[0][r] * b[c][0]
1235 + a[1][r] * b[c][1]
1236 + a[2][r] * b[c][2]
1237 + a[3][r] * b[c][3];
1238 }
1239 }
1240 out
1241 }
1242
1243 fn max_basis_scale(m: crate::engine::graphics::primitives::TransformMatrix) -> f32 {
1244 fn len3(v: [f32; 4]) -> f32 {
1245 (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt()
1246 }
1247 len3(m[0]).max(len3(m[1])).max(len3(m[2]))
1249 }
1250
1251 fn world_model_uncached(
1252 world: &World,
1253 start_transform: ComponentId,
1254 ) -> crate::engine::graphics::primitives::TransformMatrix {
1255 let mut chain: Vec<crate::engine::graphics::primitives::TransformMatrix> = Vec::new();
1257 let mut cur: Option<ComponentId> = Some(start_transform);
1258 while let Some(node) = cur {
1259 if let Some(t) = world.get_component_by_id_as::<TransformComponent>(node) {
1260 chain.push(t.transform.model);
1261 }
1262 cur = world.parent_of(node);
1263 }
1264 chain.reverse();
1265 let mut out = mat4_identity();
1266 for m in chain {
1267 out = mat4_mul(out, m);
1268 }
1269 out
1270 }
1271
1272 let parent_world = world_model_uncached(world, parent_transform);
1273 let parent_world_scale = max_basis_scale(parent_world).max(1e-4);
1274 let gizmo_local_scale = gizmo_scale;
1275
1276 fn add_pipeline_group(
1277 world: &mut World,
1278 parent: ComponentId,
1279 pipeline_name: &str,
1280 include_translation_map: bool,
1281 include_rotation_map: bool,
1282 include_scale_map: bool,
1283 drop_translation: bool,
1284 drop_rotation: bool,
1285 drop_scale: bool,
1286 ) -> ComponentId {
1287 let fork = world.add_component_boxed_named(
1288 pipeline_name,
1289 Box::new(TransformForkTRSComponent::new()),
1290 );
1291 let _ = world.add_child(parent, fork);
1292
1293 if include_translation_map {
1294 let map = world.add_component_boxed_named(
1295 format!("{pipeline_name}:map_translation"),
1296 Box::new(TransformMapTranslationComponent::new()),
1297 );
1298 let _ = world.add_child(fork, map);
1299 if drop_translation {
1300 let drop = world.add_component_boxed_named(
1301 format!("{pipeline_name}:drop_translation"),
1302 Box::new(TransformDropComponent::new()),
1303 );
1304 let _ = world.add_child(map, drop);
1305 }
1306 }
1307
1308 if include_rotation_map {
1309 let map = world.add_component_boxed_named(
1310 format!("{pipeline_name}:map_rotation"),
1311 Box::new(TransformMapRotationComponent::new()),
1312 );
1313 let _ = world.add_child(fork, map);
1314 if drop_rotation {
1315 let drop = world.add_component_boxed_named(
1316 format!("{pipeline_name}:drop_rotation"),
1317 Box::new(TransformDropComponent::new()),
1318 );
1319 let _ = world.add_child(map, drop);
1320 }
1321 }
1322
1323 if include_scale_map {
1324 let map = world.add_component_boxed_named(
1325 format!("{pipeline_name}:map_scale"),
1326 Box::new(TransformMapScaleComponent::new()),
1327 );
1328 let _ = world.add_child(fork, map);
1329 if drop_scale {
1330 let drop = world.add_component_boxed_named(
1331 format!("{pipeline_name}:drop_scale"),
1332 Box::new(TransformDropComponent::new()),
1333 );
1334 let _ = world.add_child(map, drop);
1335 }
1336 }
1337
1338 fork
1339 }
1340
1341 if Self::debug_enabled() {
1342 println!(
1343 "[TransformGizmoSystem] register gizmo={:?} target_transform={:?} requested_world_scale={:.4} parent_world_scale={:.4} gizmo_local_scale={:.4}",
1344 component, parent_transform, gizmo_scale, parent_world_scale, gizmo_local_scale
1345 );
1346 }
1347
1348 let gizmo_output = add_pipeline_group(
1351 world,
1352 component,
1353 "gizmo_pipeline",
1354 true,
1355 true,
1356 true,
1357 false,
1358 false,
1359 true,
1360 );
1361
1362 let gizmo_root =
1364 world.add_component_boxed_named("gizmo_root", Box::new(TransformComponent::new()));
1365 let _ = world.add_child(gizmo_output, gizmo_root);
1366
1367 for (name, marker) in [
1370 (
1371 "gizmo_camera_mono",
1372 TransformCameraSpecificComponent::active_monoscopic(),
1373 ),
1374 (
1375 "gizmo_camera_stereo",
1376 TransformCameraSpecificComponent::active_stereoscopic(),
1377 ),
1378 ] {
1379 let mode = world.add_component_boxed_named(name, Box::new(marker));
1380 let settings = world.add_component_boxed_named(
1381 format!("{name}_settings"),
1382 Box::new(TransformComponent::new().with_scale(
1383 gizmo_local_scale,
1384 gizmo_local_scale,
1385 gizmo_local_scale,
1386 )),
1387 );
1388 let _ = world.add_child(gizmo_root, mode);
1389 let _ = world.add_child(mode, settings);
1390 }
1391
1392 let gizmo_overlay =
1394 world.add_component_boxed_named("gizmo_overlay", Box::new(OverlayComponent::new()));
1395 let _ = world.add_child(gizmo_root, gizmo_overlay);
1396
1397 let gizmo_visual_parent = gizmo_overlay;
1398
1399 let mut translation_space = TransformGizmoCoordSpace::World;
1401 let mut rotation_space = TransformGizmoCoordSpace::Local;
1402 {
1403 let mut cur = Some(component);
1404 while let Some(node) = cur {
1405 if let Some(ed) = world.get_component_by_id_as::<EditorComponent>(node) {
1406 translation_space = ed.transform_gizmo_translation_space;
1407 rotation_space = ed.transform_gizmo_rotation_space;
1408 break;
1409 }
1410 cur = world.parent_of(node);
1411 }
1412 }
1413
1414 let gizmo_space_world = add_pipeline_group(
1420 world,
1421 gizmo_visual_parent,
1422 "gizmo_space_world_pipeline",
1423 true,
1424 true,
1425 true,
1426 false,
1427 true,
1428 false,
1429 );
1430 let gizmo_space_local = add_pipeline_group(
1431 world,
1432 gizmo_visual_parent,
1433 "gizmo_space_local_pipeline",
1434 true,
1435 true,
1436 true,
1437 false,
1438 false,
1439 false,
1440 );
1441
1442 let translate_parent = match translation_space {
1443 TransformGizmoCoordSpace::World => gizmo_space_world,
1444 TransformGizmoCoordSpace::Local => gizmo_space_local,
1445 };
1446
1447 let rotate_parent = match rotation_space {
1448 TransformGizmoCoordSpace::World => gizmo_space_world,
1449 TransformGizmoCoordSpace::Local => gizmo_space_local,
1450 };
1451
1452 if let Some(g) = world.get_component_by_id_as_mut::<TransformGizmoComponent>(component) {
1454 g.visual_root = Some(gizmo_root);
1455 }
1456 self.live_gizmos.insert(component);
1457
1458 fn spawn_part(
1460 world: &mut World,
1461 parent: ComponentId,
1462 name: &str,
1463 mesh: CpuMeshHandle,
1464 pos: [f32; 3],
1465 rot_euler: [f32; 3],
1466 scale: [f32; 3],
1467 rgba: [f32; 4],
1468 ) {
1469 use crate::engine::ecs::component::{
1470 ColorComponent, EmissiveComponent, RenderableComponent, TransformComponent,
1471 };
1472 use crate::engine::graphics::primitives::{MaterialHandle, Renderable};
1473
1474 let t = world.add_component_boxed_named(
1475 format!("{name}_t"),
1476 Box::new(
1477 TransformComponent::new()
1478 .with_position(pos[0], pos[1], pos[2])
1479 .with_rotation_euler(rot_euler[0], rot_euler[1], rot_euler[2])
1480 .with_scale(scale[0], scale[1], scale[2]),
1481 ),
1482 );
1483 let r = world.add_component_boxed_named(
1484 format!("{name}_r"),
1485 Box::new(RenderableComponent::new(Renderable::new(
1486 mesh,
1487 MaterialHandle::TOON_MESH,
1488 ))),
1489 );
1490 let c = world.add_component_boxed_named(
1491 format!("{name}_color"),
1492 Box::new(ColorComponent::rgba(rgba[0], rgba[1], rgba[2], rgba[3])),
1493 );
1494 let e = world.add_component_boxed_named(
1495 format!("{name}_emissive"),
1496 Box::new(EmissiveComponent::on()),
1497 );
1498
1499 let _ = world.add_child(parent, t);
1500 let _ = world.add_child(t, r);
1501 let _ = world.add_child(r, c);
1502 let _ = world.add_child(r, e);
1503 }
1504
1505 fn spawn_raycastable_root(
1508 world: &mut World,
1509 parent: ComponentId,
1510 name: &str,
1511 ) -> ComponentId {
1512 use crate::engine::ecs::component::RaycastableComponent;
1513
1514 let rc = world.add_component_boxed_named(
1515 name,
1516 Box::new(RaycastableComponent::drag_only().with_interaction_priority(1)),
1517 );
1518 let _ = world.add_child(parent, rc);
1519 rc
1520 }
1521
1522 fn spawn_gesture_coord_type_root(
1523 world: &mut World,
1524 parent: ComponentId,
1525 name: &str,
1526 coord_type: GestureCoordType,
1527 ) -> ComponentId {
1528 let c = world.add_component_boxed_named(
1529 name,
1530 Box::new(GestureCoordTypeComponent::new(coord_type)),
1531 );
1532 let _ = world.add_child(parent, c);
1533 c
1534 }
1535
1536 fn spawn_translate_handle_root(
1537 world: &mut World,
1538 parent: ComponentId,
1539 axis: TransformGizmoAxis,
1540 name: &str,
1541 ) -> ComponentId {
1542 let h = world.add_component_boxed_named(
1543 name,
1544 Box::new(TransformGizmoTranslateComponent::new(axis)),
1545 );
1546 let _ = world.add_child(parent, h);
1547 h
1548 }
1549
1550 fn spawn_rotate_handle_root(
1551 world: &mut World,
1552 parent: ComponentId,
1553 axis: TransformGizmoAxis,
1554 name: &str,
1555 ) -> ComponentId {
1556 let h = world.add_component_boxed_named(
1557 name,
1558 Box::new(TransformGizmoRotateComponent::new(axis)),
1559 );
1560 let _ = world.add_child(parent, h);
1561 h
1562 }
1563
1564 let red = [1.0, 0.15, 0.15, 1.0];
1566 let green = [0.15, 1.0, 0.15, 1.0];
1567 let blue = [0.15, 0.35, 1.0, 1.0];
1568
1569 let ring_mesh = CpuMeshHandle::CIRCLE_2D;
1571 let ring_scale = [1.4, 1.4, 1.0];
1572
1573 let rot_x_root =
1575 spawn_rotate_handle_root(world, rotate_parent, TransformGizmoAxis::X, "gizmo_rot_x");
1576 let rot_x_coord = spawn_gesture_coord_type_root(
1577 world,
1578 rot_x_root,
1579 "gizmo_rot_x_coord",
1580 GestureCoordType::ScreenSpace1DSlider,
1581 );
1582 let rot_x_pick = spawn_raycastable_root(world, rot_x_coord, "gizmo_rot_x_pick");
1583 spawn_part(
1584 world,
1585 rot_x_pick,
1586 "gizmo_rot_x_ring",
1587 ring_mesh,
1588 [0.0, 0.0, 0.0],
1589 [0.0, -std::f32::consts::FRAC_PI_2, 0.0],
1590 ring_scale,
1591 red,
1592 );
1593
1594 let rot_y_root =
1595 spawn_rotate_handle_root(world, rotate_parent, TransformGizmoAxis::Y, "gizmo_rot_y");
1596 let rot_y_coord = spawn_gesture_coord_type_root(
1597 world,
1598 rot_y_root,
1599 "gizmo_rot_y_coord",
1600 GestureCoordType::ScreenSpace1DSlider,
1601 );
1602 let rot_y_pick = spawn_raycastable_root(world, rot_y_coord, "gizmo_rot_y_pick");
1603 spawn_part(
1604 world,
1605 rot_y_pick,
1606 "gizmo_rot_y_ring",
1607 ring_mesh,
1608 [0.0, 0.0, 0.0],
1609 [std::f32::consts::FRAC_PI_2, 0.0, 0.0],
1610 ring_scale,
1611 green,
1612 );
1613
1614 let rot_z_root =
1615 spawn_rotate_handle_root(world, rotate_parent, TransformGizmoAxis::Z, "gizmo_rot_z");
1616 let rot_z_coord = spawn_gesture_coord_type_root(
1617 world,
1618 rot_z_root,
1619 "gizmo_rot_z_coord",
1620 GestureCoordType::ScreenSpace1DSlider,
1621 );
1622 let rot_z_pick = spawn_raycastable_root(world, rot_z_coord, "gizmo_rot_z_pick");
1623 spawn_part(
1624 world,
1625 rot_z_pick,
1626 "gizmo_rot_z_ring",
1627 ring_mesh,
1628 [0.0, 0.0, 0.0],
1629 [0.0, 0.0, 0.0],
1630 ring_scale,
1631 blue,
1632 );
1633
1634 let stem_mesh = CpuMeshHandle::CUBE;
1636 let cone_mesh = CpuMeshHandle::CONE;
1637 let stem_len = 1.0_f32;
1638 let stem_thick = 0.06_f32;
1639 let cone_len = 0.22_f32;
1640 let cone_radius = 0.12_f32;
1641
1642 let move_x_root = spawn_translate_handle_root(
1644 world,
1645 translate_parent,
1646 TransformGizmoAxis::X,
1647 "gizmo_move_x",
1648 );
1649 let move_x_pick = spawn_raycastable_root(world, move_x_root, "gizmo_move_x_pick");
1650 let rot_x = [0.0, std::f32::consts::FRAC_PI_2, 0.0];
1652 spawn_part(
1653 world,
1654 move_x_pick,
1655 "gizmo_move_x_stem",
1656 stem_mesh,
1657 [stem_len * 0.5, 0.0, 0.0],
1658 [0.0, 0.0, 0.0],
1659 [stem_len, stem_thick, stem_thick],
1660 red,
1661 );
1662 spawn_part(
1663 world,
1664 move_x_pick,
1665 "gizmo_move_x_tip",
1666 cone_mesh,
1667 [stem_len + cone_len * 0.5, 0.0, 0.0],
1668 rot_x,
1669 [cone_radius, cone_radius, cone_len],
1670 red,
1671 );
1672
1673 let move_y_root = spawn_translate_handle_root(
1674 world,
1675 translate_parent,
1676 TransformGizmoAxis::Y,
1677 "gizmo_move_y",
1678 );
1679 let move_y_pick = spawn_raycastable_root(world, move_y_root, "gizmo_move_y_pick");
1680 let rot_y = [-std::f32::consts::FRAC_PI_2, 0.0, 0.0];
1682 spawn_part(
1683 world,
1684 move_y_pick,
1685 "gizmo_move_y_stem",
1686 stem_mesh,
1687 [0.0, stem_len * 0.5, 0.0],
1688 [0.0, 0.0, 0.0],
1689 [stem_thick, stem_len, stem_thick],
1690 green,
1691 );
1692 spawn_part(
1693 world,
1694 move_y_pick,
1695 "gizmo_move_y_tip",
1696 cone_mesh,
1697 [0.0, stem_len + cone_len * 0.5, 0.0],
1698 rot_y,
1699 [cone_radius, cone_radius, cone_len],
1700 green,
1701 );
1702
1703 let move_z_root = spawn_translate_handle_root(
1704 world,
1705 translate_parent,
1706 TransformGizmoAxis::Z,
1707 "gizmo_move_z",
1708 );
1709 let move_z_pick = spawn_raycastable_root(world, move_z_root, "gizmo_move_z_pick");
1710 spawn_part(
1712 world,
1713 move_z_pick,
1714 "gizmo_move_z_stem",
1715 stem_mesh,
1716 [0.0, 0.0, stem_len * 0.5],
1717 [0.0, 0.0, 0.0],
1718 [stem_thick, stem_thick, stem_len],
1719 blue,
1720 );
1721 spawn_part(
1722 world,
1723 move_z_pick,
1724 "gizmo_move_z_tip",
1725 cone_mesh,
1726 [0.0, 0.0, stem_len + cone_len * 0.5],
1727 [0.0, 0.0, 0.0],
1728 [cone_radius, cone_radius, cone_len],
1729 blue,
1730 );
1731
1732 world.init_component_tree(gizmo_root, emit);
1734 }
1735
1736 fn resolve_gizmo_op_for_renderable(
1740 world: &World,
1741 renderable: ComponentId,
1742 ) -> Option<(ComponentId, TransformGizmoOp)> {
1743 let mut cur = Some(renderable);
1744 let mut op: Option<TransformGizmoOp> = None;
1745 let mut gizmo: Option<ComponentId> = None;
1746
1747 while let Some(node) = cur {
1748 if op.is_none() {
1749 if let Some(h) =
1750 world.get_component_by_id_as::<TransformGizmoTranslateComponent>(node)
1751 {
1752 op = Some(TransformGizmoOp::Translate(h.axis));
1753 } else if let Some(h) =
1754 world.get_component_by_id_as::<TransformGizmoRotateComponent>(node)
1755 {
1756 op = Some(TransformGizmoOp::Rotate(h.axis));
1757 } else if let Some(h) =
1758 world.get_component_by_id_as::<TransformGizmoScaleComponent>(node)
1759 {
1760 op = Some(TransformGizmoOp::Scale(h.axis));
1761 }
1762 }
1763
1764 if gizmo.is_none()
1765 && world
1766 .get_component_by_id_as::<TransformGizmoComponent>(node)
1767 .is_some()
1768 {
1769 gizmo = Some(node);
1770 }
1771
1772 if op.is_some() && gizmo.is_some() {
1773 break;
1774 }
1775
1776 cur = world.parent_of(node);
1777 }
1778
1779 let resolved = Some((gizmo?, op?));
1780
1781 if Self::debug_hit_enabled() {
1782 let (gizmo_cid, op) = resolved?;
1783 let renderable_name = world
1784 .get_component_record(renderable)
1785 .map(|n| {
1786 if n.name.is_empty() {
1787 n.component_type.clone()
1788 } else {
1789 format!("{}: {}", n.component_type, n.name)
1790 }
1791 })
1792 .unwrap_or_else(|| "<missing>".to_string());
1793 let gizmo_name = world
1794 .get_component_record(gizmo_cid)
1795 .map(|n| {
1796 if n.name.is_empty() {
1797 n.component_type.clone()
1798 } else {
1799 format!("{}: {}", n.component_type, n.name)
1800 }
1801 })
1802 .unwrap_or_else(|| "<missing>".to_string());
1803 let target_transform = world
1804 .get_component_by_id_as::<TransformGizmoComponent>(gizmo_cid)
1805 .and_then(|g| g.target_transform);
1806 let target_name = target_transform
1807 .and_then(|cid| world.get_component_record(cid).map(|n| (cid, n)))
1808 .map(|(cid, n)| {
1809 if n.name.is_empty() {
1810 format!("{cid:?} '{}'", n.component_type)
1811 } else {
1812 format!("{cid:?} '{}: {}'", n.component_type, n.name)
1813 }
1814 })
1815 .unwrap_or_else(|| "<none>".to_string());
1816 println!(
1817 "[TransformGizmoSystem] resolve_gizmo_op renderable={renderable:?} '{}' gizmo={gizmo_cid:?} '{}' op={op:?} target={target_name}",
1818 renderable_name, gizmo_name,
1819 );
1820 return Some((gizmo_cid, op));
1821 }
1822
1823 resolved
1824 }
1825
1826 fn resolve_gesture_coord_type_for_renderable(
1827 world: &World,
1828 renderable: ComponentId,
1829 ) -> Option<GestureCoordType> {
1830 let mut cur = Some(renderable);
1831 while let Some(node) = cur {
1832 if let Some(c) = world.get_component_by_id_as::<GestureCoordTypeComponent>(node) {
1833 return Some(c.coord_type);
1834 }
1835 cur = world.parent_of(node);
1836 }
1837 None
1838 }
1839
1840 #[allow(dead_code)]
1841 fn gizmos_for_hit_renderable(world: &World, renderable: ComponentId) -> Vec<ComponentId> {
1842 let mut out: Vec<ComponentId> = world
1843 .children_of(renderable)
1844 .iter()
1845 .copied()
1846 .filter(|&ch| {
1847 world
1848 .get_component_by_id_as::<TransformGizmoComponent>(ch)
1849 .is_some()
1850 })
1851 .collect();
1852
1853 let mut cur = Some(renderable);
1855 while let Some(node) = cur {
1856 if world
1857 .get_component_by_id_as::<TransformGizmoComponent>(node)
1858 .is_some()
1859 {
1860 out.push(node);
1861 }
1862 cur = world.parent_of(node);
1863 }
1864
1865 out.sort();
1866 out.dedup();
1867 out
1868 }
1869
1870 pub fn tick_with_queue(
1871 &mut self,
1872 world: &mut World,
1873 _input: &InputState,
1874 emit: &mut dyn SignalEmitter,
1875 _rx: &mut RxWorld,
1876 ) {
1877 let _ = (world, emit);
1880 }
1881}
1882
1883#[cfg(test)]
1884mod tests {
1885 use super::TransformGizmoSystem;
1886 use crate::engine::ecs::World;
1887 use crate::engine::ecs::component::{
1888 TransformComponent, TransformGizmoAxis, TransformGizmoCoordSpace,
1889 };
1890
1891 fn approx3(a: [f32; 3], b: [f32; 3]) {
1892 for i in 0..3 {
1893 assert!(
1894 (a[i] - b[i]).abs() < 1e-4,
1895 "index {i}: left={:?} right={:?}",
1896 a,
1897 b
1898 );
1899 }
1900 }
1901
1902 #[test]
1903 fn world_delta_is_converted_through_rotated_parent_inverse() {
1904 let mut world = World::default();
1905 let parent = world.add_component(TransformComponent::new().with_rotation_euler(
1906 0.0,
1907 0.0,
1908 std::f32::consts::FRAC_PI_2,
1909 ));
1910 let target = world.add_component(TransformComponent::new());
1911 world.add_child(parent, target).expect("attach target");
1912
1913 let parent_world = world
1914 .get_component_by_id_as::<TransformComponent>(parent)
1915 .expect("parent transform")
1916 .transform
1917 .model;
1918 world
1919 .get_component_by_id_as_mut::<TransformComponent>(parent)
1920 .expect("parent transform")
1921 .transform
1922 .matrix_world = parent_world;
1923
1924 let local =
1925 TransformGizmoSystem::world_delta_to_target_local(&world, target, [1.0, 0.0, 0.0]);
1926 approx3(local, [0.0, -1.0, 0.0]);
1927 }
1928
1929 #[test]
1930 fn local_translation_axis_uses_target_world_rotation() {
1931 let mut world = World::default();
1932 let target = world.add_component(TransformComponent::new().with_rotation_euler(
1933 0.0,
1934 0.0,
1935 std::f32::consts::FRAC_PI_2,
1936 ));
1937 let target_world = world
1938 .get_component_by_id_as::<TransformComponent>(target)
1939 .expect("target transform")
1940 .transform
1941 .model;
1942 world
1943 .get_component_by_id_as_mut::<TransformComponent>(target)
1944 .expect("target transform")
1945 .transform
1946 .matrix_world = target_world;
1947 let axis = TransformGizmoSystem::translation_axis_world(
1948 &world,
1949 target,
1950 TransformGizmoCoordSpace::Local,
1951 TransformGizmoAxis::X,
1952 );
1953 approx3(axis, [0.0, 1.0, 0.0]);
1954 }
1955
1956 #[test]
1957 fn translation_drag_uses_drag_start_anchor_instead_of_frame_delta() {
1958 let mut world = World::default();
1959 let target = world.add_component(TransformComponent::new().with_position(10.0, 0.0, 0.0));
1960
1961 let next = TransformGizmoSystem::translation_drag_next_local(
1962 &world,
1963 target,
1964 [1.0, 0.0, 0.0],
1965 [0.0, 0.0, 0.0],
1966 [10.0, 0.0, 0.0],
1967 [0.25, 0.5, 0.0],
1968 );
1969
1970 approx3(next, [10.25, 0.0, 0.0]);
1971 }
1972
1973 #[test]
1974 fn translation_drag_respects_rotated_parent_space_from_drag_start() {
1975 let mut world = World::default();
1976 let parent = world.add_component(TransformComponent::new().with_rotation_euler(
1977 0.0,
1978 0.0,
1979 std::f32::consts::FRAC_PI_2,
1980 ));
1981 let target = world.add_component(TransformComponent::new().with_position(0.0, 2.0, 0.0));
1982 world.add_child(parent, target).expect("attach target");
1983
1984 let parent_world = world
1985 .get_component_by_id_as::<TransformComponent>(parent)
1986 .expect("parent transform")
1987 .transform
1988 .model;
1989 world
1990 .get_component_by_id_as_mut::<TransformComponent>(parent)
1991 .expect("parent transform")
1992 .transform
1993 .matrix_world = parent_world;
1994
1995 let next = TransformGizmoSystem::translation_drag_next_local(
1996 &world,
1997 target,
1998 [1.0, 0.0, 0.0],
1999 [0.0, 0.0, 0.0],
2000 [0.0, 2.0, 0.0],
2001 [1.0, 0.25, 0.0],
2002 );
2003
2004 approx3(next, [0.0, 1.0, 0.0]);
2005 }
2006
2007 #[test]
2008 fn translation_local_world_conversion_uses_parent_space_not_target_rotation() {
2009 let mut world = World::default();
2010 let parent = world.add_component(TransformComponent::new());
2011 let target = world.add_component(
2012 TransformComponent::new()
2013 .with_position(1.0, 2.0, 3.0)
2014 .with_rotation_euler(std::f32::consts::FRAC_PI_2, 0.0, 0.0),
2015 );
2016 world.add_child(parent, target).expect("attach target");
2017
2018 let world_point = TransformGizmoSystem::target_translation_local_to_world(
2019 &world,
2020 target,
2021 [4.0, 5.0, 6.0],
2022 );
2023 approx3(world_point, [4.0, 5.0, 6.0]);
2024
2025 let local_point = TransformGizmoSystem::world_point_to_target_translation_local(
2026 &world,
2027 target,
2028 [7.0, 8.0, 9.0],
2029 );
2030 approx3(local_point, [7.0, 8.0, 9.0]);
2031 }
2032}