UNPKG

@rxflow/manhattan

Version:

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

418 lines (381 loc) 20.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.getManHattanPath = getManHattanPath; var _react = require("@xyflow/react"); var _geometry = require("./geometry"); var _obstacle = require("./obstacle"); var _options = require("./options"); var _pathfinder = require("./pathfinder"); var _svg = require("./svg"); var _utils = require("./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'] * } * }) * ``` */ function getManHattanPath(params) { const { sourceNodeId, targetNodeId, sourcePosition, targetPosition, nodeLookup, sourceX, sourceY, targetX, targetY, options: userOptions = {} } = params; // Resolve options and add position information const options = (0, _options.resolveOptions)({ ...userOptions, sourcePosition, 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 const sourceNode = nodeLookup.get(sourceNodeId); const 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'); const start = new _geometry.Point(sourceX, sourceY); const end = new _geometry.Point(targetX, targetY); return (0, _svg.pointsToPath)([start, end], options.precision); } // Get node dimensions using ReactFlow's priority logic const sourceDimensions = (0, _utils.getNodeDimensions)(sourceNode); const targetDimensions = (0, _utils.getNodeDimensions)(targetNode); // Get absolute positions from internals const sourcePos = (0, _utils.getNodePosition)(sourceNode); const targetPos = (0, _utils.getNodePosition)(targetNode); // Calculate bounding boxes const sourceBBox = new _geometry.Rectangle(sourcePos.x, sourcePos.y, sourceDimensions.width, sourceDimensions.height); const targetBBox = new _geometry.Rectangle(targetPos.x, targetPos.y, targetDimensions.width, targetDimensions.height); // Create anchor points const sourceAnchor = new _geometry.Point(sourceX, sourceY); const targetAnchor = new _geometry.Point(targetX, targetY); // Try ReactFlow's getSmoothStepPath first const [smoothStepPath] = (0, _react.getSmoothStepPath)({ sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, borderRadius: options.borderRadius }); // Parse the smooth step path to extract points const smoothStepPoints = (0, _svg.parseSVGPath)(smoothStepPath); // Check if smooth step path intersects with any obstacles if (smoothStepPoints.length > 0 && !(0, _utils.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 const obstacleMap = new _obstacle.ObstacleMap(options).build(nodeLookup, sourceNodeId, targetNodeId, sourceAnchor, targetAnchor); // Find route let route = (0, _pathfinder.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(p => `(${p.x}, ${p.y})`)); console.log('[getManHattanPath] Source anchor:', `(${sourceAnchor.x}, ${sourceAnchor.y})`); console.log('[getManHattanPath] Target anchor:', `(${targetAnchor.x}, ${targetAnchor.y})`); // If using smart point generation (sourcePosition/targetPosition specified), // the route already contains the correct extension points, so skip manual processing const useSmartPoints = sourcePosition || targetPosition; if (useSmartPoints) { console.log('[getManHattanPath] Using smart points, skipping manual extension point processing'); // Add source and target anchors to route const finalRoute = [sourceAnchor, ...route, targetAnchor]; console.log('[getManHattanPath] Final route:', finalRoute.map(p => `(${p.x}, ${p.y})`)); return (0, _svg.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 const step = options.step; const tolerance = 1; // Check if first point is an extension point from source if (route.length > 0) { const firstPoint = route[0]; const onLeft = Math.abs(sourceAnchor.x - sourceBBox.x) < tolerance; const onRight = Math.abs(sourceAnchor.x - (sourceBBox.x + sourceBBox.width)) < tolerance; const onTop = Math.abs(sourceAnchor.y - sourceBBox.y) < tolerance; const onBottom = Math.abs(sourceAnchor.y - (sourceBBox.y + sourceBBox.height)) < tolerance; // Check if firstPoint is close to source anchor (indicating it's an extension point) const 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) { const lastPoint = route[route.length - 1]; const onLeft = Math.abs(targetAnchor.x - targetBBox.x) < tolerance; const onRight = Math.abs(targetAnchor.x - (targetBBox.x + targetBBox.width)) < tolerance; const onTop = Math.abs(targetAnchor.y - targetBBox.y) < tolerance; const onBottom = Math.abs(targetAnchor.y - (targetBBox.y + targetBBox.height)) < tolerance; // Check if lastPoint is close to target anchor (indicating it's an extension point) const 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) { const extensionDistance = options.extensionDistance; const firstPoint = route[0]; // Determine which edge the source anchor is on const onLeft = Math.abs(sourceAnchor.x - sourceBBox.x) < tolerance; const onRight = Math.abs(sourceAnchor.x - (sourceBBox.x + sourceBBox.width)) < tolerance; const onTop = Math.abs(sourceAnchor.y - sourceBBox.y) < tolerance; const onBottom = Math.abs(sourceAnchor.y - (sourceBBox.y + sourceBBox.height)) < tolerance; // Insert extension point and corner point to ensure orthogonal path if (onRight) { // Anchor on right edge - extend right by step + borderRadius const extendX = sourceAnchor.x + extensionDistance; const extensionPoint = new _geometry.Point(extendX, sourceAnchor.y); // Check if we need a corner point if (Math.abs(extensionPoint.y - firstPoint.y) > tolerance) { route.unshift(new _geometry.Point(extendX, firstPoint.y)); // Corner point } route.unshift(extensionPoint); // Extension point (fixed distance) console.log('[getManHattanPath] Inserted source extension (right):', `(${extendX}, ${sourceAnchor.y})`); } else if (onLeft) { // Anchor on left edge - extend left by step + borderRadius const extendX = sourceAnchor.x - extensionDistance; const extensionPoint = new _geometry.Point(extendX, sourceAnchor.y); if (Math.abs(extensionPoint.y - firstPoint.y) > tolerance) { route.unshift(new _geometry.Point(extendX, firstPoint.y)); } route.unshift(extensionPoint); console.log('[getManHattanPath] Inserted source extension (left):', `(${extendX}, ${sourceAnchor.y})`); } else if (onBottom) { // Anchor on bottom edge - extend down by step + borderRadius const extendY = sourceAnchor.y + extensionDistance; const extensionPoint = new _geometry.Point(sourceAnchor.x, extendY); if (Math.abs(extensionPoint.x - firstPoint.x) > tolerance) { route.unshift(new _geometry.Point(firstPoint.x, extendY)); } route.unshift(extensionPoint); console.log('[getManHattanPath] Inserted source extension (down):', `(${sourceAnchor.x}, ${extendY})`); } else if (onTop) { // Anchor on top edge - extend up by step + borderRadius const extendY = sourceAnchor.y - extensionDistance; const extensionPoint = new _geometry.Point(sourceAnchor.x, extendY); if (Math.abs(extensionPoint.x - firstPoint.x) > tolerance) { route.unshift(new _geometry.Point(firstPoint.x, extendY)); } route.unshift(extensionPoint); console.log('[getManHattanPath] Inserted source extension (up):', `(${sourceAnchor.x}, ${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) { const firstRoutePoint = route[0]; // Extension point const secondRoutePoint = route[1]; // Corner point (if exists) const 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 const sameX = Math.abs(thirdRoutePoint.x - sourceAnchor.x) < tolerance; const 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) { const fourthPoint = route[3]; // If corner point and fourth point form a straight line, remove the third point const cornerToThird = Math.abs(secondRoutePoint.x - thirdRoutePoint.x) < tolerance || Math.abs(secondRoutePoint.y - thirdRoutePoint.y) < tolerance; const thirdToFourth = Math.abs(thirdRoutePoint.x - fourthPoint.x) < tolerance || Math.abs(thirdRoutePoint.y - fourthPoint.y) < tolerance; if (cornerToThird && thirdToFourth) { console.log('[getManHattanPath] Removing redundant point:', `(${thirdRoutePoint.x}, ${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) const targetBBoxWithPadding = targetBBox.moveAndExpand(options.paddingBox); console.log('[getManHattanPath] Route before zigzag check:', route.map(p => `(${p.x}, ${p.y})`)); console.log('[getManHattanPath] Target BBox with padding:', `x=${targetBBoxWithPadding.x}, y=${targetBBoxWithPadding.y}`); if (route.length >= 3) { let i = 0; while (i < route.length - 2) { const p1 = route[i]; const p2 = route[i + 1]; const p3 = route[i + 2]; // Check if p2 is on the target bbox edge (with padding) const p2OnTargetLeftEdge = Math.abs(p2.x - targetBBoxWithPadding.x) < tolerance; const p2OnTargetRightEdge = Math.abs(p2.x - (targetBBoxWithPadding.x + targetBBoxWithPadding.width)) < tolerance; const p2OnTargetTopEdge = Math.abs(p2.y - targetBBoxWithPadding.y) < tolerance; const p2OnTargetBottomEdge = Math.abs(p2.y - (targetBBoxWithPadding.y + targetBBoxWithPadding.height)) < tolerance; const p2OnTargetEdge = p2OnTargetLeftEdge || p2OnTargetRightEdge || p2OnTargetTopEdge || p2OnTargetBottomEdge; console.log(`[getManHattanPath] Checking i=${i}: p2=(${p2.x}, ${p2.y}), onEdge=${p2OnTargetEdge}`); if (p2OnTargetEdge) { // Check if p1 -> p2 -> p3 forms a zigzag const p1ToP2Horizontal = Math.abs(p1.y - p2.y) < tolerance; const p2ToP3Vertical = Math.abs(p2.x - p3.x) < tolerance; const p1ToP2Vertical = Math.abs(p1.x - p2.x) < tolerance; const p2ToP3Horizontal = Math.abs(p2.y - p3.y) < tolerance; console.log(`[getManHattanPath] Zigzag pattern: H->V=${p1ToP2Horizontal && p2ToP3Vertical}, V->H=${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:', `(${p2.x}, ${p2.y})`, `and (${p3.x}, ${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) { const extensionDistance = options.extensionDistance; const lastPoint = route[route.length - 1]; // Determine which edge the target anchor is on const onLeft = Math.abs(targetAnchor.x - targetBBox.x) < tolerance; const onRight = Math.abs(targetAnchor.x - (targetBBox.x + targetBBox.width)) < tolerance; const onTop = Math.abs(targetAnchor.y - targetBBox.y) < tolerance; const onBottom = Math.abs(targetAnchor.y - (targetBBox.y + targetBBox.height)) < tolerance; // Insert extension point and corner point to ensure orthogonal path if (onLeft) { // Anchor on left edge - extend left by step + borderRadius const extendX = targetAnchor.x - extensionDistance; const extensionPoint = new _geometry.Point(extendX, targetAnchor.y); if (Math.abs(extensionPoint.y - lastPoint.y) > tolerance) { route.push(new _geometry.Point(extendX, lastPoint.y)); // Corner point } route.push(extensionPoint); // Extension point (fixed distance) console.log('[getManHattanPath] Inserted target extension (left):', `(${extendX}, ${targetAnchor.y})`); } else if (onRight) { // Anchor on right edge - extend right by step + borderRadius const extendX = targetAnchor.x + extensionDistance; const extensionPoint = new _geometry.Point(extendX, targetAnchor.y); if (Math.abs(extensionPoint.y - lastPoint.y) > tolerance) { route.push(new _geometry.Point(extendX, lastPoint.y)); } route.push(extensionPoint); console.log('[getManHattanPath] Inserted target extension (right):', `(${extendX}, ${targetAnchor.y})`); } else if (onTop) { // Anchor on top edge - extend up by step + borderRadius const extendY = targetAnchor.y - extensionDistance; const extensionPoint = new _geometry.Point(targetAnchor.x, extendY); if (Math.abs(extensionPoint.x - lastPoint.x) > tolerance) { route.push(new _geometry.Point(lastPoint.x, extendY)); } route.push(extensionPoint); console.log('[getManHattanPath] Inserted target extension (up):', `(${targetAnchor.x}, ${extendY})`); } else if (onBottom) { // Anchor on bottom edge - extend down by step + borderRadius const extendY = targetAnchor.y + extensionDistance; const extensionPoint = new _geometry.Point(targetAnchor.x, extendY); if (Math.abs(extensionPoint.x - lastPoint.x) > tolerance) { route.push(new _geometry.Point(lastPoint.x, extendY)); } route.push(extensionPoint); console.log('[getManHattanPath] Inserted target extension (down):', `(${targetAnchor.x}, ${extendY})`); } } // Remove redundant points before target extension // Similar logic for target side if (route.length > 2) { const lastIdx = route.length - 1; const lastRoutePoint = route[lastIdx]; // Extension point const secondLastPoint = route[lastIdx - 1]; // Corner point (if exists) const thirdLastPoint = route[lastIdx - 2]; // Original A* point // Check if the third-to-last point is redundant const sameX = Math.abs(thirdLastPoint.x - targetAnchor.x) < tolerance; const sameY = Math.abs(thirdLastPoint.y - targetAnchor.y) < tolerance; if (sameX || sameY) { if (route.length > 3) { const fourthLastPoint = route[lastIdx - 3]; const fourthToThird = Math.abs(fourthLastPoint.x - thirdLastPoint.x) < tolerance || Math.abs(fourthLastPoint.y - thirdLastPoint.y) < tolerance; const thirdToSecond = Math.abs(thirdLastPoint.x - secondLastPoint.x) < tolerance || Math.abs(thirdLastPoint.y - secondLastPoint.y) < tolerance; if (fourthToThird && thirdToSecond) { console.log('[getManHattanPath] Removing redundant point:', `(${thirdLastPoint.x}, ${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 let i = 0; while (i < route.length - 2) { const p1 = route[i]; const p2 = route[i + 1]; const p3 = route[i + 2]; // Check if p2 is on the target bbox edge const p2OnTargetLeftEdge = Math.abs(p2.x - targetBBox.x) < tolerance; const p2OnTargetRightEdge = Math.abs(p2.x - (targetBBox.x + targetBBox.width)) < tolerance; const p2OnTargetTopEdge = Math.abs(p2.y - targetBBox.y) < tolerance; const p2OnTargetBottomEdge = Math.abs(p2.y - (targetBBox.y + targetBBox.height)) < tolerance; const p2OnTargetEdge = p2OnTargetLeftEdge || p2OnTargetRightEdge || p2OnTargetTopEdge || p2OnTargetBottomEdge; if (p2OnTargetEdge) { // Check if p1 -> p2 -> p3 forms a zigzag const p1ToP2Horizontal = Math.abs(p1.y - p2.y) < tolerance; const 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 const p4 = route[i + 3]; const p3ToP4Horizontal = Math.abs(p3.y - p4.y) < tolerance; if (p3ToP4Horizontal) { // Pattern: horizontal -> vertical -> horizontal (zigzag) console.log('[getManHattanPath] Removing zigzag at target edge:', `(${p2.x}, ${p2.y})`, `and (${p3.x}, ${p3.y})`); route.splice(i + 1, 2); // Remove p2 and p3 continue; } } } i++; } // Add source and target anchors to route const finalRoute = [sourceAnchor, ...route, targetAnchor]; console.log('[getManHattanPath] Final route:', finalRoute.map(p => `(${p.x}, ${p.y})`)); // Convert to SVG path string return (0, _svg.pointsToPath)(finalRoute, options.precision, options.borderRadius); }