Route long sweeps through top corridor unconditionally
Long horizontal sweeps (>40% graph width) now always route through the top corridor instead of cutting through the node field. Each successive corridor edge gets a 24px Y offset to prevent convergence. Remaining: target-join at End/top (two corridor routes converge on descent) and edge/9 flush under-node. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -27,7 +27,40 @@ internal static partial class ElkEdgeRouterIterative
|
||||
|
||||
var underNodeSeverity = new Dictionary<string, int>(StringComparer.Ordinal);
|
||||
ElkEdgeRoutingScoring.CountUnderNodeViolations(edges, nodes, underNodeSeverity, 1);
|
||||
if (underNodeSeverity.Count == 0)
|
||||
|
||||
// Also identify long sweeps that cut through the node field — they
|
||||
// need corridor routing for visual clarity even without under-node
|
||||
// violations. A 3000px horizontal line through the graph is ugly.
|
||||
var longSweepEdgeIds = new HashSet<string>(StringComparer.Ordinal);
|
||||
for (var edgeIndex = 0; edgeIndex < edges.Length; edgeIndex++)
|
||||
{
|
||||
var edge = edges[edgeIndex];
|
||||
if (underNodeSeverity.ContainsKey(edge.Id))
|
||||
{
|
||||
continue; // already handled
|
||||
}
|
||||
|
||||
var path = ExtractPath(edge);
|
||||
for (var i = 0; i < path.Count - 1; i++)
|
||||
{
|
||||
if (Math.Abs(path[i].Y - path[i + 1].Y) > 2d)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var segLength = Math.Abs(path[i + 1].X - path[i].X);
|
||||
var laneY = path[i].Y;
|
||||
if (segLength >= minSweepLength
|
||||
&& laneY > graphMinY - 10d
|
||||
&& laneY < graphMaxY + 10d)
|
||||
{
|
||||
longSweepEdgeIds.Add(edge.Id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (underNodeSeverity.Count == 0 && longSweepEdgeIds.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
@@ -37,7 +70,7 @@ internal static partial class ElkEdgeRouterIterative
|
||||
for (var edgeIndex = 0; edgeIndex < edges.Length; edgeIndex++)
|
||||
{
|
||||
var edge = edges[edgeIndex];
|
||||
if (!underNodeSeverity.ContainsKey(edge.Id))
|
||||
if (!underNodeSeverity.ContainsKey(edge.Id) && !longSweepEdgeIds.Contains(edge.Id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -76,79 +109,25 @@ internal static partial class ElkEdgeRouterIterative
|
||||
|
||||
if (bestSegLength >= minSweepLength)
|
||||
{
|
||||
// Long sweep: try to push the horizontal segment below all
|
||||
// blocking nodes first. Only use the top corridor as a
|
||||
// fallback when the safe Y would exceed the graph boundary
|
||||
// (the corridor creates distant approach stubs that disrupt
|
||||
// boundary-slot assignments on the rerouted edges).
|
||||
var laneY = path[bestSegStart].Y;
|
||||
var maxBlockBottom = 0d;
|
||||
var minX = Math.Min(path[bestSegStart].X, path[bestSegStart + 1].X);
|
||||
var maxX = Math.Max(path[bestSegStart].X, path[bestSegStart + 1].X);
|
||||
|
||||
foreach (var node in nodes)
|
||||
// Long sweep (>40% graph width): route through the top corridor.
|
||||
// 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>
|
||||
{
|
||||
if (string.Equals(node.Id, edge.SourceNodeId, StringComparison.Ordinal)
|
||||
|| string.Equals(node.Id, edge.TargetNodeId, StringComparison.Ordinal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
sourcePoint,
|
||||
new() { X = stubX, Y = sourcePoint.Y },
|
||||
new() { X = stubX, Y = corridorY },
|
||||
new() { X = approachX, Y = corridorY },
|
||||
targetPoint,
|
||||
};
|
||||
|
||||
if (maxX <= node.X + 0.5d || minX >= 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);
|
||||
}
|
||||
}
|
||||
|
||||
var pushY = maxBlockBottom + minLineClearance + 4d;
|
||||
if (maxBlockBottom > 0d && pushY <= graphMaxY - 4d)
|
||||
{
|
||||
// Safe push within the graph -- shift only the under-node
|
||||
// horizontal segment without changing approach geometry.
|
||||
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 if (maxBlockBottom > 0d)
|
||||
{
|
||||
// Push would exceed graph boundary -- use top corridor.
|
||||
var exitX = sourcePoint.X;
|
||||
var approachX = targetPoint.X;
|
||||
var stubX = exitX + 24d;
|
||||
var newPath = new List<ElkPoint>
|
||||
{
|
||||
sourcePoint,
|
||||
new() { X = stubX, Y = sourcePoint.Y },
|
||||
new() { X = stubX, Y = corridorY },
|
||||
new() { X = approachX, Y = corridorY },
|
||||
targetPoint,
|
||||
};
|
||||
|
||||
result = ReplaceEdgePath(result, edges, edgeIndex, edge, newPath);
|
||||
ElkLayoutDiagnostics.LogProgress(
|
||||
$"Corridor reroute: {edge.Id} sweep={bestSegLength:F0}px to corridorY={corridorY:F0}");
|
||||
}
|
||||
result = ReplaceEdgePath(result, edges, edgeIndex, edge, newPath);
|
||||
ElkLayoutDiagnostics.LogProgress(
|
||||
$"Corridor reroute: {edge.Id} sweep={bestSegLength:F0}px to corridorY={corridorY:F0}");
|
||||
}
|
||||
else if (bestSegLength >= 500d)
|
||||
{
|
||||
|
||||
@@ -331,6 +331,65 @@ internal static partial class ElkEdgeRouterIterative
|
||||
current = RepairRemainingEdgeNodeCrossings(current, nodes);
|
||||
}
|
||||
|
||||
// Unconditional corridor reroute for long sweeps through the node field.
|
||||
// The score-gated corridor reroute earlier may reject these because the
|
||||
// corridor candidate scores worse (more bends, longer paths). But visually,
|
||||
// long horizontal highways through the graph are ugly. Apply unconditionally.
|
||||
{
|
||||
var graphMinYLocal = nodes.Min(n => n.Y);
|
||||
var graphWidthLocal = nodes.Max(n => n.X + n.Width) - nodes.Min(n => n.X);
|
||||
var baseCorridorY = graphMinYLocal - 56d;
|
||||
var localMinSweep = graphWidthLocal * 0.4d;
|
||||
var corridorResult = current.Edges.ToArray();
|
||||
var corridorFixed = 0;
|
||||
for (var ei = 0; ei < corridorResult.Length; ei++)
|
||||
{
|
||||
var edge = corridorResult[ei];
|
||||
var path = ExtractPath(edge);
|
||||
for (var si = 0; si < path.Count - 1; si++)
|
||||
{
|
||||
if (Math.Abs(path[si].Y - path[si + 1].Y) > 2d) continue;
|
||||
var segLen = Math.Abs(path[si + 1].X - path[si].X);
|
||||
var laneY = path[si].Y;
|
||||
if (segLen < localMinSweep || laneY <= graphMinYLocal - 10d) continue;
|
||||
// Route through top corridor. Offset each successive edge
|
||||
// so multiple corridor routes don't converge at the same Y.
|
||||
var localCorridorY = baseCorridorY - (corridorFixed * 24d);
|
||||
var src = path[0];
|
||||
var tgt = path[^1];
|
||||
var stubX = src.X + 24d;
|
||||
var newPath = new List<ElkPoint>
|
||||
{
|
||||
src,
|
||||
new() { X = stubX, Y = src.Y },
|
||||
new() { X = stubX, Y = localCorridorY },
|
||||
new() { X = tgt.X, Y = localCorridorY },
|
||||
tgt,
|
||||
};
|
||||
corridorResult[ei] = new ElkRoutedEdge
|
||||
{
|
||||
Id = edge.Id, SourceNodeId = edge.SourceNodeId, TargetNodeId = edge.TargetNodeId,
|
||||
SourcePortId = edge.SourcePortId, TargetPortId = edge.TargetPortId,
|
||||
Kind = edge.Kind, Label = edge.Label,
|
||||
Sections = [new ElkEdgeSection
|
||||
{
|
||||
StartPoint = newPath[0], EndPoint = newPath[^1],
|
||||
BendPoints = newPath.Skip(1).Take(newPath.Count - 2).ToArray(),
|
||||
}],
|
||||
};
|
||||
corridorFixed++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (corridorFixed > 0)
|
||||
{
|
||||
var corridorScore = ElkEdgeRoutingScoring.ComputeScore(corridorResult, nodes);
|
||||
current = current with { Score = corridorScore, Edges = corridorResult };
|
||||
ElkLayoutDiagnostics.LogProgress(
|
||||
$"Unconditional corridor reroute: {corridorFixed} edges to corridor");
|
||||
}
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user