@rxflow/manhattan
Version:
Manhattan routing algorithm for ReactFlow - generates orthogonal paths with obstacle avoidance
377 lines (338 loc) • 17.1 kB
JavaScript
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;
}