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,336 @@
|
||||
namespace StellaOps.ElkSharp;
|
||||
|
||||
internal static partial class ElkEdgePostProcessor
|
||||
{
|
||||
internal static ElkRoutedEdge[] SeparateMixedNodeFaceLaneConflicts(
|
||||
ElkRoutedEdge[] edges,
|
||||
ElkPositionedNode[] nodes,
|
||||
double minLineClearance,
|
||||
IReadOnlyCollection<string>? restrictedEdgeIds = null)
|
||||
{
|
||||
if (edges.Length < 2 || nodes.Length == 0)
|
||||
{
|
||||
return edges;
|
||||
}
|
||||
|
||||
var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal);
|
||||
var graphMinY = nodes.Min(node => node.Y);
|
||||
var graphMaxY = nodes.Max(node => node.Y + node.Height);
|
||||
var restrictedSet = restrictedEdgeIds is null
|
||||
? null
|
||||
: restrictedEdgeIds.ToHashSet(StringComparer.Ordinal);
|
||||
var result = edges.ToArray();
|
||||
var entries = new List<(int Index, ElkRoutedEdge Edge, IReadOnlyList<ElkPoint> Path, ElkPositionedNode Node, string Side, bool IsOutgoing, ElkPoint Boundary, double BoundaryCoordinate, double AxisValue)>();
|
||||
|
||||
for (var index = 0; index < result.Length; index++)
|
||||
{
|
||||
var edge = result[index];
|
||||
var path = ExtractFullPath(edge);
|
||||
if (path.Count < 2)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (nodesById.TryGetValue(edge.SourceNodeId ?? string.Empty, out var sourceNode)
|
||||
&& (ShouldSpreadSourceDeparture(edge, graphMinY, graphMaxY)
|
||||
|| ElkShapeBoundaries.IsGatewayShape(sourceNode)))
|
||||
{
|
||||
var side = ResolveSourceDepartureSide(path, sourceNode);
|
||||
var axisValue = TryExtractSourceDepartureRun(path, side, out _, out var runEndIndex)
|
||||
? side is "left" or "right"
|
||||
? path[runEndIndex].X
|
||||
: path[runEndIndex].Y
|
||||
: ResolveDefaultSourceDepartureAxis(sourceNode, side);
|
||||
entries.Add((
|
||||
index,
|
||||
edge,
|
||||
path,
|
||||
sourceNode,
|
||||
side,
|
||||
true,
|
||||
path[0],
|
||||
side is "left" or "right" ? path[0].Y : path[0].X,
|
||||
axisValue));
|
||||
}
|
||||
|
||||
if (nodesById.TryGetValue(edge.TargetNodeId ?? string.Empty, out var targetNode)
|
||||
&& ShouldSpreadTargetApproach(edge, graphMinY, graphMaxY))
|
||||
{
|
||||
var side = ResolveTargetApproachSide(path, targetNode);
|
||||
var axisValue = ResolveTargetApproachAxisValue(path, side);
|
||||
if (double.IsNaN(axisValue))
|
||||
{
|
||||
axisValue = side is "left" or "right" ? path[^1].Y : path[^1].X;
|
||||
}
|
||||
|
||||
entries.Add((
|
||||
index,
|
||||
edge,
|
||||
path,
|
||||
targetNode,
|
||||
side,
|
||||
false,
|
||||
path[^1],
|
||||
side is "left" or "right" ? path[^1].Y : path[^1].X,
|
||||
axisValue));
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var group in entries.GroupBy(
|
||||
entry => $"{entry.Node.Id}|{entry.Side}",
|
||||
StringComparer.Ordinal))
|
||||
{
|
||||
var groupEntries = group.ToArray();
|
||||
var hasBoundarySlotIssue = groupEntries.Length >= 2
|
||||
&& HasBoundarySlotAlignmentIssue(
|
||||
groupEntries
|
||||
.Select(entry => (entry.Edge.Id, entry.BoundaryCoordinate, entry.IsOutgoing))
|
||||
.ToArray(),
|
||||
groupEntries[0].Node,
|
||||
groupEntries[0].Side,
|
||||
minLineClearance);
|
||||
if (groupEntries.Length < 2
|
||||
|| !groupEntries.Any(entry => entry.IsOutgoing)
|
||||
|| !groupEntries.Any(entry => !entry.IsOutgoing)
|
||||
|| (!GroupHasMixedNodeFaceLaneConflict(groupEntries, minLineClearance) && !hasBoundarySlotIssue))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (restrictedSet is not null && !groupEntries.Any(entry => restrictedSet.Contains(entry.Edge.Id)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var node = groupEntries[0].Node;
|
||||
var side = groupEntries[0].Side;
|
||||
var orderedEntries = groupEntries
|
||||
.OrderBy(entry => entry.BoundaryCoordinate)
|
||||
.ThenBy(entry => entry.IsOutgoing ? 0 : 1)
|
||||
.ThenBy(entry => IsRepeatCollectorLabel(entry.Edge.Label) ? 1 : 0)
|
||||
.ThenBy(entry => entry.Edge.Id, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
var assignedSlotCoordinates = ElkBoundarySlots.BuildAssignedBoundarySlotCoordinates(
|
||||
node,
|
||||
side,
|
||||
orderedEntries.Select(entry => entry.BoundaryCoordinate).ToArray());
|
||||
var desiredCoordinateByEdgeId = new Dictionary<string, double>(StringComparer.Ordinal);
|
||||
for (var i = 0; i < orderedEntries.Length; i++)
|
||||
{
|
||||
desiredCoordinateByEdgeId[orderedEntries[i].Edge.Id] = assignedSlotCoordinates[i];
|
||||
}
|
||||
var hasAssignedSlotCollision = HasDuplicateBoundarySlotCoordinates(assignedSlotCoordinates);
|
||||
|
||||
foreach (var entry in groupEntries)
|
||||
{
|
||||
var forceAlternateGatewayFaceCandidate = hasAssignedSlotCollision
|
||||
&& ElkShapeBoundaries.IsGatewayShape(entry.Node);
|
||||
if (!desiredCoordinateByEdgeId.TryGetValue(entry.Edge.Id, out var desiredCoordinate)
|
||||
|| (!forceAlternateGatewayFaceCandidate
|
||||
&& Math.Abs(desiredCoordinate - entry.BoundaryCoordinate) <= 0.5d))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var bestEdge = result[entry.Index];
|
||||
var currentGroupEdges = groupEntries
|
||||
.Select(item => result[item.Index])
|
||||
.ToArray();
|
||||
var bestSharedLaneViolations = ElkEdgeRoutingScoring.CountSharedLaneViolations(currentGroupEdges, nodes);
|
||||
var bestTargetJoinViolations = ElkEdgeRoutingScoring.CountTargetApproachJoinViolations(currentGroupEdges, nodes);
|
||||
var bestBoundarySlotViolations = ElkEdgeRoutingScoring.CountBoundarySlotViolations(currentGroupEdges, nodes);
|
||||
var bestBoundaryAngleViolations = ElkEdgeRoutingScoring.CountBadBoundaryAngles(currentGroupEdges, nodes);
|
||||
var bestGatewaySourceExitViolations = ElkEdgeRoutingScoring.CountGatewaySourceExitViolations(currentGroupEdges, nodes);
|
||||
var bestUnderNodeViolations = ElkEdgeRoutingScoring.CountUnderNodeViolations(currentGroupEdges, nodes);
|
||||
var bestPathLength = ComputePathLength(entry.Path);
|
||||
var prefersAlternateRepeatFace = !entry.IsOutgoing
|
||||
&& !ElkShapeBoundaries.IsGatewayShape(entry.Node)
|
||||
&& IsRepeatCollectorLabel(entry.Edge.Label)
|
||||
&& groupEntries.Any(other => other.IsOutgoing);
|
||||
var candidatePaths = new List<IReadOnlyList<ElkPoint>>();
|
||||
var directCandidate = entry.IsOutgoing
|
||||
? BuildMixedSourceFaceCandidate(entry.Path, entry.Node, side, desiredCoordinate, entry.AxisValue)
|
||||
: BuildMixedTargetFaceCandidate(entry.Path, entry.Node, side, desiredCoordinate, entry.AxisValue);
|
||||
AddUniquePathCandidate(candidatePaths, directCandidate);
|
||||
var availableSpan = Math.Abs(desiredCoordinate - entry.BoundaryCoordinate);
|
||||
if ((forceAlternateGatewayFaceCandidate || prefersAlternateRepeatFace || availableSpan + 0.5d < minLineClearance)
|
||||
&& TryBuildAlternateMixedFaceCandidate(entry, nodes, minLineClearance, out var alternateCandidate))
|
||||
{
|
||||
AddUniquePathCandidate(candidatePaths, alternateCandidate);
|
||||
}
|
||||
|
||||
foreach (var candidate in candidatePaths)
|
||||
{
|
||||
if (!PathChanged(entry.Path, candidate)
|
||||
|| HasNodeObstacleCrossing(candidate, nodes, entry.Edge.SourceNodeId, entry.Edge.TargetNodeId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.IsOutgoing)
|
||||
{
|
||||
if (ElkShapeBoundaries.IsGatewayShape(entry.Node))
|
||||
{
|
||||
if (!HasAcceptableGatewayBoundaryPath(candidate, nodes, entry.Edge.SourceNodeId, entry.Edge.TargetNodeId, entry.Node, fromStart: true))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else if (!HasClearBoundarySegments(candidate, nodes, entry.Edge.SourceNodeId, entry.Edge.TargetNodeId, true, 2)
|
||||
|| !HasValidBoundaryAngle(candidate[0], candidate[1], entry.Node))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (ElkShapeBoundaries.IsGatewayShape(entry.Node))
|
||||
{
|
||||
if (!CanAcceptGatewayTargetRepair(candidate, entry.Node)
|
||||
|| !HasAcceptableGatewayBoundaryPath(candidate, nodes, entry.Edge.SourceNodeId, entry.Edge.TargetNodeId, entry.Node, fromStart: false))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else if (!HasClearBoundarySegments(candidate, nodes, entry.Edge.SourceNodeId, entry.Edge.TargetNodeId, false, 4)
|
||||
|| !HasValidBoundaryAngle(candidate[^1], candidate[^2], entry.Node)
|
||||
|| HasTargetApproachBacktracking(candidate, entry.Node))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
var candidateEdge = BuildSingleSectionEdge(entry.Edge, candidate);
|
||||
var candidateGroupEdges = groupEntries
|
||||
.Select(item => item.Index == entry.Index ? candidateEdge : result[item.Index])
|
||||
.ToArray();
|
||||
var candidateSharedLaneViolations = ElkEdgeRoutingScoring.CountSharedLaneViolations(candidateGroupEdges, nodes);
|
||||
var candidateTargetJoinViolations = ElkEdgeRoutingScoring.CountTargetApproachJoinViolations(candidateGroupEdges, nodes);
|
||||
var candidateBoundarySlotViolations = ElkEdgeRoutingScoring.CountBoundarySlotViolations(candidateGroupEdges, nodes);
|
||||
var candidateBoundaryAngleViolations = ElkEdgeRoutingScoring.CountBadBoundaryAngles(candidateGroupEdges, nodes);
|
||||
var candidateGatewaySourceExitViolations = ElkEdgeRoutingScoring.CountGatewaySourceExitViolations(candidateGroupEdges, nodes);
|
||||
var candidateUnderNodeViolations = ElkEdgeRoutingScoring.CountUnderNodeViolations(candidateGroupEdges, nodes);
|
||||
var candidatePathLength = ComputePathLength(candidate);
|
||||
var prefersForcedAlternateGatewayFace = forceAlternateGatewayFaceCandidate
|
||||
&& entry.IsOutgoing
|
||||
&& ResolveSourceDepartureSide(candidate, entry.Node) != entry.Side
|
||||
&& ResolveSourceDepartureSide(ExtractFullPath(bestEdge), entry.Node) == entry.Side
|
||||
&& candidateSharedLaneViolations <= bestSharedLaneViolations
|
||||
&& candidateTargetJoinViolations <= bestTargetJoinViolations
|
||||
&& candidateBoundarySlotViolations <= bestBoundarySlotViolations
|
||||
&& candidateBoundaryAngleViolations <= bestBoundaryAngleViolations
|
||||
&& candidateGatewaySourceExitViolations <= bestGatewaySourceExitViolations
|
||||
&& candidateUnderNodeViolations <= bestUnderNodeViolations;
|
||||
|
||||
if (prefersForcedAlternateGatewayFace)
|
||||
{
|
||||
bestEdge = candidateEdge;
|
||||
bestSharedLaneViolations = candidateSharedLaneViolations;
|
||||
bestTargetJoinViolations = candidateTargetJoinViolations;
|
||||
bestBoundarySlotViolations = candidateBoundarySlotViolations;
|
||||
bestBoundaryAngleViolations = candidateBoundaryAngleViolations;
|
||||
bestGatewaySourceExitViolations = candidateGatewaySourceExitViolations;
|
||||
bestUnderNodeViolations = candidateUnderNodeViolations;
|
||||
bestPathLength = candidatePathLength;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!IsBetterMixedNodeFaceCandidate(
|
||||
candidateSharedLaneViolations,
|
||||
candidateTargetJoinViolations,
|
||||
candidateBoundarySlotViolations,
|
||||
candidateBoundaryAngleViolations,
|
||||
candidateGatewaySourceExitViolations,
|
||||
candidateUnderNodeViolations,
|
||||
candidatePathLength,
|
||||
bestSharedLaneViolations,
|
||||
bestTargetJoinViolations,
|
||||
bestBoundarySlotViolations,
|
||||
bestBoundaryAngleViolations,
|
||||
bestGatewaySourceExitViolations,
|
||||
bestUnderNodeViolations,
|
||||
bestPathLength))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
bestEdge = candidateEdge;
|
||||
bestSharedLaneViolations = candidateSharedLaneViolations;
|
||||
bestTargetJoinViolations = candidateTargetJoinViolations;
|
||||
bestBoundarySlotViolations = candidateBoundarySlotViolations;
|
||||
bestBoundaryAngleViolations = candidateBoundaryAngleViolations;
|
||||
bestGatewaySourceExitViolations = candidateGatewaySourceExitViolations;
|
||||
bestUnderNodeViolations = candidateUnderNodeViolations;
|
||||
bestPathLength = candidatePathLength;
|
||||
}
|
||||
|
||||
result[entry.Index] = bestEdge;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void AddUniquePathCandidate(
|
||||
ICollection<IReadOnlyList<ElkPoint>> candidates,
|
||||
IReadOnlyList<ElkPoint> candidate)
|
||||
{
|
||||
if (candidates.Any(existing =>
|
||||
existing.Count == candidate.Count
|
||||
&& existing.Zip(candidate, ElkEdgeRoutingGeometry.PointsEqual).All(equal => equal)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
candidates.Add(candidate);
|
||||
}
|
||||
|
||||
private static bool IsBetterMixedNodeFaceCandidate(
|
||||
int candidateSharedLaneViolations,
|
||||
int candidateTargetJoinViolations,
|
||||
int candidateBoundarySlotViolations,
|
||||
int candidateBoundaryAngleViolations,
|
||||
int candidateGatewaySourceExitViolations,
|
||||
int candidateUnderNodeViolations,
|
||||
double candidatePathLength,
|
||||
int currentSharedLaneViolations,
|
||||
int currentTargetJoinViolations,
|
||||
int currentBoundarySlotViolations,
|
||||
int currentBoundaryAngleViolations,
|
||||
int currentGatewaySourceExitViolations,
|
||||
int currentUnderNodeViolations,
|
||||
double currentPathLength)
|
||||
{
|
||||
if (candidateSharedLaneViolations != currentSharedLaneViolations)
|
||||
{
|
||||
return candidateSharedLaneViolations < currentSharedLaneViolations;
|
||||
}
|
||||
|
||||
if (candidateTargetJoinViolations != currentTargetJoinViolations)
|
||||
{
|
||||
return candidateTargetJoinViolations < currentTargetJoinViolations;
|
||||
}
|
||||
|
||||
if (candidateBoundarySlotViolations != currentBoundarySlotViolations)
|
||||
{
|
||||
return candidateBoundarySlotViolations < currentBoundarySlotViolations;
|
||||
}
|
||||
|
||||
if (candidateBoundaryAngleViolations != currentBoundaryAngleViolations)
|
||||
{
|
||||
return candidateBoundaryAngleViolations < currentBoundaryAngleViolations;
|
||||
}
|
||||
|
||||
if (candidateGatewaySourceExitViolations != currentGatewaySourceExitViolations)
|
||||
{
|
||||
return candidateGatewaySourceExitViolations < currentGatewaySourceExitViolations;
|
||||
}
|
||||
|
||||
if (candidateUnderNodeViolations != currentUnderNodeViolations)
|
||||
{
|
||||
return candidateUnderNodeViolations < currentUnderNodeViolations;
|
||||
}
|
||||
|
||||
return candidatePathLength + 0.5d < currentPathLength;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user