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

@@ -0,0 +1,36 @@
namespace StellaOps.ElkSharp;
internal static partial class ElkEdgeRouterIterative
{
private static ElkRoutedEdge[] ApplyBoundaryFirstVerification(
ElkRoutedEdge[] edges,
ElkRoutedEdge[] originalEdges,
ElkPositionedNode[] nodes,
ElkLayoutDirection direction,
double minLineClearance)
{
// Minimal structural safety only — the hybrid winner refinement
// handles remaining violations after boundary-first is promoted.
var result = ElkEdgePostProcessor.AvoidNodeCrossings(edges, nodes, direction);
result = ElkEdgePostProcessor.EliminateDiagonalSegments(result, nodes);
result = ElkEdgePostProcessorSimplify.SimplifyEdgePaths(result, nodes);
result = ElkEdgePostProcessorSimplify.TightenOuterCorridors(result, nodes);
if (HighwayProcessingEnabled)
{
result = ElkEdgeRouterHighway.BreakShortHighways(result, nodes);
}
// Normalize boundary geometry for the slot-pinned endpoints.
result = ElkEdgePostProcessor.NormalizeBoundaryAngles(result, nodes);
result = ElkEdgePostProcessor.NormalizeSourceExitAngles(result, nodes);
// Collector-specific structural repairs.
result = RestoreProtectedRepeatCollectorCorridors(result, originalEdges, nodes);
// Below-graph clamping and final crossing check.
result = ClampBelowGraphEdges(result, nodes);
result = ElkEdgePostProcessor.AvoidNodeCrossings(result, nodes, direction);
return result;
}
}