- 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>
312 lines
12 KiB
C#
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;
|
|
}
|
|
|
|
}
|