import { React, Icons } from '@esengine/editor-runtime'; import type { LucideIcon } from '@esengine/editor-runtime'; import { PropertyDefinition } from '@esengine/behavior-tree'; import { Node as BehaviorTreeNodeType } from '../../domain/models/Node'; import { Connection } from '../../domain/models/Connection'; import { ROOT_NODE_ID } from '../../domain/constants/RootNode'; import type { NodeExecutionStatus } from '../../stores'; import { BehaviorTreeExecutor } from '../../utils/BehaviorTreeExecutor'; import { BlackboardValue } from '../../domain/models/Blackboard'; const { TreePine, Database, AlertTriangle, AlertCircle } = Icons; type BlackboardVariables = Record; interface BehaviorTreeNodeProps { node: BehaviorTreeNodeType; isSelected: boolean; isBeingDragged: boolean; dragDelta: { dx: number; dy: number }; uncommittedNodeIds: Set; blackboardVariables: BlackboardVariables; initialBlackboardVariables: BlackboardVariables; isExecuting: boolean; executionStatus?: NodeExecutionStatus; executionOrder?: number; connections: Connection[]; nodes: BehaviorTreeNodeType[]; executorRef: React.RefObject; iconMap: Record; draggingNodeId: string | null; onNodeClick: (e: React.MouseEvent, node: BehaviorTreeNodeType) => void; onContextMenu: (e: React.MouseEvent, node: BehaviorTreeNodeType) => void; onNodeMouseDown: (e: React.MouseEvent, nodeId: string) => void; onNodeMouseUpForConnection: (e: React.MouseEvent, nodeId: string) => void; onPortMouseDown: (e: React.MouseEvent, nodeId: string, propertyName?: string) => void; onPortMouseUp: (e: React.MouseEvent, nodeId: string, propertyName?: string) => void; } const BehaviorTreeNodeComponent: React.FC = ({ node, isSelected, isBeingDragged, dragDelta, uncommittedNodeIds, blackboardVariables, initialBlackboardVariables, isExecuting, executionStatus, executionOrder, connections, nodes, executorRef, iconMap, draggingNodeId, onNodeClick, onContextMenu, onNodeMouseDown, onNodeMouseUpForConnection, onPortMouseDown, onPortMouseUp }) => { const isRoot = node.id === ROOT_NODE_ID; const isBlackboardVariable = node.data.nodeType === 'blackboard-variable'; const posX = node.position.x + (isBeingDragged ? dragDelta.dx : 0); const posY = node.position.y + (isBeingDragged ? dragDelta.dy : 0); const isUncommitted = uncommittedNodeIds.has(node.id); const nodeClasses = [ 'bt-node', isSelected && 'selected', isRoot && 'root', isUncommitted && 'uncommitted', executionStatus && executionStatus !== 'idle' && executionStatus ].filter(Boolean).join(' '); return (
onNodeClick(e, node)} onContextMenu={(e) => onContextMenu(e, node)} onMouseDown={(e) => onNodeMouseDown(e, node.id)} onMouseUp={(e) => onNodeMouseUpForConnection(e, node.id)} onDragStart={(e) => e.preventDefault()} style={{ left: posX, top: posY, transform: 'translate(-50%, -50%)', cursor: isRoot ? 'default' : (draggingNodeId === node.id ? 'grabbing' : 'grab'), transition: draggingNodeId === node.id ? 'none' : 'all 0.2s', zIndex: isRoot ? 50 : (draggingNodeId === node.id ? 100 : (isSelected ? 10 : 1)) }} > {/* 执行顺序角标 - 使用绝对定位,不影响节点布局 */} {executionOrder !== undefined && (
{executionOrder}
)} {isBlackboardVariable ? ( (() => { const varName = node.data.variableName as string; const currentValue = blackboardVariables[varName]; const initialValue = initialBlackboardVariables[varName]; const isModified = isExecuting && JSON.stringify(currentValue) !== JSON.stringify(initialValue); return ( <>
{varName || 'Variable'}
{isModified && ( 运行时 )}
{JSON.stringify(currentValue)}
onPortMouseDown(e, node.id, '__value__')} onMouseUp={(e) => onPortMouseUp(e, node.id, '__value__')} className="bt-node-port bt-node-port-variable-output" title="Output" /> ); })() ) : ( <>
{isRoot ? ( ) : ( node.template.icon && (() => { const IconComponent = iconMap[node.template.icon]; return IconComponent ? ( ) : ( {node.template.icon} ); })() )}
{isRoot ? 'ROOT' : node.template.displayName}
#{node.id}
{!isRoot && node.template.className && executorRef.current && !executorRef.current.hasExecutor(node.template.className) && (
e.stopPropagation()} >
缺失执行器:找不到节点对应的执行器 "{node.template.className}"
)} {isUncommitted && (
e.stopPropagation()} >
未生效节点:运行时添加的节点,需重新运行才能生效
)} {!isRoot && !isUncommitted && node.template.type === 'composite' && (node.template.requiresChildren === undefined || node.template.requiresChildren === true) && !nodes.some((n) => connections.some((c) => c.from === node.id && c.to === n.id) ) && (
e.stopPropagation()} >
空节点:没有子节点,执行时会直接跳过
)}
{!isRoot && (
{node.template.category}
)} {node.template.properties.length > 0 && (
{node.template.properties.map((prop: PropertyDefinition, idx: number) => { const hasConnection = connections.some( (conn: Connection) => conn.toProperty === prop.name && conn.to === node.id ); const propValue = node.data[prop.name]; return (
onPortMouseDown(e, node.id, prop.name)} onMouseUp={(e) => onPortMouseUp(e, node.id, prop.name)} className={`bt-node-port bt-node-port-property ${hasConnection ? 'connected' : ''}`} title={prop.description || prop.name} /> {prop.name}: {propValue !== undefined && ( {String(propValue)} )}
); })}
)}
{!isRoot && (
onPortMouseDown(e, node.id)} onMouseUp={(e) => onPortMouseUp(e, node.id)} className="bt-node-port bt-node-port-input" title="Input" /> )} {(isRoot || node.template.type === 'composite' || node.template.type === 'decorator') && (node.template.requiresChildren === undefined || node.template.requiresChildren === true) && (
onPortMouseDown(e, node.id)} onMouseUp={(e) => onPortMouseUp(e, node.id)} className="bt-node-port bt-node-port-output" title="Output" /> )} )}
); }; /** * 使用 React.memo 优化节点组件性能 * 只在关键 props 变化时重新渲染 */ export const BehaviorTreeNode = React.memo(BehaviorTreeNodeComponent, (prevProps, nextProps) => { // 如果节点本身变化,需要重新渲染 if (prevProps.node.id !== nextProps.node.id || prevProps.node.position.x !== nextProps.node.position.x || prevProps.node.position.y !== nextProps.node.position.y || prevProps.node.template.className !== nextProps.node.template.className) { return false; } if (prevProps.isSelected !== nextProps.isSelected || prevProps.isBeingDragged !== nextProps.isBeingDragged || prevProps.executionStatus !== nextProps.executionStatus || prevProps.executionOrder !== nextProps.executionOrder || prevProps.draggingNodeId !== nextProps.draggingNodeId) { return false; } // 如果正在被拖拽,且 dragDelta 变化,需要重新渲染 if (nextProps.isBeingDragged && (prevProps.dragDelta.dx !== nextProps.dragDelta.dx || prevProps.dragDelta.dy !== nextProps.dragDelta.dy)) { return false; } // 如果执行状态变化,需要重新渲染 if (prevProps.isExecuting !== nextProps.isExecuting) { return false; } // 检查 uncommittedNodeIds 中是否包含当前节点 const prevUncommitted = prevProps.uncommittedNodeIds.has(nextProps.node.id); const nextUncommitted = nextProps.uncommittedNodeIds.has(nextProps.node.id); if (prevUncommitted !== nextUncommitted) { return false; } // 节点数据变化时需要重新渲染 if (JSON.stringify(prevProps.node.data) !== JSON.stringify(nextProps.node.data)) { return false; } // 其他情况不重新渲染 return true; });