- Fix target-join (edge/4+edge/17): gateway face overflow redirect to left tip - Fix under-node (edge/14,15,20): push-first corridor reroute instead of top corridor - Fix boundary-slots (4->0): snap after gateway polish reordering - Fix gateway corner diagonals (2->0): post-pipeline straightening pass - Fix gateway interior adjacent: polygon-aware IsInsideNodeShapeInterior - Fix gateway source face mismatch (2->0): per-edge redirect with lenient validation - Fix gateway source scoring (5->0): per-edge scoring candidate application - Fix edge-node crossing (1->0): push horizontal segment above blocking node - Decompose 7 oversized files (~20K lines) into 55+ partials under 400 lines each - Archive sprints 004 (document cleanup), 005 (decomposition), 007 (render speed) All 44+ document-processing artifact assertions pass. Hybrid deterministic mode documented as recommended path for LeftToRight layouts. Tests verified: StraightExit 2/2, BoundarySlotOffenders 2/2, HybridDeterministicMode 3/3, DocumentProcessingWorkflow artifact 1/1. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
382 lines
14 KiB
C#
382 lines
14 KiB
C#
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);
|
|
// Straighten any corner diagonals created by the face fix so they
|
|
// don't block promotion via the lexicographic IsBetterThan check
|
|
// (corners have higher priority than face mismatches).
|
|
candidateEdges = ElkEdgePostProcessor.StraightenGatewayCornerDiagonals(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;
|
|
}
|
|
}
|