Refactor/clean architecture phase1 (#215)

* refactor(editor): 建立Clean Architecture领域模型层

* refactor(editor): 实现应用层架构 - 命令模式、用例和状态管理

* refactor(editor): 实现展示层核心Hooks

* refactor(editor): 实现基础设施层和展示层组件

* refactor(editor): 迁移画布和连接渲染到 Clean Architecture 组件

* feat(editor): 集成应用层架构和命令模式,实现撤销/重做功能

* refactor(editor): UI组件拆分

* refactor(editor): 提取快速创建菜单逻辑

* refactor(editor): 重构BehaviorTreeEditor,提取组件和Hook

* refactor(editor): 提取端口连接和键盘事件Hook

* refactor(editor): 提取拖放处理Hook

* refactor(editor): 提取画布交互Hook和工具函数

* refactor(editor): 完成核心重构

* fix(editor): 修复节点无法创建和连接

* refactor(behavior-tree,editor): 重构节点子节点约束系统,实现元数据驱动的架构
This commit is contained in:
YHH
2025-11-03 21:22:16 +08:00
committed by GitHub
parent 40cde9c050
commit adfc7e91b3
104 changed files with 8232 additions and 2506 deletions

View File

@@ -0,0 +1,137 @@
type EventHandler<T = any> = (data: T) => void;
interface Subscription {
unsubscribe: () => void;
}
export enum EditorEvent {
NODE_CREATED = 'node:created',
NODE_DELETED = 'node:deleted',
NODE_UPDATED = 'node:updated',
NODE_MOVED = 'node:moved',
NODE_SELECTED = 'node:selected',
CONNECTION_ADDED = 'connection:added',
CONNECTION_REMOVED = 'connection:removed',
EXECUTION_STARTED = 'execution:started',
EXECUTION_PAUSED = 'execution:paused',
EXECUTION_RESUMED = 'execution:resumed',
EXECUTION_STOPPED = 'execution:stopped',
EXECUTION_TICK = 'execution:tick',
EXECUTION_NODE_STATUS_CHANGED = 'execution:node_status_changed',
TREE_SAVED = 'tree:saved',
TREE_LOADED = 'tree:loaded',
TREE_VALIDATED = 'tree:validated',
BLACKBOARD_VARIABLE_UPDATED = 'blackboard:variable_updated',
BLACKBOARD_RESTORED = 'blackboard:restored',
CANVAS_ZOOM_CHANGED = 'canvas:zoom_changed',
CANVAS_PAN_CHANGED = 'canvas:pan_changed',
CANVAS_RESET = 'canvas:reset',
COMMAND_EXECUTED = 'command:executed',
COMMAND_UNDONE = 'command:undone',
COMMAND_REDONE = 'command:redone'
}
export class EditorEventBus {
private listeners: Map<string, Set<EventHandler>> = new Map();
private eventHistory: Array<{ event: string; data: any; timestamp: number }> = [];
private maxHistorySize: number = 100;
on<T = any>(event: EditorEvent | string, handler: EventHandler<T>): Subscription {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set());
}
this.listeners.get(event)!.add(handler);
return {
unsubscribe: () => this.off(event, handler)
};
}
once<T = any>(event: EditorEvent | string, handler: EventHandler<T>): Subscription {
const wrappedHandler = (data: T) => {
handler(data);
this.off(event, wrappedHandler);
};
return this.on(event, wrappedHandler);
}
off<T = any>(event: EditorEvent | string, handler: EventHandler<T>): void {
const handlers = this.listeners.get(event);
if (handlers) {
handlers.delete(handler);
if (handlers.size === 0) {
this.listeners.delete(event);
}
}
}
emit<T = any>(event: EditorEvent | string, data?: T): void {
if (this.eventHistory.length >= this.maxHistorySize) {
this.eventHistory.shift();
}
this.eventHistory.push({
event,
data,
timestamp: Date.now()
});
const handlers = this.listeners.get(event);
if (handlers) {
handlers.forEach((handler) => {
try {
handler(data);
} catch (error) {
console.error(`Error in event handler for ${event}:`, error);
}
});
}
}
clear(event?: EditorEvent | string): void {
if (event) {
this.listeners.delete(event);
} else {
this.listeners.clear();
}
}
getListenerCount(event: EditorEvent | string): number {
return this.listeners.get(event)?.size || 0;
}
getAllEvents(): string[] {
return Array.from(this.listeners.keys());
}
getEventHistory(count?: number): Array<{ event: string; data: any; timestamp: number }> {
if (count) {
return this.eventHistory.slice(-count);
}
return [...this.eventHistory];
}
clearHistory(): void {
this.eventHistory = [];
}
}
let globalEventBus: EditorEventBus | null = null;
export function getGlobalEventBus(): EditorEventBus {
if (!globalEventBus) {
globalEventBus = new EditorEventBus();
}
return globalEventBus;
}
export function resetGlobalEventBus(): void {
globalEventBus = null;
}

View File

@@ -0,0 +1,79 @@
import { NodeTemplate, NodeTemplates } from '@esengine/behavior-tree';
import { Node } from '../../domain/models/Node';
import { Position } from '../../domain/value-objects/Position';
import { INodeFactory } from '../../domain/interfaces/INodeFactory';
/**
* 生成唯一ID
*/
function generateUniqueId(): string {
return `node-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
/**
* 节点工厂实现
*/
export class NodeFactory implements INodeFactory {
/**
* 创建节点
*/
createNode(
template: NodeTemplate,
position: Position,
data?: Record<string, unknown>
): Node {
const nodeId = generateUniqueId();
const nodeData = {
...template.defaultConfig,
...data
};
return new Node(nodeId, template, nodeData, position, []);
}
/**
* 根据模板类型创建节点
*/
createNodeByType(
nodeType: string,
position: Position,
data?: Record<string, unknown>
): Node {
const template = this.getTemplateByType(nodeType);
if (!template) {
throw new Error(`未找到节点模板: ${nodeType}`);
}
return this.createNode(template, position, data);
}
/**
* 克隆节点
*/
cloneNode(node: Node, newPosition?: Position): Node {
const position = newPosition || node.position;
const clonedId = generateUniqueId();
return new Node(
clonedId,
node.template,
node.data,
position,
[]
);
}
/**
* 根据类型获取模板
*/
private getTemplateByType(nodeType: string): NodeTemplate | null {
const allTemplates = NodeTemplates.getAllTemplates();
const template = allTemplates.find((t: NodeTemplate) => {
const defaultNodeType = t.defaultConfig.nodeType;
return defaultNodeType === nodeType;
});
return template || null;
}
}

View File

@@ -0,0 +1 @@
export { NodeFactory } from './NodeFactory';

View File

@@ -0,0 +1,2 @@
export * from './factories';
export * from './serialization';

View File

@@ -0,0 +1,127 @@
import { BehaviorTree } from '../../domain/models/BehaviorTree';
import { ISerializer, SerializationFormat } from '../../domain/interfaces/ISerializer';
import { BehaviorTreeAssetSerializer, EditorFormatConverter } from '@esengine/behavior-tree';
/**
* 序列化选项
*/
export interface SerializationOptions {
/**
* 资产版本号
*/
version?: string;
/**
* 资产名称
*/
name?: string;
/**
* 资产描述
*/
description?: string;
/**
* 创建时间
*/
createdAt?: string;
/**
* 修改时间
*/
modifiedAt?: string;
}
/**
* 行为树序列化器实现
*/
export class BehaviorTreeSerializer implements ISerializer {
private readonly defaultOptions: Required<SerializationOptions> = {
version: '1.0.0',
name: 'Untitled Behavior Tree',
description: '',
createdAt: new Date().toISOString(),
modifiedAt: new Date().toISOString()
};
constructor(private readonly options: SerializationOptions = {}) {
this.defaultOptions = { ...this.defaultOptions, ...options };
}
/**
* 序列化行为树
*/
serialize(tree: BehaviorTree, format: SerializationFormat): string | Uint8Array {
const treeObject = tree.toObject();
if (format === 'json') {
return JSON.stringify(treeObject, null, 2);
}
throw new Error(`不支持的序列化格式: ${format}`);
}
/**
* 反序列化行为树
*/
deserialize(data: string | Uint8Array, format: SerializationFormat): BehaviorTree {
if (format === 'json') {
if (typeof data !== 'string') {
throw new Error('JSON 格式需要字符串数据');
}
const obj = JSON.parse(data);
return BehaviorTree.fromObject(obj);
}
throw new Error(`不支持的反序列化格式: ${format}`);
}
/**
* 导出为运行时资产格式
* @param tree 行为树
* @param format 导出格式
* @param options 可选的序列化选项(覆盖默认值)
*/
exportToRuntimeAsset(
tree: BehaviorTree,
format: SerializationFormat,
options?: SerializationOptions
): string | Uint8Array {
const nodes = tree.nodes.map((node) => ({
id: node.id,
template: node.template,
data: node.data,
position: node.position.toObject(),
children: Array.from(node.children)
}));
const connections = tree.connections.map((conn) => conn.toObject());
const blackboard = tree.blackboard.toObject();
const finalOptions = { ...this.defaultOptions, ...options };
finalOptions.modifiedAt = new Date().toISOString();
const editorFormat = {
version: finalOptions.version,
metadata: {
name: finalOptions.name,
description: finalOptions.description,
createdAt: finalOptions.createdAt,
modifiedAt: finalOptions.modifiedAt
},
nodes,
connections,
blackboard
};
const asset = EditorFormatConverter.toAsset(editorFormat);
if (format === 'json') {
return BehaviorTreeAssetSerializer.serialize(asset, { format: 'json', pretty: true });
} else if (format === 'binary') {
return BehaviorTreeAssetSerializer.serialize(asset, { format: 'binary' });
}
throw new Error(`不支持的导出格式: ${format}`);
}
}

View File

@@ -0,0 +1 @@
export { BehaviorTreeSerializer } from './BehaviorTreeSerializer';

View File

@@ -0,0 +1,149 @@
import { IValidator, ValidationResult, ValidationError } from '../../domain/interfaces/IValidator';
import { BehaviorTree } from '../../domain/models/BehaviorTree';
import { Node } from '../../domain/models/Node';
import { Connection } from '../../domain/models/Connection';
/**
* 行为树验证器实现
*/
export class BehaviorTreeValidator implements IValidator {
/**
* 验证整个行为树
*/
validateTree(tree: BehaviorTree): ValidationResult {
const errors: ValidationError[] = [];
// 验证所有节点
for (const node of tree.nodes) {
const nodeResult = this.validateNode(node);
errors.push(...nodeResult.errors);
}
// 验证所有连接
for (const connection of tree.connections) {
const connResult = this.validateConnection(connection, tree);
errors.push(...connResult.errors);
}
// 验证循环引用
const cycleResult = this.validateNoCycles(tree);
errors.push(...cycleResult.errors);
return {
isValid: errors.length === 0,
errors
};
}
/**
* 验证节点
*/
validateNode(node: Node): ValidationResult {
const errors: ValidationError[] = [];
// 验证节点必填字段
if (!node.id) {
errors.push({
message: '节点 ID 不能为空',
nodeId: node.id
});
}
if (!node.template) {
errors.push({
message: '节点模板不能为空',
nodeId: node.id
});
}
return {
isValid: errors.length === 0,
errors
};
}
/**
* 验证连接
*/
validateConnection(connection: Connection, tree: BehaviorTree): ValidationResult {
const errors: ValidationError[] = [];
// 验证连接的源节点和目标节点都存在
const fromNode = tree.nodes.find((n) => n.id === connection.from);
const toNode = tree.nodes.find((n) => n.id === connection.to);
if (!fromNode) {
errors.push({
message: `连接的源节点不存在: ${connection.from}`
});
}
if (!toNode) {
errors.push({
message: `连接的目标节点不存在: ${connection.to}`
});
}
// 不能自己连接自己
if (connection.from === connection.to) {
errors.push({
message: '节点不能连接到自己',
nodeId: connection.from
});
}
return {
isValid: errors.length === 0,
errors
};
}
/**
* 验证是否会产生循环引用
*/
validateNoCycles(tree: BehaviorTree): ValidationResult {
const errors: ValidationError[] = [];
const visited = new Set<string>();
const recursionStack = new Set<string>();
const hasCycle = (nodeId: string): boolean => {
if (recursionStack.has(nodeId)) {
return true;
}
if (visited.has(nodeId)) {
return false;
}
visited.add(nodeId);
recursionStack.add(nodeId);
const node = tree.nodes.find((n) => n.id === nodeId);
if (node) {
for (const childId of node.children) {
if (hasCycle(childId)) {
return true;
}
}
}
recursionStack.delete(nodeId);
return false;
};
for (const node of tree.nodes) {
if (hasCycle(node.id)) {
errors.push({
message: '行为树中存在循环引用',
nodeId: node.id
});
break;
}
}
return {
isValid: errors.length === 0,
errors
};
}
}