From acf70be367329572e11aaf42a17e19b715109f6b Mon Sep 17 00:00:00 2001 From: master <> Date: Mon, 30 Mar 2026 00:19:04 +0300 Subject: [PATCH] Enforce routing-clearance Y-gaps in Sugiyama placement MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../ElkSharpLayoutInitialPlacement.cs | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/__Libraries/StellaOps.ElkSharp/ElkSharpLayoutInitialPlacement.cs b/src/__Libraries/StellaOps.ElkSharp/ElkSharpLayoutInitialPlacement.cs index e807f2d1d..2796ca5d8 100644 --- a/src/__Libraries/StellaOps.ElkSharp/ElkSharpLayoutInitialPlacement.cs +++ b/src/__Libraries/StellaOps.ElkSharp/ElkSharpLayoutInitialPlacement.cs @@ -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) {