import React, { useState, useEffect, useRef, useCallback } from 'react'; import * as ReactDOM from 'react-dom'; import * as ReactJSXRuntime from 'react/jsx-runtime'; import { Core, createLogger, Scene } from '@esengine/ecs-framework'; import * as ECSFramework from '@esengine/ecs-framework'; import { getProfilerService } from './services/getService'; // 将 React 暴露到全局,供动态加载的插件使用 // editor-runtime.js 将 React 设为 external,需要从全局获取 (window as any).React = React; (window as any).ReactDOM = ReactDOM; (window as any).ReactJSXRuntime = ReactJSXRuntime; import { PluginManager, UIRegistry, MessageHub, EntityStoreService, ComponentRegistry, LocaleService, LogService, SettingsRegistry, SceneManagerService, ProjectService, CompilerRegistry, ICompilerRegistry, InspectorRegistry, INotification, CommandManager, BuildService } from '@esengine/editor-core'; import type { IDialogExtended } from './services/TauriDialogService'; import { GlobalBlackboardService } from '@esengine/behavior-tree'; import { ServiceRegistry, PluginInstaller, useDialogStore } from './app/managers'; import { useEditorStore } from './stores'; import { StartupPage } from './components/StartupPage'; import { ProjectCreationWizard } from './components/ProjectCreationWizard'; import { SceneHierarchy } from './components/SceneHierarchy'; import { ContentBrowser } from './components/ContentBrowser'; import { Inspector } from './components/inspectors/Inspector'; import { AssetBrowser } from './components/AssetBrowser'; import { Viewport } from './components/Viewport'; import { AdvancedProfilerWindow } from './components/AdvancedProfilerWindow'; import { RenderDebugPanel } from './components/debug/RenderDebugPanel'; import { emit, emitTo, listen } from '@tauri-apps/api/event'; import { renderDebugService } from './services/RenderDebugService'; import { PortManager } from './components/PortManager'; import { SettingsWindow } from './components/SettingsWindow'; import { AboutDialog } from './components/AboutDialog'; import { ErrorDialog } from './components/ErrorDialog'; import { ConfirmDialog } from './components/ConfirmDialog'; import { ExternalModificationDialog } from './components/ExternalModificationDialog'; import { PluginGeneratorWindow } from './components/PluginGeneratorWindow'; import { BuildSettingsWindow } from './components/BuildSettingsWindow'; import { ForumPanel } from './components/forum'; import { ToastProvider, useToast } from './components/Toast'; import { TitleBar } from './components/TitleBar'; import { MainToolbar } from './components/MainToolbar'; import { FlexLayoutDockContainer, FlexDockPanel, type FlexLayoutDockContainerHandle } from './components/FlexLayoutDockContainer'; import { StatusBar } from './components/StatusBar'; import { TauriAPI } from './api/tauri'; import { SettingsService } from './services/SettingsService'; import { PluginLoader } from './services/PluginLoader'; import { EngineService } from './services/EngineService'; import { CompilerConfigDialog } from './components/CompilerConfigDialog'; import { checkForUpdatesOnStartup } from './utils/updater'; import { useLocale } from './hooks/useLocale'; import { useStoreSubscriptions } from './hooks/useStoreSubscriptions'; import { en, zh, es } from './locales'; import type { Locale } from '@esengine/editor-core'; import { UserCodeService } from '@esengine/editor-core'; import { Loader2 } from 'lucide-react'; import './styles/App.css'; const coreInstance = Core.create({ debug: true }); const localeService = new LocaleService(); localeService.registerTranslations('en', en); localeService.registerTranslations('zh', zh); localeService.registerTranslations('es', es); Core.services.registerInstance(LocaleService, localeService); Core.services.registerSingleton(GlobalBlackboardService); Core.services.registerSingleton(CompilerRegistry); // 在 CompilerRegistry 实例化后,也用 Symbol 注册,用于跨包插件访问 // 注意:registerSingleton 会延迟实例化,所以需要在第一次使用后再注册 Symbol const compilerRegistryInstance = Core.services.resolve(CompilerRegistry); Core.services.registerInstance(ICompilerRegistry, compilerRegistryInstance); const logger = createLogger('App'); // 检查是否为独立窗口模式 | Check if standalone window mode const isFrameDebuggerMode = new URLSearchParams(window.location.search).get('mode') === 'frame-debugger'; function App() { const initRef = useRef(false); const layoutContainerRef = useRef(null); const [pluginLoader] = useState(() => new PluginLoader()); const { showToast, hideToast } = useToast(); // 如果是独立调试窗口模式,只渲染调试面板 | If standalone debugger mode, only render debug panel if (isFrameDebuggerMode) { return (
window.close()} standalone />
); } // ===== 本地初始化状态(只用于初始化阶段)| Local init state (only for initialization) ===== const [initialized, setInitialized] = useState(false); // ===== 从 EditorStore 获取状态 | Get state from EditorStore ===== const { projectLoaded, setProjectLoaded, currentProjectPath, setCurrentProjectPath, availableScenes, setAvailableScenes, isLoading, setIsLoading, loadingMessage, panels, setPanels, activeDynamicPanels, addDynamicPanel, removeDynamicPanel, clearDynamicPanels, dynamicPanelTitles, setDynamicPanelTitle, activePanelId, setActivePanelId, pluginUpdateTrigger, triggerPluginUpdate, isRemoteConnected, setIsRemoteConnected, isContentBrowserDocked, setIsContentBrowserDocked, isEditorFullscreen, setIsEditorFullscreen, status, setStatus, showProjectWizard, setShowProjectWizard, settingsInitialCategory, setSettingsInitialCategory, compilerDialog, openCompilerDialog, closeCompilerDialog, } = useEditorStore(); // ===== 服务实例用 useRef(不触发重渲染)| Service instances use useRef (no re-renders) ===== const pluginManagerRef = useRef(null); const entityStoreRef = useRef(null); const messageHubRef = useRef(null); const inspectorRegistryRef = useRef(null); const logServiceRef = useRef(null); const uiRegistryRef = useRef(null); const settingsRegistryRef = useRef(null); const sceneManagerRef = useRef(null); const notificationRef = useRef(null); const dialogRef = useRef(null); const buildServiceRef = useRef(null); const projectServiceRef = useRef(null); // 兼容层:提供 getter 访问服务 | Compatibility layer: provide getter access to services const pluginManager = pluginManagerRef.current; const entityStore = entityStoreRef.current; const messageHub = messageHubRef.current; const inspectorRegistry = inspectorRegistryRef.current; const logService = logServiceRef.current; const uiRegistry = uiRegistryRef.current; const settingsRegistry = settingsRegistryRef.current; const sceneManager = sceneManagerRef.current; const notification = notificationRef.current; const dialog = dialogRef.current; const buildService = buildServiceRef.current; const projectServiceState = projectServiceRef.current; const [commandManager] = useState(() => new CommandManager()); const { t, locale, changeLocale } = useLocale(); // 初始化 Store 订阅(集中管理 MessageHub 订阅) // Initialize store subscriptions (centrally manage MessageHub subscriptions) useStoreSubscriptions({ messageHub: messageHubRef.current, entityStore: entityStoreRef.current, sceneManager: sceneManagerRef.current, enabled: initialized, }); // 同步 locale 到 TauriDialogService useEffect(() => { if (dialogRef.current) { dialogRef.current.setLocale(locale); } }, [locale]); // ===== 从 DialogStore 获取对话框状态 | Get dialog state from DialogStore ===== const { showProfiler, setShowProfiler, showAdvancedProfiler, setShowAdvancedProfiler, showPortManager, setShowPortManager, showSettings, setShowSettings, showAbout, setShowAbout, showPluginGenerator, setShowPluginGenerator, showBuildSettings, setShowBuildSettings, showRenderDebug, setShowRenderDebug, errorDialog, setErrorDialog, confirmDialog, setConfirmDialog, externalModificationDialog, setExternalModificationDialog } = useDialogStore(); // 全局监听独立调试窗口的数据请求 | Global listener for standalone debug window requests useEffect(() => { let broadcastInterval: ReturnType | null = null; const unlistenPromise = listen('render-debug-request-data', () => { // 开始定时广播数据 | Start broadcasting data periodically if (!broadcastInterval) { const broadcast = () => { renderDebugService.setEnabled(true); const snap = renderDebugService.collectSnapshot(); if (snap) { // 使用 emitTo 发送到独立窗口 | Use emitTo to send to standalone window emitTo('frame-debugger', 'render-debug-snapshot', snap).catch(() => {}); } }; broadcast(); // 立即广播一次 | Broadcast immediately broadcastInterval = setInterval(broadcast, 500); } }); return () => { unlistenPromise.then(unlisten => unlisten()); if (broadcastInterval) { clearInterval(broadcastInterval); } }; }, []); useEffect(() => { // 禁用默认右键菜单 const handleContextMenu = (e: MouseEvent) => { e.preventDefault(); }; document.addEventListener('contextmenu', handleContextMenu); return () => { document.removeEventListener('contextmenu', handleContextMenu); }; }, []); // Global keyboard shortcuts for undo/redo | 全局撤销/重做快捷键 useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { // Skip if user is typing in an input | 如果用户正在输入则跳过 const target = e.target as HTMLElement; if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) { return; } // Ctrl+Z: Undo | 撤销 if (e.ctrlKey && !e.shiftKey && e.key === 'z') { e.preventDefault(); if (commandManager.canUndo()) { commandManager.undo(); } } // Ctrl+Y or Ctrl+Shift+Z: Redo | 重做 else if ((e.ctrlKey && e.key === 'y') || (e.ctrlKey && e.shiftKey && e.key === 'z')) { e.preventDefault(); if (commandManager.canRedo()) { commandManager.redo(); } } }; document.addEventListener('keydown', handleKeyDown); return () => document.removeEventListener('keydown', handleKeyDown); }, [commandManager]); // 快捷键监听 useEffect(() => { const handleKeyDown = async (e: KeyboardEvent) => { if (e.ctrlKey || e.metaKey) { switch (e.key.toLowerCase()) { case 's': { // Skip if any modal/dialog is open // 如果有模态窗口/对话框打开则跳过 const hasModalOpen = showBuildSettings || showSettings || showAbout || showPluginGenerator || showPortManager || showAdvancedProfiler || errorDialog || confirmDialog; if (hasModalOpen) { return; } // Skip if focus is in an input/textarea/contenteditable element // 如果焦点在输入框/文本域/可编辑元素中则跳过 const activeEl = document.activeElement; const isInInput = activeEl instanceof HTMLInputElement || activeEl instanceof HTMLTextAreaElement || activeEl?.getAttribute('contenteditable') === 'true'; if (isInInput) { return; } e.preventDefault(); if (sceneManager) { try { // 检查是否在预制体编辑模式 | Check if in prefab edit mode if (sceneManager.isPrefabEditMode()) { await sceneManager.savePrefab(); const prefabState = sceneManager.getPrefabEditModeState(); showToast(t('editMode.prefab.savedSuccess', { name: prefabState?.prefabName ?? 'Prefab' }), 'success'); } else { await sceneManager.saveScene(); const sceneState = sceneManager.getSceneState(); showToast(t('scene.savedSuccess', { name: sceneState.sceneName }), 'success'); } } catch (error) { console.error('Failed to save:', error); if (sceneManager.isPrefabEditMode()) { showToast(t('editMode.prefab.saveFailed'), 'error'); } else { showToast(t('scene.saveFailed'), 'error'); } } } break; } case 'r': e.preventDefault(); handleReloadPlugins(); break; } } }; document.addEventListener('keydown', handleKeyDown); return () => { document.removeEventListener('keydown', handleKeyDown); }; }, [sceneManager, locale, currentProjectPath, pluginManager, showBuildSettings, showSettings, showAbout, showPluginGenerator, showPortManager, showAdvancedProfiler, errorDialog, confirmDialog]); // 插件和通知订阅 | Plugin and notification subscriptions useEffect(() => { if (!initialized || !messageHubRef.current) return; const hub = messageHubRef.current; const unsubscribeEnabled = hub.subscribe('plugin:enabled', () => { triggerPluginUpdate(); }); const unsubscribeDisabled = hub.subscribe('plugin:disabled', () => { triggerPluginUpdate(); }); const unsubscribeNotification = hub.subscribe('notification:show', (notification: { message: string; type: 'success' | 'error' | 'warning' | 'info'; timestamp: number }) => { if (notification && notification.message) { showToast(notification.message, notification.type); } }); return () => { unsubscribeEnabled(); unsubscribeDisabled(); unsubscribeNotification(); }; }, [initialized, triggerPluginUpdate, showToast]); // 监听远程连接状态 // Monitor remote connection status useEffect(() => { const checkConnection = () => { const profilerService = getProfilerService(); const connected = !!(profilerService && profilerService.isConnected()); setIsRemoteConnected((prevConnected) => { if (connected !== prevConnected) { // 状态发生变化 | State has changed if (connected) { setStatus(t('header.status.remoteConnected')); } else { if (projectLoaded) { const componentRegistry = Core.services.resolve(ComponentRegistry); const componentCount = componentRegistry?.getAllComponents().length || 0; setStatus(t('header.status.projectOpened') + (componentCount > 0 ? ` (${componentCount} components registered)` : '')); } else { setStatus(t('header.status.ready')); } } return connected; } return prevConnected; }); }; const interval = setInterval(checkConnection, 1000); return () => clearInterval(interval); }, [projectLoaded, t]); useEffect(() => { const initializeEditor = async () => { // 使用 ref 防止 React StrictMode 的双重调用 if (initRef.current) { return; } initRef.current = true; try { // ECS Framework 已通过 PluginSDKRegistry 暴露到全局 // ECS Framework is exposed globally via PluginSDKRegistry const editorScene = new Scene(); Core.setScene(editorScene); const serviceRegistry = new ServiceRegistry(); const services = serviceRegistry.registerAllServices(coreInstance); serviceRegistry.setupRemoteLogListener(services.logService); const pluginInstaller = new PluginInstaller(); await pluginInstaller.installBuiltinPlugins(services.pluginManager); // 初始化编辑器模块(安装设置、面板等) await services.pluginManager.initializeEditor(Core.services); services.notification.setCallbacks(showToast, hideToast); (services.dialog as IDialogExtended).setConfirmCallback(setConfirmDialog); services.messageHub.subscribe('ui:openWindow', (data: any) => { const { windowId } = data; if (windowId === 'profiler') { setShowProfiler(true); } else if (windowId === 'advancedProfiler') { setShowAdvancedProfiler(true); } else if (windowId === 'pluginManager') { // 插件管理现在整合到设置窗口中 setSettingsInitialCategory('plugins'); setShowSettings(true); } }); // 设置服务引用(不触发重渲染)| Set service refs (no re-renders) pluginManagerRef.current = services.pluginManager; entityStoreRef.current = services.entityStore; messageHubRef.current = services.messageHub; inspectorRegistryRef.current = services.inspectorRegistry; logServiceRef.current = services.logService; uiRegistryRef.current = services.uiRegistry; settingsRegistryRef.current = services.settingsRegistry; sceneManagerRef.current = services.sceneManager; notificationRef.current = services.notification; dialogRef.current = services.dialog as IDialogExtended; buildServiceRef.current = services.buildService; // 设置初始化完成(触发一次重渲染)| Set initialized (triggers one re-render) setInitialized(true); setStatus(t('header.status.ready')); // Check for updates on startup (after 3 seconds) checkForUpdatesOnStartup(); } catch (error) { console.error('Failed to initialize editor:', error); setStatus(t('header.status.failed')); } }; initializeEditor(); }, []); // 初始化后订阅消息 | Subscribe to messages after initialization useEffect(() => { if (!initialized || !messageHubRef.current) return; const hub = messageHubRef.current; const unsubscribe = hub.subscribe('dynamic-panel:open', (data: any) => { const { panelId, title } = data; logger.info('Opening dynamic panel:', panelId, 'with title:', title); addDynamicPanel(panelId, title); setActivePanelId(panelId); }); return () => unsubscribe?.(); }, [initialized, addDynamicPanel, setActivePanelId]); useEffect(() => { if (!initialized || !messageHubRef.current) return; const hub = messageHubRef.current; const unsubscribe = hub.subscribe('editor:fullscreen', (data: any) => { const { fullscreen } = data; logger.info('Editor fullscreen state changed:', fullscreen); setIsEditorFullscreen(fullscreen); }); return () => unsubscribe?.(); }, [initialized, setIsEditorFullscreen]); useEffect(() => { if (!initialized || !messageHubRef.current) return; const hub = messageHubRef.current; const unsubscribe = hub.subscribe('compiler:open-dialog', (data: { compilerId: string; currentFileName?: string; projectPath?: string; }) => { logger.info('Opening compiler dialog:', data.compilerId); openCompilerDialog(data.compilerId, data.currentFileName); }); return () => unsubscribe?.(); }, [initialized, openCompilerDialog]); // 注册引擎快照请求处理器(用于预制体编辑模式) // Register engine snapshot request handlers (for prefab edit mode) useEffect(() => { if (!initialized || !messageHubRef.current) return; const hub = messageHubRef.current; const unsubscribeSave = hub.onRequest( 'engine:saveSceneSnapshot', async () => { const engineService = EngineService.getInstance(); return engineService.saveSceneSnapshot(); } ); const unsubscribeRestore = hub.onRequest( 'engine:restoreSceneSnapshot', async () => { const engineService = EngineService.getInstance(); return await engineService.restoreSceneSnapshot(); } ); return () => { unsubscribeSave?.(); unsubscribeRestore?.(); }; }, [initialized]); // Handle external scene file changes // 处理外部场景文件变更 useEffect(() => { if (!initialized || !messageHubRef.current || !sceneManagerRef.current) return; const hub = messageHubRef.current; const sm = sceneManagerRef.current; const unsubscribe = hub.subscribe('scene:external-change', (data: { path: string; sceneName: string; }) => { logger.info('Scene externally modified:', data.path); // Show confirmation dialog to reload the scene // 显示确认对话框以重新加载场景 setConfirmDialog({ title: t('scene.externalChange.title'), message: t('scene.externalChange.message', { name: data.sceneName }), confirmText: t('scene.externalChange.reload'), cancelText: t('scene.externalChange.ignore'), onConfirm: async () => { setConfirmDialog(null); try { await sm.openScene(data.path); showToast(t('scene.reloadedSuccess', { name: data.sceneName }), 'success'); } catch (error) { console.error('Failed to reload scene:', error); showToast(t('scene.reloadFailed'), 'error'); } }, onCancel: () => { // User chose to ignore, do nothing // 用户选择忽略,不做任何操作 } }); }); return () => unsubscribe?.(); }, [initialized, t, showToast]); // Handle external modification when saving scene // 处理保存场景时的外部修改检测 useEffect(() => { if (!initialized || !messageHubRef.current || !sceneManagerRef.current) return; const hub = messageHubRef.current; const sm = sceneManagerRef.current; const unsubscribe = hub.subscribe('scene:externalModification', (data: { path: string; sceneName: string; }) => { logger.info('Scene file externally modified during save:', data.path); // Show external modification dialog with three options // 显示外部修改对话框,提供三个选项 setExternalModificationDialog({ sceneName: data.sceneName, onReload: async () => { setExternalModificationDialog(null); try { await sm.reloadScene(); showToast(t('scene.reloadedSuccess', { name: data.sceneName }), 'success'); } catch (error) { console.error('Failed to reload scene:', error); showToast(t('scene.reloadFailed'), 'error'); } }, onOverwrite: async () => { setExternalModificationDialog(null); try { await sm.saveScene(true); // Force save, overwriting external changes showToast(t('scene.savedSuccess', { name: data.sceneName }), 'success'); } catch (error) { console.error('Failed to save scene:', error); showToast(t('scene.saveFailed'), 'error'); } } }); }); return () => unsubscribe?.(); }, [initialized, t, showToast, setExternalModificationDialog]); // Handle user code compilation results // 处理用户代码编译结果 useEffect(() => { if (!initialized || !messageHubRef.current) return; const hub = messageHubRef.current; const unsubscribe = hub.subscribe('usercode:compilation-result', (data: { success: boolean; exports: string[]; errors: string[]; }) => { if (data.success) { if (data.exports.length > 0) { showToast(t('usercode.compileSuccess', { count: data.exports.length }), 'success'); } } else { const errorMsg = data.errors[0] ?? t('usercode.compileError'); showToast(errorMsg, 'error'); } }); return () => unsubscribe?.(); }, [initialized, t, showToast]); const handleOpenRecentProject = async (projectPath: string) => { try { setIsLoading(true, t('loading.step1')); const projectService = Core.services.resolve(ProjectService); if (!projectService) { console.error('Required services not available'); setIsLoading(false); return; } projectServiceRef.current = projectService; await projectService.openProject(projectPath); // 注意:插件配置会在引擎初始化后加载和激活 // Note: Plugin config will be loaded and activated after engine initialization // 设置 Tauri project:// 协议的基础路径(用于加载插件等项目文件) await TauriAPI.setProjectBasePath(projectPath); // 更新项目 tsconfig,直接引用引擎类型定义 // Update project tsconfig to reference engine type definitions directly try { await TauriAPI.updateProjectTsconfig(projectPath); } catch (e) { console.warn('[App] Failed to update project tsconfig:', e); } const settings = SettingsService.getInstance(); settings.addRecentProject(projectPath); setCurrentProjectPath(projectPath); // Scan for available scenes in project // 扫描项目中可用的场景 try { const sceneFiles = await TauriAPI.scanDirectory(`${projectPath}/scenes`, '*.ecs'); const sceneNames = sceneFiles.map(f => `scenes/${f.split(/[\\/]/).pop()}`); setAvailableScenes(sceneNames); } catch (e) { console.warn('[App] Failed to scan scenes:', e); } // 设置 projectLoaded 为 true,触发主界面渲染(包括 Viewport) setProjectLoaded(true); // 等待引擎初始化完成(Viewport 渲染后会触发引擎初始化) setIsLoading(true, t('loading.step2')); const engineService = EngineService.getInstance(); // 等待引擎初始化(最多等待 30 秒,因为需要等待 Viewport 渲染) const engineReady = await engineService.waitForInitialization(30000); if (!engineReady) { throw new Error(t('loading.engineTimeoutError')); } // 加载项目插件配置并激活插件(在引擎初始化后、模块系统初始化前) // Load project plugin config and activate plugins (after engine init, before module system init) if (pluginManagerRef.current) { const pluginSettings = projectService.getPluginSettings(); if (pluginSettings && pluginSettings.enabledPlugins.length > 0) { await pluginManagerRef.current.loadConfig({ enabledPlugins: pluginSettings.enabledPlugins }); } } // 初始化模块系统(所有插件的 runtimeModule 会在 PluginManager 安装时自动注册) await engineService.initializeModuleSystems(); // 应用项目的 UI 设计分辨率 // Apply project's UI design resolution const uiResolution = projectService.getUIDesignResolution(); engineService.setUICanvasSize(uiResolution.width, uiResolution.height); setStatus(t('header.status.projectOpened')); setIsLoading(true, t('loading.step3')); // Wait for user code to be compiled and registered before loading scenes // 等待用户代码编译和注册完成后再加载场景 const userCodeService = Core.services.tryResolve(UserCodeService); if (userCodeService) { await userCodeService.waitForReady(); } const sceneManagerService = Core.services.resolve(SceneManagerService); if (sceneManagerService) { await sceneManagerService.newScene(); } if (pluginManagerRef.current) { setIsLoading(true, t('loading.loadingPlugins')); await pluginLoader.loadProjectPlugins(projectPath, pluginManagerRef.current); } setIsLoading(false); } catch (error) { console.error('Failed to open project:', error); setStatus(t('header.status.failed')); setIsLoading(false); const errorMessage = error instanceof Error ? error.message : String(error); setErrorDialog({ title: t('project.openFailed'), message: `${t('project.openFailed')}:\n${errorMessage}` }); } }; const handleOpenProject = async () => { try { const projectPath = await TauriAPI.openProjectDialog(); if (!projectPath) return; await handleOpenRecentProject(projectPath); } catch (error) { console.error('Failed to open project dialog:', error); } }; const handleCreateProject = () => { setShowProjectWizard(true); }; const handleCreateProjectFromWizard = async (projectName: string, projectPath: string, _templateId: string) => { // 使用与 projectPath 相同的路径分隔符 | Use same separator as projectPath const sep = projectPath.includes('/') ? '/' : '\\'; const fullProjectPath = `${projectPath}${sep}${projectName}`; try { setIsLoading(true, t('project.creating')); const projectService = Core.services.resolve(ProjectService); if (!projectService) { console.error('ProjectService not available'); setIsLoading(false); setErrorDialog({ title: t('project.createFailed'), message: t('project.serviceUnavailable') }); return; } await projectService.createProject(fullProjectPath); setIsLoading(true, t('project.createdOpening')); await handleOpenRecentProject(fullProjectPath); } catch (error) { console.error('Failed to create project:', error); setIsLoading(false); const errorMessage = error instanceof Error ? error.message : String(error); if (errorMessage.includes('already exists')) { setConfirmDialog({ title: t('project.alreadyExists'), message: t('project.existsQuestion'), confirmText: t('project.open'), cancelText: t('common.cancel'), onConfirm: () => { setConfirmDialog(null); setIsLoading(true, t('project.opening')); handleOpenRecentProject(fullProjectPath).catch((err) => { console.error('Failed to open project:', err); setIsLoading(false); setErrorDialog({ title: t('project.openFailed'), message: `${t('project.openFailed')}:\n${err instanceof Error ? err.message : String(err)}` }); }); } }); } else { setStatus(t('project.createFailed')); setErrorDialog({ title: t('project.createFailed'), message: `${t('project.createFailed')}:\n${errorMessage}` }); } } }; const handleBrowseProjectPath = async (): Promise => { try { const path = await TauriAPI.openProjectDialog(); return path || null; } catch (error) { console.error('Failed to browse path:', error); return null; } }; const handleNewScene = async () => { if (!sceneManager) { console.error('SceneManagerService not available'); return; } try { await sceneManager.newScene(); setStatus(t('scene.newCreated')); } catch (error) { console.error('Failed to create new scene:', error); setStatus(t('scene.createFailed')); } }; const handleOpenScene = async () => { if (!sceneManager) { console.error('SceneManagerService not available'); return; } try { // Wait for user code to be ready before loading scene // 在加载场景前等待用户代码就绪 const userCodeService = Core.services.tryResolve(UserCodeService); if (userCodeService) { await userCodeService.waitForReady(); } await sceneManager.openScene(); const sceneState = sceneManager.getSceneState(); setStatus(t('scene.openedSuccess', { name: sceneState.sceneName })); } catch (error) { console.error('Failed to open scene:', error); setStatus(t('scene.openFailed')); } }; const handleOpenSceneByPath = useCallback(async (scenePath: string) => { console.log('[App] handleOpenSceneByPath called:', scenePath); if (!sceneManager) { console.error('SceneManagerService not available'); return; } try { // Wait for user code to be ready before loading scene // 在加载场景前等待用户代码就绪 const userCodeService = Core.services.tryResolve(UserCodeService); if (userCodeService) { console.log('[App] Waiting for user code service...'); await userCodeService.waitForReady(); console.log('[App] User code service ready'); } console.log('[App] Calling sceneManager.openScene...'); await sceneManager.openScene(scenePath); console.log('[App] Scene opened successfully'); const sceneState = sceneManager.getSceneState(); setStatus(t('scene.openedSuccess', { name: sceneState.sceneName })); } catch (error) { console.error('Failed to open scene:', error); setStatus(t('scene.openFailed')); setErrorDialog({ title: t('scene.openFailed'), message: `${t('scene.openFailed')}:\n${error instanceof Error ? error.message : String(error)}` }); } }, [sceneManager, locale]); const handleSaveScene = async () => { if (!sceneManager) { console.error('SceneManagerService not available'); return; } try { await sceneManager.saveScene(); const sceneState = sceneManager.getSceneState(); setStatus(t('scene.savedSuccess', { name: sceneState.sceneName })); } catch (error) { console.error('Failed to save scene:', error); setStatus(t('scene.saveFailed')); } }; const handleSaveSceneAs = async () => { if (!sceneManager) { console.error('SceneManagerService not available'); return; } try { await sceneManager.saveSceneAs(); const sceneState = sceneManager.getSceneState(); setStatus(t('scene.savedSuccess', { name: sceneState.sceneName })); } catch (error) { console.error('Failed to save scene as:', error); setStatus(t('scene.saveAsFailed')); } }; const handleCloseProject = async () => { // 卸载项目插件 if (pluginManager) { await pluginLoader.unloadProjectPlugins(pluginManager); } // 清理场景(会清理所有实体和系统) // Clear scene (clears all entities and systems) const scene = Core.scene; if (scene) { scene.end(); } // 清理模块系统 const engineService = EngineService.getInstance(); engineService.clearModuleSystems(); // 关闭 ProjectService 中的项目 const projectService = Core.services.tryResolve(ProjectService); if (projectService) { await projectService.closeProject(); } setProjectLoaded(false); setCurrentProjectPath(null); setStatus(t('header.status.ready')); }; const handleExit = () => { window.close(); }; const handleLocaleChange = (newLocale: Locale) => { changeLocale(newLocale); // 通知所有已加载的插件更新语言 | Notify all loaded plugins to update locale if (pluginManagerRef.current) { pluginManagerRef.current.setLocale(newLocale); // 通过 MessageHub 通知需要重新获取节点模板 | Notify via MessageHub to refetch node templates if (messageHubRef.current) { messageHubRef.current.publish('locale:changed', { locale: newLocale }); } } }; const handleToggleDevtools = async () => { try { await TauriAPI.toggleDevtools(); } catch (error) { console.error('Failed to toggle devtools:', error); } }; const handleOpenAbout = () => { setShowAbout(true); }; const handleCreatePlugin = () => { setShowPluginGenerator(true); }; const handleReloadPlugins = async () => { if (currentProjectPath && pluginManagerRef.current) { try { // 1. 关闭所有动态面板 | Close all dynamic panels clearDynamicPanels(); // 2. 清空当前面板列表(强制卸载插件面板组件)| Clear panel list (force unmount plugin panels) setPanels((prev) => prev.filter((p) => ['scene-hierarchy', 'inspector', 'console', 'asset-browser'].includes(p.id) )); // 3. 等待React完成卸载 | Wait for React to unmount await new Promise((resolve) => setTimeout(resolve, 200)); // 4. 卸载所有项目插件(清理UIRegistry、调用uninstall)| Unload all project plugins await pluginLoader.unloadProjectPlugins(pluginManagerRef.current); // 5. 等待卸载完成 | Wait for unload await new Promise((resolve) => setTimeout(resolve, 100)); // 6. 重新加载插件 | Reload plugins await pluginLoader.loadProjectPlugins(currentProjectPath, pluginManagerRef.current); // 7. 触发面板重新渲染 | Trigger panel re-render triggerPluginUpdate(); showToast(t('plugin.reloadedSuccess'), 'success'); } catch (error) { console.error('Failed to reload plugins:', error); showToast(t('plugin.reloadFailed'), 'error'); } } }; // ===== 面板构建(拆分依赖减少重建)| Panel building (split deps to reduce rebuilds) ===== // 使用 ref 存储面板构建函数,避免频繁重建 // Use ref to store panel builder function to avoid frequent rebuilds const buildPanelsRef = useRef<() => void>(() => {}); // 更新面板构建函数(不触发重渲染)| Update panel builder (no re-render) buildPanelsRef.current = () => { if (!projectLoaded || !initialized) return; const hub = messageHubRef.current; const store = entityStoreRef.current; const registry = uiRegistryRef.current; const inspReg = inspectorRegistryRef.current; if (!hub || !store || !registry) return; const corePanels: FlexDockPanel[] = [ { id: 'scene-hierarchy', title: t('panel.sceneHierarchy'), content: , closable: false, layout: { position: 'right-top' } }, { id: 'viewport', title: t('panel.viewport'), content: , closable: false, layout: { position: 'center' } }, { id: 'inspector', title: t('panel.inspector'), content: , closable: false, layout: { position: 'right-bottom' } }, { id: 'forum', title: t('panel.forum'), content: , closable: true, layout: { position: 'center' } } ]; // 如果内容管理器已停靠,添加到面板 | If content browser is docked, add to panels if (isContentBrowserDocked) { corePanels.push({ id: 'content-browser', title: t('panel.contentBrowser'), content: ( setIsContentBrowserDocked(false)} /> ), closable: true, layout: { position: 'bottom', weight: 20, requiresSeparateTabset: true } }); } // 获取启用的插件面板 | Get enabled plugin panels const pluginPanels: FlexDockPanel[] = registry.getAllPanels() .filter((panelDesc) => panelDesc.component && !panelDesc.isDynamic) .map((panelDesc) => { const Component = panelDesc.component!; const title = panelDesc.titleKey ? t(panelDesc.titleKey) : panelDesc.title; return { id: panelDesc.id, title, content: , closable: panelDesc.closable ?? true }; }); // 添加激活的动态面板 | Add active dynamic panels const dynamicPanels: FlexDockPanel[] = activeDynamicPanels .filter((panelId) => { const panelDesc = registry.getPanel(panelId); return panelDesc && (panelDesc.component || panelDesc.render); }) .map((panelId) => { const panelDesc = registry.getPanel(panelId)!; const customTitle = dynamicPanelTitles.get(panelId); const defaultTitle = panelDesc.titleKey ? t(panelDesc.titleKey) : panelDesc.title; let content: React.ReactNode; if (panelDesc.component) { const Component = panelDesc.component; content = ; } else if (panelDesc.render) { content = panelDesc.render(); } return { id: panelDesc.id, title: customTitle || defaultTitle, content, closable: panelDesc.closable ?? true }; }); setPanels([...corePanels, ...pluginPanels, ...dynamicPanels]); }; // Effect 1: 项目加载后首次构建面板 | Build panels after project loads useEffect(() => { if (projectLoaded && initialized) { buildPanelsRef.current(); } }, [projectLoaded, initialized]); // Effect 2: 插件更新时重建 | Rebuild on plugin update useEffect(() => { if (projectLoaded && initialized && pluginUpdateTrigger > 0) { buildPanelsRef.current(); } }, [projectLoaded, initialized, pluginUpdateTrigger]); // Effect 3: 动态面板变化时重建 | Rebuild on dynamic panel change useEffect(() => { if (projectLoaded && initialized) { buildPanelsRef.current(); } }, [projectLoaded, initialized, activeDynamicPanels, isContentBrowserDocked]); // Effect 4: 语言变化时更新面板标题(不重建组件)| Update panel titles on locale change (don't rebuild components) useEffect(() => { if (projectLoaded && initialized) { // 只更新标题,不重建组件 | Only update titles, don't rebuild components setPanels((prev) => prev.map(panel => ({ ...panel, title: panel.id === 'scene-hierarchy' ? t('panel.sceneHierarchy') : panel.id === 'viewport' ? t('panel.viewport') : panel.id === 'inspector' ? t('panel.inspector') : panel.id === 'forum' ? t('panel.forum') : panel.id === 'content-browser' ? t('panel.contentBrowser') : panel.title }))); } }, [locale, t, projectLoaded, initialized, setPanels]); if (!initialized) { return (

Loading Editor...

); } if (!projectLoaded) { const settings = SettingsService.getInstance(); const recentProjects = settings.getRecentProjects(); return ( <> { settings.removeRecentProject(projectPath); // 强制重新渲染 | Force re-render setStatus(t('header.status.ready')); }} onDeleteProject={async (projectPath) => { console.log('[App] onDeleteProject called with path:', projectPath); try { console.log('[App] Calling TauriAPI.deleteFolder...'); await TauriAPI.deleteFolder(projectPath); console.log('[App] deleteFolder succeeded'); // 删除成功后从列表中移除并触发重新渲染 // Remove from list and trigger re-render after successful deletion settings.removeRecentProject(projectPath); setStatus(t('header.status.ready')); } catch (error) { console.error('[App] Failed to delete project:', error); setErrorDialog({ title: t('project.deleteFailed'), message: `${t('project.deleteFailed')}:\n${error instanceof Error ? error.message : String(error)}` }); } }} onLocaleChange={handleLocaleChange} recentProjects={recentProjects} /> setShowProjectWizard(false)} onCreateProject={handleCreateProjectFromWizard} onBrowsePath={handleBrowseProjectPath} locale={locale} /> {isLoading && (

{loadingMessage}

)} {errorDialog && ( setErrorDialog(null)} /> )} {confirmDialog && ( { confirmDialog.onConfirm(); setConfirmDialog(null); }} onCancel={() => { if (confirmDialog.onCancel) { confirmDialog.onCancel(); } setConfirmDialog(null); }} /> )} {externalModificationDialog && ( setExternalModificationDialog(null)} /> )} ); } const projectName = currentProjectPath ? currentProjectPath.split(/[\\/]/).pop() : 'Untitled'; return (
{!isEditorFullscreen && ( <> { setSettingsInitialCategory('plugins'); setShowSettings(true); }} onOpenProfiler={() => setShowProfiler(true)} onOpenPortManager={() => setShowPortManager(true)} onOpenSettings={() => setShowSettings(true)} onToggleDevtools={handleToggleDevtools} onOpenAbout={handleOpenAbout} onCreatePlugin={handleCreatePlugin} onReloadPlugins={handleReloadPlugins} onOpenBuildSettings={() => setShowBuildSettings(true)} onOpenRenderDebug={() => setShowRenderDebug(true)} /> )} { if (result.success) { showToast(result.message, 'success'); } else { showToast(result.message, 'error'); } }} />
{ logger.info('Panel closed:', panelId); // 如果关闭的是内容管理器,重置停靠状态 // If closing content browser, reset dock state if (panelId === 'content-browser') { setIsContentBrowserDocked(false); } removeDynamicPanel(panelId); }} />
setIsContentBrowserDocked(true)} onResetLayout={() => layoutContainerRef.current?.resetLayout()} /> {(showProfiler || showAdvancedProfiler) && ( { setShowProfiler(false); setShowAdvancedProfiler(false); }} /> )} {showPortManager && ( setShowPortManager(false)} /> )} {showSettings && settingsRegistry && ( { setShowSettings(false); setSettingsInitialCategory(undefined); }} settingsRegistry={settingsRegistry} initialCategoryId={settingsInitialCategory} /> )} {showAbout && ( setShowAbout(false)} /> )} {showPluginGenerator && ( setShowPluginGenerator(false)} projectPath={currentProjectPath} onSuccess={async () => { if (currentProjectPath && pluginManager) { await pluginLoader.loadProjectPlugins(currentProjectPath, pluginManager); } }} /> )} {showBuildSettings && ( setShowBuildSettings(false)} projectPath={currentProjectPath || undefined} buildService={buildService || undefined} sceneManager={sceneManager || undefined} projectService={projectServiceState || undefined} availableScenes={availableScenes} /> )} {/* 渲染调试面板 | Render Debug Panel */} setShowRenderDebug(false)} /> {errorDialog && ( setErrorDialog(null)} /> )} {confirmDialog && ( { confirmDialog.onConfirm(); setConfirmDialog(null); }} onCancel={() => { if (confirmDialog.onCancel) { confirmDialog.onCancel(); } setConfirmDialog(null); }} /> )} {externalModificationDialog && ( setExternalModificationDialog(null)} /> )}
); } function AppWithToast() { return ( ); } export default AppWithToast;