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

312 lines
12 KiB
C#

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