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:
@@ -1,293 +0,0 @@
|
||||
import type { Core, ServiceContainer } from '@esengine/ecs-framework';
|
||||
import { IEditorPlugin, EditorPluginCategory, PanelPosition, MessageHub } from '@esengine/editor-core';
|
||||
import type {
|
||||
MenuItem,
|
||||
ToolbarItem,
|
||||
PanelDescriptor,
|
||||
ISerializer,
|
||||
FileActionHandler,
|
||||
FileCreationTemplate,
|
||||
FileContextMenuItem
|
||||
} from '@esengine/editor-core';
|
||||
import { BehaviorTreeData } from '@esengine/behavior-tree';
|
||||
import { BehaviorTreeEditorPanel } from '../presentation/components/behavior-tree/panels';
|
||||
import { FileText } from 'lucide-react';
|
||||
import { TauriAPI } from '../api/tauri';
|
||||
import { createElement } from 'react';
|
||||
import { useBehaviorTreeStore } from '../stores/behaviorTreeStore';
|
||||
import { createRootNode } from '../domain/constants/RootNode';
|
||||
import { behaviorTreeFileService } from '../services/BehaviorTreeFileService';
|
||||
|
||||
/**
|
||||
* 行为树编辑器插件
|
||||
*
|
||||
* 提供行为树的可视化编辑功能
|
||||
*/
|
||||
export class BehaviorTreePlugin implements IEditorPlugin {
|
||||
readonly name = '@esengine/behavior-tree-editor';
|
||||
readonly version = '1.0.0';
|
||||
readonly displayName = 'Behavior Tree Editor';
|
||||
readonly category = EditorPluginCategory.Tool;
|
||||
readonly description = 'Visual behavior tree editor for AI development';
|
||||
readonly icon = 'Network';
|
||||
|
||||
private core?: Core;
|
||||
private services?: ServiceContainer;
|
||||
private messageHub?: MessageHub;
|
||||
|
||||
async install(core: Core, services: ServiceContainer): Promise<void> {
|
||||
this.core = core;
|
||||
this.services = services;
|
||||
this.messageHub = services.resolve(MessageHub);
|
||||
}
|
||||
|
||||
async uninstall(): Promise<void> {
|
||||
this.core = undefined;
|
||||
this.services = undefined;
|
||||
}
|
||||
|
||||
registerMenuItems(): MenuItem[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
registerToolbar(): ToolbarItem[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
registerPanels(): PanelDescriptor[] {
|
||||
return [
|
||||
{
|
||||
id: 'behavior-tree-editor',
|
||||
title: '行为树编辑器',
|
||||
icon: 'Network',
|
||||
component: BehaviorTreeEditorPanel,
|
||||
position: PanelPosition.Center,
|
||||
defaultSize: 400,
|
||||
closable: true,
|
||||
isDynamic: true
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
getSerializers(): ISerializer[] {
|
||||
return [
|
||||
{
|
||||
serialize: (data: BehaviorTreeData) => {
|
||||
const json = this.serializeBehaviorTreeData(data);
|
||||
const encoder = new TextEncoder();
|
||||
return encoder.encode(json);
|
||||
},
|
||||
deserialize: (data: Uint8Array) => {
|
||||
const decoder = new TextDecoder();
|
||||
const json = decoder.decode(data);
|
||||
return this.deserializeBehaviorTreeData(json);
|
||||
},
|
||||
getSupportedType: () => 'behavior-tree'
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
async onEditorReady(): Promise<void> {
|
||||
console.log('[BehaviorTreePlugin] Editor is ready');
|
||||
}
|
||||
|
||||
async onProjectOpen(projectPath: string): Promise<void> {
|
||||
console.log(`[BehaviorTreePlugin] Project opened: ${projectPath}`);
|
||||
}
|
||||
|
||||
async onProjectClose(): Promise<void> {
|
||||
console.log('[BehaviorTreePlugin] Project closed');
|
||||
}
|
||||
|
||||
async onBeforeSave(filePath: string, data: any): Promise<void> {
|
||||
if (filePath.endsWith('.behavior-tree.json')) {
|
||||
console.log('[BehaviorTreePlugin] Validating behavior tree before save');
|
||||
const isValid = this.validateBehaviorTreeData(data);
|
||||
if (!isValid) {
|
||||
throw new Error('Invalid behavior tree data');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async onAfterSave(filePath: string): Promise<void> {
|
||||
if (filePath.endsWith('.behavior-tree.json')) {
|
||||
console.log(`[BehaviorTreePlugin] Behavior tree saved: ${filePath}`);
|
||||
}
|
||||
}
|
||||
|
||||
registerFileActionHandlers(): FileActionHandler[] {
|
||||
return [
|
||||
{
|
||||
extensions: ['btree'],
|
||||
onDoubleClick: async (filePath: string) => {
|
||||
console.log('[BehaviorTreePlugin] onDoubleClick called for:', filePath);
|
||||
|
||||
if (this.messageHub) {
|
||||
const store = useBehaviorTreeStore.getState();
|
||||
store.setIsOpen(true);
|
||||
store.setPendingFilePath(filePath); // 状态通道(同步,时序安全)
|
||||
|
||||
// 提取文件名
|
||||
const fileName = filePath.split(/[\\/]/).pop()?.replace('.btree', '') || '行为树';
|
||||
|
||||
await this.messageHub.publish('dynamic-panel:open', {
|
||||
panelId: 'behavior-tree-editor',
|
||||
title: fileName
|
||||
});
|
||||
|
||||
// 消息通道(异步,用于其他监听者)
|
||||
await this.messageHub.publish('behavior-tree:load-file', {
|
||||
filePath
|
||||
});
|
||||
} else {
|
||||
console.error('[BehaviorTreePlugin] MessageHub is not available!');
|
||||
}
|
||||
},
|
||||
onOpen: async (filePath: string) => {
|
||||
if (this.messageHub) {
|
||||
const store = useBehaviorTreeStore.getState();
|
||||
store.setIsOpen(true);
|
||||
store.setPendingFilePath(filePath); // 状态通道(同步,时序安全)
|
||||
|
||||
// 提取文件名
|
||||
const fileName = filePath.split(/[\\/]/).pop()?.replace('.btree', '') || '行为树';
|
||||
|
||||
await this.messageHub.publish('dynamic-panel:open', {
|
||||
panelId: 'behavior-tree-editor',
|
||||
title: fileName
|
||||
});
|
||||
|
||||
// 消息通道(异步,用于其他监听者)
|
||||
await this.messageHub.publish('behavior-tree:load-file', {
|
||||
filePath
|
||||
});
|
||||
}
|
||||
},
|
||||
getContextMenuItems: (filePath: string, parentPath: string): FileContextMenuItem[] => {
|
||||
return [
|
||||
{
|
||||
label: '打开行为树编辑器',
|
||||
icon: createElement(FileText, { size: 16 }),
|
||||
onClick: async (filePath: string) => {
|
||||
if (this.messageHub) {
|
||||
const store = useBehaviorTreeStore.getState();
|
||||
store.setIsOpen(true);
|
||||
store.setPendingFilePath(filePath); // 状态通道(同步,时序安全)
|
||||
|
||||
// 提取文件名
|
||||
const fileName = filePath.split(/[\\/]/).pop()?.replace('.btree', '') || '行为树';
|
||||
|
||||
await this.messageHub.publish('dynamic-panel:open', {
|
||||
panelId: 'behavior-tree-editor',
|
||||
title: fileName
|
||||
});
|
||||
|
||||
// 消息通道(异步,用于其他监听者)
|
||||
await this.messageHub.publish('behavior-tree:load-file', {
|
||||
filePath
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
registerFileCreationTemplates(): FileCreationTemplate[] {
|
||||
return [
|
||||
{
|
||||
label: '行为树',
|
||||
extension: 'btree',
|
||||
defaultFileName: 'NewBehaviorTree',
|
||||
icon: createElement(FileText, { size: 16 }),
|
||||
createContent: async (fileName: string) => {
|
||||
const rootNode = createRootNode();
|
||||
const now = new Date().toISOString();
|
||||
const editorFormat = {
|
||||
version: '1.0.0',
|
||||
metadata: {
|
||||
name: fileName,
|
||||
description: '',
|
||||
createdAt: now,
|
||||
modifiedAt: now
|
||||
},
|
||||
nodes: [rootNode.toObject()],
|
||||
connections: [],
|
||||
blackboard: {},
|
||||
canvasState: {
|
||||
offset: { x: 0, y: 0 },
|
||||
scale: 1
|
||||
}
|
||||
};
|
||||
return JSON.stringify(editorFormat, null, 2);
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
private serializeBehaviorTreeData(treeData: BehaviorTreeData): string {
|
||||
const serializable = {
|
||||
id: treeData.id,
|
||||
name: treeData.name,
|
||||
rootNodeId: treeData.rootNodeId,
|
||||
nodes: Array.from(treeData.nodes.entries()).map(([, node]) => ({
|
||||
...node
|
||||
})),
|
||||
blackboardVariables: treeData.blackboardVariables
|
||||
? Array.from(treeData.blackboardVariables.entries()).map(([key, value]) => ({
|
||||
key,
|
||||
value
|
||||
}))
|
||||
: []
|
||||
};
|
||||
return JSON.stringify(serializable, null, 2);
|
||||
}
|
||||
|
||||
private deserializeBehaviorTreeData(json: string): BehaviorTreeData {
|
||||
const parsed = JSON.parse(json);
|
||||
const treeData: BehaviorTreeData = {
|
||||
id: parsed.id,
|
||||
name: parsed.name,
|
||||
rootNodeId: parsed.rootNodeId,
|
||||
nodes: new Map(),
|
||||
blackboardVariables: new Map()
|
||||
};
|
||||
|
||||
if (parsed.nodes) {
|
||||
for (const node of parsed.nodes) {
|
||||
treeData.nodes.set(node.id, node);
|
||||
}
|
||||
}
|
||||
|
||||
if (parsed.blackboardVariables) {
|
||||
for (const variable of parsed.blackboardVariables) {
|
||||
treeData.blackboardVariables!.set(variable.key, variable.value);
|
||||
}
|
||||
}
|
||||
|
||||
return treeData;
|
||||
}
|
||||
|
||||
private validateBehaviorTreeData(data: any): boolean {
|
||||
if (!data || typeof data !== 'object') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!data.id || !data.name || !data.rootNodeId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!data.nodes || !Array.isArray(data.nodes)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const rootNode = data.nodes.find((n: any) => n.id === data.rootNodeId);
|
||||
if (!rootNode) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,23 @@ export class EditorAppearancePlugin implements IEditorPlugin {
|
||||
step: 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'inspector',
|
||||
title: '检视器设置',
|
||||
description: '配置属性检视器显示',
|
||||
settings: [
|
||||
{
|
||||
key: 'inspector.decimalPlaces',
|
||||
label: '数字小数位数',
|
||||
type: 'number',
|
||||
defaultValue: 4,
|
||||
description: '数字类型属性显示的小数位数,设置为 -1 表示不限制',
|
||||
min: -1,
|
||||
max: 10,
|
||||
step: 1
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user