feat: 预制体系统与架构改进 (#303)
* feat(prefab): 实现预制体系统和编辑器 UX 改进 ## 预制体系统 - 新增 PrefabSerializer: 预制体序列化/反序列化 - 新增 PrefabInstanceComponent: 追踪预制体实例来源和修改 - 新增 PrefabService: 预制体核心服务 - 新增 PrefabLoader: 预制体资产加载器 - 新增预制体命令: Create/Instantiate/Apply/Revert/BreakLink ## 预制体编辑模式 - 支持双击 .prefab 文件进入编辑模式 - 预制体编辑模式工具栏 (保存/退出) - 预制体实例指示器和操作菜单 ## 编辑器 UX 改进 - SceneHierarchy 快捷键: F2 重命名, Ctrl+D 复制, ↑↓ 导航 - 支持双击实体名称内联编辑 - 删除实体时显示子节点数量警告 - 右键菜单添加重命名/复制选项及快捷键提示 - 布局持久化和重置功能 ## Bug 修复 - 修复 editor-runtime 组件类重复导致的 TransformComponent 不识别问题 - 修复 .prefab-name 样式覆盖导致预制体工具栏文字不可见 - 修复 Inspector 资源字段高度不正确问题 * feat(editor): 改进编辑器 UX 交互体验 - ContentBrowser: 加载动画 spinner、搜索高亮、改进空状态设计 - SceneHierarchy: 选中项自动滚动到视图、搜索清除按钮 - PropertyInspector: 输入框本地状态管理、Enter/Escape 键处理 - EntityInspector: 组件折叠状态持久化、属性搜索清除按钮 - Viewport: 变换操作实时数值显示 - 国际化: 添加相关文本 (en/zh) * fix(build): 修复 Web 构建资产加载和编辑器 UX 改进 构建系统修复: - 修复 asset-catalog.json 字段名不匹配 (entries vs assets) - 修复 BrowserFileSystemService 支持两种目录格式 - 修复 bundle 策略检测逻辑 (空对象判断) - 修复 module.json 中 assetExtensions 声明和类型推断 行为树修复: - 修复 BehaviorTreeExecutionSystem 使用 loadAsset 替代 loadAssetByPath - 修复 BehaviorTreeAssetType 常量与 module.json 类型名一致 (behavior-tree) 编辑器 UX 改进: - 构建完成对话框添加"打开文件夹"按钮 - 构建完成对话框样式优化 (圆形图标背景、按钮布局) - SceneHierarchy 响应式布局 (窄窗口自动隐藏 Type 列) - SceneHierarchy 隐藏滚动条 错误追踪: - 添加全局错误处理器写入日志文件 (%TEMP%/esengine-editor-crash.log) - 添加 append_to_log Tauri 命令 * feat(render): 修复 UI 渲染和点击特效系统 ## UI 渲染修复 - 修复 GUID 验证 bug,使用统一的 isValidGUID() 函数 - 修复 UI 渲染顺序随机问题,Rust 端使用 IndexMap 替代 HashMap - Web 运行时添加 assetPathResolver 支持 GUID 解析 - UIInteractableComponent.blockEvents 默认值改为 false ## 点击特效系统 - 新增 ClickFxComponent 和 ClickFxSystem - 支持在点击位置播放粒子效果 - 支持多种触发模式和粒子轮换 ## Camera 系统重构 - CameraSystem 从 ecs-engine-bindgen 移至 camera 包 - 新增 CameraManager 统一管理相机 ## 编辑器改进 - 改进属性面板 UI 交互 - 粒子编辑器面板优化 - Transform 命令系统 * feat(render): 实现 Sorting Layer 系统和 Overlay 渲染层 - 新增 SortingLayerManager 管理排序层级 (Background, Default, Foreground, UI, Overlay) - 实现 ISortable 接口,统一 Sprite、UI、Particle 的排序属性 - 修复粒子 Overlay 层被 UI 遮挡问题:添加独立的 Overlay Pass 在 UI 之后渲染 - 更新粒子资产格式:从 sortingOrder 改为 sortingLayer + orderInLayer - 更新粒子编辑器面板支持新的排序属性 - 优化 UI 渲染系统使用新的排序层级 * feat(ci): 集成 SignPath 代码签名服务 - 添加 SignPath 自动签名工作流(Windows) - 配置 release-editor.yml 支持代码签名 - 将构建改为草稿模式,等待签名完成后发布 - 添加证书文件到 .gitignore 防止泄露 * fix(asset): 修复 Web 构建资产路径解析和全局单例移除 ## 资产路径修复 - 修复 Tauri 本地服务器 `/asset?path=...` 路径解析,正确与 root 目录连接 - BrowserPathResolver 支持两种模式: - 'proxy': 使用 /asset?path=... 格式(编辑器 Run in Browser) - 'direct': 使用直接路径 /assets/path.png(独立 Web 构建) - BrowserRuntime 使用 'direct' 模式,无需 Tauri 代理 ## 架构改进 - 移除全局单例 - 移除 globalAssetManager 导出,改用 AssetManagerToken 依赖注入 - 移除 globalPathResolver 导出,改用 PathResolutionService - 移除 globalPathResolutionService 导出 - ParticleUpdateSystem/ClickFxSystem 通过 setAssetManager() 注入依赖 - EngineService 使用 new AssetManager() 替代全局实例 ## 新增服务 - PathResolutionService: 统一路径解析接口 - RuntimeModeService: 运行时模式查询服务 - SerializationContext: EntityRef 序列化上下文 ## 其他改进 - 完善 ServiceToken 注释说明本地定义的意图 - 导出 BrowserPathResolveMode 类型 * fix(build): 添加 world-streaming composite 设置修复类型检查 * fix(build): 移除 world-streaming 引用避免 composite 冲突 * fix(build): 将 const enum 改为 enum 兼容 isolatedModules * fix(build): 添加缺失的 IAssetManager 导入
This commit is contained in:
@@ -7,9 +7,9 @@
|
||||
*/
|
||||
|
||||
import { GizmoRegistry, EntityStoreService, MessageHub, SceneManagerService, ProjectService, PluginManager, IPluginManager, AssetRegistryService, type SystemContext } from '@esengine/editor-core';
|
||||
import { Core, Scene, Entity, SceneSerializer, ProfilerSDK, createLogger } from '@esengine/ecs-framework';
|
||||
import { Core, Scene, Entity, SceneSerializer, ProfilerSDK, createLogger, PluginServiceRegistry } from '@esengine/ecs-framework';
|
||||
import { CameraConfig, EngineBridgeToken, RenderSystemToken, EngineIntegrationToken } from '@esengine/ecs-engine-bindgen';
|
||||
import { TransformComponent, PluginServiceRegistry, TransformTypeToken } from '@esengine/engine-core';
|
||||
import { TransformComponent, TransformTypeToken, CanvasElementToken } from '@esengine/engine-core';
|
||||
import { SpriteComponent, SpriteAnimatorComponent, SpriteAnimatorSystemToken } from '@esengine/sprite';
|
||||
import { invalidateUIRenderCaches, UIRenderProviderToken, UIInputSystemToken } from '@esengine/ui';
|
||||
import * as esEngine from '@esengine/engine';
|
||||
@@ -18,11 +18,10 @@ import {
|
||||
EngineIntegration,
|
||||
AssetPathResolver,
|
||||
AssetPlatform,
|
||||
globalPathResolver,
|
||||
SceneResourceManager,
|
||||
assetManager as globalAssetManager,
|
||||
AssetType,
|
||||
AssetManagerToken
|
||||
AssetManagerToken,
|
||||
isValidGUID
|
||||
} from '@esengine/asset-system';
|
||||
import {
|
||||
GameRuntime,
|
||||
@@ -33,6 +32,7 @@ import {
|
||||
import { BehaviorTreeSystemToken } from '@esengine/behavior-tree';
|
||||
import { Physics2DSystemToken } from '@esengine/physics-rapier2d';
|
||||
import { getMaterialManager } from '@esengine/material-system';
|
||||
import { WebInputSubsystem } from '@esengine/platform-web';
|
||||
import { resetEngineState } from '../hooks/useEngine';
|
||||
import { convertFileSrc } from '@tauri-apps/api/core';
|
||||
import { IdGenerator } from '../utils/idGenerator';
|
||||
@@ -120,13 +120,17 @@ export class EngineService {
|
||||
};
|
||||
|
||||
// 创建编辑器平台适配器
|
||||
// Create editor platform adapter
|
||||
const platform = new EditorPlatformAdapter({
|
||||
wasmModule: esEngine,
|
||||
pathTransformer,
|
||||
gizmoDataProvider: (component, entity, isSelected) =>
|
||||
GizmoRegistry.getGizmoData(component, entity, isSelected),
|
||||
hasGizmoProvider: (component) =>
|
||||
GizmoRegistry.hasProvider(component.constructor as any)
|
||||
GizmoRegistry.hasProvider(component.constructor as any),
|
||||
// 提供输入子系统用于 Play 模式下的游戏输入
|
||||
// Provide input subsystem for game input in Play mode
|
||||
inputSubsystemFactory: () => new WebInputSubsystem()
|
||||
});
|
||||
|
||||
// 创建统一运行时
|
||||
@@ -212,6 +216,13 @@ export class EngineService {
|
||||
services.register(EngineIntegrationToken, this._engineIntegration);
|
||||
services.register(TransformTypeToken, TransformComponent);
|
||||
|
||||
// 注册 Canvas 元素(用于坐标转换等)
|
||||
// Register canvas element (for coordinate conversion, etc.)
|
||||
const canvas = this._runtime.platform.getCanvas();
|
||||
if (canvas) {
|
||||
services.register(CanvasElementToken, canvas);
|
||||
}
|
||||
|
||||
// 创建系统上下文
|
||||
const context: SystemContext = {
|
||||
isEditor: true,
|
||||
@@ -225,6 +236,10 @@ export class EngineService {
|
||||
// 插件注册完加载器后,重新同步资产(确保类型正确)
|
||||
await this._syncAssetRegistryToManager();
|
||||
|
||||
// Subscribe to asset changes to sync new assets to runtime
|
||||
// 订阅资产变化以将新资产同步到运行时
|
||||
this._subscribeToAssetChanges();
|
||||
|
||||
// 同步服务注册表到 GameRuntime(用于 start/stop 时启用/禁用系统)
|
||||
// Sync service registry to GameRuntime (for enabling/disabling systems on start/stop)
|
||||
const runtimeServices = this._runtime.getServiceRegistry();
|
||||
@@ -272,6 +287,10 @@ export class EngineService {
|
||||
* 清理模块系统
|
||||
*/
|
||||
clearModuleSystems(): void {
|
||||
// Unsubscribe from asset change events
|
||||
// 取消订阅资产变化事件
|
||||
this._unsubscribeFromAssetChanges();
|
||||
|
||||
const pluginManager = Core.services.tryResolve<PluginManager>(IPluginManager);
|
||||
if (pluginManager) {
|
||||
pluginManager.clearSceneSystems();
|
||||
@@ -394,9 +413,9 @@ export class EngineService {
|
||||
*/
|
||||
private async _initializeAssetSystem(): Promise<void> {
|
||||
try {
|
||||
// Use global assetManager instance so all systems share the same manager
|
||||
// 使用全局 assetManager 实例,以便所有系统共享同一个管理器
|
||||
this._assetManager = globalAssetManager;
|
||||
// Create a new AssetManager instance for this editor session
|
||||
// 为此编辑器会话创建新的 AssetManager 实例
|
||||
this._assetManager = new AssetManager();
|
||||
|
||||
// Set up asset reader for Tauri environment.
|
||||
// 为 Tauri 环境设置资产读取器。
|
||||
@@ -413,8 +432,8 @@ export class EngineService {
|
||||
}
|
||||
}
|
||||
|
||||
// Sync AssetRegistryService data to global assetManager's database
|
||||
// 将 AssetRegistryService 的数据同步到全局 assetManager 的数据库
|
||||
// Sync AssetRegistryService data to assetManager's database
|
||||
// 将 AssetRegistryService 的数据同步到 assetManager 的数据库
|
||||
await this._syncAssetRegistryToManager();
|
||||
|
||||
const pathTransformerFn = (path: string) => {
|
||||
@@ -440,11 +459,6 @@ export class EngineService {
|
||||
pathTransformer: pathTransformerFn
|
||||
});
|
||||
|
||||
globalPathResolver.updateConfig({
|
||||
platform: AssetPlatform.Editor,
|
||||
pathTransformer: pathTransformerFn
|
||||
});
|
||||
|
||||
if (this._runtime?.bridge) {
|
||||
this._engineIntegration = new EngineIntegration(this._assetManager, this._runtime.bridge);
|
||||
|
||||
@@ -565,6 +579,103 @@ export class EngineService {
|
||||
logger.debug('Asset sync complete');
|
||||
}
|
||||
|
||||
/** Unsubscribe function for assets:changed event | assets:changed 事件的取消订阅函数 */
|
||||
private _assetsChangedUnsubscribe: (() => void) | null = null;
|
||||
|
||||
/**
|
||||
* Subscribe to assets:changed events and sync new assets to runtime AssetManager.
|
||||
* 订阅 assets:changed 事件,将新资产同步到运行时 AssetManager。
|
||||
*/
|
||||
private _subscribeToAssetChanges(): void {
|
||||
if (this._assetsChangedUnsubscribe) return; // Already subscribed
|
||||
|
||||
const messageHub = Core.services.tryResolve(MessageHub);
|
||||
if (!messageHub || !this._assetManager) return;
|
||||
|
||||
const database = this._assetManager.getDatabase();
|
||||
const assetRegistry = Core.services.tryResolve(AssetRegistryService) as AssetRegistryService | null;
|
||||
const loaderFactory = this._assetManager.getLoaderFactory();
|
||||
|
||||
this._assetsChangedUnsubscribe = messageHub.subscribe(
|
||||
'assets:changed',
|
||||
async (data: { type: string; path: string; relativePath: string; guid: string }) => {
|
||||
if (data.type === 'add' || data.type === 'modify') {
|
||||
// Get full asset info from registry
|
||||
// 从注册表获取完整资产信息
|
||||
const asset = assetRegistry?.getAsset(data.guid);
|
||||
if (!asset) return;
|
||||
|
||||
// Determine asset type
|
||||
// 确定资产类型
|
||||
let assetType: string | null = null;
|
||||
|
||||
// 1. Try to get type from meta file
|
||||
const meta = assetRegistry?.metaManager.getMetaByGUID(data.guid);
|
||||
if (meta?.loaderType) {
|
||||
assetType = meta.loaderType;
|
||||
}
|
||||
|
||||
// 2. Try to get type from registered loaders
|
||||
if (!assetType) {
|
||||
assetType = loaderFactory?.getAssetTypeByPath?.(asset.path) ?? null;
|
||||
}
|
||||
|
||||
// 3. Fallback by extension
|
||||
if (!assetType) {
|
||||
const ext = asset.path.substring(asset.path.lastIndexOf('.')).toLowerCase();
|
||||
if (['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp'].includes(ext)) {
|
||||
assetType = AssetType.Texture;
|
||||
} else if (['.mp3', '.wav', '.ogg', '.m4a'].includes(ext)) {
|
||||
assetType = AssetType.Audio;
|
||||
} else if (['.json'].includes(ext)) {
|
||||
assetType = AssetType.Json;
|
||||
} else if (['.txt', '.md', '.xml', '.yaml'].includes(ext)) {
|
||||
assetType = AssetType.Text;
|
||||
} else {
|
||||
assetType = AssetType.Custom;
|
||||
}
|
||||
}
|
||||
|
||||
// Add to runtime database
|
||||
// 添加到运行时数据库
|
||||
database.addAsset({
|
||||
guid: asset.guid,
|
||||
path: asset.path,
|
||||
type: assetType,
|
||||
name: asset.name,
|
||||
size: asset.size,
|
||||
hash: asset.hash || '',
|
||||
dependencies: [],
|
||||
labels: [],
|
||||
tags: new Map(),
|
||||
lastModified: asset.lastModified,
|
||||
version: 1
|
||||
});
|
||||
|
||||
logger.debug(`Asset synced to runtime: ${asset.path} (${data.guid})`);
|
||||
} else if (data.type === 'remove') {
|
||||
// Remove from runtime database
|
||||
// 从运行时数据库移除
|
||||
database.removeAsset(data.guid);
|
||||
logger.debug(`Asset removed from runtime: ${data.guid}`);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
logger.debug('Subscribed to assets:changed events');
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from assets:changed events.
|
||||
* 取消订阅 assets:changed 事件。
|
||||
*/
|
||||
private _unsubscribeFromAssetChanges(): void {
|
||||
if (this._assetsChangedUnsubscribe) {
|
||||
this._assetsChangedUnsubscribe();
|
||||
this._assetsChangedUnsubscribe = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup asset path resolver for EngineRenderSystem.
|
||||
* 为 EngineRenderSystem 设置资产路径解析器。
|
||||
@@ -578,10 +689,6 @@ export class EngineService {
|
||||
const renderSystem = this._runtime?.renderSystem;
|
||||
if (!renderSystem) return;
|
||||
|
||||
// UUID v4 regex for GUID detection
|
||||
// UUID v4 正则表达式用于 GUID 检测
|
||||
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
||||
|
||||
renderSystem.setAssetPathResolver((guidOrPath: string): string => {
|
||||
// Skip if already a valid URL
|
||||
// 如果已经是有效的 URL 则跳过
|
||||
@@ -589,9 +696,9 @@ export class EngineService {
|
||||
return guidOrPath;
|
||||
}
|
||||
|
||||
// Check if this is a GUID
|
||||
// 检查是否为 GUID
|
||||
if (uuidRegex.test(guidOrPath)) {
|
||||
// Check if this is a GUID using the unified validation function
|
||||
// 使用统一的验证函数检查是否为 GUID
|
||||
if (isValidGUID(guidOrPath)) {
|
||||
const assetRegistry = Core.services.tryResolve(AssetRegistryService) as AssetRegistryService | null;
|
||||
if (assetRegistry) {
|
||||
const relativePath = assetRegistry.getPathByGuid(guidOrPath);
|
||||
@@ -659,7 +766,8 @@ export class EngineService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Load texture through asset system
|
||||
* 通过相对路径加载纹理资产(用户脚本使用)
|
||||
* Load texture asset by relative path (for user scripts)
|
||||
*/
|
||||
async loadTextureAsset(path: string): Promise<number> {
|
||||
if (!this._assetSystemInitialized || this._initializationError) {
|
||||
@@ -684,6 +792,29 @@ export class EngineService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过 GUID 加载纹理资产(内部引用使用)
|
||||
* Load texture asset by GUID (for internal references)
|
||||
*/
|
||||
async loadTextureAssetByGuid(guid: string): Promise<number> {
|
||||
if (!this._assetSystemInitialized || this._initializationError) {
|
||||
console.warn('Asset system not initialized');
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!this._engineIntegration) {
|
||||
console.warn('Engine integration not available');
|
||||
return 0;
|
||||
}
|
||||
|
||||
try {
|
||||
return await this._engineIntegration.loadTextureByGuid(guid);
|
||||
} catch (error) {
|
||||
console.error('Failed to load texture asset by GUID:', guid, error);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get asset manager
|
||||
*/
|
||||
@@ -875,7 +1006,15 @@ export class EngineService {
|
||||
* Save a snapshot of the current scene state.
|
||||
*/
|
||||
saveSceneSnapshot(): boolean {
|
||||
return this._runtime?.saveSceneSnapshot() ?? false;
|
||||
const success = this._runtime?.saveSceneSnapshot() ?? false;
|
||||
|
||||
if (success) {
|
||||
// 清除 UI 渲染缓存(因为纹理已被清除)
|
||||
// Clear UI render caches (since textures have been cleared)
|
||||
invalidateUIRenderCaches();
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,56 +2,26 @@
|
||||
* Plugin SDK Registry
|
||||
* 插件 SDK 注册器
|
||||
*
|
||||
* 将编辑器核心模块暴露为全局变量,供插件使用。
|
||||
* 插件构建时将这些模块标记为 external,运行时从全局对象获取。
|
||||
*
|
||||
* Exposes editor core modules as global variables for plugin use.
|
||||
* Plugins mark these modules as external during build, and access them from global object at runtime.
|
||||
* 将统一 SDK 暴露为全局变量,供插件和用户代码使用。
|
||||
* Exposes unified SDK as global variable for plugins and user code.
|
||||
*
|
||||
* 使用方式:
|
||||
* 1. 编辑器启动时调用 PluginSDKRegistry.initialize()
|
||||
* 2. 插件构建配置中设置 external: getSDKPackageNames()
|
||||
* 3. 插件构建配置中设置 globals: getSDKGlobalsMapping()
|
||||
* 2. 用户代码使用 import { ... } from '@esengine/sdk'
|
||||
* 3. 运行时通过 window.__ESENGINE_SDK__ 访问
|
||||
*/
|
||||
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
import type { ServiceToken } from '@esengine/engine-core';
|
||||
import type { ServiceToken } from '@esengine/ecs-framework';
|
||||
import {
|
||||
EntityStoreService,
|
||||
MessageHub,
|
||||
EditorConfig,
|
||||
getSDKGlobalsMapping,
|
||||
getSDKPackageNames,
|
||||
getEnabledSDKModules,
|
||||
type ISDKModuleConfig
|
||||
EditorConfig
|
||||
} from '@esengine/editor-core';
|
||||
|
||||
// 动态导入所有 SDK 模块
|
||||
// Dynamic import all SDK modules
|
||||
import * as ecsFramework from '@esengine/ecs-framework';
|
||||
import * as editorRuntime from '@esengine/editor-runtime';
|
||||
import * as behaviorTree from '@esengine/behavior-tree';
|
||||
import * as engineCore from '@esengine/engine-core';
|
||||
import * as sprite from '@esengine/sprite';
|
||||
import * as camera from '@esengine/camera';
|
||||
import * as audio from '@esengine/audio';
|
||||
|
||||
/**
|
||||
* 模块实例映射
|
||||
* Module instance mapping
|
||||
*
|
||||
* 由于 ES 模块的静态导入限制,我们需要维护一个包名到模块的映射。
|
||||
* Due to ES module static import limitations, we need to maintain a mapping from package name to module.
|
||||
*/
|
||||
const MODULE_INSTANCES: Record<string, any> = {
|
||||
'@esengine/ecs-framework': ecsFramework,
|
||||
'@esengine/editor-runtime': editorRuntime,
|
||||
'@esengine/behavior-tree': behaviorTree,
|
||||
'@esengine/engine-core': engineCore,
|
||||
'@esengine/sprite': sprite,
|
||||
'@esengine/camera': camera,
|
||||
'@esengine/audio': audio,
|
||||
};
|
||||
// 统一 SDK 导入
|
||||
// Unified SDK import
|
||||
import * as sdk from '@esengine/sdk';
|
||||
|
||||
// 存储服务实例引用(在初始化时设置)
|
||||
// Service instance references (set during initialization)
|
||||
@@ -62,8 +32,8 @@ let messageHubInstance: MessageHub | null = null;
|
||||
* 插件 API 接口
|
||||
* Plugin API interface
|
||||
*
|
||||
* 为插件提供统一的访问接口,避免模块实例不一致的问题。
|
||||
* Provides unified access interface for plugins, avoiding module instance inconsistency issues.
|
||||
* 为插件提供统一的访问接口。
|
||||
* Provides unified access interface for plugins.
|
||||
*/
|
||||
export interface IPluginAPI {
|
||||
/** 获取当前场景 | Get current scene */
|
||||
@@ -83,32 +53,17 @@ export interface IPluginAPI {
|
||||
getCore(): typeof Core;
|
||||
}
|
||||
|
||||
/**
|
||||
* SDK 全局对象类型
|
||||
* SDK global object type
|
||||
*/
|
||||
export interface ISDKGlobal {
|
||||
/** 动态模块加载 | Dynamic module loading */
|
||||
require: (moduleName: string) => any;
|
||||
/** 插件 API | Plugin API */
|
||||
api: IPluginAPI;
|
||||
/** 其他动态注册的模块 | Other dynamically registered modules */
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* 插件 SDK 注册器
|
||||
* Plugin SDK Registry
|
||||
*
|
||||
* 职责:
|
||||
* 1. 将 SDK 模块暴露到全局对象
|
||||
* 1. 将统一 SDK 暴露到全局对象 window.__ESENGINE_SDK__
|
||||
* 2. 提供插件 API
|
||||
* 3. 支持动态模块加载
|
||||
*
|
||||
* Responsibilities:
|
||||
* 1. Expose SDK modules to global object
|
||||
* 1. Expose unified SDK to global object window.__ESENGINE_SDK__
|
||||
* 2. Provide plugin API
|
||||
* 3. Support dynamic module loading
|
||||
*/
|
||||
export class PluginSDKRegistry {
|
||||
private static initialized = false;
|
||||
@@ -117,8 +72,8 @@ export class PluginSDKRegistry {
|
||||
* 初始化 SDK 注册器
|
||||
* Initialize SDK registry
|
||||
*
|
||||
* 将所有配置的 SDK 模块暴露到全局对象。
|
||||
* Exposes all configured SDK modules to global object.
|
||||
* 将统一 SDK 暴露到全局对象。
|
||||
* Exposes unified SDK to global object.
|
||||
*/
|
||||
static initialize(): void {
|
||||
if (this.initialized) {
|
||||
@@ -137,28 +92,13 @@ export class PluginSDKRegistry {
|
||||
console.error('[PluginSDKRegistry] MessageHub not registered yet!');
|
||||
}
|
||||
|
||||
// 创建 SDK 全局对象
|
||||
// Create SDK global object
|
||||
const sdkGlobal: ISDKGlobal = {
|
||||
require: this.requireModule.bind(this),
|
||||
// 创建增强的 SDK 全局对象,包含 API
|
||||
// Create enhanced SDK global object with API
|
||||
const sdkGlobal = {
|
||||
...sdk,
|
||||
api: this.createPluginAPI(),
|
||||
};
|
||||
|
||||
// 从配置自动注册所有启用的模块
|
||||
// Auto-register all enabled modules from config
|
||||
const enabledModules = getEnabledSDKModules();
|
||||
for (const config of enabledModules) {
|
||||
const moduleInstance = MODULE_INSTANCES[config.packageName];
|
||||
if (moduleInstance) {
|
||||
sdkGlobal[config.globalKey] = moduleInstance;
|
||||
} else {
|
||||
console.warn(
|
||||
`[PluginSDKRegistry] Module "${config.packageName}" configured but not imported. ` +
|
||||
`Please add import statement for this module.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 设置全局对象
|
||||
// Set global object
|
||||
const sdkGlobalName = EditorConfig.globals.sdk;
|
||||
@@ -166,10 +106,7 @@ export class PluginSDKRegistry {
|
||||
|
||||
this.initialized = true;
|
||||
|
||||
console.log(
|
||||
`[PluginSDKRegistry] Initialized with ${enabledModules.length} modules:`,
|
||||
enabledModules.map(m => m.globalKey)
|
||||
);
|
||||
console.log(`[PluginSDKRegistry] Initialized SDK at window.${sdkGlobalName}`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -205,24 +142,6 @@ export class PluginSDKRegistry {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 动态获取模块(用于 CommonJS 风格的插件)
|
||||
* Dynamic module loading (for CommonJS style plugins)
|
||||
*
|
||||
* @param moduleName 模块包名 | Module package name
|
||||
*/
|
||||
private static requireModule(moduleName: string): any {
|
||||
const module = MODULE_INSTANCES[moduleName];
|
||||
if (!module) {
|
||||
const availableModules = Object.keys(MODULE_INSTANCES).join(', ');
|
||||
throw new Error(
|
||||
`[PluginSDKRegistry] Unknown module: "${moduleName}". ` +
|
||||
`Available modules: ${availableModules}`
|
||||
);
|
||||
}
|
||||
return module;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否已初始化
|
||||
* Check if initialized
|
||||
@@ -232,26 +151,18 @@ export class PluginSDKRegistry {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有可用的 SDK 模块名称
|
||||
* Get all available SDK module names
|
||||
*
|
||||
* @deprecated 使用 getSDKPackageNames() 代替 | Use getSDKPackageNames() instead
|
||||
* 获取 SDK 包名
|
||||
* Get SDK package name
|
||||
*/
|
||||
static getAvailableModules(): string[] {
|
||||
return getSDKPackageNames();
|
||||
static getSDKPackageName(): string {
|
||||
return '@esengine/sdk';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取全局变量映射(用于生成插件构建配置)
|
||||
* Get globals config (for generating plugin build config)
|
||||
*
|
||||
* @deprecated 使用 getSDKGlobalsMapping() 代替 | Use getSDKGlobalsMapping() instead
|
||||
* 获取 SDK 全局变量名
|
||||
* Get SDK global variable name
|
||||
*/
|
||||
static getGlobalsConfig(): Record<string, string> {
|
||||
return getSDKGlobalsMapping();
|
||||
static getSDKGlobalName(): string {
|
||||
return EditorConfig.globals.sdk;
|
||||
}
|
||||
}
|
||||
|
||||
// 重新导出辅助函数,方便插件构建工具使用
|
||||
// Re-export helper functions for plugin build tools
|
||||
export { getSDKGlobalsMapping, getSDKPackageNames, getEnabledSDKModules };
|
||||
|
||||
@@ -40,6 +40,8 @@ export interface ModuleManifest {
|
||||
wasmPaths?: string[];
|
||||
runtimeWasmPath?: string;
|
||||
externalDependencies?: string[];
|
||||
/** Global key for window.__ESENGINE__ (optional, defaults to camelCase of id) */
|
||||
globalKey?: string;
|
||||
}
|
||||
|
||||
export class RuntimeResolver {
|
||||
@@ -263,6 +265,7 @@ export class RuntimeResolver {
|
||||
const copiedModules: string[] = [];
|
||||
|
||||
// Copy each module's dist files
|
||||
const missingModules: string[] = [];
|
||||
for (const module of modules) {
|
||||
const moduleDistDir = `${this.engineModulesPath}\\${module.id}\\dist`;
|
||||
const moduleSrcFile = `${moduleDistDir}\\index.mjs`;
|
||||
@@ -294,9 +297,18 @@ export class RuntimeResolver {
|
||||
}
|
||||
|
||||
copiedModules.push(module.id);
|
||||
console.log(`[RuntimeResolver] Copied module: ${module.id} (${module.name})`);
|
||||
} else {
|
||||
missingModules.push(module.id);
|
||||
console.warn(`[RuntimeResolver] MISSING dist for module: ${module.id} (looked in ${moduleDistDir})`);
|
||||
}
|
||||
}
|
||||
|
||||
if (missingModules.length > 0) {
|
||||
console.error(`[RuntimeResolver] ${missingModules.length} modules have missing dist files:`, missingModules);
|
||||
console.error('[RuntimeResolver] Please run: npm run build in the workspace to build all modules');
|
||||
}
|
||||
|
||||
// Copy external dependencies (e.g., rapier2d)
|
||||
await this.copyExternalDependencies(modules, libsDir, importMap);
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
import type { ServiceToken } from '@esengine/engine-core';
|
||||
import type { ServiceToken } from '@esengine/ecs-framework';
|
||||
import { ProfilerServiceToken, type IProfilerService } from './tokens';
|
||||
|
||||
/**
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
* These services are defined in editor-app, so Tokens are also defined here.
|
||||
*/
|
||||
|
||||
import { createServiceToken } from '@esengine/engine-core';
|
||||
import { createServiceToken } from '@esengine/ecs-framework';
|
||||
|
||||
// ============================================================================
|
||||
// Profiler Data Types (定义在这里以避免循环依赖)
|
||||
|
||||
Reference in New Issue
Block a user