* feat: 添加 Tilemap 编辑器插件和组件生命周期支持 * feat(editor-core): 添加声明式插件注册 API * feat(editor-core): 改进tiledmap结构合并tileset进tiledmapeditor * feat: 添加 editor-runtime SDK 和插件系统改进 * fix(ci): 修复SceneResourceManager里变量未使用问题
209 lines
6.7 KiB
TypeScript
209 lines
6.7 KiB
TypeScript
import { useState, type RefObject } from '@esengine/editor-runtime';
|
||
import { NodeTemplate } from '@esengine/behavior-tree';
|
||
import { BehaviorTreeNode, Connection, useBehaviorTreeDataStore } from '../stores';
|
||
import { Node } from '../domain/models/Node';
|
||
import { Position } from '../domain/value-objects/Position';
|
||
import { useNodeOperations } from './useNodeOperations';
|
||
import { useConnectionOperations } from './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';
|
||
|
||
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[];
|
||
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,
|
||
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)
|
||
);
|
||
|
||
// 替换节点 - 通过 store 更新
|
||
const store = useBehaviorTreeDataStore.getState();
|
||
const updatedTree = store.tree.updateNode(newNode.id, () => newNode);
|
||
store.setTree(updatedTree);
|
||
|
||
// 删除所有指向该节点的属性连接,让用户重新连接
|
||
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
|
||
};
|
||
}
|