UNPKG

@ichigo_san/graphing

Version:

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

376 lines (346 loc) 10.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _OrthogonalRouter = _interopRequireDefault(require("./OrthogonalRouter")); function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } /** * EdgeRoutingService - Handles automatic edge routing and collision avoidance * Provides smart pathfinding when nodes move or edges are created */ class EdgeRoutingService { constructor(options = {}) { this.router = new _OrthogonalRouter.default(options); this.autoRouteEnabled = options.autoRouteEnabled !== false; this.collisionAvoidanceEnabled = options.collisionAvoidanceEnabled !== false; this.rerouteOnNodeMove = options.rerouteOnNodeMove !== false; } /** * Auto-route a new edge when it's created */ autoRouteNewEdge(edge, nodes) { if (!this.autoRouteEnabled) return edge; const sourceNode = nodes.find(n => n.id === edge.source); const targetNode = nodes.find(n => n.id === edge.target); if (!sourceNode || !targetNode) return edge; // Get obstacles (all other nodes) const obstacles = nodes.filter(node => node.id !== sourceNode.id && node.id !== targetNode.id).map(node => ({ x: node.position.x, y: node.position.y, width: node.width || 100, height: node.height || 100 })); // Calculate optimal route const route = this.router.calculateOptimalRoute(sourceNode, targetNode, obstacles); return { ...edge, data: { ...edge.data, waypoints: route.waypoints, routeType: 'orthogonal', autoRouted: true } }; } /** * Re-route edges when a node moves */ rerouteEdgesOnNodeMove(movedNodeId, edges, nodes) { if (!this.rerouteOnNodeMove) return edges; const movedNode = nodes.find(n => n.id === movedNodeId); if (!movedNode) return edges; return edges.map(edge => { // Only reroute edges connected to the moved node if (edge.source === movedNodeId || edge.target === movedNodeId) { return this.rerouteEdge(edge, nodes); } return edge; }); } /** * Re-route a specific edge */ rerouteEdge(edge, nodes) { var _edge$data; const sourceNode = nodes.find(n => n.id === edge.source); const targetNode = nodes.find(n => n.id === edge.target); if (!sourceNode || !targetNode) return edge; // Only auto-reroute if no manual waypoints exist if ((_edge$data = edge.data) !== null && _edge$data !== void 0 && _edge$data.waypoints && edge.data.waypoints.length > 0) { return edge; } // Get obstacles const obstacles = nodes.filter(node => node.id !== sourceNode.id && node.id !== targetNode.id).map(node => ({ x: node.position.x, y: node.position.y, width: node.width || 100, height: node.height || 100 })); // Calculate new route const route = this.router.calculateOptimalRoute(sourceNode, targetNode, obstacles); return { ...edge, data: { ...edge.data, waypoints: route.waypoints, routeType: 'orthogonal', autoRouted: true } }; } /** * Batch reroute all edges */ rerouteAllEdges(edges, nodes) { return edges.map(edge => this.rerouteEdge(edge, nodes)); } /** * Optimize edge layout to minimize crossings */ optimizeEdgeLayout(edges, nodes) { const optimizedEdges = [...edges]; // Group edges by source and target const edgeGroups = this.groupEdgesByNodes(edges); // Optimize each group for (const [nodePair, groupEdges] of edgeGroups) { if (groupEdges.length > 1) { const optimizedGroup = this.optimizeEdgeGroup(groupEdges, nodes); // Update edges with optimized routes optimizedGroup.forEach((optimizedEdge, index) => { const originalEdge = groupEdges[index]; const edgeIndex = optimizedEdges.findIndex(e => e.id === originalEdge.id); if (edgeIndex !== -1) { optimizedEdges[edgeIndex] = optimizedEdge; } }); } } return optimizedEdges; } /** * Group edges by source-target pairs */ groupEdgesByNodes(edges) { const groups = new Map(); edges.forEach(edge => { const key = `${edge.source}-${edge.target}`; if (!groups.has(key)) { groups.set(key, []); } groups.get(key).push(edge); }); return groups; } /** * Optimize a group of edges between the same nodes */ optimizeEdgeGroup(edges, nodes) { if (edges.length <= 1) return edges; const sourceNode = nodes.find(n => n.id === edges[0].source); const targetNode = nodes.find(n => n.id === edges[0].target); if (!sourceNode || !targetNode) return edges; // Get obstacles const obstacles = nodes.filter(node => node.id !== sourceNode.id && node.id !== targetNode.id).map(node => ({ x: node.position.x, y: node.position.y, width: node.width || 100, height: node.height || 100 })); // Calculate different route patterns for each edge const patterns = [['horizontal', 'vertical'], ['vertical', 'horizontal'], ['horizontal', 'vertical', 'horizontal'], ['vertical', 'horizontal', 'vertical']]; return edges.map((edge, index) => { const pattern = patterns[index % patterns.length]; const route = this.router.calculateRoute(sourceNode, targetNode, obstacles); route.pattern = pattern; return { ...edge, data: { ...edge.data, waypoints: route.waypoints, routeType: 'orthogonal', autoRouted: true } }; }); } /** * Validate edge routing */ validateEdgeRouting(edge, nodes) { var _edge$data2; const sourceNode = nodes.find(n => n.id === edge.source); const targetNode = nodes.find(n => n.id === edge.target); if (!sourceNode || !targetNode) { return { valid: false, error: 'Source or target node not found' }; } // Check if waypoints are valid if ((_edge$data2 = edge.data) !== null && _edge$data2 !== void 0 && _edge$data2.waypoints) { for (const waypoint of edge.data.waypoints) { if (!waypoint || typeof waypoint.x !== 'number' || typeof waypoint.y !== 'number') { return { valid: false, error: 'Invalid waypoint coordinates' }; } } } // Check for collisions if (this.collisionAvoidanceEnabled) { var _edge$data3; const obstacles = nodes.filter(node => node.id !== sourceNode.id && node.id !== targetNode.id).map(node => ({ x: node.position.x, y: node.position.y, width: node.width || 100, height: node.height || 100 })); const waypoints = ((_edge$data3 = edge.data) === null || _edge$data3 === void 0 ? void 0 : _edge$data3.waypoints) || []; const points = [{ x: sourceNode.position.x + (sourceNode.width || 100) / 2, y: sourceNode.position.y + (sourceNode.height || 100) / 2 }, ...waypoints, { x: targetNode.position.x + (targetNode.width || 100) / 2, y: targetNode.position.y + (targetNode.height || 100) / 2 }]; for (let i = 0; i < points.length - 1; i++) { const p1 = points[i]; const p2 = points[i + 1]; for (const obstacle of obstacles) { if (this.segmentIntersectsObstacle(p1, p2, obstacle)) { return { valid: false, error: 'Edge intersects with obstacle' }; } } } } return { valid: true }; } /** * Check if a line segment intersects with an obstacle */ segmentIntersectsObstacle(p1, p2, obstacle) { const { x, y, width = 0, height = 0 } = obstacle; const margin = this.router.obstacleMargin; // Check if either endpoint is inside the obstacle if (this.router.isPointInObstacle(p1, obstacle) || this.router.isPointInObstacle(p2, obstacle)) { return true; } // Check if the line segment intersects with the obstacle rectangle const rect = { x: x - margin, y: y - margin, width: width + 2 * margin, height: height + 2 * margin }; return this.lineIntersectsRect(p1, p2, rect); } /** * Check if a line intersects with a rectangle */ lineIntersectsRect(p1, p2, rect) { const { x, y, width, height } = rect; // Check intersection with each edge of the rectangle const edges = [{ x1: x, y1: y, x2: x + width, y2: y }, // Top { x1: x + width, y1: y, x2: x + width, y2: y + height }, // Right { x1: x, y1: y + height, x2: x + width, y2: y + height }, // Bottom { x1: x, y1: y, x2: x, y2: y + height } // Left ]; for (const edge of edges) { if (this.linesIntersect(p1, p2, { x: edge.x1, y: edge.y1 }, { x: edge.x2, y: edge.y2 })) { return true; } } return false; } /** * Check if two line segments intersect */ linesIntersect(p1, p2, p3, p4) { const det = (a, b, c, d) => a * d - b * c; const delta = det(p2.x - p1.x, p4.x - p3.x, p2.y - p1.y, p4.y - p3.y); if (Math.abs(delta) < 1e-10) return false; // Lines are parallel const s = det(p4.x - p3.x, p4.x - p1.x, p4.y - p3.y, p4.y - p1.y) / delta; const t = det(p2.x - p1.x, p4.x - p1.x, p2.y - p1.y, p4.y - p1.y) / delta; return s >= 0 && s <= 1 && t >= 0 && t <= 1; } /** * Get routing statistics */ getRoutingStats(edges) { const stats = { totalEdges: edges.length, autoRouted: 0, manualRouted: 0, averageWaypoints: 0, totalWaypoints: 0 }; edges.forEach(edge => { var _edge$data4, _edge$data5; if ((_edge$data4 = edge.data) !== null && _edge$data4 !== void 0 && _edge$data4.autoRouted) { stats.autoRouted++; } else { stats.manualRouted++; } const waypoints = ((_edge$data5 = edge.data) === null || _edge$data5 === void 0 ? void 0 : _edge$data5.waypoints) || []; stats.totalWaypoints += waypoints.length; }); stats.averageWaypoints = stats.totalEdges > 0 ? stats.totalWaypoints / stats.totalEdges : 0; return stats; } /** * Update router configuration */ updateConfiguration(config) { this.router = new _OrthogonalRouter.default(config); this.autoRouteEnabled = config.autoRouteEnabled !== false; this.collisionAvoidanceEnabled = config.collisionAvoidanceEnabled !== false; this.rerouteOnNodeMove = config.rerouteOnNodeMove !== false; } } var _default = exports.default = EdgeRoutingService;