feat(elksharp): A* node proximity cost, increased layer spacing, bridge gap curve awareness, post-pipeline clearance enforcement
Three-layer edge-node clearance improvement: 1. A* proximity cost with correct coordinates: pass original (uninflated) node bounds to ComputeNodeProximityCost so the pathfinder penalizes edges near real node boundaries, not the inflated obstacle margin. Weight=800, clearance=40px. Grid lines added at clearance distance from real nodes. 2. Default LayerSpacing increased from 60 to 80, adaptive multiplier floor raised from 0.92 to 1.0, giving wider routing corridors between node rows. 3. Post-pipeline EnforceMinimumNodeClearance: final unconditional pass pushes horizontal segments within 8px of node tops (12px push) or within minClearance of node bottoms (full clearance push). Also: bridge gap detection now uses curve-aware effective segments (same preprocessing + corner pull-back as BuildRoundedEdgePath) so gaps only appear at genuine visual crossings. Collector trunks and same-group edges excluded from gap detection. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -94,6 +94,92 @@ internal static partial class ElkEdgeRouterAStar8Dir
|
||||
return cost;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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,
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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<ElkPoint>(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<ElkPoint>();
|
||||
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<ElkPoint>(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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<OrthogonalSoftObstacle>();
|
||||
var routedEdgeCount = 0;
|
||||
@@ -69,6 +70,7 @@ internal static partial class ElkEdgeRouterIterative
|
||||
startPoint,
|
||||
endPoint,
|
||||
obstacles,
|
||||
originalNodeBounds,
|
||||
edge.SourceNodeId ?? "",
|
||||
edge.TargetNodeId ?? "",
|
||||
strategy.RoutingParams,
|
||||
|
||||
@@ -62,6 +62,24 @@ internal static partial class ElkEdgeRouterIterative
|
||||
)).ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns uninflated node bounding boxes for proximity cost calculation.
|
||||
/// Unlike <see cref="BuildObstacles"/> which adds margin for A* blocking,
|
||||
/// these represent actual node boundaries so the proximity cost measures
|
||||
/// real distance to nodes.
|
||||
/// </summary>
|
||||
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,
|
||||
|
||||
@@ -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<ElkPoint>? bestPath = null;
|
||||
ElkRoutedEdge? bestEdge = null;
|
||||
@@ -186,6 +203,7 @@ internal static partial class ElkEdgeRouterIterative
|
||||
start,
|
||||
candidateEnd,
|
||||
aggressiveObstacles,
|
||||
originalNodeBounds,
|
||||
sourceId,
|
||||
targetId,
|
||||
aggressiveParams,
|
||||
|
||||
@@ -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<string, ElkPoint> spreadEndpoints,
|
||||
IReadOnlyDictionary<string, ElkPositionedNode> nodesById,
|
||||
IReadOnlyList<OrthogonalSoftObstacle> softObstacles,
|
||||
@@ -136,6 +137,7 @@ internal static partial class ElkEdgeRouterIterative
|
||||
startPoint,
|
||||
adjustedEndPoint,
|
||||
obstacles,
|
||||
originalNodeBounds,
|
||||
edge.SourceNodeId ?? string.Empty,
|
||||
edge.TargetNodeId ?? string.Empty,
|
||||
strategy.RoutingParams,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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).
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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<ElkPoint>(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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -77,4 +77,5 @@ internal readonly record struct AStarRoutingParams(
|
||||
double SoftObstacleWeight,
|
||||
double SoftObstacleClearance,
|
||||
double IntermediateGridSpacing,
|
||||
bool EnforceEntryAngle);
|
||||
bool EnforceEntryAngle,
|
||||
double NodeProximityClearance = 40d);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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];
|
||||
|
||||
Reference in New Issue
Block a user