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:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user