ElkSharp: gateway face overflow redirect, under-node push-first routing, boundary-slot snap

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
master
2026-04-01 10:35:23 +03:00
parent 5af14cf212
commit f275b8a267
30 changed files with 5632 additions and 2647 deletions

View File

@@ -53,7 +53,7 @@ internal static partial class ElkEdgeRouterIterative
if (current.RetryState.ExcessiveDetourViolations > 0
|| (!preferLowWaveRuntimePolish && current.RetryState.GatewaySourceExitViolations > 0))
{
current = ApplyWinnerDetourPolish(current, nodes, minLineClearance);
current = ApplyWinnerDetourPolish(current, nodes, direction, minLineClearance);
ElkLayoutDiagnostics.LogProgress($"Hybrid winner refinement after detour polish: {DescribeSolution(current)}");
}
@@ -100,6 +100,10 @@ internal static partial class ElkEdgeRouterIterative
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)
@@ -166,9 +170,158 @@ internal static partial class ElkEdgeRouterIterative
}
}
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.
if (current.RetryState.BoundarySlotViolations > 0)
{
current = ApplyFinalBoundarySlotPolish(current, nodes, direction, minLineClearance, maxRounds: 1);
ElkLayoutDiagnostics.LogProgress($"Hybrid winner refinement after final boundary-slot snap: {DescribeSolution(current)}");
}
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