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,311 @@
|
||||
namespace StellaOps.ElkSharp;
|
||||
|
||||
internal static partial class ElkEdgePostProcessor
|
||||
{
|
||||
private static List<ElkPoint> FixGatewaySourcePreferredFace(
|
||||
IReadOnlyList<ElkPoint> sourcePath,
|
||||
ElkPositionedNode sourceNode)
|
||||
{
|
||||
if (!HasGatewaySourcePreferredFaceMismatch(sourcePath, sourceNode))
|
||||
{
|
||||
return sourcePath.Select(point => new ElkPoint { X = point.X, Y = point.Y }).ToList();
|
||||
}
|
||||
|
||||
var path = sourcePath
|
||||
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
||||
.ToList();
|
||||
var firstExteriorIndex = FindFirstGatewayExteriorPointIndex(path, sourceNode);
|
||||
var continuationIndex = FindGatewaySourceCurlRecoveryIndex(path, firstExteriorIndex)
|
||||
?? FindPreferredGatewayExitContinuationIndex(path, sourceNode, firstExteriorIndex);
|
||||
var continuationPoint = path[continuationIndex];
|
||||
if (!TryResolvePreferredGatewaySourceBoundary(sourceNode, continuationPoint, path[^1], out var preferredBoundary))
|
||||
{
|
||||
return path;
|
||||
}
|
||||
|
||||
return BuildGatewaySourceRepairPath(
|
||||
path,
|
||||
sourceNode,
|
||||
preferredBoundary,
|
||||
continuationPoint,
|
||||
continuationIndex,
|
||||
path[^1]);
|
||||
}
|
||||
|
||||
private static bool HasGatewaySourcePreferredFaceMismatch(
|
||||
IReadOnlyList<ElkPoint> path,
|
||||
ElkPositionedNode sourceNode)
|
||||
{
|
||||
if (!ElkShapeBoundaries.IsGatewayShape(sourceNode) || path.Count < 2)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var centerX = sourceNode.X + (sourceNode.Width / 2d);
|
||||
var centerY = sourceNode.Y + (sourceNode.Height / 2d);
|
||||
var desiredDx = path[^1].X - centerX;
|
||||
var desiredDy = path[^1].Y - centerY;
|
||||
var boundaryDx = path[0].X - centerX;
|
||||
var boundaryDy = path[0].Y - centerY;
|
||||
|
||||
if (Math.Abs(desiredDx) >= Math.Abs(desiredDy) * 1.25d)
|
||||
{
|
||||
return Math.Sign(boundaryDx) != Math.Sign(desiredDx)
|
||||
|| Math.Abs(boundaryDy) > sourceNode.Height * 0.28d;
|
||||
}
|
||||
|
||||
if (Math.Abs(desiredDy) >= Math.Abs(desiredDx) * 1.25d)
|
||||
{
|
||||
return Math.Sign(boundaryDy) != Math.Sign(desiredDy)
|
||||
|| Math.Abs(boundaryDx) > sourceNode.Width * 0.28d;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static List<ElkPoint> FixGatewaySourceExitCurl(
|
||||
IReadOnlyList<ElkPoint> sourcePath,
|
||||
ElkPositionedNode sourceNode)
|
||||
{
|
||||
if (!ElkShapeBoundaries.IsGatewayShape(sourceNode) || sourcePath.Count < 3)
|
||||
{
|
||||
return sourcePath.Select(point => new ElkPoint { X = point.X, Y = point.Y }).ToList();
|
||||
}
|
||||
|
||||
var sample = sourcePath
|
||||
.Take(Math.Min(sourcePath.Count, 6))
|
||||
.ToArray();
|
||||
var desiredDx = sourcePath[^1].X - sourcePath[0].X;
|
||||
var desiredDy = sourcePath[^1].Y - sourcePath[0].Y;
|
||||
if (!HasGatewaySourceExitCurl(sample))
|
||||
{
|
||||
return sourcePath.Select(point => new ElkPoint { X = point.X, Y = point.Y }).ToList();
|
||||
}
|
||||
|
||||
var path = sourcePath
|
||||
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
||||
.ToList();
|
||||
var firstExteriorIndex = FindFirstGatewayExteriorPointIndex(path, sourceNode);
|
||||
var continuationIndex = FindGatewaySourceCurlRecoveryIndex(path, firstExteriorIndex)
|
||||
?? FindPreferredGatewayExitContinuationIndex(path, sourceNode, firstExteriorIndex);
|
||||
var continuationPoint = path[continuationIndex];
|
||||
var boundary = sourceNode.Kind == "Decision"
|
||||
? ResolveDecisionSourceExitBoundary(sourceNode, continuationPoint, continuationPoint)
|
||||
: PreferGatewaySourceExitBoundary(
|
||||
sourceNode,
|
||||
ElkShapeBoundaries.ProjectOntoShapeBoundary(sourceNode, continuationPoint),
|
||||
continuationPoint);
|
||||
var continuationAligned = BuildGatewaySourceRepairPath(
|
||||
path,
|
||||
sourceNode,
|
||||
boundary,
|
||||
continuationPoint,
|
||||
continuationIndex,
|
||||
continuationPoint);
|
||||
if (PathChanged(path, continuationAligned)
|
||||
&& !HasGatewaySourceExitBacktracking(continuationAligned)
|
||||
&& !HasGatewaySourceExitCurl(continuationAligned))
|
||||
{
|
||||
return continuationAligned;
|
||||
}
|
||||
|
||||
var collapsedCurl = TryBuildGatewaySourceDominantAxisShortcut(path, sourceNode, path[0]);
|
||||
if (collapsedCurl is not null
|
||||
&& PathChanged(path, collapsedCurl)
|
||||
&& !HasGatewaySourceExitBacktracking(collapsedCurl)
|
||||
&& !HasGatewaySourceExitCurl(collapsedCurl))
|
||||
{
|
||||
return collapsedCurl;
|
||||
}
|
||||
|
||||
const double axisTolerance = 4d;
|
||||
var rebuilt = path;
|
||||
var dominantHorizontal = Math.Abs(desiredDx) >= Math.Abs(desiredDy) * 1.15d && Math.Sign(desiredDx) != 0;
|
||||
var dominantVertical = Math.Abs(desiredDy) >= Math.Abs(desiredDx) * 1.15d && Math.Sign(desiredDy) != 0;
|
||||
|
||||
if (dominantHorizontal && Math.Abs(rebuilt[1].Y - rebuilt[0].Y) <= axisTolerance)
|
||||
{
|
||||
rebuilt[1] = new ElkPoint
|
||||
{
|
||||
X = rebuilt[1].X,
|
||||
Y = rebuilt[0].Y,
|
||||
};
|
||||
}
|
||||
else if (dominantVertical && Math.Abs(rebuilt[1].X - rebuilt[0].X) <= axisTolerance)
|
||||
{
|
||||
rebuilt[1] = new ElkPoint
|
||||
{
|
||||
X = rebuilt[0].X,
|
||||
Y = rebuilt[1].Y,
|
||||
};
|
||||
}
|
||||
|
||||
return NormalizePathPoints(rebuilt);
|
||||
}
|
||||
|
||||
private static List<ElkPoint> FixGatewaySourceDominantAxisDetour(
|
||||
IReadOnlyList<ElkPoint> sourcePath,
|
||||
ElkPositionedNode sourceNode)
|
||||
{
|
||||
var path = sourcePath
|
||||
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
||||
.ToList();
|
||||
if (!HasGatewaySourceDominantAxisDetour(path, sourceNode))
|
||||
{
|
||||
return path;
|
||||
}
|
||||
|
||||
var boundary = path[0];
|
||||
var desiredDx = path[^1].X - boundary.X;
|
||||
var desiredDy = path[^1].Y - boundary.Y;
|
||||
var dominantHorizontal = Math.Abs(desiredDx) >= Math.Abs(desiredDy) * 1.15d && Math.Sign(desiredDx) != 0;
|
||||
var dominantVertical = Math.Abs(desiredDy) >= Math.Abs(desiredDx) * 1.15d && Math.Sign(desiredDy) != 0;
|
||||
var firstExteriorIndex = FindFirstGatewayExteriorPointIndex(path, sourceNode);
|
||||
var boundaryReferencePoint = path[firstExteriorIndex];
|
||||
if (!TryResolvePreferredGatewaySourceBoundary(sourceNode, boundaryReferencePoint, path[^1], out var preferredBoundary))
|
||||
{
|
||||
return path;
|
||||
}
|
||||
|
||||
var localContinuationPoint = path[firstExteriorIndex];
|
||||
var localRepair = new List<ElkPoint> { preferredBoundary };
|
||||
if (!ElkEdgeRoutingGeometry.PointsEqual(localRepair[^1], localContinuationPoint))
|
||||
{
|
||||
AppendGatewayOrthogonalCorner(
|
||||
localRepair,
|
||||
localRepair[^1],
|
||||
localContinuationPoint,
|
||||
firstExteriorIndex + 1 < path.Count ? path[firstExteriorIndex + 1] : null,
|
||||
preferHorizontalFromReference: ShouldPreferHorizontalGatewayExit(localRepair[^1], localContinuationPoint));
|
||||
if (!ElkEdgeRoutingGeometry.PointsEqual(localRepair[^1], localContinuationPoint))
|
||||
{
|
||||
localRepair.Add(localContinuationPoint);
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = firstExteriorIndex + 1; i < path.Count; i++)
|
||||
{
|
||||
localRepair.Add(path[i]);
|
||||
}
|
||||
|
||||
localRepair = NormalizePathPoints(localRepair);
|
||||
if (PathChanged(path, localRepair)
|
||||
&& !HasGatewaySourceExitBacktracking(localRepair)
|
||||
&& !HasGatewaySourceExitCurl(localRepair)
|
||||
&& !HasGatewaySourceDominantAxisDetour(localRepair, sourceNode)
|
||||
&& !HasGatewaySourcePreferredFaceMismatch(localRepair, sourceNode))
|
||||
{
|
||||
return localRepair;
|
||||
}
|
||||
|
||||
var dominantAxisShortcut = TryBuildGatewaySourceDominantAxisShortcut(path, sourceNode, preferredBoundary);
|
||||
if (dominantAxisShortcut is not null
|
||||
&& PathChanged(path, dominantAxisShortcut)
|
||||
&& !HasGatewaySourceExitBacktracking(dominantAxisShortcut)
|
||||
&& !HasGatewaySourceExitCurl(dominantAxisShortcut)
|
||||
&& !HasGatewaySourceDominantAxisDetour(dominantAxisShortcut, sourceNode)
|
||||
&& !HasGatewaySourcePreferredFaceMismatch(dominantAxisShortcut, sourceNode))
|
||||
{
|
||||
return dominantAxisShortcut;
|
||||
}
|
||||
|
||||
var preferredContinuationIndex = FindPreferredGatewayExitContinuationIndex(path, sourceNode, firstExteriorIndex);
|
||||
var candidateContinuationIndices = new[]
|
||||
{
|
||||
firstExteriorIndex,
|
||||
Math.Min(path.Count - 1, firstExteriorIndex + 1),
|
||||
Math.Min(path.Count - 1, firstExteriorIndex + 2),
|
||||
preferredContinuationIndex,
|
||||
}
|
||||
.Distinct()
|
||||
.Where(index => index >= firstExteriorIndex && index < path.Count)
|
||||
.ToArray();
|
||||
|
||||
List<ElkPoint>? bestCandidate = null;
|
||||
var bestScore = double.PositiveInfinity;
|
||||
foreach (var continuationIndex in candidateContinuationIndices)
|
||||
{
|
||||
var continuationCandidates = new List<ElkPoint>
|
||||
{
|
||||
path[continuationIndex],
|
||||
};
|
||||
if (dominantHorizontal)
|
||||
{
|
||||
AddUniquePoint(
|
||||
continuationCandidates,
|
||||
new ElkPoint
|
||||
{
|
||||
X = path[continuationIndex].X,
|
||||
Y = preferredBoundary.Y,
|
||||
});
|
||||
}
|
||||
else if (dominantVertical)
|
||||
{
|
||||
AddUniquePoint(
|
||||
continuationCandidates,
|
||||
new ElkPoint
|
||||
{
|
||||
X = preferredBoundary.X,
|
||||
Y = path[continuationIndex].Y,
|
||||
});
|
||||
}
|
||||
|
||||
foreach (var continuationPoint in continuationCandidates)
|
||||
{
|
||||
var candidate = BuildGatewaySourceRepairPath(
|
||||
path,
|
||||
sourceNode,
|
||||
preferredBoundary,
|
||||
continuationPoint,
|
||||
continuationIndex,
|
||||
continuationPoint);
|
||||
if (!PathChanged(path, candidate))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var score = ComputePathLength(candidate);
|
||||
if (!ElkEdgeRoutingGeometry.PointsEqual(continuationPoint, path[continuationIndex]))
|
||||
{
|
||||
score -= 18d;
|
||||
}
|
||||
|
||||
if (HasGatewaySourceExitBacktracking(candidate)
|
||||
|| HasGatewaySourceExitCurl(candidate))
|
||||
{
|
||||
score += 100_000d;
|
||||
}
|
||||
|
||||
if (HasGatewaySourceDominantAxisDetour(candidate, sourceNode))
|
||||
{
|
||||
score += 50_000d;
|
||||
}
|
||||
|
||||
if (HasGatewaySourcePreferredFaceMismatch(candidate, sourceNode))
|
||||
{
|
||||
score += 25_000d;
|
||||
}
|
||||
|
||||
if (score >= bestScore)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
bestScore = score;
|
||||
bestCandidate = candidate;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestCandidate is null
|
||||
|| HasGatewaySourceExitBacktracking(bestCandidate)
|
||||
|| HasGatewaySourceExitCurl(bestCandidate)
|
||||
|| HasGatewaySourceDominantAxisDetour(bestCandidate, sourceNode)
|
||||
|| HasGatewaySourcePreferredFaceMismatch(bestCandidate, sourceNode))
|
||||
{
|
||||
return path;
|
||||
}
|
||||
|
||||
return bestCandidate;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user