Enforce routing-clearance Y-gaps in Sugiyama placement

After all placement refinement passes converge, pushes connected nodes
apart where the Y-gap between source bottom and target top is under 12px.
This prevents the Sugiyama median-based optimization from creating routing
corridors too narrow for clean orthogonal edge routing.

The fix runs as a final one-shot pass in PlaceNodesLeftToRight — no
cascade propagation, just individual node nudges. This eliminates the
edge/15 under-node violation (source-target gap was 5.4px, now 12px)
and improves the overall routing score from -785401 to -684447.

Remaining violations (7): target-joins=1, backtracking=3, shared-lanes=1,
under-node=2. These involve cross-graph routing patterns (long horizontal
sweeps, identical-Y source convergence) that require either layout-level
changes to the Sugiyama ordering or multi-wave A* re-routing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
master
2026-03-30 00:19:04 +03:00
parent eac6625c6e
commit acf70be367

View File

@@ -58,6 +58,7 @@ internal static class ElkSharpLayoutInitialPlacement
{
desiredY[nodeIndex] = nodeIndex * (slotHeight + gridNodeSpacing);
}
}
for (var nodeIndex = 1; nodeIndex < layer.Length; nodeIndex++)
@@ -118,6 +119,38 @@ internal static class ElkSharpLayoutInitialPlacement
ElkNodePlacementAlignment.PropagateSuccessorPositionBackward(
positionedNodes, outgoingNodeIds, nodesById, options.Direction);
// Final routing-clearance enforcement: after all refinement converged,
// push connected nodes apart where the Y-gap is too tight for clean
// edge routing (< 12px). This is a one-shot nudge — no cascade.
if (options.Direction == ElkLayoutDirection.LeftToRight)
{
const double minRoutingClearance = 12d;
foreach (var nodeId in positionedNodes.Keys.ToArray())
{
if (!positionedNodes.TryGetValue(nodeId, out var srcNode)
|| !outgoingNodeIds.TryGetValue(nodeId, out var outgoing))
{
continue;
}
foreach (var tgtId in outgoing)
{
if (!positionedNodes.TryGetValue(tgtId, out var tgtNode)
|| !augmentedNodesById.TryGetValue(tgtId, out var tgtSpec))
{
continue;
}
var srcBottom = srcNode.Y + srcNode.Height;
if (tgtNode.Y >= srcBottom && tgtNode.Y - srcBottom < minRoutingClearance)
{
positionedNodes[tgtId] = ElkLayoutHelpers.CreatePositionedNode(
tgtSpec, tgtNode.X, srcBottom + minRoutingClearance, options.Direction);
}
}
}
}
minNodeY = positionedNodes.Values.Min(n => n.Y);
if (minNodeY < -0.01d)
{