|
|
@@ -0,0 +1,573 @@
|
|
|
+/**
|
|
|
+ * 画布组件逻辑
|
|
|
+ */
|
|
|
+
|
|
|
+import { useEffect, useRef, useState } from 'react';
|
|
|
+import { initCanvasController } from '../utils/canvas-controller.js';
|
|
|
+import { ConnectionManager } from '../utils/connection-manager.js';
|
|
|
+
|
|
|
+/**
|
|
|
+ * 画布逻辑钩子
|
|
|
+ */
|
|
|
+export function useCanvasLogic(connections, externalControllerRef, nodes = []) {
|
|
|
+ const canvasRef = useRef(null);
|
|
|
+ const canvasControllerRef = useRef(null);
|
|
|
+ const connectionManagerRef = useRef(null);
|
|
|
+ const nodeWidthCacheRef = useRef(new Map()); // 缓存节点实际尺寸 {width, height}
|
|
|
+ const [transform, setTransform] = useState({ scale: 1, translateX: 0, translateY: 0 });
|
|
|
+ const [connectingStart, setConnectingStart] = useState(null);
|
|
|
+ const [connectingEnd, setConnectingEnd] = useState(null);
|
|
|
+ const [draggedNodeId, setDraggedNodeId] = useState(null);
|
|
|
+ const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 });
|
|
|
+
|
|
|
+ // 初始化画布控制器
|
|
|
+ useEffect(() => {
|
|
|
+ if (canvasRef.current) {
|
|
|
+ // 使用 canvasRef.current 的父元素(.Blueprint-canvas-area)作为事件绑定目标
|
|
|
+ // 这样可以在整个红色边框区域内开始拖拽
|
|
|
+ const canvasArea = canvasRef.current.parentElement;
|
|
|
+ const controller = initCanvasController(canvasArea || canvasRef.current, (newTransform) => {
|
|
|
+ setTransform(newTransform);
|
|
|
+ });
|
|
|
+ canvasControllerRef.current = controller;
|
|
|
+
|
|
|
+ if (externalControllerRef) {
|
|
|
+ externalControllerRef.current = controller;
|
|
|
+ }
|
|
|
+
|
|
|
+ return () => {
|
|
|
+ controller?.destroy();
|
|
|
+ };
|
|
|
+ }
|
|
|
+ }, [externalControllerRef]);
|
|
|
+
|
|
|
+ // 初始化连线管理器
|
|
|
+ useEffect(() => {
|
|
|
+ connectionManagerRef.current = new ConnectionManager(connections);
|
|
|
+ }, [connections]);
|
|
|
+
|
|
|
+ // 当节点更新时,清除宽度缓存(节点内容可能改变导致宽度变化)
|
|
|
+ useEffect(() => {
|
|
|
+ nodeWidthCacheRef.current.clear();
|
|
|
+ }, [nodes]);
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理节点端口鼠标按下(开始连线)
|
|
|
+ */
|
|
|
+ const handlePortMouseDown = (e, nodeId, portId, onConnectionStart, nodes) => {
|
|
|
+ // 只处理左键按下
|
|
|
+ if (e.button !== 0) return;
|
|
|
+
|
|
|
+ e.stopPropagation();
|
|
|
+ const rect = canvasRef.current.getBoundingClientRect();
|
|
|
+ const portElement = e.target.closest('.Blueprint-node-port');
|
|
|
+ if (!portElement) return;
|
|
|
+
|
|
|
+ // 精确计算端口中心点(考虑画布变换)
|
|
|
+ const portRect = portElement.getBoundingClientRect();
|
|
|
+ // 端口中心在屏幕坐标
|
|
|
+ const screenPortX = portRect.left + portRect.width / 2;
|
|
|
+ const screenPortY = portRect.top + portRect.height / 2;
|
|
|
+ // 转换为画布坐标(考虑缩放和平移)
|
|
|
+ const portX = (screenPortX - rect.left - transform.translateX) / transform.scale;
|
|
|
+ const portY = (screenPortY - rect.top - transform.translateY) / transform.scale;
|
|
|
+
|
|
|
+ // 从节点数据中获取端口类型信息
|
|
|
+ const node = nodes?.find(n => n.id === nodeId);
|
|
|
+ let portType = 'execution';
|
|
|
+ let paramType = 'string';
|
|
|
+
|
|
|
+ if (node) {
|
|
|
+ const port = [...(node.outputs || []), ...(node.inputs || [])].find(p => p.id === portId);
|
|
|
+ if (port) {
|
|
|
+ portType = port.type || 'execution';
|
|
|
+ paramType = port.paramType || 'string';
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ setConnectingStart({ nodeId, portId, x: portX, y: portY, portType, paramType });
|
|
|
+ setConnectingEnd({ x: portX, y: portY });
|
|
|
+ onConnectionStart?.(nodeId, portId);
|
|
|
+ };
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理节点端口鼠标释放(结束连线或自动吸附)
|
|
|
+ */
|
|
|
+ const handlePortMouseUp = (e, nodeId, portId, onConnectionEnd) => {
|
|
|
+ e.stopPropagation();
|
|
|
+
|
|
|
+ if (connectingStart) {
|
|
|
+ // 如果连接到不同的端口,创建连线
|
|
|
+ if (connectingStart.nodeId !== nodeId || connectingStart.portId !== portId) {
|
|
|
+ const rect = canvasRef.current.getBoundingClientRect();
|
|
|
+ const portElement = e.target.closest('.Blueprint-node-port');
|
|
|
+ if (portElement) {
|
|
|
+ // 验证连接是否有效(变量节点只能连接到数据端口)
|
|
|
+ const sourceNode = nodes?.find(n => n.id === connectingStart.nodeId);
|
|
|
+ const targetNode = nodes?.find(n => n.id === nodeId);
|
|
|
+
|
|
|
+ if (sourceNode && targetNode) {
|
|
|
+ const sourcePort = [...(sourceNode.outputs || []), ...(sourceNode.inputs || [])].find(p => p.id === connectingStart.portId);
|
|
|
+ const targetPort = [...(targetNode.outputs || []), ...(targetNode.inputs || [])].find(p => p.id === portId);
|
|
|
+
|
|
|
+ // 检查变量节点连接限制
|
|
|
+ let canConnect = true;
|
|
|
+ if (sourceNode.type === 'variable' && sourcePort && sourcePort.type !== 'data') {
|
|
|
+ canConnect = false;
|
|
|
+ }
|
|
|
+ if (targetNode.type === 'variable' && targetPort && targetPort.type !== 'data') {
|
|
|
+ canConnect = false;
|
|
|
+ }
|
|
|
+ if (sourceNode.type === 'variable' && targetPort && targetPort.type === 'execution') {
|
|
|
+ canConnect = false;
|
|
|
+ }
|
|
|
+ if (sourcePort && sourcePort.type === 'execution' && targetNode.type === 'variable') {
|
|
|
+ canConnect = false;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (canConnect) {
|
|
|
+ // 成功连接到端口,创建连线
|
|
|
+ onConnectionEnd?.(connectingStart.nodeId, connectingStart.portId, nodeId, portId);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 如果找不到节点,仍然尝试连接(让 createConnection 处理验证)
|
|
|
+ onConnectionEnd?.(connectingStart.nodeId, connectingStart.portId, nodeId, portId);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 无论是否成功连接,都清除连线状态
|
|
|
+ setConnectingStart(null);
|
|
|
+ setConnectingEnd(null);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理节点鼠标按下(开始拖拽)
|
|
|
+ */
|
|
|
+ const handleNodeMouseDown = (e, nodeId, nodes, selectedNodeIds, onNodeSelect) => {
|
|
|
+ if (e.button !== 0) return;
|
|
|
+
|
|
|
+ const node = nodes.find(n => n.id === nodeId);
|
|
|
+ if (!node) return;
|
|
|
+
|
|
|
+ const rect = canvasRef.current.getBoundingClientRect();
|
|
|
+ const canvasX = (e.clientX - rect.left - transform.translateX) / transform.scale;
|
|
|
+ const canvasY = (e.clientY - rect.top - transform.translateY) / transform.scale;
|
|
|
+
|
|
|
+ setDraggedNodeId(nodeId);
|
|
|
+ setDragOffset({
|
|
|
+ x: canvasX - node.x,
|
|
|
+ y: canvasY - node.y
|
|
|
+ });
|
|
|
+
|
|
|
+ // 将 cursor 应用到 document.body,确保在边界外也保持 grabbing
|
|
|
+ document.body.style.cursor = 'grabbing';
|
|
|
+ document.body.style.userSelect = 'none';
|
|
|
+
|
|
|
+ if (e.ctrlKey || e.metaKey) {
|
|
|
+ onNodeSelect?.(nodeId, true);
|
|
|
+ } else {
|
|
|
+ if (!selectedNodeIds.includes(nodeId)) {
|
|
|
+ onNodeSelect?.(nodeId, false);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ e.preventDefault();
|
|
|
+ e.stopPropagation();
|
|
|
+ };
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理鼠标移动(连线预览、节点拖拽)
|
|
|
+ */
|
|
|
+ const handleMouseMove = (e, onNodeMove) => {
|
|
|
+ // 如果正在连线,更新连线预览的终点位置
|
|
|
+ if (connectingStart) {
|
|
|
+ const rect = canvasRef.current.getBoundingClientRect();
|
|
|
+ // 转换为画布坐标(考虑缩放和平移)
|
|
|
+ const x = (e.clientX - rect.left - transform.translateX) / transform.scale;
|
|
|
+ const y = (e.clientY - rect.top - transform.translateY) / transform.scale;
|
|
|
+ setConnectingEnd({ x, y });
|
|
|
+ }
|
|
|
+
|
|
|
+ if (draggedNodeId) {
|
|
|
+ const rect = canvasRef.current.getBoundingClientRect();
|
|
|
+ const canvasX = (e.clientX - rect.left - transform.translateX) / transform.scale - dragOffset.x;
|
|
|
+ const canvasY = (e.clientY - rect.top - transform.translateY) / transform.scale - dragOffset.y;
|
|
|
+
|
|
|
+ onNodeMove?.(draggedNodeId, canvasX, canvasY);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理鼠标释放
|
|
|
+ */
|
|
|
+ const handleMouseUp = (e) => {
|
|
|
+ if (draggedNodeId) {
|
|
|
+ // 恢复 document.body 的 cursor 样式
|
|
|
+ document.body.style.cursor = '';
|
|
|
+ document.body.style.userSelect = '';
|
|
|
+ setDraggedNodeId(null);
|
|
|
+ setDragOffset({ x: 0, y: 0 });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果正在连线但没有连接到端口,取消连线
|
|
|
+ if (connectingStart) {
|
|
|
+ // 检查是否点击在端口上
|
|
|
+ const portElement = e.target?.closest('.Blueprint-node-port');
|
|
|
+ if (!portElement) {
|
|
|
+ // 没有连接到端口,取消连线
|
|
|
+ setConnectingStart(null);
|
|
|
+ setConnectingEnd(null);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理右键菜单
|
|
|
+ */
|
|
|
+ const handleContextMenu = (e, onCanvasContextMenu) => {
|
|
|
+ e.preventDefault();
|
|
|
+ const rect = canvasRef.current.getBoundingClientRect();
|
|
|
+ const x = e.clientX - rect.left;
|
|
|
+ const y = e.clientY - rect.top;
|
|
|
+ onCanvasContextMenu?.(e.clientX, e.clientY, x, y);
|
|
|
+ };
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理连线点击(删除)
|
|
|
+ */
|
|
|
+ const handleConnectionClick = (e, connectionId, onConnectionDelete) => {
|
|
|
+ if (e.ctrlKey || e.metaKey) {
|
|
|
+ onConnectionDelete?.(connectionId);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 计算连线路径和类型信息
|
|
|
+ */
|
|
|
+ const getConnectionPath = (connection, nodes, createBezierPath, connections = []) => {
|
|
|
+ const sourceNode = nodes.find(n => n.id === connection.source);
|
|
|
+ const targetNode = nodes.find(n => n.id === connection.target);
|
|
|
+ if (!sourceNode || !targetNode) {
|
|
|
+ console.warn('连线找不到节点:', connection.id, 'source:', connection.source, 'target:', connection.target);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 在所有端口中查找端口对象(用于获取类型信息)
|
|
|
+ const allSourcePorts = [...(sourceNode.outputs || []), ...(sourceNode.inputs || [])];
|
|
|
+ const allTargetPorts = [...(targetNode.inputs || []), ...(targetNode.outputs || [])];
|
|
|
+
|
|
|
+ const sourcePort = allSourcePorts.find(p => p.id === connection.sourcePort);
|
|
|
+ const targetPort = allTargetPorts.find(p => p.id === connection.targetPort);
|
|
|
+
|
|
|
+ if (!sourcePort || !targetPort) {
|
|
|
+ console.warn('连线找不到端口:', connection.id, {
|
|
|
+ sourceNodeId: connection.source,
|
|
|
+ sourcePortId: connection.sourcePort,
|
|
|
+ sourceNodeOutputs: sourceNode.outputs?.map(p => p.id),
|
|
|
+ sourceNodeInputs: sourceNode.inputs?.map(p => p.id),
|
|
|
+ targetNodeId: connection.target,
|
|
|
+ targetPortId: connection.targetPort,
|
|
|
+ targetNodeInputs: targetNode.inputs?.map(p => p.id),
|
|
|
+ targetNodeOutputs: targetNode.outputs?.map(p => p.id)
|
|
|
+ });
|
|
|
+ return null; // 找不到端口,不显示连线
|
|
|
+ }
|
|
|
+
|
|
|
+ // 计算端口位置(考虑端口索引)
|
|
|
+ // 源端口应该在 outputs 中查找索引
|
|
|
+ const sourcePortIndex = sourceNode.outputs?.findIndex(p => p.id === connection.sourcePort);
|
|
|
+ // 目标端口应该在 inputs 中查找索引
|
|
|
+ const targetPortIndex = targetNode.inputs?.findIndex(p => p.id === connection.targetPort);
|
|
|
+
|
|
|
+ // 如果端口不在对应的数组中,返回 null
|
|
|
+ if (sourcePortIndex === undefined || sourcePortIndex === -1 ||
|
|
|
+ targetPortIndex === undefined || targetPortIndex === -1) {
|
|
|
+ console.warn('连线端口索引无效:', connection.id, {
|
|
|
+ sourceNodeId: connection.source,
|
|
|
+ sourceNodeType: sourceNode.type,
|
|
|
+ sourcePortId: connection.sourcePort,
|
|
|
+ sourcePortIndex,
|
|
|
+ sourceNodeOutputs: sourceNode.outputs?.map(p => ({ id: p.id, label: p.label, type: p.type })),
|
|
|
+ targetNodeId: connection.target,
|
|
|
+ targetNodeType: targetNode.type,
|
|
|
+ targetPortId: connection.targetPort,
|
|
|
+ targetPortIndex,
|
|
|
+ targetNodeInputs: targetNode.inputs?.map(p => ({ id: p.id, label: p.label, type: p.type }))
|
|
|
+ });
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 调试:记录端口索引信息(前3个连线)
|
|
|
+ const connectionIndex = connections?.indexOf?.(connection) ?? -1;
|
|
|
+ if (connectionIndex >= 0 && connectionIndex < 3) {
|
|
|
+ console.log(`端口索引计算[${connectionIndex}]:`, connection.id);
|
|
|
+ console.log(` 源节点:`, connection.source, `端口:`, connection.sourcePort, `索引:`, sourcePortIndex, `(共${sourceNode.outputs?.length || 0}个输出端口)`);
|
|
|
+ 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 };
|
|
|
+ };
|
|
|
+
|
|
|
+ // 计算端口位置(精确对齐到端口中心)
|
|
|
+ const getPortCenterPosition = (node, port, isInput) => {
|
|
|
+ const isVariable = node.type === 'variable';
|
|
|
+ const nodeDims = getNodeDimensions(node.id);
|
|
|
+
|
|
|
+ if (isVariable) {
|
|
|
+ // 变量节点:端口垂直居中(top: 50%)
|
|
|
+ const portY = node.y + nodeDims.height / 2;
|
|
|
+ if (isInput) {
|
|
|
+ // 变量节点的输入端口:在节点内部(left: -8px,端口容器宽度16px,中心在节点左边缘)
|
|
|
+ // 变量节点的输入端口使用 left: -8px,端口中心在节点左边缘
|
|
|
+ return { x: node.x, y: portY };
|
|
|
+ } else {
|
|
|
+ // 变量节点的输出端口:在节点内部(right: -8px,端口容器宽度16px,中心在节点右边缘)
|
|
|
+ // 变量节点的输出端口使用 right: -8px,端口中心在节点右边缘
|
|
|
+ return { x: node.x + nodeDims.width, y: portY };
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 流程节点:使用标准端口位置计算
|
|
|
+ const headerHeight = 40;
|
|
|
+ const bodyPadding = 24;
|
|
|
+ const portHeight = 56;
|
|
|
+
|
|
|
+ // 分离执行端口和数据端口
|
|
|
+ const executionPorts = isInput
|
|
|
+ ? (node.inputs || []).filter(p => p.type === 'execution')
|
|
|
+ : (node.outputs || []).filter(p => p.type === 'execution');
|
|
|
+ const dataPorts = isInput
|
|
|
+ ? (node.inputs || []).filter(p => p.type !== 'execution')
|
|
|
+ : (node.outputs || []).filter(p => p.type !== 'execution');
|
|
|
+
|
|
|
+ const portIndex = isInput
|
|
|
+ ? node.inputs?.findIndex(p => p.id === port.id) ?? -1
|
|
|
+ : node.outputs?.findIndex(p => p.id === port.id) ?? -1;
|
|
|
+
|
|
|
+ if (portIndex === -1) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ const isExecutionPort = port.type === 'execution';
|
|
|
+ let portCenterY;
|
|
|
+
|
|
|
+ if (isExecutionPort) {
|
|
|
+ // 执行端口:固定在最上方,紧贴header下方
|
|
|
+ portCenterY = node.y + headerHeight + 12;
|
|
|
+ } else {
|
|
|
+ // 数据端口:在执行端口下方
|
|
|
+ const executionPortOffset = executionPorts.length > 0 ? 32 : 0;
|
|
|
+ const dataPortIndex = dataPorts.findIndex(p => p.id === port.id);
|
|
|
+ portCenterY = node.y + headerHeight + bodyPadding + executionPortOffset + dataPortIndex * portHeight + 8; // 8px是端口高度16px的一半
|
|
|
+ }
|
|
|
+
|
|
|
+ if (isInput) {
|
|
|
+ if (isExecutionPort) {
|
|
|
+ // 执行输入端口:在节点左边缘(left: -8px,端口中心在节点左边缘)
|
|
|
+ return { x: node.x, y: portCenterY };
|
|
|
+ } else {
|
|
|
+ // 数据输入端口:在节点内部(left: 8px,端口容器宽度16px,中心在 left + 8px = node.x + 16px)
|
|
|
+ return { x: node.x + 8 + 8, y: portCenterY }; // 8px是left位置,8px是容器宽度的一半
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ if (isExecutionPort) {
|
|
|
+ // 执行输出端口:在节点右边缘(right: -8px,端口中心在节点右边缘)
|
|
|
+ return { x: node.x + nodeDims.width, y: portCenterY };
|
|
|
+ } else {
|
|
|
+ // 数据输出端口:在节点内部(right: 8px,端口容器宽度16px,中心在 right - 8px = node.x + width - 16px)
|
|
|
+ return { x: node.x + nodeDims.width - 8 - 8, y: portCenterY }; // 8px是right位置,8px是容器宽度的一半
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // 计算源端口和目标端口的精确中心位置
|
|
|
+ const sourcePortPos = getPortCenterPosition(sourceNode, sourcePort, false);
|
|
|
+ const targetPortPos = getPortCenterPosition(targetNode, targetPort, true);
|
|
|
+
|
|
|
+
|
|
|
+ if (!sourcePortPos || !targetPortPos) {
|
|
|
+ console.warn('无法计算端口位置:', connection.id, {
|
|
|
+ sourceNodeId: connection.source,
|
|
|
+ sourcePortId: connection.sourcePort,
|
|
|
+ targetNodeId: connection.target,
|
|
|
+ targetPortId: connection.targetPort
|
|
|
+ });
|
|
|
+ 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;
|
|
|
+
|
|
|
+ // 调试:检查坐标是否有效
|
|
|
+ if (isNaN(sourceX) || isNaN(sourceY) || isNaN(targetX) || isNaN(targetY)) {
|
|
|
+ console.warn('连线坐标无效:', connection.id, {
|
|
|
+ sourceX, sourceY, targetX, targetY,
|
|
|
+ sourceNode: { id: sourceNode.id, x: sourceNode.x, y: sourceNode.y, type: sourceNode.type },
|
|
|
+ targetNode: { id: targetNode.id, x: targetNode.x, y: targetNode.y, type: targetNode.type },
|
|
|
+ sourcePortPos, targetPortPos
|
|
|
+ });
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 确定连线类型(优先使用源端口类型,如果没有则使用目标端口类型)
|
|
|
+ const portType = sourcePort.type || targetPort.type || 'execution';
|
|
|
+ const paramType = sourcePort.paramType || targetPort.paramType || 'string';
|
|
|
+
|
|
|
+ const path = createBezierPath(sourceX, sourceY, targetX, targetY);
|
|
|
+
|
|
|
+ // 调试:检查路径是否有效
|
|
|
+ if (!path || path === '') {
|
|
|
+ console.warn('连线路径无效:', connection.id, {
|
|
|
+ sourceX, sourceY, targetX, targetY,
|
|
|
+ path
|
|
|
+ });
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 调试:记录前几个连线的详细信息(用于排查问题)
|
|
|
+ // 注意:这里无法直接访问 connections 数组,所以暂时注释掉索引检查
|
|
|
+ // if (connectionIndex >= 0 && connectionIndex < 5) {
|
|
|
+ // console.log('连线路径计算:', {
|
|
|
+ // connectionId: connection.id,
|
|
|
+ // sourceNode: { id: sourceNode.id, type: sourceNode.type, x: sourceNode.x, y: sourceNode.y },
|
|
|
+ // targetNode: { id: targetNode.id, type: targetNode.type, x: targetNode.x, y: targetNode.y },
|
|
|
+ // sourcePort: { id: connection.sourcePort, index: sourcePortIndex, y: sourcePortY },
|
|
|
+ // targetPort: { id: connection.targetPort, index: targetPortIndex, y: targetPortY },
|
|
|
+ // sourceX, sourceY, targetX, targetY,
|
|
|
+ // path
|
|
|
+ // });
|
|
|
+ // }
|
|
|
+
|
|
|
+ return {
|
|
|
+ path,
|
|
|
+ portType,
|
|
|
+ paramType
|
|
|
+ };
|
|
|
+ };
|
|
|
+
|
|
|
+ return {
|
|
|
+ canvasRef,
|
|
|
+ transform,
|
|
|
+ connectingStart,
|
|
|
+ connectingEnd,
|
|
|
+ handlePortMouseDown,
|
|
|
+ handlePortMouseUp,
|
|
|
+ handleNodeMouseDown,
|
|
|
+ handleMouseMove,
|
|
|
+ handleMouseUp,
|
|
|
+ handleContextMenu,
|
|
|
+ handleConnectionClick,
|
|
|
+ getConnectionPath
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 绑定画布事件监听器
|
|
|
+ */
|
|
|
+export function useCanvasEventHandlers(canvasRef, transform, connectingStart, connectingEnd, handleMouseMoveLogic, handleMouseUpLogic, handleContextMenuLogic, onNodeMove, onCanvasContextMenu) {
|
|
|
+ useEffect(() => {
|
|
|
+ const canvas = canvasRef.current?.parentElement;
|
|
|
+ if (!canvas) return;
|
|
|
+
|
|
|
+ const mouseMoveHandler = (e) => handleMouseMoveLogic(e, onNodeMove);
|
|
|
+ const mouseUpHandler = (e) => handleMouseUpLogic(e);
|
|
|
+ const contextMenuHandler = (e) => handleContextMenuLogic(e, onCanvasContextMenu);
|
|
|
+
|
|
|
+ // 将 mousemove 和 mouseup 绑定到 window,允许在边界外继续拖拽
|
|
|
+ window.addEventListener('mousemove', mouseMoveHandler);
|
|
|
+ window.addEventListener('mouseup', mouseUpHandler);
|
|
|
+ canvas.addEventListener('contextmenu', contextMenuHandler);
|
|
|
+
|
|
|
+ return () => {
|
|
|
+ window.removeEventListener('mousemove', mouseMoveHandler);
|
|
|
+ window.removeEventListener('mouseup', mouseUpHandler);
|
|
|
+ canvas.removeEventListener('contextmenu', contextMenuHandler);
|
|
|
+ };
|
|
|
+ }, [canvasRef, transform, connectingStart, connectingEnd, handleMouseMoveLogic, handleMouseUpLogic, handleContextMenuLogic, onNodeMove, onCanvasContextMenu]);
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 创建事件处理函数包装器
|
|
|
+ */
|
|
|
+export function createEventHandlers(logicHandlers, props, nodesForPortType) {
|
|
|
+ const { handlePortMouseDownLogic, handlePortMouseUpLogic, handleNodeMouseDownLogic, handleConnectionClickLogic } = logicHandlers;
|
|
|
+ const { onConnectionStart, onConnectionEnd, onNodeSelect, onConnectionDelete, nodes, selectedNodeIds } = props;
|
|
|
+
|
|
|
+ return {
|
|
|
+ handlePortMouseDown: (e, nodeId, portId) => {
|
|
|
+ handlePortMouseDownLogic(e, nodeId, portId, onConnectionStart, nodesForPortType || nodes);
|
|
|
+ },
|
|
|
+ handlePortMouseUp: (e, nodeId, portId) => {
|
|
|
+ handlePortMouseUpLogic(e, nodeId, portId, onConnectionEnd);
|
|
|
+ },
|
|
|
+ handleNodeMouseDown: (e, nodeId) => {
|
|
|
+ handleNodeMouseDownLogic(e, nodeId, nodes, selectedNodeIds, onNodeSelect);
|
|
|
+ },
|
|
|
+ handleConnectionClick: (e, connectionId) => {
|
|
|
+ handleConnectionClickLogic(e, connectionId, onConnectionDelete);
|
|
|
+ }
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 根据端口类型获取连线样式类
|
|
|
+ * @param {string} portType - 端口类型 ('execution' 或 'data')
|
|
|
+ * @param {string} paramType - 参数类型 ('int' 或 'string')
|
|
|
+ * @returns {string} 连线样式类名
|
|
|
+ */
|
|
|
+export function getConnectionClass(portType, paramType) {
|
|
|
+ if (portType === 'execution') {
|
|
|
+ return 'connection-execution';
|
|
|
+ } else if (portType === 'data') {
|
|
|
+ if (paramType === 'int' || paramType === 'integer') {
|
|
|
+ return 'connection-int';
|
|
|
+ } else {
|
|
|
+ return 'connection-string';
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return 'connection-execution';
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 计算网格样式
|
|
|
+ * @param {Object} transform - 变换对象 { scale, translateX, translateY }
|
|
|
+ * @param {number} gridSize - 基础网格大小
|
|
|
+ * @returns {Object} 网格样式对象 { backgroundPosition, backgroundSize }
|
|
|
+ */
|
|
|
+export function calculateGridStyle(transform, gridSize = 20) {
|
|
|
+ const scaledGridSize = gridSize * transform.scale;
|
|
|
+ const gridOffsetX = (transform.translateX % scaledGridSize + scaledGridSize) % scaledGridSize;
|
|
|
+ const gridOffsetY = (transform.translateY % scaledGridSize + scaledGridSize) % scaledGridSize;
|
|
|
+
|
|
|
+ return {
|
|
|
+ backgroundPosition: `${gridOffsetX}px ${gridOffsetY}px`,
|
|
|
+ backgroundSize: `${scaledGridSize}px ${scaledGridSize}px`
|
|
|
+ };
|
|
|
+}
|