* 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): 修复字符串替换安全问题
165 lines
5.3 KiB
TypeScript
165 lines
5.3 KiB
TypeScript
import React, { useMemo } from 'react';
|
|
import { ConnectionViewData } from '../../types';
|
|
import { Node } from '../../domain/models/Node';
|
|
|
|
interface ConnectionRendererProps {
|
|
connectionData: ConnectionViewData;
|
|
fromNode: Node;
|
|
toNode: Node;
|
|
getPortPosition: (nodeId: string, propertyName?: string, portType?: 'input' | 'output') => { x: number; y: number } | null;
|
|
onClick?: (e: React.MouseEvent, fromId: string, toId: string) => void;
|
|
onContextMenu?: (e: React.MouseEvent, fromId: string, toId: string) => void;
|
|
}
|
|
const ConnectionRendererComponent: React.FC<ConnectionRendererProps> = ({
|
|
connectionData,
|
|
fromNode,
|
|
toNode,
|
|
getPortPosition,
|
|
onClick,
|
|
onContextMenu
|
|
}) => {
|
|
const { connection, isSelected } = connectionData;
|
|
|
|
const pathData = useMemo(() => {
|
|
let fromPos, toPos;
|
|
|
|
if (connection.connectionType === 'property') {
|
|
// 属性连接:使用 fromProperty 和 toProperty
|
|
fromPos = getPortPosition(connection.from, connection.fromProperty);
|
|
toPos = getPortPosition(connection.to, connection.toProperty);
|
|
} else {
|
|
// 节点连接:使用输出和输入端口
|
|
fromPos = getPortPosition(connection.from, undefined, 'output');
|
|
toPos = getPortPosition(connection.to, undefined, 'input');
|
|
}
|
|
|
|
if (!fromPos || !toPos) {
|
|
return null;
|
|
}
|
|
|
|
const x1 = fromPos.x;
|
|
const y1 = fromPos.y;
|
|
const x2 = toPos.x;
|
|
const y2 = toPos.y;
|
|
|
|
let pathD: string;
|
|
|
|
if (connection.connectionType === 'property') {
|
|
const controlX1 = x1 + (x2 - x1) * 0.5;
|
|
const controlX2 = x1 + (x2 - x1) * 0.5;
|
|
pathD = `M ${x1} ${y1} C ${controlX1} ${y1}, ${controlX2} ${y2}, ${x2} ${y2}`;
|
|
} else {
|
|
const controlY = y1 + (y2 - y1) * 0.5;
|
|
pathD = `M ${x1} ${y1} C ${x1} ${controlY}, ${x2} ${controlY}, ${x2} ${y2}`;
|
|
}
|
|
|
|
return {
|
|
path: pathD,
|
|
midX: (x1 + x2) / 2,
|
|
midY: (y1 + y2) / 2
|
|
};
|
|
}, [connection, fromNode, toNode, getPortPosition]);
|
|
|
|
const isPropertyConnection = connection.connectionType === 'property';
|
|
|
|
const color = isPropertyConnection ? '#ab47bc' : '#00bcd4';
|
|
const glowColor = isPropertyConnection ? 'rgba(171, 71, 188, 0.6)' : 'rgba(0, 188, 212, 0.6)';
|
|
const strokeColor = isSelected ? '#FFD700' : color;
|
|
const strokeWidth = isSelected ? 3.5 : 2.5;
|
|
|
|
const gradientId = `gradient-${connection.from}-${connection.to}`;
|
|
|
|
if (!pathData) {
|
|
return null;
|
|
}
|
|
|
|
const pathD = pathData.path;
|
|
const endPosMatch = pathD.match(/C [0-9.-]+ [0-9.-]+, [0-9.-]+ [0-9.-]+, ([0-9.-]+) ([0-9.-]+)/);
|
|
const endX = endPosMatch ? parseFloat(endPosMatch[1]) : 0;
|
|
const endY = endPosMatch ? parseFloat(endPosMatch[2]) : 0;
|
|
|
|
const handleClick = (e: React.MouseEvent) => {
|
|
e.stopPropagation();
|
|
onClick?.(e, connection.from, connection.to);
|
|
};
|
|
|
|
const handleContextMenu = (e: React.MouseEvent) => {
|
|
e.stopPropagation();
|
|
onContextMenu?.(e, connection.from, connection.to);
|
|
};
|
|
|
|
return (
|
|
<g
|
|
className="connection"
|
|
onClick={handleClick}
|
|
onContextMenu={handleContextMenu}
|
|
style={{ cursor: 'pointer' }}
|
|
data-connection-from={connection.from}
|
|
data-connection-to={connection.to}
|
|
>
|
|
<defs>
|
|
<linearGradient id={gradientId} x1="0%" y1="0%" x2="100%" y2="0%">
|
|
<stop offset="0%" stopColor={strokeColor} stopOpacity="0.8" />
|
|
<stop offset="50%" stopColor={strokeColor} stopOpacity="1" />
|
|
<stop offset="100%" stopColor={strokeColor} stopOpacity="0.8" />
|
|
</linearGradient>
|
|
</defs>
|
|
|
|
<path
|
|
d={pathData.path}
|
|
fill="none"
|
|
stroke="transparent"
|
|
strokeWidth={24}
|
|
/>
|
|
|
|
<path
|
|
d={pathData.path}
|
|
fill="none"
|
|
stroke={glowColor}
|
|
strokeWidth={strokeWidth + 2}
|
|
strokeLinecap="round"
|
|
opacity={isSelected ? 0.4 : 0.2}
|
|
/>
|
|
|
|
<path
|
|
d={pathData.path}
|
|
fill="none"
|
|
stroke={`url(#${gradientId})`}
|
|
strokeWidth={strokeWidth}
|
|
strokeLinecap="round"
|
|
/>
|
|
|
|
<circle
|
|
cx={endX}
|
|
cy={endY}
|
|
r="5"
|
|
fill={strokeColor}
|
|
stroke="rgba(0, 0, 0, 0.3)"
|
|
strokeWidth="1"
|
|
/>
|
|
|
|
{isSelected && (
|
|
<>
|
|
<circle
|
|
cx={pathData.midX}
|
|
cy={pathData.midY}
|
|
r="8"
|
|
fill={strokeColor}
|
|
opacity="0.3"
|
|
/>
|
|
<circle
|
|
cx={pathData.midX}
|
|
cy={pathData.midY}
|
|
r="5"
|
|
fill={strokeColor}
|
|
stroke="rgba(0, 0, 0, 0.5)"
|
|
strokeWidth="2"
|
|
/>
|
|
</>
|
|
)}
|
|
</g>
|
|
);
|
|
};
|
|
|
|
export const ConnectionRenderer = ConnectionRendererComponent;
|