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

299 lines
12 KiB
C#

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