Complete ElkSharp document rendering cleanup and source decomposition
- 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>
This commit is contained in:
@@ -0,0 +1,384 @@
|
||||
namespace StellaOps.ElkSharp;
|
||||
|
||||
internal static partial class ElkEdgePostProcessor
|
||||
{
|
||||
private static bool TryPolishGatewayUnderNodeTargetPeerConflicts(
|
||||
ElkRoutedEdge candidateEdge,
|
||||
IReadOnlyList<ElkRoutedEdge> currentEdges,
|
||||
int candidateIndex,
|
||||
ElkPositionedNode[] nodes,
|
||||
double minLineClearance,
|
||||
out ElkRoutedEdge polishedEdge)
|
||||
{
|
||||
polishedEdge = candidateEdge;
|
||||
if (string.IsNullOrWhiteSpace(candidateEdge.TargetNodeId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal);
|
||||
if (!nodesById.TryGetValue(candidateEdge.TargetNodeId, out var targetNode)
|
||||
|| !ElkShapeBoundaries.IsGatewayShape(targetNode))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
nodesById.TryGetValue(candidateEdge.SourceNodeId ?? string.Empty, out var sourceNode);
|
||||
var peerEdges = currentEdges
|
||||
.Where((edge, index) =>
|
||||
index != candidateIndex
|
||||
&& string.Equals(edge.TargetNodeId, candidateEdge.TargetNodeId, StringComparison.Ordinal))
|
||||
.ToArray();
|
||||
if (peerEdges.Length == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var path = ExtractFullPath(candidateEdge);
|
||||
if (path.Count < 2)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var sourceNodeId = candidateEdge.SourceNodeId;
|
||||
var targetNodeId = candidateEdge.TargetNodeId;
|
||||
var currentBundle = peerEdges
|
||||
.Append(candidateEdge)
|
||||
.ToArray();
|
||||
var currentTargetJoinViolations = ElkEdgeRoutingScoring.CountTargetApproachJoinViolations(currentBundle, nodes);
|
||||
var currentSharedLaneViolations = ElkEdgeRoutingScoring.CountSharedLaneViolations(currentBundle, nodes);
|
||||
var currentUnderNodeSegments = CountUnderNodeSegments(path, nodes, sourceNodeId, targetNodeId, minLineClearance);
|
||||
var currentUnderNodeViolations = ElkEdgeRoutingScoring.CountUnderNodeViolations([candidateEdge], nodes);
|
||||
var currentLocalHardPressure = ComputeUnderNodeRepairLocalHardPressure(candidateEdge, nodes);
|
||||
var currentPathLength = ComputePathLength(path);
|
||||
if (currentTargetJoinViolations == 0
|
||||
&& currentSharedLaneViolations == 0
|
||||
&& currentUnderNodeSegments == 0
|
||||
&& currentUnderNodeViolations == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var bestEdge = default(ElkRoutedEdge);
|
||||
var bestTargetJoinViolations = currentTargetJoinViolations;
|
||||
var bestSharedLaneViolations = currentSharedLaneViolations;
|
||||
var bestUnderNodeSegments = currentUnderNodeSegments;
|
||||
var bestUnderNodeViolations = currentUnderNodeViolations;
|
||||
var bestLocalHardPressure = currentLocalHardPressure;
|
||||
var bestPathLength = currentPathLength;
|
||||
|
||||
foreach (var candidatePath in EnumerateGatewayUnderNodePeerConflictCandidates(
|
||||
path,
|
||||
targetNode,
|
||||
sourceNode,
|
||||
peerEdges,
|
||||
nodes,
|
||||
sourceNodeId,
|
||||
targetNodeId,
|
||||
minLineClearance))
|
||||
{
|
||||
if (!PathChanged(path, candidatePath)
|
||||
|| candidatePath.Count < 2
|
||||
|| HasNodeObstacleCrossing(candidatePath, nodes, sourceNodeId, targetNodeId)
|
||||
|| !CanAcceptGatewayTargetRepair(candidatePath, targetNode)
|
||||
|| !HasAcceptableGatewayBoundaryPath(candidatePath, nodes, sourceNodeId, targetNodeId, targetNode, fromStart: false))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var localCandidateEdge = BuildSingleSectionEdge(candidateEdge, candidatePath);
|
||||
var localBundle = peerEdges
|
||||
.Append(localCandidateEdge)
|
||||
.ToArray();
|
||||
var candidateTargetJoinViolations = ElkEdgeRoutingScoring.CountTargetApproachJoinViolations(localBundle, nodes);
|
||||
var candidateSharedLaneViolations = ElkEdgeRoutingScoring.CountSharedLaneViolations(localBundle, nodes);
|
||||
var candidateUnderNodeSegments = CountUnderNodeSegments(candidatePath, nodes, sourceNodeId, targetNodeId, minLineClearance);
|
||||
var candidateUnderNodeViolations = ElkEdgeRoutingScoring.CountUnderNodeViolations([localCandidateEdge], nodes);
|
||||
var candidateLocalHardPressure = ComputeUnderNodeRepairLocalHardPressure(localCandidateEdge, nodes);
|
||||
var candidatePathLength = ComputePathLength(candidatePath);
|
||||
|
||||
if (!IsBetterGatewayUnderNodePeerConflictCandidate(
|
||||
candidateTargetJoinViolations,
|
||||
candidateSharedLaneViolations,
|
||||
candidateUnderNodeSegments,
|
||||
candidateUnderNodeViolations,
|
||||
candidateLocalHardPressure,
|
||||
candidatePathLength,
|
||||
bestTargetJoinViolations,
|
||||
bestSharedLaneViolations,
|
||||
bestUnderNodeSegments,
|
||||
bestUnderNodeViolations,
|
||||
bestLocalHardPressure,
|
||||
bestPathLength))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
bestEdge = localCandidateEdge;
|
||||
bestTargetJoinViolations = candidateTargetJoinViolations;
|
||||
bestSharedLaneViolations = candidateSharedLaneViolations;
|
||||
bestUnderNodeSegments = candidateUnderNodeSegments;
|
||||
bestUnderNodeViolations = candidateUnderNodeViolations;
|
||||
bestLocalHardPressure = candidateLocalHardPressure;
|
||||
bestPathLength = candidatePathLength;
|
||||
}
|
||||
|
||||
if (bestEdge is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
polishedEdge = bestEdge;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static IEnumerable<List<ElkPoint>> EnumerateGatewayUnderNodePeerConflictCandidates(
|
||||
IReadOnlyList<ElkPoint> path,
|
||||
ElkPositionedNode targetNode,
|
||||
ElkPositionedNode? sourceNode,
|
||||
IReadOnlyCollection<ElkRoutedEdge> peerEdges,
|
||||
IReadOnlyCollection<ElkPositionedNode> nodes,
|
||||
string? sourceNodeId,
|
||||
string? targetNodeId,
|
||||
double minLineClearance)
|
||||
{
|
||||
foreach (var side in EnumerateGatewayUnderNodePeerConflictSides(path, targetNode, peerEdges))
|
||||
{
|
||||
var slotCoordinates = EnumerateGatewayUnderNodePeerConflictSlotCoordinates(
|
||||
path,
|
||||
targetNode,
|
||||
sourceNode,
|
||||
peerEdges,
|
||||
side,
|
||||
minLineClearance)
|
||||
.ToArray();
|
||||
if (slotCoordinates.Length == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var slotCoordinate in slotCoordinates)
|
||||
{
|
||||
if (sourceNode is not null
|
||||
&& ElkShapeBoundaries.TryProjectGatewayBoundarySlot(targetNode, side, slotCoordinate, out var bandBoundary)
|
||||
&& TryBuildSafeHorizontalBandCandidate(
|
||||
sourceNode,
|
||||
targetNode,
|
||||
nodes,
|
||||
sourceNodeId,
|
||||
targetNodeId,
|
||||
path[0],
|
||||
bandBoundary,
|
||||
minLineClearance,
|
||||
preferredSourceExterior: null,
|
||||
out var bandCandidate))
|
||||
{
|
||||
yield return bandCandidate;
|
||||
}
|
||||
|
||||
foreach (var axis in EnumerateGatewayUnderNodePeerConflictAxes(
|
||||
path,
|
||||
targetNode,
|
||||
side,
|
||||
nodes,
|
||||
sourceNodeId,
|
||||
targetNodeId,
|
||||
minLineClearance))
|
||||
{
|
||||
yield return BuildMixedTargetFaceCandidate(path, targetNode, side, slotCoordinate, axis);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<string> EnumerateGatewayUnderNodePeerConflictSides(
|
||||
IReadOnlyList<ElkPoint> path,
|
||||
ElkPositionedNode targetNode,
|
||||
IReadOnlyCollection<ElkRoutedEdge> peerEdges)
|
||||
{
|
||||
var seen = new HashSet<string>(StringComparer.Ordinal);
|
||||
var currentSide = ResolveTargetApproachSide(path, targetNode);
|
||||
var peerSides = peerEdges
|
||||
.Select(edge => ExtractFullPath(edge))
|
||||
.Where(peerPath => peerPath.Count >= 2)
|
||||
.Select(peerPath => ResolveTargetApproachSide(peerPath, targetNode))
|
||||
.ToHashSet(StringComparer.Ordinal);
|
||||
|
||||
foreach (var side in new[] { "top", "bottom", "right", "left" })
|
||||
{
|
||||
if (!string.Equals(side, currentSide, StringComparison.Ordinal)
|
||||
&& !peerSides.Contains(side)
|
||||
&& seen.Add(side))
|
||||
{
|
||||
yield return side;
|
||||
}
|
||||
}
|
||||
|
||||
if (seen.Add(currentSide))
|
||||
{
|
||||
yield return currentSide;
|
||||
}
|
||||
|
||||
foreach (var side in new[] { "top", "bottom", "right", "left" })
|
||||
{
|
||||
if (seen.Add(side))
|
||||
{
|
||||
yield return side;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<double> EnumerateGatewayUnderNodePeerConflictSlotCoordinates(
|
||||
IReadOnlyList<ElkPoint> path,
|
||||
ElkPositionedNode targetNode,
|
||||
ElkPositionedNode? sourceNode,
|
||||
IReadOnlyCollection<ElkRoutedEdge> peerEdges,
|
||||
string side,
|
||||
double minLineClearance)
|
||||
{
|
||||
var coordinates = new List<double>();
|
||||
var inset = 10d;
|
||||
var spacing = Math.Max(14d, minLineClearance + 6d);
|
||||
var centerX = targetNode.X + (targetNode.Width / 2d);
|
||||
var centerY = targetNode.Y + (targetNode.Height / 2d);
|
||||
var slotMinimum = side is "left" or "right" ? targetNode.Y + inset : targetNode.X + inset;
|
||||
var slotMaximum = side is "left" or "right"
|
||||
? targetNode.Y + targetNode.Height - inset
|
||||
: targetNode.X + targetNode.Width - inset;
|
||||
|
||||
void AddClamped(double value)
|
||||
{
|
||||
AddUniqueCoordinate(coordinates, Math.Max(slotMinimum, Math.Min(slotMaximum, value)));
|
||||
}
|
||||
|
||||
if (side is "left" or "right")
|
||||
{
|
||||
AddClamped(path[^1].Y);
|
||||
foreach (var peer in peerEdges)
|
||||
{
|
||||
var peerPath = ExtractFullPath(peer);
|
||||
if (peerPath.Count > 0)
|
||||
{
|
||||
AddClamped(peerPath[^1].Y - spacing);
|
||||
AddClamped(peerPath[^1].Y + spacing);
|
||||
AddClamped(peerPath[^1].Y);
|
||||
}
|
||||
}
|
||||
|
||||
if (sourceNode is not null)
|
||||
{
|
||||
AddClamped(sourceNode.Y + (sourceNode.Height / 2d));
|
||||
}
|
||||
|
||||
AddClamped(centerY - spacing);
|
||||
AddClamped(centerY);
|
||||
AddClamped(centerY + spacing);
|
||||
}
|
||||
else
|
||||
{
|
||||
AddClamped(path[^1].X);
|
||||
foreach (var peer in peerEdges)
|
||||
{
|
||||
var peerPath = ExtractFullPath(peer);
|
||||
if (peerPath.Count > 0)
|
||||
{
|
||||
AddClamped(peerPath[^1].X - spacing);
|
||||
AddClamped(peerPath[^1].X + spacing);
|
||||
AddClamped(peerPath[^1].X);
|
||||
}
|
||||
}
|
||||
|
||||
if (sourceNode is not null)
|
||||
{
|
||||
AddClamped(sourceNode.X + (sourceNode.Width / 2d));
|
||||
}
|
||||
|
||||
AddClamped(centerX - spacing);
|
||||
AddClamped(centerX);
|
||||
AddClamped(centerX + spacing);
|
||||
}
|
||||
|
||||
foreach (var coordinate in coordinates.Take(8))
|
||||
{
|
||||
yield return coordinate;
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<double> EnumerateGatewayUnderNodePeerConflictAxes(
|
||||
IReadOnlyList<ElkPoint> path,
|
||||
ElkPositionedNode targetNode,
|
||||
string side,
|
||||
IReadOnlyCollection<ElkPositionedNode> nodes,
|
||||
string? sourceNodeId,
|
||||
string? targetNodeId,
|
||||
double minLineClearance)
|
||||
{
|
||||
var coordinates = new List<double>();
|
||||
var currentAxis = ResolveTargetApproachAxisValue(path, side);
|
||||
if (!double.IsNaN(currentAxis))
|
||||
{
|
||||
AddUniqueCoordinate(coordinates, currentAxis);
|
||||
}
|
||||
|
||||
AddUniqueCoordinate(coordinates, ResolveDefaultTargetApproachAxis(targetNode, side));
|
||||
|
||||
var clearance = Math.Max(24d, minLineClearance * 0.6d);
|
||||
if (side is "top" or "bottom")
|
||||
{
|
||||
var minX = Math.Min(path[0].X, targetNode.X);
|
||||
var maxX = Math.Max(path[0].X, targetNode.X + targetNode.Width);
|
||||
var blockers = nodes
|
||||
.Where(node =>
|
||||
!string.Equals(node.Id, sourceNodeId, StringComparison.Ordinal)
|
||||
&& !string.Equals(node.Id, targetNodeId, StringComparison.Ordinal)
|
||||
&& maxX > node.X + 0.5d
|
||||
&& minX < node.X + node.Width - 0.5d)
|
||||
.ToArray();
|
||||
if (side == "top")
|
||||
{
|
||||
var highestBlockerY = blockers.Length > 0
|
||||
? blockers.Min(node => node.Y)
|
||||
: Math.Min(path[0].Y, targetNode.Y);
|
||||
AddUniqueCoordinate(coordinates, Math.Min(targetNode.Y - 8d, highestBlockerY - clearance));
|
||||
}
|
||||
else
|
||||
{
|
||||
var lowestBlockerY = blockers.Length > 0
|
||||
? blockers.Max(node => node.Y + node.Height)
|
||||
: Math.Max(path[0].Y, targetNode.Y + targetNode.Height);
|
||||
AddUniqueCoordinate(coordinates, Math.Max(targetNode.Y + targetNode.Height + 8d, lowestBlockerY + clearance));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var minY = Math.Min(path[0].Y, targetNode.Y);
|
||||
var maxY = Math.Max(path[0].Y, targetNode.Y + targetNode.Height);
|
||||
var blockers = nodes
|
||||
.Where(node =>
|
||||
!string.Equals(node.Id, sourceNodeId, StringComparison.Ordinal)
|
||||
&& !string.Equals(node.Id, targetNodeId, StringComparison.Ordinal)
|
||||
&& maxY > node.Y + 0.5d
|
||||
&& minY < node.Y + node.Height - 0.5d)
|
||||
.ToArray();
|
||||
if (side == "left")
|
||||
{
|
||||
var leftmostBlockerX = blockers.Length > 0
|
||||
? blockers.Min(node => node.X)
|
||||
: Math.Min(path[0].X, targetNode.X);
|
||||
AddUniqueCoordinate(coordinates, Math.Min(targetNode.X - 8d, leftmostBlockerX - clearance));
|
||||
}
|
||||
else
|
||||
{
|
||||
var rightmostBlockerX = blockers.Length > 0
|
||||
? blockers.Max(node => node.X + node.Width)
|
||||
: Math.Max(path[0].X, targetNode.X + targetNode.Width);
|
||||
AddUniqueCoordinate(coordinates, Math.Max(targetNode.X + targetNode.Width + 8d, rightmostBlockerX + clearance));
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var coordinate in coordinates.Take(6))
|
||||
{
|
||||
yield return coordinate;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user