diff --git a/src/__Libraries/StellaOps.ElkSharp/ElkEdgePostProcessor.CorridorSpacing.cs b/src/__Libraries/StellaOps.ElkSharp/ElkEdgePostProcessor.CorridorSpacing.cs index 899bfbe8b..269bd1b82 100644 --- a/src/__Libraries/StellaOps.ElkSharp/ElkEdgePostProcessor.CorridorSpacing.cs +++ b/src/__Libraries/StellaOps.ElkSharp/ElkEdgePostProcessor.CorridorSpacing.cs @@ -25,11 +25,13 @@ internal static partial class ElkEdgePostProcessor // Collect all above-graph corridor lanes (distinct rounded Y values) var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal); - var corridorEntries = new List<(int EdgeIndex, double CorridorY, bool IsEndBound)>(); + var corridorEntries = new List<(int EdgeIndex, double CorridorY, bool IsEndBound, double MinX, double MaxX)>(); for (var i = 0; i < edges.Length; i++) { var bestAboveY = double.NaN; var bestLength = 0d; + var bestMinX = 0d; + var bestMaxX = 0d; foreach (var section in edges[i].Sections) { var points = new List { section.StartPoint }; @@ -48,6 +50,8 @@ internal static partial class ElkEdgePostProcessor { bestLength = length; bestAboveY = points[j].Y; + bestMinX = Math.Min(points[j].X, points[j + 1].X); + bestMaxX = Math.Max(points[j].X, points[j + 1].X); } } } @@ -57,7 +61,7 @@ internal static partial class ElkEdgePostProcessor var isEndBound = !string.IsNullOrWhiteSpace(edges[i].TargetNodeId) && nodesById.TryGetValue(edges[i].TargetNodeId!, out var targetNode) && string.Equals(targetNode.Kind, "End", StringComparison.Ordinal); - corridorEntries.Add((i, bestAboveY, isEndBound)); + corridorEntries.Add((i, bestAboveY, isEndBound, bestMinX, bestMaxX)); } } @@ -66,15 +70,57 @@ internal static partial class ElkEdgePostProcessor return edges; } - // Group by rounded corridor Y (edges sharing a corridor lane) - var lanes = corridorEntries + // Group by rounded corridor Y, then split groups where edges have + // overlapping X ranges (visually stacked on top of each other). + var rawLanes = corridorEntries .GroupBy(entry => Math.Round(entry.CorridorY, 0)) - .OrderByDescending(group => group.Key) // closest to graph first (least negative) - .Select(group => new + .OrderByDescending(group => group.Key) + .ToArray(); + + var splitLanes = new List<(double CurrentY, (int EdgeIndex, double CorridorY, bool IsEndBound, double MinX, double MaxX)[] Entries)>(); + foreach (var group in rawLanes) + { + var entries = group.ToArray(); + if (entries.Length <= 1) { - CurrentY = group.Key, - Entries = group.ToArray(), - }) + splitLanes.Add((group.Key, entries)); + continue; + } + + // Check for X-range overlaps within this lane + var hasOverlap = false; + for (var a = 0; a < entries.Length && !hasOverlap; a++) + { + for (var b = a + 1; b < entries.Length; b++) + { + var overlap = Math.Min(entries[a].MaxX, entries[b].MaxX) + - Math.Max(entries[a].MinX, entries[b].MinX); + if (overlap > 40d) + { + hasOverlap = true; + break; + } + } + } + + if (!hasOverlap) + { + splitLanes.Add((group.Key, entries)); + } + else + { + // Split: each edge gets its own sub-lane + for (var k = 0; k < entries.Length; k++) + { + splitLanes.Add((group.Key - (k * minGap), new[] { entries[k] })); + } + } + } + + // Re-sort after splitting + var lanes = splitLanes + .OrderByDescending(lane => lane.CurrentY) + .Select(lane => new { lane.CurrentY, lane.Entries }) .ToArray(); if (lanes.Length < 2) @@ -140,28 +186,27 @@ internal static partial class ElkEdgePostProcessor return edges; } - for (var i = 0; i < lanes.Length; i++) - { - var shift = targetYValues[i] - lanes[i].CurrentY; - } - // Apply shifts var result = edges.ToArray(); for (var i = 0; i < lanes.Length; i++) { - var shift = targetYValues[i] - lanes[i].CurrentY; - if (Math.Abs(shift) < 1d) - { - continue; - } foreach (var entry in lanes[i].Entries) { var edge = result[entry.EdgeIndex]; + // Use the entry's actual corridor Y for point matching, + // not the lane's synthetic CurrentY (which may differ after + // lane splitting for overlapping X ranges). + var entryShift = targetYValues[i] - Math.Round(entry.CorridorY, 0); + if (Math.Abs(entryShift) < 1d) + { + continue; + } + var shifted = ShiftEdgeCorridorY( edge, - lanes[i].CurrentY, - shift, + Math.Round(entry.CorridorY, 0), + entryShift, graphMinY); result[entry.EdgeIndex] = shifted; }