UNPKG

@ichigo_san/graphing

Version:

A lightweight UML-style diagram editor built with React Flow and Tailwind CSS

411 lines (381 loc) 10.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; /** * CollisionDetector - Handles obstacle detection and avoidance for routing * Identifies nodes, edges, and other obstacles that paths should avoid */ class CollisionDetector { constructor(options = {}) { this.nodeMargin = options.nodeMargin || 20; // Margin around nodes this.edgeMargin = options.edgeMargin || 10; // Margin around edges this.gridSize = options.gridSize || 20; this.collisionMap = new Map(); this.nodeCache = new Map(); this.edgeCache = new Map(); } /** * Update collision map with current nodes and edges */ updateCollisionMap(nodes, edges) { this.collisionMap.clear(); this.nodeCache.clear(); this.edgeCache.clear(); // Add node obstacles for (const node of nodes) { this.addNodeObstacle(node); } // Add edge obstacles for (const edge of edges) { this.addEdgeObstacle(edge, nodes); } } /** * Add node as obstacle with margin */ addNodeObstacle(node) { const bounds = this.getNodeBounds(node); this.nodeCache.set(node.id, bounds); // Add grid points around node as obstacles const startX = Math.floor(bounds.x / this.gridSize) * this.gridSize; const endX = Math.ceil((bounds.x + bounds.width) / this.gridSize) * this.gridSize; const startY = Math.floor(bounds.y / this.gridSize) * this.gridSize; const endY = Math.ceil((bounds.y + bounds.height) / this.gridSize) * this.gridSize; for (let x = startX; x <= endX; x += this.gridSize) { for (let y = startY; y <= endY; y += this.gridSize) { const key = `${x},${y}`; if (!this.collisionMap.has(key)) { this.collisionMap.set(key, []); } this.collisionMap.get(key).push({ type: 'node', id: node.id, bounds, priority: 1 }); } } } /** * Add edge as obstacle with margin */ addEdgeObstacle(edge, nodes) { const sourceNode = nodes.find(n => n.id === edge.source); const targetNode = nodes.find(n => n.id === edge.target); if (!sourceNode || !targetNode) return; const segments = this.getEdgeSegments(edge, sourceNode, targetNode); this.edgeCache.set(edge.id, segments); for (const segment of segments) { this.addSegmentObstacle(segment, edge.id); } } /** * Get edge segments with waypoints */ getEdgeSegments(edge, sourceNode, targetNode) { var _edge$data; const segments = []; const waypoints = ((_edge$data = edge.data) === null || _edge$data === void 0 ? void 0 : _edge$data.waypoints) || []; // Calculate connection points const sourcePoint = this.getConnectionPoint(sourceNode, edge.sourceHandle); const targetPoint = this.getConnectionPoint(targetNode, edge.targetHandle); // Build path: source -> waypoints -> target const pathPoints = [sourcePoint, ...waypoints, targetPoint]; // Convert to segments for (let i = 0; i < pathPoints.length - 1; i++) { segments.push({ start: pathPoints[i], end: pathPoints[i + 1], edgeId: edge.id }); } return segments; } /** * Add segment as obstacle */ addSegmentObstacle(segment, edgeId) { const points = this.getSegmentGridPoints(segment); for (const point of points) { const key = `${point.x},${point.y}`; if (!this.collisionMap.has(key)) { this.collisionMap.set(key, []); } this.collisionMap.get(key).push({ type: 'edge', id: edgeId, segment, priority: 0.5 }); } } /** * Get grid points along a segment with margin */ getSegmentGridPoints(segment) { const points = []; const { start, end } = segment; // Determine if segment is horizontal or vertical const isHorizontal = Math.abs(start.y - end.y) < Math.abs(start.x - end.x); if (isHorizontal) { // Horizontal segment const minX = Math.min(start.x, end.x); const maxX = Math.max(start.x, end.x); const y = start.y; // Add points along the segment with margin for (let x = minX; x <= maxX; x += this.gridSize) { // Add margin above and below for (let my = -this.edgeMargin; my <= this.edgeMargin; my += this.gridSize) { points.push(this.snapToGrid({ x, y: y + my })); } } } else { // Vertical segment const minY = Math.min(start.y, end.y); const maxY = Math.max(start.y, end.y); const x = start.x; // Add points along the segment with margin for (let y = minY; y <= maxY; y += this.gridSize) { // Add margin left and right for (let mx = -this.edgeMargin; mx <= this.edgeMargin; mx += this.gridSize) { points.push(this.snapToGrid({ x: x + mx, y })); } } } return points; } /** * Check if a point is blocked by obstacles */ isBlocked(point, excludeIds = []) { const key = `${point.x},${point.y}`; const obstacles = this.collisionMap.get(key) || []; for (const obstacle of obstacles) { if (!excludeIds.includes(obstacle.id)) { return true; } } return false; } /** * Get obstacles at a specific point */ getObstaclesAt(point) { const key = `${point.x},${point.y}`; return this.collisionMap.get(key) || []; } /** * Check if a path segment intersects with obstacles */ checkSegmentCollision(start, end, excludeIds = []) { const points = this.getSegmentGridPoints({ start, end }); for (const point of points) { if (this.isBlocked(point, excludeIds)) { return true; } } return false; } /** * Get node bounds with margin */ getNodeBounds(node) { const x = node.position.x - this.nodeMargin; const y = node.position.y - this.nodeMargin; const width = (node.width || 150) + this.nodeMargin * 2; const height = (node.height || 100) + this.nodeMargin * 2; return { x, y, width, height }; } /** * Get connection point on node edge */ getConnectionPoint(node, handleId) { const x = node.position.x; const y = node.position.y; const width = node.width || 150; const height = node.height || 100; // Extract position from handle ID (e.g., "top-source" -> "top") const position = (handleId === null || handleId === void 0 ? void 0 : handleId.split('-')[0]) || 'right'; switch (position) { case 'top': return this.snapToGrid({ x: x + width / 2, y }); case 'right': return this.snapToGrid({ x: x + width, y: y + height / 2 }); case 'bottom': return this.snapToGrid({ x: x + width / 2, y: y + height }); case 'left': return this.snapToGrid({ x, y: y + height / 2 }); default: return this.snapToGrid({ x: x + width / 2, y: y + height / 2 }); } } /** * Find clear areas around obstacles */ findClearAreas(bounds) { const clearAreas = []; const startX = Math.floor(bounds.x / this.gridSize) * this.gridSize; const endX = Math.ceil((bounds.x + bounds.width) / this.gridSize) * this.gridSize; const startY = Math.floor(bounds.y / this.gridSize) * this.gridSize; const endY = Math.ceil((bounds.y + bounds.height) / this.gridSize) * this.gridSize; for (let x = startX; x <= endX; x += this.gridSize) { for (let y = startY; y <= endY; y += this.gridSize) { if (!this.isBlocked({ x, y })) { clearAreas.push({ x, y }); } } } return clearAreas; } /** * Get routing channels (clear paths between obstacles) */ getRoutingChannels(start, end) { const channels = []; // Horizontal channels const minY = Math.min(start.y, end.y); const maxY = Math.max(start.y, end.y); for (let y = minY; y <= maxY; y += this.gridSize) { const channelStart = Math.min(start.x, end.x); const channelEnd = Math.max(start.x, end.x); let clearStart = null; for (let x = channelStart; x <= channelEnd; x += this.gridSize) { if (this.isBlocked({ x, y })) { if (clearStart !== null) { channels.push({ type: 'horizontal', y, startX: clearStart, endX: x - this.gridSize }); clearStart = null; } } else if (clearStart === null) { clearStart = x; } } if (clearStart !== null) { channels.push({ type: 'horizontal', y, startX: clearStart, endX: channelEnd }); } } // Vertical channels const minX = Math.min(start.x, end.x); const maxX = Math.max(start.x, end.x); for (let x = minX; x <= maxX; x += this.gridSize) { const channelStart = Math.min(start.y, end.y); const channelEnd = Math.max(start.y, end.y); let clearStart = null; for (let y = channelStart; y <= channelEnd; y += this.gridSize) { if (this.isBlocked({ x, y })) { if (clearStart !== null) { channels.push({ type: 'vertical', x, startY: clearStart, endY: y - this.gridSize }); clearStart = null; } } else if (clearStart === null) { clearStart = y; } } if (clearStart !== null) { channels.push({ type: 'vertical', x, startY: clearStart, endY: channelEnd }); } } return channels; } /** * Snap point to grid */ snapToGrid(point) { return { x: Math.round(point.x / this.gridSize) * this.gridSize, y: Math.round(point.y / this.gridSize) * this.gridSize }; } /** * Clear cache and collision map */ clear() { this.collisionMap.clear(); this.nodeCache.clear(); this.edgeCache.clear(); } /** * Get statistics about current collision map */ getStats() { const nodeCount = this.nodeCache.size; const edgeCount = this.edgeCache.size; const obstaclePoints = this.collisionMap.size; return { nodeCount, edgeCount, obstaclePoints, memoryUsage: { collisionMap: this.collisionMap.size, nodeCache: this.nodeCache.size, edgeCache: this.edgeCache.size } }; } } var _default = exports.default = CollisionDetector;