UNPKG

@ichigo_san/graphing

Version:

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

1,183 lines (1,027 loc) 36.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; /** * InlineEdgeWorkerService - Self-contained Web Worker for Edge Processing * Creates workers dynamically from inline code, eliminating external file dependencies */ // Inline worker code (the entire worker logic) const WORKER_CODE = ` /** * Orthogonal Edge Processing Web Worker * Handles intelligent waypoint optimization, segment intersection detection, * and smart pathfinding for Draw.io-style orthogonal edges */ // Worker state let workerState = { isProcessing: false, cache: new Map(), performanceMetrics: { totalProcessingTime: 0, operationsCount: 0, cacheHits: 0 } }; // Utility functions const createCacheKey = (edge, nodes) => { const edgeKey = \`\${edge.id}-\${edge.source}-\${edge.target}-\${JSON.stringify(edge.waypoints || [])}\`; const nodePositions = nodes.map(n => \`\${n.id}:\${n.position.x},\${n.position.y}\`).join('|'); return \`\${edgeKey}-\${nodePositions}\`; }; const getNodeBounds = (node) => ({ x: node.position.x, y: node.position.y, width: node.width || 150, height: node.height || 60, right: (node.position.x) + (node.width || 150), bottom: (node.position.y) + (node.height || 60) }); const getConnectionPoint = (node, position) => { const bounds = getNodeBounds(node); switch (position) { case 'top': return { x: bounds.x + bounds.width / 2, y: bounds.y }; case 'right': return { x: bounds.right, y: bounds.y + bounds.height / 2 }; case 'bottom': return { x: bounds.x + bounds.width / 2, y: bounds.bottom }; case 'left': return { x: bounds.x, y: bounds.y + bounds.height / 2 }; default: return { x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height / 2 }; } }; // Draw.io-style connection point calculation const calculateOptimalConnectionPoint = (node, targetPoint, sourcePoint) => { if (!node) return null; const bounds = getNodeBounds(node); const center = { x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height / 2 }; // Calculate which side is closest to the target const dx = targetPoint.x - center.x; const dy = targetPoint.y - center.y; // Add margin for better visual appearance (draw.io style) const margin = 5; if (Math.abs(dx) > Math.abs(dy)) { // Horizontal connection if (dx > 0) { return { x: bounds.right + margin, y: center.y }; } else { return { x: bounds.x - margin, y: center.y }; } } else { // Vertical connection if (dy > 0) { return { x: center.x, y: bounds.bottom + margin }; } else { return { x: center.x, y: bounds.y - margin }; } } }; // Core algorithms /** * Intelligent Waypoint Optimization * Removes redundant waypoints and optimizes path */ const optimizeWaypoints = (waypoints, sourcePoint, targetPoint) => { if (!waypoints || waypoints.length <= 1) return waypoints; const allPoints = [sourcePoint, ...waypoints, targetPoint]; const optimized = [sourcePoint]; for (let i = 1; i < allPoints.length - 1; i++) { const prev = optimized[optimized.length - 1]; const current = allPoints[i]; const next = allPoints[i + 1]; // Check if current point is necessary for orthogonality const needsPoint = !isRedundantPoint(prev, current, next); if (needsPoint) { optimized.push(current); } } optimized.push(targetPoint); return optimized.slice(1, -1); // Return only waypoints }; const isRedundantPoint = (p1, p2, p3) => { const threshold = 5; // Check if points are collinear (same line) const isHorizontalLine = Math.abs(p1.y - p2.y) < threshold && Math.abs(p2.y - p3.y) < threshold; const isVerticalLine = Math.abs(p1.x - p2.x) < threshold && Math.abs(p2.x - p3.x) < threshold; return isHorizontalLine || isVerticalLine; }; /** * Draw.io-style Segment Intersection Detection & Merging * Detects overlapping segments and creates shared waypoints */ const detectSegmentIntersections = (edges, nodes) => { const intersections = new Map(); edges.forEach((edge, edgeIndex) => { const segments = getEdgeSegments(edge, nodes); segments.forEach((segment, segmentIndex) => { const segmentId = \`\${edge.id}-\${segmentIndex}\`; segment.edgeId = edge.id; segment.segmentIndex = segmentIndex; segment.mark = 1 << segmentIndex; // Check intersections with other segments (draw.io style) edges.forEach((otherEdge, otherEdgeIndex) => { if (edgeIndex >= otherEdgeIndex) return; const otherSegments = getEdgeSegments(otherEdge, nodes); otherSegments.forEach((otherSegment, otherSegmentIndex) => { const intersection = findSegmentIntersection(segment, otherSegment); if (intersection) { const key = \`\${intersection.x},\${intersection.y}\`; if (!intersections.has(key)) { intersections.set(key, { point: intersection, segments: [] }); } intersections.get(key).segments.push({ edgeId: edge.id, segmentIndex, mark: segment.mark }); intersections.get(key).segments.push({ edgeId: otherEdge.id, segmentIndex: otherSegmentIndex, mark: 1 << otherSegmentIndex }); } }); }); }); }); return intersections; }; /** * Enhanced intersection detection with draw.io-style overlap detection */ const detectAdvancedIntersections = (edges, nodes) => { const intersections = new Map(); edges.forEach((edge, edgeIndex) => { const segments = getEdgeSegments(edge, nodes); segments.forEach((segment, segmentIndex) => { // Check for overlapping segments (draw.io style) edges.forEach((otherEdge, otherEdgeIndex) => { if (edgeIndex >= otherEdgeIndex) return; const otherSegments = getEdgeSegments(otherEdge, nodes); otherSegments.forEach((otherSegment, otherSegmentIndex) => { const intersection = findSegmentOverlap(segment, otherSegment); if (intersection) { // Merge overlapping segments for cleaner routing mergeOverlappingSegments(segment, otherSegment, intersection); } }); }); }); }); return intersections; }; const findSegmentOverlap = (seg1, seg2) => { // Check if segments overlap significantly const tolerance = 10; if (seg1.isHorizontal && seg2.isHorizontal) { // Both horizontal - check for vertical overlap if (Math.abs(seg1.start.y - seg2.start.y) < tolerance) { const overlap = getHorizontalOverlap(seg1, seg2); if (overlap) { return { x: overlap.x, y: seg1.start.y, type: 'horizontal' }; } } } else if (seg1.isVertical && seg2.isVertical) { // Both vertical - check for horizontal overlap if (Math.abs(seg1.start.x - seg2.start.x) < tolerance) { const overlap = getVerticalOverlap(seg1, seg2); if (overlap) { return { x: seg1.start.x, y: overlap.y, type: 'vertical' }; } } } return null; }; const getHorizontalOverlap = (seg1, seg2) => { const start1 = Math.min(seg1.start.x, seg1.end.x); const end1 = Math.max(seg1.start.x, seg1.end.x); const start2 = Math.min(seg2.start.x, seg2.end.x); const end2 = Math.max(seg2.start.x, seg2.end.x); const overlapStart = Math.max(start1, start2); const overlapEnd = Math.min(end1, end2); if (overlapStart < overlapEnd) { return { x: (overlapStart + overlapEnd) / 2 }; } return null; }; const getVerticalOverlap = (seg1, seg2) => { const start1 = Math.min(seg1.start.y, seg1.end.y); const end1 = Math.max(seg1.start.y, seg1.end.y); const start2 = Math.min(seg2.start.y, seg2.end.y); const end2 = Math.max(seg2.start.y, seg2.end.y); const overlapStart = Math.max(start1, start2); const overlapEnd = Math.min(end1, end2); if (overlapStart < overlapEnd) { return { y: (overlapStart + overlapEnd) / 2 }; } return null; }; const mergeOverlappingSegments = (seg1, seg2, intersection) => { // Mark segments for merging seg1.mergePoint = intersection; seg2.mergePoint = intersection; }; const getEdgeSegments = (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 sourcePoint = getConnectionPoint(sourceNode, edge.sourceHandle); const targetPoint = getConnectionPoint(targetNode, edge.targetHandle); const waypoints = edge.data?.waypoints || []; const allPoints = [sourcePoint, ...waypoints, targetPoint]; const segments = []; for (let i = 0; i < allPoints.length - 1; i++) { segments.push({ start: allPoints[i], end: allPoints[i + 1], isHorizontal: Math.abs(allPoints[i].y - allPoints[i + 1].y) < 5, isVertical: Math.abs(allPoints[i].x - allPoints[i + 1].x) < 5 }); } return segments; }; const findSegmentIntersection = (seg1, seg2) => { // Check if one segment is horizontal and the other is vertical if (seg1.isHorizontal && seg2.isVertical) { const y = seg1.start.y; const x = seg2.start.x; // Check if intersection point is within both segments const withinSeg1 = x >= Math.min(seg1.start.x, seg1.end.x) && x <= Math.max(seg1.start.x, seg1.end.x); const withinSeg2 = y >= Math.min(seg2.start.y, seg2.end.y) && y <= Math.max(seg2.start.y, seg2.end.y); if (withinSeg1 && withinSeg2) { return { x, y }; } } if (seg1.isVertical && seg2.isHorizontal) { const x = seg1.start.x; const y = seg2.start.y; const withinSeg1 = y >= Math.min(seg1.start.y, seg1.end.y) && y <= Math.max(seg1.start.y, seg1.end.y); const withinSeg2 = x >= Math.min(seg2.start.x, seg2.end.x) && x <= Math.max(seg2.start.x, seg2.end.x); if (withinSeg1 && withinSeg2) { return { x, y }; } } return null; }; /** * Smart Pathfinding with Obstacle Avoidance * Calculates optimal orthogonal path avoiding node obstacles */ const calculateSmartPath = (sourcePoint, targetPoint, nodes, edge) => { const obstacles = nodes.filter(n => n.id !== edge.source && n.id !== edge.target) .map(getNodeBounds); // Use A* pathfinding for optimal routing const path = aStarPathfinding(sourcePoint, targetPoint, obstacles); if (path && path.length > 2) { return path.slice(1, -1); // Return only waypoints } // Fallback to simple orthogonal routing return calculateSimpleOrthogonalPath(sourcePoint, targetPoint); }; const aStarPathfinding = (start, end, obstacles) => { // Enhanced A* implementation for orthogonal pathfinding const GRID_SIZE = 20; const startGrid = { x: Math.floor(start.x / GRID_SIZE), y: Math.floor(start.y / GRID_SIZE) }; const endGrid = { x: Math.floor(end.x / GRID_SIZE), y: Math.floor(end.y / GRID_SIZE) }; const openSet = [{ ...startGrid, f: 0, g: 0, h: manhattanDistance(startGrid, endGrid), parent: null }]; const closedSet = new Set(); const visited = new Map(); while (openSet.length > 0) { // Find node with lowest f score openSet.sort((a, b) => a.f - b.f); const current = openSet.shift(); const currentKey = \`\${current.x},\${current.y}\`; if (current.x === endGrid.x && current.y === endGrid.y) { // Reconstruct path const path = []; let node = current; while (node) { path.unshift({ x: node.x * GRID_SIZE, y: node.y * GRID_SIZE }); node = node.parent; } return path; } closedSet.add(currentKey); // Check neighbors (4-directional for orthogonal routing) const neighbors = [ { x: current.x + 1, y: current.y }, { x: current.x - 1, y: current.y }, { x: current.x, y: current.y + 1 }, { x: current.x, y: current.y - 1 } ]; for (const neighbor of neighbors) { const neighborKey = \`\${neighbor.x},\${neighbor.y}\`; if (closedSet.has(neighborKey)) continue; const realPoint = { x: neighbor.x * GRID_SIZE, y: neighbor.y * GRID_SIZE }; if (isPointInObstacle(realPoint, obstacles)) continue; const g = current.g + 1; const h = manhattanDistance(neighbor, endGrid); const f = g + h; const existingNode = visited.get(neighborKey); if (!existingNode || g < existingNode.g) { const neighborNode = { ...neighbor, f, g, h, parent: current }; visited.set(neighborKey, neighborNode); openSet.push(neighborNode); } } } return null; // No path found }; const manhattanDistance = (a, b) => Math.abs(a.x - b.x) + Math.abs(a.y - b.y); const isPointInObstacle = (point, obstacles) => { const MARGIN = 10; return obstacles.some(obstacle => point.x >= obstacle.x - MARGIN && point.x <= obstacle.right + MARGIN && point.y >= obstacle.y - MARGIN && point.y <= obstacle.bottom + MARGIN ); }; const calculateSimpleOrthogonalPath = (sourcePoint, targetPoint) => { const dx = targetPoint.x - sourcePoint.x; const dy = targetPoint.y - sourcePoint.y; if (Math.abs(dx) > Math.abs(dy)) { // Horizontal first const midX = sourcePoint.x + dx / 2; return [ { x: midX, y: sourcePoint.y }, { x: midX, y: targetPoint.y } ]; } else { // Vertical first const midY = sourcePoint.y + dy / 2; return [ { x: sourcePoint.x, y: midY }, { x: targetPoint.x, y: midY } ]; } }; /** * Virtual Bend Points Calculation * Calculates positions for virtual bend points that can be clicked to add waypoints */ const calculateVirtualBends = (edge, nodes) => { const segments = getEdgeSegments(edge, nodes); const virtualBends = []; segments.forEach((segment, index) => { const midPoint = { x: (segment.start.x + segment.end.x) / 2, y: (segment.start.y + segment.end.y) / 2, segmentIndex: index, isVirtual: true }; virtualBends.push(midPoint); }); return virtualBends; }; // Main processing function const processEdge = (edgeData) => { const startTime = performance.now(); try { const { edge, nodes, operation } = edgeData; // Check cache const cacheKey = createCacheKey(edge, nodes); if (workerState.cache.has(cacheKey)) { workerState.performanceMetrics.cacheHits++; return workerState.cache.get(cacheKey); } let result = {}; switch (operation) { case 'optimizeWaypoints': { const sourceNode = nodes.find(n => n.id === edge.source); const targetNode = nodes.find(n => n.id === edge.target); const sourcePoint = getConnectionPoint(sourceNode, edge.sourceHandle); const targetPoint = getConnectionPoint(targetNode, edge.targetHandle); result.waypoints = optimizeWaypoints(edge.data?.waypoints, sourcePoint, targetPoint); break; } case 'calculatePath': { const sourceNode = nodes.find(n => n.id === edge.source); const targetNode = nodes.find(n => n.id === edge.target); const sourcePoint = getConnectionPoint(sourceNode, edge.sourceHandle); const targetPoint = getConnectionPoint(targetNode, edge.targetHandle); result.waypoints = calculateSmartPath(sourcePoint, targetPoint, nodes, edge); break; } case 'calculateVirtualBends': { result.virtualBends = calculateVirtualBends(edge, nodes); break; } case 'detectIntersections': { const intersections = detectSegmentIntersections([edge], nodes); result.intersections = Array.from(intersections.values()); break; } case 'detectAdvancedIntersections': { const intersections = detectAdvancedIntersections([edge], nodes); result.intersections = Array.from(intersections.values()); break; } default: throw new Error(\`Unknown operation: \${operation}\`); } // Cache result workerState.cache.set(cacheKey, result); // Clean cache if it gets too large if (workerState.cache.size > 1000) { const oldestKeys = Array.from(workerState.cache.keys()).slice(0, 200); oldestKeys.forEach(key => workerState.cache.delete(key)); } const endTime = performance.now(); workerState.performanceMetrics.totalProcessingTime += (endTime - startTime); workerState.performanceMetrics.operationsCount++; return result; } catch (error) { return { error: error.message }; } }; // Batch processing for multiple edges const processBatchEdges = (batchData) => { const { edges, nodes } = batchData; const results = []; // Use advanced intersection detection for batch processing const intersections = detectAdvancedIntersections(edges, nodes); for (const edge of edges) { const result = processEdge({ edge, nodes, operation: 'optimizeWaypoints' }); result.intersections = Array.from(intersections.values()).filter(intersection => intersection.segments.some(seg => seg.edgeId === edge.id) ); results.push({ edgeId: edge.id, ...result }); } return results; }; // Worker message handler self.addEventListener('message', (event) => { const { type, data, taskId } = event.data; workerState.isProcessing = true; try { let result; switch (type) { case 'PROCESS_EDGE': result = processEdge(data); break; case 'PROCESS_BATCH': result = processBatchEdges(data); break; case 'GET_PERFORMANCE_METRICS': result = { ...workerState.performanceMetrics }; break; case 'CLEAR_CACHE': workerState.cache.clear(); result = { success: true }; break; default: throw new Error(\`Unknown message type: \${type}\`); } self.postMessage({ type: 'SUCCESS', taskId, result }); } catch (error) { self.postMessage({ type: 'ERROR', taskId, error: error.message }); } finally { workerState.isProcessing = false; } }); // Worker initialization self.postMessage({ type: 'WORKER_READY', message: 'Orthogonal Edge Worker initialized successfully' }); `; class InlineEdgeWorkerService { constructor(options = {}) { this.worker = null; this.taskQueue = new Map(); this.isInitialized = false; this.taskIdCounter = 0; this.hasWarnedAboutWorker = false; this.useWorker = options.useWorker !== false; this.performanceMetrics = { totalTasks: 0, avgProcessingTime: 0, errorCount: 0, cacheHitRate: 0 }; // Batch processing optimization this.batchQueue = []; this.batchTimeout = null; this.batchSize = options.batchSize || 10; this.batchDelay = options.batchDelay || 50; // Enhanced caching this.resultCache = new Map(); this.cacheMaxSize = options.cacheMaxSize || 1000; this.cacheExpiry = options.cacheExpiry || 30000; // 30 seconds // Only try to initialize worker if enabled if (this.useWorker) { this.initWorker(); } else { console.log('🔧 InlineEdgeWorkerService: Web Worker disabled - using synchronous fallback processing'); } } /** * Create worker from inline code */ createInlineWorker() { try { // Create a blob URL from the worker code const blob = new Blob([WORKER_CODE], { type: 'application/javascript' }); const workerUrl = URL.createObjectURL(blob); // Create the worker const worker = new Worker(workerUrl); // Clean up the blob URL after worker creation setTimeout(() => URL.revokeObjectURL(workerUrl), 1000); return worker; } catch (error) { console.error('❌ InlineEdgeWorkerService: Failed to create inline worker:', error); return null; } } /** * Initialize the Web Worker */ async initWorker() { try { console.log('🔧 InlineEdgeWorkerService: Creating inline worker...'); this.worker = this.createInlineWorker(); if (!this.worker) { throw new Error('Failed to create worker'); } this.worker.addEventListener('message', this.handleWorkerMessage.bind(this)); this.worker.addEventListener('error', this.handleWorkerError.bind(this)); // Wait for worker to be ready const isReady = await new Promise((resolve, reject) => { const timeout = setTimeout(() => { console.warn('⚠️ InlineEdgeWorkerService: Worker timeout'); reject(new Error('Worker initialization timeout')); }, 3000); const handleReady = event => { if (event.data.type === 'WORKER_READY') { clearTimeout(timeout); this.worker.removeEventListener('message', handleReady); this.isInitialized = true; console.log('🚀 InlineEdgeWorkerService: Worker initialized successfully'); resolve(true); } }; this.worker.addEventListener('message', handleReady); }); if (isReady) { return; // Successfully initialized } } catch (error) { console.warn('⚠️ InlineEdgeWorkerService: Failed to initialize worker:', error.message); this.isInitialized = false; } } /** * Handle messages from the Web Worker */ handleWorkerMessage(event) { const { type, taskId, result, error } = event.data; if (type === 'WORKER_READY') return; const task = this.taskQueue.get(taskId); if (!task) return; this.taskQueue.delete(taskId); if (type === 'SUCCESS') { task.resolve(result); this.updatePerformanceMetrics(task.startTime, false); } else if (type === 'ERROR') { task.reject(new Error(error)); this.updatePerformanceMetrics(task.startTime, true); } } /** * Handle worker errors */ handleWorkerError(error) { console.error('❌ InlineEdgeWorkerService: Worker error:', error); // Reject all pending tasks for (const [taskId, task] of this.taskQueue) { task.reject(new Error(`Worker error occurred: ${error.message}`)); } this.taskQueue.clear(); // Attempt to restart worker this.restartWorker(); } /** * Restart the Web Worker */ async restartWorker() { if (this.worker) { this.worker.terminate(); this.worker = null; } this.isInitialized = false; // Don't restart if we've already failed - prevents infinite restart loops if (this.hasWarnedAboutWorker) { console.log('⚠️ InlineEdgeWorkerService: Worker restart skipped - already using fallback processing'); return; } await this.initWorker(); } /** * Send a task to the Web Worker */ async sendTask(type, data) { if (!this.isInitialized || !this.worker) { // Only warn once to avoid spam if (!this.hasWarnedAboutWorker) { console.warn('⚠️ InlineEdgeWorkerService: Worker not available - using fallback processing for all operations'); this.hasWarnedAboutWorker = true; } return null; } const taskId = ++this.taskIdCounter; const startTime = performance.now(); return new Promise((resolve, reject) => { this.taskQueue.set(taskId, { resolve, reject, startTime }); this.worker.postMessage({ type, data, taskId }); // Timeout after 10 seconds setTimeout(() => { if (this.taskQueue.has(taskId)) { this.taskQueue.delete(taskId); reject(new Error('Task timeout')); } }, 10000); }); } /** * Enhanced batch processing with chunking */ async processBatchOptimized(edges, nodes, operation = 'optimizeWaypoints') { if (!this.isInitialized || !this.worker) { return edges.map(edge => ({ edgeId: edge.id, waypoints: this.fallbackOptimizeWaypoints(edge, nodes), intersections: [] })); } // Group operations by type for better batching const operations = edges.map(edge => ({ type: operation, edge, nodes })); // Process in chunks to avoid blocking const chunks = this.chunkArray(operations, this.batchSize); const results = []; for (const chunk of chunks) { try { const chunkResult = await this.sendTask('PROCESS_BATCH', { operations: chunk }); results.push(...chunkResult); // Allow UI to update between chunks await new Promise(resolve => setTimeout(resolve, 0)); } catch (error) { console.warn('⚠️ InlineEdgeWorkerService: Chunk processing failed, using fallback:', error.message); // Fallback for failed chunks chunk.forEach(op => { results.push({ edgeId: op.edge.id, waypoints: this.fallbackOptimizeWaypoints(op.edge, op.nodes), intersections: [] }); }); } } return results; } /** * Chunk array into smaller pieces */ chunkArray(array, size) { const chunks = []; for (let i = 0; i < array.length; i += size) { chunks.push(array.slice(i, i + size)); } return chunks; } /** * Enhanced caching with expiry */ getCachedResult(key) { const cached = this.resultCache.get(key); if (!cached) return null; if (Date.now() - cached.timestamp > this.cacheExpiry) { this.resultCache.delete(key); return null; } this.performanceMetrics.cacheHits++; return cached.data; } setCachedResult(key, data) { // Clean cache if it gets too large if (this.resultCache.size >= this.cacheMaxSize) { const oldestKeys = Array.from(this.resultCache.keys()).slice(0, this.cacheMaxSize / 2); oldestKeys.forEach(key => this.resultCache.delete(key)); } this.resultCache.set(key, { data, timestamp: Date.now() }); } /** * Create cache key for edge operations */ createCacheKey(edge, nodes, operation) { var _edge$data; const edgeKey = `${edge.id}-${edge.source}-${edge.target}-${JSON.stringify(((_edge$data = edge.data) === null || _edge$data === void 0 ? void 0 : _edge$data.waypoints) || [])}`; const nodePositions = nodes.map(n => `${n.id}:${n.position.x},${n.position.y}`).join('|'); return `${operation}-${edgeKey}-${nodePositions}`; } /** * Update performance metrics */ updatePerformanceMetrics(startTime, isError) { const processingTime = performance.now() - startTime; this.performanceMetrics.totalTasks++; if (isError) { this.performanceMetrics.errorCount++; } // Calculate moving average const alpha = 0.1; // Smoothing factor this.performanceMetrics.avgProcessingTime = this.performanceMetrics.avgProcessingTime * (1 - alpha) + processingTime * alpha; } // Helper method to clean data for worker communication cleanDataForWorker(data) { return JSON.parse(JSON.stringify(data, (key, value) => { // Skip functions and other non-serializable data if (typeof value === 'function') return undefined; if (value instanceof HTMLElement) return undefined; if (value instanceof Event) return undefined; return value; })); } // High-level API methods /** * Optimize waypoints for an edge with caching */ async optimizeWaypoints(edge, nodes) { const cacheKey = this.createCacheKey(edge, nodes, 'optimizeWaypoints'); const cached = this.getCachedResult(cacheKey); if (cached) return cached; try { const cleanData = this.cleanDataForWorker({ edge, nodes, operation: 'optimizeWaypoints' }); const result = await this.sendTask('PROCESS_EDGE', cleanData); // If sendTask returns null (worker not available), use fallback if (result === null) { const fallbackResult = this.fallbackOptimizeWaypoints(edge, nodes); this.setCachedResult(cacheKey, fallbackResult); return fallbackResult; } const waypoints = result.waypoints || []; this.setCachedResult(cacheKey, waypoints); return waypoints; } catch (error) { console.warn('⚠️ InlineEdgeWorkerService: Failed to optimize waypoints, using fallback:', error.message); const fallbackResult = this.fallbackOptimizeWaypoints(edge, nodes); this.setCachedResult(cacheKey, fallbackResult); return fallbackResult; } } /** * Calculate smart path for an edge with caching */ async calculateSmartPath(edge, nodes) { const cacheKey = this.createCacheKey(edge, nodes, 'calculatePath'); const cached = this.getCachedResult(cacheKey); if (cached) return cached; try { const cleanData = this.cleanDataForWorker({ edge, nodes, operation: 'calculatePath' }); const result = await this.sendTask('PROCESS_EDGE', cleanData); // If sendTask returns null (worker not available), use fallback if (result === null) { const fallbackResult = this.fallbackCalculatePath(edge, nodes); this.setCachedResult(cacheKey, fallbackResult); return fallbackResult; } const waypoints = result.waypoints || []; this.setCachedResult(cacheKey, waypoints); return waypoints; } catch (error) { console.warn('⚠️ InlineEdgeWorkerService: Failed to calculate smart path, using fallback:', error.message); const fallbackResult = this.fallbackCalculatePath(edge, nodes); this.setCachedResult(cacheKey, fallbackResult); return fallbackResult; } } /** * Calculate virtual bend points with caching */ async calculateVirtualBends(edge, nodes) { const cacheKey = this.createCacheKey(edge, nodes, 'calculateVirtualBends'); const cached = this.getCachedResult(cacheKey); if (cached) return cached; try { const cleanData = this.cleanDataForWorker({ edge, nodes, operation: 'calculateVirtualBends' }); const result = await this.sendTask('PROCESS_EDGE', cleanData); // If sendTask returns null (worker not available), use fallback if (result === null) { const fallbackResult = []; this.setCachedResult(cacheKey, fallbackResult); return fallbackResult; } const virtualBends = result.virtualBends || []; this.setCachedResult(cacheKey, virtualBends); return virtualBends; } catch (error) { console.warn('⚠️ InlineEdgeWorkerService: Failed to calculate virtual bends, using fallback:', error.message); const fallbackResult = []; this.setCachedResult(cacheKey, fallbackResult); return fallbackResult; } } /** * Detect intersections with other edges with caching */ async detectIntersections(edge, nodes) { const cacheKey = this.createCacheKey(edge, nodes, 'detectIntersections'); const cached = this.getCachedResult(cacheKey); if (cached) return cached; try { const cleanData = this.cleanDataForWorker({ edge, nodes, operation: 'detectIntersections' }); const result = await this.sendTask('PROCESS_EDGE', cleanData); // If sendTask returns null (worker not available), use fallback if (result === null) { const fallbackResult = []; this.setCachedResult(cacheKey, fallbackResult); return fallbackResult; } const intersections = result.intersections || []; this.setCachedResult(cacheKey, intersections); return intersections; } catch (error) { console.warn('⚠️ InlineEdgeWorkerService: Failed to detect intersections, using fallback:', error.message); const fallbackResult = []; this.setCachedResult(cacheKey, fallbackResult); return fallbackResult; } } /** * Process multiple edges in batch with optimized processing */ async processBatch(edges, nodes) { return this.processBatchOptimized(edges, nodes, 'optimizeWaypoints'); } /** * Get performance metrics */ async getPerformanceMetrics() { try { const workerMetrics = await this.sendTask('GET_PERFORMANCE_METRICS'); return { ...this.performanceMetrics, worker: workerMetrics, cacheSize: this.resultCache.size, cacheHitRate: this.performanceMetrics.cacheHits / Math.max(this.performanceMetrics.totalTasks, 1) }; } catch (error) { return { ...this.performanceMetrics, cacheSize: this.resultCache.size, cacheHitRate: this.performanceMetrics.cacheHits / Math.max(this.performanceMetrics.totalTasks, 1) }; } } /** * Clear worker cache */ async clearCache() { try { await this.sendTask('CLEAR_CACHE'); this.resultCache.clear(); return true; } catch (error) { console.error('❌ InlineEdgeWorkerService: Failed to clear cache:', error); this.resultCache.clear(); return false; } } // Fallback methods (runs on main thread) fallbackOptimizeWaypoints(edge, nodes) { var _edge$data2; const waypoints = ((_edge$data2 = edge.data) === null || _edge$data2 === void 0 ? void 0 : _edge$data2.waypoints) || []; if (waypoints.length <= 1) return waypoints; // Simple redundant point removal const optimized = []; for (let i = 0; i < waypoints.length; i++) { const prev = i > 0 ? waypoints[i - 1] : null; const current = waypoints[i]; const next = i < waypoints.length - 1 ? waypoints[i + 1] : null; if (!prev || !next) { optimized.push(current); continue; } // Check if point is necessary const isHorizontalLine = Math.abs(prev.y - current.y) < 5 && Math.abs(current.y - next.y) < 5; const isVerticalLine = Math.abs(prev.x - current.x) < 5 && Math.abs(current.x - next.x) < 5; if (!isHorizontalLine && !isVerticalLine) { optimized.push(current); } } return optimized; } fallbackCalculatePath(edge, nodes) { const sourceNode = nodes.find(n => n.id === edge.source); const targetNode = nodes.find(n => n.id === edge.target); if (!sourceNode || !targetNode) return []; // Simple orthogonal path const sourcePoint = { x: sourceNode.position.x + (sourceNode.width || 150) / 2, y: sourceNode.position.y + (sourceNode.height || 60) / 2 }; const targetPoint = { x: targetNode.position.x + (targetNode.width || 150) / 2, y: targetNode.position.y + (targetNode.height || 60) / 2 }; const dx = targetPoint.x - sourcePoint.x; const dy = targetPoint.y - sourcePoint.y; if (Math.abs(dx) > Math.abs(dy)) { const midX = sourcePoint.x + dx / 2; return [{ x: midX, y: sourcePoint.y }, { x: midX, y: targetPoint.y }]; } else { const midY = sourcePoint.y + dy / 2; return [{ x: sourcePoint.x, y: midY }, { x: targetPoint.x, y: midY }]; } } /** * Cleanup resources */ destroy() { if (this.worker) { this.worker.terminate(); this.worker = null; } this.taskQueue.clear(); this.resultCache.clear(); this.batchQueue = []; if (this.batchTimeout) { clearTimeout(this.batchTimeout); } this.isInitialized = false; } } // Create singleton instance const shouldUseWorker = typeof window !== 'undefined' ? window.__GRAPHING_USE_WORKER__ !== false : true; const inlineEdgeWorkerService = new InlineEdgeWorkerService({ useWorker: shouldUseWorker }); var _default = exports.default = inlineEdgeWorkerService;