Skip to main content

mittens_engine/engine/ecs/system/editor/
context.rs

1use std::collections::HashSet;
2use std::sync::{Arc, Mutex};
3
4use crate::engine::ecs::component::{
5    ColorComponent, EditorComponent, EditorInteractionMode, EmissiveComponent, OpacityComponent,
6    RaycastableComponent, RenderableComponent, SelectableComponent, SelectionComponent,
7    SerializeComponent, SignalObserverRouterComponent, TransformComponent,
8};
9use crate::engine::ecs::system::editor::paint_panel::COLOR_PANEL_ROOT_SELECTOR;
10use crate::engine::ecs::system::editor::settings_panel::{
11    EDITOR_SETTINGS_PAYLOAD_NAME, EDITOR_SETTINGS_SELECTION_SELECTOR, EditorSettingsOption,
12};
13use crate::engine::ecs::system::editor_system::select_editor_target;
14use crate::engine::ecs::system::object_placement_preview::PlacementPreviewSession;
15use crate::engine::ecs::system::paint_placement::SurfacePlacementFrame;
16use crate::engine::ecs::system::selection_system::resolve_semantic_target_from_payload;
17use crate::engine::ecs::{
18    ComponentId, EventSignal, IntentValue, RxWorld, Signal, SignalEmitter, SignalKind, World,
19};
20use crate::engine::graphics::primitives::{CpuMeshHandle, MaterialHandle};
21use std::f32::consts::FRAC_PI_2;
22
23const PANEL_LAYOUT_SELECTION_SELECTOR: &str = "#editor_panel_layout_selection";
24const WORLD_PANEL_SELECTION_SELECTOR: &str = "#world_panel_selection";
25const ASSETS_SELECTION_SELECTOR: &str = "#assets_selection";
26const GRID_PANEL_SELECTION_SELECTOR: &str = "#grid_panel_selection";
27const PAINT_PANEL_ROOT_SELECTOR: &str = "#paint_panel_root";
28pub const EDITOR_WORKSPACE_ASSET_SELECTION_CHANGED: &str = "EditorWorkspaceAssetSelectionChanged";
29pub const EDITOR_WORKSPACE_COLOR_SELECTION_CHANGED: &str = "EditorWorkspaceColorSelectionChanged";
30const PAINT_SYSTEM_HANDLER_NAME: &str = "paint_system";
31const EDITOR_PANEL_REFRESH_HANDLER_NAME: &str = "editor_panel_refresh";
32const EDITOR_SELECT_HANDLER_NAME: &str = "editor_select";
33const EDITOR_CURSOR_HANDLER_NAME: &str = "editor_cursor_3d";
34const DEBUG_BLACKLIST_EDITOR_PANEL_REFRESH: bool = true;
35const CURSOR_MARKER_ROOT_NAME: &str = "editor_cursor_marker";
36const CURSOR_MARKER_SIZE: f32 = 0.5;
37const CURSOR_MARKER_OPACITY: f32 = 0.9;
38
39#[derive(Debug, Clone, PartialEq)]
40pub struct EditorContextState {
41    pub active_editor: Option<ComponentId>,
42    pub selected_component: Option<ComponentId>,
43    pub active_grid_owner_transform: Option<ComponentId>,
44    pub selected_asset_payload: Option<ComponentId>,
45    pub focused_panel: Option<ComponentId>,
46    pub interaction_mode: EditorInteractionMode,
47    pub armature_visible: bool,
48    pub bounds_visible: bool,
49    pub last_scene_interacted_editor: Option<ComponentId>,
50    pub cursor_translation: Option<[f32; 3]>,
51    pub cursor_rotation: Option<[f32; 4]>,
52    pub cursor_frame: Option<SurfacePlacementFrame>,
53    pub pending_grid_placement_editor: Option<ComponentId>,
54    pub grid_preview_session: Option<PlacementPreviewSession>,
55}
56
57impl Default for EditorContextState {
58    fn default() -> Self {
59        Self {
60            active_editor: None,
61            selected_component: None,
62            active_grid_owner_transform: None,
63            selected_asset_payload: None,
64            focused_panel: None,
65            interaction_mode: EditorInteractionMode::default(),
66            armature_visible: false,
67            bounds_visible: false,
68            last_scene_interacted_editor: None,
69            cursor_translation: None,
70            cursor_rotation: None,
71            cursor_frame: None,
72            pending_grid_placement_editor: None,
73            grid_preview_session: None,
74        }
75    }
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub(crate) struct SemanticEditorSelectionResult {
80    pub(crate) target_component: ComponentId,
81    pub(crate) active_editor: Option<ComponentId>,
82    pub(crate) gizmo_target: Option<ComponentId>,
83    pub(crate) used_editor_selection_path: bool,
84}
85
86#[derive(Debug, Clone, Default)]
87struct EditorContextWorkspaceState {
88    panel_query_root: Option<ComponentId>,
89    cursor_host_root: Option<ComponentId>,
90    gizmo_host_root: Option<ComponentId>,
91    shared_transform_gizmo: Option<ComponentId>,
92    registered_editors: Vec<ComponentId>,
93}
94
95impl EditorContextWorkspaceState {
96    fn register_editor(&mut self, editor_root: ComponentId) -> bool {
97        if self.registered_editors.contains(&editor_root) {
98            return false;
99        }
100        self.registered_editors.push(editor_root);
101        true
102    }
103
104    fn default_active_editor(&self) -> Option<ComponentId> {
105        self.registered_editors.first().copied()
106    }
107}
108
109#[derive(Debug, Clone, PartialEq, Eq)]
110pub enum EditorContextEvent {
111    ActiveEditorChanged {
112        editor: Option<ComponentId>,
113        selected_component: Option<ComponentId>,
114        interaction_mode: EditorInteractionMode,
115    },
116    PanelFocusChanged {
117        focused_panel: Option<ComponentId>,
118    },
119    WorldPanelSelectionChanged {
120        component: Option<ComponentId>,
121        editor: Option<ComponentId>,
122        interaction_mode: EditorInteractionMode,
123    },
124    AssetPanelSelectionChanged {
125        asset_payload: Option<ComponentId>,
126    },
127    GridPanelSelectionChanged {
128        active_grid_owner_transform: Option<ComponentId>,
129        editor: Option<ComponentId>,
130    },
131    EditorSelectionChanged {
132        editor: ComponentId,
133        component: Option<ComponentId>,
134        interaction_mode: EditorInteractionMode,
135    },
136    InteractionModeChanged {
137        editor: Option<ComponentId>,
138        interaction_mode: EditorInteractionMode,
139    },
140}
141
142pub fn reduce_editor_context_state(
143    old: &EditorContextState,
144    event: &EditorContextEvent,
145) -> EditorContextState {
146    let mut new = old.clone();
147    match event {
148        EditorContextEvent::ActiveEditorChanged {
149            editor,
150            selected_component,
151            interaction_mode,
152        } => {
153            new.active_editor = *editor;
154            new.selected_component = *selected_component;
155            new.interaction_mode = *interaction_mode;
156        }
157        EditorContextEvent::PanelFocusChanged { focused_panel } => {
158            new.focused_panel = *focused_panel;
159        }
160        EditorContextEvent::WorldPanelSelectionChanged {
161            component,
162            editor,
163            interaction_mode,
164        } => {
165            new.selected_component = *component;
166            if editor.is_some() {
167                new.active_editor = *editor;
168            }
169            new.interaction_mode = *interaction_mode;
170        }
171        EditorContextEvent::AssetPanelSelectionChanged { asset_payload } => {
172            new.selected_asset_payload = *asset_payload;
173        }
174        EditorContextEvent::GridPanelSelectionChanged {
175            active_grid_owner_transform,
176            editor,
177        } => {
178            new.active_grid_owner_transform = *active_grid_owner_transform;
179            if editor.is_some() {
180                new.active_editor = *editor;
181            }
182        }
183        EditorContextEvent::EditorSelectionChanged {
184            editor,
185            component,
186            interaction_mode,
187        } => {
188            new.active_editor = Some(*editor);
189            new.selected_component = component.or(Some(*editor));
190            new.interaction_mode = *interaction_mode;
191        }
192        EditorContextEvent::InteractionModeChanged {
193            editor,
194            interaction_mode,
195        } => {
196            if editor.is_some() {
197                new.active_editor = *editor;
198            }
199            new.interaction_mode = *interaction_mode;
200        }
201    }
202    new
203}
204
205#[derive(Debug, Default)]
206pub struct EditorContextSystem {
207    installed_editor_roots: HashSet<ComponentId>,
208    shared_panel_handlers_installed: bool,
209    state: Arc<Mutex<EditorContextState>>,
210    workspace: Arc<Mutex<EditorContextWorkspaceState>>,
211}
212
213impl EditorContextSystem {
214    pub fn new() -> Self {
215        Self::default()
216    }
217
218    pub fn shared_state(&self) -> Arc<Mutex<EditorContextState>> {
219        Arc::clone(&self.state)
220    }
221
222    pub fn register_editor_identity(&mut self, world: &World, editor_root: ComponentId) {
223        let requested_active = world
224            .get_component_by_id_as::<EditorComponent>(editor_root)
225            .is_some_and(|editor| editor.active);
226        let registered = self
227            .workspace
228            .lock()
229            .expect("editor context workspace poisoned")
230            .register_editor(editor_root);
231
232        if requested_active {
233            let interaction_mode = world
234                .get_component_by_id_as::<EditorComponent>(editor_root)
235                .map(|editor| editor.interaction_mode)
236                .unwrap_or_default();
237            let mut state = self.state.lock().expect("editor context state poisoned");
238            *state = reduce_editor_context_state(
239                &state,
240                &EditorContextEvent::ActiveEditorChanged {
241                    editor: Some(editor_root),
242                    selected_component: None,
243                    interaction_mode,
244                },
245            );
246        } else if registered {
247            ensure_default_active_editor(&self.state, &self.workspace);
248        }
249    }
250
251    pub fn install_scoped_handlers_for_editor(
252        &mut self,
253        rx: &mut RxWorld,
254        world: &mut World,
255        editor_root: ComponentId,
256        panel_query_root: ComponentId,
257    ) {
258        ensure_editor_observer_router(world, editor_root);
259        {
260            let mut workspace = self
261                .workspace
262                .lock()
263                .expect("editor context workspace poisoned");
264            workspace.panel_query_root = Some(panel_query_root);
265            let registered = workspace.register_editor(editor_root);
266            drop(workspace);
267
268            if registered {
269                ensure_default_active_editor(&self.state, &self.workspace);
270            }
271        }
272
273        if !self.shared_panel_handlers_installed {
274            self.shared_panel_handlers_installed = true;
275            install_shared_panel_handlers(
276                rx,
277                world,
278                panel_query_root,
279                Arc::clone(&self.state),
280                Arc::clone(&self.workspace),
281            );
282            bootstrap_editor_context(world, panel_query_root, &self.state, &self.workspace);
283            sync_editor_observer_routes(world, &self.state, &self.workspace);
284        }
285
286        if self.installed_editor_roots.insert(editor_root) {
287            install_editor_handlers(
288                rx,
289                editor_root,
290                Arc::clone(&self.state),
291                Arc::clone(&self.workspace),
292            );
293        }
294        sync_global_editor_interaction_mode(world, &self.state);
295        sync_editor_observer_routes(world, &self.state, &self.workspace);
296        let mut emit = NullEmit;
297        let cursor_host = ensure_workspace_cursor_host(world, &self.workspace);
298        sync_editor_cursor_visual(world, &mut emit, &self.state, cursor_host);
299    }
300}
301
302struct NullEmit;
303
304impl SignalEmitter for NullEmit {
305    fn push_event(&mut self, _scope: ComponentId, _event: EventSignal) {}
306
307    fn push_intent(&mut self, _scope: ComponentId, _intent: crate::engine::ecs::IntentSignal) {}
308}
309
310fn install_shared_panel_handlers(
311    rx: &mut RxWorld,
312    world: &World,
313    panel_query_root: ComponentId,
314    state: Arc<Mutex<EditorContextState>>,
315    workspace: Arc<Mutex<EditorContextWorkspaceState>>,
316) {
317    let _ = world;
318    rx.add_handler_closure_named(
319        SignalKind::SelectionChanged,
320        panel_query_root,
321        Some("editor_system".to_string()),
322        move |world, emit, signal| {
323            let active_editor = state
324                .lock()
325                .expect("editor context state poisoned")
326                .active_editor;
327            let default_editor = workspace
328                .lock()
329                .expect("editor context workspace poisoned")
330                .default_active_editor();
331            let Some(event) = editor_context_event_from_shared_signal(
332                world,
333                panel_query_root,
334                signal,
335                active_editor.or(default_editor),
336            ) else {
337                return;
338            };
339            apply_editor_context_event(&state, &event);
340            if let EditorContextEvent::GridPanelSelectionChanged {
341                active_grid_owner_transform: Some(owner_transform),
342                ..
343            } = event
344            {
345                let _ = apply_semantic_target_selection(world, emit, &state, owner_transform, true);
346            }
347            emit_editor_workspace_data_event(world, emit, panel_query_root, &event);
348            sync_editor_component_selection(world, &event);
349            let cursor_host = ensure_workspace_cursor_host(world, &workspace);
350            sync_editor_cursor_visual(world, emit, &state, cursor_host);
351            sync_editor_observer_routes(world, &state, &workspace);
352        },
353    );
354}
355
356fn install_editor_handlers(
357    rx: &mut RxWorld,
358    editor_root: ComponentId,
359    state: Arc<Mutex<EditorContextState>>,
360    workspace: Arc<Mutex<EditorContextWorkspaceState>>,
361) {
362    rx.add_handler_closure_named(
363        SignalKind::SelectionChanged,
364        editor_root,
365        Some("editor_system".to_string()),
366        move |world, _emit, signal| {
367            let Some(EventSignal::SelectionChanged {
368                selection_root,
369                selected_component,
370                ..
371            }) = signal.event.as_ref()
372            else {
373                return;
374            };
375            if *selection_root != editor_root {
376                return;
377            }
378
379            let event = EditorContextEvent::EditorSelectionChanged {
380                editor: editor_root,
381                component: *selected_component,
382                interaction_mode: editor_interaction_mode(world, Some(editor_root)),
383            };
384            apply_editor_context_event(&state, &event);
385            sync_editor_component_selection(world, &event);
386            let cursor_host = ensure_workspace_cursor_host(world, &workspace).or(Some(editor_root));
387            sync_editor_cursor_visual(world, _emit, &state, cursor_host);
388        },
389    );
390}
391
392fn ensure_workspace_cursor_host(
393    world: &mut World,
394    workspace: &Arc<Mutex<EditorContextWorkspaceState>>,
395) -> Option<ComponentId> {
396    let mut workspace = workspace.lock().expect("editor context workspace poisoned");
397
398    if workspace.panel_query_root.is_none() {
399        return None;
400    }
401
402    if let Some(existing) = workspace.cursor_host_root
403        && world
404            .get_component_by_id_as::<TransformComponent>(existing)
405            .is_some()
406    {
407        return Some(existing);
408    }
409
410    let cursor_host_root = ensure_shared_workspace_cursor_host(world, workspace.panel_query_root)?;
411    workspace.cursor_host_root = Some(cursor_host_root);
412    Some(cursor_host_root)
413}
414
415pub(crate) fn ensure_workspace_gizmo_host(
416    world: &mut World,
417    workspace: &Arc<Mutex<EditorContextWorkspaceState>>,
418) -> Option<ComponentId> {
419    let mut workspace = workspace.lock().expect("editor context workspace poisoned");
420    if workspace.panel_query_root.is_none() {
421        return None;
422    }
423    if let Some(existing) = workspace.gizmo_host_root
424        && world
425            .get_component_by_id_as::<TransformComponent>(existing)
426            .is_some()
427    {
428        return Some(existing);
429    }
430    let gizmo_host_root = world.add_component_boxed_named(
431        "editor_workspace_gizmo_root",
432        Box::new(TransformComponent::new()),
433    );
434    let selectable = world.add_component_boxed_named(
435        "editor_workspace_gizmo_root_selectable",
436        Box::new(SelectableComponent::off()),
437    );
438    let serialize = world.add_component_boxed_named(
439        "editor_workspace_gizmo_root_serialize",
440        Box::new(SerializeComponent::off()),
441    );
442    let _ = world.add_child(gizmo_host_root, selectable);
443    let _ = world.add_child(gizmo_host_root, serialize);
444    workspace.gizmo_host_root = Some(gizmo_host_root);
445    Some(gizmo_host_root)
446}
447
448pub(crate) fn ensure_shared_workspace_transform_gizmo(
449    world: &mut World,
450    emit: &mut dyn SignalEmitter,
451    workspace: &Arc<Mutex<EditorContextWorkspaceState>>,
452) -> Option<ComponentId> {
453    {
454        let workspace = workspace.lock().expect("editor context workspace poisoned");
455        if let Some(gizmo) = workspace.shared_transform_gizmo
456            && world
457                .get_component_by_id_as::<crate::engine::ecs::component::TransformGizmoComponent>(
458                    gizmo,
459                )
460                .is_some()
461        {
462            return Some(gizmo);
463        }
464    }
465
466    let host = ensure_workspace_gizmo_host(world, workspace)?;
467    let anchor = world.add_component_boxed_named(
468        "editor_workspace_gizmo_anchor",
469        Box::new(TransformComponent::new()),
470    );
471    let selectable = world.add_component_boxed_named(
472        "editor_workspace_gizmo_anchor_selectable",
473        Box::new(SelectableComponent::off()),
474    );
475    let serialize = world.add_component_boxed_named(
476        "editor_workspace_gizmo_anchor_serialize",
477        Box::new(SerializeComponent::off()),
478    );
479    let gizmo = world.add_component_boxed_named(
480        "editor_transform_gizmo",
481        Box::new(crate::engine::ecs::component::TransformGizmoComponent::new().with_scale(0.5)),
482    );
483    let _ = world.add_child(host, anchor);
484    let _ = world.add_child(anchor, selectable);
485    let _ = world.add_child(anchor, serialize);
486    let _ = world.add_child(anchor, gizmo);
487    world.init_component_tree(anchor, emit);
488    if let Ok(mut guard) = workspace.lock() {
489        guard.shared_transform_gizmo = Some(gizmo);
490    }
491    Some(gizmo)
492}
493
494pub(crate) fn ensure_shared_workspace_transform_gizmo_global(
495    world: &mut World,
496    emit: &mut dyn SignalEmitter,
497) -> Option<ComponentId> {
498    if let Some(existing) = world.all_components().find(|&component_id| {
499        world
500            .get_component_by_id_as::<crate::engine::ecs::component::TransformGizmoComponent>(
501                component_id,
502            )
503            .is_some()
504            && world.component_label(component_id) == Some("editor_transform_gizmo")
505    }) {
506        return Some(existing);
507    }
508
509    let host = if let Some(existing) = world.all_components().find(|&component_id| {
510        world.parent_of(component_id).is_none()
511            && world.component_label(component_id) == Some("editor_workspace_gizmo_root")
512    }) {
513        existing
514    } else {
515        let host = world.add_component_boxed_named(
516            "editor_workspace_gizmo_root",
517            Box::new(TransformComponent::new()),
518        );
519        let selectable = world.add_component_boxed_named(
520            "editor_workspace_gizmo_root_selectable",
521            Box::new(SelectableComponent::off()),
522        );
523        let serialize = world.add_component_boxed_named(
524            "editor_workspace_gizmo_root_serialize",
525            Box::new(SerializeComponent::off()),
526        );
527        let _ = world.add_child(host, selectable);
528        let _ = world.add_child(host, serialize);
529        host
530    };
531
532    let anchor = world.add_component_boxed_named(
533        "editor_workspace_gizmo_anchor",
534        Box::new(TransformComponent::new()),
535    );
536    let selectable = world.add_component_boxed_named(
537        "editor_workspace_gizmo_anchor_selectable",
538        Box::new(SelectableComponent::off()),
539    );
540    let serialize = world.add_component_boxed_named(
541        "editor_workspace_gizmo_anchor_serialize",
542        Box::new(SerializeComponent::off()),
543    );
544    let gizmo = world.add_component_boxed_named(
545        "editor_transform_gizmo",
546        Box::new(crate::engine::ecs::component::TransformGizmoComponent::new().with_scale(0.5)),
547    );
548    let _ = world.add_child(host, anchor);
549    let _ = world.add_child(anchor, selectable);
550    let _ = world.add_child(anchor, serialize);
551    let _ = world.add_child(anchor, gizmo);
552    world.init_component_tree(anchor, emit);
553    Some(gizmo)
554}
555
556fn ensure_editor_observer_router(world: &mut World, editor_root: ComponentId) -> ComponentId {
557    if let Some(existing) = world
558        .children_of(editor_root)
559        .iter()
560        .copied()
561        .find(|&child| {
562            world
563                .get_component_by_id_as::<SignalObserverRouterComponent>(child)
564                .is_some()
565        })
566    {
567        return existing;
568    }
569
570    let router = world.add_component_boxed_named(
571        "editor_signal_observer_router",
572        Box::new(SignalObserverRouterComponent::new()),
573    );
574    let _ = world.add_child(editor_root, router);
575    router
576}
577
578fn bootstrap_editor_context(
579    world: &World,
580    panel_query_root: ComponentId,
581    state: &Arc<Mutex<EditorContextState>>,
582    workspace: &Arc<Mutex<EditorContextWorkspaceState>>,
583) {
584    if let Some(selection_root) =
585        world.find_component(panel_query_root, PANEL_LAYOUT_SELECTION_SELECTOR)
586        && let Some(selection) = world.get_component_by_id_as::<SelectionComponent>(selection_root)
587    {
588        apply_editor_context_event(
589            state,
590            &EditorContextEvent::PanelFocusChanged {
591                focused_panel: selection.selected_component,
592            },
593        );
594    }
595
596    if let Some(selection_root) =
597        world.find_component(panel_query_root, WORLD_PANEL_SELECTION_SELECTOR)
598        && let Some(selection) = world.get_component_by_id_as::<SelectionComponent>(selection_root)
599    {
600        if let Some(event) = world_panel_selection_event(world, selection) {
601            apply_editor_context_event(state, &event);
602        }
603    } else if let Some(editor_root) = workspace
604        .lock()
605        .expect("editor context workspace poisoned")
606        .default_active_editor()
607    {
608        apply_editor_context_event(
609            state,
610            &EditorContextEvent::ActiveEditorChanged {
611                editor: Some(editor_root),
612                selected_component: None,
613                interaction_mode: editor_interaction_mode(world, Some(editor_root)),
614            },
615        );
616    }
617
618    if let Some(selection_root) = world.find_component(panel_query_root, ASSETS_SELECTION_SELECTOR)
619        && let Some(selection) = world.get_component_by_id_as::<SelectionComponent>(selection_root)
620    {
621        apply_editor_context_event(
622            state,
623            &EditorContextEvent::AssetPanelSelectionChanged {
624                asset_payload: selection.selected_payload.or(selection.selected_component),
625            },
626        );
627    }
628
629    if let Some(selection_root) =
630        world.find_component(panel_query_root, GRID_PANEL_SELECTION_SELECTOR)
631        && let Some(selection) = world.get_component_by_id_as::<SelectionComponent>(selection_root)
632    {
633        if let Some(event) = grid_panel_selection_event(world, selection, panel_query_root) {
634            apply_editor_context_event(state, &event);
635        }
636    }
637}
638
639fn editor_context_event_from_shared_signal(
640    world: &World,
641    panel_query_root: ComponentId,
642    signal: &Signal,
643    fallback_editor: Option<ComponentId>,
644) -> Option<EditorContextEvent> {
645    let EventSignal::SelectionChanged {
646        selection_root,
647        selected_entries,
648        selected_component,
649        selected_payload,
650        ..
651    } = signal.event.as_ref()?
652    else {
653        return None;
654    };
655
656    let component =
657        selected_component.or_else(|| selected_entries.last().map(|entry| entry.component));
658    let is_panel_layout_selection = world.component_label(*selection_root)
659        == Some(PANEL_LAYOUT_SELECTION_SELECTOR.trim_start_matches('#'))
660        || world.find_component(panel_query_root, PANEL_LAYOUT_SELECTION_SELECTOR)
661            == Some(*selection_root);
662    let is_world_panel_selection = world.component_label(*selection_root)
663        == Some(WORLD_PANEL_SELECTION_SELECTOR.trim_start_matches('#'))
664        || world.find_component(panel_query_root, WORLD_PANEL_SELECTION_SELECTOR)
665            == Some(*selection_root);
666    let is_assets_selection = world.component_label(*selection_root)
667        == Some(ASSETS_SELECTION_SELECTOR.trim_start_matches('#'))
668        || world.find_component(panel_query_root, ASSETS_SELECTION_SELECTOR)
669            == Some(*selection_root);
670    let is_grid_panel_selection = world.component_label(*selection_root)
671        == Some(GRID_PANEL_SELECTION_SELECTOR.trim_start_matches('#'))
672        || world.find_component(panel_query_root, GRID_PANEL_SELECTION_SELECTOR)
673            == Some(*selection_root);
674    let is_editor_settings_selection = world.component_label(*selection_root)
675        == Some(EDITOR_SETTINGS_SELECTION_SELECTOR.trim_start_matches('#'))
676        || world.find_component(panel_query_root, EDITOR_SETTINGS_SELECTION_SELECTOR)
677            == Some(*selection_root);
678
679    if is_panel_layout_selection {
680        Some(EditorContextEvent::PanelFocusChanged {
681            focused_panel: component,
682        })
683    } else if is_editor_settings_selection {
684        let option = selected_payload
685            .or(component)
686            .and_then(|payload| editor_settings_option_from_payload(world, payload))?;
687        let active_editor =
688            current_or_default_editor_root(world, panel_query_root, component, fallback_editor);
689        eprintln!(
690            "\n\n⚙️🧪📝 editor settings selection selection_root={selection_root:?} component={component:?} payload={selected_payload:?} option={option:?} active_editor={active_editor:?}\n"
691        );
692        Some(EditorContextEvent::InteractionModeChanged {
693            editor: active_editor,
694            interaction_mode: option.interaction_mode(),
695        })
696    } else if is_world_panel_selection {
697        let semantic_target =
698            resolve_semantic_target_from_payload(world, *selected_payload, component);
699        let active_editor =
700            semantic_target.and_then(|target| nearest_editor_ancestor(world, target));
701        println!(
702            "\n\n[EditorContext][trace] world_panel selection_root={selection_root:?} clicked_row={selected_component:?} payload={selected_payload:?} authored_target={semantic_target:?} active_editor={:?}\n",
703            active_editor
704        );
705        Some(EditorContextEvent::WorldPanelSelectionChanged {
706            component: semantic_target,
707            editor: active_editor,
708            interaction_mode: editor_interaction_mode(world, active_editor),
709        })
710    } else if is_assets_selection {
711        Some(EditorContextEvent::AssetPanelSelectionChanged {
712            asset_payload: selected_payload.or(component),
713        })
714    } else if is_grid_panel_selection {
715        grid_panel_selection_event_from_payload(
716            world,
717            selected_payload.or(component),
718            panel_query_root,
719            fallback_editor,
720        )
721    } else {
722        None
723    }
724}
725
726fn apply_editor_context_event(state: &Arc<Mutex<EditorContextState>>, event: &EditorContextEvent) {
727    let mut state = state.lock().expect("editor context state poisoned");
728    *state = reduce_editor_context_state(&state, event);
729}
730
731fn emit_editor_workspace_data_event(
732    world: &World,
733    emit: &mut dyn SignalEmitter,
734    panel_query_root: ComponentId,
735    event: &EditorContextEvent,
736) {
737    let Some(runtime_ui_root) = world
738        .all_components()
739        .find(|&component_id| {
740            world.parent_of(component_id).is_none()
741                && world.component_label(component_id) == Some("editor_runtime_ui_root")
742        })
743        .or(Some(panel_query_root))
744    else {
745        return;
746    };
747
748    if let EditorContextEvent::AssetPanelSelectionChanged { asset_payload } = event {
749        emit.push_event(
750            runtime_ui_root,
751            EventSignal::DataEvent {
752                name: EDITOR_WORKSPACE_ASSET_SELECTION_CHANGED.to_string(),
753                payload: *asset_payload,
754            },
755        );
756    }
757}
758
759fn editor_interaction_mode(
760    world: &World,
761    editor_root: Option<ComponentId>,
762) -> EditorInteractionMode {
763    editor_root
764        .and_then(|editor_root| {
765            world
766                .get_component_by_id_as::<EditorComponent>(editor_root)
767                .map(|editor| editor.interaction_mode)
768        })
769        .unwrap_or(EditorInteractionMode::Select)
770}
771
772fn current_or_default_editor_root(
773    world: &World,
774    panel_query_root: ComponentId,
775    component: Option<ComponentId>,
776    fallback_editor: Option<ComponentId>,
777) -> Option<ComponentId> {
778    component
779        .and_then(|component| nearest_editor_ancestor(world, component))
780        .or(fallback_editor)
781        .or_else(|| {
782            world
783                .find_component(panel_query_root, WORLD_PANEL_SELECTION_SELECTOR)
784                .and_then(|selection_root| {
785                    world
786                        .get_component_by_id_as::<SelectionComponent>(selection_root)
787                        .and_then(|selection| world_panel_selection_event(world, selection))
788                        .and_then(|event| match event {
789                            EditorContextEvent::WorldPanelSelectionChanged { editor, .. } => editor,
790                            _ => None,
791                        })
792                })
793        })
794}
795
796fn editor_settings_option_from_payload(
797    world: &World,
798    payload_or_row: ComponentId,
799) -> Option<EditorSettingsOption> {
800    editor_settings_payload_data(world, payload_or_row).and_then(|data| {
801        match data.get("mode_value") {
802            Some(crate::engine::ecs::component::DataValue::Text(mode_value)) => {
803                EditorSettingsOption::from_mode_value(mode_value)
804            }
805            _ => None,
806        }
807    })
808}
809
810fn editor_settings_payload_data(
811    world: &World,
812    payload_or_row: ComponentId,
813) -> Option<&crate::engine::ecs::component::DataComponent> {
814    if let Some(data) =
815        world.get_component_by_id_as::<crate::engine::ecs::component::DataComponent>(payload_or_row)
816        && world.component_label(payload_or_row) == Some(EDITOR_SETTINGS_PAYLOAD_NAME)
817    {
818        return Some(data);
819    }
820
821    world.children_of(payload_or_row).iter().find_map(|&child| {
822        let data =
823            world.get_component_by_id_as::<crate::engine::ecs::component::DataComponent>(child)?;
824        if world.component_label(child) == Some(EDITOR_SETTINGS_PAYLOAD_NAME) {
825            Some(data)
826        } else {
827            None
828        }
829    })
830}
831
832pub(crate) fn sync_editor_cursor_visual(
833    world: &mut World,
834    emit: &mut dyn SignalEmitter,
835    state: &Arc<Mutex<EditorContextState>>,
836    cursor_root_host: Option<ComponentId>,
837) {
838    let state = state.lock().expect("editor context state poisoned").clone();
839    // Do not materialize the marker during editor bootstrap. Bootstrap currently
840    // uses a no-op emitter, so creating the component tree there would discard
841    // the registration intents emitted by `init_component_tree`. The first real
842    // cursor placement supplies both a pose and a live emitter.
843    let (Some(translation), Some(rotation)) = (state.cursor_translation, state.cursor_rotation)
844    else {
845        return;
846    };
847    let Some(cursor_root_host) = cursor_root_host.or(state.active_editor) else {
848        return;
849    };
850    let marker = ensure_cursor_marker(world, emit, cursor_root_host);
851    let Some(marker_transform) = world.get_component_by_id_as_mut::<TransformComponent>(marker)
852    else {
853        return;
854    };
855
856    marker_transform.transform.translation = translation;
857    marker_transform.transform.rotation = rotation;
858    marker_transform.transform.scale = [CURSOR_MARKER_SIZE, CURSOR_MARKER_SIZE, CURSOR_MARKER_SIZE];
859    marker_transform.transform.recompute_model();
860
861    emit.push_intent_now(
862        marker,
863        IntentValue::UpdateTransform {
864            component_ids: vec![marker],
865            translation,
866            rotation_quat_xyzw: rotation,
867            scale: [CURSOR_MARKER_SIZE, CURSOR_MARKER_SIZE, CURSOR_MARKER_SIZE],
868        },
869    );
870
871    let opacity_ids = cursor_marker_opacities(world, marker);
872    let target_opacity = CURSOR_MARKER_OPACITY;
873    for opacity_id in &opacity_ids {
874        if let Some(opacity) = world.get_component_by_id_as_mut::<OpacityComponent>(*opacity_id) {
875            opacity.opacity = target_opacity;
876        }
877    }
878    if !opacity_ids.is_empty() {
879        emit.push_intent_now(
880            marker,
881            IntentValue::RegisterOpacity {
882                component_ids: opacity_ids,
883            },
884        );
885    }
886}
887
888pub(crate) fn ensure_shared_workspace_cursor_host(
889    world: &mut World,
890    panel_query_root: Option<ComponentId>,
891) -> Option<ComponentId> {
892    let panel_query_root = panel_query_root?;
893
894    if let Some(existing) = world.all_components().find(|&component_id| {
895        world.parent_of(component_id).is_none()
896            && world.component_label(component_id) == Some("editor_workspace_cursor_root")
897    }) {
898        return Some(existing);
899    }
900
901    let cursor_host_root = world.add_component_boxed_named(
902        "editor_workspace_cursor_root",
903        Box::new(TransformComponent::new()),
904    );
905    let cursor_host_selectable = world.add_component_boxed_named(
906        "editor_workspace_cursor_root_selectable",
907        Box::new(SelectableComponent::off()),
908    );
909    let cursor_host_serialize = world.add_component_boxed_named(
910        "editor_workspace_cursor_root_serialize",
911        Box::new(SerializeComponent::off()),
912    );
913    let _ = world.add_child(cursor_host_root, cursor_host_selectable);
914    let _ = world.add_child(cursor_host_root, cursor_host_serialize);
915
916    let _ = panel_query_root;
917    Some(cursor_host_root)
918}
919
920fn ensure_cursor_marker(
921    world: &mut World,
922    emit: &mut dyn SignalEmitter,
923    cursor_root_host: ComponentId,
924) -> ComponentId {
925    if let Some(existing) = world
926        .children_of(cursor_root_host)
927        .iter()
928        .copied()
929        .find(|&child| world.component_label(child) == Some(CURSOR_MARKER_ROOT_NAME))
930    {
931        return existing;
932    }
933
934    let marker_root = world.add_component_boxed_named(
935        CURSOR_MARKER_ROOT_NAME,
936        Box::new(TransformComponent::new().with_scale(
937            CURSOR_MARKER_SIZE,
938            CURSOR_MARKER_SIZE,
939            CURSOR_MARKER_SIZE,
940        )),
941    );
942    let marker_selectable = world.add_component_boxed_named(
943        "editor_cursor_marker_selectable",
944        Box::new(SelectableComponent::off()),
945    );
946    let marker_serialize = world.add_component_boxed_named(
947        "editor_cursor_marker_serialize",
948        Box::new(SerializeComponent::off()),
949    );
950    let _ = world.add_child(cursor_root_host, marker_root);
951    let _ = world.add_child(marker_root, marker_selectable);
952    let _ = world.add_child(marker_root, marker_serialize);
953
954    let half_extent = CURSOR_MARKER_SIZE * 0.5;
955    let plane_scale = CURSOR_MARKER_SIZE;
956    let plane_roots = [
957        (
958            "editor_cursor_marker_x_plane_root",
959            TransformComponent::new()
960                .with_position(half_extent, 0.0, 0.0)
961                .with_rotation_euler(0.0, FRAC_PI_2, 0.0)
962                .with_scale(plane_scale, plane_scale, plane_scale),
963            [0.0, 0.0, 1.0, 1.0],
964        ),
965        (
966            "editor_cursor_marker_y_plane_root",
967            TransformComponent::new()
968                .with_position(0.0, half_extent, 0.0)
969                .with_rotation_euler(-FRAC_PI_2, 0.0, 0.0)
970                .with_scale(plane_scale, plane_scale, plane_scale),
971            [0.0, 1.0, 0.0, 1.0],
972        ),
973        (
974            "editor_cursor_marker_z_plane_root",
975            TransformComponent::new()
976                .with_position(0.0, 0.0, half_extent)
977                .with_scale(plane_scale, plane_scale, plane_scale),
978            [1.0, 0.0, 0.0, 1.0],
979        ),
980    ];
981
982    let mut renderable_ids = Vec::new();
983    for (name, transform, color) in plane_roots {
984        let plane_root = world.add_component_boxed_named(name, Box::new(transform));
985        let plane_renderable = world.add_component_boxed_named(
986            &format!("{name}_renderable"),
987            Box::new(RenderableComponent::from_cpu_mesh_handle(
988                CpuMeshHandle::QUAD_2D,
989                MaterialHandle::TOON_MESH,
990            )),
991        );
992        let plane_raycastable = world.add_component_boxed_named(
993            &format!("{name}_raycastable"),
994            Box::new(RaycastableComponent::disabled()),
995        );
996        let plane_color = world.add_component_boxed_named(
997            &format!("{name}_color"),
998            Box::new(ColorComponent::rgba(color[0], color[1], color[2], color[3])),
999        );
1000        let plane_opacity = world.add_component_boxed_named(
1001            &format!("{name}_opacity"),
1002            Box::new(OpacityComponent::new().with_opacity(CURSOR_MARKER_OPACITY)),
1003        );
1004        let plane_emissive = world.add_component_boxed_named(
1005            &format!("{name}_emissive"),
1006            Box::new(EmissiveComponent::new(1.0)),
1007        );
1008
1009        let _ = world.add_child(marker_root, plane_root);
1010        let _ = world.add_child(plane_root, plane_renderable);
1011        let _ = world.add_child(plane_renderable, plane_raycastable);
1012        let _ = world.add_child(plane_renderable, plane_color);
1013        let _ = world.add_child(plane_renderable, plane_opacity);
1014        let _ = world.add_child(plane_renderable, plane_emissive);
1015        renderable_ids.push(plane_renderable);
1016    }
1017
1018    world.init_component_tree(marker_root, emit);
1019    emit.push_intent_now(
1020        marker_root,
1021        IntentValue::RegisterTransform {
1022            component_ids: vec![marker_root],
1023        },
1024    );
1025    emit.push_intent_now(
1026        marker_root,
1027        IntentValue::RegisterRenderable {
1028            component_ids: renderable_ids,
1029        },
1030    );
1031    marker_root
1032}
1033
1034fn cursor_marker_opacities(world: &World, marker_root: ComponentId) -> Vec<ComponentId> {
1035    let mut opacities = Vec::new();
1036    for &child in world.children_of(marker_root) {
1037        for &grandchild in world.children_of(child) {
1038            if world
1039                .get_component_by_id_as::<RenderableComponent>(grandchild)
1040                .is_some()
1041            {
1042                for &style_child in world.children_of(grandchild) {
1043                    if world
1044                        .get_component_by_id_as::<OpacityComponent>(style_child)
1045                        .is_some()
1046                    {
1047                        opacities.push(style_child);
1048                    }
1049                }
1050            }
1051        }
1052    }
1053    opacities
1054}
1055
1056fn sync_editor_observer_routes(
1057    world: &mut World,
1058    state: &Arc<Mutex<EditorContextState>>,
1059    workspace: &Arc<Mutex<EditorContextWorkspaceState>>,
1060) {
1061    let editor_context = state.lock().expect("editor context state poisoned").clone();
1062    let workspace = workspace
1063        .lock()
1064        .expect("editor context workspace poisoned")
1065        .clone();
1066    let paint_panel_root = workspace.panel_query_root.and_then(|panel_query_root| {
1067        world.find_component(panel_query_root, PAINT_PANEL_ROOT_SELECTOR)
1068    });
1069    let color_panel_root = workspace.panel_query_root.and_then(|panel_query_root| {
1070        world.find_component(panel_query_root, COLOR_PANEL_ROOT_SELECTOR)
1071    });
1072    let paint_focused = paint_panel_root
1073        .is_some_and(|panel| editor_context.focused_panel == Some(panel))
1074        || color_panel_root.is_some_and(|panel| editor_context.focused_panel == Some(panel));
1075
1076    for editor_root in workspace.registered_editors {
1077        let router_id = ensure_editor_observer_router(world, editor_root);
1078        let interaction_mode = editor_context.interaction_mode;
1079        let Some(router) =
1080            world.get_component_by_id_as_mut::<SignalObserverRouterComponent>(router_id)
1081        else {
1082            continue;
1083        };
1084
1085        if paint_focused {
1086            router
1087                .blacklist
1088                .retain(|name| name != PAINT_SYSTEM_HANDLER_NAME);
1089        } else if !router
1090            .blacklist
1091            .iter()
1092            .any(|name| name == PAINT_SYSTEM_HANDLER_NAME)
1093        {
1094            router.blacklist.push(PAINT_SYSTEM_HANDLER_NAME.to_string());
1095        }
1096
1097        router.blacklist.retain(|name| {
1098            name != EDITOR_SELECT_HANDLER_NAME && name != EDITOR_CURSOR_HANDLER_NAME
1099        });
1100        match interaction_mode {
1101            EditorInteractionMode::Select => {
1102                router
1103                    .blacklist
1104                    .push(EDITOR_CURSOR_HANDLER_NAME.to_string());
1105            }
1106            EditorInteractionMode::Cursor3d => {
1107                router
1108                    .blacklist
1109                    .push(EDITOR_SELECT_HANDLER_NAME.to_string());
1110            }
1111            EditorInteractionMode::SelectAndCursor => {}
1112        }
1113
1114        if DEBUG_BLACKLIST_EDITOR_PANEL_REFRESH {
1115            if !router
1116                .blacklist
1117                .iter()
1118                .any(|name| name == EDITOR_PANEL_REFRESH_HANDLER_NAME)
1119            {
1120                router
1121                    .blacklist
1122                    .push(EDITOR_PANEL_REFRESH_HANDLER_NAME.to_string());
1123            }
1124        } else {
1125            router
1126                .blacklist
1127                .retain(|name| name != EDITOR_PANEL_REFRESH_HANDLER_NAME);
1128        }
1129    }
1130}
1131
1132fn ensure_default_active_editor(
1133    state: &Arc<Mutex<EditorContextState>>,
1134    workspace: &Arc<Mutex<EditorContextWorkspaceState>>,
1135) {
1136    let default_editor = workspace
1137        .lock()
1138        .expect("editor context workspace poisoned")
1139        .default_active_editor();
1140    let Some(default_editor) = default_editor else {
1141        return;
1142    };
1143
1144    let mut state = state.lock().expect("editor context state poisoned");
1145    if state.active_editor.is_none() {
1146        *state = reduce_editor_context_state(
1147            &state,
1148            &EditorContextEvent::ActiveEditorChanged {
1149                editor: Some(default_editor),
1150                selected_component: None,
1151                interaction_mode: EditorInteractionMode::Select,
1152            },
1153        );
1154    }
1155}
1156
1157fn nearest_editor_ancestor(world: &World, start: ComponentId) -> Option<ComponentId> {
1158    let mut cur = Some(start);
1159    while let Some(node) = cur {
1160        if world
1161            .get_component_by_id_as::<crate::engine::ecs::component::EditorComponent>(node)
1162            .is_some()
1163        {
1164            return Some(node);
1165        }
1166        cur = world.parent_of(node);
1167    }
1168    None
1169}
1170
1171fn nearest_transform_ancestor(world: &World, start: ComponentId) -> Option<ComponentId> {
1172    let mut cur = Some(start);
1173    while let Some(node) = cur {
1174        if world
1175            .get_component_by_id_as::<TransformComponent>(node)
1176            .is_some()
1177        {
1178            return Some(node);
1179        }
1180        cur = world.parent_of(node);
1181    }
1182    None
1183}
1184
1185pub(crate) fn apply_semantic_target_selection(
1186    world: &mut World,
1187    emit: &mut dyn SignalEmitter,
1188    state: &Arc<Mutex<EditorContextState>>,
1189    target_component: ComponentId,
1190    update_repl_cwd: bool,
1191) -> SemanticEditorSelectionResult {
1192    let active_editor = nearest_editor_ancestor(world, target_component);
1193    let gizmo_target = nearest_transform_ancestor(world, target_component);
1194    let is_editor_root_target = active_editor == Some(target_component);
1195    let used_editor_selection_path = if is_editor_root_target {
1196        false
1197    } else if let Some((editor_root, transform)) = active_editor.zip(gizmo_target) {
1198        select_editor_target(world, emit, editor_root, transform, update_repl_cwd);
1199        true
1200    } else {
1201        false
1202    };
1203
1204    {
1205        let mut editor_context = state.lock().expect("editor context state poisoned");
1206        editor_context.selected_component = Some(target_component);
1207        if active_editor.is_some() {
1208            editor_context.active_editor = active_editor;
1209        }
1210    }
1211
1212    SemanticEditorSelectionResult {
1213        target_component,
1214        active_editor,
1215        gizmo_target,
1216        used_editor_selection_path,
1217    }
1218}
1219
1220pub(crate) fn apply_editor_root_selection(
1221    world: &mut World,
1222    state: &Arc<Mutex<EditorContextState>>,
1223    editor_root: ComponentId,
1224) {
1225    let interaction_mode = editor_interaction_mode(world, Some(editor_root));
1226
1227    {
1228        let mut editor_context = state.lock().expect("editor context state poisoned");
1229        editor_context.active_editor = Some(editor_root);
1230        editor_context.selected_component = Some(editor_root);
1231        editor_context.interaction_mode = interaction_mode;
1232    }
1233
1234    if let Some(editor_component) = world.get_component_by_id_as_mut::<EditorComponent>(editor_root)
1235    {
1236        editor_component.selected = Some(editor_root);
1237        editor_component.interaction_mode = interaction_mode;
1238    }
1239}
1240
1241fn world_panel_selection_event(
1242    world: &World,
1243    selection: &SelectionComponent,
1244) -> Option<EditorContextEvent> {
1245    let semantic_target = resolve_semantic_target_from_payload(
1246        world,
1247        selection.selected_payload,
1248        selection.selected_component,
1249    )?;
1250    Some(EditorContextEvent::WorldPanelSelectionChanged {
1251        component: Some(semantic_target),
1252        editor: nearest_editor_ancestor(world, semantic_target),
1253        interaction_mode: editor_interaction_mode(
1254            world,
1255            nearest_editor_ancestor(world, semantic_target),
1256        ),
1257    })
1258}
1259
1260fn grid_panel_selection_event(
1261    world: &World,
1262    selection: &SelectionComponent,
1263    panel_query_root: ComponentId,
1264) -> Option<EditorContextEvent> {
1265    grid_panel_selection_event_from_payload(
1266        world,
1267        selection.selected_payload.or(selection.selected_component),
1268        panel_query_root,
1269        None,
1270    )
1271}
1272
1273fn grid_panel_selection_event_from_payload(
1274    world: &World,
1275    payload_or_row: Option<ComponentId>,
1276    panel_query_root: ComponentId,
1277    fallback_editor: Option<ComponentId>,
1278) -> Option<EditorContextEvent> {
1279    let owner_transform = payload_or_row.and_then(|payload| {
1280        world
1281            .get_component_by_id_as::<crate::engine::ecs::component::DataComponent>(payload)
1282            .and_then(|data| data.get_component("owner_transform"))
1283            .or_else(|| resolve_semantic_target_from_payload(world, Some(payload), Some(payload)))
1284    });
1285    Some(EditorContextEvent::GridPanelSelectionChanged {
1286        active_grid_owner_transform: owner_transform,
1287        editor: current_or_default_editor_root(
1288            world,
1289            panel_query_root,
1290            owner_transform,
1291            fallback_editor,
1292        ),
1293    })
1294}
1295
1296fn sync_editor_component_selection(world: &mut World, event: &EditorContextEvent) {
1297    match event {
1298        EditorContextEvent::WorldPanelSelectionChanged {
1299            component,
1300            editor,
1301            interaction_mode,
1302        } => {
1303            let Some(editor_root) = *editor else {
1304                return;
1305            };
1306            if let Some(editor_component) =
1307                world.get_component_by_id_as_mut::<EditorComponent>(editor_root)
1308            {
1309                editor_component.selected = *component;
1310                editor_component.interaction_mode = *interaction_mode;
1311            }
1312        }
1313        EditorContextEvent::EditorSelectionChanged {
1314            editor,
1315            component,
1316            interaction_mode,
1317        } => {
1318            if let Some(editor_component) =
1319                world.get_component_by_id_as_mut::<EditorComponent>(*editor)
1320            {
1321                editor_component.selected = component.or(Some(*editor));
1322                editor_component.interaction_mode = *interaction_mode;
1323            }
1324        }
1325        EditorContextEvent::ActiveEditorChanged {
1326            editor,
1327            selected_component,
1328            interaction_mode,
1329        } => {
1330            let Some(editor_root) = *editor else {
1331                return;
1332            };
1333            if let Some(editor_component) =
1334                world.get_component_by_id_as_mut::<EditorComponent>(editor_root)
1335            {
1336                editor_component.selected = *selected_component;
1337                editor_component.interaction_mode = *interaction_mode;
1338            }
1339        }
1340        EditorContextEvent::AssetPanelSelectionChanged { .. } => {}
1341        EditorContextEvent::GridPanelSelectionChanged { .. } => {}
1342        EditorContextEvent::InteractionModeChanged {
1343            editor,
1344            interaction_mode,
1345        } => {
1346            let _ = editor;
1347            for editor_root in world.all_components().collect::<Vec<_>>() {
1348                let Some(editor_component) =
1349                    world.get_component_by_id_as_mut::<EditorComponent>(editor_root)
1350                else {
1351                    continue;
1352                };
1353                eprintln!(
1354                    "🛠️🎚️📌 sync_editor_component_selection interaction_mode_change editor_root={editor_root:?} old_mode={:?} new_mode={interaction_mode:?}",
1355                    editor_component.interaction_mode
1356                );
1357                editor_component.interaction_mode = *interaction_mode;
1358            }
1359        }
1360        EditorContextEvent::PanelFocusChanged { .. } => {}
1361    }
1362}
1363
1364fn sync_global_editor_interaction_mode(world: &mut World, state: &Arc<Mutex<EditorContextState>>) {
1365    let interaction_mode = state
1366        .lock()
1367        .expect("editor context state poisoned")
1368        .interaction_mode;
1369
1370    for editor_root in world.all_components().collect::<Vec<_>>() {
1371        let Some(editor_component) =
1372            world.get_component_by_id_as_mut::<EditorComponent>(editor_root)
1373        else {
1374            continue;
1375        };
1376        editor_component.interaction_mode = interaction_mode;
1377    }
1378}
1379
1380#[cfg(test)]
1381mod tests {
1382    use super::{
1383        EDITOR_CURSOR_HANDLER_NAME, EDITOR_SELECT_HANDLER_NAME, EditorContextEvent,
1384        EditorContextState, EditorContextWorkspaceState, NullEmit, apply_editor_root_selection,
1385        apply_semantic_target_selection, editor_context_event_from_shared_signal,
1386        ensure_editor_observer_router, reduce_editor_context_state,
1387        sync_editor_component_selection, sync_editor_observer_routes, world_panel_selection_event,
1388    };
1389    use crate::engine::ecs::World;
1390    use crate::engine::ecs::component::{
1391        DataComponent, DataValue, EditorComponent, EditorInteractionMode, SelectionComponent,
1392        SignalObserverRouterComponent, TransformComponent,
1393    };
1394    use crate::engine::ecs::{EventSignal, Signal};
1395    use std::sync::{Arc, Mutex};
1396
1397    fn cid(world: &mut World) -> crate::engine::ecs::ComponentId {
1398        world.add_component_boxed(Box::new(TransformComponent::new()))
1399    }
1400
1401    #[test]
1402    fn defaults_to_first_editor_root_selection() {
1403        let mut world = World::default();
1404        let editor = cid(&mut world);
1405        let next = reduce_editor_context_state(
1406            &EditorContextState::default(),
1407            &EditorContextEvent::ActiveEditorChanged {
1408                editor: Some(editor),
1409                selected_component: Some(editor),
1410                interaction_mode: EditorInteractionMode::Select,
1411            },
1412        );
1413
1414        assert_eq!(next.active_editor, Some(editor));
1415        assert_eq!(next.selected_component, Some(editor));
1416    }
1417
1418    #[test]
1419    fn semantic_target_selection_derives_editor_from_target_ancestry() {
1420        let mut world = World::default();
1421        let editor = world.add_component_boxed(Box::new(EditorComponent::new()));
1422        let target = world.add_component_boxed(Box::new(TransformComponent::new()));
1423        let renderable = world.add_component_boxed(Box::new(TransformComponent::new()));
1424        let _ = world.add_child(editor, target);
1425        let _ = world.add_child(target, renderable);
1426
1427        let state = Arc::new(Mutex::new(EditorContextState::default()));
1428        let mut emit = NullEmit;
1429        let result = apply_semantic_target_selection(&mut world, &mut emit, &state, target, false);
1430
1431        assert_eq!(result.active_editor, Some(editor));
1432        assert_eq!(result.gizmo_target, Some(target));
1433        assert!(result.used_editor_selection_path);
1434
1435        let state = state.lock().expect("state");
1436        assert_eq!(state.active_editor, Some(editor));
1437        assert_eq!(state.selected_component, Some(target));
1438    }
1439
1440    #[test]
1441    fn editor_root_selection_updates_shared_state_and_editor_component() {
1442        let mut world = World::default();
1443        let editor = world.add_component_boxed(Box::new(
1444            EditorComponent::new().with_interaction_mode(EditorInteractionMode::SelectAndCursor),
1445        ));
1446        let state = Arc::new(Mutex::new(EditorContextState::default()));
1447
1448        apply_editor_root_selection(&mut world, &state, editor);
1449
1450        let state = state.lock().expect("state");
1451        assert_eq!(state.active_editor, Some(editor));
1452        assert_eq!(state.selected_component, Some(editor));
1453        assert_eq!(
1454            state.interaction_mode,
1455            EditorInteractionMode::SelectAndCursor
1456        );
1457
1458        let editor_component = world
1459            .get_component_by_id_as::<EditorComponent>(editor)
1460            .expect("editor component");
1461        assert_eq!(editor_component.selected, Some(editor));
1462        assert_eq!(
1463            editor_component.interaction_mode,
1464            EditorInteractionMode::SelectAndCursor
1465        );
1466    }
1467
1468    #[test]
1469    fn world_panel_root_selection_switches_active_editor() {
1470        let mut world = World::default();
1471        let selected = cid(&mut world);
1472        let editor = cid(&mut world);
1473        let next = reduce_editor_context_state(
1474            &EditorContextState::default(),
1475            &EditorContextEvent::WorldPanelSelectionChanged {
1476                component: Some(selected),
1477                editor: Some(editor),
1478                interaction_mode: EditorInteractionMode::Select,
1479            },
1480        );
1481
1482        assert_eq!(next.active_editor, Some(editor));
1483        assert_eq!(next.selected_component, Some(selected));
1484    }
1485
1486    #[test]
1487    fn scene_selection_switches_editor_and_component_together() {
1488        let mut world = World::default();
1489        let editor = cid(&mut world);
1490        let selected = cid(&mut world);
1491        let next = reduce_editor_context_state(
1492            &EditorContextState::default(),
1493            &EditorContextEvent::EditorSelectionChanged {
1494                editor,
1495                component: Some(selected),
1496                interaction_mode: EditorInteractionMode::Select,
1497            },
1498        );
1499
1500        assert_eq!(next.active_editor, Some(editor));
1501        assert_eq!(next.selected_component, Some(selected));
1502    }
1503
1504    #[test]
1505    fn panel_focus_updates_independently() {
1506        let mut world = World::default();
1507        let panel = cid(&mut world);
1508        let next = reduce_editor_context_state(
1509            &EditorContextState::default(),
1510            &EditorContextEvent::PanelFocusChanged {
1511                focused_panel: Some(panel),
1512            },
1513        );
1514
1515        assert_eq!(next.focused_panel, Some(panel));
1516    }
1517
1518    #[test]
1519    fn interaction_mode_changes_without_clearing_selection() {
1520        let mut world = World::default();
1521        let editor = cid(&mut world);
1522        let selected = cid(&mut world);
1523        let next = reduce_editor_context_state(
1524            &EditorContextState {
1525                active_editor: Some(editor),
1526                selected_component: Some(selected),
1527                interaction_mode: EditorInteractionMode::Select,
1528                ..EditorContextState::default()
1529            },
1530            &EditorContextEvent::InteractionModeChanged {
1531                editor: Some(editor),
1532                interaction_mode: EditorInteractionMode::Cursor3d,
1533            },
1534        );
1535
1536        assert_eq!(next.active_editor, Some(editor));
1537        assert_eq!(next.selected_component, Some(selected));
1538        assert_eq!(next.interaction_mode, EditorInteractionMode::Cursor3d);
1539    }
1540
1541    #[test]
1542    fn interaction_mode_supports_select_and_cursor() {
1543        let mut world = World::default();
1544        let editor = cid(&mut world);
1545        let selected = cid(&mut world);
1546        let next = reduce_editor_context_state(
1547            &EditorContextState {
1548                active_editor: Some(editor),
1549                selected_component: Some(selected),
1550                interaction_mode: EditorInteractionMode::Select,
1551                ..EditorContextState::default()
1552            },
1553            &EditorContextEvent::InteractionModeChanged {
1554                editor: Some(editor),
1555                interaction_mode: EditorInteractionMode::SelectAndCursor,
1556            },
1557        );
1558
1559        assert_eq!(
1560            next.interaction_mode,
1561            EditorInteractionMode::SelectAndCursor
1562        );
1563        assert_eq!(next.selected_component, Some(selected));
1564    }
1565
1566    #[test]
1567    fn observer_routes_blacklist_handlers_by_interaction_mode() {
1568        let mut world = World::default();
1569        let editor_root =
1570            world.add_component_boxed_named("editor_root", Box::new(EditorComponent::new()));
1571        ensure_editor_observer_router(&mut world, editor_root);
1572        let state = Arc::new(Mutex::new(EditorContextState {
1573            active_editor: Some(editor_root),
1574            selected_component: Some(editor_root),
1575            interaction_mode: EditorInteractionMode::Select,
1576            ..EditorContextState::default()
1577        }));
1578        let workspace = Arc::new(Mutex::new(EditorContextWorkspaceState {
1579            panel_query_root: None,
1580            cursor_host_root: None,
1581            gizmo_host_root: None,
1582            shared_transform_gizmo: None,
1583            registered_editors: vec![editor_root],
1584        }));
1585
1586        sync_editor_observer_routes(&mut world, &state, &workspace);
1587        let router = world
1588            .children_of(editor_root)
1589            .iter()
1590            .find_map(|child| world.get_component_by_id_as::<SignalObserverRouterComponent>(*child))
1591            .expect("router");
1592        assert!(
1593            router
1594                .blacklist
1595                .iter()
1596                .any(|name| name == EDITOR_CURSOR_HANDLER_NAME)
1597        );
1598        assert!(
1599            !router
1600                .blacklist
1601                .iter()
1602                .any(|name| name == EDITOR_SELECT_HANDLER_NAME)
1603        );
1604
1605        if let Ok(mut guard) = state.lock() {
1606            guard.interaction_mode = EditorInteractionMode::Cursor3d;
1607        }
1608        sync_editor_observer_routes(&mut world, &state, &workspace);
1609        let router = world
1610            .children_of(editor_root)
1611            .iter()
1612            .find_map(|child| world.get_component_by_id_as::<SignalObserverRouterComponent>(*child))
1613            .expect("router");
1614        assert!(
1615            router
1616                .blacklist
1617                .iter()
1618                .any(|name| name == EDITOR_SELECT_HANDLER_NAME)
1619        );
1620        assert!(
1621            !router
1622                .blacklist
1623                .iter()
1624                .any(|name| name == EDITOR_CURSOR_HANDLER_NAME)
1625        );
1626
1627        if let Ok(mut guard) = state.lock() {
1628            guard.interaction_mode = EditorInteractionMode::SelectAndCursor;
1629        }
1630        sync_editor_observer_routes(&mut world, &state, &workspace);
1631        let router = world
1632            .children_of(editor_root)
1633            .iter()
1634            .find_map(|child| world.get_component_by_id_as::<SignalObserverRouterComponent>(*child))
1635            .expect("router");
1636        assert!(
1637            !router
1638                .blacklist
1639                .iter()
1640                .any(|name| name == EDITOR_SELECT_HANDLER_NAME)
1641        );
1642        assert!(
1643            !router
1644                .blacklist
1645                .iter()
1646                .any(|name| name == EDITOR_CURSOR_HANDLER_NAME)
1647        );
1648    }
1649
1650    #[test]
1651    fn editor_settings_selection_uses_fallback_active_editor() {
1652        let mut world = World::default();
1653        let panel_query_root =
1654            world.add_component_boxed_named("panel_root", Box::new(TransformComponent::new()));
1655        let settings_panel_root = world.add_component_boxed_named(
1656            "editor_settings_panel_root",
1657            Box::new(TransformComponent::new()),
1658        );
1659        let settings_selection = world.add_component_boxed_named(
1660            "editor_settings_selection",
1661            Box::new(SelectionComponent::new()),
1662        );
1663        let row_root = world.add_component_boxed_named(
1664            "editor_settings_mode_select_cursor",
1665            Box::new(TransformComponent::new()),
1666        );
1667        let payload = world.add_component_boxed_named(
1668            "editor_settings_payload",
1669            Box::new(
1670                DataComponent::new()
1671                    .with_entry("mode_value", DataValue::Text("select_cursor".into())),
1672            ),
1673        );
1674        let editor_root =
1675            world.add_component_boxed_named("editor_root", Box::new(EditorComponent::new()));
1676
1677        let _ = world.add_child(panel_query_root, settings_panel_root);
1678        let _ = world.add_child(panel_query_root, settings_selection);
1679        let _ = world.add_child(settings_panel_root, row_root);
1680        let _ = world.add_child(row_root, payload);
1681
1682        let signal = Signal::event(
1683            settings_selection,
1684            EventSignal::SelectionChanged {
1685                selection_root: settings_selection,
1686                mode: crate::engine::ecs::component::SelectionMode::Single,
1687                selected_entries: vec![],
1688                selected_component: Some(row_root),
1689                selected_payload: Some(payload),
1690            },
1691        );
1692
1693        let event = editor_context_event_from_shared_signal(
1694            &world,
1695            panel_query_root,
1696            &signal,
1697            Some(editor_root),
1698        )
1699        .expect("event");
1700
1701        assert_eq!(
1702            event,
1703            EditorContextEvent::InteractionModeChanged {
1704                editor: Some(editor_root),
1705                interaction_mode: EditorInteractionMode::SelectAndCursor,
1706            }
1707        );
1708    }
1709
1710    #[test]
1711    fn editor_settings_selection_prefers_mode_value_payload() {
1712        let mut world = World::default();
1713        let panel_query_root =
1714            world.add_component_boxed_named("panel_root", Box::new(TransformComponent::new()));
1715        let settings_panel_root = world.add_component_boxed_named(
1716            "editor_settings_panel_root",
1717            Box::new(TransformComponent::new()),
1718        );
1719        let settings_selection = world.add_component_boxed_named(
1720            "editor_settings_selection",
1721            Box::new(SelectionComponent::new()),
1722        );
1723        let row_root = world
1724            .add_component_boxed_named("unexpected_row_label", Box::new(TransformComponent::new()));
1725        let payload = world.add_component_boxed_named(
1726            "editor_settings_payload",
1727            Box::new(
1728                DataComponent::new()
1729                    .with_entry("mode_value", DataValue::Text("cursor_3d".into()))
1730                    .with_entry(
1731                        "row_name",
1732                        DataValue::Text("editor_settings_mode_select".into()),
1733                    ),
1734            ),
1735        );
1736        let editor_root =
1737            world.add_component_boxed_named("editor_root", Box::new(EditorComponent::new()));
1738
1739        let _ = world.add_child(panel_query_root, settings_panel_root);
1740        let _ = world.add_child(panel_query_root, settings_selection);
1741        let _ = world.add_child(settings_panel_root, row_root);
1742        let _ = world.add_child(row_root, payload);
1743
1744        let signal = Signal::event(
1745            settings_selection,
1746            EventSignal::SelectionChanged {
1747                selection_root: settings_selection,
1748                mode: crate::engine::ecs::component::SelectionMode::Single,
1749                selected_entries: vec![],
1750                selected_component: Some(row_root),
1751                selected_payload: Some(payload),
1752            },
1753        );
1754
1755        let event = editor_context_event_from_shared_signal(
1756            &world,
1757            panel_query_root,
1758            &signal,
1759            Some(editor_root),
1760        )
1761        .expect("event");
1762
1763        assert_eq!(
1764            event,
1765            EditorContextEvent::InteractionModeChanged {
1766                editor: Some(editor_root),
1767                interaction_mode: EditorInteractionMode::Cursor3d,
1768            }
1769        );
1770    }
1771
1772    #[test]
1773    fn editor_settings_selection_requires_payload_contract_not_component_label() {
1774        let mut world = World::default();
1775        let panel_query_root =
1776            world.add_component_boxed_named("panel_root", Box::new(TransformComponent::new()));
1777        let settings_panel_root = world.add_component_boxed_named(
1778            "editor_settings_panel_root",
1779            Box::new(TransformComponent::new()),
1780        );
1781        let settings_selection = world.add_component_boxed_named(
1782            "editor_settings_selection",
1783            Box::new(SelectionComponent::new()),
1784        );
1785        let row_root = world.add_component_boxed_named(
1786            "editor_settings_mode_cursor_3d",
1787            Box::new(TransformComponent::new()),
1788        );
1789        let payload = world.add_component_boxed_named(
1790            "editor_settings_payload",
1791            Box::new(
1792                DataComponent::new().with_entry("row_kind", DataValue::Text("EditorMode".into())),
1793            ),
1794        );
1795        let editor_root =
1796            world.add_component_boxed_named("editor_root", Box::new(EditorComponent::new()));
1797
1798        let _ = world.add_child(panel_query_root, settings_panel_root);
1799        let _ = world.add_child(panel_query_root, settings_selection);
1800        let _ = world.add_child(settings_panel_root, row_root);
1801        let _ = world.add_child(row_root, payload);
1802
1803        let signal = Signal::event(
1804            settings_selection,
1805            EventSignal::SelectionChanged {
1806                selection_root: settings_selection,
1807                mode: crate::engine::ecs::component::SelectionMode::Single,
1808                selected_entries: vec![],
1809                selected_component: Some(row_root),
1810                selected_payload: Some(payload),
1811            },
1812        );
1813
1814        let event = editor_context_event_from_shared_signal(
1815            &world,
1816            panel_query_root,
1817            &signal,
1818            Some(editor_root),
1819        );
1820
1821        assert_eq!(event, None);
1822    }
1823
1824    #[test]
1825    fn cursor_visual_is_hosted_under_shared_panel_root() {
1826        let mut world = World::default();
1827        let panel_query_root =
1828            world.add_component_boxed_named("panel_root", Box::new(TransformComponent::new()));
1829        let editor_root =
1830            world.add_component_boxed_named("editor_root", Box::new(EditorComponent::new()));
1831        let workspace = Arc::new(Mutex::new(EditorContextWorkspaceState {
1832            panel_query_root: Some(panel_query_root),
1833            cursor_host_root: None,
1834            gizmo_host_root: None,
1835            shared_transform_gizmo: None,
1836            registered_editors: vec![editor_root],
1837        }));
1838        let state = Arc::new(Mutex::new(EditorContextState {
1839            active_editor: Some(editor_root),
1840            selected_component: Some(editor_root),
1841            interaction_mode: EditorInteractionMode::Cursor3d,
1842            cursor_translation: Some([1.0, 2.0, 3.0]),
1843            cursor_rotation: Some([0.0, 0.0, 0.0, 1.0]),
1844            ..EditorContextState::default()
1845        }));
1846        let mut emit = super::NullEmit;
1847        let cursor_host =
1848            super::ensure_workspace_cursor_host(&mut world, &workspace).expect("cursor host");
1849
1850        super::sync_editor_cursor_visual(&mut world, &mut emit, &state, Some(cursor_host));
1851
1852        let shared_marker = world
1853            .children_of(cursor_host)
1854            .iter()
1855            .copied()
1856            .find(|&child| world.component_label(child) == Some(super::CURSOR_MARKER_ROOT_NAME));
1857        let editor_marker = world
1858            .children_of(editor_root)
1859            .iter()
1860            .copied()
1861            .find(|&child| world.component_label(child) == Some(super::CURSOR_MARKER_ROOT_NAME));
1862        let panel_marker = world
1863            .children_of(panel_query_root)
1864            .iter()
1865            .copied()
1866            .find(|&child| world.component_label(child) == Some(super::CURSOR_MARKER_ROOT_NAME));
1867
1868        assert!(
1869            shared_marker.is_some(),
1870            "expected shared cursor marker under dedicated workspace root"
1871        );
1872        assert_eq!(editor_marker, None);
1873        assert_eq!(panel_marker, None);
1874    }
1875
1876    #[test]
1877    fn workspace_cursor_host_prefers_shared_panel_root() {
1878        let mut world = World::default();
1879        let panel_query_root = cid(&mut world);
1880        let workspace = Arc::new(Mutex::new(EditorContextWorkspaceState {
1881            panel_query_root: Some(panel_query_root),
1882            cursor_host_root: None,
1883            gizmo_host_root: None,
1884            shared_transform_gizmo: None,
1885            registered_editors: vec![],
1886        }));
1887
1888        assert_eq!(
1889            super::ensure_workspace_cursor_host(&mut world, &workspace),
1890            workspace
1891                .lock()
1892                .expect("workspace poisoned")
1893                .cursor_host_root
1894        );
1895    }
1896
1897    #[test]
1898    fn workspace_cursor_host_reuses_existing_root() {
1899        let mut world = World::default();
1900        let panel_query_root = cid(&mut world);
1901        let workspace = Arc::new(Mutex::new(EditorContextWorkspaceState {
1902            panel_query_root: Some(panel_query_root),
1903            cursor_host_root: None,
1904            gizmo_host_root: None,
1905            shared_transform_gizmo: None,
1906            registered_editors: vec![],
1907        }));
1908
1909        let first =
1910            super::ensure_workspace_cursor_host(&mut world, &workspace).expect("first host");
1911        let second =
1912            super::ensure_workspace_cursor_host(&mut world, &workspace).expect("second host");
1913
1914        assert_eq!(first, second);
1915    }
1916
1917    #[test]
1918    fn editor_root_selection_is_preserved_when_only_root_is_active() {
1919        let mut world = World::default();
1920        let editor = cid(&mut world);
1921        let next = reduce_editor_context_state(
1922            &EditorContextState::default(),
1923            &EditorContextEvent::WorldPanelSelectionChanged {
1924                component: Some(editor),
1925                editor: Some(editor),
1926                interaction_mode: EditorInteractionMode::Select,
1927            },
1928        );
1929
1930        assert_eq!(next.active_editor, Some(editor));
1931        assert_eq!(next.selected_component, Some(editor));
1932    }
1933
1934    #[test]
1935    fn world_panel_payload_event_prefers_semantic_target_over_clicked_row() {
1936        let mut world = World::default();
1937        let editor_root =
1938            world.add_component_boxed_named("editor_root", Box::new(EditorComponent::new()));
1939        let scene_target =
1940            world.add_component_boxed_named("scene_target", Box::new(TransformComponent::new()));
1941        let row = world.add_component_boxed_named("item_1", Box::new(TransformComponent::new()));
1942        let payload = world.add_component_boxed_named(
1943            "world_panel_payload",
1944            Box::new(
1945                DataComponent::new()
1946                    .with_entry("target_component", DataValue::Component(scene_target)),
1947            ),
1948        );
1949        let _ = world.add_child(editor_root, scene_target);
1950        let _ = world.add_child(row, payload);
1951
1952        let mut selection = SelectionComponent::new();
1953        selection.selected_component = Some(row);
1954        selection.selected_payload = Some(payload);
1955        let event = world_panel_selection_event(&world, &selection).expect("event");
1956
1957        assert_eq!(
1958            event,
1959            EditorContextEvent::WorldPanelSelectionChanged {
1960                component: Some(scene_target),
1961                editor: Some(editor_root),
1962                interaction_mode: EditorInteractionMode::Select,
1963            }
1964        );
1965    }
1966
1967    #[test]
1968    fn world_panel_payload_sync_updates_editor_selected_to_semantic_target() {
1969        let mut world = World::default();
1970        let editor_root =
1971            world.add_component_boxed_named("editor_root", Box::new(EditorComponent::new()));
1972        let scene_target =
1973            world.add_component_boxed_named("scene_target", Box::new(TransformComponent::new()));
1974        let _ = world.add_child(editor_root, scene_target);
1975
1976        sync_editor_component_selection(
1977            &mut world,
1978            &EditorContextEvent::WorldPanelSelectionChanged {
1979                component: Some(scene_target),
1980                editor: Some(editor_root),
1981                interaction_mode: EditorInteractionMode::Select,
1982            },
1983        );
1984
1985        assert_eq!(
1986            world
1987                .get_component_by_id_as::<EditorComponent>(editor_root)
1988                .expect("editor")
1989                .selected,
1990            Some(scene_target)
1991        );
1992    }
1993
1994    #[test]
1995    fn interaction_mode_change_updates_all_editor_roots() {
1996        let mut world = World::default();
1997        let editor_a =
1998            world.add_component_boxed_named("editor_a", Box::new(EditorComponent::new()));
1999        let editor_b =
2000            world.add_component_boxed_named("editor_b", Box::new(EditorComponent::new()));
2001
2002        sync_editor_component_selection(
2003            &mut world,
2004            &EditorContextEvent::InteractionModeChanged {
2005                editor: Some(editor_a),
2006                interaction_mode: EditorInteractionMode::Cursor3d,
2007            },
2008        );
2009
2010        assert_eq!(
2011            world
2012                .get_component_by_id_as::<EditorComponent>(editor_a)
2013                .expect("editor_a")
2014                .interaction_mode,
2015            EditorInteractionMode::Cursor3d
2016        );
2017        assert_eq!(
2018            world
2019                .get_component_by_id_as::<EditorComponent>(editor_b)
2020                .expect("editor_b")
2021                .interaction_mode,
2022            EditorInteractionMode::Cursor3d
2023        );
2024    }
2025}