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>
342 lines
14 KiB
C#
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;
|
|
}
|
|
|
|
}
|