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,298 @@
|
||||
namespace StellaOps.ElkSharp;
|
||||
|
||||
internal static partial class ElkEdgePostProcessor
|
||||
{
|
||||
private static List<ElkPoint> TryBuildGatewaySourceDominantBlockerEscapePath(
|
||||
IReadOnlyList<ElkPoint> sourcePath,
|
||||
ElkPositionedNode sourceNode,
|
||||
IReadOnlyCollection<ElkPositionedNode> nodes,
|
||||
string? sourceNodeId,
|
||||
string? targetNodeId)
|
||||
{
|
||||
var path = sourcePath
|
||||
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
||||
.ToList();
|
||||
if (sourceNode.Kind != "Decision"
|
||||
|| path.Count < 3
|
||||
|| !HasGatewaySourceDominantAxisDetour(path, sourceNode))
|
||||
{
|
||||
return path;
|
||||
}
|
||||
|
||||
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 dominantVertical = Math.Abs(desiredDy) >= Math.Abs(desiredDx) * 1.25d && Math.Sign(desiredDy) != 0;
|
||||
if (!dominantVertical)
|
||||
{
|
||||
return path;
|
||||
}
|
||||
|
||||
var clearance = 24d;
|
||||
var direction = Math.Sign(desiredDy);
|
||||
var targetNode = string.IsNullOrWhiteSpace(targetNodeId)
|
||||
? null
|
||||
: nodes.FirstOrDefault(node => string.Equals(node.Id, targetNodeId, StringComparison.Ordinal));
|
||||
var firstExteriorIndex = FindFirstGatewayExteriorPointIndex(path, sourceNode);
|
||||
List<ElkPoint>? bestCandidate = null;
|
||||
var bestScore = double.PositiveInfinity;
|
||||
|
||||
foreach (var continuationIndex in EnumerateGatewayDirectRepairContinuationIndices(path, sourceNode, firstExteriorIndex))
|
||||
{
|
||||
var anchorX = path[continuationIndex].X;
|
||||
if (Math.Abs(anchorX - path[0].X) <= 3d)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var dominantReference = new ElkPoint { X = anchorX, Y = path[^1].Y };
|
||||
if (!TryResolvePreferredGatewaySourceBoundary(sourceNode, dominantReference, path[^1], out var provisionalBoundary))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var stemBlockers = nodes
|
||||
.Where(node =>
|
||||
!string.Equals(node.Id, sourceNodeId, StringComparison.Ordinal)
|
||||
&& !string.Equals(node.Id, targetNodeId, StringComparison.Ordinal)
|
||||
&& provisionalBoundary.X > node.X + 0.5d
|
||||
&& provisionalBoundary.X < node.X + node.Width - 0.5d
|
||||
&& (direction > 0d
|
||||
? node.Y > provisionalBoundary.Y + 0.5d && node.Y < path[^1].Y - 0.5d
|
||||
: node.Y + node.Height < provisionalBoundary.Y - 0.5d && node.Y + node.Height > path[^1].Y + 0.5d))
|
||||
.OrderBy(node => direction > 0d ? node.Y : -(node.Y + node.Height))
|
||||
.ToArray();
|
||||
if (stemBlockers.Length == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var blocker in stemBlockers)
|
||||
{
|
||||
var escapeY = direction > 0d
|
||||
? blocker.Y - clearance
|
||||
: blocker.Y + blocker.Height + clearance;
|
||||
if (direction > 0d)
|
||||
{
|
||||
if (escapeY <= provisionalBoundary.Y + 8d || escapeY >= blocker.Y - 0.5d)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else if (escapeY >= provisionalBoundary.Y - 8d || escapeY <= blocker.Y + blocker.Height + 0.5d)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var continuationPoint = new ElkPoint { X = anchorX, Y = escapeY };
|
||||
var boundary = provisionalBoundary;
|
||||
|
||||
var candidate = BuildGatewaySourceRepairPath(
|
||||
path,
|
||||
sourceNode,
|
||||
boundary,
|
||||
continuationPoint,
|
||||
continuationIndex,
|
||||
continuationPoint);
|
||||
if (!PathChanged(path, candidate)
|
||||
|| !HasAcceptableGatewayBoundaryPath(candidate, nodes, sourceNodeId, targetNodeId, sourceNode, fromStart: true)
|
||||
|| !HasClearBoundarySegments(candidate, nodes, sourceNodeId, targetNodeId, true, Math.Min(4, candidate.Count - 1))
|
||||
|| (targetNode is not null
|
||||
&& (ElkShapeBoundaries.IsGatewayShape(targetNode)
|
||||
? !HasAcceptableGatewayBoundaryPath(candidate, nodes, sourceNodeId, targetNodeId, targetNode, fromStart: false)
|
||||
: candidate.Count < 2 || !HasValidBoundaryAngle(candidate[^1], candidate[^2], targetNode)))
|
||||
|| HasGatewaySourceExitBacktracking(candidate)
|
||||
|| HasGatewaySourceExitCurl(candidate)
|
||||
|| HasGatewaySourceDominantAxisDetour(candidate, sourceNode)
|
||||
|| HasGatewaySourcePreferredFaceMismatch(candidate, sourceNode))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var score = ScoreGatewayDirectRepairCandidate(path, candidate, sourceNode, continuationIndex);
|
||||
if (score >= bestScore)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
bestScore = score;
|
||||
bestCandidate = candidate;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestCandidate is null)
|
||||
{
|
||||
return path;
|
||||
}
|
||||
|
||||
return IsMaterialGatewaySourceRepairImprovement(path, bestCandidate)
|
||||
|| IsGatewaySourceGeometryRepairImprovement(path, bestCandidate, sourceNode)
|
||||
? bestCandidate
|
||||
: path;
|
||||
}
|
||||
|
||||
private static List<ElkPoint>? TryBuildDominantAxisGatewaySourceBypassPath(
|
||||
ElkPositionedNode sourceNode,
|
||||
ElkPositionedNode targetNode,
|
||||
ElkPoint boundary,
|
||||
ElkPoint targetEndpoint,
|
||||
(double Left, double Top, double Right, double Bottom, string Id)[] obstacles,
|
||||
string? sourceNodeId,
|
||||
string? targetNodeId,
|
||||
bool dominantHorizontal,
|
||||
double desiredDx,
|
||||
double desiredDy)
|
||||
{
|
||||
const double padding = 8d;
|
||||
const double coordinateTolerance = 0.5d;
|
||||
var sourceId = sourceNodeId ?? string.Empty;
|
||||
var targetId = targetNodeId ?? string.Empty;
|
||||
List<ElkPoint>? bestCandidate = null;
|
||||
var bestScore = double.PositiveInfinity;
|
||||
|
||||
void ConsiderCandidate(List<ElkPoint> rawCandidate)
|
||||
{
|
||||
var candidate = NormalizePathPoints(rawCandidate);
|
||||
if (candidate.Count < 2
|
||||
|| !IsPathClearOfObstacles(candidate, obstacles, sourceId, targetId)
|
||||
|| !HasValidBoundaryAngle(candidate[^1], candidate[^2], targetNode)
|
||||
|| HasGatewaySourceExitBacktracking(candidate)
|
||||
|| HasGatewaySourceExitCurl(candidate)
|
||||
|| HasGatewaySourceDominantAxisDetour(candidate, sourceNode)
|
||||
|| HasGatewaySourcePreferredFaceMismatch(candidate, sourceNode))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var score = ComputePathLength(candidate) + (Math.Max(0, candidate.Count - 2) * 6d);
|
||||
if (score >= bestScore)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bestScore = score;
|
||||
bestCandidate = candidate;
|
||||
}
|
||||
|
||||
if (dominantHorizontal)
|
||||
{
|
||||
var movingRight = desiredDx >= 0d;
|
||||
var firstBlocker = obstacles
|
||||
.Where(ob => !string.Equals(ob.Id, sourceId, StringComparison.Ordinal)
|
||||
&& !string.Equals(ob.Id, targetId, StringComparison.Ordinal)
|
||||
&& boundary.Y > ob.Top + coordinateTolerance
|
||||
&& boundary.Y < ob.Bottom - coordinateTolerance
|
||||
&& (movingRight
|
||||
? ob.Left > boundary.X + coordinateTolerance && ob.Left < targetEndpoint.X - coordinateTolerance
|
||||
: ob.Right < boundary.X - coordinateTolerance && ob.Right > targetEndpoint.X + coordinateTolerance))
|
||||
.OrderBy(ob => movingRight ? ob.Left : -ob.Right)
|
||||
.FirstOrDefault();
|
||||
if (string.IsNullOrWhiteSpace(firstBlocker.Id))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var axisX = movingRight
|
||||
? firstBlocker.Left - padding
|
||||
: firstBlocker.Right + padding;
|
||||
if (movingRight ? axisX <= boundary.X + 2d : axisX >= boundary.X - 2d)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var bypassYCandidates = new List<double>();
|
||||
AddUniqueCoordinate(bypassYCandidates, targetEndpoint.Y);
|
||||
AddUniqueCoordinate(bypassYCandidates, firstBlocker.Top - padding);
|
||||
AddUniqueCoordinate(bypassYCandidates, firstBlocker.Bottom + padding);
|
||||
foreach (var bypassY in bypassYCandidates)
|
||||
{
|
||||
var diagonalLead = new List<ElkPoint> { boundary };
|
||||
var diagonalLeadPoint = new ElkPoint { X = axisX, Y = bypassY };
|
||||
if (!ElkEdgeRoutingGeometry.PointsEqual(diagonalLead[^1], diagonalLeadPoint))
|
||||
{
|
||||
diagonalLead.Add(diagonalLeadPoint);
|
||||
}
|
||||
|
||||
AppendNonGatewayTargetBoundaryApproach(diagonalLead, targetNode, targetEndpoint);
|
||||
ConsiderCandidate(diagonalLead);
|
||||
|
||||
var rebuilt = new List<ElkPoint>
|
||||
{
|
||||
boundary,
|
||||
new() { X = axisX, Y = boundary.Y },
|
||||
};
|
||||
|
||||
if (Math.Abs(rebuilt[^1].Y - bypassY) > coordinateTolerance)
|
||||
{
|
||||
rebuilt.Add(new ElkPoint { X = axisX, Y = bypassY });
|
||||
}
|
||||
|
||||
AppendNonGatewayTargetBoundaryApproach(rebuilt, targetNode, targetEndpoint);
|
||||
|
||||
ConsiderCandidate(rebuilt);
|
||||
}
|
||||
|
||||
return bestCandidate;
|
||||
}
|
||||
|
||||
var movingDown = desiredDy >= 0d;
|
||||
var verticalBlocker = obstacles
|
||||
.Where(ob => !string.Equals(ob.Id, sourceId, StringComparison.Ordinal)
|
||||
&& !string.Equals(ob.Id, targetId, StringComparison.Ordinal)
|
||||
&& boundary.X > ob.Left + coordinateTolerance
|
||||
&& boundary.X < ob.Right - coordinateTolerance
|
||||
&& (movingDown
|
||||
? ob.Top > boundary.Y + coordinateTolerance && ob.Top < targetEndpoint.Y - coordinateTolerance
|
||||
: ob.Bottom < boundary.Y - coordinateTolerance && ob.Bottom > targetEndpoint.Y + coordinateTolerance))
|
||||
.OrderBy(ob => movingDown ? ob.Top : -ob.Bottom)
|
||||
.FirstOrDefault();
|
||||
if (string.IsNullOrWhiteSpace(verticalBlocker.Id))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var axisY = movingDown
|
||||
? verticalBlocker.Top - padding
|
||||
: verticalBlocker.Bottom + padding;
|
||||
if (movingDown ? axisY <= boundary.Y + 2d : axisY >= boundary.Y - 2d)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var bypassXCandidates = new List<double>();
|
||||
AddUniqueCoordinate(bypassXCandidates, targetEndpoint.X);
|
||||
AddUniqueCoordinate(bypassXCandidates, verticalBlocker.Left - padding);
|
||||
AddUniqueCoordinate(bypassXCandidates, verticalBlocker.Right + padding);
|
||||
foreach (var bypassX in bypassXCandidates)
|
||||
{
|
||||
var diagonalLead = new List<ElkPoint> { boundary };
|
||||
var diagonalLeadPoint = new ElkPoint { X = bypassX, Y = axisY };
|
||||
if (!ElkEdgeRoutingGeometry.PointsEqual(diagonalLead[^1], diagonalLeadPoint))
|
||||
{
|
||||
diagonalLead.Add(diagonalLeadPoint);
|
||||
}
|
||||
|
||||
AppendNonGatewayTargetBoundaryApproach(diagonalLead, targetNode, targetEndpoint);
|
||||
ConsiderCandidate(diagonalLead);
|
||||
|
||||
var rebuilt = new List<ElkPoint>
|
||||
{
|
||||
boundary,
|
||||
new() { X = boundary.X, Y = axisY },
|
||||
};
|
||||
|
||||
if (Math.Abs(rebuilt[^1].X - bypassX) > coordinateTolerance)
|
||||
{
|
||||
rebuilt.Add(new ElkPoint { X = bypassX, Y = axisY });
|
||||
}
|
||||
|
||||
AppendNonGatewayTargetBoundaryApproach(rebuilt, targetNode, targetEndpoint);
|
||||
|
||||
ConsiderCandidate(rebuilt);
|
||||
}
|
||||
|
||||
return bestCandidate;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user