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