编辑器核心框架
This commit is contained in:
217
packages/editor-core/src/Services/MessageHub.ts
Normal file
217
packages/editor-core/src/Services/MessageHub.ts
Normal file
@@ -0,0 +1,217 @@
|
||||
import type { IService } from '@esengine/ecs-framework';
|
||||
import { Injectable } from '@esengine/ecs-framework';
|
||||
import { createLogger } from '@esengine/ecs-framework';
|
||||
|
||||
const logger = createLogger('MessageHub');
|
||||
|
||||
/**
|
||||
* 消息处理器类型
|
||||
*/
|
||||
export type MessageHandler<T = any> = (data: T) => void | Promise<void>;
|
||||
|
||||
/**
|
||||
* 消息订阅
|
||||
*/
|
||||
interface MessageSubscription {
|
||||
topic: string;
|
||||
handler: MessageHandler;
|
||||
once: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息总线
|
||||
*
|
||||
* 提供插件间的事件通信机制,支持订阅/发布模式。
|
||||
*/
|
||||
@Injectable()
|
||||
export class MessageHub implements IService {
|
||||
private subscriptions: Map<string, MessageSubscription[]> = new Map();
|
||||
private subscriptionId: number = 0;
|
||||
|
||||
/**
|
||||
* 订阅消息
|
||||
*
|
||||
* @param topic - 消息主题
|
||||
* @param handler - 消息处理器
|
||||
* @returns 取消订阅的函数
|
||||
*/
|
||||
public subscribe<T = any>(topic: string, handler: MessageHandler<T>): () => void {
|
||||
return this.addSubscription(topic, handler, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 订阅一次性消息
|
||||
*
|
||||
* @param topic - 消息主题
|
||||
* @param handler - 消息处理器
|
||||
* @returns 取消订阅的函数
|
||||
*/
|
||||
public subscribeOnce<T = any>(topic: string, handler: MessageHandler<T>): () => void {
|
||||
return this.addSubscription(topic, handler, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加订阅
|
||||
*/
|
||||
private addSubscription(topic: string, handler: MessageHandler, once: boolean): () => void {
|
||||
const subscription: MessageSubscription = {
|
||||
topic,
|
||||
handler,
|
||||
once
|
||||
};
|
||||
|
||||
let subs = this.subscriptions.get(topic);
|
||||
if (!subs) {
|
||||
subs = [];
|
||||
this.subscriptions.set(topic, subs);
|
||||
}
|
||||
subs.push(subscription);
|
||||
|
||||
const subId = ++this.subscriptionId;
|
||||
logger.debug(`Subscribed to topic: ${topic} (id: ${subId}, once: ${once})`);
|
||||
|
||||
return () => {
|
||||
this.unsubscribe(topic, subscription);
|
||||
logger.debug(`Unsubscribed from topic: ${topic} (id: ${subId})`);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消订阅
|
||||
*/
|
||||
private unsubscribe(topic: string, subscription: MessageSubscription): void {
|
||||
const subs = this.subscriptions.get(topic);
|
||||
if (!subs) {
|
||||
return;
|
||||
}
|
||||
|
||||
const index = subs.indexOf(subscription);
|
||||
if (index !== -1) {
|
||||
subs.splice(index, 1);
|
||||
}
|
||||
|
||||
if (subs.length === 0) {
|
||||
this.subscriptions.delete(topic);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发布消息
|
||||
*
|
||||
* @param topic - 消息主题
|
||||
* @param data - 消息数据
|
||||
*/
|
||||
public async publish<T = any>(topic: string, data?: T): Promise<void> {
|
||||
const subs = this.subscriptions.get(topic);
|
||||
if (!subs || subs.length === 0) {
|
||||
logger.debug(`No subscribers for topic: ${topic}`);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug(`Publishing message to topic: ${topic} (${subs.length} subscribers)`);
|
||||
|
||||
const onceSubscriptions: MessageSubscription[] = [];
|
||||
|
||||
for (const sub of subs) {
|
||||
try {
|
||||
await sub.handler(data);
|
||||
if (sub.once) {
|
||||
onceSubscriptions.push(sub);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Error in message handler for topic ${topic}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
for (const sub of onceSubscriptions) {
|
||||
this.unsubscribe(topic, sub);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步发布消息
|
||||
*
|
||||
* @param topic - 消息主题
|
||||
* @param data - 消息数据
|
||||
*/
|
||||
public publishSync<T = any>(topic: string, data?: T): void {
|
||||
const subs = this.subscriptions.get(topic);
|
||||
if (!subs || subs.length === 0) {
|
||||
logger.debug(`No subscribers for topic: ${topic}`);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug(`Publishing sync message to topic: ${topic} (${subs.length} subscribers)`);
|
||||
|
||||
const onceSubscriptions: MessageSubscription[] = [];
|
||||
|
||||
for (const sub of subs) {
|
||||
try {
|
||||
const result = sub.handler(data);
|
||||
if (result instanceof Promise) {
|
||||
logger.warn(`Async handler used with publishSync for topic: ${topic}`);
|
||||
}
|
||||
if (sub.once) {
|
||||
onceSubscriptions.push(sub);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Error in message handler for topic ${topic}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
for (const sub of onceSubscriptions) {
|
||||
this.unsubscribe(topic, sub);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消所有指定主题的订阅
|
||||
*
|
||||
* @param topic - 消息主题
|
||||
*/
|
||||
public unsubscribeAll(topic: string): void {
|
||||
const deleted = this.subscriptions.delete(topic);
|
||||
if (deleted) {
|
||||
logger.debug(`Unsubscribed all from topic: ${topic}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查主题是否有订阅者
|
||||
*
|
||||
* @param topic - 消息主题
|
||||
* @returns 是否有订阅者
|
||||
*/
|
||||
public hasSubscribers(topic: string): boolean {
|
||||
const subs = this.subscriptions.get(topic);
|
||||
return subs !== undefined && subs.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有主题
|
||||
*
|
||||
* @returns 主题列表
|
||||
*/
|
||||
public getTopics(): string[] {
|
||||
return Array.from(this.subscriptions.keys());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取主题的订阅者数量
|
||||
*
|
||||
* @param topic - 消息主题
|
||||
* @returns 订阅者数量
|
||||
*/
|
||||
public getSubscriberCount(topic: string): number {
|
||||
const subs = this.subscriptions.get(topic);
|
||||
return subs ? subs.length : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放资源
|
||||
*/
|
||||
public dispose(): void {
|
||||
this.subscriptions.clear();
|
||||
logger.info('MessageHub disposed');
|
||||
}
|
||||
}
|
||||
181
packages/editor-core/src/Services/SerializerRegistry.ts
Normal file
181
packages/editor-core/src/Services/SerializerRegistry.ts
Normal file
@@ -0,0 +1,181 @@
|
||||
import type { IService } from '@esengine/ecs-framework';
|
||||
import { Injectable } from '@esengine/ecs-framework';
|
||||
import { createLogger } from '@esengine/ecs-framework';
|
||||
import type { ISerializer } from '../Plugins/IEditorPlugin';
|
||||
|
||||
const logger = createLogger('SerializerRegistry');
|
||||
|
||||
/**
|
||||
* 序列化器注册表
|
||||
*
|
||||
* 管理所有数据序列化器的注册和查询。
|
||||
*/
|
||||
@Injectable()
|
||||
export class SerializerRegistry implements IService {
|
||||
private serializers: Map<string, ISerializer> = new Map();
|
||||
|
||||
/**
|
||||
* 注册序列化器
|
||||
*
|
||||
* @param pluginName - 插件名称
|
||||
* @param serializer - 序列化器实例
|
||||
*/
|
||||
public register(pluginName: string, serializer: ISerializer): void {
|
||||
const type = serializer.getSupportedType();
|
||||
const key = `${pluginName}:${type}`;
|
||||
|
||||
if (this.serializers.has(key)) {
|
||||
logger.warn(`Serializer for ${key} is already registered`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.serializers.set(key, serializer);
|
||||
logger.info(`Registered serializer: ${key}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量注册序列化器
|
||||
*
|
||||
* @param pluginName - 插件名称
|
||||
* @param serializers - 序列化器实例数组
|
||||
*/
|
||||
public registerMultiple(pluginName: string, serializers: ISerializer[]): void {
|
||||
for (const serializer of serializers) {
|
||||
this.register(pluginName, serializer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注销序列化器
|
||||
*
|
||||
* @param pluginName - 插件名称
|
||||
* @param type - 数据类型
|
||||
* @returns 是否成功注销
|
||||
*/
|
||||
public unregister(pluginName: string, type: string): boolean {
|
||||
const key = `${pluginName}:${type}`;
|
||||
const result = this.serializers.delete(key);
|
||||
|
||||
if (result) {
|
||||
logger.info(`Unregistered serializer: ${key}`);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 注销插件的所有序列化器
|
||||
*
|
||||
* @param pluginName - 插件名称
|
||||
*/
|
||||
public unregisterAll(pluginName: string): void {
|
||||
const prefix = `${pluginName}:`;
|
||||
const keysToDelete: string[] = [];
|
||||
|
||||
for (const key of this.serializers.keys()) {
|
||||
if (key.startsWith(prefix)) {
|
||||
keysToDelete.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
for (const key of keysToDelete) {
|
||||
this.serializers.delete(key);
|
||||
logger.info(`Unregistered serializer: ${key}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取序列化器
|
||||
*
|
||||
* @param pluginName - 插件名称
|
||||
* @param type - 数据类型
|
||||
* @returns 序列化器实例,如果未找到则返回 undefined
|
||||
*/
|
||||
public get(pluginName: string, type: string): ISerializer | undefined {
|
||||
const key = `${pluginName}:${type}`;
|
||||
return this.serializers.get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找支持指定类型的序列化器
|
||||
*
|
||||
* @param type - 数据类型
|
||||
* @returns 序列化器实例数组
|
||||
*/
|
||||
public findByType(type: string): ISerializer[] {
|
||||
const result: ISerializer[] = [];
|
||||
|
||||
for (const [key, serializer] of this.serializers) {
|
||||
if (key.endsWith(`:${type}`)) {
|
||||
result.push(serializer);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有序列化器
|
||||
*
|
||||
* @returns 序列化器映射表
|
||||
*/
|
||||
public getAll(): Map<string, ISerializer> {
|
||||
return new Map(this.serializers);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查序列化器是否已注册
|
||||
*
|
||||
* @param pluginName - 插件名称
|
||||
* @param type - 数据类型
|
||||
* @returns 是否已注册
|
||||
*/
|
||||
public has(pluginName: string, type: string): boolean {
|
||||
const key = `${pluginName}:${type}`;
|
||||
return this.serializers.has(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 序列化数据
|
||||
*
|
||||
* @param pluginName - 插件名称
|
||||
* @param type - 数据类型
|
||||
* @param data - 要序列化的数据
|
||||
* @returns 二进制数据
|
||||
* @throws 如果序列化器未注册
|
||||
*/
|
||||
public serialize<T = any>(pluginName: string, type: string, data: T): Uint8Array {
|
||||
const serializer = this.get(pluginName, type);
|
||||
if (!serializer) {
|
||||
throw new Error(`Serializer not found: ${pluginName}:${type}`);
|
||||
}
|
||||
|
||||
return serializer.serialize(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 反序列化数据
|
||||
*
|
||||
* @param pluginName - 插件名称
|
||||
* @param type - 数据类型
|
||||
* @param data - 二进制数据
|
||||
* @returns 反序列化后的数据
|
||||
* @throws 如果序列化器未注册
|
||||
*/
|
||||
public deserialize<T = any>(pluginName: string, type: string, data: Uint8Array): T {
|
||||
const serializer = this.get(pluginName, type);
|
||||
if (!serializer) {
|
||||
throw new Error(`Serializer not found: ${pluginName}:${type}`);
|
||||
}
|
||||
|
||||
return serializer.deserialize(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放资源
|
||||
*/
|
||||
public dispose(): void {
|
||||
this.serializers.clear();
|
||||
logger.info('SerializerRegistry disposed');
|
||||
}
|
||||
}
|
||||
202
packages/editor-core/src/Services/UIRegistry.ts
Normal file
202
packages/editor-core/src/Services/UIRegistry.ts
Normal file
@@ -0,0 +1,202 @@
|
||||
import type { IService } from '@esengine/ecs-framework';
|
||||
import { Injectable } from '@esengine/ecs-framework';
|
||||
import { createLogger } from '@esengine/ecs-framework';
|
||||
import type { MenuItem, ToolbarItem, PanelDescriptor } from '../Types/UITypes';
|
||||
|
||||
const logger = createLogger('UIRegistry');
|
||||
|
||||
/**
|
||||
* UI 注册表
|
||||
*
|
||||
* 管理所有编辑器 UI 扩展点的注册和查询。
|
||||
*/
|
||||
@Injectable()
|
||||
export class UIRegistry implements IService {
|
||||
private menus: Map<string, MenuItem> = new Map();
|
||||
private toolbarItems: Map<string, ToolbarItem> = new Map();
|
||||
private panels: Map<string, PanelDescriptor> = new Map();
|
||||
|
||||
/**
|
||||
* 注册菜单项
|
||||
*/
|
||||
public registerMenu(item: MenuItem): void {
|
||||
if (this.menus.has(item.id)) {
|
||||
logger.warn(`Menu item ${item.id} is already registered`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.menus.set(item.id, item);
|
||||
logger.debug(`Registered menu item: ${item.id}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量注册菜单项
|
||||
*/
|
||||
public registerMenus(items: MenuItem[]): void {
|
||||
for (const item of items) {
|
||||
this.registerMenu(item);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注销菜单项
|
||||
*/
|
||||
public unregisterMenu(id: string): boolean {
|
||||
const result = this.menus.delete(id);
|
||||
if (result) {
|
||||
logger.debug(`Unregistered menu item: ${id}`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取菜单项
|
||||
*/
|
||||
public getMenu(id: string): MenuItem | undefined {
|
||||
return this.menus.get(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有菜单项
|
||||
*/
|
||||
public getAllMenus(): MenuItem[] {
|
||||
return Array.from(this.menus.values()).sort((a, b) => {
|
||||
return (a.order ?? 0) - (b.order ?? 0);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定父菜单的子菜单
|
||||
*/
|
||||
public getChildMenus(parentId: string): MenuItem[] {
|
||||
return this.getAllMenus()
|
||||
.filter(item => item.parentId === parentId)
|
||||
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册工具栏项
|
||||
*/
|
||||
public registerToolbarItem(item: ToolbarItem): void {
|
||||
if (this.toolbarItems.has(item.id)) {
|
||||
logger.warn(`Toolbar item ${item.id} is already registered`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.toolbarItems.set(item.id, item);
|
||||
logger.debug(`Registered toolbar item: ${item.id}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量注册工具栏项
|
||||
*/
|
||||
public registerToolbarItems(items: ToolbarItem[]): void {
|
||||
for (const item of items) {
|
||||
this.registerToolbarItem(item);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注销工具栏项
|
||||
*/
|
||||
public unregisterToolbarItem(id: string): boolean {
|
||||
const result = this.toolbarItems.delete(id);
|
||||
if (result) {
|
||||
logger.debug(`Unregistered toolbar item: ${id}`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取工具栏项
|
||||
*/
|
||||
public getToolbarItem(id: string): ToolbarItem | undefined {
|
||||
return this.toolbarItems.get(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有工具栏项
|
||||
*/
|
||||
public getAllToolbarItems(): ToolbarItem[] {
|
||||
return Array.from(this.toolbarItems.values()).sort((a, b) => {
|
||||
return (a.order ?? 0) - (b.order ?? 0);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定组的工具栏项
|
||||
*/
|
||||
public getToolbarItemsByGroup(groupId: string): ToolbarItem[] {
|
||||
return this.getAllToolbarItems()
|
||||
.filter(item => item.groupId === groupId)
|
||||
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册面板
|
||||
*/
|
||||
public registerPanel(panel: PanelDescriptor): void {
|
||||
if (this.panels.has(panel.id)) {
|
||||
logger.warn(`Panel ${panel.id} is already registered`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.panels.set(panel.id, panel);
|
||||
logger.debug(`Registered panel: ${panel.id}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量注册面板
|
||||
*/
|
||||
public registerPanels(panels: PanelDescriptor[]): void {
|
||||
for (const panel of panels) {
|
||||
this.registerPanel(panel);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注销面板
|
||||
*/
|
||||
public unregisterPanel(id: string): boolean {
|
||||
const result = this.panels.delete(id);
|
||||
if (result) {
|
||||
logger.debug(`Unregistered panel: ${id}`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取面板
|
||||
*/
|
||||
public getPanel(id: string): PanelDescriptor | undefined {
|
||||
return this.panels.get(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有面板
|
||||
*/
|
||||
public getAllPanels(): PanelDescriptor[] {
|
||||
return Array.from(this.panels.values()).sort((a, b) => {
|
||||
return (a.order ?? 0) - (b.order ?? 0);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定位置的面板
|
||||
*/
|
||||
public getPanelsByPosition(position: string): PanelDescriptor[] {
|
||||
return this.getAllPanels()
|
||||
.filter(panel => panel.position === position)
|
||||
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放资源
|
||||
*/
|
||||
public dispose(): void {
|
||||
this.menus.clear();
|
||||
this.toolbarItems.clear();
|
||||
this.panels.clear();
|
||||
logger.info('UIRegistry disposed');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user