ElkSharp: gateway face overflow redirect, under-node push-first routing, boundary-slot snap
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,377 @@
|
||||
namespace StellaOps.ElkSharp;
|
||||
|
||||
internal static partial class ElkEdgeRouterIterative
|
||||
{
|
||||
private readonly record struct GatewayArtifactState(
|
||||
int SourceVertexExits,
|
||||
int CornerDiagonals,
|
||||
int InteriorAdjacentPoints,
|
||||
int SourceFaceMismatches,
|
||||
int SourceDominantAxisDetours,
|
||||
int SourceScoringIssues)
|
||||
{
|
||||
public bool IsClean =>
|
||||
SourceVertexExits == 0
|
||||
&& CornerDiagonals == 0
|
||||
&& InteriorAdjacentPoints == 0
|
||||
&& SourceFaceMismatches == 0
|
||||
&& SourceDominantAxisDetours == 0
|
||||
&& SourceScoringIssues == 0;
|
||||
|
||||
public bool IsBetterThan(GatewayArtifactState other)
|
||||
{
|
||||
if (SourceVertexExits != other.SourceVertexExits)
|
||||
{
|
||||
return SourceVertexExits < other.SourceVertexExits;
|
||||
}
|
||||
|
||||
if (CornerDiagonals != other.CornerDiagonals)
|
||||
{
|
||||
return CornerDiagonals < other.CornerDiagonals;
|
||||
}
|
||||
|
||||
if (InteriorAdjacentPoints != other.InteriorAdjacentPoints)
|
||||
{
|
||||
return InteriorAdjacentPoints < other.InteriorAdjacentPoints;
|
||||
}
|
||||
|
||||
if (SourceFaceMismatches != other.SourceFaceMismatches)
|
||||
{
|
||||
return SourceFaceMismatches < other.SourceFaceMismatches;
|
||||
}
|
||||
|
||||
if (SourceDominantAxisDetours != other.SourceDominantAxisDetours)
|
||||
{
|
||||
return SourceDominantAxisDetours < other.SourceDominantAxisDetours;
|
||||
}
|
||||
|
||||
return SourceScoringIssues < other.SourceScoringIssues;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return
|
||||
$"vertex={SourceVertexExits} corner={CornerDiagonals} interior={InteriorAdjacentPoints} " +
|
||||
$"face={SourceFaceMismatches} detour={SourceDominantAxisDetours} scoring={SourceScoringIssues}";
|
||||
}
|
||||
}
|
||||
|
||||
private static CandidateSolution ApplyFinalGatewayArtifactPolish(
|
||||
CandidateSolution solution,
|
||||
ElkPositionedNode[] nodes,
|
||||
double minLineClearance)
|
||||
{
|
||||
var current = solution;
|
||||
|
||||
for (var round = 0; round < 2; round++)
|
||||
{
|
||||
var baselineArtifacts = EvaluateGatewayArtifacts(current.Edges, nodes, out var focusEdgeIds);
|
||||
if (baselineArtifacts.IsClean || focusEdgeIds.Length == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
ElkLayoutDiagnostics.LogProgress(
|
||||
$"Hybrid gateway artifact polish round {round + 1} start: artifacts={baselineArtifacts} focus=[{string.Join(", ", focusEdgeIds)}]");
|
||||
|
||||
var candidateEdges = current.Edges;
|
||||
candidateEdges = ElkEdgePostProcessor.FinalizeGatewayBoundaryGeometry(candidateEdges, nodes, focusEdgeIds);
|
||||
candidateEdges = ElkEdgePostProcessor.RepairBoundaryAnglesAndTargetApproaches(
|
||||
candidateEdges,
|
||||
nodes,
|
||||
minLineClearance,
|
||||
focusEdgeIds);
|
||||
candidateEdges = ElkEdgePostProcessor.FinalizeDecisionTargetEntries(candidateEdges, nodes, focusEdgeIds);
|
||||
candidateEdges = ElkEdgePostProcessor.NormalizeBoundaryAngles(candidateEdges, nodes);
|
||||
candidateEdges = ElkEdgePostProcessor.NormalizeSourceExitAngles(candidateEdges, nodes);
|
||||
candidateEdges = ElkEdgePostProcessor.SnapBoundarySlotAssignments(
|
||||
candidateEdges,
|
||||
nodes,
|
||||
minLineClearance,
|
||||
focusEdgeIds,
|
||||
enforceAllNodeEndpoints: true);
|
||||
candidateEdges = ElkEdgePostProcessor.FinalizeGatewayBoundaryGeometry(candidateEdges, nodes, focusEdgeIds);
|
||||
candidateEdges = ElkEdgePostProcessor.NormalizeBoundaryAngles(candidateEdges, nodes);
|
||||
candidateEdges = ElkEdgePostProcessor.NormalizeSourceExitAngles(candidateEdges, nodes);
|
||||
|
||||
if (!TryPromoteGatewayArtifactCandidate(current, candidateEdges, nodes, baselineArtifacts, out var promoted))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
current = promoted;
|
||||
ElkLayoutDiagnostics.LogProgress(
|
||||
$"Hybrid gateway artifact polish round {round + 1} improved: retry={DescribeRetryState(current.RetryState)}");
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
private static bool TryPromoteGatewayArtifactCandidate(
|
||||
CandidateSolution current,
|
||||
ElkRoutedEdge[] candidateEdges,
|
||||
ElkPositionedNode[] nodes,
|
||||
GatewayArtifactState baselineArtifacts,
|
||||
out CandidateSolution promoted)
|
||||
{
|
||||
promoted = current;
|
||||
var candidateArtifacts = EvaluateGatewayArtifacts(candidateEdges, nodes, out _);
|
||||
if (!candidateArtifacts.IsBetterThan(baselineArtifacts))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var candidateScore = ElkEdgeRoutingScoring.ComputeScore(candidateEdges, nodes);
|
||||
var candidateRetryState = BuildRetryState(
|
||||
candidateScore,
|
||||
HighwayProcessingEnabled
|
||||
? ElkEdgeRouterHighway.DetectRemainingBrokenHighways(candidateEdges, nodes).Count
|
||||
: 0);
|
||||
if (HasHardRuleRegression(candidateRetryState, current.RetryState)
|
||||
|| candidateScore.NodeCrossings > current.Score.NodeCrossings)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
promoted = current with
|
||||
{
|
||||
Score = candidateScore,
|
||||
RetryState = candidateRetryState,
|
||||
Edges = candidateEdges,
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
private static GatewayArtifactState EvaluateGatewayArtifacts(
|
||||
IReadOnlyCollection<ElkRoutedEdge> edges,
|
||||
IReadOnlyCollection<ElkPositionedNode> nodes,
|
||||
out string[] focusEdgeIds)
|
||||
{
|
||||
var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal);
|
||||
var focus = new HashSet<string>(StringComparer.Ordinal);
|
||||
var sourceVertexExits = 0;
|
||||
var cornerDiagonals = 0;
|
||||
var interiorAdjacentPoints = 0;
|
||||
var sourceFaceMismatches = 0;
|
||||
var sourceDominantAxisDetours = 0;
|
||||
var sourceScoringIssues = 0;
|
||||
|
||||
foreach (var edge in edges)
|
||||
{
|
||||
var path = ExtractPath(edge);
|
||||
if (path.Count < 2)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (nodesById.TryGetValue(edge.SourceNodeId ?? string.Empty, out var sourceNode)
|
||||
&& ElkShapeBoundaries.IsGatewayShape(sourceNode))
|
||||
{
|
||||
if (ElkShapeBoundaries.IsNearGatewayVertex(sourceNode, path[0]))
|
||||
{
|
||||
sourceVertexExits++;
|
||||
focus.Add(edge.Id);
|
||||
}
|
||||
|
||||
if (HasGatewayCornerDiagonalArtifact(path, sourceNode, fromSource: true))
|
||||
{
|
||||
cornerDiagonals++;
|
||||
focus.Add(edge.Id);
|
||||
}
|
||||
|
||||
if (HasGatewayInteriorAdjacentPointArtifact(path, sourceNode, fromSource: true))
|
||||
{
|
||||
interiorAdjacentPoints++;
|
||||
focus.Add(edge.Id);
|
||||
}
|
||||
|
||||
if (HasGatewaySourcePreferredFaceMismatchArtifact(
|
||||
path,
|
||||
sourceNode,
|
||||
nodes,
|
||||
edge.SourceNodeId,
|
||||
edge.TargetNodeId))
|
||||
{
|
||||
sourceFaceMismatches++;
|
||||
focus.Add(edge.Id);
|
||||
}
|
||||
|
||||
if (HasGatewaySourceDominantAxisDetourArtifact(
|
||||
path,
|
||||
sourceNode,
|
||||
nodes,
|
||||
edge.SourceNodeId,
|
||||
edge.TargetNodeId))
|
||||
{
|
||||
sourceDominantAxisDetours++;
|
||||
focus.Add(edge.Id);
|
||||
}
|
||||
|
||||
if (ElkEdgePostProcessor.HasClearGatewaySourceScoringOpportunity(
|
||||
path,
|
||||
sourceNode,
|
||||
nodes,
|
||||
edge.SourceNodeId,
|
||||
edge.TargetNodeId))
|
||||
{
|
||||
sourceScoringIssues++;
|
||||
focus.Add(edge.Id);
|
||||
}
|
||||
}
|
||||
|
||||
if (nodesById.TryGetValue(edge.TargetNodeId ?? string.Empty, out var targetNode)
|
||||
&& ElkShapeBoundaries.IsGatewayShape(targetNode))
|
||||
{
|
||||
if (HasGatewayCornerDiagonalArtifact(path, targetNode, fromSource: false))
|
||||
{
|
||||
cornerDiagonals++;
|
||||
focus.Add(edge.Id);
|
||||
}
|
||||
|
||||
if (HasGatewayInteriorAdjacentPointArtifact(path, targetNode, fromSource: false))
|
||||
{
|
||||
interiorAdjacentPoints++;
|
||||
focus.Add(edge.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
focusEdgeIds = focus.OrderBy(edgeId => edgeId, StringComparer.Ordinal).ToArray();
|
||||
return new GatewayArtifactState(
|
||||
sourceVertexExits,
|
||||
cornerDiagonals,
|
||||
interiorAdjacentPoints,
|
||||
sourceFaceMismatches,
|
||||
sourceDominantAxisDetours,
|
||||
sourceScoringIssues);
|
||||
}
|
||||
|
||||
private static bool HasGatewayCornerDiagonalArtifact(
|
||||
IReadOnlyList<ElkPoint> path,
|
||||
ElkPositionedNode node,
|
||||
bool fromSource)
|
||||
{
|
||||
if (!ElkShapeBoundaries.IsGatewayShape(node) || path.Count < 2)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var boundary = fromSource ? path[0] : path[^1];
|
||||
var adjacent = fromSource ? path[1] : path[^2];
|
||||
var deltaX = Math.Abs(boundary.X - adjacent.X);
|
||||
var deltaY = Math.Abs(boundary.Y - adjacent.Y);
|
||||
return deltaX >= 3d
|
||||
&& deltaY >= 3d
|
||||
&& ElkShapeBoundaries.IsNearGatewayVertex(node, boundary);
|
||||
}
|
||||
|
||||
private static bool HasGatewayInteriorAdjacentPointArtifact(
|
||||
IReadOnlyList<ElkPoint> path,
|
||||
ElkPositionedNode node,
|
||||
bool fromSource)
|
||||
{
|
||||
if (!ElkShapeBoundaries.IsGatewayShape(node) || path.Count < 2)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var adjacent = fromSource ? path[1] : path[^2];
|
||||
return ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(node, adjacent);
|
||||
}
|
||||
|
||||
private static bool HasGatewaySourcePreferredFaceMismatchArtifact(
|
||||
IReadOnlyList<ElkPoint> path,
|
||||
ElkPositionedNode sourceNode,
|
||||
IReadOnlyCollection<ElkPositionedNode> allNodes,
|
||||
string? sourceNodeId,
|
||||
string? targetNodeId)
|
||||
{
|
||||
if (!ElkShapeBoundaries.IsGatewayShape(sourceNode)
|
||||
|| path.Count < 2
|
||||
|| !ElkEdgePostProcessor.HasClearGatewaySourceDirectRepairOpportunity(
|
||||
path,
|
||||
sourceNode,
|
||||
allNodes,
|
||||
sourceNodeId,
|
||||
targetNodeId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var centerX = sourceNode.X + (sourceNode.Width / 2d);
|
||||
var centerY = sourceNode.Y + (sourceNode.Height / 2d);
|
||||
var desiredDx = path[^1].X - centerX;
|
||||
var desiredDy = path[^1].Y - centerY;
|
||||
var boundaryDx = path[0].X - centerX;
|
||||
var boundaryDy = path[0].Y - centerY;
|
||||
|
||||
if (Math.Abs(desiredDx) >= Math.Abs(desiredDy) * 1.25d)
|
||||
{
|
||||
return Math.Sign(boundaryDx) != Math.Sign(desiredDx)
|
||||
|| Math.Abs(boundaryDy) > sourceNode.Height * 0.28d;
|
||||
}
|
||||
|
||||
if (Math.Abs(desiredDy) >= Math.Abs(desiredDx) * 1.25d)
|
||||
{
|
||||
return Math.Sign(boundaryDy) != Math.Sign(desiredDy)
|
||||
|| Math.Abs(boundaryDx) > sourceNode.Width * 0.28d;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool HasGatewaySourceDominantAxisDetourArtifact(
|
||||
IReadOnlyList<ElkPoint> path,
|
||||
ElkPositionedNode sourceNode,
|
||||
IReadOnlyCollection<ElkPositionedNode> allNodes,
|
||||
string? sourceNodeId,
|
||||
string? targetNodeId)
|
||||
{
|
||||
if (!ElkShapeBoundaries.IsGatewayShape(sourceNode)
|
||||
|| path.Count < 3
|
||||
|| !ElkEdgePostProcessor.HasClearGatewaySourceDirectRepairOpportunity(
|
||||
path,
|
||||
sourceNode,
|
||||
allNodes,
|
||||
sourceNodeId,
|
||||
targetNodeId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const double coordinateTolerance = 0.5d;
|
||||
var centerX = sourceNode.X + (sourceNode.Width / 2d);
|
||||
var centerY = sourceNode.Y + (sourceNode.Height / 2d);
|
||||
var desiredDx = path[^1].X - centerX;
|
||||
var desiredDy = path[^1].Y - centerY;
|
||||
var dominantHorizontal = Math.Abs(desiredDx) >= Math.Abs(desiredDy) * 1.25d && Math.Sign(desiredDx) != 0;
|
||||
var dominantVertical = Math.Abs(desiredDy) >= Math.Abs(desiredDx) * 1.25d && Math.Sign(desiredDy) != 0;
|
||||
if (!dominantHorizontal && !dominantVertical)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var boundary = path[0];
|
||||
var adjacent = path[1];
|
||||
var firstDx = adjacent.X - boundary.X;
|
||||
var firstDy = adjacent.Y - boundary.Y;
|
||||
if (dominantHorizontal)
|
||||
{
|
||||
if (Math.Sign(firstDx) != Math.Sign(desiredDx) || Math.Abs(firstDx) <= coordinateTolerance)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return Math.Abs(firstDy) > Math.Max(24d, Math.Abs(desiredDy) + 12d)
|
||||
&& Math.Abs(firstDy) > Math.Abs(firstDx) * 1.25d;
|
||||
}
|
||||
|
||||
if (Math.Sign(firstDy) != Math.Sign(desiredDy) || Math.Abs(firstDy) <= coordinateTolerance)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return Math.Abs(firstDx) > Math.Max(24d, Math.Abs(desiredDx) + 12d)
|
||||
&& Math.Abs(firstDx) > Math.Abs(firstDy) * 1.25d;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user