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 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}