Feature/tilemap editor (#237)

* feat: 添加 Tilemap 编辑器插件和组件生命周期支持

* feat(editor-core): 添加声明式插件注册 API

* feat(editor-core): 改进tiledmap结构合并tileset进tiledmapeditor

* feat: 添加 editor-runtime SDK 和插件系统改进

* fix(ci): 修复SceneResourceManager里变量未使用问题
This commit is contained in:
YHH
2025-11-25 22:23:19 +08:00
committed by GitHub
parent 551ca7805d
commit 3fb6f919f8
166 changed files with 54691 additions and 8674 deletions

View File

@@ -64,6 +64,7 @@
"typescript": "^5.8.3"
},
"peerDependencies": {
"@esengine/asset-system": "*",
"@esengine/ecs-framework": "^2.2.8",
"react": "^18.2.0",
"rxjs": "^7.8.0",

5044
packages/editor-core/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,201 @@
/**
* Gizmo Registry
* Gizmo 注册表
*
* Manages gizmo providers for different component types.
* Uses registry pattern instead of prototype modification for cleaner architecture.
* 管理不同组件类型的 gizmo 提供者。
* 使用注册表模式替代原型修改,实现更清晰的架构。
*/
import type { Component, ComponentType, Entity } from '@esengine/ecs-framework';
import type { IGizmoProvider, IGizmoRenderData } from './IGizmoProvider';
/**
* Gizmo provider function type
* Gizmo 提供者函数类型
*
* A function that generates gizmo data for a specific component instance.
* 为特定组件实例生成 gizmo 数据的函数。
*/
export type GizmoProviderFn<T extends Component = Component> = (
component: T,
entity: Entity,
isSelected: boolean
) => IGizmoRenderData[];
/**
* Gizmo Registry Service
* Gizmo 注册表服务
*
* Centralized registry for component gizmo providers.
* Allows plugins to register gizmo rendering for any component type
* without modifying the component class itself.
*
* 组件 gizmo 提供者的中心化注册表。
* 允许插件为任何组件类型注册 gizmo 渲染,
* 而无需修改组件类本身。
*
* @example
* ```typescript
* // Register a gizmo provider for SpriteComponent
* GizmoRegistry.register(SpriteComponent, (sprite, entity, isSelected) => {
* const transform = entity.getComponent(TransformComponent);
* return [{
* type: 'rect',
* x: transform.position.x,
* y: transform.position.y,
* width: sprite.width,
* height: sprite.height,
* // ...
* }];
* });
*
* // Get gizmo data for a component
* const gizmos = GizmoRegistry.getGizmoData(spriteComponent, entity, true);
* ```
*/
export class GizmoRegistry {
private static providers = new Map<ComponentType, GizmoProviderFn>();
/**
* Register a gizmo provider for a component type.
* 为组件类型注册 gizmo 提供者。
*
* @param componentType - The component class to register for
* @param provider - Function that generates gizmo data
*/
static register<T extends Component>(
componentType: ComponentType<T>,
provider: GizmoProviderFn<T>
): void {
this.providers.set(componentType, provider as GizmoProviderFn);
}
/**
* Unregister a gizmo provider for a component type.
* 取消注册组件类型的 gizmo 提供者。
*
* @param componentType - The component class to unregister
*/
static unregister(componentType: ComponentType): void {
this.providers.delete(componentType);
}
/**
* Check if a component type has a registered gizmo provider.
* 检查组件类型是否有注册的 gizmo 提供者。
*
* @param componentType - The component class to check
*/
static hasProvider(componentType: ComponentType): boolean {
return this.providers.has(componentType);
}
/**
* Get the gizmo provider for a component type.
* 获取组件类型的 gizmo 提供者。
*
* @param componentType - The component class
* @returns The provider function or undefined
*/
static getProvider(componentType: ComponentType): GizmoProviderFn | undefined {
return this.providers.get(componentType);
}
/**
* Get gizmo data for a component instance.
* 获取组件实例的 gizmo 数据。
*
* @param component - The component instance
* @param entity - The entity owning the component
* @param isSelected - Whether the entity is selected
* @returns Array of gizmo render data, or empty array if no provider
*/
static getGizmoData(
component: Component,
entity: Entity,
isSelected: boolean
): IGizmoRenderData[] {
const componentType = component.constructor as ComponentType;
const provider = this.providers.get(componentType);
if (provider) {
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);
return [];
}
}
return [];
}
/**
* Get all gizmo data for an entity (from all components with providers).
* 获取实体的所有 gizmo 数据(来自所有有提供者的组件)。
*
* @param entity - The entity to get gizmos for
* @param isSelected - Whether the entity is selected
* @returns Array of all gizmo render data
*/
static getAllGizmoDataForEntity(entity: Entity, isSelected: boolean): IGizmoRenderData[] {
const allGizmos: IGizmoRenderData[] = [];
for (const component of entity.components) {
const gizmos = this.getGizmoData(component, entity, isSelected);
allGizmos.push(...gizmos);
}
return allGizmos;
}
/**
* Check if an entity has any components with gizmo providers.
* 检查实体是否有任何带有 gizmo 提供者的组件。
*
* @param entity - The entity to check
*/
static hasAnyGizmoProvider(entity: Entity): boolean {
for (const component of entity.components) {
const componentType = component.constructor as ComponentType;
if (this.providers.has(componentType)) {
return true;
}
}
return false;
}
/**
* Get all registered component types.
* 获取所有已注册的组件类型。
*/
static getRegisteredTypes(): ComponentType[] {
return Array.from(this.providers.keys());
}
/**
* Clear all registered providers.
* 清除所有已注册的提供者。
*/
static clear(): void {
this.providers.clear();
}
}
/**
* Adapter to make GizmoRegistry work with the IGizmoProvider interface.
* 使 GizmoRegistry 与 IGizmoProvider 接口兼容的适配器。
*
* This allows components to optionally implement IGizmoProvider directly,
* while also supporting the registry pattern.
* 这允许组件可选地直接实现 IGizmoProvider
* 同时也支持注册表模式。
*/
export function isGizmoProviderRegistered(component: Component): boolean {
const componentType = component.constructor as ComponentType;
return GizmoRegistry.hasProvider(componentType);
}

View File

@@ -0,0 +1,182 @@
/**
* Gizmo Provider Interface
* Gizmo 提供者接口
*
* Allows components to define custom gizmo rendering in the editor.
* Uses the Rust WebGL renderer for high-performance gizmo display.
* 允许组件定义编辑器中的自定义 gizmo 渲染。
* 使用 Rust WebGL 渲染器实现高性能 gizmo 显示。
*/
import type { Entity } from '@esengine/ecs-framework';
/**
* Gizmo type enumeration
* Gizmo 类型枚举
*/
export type GizmoType = 'rect' | 'circle' | 'line' | 'grid';
/**
* Color in RGBA format (0-1 range)
* RGBA 格式颜色0-1 范围)
*/
export interface GizmoColor {
r: number;
g: number;
b: number;
a: number;
}
/**
* Rectangle gizmo data (rendered via Rust WebGL)
* 矩形 gizmo 数据(通过 Rust WebGL 渲染)
*/
export interface IRectGizmoData {
type: 'rect';
/** Center X position in world space | 世界空间中心 X 位置 */
x: number;
/** Center Y position in world space | 世界空间中心 Y 位置 */
y: number;
/** Width in world units | 世界单位宽度 */
width: number;
/** Height in world units | 世界单位高度 */
height: number;
/** Rotation in radians | 旋转角度(弧度) */
rotation: number;
/** Origin X (0-1, default 0.5 for center) | 原点 X0-1默认 0.5 居中) */
originX: number;
/** Origin Y (0-1, default 0.5 for center) | 原点 Y0-1默认 0.5 居中) */
originY: number;
/** Color | 颜色 */
color: GizmoColor;
/** Show transform handles (move/rotate/scale based on mode) | 显示变换手柄 */
showHandles: boolean;
}
/**
* Circle gizmo data
* 圆形 gizmo 数据
*/
export interface ICircleGizmoData {
type: 'circle';
/** Center X position | 中心 X 位置 */
x: number;
/** Center Y position | 中心 Y 位置 */
y: number;
/** Radius | 半径 */
radius: number;
/** Color | 颜色 */
color: GizmoColor;
}
/**
* Line gizmo data
* 线条 gizmo 数据
*/
export interface ILineGizmoData {
type: 'line';
/** Line points | 线段点 */
points: Array<{ x: number; y: number }>;
/** Color | 颜色 */
color: GizmoColor;
/** Whether to close the path | 是否闭合路径 */
closed: boolean;
}
/**
* Grid gizmo data
* 网格 gizmo 数据
*/
export interface IGridGizmoData {
type: 'grid';
/** Top-left X position | 左上角 X 位置 */
x: number;
/** Top-left Y position | 左上角 Y 位置 */
y: number;
/** Total width | 总宽度 */
width: number;
/** Total height | 总高度 */
height: number;
/** Number of columns | 列数 */
cols: number;
/** Number of rows | 行数 */
rows: number;
/** Color | 颜色 */
color: GizmoColor;
}
/**
* Union type for all gizmo data
* 所有 gizmo 数据的联合类型
*/
export type IGizmoRenderData = IRectGizmoData | ICircleGizmoData | ILineGizmoData | IGridGizmoData;
/**
* Gizmo Provider Interface
* Gizmo 提供者接口
*
* Components can implement this interface to provide custom gizmo rendering.
* The returned data will be rendered by the Rust WebGL engine.
* 组件可以实现此接口以提供自定义 gizmo 渲染。
* 返回的数据将由 Rust WebGL 引擎渲染。
*/
export interface IGizmoProvider {
/**
* Get gizmo render data for this component
* 获取此组件的 gizmo 渲染数据
*
* @param entity The entity owning this component | 拥有此组件的实体
* @param isSelected Whether the entity is selected | 实体是否被选中
* @returns Array of gizmo render data | Gizmo 渲染数据数组
*/
getGizmoData(entity: Entity, isSelected: boolean): IGizmoRenderData[];
}
/**
* Check if a component implements IGizmoProvider
* 检查组件是否实现了 IGizmoProvider
*/
export function hasGizmoProvider(component: unknown): component is IGizmoProvider {
return component !== null &&
typeof component === 'object' &&
'getGizmoData' in component &&
typeof (component as Record<string, unknown>).getGizmoData === 'function';
}
/**
* Helper to create a gizmo color from hex string
* 从十六进制字符串创建 gizmo 颜色的辅助函数
*/
export function hexToGizmoColor(hex: string, alpha: number = 1): GizmoColor {
let r = 0, g = 1, b = 0;
if (hex.startsWith('#')) {
const hexValue = hex.slice(1);
if (hexValue.length === 3) {
r = parseInt(hexValue[0] + hexValue[0], 16) / 255;
g = parseInt(hexValue[1] + hexValue[1], 16) / 255;
b = parseInt(hexValue[2] + hexValue[2], 16) / 255;
} else if (hexValue.length === 6) {
r = parseInt(hexValue.slice(0, 2), 16) / 255;
g = parseInt(hexValue.slice(2, 4), 16) / 255;
b = parseInt(hexValue.slice(4, 6), 16) / 255;
}
}
return { r, g, b, a: alpha };
}
/**
* Predefined gizmo colors
* 预定义的 gizmo 颜色
*/
export const GizmoColors = {
/** Green for selected entities | 选中实体的绿色 */
selected: { r: 0, g: 1, b: 0.5, a: 1 } as GizmoColor,
/** Semi-transparent green for unselected | 未选中实体的半透明绿色 */
unselected: { r: 0, g: 1, b: 0.5, a: 0.4 } as GizmoColor,
/** White for camera frustum | 相机视锥体的白色 */
camera: { r: 1, g: 1, b: 1, a: 0.8 } as GizmoColor,
/** Cyan for colliders | 碰撞体的青色 */
collider: { r: 0, g: 1, b: 1, a: 0.6 } as GizmoColor,
/** Yellow for grid | 网格的黄色 */
grid: { r: 1, g: 1, b: 0, a: 0.3 } as GizmoColor,
};

View File

@@ -0,0 +1,12 @@
/**
* Gizmo System
* Gizmo 系统
*
* Provides interfaces for custom gizmo rendering in the editor.
* Gizmos are rendered by the Rust WebGL engine for optimal performance.
* 为编辑器中的自定义 gizmo 渲染提供接口。
* Gizmo 由 Rust WebGL 引擎渲染以获得最佳性能。
*/
export * from './IGizmoProvider';
export * from './GizmoRegistry';

View File

@@ -8,6 +8,10 @@ import { UIRegistry } from '../Services/UIRegistry';
import { MessageHub } from '../Services/MessageHub';
import { SerializerRegistry } from '../Services/SerializerRegistry';
import { FileActionRegistry } from '../Services/FileActionRegistry';
import { EntityCreationRegistry } from '../Services/EntityCreationRegistry';
import { ComponentActionRegistry } from '../Services/ComponentActionRegistry';
import { pluginRegistry } from './PluginRegistry';
import type { EditorPluginDefinition } from './PluginTypes';
const logger = createLogger('EditorPluginManager');
@@ -24,6 +28,8 @@ export class EditorPluginManager extends PluginManager {
private messageHub: MessageHub | null = null;
private serializerRegistry: SerializerRegistry | null = null;
private fileActionRegistry: FileActionRegistry | null = null;
private entityCreationRegistry: EntityCreationRegistry | null = null;
private componentActionRegistry: ComponentActionRegistry | null = null;
/**
* 初始化编辑器插件管理器
@@ -35,6 +41,8 @@ export class EditorPluginManager extends PluginManager {
this.messageHub = services.resolve(MessageHub);
this.serializerRegistry = services.resolve(SerializerRegistry);
this.fileActionRegistry = services.resolve(FileActionRegistry);
this.entityCreationRegistry = services.resolve(EntityCreationRegistry);
this.componentActionRegistry = services.resolve(ComponentActionRegistry);
logger.info('EditorPluginManager initialized');
}
@@ -106,6 +114,18 @@ export class EditorPluginManager extends PluginManager {
logger.debug(`Registered ${templates.length} file creation templates for ${plugin.name}`);
}
if (plugin.registerEntityCreationTemplates && this.entityCreationRegistry) {
const templates = plugin.registerEntityCreationTemplates();
this.entityCreationRegistry.registerMany(templates);
logger.debug(`Registered ${templates.length} entity creation templates for ${plugin.name}`);
}
if (plugin.registerComponentActions && this.componentActionRegistry) {
const actions = plugin.registerComponentActions();
this.componentActionRegistry.registerMany(actions);
logger.debug(`Registered ${actions.length} component actions for ${plugin.name}`);
}
if (plugin.onEditorReady) {
await plugin.onEditorReady();
}
@@ -170,6 +190,20 @@ export class EditorPluginManager extends PluginManager {
}
}
if (plugin.registerEntityCreationTemplates && this.entityCreationRegistry) {
const templates = plugin.registerEntityCreationTemplates();
for (const template of templates) {
this.entityCreationRegistry.unregister(template.id);
}
}
if (plugin.registerComponentActions && this.componentActionRegistry) {
const actions = plugin.registerComponentActions();
for (const action of actions) {
this.componentActionRegistry.unregister(action.componentName, action.id);
}
}
this.serializerRegistry?.unregisterAll(name);
await super.uninstall(name);
@@ -351,6 +385,80 @@ export class EditorPluginManager extends PluginManager {
await this.messageHub?.publish('file:afterSave', { path: filePath });
}
/**
* 使用声明式 API 注册插件
* Register plugin using declarative API
*/
public async registerPlugin(definition: EditorPluginDefinition): Promise<void> {
logger.info(`Registering plugin with declarative API: ${definition.id}`);
try {
// 使用 PluginRegistry 注册
await pluginRegistry.register(definition);
// 同步到旧的元数据系统以保持兼容性
const metadata: IEditorPluginMetadata = {
name: definition.id,
displayName: definition.name,
version: definition.version || '1.0.0',
category: EditorPluginCategory.Tool,
description: definition.description,
enabled: true,
installedAt: Date.now()
};
this.pluginMetadata.set(definition.id, metadata);
// 注册实体创建模板
if (definition.entityTemplates && this.entityCreationRegistry) {
for (const template of definition.entityTemplates) {
this.entityCreationRegistry.register({
id: `${definition.id}:${template.id}`,
label: template.label,
icon: template.icon,
order: template.priority,
create: template.create
});
}
}
// 注册组件操作
if (definition.components && this.componentActionRegistry) {
for (const comp of definition.components) {
if (comp.actions) {
for (const action of comp.actions) {
this.componentActionRegistry.register({
id: action.id,
componentName: comp.type.name,
label: action.label,
icon: action.icon,
execute: action.execute as unknown as (component: any, entity: any) => void | Promise<void>
});
}
}
}
}
await this.messageHub?.publish('plugin:installed', {
name: definition.id,
displayName: definition.name,
category: EditorPluginCategory.Tool
});
logger.info(`Plugin ${definition.id} registered successfully`);
} catch (error) {
logger.error(`Failed to register plugin ${definition.id}:`, error);
throw error;
}
}
/**
* 获取 PluginRegistry 实例
* Get PluginRegistry instance
*/
public getPluginRegistry() {
return pluginRegistry;
}
/**
* 释放资源
*/
@@ -363,6 +471,8 @@ export class EditorPluginManager extends PluginManager {
this.messageHub = null;
this.serializerRegistry = null;
this.fileActionRegistry = null;
this.entityCreationRegistry = null;
this.componentActionRegistry = null;
logger.info('EditorPluginManager disposed');
}

View File

@@ -1,5 +1,6 @@
import type { IPlugin } from '@esengine/ecs-framework';
import type { MenuItem, ToolbarItem, PanelDescriptor } from '../Types/UITypes';
import type { MenuItem, ToolbarItem, PanelDescriptor, EntityCreationTemplate } from '../Types/UITypes';
import type { ComponentAction } from '../Services/ComponentActionRegistry';
import type { ReactNode } from 'react';
/**
@@ -227,6 +228,16 @@ export interface IEditorPlugin extends IPlugin {
* 注册文件创建模板
*/
registerFileCreationTemplates?(): FileCreationTemplate[];
/**
* 注册实体创建模板
*/
registerEntityCreationTemplates?(): EntityCreationTemplate[];
/**
* 注册组件操作
*/
registerComponentActions?(): ComponentAction[];
}
/**

View File

@@ -0,0 +1,528 @@
/**
* 统一插件注册中心
* Unified plugin registry
*/
import type { ComponentType } from '@esengine/ecs-framework';
import type { ComponentType as ReactComponentType } from 'react';
import {
EditorPluginDefinition,
RegisteredPlugin,
PluginState,
ComponentRegistration,
MenuItemRegistration,
PanelRegistration,
ToolbarItemRegistration,
AssetHandlerRegistration,
MenuTreeNode,
ComponentActionDefinition
} from './PluginTypes';
/**
* 插件注册中心
* Plugin registry - central hub for all plugin registrations
*/
export class PluginRegistry {
private static _instance: PluginRegistry | null = null;
/** 已注册的插件 */
private plugins: Map<string, RegisteredPlugin> = new Map();
/** 组件到检查器的映射 */
private componentInspectors: Map<string, ReactComponentType<any>> = new Map();
/** 组件元数据 */
private componentMeta: Map<string, ComponentRegistration> = new Map();
/** 组件操作 */
private componentActions: Map<string, ComponentActionDefinition[]> = new Map();
/** 菜单树 */
private menuTree: MenuTreeNode = {
name: 'root',
path: '',
children: new Map()
};
/** 面板注册 */
private panels: Map<string, PanelRegistration & { pluginId: string }> = new Map();
/** 工具栏项 */
private toolbarItems: Map<string, ToolbarItemRegistration & { pluginId: string }> = new Map();
/** 资产处理器 */
private assetHandlers: Map<string, AssetHandlerRegistration & { pluginId: string }> = new Map();
/** 事件监听器 */
private listeners: Map<string, Set<Function>> = new Map();
private constructor() {}
/**
* 获取单例实例
*/
static getInstance(): PluginRegistry {
if (!PluginRegistry._instance) {
PluginRegistry._instance = new PluginRegistry();
}
return PluginRegistry._instance;
}
/**
* 注册插件
* Register a plugin
*/
async register(definition: EditorPluginDefinition): Promise<void> {
if (this.plugins.has(definition.id)) {
console.warn(`Plugin ${definition.id} is already registered`);
return;
}
// 检查依赖
if (definition.dependencies) {
for (const dep of definition.dependencies) {
if (!this.plugins.has(dep)) {
throw new Error(`Plugin ${definition.id} depends on ${dep}, which is not registered`);
}
const depPlugin = this.plugins.get(dep)!;
if (depPlugin.state !== 'active') {
throw new Error(`Plugin ${definition.id} depends on ${dep}, which is not active`);
}
}
}
// 创建注册记录
const registered: RegisteredPlugin = {
definition,
state: 'inactive'
};
this.plugins.set(definition.id, registered);
// 激活插件
await this.activatePlugin(definition.id);
}
/**
* 激活插件
*/
private async activatePlugin(pluginId: string): Promise<void> {
const registered = this.plugins.get(pluginId);
if (!registered) return;
const { definition } = registered;
registered.state = 'activating';
try {
// 注册组件
if (definition.components) {
for (const comp of definition.components) {
this.registerComponent(pluginId, comp);
}
}
// 注册菜单项
if (definition.menuItems) {
for (const item of definition.menuItems) {
this.registerMenuItem(pluginId, item);
}
}
// 注册面板
if (definition.panels) {
for (const panel of definition.panels) {
this.registerPanel(pluginId, panel);
}
}
// 注册工具栏项
if (definition.toolbarItems) {
for (const item of definition.toolbarItems) {
this.registerToolbarItem(pluginId, item);
}
}
// 注册资产处理器
if (definition.assetHandlers) {
for (const handler of definition.assetHandlers) {
this.registerAssetHandler(pluginId, handler);
}
}
// 调用激活钩子
if (definition.onActivate) {
await definition.onActivate();
}
registered.state = 'active';
registered.activatedAt = Date.now();
this.emit('plugin:activated', { pluginId });
} catch (error) {
registered.state = 'error';
registered.error = error instanceof Error ? error.message : String(error);
console.error(`Failed to activate plugin ${pluginId}:`, error);
throw error;
}
}
/**
* 停用插件
*/
async deactivate(pluginId: string): Promise<void> {
const registered = this.plugins.get(pluginId);
if (!registered || registered.state !== 'active') return;
const { definition } = registered;
registered.state = 'deactivating';
try {
// 调用停用钩子
if (definition.onDeactivate) {
definition.onDeactivate();
}
// 清理注册的资源
this.unregisterPluginResources(pluginId);
registered.state = 'inactive';
this.emit('plugin:deactivated', { pluginId });
} catch (error) {
registered.state = 'error';
registered.error = error instanceof Error ? error.message : String(error);
throw error;
}
}
/**
* 注册组件
*/
private registerComponent(pluginId: string, config: ComponentRegistration): void {
const typeName = config.type.name;
// 保存元数据
this.componentMeta.set(typeName, config);
// 注册检查器
if (config.inspector) {
this.componentInspectors.set(typeName, config.inspector);
}
// 注册操作
if (config.actions) {
this.componentActions.set(typeName, config.actions);
}
this.emit('component:registered', { pluginId, typeName, config });
}
/**
* 注册菜单项
*/
private registerMenuItem(pluginId: string, item: MenuItemRegistration): void {
const parts = item.path.split('/');
let current = this.menuTree;
for (let i = 0; i < parts.length; i++) {
const part = parts[i];
const isLast = i === parts.length - 1;
if (!current.children.has(part)) {
current.children.set(part, {
name: part,
path: parts.slice(0, i + 1).join('/'),
children: new Map()
});
}
current = current.children.get(part)!;
if (isLast) {
current.item = item;
current.pluginId = pluginId;
}
}
this.emit('menu:registered', { pluginId, path: item.path });
}
/**
* 注册面板
*/
private registerPanel(pluginId: string, panel: PanelRegistration): void {
this.panels.set(panel.id, { ...panel, pluginId });
this.emit('panel:registered', { pluginId, panelId: panel.id });
}
/**
* 注册工具栏项
*/
private registerToolbarItem(pluginId: string, item: ToolbarItemRegistration): void {
this.toolbarItems.set(item.id, { ...item, pluginId });
this.emit('toolbar:registered', { pluginId, itemId: item.id });
}
/**
* 注册资产处理器
*/
private registerAssetHandler(pluginId: string, handler: AssetHandlerRegistration): void {
for (const ext of handler.extensions) {
this.assetHandlers.set(ext.toLowerCase(), { ...handler, pluginId });
}
this.emit('assetHandler:registered', { pluginId, extensions: handler.extensions });
}
/**
* 清理插件资源
*/
private unregisterPluginResources(pluginId: string): void {
const definition = this.plugins.get(pluginId)?.definition;
if (!definition) return;
// 清理组件
if (definition.components) {
for (const comp of definition.components) {
const typeName = comp.type.name;
this.componentMeta.delete(typeName);
this.componentInspectors.delete(typeName);
this.componentActions.delete(typeName);
}
}
// 清理面板
if (definition.panels) {
for (const panel of definition.panels) {
this.panels.delete(panel.id);
}
}
// 清理工具栏项
if (definition.toolbarItems) {
for (const item of definition.toolbarItems) {
this.toolbarItems.delete(item.id);
}
}
// 清理资产处理器
if (definition.assetHandlers) {
for (const handler of definition.assetHandlers) {
for (const ext of handler.extensions) {
this.assetHandlers.delete(ext.toLowerCase());
}
}
}
// 清理菜单项(需要遍历树)
this.removeMenuItemsForPlugin(pluginId, this.menuTree);
}
/**
* 递归移除插件的菜单项
*/
private removeMenuItemsForPlugin(pluginId: string, node: MenuTreeNode): void {
for (const [key, child] of node.children) {
if (child.pluginId === pluginId) {
node.children.delete(key);
} else {
this.removeMenuItemsForPlugin(pluginId, child);
}
}
}
// === 查询 API ===
/**
* 获取组件的检查器
*/
getComponentInspector(typeName: string): ReactComponentType<any> | undefined {
return this.componentInspectors.get(typeName);
}
/**
* 获取组件元数据
*/
getComponentMeta(typeName: string): ComponentRegistration | undefined {
return this.componentMeta.get(typeName);
}
/**
* 获取组件操作
*/
getComponentActionDefinitions(typeName: string): ComponentActionDefinition[] {
return this.componentActions.get(typeName) || [];
}
/**
* 获取所有已注册的组件
*/
getAllComponents(): Map<string, ComponentRegistration> {
return new Map(this.componentMeta);
}
/**
* 获取菜单树
*/
getMenuTree(): MenuTreeNode {
return this.menuTree;
}
/**
* 获取指定路径下的菜单项
*/
getMenuItems(parentPath: string): MenuItemRegistration[] {
const parts = parentPath ? parentPath.split('/') : [];
let current = this.menuTree;
for (const part of parts) {
if (!current.children.has(part)) {
return [];
}
current = current.children.get(part)!;
}
const items: MenuItemRegistration[] = [];
for (const child of current.children.values()) {
if (child.item) {
items.push(child.item);
}
}
return items.sort((a, b) => (a.priority || 100) - (b.priority || 100));
}
/**
* 获取所有面板
*/
getAllPanels(): Map<string, PanelRegistration & { pluginId: string }> {
return new Map(this.panels);
}
/**
* 获取面板
*/
getPanel(panelId: string): (PanelRegistration & { pluginId: string }) | undefined {
return this.panels.get(panelId);
}
/**
* 获取所有工具栏项
*/
getAllToolbarItems(): (ToolbarItemRegistration & { pluginId: string })[] {
return Array.from(this.toolbarItems.values())
.sort((a, b) => (a.priority || 100) - (b.priority || 100));
}
/**
* 获取资产处理器
*/
getAssetHandler(extension: string): (AssetHandlerRegistration & { pluginId: string }) | undefined {
return this.assetHandlers.get(extension.toLowerCase());
}
/**
* 获取插件状态
*/
getPluginState(pluginId: string): PluginState | undefined {
return this.plugins.get(pluginId)?.state;
}
/**
* 获取所有已注册插件
*/
getAllPlugins(): Map<string, RegisteredPlugin> {
return new Map(this.plugins);
}
/**
* 检查插件是否已激活
*/
isPluginActive(pluginId: string): boolean {
return this.plugins.get(pluginId)?.state === 'active';
}
// === 事件系统 ===
/**
* 添加事件监听器
*/
on(event: string, listener: Function): void {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set());
}
this.listeners.get(event)!.add(listener);
}
/**
* 移除事件监听器
*/
off(event: string, listener: Function): void {
this.listeners.get(event)?.delete(listener);
}
/**
* 发射事件
*/
private emit(event: string, data: any): void {
const listeners = this.listeners.get(event);
if (listeners) {
for (const listener of listeners) {
try {
listener(data);
} catch (error) {
console.error(`Error in event listener for ${event}:`, error);
}
}
}
}
/**
* 通知编辑器准备就绪
*/
notifyEditorReady(): void {
for (const [pluginId, registered] of this.plugins) {
if (registered.state === 'active' && registered.definition.onEditorReady) {
try {
registered.definition.onEditorReady();
} catch (error) {
console.error(`Error in onEditorReady for plugin ${pluginId}:`, error);
}
}
}
}
/**
* 通知场景加载
*/
notifySceneLoaded(scenePath: string): void {
for (const [pluginId, registered] of this.plugins) {
if (registered.state === 'active' && registered.definition.onSceneLoaded) {
try {
registered.definition.onSceneLoaded(scenePath);
} catch (error) {
console.error(`Error in onSceneLoaded for plugin ${pluginId}:`, error);
}
}
}
}
/**
* 通知场景保存
*/
notifySceneSaving(scenePath: string): boolean {
for (const [pluginId, registered] of this.plugins) {
if (registered.state === 'active' && registered.definition.onSceneSaving) {
try {
const result = registered.definition.onSceneSaving(scenePath);
if (result === false) {
return false; // 取消保存
}
} catch (error) {
console.error(`Error in onSceneSaving for plugin ${pluginId}:`, error);
}
}
}
return true;
}
}
// 导出单例访问
export const pluginRegistry = PluginRegistry.getInstance();

View File

@@ -0,0 +1,227 @@
/**
* 统一插件类型定义
* Unified plugin type definitions
*/
import type { ComponentType } from '@esengine/ecs-framework';
import type { ComponentType as ReactComponentType } from 'react';
/**
* 组件注册配置
* Component registration configuration
*/
export interface ComponentRegistration {
/** 组件类型 */
type: ComponentType;
/** 自定义检查器组件 */
inspector?: ReactComponentType<any>;
/** 图标名称 */
icon?: string;
/** 分类 */
category?: string;
/** 显示名称 */
displayName?: string;
/** 组件操作(如右键菜单) */
actions?: ComponentActionDefinition[];
}
/**
* 组件操作定义(用于声明式 API
* Component action definition (for declarative API)
*/
export interface ComponentActionDefinition {
/** 操作ID */
id: string;
/** 显示标签 */
label: string;
/** 图标 */
icon?: string;
/** 执行函数 */
execute: (componentData: any, entityId: number) => void | Promise<void>;
}
/**
* 菜单项注册配置
* Menu item registration configuration
*/
export interface MenuItemRegistration {
/** 菜单路径,如 "GameObject/2D Object/Tilemap" */
path: string;
/** 执行动作 */
action: () => void | number | Promise<void> | Promise<number>;
/** 图标 */
icon?: string;
/** 快捷键 */
shortcut?: string;
/** 优先级(数字越小越靠前) */
priority?: number;
}
/**
* 面板注册配置
* Panel registration configuration
*/
export interface PanelRegistration {
/** 面板ID */
id: string;
/** 面板组件 */
component: ReactComponentType<any>;
/** 标题 */
title: string;
/** 图标 */
icon?: string;
/** 默认位置 */
defaultPosition?: 'left' | 'right' | 'bottom' | 'float';
/** 默认是否可见 */
defaultVisible?: boolean;
}
/**
* 工具栏项注册配置
* Toolbar item registration configuration
*/
export interface ToolbarItemRegistration {
/** 工具ID */
id: string;
/** 显示标签 */
label: string;
/** 图标 */
icon: string;
/** 工具提示 */
tooltip?: string;
/** 执行动作 */
action: () => void;
/** 分组 */
group?: string;
/** 优先级 */
priority?: number;
}
/**
* 实体创建模板注册配置
*/
export interface EntityTemplateRegistration {
/** 模板ID */
id: string;
/** 显示名称 */
label: string;
/** 分类路径,如 "2D Object" */
category?: string;
/** 图标 */
icon?: string;
/** 排序优先级 */
priority?: number;
/** 创建实体函数返回实体ID */
create: (parentEntityId?: number) => number | Promise<number>;
}
/**
* 资产处理器注册配置
* Asset handler registration configuration
*/
export interface AssetHandlerRegistration {
/** 文件扩展名列表 */
extensions: string[];
/** 处理器名称 */
name: string;
/** 图标 */
icon?: string;
/** 打开资产 */
onOpen?: (assetPath: string) => void | Promise<void>;
/** 预览资产 */
onPreview?: (assetPath: string) => ReactComponentType<any> | null;
/** 创建资产 */
onCreate?: () => Promise<string | null>;
}
/**
* 编辑器插件定义
* Editor plugin definition
*/
export interface EditorPluginDefinition {
/** 插件唯一ID */
id: string;
/** 插件显示名称 */
name: string;
/** 版本号 */
version?: string;
/** 描述 */
description?: string;
/** 依赖的其他插件ID */
dependencies?: string[];
// === 注册配置 ===
/** 组件注册 */
components?: ComponentRegistration[];
/** 菜单项注册 */
menuItems?: MenuItemRegistration[];
/** 面板注册 */
panels?: PanelRegistration[];
/** 工具栏项注册 */
toolbarItems?: ToolbarItemRegistration[];
/** 资产处理器注册 */
assetHandlers?: AssetHandlerRegistration[];
/** 实体创建模板 */
entityTemplates?: EntityTemplateRegistration[];
// === 生命周期钩子 ===
/** 插件激活时调用 */
onActivate?: () => void | Promise<void>;
/** 插件停用时调用 */
onDeactivate?: () => void;
/** 编辑器准备就绪时调用 */
onEditorReady?: () => void | Promise<void>;
/** 场景加载后调用 */
onSceneLoaded?: (scenePath: string) => void;
/** 场景保存前调用 */
onSceneSaving?: (scenePath: string) => boolean | void;
}
/**
* 插件状态
* Plugin state
*/
export type PluginState = 'inactive' | 'activating' | 'active' | 'deactivating' | 'error';
/**
* 已注册的插件信息
* Registered plugin info
*/
export interface RegisteredPlugin {
/** 插件定义 */
definition: EditorPluginDefinition;
/** 当前状态 */
state: PluginState;
/** 错误信息(如果有) */
error?: string;
/** 激活时间 */
activatedAt?: number;
}
/**
* 菜单树节点
* Menu tree node
*/
export interface MenuTreeNode {
/** 节点名称 */
name: string;
/** 完整路径 */
path: string;
/** 子节点 */
children: Map<string, MenuTreeNode>;
/** 菜单项(叶子节点) */
item?: MenuItemRegistration;
/** 来源插件ID */
pluginId?: string;
}

View File

@@ -31,3 +31,7 @@ export class CompilerRegistry implements IService {
this.clear();
}
}
// Service identifier for DI registration (用于跨包插件访问)
// 使用 Symbol.for 确保跨包共享同一个 Symbol
export const ICompilerRegistry = Symbol.for('ICompilerRegistry');

View File

@@ -0,0 +1,95 @@
/**
* Component Action Registry Service
*
* Manages component-specific actions for the inspector panel
*/
import { injectable } from 'tsyringe';
import type { IService, Component, Entity } from '@esengine/ecs-framework';
import type { ReactNode } from 'react';
export interface ComponentAction {
id: string;
componentName: string;
label: string;
icon?: ReactNode;
order?: number;
execute: (component: Component, entity: Entity) => void | Promise<void>;
}
@injectable()
export class ComponentActionRegistry implements IService {
private actions: Map<string, ComponentAction[]> = new Map();
/**
* Register a component action
*/
register(action: ComponentAction): void {
const componentName = action.componentName;
if (!this.actions.has(componentName)) {
this.actions.set(componentName, []);
}
const actions = this.actions.get(componentName)!;
const existingIndex = actions.findIndex(a => a.id === action.id);
if (existingIndex >= 0) {
console.warn(`[ComponentActionRegistry] Action '${action.id}' already exists for '${componentName}', overwriting`);
actions[existingIndex] = action;
} else {
actions.push(action);
}
}
/**
* Register multiple actions
*/
registerMany(actions: ComponentAction[]): void {
for (const action of actions) {
this.register(action);
}
}
/**
* Unregister an action by ID
*/
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);
}
}
}
/**
* Get all actions for a component type sorted by order
*/
getActionsForComponent(componentName: string): ComponentAction[] {
const actions = this.actions.get(componentName) || [];
return [...actions].sort((a, b) => (a.order ?? 100) - (b.order ?? 100));
}
/**
* Check if a component has any actions
*/
hasActions(componentName: string): boolean {
const actions = this.actions.get(componentName);
return actions !== undefined && actions.length > 0;
}
/**
* Clear all actions
*/
clear(): void {
this.actions.clear();
}
/**
* Dispose resources
*/
dispose(): void {
this.actions.clear();
}
}

View File

@@ -0,0 +1,76 @@
/**
* Entity Creation Registry Service
*
* Manages entity creation templates for the scene hierarchy context menu
*/
import { injectable } from 'tsyringe';
import type { IService } from '@esengine/ecs-framework';
import type { EntityCreationTemplate } from '../Types/UITypes';
@injectable()
export class EntityCreationRegistry implements IService {
private templates: Map<string, EntityCreationTemplate> = new Map();
/**
* 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);
}
/**
* Register multiple templates
*/
registerMany(templates: EntityCreationTemplate[]): void {
for (const template of templates) {
this.register(template);
}
}
/**
* Unregister a template by ID
*/
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();
}
}

View File

@@ -14,8 +14,13 @@ export interface SaveDialogOptions extends DialogOptions {
}
export interface IDialog {
dispose(): void;
openDialog(options: OpenDialogOptions): Promise<string | string[] | null>;
saveDialog(options: SaveDialogOptions): Promise<string | null>;
showMessage(title: string, message: string, type?: 'info' | 'warning' | 'error'): Promise<void>;
showConfirm(title: string, message: string): Promise<boolean>;
}
// Service identifier for DI registration
// 使用 Symbol.for 确保跨包共享同一个 Symbol
export const IDialogService = Symbol.for('IDialogService');

View File

@@ -1,4 +1,5 @@
export interface IFileSystem {
dispose(): void;
readFile(path: string): Promise<string>;
writeFile(path: string, content: string): Promise<void>;
writeBinary(path: string, data: Uint8Array): Promise<void>;
@@ -8,6 +9,12 @@ export interface IFileSystem {
deleteFile(path: string): Promise<void>;
deleteDirectory(path: string): Promise<void>;
scanFiles(basePath: string, pattern: string): Promise<string[]>;
/**
* Convert a local file path to an asset URL that can be used in browser contexts (img src, audio src, etc.)
* @param filePath The local file path
* @returns The converted asset URL
*/
convertToAssetUrl(filePath: string): string;
}
export interface FileEntry {
@@ -17,3 +24,7 @@ export interface FileEntry {
size?: number;
modified?: Date;
}
// Service identifier for DI registration
// 使用 Symbol.for 确保跨包共享同一个 Symbol
export const IFileSystemService = Symbol.for('IFileSystemService');

View File

@@ -75,3 +75,7 @@ export class InspectorRegistry implements IService {
this.clear();
}
}
// Service identifier for DI registration (用于跨包插件访问)
// 使用 Symbol.for 确保跨包共享同一个 Symbol
export const IInspectorRegistry = Symbol.for('IInspectorRegistry');

View File

@@ -215,3 +215,7 @@ export class MessageHub implements IService {
logger.info('MessageHub disposed');
}
}
// Service identifier for DI registration (用于跨包插件访问)
// 使用 Symbol.for 确保跨包共享同一个 Symbol
export const IMessageHub = Symbol.for('IMessageHub');

View File

@@ -1,5 +1,6 @@
import type { IService } from '@esengine/ecs-framework';
import { Injectable, Core, createLogger, SceneSerializer, Scene } from '@esengine/ecs-framework';
import type { SceneResourceManager } from '@esengine/asset-system';
import type { MessageHub } from './MessageHub';
import type { IFileAPI } from '../Types/IFileAPI';
import type { ProjectService } from './ProjectService';
@@ -24,6 +25,7 @@ export class SceneManagerService implements IService {
};
private unsubscribeHandlers: Array<() => void> = [];
private sceneResourceManager: SceneResourceManager | null = null;
constructor(
private messageHub: MessageHub,
@@ -35,6 +37,14 @@ export class SceneManagerService implements IService {
logger.info('SceneManagerService initialized');
}
/**
* 设置场景资源管理器
* Set scene resource manager
*/
public setSceneResourceManager(manager: SceneResourceManager | null): void {
this.sceneResourceManager = manager;
}
public async newScene(): Promise<void> {
if (!await this.canClose()) {
return;
@@ -91,6 +101,13 @@ export class SceneManagerService implements IService {
strategy: 'replace'
});
// 加载场景资源 / Load scene resources
if (this.sceneResourceManager) {
await this.sceneResourceManager.loadSceneResources(scene);
} else {
logger.warn('[SceneManagerService] SceneResourceManager not available, skipping resource loading');
}
const fileName = path.split(/[/\\]/).pop() || 'Untitled';
const sceneName = fileName.replace('.ecs', '');

View File

@@ -163,3 +163,35 @@ export enum UIExtensionType {
Inspector = 'inspector',
StatusBar = 'statusbar'
}
/**
* 实体创建模板
*/
export interface EntityCreationTemplate {
/**
* 模板唯一标识
*/
id: string;
/**
* 显示名称
*/
label: string;
/**
* 图标组件
*/
icon?: any;
/**
* 排序权重(数字越小越靠前)
*/
order?: number;
/**
* 创建实体的函数
* @param parentEntityId 父实体ID可选
* @returns 创建的实体ID
*/
create: (parentEntityId?: number) => number | Promise<number>;
}

View File

@@ -6,6 +6,8 @@
export * from './Plugins/IEditorPlugin';
export * from './Plugins/EditorPluginManager';
export * from './Plugins/PluginTypes';
export * from './Plugins/PluginRegistry';
export * from './Services/UIRegistry';
export * from './Services/MessageHub';
@@ -20,6 +22,7 @@ export * from './Services/LogService';
export * from './Services/SettingsRegistry';
export * from './Services/SceneManagerService';
export * from './Services/FileActionRegistry';
export * from './Services/EntityCreationRegistry';
export * from './Services/CompilerRegistry';
export * from './Services/ICompiler';
export * from './Services/ICommand';
@@ -35,6 +38,9 @@ export * from './Services/IPropertyRenderer';
export * from './Services/PropertyRendererRegistry';
export * from './Services/IFieldEditor';
export * from './Services/FieldEditorRegistry';
export * from './Services/ComponentActionRegistry';
export * from './Gizmos';
export * from './Module/IEventBus';
export * from './Module/ICommandRegistry';