diff --git a/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterIterative.WinnerRefinement.Hybrid.cs b/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterIterative.WinnerRefinement.Hybrid.cs index 1f9108644..599c08e8b 100644 --- a/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterIterative.WinnerRefinement.Hybrid.cs +++ b/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterIterative.WinnerRefinement.Hybrid.cs @@ -249,7 +249,10 @@ internal static partial class ElkEdgeRouterIterative // artifact polish ends with NormalizeBoundaryAngles + // NormalizeSourceExitAngles, which is the root cause of the // boundary-slot violations when snap ran before it. - if (current.RetryState.BoundarySlotViolations > 0) + // Final boundary-slot snap is expensive (~39s). Skip in the low-wave + // speed path since the FinalScore already handles boundary-slot + // exclusions. The full-wave path runs it for maximum quality. + if (current.RetryState.BoundarySlotViolations > 0 && !preferLowWaveRuntimePolish) { current = ApplyFinalBoundarySlotPolish(current, nodes, direction, minLineClearance, maxRounds: 1); ElkLayoutDiagnostics.LogProgress($"Hybrid winner refinement after final boundary-slot snap: {DescribeSolution(current)}"); @@ -267,27 +270,17 @@ internal static partial class ElkEdgeRouterIterative ElkLayoutDiagnostics.LogProgress($"Hybrid winner refinement after gateway diagonal straightening: {DescribeSolution(current)}"); } - // Per-edge gateway source face redirect: process each gateway artifact - // edge individually, applying the face redirect and validating that it - // doesn't create new hard-rule violations. Bulk processing (all edges at - // once) creates 19+ boundary-slot violations because the redirect paths - // conflict with each other. Per-edge with validation avoids cascading. + // Per-edge gateway fixes: only run when the gateway artifact polish + // left remaining artifacts. Skip the expensive per-edge scoring when + // artifacts are already clean. var postArtifactState = EvaluateGatewayArtifacts(current.Edges, nodes, out var postFocus); if (!postArtifactState.IsClean && postFocus.Length > 0) { current = ApplyPerEdgeGatewayFaceRedirect(current, nodes, minLineClearance, postFocus); + current = ApplyPerEdgeGatewayScoringFix(current, nodes); + current = RepairRemainingEdgeNodeCrossings(current, nodes); } - // Per-edge gateway scoring opportunity fix: for edges where a shorter - // clean exit path is available, apply it directly. Uses the same - // lenient validation as the face redirect. - current = ApplyPerEdgeGatewayScoringFix(current, nodes); - - // Final edge-node crossing repair: some post-pipeline fixes may - // leave or inherit edge segments that pass through unrelated nodes. - // Push crossing horizontal segments above/below the blocking node. - current = RepairRemainingEdgeNodeCrossings(current, nodes); - return current; } @@ -322,25 +315,25 @@ internal static partial class ElkEdgeRouterIterative // 19+ boundary-slot violations. candidate = ElkEdgePostProcessor.StraightenGatewayCornerDiagonals(candidate, nodes); - // Check if the fix creates any new hard-rule violations. + // Cheap validation: only check node crossings and shared lanes for + // the modified edge, instead of full ComputeScore on all edges. + // Full scoring per edge is O(edges^2) and causes 2min+ regression. + var modifiedEdge = candidate.First(e => string.Equals(e.Id, edgeId, StringComparison.Ordinal)); + var crossings = ElkEdgeRoutingScoring.CountEdgeNodeCrossings([modifiedEdge], nodes, null); + var sharedLanes = ElkEdgeRoutingScoring.CountSharedLaneViolations([modifiedEdge], nodes); + if (crossings > 0 || sharedLanes > 0) + { + continue; + } + + // Full score for accepted candidates only (amortized cost). var candidateScore = ElkEdgeRoutingScoring.ComputeScore(candidate, nodes); - var candidateRetry = BuildRetryState( - candidateScore, - HighwayProcessingEnabled - ? ElkEdgeRouterHighway.DetectRemainingBrokenHighways(candidate, nodes).Count - : 0); - // Allow backtracking to increase by 1 if the gateway-source count - // improves or stays equal. The face redirect naturally creates a - // small overshoot near the gateway, and the FinalScore already - // excludes gateway approach backtracking. + var candidateRetry = BuildRetryState(candidateScore, 0); var candidateGatewaySourceBetter = candidateRetry.GatewaySourceExitViolations <= current.RetryState.GatewaySourceExitViolations; var backtrackingAcceptable = candidateRetry.TargetApproachBacktrackingViolations <= current.RetryState.TargetApproachBacktrackingViolations + 1 && candidateGatewaySourceBetter; - // Also allow boundary-slots to increase by up to 3 — the redirect - // changes the exit point which may temporarily misalign with the - // slot lattice. The final boundary-slot snap pass will clean up. var boundarySlotAcceptable = candidateRetry.BoundarySlotViolations <= current.RetryState.BoundarySlotViolations + 3 && candidateGatewaySourceBetter; @@ -432,15 +425,18 @@ internal static partial class ElkEdgeRouterIterative }; candidateEdges = ElkEdgePostProcessor.StraightenGatewayCornerDiagonals(candidateEdges, nodes); - var candidateScore = ElkEdgeRoutingScoring.ComputeScore(candidateEdges, nodes); - var candidateRetry = BuildRetryState( - candidateScore, - HighwayProcessingEnabled - ? ElkEdgeRouterHighway.DetectRemainingBrokenHighways(candidateEdges, nodes).Count - : 0); - // Lenient: allow backtracking +1 and boundary-slots +3 - // (redirect may create small overshoot and shift slot alignment). + // Cheap validation first: reject if modified edge creates crossings/shared lanes. + var modifiedScoringEdge = candidateEdges[i]; + if (ElkEdgeRoutingScoring.CountEdgeNodeCrossings([modifiedScoringEdge], nodes, null) > 0 + || ElkEdgeRoutingScoring.CountSharedLaneViolations([modifiedScoringEdge], nodes) > 0) + { + continue; + } + + // Full score only for candidates that pass the cheap check. + var candidateScore = ElkEdgeRoutingScoring.ComputeScore(candidateEdges, nodes); + var candidateRetry = BuildRetryState(candidateScore, 0); var backtrackingOk = candidateRetry.TargetApproachBacktrackingViolations <= current.RetryState.TargetApproachBacktrackingViolations + 1; var boundarySlotOk = candidateRetry.BoundarySlotViolations