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;
}
}