Files
git.stella-ops.org/src/__Libraries/StellaOps.ElkSharp/ElkEdgePostProcessor.BoundarySlots.Snap.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

319 lines
12 KiB
C#

namespace StellaOps.ElkSharp;
internal static partial class ElkEdgePostProcessor
{
private static List<ElkPoint>? TryBuildGatewaySourceBoundarySlotSkirtCandidate(
IReadOnlyList<ElkPoint> currentPath,
ElkPositionedNode sourceNode,
ElkPoint boundaryPoint,
IReadOnlyCollection<ElkPositionedNode> nodes,
string? sourceNodeId,
string? targetNodeId,
double obstaclePadding)
{
if (!ElkShapeBoundaries.IsGatewayShape(sourceNode)
|| currentPath.Count < 3)
{
return null;
}
var firstExteriorIndex = FindFirstGatewayExteriorPointIndex(currentPath, sourceNode);
var preferredContinuationIndex = FindGatewaySourceCurlRecoveryIndex(currentPath, firstExteriorIndex)
?? FindPreferredGatewayExitContinuationIndex(currentPath, sourceNode, firstExteriorIndex);
var candidateIndices = new HashSet<int>
{
Math.Clamp(currentPath.Count - 2, 1, currentPath.Count - 1),
Math.Clamp(preferredContinuationIndex, 1, currentPath.Count - 1),
};
List<ElkPoint>? bestCandidate = null;
List<ElkPoint>? bestLaneRejoinCandidate = null;
var bestScore = double.PositiveInfinity;
var bestLaneRejoinScore = double.PositiveInfinity;
foreach (var continuationIndex in candidateIndices.OrderBy(index => index))
{
var continuationPoint = currentPath[continuationIndex];
var repairCandidates = new List<List<ElkPoint>>();
var laneRejoinCandidate = TryBuildGatewaySourceBoundarySlotLaneRejoinCandidate(
currentPath,
sourceNode,
boundaryPoint,
continuationPoint,
continuationIndex,
nodes,
sourceNodeId,
targetNodeId);
if (laneRejoinCandidate is not null && PathChanged(currentPath, laneRejoinCandidate))
{
var laneRejoinScore = ComputePathLength(laneRejoinCandidate) + (Math.Max(0, laneRejoinCandidate.Count - 2) * 4d);
if (laneRejoinScore < bestLaneRejoinScore)
{
bestLaneRejoinScore = laneRejoinScore;
bestLaneRejoinCandidate = laneRejoinCandidate;
}
}
var continuationAnchoredCandidate = BuildGatewaySourceRepairPath(
currentPath,
sourceNode,
boundaryPoint,
continuationPoint,
continuationIndex,
continuationPoint,
nodes,
sourceNodeId,
targetNodeId);
if (PathChanged(currentPath, continuationAnchoredCandidate))
{
repairCandidates.Add(continuationAnchoredCandidate);
}
var prefixCandidate = TryBuildLocalObstacleSkirtBoundaryShortcut(
currentPath,
boundaryPoint,
continuationPoint,
nodes,
sourceNodeId,
targetNodeId,
targetNode: null,
obstaclePadding);
if (prefixCandidate is not null && prefixCandidate.Count >= 2)
{
var rebuilt = new List<ElkPoint>(prefixCandidate);
for (var i = continuationIndex + 1; i < currentPath.Count; i++)
{
var point = currentPath[i];
if (!ElkEdgeRoutingGeometry.PointsEqual(rebuilt[^1], point))
{
rebuilt.Add(new ElkPoint { X = point.X, Y = point.Y });
}
}
var skirtCandidate = NormalizePathPoints(rebuilt);
if (PathChanged(currentPath, skirtCandidate))
{
repairCandidates.Add(skirtCandidate);
}
}
foreach (var candidate in repairCandidates)
{
var score = ComputePathLength(candidate) + (Math.Max(0, candidate.Count - 2) * 4d);
if (score >= bestScore)
{
continue;
}
bestScore = score;
bestCandidate = candidate;
}
}
return bestLaneRejoinCandidate ?? bestCandidate;
}
private static List<ElkPoint>? TryBuildGatewaySourceBoundarySlotLaneRejoinCandidate(
IReadOnlyList<ElkPoint> currentPath,
ElkPositionedNode sourceNode,
ElkPoint boundaryPoint,
ElkPoint continuationPoint,
int continuationIndex,
IReadOnlyCollection<ElkPositionedNode> nodes,
string? sourceNodeId,
string? targetNodeId)
{
if (!ElkShapeBoundaries.IsGatewayShape(sourceNode)
|| currentPath.Count < 3)
{
return null;
}
var boundarySide = ElkEdgeRoutingGeometry.ResolveBoundarySide(boundaryPoint, sourceNode);
var padding = 8d;
ElkPoint rejoinExterior;
switch (boundarySide)
{
case "left":
rejoinExterior = new ElkPoint { X = sourceNode.X - padding, Y = continuationPoint.Y };
break;
case "right":
rejoinExterior = new ElkPoint { X = sourceNode.X + sourceNode.Width + padding, Y = continuationPoint.Y };
break;
case "top":
rejoinExterior = new ElkPoint { X = continuationPoint.X, Y = sourceNode.Y - padding };
break;
case "bottom":
rejoinExterior = new ElkPoint { X = continuationPoint.X, Y = sourceNode.Y + sourceNode.Height + padding };
break;
default:
return null;
}
if (ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(sourceNode, rejoinExterior)
|| !ElkShapeBoundaries.HasValidGatewayBoundaryAngle(sourceNode, boundaryPoint, rejoinExterior))
{
return null;
}
var rebuilt = new List<ElkPoint> { boundaryPoint };
if (!ElkEdgeRoutingGeometry.PointsEqual(rebuilt[^1], rejoinExterior))
{
rebuilt.Add(rejoinExterior);
}
if (!ElkEdgeRoutingGeometry.PointsEqual(rebuilt[^1], continuationPoint))
{
rebuilt.Add(continuationPoint);
}
for (var i = continuationIndex + 1; i < currentPath.Count; i++)
{
var point = currentPath[i];
if (!ElkEdgeRoutingGeometry.PointsEqual(rebuilt[^1], point))
{
rebuilt.Add(new ElkPoint { X = point.X, Y = point.Y });
}
}
var candidate = NormalizePathPoints(rebuilt);
if (!PathChanged(currentPath, candidate)
|| HasNodeObstacleCrossing(candidate, nodes, sourceNodeId, targetNodeId)
|| !HasAcceptableGatewayBoundaryPath(candidate, nodes, sourceNodeId, targetNodeId, sourceNode, fromStart: true)
|| !HasCleanGatewaySourceBandPath(candidate, sourceNode))
{
return null;
}
return candidate;
}
private static bool TrySelectImprovedBoundarySlotSourceCandidate(
ElkRoutedEdge[] edges,
ElkRoutedEdge[] processedEdges,
int edgeIndex,
ElkRoutedEdge edge,
IReadOnlyList<ElkPoint> currentPath,
IReadOnlyCollection<List<ElkPoint>> candidates,
IReadOnlyCollection<ElkPositionedNode> nodes,
out List<ElkPoint> selectedPath)
{
selectedPath = [];
if (candidates.Count == 0)
{
return false;
}
var baselineLayout = BuildBoundarySlotEvaluationLayout(
edges,
processedEdges,
edgeIndex,
BuildSingleSectionEdge(edge, currentPath));
var baselineScore = ElkEdgeRoutingScoring.ComputeScore(baselineLayout, nodes);
var bestScore = baselineScore;
List<ElkPoint>? bestPath = null;
var seenSignatures = new HashSet<string>(StringComparer.Ordinal);
foreach (var candidate in candidates)
{
var normalizedCandidate = NormalizePathPoints(candidate);
if (!PathChanged(currentPath, normalizedCandidate)
|| !seenSignatures.Add(CreatePathSignature(normalizedCandidate)))
{
continue;
}
var candidateLayout = (ElkRoutedEdge[])baselineLayout.Clone();
candidateLayout[edgeIndex] = BuildSingleSectionEdge(edge, normalizedCandidate);
var candidateScore = ElkEdgeRoutingScoring.ComputeScore(candidateLayout, nodes);
if (!IsBetterBoundarySlotSourceCandidate(baselineScore, bestScore, candidateScore))
{
continue;
}
bestScore = candidateScore;
bestPath = normalizedCandidate;
}
if (bestPath is null)
{
return false;
}
selectedPath = bestPath;
return true;
}
private static ElkRoutedEdge[] BuildBoundarySlotEvaluationLayout(
ElkRoutedEdge[] edges,
ElkRoutedEdge[] processedEdges,
int edgeIndex,
ElkRoutedEdge currentEdge)
{
var layout = new ElkRoutedEdge[edges.Length];
for (var i = 0; i < edges.Length; i++)
{
layout[i] = i < edgeIndex ? processedEdges[i] : edges[i];
}
layout[edgeIndex] = currentEdge;
return layout;
}
private static bool IsBetterBoundarySlotSourceCandidate(
EdgeRoutingScore baselineScore,
EdgeRoutingScore currentBestScore,
EdgeRoutingScore candidateScore)
{
if (candidateScore.BoundarySlotViolations >= baselineScore.BoundarySlotViolations
|| HasBlockingBoundarySlotSourceCandidateRegression(
baselineScore,
candidateScore,
allowTemporarySoftTrade: candidateScore.BoundarySlotViolations < baselineScore.BoundarySlotViolations))
{
return false;
}
if (currentBestScore.BoundarySlotViolations >= baselineScore.BoundarySlotViolations)
{
return true;
}
if (candidateScore.BoundarySlotViolations != currentBestScore.BoundarySlotViolations)
{
return candidateScore.BoundarySlotViolations < currentBestScore.BoundarySlotViolations;
}
if (candidateScore.Value > currentBestScore.Value + 0.5d)
{
return true;
}
if (candidateScore.Value + 0.5d < currentBestScore.Value)
{
return false;
}
return candidateScore.TotalPathLength < currentBestScore.TotalPathLength - 0.5d;
}
private static bool HasBlockingBoundarySlotSourceCandidateRegression(
EdgeRoutingScore baselineScore,
EdgeRoutingScore candidateScore,
bool allowTemporarySoftTrade)
{
return candidateScore.NodeCrossings > baselineScore.NodeCrossings
|| candidateScore.BelowGraphViolations > baselineScore.BelowGraphViolations
|| candidateScore.UnderNodeViolations > baselineScore.UnderNodeViolations
|| candidateScore.LongDiagonalViolations > baselineScore.LongDiagonalViolations
|| candidateScore.EntryAngleViolations > baselineScore.EntryAngleViolations
|| candidateScore.GatewaySourceExitViolations > baselineScore.GatewaySourceExitViolations
|| candidateScore.RepeatCollectorCorridorViolations > baselineScore.RepeatCollectorCorridorViolations
|| candidateScore.RepeatCollectorNodeClearanceViolations > baselineScore.RepeatCollectorNodeClearanceViolations
|| (!allowTemporarySoftTrade
&& candidateScore.TargetApproachJoinViolations > baselineScore.TargetApproachJoinViolations)
|| candidateScore.TargetApproachBacktrackingViolations > baselineScore.TargetApproachBacktrackingViolations
|| (!allowTemporarySoftTrade
&& candidateScore.SharedLaneViolations > baselineScore.SharedLaneViolations);
}
}