diff --git a/src/__Libraries/StellaOps.ElkSharp/ElkEdgePostProcessor.ApproachExtension.cs b/src/__Libraries/StellaOps.ElkSharp/ElkEdgePostProcessor.ApproachExtension.cs
new file mode 100644
index 000000000..500686c93
--- /dev/null
+++ b/src/__Libraries/StellaOps.ElkSharp/ElkEdgePostProcessor.ApproachExtension.cs
@@ -0,0 +1,284 @@
+namespace StellaOps.ElkSharp;
+
+internal static partial class ElkEdgePostProcessor
+{
+ ///
+ /// Extends short final approach segments so the arrowhead has a clear
+ /// directional run into the target node. Shifts the penultimate vertical
+ /// bend point away from the target to create a longer horizontal approach.
+ /// Only modifies non-gateway rectangular target approaches.
+ ///
+ internal static ElkRoutedEdge[] ExtendShortApproachSegments(
+ ElkRoutedEdge[] edges,
+ ElkPositionedNode[] nodes)
+ {
+ if (edges.Length == 0 || nodes.Length == 0)
+ {
+ return edges;
+ }
+
+ var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal);
+ var serviceNodes = nodes.Where(node => node.Kind is not "Start" and not "End").ToArray();
+ var avgWidth = serviceNodes.Length > 0 ? serviceNodes.Average(node => node.Width) : 160d;
+ var desiredMinApproach = Math.Max(48d, avgWidth / 2d);
+ var graphMinY = nodes.Min(node => node.Y);
+ var graphMaxY = nodes.Max(node => node.Y + node.Height);
+
+ var result = edges.ToArray();
+ var changed = false;
+
+ for (var edgeIndex = 0; edgeIndex < result.Length; edgeIndex++)
+ {
+ var edge = result[edgeIndex];
+
+ if (string.IsNullOrWhiteSpace(edge.TargetNodeId)
+ || !nodesById.TryGetValue(edge.TargetNodeId!, out var targetNode)
+ || HasCorridorBendPoints(edge, graphMinY, graphMaxY))
+ {
+ continue;
+ }
+
+ var path = ExtractFullPath(edge);
+ if (path.Count < 3)
+ {
+ continue;
+ }
+
+ var extended = TryExtendApproach(path, targetNode, desiredMinApproach);
+ if (extended is null)
+ {
+ continue;
+ }
+
+ if (HasNodeObstacleCrossing(extended, nodes, edge.SourceNodeId, edge.TargetNodeId))
+ {
+ continue;
+ }
+
+ var candidateEdge = BuildSingleSectionEdge(edge, extended);
+ var candidateEdges = result.ToArray();
+ candidateEdges[edgeIndex] = candidateEdge;
+ var oldShared = ElkEdgeRoutingScoring.CountSharedLaneViolations(result, nodes);
+ var newShared = ElkEdgeRoutingScoring.CountSharedLaneViolations(candidateEdges, nodes);
+ if (newShared > oldShared)
+ {
+ continue;
+ }
+
+ result[edgeIndex] = candidateEdge;
+ changed = true;
+ }
+
+ return changed ? result : edges;
+ }
+
+ private static List? TryExtendApproach(
+ IReadOnlyList path,
+ ElkPositionedNode targetNode,
+ double desiredMinApproach)
+ {
+ const double tolerance = 1d;
+ if (path.Count < 3)
+ {
+ return null;
+ }
+
+ var endpoint = path[^1];
+ var penultimate = path[^2];
+
+ // Skip diagonal final segments (gateway tip approaches)
+ var dx = Math.Abs(endpoint.X - penultimate.X);
+ var dy = Math.Abs(endpoint.Y - penultimate.Y);
+ if (dx > 3d && dy > 3d)
+ {
+ return null;
+ }
+
+ // Left-face horizontal approach (most common in LTR layout)
+ if (Math.Abs(penultimate.Y - endpoint.Y) <= tolerance
+ && endpoint.X > penultimate.X)
+ {
+ return TryExtendLeftFaceApproach(path, desiredMinApproach, tolerance);
+ }
+
+ // Top-face vertical approach
+ if (Math.Abs(penultimate.X - endpoint.X) <= tolerance
+ && endpoint.Y > penultimate.Y)
+ {
+ return TryExtendTopFaceApproach(path, desiredMinApproach, tolerance);
+ }
+
+ return null;
+ }
+
+ private static List? TryExtendLeftFaceApproach(
+ IReadOnlyList path,
+ double desiredMinApproach,
+ double tolerance)
+ {
+ var endpoint = path[^1];
+ var penultimate = path[^2];
+ var currentApproach = endpoint.X - penultimate.X;
+
+ if (currentApproach <= 0 || currentApproach >= desiredMinApproach - tolerance)
+ {
+ return null;
+ }
+
+ if (path.Count < 3)
+ {
+ return null;
+ }
+
+ var preBend = path[^3];
+ var isStandardLBend = Math.Abs(preBend.X - penultimate.X) <= tolerance;
+
+ if (isStandardLBend)
+ {
+ // Standard case: vertical segment before horizontal approach
+ // Shift both path[^3] and path[^2] to new X
+ double precedingX = path.Count >= 4 ? path[^4].X : path[0].X;
+
+ var maxFeasibleApproach = endpoint.X - precedingX - 1d;
+ if (maxFeasibleApproach <= currentApproach + 2d)
+ {
+ return null;
+ }
+
+ var newApproach = Math.Min(desiredMinApproach, maxFeasibleApproach * 0.8d);
+ newApproach = Math.Max(newApproach, currentApproach);
+ if (newApproach <= currentApproach + 2d)
+ {
+ return null;
+ }
+
+ var newVerticalX = endpoint.X - newApproach;
+
+ if (path.Count >= 4 && path[^4].X > newVerticalX + tolerance)
+ {
+ return null;
+ }
+
+ var extended = path
+ .Select(point => new ElkPoint { X = point.X, Y = point.Y })
+ .ToList();
+ extended[^2] = new ElkPoint { X = newVerticalX, Y = extended[^2].Y };
+ extended[^3] = new ElkPoint { X = newVerticalX, Y = extended[^3].Y };
+
+ return NormalizePathPoints(extended);
+ }
+
+ // Non-standard case: path[^3] → path[^2] is a short horizontal
+ // left-jog before the approach. Look past the jog to find the real
+ // vertical segment and extend from there.
+ if (Math.Abs(preBend.Y - penultimate.Y) <= tolerance
+ && preBend.X > penultimate.X // jog goes LEFT
+ && preBend.X - penultimate.X < 30d // short jog
+ && path.Count >= 5
+ && Math.Abs(path[^4].X - preBend.X) <= tolerance) // vertical before jog
+ {
+ // Pattern: ...→(vertX,prevY)→(vertX,endY)→(jogX,endY)→(targetX,endY)
+ // Collapse the jog and extend the vertical
+ double precedingX = path.Count >= 6 ? path[^5].X : path[0].X;
+
+ var maxFeasibleApproach = endpoint.X - precedingX - 1d;
+ if (maxFeasibleApproach <= currentApproach + 2d)
+ {
+ return null;
+ }
+
+ var newApproach = Math.Min(desiredMinApproach, maxFeasibleApproach * 0.8d);
+ newApproach = Math.Max(newApproach, currentApproach);
+ if (newApproach <= currentApproach + 2d)
+ {
+ return null;
+ }
+
+ var newVerticalX = endpoint.X - newApproach;
+ if (path.Count >= 6 && path[^5].X > newVerticalX + tolerance)
+ {
+ return null;
+ }
+
+ // Build: keep everything before the vertical, shift the vertical,
+ // remove the jog, extend the approach
+ var extended = new List();
+ for (var i = 0; i < path.Count - 4; i++)
+ {
+ extended.Add(new ElkPoint { X = path[i].X, Y = path[i].Y });
+ }
+
+ extended.Add(new ElkPoint { X = newVerticalX, Y = path[^4].Y });
+ extended.Add(new ElkPoint { X = newVerticalX, Y = penultimate.Y });
+ extended.Add(new ElkPoint { X = endpoint.X, Y = endpoint.Y });
+
+ return NormalizePathPoints(extended);
+ }
+
+ return null;
+ }
+
+ private static List? TryExtendTopFaceApproach(
+ IReadOnlyList path,
+ double desiredMinApproach,
+ double tolerance)
+ {
+ var endpoint = path[^1];
+ var penultimate = path[^2];
+ var currentApproach = endpoint.Y - penultimate.Y;
+
+ if (currentApproach <= 0 || currentApproach >= desiredMinApproach - tolerance)
+ {
+ return null;
+ }
+
+ if (path.Count < 3)
+ {
+ return null;
+ }
+
+ var preBend = path[^3];
+ if (Math.Abs(preBend.Y - penultimate.Y) > tolerance)
+ {
+ return null;
+ }
+
+ double precedingY;
+ if (path.Count >= 4)
+ {
+ precedingY = path[^4].Y;
+ }
+ else
+ {
+ precedingY = path[0].Y;
+ }
+
+ var maxFeasibleApproach = endpoint.Y - precedingY - 1d;
+ if (maxFeasibleApproach <= currentApproach + 2d)
+ {
+ return null;
+ }
+
+ var newApproach = Math.Min(desiredMinApproach, maxFeasibleApproach * 0.8d);
+ newApproach = Math.Max(newApproach, currentApproach);
+ if (newApproach <= currentApproach + 2d)
+ {
+ return null;
+ }
+
+ var newHorizontalY = endpoint.Y - newApproach;
+
+ if (path.Count >= 4 && path[^4].Y > newHorizontalY + tolerance)
+ {
+ return null;
+ }
+
+ var extended = path
+ .Select(point => new ElkPoint { X = point.X, Y = point.Y })
+ .ToList();
+ extended[^2] = new ElkPoint { X = extended[^2].X, Y = newHorizontalY };
+ extended[^3] = new ElkPoint { X = extended[^3].X, Y = newHorizontalY };
+
+ return NormalizePathPoints(extended);
+ }
+}
diff --git a/src/__Libraries/StellaOps.ElkSharp/ElkEdgePostProcessor.BacktrackCollapse.cs b/src/__Libraries/StellaOps.ElkSharp/ElkEdgePostProcessor.BacktrackCollapse.cs
new file mode 100644
index 000000000..62d17ec58
--- /dev/null
+++ b/src/__Libraries/StellaOps.ElkSharp/ElkEdgePostProcessor.BacktrackCollapse.cs
@@ -0,0 +1,150 @@
+namespace StellaOps.ElkSharp;
+
+internal static partial class ElkEdgePostProcessor
+{
+ ///
+ /// Removes orthogonal U-turn backtracks from non-corridor edges.
+ /// Detects segments going LEFT in a left-to-right layout and collapses
+ /// the detour to a direct forward path, preserving the last forward
+ /// point before the backtrack and the next forward point after it.
+ /// Only accepts the collapse if it does not introduce node crossings
+ /// or shared lane violations.
+ ///
+ internal static ElkRoutedEdge[] CollapseOrthogonalBacktracks(
+ ElkRoutedEdge[] edges,
+ ElkPositionedNode[] nodes)
+ {
+ if (edges.Length == 0 || nodes.Length == 0)
+ {
+ return edges;
+ }
+
+ var graphMinY = nodes.Min(node => node.Y);
+ var graphMaxY = nodes.Max(node => node.Y + node.Height);
+ var result = edges.ToArray();
+ var changed = false;
+
+ for (var edgeIndex = 0; edgeIndex < result.Length; edgeIndex++)
+ {
+ var edge = result[edgeIndex];
+
+ // Skip corridor-routed edges (repeat returns intentionally go left)
+ if (HasCorridorBendPoints(edge, graphMinY, graphMaxY))
+ {
+ continue;
+ }
+
+ var path = ExtractFullPath(edge);
+ if (path.Count < 5)
+ {
+ continue;
+ }
+
+ var collapsed = TryCollapseBacktrack(path);
+ if (collapsed is null || collapsed.Count >= path.Count)
+ {
+ continue;
+ }
+
+ // Validate: no node crossings
+ if (HasNodeObstacleCrossing(collapsed, nodes, edge.SourceNodeId, edge.TargetNodeId))
+ {
+ continue;
+ }
+
+ // Validate: no new shared lane violations
+ var candidateEdge = BuildSingleSectionEdge(edge, collapsed);
+ var candidateEdges = result.ToArray();
+ candidateEdges[edgeIndex] = candidateEdge;
+ var oldShared = ElkEdgeRoutingScoring.CountSharedLaneViolations(result, nodes);
+ var newShared = ElkEdgeRoutingScoring.CountSharedLaneViolations(candidateEdges, nodes);
+ if (newShared > oldShared)
+ {
+ continue;
+ }
+
+ result[edgeIndex] = candidateEdge;
+ changed = true;
+ }
+
+ return changed ? result : edges;
+ }
+
+ private static List? TryCollapseBacktrack(IReadOnlyList path)
+ {
+ const double tolerance = 2d;
+
+ // Find leftward segments (X decreasing by more than 15px)
+ for (var i = 0; i < path.Count - 1; i++)
+ {
+ var dx = path[i + 1].X - path[i].X;
+ if (dx >= -15d)
+ {
+ continue;
+ }
+
+ // Found a LEFT-going segment at index i→i+1.
+ // The "anchor" before the backtrack is the point before this segment
+ // that was the last rightward/upward turn.
+ var anchorIndex = i;
+
+ // Find the first point AFTER the backtrack that resumes rightward
+ // progress at a similar or higher X than the anchor.
+ var anchorX = path[anchorIndex].X;
+ var resumeIndex = -1;
+ for (var j = i + 2; j < path.Count; j++)
+ {
+ if (path[j].X >= anchorX - tolerance)
+ {
+ resumeIndex = j;
+ break;
+ }
+ }
+
+ if (resumeIndex < 0)
+ {
+ continue;
+ }
+
+ // Build collapsed path: keep everything up to anchor,
+ // connect directly to resume point, keep the rest.
+ var collapsed = new List();
+ for (var j = 0; j <= anchorIndex; j++)
+ {
+ collapsed.Add(new ElkPoint { X = path[j].X, Y = path[j].Y });
+ }
+
+ // Connect anchor to resume via orthogonal bend
+ var anchor = path[anchorIndex];
+ var resume = path[resumeIndex];
+ if (Math.Abs(anchor.X - resume.X) > tolerance
+ && Math.Abs(anchor.Y - resume.Y) > tolerance)
+ {
+ // Need a bend point to keep orthogonal
+ collapsed.Add(new ElkPoint { X = anchor.X, Y = resume.Y });
+ }
+
+ for (var j = resumeIndex; j < path.Count; j++)
+ {
+ var pt = path[j];
+ if (collapsed.Count > 0
+ && Math.Abs(collapsed[^1].X - pt.X) <= tolerance
+ && Math.Abs(collapsed[^1].Y - pt.Y) <= tolerance)
+ {
+ continue;
+ }
+
+ collapsed.Add(new ElkPoint { X = pt.X, Y = pt.Y });
+ }
+
+ // Normalize: remove collinear intermediate points
+ var normalized = NormalizeOrthogonalPath(collapsed, tolerance);
+ if (normalized.Count < path.Count)
+ {
+ return normalized;
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/src/__Libraries/StellaOps.ElkSharp/ElkEdgePostProcessor.CorridorSpacing.cs b/src/__Libraries/StellaOps.ElkSharp/ElkEdgePostProcessor.CorridorSpacing.cs
index f124907bf..899bfbe8b 100644
--- a/src/__Libraries/StellaOps.ElkSharp/ElkEdgePostProcessor.CorridorSpacing.cs
+++ b/src/__Libraries/StellaOps.ElkSharp/ElkEdgePostProcessor.CorridorSpacing.cs
@@ -24,7 +24,8 @@ internal static partial class ElkEdgePostProcessor
var minGap = Math.Max(18d, minLineClearance * 0.6d);
// Collect all above-graph corridor lanes (distinct rounded Y values)
- var corridorEntries = new List<(int EdgeIndex, double CorridorY)>();
+ var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal);
+ var corridorEntries = new List<(int EdgeIndex, double CorridorY, bool IsEndBound)>();
for (var i = 0; i < edges.Length; i++)
{
var bestAboveY = double.NaN;
@@ -53,7 +54,10 @@ internal static partial class ElkEdgePostProcessor
if (!double.IsNaN(bestAboveY) && bestLength > 40d)
{
- corridorEntries.Add((i, bestAboveY));
+ var isEndBound = !string.IsNullOrWhiteSpace(edges[i].TargetNodeId)
+ && nodesById.TryGetValue(edges[i].TargetNodeId!, out var targetNode)
+ && string.Equals(targetNode.Kind, "End", StringComparison.Ordinal);
+ corridorEntries.Add((i, bestAboveY, isEndBound));
}
}
@@ -98,6 +102,39 @@ internal static partial class ElkEdgePostProcessor
}
}
+ // Second pass: enforce End-bound corridors below all repeat-return
+ // corridors. This prevents End corridor horizontals from being crossed
+ // by repeat-return verticals that span from the node field down to
+ // their corridor Y.
+ var deepestRepeatY = double.NaN;
+ for (var i = 0; i < lanes.Length; i++)
+ {
+ if (lanes[i].Entries.Any(entry =>
+ IsRepeatCollectorLabel(edges[entry.EdgeIndex].Label)))
+ {
+ var y = targetYValues[i];
+ if (double.IsNaN(deepestRepeatY) || y < deepestRepeatY)
+ {
+ deepestRepeatY = y;
+ }
+ }
+ }
+
+ if (!double.IsNaN(deepestRepeatY))
+ {
+ var endTargetY = deepestRepeatY - minGap;
+ for (var i = 0; i < lanes.Length; i++)
+ {
+ var isEndLane = lanes[i].Entries.Any(entry => entry.IsEndBound);
+ if (isEndLane && targetYValues[i] > endTargetY + 1d)
+ {
+ targetYValues[i] = endTargetY;
+ needsShift = true;
+ endTargetY -= minGap;
+ }
+ }
+ }
+
if (!needsShift)
{
return edges;
diff --git a/src/__Libraries/StellaOps.ElkSharp/ElkEdgePostProcessor.ProximityReduction.cs b/src/__Libraries/StellaOps.ElkSharp/ElkEdgePostProcessor.ProximityReduction.cs
new file mode 100644
index 000000000..958d96197
--- /dev/null
+++ b/src/__Libraries/StellaOps.ElkSharp/ElkEdgePostProcessor.ProximityReduction.cs
@@ -0,0 +1,218 @@
+namespace StellaOps.ElkSharp;
+
+internal static partial class ElkEdgePostProcessor
+{
+ ///
+ /// Reduces line-node proximity violations by shifting edge segments that
+ /// pass too close to non-source/non-target nodes. Only shifts segments
+ /// AWAY from the node (perpendicular push) and validates that the shift
+ /// does not introduce node crossings or new violations.
+ ///
+ internal static ElkRoutedEdge[] ReduceLineNodeProximity(
+ ElkRoutedEdge[] edges,
+ ElkPositionedNode[] nodes)
+ {
+ if (edges.Length == 0 || nodes.Length == 0)
+ {
+ return edges;
+ }
+
+ var serviceNodes = nodes.Where(node => node.Kind is not "Start" and not "End").ToArray();
+ var minClearance = serviceNodes.Length > 0
+ ? Math.Min(serviceNodes.Average(node => node.Width), serviceNodes.Average(node => node.Height)) / 2d
+ : 50d;
+ var graphMinY = nodes.Min(node => node.Y);
+ var graphMaxY = nodes.Max(node => node.Y + node.Height);
+
+ var result = edges.ToArray();
+ var changed = false;
+
+ for (var edgeIndex = 0; edgeIndex < result.Length; edgeIndex++)
+ {
+ var edge = result[edgeIndex];
+ if (HasCorridorBendPoints(edge, graphMinY, graphMaxY))
+ {
+ continue;
+ }
+
+ var path = ExtractFullPath(edge);
+ if (path.Count < 3)
+ {
+ continue;
+ }
+
+ var adjusted = TryPushSegmentsFromNodes(
+ path,
+ edge.SourceNodeId,
+ edge.TargetNodeId,
+ nodes,
+ minClearance);
+ if (adjusted is null)
+ {
+ continue;
+ }
+
+ if (HasNodeObstacleCrossing(adjusted, nodes, edge.SourceNodeId, edge.TargetNodeId))
+ {
+ continue;
+ }
+
+ var candidateEdge = BuildSingleSectionEdge(edge, adjusted);
+ var candidateEdges = result.ToArray();
+ candidateEdges[edgeIndex] = candidateEdge;
+
+ // Accept only if proximity actually improves and nothing regresses
+ var oldProx = ElkEdgeRoutingScoring.CountProximityViolations(result, nodes);
+ var newProx = ElkEdgeRoutingScoring.CountProximityViolations(candidateEdges, nodes);
+ if (newProx >= oldProx)
+ {
+ continue;
+ }
+
+ var oldShared = ElkEdgeRoutingScoring.CountSharedLaneViolations(result, nodes);
+ var newShared = ElkEdgeRoutingScoring.CountSharedLaneViolations(candidateEdges, nodes);
+ if (newShared > oldShared)
+ {
+ continue;
+ }
+
+ var oldJoin = ElkEdgeRoutingScoring.CountTargetApproachJoinViolations(result, nodes);
+ var newJoin = ElkEdgeRoutingScoring.CountTargetApproachJoinViolations(candidateEdges, nodes);
+ if (newJoin > oldJoin)
+ {
+ continue;
+ }
+
+ var oldCrossings = ElkEdgeRoutingScoring.CountEdgeEdgeCrossings(result, null);
+ var newCrossings = ElkEdgeRoutingScoring.CountEdgeEdgeCrossings(candidateEdges, null);
+ if (newCrossings > oldCrossings)
+ {
+ continue;
+ }
+
+ result[edgeIndex] = candidateEdge;
+ changed = true;
+ }
+
+ return changed ? result : edges;
+ }
+
+ private static List? TryPushSegmentsFromNodes(
+ IReadOnlyList path,
+ string? sourceNodeId,
+ string? targetNodeId,
+ ElkPositionedNode[] nodes,
+ double minClearance)
+ {
+ var adjusted = path
+ .Select(point => new ElkPoint { X = point.X, Y = point.Y })
+ .ToList();
+ var anyChange = false;
+
+ // Check each interior segment (skip first and last which connect to source/target)
+ for (var i = 1; i < adjusted.Count - 2; i++)
+ {
+ var start = adjusted[i];
+ var end = adjusted[i + 1];
+ var isH = Math.Abs(start.Y - end.Y) < 2d;
+ var isV = Math.Abs(start.X - end.X) < 2d;
+
+ if (!isH && !isV)
+ {
+ continue; // skip diagonal
+ }
+
+ foreach (var node in nodes)
+ {
+ if (node.Id == sourceNodeId || node.Id == targetNodeId)
+ {
+ continue;
+ }
+
+ if (isH)
+ {
+ var segMinX = Math.Min(start.X, end.X);
+ var segMaxX = Math.Max(start.X, end.X);
+ if (segMaxX <= node.X || segMinX >= node.X + node.Width)
+ {
+ continue; // no X overlap
+ }
+
+ var distTop = Math.Abs(start.Y - node.Y);
+ var distBottom = Math.Abs(start.Y - (node.Y + node.Height));
+ var minDist = Math.Min(distTop, distBottom);
+
+ if (minDist >= minClearance || minDist < 0.5d)
+ {
+ continue; // not a violation or touching
+ }
+
+ // Push away from the closest face
+ var pushAmount = minClearance - minDist + 2d;
+ double newY;
+ if (distTop < distBottom)
+ {
+ newY = node.Y - minClearance - 2d; // push above node
+ }
+ else
+ {
+ newY = node.Y + node.Height + minClearance + 2d; // push below
+ }
+
+ // Shift both endpoints of this horizontal segment
+ adjusted[i] = new ElkPoint { X = adjusted[i].X, Y = newY };
+ adjusted[i + 1] = new ElkPoint { X = adjusted[i + 1].X, Y = newY };
+
+ // Also adjust the connecting vertical segments
+ if (i > 0 && Math.Abs(adjusted[i - 1].X - adjusted[i].X) < 2d)
+ {
+ // vertical before: keep X, it will naturally connect
+ }
+
+ if (i + 2 < adjusted.Count && Math.Abs(adjusted[i + 2].X - adjusted[i + 1].X) < 2d)
+ {
+ // vertical after: keep X, it will naturally connect
+ }
+
+ anyChange = true;
+ break; // one push per segment
+ }
+ else if (isV)
+ {
+ var segMinY = Math.Min(start.Y, end.Y);
+ var segMaxY = Math.Max(start.Y, end.Y);
+ if (segMaxY <= node.Y || segMinY >= node.Y + node.Height)
+ {
+ continue;
+ }
+
+ var distLeft = Math.Abs(start.X - node.X);
+ var distRight = Math.Abs(start.X - (node.X + node.Width));
+ var minDist = Math.Min(distLeft, distRight);
+
+ if (minDist >= minClearance || minDist < 0.5d)
+ {
+ continue;
+ }
+
+ double newX;
+ if (distLeft < distRight)
+ {
+ newX = node.X - minClearance - 2d;
+ }
+ else
+ {
+ newX = node.X + node.Width + minClearance + 2d;
+ }
+
+ adjusted[i] = new ElkPoint { X = newX, Y = adjusted[i].Y };
+ adjusted[i + 1] = new ElkPoint { X = newX, Y = adjusted[i + 1].Y };
+ anyChange = true;
+ break;
+ }
+ }
+ }
+
+ return anyChange ? NormalizePathPoints(adjusted) : null;
+ }
+}
diff --git a/src/__Libraries/StellaOps.ElkSharp/ElkSharpLayeredLayoutEngine.cs b/src/__Libraries/StellaOps.ElkSharp/ElkSharpLayeredLayoutEngine.cs
index 4770c7503..5328da511 100644
--- a/src/__Libraries/StellaOps.ElkSharp/ElkSharpLayeredLayoutEngine.cs
+++ b/src/__Libraries/StellaOps.ElkSharp/ElkSharpLayeredLayoutEngine.cs
@@ -261,7 +261,9 @@ public sealed class ElkSharpLayeredLayoutEngine : IElkLayoutEngine
// 2. Iterative multi-strategy optimizer (replaces refiner + avoid crossings + diag elim + simplify + tighten)
routedEdges = ElkEdgeRouterIterative.Optimize(routedEdges, finalNodes, options, cancellationToken);
routedEdges = ElkEdgePostProcessor.SpreadOuterCorridors(routedEdges, finalNodes);
- routedEdges = ElkEdgePostProcessorSimplify.SimplifyEdgePaths(routedEdges, finalNodes);
+ routedEdges = ElkEdgePostProcessor.CollapseOrthogonalBacktracks(routedEdges, finalNodes);
+ routedEdges = ElkEdgePostProcessor.ExtendShortApproachSegments(routedEdges, finalNodes);
+ routedEdges = ElkEdgePostProcessor.ReduceLineNodeProximity(routedEdges, finalNodes);
ElkLayoutDiagnostics.LogProgress("ElkSharp layout optimize returned");
return Task.FromResult(new ElkLayoutResult