namespace StellaOps.ElkSharp; internal static partial class ElkEdgeRouterIterative { private static CandidateSolution RefineHybridWinningSolution( CandidateSolution best, ElkPositionedNode[] nodes, ElkLayoutDirection direction, double minLineClearance, bool preferLowWaveRuntimePolish = false) { static string DescribeSolution(CandidateSolution solution) { return $"score={solution.Score.Value:F0} retry={DescribeRetryState(solution.RetryState)}"; } var current = best; ElkLayoutDiagnostics.LogProgress($"Hybrid winner refinement start: {DescribeSolution(current)}"); if (current.RetryState.UnderNodeViolations > 0) { current = ApplyFinalDirectUnderNodePolish(current, nodes, minLineClearance); ElkLayoutDiagnostics.LogProgress($"Hybrid winner refinement after under-node polish: {DescribeSolution(current)}"); } if (current.RetryState.UnderNodeViolations > 0 || current.RetryState.TargetApproachJoinViolations > 0) { current = ApplyFinalProtectedLocalBundlePolish(current, nodes, minLineClearance); ElkLayoutDiagnostics.LogProgress($"Hybrid winner refinement after local-bundle polish: {DescribeSolution(current)}"); } if (current.RetryState.SharedLaneViolations > 0 || current.RetryState.TargetApproachJoinViolations > 0) { current = ApplyFinalSharedLanePolish( current, nodes, direction, minLineClearance, preferLeanTerminalCleanup: preferLowWaveRuntimePolish); ElkLayoutDiagnostics.LogProgress($"Hybrid winner refinement after shared-lane polish: {DescribeSolution(current)}"); } if (current.RetryState.BoundarySlotViolations > 0 || current.RetryState.GatewaySourceExitViolations > 0 || current.RetryState.EntryAngleViolations > 0) { current = ApplyFinalBoundarySlotPolish(current, nodes, direction, minLineClearance, maxRounds: 1); ElkLayoutDiagnostics.LogProgress($"Hybrid winner refinement after boundary-slot polish: {DescribeSolution(current)}"); } if (current.RetryState.ExcessiveDetourViolations > 0 || (!preferLowWaveRuntimePolish && current.RetryState.GatewaySourceExitViolations > 0)) { current = ApplyWinnerDetourPolish(current, nodes, direction, minLineClearance); ElkLayoutDiagnostics.LogProgress($"Hybrid winner refinement after detour polish: {DescribeSolution(current)}"); } if (HasHybridHardRulePressure(current.RetryState)) { current = preferLowWaveRuntimePolish ? ApplyHybridLeanPostSlotHardRulePolish(current, nodes, direction, minLineClearance) : ApplyFinalPostSlotHardRulePolish(current, nodes, direction, minLineClearance, maxRounds: 1); ElkLayoutDiagnostics.LogProgress($"Hybrid winner refinement after post-slot hard-rule polish: {DescribeSolution(current)}"); } // Final gateway backtracking repair: run NormalizeBoundaryAngles one // last time to catch gateway target overshoots that earlier pipeline // steps may have re-introduced. Accept with net-total comparison. if (current.RetryState.TargetApproachBacktrackingViolations > 0 || current.RetryState.EntryAngleViolations > 0) { var finalNormalized = ElkEdgePostProcessor.NormalizeBoundaryAngles(current.Edges, nodes); finalNormalized = ElkEdgePostProcessor.NormalizeSourceExitAngles(finalNormalized, nodes); var finalScore = ElkEdgeRoutingScoring.ComputeScore(finalNormalized, nodes); var finalRetry = BuildRetryState( finalScore, HighwayProcessingEnabled ? ElkEdgeRouterHighway.DetectRemainingBrokenHighways(finalNormalized, nodes).Count : 0); var currentHard = CountTotalHardViolations(current.RetryState); var finalHard = CountTotalHardViolations(finalRetry); if (finalHard < currentHard && finalScore.NodeCrossings <= current.Score.NodeCrossings) { current = current with { Score = finalScore, RetryState = finalRetry, Edges = finalNormalized }; ElkLayoutDiagnostics.LogProgress($"Hybrid winner refinement after final normalization: {DescribeSolution(current)}"); } } // Reroute long horizontal sweeps through the top corridor. // Edges spanning > 40% graph width with under-node violations // should route above the graph (like backward edges) instead of // cutting straight through the node field. // Also pushes medium-length sweeps below blocking nodes. // Each fix type is evaluated INDEPENDENTLY to prevent one fix's // detour from blocking another fix's under-node improvement. if (current.RetryState.UnderNodeViolations > 0) { var corridorCandidate = RerouteLongSweepsThroughCorridor(current.Edges, nodes, direction, minLineClearance); if (corridorCandidate is not null) { corridorCandidate = FinalizeHybridCorridorCandidate( corridorCandidate, nodes, minLineClearance); var corridorScore = ElkEdgeRoutingScoring.ComputeScore(corridorCandidate, nodes); if (corridorScore.Value > current.Score.Value && corridorScore.NodeCrossings <= current.Score.NodeCrossings) { var corridorRetry = BuildRetryState( corridorScore, HighwayProcessingEnabled ? ElkEdgeRouterHighway.DetectRemainingBrokenHighways(corridorCandidate, nodes).Count : 0); current = current with { Score = corridorScore, RetryState = corridorRetry, Edges = corridorCandidate }; ElkLayoutDiagnostics.LogProgress($"Hybrid winner refinement after corridor reroute: {DescribeSolution(current)}"); } } } // 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)}"); } } // Target-join face reassignment: when two edges converge on the // same target face with inadequate separation, redirect the outer // edge to the target's adjacent face (right side for LTR layout). if (current.RetryState.TargetApproachJoinViolations > 0) { var joinCandidate = ReassignConvergentTargetFace(current.Edges, nodes, direction); if (joinCandidate is not null) { var joinScore = ElkEdgeRoutingScoring.ComputeScore(joinCandidate, nodes); if (joinScore.Value > current.Score.Value && joinScore.NodeCrossings <= current.Score.NodeCrossings) { var joinRetry = BuildRetryState( joinScore, HighwayProcessingEnabled ? ElkEdgeRouterHighway.DetectRemainingBrokenHighways(joinCandidate, nodes).Count : 0); current = current with { Score = joinScore, RetryState = joinRetry, Edges = joinCandidate }; ElkLayoutDiagnostics.LogProgress($"Hybrid winner refinement after target-join reassignment: {DescribeSolution(current)}"); } } } if (current.RetryState.TargetApproachJoinViolations > 0) { var joinSeverity = new Dictionary(StringComparer.Ordinal); ElkEdgeRoutingScoring.CountTargetApproachJoinViolations(current.Edges, nodes, joinSeverity, 10); var focusEdgeIds = joinSeverity .OrderByDescending(pair => pair.Value) .ThenBy(pair => pair.Key, StringComparer.Ordinal) .Take(MaxWinnerPolishBatchedRootEdges + 1) .Select(pair => pair.Key) .ToArray(); focusEdgeIds = ExpandTargetApproachJoinRepairSet( focusEdgeIds, current.Edges, nodes, minLineClearance); if (focusEdgeIds.Length > 0) { var focusedJoinCandidate = ElkEdgePostProcessor.SpreadTargetApproachJoins( current.Edges, nodes, minLineClearance, focusEdgeIds, forceOutwardAxisSpacing: true); focusedJoinCandidate = ElkEdgePostProcessor.SpreadRectTargetApproachFeederBands( focusedJoinCandidate, nodes, minLineClearance, focusEdgeIds); focusedJoinCandidate = ElkEdgePostProcessor.RepairBoundaryAnglesAndTargetApproaches( focusedJoinCandidate, nodes, minLineClearance, focusEdgeIds); focusedJoinCandidate = ElkEdgePostProcessor.NormalizeBoundaryAngles(focusedJoinCandidate, nodes); focusedJoinCandidate = ElkEdgePostProcessor.NormalizeSourceExitAngles(focusedJoinCandidate, nodes); var focusedJoinScore = ElkEdgeRoutingScoring.ComputeScore(focusedJoinCandidate, nodes); if (focusedJoinScore.Value > current.Score.Value && focusedJoinScore.NodeCrossings <= current.Score.NodeCrossings) { var focusedJoinRetry = BuildRetryState( focusedJoinScore, HighwayProcessingEnabled ? ElkEdgeRouterHighway.DetectRemainingBrokenHighways(focusedJoinCandidate, nodes).Count : 0); current = current with { Score = focusedJoinScore, RetryState = focusedJoinRetry, Edges = focusedJoinCandidate, }; ElkLayoutDiagnostics.LogProgress($"Hybrid winner refinement after focused target-join polish: {DescribeSolution(current)}"); } } } if (current.RetryState.UnderNodeViolations > 0 || current.RetryState.TargetApproachJoinViolations > 0) { current = ApplyFinalProtectedLocalBundlePolish(current, nodes, minLineClearance); current = ApplyFinalDirectUnderNodePolish(current, nodes, minLineClearance); ElkLayoutDiagnostics.LogProgress($"Hybrid winner refinement after late under-node polish: {DescribeSolution(current)}"); } if (current.RetryState.ExcessiveDetourViolations > 0) { current = ApplyWinnerDetourPolish(current, nodes, direction, minLineClearance); ElkLayoutDiagnostics.LogProgress($"Hybrid winner refinement after late detour polish: {DescribeSolution(current)}"); } current = ApplyFinalGatewayArtifactPolish(current, nodes, minLineClearance); ElkLayoutDiagnostics.LogProgress($"Hybrid winner refinement after gateway artifact polish: {DescribeSolution(current)}"); // Final boundary-slot snap: run AFTER the gateway artifact polish // so that normalization passes inside the gateway polish do not // shift endpoints off the slot lattice after snapping. The gateway // artifact polish ends with NormalizeBoundaryAngles + // NormalizeSourceExitAngles, which is the root cause of the // boundary-slot violations when snap ran before it. // 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)}"); } // Eliminate large diagonal segments that may survive through the // iterative optimization (the hybrid baseline runs EliminateDiagonalSegments // but subsequent pipeline passes can re-introduce diagonals). if (current.Score.LongDiagonalViolations > 0) { var eliminated = ElkEdgePostProcessor.EliminateDiagonalSegments(current.Edges, nodes); var elimScore = ElkEdgeRoutingScoring.ComputeScore(eliminated, nodes); if (elimScore.LongDiagonalViolations < current.Score.LongDiagonalViolations && elimScore.NodeCrossings <= current.Score.NodeCrossings) { current = current with { Score = elimScore, Edges = eliminated }; } } // Straighten short diagonal stubs at gateway boundary vertices. var straightened = ElkEdgePostProcessor.StraightenGatewayCornerDiagonals(current.Edges, nodes); if (!ReferenceEquals(straightened, current.Edges)) { var straightenedScore = ElkEdgeRoutingScoring.ComputeScore(straightened, nodes); current = current with { Score = straightenedScore, Edges = straightened }; ElkLayoutDiagnostics.LogProgress($"Hybrid winner refinement after gateway diagonal straightening: {DescribeSolution(current)}"); } // 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); } // Final target-join repair: the per-edge gateway fixes may create // new target-join convergences that didn't exist before. Run the // spread one more time to catch them. var finalJoinSeverity = new Dictionary(StringComparer.Ordinal); var finalJoinCount = ElkEdgeRoutingScoring.CountTargetApproachJoinViolations(current.Edges, nodes, finalJoinSeverity, 1); ElkLayoutDiagnostics.LogProgress( $"Final target-join check: count={finalJoinCount} edges=[{string.Join(", ", finalJoinSeverity.Keys.OrderBy(k => k))}]"); if (finalJoinCount > 0 && finalJoinSeverity.Count >= 2) { // Spread target approaches apart. Accept if it fixes the join // regardless of detour (the FinalScore excludes spread-induced // detours for edges sharing a target with join partners). var joinFocus = finalJoinSeverity.Keys.ToArray(); var joinCandidate = ElkEdgePostProcessor.SpreadTargetApproachJoins( current.Edges, nodes, minLineClearance, joinFocus, forceOutwardAxisSpacing: true); joinCandidate = ElkEdgePostProcessor.StraightenGatewayCornerDiagonals(joinCandidate, nodes); var joinScore = ElkEdgeRoutingScoring.ComputeScore(joinCandidate, nodes); var joinJoinCount = ElkEdgeRoutingScoring.CountTargetApproachJoinViolations(joinCandidate, nodes); if (joinJoinCount < finalJoinCount && joinScore.NodeCrossings <= current.Score.NodeCrossings) { var joinRetry = BuildRetryState(joinScore, 0); current = current with { Score = joinScore, RetryState = joinRetry, Edges = joinCandidate }; ElkLayoutDiagnostics.LogProgress( $"Hybrid final target-join repair: joins={joinJoinCount}"); } } // Second per-edge gateway pass: the target-join spread may create new // face mismatches. Run the redirect again to clean up. var postSpreadArtifacts = EvaluateGatewayArtifacts(current.Edges, nodes, out var postSpreadFocus); if (!postSpreadArtifacts.IsClean && postSpreadFocus.Length > 0) { current = ApplyPerEdgeGatewayFaceRedirect(current, nodes, minLineClearance, postSpreadFocus); current = ApplyPerEdgeGatewayScoringFix(current, nodes); current = RepairRemainingEdgeNodeCrossings(current, nodes); } // Unconditional corridor reroute for long sweeps through the node field. // The score-gated corridor reroute earlier may reject these because the // corridor candidate scores worse (more bends, longer paths). But visually, // long horizontal highways through the graph are ugly. Apply unconditionally. { var graphMinYLocal = nodes.Min(n => n.Y); var graphWidthLocal = nodes.Max(n => n.X + n.Width) - nodes.Min(n => n.X); var baseCorridorY = graphMinYLocal - 56d; var localMinSweep = graphWidthLocal * 0.4d; var corridorResult = current.Edges.ToArray(); var corridorFixed = 0; for (var ei = 0; ei < corridorResult.Length; ei++) { var edge = corridorResult[ei]; var path = ExtractPath(edge); for (var si = 0; si < path.Count - 1; si++) { if (Math.Abs(path[si].Y - path[si + 1].Y) > 2d) continue; var segLen = Math.Abs(path[si + 1].X - path[si].X); var laneY = path[si].Y; if (segLen < localMinSweep || laneY <= graphMinYLocal - 10d) continue; // Route through top corridor. Offset each successive edge // so multiple corridor routes don't converge at the same Y. var localCorridorY = baseCorridorY - (corridorFixed * 24d); var src = path[0]; var tgt = path[^1]; var stubX = src.X + 24d; var newPath = new List { src, new() { X = stubX, Y = src.Y }, new() { X = stubX, Y = localCorridorY }, new() { X = tgt.X, Y = localCorridorY }, tgt, }; corridorResult[ei] = new ElkRoutedEdge { Id = edge.Id, SourceNodeId = edge.SourceNodeId, TargetNodeId = edge.TargetNodeId, SourcePortId = edge.SourcePortId, TargetPortId = edge.TargetPortId, Kind = edge.Kind, Label = edge.Label, Sections = [new ElkEdgeSection { StartPoint = newPath[0], EndPoint = newPath[^1], BendPoints = newPath.Skip(1).Take(newPath.Count - 2).ToArray(), }], }; corridorFixed++; break; } } if (corridorFixed > 0) { var corridorScore = ElkEdgeRoutingScoring.ComputeScore(corridorResult, nodes); current = current with { Score = corridorScore, Edges = corridorResult }; ElkLayoutDiagnostics.LogProgress( $"Unconditional corridor reroute: {corridorFixed} edges to corridor"); } } return current; } /// /// Applies gateway face redirects one edge at a time, validating each /// individually against hard-rule regressions. Bulk processing creates /// cascading conflicts (19+ boundary-slot violations), but single-edge /// fixes are often safe because they don't interact with each other. /// private static CandidateSolution ApplyPerEdgeGatewayFaceRedirect( CandidateSolution solution, ElkPositionedNode[] nodes, double minLineClearance, string[] focusEdgeIds) { var current = solution; var accepted = 0; foreach (var edgeId in focusEdgeIds) { // Apply face redirect to this single edge. var candidate = ElkEdgePostProcessor.FinalizeGatewayBoundaryGeometry( current.Edges, nodes, [edgeId]); if (ReferenceEquals(candidate, current.Edges)) { continue; } // Straighten corner diagonals but skip full normalization — running // NormalizeBoundaryAngles on ALL edges after a single-edge redirect // moves other edges' endpoints off their boundary slots, creating // 19+ boundary-slot violations. candidate = ElkEdgePostProcessor.StraightenGatewayCornerDiagonals(candidate, nodes); // 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, 0); var candidateGatewaySourceBetter = candidateRetry.GatewaySourceExitViolations <= current.RetryState.GatewaySourceExitViolations; var backtrackingAcceptable = candidateRetry.TargetApproachBacktrackingViolations <= current.RetryState.TargetApproachBacktrackingViolations + 1 && candidateGatewaySourceBetter; var boundarySlotAcceptable = candidateRetry.BoundarySlotViolations <= current.RetryState.BoundarySlotViolations + 3 && candidateGatewaySourceBetter; var leniently = current.RetryState with { TargetApproachBacktrackingViolations = backtrackingAcceptable ? candidateRetry.TargetApproachBacktrackingViolations : current.RetryState.TargetApproachBacktrackingViolations, BoundarySlotViolations = boundarySlotAcceptable ? candidateRetry.BoundarySlotViolations : current.RetryState.BoundarySlotViolations, }; if (HasHardRuleRegression(candidateRetry, leniently) || candidateScore.NodeCrossings > current.Score.NodeCrossings) { continue; } // Single-edge fix is clean — accept it. current = current with { Score = candidateScore, RetryState = candidateRetry, Edges = candidate, }; accepted++; } if (accepted > 0) { ElkLayoutDiagnostics.LogProgress( $"Hybrid per-edge gateway redirect: {accepted}/{focusEdgeIds.Length} accepted, score={current.Score.Value:F0} retry={DescribeRetryState(current.RetryState)}"); } return current; } /// /// For gateway source edges where a shorter clean exit path is available /// (HasClearGatewaySourceScoringOpportunity), applies the scoring candidate /// one edge at a time with lenient hard-rule validation. /// private static CandidateSolution ApplyPerEdgeGatewayScoringFix( CandidateSolution solution, ElkPositionedNode[] nodes) { var current = solution; var nodesById = nodes.ToDictionary(n => n.Id, StringComparer.Ordinal); var accepted = 0; for (var i = 0; i < current.Edges.Length; i++) { var edge = current.Edges[i]; if (!nodesById.TryGetValue(edge.SourceNodeId ?? string.Empty, out var sourceNode) || !ElkShapeBoundaries.IsGatewayShape(sourceNode)) { continue; } var path = ExtractPath(edge); if (!ElkEdgePostProcessor.TryBuildGatewaySourceScoringCandidate( path, sourceNode, nodes, edge.SourceNodeId, edge.TargetNodeId, out var scoringCandidate) || scoringCandidate.Count < 2) { continue; } // Build the candidate edge array with the scoring fix applied. var candidateEdges = current.Edges.ToArray(); candidateEdges[i] = new ElkRoutedEdge { Id = edge.Id, SourceNodeId = edge.SourceNodeId, TargetNodeId = edge.TargetNodeId, SourcePortId = edge.SourcePortId, TargetPortId = edge.TargetPortId, Kind = edge.Kind, Label = edge.Label, Sections = [ new ElkEdgeSection { StartPoint = scoringCandidate[0], EndPoint = scoringCandidate[^1], BendPoints = scoringCandidate.Skip(1).Take(scoringCandidate.Count - 2).ToArray(), }, ], }; candidateEdges = ElkEdgePostProcessor.StraightenGatewayCornerDiagonals(candidateEdges, nodes); // 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 <= current.RetryState.BoundarySlotViolations + 3; var leniently = current.RetryState with { TargetApproachBacktrackingViolations = backtrackingOk ? candidateRetry.TargetApproachBacktrackingViolations : current.RetryState.TargetApproachBacktrackingViolations, BoundarySlotViolations = boundarySlotOk ? candidateRetry.BoundarySlotViolations : current.RetryState.BoundarySlotViolations, }; if (HasHardRuleRegression(candidateRetry, leniently) || candidateScore.NodeCrossings > current.Score.NodeCrossings) { continue; } current = current with { Score = candidateScore, RetryState = candidateRetry, Edges = candidateEdges, }; accepted++; } if (accepted > 0) { ElkLayoutDiagnostics.LogProgress( $"Hybrid per-edge gateway scoring fix: {accepted} accepted, score={current.Score.Value:F0} retry={DescribeRetryState(current.RetryState)}"); } return current; } /// /// Pushes horizontal edge segments that cross through unrelated nodes /// above or below the blocking node. Only adjusts the segment's Y /// coordinate — no path rebuild or normalization (cosmetic fix). /// private static CandidateSolution RepairRemainingEdgeNodeCrossings( CandidateSolution solution, ElkPositionedNode[] nodes) { var current = solution; var result = current.Edges.ToArray(); var repaired = 0; for (var ei = 0; ei < result.Length; ei++) { var edge = result[ei]; var path = ExtractPath(edge); if (path.Count < 2) { continue; } List? newPath = null; for (var si = 0; si < path.Count - 1; si++) { var p1 = newPath?[si] ?? path[si]; var p2 = newPath?[si + 1] ?? path[si + 1]; if (Math.Abs(p1.Y - p2.Y) > 2d) { continue; } foreach (var node in nodes) { if (string.Equals(node.Id, edge.SourceNodeId, StringComparison.Ordinal) || string.Equals(node.Id, edge.TargetNodeId, StringComparison.Ordinal)) { continue; } if (p1.Y <= node.Y || p1.Y >= node.Y + node.Height) { continue; } if (Math.Max(p1.X, p2.X) <= node.X || Math.Min(p1.X, p2.X) >= node.X + node.Width) { continue; } // Push above the node. var pushY = node.Y - 1d; newPath ??= path.Select(p => new ElkPoint { X = p.X, Y = p.Y }).ToList(); for (var pi = si; pi <= si + 1 && pi < newPath.Count; pi++) { if (Math.Abs(newPath[pi].Y - p1.Y) <= 2d) { newPath[pi] = new ElkPoint { X = newPath[pi].X, Y = pushY }; } } repaired++; break; } } if (newPath is null) { continue; } result[ei] = new ElkRoutedEdge { Id = edge.Id, SourceNodeId = edge.SourceNodeId, TargetNodeId = edge.TargetNodeId, SourcePortId = edge.SourcePortId, TargetPortId = edge.TargetPortId, Kind = edge.Kind, Label = edge.Label, Sections = [ new ElkEdgeSection { StartPoint = newPath[0], EndPoint = newPath[^1], BendPoints = newPath.Skip(1).Take(newPath.Count - 2).ToArray(), }, ], }; } if (repaired > 0) { var repairedScore = ElkEdgeRoutingScoring.ComputeScore(result, nodes); current = current with { Score = repairedScore, Edges = result }; ElkLayoutDiagnostics.LogProgress( $"Hybrid edge-node crossing repair: {repaired} fixed, score={repairedScore.Value:F0}"); } return current; } private static ElkRoutedEdge[] FinalizeHybridCorridorCandidate( ElkRoutedEdge[] candidate, ElkPositionedNode[] nodes, double minLineClearance) { var stabilized = ClampBelowGraphEdges(candidate, nodes); var focusSeverity = new Dictionary(StringComparer.Ordinal); ElkEdgeRoutingScoring.CountUnderNodeViolations(stabilized, nodes, focusSeverity, 10); ElkEdgeRoutingScoring.CountTargetApproachJoinViolations(stabilized, nodes, focusSeverity, 10); if (focusSeverity.Count == 0) { return stabilized; } var focusEdgeIds = focusSeverity .OrderByDescending(pair => pair.Value) .ThenBy(pair => pair.Key, StringComparer.Ordinal) .Take(MaxWinnerPolishBatchedRootEdges + 1) .Select(pair => pair.Key) .ToArray(); focusEdgeIds = ExpandTargetApproachJoinRepairSet( focusEdgeIds, stabilized, nodes, minLineClearance); if (focusEdgeIds.Length == 0) { return stabilized; } var focusedCandidate = ElkEdgePostProcessor.RepairBoundaryAnglesAndTargetApproaches( stabilized, nodes, minLineClearance, focusEdgeIds); focusedCandidate = ElkEdgePostProcessor.SpreadTargetApproachJoins( focusedCandidate, nodes, minLineClearance, focusEdgeIds, forceOutwardAxisSpacing: true); focusedCandidate = ElkEdgePostProcessor.SpreadRectTargetApproachFeederBands( focusedCandidate, nodes, minLineClearance, focusEdgeIds); focusedCandidate = ElkEdgePostProcessor.PolishTargetPeerConflicts( focusedCandidate, nodes, minLineClearance, focusEdgeIds); focusedCandidate = ClampBelowGraphEdges(focusedCandidate, nodes, focusEdgeIds); focusedCandidate = ElkEdgePostProcessor.SnapBoundarySlotAssignments( focusedCandidate, nodes, minLineClearance, focusEdgeIds, enforceAllNodeEndpoints: true); focusedCandidate = ElkEdgePostProcessor.NormalizeBoundaryAngles(focusedCandidate, nodes); focusedCandidate = ElkEdgePostProcessor.NormalizeSourceExitAngles(focusedCandidate, nodes); return ChoosePreferredHardRuleLayout(stabilized, focusedCandidate, nodes); } private static bool HasHybridHardRulePressure(RoutingRetryState retryState) { return retryState.RemainingShortHighways > 0 || retryState.RepeatCollectorCorridorViolations > 0 || retryState.RepeatCollectorNodeClearanceViolations > 0 || retryState.TargetApproachJoinViolations > 0 || retryState.TargetApproachBacktrackingViolations > 0 || retryState.ExcessiveDetourViolations > 0 || retryState.SharedLaneViolations > 0 || retryState.BoundarySlotViolations > 0 || retryState.BelowGraphViolations > 0 || retryState.UnderNodeViolations > 0 || retryState.EntryAngleViolations > 0 || retryState.GatewaySourceExitViolations > 0; } }