namespace StellaOps.ElkSharp; internal static partial class ElkEdgePostProcessor { private static List TryLiftUnderNodeSegments( IReadOnlyList path, IReadOnlyCollection nodes, string? sourceNodeId, string? targetNodeId, double minClearance) { var current = path .Select(point => new ElkPoint { X = point.X, Y = point.Y }) .ToList(); for (var pass = 0; pass < 6; pass++) { var changed = false; for (var segmentIndex = 0; segmentIndex < current.Count - 1; segmentIndex++) { if (!TryResolveUnderNodeBlockers( current[segmentIndex], current[segmentIndex + 1], nodes, sourceNodeId, targetNodeId, minClearance, out var blockers)) { continue; } var minX = Math.Min(current[segmentIndex].X, current[segmentIndex + 1].X); var maxX = Math.Max(current[segmentIndex].X, current[segmentIndex + 1].X); var maxRelevantDistance = Math.Max(minClearance * 1.75d, 96d); var overlappingNodes = nodes .Where(node => !string.Equals(node.Id, sourceNodeId, StringComparison.Ordinal) && !string.Equals(node.Id, targetNodeId, StringComparison.Ordinal) && maxX > node.X + 0.5d && minX < node.X + node.Width - 0.5d) .Where(node => { var distanceBelowNode = current[segmentIndex].Y - (node.Y + node.Height); return distanceBelowNode > 0.5d && distanceBelowNode < maxRelevantDistance; }) .ToArray(); var liftY = (overlappingNodes.Length > 0 ? overlappingNodes.Min(node => node.Y) : blockers.Min(node => node.Y)) - Math.Max(24d, minClearance * 0.6d); if (liftY >= current[segmentIndex].Y - 0.5d) { continue; } var rebuilt = new List(current.Count + 2); rebuilt.AddRange(current.Take(segmentIndex + 1).Select(point => new ElkPoint { X = point.X, Y = point.Y })); rebuilt.Add(new ElkPoint { X = current[segmentIndex].X, Y = liftY }); rebuilt.Add(new ElkPoint { X = current[segmentIndex + 1].X, Y = liftY }); rebuilt.AddRange(current.Skip(segmentIndex + 1).Select(point => new ElkPoint { X = point.X, Y = point.Y })); var candidate = NormalizePathPoints(rebuilt); if (!PathChanged(current, candidate) || HasNodeObstacleCrossing(candidate, nodes, sourceNodeId, targetNodeId) || CountUnderNodeSegments(candidate, nodes, sourceNodeId, targetNodeId, minClearance) >= CountUnderNodeSegments(current, nodes, sourceNodeId, targetNodeId, minClearance)) { continue; } current = candidate; changed = true; break; } if (!changed) { break; } } return current; } private static bool TryResolveUnderNodeWithPreferredShortcut( ElkRoutedEdge edge, IReadOnlyList path, IReadOnlyCollection nodes, double minClearance, out List repairedPath) { repairedPath = path .Select(point => new ElkPoint { X = point.X, Y = point.Y }) .ToList(); if (path.Count < 2) { return false; } var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal); if (!nodesById.TryGetValue(edge.SourceNodeId ?? string.Empty, out var sourceNode) || !nodesById.TryGetValue(edge.TargetNodeId ?? string.Empty, out var targetNode)) { return false; } if (TryApplyPreferredBoundaryShortcut( path, sourceNode, targetNode, nodes, edge.SourceNodeId, edge.TargetNodeId, requireUnderNodeImprovement: true, minClearance, out repairedPath)) { return true; } if (TryBuildBestUnderNodeBandCandidate( path, sourceNode, targetNode, nodes, edge.SourceNodeId, edge.TargetNodeId, minClearance, edge.Id, out repairedPath)) { return true; } var targetSide = ResolveTargetApproachSide(path, targetNode); if (!IsRepeatCollectorLabel(edge.Label) && TryBuildGatewaySourceUnderNodeDropCandidate( path, sourceNode, targetNode, nodes, edge.SourceNodeId, edge.TargetNodeId, minClearance, out repairedPath)) { return true; } if (TryBuildSafeGatewaySourceBandCandidate( sourceNode, targetNode, nodes, edge.SourceNodeId, edge.TargetNodeId, path[^1], minClearance, out repairedPath)) { return true; } if (targetSide is "top" or "bottom" && TryBuildSafeHorizontalBandCandidate( sourceNode, targetNode, nodes, edge.SourceNodeId, edge.TargetNodeId, path[0], path[^1], minClearance, preferredSourceExterior: path.Count > 1 ? path[1] : null, out repairedPath)) { return true; } repairedPath = path .Select(point => new ElkPoint { X = point.X, Y = point.Y }) .ToList(); return false; } private static bool TryBuildBestUnderNodeBandCandidate( IReadOnlyList path, ElkPositionedNode sourceNode, ElkPositionedNode targetNode, IReadOnlyCollection nodes, string? sourceNodeId, string? targetNodeId, double minClearance, string? debugEdgeId, out List candidate) { candidate = []; if (path.Count < 2 || !TryResolveUnderNodeBand(path, nodes, sourceNodeId, targetNodeId, minClearance, out var bandY)) { WriteUnderNodeDebug(debugEdgeId, $"band-unavailable path={FormatPath(path)}"); return false; } var currentUnderNodeSegments = CountUnderNodeSegments(path, nodes, sourceNodeId, targetNodeId, minClearance); if (currentUnderNodeSegments == 0) { WriteUnderNodeDebug(debugEdgeId, $"band-skip no-under-node path={FormatPath(path)}"); return false; } WriteUnderNodeDebug(debugEdgeId, $"band-try y={bandY:F2} under={currentUnderNodeSegments} path={FormatPath(path)}"); var bestScore = double.PositiveInfinity; List? bestCandidate = null; var preferredTargetSide = ResolveTargetApproachSide(path, targetNode); foreach (var (side, endBoundary, sideBias) in EnumerateUnderNodeBandTargetBoundaries(path, sourceNode, targetNode, bandY)) { if (!TryBuildExplicitUnderNodeBandCandidate( path, sourceNode, targetNode, nodes, sourceNodeId, targetNodeId, side, bandY, endBoundary, minClearance, out var bandCandidate)) { WriteUnderNodeDebug(debugEdgeId, $"band-reject build side={side} end={FormatPoint(endBoundary)}"); continue; } if (!PathChanged(path, bandCandidate)) { WriteUnderNodeDebug(debugEdgeId, $"band-reject unchanged side={side} candidate={FormatPath(bandCandidate)}"); continue; } var candidateUnderNodeSegments = CountUnderNodeSegments(bandCandidate, nodes, sourceNodeId, targetNodeId, minClearance); if (candidateUnderNodeSegments >= currentUnderNodeSegments) { WriteUnderNodeDebug(debugEdgeId, $"band-reject no-improvement side={side} under={candidateUnderNodeSegments} blockers={FormatUnderNodeBlockers(bandCandidate, nodes, sourceNodeId, targetNodeId, minClearance)} candidate={FormatPath(bandCandidate)}"); continue; } if (ComputePathLength(bandCandidate) > ComputePathLength(path) + 260d) { WriteUnderNodeDebug(debugEdgeId, $"band-reject long side={side} candidate={FormatPath(bandCandidate)}"); continue; } var score = ScoreUnderNodeBandCandidate( path, bandCandidate, targetNode, side, preferredTargetSide, sideBias, nodes, sourceNodeId, targetNodeId, minClearance); if (score >= bestScore) { WriteUnderNodeDebug(debugEdgeId, $"band-reject score side={side} score={score:F2} candidate={FormatPath(bandCandidate)}"); continue; } WriteUnderNodeDebug(debugEdgeId, $"band-candidate side={side} score={score:F2} candidate={FormatPath(bandCandidate)}"); bestScore = score; bestCandidate = bandCandidate; } if (bestCandidate is null) { WriteUnderNodeDebug(debugEdgeId, "band-result none"); return false; } WriteUnderNodeDebug(debugEdgeId, $"band-result selected score={bestScore:F2} candidate={FormatPath(bestCandidate)}"); candidate = bestCandidate; return true; } private static bool TryBuildExplicitUnderNodeBandCandidate( IReadOnlyList originalPath, ElkPositionedNode sourceNode, ElkPositionedNode targetNode, IReadOnlyCollection nodes, string? sourceNodeId, string? targetNodeId, string targetSide, double bandY, ElkPoint endBoundary, double minClearance, out List candidate) { candidate = []; if (originalPath.Count < 2) { return false; } var currentUnderNodeSegments = CountUnderNodeSegments(originalPath, nodes, sourceNodeId, targetNodeId, minClearance); if (currentUnderNodeSegments == 0) { return false; } var bestLength = double.PositiveInfinity; List? bestCandidate = null; foreach (var bandEntryX in EnumerateUnderNodeBandEntryXs(originalPath, endBoundary, nodes, sourceNodeId, targetNodeId, minClearance)) { var candidateBandY = bandY; for (var refinement = 0; refinement < 4; refinement++) { if (!TryResolveUnderNodeBandTargetGeometry( targetNode, targetSide, endBoundary, candidateBandY, minClearance, out var targetBoundary, out var targetExterior)) { break; } if (!TryResolveUnderNodeBandSourceGeometry( originalPath, sourceNode, targetBoundary, bandEntryX, candidateBandY, out var startBoundary, out var sourceExterior)) { break; } var route = BuildUnderNodeBandCandidatePath( startBoundary, sourceExterior, bandEntryX, candidateBandY, targetExterior, targetBoundary); var bandCandidate = NormalizePathPoints(route); if (!IsUsableUnderNodeBandCandidate( bandCandidate, sourceNode, targetNode, nodes, sourceNodeId, targetNodeId)) { break; } var candidateUnderNodeSegments = CountUnderNodeSegments(bandCandidate, nodes, sourceNodeId, targetNodeId, minClearance); if (candidateUnderNodeSegments == 0) { var candidateLength = ComputePathLength(bandCandidate); if (candidateLength < bestLength) { bestLength = candidateLength; bestCandidate = bandCandidate; } break; } if (!TryResolveUnderNodeBand( bandCandidate, nodes, sourceNodeId, targetNodeId, minClearance, out var refinedBandY) || Math.Abs(refinedBandY - candidateBandY) <= 0.5d) { break; } candidateBandY = refinedBandY; } } if (bestCandidate is null) { return false; } candidate = bestCandidate; return true; } private static IEnumerable EnumerateUnderNodeBandEntryXs( IReadOnlyList originalPath, ElkPoint endBoundary, IReadOnlyCollection nodes, string? sourceNodeId, string? targetNodeId, double minClearance) { var clearance = Math.Max(24d, minClearance * 0.6d); var preferredX = originalPath.Count > 1 ? originalPath[1].X : originalPath[0].X; var coordinates = new List { preferredX, originalPath[0].X, endBoundary.X, }; var minX = Math.Min(preferredX, endBoundary.X) - (clearance * 2d); var maxX = Math.Max(preferredX, endBoundary.X) + (clearance * 2d); foreach (var node in nodes) { if (string.Equals(node.Id, sourceNodeId, StringComparison.Ordinal) || string.Equals(node.Id, targetNodeId, StringComparison.Ordinal) || node.X > maxX || node.X + node.Width < minX) { continue; } AddUniqueCoordinate(coordinates, node.X - clearance); AddUniqueCoordinate(coordinates, node.X + node.Width + clearance); } foreach (var coordinate in coordinates .OrderBy(value => Math.Abs(value - preferredX)) .ThenBy(value => Math.Abs(value - endBoundary.X)) .Take(10)) { yield return coordinate; } } private static bool TryResolveUnderNodeBandTargetGeometry( ElkPositionedNode targetNode, string targetSide, ElkPoint preferredBoundary, double bandY, double minClearance, out ElkPoint targetBoundary, out ElkPoint targetExterior) { targetBoundary = default!; targetExterior = default!; var clearance = Math.Max(24d, minClearance * 0.6d); var axisCoordinate = targetSide is "top" or "bottom" ? preferredBoundary.X : preferredBoundary.Y; if (!TryBuildUnderNodeBandTargetBoundary(targetNode, targetSide, axisCoordinate, bandY, out targetBoundary)) { return false; } var targetAnchor = targetSide switch { "left" => new ElkPoint { X = targetBoundary.X - clearance, Y = targetBoundary.Y }, "right" => new ElkPoint { X = targetBoundary.X + clearance, Y = targetBoundary.Y }, _ => new ElkPoint { X = targetBoundary.X, Y = bandY }, }; if (ElkShapeBoundaries.IsGatewayShape(targetNode)) { targetBoundary = ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(targetNode, targetBoundary, targetAnchor); targetExterior = ElkShapeBoundaries.BuildGatewayDirectionalExteriorPoint(targetNode, targetBoundary, targetAnchor); return !ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(targetNode, targetExterior) && ElkShapeBoundaries.HasValidGatewayBoundaryAngle(targetNode, targetBoundary, targetExterior); } targetExterior = targetSide switch { "left" => new ElkPoint { X = targetBoundary.X - clearance, Y = targetBoundary.Y }, "right" => new ElkPoint { X = targetBoundary.X + clearance, Y = targetBoundary.Y }, "top" => new ElkPoint { X = targetBoundary.X, Y = targetBoundary.Y - clearance }, "bottom" => new ElkPoint { X = targetBoundary.X, Y = targetBoundary.Y + clearance }, _ => targetBoundary, }; return true; } private static bool TryResolveUnderNodeBandSourceGeometry( IReadOnlyList originalPath, ElkPositionedNode sourceNode, ElkPoint targetBoundary, double bandEntryX, double bandY, out ElkPoint startBoundary, out ElkPoint sourceExterior) { startBoundary = new ElkPoint { X = originalPath[0].X, Y = originalPath[0].Y }; sourceExterior = startBoundary; if (!ElkShapeBoundaries.IsGatewayShape(sourceNode)) { return true; } var bandEntry = new ElkPoint { X = bandEntryX, Y = bandY }; sourceExterior = ElkShapeBoundaries.BuildPreferredGatewaySourceExteriorPoint(sourceNode, startBoundary, bandEntry); var canReuseCurrentBoundary = CanReuseGatewayBoundaryForBandRoute( sourceNode, startBoundary, sourceExterior, bandEntry); if (!canReuseCurrentBoundary) { if (!TryResolvePreferredGatewaySourceBoundary(sourceNode, bandEntry, targetBoundary, out startBoundary)) { return false; } sourceExterior = ElkShapeBoundaries.BuildPreferredGatewaySourceExteriorPoint(sourceNode, startBoundary, bandEntry); if (ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(sourceNode, sourceExterior) || !ElkShapeBoundaries.HasValidGatewayBoundaryAngle(sourceNode, startBoundary, sourceExterior)) { return false; } } return true; } private static List BuildUnderNodeBandCandidatePath( ElkPoint startBoundary, ElkPoint sourceExterior, double bandEntryX, double bandY, ElkPoint targetExterior, ElkPoint targetBoundary) { var route = new List { startBoundary }; if (!ElkEdgeRoutingGeometry.PointsEqual(route[^1], sourceExterior)) { route.Add(sourceExterior); } if (Math.Abs(route[^1].X - bandEntryX) > 0.5d) { route.Add(new ElkPoint { X = bandEntryX, Y = route[^1].Y }); } if (Math.Abs(route[^1].Y - bandY) > 0.5d) { route.Add(new ElkPoint { X = bandEntryX, Y = bandY }); } if (Math.Abs(route[^1].X - targetExterior.X) > 0.5d) { route.Add(new ElkPoint { X = targetExterior.X, Y = bandY }); } if (Math.Abs(route[^1].Y - targetExterior.Y) > 0.5d) { route.Add(new ElkPoint { X = targetExterior.X, Y = targetExterior.Y }); } if (!ElkEdgeRoutingGeometry.PointsEqual(route[^1], targetExterior)) { route.Add(targetExterior); } if (!ElkEdgeRoutingGeometry.PointsEqual(route[^1], targetBoundary)) { route.Add(targetBoundary); } return route; } private static bool IsUsableUnderNodeBandCandidate( IReadOnlyList candidate, ElkPositionedNode sourceNode, ElkPositionedNode targetNode, IReadOnlyCollection nodes, string? sourceNodeId, string? targetNodeId) { if (candidate.Count < 2 || HasNodeObstacleCrossing(candidate, nodes, sourceNodeId, targetNodeId)) { return false; } if (ElkShapeBoundaries.IsGatewayShape(sourceNode)) { if (!HasAcceptableGatewayBoundaryPath(candidate, nodes, sourceNodeId, targetNodeId, sourceNode, fromStart: true) || !HasCleanGatewaySourceBandPath(candidate, sourceNode)) { return false; } } else if (!HasValidBoundaryAngle(candidate[0], candidate[1], sourceNode)) { return false; } if (ElkShapeBoundaries.IsGatewayShape(targetNode)) { return CanAcceptGatewayTargetRepair(candidate, targetNode) && HasAcceptableGatewayBoundaryPath(candidate, nodes, sourceNodeId, targetNodeId, targetNode, fromStart: false); } return !HasTargetApproachBacktracking(candidate, targetNode) && HasValidBoundaryAngle(candidate[^1], candidate[^2], targetNode); } private static bool TryResolveUnderNodeBand( IReadOnlyList path, IReadOnlyCollection nodes, string? sourceNodeId, string? targetNodeId, double minClearance, out double bandY) { bandY = double.NaN; var blockers = new Dictionary(StringComparer.Ordinal); for (var i = 0; i < path.Count - 1; i++) { if (!TryResolveUnderNodeBlockers( path[i], path[i + 1], nodes, sourceNodeId, targetNodeId, minClearance, out var segmentBlockers)) { continue; } foreach (var blocker in segmentBlockers) { blockers[blocker.Id] = blocker; } } if (blockers.Count == 0) { return false; } var clearance = Math.Max(24d, minClearance * 0.6d); var graphMinY = nodes.Min(node => node.Y); bandY = Math.Max(graphMinY - 72d, blockers.Values.Min(node => node.Y) - clearance); return true; } private static IEnumerable<(string Side, ElkPoint Boundary, double SideBias)> EnumerateUnderNodeBandTargetBoundaries( IReadOnlyList path, ElkPositionedNode sourceNode, ElkPositionedNode targetNode, double bandY) { const double coordinateTolerance = 0.5d; var referenceXs = new List { path[0].X, path[^1].X, path.Count > 1 ? path[1].X : path[0].X, path.Count > 1 ? path[^2].X : path[^1].X, sourceNode.X + (sourceNode.Width / 2d), targetNode.X + (targetNode.Width / 2d), }; var referenceYs = new List { path[0].Y, path[^1].Y, path.Count > 1 ? path[1].Y : path[0].Y, path.Count > 1 ? path[^2].Y : path[^1].Y, sourceNode.Y + (sourceNode.Height / 2d), targetNode.Y + (targetNode.Height / 2d), }; var sides = new List<(string Side, double SideBias)>(); if (bandY < targetNode.Y - coordinateTolerance) { sides.Add(("top", -200d)); } if (bandY > targetNode.Y + targetNode.Height + coordinateTolerance) { sides.Add(("bottom", -200d)); } if (path[0].X <= targetNode.X - coordinateTolerance || path[^1].X <= targetNode.X - coordinateTolerance) { sides.Add(("left", -120d)); } if (path[0].X >= targetNode.X + targetNode.Width + coordinateTolerance || path[^1].X >= targetNode.X + targetNode.Width + coordinateTolerance) { sides.Add(("right", -120d)); } if (sides.Count == 0) { yield break; } var seenBoundaries = new HashSet(StringComparer.Ordinal); foreach (var (side, sideBias) in sides) { var coordinates = side is "top" or "bottom" ? referenceXs : referenceYs; foreach (var coordinate in coordinates) { if (!TryBuildUnderNodeBandTargetBoundary(targetNode, side, coordinate, bandY, out var boundary)) { continue; } var key = $"{side}|{Math.Round(boundary.X, 2):F2}|{Math.Round(boundary.Y, 2):F2}"; if (!seenBoundaries.Add(key)) { continue; } yield return (side, boundary, sideBias); } } } private static bool TryBuildUnderNodeBandTargetBoundary( ElkPositionedNode targetNode, string side, double axisCoordinate, double bandY, out ElkPoint boundary) { boundary = default!; var referencePoint = side switch { "top" or "bottom" => new ElkPoint { X = axisCoordinate, Y = bandY }, "left" => new ElkPoint { X = targetNode.X - 24d, Y = axisCoordinate }, "right" => new ElkPoint { X = targetNode.X + targetNode.Width + 24d, Y = axisCoordinate }, _ => new ElkPoint { X = axisCoordinate, Y = bandY }, }; if (ElkShapeBoundaries.IsGatewayShape(targetNode)) { var slotCoordinate = side is "top" or "bottom" ? referencePoint.X : referencePoint.Y; if (!ElkShapeBoundaries.TryProjectGatewayBoundarySlot(targetNode, side, slotCoordinate, out boundary)) { return false; } boundary = ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(targetNode, boundary, referencePoint); return true; } boundary = BuildRectBoundaryPointForSide(targetNode, side, referencePoint); return true; } private static double ScoreUnderNodeBandCandidate( IReadOnlyList originalPath, IReadOnlyList candidate, ElkPositionedNode targetNode, string requestedTargetSide, string preferredTargetSide, double sideBias, IReadOnlyCollection nodes, string? sourceNodeId, string? targetNodeId, double minClearance) { var candidateUnderNodeSegments = CountUnderNodeSegments(candidate, nodes, sourceNodeId, targetNodeId, minClearance); var score = (candidateUnderNodeSegments * 100_000d) + ComputePathLength(candidate) + (Math.Max(0, candidate.Count - 2) * 8d) + sideBias; var actualTargetSide = ResolveTargetApproachSide(candidate, targetNode); if (!string.Equals(actualTargetSide, preferredTargetSide, StringComparison.Ordinal)) { score += 1_000d; } if (!string.Equals(actualTargetSide, requestedTargetSide, StringComparison.Ordinal)) { score += 350d; } if (ComputePathLength(candidate) > ComputePathLength(originalPath)) { score += (ComputePathLength(candidate) - ComputePathLength(originalPath)) * 0.5d; } return score; } private static void WriteUnderNodeDebug(string? edgeId, string message) { if (edgeId is not ("edge/9" or "edge/25")) { return; } var path = System.IO.Path.Combine(System.AppContext.BaseDirectory, "elksharp.undernode-debug.log"); lock (UnderNodeDebugSync) { System.IO.File.AppendAllText(path, $"[{System.DateTime.UtcNow:O}] {edgeId} {message}{System.Environment.NewLine}"); } } private static string FormatPath(IReadOnlyList path) { return string.Join(" -> ", path.Select(FormatPoint)); } private static string FormatPoint(ElkPoint point) { return $"({point.X:F2},{point.Y:F2})"; } private static string FormatUnderNodeBlockers( IReadOnlyList path, IReadOnlyCollection nodes, string? sourceNodeId, string? targetNodeId, double minClearance) { var blockers = new List(); for (var i = 0; i < path.Count - 1; i++) { if (!TryResolveUnderNodeBlockers(path[i], path[i + 1], nodes, sourceNodeId, targetNodeId, minClearance, out var segmentBlockers)) { continue; } blockers.Add($"{FormatPoint(path[i])}->{FormatPoint(path[i + 1])}:{string.Join(",", segmentBlockers.Select(node => node.Id))}"); } return blockers.Count == 0 ? "" : string.Join(" | ", blockers); } private static bool TryBuildGatewaySourceUnderNodeDropCandidate( IReadOnlyList path, ElkPositionedNode sourceNode, ElkPositionedNode targetNode, IReadOnlyCollection nodes, string? sourceNodeId, string? targetNodeId, double minClearance, out List candidate) { candidate = []; if (!ElkShapeBoundaries.IsGatewayShape(sourceNode) || path.Count < 2) { return false; } ElkPositionedNode[] blockers = []; for (var i = 0; i < path.Count - 1; i++) { if (TryResolveUnderNodeBlockers( path[i], path[i + 1], nodes, sourceNodeId, targetNodeId, minClearance, out blockers)) { break; } } if (blockers.Length == 0) { return false; } if (path[1].Y <= path[0].Y + 0.5d) { return false; } var targetSide = targetNode.X >= sourceNode.X + sourceNode.Width - 0.5d ? "left" : targetNode.X + targetNode.Width <= sourceNode.X + 0.5d ? "right" : ResolveTargetApproachSide(path, targetNode); if (targetSide is not "left" and not "right") { return false; } var currentUnderNodeSegments = CountUnderNodeSegments(path, nodes, sourceNodeId, targetNodeId, minClearance); if (currentUnderNodeSegments == 0) { return false; } var clearance = Math.Max(24d, minClearance * 0.6d); var graphMinY = nodes.Min(node => node.Y); var bandY = blockers.Min(node => node.Y) - clearance; if (bandY <= graphMinY - 96d) { return false; } var targetAnchor = targetSide == "left" ? new ElkPoint { X = targetNode.X - clearance, Y = bandY } : new ElkPoint { X = targetNode.X + targetNode.Width + clearance, Y = bandY }; ElkPoint targetBoundary; ElkPoint targetExterior; if (ElkShapeBoundaries.IsGatewayShape(targetNode)) { if (!ElkShapeBoundaries.TryProjectGatewayBoundarySlot(targetNode, targetSide, bandY, out targetBoundary)) { return false; } targetBoundary = ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(targetNode, targetBoundary, targetAnchor); targetExterior = ElkShapeBoundaries.BuildGatewayDirectionalExteriorPoint(targetNode, targetBoundary, targetAnchor); } else { targetBoundary = BuildRectBoundaryPointForSide(targetNode, targetSide, targetAnchor); targetExterior = new ElkPoint { X = targetSide == "left" ? targetBoundary.X - clearance : targetBoundary.X + clearance, Y = targetBoundary.Y, }; } var bandEntry = new ElkPoint { X = targetExterior.X, Y = bandY }; var startBoundary = path[0]; var sourceExterior = ElkShapeBoundaries.BuildPreferredGatewaySourceExteriorPoint(sourceNode, startBoundary, bandEntry); var canReuseCurrentBoundary = CanReuseGatewayBoundaryForBandRoute( sourceNode, startBoundary, sourceExterior, bandEntry); if (!canReuseCurrentBoundary) { if (!TryResolvePreferredGatewaySourceBoundary(sourceNode, bandEntry, targetBoundary, out startBoundary)) { return false; } sourceExterior = ElkShapeBoundaries.BuildPreferredGatewaySourceExteriorPoint(sourceNode, startBoundary, bandEntry); if (ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(sourceNode, sourceExterior) || !ElkShapeBoundaries.HasValidGatewayBoundaryAngle(sourceNode, startBoundary, sourceExterior)) { return false; } } var route = new List { startBoundary }; if (!ElkEdgeRoutingGeometry.PointsEqual(route[^1], sourceExterior)) { route.Add(sourceExterior); } if (Math.Abs(route[^1].Y - bandY) > 0.5d) { route.Add(new ElkPoint { X = route[^1].X, Y = bandY }); } if (Math.Abs(route[^1].X - targetExterior.X) > 0.5d) { route.Add(new ElkPoint { X = targetExterior.X, Y = bandY }); } if (Math.Abs(route[^1].Y - targetExterior.Y) > 0.5d) { route.Add(new ElkPoint { X = targetExterior.X, Y = targetExterior.Y }); } if (!ElkEdgeRoutingGeometry.PointsEqual(route[^1], targetExterior)) { route.Add(targetExterior); } route.Add(targetBoundary); candidate = NormalizePathPoints(route); if (candidate.Count < 2 || HasNodeObstacleCrossing(candidate, nodes, sourceNodeId, targetNodeId) || !HasAcceptableGatewayBoundaryPath(candidate, nodes, sourceNodeId, targetNodeId, sourceNode, fromStart: true) || !HasCleanGatewaySourceBandPath(candidate, sourceNode) || HasTargetApproachBacktracking(candidate, targetNode) || (ElkShapeBoundaries.IsGatewayShape(targetNode) ? !HasAcceptableGatewayBoundaryPath(candidate, nodes, sourceNodeId, targetNodeId, targetNode, fromStart: false) : !HasValidBoundaryAngle(candidate[^1], candidate[^2], targetNode)) || CountUnderNodeSegments(candidate, nodes, sourceNodeId, targetNodeId, minClearance) >= currentUnderNodeSegments) { candidate = []; return false; } if (ComputePathLength(candidate) > ComputePathLength(path) + 220d) { candidate = []; return false; } return true; } private static bool TryBuildSafeGatewaySourceBandCandidate( ElkPositionedNode sourceNode, ElkPositionedNode targetNode, IReadOnlyCollection nodes, string? sourceNodeId, string? targetNodeId, ElkPoint endBoundary, double minClearance, out List candidate) { candidate = []; if (!ElkShapeBoundaries.IsGatewayShape(sourceNode)) { return false; } var clearance = Math.Max(24d, minClearance * 0.6d); var graphMinY = nodes.Min(node => node.Y); var minX = Math.Min(sourceNode.X, endBoundary.X); var maxX = Math.Max(sourceNode.X + sourceNode.Width, endBoundary.X); var blockers = nodes .Where(node => !string.Equals(node.Id, sourceNodeId, StringComparison.Ordinal) && !string.Equals(node.Id, targetNodeId, StringComparison.Ordinal) && maxX > node.X + 0.5d && minX < node.X + node.Width - 0.5d && node.Y <= Math.Max(sourceNode.Y + sourceNode.Height, endBoundary.Y) + clearance) .ToArray(); var baseY = Math.Min(targetNode.Y, sourceNode.Y); if (blockers.Length > 0) { baseY = Math.Min(baseY, blockers.Min(node => node.Y)); } var bandY = Math.Max(graphMinY - 72d, baseY - clearance); var continuationPoint = new ElkPoint { X = endBoundary.X, Y = bandY }; if (!TryResolvePreferredGatewaySourceBoundary(sourceNode, continuationPoint, endBoundary, out var startBoundary)) { return false; } var sourceExterior = ElkShapeBoundaries.BuildPreferredGatewaySourceExteriorPoint(sourceNode, startBoundary, continuationPoint); if (ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(sourceNode, sourceExterior) || !ElkShapeBoundaries.HasValidGatewayBoundaryAngle(sourceNode, startBoundary, sourceExterior)) { return false; } if (bandY >= Math.Min(sourceExterior.Y, endBoundary.Y) - 0.5d) { return false; } var route = new List { new() { X = startBoundary.X, Y = startBoundary.Y }, new() { X = sourceExterior.X, Y = sourceExterior.Y }, }; if (Math.Abs(route[^1].Y - bandY) > 0.5d) { route.Add(new ElkPoint { X = route[^1].X, Y = bandY }); } if (Math.Abs(route[^1].X - endBoundary.X) > 0.5d) { route.Add(new ElkPoint { X = endBoundary.X, Y = bandY }); } if (Math.Abs(route[^1].Y - endBoundary.Y) > 0.5d) { route.Add(new ElkPoint { X = endBoundary.X, Y = endBoundary.Y }); } candidate = NormalizePathPoints(route); if (candidate.Count < 2 || HasNodeObstacleCrossing(candidate, nodes, sourceNodeId, targetNodeId)) { candidate = []; return false; } if (!HasAcceptableGatewayBoundaryPath(candidate, nodes, sourceNodeId, targetNodeId, sourceNode, fromStart: true)) { candidate = []; return false; } if (!HasCleanGatewaySourceBandPath(candidate, sourceNode)) { candidate = []; return false; } if (ElkShapeBoundaries.IsGatewayShape(targetNode)) { if (!CanAcceptGatewayTargetRepair(candidate, targetNode) || !HasAcceptableGatewayBoundaryPath(candidate, nodes, sourceNodeId, targetNodeId, targetNode, fromStart: false)) { candidate = []; return false; } } else if (HasTargetApproachBacktracking(candidate, targetNode) || !HasValidBoundaryAngle(candidate[^1], candidate[^2], targetNode)) { candidate = []; return false; } return true; } private static bool CanReuseGatewayBoundaryForBandRoute( ElkPositionedNode sourceNode, ElkPoint startBoundary, ElkPoint sourceExterior, ElkPoint bandEntry) { if (!ElkShapeBoundaries.IsGatewayShape(sourceNode) || !ElkShapeBoundaries.IsGatewayBoundaryPoint(sourceNode, startBoundary) || ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(sourceNode, sourceExterior) || !ElkShapeBoundaries.HasValidGatewayBoundaryAngle(sourceNode, startBoundary, sourceExterior)) { return false; } var prefix = new List { startBoundary }; if (!ElkEdgeRoutingGeometry.PointsEqual(prefix[^1], sourceExterior)) { prefix.Add(sourceExterior); } if (Math.Abs(prefix[^1].X - bandEntry.X) > 0.5d) { prefix.Add(new ElkPoint { X = bandEntry.X, Y = prefix[^1].Y }); } if (Math.Abs(prefix[^1].Y - bandEntry.Y) > 0.5d) { prefix.Add(new ElkPoint { X = bandEntry.X, Y = bandEntry.Y }); } prefix = NormalizePathPoints(prefix); return HasCleanGatewaySourceBandPath(prefix, sourceNode); } private static bool HasCleanGatewaySourceBandPath( IReadOnlyList path, ElkPositionedNode sourceNode) { var prefix = ExtractGatewaySourceBandPrefix(path); return !HasGatewaySourceExitBacktracking(prefix) && !HasGatewaySourceExitCurl(prefix) && !HasGatewaySourceDominantAxisDetour(prefix, sourceNode); } private static IReadOnlyList ExtractGatewaySourceBandPrefix(IReadOnlyList path) { if (path.Count < 4) { return path; } var bandY = path.Min(point => point.Y); var bandIndex = -1; for (var i = 1; i < path.Count; i++) { if (Math.Abs(path[i].Y - bandY) <= 0.5d) { bandIndex = i; break; } } if (bandIndex < 1) { bandIndex = Math.Min(path.Count - 1, 3); } return NormalizePathPoints( path.Take(bandIndex + 1) .Select(point => new ElkPoint { X = point.X, Y = point.Y }) .ToList()); } private static bool TryApplyPreferredBoundaryShortcut( IReadOnlyList path, ElkPositionedNode sourceNode, ElkPositionedNode targetNode, IReadOnlyCollection nodes, string? sourceNodeId, string? targetNodeId, bool requireUnderNodeImprovement, double minClearance, out List repairedPath) { repairedPath = path .Select(point => new ElkPoint { X = point.X, Y = point.Y }) .ToList(); if (!TryBuildPreferredBoundaryShortcutPath( sourceNode, targetNode, nodes, sourceNodeId, targetNodeId, out var shortcut)) { return false; } if (HasNodeObstacleCrossing(shortcut, nodes, sourceNodeId, targetNodeId)) { return false; } if (ElkShapeBoundaries.IsGatewayShape(sourceNode)) { if (!HasAcceptableGatewayBoundaryPath(shortcut, nodes, sourceNodeId, targetNodeId, sourceNode, fromStart: true)) { return false; } } else if (shortcut.Count < 2 || !HasValidBoundaryAngle(shortcut[0], shortcut[1], sourceNode)) { return false; } if (ElkShapeBoundaries.IsGatewayShape(targetNode)) { if (!CanAcceptGatewayTargetRepair(shortcut, targetNode) || !HasAcceptableGatewayBoundaryPath(shortcut, nodes, sourceNodeId, targetNodeId, targetNode, fromStart: false)) { return false; } } else if (shortcut.Count < 2 || HasTargetApproachBacktracking(shortcut, targetNode) || !HasValidBoundaryAngle(shortcut[^1], shortcut[^2], targetNode)) { return false; } var currentUnderNodeSegments = CountUnderNodeSegments(path, nodes, sourceNodeId, targetNodeId, minClearance); var shortcutUnderNodeSegments = CountUnderNodeSegments(shortcut, nodes, sourceNodeId, targetNodeId, minClearance); if (requireUnderNodeImprovement && shortcutUnderNodeSegments >= currentUnderNodeSegments) { return false; } var currentLength = ComputePathLength(path); var shortcutLength = ComputePathLength(shortcut); var boundaryInvalid = ElkShapeBoundaries.IsGatewayShape(targetNode) ? NeedsGatewayTargetBoundaryRepair(path, targetNode) : path.Count >= 2 && !HasValidBoundaryAngle(path[^1], path[^2], targetNode); var underNodeImproved = shortcutUnderNodeSegments < currentUnderNodeSegments; if (!underNodeImproved && !boundaryInvalid && shortcutLength > currentLength - 8d) { return false; } repairedPath = shortcut; return true; } private static int CountUnderNodeSegments( IReadOnlyList path, IReadOnlyCollection nodes, string? sourceNodeId, string? targetNodeId, double minClearance) { var count = 0; for (var i = 0; i < path.Count - 1; i++) { if (TryResolveUnderNodeBlockers( path[i], path[i + 1], nodes, sourceNodeId, targetNodeId, minClearance, out _)) { count++; } } return count; } private static bool TryResolveUnderNodeBlockers( ElkPoint start, ElkPoint end, IReadOnlyCollection nodes, string? sourceNodeId, string? targetNodeId, double minClearance, out ElkPositionedNode[] blockers) { blockers = []; if (Math.Abs(start.Y - end.Y) > 2d) { return false; } var minX = Math.Min(start.X, end.X); var maxX = Math.Max(start.X, end.X); blockers = nodes .Where(node => !string.Equals(node.Id, sourceNodeId, StringComparison.Ordinal) && !string.Equals(node.Id, targetNodeId, StringComparison.Ordinal) && maxX > node.X + 0.5d && minX < node.X + node.Width - 0.5d) .Where(node => { var distanceBelowNode = start.Y - (node.Y + node.Height); return distanceBelowNode > 0.5d && distanceBelowNode < minClearance; }) .ToArray(); return blockers.Length > 0; } private static IReadOnlyList<(ElkPoint Start, ElkPoint End)> FlattenSegmentsNearStart( IReadOnlyList path, int maxSegmentsFromStart) { if (path.Count < 2 || maxSegmentsFromStart <= 0) { return []; } var segments = new List<(ElkPoint Start, ElkPoint End)>(Math.Min(path.Count - 1, maxSegmentsFromStart)); var segmentCount = Math.Min(path.Count - 1, maxSegmentsFromStart); for (var i = 0; i < segmentCount; i++) { segments.Add((path[i], path[i + 1])); } return segments; } private static bool IsOrthogonal(ElkPoint start, ElkPoint end) { return Math.Abs(start.X - end.X) <= 0.5d || Math.Abs(start.Y - end.Y) <= 0.5d; } private static bool ShouldSpreadTargetApproach( ElkRoutedEdge edge, double graphMinY, double graphMaxY) { if (!string.IsNullOrWhiteSpace(edge.SourcePortId) || !string.IsNullOrWhiteSpace(edge.TargetPortId)) { return false; } if (!string.IsNullOrWhiteSpace(edge.Kind) && edge.Kind.StartsWith("backward|", StringComparison.OrdinalIgnoreCase)) { return false; } if (IsRepeatCollectorLabel(edge.Label)) { return true; } if (HasProtectedUnderNodeGeometry(edge)) { return false; } if (HasCorridorBendPoints(edge, graphMinY, graphMaxY)) { return false; } return true; } private static bool ShouldSpreadSourceDeparture( ElkRoutedEdge edge, double graphMinY, double graphMaxY) { if (!string.IsNullOrWhiteSpace(edge.SourcePortId) || !string.IsNullOrWhiteSpace(edge.TargetPortId)) { return false; } if (!string.IsNullOrWhiteSpace(edge.Kind) && edge.Kind.StartsWith("backward|", StringComparison.OrdinalIgnoreCase)) { return false; } if (ShouldPreserveSourceExitGeometry(edge, graphMinY, graphMaxY)) { return false; } return true; } private static bool HasClearBoundarySegments( IReadOnlyList path, IReadOnlyCollection nodes, string? sourceNodeId, string? targetNodeId, bool fromStart, int segmentCount) { if (path.Count < 2) { return true; } var obstacles = nodes.Select(node => ( Left: node.X, Top: node.Y, Right: node.X + node.Width, Bottom: node.Y + node.Height, Id: node.Id)).ToArray(); if (fromStart) { var maxIndex = Math.Min(path.Count - 1, segmentCount); for (var i = 0; i < maxIndex; i++) { if (SegmentCrossesObstacle(path[i], path[i + 1], obstacles, sourceNodeId ?? string.Empty, targetNodeId ?? string.Empty)) { return false; } } return true; } var startIndex = Math.Max(0, path.Count - 1 - segmentCount); for (var i = startIndex; i < path.Count - 1; i++) { if (SegmentCrossesObstacle(path[i], path[i + 1], obstacles, sourceNodeId ?? string.Empty, targetNodeId ?? string.Empty)) { return false; } } return true; } private static bool HasValidBoundaryAngle( ElkPoint boundaryPoint, ElkPoint adjacentPoint, ElkPositionedNode node) { if (ElkShapeBoundaries.IsGatewayShape(node)) { return ElkShapeBoundaries.HasValidGatewayBoundaryAngle(node, boundaryPoint, adjacentPoint); } var segDx = Math.Abs(boundaryPoint.X - adjacentPoint.X); var segDy = Math.Abs(boundaryPoint.Y - adjacentPoint.Y); if (segDx < 3d && segDy < 3d) { return true; } var side = ElkEdgeRoutingGeometry.ResolveBoundarySide(boundaryPoint, node); var validForVerticalSide = segDx > segDy * 3d; var validForHorizontalSide = segDy > segDx * 3d; return side switch { "left" or "right" => validForVerticalSide, "top" or "bottom" => validForHorizontalSide, _ => true, }; } private static bool PathChanged(IReadOnlyList left, IReadOnlyList right) { return left.Count != right.Count || !left.Zip(right, ElkEdgeRoutingGeometry.PointsEqual).All(equal => equal); } private static string CreatePathSignature(IReadOnlyList path) { return string.Join(";", path.Select(point => $"{point.X:F3},{point.Y:F3}")); } private static bool HasAcceptableGatewayBoundaryPath( IReadOnlyList path, IReadOnlyCollection nodes, string? sourceNodeId, string? targetNodeId, ElkPositionedNode gatewayNode, bool fromStart) { if (path.Count < 2) { return false; } var boundaryPoint = fromStart ? path[0] : path[^1]; var adjacentPoint = fromStart ? path[1] : path[^2]; if (!ElkShapeBoundaries.HasValidGatewayBoundaryAngle(gatewayNode, boundaryPoint, adjacentPoint)) { return false; } return HasClearBoundarySegments(path, nodes, sourceNodeId, targetNodeId, fromStart, 1) && !HasExcessiveGatewayDiagonalLength(path, gatewayNode) && !HasNodeObstacleCrossing(path, nodes, sourceNodeId, targetNodeId); } private static bool HasNodeObstacleCrossing( IReadOnlyList path, IReadOnlyCollection nodes, string? sourceNodeId, string? targetNodeId) { if (path.Count < 2) { return false; } var obstacles = nodes.Select(node => ( Left: node.X, Top: node.Y, Right: node.X + node.Width, Bottom: node.Y + node.Height, Id: node.Id)).ToArray(); for (var i = 0; i < path.Count - 1; i++) { if (SegmentCrossesObstacle(path[i], path[i + 1], obstacles, sourceNodeId ?? string.Empty, targetNodeId ?? string.Empty)) { return true; } } return false; } private static bool HasNodeObstacleCrossing( IReadOnlyList path, (double Left, double Top, double Right, double Bottom, string Id)[] nodeObstacles, string? sourceNodeId, string? targetNodeId) { if (path.Count < 2) { return false; } for (var i = 0; i < path.Count - 1; i++) { if (SegmentCrossesObstacle(path[i], path[i + 1], nodeObstacles, sourceNodeId ?? string.Empty, targetNodeId ?? string.Empty)) { return true; } } return false; } private static bool CanAcceptGatewayTargetRepair( IReadOnlyList path, ElkPositionedNode targetNode) { return path.Count >= 2 && !ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(targetNode, path[^2]) && !HasExcessiveGatewayDiagonalLength(path, targetNode) && ElkShapeBoundaries.HasValidGatewayBoundaryAngle(targetNode, path[^1], path[^2]); } private static bool HasExcessiveGatewayDiagonalLength( IReadOnlyList path, ElkPositionedNode gatewayNode) { var maxDiagonalLength = (gatewayNode.Width + gatewayNode.Height) / 2d; for (var i = 0; i < path.Count - 1; i++) { var start = path[i]; var end = path[i + 1]; var dx = Math.Abs(end.X - start.X); var dy = Math.Abs(end.Y - start.Y); if (dx <= 3d || dy <= 3d) { continue; } if (ElkEdgeRoutingGeometry.ComputeSegmentLength(start, end) > maxDiagonalLength) { return true; } } return false; } private static int FindFirstGatewayExteriorPointIndex( IReadOnlyList path, ElkPositionedNode node) { for (var i = 1; i < path.Count; i++) { if (!ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(node, path[i])) { return i; } } return Math.Min(1, path.Count - 1); } private static int FindLastGatewayExteriorPointIndex( IReadOnlyList path, ElkPositionedNode node) { for (var i = path.Count - 2; i >= 0; i--) { if (!ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(node, path[i])) { return i; } } return Math.Max(0, path.Count - 2); } private static List ForceGatewayExteriorTargetApproach( IReadOnlyList sourcePath, ElkPositionedNode targetNode, ElkPoint boundaryPoint) { var path = sourcePath .Select(point => new ElkPoint { X = point.X, Y = point.Y }) .ToList(); if (path.Count < 2) { return path; } var exteriorIndex = FindLastGatewayExteriorPointIndex(path, targetNode); var exteriorAnchor = path[exteriorIndex]; var boundary = ElkShapeBoundaries.IsGatewayBoundaryPoint(targetNode, boundaryPoint) ? boundaryPoint : ElkShapeBoundaries.ProjectOntoShapeBoundary(targetNode, exteriorAnchor); boundary = ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(targetNode, boundary, exteriorAnchor); var candidates = ResolveForcedGatewayExteriorApproachCandidates(targetNode, boundary, exteriorAnchor).ToArray(); if (candidates.Length == 0) { return path; } var prefix = path.Take(exteriorIndex + 1) .Select(point => new ElkPoint { X = point.X, Y = point.Y }) .ToList(); if (prefix.Count == 0 || !ElkEdgeRoutingGeometry.PointsEqual(prefix[^1], exteriorAnchor)) { prefix.Add(exteriorAnchor); } foreach (var candidate in candidates) { var rebuilt = prefix .Select(point => new ElkPoint { X = point.X, Y = point.Y }) .ToList(); AppendGatewayTargetOrthogonalCorner( rebuilt, rebuilt[^1], candidate, rebuilt.Count >= 2 ? rebuilt[^2] : null, preferHorizontalFromReference: ShouldPreferHorizontalGatewayExit(rebuilt[^1], candidate), targetNode); if (!ElkEdgeRoutingGeometry.PointsEqual(rebuilt[^1], candidate)) { rebuilt.Add(candidate); } rebuilt.Add(boundary); var normalized = NormalizePathPoints(rebuilt); if (normalized.Count < 2 || ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(targetNode, normalized[^2]) || !ElkShapeBoundaries.HasValidGatewayBoundaryAngle(targetNode, normalized[^1], normalized[^2])) { continue; } return normalized; } return path; } private static IEnumerable ResolveForcedGatewayExteriorApproachCandidates( ElkPositionedNode targetNode, ElkPoint boundaryPoint, ElkPoint exteriorAnchor) { const double padding = 8d; var centerX = targetNode.X + (targetNode.Width / 2d); var centerY = targetNode.Y + (targetNode.Height / 2d); var candidates = new List(); AddUniquePoint( candidates, ElkShapeBoundaries.BuildGatewayDirectionalExteriorPoint(targetNode, boundaryPoint, exteriorAnchor, padding)); AddUniquePoint( candidates, ElkShapeBoundaries.BuildGatewayExteriorApproachPoint(targetNode, boundaryPoint, padding)); if (boundaryPoint.X <= centerX + 0.5d) { AddUniquePoint( candidates, new ElkPoint { X = targetNode.X - padding, Y = boundaryPoint.Y, }); } if (boundaryPoint.X >= centerX - 0.5d) { AddUniquePoint( candidates, new ElkPoint { X = targetNode.X + targetNode.Width + padding, Y = boundaryPoint.Y, }); } if (boundaryPoint.Y <= centerY + 0.5d) { AddUniquePoint( candidates, new ElkPoint { X = boundaryPoint.X, Y = targetNode.Y - padding, }); } if (boundaryPoint.Y >= centerY - 0.5d) { AddUniquePoint( candidates, new ElkPoint { X = boundaryPoint.X, Y = targetNode.Y + targetNode.Height + padding, }); } if (targetNode.Kind == "Decision") { if (boundaryPoint.X <= centerX + 0.5d) { var diagonalDx = Math.Abs(boundaryPoint.X - (targetNode.X - padding)); AddUniquePoint( candidates, new ElkPoint { X = targetNode.X - padding, Y = boundaryPoint.Y - diagonalDx, }); AddUniquePoint( candidates, new ElkPoint { X = targetNode.X - padding, Y = boundaryPoint.Y + diagonalDx, }); } if (boundaryPoint.X >= centerX - 0.5d) { var diagonalDx = Math.Abs((targetNode.X + targetNode.Width + padding) - boundaryPoint.X); AddUniquePoint( candidates, new ElkPoint { X = targetNode.X + targetNode.Width + padding, Y = boundaryPoint.Y - diagonalDx, }); AddUniquePoint( candidates, new ElkPoint { X = targetNode.X + targetNode.Width + padding, Y = boundaryPoint.Y + diagonalDx, }); } if (boundaryPoint.Y <= centerY + 0.5d) { var diagonalDy = Math.Abs(boundaryPoint.Y - (targetNode.Y - padding)); AddUniquePoint( candidates, new ElkPoint { X = boundaryPoint.X - diagonalDy, Y = targetNode.Y - padding, }); AddUniquePoint( candidates, new ElkPoint { X = boundaryPoint.X + diagonalDy, Y = targetNode.Y - padding, }); } if (boundaryPoint.Y >= centerY - 0.5d) { var diagonalDy = Math.Abs((targetNode.Y + targetNode.Height + padding) - boundaryPoint.Y); AddUniquePoint( candidates, new ElkPoint { X = boundaryPoint.X - diagonalDy, Y = targetNode.Y + targetNode.Height + padding, }); AddUniquePoint( candidates, new ElkPoint { X = boundaryPoint.X + diagonalDy, Y = targetNode.Y + targetNode.Height + padding, }); } } return candidates .Where(candidate => !ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(targetNode, candidate) && ElkShapeBoundaries.HasValidGatewayBoundaryAngle(targetNode, boundaryPoint, candidate)) .OrderBy(candidate => ScoreForcedGatewayExteriorApproachCandidate(targetNode, boundaryPoint, candidate, exteriorAnchor)) .ToArray(); } private static double ScoreForcedGatewayExteriorApproachCandidate( ElkPositionedNode targetNode, ElkPoint boundaryPoint, ElkPoint candidate, ElkPoint exteriorAnchor) { var score = Math.Abs(candidate.X - exteriorAnchor.X) + Math.Abs(candidate.Y - exteriorAnchor.Y); score += (Math.Abs(candidate.X - boundaryPoint.X) + Math.Abs(candidate.Y - boundaryPoint.Y)) * 0.25d; var desiredDx = boundaryPoint.X - exteriorAnchor.X; var desiredDy = boundaryPoint.Y - exteriorAnchor.Y; var approachDx = candidate.X - boundaryPoint.X; var approachDy = candidate.Y - boundaryPoint.Y; if (Math.Abs(desiredDx) >= Math.Abs(desiredDy) * 1.15d && Math.Sign(approachDx) != 0 && Math.Sign(approachDx) != Math.Sign(exteriorAnchor.X - boundaryPoint.X)) { score += 10_000d; } if (Math.Abs(desiredDy) >= Math.Abs(desiredDx) * 1.15d && Math.Sign(approachDy) != 0 && Math.Sign(approachDy) != Math.Sign(exteriorAnchor.Y - boundaryPoint.Y)) { score += 10_000d; } var preferredExterior = ElkShapeBoundaries.BuildGatewayExteriorApproachPoint(targetNode, boundaryPoint); if (ElkEdgeRoutingGeometry.PointsEqual(candidate, preferredExterior)) { score -= 8d; } return score; } private static bool NeedsGatewayDiagonalStub(ElkPoint start, ElkPoint end) { var deltaX = Math.Abs(end.X - start.X); var deltaY = Math.Abs(end.Y - start.Y); if (deltaX < 3d || deltaY < 3d) { return false; } var ratio = deltaX / Math.Max(deltaY, 0.001d); return ratio < 0.55d || ratio > 1.85d; } private static bool ShouldUseGatewayDiagonalStub( ElkPositionedNode node, ElkPoint start, ElkPoint end) { return !ElkShapeBoundaries.IsNearGatewayVertex(node, start) && !ElkShapeBoundaries.IsNearGatewayVertex(node, end) && NeedsGatewayDiagonalStub(start, end); } private static List BuildGatewayExitStubbedPath( IReadOnlyList path, ElkPoint boundary, ElkPoint anchor) { var stub = BuildGatewayDiagonalStubPoint(boundary, anchor); var rebuilt = new List { boundary, stub }; AppendGatewayOrthogonalCorner( rebuilt, stub, anchor, path.Count > 2 ? path[2] : null, preferHorizontalFromReference: true); rebuilt.Add(anchor); rebuilt.AddRange(path.Skip(2)); return NormalizePathPoints(rebuilt); } private static List BuildGatewayEntryStubbedPath( IReadOnlyList path, ElkPoint anchor, ElkPoint boundary) { var stub = BuildGatewayDiagonalStubPoint(boundary, anchor); var rebuilt = path.Take(path.Count - 2).ToList(); if (rebuilt.Count == 0 || !ElkEdgeRoutingGeometry.PointsEqual(rebuilt[^1], anchor)) { rebuilt.Add(anchor); } AppendGatewayOrthogonalCorner( rebuilt, anchor, stub, rebuilt.Count >= 2 ? rebuilt[^2] : null, preferHorizontalFromReference: false); rebuilt.Add(stub); rebuilt.Add(boundary); return NormalizePathPoints(rebuilt); } private static ElkPoint BuildGatewayDiagonalStubPoint(ElkPoint boundary, ElkPoint anchor) { var deltaX = Math.Abs(anchor.X - boundary.X); var deltaY = Math.Abs(anchor.Y - boundary.Y); var stubLength = Math.Min(24d, Math.Max(12d, Math.Min(deltaX, deltaY) * 0.5d)); return new ElkPoint { X = boundary.X + (Math.Sign(anchor.X - boundary.X) * stubLength), Y = boundary.Y + (Math.Sign(anchor.Y - boundary.Y) * stubLength), }; } private static void AppendGatewayOrthogonalCorner( IList points, ElkPoint from, ElkPoint to, ElkPoint? referencePoint, bool preferHorizontalFromReference) { const double coordinateTolerance = 0.5d; if (Math.Abs(from.X - to.X) <= coordinateTolerance || Math.Abs(from.Y - to.Y) <= coordinateTolerance) { return; } var cornerA = new ElkPoint { X = to.X, Y = from.Y }; var cornerB = new ElkPoint { X = from.X, Y = to.Y }; var scoreA = ScoreGatewayOrthogonalCorner(cornerA, from, to, referencePoint, preferHorizontalFromReference); var scoreB = ScoreGatewayOrthogonalCorner(cornerB, from, to, referencePoint, !preferHorizontalFromReference); points.Add(scoreA <= scoreB ? cornerA : cornerB); } private static void AppendGatewayTargetOrthogonalCorner( IList points, ElkPoint from, ElkPoint to, ElkPoint? referencePoint, bool preferHorizontalFromReference, ElkPositionedNode targetNode) { const double coordinateTolerance = 0.5d; if (Math.Abs(from.X - to.X) <= coordinateTolerance || Math.Abs(from.Y - to.Y) <= coordinateTolerance) { return; } var cornerA = new ElkPoint { X = to.X, Y = from.Y }; var cornerB = new ElkPoint { X = from.X, Y = to.Y }; var scoreA = ScoreGatewayOrthogonalCorner(cornerA, from, to, referencePoint, preferHorizontalFromReference); var scoreB = ScoreGatewayOrthogonalCorner(cornerB, from, to, referencePoint, !preferHorizontalFromReference); if (ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(targetNode, cornerA)) { scoreA += 100_000d; } if (ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(targetNode, cornerB)) { scoreB += 100_000d; } points.Add(scoreA <= scoreB ? cornerA : cornerB); } private static double ScoreGatewayOrthogonalCorner( ElkPoint corner, ElkPoint from, ElkPoint to, ElkPoint? referencePoint, bool preferHorizontalFirst) { const double coordinateTolerance = 0.5d; var score = preferHorizontalFirst ? 0d : 1d; var totalDx = to.X - from.X; var totalDy = to.Y - from.Y; var firstDx = corner.X - from.X; var firstDy = corner.Y - from.Y; var secondDx = to.X - corner.X; var secondDy = to.Y - corner.Y; if (Math.Abs(firstDx) > coordinateTolerance && Math.Abs(totalDx) > coordinateTolerance && Math.Sign(firstDx) != Math.Sign(totalDx)) { score += 50d; } if (Math.Abs(firstDy) > coordinateTolerance && Math.Abs(totalDy) > coordinateTolerance && Math.Sign(firstDy) != Math.Sign(totalDy)) { score += 50d; } if (Math.Abs(secondDx) > coordinateTolerance && Math.Abs(totalDx) > coordinateTolerance && Math.Sign(secondDx) != Math.Sign(totalDx)) { score += 25d; } if (Math.Abs(secondDy) > coordinateTolerance && Math.Abs(totalDy) > coordinateTolerance && Math.Sign(secondDy) != Math.Sign(totalDy)) { score += 25d; } if (referencePoint is not null) { var reference = referencePoint; score += (Math.Abs(corner.X - reference.X) + Math.Abs(corner.Y - reference.Y)) * 0.02d; if (Math.Abs(reference.Y - from.Y) <= coordinateTolerance) { score -= Math.Abs(corner.Y - from.Y) <= coordinateTolerance ? 1d : 0d; } else if (Math.Abs(reference.X - from.X) <= coordinateTolerance) { score -= Math.Abs(corner.X - from.X) <= coordinateTolerance ? 1d : 0d; } } return score; } private static List ExtractFullPath(ElkRoutedEdge edge) { var path = new List(); foreach (var section in edge.Sections) { if (path.Count == 0) { path.Add(section.StartPoint); } path.AddRange(section.BendPoints); path.Add(section.EndPoint); } return path; } private static double ComputePathLength(IReadOnlyList points) { var length = 0d; for (var i = 1; i < points.Count; i++) { length += ElkEdgeRoutingGeometry.ComputeSegmentLength(points[i - 1], points[i]); } return length; } private static List? TryBuildLocalObstacleSkirtBoundaryShortcut( IReadOnlyList currentPath, ElkPoint start, ElkPoint end, IReadOnlyCollection nodes, string? sourceNodeId, string? targetNodeId, ElkPositionedNode? targetNode, double obstaclePadding) { var rawObstacles = nodes.Select(node => ( Left: node.X, Top: node.Y, Right: node.X + node.Width, Bottom: node.Y + node.Height, Id: node.Id)).ToArray(); var sourceId = sourceNodeId ?? string.Empty; var targetId = targetNodeId ?? string.Empty; var sourceNode = nodes.FirstOrDefault(node => string.Equals(node.Id, sourceId, StringComparison.Ordinal)); var minLineClearance = ResolveMinLineClearance(nodes); List? bestPath = null; var bestScore = double.MaxValue; static (double Left, double Top, double Right, double Bottom, string Id)[] ExpandObstacles( IReadOnlyList<(double Left, double Top, double Right, double Bottom, string Id)> obstacles, double clearance) { return obstacles .Select(obstacle => ( Left: obstacle.Left - clearance, Top: obstacle.Top - clearance, Right: obstacle.Right + clearance, Bottom: obstacle.Bottom + clearance, obstacle.Id)) .ToArray(); } var candidateClearances = new List(); AddUniqueCoordinate(candidateClearances, Math.Max(0d, obstaclePadding)); AddUniqueCoordinate(candidateClearances, Math.Min(Math.Max(0d, obstaclePadding), 24d)); AddUniqueCoordinate(candidateClearances, 8d); AddUniqueCoordinate(candidateClearances, 0d); candidateClearances.Sort((left, right) => right.CompareTo(left)); void ConsiderCandidate( IReadOnlyList rawCandidate, IReadOnlyList<(double Left, double Top, double Right, double Bottom, string Id)> obstacles) { var candidate = NormalizePathPoints(rawCandidate); if (candidate.Count < 2) { return; } for (var i = 1; i < candidate.Count; i++) { if (SegmentCrossesObstacle(candidate[i - 1], candidate[i], obstacles.ToArray(), sourceNodeId, targetNodeId)) { return; } } if (sourceNode is not null) { if (ElkShapeBoundaries.IsGatewayShape(sourceNode) && !HasAcceptableGatewayBoundaryPath( candidate, nodes, sourceNodeId, targetNodeId, sourceNode, fromStart: true)) { return; } if (!ElkShapeBoundaries.IsGatewayShape(sourceNode) && !HasValidBoundaryAngle(candidate[0], candidate[1], sourceNode)) { return; } } if (targetNode is not null && !ElkShapeBoundaries.IsGatewayShape(targetNode) && !HasValidBoundaryAngle(candidate[^1], candidate[^2], targetNode)) { return; } if (targetNode is not null && !ElkShapeBoundaries.IsGatewayShape(targetNode) && HasTargetApproachBacktracking(candidate, targetNode)) { return; } if (targetNode is not null && ElkShapeBoundaries.IsGatewayShape(targetNode) && (!CanAcceptGatewayTargetRepair(candidate, targetNode) || !HasAcceptableGatewayBoundaryPath( candidate, nodes, sourceNodeId, targetNodeId, targetNode, fromStart: false))) { return; } var underNodeSegments = CountUnderNodeSegments( candidate, nodes, sourceNodeId, targetNodeId, minLineClearance); var score = (underNodeSegments * 100_000d) + ComputePathLength(candidate) + (Math.Max(0, candidate.Count - 2) * 4d); if (score >= bestScore - 0.5d) { return; } bestScore = score; bestPath = candidate; } static bool IsUsableForwardBridgeAxis(double startAxis, double endAxis, double candidateAxis) { const double tolerance = 0.5d; var desiredDelta = endAxis - startAxis; var candidateDelta = candidateAxis - startAxis; if (Math.Abs(desiredDelta) <= tolerance || Math.Abs(candidateDelta) <= tolerance || Math.Sign(candidateDelta) != Math.Sign(desiredDelta)) { return false; } var minAxis = Math.Min(startAxis, endAxis) + tolerance; var maxAxis = Math.Max(startAxis, endAxis) - tolerance; return candidateAxis >= minAxis && candidateAxis <= maxAxis; } static void AddForwardBridgeAxisCandidates(List axes, double startAxis, double endAxis) { var desiredDelta = endAxis - startAxis; if (Math.Abs(desiredDelta) <= 1d) { return; } var midpoint = startAxis + (desiredDelta / 2d); if (IsUsableForwardBridgeAxis(startAxis, endAxis, midpoint)) { AddUniqueCoordinate(axes, midpoint); } var forwardStep = startAxis + (Math.Sign(desiredDelta) * Math.Min(48d, Math.Abs(desiredDelta) / 2d)); if (IsUsableForwardBridgeAxis(startAxis, endAxis, forwardStep)) { AddUniqueCoordinate(axes, forwardStep); } } var horizontalDominant = Math.Abs(end.X - start.X) >= Math.Abs(end.Y - start.Y); var startAxis = horizontalDominant ? start.X : start.Y; var endAxis = horizontalDominant ? end.X : end.Y; var sourceBridgeAxes = new List(); AddUniqueCoordinate(sourceBridgeAxes, startAxis); if (currentPath.Count >= 2 && !ElkEdgeRoutingGeometry.PointsEqual(currentPath[1], start)) { var currentBridgeAxis = horizontalDominant ? currentPath[1].X : currentPath[1].Y; if (IsUsableForwardBridgeAxis(startAxis, endAxis, currentBridgeAxis)) { AddUniqueCoordinate(sourceBridgeAxes, currentBridgeAxis); } } AddForwardBridgeAxisCandidates(sourceBridgeAxes, startAxis, endAxis); var targetBridgeAxis = horizontalDominant ? end.X : end.Y; ElkPoint? preservedGatewayTargetApproach = null; double? preservedRectTargetApproachAxis = null; if (targetNode is not null) { if (currentPath.Count >= 2 && !ElkEdgeRoutingGeometry.PointsEqual(currentPath[^2], end)) { if (ElkShapeBoundaries.IsGatewayShape(targetNode)) { if (ElkShapeBoundaries.HasValidGatewayBoundaryAngle(targetNode, end, currentPath[^2])) { preservedGatewayTargetApproach = currentPath[^2]; targetBridgeAxis = horizontalDominant ? currentPath[^2].X : currentPath[^2].Y; } } else if (HasValidBoundaryAngle(end, currentPath[^2], targetNode)) { const double coordinateTolerance = 0.5d; if (horizontalDominant && Math.Abs(currentPath[^2].X - end.X) <= coordinateTolerance) { preservedRectTargetApproachAxis = currentPath[^2].X; } else if (!horizontalDominant && Math.Abs(currentPath[^2].Y - end.Y) <= coordinateTolerance) { preservedRectTargetApproachAxis = currentPath[^2].Y; } } } else if (ElkShapeBoundaries.IsGatewayShape(targetNode)) { var targetExterior = ElkShapeBoundaries.BuildGatewayDirectionalExteriorPoint(targetNode, end, start); if (!ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(targetNode, targetExterior) && ElkShapeBoundaries.HasValidGatewayBoundaryAngle(targetNode, end, targetExterior)) { targetBridgeAxis = horizontalDominant ? targetExterior.X : targetExterior.Y; } } } if (horizontalDominant) { foreach (var clearance in candidateClearances) { var obstacles = ExpandObstacles(rawObstacles, clearance); var minX = Math.Min(start.X, end.X) + 0.5d; var maxX = Math.Max(start.X, end.X) - 0.5d; var corridorTop = Math.Min(start.Y, end.Y) - clearance; var corridorBottom = Math.Max(start.Y, end.Y) + clearance; var bypassYCandidates = new List { start.Y, end.Y }; var cornerBridgeXCandidates = new List(); foreach (var point in currentPath.Skip(1).Take(Math.Max(0, currentPath.Count - 2))) { AddUniqueCoordinate(bypassYCandidates, point.Y); if (IsUsableForwardBridgeAxis(start.X, end.X, point.X)) { AddUniqueCoordinate(cornerBridgeXCandidates, point.X); } } foreach (var obstacle in obstacles) { if (string.Equals(obstacle.Id, sourceId, StringComparison.Ordinal) || string.Equals(obstacle.Id, targetId, StringComparison.Ordinal) || obstacle.Right <= minX || obstacle.Left >= maxX || obstacle.Bottom <= corridorTop || obstacle.Top >= corridorBottom) { continue; } AddUniqueCoordinate(bypassYCandidates, obstacle.Top); AddUniqueCoordinate(bypassYCandidates, obstacle.Bottom); if (IsUsableForwardBridgeAxis(start.X, end.X, obstacle.Left)) { AddUniqueCoordinate(cornerBridgeXCandidates, obstacle.Left); } if (IsUsableForwardBridgeAxis(start.X, end.X, obstacle.Right)) { AddUniqueCoordinate(cornerBridgeXCandidates, obstacle.Right); } } foreach (var bypassY in bypassYCandidates) { foreach (var sourceBridgeAxis in sourceBridgeAxes) { ConsiderCandidate( [ start, new ElkPoint { X = sourceBridgeAxis, Y = start.Y }, new ElkPoint { X = sourceBridgeAxis, Y = bypassY }, end, ], obstacles); if (preservedRectTargetApproachAxis is double rectTargetApproachX && Math.Abs(bypassY - end.Y) > 0.5d) { ConsiderCandidate( [ start, new ElkPoint { X = sourceBridgeAxis, Y = start.Y }, new ElkPoint { X = sourceBridgeAxis, Y = bypassY }, new ElkPoint { X = rectTargetApproachX, Y = bypassY }, end, ], obstacles); } foreach (var cornerBridgeX in cornerBridgeXCandidates) { if (!IsUsableForwardBridgeAxis(sourceBridgeAxis, end.X, cornerBridgeX)) { continue; } ConsiderCandidate( [ start, new ElkPoint { X = sourceBridgeAxis, Y = start.Y }, new ElkPoint { X = sourceBridgeAxis, Y = bypassY }, new ElkPoint { X = cornerBridgeX, Y = bypassY }, end, ], obstacles); } if (targetNode is not null && ElkShapeBoundaries.IsGatewayShape(targetNode) && Math.Abs(targetBridgeAxis - end.X) > 0.5d) { ConsiderCandidate( [ start, new ElkPoint { X = sourceBridgeAxis, Y = start.Y }, new ElkPoint { X = sourceBridgeAxis, Y = bypassY }, new ElkPoint { X = targetBridgeAxis, Y = bypassY }, end, ], obstacles); } if (preservedGatewayTargetApproach is not null && !ElkEdgeRoutingGeometry.PointsEqual( new ElkPoint { X = preservedGatewayTargetApproach.X, Y = bypassY }, preservedGatewayTargetApproach)) { foreach (var cornerBridgeX in cornerBridgeXCandidates) { if (!IsUsableForwardBridgeAxis(sourceBridgeAxis, preservedGatewayTargetApproach.X, cornerBridgeX)) { continue; } ConsiderCandidate( [ start, new ElkPoint { X = sourceBridgeAxis, Y = start.Y }, new ElkPoint { X = sourceBridgeAxis, Y = bypassY }, new ElkPoint { X = cornerBridgeX, Y = bypassY }, preservedGatewayTargetApproach, end, ], obstacles); } ConsiderCandidate( [ start, new ElkPoint { X = sourceBridgeAxis, Y = start.Y }, new ElkPoint { X = sourceBridgeAxis, Y = bypassY }, new ElkPoint { X = preservedGatewayTargetApproach.X, Y = bypassY }, preservedGatewayTargetApproach, end, ], obstacles); } } } } } else { foreach (var clearance in candidateClearances) { var obstacles = ExpandObstacles(rawObstacles, clearance); var minY = Math.Min(start.Y, end.Y) + 0.5d; var maxY = Math.Max(start.Y, end.Y) - 0.5d; var corridorLeft = Math.Min(start.X, end.X) - clearance; var corridorRight = Math.Max(start.X, end.X) + clearance; var bypassXCandidates = new List { start.X, end.X }; foreach (var point in currentPath.Skip(1).Take(Math.Max(0, currentPath.Count - 2))) { AddUniqueCoordinate(bypassXCandidates, point.X); } foreach (var obstacle in obstacles) { if (string.Equals(obstacle.Id, sourceId, StringComparison.Ordinal) || string.Equals(obstacle.Id, targetId, StringComparison.Ordinal) || obstacle.Bottom <= minY || obstacle.Top >= maxY || obstacle.Right <= corridorLeft || obstacle.Left >= corridorRight) { continue; } AddUniqueCoordinate(bypassXCandidates, obstacle.Left); AddUniqueCoordinate(bypassXCandidates, obstacle.Right); } foreach (var bypassX in bypassXCandidates) { foreach (var sourceBridgeAxis in sourceBridgeAxes) { ConsiderCandidate( [ start, new ElkPoint { X = start.X, Y = sourceBridgeAxis }, new ElkPoint { X = bypassX, Y = sourceBridgeAxis }, end, ], obstacles); if (preservedRectTargetApproachAxis is double rectTargetApproachY && Math.Abs(bypassX - end.X) > 0.5d) { ConsiderCandidate( [ start, new ElkPoint { X = start.X, Y = sourceBridgeAxis }, new ElkPoint { X = bypassX, Y = sourceBridgeAxis }, new ElkPoint { X = bypassX, Y = rectTargetApproachY }, end, ], obstacles); } if (targetNode is not null && ElkShapeBoundaries.IsGatewayShape(targetNode) && Math.Abs(targetBridgeAxis - end.Y) > 0.5d) { ConsiderCandidate( [ start, new ElkPoint { X = start.X, Y = sourceBridgeAxis }, new ElkPoint { X = bypassX, Y = sourceBridgeAxis }, new ElkPoint { X = bypassX, Y = targetBridgeAxis }, end, ], obstacles); } if (preservedGatewayTargetApproach is not null && !ElkEdgeRoutingGeometry.PointsEqual( new ElkPoint { X = bypassX, Y = preservedGatewayTargetApproach.Y }, preservedGatewayTargetApproach)) { ConsiderCandidate( [ start, new ElkPoint { X = start.X, Y = sourceBridgeAxis }, new ElkPoint { X = bypassX, Y = sourceBridgeAxis }, new ElkPoint { X = bypassX, Y = preservedGatewayTargetApproach.Y }, preservedGatewayTargetApproach, end, ], obstacles); } } } } } return bestPath; } private static double ResolveMinLineClearance(IReadOnlyCollection nodes) { var serviceNodes = nodes.Where(node => node.Kind is not "Start" and not "End").ToArray(); return serviceNodes.Length > 0 ? Math.Min(serviceNodes.Average(node => node.Width), serviceNodes.Average(node => node.Height)) / 2d : 50d; } private static ElkRoutedEdge BuildSingleSectionEdge( ElkRoutedEdge edge, IReadOnlyList path) { return 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 = path[0], EndPoint = path[^1], BendPoints = path.Count > 2 ? path.Skip(1).Take(path.Count - 2).ToArray() : [], }, ], }; } private static bool HasProtectedUnderNodeGeometry(ElkRoutedEdge edge) { return ContainsInternalKindMarker(edge.Kind, ProtectedUnderNodeKindMarker); } private static ElkRoutedEdge ProtectUnderNodeGeometry(ElkRoutedEdge edge) { if (HasProtectedUnderNodeGeometry(edge)) { return edge; } return CloneEdgeWithKind(edge, AppendInternalKindMarker(edge.Kind, ProtectedUnderNodeKindMarker)); } }