Files
git.stella-ops.org/src/__Libraries/StellaOps.ElkSharp/ElkEdgePostProcessor.FaceConflictRepair.GatewayEntry.cs
master d04483560b 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>
2026-04-01 14:16:10 +03:00

385 lines
15 KiB
C#

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;
}
}
}