1use std::collections::HashMap;
2use std::sync::{Arc, Mutex};
3
4use crate::engine::ecs::component::{
5 ColorComponent, EditorComponent, GridComponent, OpacityComponent, RaycastableComponent,
6 RenderableComponent, SelectableComponent, SerializeComponent, TransformComponent,
7};
8use crate::engine::ecs::system::TransformSystem;
9use crate::engine::ecs::{
10 ComponentId, EventSignal, IntentValue, RxWorld, SignalEmitter, SignalKind, World,
11};
12use crate::utils::math::{
13 mat_to_quat, mat4_inverse, mat4_mul, quat_from_axis_angle, quat_mul, vec3_normalize,
14};
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub struct GridEntry {
18 pub grid_component: ComponentId,
19 pub owner_transform: ComponentId,
20 pub editor_root: ComponentId,
21 pub enabled: bool,
22 pub hidden: bool,
23 pub selectable: bool,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq)]
27pub struct ActiveGrid {
28 pub component: ComponentId,
29 pub spacing: f32,
30 pub origin_world: [f32; 3],
31 pub normal_world: [f32; 3],
32 pub matrix_world: [[f32; 4]; 4],
33 pub inverse_world: [[f32; 4]; 4],
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub struct GridStep {
38 pub cell: [i32; 2],
39}
40
41#[derive(Debug, Clone, Copy, PartialEq)]
42pub struct GridSnapResult {
43 pub point_world: [f32; 3],
44 pub normal_world: [f32; 3],
45 pub step: GridStep,
46}
47
48#[derive(Debug, Default)]
49struct GridRegistry {
50 dirty: bool,
51 handlers_installed: bool,
52 by_grid: HashMap<ComponentId, GridEntry>,
53 by_editor: HashMap<ComponentId, Vec<ComponentId>>,
54 cached_component_count: usize,
55}
56
57#[derive(Debug, Clone, Default)]
58pub struct GridSystem {
59 registry: Arc<Mutex<GridRegistry>>,
60}
61
62const GRID_LIVE_ROOT_NAME: &str = "grid_live_root";
63const GRID_LIVE_SELECTABLE_NAME: &str = "grid_live_selectable";
64const GRID_LIVE_RAYCASTABLE_NAME: &str = "grid_live_raycastable";
65const GRID_LIVE_SERIALIZE_NAME: &str = "grid_live_serialize";
66const GRID_LIVE_SHAPE_NAME: &str = "grid_live_shape";
67const GRID_LIVE_RENDERABLE_NAME: &str = "grid_live_renderable";
68const GRID_LIVE_COLOR_NAME: &str = "grid_live_color";
69const GRID_LIVE_OPACITY_NAME: &str = "grid_live_opacity";
70
71impl GridSystem {
72 pub const DEFAULT_EDITOR_GRID_SIZE_X: u32 = 8192;
73 pub const DEFAULT_EDITOR_GRID_SIZE_Z: u32 = 8192;
74 pub const DEFAULT_EDITOR_GRID_SPACING: f32 = 1.0;
75
76 pub fn new() -> Self {
77 Self::default()
78 }
79
80 pub fn install_handlers(&self, rx: &mut RxWorld) {
81 let mut registry = self.registry.lock().expect("grid registry mutex poisoned");
82 if registry.handlers_installed {
83 return;
84 }
85 registry.handlers_installed = true;
86 drop(registry);
87
88 let registry = Arc::clone(&self.registry);
89 rx.add_global_handler_closure_named(
90 SignalKind::ParentChanged,
91 Some("grid_system_live_registry".to_string()),
92 move |_world, _emit, signal| {
93 if matches!(
94 signal.event.as_ref(),
95 Some(EventSignal::ParentChanged { .. })
96 ) {
97 mark_registry_dirty(®istry);
98 }
99 },
100 );
101 }
102
103 pub fn mark_dirty(&self) {
104 mark_registry_dirty(&self.registry);
105 }
106
107 pub fn grid_owner_transform(world: &World, grid_component: ComponentId) -> Option<ComponentId> {
108 let mut current = world.parent_of(grid_component);
109 while let Some(component_id) = current {
110 if world
111 .get_component_by_id_as::<TransformComponent>(component_id)
112 .is_some()
113 {
114 return Some(component_id);
115 }
116 current = world.parent_of(component_id);
117 }
118 None
119 }
120
121 pub fn active_grid_for_editor(
122 &self,
123 world: &World,
124 editor_root: ComponentId,
125 ) -> Option<ActiveGrid> {
126 let selected = self
127 .selected_grid_for_editor(world, editor_root)
128 .or_else(|| {
129 self.enumerate_grids_for_editor(world, editor_root)
130 .into_iter()
131 .next()
132 })?;
133 if !selected.selectable || !selected.enabled || selected.hidden {
134 return None;
135 }
136 self.active_grid_from_entry(world, selected)
137 }
138
139 pub fn selected_grid_for_editor(
140 &self,
141 world: &World,
142 editor_root: ComponentId,
143 ) -> Option<GridEntry> {
144 let editor = world.get_component_by_id_as::<EditorComponent>(editor_root)?;
145 let selected = editor.selected?;
146 if let Some(entry) = self.grid_entry(world, selected)
147 && entry.editor_root == editor_root
148 {
149 return Some(entry);
150 }
151 self.grid_owned_by_transform(world, selected)
152 .filter(|entry| entry.editor_root == editor_root)
153 }
154
155 pub fn enumerate_grids_for_editor(
156 &self,
157 world: &World,
158 editor_root: ComponentId,
159 ) -> Vec<GridEntry> {
160 self.ensure_registry_current(world);
161 let registry = self.registry.lock().expect("grid registry mutex poisoned");
162 registry
163 .by_editor
164 .get(&editor_root)
165 .into_iter()
166 .flat_map(|ids| ids.iter())
167 .filter_map(|grid_id| {
168 registry
169 .by_grid
170 .get(grid_id)
171 .copied()
172 .map(|entry| refresh_grid_entry(world, entry))
173 })
174 .collect()
175 }
176
177 pub fn grid_owned_by_transform(
178 &self,
179 world: &World,
180 transform: ComponentId,
181 ) -> Option<GridEntry> {
182 self.ensure_registry_current(world);
183 world
184 .children_of(transform)
185 .iter()
186 .copied()
187 .find_map(|child| self.grid_entry(world, child))
188 }
189
190 pub fn active_grid_for_owner_transform(
191 &self,
192 world: &World,
193 owner_transform: ComponentId,
194 ) -> Option<ActiveGrid> {
195 let entry = self.grid_owned_by_transform(world, owner_transform)?;
196 if !entry.enabled || entry.hidden || !entry.selectable {
197 return None;
198 }
199 self.active_grid_from_entry(world, entry)
200 }
201
202 pub fn grid_entry(&self, world: &World, grid_component: ComponentId) -> Option<GridEntry> {
203 self.ensure_registry_current(world);
204 let registry = self.registry.lock().expect("grid registry mutex poisoned");
205 registry
206 .by_grid
207 .get(&grid_component)
208 .copied()
209 .map(|entry| refresh_grid_entry(world, entry))
210 }
211
212 pub fn grid_hit_context_for_renderable(
213 &self,
214 world: &World,
215 renderable: ComponentId,
216 ) -> Option<ActiveGrid> {
217 let grid_component = self.grid_component_for_renderable(world, renderable)?;
218 let entry = self.grid_entry(world, grid_component)?;
219 if !entry.enabled || entry.hidden || !entry.selectable {
220 return None;
221 }
222 self.active_grid_from_entry(world, entry)
223 }
224
225 pub fn grid_component_for_renderable(
226 &self,
227 world: &World,
228 renderable: ComponentId,
229 ) -> Option<ComponentId> {
230 let owner_transform = self.grid_owner_from_renderable(world, renderable)?;
231 self.grid_owned_by_transform(world, owner_transform)
232 .map(|entry| entry.grid_component)
233 }
234
235 pub fn grid_owner_from_renderable(
236 &self,
237 world: &World,
238 renderable: ComponentId,
239 ) -> Option<ComponentId> {
240 let mut current = Some(renderable);
241 while let Some(component_id) = current {
242 if world.component_label(component_id) == Some(GRID_LIVE_ROOT_NAME) {
243 return Self::grid_owner_transform(world, component_id);
244 }
245 current = world.parent_of(component_id);
246 }
247 None
248 }
249
250 pub fn snap_hit(active: &ActiveGrid, hit_point_world: [f32; 3]) -> GridSnapResult {
251 let local = transform_point(active.inverse_world, hit_point_world);
252 let cell_x = round_to_i32(local[0] / active.spacing);
253 let cell_y = round_to_i32(local[2] / active.spacing);
254 let snapped_local = [
255 cell_x as f32 * active.spacing,
256 0.0,
257 cell_y as f32 * active.spacing,
258 ];
259 let snapped_world = transform_point(active.matrix_world, snapped_local);
260
261 GridSnapResult {
262 point_world: snapped_world,
263 normal_world: active.normal_world,
264 step: GridStep {
265 cell: [cell_x, cell_y],
266 },
267 }
268 }
269
270 pub fn snap_point_preserving_plane_offset(
271 active: &ActiveGrid,
272 point_world: [f32; 3],
273 ) -> GridSnapResult {
274 let local = transform_point(active.inverse_world, point_world);
275 let cell_x = round_to_i32(local[0] / active.spacing);
276 let cell_y = round_to_i32(local[2] / active.spacing);
277 let snapped_local = [
278 cell_x as f32 * active.spacing,
279 local[1],
280 cell_y as f32 * active.spacing,
281 ];
282 let snapped_world = transform_point(active.matrix_world, snapped_local);
283
284 GridSnapResult {
285 point_world: snapped_world,
286 normal_world: active.normal_world,
287 step: GridStep {
288 cell: [cell_x, cell_y],
289 },
290 }
291 }
292
293 pub fn same_step(a: Option<GridStep>, b: GridStep) -> bool {
294 a == Some(b)
295 }
296
297 pub fn ensure_default_grid(
298 &self,
299 world: &mut World,
300 emit: &mut dyn SignalEmitter,
301 editor_root: ComponentId,
302 ) -> ComponentId {
303 if let Some(existing) = self
304 .enumerate_grids_for_editor(world, editor_root)
305 .into_iter()
306 .next()
307 {
308 return existing.owner_transform;
309 }
310
311 self.spawn_grid_for_editor(
312 world,
313 emit,
314 editor_root,
315 GridSpawnSpec::default_hidden_editor_grid(),
316 )
317 }
318
319 pub fn spawn_grid_for_editor(
320 &self,
321 world: &mut World,
322 emit: &mut dyn SignalEmitter,
323 editor_root: ComponentId,
324 spec: GridSpawnSpec,
325 ) -> ComponentId {
326 let index = self.enumerate_grids_for_editor(world, editor_root).len() + 1;
327 let (translation, rotation) = if spec.world_space_pose {
328 editor_local_pose_from_world(world, editor_root, spec.translation, spec.rotation)
329 .unwrap_or((spec.translation, spec.rotation))
330 } else {
331 (spec.translation, spec.rotation)
332 };
333 let grid_component = GridComponent::new(spec.spacing)
334 .with_size_x(spec.size_x)
335 .with_size_z(spec.size_z)
336 .with_enabled(spec.enabled)
337 .with_hidden(spec.hidden)
338 .with_selectable(true);
339
340 let owner_transform = world.add_component_boxed_named(
341 &format!("grid_{index}"),
342 Box::new(
343 TransformComponent::new()
344 .with_position(translation[0], translation[1], translation[2])
345 .with_rotation_quat(rotation),
346 ),
347 );
348 let grid = world.add_component_boxed_named(
349 &format!("grid_{index}_component"),
350 Box::new(grid_component),
351 );
352
353 let _ = world.add_child(editor_root, owner_transform);
354 let _ = world.add_child(owner_transform, grid);
355 world.init_component_tree(owner_transform, emit);
356 if spec.preview_mode || spec.enabled {
357 self.ensure_live_runtime(world, emit, owner_transform, spec.preview_mode);
358 }
359 self.mark_dirty();
360 owner_transform
361 }
362
363 pub fn set_grid_hidden(
364 &self,
365 world: &mut World,
366 emit: &mut dyn SignalEmitter,
367 owner_transform: ComponentId,
368 hidden: bool,
369 ) -> bool {
370 let Some(entry) = self.grid_owned_by_transform(world, owner_transform) else {
371 return false;
372 };
373 let Some(grid) = world.get_component_by_id_as_mut::<GridComponent>(entry.grid_component)
374 else {
375 return false;
376 };
377 if grid.hidden == hidden {
378 return false;
379 }
380 grid.hidden = hidden;
381 if grid.enabled {
382 self.ensure_live_runtime(world, emit, owner_transform, false);
383 self.sync_live_runtime_visibility(world, emit, owner_transform, false);
384 }
385 self.mark_dirty();
386 true
387 }
388
389 pub fn set_grid_enabled(
390 &self,
391 world: &mut World,
392 emit: &mut dyn SignalEmitter,
393 owner_transform: ComponentId,
394 enabled: bool,
395 ) -> bool {
396 let Some(entry) = self.grid_owned_by_transform(world, owner_transform) else {
397 return false;
398 };
399 let Some(grid) = world.get_component_by_id_as_mut::<GridComponent>(entry.grid_component)
400 else {
401 return false;
402 };
403 if grid.enabled == enabled {
404 return false;
405 }
406 grid.enabled = enabled;
407 if enabled {
408 self.ensure_live_runtime(world, emit, owner_transform, false);
409 self.sync_live_runtime_visibility(world, emit, owner_transform, false);
410 } else {
411 self.remove_live_runtime(world, owner_transform);
412 }
413 self.mark_dirty();
414 true
415 }
416
417 pub fn toggle_grid_hidden(
418 &self,
419 world: &mut World,
420 emit: &mut dyn SignalEmitter,
421 owner_transform: ComponentId,
422 ) -> bool {
423 let hidden = self
424 .grid_owned_by_transform(world, owner_transform)
425 .and_then(|entry| world.get_component_by_id_as::<GridComponent>(entry.grid_component))
426 .map(|grid| !grid.hidden);
427 hidden.is_some_and(|next| self.set_grid_hidden(world, emit, owner_transform, next))
428 }
429
430 pub fn toggle_grid_enabled(
431 &self,
432 world: &mut World,
433 emit: &mut dyn SignalEmitter,
434 owner_transform: ComponentId,
435 ) -> bool {
436 let enabled = self
437 .grid_owned_by_transform(world, owner_transform)
438 .and_then(|entry| world.get_component_by_id_as::<GridComponent>(entry.grid_component))
439 .map(|grid| !grid.enabled);
440 enabled.is_some_and(|next| self.set_grid_enabled(world, emit, owner_transform, next))
441 }
442
443 pub fn delete_grid(
444 &self,
445 world: &mut World,
446 emit: &mut dyn SignalEmitter,
447 owner_transform: ComponentId,
448 ) -> bool {
449 if world.get_component_record(owner_transform).is_none() {
450 return false;
451 }
452
453 emit.push_intent_now(
458 owner_transform,
459 IntentValue::RemoveSubtree {
460 component_ids: vec![owner_transform],
461 },
462 );
463 self.mark_dirty();
464 true
465 }
466
467 fn ensure_registry_current(&self, world: &World) {
468 let component_count = world.all_components().count();
469 let needs_sync = {
470 let registry = self.registry.lock().expect("grid registry mutex poisoned");
471 registry.dirty || registry.cached_component_count != component_count
472 };
473 if !needs_sync {
474 return;
475 }
476
477 let mut by_grid = HashMap::new();
478 let mut by_editor: HashMap<ComponentId, Vec<ComponentId>> = HashMap::new();
479 for component_id in world.all_components() {
480 let Some(grid) = world.get_component_by_id_as::<GridComponent>(component_id) else {
481 continue;
482 };
483 let Some(owner_transform) = Self::grid_owner_transform(world, component_id) else {
484 continue;
485 };
486 let Some(editor_root) = nearest_editor_ancestor(world, owner_transform) else {
487 continue;
488 };
489 let entry = GridEntry {
490 grid_component: component_id,
491 owner_transform,
492 editor_root,
493 enabled: grid.enabled,
494 hidden: grid.hidden,
495 selectable: grid.selectable,
496 };
497 by_grid.insert(component_id, entry);
498 by_editor.entry(editor_root).or_default().push(component_id);
499 }
500
501 let mut registry = self.registry.lock().expect("grid registry mutex poisoned");
502 registry.by_grid = by_grid;
503 registry.by_editor = by_editor;
504 registry.cached_component_count = component_count;
505 registry.dirty = false;
506 }
507
508 fn active_grid_from_entry(&self, world: &World, entry: GridEntry) -> Option<ActiveGrid> {
509 let matrix_world = TransformSystem::world_model(world, entry.owner_transform)?;
510 let inverse_world = mat4_inverse(matrix_world)?;
511 let origin_world = [matrix_world[3][0], matrix_world[3][1], matrix_world[3][2]];
512 let normal_world = vec3_normalize(transform_direction(matrix_world, [0.0, 1.0, 0.0]));
513 if normal_world == [0.0, 0.0, 0.0] {
514 return None;
515 }
516
517 Some(ActiveGrid {
518 component: entry.grid_component,
519 spacing: world
520 .get_component_by_id_as::<GridComponent>(entry.grid_component)?
521 .spacing
522 .max(1e-4),
523 origin_world,
524 normal_world,
525 matrix_world,
526 inverse_world,
527 })
528 }
529
530 fn ensure_live_runtime(
531 &self,
532 world: &mut World,
533 emit: &mut dyn SignalEmitter,
534 owner_transform: ComponentId,
535 preview_mode: bool,
536 ) {
537 if self.live_runtime_root(world, owner_transform).is_some() {
538 self.sync_live_runtime_visibility(world, emit, owner_transform, preview_mode);
539 return;
540 }
541 let Some(entry) = self.grid_owned_by_transform(world, owner_transform) else {
542 return;
543 };
544 let Some(grid) = world
545 .get_component_by_id_as::<GridComponent>(entry.grid_component)
546 .copied()
547 else {
548 return;
549 };
550
551 let visual_scale_x = grid.size_x as f32 * grid.spacing;
552 let visual_scale_z = grid.size_z as f32 * grid.spacing;
553 let live_root = world
554 .add_component_boxed_named(GRID_LIVE_ROOT_NAME, Box::new(TransformComponent::new()));
555 let live_selectable = world.add_component_boxed_named(
556 GRID_LIVE_SELECTABLE_NAME,
557 Box::new(SelectableComponent::off()),
558 );
559 let live_raycastable = world.add_component_boxed_named(
560 GRID_LIVE_RAYCASTABLE_NAME,
561 Box::new(RaycastableComponent::disabled()),
562 );
563 let live_serialize = world.add_component_boxed_named(
564 GRID_LIVE_SERIALIZE_NAME,
565 Box::new(SerializeComponent::off()),
566 );
567 let live_shape = world.add_component_boxed_named(
568 GRID_LIVE_SHAPE_NAME,
569 Box::new(
570 TransformComponent::new()
571 .with_position(0.0, 0.005, 0.0)
572 .with_scale(visual_scale_x, 0.0025, visual_scale_z),
573 ),
574 );
575 let live_renderable = world.add_component_boxed_named(
576 GRID_LIVE_RENDERABLE_NAME,
577 Box::new(RenderableComponent::from_cpu_mesh_handle(
578 crate::engine::graphics::primitives::CpuMeshHandle::CUBE,
579 crate::engine::graphics::primitives::MaterialHandle::GRID_MESH,
580 )),
581 );
582 let live_color = world.add_component_boxed_named(
583 GRID_LIVE_COLOR_NAME,
584 Box::new(ColorComponent::rgba(1.0, 1.0, 1.0, 1.0)),
585 );
586 let live_opacity = world.add_component_boxed_named(
587 GRID_LIVE_OPACITY_NAME,
588 Box::new(OpacityComponent::new().with_opacity(grid_opacity(grid.hidden, preview_mode))),
589 );
590
591 let _ = world.add_child(owner_transform, live_root);
592 let _ = world.add_child(live_root, live_selectable);
593 let _ = world.add_child(live_root, live_serialize);
594 let _ = world.add_child(live_root, live_shape);
595 let _ = world.add_child(live_shape, live_renderable);
596 let _ = world.add_child(live_renderable, live_color);
597 let _ = world.add_child(live_renderable, live_opacity);
598 let _ = world.add_child(live_renderable, live_raycastable);
599 world.init_component_tree(live_root, emit);
600 emit.push_intent_now(
601 live_root,
602 IntentValue::RegisterTransform {
603 component_ids: vec![live_root, live_shape],
604 },
605 );
606 emit.push_intent_now(
607 live_renderable,
608 IntentValue::RegisterRenderable {
609 component_ids: vec![live_renderable],
610 },
611 );
612 }
613
614 fn sync_live_runtime_visibility(
615 &self,
616 world: &mut World,
617 emit: &mut dyn SignalEmitter,
618 owner_transform: ComponentId,
619 preview_mode: bool,
620 ) {
621 let Some(entry) = self.grid_owned_by_transform(world, owner_transform) else {
622 return;
623 };
624 let Some(grid) = world
625 .get_component_by_id_as::<GridComponent>(entry.grid_component)
626 .copied()
627 else {
628 return;
629 };
630 if let Some(opacity_id) = world.find_component(owner_transform, "#grid_live_opacity")
631 && let Some(opacity) = world.get_component_by_id_as_mut::<OpacityComponent>(opacity_id)
632 {
633 opacity.opacity = grid_opacity(grid.hidden, preview_mode);
634 emit.push_intent_now(
635 owner_transform,
636 IntentValue::RegisterOpacity {
637 component_ids: vec![opacity_id],
638 },
639 );
640 }
641 if let Some(selectable_id) = world.find_component(owner_transform, "#grid_live_selectable")
642 && let Some(selectable) =
643 world.get_component_by_id_as_mut::<SelectableComponent>(selectable_id)
644 {
645 selectable.enabled = false;
646 }
647 if let Some(raycastable_id) =
648 world.find_component(owner_transform, "#grid_live_raycastable")
649 && let Some(raycastable) =
650 world.get_component_by_id_as_mut::<RaycastableComponent>(raycastable_id)
651 {
652 raycastable.enable = false;
653 }
654 }
655
656 fn live_runtime_root(
657 &self,
658 world: &World,
659 owner_transform: ComponentId,
660 ) -> Option<ComponentId> {
661 world.find_component(owner_transform, "#grid_live_root")
662 }
663
664 fn remove_live_runtime(&self, world: &mut World, owner_transform: ComponentId) {
665 if let Some(live_root) = self.live_runtime_root(world, owner_transform) {
666 let _ = world.remove_component_subtree(live_root);
667 }
668 }
669}
670
671fn mark_registry_dirty(registry: &Arc<Mutex<GridRegistry>>) {
672 registry.lock().expect("grid registry mutex poisoned").dirty = true;
673}
674
675fn refresh_grid_entry(world: &World, mut entry: GridEntry) -> GridEntry {
676 if let Some(grid) = world.get_component_by_id_as::<GridComponent>(entry.grid_component) {
677 entry.enabled = grid.enabled;
678 entry.hidden = grid.hidden;
679 entry.selectable = grid.selectable;
680 }
681 entry
682}
683
684fn grid_opacity(hidden: bool, preview_mode: bool) -> f32 {
685 if preview_mode {
686 0.45
687 } else if hidden {
688 0.0
689 } else {
690 1.0
691 }
692}
693
694fn nearest_editor_ancestor(world: &World, start: ComponentId) -> Option<ComponentId> {
695 let mut current = Some(start);
696 while let Some(component_id) = current {
697 if world
698 .get_component_by_id_as::<EditorComponent>(component_id)
699 .is_some()
700 {
701 return Some(component_id);
702 }
703 current = world.parent_of(component_id);
704 }
705 None
706}
707
708fn round_to_i32(value: f32) -> i32 {
709 value.round().clamp(i32::MIN as f32, i32::MAX as f32) as i32
710}
711
712fn transform_point(m: [[f32; 4]; 4], p: [f32; 3]) -> [f32; 3] {
713 [
714 m[0][0] * p[0] + m[1][0] * p[1] + m[2][0] * p[2] + m[3][0],
715 m[0][1] * p[0] + m[1][1] * p[1] + m[2][1] * p[2] + m[3][1],
716 m[0][2] * p[0] + m[1][2] * p[1] + m[2][2] * p[2] + m[3][2],
717 ]
718}
719
720fn transform_direction(m: [[f32; 4]; 4], v: [f32; 3]) -> [f32; 3] {
721 [
722 m[0][0] * v[0] + m[1][0] * v[1] + m[2][0] * v[2],
723 m[0][1] * v[0] + m[1][1] * v[1] + m[2][1] * v[2],
724 m[0][2] * v[0] + m[1][2] * v[1] + m[2][2] * v[2],
725 ]
726}
727
728fn editor_local_pose_from_world(
729 world: &World,
730 editor_root: ComponentId,
731 world_translation: [f32; 3],
732 world_rotation: [f32; 4],
733) -> Option<([f32; 3], [f32; 4])> {
734 let editor_world = TransformSystem::world_model(world, editor_root)?;
735 let editor_inverse = mat4_inverse(editor_world)?;
736 let world_model = TransformComponent::new()
737 .with_position(
738 world_translation[0],
739 world_translation[1],
740 world_translation[2],
741 )
742 .with_rotation_quat(world_rotation)
743 .transform
744 .model;
745 let local_model = mat4_mul(editor_inverse, world_model);
746 Some((
747 [local_model[3][0], local_model[3][1], local_model[3][2]],
748 mat_to_quat(local_model),
749 ))
750}
751
752#[derive(Debug, Clone, Copy, PartialEq)]
753pub struct GridSpawnSpec {
754 pub translation: [f32; 3],
755 pub rotation: [f32; 4],
756 pub world_space_pose: bool,
757 pub spacing: f32,
758 pub size_x: u32,
759 pub size_z: u32,
760 pub enabled: bool,
761 pub hidden: bool,
762 pub preview_mode: bool,
763}
764
765impl GridSpawnSpec {
766 pub fn default_hidden_editor_grid() -> Self {
767 Self {
768 translation: [0.0, 0.0, 0.0],
769 rotation: [0.0, 0.0, 0.0, 1.0],
770 world_space_pose: false,
771 spacing: GridSystem::DEFAULT_EDITOR_GRID_SPACING,
772 size_x: GridSystem::DEFAULT_EDITOR_GRID_SIZE_X,
773 size_z: GridSystem::DEFAULT_EDITOR_GRID_SIZE_Z,
774 enabled: true,
775 hidden: true,
776 preview_mode: false,
777 }
778 }
779
780 pub fn from_cursor_pose(
781 translation: Option<[f32; 3]>,
782 rotation: Option<[f32; 4]>,
783 preview_mode: bool,
784 ) -> Self {
785 Self {
786 translation: translation.unwrap_or([0.0, 0.0, 0.0]),
787 rotation: rotation.unwrap_or([0.0, 0.0, 0.0, 1.0]),
788 world_space_pose: true,
789 spacing: GridSystem::DEFAULT_EDITOR_GRID_SPACING,
790 size_x: GridComponent::DEFAULT_SIZE_X,
791 size_z: GridComponent::DEFAULT_SIZE_Z,
792 enabled: true,
793 hidden: preview_mode,
794 preview_mode,
795 }
796 }
797}
798
799pub fn remap_grid_rotation_to_surface_up(surface_aligned_rotation: [f32; 4]) -> [f32; 4] {
800 let z_to_y = quat_from_axis_angle([1.0, 0.0, 0.0], std::f32::consts::FRAC_PI_2);
801 quat_mul(surface_aligned_rotation, z_to_y)
802}
803
804#[cfg(test)]
805mod tests {
806 use super::*;
807 use crate::engine::ecs::CommandQueue;
808
809 #[test]
810 fn snap_hit_rounds_to_grid_cell_on_xz_plane() {
811 let mut world = World::default();
812 let active = ActiveGrid {
813 component: world.add_component(TransformComponent::new()),
814 spacing: 0.5,
815 origin_world: [0.0, 0.0, 0.0],
816 normal_world: [0.0, 1.0, 0.0],
817 matrix_world: TransformComponent::new().transform.matrix_world,
818 inverse_world: TransformComponent::new().transform.matrix_world,
819 };
820
821 let snapped = GridSystem::snap_hit(&active, [0.24, 0.76, 0.18]);
822 assert_eq!(snapped.step.cell, [0, 0]);
823 assert!((snapped.point_world[0] - 0.0).abs() < 1e-5);
824 assert!((snapped.point_world[1] - 0.0).abs() < 1e-5);
825 assert!((snapped.point_world[2] - 0.0).abs() < 1e-5);
826 }
827
828 #[test]
829 fn snap_point_preserving_plane_offset_keeps_local_height() {
830 let mut world = World::default();
831 let active = ActiveGrid {
832 component: world.add_component(TransformComponent::new()),
833 spacing: 0.5,
834 origin_world: [0.0, 0.0, 0.0],
835 normal_world: [0.0, 1.0, 0.0],
836 matrix_world: TransformComponent::new().transform.matrix_world,
837 inverse_world: TransformComponent::new().transform.matrix_world,
838 };
839
840 let snapped = GridSystem::snap_point_preserving_plane_offset(&active, [0.24, 0.76, 0.18]);
841 assert_eq!(snapped.step.cell, [0, 0]);
842 assert!((snapped.point_world[0] - 0.0).abs() < 1e-5);
843 assert!((snapped.point_world[1] - 0.76).abs() < 1e-5);
844 assert!((snapped.point_world[2] - 0.0).abs() < 1e-5);
845 }
846
847 #[test]
848 fn active_grid_uses_editor_selected_grid_component() {
849 let mut world = World::default();
850 let grids = GridSystem::new();
851 let editor = world.add_component(EditorComponent::new());
852 let grid_transform = world.add_component(TransformComponent::new());
853 let grid = world.add_component(GridComponent::new(0.25));
854 let _ = world.add_child(editor, grid_transform);
855 let _ = world.add_child(grid_transform, grid);
856
857 world
858 .get_component_by_id_as_mut::<EditorComponent>(editor)
859 .expect("editor")
860 .selected = Some(grid);
861
862 let active = grids
863 .active_grid_for_editor(&world, editor)
864 .expect("active grid");
865 assert_eq!(active.component, grid);
866 assert!((active.spacing - 0.25).abs() < 1e-5);
867 }
868
869 #[test]
870 fn active_grid_uses_grid_owned_by_selected_transform() {
871 let mut world = World::default();
872 let grids = GridSystem::new();
873 let editor = world.add_component(EditorComponent::new());
874 let grid_transform = world.add_component(TransformComponent::new());
875 let grid = world.add_component(GridComponent::new(0.75));
876 let _ = world.add_child(editor, grid_transform);
877 let _ = world.add_child(grid_transform, grid);
878
879 world
880 .get_component_by_id_as_mut::<EditorComponent>(editor)
881 .expect("editor")
882 .selected = Some(grid_transform);
883
884 let active = grids
885 .active_grid_for_editor(&world, editor)
886 .expect("active grid");
887 assert_eq!(active.component, grid);
888 assert!((active.spacing - 0.75).abs() < 1e-5);
889 }
890
891 #[test]
892 fn enumerate_grids_for_editor_returns_owner_transforms() {
893 let mut world = World::default();
894 let grids = GridSystem::new();
895 let editor = world.add_component(EditorComponent::new());
896 let a_transform = world.add_component(TransformComponent::new());
897 let a_grid = world.add_component(GridComponent::new(0.25));
898 let b_transform = world.add_component(TransformComponent::new());
899 let b_grid = world.add_component(GridComponent::new(0.5).with_enabled(false));
900 let _ = world.add_child(editor, a_transform);
901 let _ = world.add_child(a_transform, a_grid);
902 let _ = world.add_child(editor, b_transform);
903 let _ = world.add_child(b_transform, b_grid);
904
905 let entries = grids.enumerate_grids_for_editor(&world, editor);
906 assert_eq!(entries.len(), 2);
907 assert!(entries.iter().any(|entry| {
908 entry.grid_component == a_grid
909 && entry.owner_transform == a_transform
910 && entry.editor_root == editor
911 && entry.enabled
912 && !entry.hidden
913 && entry.selectable
914 }));
915 assert!(entries.iter().any(|entry| {
916 entry.grid_component == b_grid
917 && entry.owner_transform == b_transform
918 && entry.editor_root == editor
919 && !entry.enabled
920 }));
921 }
922
923 #[test]
924 fn grid_entry_reads_live_enabled_state_after_component_edit() {
925 let mut world = World::default();
926 let grids = GridSystem::new();
927 let editor = world.add_component(EditorComponent::new());
928 let transform = world.add_component(TransformComponent::new());
929 let grid = world.add_component(GridComponent::new(1.0));
930 let _ = world.add_child(editor, transform);
931 let _ = world.add_child(transform, grid);
932
933 assert!(
934 grids
935 .grid_entry(&world, grid)
936 .expect("grid entry before toggle")
937 .enabled
938 );
939
940 world
941 .get_component_by_id_as_mut::<GridComponent>(grid)
942 .expect("grid component")
943 .enabled = false;
944
945 assert!(
946 !grids
947 .grid_entry(&world, grid)
948 .expect("grid entry after toggle")
949 .enabled
950 );
951 }
952
953 #[test]
954 fn active_grid_ignores_hidden_grid() {
955 let mut world = World::default();
956 let grids = GridSystem::new();
957 let editor = world.add_component(EditorComponent::new());
958 let grid_transform = world.add_component(TransformComponent::new());
959 let grid = world.add_component(GridComponent::new(1.0).with_hidden(true));
960 let _ = world.add_child(editor, grid_transform);
961 let _ = world.add_child(grid_transform, grid);
962
963 world
964 .get_component_by_id_as_mut::<EditorComponent>(editor)
965 .expect("editor")
966 .selected = Some(grid_transform);
967
968 assert!(grids.active_grid_for_editor(&world, editor).is_none());
969 }
970
971 #[test]
972 fn set_grid_hidden_re_registers_live_opacity() {
973 let mut world = World::default();
974 let grids = GridSystem::new();
975 let editor = world.add_component(EditorComponent::new());
976 let mut emit = CommandQueue::new();
977
978 let owner_transform = grids.spawn_grid_for_editor(
979 &mut world,
980 &mut emit,
981 editor,
982 GridSpawnSpec::default_hidden_editor_grid(),
983 );
984
985 let mut rx = RxWorld::default();
986 emit.drain_into_rx(&mut rx);
987 let _ = rx.drain_ready_intents();
988
989 assert!(grids.set_grid_hidden(&mut world, &mut emit, owner_transform, false));
990
991 emit.drain_into_rx(&mut rx);
992 let intents = rx.drain_ready_intents();
993 assert!(intents.iter().any(|signal| {
994 matches!(
995 signal.intent.as_ref().map(|intent| &intent.value),
996 Some(IntentValue::RegisterOpacity { .. })
997 )
998 }));
999 }
1000
1001 #[test]
1002 fn spawn_grid_from_cursor_pose_converts_world_pose_into_editor_local_space() {
1003 let mut world = World::default();
1004 let grids = GridSystem::new();
1005 let mut emit = CommandQueue::new();
1006 let editor_mount = world.add_component(
1007 TransformComponent::new()
1008 .with_position(5.0, 0.0, -2.0)
1009 .with_rotation_quat(quat_from_axis_angle(
1010 [0.0, 1.0, 0.0],
1011 std::f32::consts::FRAC_PI_2,
1012 )),
1013 );
1014 let editor = world.add_component(EditorComponent::new());
1015 let _ = world.add_child(editor_mount, editor);
1016
1017 let desired_world_translation = [8.0, 1.5, 4.0];
1018 let desired_world_rotation =
1019 quat_from_axis_angle([1.0, 0.0, 0.0], std::f32::consts::FRAC_PI_2);
1020
1021 let owner_transform = grids.spawn_grid_for_editor(
1022 &mut world,
1023 &mut emit,
1024 editor,
1025 GridSpawnSpec::from_cursor_pose(
1026 Some(desired_world_translation),
1027 Some(desired_world_rotation),
1028 false,
1029 ),
1030 );
1031
1032 let editor_world = TransformSystem::world_model(&world, editor).expect("editor world");
1033 let owner_local = world
1034 .get_component_by_id_as::<TransformComponent>(owner_transform)
1035 .expect("grid local")
1036 .transform
1037 .model;
1038 let owner_world = mat4_mul(editor_world, owner_local);
1039 let actual_world_translation = [owner_world[3][0], owner_world[3][1], owner_world[3][2]];
1040 let actual_world_rotation = mat_to_quat(owner_world);
1041
1042 for axis in 0..3 {
1043 assert!(
1044 (actual_world_translation[axis] - desired_world_translation[axis]).abs() < 1e-4
1045 );
1046 }
1047 let rotation_dot = actual_world_rotation[0] * desired_world_rotation[0]
1048 + actual_world_rotation[1] * desired_world_rotation[1]
1049 + actual_world_rotation[2] * desired_world_rotation[2]
1050 + actual_world_rotation[3] * desired_world_rotation[3];
1051 assert!(rotation_dot.abs() > 0.9999);
1052 }
1053}