|
|
@@ -14,6 +14,9 @@ export function useCanvasLogic(connections, externalControllerRef, nodes = []) {
|
|
|
const canvasControllerRef = useRef(null);
|
|
|
const connectionManagerRef = useRef(null);
|
|
|
const nodeWidthCacheRef = useRef(new Map()); // 缓存节点实际尺寸 {width, height}
|
|
|
+ // 端口坐标缓存:{ nodeId: { portId: { x, y }, nodeX, nodeY } }
|
|
|
+ // nodeX, nodeY 是缓存时节点的位置,用于计算偏差量
|
|
|
+ const portPositionCacheRef = useRef(new Map());
|
|
|
const [transform, setTransform] = useState({ scale: 1, translateX: 0, translateY: 0 });
|
|
|
const [connectingStart, setConnectingStart] = useState(null);
|
|
|
const [connectingEnd, setConnectingEnd] = useState(null);
|
|
|
@@ -46,10 +49,162 @@ export function useCanvasLogic(connections, externalControllerRef, nodes = []) {
|
|
|
connectionManagerRef.current = new ConnectionManager(connections);
|
|
|
}, [connections]);
|
|
|
|
|
|
- // 当节点更新时,清除宽度缓存(节点内容可能改变导致宽度变化)
|
|
|
+ // 当节点数量变化时,清除缓存(节点内容可能改变)
|
|
|
useEffect(() => {
|
|
|
nodeWidthCacheRef.current.clear();
|
|
|
- }, [nodes]);
|
|
|
+ portPositionCacheRef.current.clear();
|
|
|
+ }, [nodes.length]);
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 从 DOM 获取端口的精确坐标并缓存
|
|
|
+ * @param {string} nodeId - 节点ID
|
|
|
+ * @param {Object} node - 节点对象
|
|
|
+ * @returns {Object|null} 该节点所有端口的坐标 { portId: { x, y } }
|
|
|
+ */
|
|
|
+ const cacheNodePortPositions = (nodeId, node) => {
|
|
|
+ if (!canvasRef.current) return null;
|
|
|
+
|
|
|
+ const nodeElement = canvasRef.current.querySelector(`[data-node-id="${nodeId}"]`);
|
|
|
+ if (!nodeElement) return null;
|
|
|
+
|
|
|
+ const portPositions = {};
|
|
|
+ const allPorts = [...(node.inputs || []), ...(node.outputs || [])];
|
|
|
+
|
|
|
+ allPorts.forEach(port => {
|
|
|
+ // 查找端口元素
|
|
|
+ const isInput = (node.inputs || []).some(p => p.id === port.id);
|
|
|
+ const isExecutionPort = port.type === 'execution';
|
|
|
+
|
|
|
+ const portElements = nodeElement.querySelectorAll('.Blueprint-node-port');
|
|
|
+ const matchingPorts = Array.from(portElements).filter(portEl => {
|
|
|
+ const isInputPort = portEl.classList.contains('input');
|
|
|
+ const isOutputPort = portEl.classList.contains('output');
|
|
|
+ const isExecPort = portEl.classList.contains('port-execution');
|
|
|
+
|
|
|
+ const directionMatch = (isInput && isInputPort) || (!isInput && isOutputPort);
|
|
|
+ const typeMatch = isExecutionPort ? isExecPort : !isExecPort;
|
|
|
+
|
|
|
+ return directionMatch && typeMatch;
|
|
|
+ });
|
|
|
+
|
|
|
+ // 获取端口在同类型中的索引
|
|
|
+ const ports = isInput
|
|
|
+ ? (node.inputs || []).filter(p => isExecutionPort ? p.type === 'execution' : p.type !== 'execution')
|
|
|
+ : (node.outputs || []).filter(p => isExecutionPort ? p.type === 'execution' : p.type !== 'execution');
|
|
|
+ const portIndex = ports.findIndex(p => p.id === port.id);
|
|
|
+
|
|
|
+ if (portIndex >= 0 && portIndex < matchingPorts.length) {
|
|
|
+ const portEl = matchingPorts[portIndex];
|
|
|
+ const dotElement = portEl.querySelector('.Blueprint-node-port-dot');
|
|
|
+ const arrowElement = portEl.querySelector('.Blueprint-node-port-arrow');
|
|
|
+ const targetElement = dotElement || arrowElement;
|
|
|
+
|
|
|
+ if (targetElement) {
|
|
|
+ const targetRect = targetElement.getBoundingClientRect();
|
|
|
+ const canvasRect = canvasRef.current.getBoundingClientRect();
|
|
|
+
|
|
|
+ // 计算相对于画布的坐标(考虑当前 transform)
|
|
|
+ const x = (targetRect.left + targetRect.width / 2 - canvasRect.left - transform.translateX) / transform.scale;
|
|
|
+ const y = (targetRect.top + targetRect.height / 2 - canvasRect.top - transform.translateY) / transform.scale;
|
|
|
+
|
|
|
+ portPositions[port.id] = { x, y };
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 缓存端口位置和当时的节点坐标
|
|
|
+ portPositionCacheRef.current.set(nodeId, {
|
|
|
+ ports: portPositions,
|
|
|
+ nodeX: node.x,
|
|
|
+ nodeY: node.y
|
|
|
+ });
|
|
|
+
|
|
|
+ return portPositions;
|
|
|
+ };
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取端口坐标(优先从缓存获取,根据节点偏移量计算)
|
|
|
+ * @param {Object} node - 节点对象
|
|
|
+ * @param {Object} port - 端口对象
|
|
|
+ * @returns {Object|null} { x, y }
|
|
|
+ */
|
|
|
+ const getPortPosition = (node, port) => {
|
|
|
+ const cached = portPositionCacheRef.current.get(node.id);
|
|
|
+
|
|
|
+ if (cached && cached.ports[port.id]) {
|
|
|
+ // 计算节点移动的偏差量
|
|
|
+ const deltaX = node.x - cached.nodeX;
|
|
|
+ const deltaY = node.y - cached.nodeY;
|
|
|
+
|
|
|
+ // 返回缓存坐标 + 偏差量
|
|
|
+ return {
|
|
|
+ x: cached.ports[port.id].x + deltaX,
|
|
|
+ y: cached.ports[port.id].y + deltaY
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ // 缓存不存在,从 DOM 获取并缓存
|
|
|
+ const newCache = cacheNodePortPositions(node.id, node);
|
|
|
+ if (newCache && newCache[port.id]) {
|
|
|
+ return newCache[port.id];
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果 DOM 也获取不到,使用计算值作为后备
|
|
|
+ return getPortPositionFallback(node, port);
|
|
|
+ };
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 后备方案:纯计算端口位置(不依赖 DOM)
|
|
|
+ */
|
|
|
+ const getPortPositionFallback = (node, port) => {
|
|
|
+ const isInput = (node.inputs || []).some(p => p.id === port.id);
|
|
|
+ const isVariable = node.type === 'variable';
|
|
|
+
|
|
|
+ // 获取节点尺寸
|
|
|
+ const nodeDims = nodeWidthCacheRef.current.get(node.id) || { width: 180, height: isVariable ? 40 : 140 };
|
|
|
+
|
|
|
+ if (isVariable) {
|
|
|
+ const portY = node.y + nodeDims.height / 2;
|
|
|
+ if (isInput) {
|
|
|
+ return { x: node.x, y: portY };
|
|
|
+ } else {
|
|
|
+ return { x: node.x + nodeDims.width, y: portY };
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ const headerHeight = 40;
|
|
|
+ const executionHeight = 40;
|
|
|
+ const paramsTopPadding = 12;
|
|
|
+ const portGap = 8;
|
|
|
+ const portRowHeight = 20;
|
|
|
+
|
|
|
+ const isExecutionPort = port.type === 'execution';
|
|
|
+
|
|
|
+ if (isExecutionPort) {
|
|
|
+ const portCenterY = node.y + headerHeight + executionHeight / 2;
|
|
|
+ if (isInput) {
|
|
|
+ return { x: node.x + 20, y: portCenterY };
|
|
|
+ } else {
|
|
|
+ return { x: node.x + nodeDims.width - 5, y: portCenterY };
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ const dataPorts = isInput
|
|
|
+ ? (node.inputs || []).filter(p => p.type !== 'execution')
|
|
|
+ : (node.outputs || []).filter(p => p.type !== 'execution');
|
|
|
+ const dataPortIndex = dataPorts.findIndex(p => p.id === port.id);
|
|
|
+
|
|
|
+ if (dataPortIndex === -1) return null;
|
|
|
+
|
|
|
+ const paramsStartY = node.y + headerHeight + executionHeight + paramsTopPadding;
|
|
|
+ const portCenterY = paramsStartY + dataPortIndex * (portRowHeight + portGap) + portRowHeight / 2 + 4;
|
|
|
+
|
|
|
+ if (isInput) {
|
|
|
+ return { x: node.x + 22, y: portCenterY };
|
|
|
+ } else {
|
|
|
+ return { x: node.x + nodeDims.width - 22, y: portCenterY };
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ };
|
|
|
|
|
|
/**
|
|
|
* 处理节点端口鼠标按下(开始连线)
|
|
|
@@ -307,183 +462,9 @@ export function useCanvasLogic(connections, externalControllerRef, nodes = []) {
|
|
|
console.log(` 目标节点:`, connection.target, `端口:`, connection.targetPort, `索引:`, targetPortIndex, `(共${targetNode.inputs?.length || 0}个输入端口)`);
|
|
|
}
|
|
|
|
|
|
- // 获取节点实际宽度和高度(从DOM或缓存)
|
|
|
- const getNodeDimensions = (nodeId) => {
|
|
|
- if (nodeWidthCacheRef.current.has(nodeId)) {
|
|
|
- const cached = nodeWidthCacheRef.current.get(nodeId);
|
|
|
- return { width: cached.width || 180, height: cached.height || 40 };
|
|
|
- }
|
|
|
- // 尝试从DOM获取节点实际尺寸
|
|
|
- if (canvasRef.current) {
|
|
|
- const nodeElement = canvasRef.current.querySelector(`[data-node-id="${nodeId}"]`) ||
|
|
|
- canvasRef.current.querySelector(`.Blueprint-node[id="${nodeId}"]`) ||
|
|
|
- canvasRef.current.querySelector(`.Blueprint-variable-node[data-node-id="${nodeId}"]`);
|
|
|
- if (nodeElement) {
|
|
|
- const width = nodeElement.offsetWidth || 180;
|
|
|
- const height = nodeElement.offsetHeight || (nodeElement.classList.contains('Blueprint-variable-node') ? 40 : 100);
|
|
|
- nodeWidthCacheRef.current.set(nodeId, { width, height });
|
|
|
- return { width, height };
|
|
|
- }
|
|
|
- }
|
|
|
- // 默认值
|
|
|
- const isVariable = nodes.find(n => n.id === nodeId)?.type === 'variable';
|
|
|
- return { width: 180, height: isVariable ? 40 : 100 };
|
|
|
- };
|
|
|
-
|
|
|
- // 计算端口位置(精确对齐到端口中心)- 优先从 DOM 获取实际坐标
|
|
|
- const getPortCenterPosition = (node, port, isInput) => {
|
|
|
- // 尝试从 DOM 获取端口内圆点/箭头的实际位置
|
|
|
- if (canvasRef.current) {
|
|
|
- const nodeElement = canvasRef.current.querySelector(`[data-node-id="${node.id}"]`);
|
|
|
- if (nodeElement) {
|
|
|
- // 查找所有端口元素
|
|
|
- const portElements = nodeElement.querySelectorAll('.Blueprint-node-port');
|
|
|
-
|
|
|
- // 根据端口类型筛选
|
|
|
- const isExecutionPort = port.type === 'execution';
|
|
|
- const matchingPorts = Array.from(portElements).filter(portEl => {
|
|
|
- const isInputPort = portEl.classList.contains('input');
|
|
|
- const isOutputPort = portEl.classList.contains('output');
|
|
|
- const isExecPort = portEl.classList.contains('port-execution');
|
|
|
-
|
|
|
- // 匹配方向和类型
|
|
|
- const directionMatch = (isInput && isInputPort) || (!isInput && isOutputPort);
|
|
|
- const typeMatch = isExecutionPort ? isExecPort : !isExecPort;
|
|
|
-
|
|
|
- return directionMatch && typeMatch;
|
|
|
- });
|
|
|
-
|
|
|
- // 根据端口索引获取对应的端口元素
|
|
|
- const ports = isInput
|
|
|
- ? (node.inputs || []).filter(p => isExecutionPort ? p.type === 'execution' : p.type !== 'execution')
|
|
|
- : (node.outputs || []).filter(p => isExecutionPort ? p.type === 'execution' : p.type !== 'execution');
|
|
|
- const portIndex = ports.findIndex(p => p.id === port.id);
|
|
|
-
|
|
|
- if (portIndex >= 0 && portIndex < matchingPorts.length) {
|
|
|
- const portEl = matchingPorts[portIndex];
|
|
|
- // 查找端口内的圆点或箭头元素
|
|
|
- const dotElement = portEl.querySelector('.Blueprint-node-port-dot');
|
|
|
- const arrowElement = portEl.querySelector('.Blueprint-node-port-arrow');
|
|
|
- const targetElement = dotElement || arrowElement;
|
|
|
-
|
|
|
- if (targetElement) {
|
|
|
- // 获取圆点/箭头相对于画布的位置
|
|
|
- const targetRect = targetElement.getBoundingClientRect();
|
|
|
- const canvasRect = canvasRef.current.getBoundingClientRect();
|
|
|
-
|
|
|
- // 计算相对于画布的中心坐标,并考虑画布的 transform
|
|
|
- const centerX = (targetRect.left + targetRect.width / 2 - canvasRect.left) / transform.scale - transform.translateX / transform.scale;
|
|
|
- const centerY = (targetRect.top + targetRect.height / 2 - canvasRect.top) / transform.scale - transform.translateY / transform.scale;
|
|
|
-
|
|
|
- return { x: centerX, y: centerY };
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // 如果无法从 DOM 获取,使用计算值作为后备
|
|
|
- const isVariable = node.type === 'variable';
|
|
|
- const nodeDims = getNodeDimensions(node.id);
|
|
|
-
|
|
|
- if (isVariable) {
|
|
|
- // 变量节点:端口垂直居中(top: 50%)
|
|
|
- const portY = node.y + nodeDims.height / 2;
|
|
|
- // 端口容器宽度16px(min-width: 16px),圆点在容器内居中(justify-content: center)
|
|
|
- const portContainerWidth = 16;
|
|
|
- const portContainerHalfWidth = portContainerWidth / 2; // 8px
|
|
|
-
|
|
|
- if (isInput) {
|
|
|
- // Set 变量节点的输入端口(左侧)
|
|
|
- return { x: node.x - 8 + portContainerHalfWidth, y: portY };
|
|
|
- } else {
|
|
|
- // Get 变量节点的输出端口(右侧)
|
|
|
- return { x: node.x + nodeDims.width + 8 - portContainerHalfWidth, y: portY };
|
|
|
- }
|
|
|
- } else {
|
|
|
- // 流程节点:三区域结构(header + execution + params)
|
|
|
- const headerHeight = 40; // 区域1:标题区域高度
|
|
|
- const executionHeight = 40; // 区域2:执行箭头区域高度(固定)
|
|
|
- const paramsTopPadding = 12; // 区域3:参数区域顶部padding
|
|
|
- const portGap = 8; // 参数之间的间距(gap: 8px)
|
|
|
- const portRowHeight = 20; // 每个参数行的高度(约20px,包含圆点和标签)
|
|
|
-
|
|
|
- // 分离执行端口和数据端口
|
|
|
- const dataPorts = isInput
|
|
|
- ? (node.inputs || []).filter(p => p.type !== 'execution')
|
|
|
- : (node.outputs || []).filter(p => p.type !== 'execution');
|
|
|
-
|
|
|
- const isExecutionPort = port.type === 'execution';
|
|
|
- let portCenterY;
|
|
|
- let portCenterX;
|
|
|
-
|
|
|
- if (isExecutionPort) {
|
|
|
- // 执行端口:在执行区域内垂直居中
|
|
|
- // 执行区域起始Y = node.y + headerHeight
|
|
|
- // 执行区域高度 = executionHeight
|
|
|
- // 端口垂直居中 = node.y + headerHeight + executionHeight / 2
|
|
|
- portCenterY = node.y + headerHeight + executionHeight / 2;
|
|
|
-
|
|
|
- // 箭头是 CSS border 三角形:border-width: 6px 0 6px 10px
|
|
|
- // 箭头宽度10px,高度12px,箭头中心在宽度的中点(5px)
|
|
|
- const arrowWidth = 10;
|
|
|
- const arrowHalfWidth = arrowWidth / 2; // 5px
|
|
|
-
|
|
|
- if (isInput) {
|
|
|
- // 执行输入端口箭头:
|
|
|
- // 执行区域 padding: 0 8px,端口从 node.x + 8 开始
|
|
|
- // 箭头有 margin-left: 5%(约9px,按180px宽度算)
|
|
|
- // 箭头中心 ≈ node.x + 8 + 9 + 5 = node.x + 22
|
|
|
- // 简化:箭头中心约在 node.x + 8 + arrowHalfWidth + 一些margin
|
|
|
- portCenterX = node.x + 8 + arrowHalfWidth + 8; // 加上margin估算
|
|
|
- } else {
|
|
|
- // 执行输出端口箭头:
|
|
|
- // 执行区域 padding: 0 8px,端口容器在右侧(margin-left: auto)
|
|
|
- // 箭头有 margin-right: -8px,箭头向右伸出到节点边缘外
|
|
|
- // 端口容器右边缘在 node.x + width - 8(padding)
|
|
|
- // 箭头因为 margin-right: -8px 向右伸出8px
|
|
|
- // 箭头宽度10px,箭头尖端在 node.x + width - 8 + 8 + 10 = node.x + width + 10
|
|
|
- // 箭头中心(几何中心)在箭头尖端左边 5px 处
|
|
|
- // 箭头中心 = node.x + width + 10 - 5 = node.x + width + 5
|
|
|
- portCenterX = node.x + nodeDims.width + arrowHalfWidth;
|
|
|
- }
|
|
|
- } else {
|
|
|
- // 数据端口:在参数区域内
|
|
|
- // 参数区域起始Y = node.y + headerHeight + executionHeight + paramsTopPadding
|
|
|
- const dataPortIndex = dataPorts.findIndex(p => p.id === port.id);
|
|
|
- if (dataPortIndex === -1) {
|
|
|
- return null;
|
|
|
- }
|
|
|
-
|
|
|
- // 每个端口的Y位置 = 参数区域起始Y + 索引 * (行高 + 间距) + 行高/2
|
|
|
- const paramsStartY = node.y + headerHeight + executionHeight + paramsTopPadding;
|
|
|
- // 增加额外偏移量让Y坐标更准确对齐圆点中心
|
|
|
- const extraYOffset = 4;
|
|
|
- portCenterY = paramsStartY + dataPortIndex * (portRowHeight + portGap) + portRowHeight / 2 + extraYOffset;
|
|
|
-
|
|
|
- // 圆点直径12px,半径6px
|
|
|
- const dotRadius = 6;
|
|
|
-
|
|
|
- if (isInput) {
|
|
|
- // 数据输入端口:圆点在最左侧
|
|
|
- // CSS: .Blueprint-node-params padding: 12px 16px,端口 position: relative
|
|
|
- // 圆点在端口容器的最左边
|
|
|
- portCenterX = node.x + 16 + dotRadius; // 16px是左padding,dotRadius是圆点中心
|
|
|
- } else {
|
|
|
- // 数据输出端口:
|
|
|
- // - align-self: flex-end 让端口容器靠右
|
|
|
- // - flex-direction: row-reverse 让圆点在视觉上靠右
|
|
|
- // 圆点中心 = 节点右边缘 - 右padding(16px) - 圆点半径
|
|
|
- portCenterX = node.x + nodeDims.width - 16 - dotRadius;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- return { x: portCenterX, y: portCenterY };
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
- // 计算源端口和目标端口的精确中心位置
|
|
|
- const sourcePortPos = getPortCenterPosition(sourceNode, sourcePort, false);
|
|
|
- const targetPortPos = getPortCenterPosition(targetNode, targetPort, true);
|
|
|
+ // 使用缓存方案获取端口坐标
|
|
|
+ const sourcePortPos = getPortPosition(sourceNode, sourcePort);
|
|
|
+ const targetPortPos = getPortPosition(targetNode, targetPort);
|
|
|
|
|
|
|
|
|
if (!sourcePortPos || !targetPortPos) {
|
|
|
@@ -496,13 +477,11 @@ export function useCanvasLogic(connections, externalControllerRef, nodes = []) {
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
- // SVG 偏移量:SVG 的左上角在 (-50000, -50000),所以连线坐标需要加上这个偏移
|
|
|
- const svgOffsetX = 50000;
|
|
|
- const svgOffsetY = 50000;
|
|
|
- const sourceX = sourcePortPos.x + svgOffsetX;
|
|
|
- const sourceY = sourcePortPos.y + svgOffsetY;
|
|
|
- const targetX = targetPortPos.x + svgOffsetX;
|
|
|
- const targetY = targetPortPos.y + svgOffsetY;
|
|
|
+ // SVG 现在与画布坐标系一致,不需要偏移
|
|
|
+ const sourceX = sourcePortPos.x;
|
|
|
+ const sourceY = sourcePortPos.y;
|
|
|
+ const targetX = targetPortPos.x;
|
|
|
+ const targetY = targetPortPos.y;
|
|
|
|
|
|
// 调试:检查坐标是否有效
|
|
|
if (isNaN(sourceX) || isNaN(sourceY) || isNaN(targetX) || isNaN(targetY)) {
|
|
|
@@ -551,6 +530,26 @@ export function useCanvasLogic(connections, externalControllerRef, nodes = []) {
|
|
|
};
|
|
|
};
|
|
|
|
|
|
+ /**
|
|
|
+ * 刷新所有节点的端口坐标缓存
|
|
|
+ */
|
|
|
+ const refreshPortPositionCache = () => {
|
|
|
+ portPositionCacheRef.current.clear();
|
|
|
+ nodes.forEach(node => {
|
|
|
+ cacheNodePortPositions(node.id, node);
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 刷新单个节点的端口坐标缓存
|
|
|
+ */
|
|
|
+ const refreshNodePortCache = (nodeId) => {
|
|
|
+ const node = nodes.find(n => n.id === nodeId);
|
|
|
+ if (node) {
|
|
|
+ cacheNodePortPositions(nodeId, node);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
return {
|
|
|
canvasRef,
|
|
|
transform,
|
|
|
@@ -563,7 +562,9 @@ export function useCanvasLogic(connections, externalControllerRef, nodes = []) {
|
|
|
handleMouseUp,
|
|
|
handleContextMenu,
|
|
|
handleConnectionClick,
|
|
|
- getConnectionPath
|
|
|
+ getConnectionPath,
|
|
|
+ refreshPortPositionCache,
|
|
|
+ refreshNodePortCache
|
|
|
};
|
|
|
}
|
|
|
|