UNPKG

@rxflow/manhattan

Version:

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

437 lines (400 loc) 24.7 kB
function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); } function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread(); } function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } function _iterableToArray(iter) { if (typeof Symbol !== "undefined" && iter[Symbol.iterator] != null || iter["@@iterator"] != null) return Array.from(iter); } function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) return _arrayLikeToArray(arr); } function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); } function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } 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; } function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } } function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; } function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : String(i); } function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } import { getSmoothStepPath } from '@xyflow/react'; import { Point, Rectangle } from "./geometry"; import { ObstacleMap } from "./obstacle"; import { resolveOptions } from "./options"; import { findRoute } from "./pathfinder"; import { pointsToPath, parseSVGPath } from "./svg"; import { getNodeDimensions, getNodePosition, pathIntersectsObstacles } from "./utils"; /** * Parameters for getManHattanPath function */ /** * Generate Manhattan-routed path for ReactFlow edges * * @param params - Path generation parameters * @returns SVG path string that can be used with ReactFlow's BaseEdge * * @example * ```typescript * const path = getManHattanPath({ * sourceNodeId: 'node1', * targetNodeId: 'node2', * sourcePosition: { x: 100, y: 100 }, * targetPosition: { x: 300, y: 300 }, * nodeLookup: nodes, * options: { * step: 10, * startDirections: ['bottom'], * endDirections: ['top'] * } * }) * ``` */ export function getManHattanPath(params) { var sourceNodeId = params.sourceNodeId, targetNodeId = params.targetNodeId, sourcePosition = params.sourcePosition, targetPosition = params.targetPosition, nodeLookup = params.nodeLookup, sourceX = params.sourceX, sourceY = params.sourceY, targetX = params.targetX, targetY = params.targetY, _params$options = params.options, userOptions = _params$options === void 0 ? {} : _params$options; // Resolve options and add position information var options = resolveOptions(_objectSpread(_objectSpread({}, userOptions), {}, { sourcePosition: sourcePosition, targetPosition: targetPosition })); // Direction control is automatically handled by getRectPoints: // - When anchor is on an edge, only outward directions are allowed (via isDirectionOutward) // - For sourcePosition="right": anchor on right edge -> only extends right // - For targetPosition="left": anchor on left edge -> path approaches from right (outward from left) // This ensures paths follow the sourcePosition and targetPosition constraints // Get source and target nodes var sourceNode = nodeLookup.get(sourceNodeId); var targetNode = nodeLookup.get(targetNodeId); if (!sourceNode || !targetNode) { // Fallback to simple straight line if nodes not found console.warn('Source or target node not found in nodeLookup'); var start = new Point(sourceX, sourceY); var end = new Point(targetX, targetY); return pointsToPath([start, end], options.precision); } // Get node dimensions using ReactFlow's priority logic var sourceDimensions = getNodeDimensions(sourceNode); var targetDimensions = getNodeDimensions(targetNode); // Get absolute positions from internals var sourcePos = getNodePosition(sourceNode); var targetPos = getNodePosition(targetNode); // Calculate bounding boxes var sourceBBox = new Rectangle(sourcePos.x, sourcePos.y, sourceDimensions.width, sourceDimensions.height); var targetBBox = new Rectangle(targetPos.x, targetPos.y, targetDimensions.width, targetDimensions.height); // Create anchor points var sourceAnchor = new Point(sourceX, sourceY); var targetAnchor = new Point(targetX, targetY); // Try ReactFlow's getSmoothStepPath first var _getSmoothStepPath = getSmoothStepPath({ sourceX: sourceX, sourceY: sourceY, targetX: targetX, targetY: targetY, sourcePosition: sourcePosition, targetPosition: targetPosition, borderRadius: options.borderRadius }), _getSmoothStepPath2 = _slicedToArray(_getSmoothStepPath, 1), smoothStepPath = _getSmoothStepPath2[0]; // Parse the smooth step path to extract points var smoothStepPoints = parseSVGPath(smoothStepPath); // Check if smooth step path intersects with any obstacles if (smoothStepPoints.length > 0 && !pathIntersectsObstacles(smoothStepPoints, nodeLookup)) { console.log('[getManHattanPath] Using ReactFlow getSmoothStepPath (no obstacles)'); return smoothStepPath; } console.log('[getManHattanPath] SmoothStepPath intersects obstacles, using Manhattan routing'); // Build obstacle map with anchor information var obstacleMap = new ObstacleMap(options).build(nodeLookup, sourceNodeId, targetNodeId, sourceAnchor, targetAnchor); // Find route var route = findRoute(sourceBBox, targetBBox, sourceAnchor, targetAnchor, obstacleMap, options); // Fallback to straight line if no route found if (!route) { console.warn('Unable to find Manhattan route, using straight line fallback'); route = [sourceAnchor, targetAnchor]; } console.log('[getManHattanPath] Route from findRoute:', route.map(function (p) { return "(".concat(p.x, ", ").concat(p.y, ")"); })); console.log('[getManHattanPath] Source anchor:', "(".concat(sourceAnchor.x, ", ").concat(sourceAnchor.y, ")")); console.log('[getManHattanPath] Target anchor:', "(".concat(targetAnchor.x, ", ").concat(targetAnchor.y, ")")); // If using smart point generation (sourcePosition/targetPosition specified), // the route already contains the correct extension points, so skip manual processing var useSmartPoints = sourcePosition || targetPosition; if (useSmartPoints) { console.log('[getManHattanPath] Using smart points, skipping manual extension point processing'); // Add source and target anchors to route var _finalRoute = [sourceAnchor].concat(_toConsumableArray(route), [targetAnchor]); console.log('[getManHattanPath] Final route:', _finalRoute.map(function (p) { return "(".concat(p.x, ", ").concat(p.y, ")"); })); return pointsToPath(_finalRoute, options.precision, options.borderRadius); } // Remove extension points from route that were added by getRectPoints // We will add our own with fixed step distance var step = options.step; var tolerance = 1; // Check if first point is an extension point from source if (route.length > 0) { var firstPoint = route[0]; var onLeft = Math.abs(sourceAnchor.x - sourceBBox.x) < tolerance; var onRight = Math.abs(sourceAnchor.x - (sourceBBox.x + sourceBBox.width)) < tolerance; var onTop = Math.abs(sourceAnchor.y - sourceBBox.y) < tolerance; var onBottom = Math.abs(sourceAnchor.y - (sourceBBox.y + sourceBBox.height)) < tolerance; // Check if firstPoint is close to source anchor (indicating it's an extension point) var distToFirst = sourceAnchor.manhattanDistance(firstPoint); if (distToFirst < step * 2) { // This is likely an extension point, remove it if (onRight && firstPoint.x > sourceAnchor.x || onLeft && firstPoint.x < sourceAnchor.x || onBottom && firstPoint.y > sourceAnchor.y || onTop && firstPoint.y < sourceAnchor.y) { route.shift(); console.log('[getManHattanPath] Removed extension point from route start'); } } } // Check if last point is an extension point from target if (route.length > 0) { var lastPoint = route[route.length - 1]; var _onLeft = Math.abs(targetAnchor.x - targetBBox.x) < tolerance; var _onRight = Math.abs(targetAnchor.x - (targetBBox.x + targetBBox.width)) < tolerance; var _onTop = Math.abs(targetAnchor.y - targetBBox.y) < tolerance; var _onBottom = Math.abs(targetAnchor.y - (targetBBox.y + targetBBox.height)) < tolerance; // Check if lastPoint is close to target anchor (indicating it's an extension point) var distToLast = targetAnchor.manhattanDistance(lastPoint); if (distToLast < step * 2) { // This is likely an extension point, remove it if (_onLeft && lastPoint.x < targetAnchor.x || _onRight && lastPoint.x > targetAnchor.x || _onTop && lastPoint.y < targetAnchor.y || _onBottom && lastPoint.y > targetAnchor.y) { route.pop(); console.log('[getManHattanPath] Removed extension point from route end'); } } } // Insert extension point at source - always extend away from node edge by fixed distance if (route.length > 0) { var extensionDistance = options.extensionDistance; var _firstPoint = route[0]; // Determine which edge the source anchor is on var _onLeft2 = Math.abs(sourceAnchor.x - sourceBBox.x) < tolerance; var _onRight2 = Math.abs(sourceAnchor.x - (sourceBBox.x + sourceBBox.width)) < tolerance; var _onTop2 = Math.abs(sourceAnchor.y - sourceBBox.y) < tolerance; var _onBottom2 = Math.abs(sourceAnchor.y - (sourceBBox.y + sourceBBox.height)) < tolerance; // Insert extension point and corner point to ensure orthogonal path if (_onRight2) { // Anchor on right edge - extend right by step + borderRadius var extendX = sourceAnchor.x + extensionDistance; var extensionPoint = new Point(extendX, sourceAnchor.y); // Check if we need a corner point if (Math.abs(extensionPoint.y - _firstPoint.y) > tolerance) { route.unshift(new Point(extendX, _firstPoint.y)); // Corner point } route.unshift(extensionPoint); // Extension point (fixed distance) console.log('[getManHattanPath] Inserted source extension (right):', "(".concat(extendX, ", ").concat(sourceAnchor.y, ")")); } else if (_onLeft2) { // Anchor on left edge - extend left by step + borderRadius var _extendX = sourceAnchor.x - extensionDistance; var _extensionPoint = new Point(_extendX, sourceAnchor.y); if (Math.abs(_extensionPoint.y - _firstPoint.y) > tolerance) { route.unshift(new Point(_extendX, _firstPoint.y)); } route.unshift(_extensionPoint); console.log('[getManHattanPath] Inserted source extension (left):', "(".concat(_extendX, ", ").concat(sourceAnchor.y, ")")); } else if (_onBottom2) { // Anchor on bottom edge - extend down by step + borderRadius var extendY = sourceAnchor.y + extensionDistance; var _extensionPoint2 = new Point(sourceAnchor.x, extendY); if (Math.abs(_extensionPoint2.x - _firstPoint.x) > tolerance) { route.unshift(new Point(_firstPoint.x, extendY)); } route.unshift(_extensionPoint2); console.log('[getManHattanPath] Inserted source extension (down):', "(".concat(sourceAnchor.x, ", ").concat(extendY, ")")); } else if (_onTop2) { // Anchor on top edge - extend up by step + borderRadius var _extendY = sourceAnchor.y - extensionDistance; var _extensionPoint3 = new Point(sourceAnchor.x, _extendY); if (Math.abs(_extensionPoint3.x - _firstPoint.x) > tolerance) { route.unshift(new Point(_firstPoint.x, _extendY)); } route.unshift(_extensionPoint3); console.log('[getManHattanPath] Inserted source extension (up):', "(".concat(sourceAnchor.x, ", ").concat(_extendY, ")")); } } // Remove redundant points after source extension // If the first route point has the same x or y coordinate as the source anchor, it's redundant if (route.length > 2) { var firstRoutePoint = route[0]; // Extension point var secondRoutePoint = route[1]; // Corner point (if exists) var thirdRoutePoint = route[2]; // Original A* point // Check if the third point (original A* point) is redundant // It's redundant if it's on the same line as the corner point and can be skipped var sameX = Math.abs(thirdRoutePoint.x - sourceAnchor.x) < tolerance; var sameY = Math.abs(thirdRoutePoint.y - sourceAnchor.y) < tolerance; if (sameX || sameY) { // The third point is aligned with the source anchor, likely redundant // Check if we can skip it by connecting corner point directly to the next point if (route.length > 3) { var fourthPoint = route[3]; // If corner point and fourth point form a straight line, remove the third point var cornerToThird = Math.abs(secondRoutePoint.x - thirdRoutePoint.x) < tolerance || Math.abs(secondRoutePoint.y - thirdRoutePoint.y) < tolerance; var thirdToFourth = Math.abs(thirdRoutePoint.x - fourthPoint.x) < tolerance || Math.abs(thirdRoutePoint.y - fourthPoint.y) < tolerance; if (cornerToThird && thirdToFourth) { console.log('[getManHattanPath] Removing redundant point:', "(".concat(thirdRoutePoint.x, ", ").concat(thirdRoutePoint.y, ")")); route.splice(2, 1); // Remove the third point } } } } // Optimize zigzag patterns BEFORE inserting target extension // Check for patterns like: (1360, 16) -> (815, 16) -> (815, -134) // where the middle segment goes to target edge and then moves along it // Use target bbox with padding (same as ObstacleMap) var targetBBoxWithPadding = targetBBox.moveAndExpand(options.paddingBox); console.log('[getManHattanPath] Route before zigzag check:', route.map(function (p) { return "(".concat(p.x, ", ").concat(p.y, ")"); })); console.log('[getManHattanPath] Target BBox with padding:', "x=".concat(targetBBoxWithPadding.x, ", y=").concat(targetBBoxWithPadding.y)); if (route.length >= 3) { var _i = 0; while (_i < route.length - 2) { var p1 = route[_i]; var p2 = route[_i + 1]; var p3 = route[_i + 2]; // Check if p2 is on the target bbox edge (with padding) var p2OnTargetLeftEdge = Math.abs(p2.x - targetBBoxWithPadding.x) < tolerance; var p2OnTargetRightEdge = Math.abs(p2.x - (targetBBoxWithPadding.x + targetBBoxWithPadding.width)) < tolerance; var p2OnTargetTopEdge = Math.abs(p2.y - targetBBoxWithPadding.y) < tolerance; var p2OnTargetBottomEdge = Math.abs(p2.y - (targetBBoxWithPadding.y + targetBBoxWithPadding.height)) < tolerance; var p2OnTargetEdge = p2OnTargetLeftEdge || p2OnTargetRightEdge || p2OnTargetTopEdge || p2OnTargetBottomEdge; console.log("[getManHattanPath] Checking i=".concat(_i, ": p2=(").concat(p2.x, ", ").concat(p2.y, "), onEdge=").concat(p2OnTargetEdge)); if (p2OnTargetEdge) { // Check if p1 -> p2 -> p3 forms a zigzag var p1ToP2Horizontal = Math.abs(p1.y - p2.y) < tolerance; var p2ToP3Vertical = Math.abs(p2.x - p3.x) < tolerance; var p1ToP2Vertical = Math.abs(p1.x - p2.x) < tolerance; var p2ToP3Horizontal = Math.abs(p2.y - p3.y) < tolerance; console.log("[getManHattanPath] Zigzag pattern: H->V=".concat(p1ToP2Horizontal && p2ToP3Vertical, ", V->H=").concat(p1ToP2Vertical && p2ToP3Horizontal)); if (p1ToP2Horizontal && p2ToP3Vertical || p1ToP2Vertical && p2ToP3Horizontal) { // We have a zigzag at target edge, remove p2 and p3 console.log('[getManHattanPath] Removing zigzag at target edge:', "(".concat(p2.x, ", ").concat(p2.y, ")"), "and (".concat(p3.x, ", ").concat(p3.y, ")")); route.splice(_i + 1, 2); // Remove p2 and p3 continue; } } _i++; } } // Insert extension point at target - always extend away from node edge by fixed distance if (route.length > 0) { var _extensionDistance = options.extensionDistance; var _lastPoint = route[route.length - 1]; // Determine which edge the target anchor is on var _onLeft3 = Math.abs(targetAnchor.x - targetBBox.x) < tolerance; var _onRight3 = Math.abs(targetAnchor.x - (targetBBox.x + targetBBox.width)) < tolerance; var _onTop3 = Math.abs(targetAnchor.y - targetBBox.y) < tolerance; var _onBottom3 = Math.abs(targetAnchor.y - (targetBBox.y + targetBBox.height)) < tolerance; // Insert extension point and corner point to ensure orthogonal path if (_onLeft3) { // Anchor on left edge - extend left by step + borderRadius var _extendX2 = targetAnchor.x - _extensionDistance; var _extensionPoint4 = new Point(_extendX2, targetAnchor.y); if (Math.abs(_extensionPoint4.y - _lastPoint.y) > tolerance) { route.push(new Point(_extendX2, _lastPoint.y)); // Corner point } route.push(_extensionPoint4); // Extension point (fixed distance) console.log('[getManHattanPath] Inserted target extension (left):', "(".concat(_extendX2, ", ").concat(targetAnchor.y, ")")); } else if (_onRight3) { // Anchor on right edge - extend right by step + borderRadius var _extendX3 = targetAnchor.x + _extensionDistance; var _extensionPoint5 = new Point(_extendX3, targetAnchor.y); if (Math.abs(_extensionPoint5.y - _lastPoint.y) > tolerance) { route.push(new Point(_extendX3, _lastPoint.y)); } route.push(_extensionPoint5); console.log('[getManHattanPath] Inserted target extension (right):', "(".concat(_extendX3, ", ").concat(targetAnchor.y, ")")); } else if (_onTop3) { // Anchor on top edge - extend up by step + borderRadius var _extendY2 = targetAnchor.y - _extensionDistance; var _extensionPoint6 = new Point(targetAnchor.x, _extendY2); if (Math.abs(_extensionPoint6.x - _lastPoint.x) > tolerance) { route.push(new Point(_lastPoint.x, _extendY2)); } route.push(_extensionPoint6); console.log('[getManHattanPath] Inserted target extension (up):', "(".concat(targetAnchor.x, ", ").concat(_extendY2, ")")); } else if (_onBottom3) { // Anchor on bottom edge - extend down by step + borderRadius var _extendY3 = targetAnchor.y + _extensionDistance; var _extensionPoint7 = new Point(targetAnchor.x, _extendY3); if (Math.abs(_extensionPoint7.x - _lastPoint.x) > tolerance) { route.push(new Point(_lastPoint.x, _extendY3)); } route.push(_extensionPoint7); console.log('[getManHattanPath] Inserted target extension (down):', "(".concat(targetAnchor.x, ", ").concat(_extendY3, ")")); } } // Remove redundant points before target extension // Similar logic for target side if (route.length > 2) { var lastIdx = route.length - 1; var lastRoutePoint = route[lastIdx]; // Extension point var secondLastPoint = route[lastIdx - 1]; // Corner point (if exists) var thirdLastPoint = route[lastIdx - 2]; // Original A* point // Check if the third-to-last point is redundant var _sameX = Math.abs(thirdLastPoint.x - targetAnchor.x) < tolerance; var _sameY = Math.abs(thirdLastPoint.y - targetAnchor.y) < tolerance; if (_sameX || _sameY) { if (route.length > 3) { var fourthLastPoint = route[lastIdx - 3]; var fourthToThird = Math.abs(fourthLastPoint.x - thirdLastPoint.x) < tolerance || Math.abs(fourthLastPoint.y - thirdLastPoint.y) < tolerance; var thirdToSecond = Math.abs(thirdLastPoint.x - secondLastPoint.x) < tolerance || Math.abs(thirdLastPoint.y - secondLastPoint.y) < tolerance; if (fourthToThird && thirdToSecond) { console.log('[getManHattanPath] Removing redundant point:', "(".concat(thirdLastPoint.x, ", ").concat(thirdLastPoint.y, ")")); route.splice(lastIdx - 2, 1); // Remove the third-to-last point } } } } // Additional optimization: Remove unnecessary zigzag patterns near target // Check for patterns like: (1360, 16) -> (815, 16) -> (815, -134) -> (800, -134) // where (815, 16) and (815, -134) form a zigzag at the target edge var i = 0; while (i < route.length - 2) { var _p = route[i]; var _p2 = route[i + 1]; var _p3 = route[i + 2]; // Check if p2 is on the target bbox edge var _p2OnTargetLeftEdge = Math.abs(_p2.x - targetBBox.x) < tolerance; var _p2OnTargetRightEdge = Math.abs(_p2.x - (targetBBox.x + targetBBox.width)) < tolerance; var _p2OnTargetTopEdge = Math.abs(_p2.y - targetBBox.y) < tolerance; var _p2OnTargetBottomEdge = Math.abs(_p2.y - (targetBBox.y + targetBBox.height)) < tolerance; var _p2OnTargetEdge = _p2OnTargetLeftEdge || _p2OnTargetRightEdge || _p2OnTargetTopEdge || _p2OnTargetBottomEdge; if (_p2OnTargetEdge) { // Check if p1 -> p2 -> p3 forms a zigzag var _p1ToP2Horizontal = Math.abs(_p.y - _p2.y) < tolerance; var _p2ToP3Vertical = Math.abs(_p2.x - _p3.x) < tolerance; if (_p1ToP2Horizontal && _p2ToP3Vertical && i < route.length - 3) { // We have horizontal -> vertical at target edge // Check if we can skip p2 and p3 var p4 = route[i + 3]; var p3ToP4Horizontal = Math.abs(_p3.y - p4.y) < tolerance; if (p3ToP4Horizontal) { // Pattern: horizontal -> vertical -> horizontal (zigzag) console.log('[getManHattanPath] Removing zigzag at target edge:', "(".concat(_p2.x, ", ").concat(_p2.y, ")"), "and (".concat(_p3.x, ", ").concat(_p3.y, ")")); route.splice(i + 1, 2); // Remove p2 and p3 continue; } } } i++; } // Add source and target anchors to route var finalRoute = [sourceAnchor].concat(_toConsumableArray(route), [targetAnchor]); console.log('[getManHattanPath] Final route:', finalRoute.map(function (p) { return "(".concat(p.x, ", ").concat(p.y, ")"); })); // Convert to SVG path string return pointsToPath(finalRoute, options.precision, options.borderRadius); }