UNPKG

@ichigo_san/graphing

Version:

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

433 lines (398 loc) 12.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.calculateOrthogonalRoute = calculateOrthogonalRoute; exports.calculatePatternPoint = calculatePatternPoint; exports.calculatePatternRoute = calculatePatternRoute; exports.calculateRouteWithObstacles = calculateRouteWithObstacles; exports.calculateStraightRoute = calculateStraightRoute; exports.hasCollisions = hasCollisions; exports.linesIntersect = linesIntersect; exports.segmentIntersectsRect = segmentIntersectsRect; exports.snapPointToGrid = snapPointToGrid; var _edgeTypes = require("./edgeTypes.js"); var _routePatterns = require("./routePatterns.js"); var _anchorPoint = require("./anchorPoint.js"); var _jetty = require("./jetty.js"); /** * @typedef {Object} RoutingContext * @property {Object} sourceNode - Source node * @property {Object} targetNode - Target node * @property {Object} sourcePort - Source port configuration * @property {Object} targetPort - Target port configuration * @property {Object} transform - Current view transform * @property {Object} gridConfig - Grid configuration */ /** * @typedef {Object} RoutingResult * @property {Array<{x: number, y: number}>} points - Array of route points * @property {Array<string>} sides - Array of sides for each point * @property {string} pattern - Pattern name used * @property {boolean} success - Whether routing was successful */ /** * Calculate the best route between two nodes using orthogonal routing * @param {RoutingContext} context - Routing context * @returns {RoutingResult} Routing result */ function calculateOrthogonalRoute(context) { const { sourceNode, targetNode, sourcePort, targetPort, transform, gridConfig } = context; // Get anchor points const sourceAnchor = (0, _anchorPoint.anchorPoint)(sourceNode, sourcePort, { x: targetNode.position.x, y: targetNode.position.y }); const targetAnchor = (0, _anchorPoint.anchorPoint)(targetNode, targetPort, { x: sourceNode.position.x, y: sourceNode.position.y }); if (!sourceAnchor || !targetAnchor) { return { points: [], sides: [], pattern: null, success: false }; } // Determine source and target sides const sourceSide = sourceAnchor.side; const targetSide = targetAnchor.side; if (!sourceSide || !targetSide) { return { points: [], sides: [], pattern: null, success: false }; } // Get available patterns const patterns = (0, _routePatterns.getAllPatterns)(sourceSide, targetSide); if (patterns.length === 0) { // Fallback to straight line return calculateStraightRoute(sourceAnchor.point, targetAnchor.point); } // Try L-shape patterns first, then S-shape const lPatterns = patterns.filter(_routePatterns.isLPattern); const sPatterns = patterns.filter(_routePatterns.isSPattern); // Try L-shape routing first if (lPatterns.length > 0) { const result = calculatePatternRoute(sourceAnchor, targetAnchor, lPatterns[0], context); if (result.success) { return result; } } // Try S-shape routing if (sPatterns.length > 0) { const result = calculatePatternRoute(sourceAnchor, targetAnchor, sPatterns[0], context); if (result.success) { return result; } } // Fallback to straight line return calculateStraightRoute(sourceAnchor.point, targetAnchor.point); } /** * Calculate route using a specific pattern * @param {Object} sourceAnchor - Source anchor point * @param {Object} targetAnchor - Target anchor point * @param {Object} pattern - Route pattern to use * @param {RoutingContext} context - Routing context * @returns {RoutingResult} Routing result */ function calculatePatternRoute(sourceAnchor, targetAnchor, pattern, context) { const { sourceNode, targetNode, transform, gridConfig } = context; try { // Debug: Check if nodes are defined if (!sourceNode || !targetNode) { throw new Error('Source or target node is undefined'); } // Convert relative pattern segments to absolute coordinates const points = pattern.segments.map((segment, index) => { const side = pattern.sides[index]; if (index === 0) { // First point is source anchor return sourceAnchor.point; } else if (index === pattern.segments.length - 1) { // Last point is target anchor return targetAnchor.point; } else { // Intermediate points are calculated based on pattern return calculatePatternPoint(segment, side, sourceNode, targetNode, context); } }); // Apply jetty to endpoints const jettiedPoints = (0, _jetty.jettyEdgeEndpoints)(points[0], sourceAnchor.side, points[points.length - 1], targetAnchor.side); // Update first and last points with jetty points[0] = jettiedPoints.sourcePoint; points[points.length - 1] = jettiedPoints.targetPoint; // Snap points to grid const snappedPoints = points.map(point => gridConfig !== null && gridConfig !== void 0 && gridConfig.enabled ? snapPointToGrid(point, gridConfig) : point); return { points: snappedPoints, sides: pattern.sides, pattern: pattern.name, success: true }; } catch (error) { return { points: [], sides: [], pattern: pattern.name, success: false }; } } /** * Calculate a pattern point based on relative coordinates and side * @param {Object} segment - Relative segment coordinates * @param {string} side - Side for this segment * @param {Object} sourceNode - Source node * @param {Object} targetNode - Target node * @param {RoutingContext} context - Routing context * @returns {Object} Absolute point coordinates */ function calculatePatternPoint(segment, side, sourceNode, targetNode, context) { const { transform } = context; // Calculate bounding box of both nodes const minX = Math.min(sourceNode.position.x, targetNode.position.x); const maxX = Math.max(sourceNode.position.x + sourceNode.width, targetNode.position.x + targetNode.width); const minY = Math.min(sourceNode.position.y, targetNode.position.y); const maxY = Math.max(sourceNode.position.y + sourceNode.height, targetNode.position.y + targetNode.height); // Calculate center point const centerX = (minX + maxX) / 2; const centerY = (minY + maxY) / 2; // Convert relative coordinates to absolute let x, y; if (side === _edgeTypes.SIDES.NORTH || side === _edgeTypes.SIDES.SOUTH) { // Vertical segment - x is relative to source node width x = sourceNode.position.x + segment.x * sourceNode.width; y = segment.y === 0.5 ? centerY : segment.y < 0.5 ? minY : maxY; } else { // Horizontal segment - y is relative to source node height x = segment.x === 0.5 ? centerX : segment.x < 0.5 ? minX : maxX; y = sourceNode.position.y + segment.y * sourceNode.height; } return { x, y }; } /** * Calculate a straight line route between two points * @param {Object} sourcePoint - Source point * @param {Object} targetPoint - Target point * @returns {RoutingResult} Routing result */ function calculateStraightRoute(sourcePoint, targetPoint) { return { points: [sourcePoint, targetPoint], sides: [null, null], pattern: 'straight', success: true }; } /** * Snap a point to the grid * @param {Object} point - Point to snap * @param {Object} gridConfig - Grid configuration * @returns {Object} Snapped point */ function snapPointToGrid(point, gridConfig) { const { size, tolerance } = gridConfig; const snappedX = Math.round(point.x / size) * size; const snappedY = Math.round(point.y / size) * size; // Only snap if within tolerance if (Math.abs(point.x - snappedX) <= tolerance && Math.abs(point.y - snappedY) <= tolerance) { return { x: snappedX, y: snappedY }; } return point; } /** * Calculate route with obstacle avoidance * @param {RoutingContext} context - Routing context * @param {Array<Object>} obstacles - Array of obstacle nodes * @returns {RoutingResult} Routing result */ function calculateRouteWithObstacles(context, obstacles) { // First try simple orthogonal routing const simpleRoute = calculateOrthogonalRoute(context); if (simpleRoute.success && !hasCollisions(simpleRoute.points, obstacles)) { return simpleRoute; } // If collision detected, try alternative patterns const { sourceNode, targetNode, sourcePort, targetPort, transform, gridConfig } = context; const sourceAnchor = (0, _anchorPoint.anchorPoint)(sourceNode, sourcePort, { x: targetNode.position.x, y: targetNode.position.y }); const targetAnchor = (0, _anchorPoint.anchorPoint)(targetNode, targetPort, { x: sourceNode.position.x, y: sourceNode.position.y }); if (!sourceAnchor || !targetAnchor) { return simpleRoute; } const patterns = (0, _routePatterns.getAllPatterns)(sourceAnchor.side, targetAnchor.side); // Try each pattern until we find one without collisions for (const pattern of patterns) { const route = calculatePatternRoute(sourceAnchor, targetAnchor, pattern, context); if (route.success && !hasCollisions(route.points, obstacles)) { return route; } } // If all patterns have collisions, return the best one return simpleRoute; } /** * Check if a route has collisions with obstacles * @param {Array<Object>} points - Route points * @param {Array<Object>} obstacles - Array of obstacle nodes * @returns {boolean} True if collision detected */ function hasCollisions(points, obstacles) { if (!obstacles || obstacles.length === 0) { return false; } // Check each segment of the route for (let i = 0; i < points.length - 1; i++) { const segment = { x1: points[i].x, y1: points[i].y, x2: points[i + 1].x, y2: points[i + 1].y }; for (const obstacle of obstacles) { if (segmentIntersectsRect(segment, obstacle)) { return true; } } } return false; } /** * Check if a line segment intersects with a rectangle * @param {Object} segment - Line segment {x1, y1, x2, y2} * @param {Object} rect - Rectangle {x, y, width, height} * @returns {boolean} True if intersection detected */ function segmentIntersectsRect(segment, rect) { const { x1, y1, x2, y2 } = segment; const { x, y, width, height } = rect; // Check if either endpoint is inside the rectangle if (x1 >= x && x1 <= x + width && y1 >= y && y1 <= y + height) { return true; } if (x2 >= x && x2 <= x + width && y2 >= y && y2 <= y + height) { return true; } // Check intersection with rectangle edges 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 (linesIntersect(segment, edge)) { return true; } } return false; } /** * Check if two line segments intersect * @param {Object} line1 - First line segment {x1, y1, x2, y2} * @param {Object} line2 - Second line segment {x1, y1, x2, y2} * @returns {boolean} True if lines intersect */ function linesIntersect(line1, line2) { const { x1: x1a, y1: y1a, x2: x2a, y2: y2a } = line1; const { x1: x1b, y1: y1b, x2: x2b, y2: y2b } = line2; // Calculate the direction vectors const dx1 = x2a - x1a; const dy1 = y2a - y1a; const dx2 = x2b - x1b; const dy2 = y2b - y1b; // Calculate the determinant const det = dx1 * dy2 - dy1 * dx2; if (Math.abs(det) < 1e-10) { return false; // Parallel lines } // Calculate the parameters const dx = x1b - x1a; const dy = y1b - y1a; const t1 = (dx * dy2 - dy * dx2) / det; const t2 = (dx * dy1 - dy * dx1) / det; const result = t1 >= 0 && t1 <= 1 && t2 >= 0 && t2 <= 1; return result; }