namespace StellaOps.ElkSharp; internal static partial class ElkEdgePostProcessor { /// /// Removes orthogonal U-turn backtracks from non-corridor edges. /// Detects segments going LEFT in a left-to-right layout and collapses /// the detour to a direct forward path, preserving the last forward /// point before the backtrack and the next forward point after it. /// Only accepts the collapse if it does not introduce node crossings /// or shared lane violations. /// internal static ElkRoutedEdge[] CollapseOrthogonalBacktracks( ElkRoutedEdge[] edges, ElkPositionedNode[] nodes) { if (edges.Length == 0 || nodes.Length == 0) { return edges; } 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]; // Skip corridor-routed edges (repeat returns intentionally go left) if (HasCorridorBendPoints(edge, graphMinY, graphMaxY)) { continue; } var path = ExtractFullPath(edge); if (path.Count < 5) { continue; } var collapsed = TryCollapseBacktrack(path); if (collapsed is null || collapsed.Count >= path.Count) { continue; } // Validate: no node crossings if (HasNodeObstacleCrossing(collapsed, nodes, edge.SourceNodeId, edge.TargetNodeId)) { continue; } // Validate: no new shared lane violations var candidateEdge = BuildSingleSectionEdge(edge, collapsed); 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? TryCollapseBacktrack(IReadOnlyList path) { const double tolerance = 2d; // Find leftward segments (X decreasing by more than 15px) for (var i = 0; i < path.Count - 1; i++) { var dx = path[i + 1].X - path[i].X; if (dx >= -15d) { continue; } // Found a LEFT-going segment at index i→i+1. // The "anchor" before the backtrack is the point before this segment // that was the last rightward/upward turn. var anchorIndex = i; // Find the first point AFTER the backtrack that resumes rightward // progress at a similar or higher X than the anchor. var anchorX = path[anchorIndex].X; var resumeIndex = -1; for (var j = i + 2; j < path.Count; j++) { if (path[j].X >= anchorX - tolerance) { resumeIndex = j; break; } } if (resumeIndex < 0) { continue; } // Build collapsed path: keep everything up to anchor, // connect directly to resume point, keep the rest. var collapsed = new List(); for (var j = 0; j <= anchorIndex; j++) { collapsed.Add(new ElkPoint { X = path[j].X, Y = path[j].Y }); } // Connect anchor to resume via orthogonal bend var anchor = path[anchorIndex]; var resume = path[resumeIndex]; if (Math.Abs(anchor.X - resume.X) > tolerance && Math.Abs(anchor.Y - resume.Y) > tolerance) { // Need a bend point to keep orthogonal collapsed.Add(new ElkPoint { X = anchor.X, Y = resume.Y }); } for (var j = resumeIndex; j < path.Count; j++) { var pt = path[j]; if (collapsed.Count > 0 && Math.Abs(collapsed[^1].X - pt.X) <= tolerance && Math.Abs(collapsed[^1].Y - pt.Y) <= tolerance) { continue; } collapsed.Add(new ElkPoint { X = pt.X, Y = pt.Y }); } // Normalize: remove collinear intermediate points var normalized = NormalizeOrthogonalPath(collapsed, tolerance); if (normalized.Count < path.Count) { return normalized; } } return null; } }