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:
210
packages/editor-app/src/application/hooks/useQuickCreateMenu.ts
Normal file
210
packages/editor-app/src/application/hooks/useQuickCreateMenu.ts
Normal file
@@ -0,0 +1,210 @@
|
||||
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;
|
||||
}
|
||||
|
||||
// 创建模式:需要连接
|
||||
if (!connectingFrom) {
|
||||
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
|
||||
);
|
||||
|
||||
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
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user