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:
@@ -0,0 +1,121 @@
|
||||
namespace StellaOps.ElkSharp;
|
||||
|
||||
internal static partial class ElkEdgeRouterIterative
|
||||
{
|
||||
private static CandidateSolution? TryBoundaryFirstBaseline(
|
||||
ElkRoutedEdge[] baselineEdges,
|
||||
ElkPositionedNode[] nodes,
|
||||
ElkLayoutOptions layoutOptions,
|
||||
RoutingStrategy strategy,
|
||||
double minLineClearance,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var graphMinY = nodes.Length > 0 ? nodes.Min(n => n.Y) : 0d;
|
||||
var graphMaxY = nodes.Length > 0 ? nodes.Max(n => n.Y + n.Height) : 0d;
|
||||
|
||||
// Phase 1: Compute boundary slot assignments for all routable edges
|
||||
// (needed for correct slot positions), then filter to only violating edges.
|
||||
var allAssignments = ComputeGlobalBoundarySlotAssignments(
|
||||
baselineEdges, nodes, graphMinY, graphMaxY);
|
||||
if (allAssignments.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Identify edges with violations in the baseline.
|
||||
var violatingSeverity = new Dictionary<string, int>(StringComparer.Ordinal);
|
||||
ElkEdgeRoutingScoring.CountBadBoundaryAngles(baselineEdges, nodes, violatingSeverity, 10);
|
||||
ElkEdgeRoutingScoring.CountGatewaySourceExitViolations(baselineEdges, nodes, violatingSeverity, 10);
|
||||
ElkEdgeRoutingScoring.CountTargetApproachJoinViolations(baselineEdges, nodes, violatingSeverity, 10);
|
||||
ElkEdgeRoutingScoring.CountTargetApproachBacktrackingViolations(baselineEdges, nodes, violatingSeverity, 10);
|
||||
ElkEdgeRoutingScoring.CountSharedLaneViolations(baselineEdges, nodes, violatingSeverity, 10);
|
||||
ElkEdgeRoutingScoring.CountBoundarySlotViolations(baselineEdges, nodes, violatingSeverity, 10);
|
||||
ElkEdgeRoutingScoring.CountUnderNodeViolations(baselineEdges, nodes, violatingSeverity, 10);
|
||||
ElkEdgeRoutingScoring.CountExcessiveDetourViolations(baselineEdges, nodes, violatingSeverity, 10);
|
||||
ElkEdgeRoutingScoring.CountBelowGraphViolations(baselineEdges, nodes, violatingSeverity, 10);
|
||||
|
||||
if (violatingSeverity.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Expand to include neighbor edges sharing source/target nodes.
|
||||
var repairEdgeIds = ExpandWinningSolutionFocus(baselineEdges, violatingSeverity.Keys)
|
||||
.ToHashSet(StringComparer.Ordinal);
|
||||
|
||||
// Filter assignments to only repair edges.
|
||||
var assignments = new Dictionary<string, BoundaryFirstAssignment>(StringComparer.Ordinal);
|
||||
foreach (var (edgeId, assignment) in allAssignments)
|
||||
{
|
||||
if (repairEdgeIds.Contains(edgeId))
|
||||
{
|
||||
assignments[edgeId] = assignment;
|
||||
}
|
||||
}
|
||||
|
||||
if (assignments.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
ElkLayoutDiagnostics.LogProgress(
|
||||
$"Boundary-first: {allAssignments.Count} slots computed, {assignments.Count}/{repairEdgeIds.Count} edges targeted for repair");
|
||||
|
||||
// Phase 2: Route only the targeted edges between pre-assigned boundary slots.
|
||||
// Non-targeted edges keep their baseline paths and contribute soft obstacles.
|
||||
var routed = RouteBoundaryFirstEdges(
|
||||
baselineEdges,
|
||||
nodes,
|
||||
assignments,
|
||||
strategy,
|
||||
cancellationToken);
|
||||
|
||||
ElkLayoutDiagnostics.LogProgress(
|
||||
$"Boundary-first: routed={routed.Diagnostics.RoutedEdges} " +
|
||||
$"skipped={routed.Diagnostics.SkippedEdges} " +
|
||||
$"sections={routed.Diagnostics.RoutedSections} " +
|
||||
$"fallback={routed.Diagnostics.FallbackSections}");
|
||||
|
||||
// Phase 3: Apply lean verification pass.
|
||||
var verified = ApplyBoundaryFirstVerification(
|
||||
routed.Edges,
|
||||
baselineEdges,
|
||||
nodes,
|
||||
layoutOptions.Direction,
|
||||
minLineClearance);
|
||||
|
||||
// Score and build candidate.
|
||||
var score = ElkEdgeRoutingScoring.ComputeScore(verified, nodes);
|
||||
var brokenHighways = HighwayProcessingEnabled
|
||||
? ElkEdgeRouterHighway.DetectRemainingBrokenHighways(verified, nodes).Count
|
||||
: 0;
|
||||
var retryState = BuildRetryState(score, brokenHighways);
|
||||
|
||||
ElkLayoutDiagnostics.LogProgress(
|
||||
$"Boundary-first result: score={score.Value:F0} retry={DescribeRetryState(retryState)}");
|
||||
|
||||
// Record in diagnostics if available.
|
||||
var diagnostics = ElkLayoutDiagnostics.Current;
|
||||
if (diagnostics is not null)
|
||||
{
|
||||
lock (diagnostics.SyncRoot)
|
||||
{
|
||||
diagnostics.IterativeStrategies.Add(new ElkIterativeStrategyDiagnostics
|
||||
{
|
||||
StrategyIndex = -1,
|
||||
OrderingName = "boundary-first",
|
||||
Attempts = 1,
|
||||
BestScore = score,
|
||||
Outcome = retryState.RequiresPrimaryRetry
|
||||
? $"retry({DescribeRetryState(retryState)})"
|
||||
: "valid",
|
||||
BestEdges = verified,
|
||||
});
|
||||
}
|
||||
|
||||
ElkLayoutDiagnostics.FlushSnapshot(diagnostics);
|
||||
}
|
||||
|
||||
return new CandidateSolution(score, retryState, verified, -1);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user