@ichigo_san/graphing
Version:
A lightweight UML-style diagram editor built with React Flow and Tailwind CSS
347 lines (326 loc) • 11.2 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.calculateRouteDistance = calculateRouteDistance;
exports.createRoutingContext = createRoutingContext;
exports.generateRoutingMetadata = generateRoutingMetadata;
exports.hasNodesMovedSignificantly = hasNodesMovedSignificantly;
exports.optimizeRoute = optimizeRoute;
exports.routeOrthogonalEdge = routeOrthogonalEdge;
exports.updateRoute = updateRoute;
exports.validateRoutingInputs = validateRoutingInputs;
var _routingAlgorithms = require("./routingAlgorithms.js");
var _coordinateTransforms = require("./coordinateTransforms.js");
var _gridSnapping = require("./gridSnapping.js");
/**
* @typedef {Object} RoutingPipelineContext
* @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
* @property {Array<Object>} obstacles - Array of obstacle nodes
* @property {boolean} avoidObstacles - Whether to avoid obstacles
*/
/**
* @typedef {Object} RoutingPipelineResult
* @property {Array<{x: number, y: number}>} modelPoints - Route points in model coordinates
* @property {Array<{x: number, y: number}>} viewPoints - Route points in view coordinates
* @property {Array<string>} sides - Array of sides for each point
* @property {string} pattern - Pattern name used
* @property {boolean} success - Whether routing was successful
* @property {Array<Object>} metadata - Additional routing metadata
*/
/**
* Complete routing pipeline for orthogonal edges
* @param {RoutingPipelineContext} context - Routing context
* @returns {RoutingPipelineResult} Routing result
*/
function routeOrthogonalEdge(context) {
const {
sourceNode,
targetNode,
sourcePort,
targetPort,
transform,
gridConfig,
obstacles = [],
avoidObstacles = true
} = context;
try {
// Step 1: Validate inputs
const validation = validateRoutingInputs(context);
if (!validation.valid) {
return {
modelPoints: [],
viewPoints: [],
sides: [],
pattern: null,
success: false,
metadata: {
error: validation.error
}
};
}
// Step 2: Calculate route in model coordinates
const routingContext = {
sourceNode,
targetNode,
sourcePort,
targetPort,
transform,
gridConfig
};
let routeResult;
if (avoidObstacles && obstacles.length > 0) {
routeResult = (0, _routingAlgorithms.calculateRouteWithObstacles)(routingContext, obstacles);
} else {
routeResult = (0, _routingAlgorithms.calculateOrthogonalRoute)(routingContext);
}
if (!routeResult.success) {
return {
modelPoints: [],
viewPoints: [],
sides: [],
pattern: routeResult.pattern,
success: false,
metadata: {
error: 'Routing calculation failed'
}
};
}
// Step 3: Apply grid snapping to model points
const snappedModelPoints = routeResult.points.map(point => gridConfig !== null && gridConfig !== void 0 && gridConfig.enabled ? (0, _gridSnapping.snapPoint)(point, gridConfig) : point);
// Step 4: Transform model points to view coordinates
const viewPoints = snappedModelPoints.map(point => (0, _coordinateTransforms.modelToView)(point, transform));
// Step 5: Generate metadata
const metadata = generateRoutingMetadata(context, routeResult, snappedModelPoints);
return {
modelPoints: snappedModelPoints,
viewPoints,
sides: routeResult.sides,
pattern: routeResult.pattern,
success: true,
metadata
};
} catch (error) {
return {
modelPoints: [],
viewPoints: [],
sides: [],
pattern: null,
success: false,
metadata: {
error: error.message
}
};
}
}
/**
* Validate routing pipeline inputs
* @param {RoutingPipelineContext} context - Routing context
* @returns {Object} Validation result {valid: boolean, error?: string}
*/
function validateRoutingInputs(context) {
const {
sourceNode,
targetNode,
transform
} = context;
if (!sourceNode || !targetNode) {
return {
valid: false,
error: 'Source and target nodes are required'
};
}
if (!sourceNode.position || !sourceNode.position.x || !sourceNode.position.y || !sourceNode.width || !sourceNode.height) {
return {
valid: false,
error: 'Source node must have valid position and dimensions'
};
}
if (!targetNode.position || !targetNode.position.x || !targetNode.position.y || !targetNode.width || !targetNode.height) {
return {
valid: false,
error: 'Target node must have valid position and dimensions'
};
}
if (!transform || typeof transform.x !== 'number' || typeof transform.y !== 'number' || typeof transform.k !== 'number') {
return {
valid: false,
error: 'Valid transform is required'
};
}
// Check if nodes are the same
if (sourceNode === targetNode) {
return {
valid: false,
error: 'Source and target nodes cannot be the same'
};
}
return {
valid: true
};
}
/**
* Generate routing metadata
* @param {RoutingPipelineContext} context - Routing context
* @param {Object} routeResult - Route calculation result
* @param {Array<Object>} modelPoints - Model points
* @returns {Object} Metadata object
*/
function generateRoutingMetadata(context, routeResult, modelPoints) {
const {
sourceNode,
targetNode,
obstacles,
avoidObstacles
} = context;
// Calculate route statistics
const totalDistance = calculateRouteDistance(modelPoints);
const segmentCount = Math.max(0, modelPoints.length - 1);
// Calculate bounding box
const minX = Math.min(...modelPoints.map(p => p.x));
const maxX = Math.max(...modelPoints.map(p => p.x));
const minY = Math.min(...modelPoints.map(p => p.y));
const maxY = Math.max(...modelPoints.map(p => p.y));
return {
totalDistance,
segmentCount,
boundingBox: {
minX,
maxX,
minY,
maxY
},
pattern: routeResult.pattern,
obstacleCount: (obstacles === null || obstacles === void 0 ? void 0 : obstacles.length) || 0,
obstacleAvoidance: avoidObstacles,
timestamp: Date.now()
};
}
/**
* Calculate total distance of a route
* @param {Array<Object>} points - Route points
* @returns {number} Total distance
*/
function calculateRouteDistance(points) {
if (points.length < 2) {
return 0;
}
let totalDistance = 0;
for (let i = 0; i < points.length - 1; i++) {
const dx = points[i + 1].x - points[i].x;
const dy = points[i + 1].y - points[i].y;
totalDistance += Math.sqrt(dx * dx + dy * dy);
}
return totalDistance;
}
/**
* Update route when nodes move
* @param {RoutingPipelineResult} originalRoute - Original route result
* @param {Object} sourceNode - Updated source node
* @param {Object} targetNode - Updated target node
* @param {RoutingPipelineContext} context - Updated routing context
* @returns {RoutingPipelineResult} Updated route result
*/
function updateRoute(originalRoute, sourceNode, targetNode, context) {
// If nodes haven't moved significantly, return original route
if (!hasNodesMovedSignificantly(originalRoute, sourceNode, targetNode, context)) {
return originalRoute;
}
// Recalculate route with updated nodes
const updatedContext = {
...context,
sourceNode,
targetNode
};
return routeOrthogonalEdge(updatedContext);
}
/**
* Check if nodes have moved significantly enough to warrant route recalculation
* @param {RoutingPipelineResult} route - Current route
* @param {Object} sourceNode - Current source node
* @param {Object} targetNode - Current target node
* @param {RoutingPipelineContext} context - Routing context
* @returns {boolean} True if nodes have moved significantly
*/
function hasNodesMovedSignificantly(route, sourceNode, targetNode, context) {
var _context$gridConfig;
if (!route.success || route.modelPoints.length === 0) {
return true;
}
const tolerance = ((_context$gridConfig = context.gridConfig) === null || _context$gridConfig === void 0 ? void 0 : _context$gridConfig.tolerance) || 5;
// Check if source or target anchor points have moved significantly
const sourceAnchor = route.modelPoints[0];
const targetAnchor = route.modelPoints[route.modelPoints.length - 1];
const sourceMoved = Math.abs(sourceAnchor.x - sourceNode.position.x) > tolerance || Math.abs(sourceAnchor.y - sourceNode.position.y) > tolerance;
const targetMoved = Math.abs(targetAnchor.x - targetNode.position.x) > tolerance || Math.abs(targetAnchor.y - targetNode.position.y) > tolerance;
return sourceMoved || targetMoved;
}
/**
* Optimize route for better visual appearance
* @param {RoutingPipelineResult} route - Route to optimize
* @param {RoutingPipelineContext} context - Routing context
* @returns {RoutingPipelineResult} Optimized route
*/
function optimizeRoute(route, context) {
if (!route.success || route.modelPoints.length < 3) {
return route;
}
const {
gridConfig
} = context;
const optimizedPoints = [...route.modelPoints];
// Remove redundant points (points that are too close together)
const minDistance = (gridConfig === null || gridConfig === void 0 ? void 0 : gridConfig.size) || 20;
for (let i = optimizedPoints.length - 2; i > 0; i--) {
const prev = optimizedPoints[i - 1];
const curr = optimizedPoints[i];
const next = optimizedPoints[i + 1];
const dist1 = Math.sqrt((curr.x - prev.x) ** 2 + (curr.y - prev.y) ** 2);
const dist2 = Math.sqrt((next.x - curr.x) ** 2 + (next.y - curr.y) ** 2);
if (dist1 < minDistance && dist2 < minDistance) {
optimizedPoints.splice(i, 1);
}
}
// Transform optimized points to view coordinates
const optimizedViewPoints = optimizedPoints.map(point => (0, _coordinateTransforms.modelToView)(point, context.transform));
return {
...route,
modelPoints: optimizedPoints,
viewPoints: optimizedViewPoints,
metadata: {
...route.metadata,
optimized: true,
originalPointCount: route.modelPoints.length,
optimizedPointCount: optimizedPoints.length
}
};
}
/**
* Create a routing context from basic parameters
* @param {Object} sourceNode - Source node
* @param {Object} targetNode - Target node
* @param {Object} transform - View transform
* @param {Object} options - Additional options
* @returns {RoutingPipelineContext} Routing context
*/
function createRoutingContext(sourceNode, targetNode, transform, options = {}) {
return {
sourceNode,
targetNode,
sourcePort: options.sourcePort || null,
targetPort: options.targetPort || null,
transform,
gridConfig: options.gridConfig || {
enabled: true,
size: 20,
tolerance: 5
},
obstacles: options.obstacles || [],
avoidObstacles: options.avoidObstacles !== false
};
}