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,318 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user