Fix entry-angle violations and add boundary-first routing infrastructure

The short-stub fallback in NormalizeExitPath fixes 2 entry-angle violations
(edge/7, edge/27) that persisted because the default long-stub normalization
created horizontal segments crossing nodes in occupied Y-bands. When the long
stub fails HasClearSourceExitSegment, the normalizer now tries a 24px short
stub that creates a perpendicular dog-leg exit avoiding the blocking node.

Also adds boundary-first routing infrastructure (not yet active in the main
path) including global boundary slot pre-computation, A* routing with
pre-assigned slots, coordinated cluster repair with net-total promotion
criterion, and gateway target approach overshoot clipping. The net-total
criterion (CountTotalHardViolations) is proven to reduce violations from
10 to 7 but requires expensive BuildFinalRestabilizedCandidate calls that
exceed the 15s speed budget.

Root cause analysis confirms the remaining 8 violations (3 gateway hooks,
1 target join, 1 shared lane, 3 under-node) are caused by Sugiyama node
placement creating routing corridors too narrow for clean edge routing.
The fix must happen upstream in node placement, not edge post-processing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
master
2026-03-29 23:39:02 +03:00
parent e8f7ad7652
commit d894a8a349
10 changed files with 846 additions and 12 deletions

View File

@@ -65,6 +65,29 @@ internal static partial class ElkEdgeRouterIterative
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)}");
}
}
return current;
}