2025-10-27 09:29:11 +08:00
|
|
|
import type { Core, ServiceContainer } from '@esengine/ecs-framework';
|
|
|
|
|
import { IEditorPlugin, EditorPluginCategory, PanelPosition, MessageHub } from '@esengine/editor-core';
|
2025-11-04 18:29:28 +08:00
|
|
|
import type { MenuItem, ToolbarItem, PanelDescriptor, ISerializer, FileActionHandler, FileCreationTemplate, FileContextMenuItem } from '@esengine/editor-core';
|
2025-10-31 17:27:38 +08:00
|
|
|
import { BehaviorTreeData } from '@esengine/behavior-tree';
|
2025-11-04 18:29:28 +08:00
|
|
|
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';
|
2025-10-27 09:29:11 +08:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 行为树编辑器插件
|
|
|
|
|
*
|
|
|
|
|
* 提供行为树的可视化编辑功能
|
|
|
|
|
*/
|
|
|
|
|
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[] {
|
2025-11-04 18:29:28 +08:00
|
|
|
return [];
|
2025-10-27 09:29:11 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
registerToolbar(): ToolbarItem[] {
|
2025-11-04 18:29:28 +08:00
|
|
|
return [];
|
2025-10-27 09:29:11 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
registerPanels(): PanelDescriptor[] {
|
|
|
|
|
return [
|
|
|
|
|
{
|
2025-11-04 18:29:28 +08:00
|
|
|
id: 'behavior-tree-editor',
|
|
|
|
|
title: '行为树编辑器',
|
2025-10-27 09:29:11 +08:00
|
|
|
icon: 'Network',
|
2025-11-04 18:29:28 +08:00
|
|
|
component: BehaviorTreeEditorPanel,
|
|
|
|
|
position: PanelPosition.Center,
|
|
|
|
|
defaultSize: 400,
|
2025-10-27 09:29:11 +08:00
|
|
|
closable: true,
|
2025-11-04 18:29:28 +08:00
|
|
|
isDynamic: true
|
2025-10-27 09:29:11 +08:00
|
|
|
}
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getSerializers(): ISerializer[] {
|
|
|
|
|
return [
|
|
|
|
|
{
|
2025-10-31 17:27:38 +08:00
|
|
|
serialize: (data: BehaviorTreeData) => {
|
|
|
|
|
const json = this.serializeBehaviorTreeData(data);
|
|
|
|
|
const encoder = new TextEncoder();
|
|
|
|
|
return encoder.encode(json);
|
2025-10-27 09:29:11 +08:00
|
|
|
},
|
|
|
|
|
deserialize: (data: Uint8Array) => {
|
2025-10-31 17:27:38 +08:00
|
|
|
const decoder = new TextDecoder();
|
|
|
|
|
const json = decoder.decode(data);
|
|
|
|
|
return this.deserializeBehaviorTreeData(json);
|
2025-10-27 09:29:11 +08:00
|
|
|
},
|
|
|
|
|
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');
|
2025-10-31 17:27:38 +08:00
|
|
|
const isValid = this.validateBehaviorTreeData(data);
|
2025-10-27 09:29:11 +08:00
|
|
|
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}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-04 18:29:28 +08:00
|
|
|
registerFileActionHandlers(): FileActionHandler[] {
|
|
|
|
|
return [
|
|
|
|
|
{
|
|
|
|
|
extensions: ['btree'],
|
|
|
|
|
onDoubleClick: async (filePath: string) => {
|
|
|
|
|
console.log('[BehaviorTreePlugin] onDoubleClick called for:', filePath);
|
|
|
|
|
|
|
|
|
|
if (this.messageHub) {
|
|
|
|
|
useBehaviorTreeStore.getState().setIsOpen(true);
|
|
|
|
|
|
|
|
|
|
await this.messageHub.publish('dynamic-panel:open', {
|
|
|
|
|
panelId: 'behavior-tree-editor'
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await this.messageHub.publish('behavior-tree:open-file', {
|
|
|
|
|
filePath: filePath
|
|
|
|
|
});
|
|
|
|
|
console.log('[BehaviorTreePlugin] Panel opened and file loaded');
|
|
|
|
|
} else {
|
|
|
|
|
console.error('[BehaviorTreePlugin] MessageHub is not available!');
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
onOpen: async (filePath: string) => {
|
|
|
|
|
if (this.messageHub) {
|
|
|
|
|
useBehaviorTreeStore.getState().setIsOpen(true);
|
|
|
|
|
|
|
|
|
|
await this.messageHub.publish('dynamic-panel:open', {
|
|
|
|
|
panelId: 'behavior-tree-editor'
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await this.messageHub.publish('behavior-tree:open-file', {
|
|
|
|
|
filePath: filePath
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
getContextMenuItems: (filePath: string, parentPath: string): FileContextMenuItem[] => {
|
|
|
|
|
return [
|
|
|
|
|
{
|
|
|
|
|
label: '打开行为树编辑器',
|
|
|
|
|
icon: createElement(FileText, { size: 16 }),
|
|
|
|
|
onClick: async (filePath: string) => {
|
|
|
|
|
if (this.messageHub) {
|
|
|
|
|
useBehaviorTreeStore.getState().setIsOpen(true);
|
|
|
|
|
|
|
|
|
|
await this.messageHub.publish('dynamic-panel:open', {
|
|
|
|
|
panelId: 'behavior-tree-editor'
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await this.messageHub.publish('behavior-tree:open-file', {
|
|
|
|
|
filePath: filePath
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
];
|
2025-10-27 09:29:11 +08:00
|
|
|
}
|
|
|
|
|
|
2025-11-04 18:29:28 +08:00
|
|
|
registerFileCreationTemplates(): FileCreationTemplate[] {
|
|
|
|
|
return [
|
|
|
|
|
{
|
|
|
|
|
label: '行为树',
|
|
|
|
|
extension: 'btree',
|
|
|
|
|
defaultFileName: 'NewBehaviorTree',
|
|
|
|
|
icon: createElement(FileText, { size: 16 }),
|
|
|
|
|
createContent: async (fileName: string) => {
|
|
|
|
|
const emptyTree: BehaviorTreeData = {
|
|
|
|
|
id: `tree_${Date.now()}`,
|
|
|
|
|
name: fileName,
|
|
|
|
|
rootNodeId: '',
|
|
|
|
|
nodes: new Map(),
|
|
|
|
|
blackboardVariables: new Map()
|
|
|
|
|
};
|
|
|
|
|
return this.serializeBehaviorTreeData(emptyTree);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
];
|
2025-10-31 17:27:38 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
2025-10-27 09:29:11 +08:00
|
|
|
}
|
|
|
|
|
}
|