Files
esengine/packages/editor-app/src/App.tsx
YHH dbc6793dc4 refactor: 代码规范化与依赖清理 (#317)
* refactor(deps): 统一编辑器包依赖配置 & 优化分层架构

- 将 ecs-engine-bindgen 提升为 Layer 1 核心包
- 统一 9 个编辑器包的依赖声明模式
- 清理废弃的包目录 (ui, ui-editor, network-*)

* refactor(tokens): 修复 PrefabService 令牌冲突 & 补充 module.json

- 将 editor-core 的 PrefabServiceToken 改名为 EditorPrefabServiceToken
  避免与 asset-system 的 PrefabServiceToken 冲突 (Symbol.for 冲突)
- 为 mesh-3d 添加 module.json
- 为 world-streaming 添加 module.json

* refactor(editor-core): 整理导出结构 & 添加 blueprint tokens.ts

- 按功能分组整理 editor-core 的 65 行导出
- 添加清晰的分组注释 (中英双语)
- 为 blueprint 添加占位符 tokens.ts

* chore(editor): 为 14 个编辑器插件包添加 module.json

统一编辑器包的模块配置,包含:
- isEditorPlugin 标识
- runtimeModule 关联
- exports 导出清单 (inspectors, panels, gizmos)

* refactor(core): 改进类型安全 - 减少 as any 使用

- 添加 GlobalTypes.ts 定义小游戏平台和 Chrome API 类型
- SoAStorage 使用 IComponentTypeMetadata 替代 as any
- PlatformDetector 使用类型安全的平台检测
- 添加 ISoAStorageStats/ISoAFieldStats 接口

* feat(editor): 添加 EditorServicesContext 解决 prop drilling

- 新增 contexts/EditorServicesContext.tsx 提供统一服务访问
- App.tsx 包裹 EditorServicesProvider
- 提供 useEditorServices/useMessageHub 等便捷 hooks
- SceneHierarchy 添加迁移注释,后续可移除 props

* docs(editor): 澄清 inspector 目录架构关系

- inspector/ 标记为内部实现,添加 @deprecated 警告
- inspectors/ 标记为公共 API 入口点
- 添加架构说明文档

* refactor(editor): 添加全局类型声明消除 window as any

- 创建 editor-app/src/global.d.ts 声明 Window 接口扩展
- 创建 editor-core/src/global.d.ts 声明 Window 接口扩展
- 更新 App.tsx 使用类型安全的 window 属性访问
- 更新 PluginLoader.ts 使用 window.__ESENGINE_PLUGINS__
- 更新 PluginSDKRegistry.ts 使用 window.__ESENGINE_SDK__
- 更新 UserCodeService.ts 使用类型安全的全局变量访问

* refactor(editor): 提取项目和场景操作到独立 hooks

- 创建 useProjectActions hook 封装项目操作
- 创建 useSceneActions hook 封装场景操作
- 为渐进式重构 App.tsx 做准备

* refactor(editor): 清理冗余代码和未使用文件

删除的目录和文件:
- application/state/ - 重复的状态管理(与 stores/ 重复)
- 8 个孤立 CSS 文件(对应组件不存在)
- AssetBrowser.tsx - 仅为 ContentBrowser 的向后兼容包装
- AssetPicker.tsx - 未被使用
- AssetPickerDialog.tsx (顶级) - 已被 dialogs/ 版本取代
- EntityInspector.tsx (顶级) - 已被 inspectors/views/ 版本取代

修复:
- 移除 App.tsx 中未使用的导入
- 更新 application/index.ts 移除已删除模块
- 修复 useProjectActions.ts 的 MutableRefObject 类型

* refactor(editor): 统一 inspectors 模块导出结构

- 在 inspectors/index.ts 重新导出 PropertyInspector
- 创建 inspectors/fields/index.ts barrel export
- 导出 views、fields、common 子模块
- 更新 EntityInspector 使用统一入口导入

* refactor(editor): 删除废弃的 Profiler 组件

删除未使用的组件(共 1059 行):
- ProfilerPanel.tsx (229 行)
- ProfilerWindow.tsx (589 行)
- ProfilerDockPanel.tsx (241 行)
- ProfilerPanel.css
- ProfilerDockPanel.css

保留:AdvancedProfiler + AdvancedProfilerWindow(正在使用)

* refactor(runtime-core): 统一依赖处理与插件状态管理

- 新增 DependencyUtils 统一拓扑排序和依赖验证
- 新增 PluginState 定义插件生命周期状态机
- 合并 UnifiedPluginLoader 到 PluginLoader
- 清理 index.ts 移除不必要的 Token re-exports
- 新增 RuntimeMode/UserCodeRealm/ImportMapGenerator

* refactor(editor-core): 使用统一的 ImportMapGenerator

- WebBuildPipeline 使用 runtime-core 的 generateImportMap
- UserCodeService 添加 ImportMap 相关接口

* feat(compiler): 增强 esbuild 查找策略

- 支持本地 node_modules、pnpm exec、npx、全局多种来源
- EngineService 使用 RuntimeMode

* refactor(runtime-core): 简化 GameRuntime 代码

- 合并 _disableGameLogicSystems/_enableGameLogicSystems 为 _setGameLogicSystemsEnabled
- 精简本地 Token 定义的注释

* refactor(editor-core): 引入 BaseRegistry 基类消除代码重复

- 新增 BaseRegistry 和 PrioritizedRegistry 基类
- 重构 CompilerRegistry, InspectorRegistry, FieldEditorRegistry
- 统一注册表的日志记录和错误处理

* refactor(editor-core): 扩展 BaseRegistry 重构

- ComponentInspectorRegistry 继承 PrioritizedRegistry
- EditorComponentRegistry 继承 BaseRegistry
- EntityCreationRegistry 继承 BaseRegistry
- PropertyRendererRegistry 继承 PrioritizedRegistry
- 导出 BaseRegistry 基类供外部使用
- 统一双语注释格式

* refactor(editor-core): 代码优雅性优化

CommandManager:
- 提取 tryMergeWithLast() 和 pushToUndoStack() 消除重复代码
- 统一双语注释格式

FileActionRegistry:
- 提取 normalizeExtension() 消除扩展名规范化重复
- 统一私有属性命名风格(_前缀)
- 使用 createRegistryToken 统一 Token 创建

BaseRegistry:
- 添加 IOrdered 接口
- 添加 sortByOrder() 排序辅助方法

EntityCreationRegistry:
- 使用 sortByOrder() 简化排序逻辑

* refactor(editor-core): 统一日志系统 & 代码规范优化

- GizmoRegistry: 使用 createLogger 替代 console.warn
- VirtualNodeRegistry: 使用 createLogger 替代 console.warn
- WindowRegistry: 使用 logger、添加 _ 前缀、导出 IWindowRegistry token
- EditorViewportService: 使用 createLogger 替代 console.warn
- ComponentActionRegistry: 使用 logger、添加 _ 前缀、返回值改进
- SettingsRegistry: 使用 logger、提取 ensureCategory/ensureSection 方法
- 添加 WindowRegistry 到主导出

* refactor(editor-core): ModuleRegistry 使用 logger 替代 console

* refactor(editor-core): SerializerRegistry/UIRegistry 添加 token 和 _ 前缀

* refactor(editor-core): UIRegistry 代码优雅性 & Token 命名统一

- UIRegistry: 提取 _sortByOrder 消除 6 处重复排序逻辑
- UIRegistry: 添加分节注释和双语文档
- FieldEditorRegistry: Token 重命名为 FieldEditorRegistryToken
- PropertyRendererRegistry: Token 重命名为 PropertyRendererRegistryToken

* refactor(core): 统一日志系统 - console 替换为 logger

- ComponentSerializer: 使用 logger 替代 console.warn
- ComponentRegistry: console.warn → logger.warn (已有 logger)
- SceneSerializer: 添加 logger,替换 console.warn/error
- SystemScheduler: 添加 logger,替换 console.warn
- VersionMigration: 添加 logger,替换所有 console.warn
- RuntimeModeService: console.error → logger.error
- Core.ts: _logger 改为 readonly,双语错误消息
- SceneSerializer 修复:使用 getComponentTypeName 替代 constructor.name

* fix(core): 修复 constructor.name 压缩后失效问题

- Scene.ts: 使用 system.systemName 替代 system.constructor.name
- CommandBuffer.ts: 使用 getComponentTypeName() 替代 constructor.name

* refactor(editor-core): 代码规范优化 - 私有方法命名 & 日志统一

- BuildService: console → logger
- FileActionRegistry: 添加 logger, 私有方法 _ 前缀
- SettingsRegistry: 私有方法 _ 前缀 (ensureCategory → _ensureCategory)

* refactor(core): Scene.ts 私有变量命名规范化

- logger → _logger (遵循私有变量 _ 前缀规范)

* refactor(editor-core): 服务类私有成员命名规范化

- CommandManager: 私有变量/方法添加 _ 前缀
  - undoStack/redoStack/config/isExecuting
  - tryMergeWithLast/pushToUndoStack
- LocaleService: 私有变量/方法添加 _ 前缀
  - currentLocale/translations/changeListeners
  - deepMerge/getNestedValue/loadSavedLocale/saveLocale

* refactor(core): 私有成员命名规范化 & 单例模式优化

- Component.ts: _idGenerator 私有静态变量规范化
- PlatformManager.ts: _instance, _adapter, _logger 规范化
- AutoProfiler.ts: _instance, _config 及所有私有方法规范化
- ProfilerSDK.ts: _instance, _config 及所有私有方法规范化
- ComponentPoolManager: _instance, _pools, _usageTracker 规范化
- GlobalEventBus: _instance 规范化
- 添加中英双语 JSDoc 注释

* refactor(editor-app,behavior-tree-editor): 私有成员 & 单例模式命名规范化

editor-app:
- EngineService: private static instance → _instance
- EditorEngineSync: 所有私有成员添加 _ 前缀
- RuntimeResolver: 所有私有成员和方法添加 _ 前缀
- SettingsService: 所有私有成员和方法添加 _ 前缀

behavior-tree-editor:
- GlobalBlackboardService: 所有私有成员和方法添加 _ 前缀
- NotificationService: private static instance → _instance
- NodeRegistryService: 所有私有成员和方法添加 _ 前缀
- TreeStateAdapter: private static instance → _instance

* fix(editor-runtime): 添加 editor-core 到 external 避免传递依赖问题

将 @esengine/editor-core 添加到 vite external 配置,
避免 editor-core → runtime-core → ecs-engine-bindgen 的传递依赖
被错误地打包进 editor-runtime.js,导致 CI 构建失败。

* fix(core): 修复空接口 lint 错误

将 IByteDanceMiniGameAPI、IAlipayMiniGameAPI、IBaiduMiniGameAPI 从空接口改为类型别名,修复 no-empty-object-type 规则报错
2025-12-24 20:57:08 +08:00

1551 lines
63 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useEffect, useRef, useCallback, useMemo } 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.React = React;
window.ReactDOM = ReactDOM;
window.ReactJSXRuntime = ReactJSXRuntime;
import {
PluginManager,
UIRegistry,
MessageHub,
EntityStoreService,
EditorComponentRegistry,
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 { 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 { AssetPickerDialog } from './components/dialogs/AssetPickerDialog';
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 { EditorServicesProvider, type EditorServices } from './contexts';
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<FlexLayoutDockContainerHandle>(null);
const [pluginLoader] = useState(() => new PluginLoader());
const { showToast, hideToast } = useToast();
// 如果是独立调试窗口模式,只渲染调试面板 | If standalone debugger mode, only render debug panel
if (isFrameDebuggerMode) {
return (
<div style={{ width: '100vw', height: '100vh', overflow: 'hidden' }}>
<RenderDebugPanel visible={true} onClose={() => window.close()} standalone />
</div>
);
}
// ===== 本地初始化状态(只用于初始化阶段)| 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<PluginManager | null>(null);
const entityStoreRef = useRef<EntityStoreService | null>(null);
const messageHubRef = useRef<MessageHub | null>(null);
const inspectorRegistryRef = useRef<InspectorRegistry | null>(null);
const logServiceRef = useRef<LogService | null>(null);
const uiRegistryRef = useRef<UIRegistry | null>(null);
const settingsRegistryRef = useRef<SettingsRegistry | null>(null);
const sceneManagerRef = useRef<SceneManagerService | null>(null);
const notificationRef = useRef<INotification | null>(null);
const dialogRef = useRef<IDialogExtended | null>(null);
const buildServiceRef = useRef<BuildService | null>(null);
const projectServiceRef = useRef<ProjectService | null>(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();
// 编辑器服务对象(用于 Context 传递)| Editor services object (for Context)
const editorServices = useMemo<EditorServices>(() => ({
entityStore: entityStoreRef.current,
messageHub: messageHubRef.current,
commandManager,
sceneManager: sceneManagerRef.current,
projectService: projectServiceRef.current,
pluginManager: pluginManagerRef.current,
inspectorRegistry: inspectorRegistryRef.current,
uiRegistry: uiRegistryRef.current,
settingsRegistry: settingsRegistryRef.current,
buildService: buildServiceRef.current,
logService: logServiceRef.current,
notification: notificationRef.current,
dialog: dialogRef.current,
projectPath: currentProjectPath,
}), [
commandManager,
currentProjectPath,
// 注意: refs 不会变化,但为了初始化后更新需要依赖 initialized
initialized,
]);
// Play 模式状态(用于层级面板实时同步)
// Play mode state (for hierarchy panel real-time sync)
const [isPlaying, setIsPlaying] = useState(false);
// 监听 Play 状态变化
// Listen for play state changes
useEffect(() => {
if (!messageHubRef.current || !initialized) return;
const unsubscribe = messageHubRef.current.subscribe('viewport:playState:changed', (data: { isPlaying: boolean }) => {
setIsPlaying(data.isPlaying);
});
return () => unsubscribe();
}, [initialized]);
// 初始化 Store 订阅(集中管理 MessageHub 订阅)
// Initialize store subscriptions (centrally manage MessageHub subscriptions)
useStoreSubscriptions({
messageHub: messageHubRef.current,
entityStore: entityStoreRef.current,
sceneManager: sceneManagerRef.current,
enabled: initialized,
isPlaying,
});
// 同步 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();
// 资产选择器对话框状态 | Asset picker dialog state
const [assetPickerState, setAssetPickerState] = useState<{
isOpen: boolean;
extensions?: string[];
onSelect?: (path: string) => void;
}>({ isOpen: false });
// 全局监听独立调试窗口的数据请求 | Global listener for standalone debug window requests
useEffect(() => {
let broadcastInterval: ReturnType<typeof setInterval> | 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(EditorComponentRegistry);
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]);
// 资产选择器消息订阅 | Asset picker message subscription
useEffect(() => {
if (!initialized || !messageHubRef.current) return;
const hub = messageHubRef.current;
const unsubscribe = hub.subscribe('asset:pick', (data: {
extensions?: string[];
onSelect?: (path: string) => void;
}) => {
logger.info('Opening asset picker dialog with extensions:', data.extensions);
setAssetPickerState({
isOpen: true,
extensions: data.extensions,
onSelect: data.onSelect
});
});
return () => unsubscribe?.();
}, [initialized]);
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<void, boolean>(
'engine:saveSceneSnapshot',
async () => {
const engineService = EngineService.getInstance();
return engineService.saveSceneSnapshot();
}
);
const unsubscribeRestore = hub.onRequest<void, boolean>(
'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<string | null> => {
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: <SceneHierarchy entityStore={store} messageHub={hub} commandManager={commandManager} />,
closable: false,
layout: { position: 'right-top' }
},
{
id: 'viewport',
title: t('panel.viewport'),
content: <Viewport locale={locale} messageHub={hub} commandManager={commandManager} />,
closable: false,
layout: { position: 'center' }
},
{
id: 'inspector',
title: t('panel.inspector'),
content: <Inspector entityStore={store} messageHub={hub} inspectorRegistry={inspReg!} projectPath={currentProjectPath} commandManager={commandManager} />,
closable: false,
layout: { position: 'right-bottom' }
},
{
id: 'forum',
title: t('panel.forum'),
content: <ForumPanel />,
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: (
<ContentBrowser
projectPath={currentProjectPath}
locale={locale}
onOpenScene={handleOpenSceneByPath}
isDrawer={false}
onDockInLayout={() => 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: <Component key={`${panelDesc.id}-${pluginUpdateTrigger}`} projectPath={currentProjectPath} />,
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 = <Component projectPath={currentProjectPath} locale={locale} />;
} 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 (
<div className="editor-loading">
<Loader2 size={32} className="animate-spin" />
<h2>Loading Editor...</h2>
</div>
);
}
if (!projectLoaded) {
const settings = SettingsService.getInstance();
const recentProjects = settings.getRecentProjects();
return (
<>
<StartupPage
onOpenProject={handleOpenProject}
onCreateProject={handleCreateProject}
onOpenRecentProject={handleOpenRecentProject}
onRemoveRecentProject={(projectPath) => {
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}
/>
<ProjectCreationWizard
isOpen={showProjectWizard}
onClose={() => setShowProjectWizard(false)}
onCreateProject={handleCreateProjectFromWizard}
onBrowsePath={handleBrowseProjectPath}
locale={locale}
/>
{isLoading && (
<div className="loading-overlay">
<div className="loading-content">
<Loader2 size={40} className="animate-spin" />
<p className="loading-message">{loadingMessage}</p>
</div>
</div>
)}
{errorDialog && (
<ErrorDialog
title={errorDialog.title}
message={errorDialog.message}
onClose={() => setErrorDialog(null)}
/>
)}
{confirmDialog && (
<ConfirmDialog
title={confirmDialog.title}
message={confirmDialog.message}
confirmText={confirmDialog.confirmText}
cancelText={confirmDialog.cancelText}
onConfirm={() => {
confirmDialog.onConfirm();
setConfirmDialog(null);
}}
onCancel={() => {
if (confirmDialog.onCancel) {
confirmDialog.onCancel();
}
setConfirmDialog(null);
}}
/>
)}
{externalModificationDialog && (
<ExternalModificationDialog
sceneName={externalModificationDialog.sceneName}
onReload={externalModificationDialog.onReload}
onOverwrite={externalModificationDialog.onOverwrite}
onCancel={() => setExternalModificationDialog(null)}
/>
)}
</>
);
}
const projectName = currentProjectPath ? currentProjectPath.split(/[\\/]/).pop() : 'Untitled';
return (
<EditorServicesProvider services={editorServices}>
<div className="editor-container">
{!isEditorFullscreen && (
<>
<TitleBar
projectName={projectName}
uiRegistry={uiRegistry || undefined}
messageHub={messageHub || undefined}
pluginManager={pluginManager || undefined}
onNewScene={handleNewScene}
onOpenScene={handleOpenScene}
onSaveScene={handleSaveScene}
onSaveSceneAs={handleSaveSceneAs}
onOpenProject={handleOpenProject}
onCloseProject={handleCloseProject}
onExit={handleExit}
onOpenPluginManager={() => {
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)}
/>
<MainToolbar
messageHub={messageHub || undefined}
commandManager={commandManager}
onSaveScene={handleSaveScene}
onOpenScene={handleOpenScene}
/>
</>
)}
<CompilerConfigDialog
isOpen={compilerDialog.isOpen}
compilerId={compilerDialog.compilerId}
projectPath={currentProjectPath}
currentFileName={compilerDialog.currentFileName}
onClose={closeCompilerDialog}
onCompileComplete={(result) => {
if (result.success) {
showToast(result.message, 'success');
} else {
showToast(result.message, 'error');
}
}}
/>
<div className="editor-content">
<FlexLayoutDockContainer
ref={layoutContainerRef}
panels={panels}
activePanelId={activePanelId}
messageHub={messageHubRef.current}
onPanelClose={(panelId) => {
logger.info('Panel closed:', panelId);
// 如果关闭的是内容管理器,重置停靠状态
// If closing content browser, reset dock state
if (panelId === 'content-browser') {
setIsContentBrowserDocked(false);
}
removeDynamicPanel(panelId);
}}
/>
</div>
<StatusBar
pluginCount={pluginManager?.getAllPlugins().length ?? 0}
entityCount={entityStore?.getAllEntities().length ?? 0}
messageHub={messageHub}
logService={logService}
locale={locale}
projectPath={currentProjectPath}
onOpenScene={handleOpenSceneByPath}
onDockContentBrowser={() => setIsContentBrowserDocked(true)}
onResetLayout={() => layoutContainerRef.current?.resetLayout()}
/>
{(showProfiler || showAdvancedProfiler) && (
<AdvancedProfilerWindow onClose={() => {
setShowProfiler(false);
setShowAdvancedProfiler(false);
}} />
)}
{showPortManager && (
<PortManager onClose={() => setShowPortManager(false)} />
)}
{showSettings && settingsRegistry && (
<SettingsWindow
onClose={() => {
setShowSettings(false);
setSettingsInitialCategory(undefined);
}}
settingsRegistry={settingsRegistry}
initialCategoryId={settingsInitialCategory}
/>
)}
{showAbout && (
<AboutDialog onClose={() => setShowAbout(false)} />
)}
{showPluginGenerator && (
<PluginGeneratorWindow
onClose={() => setShowPluginGenerator(false)}
projectPath={currentProjectPath}
onSuccess={async () => {
if (currentProjectPath && pluginManager) {
await pluginLoader.loadProjectPlugins(currentProjectPath, pluginManager);
}
}}
/>
)}
{showBuildSettings && (
<BuildSettingsWindow
onClose={() => setShowBuildSettings(false)}
projectPath={currentProjectPath || undefined}
buildService={buildService || undefined}
sceneManager={sceneManager || undefined}
projectService={projectServiceState || undefined}
availableScenes={availableScenes}
/>
)}
{/* 资产选择器对话框 | Asset Picker Dialog */}
<AssetPickerDialog
isOpen={assetPickerState.isOpen}
onClose={() => setAssetPickerState({ isOpen: false })}
onSelect={(path) => {
if (assetPickerState.onSelect) {
assetPickerState.onSelect(path);
}
setAssetPickerState({ isOpen: false });
}}
title={t('asset.selectAsset')}
fileExtensions={assetPickerState.extensions}
/>
{/* 渲染调试面板 | Render Debug Panel */}
<RenderDebugPanel
visible={showRenderDebug}
onClose={() => setShowRenderDebug(false)}
/>
{errorDialog && (
<ErrorDialog
title={errorDialog.title}
message={errorDialog.message}
onClose={() => setErrorDialog(null)}
/>
)}
{confirmDialog && (
<ConfirmDialog
title={confirmDialog.title}
message={confirmDialog.message}
confirmText={confirmDialog.confirmText}
cancelText={confirmDialog.cancelText}
onConfirm={() => {
confirmDialog.onConfirm();
setConfirmDialog(null);
}}
onCancel={() => {
if (confirmDialog.onCancel) {
confirmDialog.onCancel();
}
setConfirmDialog(null);
}}
/>
)}
{externalModificationDialog && (
<ExternalModificationDialog
sceneName={externalModificationDialog.sceneName}
onReload={externalModificationDialog.onReload}
onOverwrite={externalModificationDialog.onOverwrite}
onCancel={() => setExternalModificationDialog(null)}
/>
)}
</div>
</EditorServicesProvider>
);
}
function AppWithToast() {
return (
<ToastProvider>
<App />
</ToastProvider>
);
}
export default AppWithToast;