using System.Collections.Concurrent; using System.Diagnostics; using System.Globalization; namespace StellaOps.ElkSharp; internal static partial class ElkEdgeRouterIterative { private static List? TryBuildLocalObstacleSkirtPath( ElkPoint start, ElkPoint end, IReadOnlyCollection nodes, string sourceId, string targetId, ElkPositionedNode? targetNode, double obstaclePadding) { var obstacles = nodes.Select(node => ( Left: node.X - obstaclePadding, Top: node.Y - obstaclePadding, Right: node.X + node.Width + obstaclePadding, Bottom: node.Y + node.Height + obstaclePadding, Id: node.Id)).ToArray(); List? bestPath = null; var bestScore = double.MaxValue; bool SegmentIsClear(ElkPoint from, ElkPoint to) => !ElkEdgePostProcessor.SegmentCrossesObstacle(from, to, obstacles, sourceId, targetId); void ConsiderCandidate(IReadOnlyList rawCandidate) { var candidate = NormalizePolyline(rawCandidate); if (candidate.Count < 2) { return; } for (var i = 1; i < candidate.Count; i++) { if (!SegmentIsClear(candidate[i - 1], candidate[i])) { return; } } if (targetNode is not null && !ElkShapeBoundaries.IsGatewayShape(targetNode) && HasTargetApproachBacktracking(candidate, targetNode)) { return; } var score = ComputePolylineLength(candidate) + (Math.Max(0, candidate.Count - 2) * 4d); if (score >= bestScore - 0.5d) { return; } bestScore = score; bestPath = candidate; } var horizontalDominant = Math.Abs(end.X - start.X) >= Math.Abs(end.Y - start.Y); if (horizontalDominant) { var targetBridgeX = end.X; if (targetNode is not null && ElkShapeBoundaries.IsGatewayShape(targetNode)) { targetBridgeX = ResolveGatewayRoutingApproachPoint(targetNode, start, end).X; } var minX = Math.Min(start.X, end.X) + 0.5d; var maxX = Math.Max(start.X, end.X) - 0.5d; var corridorTop = Math.Min(start.Y, end.Y) - obstaclePadding; var corridorBottom = Math.Max(start.Y, end.Y) + obstaclePadding; var bypassYCandidates = new List { start.Y, end.Y }; foreach (var obstacle in obstacles) { if (string.Equals(obstacle.Id, sourceId, StringComparison.Ordinal) || string.Equals(obstacle.Id, targetId, StringComparison.Ordinal) || obstacle.Right <= minX || obstacle.Left >= maxX || obstacle.Bottom <= corridorTop || obstacle.Top >= corridorBottom) { continue; } AddUniqueCoordinate(bypassYCandidates, obstacle.Top); AddUniqueCoordinate(bypassYCandidates, obstacle.Bottom); } foreach (var bypassY in bypassYCandidates) { ConsiderCandidate( [ start, new ElkPoint { X = start.X, Y = bypassY }, new ElkPoint { X = targetBridgeX, Y = bypassY }, end, ]); } } else { var targetBridgeY = end.Y; if (targetNode is not null && ElkShapeBoundaries.IsGatewayShape(targetNode)) { targetBridgeY = ResolveGatewayRoutingApproachPoint(targetNode, start, end).Y; } var minY = Math.Min(start.Y, end.Y) + 0.5d; var maxY = Math.Max(start.Y, end.Y) - 0.5d; var corridorLeft = Math.Min(start.X, end.X) - obstaclePadding; var corridorRight = Math.Max(start.X, end.X) + obstaclePadding; var bypassXCandidates = new List { start.X, end.X }; foreach (var obstacle in obstacles) { if (string.Equals(obstacle.Id, sourceId, StringComparison.Ordinal) || string.Equals(obstacle.Id, targetId, StringComparison.Ordinal) || obstacle.Bottom <= minY || obstacle.Top >= maxY || obstacle.Right <= corridorLeft || obstacle.Left >= corridorRight) { continue; } AddUniqueCoordinate(bypassXCandidates, obstacle.Left); AddUniqueCoordinate(bypassXCandidates, obstacle.Right); } foreach (var bypassX in bypassXCandidates) { ConsiderCandidate( [ start, new ElkPoint { X = bypassX, Y = start.Y }, new ElkPoint { X = bypassX, Y = targetBridgeY }, end, ]); } } return bestPath; } private static double ComputePolylineLength(IReadOnlyList points) { var length = 0d; for (var i = 1; i < points.Count; i++) { length += ElkEdgeRoutingGeometry.ComputeSegmentLength(points[i - 1], points[i]); } return length; } private static double ResolveMinLineClearance(IReadOnlyCollection nodes) { var overrideClearance = ElkLayoutClearance.Current; if (overrideClearance > 0d) { return overrideClearance; } var serviceNodes = nodes.Where(node => node.Kind is not "Start" and not "End").ToArray(); return serviceNodes.Length > 0 ? Math.Min(serviceNodes.Average(node => node.Width), serviceNodes.Average(node => node.Height)) / 2d : 50d; } }