UNPKG

@rxflow/manhattan

Version:

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

223 lines (204 loc) 8.27 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ObstacleMap = void 0; var _geometry = require("../geometry"); var _utils = require("../utils"); /** * ObstacleMap class for managing obstacles in pathfinding * Uses a grid-based spatial partitioning for efficient queries */ class ObstacleMap { options; mapGridSize; map; sourceAnchor; targetAnchor; constructor(options) { this.options = options; this.mapGridSize = 100; this.map = new Map(); } /** * Build obstacle map from node lookup */ build(nodeLookup, sourceNodeId, targetNodeId, sourceAnchor, targetAnchor) { const { excludeNodes, excludeShapes, excludeTerminals, paddingBox } = this.options; // Store anchors for later use in isAccessible this.sourceAnchor = sourceAnchor; this.targetAnchor = targetAnchor; // Iterate through all nodes nodeLookup.forEach(node => { // Check if node should be excluded const isExcludedNode = excludeNodes.includes(node.id); const isExcludedShape = node.type ? excludeShapes.includes(node.type) : false; const isSourceTerminal = excludeTerminals.includes('source') && node.id === sourceNodeId; const isTargetTerminal = excludeTerminals.includes('target') && node.id === targetNodeId; const isSource = node.id === sourceNodeId; const isTarget = node.id === targetNodeId; // Skip if node should be excluded (but not source/target - we handle them specially) if (isExcludedNode || isExcludedShape || isSourceTerminal || isTargetTerminal) { return; } // Calculate node bounding box with padding const position = (0, _utils.getNodePosition)(node); const dimensions = (0, _utils.getNodeDimensions)(node); let bbox = new _geometry.Rectangle(position.x, position.y, dimensions.width, dimensions.height).moveAndExpand(paddingBox); // For source and target nodes, add them as full obstacles // We'll handle anchor accessibility in isAccessible() method if (isSource) { console.log('[ObstacleMap] Adding source node as obstacle:'); console.log(' Node ID:', node.id); console.log(' BBox:', `(${bbox.x}, ${bbox.y}, ${bbox.width}, ${bbox.height})`); console.log(' Anchor:', sourceAnchor ? `(${sourceAnchor.x}, ${sourceAnchor.y})` : 'none'); } else if (isTarget) { console.log('[ObstacleMap] Adding target node as obstacle:'); console.log(' Node ID:', node.id); console.log(' BBox:', `(${bbox.x}, ${bbox.y}, ${bbox.width}, ${bbox.height})`); console.log(' Anchor:', targetAnchor ? `(${targetAnchor.x}, ${targetAnchor.y})` : 'none'); } // Map bbox to grid cells const origin = bbox.getOrigin().snapToGrid(this.mapGridSize); const corner = bbox.getCorner().snapToGrid(this.mapGridSize); for (let x = origin.x; x <= corner.x; x += this.mapGridSize) { for (let y = origin.y; y <= corner.y; y += this.mapGridSize) { const key = new _geometry.Point(x, y).toString(); if (!this.map.has(key)) { this.map.set(key, []); } this.map.get(key).push(bbox); } } }); return this; } /** * Shrink bbox to exclude the area around the anchor point * This allows paths to start/end at the anchor but prevents crossing the node */ shrinkBBoxAroundAnchor(bbox, anchor, nodePosition, nodeDimensions) { const tolerance = 1; const step = this.options.step || 10; const margin = step * 3; // Increased margin for better clearance // Determine which edge the anchor is on (relative to original node position) const onLeft = Math.abs(anchor.x - nodePosition.x) < tolerance; const onRight = Math.abs(anchor.x - (nodePosition.x + nodeDimensions.width)) < tolerance; const onTop = Math.abs(anchor.y - nodePosition.y) < tolerance; const onBottom = Math.abs(anchor.y - (nodePosition.y + nodeDimensions.height)) < tolerance; console.log('[shrinkBBoxAroundAnchor] Edge detection:'); console.log(' anchor.x:', anchor.x, 'nodePosition.x:', nodePosition.x, 'diff:', Math.abs(anchor.x - nodePosition.x)); console.log(' anchor.x:', anchor.x, 'nodeRight:', nodePosition.x + nodeDimensions.width, 'diff:', Math.abs(anchor.x - (nodePosition.x + nodeDimensions.width))); console.log(' onLeft:', onLeft, 'onRight:', onRight, 'onTop:', onTop, 'onBottom:', onBottom); console.log(' margin:', margin); // Shrink bbox based on anchor position if (onLeft) { // Anchor on left edge - exclude left portion return new _geometry.Rectangle(bbox.x + margin, bbox.y, Math.max(0, bbox.width - margin), bbox.height); } else if (onRight) { // Anchor on right edge - exclude right portion return new _geometry.Rectangle(bbox.x, bbox.y, Math.max(0, bbox.width - margin), bbox.height); } else if (onTop) { // Anchor on top edge - exclude top portion return new _geometry.Rectangle(bbox.x, bbox.y + margin, bbox.width, Math.max(0, bbox.height - margin)); } else if (onBottom) { // Anchor on bottom edge - exclude bottom portion return new _geometry.Rectangle(bbox.x, bbox.y, bbox.width, Math.max(0, bbox.height - margin)); } // If anchor is not on an edge, return original bbox return bbox; } /** * Check if a point is accessible (not inside any obstacle) * Uses binary search optimization: step -> step/2 -> step/4 -> ... -> 1px */ isAccessible(point, checkRadius = 0) { const key = point.clone().snapToGrid(this.mapGridSize).toString(); const rects = this.map.get(key); if (!rects) { return true; } // Check if point is near an anchor - if so, allow it const margin = (this.options.step || 10) * 3; if (this.sourceAnchor) { const distToSource = Math.abs(point.x - this.sourceAnchor.x) + Math.abs(point.y - this.sourceAnchor.y); if (distToSource < margin) { console.log(`[isAccessible] Point (${point.x}, ${point.y}): accessible (near source anchor)`); return true; } } if (this.targetAnchor) { const distToTarget = Math.abs(point.x - this.targetAnchor.x) + Math.abs(point.y - this.targetAnchor.y); if (distToTarget < margin) { console.log(`[isAccessible] Point (${point.x}, ${point.y}): accessible (near target anchor)`); return true; } } // If checkRadius is specified, use binary search to find accessible points if (checkRadius > 0) { return this.isAccessibleWithBinarySearch(point, checkRadius, rects); } const accessible = rects.every(rect => !rect.containsPoint(point)); // Debug: log points on the direct path if (point.y === 40 && point.x >= 0 && point.x <= 545) { console.log(`[isAccessible] Point (${point.x}, ${point.y}): ${accessible ? 'accessible' : 'BLOCKED'}`); if (!accessible) { rects.forEach((rect, i) => { if (rect.containsPoint(point)) { console.log(` Blocked by rect ${i}: (${rect.x}, ${rect.y}, ${rect.width}, ${rect.height})`); } }); } } return accessible; } /** * Check accessibility using binary search optimization * Tries step -> step/2 -> step/4 -> ... -> 1px */ isAccessibleWithBinarySearch(point, maxRadius, rects) { // First check the point itself if (rects.every(rect => !rect.containsPoint(point))) { return true; } // Binary search: start with step, then halve until we reach 1px let radius = maxRadius; const offsets = [{ dx: 1, dy: 0 }, // right { dx: -1, dy: 0 }, // left { dx: 0, dy: 1 }, // down { dx: 0, dy: -1 } // up ]; while (radius >= 1) { for (const offset of offsets) { const testPoint = new _geometry.Point(point.x + offset.dx * radius, point.y + offset.dy * radius); if (rects.every(rect => !rect.containsPoint(testPoint))) { return true; } } // Halve the radius for next iteration radius = Math.floor(radius / 2); } return false; } } exports.ObstacleMap = ObstacleMap;