1use std::sync::{Arc, Mutex};
23use std::time::Instant;
24
25use khora_sdk::editor_ui::{viewport_texture::ViewportTextureHandle, AssetEntry};
26use khora_sdk::khora_core::platform::KhoraWindow;
27use khora_sdk::khora_core::renderer::api::resource::ViewInfo;
28use khora_sdk::khora_core::ui::editor::dock::{DockTree, DropZone};
29use khora_sdk::khora_core::ui::{EditorOverlay, OverlayScreenDescriptor};
30use khora_sdk::prelude::ecs::*;
31use khora_sdk::prelude::*;
32use khora_sdk::winit;
33use khora_sdk::RenderSystem;
34use khora_sdk::WgpuRenderSystem;
35use khora_sdk::{AgentProvider, EngineApp, GameWorld, InputEvent, Runtime};
36use khora_sdk::{CommandHistory, DccService, PlayMode};
37use khora_sdk::{
38 EditorCamera, EditorLogCapture, EditorMode, EditorShell, EditorState, LogEntry, PanelLocation,
39};
40
41use crate::bootstrap::{load_logo_icon, PROJECT_PATH};
42use crate::chrome::{SpinePanel, StatusBarPanel, TitleBarPanel};
43use crate::cmd_palette::CommandPalettePanel;
44use crate::input::InputState;
45use crate::panels::{
46 AssetBrowserPanel, ConsolePanel, ControlPlanePanel, PropertiesPanel, SceneTreePanel,
47 ViewportPanel,
48};
49use crate::project_vfs::ProjectVfs;
50use crate::{commands, hot_reload, input, mod_agents, mod_gizmo, ops, scene_io, util};
51
52pub struct EditorApp {
53 camera: Arc<Mutex<EditorCamera>>,
54 editor_state: Arc<Mutex<EditorState>>,
55 command_history: Arc<Mutex<CommandHistory>>,
56 log_handle: Arc<Mutex<Vec<LogEntry>>>,
57 shell: Option<Arc<Mutex<Box<dyn EditorShell>>>>,
58 overlay: Option<Arc<Mutex<Box<dyn EditorOverlay>>>>,
59 raw_window: Option<Arc<winit::window::Window>>,
63 monitors: Option<khora_sdk::MonitorRegistry>,
68 agent_registry: Option<Arc<Mutex<khora_sdk::AgentRegistry>>>,
71 dcc_context: Option<Arc<std::sync::RwLock<khora_sdk::DccContext>>>,
75 last_view_info: Option<ViewInfo>,
78 input: InputState,
79 wireframe_enabled: bool,
83 last_frame_time: Instant,
84 viewport_override: khora_sdk::khora_data::render::EditorViewportOverride,
88 project_vfs: Option<Arc<Mutex<ProjectVfs>>>,
93}
94
95impl EngineApp for EditorApp {
96 fn window_config() -> WindowConfig {
97 WindowConfig {
98 title: "Khora Engine Editor".to_owned(),
99 icon: Some(load_logo_icon()),
100 ..WindowConfig::default()
101 }
102 }
103
104 fn new() -> Self {
105 let editor_state = Arc::new(Mutex::new(EditorState::default()));
106 let command_history = Arc::new(Mutex::new(CommandHistory::default()));
107
108 let (capture, log_handle) = EditorLogCapture::new();
109 let _ = log::set_boxed_logger(Box::new(capture));
110 log::set_max_level(log::LevelFilter::Debug);
111
112 Self {
113 camera: Arc::new(Mutex::new(EditorCamera::default())),
114 editor_state,
115 command_history,
116 log_handle,
117 shell: None,
118 overlay: None,
119 raw_window: None,
120 monitors: None,
121 agent_registry: None,
122 dcc_context: None,
123 last_view_info: None,
124 input: InputState::default(),
125 wireframe_enabled: false,
126 last_frame_time: Instant::now(),
127 viewport_override: khora_sdk::khora_data::render::EditorViewportOverride::new(),
128 project_vfs: None,
129 }
130 }
131
132 fn setup(&mut self, world: &mut GameWorld, runtime: &Runtime) {
133 self.cache_services(runtime);
134 self.register_panels(runtime);
135 self.open_cli_project(world);
136 }
140
141 fn update(&mut self, world: &mut GameWorld, inputs: &[InputEvent]) {
142 let pending_load = self
144 .editor_state
145 .lock()
146 .ok()
147 .and_then(|mut s| s.pending_scene_load.take());
148 if let Some(path) = pending_load {
149 let abs = std::path::PathBuf::from(&path);
150 commands::load_scene_dispatch(
151 self.project_vfs.as_ref(),
152 world,
153 &self.editor_state,
154 &abs,
155 );
156 if let Ok(mut state) = self.editor_state.lock() {
157 state.current_scene_path = Some(path);
158 }
159 }
160
161 input::process_events(
162 &mut self.input,
163 inputs,
164 world,
165 &self.editor_state,
166 &self.camera,
167 );
168
169 if let Ok(mut s) = self.editor_state.lock() {
174 if s.pending_menu_action.as_deref() == Some("toggle_wireframe") {
175 s.pending_menu_action = None;
176 self.wireframe_enabled = !self.wireframe_enabled;
177 }
178 }
179
180 commands::process_menu_actions(
181 &mut self.project_vfs,
182 &self.editor_state,
183 &self.command_history,
184 world,
185 );
186
187 commands::process_pending_save_as_prefab(
188 self.project_vfs.as_ref(),
189 world,
190 &self.editor_state,
191 );
192 commands::process_pending_prefab_spawn(
193 self.project_vfs.as_ref(),
194 world,
195 &self.editor_state,
196 );
197 commands::process_pending_save_as_material(
198 self.project_vfs.as_ref(),
199 world,
200 &self.editor_state,
201 );
202 commands::process_pending_assign_material(
203 self.project_vfs.as_ref(),
204 world,
205 &self.editor_state,
206 );
207 commands::process_pending_asset_file_ops(self.project_vfs.as_ref(), &self.editor_state);
208 commands::process_pending_spawn_mesh_asset(
209 self.project_vfs.as_ref(),
210 world,
211 &self.editor_state,
212 );
213 commands::process_pending_assign_texture(
214 self.project_vfs.as_ref(),
215 world,
216 &self.editor_state,
217 );
218
219 if let Ok(mut state) = self.editor_state.lock() {
220 ops::apply_edits(world, &mut state);
221 }
222
223 let play_mode = self
224 .editor_state
225 .lock()
226 .map(|s| s.play_mode)
227 .unwrap_or(PlayMode::Editing);
228 ops::sync_scene_cameras_for_mode(world, play_mode);
229
230 self.tick_state(world);
231 }
232
233 fn on_shutdown(&mut self) {
234 log::info!("EditorApp: Shutting down");
235 }
236
237 fn intercept_window_event(
238 &mut self,
239 event: &dyn std::any::Any,
240 _window: &dyn KhoraWindow,
241 ) -> bool {
242 let Some(overlay_arc) = self.overlay.as_ref() else {
243 return false;
244 };
245 let Some(raw_window) = self.raw_window.as_ref() else {
246 return false;
247 };
248 let Ok(mut overlay) = overlay_arc.lock() else {
249 return false;
250 };
251 let consumed_by_egui =
257 overlay.handle_window_event((&**raw_window) as &dyn std::any::Any, event);
258
259 let we = event.downcast_ref::<winit::event::WindowEvent>();
260 let Some(we) = we else {
261 return consumed_by_egui;
262 };
263 use winit::event::WindowEvent;
264
265 if let WindowEvent::CursorMoved { position, .. } = we {
268 self.input.last_cursor_pos = Some((position.x as f32, position.y as f32));
269 }
270
271 if matches!(we, WindowEvent::Focused(false)) {
275 self.input.release_all();
276 }
277
278 let is_pointer_event = matches!(
279 we,
280 WindowEvent::MouseInput { .. }
281 | WindowEvent::CursorMoved { .. }
282 | WindowEvent::MouseWheel { .. }
283 );
284 if !is_pointer_event {
285 return consumed_by_egui;
286 }
287
288 let viewport_rect = self
289 .editor_state
290 .lock()
291 .ok()
292 .and_then(|s| s.viewport_screen_rect);
293 let pos = match we {
294 WindowEvent::CursorMoved { position, .. } => {
295 Some((position.x as f32, position.y as f32))
296 }
297 _ => self.input.last_cursor_pos,
298 };
299
300 if let (Some([rx, ry, rw, rh]), Some((cx, cy))) = (viewport_rect, pos) {
301 let in_viewport = cx >= rx && cx < rx + rw && cy >= ry && cy < ry + rh;
302 if in_viewport {
303 return false;
304 }
305 }
306 consumed_by_egui
307 }
308
309 fn before_frame(
310 &mut self,
311 _world: &mut GameWorld,
312 runtime: &Runtime,
313 window: &dyn KhoraWindow,
314 ) {
315 if let Some(rs_arc) = runtime
321 .backends
322 .get::<Arc<Mutex<Box<dyn RenderSystem>>>>()
323 .cloned()
324 {
325 if let Ok(mut rs) = rs_arc.lock() {
326 rs.set_render_to_viewport(true);
327 }
328 }
329
330 let Some(overlay_arc) = self.overlay.as_ref() else {
331 return;
332 };
333 let Some(raw_window) = self.raw_window.as_ref() else {
334 return;
335 };
336
337 let (w_px, h_px) = window.inner_size();
338 let screen = OverlayScreenDescriptor {
339 width_px: w_px,
340 height_px: h_px,
341 scale_factor: window.scale_factor() as f32,
342 };
343
344 if let Ok(mut overlay) = overlay_arc.lock() {
345 overlay.begin_frame((&**raw_window) as &dyn std::any::Any, screen);
346 }
347
348 if let Some(shell) = self.shell.as_ref() {
349 if let Ok(mut shell) = shell.lock() {
350 shell.show_frame();
351 }
352 }
353 }
354
355 fn before_agents(&mut self, world: &mut GameWorld, runtime: &Runtime) {
356 if let Some(pvfs_arc) = self.project_vfs.as_ref() {
357 hot_reload::pump(pvfs_arc, &self.editor_state);
358 }
359
360 self.last_view_info = None;
361
362 let Some(rs_arc) = runtime
363 .backends
364 .get::<Arc<Mutex<Box<dyn RenderSystem>>>>()
365 .cloned()
366 else {
367 return;
368 };
369 let Ok(mut rs) = rs_arc.lock() else { return };
370 let Some(wgpu_rs) = rs.as_any_mut().downcast_mut::<WgpuRenderSystem>() else {
371 return;
372 };
373
374 let (vw, vh) = wgpu_rs.viewport_size();
375 let play_mode = self
376 .editor_state
377 .lock()
378 .ok()
379 .map(|s| s.play_mode)
380 .unwrap_or(PlayMode::Editing);
381
382 let view_info = match play_mode {
383 PlayMode::Editing => match self.camera.lock() {
384 Ok(cam) => cam.view_info(vw as f32, vh as f32),
385 Err(_) => ViewInfo::default(),
386 },
387 PlayMode::Playing | PlayMode::Paused => {
388 use khora_sdk::khora_data::render::extract_active_camera_view;
389 extract_active_camera_view(world.inner_world())
390 .or_else(|| {
391 self.camera
396 .lock()
397 .ok()
398 .map(|cam| cam.view_info(vw as f32, vh as f32))
399 })
400 .unwrap_or_default()
401 }
402 };
403
404 self.viewport_override
410 .set(Some(khora_sdk::khora_data::render::ExtractedView {
411 view_proj: view_info.view_projection_matrix(),
412 position: view_info.camera_position,
413 }));
414
415 wgpu_rs.prepare_frame(&view_info);
420
421 let grid_on = matches!(play_mode, PlayMode::Editing);
426 if let Some(grid_cfg) = runtime
427 .resources
428 .get::<khora_sdk::khora_lanes::render_lane::SharedGridConfig>()
429 {
430 if let Ok(mut cfg) = grid_cfg.lock() {
431 cfg.enabled = grid_on;
432 }
433 }
434
435 if let Some(wf_cfg) = runtime
438 .resources
439 .get::<khora_sdk::khora_lanes::render_lane::SharedWireframeConfig>()
440 {
441 if let Ok(mut cfg) = wf_cfg.lock() {
442 cfg.enabled = self.wireframe_enabled && grid_on;
443 }
444 }
445
446 let gizmo_lines = if let Ok(state) = self.editor_state.lock() {
452 if state.play_mode != PlayMode::Editing || state.selection.is_empty() {
453 Vec::new()
454 } else {
455 mod_gizmo::collect_gizmo_lines(world, &state, &view_info)
456 }
457 } else {
458 Vec::new()
459 };
460 if let Some(shared) = runtime
461 .resources
462 .get::<khora_sdk::khora_lanes::render_lane::SharedGizmoFrame>()
463 {
464 if let Ok(mut frame) = shared.lock() {
465 frame.lines = gizmo_lines;
466 }
467 }
468
469 self.last_view_info = Some(view_info);
470 }
471
472 fn after_agents(&mut self, _world: &mut GameWorld, runtime: &Runtime) {
473 let Some(rs_arc) = runtime
474 .backends
475 .get::<Arc<Mutex<Box<dyn RenderSystem>>>>()
476 .cloned()
477 else {
478 return;
479 };
480
481 let Some(overlay_arc) = self.overlay.as_ref() else {
489 return;
490 };
491 let Some(raw_window) = self.raw_window.as_ref() else {
492 return;
493 };
494
495 let inner = (**raw_window).inner_size();
496 let screen = OverlayScreenDescriptor {
497 width_px: inner.width,
498 height_px: inner.height,
499 scale_factor: (**raw_window).scale_factor() as f32,
500 };
501
502 {
503 let mut rs = match rs_arc.lock() {
504 Ok(g) => g,
505 Err(_) => return,
506 };
507 rs.set_render_to_viewport(false);
511 let mut overlay = match overlay_arc.lock() {
512 Ok(g) => g,
513 Err(_) => return,
514 };
515 let overlay_ref: &mut dyn EditorOverlay = &mut **overlay;
516 if let Err(e) = rs.render_overlay(overlay_ref, screen) {
517 log::error!("editor: render_overlay failed: {e:?}");
518 }
519 }
520 }
521}
522
523impl EditorApp {
524 fn cache_services(&mut self, runtime: &Runtime) {
525 if let Some(camera) = runtime
526 .resources
527 .get::<std::sync::Arc<std::sync::Mutex<EditorCamera>>>()
528 .cloned()
529 {
530 self.camera = camera;
531 }
532 self.overlay = runtime
533 .backends
534 .get::<Arc<Mutex<Box<dyn EditorOverlay>>>>()
535 .cloned();
536 self.monitors = runtime
537 .resources
538 .get::<khora_sdk::MonitorRegistry>()
539 .cloned();
540 self.agent_registry = runtime
541 .resources
542 .get::<Arc<Mutex<khora_sdk::AgentRegistry>>>()
543 .cloned();
544 self.dcc_context = runtime
545 .resources
546 .get::<Arc<std::sync::RwLock<khora_sdk::DccContext>>>()
547 .cloned();
548 self.raw_window = runtime
549 .resources
550 .get::<Arc<winit::window::Window>>()
551 .cloned();
552 }
553
554 fn scene_layout() -> DockTree {
561 const VIEWPORT: &str = "khora.editor.viewport";
562 let mut t = DockTree::single(VIEWPORT);
563
564 let hierarchy = t.insert("khora.editor.scene_tree", Some(VIEWPORT), DropZone::Left);
568 let inspector = t.insert("khora.editor.properties", Some(VIEWPORT), DropZone::Right);
569 let bottom = t.insert(
570 "khora.editor.asset_browser",
571 Some(VIEWPORT),
572 DropZone::Bottom,
573 );
574 t.insert(
575 "khora.editor.console",
576 Some("khora.editor.asset_browser"),
577 DropZone::Center,
578 );
579
580 if let Some(id) = hierarchy {
581 t.set_ratio(id, 0.18); }
583 if let Some(id) = inspector {
584 t.set_ratio(id, 0.74); }
586 if let Some(id) = bottom {
587 t.set_ratio(id, 0.72); }
589
590 t.activate("khora.editor.asset_browser");
592 t
593 }
594
595 fn control_plane_layout() -> DockTree {
599 DockTree::single("khora.editor.control_plane")
600 }
601
602 fn register_panels(&mut self, runtime: &Runtime) {
603 let viewport_handle = runtime
604 .resources
605 .get::<ViewportTextureHandle>()
606 .copied()
607 .unwrap_or(khora_sdk::PRIMARY_VIEWPORT);
608
609 let Some(shell_ref) = runtime
610 .backends
611 .get::<std::sync::Arc<std::sync::Mutex<Box<dyn EditorShell>>>>()
612 .cloned()
613 else {
614 log::warn!("EditorApp: no EditorShell found in runtime.backends.");
615 return;
616 };
617
618 if let Ok(mut shell) = shell_ref.lock() {
619 shell.set_editor_state(self.editor_state.clone());
620
621 let brand_theme = khora_tool_ui::khora_dark();
623 shell.set_theme(brand_theme.clone());
624 shell.set_fonts(crate::fonts::load_pack());
625
626 shell.register_panel(
628 PanelLocation::TopBar,
629 Box::new(TitleBarPanel::new(
630 self.editor_state.clone(),
631 brand_theme.clone(),
632 )),
633 );
634 shell.register_panel(
635 PanelLocation::Spine,
636 Box::new(SpinePanel::new(
637 self.editor_state.clone(),
638 brand_theme.clone(),
639 )),
640 );
641 shell.register_panel(
642 PanelLocation::StatusBar,
643 Box::new(StatusBarPanel::new(
644 self.editor_state.clone(),
645 brand_theme.clone(),
646 )),
647 );
648
649 let mut workbench = crate::workbench::WorkbenchPanel::new(
654 self.editor_state.clone(),
655 brand_theme.clone(),
656 );
657 workbench.add_panel(Box::new(SceneTreePanel::new(
658 self.editor_state.clone(),
659 brand_theme.clone(),
660 )));
661 workbench.add_panel(Box::new(PropertiesPanel::new(
662 self.editor_state.clone(),
663 self.command_history.clone(),
664 brand_theme.clone(),
665 )));
666 workbench.add_panel(Box::new(AssetBrowserPanel::new(
667 self.editor_state.clone(),
668 brand_theme.clone(),
669 )));
670 workbench.add_panel(Box::new(ConsolePanel::new(
671 self.editor_state.clone(),
672 brand_theme.clone(),
673 )));
674 workbench.add_panel(Box::new(ViewportPanel::new(
675 viewport_handle,
676 self.editor_state.clone(),
677 self.camera.clone(),
678 brand_theme.clone(),
679 )));
680 workbench.add_panel(Box::new(ControlPlanePanel::new(
681 self.editor_state.clone(),
682 brand_theme.clone(),
683 self.agent_registry.clone(),
684 self.dcc_context.clone(),
685 )));
686 workbench.set_layout(EditorMode::Scene, Self::scene_layout());
687 workbench.set_layout(EditorMode::ControlPlane, Self::control_plane_layout());
688 shell.register_panel(PanelLocation::Center, Box::new(workbench));
689
690 shell.register_panel(
692 PanelLocation::Floating(100),
693 Box::new(CommandPalettePanel::new(
694 self.editor_state.clone(),
695 brand_theme.clone(),
696 )),
697 );
698 log::info!("EditorApp: panels registered with shell.");
699 }
700 self.shell = Some(shell_ref);
701 }
702
703 fn open_cli_project(&mut self, world: &mut GameWorld) {
704 let Some(Some(project_path)) = PROJECT_PATH.get() else {
705 return;
706 };
707 let path = std::path::PathBuf::from(project_path);
708 if !path.exists() {
709 log::warn!("--project path does not exist: {}", project_path);
710 return;
711 }
712
713 let metrics = std::sync::Arc::new(khora_sdk::MetricsRegistry::new());
717 let mut pvfs = match ProjectVfs::open(path.clone(), metrics) {
718 Ok(p) => p,
719 Err(e) => {
720 log::error!(
721 "Failed to open ProjectVfs for '{}': {:#}",
722 path.display(),
723 e
724 );
725 return;
726 }
727 };
728
729 let project_json: Option<serde_json::Value> = pvfs.read_project_json().ok();
732 let project_name = project_json
733 .as_ref()
734 .and_then(|v| v.get("name").and_then(|n| n.as_str()).map(String::from));
735 let project_engine_version = project_json.as_ref().and_then(|v| {
736 v.get("engine_version")
737 .and_then(|n| n.as_str())
738 .map(String::from)
739 });
740
741 let entries = hot_reload::collect_asset_entries(&pvfs);
742 let dirs = pvfs.list_dirs();
743 let git_branch = util::read_git_branch(&path);
744
745 if let Ok(mut state) = self.editor_state.lock() {
746 state.project_folder = Some(path.to_string_lossy().to_string());
747 state.project_name = project_name.clone();
748 state.project_engine_version = project_engine_version.clone();
749 state.asset_entries = entries;
750 state.asset_dirs = dirs;
751 state.asset_epoch = state.asset_epoch.wrapping_add(1);
752 state.current_git_branch = git_branch.clone();
753 log::info!(
754 "Opened project '{}' from CLI: '{}' ({} assets, git: {}, engine: {})",
755 project_name.as_deref().unwrap_or("<unknown>"),
756 path.display(),
757 state.asset_entries.len(),
758 git_branch.as_deref().unwrap_or("none"),
759 project_engine_version.as_deref().unwrap_or("<unknown>")
760 );
761 }
762
763 scene_io::auto_load_or_create_default_scene(&mut pvfs, world);
766
767 let entries: Vec<AssetEntry> = hot_reload::collect_asset_entries(&pvfs);
770 let dirs = pvfs.list_dirs();
771 if let Ok(mut state) = self.editor_state.lock() {
772 state.asset_entries = entries;
773 state.asset_dirs = dirs;
774 state.asset_epoch = state.asset_epoch.wrapping_add(1);
775 }
776
777 self.project_vfs = Some(Arc::new(Mutex::new(pvfs)));
778 }
779
780 fn tick_state(&mut self, world: &mut GameWorld) {
781 let mut state = match self.editor_state.lock() {
782 Ok(s) => s,
783 Err(_) => return,
784 };
785 state.ctrl_held = self.input.ctrl_held;
786
787 ops::process_spawns(world, &mut state);
788 ops::process_reparents(world, &mut state);
789
790 if let Some((entity, new_name)) = state.pending_rename.take() {
791 if let Some(name) = world.get_component_mut::<Name>(entity) {
792 *name = Name::new(&new_name);
793 log::info!("Renamed entity {:?} to '{}'", entity, new_name);
794 }
795 }
796
797 if let Some(entity) = state.pending_delete.take() {
798 world.despawn(entity);
799 state.selection.remove(&entity);
800 if state.inspected.as_ref().is_some_and(|i| i.entity == entity) {
801 state.inspected = None;
802 }
803 log::info!("Deleted entity {:?}", entity);
804 }
805
806 if let Some(entity) = state.pending_duplicate.take() {
807 ops::duplicate_entity(world, entity, &mut state);
808 }
809
810 if let Some((entity, type_name)) = state.pending_add_component.take() {
811 ops::add_component_to_entity(world, entity, &type_name);
812 }
813
814 ops::extract_scene_tree(world, &mut state);
815 ops::extract_inspected(world, &mut state);
816
817 if let Ok(log_entries) = self.log_handle.lock() {
818 state.log_entries.clone_from(&log_entries);
819 }
820
821 let now = Instant::now();
822 let dt = now.duration_since(self.last_frame_time).as_secs_f32();
823 self.last_frame_time = now;
824 state.status.frame_time_ms = dt * 1000.0;
825 state.status.fps = if dt > 0.0 { 1.0 / dt } else { 0.0 };
826 state.status.entity_count = state.entity_count;
827
828 if let Some(ref monitors) = self.monitors {
832 use khora_sdk::MonitoredResourceType;
833 for monitor in monitors.get_all_monitors() {
834 match monitor.resource_type() {
835 MonitoredResourceType::Vram => {
836 let r = monitor.get_usage_report();
837 state.status.vram_mb = r.current_bytes as f32 / (1024.0 * 1024.0);
838 }
839 MonitoredResourceType::Gpu => {
840 if let Some(g) = monitor.get_gpu_report() {
841 state.status.draw_calls = g.draw_calls;
842 state.status.triangles = g.triangles_rendered as u64;
843 }
844 if let Some(hw) = monitor.get_hardware_report() {
845 state.status.gpu_load = hw.gpu_load.unwrap_or(0.0);
846 }
847 }
848 MonitoredResourceType::Hardware => {
849 if let Some(hw) = monitor.get_hardware_report() {
850 state.status.cpu_load = hw.cpu_load;
851 if let Some(g) = hw.gpu_load {
852 state.status.gpu_load = g;
853 }
854 }
855 }
856 MonitoredResourceType::SystemRam => {
857 let r = monitor.get_usage_report();
858 state.status.memory_used_mb = r.current_bytes as f32 / (1024.0 * 1024.0);
859 }
860 }
861 }
862 }
863
864 let status_copy = state.status.clone();
865 drop(state);
866
867 if let Some(ref shell) = self.shell {
868 if let Ok(mut shell) = shell.lock() {
869 shell.set_status(status_copy);
870 }
871 }
872 }
873}
874
875impl AgentProvider for EditorApp {
876 fn register_agents(&self, dcc: &DccService, runtime: &mut Runtime) {
877 runtime.resources.insert(self.editor_state.clone());
878 runtime.resources.insert(self.camera.clone());
879 runtime.resources.insert(self.viewport_override.clone());
882 mod_agents::register_editor_agents(dcc);
883 }
884}
885
886impl khora_sdk::PhaseProvider for EditorApp {
887 fn custom_phases(&self) -> Vec<khora_sdk::ExecutionPhase> {
888 Vec::new()
889 }
890
891 fn removed_phases(&self) -> Vec<khora_sdk::ExecutionPhase> {
892 Vec::new()
893 }
894}