Files
esengine/packages/editor-app/src/App.tsx
YHH 536c4c5593 refactor(ui): UI 系统架构重构 (#309)
* feat(ui): 动态图集系统与渲染调试增强

## 核心功能

### 动态图集系统 (Dynamic Atlas)
- 新增 DynamicAtlasManager:运行时纹理打包,支持 MaxRects 算法
- 新增 DynamicAtlasService:自动纹理加载与图集管理
- 新增 BinPacker:高效矩形打包算法
- 支持动态/固定两种扩展策略
- 自动 UV 重映射,实现 UI 元素合批渲染

### Frame Debugger 增强
- 新增合批分析面板,显示批次中断原因
- 新增 UI 元素层级信息(depth, worldOrderInLayer)
- 新增实体高亮功能,点击可在场景中定位
- 新增动态图集可视化面板
- 改进渲染原语详情展示

### 闪光效果 (Shiny Effect)
- 新增 UIShinyEffectComponent:UI 闪光参数配置
- 新增 UIShinyEffectSystem:材质覆盖驱动的闪光动画
- 新增 ShinyEffectComponent/System(Sprite 版本)

## 引擎层改进

### Rust 纹理管理扩展
- create_blank_texture:创建空白 GPU 纹理
- update_texture_region:局部纹理更新
- 支持动态图集的 GPU 端操作

### 材质系统
- 新增 effects/ 目录:ShinyEffect 等效果实现
- 新增 interfaces/ 目录:IMaterial 等接口定义
- 新增 mixins/ 目录:可组合的材质功能

### EngineBridge 扩展
- 新增 createBlankTexture/updateTextureRegion 方法
- 改进纹理加载回调机制

## UI 渲染改进
- UIRenderCollector:支持合批调试信息
- 稳定排序:addIndex 保证渲染顺序一致性
- 九宫格渲染优化
- 材质覆盖支持

## 其他改进
- 国际化:新增 Frame Debugger 相关翻译
- 编辑器:新增渲染调试入口
- 文档:新增架构设计文档目录

* refactor(ui): 引入新基础组件架构与渲染工具函数

Phase 1 重构 - 组件职责分离与代码复用:

新增基础组件层:
- UIGraphicComponent: 所有可视 UI 元素的基类(颜色、透明度、raycast)
- UIImageComponent: 纹理显示组件(支持简单、切片、平铺、填充模式)
- UISelectableComponent: 可交互元素的基类(状态管理、颜色过渡)

新增渲染工具:
- UIRenderUtils: 提取共享的坐标计算、边框渲染、阴影渲染等工具函数
- getUIRenderTransform: 统一的变换数据提取
- renderBorder/renderShadow: 复用的边框和阴影渲染逻辑

新增渲染系统:
- UIGraphicRenderSystem: 处理新基础组件的统一渲染器

重构现有系统:
- UIRectRenderSystem: 使用新工具函数,移除重复代码
- UIButtonRenderSystem: 使用新工具函数,移除重复代码

这些改动为后续统一渲染系统奠定基础。

* refactor(ui): UIProgressBarRenderSystem 使用渲染工具函数

- 使用 getUIRenderTransform 替代手动变换计算
- 使用 renderBorder 工具函数替代重复的边框渲染
- 使用 lerpColor 工具函数替代重复的颜色插值
- 简化方法签名,使用 UIRenderTransform 类型
- 移除约 135 行重复代码

* refactor(ui): Slider 和 ScrollView 渲染系统使用工具函数

- UISliderRenderSystem: 使用 getUIRenderTransform,简化方法签名
- UIScrollViewRenderSystem: 使用 getUIRenderTransform,简化方法签名
- 统一使用 UIRenderTransform 类型减少参数传递
- 消除重复的变换计算代码

* refactor(ui): 使用 UIWidgetMarker 消除硬编码组件依赖

- 新增 UIWidgetMarker 标记组件
- UIRectRenderSystem 改为检查标记而非硬编码4种组件类型
- 各 Widget 渲染系统自动添加标记组件
- 减少模块间耦合,提高可扩展性

* feat(ui): 实现 Canvas 隔离机制

- 新增 UICanvasComponent 定义 Canvas 渲染组
- UITransformComponent 添加 Canvas 相关字段:canvasEntityId, worldSortingLayer, pixelPerfect
- UILayoutSystem 传播 Canvas 设置给子元素
- UIRenderUtils 使用 Canvas 继承的排序层
- 支持嵌套 Canvas 和不同渲染模式

* refactor(ui): 统一纹理管理工具函数

Phase 4: 纹理管理统一

新增:
- UITextureUtils.ts: 统一的纹理描述符接口和验证函数
  - UITextureDescriptor: 支持 GUID/textureId/path 多种纹理源
  - isValidTextureGuid: GUID 验证
  - getTextureKey: 获取用于合批的纹理键
  - normalizeTextureDescriptor: 规范化各种输入格式
- utils/index.ts: 工具函数导出

修改:
- UIGraphicRenderSystem: 使用新的纹理工具函数
- index.ts: 导出纹理工具类型和函数

* refactor(ui): 实现统一的脏标记机制

Phase 5: Dirty 标记机制

新增:
- UIDirtyFlags.ts: 位标记枚举和追踪工具
  - UIDirtyFlags: Visual/Layout/Transform/Material/Text 标记
  - IDirtyTrackable: 脏追踪接口
  - DirtyTracker: 辅助工具类
  - 帧级别脏状态追踪 (markFrameDirty, isFrameDirty)

修改:
- UIGraphicComponent: 实现 IDirtyTrackable
  - 属性 setter 自动设置脏标记
  - 保留 setDirty/clearDirty 向后兼容
- UIImageComponent: 所有属性支持脏追踪
  - textureGuid/imageType/fillAmount 等变化自动标记
- UIGraphicRenderSystem: 使用 clearDirtyFlags()

导出:
- UIDirtyFlags, IDirtyTrackable, DirtyTracker
- markFrameDirty, isFrameDirty, clearFrameDirty

* refactor(ui): 移除过时的 dirty flag API

移除 UIGraphicComponent 中的兼容性 API:
- 移除 _isDirty getter/setter
- 移除 setDirty() 方法
- 移除 clearDirty() 方法

现在统一使用新的 dirty flag 系统:
- isDirty() / hasDirtyFlag(flags)
- markDirty(flags) / clearDirtyFlags()

* fix(ui): 修复两个 TODO 功能

1. 滑块手柄命中测试 (UIInputSystem)
   - UISliderComponent 添加 getHandleBounds() 计算手柄边界
   - UISliderComponent 添加 isPointInHandle() 精确命中测试
   - UIInputSystem.handleSlider() 使用精确测试更新悬停状态

2. 径向填充渲染 (UIGraphicRenderSystem)
   - 实现 renderRadialFill() 方法
   - 支持 radial90/radial180/radial360 三种模式
   - 支持 fillOrigin (top/right/bottom/left) 和 fillClockwise
   - 使用多段矩形近似饼形填充效果

* feat(ui): 完善 UI 系统架构和九宫格渲染

* fix(ui): 修复文本渲染层级问题并清理调试代码

- 修复纹理就绪后调用 invalidateUIRenderCaches() 导致的无限循环
- 移除 UITextRenderSystem、UIButtonRenderSystem、UIRectRenderSystem 中的首帧调试输出
- 移除 UILayoutSystem 中的布局调试日志
- 清理所有 __UI_RENDER_DEBUG__ 条件日志

* refactor(ui): 优化渲染批处理和输入框组件

渲染系统:
- 修复 RenderBatcher 保持渲染顺序
- 优化 Rust SpriteBatch 避免合并非连续精灵
- 增强 EngineRenderSystem 纹理就绪检测

输入框组件:
- 增强 UIInputFieldComponent 功能
- 改进 UIInputSystem 输入处理
- 新增 TextMeasureService 文本测量服务

* fix(ui): 修复九宫格首帧渲染和InputField输入问题

- 修复九宫格首帧 size=0x0 问题:
  - Viewport.tsx: 预览模式读取图片尺寸存储到 importSettings
  - AssetDatabase: ISpriteSettings 添加 width/height 字段
  - AssetMetadataService: getTextureSpriteInfo 使用元数据尺寸作为后备
  - UIRectRenderSystem: 当 atlasEntry 不存在时使用 spriteInfo 尺寸
  - WebBuildPipeline: 构建时包含 importSettings
  - AssetManager: 从 catalog 初始化时复制 importSettings
  - AssetTypes: IAssetCatalogEntry 添加 importSettings 字段

- 修复 InputField 无法输入问题:
  - UIRuntimeModule: manifest 添加 pluginExport: 'UIPlugin'
  - 确保预览模式正确加载 UI 插件并绑定 UIInputSystem

- 添加调试日志用于排查纹理加载问题

* fix(sprite): 修复类型导出错误

MaterialPropertyOverride 和 MaterialOverrides 应从 @esengine/material-system 导出

* fix(ui-editor): 补充 AnchorPreset 拉伸预设的映射

添加 StretchTop, StretchMiddle, StretchBottom, StretchLeft, StretchCenter, StretchRight 的位置和锚点值映射
2025-12-19 15:33:36 +08:00

1484 lines
60 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 } 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<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();
// 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();
// 全局监听独立调试窗口的数据请求 | 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(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<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 (
<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}
/>
)}
{/* 渲染调试面板 | 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>
);
}
function AppWithToast() {
return (
<ToastProvider>
<App />
</ToastProvider>
);
}
export default AppWithToast;