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>
This commit is contained in:
@@ -0,0 +1,248 @@
|
||||
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),
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user