refactor(editor): 提取行为树编辑器为独立包并重构编辑器架构 (#216)
* refactor(editor): 提取行为树编辑器为独立包并重构编辑器架构 * feat(editor): 添加插件市场功能 * feat(editor): 重构插件市场以支持版本管理和ZIP打包 * feat(editor): 重构插件发布流程并修复React渲染警告 * fix(plugin): 修复插件发布和市场的路径不一致问题 * feat: 重构插件发布流程并添加插件删除功能 * fix(editor): 完善插件删除功能并修复多个关键问题 * fix(auth): 修复自动登录与手动登录的竞态条件问题 * feat(editor): 重构插件管理流程 * feat(editor): 支持 ZIP 文件直接发布插件 - 新增 PluginSourceParser 解析插件源 - 重构发布流程支持文件夹和 ZIP 两种方式 - 优化发布向导 UI * feat(editor): 插件市场支持多版本安装 - 插件解压到项目 plugins 目录 - 新增 Tauri 后端安装/卸载命令 - 支持选择任意版本安装 - 修复打包逻辑,保留完整 dist 目录结构 * feat(editor): 个人中心支持多版本管理 - 合并同一插件的不同版本 - 添加版本历史展开/折叠功能 - 禁止有待审核 PR 时更新插件 * fix(editor): 修复 InspectorRegistry 服务注册 - InspectorRegistry 实现 IService 接口 - 注册到 Core.services 供插件使用 * feat(behavior-tree-editor): 完善插件注册和文件操作 - 添加文件创建模板和操作处理器 - 实现右键菜单创建行为树功能 - 修复文件读取权限问题(使用 Tauri 命令) - 添加 BehaviorTreeEditorPanel 组件 - 修复 rollup 配置支持动态导入 * feat(plugin): 完善插件构建和发布流程 * fix(behavior-tree-editor): 完整恢复编辑器并修复 Toast 集成 * fix(behavior-tree-editor): 修复节点选中、连线跟随和文件加载问题并优化性能 * fix(behavior-tree-editor): 修复端口连接失败问题并优化连线样式 * refactor(behavior-tree-editor): 移除调试面板功能简化代码结构 * refactor(behavior-tree-editor): 清理冗余代码合并重复逻辑 * feat(behavior-tree-editor): 完善编辑器核心功能增强扩展性 * fix(lint): 修复ESLint错误确保CI通过 * refactor(behavior-tree-editor): 优化编辑器工具栏和编译器功能 * refactor(behavior-tree-editor): 清理技术债务,优化代码质量 * fix(editor-app): 修复字符串替换安全问题
This commit is contained in:
25
packages/editor-core/src/Module/ICommandRegistry.ts
Normal file
25
packages/editor-core/src/Module/ICommandRegistry.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
export interface KeyBinding {
|
||||
key: string;
|
||||
ctrl?: boolean;
|
||||
alt?: boolean;
|
||||
shift?: boolean;
|
||||
meta?: boolean;
|
||||
}
|
||||
|
||||
export interface IUICommand {
|
||||
readonly id: string;
|
||||
readonly label: string;
|
||||
readonly icon?: string;
|
||||
readonly keybinding?: KeyBinding;
|
||||
readonly when?: () => boolean;
|
||||
execute(context?: unknown): void | Promise<void>;
|
||||
}
|
||||
|
||||
export interface ICommandRegistry {
|
||||
register(command: IUICommand): void;
|
||||
unregister(commandId: string): void;
|
||||
execute(commandId: string, context?: unknown): Promise<void>;
|
||||
getCommand(commandId: string): IUICommand | undefined;
|
||||
getCommands(): IUICommand[];
|
||||
getKeybindings(): Array<{ command: IUICommand; keybinding: KeyBinding }>;
|
||||
}
|
||||
12
packages/editor-core/src/Module/IEditorModule.ts
Normal file
12
packages/editor-core/src/Module/IEditorModule.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import type { IModuleContext } from './IModuleContext';
|
||||
|
||||
export interface IEditorModule<TEvents = Record<string, unknown>> {
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
readonly version?: string;
|
||||
readonly dependencies?: string[];
|
||||
|
||||
load(context: IModuleContext<TEvents>): Promise<void>;
|
||||
unload?(): Promise<void>;
|
||||
reload?(): Promise<void>;
|
||||
}
|
||||
16
packages/editor-core/src/Module/IEventBus.ts
Normal file
16
packages/editor-core/src/Module/IEventBus.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { Observable } from 'rxjs';
|
||||
|
||||
export type Unsubscribe = () => void;
|
||||
|
||||
export interface IEventBus<TEvents = Record<string, unknown>> {
|
||||
publish<K extends keyof TEvents>(topic: K, data: TEvents[K]): Promise<void>;
|
||||
|
||||
subscribe<K extends keyof TEvents>(
|
||||
topic: K,
|
||||
handler: (data: TEvents[K]) => void | Promise<void>
|
||||
): Unsubscribe;
|
||||
|
||||
observe<K extends keyof TEvents>(topic: K): Observable<TEvents[K]>;
|
||||
|
||||
dispose(): void;
|
||||
}
|
||||
19
packages/editor-core/src/Module/IModuleContext.ts
Normal file
19
packages/editor-core/src/Module/IModuleContext.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import type { DependencyContainer } from 'tsyringe';
|
||||
import type { IEventBus } from './IEventBus';
|
||||
import type { ICommandRegistry } from './ICommandRegistry';
|
||||
import type { IPanelRegistry } from './IPanelRegistry';
|
||||
import type { IFileSystem } from '../Services/IFileSystem';
|
||||
import type { IDialog } from '../Services/IDialog';
|
||||
import type { INotification } from '../Services/INotification';
|
||||
import type { InspectorRegistry } from '../Services/InspectorRegistry';
|
||||
|
||||
export interface IModuleContext<TEvents = Record<string, unknown>> {
|
||||
readonly container: DependencyContainer;
|
||||
readonly eventBus: IEventBus<TEvents>;
|
||||
readonly commands: ICommandRegistry;
|
||||
readonly panels: IPanelRegistry;
|
||||
readonly fileSystem: IFileSystem;
|
||||
readonly dialog: IDialog;
|
||||
readonly notification: INotification;
|
||||
readonly inspectorRegistry: InspectorRegistry;
|
||||
}
|
||||
8
packages/editor-core/src/Module/IPanelRegistry.ts
Normal file
8
packages/editor-core/src/Module/IPanelRegistry.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import type { PanelDescriptor } from '../Types/UITypes';
|
||||
|
||||
export interface IPanelRegistry {
|
||||
register(panel: PanelDescriptor): void;
|
||||
unregister(panelId: string): void;
|
||||
getPanel(panelId: string): PanelDescriptor | undefined;
|
||||
getPanels(category?: string): PanelDescriptor[];
|
||||
}
|
||||
@@ -95,9 +95,7 @@ export class EditorPluginManager extends PluginManager {
|
||||
|
||||
if (plugin.registerFileActionHandlers && this.fileActionRegistry) {
|
||||
const handlers = plugin.registerFileActionHandlers();
|
||||
console.log(`[EditorPluginManager] Registering ${handlers.length} file action handlers for ${plugin.name}`);
|
||||
for (const handler of handlers) {
|
||||
console.log(`[EditorPluginManager] Handler for extensions:`, handler.extensions);
|
||||
this.fileActionRegistry.registerActionHandler(handler);
|
||||
}
|
||||
logger.debug(`Registered ${handlers.length} file action handlers for ${plugin.name}`);
|
||||
@@ -161,6 +159,20 @@ export class EditorPluginManager extends PluginManager {
|
||||
}
|
||||
}
|
||||
|
||||
if (plugin.registerFileActionHandlers && this.fileActionRegistry) {
|
||||
const handlers = plugin.registerFileActionHandlers();
|
||||
for (const handler of handlers) {
|
||||
this.fileActionRegistry.unregisterActionHandler(handler);
|
||||
}
|
||||
}
|
||||
|
||||
if (plugin.registerFileCreationTemplates && this.fileActionRegistry) {
|
||||
const templates = plugin.registerFileCreationTemplates();
|
||||
for (const template of templates) {
|
||||
this.fileActionRegistry.unregisterCreationTemplate(template);
|
||||
}
|
||||
}
|
||||
|
||||
this.serializerRegistry?.unregisterAll(name);
|
||||
|
||||
await super.uninstall(name);
|
||||
|
||||
25
packages/editor-core/src/Services/BaseCommand.ts
Normal file
25
packages/editor-core/src/Services/BaseCommand.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { ICommand } from './ICommand';
|
||||
|
||||
/**
|
||||
* 命令基类
|
||||
* 提供默认实现,具体命令继承此类
|
||||
*/
|
||||
export abstract class BaseCommand implements ICommand {
|
||||
abstract execute(): void;
|
||||
abstract undo(): void;
|
||||
abstract getDescription(): string;
|
||||
|
||||
/**
|
||||
* 默认不支持合并
|
||||
*/
|
||||
canMergeWith(_other: ICommand): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认抛出错误
|
||||
*/
|
||||
mergeWith(_other: ICommand): ICommand {
|
||||
throw new Error(`${this.constructor.name} 不支持合并操作`);
|
||||
}
|
||||
}
|
||||
203
packages/editor-core/src/Services/CommandManager.ts
Normal file
203
packages/editor-core/src/Services/CommandManager.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
import { ICommand } from './ICommand';
|
||||
|
||||
/**
|
||||
* 命令历史记录配置
|
||||
*/
|
||||
export interface CommandManagerConfig {
|
||||
/**
|
||||
* 最大历史记录数量
|
||||
*/
|
||||
maxHistorySize?: number;
|
||||
|
||||
/**
|
||||
* 是否自动合并相似命令
|
||||
*/
|
||||
autoMerge?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 命令管理器
|
||||
* 管理命令的执行、撤销、重做以及历史记录
|
||||
*/
|
||||
export class CommandManager {
|
||||
private undoStack: ICommand[] = [];
|
||||
private redoStack: ICommand[] = [];
|
||||
private readonly config: Required<CommandManagerConfig>;
|
||||
private isExecuting = false;
|
||||
|
||||
constructor(config: CommandManagerConfig = {}) {
|
||||
this.config = {
|
||||
maxHistorySize: config.maxHistorySize ?? 100,
|
||||
autoMerge: config.autoMerge ?? true
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行命令
|
||||
*/
|
||||
execute(command: ICommand): void {
|
||||
if (this.isExecuting) {
|
||||
throw new Error('不能在命令执行过程中执行新命令');
|
||||
}
|
||||
|
||||
this.isExecuting = true;
|
||||
|
||||
try {
|
||||
command.execute();
|
||||
|
||||
if (this.config.autoMerge && this.undoStack.length > 0) {
|
||||
const lastCommand = this.undoStack[this.undoStack.length - 1];
|
||||
if (lastCommand && lastCommand.canMergeWith(command)) {
|
||||
const mergedCommand = lastCommand.mergeWith(command);
|
||||
this.undoStack[this.undoStack.length - 1] = mergedCommand;
|
||||
this.redoStack = [];
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.undoStack.push(command);
|
||||
this.redoStack = [];
|
||||
|
||||
if (this.undoStack.length > this.config.maxHistorySize) {
|
||||
this.undoStack.shift();
|
||||
}
|
||||
} finally {
|
||||
this.isExecuting = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 撤销上一个命令
|
||||
*/
|
||||
undo(): void {
|
||||
if (this.isExecuting) {
|
||||
throw new Error('不能在命令执行过程中撤销');
|
||||
}
|
||||
|
||||
const command = this.undoStack.pop();
|
||||
if (!command) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isExecuting = true;
|
||||
|
||||
try {
|
||||
command.undo();
|
||||
this.redoStack.push(command);
|
||||
} catch (error) {
|
||||
this.undoStack.push(command);
|
||||
throw error;
|
||||
} finally {
|
||||
this.isExecuting = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重做上一个被撤销的命令
|
||||
*/
|
||||
redo(): void {
|
||||
if (this.isExecuting) {
|
||||
throw new Error('不能在命令执行过程中重做');
|
||||
}
|
||||
|
||||
const command = this.redoStack.pop();
|
||||
if (!command) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isExecuting = true;
|
||||
|
||||
try {
|
||||
command.execute();
|
||||
this.undoStack.push(command);
|
||||
} catch (error) {
|
||||
this.redoStack.push(command);
|
||||
throw error;
|
||||
} finally {
|
||||
this.isExecuting = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否可以撤销
|
||||
*/
|
||||
canUndo(): boolean {
|
||||
return this.undoStack.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否可以重做
|
||||
*/
|
||||
canRedo(): boolean {
|
||||
return this.redoStack.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取撤销栈的描述列表
|
||||
*/
|
||||
getUndoHistory(): string[] {
|
||||
return this.undoStack.map((cmd) => cmd.getDescription());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取重做栈的描述列表
|
||||
*/
|
||||
getRedoHistory(): string[] {
|
||||
return this.redoStack.map((cmd) => cmd.getDescription());
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有历史记录
|
||||
*/
|
||||
clear(): void {
|
||||
this.undoStack = [];
|
||||
this.redoStack = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量执行命令(作为单一操作,可以一次撤销)
|
||||
*/
|
||||
executeBatch(commands: ICommand[]): void {
|
||||
if (commands.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const batchCommand = new BatchCommand(commands);
|
||||
this.execute(batchCommand);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量命令
|
||||
* 将多个命令组合为一个命令
|
||||
*/
|
||||
class BatchCommand implements ICommand {
|
||||
constructor(private readonly commands: ICommand[]) {}
|
||||
|
||||
execute(): void {
|
||||
for (const command of this.commands) {
|
||||
command.execute();
|
||||
}
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
for (let i = this.commands.length - 1; i >= 0; i--) {
|
||||
const command = this.commands[i];
|
||||
if (command) {
|
||||
command.undo();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return `批量操作 (${this.commands.length} 个命令)`;
|
||||
}
|
||||
|
||||
canMergeWith(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
mergeWith(): ICommand {
|
||||
throw new Error('批量命令不支持合并');
|
||||
}
|
||||
}
|
||||
35
packages/editor-core/src/Services/CompilerRegistry.ts
Normal file
35
packages/editor-core/src/Services/CompilerRegistry.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { IService } from '@esengine/ecs-framework';
|
||||
import { ICompiler } from './ICompiler';
|
||||
|
||||
export class CompilerRegistry implements IService {
|
||||
private compilers: Map<string, ICompiler> = new Map();
|
||||
|
||||
register(compiler: ICompiler): void {
|
||||
if (this.compilers.has(compiler.id)) {
|
||||
console.warn(`Compiler with id "${compiler.id}" is already registered. Overwriting.`);
|
||||
}
|
||||
this.compilers.set(compiler.id, compiler);
|
||||
console.log(`[CompilerRegistry] Registered compiler: ${compiler.name} (${compiler.id})`);
|
||||
}
|
||||
|
||||
unregister(compilerId: string): void {
|
||||
this.compilers.delete(compilerId);
|
||||
console.log(`[CompilerRegistry] Unregistered compiler: ${compilerId}`);
|
||||
}
|
||||
|
||||
get(compilerId: string): ICompiler | undefined {
|
||||
return this.compilers.get(compilerId);
|
||||
}
|
||||
|
||||
getAll(): ICompiler[] {
|
||||
return Array.from(this.compilers.values());
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.compilers.clear();
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.clear();
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,24 @@ export class FileActionRegistry implements IService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注销文件操作处理器
|
||||
*/
|
||||
unregisterActionHandler(handler: FileActionHandler): void {
|
||||
for (const ext of handler.extensions) {
|
||||
const handlers = this.actionHandlers.get(ext);
|
||||
if (handlers) {
|
||||
const index = handlers.indexOf(handler);
|
||||
if (index !== -1) {
|
||||
handlers.splice(index, 1);
|
||||
}
|
||||
if (handlers.length === 0) {
|
||||
this.actionHandlers.delete(ext);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册文件创建模板
|
||||
*/
|
||||
@@ -28,6 +46,16 @@ export class FileActionRegistry implements IService {
|
||||
this.creationTemplates.push(template);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注销文件创建模板
|
||||
*/
|
||||
unregisterCreationTemplate(template: FileCreationTemplate): void {
|
||||
const index = this.creationTemplates.indexOf(template);
|
||||
if (index !== -1) {
|
||||
this.creationTemplates.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件扩展名的处理器
|
||||
*/
|
||||
|
||||
19
packages/editor-core/src/Services/ICommand.ts
Normal file
19
packages/editor-core/src/Services/ICommand.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
export interface ICommand {
|
||||
execute(): void | Promise<void>;
|
||||
undo(): void | Promise<void>;
|
||||
getDescription(): string;
|
||||
canMergeWith(other: ICommand): boolean;
|
||||
mergeWith(other: ICommand): ICommand;
|
||||
}
|
||||
|
||||
export interface ICommandManager {
|
||||
execute(command: ICommand): void;
|
||||
executeBatch(commands: ICommand[]): void;
|
||||
undo(): void;
|
||||
redo(): void;
|
||||
canUndo(): boolean;
|
||||
canRedo(): boolean;
|
||||
clear(): void;
|
||||
getUndoHistory(): string[];
|
||||
getRedoHistory(): string[];
|
||||
}
|
||||
33
packages/editor-core/src/Services/ICompiler.ts
Normal file
33
packages/editor-core/src/Services/ICompiler.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
export interface CompileResult {
|
||||
success: boolean;
|
||||
message: string;
|
||||
outputFiles?: string[];
|
||||
errors?: string[];
|
||||
}
|
||||
|
||||
import type { IFileSystem } from './IFileSystem';
|
||||
import type { IDialog } from './IDialog';
|
||||
import type { IService, ServiceType } from '@esengine/ecs-framework';
|
||||
|
||||
export interface CompilerModuleContext {
|
||||
fileSystem: IFileSystem;
|
||||
dialog: IDialog;
|
||||
}
|
||||
|
||||
export interface CompilerContext {
|
||||
projectPath: string | null;
|
||||
moduleContext: CompilerModuleContext;
|
||||
getService<T extends IService>(serviceClass: ServiceType<T>): T | undefined;
|
||||
}
|
||||
|
||||
export interface ICompiler<TOptions = unknown> {
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
readonly description: string;
|
||||
|
||||
compile(options: TOptions, context: CompilerContext): Promise<CompileResult>;
|
||||
|
||||
createConfigUI?(onOptionsChange: (options: TOptions) => void, context: CompilerContext): React.ReactElement;
|
||||
|
||||
validateOptions?(options: TOptions): string | null;
|
||||
}
|
||||
21
packages/editor-core/src/Services/IDialog.ts
Normal file
21
packages/editor-core/src/Services/IDialog.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
export interface DialogOptions {
|
||||
title?: string;
|
||||
defaultPath?: string;
|
||||
filters?: Array<{ name: string; extensions: string[] }>;
|
||||
}
|
||||
|
||||
export interface OpenDialogOptions extends DialogOptions {
|
||||
directory?: boolean;
|
||||
multiple?: boolean;
|
||||
}
|
||||
|
||||
export interface SaveDialogOptions extends DialogOptions {
|
||||
defaultFileName?: string;
|
||||
}
|
||||
|
||||
export interface IDialog {
|
||||
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>;
|
||||
}
|
||||
12
packages/editor-core/src/Services/IEditorDataStore.ts
Normal file
12
packages/editor-core/src/Services/IEditorDataStore.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export interface IEditorDataStore<TNode, TConnection> {
|
||||
getNodes(): TNode[];
|
||||
getConnections(): TConnection[];
|
||||
getNode(nodeId: string): TNode | undefined;
|
||||
getConnection(connectionId: string): TConnection | undefined;
|
||||
addNode(node: TNode): void;
|
||||
removeNode(nodeId: string): void;
|
||||
updateNode(nodeId: string, updates: Partial<TNode>): void;
|
||||
addConnection(connection: TConnection): void;
|
||||
removeConnection(connectionId: string): void;
|
||||
clear(): void;
|
||||
}
|
||||
19
packages/editor-core/src/Services/IFileSystem.ts
Normal file
19
packages/editor-core/src/Services/IFileSystem.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
export interface IFileSystem {
|
||||
readFile(path: string): Promise<string>;
|
||||
writeFile(path: string, content: string): Promise<void>;
|
||||
writeBinary(path: string, data: Uint8Array): Promise<void>;
|
||||
exists(path: string): Promise<boolean>;
|
||||
createDirectory(path: string): Promise<void>;
|
||||
listDirectory(path: string): Promise<FileEntry[]>;
|
||||
deleteFile(path: string): Promise<void>;
|
||||
deleteDirectory(path: string): Promise<void>;
|
||||
scanFiles(basePath: string, pattern: string): Promise<string[]>;
|
||||
}
|
||||
|
||||
export interface FileEntry {
|
||||
name: string;
|
||||
path: string;
|
||||
isDirectory: boolean;
|
||||
size?: number;
|
||||
modified?: Date;
|
||||
}
|
||||
77
packages/editor-core/src/Services/IInspectorProvider.ts
Normal file
77
packages/editor-core/src/Services/IInspectorProvider.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* Inspector提供器接口
|
||||
* 用于扩展Inspector面板,支持不同类型的对象检视
|
||||
*/
|
||||
export interface IInspectorProvider<T = unknown> {
|
||||
/**
|
||||
* 提供器唯一标识
|
||||
*/
|
||||
readonly id: string;
|
||||
|
||||
/**
|
||||
* 提供器名称
|
||||
*/
|
||||
readonly name: string;
|
||||
|
||||
/**
|
||||
* 优先级,数字越大优先级越高
|
||||
*/
|
||||
readonly priority?: number;
|
||||
|
||||
/**
|
||||
* 判断是否可以处理该目标
|
||||
*/
|
||||
canHandle(target: unknown): target is T;
|
||||
|
||||
/**
|
||||
* 渲染Inspector内容
|
||||
*/
|
||||
render(target: T, context: InspectorContext): React.ReactElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inspector上下文
|
||||
*/
|
||||
export interface InspectorContext {
|
||||
/**
|
||||
* 当前选中的目标
|
||||
*/
|
||||
target: unknown;
|
||||
|
||||
/**
|
||||
* 是否只读模式
|
||||
*/
|
||||
readonly?: boolean;
|
||||
|
||||
/**
|
||||
* 项目路径
|
||||
*/
|
||||
projectPath?: string | null;
|
||||
|
||||
/**
|
||||
* 额外的上下文数据
|
||||
*/
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inspector目标类型
|
||||
*/
|
||||
export interface InspectorTarget<T = unknown> {
|
||||
/**
|
||||
* 目标类型
|
||||
*/
|
||||
type: string;
|
||||
|
||||
/**
|
||||
* 目标数据
|
||||
*/
|
||||
data: T;
|
||||
|
||||
/**
|
||||
* 额外的元数据
|
||||
*/
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
6
packages/editor-core/src/Services/INotification.ts
Normal file
6
packages/editor-core/src/Services/INotification.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export type NotificationType = 'success' | 'error' | 'warning' | 'info';
|
||||
|
||||
export interface INotification {
|
||||
show(message: string, type?: NotificationType, duration?: number): void;
|
||||
hide(id: string): void;
|
||||
}
|
||||
77
packages/editor-core/src/Services/InspectorRegistry.ts
Normal file
77
packages/editor-core/src/Services/InspectorRegistry.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { IInspectorProvider, InspectorContext } from './IInspectorProvider';
|
||||
import { IService } from '@esengine/ecs-framework';
|
||||
import React from 'react';
|
||||
|
||||
export class InspectorRegistry implements IService {
|
||||
private providers: Map<string, IInspectorProvider> = new Map();
|
||||
|
||||
/**
|
||||
* 注册Inspector提供器
|
||||
*/
|
||||
register(provider: IInspectorProvider): void {
|
||||
if (this.providers.has(provider.id)) {
|
||||
console.warn(`Inspector provider with id "${provider.id}" is already registered`);
|
||||
return;
|
||||
}
|
||||
this.providers.set(provider.id, provider);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注销Inspector提供器
|
||||
*/
|
||||
unregister(providerId: string): void {
|
||||
this.providers.delete(providerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定ID的提供器
|
||||
*/
|
||||
getProvider(providerId: string): IInspectorProvider | undefined {
|
||||
return this.providers.get(providerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有提供器
|
||||
*/
|
||||
getAllProviders(): IInspectorProvider[] {
|
||||
return Array.from(this.providers.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找可以处理指定目标的提供器
|
||||
* 按优先级排序,返回第一个可以处理的提供器
|
||||
*/
|
||||
findProvider(target: unknown): IInspectorProvider | undefined {
|
||||
const providers = Array.from(this.providers.values())
|
||||
.sort((a, b) => (b.priority || 0) - (a.priority || 0));
|
||||
|
||||
for (const provider of providers) {
|
||||
if (provider.canHandle(target)) {
|
||||
return provider;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染Inspector内容
|
||||
* 自动查找合适的提供器并渲染
|
||||
*/
|
||||
render(target: unknown, context: InspectorContext): React.ReactElement | null {
|
||||
const provider = this.findProvider(target);
|
||||
if (!provider) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return provider.render(target, context);
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.providers.clear();
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.clear();
|
||||
}
|
||||
}
|
||||
@@ -22,8 +22,10 @@ export type LogListener = (entry: LogEntry) => void;
|
||||
export class LogService implements IService {
|
||||
private logs: LogEntry[] = [];
|
||||
private listeners: Set<LogListener> = new Set();
|
||||
private nextId = 0;
|
||||
private nextId = Date.now(); // 使用时间戳作为起始ID,避免重复
|
||||
private maxLogs = 1000;
|
||||
private pendingNotifications: LogEntry[] = [];
|
||||
private notificationScheduled = false;
|
||||
|
||||
private originalConsole = {
|
||||
log: console.log.bind(console),
|
||||
@@ -146,15 +148,29 @@ export class LogService implements IService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知监听器
|
||||
* 通知监听器(批处理日志通知以避免在React渲染期间触发状态更新)
|
||||
*/
|
||||
private notifyListeners(entry: LogEntry): void {
|
||||
for (const listener of this.listeners) {
|
||||
try {
|
||||
listener(entry);
|
||||
} catch (error) {
|
||||
this.originalConsole.error('Error in log listener:', error);
|
||||
}
|
||||
this.pendingNotifications.push(entry);
|
||||
|
||||
if (!this.notificationScheduled) {
|
||||
this.notificationScheduled = true;
|
||||
|
||||
queueMicrotask(() => {
|
||||
const notifications = [...this.pendingNotifications];
|
||||
this.pendingNotifications = [];
|
||||
this.notificationScheduled = false;
|
||||
|
||||
for (const notification of notifications) {
|
||||
for (const listener of this.listeners) {
|
||||
try {
|
||||
listener(notification);
|
||||
} catch (error) {
|
||||
this.originalConsole.error('Error in log listener:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,23 @@ export * from './Services/LogService';
|
||||
export * from './Services/SettingsRegistry';
|
||||
export * from './Services/SceneManagerService';
|
||||
export * from './Services/FileActionRegistry';
|
||||
export * from './Services/CompilerRegistry';
|
||||
export * from './Services/ICompiler';
|
||||
export * from './Services/ICommand';
|
||||
export * from './Services/BaseCommand';
|
||||
export * from './Services/CommandManager';
|
||||
export * from './Services/IEditorDataStore';
|
||||
export * from './Services/IFileSystem';
|
||||
export * from './Services/IDialog';
|
||||
export * from './Services/INotification';
|
||||
export * from './Services/IInspectorProvider';
|
||||
export * from './Services/InspectorRegistry';
|
||||
|
||||
export * from './Module/IEventBus';
|
||||
export * from './Module/ICommandRegistry';
|
||||
export * from './Module/IPanelRegistry';
|
||||
export * from './Module/IModuleContext';
|
||||
export * from './Module/IEditorModule';
|
||||
|
||||
export * from './Types/UITypes';
|
||||
export * from './Types/IFileAPI';
|
||||
|
||||
Reference in New Issue
Block a user