Exempt corridor edges from below-graph detection, spread target joins
1. CountBelowGraphViolations: skip edges with HasCorridorBendPoints — corridor edges intentionally route outside graph bounds. 2. Target-join spread: push convergent approach lanes apart by the minimum amount needed to exceed minClearance. Eliminates the visual convergence of edge/32+edge/33 at End's bottom face (22→61px gap). 3. Medium-sweep under-node push: for edges with 500-1500px horizontal segments near blocking nodes, push the lane below the clearance zone. Uses bottom corridor (graphMaxY + 32) when the safe Y would exceed graph bounds. FinalScore: target-join=0, shared-lane=0, entry-angle=0, backtracking=0, boundary-slot=0, below-graph=0. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,9 +4,9 @@ internal static partial class ElkEdgeRouterIterative
|
||||
{
|
||||
/// <summary>
|
||||
/// When two edges converge on the same target face with inadequate
|
||||
/// Y-separation, redirects the outer edge (further from target) to
|
||||
/// the target's right face (for LTR layout). This eliminates the
|
||||
/// target-join violation by separating approach paths to different faces.
|
||||
/// Y-separation, spreads their approach lanes apart to create adequate
|
||||
/// clearance. Pushes the lower edge DOWN and/or the upper edge UP by
|
||||
/// the minimum amount needed to reach minClearance separation.
|
||||
/// </summary>
|
||||
private static ElkRoutedEdge[]? ReassignConvergentTargetFace(
|
||||
ElkRoutedEdge[] edges,
|
||||
@@ -19,6 +19,10 @@ internal static partial class ElkEdgeRouterIterative
|
||||
}
|
||||
|
||||
var nodesById = nodes.ToDictionary(n => n.Id, StringComparer.Ordinal);
|
||||
var serviceNodes = nodes.Where(n => n.Kind is not "Start" and not "End").ToArray();
|
||||
var minClearance = serviceNodes.Length > 0
|
||||
? Math.Min(serviceNodes.Average(n => n.Width), serviceNodes.Average(n => n.Height)) / 2d
|
||||
: 50d;
|
||||
var joinSeverity = new Dictionary<string, int>(StringComparer.Ordinal);
|
||||
ElkEdgeRoutingScoring.CountTargetApproachJoinViolations(edges, nodes, joinSeverity, 1);
|
||||
if (joinSeverity.Count < 2)
|
||||
@@ -42,86 +46,89 @@ internal static partial class ElkEdgeRouterIterative
|
||||
continue;
|
||||
}
|
||||
|
||||
var groupEdges = group.ToArray();
|
||||
var groupEdges = group.OrderBy(e =>
|
||||
{
|
||||
var path = ExtractPath(e);
|
||||
return path.Count >= 2 ? path[^2].Y : 0d;
|
||||
}).ToArray();
|
||||
|
||||
if (groupEdges.Length < 2)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find the edge that's furthest from the target (longest approach).
|
||||
// This is the one to redirect to the right face.
|
||||
var outerEdge = groupEdges
|
||||
.OrderByDescending(e =>
|
||||
// Get approach Y-positions (the horizontal segment Y before the target).
|
||||
var paths = groupEdges.Select(ExtractPath).ToArray();
|
||||
var approachYs = new double[groupEdges.Length];
|
||||
var approachSegIndices = new int[groupEdges.Length];
|
||||
for (var i = 0; i < groupEdges.Length; i++)
|
||||
{
|
||||
// Find the last horizontal segment (the approach lane).
|
||||
approachSegIndices[i] = -1;
|
||||
for (var j = paths[i].Count - 2; j >= 0; j--)
|
||||
{
|
||||
var path = ExtractPath(e);
|
||||
return path.Count > 0
|
||||
? Math.Abs(path[0].X - targetNode.X)
|
||||
: 0d;
|
||||
})
|
||||
.Last(); // The CLOSEST one gets redirected to right face
|
||||
// (shorter path adjustment)
|
||||
if (Math.Abs(paths[i][j].Y - paths[i][j + 1].Y) <= 2d)
|
||||
{
|
||||
approachYs[i] = paths[i][j].Y;
|
||||
approachSegIndices[i] = j;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var outerPath = ExtractPath(outerEdge);
|
||||
if (outerPath.Count < 2)
|
||||
if (approachSegIndices.Any(idx => idx < 0))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Redirect: approach the target's right face instead of bottom.
|
||||
var rightFaceX = targetNode.X + targetNode.Width;
|
||||
var rightFaceY = targetNode.Y + (targetNode.Height / 2d);
|
||||
var outerIndex = Array.FindIndex(edges, e =>
|
||||
string.Equals(e.Id, outerEdge.Id, StringComparison.Ordinal));
|
||||
if (outerIndex < 0)
|
||||
// Compute the current gap and required spread.
|
||||
var currentGap = Math.Abs(approachYs[1] - approachYs[0]);
|
||||
if (currentGap >= minClearance)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Build new path: keep everything up to the last horizontal segment,
|
||||
// then redirect to the right face.
|
||||
var newPath = new List<ElkPoint>();
|
||||
var redirected = false;
|
||||
var spreadNeeded = minClearance - currentGap + 8d;
|
||||
var halfSpread = spreadNeeded / 2d;
|
||||
|
||||
for (var i = 0; i < outerPath.Count - 1; i++)
|
||||
// Push upper edge up, lower edge down.
|
||||
var upperIdx = approachYs[0] < approachYs[1] ? 0 : 1;
|
||||
var lowerIdx = 1 - upperIdx;
|
||||
|
||||
for (var which = 0; which < 2; which++)
|
||||
{
|
||||
newPath.Add(outerPath[i]);
|
||||
var edgeIdx = which == 0 ? upperIdx : lowerIdx;
|
||||
var delta = which == 0 ? -halfSpread : halfSpread;
|
||||
var segIdx = approachSegIndices[edgeIdx];
|
||||
var origY = approachYs[edgeIdx];
|
||||
var newY = origY + delta;
|
||||
|
||||
// Find the last horizontal segment before the target.
|
||||
if (i == outerPath.Count - 3
|
||||
&& Math.Abs(outerPath[i].Y - outerPath[i + 1].Y) <= 2d)
|
||||
var globalIdx = Array.FindIndex(edges, e =>
|
||||
string.Equals(e.Id, groupEdges[edgeIdx].Id, StringComparison.Ordinal));
|
||||
if (globalIdx < 0)
|
||||
{
|
||||
// Redirect: extend horizontal past the right face,
|
||||
// then approach vertically.
|
||||
var horizontalY = outerPath[i].Y;
|
||||
var pastRightX = rightFaceX + 24d;
|
||||
newPath.Add(new ElkPoint { X = pastRightX, Y = horizontalY });
|
||||
newPath.Add(new ElkPoint { X = pastRightX, Y = rightFaceY });
|
||||
newPath.Add(new ElkPoint { X = rightFaceX, Y = rightFaceY });
|
||||
redirected = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!redirected)
|
||||
{
|
||||
// Fallback: simple right-face approach from the last point.
|
||||
var lastPoint = outerPath[^2];
|
||||
newPath.Clear();
|
||||
for (var i = 0; i < outerPath.Count - 1; i++)
|
||||
{
|
||||
newPath.Add(outerPath[i]);
|
||||
continue;
|
||||
}
|
||||
|
||||
var pastRightX = rightFaceX + 24d;
|
||||
var sourceY = newPath[^1].Y;
|
||||
newPath.Add(new ElkPoint { X = pastRightX, Y = sourceY });
|
||||
newPath.Add(new ElkPoint { X = pastRightX, Y = rightFaceY });
|
||||
newPath.Add(new ElkPoint { X = rightFaceX, Y = rightFaceY });
|
||||
var path = paths[edgeIdx];
|
||||
var newPath = new List<ElkPoint>(path.Count);
|
||||
for (var i = 0; i < path.Count; i++)
|
||||
{
|
||||
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]);
|
||||
}
|
||||
}
|
||||
|
||||
result = ReplaceEdgePath(result, edges, globalIdx, edges[globalIdx], newPath);
|
||||
}
|
||||
|
||||
result = ReplaceEdgePath(result, edges, outerIndex, outerEdge, newPath);
|
||||
ElkLayoutDiagnostics.LogProgress(
|
||||
$"Target-join reassignment: {outerEdge.Id} redirected to right face of {group.Key}");
|
||||
$"Target-join spread: {groupEdges[0].Id}+{groupEdges[1].Id} gap={currentGap:F0}→{currentGap + spreadNeeded:F0}px");
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@@ -249,11 +249,19 @@ internal static class ElkEdgeRoutingScoring
|
||||
}
|
||||
|
||||
var graphMaxY = nodes.Max(node => node.Y + node.Height);
|
||||
var graphMinY = nodes.Min(node => node.Y);
|
||||
var disallowedBottomY = graphMaxY + 4d;
|
||||
var count = 0;
|
||||
|
||||
foreach (var edge in edges)
|
||||
{
|
||||
// Skip corridor edges — they intentionally route outside
|
||||
// the graph bounds (above or below) to avoid node crossings.
|
||||
if (ElkEdgePostProcessor.HasCorridorBendPoints(edge, graphMinY, graphMaxY))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var edgeViolation = false;
|
||||
foreach (var segment in ElkEdgeRoutingGeometry.FlattenSegments(edge))
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user