- 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>
249 lines
8.3 KiB
C#
249 lines
8.3 KiB
C#
namespace StellaOps.ElkSharp;
|
|
|
|
internal static partial class ElkEdgePostProcessor
|
|
{
|
|
internal static bool IsRepeatCollectorLabel(string? label)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(label))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var normalized = label.Trim().ToLowerInvariant();
|
|
return normalized.StartsWith("repeat ", StringComparison.Ordinal)
|
|
|| normalized.Equals("body", StringComparison.Ordinal);
|
|
}
|
|
|
|
private static bool ShouldPreserveSourceExitGeometry(
|
|
ElkRoutedEdge edge,
|
|
double graphMinY,
|
|
double graphMaxY)
|
|
{
|
|
if (HasProtectedUnderNodeGeometry(edge))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (!HasCorridorBendPoints(edge, graphMinY, graphMaxY))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return IsRepeatCollectorLabel(edge.Label)
|
|
|| (!string.IsNullOrWhiteSpace(edge.Kind)
|
|
&& edge.Kind.StartsWith("backward|", StringComparison.OrdinalIgnoreCase));
|
|
}
|
|
|
|
internal static bool IsCorridorSegment(ElkPoint p1, ElkPoint p2, double graphMinY, double graphMaxY)
|
|
{
|
|
return p1.Y < graphMinY - 8d || p1.Y > graphMaxY + 8d
|
|
|| p2.Y < graphMinY - 8d || p2.Y > graphMaxY + 8d;
|
|
}
|
|
|
|
internal static bool HasCorridorBendPoints(ElkRoutedEdge edge, double graphMinY, double graphMaxY)
|
|
{
|
|
foreach (var section in edge.Sections)
|
|
{
|
|
foreach (var bp in section.BendPoints)
|
|
{
|
|
if (bp.Y < graphMinY - 8d || bp.Y > graphMaxY + 8d)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
internal static bool SegmentCrossesObstacle(
|
|
ElkPoint p1, ElkPoint p2,
|
|
(double Left, double Top, double Right, double Bottom, string Id)[] obstacles,
|
|
string sourceId, string targetId)
|
|
{
|
|
var isH = Math.Abs(p1.Y - p2.Y) < 2d;
|
|
var isV = Math.Abs(p1.X - p2.X) < 2d;
|
|
|
|
foreach (var ob in obstacles)
|
|
{
|
|
if (ob.Id == sourceId || ob.Id == targetId) continue;
|
|
if (isH && p1.Y > ob.Top && p1.Y < ob.Bottom)
|
|
{
|
|
var minX = Math.Min(p1.X, p2.X);
|
|
var maxX = Math.Max(p1.X, p2.X);
|
|
if (maxX > ob.Left && minX < ob.Right) return true;
|
|
}
|
|
else if (isV && p1.X > ob.Left && p1.X < ob.Right)
|
|
{
|
|
var minY = Math.Min(p1.Y, p2.Y);
|
|
var maxY = Math.Max(p1.Y, p2.Y);
|
|
if (maxY > ob.Top && minY < ob.Bottom) return true;
|
|
}
|
|
else if (!isH && !isV)
|
|
{
|
|
// Diagonal segment: check actual intersection with obstacle rectangle
|
|
if (ElkEdgeRoutingGeometry.SegmentsIntersect(p1, p2,
|
|
new ElkPoint { X = ob.Left, Y = ob.Top }, new ElkPoint { X = ob.Right, Y = ob.Top })
|
|
|| ElkEdgeRoutingGeometry.SegmentsIntersect(p1, p2,
|
|
new ElkPoint { X = ob.Right, Y = ob.Top }, new ElkPoint { X = ob.Right, Y = ob.Bottom })
|
|
|| ElkEdgeRoutingGeometry.SegmentsIntersect(p1, p2,
|
|
new ElkPoint { X = ob.Right, Y = ob.Bottom }, new ElkPoint { X = ob.Left, Y = ob.Bottom })
|
|
|| ElkEdgeRoutingGeometry.SegmentsIntersect(p1, p2,
|
|
new ElkPoint { X = ob.Left, Y = ob.Bottom }, new ElkPoint { X = ob.Left, Y = ob.Top }))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static bool HasClearSourceExitSegment(
|
|
IReadOnlyList<ElkPoint> path,
|
|
IReadOnlyCollection<ElkPositionedNode> nodes,
|
|
string? sourceNodeId,
|
|
string? targetNodeId)
|
|
{
|
|
return HasClearBoundarySegments(path, nodes, sourceNodeId, targetNodeId, true, 2);
|
|
}
|
|
|
|
private static string ResolvePreferredRectSourceExitSide(
|
|
IReadOnlyList<ElkPoint> path,
|
|
ElkPositionedNode sourceNode)
|
|
{
|
|
var currentSide = ElkEdgeRoutingGeometry.ResolveBoundarySide(path[0], sourceNode);
|
|
if (path.Count < 2)
|
|
{
|
|
return currentSide;
|
|
}
|
|
|
|
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, sourceNode.Height / 3d);
|
|
var sameColumnThreshold = Math.Max(24d, sourceNode.Width / 3d);
|
|
if (overallAbsDx >= overallAbsDy * 1.15d
|
|
&& overallAbsDy <= sameRowThreshold
|
|
&& Math.Sign(overallDeltaX) != 0)
|
|
{
|
|
return overallDeltaX > 0d ? "right" : "left";
|
|
}
|
|
|
|
if (overallAbsDy >= overallAbsDx * 1.15d
|
|
&& overallAbsDx <= sameColumnThreshold
|
|
&& Math.Sign(overallDeltaY) != 0)
|
|
{
|
|
return overallDeltaY > 0d ? "bottom" : "top";
|
|
}
|
|
|
|
if (HasValidBoundaryAngle(path[0], path[1], sourceNode))
|
|
{
|
|
return currentSide;
|
|
}
|
|
|
|
var deltaX = path[1].X - path[0].X;
|
|
var deltaY = path[1].Y - path[0].Y;
|
|
var absDx = Math.Abs(deltaX);
|
|
var absDy = Math.Abs(deltaY);
|
|
if (absDx >= absDy * 1.15d && Math.Sign(deltaX) != 0)
|
|
{
|
|
return deltaX > 0d ? "right" : "left";
|
|
}
|
|
|
|
if (absDy >= absDx * 1.15d && Math.Sign(deltaY) != 0)
|
|
{
|
|
return deltaY > 0d ? "bottom" : "top";
|
|
}
|
|
|
|
return currentSide;
|
|
}
|
|
|
|
private static string ResolvePreferredRectTargetEntrySide(
|
|
IReadOnlyList<ElkPoint> path,
|
|
ElkPositionedNode targetNode)
|
|
{
|
|
var currentSide = ElkEdgeRoutingGeometry.ResolveBoundarySide(path[^1], targetNode);
|
|
if (path.Count < 2)
|
|
{
|
|
return currentSide;
|
|
}
|
|
|
|
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);
|
|
if (overallAbsDx >= overallAbsDy * 1.15d
|
|
&& overallAbsDy <= sameRowThreshold
|
|
&& Math.Sign(overallDeltaX) != 0)
|
|
{
|
|
return overallDeltaX > 0d ? "left" : "right";
|
|
}
|
|
|
|
if (overallAbsDy >= overallAbsDx * 1.15d
|
|
&& overallAbsDx <= sameColumnThreshold
|
|
&& Math.Sign(overallDeltaY) != 0)
|
|
{
|
|
return overallDeltaY > 0d ? "top" : "bottom";
|
|
}
|
|
|
|
if (HasValidBoundaryAngle(path[^1], path[^2], targetNode))
|
|
{
|
|
return currentSide;
|
|
}
|
|
|
|
var deltaX = path[^1].X - path[^2].X;
|
|
var deltaY = path[^1].Y - path[^2].Y;
|
|
var absDx = Math.Abs(deltaX);
|
|
var absDy = Math.Abs(deltaY);
|
|
if (absDx >= absDy * 1.15d && Math.Sign(deltaX) != 0)
|
|
{
|
|
return deltaX > 0d ? "left" : "right";
|
|
}
|
|
|
|
if (absDy >= absDx * 1.15d && Math.Sign(deltaY) != 0)
|
|
{
|
|
return deltaY > 0d ? "top" : "bottom";
|
|
}
|
|
|
|
return currentSide;
|
|
}
|
|
|
|
private static ElkPoint BuildRectBoundaryPointForSide(
|
|
ElkPositionedNode node,
|
|
string side,
|
|
ElkPoint referencePoint)
|
|
{
|
|
var insetX = Math.Min(24d, Math.Max(8d, node.Width / 4d));
|
|
var insetY = Math.Min(24d, Math.Max(8d, node.Height / 4d));
|
|
return side switch
|
|
{
|
|
"left" => new ElkPoint
|
|
{
|
|
X = node.X,
|
|
Y = Math.Clamp(referencePoint.Y, node.Y + insetY, (node.Y + node.Height) - insetY),
|
|
},
|
|
"right" => new ElkPoint
|
|
{
|
|
X = node.X + node.Width,
|
|
Y = Math.Clamp(referencePoint.Y, node.Y + insetY, (node.Y + node.Height) - insetY),
|
|
},
|
|
"top" => new ElkPoint
|
|
{
|
|
X = Math.Clamp(referencePoint.X, node.X + insetX, (node.X + node.Width) - insetX),
|
|
Y = node.Y,
|
|
},
|
|
"bottom" => new ElkPoint
|
|
{
|
|
X = Math.Clamp(referencePoint.X, node.X + insetX, (node.X + node.Width) - insetX),
|
|
Y = node.Y + node.Height,
|
|
},
|
|
_ => ElkShapeBoundaries.ProjectOntoShapeBoundary(node, referencePoint),
|
|
};
|
|
}
|
|
|
|
}
|