Files
git.stella-ops.org/src/__Libraries/StellaOps.ElkSharp/ElkEdgePostProcessor.GatewayBoundary.TargetBacktracking.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
9.4 KiB
C#

namespace StellaOps.ElkSharp;
internal static partial class ElkEdgePostProcessor
{
private static List<ElkPoint> TrimTargetApproachBacktracking(
IReadOnlyList<ElkPoint> sourcePath,
ElkPositionedNode targetNode,
string side,
ElkPoint explicitEndpoint)
{
if (sourcePath.Count < 4)
{
return sourcePath
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
.ToList();
}
const double tolerance = 0.5d;
var startIndex = Math.Max(0, sourcePath.Count - 5);
var firstOffendingIndex = -1;
for (var i = startIndex; i < sourcePath.Count - 1; i++)
{
if (IsOnWrongSideOfTarget(sourcePath[i], targetNode, side, tolerance))
{
firstOffendingIndex = i;
break;
}
}
if (firstOffendingIndex < 0)
{
return sourcePath
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
.ToList();
}
var trimmed = sourcePath
.Take(Math.Max(1, firstOffendingIndex))
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
.ToList();
if (trimmed.Count == 0 || !ElkEdgeRoutingGeometry.PointsEqual(trimmed[^1], explicitEndpoint))
{
trimmed.Add(explicitEndpoint);
}
return NormalizeEntryPath(trimmed, targetNode, side, explicitEndpoint);
}
private static bool TryNormalizeNonGatewayBacktrackingEntry(
IReadOnlyList<ElkPoint> sourcePath,
ElkPositionedNode targetNode,
out List<ElkPoint> repairedPath)
{
repairedPath = sourcePath
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
.ToList();
if (sourcePath.Count < 2)
{
return false;
}
if (!TryResolveNonGatewayBacktrackingEndpoint(sourcePath, targetNode, out var side, out var endpoint))
{
return false;
}
var candidate = NormalizeEntryPath(sourcePath, targetNode, side, endpoint);
if (HasTargetApproachBacktracking(candidate, targetNode))
{
return false;
}
repairedPath = candidate;
return true;
}
private static bool TryResolveNonGatewayBacktrackingEndpoint(
IReadOnlyList<ElkPoint> sourcePath,
ElkPositionedNode targetNode,
out string side,
out ElkPoint endpoint)
{
side = string.Empty;
endpoint = default!;
if (sourcePath.Count < 2)
{
return false;
}
var anchor = sourcePath[^2];
var centerX = targetNode.X + (targetNode.Width / 2d);
var centerY = targetNode.Y + (targetNode.Height / 2d);
var deltaX = anchor.X - centerX;
var deltaY = anchor.Y - centerY;
var dominantHorizontal = Math.Abs(deltaX) >= Math.Abs(deltaY) * 1.15d;
side = dominantHorizontal
? (deltaX <= 0d ? "left" : "right")
: (deltaY <= 0d ? "top" : "bottom");
if (side is "left" or "right")
{
endpoint = new ElkPoint
{
X = side == "left" ? targetNode.X : targetNode.X + targetNode.Width,
Y = Math.Clamp(anchor.Y, targetNode.Y + 4d, targetNode.Y + targetNode.Height - 4d),
};
}
else
{
endpoint = new ElkPoint
{
X = Math.Clamp(anchor.X, targetNode.X + 4d, targetNode.X + targetNode.Width - 4d),
Y = side == "top" ? targetNode.Y : targetNode.Y + targetNode.Height,
};
}
return true;
}
private static bool HasTargetApproachBacktracking(
IReadOnlyList<ElkPoint> path,
ElkPositionedNode targetNode)
{
if (path.Count < 3 || ElkShapeBoundaries.IsGatewayShape(targetNode))
{
return false;
}
var side = ElkEdgeRoutingGeometry.ResolveBoundarySide(path[^1], targetNode);
if (side is not "left" and not "right" and not "top" and not "bottom")
{
return false;
}
const double tolerance = 0.5d;
if (HasShortOrthogonalTargetHook(path, targetNode, side, tolerance))
{
return true;
}
var startIndex = Math.Max(0, path.Count - (side is "left" or "right" ? 4 : 3));
var axisValues = new List<double>(path.Count - startIndex);
for (var i = startIndex; i < path.Count; i++)
{
var value = side is "left" or "right"
? path[i].X
: path[i].Y;
if (axisValues.Count == 0 || Math.Abs(axisValues[^1] - value) > tolerance)
{
axisValues.Add(value);
}
}
if (axisValues.Count < 3)
{
return false;
}
var targetAxis = side switch
{
"left" => targetNode.X,
"right" => targetNode.X + targetNode.Width,
"top" => targetNode.Y,
"bottom" => targetNode.Y + targetNode.Height,
_ => double.NaN,
};
var overshootsTargetSide = side switch
{
"left" or "top" => axisValues.Any(value => value > targetAxis + tolerance),
"right" or "bottom" => axisValues.Any(value => value < targetAxis - tolerance),
_ => false,
};
if (overshootsTargetSide)
{
return true;
}
var expectsIncreasing = side is "left" or "top";
var sawProgress = false;
for (var i = 1; i < axisValues.Count; i++)
{
var delta = axisValues[i] - axisValues[i - 1];
if (Math.Abs(delta) <= tolerance)
{
continue;
}
if (expectsIncreasing)
{
if (delta > tolerance)
{
sawProgress = true;
}
else if (sawProgress)
{
return true;
}
}
else
{
if (delta < -tolerance)
{
sawProgress = true;
}
else if (sawProgress)
{
return true;
}
}
}
return false;
}
private static bool HasShortOrthogonalTargetHook(
IReadOnlyList<ElkPoint> path,
ElkPositionedNode targetNode,
string side,
double tolerance)
{
if (path.Count < 3)
{
return false;
}
var boundaryPoint = path[^1];
var runStartIndex = path.Count - 2;
if (side is "left" or "right")
{
while (runStartIndex > 0 && Math.Abs(path[runStartIndex - 1].Y - boundaryPoint.Y) <= tolerance)
{
runStartIndex--;
}
}
else
{
while (runStartIndex > 0 && Math.Abs(path[runStartIndex - 1].X - boundaryPoint.X) <= tolerance)
{
runStartIndex--;
}
}
if (runStartIndex == 0)
{
return false;
}
var overallDeltaX = path[^1].X - path[0].X;
var overallDeltaY = path[^1].Y - path[0].Y;
var overallAbsDx = Math.Abs(overallDeltaX);
var overallAbsDy = Math.Abs(overallDeltaY);
var sameRowThreshold = Math.Max(24d, targetNode.Height / 3d);
var sameColumnThreshold = Math.Max(24d, targetNode.Width / 3d);
var looksHorizontal = overallAbsDx >= overallAbsDy * 1.15d
&& overallAbsDy <= sameRowThreshold
&& Math.Sign(overallDeltaX) != 0;
var looksVertical = overallAbsDy >= overallAbsDx * 1.15d
&& overallAbsDx <= sameColumnThreshold
&& Math.Sign(overallDeltaY) != 0;
var contradictsDominantApproach = side switch
{
"left" or "right" => looksVertical,
"top" or "bottom" => looksHorizontal,
_ => false,
};
if (!contradictsDominantApproach)
{
return false;
}
var runStart = path[runStartIndex];
var boundaryDepth = side is "left" or "right"
? Math.Abs(boundaryPoint.X - runStart.X)
: Math.Abs(boundaryPoint.Y - runStart.Y);
var requiredDepth = side is "left" or "right"
? targetNode.Width
: targetNode.Height;
if (boundaryDepth + tolerance >= requiredDepth)
{
return false;
}
var predecessor = path[runStartIndex - 1];
var predecessorDx = Math.Abs(runStart.X - predecessor.X);
var predecessorDy = Math.Abs(runStart.Y - predecessor.Y);
return side switch
{
"left" or "right" => predecessorDy > predecessorDx * 3d,
"top" or "bottom" => predecessorDx > predecessorDy * 3d,
_ => false,
};
}
private static bool IsOnWrongSideOfTarget(
ElkPoint point,
ElkPositionedNode targetNode,
string side,
double tolerance)
{
return side switch
{
"left" => point.X > targetNode.X + tolerance,
"right" => point.X < (targetNode.X + targetNode.Width) - tolerance,
"top" => point.Y > targetNode.Y + tolerance,
"bottom" => point.Y < (targetNode.Y + targetNode.Height) - tolerance,
_ => false,
};
}
}