ElkSharp: gateway face overflow redirect, under-node push-first routing, boundary-slot snap

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
master
2026-04-01 10:35:23 +03:00
parent 5af14cf212
commit f275b8a267
30 changed files with 5632 additions and 2647 deletions

View File

@@ -85,6 +85,20 @@ internal static partial class ElkEdgeRouterIterative
var currentGap = Math.Abs(approachYs[1] - approachYs[0]);
if (currentGap >= minClearance)
{
// Horizontal approach lanes are well separated, but vertical
// approach segments near the target may still converge (e.g.,
// two edges arriving at a gateway bottom face with parallel
// vertical segments only 28px apart). Redirect the edge whose
// horizontal approach is closest to the node center to the
// upstream face (left tip for LTR).
if (ElkShapeBoundaries.IsGatewayShape(targetNode)
&& ElkEdgeRoutingScoring.HasTargetApproachJoin(
paths[0], paths[1], minClearance, 3))
{
result = TryRedirectGatewayFaceOverflowEntry(
result, edges, groupEdges, paths, targetNode, approachYs);
}
continue;
}
@@ -100,8 +114,7 @@ internal static partial class ElkEdgeRouterIterative
var edgeIdx = which == 0 ? upperIdx : lowerIdx;
var delta = which == 0 ? -halfSpread : halfSpread;
var segIdx = approachSegIndices[edgeIdx];
var origY = approachYs[edgeIdx];
var newY = origY + delta;
var newY = approachYs[edgeIdx] + delta;
var globalIdx = Array.FindIndex(edges, e =>
string.Equals(e.Id, groupEdges[edgeIdx].Id, StringComparison.Ordinal));
@@ -111,17 +124,10 @@ internal static partial class ElkEdgeRouterIterative
}
var path = paths[edgeIdx];
var newPath = new List<ElkPoint>(path.Count);
for (var i = 0; i < path.Count; i++)
var newPath = BuildTargetJoinSpreadPath(path, segIdx, newY);
if (newPath.Count == 0)
{
if ((i == segIdx || i == segIdx + 1) && Math.Abs(path[i].Y - origY) <= 2d)
{
newPath.Add(new ElkPoint { X = path[i].X, Y = newY });
}
else
{
newPath.Add(path[i]);
}
continue;
}
result = ReplaceEdgePath(result, edges, globalIdx, edges[globalIdx], newPath);
@@ -133,4 +139,167 @@ internal static partial class ElkEdgeRouterIterative
return result;
}
private static List<ElkPoint> BuildTargetJoinSpreadPath(
IReadOnlyList<ElkPoint> path,
int approachSegmentIndex,
double newY)
{
if (approachSegmentIndex < 0 || approachSegmentIndex >= path.Count - 1)
{
return [];
}
var segmentStart = path[approachSegmentIndex];
var segmentEnd = path[approachSegmentIndex + 1];
var segmentLength = Math.Abs(segmentEnd.X - segmentStart.X);
if (segmentLength <= 4d)
{
return [];
}
var inset = Math.Clamp(segmentLength / 4d, 12d, 24d);
var transitionX = segmentEnd.X >= segmentStart.X
? Math.Max(segmentStart.X + 4d, segmentEnd.X - inset)
: Math.Min(segmentStart.X - 4d, segmentEnd.X + inset);
if (Math.Abs(transitionX - segmentStart.X) <= 2d
|| Math.Abs(segmentEnd.X - transitionX) <= 2d)
{
transitionX = (segmentStart.X + segmentEnd.X) / 2d;
}
var newPath = new List<ElkPoint>(path.Count + 2);
for (var i = 0; i < approachSegmentIndex; i++)
{
AddUnique(newPath, path[i]);
}
AddUnique(newPath, segmentStart);
AddUnique(newPath, new ElkPoint { X = transitionX, Y = segmentStart.Y });
AddUnique(newPath, new ElkPoint { X = transitionX, Y = newY });
AddUnique(newPath, new ElkPoint { X = segmentEnd.X, Y = newY });
for (var i = approachSegmentIndex + 2; i < path.Count; i++)
{
AddUnique(newPath, path[i]);
}
return newPath;
}
private static void AddUnique(List<ElkPoint> points, ElkPoint point)
{
if (points.Count > 0 && ElkEdgeRoutingGeometry.PointsEqual(points[^1], point))
{
return;
}
points.Add(new ElkPoint { X = point.X, Y = point.Y });
}
/// <summary>
/// When two edges converge on a gateway face with insufficient room for
/// proper slot spacing, redirects the edge whose horizontal approach is
/// closest to the node center Y to the left tip vertex (for LTR layout).
/// This handles the case where horizontal approach Y gaps are large but
/// vertical approach segments near the target are too close in X.
/// </summary>
private static ElkRoutedEdge[]? TryRedirectGatewayFaceOverflowEntry(
ElkRoutedEdge[]? result,
ElkRoutedEdge[] edges,
ElkRoutedEdge[] groupEdges,
IReadOnlyList<ElkPoint>[] paths,
ElkPositionedNode targetNode,
double[] approachYs)
{
if (groupEdges.Length < 2)
{
return result;
}
var centerY = targetNode.Y + targetNode.Height / 2d;
// Pick the edge whose horizontal approach Y is closest to the node
// center -- that edge naturally wants to enter from the upstream face
// (left for LTR) rather than the bottom/top face.
var redirectIdx = -1;
var bestDistToCenter = double.MaxValue;
for (var i = 0; i < groupEdges.Length; i++)
{
var dist = Math.Abs(approachYs[i] - centerY);
if (dist < bestDistToCenter)
{
bestDistToCenter = dist;
redirectIdx = i;
}
}
if (redirectIdx < 0)
{
return result;
}
var redirectPath = paths[redirectIdx];
var leftTipX = targetNode.X;
var leftTipY = centerY;
// Find the last path point that is clearly outside the target node's
// left boundary. Keep all path segments up to that point and build a
// clean entry through the left tip.
var lastOutsideIdx = -1;
for (var j = redirectPath.Count - 1; j >= 0; j--)
{
if (redirectPath[j].X < leftTipX - 4d)
{
lastOutsideIdx = j;
break;
}
}
if (lastOutsideIdx < 0)
{
return result;
}
// Build the redirected path: keep everything up to the last outside
// point, then route horizontally to a stub 24px left of the tip,
// bend vertically to the tip Y, and enter at the tip. The stub X
// must be near the target (not the source) to preserve the source
// exit angle as a clean horizontal departure.
var outsidePoint = redirectPath[lastOutsideIdx];
var stubX = leftTipX - 24d;
var newPath = new List<ElkPoint>(lastOutsideIdx + 5);
for (var j = 0; j <= lastOutsideIdx; j++)
{
AddUnique(newPath, redirectPath[j]);
}
// Horizontal approach to the stub X (preserves source exit angle).
if (Math.Abs(outsidePoint.X - stubX) > 2d)
{
AddUnique(newPath, new ElkPoint { X = stubX, Y = outsidePoint.Y });
}
// Vertical transition to the tip Y.
var currentY = newPath.Count > 0 ? newPath[^1].Y : outsidePoint.Y;
if (Math.Abs(currentY - leftTipY) > 2d)
{
AddUnique(newPath, new ElkPoint { X = stubX, Y = leftTipY });
}
// Enter at the left tip vertex.
AddUnique(newPath, new ElkPoint { X = leftTipX, Y = leftTipY });
var globalIdx = Array.FindIndex(edges, e =>
string.Equals(e.Id, groupEdges[redirectIdx].Id, StringComparison.Ordinal));
if (globalIdx < 0)
{
return result;
}
result = ReplaceEdgePath(result, edges, globalIdx, edges[globalIdx], newPath);
ElkLayoutDiagnostics.LogProgress(
$"Gateway face redirect: {groupEdges[redirectIdx].Id} to left tip of {targetNode.Id}");
return result;
}
}