Skip to main content

mittens_engine/engine/ecs/system/
grid_system.rs

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(&registry);
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        // The cleanup-aware subtree executor unregisters renderables, BVH/raycast
454        // entries, transforms, and other runtime state before removing ECS records.
455        // Removing the World subtree here first leaves those later cleanup intents
456        // without the component handles they need, producing orphaned visuals.
457        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}