namespace StellaOps.ElkSharp; internal static partial class ElkEdgePostProcessor { 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 alignedAssignedSideEntry = NormalizeEntryPath(normalized, targetNode, targetSide, assignedEndpoint); if (HasClearBoundarySegments(alignedAssignedSideEntry, nodes, edge.SourceNodeId, edge.TargetNodeId, false, 3) && HasValidBoundaryAngle(alignedAssignedSideEntry[^1], alignedAssignedSideEntry[^2], targetNode)) { normalized = alignedAssignedSideEntry; } else { 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[] 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; } 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; } }