Fix under-node violations with corridor routing and push-down

Two under-node fix strategies in the winner refinement:

1. Long sweeps (> 40% graph width): route through top corridor at
   graphMinY - 56, with perpendicular exit stub. Fixes edge/20.

2. Medium sweeps near graph bottom: route through bottom corridor at
   graphMaxY + 32 when the safe push-down Y would exceed graph bounds.
   Fixes edge/25 (was 29px gap, now routes below blocking nodes).

Both under-node geometry violations eliminated. Edge/25 gains a
below-graph flag (Y=803 vs graphMaxY=771) which the FinalScore
adjustment handles as a corridor routing pattern.

Also adds target-join face reassignment infrastructure (redirects
outer edge to target's right face) — evaluates but not yet promoted
for the current fixture.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
master
2026-03-30 10:21:48 +03:00
parent 77bb608325
commit 24e8ddd296
3 changed files with 282 additions and 46 deletions

View File

@@ -89,18 +89,17 @@ internal static partial class ElkEdgeRouterIterative
}
// Reroute long horizontal sweeps through the top corridor.
// Edges spanning > half the graph width with under-node violations
// Edges spanning > 40% graph width with under-node violations
// should route above the graph (like backward edges) instead of
// cutting straight through the node field.
// Also pushes medium-length sweeps below blocking nodes.
// Each fix type is evaluated INDEPENDENTLY to prevent one fix's
// detour from blocking another fix's under-node improvement.
if (current.RetryState.UnderNodeViolations > 0)
{
var corridorCandidate = RerouteLongSweepsThroughCorridor(current.Edges, nodes, direction, minLineClearance);
if (corridorCandidate is not null)
{
// Skip NormalizeBoundaryAngles for corridor-rerouted edges —
// the normalization's NormalizeExitPath collapses corridor
// vertical segments. The corridor path already has a correct
// perpendicular exit stub.
var corridorScore = ElkEdgeRoutingScoring.ComputeScore(corridorCandidate, nodes);
if (corridorScore.Value > current.Score.Value
&& corridorScore.NodeCrossings <= current.Score.NodeCrossings)
@@ -144,6 +143,29 @@ internal static partial class ElkEdgeRouterIterative
}
}
// Target-join face reassignment: when two edges converge on the
// same target face with inadequate separation, redirect the outer
// edge to the target's adjacent face (right side for LTR layout).
if (current.RetryState.TargetApproachJoinViolations > 0)
{
var joinCandidate = ReassignConvergentTargetFace(current.Edges, nodes, direction);
if (joinCandidate is not null)
{
var joinScore = ElkEdgeRoutingScoring.ComputeScore(joinCandidate, nodes);
if (joinScore.Value > current.Score.Value
&& joinScore.NodeCrossings <= current.Score.NodeCrossings)
{
var joinRetry = BuildRetryState(
joinScore,
HighwayProcessingEnabled
? ElkEdgeRouterHighway.DetectRemainingBrokenHighways(joinCandidate, nodes).Count
: 0);
current = current with { Score = joinScore, RetryState = joinRetry, Edges = joinCandidate };
ElkLayoutDiagnostics.LogProgress($"Hybrid winner refinement after target-join reassignment: {DescribeSolution(current)}");
}
}
}
return current;
}