Skip to main content
Glama

McFlow

positioning.ts10.6 kB
/** * Node positioning utilities for McFlow * Provides better spacing for workflow node layouts with node size awareness */ export interface Position { x: number; y: number; } export interface NodeDimensions { width: number; height: number; } export class NodePositioning { // Default spacing between nodes (accounting for typical node sizes) private static readonly HORIZONTAL_SPACING = 350; // Increased for LangChain and other wide nodes private static readonly VERTICAL_SPACING = 250; // Increased to account for taller nodes private static readonly INITIAL_X = 250; private static readonly INITIAL_Y = 300; // Typical node dimensions based on n8n UI patterns // Each grid square in n8n is approximately 20px private static readonly NODE_DIMENSIONS: Record<string, NodeDimensions> = { // Trigger nodes (typically smaller) 'n8n-nodes-base.webhook': { width: 200, height: 100 }, 'n8n-nodes-base.manualTrigger': { width: 200, height: 100 }, 'n8n-nodes-base.scheduleTrigger': { width: 200, height: 100 }, 'n8n-nodes-base.emailTriggerImap': { width: 200, height: 120 }, // Core nodes (standard size) 'n8n-nodes-base.httpRequest': { width: 240, height: 140 }, 'n8n-nodes-base.code': { width: 240, height: 140 }, 'n8n-nodes-base.set': { width: 240, height: 140 }, 'n8n-nodes-base.function': { width: 240, height: 140 }, 'n8n-nodes-base.functionItem': { width: 240, height: 140 }, // AI/LLM nodes (typically larger due to more parameters) 'n8n-nodes-base.openAi': { width: 260, height: 160 }, '@n8n/n8n-nodes-langchain.chainLlm': { width: 320, height: 200 }, '@n8n/n8n-nodes-langchain.agent': { width: 320, height: 200 }, '@n8n/n8n-nodes-langchain.vectorStoreInMemory': { width: 280, height: 180 }, // Data processing nodes 'n8n-nodes-base.mergeV3': { width: 240, height: 140 }, 'n8n-nodes-base.splitInBatches': { width: 240, height: 140 }, 'n8n-nodes-base.itemLists': { width: 240, height: 140 }, 'n8n-nodes-base.aggregate': { width: 240, height: 140 }, // Database nodes (larger due to query fields) 'n8n-nodes-base.postgres': { width: 260, height: 160 }, 'n8n-nodes-base.mysql': { width: 260, height: 160 }, 'n8n-nodes-base.mongodb': { width: 260, height: 160 }, // Response nodes (smaller) 'n8n-nodes-base.respondToWebhook': { width: 200, height: 100 }, 'n8n-nodes-base.noOp': { width: 180, height: 80 }, // Default for unknown nodes 'default': { width: 240, height: 140 } }; /** * Get dimensions for a specific node type */ static getNodeDimensions(nodeType: string): NodeDimensions { return this.NODE_DIMENSIONS[nodeType] || this.NODE_DIMENSIONS.default; } /** * Calculate spacing based on node dimensions */ static calculateSpacing(nodeType1: string, nodeType2: string, isHorizontal: boolean): number { const dim1 = this.getNodeDimensions(nodeType1); const dim2 = this.getNodeDimensions(nodeType2); if (isHorizontal) { // Horizontal spacing: half of each node's width plus buffer // Use larger buffer for wider nodes (LangChain nodes need more space) const maxWidth = Math.max(dim1.width, dim2.width); const buffer = maxWidth >= 280 ? 120 : 80; // Larger buffer for wide nodes return (dim1.width + dim2.width) / 2 + buffer; } else { // Vertical spacing: half of each node's height plus buffer return (dim1.height + dim2.height) / 2 + 80; // 80px buffer } } /** * Calculate position for a node in a horizontal layout */ static getHorizontalPosition(index: number, startY: number = this.INITIAL_Y): [number, number] { return [ this.INITIAL_X + (index * this.HORIZONTAL_SPACING), startY ]; } /** * Calculate position for a node in a vertical layout */ static getVerticalPosition(index: number, startX: number = this.INITIAL_X): [number, number] { return [ startX, this.INITIAL_Y + (index * this.VERTICAL_SPACING) ]; } /** * Calculate position for a node in a grid layout */ static getGridPosition( index: number, columns: number = 3, horizontalSpacing: number = this.HORIZONTAL_SPACING, verticalSpacing: number = this.VERTICAL_SPACING ): [number, number] { const row = Math.floor(index / columns); const col = index % columns; return [ this.INITIAL_X + (col * horizontalSpacing), this.INITIAL_Y + (row * verticalSpacing) ]; } /** * Calculate position for a node in a diagonal/staircase layout */ static getDiagonalPosition( index: number, horizontalStep: number = 200, verticalStep: number = 150 ): [number, number] { return [ this.INITIAL_X + (index * horizontalStep), this.INITIAL_Y + (index * verticalStep) ]; } /** * Calculate position for a node in a tree/branching layout */ static getTreePosition( level: number, indexInLevel: number, nodesPerLevel: number, horizontalSpacing: number = this.HORIZONTAL_SPACING, verticalSpacing: number = this.VERTICAL_SPACING ): [number, number] { const totalWidth = (nodesPerLevel - 1) * horizontalSpacing; const startX = this.INITIAL_X - (totalWidth / 2); return [ startX + (indexInLevel * horizontalSpacing), this.INITIAL_Y + (level * verticalSpacing) ]; } /** * Auto-layout nodes based on their connections and node dimensions */ static autoLayout(nodes: any[], connections: any): Map<string, [number, number]> { const positions = new Map<string, [number, number]>(); const visited = new Set<string>(); const levels = new Map<string, number>(); // Find trigger/start nodes (nodes with no incoming connections) const startNodes = nodes.filter(node => { const nodeId = node.id || node.name; return !Object.values(connections).some((conns: any) => { return Object.values(conns).some((targets: any) => { return targets.some((target: any) => { return Array.isArray(target) ? target.some((t: any) => t.node === nodeId) : target.node === nodeId; }); }); }); }); // BFS to assign levels const queue: Array<{node: any, level: number}> = []; startNodes.forEach(node => { queue.push({node, level: 0}); levels.set(node.id || node.name, 0); }); while (queue.length > 0) { const {node, level} = queue.shift()!; const nodeId = node.id || node.name; if (visited.has(nodeId)) continue; visited.add(nodeId); // Process connections from this node const nodeConnections = connections[nodeId]; if (nodeConnections && nodeConnections.main) { nodeConnections.main.forEach((targets: any[]) => { targets.forEach((target: any) => { const targetNode = nodes.find(n => (n.id || n.name) === target.node); if (targetNode && !visited.has(target.node)) { queue.push({node: targetNode, level: level + 1}); levels.set(target.node, level + 1); } }); }); } } // Group nodes by level const nodesByLevel = new Map<number, any[]>(); nodes.forEach(node => { const nodeId = node.id || node.name; const level = levels.get(nodeId) || 0; if (!nodesByLevel.has(level)) { nodesByLevel.set(level, []); } nodesByLevel.get(level)!.push(node); }); // Assign positions with dynamic spacing based on node sizes let currentY = this.INITIAL_Y; nodesByLevel.forEach((nodesInLevel, level) => { let maxHeightInLevel = 0; let currentX = this.INITIAL_X; // Calculate total width needed for this level const totalWidth = nodesInLevel.reduce((sum, node) => { const dim = this.getNodeDimensions(node.type); maxHeightInLevel = Math.max(maxHeightInLevel, dim.height); return sum + dim.width; }, 0); // Add spacing between nodes const spacingWidth = (nodesInLevel.length - 1) * 60; // 60px between nodes const totalLevelWidth = totalWidth + spacingWidth; // Center the level horizontally currentX = this.INITIAL_X + 400 - (totalLevelWidth / 2); // 400 is rough canvas center nodesInLevel.forEach((node, index) => { const nodeDim = this.getNodeDimensions(node.type); // Position node at center of its allocated space const nodeX = currentX + (nodeDim.width / 2); const position: [number, number] = [nodeX, currentY]; positions.set(node.id || node.name, position); // Move X position for next node currentX += nodeDim.width + 60; // 60px spacing }); // Move to next level with appropriate vertical spacing currentY += maxHeightInLevel + 100; // 100px vertical spacing between levels }); return positions; } /** * Adjust positions to avoid overlaps considering node dimensions */ static avoidOverlaps( positions: Map<string, [number, number]>, nodes: any[], minBuffer: number = 40 ): void { const posArray = Array.from(positions.entries()); for (let i = 0; i < posArray.length; i++) { for (let j = i + 1; j < posArray.length; j++) { const [id1, pos1] = posArray[i]; const [id2, pos2] = posArray[j]; // Get node types to determine dimensions const node1 = nodes.find(n => (n.id || n.name) === id1); const node2 = nodes.find(n => (n.id || n.name) === id2); if (!node1 || !node2) continue; const dim1 = this.getNodeDimensions(node1.type); const dim2 = this.getNodeDimensions(node2.type); // Calculate minimum required distance based on node dimensions const minDistanceX = (dim1.width + dim2.width) / 2 + minBuffer; const minDistanceY = (dim1.height + dim2.height) / 2 + minBuffer; const dx = Math.abs(pos2[0] - pos1[0]); const dy = Math.abs(pos2[1] - pos1[1]); // Check for overlap if (dx < minDistanceX && dy < minDistanceY) { // Calculate how much to move const moveX = minDistanceX - dx; const moveY = minDistanceY - dy; // Move in the direction with less overlap if (moveX < moveY) { // Move horizontally pos2[0] += pos2[0] > pos1[0] ? moveX : -moveX; } else { // Move vertically pos2[1] += pos2[1] > pos1[1] ? moveY : -moveY; } positions.set(id2, pos2); } } } } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/mckinleymedia/mcflow-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server