diff --git a/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterIterative.WinnerRefinement.Hybrid.cs b/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterIterative.WinnerRefinement.Hybrid.cs index 1cb7cdc7a..dacb91e16 100644 --- a/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterIterative.WinnerRefinement.Hybrid.cs +++ b/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterIterative.WinnerRefinement.Hybrid.cs @@ -88,6 +88,34 @@ internal static partial class ElkEdgeRouterIterative } } + // Targeted under-node elevation with net-total promotion. + // ElevateUnderNodeViolations can fix remaining under-node edges + // (gateway-exit lanes, long horizontal sweeps) but the standard + // promotion gating blocks it because elevation increases path + // length (detour). Net-total allows the tradeoff: under-node + // fix (100K savings) outweighs detour cost (50K). + if (current.RetryState.UnderNodeViolations > 0) + { + var elevated = ElkEdgePostProcessor.ElevateUnderNodeViolations( + current.Edges, nodes, minLineClearance); + elevated = ElkEdgePostProcessor.NormalizeBoundaryAngles(elevated, nodes); + elevated = ElkEdgePostProcessor.NormalizeSourceExitAngles(elevated, nodes); + var elevatedScore = ElkEdgeRoutingScoring.ComputeScore(elevated, nodes); + var elevatedRetry = BuildRetryState( + elevatedScore, + HighwayProcessingEnabled + ? ElkEdgeRouterHighway.DetectRemainingBrokenHighways(elevated, nodes).Count + : 0); + // Use weighted comparison: under-node (100K) is worth more than + // detour (50K), so trading 1 under-node for 1 detour is a net win. + if (elevatedScore.Value > current.Score.Value + && elevatedScore.NodeCrossings <= current.Score.NodeCrossings) + { + current = current with { Score = elevatedScore, RetryState = elevatedRetry, Edges = elevated }; + ElkLayoutDiagnostics.LogProgress($"Hybrid winner refinement after under-node elevation: {DescribeSolution(current)}"); + } + } + return current; }