UNPKG

@ichigo_san/graphing

Version:

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

355 lines (318 loc) 11.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _PathfindingEngine = _interopRequireDefault(require("./PathfindingEngine")); var _GridSystem = _interopRequireDefault(require("./GridSystem")); var _CollisionDetector = _interopRequireDefault(require("./CollisionDetector")); function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } /** * OrthogonalRoutingService - Centralized routing logic and management * Coordinates pathfinding, grid alignment, and collision detection */ class OrthogonalRoutingService { constructor(options = {}) { // Initialize core services this.pathfindingEngine = new _PathfindingEngine.default({ gridSize: options.gridSize || 20, jettySize: options.jettySize || 20, maxIterations: options.maxIterations || 1000 }); this.gridSystem = new _GridSystem.default({ gridSize: options.gridSize || 20, snapThreshold: options.snapThreshold || 10, showGrid: options.showGrid || false }); this.collisionDetector = new _CollisionDetector.default({ gridSize: options.gridSize || 20, nodeMargin: options.nodeMargin || 20, edgeMargin: options.edgeMargin || 10 }); // Service configuration this.config = { autoRerouteOnNodeMove: options.autoRerouteOnNodeMove !== false, enableCollisionAvoidance: options.enableCollisionAvoidance !== false, optimizeExistingPaths: options.optimizeExistingPaths !== false, maxRerouteAttempts: options.maxRerouteAttempts || 3, performanceMode: options.performanceMode || 'balanced' // 'fast', 'balanced', 'quality' }; // Performance tracking this.stats = { routeCalculations: 0, rerouteOperations: 0, cacheHits: 0, totalRoutingTime: 0, averageRoutingTime: 0 }; // Route cache for performance this.routeCache = new Map(); this.lastNodePositions = new Map(); this.rerouteQueue = new Set(); } /** * Calculate optimal route for a new edge */ calculateRoute(sourceNode, targetNode, sourceHandle, targetHandle, excludeEdges = []) { const startTime = performance.now(); try { // Generate cache key const cacheKey = this.generateCacheKey(sourceNode, targetNode, sourceHandle, targetHandle); // Check cache first if (this.routeCache.has(cacheKey)) { this.stats.cacheHits++; return this.routeCache.get(cacheKey); } // Get connection points const sourcePoint = this.pathfindingEngine.getConnectionPoint(sourceNode, sourceHandle); const targetPoint = this.pathfindingEngine.getConnectionPoint(targetNode, targetHandle); // Update collision detection (exclude current edge and specified edges) const allNodes = this.getCurrentNodes(); const allEdges = this.getCurrentEdges().filter(e => !excludeEdges.includes(e.id)); this.collisionDetector.updateCollisionMap(allNodes.filter(n => n.id !== sourceNode.id && n.id !== targetNode.id), allEdges); // Calculate optimal path const path = this.pathfindingEngine.findPath(sourcePoint, targetPoint, [], allNodes.filter(n => n.id !== sourceNode.id && n.id !== targetNode.id)); // Ensure orthogonal constraints and grid alignment const optimizedPath = this.gridSystem.enforceOrthogonalPath(path); // Create route result const route = { path: optimizedPath, waypoints: optimizedPath.slice(1, -1), // Exclude start and end sourcePoint, targetPoint, metadata: { segments: optimizedPath.length - 1, totalLength: this.calculatePathLength(optimizedPath), isOrthogonal: this.gridSystem.validateOrthogonalPath(optimizedPath), hasCollisions: this.checkPathCollisions(optimizedPath, excludeEdges), calculationTime: performance.now() - startTime, cacheKey } }; // Cache the result this.routeCache.set(cacheKey, route); // Update statistics this.stats.routeCalculations++; this.updatePerformanceStats(performance.now() - startTime); return route; } catch (error) { console.error('OrthogonalRoutingService: Route calculation failed', error); // Return fallback route return this.createFallbackRoute(sourceNode, targetNode, sourceHandle, targetHandle); } } /** * Reroute existing edges when nodes move */ rerouteEdges(movedNodeIds, allNodes, allEdges) { if (!this.config.autoRerouteOnNodeMove) return []; const startTime = performance.now(); const reroutedEdges = []; // Find edges connected to moved nodes const affectedEdges = allEdges.filter(edge => movedNodeIds.includes(edge.source) || movedNodeIds.includes(edge.target)); for (const edge of affectedEdges) { try { this.rerouteQueue.add(edge.id); const sourceNode = allNodes.find(n => n.id === edge.source); const targetNode = allNodes.find(n => n.id === edge.target); if (sourceNode && targetNode) { const newRoute = this.calculateRoute(sourceNode, targetNode, edge.sourceHandle, edge.targetHandle, [edge.id] // Exclude current edge from collision detection ); // Update edge with new route const updatedEdge = { ...edge, data: { ...edge.data, waypoints: newRoute.waypoints, autoRouted: true, lastReroute: Date.now(), rerouteReason: 'node-moved' } }; reroutedEdges.push(updatedEdge); } this.rerouteQueue.delete(edge.id); } catch (error) { console.error(`OrthogonalRoutingService: Failed to reroute edge ${edge.id}`, error); this.rerouteQueue.delete(edge.id); } } // Update statistics this.stats.rerouteOperations++; this.updatePerformanceStats(performance.now() - startTime); // Update node position cache for (const nodeId of movedNodeIds) { const node = allNodes.find(n => n.id === nodeId); if (node) { this.lastNodePositions.set(nodeId, { ...node.position }); } } return reroutedEdges; } /** * Optimize all existing routes */ optimizeAllRoutes(nodes, edges) { if (!this.config.optimizeExistingPaths) return []; const optimizedEdges = []; for (const edge of edges) { var _edge$data; if (((_edge$data = edge.data) === null || _edge$data === void 0 ? void 0 : _edge$data.autoRouted) !== false) { const sourceNode = nodes.find(n => n.id === edge.source); const targetNode = nodes.find(n => n.id === edge.target); if (sourceNode && targetNode) { const newRoute = this.calculateRoute(sourceNode, targetNode, edge.sourceHandle, edge.targetHandle, [edge.id]); // Only update if route is significantly better if (this.isRouteBetter(newRoute, edge)) { optimizedEdges.push({ ...edge, data: { ...edge.data, waypoints: newRoute.waypoints, lastOptimization: Date.now() } }); } } } } return optimizedEdges; } /** * Create fallback route when pathfinding fails */ createFallbackRoute(sourceNode, targetNode, sourceHandle, targetHandle) { const sourcePoint = this.gridSystem.snapToGrid({ x: sourceNode.position.x + (sourceNode.width || 150) / 2, y: sourceNode.position.y + (sourceNode.height || 100) / 2 }); const targetPoint = this.gridSystem.snapToGrid({ x: targetNode.position.x + (targetNode.width || 150) / 2, y: targetNode.position.y + (targetNode.height || 100) / 2 }); const fallbackPath = this.gridSystem.enforceOrthogonalPath([sourcePoint, targetPoint]); return { path: fallbackPath, waypoints: fallbackPath.slice(1, -1), sourcePoint, targetPoint, metadata: { segments: fallbackPath.length - 1, totalLength: this.calculatePathLength(fallbackPath), isOrthogonal: true, hasCollisions: false, isFallback: true, calculationTime: 0, cacheKey: null } }; } /** * Check if new route is better than existing */ isRouteBetter(newRoute, existingEdge) { var _existingEdge$data; if (!((_existingEdge$data = existingEdge.data) !== null && _existingEdge$data !== void 0 && _existingEdge$data.waypoints)) return true; const existingLength = this.calculatePathLength([newRoute.sourcePoint, ...existingEdge.data.waypoints, newRoute.targetPoint]); const improvement = (existingLength - newRoute.metadata.totalLength) / existingLength; // Consider route better if it's at least 10% shorter or has fewer segments return improvement > 0.1 || newRoute.metadata.segments < existingEdge.data.waypoints.length + 1; } /** * Calculate total path length */ calculatePathLength(path) { let length = 0; for (let i = 1; i < path.length; i++) { const prev = path[i - 1]; const current = path[i]; length += Math.abs(current.x - prev.x) + Math.abs(current.y - prev.y); } return length; } /** * Check if path has collisions */ checkPathCollisions(path, excludeEdges = []) { for (let i = 1; i < path.length; i++) { if (this.collisionDetector.checkSegmentCollision(path[i - 1], path[i], excludeEdges)) { return true; } } return false; } /** * Generate cache key for route */ generateCacheKey(sourceNode, targetNode, sourceHandle, targetHandle) { return `${sourceNode.id}:${sourceHandle}-${targetNode.id}:${targetHandle}:${sourceNode.position.x},${sourceNode.position.y}-${targetNode.position.x},${targetNode.position.y}`; } /** * Update performance statistics */ updatePerformanceStats(duration) { this.stats.totalRoutingTime += duration; this.stats.averageRoutingTime = this.stats.totalRoutingTime / this.stats.routeCalculations; } /** * Clear route cache */ clearCache() { this.routeCache.clear(); this.lastNodePositions.clear(); } /** * Get current nodes (to be overridden by implementation) */ getCurrentNodes() { // This should be overridden to return current React Flow nodes return []; } /** * Get current edges (to be overridden by implementation) */ getCurrentEdges() { // This should be overridden to return current React Flow edges return []; } /** * Update service configuration */ updateConfig(newConfig) { this.config = { ...this.config, ...newConfig }; // Update sub-service configurations if (newConfig.gridSize) { this.pathfindingEngine.gridSize = newConfig.gridSize; this.gridSystem.gridSize = newConfig.gridSize; this.collisionDetector.gridSize = newConfig.gridSize; } // Clear cache when config changes this.clearCache(); } /** * Get service statistics */ getStats() { return { ...this.stats, cacheSize: this.routeCache.size, queueSize: this.rerouteQueue.size, collisionDetectorStats: this.collisionDetector.getStats() }; } /** * Get service configuration */ getConfig() { return { ...this.config }; } } var _default = exports.default = OrthogonalRoutingService;