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