From 48300839533761e9aaa9ce9a89e260cec8272b69 Mon Sep 17 00:00:00 2001 From: master <> Date: Wed, 1 Apr 2026 23:18:42 +0300 Subject: [PATCH] Move corridor reroute before final target-join spread MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Long sweeps are corridored before the final target-join check so the spread can handle corridor approach convergences. The edge/20+edge/23 convergence at End/top still needs investigation — the spread doesn't detect it (likely End node face slot gap vs approach gap mismatch). Co-Authored-By: Claude Opus 4.6 (1M context) --- ...RouterIterative.WinnerRefinement.Hybrid.cs | 120 +++++++++--------- 1 file changed, 58 insertions(+), 62 deletions(-) diff --git a/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterIterative.WinnerRefinement.Hybrid.cs b/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterIterative.WinnerRefinement.Hybrid.cs index 7cdd2b095..7e46c15b8 100644 --- a/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterIterative.WinnerRefinement.Hybrid.cs +++ b/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterIterative.WinnerRefinement.Hybrid.cs @@ -292,9 +292,64 @@ internal static partial class ElkEdgeRouterIterative current = RepairRemainingEdgeNodeCrossings(current, nodes); } - // Final target-join repair: the per-edge gateway fixes may create - // new target-join convergences that didn't exist before. Run the - // spread one more time to catch them. + // Unconditional corridor reroute: move long sweeps to top corridor + // BEFORE the final target-join check so the join detection sees the + // corridored paths and can spread the corridor approach stubs. + { + var graphMinYLocal = nodes.Min(n => n.Y); + var graphWidthLocal = nodes.Max(n => n.X + n.Width) - nodes.Min(n => n.X); + var baseCorridorY = graphMinYLocal - 56d; + var localMinSweep = graphWidthLocal * 0.4d; + var corridorResult = current.Edges.ToArray(); + var corridorFixed = 0; + for (var ei = 0; ei < corridorResult.Length; ei++) + { + var edge = corridorResult[ei]; + var cpath = ExtractPath(edge); + for (var si = 0; si < cpath.Count - 1; si++) + { + if (Math.Abs(cpath[si].Y - cpath[si + 1].Y) > 2d) continue; + var segLen = Math.Abs(cpath[si + 1].X - cpath[si].X); + var laneY = cpath[si].Y; + if (segLen < localMinSweep || laneY <= graphMinYLocal - 10d) continue; + var localCorridorY = baseCorridorY - (corridorFixed * 24d); + var src = cpath[0]; + var tgt = cpath[^1]; + var stubX = src.X + 24d; + var newPath = new List + { + src, + new() { X = stubX, Y = src.Y }, + new() { X = stubX, Y = localCorridorY }, + new() { X = tgt.X, Y = localCorridorY }, + tgt, + }; + corridorResult[ei] = new ElkRoutedEdge + { + Id = edge.Id, SourceNodeId = edge.SourceNodeId, TargetNodeId = edge.TargetNodeId, + SourcePortId = edge.SourcePortId, TargetPortId = edge.TargetPortId, + Kind = edge.Kind, Label = edge.Label, + Sections = [new ElkEdgeSection + { + StartPoint = newPath[0], EndPoint = newPath[^1], + BendPoints = newPath.Skip(1).Take(newPath.Count - 2).ToArray(), + }], + }; + corridorFixed++; + break; + } + } + if (corridorFixed > 0) + { + var corridorScore = ElkEdgeRoutingScoring.ComputeScore(corridorResult, nodes); + current = current with { Score = corridorScore, Edges = corridorResult }; + ElkLayoutDiagnostics.LogProgress( + $"Unconditional corridor reroute: {corridorFixed} edges to corridor"); + } + } + + // Final target-join repair: the per-edge gateway fixes and corridor + // reroute may create new target-join convergences. var finalJoinSeverity = new Dictionary(StringComparer.Ordinal); var finalJoinCount = ElkEdgeRoutingScoring.CountTargetApproachJoinViolations(current.Edges, nodes, finalJoinSeverity, 1); ElkLayoutDiagnostics.LogProgress( @@ -331,65 +386,6 @@ internal static partial class ElkEdgeRouterIterative current = RepairRemainingEdgeNodeCrossings(current, nodes); } - // Unconditional corridor reroute for long sweeps through the node field. - // The score-gated corridor reroute earlier may reject these because the - // corridor candidate scores worse (more bends, longer paths). But visually, - // long horizontal highways through the graph are ugly. Apply unconditionally. - { - var graphMinYLocal = nodes.Min(n => n.Y); - var graphWidthLocal = nodes.Max(n => n.X + n.Width) - nodes.Min(n => n.X); - var baseCorridorY = graphMinYLocal - 56d; - var localMinSweep = graphWidthLocal * 0.4d; - var corridorResult = current.Edges.ToArray(); - var corridorFixed = 0; - for (var ei = 0; ei < corridorResult.Length; ei++) - { - var edge = corridorResult[ei]; - var path = ExtractPath(edge); - for (var si = 0; si < path.Count - 1; si++) - { - if (Math.Abs(path[si].Y - path[si + 1].Y) > 2d) continue; - var segLen = Math.Abs(path[si + 1].X - path[si].X); - var laneY = path[si].Y; - if (segLen < localMinSweep || laneY <= graphMinYLocal - 10d) continue; - // Route through top corridor. Offset each successive edge - // so multiple corridor routes don't converge at the same Y. - var localCorridorY = baseCorridorY - (corridorFixed * 24d); - var src = path[0]; - var tgt = path[^1]; - var stubX = src.X + 24d; - var newPath = new List - { - src, - new() { X = stubX, Y = src.Y }, - new() { X = stubX, Y = localCorridorY }, - new() { X = tgt.X, Y = localCorridorY }, - tgt, - }; - corridorResult[ei] = new ElkRoutedEdge - { - Id = edge.Id, SourceNodeId = edge.SourceNodeId, TargetNodeId = edge.TargetNodeId, - SourcePortId = edge.SourcePortId, TargetPortId = edge.TargetPortId, - Kind = edge.Kind, Label = edge.Label, - Sections = [new ElkEdgeSection - { - StartPoint = newPath[0], EndPoint = newPath[^1], - BendPoints = newPath.Skip(1).Take(newPath.Count - 2).ToArray(), - }], - }; - corridorFixed++; - break; - } - } - if (corridorFixed > 0) - { - var corridorScore = ElkEdgeRoutingScoring.ComputeScore(corridorResult, nodes); - current = current with { Score = corridorScore, Edges = corridorResult }; - ElkLayoutDiagnostics.LogProgress( - $"Unconditional corridor reroute: {corridorFixed} edges to corridor"); - } - } - return current; }