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 规则报错
This commit is contained in:
YHH
2025-12-24 20:57:08 +08:00
committed by GitHub
parent 58f70a5783
commit dbc6793dc4
133 changed files with 6880 additions and 9141 deletions

View File

@@ -8,9 +8,11 @@
* 使用注册表模式替代原型修改,实现更清晰的架构。
*/
import type { Component, ComponentType, Entity } from '@esengine/ecs-framework';
import { createLogger, type Component, type ComponentType, type Entity } from '@esengine/ecs-framework';
import type { IGizmoProvider, IGizmoRenderData } from './IGizmoProvider';
const logger = createLogger('GizmoRegistry');
/**
* Gizmo provider function type
* Gizmo 提供者函数类型
@@ -124,9 +126,7 @@ export class GizmoRegistry {
try {
return provider(component, entity, isSelected);
} catch (e) {
// Silently ignore errors from gizmo providers
// 静默忽略 gizmo 提供者的错误
console.warn(`[GizmoRegistry] Error in gizmo provider for ${componentType.name}:`, e);
logger.warn(`Error in gizmo provider for ${componentType.name}:`, e);
return [];
}
}

View File

@@ -0,0 +1,301 @@
/**
* @zh 通用注册表基类
* @en Generic Registry Base Class
*
* @zh 提供注册表的通用实现,消除 16+ 个 Registry 类中的重复代码。
* @en Provides common registry implementation, eliminating duplicate code in 16+ Registry classes.
*
* @example
* ```typescript
* interface IMyItem { id: string; name: string; }
*
* class MyRegistry extends BaseRegistry<IMyItem> {
* constructor() {
* super('MyRegistry');
* }
*
* protected getItemId(item: IMyItem): string {
* return item.id;
* }
* }
* ```
*/
import { IService, createLogger, type ILogger } from '@esengine/ecs-framework';
/**
* @zh 可注册项的基础接口
* @en Base interface for registrable items
*/
export interface IRegistrable {
/** @zh 唯一标识符 @en Unique identifier */
readonly id?: string;
}
/**
* @zh 带优先级的可注册项
* @en Registrable item with priority
*/
export interface IPrioritized {
/** @zh 优先级(越高越先匹配) @en Priority (higher = matched first) */
readonly priority?: number;
}
/**
* @zh 带顺序的可注册项
* @en Registrable item with order
*/
export interface IOrdered {
/** @zh 排序权重(越小越靠前) @en Sort order (lower = first) */
readonly order?: number;
}
/**
* @zh 注册表配置
* @en Registry configuration
*/
export interface RegistryOptions {
/** @zh 是否允许覆盖已存在的项 @en Allow overwriting existing items */
allowOverwrite?: boolean;
/** @zh 是否在覆盖时发出警告 @en Warn when overwriting */
warnOnOverwrite?: boolean;
}
/**
* @zh 通用注册表基类
* @en Generic Registry Base Class
*
* @typeParam T - @zh 注册项类型 @en Registered item type
* @typeParam K - @zh 键类型(默认 string @en Key type (default string)
*/
export abstract class BaseRegistry<T, K extends string = string> implements IService {
protected readonly _items: Map<K, T> = new Map();
protected readonly _logger: ILogger;
protected readonly _options: Required<RegistryOptions>;
constructor(
protected readonly _name: string,
options: RegistryOptions = {}
) {
this._logger = createLogger(_name);
this._options = {
allowOverwrite: options.allowOverwrite ?? true,
warnOnOverwrite: options.warnOnOverwrite ?? true
};
}
/**
* @zh 获取项的键
* @en Get item key
*/
protected abstract getItemKey(item: T): K;
/**
* @zh 获取项的显示名称(用于日志)
* @en Get item display name (for logging)
*/
protected getItemDisplayName(item: T): string {
const key = this.getItemKey(item);
return String(key);
}
// ========== 核心 CRUD 操作 | Core CRUD Operations ==========
/**
* @zh 注册项
* @en Register item
*/
register(item: T): boolean {
const key = this.getItemKey(item);
const displayName = this.getItemDisplayName(item);
if (this._items.has(key)) {
if (this._options.warnOnOverwrite) {
this._logger.warn(`Overwriting: ${displayName}`);
}
if (!this._options.allowOverwrite) {
return false;
}
}
this._items.set(key, item);
this._logger.debug(`Registered: ${displayName}`);
return true;
}
/**
* @zh 批量注册
* @en Register multiple items
*/
registerMany(items: T[]): number {
let count = 0;
for (const item of items) {
if (this.register(item)) count++;
}
return count;
}
/**
* @zh 注销项
* @en Unregister item
*/
unregister(key: K): boolean {
if (this._items.delete(key)) {
this._logger.debug(`Unregistered: ${key}`);
return true;
}
return false;
}
/**
* @zh 获取项
* @en Get item
*/
get(key: K): T | undefined {
return this._items.get(key);
}
/**
* @zh 检查是否存在
* @en Check if exists
*/
has(key: K): boolean {
return this._items.has(key);
}
// ========== 查询操作 | Query Operations ==========
/**
* @zh 获取所有项
* @en Get all items
*/
getAll(): T[] {
return Array.from(this._items.values());
}
/**
* @zh 获取所有键
* @en Get all keys
*/
getAllKeys(): K[] {
return Array.from(this._items.keys());
}
/**
* @zh 获取项数量
* @en Get item count
*/
get size(): number {
return this._items.size;
}
/**
* @zh 是否为空
* @en Is empty
*/
get isEmpty(): boolean {
return this._items.size === 0;
}
/**
* @zh 按条件过滤
* @en Filter by predicate
*/
filter(predicate: (item: T, key: K) => boolean): T[] {
const result: T[] = [];
for (const [key, item] of this._items) {
if (predicate(item, key)) {
result.push(item);
}
}
return result;
}
/**
* @zh 查找第一个匹配项
* @en Find first matching item
*/
find(predicate: (item: T, key: K) => boolean): T | undefined {
for (const [key, item] of this._items) {
if (predicate(item, key)) {
return item;
}
}
return undefined;
}
// ========== 排序操作 | Sorting Operations ==========
/**
* @zh 按 order 字段排序(越小越靠前)
* @en Sort items by order field (lower = first)
*/
protected sortByOrder<U extends IOrdered>(items: U[], defaultOrder = 0): U[] {
return items.sort((a, b) => (a.order ?? defaultOrder) - (b.order ?? defaultOrder));
}
// ========== 生命周期 | Lifecycle ==========
/**
* @zh 清空注册表
* @en Clear registry
*/
clear(): void {
this._items.clear();
this._logger.debug('Cleared');
}
/**
* @zh 释放资源
* @en Dispose resources
*/
dispose(): void {
this.clear();
this._logger.debug('Disposed');
}
}
/**
* @zh 带优先级查找的注册表基类
* @en Registry base class with priority-based lookup
*
* @zh 适用于需要按优先级匹配的场景(如 Inspector、FieldEditor
* @en For scenarios requiring priority-based matching (e.g., Inspector, FieldEditor)
*/
export abstract class PrioritizedRegistry<T extends IPrioritized, K extends string = string>
extends BaseRegistry<T, K> {
/**
* @zh 按优先级排序获取所有项
* @en Get all items sorted by priority
*/
getAllSorted(): T[] {
return this.getAll().sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
}
/**
* @zh 查找第一个能处理目标的项
* @en Find first item that can handle the target
*
* @param canHandle - @zh 判断函数 @en Predicate function
*/
findByPriority(canHandle: (item: T) => boolean): T | undefined {
for (const item of this.getAllSorted()) {
if (canHandle(item)) {
return item;
}
}
return undefined;
}
}
/**
* @zh 创建注册表服务标识符
* @en Create registry service identifier
*
* @zh 使用 Symbol.for 确保跨包共享同一个 Symbol
* @en Uses Symbol.for to ensure same Symbol is shared across packages
*/
export function createRegistryToken<T>(name: string): symbol {
return Symbol.for(`IRegistry:${name}`);
}

View File

@@ -7,6 +7,7 @@
*/
import type { IService } from '@esengine/ecs-framework';
import { createLogger } from '@esengine/ecs-framework';
import type {
IBuildPipeline,
IBuildPipelineRegistry,
@@ -17,6 +18,8 @@ import type {
} from './IBuildPipeline';
import { BuildStatus } from './IBuildPipeline';
const logger = createLogger('BuildService');
/**
* Build task.
* 构建任务。
@@ -88,10 +91,10 @@ export class BuildService implements IService, IBuildPipelineRegistry {
*/
register(pipeline: IBuildPipeline): void {
if (this._pipelines.has(pipeline.platform)) {
console.warn(`[BuildService] Overwriting existing pipeline: ${pipeline.platform} | 覆盖已存在的构建管线: ${pipeline.platform}`);
logger.warn(`Overwriting existing pipeline: ${pipeline.platform} | 覆盖已存在的构建管线: ${pipeline.platform}`);
}
this._pipelines.set(pipeline.platform, pipeline);
console.log(`[BuildService] Registered pipeline: ${pipeline.displayName} | 注册构建管线: ${pipeline.displayName}`);
logger.info(`Registered pipeline: ${pipeline.displayName} | 注册构建管线: ${pipeline.displayName}`);
}
/**
@@ -256,7 +259,7 @@ export class BuildService implements IService, IBuildPipelineRegistry {
if (this._currentTask) {
this._currentTask.abortController.abort();
this._currentTask.progress.status = BuildStatus.Cancelled;
console.log('[BuildService] Build cancelled | 构建已取消');
logger.info('Build cancelled | 构建已取消');
}
}

View File

@@ -50,6 +50,7 @@ import type {
import { BuildPlatform, BuildStatus } from '../IBuildPipeline';
import type { ModuleManifest } from '../../Module/ModuleTypes';
import { hashFileInfo } from '@esengine/asset-system';
import { generateImportMap, type ImportMapConfig } from '@esengine/runtime-core';
// ============================================================================
// Build File System Interface
@@ -1655,33 +1656,20 @@ Built with ESEngine | 使用 ESEngine 构建
}
/**
* Generate Import Map from module manifests.
* 从模块清单生成 Import Map。
* @zh 使用共享的 ImportMapGenerator 生成 Import Map
* @en Generate Import Map using shared ImportMapGenerator
*
* @zh 统一使用 @esengine/runtime-core 的 generateImportMap 工具
* @en Unified usage of generateImportMap utility from @esengine/runtime-core
*/
private _generateImportMap(coreModules: ModuleManifest[], pluginModules: ModuleManifest[]): Record<string, string> {
const imports: Record<string, string> = {};
for (const module of coreModules) {
if (module.name) {
imports[module.name] = './libs/esengine.core.js';
}
}
for (const module of pluginModules) {
if (module.name) {
imports[module.name] = `./libs/plugins/${module.id}.js`;
}
if (module.externalDependencies) {
for (const dep of module.externalDependencies) {
if (!imports[dep]) {
const depId = dep.startsWith('@esengine/') ? dep.slice(10) : dep;
imports[dep] = `./libs/plugins/${depId}.js`;
}
}
}
}
return imports;
const config: ImportMapConfig = {
mode: 'production',
basePath: '.',
coreModules,
pluginModules
};
return generateImportMap(config);
}
private _generateSingleBundleHtml(mainScenePath: string, wasmRuntimePath: string | null): string {

View File

@@ -1,202 +1,185 @@
import { ICommand } from './ICommand';
/**
* 命令历史记录配置
* @zh 命令历史记录配置
* @en Command history configuration
*/
export interface CommandManagerConfig {
/**
* 最大历史记录数量
*/
/** @zh 最大历史记录数量 @en Maximum history size */
maxHistorySize?: number;
/**
* 是否自动合并相似命令
*/
/** @zh 是否自动合并相似命令 @en Auto merge similar commands */
autoMerge?: boolean;
}
/**
* 命令管理器
* 管理命令的执行、撤销、重做以及历史记录
* @zh 命令管理器 - 管理命令的执行、撤销、重做以及历史记录
* @en Command Manager - Manages command execution, undo, redo and history
*/
export class CommandManager {
private undoStack: ICommand[] = [];
private redoStack: ICommand[] = [];
private readonly config: Required<CommandManagerConfig>;
private isExecuting = false;
private _undoStack: ICommand[] = [];
private _redoStack: ICommand[] = [];
private readonly _config: Required<CommandManagerConfig>;
private _isExecuting = false;
constructor(config: CommandManagerConfig = {}) {
this.config = {
this._config = {
maxHistorySize: config.maxHistorySize ?? 100,
autoMerge: config.autoMerge ?? true
};
}
/**
* 执行命令
* @zh 尝试将命令与栈顶命令合并
* @en Try to merge command with the top of stack
*/
execute(command: ICommand): void {
if (this.isExecuting) {
throw new Error('不能在命令执行过程中执行新命令');
private _tryMergeWithLast(command: ICommand): boolean {
if (!this._config.autoMerge || this._undoStack.length === 0) {
return false;
}
this.isExecuting = true;
const lastCommand = this._undoStack[this._undoStack.length - 1];
if (lastCommand?.canMergeWith(command)) {
this._undoStack[this._undoStack.length - 1] = lastCommand.mergeWith(command);
this._redoStack = [];
return true;
}
try {
command.execute();
return false;
}
if (this.config.autoMerge && this.undoStack.length > 0) {
const lastCommand = this.undoStack[this.undoStack.length - 1];
if (lastCommand && lastCommand.canMergeWith(command)) {
const mergedCommand = lastCommand.mergeWith(command);
this.undoStack[this.undoStack.length - 1] = mergedCommand;
this.redoStack = [];
return;
}
}
/**
* @zh 将命令推入撤销栈
* @en Push command to undo stack
*/
private _pushToUndoStack(command: ICommand): void {
if (this._tryMergeWithLast(command)) {
return;
}
this.undoStack.push(command);
this.redoStack = [];
this._undoStack.push(command);
this._redoStack = [];
if (this.undoStack.length > this.config.maxHistorySize) {
this.undoStack.shift();
}
} finally {
this.isExecuting = false;
if (this._undoStack.length > this._config.maxHistorySize) {
this._undoStack.shift();
}
}
/**
* 撤销上一个命令
* @zh 执行命令
* @en Execute command
*/
execute(command: ICommand): void {
if (this._isExecuting) {
throw new Error('Cannot execute command while another is executing');
}
this._isExecuting = true;
try {
command.execute();
this._pushToUndoStack(command);
} finally {
this._isExecuting = false;
}
}
/**
* @zh 撤销上一个命令
* @en Undo last command
*/
undo(): void {
if (this.isExecuting) {
throw new Error('不能在命令执行过程中撤销');
if (this._isExecuting) {
throw new Error('Cannot undo while executing');
}
const command = this.undoStack.pop();
if (!command) {
return;
}
const command = this._undoStack.pop();
if (!command) return;
this.isExecuting = true;
this._isExecuting = true;
try {
command.undo();
this.redoStack.push(command);
this._redoStack.push(command);
} catch (error) {
this.undoStack.push(command);
this._undoStack.push(command);
throw error;
} finally {
this.isExecuting = false;
this._isExecuting = false;
}
}
/**
* 重做上一个被撤销的命令
* @zh 重做上一个被撤销的命令
* @en Redo last undone command
*/
redo(): void {
if (this.isExecuting) {
throw new Error('不能在命令执行过程中重做');
if (this._isExecuting) {
throw new Error('Cannot redo while executing');
}
const command = this.redoStack.pop();
if (!command) {
return;
}
const command = this._redoStack.pop();
if (!command) return;
this.isExecuting = true;
this._isExecuting = true;
try {
command.execute();
this.undoStack.push(command);
this._undoStack.push(command);
} catch (error) {
this.redoStack.push(command);
this._redoStack.push(command);
throw error;
} finally {
this.isExecuting = false;
this._isExecuting = false;
}
}
/**
* 检查是否可以撤销
*/
/** @zh 检查是否可以撤销 @en Check if can undo */
canUndo(): boolean {
return this.undoStack.length > 0;
return this._undoStack.length > 0;
}
/**
* 检查是否可以重做
*/
/** @zh 检查是否可以重做 @en Check if can redo */
canRedo(): boolean {
return this.redoStack.length > 0;
return this._redoStack.length > 0;
}
/**
* 获取撤销栈的描述列表
*/
/** @zh 获取撤销栈的描述列表 @en Get undo history descriptions */
getUndoHistory(): string[] {
return this.undoStack.map((cmd) => cmd.getDescription());
return this._undoStack.map(cmd => cmd.getDescription());
}
/**
* 获取重做栈的描述列表
*/
/** @zh 获取重做栈的描述列表 @en Get redo history descriptions */
getRedoHistory(): string[] {
return this.redoStack.map((cmd) => cmd.getDescription());
return this._redoStack.map(cmd => cmd.getDescription());
}
/**
* 清空所有历史记录
*/
/** @zh 清空所有历史记录 @en Clear all history */
clear(): void {
this.undoStack = [];
this.redoStack = [];
this._undoStack = [];
this._redoStack = [];
}
/**
* 批量执行命令(作为单一操作,可以一次撤销)
* @zh 批量执行命令(作为单一操作,可以一次撤销)
* @en Execute batch commands (as single operation, can be undone at once)
*/
executeBatch(commands: ICommand[]): void {
if (commands.length === 0) {
return;
}
const batchCommand = new BatchCommand(commands);
this.execute(batchCommand);
if (commands.length === 0) return;
this.execute(new BatchCommand(commands));
}
/**
* 将命令推入撤销栈但不执行
* Push command to undo stack without executing
*
* 用于已经执行过的操作(如拖动变换),只需要记录到历史
* Used for operations that have already been performed (like drag transforms),
* only need to record to history
* @zh 将命令推入撤销栈但不执行(用于已执行的操作如拖动变换)
* @en Push command to undo stack without executing (for already performed operations)
*/
pushWithoutExecute(command: ICommand): void {
if (this.config.autoMerge && this.undoStack.length > 0) {
const lastCommand = this.undoStack[this.undoStack.length - 1];
if (lastCommand && lastCommand.canMergeWith(command)) {
const mergedCommand = lastCommand.mergeWith(command);
this.undoStack[this.undoStack.length - 1] = mergedCommand;
this.redoStack = [];
return;
}
}
this.undoStack.push(command);
this.redoStack = [];
if (this.undoStack.length > this.config.maxHistorySize) {
this.undoStack.shift();
}
this._pushToUndoStack(command);
}
}
/**
* 批量命令
* 将多个命令组合为一个命令
* @zh 批量命令 - 将多个命令组合为一个命令
* @en Batch Command - Combines multiple commands into one
*/
class BatchCommand implements ICommand {
constructor(private readonly commands: ICommand[]) {}
@@ -209,15 +192,12 @@ class BatchCommand implements ICommand {
undo(): void {
for (let i = this.commands.length - 1; i >= 0; i--) {
const command = this.commands[i];
if (command) {
command.undo();
}
this.commands[i]?.undo();
}
}
getDescription(): string {
return `批量操作 (${this.commands.length} 个命令)`;
return `Batch (${this.commands.length} commands)`;
}
canMergeWith(): boolean {
@@ -225,6 +205,6 @@ class BatchCommand implements ICommand {
}
mergeWith(): ICommand {
throw new Error('批量命令不支持合并');
throw new Error('Batch commands cannot be merged');
}
}

View File

@@ -1,37 +1,28 @@
import { IService } from '@esengine/ecs-framework';
import { ICompiler } from './ICompiler';
/**
* @zh 编译器注册表
* @en Compiler Registry
*/
export class CompilerRegistry implements IService {
private compilers: Map<string, ICompiler> = new Map();
import { BaseRegistry, createRegistryToken } from './BaseRegistry';
import type { ICompiler } from './ICompiler';
register(compiler: ICompiler): void {
if (this.compilers.has(compiler.id)) {
console.warn(`Compiler with id "${compiler.id}" is already registered. Overwriting.`);
}
this.compilers.set(compiler.id, compiler);
/**
* @zh 编译器注册表
* @en Compiler Registry
*/
export class CompilerRegistry extends BaseRegistry<ICompiler> {
constructor() {
super('CompilerRegistry');
}
unregister(compilerId: string): void {
this.compilers.delete(compilerId);
protected getItemKey(item: ICompiler): string {
return item.id;
}
get(compilerId: string): ICompiler | undefined {
return this.compilers.get(compilerId);
}
getAll(): ICompiler[] {
return Array.from(this.compilers.values());
}
clear(): void {
this.compilers.clear();
}
dispose(): void {
this.clear();
protected override getItemDisplayName(item: ICompiler): string {
return `${item.name} (${item.id})`;
}
}
// Service identifier for DI registration (用于跨包插件访问)
// 使用 Symbol.for 确保跨包共享同一个 Symbol
export const ICompilerRegistry = Symbol.for('ICompilerRegistry');
/** @zh 编译器注册表服务标识符 @en Compiler registry service identifier */
export const ICompilerRegistry = createRegistryToken<CompilerRegistry>('CompilerRegistry');

View File

@@ -1,41 +1,55 @@
/**
* Component Action Registry Service
* @zh 组件动作注册表服务
* @en Component Action Registry Service
*
* Manages component-specific actions for the inspector panel
* @zh 管理检视器面板中的组件特定动作
* @en Manages component-specific actions for the inspector panel
*/
import { injectable } from 'tsyringe';
import type { IService } from '@esengine/ecs-framework';
import { createLogger, type ILogger, type IService } from '@esengine/ecs-framework';
import type { ComponentAction } from '../Plugin/EditorModule';
// Re-export ComponentAction type from Plugin system
import { createRegistryToken } from './BaseRegistry';
export type { ComponentAction } from '../Plugin/EditorModule';
@injectable()
/**
* @zh 组件动作注册表
* @en Component Action Registry
*/
export class ComponentActionRegistry implements IService {
private actions: Map<string, ComponentAction[]> = new Map();
private readonly _actions = new Map<string, ComponentAction[]>();
private readonly _logger: ILogger;
constructor() {
this._logger = createLogger('ComponentActionRegistry');
}
/**
* Register a component action
* @zh 注册组件动作
* @en Register component action
*/
register(action: ComponentAction): void {
const componentName = action.componentName;
if (!this.actions.has(componentName)) {
this.actions.set(componentName, []);
const { componentName, id } = action;
if (!this._actions.has(componentName)) {
this._actions.set(componentName, []);
}
const actions = this.actions.get(componentName)!;
const existingIndex = actions.findIndex(a => a.id === action.id);
const actions = this._actions.get(componentName)!;
const existingIndex = actions.findIndex(a => a.id === id);
if (existingIndex >= 0) {
console.warn(`[ComponentActionRegistry] Action '${action.id}' already exists for '${componentName}', overwriting`);
this._logger.warn(`Overwriting action: ${id} for ${componentName}`);
actions[existingIndex] = action;
} else {
actions.push(action);
this._logger.debug(`Registered action: ${id} for ${componentName}`);
}
}
/**
* Register multiple actions
* @zh 批量注册动作
* @en Register multiple actions
*/
registerMany(actions: ComponentAction[]): void {
for (const action of actions) {
@@ -44,45 +58,55 @@ export class ComponentActionRegistry implements IService {
}
/**
* Unregister an action by ID
* @zh 注销动作
* @en Unregister action
*/
unregister(componentName: string, actionId: string): void {
const actions = this.actions.get(componentName);
if (actions) {
const index = actions.findIndex(a => a.id === actionId);
if (index >= 0) {
actions.splice(index, 1);
}
unregister(componentName: string, actionId: string): boolean {
const actions = this._actions.get(componentName);
if (!actions) return false;
const index = actions.findIndex(a => a.id === actionId);
if (index < 0) return false;
actions.splice(index, 1);
this._logger.debug(`Unregistered action: ${actionId} from ${componentName}`);
if (actions.length === 0) {
this._actions.delete(componentName);
}
return true;
}
/**
* Get all actions for a component type sorted by order
* @zh 获取组件的所有动作(按 order 排序)
* @en Get all actions for component (sorted by order)
*/
getActionsForComponent(componentName: string): ComponentAction[] {
const actions = this.actions.get(componentName) || [];
const actions = this._actions.get(componentName);
if (!actions) return [];
return [...actions].sort((a, b) => (a.order ?? 100) - (b.order ?? 100));
}
/**
* Check if a component has any actions
* @zh 检查组件是否有动作
* @en Check if component has actions
*/
hasActions(componentName: string): boolean {
const actions = this.actions.get(componentName);
const actions = this._actions.get(componentName);
return actions !== undefined && actions.length > 0;
}
/**
* Clear all actions
*/
/** @zh 清空所有动作 @en Clear all actions */
clear(): void {
this.actions.clear();
this._actions.clear();
this._logger.debug('Cleared');
}
/**
* Dispose resources
*/
/** @zh 释放资源 @en Dispose resources */
dispose(): void {
this.actions.clear();
this.clear();
}
}
/** @zh 组件动作注册表服务标识符 @en Component action registry service identifier */
export const IComponentActionRegistry = createRegistryToken<ComponentActionRegistry>('ComponentActionRegistry');

View File

@@ -1,162 +1,136 @@
import React from 'react';
import { Component, IService, createLogger } from '@esengine/ecs-framework';
const logger = createLogger('ComponentInspectorRegistry');
import { Component } from '@esengine/ecs-framework';
import { PrioritizedRegistry, createRegistryToken, type IPrioritized } from './BaseRegistry';
/**
* 组件检查器上下文
* Context passed to component inspectors
* @zh 组件检查器上下文
* @en Context passed to component inspectors
*/
export interface ComponentInspectorContext {
/** 被检查的组件 */
/** @zh 被检查的组件 @en The component being inspected */
component: Component;
/** 所属实体 */
/** @zh 所属实体 @en Owner entity */
entity: any;
/** 版本号(用于触发重渲染) */
/** @zh 版本号(用于触发重渲染) @en Version (for triggering re-renders) */
version?: number;
/** 属性变更回调 */
/** @zh 属性变更回调 @en Property change callback */
onChange?: (propertyName: string, value: any) => void;
/** 动作回调 */
/** @zh 动作回调 @en Action callback */
onAction?: (actionId: string, propertyName: string, component: Component) => void;
}
/**
* Inspector render mode.
* 检查器渲染模式。
* @zh 检查器渲染模式
* @en Inspector render mode
*/
export type InspectorRenderMode = 'replace' | 'append';
/**
* 组件检查器接口
* Interface for custom component inspectors
* @zh 组件检查器接口
* @en Interface for custom component inspectors
*/
export interface IComponentInspector<T extends Component = Component> {
/** 唯一标识符 */
export interface IComponentInspector<T extends Component = Component> extends IPrioritized {
/** @zh 唯一标识符 @en Unique identifier */
readonly id: string;
/** 显示名称 */
/** @zh 显示名称 @en Display name */
readonly name: string;
/** 优先级(数字越大优先级越高) */
readonly priority?: number;
/** 目标组件类型名称列表 */
/** @zh 目标组件类型名称列表 @en Target component type names */
readonly targetComponents: string[];
/**
* 渲染模式
* - 'replace': 替换默认的 PropertyInspector默认
* - 'append': 追加到默认的 PropertyInspector 后面
* @zh 渲染模式:'replace' 替换默认检查器,'append' 追加到默认检查器后
* @en Render mode: 'replace' replaces default, 'append' appends after default
*/
readonly renderMode?: InspectorRenderMode;
/**
* 判断是否可以处理该组件
*/
/** @zh 判断是否可以处理该组件 @en Check if can handle the component */
canHandle(component: Component): component is T;
/**
* 渲染组件检查器
*/
/** @zh 渲染组件检查器 @en Render component inspector */
render(context: ComponentInspectorContext): React.ReactElement;
}
/**
* 组件检查器注册表
* Registry for custom component inspectors
* @zh 组件检查器注册表
* @en Registry for custom component inspectors
*/
export class ComponentInspectorRegistry implements IService {
private inspectors: Map<string, IComponentInspector> = new Map();
export class ComponentInspectorRegistry extends PrioritizedRegistry<IComponentInspector> {
constructor() {
super('ComponentInspectorRegistry');
}
/**
* 注册组件检查器
*/
register(inspector: IComponentInspector): void {
if (this.inspectors.has(inspector.id)) {
logger.warn(`Overwriting existing component inspector: ${inspector.id}`);
}
this.inspectors.set(inspector.id, inspector);
logger.debug(`Registered component inspector: ${inspector.name} (${inspector.id})`);
protected getItemKey(item: IComponentInspector): string {
return item.id;
}
protected override getItemDisplayName(item: IComponentInspector): string {
return `${item.name} (${item.id})`;
}
/**
* 注销组件检查器
*/
unregister(inspectorId: string): void {
if (this.inspectors.delete(inspectorId)) {
logger.debug(`Unregistered component inspector: ${inspectorId}`);
}
}
/**
* 查找可以处理指定组件的检查器(仅 replace 模式)
* Find inspector that can handle the component (replace mode only)
* @zh 查找可以处理指定组件检查器(仅 replace 模式)
* @en Find inspector that can handle the component (replace mode only)
*/
findInspector(component: Component): IComponentInspector | undefined {
const inspectors = Array.from(this.inspectors.values())
.filter(i => i.renderMode !== 'append')
.sort((a, b) => (b.priority || 0) - (a.priority || 0));
for (const inspector of inspectors) {
const sorted = this.getAllSorted().filter(i => i.renderMode !== 'append');
for (const inspector of sorted) {
try {
if (inspector.canHandle(component)) {
return inspector;
}
} catch (error) {
logger.error(`Error in canHandle for inspector ${inspector.id}:`, error);
this._logger.error(`Error in canHandle for ${inspector.id}:`, error);
}
}
return undefined;
}
/**
* 查找所有追加模式的检查器
* Find all append-mode inspectors for the component
* @zh 查找所有追加模式的检查器
* @en Find all append-mode inspectors for the component
*/
findAppendInspectors(component: Component): IComponentInspector[] {
const sorted = this.getAllSorted().filter(i => i.renderMode === 'append');
const result: IComponentInspector[] = [];
const inspectors = Array.from(this.inspectors.values())
.filter(i => i.renderMode === 'append')
.sort((a, b) => (b.priority || 0) - (a.priority || 0));
for (const inspector of inspectors) {
for (const inspector of sorted) {
try {
if (inspector.canHandle(component)) {
result.push(inspector);
}
} catch (error) {
logger.error(`Error in canHandle for inspector ${inspector.id}:`, error);
this._logger.error(`Error in canHandle for ${inspector.id}:`, error);
}
}
return result;
}
/**
* 检查是否有自定义检查器replace 模式)
* @zh 检查是否有自定义检查器replace 模式)
* @en Check if has custom inspector (replace mode)
*/
hasInspector(component: Component): boolean {
return this.findInspector(component) !== undefined;
}
/**
* 检查是否有追加检查器
* @zh 检查是否有追加检查器
* @en Check if has append inspectors
*/
hasAppendInspectors(component: Component): boolean {
return this.findAppendInspectors(component).length > 0;
}
/**
* 渲染组件replace 模式)
* Render component with replace-mode inspector
* @zh 渲染组件replace 模式)
* @en Render component with replace-mode inspector
*/
render(context: ComponentInspectorContext): React.ReactElement | null {
const inspector = this.findInspector(context.component);
if (!inspector) {
return null;
}
if (!inspector) return null;
try {
return inspector.render(context);
} catch (error) {
logger.error(`Error rendering with inspector ${inspector.id}:`, error);
this._logger.error(`Error rendering with ${inspector.id}:`, error);
return React.createElement(
'span',
{ style: { color: '#f87171', fontStyle: 'italic' } },
@@ -166,46 +140,37 @@ export class ComponentInspectorRegistry implements IService {
}
/**
* 渲染追加检查器
* Render append-mode inspectors
* @zh 渲染追加检查器
* @en Render append-mode inspectors
*/
renderAppendInspectors(context: ComponentInspectorContext): React.ReactElement[] {
const inspectors = this.findAppendInspectors(context.component);
const elements: React.ReactElement[] = [];
for (const inspector of inspectors) {
return inspectors.map(inspector => {
try {
elements.push(
React.createElement(
React.Fragment,
{ key: inspector.id },
inspector.render(context)
)
return React.createElement(
React.Fragment,
{ key: inspector.id },
inspector.render(context)
);
} catch (error) {
logger.error(`Error rendering append inspector ${inspector.id}:`, error);
elements.push(
React.createElement(
'span',
{ key: inspector.id, style: { color: '#f87171', fontStyle: 'italic' } },
`[${inspector.name} Error]`
)
this._logger.error(`Error rendering ${inspector.id}:`, error);
return React.createElement(
'span',
{ key: inspector.id, style: { color: '#f87171', fontStyle: 'italic' } },
`[${inspector.name} Error]`
);
}
}
return elements;
});
}
/**
* 获取所有注册的检查器
* @zh 获取所有注册的检查器
* @en Get all registered inspectors
*/
getAllInspectors(): IComponentInspector[] {
return Array.from(this.inspectors.values());
}
dispose(): void {
this.inspectors.clear();
logger.debug('ComponentInspectorRegistry disposed');
return this.getAll();
}
}
/** @zh 组件检查器注册表服务标识符 @en Component inspector registry service identifier */
export const IComponentInspectorRegistry = createRegistryToken<ComponentInspectorRegistry>('ComponentInspectorRegistry');

View File

@@ -1,60 +1,85 @@
import { Injectable, IService, Component } from '@esengine/ecs-framework';
import { Component } from '@esengine/ecs-framework';
import { BaseRegistry, createRegistryToken } from './BaseRegistry';
/**
* @zh 组件类型信息
* @en Component type info
*/
export interface ComponentTypeInfo {
name: string;
type?: new (...args: any[]) => Component;
category?: string;
description?: string;
icon?: string;
metadata?: {
path?: string;
fileName?: string;
[key: string]: any;
};
/** @zh 组件名称 @en Component name */
name: string;
/** @zh 组件类型 @en Component type */
type?: new (...args: any[]) => Component;
/** @zh 分类 @en Category */
category?: string;
/** @zh 描述 @en Description */
description?: string;
/** @zh 图标 @en Icon */
icon?: string;
/** @zh 元数据 @en Metadata */
metadata?: {
path?: string;
fileName?: string;
[key: string]: any;
};
}
/**
* 编辑器组件注册表
* Editor Component Registry
* @zh 编辑器组件注册表
* @en Editor Component Registry
*
* 管理编辑器中可用的组件类型元数据(名称、分类、图标等)。
* 与 ECS 核心的 ComponentRegistry管理组件位掩码不同。
*
* Manages component type metadata (name, category, icon, etc.) for the editor.
* Different from the ECS core ComponentRegistry (which manages component bitmasks).
* @zh 管理编辑器中可用的组件类型元数据(名称、分类、图标等)。
* 与 ECS 核心的 ComponentRegistry管理组件位掩码不同。
* @en Manages component type metadata (name, category, icon, etc.) for the editor.
* Different from the ECS core ComponentRegistry (which manages component bitmasks).
*/
@Injectable()
export class EditorComponentRegistry implements IService {
private components: Map<string, ComponentTypeInfo> = new Map();
public dispose(): void {
this.components.clear();
export class EditorComponentRegistry extends BaseRegistry<ComponentTypeInfo> {
constructor() {
super('EditorComponentRegistry');
}
public register(info: ComponentTypeInfo): void {
this.components.set(info.name, info);
protected getItemKey(item: ComponentTypeInfo): string {
return item.name;
}
public unregister(name: string): void {
this.components.delete(name);
protected override getItemDisplayName(item: ComponentTypeInfo): string {
return `${item.name}${item.category ? ` [${item.category}]` : ''}`;
}
public getComponent(name: string): ComponentTypeInfo | undefined {
return this.components.get(name);
/**
* @zh 获取组件信息
* @en Get component info
*/
getComponent(name: string): ComponentTypeInfo | undefined {
return this.get(name);
}
public getAllComponents(): ComponentTypeInfo[] {
return Array.from(this.components.values());
/**
* @zh 获取所有组件
* @en Get all components
*/
getAllComponents(): ComponentTypeInfo[] {
return this.getAll();
}
public getComponentsByCategory(category: string): ComponentTypeInfo[] {
return this.getAllComponents().filter((c) => c.category === category);
/**
* @zh 按分类获取组件
* @en Get components by category
*/
getComponentsByCategory(category: string): ComponentTypeInfo[] {
return this.filter(c => c.category === category);
}
public createInstance(name: string, ...args: any[]): Component | null {
const info = this.components.get(name);
/**
* @zh 创建组件实例
* @en Create component instance
*/
createInstance(name: string, ...args: any[]): Component | null {
const info = this.get(name);
if (!info || !info.type) return null;
return new info.type(...args);
}
}
/** @zh 编辑器组件注册表服务标识符 @en Editor component registry service identifier */
export const IEditorComponentRegistry = createRegistryToken<EditorComponentRegistry>('EditorComponentRegistry');

View File

@@ -6,11 +6,14 @@
* 管理带有预览场景支持和覆盖层渲染的编辑器视口。
*/
import { createLogger } from '@esengine/ecs-framework';
import type { IViewportService, ViewportCameraConfig } from './IViewportService';
import type { IPreviewScene } from './PreviewSceneService';
import { PreviewSceneService } from './PreviewSceneService';
import type { IViewportOverlay, OverlayRenderContext } from '../Rendering/IViewportOverlay';
const logger = createLogger('EditorViewportService');
/**
* Configuration for an editor viewport
* 编辑器视口配置
@@ -201,7 +204,7 @@ export class EditorViewportService implements IEditorViewportService {
registerViewport(config: EditorViewportConfig): void {
if (this._viewports.has(config.id)) {
console.warn(`[EditorViewportService] Viewport "${config.id}" already registered`);
logger.warn(`Viewport already registered: ${config.id}`);
return;
}
@@ -266,16 +269,17 @@ export class EditorViewportService implements IEditorViewportService {
addOverlay(viewportId: string, overlay: IViewportOverlay): void {
const state = this._viewports.get(viewportId);
if (!state) {
console.warn(`[EditorViewportService] Viewport "${viewportId}" not found`);
logger.warn(`Viewport not found: ${viewportId}`);
return;
}
if (state.overlays.has(overlay.id)) {
console.warn(`[EditorViewportService] Overlay "${overlay.id}" already exists in viewport "${viewportId}"`);
logger.warn(`Overlay already exists: ${overlay.id} in ${viewportId}`);
return;
}
state.overlays.set(overlay.id, overlay);
logger.debug(`Added overlay: ${overlay.id} to ${viewportId}`);
}
removeOverlay(viewportId: string, overlayId: string): void {

View File

@@ -1,76 +1,47 @@
/**
* Entity Creation Registry Service
* @zh 实体创建模板注册表
* @en Entity Creation Registry Service
*
* Manages entity creation templates for the scene hierarchy context menu
* @zh 管理场景层级右键菜单中的实体创建模板
* @en Manages entity creation templates for the scene hierarchy context menu
*/
import { injectable } from 'tsyringe';
import type { IService } from '@esengine/ecs-framework';
import { BaseRegistry, createRegistryToken } from './BaseRegistry';
import type { EntityCreationTemplate } from '../Types/UITypes';
@injectable()
export class EntityCreationRegistry implements IService {
private templates: Map<string, EntityCreationTemplate> = new Map();
/**
* @zh 实体创建模板注册表
* @en Entity Creation Registry
*/
export class EntityCreationRegistry extends BaseRegistry<EntityCreationTemplate> {
constructor() {
super('EntityCreationRegistry');
}
/**
* Register an entity creation template
*/
register(template: EntityCreationTemplate): void {
if (this.templates.has(template.id)) {
console.warn(`[EntityCreationRegistry] Template '${template.id}' already exists, overwriting`);
}
this.templates.set(template.id, template);
protected getItemKey(item: EntityCreationTemplate): string {
return item.id;
}
protected override getItemDisplayName(item: EntityCreationTemplate): string {
return `${item.label} (${item.id})`;
}
/**
* Register multiple templates
* @zh 获取所有模板(按 order 排序)
* @en Get all templates sorted by order
*/
registerMany(templates: EntityCreationTemplate[]): void {
for (const template of templates) {
this.register(template);
}
getAllSorted(): EntityCreationTemplate[] {
return this.sortByOrder(this.getAll(), 100);
}
/**
* Unregister a template by ID
* @zh 获取指定分类的模板
* @en Get templates by category
*/
unregister(id: string): void {
this.templates.delete(id);
}
/**
* Get all registered templates sorted by order
*/
getAll(): EntityCreationTemplate[] {
return Array.from(this.templates.values())
.sort((a, b) => (a.order ?? 100) - (b.order ?? 100));
}
/**
* Get a template by ID
*/
get(id: string): EntityCreationTemplate | undefined {
return this.templates.get(id);
}
/**
* Check if a template exists
*/
has(id: string): boolean {
return this.templates.has(id);
}
/**
* Clear all templates
*/
clear(): void {
this.templates.clear();
}
/**
* Dispose resources
*/
dispose(): void {
this.templates.clear();
getByCategory(category: string): EntityCreationTemplate[] {
return this.sortByOrder(this.filter(t => t.category === category), 100);
}
}
/** @zh 实体创建模板注册表服务标识符 @en Entity creation registry service identifier */
export const IEntityCreationRegistry = createRegistryToken<EntityCreationRegistry>('EntityCreationRegistry');

View File

@@ -1,50 +1,52 @@
import { IService, createLogger } from '@esengine/ecs-framework';
import { IFieldEditor, IFieldEditorRegistry, FieldEditorContext } from './IFieldEditor';
/**
* @zh 字段编辑器注册表
* @en Field Editor Registry
*/
const logger = createLogger('FieldEditorRegistry');
import { PrioritizedRegistry, createRegistryToken } from './BaseRegistry';
import type { IFieldEditor, IFieldEditorRegistry, FieldEditorContext } from './IFieldEditor';
export class FieldEditorRegistry implements IFieldEditorRegistry, IService {
private editors: Map<string, IFieldEditor> = new Map();
/**
* @zh 字段编辑器注册表
* @en Field Editor Registry
*/
export class FieldEditorRegistry
extends PrioritizedRegistry<IFieldEditor>
implements IFieldEditorRegistry {
register(editor: IFieldEditor): void {
if (this.editors.has(editor.type)) {
logger.warn(`Overwriting existing field editor: ${editor.type}`);
}
this.editors.set(editor.type, editor);
logger.debug(`Registered field editor: ${editor.name} (${editor.type})`);
constructor() {
super('FieldEditorRegistry');
}
unregister(type: string): void {
if (this.editors.delete(type)) {
logger.debug(`Unregistered field editor: ${type}`);
}
protected getItemKey(item: IFieldEditor): string {
return item.type;
}
protected override getItemDisplayName(item: IFieldEditor): string {
return `${item.name} (${item.type})`;
}
/**
* @zh 获取字段编辑器
* @en Get field editor
*/
getEditor(type: string, context?: FieldEditorContext): IFieldEditor | undefined {
const editor = this.editors.get(type);
if (editor) {
return editor;
}
// 先尝试精确匹配
const exact = this.get(type);
if (exact) return exact;
const editors = Array.from(this.editors.values())
.sort((a, b) => (b.priority || 0) - (a.priority || 0));
for (const editor of editors) {
if (editor.canHandle(type, context)) {
return editor;
}
}
return undefined;
// 再按优先级查找可处理的编辑器
return this.findByPriority(editor => editor.canHandle(type, context));
}
/**
* @zh 获取所有编辑器
* @en Get all editors
*/
getAllEditors(): IFieldEditor[] {
return Array.from(this.editors.values());
}
dispose(): void {
this.editors.clear();
logger.debug('FieldEditorRegistry disposed');
return this.getAll();
}
}
/** @zh 字段编辑器注册表服务标识符 @en Field editor registry service identifier */
export const FieldEditorRegistryToken = createRegistryToken<FieldEditorRegistry>('FieldEditorRegistry');

View File

@@ -1,115 +1,99 @@
import { IService } from '@esengine/ecs-framework';
import { IService, createLogger, type ILogger } from '@esengine/ecs-framework';
import type { FileActionHandler, FileCreationTemplate } from '../Plugin/EditorModule';
import { createRegistryToken } from './BaseRegistry';
// Re-export for backwards compatibility
export type { FileCreationTemplate } from '../Plugin/EditorModule';
/**
* 资产创建消息映射
* Asset creation message mapping
*
* 定义扩展名到创建消息的映射,用于 PropertyInspector 中的资产字段创建按钮
* @zh 资产创建消息映射
* @en Asset creation message mapping
*/
export interface AssetCreationMapping {
/** 文件扩展名(包含点号,如 '.tilemap'| File extension (with dot) */
/** @zh 文件扩展名(包含点号,如 '.tilemap' @en File extension (with dot) */
extension: string;
/** 创建资产时发送的消息名 | Message name to publish when creating asset */
/** @zh 创建资产时发送的消息名 @en Message name to publish when creating asset */
createMessage: string;
/** 是否支持创建(可选,默认 true| Whether creation is supported */
/** @zh 是否支持创建(可选,默认 true @en Whether creation is supported */
canCreate?: boolean;
}
/**
* FileActionRegistry 服务标识符
* FileActionRegistry service identifier
*/
export const IFileActionRegistry = Symbol.for('IFileActionRegistry');
/** @zh FileActionRegistry 服务标识符 @en FileActionRegistry service identifier */
export const IFileActionRegistry = createRegistryToken<FileActionRegistry>('FileActionRegistry');
/**
* 文件操作注册表服务
*
* 管理插件注册的文件操作处理器和文件创建模板
* @zh 文件操作注册表服务 - 管理插件注册的文件操作处理器和文件创建模板
* @en File Action Registry Service - Manages file action handlers and creation templates
*/
export class FileActionRegistry implements IService {
private actionHandlers: Map<string, FileActionHandler[]> = new Map();
private creationTemplates: FileCreationTemplate[] = [];
private assetCreationMappings: Map<string, AssetCreationMapping> = new Map();
private readonly _actionHandlers = new Map<string, FileActionHandler[]>();
private readonly _creationTemplates: FileCreationTemplate[] = [];
private readonly _assetCreationMappings = new Map<string, AssetCreationMapping>();
private readonly _logger: ILogger;
constructor() {
this._logger = createLogger('FileActionRegistry');
}
/**
* 注册文件操作处理器
* @zh 规范化扩展名(确保以 . 开头且小写)
* @en Normalize extension (ensure starts with . and lowercase)
*/
private _normalizeExtension(ext: string): string {
const lower = ext.toLowerCase();
return lower.startsWith('.') ? lower : `.${lower}`;
}
/** @zh 注册文件操作处理器 @en Register file action handler */
registerActionHandler(handler: FileActionHandler): void {
for (const ext of handler.extensions) {
const handlers = this.actionHandlers.get(ext) || [];
const handlers = this._actionHandlers.get(ext) ?? [];
handlers.push(handler);
this.actionHandlers.set(ext, handlers);
this._actionHandlers.set(ext, handlers);
}
}
/**
* 注销文件操作处理器
*/
/** @zh 注销文件操作处理器 @en Unregister file action handler */
unregisterActionHandler(handler: FileActionHandler): void {
for (const ext of handler.extensions) {
const handlers = this.actionHandlers.get(ext);
if (handlers) {
const index = handlers.indexOf(handler);
if (index !== -1) {
handlers.splice(index, 1);
}
if (handlers.length === 0) {
this.actionHandlers.delete(ext);
}
}
const handlers = this._actionHandlers.get(ext);
if (!handlers) continue;
const index = handlers.indexOf(handler);
if (index !== -1) handlers.splice(index, 1);
if (handlers.length === 0) this._actionHandlers.delete(ext);
}
}
/**
* 注册文件创建模板
*/
/** @zh 注册文件创建模板 @en Register file creation template */
registerCreationTemplate(template: FileCreationTemplate): void {
this.creationTemplates.push(template);
this._creationTemplates.push(template);
}
/**
* 注销文件创建模板
*/
/** @zh 注销文件创建模板 @en Unregister file creation template */
unregisterCreationTemplate(template: FileCreationTemplate): void {
const index = this.creationTemplates.indexOf(template);
if (index !== -1) {
this.creationTemplates.splice(index, 1);
}
const index = this._creationTemplates.indexOf(template);
if (index !== -1) this._creationTemplates.splice(index, 1);
}
/**
* 获取文件扩展名的处理器
*/
/** @zh 获取文件扩展名的处理器 @en Get handlers for extension */
getHandlersForExtension(extension: string): FileActionHandler[] {
return this.actionHandlers.get(extension) || [];
return this._actionHandlers.get(extension) ?? [];
}
/**
* 获取文件的处理器
*/
/** @zh 获取文件的处理器 @en Get handlers for file */
getHandlersForFile(filePath: string): FileActionHandler[] {
const extension = this.getFileExtension(filePath);
return extension ? this.getHandlersForExtension(extension) : [];
const ext = this._extractFileExtension(filePath);
return ext ? this.getHandlersForExtension(ext) : [];
}
/**
* 获取所有文件创建模板
*/
/** @zh 获取所有文件创建模板 @en Get all creation templates */
getCreationTemplates(): FileCreationTemplate[] {
return this.creationTemplates;
return this._creationTemplates;
}
/**
* 处理文件双击
*/
/** @zh 处理文件双击 @en Handle file double click */
async handleDoubleClick(filePath: string): Promise<boolean> {
const handlers = this.getHandlersForFile(filePath);
for (const handler of handlers) {
for (const handler of this.getHandlersForFile(filePath)) {
if (handler.onDoubleClick) {
await handler.onDoubleClick(filePath);
return true;
@@ -118,12 +102,9 @@ export class FileActionRegistry implements IService {
return false;
}
/**
* 处理文件打开
*/
/** @zh 处理文件打开 @en Handle file open */
async handleOpen(filePath: string): Promise<boolean> {
const handlers = this.getHandlersForFile(filePath);
for (const handler of handlers) {
for (const handler of this.getHandlersForFile(filePath)) {
if (handler.onOpen) {
await handler.onOpen(filePath);
return true;
@@ -132,78 +113,51 @@ export class FileActionRegistry implements IService {
return false;
}
/**
* 注册资产创建消息映射
* Register asset creation message mapping
*/
/** @zh 注册资产创建消息映射 @en Register asset creation mapping */
registerAssetCreationMapping(mapping: AssetCreationMapping): void {
const normalizedExt = mapping.extension.startsWith('.')
? mapping.extension.toLowerCase()
: `.${mapping.extension.toLowerCase()}`;
this.assetCreationMappings.set(normalizedExt, {
...mapping,
extension: normalizedExt
});
const ext = this._normalizeExtension(mapping.extension);
this._assetCreationMappings.set(ext, { ...mapping, extension: ext });
this._logger.debug(`Registered asset creation mapping: ${ext}`);
}
/**
* 注销资产创建消息映射
* Unregister asset creation message mapping
*/
/** @zh 注销资产创建消息映射 @en Unregister asset creation mapping */
unregisterAssetCreationMapping(extension: string): void {
const normalizedExt = extension.startsWith('.')
? extension.toLowerCase()
: `.${extension.toLowerCase()}`;
this.assetCreationMappings.delete(normalizedExt);
const ext = this._normalizeExtension(extension);
if (this._assetCreationMappings.delete(ext)) {
this._logger.debug(`Unregistered asset creation mapping: ${ext}`);
}
}
/**
* 获取扩展名对应的资产创建消息映射
* Get asset creation mapping for extension
*/
/** @zh 获取扩展名对应的资产创建消息映射 @en Get asset creation mapping for extension */
getAssetCreationMapping(extension: string): AssetCreationMapping | undefined {
const normalizedExt = extension.startsWith('.')
? extension.toLowerCase()
: `.${extension.toLowerCase()}`;
return this.assetCreationMappings.get(normalizedExt);
return this._assetCreationMappings.get(this._normalizeExtension(extension));
}
/**
* 检查扩展名是否支持创建资产
* Check if extension supports asset creation
*/
/** @zh 检查扩展名是否支持创建资产 @en Check if extension supports asset creation */
canCreateAsset(extension: string): boolean {
const mapping = this.getAssetCreationMapping(extension);
return mapping?.canCreate !== false;
return this.getAssetCreationMapping(extension)?.canCreate !== false;
}
/**
* 获取所有资产创建映射
* Get all asset creation mappings
*/
/** @zh 获取所有资产创建映射 @en Get all asset creation mappings */
getAllAssetCreationMappings(): AssetCreationMapping[] {
return Array.from(this.assetCreationMappings.values());
return Array.from(this._assetCreationMappings.values());
}
/**
* 清空所有注册
*/
/** @zh 清空所有注册 @en Clear all registrations */
clear(): void {
this.actionHandlers.clear();
this.creationTemplates = [];
this.assetCreationMappings.clear();
this._actionHandlers.clear();
this._creationTemplates.length = 0;
this._assetCreationMappings.clear();
}
/**
* 释放资源
*/
/** @zh 释放资源 @en Dispose resources */
dispose(): void {
this.clear();
}
private getFileExtension(filePath: string): string | null {
/** @zh 提取文件扩展名 @en Extract file extension */
private _extractFileExtension(filePath: string): string | null {
const lastDot = filePath.lastIndexOf('.');
if (lastDot === -1) return null;
return filePath.substring(lastDot + 1).toLowerCase();
return lastDot === -1 ? null : filePath.substring(lastDot + 1).toLowerCase();
}
}

View File

@@ -1,81 +1,58 @@
import { IInspectorProvider, InspectorContext } from './IInspectorProvider';
import { IService } from '@esengine/ecs-framework';
/**
* @zh Inspector 注册表
* @en Inspector Registry
*/
import React from 'react';
import { PrioritizedRegistry, createRegistryToken } from './BaseRegistry';
import type { IInspectorProvider, InspectorContext } from './IInspectorProvider';
export class InspectorRegistry implements IService {
private providers: Map<string, IInspectorProvider> = new Map();
/**
* @zh Inspector 注册表
* @en Inspector Registry
*/
export class InspectorRegistry extends PrioritizedRegistry<IInspectorProvider> {
constructor() {
super('InspectorRegistry');
}
/**
* 注册Inspector提供器
*/
register(provider: IInspectorProvider): void {
if (this.providers.has(provider.id)) {
console.warn(`Inspector provider with id "${provider.id}" is already registered`);
return;
}
this.providers.set(provider.id, provider);
protected getItemKey(item: IInspectorProvider): string {
return item.id;
}
/**
* 注销Inspector提供器
*/
unregister(providerId: string): void {
this.providers.delete(providerId);
}
/**
* 获取指定ID的提供器
* @zh 获取指定 ID 的提供器
* @en Get provider by ID
*/
getProvider(providerId: string): IInspectorProvider | undefined {
return this.providers.get(providerId);
return this.get(providerId);
}
/**
* 获取所有提供器
* @zh 获取所有提供器
* @en Get all providers
*/
getAllProviders(): IInspectorProvider[] {
return Array.from(this.providers.values());
return this.getAll();
}
/**
* 查找可以处理指定目标的提供器
* 按优先级排序,返回第一个可以处理的提供器
* @zh 查找可以处理指定目标的提供器
* @en Find provider that can handle the target
*/
findProvider(target: unknown): IInspectorProvider | undefined {
const providers = Array.from(this.providers.values())
.sort((a, b) => (b.priority || 0) - (a.priority || 0));
for (const provider of providers) {
if (provider.canHandle(target)) {
return provider;
}
}
return undefined;
return this.findByPriority(provider => provider.canHandle(target));
}
/**
* 渲染Inspector内容
* 自动查找合适的提供器并渲染
* @zh 渲染 Inspector 内容
* @en Render inspector content
*/
render(target: unknown, context: InspectorContext): React.ReactElement | null {
const provider = this.findProvider(target);
if (!provider) {
return null;
}
return provider.render(target, context);
}
clear(): void {
this.providers.clear();
}
dispose(): void {
this.clear();
return provider?.render(target, context) ?? null;
}
}
// Service identifier for DI registration (用于跨包插件访问)
// 使用 Symbol.for 确保跨包共享同一个 Symbol
export const IInspectorRegistry = Symbol.for('IInspectorRegistry');
/** @zh Inspector 注册表服务标识符 @en Inspector registry service identifier */
export const IInspectorRegistry = createRegistryToken<InspectorRegistry>('InspectorRegistry');

View File

@@ -91,14 +91,14 @@ export type TranslationParams = Record<string, string | number>;
*/
@Injectable()
export class LocaleService implements IService {
private currentLocale: Locale = 'en';
private translations: Map<Locale, Translations> = new Map();
private changeListeners: Set<(locale: Locale) => void> = new Set();
private _currentLocale: Locale = 'en';
private _translations: Map<Locale, Translations> = new Map();
private _changeListeners: Set<(locale: Locale) => void> = new Set();
constructor() {
const savedLocale = this.loadSavedLocale();
const savedLocale = this._loadSavedLocale();
if (savedLocale) {
this.currentLocale = savedLocale;
this._currentLocale = savedLocale;
}
}
@@ -113,7 +113,7 @@ export class LocaleService implements IService {
* @param translations - 翻译对象 | Translation object
*/
public registerTranslations(locale: Locale, translations: Translations): void {
this.translations.set(locale, translations);
this._translations.set(locale, translations);
logger.info(`Registered translations for locale: ${locale}`);
}
@@ -153,19 +153,19 @@ export class LocaleService implements IService {
const locales: Locale[] = ['en', 'zh', 'es'];
for (const locale of locales) {
const existing = this.translations.get(locale) || {};
const existing = this._translations.get(locale) || {};
const pluginTrans = pluginTranslations[locale];
if (pluginTrans) {
// 深度合并到命名空间下 | Deep merge under namespace
const merged = {
...existing,
[namespace]: this.deepMerge(
[namespace]: this._deepMerge(
(existing[namespace] as Translations) || {},
pluginTrans
)
};
this.translations.set(locale, merged);
this._translations.set(locale, merged);
}
}
@@ -176,7 +176,7 @@ export class LocaleService implements IService {
* 深度合并两个翻译对象
* Deep merge two translation objects
*/
private deepMerge(target: Translations, source: Translations): Translations {
private _deepMerge(target: Translations, source: Translations): Translations {
const result: Translations = { ...target };
for (const key of Object.keys(source)) {
@@ -189,7 +189,7 @@ export class LocaleService implements IService {
typeof targetValue === 'object' &&
targetValue !== null
) {
result[key] = this.deepMerge(
result[key] = this._deepMerge(
targetValue as Translations,
sourceValue as Translations
);
@@ -206,7 +206,7 @@ export class LocaleService implements IService {
* Get current locale
*/
public getCurrentLocale(): Locale {
return this.currentLocale;
return this._currentLocale;
}
/**
@@ -224,15 +224,15 @@ export class LocaleService implements IService {
* @param locale - 目标语言代码 | Target locale code
*/
public setLocale(locale: Locale): void {
if (!this.translations.has(locale)) {
if (!this._translations.has(locale)) {
logger.warn(`Translations not found for locale: ${locale}`);
return;
}
this.currentLocale = locale;
this.saveLocale(locale);
this._currentLocale = locale;
this._saveLocale(locale);
this.changeListeners.forEach((listener) => listener(locale));
this._changeListeners.forEach((listener) => listener(locale));
logger.info(`Locale changed to: ${locale}`);
}
@@ -261,12 +261,12 @@ export class LocaleService implements IService {
* ```
*/
public t(key: string, params?: TranslationParams, fallback?: string): string {
const translations = this.translations.get(this.currentLocale);
const translations = this._translations.get(this._currentLocale);
if (!translations) {
return fallback || key;
}
const value = this.getNestedValue(translations, key);
const value = this._getNestedValue(translations, key);
if (typeof value === 'string') {
// 支持参数替换 {{key}} | Support parameter substitution {{key}}
if (params) {
@@ -288,10 +288,10 @@ export class LocaleService implements IService {
* @returns 取消订阅函数 | Unsubscribe function
*/
public onChange(listener: (locale: Locale) => void): () => void {
this.changeListeners.add(listener);
this._changeListeners.add(listener);
return () => {
this.changeListeners.delete(listener);
this._changeListeners.delete(listener);
};
}
@@ -303,12 +303,12 @@ export class LocaleService implements IService {
* @param locale - 可选的语言代码,默认使用当前语言 | Optional locale, defaults to current
*/
public hasKey(key: string, locale?: Locale): boolean {
const targetLocale = locale || this.currentLocale;
const translations = this.translations.get(targetLocale);
const targetLocale = locale || this._currentLocale;
const translations = this._translations.get(targetLocale);
if (!translations) {
return false;
}
const value = this.getNestedValue(translations, key);
const value = this._getNestedValue(translations, key);
return typeof value === 'string';
}
@@ -316,7 +316,7 @@ export class LocaleService implements IService {
* 获取嵌套对象的值
* Get nested object value
*/
private getNestedValue(obj: Translations, path: string): string | Translations | undefined {
private _getNestedValue(obj: Translations, path: string): string | Translations | undefined {
const keys = path.split('.');
let current: string | Translations | undefined = obj;
@@ -335,7 +335,7 @@ export class LocaleService implements IService {
* 从 localStorage 加载保存的语言设置
* Load saved locale from localStorage
*/
private loadSavedLocale(): Locale | null {
private _loadSavedLocale(): Locale | null {
try {
const saved = localStorage.getItem('editor-locale');
if (saved === 'en' || saved === 'zh' || saved === 'es') {
@@ -351,7 +351,7 @@ export class LocaleService implements IService {
* 保存语言设置到 localStorage
* Save locale to localStorage
*/
private saveLocale(locale: Locale): void {
private _saveLocale(locale: Locale): void {
try {
localStorage.setItem('editor-locale', locale);
} catch (error) {
@@ -360,8 +360,8 @@ export class LocaleService implements IService {
}
public dispose(): void {
this.translations.clear();
this.changeListeners.clear();
this._translations.clear();
this._changeListeners.clear();
logger.info('LocaleService disposed');
}
}

View File

@@ -6,6 +6,7 @@
* 管理引擎模块、其依赖关系和项目配置。
*/
import { createLogger } from '@esengine/ecs-framework';
import type {
ModuleManifest,
ModuleRegistryEntry,
@@ -37,7 +38,8 @@ export interface IModuleFileSystem {
* 模块注册表服务。
*/
export class ModuleRegistry {
private _modules: Map<string, ModuleRegistryEntry> = new Map();
private readonly _modules = new Map<string, ModuleRegistryEntry>();
private readonly _logger = createLogger('ModuleRegistry');
private _projectConfig: ProjectModuleConfig = { enabled: [] };
private _fileSystem: IModuleFileSystem | null = null;
private _engineModulesPath: string = '';
@@ -332,7 +334,7 @@ export class ModuleRegistry {
}>;
}>(indexPath);
console.log(`[ModuleRegistry] Loaded ${index.modules.length} modules from index.json`);
this._logger.debug(`Loaded ${index.modules.length} modules from index.json`);
// Use data directly from index.json (includes jsSize, wasmSize)
// 直接使用 index.json 中的数据(包含 jsSize、wasmSize
@@ -386,10 +388,10 @@ export class ModuleRegistry {
}
}
} else {
console.warn(`[ModuleRegistry] index.json not found at ${indexPath}, run 'pnpm copy-modules' first`);
this._logger.warn(`index.json not found at ${indexPath}, run 'pnpm copy-modules' first`);
}
} catch (error) {
console.error('[ModuleRegistry] Failed to load index.json:', error);
this._logger.error('Failed to load index.json:', error);
}
// Compute dependents | 计算依赖者
@@ -433,7 +435,7 @@ export class ModuleRegistry {
this._projectConfig = { enabled: this._getDefaultEnabledModules() };
}
} catch (error) {
console.error('[ModuleRegistry] Failed to load project config:', error);
this._logger.error('Failed to load project config:', error);
this._projectConfig = { enabled: this._getDefaultEnabledModules() };
}
}
@@ -457,7 +459,7 @@ export class ModuleRegistry {
config.modules = this._projectConfig;
await this._fileSystem.writeJson(configPath, config);
} catch (error) {
console.error('[ModuleRegistry] Failed to save project config:', error);
this._logger.error('Failed to save project config:', error);
}
}
@@ -545,7 +547,7 @@ export class ModuleRegistry {
}
}
} catch (error) {
console.warn('[ModuleRegistry] Failed to check scene usage:', error);
this._logger.warn('Failed to check scene usage:', error);
}
return usages;
@@ -602,7 +604,7 @@ export class ModuleRegistry {
}
}
} catch (error) {
console.warn('[ModuleRegistry] Failed to check script usage:', error);
this._logger.warn('Failed to check script usage:', error);
}
return usages;

View File

@@ -1,56 +1,63 @@
/**
* @zh 属性渲染器注册表
* @en Property Renderer Registry
*/
import React from 'react';
import { IService, createLogger } from '@esengine/ecs-framework';
import { IPropertyRenderer, IPropertyRendererRegistry, PropertyContext } from './IPropertyRenderer';
import { PrioritizedRegistry, createRegistryToken } from './BaseRegistry';
import type { IPropertyRenderer, IPropertyRendererRegistry, PropertyContext } from './IPropertyRenderer';
const logger = createLogger('PropertyRendererRegistry');
/**
* @zh 属性渲染器注册表
* @en Property Renderer Registry
*/
export class PropertyRendererRegistry
extends PrioritizedRegistry<IPropertyRenderer>
implements IPropertyRendererRegistry {
export class PropertyRendererRegistry implements IPropertyRendererRegistry, IService {
private renderers: Map<string, IPropertyRenderer> = new Map();
register(renderer: IPropertyRenderer): void {
if (this.renderers.has(renderer.id)) {
logger.warn(`Overwriting existing property renderer: ${renderer.id}`);
}
this.renderers.set(renderer.id, renderer);
logger.debug(`Registered property renderer: ${renderer.name} (${renderer.id})`);
constructor() {
super('PropertyRendererRegistry');
}
unregister(rendererId: string): void {
if (this.renderers.delete(rendererId)) {
logger.debug(`Unregistered property renderer: ${rendererId}`);
}
protected getItemKey(item: IPropertyRenderer): string {
return item.id;
}
findRenderer(value: any, context: PropertyContext): IPropertyRenderer | undefined {
const renderers = Array.from(this.renderers.values())
.sort((a, b) => (b.priority || 0) - (a.priority || 0));
protected override getItemDisplayName(item: IPropertyRenderer): string {
return `${item.name} (${item.id})`;
}
for (const renderer of renderers) {
/**
* @zh 查找渲染器
* @en Find renderer
*/
findRenderer(value: unknown, context: PropertyContext): IPropertyRenderer | undefined {
return this.findByPriority(renderer => {
try {
if (renderer.canHandle(value, context)) {
return renderer;
}
return renderer.canHandle(value, context);
} catch (error) {
logger.error(`Error in canHandle for renderer ${renderer.id}:`, error);
this._logger.error(`Error in canHandle for ${renderer.id}:`, error);
return false;
}
}
return undefined;
});
}
render(value: any, context: PropertyContext): React.ReactElement | null {
/**
* @zh 渲染属性
* @en Render property
*/
render(value: unknown, context: PropertyContext): React.ReactElement | null {
const renderer = this.findRenderer(value, context);
if (!renderer) {
logger.debug(`No renderer found for value type: ${typeof value}`);
this._logger.debug(`No renderer found for value type: ${typeof value}`);
return null;
}
try {
return renderer.render(value, context);
} catch (error) {
logger.error(`Error rendering with ${renderer.id}:`, error);
this._logger.error(`Error rendering with ${renderer.id}:`, error);
return React.createElement(
'span',
{ style: { color: '#f87171', fontStyle: 'italic' } },
@@ -59,16 +66,22 @@ export class PropertyRendererRegistry implements IPropertyRendererRegistry, ISer
}
}
/**
* @zh 获取所有渲染器
* @en Get all renderers
*/
getAllRenderers(): IPropertyRenderer[] {
return Array.from(this.renderers.values());
return this.getAll();
}
hasRenderer(value: any, context: PropertyContext): boolean {
/**
* @zh 检查是否有可用渲染器
* @en Check if renderer is available
*/
hasRenderer(value: unknown, context: PropertyContext): boolean {
return this.findRenderer(value, context) !== undefined;
}
dispose(): void {
this.renderers.clear();
logger.debug('PropertyRendererRegistry disposed');
}
}
/** @zh 属性渲染器注册表服务标识符 @en Property renderer registry service identifier */
export const PropertyRendererRegistryToken = createRegistryToken<PropertyRendererRegistry>('PropertyRendererRegistry');

View File

@@ -1,7 +1,7 @@
import type { IService } from '@esengine/ecs-framework';
import { Injectable } from '@esengine/ecs-framework';
import { createLogger } from '@esengine/ecs-framework';
import { Injectable, createLogger } from '@esengine/ecs-framework';
import type { ISerializer } from '../Plugin/EditorModule';
import { createRegistryToken } from './BaseRegistry';
const logger = createLogger('SerializerRegistry');
@@ -12,7 +12,7 @@ const logger = createLogger('SerializerRegistry');
*/
@Injectable()
export class SerializerRegistry implements IService {
private serializers: Map<string, ISerializer> = new Map();
private readonly _serializers = new Map<string, ISerializer>();
/**
* 注册序列化器
@@ -24,12 +24,12 @@ export class SerializerRegistry implements IService {
const type = serializer.getSupportedType();
const key = `${pluginName}:${type}`;
if (this.serializers.has(key)) {
if (this._serializers.has(key)) {
logger.warn(`Serializer for ${key} is already registered`);
return;
}
this.serializers.set(key, serializer);
this._serializers.set(key, serializer);
logger.info(`Registered serializer: ${key}`);
}
@@ -54,7 +54,7 @@ export class SerializerRegistry implements IService {
*/
public unregister(pluginName: string, type: string): boolean {
const key = `${pluginName}:${type}`;
const result = this.serializers.delete(key);
const result = this._serializers.delete(key);
if (result) {
logger.info(`Unregistered serializer: ${key}`);
@@ -72,14 +72,14 @@ export class SerializerRegistry implements IService {
const prefix = `${pluginName}:`;
const keysToDelete: string[] = [];
for (const key of this.serializers.keys()) {
for (const key of this._serializers.keys()) {
if (key.startsWith(prefix)) {
keysToDelete.push(key);
}
}
for (const key of keysToDelete) {
this.serializers.delete(key);
this._serializers.delete(key);
logger.info(`Unregistered serializer: ${key}`);
}
}
@@ -93,7 +93,7 @@ export class SerializerRegistry implements IService {
*/
public get(pluginName: string, type: string): ISerializer | undefined {
const key = `${pluginName}:${type}`;
return this.serializers.get(key);
return this._serializers.get(key);
}
/**
@@ -105,7 +105,7 @@ export class SerializerRegistry implements IService {
public findByType(type: string): ISerializer[] {
const result: ISerializer[] = [];
for (const [key, serializer] of this.serializers) {
for (const [key, serializer] of this._serializers) {
if (key.endsWith(`:${type}`)) {
result.push(serializer);
}
@@ -120,7 +120,7 @@ export class SerializerRegistry implements IService {
* @returns 序列化器映射表
*/
public getAll(): Map<string, ISerializer> {
return new Map(this.serializers);
return new Map(this._serializers);
}
/**
@@ -132,7 +132,7 @@ export class SerializerRegistry implements IService {
*/
public has(pluginName: string, type: string): boolean {
const key = `${pluginName}:${type}`;
return this.serializers.has(key);
return this._serializers.has(key);
}
/**
@@ -175,7 +175,10 @@ export class SerializerRegistry implements IService {
* 释放资源
*/
public dispose(): void {
this.serializers.clear();
this._serializers.clear();
logger.info('SerializerRegistry disposed');
}
}
/** @zh 序列化器注册表服务标识符 @en Serializer registry service identifier */
export const ISerializerRegistry = createRegistryToken<SerializerRegistry>('SerializerRegistry');

View File

@@ -1,5 +1,10 @@
import { Injectable, IService } from '@esengine/ecs-framework';
import { createLogger, type ILogger, type IService } from '@esengine/ecs-framework';
import { createRegistryToken } from './BaseRegistry';
/**
* @zh 设置类型
* @en Setting type
*/
export type SettingType = 'string' | 'number' | 'boolean' | 'select' | 'color' | 'range' | 'pluginList' | 'collisionMatrix' | 'moduleList';
/**
@@ -82,127 +87,154 @@ export interface SettingCategory {
sections: SettingSection[];
}
@Injectable()
/**
* @zh 设置注册表
* @en Settings Registry
*/
export class SettingsRegistry implements IService {
private categories: Map<string, SettingCategory> = new Map();
private readonly _categories = new Map<string, SettingCategory>();
private readonly _logger: ILogger;
public dispose(): void {
this.categories.clear();
constructor() {
this._logger = createLogger('SettingsRegistry');
}
public registerCategory(category: SettingCategory): void {
if (this.categories.has(category.id)) {
console.warn(`[SettingsRegistry] Category ${category.id} already registered, overwriting`);
}
console.log(`[SettingsRegistry] Registering category: ${category.id} (${category.title}), sections: ${category.sections.map(s => s.id).join(', ')}`);
this.categories.set(category.id, category);
/** @zh 释放资源 @en Dispose resources */
dispose(): void {
this._categories.clear();
this._logger.debug('Disposed');
}
public registerSection(categoryId: string, section: SettingSection): void {
let category = this.categories.get(categoryId);
if (!category) {
category = {
id: categoryId,
title: categoryId,
sections: []
};
this.categories.set(categoryId, category);
/**
* @zh 注册设置分类
* @en Register setting category
*/
registerCategory(category: SettingCategory): void {
if (this._categories.has(category.id)) {
this._logger.warn(`Overwriting category: ${category.id}`);
}
this._categories.set(category.id, category);
this._logger.debug(`Registered category: ${category.id}`);
}
const existingIndex = category.sections.findIndex((s) => s.id === section.id);
/**
* @zh 注册设置分区
* @en Register setting section
*/
registerSection(categoryId: string, section: SettingSection): void {
const category = this._ensureCategory(categoryId);
const existingIndex = category.sections.findIndex(s => s.id === section.id);
if (existingIndex >= 0) {
category.sections[existingIndex] = section;
console.warn(`[SettingsRegistry] Section ${section.id} in category ${categoryId} already exists, overwriting`);
this._logger.warn(`Overwriting section: ${section.id} in ${categoryId}`);
} else {
category.sections.push(section);
this._logger.debug(`Registered section: ${section.id} in ${categoryId}`);
}
}
public registerSetting(categoryId: string, sectionId: string, setting: SettingDescriptor): void {
let category = this.categories.get(categoryId);
/**
* @zh 注册单个设置项
* @en Register single setting
*/
registerSetting(categoryId: string, sectionId: string, setting: SettingDescriptor): void {
const category = this._ensureCategory(categoryId);
const section = this._ensureSection(category, sectionId);
if (!category) {
category = {
id: categoryId,
title: categoryId,
sections: []
};
this.categories.set(categoryId, category);
}
let section = category.sections.find((s) => s.id === sectionId);
if (!section) {
section = {
id: sectionId,
title: sectionId,
settings: []
};
category.sections.push(section);
}
const existingIndex = section.settings.findIndex((s) => s.key === setting.key);
const existingIndex = section.settings.findIndex(s => s.key === setting.key);
if (existingIndex >= 0) {
section.settings[existingIndex] = setting;
console.warn(`[SettingsRegistry] Setting ${setting.key} in section ${sectionId} already exists, overwriting`);
this._logger.warn(`Overwriting setting: ${setting.key} in ${sectionId}`);
} else {
section.settings.push(setting);
this._logger.debug(`Registered setting: ${setting.key} in ${sectionId}`);
}
}
public unregisterCategory(categoryId: string): void {
this.categories.delete(categoryId);
/**
* @zh 确保分类存在
* @en Ensure category exists
*/
private _ensureCategory(categoryId: string): SettingCategory {
let category = this._categories.get(categoryId);
if (!category) {
category = { id: categoryId, title: categoryId, sections: [] };
this._categories.set(categoryId, category);
}
return category;
}
public unregisterSection(categoryId: string, sectionId: string): void {
const category = this.categories.get(categoryId);
if (category) {
category.sections = category.sections.filter((s) => s.id !== sectionId);
if (category.sections.length === 0) {
this.categories.delete(categoryId);
}
/**
* @zh 确保分区存在
* @en Ensure section exists
*/
private _ensureSection(category: SettingCategory, sectionId: string): SettingSection {
let section = category.sections.find(s => s.id === sectionId);
if (!section) {
section = { id: sectionId, title: sectionId, settings: [] };
category.sections.push(section);
}
return section;
}
/** @zh 注销分类 @en Unregister category */
unregisterCategory(categoryId: string): void {
this._categories.delete(categoryId);
}
/** @zh 注销分区 @en Unregister section */
unregisterSection(categoryId: string, sectionId: string): void {
const category = this._categories.get(categoryId);
if (!category) return;
category.sections = category.sections.filter(s => s.id !== sectionId);
if (category.sections.length === 0) {
this._categories.delete(categoryId);
}
}
public getCategory(categoryId: string): SettingCategory | undefined {
return this.categories.get(categoryId);
/** @zh 获取分类 @en Get category */
getCategory(categoryId: string): SettingCategory | undefined {
return this._categories.get(categoryId);
}
public getAllCategories(): SettingCategory[] {
return Array.from(this.categories.values());
/** @zh 获取所有分类 @en Get all categories */
getAllCategories(): SettingCategory[] {
return Array.from(this._categories.values());
}
public getSetting(categoryId: string, sectionId: string, key: string): SettingDescriptor | undefined {
const category = this.categories.get(categoryId);
if (!category) return undefined;
const section = category.sections.find((s) => s.id === sectionId);
if (!section) return undefined;
return section.settings.find((s) => s.key === key);
/** @zh 获取设置项 @en Get setting */
getSetting(categoryId: string, sectionId: string, key: string): SettingDescriptor | undefined {
const section = this._categories.get(categoryId)?.sections.find(s => s.id === sectionId);
return section?.settings.find(s => s.key === key);
}
public getAllSettings(): Map<string, SettingDescriptor> {
/** @zh 获取所有设置项 @en Get all settings */
getAllSettings(): Map<string, SettingDescriptor> {
const allSettings = new Map<string, SettingDescriptor>();
for (const category of this.categories.values()) {
for (const category of this._categories.values()) {
for (const section of category.sections) {
for (const setting of section.settings) {
allSettings.set(setting.key, setting);
}
}
}
return allSettings;
}
public validateSetting(setting: SettingDescriptor, value: any): boolean {
/**
* @zh 验证设置值
* @en Validate setting value
*/
validateSetting(setting: SettingDescriptor, value: unknown): boolean {
if (setting.validator) {
return setting.validator.validate(value);
}
switch (setting.type) {
case 'number':
case 'range':
if (typeof value !== 'number') return false;
if (setting.min !== undefined && value < setting.min) return false;
if (setting.max !== undefined && value > setting.max) return false;
@@ -215,14 +247,7 @@ export class SettingsRegistry implements IService {
return typeof value === 'string';
case 'select':
if (!setting.options) return false;
return setting.options.some((opt) => opt.value === value);
case 'range':
if (typeof value !== 'number') return false;
if (setting.min !== undefined && value < setting.min) return false;
if (setting.max !== undefined && value > setting.max) return false;
return true;
return setting.options?.some(opt => opt.value === value) ?? false;
case 'color':
return typeof value === 'string' && /^#[0-9A-Fa-f]{6}$/.test(value);
@@ -232,3 +257,6 @@ export class SettingsRegistry implements IService {
}
}
}
/** @zh 设置注册表服务标识符 @en Settings registry service identifier */
export const ISettingsRegistry = createRegistryToken<SettingsRegistry>('SettingsRegistry');

View File

@@ -1,36 +1,52 @@
import type { IService } from '@esengine/ecs-framework';
import { Injectable } from '@esengine/ecs-framework';
import { createLogger } from '@esengine/ecs-framework';
import { Injectable, createLogger } from '@esengine/ecs-framework';
import type { MenuItem, ToolbarItem, PanelDescriptor } from '../Types/UITypes';
import { createRegistryToken } from './BaseRegistry';
const logger = createLogger('UIRegistry');
/**
* UI 注册表
*
* 管理所有编辑器 UI 扩展点的注册和查询。
* @zh UI 注册表 - 管理所有编辑器 UI 扩展点的注册和查询
* @en UI Registry - Manages all editor UI extension point registration and queries
*/
/** @zh 带排序权重的项 @en Item with sort order */
interface IOrdered {
readonly order?: number;
}
@Injectable()
export class UIRegistry implements IService {
private menus: Map<string, MenuItem> = new Map();
private toolbarItems: Map<string, ToolbarItem> = new Map();
private panels: Map<string, PanelDescriptor> = new Map();
private readonly _menus = new Map<string, MenuItem>();
private readonly _toolbarItems = new Map<string, ToolbarItem>();
private readonly _panels = new Map<string, PanelDescriptor>();
// ========== 辅助方法 | Helper Methods ==========
/** @zh 按 order 排序 @en Sort by order */
private _sortByOrder<T extends IOrdered>(items: T[]): T[] {
return items.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
}
// ========== 菜单管理 | Menu Management ==========
/**
* 注册菜单项
* @zh 注册菜单项
* @en Register menu item
*/
public registerMenu(item: MenuItem): void {
if (this.menus.has(item.id)) {
if (this._menus.has(item.id)) {
logger.warn(`Menu item ${item.id} is already registered`);
return;
}
this.menus.set(item.id, item);
this._menus.set(item.id, item);
logger.debug(`Registered menu item: ${item.id}`);
}
/**
* 批量注册菜单项
* @zh 批量注册菜单项
* @en Register multiple menu items
*/
public registerMenus(items: MenuItem[]): void {
for (const item of items) {
@@ -39,56 +55,59 @@ export class UIRegistry implements IService {
}
/**
* 注销菜单项
* @zh 注销菜单项
* @en Unregister menu item
*/
public unregisterMenu(id: string): boolean {
const result = this.menus.delete(id);
const result = this._menus.delete(id);
if (result) {
logger.debug(`Unregistered menu item: ${id}`);
}
return result;
}
/**
* 获取菜单项
*/
/** @zh 获取菜单项 @en Get menu item */
public getMenu(id: string): MenuItem | undefined {
return this.menus.get(id);
return this._menus.get(id);
}
/**
* 获取所有菜单项
* @zh 获取所有菜单项
* @en Get all menu items
*/
public getAllMenus(): MenuItem[] {
return Array.from(this.menus.values()).sort((a, b) => {
return (a.order ?? 0) - (b.order ?? 0);
});
return this._sortByOrder(Array.from(this._menus.values()));
}
/**
* 获取指定父菜单的子菜单
* @zh 获取指定父菜单的子菜单
* @en Get child menus of specified parent
*/
public getChildMenus(parentId: string): MenuItem[] {
return this.getAllMenus()
.filter((item) => item.parentId === parentId)
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
return this._sortByOrder(
Array.from(this._menus.values()).filter((item) => item.parentId === parentId)
);
}
// ========== 工具栏管理 | Toolbar Management ==========
/**
* 注册工具栏项
* @zh 注册工具栏项
* @en Register toolbar item
*/
public registerToolbarItem(item: ToolbarItem): void {
if (this.toolbarItems.has(item.id)) {
if (this._toolbarItems.has(item.id)) {
logger.warn(`Toolbar item ${item.id} is already registered`);
return;
}
this.toolbarItems.set(item.id, item);
this._toolbarItems.set(item.id, item);
logger.debug(`Registered toolbar item: ${item.id}`);
}
/**
* 批量注册工具栏项
* @zh 批量注册工具栏项
* @en Register multiple toolbar items
*/
public registerToolbarItems(items: ToolbarItem[]): void {
for (const item of items) {
@@ -97,56 +116,59 @@ export class UIRegistry implements IService {
}
/**
* 注销工具栏项
* @zh 注销工具栏项
* @en Unregister toolbar item
*/
public unregisterToolbarItem(id: string): boolean {
const result = this.toolbarItems.delete(id);
const result = this._toolbarItems.delete(id);
if (result) {
logger.debug(`Unregistered toolbar item: ${id}`);
}
return result;
}
/**
* 获取工具栏项
*/
/** @zh 获取工具栏项 @en Get toolbar item */
public getToolbarItem(id: string): ToolbarItem | undefined {
return this.toolbarItems.get(id);
return this._toolbarItems.get(id);
}
/**
* 获取所有工具栏项
* @zh 获取所有工具栏项
* @en Get all toolbar items
*/
public getAllToolbarItems(): ToolbarItem[] {
return Array.from(this.toolbarItems.values()).sort((a, b) => {
return (a.order ?? 0) - (b.order ?? 0);
});
return this._sortByOrder(Array.from(this._toolbarItems.values()));
}
/**
* 获取指定组的工具栏项
* @zh 获取指定组的工具栏项
* @en Get toolbar items by group
*/
public getToolbarItemsByGroup(groupId: string): ToolbarItem[] {
return this.getAllToolbarItems()
.filter((item) => item.groupId === groupId)
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
return this._sortByOrder(
Array.from(this._toolbarItems.values()).filter((item) => item.groupId === groupId)
);
}
// ========== 面板管理 | Panel Management ==========
/**
* 注册面板
* @zh 注册面板
* @en Register panel
*/
public registerPanel(panel: PanelDescriptor): void {
if (this.panels.has(panel.id)) {
if (this._panels.has(panel.id)) {
logger.warn(`Panel ${panel.id} is already registered`);
return;
}
this.panels.set(panel.id, panel);
this._panels.set(panel.id, panel);
logger.debug(`Registered panel: ${panel.id}`);
}
/**
* 批量注册面板
* @zh 批量注册面板
* @en Register multiple panels
*/
public registerPanels(panels: PanelDescriptor[]): void {
for (const panel of panels) {
@@ -155,48 +177,50 @@ export class UIRegistry implements IService {
}
/**
* 注销面板
* @zh 注销面板
* @en Unregister panel
*/
public unregisterPanel(id: string): boolean {
const result = this.panels.delete(id);
const result = this._panels.delete(id);
if (result) {
logger.debug(`Unregistered panel: ${id}`);
}
return result;
}
/**
* 获取面板
*/
/** @zh 获取面板 @en Get panel */
public getPanel(id: string): PanelDescriptor | undefined {
return this.panels.get(id);
return this._panels.get(id);
}
/**
* 获取所有面板
* @zh 获取所有面板
* @en Get all panels
*/
public getAllPanels(): PanelDescriptor[] {
return Array.from(this.panels.values()).sort((a, b) => {
return (a.order ?? 0) - (b.order ?? 0);
});
return this._sortByOrder(Array.from(this._panels.values()));
}
/**
* 获取指定位置的面板
* @zh 获取指定位置的面板
* @en Get panels by position
*/
public getPanelsByPosition(position: string): PanelDescriptor[] {
return this.getAllPanels()
.filter((panel) => panel.position === position)
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
return this._sortByOrder(
Array.from(this._panels.values()).filter((panel) => panel.position === position)
);
}
/**
* 释放资源
*/
// ========== 生命周期 | Lifecycle ==========
/** @zh 释放资源 @en Dispose resources */
public dispose(): void {
this.menus.clear();
this.toolbarItems.clear();
this.panels.clear();
logger.info('UIRegistry disposed');
this._menus.clear();
this._toolbarItems.clear();
this._panels.clear();
logger.debug('Disposed');
}
}
/** @zh UI 注册表服务标识符 @en UI registry service identifier */
export const IUIRegistry = createRegistryToken<UIRegistry>('UIRegistry');

View File

@@ -411,6 +411,27 @@ export interface IUserCodeService {
* 打开新项目时调用以重置就绪 Promise。
*/
resetReady(): void;
/**
* Get the user code realm.
* 获取用户代码隔离域。
*
* The realm provides isolated registration for user components, systems, and services.
* 隔离域提供用户组件、系统和服务的隔离注册。
*
* @returns User code realm instance | 用户代码隔离域实例
*/
getUserCodeRealm(): import('@esengine/runtime-core').UserCodeRealm;
/**
* Reset the user code realm for project switching.
* 重置用户代码隔离域(用于项目切换)。
*
* This clears all user-registered components, systems, and services,
* preparing for a new project's user code.
* 这会清除所有用户注册的组件、系统和服务,为新项目的用户代码做准备。
*/
resetRealm(): void;
}
import { EditorConfig } from '../../Config';

View File

@@ -15,6 +15,7 @@ import {
COMPONENT_TYPE_NAME,
SYSTEM_TYPE_NAME
} from '@esengine/ecs-framework';
import { UserCodeRealm, type UserCodeRealmConfig } from '@esengine/runtime-core';
import type {
IUserCodeService,
UserScriptInfo,
@@ -89,10 +90,17 @@ export class UserCodeService implements IService, IUserCodeService {
private _readyPromise: Promise<void>;
private _readyResolve: (() => void) | undefined;
constructor(fileSystem: IFileSystem) {
/**
* 用户代码隔离域
* User code realm for isolation
*/
private _userCodeRealm: UserCodeRealm;
constructor(fileSystem: IFileSystem, realmConfig?: UserCodeRealmConfig) {
this._fileSystem = fileSystem;
this._hotReloadCoordinator = new HotReloadCoordinator();
this._readyPromise = this._createReadyPromise();
this._userCodeRealm = new UserCodeRealm(realmConfig);
}
/**
@@ -388,6 +396,15 @@ export class UserCodeService implements IService, IUserCodeService {
if (this._isComponentClass(exported)) {
logger.debug(`Found component: ${name} | 发现组件: ${name}`);
// Register to UserCodeRealm for isolation
// 注册到 UserCodeRealm 实现隔离
try {
this._userCodeRealm.registerComponent(exported);
logger.info(`Component ${name} registered to user code realm`);
} catch (err) {
logger.warn(`Failed to register component ${name} to realm:`, err);
}
// Register with Core ComponentRegistry for serialization/deserialization
// 注册到核心 ComponentRegistry 用于序列化/反序列化
try {
@@ -459,7 +476,7 @@ export class UserCodeService implements IService, IUserCodeService {
// Access scene through Core.scene
// 通过 Core.scene 访问场景
const sdkGlobal = (window as any)[EditorConfig.globals.sdk];
const sdkGlobal = window.__ESENGINE_SDK__;
const Core = sdkGlobal?.Core;
const scene = Core?.scene;
if (!scene || !scene.entities) {
@@ -782,7 +799,7 @@ export class UserCodeService implements IService, IUserCodeService {
// Initialize hot reload coordinator with Core reference
// 使用 Core 引用初始化热更新协调器
const sdkGlobal = (window as any)[EditorConfig.globals.sdk];
const sdkGlobal = window.__ESENGINE_SDK__;
const Core = sdkGlobal?.Core;
if (Core) {
this._hotReloadCoordinator.initialize(Core);
@@ -982,6 +999,32 @@ export class UserCodeService implements IService, IUserCodeService {
this.stopWatch();
this._runtimeModule = undefined;
this._editorModule = undefined;
this._userCodeRealm.dispose();
}
/**
* Get the user code realm.
* 获取用户代码隔离域。
*
* The realm provides isolated registration for user components, systems, and services.
* 隔离域提供用户组件、系统和服务的隔离注册。
*
* @returns User code realm instance | 用户代码隔离域实例
*/
getUserCodeRealm(): UserCodeRealm {
return this._userCodeRealm;
}
/**
* Reset the user code realm for project switching.
* 重置用户代码隔离域(用于项目切换)。
*
* This clears all user-registered components, systems, and services,
* preparing for a new project's user code.
* 这会清除所有用户注册的组件、系统和服务,为新项目的用户代码做准备。
*/
resetRealm(): void {
this._userCodeRealm.reset();
}
// ==================== Private Methods | 私有方法 ====================

View File

@@ -12,7 +12,9 @@
* 使用事件驱动架构实现高效的变化通知。
*/
import type { Component, ComponentType, Entity } from '@esengine/ecs-framework';
import { createLogger, type Component, type ComponentType, type Entity } from '@esengine/ecs-framework';
const logger = createLogger('VirtualNodeRegistry');
/**
* Virtual node data
@@ -154,7 +156,7 @@ export class VirtualNodeRegistry {
try {
return provider(component, entity);
} catch (e) {
console.warn(`[VirtualNodeRegistry] Error in provider for ${componentType.name}:`, e);
logger.warn(`Error in provider for ${componentType.name}:`, e);
return [];
}
}
@@ -249,7 +251,7 @@ export class VirtualNodeRegistry {
try {
listener(event);
} catch (e) {
console.warn('[VirtualNodeRegistry] Error in change listener:', e);
logger.warn('Error in change listener:', e);
}
}
}

View File

@@ -1,177 +1,162 @@
import { IService } from '@esengine/ecs-framework';
import { ComponentType } from 'react';
import { createLogger, type ILogger, type IService } from '@esengine/ecs-framework';
import type { ComponentType } from 'react';
import { createRegistryToken } from './BaseRegistry';
/**
* 窗口描述符
* @zh 窗口描述符
* @en Window descriptor
*/
export interface WindowDescriptor {
/**
* 窗口唯一标识
*/
/** @zh 窗口唯一标识 @en Unique window ID */
id: string;
/**
* 窗口组件
*/
component: ComponentType<any>;
/**
* 窗口标题
*/
/** @zh 窗口组件 @en Window component */
component: ComponentType<unknown>;
/** @zh 窗口标题 @en Window title */
title?: string;
/**
* 默认宽度
*/
/** @zh 默认宽度 @en Default width */
defaultWidth?: number;
/**
* 默认高度
*/
/** @zh 默认高度 @en Default height */
defaultHeight?: number;
}
/**
* 窗口实例
* @zh 窗口实例
* @en Window instance
*/
export interface WindowInstance {
/**
* 窗口描述符
*/
/** @zh 窗口描述符 @en Window descriptor */
descriptor: WindowDescriptor;
/**
* 是否打开
*/
/** @zh 是否打开 @en Whether the window is open */
isOpen: boolean;
/**
* 窗口参数
*/
params?: Record<string, any>;
/** @zh 窗口参数 @en Window parameters */
params?: Record<string, unknown>;
}
/**
* 窗口注册表服务
*
* 管理插件注册的窗口组件
* @zh 窗口注册表服务 - 管理插件注册的窗口组件
* @en Window Registry Service - Manages plugin-registered window components
*/
export class WindowRegistry implements IService {
private windows: Map<string, WindowDescriptor> = new Map();
private openWindows: Map<string, WindowInstance> = new Map();
private listeners: Set<() => void> = new Set();
private readonly _windows = new Map<string, WindowDescriptor>();
private readonly _openWindows = new Map<string, WindowInstance>();
private readonly _listeners = new Set<() => void>();
private readonly _logger: ILogger;
constructor() {
this._logger = createLogger('WindowRegistry');
}
/**
* 注册窗口
* @zh 注册窗口
* @en Register a window
*/
registerWindow(descriptor: WindowDescriptor): void {
if (this.windows.has(descriptor.id)) {
console.warn(`Window ${descriptor.id} is already registered`);
if (this._windows.has(descriptor.id)) {
this._logger.warn(`Window already registered: ${descriptor.id}`);
return;
}
this.windows.set(descriptor.id, descriptor);
this._windows.set(descriptor.id, descriptor);
this._logger.debug(`Registered window: ${descriptor.id}`);
}
/**
* 取消注册窗口
* @zh 取消注册窗口
* @en Unregister a window
*/
unregisterWindow(windowId: string): void {
this.windows.delete(windowId);
this.openWindows.delete(windowId);
this.notifyListeners();
this._windows.delete(windowId);
this._openWindows.delete(windowId);
this._notifyListeners();
this._logger.debug(`Unregistered window: ${windowId}`);
}
/**
* 获取窗口描述符
*/
/** @zh 获取窗口描述符 @en Get window descriptor */
getWindow(windowId: string): WindowDescriptor | undefined {
return this.windows.get(windowId);
return this._windows.get(windowId);
}
/**
* 获取所有窗口描述符
*/
/** @zh 获取所有窗口描述符 @en Get all window descriptors */
getAllWindows(): WindowDescriptor[] {
return Array.from(this.windows.values());
return Array.from(this._windows.values());
}
/**
* 打开窗口
* @zh 打开窗口
* @en Open a window
*/
openWindow(windowId: string, params?: Record<string, any>): void {
const descriptor = this.windows.get(windowId);
openWindow(windowId: string, params?: Record<string, unknown>): void {
const descriptor = this._windows.get(windowId);
if (!descriptor) {
console.warn(`Window ${windowId} is not registered`);
this._logger.warn(`Window not registered: ${windowId}`);
return;
}
this.openWindows.set(windowId, {
this._openWindows.set(windowId, {
descriptor,
isOpen: true,
params
});
this.notifyListeners();
this._notifyListeners();
this._logger.debug(`Opened window: ${windowId}`);
}
/**
* 关闭窗口
* @zh 关闭窗口
* @en Close a window
*/
closeWindow(windowId: string): void {
this.openWindows.delete(windowId);
this.notifyListeners();
this._openWindows.delete(windowId);
this._notifyListeners();
this._logger.debug(`Closed window: ${windowId}`);
}
/**
* 获取打开的窗口实例
*/
/** @zh 获取打开的窗口实例 @en Get open window instance */
getOpenWindow(windowId: string): WindowInstance | undefined {
return this.openWindows.get(windowId);
return this._openWindows.get(windowId);
}
/**
* 获取所有打开的窗口
*/
/** @zh 获取所有打开的窗口 @en Get all open windows */
getAllOpenWindows(): WindowInstance[] {
return Array.from(this.openWindows.values());
return Array.from(this._openWindows.values());
}
/**
* 检查窗口是否打开
*/
/** @zh 检查窗口是否打开 @en Check if window is open */
isWindowOpen(windowId: string): boolean {
return this.openWindows.has(windowId);
return this._openWindows.has(windowId);
}
/**
* 添加变化监听器
* @zh 添加变化监听器
* @en Add change listener
* @returns @zh 取消订阅函数 @en Unsubscribe function
*/
addListener(listener: () => void): () => void {
this.listeners.add(listener);
this._listeners.add(listener);
return () => {
this.listeners.delete(listener);
this._listeners.delete(listener);
};
}
/**
* 通知所有监听器
*/
private notifyListeners(): void {
this.listeners.forEach((listener) => listener());
/** @zh 通知所有监听器 @en Notify all listeners */
private _notifyListeners(): void {
for (const listener of this._listeners) {
listener();
}
}
/**
* 清空所有窗口
*/
/** @zh 清空所有窗口 @en Clear all windows */
clear(): void {
this.windows.clear();
this.openWindows.clear();
this.listeners.clear();
this._windows.clear();
this._openWindows.clear();
this._listeners.clear();
this._logger.debug('Cleared');
}
/**
* 释放资源
*/
/** @zh 释放资源 @en Dispose resources */
dispose(): void {
this.clear();
}
}
/** @zh 窗口注册表服务标识符 @en Window registry service identifier */
export const IWindowRegistry = createRegistryToken<WindowRegistry>('WindowRegistry');

49
packages/editor-core/src/global.d.ts vendored Normal file
View File

@@ -0,0 +1,49 @@
/**
* @zh 全局类型声明
* @en Global type declarations
*
* @zh 扩展 Window 接口以支持编辑器运行时全局变量
* @en Extend Window interface to support editor runtime global variables
*/
/**
* @zh SDK 全局对象结构
* @en SDK global object structure
*/
interface ESEngineSDK {
Core: typeof import('@esengine/ecs-framework').Core;
Scene: typeof import('@esengine/ecs-framework').Scene;
Entity: typeof import('@esengine/ecs-framework').Entity;
Component: typeof import('@esengine/ecs-framework').Component;
System: typeof import('@esengine/ecs-framework').System;
[key: string]: unknown;
}
/**
* @zh 插件容器结构
* @en Plugin container structure
*/
interface PluginContainer {
[pluginName: string]: unknown;
}
/**
* @zh 用户代码导出结构
* @en User code exports structure
*/
interface UserExports {
[name: string]: unknown;
}
declare global {
interface Window {
// ESEngine 全局变量(与 EditorConfig.globals 对应)
// ESEngine globals (matching EditorConfig.globals)
__ESENGINE_SDK__: ESEngineSDK | undefined;
__ESENGINE_PLUGINS__: PluginContainer | undefined;
__USER_RUNTIME_EXPORTS__: UserExports | undefined;
__USER_EDITOR_EXPORTS__: UserExports | undefined;
}
}
export {};

View File

@@ -1,75 +1,110 @@
/**
* ECS Framework Editor Core
* @esengine/editor-core
*
* Plugin-based editor framework for ECS Framework
* 基于插件的 ECS 编辑器框架
*/
// Service Tokens | 服务令牌
// ============================================================================
// Service Tokens | 服务令牌 (推荐导入)
// ============================================================================
export * from './tokens';
// 配置 | Configuration
// ============================================================================
// Plugin System | 插件系统
// ============================================================================
export * from './Config';
// 新插件系统 | New plugin system
export * from './Plugin';
export * from './Services/UIRegistry';
// ============================================================================
// Registry Base | 注册表基类
// ============================================================================
export * from './Services/BaseRegistry';
// ============================================================================
// Core Services | 核心服务
// ============================================================================
export * from './Services/MessageHub';
export * from './Services/SerializerRegistry';
export * from './Services/LocaleService';
export * from './Services/LogService';
export * from './Services/CommandManager';
export * from './Services/SettingsRegistry';
// ============================================================================
// Entity & Component Services | 实体与组件服务
// ============================================================================
export * from './Services/EntityStoreService';
export * from './Services/ComponentRegistry';
export * from './Services/LocaleService';
export * from './Services/PropertyMetadata';
export * from './Services/ProjectService';
export * from './Services/ComponentDiscoveryService';
export * from './Services/LogService';
export * from './Services/SettingsRegistry';
export * from './Services/SerializerRegistry';
export * from './Services/PropertyMetadata';
// ============================================================================
// Scene & Project Services | 场景与项目服务
// ============================================================================
export * from './Services/ProjectService';
export * from './Services/SceneManagerService';
export * from './Services/SceneTemplateRegistry';
export * from './Services/FileActionRegistry';
export * from './Services/EntityCreationRegistry';
export * from './Services/CompilerRegistry';
export * from './Services/ICompiler';
export * from './Services/ICommand';
export * from './Services/BaseCommand';
export * from './Services/CommandManager';
export * from './Services/IEditorDataStore';
export * from './Services/IFileSystem';
export * from './Services/IDialog';
export * from './Services/INotification';
export * from './Services/IInspectorProvider';
export * from './Services/PrefabService';
// ============================================================================
// UI & Inspector Services | UI 与检视器服务
// ============================================================================
export * from './Services/UIRegistry';
export * from './Services/InspectorRegistry';
export * from './Services/IPropertyRenderer';
export * from './Services/PropertyRendererRegistry';
export * from './Services/IFieldEditor';
export * from './Services/FieldEditorRegistry';
export * from './Services/ComponentInspectorRegistry';
export * from './Services/ComponentActionRegistry';
export * from './Services/WindowRegistry';
// ============================================================================
// Asset & File Services | 资产与文件服务
// ============================================================================
export * from './Services/AssetRegistryService';
export * from './Services/FileActionRegistry';
export * from './Services/VirtualNodeRegistry';
// ============================================================================
// Viewport & Gizmo Services | 视口与 Gizmo 服务
// ============================================================================
export * from './Services/IViewportService';
export * from './Services/PreviewSceneService';
export * from './Services/EditorViewportService';
export * from './Services/PrefabService';
export * from './Services/VirtualNodeRegistry';
export * from './Services/GizmoInteractionService';
// Build System | 构建系统
export * from './Services/Build';
// User Code System | 用户代码系统
export * from './Services/UserCode';
// Module System | 模块系统
export * from './Services/Module';
export * from './Gizmos';
export * from './Rendering';
// ============================================================================
// Build & Compile System | 构建与编译系统
// ============================================================================
export * from './Services/Build';
export * from './Services/UserCode';
export * from './Services/CompilerRegistry';
// ============================================================================
// Module System | 模块系统
// ============================================================================
export * from './Services/Module';
export * from './Module/IEventBus';
export * from './Module/ICommandRegistry';
export * from './Module/IPanelRegistry';
export * from './Module/IModuleContext';
export * from './Module/IEditorModule';
// ============================================================================
// Interfaces | 接口定义
// ============================================================================
export * from './Services/ICompiler';
export * from './Services/ICommand';
export * from './Services/BaseCommand';
export * from './Services/IEditorDataStore';
export * from './Services/IFileSystem';
export * from './Services/IDialog';
export * from './Services/INotification';
export * from './Services/IInspectorProvider';
export * from './Services/IPropertyRenderer';
export * from './Services/IFieldEditor';
export * from './Services/ComponentActionRegistry';
export * from './Services/EntityCreationRegistry';
export * from './Types/IFileAPI';
export * from './Types/UITypes';

View File

@@ -153,18 +153,26 @@ export type { MessageHandler, RequestHandler } from './Services/MessageHub';
export type { EntityTreeNode } from './Services/EntityStoreService';
// ============================================================================
// PrefabService Token
// 预制体服务令牌
// EditorPrefabService Token
// 编辑器预制体服务令牌
// ============================================================================
/**
* PrefabService 接口
* PrefabService interface
* EditorPrefabService 接口
* EditorPrefabService interface
*
* 提供类型安全的预制体服务访问接口
* Provides type-safe prefab service access interface.
* 编辑器侧的预制体实例管理服务
* Editor-side prefab instance management service.
*
* 注意:这与 asset-system 的 IPrefabService 不同!
* - asset-system.IPrefabService: 运行时预制体资源加载/实例化
* - editor-core.IEditorPrefabService: 编辑器预制体实例状态管理
*
* Note: This is different from asset-system's IPrefabService!
* - asset-system.IPrefabService: Runtime prefab asset loading/instantiation
* - editor-core.IEditorPrefabService: Editor prefab instance state management
*/
export interface IPrefabService {
export interface IEditorPrefabService {
/** 设置文件 API | Set file API */
setFileAPI(fileAPI: IPrefabFileAPI): void;
/** 检查是否为预制体实例 | Check if prefab instance */
@@ -192,13 +200,13 @@ export interface IPrefabService {
}
/**
* 预制体服务令牌
* Prefab service token
* 编辑器预制体服务令牌
* Editor prefab service token
*
* 用于注册和获取预制体服务。
* For registering and getting prefab service.
* 用于注册和获取编辑器预制体服务。
* For registering and getting editor prefab service.
*/
export const PrefabServiceToken = createServiceToken<IPrefabService>('prefabService');
export const EditorPrefabServiceToken = createServiceToken<IEditorPrefabService>('editorPrefabService');
// Re-export types for convenience
// 重新导出类型方便使用