namespace StellaOps.ElkSharp; internal static partial class ElkEdgePostProcessor { /// /// Extends short final approach segments so the arrowhead has a clear /// directional run into the target node. Shifts the penultimate vertical /// bend point away from the target to create a longer horizontal approach. /// Only modifies non-gateway rectangular target approaches. /// internal static ElkRoutedEdge[] ExtendShortApproachSegments( ElkRoutedEdge[] edges, ElkPositionedNode[] nodes) { if (edges.Length == 0 || nodes.Length == 0) { return edges; } var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal); var serviceNodes = nodes.Where(node => node.Kind is not "Start" and not "End").ToArray(); var avgWidth = serviceNodes.Length > 0 ? serviceNodes.Average(node => node.Width) : 160d; var desiredMinApproach = Math.Max(48d, avgWidth / 2d); var graphMinY = nodes.Min(node => node.Y); var graphMaxY = nodes.Max(node => node.Y + node.Height); var result = edges.ToArray(); var changed = false; for (var edgeIndex = 0; edgeIndex < result.Length; edgeIndex++) { var edge = result[edgeIndex]; if (string.IsNullOrWhiteSpace(edge.TargetNodeId) || !nodesById.TryGetValue(edge.TargetNodeId!, out var targetNode) || HasCorridorBendPoints(edge, graphMinY, graphMaxY)) { continue; } var path = ExtractFullPath(edge); if (path.Count < 3) { continue; } var extended = TryExtendApproach(path, targetNode, desiredMinApproach); if (extended is null) { continue; } if (HasNodeObstacleCrossing(extended, nodes, edge.SourceNodeId, edge.TargetNodeId)) { continue; } var candidateEdge = BuildSingleSectionEdge(edge, extended); var candidateEdges = result.ToArray(); candidateEdges[edgeIndex] = candidateEdge; var oldShared = ElkEdgeRoutingScoring.CountSharedLaneViolations(result, nodes); var newShared = ElkEdgeRoutingScoring.CountSharedLaneViolations(candidateEdges, nodes); if (newShared > oldShared) { continue; } result[edgeIndex] = candidateEdge; changed = true; } return changed ? result : edges; } private static List? TryExtendApproach( IReadOnlyList path, ElkPositionedNode targetNode, double desiredMinApproach) { const double tolerance = 1d; if (path.Count < 3) { return null; } var endpoint = path[^1]; var penultimate = path[^2]; // Skip diagonal final segments (gateway tip approaches) var dx = Math.Abs(endpoint.X - penultimate.X); var dy = Math.Abs(endpoint.Y - penultimate.Y); if (dx > 3d && dy > 3d) { return null; } // Left-face horizontal approach (most common in LTR layout) if (Math.Abs(penultimate.Y - endpoint.Y) <= tolerance && endpoint.X > penultimate.X) { return TryExtendLeftFaceApproach(path, desiredMinApproach, tolerance); } // Top-face vertical approach if (Math.Abs(penultimate.X - endpoint.X) <= tolerance && endpoint.Y > penultimate.Y) { return TryExtendTopFaceApproach(path, desiredMinApproach, tolerance); } return null; } private static List? TryExtendLeftFaceApproach( IReadOnlyList path, double desiredMinApproach, double tolerance) { var endpoint = path[^1]; var penultimate = path[^2]; var currentApproach = endpoint.X - penultimate.X; if (currentApproach <= 0 || currentApproach >= desiredMinApproach - tolerance) { return null; } if (path.Count < 3) { return null; } var preBend = path[^3]; var isStandardLBend = Math.Abs(preBend.X - penultimate.X) <= tolerance; if (isStandardLBend) { // Standard case: vertical segment before horizontal approach // Shift both path[^3] and path[^2] to new X double precedingX = path.Count >= 4 ? path[^4].X : path[0].X; var maxFeasibleApproach = endpoint.X - precedingX - 1d; if (maxFeasibleApproach <= currentApproach + 2d) { return null; } var newApproach = Math.Min(desiredMinApproach, maxFeasibleApproach * 0.8d); newApproach = Math.Max(newApproach, currentApproach); if (newApproach <= currentApproach + 2d) { return null; } var newVerticalX = endpoint.X - newApproach; if (path.Count >= 4 && path[^4].X > newVerticalX + tolerance) { return null; } var extended = path .Select(point => new ElkPoint { X = point.X, Y = point.Y }) .ToList(); extended[^2] = new ElkPoint { X = newVerticalX, Y = extended[^2].Y }; extended[^3] = new ElkPoint { X = newVerticalX, Y = extended[^3].Y }; return NormalizePathPoints(extended); } // Non-standard case: path[^3] → path[^2] is a short horizontal // left-jog before the approach. Look past the jog to find the real // vertical segment and extend from there. if (Math.Abs(preBend.Y - penultimate.Y) <= tolerance && preBend.X > penultimate.X // jog goes LEFT && preBend.X - penultimate.X < 30d // short jog && path.Count >= 5 && Math.Abs(path[^4].X - preBend.X) <= tolerance) // vertical before jog { // Pattern: ...→(vertX,prevY)→(vertX,endY)→(jogX,endY)→(targetX,endY) // Collapse the jog and extend the vertical double precedingX = path.Count >= 6 ? path[^5].X : path[0].X; var maxFeasibleApproach = endpoint.X - precedingX - 1d; if (maxFeasibleApproach <= currentApproach + 2d) { return null; } var newApproach = Math.Min(desiredMinApproach, maxFeasibleApproach * 0.8d); newApproach = Math.Max(newApproach, currentApproach); if (newApproach <= currentApproach + 2d) { return null; } var newVerticalX = endpoint.X - newApproach; if (path.Count >= 6 && path[^5].X > newVerticalX + tolerance) { return null; } // Build: keep everything before the vertical, shift the vertical, // remove the jog, extend the approach var extended = new List(); for (var i = 0; i < path.Count - 4; i++) { extended.Add(new ElkPoint { X = path[i].X, Y = path[i].Y }); } extended.Add(new ElkPoint { X = newVerticalX, Y = path[^4].Y }); extended.Add(new ElkPoint { X = newVerticalX, Y = penultimate.Y }); extended.Add(new ElkPoint { X = endpoint.X, Y = endpoint.Y }); return NormalizePathPoints(extended); } return null; } private static List? TryExtendTopFaceApproach( IReadOnlyList path, double desiredMinApproach, double tolerance) { var endpoint = path[^1]; var penultimate = path[^2]; var currentApproach = endpoint.Y - penultimate.Y; if (currentApproach <= 0 || currentApproach >= desiredMinApproach - tolerance) { return null; } if (path.Count < 3) { return null; } var preBend = path[^3]; if (Math.Abs(preBend.Y - penultimate.Y) > tolerance) { return null; } double precedingY; if (path.Count >= 4) { precedingY = path[^4].Y; } else { precedingY = path[0].Y; } var maxFeasibleApproach = endpoint.Y - precedingY - 1d; if (maxFeasibleApproach <= currentApproach + 2d) { return null; } var newApproach = Math.Min(desiredMinApproach, maxFeasibleApproach * 0.8d); newApproach = Math.Max(newApproach, currentApproach); if (newApproach <= currentApproach + 2d) { return null; } var newHorizontalY = endpoint.Y - newApproach; if (path.Count >= 4 && path[^4].Y > newHorizontalY + tolerance) { return null; } var extended = path .Select(point => new ElkPoint { X = point.X, Y = point.Y }) .ToList(); extended[^2] = new ElkPoint { X = extended[^2].X, Y = newHorizontalY }; extended[^3] = new ElkPoint { X = extended[^3].X, Y = newHorizontalY }; return NormalizePathPoints(extended); } }