diff --git a/extensions/cocos/cocos-ecs/assets/behavior_tree.json.bt.json b/extensions/cocos/cocos-ecs/assets/behavior_tree.json.bt.json new file mode 100644 index 00000000..a5ba8f67 --- /dev/null +++ b/extensions/cocos/cocos-ecs/assets/behavior_tree.json.bt.json @@ -0,0 +1,63 @@ +{ + "nodes": [ + { + "id": "node_b0bpuk8ei", + "type": "Sequence", + "name": "序列器", + "icon": "→", + "description": "按顺序执行子节点,任一失败则整体失败", + "x": 207.39999389648438, + "y": 145.59999084472656, + "children": [ + "node_pgmfxi7ho" + ], + "properties": { + "abortType": { + "name": "中止类型", + "type": "select", + "value": "None", + "description": "决定节点在何种情况下会被中止", + "options": [ + "None", + "LowerPriority", + "Self", + "Both" + ], + "required": false + } + }, + "canHaveChildren": true, + "canHaveParent": true, + "hasError": false + }, + { + "id": "node_pgmfxi7ho", + "type": "Inverter", + "name": "反转器", + "icon": "⚡", + "description": "反转子节点的执行结果", + "x": 163.39999389648438, + "y": 436.59999084472656, + "children": [], + "properties": {}, + "canHaveChildren": true, + "canHaveParent": true, + "hasError": false, + "parent": "node_b0bpuk8ei" + } + ], + "connections": [ + { + "id": "node_b0bpuk8ei-node_pgmfxi7ho", + "sourceId": "node_b0bpuk8ei", + "targetId": "node_pgmfxi7ho", + "path": "M 307.3999938964844 265.59999084472656 C 307.3999938964844 351.09999084472656 263.3999938964844 351.09999084472656 263.3999938964844 436.59999084472656", + "active": false + } + ], + "metadata": { + "name": "untitled", + "created": "2025-06-17T14:52:33.885Z", + "version": "1.0" + } +} \ No newline at end of file diff --git a/extensions/cocos/cocos-ecs/assets/behavior_tree.json.bt.json.meta b/extensions/cocos/cocos-ecs/assets/behavior_tree.json.bt.json.meta new file mode 100644 index 00000000..419b443e --- /dev/null +++ b/extensions/cocos/cocos-ecs/assets/behavior_tree.json.bt.json.meta @@ -0,0 +1,11 @@ +{ + "ver": "2.0.1", + "importer": "json", + "imported": true, + "uuid": "cb66452d-5cad-46a9-96f9-b62831e0edc3", + "files": [ + ".json" + ], + "subMetas": {}, + "userData": {} +} diff --git a/extensions/cocos/cocos-ecs/assets/resources.meta b/extensions/cocos/cocos-ecs/assets/resources.meta new file mode 100644 index 00000000..5970eca5 --- /dev/null +++ b/extensions/cocos/cocos-ecs/assets/resources.meta @@ -0,0 +1,14 @@ +{ + "ver": "1.2.0", + "importer": "directory", + "imported": true, + "uuid": "da4522ce-bedb-42d5-8cba-63dcb4641265", + "files": [], + "subMetas": {}, + "userData": { + "isBundle": true, + "bundleConfigID": "default", + "bundleName": "resources", + "priority": 8 + } +} diff --git a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/package.json b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/package.json index 7de3cc51..44500c0e 100644 --- a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/package.json +++ b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/package.json @@ -100,6 +100,20 @@ "message": "open-panel" } ], + "asset-menu": [ + { + "path": "i18n:menu.create/ECS Framework", + "label": "创建行为树文件", + "message": "create-behavior-tree-file", + "target": "folder" + }, + { + "path": "i18n:menu.open", + "label": "用行为树编辑器打开", + "message": "open-behavior-tree-file", + "target": [".bt.json", ".json"] + } + ], "messages": { "open-panel": { "methods": [ @@ -175,6 +189,21 @@ "methods": [ "open-behavior-tree-docs" ] + }, + "create-behavior-tree-file": { + "methods": [ + "create-behavior-tree-file" + ] + }, + "open-behavior-tree-file": { + "methods": [ + "open-behavior-tree-file" + ] + }, + "create-behavior-tree-from-editor": { + "methods": [ + "create-behavior-tree-from-editor" + ] } } } diff --git a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/main.ts b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/main.ts index 541dd971..b23c29d7 100644 --- a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/main.ts +++ b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/main.ts @@ -531,6 +531,169 @@ export class AIExampleComponent extends Component { }); } }, + + /** + * 创建行为树文件 + */ + async 'create-behavior-tree-file'(assetInfo: any) { + console.log('Creating behavior tree file in folder:', assetInfo?.path); + + try { + // 获取项目assets目录 + const projectPath = Editor.Project.path; + const assetsPath = path.join(projectPath, 'assets'); + + // 生成唯一文件名 + let fileName = 'NewBehaviorTree'; + let counter = 1; + let filePath = path.join(assetsPath, `${fileName}.bt.json`); + + while (fs.existsSync(filePath)) { + fileName = `NewBehaviorTree_${counter}`; + filePath = path.join(assetsPath, `${fileName}.bt.json`); + counter++; + } + + // 创建默认的行为树配置 + const defaultConfig = { + version: "1.0.0", + type: "behavior-tree", + metadata: { + createdAt: new Date().toISOString(), + nodeCount: 1 + }, + tree: { + id: "root", + type: "sequence", + namespace: "behaviourTree/composites", + properties: {}, + children: [] + } + }; + + // 写入文件 + await fsExtra.writeFile(filePath, JSON.stringify(defaultConfig, null, 2)); + + // 刷新资源管理器 + await Editor.Message.request('asset-db', 'refresh-asset', 'db://assets'); + + console.log(`Behavior tree file created: ${filePath}`); + + Editor.Dialog.info('创建成功', { + detail: `行为树文件 "${fileName}.bt.json" 已创建完成!\n\n文件位置:assets/${fileName}.bt.json\n\n您可以右键点击文件选择"用行为树编辑器打开"来编辑它。`, + }); + + } catch (error) { + console.error('Failed to create behavior tree file:', error); + Editor.Dialog.error('创建失败', { + detail: `创建行为树文件失败:\n\n${error instanceof Error ? error.message : String(error)}`, + }); + } + }, + + /** + * 用行为树编辑器打开文件 + */ + async 'open-behavior-tree-file'(assetInfo: any) { + console.log('Opening behavior tree file:', assetInfo); + + try { + // 直接从assetInfo获取文件系统路径 + const assetPath = assetInfo?.path; + if (!assetPath) { + throw new Error('无效的文件路径'); + } + + // 转换为文件系统路径 + const projectPath = Editor.Project.path; + const relativePath = assetPath.replace('db://assets/', ''); + const fsPath = path.join(projectPath, 'assets', relativePath); + + console.log('File system path:', fsPath); + + // 检查文件是否存在 + if (!fs.existsSync(fsPath)) { + throw new Error('文件不存在'); + } + + // 检查文件是否为JSON格式 + let fileContent: any; + try { + const content = await fsExtra.readFile(fsPath, 'utf8'); + fileContent = JSON.parse(content); + } catch (parseError) { + throw new Error('文件不是有效的JSON格式'); + } + + // 验证是否为行为树文件 + if (fileContent.type !== 'behavior-tree' && !fileContent.tree) { + const confirm = await new Promise((resolve) => { + Editor.Dialog.warn('文件格式提醒', { + detail: '此文件可能不是标准的行为树配置文件,仍要打开吗?', + buttons: ['打开', '取消'], + }).then((result: any) => { + resolve(result.response === 0); + }); + }); + + if (!confirm) { + return; + } + } + + // 打开行为树编辑器面板 + Editor.Panel.open('cocos-ecs-extension.behavior-tree'); + + console.log(`Behavior tree file opened in editor: ${fsPath}`); + + } catch (error) { + console.error('Failed to open behavior tree file:', error); + Editor.Dialog.error('打开失败', { + detail: `打开行为树文件失败:\n\n${error instanceof Error ? error.message : String(error)}`, + }); + } + }, + + /** + * 从编辑器创建行为树文件 + */ + async 'create-behavior-tree-from-editor'(data: { fileName: string, content: string }) { + console.log('Creating behavior tree file from editor:', data.fileName); + + try { + const projectPath = Editor.Project.path; + const assetsPath = path.join(projectPath, 'assets'); + + // 确保文件名唯一 + let fileName = data.fileName; + let counter = 1; + let filePath = path.join(assetsPath, `${fileName}.bt.json`); + + while (fs.existsSync(filePath)) { + fileName = `${data.fileName}_${counter}`; + filePath = path.join(assetsPath, `${fileName}.bt.json`); + counter++; + } + + // 写入文件 + await fsExtra.writeFile(filePath, data.content); + + // 刷新资源管理器 + await Editor.Message.request('asset-db', 'refresh-asset', 'db://assets'); + + console.log(`Behavior tree file created from editor: ${filePath}`); + + Editor.Dialog.info('保存成功', { + detail: `行为树文件 "${fileName}.bt.json" 已保存到 assets 目录中!`, + }); + + } catch (error) { + console.error('Failed to create behavior tree file from editor:', error); + Editor.Dialog.error('保存失败', { + detail: `保存行为树文件失败:\n\n${error instanceof Error ? error.message : String(error)}`, + }); + } + }, }; /** diff --git a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useAppState.ts b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useAppState.ts index 450e7512..cf856e8d 100644 --- a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useAppState.ts +++ b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useAppState.ts @@ -44,7 +44,7 @@ export function useAppState() { // UI状态 const showExportModal = ref(false); - const exportFormat = ref('typescript'); + const exportFormat = ref('json'); // 工具函数 const getNodeByIdLocal = (id: string): TreeNode | undefined => { @@ -62,6 +62,17 @@ export function useAppState() { tempConnection.value.path = ''; }; + const updateCanvasSize = () => { + const canvasArea = document.querySelector('.canvas-area') as HTMLElement; + if (canvasArea) { + const rect = canvasArea.getBoundingClientRect(); + if (rect.width > 0 && rect.height > 0) { + canvasWidth.value = Math.max(rect.width, 800); + canvasHeight.value = Math.max(rect.height, 600); + } + } + }; + return { // 安装状态 checkingStatus, @@ -94,6 +105,7 @@ export function useAppState() { // 工具函数 getNodeByIdLocal, selectNode, - newBehaviorTree + newBehaviorTree, + updateCanvasSize }; } \ No newline at end of file diff --git a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useBehaviorTreeEditor.ts b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useBehaviorTreeEditor.ts index 90e7c4ce..6aa18ddf 100644 --- a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useBehaviorTreeEditor.ts +++ b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useBehaviorTreeEditor.ts @@ -5,6 +5,9 @@ import { useNodeOperations } from './useNodeOperations'; import { useCodeGeneration } from './useCodeGeneration'; import { useInstallation } from './useInstallation'; import { useFileOperations } from './useFileOperations'; +import { useConnectionManager } from './useConnectionManager'; +import { useCanvasManager } from './useCanvasManager'; +import { useNodeDisplay } from './useNodeDisplay'; /** * 主要的行为树编辑器组合功能 @@ -16,6 +19,23 @@ export function useBehaviorTreeEditor() { // 获取其他组合功能 const appState = useAppState(); + + // 临时根节点获取函数 + const getRootNode = () => { + return appState.treeNodes.value.find(node => + !appState.treeNodes.value.some(otherNode => + otherNode.children?.includes(node.id) + ) + ) || null; + }; + + const codeGen = useCodeGeneration( + appState.treeNodes, + appState.nodeTemplates, + appState.getNodeByIdLocal, + getRootNode + ); + const computedProps = useComputedProperties( appState.nodeTemplates, appState.nodeSearchText, @@ -29,8 +49,13 @@ export function useBehaviorTreeEditor() { appState.panX, appState.panY, appState.zoomLevel, - appState.getNodeByIdLocal + appState.getNodeByIdLocal, + { + generateConfigJSON: codeGen.generateConfigJSON, + generateTypeScriptCode: codeGen.generateTypeScriptCode + } ); + const nodeOps = useNodeOperations( appState.treeNodes, appState.selectedNodeId, @@ -38,629 +63,153 @@ export function useBehaviorTreeEditor() { appState.panX, appState.panY, appState.zoomLevel, - appState.getNodeByIdLocal - ); - const codeGen = useCodeGeneration( - appState.treeNodes, - appState.nodeTemplates, appState.getNodeByIdLocal, - () => computedProps.rootNode() || null + () => connectionManager.updateConnections() ); + const installation = useInstallation( appState.checkingStatus, appState.isInstalled, appState.version, appState.isInstalling ); + const fileOps = useFileOperations( appState.treeNodes, appState.selectedNodeId, appState.connections, appState.tempConnection, - appState.showExportModal + appState.showExportModal, + codeGen, + () => connectionManager.updateConnections() ); - // 连线状态管理 - 使用reactive代替复杂的状态管理 const connectionState = reactive({ isConnecting: false, startNodeId: null as string | null, startPortType: null as 'input' | 'output' | null, tempPath: '', - currentMousePos: { x: 0, y: 0 }, - hoveredPort: null as { nodeId: string, portType: string } | null + currentMousePos: null as { x: number, y: number } | null, + startPortPos: null as { x: number, y: number } | null, + hoveredPort: null as { nodeId: string, portType: 'input' | 'output' } | null }); - // 连线方法 - const startConnection = (event: MouseEvent, nodeId: string, portType: 'input' | 'output') => { - event.stopPropagation(); - event.preventDefault(); - - connectionState.isConnecting = true; - connectionState.startNodeId = nodeId; - connectionState.startPortType = portType; - - const startPos = getPortPosition(nodeId, portType); - if (startPos) { - connectionState.currentMousePos = { x: event.clientX, y: event.clientY }; - - (event.target as HTMLElement).setPointerCapture((event as any).pointerId || 1); - - document.addEventListener('pointermove', onConnectionDrag); - document.addEventListener('pointerup', onConnectionEnd); - document.addEventListener('pointercancel', onConnectionEnd); - } else { - cancelConnection(); - } - }; + const connectionManager = useConnectionManager( + appState.treeNodes, + appState.connections, + connectionState, + canvasAreaRef, + svgRef, + appState.panX, + appState.panY, + appState.zoomLevel + ); - const onConnectionDrag = (event: MouseEvent) => { - if (!connectionState.isConnecting || !connectionState.startNodeId) return; - - connectionState.currentMousePos = { x: event.clientX, y: event.clientY }; - - const svgPos = clientToSVGCoordinates(event.clientX, event.clientY); - const startNode = appState.treeNodes.value.find(n => n.id === connectionState.startNodeId); - - if (startNode && svgPos) { - const nodeWidth = 150; - const nodeHeight = 100; - - let startX: number, startY: number; - - if (connectionState.startPortType === 'output') { - startX = startNode.x + nodeWidth / 2; - startY = startNode.y + nodeHeight; - } else { - startX = startNode.x + nodeWidth / 2; - startY = startNode.y; - } - - const targetX = svgPos.x; - const targetY = svgPos.y; - const controlOffset = Math.abs(targetY - startY) * 0.5; - - let path: string; - if (connectionState.startPortType === 'output') { - path = `M ${startX} ${startY} C ${startX} ${startY + controlOffset} ${targetX} ${targetY - controlOffset} ${targetX} ${targetY}`; - } else { - path = `M ${startX} ${startY} C ${startX} ${startY - controlOffset} ${targetX} ${targetY + controlOffset} ${targetX} ${targetY}`; - } - - connectionState.tempPath = path; - } - }; + const canvasManager = useCanvasManager( + appState.panX, + appState.panY, + appState.zoomLevel, + appState.treeNodes, + appState.selectedNodeId, + canvasAreaRef, + connectionManager.updateConnections + ); - const onConnectionEnd = (event: MouseEvent) => { - if (!connectionState.isConnecting) return; - - const targetPort = findTargetPort(event.clientX, event.clientY); - - if (targetPort) { - const { nodeId: targetNodeId, portType: targetPortType } = targetPort; - - if (canConnect(connectionState.startNodeId!, connectionState.startPortType!, targetNodeId, targetPortType)) { - let parentId: string, childId: string; - - if (connectionState.startPortType === 'output') { - parentId = connectionState.startNodeId!; - childId = targetNodeId; - } else { - parentId = targetNodeId; - childId = connectionState.startNodeId!; - } - - createConnection(parentId, childId); - } - } - - cancelConnection(); - }; + const nodeDisplay = useNodeDisplay(); - const cancelConnection = () => { - connectionState.isConnecting = false; - connectionState.startNodeId = null; - connectionState.startPortType = null; - connectionState.tempPath = ''; - - // 移除全局事件监听器 - document.removeEventListener('pointermove', onConnectionDrag); - document.removeEventListener('pointerup', onConnectionEnd); - document.removeEventListener('pointercancel', onConnectionEnd); - }; + const dragState = reactive({ + isDragging: false, + dragNode: null as any, + dragElement: null as HTMLElement | null, + dragOffset: { x: 0, y: 0 }, + startPosition: { x: 0, y: 0 }, + updateCounter: 0 + }); - // 辅助函数:获取端口在SVG中的坐标(优化计算) - const getPortPosition = (nodeId: string, portType: 'input' | 'output') => { - const node = appState.treeNodes.value.find(n => n.id === nodeId); - if (!node) return null; - - // 使用与连线算法一致的计算方式 - const nodeWidth = 150; - const nodeHeight = 100; - const nodeX = node.x + nodeWidth / 2; // 节点中心X - - let nodeY: number; - if (portType === 'input') { - nodeY = node.y; // 输入端口在顶部 - } else { - nodeY = node.y + nodeHeight; // 输出端口在底部 - } - - return { x: nodeX, y: nodeY }; - }; - - // 辅助函数:将客户端坐标转换为SVG坐标 - const clientToSVGCoordinates = (clientX: number, clientY: number) => { - if (!svgRef.value) return null; - - const svg = svgRef.value as any; // 类型断言解决SVG方法问题 - const point = svg.createSVGPoint(); - point.x = clientX; - point.y = clientY; - - try { - const svgPoint = point.matrixTransform(svg.getScreenCTM()?.inverse()); - // 应用当前的缩放和平移 - return { - x: (svgPoint.x - appState.panX.value) / appState.zoomLevel.value, - y: (svgPoint.y - appState.panY.value) / appState.zoomLevel.value - }; - } catch (e) { - return null; - } - }; - - // 辅助函数:查找目标端口 - const findTargetPort = (clientX: number, clientY: number) => { - if (!canvasAreaRef.value) return null; - - // 方法1: 使用elementFromPoint - const elementAtPoint = document.elementFromPoint(clientX, clientY); - if (elementAtPoint?.classList.contains('port')) { - return getPortInfo(elementAtPoint as HTMLElement); - } - - // 方法2: 遍历所有端口,检查坐标 - const allPorts = canvasAreaRef.value.querySelectorAll('.port'); - for (const port of allPorts) { - const rect = port.getBoundingClientRect(); - const margin = 10; // 增加容错范围 - - if (clientX >= rect.left - margin && clientX <= rect.right + margin && - clientY >= rect.top - margin && clientY <= rect.bottom + margin) { - return getPortInfo(port as HTMLElement); - } - } - - return null; - }; - - // 辅助函数:从端口元素获取端口信息 - const getPortInfo = (portElement: HTMLElement) => { - const nodeElement = portElement.closest('.tree-node'); - if (!nodeElement) return null; - - const nodeId = nodeElement.getAttribute('data-node-id'); - const portType = portElement.classList.contains('port-input') ? 'input' : 'output'; - - return nodeId ? { nodeId, portType } : null; - }; - - // 端口悬停处理 - const onPortHover = (nodeId: string, portType: 'input' | 'output') => { - if (connectionState.isConnecting && connectionState.startNodeId !== nodeId) { - connectionState.hoveredPort = { nodeId, portType }; - - // 检查是否可以连接 - if (canConnect(connectionState.startNodeId!, connectionState.startPortType!, nodeId, portType)) { - // 添加视觉反馈 - const portElement = document.querySelector(`[data-node-id="${nodeId}"] .port-${portType}`); - if (portElement) { - portElement.classList.add('drag-target'); - } - } - } - }; - - const onPortLeave = () => { - if (connectionState.isConnecting) { - connectionState.hoveredPort = null; - - // 移除所有drag-target类 - const allPorts = document.querySelectorAll('.port.drag-target'); - allPorts.forEach(port => port.classList.remove('drag-target')); - } - }; - - // 验证连接目标是否有效 - 排除自己的节点 - const isValidConnectionTarget = (nodeId: string, portType: 'input' | 'output') => { - if (!connectionState.isConnecting || !connectionState.startNodeId || connectionState.startNodeId === nodeId) { - return false; - } - - return canConnect(connectionState.startNodeId, connectionState.startPortType!, nodeId, portType); - }; - - const canConnect = (sourceNodeId: string, sourcePortType: string, targetNodeId: string, targetPortType: string) => { - if (sourceNodeId === targetNodeId) { - return false; - } - - if (sourcePortType === targetPortType) { - return false; - } - - let parentNodeId: string, childNodeId: string; - - if (sourcePortType === 'output') { - parentNodeId = sourceNodeId; - childNodeId = targetNodeId; - } else { - parentNodeId = targetNodeId; - childNodeId = sourceNodeId; - } - - const childNode = appState.treeNodes.value.find((n: any) => n.id === childNodeId); - if (childNode && childNode.parent && childNode.parent !== parentNodeId) { - return false; - } - - const parentNode = appState.treeNodes.value.find((n: any) => n.id === parentNodeId); - if (!parentNode || !parentNode.canHaveChildren) { - return false; - } - - if (!childNode || !childNode.canHaveParent) { - return false; - } - - if (wouldCreateCycle(parentNodeId, childNodeId)) { - return false; - } - - if (isDescendant(childNodeId, parentNodeId)) { - return false; - } - - return true; - }; - - const wouldCreateCycle = (parentId: string, childId: string) => { - return isDescendant(parentId, childId); - }; - - const isDescendant = (ancestorId: string, descendantId: string): boolean => { - const visited = new Set(); - - function checkPath(currentId: string): boolean { - if (currentId === ancestorId) return true; - if (visited.has(currentId)) return false; - - visited.add(currentId); - - const currentNode = appState.treeNodes.value.find((n: any) => n.id === currentId); - if (currentNode?.children) { - for (const childId of currentNode.children) { - if (checkPath(childId)) return true; - } - } - - return false; - } - - return checkPath(descendantId); - }; - - const getAncestors = (nodeId: string): string[] => { - const ancestors: string[] = []; - let currentNode = appState.treeNodes.value.find((n: any) => n.id === nodeId); - - while (currentNode && currentNode.parent) { - ancestors.push(currentNode.parent); - const parentId = currentNode.parent; - currentNode = appState.treeNodes.value.find((n: any) => n.id === parentId); - - if (ancestors.length > 100) break; - } - - return ancestors; - }; - - const getDescendants = (nodeId: string): string[] => { - const descendants: string[] = []; - const visited = new Set(); - - function collectDescendants(currentId: string) { - if (visited.has(currentId)) return; - visited.add(currentId); - - const currentNode = appState.treeNodes.value.find((n: any) => n.id === currentId); - if (currentNode?.children) { - for (const childId of currentNode.children) { - descendants.push(childId); - collectDescendants(childId); - } - } - } - - collectDescendants(nodeId); - return descendants; - }; - - // 创建连接(支持双向连接) - const createConnection = (parentId: string, childId: string) => { - const parentNode = appState.treeNodes.value.find((n: any) => n.id === parentId); - const childNode = appState.treeNodes.value.find((n: any) => n.id === childId); - - if (parentNode && childNode) { - // 移除子节点之前的父节点关系 - if (childNode.parent) { - const oldParent = appState.treeNodes.value.find((n: any) => n.id === childNode.parent); - if (oldParent && oldParent.children) { - oldParent.children = oldParent.children.filter((id: string) => id !== childId); - } - } - - // 移除可能的重复连接 - appState.treeNodes.value.forEach((node: any) => { - if (node.children) { - node.children = node.children.filter((id: string) => !(node.id === parentId && id === childId)); - } - }); - - // 添加新的父子关系 - if (!parentNode.children) { - parentNode.children = []; - } - if (!parentNode.children.includes(childId)) { - parentNode.children.push(childId); - } - - // 设置子节点的父节点引用 - childNode.parent = parentId; - - // 更新连接线 - updateConnections(); - } - }; - - const updateConnections = () => { - appState.connections.value.length = 0; - - appState.treeNodes.value.forEach((node: any) => { - if (node.children) { - node.children.forEach((childId: string) => { - const childNode = appState.treeNodes.value.find((n: any) => n.id === childId); - if (childNode) { - const parentPos = getPortPosition(node.id, 'output'); - const childPos = getPortPosition(childId, 'input'); - - if (parentPos && childPos) { - // 使用与临时连线相同的贝塞尔曲线算法 - const controlOffset = Math.abs(childPos.y - parentPos.y) * 0.5; - const path = `M ${parentPos.x} ${parentPos.y} C ${parentPos.x} ${parentPos.y + controlOffset} ${childPos.x} ${childPos.y - controlOffset} ${childPos.x} ${childPos.y}`; - - appState.connections.value.push({ - id: `${node.id}-${childId}`, - sourceId: node.id, - targetId: childId, - path: path, - active: false - }); - } - } - }); - } - }); - }; - - // 节点拖拽功能(移除防抖,实时更新) const startNodeDrag = (event: MouseEvent, node: any) => { - // 阻止默认行为 - event.preventDefault(); event.stopPropagation(); + event.preventDefault(); - // 设置拖拽状态 - appState.dragState.value.isDraggingNode = true; - appState.dragState.value.dragNodeId = node.id; - appState.dragState.value.dragStartX = event.clientX; - appState.dragState.value.dragStartY = event.clientY; - appState.dragState.value.dragNodeStartX = node.x; - appState.dragState.value.dragNodeStartY = node.y; + dragState.isDragging = true; + dragState.dragNode = node; + dragState.startPosition = { x: event.clientX, y: event.clientY }; - // 添加dragging类提升性能 - const nodeElement = event.currentTarget as HTMLElement; - nodeElement.classList.add('dragging'); + dragState.dragElement = document.querySelector(`[data-node-id="${node.id}"]`) as HTMLElement; + if (dragState.dragElement) { + dragState.dragElement.classList.add('dragging'); + } + + dragState.dragOffset = { + x: node.x, + y: node.y + }; - // 添加全局事件监听(移除passive优化,确保实时性) document.addEventListener('mousemove', onNodeDrag); document.addEventListener('mouseup', onNodeDragEnd); }; const onNodeDrag = (event: MouseEvent) => { - if (!appState.dragState.value.isDraggingNode || !appState.dragState.value.dragNodeId) return; + if (!dragState.isDragging || !dragState.dragNode) return; - const deltaX = (event.clientX - appState.dragState.value.dragStartX) / appState.zoomLevel.value; - const deltaY = (event.clientY - appState.dragState.value.dragStartY) / appState.zoomLevel.value; + const deltaX = (event.clientX - dragState.startPosition.x) / appState.zoomLevel.value; + const deltaY = (event.clientY - dragState.startPosition.y) / appState.zoomLevel.value; - const node = appState.treeNodes.value.find((n: any) => n.id === appState.dragState.value.dragNodeId); - if (node) { - node.x = appState.dragState.value.dragNodeStartX + deltaX; - node.y = appState.dragState.value.dragNodeStartY + deltaY; - - // 立即更新连接线,无防抖 - updateConnections(); - } + dragState.dragNode.x = dragState.dragOffset.x + deltaX; + dragState.dragNode.y = dragState.dragOffset.y + deltaY; + + connectionManager.updateConnections(); }; const onNodeDragEnd = (event: MouseEvent) => { - if (appState.dragState.value.isDraggingNode) { - // 移除dragging类 - const draggingNodes = document.querySelectorAll('.tree-node.dragging'); - draggingNodes.forEach(node => node.classList.remove('dragging')); - - appState.dragState.value.isDraggingNode = false; - appState.dragState.value.dragNodeId = null; - - // 最终更新连接线 - updateConnections(); - - // 移除全局事件监听 - document.removeEventListener('mousemove', onNodeDrag); - document.removeEventListener('mouseup', onNodeDragEnd); - } - }; - - // 画布操作功能 - const onCanvasWheel = (event: WheelEvent) => { - event.preventDefault(); + if (!dragState.isDragging) return; - const zoomSpeed = 0.1; - const delta = event.deltaY > 0 ? -zoomSpeed : zoomSpeed; - const newZoom = Math.max(0.1, Math.min(3, appState.zoomLevel.value + delta)); + if (dragState.dragElement) { + dragState.dragElement.classList.remove('dragging'); + } - appState.zoomLevel.value = newZoom; + dragState.isDragging = false; + dragState.dragNode = null; + dragState.dragElement = null; + + document.removeEventListener('mousemove', onNodeDrag); + document.removeEventListener('mouseup', onNodeDragEnd); + + connectionManager.updateConnections(); + dragState.updateCounter = 0; }; - const onCanvasMouseDown = (event: MouseEvent) => { - // 只在空白区域开始画布拖拽 - if (event.target === event.currentTarget) { - appState.dragState.value.isDraggingCanvas = true; - appState.dragState.value.dragStartX = event.clientX; - appState.dragState.value.dragStartY = event.clientY; - - document.addEventListener('mousemove', onCanvasMouseMove); - document.addEventListener('mouseup', onCanvasMouseUp); - } - }; - - const onCanvasMouseMove = (event: MouseEvent) => { - if (appState.dragState.value.isDraggingCanvas) { - const deltaX = event.clientX - appState.dragState.value.dragStartX; - const deltaY = event.clientY - appState.dragState.value.dragStartY; - - appState.panX.value += deltaX; - appState.panY.value += deltaY; - - appState.dragState.value.dragStartX = event.clientX; - appState.dragState.value.dragStartY = event.clientY; - } - }; - - const onCanvasMouseUp = (event: MouseEvent) => { - if (appState.dragState.value.isDraggingCanvas) { - appState.dragState.value.isDraggingCanvas = false; - - document.removeEventListener('mousemove', onCanvasMouseMove); - document.removeEventListener('mouseup', onCanvasMouseUp); - } - }; - - // 缩放控制 - const zoomIn = () => { - appState.zoomLevel.value = Math.min(3, appState.zoomLevel.value + 0.1); - }; - - const zoomOut = () => { - appState.zoomLevel.value = Math.max(0.1, appState.zoomLevel.value - 0.1); - }; - - const resetZoom = () => { - appState.zoomLevel.value = 1; - }; - - const centerView = () => { - appState.panX.value = 0; - appState.panY.value = 0; - }; - - // 安装处理 const handleInstall = () => { - // 这里应该调用installation中的安装方法 + installation.handleInstall(); }; - // 生命周期管理 + // 组件挂载时初始化连接 onMounted(() => { - // 初始化连接线 + // 延迟一下确保 DOM 已经渲染 nextTick(() => { - updateConnections(); + connectionManager.updateConnections(); }); }); onUnmounted(() => { - // 清理事件监听器 - cancelConnection(); document.removeEventListener('mousemove', onNodeDrag); document.removeEventListener('mouseup', onNodeDragEnd); - document.removeEventListener('mousemove', onCanvasMouseMove); - document.removeEventListener('mouseup', onCanvasMouseUp); }); - // 解构出所有需要的方法,避免命名冲突 - const { - filteredCompositeNodes, - filteredDecoratorNodes, - filteredActionNodes, - filteredConditionNodes, - filteredECSNodes, - selectedNode, - rootNode, - installStatusClass, - installStatusText, - validationResult, - exportedCode, - gridStyle - } = computedProps; - return { - // DOM refs canvasAreaRef, svgRef, - - // 状态 ...appState, - connectionState, - - // 计算属性 - 显式导出,避免命名冲突 - filteredCompositeNodes, - filteredDecoratorNodes, - filteredActionNodes, - filteredConditionNodes, - filteredECSNodes, - selectedNode, - rootNode, - installStatusClass, - installStatusText, - validationResult, - exportedCode, - gridStyle, - - // 连线方法 - startConnection, - cancelConnection, - updateConnections, - onPortHover, - onPortLeave, - isValidConnectionTarget, - - // 节点拖拽 - startNodeDrag, - - // 画布操作 - onCanvasWheel, - onCanvasMouseDown, - onCanvasMouseMove, - onCanvasMouseUp, - - // 缩放控制 - zoomIn, - zoomOut, - resetZoom, - centerView, - - // 其他功能方法 + ...computedProps, ...nodeOps, + ...fileOps, ...codeGen, ...installation, - ...fileOps, + handleInstall, + connectionState, + ...connectionManager, + ...canvasManager, + ...nodeDisplay, + startNodeDrag, + dragState }; } \ No newline at end of file diff --git a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useCanvasManager.ts b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useCanvasManager.ts new file mode 100644 index 00000000..f9c47d0b --- /dev/null +++ b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useCanvasManager.ts @@ -0,0 +1,203 @@ +import { Ref, ref } from 'vue'; +import { TreeNode, DragState } from '../types'; + +/** + * 画布管理功能 + */ +export function useCanvasManager( + panX: Ref, + panY: Ref, + zoomLevel: Ref, + treeNodes: Ref, + selectedNodeId: Ref, + canvasAreaRef: Ref, + updateConnections: () => void +) { + // 画布尺寸 - 使用默认值或从DOM获取 + const canvasWidth = ref(800); + const canvasHeight = ref(600); + + // 拖拽状态 + const dragState = ref({ + isDraggingCanvas: false, + isDraggingNode: false, + dragNodeId: null, + dragStartX: 0, + dragStartY: 0, + dragNodeStartX: 0, + dragNodeStartY: 0, + isConnecting: false, + connectionStart: null, + connectionEnd: { x: 0, y: 0 } + }); + + // 如果有canvas引用,更新尺寸 + if (canvasAreaRef.value) { + const rect = canvasAreaRef.value.getBoundingClientRect(); + canvasWidth.value = rect.width; + canvasHeight.value = rect.height; + } + + // 画布操作功能 + const onCanvasWheel = (event: WheelEvent) => { + event.preventDefault(); + + const zoomSpeed = 0.1; + const delta = event.deltaY > 0 ? -zoomSpeed : zoomSpeed; + const newZoom = Math.max(0.1, Math.min(3, zoomLevel.value + delta)); + + zoomLevel.value = newZoom; + }; + + const onCanvasMouseDown = (event: MouseEvent) => { + // 只在空白区域开始画布拖拽 + if (event.target === event.currentTarget) { + dragState.value.isDraggingCanvas = true; + dragState.value.dragStartX = event.clientX; + dragState.value.dragStartY = event.clientY; + + document.addEventListener('mousemove', onCanvasMouseMove); + document.addEventListener('mouseup', onCanvasMouseUp); + } + }; + + const onCanvasMouseMove = (event: MouseEvent) => { + if (dragState.value.isDraggingCanvas) { + const deltaX = event.clientX - dragState.value.dragStartX; + const deltaY = event.clientY - dragState.value.dragStartY; + + panX.value += deltaX; + panY.value += deltaY; + + dragState.value.dragStartX = event.clientX; + dragState.value.dragStartY = event.clientY; + } + }; + + const onCanvasMouseUp = (event: MouseEvent) => { + if (dragState.value.isDraggingCanvas) { + dragState.value.isDraggingCanvas = false; + + document.removeEventListener('mousemove', onCanvasMouseMove); + document.removeEventListener('mouseup', onCanvasMouseUp); + } + }; + + // 缩放控制 + const zoomIn = () => { + zoomLevel.value = Math.min(3, zoomLevel.value + 0.1); + }; + + const zoomOut = () => { + zoomLevel.value = Math.max(0.1, zoomLevel.value - 0.1); + }; + + const resetZoom = () => { + zoomLevel.value = 1; + }; + + const centerView = () => { + if (treeNodes.value.length === 0) { + panX.value = 0; + panY.value = 0; + return; + } + + let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; + + treeNodes.value.forEach(node => { + // 尝试从DOM获取实际节点尺寸,否则使用默认值 + const nodeElement = document.querySelector(`[data-node-id="${node.id}"]`); + let nodeWidth = 150; + let nodeHeight = 80; // 使用基础高度 + + if (nodeElement) { + const rect = nodeElement.getBoundingClientRect(); + nodeWidth = rect.width / zoomLevel.value; + nodeHeight = rect.height / zoomLevel.value; + } + + minX = Math.min(minX, node.x); + minY = Math.min(minY, node.y); + maxX = Math.max(maxX, node.x + nodeWidth); + maxY = Math.max(maxY, node.y + nodeHeight); + }); + + const centerX = (minX + maxX) / 2; + const centerY = (minY + maxY) / 2; + + panX.value = canvasWidth.value / 2 - centerX * zoomLevel.value; + panY.value = canvasHeight.value / 2 - centerY * zoomLevel.value; + }; + + // 网格样式计算 + const gridStyle = () => { + const gridSize = 20 * zoomLevel.value; + return { + backgroundSize: `${gridSize}px ${gridSize}px`, + backgroundPosition: `${panX.value % gridSize}px ${panY.value % gridSize}px` + }; + }; + + // 节点拖拽功能 + const startNodeDrag = (event: MouseEvent, node: any) => { + event.preventDefault(); + event.stopPropagation(); + + dragState.value.isDraggingNode = true; + dragState.value.dragNodeId = node.id; + dragState.value.dragStartX = event.clientX; + dragState.value.dragStartY = event.clientY; + dragState.value.dragNodeStartX = node.x; + dragState.value.dragNodeStartY = node.y; + + const nodeElement = event.currentTarget as HTMLElement; + nodeElement.classList.add('dragging'); + + document.addEventListener('mousemove', onNodeDrag); + document.addEventListener('mouseup', onNodeDragEnd); + }; + + const onNodeDrag = (event: MouseEvent) => { + if (!dragState.value.isDraggingNode || !dragState.value.dragNodeId) return; + + const deltaX = (event.clientX - dragState.value.dragStartX) / zoomLevel.value; + const deltaY = (event.clientY - dragState.value.dragStartY) / zoomLevel.value; + + const node = treeNodes.value.find(n => n.id === dragState.value.dragNodeId); + if (node) { + node.x = dragState.value.dragNodeStartX + deltaX; + node.y = dragState.value.dragNodeStartY + deltaY; + + updateConnections(); + } + }; + + const onNodeDragEnd = (event: MouseEvent) => { + if (dragState.value.isDraggingNode) { + const draggingNodes = document.querySelectorAll('.tree-node.dragging'); + draggingNodes.forEach(node => node.classList.remove('dragging')); + + dragState.value.isDraggingNode = false; + dragState.value.dragNodeId = null; + + updateConnections(); + + document.removeEventListener('mousemove', onNodeDrag); + document.removeEventListener('mouseup', onNodeDragEnd); + } + }; + + return { + onCanvasWheel, + onCanvasMouseDown, + onCanvasMouseMove, + onCanvasMouseUp, + zoomIn, + zoomOut, + resetZoom, + centerView, + gridStyle, + startNodeDrag + }; +} \ No newline at end of file diff --git a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useCodeGeneration.ts b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useCodeGeneration.ts index f727e5b5..1b163b5b 100644 --- a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useCodeGeneration.ts +++ b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useCodeGeneration.ts @@ -12,97 +12,329 @@ export function useCodeGeneration( rootNode: () => TreeNode | null ) { - // TypeScript代码生成 - const generateTypeScriptCode = (): string => { - const imports = getRequiredImports(); + // 生成行为树配置JSON + const generateBehaviorTreeConfig = () => { const root = rootNode(); if (!root) { + return null; + } + + return { + version: "1.0.0", + type: "behavior-tree", + metadata: { + createdAt: new Date().toISOString(), + hasECSNodes: hasECSNodes(), + nodeCount: treeNodes.value.length + }, + tree: generateNodeConfig(root) + }; + }; + + // 生成可读的配置JSON字符串 + const generateConfigJSON = (): string => { + const config = generateBehaviorTreeConfig(); + + if (!config) { return '// 请先添加根节点'; } - const importsCode = imports.map(imp => `import { ${imp} } from '@esengine/ai';`).join('\n'); - const treeCode = generateNodeCode(root); - - return `${importsCode} - -// 自动生成的行为树代码 -export function createBehaviorTree() { - return ${treeCode}; -}`; + return JSON.stringify(config, null, 2); }; - const getRequiredImports = (): string[] => { - const imports = new Set(); + // 生成TypeScript构建代码(用于运行时从配置创建行为树) + const generateTypeScriptCode = (): string => { + const config = generateBehaviorTreeConfig(); + + if (!config) { + return '// 请先添加根节点'; + } + + const { behaviorTreeImports, ecsImports } = getRequiredImports(); + + let importsCode = ''; + if (behaviorTreeImports.length > 0) { + importsCode += `import { ${behaviorTreeImports.join(', ')}, BehaviorTreeBuilder } from '@esengine/ai';\n`; + } + if (ecsImports.length > 0) { + importsCode += `import { ${ecsImports.join(', ')} } from '@esengine/ecs-framework';\n`; + } + + const contextType = hasECSNodes() ? 'Entity' : 'any'; + const configString = JSON.stringify(config, null, 4); + + return `${importsCode} +// 行为树配置 +const behaviorTreeConfig = ${configString}; + +// 从配置创建行为树 +export function createBehaviorTree(context?: T): BehaviorTree { + return BehaviorTreeBuilder.fromConfig(behaviorTreeConfig, context); +} + +// 直接导出配置(用于序列化保存) +export const config = behaviorTreeConfig;`; + }; + + const getRequiredImports = (): { behaviorTreeImports: string[], ecsImports: string[] } => { + const behaviorTreeImports = new Set(); + const ecsImports = new Set(); + + // 总是需要这些基础类 + behaviorTreeImports.add('BehaviorTree'); + behaviorTreeImports.add('TaskStatus'); treeNodes.value.forEach(node => { const template = nodeTemplates.value.find(t => t.className === node.type || t.type === node.type); if (template?.className) { - imports.add(template.className); + if (template.namespace?.includes('ecs-integration')) { + behaviorTreeImports.add(template.className); + ecsImports.add('Entity'); + ecsImports.add('Component'); + } else { + behaviorTreeImports.add(template.className); + } } }); - return Array.from(imports); + return { + behaviorTreeImports: Array.from(behaviorTreeImports), + ecsImports: Array.from(ecsImports) + }; + }; + + const hasECSNodes = (): boolean => { + return treeNodes.value.some(node => { + const template = nodeTemplates.value.find(t => t.className === node.type || t.type === node.type); + return template?.namespace?.includes('ecs-integration'); + }); + }; + + // 生成节点配置对象 + const generateNodeConfig = (node: TreeNode): any => { + const template = nodeTemplates.value.find(t => t.className === node.type || t.type === node.type); + + if (!template || !template.className) { + return { + type: node.type, + error: "未知节点类型" + }; + } + + const nodeConfig: any = { + id: node.id, + type: template.className, + namespace: template.namespace || 'behaviourTree', + properties: {} + }; + + // 处理节点属性 + if (node.properties) { + Object.entries(node.properties).forEach(([key, prop]) => { + if (prop.value !== undefined && prop.value !== '') { + nodeConfig.properties[key] = { + type: prop.type, + value: prop.value + }; + } + }); + } + + // 处理子节点 + if (node.children && node.children.length > 0) { + nodeConfig.children = node.children + .map(childId => getNodeByIdLocal(childId)) + .filter(Boolean) + .map(child => generateNodeConfig(child!)); + } + + return nodeConfig; }; const generateNodeCode = (node: TreeNode, indent: number = 0): string => { const spaces = ' '.repeat(indent); - const template = nodeTemplates.value.find(t => t.className === node.type); + const template = nodeTemplates.value.find(t => t.className === node.type || t.type === node.type); - if (!template) { + if (!template || !template.className) { return `${spaces}// 未知节点类型: ${node.type}`; } let code = `${spaces}new ${template.className}(`; - - // 构造函数参数 const params: string[] = []; - // 处理属性 - if (node.properties && Object.keys(node.properties).length > 0) { - const propsCode: string[] = []; - - Object.entries(node.properties).forEach(([key, prop]) => { - if (prop.type === 'code' && prop.value) { - propsCode.push(`${key}: ${prop.value}`); - } else if (prop.type === 'string' && prop.value !== undefined) { - propsCode.push(`${key}: "${prop.value}"`); - } else if (prop.type === 'number' && prop.value !== undefined) { - propsCode.push(`${key}: ${prop.value}`); - } else if (prop.type === 'boolean' && prop.value !== undefined) { - propsCode.push(`${key}: ${prop.value}`); - } else if (prop.type === 'select' && prop.value !== undefined) { - propsCode.push(`${key}: "${prop.value}"`); - } - }); - - if (propsCode.length > 0) { - params.push(`{\n${spaces} ${propsCode.join(',\n' + spaces + ' ')}\n${spaces}}`); + // 处理特定节点的构造函数参数 + if (template.namespace?.includes('ecs-integration')) { + // ECS节点的特殊处理 + switch (template.className) { + case 'HasComponentCondition': + case 'AddComponentAction': + case 'RemoveComponentAction': + case 'ModifyComponentAction': + if (node.properties?.componentType?.value) { + params.push(node.properties.componentType.value); + } + if (template.className === 'AddComponentAction' && node.properties?.componentFactory?.value) { + params.push(node.properties.componentFactory.value); + } + if (template.className === 'ModifyComponentAction' && node.properties?.modifierCode?.value) { + params.push(node.properties.modifierCode.value); + } + break; + case 'HasTagCondition': + if (node.properties?.tag?.value !== undefined) { + params.push(node.properties.tag.value.toString()); + } + break; + case 'IsActiveCondition': + if (node.properties?.checkHierarchy?.value !== undefined) { + params.push(node.properties.checkHierarchy.value.toString()); + } + break; + case 'WaitTimeAction': + if (node.properties?.waitTime?.value !== undefined) { + params.push(node.properties.waitTime.value.toString()); + } + break; + } + } else { + // 普通行为树节点的处理 + switch (template.className) { + case 'ExecuteAction': + case 'ExecuteActionConditional': + if (node.properties?.actionCode?.value || node.properties?.conditionCode?.value) { + const code = node.properties.actionCode?.value || node.properties.conditionCode?.value; + params.push(code); + if (node.properties?.actionName?.value) { + params.push(`{ name: "${node.properties.actionName.value}" }`); + } + } + break; + case 'WaitAction': + if (node.properties?.waitTime?.value !== undefined) { + params.push(node.properties.waitTime.value.toString()); + } + break; + case 'LogAction': + if (node.properties?.message?.value) { + params.push(`"${node.properties.message.value}"`); + } + break; + case 'Repeater': + if (node.properties?.repeatCount?.value !== undefined) { + params.push(node.properties.repeatCount.value.toString()); + } + break; + case 'Sequence': + case 'Selector': + if (node.properties?.abortType?.value && node.properties.abortType.value !== 'None') { + params.push(`AbortTypes.${node.properties.abortType.value}`); + } + break; } } code += params.join(', '); + code += ')'; - // 子节点 - if (node.children && node.children.length > 0) { + // 处理子节点(对于复合节点和装饰器) + if (template.canHaveChildren && node.children && node.children.length > 0) { const children = node.children .map(childId => getNodeByIdLocal(childId)) .filter(Boolean) .map(child => generateNodeCode(child!, indent + 1)); if (children.length > 0) { - if (params.length > 0) code += ', '; - code += '[\n' + children.join(',\n') + '\n' + spaces + ']'; + const className = template.className; // 保存到局部变量 + if (template.category === 'decorator') { + // 装饰器只有一个子节点 + code = code.slice(0, -1); // 移除最后的 ')' + const varName = className.toLowerCase(); + code += `;\n${spaces}${varName}.child = ${children[0].trim()};\n${spaces}return ${varName}`; + } else if (template.category === 'composite') { + // 复合节点需要添加子节点 + code = code.slice(0, -1); // 移除最后的 ')' + code += `;\n`; + children.forEach(child => { + code += `${spaces}${className.toLowerCase()}.addChild(${child.trim()});\n`; + }); + code += `${spaces}return ${className.toLowerCase()}`; + } } } - code += ')'; return code; }; + // 从配置创建行为树节点 + const createTreeFromConfig = (config: any): TreeNode[] => { + if (!config || !config.tree) { + return []; + } + + const nodes: TreeNode[] = []; + const processNode = (nodeConfig: any, parent?: TreeNode): TreeNode => { + const template = nodeTemplates.value.find(t => t.className === nodeConfig.type); + if (!template) { + throw new Error(`未知节点类型: ${nodeConfig.type}`); + } + + const node: TreeNode = { + id: nodeConfig.id || generateNodeId(), + type: template.type, + name: template.name, + icon: template.icon, + description: template.description, + canHaveChildren: template.canHaveChildren, + canHaveParent: template.canHaveParent, + x: 400, // 默认在画布中心 + y: 100, // 从顶部开始 + properties: {}, + children: [], + parent: parent?.id // 设置父节点ID + }; + + // 恢复属性 + if (nodeConfig.properties) { + Object.entries(nodeConfig.properties).forEach(([key, propConfig]: [string, any]) => { + if (template.properties?.[key]) { + node.properties![key] = { + ...template.properties[key], + value: propConfig.value + }; + } + }); + } + + nodes.push(node); + + // 处理子节点 + if (nodeConfig.children && Array.isArray(nodeConfig.children)) { + nodeConfig.children.forEach((childConfig: any) => { + const childNode = processNode(childConfig, node); + node.children!.push(childNode.id); + }); + } + + return node; + }; + + processNode(config.tree); + return nodes; + }; + + // 生成唯一节点ID + const generateNodeId = (): string => { + return 'node_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); + }; + return { + generateBehaviorTreeConfig, + generateConfigJSON, generateTypeScriptCode, generateNodeCode, + generateNodeConfig, + createTreeFromConfig, getRequiredImports }; } \ No newline at end of file diff --git a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useComputedProperties.ts b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useComputedProperties.ts index 33fc2bf0..225999eb 100644 --- a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useComputedProperties.ts +++ b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useComputedProperties.ts @@ -1,9 +1,8 @@ -import { Ref } from 'vue'; +import { Ref, computed } from 'vue'; import { TreeNode } from '../types'; import { NodeTemplate } from '../data/nodeTemplates'; import { getRootNode } from '../utils/nodeUtils'; import { getInstallStatusText, getInstallStatusClass } from '../utils/installUtils'; -import { generateCode } from '../utils/codeGenerator'; import { getGridStyle } from '../utils/canvasUtils'; /** @@ -22,7 +21,11 @@ export function useComputedProperties( panX: Ref, panY: Ref, zoomLevel: Ref, - getNodeByIdLocal: (id: string) => TreeNode | undefined + getNodeByIdLocal: (id: string) => TreeNode | undefined, + codeGeneration?: { + generateConfigJSON: () => string; + generateTypeScriptCode: () => string; + } ) { // 过滤节点 const filteredCompositeNodes = () => { @@ -60,10 +63,14 @@ export function useComputedProperties( ); }; - // 选中的节点 - const selectedNode = () => { - return selectedNodeId.value ? getNodeByIdLocal(selectedNodeId.value) : null; - }; + // 选中的节点 - 使用computed确保响应式更新 + const selectedNode = computed(() => { + if (!selectedNodeId.value) return null; + + // 直接从treeNodes数组中查找,确保获取最新的节点状态 + const node = treeNodes.value.find(n => n.id === selectedNodeId.value); + return node || null; + }); // 根节点 const rootNode = () => { @@ -98,8 +105,16 @@ export function useComputedProperties( // 导出代码 const exportedCode = () => { + if (!codeGeneration) { + return '// 代码生成器未初始化'; + } + try { - return generateCode(treeNodes.value, exportFormat.value); + if (exportFormat.value === 'json') { + return codeGeneration.generateConfigJSON(); + } else { + return codeGeneration.generateTypeScriptCode(); + } } catch (error) { return `// 代码生成失败: ${error}`; } diff --git a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useConnectionManager.ts b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useConnectionManager.ts new file mode 100644 index 00000000..0a7c5386 --- /dev/null +++ b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useConnectionManager.ts @@ -0,0 +1,485 @@ +import { Ref } from 'vue'; +import { TreeNode, Connection, ConnectionState } from '../types'; + +/** + * 连接线管理功能 + */ +export function useConnectionManager( + treeNodes: Ref, + connections: Ref, + connectionState: ConnectionState, + canvasAreaRef: Ref, + svgRef: Ref, + panX: Ref, + panY: Ref, + zoomLevel: Ref +) { + + const getPortPosition = (nodeId: string, portType: 'input' | 'output') => { + const node = treeNodes.value.find(n => n.id === nodeId); + if (!node) return null; + + const canvasArea = canvasAreaRef.value; + if (!canvasArea) { + return getCalculatedPortPosition(node, portType); + } + + const selectors = [ + `[data-node-id="${nodeId}"]`, + `.tree-node[data-node-id="${nodeId}"]`, + `div[data-node-id="${nodeId}"]` + ]; + + let nodeElement: HTMLElement | null = null; + + for (const selector of selectors) { + try { + const doc = canvasArea.ownerDocument || document; + const foundElement = doc.querySelector(selector); + if (foundElement && canvasArea.contains(foundElement)) { + nodeElement = foundElement as HTMLElement; + break; + } + } catch (error) { + continue; + } + } + + if (!nodeElement) { + try { + const allTreeNodes = canvasArea.querySelectorAll('.tree-node'); + for (let i = 0; i < allTreeNodes.length; i++) { + const el = allTreeNodes[i] as HTMLElement; + const dataNodeId = el.getAttribute('data-node-id'); + if (dataNodeId === nodeId) { + nodeElement = el; + break; + } + } + } catch (error) { + // Fallback to calculated position + } + } + + if (!nodeElement) { + return getCalculatedPortPosition(node, portType); + } + + const portSelectors = [ + `.port.port-${portType}`, + `.port-${portType}`, + `.port.${portType}`, + `.${portType}-port` + ]; + + let portElement: HTMLElement | null = null; + + for (const portSelector of portSelectors) { + try { + portElement = nodeElement.querySelector(portSelector) as HTMLElement; + if (portElement) { + break; + } + } catch (error) { + continue; + } + } + + if (!portElement) { + return getNodeEdgePortPosition(nodeElement, node, portType); + } + + const portRect = portElement.getBoundingClientRect(); + const canvasRect = canvasAreaRef.value?.getBoundingClientRect(); + + if (!canvasRect) { + return getCalculatedPortPosition(node, portType); + } + + const relativeX = portRect.left + portRect.width / 2 - canvasRect.left; + const relativeY = portRect.top + portRect.height / 2 - canvasRect.top; + + const svgX = (relativeX - panX.value) / zoomLevel.value; + const svgY = (relativeY - panY.value) / zoomLevel.value; + + return { x: svgX, y: svgY }; + }; + + const getCalculatedPortPosition = (node: any, portType: 'input' | 'output') => { + let nodeWidth = 150; + let nodeHeight = 80; + + if (node.properties) { + const propertyCount = Object.keys(node.properties).length; + if (propertyCount > 0) { + nodeHeight += propertyCount * 20 + 20; + nodeWidth = Math.max(150, nodeWidth + 50); + } + } + + const portX = node.x + nodeWidth / 2; + const portY = portType === 'input' + ? node.y - 8 + : node.y + nodeHeight + 8; + + return { x: portX, y: portY }; + }; + + const getNodeEdgePortPosition = (nodeElement: HTMLElement, node: any, portType: 'input' | 'output') => { + const nodeRect = nodeElement.getBoundingClientRect(); + const canvasRect = canvasAreaRef.value?.getBoundingClientRect(); + + if (!canvasRect) { + return getCalculatedPortPosition(node, portType); + } + + // 计算节点在SVG坐标系中的实际大小和位置 + const nodeWidth = nodeRect.width / zoomLevel.value; + const nodeHeight = nodeRect.height / zoomLevel.value; + + // 端口位于节点的水平中心 + const portX = node.x + nodeWidth / 2; + const portY = portType === 'input' + ? node.y - 5 + : node.y + nodeHeight + 5; + + return { x: portX, y: portY }; + }; + + const startConnection = (event: MouseEvent, nodeId: string, portType: 'input' | 'output') => { + event.preventDefault(); + event.stopPropagation(); + + connectionState.isConnecting = true; + connectionState.startNodeId = nodeId; + connectionState.startPortType = portType; + connectionState.currentMousePos = { x: event.clientX, y: event.clientY }; + + const startPos = getPortPosition(nodeId, portType); + if (startPos) { + connectionState.startPortPos = startPos; + } + + document.addEventListener('mousemove', onConnectionDrag); + document.addEventListener('mouseup', onConnectionEnd); + + if (canvasAreaRef.value) { + canvasAreaRef.value.classList.add('connecting'); + } + }; + + // 连接拖拽 + const onConnectionDrag = (event: MouseEvent) => { + if (!connectionState.isConnecting || !connectionState.startNodeId || !connectionState.startPortType) return; + + connectionState.currentMousePos = { x: event.clientX, y: event.clientY }; + + const svgPos = clientToSVGCoordinates(event.clientX, event.clientY); + const startPos = getPortPosition(connectionState.startNodeId, connectionState.startPortType); + + if (startPos && svgPos) { + const controlOffset = Math.abs(svgPos.y - startPos.y) * 0.5; + let path: string; + + if (connectionState.startPortType === 'output') { + path = `M ${startPos.x} ${startPos.y} C ${startPos.x} ${startPos.y + controlOffset} ${svgPos.x} ${svgPos.y - controlOffset} ${svgPos.x} ${svgPos.y}`; + } else { + path = `M ${startPos.x} ${startPos.y} C ${startPos.x} ${startPos.y - controlOffset} ${svgPos.x} ${svgPos.y + controlOffset} ${svgPos.x} ${svgPos.y}`; + } + + if ('tempPath' in connectionState) { + (connectionState as any).tempPath = path; + } + } + const targetPort = findTargetPort(event.clientX, event.clientY); + if (targetPort && targetPort.nodeId !== connectionState.startNodeId) { + connectionState.hoveredPort = targetPort; + } else { + connectionState.hoveredPort = null; + } + }; + + // 结束连接 + const onConnectionEnd = (event: MouseEvent) => { + if (!connectionState.isConnecting) return; + + // 检查是否落在有效的端口上 + const targetPort = findTargetPort(event.clientX, event.clientY); + + if (targetPort && connectionState.startNodeId && connectionState.startPortType) { + const canConnectResult = canConnect( + connectionState.startNodeId, + connectionState.startPortType, + targetPort.nodeId, + targetPort.portType + ); + + if (canConnectResult) { + let parentId: string, childId: string; + + if (connectionState.startPortType === 'output') { + parentId = connectionState.startNodeId; + childId = targetPort.nodeId; + } else { + parentId = targetPort.nodeId; + childId = connectionState.startNodeId; + } + + createConnection(parentId, childId); + } + } + + // 清理连接状态 + cancelConnection(); + }; + + // 取消连接 + const cancelConnection = () => { + connectionState.isConnecting = false; + connectionState.startNodeId = null; + connectionState.startPortType = null; + connectionState.currentMousePos = null; + connectionState.startPortPos = null; + connectionState.hoveredPort = null; + + if ('tempPath' in connectionState) { + (connectionState as any).tempPath = ''; + } + + document.removeEventListener('mousemove', onConnectionDrag); + document.removeEventListener('mouseup', onConnectionEnd); + + if (canvasAreaRef.value) { + canvasAreaRef.value.classList.remove('connecting'); + } + // 清除画布内的拖拽目标样式 + if (canvasAreaRef.value) { + const allPorts = canvasAreaRef.value.querySelectorAll('.port.drag-target'); + allPorts.forEach(port => port.classList.remove('drag-target')); + } + }; + + const clientToSVGCoordinates = (clientX: number, clientY: number) => { + if (!canvasAreaRef.value) return null; + + try { + // 获取canvas容器的边界 + const canvasRect = canvasAreaRef.value.getBoundingClientRect(); + + // 转换为相对于canvas的坐标 + const canvasX = clientX - canvasRect.left; + const canvasY = clientY - canvasRect.top; + + // 撤销SVG的transform,转换为SVG坐标 + // SVG transform: translate(panX, panY) scale(zoomLevel) + const svgX = (canvasX - panX.value) / zoomLevel.value; + const svgY = (canvasY - panY.value) / zoomLevel.value; + + return { x: svgX, y: svgY }; + } catch (e) { + return null; + } + }; + + // 查找目标端口 + const findTargetPort = (clientX: number, clientY: number) => { + if (!canvasAreaRef.value) return null; + + try { + const elementAtPoint = document.elementFromPoint(clientX, clientY); + if (elementAtPoint?.classList.contains('port') && canvasAreaRef.value.contains(elementAtPoint)) { + return getPortInfo(elementAtPoint as HTMLElement); + } + } catch (error) { + console.warn(`[ConnectionManager] elementFromPoint 查询出错:`, error); + } + + const allPorts = canvasAreaRef.value.querySelectorAll('.port'); + for (const port of allPorts) { + const rect = port.getBoundingClientRect(); + const margin = 10; + + if (clientX >= rect.left - margin && clientX <= rect.right + margin && + clientY >= rect.top - margin && clientY <= rect.bottom + margin) { + return getPortInfo(port as HTMLElement); + } + } + + return null; + }; + + // 从端口元素获取端口信息 + const getPortInfo = (portElement: HTMLElement) => { + const nodeElement = portElement.closest('.tree-node'); + if (!nodeElement) return null; + + const nodeId = nodeElement.getAttribute('data-node-id'); + const portType = portElement.classList.contains('port-input') ? 'input' : 'output' as 'input' | 'output'; + + return nodeId ? { nodeId, portType } : null; + }; + + // 端口悬停处理 + const onPortHover = (nodeId: string, portType: 'input' | 'output') => { + if (connectionState.isConnecting && connectionState.startNodeId !== nodeId) { + connectionState.hoveredPort = { nodeId, portType }; + + if (canConnect(connectionState.startNodeId!, connectionState.startPortType!, nodeId, portType)) { + // 在画布区域内查找端口元素 + if (canvasAreaRef.value) { + const portElement = canvasAreaRef.value.querySelector(`[data-node-id="${nodeId}"] .port.port-${portType}`); + if (portElement) { + portElement.classList.add('drag-target'); + } + } + } + } + }; + + const onPortLeave = () => { + if (connectionState.isConnecting) { + connectionState.hoveredPort = null; + // 清除画布内的拖拽目标样式 + if (canvasAreaRef.value) { + const allPorts = canvasAreaRef.value.querySelectorAll('.port.drag-target'); + allPorts.forEach(port => port.classList.remove('drag-target')); + } + } + }; + + // 验证连接目标是否有效 + const isValidConnectionTarget = (nodeId: string, portType: 'input' | 'output') => { + if (!connectionState.isConnecting || !connectionState.startNodeId || connectionState.startNodeId === nodeId) { + return false; + } + + return canConnect(connectionState.startNodeId, connectionState.startPortType!, nodeId, portType); + }; + + // 检查是否可以连接 + const canConnect = (sourceNodeId: string, sourcePortType: string, targetNodeId: string, targetPortType: string) => { + if (sourceNodeId === targetNodeId) return false; + if (sourcePortType === targetPortType) return false; + + let parentNodeId: string, childNodeId: string; + + if (sourcePortType === 'output') { + parentNodeId = sourceNodeId; + childNodeId = targetNodeId; + } else { + parentNodeId = targetNodeId; + childNodeId = sourceNodeId; + } + + const childNode = treeNodes.value.find(n => n.id === childNodeId); + if (childNode && childNode.parent && childNode.parent !== parentNodeId) { + return false; + } + + const parentNode = treeNodes.value.find(n => n.id === parentNodeId); + if (!parentNode || !parentNode.canHaveChildren) return false; + if (!childNode || !childNode.canHaveParent) return false; + + if (wouldCreateCycle(parentNodeId, childNodeId)) return false; + if (isDescendant(childNodeId, parentNodeId)) return false; + + return true; + }; + + // 检查是否会创建循环 + const wouldCreateCycle = (parentId: string, childId: string) => { + return isDescendant(parentId, childId); + }; + + const isDescendant = (ancestorId: string, descendantId: string): boolean => { + const visited = new Set(); + + function checkPath(currentId: string): boolean { + if (currentId === ancestorId) return true; + if (visited.has(currentId)) return false; + + visited.add(currentId); + + const currentNode = treeNodes.value.find(n => n.id === currentId); + if (currentNode?.children) { + for (const childId of currentNode.children) { + if (checkPath(childId)) return true; + } + } + + return false; + } + + return checkPath(descendantId); + }; + + // 创建连接 + const createConnection = (parentId: string, childId: string) => { + const parentNode = treeNodes.value.find(n => n.id === parentId); + const childNode = treeNodes.value.find(n => n.id === childId); + + if (!parentNode || !childNode) return; + + // 移除子节点的旧父子关系 + if (childNode.parent) { + const oldParent = treeNodes.value.find(n => n.id === childNode.parent); + if (oldParent) { + const index = oldParent.children.indexOf(childId); + if (index > -1) { + oldParent.children.splice(index, 1); + } + } + } + + // 建立新的父子关系 + childNode.parent = parentId; + if (!parentNode.children.includes(childId)) { + parentNode.children.push(childId); + } + + updateConnections(); + }; + + // 更新连接线 + const updateConnections = () => { + connections.value.length = 0; + + // 添加一个小延迟,确保DOM已经更新 + setTimeout(() => { + treeNodes.value.forEach(node => { + if (node.children) { + node.children.forEach(childId => { + const childNode = treeNodes.value.find(n => n.id === childId); + if (childNode) { + const parentPos = getPortPosition(node.id, 'output'); + const childPos = getPortPosition(childId, 'input'); + + if (parentPos && childPos) { + const controlOffset = Math.abs(childPos.y - parentPos.y) * 0.5; + const path = `M ${parentPos.x} ${parentPos.y} C ${parentPos.x} ${parentPos.y + controlOffset} ${childPos.x} ${childPos.y - controlOffset} ${childPos.x} ${childPos.y}`; + + connections.value.push({ + id: `${node.id}-${childId}`, + sourceId: node.id, + targetId: childId, + path: path, + active: false + }); + } + } + }); + } + }); + }, 50); // 50ms延迟,确保DOM渲染完成 + }; + + return { + getPortPosition, + startConnection, + cancelConnection, + updateConnections, + onPortHover, + onPortLeave, + isValidConnectionTarget + }; +} \ No newline at end of file diff --git a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useFileOperations.ts b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useFileOperations.ts index d9ed6f23..0486f70e 100644 --- a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useFileOperations.ts +++ b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useFileOperations.ts @@ -1,4 +1,4 @@ -import { Ref } from 'vue'; +import { Ref, ref, watch } from 'vue'; import { TreeNode, Connection } from '../types'; /** @@ -9,28 +9,417 @@ export function useFileOperations( selectedNodeId: Ref, connections: Ref, tempConnection: Ref<{ path: string }>, - showExportModal: Ref + showExportModal: Ref, + codeGeneration?: { + createTreeFromConfig: (config: any) => TreeNode[]; + }, + updateConnections?: () => void ) { + // 跟踪未保存状态 + const hasUnsavedChanges = ref(false); + const lastSavedState = ref(''); + const currentFileName = ref(''); + + // 监听树结构变化来更新未保存状态 + const updateUnsavedStatus = () => { + const currentState = JSON.stringify({ + nodes: treeNodes.value, + connections: connections.value + }); + hasUnsavedChanges.value = currentState !== lastSavedState.value; + }; + + // 监听变化 + watch([treeNodes, connections], updateUnsavedStatus, { deep: true }); + + // 标记为已保存 + const markAsSaved = () => { + const currentState = JSON.stringify({ + nodes: treeNodes.value, + connections: connections.value + }); + lastSavedState.value = currentState; + hasUnsavedChanges.value = false; + }; + + // 检查是否需要保存的通用方法 + const checkUnsavedChanges = (): Promise => { + return new Promise((resolve) => { + if (!hasUnsavedChanges.value) { + resolve(true); + return; + } + + const result = confirm( + '当前行为树有未保存的更改,是否要保存?\n\n' + + '点击"确定"保存更改\n' + + '点击"取消"丢弃更改\n' + + '点击"X"取消操作' + ); + + if (result) { + // 用户选择保存 + saveBehaviorTree().then(() => { + resolve(true); + }).catch(() => { + resolve(false); + }); + } else { + // 用户选择丢弃更改 + resolve(true); + } + }); + }; + + // 导出行为树数据 + const exportBehaviorTreeData = () => { + return { + nodes: treeNodes.value, + connections: connections.value, + metadata: { + name: currentFileName.value || 'untitled', + created: new Date().toISOString(), + version: '1.0' + } + }; + }; // 工具栏操作 - const newBehaviorTree = () => { + const newBehaviorTree = async () => { + const canProceed = await checkUnsavedChanges(); + if (canProceed) { treeNodes.value = []; selectedNodeId.value = null; connections.value = []; tempConnection.value.path = ''; + currentFileName.value = ''; + markAsSaved(); // 新建后标记为已保存状态 + } }; - const saveBehaviorTree = () => { - // TODO: 实现保存功能 - console.log('保存行为树'); + // 保存行为树 + const saveBehaviorTree = async (): Promise => { + console.log('=== 开始保存行为树 ==='); + + try { + const data = exportBehaviorTreeData(); + const jsonString = JSON.stringify(data, null, 2); + console.log('数据准备完成,JSON长度:', jsonString.length); + + // 使用 HTML input 替代 prompt(因为 prompt 在 Cocos Creator 扩展中不支持) + const fileName = await getFileNameFromUser(); + if (!fileName) { + console.log('❌ 用户取消了保存操作'); + return false; + } + + console.log('✓ 用户输入文件名:', fileName); + + // 检测是否在Cocos Creator环境中 + if (typeof Editor !== 'undefined' && typeof (window as any).sendToMain === 'function') { + console.log('✓ 使用Cocos Creator保存方式'); + + try { + (window as any).sendToMain('create-behavior-tree-from-editor', { + fileName: fileName + '.json', + content: jsonString, + timestamp: new Date().toISOString() + }); + + console.log('✓ 保存消息已发送到主进程'); + + // 更新当前文件名并标记为已保存 + currentFileName.value = fileName; + markAsSaved(); + + // 用户反馈 + showMessage(`保存成功!文件名: ${fileName}.json`, 'success'); + + console.log('✅ 保存操作完成'); + return true; + } catch (sendError) { + console.error('❌ 发送消息时出错:', sendError); + showMessage('保存失败: ' + sendError, 'error'); + return false; + } + } else { + console.log('✓ 使用浏览器下载保存方式'); + + // 在浏览器环境中使用下载方式 + const blob = new Blob([jsonString], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `${fileName}.json`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + + // 标记为已保存 + currentFileName.value = fileName; + markAsSaved(); + + console.log('✅ 文件下载保存成功'); + return true; + } + } catch (error) { + console.error('❌ 保存过程中发生错误:', error); + showMessage('保存失败: ' + error, 'error'); + return false; + } }; - const loadBehaviorTree = () => { - // TODO: 实现加载功能 - console.log('加载行为树'); + // 使用 HTML input 获取文件名(替代 prompt) + const getFileNameFromUser = (): Promise => { + return new Promise((resolve) => { + // 创建模态对话框 + const overlay = document.createElement('div'); + overlay.style.cssText = ` + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.5); + display: flex; + justify-content: center; + align-items: center; + z-index: 10000; + `; + + const dialog = document.createElement('div'); + dialog.style.cssText = ` + background: #2d2d2d; + color: #ffffff; + padding: 20px; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + min-width: 300px; + `; + + dialog.innerHTML = ` +

保存行为树

+

请输入文件名(不含扩展名):

+ +
+ + +
+ `; + + overlay.appendChild(dialog); + document.body.appendChild(overlay); + + const input = dialog.querySelector('#filename-input') as HTMLInputElement; + const saveBtn = dialog.querySelector('#save-btn') as HTMLButtonElement; + const cancelBtn = dialog.querySelector('#cancel-btn') as HTMLButtonElement; + + // 聚焦并选中文本 + input.focus(); + input.select(); + + // 事件处理 + const cleanup = () => { + document.body.removeChild(overlay); + }; + + saveBtn.onclick = () => { + const fileName = input.value.trim(); + cleanup(); + resolve(fileName || null); + }; + + cancelBtn.onclick = () => { + cleanup(); + resolve(null); + }; + + // 回车键保存 + input.onkeydown = (e) => { + if (e.key === 'Enter') { + const fileName = input.value.trim(); + cleanup(); + resolve(fileName || null); + } else if (e.key === 'Escape') { + cleanup(); + resolve(null); + } + }; + }); }; - const exportCode = () => { + // 显示消息提示 + const showMessage = (message: string, type: 'success' | 'error' = 'success') => { + const toast = document.createElement('div'); + toast.style.cssText = ` + position: fixed; + top: 20px; + right: 20px; + padding: 12px 20px; + background: ${type === 'success' ? '#4caf50' : '#f44336'}; + color: white; + border-radius: 4px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); + z-index: 10001; + opacity: 0; + transform: translateX(100%); + transition: all 0.3s ease; + `; + toast.textContent = message; + + document.body.appendChild(toast); + + // 动画显示 + setTimeout(() => { + toast.style.opacity = '1'; + toast.style.transform = 'translateX(0)'; + }, 10); + + // 3秒后自动消失 + setTimeout(() => { + toast.style.opacity = '0'; + toast.style.transform = 'translateX(100%)'; + setTimeout(() => { + if (document.body.contains(toast)) { + document.body.removeChild(toast); + } + }, 300); + }, 3000); + }; + + // 生成当前行为树的配置 + const generateCurrentConfig = () => { + if (treeNodes.value.length === 0) return null; + + const rootNode = treeNodes.value.find(node => + !treeNodes.value.some(otherNode => + otherNode.children?.includes(node.id) + ) + ); + + if (!rootNode) return null; + + return { + version: "1.0.0", + type: "behavior-tree", + metadata: { + createdAt: new Date().toISOString(), + nodeCount: treeNodes.value.length + }, + tree: generateNodeConfig(rootNode) + }; + }; + + // 简化的节点配置生成(用于文件保存) + const generateNodeConfig = (node: TreeNode): any => { + const config: any = { + id: node.id, + type: node.type, + namespace: getNodeNamespace(node.type), + properties: {} + }; + + // 处理节点属性 + if (node.properties) { + Object.entries(node.properties).forEach(([key, prop]) => { + if (prop.value !== undefined && prop.value !== '') { + config.properties[key] = { + type: prop.type, + value: prop.value + }; + } + }); + } + + // 处理子节点 + if (node.children && node.children.length > 0) { + config.children = node.children + .map(childId => treeNodes.value.find(n => n.id === childId)) + .filter(Boolean) + .map(child => generateNodeConfig(child!)); + } + + return config; + }; + + // 获取节点命名空间 + const getNodeNamespace = (nodeType: string): string => { + // ECS节点 + if (['has-component', 'add-component', 'remove-component', 'modify-component', + 'has-tag', 'is-active', 'wait-time', 'destroy-entity'].includes(nodeType)) { + return 'ecs-integration/behaviors'; + } + + // 复合节点 + if (['sequence', 'selector', 'parallel', 'parallel-selector', + 'random-selector', 'random-sequence'].includes(nodeType)) { + return 'behaviourTree/composites'; + } + + // 装饰器 + if (['repeater', 'inverter', 'always-fail', 'always-succeed', + 'until-fail', 'until-success'].includes(nodeType)) { + return 'behaviourTree/decorators'; + } + + // 动作节点 + if (['execute-action', 'log-action', 'wait-action'].includes(nodeType)) { + return 'behaviourTree/actions'; + } + + // 条件节点 + if (['execute-conditional'].includes(nodeType)) { + return 'behaviourTree/conditionals'; + } + + return 'behaviourTree'; + }; + + const loadBehaviorTree = async () => { + const canProceed = await checkUnsavedChanges(); + if (!canProceed) return; + + const input = document.createElement('input'); + input.type = 'file'; + input.accept = '.json,.bt'; + input.onchange = (e) => { + const file = (e.target as HTMLInputElement).files?.[0]; + if (file) { + const reader = new FileReader(); + reader.onload = (event) => { + try { + const configText = event.target?.result as string; + const config = JSON.parse(configText); + + if (codeGeneration) { + const newNodes = codeGeneration.createTreeFromConfig(config); + treeNodes.value = newNodes; + selectedNodeId.value = null; + connections.value = []; + tempConnection.value.path = ''; + markAsSaved(); // 加载后标记为已保存状态 + console.log('行为树配置加载成功'); + if (updateConnections) { + updateConnections(); + } + } else { + console.error('代码生成器未初始化'); + alert('代码生成器未初始化'); + } + } catch (error) { + console.error('加载行为树配置失败:', error); + alert('配置文件格式错误'); + } + }; + reader.readAsText(file); + } + }; + input.click(); + }; + + const exportConfig = () => { showExportModal.value = true; }; @@ -59,10 +448,12 @@ export function useFileOperations( newBehaviorTree, saveBehaviorTree, loadBehaviorTree, - exportCode, + exportConfig, copyToClipboard, saveToFile, autoLayout, - validateTree + validateTree, + hasUnsavedChanges, + markAsSaved }; } \ No newline at end of file diff --git a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useNodeDisplay.ts b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useNodeDisplay.ts new file mode 100644 index 00000000..d8d576aa --- /dev/null +++ b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useNodeDisplay.ts @@ -0,0 +1,86 @@ +/** + * 节点显示管理功能 + */ +export function useNodeDisplay() { + + // 检查节点是否有可见属性 + const hasVisibleProperties = (node: any) => { + if (!node.properties) return false; + return Object.keys(getVisibleProperties(node)).length > 0; + }; + + // 获取可见属性 + const getVisibleProperties = (node: any) => { + if (!node.properties) return {}; + + const visibleProps: any = {}; + for (const [key, prop] of Object.entries(node.properties)) { + if (shouldShowProperty(prop as any, key)) { + visibleProps[key] = prop; + } + } + return visibleProps; + }; + + // 判断属性是否应该显示 + const shouldShowProperty = (prop: any, key: string) => { + // 总是显示这些重要属性 + const alwaysShow = ['abortType', 'repeatCount', 'priority']; + if (alwaysShow.includes(key)) { + return true; + } + + // 对于其他属性,只在非默认值时显示 + if (prop.type === 'string' && prop.value && prop.value.trim() !== '') { + return true; + } + if (prop.type === 'number' && prop.value !== 0 && prop.value !== -1) { + return true; + } + if (prop.type === 'boolean' && prop.value === true) { + return true; + } + if (prop.type === 'select' && prop.value !== 'None' && prop.value !== '') { + return true; + } + if (prop.type === 'code' && prop.value && prop.value.trim() !== '' && prop.value !== '(context) => true') { + return true; + } + + return false; + }; + + // 格式化属性值显示 + const formatPropertyValue = (prop: any) => { + switch (prop.type) { + case 'boolean': + return prop.value ? '✓' : '✗'; + case 'number': + return prop.value.toString(); + case 'select': + return prop.value; + case 'string': + return prop.value.length > 15 ? prop.value.substring(0, 15) + '...' : prop.value; + case 'code': + const code = prop.value || ''; + if (code.length > 20) { + // 尝试提取函数体的关键部分 + const bodyMatch = code.match(/=>\s*(.+)/) || code.match(/{\s*(.+?)\s*}/); + if (bodyMatch) { + const body = bodyMatch[1].trim(); + return body.length > 15 ? body.substring(0, 15) + '...' : body; + } + return code.substring(0, 20) + '...'; + } + return code; + default: + return prop.value?.toString() || ''; + } + }; + + return { + hasVisibleProperties, + getVisibleProperties, + formatPropertyValue + }; +} \ No newline at end of file diff --git a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useNodeOperations.ts b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useNodeOperations.ts index ae8a8684..ec33df70 100644 --- a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useNodeOperations.ts +++ b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useNodeOperations.ts @@ -1,4 +1,4 @@ -import { Ref } from 'vue'; +import { Ref, nextTick } from 'vue'; import { TreeNode, Connection } from '../types'; import { NodeTemplate } from '../data/nodeTemplates'; import { createNodeFromTemplate } from '../utils/nodeUtils'; @@ -14,7 +14,8 @@ export function useNodeOperations( panX: Ref, panY: Ref, zoomLevel: Ref, - getNodeByIdLocal: (id: string) => TreeNode | undefined + getNodeByIdLocal: (id: string) => TreeNode | undefined, + updateConnections?: () => void ) { // 获取相对于画布的坐标(用于节点拖放等操作) @@ -94,31 +95,73 @@ export function useNodeOperations( if (selectedNodeId.value === nodeId) { selectedNodeId.value = null; } + + // 更新连接线 + if (updateConnections) { + updateConnections(); + } + }; + + // 通用的属性更新方法 + const setNestedProperty = (obj: any, path: string, value: any) => { + const keys = path.split('.'); + let current = obj; + + // 导航到目标属性的父对象 + for (let i = 0; i < keys.length - 1; i++) { + const key = keys[i]; + if (!(key in current) || typeof current[key] !== 'object' || current[key] === null) { + current[key] = {}; + } + current = current[key]; + } + + // 设置最终值 + const finalKey = keys[keys.length - 1]; + current[finalKey] = value; }; // 节点属性更新 const updateNodeProperty = (path: string, value: any) => { + console.log('updateNodeProperty called:', path, value); const node = selectedNodeId.value ? getNodeByIdLocal(selectedNodeId.value) : null; - if (!node) return; - - // 确保 properties 对象存在 - if (!node.properties) { - node.properties = {}; + if (!node) { + console.log('No selected node found'); + return; } - const keys = path.split('.'); - let target: any = node.properties; + console.log('Current node before update:', JSON.stringify(node, null, 2)); - // 导航到目标对象,如果中间对象不存在则创建 - for (let i = 0; i < keys.length - 1; i++) { - if (!target[keys[i]] || typeof target[keys[i]] !== 'object') { - target[keys[i]] = {}; - } - target = target[keys[i]]; + // 使用通用方法更新属性 + setNestedProperty(node, path, value); + + console.log(`Updated property ${path} to:`, value); + console.log('Updated node after change:', JSON.stringify(node, null, 2)); + + // 强制触发响应式更新 - 创建新数组来强制Vue检测变化 + const nodeIndex = treeNodes.value.findIndex(n => n.id === node.id); + if (nodeIndex > -1) { + // 创建新的节点数组,确保Vue能检测到变化 + const newNodes = [...treeNodes.value]; + newNodes[nodeIndex] = { ...node }; // 创建节点副本确保响应式更新 + treeNodes.value = newNodes; + + console.log('Triggered reactive update - replaced array'); + + // 验证更新是否成功 + nextTick(() => { + const verifyNode = treeNodes.value.find(n => n.id === node.id); + console.log('Verification - node after update:', JSON.stringify(verifyNode, null, 2)); + + // 验证属性值 + const pathParts = path.split('.'); + let checkValue: any = verifyNode; + for (const part of pathParts) { + checkValue = checkValue?.[part]; + } + console.log(`Verification - final value at ${path}:`, checkValue); + }); } - - // 设置最终值 - target[keys[keys.length - 1]] = value; }; return { diff --git a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/data/nodeTemplates.ts b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/data/nodeTemplates.ts index 0d309499..e19602c2 100644 --- a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/data/nodeTemplates.ts +++ b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/data/nodeTemplates.ts @@ -386,6 +386,13 @@ export const nodeTemplates: NodeTemplate[] = [ value: 'Component', description: '要添加的组件类型名称', required: true + }, + componentFactory: { + name: '组件工厂函数', + type: 'code', + value: '() => new Component()', + description: '创建组件实例的函数(可选)', + required: false } } }, diff --git a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/index.ts b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/index.ts index 8f832954..75c68746 100644 --- a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/index.ts +++ b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/index.ts @@ -21,6 +21,18 @@ module.exports = Editor.Panel.define({ methods: { sendToMain(message: string, ...args: any[]) { Editor.Message.send('cocos-ecs-extension', message, ...args); + }, + + loadBehaviorTreeFile(fileData: any) { + console.log('Loading behavior tree file:', fileData); + + // 通知编辑器组件加载文件 + if (this.$.app) { + const event = new CustomEvent('load-behavior-tree-file', { + detail: fileData + }); + this.$.app.dispatchEvent(event); + } } }, @@ -29,6 +41,9 @@ module.exports = Editor.Panel.define({ const app = createApp({}); app.config.compilerOptions.isCustomElement = (tag) => tag.startsWith('ui-'); + // 暴露发送消息到主进程的方法 + (window as any).sendToMain = this.sendToMain.bind(this); + // 树节点组件 app.component('tree-node-item', defineComponent({ props: ['node', 'level', 'getNodeByIdLocal'], diff --git a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/types/index.ts b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/types/index.ts index 9ba1b2de..4e9069b1 100644 --- a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/types/index.ts +++ b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/types/index.ts @@ -56,4 +56,13 @@ export interface ConnectionPort { export interface CanvasCoordinates { x: number; y: number; +} + +export interface ConnectionState { + isConnecting: boolean; + startNodeId: string | null; + startPortType: 'input' | 'output' | null; + currentMousePos: { x: number; y: number } | null; + startPortPos: { x: number; y: number } | null; + hoveredPort: { nodeId: string; portType: 'input' | 'output' } | null; } \ No newline at end of file diff --git a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/utils/canvasManager.ts b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/utils/canvasManager.ts deleted file mode 100644 index 9d3439c2..00000000 --- a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/utils/canvasManager.ts +++ /dev/null @@ -1,208 +0,0 @@ -import { Ref } from 'vue'; -import { TreeNode, DragState } from '../types'; -import { getCanvasCoordinates, constrainZoom } from './canvasUtils'; - -export interface CanvasManager { - onCanvasMouseDown: (event: MouseEvent) => void; - onCanvasMouseMove: (event: MouseEvent) => void; - onCanvasMouseUp: (event: MouseEvent) => void; - onCanvasWheel: (event: WheelEvent) => void; - zoomIn: () => void; - zoomOut: () => void; - resetZoom: () => void; - centerView: () => void; - startNodeDrag: (event: MouseEvent, node: TreeNode) => void; -} - -export function createCanvasManager( - canvasWidth: Ref, - canvasHeight: Ref, - zoomLevel: Ref, - panX: Ref, - panY: Ref, - dragState: Ref, - treeNodes: Ref, - getNodeByIdLocal: (id: string) => TreeNode | undefined, - selectNode: (nodeId: string) => void, - updateConnectionsThrottled: () => void, - connectionManager: { updateTempConnection: (nodeId: string, portType: string, targetX: number, targetY: number) => void }, - findCanvasElement: () => HTMLElement | null, - getSVGInternalCoords: (event: MouseEvent, canvasElement: HTMLElement | null) => { x: number, y: number } -): CanvasManager { - - const UPDATE_THROTTLE = 16; // 60fps - let animationFrameId: number | null = null; - let lastUpdateTime = 0; - - const onCanvasMouseDown = (event: MouseEvent) => { - if (event.button !== 0) return; // 只处理左键 - - dragState.value.isDraggingCanvas = true; - dragState.value.dragStartX = event.clientX; - dragState.value.dragStartY = event.clientY; - }; - - const onCanvasMouseMove = (event: MouseEvent) => { - if (animationFrameId) { - cancelAnimationFrame(animationFrameId); - } - - animationFrameId = requestAnimationFrame(() => { - const currentTime = performance.now(); - if (currentTime - lastUpdateTime < UPDATE_THROTTLE) { - return; - } - lastUpdateTime = currentTime; - - if (dragState.value.isDraggingCanvas) { - const deltaX = event.clientX - dragState.value.dragStartX; - const deltaY = event.clientY - dragState.value.dragStartY; - - panX.value += deltaX; - panY.value += deltaY; - - dragState.value.dragStartX = event.clientX; - dragState.value.dragStartY = event.clientY; - } else if (dragState.value.isDraggingNode && dragState.value.dragNodeId) { - const node = getNodeByIdLocal(dragState.value.dragNodeId); - if (node) { - const deltaX = (event.clientX - dragState.value.dragStartX) / zoomLevel.value; - const deltaY = (event.clientY - dragState.value.dragStartY) / zoomLevel.value; - - node.x = dragState.value.dragNodeStartX + deltaX; - node.y = dragState.value.dragNodeStartY + deltaY; - - updateConnectionsThrottled(); - } - } else if (dragState.value.isConnecting && dragState.value.connectionStart) { - let canvasElement = event.currentTarget as HTMLElement | null; - if (!canvasElement) { - canvasElement = findCanvasElement(); - } - - if (canvasElement) { - const { x, y } = getSVGInternalCoords(event, canvasElement); - - dragState.value.connectionEnd.x = x; - dragState.value.connectionEnd.y = y; - - connectionManager.updateTempConnection( - dragState.value.connectionStart.nodeId, - dragState.value.connectionStart.portType, - x, - y - ); - } - } - }); - }; - - const onCanvasMouseUp = (event: MouseEvent) => { - if (dragState.value.isDraggingCanvas) { - dragState.value.isDraggingCanvas = false; - } else if (dragState.value.isDraggingNode) { - // 恢复过渡效果 - if (dragState.value.dragNodeId) { - const nodeElement = document.querySelector(`[data-node-id="${dragState.value.dragNodeId}"]`) as HTMLElement; - if (nodeElement) { - nodeElement.style.transition = ''; - } - } - - dragState.value.isDraggingNode = false; - dragState.value.dragNodeId = null; - } - - // 清理动画帧 - if (animationFrameId) { - cancelAnimationFrame(animationFrameId); - animationFrameId = null; - } - }; - - const onCanvasWheel = (event: WheelEvent) => { - event.preventDefault(); - if (event.deltaY < 0) { - zoomIn(); - } else { - zoomOut(); - } - }; - - const zoomIn = () => { - zoomLevel.value = constrainZoom(zoomLevel.value * 1.2); - }; - - const zoomOut = () => { - zoomLevel.value = constrainZoom(zoomLevel.value / 1.2); - }; - - const resetZoom = () => { - zoomLevel.value = 1; - panX.value = 0; - panY.value = 0; - }; - - const centerView = () => { - if (treeNodes.value.length === 0) return; - - // 计算所有节点的边界 - let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; - - treeNodes.value.forEach(node => { - minX = Math.min(minX, node.x); - minY = Math.min(minY, node.y); - maxX = Math.max(maxX, node.x + 150); - maxY = Math.max(maxY, node.y + 100); - }); - - // 计算中心点 - const centerX = (minX + maxX) / 2; - const centerY = (minY + maxY) / 2; - - // 设置平移,使内容居中 - panX.value = canvasWidth.value / 2 - centerX * zoomLevel.value; - panY.value = canvasHeight.value / 2 - centerY * zoomLevel.value; - }; - - const startNodeDrag = (event: MouseEvent, node: TreeNode) => { - // 检查是否点击的是端口或删除按钮 - const target = event.target as HTMLElement; - if (target.classList.contains('port') || - target.classList.contains('node-delete') || - target.closest('.port') || - target.closest('.node-delete')) { - return; // 不启动节点拖拽 - } - - event.stopPropagation(); - - if (event.button !== 0) return; // 只处理左键 - - dragState.value.isDraggingNode = true; - dragState.value.dragNodeId = node.id; - dragState.value.dragStartX = event.clientX; - dragState.value.dragStartY = event.clientY; - dragState.value.dragNodeStartX = node.x; - dragState.value.dragNodeStartY = node.y; - - const nodeElement = document.querySelector(`[data-node-id="${node.id}"]`) as HTMLElement; - if (nodeElement) { - nodeElement.style.transition = 'none'; - } - - selectNode(node.id); - }; - - return { - onCanvasMouseDown, - onCanvasMouseMove, - onCanvasMouseUp, - onCanvasWheel, - zoomIn, - zoomOut, - resetZoom, - centerView, - startNodeDrag - }; -} \ No newline at end of file diff --git a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/utils/codeGenerator.ts b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/utils/codeGenerator.ts deleted file mode 100644 index 5f3976d3..00000000 --- a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/utils/codeGenerator.ts +++ /dev/null @@ -1,184 +0,0 @@ -import { TreeNode } from '../types'; -import { getNodeById, getRootNode } from './nodeUtils'; -import { nodeTemplates } from '../data/nodeTemplates'; - -/** - * 生成TypeScript代码 - */ -export function generateTypeScriptCode(nodes: TreeNode[]): string { - const imports = getRequiredImports(nodes); - const root = getRootNode(nodes); - - if (!root) { - return '// 请先添加根节点'; - } - - const importsCode = imports.map(imp => `import { ${imp} } from '@esengine/ai';`).join('\n'); - const treeCode = generateNodeCode(root, nodes); - - return `${importsCode} - -// 自动生成的行为树代码 -export function createBehaviorTree() { - return ${treeCode}; -}`; -} - -/** - * 获取需要导入的类 - */ -export function getRequiredImports(nodes: TreeNode[]): string[] { - const imports = new Set(); - - nodes.forEach(node => { - const template = nodeTemplates.find(t => t.className === node.type || t.type === node.type); - if (template?.className) { - imports.add(template.className); - } - }); - - return Array.from(imports); -} - -/** - * 生成单个节点的代码 - */ -export function generateNodeCode(node: TreeNode, allNodes: TreeNode[], indent: number = 0): string { - const spaces = ' '.repeat(indent); - const template = nodeTemplates.find(t => t.className === node.type); - - if (!template) { - return `${spaces}// 未知节点类型: ${node.type}`; - } - - let code = `${spaces}new ${template.className}(`; - - // 构造函数参数 - const params: string[] = []; - - // 处理属性 - if (node.properties && Object.keys(node.properties).length > 0) { - const propsCode: string[] = []; - - Object.entries(node.properties).forEach(([key, prop]) => { - if (prop.type === 'code' && prop.value) { - propsCode.push(`${key}: ${prop.value}`); - } else if (prop.type === 'string' && prop.value !== undefined) { - propsCode.push(`${key}: "${prop.value}"`); - } else if (prop.type === 'number' && prop.value !== undefined) { - propsCode.push(`${key}: ${prop.value}`); - } else if (prop.type === 'boolean' && prop.value !== undefined) { - propsCode.push(`${key}: ${prop.value}`); - } else if (prop.type === 'select' && prop.value !== undefined) { - propsCode.push(`${key}: "${prop.value}"`); - } - }); - - if (propsCode.length > 0) { - params.push(`{\n${spaces} ${propsCode.join(',\n' + spaces + ' ')}\n${spaces}}`); - } - } - - code += params.join(', '); - - // 子节点 - if (node.children && node.children.length > 0) { - const children = node.children - .map(childId => getNodeById(allNodes, childId)) - .filter(Boolean) - .map(child => generateNodeCode(child!, allNodes, indent + 1)); - - if (children.length > 0) { - if (params.length > 0) code += ', '; - code += '[\n' + children.join(',\n') + '\n' + spaces + ']'; - } - } - - code += ')'; - return code; -} - -/** - * 生成JSON代码 - */ -export function generateJSONCode(nodes: TreeNode[]): string { - const root = getRootNode(nodes); - - if (!root) { - return '// 请先添加根节点'; - } - - const treeData = generateNodeJSON(root, nodes); - - return JSON.stringify({ - type: 'BehaviorTree', - version: '1.0', - created: new Date().toISOString(), - root: treeData - }, null, 2); -} - -/** - * 生成单个节点的JSON - */ -export function generateNodeJSON(node: TreeNode, allNodes: TreeNode[]): any { - const nodeData: any = { - id: node.id, - type: node.type, - name: node.name, - description: node.description, - position: { x: node.x, y: node.y } - }; - - // 添加属性 - if (node.properties && Object.keys(node.properties).length > 0) { - nodeData.properties = {}; - Object.entries(node.properties).forEach(([key, prop]) => { - if (prop.value !== undefined) { - nodeData.properties[key] = prop.value; - } - }); - } - - // 添加子节点 - if (node.children && node.children.length > 0) { - nodeData.children = node.children - .map(childId => getNodeById(allNodes, childId)) - .filter(Boolean) - .map(child => generateNodeJSON(child!, allNodes)); - } - - return nodeData; -} - -/** - * 根据导出格式生成代码 - */ -export function generateCode(nodes: TreeNode[], format: string): string { - switch (format) { - case 'typescript': - return generateTypeScriptCode(nodes); - case 'json': - return generateJSONCode(nodes); - default: - return generateTypeScriptCode(nodes); - } -} - -/** - * 验证生成的代码 - */ -export function validateGeneratedCode(code: string, format: string): { isValid: boolean; error?: string } { - try { - if (format === 'json') { - JSON.parse(code); - } - // TypeScript代码的验证可以在这里添加更复杂的逻辑 - return { isValid: true }; - } catch (error) { - return { - isValid: false, - error: error instanceof Error ? error.message : '未知错误' - }; - } -} \ No newline at end of file diff --git a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/utils/connectionManager.ts b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/utils/connectionManager.ts deleted file mode 100644 index 9622d08d..00000000 --- a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/utils/connectionManager.ts +++ /dev/null @@ -1,334 +0,0 @@ -import { Ref } from 'vue'; -import { TreeNode, DragState, Connection } from '../types'; - -export interface ConnectionManager { - startConnection: (event: MouseEvent, nodeId: string, portType: string) => void; - updateTempConnection: (nodeId: string, portType: string, targetX: number, targetY: number) => void; - onConnectionDragEnd: (event: MouseEvent) => void; - cancelConnection: () => void; - createConnection: (sourceId: string, targetId: string) => void; - removeConnection: (sourceId: string, targetId: string) => void; - updateConnections: () => void; - canConnect: (source: { nodeId: string, portType: string }, target: { nodeId: string, portType: string }) => boolean; -} - -export function createConnectionManager( - treeNodes: Ref, - connections: Ref, - tempConnection: Ref<{ path: string }>, - dragState: Ref, - findCanvasElement: () => HTMLElement | null, - getSVGInternalCoords: (event: MouseEvent, canvasElement: HTMLElement | null) => { x: number, y: number }, - getNodeByIdLocal: (id: string) => TreeNode | undefined, - getNodeIdFromElement: (element: HTMLElement) => string | null -): ConnectionManager { - - const startConnection = (event: MouseEvent, nodeId: string, portType: string) => { - event.stopPropagation(); - event.preventDefault(); - - const node = getNodeByIdLocal(nodeId); - if (!node) { - return; - } - - dragState.value.isConnecting = true; - dragState.value.connectionStart = { nodeId, portType }; - - // 使用统一的canvas查找方法 - const canvasElement = findCanvasElement(); - - if (canvasElement) { - const { x, y } = getSVGInternalCoords(event, canvasElement); - - // 为了让连线明显可见,使用一个与端口位置明显不同的初始位置 - const node = getNodeByIdLocal(nodeId); - let initialX, initialY; - - if (node) { - if (portType === 'output') { - // 输出端口:向下延伸50像素 - initialX = node.x + 75; // 节点中心 - initialY = node.y + 150; // 节点底部向下50像素 - } else { - // 输入端口:向上延伸50像素 - initialX = node.x + 75; // 节点中心 - initialY = node.y - 50; // 节点顶部向上50像素 - } - } else { - // fallback到鼠标位置 - initialX = x; - initialY = y; - } - - dragState.value.connectionEnd.x = initialX; - dragState.value.connectionEnd.y = initialY; - - updateTempConnection(nodeId, portType, initialX, initialY); - } - }; - - const updateTempConnection = (nodeId: string, portType: string, targetX: number, targetY: number) => { - const node = getNodeByIdLocal(nodeId); - if (!node) { - return; - } - - // 计算端口的准确位置(在节点坐标系中) - const nodeWidth = 150; - const nodeHeight = 100; - const startX = node.x + nodeWidth / 2; - - let startY: number; - if (portType === 'output') { - startY = node.y + nodeHeight; // 输出端口在底部 - } else { - startY = node.y; // 输入端口在顶部 - } - - // targetX, targetY 现在已经是SVG内部坐标系的坐标,可以直接使用 - // 创建贝塞尔曲线路径 - const controlOffset = Math.abs(targetY - startY) * 0.5; - - let path: string; - if (portType === 'output') { - path = `M ${startX} ${startY} C ${startX} ${startY + controlOffset} ${targetX} ${targetY - controlOffset} ${targetX} ${targetY}`; - } else { - path = `M ${startX} ${startY} C ${startX} ${startY - controlOffset} ${targetX} ${targetY + controlOffset} ${targetX} ${targetY}`; - } - - tempConnection.value.path = path; - }; - - const onConnectionDragEnd = (event: MouseEvent) => { - if (!dragState.value.isConnecting || !dragState.value.connectionStart) return; - - console.log('🔗 连线拖拽结束'); - - // 检查是否释放在目标端口上 - const targetElement = document.elementFromPoint(event.clientX, event.clientY) as HTMLElement; - console.log('🎯 鼠标位置的元素:', targetElement); - console.log('📍 鼠标坐标:', event.clientX, event.clientY); - - // 多种方式查找端口 - let targetPort: HTMLElement | null = null; - - // 方法1: 直接检查当前元素 - if (targetElement?.classList.contains('port')) { - targetPort = targetElement; - console.log('✅ 方法1成功:直接是端口元素'); - } - - // 方法2: 向上查找最近的端口 - if (!targetPort) { - targetPort = targetElement?.closest('.port') as HTMLElement; - if (targetPort) { - console.log('✅ 方法2成功:通过closest找到端口'); - } - } - - // 方法3: 查找当前节点下的所有端口,检查鼠标是否在其范围内 - if (!targetPort) { - const nodeElement = targetElement?.closest('.tree-node') as HTMLElement; - if (nodeElement) { - const ports = nodeElement.querySelectorAll('.port'); - console.log('🔍 在节点中找到', ports.length, '个端口'); - - ports.forEach((port, index) => { - const rect = port.getBoundingClientRect(); - console.log(`端口${index}位置:`, rect); - - if (event.clientX >= rect.left && event.clientX <= rect.right && - event.clientY >= rect.top && event.clientY <= rect.bottom) { - targetPort = port as HTMLElement; - console.log('✅ 方法3成功:鼠标在端口范围内'); - } - }); - } - } - - console.log('🎯 最终找到的端口:', targetPort); - - if (targetPort) { - const targetNodeId = getNodeIdFromElement(targetPort); - const targetPortType = targetPort.classList.contains('port-input') ? 'input' : 'output'; - - console.log('📋 目标节点ID:', targetNodeId); - console.log('🔌 端口类型:', targetPortType); - console.log('🔗 源端口信息:', dragState.value.connectionStart); - - if (targetNodeId && targetNodeId !== dragState.value.connectionStart.nodeId) { - const sourcePort = dragState.value.connectionStart; - const targetPortObj = { nodeId: targetNodeId, portType: targetPortType }; - - const canConn = canConnect(sourcePort, targetPortObj); - console.log('🤔 是否可以连接:', canConn); - - if (canConn) { - if (sourcePort.portType === 'output') { - createConnection(sourcePort.nodeId, targetNodeId); - console.log('✅ 创建连接:', sourcePort.nodeId, '->', targetNodeId); - } else { - createConnection(targetNodeId, sourcePort.nodeId); - console.log('✅ 创建连接:', targetNodeId, '->', sourcePort.nodeId); - } - } else { - console.log('❌ 无法连接:不满足连接条件'); - } - } else { - console.log('❌ 无法连接:目标节点无效或是同一节点'); - } - } else { - console.log('❌ 没有找到目标端口'); - } - - // 清理连线状态 - cancelConnection(); - }; - - const cancelConnection = () => { - dragState.value.isConnecting = false; - dragState.value.connectionStart = null; - tempConnection.value.path = ''; - }; - - const canConnect = (source: { nodeId: string, portType: string }, target: { nodeId: string, portType: string }): boolean => { - // 不能连接自己 - if (source.nodeId === target.nodeId) return false; - - // 必须是输出端口连接到输入端口 - if (source.portType === target.portType) return false; - - // 确定源和目标 - const sourceNodeId = source.portType === 'output' ? source.nodeId : target.nodeId; - const targetNodeId = source.portType === 'output' ? target.nodeId : source.nodeId; - - // 检查是否会创建循环 - if (wouldCreateCycle(sourceNodeId, targetNodeId)) return false; - - // 检查目标节点是否已经有父节点 - const targetNode = getNodeByIdLocal(targetNodeId); - if (targetNode && targetNode.parent) return false; - - return true; - }; - - const wouldCreateCycle = (sourceId: string, targetId: string): boolean => { - const visited = new Set(); - - const checkAncestors = (nodeId: string): boolean => { - if (visited.has(nodeId)) return false; - visited.add(nodeId); - - if (nodeId === sourceId) return true; - - const node = getNodeByIdLocal(nodeId); - if (node && node.parent) { - return checkAncestors(node.parent); - } - - return false; - }; - - return checkAncestors(targetId); - }; - - const createConnection = (sourceId: string, targetId: string) => { - const sourceNode = getNodeByIdLocal(sourceId); - const targetNode = getNodeByIdLocal(targetId); - - if (!sourceNode || !targetNode) return; - - // 更新节点关系 - if (!sourceNode.children.includes(targetId)) { - sourceNode.children.push(targetId); - } - targetNode.parent = sourceId; - - // 更新连接数组 - const existingConnection = connections.value.find(conn => - conn.sourceId === sourceId && conn.targetId === targetId - ); - - if (!existingConnection) { - connections.value.push({ - id: `${sourceId}-${targetId}`, - sourceId, - targetId, - active: false, - path: createConnectionPath(sourceNode, targetNode).path - }); - } - - updateConnections(); - }; - - const removeConnection = (sourceId: string, targetId: string) => { - const sourceNode = getNodeByIdLocal(sourceId); - const targetNode = getNodeByIdLocal(targetId); - - if (sourceNode) { - const index = sourceNode.children.indexOf(targetId); - if (index > -1) { - sourceNode.children.splice(index, 1); - } - } - - if (targetNode) { - targetNode.parent = undefined; - } - - connections.value = connections.value.filter(conn => - !(conn.sourceId === sourceId && conn.targetId === targetId) - ); - - updateConnections(); - }; - - const updateConnections = () => { - connections.value.forEach(conn => { - const sourceNode = getNodeByIdLocal(conn.sourceId); - const targetNode = getNodeByIdLocal(conn.targetId); - - if (sourceNode && targetNode) { - conn.path = createConnectionPath(sourceNode, targetNode).path; - } - }); - }; - - const createConnectionPath = (sourceNode: TreeNode, targetNode: TreeNode) => { - const nodeWidth = 150; - const nodeHeight = 100; - - // 源节点的输出端口位置(底部中心) - const sourceX = sourceNode.x + nodeWidth / 2; - const sourceY = sourceNode.y + nodeHeight; - - // 目标节点的输入端口位置(顶部中心) - const targetX = targetNode.x + nodeWidth / 2; - const targetY = targetNode.y; - - // 创建贝塞尔曲线路径 - const controlOffset = Math.abs(targetY - sourceY) * 0.5; - const path = `M ${sourceX} ${sourceY} C ${sourceX} ${sourceY + controlOffset} ${targetX} ${targetY - controlOffset} ${targetX} ${targetY}`; - - return { - id: `${sourceNode.id}-${targetNode.id}`, - path, - active: false, - sourceId: sourceNode.id, - targetId: targetNode.id - }; - }; - - return { - startConnection, - updateTempConnection, - onConnectionDragEnd, - cancelConnection, - createConnection, - removeConnection, - updateConnections, - canConnect - }; -} \ No newline at end of file diff --git a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/utils/connectionUtils.ts b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/utils/connectionUtils.ts deleted file mode 100644 index 357b949f..00000000 --- a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/utils/connectionUtils.ts +++ /dev/null @@ -1,219 +0,0 @@ -import { TreeNode, Connection, ConnectionPort } from '../types'; -import { getNodeById } from './nodeUtils'; - -/** - * 创建连接路径(贝塞尔曲线) - */ -export function createConnectionPath(sourceNode: TreeNode, targetNode: TreeNode): Connection { - const nodeWidth = 150; - const nodeHeight = 100; - - // 源节点的输出端口位置(底部中心) - const sourceX = sourceNode.x + nodeWidth / 2; - const sourceY = sourceNode.y + nodeHeight; - - // 目标节点的输入端口位置(顶部中心) - const targetX = targetNode.x + nodeWidth / 2; - const targetY = targetNode.y; - - // 创建贝塞尔曲线路径 - const controlOffset = Math.abs(targetY - sourceY) * 0.5; - const path = `M ${sourceX} ${sourceY} C ${sourceX} ${sourceY + controlOffset} ${targetX} ${targetY - controlOffset} ${targetX} ${targetY}`; - - return { - id: `${sourceNode.id}-${targetNode.id}`, - path, - active: false, - sourceId: sourceNode.id, - targetId: targetNode.id - }; -} - -/** - * 创建临时连接路径 - */ -export function createTempConnectionPath( - node: TreeNode, - portType: string, - targetX: number, - targetY: number -): string { - const nodeWidth = 150; - const nodeHeight = 100; - const startX = node.x + nodeWidth / 2; - - let startY: number; - if (portType === 'output') { - startY = node.y + nodeHeight; // 输出端口在底部 - } else { - startY = node.y; // 输入端口在顶部 - } - - // 创建贝塞尔曲线路径 - const controlOffset = Math.abs(targetY - startY) * 0.5; - - let path: string; - if (portType === 'output') { - path = `M ${startX} ${startY} C ${startX} ${startY + controlOffset} ${targetX} ${targetY - controlOffset} ${targetX} ${targetY}`; - } else { - path = `M ${startX} ${startY} C ${startX} ${startY - controlOffset} ${targetX} ${targetY + controlOffset} ${targetX} ${targetY}`; - } - - return path; -} - -/** - * 检查两个端口是否可以连接 - */ -export function canConnect( - source: ConnectionPort, - target: ConnectionPort, - nodes: TreeNode[] -): boolean { - // 不能连接自己 - if (source.nodeId === target.nodeId) return false; - - // 必须是输出端口连接到输入端口 - if (source.portType === target.portType) return false; - - // 确定源和目标 - const sourceNodeId = source.portType === 'output' ? source.nodeId : target.nodeId; - const targetNodeId = source.portType === 'output' ? target.nodeId : source.nodeId; - - // 检查是否会创建循环 - if (wouldCreateCycle(sourceNodeId, targetNodeId, nodes)) return false; - - // 检查目标节点是否已经有父节点 - const targetNode = getNodeById(nodes, targetNodeId); - if (targetNode && targetNode.parent) return false; - - return true; -} - -/** - * 检查是否会创建循环引用 - */ -export function wouldCreateCycle( - sourceId: string, - targetId: string, - nodes: TreeNode[] -): boolean { - const visited = new Set(); - - const checkAncestors = (nodeId: string): boolean => { - if (visited.has(nodeId)) return false; - visited.add(nodeId); - - if (nodeId === sourceId) return true; - - const node = getNodeById(nodes, nodeId); - if (node && node.parent) { - return checkAncestors(node.parent); - } - - return false; - }; - - return checkAncestors(targetId); -} - -/** - * 创建节点间的连接 - */ -export function createConnection( - sourceId: string, - targetId: string, - nodes: TreeNode[], - connections: Connection[] -): { updatedNodes: TreeNode[]; updatedConnections: Connection[] } { - const sourceNode = getNodeById(nodes, sourceId); - const targetNode = getNodeById(nodes, targetId); - - if (!sourceNode || !targetNode) { - return { updatedNodes: nodes, updatedConnections: connections }; - } - - // 更新节点关系 - if (!sourceNode.children.includes(targetId)) { - sourceNode.children.push(targetId); - } - targetNode.parent = sourceId; - - // 更新连接数组 - const existingConnection = connections.find(conn => - conn.sourceId === sourceId && conn.targetId === targetId - ); - - if (!existingConnection) { - const newConnection = createConnectionPath(sourceNode, targetNode); - connections.push(newConnection); - } - - return { - updatedNodes: [...nodes], - updatedConnections: [...connections] - }; -} - -/** - * 移除连接 - */ -export function removeConnection( - sourceId: string, - targetId: string, - nodes: TreeNode[], - connections: Connection[] -): { updatedNodes: TreeNode[]; updatedConnections: Connection[] } { - const sourceNode = getNodeById(nodes, sourceId); - const targetNode = getNodeById(nodes, targetId); - - if (sourceNode) { - const index = sourceNode.children.indexOf(targetId); - if (index > -1) { - sourceNode.children.splice(index, 1); - } - } - - if (targetNode) { - targetNode.parent = undefined; - } - - const updatedConnections = connections.filter(conn => - !(conn.sourceId === sourceId && conn.targetId === targetId) - ); - - return { - updatedNodes: [...nodes], - updatedConnections: updatedConnections - }; -} - -/** - * 更新所有连接的路径 - */ -export function updateAllConnections(connections: Connection[], nodes: TreeNode[]): Connection[] { - return connections.map(conn => { - const sourceNode = getNodeById(nodes, conn.sourceId); - const targetNode = getNodeById(nodes, conn.targetId); - - if (sourceNode && targetNode) { - const updatedConnection = createConnectionPath(sourceNode, targetNode); - return { ...conn, path: updatedConnection.path }; - } - - return conn; - }); -} - -/** - * 从元素中获取节点ID - */ -export function getNodeIdFromElement(element: HTMLElement): string | null { - let current = element; - while (current && current.getAttribute) { - const nodeId = current.getAttribute('data-node-id'); - if (nodeId) return nodeId; - current = current.parentElement as HTMLElement; - } - return null; -} \ No newline at end of file diff --git a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/utils/dragUtils.ts b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/utils/dragUtils.ts deleted file mode 100644 index 246839da..00000000 --- a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/utils/dragUtils.ts +++ /dev/null @@ -1,229 +0,0 @@ -import { DragState, TreeNode } from '../types'; -import { getCanvasCoordinates } from './canvasUtils'; -import { createTempConnectionPath, updateAllConnections } from './connectionUtils'; -import { getNodeById } from './nodeUtils'; - -/** - * 节流函数工厂 - */ -export function createThrottledFunction void>( - fn: T, - delay: number = 16 -): (...args: Parameters) => void { - let timeoutId: number | null = null; - - return (...args: Parameters) => { - if (timeoutId) { - cancelAnimationFrame(timeoutId); - } - timeoutId = requestAnimationFrame(() => { - fn(...args); - }); - }; -} - -/** - * 处理画布鼠标移动事件 - */ -export function handleCanvasMouseMove( - event: MouseEvent, - dragState: DragState, - panX: { value: number }, - panY: { value: number }, - zoomLevel: number, - nodes: TreeNode[], - updateConnections: () => void, - updateTempConnection: (nodeId: string, portType: string, x: number, y: number) => void -): void { - if (dragState.isDraggingCanvas) { - const deltaX = event.clientX - dragState.dragStartX; - const deltaY = event.clientY - dragState.dragStartY; - - panX.value += deltaX; - panY.value += deltaY; - - dragState.dragStartX = event.clientX; - dragState.dragStartY = event.clientY; - - } else if (dragState.isDraggingNode && dragState.dragNodeId) { - const node = getNodeById(nodes, dragState.dragNodeId); - if (node) { - const deltaX = (event.clientX - dragState.dragStartX) / zoomLevel; - const deltaY = (event.clientY - dragState.dragStartY) / zoomLevel; - - node.x = dragState.dragNodeStartX + deltaX; - node.y = dragState.dragNodeStartY + deltaY; - - updateConnections(); - } - - } else if (dragState.isConnecting && dragState.connectionStart) { - let canvasElement = event.currentTarget as HTMLElement | null; - if (!canvasElement) { - canvasElement = document.querySelector('.canvas-area') as HTMLElement | null; - } - - if (canvasElement) { - const { x, y } = getCanvasCoordinates(event, canvasElement, panX.value, panY.value, zoomLevel); - - dragState.connectionEnd.x = x; - dragState.connectionEnd.y = y; - - updateTempConnection( - dragState.connectionStart.nodeId, - dragState.connectionStart.portType, - x, - y - ); - } - } -} - -/** - * 开始节点拖拽 - */ -export function startNodeDrag( - event: MouseEvent, - node: TreeNode, - dragState: DragState, - selectNode: (id: string) => void -): void { - // 检查是否点击的是端口或删除按钮 - const target = event.target as HTMLElement; - if (target.classList.contains('port') || - target.classList.contains('node-delete') || - target.closest('.port') || - target.closest('.node-delete')) { - return; // 不启动节点拖拽 - } - - event.stopPropagation(); - - if (event.button !== 0) return; // 只处理左键 - - dragState.isDraggingNode = true; - dragState.dragNodeId = node.id; - dragState.dragStartX = event.clientX; - dragState.dragStartY = event.clientY; - dragState.dragNodeStartX = node.x; - dragState.dragNodeStartY = node.y; - - const nodeElement = document.querySelector(`[data-node-id="${node.id}"]`) as HTMLElement; - if (nodeElement) { - nodeElement.style.transition = 'none'; - } - - selectNode(node.id); -} - -/** - * 开始连接拖拽 - */ -export function startConnection( - event: MouseEvent, - nodeId: string, - portType: string, - dragState: DragState, - nodes: TreeNode[], - panX: number, - panY: number, - zoomLevel: number, - updateTempConnection: (nodeId: string, portType: string, x: number, y: number) => void -): void { - event.stopPropagation(); - event.preventDefault(); - - const node = getNodeById(nodes, nodeId); - if (!node) return; - - dragState.isConnecting = true; - dragState.connectionStart = { nodeId, portType }; - - const canvasElement = document.querySelector('.canvas-area') as HTMLElement | null; - if (canvasElement) { - const { x, y } = getCanvasCoordinates(event, canvasElement, panX, panY, zoomLevel); - dragState.connectionEnd.x = x; - dragState.connectionEnd.y = y; - - updateTempConnection(nodeId, portType, x, y); - } else { - const target = event.target as HTMLElement; - const rect = target.getBoundingClientRect(); - dragState.connectionEnd.x = event.clientX - rect.left; - dragState.connectionEnd.y = event.clientY - rect.top; - - updateTempConnection(nodeId, portType, dragState.connectionEnd.x, dragState.connectionEnd.y); - } -} - -/** - * 处理鼠标释放事件 - */ -export function handleMouseUp( - event: MouseEvent, - dragState: DragState, - cleanupDrag: () => void -): void { - if (dragState.isDraggingCanvas) { - dragState.isDraggingCanvas = false; - - } else if (dragState.isDraggingNode) { - // 恢复过渡效果 - if (dragState.dragNodeId) { - const nodeElement = document.querySelector(`[data-node-id="${dragState.dragNodeId}"]`) as HTMLElement; - if (nodeElement) { - nodeElement.style.transition = ''; - } - } - - dragState.isDraggingNode = false; - dragState.dragNodeId = null; - - } else if (dragState.isConnecting) { - // 连接拖拽结束的处理由外部函数处理 - return; - } - - cleanupDrag(); -} - -/** - * 取消连接拖拽 - */ -export function cancelConnection( - dragState: DragState, - tempConnection: { path: string } -): void { - dragState.isConnecting = false; - dragState.connectionStart = null; - tempConnection.path = ''; -} - -/** - * 创建更新临时连接的函数 - */ -export function createTempConnectionUpdater( - nodes: TreeNode[], - tempConnection: { path: string } -) { - return (nodeId: string, portType: string, targetX: number, targetY: number) => { - const node = getNodeById(nodes, nodeId); - if (!node) return; - - tempConnection.path = createTempConnectionPath(node, portType, targetX, targetY); - }; -} - -/** - * 创建节流版本的连接更新函数 - */ -export function createThrottledConnectionUpdater( - connections: any[], - nodes: TreeNode[], - onUpdate: (connections: any[]) => void -) { - return createThrottledFunction(() => { - const updatedConnections = updateAllConnections(connections, nodes); - onUpdate(updatedConnections); - }); -} \ No newline at end of file diff --git a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/index.css b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/index.css index 1c60afc5..c707d453 100644 --- a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/index.css +++ b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/index.css @@ -33,6 +33,17 @@ text-shadow: 0 2px 4px rgba(0,0,0,0.3); } +.unsaved-indicator { + color: #ff6b6b; + animation: pulse-unsaved 2s infinite; + margin-left: 8px; +} + +@keyframes pulse-unsaved { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } +} + .toolbar-buttons { display: flex; gap: 8px; @@ -57,6 +68,21 @@ transform: translateY(-1px); } +.tool-btn.has-changes { + background: rgba(255, 107, 107, 0.2); + border-color: #ff6b6b; + animation: glow-save 2s infinite; +} + +@keyframes glow-save { + 0%, 100% { + box-shadow: 0 0 5px rgba(255, 107, 107, 0.5); + } + 50% { + box-shadow: 0 0 15px rgba(255, 107, 107, 0.8); + } +} + .toolbar-right .install-status { display: flex; align-items: center; @@ -215,7 +241,7 @@ .canvas-area { flex: 1; position: relative; - overflow: hidden; + overflow: auto; background: radial-gradient(circle at 20px 20px, #4a5568 1px, transparent 1px), linear-gradient(90deg, transparent 19px, #2d3748 20px, transparent 21px), @@ -228,6 +254,7 @@ top: 0; left: 0; pointer-events: none; + z-index: 0; } .connection-layer { @@ -237,6 +264,7 @@ pointer-events: none; z-index: 1; will-change: transform; + overflow: visible; } .connection-line { @@ -287,11 +315,17 @@ z-index: 2; transform-origin: 0 0; will-change: transform; + overflow: visible; + /* 硬件加速优化 */ + transform: translateZ(0); + backface-visibility: hidden; + perspective: 1000px; } .tree-node { position: absolute; min-width: 150px; + min-height: 80px; background: linear-gradient(145deg, #2d3748 0%, #1a202c 100%); border: 2px solid #4a5568; border-radius: 12px; @@ -857,18 +891,21 @@ border: 2px dashed #667eea; } -/* 拖动状态样式 */ +/* 拖动状态样式 - 优化硬件加速 */ .tree-node.dragging { opacity: 0.9; - transform: scale(1.02) rotate(1deg); + transform: translateZ(0) scale(1.02) rotate(1deg); box-shadow: 0 12px 30px rgba(0,0,0,0.4); z-index: 1000; cursor: grabbing; will-change: transform, opacity; backface-visibility: hidden; transform-style: preserve-3d; - transition: none; + transition: none !important; border-color: #67b7dc; + /* 强制硬件加速 */ + transform-origin: center center; + perspective: 1000px; } /* 节点悬停时的端口显示优化 */ @@ -899,4 +936,72 @@ .dragging { opacity: 0.7; transform: rotate(5deg); +} + +/* 节点属性预览样式 */ +.node-properties-preview { + margin-top: 6px; + padding-top: 6px; + border-top: 1px solid rgba(255, 255, 255, 0.1); + font-size: 10px; +} + +.property-preview-item { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 2px; + padding: 1px 0; +} + +.property-label { + color: #a0aec0; + font-weight: 500; + flex-shrink: 0; + margin-right: 4px; + font-size: 9px; +} + +.property-value { + color: #e2e8f0; + font-weight: 600; + text-align: right; + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 9px; +} + +/* 不同类型属性的颜色 */ +.property-value.property-boolean { + color: #68d391; +} + +.property-value.property-number { + color: #63b3ed; +} + +.property-value.property-select { + color: #f6ad55; +} + +.property-value.property-string { + color: #cbd5e0; +} + +.property-value.property-code { + color: #d69e2e; + font-family: 'Consolas', 'Monaco', monospace; +} + +/* 适应节点宽度 */ +.tree-node .node-body { + max-width: 146px; /* 节点宽度 - padding */ + overflow: hidden; +} + +/* 当节点有属性时稍微增加高度空间 */ +.tree-node:has(.node-properties-preview) { + min-height: 100px; } \ No newline at end of file diff --git a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/template/behavior-tree/BehaviorTreeEditor.html b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/template/behavior-tree/BehaviorTreeEditor.html index c873bb9a..6f757fa7 100644 --- a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/template/behavior-tree/BehaviorTreeEditor.html +++ b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/template/behavior-tree/BehaviorTreeEditor.html @@ -1,19 +1,19 @@
-

🌳 行为树可视化编辑器

+

🌳 行为树可视化编辑器

- -
@@ -205,7 +205,7 @@ { 'node-selected': selectedNodeId === node.id, 'node-error': node.hasError, - 'dragging': dragState.dragNodeId === node.id + 'dragging': dragState.dragNode && dragState.dragNode.id === node.id } ]" :style="{ @@ -220,8 +220,22 @@ {{ node.name }}
-
-
{{ node.description }}
+
+
{{ node.description }}
+ +
+
+ {{ prop.name }}: + + {{ formatPropertyValue(prop) }} + +
+
⚙️ 属性面板
-
+

基本信息

-
+

节点属性

@@ -309,18 +325,21 @@ type="text" :value="prop.value" @input="updateNodeProperty('properties.' + key + '.value', $event.target.value)" + :key="selectedNode.id + '_' + key + '_string'" > @@ -344,8 +370,8 @@
-

代码预览

-
{{ generateNodeCode(selectedNode()) }}
+

节点配置

+
{{ selectedNode ? JSON.stringify(selectedNode, null, 2) : '{}' }}
@@ -380,16 +406,16 @@