Fix corridor reroute: push-first for under-node, corridor for visual
Restored push-first approach for long sweeps WITH under-node violations (NodeSpacing=40 needs small Y adjustments, not corridor routing). Corridor-only for visual sweeps WITHOUT under-node violations (handled by unconditional corridor in winner refinement). Corridor offset uses node-size clearance + 4px (not spacing-scaled) to avoid repeat-collector conflicts. Gated on no new repeat-collector or node-crossing regressions. Both NodeSpacing=40 and NodeSpacing=50 pass all 44+ assertions. NodeSpacing=50 set as test default (visually cleaner, 56s vs 2m43s). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -109,25 +109,52 @@ internal static partial class ElkEdgeRouterIterative
|
|||||||
|
|
||||||
if (bestSegLength >= minSweepLength)
|
if (bestSegLength >= minSweepLength)
|
||||||
{
|
{
|
||||||
// Long sweep (>40% graph width): route through the top corridor.
|
if (underNodeSeverity.ContainsKey(edge.Id))
|
||||||
// These are "highway" edges that span the graph and look ugly
|
|
||||||
// cutting through the node field. The corridor moves them above
|
|
||||||
// the graph for clean visual separation.
|
|
||||||
var exitX = sourcePoint.X;
|
|
||||||
var approachX = targetPoint.X;
|
|
||||||
var stubX = exitX + 24d;
|
|
||||||
var newPath = new List<ElkPoint>
|
|
||||||
{
|
{
|
||||||
sourcePoint,
|
// Under-node long sweep: push below blocking nodes first.
|
||||||
new() { X = stubX, Y = sourcePoint.Y },
|
var laneY = path[bestSegStart].Y;
|
||||||
new() { X = stubX, Y = corridorY },
|
var maxBlockBottom = 0d;
|
||||||
new() { X = approachX, Y = corridorY },
|
var pushMinX = Math.Min(path[bestSegStart].X, path[bestSegStart + 1].X);
|
||||||
targetPoint,
|
var pushMaxX = Math.Max(path[bestSegStart].X, path[bestSegStart + 1].X);
|
||||||
};
|
foreach (var node in nodes)
|
||||||
|
{
|
||||||
|
if (string.Equals(node.Id, edge.SourceNodeId, StringComparison.Ordinal)
|
||||||
|
|| string.Equals(node.Id, edge.TargetNodeId, StringComparison.Ordinal))
|
||||||
|
continue;
|
||||||
|
if (pushMaxX <= node.X + 0.5d || pushMinX >= node.X + node.Width - 0.5d)
|
||||||
|
continue;
|
||||||
|
var nodeBottom = node.Y + node.Height;
|
||||||
|
var gap = laneY - nodeBottom;
|
||||||
|
if (gap > -4d && gap < minLineClearance)
|
||||||
|
maxBlockBottom = Math.Max(maxBlockBottom, nodeBottom);
|
||||||
|
}
|
||||||
|
|
||||||
result = ReplaceEdgePath(result, edges, edgeIndex, edge, newPath);
|
if (maxBlockBottom > 0d)
|
||||||
ElkLayoutDiagnostics.LogProgress(
|
{
|
||||||
$"Corridor reroute: {edge.Id} sweep={bestSegLength:F0}px to corridorY={corridorY:F0}");
|
var pushY = maxBlockBottom + minLineClearance + 4d;
|
||||||
|
if (pushY <= graphMaxY - 4d)
|
||||||
|
{
|
||||||
|
var newPath = new List<ElkPoint>(path.Count);
|
||||||
|
for (var pi = 0; pi < path.Count; pi++)
|
||||||
|
{
|
||||||
|
if (pi >= bestSegStart && pi <= bestSegStart + 1
|
||||||
|
&& Math.Abs(path[pi].Y - laneY) <= 2d)
|
||||||
|
newPath.Add(new ElkPoint { X = path[pi].X, Y = pushY });
|
||||||
|
else
|
||||||
|
newPath.Add(path[pi]);
|
||||||
|
}
|
||||||
|
|
||||||
|
result = ReplaceEdgePath(result, edges, edgeIndex, edge, newPath);
|
||||||
|
ElkLayoutDiagnostics.LogProgress(
|
||||||
|
$"Under-node push: {edge.Id} from Y={laneY:F0} to Y={pushY:F0} (blocker bottom={maxBlockBottom:F0})");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Visual-only long sweep: corridor route (handled by
|
||||||
|
// unconditional corridor in the winner refinement).
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if (bestSegLength >= 500d)
|
else if (bestSegLength >= 500d)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -312,7 +312,11 @@ internal static partial class ElkEdgeRouterIterative
|
|||||||
var segLen = Math.Abs(cpath[si + 1].X - cpath[si].X);
|
var segLen = Math.Abs(cpath[si + 1].X - cpath[si].X);
|
||||||
var laneY = cpath[si].Y;
|
var laneY = cpath[si].Y;
|
||||||
if (segLen < localMinSweep || laneY <= graphMinYLocal - 10d) continue;
|
if (segLen < localMinSweep || laneY <= graphMinYLocal - 10d) continue;
|
||||||
var localCorridorY = baseCorridorY - (corridorFixed * 24d);
|
// Offset must exceed the target-join detection threshold
|
||||||
|
// (node-size clearance, not spacing-scaled) so parallel
|
||||||
|
// corridor segments aren't flagged as joins.
|
||||||
|
var nodeSizeClearance = ElkEdgeRoutingScoring.ResolveNodeSizeClearance(nodes);
|
||||||
|
var localCorridorY = baseCorridorY - (corridorFixed * (nodeSizeClearance + 4d));
|
||||||
var src = cpath[0];
|
var src = cpath[0];
|
||||||
var tgt = cpath[^1];
|
var tgt = cpath[^1];
|
||||||
var stubX = src.X + 24d;
|
var stubX = src.X + 24d;
|
||||||
@@ -342,9 +346,14 @@ internal static partial class ElkEdgeRouterIterative
|
|||||||
if (corridorFixed > 0)
|
if (corridorFixed > 0)
|
||||||
{
|
{
|
||||||
var corridorScore = ElkEdgeRoutingScoring.ComputeScore(corridorResult, nodes);
|
var corridorScore = ElkEdgeRoutingScoring.ComputeScore(corridorResult, nodes);
|
||||||
current = current with { Score = corridorScore, Edges = corridorResult };
|
// Accept only if no new repeat-collector or node-crossing regressions.
|
||||||
ElkLayoutDiagnostics.LogProgress(
|
if (corridorScore.RepeatCollectorCorridorViolations <= current.Score.RepeatCollectorCorridorViolations
|
||||||
$"Unconditional corridor reroute: {corridorFixed} edges to corridor");
|
&& corridorScore.NodeCrossings <= current.Score.NodeCrossings)
|
||||||
|
{
|
||||||
|
current = current with { Score = corridorScore, Edges = corridorResult };
|
||||||
|
ElkLayoutDiagnostics.LogProgress(
|
||||||
|
$"Unconditional corridor reroute: {corridorFixed} edges to corridor");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -247,7 +247,7 @@ internal static partial class ElkEdgeRoutingScoring
|
|||||||
/// face-related detections (target-join, boundary-slot) where the gap
|
/// face-related detections (target-join, boundary-slot) where the gap
|
||||||
/// depends on node face geometry, not inter-node routing corridors.
|
/// depends on node face geometry, not inter-node routing corridors.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static double ResolveNodeSizeClearance(IReadOnlyCollection<ElkPositionedNode> nodes)
|
internal static double ResolveNodeSizeClearance(IReadOnlyCollection<ElkPositionedNode> nodes)
|
||||||
{
|
{
|
||||||
var serviceNodes = nodes.Where(n => n.Kind is not "Start" and not "End").ToArray();
|
var serviceNodes = nodes.Where(n => n.Kind is not "Start" and not "End").ToArray();
|
||||||
return serviceNodes.Length > 0
|
return serviceNodes.Length > 0
|
||||||
|
|||||||
Reference in New Issue
Block a user