Files
git.stella-ops.org/src/__Libraries/StellaOps.ElkSharp/ElkEdgePostProcessor.GatewayBoundary.Helpers.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

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),
};
}
}