- 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
9.4 KiB
C#
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,
|
|
};
|
|
}
|
|
|
|
}
|