Files
git.stella-ops.org/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterIterative.StrategyRepair.RouteAllEdges.cs
master 95f9ac379f 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>
2026-04-03 07:41:19 +03:00

137 lines
4.9 KiB
C#

using System.Collections.Concurrent;
using System.Diagnostics;
using System.Globalization;
namespace StellaOps.ElkSharp;
internal static partial class ElkEdgeRouterIterative
{
private static RouteAllEdgesResult? RouteAllEdges(
ElkRoutedEdge[] existingEdges,
ElkPositionedNode[] nodes,
double baseObstacleMargin,
RoutingStrategy strategy,
CancellationToken cancellationToken)
{
var routedEdges = new ElkRoutedEdge[existingEdges.Length];
Array.Copy(existingEdges, routedEdges, existingEdges.Length);
var obstacleMargin = Math.Max(
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);
var routedEdgeCount = 0;
var skippedEdgeCount = 0;
var routedSectionCount = 0;
var fallbackSectionCount = 0;
// Spread endpoints: distribute edges arriving at the same target side
var spreadEndpoints = SpreadTargetEndpoints(existingEdges, nodesById, graphMinY, graphMaxY, strategy.MinLineClearance);
var softObstacles = new List<OrthogonalSoftObstacle>();
foreach (var edgeIndex in strategy.EdgeOrder)
{
if (edgeIndex < 0 || edgeIndex >= existingEdges.Length)
{
continue;
}
cancellationToken.ThrowIfCancellationRequested();
var edge = existingEdges[edgeIndex];
// Skip edges that need special routing (backward, ports, corridors, collectors)
if (!CanRepairEdgeLocally(edge, nodes, graphMinY, graphMaxY))
{
skippedEdgeCount++;
foreach (var segment in ElkEdgeRoutingGeometry.FlattenSegments(edge))
{
softObstacles.Add(new OrthogonalSoftObstacle(segment.Start, segment.End));
}
continue;
}
var newSections = new List<ElkEdgeSection>(edge.Sections.Count);
foreach (var section in edge.Sections)
{
var endPoint = spreadEndpoints.TryGetValue(edge.Id, out var spread)
? spread
: section.EndPoint;
var (startPoint, adjustedEndPoint) = ResolveRoutingEndpoints(
section.StartPoint,
endPoint,
edge.SourceNodeId,
edge.TargetNodeId,
nodesById);
var rerouted = ElkEdgeRouterAStar8Dir.Route(
startPoint,
adjustedEndPoint,
obstacles,
originalNodeBounds,
edge.SourceNodeId ?? "",
edge.TargetNodeId ?? "",
strategy.RoutingParams,
softObstacles,
cancellationToken);
if (rerouted is not null && rerouted.Count >= 2)
{
routedSectionCount++;
newSections.Add(new ElkEdgeSection
{
StartPoint = rerouted[0],
EndPoint = rerouted[^1],
BendPoints = rerouted.Skip(1).Take(rerouted.Count - 2).ToArray(),
});
}
else
{
fallbackSectionCount++;
newSections.Add(section);
}
}
routedEdgeCount++;
routedEdges[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 = newSections,
};
foreach (var segment in ElkEdgeRoutingGeometry.FlattenSegments(routedEdges[edgeIndex]))
{
softObstacles.Add(new OrthogonalSoftObstacle(segment.Start, segment.End));
}
}
return new RouteAllEdgesResult(
routedEdges,
new ElkIterativeRouteDiagnostics
{
Mode = "full-strategy",
TotalEdges = existingEdges.Length,
RoutedEdges = routedEdgeCount,
SkippedEdges = skippedEdgeCount,
RoutedSections = routedSectionCount,
FallbackSections = fallbackSectionCount,
SoftObstacleSegments = softObstacles.Count,
});
}
}