UNPKG

@rxflow/manhattan

Version:

Manhattan routing algorithm for ReactFlow - generates orthogonal paths with obstacle avoidance

377 lines (338 loc) 17.1 kB
function _createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it.return != null) it.return(); } finally { if (didErr) throw err; } } }; } function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; } import { Point } from "../geometry"; import { SortedSet } from "./SortedSet"; import { getGrid, round, getDirectionAngle, getDirectionChange, getGridOffsets, getRectPoints, getCost, getKey, reconstructRoute } from "../utils"; /** * Generate smart points based on position using extensionDistance and binary search * * Algorithm: * 1. First try extensionDistance in the specified direction * 2. If blocked, use binary search starting from step, halving until finding accessible point * 3. For target points, approach from opposite direction */ function generateSmartPoints(anchor, bbox, position, grid, map, options) { var isTarget = arguments.length > 6 && arguments[6] !== undefined ? arguments[6] : false; var directionMap = { 'right': { x: 1, y: 0 }, 'left': { x: -1, y: 0 }, 'top': { x: 0, y: -1 }, 'bottom': { x: 0, y: 1 } }; var direction = directionMap[position]; if (!direction) { console.warn("[generateSmartPoints] Unknown position: ".concat(position, ", falling back to anchor")); return [anchor]; } // Both source and target extend in the specified direction // - Source: extends away from node in sourcePosition direction // - Target: extends away from node in targetPosition direction (path approaches from this direction) var actualDirection = direction; var points = []; // 1. First try extensionDistance var extensionPoint = new Point(anchor.x + actualDirection.x * options.extensionDistance, anchor.y + actualDirection.y * options.extensionDistance).round(options.precision); console.log("[generateSmartPoints] ".concat(isTarget ? 'Target' : 'Source', " position=").concat(position, ", trying extension point: (").concat(extensionPoint.x, ", ").concat(extensionPoint.y, ")")); if (map.isAccessible(extensionPoint)) { points.push(extensionPoint); console.log("[generateSmartPoints] Extension point is accessible"); return points; } console.log("[generateSmartPoints] Extension point blocked, using step-based search"); // 2. Step-based search with binary refinement // First, extend outward by step increments until we find an accessible point var stepMultiplier = 1; var maxSteps = 20; // Prevent infinite loop var foundAccessibleDistance = -1; console.log("[generateSmartPoints] Starting outward search with step=".concat(options.step)); while (stepMultiplier <= maxSteps) { var distance = stepMultiplier * options.step; var testPoint = new Point(anchor.x + actualDirection.x * distance, anchor.y + actualDirection.y * distance).round(options.precision); console.log("[generateSmartPoints] Testing ".concat(stepMultiplier, "*step (distance=").concat(distance, "): (").concat(testPoint.x, ", ").concat(testPoint.y, ")")); if (map.isAccessible(testPoint)) { foundAccessibleDistance = distance; console.log("[generateSmartPoints] Found accessible point at ".concat(stepMultiplier, "*step (distance=").concat(distance, ")")); break; } stepMultiplier++; } // 3. If we found an accessible point, refine by binary search within the last step interval if (foundAccessibleDistance > 0) { var outerDistance = foundAccessibleDistance; var innerDistance = foundAccessibleDistance - options.step; console.log("[generateSmartPoints] Refining between ".concat(innerDistance, " and ").concat(outerDistance)); // Binary search within the last step interval to find the closest accessible point var left = innerDistance; var right = outerDistance; var bestDistance = outerDistance; // Binary search with precision of 1px while (right - left > 1) { var mid = (left + right) / 2; var _testPoint = new Point(anchor.x + actualDirection.x * mid, anchor.y + actualDirection.y * mid).round(options.precision); console.log("[generateSmartPoints] Binary search testing distance ".concat(mid.toFixed(1), ": (").concat(_testPoint.x, ", ").concat(_testPoint.y, ")")); if (map.isAccessible(_testPoint)) { bestDistance = mid; right = mid; console.log("[generateSmartPoints] Point accessible, searching closer (right=".concat(right, ")")); } else { left = mid; console.log("[generateSmartPoints] Point blocked, searching further (left=".concat(left, ")")); } } // Use the best distance found var finalPoint = new Point(anchor.x + actualDirection.x * bestDistance, anchor.y + actualDirection.y * bestDistance).round(options.precision); points.push(finalPoint); console.log("[generateSmartPoints] Final point at distance ".concat(bestDistance, ": (").concat(finalPoint.x, ", ").concat(finalPoint.y, ")")); } else { // 4. If no accessible point found after maxSteps, use anchor as fallback console.log("[generateSmartPoints] No accessible point found after ".concat(maxSteps, " steps, using anchor: (").concat(anchor.x, ", ").concat(anchor.y, ")")); points.push(anchor); } return points; } /** * Find route between two points using A* algorithm */ export function findRoute(sourceBBox, targetBBox, sourceAnchor, targetAnchor, map, options) { var precision = options.precision; // Round anchor points var sourceEndpoint = round(sourceAnchor.clone(), precision); var targetEndpoint = round(targetAnchor.clone(), precision); // Get grid for this route var grid = getGrid(options.step, sourceEndpoint, targetEndpoint); // Get pathfinding points var startPoint = sourceEndpoint; var endPoint = targetEndpoint; // Get start and end points around rectangles // Use smart point generation based on position if available var startPoints; var endPoints; // Generate smart start points based on sourcePosition if (options.sourcePosition) { startPoints = generateSmartPoints(startPoint, sourceBBox, options.sourcePosition, grid, map, options, false); console.log('[findRoute] Start points from smart generation:', startPoints.map(function (p) { return "(".concat(p.x, ", ").concat(p.y, ")"); })); } else { startPoints = getRectPoints(startPoint, sourceBBox, options.startDirections, grid, options); console.log('[findRoute] Start points from getRectPoints:', startPoints.map(function (p) { return "(".concat(p.x, ", ").concat(p.y, ")"); })); // Take into account only accessible rect points startPoints = startPoints.filter(function (p) { return map.isAccessible(p); }); } // Generate smart end points based on targetPosition if (options.targetPosition) { endPoints = generateSmartPoints(targetEndpoint, targetBBox, options.targetPosition, grid, map, options, true); console.log('[findRoute] End points from smart generation:', endPoints.map(function (p) { return "(".concat(p.x, ", ").concat(p.y, ")"); })); } else { endPoints = getRectPoints(targetEndpoint, targetBBox, options.endDirections, grid, options); console.log('[findRoute] End points from getRectPoints:', endPoints.map(function (p) { return "(".concat(p.x, ", ").concat(p.y, ")"); })); // Take into account only accessible rect points endPoints = endPoints.filter(function (p) { return map.isAccessible(p); }); } console.log('[findRoute] Start points after filter:', startPoints.map(function (p) { return "(".concat(p.x, ", ").concat(p.y, ")"); })); console.log('[findRoute] End points after filter:', endPoints.map(function (p) { return "(".concat(p.x, ", ").concat(p.y, ")"); })); // Ensure we always have at least the anchor points // This handles edge cases where anchor is on the node boundary if (startPoints.length === 0) { startPoints = [round(startPoint, precision)]; } if (endPoints.length === 0) { endPoints = [round(endPoint, precision)]; } // Initialize A* data structures var openSet = new SortedSet(); var points = new Map(); var parents = new Map(); var costs = new Map(); // Add all start points to open set var _iterator = _createForOfIteratorHelper(startPoints), _step; try { for (_iterator.s(); !(_step = _iterator.n()).done;) { var _startPoint = _step.value; var key = getKey(_startPoint); openSet.add(key, getCost(_startPoint, endPoints)); points.set(key, _startPoint); costs.set(key, 0); } } catch (err) { _iterator.e(err); } finally { _iterator.f(); } var previousRouteDirectionAngle = options.previousDirectionAngle; var isPathBeginning = previousRouteDirectionAngle === undefined; // Get directions with grid offsets var directions = getGridOffsets(grid, options); var numDirections = directions.length; // Create set of end point keys for quick lookup var endPointsKeys = new Set(endPoints.map(function (p) { return getKey(p); })); // Check if start and end points are the same var sameStartEndPoints = startPoints.length === endPoints.length && startPoints.every(function (sp, i) { return sp.equals(endPoints[i]); }); // Main A* loop var loopsRemaining = options.maxLoopCount; while (!openSet.isEmpty() && loopsRemaining > 0) { // Get the closest item and mark it CLOSED var currentKey = openSet.pop(); if (!currentKey) break; var currentPoint = points.get(currentKey); var currentParent = parents.get(currentKey); var currentCost = costs.get(currentKey); var isStartPoint = currentPoint.equals(startPoint); var isRouteBeginning = currentParent === undefined; // Calculate previous direction angle var previousDirectionAngle = void 0; if (!isRouteBeginning) { previousDirectionAngle = getDirectionAngle(currentParent, currentPoint, numDirections, grid, options); } else if (!isPathBeginning) { previousDirectionAngle = previousRouteDirectionAngle; } else if (!isStartPoint) { previousDirectionAngle = getDirectionAngle(startPoint, currentPoint, numDirections, grid, options); } else { previousDirectionAngle = null; } // Check if we reached any endpoint var skipEndCheck = isRouteBeginning && sameStartEndPoints; if (!skipEndCheck && endPointsKeys.has(currentKey)) { options.previousDirectionAngle = previousDirectionAngle; return reconstructRoute(parents, points, currentPoint, startPoint, endPoint); } // Explore neighbors in all directions var _iterator2 = _createForOfIteratorHelper(directions), _step2; try { for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) { var _previousDirectionAng; var direction = _step2.value; var directionAngle = direction.angle; var directionChange = getDirectionChange((_previousDirectionAng = previousDirectionAngle) !== null && _previousDirectionAng !== void 0 ? _previousDirectionAng : 0, directionAngle); // Don't use the point if direction changed too rapidly if (!(isPathBeginning && isStartPoint) && directionChange > options.maxDirectionChange) { continue; } // Calculate neighbor point and align to global grid var rawNeighbor = currentPoint.clone().translate(direction.gridOffsetX || 0, direction.gridOffsetY || 0); // Align to global grid for consistent path alignment var neighborPoint = new Point(Math.round(rawNeighbor.x / grid.x) * grid.x, Math.round(rawNeighbor.y / grid.y) * grid.y).round(precision); var neighborKey = getKey(neighborPoint); // Skip if closed or not accessible if (openSet.isClose(neighborKey) || !map.isAccessible(neighborPoint)) { continue; } // Check if we can reach any end point directly from this neighbor // This allows connecting to end points that are not on the grid var canReachEndPoint = false; var reachableEndPoint = null; var _iterator3 = _createForOfIteratorHelper(endPoints), _step3; try { for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) { var endPt = _step3.value; var distanceToEnd = neighborPoint.manhattanDistance(endPt); // If close enough to end point (within step distance), try direct connection if (distanceToEnd < options.step * 1.5) { // Check if we can move directly to the end point var dx = endPt.x - neighborPoint.x; var dy = endPt.y - neighborPoint.y; // Allow direct connection if it's orthogonal or close to orthogonal var isOrthogonal = Math.abs(dx) < 0.1 || Math.abs(dy) < 0.1; if (isOrthogonal && map.isAccessible(endPt)) { canReachEndPoint = true; reachableEndPoint = endPt; break; } } } // If we can reach an end point directly, add it as the final step } catch (err) { _iterator3.e(err); } finally { _iterator3.f(); } if (canReachEndPoint && reachableEndPoint) { var endKey = getKey(reachableEndPoint); var endCost = neighborPoint.manhattanDistance(reachableEndPoint); var totalCost = currentCost + direction.cost + endCost; if (!openSet.isOpen(endKey) || totalCost < (costs.get(endKey) || Infinity)) { points.set(endKey, reachableEndPoint); parents.set(endKey, neighborPoint); costs.set(endKey, totalCost); // Also add the neighbor point if not already added if (!points.has(neighborKey)) { points.set(neighborKey, neighborPoint); parents.set(neighborKey, currentPoint); costs.set(neighborKey, currentCost + direction.cost); } // Check if this is our target end point if (endPointsKeys.has(endKey)) { options.previousDirectionAngle = directionAngle; return reconstructRoute(parents, points, reachableEndPoint, startPoint, endPoint); } } } // Check if neighbor is an end point (exact match) if (endPointsKeys.has(neighborKey)) { var isEndPoint = neighborPoint.equals(endPoint); if (!isEndPoint) { var endDirectionAngle = getDirectionAngle(neighborPoint, endPoint, numDirections, grid, options); var endDirectionChange = getDirectionChange(directionAngle, endDirectionAngle); if (endDirectionChange > options.maxDirectionChange) { continue; } } } // Calculate costs var neighborCost = direction.cost; var neighborPenalty = isStartPoint ? 0 : options.penalties[directionChange] || 0; var costFromStart = currentCost + neighborCost + neighborPenalty; // Update if not in open set or found better path if (!openSet.isOpen(neighborKey) || costFromStart < (costs.get(neighborKey) || Infinity)) { points.set(neighborKey, neighborPoint); parents.set(neighborKey, currentPoint); costs.set(neighborKey, costFromStart); openSet.add(neighborKey, costFromStart + getCost(neighborPoint, endPoints)); } } } catch (err) { _iterator2.e(err); } finally { _iterator2.f(); } loopsRemaining -= 1; } // No path found, try fallback if (options.fallbackRoute) { return options.fallbackRoute(startPoint, endPoint); } return null; }