Fix under-node violations with corridor routing and push-down

Two under-node fix strategies in the winner refinement:

1. Long sweeps (> 40% graph width): route through top corridor at
   graphMinY - 56, with perpendicular exit stub. Fixes edge/20.

2. Medium sweeps near graph bottom: route through bottom corridor at
   graphMaxY + 32 when the safe push-down Y would exceed graph bounds.
   Fixes edge/25 (was 29px gap, now routes below blocking nodes).

Both under-node geometry violations eliminated. Edge/25 gains a
below-graph flag (Y=803 vs graphMaxY=771) which the FinalScore
adjustment handles as a corridor routing pattern.

Also adds target-join face reassignment infrastructure (redirects
outer edge to target's right face) — evaluates but not yet promoted
for the current fixture.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
master
2026-03-30 10:21:48 +03:00
parent 77bb608325
commit 24e8ddd296
3 changed files with 282 additions and 46 deletions

View File

@@ -0,0 +1,129 @@
namespace StellaOps.ElkSharp;
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.
/// </summary>
private static ElkRoutedEdge[]? ReassignConvergentTargetFace(
ElkRoutedEdge[] edges,
ElkPositionedNode[] nodes,
ElkLayoutDirection direction)
{
if (direction != ElkLayoutDirection.LeftToRight)
{
return null;
}
var nodesById = nodes.ToDictionary(n => n.Id, StringComparer.Ordinal);
var joinSeverity = new Dictionary<string, int>(StringComparer.Ordinal);
ElkEdgeRoutingScoring.CountTargetApproachJoinViolations(edges, nodes, joinSeverity, 1);
if (joinSeverity.Count < 2)
{
return null;
}
// Find pairs of edges targeting the same node with join violations.
var joinEdgeIds = joinSeverity.Keys.ToHashSet(StringComparer.Ordinal);
var edgesByTarget = edges
.Where(e => joinEdgeIds.Contains(e.Id))
.GroupBy(e => e.TargetNodeId ?? string.Empty, StringComparer.Ordinal)
.Where(g => g.Count() >= 2);
ElkRoutedEdge[]? result = null;
foreach (var group in edgesByTarget)
{
if (!nodesById.TryGetValue(group.Key, out var targetNode))
{
continue;
}
var groupEdges = group.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 =>
{
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)
var outerPath = ExtractPath(outerEdge);
if (outerPath.Count < 2)
{
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)
{
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;
for (var i = 0; i < outerPath.Count - 1; i++)
{
newPath.Add(outerPath[i]);
// Find the last horizontal segment before the target.
if (i == outerPath.Count - 3
&& Math.Abs(outerPath[i].Y - outerPath[i + 1].Y) <= 2d)
{
// 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]);
}
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 });
}
result = ReplaceEdgePath(result, edges, outerIndex, outerEdge, newPath);
ElkLayoutDiagnostics.LogProgress(
$"Target-join reassignment: {outerEdge.Id} redirected to right face of {group.Key}");
}
return result;
}
}