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:
@@ -25,11 +25,13 @@ internal static partial class ElkEdgePostProcessor
|
|||||||
|
|
||||||
// Collect all above-graph corridor lanes (distinct rounded Y values)
|
// Collect all above-graph corridor lanes (distinct rounded Y values)
|
||||||
var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal);
|
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++)
|
for (var i = 0; i < edges.Length; i++)
|
||||||
{
|
{
|
||||||
var bestAboveY = double.NaN;
|
var bestAboveY = double.NaN;
|
||||||
var bestLength = 0d;
|
var bestLength = 0d;
|
||||||
|
var bestMinX = 0d;
|
||||||
|
var bestMaxX = 0d;
|
||||||
foreach (var section in edges[i].Sections)
|
foreach (var section in edges[i].Sections)
|
||||||
{
|
{
|
||||||
var points = new List<ElkPoint> { section.StartPoint };
|
var points = new List<ElkPoint> { section.StartPoint };
|
||||||
@@ -48,6 +50,8 @@ internal static partial class ElkEdgePostProcessor
|
|||||||
{
|
{
|
||||||
bestLength = length;
|
bestLength = length;
|
||||||
bestAboveY = points[j].Y;
|
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)
|
var isEndBound = !string.IsNullOrWhiteSpace(edges[i].TargetNodeId)
|
||||||
&& nodesById.TryGetValue(edges[i].TargetNodeId!, out var targetNode)
|
&& nodesById.TryGetValue(edges[i].TargetNodeId!, out var targetNode)
|
||||||
&& string.Equals(targetNode.Kind, "End", StringComparison.Ordinal);
|
&& 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;
|
return edges;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Group by rounded corridor Y (edges sharing a corridor lane)
|
// Group by rounded corridor Y, then split groups where edges have
|
||||||
var lanes = corridorEntries
|
// overlapping X ranges (visually stacked on top of each other).
|
||||||
|
var rawLanes = corridorEntries
|
||||||
.GroupBy(entry => Math.Round(entry.CorridorY, 0))
|
.GroupBy(entry => Math.Round(entry.CorridorY, 0))
|
||||||
.OrderByDescending(group => group.Key) // closest to graph first (least negative)
|
.OrderByDescending(group => group.Key)
|
||||||
.Select(group => new
|
.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,
|
splitLanes.Add((group.Key, entries));
|
||||||
Entries = group.ToArray(),
|
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();
|
.ToArray();
|
||||||
|
|
||||||
if (lanes.Length < 2)
|
if (lanes.Length < 2)
|
||||||
@@ -140,28 +186,27 @@ internal static partial class ElkEdgePostProcessor
|
|||||||
return edges;
|
return edges;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var i = 0; i < lanes.Length; i++)
|
|
||||||
{
|
|
||||||
var shift = targetYValues[i] - lanes[i].CurrentY;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply shifts
|
// Apply shifts
|
||||||
var result = edges.ToArray();
|
var result = edges.ToArray();
|
||||||
for (var i = 0; i < lanes.Length; i++)
|
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)
|
foreach (var entry in lanes[i].Entries)
|
||||||
{
|
{
|
||||||
var edge = result[entry.EdgeIndex];
|
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(
|
var shifted = ShiftEdgeCorridorY(
|
||||||
edge,
|
edge,
|
||||||
lanes[i].CurrentY,
|
Math.Round(entry.CorridorY, 0),
|
||||||
shift,
|
entryShift,
|
||||||
graphMinY);
|
graphMinY);
|
||||||
result[entry.EdgeIndex] = shifted;
|
result[entry.EdgeIndex] = shifted;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user