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>
209 lines
8.7 KiB
C#
209 lines
8.7 KiB
C#
using System.Collections.Concurrent;
|
|
using System.Diagnostics;
|
|
using System.Globalization;
|
|
|
|
namespace StellaOps.ElkSharp;
|
|
|
|
internal static partial class ElkEdgeRouterIterative
|
|
{
|
|
private static RouteAllEdgesResult RepairPenalizedEdges(
|
|
ElkRoutedEdge[] existingEdges,
|
|
ElkPositionedNode[] nodes,
|
|
double baseObstacleMargin,
|
|
RoutingStrategy strategy,
|
|
RepairPlan repairPlan,
|
|
CancellationToken cancellationToken,
|
|
int maxParallelRepairBuilds,
|
|
bool trustIndependentParallelBuilds = false)
|
|
{
|
|
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;
|
|
var repairSet = repairPlan.EdgeIndices.ToHashSet();
|
|
var routeRepairEdgeIdSet = repairPlan.RouteRepairEdgeIds.ToHashSet(StringComparer.Ordinal);
|
|
var collectorRepairSet = repairPlan.Reasons.Contains("collector-corridors", StringComparer.Ordinal)
|
|
? repairSet
|
|
.Where(edgeIndex => edgeIndex >= 0
|
|
&& edgeIndex < existingEdges.Length
|
|
&& ElkEdgePostProcessor.IsRepeatCollectorLabel(existingEdges[edgeIndex].Label))
|
|
.Where(edgeIndex => !routeRepairEdgeIdSet.Contains(existingEdges[edgeIndex].Id))
|
|
.ToHashSet()
|
|
: [];
|
|
var preferredShortestEdgeIdSet = repairPlan.PreferredShortestEdgeIds.ToHashSet(StringComparer.Ordinal);
|
|
if (collectorRepairSet.Count > 0)
|
|
{
|
|
var collectorEdgeIds = collectorRepairSet
|
|
.Select(edgeIndex => existingEdges[edgeIndex].Id)
|
|
.ToArray();
|
|
routedEdges = ElkRepeatCollectorCorridors.SeparateSharedLanes(routedEdges, nodes, collectorEdgeIds);
|
|
routedEdgeCount += collectorRepairSet.Count;
|
|
}
|
|
|
|
var aStarRepairSet = repairSet
|
|
.Where(edgeIndex => !collectorRepairSet.Contains(edgeIndex))
|
|
.ToHashSet();
|
|
var spreadEndpoints = SpreadTargetEndpoints(existingEdges, nodesById, graphMinY, graphMaxY, strategy.MinLineClearance);
|
|
var softObstacles = new List<OrthogonalSoftObstacle>();
|
|
|
|
for (var edgeIndex = 0; edgeIndex < existingEdges.Length; edgeIndex++)
|
|
{
|
|
if (aStarRepairSet.Contains(edgeIndex))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
foreach (var segment in ElkEdgeRoutingGeometry.FlattenSegments(routedEdges[edgeIndex]))
|
|
{
|
|
softObstacles.Add(new OrthogonalSoftObstacle(segment.Start, segment.End));
|
|
}
|
|
}
|
|
|
|
var orderedRepairIndices = strategy.EdgeOrder
|
|
.Where(aStarRepairSet.Contains)
|
|
.Concat(aStarRepairSet.Where(edgeIndex => !strategy.EdgeOrder.Contains(edgeIndex)))
|
|
.Distinct()
|
|
.ToArray();
|
|
var repairBuilderParallelism = (trustIndependentParallelBuilds || CanParallelizeRepairBuilds(orderedRepairIndices, existingEdges))
|
|
? DetermineRepairBuildParallelism(orderedRepairIndices.Length, maxParallelRepairBuilds)
|
|
: 1;
|
|
var builtRepairResults = new ConcurrentDictionary<int, RepairEdgeBuildResult>();
|
|
var repairBuildLocks = new ConcurrentDictionary<string, object>(StringComparer.Ordinal);
|
|
if (repairBuilderParallelism > 1 && orderedRepairIndices.Length > 1)
|
|
{
|
|
var immutableSoftObstacles = softObstacles.ToArray();
|
|
var parallelOptions = new ParallelOptions
|
|
{
|
|
CancellationToken = cancellationToken,
|
|
MaxDegreeOfParallelism = repairBuilderParallelism,
|
|
};
|
|
|
|
Parallel.ForEach(
|
|
orderedRepairIndices,
|
|
parallelOptions,
|
|
edgeIndex =>
|
|
{
|
|
if (edgeIndex < 0 || edgeIndex >= existingEdges.Length)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var edge = existingEdges[edgeIndex];
|
|
var lockKeys = trustIndependentParallelBuilds
|
|
? []
|
|
: GetRepairBuildLockKeys(edge);
|
|
ExecuteWithRepairBuildLocks(
|
|
repairBuildLocks,
|
|
lockKeys,
|
|
() =>
|
|
{
|
|
builtRepairResults[edgeIndex] = BuildRepairEdgeResult(
|
|
edgeIndex,
|
|
existingEdges,
|
|
nodes,
|
|
obstacles,
|
|
originalNodeBounds,
|
|
spreadEndpoints,
|
|
nodesById,
|
|
immutableSoftObstacles,
|
|
routeRepairEdgeIdSet,
|
|
preferredShortestEdgeIdSet,
|
|
repairPlan.Reasons,
|
|
graphMinY,
|
|
graphMaxY,
|
|
strategy,
|
|
cancellationToken);
|
|
});
|
|
});
|
|
}
|
|
|
|
foreach (var edgeIndex in orderedRepairIndices)
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
|
|
if (edgeIndex < 0 || edgeIndex >= existingEdges.Length)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var buildResult = builtRepairResults.TryGetValue(edgeIndex, out var parallelBuildResult)
|
|
? parallelBuildResult
|
|
: BuildRepairEdgeResult(
|
|
edgeIndex,
|
|
existingEdges,
|
|
nodes,
|
|
obstacles,
|
|
originalNodeBounds,
|
|
spreadEndpoints,
|
|
nodesById,
|
|
softObstacles,
|
|
routeRepairEdgeIdSet,
|
|
preferredShortestEdgeIdSet,
|
|
repairPlan.Reasons,
|
|
graphMinY,
|
|
graphMaxY,
|
|
strategy,
|
|
cancellationToken);
|
|
if (buildResult.WasSkipped)
|
|
{
|
|
skippedEdgeCount++;
|
|
foreach (var segment in ElkEdgeRoutingGeometry.FlattenSegments(buildResult.Edge))
|
|
{
|
|
softObstacles.Add(new OrthogonalSoftObstacle(segment.Start, segment.End));
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
routedSectionCount += buildResult.RoutedSections;
|
|
fallbackSectionCount += buildResult.FallbackSections;
|
|
routedEdgeCount++;
|
|
routedEdges[edgeIndex] = buildResult.Edge;
|
|
|
|
foreach (var segment in ElkEdgeRoutingGeometry.FlattenSegments(routedEdges[edgeIndex]))
|
|
{
|
|
softObstacles.Add(new OrthogonalSoftObstacle(segment.Start, segment.End));
|
|
}
|
|
}
|
|
|
|
var repeatRouteRepairIds = repairPlan.RouteRepairEdgeIds
|
|
.Where(edgeId => routedEdges.Any(edge =>
|
|
string.Equals(edge.Id, edgeId, StringComparison.Ordinal)
|
|
&& ElkEdgePostProcessor.IsRepeatCollectorLabel(edge.Label)))
|
|
.ToArray();
|
|
if (repeatRouteRepairIds.Length > 0)
|
|
{
|
|
routedEdges = ElkRepeatCollectorCorridors.SeparateSharedLanes(routedEdges, nodes, repeatRouteRepairIds);
|
|
}
|
|
|
|
return new RouteAllEdgesResult(
|
|
routedEdges,
|
|
new ElkIterativeRouteDiagnostics
|
|
{
|
|
Mode = "local-repair",
|
|
TotalEdges = existingEdges.Length,
|
|
RoutedEdges = routedEdgeCount,
|
|
SkippedEdges = skippedEdgeCount,
|
|
RoutedSections = routedSectionCount,
|
|
FallbackSections = fallbackSectionCount,
|
|
SoftObstacleSegments = softObstacles.Count,
|
|
RepairedEdgeIds = repairPlan.EdgeIds,
|
|
RepairReasons = repairPlan.Reasons,
|
|
BuilderMode = repairBuilderParallelism > 1 ? "parallel-locked-local-build" : "sequential-local-build",
|
|
BuilderParallelism = repairBuilderParallelism,
|
|
});
|
|
}
|
|
|
|
}
|