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:
@@ -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
5044
packages/editor-core/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
201
packages/editor-core/src/Gizmos/GizmoRegistry.ts
Normal file
201
packages/editor-core/src/Gizmos/GizmoRegistry.ts
Normal 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);
|
||||
}
|
||||
182
packages/editor-core/src/Gizmos/IGizmoProvider.ts
Normal file
182
packages/editor-core/src/Gizmos/IGizmoProvider.ts
Normal 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) | 原点 X(0-1,默认 0.5 居中) */
|
||||
originX: number;
|
||||
/** Origin Y (0-1, default 0.5 for center) | 原点 Y(0-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,
|
||||
};
|
||||
12
packages/editor-core/src/Gizmos/index.ts
Normal file
12
packages/editor-core/src/Gizmos/index.ts
Normal 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';
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
528
packages/editor-core/src/Plugins/PluginRegistry.ts
Normal file
528
packages/editor-core/src/Plugins/PluginRegistry.ts
Normal 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();
|
||||
227
packages/editor-core/src/Plugins/PluginTypes.ts
Normal file
227
packages/editor-core/src/Plugins/PluginTypes.ts
Normal 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;
|
||||
}
|
||||
@@ -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');
|
||||
|
||||
95
packages/editor-core/src/Services/ComponentActionRegistry.ts
Normal file
95
packages/editor-core/src/Services/ComponentActionRegistry.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
76
packages/editor-core/src/Services/EntityCreationRegistry.ts
Normal file
76
packages/editor-core/src/Services/EntityCreationRegistry.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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', '');
|
||||
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user