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,17 +0,0 @@
|
||||
import { BehaviorTree } from '../../domain/models/BehaviorTree';
|
||||
|
||||
/**
|
||||
* 行为树状态接口
|
||||
* 命令通过此接口操作状态
|
||||
*/
|
||||
export interface ITreeState {
|
||||
/**
|
||||
* 获取当前行为树
|
||||
*/
|
||||
getTree(): BehaviorTree;
|
||||
|
||||
/**
|
||||
* 设置行为树
|
||||
*/
|
||||
setTree(tree: BehaviorTree): void;
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
export type { ICommand } from './ICommand';
|
||||
export { BaseCommand } from './BaseCommand';
|
||||
export { CommandManager } from './CommandManager';
|
||||
export type { ITreeState } from './ITreeState';
|
||||
export * from './tree';
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import { Connection } from '../../../domain/models/Connection';
|
||||
import { BaseCommand } from '../BaseCommand';
|
||||
import { ITreeState } from '../ITreeState';
|
||||
|
||||
/**
|
||||
* 添加连接命令
|
||||
*/
|
||||
export class AddConnectionCommand extends BaseCommand {
|
||||
constructor(
|
||||
private readonly state: ITreeState,
|
||||
private readonly connection: Connection
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
execute(): void {
|
||||
const tree = this.state.getTree();
|
||||
const newTree = tree.addConnection(this.connection);
|
||||
this.state.setTree(newTree);
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
const tree = this.state.getTree();
|
||||
const newTree = tree.removeConnection(
|
||||
this.connection.from,
|
||||
this.connection.to,
|
||||
this.connection.fromProperty,
|
||||
this.connection.toProperty
|
||||
);
|
||||
this.state.setTree(newTree);
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return `添加连接: ${this.connection.from} -> ${this.connection.to}`;
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import { Node } from '../../../domain/models/Node';
|
||||
import { BaseCommand } from '../BaseCommand';
|
||||
import { ITreeState } from '../ITreeState';
|
||||
|
||||
/**
|
||||
* 创建节点命令
|
||||
*/
|
||||
export class CreateNodeCommand extends BaseCommand {
|
||||
private createdNodeId: string;
|
||||
|
||||
constructor(
|
||||
private readonly state: ITreeState,
|
||||
private readonly node: Node
|
||||
) {
|
||||
super();
|
||||
this.createdNodeId = node.id;
|
||||
}
|
||||
|
||||
execute(): void {
|
||||
const tree = this.state.getTree();
|
||||
const newTree = tree.addNode(this.node);
|
||||
this.state.setTree(newTree);
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
const tree = this.state.getTree();
|
||||
const newTree = tree.removeNode(this.createdNodeId);
|
||||
this.state.setTree(newTree);
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return `创建节点: ${this.node.template.displayName}`;
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import { Node } from '../../../domain/models/Node';
|
||||
import { BaseCommand } from '../BaseCommand';
|
||||
import { ITreeState } from '../ITreeState';
|
||||
|
||||
/**
|
||||
* 删除节点命令
|
||||
*/
|
||||
export class DeleteNodeCommand extends BaseCommand {
|
||||
private deletedNode: Node | null = null;
|
||||
|
||||
constructor(
|
||||
private readonly state: ITreeState,
|
||||
private readonly nodeId: string
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
execute(): void {
|
||||
const tree = this.state.getTree();
|
||||
this.deletedNode = tree.getNode(this.nodeId);
|
||||
const newTree = tree.removeNode(this.nodeId);
|
||||
this.state.setTree(newTree);
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
if (!this.deletedNode) {
|
||||
throw new Error('无法撤销:未保存已删除的节点');
|
||||
}
|
||||
|
||||
const tree = this.state.getTree();
|
||||
const newTree = tree.addNode(this.deletedNode);
|
||||
this.state.setTree(newTree);
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return `删除节点: ${this.deletedNode?.template.displayName ?? this.nodeId}`;
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
import { Position } from '../../../domain/value-objects/Position';
|
||||
import { BaseCommand } from '../BaseCommand';
|
||||
import { ITreeState } from '../ITreeState';
|
||||
import { ICommand } from '../ICommand';
|
||||
|
||||
/**
|
||||
* 移动节点命令
|
||||
* 支持合并连续的移动操作
|
||||
*/
|
||||
export class MoveNodeCommand extends BaseCommand {
|
||||
private oldPosition: Position;
|
||||
|
||||
constructor(
|
||||
private readonly state: ITreeState,
|
||||
private readonly nodeId: string,
|
||||
private readonly newPosition: Position
|
||||
) {
|
||||
super();
|
||||
const tree = this.state.getTree();
|
||||
const node = tree.getNode(nodeId);
|
||||
this.oldPosition = node.position;
|
||||
}
|
||||
|
||||
execute(): void {
|
||||
const tree = this.state.getTree();
|
||||
const newTree = tree.updateNode(this.nodeId, (node) =>
|
||||
node.moveToPosition(this.newPosition)
|
||||
);
|
||||
this.state.setTree(newTree);
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
const tree = this.state.getTree();
|
||||
const newTree = tree.updateNode(this.nodeId, (node) =>
|
||||
node.moveToPosition(this.oldPosition)
|
||||
);
|
||||
this.state.setTree(newTree);
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return `移动节点: ${this.nodeId}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移动命令可以合并
|
||||
*/
|
||||
canMergeWith(other: ICommand): boolean {
|
||||
if (!(other instanceof MoveNodeCommand)) {
|
||||
return false;
|
||||
}
|
||||
return this.nodeId === other.nodeId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并移动命令
|
||||
* 保留初始位置,更新最终位置
|
||||
*/
|
||||
mergeWith(other: ICommand): ICommand {
|
||||
if (!(other instanceof MoveNodeCommand)) {
|
||||
throw new Error('只能与 MoveNodeCommand 合并');
|
||||
}
|
||||
|
||||
if (this.nodeId !== other.nodeId) {
|
||||
throw new Error('只能合并同一节点的移动命令');
|
||||
}
|
||||
|
||||
const merged = new MoveNodeCommand(
|
||||
this.state,
|
||||
this.nodeId,
|
||||
other.newPosition
|
||||
);
|
||||
merged.oldPosition = this.oldPosition;
|
||||
return merged;
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
import { Connection } from '../../../domain/models/Connection';
|
||||
import { BaseCommand } from '../BaseCommand';
|
||||
import { ITreeState } from '../ITreeState';
|
||||
|
||||
/**
|
||||
* 移除连接命令
|
||||
*/
|
||||
export class RemoveConnectionCommand extends BaseCommand {
|
||||
private removedConnection: Connection | null = null;
|
||||
|
||||
constructor(
|
||||
private readonly state: ITreeState,
|
||||
private readonly from: string,
|
||||
private readonly to: string,
|
||||
private readonly fromProperty?: string,
|
||||
private readonly toProperty?: string
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
execute(): void {
|
||||
const tree = this.state.getTree();
|
||||
|
||||
const connection = tree.connections.find((c) =>
|
||||
c.matches(this.from, this.to, this.fromProperty, this.toProperty)
|
||||
);
|
||||
|
||||
if (!connection) {
|
||||
throw new Error(`连接不存在: ${this.from} -> ${this.to}`);
|
||||
}
|
||||
|
||||
this.removedConnection = connection;
|
||||
const newTree = tree.removeConnection(this.from, this.to, this.fromProperty, this.toProperty);
|
||||
this.state.setTree(newTree);
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
if (!this.removedConnection) {
|
||||
throw new Error('无法撤销:未保存已删除的连接');
|
||||
}
|
||||
|
||||
const tree = this.state.getTree();
|
||||
const newTree = tree.addConnection(this.removedConnection);
|
||||
this.state.setTree(newTree);
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return `移除连接: ${this.from} -> ${this.to}`;
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import { BaseCommand } from '../BaseCommand';
|
||||
import { ITreeState } from '../ITreeState';
|
||||
|
||||
/**
|
||||
* 更新节点数据命令
|
||||
*/
|
||||
export class UpdateNodeDataCommand extends BaseCommand {
|
||||
private oldData: Record<string, unknown>;
|
||||
|
||||
constructor(
|
||||
private readonly state: ITreeState,
|
||||
private readonly nodeId: string,
|
||||
private readonly newData: Record<string, unknown>
|
||||
) {
|
||||
super();
|
||||
const tree = this.state.getTree();
|
||||
const node = tree.getNode(nodeId);
|
||||
this.oldData = node.data;
|
||||
}
|
||||
|
||||
execute(): void {
|
||||
const tree = this.state.getTree();
|
||||
const newTree = tree.updateNode(this.nodeId, (node) =>
|
||||
node.updateData(this.newData)
|
||||
);
|
||||
this.state.setTree(newTree);
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
const tree = this.state.getTree();
|
||||
const newTree = tree.updateNode(this.nodeId, (node) =>
|
||||
node.updateData(this.oldData)
|
||||
);
|
||||
this.state.setTree(newTree);
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return `更新节点数据: ${this.nodeId}`;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
export { CreateNodeCommand } from './CreateNodeCommand';
|
||||
export { DeleteNodeCommand } from './DeleteNodeCommand';
|
||||
export { AddConnectionCommand } from './AddConnectionCommand';
|
||||
export { RemoveConnectionCommand } from './RemoveConnectionCommand';
|
||||
export { MoveNodeCommand } from './MoveNodeCommand';
|
||||
export { UpdateNodeDataCommand } from './UpdateNodeDataCommand';
|
||||
@@ -1,55 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { BehaviorTreeNode, ROOT_NODE_ID } from '../../stores/behaviorTreeStore';
|
||||
|
||||
interface ContextMenuState {
|
||||
visible: boolean;
|
||||
position: { x: number; y: number };
|
||||
nodeId: string | null;
|
||||
}
|
||||
|
||||
export function useContextMenu() {
|
||||
const [contextMenu, setContextMenu] = useState<ContextMenuState>({
|
||||
visible: false,
|
||||
position: { x: 0, y: 0 },
|
||||
nodeId: null
|
||||
});
|
||||
|
||||
const handleNodeContextMenu = (e: React.MouseEvent, node: BehaviorTreeNode) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// 不允许对Root节点右键
|
||||
if (node.id === ROOT_NODE_ID) {
|
||||
return;
|
||||
}
|
||||
|
||||
setContextMenu({
|
||||
visible: true,
|
||||
position: { x: e.clientX, y: e.clientY },
|
||||
nodeId: node.id
|
||||
});
|
||||
};
|
||||
|
||||
const handleCanvasContextMenu = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
setContextMenu({
|
||||
visible: true,
|
||||
position: { x: e.clientX, y: e.clientY },
|
||||
nodeId: null
|
||||
});
|
||||
};
|
||||
|
||||
const closeContextMenu = () => {
|
||||
setContextMenu({ ...contextMenu, visible: false });
|
||||
};
|
||||
|
||||
return {
|
||||
contextMenu,
|
||||
setContextMenu,
|
||||
handleNodeContextMenu,
|
||||
handleCanvasContextMenu,
|
||||
closeContextMenu
|
||||
};
|
||||
}
|
||||
@@ -1,208 +0,0 @@
|
||||
import { useState, RefObject } from 'react';
|
||||
import { NodeTemplate } from '@esengine/behavior-tree';
|
||||
import { BehaviorTreeNode, Connection } from '../../stores/behaviorTreeStore';
|
||||
import { Node } from '../../domain/models/Node';
|
||||
import { Position } from '../../domain/value-objects/Position';
|
||||
import { useNodeOperations } from '../../presentation/hooks/useNodeOperations';
|
||||
import { useConnectionOperations } from '../../presentation/hooks/useConnectionOperations';
|
||||
|
||||
interface QuickCreateMenuState {
|
||||
visible: boolean;
|
||||
position: { x: number; y: number };
|
||||
searchText: string;
|
||||
selectedIndex: number;
|
||||
mode: 'create' | 'replace';
|
||||
replaceNodeId: string | null;
|
||||
}
|
||||
|
||||
type ExecutionMode = 'idle' | 'running' | 'paused' | 'step';
|
||||
|
||||
interface UseQuickCreateMenuParams {
|
||||
nodeOperations: ReturnType<typeof useNodeOperations>;
|
||||
connectionOperations: ReturnType<typeof useConnectionOperations>;
|
||||
canvasRef: RefObject<HTMLDivElement>;
|
||||
canvasOffset: { x: number; y: number };
|
||||
canvasScale: number;
|
||||
connectingFrom: string | null;
|
||||
connectingFromProperty: string | null;
|
||||
clearConnecting: () => void;
|
||||
nodes: BehaviorTreeNode[];
|
||||
setNodes: (nodes: BehaviorTreeNode[]) => void;
|
||||
connections: Connection[];
|
||||
executionMode: ExecutionMode;
|
||||
onStop: () => void;
|
||||
onNodeCreate?: (template: NodeTemplate, position: { x: number; y: number }) => void;
|
||||
showToast?: (message: string, type: 'success' | 'error' | 'info') => void;
|
||||
}
|
||||
|
||||
export function useQuickCreateMenu(params: UseQuickCreateMenuParams) {
|
||||
const {
|
||||
nodeOperations,
|
||||
connectionOperations,
|
||||
canvasRef,
|
||||
canvasOffset,
|
||||
canvasScale,
|
||||
connectingFrom,
|
||||
connectingFromProperty,
|
||||
clearConnecting,
|
||||
nodes,
|
||||
setNodes,
|
||||
connections,
|
||||
executionMode,
|
||||
onStop,
|
||||
onNodeCreate,
|
||||
showToast
|
||||
} = params;
|
||||
|
||||
const [quickCreateMenu, setQuickCreateMenu] = useState<QuickCreateMenuState>({
|
||||
visible: false,
|
||||
position: { x: 0, y: 0 },
|
||||
searchText: '',
|
||||
selectedIndex: 0,
|
||||
mode: 'create',
|
||||
replaceNodeId: null
|
||||
});
|
||||
|
||||
const handleReplaceNode = (newTemplate: NodeTemplate) => {
|
||||
const nodeToReplace = nodes.find((n) => n.id === quickCreateMenu.replaceNodeId);
|
||||
if (!nodeToReplace) return;
|
||||
|
||||
// 如果行为树正在执行,先停止
|
||||
if (executionMode !== 'idle') {
|
||||
onStop();
|
||||
}
|
||||
|
||||
// 合并数据:新模板的默认配置 + 保留旧节点中同名属性的值
|
||||
const newData = { ...newTemplate.defaultConfig };
|
||||
|
||||
// 获取新模板的属性名列表
|
||||
const newPropertyNames = new Set(newTemplate.properties.map((p) => p.name));
|
||||
|
||||
// 遍历旧节点的 data,保留新模板中也存在的属性
|
||||
for (const [key, value] of Object.entries(nodeToReplace.data)) {
|
||||
// 跳过节点类型相关的字段
|
||||
if (key === 'nodeType' || key === 'compositeType' || key === 'decoratorType' ||
|
||||
key === 'actionType' || key === 'conditionType') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 如果新模板也有这个属性,保留旧值(包括绑定信息)
|
||||
if (newPropertyNames.has(key)) {
|
||||
newData[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
// 创建新节点,保留原节点的位置和连接
|
||||
const newNode = new Node(
|
||||
nodeToReplace.id,
|
||||
newTemplate,
|
||||
newData,
|
||||
nodeToReplace.position,
|
||||
Array.from(nodeToReplace.children)
|
||||
);
|
||||
|
||||
// 替换节点
|
||||
setNodes(nodes.map((n) => n.id === newNode.id ? newNode : n));
|
||||
|
||||
// 删除所有指向该节点的属性连接,让用户重新连接
|
||||
const propertyConnections = connections.filter((conn) =>
|
||||
conn.connectionType === 'property' && conn.to === newNode.id
|
||||
);
|
||||
propertyConnections.forEach((conn) => {
|
||||
connectionOperations.removeConnection(
|
||||
conn.from,
|
||||
conn.to,
|
||||
conn.fromProperty,
|
||||
conn.toProperty
|
||||
);
|
||||
});
|
||||
|
||||
// 关闭快速创建菜单
|
||||
closeQuickCreateMenu();
|
||||
|
||||
// 显示提示
|
||||
showToast?.(`已将节点替换为 ${newTemplate.displayName}`, 'success');
|
||||
};
|
||||
|
||||
const handleQuickCreateNode = (template: NodeTemplate) => {
|
||||
// 如果是替换模式,直接调用替换函数
|
||||
if (quickCreateMenu.mode === 'replace') {
|
||||
handleReplaceNode(template);
|
||||
return;
|
||||
}
|
||||
|
||||
const rect = canvasRef.current?.getBoundingClientRect();
|
||||
if (!rect) {
|
||||
return;
|
||||
}
|
||||
|
||||
const posX = (quickCreateMenu.position.x - rect.left - canvasOffset.x) / canvasScale;
|
||||
const posY = (quickCreateMenu.position.y - rect.top - canvasOffset.y) / canvasScale;
|
||||
|
||||
const newNode = nodeOperations.createNode(
|
||||
template,
|
||||
new Position(posX, posY),
|
||||
template.defaultConfig
|
||||
);
|
||||
|
||||
// 如果有连接源,创建连接
|
||||
if (connectingFrom) {
|
||||
const fromNode = nodes.find((n: BehaviorTreeNode) => n.id === connectingFrom);
|
||||
if (fromNode) {
|
||||
if (connectingFromProperty) {
|
||||
// 属性连接
|
||||
connectionOperations.addConnection(
|
||||
connectingFrom,
|
||||
newNode.id,
|
||||
'property',
|
||||
connectingFromProperty,
|
||||
undefined
|
||||
);
|
||||
} else {
|
||||
// 节点连接
|
||||
connectionOperations.addConnection(connectingFrom, newNode.id, 'node');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
closeQuickCreateMenu();
|
||||
|
||||
onNodeCreate?.(template, { x: posX, y: posY });
|
||||
};
|
||||
|
||||
const openQuickCreateMenu = (
|
||||
position: { x: number; y: number },
|
||||
mode: 'create' | 'replace',
|
||||
replaceNodeId?: string | null
|
||||
) => {
|
||||
setQuickCreateMenu({
|
||||
visible: true,
|
||||
position,
|
||||
searchText: '',
|
||||
selectedIndex: 0,
|
||||
mode,
|
||||
replaceNodeId: replaceNodeId || null
|
||||
});
|
||||
};
|
||||
|
||||
const closeQuickCreateMenu = () => {
|
||||
setQuickCreateMenu({
|
||||
visible: false,
|
||||
position: { x: 0, y: 0 },
|
||||
searchText: '',
|
||||
selectedIndex: 0,
|
||||
mode: 'create',
|
||||
replaceNodeId: null
|
||||
});
|
||||
clearConnecting();
|
||||
};
|
||||
|
||||
return {
|
||||
quickCreateMenu,
|
||||
setQuickCreateMenu,
|
||||
handleQuickCreateNode,
|
||||
handleReplaceNode,
|
||||
openQuickCreateMenu,
|
||||
closeQuickCreateMenu
|
||||
};
|
||||
}
|
||||
@@ -1,3 +1,2 @@
|
||||
export * from './commands';
|
||||
export * from './use-cases';
|
||||
export * from './state';
|
||||
|
||||
@@ -1,250 +0,0 @@
|
||||
import { NodeTemplate } from '@esengine/behavior-tree';
|
||||
import { BehaviorTreeNode } from '../../stores/behaviorTreeStore';
|
||||
import { LucideIcon } from 'lucide-react';
|
||||
import React from 'react';
|
||||
|
||||
export interface INodeRenderer {
|
||||
canRender(node: BehaviorTreeNode): boolean;
|
||||
|
||||
render(node: BehaviorTreeNode, context: NodeRenderContext): React.ReactElement;
|
||||
}
|
||||
|
||||
export interface NodeRenderContext {
|
||||
isSelected: boolean;
|
||||
isExecuting: boolean;
|
||||
onNodeClick: (e: React.MouseEvent, node: BehaviorTreeNode) => void;
|
||||
onContextMenu: (e: React.MouseEvent, node: BehaviorTreeNode) => void;
|
||||
}
|
||||
|
||||
export interface IPropertyEditor {
|
||||
canEdit(propertyType: string): boolean;
|
||||
|
||||
render(property: PropertyEditorProps): React.ReactElement;
|
||||
}
|
||||
|
||||
export type PropertyValue = string | number | boolean | object | null | undefined;
|
||||
|
||||
export interface PropertyEditorProps<T = PropertyValue> {
|
||||
propertyName: string;
|
||||
propertyType: string;
|
||||
value: T;
|
||||
onChange: (value: T) => void;
|
||||
config?: Record<string, PropertyValue>;
|
||||
}
|
||||
|
||||
export interface INodeProvider {
|
||||
getNodeTemplates(): NodeTemplate[];
|
||||
|
||||
getCategory(): string;
|
||||
|
||||
getIcon(): string | LucideIcon;
|
||||
}
|
||||
|
||||
export interface IToolbarButton {
|
||||
id: string;
|
||||
label: string;
|
||||
icon: LucideIcon;
|
||||
tooltip?: string;
|
||||
onClick: () => void;
|
||||
isVisible?: () => boolean;
|
||||
isEnabled?: () => boolean;
|
||||
}
|
||||
|
||||
export interface IPanelProvider {
|
||||
id: string;
|
||||
title: string;
|
||||
icon?: LucideIcon;
|
||||
|
||||
render(): React.ReactElement;
|
||||
|
||||
canActivate?(): boolean;
|
||||
}
|
||||
|
||||
export interface IValidator {
|
||||
name: string;
|
||||
|
||||
validate(nodes: BehaviorTreeNode[]): ValidationResult[];
|
||||
}
|
||||
|
||||
export interface ValidationResult {
|
||||
severity: 'error' | 'warning' | 'info';
|
||||
nodeId?: string;
|
||||
message: string;
|
||||
code?: string;
|
||||
}
|
||||
|
||||
export interface ICommandProvider {
|
||||
getCommandId(): string;
|
||||
|
||||
getCommandName(): string;
|
||||
|
||||
getShortcut?(): string;
|
||||
|
||||
canExecute?(): boolean;
|
||||
|
||||
execute(context: CommandExecutionContext): void | Promise<void>;
|
||||
}
|
||||
|
||||
export interface CommandExecutionContext {
|
||||
selectedNodeIds: string[];
|
||||
nodes: BehaviorTreeNode[];
|
||||
currentFile?: string;
|
||||
}
|
||||
|
||||
export class EditorExtensionRegistry {
|
||||
private nodeRenderers: Set<INodeRenderer> = new Set();
|
||||
private propertyEditors: Set<IPropertyEditor> = new Set();
|
||||
private nodeProviders: Set<INodeProvider> = new Set();
|
||||
private toolbarButtons: Set<IToolbarButton> = new Set();
|
||||
private panelProviders: Set<IPanelProvider> = new Set();
|
||||
private validators: Set<IValidator> = new Set();
|
||||
private commandProviders: Set<ICommandProvider> = new Set();
|
||||
|
||||
registerNodeRenderer(renderer: INodeRenderer): void {
|
||||
this.nodeRenderers.add(renderer);
|
||||
}
|
||||
|
||||
unregisterNodeRenderer(renderer: INodeRenderer): void {
|
||||
this.nodeRenderers.delete(renderer);
|
||||
}
|
||||
|
||||
getNodeRenderer(node: BehaviorTreeNode): INodeRenderer | undefined {
|
||||
for (const renderer of this.nodeRenderers) {
|
||||
if (renderer.canRender(node)) {
|
||||
return renderer;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
registerPropertyEditor(editor: IPropertyEditor): void {
|
||||
this.propertyEditors.add(editor);
|
||||
}
|
||||
|
||||
unregisterPropertyEditor(editor: IPropertyEditor): void {
|
||||
this.propertyEditors.delete(editor);
|
||||
}
|
||||
|
||||
getPropertyEditor(propertyType: string): IPropertyEditor | undefined {
|
||||
for (const editor of this.propertyEditors) {
|
||||
if (editor.canEdit(propertyType)) {
|
||||
return editor;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
registerNodeProvider(provider: INodeProvider): void {
|
||||
this.nodeProviders.add(provider);
|
||||
}
|
||||
|
||||
unregisterNodeProvider(provider: INodeProvider): void {
|
||||
this.nodeProviders.delete(provider);
|
||||
}
|
||||
|
||||
getAllNodeTemplates(): NodeTemplate[] {
|
||||
const templates: NodeTemplate[] = [];
|
||||
this.nodeProviders.forEach((provider) => {
|
||||
templates.push(...provider.getNodeTemplates());
|
||||
});
|
||||
return templates;
|
||||
}
|
||||
|
||||
registerToolbarButton(button: IToolbarButton): void {
|
||||
this.toolbarButtons.add(button);
|
||||
}
|
||||
|
||||
unregisterToolbarButton(button: IToolbarButton): void {
|
||||
this.toolbarButtons.delete(button);
|
||||
}
|
||||
|
||||
getToolbarButtons(): IToolbarButton[] {
|
||||
return Array.from(this.toolbarButtons).filter((btn) => {
|
||||
return btn.isVisible ? btn.isVisible() : true;
|
||||
});
|
||||
}
|
||||
|
||||
registerPanelProvider(provider: IPanelProvider): void {
|
||||
this.panelProviders.add(provider);
|
||||
}
|
||||
|
||||
unregisterPanelProvider(provider: IPanelProvider): void {
|
||||
this.panelProviders.delete(provider);
|
||||
}
|
||||
|
||||
getPanelProviders(): IPanelProvider[] {
|
||||
return Array.from(this.panelProviders).filter((panel) => {
|
||||
return panel.canActivate ? panel.canActivate() : true;
|
||||
});
|
||||
}
|
||||
|
||||
registerValidator(validator: IValidator): void {
|
||||
this.validators.add(validator);
|
||||
}
|
||||
|
||||
unregisterValidator(validator: IValidator): void {
|
||||
this.validators.delete(validator);
|
||||
}
|
||||
|
||||
async validateTree(nodes: BehaviorTreeNode[]): Promise<ValidationResult[]> {
|
||||
const results: ValidationResult[] = [];
|
||||
for (const validator of this.validators) {
|
||||
try {
|
||||
const validationResults = validator.validate(nodes);
|
||||
results.push(...validationResults);
|
||||
} catch (error) {
|
||||
console.error(`Error in validator ${validator.name}:`, error);
|
||||
results.push({
|
||||
severity: 'error',
|
||||
message: `Validator ${validator.name} failed: ${error}`,
|
||||
code: 'VALIDATOR_ERROR'
|
||||
});
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
registerCommandProvider(provider: ICommandProvider): void {
|
||||
this.commandProviders.add(provider);
|
||||
}
|
||||
|
||||
unregisterCommandProvider(provider: ICommandProvider): void {
|
||||
this.commandProviders.delete(provider);
|
||||
}
|
||||
|
||||
getCommandProvider(commandId: string): ICommandProvider | undefined {
|
||||
for (const provider of this.commandProviders) {
|
||||
if (provider.getCommandId() === commandId) {
|
||||
return provider;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
getAllCommandProviders(): ICommandProvider[] {
|
||||
return Array.from(this.commandProviders);
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.nodeRenderers.clear();
|
||||
this.propertyEditors.clear();
|
||||
this.nodeProviders.clear();
|
||||
this.toolbarButtons.clear();
|
||||
this.panelProviders.clear();
|
||||
this.validators.clear();
|
||||
this.commandProviders.clear();
|
||||
}
|
||||
}
|
||||
|
||||
let globalExtensionRegistry: EditorExtensionRegistry | null = null;
|
||||
|
||||
export function getGlobalExtensionRegistry(): EditorExtensionRegistry {
|
||||
if (!globalExtensionRegistry) {
|
||||
globalExtensionRegistry = new EditorExtensionRegistry();
|
||||
}
|
||||
return globalExtensionRegistry;
|
||||
}
|
||||
|
||||
export function resetGlobalExtensionRegistry(): void {
|
||||
globalExtensionRegistry = null;
|
||||
}
|
||||
@@ -1,249 +0,0 @@
|
||||
import { BehaviorTreeNode, Connection } from '../../stores/behaviorTreeStore';
|
||||
import { ExecutionLog } from '../../utils/BehaviorTreeExecutor';
|
||||
import { BlackboardValue } from '../../domain/models/Blackboard';
|
||||
|
||||
type BlackboardVariables = Record<string, BlackboardValue>;
|
||||
type NodeExecutionStatus = 'idle' | 'running' | 'success' | 'failure';
|
||||
|
||||
export interface ExecutionContext {
|
||||
nodes: BehaviorTreeNode[];
|
||||
connections: Connection[];
|
||||
blackboardVariables: BlackboardVariables;
|
||||
rootNodeId: string;
|
||||
tickCount: number;
|
||||
}
|
||||
|
||||
export interface NodeStatusChangeEvent {
|
||||
nodeId: string;
|
||||
status: NodeExecutionStatus;
|
||||
previousStatus?: NodeExecutionStatus;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export interface IExecutionHooks {
|
||||
beforePlay?(context: ExecutionContext): void | Promise<void>;
|
||||
|
||||
afterPlay?(context: ExecutionContext): void | Promise<void>;
|
||||
|
||||
beforePause?(): void | Promise<void>;
|
||||
|
||||
afterPause?(): void | Promise<void>;
|
||||
|
||||
beforeResume?(): void | Promise<void>;
|
||||
|
||||
afterResume?(): void | Promise<void>;
|
||||
|
||||
beforeStop?(): void | Promise<void>;
|
||||
|
||||
afterStop?(): void | Promise<void>;
|
||||
|
||||
beforeStep?(deltaTime: number): void | Promise<void>;
|
||||
|
||||
afterStep?(deltaTime: number): void | Promise<void>;
|
||||
|
||||
onTick?(tickCount: number, deltaTime: number): void | Promise<void>;
|
||||
|
||||
onNodeStatusChange?(event: NodeStatusChangeEvent): void | Promise<void>;
|
||||
|
||||
onExecutionComplete?(logs: ExecutionLog[]): void | Promise<void>;
|
||||
|
||||
onBlackboardUpdate?(variables: BlackboardVariables): void | Promise<void>;
|
||||
|
||||
onError?(error: Error, context?: string): void | Promise<void>;
|
||||
}
|
||||
|
||||
export class ExecutionHooksManager {
|
||||
private hooks: Set<IExecutionHooks> = new Set();
|
||||
|
||||
register(hook: IExecutionHooks): void {
|
||||
this.hooks.add(hook);
|
||||
}
|
||||
|
||||
unregister(hook: IExecutionHooks): void {
|
||||
this.hooks.delete(hook);
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.hooks.clear();
|
||||
}
|
||||
|
||||
async triggerBeforePlay(context: ExecutionContext): Promise<void> {
|
||||
for (const hook of this.hooks) {
|
||||
if (hook.beforePlay) {
|
||||
try {
|
||||
await hook.beforePlay(context);
|
||||
} catch (error) {
|
||||
console.error('Error in beforePlay hook:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async triggerAfterPlay(context: ExecutionContext): Promise<void> {
|
||||
for (const hook of this.hooks) {
|
||||
if (hook.afterPlay) {
|
||||
try {
|
||||
await hook.afterPlay(context);
|
||||
} catch (error) {
|
||||
console.error('Error in afterPlay hook:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async triggerBeforePause(): Promise<void> {
|
||||
for (const hook of this.hooks) {
|
||||
if (hook.beforePause) {
|
||||
try {
|
||||
await hook.beforePause();
|
||||
} catch (error) {
|
||||
console.error('Error in beforePause hook:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async triggerAfterPause(): Promise<void> {
|
||||
for (const hook of this.hooks) {
|
||||
if (hook.afterPause) {
|
||||
try {
|
||||
await hook.afterPause();
|
||||
} catch (error) {
|
||||
console.error('Error in afterPause hook:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async triggerBeforeResume(): Promise<void> {
|
||||
for (const hook of this.hooks) {
|
||||
if (hook.beforeResume) {
|
||||
try {
|
||||
await hook.beforeResume();
|
||||
} catch (error) {
|
||||
console.error('Error in beforeResume hook:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async triggerAfterResume(): Promise<void> {
|
||||
for (const hook of this.hooks) {
|
||||
if (hook.afterResume) {
|
||||
try {
|
||||
await hook.afterResume();
|
||||
} catch (error) {
|
||||
console.error('Error in afterResume hook:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async triggerBeforeStop(): Promise<void> {
|
||||
for (const hook of this.hooks) {
|
||||
if (hook.beforeStop) {
|
||||
try {
|
||||
await hook.beforeStop();
|
||||
} catch (error) {
|
||||
console.error('Error in beforeStop hook:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async triggerAfterStop(): Promise<void> {
|
||||
for (const hook of this.hooks) {
|
||||
if (hook.afterStop) {
|
||||
try {
|
||||
await hook.afterStop();
|
||||
} catch (error) {
|
||||
console.error('Error in afterStop hook:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async triggerBeforeStep(deltaTime: number): Promise<void> {
|
||||
for (const hook of this.hooks) {
|
||||
if (hook.beforeStep) {
|
||||
try {
|
||||
await hook.beforeStep(deltaTime);
|
||||
} catch (error) {
|
||||
console.error('Error in beforeStep hook:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async triggerAfterStep(deltaTime: number): Promise<void> {
|
||||
for (const hook of this.hooks) {
|
||||
if (hook.afterStep) {
|
||||
try {
|
||||
await hook.afterStep(deltaTime);
|
||||
} catch (error) {
|
||||
console.error('Error in afterStep hook:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async triggerOnTick(tickCount: number, deltaTime: number): Promise<void> {
|
||||
for (const hook of this.hooks) {
|
||||
if (hook.onTick) {
|
||||
try {
|
||||
await hook.onTick(tickCount, deltaTime);
|
||||
} catch (error) {
|
||||
console.error('Error in onTick hook:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async triggerOnNodeStatusChange(event: NodeStatusChangeEvent): Promise<void> {
|
||||
for (const hook of this.hooks) {
|
||||
if (hook.onNodeStatusChange) {
|
||||
try {
|
||||
await hook.onNodeStatusChange(event);
|
||||
} catch (error) {
|
||||
console.error('Error in onNodeStatusChange hook:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async triggerOnExecutionComplete(logs: ExecutionLog[]): Promise<void> {
|
||||
for (const hook of this.hooks) {
|
||||
if (hook.onExecutionComplete) {
|
||||
try {
|
||||
await hook.onExecutionComplete(logs);
|
||||
} catch (error) {
|
||||
console.error('Error in onExecutionComplete hook:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async triggerOnBlackboardUpdate(variables: BlackboardVariables): Promise<void> {
|
||||
for (const hook of this.hooks) {
|
||||
if (hook.onBlackboardUpdate) {
|
||||
try {
|
||||
await hook.onBlackboardUpdate(variables);
|
||||
} catch (error) {
|
||||
console.error('Error in onBlackboardUpdate hook:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async triggerOnError(error: Error, context?: string): Promise<void> {
|
||||
for (const hook of this.hooks) {
|
||||
if (hook.onError) {
|
||||
try {
|
||||
await hook.onError(error, context);
|
||||
} catch (err) {
|
||||
console.error('Error in onError hook:', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
import { BlackboardValue } from '../../domain/models/Blackboard';
|
||||
|
||||
type BlackboardVariables = Record<string, BlackboardValue>;
|
||||
|
||||
export class BlackboardManager {
|
||||
private initialVariables: BlackboardVariables = {};
|
||||
private currentVariables: BlackboardVariables = {};
|
||||
|
||||
setInitialVariables(variables: BlackboardVariables): void {
|
||||
this.initialVariables = JSON.parse(JSON.stringify(variables)) as BlackboardVariables;
|
||||
}
|
||||
|
||||
getInitialVariables(): BlackboardVariables {
|
||||
return { ...this.initialVariables };
|
||||
}
|
||||
|
||||
setCurrentVariables(variables: BlackboardVariables): void {
|
||||
this.currentVariables = { ...variables };
|
||||
}
|
||||
|
||||
getCurrentVariables(): BlackboardVariables {
|
||||
return { ...this.currentVariables };
|
||||
}
|
||||
|
||||
updateVariable(key: string, value: BlackboardValue): void {
|
||||
this.currentVariables[key] = value;
|
||||
}
|
||||
|
||||
restoreInitialVariables(): BlackboardVariables {
|
||||
this.currentVariables = { ...this.initialVariables };
|
||||
return this.getInitialVariables();
|
||||
}
|
||||
|
||||
hasChanges(): boolean {
|
||||
return JSON.stringify(this.currentVariables) !== JSON.stringify(this.initialVariables);
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.initialVariables = {};
|
||||
this.currentVariables = {};
|
||||
}
|
||||
}
|
||||
@@ -1,457 +0,0 @@
|
||||
import { BehaviorTreeExecutor, ExecutionStatus, ExecutionLog } from '../../utils/BehaviorTreeExecutor';
|
||||
import { BehaviorTreeNode, Connection, NodeExecutionStatus } from '../../stores/behaviorTreeStore';
|
||||
import { BlackboardValue } from '../../domain/models/Blackboard';
|
||||
import { DOMCache } from '../../presentation/utils/DOMCache';
|
||||
import { EditorEventBus, EditorEvent } from '../../infrastructure/events/EditorEventBus';
|
||||
import { ExecutionHooksManager } from '../interfaces/IExecutionHooks';
|
||||
|
||||
export type ExecutionMode = 'idle' | 'running' | 'paused' | 'step';
|
||||
type BlackboardVariables = Record<string, BlackboardValue>;
|
||||
|
||||
interface ExecutionControllerConfig {
|
||||
rootNodeId: string;
|
||||
projectPath: string | null;
|
||||
onLogsUpdate: (logs: ExecutionLog[]) => void;
|
||||
onBlackboardUpdate: (variables: BlackboardVariables) => void;
|
||||
onTickCountUpdate: (count: number) => void;
|
||||
onExecutionStatusUpdate: (statuses: Map<string, NodeExecutionStatus>, orders: Map<string, number>) => void;
|
||||
eventBus?: EditorEventBus;
|
||||
hooksManager?: ExecutionHooksManager;
|
||||
}
|
||||
|
||||
export class ExecutionController {
|
||||
private executor: BehaviorTreeExecutor | null = null;
|
||||
private mode: ExecutionMode = 'idle';
|
||||
private animationFrameId: number | null = null;
|
||||
private lastTickTime: number = 0;
|
||||
private speed: number = 1.0;
|
||||
private tickCount: number = 0;
|
||||
|
||||
private domCache: DOMCache = new DOMCache();
|
||||
private eventBus?: EditorEventBus;
|
||||
private hooksManager?: ExecutionHooksManager;
|
||||
|
||||
private config: ExecutionControllerConfig;
|
||||
private currentNodes: BehaviorTreeNode[] = [];
|
||||
private currentConnections: Connection[] = [];
|
||||
private currentBlackboard: BlackboardVariables = {};
|
||||
|
||||
private stepByStepMode: boolean = true;
|
||||
private pendingStatusUpdates: ExecutionStatus[] = [];
|
||||
private currentlyDisplayedIndex: number = 0;
|
||||
private lastStepTime: number = 0;
|
||||
private stepInterval: number = 200;
|
||||
|
||||
constructor(config: ExecutionControllerConfig) {
|
||||
this.config = config;
|
||||
this.executor = new BehaviorTreeExecutor();
|
||||
this.eventBus = config.eventBus;
|
||||
this.hooksManager = config.hooksManager;
|
||||
}
|
||||
|
||||
getMode(): ExecutionMode {
|
||||
return this.mode;
|
||||
}
|
||||
|
||||
getTickCount(): number {
|
||||
return this.tickCount;
|
||||
}
|
||||
|
||||
getSpeed(): number {
|
||||
return this.speed;
|
||||
}
|
||||
|
||||
setSpeed(speed: number): void {
|
||||
this.speed = speed;
|
||||
this.lastTickTime = 0;
|
||||
}
|
||||
|
||||
async play(
|
||||
nodes: BehaviorTreeNode[],
|
||||
blackboardVariables: BlackboardVariables,
|
||||
connections: Connection[]
|
||||
): Promise<void> {
|
||||
if (this.mode === 'running') return;
|
||||
|
||||
this.currentNodes = nodes;
|
||||
this.currentConnections = connections;
|
||||
this.currentBlackboard = blackboardVariables;
|
||||
|
||||
const context = {
|
||||
nodes,
|
||||
connections,
|
||||
blackboardVariables,
|
||||
rootNodeId: this.config.rootNodeId,
|
||||
tickCount: 0
|
||||
};
|
||||
|
||||
try {
|
||||
await this.hooksManager?.triggerBeforePlay(context);
|
||||
|
||||
this.mode = 'running';
|
||||
this.tickCount = 0;
|
||||
this.lastTickTime = 0;
|
||||
|
||||
if (!this.executor) {
|
||||
this.executor = new BehaviorTreeExecutor();
|
||||
}
|
||||
|
||||
this.executor.buildTree(
|
||||
nodes,
|
||||
this.config.rootNodeId,
|
||||
blackboardVariables,
|
||||
connections,
|
||||
this.handleExecutionStatusUpdate.bind(this)
|
||||
);
|
||||
|
||||
this.executor.start();
|
||||
this.animationFrameId = requestAnimationFrame(this.tickLoop.bind(this));
|
||||
|
||||
this.eventBus?.emit(EditorEvent.EXECUTION_STARTED, context);
|
||||
await this.hooksManager?.triggerAfterPlay(context);
|
||||
} catch (error) {
|
||||
console.error('Error in play:', error);
|
||||
await this.hooksManager?.triggerOnError(error as Error, 'play');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async pause(): Promise<void> {
|
||||
try {
|
||||
if (this.mode === 'running') {
|
||||
await this.hooksManager?.triggerBeforePause();
|
||||
|
||||
this.mode = 'paused';
|
||||
|
||||
if (this.executor) {
|
||||
this.executor.pause();
|
||||
}
|
||||
|
||||
if (this.animationFrameId !== null) {
|
||||
cancelAnimationFrame(this.animationFrameId);
|
||||
this.animationFrameId = null;
|
||||
}
|
||||
|
||||
this.eventBus?.emit(EditorEvent.EXECUTION_PAUSED);
|
||||
await this.hooksManager?.triggerAfterPause();
|
||||
} else if (this.mode === 'paused') {
|
||||
await this.hooksManager?.triggerBeforeResume();
|
||||
|
||||
this.mode = 'running';
|
||||
this.lastTickTime = 0;
|
||||
|
||||
if (this.executor) {
|
||||
this.executor.resume();
|
||||
}
|
||||
|
||||
this.animationFrameId = requestAnimationFrame(this.tickLoop.bind(this));
|
||||
|
||||
this.eventBus?.emit(EditorEvent.EXECUTION_RESUMED);
|
||||
await this.hooksManager?.triggerAfterResume();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error in pause/resume:', error);
|
||||
await this.hooksManager?.triggerOnError(error as Error, 'pause');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
try {
|
||||
await this.hooksManager?.triggerBeforeStop();
|
||||
|
||||
this.mode = 'idle';
|
||||
this.tickCount = 0;
|
||||
this.lastTickTime = 0;
|
||||
this.lastStepTime = 0;
|
||||
this.pendingStatusUpdates = [];
|
||||
this.currentlyDisplayedIndex = 0;
|
||||
|
||||
this.domCache.clearAllStatusTimers();
|
||||
this.domCache.clearStatusCache();
|
||||
|
||||
this.config.onExecutionStatusUpdate(new Map(), new Map());
|
||||
|
||||
if (this.animationFrameId !== null) {
|
||||
cancelAnimationFrame(this.animationFrameId);
|
||||
this.animationFrameId = null;
|
||||
}
|
||||
|
||||
if (this.executor) {
|
||||
this.executor.stop();
|
||||
}
|
||||
|
||||
this.eventBus?.emit(EditorEvent.EXECUTION_STOPPED);
|
||||
await this.hooksManager?.triggerAfterStop();
|
||||
} catch (error) {
|
||||
console.error('Error in stop:', error);
|
||||
await this.hooksManager?.triggerOnError(error as Error, 'stop');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async reset(): Promise<void> {
|
||||
await this.stop();
|
||||
|
||||
if (this.executor) {
|
||||
this.executor.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
step(): void {
|
||||
// 单步执行功能预留
|
||||
}
|
||||
|
||||
updateBlackboardVariable(key: string, value: BlackboardValue): void {
|
||||
if (this.executor && this.mode !== 'idle') {
|
||||
this.executor.updateBlackboardVariable(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
getBlackboardVariables(): BlackboardVariables {
|
||||
if (this.executor) {
|
||||
return this.executor.getBlackboardVariables();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
updateNodes(nodes: BehaviorTreeNode[]): void {
|
||||
if (this.mode === 'idle' || !this.executor) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentNodes = nodes;
|
||||
|
||||
this.executor.buildTree(
|
||||
nodes,
|
||||
this.config.rootNodeId,
|
||||
this.currentBlackboard,
|
||||
this.currentConnections,
|
||||
this.handleExecutionStatusUpdate.bind(this)
|
||||
);
|
||||
|
||||
this.executor.start();
|
||||
}
|
||||
|
||||
clearDOMCache(): void {
|
||||
this.domCache.clearAll();
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
this.stop();
|
||||
|
||||
if (this.executor) {
|
||||
this.executor.destroy();
|
||||
this.executor = null;
|
||||
}
|
||||
}
|
||||
|
||||
private tickLoop(currentTime: number): void {
|
||||
if (this.mode !== 'running') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.executor) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.stepByStepMode) {
|
||||
this.handleStepByStepExecution(currentTime);
|
||||
} else {
|
||||
this.handleNormalExecution(currentTime);
|
||||
}
|
||||
|
||||
this.animationFrameId = requestAnimationFrame(this.tickLoop.bind(this));
|
||||
}
|
||||
|
||||
private handleNormalExecution(currentTime: number): void {
|
||||
const baseTickInterval = 16.67;
|
||||
const scaledTickInterval = baseTickInterval / this.speed;
|
||||
|
||||
if (this.lastTickTime === 0) {
|
||||
this.lastTickTime = currentTime;
|
||||
}
|
||||
|
||||
const elapsed = currentTime - this.lastTickTime;
|
||||
|
||||
if (elapsed >= scaledTickInterval) {
|
||||
const deltaTime = baseTickInterval / 1000;
|
||||
|
||||
this.executor!.tick(deltaTime);
|
||||
|
||||
this.tickCount = this.executor!.getTickCount();
|
||||
this.config.onTickCountUpdate(this.tickCount);
|
||||
|
||||
this.lastTickTime = currentTime;
|
||||
}
|
||||
}
|
||||
|
||||
private handleStepByStepExecution(currentTime: number): void {
|
||||
if (this.lastStepTime === 0) {
|
||||
this.lastStepTime = currentTime;
|
||||
}
|
||||
|
||||
const stepElapsed = currentTime - this.lastStepTime;
|
||||
const actualStepInterval = this.stepInterval / this.speed;
|
||||
|
||||
if (stepElapsed >= actualStepInterval) {
|
||||
if (this.currentlyDisplayedIndex < this.pendingStatusUpdates.length) {
|
||||
this.displayNextNode();
|
||||
this.lastStepTime = currentTime;
|
||||
} else {
|
||||
if (this.lastTickTime === 0) {
|
||||
this.lastTickTime = currentTime;
|
||||
}
|
||||
|
||||
const tickElapsed = currentTime - this.lastTickTime;
|
||||
const baseTickInterval = 16.67;
|
||||
const scaledTickInterval = baseTickInterval / this.speed;
|
||||
|
||||
if (tickElapsed >= scaledTickInterval) {
|
||||
const deltaTime = baseTickInterval / 1000;
|
||||
this.executor!.tick(deltaTime);
|
||||
this.tickCount = this.executor!.getTickCount();
|
||||
this.config.onTickCountUpdate(this.tickCount);
|
||||
this.lastTickTime = currentTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private displayNextNode(): void {
|
||||
if (this.currentlyDisplayedIndex >= this.pendingStatusUpdates.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const statusesToDisplay = this.pendingStatusUpdates.slice(0, this.currentlyDisplayedIndex + 1);
|
||||
const currentNode = this.pendingStatusUpdates[this.currentlyDisplayedIndex];
|
||||
|
||||
if (!currentNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const statusMap = new Map<string, NodeExecutionStatus>();
|
||||
const orderMap = new Map<string, number>();
|
||||
|
||||
statusesToDisplay.forEach((s) => {
|
||||
statusMap.set(s.nodeId, s.status);
|
||||
if (s.executionOrder !== undefined) {
|
||||
orderMap.set(s.nodeId, s.executionOrder);
|
||||
}
|
||||
});
|
||||
|
||||
const nodeName = this.currentNodes.find(n => n.id === currentNode.nodeId)?.template.displayName || 'Unknown';
|
||||
console.log(`[StepByStep] Displaying ${this.currentlyDisplayedIndex + 1}/${this.pendingStatusUpdates.length} | ${nodeName} | Order: ${currentNode.executionOrder} | ID: ${currentNode.nodeId}`);
|
||||
this.config.onExecutionStatusUpdate(statusMap, orderMap);
|
||||
|
||||
this.currentlyDisplayedIndex++;
|
||||
}
|
||||
|
||||
private handleExecutionStatusUpdate(
|
||||
statuses: ExecutionStatus[],
|
||||
logs: ExecutionLog[],
|
||||
runtimeBlackboardVars?: BlackboardVariables
|
||||
): void {
|
||||
this.config.onLogsUpdate([...logs]);
|
||||
|
||||
if (runtimeBlackboardVars) {
|
||||
this.config.onBlackboardUpdate(runtimeBlackboardVars);
|
||||
}
|
||||
|
||||
if (this.stepByStepMode) {
|
||||
const statusesWithOrder = statuses.filter(s => s.executionOrder !== undefined);
|
||||
|
||||
if (statusesWithOrder.length > 0) {
|
||||
const minOrder = Math.min(...statusesWithOrder.map(s => s.executionOrder!));
|
||||
|
||||
if (minOrder === 1 || this.pendingStatusUpdates.length === 0) {
|
||||
this.pendingStatusUpdates = statusesWithOrder.sort((a, b) =>
|
||||
(a.executionOrder || 0) - (b.executionOrder || 0)
|
||||
);
|
||||
this.currentlyDisplayedIndex = 0;
|
||||
this.lastStepTime = 0;
|
||||
} else {
|
||||
const maxExistingOrder = this.pendingStatusUpdates.length > 0
|
||||
? Math.max(...this.pendingStatusUpdates.map(s => s.executionOrder || 0))
|
||||
: 0;
|
||||
|
||||
const newStatuses = statusesWithOrder.filter(s =>
|
||||
(s.executionOrder || 0) > maxExistingOrder
|
||||
);
|
||||
|
||||
if (newStatuses.length > 0) {
|
||||
console.log(`[StepByStep] Appending ${newStatuses.length} new nodes, orders:`, newStatuses.map(s => s.executionOrder));
|
||||
this.pendingStatusUpdates = [
|
||||
...this.pendingStatusUpdates,
|
||||
...newStatuses
|
||||
].sort((a, b) => (a.executionOrder || 0) - (b.executionOrder || 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const statusMap = new Map<string, NodeExecutionStatus>();
|
||||
const orderMap = new Map<string, number>();
|
||||
|
||||
statuses.forEach((s) => {
|
||||
statusMap.set(s.nodeId, s.status);
|
||||
if (s.executionOrder !== undefined) {
|
||||
orderMap.set(s.nodeId, s.executionOrder);
|
||||
}
|
||||
});
|
||||
|
||||
this.config.onExecutionStatusUpdate(statusMap, orderMap);
|
||||
}
|
||||
}
|
||||
|
||||
private updateConnectionStyles(
|
||||
statusMap: Record<string, NodeExecutionStatus>,
|
||||
connections?: Connection[]
|
||||
): void {
|
||||
if (!connections) return;
|
||||
|
||||
connections.forEach((conn) => {
|
||||
const connKey = `${conn.from}-${conn.to}`;
|
||||
|
||||
const pathElement = this.domCache.getConnection(connKey);
|
||||
if (!pathElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fromStatus = statusMap[conn.from];
|
||||
const toStatus = statusMap[conn.to];
|
||||
const isActive = fromStatus === 'running' || toStatus === 'running';
|
||||
|
||||
if (conn.connectionType === 'property') {
|
||||
this.domCache.setConnectionAttribute(connKey, 'stroke', '#9c27b0');
|
||||
this.domCache.setConnectionAttribute(connKey, 'stroke-width', '2');
|
||||
} else if (isActive) {
|
||||
this.domCache.setConnectionAttribute(connKey, 'stroke', '#ffa726');
|
||||
this.domCache.setConnectionAttribute(connKey, 'stroke-width', '3');
|
||||
} else {
|
||||
const isExecuted = this.domCache.hasNodeClass(conn.from, 'executed') &&
|
||||
this.domCache.hasNodeClass(conn.to, 'executed');
|
||||
|
||||
if (isExecuted) {
|
||||
this.domCache.setConnectionAttribute(connKey, 'stroke', '#4caf50');
|
||||
this.domCache.setConnectionAttribute(connKey, 'stroke-width', '2.5');
|
||||
} else {
|
||||
this.domCache.setConnectionAttribute(connKey, 'stroke', '#0e639c');
|
||||
this.domCache.setConnectionAttribute(connKey, 'stroke-width', '2');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setConnections(connections: Connection[]): void {
|
||||
if (this.mode !== 'idle') {
|
||||
const currentStatuses: Record<string, NodeExecutionStatus> = {};
|
||||
connections.forEach((conn) => {
|
||||
const fromStatus = this.domCache.getLastStatus(conn.from);
|
||||
const toStatus = this.domCache.getLastStatus(conn.to);
|
||||
if (fromStatus) currentStatuses[conn.from] = fromStatus;
|
||||
if (toStatus) currentStatuses[conn.to] = toStatus;
|
||||
});
|
||||
this.updateConnectionStyles(currentStatuses, connections);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
import { create } from 'zustand';
|
||||
import { BehaviorTree } from '../../domain/models/BehaviorTree';
|
||||
import { ITreeState } from '../commands/ITreeState';
|
||||
import { useBehaviorTreeStore } from '../../stores/behaviorTreeStore';
|
||||
import { Blackboard } from '../../domain/models/Blackboard';
|
||||
import { createRootNode, ROOT_NODE_ID } from '../../domain/constants/RootNode';
|
||||
|
||||
const createInitialTree = (): BehaviorTree => {
|
||||
const rootNode = createRootNode();
|
||||
return new BehaviorTree([rootNode], [], Blackboard.empty(), ROOT_NODE_ID);
|
||||
};
|
||||
|
||||
/**
|
||||
* 行为树数据状态
|
||||
* 管理核心业务数据
|
||||
*/
|
||||
interface BehaviorTreeDataState {
|
||||
/**
|
||||
* 当前行为树
|
||||
*/
|
||||
tree: BehaviorTree;
|
||||
|
||||
/**
|
||||
* 设置行为树
|
||||
*/
|
||||
setTree: (tree: BehaviorTree) => void;
|
||||
|
||||
/**
|
||||
* 重置为空树
|
||||
*/
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 行为树数据 Store
|
||||
* 实现 ITreeState 接口,供命令使用
|
||||
*/
|
||||
export const useBehaviorTreeDataStore = create<BehaviorTreeDataState>((set) => ({
|
||||
tree: createInitialTree(),
|
||||
|
||||
setTree: (tree: BehaviorTree) => set({ tree }),
|
||||
|
||||
reset: () => set({ tree: createInitialTree() })
|
||||
}));
|
||||
|
||||
/**
|
||||
* TreeState 适配器
|
||||
* 将 Zustand Store 适配为 ITreeState 接口
|
||||
* 同步更新领域层和表现层的状态
|
||||
*/
|
||||
export class TreeStateAdapter implements ITreeState {
|
||||
getTree(): BehaviorTree {
|
||||
return useBehaviorTreeDataStore.getState().tree;
|
||||
}
|
||||
|
||||
setTree(tree: BehaviorTree): void {
|
||||
useBehaviorTreeDataStore.getState().setTree(tree);
|
||||
|
||||
const nodes = Array.from(tree.nodes);
|
||||
const connections = Array.from(tree.connections);
|
||||
|
||||
useBehaviorTreeStore.getState().setNodes(nodes);
|
||||
useBehaviorTreeStore.getState().setConnections(connections);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,2 @@
|
||||
export { useBehaviorTreeDataStore, TreeStateAdapter } from './BehaviorTreeDataStore';
|
||||
export { useUIStore } from './UIStore';
|
||||
export { useEditorStore } from './EditorStore';
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import { Connection, ConnectionType } from '../../domain/models/Connection';
|
||||
import { CommandManager } from '../commands/CommandManager';
|
||||
import { AddConnectionCommand } from '../commands/tree/AddConnectionCommand';
|
||||
import { ITreeState } from '../commands/ITreeState';
|
||||
import { IValidator } from '../../domain/interfaces/IValidator';
|
||||
|
||||
/**
|
||||
* 添加连接用例
|
||||
*/
|
||||
export class AddConnectionUseCase {
|
||||
constructor(
|
||||
private readonly commandManager: CommandManager,
|
||||
private readonly treeState: ITreeState,
|
||||
private readonly validator: IValidator
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 执行添加连接操作
|
||||
*/
|
||||
execute(
|
||||
from: string,
|
||||
to: string,
|
||||
connectionType: ConnectionType = 'node',
|
||||
fromProperty?: string,
|
||||
toProperty?: string
|
||||
): Connection {
|
||||
const connection = new Connection(from, to, connectionType, fromProperty, toProperty);
|
||||
|
||||
const tree = this.treeState.getTree();
|
||||
const validationResult = this.validator.validateConnection(connection, tree);
|
||||
|
||||
if (!validationResult.isValid) {
|
||||
const errorMessages = validationResult.errors.map((e) => e.message).join(', ');
|
||||
throw new Error(`连接验证失败: ${errorMessages}`);
|
||||
}
|
||||
|
||||
const command = new AddConnectionCommand(this.treeState, connection);
|
||||
this.commandManager.execute(command);
|
||||
|
||||
return connection;
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
import { NodeTemplate } from '@esengine/behavior-tree';
|
||||
import { Node } from '../../domain/models/Node';
|
||||
import { Position } from '../../domain/value-objects/Position';
|
||||
import { INodeFactory } from '../../domain/interfaces/INodeFactory';
|
||||
import { CommandManager } from '../commands/CommandManager';
|
||||
import { CreateNodeCommand } from '../commands/tree/CreateNodeCommand';
|
||||
import { ITreeState } from '../commands/ITreeState';
|
||||
|
||||
/**
|
||||
* 创建节点用例
|
||||
*/
|
||||
export class CreateNodeUseCase {
|
||||
constructor(
|
||||
private readonly nodeFactory: INodeFactory,
|
||||
private readonly commandManager: CommandManager,
|
||||
private readonly treeState: ITreeState
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 执行创建节点操作
|
||||
*/
|
||||
execute(template: NodeTemplate, position: Position, data?: Record<string, unknown>): Node {
|
||||
const node = this.nodeFactory.createNode(template, position, data);
|
||||
|
||||
const command = new CreateNodeCommand(this.treeState, node);
|
||||
this.commandManager.execute(command);
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据类型创建节点
|
||||
*/
|
||||
executeByType(nodeType: string, position: Position, data?: Record<string, unknown>): Node {
|
||||
const node = this.nodeFactory.createNodeByType(nodeType, position, data);
|
||||
|
||||
const command = new CreateNodeCommand(this.treeState, node);
|
||||
this.commandManager.execute(command);
|
||||
|
||||
return node;
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
import { CommandManager } from '../commands/CommandManager';
|
||||
import { DeleteNodeCommand } from '../commands/tree/DeleteNodeCommand';
|
||||
import { RemoveConnectionCommand } from '../commands/tree/RemoveConnectionCommand';
|
||||
import { ITreeState } from '../commands/ITreeState';
|
||||
import { ICommand } from '../commands/ICommand';
|
||||
|
||||
/**
|
||||
* 删除节点用例
|
||||
* 删除节点时会自动删除相关连接
|
||||
*/
|
||||
export class DeleteNodeUseCase {
|
||||
constructor(
|
||||
private readonly commandManager: CommandManager,
|
||||
private readonly treeState: ITreeState
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 删除单个节点
|
||||
*/
|
||||
execute(nodeId: string): void {
|
||||
const tree = this.treeState.getTree();
|
||||
|
||||
const relatedConnections = tree.connections.filter(
|
||||
(conn) => conn.from === nodeId || conn.to === nodeId
|
||||
);
|
||||
|
||||
const commands: ICommand[] = [];
|
||||
|
||||
relatedConnections.forEach((conn) => {
|
||||
commands.push(
|
||||
new RemoveConnectionCommand(
|
||||
this.treeState,
|
||||
conn.from,
|
||||
conn.to,
|
||||
conn.fromProperty,
|
||||
conn.toProperty
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
commands.push(new DeleteNodeCommand(this.treeState, nodeId));
|
||||
|
||||
this.commandManager.executeBatch(commands);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除节点
|
||||
*/
|
||||
executeBatch(nodeIds: string[]): void {
|
||||
const tree = this.treeState.getTree();
|
||||
const commands: ICommand[] = [];
|
||||
|
||||
const nodeIdSet = new Set(nodeIds);
|
||||
|
||||
const relatedConnections = tree.connections.filter(
|
||||
(conn) => nodeIdSet.has(conn.from) || nodeIdSet.has(conn.to)
|
||||
);
|
||||
|
||||
relatedConnections.forEach((conn) => {
|
||||
commands.push(
|
||||
new RemoveConnectionCommand(
|
||||
this.treeState,
|
||||
conn.from,
|
||||
conn.to,
|
||||
conn.fromProperty,
|
||||
conn.toProperty
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
nodeIds.forEach((nodeId) => {
|
||||
commands.push(new DeleteNodeCommand(this.treeState, nodeId));
|
||||
});
|
||||
|
||||
this.commandManager.executeBatch(commands);
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import { Position } from '../../domain/value-objects/Position';
|
||||
import { CommandManager } from '../commands/CommandManager';
|
||||
import { MoveNodeCommand } from '../commands/tree/MoveNodeCommand';
|
||||
import { ITreeState } from '../commands/ITreeState';
|
||||
|
||||
/**
|
||||
* 移动节点用例
|
||||
*/
|
||||
export class MoveNodeUseCase {
|
||||
constructor(
|
||||
private readonly commandManager: CommandManager,
|
||||
private readonly treeState: ITreeState
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 移动单个节点
|
||||
*/
|
||||
execute(nodeId: string, newPosition: Position): void {
|
||||
const command = new MoveNodeCommand(this.treeState, nodeId, newPosition);
|
||||
this.commandManager.execute(command);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量移动节点
|
||||
*/
|
||||
executeBatch(moves: Array<{ nodeId: string; position: Position }>): void {
|
||||
const commands = moves.map(
|
||||
({ nodeId, position }) => new MoveNodeCommand(this.treeState, nodeId, position)
|
||||
);
|
||||
this.commandManager.executeBatch(commands);
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import { CommandManager } from '../commands/CommandManager';
|
||||
import { RemoveConnectionCommand } from '../commands/tree/RemoveConnectionCommand';
|
||||
import { ITreeState } from '../commands/ITreeState';
|
||||
|
||||
/**
|
||||
* 移除连接用例
|
||||
*/
|
||||
export class RemoveConnectionUseCase {
|
||||
constructor(
|
||||
private readonly commandManager: CommandManager,
|
||||
private readonly treeState: ITreeState
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 执行移除连接操作
|
||||
*/
|
||||
execute(from: string, to: string, fromProperty?: string, toProperty?: string): void {
|
||||
const command = new RemoveConnectionCommand(
|
||||
this.treeState,
|
||||
from,
|
||||
to,
|
||||
fromProperty,
|
||||
toProperty
|
||||
);
|
||||
this.commandManager.execute(command);
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import { CommandManager } from '../commands/CommandManager';
|
||||
import { UpdateNodeDataCommand } from '../commands/tree/UpdateNodeDataCommand';
|
||||
import { ITreeState } from '../commands/ITreeState';
|
||||
|
||||
/**
|
||||
* 更新节点数据用例
|
||||
*/
|
||||
export class UpdateNodeDataUseCase {
|
||||
constructor(
|
||||
private readonly commandManager: CommandManager,
|
||||
private readonly treeState: ITreeState
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 更新节点数据
|
||||
*/
|
||||
execute(nodeId: string, data: Record<string, unknown>): void {
|
||||
const command = new UpdateNodeDataCommand(this.treeState, nodeId, data);
|
||||
this.commandManager.execute(command);
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import { IValidator, ValidationResult } from '../../domain/interfaces/IValidator';
|
||||
import { ITreeState } from '../commands/ITreeState';
|
||||
|
||||
/**
|
||||
* 验证行为树用例
|
||||
*/
|
||||
export class ValidateTreeUseCase {
|
||||
constructor(
|
||||
private readonly validator: IValidator,
|
||||
private readonly treeState: ITreeState
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 验证当前行为树
|
||||
*/
|
||||
execute(): ValidationResult {
|
||||
const tree = this.treeState.getTree();
|
||||
return this.validator.validateTree(tree);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证并抛出错误(如果验证失败)
|
||||
*/
|
||||
executeAndThrow(): void {
|
||||
const result = this.execute();
|
||||
|
||||
if (!result.isValid) {
|
||||
const errorMessages = result.errors.map((e) => e.message).join('\n');
|
||||
throw new Error(`行为树验证失败:\n${errorMessages}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
export { CreateNodeUseCase } from './CreateNodeUseCase';
|
||||
export { DeleteNodeUseCase } from './DeleteNodeUseCase';
|
||||
export { AddConnectionUseCase } from './AddConnectionUseCase';
|
||||
export { RemoveConnectionUseCase } from './RemoveConnectionUseCase';
|
||||
export { MoveNodeUseCase } from './MoveNodeUseCase';
|
||||
export { UpdateNodeDataUseCase } from './UpdateNodeDataUseCase';
|
||||
export { ValidateTreeUseCase } from './ValidateTreeUseCase';
|
||||
Reference in New Issue
Block a user