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>
|
/// <summary>
|
||||||
/// When two edges converge on the same target face with inadequate
|
/// When two edges converge on the same target face with inadequate
|
||||||
/// Y-separation, redirects the outer edge (further from target) to
|
/// Y-separation, spreads their approach lanes apart to create adequate
|
||||||
/// the target's right face (for LTR layout). This eliminates the
|
/// clearance. Pushes the lower edge DOWN and/or the upper edge UP by
|
||||||
/// target-join violation by separating approach paths to different faces.
|
/// the minimum amount needed to reach minClearance separation.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static ElkRoutedEdge[]? ReassignConvergentTargetFace(
|
private static ElkRoutedEdge[]? ReassignConvergentTargetFace(
|
||||||
ElkRoutedEdge[] edges,
|
ElkRoutedEdge[] edges,
|
||||||
@@ -19,6 +19,10 @@ internal static partial class ElkEdgeRouterIterative
|
|||||||
}
|
}
|
||||||
|
|
||||||
var nodesById = nodes.ToDictionary(n => n.Id, StringComparer.Ordinal);
|
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);
|
var joinSeverity = new Dictionary<string, int>(StringComparer.Ordinal);
|
||||||
ElkEdgeRoutingScoring.CountTargetApproachJoinViolations(edges, nodes, joinSeverity, 1);
|
ElkEdgeRoutingScoring.CountTargetApproachJoinViolations(edges, nodes, joinSeverity, 1);
|
||||||
if (joinSeverity.Count < 2)
|
if (joinSeverity.Count < 2)
|
||||||
@@ -42,86 +46,89 @@ internal static partial class ElkEdgeRouterIterative
|
|||||||
continue;
|
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)
|
if (groupEdges.Length < 2)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the edge that's furthest from the target (longest approach).
|
// Get approach Y-positions (the horizontal segment Y before the target).
|
||||||
// This is the one to redirect to the right face.
|
var paths = groupEdges.Select(ExtractPath).ToArray();
|
||||||
var outerEdge = groupEdges
|
var approachYs = new double[groupEdges.Length];
|
||||||
.OrderByDescending(e =>
|
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);
|
if (Math.Abs(paths[i][j].Y - paths[i][j + 1].Y) <= 2d)
|
||||||
return path.Count > 0
|
{
|
||||||
? Math.Abs(path[0].X - targetNode.X)
|
approachYs[i] = paths[i][j].Y;
|
||||||
: 0d;
|
approachSegIndices[i] = j;
|
||||||
})
|
break;
|
||||||
.Last(); // The CLOSEST one gets redirected to right face
|
}
|
||||||
// (shorter path adjustment)
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var outerPath = ExtractPath(outerEdge);
|
if (approachSegIndices.Any(idx => idx < 0))
|
||||||
if (outerPath.Count < 2)
|
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redirect: approach the target's right face instead of bottom.
|
// Compute the current gap and required spread.
|
||||||
var rightFaceX = targetNode.X + targetNode.Width;
|
var currentGap = Math.Abs(approachYs[1] - approachYs[0]);
|
||||||
var rightFaceY = targetNode.Y + (targetNode.Height / 2d);
|
if (currentGap >= minClearance)
|
||||||
var outerIndex = Array.FindIndex(edges, e =>
|
|
||||||
string.Equals(e.Id, outerEdge.Id, StringComparison.Ordinal));
|
|
||||||
if (outerIndex < 0)
|
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build new path: keep everything up to the last horizontal segment,
|
var spreadNeeded = minClearance - currentGap + 8d;
|
||||||
// then redirect to the right face.
|
var halfSpread = spreadNeeded / 2d;
|
||||||
var newPath = new List<ElkPoint>();
|
|
||||||
var redirected = false;
|
|
||||||
|
|
||||||
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.
|
var globalIdx = Array.FindIndex(edges, e =>
|
||||||
if (i == outerPath.Count - 3
|
string.Equals(e.Id, groupEdges[edgeIdx].Id, StringComparison.Ordinal));
|
||||||
&& Math.Abs(outerPath[i].Y - outerPath[i + 1].Y) <= 2d)
|
if (globalIdx < 0)
|
||||||
{
|
{
|
||||||
// Redirect: extend horizontal past the right face,
|
continue;
|
||||||
// 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]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var pastRightX = rightFaceX + 24d;
|
var path = paths[edgeIdx];
|
||||||
var sourceY = newPath[^1].Y;
|
var newPath = new List<ElkPoint>(path.Count);
|
||||||
newPath.Add(new ElkPoint { X = pastRightX, Y = sourceY });
|
for (var i = 0; i < path.Count; i++)
|
||||||
newPath.Add(new ElkPoint { X = pastRightX, Y = rightFaceY });
|
{
|
||||||
newPath.Add(new ElkPoint { X = rightFaceX, Y = rightFaceY });
|
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(
|
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;
|
return result;
|
||||||
|
|||||||
@@ -249,11 +249,19 @@ internal static class ElkEdgeRoutingScoring
|
|||||||
}
|
}
|
||||||
|
|
||||||
var graphMaxY = nodes.Max(node => node.Y + node.Height);
|
var graphMaxY = nodes.Max(node => node.Y + node.Height);
|
||||||
|
var graphMinY = nodes.Min(node => node.Y);
|
||||||
var disallowedBottomY = graphMaxY + 4d;
|
var disallowedBottomY = graphMaxY + 4d;
|
||||||
var count = 0;
|
var count = 0;
|
||||||
|
|
||||||
foreach (var edge in edges)
|
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;
|
var edgeViolation = false;
|
||||||
foreach (var segment in ElkEdgeRoutingGeometry.FlattenSegments(edge))
|
foreach (var segment in ElkEdgeRoutingGeometry.FlattenSegments(edge))
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user