UNPKG

@ichigo_san/graphing

Version:

A lightweight UML-style diagram editor built with React Flow and Tailwind CSS

495 lines (456 loc) 13.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; /** * PathfindingEngine - A* algorithm optimized for orthogonal routing * Based on draw.io's pathfinding approach with React Flow integration */ class PathfindingEngine { constructor(options = {}) { this.gridSize = options.gridSize || 20; this.jettySize = options.jettySize || 20; // Minimum distance from nodes this.maxIterations = options.maxIterations || 1000; this.obstacles = new Set(); this.nodeCache = new Map(); } /** * Find optimal orthogonal path between two points */ findPath(startPoint, endPoint, obstacles = [], nodes = []) { try { // Convert to grid coordinates const start = this.snapToGrid(startPoint); const end = this.snapToGrid(endPoint); // Update obstacle map this.updateObstacles(obstacles, nodes); // A* pathfinding with orthogonal constraints const path = this.aStarOrthogonal(start, end); // Convert back to world coordinates and optimize const optimizedPath = this.optimizePath(path); // Ensure we always return a valid path if (optimizedPath && optimizedPath.length >= 2) { return optimizedPath; } // If optimization failed, return the original path if (path && path.length >= 2) { return path; } // Final fallback - create direct path return this.createDirectOrthogonalPath(start, end); } catch (error) { console.warn('PathfindingEngine: Error in findPath, using direct route', error); return this.createDirectOrthogonalPath(startPoint, endPoint); } } /** * A* pathfinding algorithm with orthogonal movement only */ aStarOrthogonal(start, end) { // If start and end are the same, return direct path if (start.x === end.x && start.y === end.y) { return [start]; } const openSet = [{ point: start, g: 0, h: this.heuristic(start, end), f: 0, parent: null }]; const closedSet = new Set(); const visited = new Map(); let iterations = 0; const maxIterations = this.maxIterations * 3; // Increase max iterations significantly while (openSet.length > 0 && iterations < maxIterations) { iterations++; // Get node with lowest f score openSet.sort((a, b) => a.f - b.f); const current = openSet.shift(); const currentKey = `${current.point.x},${current.point.y}`; // Reached destination if (current.point.x === end.x && current.point.y === end.y) { return this.reconstructPath(current); } closedSet.add(currentKey); // Check orthogonal neighbors (up, down, left, right) const neighbors = this.getOrthogonalNeighbors(current.point); for (const neighbor of neighbors) { const neighborKey = `${neighbor.x},${neighbor.y}`; if (closedSet.has(neighborKey) || this.isObstacle(neighbor)) { continue; } const g = current.g + this.gridSize; const h = this.heuristic(neighbor, end); const f = g + h; const existingNode = visited.get(neighborKey); if (!existingNode || g < existingNode.g) { const node = { point: neighbor, g, h, f, parent: current }; visited.set(neighborKey, node); if (!openSet.find(n => n.point.x === neighbor.x && n.point.y === neighbor.y)) { openSet.push(node); } } } } // If we can't find a path, try with a larger search area if (iterations >= maxIterations) { console.warn('PathfindingEngine: Max iterations reached, using direct route'); } return this.createDirectOrthogonalPath(start, end); } /** * Get orthogonal neighbors (up, down, left, right only) */ getOrthogonalNeighbors(point) { return [{ x: point.x, y: point.y - this.gridSize }, // Up { x: point.x, y: point.y + this.gridSize }, // Down { x: point.x - this.gridSize, y: point.y }, // Left { x: point.x + this.gridSize, y: point.y } // Right ]; } /** * Manhattan distance heuristic (perfect for orthogonal routing) */ heuristic(point, end) { return Math.abs(point.x - end.x) + Math.abs(point.y - end.y); } /** * Reconstruct path from A* result */ reconstructPath(node) { const path = []; let current = node; while (current) { path.unshift(current.point); current = current.parent; } return path; } /** * Create direct orthogonal path as fallback */ createDirectOrthogonalPath(start, end) { // Ensure we have valid start and end points if (!start || !end) { console.warn('PathfindingEngine: Invalid start or end point', { start, end }); return [{ x: 0, y: 0 }, { x: 100, y: 100 }]; } const path = [start]; // Create L-shaped path with better positioning const dx = Math.abs(end.x - start.x); const dy = Math.abs(end.y - start.y); // Only add intermediate points if there's significant distance if (dx > this.gridSize || dy > this.gridSize) { // Create L-shaped path with intelligent direction choice if (dx > dy) { // Horizontal first - go most of the way horizontally, then vertically const midX = start.x + (end.x - start.x) * 0.8; const midXSnapped = this.snapToGrid({ x: midX, y: start.y }).x; // Only add intermediate points if they're different from start/end if (Math.abs(midXSnapped - start.x) > this.gridSize) { path.push({ x: midXSnapped, y: start.y }); } if (Math.abs(end.y - start.y) > this.gridSize) { path.push({ x: midXSnapped, y: end.y }); } } else { // Vertical first - go most of the way vertically, then horizontally const midY = start.y + (end.y - start.y) * 0.8; const midYSnapped = this.snapToGrid({ x: start.x, y: midY }).y; // Only add intermediate points if they're different from start/end if (Math.abs(midYSnapped - start.y) > this.gridSize) { path.push({ x: start.x, y: midYSnapped }); } if (Math.abs(end.x - start.x) > this.gridSize) { path.push({ x: end.x, y: midYSnapped }); } } } // Only add end point if it's different from the last point const lastPoint = path[path.length - 1]; if (lastPoint.x !== end.x || lastPoint.y !== end.y) { path.push(end); } // Ensure we always have at least 2 points if (path.length < 2) { path.push(end); } return path; } /** * Update obstacle map from nodes and existing edges */ updateObstacles(obstacles, nodes) { this.obstacles.clear(); // Add node boundaries as obstacles with minimal margin for (const node of nodes) { const bounds = this.getNodeBounds(node); this.addRectangleObstacle(bounds); } // Add custom obstacles for (const obstacle of obstacles) { this.addRectangleObstacle(obstacle); } } /** * Get node boundaries with minimal jetty margin */ getNodeBounds(node) { var _node$style, _node$style2; const margin = Math.max(5, this.jettySize / 4); // Reduce margin significantly const x = node.position.x - margin; const y = node.position.y - margin; const width = (node.width || ((_node$style = node.style) === null || _node$style === void 0 ? void 0 : _node$style.width) || 150) + margin * 2; const height = (node.height || ((_node$style2 = node.style) === null || _node$style2 === void 0 ? void 0 : _node$style2.height) || 100) + margin * 2; return { x, y, width, height }; } /** * Add rectangular obstacle to obstacle map */ addRectangleObstacle(rect) { const startX = this.snapToGrid({ x: rect.x, y: 0 }).x; const endX = this.snapToGrid({ x: rect.x + rect.width, y: 0 }).x; const startY = this.snapToGrid({ x: 0, y: rect.y }).y; const endY = this.snapToGrid({ x: 0, y: rect.y + rect.height }).y; for (let x = startX; x <= endX; x += this.gridSize) { for (let y = startY; y <= endY; y += this.gridSize) { this.obstacles.add(`${x},${y}`); } } } /** * Check if point is an obstacle */ isObstacle(point) { return this.obstacles.has(`${point.x},${point.y}`); } /** * Snap point to grid */ snapToGrid(point) { return { x: Math.round(point.x / this.gridSize) * this.gridSize, y: Math.round(point.y / this.gridSize) * this.gridSize }; } /** * Optimize path by removing unnecessary waypoints */ optimizePath(path) { if (path.length <= 2) return path; const optimized = [path[0]]; for (let i = 1; i < path.length - 1; i++) { const prev = path[i - 1]; const current = path[i]; const next = path[i + 1]; // Keep waypoint if it changes direction if (!this.isCollinear(prev, current, next)) { optimized.push(current); } } optimized.push(path[path.length - 1]); return optimized; } /** * Check if three points are collinear (on same line) */ isCollinear(p1, p2, p3) { return p1.x === p2.x && p2.x === p3.x || p1.y === p2.y && p2.y === p3.y; } /** * Get connection point on node edge with support for multiple connection points */ getConnectionPoint(node, handleId) { var _node$style3, _node$style4; if (!node) { console.error('PathfindingEngine: Node is null/undefined', { handleId }); return { x: 0, y: 0 }; } const x = node.position.x; const y = node.position.y; const width = node.width || ((_node$style3 = node.style) === null || _node$style3 === void 0 ? void 0 : _node$style3.width) || 150; const height = node.height || ((_node$style4 = node.style) === null || _node$style4 === void 0 ? void 0 : _node$style4.height) || 100; // Calculate connection point based on handle position // Handle various handle ID formats and provide intelligent defaults if (!handleId || handleId === 'undefined') { // Default to right for source, left for target handleId = 'right'; } // Extract position and location from handle ID // Support formats: "top-left-source", "right-center-source", "top-source", "right" const parts = handleId.split('-'); const position = parts[0] || 'right'; const location = parts.length > 2 ? parts[1] : parts.length === 2 && !['source', 'target'].includes(parts[1]) ? parts[1] : 'center'; let connectionPoint; switch (position) { case 'top': switch (location) { case 'left': connectionPoint = { x: x + width * 0.25, y }; break; case 'right': connectionPoint = { x: x + width * 0.75, y }; break; default: connectionPoint = { x: x + width / 2, y }; break; } break; case 'right': switch (location) { case 'top': connectionPoint = { x: x + width, y: y + height * 0.25 }; break; case 'bottom': connectionPoint = { x: x + width, y: y + height * 0.75 }; break; default: connectionPoint = { x: x + width, y: y + height / 2 }; break; } break; case 'bottom': switch (location) { case 'left': connectionPoint = { x: x + width * 0.25, y: y + height }; break; case 'right': connectionPoint = { x: x + width * 0.75, y: y + height }; break; default: connectionPoint = { x: x + width / 2, y: y + height }; break; } break; case 'left': switch (location) { case 'top': connectionPoint = { x, y: y + height * 0.25 }; break; case 'bottom': connectionPoint = { x, y: y + height * 0.75 }; break; default: connectionPoint = { x, y: y + height / 2 }; break; } break; default: // Fallback to right center for unknown positions connectionPoint = { x: x + width, y: y + height / 2 }; } return this.snapToGrid(connectionPoint); } } var _default = exports.default = PathfindingEngine;