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];