diff --git a/packages/editor-app/src-tauri/Cargo.lock b/packages/editor-app/src-tauri/Cargo.lock index 203640a1..951d38c3 100644 --- a/packages/editor-app/src-tauri/Cargo.lock +++ b/packages/editor-app/src-tauri/Cargo.lock @@ -362,8 +362,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" dependencies = [ "iana-time-zone", + "js-sys", "num-traits", "serde", + "wasm-bindgen", "windows-link 0.2.1", ] @@ -557,6 +559,12 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "data-encoding" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" + [[package]] name = "deranged" version = "0.5.4" @@ -718,6 +726,8 @@ checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" name = "ecs-editor" version = "1.0.0" dependencies = [ + "chrono", + "futures-util", "glob", "serde", "serde_json", @@ -725,6 +735,8 @@ dependencies = [ "tauri-build", "tauri-plugin-dialog", "tauri-plugin-shell", + "tokio", + "tokio-tungstenite", ] [[package]] @@ -3269,6 +3281,17 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sha2" version = "0.10.9" @@ -3989,13 +4012,38 @@ dependencies = [ "bytes", "libc", "mio", + "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2", + "tokio-macros", "tracing", "windows-sys 0.61.2", ] +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite", +] + [[package]] name = "tokio-util" version = "0.7.16" @@ -4209,6 +4257,25 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "tungstenite" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand 0.8.5", + "sha1", + "thiserror 1.0.69", + "url", + "utf-8", +] + [[package]] name = "typeid" version = "1.0.3" diff --git a/packages/editor-app/src-tauri/Cargo.toml b/packages/editor-app/src-tauri/Cargo.toml index c9211592..c7181f5a 100644 --- a/packages/editor-app/src-tauri/Cargo.toml +++ b/packages/editor-app/src-tauri/Cargo.toml @@ -19,6 +19,10 @@ tauri-plugin-dialog = "2.0" serde = { version = "1", features = ["derive"] } serde_json = "1" glob = "0.3" +tokio = { version = "1", features = ["full"] } +tokio-tungstenite = "0.21" +futures-util = "0.3" +chrono = "0.4" [profile.dev] incremental = true diff --git a/packages/editor-app/src-tauri/src/commands.rs b/packages/editor-app/src-tauri/src/commands.rs index 9792f433..6e4abf72 100644 --- a/packages/editor-app/src-tauri/src/commands.rs +++ b/packages/editor-app/src-tauri/src/commands.rs @@ -1,4 +1,7 @@ use serde::{Deserialize, Serialize}; +use std::sync::Arc; +use tokio::sync::Mutex; +use crate::profiler_ws::ProfilerServer; #[derive(Debug, Serialize, Deserialize)] pub struct ProjectInfo { @@ -23,3 +26,51 @@ impl Default for EditorConfig { } } } + +pub struct ProfilerState { + pub server: Arc>>>, +} + +#[tauri::command] +pub async fn start_profiler_server( + port: u16, + state: tauri::State<'_, ProfilerState>, +) -> Result { + let mut server_lock = state.server.lock().await; + + if server_lock.is_some() { + return Err("Profiler server is already running".to_string()); + } + + let server = Arc::new(ProfilerServer::new(port)); + + match server.start().await { + Ok(_) => { + *server_lock = Some(server); + Ok(format!("Profiler server started on port {}", port)) + } + Err(e) => Err(format!("Failed to start profiler server: {}", e)), + } +} + +#[tauri::command] +pub async fn stop_profiler_server( + state: tauri::State<'_, ProfilerState>, +) -> Result { + let mut server_lock = state.server.lock().await; + + if server_lock.is_none() { + return Err("Profiler server is not running".to_string()); + } + + *server_lock = None; + Ok("Profiler server stopped".to_string()) +} + +#[tauri::command] +pub async fn get_profiler_status( + state: tauri::State<'_, ProfilerState>, +) -> Result { + let server_lock = state.server.lock().await; + Ok(server_lock.is_some()) +} diff --git a/packages/editor-app/src-tauri/src/lib.rs b/packages/editor-app/src-tauri/src/lib.rs index 0fedf2b2..6f767456 100644 --- a/packages/editor-app/src-tauri/src/lib.rs +++ b/packages/editor-app/src-tauri/src/lib.rs @@ -2,6 +2,8 @@ pub mod commands; pub mod project; +pub mod profiler_ws; pub use commands::*; pub use project::*; +pub use profiler_ws::*; diff --git a/packages/editor-app/src-tauri/src/main.rs b/packages/editor-app/src-tauri/src/main.rs index ecebf13e..37d99ff5 100644 --- a/packages/editor-app/src-tauri/src/main.rs +++ b/packages/editor-app/src-tauri/src/main.rs @@ -5,6 +5,7 @@ use tauri::Manager; use tauri::AppHandle; use std::sync::{Arc, Mutex}; use std::collections::HashMap; +use ecs_editor_lib::profiler_ws::ProfilerServer; // IPC Commands #[tauri::command] @@ -177,10 +178,68 @@ fn toggle_devtools(app: AppHandle) -> Result<(), String> { } } +// Profiler State +pub struct ProfilerState { + pub server: Arc>>>, +} + +#[tauri::command] +async fn start_profiler_server( + port: u16, + state: tauri::State<'_, ProfilerState>, +) -> Result { + let mut server_lock = state.server.lock().await; + + if server_lock.is_some() { + return Err("Profiler server is already running".to_string()); + } + + let server = Arc::new(ProfilerServer::new(port)); + + match server.start().await { + Ok(_) => { + *server_lock = Some(server); + Ok(format!("Profiler server started on port {}", port)) + } + Err(e) => Err(format!("Failed to start profiler server: {}", e)), + } +} + +#[tauri::command] +async fn stop_profiler_server( + state: tauri::State<'_, ProfilerState>, +) -> Result { + let mut server_lock = state.server.lock().await; + + if server_lock.is_none() { + return Err("Profiler server is not running".to_string()); + } + + // 调用 stop 方法正确关闭服务器 + if let Some(server) = server_lock.as_ref() { + server.stop().await; + } + + *server_lock = None; + Ok("Profiler server stopped".to_string()) +} + +#[tauri::command] +async fn get_profiler_status( + state: tauri::State<'_, ProfilerState>, +) -> Result { + let server_lock = state.server.lock().await; + Ok(server_lock.is_some()) +} + fn main() { let project_paths: Arc>> = Arc::new(Mutex::new(HashMap::new())); let project_paths_clone = Arc::clone(&project_paths); + let profiler_state = ProfilerState { + server: Arc::new(tokio::sync::Mutex::new(None)), + }; + tauri::Builder::default() .plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_dialog::init()) @@ -232,6 +291,7 @@ fn main() { }) .setup(move |app| { app.manage(project_paths); + app.manage(profiler_state); Ok(()) }) .invoke_handler(tauri::generate_handler![ @@ -244,7 +304,10 @@ fn main() { read_file_content, list_directory, set_project_base_path, - toggle_devtools + toggle_devtools, + start_profiler_server, + stop_profiler_server, + get_profiler_status ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/packages/editor-app/src-tauri/src/profiler_ws.rs b/packages/editor-app/src-tauri/src/profiler_ws.rs new file mode 100644 index 00000000..f88430ac --- /dev/null +++ b/packages/editor-app/src-tauri/src/profiler_ws.rs @@ -0,0 +1,176 @@ +use std::net::SocketAddr; +use std::sync::Arc; +use tokio::net::{TcpListener, TcpStream}; +use tokio::sync::{broadcast, Mutex}; +use tokio::task::JoinHandle; +use tokio_tungstenite::{accept_async, tungstenite::Message}; +use futures_util::{SinkExt, StreamExt}; + +pub struct ProfilerServer { + tx: broadcast::Sender, + port: u16, + shutdown_tx: Arc>>>, + task_handle: Arc>>>, +} + +impl ProfilerServer { + pub fn new(port: u16) -> Self { + let (tx, _) = broadcast::channel(100); + Self { + tx, + port, + shutdown_tx: Arc::new(Mutex::new(None)), + task_handle: Arc::new(Mutex::new(None)), + } + } + + pub async fn start(&self) -> Result<(), Box> { + let addr = format!("127.0.0.1:{}", self.port); + let listener = TcpListener::bind(&addr).await?; + println!("[ProfilerServer] Listening on: {}", addr); + + let tx = self.tx.clone(); + let (shutdown_tx, mut shutdown_rx) = tokio::sync::oneshot::channel(); + + // 存储 shutdown sender + *self.shutdown_tx.lock().await = Some(shutdown_tx); + + // 启动服务器任务 + let task = tokio::spawn(async move { + loop { + tokio::select! { + // 监听新连接 + result = listener.accept() => { + match result { + Ok((stream, peer_addr)) => { + println!("[ProfilerServer] New connection from: {}", peer_addr); + let tx = tx.clone(); + tokio::spawn(handle_connection(stream, peer_addr, tx)); + } + Err(e) => { + eprintln!("[ProfilerServer] Failed to accept connection: {}", e); + } + } + } + // 监听关闭信号 + _ = &mut shutdown_rx => { + println!("[ProfilerServer] Received shutdown signal"); + break; + } + } + } + println!("[ProfilerServer] Server task ending"); + }); + + // 存储任务句柄 + *self.task_handle.lock().await = Some(task); + + Ok(()) + } + + pub async fn stop(&self) { + println!("[ProfilerServer] Stopping server..."); + + // 发送关闭信号 + if let Some(shutdown_tx) = self.shutdown_tx.lock().await.take() { + let _ = shutdown_tx.send(()); + } + + // 等待任务完成 + if let Some(handle) = self.task_handle.lock().await.take() { + let _ = handle.await; + } + + println!("[ProfilerServer] Server stopped"); + } + + pub fn broadcast(&self, message: String) { + let _ = self.tx.send(message); + } +} + +async fn handle_connection( + stream: TcpStream, + peer_addr: SocketAddr, + tx: broadcast::Sender, +) { + let ws_stream = match accept_async(stream).await { + Ok(ws) => ws, + Err(e) => { + eprintln!("[ProfilerServer] WebSocket error: {}", e); + return; + } + }; + + let (mut ws_sender, mut ws_receiver) = ws_stream.split(); + let mut rx = tx.subscribe(); + + println!("[ProfilerServer] Client {} connected", peer_addr); + + // Send initial connection confirmation + let _ = ws_sender + .send(Message::Text( + serde_json::json!({ + "type": "connected", + "message": "Connected to ECS Editor Profiler" + }) + .to_string(), + )) + .await; + + // Spawn task to forward broadcast messages to this client + let forward_task = tokio::spawn(async move { + while let Ok(msg) = rx.recv().await { + if ws_sender.send(Message::Text(msg)).await.is_err() { + break; + } + } + }); + + // Handle incoming messages from client + while let Some(msg) = ws_receiver.next().await { + match msg { + Ok(Message::Text(text)) => { + // Parse incoming debug data from game client + if let Ok(json_value) = serde_json::from_str::(&text) { + if json_value.get("type").and_then(|t| t.as_str()) == Some("debug_data") { + // Broadcast to frontend (ProfilerWindow) + tx.send(text).ok(); + } else if json_value.get("type").and_then(|t| t.as_str()) == Some("ping") { + // Respond to ping + let _ = tx.send( + serde_json::json!({ + "type": "pong", + "timestamp": chrono::Utc::now().timestamp_millis() + }) + .to_string(), + ); + } + } + } + Ok(Message::Close(_)) => { + println!("[ProfilerServer] Client {} disconnected", peer_addr); + break; + } + Ok(Message::Ping(data)) => { + // Respond to WebSocket ping + tx.send( + serde_json::json!({ + "type": "pong", + "data": String::from_utf8_lossy(&data) + }) + .to_string(), + ) + .ok(); + } + Err(e) => { + eprintln!("[ProfilerServer] Error: {}", e); + break; + } + _ => {} + } + } + + forward_task.abort(); + println!("[ProfilerServer] Connection handler ended for {}", peer_addr); +} diff --git a/packages/editor-app/src/App.tsx b/packages/editor-app/src/App.tsx index c84e6729..534efcb0 100644 --- a/packages/editor-app/src/App.tsx +++ b/packages/editor-app/src/App.tsx @@ -1,13 +1,17 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useRef } from 'react'; import { Core, Scene } from '@esengine/ecs-framework'; import { EditorPluginManager, UIRegistry, MessageHub, SerializerRegistry, EntityStoreService, ComponentRegistry, LocaleService, ProjectService, ComponentDiscoveryService, ComponentLoaderService, PropertyMetadataService, LogService } from '@esengine/editor-core'; import { SceneInspectorPlugin } from './plugins/SceneInspectorPlugin'; +import { ProfilerPlugin } from './plugins/ProfilerPlugin'; import { StartupPage } from './components/StartupPage'; import { SceneHierarchy } from './components/SceneHierarchy'; import { EntityInspector } from './components/EntityInspector'; import { AssetBrowser } from './components/AssetBrowser'; import { ConsolePanel } from './components/ConsolePanel'; +import { ProfilerPanel } from './components/ProfilerPanel'; import { PluginManagerWindow } from './components/PluginManagerWindow'; +import { ProfilerWindow } from './components/ProfilerWindow'; +import { PortManager } from './components/PortManager'; import { Viewport } from './components/Viewport'; import { MenuBar } from './components/MenuBar'; import { DockContainer, DockablePanel } from './components/DockContainer'; @@ -25,6 +29,7 @@ localeService.registerTranslations('zh', zh); Core.services.registerInstance(LocaleService, localeService); function App() { + const initRef = useRef(false); const [initialized, setInitialized] = useState(false); const [projectLoaded, setProjectLoaded] = useState(false); const [isLoading, setIsLoading] = useState(false); @@ -34,10 +39,13 @@ function App() { const [entityStore, setEntityStore] = useState(null); const [messageHub, setMessageHub] = useState(null); const [logService, setLogService] = useState(null); + const [uiRegistry, setUiRegistry] = useState(null); const { t, locale, changeLocale } = useLocale(); const [status, setStatus] = useState(t('header.status.initializing')); const [panels, setPanels] = useState([]); const [showPluginManager, setShowPluginManager] = useState(false); + const [showProfiler, setShowProfiler] = useState(false); + const [showPortManager, setShowPortManager] = useState(false); useEffect(() => { // 禁用默认右键菜单 @@ -54,7 +62,15 @@ function App() { useEffect(() => { const initializeEditor = async () => { + // 使用 ref 防止 React StrictMode 的双重调用 + if (initRef.current) { + console.log('[App] Already initialized via ref, skipping second initialization'); + return; + } + initRef.current = true; + try { + console.log('[App] Starting editor initialization...'); (window as any).__ECS_FRAMEWORK__ = await import('@esengine/ecs-framework'); const editorScene = new Scene(); @@ -84,8 +100,23 @@ function App() { const pluginMgr = new EditorPluginManager(); pluginMgr.initialize(coreInstance, Core.services); + Core.services.registerInstance(EditorPluginManager, pluginMgr); await pluginMgr.installEditor(new SceneInspectorPlugin()); + await pluginMgr.installEditor(new ProfilerPlugin()); + + console.log('[App] All plugins installed'); + console.log('[App] UIRegistry menu count:', uiRegistry.getAllMenus().length); + console.log('[App] UIRegistry all menus:', uiRegistry.getAllMenus()); + console.log('[App] UIRegistry window menus:', uiRegistry.getChildMenus('window')); + + messageHub.subscribe('ui:openWindow', (data: any) => { + if (data.windowId === 'profiler') { + setShowProfiler(true); + } else if (data.windowId === 'pluginManager') { + setShowPluginManager(true); + } + }); const greeting = await TauriAPI.greet('Developer'); console.log(greeting); @@ -95,6 +126,7 @@ function App() { setEntityStore(entityStore); setMessageHub(messageHub); setLogService(logService); + setUiRegistry(uiRegistry); setStatus(t('header.status.ready')); } catch (error) { console.error('Failed to initialize editor:', error); @@ -296,6 +328,9 @@ function App() {
setShowPluginManager(true)} + onOpenProfiler={() => setShowProfiler(true)} + onOpenPortManager={() => setShowPortManager(true)} onToggleDevtools={handleToggleDevtools} />
@@ -330,6 +367,14 @@ function App() { onClose={() => setShowPluginManager(false)} /> )} + + {showProfiler && ( + setShowProfiler(false)} /> + )} + + {showPortManager && ( + setShowPortManager(false)} /> + )}
); } diff --git a/packages/editor-app/src/components/MenuBar.tsx b/packages/editor-app/src/components/MenuBar.tsx index a00ed84c..39a53bbb 100644 --- a/packages/editor-app/src/components/MenuBar.tsx +++ b/packages/editor-app/src/components/MenuBar.tsx @@ -1,4 +1,6 @@ import { useState, useRef, useEffect } from 'react'; +import { UIRegistry, MessageHub, EditorPluginManager } from '@esengine/editor-core'; +import type { MenuItem as PluginMenuItem } from '@esengine/editor-core'; import '../styles/MenuBar.css'; interface MenuItem { @@ -12,6 +14,9 @@ interface MenuItem { interface MenuBarProps { locale?: string; + uiRegistry?: UIRegistry; + messageHub?: MessageHub; + pluginManager?: EditorPluginManager; onNewScene?: () => void; onOpenScene?: () => void; onSaveScene?: () => void; @@ -20,11 +25,16 @@ interface MenuBarProps { onCloseProject?: () => void; onExit?: () => void; onOpenPluginManager?: () => void; + onOpenProfiler?: () => void; + onOpenPortManager?: () => void; onToggleDevtools?: () => void; } export function MenuBar({ locale = 'en', + uiRegistry, + messageHub, + pluginManager, onNewScene, onOpenScene, onSaveScene, @@ -33,11 +43,74 @@ export function MenuBar({ onCloseProject, onExit, onOpenPluginManager, + onOpenProfiler, + onOpenPortManager, onToggleDevtools }: MenuBarProps) { const [openMenu, setOpenMenu] = useState(null); + const [pluginMenuItems, setPluginMenuItems] = useState([]); const menuRef = useRef(null); + const updateMenuItems = () => { + if (uiRegistry && pluginManager) { + const items = uiRegistry.getChildMenus('window'); + // 过滤掉被禁用插件的菜单项 + const enabledPlugins = pluginManager.getAllPluginMetadata() + .filter(p => p.enabled) + .map(p => p.name); + + // 只显示启用插件的菜单项 + const filteredItems = items.filter(item => { + // 检查菜单项是否属于某个插件 + return enabledPlugins.some(pluginName => { + const plugin = pluginManager.getEditorPlugin(pluginName); + if (plugin && plugin.registerMenuItems) { + const pluginMenus = plugin.registerMenuItems(); + return pluginMenus.some(m => m.id === item.id); + } + return false; + }); + }); + + setPluginMenuItems(filteredItems); + console.log('[MenuBar] Updated menu items:', filteredItems); + } else if (uiRegistry) { + // 如果没有 pluginManager,显示所有菜单项 + const items = uiRegistry.getChildMenus('window'); + setPluginMenuItems(items); + console.log('[MenuBar] Updated menu items (no filter):', items); + } + }; + + useEffect(() => { + updateMenuItems(); + }, [uiRegistry, pluginManager]); + + useEffect(() => { + if (messageHub) { + const unsubscribeInstalled = messageHub.subscribe('plugin:installed', () => { + console.log('[MenuBar] Plugin installed, updating menu items'); + updateMenuItems(); + }); + + const unsubscribeEnabled = messageHub.subscribe('plugin:enabled', () => { + console.log('[MenuBar] Plugin enabled, updating menu items'); + updateMenuItems(); + }); + + const unsubscribeDisabled = messageHub.subscribe('plugin:disabled', () => { + console.log('[MenuBar] Plugin disabled, updating menu items'); + updateMenuItems(); + }); + + return () => { + unsubscribeInstalled(); + unsubscribeEnabled(); + unsubscribeDisabled(); + }; + } + }, [messageHub, uiRegistry, pluginManager]); + const t = (key: string) => { const translations: Record> = { en: { @@ -64,6 +137,8 @@ export function MenuBar({ console: 'Console', viewport: 'Viewport', pluginManager: 'Plugin Manager', + tools: 'Tools', + portManager: 'Port Manager', help: 'Help', documentation: 'Documentation', about: 'About', @@ -93,6 +168,8 @@ export function MenuBar({ console: '控制台', viewport: '视口', pluginManager: '插件管理器', + tools: '工具', + portManager: '端口管理器', help: '帮助', documentation: '文档', about: '关于', @@ -133,10 +210,20 @@ export function MenuBar({ { label: t('console'), disabled: true }, { label: t('viewport'), disabled: true }, { separator: true }, + ...pluginMenuItems.map(item => ({ + label: item.label, + shortcut: item.shortcut, + disabled: item.disabled, + onClick: item.onClick + })), + ...(pluginMenuItems.length > 0 ? [{ separator: true }] : []), { label: t('pluginManager'), onClick: onOpenPluginManager }, { separator: true }, { label: t('devtools'), onClick: onToggleDevtools } ], + tools: [ + { label: t('portManager'), onClick: onOpenPortManager } + ], help: [ { label: t('documentation'), disabled: true }, { separator: true }, diff --git a/packages/editor-app/src/components/PortManager.tsx b/packages/editor-app/src/components/PortManager.tsx new file mode 100644 index 00000000..954d3823 --- /dev/null +++ b/packages/editor-app/src/components/PortManager.tsx @@ -0,0 +1,110 @@ +import { useState, useEffect } from 'react'; +import { invoke } from '@tauri-apps/api/core'; +import { X, Server, WifiOff } from 'lucide-react'; +import '../styles/PortManager.css'; + +interface PortManagerProps { + onClose: () => void; +} + +export function PortManager({ onClose }: PortManagerProps) { + const [isServerRunning, setIsServerRunning] = useState(false); + const [serverPort, setServerPort] = useState(8080); + const [isChecking, setIsChecking] = useState(false); + const [isStopping, setIsStopping] = useState(false); + + useEffect(() => { + checkServerStatus(); + }, []); + + const checkServerStatus = async () => { + setIsChecking(true); + try { + const status = await invoke('get_profiler_status'); + setIsServerRunning(status); + } catch (error) { + console.error('[PortManager] Failed to check server status:', error); + setIsServerRunning(false); + } finally { + setIsChecking(false); + } + }; + + const handleStopServer = async () => { + setIsStopping(true); + try { + const result = await invoke('stop_profiler_server'); + console.log('[PortManager]', result); + setIsServerRunning(false); + } catch (error) { + console.error('[PortManager] Failed to stop server:', error); + } finally { + setIsStopping(false); + } + }; + + return ( +
+
e.stopPropagation()}> +
+
+ +

Port Manager

+
+ +
+ +
+
+

Profiler Server

+
+
+ Status: + + {isChecking ? 'Checking...' : isServerRunning ? 'Running' : 'Stopped'} + +
+ {isServerRunning && ( +
+ Port: + {serverPort} +
+ )} +
+ + {isServerRunning && ( +
+ +
+ )} + + {!isServerRunning && ( +
+

No server is currently running.

+

Open Profiler window to start the server.

+
+ )} +
+ +
+

Tips

+
    +
  • Use this when the Profiler server port is stuck and cannot be restarted
  • +
  • The server will automatically stop when the Profiler window is closed
  • +
  • Default port: 8080
  • +
+
+
+
+
+ ); +} diff --git a/packages/editor-app/src/components/ProfilerPanel.tsx b/packages/editor-app/src/components/ProfilerPanel.tsx new file mode 100644 index 00000000..ebc950e0 --- /dev/null +++ b/packages/editor-app/src/components/ProfilerPanel.tsx @@ -0,0 +1,234 @@ +import { useState, useEffect, useRef } from 'react'; +import { Core } from '@esengine/ecs-framework'; +import { Activity, BarChart3, Clock, Cpu, TrendingUp, RefreshCw, Pause, Play } from 'lucide-react'; +import '../styles/ProfilerPanel.css'; + +interface SystemPerformanceData { + name: string; + executionTime: number; + entityCount: number; + averageTime: number; + minTime: number; + maxTime: number; + percentage: number; +} + +export function ProfilerPanel() { + const [systems, setSystems] = useState([]); + const [totalFrameTime, setTotalFrameTime] = useState(0); + const [isPaused, setIsPaused] = useState(false); + const [sortBy, setSortBy] = useState<'time' | 'average' | 'name'>('time'); + const animationRef = useRef(); + + useEffect(() => { + const updateProfilerData = () => { + if (isPaused) { + animationRef.current = requestAnimationFrame(updateProfilerData); + return; + } + + const coreInstance = Core.Instance; + if (!coreInstance || !coreInstance._performanceMonitor?.isEnabled) { + animationRef.current = requestAnimationFrame(updateProfilerData); + return; + } + + const performanceMonitor = coreInstance._performanceMonitor; + const systemDataMap = performanceMonitor.getAllSystemData(); + const systemStatsMap = performanceMonitor.getAllSystemStats(); + + const systemsData: SystemPerformanceData[] = []; + let total = 0; + + for (const [name, data] of systemDataMap.entries()) { + const stats = systemStatsMap.get(name); + if (stats) { + systemsData.push({ + name, + executionTime: data.executionTime, + entityCount: data.entityCount, + averageTime: stats.averageTime, + minTime: stats.minTime, + maxTime: stats.maxTime, + percentage: 0 + }); + total += data.executionTime; + } + } + + // Calculate percentages + systemsData.forEach(system => { + system.percentage = total > 0 ? (system.executionTime / total) * 100 : 0; + }); + + // Sort systems + systemsData.sort((a, b) => { + switch (sortBy) { + case 'time': + return b.executionTime - a.executionTime; + case 'average': + return b.averageTime - a.averageTime; + case 'name': + return a.name.localeCompare(b.name); + default: + return 0; + } + }); + + setSystems(systemsData); + setTotalFrameTime(total); + + animationRef.current = requestAnimationFrame(updateProfilerData); + }; + + animationRef.current = requestAnimationFrame(updateProfilerData); + + return () => { + if (animationRef.current) { + cancelAnimationFrame(animationRef.current); + } + }; + }, [isPaused, sortBy]); + + const handleReset = () => { + const coreInstance = Core.Instance; + if (coreInstance && coreInstance._performanceMonitor) { + coreInstance._performanceMonitor.reset(); + } + }; + + const fps = totalFrameTime > 0 ? Math.round(1000 / totalFrameTime) : 0; + const targetFrameTime = 16.67; // 60 FPS + const isOverBudget = totalFrameTime > targetFrameTime; + + return ( +
+
+
+
+
+ + Frame: + + {totalFrameTime.toFixed(2)}ms + +
+
+ + FPS: + {fps} +
+
+ + Systems: + {systems.length} +
+
+
+
+ + + +
+
+ +
+ {systems.length === 0 ? ( +
+ +

No performance data available

+

+ Make sure Core debug mode is enabled and systems are running +

+
+ ) : ( +
+ {systems.map((system, index) => ( +
+
+
+ #{index + 1} + {system.name} + {system.entityCount > 0 && ( + + ({system.entityCount} entities) + + )} +
+
+ {system.executionTime.toFixed(2)}ms + {system.percentage.toFixed(1)}% +
+
+
+
targetFrameTime + ? 'var(--color-danger)' + : system.executionTime > targetFrameTime * 0.5 + ? 'var(--color-warning)' + : 'var(--color-success)' + }} + /> +
+
+
+ Avg: + {system.averageTime.toFixed(2)}ms +
+
+ Min: + {system.minTime.toFixed(2)}ms +
+
+ Max: + {system.maxTime.toFixed(2)}ms +
+
+
+ ))} +
+ )} +
+ +
+
+
+
+ Good (<8ms) +
+
+
+ Warning (8-16ms) +
+
+
+ Critical (>16ms) +
+
+
+
+ ); +} diff --git a/packages/editor-app/src/components/ProfilerWindow.tsx b/packages/editor-app/src/components/ProfilerWindow.tsx new file mode 100644 index 00000000..43dd014c --- /dev/null +++ b/packages/editor-app/src/components/ProfilerWindow.tsx @@ -0,0 +1,633 @@ +import { useState, useEffect, useRef } from 'react'; +import { Core } from '@esengine/ecs-framework'; +import { Activity, BarChart3, Clock, Cpu, RefreshCw, Pause, Play, X, Wifi, WifiOff, Server, Search, Table2, TreePine } from 'lucide-react'; +import { invoke } from '@tauri-apps/api/core'; +import '../styles/ProfilerWindow.css'; + +interface SystemPerformanceData { + name: string; + executionTime: number; + entityCount: number; + averageTime: number; + minTime: number; + maxTime: number; + percentage: number; + level: number; + children?: SystemPerformanceData[]; + isExpanded?: boolean; +} + +interface ProfilerWindowProps { + onClose: () => void; +} + +type DataSource = 'local' | 'remote'; + +export function ProfilerWindow({ onClose }: ProfilerWindowProps) { + const [systems, setSystems] = useState([]); + const [totalFrameTime, setTotalFrameTime] = useState(0); + const [isPaused, setIsPaused] = useState(false); + const [sortBy, setSortBy] = useState<'time' | 'average' | 'name'>('time'); + const [dataSource, setDataSource] = useState('local'); + const [viewMode, setViewMode] = useState<'tree' | 'table'>('table'); + const [searchQuery, setSearchQuery] = useState(''); + const [wsPort, setWsPort] = useState('8080'); + const [isConnected, setIsConnected] = useState(false); + const [isConnecting, setIsConnecting] = useState(false); + const [connectionError, setConnectionError] = useState(null); + const animationRef = useRef(); + const wsRef = useRef(null); + + // WebSocket connection management + useEffect(() => { + if (dataSource === 'remote' && isConnected && wsRef.current) { + // Keep WebSocket connection alive + const pingInterval = setInterval(() => { + if (wsRef.current?.readyState === WebSocket.OPEN) { + wsRef.current.send(JSON.stringify({ type: 'ping' })); + } + }, 5000); + + return () => clearInterval(pingInterval); + } + }, [dataSource, isConnected]); + + // Cleanup WebSocket and stop server on unmount + useEffect(() => { + return () => { + if (wsRef.current) { + wsRef.current.close(); + wsRef.current = null; + } + + // Check if server is running and stop it + invoke('get_profiler_status') + .then(isRunning => { + if (isRunning) { + return invoke('stop_profiler_server'); + } + }) + .then(() => console.log('[Profiler] Server stopped on unmount')) + .catch(err => console.error('[Profiler] Failed to stop server on unmount:', err)); + }; + }, []); + + const buildSystemTree = (flatSystems: Map, statsMap: Map): SystemPerformanceData[] => { + const coreUpdate = flatSystems.get('Core.update'); + const servicesUpdate = flatSystems.get('Services.update'); + + if (!coreUpdate) return []; + + const coreStats = statsMap.get('Core.update'); + const coreNode: SystemPerformanceData = { + name: 'Core.update', + executionTime: coreUpdate.executionTime, + entityCount: 0, + averageTime: coreStats?.averageTime || 0, + minTime: coreStats?.minTime || 0, + maxTime: coreStats?.maxTime || 0, + percentage: 100, + level: 0, + children: [], + isExpanded: true + }; + + if (servicesUpdate) { + const servicesStats = statsMap.get('Services.update'); + coreNode.children!.push({ + name: 'Services.update', + executionTime: servicesUpdate.executionTime, + entityCount: 0, + averageTime: servicesStats?.averageTime || 0, + minTime: servicesStats?.minTime || 0, + maxTime: servicesStats?.maxTime || 0, + percentage: coreUpdate.executionTime > 0 + ? (servicesUpdate.executionTime / coreUpdate.executionTime) * 100 + : 0, + level: 1, + isExpanded: false + }); + } + + const sceneSystems: SystemPerformanceData[] = []; + let sceneSystemsTotal = 0; + + for (const [name, data] of flatSystems.entries()) { + if (name !== 'Core.update' && name !== 'Services.update') { + const stats = statsMap.get(name); + if (stats) { + sceneSystems.push({ + name, + executionTime: data.executionTime, + entityCount: data.entityCount, + averageTime: stats.averageTime, + minTime: stats.minTime, + maxTime: stats.maxTime, + percentage: 0, + level: 1, + isExpanded: false + }); + sceneSystemsTotal += data.executionTime; + } + } + } + + sceneSystems.forEach(system => { + system.percentage = coreUpdate.executionTime > 0 + ? (system.executionTime / coreUpdate.executionTime) * 100 + : 0; + }); + + sceneSystems.sort((a, b) => b.executionTime - a.executionTime); + coreNode.children!.push(...sceneSystems); + + return [coreNode]; + }; + + useEffect(() => { + if (dataSource !== 'local') return; + + const updateProfilerData = () => { + if (isPaused) { + animationRef.current = requestAnimationFrame(updateProfilerData); + return; + } + + const coreInstance = Core.Instance; + if (!coreInstance || !coreInstance._performanceMonitor?.isEnabled) { + animationRef.current = requestAnimationFrame(updateProfilerData); + return; + } + + const performanceMonitor = coreInstance._performanceMonitor; + const systemDataMap = performanceMonitor.getAllSystemData(); + const systemStatsMap = performanceMonitor.getAllSystemStats(); + + const tree = buildSystemTree(systemDataMap, systemStatsMap); + const coreData = systemDataMap.get('Core.update'); + + setSystems(tree); + setTotalFrameTime(coreData?.executionTime || 0); + + animationRef.current = requestAnimationFrame(updateProfilerData); + }; + + animationRef.current = requestAnimationFrame(updateProfilerData); + + return () => { + if (animationRef.current) { + cancelAnimationFrame(animationRef.current); + } + }; + }, [isPaused, sortBy, dataSource]); + + const handleReset = () => { + if (dataSource === 'local') { + const coreInstance = Core.Instance; + if (coreInstance && coreInstance._performanceMonitor) { + coreInstance._performanceMonitor.reset(); + } + } else { + // Reset remote data + setSystems([]); + setTotalFrameTime(0); + } + }; + + const handleConnect = async () => { + if (isConnecting) return; + + if (wsRef.current) { + wsRef.current.close(); + } + + setIsConnecting(true); + setConnectionError(null); + + try { + const port = parseInt(wsPort); + const result = await invoke('start_profiler_server', { port }); + console.log('[Profiler]', result); + + const ws = new WebSocket(`ws://localhost:${wsPort}`); + + ws.onopen = () => { + console.log('[Profiler] Frontend connected to profiler server'); + setIsConnected(true); + setIsConnecting(false); + setConnectionError(null); + }; + + ws.onclose = () => { + console.log('[Profiler] Frontend disconnected'); + setIsConnected(false); + setIsConnecting(false); + }; + + ws.onerror = (error) => { + console.error('[Profiler] WebSocket error:', error); + setConnectionError(`Failed to connect to profiler server`); + setIsConnected(false); + setIsConnecting(false); + }; + + ws.onmessage = (event) => { + try { + const message = JSON.parse(event.data); + + if (message.type === 'debug_data' && message.data) { + handleRemoteDebugData(message.data); + } else if (message.type === 'pong') { + // Ping-pong response, connection is alive + } + } catch (error) { + console.error('[Profiler] Failed to parse message:', error); + } + }; + + wsRef.current = ws; + } catch (error) { + console.error('[Profiler] Failed to start server:', error); + setConnectionError(String(error)); + setIsConnected(false); + setIsConnecting(false); + } + }; + + const handleDisconnect = async () => { + if (wsRef.current) { + wsRef.current.close(); + wsRef.current = null; + } + + try { + // Stop WebSocket server in Tauri backend + const result = await invoke('stop_profiler_server'); + console.log('[Profiler]', result); + } catch (error) { + console.error('[Profiler] Failed to stop server:', error); + } + + setIsConnected(false); + setSystems([]); + setTotalFrameTime(0); + }; + + const handleRemoteDebugData = (debugData: any) => { + if (isPaused) return; + + const performance = debugData.performance; + if (!performance) return; + + if (!performance.systemPerformance || !Array.isArray(performance.systemPerformance)) { + return; + } + + const flatSystemsMap = new Map(); + const statsMap = new Map(); + + for (const system of performance.systemPerformance) { + flatSystemsMap.set(system.systemName, { + executionTime: system.lastExecutionTime || system.averageTime || 0, + entityCount: system.entityCount || 0 + }); + + statsMap.set(system.systemName, { + averageTime: system.averageTime || 0, + minTime: system.minTime || 0, + maxTime: system.maxTime || 0 + }); + } + + const tree = buildSystemTree(flatSystemsMap, statsMap); + setSystems(tree); + setTotalFrameTime(performance.frameTime || 0); + }; + + const handleDataSourceChange = (newSource: DataSource) => { + if (newSource === 'remote' && dataSource === 'local') { + // Switching to remote + if (animationRef.current) { + cancelAnimationFrame(animationRef.current); + } + } else if (newSource === 'local' && dataSource === 'remote') { + // Switching to local + handleDisconnect(); + } + setDataSource(newSource); + setSystems([]); + setTotalFrameTime(0); + }; + + const toggleExpand = (systemName: string) => { + const toggleNode = (nodes: SystemPerformanceData[]): SystemPerformanceData[] => { + return nodes.map(node => { + if (node.name === systemName) { + return { ...node, isExpanded: !node.isExpanded }; + } + if (node.children) { + return { ...node, children: toggleNode(node.children) }; + } + return node; + }); + }; + setSystems(toggleNode(systems)); + }; + + const flattenTree = (nodes: SystemPerformanceData[]): SystemPerformanceData[] => { + const result: SystemPerformanceData[] = []; + for (const node of nodes) { + result.push(node); + if (node.isExpanded && node.children) { + result.push(...flattenTree(node.children)); + } + } + return result; + }; + + const fps = totalFrameTime > 0 ? Math.round(1000 / totalFrameTime) : 0; + const targetFrameTime = 16.67; + const isOverBudget = totalFrameTime > targetFrameTime; + + let displaySystems = viewMode === 'tree' ? flattenTree(systems) : systems; + + // Apply search filter + if (searchQuery.trim()) { + const query = searchQuery.toLowerCase(); + if (viewMode === 'tree') { + displaySystems = displaySystems.filter(sys => + sys.name.toLowerCase().includes(query) + ); + } else { + // For table view, flatten and filter + const flatList: SystemPerformanceData[] = []; + const flatten = (nodes: SystemPerformanceData[]) => { + for (const node of nodes) { + flatList.push(node); + if (node.children) flatten(node.children); + } + }; + flatten(systems); + displaySystems = flatList.filter(sys => + sys.name.toLowerCase().includes(query) + ); + } + } else if (viewMode === 'table') { + // For table view without search, flatten all + const flatList: SystemPerformanceData[] = []; + const flatten = (nodes: SystemPerformanceData[]) => { + for (const node of nodes) { + flatList.push(node); + if (node.children) flatten(node.children); + } + }; + flatten(systems); + displaySystems = flatList; + } + + return ( +
+
e.stopPropagation()}> +
+
+ +

Performance Profiler

+ {isPaused && ( + PAUSED + )} +
+ +
+ +
+
+
+ + +
+ + {dataSource === 'remote' && ( +
+ setWsPort(e.target.value)} + disabled={isConnected || isConnecting} + /> + {isConnected ? ( + + ) : ( + + )} + {isConnected && ( + Connected + )} + {isConnecting && ( + Connecting... + )} + {connectionError && ( + Error + )} +
+ )} + + {dataSource === 'local' && ( +
+
+ + Frame: + + {totalFrameTime.toFixed(2)}ms + +
+
+ + FPS: + {fps} +
+
+ + Systems: + {systems.length} +
+
+ )} +
+
+
+ + setSearchQuery(e.target.value)} + className="search-input" + /> +
+
+ + +
+ + +
+
+ +
+ {displaySystems.length === 0 ? ( +
+ +

No performance data available

+

+ {searchQuery ? 'No systems match your search' : 'Make sure Core debug mode is enabled and systems are running'} +

+
+ ) : viewMode === 'table' ? ( + + + + + + + + + + + + + + {displaySystems.map((system) => ( + + + + + + + + + + ))} + +
System NameCurrentAverageMinMax%Entities
+ + {system.name} + + + targetFrameTime ? 'critical' : system.executionTime > targetFrameTime * 0.5 ? 'warning' : ''}`}> + {system.executionTime.toFixed(2)}ms + + {system.averageTime.toFixed(2)}ms{system.minTime.toFixed(2)}ms{system.maxTime.toFixed(2)}ms{system.percentage.toFixed(1)}%{system.entityCount || '-'}
+ ) : ( +
+ {displaySystems.map((system) => ( +
+
+
+ {system.children && system.children.length > 0 && ( + + )} + {system.name} + {system.entityCount > 0 && ( + ({system.entityCount}) + )} +
+
+ targetFrameTime ? 'critical' : system.executionTime > targetFrameTime * 0.5 ? 'warning' : ''}`}> + {system.executionTime.toFixed(2)}ms + + {system.percentage.toFixed(1)}% +
+
+
+ Avg: {system.averageTime.toFixed(2)}ms + Min: {system.minTime.toFixed(2)}ms + Max: {system.maxTime.toFixed(2)}ms +
+
+ ))} +
+ )} +
+ +
+
+
+
+ Good (<8ms) +
+
+
+ Warning (8-16ms) +
+
+
+ Critical (>16ms) +
+
+
+
+
+ ); +} diff --git a/packages/editor-app/src/plugins/ProfilerPlugin.tsx b/packages/editor-app/src/plugins/ProfilerPlugin.tsx new file mode 100644 index 00000000..f76e5496 --- /dev/null +++ b/packages/editor-app/src/plugins/ProfilerPlugin.tsx @@ -0,0 +1,48 @@ +import type { Core, ServiceContainer } from '@esengine/ecs-framework'; +import { IEditorPlugin, EditorPluginCategory, MenuItem, MessageHub } from '@esengine/editor-core'; + +/** + * Profiler Plugin + * + * Displays real-time performance metrics for ECS systems + */ +export class ProfilerPlugin implements IEditorPlugin { + readonly name = '@esengine/profiler'; + readonly version = '1.0.0'; + readonly displayName = 'Performance Profiler'; + readonly category = EditorPluginCategory.Tool; + readonly description = 'Real-time performance monitoring for ECS systems'; + readonly icon = '📊'; + + private messageHub: MessageHub | null = null; + + async install(_core: Core, services: ServiceContainer): Promise { + this.messageHub = services.resolve(MessageHub); + console.log('[ProfilerPlugin] Installed'); + } + + async uninstall(): Promise { + console.log('[ProfilerPlugin] Uninstalled'); + } + + async onEditorReady(): Promise { + console.log('[ProfilerPlugin] Editor is ready'); + } + + registerMenuItems(): MenuItem[] { + const items = [ + { + id: 'window.profiler', + label: 'Profiler', + parentId: 'window', + order: 100, + onClick: () => { + console.log('[ProfilerPlugin] Menu item clicked!'); + this.messageHub?.publish('ui:openWindow', { windowId: 'profiler' }); + } + } + ]; + console.log('[ProfilerPlugin] Registering menu items:', items); + return items; + } +} diff --git a/packages/editor-app/src/styles/PortManager.css b/packages/editor-app/src/styles/PortManager.css new file mode 100644 index 00000000..7da9175a --- /dev/null +++ b/packages/editor-app/src/styles/PortManager.css @@ -0,0 +1,242 @@ +.port-manager-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.7); + backdrop-filter: blur(4px); + display: flex; + align-items: center; + justify-content: center; + z-index: var(--z-index-modal); + animation: fadeIn 0.15s ease-out; +} + +.port-manager { + width: 90%; + max-width: 500px; + background: var(--color-bg-elevated); + border: 1px solid var(--color-border-default); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-xl); + display: flex; + flex-direction: column; + animation: slideUp 0.2s ease-out; +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes slideUp { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.port-manager-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px 20px; + border-bottom: 1px solid var(--color-border-default); + background: var(--color-bg-overlay); + border-radius: var(--radius-lg) var(--radius-lg) 0 0; + flex-shrink: 0; +} + +.port-manager-title { + display: flex; + align-items: center; + gap: 12px; + color: var(--color-text-primary); +} + +.port-manager-title h2 { + margin: 0; + font-size: var(--font-size-lg); + font-weight: var(--font-weight-semibold); +} + +.port-manager-close { + padding: 6px; + background: transparent; + border: none; + border-radius: var(--radius-sm); + color: var(--color-text-secondary); + cursor: pointer; + transition: all var(--transition-fast); + display: flex; + align-items: center; + justify-content: center; +} + +.port-manager-close:hover { + background: var(--color-bg-hover); + color: var(--color-text-primary); +} + +.port-manager-content { + padding: 20px; + display: flex; + flex-direction: column; + gap: 24px; +} + +.port-section { + display: flex; + flex-direction: column; + gap: 16px; +} + +.port-section h3 { + margin: 0; + font-size: var(--font-size-md); + font-weight: var(--font-weight-semibold); + color: var(--color-text-primary); +} + +.port-info { + display: flex; + flex-direction: column; + gap: 12px; + padding: 16px; + background: var(--color-bg-inset); + border-radius: var(--radius-md); +} + +.port-item { + display: flex; + align-items: center; + justify-content: space-between; + font-size: var(--font-size-sm); +} + +.port-label { + color: var(--color-text-secondary); + font-weight: var(--font-weight-medium); +} + +.port-status { + padding: 4px 12px; + border-radius: var(--radius-sm); + font-size: 11px; + font-weight: var(--font-weight-semibold); + text-transform: uppercase; +} + +.port-status.running { + background: rgba(34, 197, 94, 0.2); + color: var(--color-success); +} + +.port-status.stopped { + background: rgba(107, 114, 128, 0.2); + color: var(--color-text-tertiary); +} + +.port-value { + font-family: var(--font-family-mono); + font-weight: var(--font-weight-semibold); + color: var(--color-text-primary); +} + +.port-actions { + display: flex; + gap: 8px; +} + +.action-btn { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 8px 16px; + border: none; + border-radius: var(--radius-sm); + font-size: var(--font-size-sm); + font-weight: var(--font-weight-medium); + cursor: pointer; + transition: all var(--transition-fast); +} + +.action-btn.danger { + background: var(--color-danger); + color: white; +} + +.action-btn.danger:hover:not(:disabled) { + background: var(--color-danger-dark); +} + +.action-btn:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.port-hint { + padding: 16px; + background: var(--color-bg-inset); + border-radius: var(--radius-md); + text-align: center; +} + +.port-hint p { + margin: 0; + color: var(--color-text-secondary); + font-size: var(--font-size-sm); +} + +.port-hint p:first-child { + font-weight: var(--font-weight-medium); + margin-bottom: 8px; +} + +.hint-text { + font-size: 12px !important; + color: var(--color-text-tertiary) !important; +} + +.port-tips { + padding: 16px; + background: var(--color-bg-inset); + border-radius: var(--radius-md); +} + +.port-tips h4 { + margin: 0 0 12px 0; + font-size: var(--font-size-sm); + font-weight: var(--font-weight-semibold); + color: var(--color-text-primary); +} + +.port-tips ul { + margin: 0; + padding-left: 20px; + display: flex; + flex-direction: column; + gap: 8px; +} + +.port-tips li { + font-size: 12px; + color: var(--color-text-secondary); + line-height: 1.5; +} + +@media (prefers-reduced-motion: reduce) { + .port-manager-overlay, + .port-manager, + .action-btn, + .port-manager-close { + animation: none; + transition: none; + } +} diff --git a/packages/editor-app/src/styles/ProfilerPanel.css b/packages/editor-app/src/styles/ProfilerPanel.css new file mode 100644 index 00000000..0381cfb5 --- /dev/null +++ b/packages/editor-app/src/styles/ProfilerPanel.css @@ -0,0 +1,303 @@ +.profiler-panel { + display: flex; + flex-direction: column; + height: 100%; + background: var(--color-bg-base); +} + +.profiler-toolbar { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 12px; + background: var(--color-bg-elevated); + border-bottom: 1px solid var(--color-border-default); + flex-shrink: 0; + gap: 12px; +} + +.profiler-toolbar-left { + display: flex; + align-items: center; + gap: 12px; + flex: 1; +} + +.profiler-toolbar-right { + display: flex; + align-items: center; + gap: 8px; +} + +.profiler-stats-summary { + display: flex; + align-items: center; + gap: 20px; +} + +.summary-item { + display: flex; + align-items: center; + gap: 6px; + font-size: 12px; + color: var(--color-text-secondary); +} + +.summary-item svg { + color: var(--color-primary); +} + +.summary-label { + font-weight: 500; +} + +.summary-value { + font-family: var(--font-family-mono); + font-weight: 600; + color: var(--color-text-primary); +} + +.summary-value.over-budget { + color: var(--color-danger); +} + +.summary-value.low-fps { + color: var(--color-warning); +} + +.profiler-sort { + padding: 4px 8px; + background: var(--color-bg-inset); + border: 1px solid var(--color-border-default); + border-radius: var(--radius-sm); + color: var(--color-text-primary); + font-size: 11px; + cursor: pointer; + outline: none; + transition: all var(--transition-fast); +} + +.profiler-sort:hover { + border-color: var(--color-primary); +} + +.profiler-sort:focus { + border-color: var(--color-primary); +} + +.profiler-btn { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 6px; + background: transparent; + border: none; + border-radius: var(--radius-sm); + color: var(--color-text-secondary); + cursor: pointer; + transition: all var(--transition-fast); +} + +.profiler-btn:hover { + background: var(--color-bg-hover); + color: var(--color-text-primary); +} + +.profiler-content { + flex: 1; + overflow-y: auto; + overflow-x: hidden; + padding: 12px; +} + +.profiler-content::-webkit-scrollbar { + width: 8px; +} + +.profiler-content::-webkit-scrollbar-track { + background: var(--color-bg-elevated); +} + +.profiler-content::-webkit-scrollbar-thumb { + background: var(--color-border-default); + border-radius: 4px; +} + +.profiler-content::-webkit-scrollbar-thumb:hover { + background: var(--color-text-secondary); +} + +.profiler-empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + color: var(--color-text-tertiary); + gap: 12px; +} + +.profiler-empty p { + margin: 0; + font-size: 13px; +} + +.profiler-empty-hint { + font-size: 11px !important; + opacity: 0.7; +} + +.profiler-systems { + display: flex; + flex-direction: column; + gap: 12px; +} + +.system-row { + background: var(--color-bg-elevated); + border: 1px solid var(--color-border-default); + border-radius: var(--radius-md); + padding: 12px; + transition: all var(--transition-fast); +} + +.system-row:hover { + border-color: var(--color-border-strong); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.system-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 8px; +} + +.system-info { + display: flex; + align-items: center; + gap: 8px; + flex: 1; +} + +.system-rank { + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + height: 22px; + background: var(--color-bg-inset); + border-radius: var(--radius-sm); + font-size: 11px; + font-weight: 600; + font-family: var(--font-family-mono); + color: var(--color-text-secondary); +} + +.system-name { + font-size: 13px; + font-weight: 600; + color: var(--color-text-primary); + font-family: var(--font-family-mono); +} + +.system-entities { + font-size: 11px; + color: var(--color-text-tertiary); +} + +.system-metrics { + display: flex; + align-items: center; + gap: 12px; +} + +.metric-time { + font-size: 13px; + font-weight: 600; + font-family: var(--font-family-mono); + color: var(--color-text-primary); +} + +.metric-percentage { + font-size: 12px; + font-family: var(--font-family-mono); + color: var(--color-text-secondary); + background: var(--color-bg-inset); + padding: 2px 6px; + border-radius: var(--radius-sm); +} + +.system-bar { + width: 100%; + height: 6px; + background: var(--color-bg-inset); + border-radius: 3px; + overflow: hidden; + margin-bottom: 8px; +} + +.system-bar-fill { + height: 100%; + transition: width 0.3s ease; + border-radius: 3px; +} + +.system-stats { + display: flex; + align-items: center; + gap: 16px; +} + +.stat-item { + display: flex; + align-items: center; + gap: 4px; + font-size: 11px; +} + +.stat-label { + color: var(--color-text-tertiary); +} + +.stat-value { + font-family: var(--font-family-mono); + font-weight: 500; + color: var(--color-text-secondary); +} + +.profiler-footer { + padding: 10px 12px; + background: var(--color-bg-elevated); + border-top: 1px solid var(--color-border-default); + flex-shrink: 0; +} + +.profiler-legend { + display: flex; + align-items: center; + gap: 16px; + justify-content: center; +} + +.legend-item { + display: flex; + align-items: center; + gap: 6px; + font-size: 11px; + color: var(--color-text-secondary); +} + +.legend-color { + width: 12px; + height: 12px; + border-radius: 2px; +} + +@media (prefers-reduced-motion: reduce) { + .system-row, + .system-bar-fill, + .profiler-btn { + transition: none; + } +} diff --git a/packages/editor-app/src/styles/ProfilerWindow.css b/packages/editor-app/src/styles/ProfilerWindow.css new file mode 100644 index 00000000..31ce97bc --- /dev/null +++ b/packages/editor-app/src/styles/ProfilerWindow.css @@ -0,0 +1,848 @@ +.profiler-window-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.7); + backdrop-filter: blur(4px); + display: flex; + align-items: center; + justify-content: center; + z-index: var(--z-index-modal); + animation: fadeIn 0.15s ease-out; +} + +.profiler-window { + width: 90%; + max-width: 900px; + height: 80vh; + max-height: 700px; + background: var(--color-bg-elevated); + border: 1px solid var(--color-border-default); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-xl); + display: flex; + flex-direction: column; + animation: slideUp 0.2s ease-out; +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes slideUp { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.profiler-window-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px 20px; + border-bottom: 1px solid var(--color-border-default); + background: var(--color-bg-overlay); + border-radius: var(--radius-lg) var(--radius-lg) 0 0; + flex-shrink: 0; +} + +.profiler-window-title { + display: flex; + align-items: center; + gap: 12px; + color: var(--color-text-primary); +} + +.profiler-window-title h2 { + margin: 0; + font-size: var(--font-size-lg); + font-weight: var(--font-weight-semibold); +} + +.paused-indicator { + padding: 4px 10px; + background: var(--color-warning); + color: white; + font-size: 10px; + font-weight: 700; + letter-spacing: 0.5px; + border-radius: var(--radius-sm); + animation: pulse 2s ease-in-out infinite; +} + +@keyframes pulse { + 0%, 100% { + opacity: 1; + } + 50% { + opacity: 0.6; + } +} + +.profiler-window-close { + padding: 6px; + background: transparent; + border: none; + border-radius: var(--radius-sm); + color: var(--color-text-secondary); + cursor: pointer; + transition: all var(--transition-fast); + display: flex; + align-items: center; + justify-content: center; +} + +.profiler-window-close:hover { + background: var(--color-bg-hover); + color: var(--color-text-primary); +} + +.profiler-window-toolbar { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 20px; + background: var(--color-bg-elevated); + border-bottom: 1px solid var(--color-border-default); + flex-shrink: 0; + gap: 12px; +} + +.profiler-toolbar-left { + display: flex; + align-items: center; + gap: 12px; + flex: 1; + flex-wrap: wrap; +} + +.profiler-toolbar-right { + display: flex; + align-items: center; + gap: 8px; +} + +.profiler-mode-switch { + display: flex; + align-items: center; + gap: 4px; + background: var(--color-bg-inset); + padding: 2px; + border-radius: var(--radius-sm); +} + +.mode-btn { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 6px 12px; + background: transparent; + border: none; + border-radius: var(--radius-sm); + color: var(--color-text-secondary); + font-size: 12px; + font-weight: 500; + cursor: pointer; + transition: all var(--transition-fast); +} + +.mode-btn:hover { + background: var(--color-bg-hover); + color: var(--color-text-primary); +} + +.mode-btn.active { + background: var(--color-primary); + color: white; +} + +.profiler-connection { + display: flex; + align-items: center; + gap: 8px; +} + +.connection-port { + width: 80px; + padding: 6px 10px; + background: var(--color-bg-inset); + border: 1px solid var(--color-border-default); + border-radius: var(--radius-sm); + color: var(--color-text-primary); + font-size: 12px; + font-family: var(--font-family-mono); + outline: none; + transition: all var(--transition-fast); +} + +.connection-port:hover:not(:disabled) { + border-color: var(--color-primary); +} + +.connection-port:focus { + border-color: var(--color-primary); +} + +.connection-port:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.connection-btn { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 6px 12px; + border: none; + border-radius: var(--radius-sm); + font-size: 12px; + font-weight: 500; + cursor: pointer; + transition: all var(--transition-fast); +} + +.connection-btn.connect { + background: var(--color-success); + color: white; +} + +.connection-btn.connect:hover { + background: var(--color-success-dark); +} + +.connection-btn.disconnect { + background: var(--color-danger); + color: white; +} + +.connection-btn.disconnect:hover { + background: var(--color-danger-dark); +} + +.connection-btn:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.connection-btn.connect:disabled:hover { + background: var(--color-success); +} + +.connection-status { + font-size: 11px; + font-weight: 500; + padding: 4px 8px; + border-radius: var(--radius-sm); +} + +.connection-status.connected { + background: rgba(34, 197, 94, 0.2); + color: var(--color-success); +} + +.connection-status.error { + background: rgba(239, 68, 68, 0.2); + color: var(--color-danger); +} + +.profiler-stats-summary { + display: flex; + align-items: center; + gap: 20px; +} + +.summary-item { + display: flex; + align-items: center; + gap: 6px; + font-size: 12px; + color: var(--color-text-secondary); +} + +.summary-item svg { + color: var(--color-primary); +} + +.summary-label { + font-weight: 500; +} + +.summary-value { + font-family: var(--font-family-mono); + font-weight: 600; + color: var(--color-text-primary); +} + +.summary-value.over-budget { + color: var(--color-danger); +} + +.summary-value.low-fps { + color: var(--color-warning); +} + +.profiler-sort { + padding: 4px 8px; + background: var(--color-bg-inset); + border: 1px solid var(--color-border-default); + border-radius: var(--radius-sm); + color: var(--color-text-primary); + font-size: 11px; + cursor: pointer; + outline: none; + transition: all var(--transition-fast); +} + +.profiler-sort:hover { + border-color: var(--color-primary); +} + +.profiler-sort:focus { + border-color: var(--color-primary); +} + +.profiler-btn { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 6px; + background: transparent; + border: none; + border-radius: var(--radius-sm); + color: var(--color-text-secondary); + cursor: pointer; + transition: all var(--transition-fast); +} + +.profiler-btn:hover { + background: var(--color-bg-hover); + color: var(--color-text-primary); +} + +.profiler-btn.paused { + background: var(--color-warning); + color: white; +} + +.profiler-btn.paused:hover { + background: var(--color-warning-dark); +} + +.profiler-window-content { + flex: 1; + overflow-y: auto; + overflow-x: hidden; + padding: 20px; +} + +.profiler-window-content::-webkit-scrollbar { + width: 8px; +} + +.profiler-window-content::-webkit-scrollbar-track { + background: var(--color-bg-elevated); +} + +.profiler-window-content::-webkit-scrollbar-thumb { + background: var(--color-border-default); + border-radius: 4px; +} + +.profiler-window-content::-webkit-scrollbar-thumb:hover { + background: var(--color-text-secondary); +} + +.profiler-empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + color: var(--color-text-tertiary); + gap: 12px; +} + +.profiler-empty p { + margin: 0; + font-size: 13px; +} + +.profiler-empty-hint { + font-size: 11px !important; + opacity: 0.7; +} + +/* Search */ +.profiler-search { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 12px; + background: var(--color-bg-inset); + border: 1px solid var(--color-border-default); + border-radius: var(--radius-sm); + transition: all var(--transition-fast); +} + +.profiler-search:focus-within { + border-color: var(--color-primary); +} + +.profiler-search svg { + color: var(--color-text-tertiary); +} + +.search-input { + border: none; + background: transparent; + outline: none; + font-size: 12px; + color: var(--color-text-primary); + width: 150px; +} + +.search-input::placeholder { + color: var(--color-text-tertiary); +} + +/* View Mode Switch */ +.view-mode-switch { + display: flex; + align-items: center; + gap: 2px; + background: var(--color-bg-inset); + padding: 2px; + border-radius: var(--radius-sm); +} + +.view-mode-btn { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 6px 8px; + background: transparent; + border: none; + border-radius: var(--radius-sm); + color: var(--color-text-secondary); + cursor: pointer; + transition: all var(--transition-fast); +} + +.view-mode-btn:hover { + background: var(--color-bg-hover); + color: var(--color-text-primary); +} + +.view-mode-btn.active { + background: var(--color-primary); + color: white; +} + +/* Table View */ +.profiler-table { + width: 100%; + border-collapse: collapse; + font-size: 13px; +} + +.profiler-table thead { + position: sticky; + top: 0; + background: var(--color-bg-overlay); + z-index: 1; +} + +.profiler-table th { + text-align: left; + padding: 12px 16px; + font-weight: 600; + color: var(--color-text-secondary); + border-bottom: 2px solid var(--color-border-default); + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.profiler-table tbody tr { + border-bottom: 1px solid var(--color-border-default); + transition: background var(--transition-fast); +} + +.profiler-table tbody tr:hover { + background: var(--color-bg-hover); +} + +.profiler-table tbody tr.level-0 { + background: rgba(99, 102, 241, 0.05); + font-weight: 600; +} + +.profiler-table tbody tr.level-0:hover { + background: rgba(99, 102, 241, 0.1); +} + +.profiler-table td { + padding: 10px 16px; + color: var(--color-text-primary); + font-family: var(--font-family-mono); +} + +.col-name { + width: 40%; +} + +.col-time { + width: 12%; + text-align: right; +} + +.col-percent { + width: 8%; + text-align: right; +} + +.col-entities { + width: 10%; + text-align: right; +} + +.system-name-cell { + display: inline-block; + font-weight: 500; +} + +.time-value { + font-weight: 600; +} + +.time-value.warning { + color: var(--color-warning); +} + +.time-value.critical { + color: var(--color-danger); + font-weight: 700; +} + +/* Tree View */ +.profiler-tree { + display: flex; + flex-direction: column; + gap: 4px; +} + +.tree-row { + background: var(--color-bg-elevated); + border: 1px solid var(--color-border-default); + border-radius: var(--radius-md); + padding: 12px 16px; + transition: all var(--transition-fast); +} + +.tree-row:hover { + background: var(--color-bg-hover); + border-color: var(--color-border-strong); +} + +.tree-row.level-0 { + background: rgba(99, 102, 241, 0.05); + border-color: rgba(99, 102, 241, 0.2); +} + +.tree-row.level-1 { + margin-left: 32px; +} + +.tree-row-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 8px; +} + +.tree-row-left { + display: flex; + align-items: center; + gap: 8px; +} + +.tree-row-right { + display: flex; + align-items: center; + gap: 12px; +} + +.percentage-badge { + font-size: 11px; + font-weight: 600; + padding: 3px 8px; + background: var(--color-bg-inset); + border-radius: var(--radius-sm); + color: var(--color-text-secondary); + font-family: var(--font-family-mono); +} + +.tree-row-stats { + display: flex; + gap: 16px; + font-size: 11px; + color: var(--color-text-tertiary); + font-family: var(--font-family-mono); +} + +.profiler-systems { + display: flex; + flex-direction: column; + gap: 8px; +} + +.system-row { + background: var(--color-bg-elevated); + border: 1px solid var(--color-border-default); + border-radius: var(--radius-md); + padding: 16px; + transition: all var(--transition-fast); + position: relative; +} + +.system-row.level-0 { + background: linear-gradient(135deg, rgba(99, 102, 241, 0.1) 0%, rgba(168, 85, 247, 0.05) 100%); + border: 2px solid rgba(99, 102, 241, 0.3); + box-shadow: 0 4px 12px rgba(99, 102, 241, 0.15); + margin-bottom: 16px; +} + +.system-row.level-1 { + margin-left: 24px; + border-left: 3px solid rgba(99, 102, 241, 0.2); +} + +.system-row.level-1::before { + content: ''; + position: absolute; + left: -24px; + top: 50%; + width: 20px; + height: 2px; + background: rgba(99, 102, 241, 0.2); +} + +.system-row:hover { + border-color: rgba(99, 102, 241, 0.4); + box-shadow: 0 4px 16px rgba(99, 102, 241, 0.2); + transform: translateY(-1px); +} + +.system-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 8px; +} + +.system-info { + display: flex; + align-items: center; + gap: 8px; + flex: 1; +} + +.expand-btn { + display: inline-flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + padding: 0; + background: rgba(99, 102, 241, 0.1); + border: 1px solid rgba(99, 102, 241, 0.2); + border-radius: var(--radius-sm); + color: rgb(99, 102, 241); + cursor: pointer; + transition: all var(--transition-fast); + font-size: 10px; +} + +.expand-btn:hover { + background: rgba(99, 102, 241, 0.2); + border-color: rgba(99, 102, 241, 0.4); + transform: scale(1.1); +} + +.system-rank { + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + height: 22px; + background: var(--color-bg-inset); + border-radius: var(--radius-sm); + font-size: 11px; + font-weight: 600; + font-family: var(--font-family-mono); + color: var(--color-text-secondary); +} + +.system-name { + font-size: 13px; + font-weight: 600; + color: var(--color-text-primary); + font-family: var(--font-family-mono); +} + +.system-row.level-0 .system-name { + font-size: 15px; + font-weight: 700; + color: rgb(99, 102, 241); + text-shadow: 0 1px 2px rgba(99, 102, 241, 0.1); +} + +.system-entities { + font-size: 11px; + color: var(--color-text-tertiary); +} + +.system-metrics { + display: flex; + align-items: center; + gap: 12px; +} + +.metric-time { + font-size: 13px; + font-weight: 600; + font-family: var(--font-family-mono); + color: var(--color-text-primary); +} + +.system-row.level-0 .metric-time { + font-size: 16px; + font-weight: 700; + color: rgb(99, 102, 241); +} + +.metric-percentage { + font-size: 12px; + font-family: var(--font-family-mono); + color: var(--color-text-secondary); + background: var(--color-bg-inset); + padding: 3px 8px; + border-radius: var(--radius-sm); + font-weight: 600; +} + +.system-row.level-0 .metric-percentage { + background: rgba(99, 102, 241, 0.15); + color: rgb(99, 102, 241); + font-size: 13px; + padding: 4px 10px; +} + +.system-bar { + width: 100%; + height: 8px; + background: var(--color-bg-inset); + border-radius: 4px; + overflow: hidden; + margin-bottom: 8px; + box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.system-row.level-0 .system-bar { + height: 10px; + border-radius: 5px; +} + +.system-bar-fill { + height: 100%; + transition: width 0.3s ease, background 0.3s ease; + border-radius: 4px; + position: relative; + overflow: hidden; +} + +.system-bar-fill::after { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(90deg, + rgba(255, 255, 255, 0) 0%, + rgba(255, 255, 255, 0.2) 50%, + rgba(255, 255, 255, 0) 100%); +} + +.system-stats { + display: flex; + align-items: center; + gap: 16px; +} + +.stat-item { + display: flex; + align-items: center; + gap: 4px; + font-size: 11px; +} + +.stat-label { + color: var(--color-text-tertiary); +} + +.stat-value { + font-family: var(--font-family-mono); + font-weight: 500; + color: var(--color-text-secondary); +} + +.profiler-window-footer { + padding: 12px 20px; + background: var(--color-bg-overlay); + border-top: 1px solid var(--color-border-default); + border-radius: 0 0 var(--radius-lg) var(--radius-lg); + flex-shrink: 0; +} + +.profiler-legend { + display: flex; + align-items: center; + gap: 16px; + justify-content: center; +} + +.legend-item { + display: flex; + align-items: center; + gap: 6px; + font-size: 11px; + color: var(--color-text-secondary); +} + +.legend-color { + width: 12px; + height: 12px; + border-radius: 2px; +} + +@media (prefers-reduced-motion: reduce) { + .profiler-window-overlay, + .profiler-window, + .system-row, + .system-bar-fill, + .profiler-btn, + .profiler-window-close { + animation: none; + transition: none; + } +} diff --git a/packages/editor-app/src/styles/design-tokens.css b/packages/editor-app/src/styles/design-tokens.css index 7b0dbdf4..5a6f7ae4 100644 --- a/packages/editor-app/src/styles/design-tokens.css +++ b/packages/editor-app/src/styles/design-tokens.css @@ -4,6 +4,7 @@ --color-bg-elevated: #252526; --color-bg-overlay: #2d2d2d; --color-bg-input: #3c3c3c; + --color-bg-inset: #181818; --color-bg-hover: #2a2d2e; --color-bg-active: #37373d; @@ -29,6 +30,7 @@ --color-success: #4ec9b0; --color-warning: #ce9178; --color-error: #f48771; + --color-danger: #f14c4c; --color-info: #4fc1ff; /* 颜色系统 - 特殊 */ diff --git a/packages/editor-core/src/Plugins/EditorPluginManager.ts b/packages/editor-core/src/Plugins/EditorPluginManager.ts index a9d603f3..c51081c4 100644 --- a/packages/editor-core/src/Plugins/EditorPluginManager.ts +++ b/packages/editor-core/src/Plugins/EditorPluginManager.ts @@ -63,9 +63,12 @@ export class EditorPluginManager extends PluginManager { this.pluginMetadata.set(plugin.name, metadata); try { + console.log('[EditorPluginManager] Checking registerMenuItems:', !!plugin.registerMenuItems); if (plugin.registerMenuItems) { const menuItems = plugin.registerMenuItems(); + console.log('[EditorPluginManager] Got menu items:', menuItems); this.uiRegistry.registerMenus(menuItems); + console.log('[EditorPluginManager] Registered menu items to UIRegistry'); logger.debug(`Registered ${menuItems.length} menu items for ${plugin.name}`); }