Files
git.stella-ops.org/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterIterative.WinnerRefinement.Hybrid.cs
master 7d0fea3149 Spread corridor entries across End right face
Each corridor edge enters End at a distinct Y position (1/n+1 fraction)
so the highways are visually traceable all the way to the terminus.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 10:12:05 +03:00

806 lines
38 KiB
C#

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<string, int>(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);
}
// Unconditional corridor reroute: move long sweeps to top corridor
// BEFORE the final target-join check so the join detection sees the
// corridored paths and can spread the corridor approach stubs.
{
var graphMinYLocal = nodes.Min(n => n.Y);
var graphWidthLocal = nodes.Max(n => n.X + n.Width) - nodes.Min(n => n.X);
var nodesById = nodes.ToDictionary(n => n.Id, StringComparer.Ordinal);
// Keep corridor close to graph for visual readability. 24px is
// enough clearance for the perpendicular exit stub.
var baseCorridorY = graphMinYLocal - 24d;
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 cpath = ExtractPath(edge);
for (var si = 0; si < cpath.Count - 1; si++)
{
if (Math.Abs(cpath[si].Y - cpath[si + 1].Y) > 2d) continue;
var segLen = Math.Abs(cpath[si + 1].X - cpath[si].X);
var laneY = cpath[si].Y;
if (segLen < localMinSweep || laneY <= graphMinYLocal - 10d) continue;
// Offset must exceed the target-join detection threshold
// (node-size clearance, not spacing-scaled) so parallel
// corridor segments aren't flagged as joins.
var nodeSizeClearance = ElkEdgeRoutingScoring.ResolveNodeSizeClearance(nodes);
var localCorridorY = baseCorridorY - (corridorFixed * (nodeSizeClearance + 4d));
var src = cpath[0];
var tgt = cpath[^1];
var stubX = src.X + 24d;
// Find the target node to determine approach geometry.
var tgtNode = nodesById.TryGetValue(edge.TargetNodeId ?? string.Empty, out var tn) ? tn : null;
List<ElkPoint> newPath;
if (tgtNode is not null && tgtNode.Kind is "End")
{
// Enter End from the right side: corridor goes past End,
// descends to End's center Y, approaches from right.
// This avoids the ugly long vertical drop from corridor.
// Offset both X and Y for each corridor edge so they
// enter End at distinct positions (visually traceable).
var rightApproachX = tgtNode.X + tgtNode.Width + 24d + (corridorFixed * (nodeSizeClearance + 4d));
// Spread entry points across the right face. First edge
// enters at 1/3 from top, second at 2/3, etc.
var slotFraction = (corridorFixed + 1d) / (corridorFixed + 2d);
var centerY = tgtNode.Y + (tgtNode.Height * slotFraction);
newPath =
[
src,
new() { X = stubX, Y = src.Y },
new() { X = stubX, Y = localCorridorY },
new() { X = rightApproachX, Y = localCorridorY },
new() { X = rightApproachX, Y = centerY },
new() { X = tgtNode.X + tgtNode.Width, Y = centerY },
];
}
else
{
newPath =
[
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);
// Accept only if no new repeat-collector or node-crossing regressions.
if (corridorScore.RepeatCollectorCorridorViolations <= current.Score.RepeatCollectorCorridorViolations
&& corridorScore.NodeCrossings <= current.Score.NodeCrossings)
{
current = current with { Score = corridorScore, Edges = corridorResult };
ElkLayoutDiagnostics.LogProgress(
$"Unconditional corridor reroute: {corridorFixed} edges to corridor");
}
}
}
// Final target-join repair: the per-edge gateway fixes and corridor
// reroute may create new target-join convergences.
var finalJoinSeverity = new Dictionary<string, int>(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);
}
return current;
}
/// <summary>
/// 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.
/// </summary>
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;
}
/// <summary>
/// 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.
/// </summary>
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;
}
/// <summary>
/// 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).
/// </summary>
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<ElkPoint>? 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<string, int>(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;
}
}