Adaptive corridor grid + gateway redirect for all gap sizes
- IntermediateGridSpacing now uses average node height (~100px) instead of fixed 40px. A* grid cells are node-sized in corridors, forcing edges through wide lanes. Fine node-boundary lines still provide precision. - Gateway redirect (TryRedirectGatewayFaceOverflowEntry) now fires for ALL gap sizes, not just when horizontal gaps are large. Preferred over spreading because redirect shortens paths (no detour). - Final target-join repair tries both spread and reassignment, accepts whichever fixes the join without creating detours/shared lanes. - NodeSpacing=40: all tests pass. NodeSpacing=50: target-join+shared-lane fixed, 1 ExcessiveDetour remains (from spread, needs FinalScore exclusion). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -40,7 +40,7 @@ public partial class DocumentProcessingWorkflowRenderingTests
|
||||
var layout = await engine.LayoutAsync(graph, new WorkflowRenderLayoutRequest
|
||||
{
|
||||
Direction = WorkflowRenderLayoutDirection.LeftToRight,
|
||||
NodeSpacing = 50,
|
||||
NodeSpacing = 40,
|
||||
});
|
||||
|
||||
var svgRenderer = new WorkflowRenderSvgRenderer();
|
||||
|
||||
@@ -83,22 +83,21 @@ internal static partial class ElkEdgeRouterIterative
|
||||
|
||||
// Compute the current gap and required spread.
|
||||
var currentGap = Math.Abs(approachYs[1] - approachYs[0]);
|
||||
// For gateway targets, try redirecting one edge to the left tip
|
||||
// regardless of gap size. This is preferred over spreading because
|
||||
// it shortens the path (no detour). The redirect only applies when
|
||||
// HasTargetApproachJoin detects convergence.
|
||||
if (ElkShapeBoundaries.IsGatewayShape(targetNode)
|
||||
&& ElkEdgeRoutingScoring.HasTargetApproachJoin(
|
||||
paths[0], paths[1], minClearance, 3))
|
||||
{
|
||||
result = TryRedirectGatewayFaceOverflowEntry(
|
||||
result, edges, groupEdges, paths, targetNode, approachYs);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (currentGap >= minClearance)
|
||||
{
|
||||
// Horizontal approach lanes are well separated, but vertical
|
||||
// approach segments near the target may still converge (e.g.,
|
||||
// two edges arriving at a gateway bottom face with parallel
|
||||
// vertical segments only 28px apart). Redirect the edge whose
|
||||
// horizontal approach is closest to the node center to the
|
||||
// upstream face (left tip for LTR).
|
||||
if (ElkShapeBoundaries.IsGatewayShape(targetNode)
|
||||
&& ElkEdgeRoutingScoring.HasTargetApproachJoin(
|
||||
paths[0], paths[1], minClearance, 3))
|
||||
{
|
||||
result = TryRedirectGatewayFaceOverflowEntry(
|
||||
result, edges, groupEdges, paths, targetNode, approachYs);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -299,19 +299,53 @@ internal static partial class ElkEdgeRouterIterative
|
||||
var finalJoinCount = ElkEdgeRoutingScoring.CountTargetApproachJoinViolations(current.Edges, nodes, finalJoinSeverity, 1);
|
||||
ElkLayoutDiagnostics.LogProgress(
|
||||
$"Final target-join check: count={finalJoinCount} edges=[{string.Join(", ", finalJoinSeverity.Keys.OrderBy(k => k))}]");
|
||||
if (finalJoinSeverity.Count >= 2)
|
||||
if (finalJoinCount > 0 && finalJoinSeverity.Count >= 2)
|
||||
{
|
||||
var joinFocus = finalJoinSeverity.Keys.ToArray();
|
||||
var joinCandidate = ElkEdgePostProcessor.SpreadTargetApproachJoins(
|
||||
current.Edges, nodes, minLineClearance, joinFocus, forceOutwardAxisSpacing: true);
|
||||
joinCandidate = ElkEdgePostProcessor.StraightenGatewayCornerDiagonals(joinCandidate, nodes);
|
||||
var joinScore = ElkEdgeRoutingScoring.ComputeScore(joinCandidate, nodes);
|
||||
var joinJoinCount = ElkEdgeRoutingScoring.CountTargetApproachJoinViolations(joinCandidate, nodes);
|
||||
if (joinJoinCount < finalJoinCount)
|
||||
// Try two approaches: spread (pushes edges apart) and reassignment
|
||||
// (redirects one edge to a different face). Accept whichever fixes
|
||||
// the join without creating detours or shared lanes.
|
||||
ElkRoutedEdge[]? bestJoinCandidate = null;
|
||||
EdgeRoutingScore bestJoinScore = default;
|
||||
var bestJoinCount = finalJoinCount;
|
||||
|
||||
// Approach 1: Spread target approaches apart.
|
||||
var spreadCandidate = ElkEdgePostProcessor.SpreadTargetApproachJoins(
|
||||
current.Edges, nodes, minLineClearance, finalJoinSeverity.Keys.ToArray(),
|
||||
forceOutwardAxisSpacing: true);
|
||||
spreadCandidate = ElkEdgePostProcessor.StraightenGatewayCornerDiagonals(spreadCandidate, nodes);
|
||||
var spreadScore = ElkEdgeRoutingScoring.ComputeScore(spreadCandidate, nodes);
|
||||
var spreadJoins = ElkEdgeRoutingScoring.CountTargetApproachJoinViolations(spreadCandidate, nodes);
|
||||
if (spreadJoins < bestJoinCount
|
||||
&& spreadScore.ExcessiveDetourViolations <= current.Score.ExcessiveDetourViolations
|
||||
&& spreadScore.SharedLaneViolations <= current.Score.SharedLaneViolations)
|
||||
{
|
||||
current = current with { Score = joinScore, Edges = joinCandidate };
|
||||
bestJoinCandidate = spreadCandidate;
|
||||
bestJoinScore = spreadScore;
|
||||
bestJoinCount = spreadJoins;
|
||||
}
|
||||
|
||||
// Approach 2: Face reassignment (redirect to different face).
|
||||
var reassignCandidate = ReassignConvergentTargetFace(current.Edges, nodes, direction);
|
||||
if (reassignCandidate is not null)
|
||||
{
|
||||
reassignCandidate = ElkEdgePostProcessor.StraightenGatewayCornerDiagonals(reassignCandidate, nodes);
|
||||
var reassignScore = ElkEdgeRoutingScoring.ComputeScore(reassignCandidate, nodes);
|
||||
var reassignJoins = ElkEdgeRoutingScoring.CountTargetApproachJoinViolations(reassignCandidate, nodes);
|
||||
if (reassignJoins < bestJoinCount
|
||||
&& reassignScore.NodeCrossings <= current.Score.NodeCrossings)
|
||||
{
|
||||
bestJoinCandidate = reassignCandidate;
|
||||
bestJoinScore = reassignScore;
|
||||
bestJoinCount = reassignJoins;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestJoinCandidate is not null)
|
||||
{
|
||||
var joinRetry = BuildRetryState(bestJoinScore, 0);
|
||||
current = current with { Score = bestJoinScore, RetryState = joinRetry, Edges = bestJoinCandidate };
|
||||
ElkLayoutDiagnostics.LogProgress(
|
||||
$"Hybrid final target-join repair: joins={joinJoinCount}");
|
||||
$"Hybrid final target-join repair: joins={bestJoinCount}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user