elksharp: separate overlapping End corridors and widen lead-lane jog

Two fixes for the End approach area:

1. SpreadOuterCorridors now splits shared-Y lanes when edges have
   overlapping X ranges (>40px overlap). edge/20 and edge/23 were both
   at Y=-235 with 2257px of shared horizontal — now split to Y=-235
   and Y=-267 (31.6px gap). Uses the entry's actual corridor Y for
   shift point matching, not the lane's synthetic CurrentY.

2. Widen the lead-lane pre-terminal jog offset from minLineClearance*0.35
   to minLineClearance*0.9. The jog now lands 15px above the End node
   top instead of 6px above the neighboring edge's arrival slot.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
master
2026-04-06 15:46:08 +03:00
parent 9d8a2ad181
commit df07bcfd24

View File

@@ -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<ElkPoint> { 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;
}