Reroute long horizontal sweeps through top corridor
Detects horizontal segments > 40% of graph width with under-node violations and reroutes them through the top corridor (Y = graphMinY - 56), similar to backward edge routing. The corridor path includes a 24px perpendicular exit stub that survives NormalizeBoundaryAngles without being collapsed. Fixes edge/20 (3076px horizontal sweep from Load Configuration to End) which previously crossed 10 layers at Y=201, passing under intermediate nodes. Now routes above the graph at Y=-24. Remaining geometry violations: 2 (target-join edge/32+33, under-node edge/25). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
namespace StellaOps.ElkSharp;
|
||||
|
||||
internal static partial class ElkEdgeRouterIterative
|
||||
{
|
||||
/// <summary>
|
||||
/// Detects long horizontal sweeps with under-node violations and reroutes
|
||||
/// them through the top corridor (above the graph), similar to how backward
|
||||
/// edges are routed through external corridors.
|
||||
/// </summary>
|
||||
private static ElkRoutedEdge[]? RerouteLongSweepsThroughCorridor(
|
||||
ElkRoutedEdge[] edges,
|
||||
ElkPositionedNode[] nodes,
|
||||
ElkLayoutDirection direction,
|
||||
double minLineClearance)
|
||||
{
|
||||
if (direction != ElkLayoutDirection.LeftToRight || nodes.Length == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var graphMinY = nodes.Min(n => n.Y);
|
||||
var graphMaxY = nodes.Max(n => n.Y + n.Height);
|
||||
var graphWidth = nodes.Max(n => n.X + n.Width) - nodes.Min(n => n.X);
|
||||
var minSweepLength = graphWidth * 0.4d;
|
||||
var corridorY = graphMinY - 56d;
|
||||
var nodesById = nodes.ToDictionary(n => n.Id, StringComparer.Ordinal);
|
||||
|
||||
var underNodeSeverity = new Dictionary<string, int>(StringComparer.Ordinal);
|
||||
ElkEdgeRoutingScoring.CountUnderNodeViolations(edges, nodes, underNodeSeverity, 1);
|
||||
if (underNodeSeverity.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
ElkRoutedEdge[]? result = null;
|
||||
|
||||
for (var edgeIndex = 0; edgeIndex < edges.Length; edgeIndex++)
|
||||
{
|
||||
var edge = edges[edgeIndex];
|
||||
if (!underNodeSeverity.ContainsKey(edge.Id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var path = ExtractPath(edge);
|
||||
if (path.Count < 2)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find the longest horizontal segment.
|
||||
var bestSegStart = -1;
|
||||
var bestSegLength = 0d;
|
||||
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);
|
||||
if (segLength > bestSegLength)
|
||||
{
|
||||
bestSegLength = segLength;
|
||||
bestSegStart = i;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestSegStart < 0 || bestSegLength < minSweepLength)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Build corridor path: source exit → up to corridor → across → down to target.
|
||||
var sourcePoint = path[0];
|
||||
var targetPoint = path[^1];
|
||||
var exitX = sourcePoint.X;
|
||||
var approachX = targetPoint.X;
|
||||
|
||||
// Determine corridor direction based on source position relative to graph.
|
||||
var sourceNode = nodesById.GetValueOrDefault(edge.SourceNodeId ?? string.Empty);
|
||||
var sourceTopY = sourceNode?.Y ?? sourcePoint.Y;
|
||||
|
||||
// Build corridor path with a perpendicular exit stub.
|
||||
// The stub prevents NormalizeBoundaryAngles from collapsing
|
||||
// the vertical corridor segment (it removes path[1] while
|
||||
// path[1].X == sourceX, so the stub at exitX+24 survives).
|
||||
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 ??= (ElkRoutedEdge[])edges.Clone();
|
||||
result[edgeIndex] = 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(),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
ElkLayoutDiagnostics.LogProgress(
|
||||
$"Corridor reroute: {edge.Id} sweep={bestSegLength:F0}px " +
|
||||
$"from Y={path[bestSegStart].Y:F0} to corridorY={corridorY:F0}");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -88,6 +88,34 @@ internal static partial class ElkEdgeRouterIterative
|
||||
}
|
||||
}
|
||||
|
||||
// Reroute long horizontal sweeps through the top corridor.
|
||||
// Edges spanning > half the graph width with under-node violations
|
||||
// should route above the graph (like backward edges) instead of
|
||||
// cutting straight through the node field.
|
||||
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)
|
||||
{
|
||||
var corridorRetry = BuildRetryState(
|
||||
corridorScore,
|
||||
HighwayProcessingEnabled
|
||||
? ElkEdgeRouterHighway.DetectRemainingBrokenHighways(corridorCandidate, nodes).Count
|
||||
: 0);
|
||||
current = current with { Score = corridorScore, RetryState = corridorRetry, Edges = corridorCandidate };
|
||||
ElkLayoutDiagnostics.LogProgress($"Hybrid winner refinement after corridor reroute: {DescribeSolution(current)}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Targeted under-node elevation with net-total promotion.
|
||||
// ElevateUnderNodeViolations can fix remaining under-node edges
|
||||
// (gateway-exit lanes, long horizontal sweeps) but the standard
|
||||
|
||||
Reference in New Issue
Block a user