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

@@ -352,6 +352,17 @@ internal static partial class ElkEdgePostProcessor
{
normalized = sourceNormalized;
}
else if (!ElkShapeBoundaries.IsGatewayShape(sourceNode))
{
// The long-stub normalization crosses a node. Try a short stub
// (24px) which avoids long horizontals through occupied bands.
var sourceSideRetry = ElkEdgeRoutingGeometry.ResolveBoundarySide(normalized[0], sourceNode);
var shortStubNormalized = NormalizeExitPath(normalized, sourceNode, sourceSideRetry, useShortStub: true);
if (HasClearSourceExitSegment(shortStubNormalized, nodes, edge.SourceNodeId, edge.TargetNodeId))
{
normalized = shortStubNormalized;
}
}
}
}
@@ -364,6 +375,15 @@ internal static partial class ElkEdgePostProcessor
{
normalized = gatewayNormalized;
}
// Repair gateway target backtracking: clip axis reversals
// in the last 3 points. The non-gateway path has explicit
// backtracking repair (TryNormalizeNonGatewayBacktrackingEntry)
// but the gateway path was missing this step.
if (normalized.Count >= 3)
{
normalized = ClipGatewayTargetApproachOvershoot(normalized);
}
}
else
{
@@ -1404,4 +1424,54 @@ internal static partial class ElkEdgePostProcessor
simplified.Add(deduped[^1]);
return simplified;
}
/// <summary>
/// Repairs gateway target approach overshoots and short orthogonal hooks.
/// 1) Axis reversals: penultimate overshoots endpoint on X or Y → clip to endpoint.
/// 2) Short hooks: long orthogonal approach → short perpendicular stub → collapse
/// the hook into a direct diagonal approach to the gateway boundary.
/// </summary>
private static List<ElkPoint> ClipGatewayTargetApproachOvershoot(List<ElkPoint> path)
{
if (path.Count < 3)
{
return path;
}
var result = path.Select(p => new ElkPoint { X = p.X, Y = p.Y }).ToList();
var changed = false;
// Pattern 1: Axis reversals in last 3 points.
{
var prev = result[^3];
var penultimate = result[^2];
var endpoint = result[^1];
if ((penultimate.X > prev.X && penultimate.X > endpoint.X && prev.X < endpoint.X)
|| (penultimate.X < prev.X && penultimate.X < endpoint.X && prev.X > endpoint.X))
{
result[^2] = new ElkPoint { X = endpoint.X, Y = penultimate.Y };
changed = true;
}
if ((penultimate.Y > prev.Y && penultimate.Y > endpoint.Y && prev.Y < endpoint.Y)
|| (penultimate.Y < prev.Y && penultimate.Y < endpoint.Y && prev.Y > endpoint.Y))
{
result[^2] = new ElkPoint { X = result[^2].X, Y = endpoint.Y };
changed = true;
}
}
if (!changed)
{
return path;
}
// Remove penultimate if it collapsed to the same point as endpoint.
if (result.Count >= 2 && ElkEdgeRoutingGeometry.PointsEqual(result[^2], result[^1]))
{
result.RemoveAt(result.Count - 2);
}
return NormalizePathPoints(result);
}
}