namespace StellaOps.ElkSharp; internal static partial 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)) { if (!(ElkShapeBoundaries.IsGatewayShape(sourceNode) && ShouldPreserveSaturatedGatewaySourceFace(edge, edges, sourceNode, normalized))) { 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; } } if (ElkShapeBoundaries.IsGatewayShape(sourceNode) && ShouldPreserveSaturatedGatewaySourceFace(edge, edges, sourceNode, path)) { 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)) { normalized = EnforceGatewaySourceExitQuality( 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(); var changedEdgeIds = new List(); 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]) || HasShortGatewayTargetOrthogonalHook(repaired, targetNode)) && targetNode.Kind == "Decision") { repaired = ForceDecisionExteriorTargetEntry(path, targetNode); } if ((!PathChanged(path, repaired) || !CanAcceptGatewayTargetRepair(repaired, targetNode) || HasShortGatewayTargetOrthogonalHook(repaired, targetNode)) && targetNode.Kind == "Decision") { repaired = ForceDecisionDirectTargetEntry(path, targetNode); } if (!PathChanged(path, repaired) || !CanAcceptGatewayTargetRepair(repaired, targetNode) || HasShortGatewayTargetOrthogonalHook(repaired, targetNode)) { continue; } result[i] = BuildSingleSectionEdge(edge, repaired); changedEdgeIds.Add(edge.Id); } if (changedEdgeIds.Count == 0) { return result; } var focusedEdgeIds = changedEdgeIds .Distinct(StringComparer.Ordinal) .OrderBy(edgeId => edgeId, StringComparer.Ordinal) .ToArray(); var minLineClearance = ResolveMinLineClearance(nodes); result = SpreadSourceDepartureJoins(result, nodes, minLineClearance, focusedEdgeIds); result = SeparateMixedNodeFaceLaneConflicts(result, nodes, minLineClearance, focusedEdgeIds); result = SeparateSharedLaneConflicts(result, nodes, minLineClearance, focusedEdgeIds); 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) && ElkEdgeRoutingScoring.CountUnderNodeViolations([edge], nodes) > 0) { 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; } } 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; } }