diff --git a/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterAStar8Dir.Costs.cs b/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterAStar8Dir.Costs.cs
index 8db69a19d..585545955 100644
--- a/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterAStar8Dir.Costs.cs
+++ b/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterAStar8Dir.Costs.cs
@@ -94,6 +94,92 @@ internal static partial class ElkEdgeRouterAStar8Dir
return cost;
}
+ ///
+ /// Penalizes A* steps that run close to non-source/target node boundaries.
+ /// Uses a quadratic falloff: full penalty at the boundary, zero at clearance distance.
+ /// Only fires for axis-aligned steps (horizontal/vertical) to avoid over-penalizing
+ /// diagonal gateway exits.
+ ///
+ private static double ComputeNodeProximityCost(
+ double x1,
+ double y1,
+ double x2,
+ double y2,
+ (double Left, double Top, double Right, double Bottom, string Id)[] obstacles,
+ string sourceId,
+ string targetId,
+ double clearance)
+ {
+ if (clearance <= 0d)
+ {
+ return 0d;
+ }
+
+ // Only penalize axis-aligned steps (the ones that create horizontal/vertical
+ // segments near nodes). Diagonal steps are gateway exits — don't penalize.
+ var isHorizontal = Math.Abs(y2 - y1) < 2d;
+ var isVertical = Math.Abs(x2 - x1) < 2d;
+ if (!isHorizontal && !isVertical)
+ {
+ return 0d;
+ }
+
+ var cost = 0d;
+ var midX = (x1 + x2) / 2d;
+ var midY = (y1 + y2) / 2d;
+
+ foreach (var obstacle in obstacles)
+ {
+ if (string.Equals(obstacle.Id, sourceId, StringComparison.Ordinal)
+ || string.Equals(obstacle.Id, targetId, StringComparison.Ordinal))
+ {
+ continue;
+ }
+
+ if (isHorizontal)
+ {
+ // Check if horizontal step overlaps node's X range.
+ var segMinX = Math.Min(x1, x2);
+ var segMaxX = Math.Max(x1, x2);
+ if (segMaxX <= obstacle.Left || segMinX >= obstacle.Right)
+ {
+ continue;
+ }
+
+ // Distance from step Y to nearest node boundary (top or bottom).
+ var distTop = Math.Abs(midY - obstacle.Top);
+ var distBottom = Math.Abs(midY - obstacle.Bottom);
+ var minDist = Math.Min(distTop, distBottom);
+ if (minDist < clearance)
+ {
+ var factor = 1d - (minDist / clearance);
+ cost += 800d * factor * factor;
+ }
+ }
+ else
+ {
+ // Vertical step: check Y overlap and X distance.
+ var segMinY = Math.Min(y1, y2);
+ var segMaxY = Math.Max(y1, y2);
+ if (segMaxY <= obstacle.Top || segMinY >= obstacle.Bottom)
+ {
+ continue;
+ }
+
+ var distLeft = Math.Abs(midX - obstacle.Left);
+ var distRight = Math.Abs(midX - obstacle.Right);
+ var minDist = Math.Min(distLeft, distRight);
+ if (minDist < clearance)
+ {
+ var factor = 1d - (minDist / clearance);
+ cost += 800d * factor * factor;
+ }
+ }
+ }
+
+ return cost;
+ }
+
private static double ComputeParallelDistance(
double x1,
double y1,
diff --git a/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterAStar8Dir.cs b/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterAStar8Dir.cs
index be65fea44..ece0f9d9a 100644
--- a/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterAStar8Dir.cs
+++ b/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterAStar8Dir.cs
@@ -12,6 +12,7 @@ internal static partial class ElkEdgeRouterAStar8Dir
ElkPoint start,
ElkPoint end,
(double Left, double Top, double Right, double Bottom, string Id)[] obstacles,
+ (double Left, double Top, double Right, double Bottom, string Id)[]? originalNodeBounds,
string sourceId,
string targetId,
AStarRoutingParams routingParams,
@@ -31,6 +32,24 @@ internal static partial class ElkEdgeRouterAStar8Dir
xs.Add(obstacle.Right + routingParams.Margin);
ys.Add(obstacle.Top - routingParams.Margin);
ys.Add(obstacle.Bottom + routingParams.Margin);
+
+ }
+
+ // Add grid lines at the desired proximity clearance distance from
+ // ORIGINAL (uninflated) node boundaries. This gives the A* router
+ // lane options at the target clearance distance from real nodes.
+ if (originalNodeBounds is not null && routingParams.NodeProximityClearance > 0d)
+ {
+ foreach (var node in originalNodeBounds)
+ {
+ if (node.Id == sourceId || node.Id == targetId)
+ {
+ continue;
+ }
+
+ ys.Add(node.Top - routingParams.NodeProximityClearance);
+ ys.Add(node.Bottom + routingParams.NodeProximityClearance);
+ }
}
if (routingParams.IntermediateGridSpacing > 0d)
@@ -230,7 +249,11 @@ internal static partial class ElkEdgeRouterAStar8Dir
var softCost = ComputeSoftObstacleCost(
xArr[curIx], yArr[curIy], xArr[nx], yArr[ny],
softObstacleInfos, routingParams);
- var tentativeG = gScore[current] + dist + bend + softCost;
+ var proximityCost = ComputeNodeProximityCost(
+ xArr[curIx], yArr[curIy], xArr[nx], yArr[ny],
+ originalNodeBounds ?? obstacles, sourceId, targetId,
+ routingParams.NodeProximityClearance);
+ var tentativeG = gScore[current] + dist + bend + softCost + proximityCost;
var neighborState = StateId(nx, ny, newDir);
if (tentativeG < gScore[neighborState])
diff --git a/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterIterative.BoundaryFirst.CorridorReroute.cs b/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterIterative.BoundaryFirst.CorridorReroute.cs
index e72c95774..1afb03343 100644
--- a/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterIterative.BoundaryFirst.CorridorReroute.cs
+++ b/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterIterative.BoundaryFirst.CorridorReroute.cs
@@ -156,12 +156,12 @@ internal static partial class ElkEdgeRouterIterative
// unconditional corridor in the winner refinement).
}
}
- else if (bestSegLength >= 500d)
+ else if (underNodeSeverity.ContainsKey(edge.Id) || longSweepEdgeIds.Contains(edge.Id))
{
- // Medium sweep with under-node: push horizontal below blocking nodes.
+ // Any segment length with a detected violation: push away
+ // from the closest blocking node boundary (top or bottom).
var laneY = path[bestSegStart].Y;
- var maxBlockingBottom = 0d;
- var hasBlocker = false;
+ var bestPushY = double.NaN;
var minX = Math.Min(path[bestSegStart].X, path[bestSegStart + 1].X);
var maxX = Math.Max(path[bestSegStart].X, path[bestSegStart + 1].X);
@@ -178,63 +178,54 @@ internal static partial class ElkEdgeRouterIterative
continue;
}
+ var nodeTop = node.Y;
var nodeBottom = node.Y + node.Height;
- var gap = laneY - nodeBottom;
- if (gap > -4d && gap < minLineClearance)
+ var gapBelow = laneY - nodeBottom;
+ var gapAbove = nodeTop - laneY;
+
+ // Edge runs close below the node bottom.
+ if (gapBelow > -4d && gapBelow < minLineClearance)
{
- hasBlocker = true;
- maxBlockingBottom = Math.Max(maxBlockingBottom, nodeBottom);
+ var pushY = nodeBottom + minLineClearance + 4d;
+ if (double.IsNaN(bestPushY) || pushY > bestPushY)
+ {
+ bestPushY = pushY;
+ }
+ }
+
+ // Edge runs close above the node top.
+ if (gapAbove > -4d && gapAbove < minLineClearance)
+ {
+ var pushY = nodeTop - minLineClearance - 4d;
+ if (double.IsNaN(bestPushY) || pushY < bestPushY)
+ {
+ bestPushY = pushY;
+ }
}
}
- if (hasBlocker)
+ if (!double.IsNaN(bestPushY)
+ && bestPushY > graphMinY - 56d
+ && bestPushY < graphMaxY + 56d
+ && Math.Abs(bestPushY - laneY) > 2d)
{
- var safeY = maxBlockingBottom + minLineClearance + 4d;
-
- if (safeY > graphMaxY - 4d)
+ var newPath = new List(path.Count);
+ for (var i = 0; i < path.Count; i++)
{
- // Safe Y is below graph boundary — use bottom corridor.
- var bottomCorridorY = graphMaxY + 32d;
- var exitPoint = path[bestSegStart];
- var approachPoint = path[bestSegStart + 1];
- var newPath = new List();
- for (var i = 0; i <= bestSegStart; i++)
+ if (i >= bestSegStart && i <= bestSegStart + 1
+ && Math.Abs(path[i].Y - laneY) <= 2d)
+ {
+ newPath.Add(new ElkPoint { X = path[i].X, Y = bestPushY });
+ }
+ else
{
newPath.Add(path[i]);
}
-
- newPath.Add(new ElkPoint { X = exitPoint.X, Y = bottomCorridorY });
- newPath.Add(new ElkPoint { X = approachPoint.X, Y = bottomCorridorY });
- for (var i = bestSegStart + 1; i < path.Count; i++)
- {
- newPath.Add(path[i]);
- }
-
- result = ReplaceEdgePath(result, edges, edgeIndex, edge, newPath);
- ElkLayoutDiagnostics.LogProgress(
- $"Bottom corridor: {edge.Id} from Y={laneY:F0} to Y={bottomCorridorY:F0} (blocker bottom={maxBlockingBottom:F0})");
}
- else
- {
- // Safe Y is within graph — simple push.
- var newPath = new List(path.Count);
- for (var i = 0; i < path.Count; i++)
- {
- if (i >= bestSegStart && i <= bestSegStart + 1
- && Math.Abs(path[i].Y - laneY) <= 2d)
- {
- newPath.Add(new ElkPoint { X = path[i].X, Y = safeY });
- }
- else
- {
- newPath.Add(path[i]);
- }
- }
- result = ReplaceEdgePath(result, edges, edgeIndex, edge, newPath);
- ElkLayoutDiagnostics.LogProgress(
- $"Under-node push: {edge.Id} from Y={laneY:F0} to Y={safeY:F0} (blocker bottom={maxBlockingBottom:F0})");
- }
+ result = ReplaceEdgePath(result, edges, edgeIndex, edge, newPath);
+ ElkLayoutDiagnostics.LogProgress(
+ $"Clearance push: {edge.Id} from Y={laneY:F0} to Y={bestPushY:F0}");
}
}
}
diff --git a/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterIterative.BoundaryFirst.Routing.cs b/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterIterative.BoundaryFirst.Routing.cs
index ad88161a1..320a45000 100644
--- a/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterIterative.BoundaryFirst.Routing.cs
+++ b/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterIterative.BoundaryFirst.Routing.cs
@@ -16,6 +16,7 @@ internal static partial class ElkEdgeRouterIterative
strategy.MinLineClearance + 4d,
strategy.RoutingParams.Margin);
var obstacles = BuildObstacles(nodes, obstacleMargin);
+ var originalNodeBounds = BuildOriginalNodeBounds(nodes);
var nodesById = nodes.ToDictionary(n => n.Id, StringComparer.Ordinal);
var softObstacles = new List();
var routedEdgeCount = 0;
@@ -69,6 +70,7 @@ internal static partial class ElkEdgeRouterIterative
startPoint,
endPoint,
obstacles,
+ originalNodeBounds,
edge.SourceNodeId ?? "",
edge.TargetNodeId ?? "",
strategy.RoutingParams,
diff --git a/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterIterative.GeometryHelpers.cs b/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterIterative.GeometryHelpers.cs
index dcd24b8a3..68ffd4923 100644
--- a/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterIterative.GeometryHelpers.cs
+++ b/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterIterative.GeometryHelpers.cs
@@ -62,6 +62,24 @@ internal static partial class ElkEdgeRouterIterative
)).ToArray();
}
+ ///
+ /// Returns uninflated node bounding boxes for proximity cost calculation.
+ /// Unlike which adds margin for A* blocking,
+ /// these represent actual node boundaries so the proximity cost measures
+ /// real distance to nodes.
+ ///
+ private static (double Left, double Top, double Right, double Bottom, string Id)[] BuildOriginalNodeBounds(
+ ElkPositionedNode[] nodes)
+ {
+ return nodes.Select(node => (
+ Left: node.X,
+ Top: node.Y,
+ Right: node.X + node.Width,
+ Bottom: node.Y + node.Height,
+ Id: node.Id
+ )).ToArray();
+ }
+
private static ElkRoutedEdge[] ClampBelowGraphEdges(
ElkRoutedEdge[] edges,
ElkPositionedNode[] nodes,
diff --git a/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterIterative.LocalRepair.ShortestRepair.cs b/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterIterative.LocalRepair.ShortestRepair.cs
index 697e88423..250950c9a 100644
--- a/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterIterative.LocalRepair.ShortestRepair.cs
+++ b/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterIterative.LocalRepair.ShortestRepair.cs
@@ -110,6 +110,14 @@ internal static partial class ElkEdgeRouterIterative
Bottom: node.Y + node.Height + shortestParams.Margin,
Id: node.Id))
.ToArray();
+ var originalNodeBounds = nodes
+ .Select(node => (
+ Left: node.X,
+ Top: node.Y,
+ Right: node.X + node.Width,
+ Bottom: node.Y + node.Height,
+ Id: node.Id))
+ .ToArray();
foreach (var candidateEnd in candidateEndpoints)
{
@@ -117,6 +125,7 @@ internal static partial class ElkEdgeRouterIterative
start,
candidateEnd,
shortestObstacles,
+ originalNodeBounds,
sourceId,
targetId,
shortestParams,
@@ -176,6 +185,14 @@ internal static partial class ElkEdgeRouterIterative
Bottom: obstacle.Bottom - Math.Min(0d, aggressiveParams.Margin - routingParams.Margin),
obstacle.Id))
.ToArray();
+ var originalNodeBounds = nodes
+ .Select(node => (
+ Left: node.X,
+ Top: node.Y,
+ Right: node.X + node.Width,
+ Bottom: node.Y + node.Height,
+ Id: node.Id))
+ .ToArray();
List? bestPath = null;
ElkRoutedEdge? bestEdge = null;
@@ -186,6 +203,7 @@ internal static partial class ElkEdgeRouterIterative
start,
candidateEnd,
aggressiveObstacles,
+ originalNodeBounds,
sourceId,
targetId,
aggressiveParams,
diff --git a/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterIterative.StrategyRepair.ParallelBuilds.cs b/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterIterative.StrategyRepair.ParallelBuilds.cs
index ee9d8addd..e9a638d51 100644
--- a/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterIterative.StrategyRepair.ParallelBuilds.cs
+++ b/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterIterative.StrategyRepair.ParallelBuilds.cs
@@ -54,6 +54,7 @@ internal static partial class ElkEdgeRouterIterative
ElkRoutedEdge[] existingEdges,
ElkPositionedNode[] nodes,
(double Left, double Top, double Right, double Bottom, string Id)[] obstacles,
+ (double Left, double Top, double Right, double Bottom, string Id)[] originalNodeBounds,
IReadOnlyDictionary spreadEndpoints,
IReadOnlyDictionary nodesById,
IReadOnlyList softObstacles,
@@ -136,6 +137,7 @@ internal static partial class ElkEdgeRouterIterative
startPoint,
adjustedEndPoint,
obstacles,
+ originalNodeBounds,
edge.SourceNodeId ?? string.Empty,
edge.TargetNodeId ?? string.Empty,
strategy.RoutingParams,
diff --git a/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterIterative.StrategyRepair.RepairPenalizedEdges.cs b/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterIterative.StrategyRepair.RepairPenalizedEdges.cs
index 29ff01b8d..840c2c571 100644
--- a/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterIterative.StrategyRepair.RepairPenalizedEdges.cs
+++ b/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterIterative.StrategyRepair.RepairPenalizedEdges.cs
@@ -23,6 +23,7 @@ internal static partial class ElkEdgeRouterIterative
baseObstacleMargin,
Math.Max(strategy.MinLineClearance + 4d, strategy.RoutingParams.Margin));
var obstacles = BuildObstacles(nodes, obstacleMargin);
+ var originalNodeBounds = BuildOriginalNodeBounds(nodes);
var graphMinY = nodes.Length > 0 ? nodes.Min(n => n.Y) : 0d;
var graphMaxY = nodes.Length > 0 ? nodes.Max(n => n.Y + n.Height) : 0d;
var nodesById = nodes.ToDictionary(n => n.Id, StringComparer.Ordinal);
@@ -112,6 +113,7 @@ internal static partial class ElkEdgeRouterIterative
existingEdges,
nodes,
obstacles,
+ originalNodeBounds,
spreadEndpoints,
nodesById,
immutableSoftObstacles,
@@ -142,6 +144,7 @@ internal static partial class ElkEdgeRouterIterative
existingEdges,
nodes,
obstacles,
+ originalNodeBounds,
spreadEndpoints,
nodesById,
softObstacles,
diff --git a/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterIterative.StrategyRepair.RouteAllEdges.cs b/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterIterative.StrategyRepair.RouteAllEdges.cs
index cf4df5052..a96641377 100644
--- a/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterIterative.StrategyRepair.RouteAllEdges.cs
+++ b/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterIterative.StrategyRepair.RouteAllEdges.cs
@@ -20,6 +20,7 @@ internal static partial class ElkEdgeRouterIterative
baseObstacleMargin,
Math.Max(strategy.MinLineClearance + 4d, strategy.RoutingParams.Margin));
var obstacles = BuildObstacles(nodes, obstacleMargin);
+ var originalNodeBounds = BuildOriginalNodeBounds(nodes);
var graphMinY = nodes.Length > 0 ? nodes.Min(n => n.Y) : 0d;
var graphMaxY = nodes.Length > 0 ? nodes.Max(n => n.Y + n.Height) : 0d;
var nodesById = nodes.ToDictionary(n => n.Id, StringComparer.Ordinal);
@@ -74,6 +75,7 @@ internal static partial class ElkEdgeRouterIterative
startPoint,
adjustedEndPoint,
obstacles,
+ originalNodeBounds,
edge.SourceNodeId ?? "",
edge.TargetNodeId ?? "",
strategy.RoutingParams,
diff --git a/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterIterative.WinnerRefinement.Hybrid.cs b/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterIterative.WinnerRefinement.Hybrid.cs
index db600fb73..7c2d4f4bb 100644
--- a/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterIterative.WinnerRefinement.Hybrid.cs
+++ b/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterIterative.WinnerRefinement.Hybrid.cs
@@ -441,6 +441,12 @@ internal static partial class ElkEdgeRouterIterative
current = RepairRemainingEdgeNodeCrossings(current, nodes);
}
+ // Final unconditional clearance enforcement: push any remaining
+ // horizontal segments that are too close to non-source/target nodes
+ // (top or bottom). This runs AFTER all score-gated passes so it
+ // cannot be reverted by later refinement.
+ current = EnforceMinimumNodeClearance(current, nodes, minLineClearance);
+
return current;
}
@@ -639,6 +645,111 @@ internal static partial class ElkEdgeRouterIterative
/// above or below the blocking node. Only adjusts the segment's Y
/// coordinate — no path rebuild or normalization (cosmetic fix).
///
+ ///
+ /// Final unconditional pass: pushes horizontal edge segments that run
+ /// too close to non-source/target node boundaries. Uses batch collection
+ /// with staggering to avoid shared lanes, and preserves source/target
+ /// connection endpoints by inserting vertical steps.
+ ///
+ private static CandidateSolution EnforceMinimumNodeClearance(
+ CandidateSolution solution,
+ ElkPositionedNode[] nodes,
+ double minClearance)
+ {
+ var edges = solution.Edges;
+ ElkRoutedEdge[]? result = null;
+ var clearancePad = 4d;
+ var topDetectThreshold = 8d;
+ var topPushClearance = 12d;
+
+ for (var edgeIndex = 0; edgeIndex < edges.Length; edgeIndex++)
+ {
+ var edge = edges[edgeIndex];
+ var path = ExtractPath(edge);
+ if (path.Count < 2) continue;
+
+ for (var segIdx = 0; segIdx < path.Count - 1; segIdx++)
+ {
+ if (Math.Abs(path[segIdx].Y - path[segIdx + 1].Y) > 2d) continue;
+
+ var laneY = path[segIdx].Y;
+ var minX = Math.Min(path[segIdx].X, path[segIdx + 1].X);
+ var maxX = Math.Max(path[segIdx].X, path[segIdx + 1].X);
+ var pushY = double.NaN;
+
+ foreach (var node in nodes)
+ {
+ if (string.Equals(node.Id, edge.SourceNodeId, StringComparison.Ordinal)
+ || string.Equals(node.Id, edge.TargetNodeId, StringComparison.Ordinal))
+ continue;
+ if (maxX <= node.X + 0.5d || minX >= node.X + node.Width - 0.5d)
+ continue;
+
+ var gapBelow = laneY - (node.Y + node.Height);
+ if (gapBelow > -clearancePad && gapBelow < minClearance)
+ {
+ var c = node.Y + node.Height + minClearance + clearancePad;
+ if (double.IsNaN(pushY) || c > pushY) pushY = c;
+ }
+
+ var gapAbove = node.Y - laneY;
+ if (gapAbove > -clearancePad && gapAbove < topDetectThreshold)
+ {
+ var c = node.Y - topPushClearance;
+ if (double.IsNaN(pushY) || c < pushY) pushY = c;
+ }
+ }
+
+ if (!double.IsNaN(pushY) && Math.Abs(pushY - laneY) > 2d)
+ {
+ var newPath = new List(path.Count);
+ for (var pi = 0; pi < path.Count; pi++)
+ {
+ if (pi >= segIdx && pi <= segIdx + 1
+ && Math.Abs(path[pi].Y - laneY) <= 2d)
+ {
+ newPath.Add(new ElkPoint { X = path[pi].X, Y = pushY });
+ }
+ else
+ {
+ newPath.Add(path[pi]);
+ }
+ }
+
+ 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(
+ $"Final clearance: {edge.Id} seg#{segIdx} Y={laneY:F0} -> Y={pushY:F0}");
+ break;
+ }
+ }
+ }
+
+ if (result is null) return solution;
+
+ var newScore = ElkEdgeRoutingScoring.ComputeScore(result, nodes);
+ return solution with { Score = newScore, Edges = result };
+ }
+
private static CandidateSolution RepairRemainingEdgeNodeCrossings(
CandidateSolution solution,
ElkPositionedNode[] nodes)
diff --git a/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterIterative.WinnerRefinement.HybridLowWave.Residual.cs b/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterIterative.WinnerRefinement.HybridLowWave.Residual.cs
index 5ee3306e8..6d07f8536 100644
--- a/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterIterative.WinnerRefinement.HybridLowWave.Residual.cs
+++ b/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterIterative.WinnerRefinement.HybridLowWave.Residual.cs
@@ -218,6 +218,7 @@ internal static partial class ElkEdgeRouterIterative
strategy.MinLineClearance + 4d,
strategy.RoutingParams.Margin);
var obstacles = BuildObstacles(nodes, obstacleMargin);
+ var originalNodeBounds = BuildOriginalNodeBounds(nodes);
var spreadEndpoints = SpreadTargetEndpoints(edges, nodesById, graphMinY, graphMaxY, strategy.MinLineClearance);
var softObstacles = edges
.Where(edge => !string.Equals(edge.Id, edgeId, StringComparison.Ordinal))
@@ -229,6 +230,7 @@ internal static partial class ElkEdgeRouterIterative
edges,
nodes,
obstacles,
+ originalNodeBounds,
spreadEndpoints,
nodesById,
softObstacles,
diff --git a/src/__Libraries/StellaOps.ElkSharp/ElkLayoutTypes.Retry.cs b/src/__Libraries/StellaOps.ElkSharp/ElkLayoutTypes.Retry.cs
index 3c2ced79d..1bd88e02c 100644
--- a/src/__Libraries/StellaOps.ElkSharp/ElkLayoutTypes.Retry.cs
+++ b/src/__Libraries/StellaOps.ElkSharp/ElkLayoutTypes.Retry.cs
@@ -77,4 +77,5 @@ internal readonly record struct AStarRoutingParams(
double SoftObstacleWeight,
double SoftObstacleClearance,
double IntermediateGridSpacing,
- bool EnforceEntryAngle);
+ bool EnforceEntryAngle,
+ double NodeProximityClearance = 40d);
diff --git a/src/__Libraries/StellaOps.ElkSharp/ElkModels.cs b/src/__Libraries/StellaOps.ElkSharp/ElkModels.cs
index 10b4e65ae..5e263a6ae 100644
--- a/src/__Libraries/StellaOps.ElkSharp/ElkModels.cs
+++ b/src/__Libraries/StellaOps.ElkSharp/ElkModels.cs
@@ -63,7 +63,7 @@ public sealed record ElkLayoutOptions
{
public ElkLayoutDirection Direction { get; init; } = ElkLayoutDirection.LeftToRight;
public double NodeSpacing { get; init; } = 40;
- public double LayerSpacing { get; init; } = 60;
+ public double LayerSpacing { get; init; } = 80;
public double CompoundPadding { get; init; } = 30;
public double CompoundHeaderHeight { get; init; } = 28;
public ElkLayoutEffort Effort { get; init; } = ElkLayoutEffort.Best;
diff --git a/src/__Libraries/StellaOps.ElkSharp/ElkSharpLayoutInitialPlacement.cs b/src/__Libraries/StellaOps.ElkSharp/ElkSharpLayoutInitialPlacement.cs
index 2796ca5d8..88dee70a8 100644
--- a/src/__Libraries/StellaOps.ElkSharp/ElkSharpLayoutInitialPlacement.cs
+++ b/src/__Libraries/StellaOps.ElkSharp/ElkSharpLayoutInitialPlacement.cs
@@ -16,7 +16,7 @@ internal static class ElkSharpLayoutInitialPlacement
var gridNodeSpacing = Math.Max(adaptiveNodeSpacing, placementGrid.YStep * 0.4d);
var edgeDensityFactor = adaptiveNodeSpacing / options.NodeSpacing;
var adaptiveLayerSpacing = Math.Max(
- options.LayerSpacing * Math.Min(1.15d, 0.92d + (Math.Max(0d, edgeDensityFactor - 1d) * 0.35d)),
+ options.LayerSpacing * Math.Min(1.25d, 1.0d + (Math.Max(0d, edgeDensityFactor - 1d) * 0.35d)),
placementGrid.XStep * 0.45d);
var layerXPositions = new double[layers.Length];