Files
git.stella-ops.org/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterIterative.WinnerRefinement.BoundarySlots.cs
master 5d6435fdb2 ElkSharp edge routing: boundary slots, gateway repairs, corridor spacing
Major edge routing improvements including corridor spacing, crossing reduction,
focused gateway boundary repairs, setter families, and advanced restabilization.
Adds workflow renderer tests for document-processing and artifact inspection.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 08:52:02 +03:00

342 lines
14 KiB
C#

using System.Collections.Concurrent;
using System.Diagnostics;
using System.Globalization;
namespace StellaOps.ElkSharp;
internal static partial class ElkEdgeRouterIterative
{
private static CandidateSolution ApplyFinalBoundarySlotPolish(
CandidateSolution solution,
ElkPositionedNode[] nodes,
ElkLayoutDirection direction,
double minLineClearance,
int maxRounds = 3)
{
var current = solution;
var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal);
for (var round = 0; round < maxRounds; round++)
{
var boundarySlotSeverity = new Dictionary<string, int>(StringComparer.Ordinal);
ElkEdgeRoutingScoring.CountBoundarySlotViolations(current.Edges, nodes, boundarySlotSeverity, 10);
if (boundarySlotSeverity.Count == 0)
{
break;
}
var batchedRootEdgeIds = boundarySlotSeverity
.OrderByDescending(pair => pair.Value)
.ThenBy(pair => pair.Key, StringComparer.Ordinal)
.Take(MaxWinnerPolishBatchedRootEdges)
.Select(pair => pair.Key)
.ToArray();
var batchedFocusEdgeIds = ResolveBoundarySlotRepairFocus(
current.Edges,
nodesById,
batchedRootEdgeIds);
if (batchedFocusEdgeIds.Length > 0)
{
var batchedCandidateEdges = BuildFinalBoundarySlotCandidate(
current.Edges,
nodes,
direction,
minLineClearance,
batchedFocusEdgeIds,
allowLateRestabilizedClosure: false);
if (TryPromoteFinalBoundarySlotCandidate(current, batchedCandidateEdges, nodes, out var batchedPromoted))
{
current = batchedPromoted;
continue;
}
}
var improved = false;
foreach (var edgeId in boundarySlotSeverity
.OrderByDescending(pair => pair.Value)
.ThenBy(pair => pair.Key, StringComparer.Ordinal)
.Select(pair => pair.Key))
{
var focusEdgeIds = ResolveBoundarySlotRepairFocus(
current.Edges,
nodesById,
[edgeId]);
if (focusEdgeIds.Length == 0)
{
continue;
}
var candidateEdges = BuildFinalBoundarySlotCandidate(
current.Edges,
nodes,
direction,
minLineClearance,
focusEdgeIds,
allowLateRestabilizedClosure: false);
if (!TryPromoteFinalBoundarySlotCandidate(current, candidateEdges, nodes, out var promoted))
{
continue;
}
current = promoted;
improved = true;
break;
}
if (!improved)
{
break;
}
}
return current;
}
private static string[] ResolveBoundarySlotRepairFocus(
IReadOnlyCollection<ElkRoutedEdge> edges,
IReadOnlyDictionary<string, ElkPositionedNode> nodesById,
IReadOnlyCollection<string> rootEdgeIds)
{
if (rootEdgeIds.Count == 0)
{
return [];
}
if (TryResolveGatewayBoundarySlotLocalFocus(edges, nodesById, rootEdgeIds, out var localFocus))
{
return localFocus;
}
return ExpandWinningSolutionFocus(edges, rootEdgeIds).ToArray();
}
private static bool TryResolveGatewayBoundarySlotLocalFocus(
IReadOnlyCollection<ElkRoutedEdge> edges,
IReadOnlyDictionary<string, ElkPositionedNode> nodesById,
IReadOnlyCollection<string> rootEdgeIds,
out string[] focusEdgeIds)
{
focusEdgeIds = [];
if (rootEdgeIds.Count == 0)
{
return false;
}
var edgesById = edges.ToDictionary(edge => edge.Id, StringComparer.Ordinal);
var localFocus = new HashSet<string>(StringComparer.Ordinal);
foreach (var edgeId in rootEdgeIds)
{
if (!edgesById.TryGetValue(edgeId, out var edge))
{
return false;
}
var touchesGateway = false;
if (!string.IsNullOrWhiteSpace(edge.SourceNodeId)
&& nodesById.TryGetValue(edge.SourceNodeId, out var sourceNode)
&& ElkShapeBoundaries.IsGatewayShape(sourceNode))
{
touchesGateway = true;
}
if (!touchesGateway
&& !string.IsNullOrWhiteSpace(edge.TargetNodeId)
&& nodesById.TryGetValue(edge.TargetNodeId, out var targetNode)
&& ElkShapeBoundaries.IsGatewayShape(targetNode))
{
touchesGateway = true;
}
if (!touchesGateway)
{
return false;
}
localFocus.Add(edgeId);
}
focusEdgeIds = localFocus
.OrderBy(edgeId => edgeId, StringComparer.Ordinal)
.ToArray();
return focusEdgeIds.Length > 0;
}
private static CandidateSolution ApplyFinalPostSlotHardRulePolish(
CandidateSolution solution,
ElkPositionedNode[] nodes,
ElkLayoutDirection direction,
double minLineClearance,
int maxRounds = 3)
{
var current = solution;
for (var round = 0; round < maxRounds; round++)
{
var severityByEdgeId = new Dictionary<string, int>(StringComparer.Ordinal);
var pressure =
ElkEdgeRoutingScoring.CountEdgeNodeCrossings(current.Edges, nodes, severityByEdgeId, 10)
+ ElkEdgeRoutingScoring.CountBadBoundaryAngles(current.Edges, nodes, severityByEdgeId, 10)
+ ElkEdgeRoutingScoring.CountGatewaySourceExitViolations(current.Edges, nodes, severityByEdgeId, 10)
+ ElkEdgeRoutingScoring.CountTargetApproachJoinViolations(current.Edges, nodes, severityByEdgeId, 10)
+ ElkEdgeRoutingScoring.CountSharedLaneViolations(current.Edges, nodes, severityByEdgeId, 10)
+ ElkEdgeRoutingScoring.CountBoundarySlotViolations(current.Edges, nodes, severityByEdgeId, 10)
+ ElkEdgeRoutingScoring.CountTargetApproachBacktrackingViolations(current.Edges, nodes, severityByEdgeId, 10)
+ ElkEdgeRoutingScoring.CountExcessiveDetourViolations(current.Edges, nodes, severityByEdgeId, 10)
+ ElkEdgeRoutingScoring.CountBelowGraphViolations(current.Edges, nodes, severityByEdgeId, 10)
+ ElkEdgeRoutingScoring.CountUnderNodeViolations(current.Edges, nodes, severityByEdgeId, 10)
+ ElkEdgeRoutingScoring.CountLongDiagonalViolations(current.Edges, nodes, severityByEdgeId, 10);
ElkLayoutDiagnostics.LogProgress(
$"Winner post-slot hard-rule round {round + 1} start: pressure={pressure} retry={DescribeRetryState(current.RetryState)} focus={severityByEdgeId.Count}");
if (pressure == 0)
{
break;
}
var preferFastTerminalOnly = ShouldPreferFastTerminalOnlyHardRuleClosure(current.RetryState);
var batchedRootEdgeIds = severityByEdgeId
.OrderByDescending(pair => pair.Value)
.ThenBy(pair => pair.Key, StringComparer.Ordinal)
.Take(MaxWinnerPolishBatchedRootEdges)
.Select(pair => pair.Key)
.ToArray();
var batchedFocusEdgeIds = preferFastTerminalOnly
? batchedRootEdgeIds
: ExpandWinningSolutionFocus(current.Edges, batchedRootEdgeIds).ToArray();
if (batchedFocusEdgeIds.Length > 0)
{
if (preferFastTerminalOnly)
{
ElkLayoutDiagnostics.LogProgress(
$"Winner post-slot hard-rule round {round + 1} fast-terminal focus=[{string.Join(", ", batchedFocusEdgeIds)}]");
var quickBatchedCandidateEdges = BuildFastTerminalOnlyHardRuleCandidate(
current.Edges,
nodes,
direction,
minLineClearance,
batchedFocusEdgeIds);
if (TryPromoteFinalHardRuleCandidate(current, quickBatchedCandidateEdges, nodes, out var quickBatchedPromoted))
{
current = quickBatchedPromoted;
continue;
}
var focusedTerminalClosureEdges = CloseRemainingTerminalViolations(
current.Edges,
nodes,
direction,
minLineClearance,
batchedFocusEdgeIds);
if (TryPromoteFinalHardRuleCandidate(current, focusedTerminalClosureEdges, nodes, out var focusedTerminalPromoted))
{
current = focusedTerminalPromoted;
continue;
}
var exactRestabilizedEdges = BuildFinalRestabilizedCandidate(
current.Edges,
nodes,
direction,
minLineClearance,
batchedFocusEdgeIds);
if (TryPromoteFinalHardRuleCandidate(current, exactRestabilizedEdges, nodes, out var exactRestabilizedPromoted))
{
current = exactRestabilizedPromoted;
continue;
}
var quickBatchedScore = ElkEdgeRoutingScoring.ComputeScore(quickBatchedCandidateEdges, nodes);
var quickBatchedRetryState = BuildRetryState(
quickBatchedScore,
HighwayProcessingEnabled
? ElkEdgeRouterHighway.DetectRemainingBrokenHighways(quickBatchedCandidateEdges, nodes).Count
: 0);
var changedFocusEdgeIds = batchedFocusEdgeIds
.Where(edgeId => HasEdgeGeometryChanged(current.Edges, quickBatchedCandidateEdges, edgeId))
.ToArray();
ElkLayoutDiagnostics.LogProgress(
$"Winner post-slot hard-rule round {round + 1} fast-terminal made no promotion: candidate={DescribeRetryState(quickBatchedRetryState)} changed=[{string.Join(", ", changedFocusEdgeIds)}]");
}
else
{
ElkLayoutDiagnostics.LogProgress(
$"Winner post-slot hard-rule round {round + 1} full-restabilize focus=[{string.Join(", ", batchedFocusEdgeIds)}]");
var batchedCandidateEdges = BuildFinalRestabilizedCandidate(
current.Edges,
nodes,
direction,
minLineClearance,
batchedFocusEdgeIds);
if (TryPromoteFinalHardRuleCandidate(current, batchedCandidateEdges, nodes, out var batchedPromoted))
{
current = batchedPromoted;
continue;
}
}
}
var orderedSeverityEdgeIds = severityByEdgeId
.OrderByDescending(pair => pair.Value)
.ThenBy(pair => pair.Key, StringComparer.Ordinal)
.Select(pair => pair.Key)
.ToArray();
var improved = false;
if (preferFastTerminalOnly)
{
var candidateEdgeIds = orderedSeverityEdgeIds
.Take(MaxWinnerPolishFastTerminalSingleEdgeCandidates)
.ToArray();
ElkLayoutDiagnostics.LogProgress(
$"Winner post-slot hard-rule round {round + 1} evaluating {candidateEdgeIds.Length}/{orderedSeverityEdgeIds.Length} fast-terminal single-edge candidates in parallel");
if (TryPromoteFastTerminalCandidates(
current,
nodes,
direction,
minLineClearance,
candidateEdgeIds,
out var parallelPromoted))
{
current = parallelPromoted;
improved = true;
}
}
else
{
foreach (var edgeId in orderedSeverityEdgeIds)
{
var focusEdgeIds = ExpandWinningSolutionFocus(current.Edges, [edgeId]).ToArray();
if (focusEdgeIds.Length == 0)
{
continue;
}
var candidateEdges = BuildFinalRestabilizedCandidate(
current.Edges,
nodes,
direction,
minLineClearance,
focusEdgeIds);
if (!TryPromoteFinalHardRuleCandidate(current, candidateEdges, nodes, out var promoted))
{
continue;
}
current = promoted;
improved = true;
break;
}
}
if (!improved)
{
break;
}
}
return current;
}
}