- 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>
156 lines
5.1 KiB
C#
156 lines
5.1 KiB
C#
namespace StellaOps.ElkSharp;
|
|
|
|
internal static partial class ElkEdgePostProcessor
|
|
{
|
|
private static List<ElkPoint> ShiftSingleOrthogonalRun(
|
|
IReadOnlyList<ElkPoint> path,
|
|
int segmentIndex,
|
|
double desiredCoordinate)
|
|
{
|
|
var candidate = path
|
|
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
|
.ToList();
|
|
if (segmentIndex < 0 || segmentIndex >= candidate.Count - 1)
|
|
{
|
|
return candidate;
|
|
}
|
|
|
|
var start = candidate[segmentIndex];
|
|
var end = candidate[segmentIndex + 1];
|
|
if (Math.Abs(start.Y - end.Y) <= 0.5d)
|
|
{
|
|
var original = start.Y;
|
|
for (var i = 0; i < candidate.Count; i++)
|
|
{
|
|
if (Math.Abs(candidate[i].Y - original) <= 0.5d)
|
|
{
|
|
candidate[i] = new ElkPoint { X = candidate[i].X, Y = desiredCoordinate };
|
|
}
|
|
}
|
|
}
|
|
else if (Math.Abs(start.X - end.X) <= 0.5d)
|
|
{
|
|
var original = start.X;
|
|
for (var i = 0; i < candidate.Count; i++)
|
|
{
|
|
if (Math.Abs(candidate[i].X - original) <= 0.5d)
|
|
{
|
|
candidate[i] = new ElkPoint { X = desiredCoordinate, Y = candidate[i].Y };
|
|
}
|
|
}
|
|
}
|
|
|
|
return NormalizePathPoints(candidate);
|
|
}
|
|
|
|
private static List<ElkPoint> ShiftStraightOrthogonalPath(
|
|
IReadOnlyList<ElkPoint> path,
|
|
double desiredCoordinate)
|
|
{
|
|
var candidate = path
|
|
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
|
.ToList();
|
|
if (candidate.Count != 2)
|
|
{
|
|
return candidate;
|
|
}
|
|
|
|
var start = candidate[0];
|
|
var end = candidate[1];
|
|
if (Math.Abs(start.Y - end.Y) <= 0.5d)
|
|
{
|
|
return NormalizePathPoints(
|
|
[
|
|
new ElkPoint { X = start.X, Y = start.Y },
|
|
new ElkPoint { X = start.X, Y = desiredCoordinate },
|
|
new ElkPoint { X = end.X, Y = desiredCoordinate },
|
|
new ElkPoint { X = end.X, Y = end.Y },
|
|
]);
|
|
}
|
|
|
|
if (Math.Abs(start.X - end.X) <= 0.5d)
|
|
{
|
|
return NormalizePathPoints(
|
|
[
|
|
new ElkPoint { X = start.X, Y = start.Y },
|
|
new ElkPoint { X = desiredCoordinate, Y = start.Y },
|
|
new ElkPoint { X = desiredCoordinate, Y = end.Y },
|
|
new ElkPoint { X = end.X, Y = end.Y },
|
|
]);
|
|
}
|
|
|
|
return candidate;
|
|
}
|
|
|
|
private static double[] ResolveLaneShiftCoordinates(
|
|
ElkPoint start,
|
|
ElkPoint end,
|
|
ElkPoint otherStart,
|
|
ElkPoint otherEnd,
|
|
double minLineClearance)
|
|
{
|
|
var offset = minLineClearance + 4d;
|
|
if (Math.Abs(start.Y - end.Y) <= 0.5d && Math.Abs(otherStart.Y - otherEnd.Y) <= 0.5d)
|
|
{
|
|
var lower = otherStart.Y - offset;
|
|
var upper = otherStart.Y + offset;
|
|
return start.Y <= otherStart.Y
|
|
? [lower, upper]
|
|
: [upper, lower];
|
|
}
|
|
|
|
if (Math.Abs(start.X - end.X) <= 0.5d && Math.Abs(otherStart.X - otherEnd.X) <= 0.5d)
|
|
{
|
|
var lower = otherStart.X - offset;
|
|
var upper = otherStart.X + offset;
|
|
return start.X <= otherStart.X
|
|
? [lower, upper]
|
|
: [upper, lower];
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
private static bool SegmentLeavesGraphBand(
|
|
IReadOnlyList<ElkPoint> path,
|
|
double graphMinY,
|
|
double graphMaxY)
|
|
{
|
|
return path.Any(point => point.Y < graphMinY - 96d || point.Y > graphMaxY + 96d);
|
|
}
|
|
|
|
private static bool SegmentsShareLane(
|
|
ElkPoint leftStart,
|
|
ElkPoint leftEnd,
|
|
ElkPoint rightStart,
|
|
ElkPoint rightEnd,
|
|
double minLineClearance)
|
|
{
|
|
var laneTolerance = Math.Max(4d, Math.Min(12d, minLineClearance * 0.2d));
|
|
var minSharedLength = Math.Max(24d, minLineClearance * 0.4d);
|
|
if (Math.Abs(leftStart.Y - leftEnd.Y) <= 0.5d
|
|
&& Math.Abs(rightStart.Y - rightEnd.Y) <= 0.5d
|
|
&& Math.Abs(leftStart.Y - rightStart.Y) <= laneTolerance)
|
|
{
|
|
var leftMinX = Math.Min(leftStart.X, leftEnd.X);
|
|
var leftMaxX = Math.Max(leftStart.X, leftEnd.X);
|
|
var rightMinX = Math.Min(rightStart.X, rightEnd.X);
|
|
var rightMaxX = Math.Max(rightStart.X, rightEnd.X);
|
|
return Math.Min(leftMaxX, rightMaxX) - Math.Max(leftMinX, rightMinX) >= minSharedLength;
|
|
}
|
|
|
|
if (Math.Abs(leftStart.X - leftEnd.X) <= 0.5d
|
|
&& Math.Abs(rightStart.X - rightEnd.X) <= 0.5d
|
|
&& Math.Abs(leftStart.X - rightStart.X) <= laneTolerance)
|
|
{
|
|
var leftMinY = Math.Min(leftStart.Y, leftEnd.Y);
|
|
var leftMaxY = Math.Max(leftStart.Y, leftEnd.Y);
|
|
var rightMinY = Math.Min(rightStart.Y, rightEnd.Y);
|
|
var rightMaxY = Math.Max(rightStart.Y, rightEnd.Y);
|
|
return Math.Min(leftMaxY, rightMaxY) - Math.Max(leftMinY, rightMinY) >= minSharedLength;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|