refactor(editor): 提取行为树编辑器为独立包并重构编辑器架构 (#216)
* refactor(editor): 提取行为树编辑器为独立包并重构编辑器架构 * feat(editor): 添加插件市场功能 * feat(editor): 重构插件市场以支持版本管理和ZIP打包 * feat(editor): 重构插件发布流程并修复React渲染警告 * fix(plugin): 修复插件发布和市场的路径不一致问题 * feat: 重构插件发布流程并添加插件删除功能 * fix(editor): 完善插件删除功能并修复多个关键问题 * fix(auth): 修复自动登录与手动登录的竞态条件问题 * feat(editor): 重构插件管理流程 * feat(editor): 支持 ZIP 文件直接发布插件 - 新增 PluginSourceParser 解析插件源 - 重构发布流程支持文件夹和 ZIP 两种方式 - 优化发布向导 UI * feat(editor): 插件市场支持多版本安装 - 插件解压到项目 plugins 目录 - 新增 Tauri 后端安装/卸载命令 - 支持选择任意版本安装 - 修复打包逻辑,保留完整 dist 目录结构 * feat(editor): 个人中心支持多版本管理 - 合并同一插件的不同版本 - 添加版本历史展开/折叠功能 - 禁止有待审核 PR 时更新插件 * fix(editor): 修复 InspectorRegistry 服务注册 - InspectorRegistry 实现 IService 接口 - 注册到 Core.services 供插件使用 * feat(behavior-tree-editor): 完善插件注册和文件操作 - 添加文件创建模板和操作处理器 - 实现右键菜单创建行为树功能 - 修复文件读取权限问题(使用 Tauri 命令) - 添加 BehaviorTreeEditorPanel 组件 - 修复 rollup 配置支持动态导入 * feat(plugin): 完善插件构建和发布流程 * fix(behavior-tree-editor): 完整恢复编辑器并修复 Toast 集成 * fix(behavior-tree-editor): 修复节点选中、连线跟随和文件加载问题并优化性能 * fix(behavior-tree-editor): 修复端口连接失败问题并优化连线样式 * refactor(behavior-tree-editor): 移除调试面板功能简化代码结构 * refactor(behavior-tree-editor): 清理冗余代码合并重复逻辑 * feat(behavior-tree-editor): 完善编辑器核心功能增强扩展性 * fix(lint): 修复ESLint错误确保CI通过 * refactor(behavior-tree-editor): 优化编辑器工具栏和编译器功能 * refactor(behavior-tree-editor): 清理技术债务,优化代码质量 * fix(editor-app): 修复字符串替换安全问题
This commit is contained in:
@@ -0,0 +1,407 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
TreePine,
|
||||
Database,
|
||||
AlertTriangle,
|
||||
AlertCircle,
|
||||
LucideIcon
|
||||
} from 'lucide-react';
|
||||
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';
|
||||
|
||||
type BlackboardVariables = Record<string, BlackboardValue>;
|
||||
|
||||
interface BehaviorTreeNodeProps {
|
||||
node: BehaviorTreeNodeType;
|
||||
isSelected: boolean;
|
||||
isBeingDragged: boolean;
|
||||
dragDelta: { dx: number; dy: number };
|
||||
uncommittedNodeIds: Set<string>;
|
||||
blackboardVariables: BlackboardVariables;
|
||||
initialBlackboardVariables: BlackboardVariables;
|
||||
isExecuting: boolean;
|
||||
executionStatus?: NodeExecutionStatus;
|
||||
executionOrder?: number;
|
||||
connections: Connection[];
|
||||
nodes: BehaviorTreeNodeType[];
|
||||
executorRef: React.RefObject<BehaviorTreeExecutor | null>;
|
||||
iconMap: Record<string, LucideIcon>;
|
||||
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<BehaviorTreeNodeProps> = ({
|
||||
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 (
|
||||
<div
|
||||
key={node.id}
|
||||
data-node-id={node.id}
|
||||
className={nodeClasses}
|
||||
onClick={(e) => 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 && (
|
||||
<div
|
||||
className="bt-node-execution-badge"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '-8px',
|
||||
right: '-8px',
|
||||
backgroundColor: '#2196f3',
|
||||
color: '#fff',
|
||||
borderRadius: '50%',
|
||||
minWidth: '24px',
|
||||
height: '24px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '12px',
|
||||
fontWeight: 'bold',
|
||||
padding: '0 6px',
|
||||
boxShadow: '0 2px 8px rgba(33, 150, 243, 0.5)',
|
||||
border: '2px solid #1a1a1d',
|
||||
zIndex: 10,
|
||||
pointerEvents: 'none'
|
||||
}}
|
||||
title={`执行顺序: ${executionOrder}`}
|
||||
>
|
||||
{executionOrder}
|
||||
</div>
|
||||
)}
|
||||
{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 (
|
||||
<>
|
||||
<div className="bt-node-header blackboard">
|
||||
<Database size={16} className="bt-node-header-icon" />
|
||||
<div className="bt-node-header-title">
|
||||
{varName || 'Variable'}
|
||||
</div>
|
||||
{isModified && (
|
||||
<span style={{
|
||||
fontSize: '9px',
|
||||
color: '#ffbb00',
|
||||
backgroundColor: 'rgba(255, 187, 0, 0.2)',
|
||||
padding: '2px 4px',
|
||||
borderRadius: '2px',
|
||||
marginLeft: '4px'
|
||||
}}>
|
||||
运行时
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="bt-node-body">
|
||||
<div
|
||||
className="bt-node-blackboard-value"
|
||||
style={{
|
||||
backgroundColor: isModified ? 'rgba(255, 187, 0, 0.15)' : 'transparent',
|
||||
border: isModified ? '1px solid rgba(255, 187, 0, 0.3)' : 'none',
|
||||
borderRadius: '2px',
|
||||
padding: '2px 4px'
|
||||
}}
|
||||
title={isModified ? `初始值: ${JSON.stringify(initialValue)}\n当前值: ${JSON.stringify(currentValue)}` : undefined}
|
||||
>
|
||||
{JSON.stringify(currentValue)}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
data-port="true"
|
||||
data-node-id={node.id}
|
||||
data-property="__value__"
|
||||
data-port-type="variable-output"
|
||||
onMouseDown={(e) => onPortMouseDown(e, node.id, '__value__')}
|
||||
onMouseUp={(e) => onPortMouseUp(e, node.id, '__value__')}
|
||||
className="bt-node-port bt-node-port-variable-output"
|
||||
title="Output"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
})()
|
||||
) : (
|
||||
<>
|
||||
<div className={`bt-node-header ${isRoot ? 'root' : (node.template.type || 'action')}`}>
|
||||
{isRoot ? (
|
||||
<TreePine size={16} className="bt-node-header-icon" />
|
||||
) : (
|
||||
node.template.icon && (() => {
|
||||
const IconComponent = iconMap[node.template.icon];
|
||||
return IconComponent ? (
|
||||
<IconComponent size={16} className="bt-node-header-icon" />
|
||||
) : (
|
||||
<span className="bt-node-header-icon">{node.template.icon}</span>
|
||||
);
|
||||
})()
|
||||
)}
|
||||
<div className="bt-node-header-title">
|
||||
<div>{isRoot ? 'ROOT' : node.template.displayName}</div>
|
||||
<div className="bt-node-id" title={node.id}>
|
||||
#{node.id}
|
||||
</div>
|
||||
</div>
|
||||
{!isRoot && node.template.className && executorRef.current && !executorRef.current.hasExecutor(node.template.className) && (
|
||||
<div
|
||||
className="bt-node-missing-executor-warning"
|
||||
style={{
|
||||
marginLeft: 'auto',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
cursor: 'help',
|
||||
pointerEvents: 'auto',
|
||||
position: 'relative'
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<AlertCircle
|
||||
size={14}
|
||||
style={{
|
||||
color: '#f44336',
|
||||
flexShrink: 0
|
||||
}}
|
||||
/>
|
||||
<div className="bt-node-missing-executor-tooltip">
|
||||
缺失执行器:找不到节点对应的执行器 "{node.template.className}"
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{isUncommitted && (
|
||||
<div
|
||||
className="bt-node-uncommitted-warning"
|
||||
style={{
|
||||
marginLeft: (!isRoot && node.template.className && executorRef.current && !executorRef.current.hasExecutor(node.template.className)) ? '4px' : 'auto',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
cursor: 'help',
|
||||
pointerEvents: 'auto',
|
||||
position: 'relative'
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<AlertTriangle
|
||||
size={14}
|
||||
style={{
|
||||
color: '#ff5722',
|
||||
flexShrink: 0
|
||||
}}
|
||||
/>
|
||||
<div className="bt-node-uncommitted-tooltip">
|
||||
未生效节点:运行时添加的节点,需重新运行才能生效
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!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)
|
||||
) && (
|
||||
<div
|
||||
className="bt-node-empty-warning-container"
|
||||
style={{
|
||||
marginLeft: ((!isRoot && node.template.className && executorRef.current && !executorRef.current.hasExecutor(node.template.className)) || isUncommitted) ? '4px' : 'auto',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
cursor: 'help',
|
||||
pointerEvents: 'auto',
|
||||
position: 'relative'
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<AlertTriangle
|
||||
size={14}
|
||||
style={{
|
||||
color: '#ff9800',
|
||||
flexShrink: 0
|
||||
}}
|
||||
/>
|
||||
<div className="bt-node-empty-warning-tooltip">
|
||||
空节点:没有子节点,执行时会直接跳过
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="bt-node-body">
|
||||
{!isRoot && (
|
||||
<div className="bt-node-category">
|
||||
{node.template.category}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{node.template.properties.length > 0 && (
|
||||
<div className="bt-node-properties">
|
||||
{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 (
|
||||
<div key={idx} className="bt-node-property">
|
||||
<div
|
||||
data-port="true"
|
||||
data-node-id={node.id}
|
||||
data-property={prop.name}
|
||||
data-port-type="property-input"
|
||||
onMouseDown={(e) => 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}
|
||||
/>
|
||||
<span
|
||||
className="bt-node-property-label"
|
||||
title={prop.description}
|
||||
>
|
||||
{prop.name}:
|
||||
</span>
|
||||
{propValue !== undefined && (
|
||||
<span className="bt-node-property-value">
|
||||
{String(propValue)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!isRoot && (
|
||||
<div
|
||||
data-port="true"
|
||||
data-node-id={node.id}
|
||||
data-port-type="node-input"
|
||||
onMouseDown={(e) => 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) && (
|
||||
<div
|
||||
data-port="true"
|
||||
data-node-id={node.id}
|
||||
data-port-type="node-output"
|
||||
onMouseDown={(e) => onPortMouseDown(e, node.id)}
|
||||
onMouseUp={(e) => onPortMouseUp(e, node.id)}
|
||||
className="bt-node-port bt-node-port-output"
|
||||
title="Output"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 使用 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;
|
||||
});
|
||||
@@ -0,0 +1,219 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import * as LucideIcons from 'lucide-react';
|
||||
import type { LucideIcon } from 'lucide-react';
|
||||
import { NodeViewData } from '../../types';
|
||||
|
||||
/**
|
||||
* 图标映射
|
||||
*/
|
||||
const iconMap: Record<string, LucideIcon> = {
|
||||
TreePine: LucideIcons.TreePine,
|
||||
GitBranch: LucideIcons.GitBranch,
|
||||
Shuffle: LucideIcons.Shuffle,
|
||||
Repeat: LucideIcons.Repeat,
|
||||
RotateCcw: LucideIcons.RotateCcw,
|
||||
FlipHorizontal: LucideIcons.FlipHorizontal,
|
||||
CheckCircle: LucideIcons.CheckCircle,
|
||||
XCircle: LucideIcons.XCircle,
|
||||
Play: LucideIcons.Play,
|
||||
Pause: LucideIcons.Pause,
|
||||
Square: LucideIcons.Square,
|
||||
Circle: LucideIcons.Circle,
|
||||
Diamond: LucideIcons.Diamond,
|
||||
Box: LucideIcons.Box,
|
||||
Flag: LucideIcons.Flag,
|
||||
Target: LucideIcons.Target
|
||||
};
|
||||
|
||||
/**
|
||||
* 节点渲染器属性
|
||||
*/
|
||||
interface BehaviorTreeNodeRendererProps {
|
||||
/**
|
||||
* 节点视图数据
|
||||
*/
|
||||
nodeData: NodeViewData;
|
||||
|
||||
/**
|
||||
* 节点点击事件
|
||||
*/
|
||||
onClick?: (e: React.MouseEvent, nodeId: string) => void;
|
||||
|
||||
/**
|
||||
* 节点双击事件
|
||||
*/
|
||||
onDoubleClick?: (e: React.MouseEvent, nodeId: string) => void;
|
||||
|
||||
/**
|
||||
* 节点右键事件
|
||||
*/
|
||||
onContextMenu?: (e: React.MouseEvent, nodeId: string) => void;
|
||||
|
||||
/**
|
||||
* 鼠标按下事件
|
||||
*/
|
||||
onMouseDown?: (e: React.MouseEvent, nodeId: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 行为树节点渲染器
|
||||
* 负责单个节点的渲染
|
||||
*/
|
||||
export const BehaviorTreeNodeRenderer: React.FC<BehaviorTreeNodeRendererProps> = ({
|
||||
nodeData,
|
||||
onClick,
|
||||
onDoubleClick,
|
||||
onContextMenu,
|
||||
onMouseDown
|
||||
}) => {
|
||||
const { node, isSelected, isDragging, executionStatus } = nodeData;
|
||||
const { template, position } = node;
|
||||
|
||||
const IconComponent = iconMap[template.icon || 'Box'] || LucideIcons.Box;
|
||||
|
||||
const nodeStyle = useMemo(() => {
|
||||
let borderColor = template.color || '#4a9eff';
|
||||
const backgroundColor = '#2a2a2a';
|
||||
let boxShadow = 'none';
|
||||
|
||||
if (isSelected) {
|
||||
boxShadow = `0 0 0 2px ${borderColor}`;
|
||||
}
|
||||
|
||||
if (executionStatus === 'running') {
|
||||
borderColor = '#ffa500';
|
||||
boxShadow = `0 0 10px ${borderColor}`;
|
||||
} else if (executionStatus === 'success') {
|
||||
borderColor = '#00ff00';
|
||||
} else if (executionStatus === 'failure') {
|
||||
borderColor = '#ff0000';
|
||||
}
|
||||
|
||||
return {
|
||||
position: 'absolute' as const,
|
||||
left: position.x,
|
||||
top: position.y,
|
||||
minWidth: '180px',
|
||||
padding: '12px',
|
||||
backgroundColor,
|
||||
borderRadius: '8px',
|
||||
border: `2px solid ${borderColor}`,
|
||||
boxShadow,
|
||||
cursor: 'pointer',
|
||||
userSelect: 'none' as const,
|
||||
transition: 'box-shadow 0.2s',
|
||||
opacity: isDragging ? 0.7 : 1
|
||||
};
|
||||
}, [template.color, position, isSelected, isDragging, executionStatus]);
|
||||
|
||||
const handleClick = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
onClick?.(e, node.id);
|
||||
};
|
||||
|
||||
const handleDoubleClick = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
onDoubleClick?.(e, node.id);
|
||||
};
|
||||
|
||||
const handleContextMenu = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
onContextMenu?.(e, node.id);
|
||||
};
|
||||
|
||||
const handleMouseDown = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
onMouseDown?.(e, node.id);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="behavior-tree-node"
|
||||
style={nodeStyle}
|
||||
onClick={handleClick}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
onContextMenu={handleContextMenu}
|
||||
onMouseDown={handleMouseDown}
|
||||
data-node-id={node.id}
|
||||
>
|
||||
{/* 节点头部 */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
marginBottom: '4px'
|
||||
}}>
|
||||
<IconComponent size={20} color={template.color || '#4a9eff'} />
|
||||
<div style={{
|
||||
fontSize: '14px',
|
||||
fontWeight: 'bold',
|
||||
color: '#ffffff',
|
||||
flex: 1
|
||||
}}>
|
||||
{template.displayName}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 节点类型 */}
|
||||
{template.category && (
|
||||
<div style={{
|
||||
fontSize: '11px',
|
||||
color: '#888888',
|
||||
marginBottom: '4px'
|
||||
}}>
|
||||
{template.category}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 节点描述 */}
|
||||
{template.description && (
|
||||
<div style={{
|
||||
fontSize: '12px',
|
||||
color: '#cccccc',
|
||||
marginTop: '8px',
|
||||
lineHeight: '1.4'
|
||||
}}>
|
||||
{template.description}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 输入连接点 */}
|
||||
<div
|
||||
className="node-input-pin"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '-6px',
|
||||
width: '12px',
|
||||
height: '12px',
|
||||
borderRadius: '50%',
|
||||
backgroundColor: template.color || '#4a9eff',
|
||||
border: '2px solid #1a1a1a',
|
||||
transform: 'translateY(-50%)',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
data-pin-type="input"
|
||||
data-node-id={node.id}
|
||||
/>
|
||||
|
||||
{/* 输出连接点 */}
|
||||
<div
|
||||
className="node-output-pin"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
right: '-6px',
|
||||
width: '12px',
|
||||
height: '12px',
|
||||
borderRadius: '50%',
|
||||
backgroundColor: template.color || '#4a9eff',
|
||||
border: '2px solid #1a1a1a',
|
||||
transform: 'translateY(-50%)',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
data-pin-type="output"
|
||||
data-node-id={node.id}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { BehaviorTreeNodeRenderer } from './BehaviorTreeNodeRenderer';
|
||||
Reference in New Issue
Block a user