namespace StellaOps.ElkSharp; internal static class ElkEdgePostProcessor { private static readonly object UnderNodeDebugSync = new(); private const string ProtectedUnderNodeKindMarker = "protected-undernode"; internal static ElkRoutedEdge[] SnapAnchorsToNodeBoundary( ElkRoutedEdge[] edges, ElkPositionedNode[] nodes) { var nodesById = nodes.ToDictionary(n => n.Id, StringComparer.Ordinal); var result = new ElkRoutedEdge[edges.Length]; for (var i = 0; i < edges.Length; i++) { var edge = edges[i]; var anyChanged = false; var newSections = edge.Sections.ToList(); for (var s = 0; s < newSections.Count; s++) { var section = newSections[s]; var startFixed = false; var endFixed = false; var newStart = section.StartPoint; var newEnd = section.EndPoint; if (edge.SourceNodeId is not null && nodesById.TryGetValue(edge.SourceNodeId, out var srcNode) && s == 0) { if (newStart.X > srcNode.X + 1d && newStart.X < srcNode.X + srcNode.Width - 1d && newStart.Y > srcNode.Y + 1d && newStart.Y < srcNode.Y + srcNode.Height - 1d) { var target = section.BendPoints.Count > 0 ? section.BendPoints.First() : section.EndPoint; newStart = ElkShapeBoundaries.ProjectOntoShapeBoundary(srcNode, target); startFixed = true; } } if (edge.TargetNodeId is not null && nodesById.TryGetValue(edge.TargetNodeId, out var tgtNode) && s == newSections.Count - 1) { var source = section.BendPoints.Count > 0 ? section.BendPoints.Last() : section.StartPoint; var projected = ElkShapeBoundaries.ProjectOntoShapeBoundary(tgtNode, source); if (Math.Abs(projected.X - newEnd.X) > 3d || Math.Abs(projected.Y - newEnd.Y) > 3d) { newEnd = projected; endFixed = true; } } if (startFixed || endFixed) { anyChanged = true; newSections[s] = new ElkEdgeSection { StartPoint = newStart, EndPoint = newEnd, BendPoints = section.BendPoints, }; } } result[i] = anyChanged ? new ElkRoutedEdge { Id = edge.Id, SourceNodeId = edge.SourceNodeId, TargetNodeId = edge.TargetNodeId, Label = edge.Label, Sections = newSections } : edge; } return result; } internal static ElkRoutedEdge[] ClearInternalRoutingMarkers(ElkRoutedEdge[] edges) { if (edges.Length == 0) { return edges; } var changed = false; var result = new ElkRoutedEdge[edges.Length]; for (var i = 0; i < edges.Length; i++) { var edge = edges[i]; var cleanedKind = RemoveInternalKindMarker(edge.Kind, ProtectedUnderNodeKindMarker); if (string.Equals(cleanedKind, edge.Kind, StringComparison.Ordinal)) { result[i] = edge; continue; } changed = true; result[i] = CloneEdgeWithKind(edge, cleanedKind); } return changed ? result : edges; } internal static ElkRoutedEdge[] AvoidNodeCrossings( ElkRoutedEdge[] edges, ElkPositionedNode[] nodes, ElkLayoutDirection direction, IReadOnlyCollection? restrictedEdgeIds = null) { if (direction != ElkLayoutDirection.LeftToRight || nodes.Length == 0) { return edges; } const double margin = 18d; var obstacles = nodes.Select(n => ( Left: n.X - margin, Top: n.Y - margin, Right: n.X + n.Width + margin, Bottom: n.Y + n.Height + margin, Id: n.Id )).ToArray(); var graphMinY = nodes.Min(n => n.Y); var graphMaxY = nodes.Max(n => n.Y + n.Height); var restrictedSet = restrictedEdgeIds is null ? null : restrictedEdgeIds.ToHashSet(StringComparer.Ordinal); var result = new ElkRoutedEdge[edges.Length]; for (var edgeIndex = 0; edgeIndex < edges.Length; edgeIndex++) { var edge = edges[edgeIndex]; if (restrictedSet is not null && !restrictedSet.Contains(edge.Id)) { result[edgeIndex] = edge; continue; } var sourceId = edge.SourceNodeId ?? ""; var targetId = edge.TargetNodeId ?? ""; var hasCorridorPts = HasCorridorBendPoints(edge, graphMinY, graphMaxY); var hasCrossing = false; foreach (var section in edge.Sections) { var pts = new List { section.StartPoint }; pts.AddRange(section.BendPoints); pts.Add(section.EndPoint); for (var i = 0; i < pts.Count - 1 && !hasCrossing; i++) { if (hasCorridorPts && IsCorridorSegment(pts[i], pts[i + 1], graphMinY, graphMaxY)) { continue; } hasCrossing = SegmentCrossesObstacle(pts[i], pts[i + 1], obstacles, sourceId, targetId); } } if (!hasCrossing) { result[edgeIndex] = edge; continue; } var hasCorridorPoints = hasCorridorPts; var newSections = new List(edge.Sections.Count); foreach (var section in edge.Sections) { if (hasCorridorPoints) { var corridorRerouted = ElkEdgePostProcessorCorridor.ReroutePreservingCorridor( section, obstacles, sourceId, targetId, margin, graphMinY, graphMaxY); if (corridorRerouted is not null) { newSections.Add(corridorRerouted); continue; } } var rerouted = ElkEdgePostProcessorAStar.RerouteWithGridAStar( section.StartPoint, section.EndPoint, obstacles, sourceId, targetId, margin); if (rerouted is not null && rerouted.Count >= 2) { newSections.Add(new ElkEdgeSection { StartPoint = rerouted[0], EndPoint = rerouted[^1], BendPoints = rerouted.Skip(1).Take(rerouted.Count - 2).ToArray(), }); } else { newSections.Add(section); } } result[edgeIndex] = new ElkRoutedEdge { Id = edge.Id, SourceNodeId = edge.SourceNodeId, TargetNodeId = edge.TargetNodeId, Label = edge.Label, Sections = newSections, }; } return result; } internal static ElkRoutedEdge[] EliminateDiagonalSegments(ElkRoutedEdge[] edges, ElkPositionedNode[] nodes) { var graphMinY = nodes.Length > 0 ? nodes.Min(n => n.Y) : 0d; var graphMaxY = nodes.Length > 0 ? nodes.Max(n => n.Y + n.Height) : 0d; var nodesById = nodes.ToDictionary(n => n.Id, StringComparer.Ordinal); var obstacles = nodes.Select(n => (L: n.X - 4d, T: n.Y - 4d, R: n.X + n.Width + 4d, B: n.Y + n.Height + 4d, Id: n.Id)).ToArray(); var result = new ElkRoutedEdge[edges.Length]; for (var i = 0; i < edges.Length; i++) { var edge = edges[i]; var anyFixed = false; var newSections = new List(); var sectionList = edge.Sections.ToList(); for (var sIdx = 0; sIdx < sectionList.Count; sIdx++) { var section = sectionList[sIdx]; var isLastSection = sIdx == sectionList.Count - 1; var pts = new List { section.StartPoint }; pts.AddRange(section.BendPoints); pts.Add(section.EndPoint); var fixedPts = new List { pts[0] }; for (var j = 1; j < pts.Count; j++) { var prev = fixedPts[^1]; var curr = pts[j]; var dx = Math.Abs(curr.X - prev.X); var dy = Math.Abs(curr.Y - prev.Y); if (dx > 3d && dy > 3d) { var prevIsCorridor = prev.Y < graphMinY - 8d || prev.Y > graphMaxY + 8d; var currIsCorridor = curr.Y < graphMinY - 8d || curr.Y > graphMaxY + 8d; var isBackwardSection = section.EndPoint.X < section.StartPoint.X - 1d; if (prevIsCorridor) { fixedPts.Add(new ElkPoint { X = curr.X, Y = prev.Y }); anyFixed = true; } else if (currIsCorridor && isBackwardSection) { // Preserve diagonal for backward collector edges } else if (isLastSection && j == pts.Count - 1 && !isBackwardSection) { // Target approach: L-corner must be perpendicular to the entry side. // Vertical side (left/right) → last segment horizontal (default). // Horizontal side (top/bottom) → last segment vertical (flipped). var targetNode = nodesById.GetValueOrDefault(edge.TargetNodeId ?? ""); var onHorizontalSide = targetNode is not null && (Math.Abs(curr.Y - targetNode.Y) < 2d || Math.Abs(curr.Y - (targetNode.Y + targetNode.Height)) < 2d); if (onHorizontalSide) { fixedPts.Add(new ElkPoint { X = curr.X, Y = prev.Y }); } else { fixedPts.Add(new ElkPoint { X = prev.X, Y = curr.Y }); } anyFixed = true; } else { fixedPts.Add(new ElkPoint { X = prev.X, Y = curr.Y }); anyFixed = true; } } fixedPts.Add(curr); } newSections.Add(new ElkEdgeSection { StartPoint = fixedPts[0], EndPoint = fixedPts[^1], BendPoints = fixedPts.Skip(1).Take(fixedPts.Count - 2).ToArray(), }); } result[i] = anyFixed ? new ElkRoutedEdge { Id = edge.Id, SourceNodeId = edge.SourceNodeId, TargetNodeId = edge.TargetNodeId, Label = edge.Label, Sections = newSections } : edge; } return result; } internal static ElkRoutedEdge[] NormalizeBoundaryAngles( ElkRoutedEdge[] edges, ElkPositionedNode[] nodes) { if (edges.Length == 0 || nodes.Length == 0) { return edges; } var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal); var graphMinY = nodes.Min(node => node.Y); var graphMaxY = nodes.Max(node => node.Y + node.Height); var result = new ElkRoutedEdge[edges.Length]; for (var i = 0; i < edges.Length; i++) { var edge = edges[i]; 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); } if (path.Count < 2) { result[i] = edge; continue; } var normalized = path; var preserveSourceExit = ShouldPreserveSourceExitGeometry(edge, graphMinY, graphMaxY); if (!preserveSourceExit && nodesById.TryGetValue(edge.SourceNodeId ?? string.Empty, out var sourceNode)) { List sourceNormalized; if (ElkShapeBoundaries.IsGatewayShape(sourceNode)) { sourceNormalized = NormalizeGatewayExitPath(normalized, sourceNode, nodes, edge.SourceNodeId, edge.TargetNodeId); if (PathStartsAtDecisionVertex(sourceNormalized, sourceNode)) { sourceNormalized = ForceDecisionSourceExitOffVertex( sourceNormalized, sourceNode, nodes, edge.SourceNodeId, edge.TargetNodeId); } } else { var sourceSide = ElkEdgeRoutingGeometry.ResolveBoundarySide(normalized[0], sourceNode); sourceNormalized = NormalizeExitPath(normalized, sourceNode, sourceSide); } if (HasClearSourceExitSegment(sourceNormalized, nodes, edge.SourceNodeId, edge.TargetNodeId)) { normalized = sourceNormalized; } } if (nodesById.TryGetValue(edge.TargetNodeId ?? string.Empty, out var targetNode)) { if (ElkShapeBoundaries.IsGatewayShape(targetNode)) { var gatewayNormalized = NormalizeGatewayEntryPath(normalized, targetNode, normalized[^1]); if (HasAcceptableGatewayBoundaryPath(gatewayNormalized, nodes, edge.SourceNodeId, edge.TargetNodeId, targetNode, fromStart: false)) { normalized = gatewayNormalized; } } else { var targetSide = ElkEdgeRoutingGeometry.ResolveBoundarySide(normalized[^1], targetNode); ElkPoint? preferredEndpoint = null; if (HasTargetApproachBacktracking(normalized, targetNode) && TryResolveNonGatewayBacktrackingEndpoint(normalized, targetNode, out var preferredSide, out var preferredBoundary)) { targetSide = preferredSide; preferredEndpoint = preferredBoundary; } normalized = NormalizeEntryPath(normalized, targetNode, targetSide, preferredEndpoint); if (HasTargetApproachBacktracking(normalized, targetNode) && TryNormalizeNonGatewayBacktrackingEntry(normalized, targetNode, out var backtrackingRepair)) { normalized = backtrackingRepair; } } } if (normalized.Count == path.Count && normalized.Zip(path, ElkEdgeRoutingGeometry.PointsEqual).All(equal => equal)) { result[i] = edge; continue; } result[i] = 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 = normalized[0], EndPoint = normalized[^1], BendPoints = normalized.Count > 2 ? normalized.Skip(1).Take(normalized.Count - 2).ToArray() : [], }, ], }; } return result; } internal static ElkRoutedEdge[] NormalizeTargetEntryAngles( ElkRoutedEdge[] edges, ElkPositionedNode[] nodes) { return NormalizeBoundaryAngles(edges, nodes); } internal static ElkRoutedEdge[] NormalizeSourceExitAngles( ElkRoutedEdge[] edges, ElkPositionedNode[] nodes) { if (edges.Length == 0 || nodes.Length == 0) { return edges; } var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal); var graphMinY = nodes.Min(node => node.Y); var graphMaxY = nodes.Max(node => node.Y + node.Height); var result = new ElkRoutedEdge[edges.Length]; for (var i = 0; i < edges.Length; i++) { var edge = edges[i]; var preserveSourceExit = ShouldPreserveSourceExitGeometry(edge, graphMinY, graphMaxY); if (!nodesById.TryGetValue(edge.SourceNodeId ?? string.Empty, out var sourceNode)) { result[i] = edge; continue; } 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); } if (path.Count < 2) { result[i] = edge; continue; } if (preserveSourceExit) { var hasAcceptablePreservedExit = ElkShapeBoundaries.IsGatewayShape(sourceNode) ? HasAcceptableGatewayBoundaryPath(path, nodes, edge.SourceNodeId, edge.TargetNodeId, sourceNode, fromStart: true) : HasValidBoundaryAngle(path[0], path[1], sourceNode); if (hasAcceptablePreservedExit) { result[i] = edge; continue; } } var sourceSide = ElkShapeBoundaries.IsGatewayShape(sourceNode) ? ElkEdgeRoutingGeometry.ResolveBoundarySide(path[0], sourceNode) : ResolvePreferredRectSourceExitSide(path, sourceNode); List normalized; if (ElkShapeBoundaries.IsGatewayShape(sourceNode)) { normalized = NormalizeGatewayExitPath(path, sourceNode, nodes, edge.SourceNodeId, edge.TargetNodeId); } else { var sourcePath = path .Select(point => new ElkPoint { X = point.X, Y = point.Y }) .ToList(); if (!HasValidBoundaryAngle(sourcePath[0], sourcePath[1], sourceNode)) { sourcePath[0] = BuildRectBoundaryPointForSide(sourceNode, sourceSide, sourcePath[1]); } normalized = NormalizeExitPath(sourcePath, sourceNode, sourceSide); } if (ElkShapeBoundaries.IsGatewayShape(sourceNode) && PathStartsAtDecisionVertex(normalized, sourceNode)) { normalized = ForceDecisionSourceExitOffVertex( normalized, sourceNode, nodes, edge.SourceNodeId, edge.TargetNodeId); } if (ElkShapeBoundaries.IsGatewayShape(sourceNode)) { if (!HasAcceptableGatewayBoundaryPath(normalized, nodes, edge.SourceNodeId, edge.TargetNodeId, sourceNode, fromStart: true)) { result[i] = edge; continue; } } else if (!HasClearSourceExitSegment(normalized, nodes, edge.SourceNodeId, edge.TargetNodeId)) { result[i] = edge; continue; } if (normalized.Count == path.Count && normalized.Zip(path, ElkEdgeRoutingGeometry.PointsEqual).All(equal => equal)) { result[i] = edge; continue; } result[i] = 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 = normalized[0], EndPoint = normalized[^1], BendPoints = normalized.Count > 2 ? normalized.Skip(1).Take(normalized.Count - 2).ToArray() : [], }, ], }; } return result; } internal static ElkRoutedEdge[] FinalizeDecisionTargetEntries( ElkRoutedEdge[] edges, ElkPositionedNode[] nodes, IReadOnlyCollection? restrictedEdgeIds = null) { if (edges.Length == 0 || nodes.Length == 0) { return edges; } var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal); var restrictedSet = restrictedEdgeIds is null ? null : restrictedEdgeIds.ToHashSet(StringComparer.Ordinal); var result = edges.ToArray(); for (var i = 0; i < result.Length; i++) { var edge = result[i]; if (restrictedSet is not null && !restrictedSet.Contains(edge.Id)) { continue; } if (!nodesById.TryGetValue(edge.TargetNodeId ?? string.Empty, out var targetNode) || targetNode.Kind != "Decision") { continue; } var path = ExtractFullPath(edge); List repaired; if (nodesById.TryGetValue(edge.SourceNodeId ?? string.Empty, out var sourceNode) && TryBuildPreferredGatewayTargetEntryPath( path, sourceNode, targetNode, nodes, edge.SourceNodeId, edge.TargetNodeId, out var preferredGatewayTargetRepair)) { repaired = preferredGatewayTargetRepair; } else { repaired = NormalizeGatewayEntryPath(path, targetNode, path[^1]); } if ((!CanAcceptGatewayTargetRepair(repaired, targetNode) || ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(targetNode, repaired[^2])) && targetNode.Kind == "Decision") { repaired = ForceDecisionExteriorTargetEntry(path, targetNode); } if ((!PathChanged(path, repaired) || !CanAcceptGatewayTargetRepair(repaired, targetNode)) && targetNode.Kind == "Decision") { repaired = ForceDecisionDirectTargetEntry(path, targetNode); } if (!PathChanged(path, repaired) || !CanAcceptGatewayTargetRepair(repaired, targetNode)) { continue; } result[i] = BuildSingleSectionEdge(edge, repaired); } return result; } private static bool TryBuildPreferredGatewayTargetEntryPath( IReadOnlyList path, ElkPositionedNode sourceNode, ElkPositionedNode targetNode, IReadOnlyCollection nodes, string? sourceNodeId, string? targetNodeId, out List repaired) { repaired = []; if (!ElkShapeBoundaries.IsGatewayShape(targetNode) || path.Count < 2) { return false; } var sourceCenterX = sourceNode.X + (sourceNode.Width / 2d); var sourceCenterY = sourceNode.Y + (sourceNode.Height / 2d); var targetCenterX = targetNode.X + (targetNode.Width / 2d); var targetCenterY = targetNode.Y + (targetNode.Height / 2d); var deltaX = targetCenterX - sourceCenterX; var deltaY = targetCenterY - sourceCenterY; var absDx = Math.Abs(deltaX); var absDy = Math.Abs(deltaY); var sameRowThreshold = Math.Max(40d, (sourceNode.Height + targetNode.Height) / 3d); var sameColumnThreshold = Math.Max(40d, (sourceNode.Width + targetNode.Width) / 3d); string? preferredSide = null; double preferredSlotCoordinate = 0d; if (((absDx >= absDy * 1.15d && absDy <= sameRowThreshold) || (absDx >= absDy * 1.75d)) && Math.Sign(deltaX) != 0) { preferredSide = deltaX > 0d ? "left" : "right"; preferredSlotCoordinate = sourceCenterY; } else if (((absDy >= absDx * 1.15d && absDx <= sameColumnThreshold) || (absDy >= absDx * 1.75d)) && Math.Sign(deltaY) != 0) { preferredSide = deltaY > 0d ? "top" : "bottom"; preferredSlotCoordinate = sourceCenterX; } if (preferredSide is null || !ElkShapeBoundaries.TryProjectGatewayBoundarySlot(targetNode, preferredSide, preferredSlotCoordinate, out var preferredBoundary)) { return false; } var exteriorIndex = FindLastGatewayExteriorPointIndex(path, targetNode); var exteriorAnchor = path[exteriorIndex]; preferredBoundary = ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(targetNode, preferredBoundary, exteriorAnchor); var directPreferred = path.Take(exteriorIndex + 1) .Select(point => new ElkPoint { X = point.X, Y = point.Y }) .ToList(); if (directPreferred.Count == 0 || !ElkEdgeRoutingGeometry.PointsEqual(directPreferred[^1], exteriorAnchor)) { directPreferred.Add(new ElkPoint { X = exteriorAnchor.X, Y = exteriorAnchor.Y }); } directPreferred.Add(new ElkPoint { X = preferredBoundary.X, Y = preferredBoundary.Y }); var directNormalized = NormalizePathPoints(directPreferred); if (directNormalized.Count >= 2 && string.Equals(ElkEdgeRoutingGeometry.ResolveBoundarySide(directNormalized[^1], targetNode), preferredSide, StringComparison.Ordinal) && CanAcceptGatewayTargetRepair(directNormalized, targetNode) && HasAcceptableGatewayBoundaryPath(directNormalized, nodes, sourceNodeId, targetNodeId, targetNode, fromStart: false)) { repaired = directNormalized; return true; } var normalizedPreferred = NormalizeGatewayEntryPath(path, targetNode, preferredBoundary); if (normalizedPreferred.Count < 2 || !string.Equals(ElkEdgeRoutingGeometry.ResolveBoundarySide(normalizedPreferred[^1], targetNode), preferredSide, StringComparison.Ordinal) || !CanAcceptGatewayTargetRepair(normalizedPreferred, targetNode) || !HasAcceptableGatewayBoundaryPath(normalizedPreferred, nodes, sourceNodeId, targetNodeId, targetNode, fromStart: false)) { return false; } repaired = normalizedPreferred; return true; } internal static ElkRoutedEdge[] PreferShortestBoundaryShortcuts( ElkRoutedEdge[] edges, ElkPositionedNode[] nodes, IReadOnlyCollection? restrictedEdgeIds = null) { if (edges.Length == 0 || nodes.Length == 0) { return edges; } var restrictedSet = restrictedEdgeIds is null ? null : restrictedEdgeIds.ToHashSet(StringComparer.Ordinal); var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal); var graphMinY = nodes.Min(node => node.Y); var graphMaxY = nodes.Max(node => node.Y + node.Height); var minLineClearance = ResolveMinLineClearance(nodes); var result = edges.ToArray(); for (var i = 0; i < result.Length; i++) { var edge = result[i]; if (restrictedSet is not null && !restrictedSet.Contains(edge.Id)) { continue; } if (!string.IsNullOrWhiteSpace(edge.SourcePortId) || !string.IsNullOrWhiteSpace(edge.TargetPortId) || !nodesById.TryGetValue(edge.SourceNodeId ?? string.Empty, out var sourceNode) || !nodesById.TryGetValue(edge.TargetNodeId ?? string.Empty, out var targetNode)) { continue; } if (!string.IsNullOrWhiteSpace(edge.Kind) && edge.Kind.StartsWith("backward|", StringComparison.OrdinalIgnoreCase)) { continue; } if (HasProtectedUnderNodeGeometry(edge)) { continue; } if (IsRepeatCollectorLabel(edge.Label) && HasCorridorBendPoints(edge, graphMinY, graphMaxY)) { continue; } var path = ExtractFullPath(edge); if (path.Count < 2) { continue; } List? bestShortcut = null; var currentLength = ComputePathLength(path); bool IsAcceptableShortcutCandidate(IReadOnlyList candidate) { if (candidate.Count < 2 || HasNodeObstacleCrossing(candidate, nodes, edge.SourceNodeId, edge.TargetNodeId)) { return false; } if (ElkShapeBoundaries.IsGatewayShape(sourceNode)) { if (!HasAcceptableGatewayBoundaryPath( candidate, nodes, edge.SourceNodeId, edge.TargetNodeId, sourceNode, fromStart: true)) { return false; } } else if (!HasValidBoundaryAngle(candidate[0], candidate[1], sourceNode)) { return false; } if (ElkShapeBoundaries.IsGatewayShape(targetNode)) { return CanAcceptGatewayTargetRepair(candidate, targetNode) && HasAcceptableGatewayBoundaryPath( candidate, nodes, edge.SourceNodeId, edge.TargetNodeId, targetNode, fromStart: false); } return !HasTargetApproachBacktracking(candidate, targetNode) && HasValidBoundaryAngle(candidate[^1], candidate[^2], targetNode); } void ConsiderShortcutCandidate(List? candidate) { if (candidate is null || ComputePathLength(candidate) + 16d >= currentLength || !IsAcceptableShortcutCandidate(candidate)) { return; } if (bestShortcut is not null && ComputePathLength(candidate) + 0.5d >= ComputePathLength(bestShortcut)) { return; } bestShortcut = candidate; } if (TryBuildDominantPreferredBoundaryShortcutPath( sourceNode, targetNode, nodes, edge.SourceNodeId, edge.TargetNodeId, out var dominantShortcut)) { ConsiderShortcutCandidate(dominantShortcut); } if (TryBuildPreferredBoundaryShortcutPath( sourceNode, targetNode, nodes, edge.SourceNodeId, edge.TargetNodeId, out var preferredShortcut)) { ConsiderShortcutCandidate(preferredShortcut); } var localSkirtShortcut = TryBuildLocalObstacleSkirtBoundaryShortcut( path, path[0], path[^1], nodes, edge.SourceNodeId, edge.TargetNodeId, targetNode, minLineClearance); ConsiderShortcutCandidate(localSkirtShortcut); if (bestShortcut is null) { continue; } result[i] = BuildSingleSectionEdge(edge, bestShortcut); } return result; } private static bool TryBuildDominantPreferredBoundaryShortcutPath( ElkPositionedNode sourceNode, ElkPositionedNode targetNode, IReadOnlyCollection nodes, string? sourceNodeId, string? targetNodeId, out List shortcut) { shortcut = []; var sourceCenterX = sourceNode.X + (sourceNode.Width / 2d); var sourceCenterY = sourceNode.Y + (sourceNode.Height / 2d); var targetCenterX = targetNode.X + (targetNode.Width / 2d); var targetCenterY = targetNode.Y + (targetNode.Height / 2d); var deltaX = targetCenterX - sourceCenterX; var deltaY = targetCenterY - sourceCenterY; var absDx = Math.Abs(deltaX); var absDy = Math.Abs(deltaY); if (absDx < 16d && absDy < 16d) { return false; } var horizontalDominant = absDx >= absDy; var preferredSourceSide = horizontalDominant ? deltaX >= 0d ? "right" : "left" : deltaY >= 0d ? "bottom" : "top"; var preferredTargetSide = horizontalDominant ? deltaX >= 0d ? "left" : "right" : deltaY >= 0d ? "top" : "bottom"; return TryBuildPreferredBoundaryShortcutPath( sourceNode, targetNode, preferredSourceSide, preferredTargetSide, nodes, sourceNodeId, targetNodeId, out shortcut); } internal static bool TryBuildPreferredBoundaryShortcutPath( ElkPositionedNode sourceNode, ElkPositionedNode targetNode, IReadOnlyCollection nodes, string? sourceNodeId, string? targetNodeId, out List shortcut) { shortcut = []; var sourceCenterX = sourceNode.X + (sourceNode.Width / 2d); var sourceCenterY = sourceNode.Y + (sourceNode.Height / 2d); var targetCenterX = targetNode.X + (targetNode.Width / 2d); var targetCenterY = targetNode.Y + (targetNode.Height / 2d); var deltaX = targetCenterX - sourceCenterX; var deltaY = targetCenterY - sourceCenterY; var absDx = Math.Abs(deltaX); var absDy = Math.Abs(deltaY); if (absDx < 16d && absDy < 16d) { return false; } var horizontalDominant = absDx >= absDy; var preferredSourceSide = horizontalDominant ? deltaX >= 0d ? "right" : "left" : deltaY >= 0d ? "bottom" : "top"; var preferredTargetSide = horizontalDominant ? deltaX >= 0d ? "left" : "right" : deltaY >= 0d ? "top" : "bottom"; if (!ElkShapeBoundaries.IsGatewayShape(sourceNode) && !ElkShapeBoundaries.IsGatewayShape(targetNode)) { return TryBuildPreferredBoundaryShortcutPath( sourceNode, targetNode, preferredSourceSide, preferredTargetSide, nodes, sourceNodeId, targetNodeId, out shortcut); } var minLineClearance = ResolveMinLineClearance(nodes); var bestScore = double.PositiveInfinity; foreach (var candidateTargetSide in EnumeratePreferredShortcutTargetSides(preferredTargetSide)) { if (!TryBuildPreferredBoundaryShortcutPath( sourceNode, targetNode, preferredSourceSide, candidateTargetSide, nodes, sourceNodeId, targetNodeId, out var candidate)) { continue; } var score = (CountUnderNodeSegments(candidate, nodes, sourceNodeId, targetNodeId, minLineClearance) * 100_000d) + ComputePathLength(candidate) + (Math.Max(0, candidate.Count - 2) * 4d); if (!string.Equals(candidateTargetSide, preferredTargetSide, StringComparison.Ordinal)) { score += 40d; } if (score >= bestScore) { continue; } bestScore = score; shortcut = candidate; } return shortcut.Count > 0; } internal static bool TryBuildPreferredBoundaryShortcutPath( ElkPositionedNode sourceNode, ElkPositionedNode targetNode, string preferredSourceSide, string preferredTargetSide, IReadOnlyCollection nodes, string? sourceNodeId, string? targetNodeId, out List shortcut) { shortcut = []; var start = BuildPreferredShortcutBoundaryPoint(sourceNode, preferredSourceSide, targetNode); var end = BuildPreferredShortcutBoundaryPoint(targetNode, preferredTargetSide, sourceNode); bool SegmentIsClear(ElkPoint from, ElkPoint to) { 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(); return !SegmentCrossesObstacle(from, to, obstacles, sourceNodeId, targetNodeId); } var prefix = new List { start }; var routeStart = start; if (ElkShapeBoundaries.IsGatewayShape(sourceNode)) { var sourceExterior = ElkShapeBoundaries.BuildPreferredGatewaySourceExteriorPoint(sourceNode, start, end); if (ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(sourceNode, sourceExterior) || !ElkShapeBoundaries.HasValidGatewayBoundaryAngle(sourceNode, start, sourceExterior)) { return false; } if (!SegmentIsClear(start, sourceExterior)) { return false; } prefix.Add(sourceExterior); routeStart = sourceExterior; } var routeEnd = end; if (ElkShapeBoundaries.IsGatewayShape(targetNode)) { var targetExterior = ElkShapeBoundaries.BuildGatewayDirectionalExteriorPoint(targetNode, end, routeStart); if (ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(targetNode, targetExterior) || !ElkShapeBoundaries.HasValidGatewayBoundaryAngle(targetNode, end, targetExterior)) { return false; } routeEnd = targetExterior; } List? bridge = null; if (Math.Abs(routeStart.X - routeEnd.X) <= 0.5d || Math.Abs(routeStart.Y - routeEnd.Y) <= 0.5d) { if (SegmentIsClear(routeStart, routeEnd)) { bridge = [ routeStart, routeEnd, ]; } } if (bridge is null) { var pivots = new[] { new ElkPoint { X = routeEnd.X, Y = routeStart.Y }, new ElkPoint { X = routeStart.X, Y = routeEnd.Y }, }; foreach (var pivot in pivots) { if (!SegmentIsClear(routeStart, pivot) || !SegmentIsClear(pivot, routeEnd)) { continue; } bridge = [ routeStart, pivot, routeEnd, ]; break; } } if (bridge is null) { return false; } var candidate = prefix .Select(point => new ElkPoint { X = point.X, Y = point.Y }) .ToList(); foreach (var point in bridge.Skip(1)) { if (!ElkEdgeRoutingGeometry.PointsEqual(candidate[^1], point)) { candidate.Add(new ElkPoint { X = point.X, Y = point.Y }); } } if (ElkShapeBoundaries.IsGatewayShape(targetNode) && !ElkEdgeRoutingGeometry.PointsEqual(candidate[^1], end)) { candidate.Add(new ElkPoint { X = end.X, Y = end.Y }); } shortcut = NormalizePathPoints(candidate); if (shortcut.Count < 2) { shortcut = []; return false; } if (ElkShapeBoundaries.IsGatewayShape(sourceNode)) { if (!HasAcceptableGatewayBoundaryPath(shortcut, nodes, sourceNodeId, targetNodeId, sourceNode, fromStart: true)) { shortcut = []; return false; } } else if (!HasValidBoundaryAngle(shortcut[0], shortcut[1], sourceNode)) { shortcut = []; return false; } if (ElkShapeBoundaries.IsGatewayShape(targetNode)) { if (!CanAcceptGatewayTargetRepair(shortcut, targetNode) || !HasAcceptableGatewayBoundaryPath(shortcut, nodes, sourceNodeId, targetNodeId, targetNode, fromStart: false)) { shortcut = []; return false; } } else if (HasTargetApproachBacktracking(shortcut, targetNode) || !HasValidBoundaryAngle(shortcut[^1], shortcut[^2], targetNode)) { shortcut = []; return false; } return true; } private static ElkPoint BuildPreferredShortcutBoundaryPoint( ElkPositionedNode node, string side, ElkPositionedNode otherNode) { var horizontalInset = Math.Min(24d, Math.Max(12d, node.Width / 4d)); var verticalInset = Math.Min(24d, Math.Max(12d, node.Height / 4d)); var otherCenterX = otherNode.X + (otherNode.Width / 2d); var otherCenterY = otherNode.Y + (otherNode.Height / 2d); var boundary = side switch { "left" => new ElkPoint { X = node.X, Y = Math.Clamp(otherCenterY, node.Y + verticalInset, (node.Y + node.Height) - verticalInset), }, "right" => new ElkPoint { X = node.X + node.Width, Y = Math.Clamp(otherCenterY, node.Y + verticalInset, (node.Y + node.Height) - verticalInset), }, "top" => new ElkPoint { X = Math.Clamp(otherCenterX, node.X + horizontalInset, (node.X + node.Width) - horizontalInset), Y = node.Y, }, _ => new ElkPoint { X = Math.Clamp(otherCenterX, node.X + horizontalInset, (node.X + node.Width) - horizontalInset), Y = node.Y + node.Height, }, }; if (!ElkShapeBoundaries.IsGatewayShape(node)) { return boundary; } var referencePoint = side switch { "left" => new ElkPoint { X = node.X - Math.Max(24d, node.Width / 3d), Y = boundary.Y }, "right" => new ElkPoint { X = node.X + node.Width + Math.Max(24d, node.Width / 3d), Y = boundary.Y }, "top" => new ElkPoint { X = boundary.X, Y = node.Y - Math.Max(24d, node.Height / 3d) }, _ => new ElkPoint { X = boundary.X, Y = node.Y + node.Height + Math.Max(24d, node.Height / 3d) }, }; var projected = ElkShapeBoundaries.ProjectOntoShapeBoundary(node, referencePoint); return ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(node, projected, referencePoint); } private static IEnumerable EnumeratePreferredShortcutTargetSides(string preferredTargetSide) { var seen = new HashSet(StringComparer.Ordinal); bool Add(string side) { return side is "left" or "right" or "top" or "bottom" && seen.Add(side); } if (Add(preferredTargetSide)) { yield return preferredTargetSide; } if (preferredTargetSide is "left" or "right") { if (Add("top")) { yield return "top"; } if (Add("bottom")) { yield return "bottom"; } var oppositeHorizontal = string.Equals(preferredTargetSide, "left", StringComparison.Ordinal) ? "right" : "left"; if (Add(oppositeHorizontal)) { yield return oppositeHorizontal; } yield break; } if (Add("left")) { yield return "left"; } if (Add("right")) { yield return "right"; } var oppositeVertical = string.Equals(preferredTargetSide, "top", StringComparison.Ordinal) ? "bottom" : "top"; if (Add(oppositeVertical)) { yield return oppositeVertical; } } internal static ElkRoutedEdge[] RepairBoundaryAnglesAndTargetApproaches( ElkRoutedEdge[] edges, ElkPositionedNode[] nodes, double minLineClearance, IReadOnlyCollection? restrictedEdgeIds = null) { if (edges.Length == 0 || nodes.Length == 0) { return edges; } var restrictedSet = restrictedEdgeIds is null ? null : restrictedEdgeIds.ToHashSet(StringComparer.Ordinal); var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal); var graphMinY = nodes.Min(node => node.Y); var graphMaxY = nodes.Max(node => node.Y + node.Height); var targetSlots = ResolveTargetApproachSlots(edges, nodesById, graphMinY, graphMaxY, minLineClearance, restrictedSet); var result = new ElkRoutedEdge[edges.Length]; for (var i = 0; i < edges.Length; i++) { var edge = edges[i]; if (restrictedSet is not null && !restrictedSet.Contains(edge.Id)) { result[i] = edge; continue; } var path = ExtractFullPath(edge); if (path.Count < 2) { result[i] = edge; continue; } var normalized = path .Select(point => new ElkPoint { X = point.X, Y = point.Y }) .ToList(); if (nodesById.TryGetValue(edge.SourceNodeId ?? string.Empty, out var sourceNode) && ElkShapeBoundaries.IsGatewayShape(sourceNode)) { var preserveSourceExit = ShouldPreserveSourceExitGeometry(edge, graphMinY, graphMaxY); if (!preserveSourceExit) { var gatewaySourceNormalized = NormalizeGatewayExitPath(normalized, sourceNode, nodes, edge.SourceNodeId, edge.TargetNodeId); if (PathStartsAtDecisionVertex(gatewaySourceNormalized, sourceNode)) { gatewaySourceNormalized = ForceDecisionSourceExitOffVertex( gatewaySourceNormalized, sourceNode, nodes, edge.SourceNodeId, edge.TargetNodeId); } if (PathChanged(normalized, gatewaySourceNormalized) && HasAcceptableGatewayBoundaryPath(gatewaySourceNormalized, nodes, edge.SourceNodeId, edge.TargetNodeId, sourceNode, fromStart: true)) { normalized = gatewaySourceNormalized; } } } else if (nodesById.TryGetValue(edge.SourceNodeId ?? string.Empty, out sourceNode) && !HasValidBoundaryAngle(normalized[0], normalized[1], sourceNode)) { var sourceSide = ResolvePreferredRectSourceExitSide(normalized, sourceNode); var sourcePath = normalized .Select(point => new ElkPoint { X = point.X, Y = point.Y }) .ToList(); sourcePath[0] = BuildRectBoundaryPointForSide(sourceNode, sourceSide, sourcePath[1]); var sourceNormalized = NormalizeExitPath(sourcePath, sourceNode, sourceSide); if (HasClearBoundarySegments(sourceNormalized, nodes, edge.SourceNodeId, edge.TargetNodeId, true, 3)) { normalized = sourceNormalized; } } if (nodesById.TryGetValue(edge.TargetNodeId ?? string.Empty, out var targetNode)) { var assignedEndpoint = targetSlots.TryGetValue(edge.Id, out var slot) ? slot : normalized[^1]; if (ElkShapeBoundaries.IsGatewayShape(targetNode)) { List? preferredGatewayTargetNormalized = null; if (nodesById.TryGetValue(edge.SourceNodeId ?? string.Empty, out var gatewaySourceNode) && TryBuildPreferredGatewayTargetEntryPath( normalized, gatewaySourceNode, targetNode, nodes, edge.SourceNodeId, edge.TargetNodeId, out var preferredGatewayTargetRepair)) { preferredGatewayTargetNormalized = preferredGatewayTargetRepair; } var gatewayTargetNormalized = NormalizeGatewayEntryPath(normalized, targetNode, assignedEndpoint); if (gatewayTargetNormalized.Count >= 2 && !ElkShapeBoundaries.HasValidGatewayBoundaryAngle(targetNode, gatewayTargetNormalized[^1], gatewayTargetNormalized[^2])) { var projectedBoundary = ElkShapeBoundaries.ProjectOntoShapeBoundary(targetNode, gatewayTargetNormalized[^2]); projectedBoundary = ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(targetNode, projectedBoundary, gatewayTargetNormalized[^2]); var projectedGatewayTargetNormalized = NormalizeGatewayEntryPath(normalized, targetNode, projectedBoundary); if (PathChanged(gatewayTargetNormalized, projectedGatewayTargetNormalized) && HasAcceptableGatewayBoundaryPath(projectedGatewayTargetNormalized, nodes, edge.SourceNodeId, edge.TargetNodeId, targetNode, fromStart: false)) { gatewayTargetNormalized = projectedGatewayTargetNormalized; } } if (preferredGatewayTargetNormalized is not null && (gatewayTargetNormalized.Count < 2 || NeedsGatewayTargetBoundaryRepair(gatewayTargetNormalized, targetNode) || !string.Equals( ElkEdgeRoutingGeometry.ResolveBoundarySide(gatewayTargetNormalized[^1], targetNode), ElkEdgeRoutingGeometry.ResolveBoundarySide(preferredGatewayTargetNormalized[^1], targetNode), StringComparison.Ordinal) || ComputePathLength(preferredGatewayTargetNormalized) + 4d < ComputePathLength(gatewayTargetNormalized) || HasTargetApproachBacktracking(gatewayTargetNormalized, targetNode))) { gatewayTargetNormalized = preferredGatewayTargetNormalized; } if (PathChanged(normalized, gatewayTargetNormalized) && HasAcceptableGatewayBoundaryPath(gatewayTargetNormalized, nodes, edge.SourceNodeId, edge.TargetNodeId, targetNode, fromStart: false)) { normalized = gatewayTargetNormalized; } } else { if (nodesById.TryGetValue(edge.SourceNodeId ?? string.Empty, out var shortcutSourceNode) && TryApplyPreferredBoundaryShortcut( normalized, shortcutSourceNode, targetNode, nodes, edge.SourceNodeId, edge.TargetNodeId, requireUnderNodeImprovement: false, minLineClearance, out var preferredShortcut)) { normalized = preferredShortcut; } var targetSide = ElkEdgeRoutingGeometry.ResolveBoundarySide(assignedEndpoint, targetNode); if (IsOnWrongSideOfTarget(normalized[^2], targetNode, targetSide, 0.5d) && TryResolveNonGatewayBacktrackingEndpoint(normalized, targetNode, out var correctedSide, out var correctedBoundary)) { targetSide = correctedSide; assignedEndpoint = correctedBoundary; } if (HasTargetApproachBacktracking(normalized, targetNode) && TryResolveNonGatewayBacktrackingEndpoint(normalized, targetNode, out var preferredSide, out var preferredBoundary)) { targetSide = preferredSide; assignedEndpoint = preferredBoundary; } if (!HasValidBoundaryAngle(normalized[^1], normalized[^2], targetNode)) { var preferredEntrySide = ResolvePreferredRectTargetEntrySide(normalized, targetNode); if (!string.Equals(preferredEntrySide, targetSide, StringComparison.Ordinal)) { targetSide = preferredEntrySide; assignedEndpoint = BuildRectBoundaryPointForSide(targetNode, targetSide, normalized[^2]); } } if (!ElkEdgeRoutingGeometry.PointsEqual(assignedEndpoint, normalized[^1]) || !HasValidBoundaryAngle(normalized[^1], normalized[^2], targetNode)) { var targetNormalized = NormalizeEntryPath(normalized, targetNode, targetSide, assignedEndpoint); if (HasClearBoundarySegments(targetNormalized, nodes, edge.SourceNodeId, edge.TargetNodeId, false, 3)) { normalized = targetNormalized; } } var shortenedApproach = TrimTargetApproachBacktracking(normalized, targetNode, targetSide, assignedEndpoint); if (PathChanged(normalized, shortenedApproach) && HasClearBoundarySegments(shortenedApproach, nodes, edge.SourceNodeId, edge.TargetNodeId, false, 3) && HasValidBoundaryAngle(shortenedApproach[^1], shortenedApproach[^2], targetNode)) { normalized = shortenedApproach; } if (HasTargetApproachBacktracking(normalized, targetNode) && TryNormalizeNonGatewayBacktrackingEntry(normalized, targetNode, out var backtrackingRepair) && HasClearBoundarySegments(backtrackingRepair, nodes, edge.SourceNodeId, edge.TargetNodeId, false, 3) && HasValidBoundaryAngle(backtrackingRepair[^1], backtrackingRepair[^2], targetNode)) { normalized = backtrackingRepair; } } } if (normalized.Count == path.Count && normalized.Zip(path, ElkEdgeRoutingGeometry.PointsEqual).All(equal => equal)) { result[i] = edge; continue; } result[i] = BuildSingleSectionEdge(edge, normalized); } return result; } internal static ElkRoutedEdge[] SpreadTargetApproachJoins( ElkRoutedEdge[] edges, ElkPositionedNode[] nodes, double minLineClearance, IReadOnlyCollection? restrictedEdgeIds = null, bool forceOutwardAxisSpacing = false) { if (edges.Length == 0 || nodes.Length == 0) { return edges; } var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal); var graphMinY = nodes.Min(node => node.Y); var graphMaxY = nodes.Max(node => node.Y + node.Height); var restrictedSet = restrictedEdgeIds is null ? null : restrictedEdgeIds.ToHashSet(StringComparer.Ordinal); var result = edges.ToArray(); var groups = result .Select((edge, index) => new { Edge = edge, Index = index, Path = ExtractFullPath(edge), }) .Where(item => item.Path.Count >= 2 && nodesById.TryGetValue(item.Edge.TargetNodeId ?? string.Empty, out _)) .GroupBy( item => { var targetNode = nodesById[item.Edge.TargetNodeId ?? string.Empty]; var side = ResolveTargetApproachSide(item.Path, targetNode); return $"{targetNode.Id}|{side}"; }, StringComparer.Ordinal); foreach (var group in groups) { var entries = group .Select(item => { var targetNode = nodesById[item.Edge.TargetNodeId ?? string.Empty]; var side = ResolveTargetApproachSide(item.Path, targetNode); var endpoint = item.Path[^1]; return new { item.Edge, item.Index, item.Path, TargetNode = targetNode, Side = side, Endpoint = endpoint, }; }) .ToArray(); if (entries.Length < 2) { continue; } if (restrictedSet is not null && !entries.Any(entry => restrictedSet.Contains(entry.Edge.Id))) { continue; } var joinEntries = entries .Select(entry => (Path: (IReadOnlyList)entry.Path, Side: entry.Side)) .ToArray(); if (!GroupHasTargetApproachJoin(joinEntries, minLineClearance)) { continue; } var targetNode = entries[0].TargetNode; var side = entries[0].Side; var isGatewayTarget = ElkShapeBoundaries.IsGatewayShape(targetNode); var preserveBoundaryBandSlots = entries.Any(entry => HasProtectedUnderNodeGeometry(entry.Edge) || HasCorridorBendPoints(entry.Edge, graphMinY, graphMaxY)); var sorted = side is "left" or "right" ? entries.OrderBy(entry => entry.Endpoint.Y).ThenBy(entry => entry.Edge.Id, StringComparer.Ordinal).ToArray() : entries.OrderBy(entry => entry.Endpoint.X).ThenBy(entry => entry.Edge.Id, StringComparer.Ordinal).ToArray(); var sideLength = side is "left" or "right" ? Math.Max(8d, targetNode.Height - 8d) : Math.Max(8d, targetNode.Width - 8d); var slotSpacing = sorted.Length > 1 ? ResolveBoundaryJoinSlotSpacing(minLineClearance, sideLength, sorted.Length) : 0d; var totalSpan = (sorted.Length - 1) * slotSpacing; var baseBoundaryCoordinate = side switch { "left" or "right" => entries.Min(entry => entry.Endpoint.Y), "top" or "bottom" => entries.Min(entry => entry.Endpoint.X), _ => double.NaN, }; var currentApproachAxes = sorted .Select(entry => ResolveSpreadableTargetApproachAxis( entry.Path, targetNode, entry.Side, minLineClearance)) .Where(axis => !double.IsNaN(axis)) .ToArray(); var baseApproachAxis = isGatewayTarget ? ResolveDefaultTargetApproachAxis(targetNode, side) : currentApproachAxes.Length > 0 ? forceOutwardAxisSpacing ? side switch { "left" or "top" => currentApproachAxes.Max(), "right" or "bottom" => currentApproachAxes.Min(), _ => ResolveDefaultTargetApproachAxis(targetNode, side), } : currentApproachAxes.Min() : ResolveDefaultTargetApproachAxis(targetNode, side); for (var i = 0; i < sorted.Length; i++) { var desiredBoundaryCoordinate = baseBoundaryCoordinate + (i * slotSpacing); var desiredApproachAxis = ResolveDesiredTargetApproachAxis( targetNode, side, baseApproachAxis, slotSpacing, i, forceOutwardAxisSpacing); if (isGatewayTarget) { ElkPoint slotPoint; if (side is "left" or "right") { var centerY = targetNode.Y + (targetNode.Height / 2d); var startY = Math.Max(targetNode.Y + 4d, centerY - (totalSpan / 2d)); var slotY = Math.Min(targetNode.Y + targetNode.Height - 4d, startY + (i * slotSpacing)); if (!ElkShapeBoundaries.TryProjectGatewayBoundarySlot(targetNode, side, slotY, out slotPoint)) { continue; } } else { var centerX = targetNode.X + (targetNode.Width / 2d); var startX = Math.Max(targetNode.X + 4d, centerX - (totalSpan / 2d)); var slotX = Math.Min(targetNode.X + targetNode.Width - 4d, startX + (i * slotSpacing)); if (!ElkShapeBoundaries.TryProjectGatewayBoundarySlot(targetNode, side, slotX, out slotPoint)) { continue; } } var exteriorIndex = FindLastGatewayExteriorPointIndex(sorted[i].Path, targetNode); var exteriorAnchor = sorted[i].Path[exteriorIndex]; var gatewayCandidate = TryBuildSlottedGatewayEntryPath( sorted[i].Path, targetNode, exteriorIndex, exteriorAnchor, slotPoint) ?? NormalizeGatewayEntryPath(sorted[i].Path, targetNode, slotPoint); var gatewayApproachAxis = double.IsNaN(desiredApproachAxis) ? ResolveTargetApproachAxisValue(gatewayCandidate, sorted[i].Side) : desiredApproachAxis; if (double.IsNaN(gatewayApproachAxis)) { gatewayApproachAxis = ResolveDefaultTargetApproachAxis(targetNode, side); } var spreadGatewayCandidate = RewriteTargetApproachRun( gatewayCandidate, sorted[i].Side, slotPoint, gatewayApproachAxis); if (PathChanged(gatewayCandidate, spreadGatewayCandidate) && CanAcceptGatewayTargetRepair(spreadGatewayCandidate, targetNode)) { gatewayCandidate = spreadGatewayCandidate; } if (!PathChanged(sorted[i].Path, gatewayCandidate) || !CanAcceptGatewayTargetRepair(gatewayCandidate, targetNode)) { continue; } result[sorted[i].Index] = BuildSingleSectionEdge(sorted[i].Edge, gatewayCandidate); continue; } ElkPoint desiredEndpoint; if (side is "left" or "right") { var centerY = targetNode.Y + (targetNode.Height / 2d); var startY = Math.Max(targetNode.Y + 4d, centerY - (totalSpan / 2d)); var slotY = preserveBoundaryBandSlots && !double.IsNaN(desiredBoundaryCoordinate) ? Math.Clamp(desiredBoundaryCoordinate, targetNode.Y + 4d, targetNode.Y + targetNode.Height - 4d) : Math.Min(targetNode.Y + targetNode.Height - 4d, startY + (i * slotSpacing)); desiredEndpoint = new ElkPoint { X = side == "left" ? targetNode.X : targetNode.X + targetNode.Width, Y = slotY, }; } else { var centerX = targetNode.X + (targetNode.Width / 2d); var startX = Math.Max(targetNode.X + 4d, centerX - (totalSpan / 2d)); var slotX = preserveBoundaryBandSlots && !double.IsNaN(desiredBoundaryCoordinate) ? Math.Clamp(desiredBoundaryCoordinate, targetNode.X + 4d, targetNode.X + targetNode.Width - 4d) : Math.Min(targetNode.X + targetNode.Width - 4d, startX + (i * slotSpacing)); desiredEndpoint = new ElkPoint { X = slotX, Y = side == "top" ? targetNode.Y : targetNode.Y + targetNode.Height, }; } var currentRunAxis = ResolveTargetApproachAxisValue(sorted[i].Path, sorted[i].Side); var preserveApproachBand = HasProtectedUnderNodeGeometry(sorted[i].Edge) || HasCorridorBendPoints(sorted[i].Edge, graphMinY, graphMaxY); var desiredRunAxis = preserveApproachBand ? currentRunAxis : double.IsNaN(desiredApproachAxis) ? currentRunAxis : desiredApproachAxis; if (double.IsNaN(desiredRunAxis)) { desiredRunAxis = double.IsNaN(desiredApproachAxis) ? ResolveDefaultTargetApproachAxis(targetNode, side) : desiredApproachAxis; } var candidate = RewriteTargetApproachRun( sorted[i].Path, sorted[i].Side, desiredEndpoint, desiredRunAxis); if (!PathChanged(sorted[i].Path, candidate) || !HasClearBoundarySegments(candidate, nodes, sorted[i].Edge.SourceNodeId, sorted[i].Edge.TargetNodeId, false, 4)) { continue; } result[sorted[i].Index] = BuildSingleSectionEdge(sorted[i].Edge, candidate); } } return result; } internal static ElkRoutedEdge[] SpreadSourceDepartureJoins( ElkRoutedEdge[] edges, ElkPositionedNode[] nodes, double minLineClearance, IReadOnlyCollection? restrictedEdgeIds = null) { if (edges.Length == 0 || nodes.Length == 0) { return edges; } var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal); var graphMinY = nodes.Min(node => node.Y); var graphMaxY = nodes.Max(node => node.Y + node.Height); var restrictedSet = restrictedEdgeIds is null ? null : restrictedEdgeIds.ToHashSet(StringComparer.Ordinal); var result = edges.ToArray(); var groups = result .Select((edge, index) => new { Edge = edge, Index = index, Path = ExtractFullPath(edge), }) .Where(item => item.Path.Count >= 2 && nodesById.TryGetValue(item.Edge.SourceNodeId ?? string.Empty, out _) && ShouldSpreadSourceDeparture(item.Edge, graphMinY, graphMaxY)) .GroupBy( item => { var sourceNode = nodesById[item.Edge.SourceNodeId ?? string.Empty]; var side = ResolveSourceDepartureSide(item.Path, sourceNode); return $"{sourceNode.Id}|{side}"; }, StringComparer.Ordinal); foreach (var group in groups) { var entries = group .Select(item => { var sourceNode = nodesById[item.Edge.SourceNodeId ?? string.Empty]; var side = ResolveSourceDepartureSide(item.Path, sourceNode); return new { item.Edge, item.Index, item.Path, SourceNode = sourceNode, Side = side, Boundary = item.Path[0], TargetReference = side is "left" or "right" ? item.Path[^1].Y : item.Path[^1].X, PathLength = ComputePathLength(item.Path), }; }) .ToArray(); if (entries.Length < 2) { continue; } if (restrictedSet is not null && !entries.Any(entry => restrictedSet.Contains(entry.Edge.Id))) { continue; } var joinEntries = entries .Select(entry => (Path: (IReadOnlyList)entry.Path, Side: entry.Side)) .ToArray(); if (!GroupHasSourceDepartureJoin(joinEntries, minLineClearance)) { continue; } var sourceNode = entries[0].SourceNode; var side = entries[0].Side; var isGatewaySource = ElkShapeBoundaries.IsGatewayShape(sourceNode); var boundaryCoordinate = side is "left" or "right" ? entries[0].Boundary.Y : entries[0].Boundary.X; var anchor = entries .OrderBy(entry => Math.Abs(entry.TargetReference - boundaryCoordinate)) .ThenBy(entry => entry.PathLength) .ThenBy(entry => entry.Edge.Id, StringComparer.Ordinal) .First(); var sideLength = side is "left" or "right" ? Math.Max(8d, sourceNode.Height - 8d) : Math.Max(8d, sourceNode.Width - 8d); var slotSpacing = entries.Length > 1 ? Math.Max(12d, Math.Min(minLineClearance, sideLength / entries.Length)) : 0d; var minSlot = side is "left" or "right" ? sourceNode.Y + 4d : sourceNode.X + 4d; var maxSlot = side is "left" or "right" ? sourceNode.Y + sourceNode.Height - 4d : sourceNode.X + sourceNode.Width - 4d; var negativeEntries = entries .Where(entry => !ReferenceEquals(entry, anchor) && entry.TargetReference < anchor.TargetReference) .OrderByDescending(entry => entry.TargetReference) .ThenBy(entry => entry.PathLength) .ThenBy(entry => entry.Edge.Id, StringComparer.Ordinal) .ToArray(); var positiveEntries = entries .Where(entry => !ReferenceEquals(entry, anchor) && entry.TargetReference >= anchor.TargetReference) .OrderBy(entry => entry.TargetReference) .ThenBy(entry => entry.PathLength) .ThenBy(entry => entry.Edge.Id, StringComparer.Ordinal) .ToArray(); var desiredCoordinateByEdgeId = new Dictionary(StringComparer.Ordinal) { [anchor.Edge.Id] = boundaryCoordinate, }; var anchorDepartureAxis = TryExtractSourceDepartureRun(anchor.Path, side, out _, out var anchorRunEndIndex) ? side is "left" or "right" ? anchor.Path[anchorRunEndIndex].X : anchor.Path[anchorRunEndIndex].Y : side switch { "left" => sourceNode.X - 24d, "right" => sourceNode.X + sourceNode.Width + 24d, "top" => sourceNode.Y - 24d, "bottom" => sourceNode.Y + sourceNode.Height + 24d, _ => 0d, }; var axisStep = Math.Max(12d, minLineClearance * 0.5d); var desiredAxisByEdgeId = new Dictionary(StringComparer.Ordinal) { [anchor.Edge.Id] = anchorDepartureAxis, }; for (var i = 0; i < negativeEntries.Length; i++) { desiredCoordinateByEdgeId[negativeEntries[i].Edge.Id] = Math.Max(minSlot, boundaryCoordinate - ((i + 1) * slotSpacing)); desiredAxisByEdgeId[negativeEntries[i].Edge.Id] = anchorDepartureAxis; } for (var i = 0; i < positiveEntries.Length; i++) { desiredCoordinateByEdgeId[positiveEntries[i].Edge.Id] = Math.Min(maxSlot, boundaryCoordinate + ((i + 1) * slotSpacing)); desiredAxisByEdgeId[positiveEntries[i].Edge.Id] = anchorDepartureAxis; } foreach (var entry in entries) { if (!desiredCoordinateByEdgeId.TryGetValue(entry.Edge.Id, out var slotCoordinate)) { continue; } if (!desiredAxisByEdgeId.TryGetValue(entry.Edge.Id, out var desiredAxis)) { continue; } var originalCoordinate = side is "left" or "right" ? entry.Boundary.Y : entry.Boundary.X; var originalAxis = TryExtractSourceDepartureRun(entry.Path, side, out _, out var runEndIndex) ? side is "left" or "right" ? entry.Path[runEndIndex].X : entry.Path[runEndIndex].Y : desiredAxis; if (Math.Abs(originalCoordinate - slotCoordinate) <= 0.5d && Math.Abs(originalAxis - desiredAxis) <= 0.5d) { continue; } ElkPoint boundaryPoint; if (isGatewaySource) { if (!ElkShapeBoundaries.TryProjectGatewayBoundarySlot(sourceNode, side, slotCoordinate, out boundaryPoint)) { continue; } var continuation = entry.Path.Count > 1 ? entry.Path[1] : entry.Path[0]; boundaryPoint = ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(sourceNode, boundaryPoint, continuation); } else { boundaryPoint = side switch { "left" => new ElkPoint { X = sourceNode.X, Y = slotCoordinate }, "right" => new ElkPoint { X = sourceNode.X + sourceNode.Width, Y = slotCoordinate }, "top" => new ElkPoint { X = slotCoordinate, Y = sourceNode.Y }, "bottom" => new ElkPoint { X = slotCoordinate, Y = sourceNode.Y + sourceNode.Height }, _ => entry.Boundary, }; } var candidate = RewriteSourceDepartureRun(entry.Path, side, boundaryPoint, desiredAxis); if (!PathChanged(entry.Path, candidate)) { continue; } if (isGatewaySource) { if (!HasAcceptableGatewayBoundaryPath(candidate, nodes, entry.Edge.SourceNodeId, entry.Edge.TargetNodeId, sourceNode, fromStart: true)) { continue; } } else { if (!HasClearBoundarySegments(candidate, nodes, entry.Edge.SourceNodeId, entry.Edge.TargetNodeId, true, 2) || !HasValidBoundaryAngle(candidate[0], candidate[1], sourceNode) || HasNodeObstacleCrossing(candidate, nodes, entry.Edge.SourceNodeId, entry.Edge.TargetNodeId)) { continue; } } result[entry.Index] = BuildSingleSectionEdge(entry.Edge, candidate); } } return result; } internal static ElkRoutedEdge[] SpreadRectTargetApproachFeederBands( ElkRoutedEdge[] edges, ElkPositionedNode[] nodes, double minLineClearance, IReadOnlyCollection? restrictedEdgeIds = null) { if (edges.Length < 2 || nodes.Length == 0) { return edges; } var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal); var restrictedSet = restrictedEdgeIds is null ? null : restrictedEdgeIds.ToHashSet(StringComparer.Ordinal); var result = edges.ToArray(); var groups = result .Select((edge, index) => new { Edge = edge, Index = index, Path = ExtractFullPath(edge), }) .Where(item => item.Path.Count >= 3 && nodesById.TryGetValue(item.Edge.TargetNodeId ?? string.Empty, out var targetNode) && !ElkShapeBoundaries.IsGatewayShape(targetNode)) .GroupBy( item => { var targetNode = nodesById[item.Edge.TargetNodeId ?? string.Empty]; var side = ResolveTargetApproachSide(item.Path, targetNode); return $"{targetNode.Id}|{side}"; }, StringComparer.Ordinal); foreach (var group in groups) { var entries = group .Select(item => { var targetNode = nodesById[item.Edge.TargetNodeId ?? string.Empty]; var side = ResolveTargetApproachSide(item.Path, targetNode); return TryExtractTargetApproachFeeder(item.Path, side, out var feeder) ? new { item.Edge, item.Index, item.Path, TargetNode = targetNode, Side = side, Feeder = feeder, } : null; }) .Where(entry => entry is not null) .Select(entry => entry!) .ToArray(); if (entries.Length < 2) { continue; } if (restrictedSet is not null && !entries.Any(entry => restrictedSet.Contains(entry.Edge.Id))) { continue; } var hasConflict = false; for (var i = 0; i < entries.Length && !hasConflict; i++) { for (var j = i + 1; j < entries.Length; j++) { if (ElkEdgeRoutingGeometry.AreParallelAndClose( entries[i].Feeder.Start, entries[i].Feeder.End, entries[j].Feeder.Start, entries[j].Feeder.End, minLineClearance)) { hasConflict = true; break; } } } if (!hasConflict) { continue; } var spacing = Math.Max(12d, minLineClearance + 4d); var sorted = entries .OrderBy(entry => entry.Feeder.BandCoordinate) .ThenBy(entry => entry.Edge.Id, StringComparer.Ordinal) .ToArray(); var baseBand = sorted[0].Side is "left" or "top" ? sorted.Max(entry => entry.Feeder.BandCoordinate) : sorted.Min(entry => entry.Feeder.BandCoordinate); for (var i = 0; i < sorted.Length; i++) { var desiredBand = ResolveDesiredTargetApproachAxis( sorted[i].TargetNode, sorted[i].Side, baseBand, spacing, i, forceOutwardFromBoundary: true); if (Math.Abs(sorted[i].Feeder.BandCoordinate - desiredBand) <= 0.5d) { continue; } var candidate = RewriteTargetApproachFeederBand(sorted[i].Path, sorted[i].Side, desiredBand); if (!PathChanged(sorted[i].Path, candidate) || !HasClearBoundarySegments(candidate, nodes, sorted[i].Edge.SourceNodeId, sorted[i].Edge.TargetNodeId, false, 4) || HasNodeObstacleCrossing(candidate, nodes, sorted[i].Edge.SourceNodeId, sorted[i].Edge.TargetNodeId)) { continue; } result[sorted[i].Index] = BuildSingleSectionEdge(sorted[i].Edge, candidate); } } return result; } internal static ElkRoutedEdge[] SeparateMixedNodeFaceLaneConflicts( ElkRoutedEdge[] edges, ElkPositionedNode[] nodes, double minLineClearance, IReadOnlyCollection? restrictedEdgeIds = null) { if (edges.Length < 2 || nodes.Length == 0) { return edges; } var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal); var graphMinY = nodes.Min(node => node.Y); var graphMaxY = nodes.Max(node => node.Y + node.Height); var restrictedSet = restrictedEdgeIds is null ? null : restrictedEdgeIds.ToHashSet(StringComparer.Ordinal); var result = edges.ToArray(); var entries = new List<(int Index, ElkRoutedEdge Edge, IReadOnlyList Path, ElkPositionedNode Node, string Side, bool IsOutgoing, ElkPoint Boundary, double BoundaryCoordinate, double AxisValue)>(); for (var index = 0; index < result.Length; index++) { var edge = result[index]; var path = ExtractFullPath(edge); if (path.Count < 2) { continue; } if (nodesById.TryGetValue(edge.SourceNodeId ?? string.Empty, out var sourceNode) && ShouldSpreadSourceDeparture(edge, graphMinY, graphMaxY)) { var side = ResolveSourceDepartureSide(path, sourceNode); var axisValue = TryExtractSourceDepartureRun(path, side, out _, out var runEndIndex) ? side is "left" or "right" ? path[runEndIndex].X : path[runEndIndex].Y : ResolveDefaultSourceDepartureAxis(sourceNode, side); entries.Add(( index, edge, path, sourceNode, side, true, path[0], side is "left" or "right" ? path[0].Y : path[0].X, axisValue)); } if (nodesById.TryGetValue(edge.TargetNodeId ?? string.Empty, out var targetNode) && ShouldSpreadTargetApproach(edge, graphMinY, graphMaxY)) { var side = ResolveTargetApproachSide(path, targetNode); var axisValue = ResolveTargetApproachAxisValue(path, side); if (double.IsNaN(axisValue)) { axisValue = side is "left" or "right" ? path[^1].Y : path[^1].X; } entries.Add(( index, edge, path, targetNode, side, false, path[^1], side is "left" or "right" ? path[^1].Y : path[^1].X, axisValue)); } } foreach (var group in entries.GroupBy( entry => $"{entry.Node.Id}|{entry.Side}", StringComparer.Ordinal)) { var groupEntries = group.ToArray(); if (groupEntries.Length < 2 || !groupEntries.Any(entry => entry.IsOutgoing) || !groupEntries.Any(entry => !entry.IsOutgoing) || !GroupHasMixedNodeFaceLaneConflict(groupEntries, minLineClearance)) { continue; } if (restrictedSet is not null && !groupEntries.Any(entry => restrictedSet.Contains(entry.Edge.Id))) { continue; } var node = groupEntries[0].Node; var side = groupEntries[0].Side; var sideLength = side is "left" or "right" ? Math.Max(8d, node.Height - 8d) : Math.Max(8d, node.Width - 8d); var slotSpacing = groupEntries.Length > 1 ? groupEntries.Length == 2 ? Math.Max( 12d, Math.Min( ResolveBoundaryJoinSlotSpacing(minLineClearance, sideLength, groupEntries.Length), Math.Min(28d, minLineClearance * 0.45d))) : ResolveBoundaryJoinSlotSpacing(minLineClearance, sideLength, groupEntries.Length) : 0d; var centerCoordinate = side is "left" or "right" ? node.Y + (node.Height / 2d) : node.X + (node.Width / 2d); var anchor = groupEntries .OrderBy(entry => entry.IsOutgoing ? 0 : 1) .ThenBy(entry => IsRepeatCollectorLabel(entry.Edge.Label) ? 1 : 0) .ThenBy(entry => Math.Abs(entry.BoundaryCoordinate - centerCoordinate)) .ThenBy(entry => ComputePathLength(entry.Path)) .ThenBy(entry => entry.Edge.Id, StringComparer.Ordinal) .First(); var minSlot = side is "left" or "right" ? node.Y + 4d : node.X + 4d; var maxSlot = side is "left" or "right" ? node.Y + node.Height - 4d : node.X + node.Width - 4d; var negativeEntries = groupEntries .Where(entry => entry.Edge.Id != anchor.Edge.Id && entry.BoundaryCoordinate < anchor.BoundaryCoordinate) .OrderByDescending(entry => entry.BoundaryCoordinate) .ThenBy(entry => entry.IsOutgoing ? 0 : 1) .ThenBy(entry => entry.Edge.Id, StringComparer.Ordinal) .ToArray(); var positiveEntries = groupEntries .Where(entry => entry.Edge.Id != anchor.Edge.Id && entry.BoundaryCoordinate >= anchor.BoundaryCoordinate) .OrderBy(entry => entry.BoundaryCoordinate) .ThenBy(entry => entry.IsOutgoing ? 0 : 1) .ThenBy(entry => entry.Edge.Id, StringComparer.Ordinal) .ToArray(); var desiredCoordinateByEdgeId = new Dictionary(StringComparer.Ordinal) { [anchor.Edge.Id] = anchor.BoundaryCoordinate, }; for (var i = 0; i < negativeEntries.Length; i++) { desiredCoordinateByEdgeId[negativeEntries[i].Edge.Id] = Math.Max(minSlot, anchor.BoundaryCoordinate - ((i + 1) * slotSpacing)); } for (var i = 0; i < positiveEntries.Length; i++) { desiredCoordinateByEdgeId[positiveEntries[i].Edge.Id] = Math.Min(maxSlot, anchor.BoundaryCoordinate + ((i + 1) * slotSpacing)); } foreach (var entry in groupEntries) { if (!desiredCoordinateByEdgeId.TryGetValue(entry.Edge.Id, out var desiredCoordinate) || Math.Abs(desiredCoordinate - entry.BoundaryCoordinate) <= 0.5d) { continue; } var bestEdge = result[entry.Index]; var currentGroupEdges = groupEntries .Select(item => result[item.Index]) .ToArray(); var bestSharedLaneViolations = ElkEdgeRoutingScoring.CountSharedLaneViolations(currentGroupEdges, nodes); var bestTargetJoinViolations = ElkEdgeRoutingScoring.CountTargetApproachJoinViolations(currentGroupEdges, nodes); var bestBoundaryAngleViolations = ElkEdgeRoutingScoring.CountBadBoundaryAngles(currentGroupEdges, nodes); var bestGatewaySourceExitViolations = ElkEdgeRoutingScoring.CountGatewaySourceExitViolations(currentGroupEdges, nodes); var bestUnderNodeViolations = ElkEdgeRoutingScoring.CountUnderNodeViolations(currentGroupEdges, nodes); var bestPathLength = ComputePathLength(entry.Path); var prefersAlternateRepeatFace = !entry.IsOutgoing && !ElkShapeBoundaries.IsGatewayShape(entry.Node) && IsRepeatCollectorLabel(entry.Edge.Label) && groupEntries.Any(other => other.IsOutgoing); var candidatePaths = new List>(); var directCandidate = entry.IsOutgoing ? BuildMixedSourceFaceCandidate(entry.Path, entry.Node, side, desiredCoordinate, entry.AxisValue) : BuildMixedTargetFaceCandidate(entry.Path, entry.Node, side, desiredCoordinate, entry.AxisValue); AddUniquePathCandidate(candidatePaths, directCandidate); var availableSpan = Math.Abs(desiredCoordinate - anchor.BoundaryCoordinate); if ((prefersAlternateRepeatFace || availableSpan + 0.5d < minLineClearance) && TryBuildAlternateMixedFaceCandidate(entry, nodes, minLineClearance, out var alternateCandidate)) { AddUniquePathCandidate(candidatePaths, alternateCandidate); } foreach (var candidate in candidatePaths) { if (!PathChanged(entry.Path, candidate) || HasNodeObstacleCrossing(candidate, nodes, entry.Edge.SourceNodeId, entry.Edge.TargetNodeId)) { continue; } if (entry.IsOutgoing) { if (ElkShapeBoundaries.IsGatewayShape(entry.Node)) { if (!HasAcceptableGatewayBoundaryPath(candidate, nodes, entry.Edge.SourceNodeId, entry.Edge.TargetNodeId, entry.Node, fromStart: true)) { continue; } } else if (!HasClearBoundarySegments(candidate, nodes, entry.Edge.SourceNodeId, entry.Edge.TargetNodeId, true, 2) || !HasValidBoundaryAngle(candidate[0], candidate[1], entry.Node)) { continue; } } else { if (ElkShapeBoundaries.IsGatewayShape(entry.Node)) { if (!CanAcceptGatewayTargetRepair(candidate, entry.Node) || !HasAcceptableGatewayBoundaryPath(candidate, nodes, entry.Edge.SourceNodeId, entry.Edge.TargetNodeId, entry.Node, fromStart: false)) { continue; } } else if (!HasClearBoundarySegments(candidate, nodes, entry.Edge.SourceNodeId, entry.Edge.TargetNodeId, false, 4) || !HasValidBoundaryAngle(candidate[^1], candidate[^2], entry.Node) || HasTargetApproachBacktracking(candidate, entry.Node)) { continue; } } var candidateEdge = BuildSingleSectionEdge(entry.Edge, candidate); var candidateGroupEdges = groupEntries .Select(item => item.Index == entry.Index ? candidateEdge : result[item.Index]) .ToArray(); var candidateSharedLaneViolations = ElkEdgeRoutingScoring.CountSharedLaneViolations(candidateGroupEdges, nodes); var candidateTargetJoinViolations = ElkEdgeRoutingScoring.CountTargetApproachJoinViolations(candidateGroupEdges, nodes); var candidateBoundaryAngleViolations = ElkEdgeRoutingScoring.CountBadBoundaryAngles(candidateGroupEdges, nodes); var candidateGatewaySourceExitViolations = ElkEdgeRoutingScoring.CountGatewaySourceExitViolations(candidateGroupEdges, nodes); var candidateUnderNodeViolations = ElkEdgeRoutingScoring.CountUnderNodeViolations(candidateGroupEdges, nodes); var candidatePathLength = ComputePathLength(candidate); if (!IsBetterMixedNodeFaceCandidate( candidateSharedLaneViolations, candidateTargetJoinViolations, candidateBoundaryAngleViolations, candidateGatewaySourceExitViolations, candidateUnderNodeViolations, candidatePathLength, bestSharedLaneViolations, bestTargetJoinViolations, bestBoundaryAngleViolations, bestGatewaySourceExitViolations, bestUnderNodeViolations, bestPathLength)) { continue; } bestEdge = candidateEdge; bestSharedLaneViolations = candidateSharedLaneViolations; bestTargetJoinViolations = candidateTargetJoinViolations; bestBoundaryAngleViolations = candidateBoundaryAngleViolations; bestGatewaySourceExitViolations = candidateGatewaySourceExitViolations; bestUnderNodeViolations = candidateUnderNodeViolations; bestPathLength = candidatePathLength; } result[entry.Index] = bestEdge; } } return result; } internal static ElkRoutedEdge[] SeparateRepeatCollectorLocalLaneConflicts( ElkRoutedEdge[] edges, ElkPositionedNode[] nodes, double minLineClearance, IReadOnlyCollection? restrictedEdgeIds = null) { if (edges.Length < 2 || nodes.Length == 0) { return edges; } var result = edges.ToArray(); var graphMinY = nodes.Min(node => node.Y); var graphMaxY = nodes.Max(node => node.Y + node.Height); var restrictedSet = restrictedEdgeIds is null ? null : restrictedEdgeIds.ToHashSet(StringComparer.Ordinal); var nodeObstacles = 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 < result.Length; i++) { var edge = result[i]; if (restrictedSet is not null && !restrictedSet.Contains(edge.Id)) { continue; } if (!IsRepeatCollectorLabel(edge.Label)) { continue; } var path = ExtractFullPath(edge); if (path.Count < 3) { continue; } for (var segmentIndex = 0; segmentIndex < path.Count - 1; segmentIndex++) { var start = path[segmentIndex]; var end = path[segmentIndex + 1]; var isHorizontal = Math.Abs(start.Y - end.Y) <= 0.5d; var isVertical = Math.Abs(start.X - end.X) <= 0.5d; if (!isHorizontal && !isVertical) { continue; } var conflictFound = false; var desiredCoordinate = 0d; foreach (var otherEdge in result) { if (otherEdge.Id == edge.Id) { continue; } foreach (var otherSegment in ElkEdgeRoutingGeometry.FlattenSegments(otherEdge)) { if (!ElkEdgeRoutingGeometry.AreParallelAndClose(start, end, otherSegment.Start, otherSegment.End, minLineClearance)) { continue; } if (isHorizontal) { desiredCoordinate = start.Y <= otherSegment.Start.Y ? otherSegment.Start.Y - (minLineClearance + 4d) : otherSegment.Start.Y + (minLineClearance + 4d); } else { desiredCoordinate = start.X <= otherSegment.Start.X ? otherSegment.Start.X - (minLineClearance + 4d) : otherSegment.Start.X + (minLineClearance + 4d); } conflictFound = true; break; } if (conflictFound) { break; } } if (!conflictFound) { continue; } var preferredCoordinate = desiredCoordinate; var fallbackCoordinate = isHorizontal ? start.Y + (start.Y - desiredCoordinate) : start.X + (start.X - desiredCoordinate); foreach (var alternateCoordinate in new[] { preferredCoordinate, fallbackCoordinate }.Distinct()) { var candidate = ShiftSingleOrthogonalRun(path, segmentIndex, alternateCoordinate); if (!PathChanged(path, candidate) || HasNodeObstacleCrossing(candidate, nodes, edge.SourceNodeId, edge.TargetNodeId) || SegmentLeavesGraphBand(candidate, graphMinY, graphMaxY)) { continue; } var crossesObstacle = false; for (var candidateIndex = 0; candidateIndex < candidate.Count - 1; candidateIndex++) { if (!SegmentCrossesObstacle(candidate[candidateIndex], candidate[candidateIndex + 1], nodeObstacles, edge.SourceNodeId, edge.TargetNodeId)) { continue; } crossesObstacle = true; break; } if (crossesObstacle) { continue; } var repairedEdge = BuildSingleSectionEdge(edge, candidate); repairedEdge = RepairBoundaryAnglesAndTargetApproaches( [repairedEdge], nodes, minLineClearance)[0]; var repairedPath = ExtractFullPath(repairedEdge); if (HasNodeObstacleCrossing(repairedPath, nodes, edge.SourceNodeId, edge.TargetNodeId) || SegmentLeavesGraphBand(repairedPath, graphMinY, graphMaxY)) { continue; } result[i] = repairedEdge; break; } } } return result; } internal static ElkRoutedEdge[] ElevateRepeatCollectorNodeClearanceViolations( ElkRoutedEdge[] edges, ElkPositionedNode[] nodes, double minLineClearance, IReadOnlyCollection? restrictedEdgeIds = null) { if (edges.Length == 0 || nodes.Length == 0) { return edges; } var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal); var graphMinY = nodes.Min(node => node.Y); var corridorY = graphMinY - Math.Max(24d, minLineClearance * 0.6d); var restrictedSet = restrictedEdgeIds is null ? null : restrictedEdgeIds.ToHashSet(StringComparer.Ordinal); var result = edges.ToArray(); for (var i = 0; i < result.Length; i++) { var edge = result[i]; if (restrictedSet is not null && !restrictedSet.Contains(edge.Id)) { continue; } if (!IsRepeatCollectorLabel(edge.Label) || !nodesById.TryGetValue(edge.TargetNodeId ?? string.Empty, out var targetNode)) { continue; } var path = ExtractFullPath(edge); if (path.Count < 2 || !HasRepeatCollectorNodeClearanceViolation(path, nodes, edge.SourceNodeId, edge.TargetNodeId, minLineClearance)) { continue; } var targetApproachY = Math.Min(corridorY, targetNode.Y - 24d); ElkPoint targetEndpoint; if (ElkShapeBoundaries.IsGatewayShape(targetNode)) { var slotCoordinate = Math.Max(targetNode.X + 4d, Math.Min(targetNode.X + targetNode.Width - 4d, path[^1].X)); if (!ElkShapeBoundaries.TryProjectGatewayBoundarySlot(targetNode, "top", slotCoordinate, out targetEndpoint)) { continue; } targetEndpoint = ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary( targetNode, targetEndpoint, new ElkPoint { X = targetEndpoint.X, Y = targetApproachY }); } else { targetEndpoint = BuildRectBoundaryPointForSide(targetNode, "top", path[0]); } var rebuilt = new List { new() { X = path[0].X, Y = path[0].Y }, }; if (Math.Abs(rebuilt[^1].Y - corridorY) > 0.5d) { rebuilt.Add(new ElkPoint { X = rebuilt[^1].X, Y = corridorY }); } if (Math.Abs(rebuilt[^1].X - targetEndpoint.X) > 0.5d) { rebuilt.Add(new ElkPoint { X = targetEndpoint.X, Y = rebuilt[^1].Y }); } if (Math.Abs(rebuilt[^1].Y - targetApproachY) > 0.5d) { rebuilt.Add(new ElkPoint { X = targetEndpoint.X, Y = targetApproachY }); } rebuilt.Add(targetEndpoint); var candidate = NormalizePathPoints(rebuilt); if (ElkShapeBoundaries.IsGatewayShape(targetNode)) { candidate = NormalizeGatewayEntryPath(candidate, targetNode, targetEndpoint); } if (!PathChanged(path, candidate) || HasNodeObstacleCrossing(candidate, nodes, edge.SourceNodeId, edge.TargetNodeId) || HasRepeatCollectorNodeClearanceViolation(candidate, nodes, edge.SourceNodeId, edge.TargetNodeId, minLineClearance)) { continue; } var repairedEdge = BuildSingleSectionEdge(edge, candidate); repairedEdge = NormalizeSourceExitAngles([repairedEdge], nodes)[0]; var repairedPath = ExtractFullPath(repairedEdge); if (repairedPath.Count < 2 || HasNodeObstacleCrossing(repairedPath, nodes, edge.SourceNodeId, edge.TargetNodeId) || HasRepeatCollectorNodeClearanceViolation(repairedPath, nodes, edge.SourceNodeId, edge.TargetNodeId, minLineClearance) || (ElkShapeBoundaries.IsGatewayShape(targetNode) ? !CanAcceptGatewayTargetRepair(repairedPath, targetNode) : !HasValidBoundaryAngle(repairedPath[^1], repairedPath[^2], targetNode))) { continue; } result[i] = repairedEdge; } return result; } internal static ElkRoutedEdge[] ElevateUnderNodeViolations( ElkRoutedEdge[] edges, ElkPositionedNode[] nodes, double minLineClearance, IReadOnlyCollection? restrictedEdgeIds = null) { if (edges.Length == 0 || nodes.Length == 0) { return edges; } var restrictedSet = restrictedEdgeIds is null ? null : restrictedEdgeIds.ToHashSet(StringComparer.Ordinal); var result = edges.ToArray(); for (var i = 0; i < result.Length; i++) { var edge = result[i]; if (restrictedSet is not null && !restrictedSet.Contains(edge.Id)) { continue; } var path = ExtractFullPath(edge); if (path.Count < 2) { continue; } if (TryResolveUnderNodeWithPreferredShortcut( edge, path, nodes, minLineClearance, out var directRepair)) { var currentLocalHardPressure = ComputeUnderNodeRepairLocalHardPressure(edge, nodes); var repairedEdge = BuildSingleSectionEdge(edge, directRepair); repairedEdge = ResolveUnderNodePeerTargetConflicts( repairedEdge, result, i, nodes, minLineClearance); var repairedPath = ExtractFullPath(repairedEdge); var repairedUnderNodeSegments = CountUnderNodeSegments(repairedPath, nodes, edge.SourceNodeId, edge.TargetNodeId, minLineClearance); var currentUnderNodeSegments = CountUnderNodeSegments(path, nodes, edge.SourceNodeId, edge.TargetNodeId, minLineClearance); var repairedCrossesNode = HasNodeObstacleCrossing(repairedPath, nodes, edge.SourceNodeId, edge.TargetNodeId); var repairedLocalHardPressure = ComputeUnderNodeRepairLocalHardPressure(repairedEdge, nodes); WriteUnderNodeDebug( edge.Id, $"accept-check raw current={currentUnderNodeSegments} repaired={repairedUnderNodeSegments} crossing={repairedCrossesNode} local={repairedLocalHardPressure}/{currentLocalHardPressure} repaired={FormatPath(repairedPath)}"); if (repairedUnderNodeSegments < currentUnderNodeSegments && !repairedCrossesNode && repairedLocalHardPressure <= currentLocalHardPressure) { WriteUnderNodeDebug(edge.Id, "accept-check raw accepted"); result[i] = repairedUnderNodeSegments == 0 ? ProtectUnderNodeGeometry(repairedEdge) : repairedEdge; continue; } repairedEdge = RepairBoundaryAnglesAndTargetApproaches( [repairedEdge], nodes, minLineClearance)[0]; repairedEdge = FinalizeGatewayBoundaryGeometry([repairedEdge], nodes)[0]; repairedEdge = NormalizeBoundaryAngles([repairedEdge], nodes)[0]; repairedEdge = NormalizeSourceExitAngles([repairedEdge], nodes)[0]; repairedEdge = ResolveUnderNodePeerTargetConflicts( repairedEdge, result, i, nodes, minLineClearance); repairedPath = ExtractFullPath(repairedEdge); repairedUnderNodeSegments = CountUnderNodeSegments(repairedPath, nodes, edge.SourceNodeId, edge.TargetNodeId, minLineClearance); repairedCrossesNode = HasNodeObstacleCrossing(repairedPath, nodes, edge.SourceNodeId, edge.TargetNodeId); repairedLocalHardPressure = ComputeUnderNodeRepairLocalHardPressure(repairedEdge, nodes); WriteUnderNodeDebug( edge.Id, $"accept-check normalized current={currentUnderNodeSegments} repaired={repairedUnderNodeSegments} crossing={repairedCrossesNode} local={repairedLocalHardPressure}/{currentLocalHardPressure} repaired={FormatPath(repairedPath)}"); if (repairedUnderNodeSegments < currentUnderNodeSegments && !repairedCrossesNode && repairedLocalHardPressure <= currentLocalHardPressure) { WriteUnderNodeDebug(edge.Id, "accept-check normalized accepted"); result[i] = repairedUnderNodeSegments == 0 ? ProtectUnderNodeGeometry(repairedEdge) : repairedEdge; } continue; } var lifted = TryLiftUnderNodeSegments( path, nodes, edge.SourceNodeId, edge.TargetNodeId, minLineClearance); if (!PathChanged(path, lifted)) { continue; } var liftedEdge = BuildSingleSectionEdge(edge, lifted); liftedEdge = NormalizeBoundaryAngles([liftedEdge], nodes)[0]; liftedEdge = NormalizeSourceExitAngles([liftedEdge], nodes)[0]; var liftedPath = ExtractFullPath(liftedEdge); if (CountUnderNodeSegments(liftedPath, nodes, edge.SourceNodeId, edge.TargetNodeId, minLineClearance) < CountUnderNodeSegments(path, nodes, edge.SourceNodeId, edge.TargetNodeId, minLineClearance) && !HasNodeObstacleCrossing(liftedPath, nodes, edge.SourceNodeId, edge.TargetNodeId)) { result[i] = CountUnderNodeSegments(liftedPath, nodes, edge.SourceNodeId, edge.TargetNodeId, minLineClearance) == 0 ? ProtectUnderNodeGeometry(liftedEdge) : liftedEdge; } } return result; } private static int ComputeUnderNodeRepairLocalHardPressure( ElkRoutedEdge edge, IReadOnlyCollection nodes) { return ElkEdgeRoutingScoring.CountBelowGraphViolations([edge], nodes) + ElkEdgeRoutingScoring.CountUnderNodeViolations([edge], nodes) + ElkEdgeRoutingScoring.CountLongDiagonalViolations([edge], nodes) + ElkEdgeRoutingScoring.CountBadBoundaryAngles([edge], nodes) + ElkEdgeRoutingScoring.CountGatewaySourceExitViolations([edge], nodes) + ElkEdgeRoutingScoring.CountTargetApproachJoinViolations([edge], nodes) + ElkEdgeRoutingScoring.CountSharedLaneViolations([edge], nodes) + ElkEdgeRoutingScoring.CountTargetApproachBacktrackingViolations([edge], nodes) + ElkEdgeRoutingScoring.CountExcessiveDetourViolations([edge], nodes); } internal static ElkRoutedEdge[] PolishTargetPeerConflicts( ElkRoutedEdge[] edges, ElkPositionedNode[] nodes, double minLineClearance, IReadOnlyCollection? restrictedEdgeIds = null) { if (edges.Length < 2 || nodes.Length == 0) { return edges; } var restrictedSet = restrictedEdgeIds is null ? null : restrictedEdgeIds.ToHashSet(StringComparer.Ordinal); var result = edges.ToArray(); for (var i = 0; i < result.Length; i++) { if (restrictedSet is not null && !restrictedSet.Contains(result[i].Id)) { continue; } result[i] = ResolveUnderNodePeerTargetConflicts( result[i], result, i, nodes, minLineClearance); } return result; } private static ElkRoutedEdge ResolveUnderNodePeerTargetConflicts( ElkRoutedEdge candidateEdge, IReadOnlyList currentEdges, int candidateIndex, ElkPositionedNode[] nodes, double minLineClearance) { if (TryPolishGatewayUnderNodeTargetPeerConflicts( candidateEdge, currentEdges, candidateIndex, nodes, minLineClearance, out var gatewayPolishedEdge)) { return gatewayPolishedEdge; } return TryPolishRectUnderNodeTargetPeerConflicts( candidateEdge, currentEdges, candidateIndex, nodes, minLineClearance, out var polishedEdge) ? polishedEdge : candidateEdge; } private static bool TryPolishGatewayUnderNodeTargetPeerConflicts( ElkRoutedEdge candidateEdge, IReadOnlyList currentEdges, int candidateIndex, ElkPositionedNode[] nodes, double minLineClearance, out ElkRoutedEdge polishedEdge) { polishedEdge = candidateEdge; if (string.IsNullOrWhiteSpace(candidateEdge.TargetNodeId)) { return false; } var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal); if (!nodesById.TryGetValue(candidateEdge.TargetNodeId, out var targetNode) || !ElkShapeBoundaries.IsGatewayShape(targetNode)) { return false; } nodesById.TryGetValue(candidateEdge.SourceNodeId ?? string.Empty, out var sourceNode); var peerEdges = currentEdges .Where((edge, index) => index != candidateIndex && string.Equals(edge.TargetNodeId, candidateEdge.TargetNodeId, StringComparison.Ordinal)) .ToArray(); if (peerEdges.Length == 0) { return false; } var path = ExtractFullPath(candidateEdge); if (path.Count < 2) { return false; } var sourceNodeId = candidateEdge.SourceNodeId; var targetNodeId = candidateEdge.TargetNodeId; var currentBundle = peerEdges .Append(candidateEdge) .ToArray(); var currentTargetJoinViolations = ElkEdgeRoutingScoring.CountTargetApproachJoinViolations(currentBundle, nodes); var currentSharedLaneViolations = ElkEdgeRoutingScoring.CountSharedLaneViolations(currentBundle, nodes); var currentUnderNodeSegments = CountUnderNodeSegments(path, nodes, sourceNodeId, targetNodeId, minLineClearance); var currentUnderNodeViolations = ElkEdgeRoutingScoring.CountUnderNodeViolations([candidateEdge], nodes); var currentLocalHardPressure = ComputeUnderNodeRepairLocalHardPressure(candidateEdge, nodes); var currentPathLength = ComputePathLength(path); if (currentTargetJoinViolations == 0 && currentSharedLaneViolations == 0 && currentUnderNodeSegments == 0 && currentUnderNodeViolations == 0) { return false; } var bestEdge = default(ElkRoutedEdge); var bestTargetJoinViolations = currentTargetJoinViolations; var bestSharedLaneViolations = currentSharedLaneViolations; var bestUnderNodeSegments = currentUnderNodeSegments; var bestUnderNodeViolations = currentUnderNodeViolations; var bestLocalHardPressure = currentLocalHardPressure; var bestPathLength = currentPathLength; foreach (var candidatePath in EnumerateGatewayUnderNodePeerConflictCandidates( path, targetNode, sourceNode, peerEdges, nodes, sourceNodeId, targetNodeId, minLineClearance)) { if (!PathChanged(path, candidatePath) || candidatePath.Count < 2 || HasNodeObstacleCrossing(candidatePath, nodes, sourceNodeId, targetNodeId) || !CanAcceptGatewayTargetRepair(candidatePath, targetNode) || !HasAcceptableGatewayBoundaryPath(candidatePath, nodes, sourceNodeId, targetNodeId, targetNode, fromStart: false)) { continue; } var localCandidateEdge = BuildSingleSectionEdge(candidateEdge, candidatePath); var localBundle = peerEdges .Append(localCandidateEdge) .ToArray(); var candidateTargetJoinViolations = ElkEdgeRoutingScoring.CountTargetApproachJoinViolations(localBundle, nodes); var candidateSharedLaneViolations = ElkEdgeRoutingScoring.CountSharedLaneViolations(localBundle, nodes); var candidateUnderNodeSegments = CountUnderNodeSegments(candidatePath, nodes, sourceNodeId, targetNodeId, minLineClearance); var candidateUnderNodeViolations = ElkEdgeRoutingScoring.CountUnderNodeViolations([localCandidateEdge], nodes); var candidateLocalHardPressure = ComputeUnderNodeRepairLocalHardPressure(localCandidateEdge, nodes); var candidatePathLength = ComputePathLength(candidatePath); if (!IsBetterGatewayUnderNodePeerConflictCandidate( candidateTargetJoinViolations, candidateSharedLaneViolations, candidateUnderNodeSegments, candidateUnderNodeViolations, candidateLocalHardPressure, candidatePathLength, bestTargetJoinViolations, bestSharedLaneViolations, bestUnderNodeSegments, bestUnderNodeViolations, bestLocalHardPressure, bestPathLength)) { continue; } bestEdge = localCandidateEdge; bestTargetJoinViolations = candidateTargetJoinViolations; bestSharedLaneViolations = candidateSharedLaneViolations; bestUnderNodeSegments = candidateUnderNodeSegments; bestUnderNodeViolations = candidateUnderNodeViolations; bestLocalHardPressure = candidateLocalHardPressure; bestPathLength = candidatePathLength; } if (bestEdge is null) { return false; } polishedEdge = bestEdge; return true; } private static bool TryPolishRectUnderNodeTargetPeerConflicts( ElkRoutedEdge candidateEdge, IReadOnlyList currentEdges, int candidateIndex, ElkPositionedNode[] nodes, double minLineClearance, out ElkRoutedEdge polishedEdge) { polishedEdge = candidateEdge; if (string.IsNullOrWhiteSpace(candidateEdge.TargetNodeId)) { return false; } var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal); if (!nodesById.TryGetValue(candidateEdge.TargetNodeId, out var targetNode) || ElkShapeBoundaries.IsGatewayShape(targetNode)) { return false; } var peerEdges = currentEdges .Where((edge, index) => index != candidateIndex && string.Equals(edge.TargetNodeId, candidateEdge.TargetNodeId, StringComparison.Ordinal)) .ToArray(); if (peerEdges.Length == 0) { return false; } var currentBundle = peerEdges .Append(candidateEdge) .ToArray(); if (ElkEdgeRoutingScoring.CountTargetApproachJoinViolations(currentBundle, nodes) == 0) { return false; } var path = ExtractFullPath(candidateEdge); if (path.Count < 2) { return false; } var sourceNodeId = candidateEdge.SourceNodeId; var targetNodeId = candidateEdge.TargetNodeId; var currentUnderNodeSegments = CountUnderNodeSegments(path, nodes, sourceNodeId, targetNodeId, minLineClearance); var currentSide = ResolveTargetApproachSide(path, targetNode); var bestScore = double.PositiveInfinity; ElkRoutedEdge? bestEdge = null; foreach (var side in EnumerateRectTargetPeerConflictSides(path, targetNode, currentSide)) { var axisCandidates = EnumerateRectTargetPeerConflictAxes(path, targetNode, side, minLineClearance).ToArray(); if (axisCandidates.Length == 0) { continue; } var boundaryCoordinates = EnumerateRectTargetPeerConflictBoundaryCoordinates(path, targetNode, side).ToArray(); if (boundaryCoordinates.Length == 0) { continue; } foreach (var axis in axisCandidates) { foreach (var boundaryCoordinate in boundaryCoordinates) { var candidatePath = BuildMixedTargetFaceCandidate(path, targetNode, side, boundaryCoordinate, axis); if (!PathChanged(path, candidatePath) || HasNodeObstacleCrossing(candidatePath, nodes, sourceNodeId, targetNodeId) || HasTargetApproachBacktracking(candidatePath, targetNode) || !HasValidBoundaryAngle(candidatePath[^1], candidatePath[^2], targetNode)) { continue; } var candidateUnderNodeSegments = CountUnderNodeSegments(candidatePath, nodes, sourceNodeId, targetNodeId, minLineClearance); if (candidateUnderNodeSegments > currentUnderNodeSegments) { continue; } var localCandidateEdge = BuildSingleSectionEdge(candidateEdge, candidatePath); var localBundle = peerEdges .Append(localCandidateEdge) .ToArray(); if (ElkEdgeRoutingScoring.CountTargetApproachJoinViolations(localBundle, nodes) > 0) { continue; } var score = ComputeRectTargetPeerConflictPolishScore(candidatePath, currentSide, side); if (score >= bestScore) { continue; } bestScore = score; bestEdge = localCandidateEdge; } } } if (bestEdge is null) { return false; } polishedEdge = bestEdge; return true; } private static IEnumerable> EnumerateGatewayUnderNodePeerConflictCandidates( IReadOnlyList path, ElkPositionedNode targetNode, ElkPositionedNode? sourceNode, IReadOnlyCollection peerEdges, IReadOnlyCollection nodes, string? sourceNodeId, string? targetNodeId, double minLineClearance) { foreach (var side in EnumerateGatewayUnderNodePeerConflictSides(path, targetNode, peerEdges)) { var slotCoordinates = EnumerateGatewayUnderNodePeerConflictSlotCoordinates( path, targetNode, sourceNode, peerEdges, side, minLineClearance) .ToArray(); if (slotCoordinates.Length == 0) { continue; } foreach (var slotCoordinate in slotCoordinates) { if (sourceNode is not null && ElkShapeBoundaries.TryProjectGatewayBoundarySlot(targetNode, side, slotCoordinate, out var bandBoundary) && TryBuildSafeHorizontalBandCandidate( sourceNode, targetNode, nodes, sourceNodeId, targetNodeId, path[0], bandBoundary, minLineClearance, preferredSourceExterior: null, out var bandCandidate)) { yield return bandCandidate; } foreach (var axis in EnumerateGatewayUnderNodePeerConflictAxes( path, targetNode, side, nodes, sourceNodeId, targetNodeId, minLineClearance)) { yield return BuildMixedTargetFaceCandidate(path, targetNode, side, slotCoordinate, axis); } } } } private static IEnumerable EnumerateGatewayUnderNodePeerConflictSides( IReadOnlyList path, ElkPositionedNode targetNode, IReadOnlyCollection peerEdges) { var seen = new HashSet(StringComparer.Ordinal); var currentSide = ResolveTargetApproachSide(path, targetNode); var peerSides = peerEdges .Select(edge => ExtractFullPath(edge)) .Where(peerPath => peerPath.Count >= 2) .Select(peerPath => ResolveTargetApproachSide(peerPath, targetNode)) .ToHashSet(StringComparer.Ordinal); foreach (var side in new[] { "top", "bottom", "right", "left" }) { if (!string.Equals(side, currentSide, StringComparison.Ordinal) && !peerSides.Contains(side) && seen.Add(side)) { yield return side; } } if (seen.Add(currentSide)) { yield return currentSide; } foreach (var side in new[] { "top", "bottom", "right", "left" }) { if (seen.Add(side)) { yield return side; } } } private static IEnumerable EnumerateGatewayUnderNodePeerConflictSlotCoordinates( IReadOnlyList path, ElkPositionedNode targetNode, ElkPositionedNode? sourceNode, IReadOnlyCollection peerEdges, string side, double minLineClearance) { var coordinates = new List(); var inset = 10d; var spacing = Math.Max(14d, minLineClearance + 6d); var centerX = targetNode.X + (targetNode.Width / 2d); var centerY = targetNode.Y + (targetNode.Height / 2d); var slotMinimum = side is "left" or "right" ? targetNode.Y + inset : targetNode.X + inset; var slotMaximum = side is "left" or "right" ? targetNode.Y + targetNode.Height - inset : targetNode.X + targetNode.Width - inset; void AddClamped(double value) { AddUniqueCoordinate(coordinates, Math.Max(slotMinimum, Math.Min(slotMaximum, value))); } if (side is "left" or "right") { AddClamped(path[^1].Y); foreach (var peer in peerEdges) { var peerPath = ExtractFullPath(peer); if (peerPath.Count > 0) { AddClamped(peerPath[^1].Y - spacing); AddClamped(peerPath[^1].Y + spacing); AddClamped(peerPath[^1].Y); } } if (sourceNode is not null) { AddClamped(sourceNode.Y + (sourceNode.Height / 2d)); } AddClamped(centerY - spacing); AddClamped(centerY); AddClamped(centerY + spacing); } else { AddClamped(path[^1].X); foreach (var peer in peerEdges) { var peerPath = ExtractFullPath(peer); if (peerPath.Count > 0) { AddClamped(peerPath[^1].X - spacing); AddClamped(peerPath[^1].X + spacing); AddClamped(peerPath[^1].X); } } if (sourceNode is not null) { AddClamped(sourceNode.X + (sourceNode.Width / 2d)); } AddClamped(centerX - spacing); AddClamped(centerX); AddClamped(centerX + spacing); } foreach (var coordinate in coordinates.Take(8)) { yield return coordinate; } } private static IEnumerable EnumerateGatewayUnderNodePeerConflictAxes( IReadOnlyList path, ElkPositionedNode targetNode, string side, IReadOnlyCollection nodes, string? sourceNodeId, string? targetNodeId, double minLineClearance) { var coordinates = new List(); var currentAxis = ResolveTargetApproachAxisValue(path, side); if (!double.IsNaN(currentAxis)) { AddUniqueCoordinate(coordinates, currentAxis); } AddUniqueCoordinate(coordinates, ResolveDefaultTargetApproachAxis(targetNode, side)); var clearance = Math.Max(24d, minLineClearance * 0.6d); if (side is "top" or "bottom") { var minX = Math.Min(path[0].X, targetNode.X); var maxX = Math.Max(path[0].X, targetNode.X + targetNode.Width); 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) .ToArray(); if (side == "top") { var highestBlockerY = blockers.Length > 0 ? blockers.Min(node => node.Y) : Math.Min(path[0].Y, targetNode.Y); AddUniqueCoordinate(coordinates, Math.Min(targetNode.Y - 8d, highestBlockerY - clearance)); } else { var lowestBlockerY = blockers.Length > 0 ? blockers.Max(node => node.Y + node.Height) : Math.Max(path[0].Y, targetNode.Y + targetNode.Height); AddUniqueCoordinate(coordinates, Math.Max(targetNode.Y + targetNode.Height + 8d, lowestBlockerY + clearance)); } } else { var minY = Math.Min(path[0].Y, targetNode.Y); var maxY = Math.Max(path[0].Y, targetNode.Y + targetNode.Height); var blockers = nodes .Where(node => !string.Equals(node.Id, sourceNodeId, StringComparison.Ordinal) && !string.Equals(node.Id, targetNodeId, StringComparison.Ordinal) && maxY > node.Y + 0.5d && minY < node.Y + node.Height - 0.5d) .ToArray(); if (side == "left") { var leftmostBlockerX = blockers.Length > 0 ? blockers.Min(node => node.X) : Math.Min(path[0].X, targetNode.X); AddUniqueCoordinate(coordinates, Math.Min(targetNode.X - 8d, leftmostBlockerX - clearance)); } else { var rightmostBlockerX = blockers.Length > 0 ? blockers.Max(node => node.X + node.Width) : Math.Max(path[0].X, targetNode.X + targetNode.Width); AddUniqueCoordinate(coordinates, Math.Max(targetNode.X + targetNode.Width + 8d, rightmostBlockerX + clearance)); } } foreach (var coordinate in coordinates.Take(6)) { yield return coordinate; } } private static bool IsBetterGatewayUnderNodePeerConflictCandidate( int candidateTargetJoinViolations, int candidateSharedLaneViolations, int candidateUnderNodeSegments, int candidateUnderNodeViolations, int candidateLocalHardPressure, double candidatePathLength, int currentTargetJoinViolations, int currentSharedLaneViolations, int currentUnderNodeSegments, int currentUnderNodeViolations, int currentLocalHardPressure, double currentPathLength) { if (candidateTargetJoinViolations != currentTargetJoinViolations) { return candidateTargetJoinViolations < currentTargetJoinViolations; } if (candidateUnderNodeViolations != currentUnderNodeViolations) { return candidateUnderNodeViolations < currentUnderNodeViolations; } if (candidateUnderNodeSegments != currentUnderNodeSegments) { return candidateUnderNodeSegments < currentUnderNodeSegments; } if (candidateSharedLaneViolations != currentSharedLaneViolations) { return candidateSharedLaneViolations < currentSharedLaneViolations; } if (candidateLocalHardPressure != currentLocalHardPressure) { return candidateLocalHardPressure < currentLocalHardPressure; } return candidatePathLength + 0.5d < currentPathLength; } private static IEnumerable EnumerateRectTargetPeerConflictSides( IReadOnlyList path, ElkPositionedNode targetNode, string currentSide) { var seen = new HashSet(StringComparer.Ordinal); const double tolerance = 0.5d; if (path.Any(point => point.Y < targetNode.Y - tolerance) && seen.Add("top")) { yield return "top"; } if (path.Any(point => point.Y > targetNode.Y + targetNode.Height + tolerance) && seen.Add("bottom")) { yield return "bottom"; } if (seen.Add(currentSide)) { yield return currentSide; } } private static IEnumerable EnumerateRectTargetPeerConflictAxes( IReadOnlyList path, ElkPositionedNode targetNode, string side, double minLineClearance) { var coordinates = new List(); var clearance = Math.Max(24d, minLineClearance * 0.6d); const double tolerance = 0.5d; switch (side) { case "top": foreach (var value in path .Select(point => point.Y) .Where(coordinate => coordinate < targetNode.Y - tolerance) .OrderByDescending(coordinate => coordinate)) { AddUniqueCoordinate(coordinates, value); } AddUniqueCoordinate(coordinates, targetNode.Y - clearance); break; case "bottom": foreach (var value in path .Select(point => point.Y) .Where(coordinate => coordinate > targetNode.Y + targetNode.Height + tolerance) .OrderBy(coordinate => coordinate)) { AddUniqueCoordinate(coordinates, value); } AddUniqueCoordinate(coordinates, targetNode.Y + targetNode.Height + clearance); break; case "left": foreach (var value in path .Select(point => point.X) .Where(coordinate => coordinate < targetNode.X - tolerance) .OrderByDescending(coordinate => coordinate)) { AddUniqueCoordinate(coordinates, value); } AddUniqueCoordinate(coordinates, targetNode.X - clearance); break; case "right": foreach (var value in path .Select(point => point.X) .Where(coordinate => coordinate > targetNode.X + targetNode.Width + tolerance) .OrderBy(coordinate => coordinate)) { AddUniqueCoordinate(coordinates, value); } AddUniqueCoordinate(coordinates, targetNode.X + targetNode.Width + clearance); break; } foreach (var coordinate in coordinates.Take(6)) { yield return coordinate; } } private static IEnumerable EnumerateRectTargetPeerConflictBoundaryCoordinates( IReadOnlyList path, ElkPositionedNode targetNode, string side) { var coordinates = new List(); var insetX = Math.Min(24d, Math.Max(8d, targetNode.Width / 4d)); var insetY = Math.Min(24d, Math.Max(8d, targetNode.Height / 4d)); if (side is "top" or "bottom") { var referenceX = path.Count > 1 ? path[^2].X : path[^1].X; AddUniqueCoordinate(coordinates, referenceX); AddUniqueCoordinate(coordinates, targetNode.X + insetX); AddUniqueCoordinate(coordinates, targetNode.X + (targetNode.Width / 2d)); AddUniqueCoordinate(coordinates, targetNode.X + targetNode.Width - insetX); foreach (var coordinate in coordinates .OrderBy(value => Math.Abs(Math.Clamp(value, targetNode.X + insetX, (targetNode.X + targetNode.Width) - insetX) - referenceX)) .Take(6)) { yield return coordinate; } yield break; } var referenceY = path[^1].Y; AddUniqueCoordinate(coordinates, referenceY); AddUniqueCoordinate(coordinates, targetNode.Y + insetY); AddUniqueCoordinate(coordinates, targetNode.Y + (targetNode.Height / 2d)); AddUniqueCoordinate(coordinates, targetNode.Y + targetNode.Height - insetY); foreach (var coordinate in coordinates .OrderBy(value => Math.Abs(Math.Clamp(value, targetNode.Y + insetY, (targetNode.Y + targetNode.Height) - insetY) - referenceY)) .Take(6)) { yield return coordinate; } } private static double ComputeRectTargetPeerConflictPolishScore( IReadOnlyList candidatePath, string currentSide, string candidateSide) { var score = ComputePathLength(candidatePath) + (Math.Max(0, candidatePath.Count - 2) * 8d); if (!string.Equals(currentSide, candidateSide, StringComparison.Ordinal)) { score += 12d; } return score; } internal static ElkRoutedEdge[] SeparateSharedLaneConflicts( ElkRoutedEdge[] edges, ElkPositionedNode[] nodes, double minLineClearance, IReadOnlyCollection? restrictedEdgeIds = null) { if (edges.Length < 2 || nodes.Length == 0) { return edges; } var result = edges.ToArray(); var graphMinY = nodes.Min(node => node.Y); var graphMaxY = nodes.Max(node => node.Y + node.Height); var restrictedSet = restrictedEdgeIds is null ? null : restrictedEdgeIds.ToHashSet(StringComparer.Ordinal); var nodeObstacles = nodes.Select(node => ( Left: node.X, Top: node.Y, Right: node.X + node.Width, Bottom: node.Y + node.Height, Id: node.Id)).ToArray(); var conflicts = ElkEdgeRoutingScoring.DetectSharedLaneConflicts(result, nodes) .Where(conflict => restrictedSet is null || restrictedSet.Contains(conflict.LeftEdgeId) || restrictedSet.Contains(conflict.RightEdgeId)) .Distinct() .ToArray(); foreach (var conflict in conflicts) { var leftIndex = Array.FindIndex(result, edge => string.Equals(edge.Id, conflict.LeftEdgeId, StringComparison.Ordinal)); var rightIndex = Array.FindIndex(result, edge => string.Equals(edge.Id, conflict.RightEdgeId, StringComparison.Ordinal)); if (leftIndex < 0 || rightIndex < 0) { continue; } var leftEdge = result[leftIndex]; var rightEdge = result[rightIndex]; if (TryResolveSharedLaneByPairedNodeHandoffSlotRepair( result, leftIndex, leftEdge, rightIndex, rightEdge, nodes, minLineClearance, graphMinY, graphMaxY, out var pairedLeftEdge, out var pairedRightEdge)) { result[leftIndex] = pairedLeftEdge; result[rightIndex] = pairedRightEdge; continue; } var repairOrder = new[] { (Index: ComputePathLength(ExtractFullPath(leftEdge)) >= ComputePathLength(ExtractFullPath(rightEdge)) ? leftIndex : rightIndex, Other: ComputePathLength(ExtractFullPath(leftEdge)) >= ComputePathLength(ExtractFullPath(rightEdge)) ? rightEdge : leftEdge), (Index: ComputePathLength(ExtractFullPath(leftEdge)) >= ComputePathLength(ExtractFullPath(rightEdge)) ? rightIndex : leftIndex, Other: ComputePathLength(ExtractFullPath(leftEdge)) >= ComputePathLength(ExtractFullPath(rightEdge)) ? leftEdge : rightEdge), }; foreach (var repairCandidate in repairOrder) { if (TryResolveSharedLaneByAlternateRepeatFace( result[repairCandidate.Index], repairCandidate.Other, nodes, minLineClearance, graphMinY, graphMaxY, out var alternateFaceEdge)) { result[repairCandidate.Index] = alternateFaceEdge; break; } if (TryResolveSharedLaneByDirectSourceSlotRepair( result, repairCandidate.Index, result[repairCandidate.Index], repairCandidate.Other, nodes, minLineClearance, graphMinY, graphMaxY, out var directSourceSlotEdge)) { result[repairCandidate.Index] = directSourceSlotEdge; break; } if (TryResolveSharedLaneByDirectNodeHandoffSlotRepair( result, repairCandidate.Index, result[repairCandidate.Index], repairCandidate.Other, nodes, minLineClearance, graphMinY, graphMaxY, out var directNodeHandoffEdge)) { result[repairCandidate.Index] = directNodeHandoffEdge; break; } if (TryResolveSharedLaneByFocusedSourceDepartureSpread( result, repairCandidate.Index, result[repairCandidate.Index], repairCandidate.Other, nodes, minLineClearance, graphMinY, graphMaxY, out var sourceSpreadEdge)) { result[repairCandidate.Index] = sourceSpreadEdge; break; } if (TryResolveSharedLaneByFocusedMixedNodeFaceRepair( result, repairCandidate.Index, result[repairCandidate.Index], repairCandidate.Other, nodes, minLineClearance, graphMinY, graphMaxY, out var mixedFaceEdge)) { result[repairCandidate.Index] = mixedFaceEdge; break; } if (!TrySeparateSharedLaneConflict( result[repairCandidate.Index], repairCandidate.Other, nodes, minLineClearance, graphMinY, graphMaxY, nodeObstacles, out var repairedEdge)) { continue; } result[repairCandidate.Index] = repairedEdge; break; } } return result; } private static bool TryResolveSharedLaneByPairedNodeHandoffSlotRepair( ElkRoutedEdge[] currentEdges, int leftIndex, ElkRoutedEdge leftEdge, int rightIndex, ElkRoutedEdge rightEdge, ElkPositionedNode[] nodes, double minLineClearance, double graphMinY, double graphMaxY, out ElkRoutedEdge repairedLeftEdge, out ElkRoutedEdge repairedRightEdge) { repairedLeftEdge = leftEdge; repairedRightEdge = rightEdge; var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal); if (!TryResolveSharedLaneNodeHandoffContext(leftEdge, rightEdge, nodesById, graphMinY, graphMaxY, out var leftContext) || !TryResolveSharedLaneNodeHandoffContext(rightEdge, leftEdge, nodesById, graphMinY, graphMaxY, out var rightContext) || !string.Equals(leftContext.SharedNode.Id, rightContext.SharedNode.Id, StringComparison.Ordinal) || !string.Equals(leftContext.Side, rightContext.Side, StringComparison.Ordinal) || leftContext.IsOutgoing == rightContext.IsOutgoing) { return false; } var baselineConflicts = ElkEdgeRoutingScoring.DetectSharedLaneConflicts(currentEdges, nodes); var baselineConflictCount = baselineConflicts.Count; var baselineLeftConflictCount = baselineConflicts.Count(conflict => string.Equals(conflict.LeftEdgeId, leftEdge.Id, StringComparison.Ordinal) || string.Equals(conflict.RightEdgeId, leftEdge.Id, StringComparison.Ordinal)); var baselineRightConflictCount = baselineConflicts.Count(conflict => string.Equals(conflict.LeftEdgeId, rightEdge.Id, StringComparison.Ordinal) || string.Equals(conflict.RightEdgeId, rightEdge.Id, StringComparison.Ordinal)); var baselineCombinedPathLength = ComputePathLength(leftContext.Path) + ComputePathLength(rightContext.Path); var peerCoordinates = CollectSharedLaneNodeFaceBoundaryCoordinates( currentEdges, leftContext.SharedNode, leftContext.Side, graphMinY, graphMaxY, leftEdge.Id); var leftRepairCoordinates = EnumerateSharedLaneBoundaryRepairCoordinates( leftContext.SharedNode, leftContext.Side, leftContext.CurrentBoundaryCoordinate, minLineClearance, peerCoordinates) .ToArray(); var rightRepairCoordinates = EnumerateSharedLaneBoundaryRepairCoordinates( rightContext.SharedNode, rightContext.Side, rightContext.CurrentBoundaryCoordinate, minLineClearance, peerCoordinates) .ToArray(); ElkRoutedEdge? bestLeft = null; ElkRoutedEdge? bestRight = null; var bestConflictCount = baselineConflictCount; var bestLeftConflictCount = baselineLeftConflictCount; var bestRightConflictCount = baselineRightConflictCount; var bestCombinedPathLength = baselineCombinedPathLength; foreach (var leftCoordinate in leftRepairCoordinates) { var leftCandidatePath = leftContext.IsOutgoing ? BuildMixedSourceFaceCandidate(leftContext.Path, leftContext.SharedNode, leftContext.Side, leftCoordinate, leftContext.AxisValue) : BuildMixedTargetFaceCandidate(leftContext.Path, leftContext.SharedNode, leftContext.Side, leftCoordinate, leftContext.AxisValue); if (!IsValidSharedLaneBoundaryRepairCandidate( leftEdge, leftContext.Path, leftCandidatePath, leftContext.SharedNode, leftContext.IsOutgoing, nodes, graphMinY, graphMaxY)) { continue; } foreach (var rightCoordinate in rightRepairCoordinates) { var rightCandidatePath = rightContext.IsOutgoing ? BuildMixedSourceFaceCandidate(rightContext.Path, rightContext.SharedNode, rightContext.Side, rightCoordinate, rightContext.AxisValue) : BuildMixedTargetFaceCandidate(rightContext.Path, rightContext.SharedNode, rightContext.Side, rightCoordinate, rightContext.AxisValue); if (!IsValidSharedLaneBoundaryRepairCandidate( rightEdge, rightContext.Path, rightCandidatePath, rightContext.SharedNode, rightContext.IsOutgoing, nodes, graphMinY, graphMaxY)) { continue; } var candidateLeft = BuildSingleSectionEdge(leftEdge, leftCandidatePath); var candidateRight = BuildSingleSectionEdge(rightEdge, rightCandidatePath); if (ElkEdgeRoutingScoring.DetectSharedLaneConflicts([candidateLeft, candidateRight], nodes).Count > 0 || ComputeUnderNodeRepairLocalHardPressure(candidateLeft, nodes) > ComputeUnderNodeRepairLocalHardPressure(leftEdge, nodes) || ComputeUnderNodeRepairLocalHardPressure(candidateRight, nodes) > ComputeUnderNodeRepairLocalHardPressure(rightEdge, nodes)) { continue; } var candidateEdges = currentEdges.ToArray(); candidateEdges[leftIndex] = candidateLeft; candidateEdges[rightIndex] = candidateRight; var candidateConflicts = ElkEdgeRoutingScoring.DetectSharedLaneConflicts(candidateEdges, nodes); var candidateConflictCount = candidateConflicts.Count; var candidateLeftConflictCount = candidateConflicts.Count(conflict => string.Equals(conflict.LeftEdgeId, leftEdge.Id, StringComparison.Ordinal) || string.Equals(conflict.RightEdgeId, leftEdge.Id, StringComparison.Ordinal)); var candidateRightConflictCount = candidateConflicts.Count(conflict => string.Equals(conflict.LeftEdgeId, rightEdge.Id, StringComparison.Ordinal) || string.Equals(conflict.RightEdgeId, rightEdge.Id, StringComparison.Ordinal)); if (candidateConflictCount > bestConflictCount || candidateLeftConflictCount > bestLeftConflictCount || candidateRightConflictCount > bestRightConflictCount) { continue; } var candidateCombinedPathLength = ComputePathLength(leftCandidatePath) + ComputePathLength(rightCandidatePath); var isBetter = candidateConflictCount < bestConflictCount || candidateLeftConflictCount < bestLeftConflictCount || candidateRightConflictCount < bestRightConflictCount || candidateCombinedPathLength + 0.5d < bestCombinedPathLength; if (!isBetter) { continue; } bestLeft = candidateLeft; bestRight = candidateRight; bestConflictCount = candidateConflictCount; bestLeftConflictCount = candidateLeftConflictCount; bestRightConflictCount = candidateRightConflictCount; bestCombinedPathLength = candidateCombinedPathLength; } } if (bestLeft is null || bestRight is null || bestConflictCount >= baselineConflictCount) { return false; } repairedLeftEdge = bestLeft; repairedRightEdge = bestRight; return true; } private static bool TryResolveSharedLaneByDirectSourceSlotRepair( ElkRoutedEdge[] currentEdges, int repairIndex, ElkRoutedEdge edge, ElkRoutedEdge otherEdge, ElkPositionedNode[] nodes, double minLineClearance, double graphMinY, double graphMaxY, out ElkRoutedEdge repairedEdge) { repairedEdge = edge; if (string.IsNullOrWhiteSpace(edge.SourceNodeId) || !string.Equals(edge.SourceNodeId, otherEdge.SourceNodeId, StringComparison.Ordinal)) { return false; } var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal); if (!nodesById.TryGetValue(edge.SourceNodeId ?? string.Empty, out var sourceNode)) { return false; } var path = ExtractFullPath(edge); var otherPath = ExtractFullPath(otherEdge); if (path.Count < 2 || otherPath.Count < 2 || !ShouldSpreadSourceDeparture(edge, graphMinY, graphMaxY) || !ShouldSpreadSourceDeparture(otherEdge, graphMinY, graphMaxY)) { return false; } var side = ResolveSourceDepartureSide(path, sourceNode); var otherSide = ResolveSourceDepartureSide(otherPath, sourceNode); if (!string.Equals(side, otherSide, StringComparison.Ordinal)) { return false; } var axisValue = TryExtractSourceDepartureRun(path, side, out _, out var runEndIndex) ? side is "left" or "right" ? path[runEndIndex].X : path[runEndIndex].Y : ResolveDefaultSourceDepartureAxis(sourceNode, side); var currentBoundaryCoordinate = side is "left" or "right" ? path[0].Y : path[0].X; var peerCoordinates = CollectSharedLaneSourceBoundaryCoordinates( currentEdges, sourceNode, side, graphMinY, graphMaxY, edge.Id); foreach (var desiredCoordinate in EnumerateSharedLaneBoundaryRepairCoordinates( sourceNode, side, currentBoundaryCoordinate, minLineClearance, peerCoordinates)) { var candidatePath = BuildMixedSourceFaceCandidate(path, sourceNode, side, desiredCoordinate, axisValue); if (!IsValidSharedLaneBoundaryRepairCandidate( edge, path, candidatePath, sourceNode, isOutgoing: true, nodes, graphMinY, graphMaxY)) { continue; } var candidateEdges = currentEdges.ToArray(); candidateEdges[repairIndex] = BuildSingleSectionEdge(edge, candidatePath); if (TryAcceptFocusedSharedLanePairRepair( currentEdges, candidateEdges, repairIndex, edge, otherEdge, nodes, graphMinY, graphMaxY, out repairedEdge)) { return true; } } return false; } private static bool TryResolveSharedLaneByDirectNodeHandoffSlotRepair( ElkRoutedEdge[] currentEdges, int repairIndex, ElkRoutedEdge edge, ElkRoutedEdge otherEdge, ElkPositionedNode[] nodes, double minLineClearance, double graphMinY, double graphMaxY, out ElkRoutedEdge repairedEdge) { repairedEdge = edge; var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal); if (!TryResolveSharedLaneNodeHandoffContext(edge, otherEdge, nodesById, graphMinY, graphMaxY, out var context)) { return false; } var peerCoordinates = CollectSharedLaneNodeFaceBoundaryCoordinates( currentEdges, context.SharedNode, context.Side, graphMinY, graphMaxY, edge.Id); foreach (var desiredCoordinate in EnumerateSharedLaneBoundaryRepairCoordinates( context.SharedNode, context.Side, context.CurrentBoundaryCoordinate, minLineClearance, peerCoordinates)) { var candidatePath = context.IsOutgoing ? BuildMixedSourceFaceCandidate(context.Path, context.SharedNode, context.Side, desiredCoordinate, context.AxisValue) : BuildMixedTargetFaceCandidate(context.Path, context.SharedNode, context.Side, desiredCoordinate, context.AxisValue); if (!IsValidSharedLaneBoundaryRepairCandidate( edge, context.Path, candidatePath, context.SharedNode, context.IsOutgoing, nodes, graphMinY, graphMaxY)) { continue; } var candidateEdges = currentEdges.ToArray(); candidateEdges[repairIndex] = BuildSingleSectionEdge(edge, candidatePath); if (TryAcceptFocusedSharedLanePairRepair( currentEdges, candidateEdges, repairIndex, edge, otherEdge, nodes, graphMinY, graphMaxY, out repairedEdge)) { return true; } } return false; } private static bool TryResolveSharedLaneByFocusedSourceDepartureSpread( ElkRoutedEdge[] currentEdges, int repairIndex, ElkRoutedEdge edge, ElkRoutedEdge otherEdge, ElkPositionedNode[] nodes, double minLineClearance, double graphMinY, double graphMaxY, out ElkRoutedEdge repairedEdge) { repairedEdge = edge; if (string.IsNullOrWhiteSpace(edge.SourceNodeId) || !string.Equals(edge.SourceNodeId, otherEdge.SourceNodeId, StringComparison.Ordinal)) { return false; } var focusedIds = new[] { edge.Id, otherEdge.Id }; var candidateEdges = SpreadSourceDepartureJoins(currentEdges, nodes, minLineClearance, focusedIds); return TryAcceptFocusedSharedLanePairRepair( currentEdges, candidateEdges, repairIndex, edge, otherEdge, nodes, graphMinY, graphMaxY, out repairedEdge); } private static bool TryResolveSharedLaneByFocusedMixedNodeFaceRepair( ElkRoutedEdge[] currentEdges, int repairIndex, ElkRoutedEdge edge, ElkRoutedEdge otherEdge, ElkPositionedNode[] nodes, double minLineClearance, double graphMinY, double graphMaxY, out ElkRoutedEdge repairedEdge) { repairedEdge = edge; var sharesIncomingOutgoingNode = (!string.IsNullOrWhiteSpace(edge.TargetNodeId) && string.Equals(edge.TargetNodeId, otherEdge.SourceNodeId, StringComparison.Ordinal)) || (!string.IsNullOrWhiteSpace(edge.SourceNodeId) && string.Equals(edge.SourceNodeId, otherEdge.TargetNodeId, StringComparison.Ordinal)); if (!sharesIncomingOutgoingNode) { return false; } var focusedIds = new[] { edge.Id, otherEdge.Id }; var candidateEdges = SeparateMixedNodeFaceLaneConflicts(currentEdges, nodes, minLineClearance, focusedIds); return TryAcceptFocusedSharedLanePairRepair( currentEdges, candidateEdges, repairIndex, edge, otherEdge, nodes, graphMinY, graphMaxY, out repairedEdge); } private static bool TryAcceptFocusedSharedLanePairRepair( ElkRoutedEdge[] currentEdges, ElkRoutedEdge[] candidateEdges, int repairIndex, ElkRoutedEdge edge, ElkRoutedEdge otherEdge, ElkPositionedNode[] nodes, double graphMinY, double graphMaxY, out ElkRoutedEdge repairedEdge) { repairedEdge = edge; if (repairIndex < 0 || repairIndex >= candidateEdges.Length) { return false; } var candidateEdge = candidateEdges[repairIndex]; var currentPath = ExtractFullPath(edge); var candidatePath = ExtractFullPath(candidateEdge); if (!PathChanged(currentPath, candidatePath) || HasNodeObstacleCrossing(candidatePath, nodes, edge.SourceNodeId, edge.TargetNodeId) || SegmentLeavesGraphBand(candidatePath, graphMinY, graphMaxY)) { return false; } var currentSharedLaneConflicts = ElkEdgeRoutingScoring.DetectSharedLaneConflicts(currentEdges, nodes); var candidateSharedLaneConflicts = ElkEdgeRoutingScoring.DetectSharedLaneConflicts(candidateEdges, nodes); var currentSharedLaneCount = currentSharedLaneConflicts.Count; var candidateSharedLaneCount = candidateSharedLaneConflicts.Count; var currentEdgeSharedLaneCount = currentSharedLaneConflicts.Count(conflict => string.Equals(conflict.LeftEdgeId, edge.Id, StringComparison.Ordinal) || string.Equals(conflict.RightEdgeId, edge.Id, StringComparison.Ordinal)); var candidateEdgeSharedLaneCount = candidateSharedLaneConflicts.Count(conflict => string.Equals(conflict.LeftEdgeId, candidateEdge.Id, StringComparison.Ordinal) || string.Equals(conflict.RightEdgeId, candidateEdge.Id, StringComparison.Ordinal)); if (candidateSharedLaneCount > currentSharedLaneCount || candidateEdgeSharedLaneCount >= currentEdgeSharedLaneCount || ElkEdgeRoutingScoring.DetectSharedLaneConflicts([candidateEdge, otherEdge], nodes).Count > 0 || ComputeUnderNodeRepairLocalHardPressure(candidateEdge, nodes) > ComputeUnderNodeRepairLocalHardPressure(edge, nodes)) { return false; } repairedEdge = candidateEdge; return true; } private static IReadOnlyList CollectSharedLaneSourceBoundaryCoordinates( IReadOnlyCollection edges, ElkPositionedNode sourceNode, string side, double graphMinY, double graphMaxY, string excludeEdgeId) { var coordinates = new List(); foreach (var peerEdge in edges) { if (string.Equals(peerEdge.Id, excludeEdgeId, StringComparison.Ordinal) || !string.Equals(peerEdge.SourceNodeId, sourceNode.Id, StringComparison.Ordinal) || !ShouldSpreadSourceDeparture(peerEdge, graphMinY, graphMaxY)) { continue; } var peerPath = ExtractFullPath(peerEdge); if (peerPath.Count < 2) { continue; } var peerSide = ResolveSourceDepartureSide(peerPath, sourceNode); if (!string.Equals(peerSide, side, StringComparison.Ordinal)) { continue; } AddUniqueCoordinate(coordinates, side is "left" or "right" ? peerPath[0].Y : peerPath[0].X); } return coordinates .OrderBy(value => value) .ToArray(); } private static IReadOnlyList CollectSharedLaneNodeFaceBoundaryCoordinates( IReadOnlyCollection edges, ElkPositionedNode node, string side, double graphMinY, double graphMaxY, string excludeEdgeId) { var coordinates = new List(); foreach (var peerEdge in edges) { if (string.Equals(peerEdge.Id, excludeEdgeId, StringComparison.Ordinal)) { continue; } var peerPath = ExtractFullPath(peerEdge); if (peerPath.Count < 2) { continue; } if (string.Equals(peerEdge.SourceNodeId, node.Id, StringComparison.Ordinal) && ShouldSpreadSourceDeparture(peerEdge, graphMinY, graphMaxY)) { var peerSide = ResolveSourceDepartureSide(peerPath, node); if (string.Equals(peerSide, side, StringComparison.Ordinal)) { AddUniqueCoordinate(coordinates, side is "left" or "right" ? peerPath[0].Y : peerPath[0].X); } } if (string.Equals(peerEdge.TargetNodeId, node.Id, StringComparison.Ordinal) && ShouldSpreadTargetApproach(peerEdge, graphMinY, graphMaxY)) { var peerSide = ResolveTargetApproachSide(peerPath, node); if (string.Equals(peerSide, side, StringComparison.Ordinal)) { AddUniqueCoordinate(coordinates, side is "left" or "right" ? peerPath[^1].Y : peerPath[^1].X); } } } return coordinates .OrderBy(value => value) .ToArray(); } private static IEnumerable EnumerateSharedLaneBoundaryRepairCoordinates( ElkPositionedNode node, string side, double currentCoordinate, double minLineClearance, IReadOnlyList peerCoordinates) { var minSlot = side is "left" or "right" ? node.Y + 4d : node.X + 4d; var maxSlot = side is "left" or "right" ? node.Y + node.Height - 4d : node.X + node.Width - 4d; if (maxSlot <= minSlot + 0.5d) { yield break; } var sideLength = side is "left" or "right" ? Math.Max(8d, node.Height - 8d) : Math.Max(8d, node.Width - 8d); var spacing = ResolveBoundaryJoinSlotSpacing( minLineClearance, sideLength, Math.Max(2, peerCoordinates.Count + 1)); // Direct pair repairs do not need full group-spacing. Trying a closer // escape slot first keeps the edge away from the face corners and avoids // manufacturing new boundary-angle / join defects while still clearing // the shared-lane tolerance band. var repairSpacing = Math.Max( 12d, Math.Min( spacing, Math.Min(28d, minLineClearance * 0.45d))); var candidates = new List(); var sortedPeers = peerCoordinates .OrderBy(value => value) .ToArray(); AddUniqueCoordinate(candidates, Math.Max(minSlot, Math.Min(maxSlot, currentCoordinate - repairSpacing))); AddUniqueCoordinate(candidates, Math.Max(minSlot, Math.Min(maxSlot, currentCoordinate + repairSpacing))); AddUniqueCoordinate(candidates, Math.Max(minSlot, Math.Min(maxSlot, currentCoordinate - spacing))); AddUniqueCoordinate(candidates, Math.Max(minSlot, Math.Min(maxSlot, currentCoordinate + spacing))); AddUniqueCoordinate(candidates, minSlot); AddUniqueCoordinate(candidates, maxSlot); foreach (var peerCoordinate in sortedPeers) { AddUniqueCoordinate(candidates, Math.Max(minSlot, Math.Min(maxSlot, peerCoordinate - repairSpacing))); AddUniqueCoordinate(candidates, Math.Max(minSlot, Math.Min(maxSlot, peerCoordinate + repairSpacing))); AddUniqueCoordinate(candidates, Math.Max(minSlot, Math.Min(maxSlot, peerCoordinate - spacing))); AddUniqueCoordinate(candidates, Math.Max(minSlot, Math.Min(maxSlot, peerCoordinate + spacing))); } for (var i = 0; i < sortedPeers.Length - 1; i++) { var midpoint = (sortedPeers[i] + sortedPeers[i + 1]) / 2d; AddUniqueCoordinate(candidates, Math.Max(minSlot, Math.Min(maxSlot, midpoint))); } var laneTolerance = Math.Max(4d, Math.Min(12d, minLineClearance * 0.2d)); foreach (var coordinate in candidates .Where(value => Math.Abs(value - currentCoordinate) > 0.5d) .Select(value => new { Value = value, TooCloseToPeer = sortedPeers.Any(peer => Math.Abs(peer - value) <= laneTolerance + 0.5d), }) .OrderBy(item => item.TooCloseToPeer ? 1 : 0) .ThenBy(item => Math.Abs(item.Value - currentCoordinate)) .ThenBy(item => item.Value)) { yield return coordinate.Value; } } private static void AddUniquePathCandidate( ICollection> candidates, IReadOnlyList candidate) { if (candidates.Any(existing => existing.Count == candidate.Count && existing.Zip(candidate, ElkEdgeRoutingGeometry.PointsEqual).All(equal => equal))) { return; } candidates.Add(candidate); } private static bool IsBetterMixedNodeFaceCandidate( int candidateSharedLaneViolations, int candidateTargetJoinViolations, int candidateBoundaryAngleViolations, int candidateGatewaySourceExitViolations, int candidateUnderNodeViolations, double candidatePathLength, int currentSharedLaneViolations, int currentTargetJoinViolations, int currentBoundaryAngleViolations, int currentGatewaySourceExitViolations, int currentUnderNodeViolations, double currentPathLength) { if (candidateSharedLaneViolations != currentSharedLaneViolations) { return candidateSharedLaneViolations < currentSharedLaneViolations; } if (candidateTargetJoinViolations != currentTargetJoinViolations) { return candidateTargetJoinViolations < currentTargetJoinViolations; } if (candidateBoundaryAngleViolations != currentBoundaryAngleViolations) { return candidateBoundaryAngleViolations < currentBoundaryAngleViolations; } if (candidateGatewaySourceExitViolations != currentGatewaySourceExitViolations) { return candidateGatewaySourceExitViolations < currentGatewaySourceExitViolations; } if (candidateUnderNodeViolations != currentUnderNodeViolations) { return candidateUnderNodeViolations < currentUnderNodeViolations; } return candidatePathLength + 0.5d < currentPathLength; } private static bool TryResolveSharedLaneNodeHandoffContext( ElkRoutedEdge edge, ElkRoutedEdge otherEdge, IReadOnlyDictionary nodesById, double graphMinY, double graphMaxY, out (ElkPositionedNode SharedNode, string Side, bool IsOutgoing, IReadOnlyList Path, double CurrentBoundaryCoordinate, double AxisValue) context) { context = default; var path = ExtractFullPath(edge); var otherPath = ExtractFullPath(otherEdge); if (path.Count < 2 || otherPath.Count < 2) { return false; } if (!string.IsNullOrWhiteSpace(edge.TargetNodeId) && string.Equals(edge.TargetNodeId, otherEdge.SourceNodeId, StringComparison.Ordinal) && nodesById.TryGetValue(edge.TargetNodeId, out var incomingTargetNode) && ShouldSpreadTargetApproach(edge, graphMinY, graphMaxY) && ShouldSpreadSourceDeparture(otherEdge, graphMinY, graphMaxY)) { var incomingSide = ResolveTargetApproachSide(path, incomingTargetNode); var outgoingSide = ResolveSourceDepartureSide(otherPath, incomingTargetNode); if (string.Equals(incomingSide, outgoingSide, StringComparison.Ordinal)) { var axisValue = ResolveTargetApproachAxisValue(path, incomingSide); if (double.IsNaN(axisValue)) { axisValue = ResolveDefaultTargetApproachAxis(incomingTargetNode, incomingSide); } context = ( incomingTargetNode, incomingSide, IsOutgoing: false, path, incomingSide is "left" or "right" ? path[^1].Y : path[^1].X, axisValue); return true; } } if (!string.IsNullOrWhiteSpace(edge.SourceNodeId) && string.Equals(edge.SourceNodeId, otherEdge.TargetNodeId, StringComparison.Ordinal) && nodesById.TryGetValue(edge.SourceNodeId, out var outgoingSourceNode) && ShouldSpreadSourceDeparture(edge, graphMinY, graphMaxY) && ShouldSpreadTargetApproach(otherEdge, graphMinY, graphMaxY)) { var outgoingSide = ResolveSourceDepartureSide(path, outgoingSourceNode); var incomingSide = ResolveTargetApproachSide(otherPath, outgoingSourceNode); if (string.Equals(outgoingSide, incomingSide, StringComparison.Ordinal)) { var axisValue = TryExtractSourceDepartureRun(path, outgoingSide, out _, out var runEndIndex) ? outgoingSide is "left" or "right" ? path[runEndIndex].X : path[runEndIndex].Y : ResolveDefaultSourceDepartureAxis(outgoingSourceNode, outgoingSide); context = ( outgoingSourceNode, outgoingSide, IsOutgoing: true, path, outgoingSide is "left" or "right" ? path[0].Y : path[0].X, axisValue); return true; } } return false; } private static bool IsValidSharedLaneBoundaryRepairCandidate( ElkRoutedEdge edge, IReadOnlyList currentPath, IReadOnlyList candidatePath, ElkPositionedNode node, bool isOutgoing, IReadOnlyCollection nodes, double graphMinY, double graphMaxY) { if (!PathChanged(currentPath, candidatePath) || HasNodeObstacleCrossing(candidatePath, nodes, edge.SourceNodeId, edge.TargetNodeId) || SegmentLeavesGraphBand(candidatePath, graphMinY, graphMaxY)) { return false; } if (isOutgoing) { if (ElkShapeBoundaries.IsGatewayShape(node)) { return HasAcceptableGatewayBoundaryPath(candidatePath, nodes, edge.SourceNodeId, edge.TargetNodeId, node, fromStart: true); } return HasClearBoundarySegments(candidatePath, nodes, edge.SourceNodeId, edge.TargetNodeId, true, 2) && HasValidBoundaryAngle(candidatePath[0], candidatePath[1], node); } if (ElkShapeBoundaries.IsGatewayShape(node)) { return CanAcceptGatewayTargetRepair(candidatePath, node) && HasAcceptableGatewayBoundaryPath(candidatePath, nodes, edge.SourceNodeId, edge.TargetNodeId, node, fromStart: false); } return HasClearBoundarySegments(candidatePath, nodes, edge.SourceNodeId, edge.TargetNodeId, false, 4) && HasValidBoundaryAngle(candidatePath[^1], candidatePath[^2], node) && !HasTargetApproachBacktracking(candidatePath, node); } private static bool TryResolveSharedLaneByAlternateRepeatFace( ElkRoutedEdge edge, ElkRoutedEdge otherEdge, ElkPositionedNode[] nodes, double minLineClearance, double graphMinY, double graphMaxY, out ElkRoutedEdge repairedEdge) { repairedEdge = edge; if (!IsRepeatCollectorLabel(edge.Label)) { return false; } var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal); if (!nodesById.TryGetValue(edge.TargetNodeId ?? string.Empty, out var targetNode) || !nodesById.TryGetValue(otherEdge.SourceNodeId ?? string.Empty, out var sourceNode) || !string.Equals(targetNode.Id, sourceNode.Id, StringComparison.Ordinal) || ElkShapeBoundaries.IsGatewayShape(targetNode)) { return false; } var path = ExtractFullPath(edge); var otherPath = ExtractFullPath(otherEdge); if (path.Count < 2 || otherPath.Count < 2) { return false; } var incomingSide = ResolveTargetApproachSide(path, targetNode); var outgoingSide = ResolveSourceDepartureSide(otherPath, sourceNode); if (!string.Equals(incomingSide, outgoingSide, StringComparison.Ordinal)) { return false; } var axisValue = ResolveTargetApproachAxisValue(path, incomingSide); if (double.IsNaN(axisValue)) { axisValue = incomingSide is "left" or "right" ? path[^1].Y : path[^1].X; } var incomingEntry = ( Index: 0, Edge: edge, Path: (IReadOnlyList)path, Node: targetNode, Side: incomingSide, IsOutgoing: false, Boundary: path[^1], BoundaryCoordinate: incomingSide is "left" or "right" ? path[^1].Y : path[^1].X, AxisValue: axisValue); if (!TryBuildAlternateMixedFaceCandidate(incomingEntry, nodes, minLineClearance, out var candidate) || !PathChanged(path, candidate) || HasNodeObstacleCrossing(candidate, nodes, edge.SourceNodeId, edge.TargetNodeId) || SegmentLeavesGraphBand(candidate, graphMinY, graphMaxY) || !HasClearBoundarySegments(candidate, nodes, edge.SourceNodeId, edge.TargetNodeId, false, 4) || !HasValidBoundaryAngle(candidate[^1], candidate[^2], targetNode)) { return false; } repairedEdge = BuildSingleSectionEdge(edge, candidate); var repairedPath = ExtractFullPath(repairedEdge); if (!HasNodeObstacleCrossing(repairedPath, nodes, edge.SourceNodeId, edge.TargetNodeId) && !SegmentLeavesGraphBand(repairedPath, graphMinY, graphMaxY) && ElkEdgeRoutingScoring.DetectSharedLaneConflicts([repairedEdge, otherEdge], nodes).Count == 0) { return true; } repairedEdge = RepairBoundaryAnglesAndTargetApproaches( [repairedEdge], nodes, minLineClearance)[0]; repairedEdge = NormalizeSourceExitAngles([repairedEdge], nodes)[0]; repairedPath = ExtractFullPath(repairedEdge); if (HasNodeObstacleCrossing(repairedPath, nodes, edge.SourceNodeId, edge.TargetNodeId) || SegmentLeavesGraphBand(repairedPath, graphMinY, graphMaxY) || ElkEdgeRoutingScoring.DetectSharedLaneConflicts([repairedEdge, otherEdge], nodes).Count > 0) { repairedEdge = edge; return false; } return true; } internal static ElkRoutedEdge[] FinalizeGatewayBoundaryGeometry( ElkRoutedEdge[] edges, ElkPositionedNode[] nodes, IReadOnlyCollection? restrictedEdgeIds = null) { if (edges.Length == 0 || nodes.Length == 0) { return edges; } var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal); var graphMinY = nodes.Min(node => node.Y); var graphMaxY = nodes.Max(node => node.Y + node.Height); var restrictedSet = restrictedEdgeIds is null ? null : restrictedEdgeIds.ToHashSet(StringComparer.Ordinal); var result = new ElkRoutedEdge[edges.Length]; for (var i = 0; i < edges.Length; i++) { var edge = edges[i]; if (restrictedSet is not null && !restrictedSet.Contains(edge.Id)) { result[i] = edge; continue; } var path = ExtractFullPath(edge); if (path.Count < 2) { result[i] = edge; continue; } var normalized = path .Select(point => new ElkPoint { X = point.X, Y = point.Y }) .ToList(); var changed = false; var preserveSourceExit = ShouldPreserveSourceExitGeometry(edge, graphMinY, graphMaxY); if (nodesById.TryGetValue(edge.SourceNodeId ?? string.Empty, out var sourceNode) && ElkShapeBoundaries.IsGatewayShape(sourceNode) && NeedsGatewaySourceBoundaryRepair(normalized, sourceNode)) { var sourceRepaired = preserveSourceExit ? RepairProtectedGatewaySourceBoundaryPath(normalized, sourceNode, graphMinY, graphMaxY) : RepairGatewaySourceBoundaryPath( normalized, sourceNode, nodes, edge.SourceNodeId, edge.TargetNodeId); if (PathChanged(normalized, sourceRepaired) && HasAcceptableGatewayBoundaryPath(sourceRepaired, nodes, edge.SourceNodeId, edge.TargetNodeId, sourceNode, fromStart: true)) { normalized = sourceRepaired; changed = true; } } if (nodesById.TryGetValue(edge.TargetNodeId ?? string.Empty, out var targetNode) && ElkShapeBoundaries.IsGatewayShape(targetNode) && NeedsGatewayTargetBoundaryRepair(normalized, targetNode)) { var targetRepaired = NormalizeGatewayEntryPath(normalized, targetNode, normalized[^1]); if (PathChanged(normalized, targetRepaired) && CanAcceptGatewayTargetRepair(targetRepaired, targetNode)) { normalized = targetRepaired; changed = true; } } if (nodesById.TryGetValue(edge.SourceNodeId ?? string.Empty, out sourceNode) && ElkShapeBoundaries.IsGatewayShape(sourceNode) && NeedsGatewaySourceBoundaryRepair(normalized, sourceNode)) { var sourceRepaired = preserveSourceExit ? RepairProtectedGatewaySourceBoundaryPath(normalized, sourceNode, graphMinY, graphMaxY) : RepairGatewaySourceBoundaryPath( normalized, sourceNode, nodes, edge.SourceNodeId, edge.TargetNodeId); if (PathChanged(normalized, sourceRepaired) && HasAcceptableGatewayBoundaryPath(sourceRepaired, nodes, edge.SourceNodeId, edge.TargetNodeId, sourceNode, fromStart: true)) { normalized = sourceRepaired; changed = true; } } if (nodesById.TryGetValue(edge.TargetNodeId ?? string.Empty, out targetNode) && !ElkShapeBoundaries.IsGatewayShape(targetNode) && normalized.Count >= 2 && !HasValidBoundaryAngle(normalized[^1], normalized[^2], targetNode)) { var targetSide = ElkEdgeRoutingGeometry.ResolveBoundarySide(normalized[^1], targetNode); var targetRepaired = NormalizeEntryPath(normalized, targetNode, targetSide); if (HasClearBoundarySegments(targetRepaired, nodes, edge.SourceNodeId, edge.TargetNodeId, false, 3)) { normalized = targetRepaired; changed = true; } } if (nodesById.TryGetValue(edge.TargetNodeId ?? string.Empty, out targetNode) && !ElkShapeBoundaries.IsGatewayShape(targetNode) && HasTargetApproachBacktracking(normalized, targetNode) && TryNormalizeNonGatewayBacktrackingEntry(normalized, targetNode, out var backtrackingRepair)) { normalized = backtrackingRepair; changed = true; } if (nodesById.TryGetValue(edge.TargetNodeId ?? string.Empty, out targetNode) && ElkShapeBoundaries.IsGatewayShape(targetNode) && NeedsGatewayTargetBoundaryRepair(normalized, targetNode)) { var targetRepaired = NormalizeGatewayEntryPath(normalized, targetNode, normalized[^1]); if (PathChanged(normalized, targetRepaired) && CanAcceptGatewayTargetRepair(targetRepaired, targetNode)) { normalized = targetRepaired; changed = true; } } if (nodesById.TryGetValue(edge.SourceNodeId ?? string.Empty, out sourceNode) && ElkShapeBoundaries.IsGatewayShape(sourceNode)) { if (preserveSourceExit) { var protectedExitFixed = TryBuildProtectedGatewaySourcePath( normalized, sourceNode, nodes, edge.SourceNodeId, edge.TargetNodeId); if (PathChanged(normalized, protectedExitFixed) && HasAcceptableGatewayBoundaryPath(protectedExitFixed, nodes, edge.SourceNodeId, edge.TargetNodeId, sourceNode, fromStart: true) && !HasGatewaySourceExitBacktracking(protectedExitFixed) && !HasGatewaySourceExitCurl(protectedExitFixed)) { normalized = protectedExitFixed; changed = true; } } else { var directExitFixed = TryBuildDirectGatewaySourcePath( normalized, sourceNode, nodes, edge.SourceNodeId, edge.TargetNodeId); if (PathChanged(normalized, directExitFixed) && HasAcceptableGatewayBoundaryPath(directExitFixed, nodes, edge.SourceNodeId, edge.TargetNodeId, sourceNode, fromStart: true) && !HasGatewaySourceExitBacktracking(directExitFixed) && !HasGatewaySourceExitCurl(directExitFixed) && !HasGatewaySourceDominantAxisDetour(directExitFixed, sourceNode) && !HasGatewaySourcePreferredFaceMismatch(directExitFixed, sourceNode)) { normalized = directExitFixed; changed = true; } if (sourceNode.Kind == "Decision") { var diagonalExitFixed = ForceDecisionDiagonalSourceExit( normalized, sourceNode, nodes, edge.SourceNodeId, edge.TargetNodeId); if (PathChanged(normalized, diagonalExitFixed) && HasAcceptableGatewayBoundaryPath(diagonalExitFixed, nodes, edge.SourceNodeId, edge.TargetNodeId, sourceNode, fromStart: true) && HasClearBoundarySegments(diagonalExitFixed, nodes, edge.SourceNodeId, edge.TargetNodeId, true, Math.Min(4, diagonalExitFixed.Count - 1)) && !HasGatewaySourceExitBacktracking(diagonalExitFixed) && !HasGatewaySourceExitCurl(diagonalExitFixed)) { normalized = diagonalExitFixed; changed = true; } } var faceFixed = FixGatewaySourcePreferredFace(normalized, sourceNode); if (PathChanged(normalized, faceFixed) && HasAcceptableGatewayBoundaryPath(faceFixed, nodes, edge.SourceNodeId, edge.TargetNodeId, sourceNode, fromStart: true)) { normalized = faceFixed; changed = true; } var curlFixed = FixGatewaySourceExitCurl(normalized, sourceNode); if (PathChanged(normalized, curlFixed) && HasAcceptableGatewayBoundaryPath(curlFixed, nodes, edge.SourceNodeId, edge.TargetNodeId, sourceNode, fromStart: true) && !HasGatewaySourceExitCurl(curlFixed)) { normalized = curlFixed; changed = true; } var dominantAxisFixed = FixGatewaySourceDominantAxisDetour(normalized, sourceNode); if (PathChanged(normalized, dominantAxisFixed) && HasAcceptableGatewayBoundaryPath(dominantAxisFixed, nodes, edge.SourceNodeId, edge.TargetNodeId, sourceNode, fromStart: true)) { normalized = dominantAxisFixed; changed = true; } if (HasGatewaySourcePreferredFaceMismatch(normalized, sourceNode)) { var forceAligned = ForceGatewaySourcePreferredFaceAlignment(normalized, sourceNode); if (PathChanged(normalized, forceAligned) && HasAcceptableGatewayBoundaryPath(forceAligned, nodes, edge.SourceNodeId, edge.TargetNodeId, sourceNode, fromStart: true)) { normalized = forceAligned; changed = true; } } var finalDirectExit = TryBuildDirectGatewaySourcePath( normalized, sourceNode, nodes, edge.SourceNodeId, edge.TargetNodeId); if (PathChanged(normalized, finalDirectExit) && HasAcceptableGatewayBoundaryPath(finalDirectExit, nodes, edge.SourceNodeId, edge.TargetNodeId, sourceNode, fromStart: true) && !HasGatewaySourceExitBacktracking(finalDirectExit) && !HasGatewaySourceExitCurl(finalDirectExit) && !HasGatewaySourceDominantAxisDetour(finalDirectExit, sourceNode) && !HasGatewaySourcePreferredFaceMismatch(finalDirectExit, sourceNode)) { normalized = finalDirectExit; changed = true; } } } if (nodesById.TryGetValue(edge.TargetNodeId ?? string.Empty, out targetNode) && !ElkShapeBoundaries.IsGatewayShape(targetNode) && normalized.Count >= 2 && !HasValidBoundaryAngle(normalized[^1], normalized[^2], targetNode)) { var targetSide = ElkEdgeRoutingGeometry.ResolveBoundarySide(normalized[^1], targetNode); var targetRepaired = NormalizeEntryPath(normalized, targetNode, targetSide); if (HasClearBoundarySegments(targetRepaired, nodes, edge.SourceNodeId, edge.TargetNodeId, false, 3)) { normalized = targetRepaired; changed = true; } } if (nodesById.TryGetValue(edge.TargetNodeId ?? string.Empty, out targetNode) && ElkShapeBoundaries.IsGatewayShape(targetNode) && normalized.Count >= 2 && !HasValidBoundaryAngle(normalized[^1], normalized[^2], targetNode)) { var lateTargetRepair = NormalizeGatewayEntryPath(normalized, targetNode, normalized[^1]); if (PathChanged(normalized, lateTargetRepair) && CanAcceptGatewayTargetRepair(lateTargetRepair, targetNode)) { normalized = lateTargetRepair; changed = true; } else if (targetNode.Kind == "Decision") { var directDecisionRepair = ForceDecisionDirectTargetEntry(normalized, targetNode); if (PathChanged(normalized, directDecisionRepair) && CanAcceptGatewayTargetRepair(directDecisionRepair, targetNode)) { normalized = directDecisionRepair; changed = true; } } } if (nodesById.TryGetValue(edge.TargetNodeId ?? string.Empty, out targetNode) && ElkShapeBoundaries.IsGatewayShape(targetNode) && normalized.Count >= 2 && !HasValidBoundaryAngle(normalized[^1], normalized[^2], targetNode)) { var forcedGatewayStub = ForceGatewayTargetBoundaryStub(normalized, targetNode); if (PathChanged(normalized, forcedGatewayStub) && CanAcceptGatewayTargetRepair(forcedGatewayStub, targetNode)) { normalized = forcedGatewayStub; changed = true; } } if (nodesById.TryGetValue(edge.SourceNodeId ?? string.Empty, out sourceNode) && ElkShapeBoundaries.IsGatewayShape(sourceNode)) { var lateSourceRepaired = RepairGatewaySourceBoundaryPath( normalized, sourceNode, nodes, edge.SourceNodeId, edge.TargetNodeId); if (PathChanged(normalized, lateSourceRepaired) && HasAcceptableGatewayBoundaryPath(lateSourceRepaired, nodes, edge.SourceNodeId, edge.TargetNodeId, sourceNode, fromStart: true) && !HasGatewaySourceExitBacktracking(lateSourceRepaired) && !HasGatewaySourceExitCurl(lateSourceRepaired) && !HasGatewaySourceDominantAxisDetour(lateSourceRepaired, sourceNode) && !HasGatewaySourcePreferredFaceMismatch(lateSourceRepaired, sourceNode)) { normalized = lateSourceRepaired; changed = true; } } if (nodesById.TryGetValue(edge.TargetNodeId ?? string.Empty, out targetNode)) { if (ElkShapeBoundaries.IsGatewayShape(targetNode)) { if (NeedsGatewayTargetBoundaryRepair(normalized, targetNode)) { var finalGatewayTargetRepair = NormalizeGatewayEntryPath(normalized, targetNode, normalized[^1]); if (PathChanged(normalized, finalGatewayTargetRepair) && CanAcceptGatewayTargetRepair(finalGatewayTargetRepair, targetNode)) { normalized = finalGatewayTargetRepair; changed = true; } else if (targetNode.Kind == "Decision") { var directDecisionRepair = ForceDecisionDirectTargetEntry(normalized, targetNode); if (PathChanged(normalized, directDecisionRepair) && CanAcceptGatewayTargetRepair(directDecisionRepair, targetNode)) { normalized = directDecisionRepair; changed = true; } } } } else if (normalized.Count >= 2) { if (!HasValidBoundaryAngle(normalized[^1], normalized[^2], targetNode)) { var finalTargetSide = ElkEdgeRoutingGeometry.ResolveBoundarySide(normalized[^1], targetNode); var finalTargetRepair = NormalizeEntryPath(normalized, targetNode, finalTargetSide); if (HasClearBoundarySegments(finalTargetRepair, nodes, edge.SourceNodeId, edge.TargetNodeId, false, 3)) { normalized = finalTargetRepair; changed = true; } } if (HasTargetApproachBacktracking(normalized, targetNode) && TryNormalizeNonGatewayBacktrackingEntry(normalized, targetNode, out var finalBacktrackingRepair) && HasClearBoundarySegments(finalBacktrackingRepair, nodes, edge.SourceNodeId, edge.TargetNodeId, false, 3) && HasValidBoundaryAngle(finalBacktrackingRepair[^1], finalBacktrackingRepair[^2], targetNode)) { normalized = finalBacktrackingRepair; changed = true; } } } if (nodesById.TryGetValue(edge.SourceNodeId ?? string.Empty, out sourceNode) && ElkShapeBoundaries.IsGatewayShape(sourceNode)) { var finalSourceRepair = EnforceGatewaySourceExitQuality( normalized, sourceNode, nodes, edge.SourceNodeId, edge.TargetNodeId); if (PathChanged(normalized, finalSourceRepair)) { normalized = finalSourceRepair; changed = true; } } if (nodesById.TryGetValue(edge.SourceNodeId ?? string.Empty, out sourceNode) && ElkShapeBoundaries.IsGatewayShape(sourceNode) && nodesById.TryGetValue(edge.TargetNodeId ?? string.Empty, out targetNode) && !ElkShapeBoundaries.IsGatewayShape(targetNode) && normalized.Count >= 2 && TryRealignNonGatewayTargetBoundarySlot( normalized, targetNode, nodes, edge.SourceNodeId, edge.TargetNodeId, out var finalSourceDrivenTargetRepair) && PathChanged(normalized, finalSourceDrivenTargetRepair)) { normalized = finalSourceDrivenTargetRepair; changed = true; } result[i] = changed ? BuildSingleSectionEdge(edge, normalized) : edge; } return result; } private static List FixGatewaySourcePreferredFace( IReadOnlyList sourcePath, ElkPositionedNode sourceNode) { if (!HasGatewaySourcePreferredFaceMismatch(sourcePath, sourceNode)) { return sourcePath.Select(point => new ElkPoint { X = point.X, Y = point.Y }).ToList(); } var path = sourcePath .Select(point => new ElkPoint { X = point.X, Y = point.Y }) .ToList(); var firstExteriorIndex = FindFirstGatewayExteriorPointIndex(path, sourceNode); var continuationIndex = FindGatewaySourceCurlRecoveryIndex(path, firstExteriorIndex) ?? FindPreferredGatewayExitContinuationIndex(path, sourceNode, firstExteriorIndex); var continuationPoint = path[continuationIndex]; if (!TryResolvePreferredGatewaySourceBoundary(sourceNode, continuationPoint, path[^1], out var preferredBoundary)) { return path; } return BuildGatewaySourceRepairPath( path, sourceNode, preferredBoundary, continuationPoint, continuationIndex, path[^1]); } private static bool HasGatewaySourcePreferredFaceMismatch( IReadOnlyList path, ElkPositionedNode sourceNode) { if (!ElkShapeBoundaries.IsGatewayShape(sourceNode) || path.Count < 2) { return false; } var centerX = sourceNode.X + (sourceNode.Width / 2d); var centerY = sourceNode.Y + (sourceNode.Height / 2d); var desiredDx = path[^1].X - centerX; var desiredDy = path[^1].Y - centerY; var boundaryDx = path[0].X - centerX; var boundaryDy = path[0].Y - centerY; if (Math.Abs(desiredDx) >= Math.Abs(desiredDy) * 1.25d) { return Math.Sign(boundaryDx) != Math.Sign(desiredDx) || Math.Abs(boundaryDy) > sourceNode.Height * 0.28d; } if (Math.Abs(desiredDy) >= Math.Abs(desiredDx) * 1.25d) { return Math.Sign(boundaryDy) != Math.Sign(desiredDy) || Math.Abs(boundaryDx) > sourceNode.Width * 0.28d; } return false; } private static List FixGatewaySourceExitCurl( IReadOnlyList sourcePath, ElkPositionedNode sourceNode) { if (!ElkShapeBoundaries.IsGatewayShape(sourceNode) || sourcePath.Count < 3) { return sourcePath.Select(point => new ElkPoint { X = point.X, Y = point.Y }).ToList(); } var sample = sourcePath .Take(Math.Min(sourcePath.Count, 6)) .ToArray(); var desiredDx = sourcePath[^1].X - sourcePath[0].X; var desiredDy = sourcePath[^1].Y - sourcePath[0].Y; if (!HasGatewaySourceExitCurl(sample)) { return sourcePath.Select(point => new ElkPoint { X = point.X, Y = point.Y }).ToList(); } var path = sourcePath .Select(point => new ElkPoint { X = point.X, Y = point.Y }) .ToList(); var firstExteriorIndex = FindFirstGatewayExteriorPointIndex(path, sourceNode); var continuationIndex = FindGatewaySourceCurlRecoveryIndex(path, firstExteriorIndex) ?? FindPreferredGatewayExitContinuationIndex(path, sourceNode, firstExteriorIndex); var continuationPoint = path[continuationIndex]; var boundary = sourceNode.Kind == "Decision" ? ResolveDecisionSourceExitBoundary(sourceNode, continuationPoint, continuationPoint) : ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary( sourceNode, ElkShapeBoundaries.ProjectOntoShapeBoundary(sourceNode, continuationPoint), continuationPoint); var continuationAligned = BuildGatewaySourceRepairPath( path, sourceNode, boundary, continuationPoint, continuationIndex, continuationPoint); if (PathChanged(path, continuationAligned) && !HasGatewaySourceExitBacktracking(continuationAligned) && !HasGatewaySourceExitCurl(continuationAligned)) { return continuationAligned; } var collapsedCurl = TryBuildGatewaySourceDominantAxisShortcut(path, sourceNode, path[0]); if (collapsedCurl is not null && PathChanged(path, collapsedCurl) && !HasGatewaySourceExitBacktracking(collapsedCurl) && !HasGatewaySourceExitCurl(collapsedCurl)) { return collapsedCurl; } const double axisTolerance = 4d; var rebuilt = path; var dominantHorizontal = Math.Abs(desiredDx) >= Math.Abs(desiredDy) * 1.15d && Math.Sign(desiredDx) != 0; var dominantVertical = Math.Abs(desiredDy) >= Math.Abs(desiredDx) * 1.15d && Math.Sign(desiredDy) != 0; if (dominantHorizontal && Math.Abs(rebuilt[1].Y - rebuilt[0].Y) <= axisTolerance) { rebuilt[1] = new ElkPoint { X = rebuilt[1].X, Y = rebuilt[0].Y, }; } else if (dominantVertical && Math.Abs(rebuilt[1].X - rebuilt[0].X) <= axisTolerance) { rebuilt[1] = new ElkPoint { X = rebuilt[0].X, Y = rebuilt[1].Y, }; } return NormalizePathPoints(rebuilt); } private static List FixGatewaySourceDominantAxisDetour( IReadOnlyList sourcePath, ElkPositionedNode sourceNode) { var path = sourcePath .Select(point => new ElkPoint { X = point.X, Y = point.Y }) .ToList(); if (!HasGatewaySourceDominantAxisDetour(path, sourceNode)) { return path; } var boundary = path[0]; var desiredDx = path[^1].X - boundary.X; var desiredDy = path[^1].Y - boundary.Y; var dominantHorizontal = Math.Abs(desiredDx) >= Math.Abs(desiredDy) * 1.15d && Math.Sign(desiredDx) != 0; var dominantVertical = Math.Abs(desiredDy) >= Math.Abs(desiredDx) * 1.15d && Math.Sign(desiredDy) != 0; var firstExteriorIndex = FindFirstGatewayExteriorPointIndex(path, sourceNode); var boundaryReferencePoint = path[firstExteriorIndex]; if (!TryResolvePreferredGatewaySourceBoundary(sourceNode, boundaryReferencePoint, path[^1], out var preferredBoundary)) { return path; } var localContinuationPoint = path[firstExteriorIndex]; var localRepair = new List { preferredBoundary }; if (!ElkEdgeRoutingGeometry.PointsEqual(localRepair[^1], localContinuationPoint)) { AppendGatewayOrthogonalCorner( localRepair, localRepair[^1], localContinuationPoint, firstExteriorIndex + 1 < path.Count ? path[firstExteriorIndex + 1] : null, preferHorizontalFromReference: ShouldPreferHorizontalGatewayExit(localRepair[^1], localContinuationPoint)); if (!ElkEdgeRoutingGeometry.PointsEqual(localRepair[^1], localContinuationPoint)) { localRepair.Add(localContinuationPoint); } } for (var i = firstExteriorIndex + 1; i < path.Count; i++) { localRepair.Add(path[i]); } localRepair = NormalizePathPoints(localRepair); if (PathChanged(path, localRepair) && !HasGatewaySourceExitBacktracking(localRepair) && !HasGatewaySourceExitCurl(localRepair) && !HasGatewaySourceDominantAxisDetour(localRepair, sourceNode) && !HasGatewaySourcePreferredFaceMismatch(localRepair, sourceNode)) { return localRepair; } var dominantAxisShortcut = TryBuildGatewaySourceDominantAxisShortcut(path, sourceNode, preferredBoundary); if (dominantAxisShortcut is not null && PathChanged(path, dominantAxisShortcut) && !HasGatewaySourceExitBacktracking(dominantAxisShortcut) && !HasGatewaySourceExitCurl(dominantAxisShortcut) && !HasGatewaySourceDominantAxisDetour(dominantAxisShortcut, sourceNode) && !HasGatewaySourcePreferredFaceMismatch(dominantAxisShortcut, sourceNode)) { return dominantAxisShortcut; } var preferredContinuationIndex = FindPreferredGatewayExitContinuationIndex(path, sourceNode, firstExteriorIndex); var candidateContinuationIndices = new[] { firstExteriorIndex, Math.Min(path.Count - 1, firstExteriorIndex + 1), Math.Min(path.Count - 1, firstExteriorIndex + 2), preferredContinuationIndex, } .Distinct() .Where(index => index >= firstExteriorIndex && index < path.Count) .ToArray(); List? bestCandidate = null; var bestScore = double.PositiveInfinity; foreach (var continuationIndex in candidateContinuationIndices) { var continuationCandidates = new List { path[continuationIndex], }; if (dominantHorizontal) { AddUniquePoint( continuationCandidates, new ElkPoint { X = path[continuationIndex].X, Y = preferredBoundary.Y, }); } else if (dominantVertical) { AddUniquePoint( continuationCandidates, new ElkPoint { X = preferredBoundary.X, Y = path[continuationIndex].Y, }); } foreach (var continuationPoint in continuationCandidates) { var candidate = BuildGatewaySourceRepairPath( path, sourceNode, preferredBoundary, continuationPoint, continuationIndex, continuationPoint); if (!PathChanged(path, candidate)) { continue; } var score = ComputePathLength(candidate); if (!ElkEdgeRoutingGeometry.PointsEqual(continuationPoint, path[continuationIndex])) { score -= 18d; } if (HasGatewaySourceExitBacktracking(candidate) || HasGatewaySourceExitCurl(candidate)) { score += 100_000d; } if (HasGatewaySourceDominantAxisDetour(candidate, sourceNode)) { score += 50_000d; } if (HasGatewaySourcePreferredFaceMismatch(candidate, sourceNode)) { score += 25_000d; } if (score >= bestScore) { continue; } bestScore = score; bestCandidate = candidate; } } return bestCandidate ?? path; } private static List? TryBuildGatewaySourceDominantAxisShortcut( IReadOnlyList path, ElkPositionedNode sourceNode, ElkPoint preferredBoundary) { if (path.Count < 4) { return null; } var desiredDx = path[^1].X - preferredBoundary.X; var desiredDy = path[^1].Y - preferredBoundary.Y; var dominantHorizontal = Math.Abs(desiredDx) >= Math.Abs(desiredDy) * 1.15d && Math.Sign(desiredDx) != 0; var dominantVertical = Math.Abs(desiredDy) >= Math.Abs(desiredDx) * 1.15d && Math.Sign(desiredDy) != 0; if (!dominantHorizontal && !dominantVertical) { return null; } List? bestCandidate = null; var bestLength = double.PositiveInfinity; var maxShortcutIndex = Math.Min(path.Count - 2, 3); for (var shortcutIndex = 1; shortcutIndex <= maxShortcutIndex; shortcutIndex++) { var shortcutAnchor = path[shortcutIndex]; var shortcutPoint = dominantHorizontal ? new ElkPoint { X = shortcutAnchor.X, Y = preferredBoundary.Y } : new ElkPoint { X = preferredBoundary.X, Y = shortcutAnchor.Y }; if (ElkEdgeRoutingGeometry.PointsEqual(preferredBoundary, shortcutPoint)) { continue; } var candidate = new List { preferredBoundary, shortcutPoint }; for (var i = shortcutIndex + 1; i < path.Count; i++) { candidate.Add(path[i]); } candidate = NormalizePathPoints(candidate); if (HasGatewaySourceExitBacktracking(candidate) || HasGatewaySourceExitCurl(candidate) || HasGatewaySourceDominantAxisDetour(candidate, sourceNode) || HasGatewaySourcePreferredFaceMismatch(candidate, sourceNode)) { continue; } var length = ComputePathLength(candidate); if (length >= bestLength) { continue; } bestLength = length; bestCandidate = candidate; } return bestCandidate; } private static bool HasGatewaySourceDominantAxisDetour( IReadOnlyList path, ElkPositionedNode sourceNode) { if (!ElkShapeBoundaries.IsGatewayShape(sourceNode) || path.Count < 3) { return false; } const double coordinateTolerance = 0.5d; var centerX = sourceNode.X + (sourceNode.Width / 2d); var centerY = sourceNode.Y + (sourceNode.Height / 2d); var desiredDx = path[^1].X - centerX; var desiredDy = path[^1].Y - centerY; var dominantHorizontal = Math.Abs(desiredDx) >= Math.Abs(desiredDy) * 1.25d && Math.Sign(desiredDx) != 0; var dominantVertical = Math.Abs(desiredDy) >= Math.Abs(desiredDx) * 1.25d && Math.Sign(desiredDy) != 0; if (!dominantHorizontal && !dominantVertical) { return false; } var boundary = path[0]; var adjacent = path[1]; var firstDx = adjacent.X - boundary.X; var firstDy = adjacent.Y - boundary.Y; if (dominantHorizontal) { if (Math.Sign(firstDx) != Math.Sign(desiredDx) || Math.Abs(firstDx) <= coordinateTolerance) { return true; } if (Math.Abs(firstDy) > Math.Max(12d, Math.Abs(firstDx) + 6d)) { return true; } return Math.Abs(firstDy) > Math.Max(24d, Math.Abs(desiredDy) + 12d) && Math.Abs(firstDy) > Math.Abs(firstDx) * 1.25d; } if (Math.Sign(firstDy) != Math.Sign(desiredDy) || Math.Abs(firstDy) <= coordinateTolerance) { return true; } if (Math.Abs(firstDx) > Math.Max(12d, Math.Abs(firstDy) + 6d)) { return true; } return Math.Abs(firstDx) > Math.Max(24d, Math.Abs(desiredDx) + 12d) && Math.Abs(firstDx) > Math.Abs(firstDy) * 1.25d; } private static List ForceGatewaySourcePreferredFaceAlignment( IReadOnlyList sourcePath, ElkPositionedNode sourceNode) { var path = sourcePath .Select(point => new ElkPoint { X = point.X, Y = point.Y }) .ToList(); if (!ElkShapeBoundaries.IsGatewayShape(sourceNode) || path.Count < 2) { return path; } var centerX = sourceNode.X + (sourceNode.Width / 2d); var centerY = sourceNode.Y + (sourceNode.Height / 2d); var desiredDx = path[^1].X - centerX; var desiredDy = path[^1].Y - centerY; var dominantHorizontal = Math.Abs(desiredDx) >= Math.Abs(desiredDy) * 1.25d && Math.Sign(desiredDx) != 0; var dominantVertical = Math.Abs(desiredDy) >= Math.Abs(desiredDx) * 1.25d && Math.Sign(desiredDy) != 0; if (!dominantHorizontal && !dominantVertical) { return path; } var preferredSide = dominantHorizontal ? desiredDx >= 0d ? "right" : "left" : desiredDy >= 0d ? "bottom" : "top"; var slotCoordinate = dominantHorizontal ? centerY + Math.Clamp(path[^1].Y - centerY, -sourceNode.Height * 0.18d, sourceNode.Height * 0.18d) : centerX + Math.Clamp(path[^1].X - centerX, -sourceNode.Width * 0.18d, sourceNode.Width * 0.18d); if (!ElkShapeBoundaries.TryProjectGatewayBoundarySlot(sourceNode, preferredSide, slotCoordinate, out var preferredBoundary)) { return path; } preferredBoundary = ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(sourceNode, preferredBoundary, path[^1]); var firstExteriorIndex = FindFirstGatewayExteriorPointIndex(path, sourceNode); var continuationPoint = path[firstExteriorIndex]; var adjacentPoint = ElkShapeBoundaries.BuildPreferredGatewaySourceExteriorPoint(sourceNode, preferredBoundary, continuationPoint); if (dominantHorizontal) { var candidateX = continuationPoint.X; if (Math.Sign(candidateX - preferredBoundary.X) != Math.Sign(desiredDx) || Math.Abs(candidateX - preferredBoundary.X) <= 0.5d) { candidateX = desiredDx >= 0d ? sourceNode.X + sourceNode.Width + 8d : sourceNode.X - 8d; } adjacentPoint = new ElkPoint { X = candidateX, Y = preferredBoundary.Y, }; } else if (dominantVertical) { var candidateY = continuationPoint.Y; if (Math.Sign(candidateY - preferredBoundary.Y) != Math.Sign(desiredDy) || Math.Abs(candidateY - preferredBoundary.Y) <= 0.5d) { candidateY = desiredDy >= 0d ? sourceNode.Y + sourceNode.Height + 8d : sourceNode.Y - 8d; } adjacentPoint = new ElkPoint { X = preferredBoundary.X, Y = candidateY, }; } if (ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(sourceNode, adjacentPoint)) { adjacentPoint = ElkShapeBoundaries.BuildPreferredGatewaySourceExteriorPoint(sourceNode, preferredBoundary, continuationPoint); } var rebuilt = new List { preferredBoundary }; if (!ElkEdgeRoutingGeometry.PointsEqual(rebuilt[^1], adjacentPoint)) { rebuilt.Add(adjacentPoint); } if (!ElkEdgeRoutingGeometry.PointsEqual(rebuilt[^1], continuationPoint)) { AppendGatewayOrthogonalCorner( rebuilt, rebuilt[^1], continuationPoint, firstExteriorIndex + 1 < path.Count ? path[firstExteriorIndex + 1] : null, preferHorizontalFromReference: ShouldPreferHorizontalGatewayExit(rebuilt[^1], continuationPoint)); if (!ElkEdgeRoutingGeometry.PointsEqual(rebuilt[^1], continuationPoint)) { rebuilt.Add(continuationPoint); } } for (var i = firstExteriorIndex + 1; i < path.Count; i++) { rebuilt.Add(path[i]); } return NormalizePathPoints(rebuilt); } 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 path, IReadOnlyCollection nodes, string? sourceNodeId, string? targetNodeId) { return HasClearBoundarySegments(path, nodes, sourceNodeId, targetNodeId, true, 2); } private static List NormalizeExitPath( IReadOnlyList sourcePath, ElkPositionedNode sourceNode, string side) { const double coordinateTolerance = 0.5d; var path = sourcePath .Select(point => new ElkPoint { X = point.X, Y = point.Y }) .ToList(); if (path.Count < 2) { return path; } if (side is "left" or "right") { var sourceX = side == "left" ? sourceNode.X : sourceNode.X + sourceNode.Width; while (path.Count >= 3 && Math.Abs(path[1].X - sourceX) <= coordinateTolerance) { path.RemoveAt(1); } var anchor = path[1]; var boundaryPoint = BuildRectBoundaryPointForSide(sourceNode, side, anchor); var rebuilt = new List { new() { X = sourceX, Y = boundaryPoint.Y }, }; var stubX = side == "left" ? Math.Min(sourceX - 24d, anchor.X) : Math.Max(sourceX + 24d, anchor.X); if (Math.Abs(stubX - sourceX) > coordinateTolerance) { rebuilt.Add(new ElkPoint { X = stubX, Y = boundaryPoint.Y, }); } if (Math.Abs(anchor.Y - boundaryPoint.Y) > coordinateTolerance) { rebuilt.Add(new ElkPoint { X = stubX, Y = anchor.Y }); } if (Math.Abs(anchor.X - stubX) > coordinateTolerance) { rebuilt.Add(new ElkPoint { X = anchor.X, Y = anchor.Y }); } rebuilt.AddRange(path.Skip(2)); return NormalizePathPoints(rebuilt); } var sourceY = side == "top" ? sourceNode.Y : sourceNode.Y + sourceNode.Height; while (path.Count >= 3 && Math.Abs(path[1].Y - sourceY) <= coordinateTolerance) { path.RemoveAt(1); } var verticalAnchor = path[1]; var verticalBoundaryPoint = BuildRectBoundaryPointForSide(sourceNode, side, verticalAnchor); var verticalRebuilt = new List { new() { X = verticalBoundaryPoint.X, Y = sourceY }, }; var stubY = side == "top" ? Math.Min(sourceY - 24d, verticalAnchor.Y) : Math.Max(sourceY + 24d, verticalAnchor.Y); if (Math.Abs(stubY - sourceY) > coordinateTolerance) { verticalRebuilt.Add(new ElkPoint { X = verticalBoundaryPoint.X, Y = stubY, }); } if (Math.Abs(verticalAnchor.X - verticalBoundaryPoint.X) > coordinateTolerance) { verticalRebuilt.Add(new ElkPoint { X = verticalAnchor.X, Y = stubY }); } if (Math.Abs(verticalAnchor.Y - stubY) > coordinateTolerance) { verticalRebuilt.Add(new ElkPoint { X = verticalAnchor.X, Y = verticalAnchor.Y }); } verticalRebuilt.AddRange(path.Skip(2)); return NormalizePathPoints(verticalRebuilt); } private static List NormalizeEntryPath( IReadOnlyList sourcePath, ElkPositionedNode targetNode, string side) { return NormalizeEntryPath(sourcePath, targetNode, side, null); } private static List NormalizeEntryPath( IReadOnlyList sourcePath, ElkPositionedNode targetNode, string side, ElkPoint? explicitEndpoint) { const double coordinateTolerance = 0.5d; var path = sourcePath .Select(point => new ElkPoint { X = point.X, Y = point.Y }) .ToList(); if (path.Count < 2) { return path; } if (HasTargetApproachBacktracking(path, targetNode) && TryResolveNonGatewayBacktrackingEndpoint(path, targetNode, out var correctedSide, out var correctedEndpoint)) { side = correctedSide; explicitEndpoint = correctedEndpoint; } if (side is "left" or "right") { var targetX = side == "left" ? targetNode.X : targetNode.X + targetNode.Width; while (path.Count >= 3 && Math.Abs(path[^2].X - targetX) <= coordinateTolerance) { path.RemoveAt(path.Count - 2); } var anchor = path[^2]; var endpoint = explicitEndpoint ?? BuildRectBoundaryPointForSide(targetNode, side, anchor); var rebuilt = path.Take(path.Count - 2).ToList(); if (rebuilt.Count == 0 || !ElkEdgeRoutingGeometry.PointsEqual(rebuilt[^1], anchor)) { rebuilt.Add(anchor); } var stubX = side == "left" ? targetX - 24d : targetX + 24d; if (Math.Abs(anchor.X - stubX) > coordinateTolerance) { rebuilt.Add(new ElkPoint { X = stubX, Y = anchor.Y }); } if (Math.Abs(anchor.Y - endpoint.Y) > coordinateTolerance) { rebuilt.Add(new ElkPoint { X = stubX, Y = endpoint.Y }); } rebuilt.Add(endpoint); return NormalizePathPoints(rebuilt); } var targetY = side == "top" ? targetNode.Y : targetNode.Y + targetNode.Height; while (path.Count >= 3 && Math.Abs(path[^2].Y - targetY) <= coordinateTolerance) { path.RemoveAt(path.Count - 2); } var verticalAnchor = path[^2]; var verticalEndpoint = explicitEndpoint ?? BuildRectBoundaryPointForSide(targetNode, side, verticalAnchor); var verticalRebuilt = path.Take(path.Count - 2).ToList(); if (verticalRebuilt.Count == 0 || !ElkEdgeRoutingGeometry.PointsEqual(verticalRebuilt[^1], verticalAnchor)) { verticalRebuilt.Add(verticalAnchor); } var stubY = side == "top" ? targetY - 24d : targetY + 24d; if (Math.Abs(verticalAnchor.X - verticalEndpoint.X) > coordinateTolerance) { verticalRebuilt.Add(new ElkPoint { X = verticalEndpoint.X, Y = verticalAnchor.Y }); } if (Math.Abs(verticalAnchor.Y - stubY) > coordinateTolerance) { verticalRebuilt.Add(new ElkPoint { X = verticalEndpoint.X, Y = stubY }); } verticalRebuilt.Add(verticalEndpoint); return NormalizePathPoints(verticalRebuilt); } private static string ResolvePreferredRectSourceExitSide( IReadOnlyList 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 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), }; } private static List NormalizeGatewayExitPath( IReadOnlyList sourcePath, ElkPositionedNode sourceNode, IReadOnlyCollection nodes, string? sourceNodeId, string? targetNodeId) { var path = sourcePath .Select(point => new ElkPoint { X = point.X, Y = point.Y }) .ToList(); if (path.Count < 2) { return path; } var firstExteriorIndex = FindFirstGatewayExteriorPointIndex(path, sourceNode); var firstContinuationIndex = FindPreferredGatewayExitContinuationIndex(path, sourceNode, firstExteriorIndex); var forceLocalExitRepair = path.Count > 2 && ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(sourceNode, path[1]); var minContinuationIndex = forceLocalExitRepair ? firstExteriorIndex : firstContinuationIndex; var maxContinuationIndex = forceLocalExitRepair ? Math.Min(path.Count - 1, firstExteriorIndex + 1) : path.Count - 1; var exitReferences = CollectGatewayExitReferencePoints(path, nodes, targetNodeId, firstContinuationIndex); List? bestCandidate = null; var bestScore = double.PositiveInfinity; foreach (var exitReference in exitReferences) { foreach (var boundary in ResolveGatewayExitBoundaryCandidates(sourceNode, exitReference)) { for (var continuationIndex = minContinuationIndex; continuationIndex <= maxContinuationIndex; continuationIndex++) { if (ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(sourceNode, path[continuationIndex])) { continue; } var continuationReference = path[continuationIndex]; foreach (var exteriorApproach in ResolveGatewayExteriorApproachCandidates(sourceNode, boundary, continuationReference)) { var candidate = BuildGatewayExitCandidate(path, boundary, exteriorApproach, continuationIndex); if (!HasAcceptableGatewayBoundaryPath(candidate, nodes, sourceNodeId, targetNodeId, sourceNode, fromStart: true) || !HasClearBoundarySegments(candidate, nodes, sourceNodeId, targetNodeId, true, Math.Min(4, candidate.Count - 1)) || HasGatewaySourceExitBacktracking(candidate) || HasGatewaySourceExitCurl(candidate)) { continue; } var score = ScoreGatewayExitCandidate(candidate, exitReference, continuationIndex, path, sourceNode); if (score >= bestScore) { continue; } bestScore = score; bestCandidate = candidate; } } } } if (bestCandidate is not null) { return ForceDecisionDiagonalSourceExit(bestCandidate, sourceNode, nodes, sourceNodeId, targetNodeId); } var exteriorAnchor = path[firstContinuationIndex]; var fallbackReference = exitReferences[0]; var fallbackBoundaryCandidates = ResolveGatewayExitBoundaryCandidates(sourceNode, fallbackReference).ToArray(); if (fallbackBoundaryCandidates.Length == 0) { fallbackBoundaryCandidates = [ ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary( sourceNode, ElkShapeBoundaries.ProjectOntoShapeBoundary(sourceNode, fallbackReference), fallbackReference), ]; } List? fallbackPath = null; var fallbackScore = double.PositiveInfinity; foreach (var fallbackBoundary in fallbackBoundaryCandidates) { var candidate = BuildGatewayFallbackExitPath(path, sourceNode, fallbackBoundary, exteriorAnchor, firstContinuationIndex); var candidateScore = ScoreGatewayExitCandidate(candidate, fallbackReference, firstContinuationIndex, path, sourceNode); if (!HasAcceptableGatewayBoundaryPath(candidate, nodes, sourceNodeId, targetNodeId, sourceNode, fromStart: true)) { candidateScore += 5_000d; } if (candidateScore >= fallbackScore) { continue; } fallbackScore = candidateScore; fallbackPath = candidate; } if (fallbackPath is not null) { return ForceDecisionDiagonalSourceExit(fallbackPath, sourceNode, nodes, sourceNodeId, targetNodeId); } if (sourceNode.Kind == "Decision" && path.Count >= 3 && ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(sourceNode, path[1])) { var continuationIndex = Math.Max(firstExteriorIndex, Math.Min(path.Count - 1, 2)); var continuationPoint = path[continuationIndex]; var directBoundary = ResolveDecisionSourceExitBoundary(sourceNode, continuationPoint, path[^1]); var directApproach = ElkShapeBoundaries.BuildPreferredGatewaySourceExteriorPoint(sourceNode, directBoundary, continuationPoint); var directRepair = BuildGatewayExitCandidate(path, directBoundary, directApproach, continuationIndex); if (HasAcceptableGatewayBoundaryPath(directRepair, nodes, sourceNodeId, targetNodeId, sourceNode, fromStart: true) && HasClearBoundarySegments(directRepair, nodes, sourceNodeId, targetNodeId, true, Math.Min(4, directRepair.Count - 1)) && !HasGatewaySourceExitBacktracking(directRepair) && !HasGatewaySourceExitCurl(directRepair)) { return ForceDecisionDiagonalSourceExit(directRepair, sourceNode, nodes, sourceNodeId, targetNodeId); } } return ForceDecisionDiagonalSourceExit(path, sourceNode, nodes, sourceNodeId, targetNodeId); } private static List CollectGatewayExitReferencePoints( IReadOnlyList path, IReadOnlyCollection nodes, string? targetNodeId, int firstContinuationIndex) { var references = new List(); if (!string.IsNullOrWhiteSpace(targetNodeId)) { var targetNode = nodes.FirstOrDefault(node => string.Equals(node.Id, targetNodeId, StringComparison.Ordinal)); if (targetNode is not null) { AddUniquePoint(references, new ElkPoint { X = targetNode.X + (targetNode.Width / 2d), Y = targetNode.Y + (targetNode.Height / 2d), }); } } var maxReferenceIndex = Math.Min(path.Count - 1, firstContinuationIndex + 4); for (var i = firstContinuationIndex; i <= maxReferenceIndex; i++) { AddUniquePoint(references, path[i]); } AddUniquePoint(references, path[^1]); if (references.Count == 0) { references.Add(path[^1]); } return references; } private static IEnumerable ResolveGatewayExitBoundaryCandidates( ElkPositionedNode sourceNode, ElkPoint exitReference) { var candidates = new List(); AddUniquePoint( candidates, ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary( sourceNode, ElkShapeBoundaries.ProjectOntoShapeBoundary(sourceNode, exitReference), exitReference)); foreach (var side in EnumeratePreferredGatewayExitSides(sourceNode, exitReference)) { var centerX = sourceNode.X + (sourceNode.Width / 2d); var centerY = sourceNode.Y + (sourceNode.Height / 2d); var slotCoordinate = side is "left" or "right" ? centerY + Math.Clamp(exitReference.Y - centerY, -sourceNode.Height * 0.18d, sourceNode.Height * 0.18d) : centerX + Math.Clamp(exitReference.X - centerX, -sourceNode.Width * 0.18d, sourceNode.Width * 0.18d); if (!ElkShapeBoundaries.TryProjectGatewayBoundarySlot(sourceNode, side, slotCoordinate, out var slotBoundary)) { continue; } AddUniquePoint( candidates, ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(sourceNode, slotBoundary, exitReference)); } return candidates; } private static IEnumerable EnumeratePreferredGatewayExitSides( ElkPositionedNode sourceNode, ElkPoint exitReference) { var centerX = sourceNode.X + (sourceNode.Width / 2d); var centerY = sourceNode.Y + (sourceNode.Height / 2d); var deltaX = exitReference.X - centerX; var deltaY = exitReference.Y - centerY; var absDx = Math.Abs(deltaX); var absDy = Math.Abs(deltaY); var primary = absDx >= absDy ? (deltaX >= 0d ? "right" : "left") : (deltaY >= 0d ? "bottom" : "top"); yield return primary; if (absDx > 0.5d && absDy > 0.5d) { var secondary = primary is "left" or "right" ? (deltaY >= 0d ? "bottom" : "top") : (deltaX >= 0d ? "right" : "left"); if (!string.Equals(primary, secondary, StringComparison.Ordinal)) { yield return secondary; } } } private static void AddUniquePoint(ICollection points, ElkPoint point) { if (points.Any(existing => ElkEdgeRoutingGeometry.PointsEqual(existing, point))) { return; } points.Add(point); } private static List BuildGatewayExitCandidate( IReadOnlyList path, ElkPoint boundary, ElkPoint exteriorApproach, int continuationIndex) { var rebuilt = new List { boundary }; if (!ElkEdgeRoutingGeometry.PointsEqual(boundary, exteriorApproach)) { rebuilt.Add(exteriorApproach); } var continuationPoint = path[continuationIndex]; if (!ElkEdgeRoutingGeometry.PointsEqual(rebuilt[^1], continuationPoint)) { var preferHorizontal = ShouldPreferHorizontalGatewayExit(rebuilt[^1], continuationPoint); AppendGatewayOrthogonalCorner( rebuilt, rebuilt[^1], continuationPoint, continuationIndex + 1 < path.Count ? path[continuationIndex + 1] : null, preferHorizontalFromReference: preferHorizontal); if (!ElkEdgeRoutingGeometry.PointsEqual(rebuilt[^1], continuationPoint)) { rebuilt.Add(continuationPoint); } } for (var i = continuationIndex + 1; i < path.Count; i++) { rebuilt.Add(path[i]); } return NormalizePathPoints(rebuilt); } private static List BuildGatewayFallbackExitPath( IReadOnlyList path, ElkPositionedNode sourceNode, ElkPoint boundary, ElkPoint exteriorAnchor, int continuationIndex) { var exteriorApproach = ElkShapeBoundaries.BuildPreferredGatewaySourceExteriorPoint(sourceNode, boundary, exteriorAnchor); var rebuilt = new List { boundary }; if (!ElkEdgeRoutingGeometry.PointsEqual(boundary, exteriorApproach)) { rebuilt.Add(exteriorApproach); } AppendGatewayOrthogonalCorner( rebuilt, rebuilt[^1], exteriorAnchor, continuationIndex + 1 < path.Count ? path[continuationIndex + 1] : null, preferHorizontalFromReference: ShouldPreferHorizontalGatewayExit(rebuilt[^1], exteriorAnchor)); if (!ElkEdgeRoutingGeometry.PointsEqual(rebuilt[^1], exteriorAnchor)) { rebuilt.Add(exteriorAnchor); } for (var i = continuationIndex + 1; i < path.Count; i++) { rebuilt.Add(path[i]); } return NormalizePathPoints(rebuilt); } private static bool PathStartsAtDecisionVertex( IReadOnlyList path, ElkPositionedNode sourceNode) { return sourceNode.Kind == "Decision" && path.Count >= 2 && ElkShapeBoundaries.IsNearGatewayVertex(sourceNode, path[0]); } private static List ForceDecisionSourceExitOffVertex( IReadOnlyList sourcePath, ElkPositionedNode sourceNode, IReadOnlyCollection nodes, string? sourceNodeId, string? targetNodeId) { var path = sourcePath .Select(point => new ElkPoint { X = point.X, Y = point.Y }) .ToList(); if (path.Count < 3 || sourceNode.Kind != "Decision") { return path; } var continuationIndex = Math.Min(path.Count - 1, 2); var reference = path[^1]; var boundary = ResolveDecisionSourceExitBoundary(sourceNode, path[continuationIndex], reference); if (ElkShapeBoundaries.IsNearGatewayVertex(sourceNode, boundary)) { return path; } var continuationPoint = path[continuationIndex]; var exteriorApproach = ElkShapeBoundaries.BuildPreferredGatewaySourceExteriorPoint(sourceNode, boundary, continuationPoint); var rebuilt = new List { boundary }; if (!ElkEdgeRoutingGeometry.PointsEqual(boundary, exteriorApproach)) { rebuilt.Add(exteriorApproach); } AppendGatewayOrthogonalCorner( rebuilt, rebuilt[^1], continuationPoint, continuationIndex + 1 < path.Count ? path[continuationIndex + 1] : null, preferHorizontalFromReference: ShouldPreferHorizontalGatewayExit(rebuilt[^1], continuationPoint)); if (!ElkEdgeRoutingGeometry.PointsEqual(rebuilt[^1], continuationPoint)) { rebuilt.Add(continuationPoint); } for (var i = continuationIndex + 1; i < path.Count; i++) { rebuilt.Add(path[i]); } return NormalizePathPoints(rebuilt); } private static ElkPoint ResolveDecisionSourceExitBoundary( ElkPositionedNode sourceNode, ElkPoint continuationPoint, ElkPoint reference) { var projectedReference = ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary( sourceNode, ElkShapeBoundaries.ProjectOntoShapeBoundary(sourceNode, reference), reference); var projectedContinuation = ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary( sourceNode, ElkShapeBoundaries.ProjectOntoShapeBoundary(sourceNode, continuationPoint), continuationPoint); var candidates = new List(); AddDecisionBoundaryCandidate(candidates, projectedReference); AddDecisionBoundaryCandidate(candidates, projectedContinuation); foreach (var side in EnumeratePreferredGatewaySourceSides(sourceNode, continuationPoint, reference)) { foreach (var slotBoundary in ResolveGatewaySourceBoundarySlotCandidates(sourceNode, side, continuationPoint, reference)) { AddDecisionBoundaryCandidate(candidates, slotBoundary); } } var bestCandidate = projectedReference; var bestScore = double.PositiveInfinity; foreach (var candidate in candidates) { var score = ScoreDecisionSourceExitBoundaryCandidate( sourceNode, candidate, projectedReference, projectedContinuation, continuationPoint, reference); if (score >= bestScore) { continue; } bestScore = score; bestCandidate = candidate; } return bestCandidate; } private static void AddDecisionBoundaryCandidate( ICollection candidates, ElkPoint candidate) { if (candidates.Any(existing => ElkEdgeRoutingGeometry.PointsEqual(existing, candidate))) { return; } candidates.Add(candidate); } private static double ScoreDecisionSourceExitBoundaryCandidate( ElkPositionedNode sourceNode, ElkPoint candidate, ElkPoint projectedReference, ElkPoint projectedContinuation, ElkPoint continuationPoint, ElkPoint reference) { var centerX = sourceNode.X + (sourceNode.Width / 2d); var centerY = sourceNode.Y + (sourceNode.Height / 2d); var desiredDx = reference.X - centerX; var desiredDy = reference.Y - centerY; var candidateDx = candidate.X - centerX; var candidateDy = candidate.Y - centerY; var score = Math.Abs(candidate.X - projectedReference.X) + Math.Abs(candidate.Y - projectedReference.Y); score += (Math.Abs(candidate.X - projectedContinuation.X) + Math.Abs(candidate.Y - projectedContinuation.Y)) * 0.35d; var preferredSides = EnumeratePreferredGatewaySourceSides(sourceNode, continuationPoint, reference).ToArray(); if (preferredSides.Length > 0 && !IsBoundaryOnGatewaySourceSide(sourceNode, candidate, preferredSides[0])) { score += preferredSides[0] is "left" or "right" ? 12_000d : 8_000d; } else if (preferredSides.Length > 0 && preferredSides[0] is "left" or "right") { score -= Math.Abs(candidateDx) * 0.4d; } if (preferredSides.Length > 1 && !IsBoundaryOnGatewaySourceSide(sourceNode, candidate, preferredSides[0]) && !IsBoundaryOnGatewaySourceSide(sourceNode, candidate, preferredSides[1])) { score += 4_000d; } var dominantHorizontal = Math.Abs(desiredDx) >= Math.Abs(desiredDy) * 1.25d; var dominantVertical = Math.Abs(desiredDy) >= Math.Abs(desiredDx) * 1.25d; if (dominantHorizontal) { if (Math.Sign(candidateDx) != Math.Sign(desiredDx)) { score += 10_000d; } if (Math.Abs(candidateDy) > sourceNode.Height * 0.28d) { score += 25_000d; } score += Math.Abs(candidateDy) * 6d; } else if (dominantVertical) { if (Math.Sign(candidateDy) != Math.Sign(desiredDy)) { score += 10_000d; } if (Math.Abs(candidateDx) > sourceNode.Width * 0.28d) { score += 25_000d; } score += Math.Abs(candidateDx) * 6d; } else { score += (Math.Abs(candidateDx - desiredDx) + Math.Abs(candidateDy - desiredDy)) * 0.08d; } if (ElkShapeBoundaries.IsNearGatewayVertex(sourceNode, candidate, 8d)) { score += 4_000d; } var exterior = ElkShapeBoundaries.BuildPreferredGatewaySourceExteriorPoint(sourceNode, candidate, continuationPoint); score += (Math.Abs(exterior.X - continuationPoint.X) + Math.Abs(exterior.Y - continuationPoint.Y)) * 0.04d; return score; } private static bool ShouldPreferHorizontalGatewayExit(ElkPoint from, ElkPoint to) { return Math.Abs(to.X - from.X) >= Math.Abs(to.Y - from.Y); } private static bool CanUseDirectGatewayContinuation(ElkPoint from, ElkPoint to) { const double coordinateTolerance = 0.5d; var deltaX = Math.Abs(to.X - from.X); var deltaY = Math.Abs(to.Y - from.Y); if (deltaX <= coordinateTolerance || deltaY <= coordinateTolerance) { return true; } var length = Math.Sqrt((deltaX * deltaX) + (deltaY * deltaY)); return length <= 36d && Math.Abs(deltaX - deltaY) <= Math.Max(6d, Math.Min(deltaX, deltaY) * 0.45d); } private static void AddUniqueCoordinate(ICollection coordinates, double value) { if (coordinates.Any(existing => Math.Abs(existing - value) <= 0.5d)) { return; } coordinates.Add(value); } private static double ScoreGatewayExitCandidate( IReadOnlyList candidate, ElkPoint exitReference, int continuationIndex, IReadOnlyList originalPath, ElkPositionedNode sourceNode) { var score = ComputePathLength(candidate); score += Math.Max(0, candidate.Count - 2) * 3d; score += continuationIndex * 2d; score += (Math.Abs(candidate[0].X - exitReference.X) + Math.Abs(candidate[0].Y - exitReference.Y)) * 0.1d; if (continuationIndex < originalPath.Count) { score += (Math.Abs(candidate[1].X - originalPath[continuationIndex].X) + Math.Abs(candidate[1].Y - originalPath[continuationIndex].Y)) * 0.02d; } score += ScoreGatewayExitProgress(sourceNode, candidate, exitReference); if (HasGatewaySourceDominantAxisDetour(candidate, sourceNode)) { score += 50_000d; } if (HasGatewaySourcePreferredFaceMismatch(candidate, sourceNode)) { score += 25_000d; } if (NeedsDecisionSourcePreferredFaceRepair(candidate, sourceNode)) { score += 15_000d; } return score; } private static double ScoreGatewayExitProgress( ElkPositionedNode sourceNode, IReadOnlyList candidate, ElkPoint exitReference) { if (candidate.Count < 2) { return 0d; } var boundary = candidate[0]; var next = candidate[1]; var score = 0d; if (ElkShapeBoundaries.IsNearGatewayVertex(sourceNode, boundary)) { score += sourceNode.Kind == "Decision" ? 5_000d : 1_500d; } var startDistance = Math.Abs(boundary.X - exitReference.X) + Math.Abs(boundary.Y - exitReference.Y); var nextDistance = Math.Abs(next.X - exitReference.X) + Math.Abs(next.Y - exitReference.Y); if (nextDistance > startDistance + 0.5d) { score += (nextDistance - startDistance) * 6d; } var totalDx = exitReference.X - boundary.X; var totalDy = exitReference.Y - boundary.Y; var firstDx = next.X - boundary.X; var firstDy = next.Y - boundary.Y; var absTotalDx = Math.Abs(totalDx); var absTotalDy = Math.Abs(totalDy); var absFirstDx = Math.Abs(firstDx); var absFirstDy = Math.Abs(firstDy); const double coordinateTolerance = 0.5d; if (sourceNode.Kind == "Decision" && (absFirstDx <= coordinateTolerance || absFirstDy <= coordinateTolerance)) { score += ElkShapeBoundaries.IsNearGatewayVertex(sourceNode, boundary) ? 350d : 0d; } if (absTotalDx >= absTotalDy * 1.25d) { if (absFirstDx <= coordinateTolerance || Math.Sign(firstDx) != Math.Sign(totalDx)) { score += 600d; } else if (absFirstDy > absFirstDx * 1.25d) { score += 120d; } } else if (absTotalDy >= absTotalDx * 1.25d) { if (absFirstDy <= coordinateTolerance || Math.Sign(firstDy) != Math.Sign(totalDy)) { score += 600d; } else if (absFirstDx > absFirstDy * 1.25d) { score += 120d; } } return score; } private static int FindPreferredGatewayExitContinuationIndex( IReadOnlyList path, ElkPositionedNode sourceNode, int firstExteriorIndex) { if (path.Count <= firstExteriorIndex + 1) { return firstExteriorIndex; } var firstContinuation = path[firstExteriorIndex]; var finalTarget = path[^1]; var start = path[0]; var firstDx = firstContinuation.X - start.X; var firstDy = firstContinuation.Y - start.Y; var desiredDx = finalTarget.X - start.X; var desiredDy = finalTarget.Y - start.Y; const double coordinateTolerance = 0.5d; var bestIndex = firstExteriorIndex; var bestScore = ScoreGatewayExitContinuationPoint(path[firstExteriorIndex], start, finalTarget, firstExteriorIndex, sourceNode.Kind, coordinateTolerance); for (var i = firstExteriorIndex + 1; i < path.Count; i++) { if (!ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(sourceNode, path[i])) { var score = ScoreGatewayExitContinuationPoint(path[i], start, finalTarget, i, sourceNode.Kind, coordinateTolerance); if (score < bestScore) { bestScore = score; bestIndex = i; } } } return bestIndex; } private static int? FindGatewaySourceCurlRecoveryIndex( IReadOnlyList path, int firstExteriorIndex) { if (!HasGatewaySourceExitCurl(path) || path.Count <= firstExteriorIndex + 1) { return null; } const double coordinateTolerance = 0.5d; var start = path[0]; var finalTarget = path[^1]; var desiredDx = finalTarget.X - start.X; var desiredDy = finalTarget.Y - start.Y; for (var i = firstExteriorIndex + 1; i < path.Count; i++) { var point = path[i]; var deltaX = point.X - start.X; var deltaY = point.Y - start.Y; if (IsGatewayExitAxisAlignedWithDesiredDirection(deltaX, desiredDx, coordinateTolerance) && IsGatewayExitAxisAlignedWithDesiredDirection(deltaY, desiredDy, coordinateTolerance)) { return i; } } return null; } private static bool IsGatewayExitAxisAlignedWithDesiredDirection( double delta, double desiredDelta, double tolerance) { if (Math.Abs(desiredDelta) <= tolerance || Math.Abs(delta) <= tolerance) { return true; } return Math.Sign(delta) == Math.Sign(desiredDelta); } private static double ScoreGatewayExitContinuationPoint( ElkPoint point, ElkPoint start, ElkPoint finalTarget, int index, string sourceKind, double tolerance) { var desiredDx = finalTarget.X - start.X; var desiredDy = finalTarget.Y - start.Y; var deltaX = point.X - start.X; var deltaY = point.Y - start.Y; var score = index * 4d; if (Math.Abs(desiredDx) >= Math.Abs(desiredDy) * 1.25d) { if (Math.Sign(deltaX) != Math.Sign(desiredDx) || Math.Abs(deltaX) <= tolerance) { score += 1_000d; } if (Math.Abs(desiredDy) <= tolerance) { score += Math.Abs(point.Y - start.Y) * 1.25d; } else { if (Math.Sign(deltaY) != Math.Sign(desiredDy) && Math.Abs(deltaY) > tolerance) { score += 1_600d; } if (desiredDy > tolerance && point.Y > finalTarget.Y + tolerance) { score += 4_000d; } else if (desiredDy < -tolerance && point.Y < finalTarget.Y - tolerance) { score += 4_000d; } score += Math.Abs(point.Y - finalTarget.Y) * 2.4d; } score -= Math.Abs(deltaX) * 0.2d; } else if (Math.Abs(desiredDy) >= Math.Abs(desiredDx) * 1.25d) { if (Math.Sign(deltaY) != Math.Sign(desiredDy) || Math.Abs(deltaY) <= tolerance) { score += 1_000d; } if (Math.Abs(desiredDx) <= tolerance) { score += Math.Abs(point.X - start.X) * 1.25d; } else { if (Math.Sign(deltaX) != Math.Sign(desiredDx) && Math.Abs(deltaX) > tolerance) { score += 1_600d; } if (desiredDx > tolerance && point.X > finalTarget.X + tolerance) { score += 4_000d; } else if (desiredDx < -tolerance && point.X < finalTarget.X - tolerance) { score += 4_000d; } score += Math.Abs(point.X - finalTarget.X) * 2.4d; } score -= Math.Abs(deltaY) * 0.2d; } else { if (Math.Sign(deltaX) != Math.Sign(desiredDx)) { score += 500d; } if (Math.Sign(deltaY) != Math.Sign(desiredDy)) { score += 500d; } score -= (Math.Abs(deltaX) + Math.Abs(deltaY)) * 0.12d; } if (sourceKind == "Decision" && (Math.Abs(deltaX) <= tolerance || Math.Abs(deltaY) <= tolerance)) { score += 120d; } return score; } private static bool HasGatewaySourceExitBacktracking(IReadOnlyList path) { if (path.Count < 4) { return false; } var reference = path[^1]; var desiredDx = reference.X - path[0].X; var desiredDy = reference.Y - path[0].Y; var sampleCount = Math.Min(path.Count, 6); var absDx = Math.Abs(desiredDx); var absDy = Math.Abs(desiredDy); if (absDx >= absDy * 1.35d) { return HasAxisReversalFromStart(path.Take(sampleCount).Select(point => point.X), desiredDx); } if (absDy >= absDx * 1.35d) { return HasAxisReversalFromStart(path.Take(sampleCount).Select(point => point.Y), desiredDy); } return HasAxisReversalFromStart(path.Take(sampleCount).Select(point => point.X), desiredDx) && HasAxisReversalFromStart(path.Take(sampleCount).Select(point => point.Y), desiredDy); } private static bool HasGatewaySourceExitCurl(IReadOnlyList path) { if (path.Count < 4) { return false; } var sampleCount = Math.Min(path.Count, 6); var desiredDx = path[^1].X - path[0].X; var desiredDy = path[^1].Y - path[0].Y; return HasAxisReversalFromStart(path.Take(sampleCount).Select(point => point.X), desiredDx) || HasAxisReversalFromStart(path.Take(sampleCount).Select(point => point.Y), desiredDy); } private static bool NeedsGatewaySourceBoundaryRepair( IReadOnlyList path, ElkPositionedNode sourceNode) { if (path.Count < 2) { return false; } return ElkShapeBoundaries.IsNearGatewayVertex(sourceNode, path[0]) || ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(sourceNode, path[1]) || !ElkShapeBoundaries.HasValidGatewayBoundaryAngle(sourceNode, path[0], path[1]) || NeedsDecisionSourcePreferredFaceRepair(path, sourceNode) || HasGatewaySourceExitCurl(path) || HasGatewaySourceExitBacktracking(path); } private static List ForceDecisionDiagonalSourceExit( IReadOnlyList sourcePath, ElkPositionedNode sourceNode, IReadOnlyCollection nodes, string? sourceNodeId, string? targetNodeId) { var path = sourcePath .Select(point => new ElkPoint { X = point.X, Y = point.Y }) .ToList(); if (sourceNode.Kind != "Decision" || path.Count < 3) { return path; } if (ElkShapeBoundaries.IsNearGatewayVertex(sourceNode, path[0], 8d)) { return path; } var firstDx = path[1].X - path[0].X; var firstDy = path[1].Y - path[0].Y; var startsAxisAligned = Math.Abs(firstDx) <= 0.5d || Math.Abs(firstDy) <= 0.5d; if (!startsAxisAligned) { return path; } var referenceDx = path[^1].X - path[0].X; var referenceDy = path[^1].Y - path[0].Y; if (Math.Abs(referenceDx) <= 24d || Math.Abs(referenceDy) <= 24d || Math.Abs(referenceDx) > Math.Abs(referenceDy) * 3d || Math.Abs(referenceDy) > Math.Abs(referenceDx) * 3d) { return path; } var firstExteriorIndex = FindFirstGatewayExteriorPointIndex(path, sourceNode); var continuationIndex = FindPreferredGatewayExitContinuationIndex(path, sourceNode, firstExteriorIndex); var continuationPoint = path[continuationIndex]; var boundary = ResolveDecisionSourceExitBoundary(sourceNode, continuationPoint, path[^1]); var diagonalExterior = ElkShapeBoundaries.BuildGatewayExteriorApproachPoint(sourceNode, boundary); if (ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(sourceNode, diagonalExterior) || !ElkShapeBoundaries.HasValidGatewayBoundaryAngle(sourceNode, boundary, diagonalExterior)) { return path; } var candidate = BuildGatewayExitCandidate(path, boundary, diagonalExterior, continuationIndex); if (!PathChanged(path, candidate)) { return path; } if (!HasAcceptableGatewayBoundaryPath(candidate, nodes, sourceNodeId, targetNodeId, sourceNode, fromStart: true) || HasGatewaySourceExitBacktracking(candidate) || HasGatewaySourceExitCurl(candidate)) { return path; } return ComputePathLength(candidate) <= ComputePathLength(path) + 2d ? candidate : path; } private static bool NeedsDecisionSourcePreferredFaceRepair( IReadOnlyList path, ElkPositionedNode sourceNode) { if (sourceNode.Kind != "Decision" || path.Count < 3) { return false; } var firstExteriorIndex = FindFirstGatewayExteriorPointIndex(path, sourceNode); var continuationIndex = FindPreferredGatewayExitContinuationIndex(path, sourceNode, firstExteriorIndex); var continuationPoint = path[continuationIndex]; var preferredBoundary = ResolveDecisionSourceExitBoundary(sourceNode, continuationPoint, path[^1]); if (ElkEdgeRoutingGeometry.PointsEqual(path[0], preferredBoundary)) { return false; } return ElkShapeBoundaries.IsNearGatewayVertex(sourceNode, path[0], 8d) || ElkEdgeRoutingGeometry.ComputeSegmentLength(path[0], preferredBoundary) > 6d; } private static bool NeedsGatewayTargetBoundaryRepair( IReadOnlyList path, ElkPositionedNode targetNode) { if (path.Count < 2) { return false; } return ElkShapeBoundaries.IsNearGatewayVertex(targetNode, path[^1]) || ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(targetNode, path[^2]) || !ElkShapeBoundaries.HasValidGatewayBoundaryAngle(targetNode, path[^1], path[^2]); } private static List RepairGatewaySourceBoundaryPath( IReadOnlyList sourcePath, ElkPositionedNode sourceNode, IReadOnlyCollection nodes, string? sourceNodeId, string? targetNodeId) { var directSourceCandidate = TryBuildDirectGatewaySourcePath( sourcePath, sourceNode, nodes, sourceNodeId, targetNodeId); if (PathChanged(sourcePath, directSourceCandidate) && HasAcceptableGatewayBoundaryPath(directSourceCandidate, nodes, sourceNodeId, targetNodeId, sourceNode, fromStart: true) && !HasGatewaySourceExitBacktracking(directSourceCandidate) && !HasGatewaySourceExitCurl(directSourceCandidate) && !HasGatewaySourceDominantAxisDetour(directSourceCandidate, sourceNode) && !HasGatewaySourcePreferredFaceMismatch(directSourceCandidate, sourceNode)) { return directSourceCandidate; } var normalizedCandidate = NormalizeGatewayExitPath( sourcePath, sourceNode, nodes, sourceNodeId, targetNodeId); if (PathChanged(sourcePath, normalizedCandidate) && HasAcceptableGatewayBoundaryPath(normalizedCandidate, nodes, sourceNodeId, targetNodeId, sourceNode, fromStart: true) && !HasGatewaySourceExitBacktracking(normalizedCandidate) && !HasGatewaySourceExitCurl(normalizedCandidate) && !HasGatewaySourceDominantAxisDetour(normalizedCandidate, sourceNode) && !HasGatewaySourcePreferredFaceMismatch(normalizedCandidate, sourceNode)) { return normalizedCandidate; } var blockerEscapeCandidate = TryBuildGatewaySourceDominantBlockerEscapePath( sourcePath, sourceNode, nodes, sourceNodeId, targetNodeId); if (PathChanged(sourcePath, blockerEscapeCandidate) && HasAcceptableGatewayBoundaryPath(blockerEscapeCandidate, nodes, sourceNodeId, targetNodeId, sourceNode, fromStart: true) && !HasGatewaySourceExitBacktracking(blockerEscapeCandidate) && !HasGatewaySourceExitCurl(blockerEscapeCandidate) && !HasGatewaySourceDominantAxisDetour(blockerEscapeCandidate, sourceNode) && !HasGatewaySourcePreferredFaceMismatch(blockerEscapeCandidate, sourceNode)) { return blockerEscapeCandidate; } var path = sourcePath .Select(point => new ElkPoint { X = point.X, Y = point.Y }) .ToList(); if (path.Count < 2) { return path; } var firstExteriorIndex = FindFirstGatewayExteriorPointIndex(path, sourceNode); var continuationIndex = FindPreferredGatewayExitContinuationIndex(path, sourceNode, firstExteriorIndex); var continuationPoint = path[continuationIndex]; ElkPoint boundary; if (sourceNode.Kind == "Decision") { boundary = ResolveDecisionSourceExitBoundary(sourceNode, continuationPoint, path[^1]); } else { boundary = ElkShapeBoundaries.ProjectOntoShapeBoundary(sourceNode, continuationPoint); boundary = ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(sourceNode, boundary, continuationPoint); } var normalized = BuildGatewaySourceRepairPath( path, sourceNode, boundary, continuationPoint, continuationIndex, path[^1]); return HasAcceptableGatewayBoundaryPath(normalized, nodes, sourceNodeId, targetNodeId, sourceNode, fromStart: true) ? normalized : path; } private static List TryBuildGatewaySourceDominantBlockerEscapePath( IReadOnlyList sourcePath, ElkPositionedNode sourceNode, IReadOnlyCollection nodes, string? sourceNodeId, string? targetNodeId) { var path = sourcePath .Select(point => new ElkPoint { X = point.X, Y = point.Y }) .ToList(); if (sourceNode.Kind != "Decision" || path.Count < 3 || !HasGatewaySourceDominantAxisDetour(path, sourceNode)) { return path; } var centerX = sourceNode.X + (sourceNode.Width / 2d); var centerY = sourceNode.Y + (sourceNode.Height / 2d); var desiredDx = path[^1].X - centerX; var desiredDy = path[^1].Y - centerY; var dominantVertical = Math.Abs(desiredDy) >= Math.Abs(desiredDx) * 1.25d && Math.Sign(desiredDy) != 0; if (!dominantVertical) { return path; } var clearance = 24d; var direction = Math.Sign(desiredDy); var targetNode = string.IsNullOrWhiteSpace(targetNodeId) ? null : nodes.FirstOrDefault(node => string.Equals(node.Id, targetNodeId, StringComparison.Ordinal)); var firstExteriorIndex = FindFirstGatewayExteriorPointIndex(path, sourceNode); List? bestCandidate = null; var bestScore = double.PositiveInfinity; foreach (var continuationIndex in EnumerateGatewayDirectRepairContinuationIndices(path, sourceNode, firstExteriorIndex)) { var anchorX = path[continuationIndex].X; if (Math.Abs(anchorX - path[0].X) <= 3d) { continue; } var dominantReference = new ElkPoint { X = anchorX, Y = path[^1].Y }; if (!TryResolvePreferredGatewaySourceBoundary(sourceNode, dominantReference, path[^1], out var provisionalBoundary)) { continue; } var stemBlockers = nodes .Where(node => !string.Equals(node.Id, sourceNodeId, StringComparison.Ordinal) && !string.Equals(node.Id, targetNodeId, StringComparison.Ordinal) && provisionalBoundary.X > node.X + 0.5d && provisionalBoundary.X < node.X + node.Width - 0.5d && (direction > 0d ? node.Y > provisionalBoundary.Y + 0.5d && node.Y < path[^1].Y - 0.5d : node.Y + node.Height < provisionalBoundary.Y - 0.5d && node.Y + node.Height > path[^1].Y + 0.5d)) .OrderBy(node => direction > 0d ? node.Y : -(node.Y + node.Height)) .ToArray(); if (stemBlockers.Length == 0) { continue; } foreach (var blocker in stemBlockers) { var escapeY = direction > 0d ? blocker.Y - clearance : blocker.Y + blocker.Height + clearance; if (direction > 0d) { if (escapeY <= provisionalBoundary.Y + 8d || escapeY >= blocker.Y - 0.5d) { continue; } } else if (escapeY >= provisionalBoundary.Y - 8d || escapeY <= blocker.Y + blocker.Height + 0.5d) { continue; } var continuationPoint = new ElkPoint { X = anchorX, Y = escapeY }; var boundary = provisionalBoundary; var candidate = BuildGatewaySourceRepairPath( path, sourceNode, boundary, continuationPoint, continuationIndex, continuationPoint); if (!PathChanged(path, candidate) || !HasAcceptableGatewayBoundaryPath(candidate, nodes, sourceNodeId, targetNodeId, sourceNode, fromStart: true) || !HasClearBoundarySegments(candidate, nodes, sourceNodeId, targetNodeId, true, Math.Min(4, candidate.Count - 1)) || (targetNode is not null && (ElkShapeBoundaries.IsGatewayShape(targetNode) ? !HasAcceptableGatewayBoundaryPath(candidate, nodes, sourceNodeId, targetNodeId, targetNode, fromStart: false) : candidate.Count < 2 || !HasValidBoundaryAngle(candidate[^1], candidate[^2], targetNode))) || HasGatewaySourceExitBacktracking(candidate) || HasGatewaySourceExitCurl(candidate) || HasGatewaySourceDominantAxisDetour(candidate, sourceNode) || HasGatewaySourcePreferredFaceMismatch(candidate, sourceNode)) { continue; } var score = ScoreGatewayDirectRepairCandidate(path, candidate, sourceNode, continuationIndex); if (score >= bestScore) { continue; } bestScore = score; bestCandidate = candidate; } } if (bestCandidate is null) { return path; } return IsMaterialGatewaySourceRepairImprovement(path, bestCandidate) || IsGatewaySourceGeometryRepairImprovement(path, bestCandidate, sourceNode) ? bestCandidate : path; } private static List RepairProtectedGatewaySourceBoundaryPath( IReadOnlyList sourcePath, ElkPositionedNode sourceNode, double graphMinY, double graphMaxY) { var path = sourcePath .Select(point => new ElkPoint { X = point.X, Y = point.Y }) .ToList(); if (path.Count < 3) { return path; } var corridorIndex = -1; for (var i = 1; i < path.Count; i++) { if (path[i].Y < graphMinY - 8d || path[i].Y > graphMaxY + 8d) { corridorIndex = i; break; } } if (corridorIndex < 1) { return path; } var corridorPoint = path[corridorIndex]; var boundary = sourceNode.Kind == "Decision" ? ResolveDecisionSourceExitBoundary(sourceNode, corridorPoint, corridorPoint) : ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary( sourceNode, ElkShapeBoundaries.ProjectOntoShapeBoundary(sourceNode, corridorPoint), corridorPoint); return BuildGatewaySourceRepairPath( path, sourceNode, boundary, corridorPoint, corridorIndex, corridorPoint); } private static List TryBuildProtectedGatewaySourcePath( IReadOnlyList sourcePath, ElkPositionedNode sourceNode, IReadOnlyCollection nodes, string? sourceNodeId, string? targetNodeId) { var graphMinY = nodes.Min(node => node.Y); var graphMaxY = nodes.Max(node => node.Y + node.Height); var candidate = RepairProtectedGatewaySourceBoundaryPath(sourcePath, sourceNode, graphMinY, graphMaxY); if (!PathChanged(sourcePath, candidate)) { return sourcePath.Select(point => new ElkPoint { X = point.X, Y = point.Y }).ToList(); } if (!TryNormalizeTargetBoundaryAfterSourceRepair( candidate, nodes, sourceNodeId, targetNodeId, out candidate)) { return sourcePath.Select(point => new ElkPoint { X = point.X, Y = point.Y }).ToList(); } return candidate; } private static List BuildGatewaySourceRepairPath( IReadOnlyList path, ElkPositionedNode sourceNode, ElkPoint boundary, ElkPoint continuationPoint, int continuationIndex, ElkPoint referencePoint) { List? bestCandidate = null; var bestScore = double.PositiveInfinity; foreach (var exteriorApproach in ResolveGatewayExteriorApproachCandidates(sourceNode, boundary, referencePoint)) { foreach (var preferDirectContinuation in new[] { true, false }) { var rebuilt = new List { boundary }; if (!ElkEdgeRoutingGeometry.PointsEqual(boundary, exteriorApproach)) { rebuilt.Add(exteriorApproach); } if (!ElkEdgeRoutingGeometry.PointsEqual(rebuilt[^1], continuationPoint)) { var allowDirectContinuation = preferDirectContinuation && CanUseDirectGatewayContinuation(rebuilt[^1], continuationPoint); if (!allowDirectContinuation) { var curlRecoveryCorner = ResolveGatewaySourceCurlRecoveryCorner(path, rebuilt[^1], continuationPoint); if (curlRecoveryCorner is not null) { rebuilt.Add(curlRecoveryCorner); } else { AppendGatewayOrthogonalCorner( rebuilt, rebuilt[^1], continuationPoint, continuationIndex + 1 < path.Count ? path[continuationIndex + 1] : null, preferHorizontalFromReference: ShouldPreferHorizontalGatewayExit(rebuilt[^1], continuationPoint)); } } if (!ElkEdgeRoutingGeometry.PointsEqual(rebuilt[^1], continuationPoint)) { rebuilt.Add(continuationPoint); } } for (var i = continuationIndex + 1; i < path.Count; i++) { rebuilt.Add(path[i]); } var candidate = NormalizePathPoints(rebuilt); candidate = SnapGatewaySourceStubToDominantAxis(candidate, sourceNode, referencePoint); var score = ComputePathLength(candidate) + ScoreGatewayExitProgress(sourceNode, candidate, referencePoint); if (preferDirectContinuation) { score -= 6d; } if (HasGatewaySourceExitBacktracking(candidate) || HasGatewaySourceExitCurl(candidate)) { score += 100_000d; } if (HasGatewaySourceDominantAxisDetour(candidate, sourceNode)) { score += 100_000d; } if (HasGatewaySourcePreferredFaceMismatch(candidate, sourceNode)) { score += 50_000d; } if (NeedsDecisionSourcePreferredFaceRepair(candidate, sourceNode)) { score += 50_000d; } if (score >= bestScore) { continue; } bestScore = score; bestCandidate = candidate; } } return bestCandidate ?? path.Select(point => new ElkPoint { X = point.X, Y = point.Y }).ToList(); } private static ElkPoint? ResolveGatewaySourceCurlRecoveryCorner( IReadOnlyList path, ElkPoint from, ElkPoint to) { const double coordinateTolerance = 0.5d; if (!HasGatewaySourceExitCurl(path) || Math.Abs(from.X - to.X) <= coordinateTolerance || Math.Abs(from.Y - to.Y) <= coordinateTolerance) { return null; } var desiredDx = path[^1].X - path[0].X; var desiredDy = path[^1].Y - path[0].Y; if (Math.Abs(desiredDx) >= Math.Abs(desiredDy) * 1.25d && Math.Abs(desiredDy) > coordinateTolerance && Math.Sign(to.Y - from.Y) == Math.Sign(desiredDy)) { return new ElkPoint { X = from.X, Y = to.Y }; } if (Math.Abs(desiredDy) >= Math.Abs(desiredDx) * 1.25d && Math.Abs(desiredDx) > coordinateTolerance && Math.Sign(to.X - from.X) == Math.Sign(desiredDx)) { return new ElkPoint { X = to.X, Y = from.Y }; } return null; } private static List TryBuildDirectGatewaySourcePath( IReadOnlyList sourcePath, ElkPositionedNode sourceNode, IReadOnlyCollection nodes, string? sourceNodeId, string? targetNodeId) { var path = sourcePath .Select(point => new ElkPoint { X = point.X, Y = point.Y }) .ToList(); if (path.Count < 2 || !ElkShapeBoundaries.IsGatewayShape(sourceNode)) { return path; } var firstExteriorIndex = FindFirstGatewayExteriorPointIndex(path, sourceNode); List? bestCandidate = null; var bestScore = double.PositiveInfinity; foreach (var continuationIndex in EnumerateGatewayDirectRepairContinuationIndices(path, sourceNode, firstExteriorIndex)) { var continuationPoint = path[continuationIndex]; var boundaryCandidates = new List(); if (TryResolvePreferredGatewaySourceBoundary(sourceNode, continuationPoint, path[^1], out var preferredBoundary)) { AddUniquePoint(boundaryCandidates, preferredBoundary); } AddUniquePoint( boundaryCandidates, ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary( sourceNode, ElkShapeBoundaries.ProjectOntoShapeBoundary(sourceNode, continuationPoint), continuationPoint)); foreach (var candidateBoundary in ResolveGatewayExitBoundaryCandidates(sourceNode, path[^1])) { AddUniquePoint(boundaryCandidates, candidateBoundary); } foreach (var boundaryCandidate in boundaryCandidates) { var candidate = BuildGatewaySourceRepairPath( path, sourceNode, boundaryCandidate, continuationPoint, continuationIndex, path[^1]); if (!PathChanged(path, candidate)) { continue; } if (!HasAcceptableGatewayBoundaryPath(candidate, nodes, sourceNodeId, targetNodeId, sourceNode, fromStart: true) || !HasClearBoundarySegments(candidate, nodes, sourceNodeId, targetNodeId, true, Math.Min(4, candidate.Count - 1)) || HasGatewaySourceExitBacktracking(candidate) || HasGatewaySourceExitCurl(candidate) || HasGatewaySourceDominantAxisDetour(candidate, sourceNode) || HasGatewaySourcePreferredFaceMismatch(candidate, sourceNode)) { continue; } var score = ScoreGatewayDirectRepairCandidate(path, candidate, sourceNode, continuationIndex); if (score >= bestScore) { continue; } bestScore = score; bestCandidate = candidate; } } if (bestCandidate is null) { return path; } if (!IsMaterialGatewaySourceRepairImprovement(path, bestCandidate) && !IsGatewaySourceGeometryRepairImprovement(path, bestCandidate, sourceNode)) { return path; } return bestCandidate; } internal static bool HasClearGatewaySourceDirectRepairOpportunity( IReadOnlyList sourcePath, ElkPositionedNode sourceNode, IReadOnlyCollection nodes, string? sourceNodeId, string? targetNodeId) { if (!ElkShapeBoundaries.IsGatewayShape(sourceNode) || sourcePath.Count < 2) { return false; } var candidate = TryBuildDirectGatewaySourcePath( sourcePath, sourceNode, nodes, sourceNodeId, targetNodeId); return IsMaterialGatewaySourceRepairImprovement(sourcePath, candidate); } internal static bool HasClearGatewaySourceScoringOpportunity( IReadOnlyList sourcePath, ElkPositionedNode sourceNode, IReadOnlyCollection nodes, string? sourceNodeId, string? targetNodeId) { if (!ElkShapeBoundaries.IsGatewayShape(sourceNode) || sourcePath.Count < 2) { return false; } var candidate = HasProtectedGatewaySourceCorridorPath(sourcePath, nodes) ? TryBuildProtectedGatewaySourcePath( sourcePath, sourceNode, nodes, sourceNodeId, targetNodeId) : TryBuildDirectGatewaySourcePath( sourcePath, sourceNode, nodes, sourceNodeId, targetNodeId); if (!IsMaterialGatewaySourceRepairImprovement(sourcePath, candidate) || !HasAcceptableGatewayBoundaryPath(candidate, nodes, sourceNodeId, targetNodeId, sourceNode, fromStart: true)) { return false; } if (!TryNormalizeTargetBoundaryAfterSourceRepair( candidate, nodes, sourceNodeId, targetNodeId, out candidate)) { return false; } if (HasGatewaySourceExitBacktracking(candidate) || HasGatewaySourceExitCurl(candidate) || HasGatewaySourceDominantAxisDetour(candidate, sourceNode) || HasGatewaySourcePreferredFaceMismatch(candidate, sourceNode) || HasGatewaySourceLeadIntoDominantBlocker( candidate, sourceNode, nodes, sourceNodeId, targetNodeId)) { return false; } var lengthGain = ComputePathLength(sourcePath) - ComputePathLength(candidate); var originalBends = Math.Max(0, sourcePath.Count - 2); var candidateBends = Math.Max(0, candidate.Count - 2); if (lengthGain < 12d && candidateBends >= originalBends) { return false; } return IsMaterialGatewaySourceRepairImprovement(sourcePath, candidate); } private static List EnforceGatewaySourceExitQuality( IReadOnlyList sourcePath, ElkPositionedNode sourceNode, IReadOnlyCollection nodes, string? sourceNodeId, string? targetNodeId) { var path = sourcePath .Select(point => new ElkPoint { X = point.X, Y = point.Y }) .ToList(); if (!ElkShapeBoundaries.IsGatewayShape(sourceNode) || path.Count < 2) { return path; } var centerX = sourceNode.X + (sourceNode.Width / 2d); var centerY = sourceNode.Y + (sourceNode.Height / 2d); var desiredDx = path[^1].X - centerX; var desiredDy = path[^1].Y - centerY; var dominantHorizontal = Math.Abs(desiredDx) >= Math.Abs(desiredDy) * 1.25d && Math.Sign(desiredDx) != 0; var dominantVertical = Math.Abs(desiredDy) >= Math.Abs(desiredDx) * 1.25d && Math.Sign(desiredDy) != 0; var allowDominantAxisRepair = sourceNode.Kind is not "Decision" || dominantHorizontal || dominantVertical; var scoringCandidate = HasProtectedGatewaySourceCorridorPath(path, nodes) ? TryBuildProtectedGatewaySourcePath( path, sourceNode, nodes, sourceNodeId, targetNodeId) : TryBuildDirectGatewaySourcePath( path, sourceNode, nodes, sourceNodeId, targetNodeId); var directDominantCandidate = TryBuildDirectDominantGatewaySourcePath(path, sourceNode, nodes, sourceNodeId, targetNodeId); var hasDominantDirectOpportunity = allowDominantAxisRepair && PathChanged(path, directDominantCandidate) && ComputePathLength(directDominantCandidate) + 1d < ComputePathLength(path); var requiresRepair = HasGatewaySourceExitBacktracking(path) || HasGatewaySourceExitCurl(path) || hasDominantDirectOpportunity || (allowDominantAxisRepair && HasGatewaySourcePreferredFaceMismatch(path, sourceNode)) || (allowDominantAxisRepair && HasGatewaySourceDominantAxisDetour(path, sourceNode)) || (allowDominantAxisRepair && HasClearGatewaySourceScoringOpportunity(path, sourceNode, nodes, sourceNodeId, targetNodeId)); if (!requiresRepair) { return path; } List? bestCandidate = null; var bestScore = double.PositiveInfinity; void ConsiderCandidate(IReadOnlyList rawCandidate) { if (!PathChanged(path, rawCandidate)) { return; } var candidate = rawCandidate .Select(point => new ElkPoint { X = point.X, Y = point.Y }) .ToList(); if (!TryNormalizeTargetBoundaryAfterSourceRepair( candidate, nodes, sourceNodeId, targetNodeId, out candidate)) { return; } if (!HasAcceptableGatewayBoundaryPath(candidate, nodes, sourceNodeId, targetNodeId, sourceNode, fromStart: true) || HasGatewaySourceExitBacktracking(candidate) || HasGatewaySourceExitCurl(candidate) || HasGatewaySourceDominantAxisDetour(candidate, sourceNode) || HasGatewaySourcePreferredFaceMismatch(candidate, sourceNode) || HasGatewaySourceLeadIntoDominantBlocker(candidate, sourceNode, nodes, sourceNodeId, targetNodeId) || HasClearGatewaySourceScoringOpportunity(candidate, sourceNode, nodes, sourceNodeId, targetNodeId)) { return; } var score = ComputePathLength(candidate) + (Math.Max(0, candidate.Count - 2) * 6d); if (score >= bestScore) { return; } bestScore = score; bestCandidate = candidate; } ConsiderCandidate(scoringCandidate); ConsiderCandidate(directDominantCandidate); ConsiderCandidate(TryBuildDirectGatewaySourcePath(path, sourceNode, nodes, sourceNodeId, targetNodeId)); ConsiderCandidate(ForceGatewaySourcePreferredFaceAlignment(path, sourceNode)); ConsiderCandidate(FixGatewaySourceDominantAxisDetour(path, sourceNode)); ConsiderCandidate(NormalizeGatewayExitPath(path, sourceNode, nodes, sourceNodeId, targetNodeId)); return bestCandidate ?? path; } private static List TryBuildDirectDominantGatewaySourcePath( IReadOnlyList sourcePath, ElkPositionedNode sourceNode, IReadOnlyCollection nodes, string? sourceNodeId, string? targetNodeId) { var path = sourcePath .Select(point => new ElkPoint { X = point.X, Y = point.Y }) .ToList(); if (!ElkShapeBoundaries.IsGatewayShape(sourceNode) || path.Count < 2 || string.IsNullOrWhiteSpace(targetNodeId)) { return path; } var targetNode = nodes.FirstOrDefault(node => string.Equals(node.Id, targetNodeId, StringComparison.Ordinal)); if (targetNode is null || ElkShapeBoundaries.IsGatewayShape(targetNode)) { return path; } var centerX = sourceNode.X + (sourceNode.Width / 2d); var centerY = sourceNode.Y + (sourceNode.Height / 2d); var targetCenterX = targetNode.X + (targetNode.Width / 2d); var targetCenterY = targetNode.Y + (targetNode.Height / 2d); var desiredDx = targetCenterX - centerX; var desiredDy = targetCenterY - centerY; var dominantHorizontal = Math.Abs(desiredDx) >= Math.Abs(desiredDy) * 1.25d && Math.Sign(desiredDx) != 0; var dominantVertical = Math.Abs(desiredDy) >= Math.Abs(desiredDx) * 1.25d && Math.Sign(desiredDy) != 0; if (!dominantHorizontal && !dominantVertical) { return path; } var firstExteriorIndex = FindFirstGatewayExteriorPointIndex(path, sourceNode); var continuationIndex = FindPreferredGatewayExitContinuationIndex(path, sourceNode, firstExteriorIndex); var continuationPoint = path[continuationIndex]; if (!TryResolvePreferredGatewaySourceBoundary(sourceNode, continuationPoint, path[^1], out var boundary)) { return path; } var targetEndpoint = dominantHorizontal ? new ElkPoint { X = desiredDx >= 0d ? targetNode.X : targetNode.X + targetNode.Width, Y = Math.Clamp(boundary.Y, targetNode.Y, targetNode.Y + targetNode.Height), } : new ElkPoint { X = Math.Clamp(boundary.X, targetNode.X, targetNode.X + targetNode.Width), Y = desiredDy >= 0d ? targetNode.Y : targetNode.Y + targetNode.Height, }; 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 (SegmentCrossesObstacle(boundary, targetEndpoint, obstacles, sourceNodeId ?? string.Empty, targetNodeId)) { var bypassCandidate = TryBuildDominantAxisGatewaySourceBypassPath( sourceNode, targetNode, boundary, targetEndpoint, obstacles, sourceNodeId, targetNodeId, dominantHorizontal, desiredDx, desiredDy); return bypassCandidate ?? path; } var rebuilt = new List { boundary }; var gatewayStub = dominantHorizontal ? new ElkPoint { X = boundary.X + (desiredDx >= 0d ? 24d : -24d), Y = boundary.Y, } : new ElkPoint { X = boundary.X, Y = boundary.Y + (desiredDy >= 0d ? 24d : -24d), }; if (!ElkEdgeRoutingGeometry.PointsEqual(rebuilt[^1], gatewayStub)) { rebuilt.Add(gatewayStub); } AppendNonGatewayTargetBoundaryApproach(rebuilt, targetNode, targetEndpoint); return NormalizePathPoints(rebuilt); } private static List? TryBuildDominantAxisGatewaySourceBypassPath( ElkPositionedNode sourceNode, ElkPositionedNode targetNode, ElkPoint boundary, ElkPoint targetEndpoint, (double Left, double Top, double Right, double Bottom, string Id)[] obstacles, string? sourceNodeId, string? targetNodeId, bool dominantHorizontal, double desiredDx, double desiredDy) { const double padding = 8d; const double coordinateTolerance = 0.5d; var sourceId = sourceNodeId ?? string.Empty; var targetId = targetNodeId ?? string.Empty; List? bestCandidate = null; var bestScore = double.PositiveInfinity; void ConsiderCandidate(List rawCandidate) { var candidate = NormalizePathPoints(rawCandidate); if (candidate.Count < 2 || !IsPathClearOfObstacles(candidate, obstacles, sourceId, targetId) || !HasValidBoundaryAngle(candidate[^1], candidate[^2], targetNode) || HasGatewaySourceExitBacktracking(candidate) || HasGatewaySourceExitCurl(candidate) || HasGatewaySourceDominantAxisDetour(candidate, sourceNode) || HasGatewaySourcePreferredFaceMismatch(candidate, sourceNode)) { return; } var score = ComputePathLength(candidate) + (Math.Max(0, candidate.Count - 2) * 6d); if (score >= bestScore) { return; } bestScore = score; bestCandidate = candidate; } if (dominantHorizontal) { var movingRight = desiredDx >= 0d; var firstBlocker = obstacles .Where(ob => !string.Equals(ob.Id, sourceId, StringComparison.Ordinal) && !string.Equals(ob.Id, targetId, StringComparison.Ordinal) && boundary.Y > ob.Top + coordinateTolerance && boundary.Y < ob.Bottom - coordinateTolerance && (movingRight ? ob.Left > boundary.X + coordinateTolerance && ob.Left < targetEndpoint.X - coordinateTolerance : ob.Right < boundary.X - coordinateTolerance && ob.Right > targetEndpoint.X + coordinateTolerance)) .OrderBy(ob => movingRight ? ob.Left : -ob.Right) .FirstOrDefault(); if (string.IsNullOrWhiteSpace(firstBlocker.Id)) { return null; } var axisX = movingRight ? firstBlocker.Left - padding : firstBlocker.Right + padding; if (movingRight ? axisX <= boundary.X + 2d : axisX >= boundary.X - 2d) { return null; } var bypassYCandidates = new List(); AddUniqueCoordinate(bypassYCandidates, targetEndpoint.Y); AddUniqueCoordinate(bypassYCandidates, firstBlocker.Top - padding); AddUniqueCoordinate(bypassYCandidates, firstBlocker.Bottom + padding); foreach (var bypassY in bypassYCandidates) { var diagonalLead = new List { boundary }; var diagonalLeadPoint = new ElkPoint { X = axisX, Y = bypassY }; if (!ElkEdgeRoutingGeometry.PointsEqual(diagonalLead[^1], diagonalLeadPoint)) { diagonalLead.Add(diagonalLeadPoint); } AppendNonGatewayTargetBoundaryApproach(diagonalLead, targetNode, targetEndpoint); ConsiderCandidate(diagonalLead); var rebuilt = new List { boundary, new() { X = axisX, Y = boundary.Y }, }; if (Math.Abs(rebuilt[^1].Y - bypassY) > coordinateTolerance) { rebuilt.Add(new ElkPoint { X = axisX, Y = bypassY }); } AppendNonGatewayTargetBoundaryApproach(rebuilt, targetNode, targetEndpoint); ConsiderCandidate(rebuilt); } return bestCandidate; } var movingDown = desiredDy >= 0d; var verticalBlocker = obstacles .Where(ob => !string.Equals(ob.Id, sourceId, StringComparison.Ordinal) && !string.Equals(ob.Id, targetId, StringComparison.Ordinal) && boundary.X > ob.Left + coordinateTolerance && boundary.X < ob.Right - coordinateTolerance && (movingDown ? ob.Top > boundary.Y + coordinateTolerance && ob.Top < targetEndpoint.Y - coordinateTolerance : ob.Bottom < boundary.Y - coordinateTolerance && ob.Bottom > targetEndpoint.Y + coordinateTolerance)) .OrderBy(ob => movingDown ? ob.Top : -ob.Bottom) .FirstOrDefault(); if (string.IsNullOrWhiteSpace(verticalBlocker.Id)) { return null; } var axisY = movingDown ? verticalBlocker.Top - padding : verticalBlocker.Bottom + padding; if (movingDown ? axisY <= boundary.Y + 2d : axisY >= boundary.Y - 2d) { return null; } var bypassXCandidates = new List(); AddUniqueCoordinate(bypassXCandidates, targetEndpoint.X); AddUniqueCoordinate(bypassXCandidates, verticalBlocker.Left - padding); AddUniqueCoordinate(bypassXCandidates, verticalBlocker.Right + padding); foreach (var bypassX in bypassXCandidates) { var diagonalLead = new List { boundary }; var diagonalLeadPoint = new ElkPoint { X = bypassX, Y = axisY }; if (!ElkEdgeRoutingGeometry.PointsEqual(diagonalLead[^1], diagonalLeadPoint)) { diagonalLead.Add(diagonalLeadPoint); } AppendNonGatewayTargetBoundaryApproach(diagonalLead, targetNode, targetEndpoint); ConsiderCandidate(diagonalLead); var rebuilt = new List { boundary, new() { X = boundary.X, Y = axisY }, }; if (Math.Abs(rebuilt[^1].X - bypassX) > coordinateTolerance) { rebuilt.Add(new ElkPoint { X = bypassX, Y = axisY }); } AppendNonGatewayTargetBoundaryApproach(rebuilt, targetNode, targetEndpoint); ConsiderCandidate(rebuilt); } return bestCandidate; } private static bool IsPathClearOfObstacles( IReadOnlyList path, (double Left, double Top, double Right, double Bottom, string Id)[] obstacles, string sourceId, string targetId) { for (var i = 0; i < path.Count - 1; i++) { if (SegmentCrossesObstacle(path[i], path[i + 1], obstacles, sourceId, targetId)) { return false; } } return true; } private static void AppendNonGatewayTargetBoundaryApproach( ICollection rawPoints, ElkPositionedNode targetNode, ElkPoint targetEndpoint) { var rebuilt = rawPoints as List; if (rebuilt is null || rebuilt.Count == 0) { return; } var targetSide = ElkEdgeRoutingGeometry.ResolveBoundarySide(targetEndpoint, targetNode); ElkPoint approachPoint; switch (targetSide) { case "left": approachPoint = new ElkPoint { X = targetEndpoint.X - 24d, Y = targetEndpoint.Y }; break; case "right": approachPoint = new ElkPoint { X = targetEndpoint.X + 24d, Y = targetEndpoint.Y }; break; case "top": approachPoint = new ElkPoint { X = targetEndpoint.X, Y = targetEndpoint.Y - 24d }; break; case "bottom": approachPoint = new ElkPoint { X = targetEndpoint.X, Y = targetEndpoint.Y + 24d }; break; default: approachPoint = targetEndpoint; break; } if (!ElkEdgeRoutingGeometry.PointsEqual(rebuilt[^1], approachPoint)) { AppendGatewayOrthogonalCorner( rebuilt, rebuilt[^1], approachPoint, null, preferHorizontalFromReference: targetSide is "top" or "bottom"); if (!ElkEdgeRoutingGeometry.PointsEqual(rebuilt[^1], approachPoint)) { rebuilt.Add(approachPoint); } } if (!ElkEdgeRoutingGeometry.PointsEqual(rebuilt[^1], targetEndpoint)) { rebuilt.Add(targetEndpoint); } } private static bool HasGatewaySourceLeadIntoDominantBlocker( IReadOnlyList path, ElkPositionedNode sourceNode, IReadOnlyCollection nodes, string? sourceNodeId, string? targetNodeId) { if (!ElkShapeBoundaries.IsGatewayShape(sourceNode) || path.Count < 2) { return false; } const double tolerance = 0.5d; var centerX = sourceNode.X + (sourceNode.Width / 2d); var centerY = sourceNode.Y + (sourceNode.Height / 2d); var desiredDx = path[^1].X - centerX; var desiredDy = path[^1].Y - centerY; var dominantHorizontal = Math.Abs(desiredDx) >= Math.Abs(desiredDy) * 1.25d && Math.Sign(desiredDx) != 0; var dominantVertical = Math.Abs(desiredDy) >= Math.Abs(desiredDx) * 1.25d && Math.Sign(desiredDy) != 0; if (!dominantHorizontal && !dominantVertical) { return false; } var boundary = path[0]; var adjacent = path[1]; if (dominantHorizontal) { var movingRight = desiredDx > 0d; var blocker = nodes .Where(node => !string.Equals(node.Id, sourceNodeId, StringComparison.Ordinal) && !string.Equals(node.Id, targetNodeId, StringComparison.Ordinal) && boundary.Y > node.Y + tolerance && boundary.Y < node.Y + node.Height - tolerance && (movingRight ? node.X > boundary.X + tolerance && node.X < path[^1].X - tolerance : node.X + node.Width < boundary.X - tolerance && node.X + node.Width > path[^1].X + tolerance)) .OrderBy(node => movingRight ? node.X : -(node.X + node.Width)) .FirstOrDefault(); if (blocker is null) { return false; } return adjacent.Y > blocker.Y + tolerance && adjacent.Y < blocker.Y + blocker.Height - tolerance; } var movingDown = desiredDy > 0d; var verticalBlocker = nodes .Where(node => !string.Equals(node.Id, sourceNodeId, StringComparison.Ordinal) && !string.Equals(node.Id, targetNodeId, StringComparison.Ordinal) && boundary.X > node.X + tolerance && boundary.X < node.X + node.Width - tolerance && (movingDown ? node.Y > boundary.Y + tolerance && node.Y < path[^1].Y - tolerance : node.Y + node.Height < boundary.Y - tolerance && node.Y + node.Height > path[^1].Y + tolerance)) .OrderBy(node => movingDown ? node.Y : -(node.Y + node.Height)) .FirstOrDefault(); if (verticalBlocker is null) { return false; } return adjacent.X > verticalBlocker.X + tolerance && adjacent.X < verticalBlocker.X + verticalBlocker.Width - tolerance; } private static bool TryNormalizeTargetBoundaryAfterSourceRepair( IReadOnlyList candidatePath, IReadOnlyCollection nodes, string? sourceNodeId, string? targetNodeId, out List normalized) { normalized = candidatePath .Select(point => new ElkPoint { X = point.X, Y = point.Y }) .ToList(); if (string.IsNullOrWhiteSpace(targetNodeId) || normalized.Count < 2) { return true; } var targetNode = nodes.FirstOrDefault(node => string.Equals(node.Id, targetNodeId, StringComparison.Ordinal)); if (targetNode is null) { return true; } if (ElkShapeBoundaries.IsGatewayShape(targetNode)) { if (!NeedsGatewayTargetBoundaryRepair(normalized, targetNode)) { return true; } var repairedTargetCandidate = NormalizeGatewayEntryPath(normalized, targetNode, normalized[^1]); if (!CanAcceptGatewayTargetRepair(repairedTargetCandidate, targetNode)) { return false; } normalized = repairedTargetCandidate; return true; } if (TryRealignNonGatewayTargetBoundarySlot(normalized, targetNode, nodes, sourceNodeId, targetNodeId, out var realignedTargetCandidate)) { normalized = realignedTargetCandidate; } if (HasValidBoundaryAngle(normalized[^1], normalized[^2], targetNode)) { return true; } var targetSide = ElkEdgeRoutingGeometry.ResolveBoundarySide(normalized[^1], targetNode); var repairedNonGatewayTarget = NormalizeEntryPath(normalized, targetNode, targetSide); if (!HasClearBoundarySegments(repairedNonGatewayTarget, nodes, sourceNodeId, targetNodeId, false, 3) || !HasValidBoundaryAngle(repairedNonGatewayTarget[^1], repairedNonGatewayTarget[^2], targetNode)) { return false; } normalized = repairedNonGatewayTarget; return true; } private static bool TryRealignNonGatewayTargetBoundarySlot( IReadOnlyList path, ElkPositionedNode targetNode, IReadOnlyCollection nodes, string? sourceNodeId, string? targetNodeId, out List realigned) { realigned = path .Select(point => new ElkPoint { X = point.X, Y = point.Y }) .ToList(); if (path.Count < 2) { return false; } var side = ElkEdgeRoutingGeometry.ResolveBoundarySide(path[^1], targetNode); if (side is not "left" and not "right" and not "top" and not "bottom") { return false; } var approach = path[^2]; var candidateEndpoint = side switch { "left" => new ElkPoint { X = targetNode.X, Y = Math.Clamp(approach.Y, targetNode.Y, targetNode.Y + targetNode.Height), }, "right" => new ElkPoint { X = targetNode.X + targetNode.Width, Y = Math.Clamp(approach.Y, targetNode.Y, targetNode.Y + targetNode.Height), }, "top" => new ElkPoint { X = Math.Clamp(approach.X, targetNode.X, targetNode.X + targetNode.Width), Y = targetNode.Y, }, "bottom" => new ElkPoint { X = Math.Clamp(approach.X, targetNode.X, targetNode.X + targetNode.Width), Y = targetNode.Y + targetNode.Height, }, _ => path[^1], }; if (ElkEdgeRoutingGeometry.PointsEqual(candidateEndpoint, path[^1])) { return false; } realigned[^1] = candidateEndpoint; realigned = NormalizePathPoints(realigned); if (!HasClearBoundarySegments(realigned, nodes, sourceNodeId, targetNodeId, false, 3) || !HasValidBoundaryAngle(realigned[^1], realigned[^2], targetNode)) { realigned = path .Select(point => new ElkPoint { X = point.X, Y = point.Y }) .ToList(); return false; } var originalLength = ComputePathLength(path); var realignedLength = ComputePathLength(realigned); if (realignedLength + 0.5d < originalLength) { return true; } var directlyAligned = side is "left" or "right" ? Math.Abs(realigned[^1].Y - approach.Y) <= 0.6d : Math.Abs(realigned[^1].X - approach.X) <= 0.6d; return directlyAligned && realignedLength <= originalLength + 0.5d; } private static IEnumerable EnumerateGatewayDirectRepairContinuationIndices( IReadOnlyList path, ElkPositionedNode sourceNode, int firstExteriorIndex) { if (path.Count <= firstExteriorIndex) { yield return firstExteriorIndex; yield break; } var preferredIndex = FindPreferredGatewayExitContinuationIndex(path, sourceNode, firstExteriorIndex); var curlRecoveryIndex = FindGatewaySourceCurlRecoveryIndex(path, firstExteriorIndex); var seen = new HashSet(); var candidates = new[] { firstExteriorIndex, Math.Min(path.Count - 1, firstExteriorIndex + 1), Math.Min(path.Count - 1, firstExteriorIndex + 2), preferredIndex, Math.Min(path.Count - 1, preferredIndex + 1), curlRecoveryIndex ?? -1, curlRecoveryIndex is int recoveryIndex ? Math.Min(path.Count - 1, recoveryIndex + 1) : -1, }; foreach (var candidate in candidates) { if (candidate < firstExteriorIndex || candidate >= path.Count || ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(sourceNode, path[candidate]) || !seen.Add(candidate)) { continue; } yield return candidate; } } private static double ScoreGatewayDirectRepairCandidate( IReadOnlyList originalPath, IReadOnlyList candidate, ElkPositionedNode sourceNode, int continuationIndex) { var score = ComputePathLength(candidate) + (Math.Max(0, candidate.Count - 2) * 4d) + (continuationIndex * 6d) + ScoreGatewayExitProgress(sourceNode, candidate, originalPath[^1]); if (HasGatewaySourceExitBacktracking(candidate) || HasGatewaySourceExitCurl(candidate)) { score += 100_000d; } if (HasGatewaySourceDominantAxisDetour(candidate, sourceNode)) { score += 50_000d; } if (HasGatewaySourcePreferredFaceMismatch(candidate, sourceNode)) { score += 25_000d; } return score; } private static bool IsMaterialGatewaySourceRepairImprovement( IReadOnlyList originalPath, IReadOnlyList candidate) { if (!PathChanged(originalPath, candidate)) { return false; } var originalLength = ComputePathLength(originalPath); var candidateLength = ComputePathLength(candidate); var originalBends = Math.Max(0, originalPath.Count - 2); var candidateBends = Math.Max(0, candidate.Count - 2); var lengthGain = originalLength - candidateLength; if (originalPath.Count <= 3 && lengthGain < 24d && candidateBends <= originalBends) { return false; } if (lengthGain > 4d) { return true; } if (lengthGain > 1d && candidateBends <= originalBends) { return true; } if (candidateBends + 1 < originalBends && candidateLength <= originalLength + 4d) { return true; } return candidateBends < originalBends && candidateLength <= originalLength + 1d; } private static bool IsGatewaySourceGeometryRepairImprovement( IReadOnlyList originalPath, IReadOnlyList candidate, ElkPositionedNode sourceNode) { if (!PathChanged(originalPath, candidate)) { return false; } var originalHasGeometryDefect = NeedsGatewaySourceBoundaryRepair(originalPath, sourceNode) || HasGatewaySourceExitBacktracking(originalPath) || HasGatewaySourceExitCurl(originalPath) || HasGatewaySourceDominantAxisDetour(originalPath, sourceNode) || HasGatewaySourcePreferredFaceMismatch(originalPath, sourceNode) || NeedsDecisionSourcePreferredFaceRepair(originalPath, sourceNode); if (!originalHasGeometryDefect) { return false; } var candidateIsClean = !NeedsGatewaySourceBoundaryRepair(candidate, sourceNode) && !HasGatewaySourceExitBacktracking(candidate) && !HasGatewaySourceExitCurl(candidate) && !HasGatewaySourceDominantAxisDetour(candidate, sourceNode) && !HasGatewaySourcePreferredFaceMismatch(candidate, sourceNode) && !NeedsDecisionSourcePreferredFaceRepair(candidate, sourceNode); if (!candidateIsClean) { return false; } var originalLength = ComputePathLength(originalPath); var candidateLength = ComputePathLength(candidate); return candidateLength <= originalLength + 120d; } private static bool TryResolvePreferredGatewaySourceBoundary( ElkPositionedNode sourceNode, ElkPoint referencePoint, out ElkPoint boundary) { return TryResolvePreferredGatewaySourceBoundary( sourceNode, referencePoint, referencePoint, out boundary); } private static bool TryResolvePreferredGatewaySourceBoundary( ElkPositionedNode sourceNode, ElkPoint continuationPoint, ElkPoint referencePoint, out ElkPoint boundary) { boundary = default!; if (!ElkShapeBoundaries.IsGatewayShape(sourceNode)) { return false; } if (sourceNode.Kind == "Decision") { boundary = ResolveDecisionSourceExitBoundary(sourceNode, continuationPoint, referencePoint); return true; } foreach (var preferredSide in EnumeratePreferredGatewaySourceSides(sourceNode, continuationPoint, referencePoint)) { foreach (var candidate in ResolveGatewaySourceBoundarySlotCandidates(sourceNode, preferredSide, continuationPoint, referencePoint)) { boundary = candidate; return true; } } boundary = sourceNode.Kind == "Decision" ? ResolveDecisionSourceExitBoundary(sourceNode, continuationPoint, referencePoint) : ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary( sourceNode, ElkShapeBoundaries.ProjectOntoShapeBoundary(sourceNode, continuationPoint), continuationPoint); return true; } private static bool HasProtectedGatewaySourceCorridorPath( IReadOnlyList path, IReadOnlyCollection nodes) { if (path.Count < 3 || nodes.Count == 0) { return false; } var graphMinY = nodes.Min(node => node.Y); var graphMaxY = nodes.Max(node => node.Y + node.Height); return path.Skip(1).Any(point => point.Y < graphMinY - 8d || point.Y > graphMaxY + 8d); } private static List SnapGatewaySourceStubToDominantAxis( IReadOnlyList sourcePath, ElkPositionedNode sourceNode, ElkPoint referencePoint) { if (!ElkShapeBoundaries.IsGatewayShape(sourceNode) || sourcePath.Count < 2) { return sourcePath.Select(point => new ElkPoint { X = point.X, Y = point.Y }).ToList(); } const double axisTolerance = 4d; var boundary = sourcePath[0]; var adjacent = sourcePath[1]; var desiredDx = referencePoint.X - boundary.X; var desiredDy = referencePoint.Y - boundary.Y; var dominantHorizontal = Math.Abs(desiredDx) >= Math.Abs(desiredDy) * 1.15d && Math.Sign(desiredDx) != 0; var dominantVertical = Math.Abs(desiredDy) >= Math.Abs(desiredDx) * 1.15d && Math.Sign(desiredDy) != 0; if (!dominantHorizontal && !dominantVertical) { return sourcePath.Select(point => new ElkPoint { X = point.X, Y = point.Y }).ToList(); } var snapped = sourcePath .Select(point => new ElkPoint { X = point.X, Y = point.Y }) .ToList(); if (dominantHorizontal && Math.Abs(adjacent.Y - boundary.Y) <= axisTolerance) { snapped[1] = new ElkPoint { X = adjacent.X, Y = boundary.Y, }; } else if (dominantVertical && Math.Abs(adjacent.X - boundary.X) <= axisTolerance) { snapped[1] = new ElkPoint { X = boundary.X, Y = adjacent.Y, }; } return NormalizePathPoints(snapped); } private static bool HasAxisReversalFromStart(IEnumerable values, double desiredDelta) { const double tolerance = 0.5d; var distinctValues = new List(); foreach (var value in values) { if (distinctValues.Count == 0 || Math.Abs(distinctValues[^1] - value) > tolerance) { distinctValues.Add(value); } } if (distinctValues.Count < 3) { return false; } var nonZeroDirections = new List(); for (var i = 1; i < distinctValues.Count; i++) { var delta = distinctValues[i] - distinctValues[i - 1]; if (Math.Abs(delta) <= tolerance) { continue; } nonZeroDirections.Add(Math.Sign(delta)); } if (nonZeroDirections.Count < 2) { return false; } if (Math.Abs(desiredDelta) <= tolerance) { return nonZeroDirections.Distinct().Count() > 1; } var desiredSign = Math.Sign(desiredDelta); var sawOpposite = false; foreach (var direction in nonZeroDirections) { if (direction == desiredSign) { if (sawOpposite) { return true; } continue; } sawOpposite = true; } return false; } private static List NormalizeGatewayEntryPath( IReadOnlyList sourcePath, ElkPositionedNode targetNode, ElkPoint assignedEndpoint) { 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 actualAdjacent = path[^2]; var assignedApproach = ElkShapeBoundaries.IsGatewayBoundaryPoint(targetNode, assignedEndpoint) ? ElkShapeBoundaries.BuildGatewayDirectionalExteriorPoint(targetNode, assignedEndpoint, exteriorAnchor) : assignedEndpoint; ElkPoint boundary; var assignedEndpointUsable = ElkShapeBoundaries.IsGatewayBoundaryPoint(targetNode, assignedEndpoint) && !ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(targetNode, assignedApproach) && ElkShapeBoundaries.HasValidGatewayBoundaryAngle(targetNode, assignedEndpoint, assignedApproach) && !ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(targetNode, exteriorAnchor); if (assignedEndpointUsable) { boundary = assignedEndpoint; } else { var boundaryCandidates = ResolveGatewayEntryBoundaryCandidates(targetNode, exteriorAnchor, assignedEndpoint).ToArray(); boundary = boundaryCandidates.Length > 0 ? boundaryCandidates .OrderBy(candidate => ScoreGatewayEntryBoundaryCandidate(targetNode, candidate, exteriorAnchor, assignedEndpoint)) .First() : ElkShapeBoundaries.ProjectOntoShapeBoundary(targetNode, exteriorAnchor); if (!ElkShapeBoundaries.IsGatewayBoundaryPoint(targetNode, boundary) || !ElkShapeBoundaries.HasValidGatewayBoundaryAngle(targetNode, boundary, exteriorAnchor)) { var fallbackBoundary = ElkShapeBoundaries.ProjectOntoShapeBoundary(targetNode, exteriorAnchor); if (!ElkShapeBoundaries.TryProjectGatewayDiagonalBoundary(targetNode, exteriorAnchor, fallbackBoundary, out boundary)) { boundary = fallbackBoundary; } } } boundary = ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(targetNode, boundary, exteriorAnchor); var directEntryCandidate = TryBuildDirectGatewayTargetEntry( path, targetNode, exteriorIndex, exteriorAnchor, boundary, assignedEndpoint); if (ShouldPreferDirectGatewayTargetEntry( directEntryCandidate, targetNode, assignedEndpoint, preserveAssignedSlot: assignedEndpointUsable)) { return directEntryCandidate!; } var exteriorApproach = ElkShapeBoundaries.BuildGatewayDirectionalExteriorPoint(targetNode, boundary, exteriorAnchor); var rebuilt = path.Take(exteriorIndex + 1).ToList(); if (rebuilt.Count == 0 || !ElkEdgeRoutingGeometry.PointsEqual(rebuilt[^1], exteriorAnchor)) { rebuilt.Add(exteriorAnchor); } AppendGatewayTargetOrthogonalCorner( rebuilt, rebuilt[^1], exteriorApproach, rebuilt.Count >= 2 ? rebuilt[^2] : null, preferHorizontalFromReference: ShouldPreferHorizontalGatewayExit(rebuilt[^1], exteriorApproach), targetNode); if (!ElkEdgeRoutingGeometry.PointsEqual(rebuilt[^1], exteriorApproach)) { rebuilt.Add(exteriorApproach); } rebuilt.Add(boundary); var normalizedRebuilt = NormalizePathPoints(rebuilt); if (normalizedRebuilt.Count >= 2 && ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(targetNode, normalizedRebuilt[^2])) { var repairedAnchorIndex = FindLastGatewayExteriorPointIndex(normalizedRebuilt, targetNode); var repairedAnchor = normalizedRebuilt[repairedAnchorIndex]; var repairedApproach = ElkShapeBoundaries.BuildGatewayDirectionalExteriorPoint(targetNode, boundary, repairedAnchor); var repaired = normalizedRebuilt.Take(repairedAnchorIndex + 1).ToList(); if (repaired.Count == 0 || !ElkEdgeRoutingGeometry.PointsEqual(repaired[^1], repairedAnchor)) { repaired.Add(repairedAnchor); } AppendGatewayTargetOrthogonalCorner( repaired, repaired[^1], repairedApproach, repaired.Count >= 2 ? repaired[^2] : null, preferHorizontalFromReference: ShouldPreferHorizontalGatewayExit(repaired[^1], repairedApproach), targetNode); if (!ElkEdgeRoutingGeometry.PointsEqual(repaired[^1], repairedApproach)) { repaired.Add(repairedApproach); } repaired.Add(boundary); normalizedRebuilt = NormalizePathPoints(repaired); } if (normalizedRebuilt.Count >= 2 && (ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(targetNode, normalizedRebuilt[^2]) || !ElkShapeBoundaries.HasValidGatewayBoundaryAngle(targetNode, normalizedRebuilt[^1], normalizedRebuilt[^2]))) { normalizedRebuilt = ForceGatewayExteriorTargetApproach( normalizedRebuilt, targetNode, boundary); } normalizedRebuilt = PreferGatewayDiagonalTargetEntry(normalizedRebuilt, targetNode); if (normalizedRebuilt.Count >= 2 && !ElkShapeBoundaries.HasValidGatewayBoundaryAngle(targetNode, normalizedRebuilt[^1], normalizedRebuilt[^2])) { var slottedGatewayTargetRepair = TryBuildSlottedGatewayEntryPath(path, targetNode, exteriorIndex, exteriorAnchor); if (slottedGatewayTargetRepair is not null && ElkShapeBoundaries.HasValidGatewayBoundaryAngle(targetNode, slottedGatewayTargetRepair[^1], slottedGatewayTargetRepair[^2])) { normalizedRebuilt = slottedGatewayTargetRepair; } } var preserveAssignedSlot = assignedEndpointUsable; if (directEntryCandidate is not null && (!preserveAssignedSlot || ElkEdgeRoutingGeometry.ComputeSegmentLength(directEntryCandidate[^1], assignedEndpoint) <= 6d) && (normalizedRebuilt.Count < 2 || !ElkShapeBoundaries.HasValidGatewayBoundaryAngle(targetNode, normalizedRebuilt[^1], normalizedRebuilt[^2]) || ComputePathLength(directEntryCandidate) <= ComputePathLength(normalizedRebuilt) + 2d || directEntryCandidate.Count < normalizedRebuilt.Count)) { normalizedRebuilt = directEntryCandidate; } normalizedRebuilt = CollapseGatewayTargetTailIfPossible(normalizedRebuilt, targetNode); return normalizedRebuilt; } private static List ForceGatewayTargetBoundaryStub( IReadOnlyList sourcePath, ElkPositionedNode targetNode) { var path = sourcePath .Select(point => new ElkPoint { X = point.X, Y = point.Y }) .ToList(); if (path.Count < 2) { return path; } var boundary = path[^1]; var exteriorAnchor = path[^2]; var exteriorApproach = ElkShapeBoundaries.BuildGatewayDirectionalExteriorPoint(targetNode, boundary, exteriorAnchor); if (ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(targetNode, exteriorApproach) || !ElkShapeBoundaries.HasValidGatewayBoundaryAngle(targetNode, boundary, exteriorApproach)) { return path; } var rebuilt = path.Take(path.Count - 1) .Select(point => new ElkPoint { X = point.X, Y = point.Y }) .ToList(); if (rebuilt.Count == 0 || !ElkEdgeRoutingGeometry.PointsEqual(rebuilt[^1], exteriorAnchor)) { rebuilt.Add(exteriorAnchor); } AppendGatewayTargetOrthogonalCorner( rebuilt, rebuilt[^1], exteriorApproach, rebuilt.Count >= 2 ? rebuilt[^2] : null, preferHorizontalFromReference: ShouldPreferHorizontalGatewayExit(rebuilt[^1], exteriorApproach), targetNode); if (!ElkEdgeRoutingGeometry.PointsEqual(rebuilt[^1], exteriorApproach)) { rebuilt.Add(exteriorApproach); } rebuilt.Add(boundary); var normalized = NormalizePathPoints(rebuilt); return normalized.Count >= 2 && !ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(targetNode, normalized[^2]) && ElkShapeBoundaries.HasValidGatewayBoundaryAngle(targetNode, normalized[^1], normalized[^2]) ? normalized : path; } private static List? TryBuildSlottedGatewayEntryPath( IReadOnlyList sourcePath, ElkPositionedNode targetNode, int exteriorIndex, ElkPoint exteriorAnchor) { if (!ElkShapeBoundaries.IsGatewayShape(targetNode)) { return null; } var centerX = targetNode.X + (targetNode.Width / 2d); var centerY = targetNode.Y + (targetNode.Height / 2d); var deltaX = exteriorAnchor.X - centerX; var deltaY = exteriorAnchor.Y - centerY; string side; double slotCoordinate; if (Math.Abs(deltaX) >= Math.Abs(deltaY) * 1.15d) { side = deltaX <= 0d ? "left" : "right"; slotCoordinate = exteriorAnchor.Y; } else { side = deltaY <= 0d ? "top" : "bottom"; slotCoordinate = exteriorAnchor.X; } if (!ElkShapeBoundaries.TryProjectGatewayBoundarySlot(targetNode, side, slotCoordinate, out var boundary)) { return null; } return TryBuildSlottedGatewayEntryPath(sourcePath, targetNode, exteriorIndex, exteriorAnchor, boundary); } private static List? TryBuildSlottedGatewayEntryPath( IReadOnlyList sourcePath, ElkPositionedNode targetNode, int exteriorIndex, ElkPoint exteriorAnchor, ElkPoint boundary) { if (!ElkShapeBoundaries.IsGatewayShape(targetNode)) { return null; } boundary = ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(targetNode, boundary, exteriorAnchor); var exteriorApproach = ElkShapeBoundaries.BuildGatewayDirectionalExteriorPoint(targetNode, boundary, exteriorAnchor); var rebuilt = sourcePath.Take(exteriorIndex + 1) .Select(point => new ElkPoint { X = point.X, Y = point.Y }) .ToList(); if (rebuilt.Count == 0 || !ElkEdgeRoutingGeometry.PointsEqual(rebuilt[^1], exteriorAnchor)) { rebuilt.Add(exteriorAnchor); } AppendGatewayTargetOrthogonalCorner( rebuilt, rebuilt[^1], exteriorApproach, rebuilt.Count >= 2 ? rebuilt[^2] : null, preferHorizontalFromReference: ShouldPreferHorizontalGatewayExit(rebuilt[^1], exteriorApproach), targetNode); if (!ElkEdgeRoutingGeometry.PointsEqual(rebuilt[^1], exteriorApproach)) { rebuilt.Add(exteriorApproach); } rebuilt.Add(boundary); var normalized = NormalizePathPoints(rebuilt); if (normalized.Count >= 2 && (ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(targetNode, normalized[^2]) || !ElkShapeBoundaries.HasValidGatewayBoundaryAngle(targetNode, normalized[^1], normalized[^2]))) { normalized = ForceGatewayExteriorTargetApproach(normalized, targetNode, boundary); } return normalized.Count >= 2 && !ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(targetNode, normalized[^2]) && ElkShapeBoundaries.HasValidGatewayBoundaryAngle(targetNode, normalized[^1], normalized[^2]) ? normalized : null; } private static List? TryBuildDirectGatewayTargetEntry( IReadOnlyList sourcePath, ElkPositionedNode targetNode, int exteriorIndex, ElkPoint exteriorAnchor, ElkPoint boundaryPoint, ElkPoint assignedEndpoint) { if (!ElkShapeBoundaries.IsGatewayShape(targetNode)) { return null; } var prefix = sourcePath.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(new ElkPoint { X = exteriorAnchor.X, Y = exteriorAnchor.Y }); } var bestPath = default(List); var bestScore = double.PositiveInfinity; foreach (var candidate in ResolveDirectGatewayTargetBoundaryCandidates(targetNode, exteriorAnchor, boundaryPoint, assignedEndpoint)) { if (ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(targetNode, exteriorAnchor) || !ElkShapeBoundaries.HasValidGatewayBoundaryAngle(targetNode, candidate, exteriorAnchor)) { continue; } var rebuilt = prefix .Select(point => new ElkPoint { X = point.X, Y = point.Y }) .ToList(); rebuilt.Add(candidate); var normalized = NormalizePathPoints(rebuilt); if (normalized.Count < 2 || ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(targetNode, normalized[^2]) || !ElkShapeBoundaries.HasValidGatewayBoundaryAngle(targetNode, normalized[^1], normalized[^2])) { continue; } var score = ComputePathLength(normalized); score += Math.Abs(candidate.X - boundaryPoint.X) + Math.Abs(candidate.Y - boundaryPoint.Y); if (ElkShapeBoundaries.IsNearGatewayVertex(targetNode, candidate, 8d)) { score += 1_000d; } if (score >= bestScore) { continue; } bestScore = score; bestPath = normalized; } return bestPath; } private static List ForceDecisionDirectTargetEntry( IReadOnlyList sourcePath, ElkPositionedNode targetNode) { if (targetNode.Kind != "Decision" || sourcePath.Count < 3) { return sourcePath .Select(point => new ElkPoint { X = point.X, Y = point.Y }) .ToList(); } var anchor = sourcePath[^3]; if (ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(targetNode, anchor)) { return sourcePath .Select(point => new ElkPoint { X = point.X, Y = point.Y }) .ToList(); } var boundary = ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary( targetNode, ElkShapeBoundaries.ProjectOntoShapeBoundary(targetNode, anchor), anchor); if (ElkShapeBoundaries.TryProjectGatewayDiagonalBoundary(targetNode, anchor, boundary, out var diagonalBoundary)) { boundary = ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(targetNode, diagonalBoundary, anchor); } var rebuilt = sourcePath.Take(sourcePath.Count - 2) .Select(point => new ElkPoint { X = point.X, Y = point.Y }) .ToList(); rebuilt.Add(boundary); var normalized = NormalizePathPoints(rebuilt); return CanAcceptGatewayTargetRepair(normalized, targetNode) ? normalized : sourcePath .Select(point => new ElkPoint { X = point.X, Y = point.Y }) .ToList(); } private static List ForceDecisionExteriorTargetEntry( IReadOnlyList sourcePath, ElkPositionedNode targetNode) { var current = sourcePath .Select(point => new ElkPoint { X = point.X, Y = point.Y }) .ToList(); if (targetNode.Kind != "Decision" || current.Count < 2) { return current; } var exteriorIndex = FindLastGatewayExteriorPointIndex(current, targetNode); var exteriorAnchor = current[exteriorIndex]; if (ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(targetNode, exteriorAnchor)) { return current; } var projectedBoundary = ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary( targetNode, ElkShapeBoundaries.ProjectOntoShapeBoundary(targetNode, exteriorAnchor), exteriorAnchor); List? bestPath = null; var bestScore = double.PositiveInfinity; foreach (var boundary in ResolveDirectGatewayTargetBoundaryCandidates( targetNode, exteriorAnchor, projectedBoundary, projectedBoundary)) { foreach (var exteriorApproach in ResolveForcedGatewayExteriorApproachCandidates( targetNode, boundary, exteriorAnchor)) { var rebuilt = current.Take(exteriorIndex + 1) .Select(point => new ElkPoint { X = point.X, Y = point.Y }) .ToList(); if (rebuilt.Count == 0 || !ElkEdgeRoutingGeometry.PointsEqual(rebuilt[^1], exteriorAnchor)) { rebuilt.Add(new ElkPoint { X = exteriorAnchor.X, Y = exteriorAnchor.Y }); } AppendGatewayTargetOrthogonalCorner( rebuilt, rebuilt[^1], exteriorApproach, rebuilt.Count >= 2 ? rebuilt[^2] : null, preferHorizontalFromReference: ShouldPreferHorizontalGatewayExit(rebuilt[^1], exteriorApproach), targetNode); if (!ElkEdgeRoutingGeometry.PointsEqual(rebuilt[^1], exteriorApproach)) { rebuilt.Add(new ElkPoint { X = exteriorApproach.X, Y = exteriorApproach.Y }); } rebuilt.Add(new ElkPoint { X = boundary.X, Y = boundary.Y }); var normalized = NormalizePathPoints(rebuilt); if (!CanAcceptGatewayTargetRepair(normalized, targetNode)) { continue; } var score = ComputePathLength(normalized); if (score >= bestScore) { continue; } bestScore = score; bestPath = normalized; } } return bestPath ?? current; } private static bool ShouldPreferDirectGatewayTargetEntry( IReadOnlyList? candidate, ElkPositionedNode targetNode, ElkPoint assignedEndpoint, bool preserveAssignedSlot) { if (candidate is null || candidate.Count < 2) { return false; } if (ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(targetNode, candidate[^2]) || !ElkShapeBoundaries.HasValidGatewayBoundaryAngle(targetNode, candidate[^1], candidate[^2])) { return false; } if (!preserveAssignedSlot) { return true; } var endpointDelta = ElkEdgeRoutingGeometry.ComputeSegmentLength(candidate[^1], assignedEndpoint); if (endpointDelta <= 6d) { return true; } // Decision targets can still prefer a direct face entry, but join/fork // targets must honor materially different assigned slots so target-side // lane separation survives the normalization pass. return targetNode.Kind == "Decision"; } private static List CollapseGatewayTargetTailIfPossible( IReadOnlyList sourcePath, ElkPositionedNode targetNode) { if (!ElkShapeBoundaries.IsGatewayShape(targetNode) || sourcePath.Count < 3) { return sourcePath .Select(point => new ElkPoint { X = point.X, Y = point.Y }) .ToList(); } var current = sourcePath .Select(point => new ElkPoint { X = point.X, Y = point.Y }) .ToList(); var boundary = current[^1]; for (var anchorIndex = current.Count - 3; anchorIndex >= 0; anchorIndex--) { var anchor = current[anchorIndex]; if (ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(targetNode, anchor)) { continue; } foreach (var candidateBoundary in ResolveDirectGatewayTargetBoundaryCandidates(targetNode, anchor, boundary, boundary)) { var rebuilt = current.Take(anchorIndex + 1) .Select(point => new ElkPoint { X = point.X, Y = point.Y }) .ToList(); rebuilt.Add(candidateBoundary); var normalized = NormalizePathPoints(rebuilt); if (normalized.Count >= 2 && CanAcceptGatewayTargetRepair(normalized, targetNode)) { return normalized; } } } return current; } private static IEnumerable ResolveDirectGatewayTargetBoundaryCandidates( ElkPositionedNode targetNode, ElkPoint exteriorAnchor, ElkPoint boundaryPoint, ElkPoint assignedEndpoint) { var candidates = new List(); AddUniquePoint(candidates, boundaryPoint); var projectedBoundary = ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary( targetNode, ElkShapeBoundaries.ProjectOntoShapeBoundary(targetNode, exteriorAnchor), exteriorAnchor); AddUniquePoint(candidates, projectedBoundary); if (ElkShapeBoundaries.IsGatewayBoundaryPoint(targetNode, assignedEndpoint)) { AddUniquePoint( candidates, ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(targetNode, assignedEndpoint, exteriorAnchor)); } if (ElkShapeBoundaries.TryProjectGatewayDiagonalBoundary(targetNode, exteriorAnchor, projectedBoundary, out var diagonalProjected)) { AddUniquePoint( candidates, ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(targetNode, diagonalProjected, exteriorAnchor)); } if (ElkShapeBoundaries.TryProjectGatewayDiagonalBoundary(targetNode, exteriorAnchor, boundaryPoint, out var diagonalBoundary)) { AddUniquePoint( candidates, ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(targetNode, diagonalBoundary, exteriorAnchor)); } return candidates; } private static List PreferGatewayDiagonalTargetEntry( IReadOnlyList sourcePath, ElkPositionedNode targetNode) { var path = sourcePath .Select(point => new ElkPoint { X = point.X, Y = point.Y }) .ToList(); if (!ElkShapeBoundaries.IsGatewayShape(targetNode) || path.Count < 3) { return path; } const double tolerance = 0.5d; var boundary = path[^1]; var adjacent = path[^2]; var previous = path[^3]; var lastOrthogonal = Math.Abs(boundary.X - adjacent.X) <= tolerance || Math.Abs(boundary.Y - adjacent.Y) <= tolerance; var previousOrthogonal = path.Count == 3 || Math.Abs(adjacent.X - previous.X) <= tolerance || Math.Abs(adjacent.Y - previous.Y) <= tolerance; if (!lastOrthogonal || !previousOrthogonal) { return path; } if (Math.Abs(boundary.X - previous.X) <= tolerance || Math.Abs(boundary.Y - previous.Y) <= tolerance || ElkShapeBoundaries.IsNearGatewayVertex(targetNode, boundary, 8d) || !ElkShapeBoundaries.HasValidGatewayBoundaryAngle(targetNode, boundary, previous)) { var projectedBoundary = ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary( targetNode, ElkShapeBoundaries.ProjectOntoShapeBoundary(targetNode, previous), previous); if (ElkShapeBoundaries.TryProjectGatewayDiagonalBoundary(targetNode, previous, projectedBoundary, out var diagonalBoundary)) { projectedBoundary = ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(targetNode, diagonalBoundary, previous); } boundary = projectedBoundary; } if (ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(targetNode, previous) || !ElkShapeBoundaries.HasValidGatewayBoundaryAngle(targetNode, boundary, previous)) { return path; } var rebuilt = path.Take(path.Count - 2) .Select(point => new ElkPoint { X = point.X, Y = point.Y }) .ToList(); rebuilt.Add(new ElkPoint { X = boundary.X, Y = boundary.Y }); return NormalizePathPoints(rebuilt); } private static IEnumerable ResolveGatewayEntryBoundaryCandidates( ElkPositionedNode targetNode, ElkPoint exteriorAnchor, ElkPoint assignedEndpoint) { var candidates = new List(); AddUniquePoint( candidates, ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary( targetNode, ElkShapeBoundaries.ProjectOntoShapeBoundary(targetNode, exteriorAnchor), exteriorAnchor)); var projectedAssigned = ElkShapeBoundaries.ProjectOntoShapeBoundary(targetNode, assignedEndpoint); AddUniquePoint( candidates, ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(targetNode, projectedAssigned, exteriorAnchor)); if (ElkShapeBoundaries.IsGatewayBoundaryPoint(targetNode, assignedEndpoint)) { AddUniquePoint( candidates, ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(targetNode, assignedEndpoint, exteriorAnchor)); } foreach (var side in EnumeratePreferredGatewayEntrySides(targetNode, exteriorAnchor)) { var centerX = targetNode.X + (targetNode.Width / 2d); var centerY = targetNode.Y + (targetNode.Height / 2d); var slotCoordinate = side is "left" or "right" ? centerY + Math.Clamp(exteriorAnchor.Y - centerY, -targetNode.Height * 0.18d, targetNode.Height * 0.18d) : centerX + Math.Clamp(exteriorAnchor.X - centerX, -targetNode.Width * 0.18d, targetNode.Width * 0.18d); if (!ElkShapeBoundaries.TryProjectGatewayBoundarySlot(targetNode, side, slotCoordinate, out var slotBoundary)) { continue; } AddUniquePoint( candidates, ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(targetNode, slotBoundary, exteriorAnchor)); } if (ElkShapeBoundaries.TryProjectGatewayDiagonalBoundary(targetNode, exteriorAnchor, projectedAssigned, out var diagonalBoundary)) { AddUniquePoint( candidates, ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(targetNode, diagonalBoundary, exteriorAnchor)); } return candidates; } private static IEnumerable ResolveGatewayExteriorApproachCandidates( ElkPositionedNode node, ElkPoint boundary, ElkPoint referencePoint, double padding = 8d) { var candidates = new List(); AddUniquePoint( candidates, ElkShapeBoundaries.BuildPreferredGatewaySourceExteriorPoint(node, boundary, referencePoint, padding)); var faceNormalCandidate = ElkShapeBoundaries.BuildGatewayExteriorApproachPoint(node, boundary, padding); AddUniquePoint(candidates, faceNormalCandidate); var horizontalDirection = Math.Sign(referencePoint.X - boundary.X); if (horizontalDirection != 0d) { AddUniquePoint( candidates, new ElkPoint { X = horizontalDirection > 0d ? node.X + node.Width + padding : node.X - padding, Y = boundary.Y, }); } var verticalDirection = Math.Sign(referencePoint.Y - boundary.Y); if (verticalDirection != 0d) { AddUniquePoint( candidates, new ElkPoint { X = boundary.X, Y = verticalDirection > 0d ? node.Y + node.Height + padding : node.Y - padding, }); } return candidates .Where(candidate => !ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(node, candidate) && ElkShapeBoundaries.HasValidGatewayBoundaryAngle(node, boundary, candidate)) .OrderBy(candidate => ScoreGatewayExteriorApproachCandidate(node, boundary, candidate, referencePoint)) .ToArray(); } private static IEnumerable EnumeratePreferredGatewaySourceSides( ElkPositionedNode sourceNode, ElkPoint continuationPoint, ElkPoint referencePoint) { var centerX = sourceNode.X + (sourceNode.Width / 2d); var centerY = sourceNode.Y + (sourceNode.Height / 2d); var continuationDx = continuationPoint.X - centerX; var continuationDy = continuationPoint.Y - centerY; var referenceDx = referencePoint.X - centerX; var referenceDy = referencePoint.Y - centerY; var effectiveDx = Math.Abs(continuationDx) > 12d ? continuationDx : referenceDx; var effectiveDy = Math.Abs(continuationDy) > 12d ? continuationDy : referenceDy; var absDx = Math.Abs(effectiveDx); var absDy = Math.Abs(effectiveDy); var primary = absDx > 12d && (absDx >= absDy * 0.55d || absDy < 20d) ? effectiveDx >= 0d ? "right" : "left" : absDy > 12d ? effectiveDy >= 0d ? "bottom" : "top" : Math.Abs(referenceDx) >= Math.Abs(referenceDy) ? referenceDx >= 0d ? "right" : "left" : referenceDy >= 0d ? "bottom" : "top"; yield return primary; string? secondary = null; if (primary is "left" or "right") { if (absDy > 12d) { secondary = effectiveDy >= 0d ? "bottom" : "top"; } } else if (absDx > 12d) { secondary = effectiveDx >= 0d ? "right" : "left"; } if (secondary is not null && !string.Equals(primary, secondary, StringComparison.Ordinal)) { yield return secondary; } var referencePrimary = Math.Abs(referenceDx) > 12d && Math.Abs(referenceDx) >= Math.Abs(referenceDy) * 0.55d ? referenceDx >= 0d ? "right" : "left" : Math.Abs(referenceDy) > 12d ? referenceDy >= 0d ? "bottom" : "top" : null; if (referencePrimary is not null && !string.Equals(referencePrimary, primary, StringComparison.Ordinal) && !string.Equals(referencePrimary, secondary, StringComparison.Ordinal)) { yield return referencePrimary; } } private static bool TryProjectGatewaySourceBoundarySlot( ElkPositionedNode sourceNode, string side, ElkPoint continuationPoint, ElkPoint referencePoint, out ElkPoint boundary) { boundary = default!; var slotCoordinate = ResolveGatewaySourceSlotCoordinate(sourceNode, side, continuationPoint, referencePoint); if (!ElkShapeBoundaries.TryProjectGatewayBoundarySlot(sourceNode, side, slotCoordinate, out boundary)) { return false; } boundary = ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(sourceNode, boundary, continuationPoint); return true; } private static IEnumerable ResolveGatewaySourceBoundarySlotCandidates( ElkPositionedNode sourceNode, string side, ElkPoint continuationPoint, ElkPoint referencePoint) { var candidates = new List(); foreach (var slotCoordinate in EnumerateGatewaySourceSlotCoordinates(sourceNode, side, continuationPoint, referencePoint)) { if (!ElkShapeBoundaries.TryProjectGatewayBoundarySlot(sourceNode, side, slotCoordinate, out var boundary)) { continue; } boundary = ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(sourceNode, boundary, continuationPoint); AddUniquePoint(candidates, boundary); } return candidates; } private static double ResolveGatewaySourceSlotCoordinate( ElkPositionedNode sourceNode, string side, ElkPoint continuationPoint, ElkPoint referencePoint) { var centerX = sourceNode.X + (sourceNode.Width / 2d); var centerY = sourceNode.Y + (sourceNode.Height / 2d); var referenceDx = referencePoint.X - centerX; var referenceDy = referencePoint.Y - centerY; var dominantHorizontal = Math.Abs(referenceDx) >= Math.Abs(referenceDy) * 1.25d; var dominantVertical = Math.Abs(referenceDy) >= Math.Abs(referenceDx) * 1.25d; return side is "left" or "right" ? Math.Clamp( dominantHorizontal ? centerY + Math.Clamp(referenceDy, -sourceNode.Height * 0.18d, sourceNode.Height * 0.18d) : Math.Abs(continuationPoint.Y - centerY) > 2d ? continuationPoint.Y : referencePoint.Y, sourceNode.Y + 4d, sourceNode.Y + sourceNode.Height - 4d) : Math.Clamp( dominantVertical ? centerX + Math.Clamp(referenceDx, -sourceNode.Width * 0.18d, sourceNode.Width * 0.18d) : Math.Abs(continuationPoint.X - centerX) > 2d ? continuationPoint.X : referencePoint.X, sourceNode.X + 4d, sourceNode.X + sourceNode.Width - 4d); } private static IEnumerable EnumerateGatewaySourceSlotCoordinates( ElkPositionedNode sourceNode, string side, ElkPoint continuationPoint, ElkPoint referencePoint) { var primary = ResolveGatewaySourceSlotCoordinate(sourceNode, side, continuationPoint, referencePoint); yield return primary; var center = side is "left" or "right" ? sourceNode.Y + (sourceNode.Height / 2d) : sourceNode.X + (sourceNode.Width / 2d); if (Math.Abs(center - primary) > 1d) { yield return center; } var alternate = side is "left" or "right" ? Math.Clamp(referencePoint.Y, sourceNode.Y + 4d, sourceNode.Y + sourceNode.Height - 4d) : Math.Clamp(referencePoint.X, sourceNode.X + 4d, sourceNode.X + sourceNode.Width - 4d); if (Math.Abs(alternate - primary) > 1d) { yield return alternate; } var blended = side is "left" or "right" ? Math.Clamp((continuationPoint.Y + referencePoint.Y) / 2d, sourceNode.Y + 4d, sourceNode.Y + sourceNode.Height - 4d) : Math.Clamp((continuationPoint.X + referencePoint.X) / 2d, sourceNode.X + 4d, sourceNode.X + sourceNode.Width - 4d); if (Math.Abs(blended - primary) > 1d && Math.Abs(blended - alternate) > 1d && Math.Abs(blended - center) > 1d) { yield return blended; } } private static bool IsBoundaryOnGatewaySourceSide( ElkPositionedNode sourceNode, ElkPoint boundary, string side) { var centerX = sourceNode.X + (sourceNode.Width / 2d); var centerY = sourceNode.Y + (sourceNode.Height / 2d); var deltaX = boundary.X - centerX; var deltaY = boundary.Y - centerY; return side switch { "right" => deltaX > 0d && Math.Abs(deltaX) >= Math.Abs(deltaY) * 0.35d, "left" => deltaX < 0d && Math.Abs(deltaX) >= Math.Abs(deltaY) * 0.35d, "bottom" => deltaY > 0d && Math.Abs(deltaY) >= Math.Abs(deltaX) * 0.35d, "top" => deltaY < 0d && Math.Abs(deltaY) >= Math.Abs(deltaX) * 0.35d, _ => false, }; } private static IEnumerable EnumeratePreferredGatewayEntrySides( ElkPositionedNode targetNode, ElkPoint exteriorAnchor) { var centerX = targetNode.X + (targetNode.Width / 2d); var centerY = targetNode.Y + (targetNode.Height / 2d); var deltaX = exteriorAnchor.X - centerX; var deltaY = exteriorAnchor.Y - centerY; var absDx = Math.Abs(deltaX); var absDy = Math.Abs(deltaY); var primary = absDx >= absDy ? (deltaX >= 0d ? "right" : "left") : (deltaY >= 0d ? "bottom" : "top"); yield return primary; if (absDx > 0.5d && absDy > 0.5d) { var secondary = primary is "left" or "right" ? (deltaY >= 0d ? "bottom" : "top") : (deltaX >= 0d ? "right" : "left"); if (!string.Equals(primary, secondary, StringComparison.Ordinal)) { yield return secondary; } } } private static double ScoreGatewayEntryBoundaryCandidate( ElkPositionedNode targetNode, ElkPoint candidate, ElkPoint exteriorAnchor, ElkPoint assignedEndpoint) { if (!ElkShapeBoundaries.IsGatewayBoundaryPoint(targetNode, candidate)) { return double.PositiveInfinity; } var exteriorApproach = ElkShapeBoundaries.BuildGatewayDirectionalExteriorPoint(targetNode, candidate, exteriorAnchor); if (!ElkShapeBoundaries.HasValidGatewayBoundaryAngle(targetNode, candidate, exteriorApproach)) { return double.PositiveInfinity; } var centerX = targetNode.X + (targetNode.Width / 2d); var centerY = targetNode.Y + (targetNode.Height / 2d); var desiredDx = exteriorAnchor.X - centerX; var desiredDy = exteriorAnchor.Y - centerY; var candidateDx = candidate.X - centerX; var candidateDy = candidate.Y - centerY; var score = Math.Abs(candidate.X - exteriorAnchor.X) + Math.Abs(candidate.Y - exteriorAnchor.Y); score += (Math.Abs(candidate.X - assignedEndpoint.X) + Math.Abs(candidate.Y - assignedEndpoint.Y)) * 0.2d; score += (Math.Abs(exteriorApproach.X - exteriorAnchor.X) + Math.Abs(exteriorApproach.Y - exteriorAnchor.Y)) * 0.05d; var dominantHorizontal = Math.Abs(desiredDx) >= Math.Abs(desiredDy) * 1.25d; var dominantVertical = Math.Abs(desiredDy) >= Math.Abs(desiredDx) * 1.25d; if (dominantHorizontal) { if (Math.Sign(candidateDx) != Math.Sign(desiredDx)) { score += 10_000d; } score += Math.Abs(candidateDy) * 6d; } else if (dominantVertical) { if (Math.Sign(candidateDy) != Math.Sign(desiredDy)) { score += 10_000d; } score += Math.Abs(candidateDx) * 6d; } else { score += (Math.Abs(candidateDx - desiredDx) + Math.Abs(candidateDy - desiredDy)) * 0.08d; } if (ElkShapeBoundaries.IsNearGatewayVertex(targetNode, candidate, 8d)) { score += 4_000d; } return score; } private static double ScoreGatewayExteriorApproachCandidate( ElkPositionedNode node, ElkPoint boundary, ElkPoint candidate, ElkPoint referencePoint) { var deltaX = candidate.X - boundary.X; var deltaY = candidate.Y - boundary.Y; var moveLength = Math.Sqrt((deltaX * deltaX) + (deltaY * deltaY)); var referenceDistance = Math.Abs(referencePoint.X - candidate.X) + Math.Abs(referencePoint.Y - candidate.Y); var score = moveLength + (referenceDistance * 0.1d); var dominantHorizontal = Math.Abs(referencePoint.X - boundary.X) >= Math.Abs(referencePoint.Y - boundary.Y) * 1.2d; var dominantVertical = Math.Abs(referencePoint.Y - boundary.Y) >= Math.Abs(referencePoint.X - boundary.X) * 1.2d; if (node.Kind == "Decision" && !ElkShapeBoundaries.IsNearGatewayVertex(node, boundary, 8d)) { var preferredCandidate = ElkShapeBoundaries.BuildGatewayExteriorApproachPoint(node, boundary); var isAxisAlignedStub = Math.Abs(deltaX) <= 0.5d || Math.Abs(deltaY) <= 0.5d; if (!dominantHorizontal && !dominantVertical) { if (ElkEdgeRoutingGeometry.PointsEqual(candidate, preferredCandidate)) { score -= 220d; } else { if (isAxisAlignedStub) { score += 120d; } score += Math.Abs(Math.Abs(deltaX) - Math.Abs(deltaY)) * 0.25d; } } else { if (dominantHorizontal) { if (Math.Abs(deltaX) <= 0.5d || Math.Sign(deltaX) != Math.Sign(referencePoint.X - boundary.X)) { score += 8_000d; } if (Math.Abs(deltaY) <= 0.5d) { score -= 120d; } score += Math.Abs(deltaY) * 8d; } else if (dominantVertical) { if (Math.Abs(deltaY) <= 0.5d || Math.Sign(deltaY) != Math.Sign(referencePoint.Y - boundary.Y)) { score += 8_000d; } if (Math.Abs(deltaX) <= 0.5d) { score -= 120d; } score += Math.Abs(deltaX) * 8d; } } } if (dominantHorizontal) { if (Math.Sign(deltaX) != Math.Sign(referencePoint.X - boundary.X)) { score += 10_000d; } score += Math.Abs(deltaY) * 0.35d; } else if (dominantVertical) { if (Math.Sign(deltaY) != Math.Sign(referencePoint.Y - boundary.Y)) { score += 10_000d; } score += Math.Abs(deltaX) * 0.35d; } return score; } private static List TrimTargetApproachBacktracking( IReadOnlyList sourcePath, ElkPositionedNode targetNode, string side, ElkPoint explicitEndpoint) { if (sourcePath.Count < 4) { return sourcePath .Select(point => new ElkPoint { X = point.X, Y = point.Y }) .ToList(); } const double tolerance = 0.5d; var startIndex = Math.Max(0, sourcePath.Count - 5); var firstOffendingIndex = -1; for (var i = startIndex; i < sourcePath.Count - 1; i++) { if (IsOnWrongSideOfTarget(sourcePath[i], targetNode, side, tolerance)) { firstOffendingIndex = i; break; } } if (firstOffendingIndex < 0) { return sourcePath .Select(point => new ElkPoint { X = point.X, Y = point.Y }) .ToList(); } var trimmed = sourcePath .Take(Math.Max(1, firstOffendingIndex)) .Select(point => new ElkPoint { X = point.X, Y = point.Y }) .ToList(); if (trimmed.Count == 0 || !ElkEdgeRoutingGeometry.PointsEqual(trimmed[^1], explicitEndpoint)) { trimmed.Add(explicitEndpoint); } return NormalizeEntryPath(trimmed, targetNode, side, explicitEndpoint); } private static bool TryNormalizeNonGatewayBacktrackingEntry( IReadOnlyList sourcePath, ElkPositionedNode targetNode, out List repairedPath) { repairedPath = sourcePath .Select(point => new ElkPoint { X = point.X, Y = point.Y }) .ToList(); if (sourcePath.Count < 2) { return false; } if (!TryResolveNonGatewayBacktrackingEndpoint(sourcePath, targetNode, out var side, out var endpoint)) { return false; } var candidate = NormalizeEntryPath(sourcePath, targetNode, side, endpoint); if (HasTargetApproachBacktracking(candidate, targetNode)) { return false; } repairedPath = candidate; return true; } private static bool TryResolveNonGatewayBacktrackingEndpoint( IReadOnlyList sourcePath, ElkPositionedNode targetNode, out string side, out ElkPoint endpoint) { side = string.Empty; endpoint = default!; if (sourcePath.Count < 2) { return false; } var anchor = sourcePath[^2]; var centerX = targetNode.X + (targetNode.Width / 2d); var centerY = targetNode.Y + (targetNode.Height / 2d); var deltaX = anchor.X - centerX; var deltaY = anchor.Y - centerY; var dominantHorizontal = Math.Abs(deltaX) >= Math.Abs(deltaY) * 1.15d; side = dominantHorizontal ? (deltaX <= 0d ? "left" : "right") : (deltaY <= 0d ? "top" : "bottom"); if (side is "left" or "right") { endpoint = new ElkPoint { X = side == "left" ? targetNode.X : targetNode.X + targetNode.Width, Y = Math.Clamp(anchor.Y, targetNode.Y + 4d, targetNode.Y + targetNode.Height - 4d), }; } else { endpoint = new ElkPoint { X = Math.Clamp(anchor.X, targetNode.X + 4d, targetNode.X + targetNode.Width - 4d), Y = side == "top" ? targetNode.Y : targetNode.Y + targetNode.Height, }; } return true; } private static bool HasTargetApproachBacktracking( IReadOnlyList path, ElkPositionedNode targetNode) { if (path.Count < 3 || ElkShapeBoundaries.IsGatewayShape(targetNode)) { return false; } var side = ElkEdgeRoutingGeometry.ResolveBoundarySide(path[^1], targetNode); if (side is not "left" and not "right" and not "top" and not "bottom") { return false; } const double tolerance = 0.5d; if (HasShortOrthogonalTargetHook(path, targetNode, side, tolerance)) { return true; } var startIndex = Math.Max(0, path.Count - (side is "left" or "right" ? 4 : 3)); var axisValues = new List(path.Count - startIndex); for (var i = startIndex; i < path.Count; i++) { var value = side is "left" or "right" ? path[i].X : path[i].Y; if (axisValues.Count == 0 || Math.Abs(axisValues[^1] - value) > tolerance) { axisValues.Add(value); } } if (axisValues.Count < 3) { return false; } var targetAxis = side switch { "left" => targetNode.X, "right" => targetNode.X + targetNode.Width, "top" => targetNode.Y, "bottom" => targetNode.Y + targetNode.Height, _ => double.NaN, }; var overshootsTargetSide = side switch { "left" or "top" => axisValues.Any(value => value > targetAxis + tolerance), "right" or "bottom" => axisValues.Any(value => value < targetAxis - tolerance), _ => false, }; if (overshootsTargetSide) { return true; } var expectsIncreasing = side is "left" or "top"; var sawProgress = false; for (var i = 1; i < axisValues.Count; i++) { var delta = axisValues[i] - axisValues[i - 1]; if (Math.Abs(delta) <= tolerance) { continue; } if (expectsIncreasing) { if (delta > tolerance) { sawProgress = true; } else if (sawProgress) { return true; } } else { if (delta < -tolerance) { sawProgress = true; } else if (sawProgress) { return true; } } } return false; } private static bool HasShortOrthogonalTargetHook( IReadOnlyList path, ElkPositionedNode targetNode, string side, double tolerance) { if (path.Count < 3) { return false; } var boundaryPoint = path[^1]; var runStartIndex = path.Count - 2; if (side is "left" or "right") { while (runStartIndex > 0 && Math.Abs(path[runStartIndex - 1].Y - boundaryPoint.Y) <= tolerance) { runStartIndex--; } } else { while (runStartIndex > 0 && Math.Abs(path[runStartIndex - 1].X - boundaryPoint.X) <= tolerance) { runStartIndex--; } } if (runStartIndex == 0) { return false; } 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); var looksHorizontal = overallAbsDx >= overallAbsDy * 1.15d && overallAbsDy <= sameRowThreshold && Math.Sign(overallDeltaX) != 0; var looksVertical = overallAbsDy >= overallAbsDx * 1.15d && overallAbsDx <= sameColumnThreshold && Math.Sign(overallDeltaY) != 0; var contradictsDominantApproach = side switch { "left" or "right" => looksVertical, "top" or "bottom" => looksHorizontal, _ => false, }; if (!contradictsDominantApproach) { return false; } var runStart = path[runStartIndex]; var boundaryDepth = side is "left" or "right" ? Math.Abs(boundaryPoint.X - runStart.X) : Math.Abs(boundaryPoint.Y - runStart.Y); var requiredDepth = side is "left" or "right" ? targetNode.Width : targetNode.Height; if (boundaryDepth + tolerance >= requiredDepth) { return false; } var predecessor = path[runStartIndex - 1]; var predecessorDx = Math.Abs(runStart.X - predecessor.X); var predecessorDy = Math.Abs(runStart.Y - predecessor.Y); return side switch { "left" or "right" => predecessorDy > predecessorDx * 3d, "top" or "bottom" => predecessorDx > predecessorDy * 3d, _ => false, }; } private static bool IsOnWrongSideOfTarget( ElkPoint point, ElkPositionedNode targetNode, string side, double tolerance) { return side switch { "left" => point.X > targetNode.X + tolerance, "right" => point.X < (targetNode.X + targetNode.Width) - tolerance, "top" => point.Y > targetNode.Y + tolerance, "bottom" => point.Y < (targetNode.Y + targetNode.Height) - tolerance, _ => false, }; } private static Dictionary ResolveTargetApproachSlots( IReadOnlyCollection edges, IReadOnlyDictionary nodesById, double graphMinY, double graphMaxY, double minLineClearance, IReadOnlySet? restrictedEdgeIds) { var result = new Dictionary(StringComparer.Ordinal); var groups = new Dictionary>(StringComparer.Ordinal); foreach (var edge in edges) { if (!ShouldSpreadTargetApproach(edge, graphMinY, graphMaxY) || !nodesById.TryGetValue(edge.TargetNodeId ?? string.Empty, out var targetNode)) { continue; } var path = ExtractFullPath(edge); if (path.Count < 2) { continue; } var endpoint = path[^1]; var side = ResolveTargetApproachSide(path, targetNode); var key = $"{targetNode.Id}|{side}"; if (!groups.TryGetValue(key, out var group)) { group = []; groups[key] = group; } group.Add((edge.Id, endpoint)); } foreach (var (key, group) in groups) { if (group.Count < 2) { continue; } var separator = key.IndexOf('|', StringComparison.Ordinal); var targetId = key[..separator]; var side = key[(separator + 1)..]; if (!nodesById.TryGetValue(targetId, out var targetNode)) { continue; } if (restrictedEdgeIds is not null && !group.Any(item => restrictedEdgeIds.Contains(item.EdgeId))) { continue; } var sideLength = side is "left" or "right" ? Math.Max(8d, targetNode.Height - 8d) : Math.Max(8d, targetNode.Width - 8d); var slotSpacing = group.Count > 1 ? ResolveBoundaryJoinSlotSpacing(minLineClearance, sideLength, group.Count) : 0d; var totalSpan = (group.Count - 1) * slotSpacing; if (side is "left" or "right") { var centerY = targetNode.Y + (targetNode.Height / 2d); var startY = Math.Max(targetNode.Y + 4d, centerY - (totalSpan / 2d)); var sorted = group.OrderBy(item => item.Endpoint.Y).ToArray(); for (var i = 0; i < sorted.Length; i++) { var slotY = Math.Min(targetNode.Y + targetNode.Height - 4d, startY + (i * slotSpacing)); if (restrictedEdgeIds is not null && !restrictedEdgeIds.Contains(sorted[i].EdgeId)) { continue; } var slotPoint = new ElkPoint { X = side == "left" ? targetNode.X : targetNode.X + targetNode.Width, Y = slotY, }; if (ElkShapeBoundaries.TryProjectGatewayBoundarySlot(targetNode, side, slotY, out var gatewaySlot)) { slotPoint = gatewaySlot; } result[sorted[i].EdgeId] = slotPoint; } } else { var centerX = targetNode.X + (targetNode.Width / 2d); var startX = Math.Max(targetNode.X + 4d, centerX - (totalSpan / 2d)); var sorted = group.OrderBy(item => item.Endpoint.X).ToArray(); for (var i = 0; i < sorted.Length; i++) { var slotX = Math.Min(targetNode.X + targetNode.Width - 4d, startX + (i * slotSpacing)); if (restrictedEdgeIds is not null && !restrictedEdgeIds.Contains(sorted[i].EdgeId)) { continue; } var slotPoint = new ElkPoint { X = slotX, Y = side == "top" ? targetNode.Y : targetNode.Y + targetNode.Height, }; if (ElkShapeBoundaries.TryProjectGatewayBoundarySlot(targetNode, side, slotX, out var gatewaySlot)) { slotPoint = gatewaySlot; } result[sorted[i].EdgeId] = slotPoint; } } } return result; } private static bool GroupHasTargetApproachJoin( IReadOnlyList<(IReadOnlyList Path, string Side)> entries, double minLineClearance) { for (var i = 0; i < entries.Count; i++) { var left = entries[i]; if (!TryExtractTargetApproachRun(left.Path, left.Side, out var leftRunStartIndex, out var leftRunEndIndex)) { continue; } var leftStart = left.Path[leftRunStartIndex]; var leftEnd = left.Path[leftRunEndIndex]; for (var j = i + 1; j < entries.Count; j++) { var right = entries[j]; if (!string.Equals(left.Side, right.Side, StringComparison.Ordinal) || !TryExtractTargetApproachRun(right.Path, right.Side, out var rightRunStartIndex, out var rightRunEndIndex)) { continue; } var rightStart = right.Path[rightRunStartIndex]; var rightEnd = right.Path[rightRunEndIndex]; if (ElkEdgeRoutingGeometry.AreParallelAndClose(leftStart, leftEnd, rightStart, rightEnd, minLineClearance)) { return true; } } } return false; } private static double ResolveBoundaryJoinSlotSpacing( double minLineClearance, double sideLength, int entryCount) { if (entryCount <= 1) { return 0d; } // Keep slot spacing slightly above the violation threshold so a final // normalize pass does not collapse two target lanes back into the same // effective rail by a fraction of a pixel. var desiredSpacing = minLineClearance + 6d; return Math.Max(12d, Math.Min(desiredSpacing, sideLength / (entryCount - 1))); } private static string ResolveTargetApproachSide( IReadOnlyList path, ElkPositionedNode targetNode) { if (ElkShapeBoundaries.IsGatewayShape(targetNode)) { if (path.Count >= 2) { return ElkEdgeRoutingGeometry.ResolveBoundaryApproachSide(path[^1], path[^2], targetNode); } return ElkEdgeRoutingGeometry.ResolveBoundarySide(path[^1], targetNode); } if (path.Count < 2) { return ElkEdgeRoutingGeometry.ResolveBoundarySide(path[^1], targetNode); } return ElkEdgeRoutingGeometry.ResolveBoundaryApproachSide(path[^1], path[^2], targetNode); } private static double ResolveTargetApproachAxisValue( IReadOnlyList path, string side) { if (!TryExtractTargetApproachRun(path, side, out var runStartIndex, out _)) { return double.NaN; } return side switch { "left" or "right" => path[runStartIndex].X, "top" or "bottom" => path[runStartIndex].Y, _ => double.NaN, }; } private static double ResolveSpreadableTargetApproachAxis( IReadOnlyList path, ElkPositionedNode targetNode, string side, double minLineClearance) { if (!TryExtractTargetApproachRun(path, side, out var runStartIndex, out _)) { return double.NaN; } var rawAxis = ResolveTargetApproachAxisValue(path, side); if (double.IsNaN(rawAxis)) { return double.NaN; } var maxOffset = Math.Max( Math.Max(targetNode.Width, targetNode.Height), (minLineClearance * 2d) + 16d); return side switch { "left" => runStartIndex == 0 ? Math.Max(rawAxis, targetNode.X - maxOffset) : Math.Max(rawAxis, targetNode.X - maxOffset), "right" => runStartIndex == 0 ? Math.Min(rawAxis, targetNode.X + targetNode.Width + maxOffset) : Math.Min(rawAxis, targetNode.X + targetNode.Width + maxOffset), "top" => runStartIndex == 0 ? Math.Max(rawAxis, targetNode.Y - maxOffset) : Math.Max(rawAxis, targetNode.Y - maxOffset), "bottom" => runStartIndex == 0 ? Math.Min(rawAxis, targetNode.Y + targetNode.Height + maxOffset) : Math.Min(rawAxis, targetNode.Y + targetNode.Height + maxOffset), _ => rawAxis, }; } private static string ResolveSourceDepartureSide( IReadOnlyList path, ElkPositionedNode sourceNode) { if (path.Count < 2) { return ElkEdgeRoutingGeometry.ResolveBoundarySide(path[0], sourceNode); } return ElkEdgeRoutingGeometry.ResolveBoundaryApproachSide(path[0], path[1], sourceNode); } private static double ResolveDefaultSourceDepartureAxis( ElkPositionedNode sourceNode, string side) { return side switch { "left" => sourceNode.X - 24d, "right" => sourceNode.X + sourceNode.Width + 24d, "top" => sourceNode.Y - 24d, "bottom" => sourceNode.Y + sourceNode.Height + 24d, _ => 0d, }; } private static double ResolveDefaultTargetApproachAxis( ElkPositionedNode targetNode, string side) { return side switch { "left" => targetNode.X - 24d, "right" => targetNode.X + targetNode.Width + 24d, "top" => targetNode.Y - 24d, "bottom" => targetNode.Y + targetNode.Height + 24d, _ => double.NaN, }; } private static double ResolveDesiredTargetApproachAxis( ElkPositionedNode targetNode, string side, double baseApproachAxis, double slotSpacing, int slotIndex, bool forceOutwardFromBoundary = false) { var originAxis = double.IsNaN(baseApproachAxis) ? ResolveDefaultTargetApproachAxis(targetNode, side) : baseApproachAxis; var axis = forceOutwardFromBoundary ? side switch { "left" or "top" => originAxis - (slotIndex * slotSpacing), "right" or "bottom" => originAxis + (slotIndex * slotSpacing), _ => originAxis, } : originAxis + (slotIndex * slotSpacing); return side switch { "left" => Math.Min(axis, targetNode.X - 8d), "right" => Math.Max(axis, targetNode.X + targetNode.Width + 8d), "top" => Math.Min(axis, targetNode.Y - 8d), "bottom" => Math.Max(axis, targetNode.Y + targetNode.Height + 8d), _ => axis, }; } private static bool GroupHasMixedNodeFaceLaneConflict( IReadOnlyList<(int Index, ElkRoutedEdge Edge, IReadOnlyList Path, ElkPositionedNode Node, string Side, bool IsOutgoing, ElkPoint Boundary, double BoundaryCoordinate, double AxisValue)> entries, double minLineClearance) { for (var i = 0; i < entries.Count; i++) { for (var j = i + 1; j < entries.Count; j++) { if (entries[i].IsOutgoing == entries[j].IsOutgoing || !string.Equals(entries[i].Side, entries[j].Side, StringComparison.Ordinal)) { continue; } var outgoing = entries[i].IsOutgoing ? entries[i] : entries[j]; var incoming = entries[i].IsOutgoing ? entries[j] : entries[i]; if (!TryExtractSourceDepartureRun(outgoing.Path, outgoing.Side, out _, out var outgoingRunEndIndex) || !TryExtractTargetApproachRun(incoming.Path, incoming.Side, out var incomingRunStartIndex, out _)) { continue; } if (ElkEdgeRoutingGeometry.AreParallelAndClose( outgoing.Path[0], outgoing.Path[outgoingRunEndIndex], incoming.Path[incomingRunStartIndex], incoming.Path[^1], minLineClearance)) { return true; } } } return false; } private static List BuildMixedSourceFaceCandidate( IReadOnlyList path, ElkPositionedNode sourceNode, string side, double desiredCoordinate, double axisValue) { ElkPoint boundaryPoint; if (ElkShapeBoundaries.IsGatewayShape(sourceNode)) { if (!ElkShapeBoundaries.TryProjectGatewayBoundarySlot(sourceNode, side, desiredCoordinate, out boundaryPoint)) { return path.Select(point => new ElkPoint { X = point.X, Y = point.Y }).ToList(); } var continuation = path.Count > 1 ? path[1] : path[0]; boundaryPoint = ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(sourceNode, boundaryPoint, continuation); } else { boundaryPoint = side switch { "left" => new ElkPoint { X = sourceNode.X, Y = desiredCoordinate }, "right" => new ElkPoint { X = sourceNode.X + sourceNode.Width, Y = desiredCoordinate }, "top" => new ElkPoint { X = desiredCoordinate, Y = sourceNode.Y }, "bottom" => new ElkPoint { X = desiredCoordinate, Y = sourceNode.Y + sourceNode.Height }, _ => path[0], }; } return RewriteSourceDepartureRun( path, side, boundaryPoint, double.IsNaN(axisValue) ? ResolveDefaultSourceDepartureAxis(sourceNode, side) : axisValue); } private static List BuildMixedTargetFaceCandidate( IReadOnlyList path, ElkPositionedNode targetNode, string side, double desiredCoordinate, double axisValue) { ElkPoint desiredEndpoint; if (ElkShapeBoundaries.IsGatewayShape(targetNode)) { if (!ElkShapeBoundaries.TryProjectGatewayBoundarySlot(targetNode, side, desiredCoordinate, out desiredEndpoint)) { return path.Select(point => new ElkPoint { X = point.X, Y = point.Y }).ToList(); } return BuildTargetApproachCandidatePath( path, targetNode, side, desiredEndpoint, axisValue); } desiredEndpoint = side switch { "left" => new ElkPoint { X = targetNode.X, Y = desiredCoordinate }, "right" => new ElkPoint { X = targetNode.X + targetNode.Width, Y = desiredCoordinate }, "top" => new ElkPoint { X = desiredCoordinate, Y = targetNode.Y }, "bottom" => new ElkPoint { X = desiredCoordinate, Y = targetNode.Y + targetNode.Height }, _ => path[^1], }; return BuildTargetApproachCandidatePath( path, targetNode, side, desiredEndpoint, axisValue); } private static List BuildTargetApproachCandidatePath( IReadOnlyList path, ElkPositionedNode targetNode, string side, ElkPoint desiredEndpoint, double axisValue) { List normalized; if (ElkShapeBoundaries.IsGatewayShape(targetNode)) { var exteriorIndex = FindLastGatewayExteriorPointIndex(path, targetNode); var exteriorAnchor = path[exteriorIndex]; normalized = TryBuildSlottedGatewayEntryPath( path, targetNode, exteriorIndex, exteriorAnchor, desiredEndpoint) ?? NormalizeGatewayEntryPath(path, targetNode, desiredEndpoint); } else { normalized = NormalizeEntryPath(path, targetNode, side, desiredEndpoint); } var targetAxis = double.IsNaN(axisValue) ? ResolveDefaultTargetApproachAxis(targetNode, side) : axisValue; if (!TryExtractTargetApproachFeeder(normalized, side, out _)) { return normalized; } var rewritten = RewriteTargetApproachRun( normalized, side, desiredEndpoint, targetAxis); if (!PathChanged(normalized, rewritten)) { return normalized; } if (ElkShapeBoundaries.IsGatewayShape(targetNode) && !CanAcceptGatewayTargetRepair(rewritten, targetNode)) { return normalized; } return rewritten; } private static bool TryBuildAlternateMixedFaceCandidate( (int Index, ElkRoutedEdge Edge, IReadOnlyList Path, ElkPositionedNode Node, string Side, bool IsOutgoing, ElkPoint Boundary, double BoundaryCoordinate, double AxisValue) entry, IReadOnlyCollection nodes, double minLineClearance, out List candidate) { candidate = entry.Path .Select(point => new ElkPoint { X = point.X, Y = point.Y }) .ToList(); if (ElkShapeBoundaries.IsGatewayShape(entry.Node) || string.IsNullOrWhiteSpace(entry.Edge.Label) || !IsRepeatCollectorLabel(entry.Edge.Label)) { return false; } var alternateSide = entry.Side switch { "left" or "right" => "top", "top" or "bottom" => "right", _ => string.Empty, }; if (string.IsNullOrWhiteSpace(alternateSide)) { return false; } if (entry.IsOutgoing) { var sourcePath = entry.Path .Select(point => new ElkPoint { X = point.X, Y = point.Y }) .ToList(); var toward = sourcePath.Count > 1 ? sourcePath[1] : sourcePath[0]; sourcePath[0] = BuildRectBoundaryPointForSide(entry.Node, alternateSide, toward); candidate = NormalizeExitPath(sourcePath, entry.Node, alternateSide); return true; } var explicitEndpoint = BuildRectBoundaryPointForSide(entry.Node, alternateSide, entry.Path[^2]); var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal); if (nodesById.TryGetValue(entry.Edge.SourceNodeId ?? string.Empty, out var sourceNode) && (alternateSide is "top" or "bottom") && TryBuildSafeHorizontalBandCandidate( sourceNode, entry.Node, nodes, entry.Edge.SourceNodeId, entry.Edge.TargetNodeId, entry.Path[0], explicitEndpoint, minLineClearance, preferredSourceExterior: null, out var bandCandidate)) { candidate = bandCandidate; return true; } candidate = NormalizeEntryPath(entry.Path, entry.Node, alternateSide, explicitEndpoint); return true; } private static bool TryBuildSafeHorizontalBandCandidate( ElkPositionedNode sourceNode, ElkPositionedNode targetNode, IReadOnlyCollection nodes, string? sourceNodeId, string? targetNodeId, ElkPoint startBoundary, ElkPoint endBoundary, double minClearance, ElkPoint? preferredSourceExterior, out List candidate) { candidate = []; var route = new List { new() { X = startBoundary.X, Y = startBoundary.Y }, }; var routeStart = route[0]; if (ElkShapeBoundaries.IsGatewayShape(sourceNode)) { var gatewayExteriorCandidates = new List(); if (preferredSourceExterior is { } preferredExterior) { gatewayExteriorCandidates.Add(preferredExterior); } gatewayExteriorCandidates.Add(ElkShapeBoundaries.BuildPreferredGatewaySourceExteriorPoint(sourceNode, startBoundary, endBoundary)); gatewayExteriorCandidates.Add(ElkShapeBoundaries.BuildGatewayExteriorApproachPoint(sourceNode, startBoundary)); ElkPoint? sourceExterior = null; foreach (var exteriorCandidate in gatewayExteriorCandidates) { if (ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(sourceNode, exteriorCandidate) || !ElkShapeBoundaries.HasValidGatewayBoundaryAngle(sourceNode, startBoundary, exteriorCandidate)) { continue; } sourceExterior = exteriorCandidate; break; } if (sourceExterior is null) { return false; } if (!ElkEdgeRoutingGeometry.PointsEqual(route[^1], sourceExterior)) { route.Add(sourceExterior); routeStart = sourceExterior; } } var clearance = Math.Max(24d, minClearance * 0.6d); var minX = Math.Min(routeStart.X, endBoundary.X); var maxX = Math.Max(routeStart.X, endBoundary.X); var graphMinY = nodes.Min(node => node.Y); 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(routeStart.Y, endBoundary.Y) + clearance) .ToArray(); var baseY = Math.Min(Math.Min(routeStart.Y, endBoundary.Y), targetNode.Y); if (blockers.Length > 0) { baseY = Math.Min(baseY, blockers.Min(node => node.Y)); } var bandY = Math.Max(graphMinY - 72d, baseY - clearance); if (bandY >= Math.Min(routeStart.Y, endBoundary.Y) - 0.5d) { return false; } 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 (ElkShapeBoundaries.IsGatewayShape(sourceNode)) { if (!HasAcceptableGatewayBoundaryPath(candidate, nodes, sourceNodeId, targetNodeId, sourceNode, fromStart: true)) { candidate = []; return false; } } else if (!HasValidBoundaryAngle(candidate[0], candidate[1], 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 List RewriteTargetApproachRun( IReadOnlyList path, string side, ElkPoint endpoint, double desiredAxis) { if (!TryExtractTargetApproachRun(path, side, out var runStartIndex, out _)) { return path .Select(point => new ElkPoint { X = point.X, Y = point.Y }) .ToList(); } var prefixEndExclusive = runStartIndex; if (runStartIndex > 0 && !IsOrthogonal(path[runStartIndex - 1], path[runStartIndex])) { prefixEndExclusive = runStartIndex + 1; } else if (prefixEndExclusive < 2 && path.Count > 2) { // Preserve the initial source-exit stub while spreading only the target-side run. prefixEndExclusive = 2; } var rebuilt = path.Take(prefixEndExclusive) .Select(point => new ElkPoint { X = point.X, Y = point.Y }) .ToList(); if (rebuilt.Count == 0) { rebuilt.Add(new ElkPoint { X = path[0].X, Y = path[0].Y }); } const double coordinateTolerance = 0.5d; if (side is "top" or "bottom") { var approachY = double.IsNaN(desiredAxis) ? rebuilt[^1].Y : desiredAxis; if (Math.Abs(rebuilt[^1].Y - approachY) > coordinateTolerance) { rebuilt.Add(new ElkPoint { X = rebuilt[^1].X, Y = approachY }); } if (Math.Abs(rebuilt[^1].X - endpoint.X) > coordinateTolerance) { rebuilt.Add(new ElkPoint { X = endpoint.X, Y = approachY }); } if (Math.Abs(rebuilt[^1].Y - endpoint.Y) > coordinateTolerance) { rebuilt.Add(new ElkPoint { X = endpoint.X, Y = endpoint.Y }); } } else { var approachX = double.IsNaN(desiredAxis) ? rebuilt[^1].X : desiredAxis; if (Math.Abs(rebuilt[^1].X - approachX) > coordinateTolerance) { rebuilt.Add(new ElkPoint { X = approachX, Y = rebuilt[^1].Y }); } if (Math.Abs(rebuilt[^1].Y - endpoint.Y) > coordinateTolerance) { rebuilt.Add(new ElkPoint { X = approachX, Y = endpoint.Y }); } if (Math.Abs(rebuilt[^1].X - endpoint.X) > coordinateTolerance) { rebuilt.Add(new ElkPoint { X = endpoint.X, Y = endpoint.Y }); } } if (!ElkEdgeRoutingGeometry.PointsEqual(rebuilt[^1], endpoint)) { rebuilt.Add(new ElkPoint { X = endpoint.X, Y = endpoint.Y }); } return NormalizePathPoints(rebuilt); } private static bool TryExtractTargetApproachFeeder( IReadOnlyList path, string side, out (ElkPoint Start, ElkPoint End, double BandCoordinate) feeder) { feeder = default; if (!TryExtractTargetApproachRun(path, side, out var runStartIndex, out _) || runStartIndex < 1) { return false; } var start = path[runStartIndex - 1]; var end = path[runStartIndex]; const double coordinateTolerance = 0.5d; if (side is "top" or "bottom") { if (Math.Abs(start.Y - end.Y) > coordinateTolerance) { return false; } feeder = (start, end, start.Y); return true; } if (Math.Abs(start.X - end.X) > coordinateTolerance) { return false; } feeder = (start, end, start.X); return true; } private static List RewriteTargetApproachFeederBand( IReadOnlyList path, string side, double desiredBand) { if (!TryExtractTargetApproachRun(path, side, out var runStartIndex, out var runEndIndex) || runStartIndex < 1) { return path .Select(point => new ElkPoint { X = point.X, Y = point.Y }) .ToList(); } var prefix = path.Take(runStartIndex) .Select(point => new ElkPoint { X = point.X, Y = point.Y }) .ToList(); if (prefix.Count == 0) { prefix.Add(new ElkPoint { X = path[0].X, Y = path[0].Y }); } var endpoint = path[^1]; const double coordinateTolerance = 0.5d; if (side is "top" or "bottom") { if (Math.Abs(prefix[^1].Y - desiredBand) > coordinateTolerance) { prefix.Add(new ElkPoint { X = prefix[^1].X, Y = desiredBand }); } var approachAxis = path[runEndIndex].X; if (Math.Abs(prefix[^1].X - approachAxis) > coordinateTolerance) { prefix.Add(new ElkPoint { X = approachAxis, Y = desiredBand }); } if (Math.Abs(prefix[^1].Y - endpoint.Y) > coordinateTolerance) { prefix.Add(new ElkPoint { X = approachAxis, Y = endpoint.Y }); } } else { if (Math.Abs(prefix[^1].X - desiredBand) > coordinateTolerance) { prefix.Add(new ElkPoint { X = desiredBand, Y = prefix[^1].Y }); } var approachAxis = path[runEndIndex].Y; if (Math.Abs(prefix[^1].Y - approachAxis) > coordinateTolerance) { prefix.Add(new ElkPoint { X = desiredBand, Y = approachAxis }); } if (Math.Abs(prefix[^1].X - endpoint.X) > coordinateTolerance) { prefix.Add(new ElkPoint { X = endpoint.X, Y = approachAxis }); } } prefix.Add(new ElkPoint { X = endpoint.X, Y = endpoint.Y }); return NormalizePathPoints(prefix); } private static List ShiftSingleOrthogonalRun( IReadOnlyList path, int segmentIndex, double desiredCoordinate) { var candidate = path .Select(point => new ElkPoint { X = point.X, Y = point.Y }) .ToList(); if (segmentIndex < 0 || segmentIndex >= candidate.Count - 1) { return candidate; } var start = candidate[segmentIndex]; var end = candidate[segmentIndex + 1]; if (Math.Abs(start.Y - end.Y) <= 0.5d) { var original = start.Y; for (var i = 0; i < candidate.Count; i++) { if (Math.Abs(candidate[i].Y - original) <= 0.5d) { candidate[i] = new ElkPoint { X = candidate[i].X, Y = desiredCoordinate }; } } } else if (Math.Abs(start.X - end.X) <= 0.5d) { var original = start.X; for (var i = 0; i < candidate.Count; i++) { if (Math.Abs(candidate[i].X - original) <= 0.5d) { candidate[i] = new ElkPoint { X = desiredCoordinate, Y = candidate[i].Y }; } } } return NormalizePathPoints(candidate); } private static List ShiftStraightOrthogonalPath( IReadOnlyList path, double desiredCoordinate) { var candidate = path .Select(point => new ElkPoint { X = point.X, Y = point.Y }) .ToList(); if (candidate.Count != 2) { return candidate; } var start = candidate[0]; var end = candidate[1]; if (Math.Abs(start.Y - end.Y) <= 0.5d) { return NormalizePathPoints( [ new ElkPoint { X = start.X, Y = start.Y }, new ElkPoint { X = start.X, Y = desiredCoordinate }, new ElkPoint { X = end.X, Y = desiredCoordinate }, new ElkPoint { X = end.X, Y = end.Y }, ]); } if (Math.Abs(start.X - end.X) <= 0.5d) { return NormalizePathPoints( [ new ElkPoint { X = start.X, Y = start.Y }, new ElkPoint { X = desiredCoordinate, Y = start.Y }, new ElkPoint { X = desiredCoordinate, Y = end.Y }, new ElkPoint { X = end.X, Y = end.Y }, ]); } return candidate; } private static double[] ResolveLaneShiftCoordinates( ElkPoint start, ElkPoint end, ElkPoint otherStart, ElkPoint otherEnd, double minLineClearance) { var offset = minLineClearance + 4d; if (Math.Abs(start.Y - end.Y) <= 0.5d && Math.Abs(otherStart.Y - otherEnd.Y) <= 0.5d) { var lower = otherStart.Y - offset; var upper = otherStart.Y + offset; return start.Y <= otherStart.Y ? [lower, upper] : [upper, lower]; } if (Math.Abs(start.X - end.X) <= 0.5d && Math.Abs(otherStart.X - otherEnd.X) <= 0.5d) { var lower = otherStart.X - offset; var upper = otherStart.X + offset; return start.X <= otherStart.X ? [lower, upper] : [upper, lower]; } return []; } private static bool SegmentLeavesGraphBand( IReadOnlyList path, double graphMinY, double graphMaxY) { return path.Any(point => point.Y < graphMinY - 96d || point.Y > graphMaxY + 96d); } private static bool TrySeparateSharedLaneConflict( ElkRoutedEdge edge, ElkRoutedEdge otherEdge, ElkPositionedNode[] nodes, double minLineClearance, double graphMinY, double graphMaxY, (double Left, double Top, double Right, double Bottom, string Id)[] nodeObstacles, out ElkRoutedEdge repairedEdge) { repairedEdge = edge; var path = ExtractFullPath(edge); if (path.Count < 2) { return false; } var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal); var originalTargetSide = nodesById.TryGetValue(edge.TargetNodeId ?? string.Empty, out var targetNode) && !ElkShapeBoundaries.IsGatewayShape(targetNode) ? ResolveTargetApproachSide(path, targetNode) : null; for (var segmentIndex = 0; segmentIndex < path.Count - 1; segmentIndex++) { var start = path[segmentIndex]; var end = path[segmentIndex + 1]; var isHorizontal = Math.Abs(start.Y - end.Y) <= 0.5d; var isVertical = Math.Abs(start.X - end.X) <= 0.5d; if (!isHorizontal && !isVertical) { continue; } foreach (var otherSegment in ElkEdgeRoutingGeometry.FlattenSegments(otherEdge)) { if (!SegmentsShareLane( start, end, otherSegment.Start, otherSegment.End, minLineClearance)) { continue; } foreach (var alternateCoordinate in ResolveLaneShiftCoordinates( start, end, otherSegment.Start, otherSegment.End, minLineClearance)) { var candidate = path.Count == 2 ? ShiftStraightOrthogonalPath(path, alternateCoordinate) : ShiftSingleOrthogonalRun(path, segmentIndex, alternateCoordinate); if (!PathChanged(path, candidate) || (originalTargetSide is not null && ResolveTargetApproachSide(candidate, targetNode) != originalTargetSide) || HasNodeObstacleCrossing(candidate, nodes, edge.SourceNodeId, edge.TargetNodeId) || SegmentLeavesGraphBand(candidate, graphMinY, graphMaxY)) { continue; } var crossesObstacle = false; for (var candidateIndex = 0; candidateIndex < candidate.Count - 1; candidateIndex++) { if (!SegmentCrossesObstacle(candidate[candidateIndex], candidate[candidateIndex + 1], nodeObstacles, edge.SourceNodeId, edge.TargetNodeId)) { continue; } crossesObstacle = true; break; } if (crossesObstacle) { continue; } repairedEdge = BuildSingleSectionEdge(edge, candidate); repairedEdge = RepairBoundaryAnglesAndTargetApproaches( [repairedEdge], nodes, minLineClearance)[0]; repairedEdge = NormalizeSourceExitAngles([repairedEdge], nodes)[0]; var repairedPath = ExtractFullPath(repairedEdge); if ((originalTargetSide is not null && ResolveTargetApproachSide(repairedPath, targetNode) != originalTargetSide) || HasNodeObstacleCrossing(repairedPath, nodes, edge.SourceNodeId, edge.TargetNodeId) || SegmentLeavesGraphBand(repairedPath, graphMinY, graphMaxY)) { repairedEdge = edge; continue; } if (ElkEdgeRoutingScoring.DetectSharedLaneConflicts([repairedEdge, otherEdge], nodes).Count > 0) { repairedEdge = edge; continue; } return true; } } } repairedEdge = edge; return false; } private static bool SegmentsShareLane( ElkPoint leftStart, ElkPoint leftEnd, ElkPoint rightStart, ElkPoint rightEnd, double minLineClearance) { var laneTolerance = Math.Max(4d, Math.Min(12d, minLineClearance * 0.2d)); var minSharedLength = Math.Max(24d, minLineClearance * 0.4d); if (Math.Abs(leftStart.Y - leftEnd.Y) <= 0.5d && Math.Abs(rightStart.Y - rightEnd.Y) <= 0.5d && Math.Abs(leftStart.Y - rightStart.Y) <= laneTolerance) { var leftMinX = Math.Min(leftStart.X, leftEnd.X); var leftMaxX = Math.Max(leftStart.X, leftEnd.X); var rightMinX = Math.Min(rightStart.X, rightEnd.X); var rightMaxX = Math.Max(rightStart.X, rightEnd.X); return Math.Min(leftMaxX, rightMaxX) - Math.Max(leftMinX, rightMinX) >= minSharedLength; } if (Math.Abs(leftStart.X - leftEnd.X) <= 0.5d && Math.Abs(rightStart.X - rightEnd.X) <= 0.5d && Math.Abs(leftStart.X - rightStart.X) <= laneTolerance) { var leftMinY = Math.Min(leftStart.Y, leftEnd.Y); var leftMaxY = Math.Max(leftStart.Y, leftEnd.Y); var rightMinY = Math.Min(rightStart.Y, rightEnd.Y); var rightMaxY = Math.Max(rightStart.Y, rightEnd.Y); return Math.Min(leftMaxY, rightMaxY) - Math.Max(leftMinY, rightMinY) >= minSharedLength; } return false; } private static List RewriteSourceDepartureRun( IReadOnlyList path, string side, ElkPoint boundaryPoint, double desiredAxis) { if (!TryExtractSourceDepartureRun(path, side, out _, out var runEndIndex)) { return path .Select(point => new ElkPoint { X = point.X, Y = point.Y }) .ToList(); } var suffixStartIndex = runEndIndex + 1; if (suffixStartIndex >= path.Count) { return path .Select(point => new ElkPoint { X = point.X, Y = point.Y }) .ToList(); } const double coordinateTolerance = 0.5d; var suffixStart = path[suffixStartIndex]; var rebuilt = new List { new() { X = boundaryPoint.X, Y = boundaryPoint.Y }, }; if (side is "left" or "right") { if (Math.Abs(rebuilt[^1].X - desiredAxis) > coordinateTolerance) { rebuilt.Add(new ElkPoint { X = desiredAxis, Y = rebuilt[^1].Y }); } if (Math.Abs(rebuilt[^1].Y - suffixStart.Y) > coordinateTolerance) { rebuilt.Add(new ElkPoint { X = desiredAxis, Y = suffixStart.Y }); } } else { if (Math.Abs(rebuilt[^1].Y - desiredAxis) > coordinateTolerance) { rebuilt.Add(new ElkPoint { X = rebuilt[^1].X, Y = desiredAxis }); } if (Math.Abs(rebuilt[^1].X - suffixStart.X) > coordinateTolerance) { rebuilt.Add(new ElkPoint { X = suffixStart.X, Y = desiredAxis }); } } for (var i = suffixStartIndex; i < path.Count; i++) { var point = path[i]; if (!ElkEdgeRoutingGeometry.PointsEqual(rebuilt[^1], point)) { rebuilt.Add(new ElkPoint { X = point.X, Y = point.Y }); } } return NormalizePathPoints(rebuilt); } private static bool TryExtractTargetApproachRun( IReadOnlyList path, string side, out int runStartIndex, out int runEndIndex) { runStartIndex = -1; runEndIndex = -1; if (path.Count < 2 || side is not ("left" or "right" or "top" or "bottom")) { return false; } const double coordinateTolerance = 0.5d; runEndIndex = path.Count - 1; if (side is "top" or "bottom") { var axis = path[runEndIndex].X; runStartIndex = runEndIndex; while (runStartIndex > 0 && Math.Abs(path[runStartIndex - 1].X - axis) <= coordinateTolerance) { runStartIndex--; } return runEndIndex >= runStartIndex; } var xAxis = path[runEndIndex].Y; runStartIndex = runEndIndex; while (runStartIndex > 0 && Math.Abs(path[runStartIndex - 1].Y - xAxis) <= coordinateTolerance) { runStartIndex--; } return runEndIndex >= runStartIndex; } private static bool TryExtractSourceDepartureRun( IReadOnlyList path, string side, out int runStartIndex, out int runEndIndex) { runStartIndex = -1; runEndIndex = -1; if (path.Count < 2 || side is not ("left" or "right" or "top" or "bottom")) { return false; } const double coordinateTolerance = 0.5d; runStartIndex = 0; runEndIndex = 1; if (side is "left" or "right") { var axis = path[1].Y; if (Math.Abs(path[0].Y - axis) > coordinateTolerance) { return false; } while (runEndIndex + 1 < path.Count && Math.Abs(path[runEndIndex + 1].Y - axis) <= coordinateTolerance) { runEndIndex++; } return runEndIndex > runStartIndex; } var xAxis = path[1].X; if (Math.Abs(path[0].X - xAxis) > coordinateTolerance) { return false; } while (runEndIndex + 1 < path.Count && Math.Abs(path[runEndIndex + 1].X - xAxis) <= coordinateTolerance) { runEndIndex++; } return runEndIndex > runStartIndex; } private static bool GroupHasSourceDepartureJoin( IReadOnlyList<(IReadOnlyList Path, string Side)> entries, double minLineClearance) { for (var i = 0; i < entries.Count; i++) { var left = entries[i]; var leftSegments = FlattenSegmentsNearStart(left.Path, 3); for (var j = i + 1; j < entries.Count; j++) { var right = entries[j]; if (!string.Equals(left.Side, right.Side, StringComparison.Ordinal)) { continue; } var rightSegments = FlattenSegmentsNearStart(right.Path, 3); foreach (var leftSegment in leftSegments) { foreach (var rightSegment in rightSegments) { if (!ElkEdgeRoutingGeometry.AreParallelAndClose( leftSegment.Start, leftSegment.End, rightSegment.Start, rightSegment.End, minLineClearance)) { continue; } var overlap = ElkEdgeRoutingGeometry.ComputeSharedSegmentLength( leftSegment.Start, leftSegment.End, rightSegment.Start, rightSegment.End); if (overlap > 8d) { return true; } } } } } return false; } private static bool HasRepeatCollectorNodeClearanceViolation( IReadOnlyList path, IReadOnlyCollection nodes, string? sourceNodeId, string? targetNodeId, double minClearance) { for (var i = 0; i < path.Count - 1; i++) { var start = path[i]; var end = path[i + 1]; var horizontal = Math.Abs(start.Y - end.Y) < 2d; var vertical = Math.Abs(start.X - end.X) < 2d; if (!horizontal && !vertical) { continue; } foreach (var node in nodes) { if (string.Equals(node.Id, sourceNodeId, StringComparison.Ordinal) || string.Equals(node.Id, targetNodeId, StringComparison.Ordinal)) { continue; } if (horizontal) { var overlapX = Math.Max(start.X, end.X) > node.X && Math.Min(start.X, end.X) < node.X + node.Width; if (!overlapX) { continue; } var distance = Math.Min(Math.Abs(start.Y - node.Y), Math.Abs(start.Y - (node.Y + node.Height))); if (distance > 0.5d && distance < minClearance) { return true; } continue; } var overlapY = Math.Max(start.Y, end.Y) > node.Y && Math.Min(start.Y, end.Y) < node.Y + node.Height; if (!overlapY) { continue; } var verticalDistance = Math.Min(Math.Abs(start.X - node.X), Math.Abs(start.X - (node.X + node.Width))); if (verticalDistance > 0.5d && verticalDistance < minClearance) { return true; } } } return false; } 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 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 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 = Math.Max(96d, gatewayNode.Width + gatewayNode.Height); 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, }); } 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); 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) && 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; if (targetNode is not null && ElkShapeBoundaries.IsGatewayShape(targetNode)) { if (currentPath.Count >= 2 && !ElkEdgeRoutingGeometry.PointsEqual(currentPath[^2], end) && ElkShapeBoundaries.HasValidGatewayBoundaryAngle(targetNode, end, currentPath[^2])) { preservedGatewayTargetApproach = currentPath[^2]; targetBridgeAxis = horizontalDominant ? currentPath[^2].X : currentPath[^2].Y; } else { 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 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); 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 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 (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)); } private static ElkRoutedEdge CloneEdgeWithKind(ElkRoutedEdge edge, string? kind) { return new ElkRoutedEdge { Id = edge.Id, SourceNodeId = edge.SourceNodeId, TargetNodeId = edge.TargetNodeId, SourcePortId = edge.SourcePortId, TargetPortId = edge.TargetPortId, Kind = kind, Label = edge.Label, Sections = edge.Sections .Select(section => new ElkEdgeSection { StartPoint = section.StartPoint, EndPoint = section.EndPoint, BendPoints = section.BendPoints.ToArray(), }) .ToArray(), }; } private static bool ContainsInternalKindMarker(string? kind, string marker) { if (string.IsNullOrWhiteSpace(kind)) { return false; } return kind .Split('|', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) .Any(part => string.Equals(part, marker, StringComparison.OrdinalIgnoreCase)); } private static string AppendInternalKindMarker(string? kind, string marker) { if (ContainsInternalKindMarker(kind, marker)) { return kind!; } return string.IsNullOrWhiteSpace(kind) ? marker : $"{kind}|{marker}"; } private static string? RemoveInternalKindMarker(string? kind, string marker) { if (string.IsNullOrWhiteSpace(kind)) { return kind; } var parts = kind .Split('|', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) .Where(part => !string.Equals(part, marker, StringComparison.OrdinalIgnoreCase)) .ToArray(); if (parts.Length == 0) { return null; } return string.Join("|", parts); } private static List NormalizePathPoints(IReadOnlyList points) { const double coordinateTolerance = 0.5d; var deduped = new List(); foreach (var point in points) { if (deduped.Count == 0 || !ElkEdgeRoutingGeometry.PointsEqual(deduped[^1], point)) { deduped.Add(point); } } if (deduped.Count <= 2) { return deduped; } var simplified = new List { deduped[0] }; for (var i = 1; i < deduped.Count - 1; i++) { var previous = simplified[^1]; var current = deduped[i]; var next = deduped[i + 1]; var sameX = Math.Abs(previous.X - current.X) <= coordinateTolerance && Math.Abs(current.X - next.X) <= coordinateTolerance; var sameY = Math.Abs(previous.Y - current.Y) <= coordinateTolerance && Math.Abs(current.Y - next.Y) <= coordinateTolerance; if (!sameX && !sameY) { simplified.Add(current); } } simplified.Add(deduped[^1]); return simplified; } }