Files
git.stella-ops.org/src/__Libraries/StellaOps.ElkSharp/ElkEdgePostProcessor.UnderNode.cs

2619 lines
92 KiB
C#

namespace StellaOps.ElkSharp;
internal static partial class ElkEdgePostProcessor
{
private static List<ElkPoint> TryLiftUnderNodeSegments(
IReadOnlyList<ElkPoint> path,
IReadOnlyCollection<ElkPositionedNode> 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<ElkPoint>(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<ElkPoint> path,
IReadOnlyCollection<ElkPositionedNode> nodes,
double minClearance,
out List<ElkPoint> 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<ElkPoint> path,
ElkPositionedNode sourceNode,
ElkPositionedNode targetNode,
IReadOnlyCollection<ElkPositionedNode> nodes,
string? sourceNodeId,
string? targetNodeId,
double minClearance,
string? debugEdgeId,
out List<ElkPoint> 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<ElkPoint>? 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<ElkPoint> originalPath,
ElkPositionedNode sourceNode,
ElkPositionedNode targetNode,
IReadOnlyCollection<ElkPositionedNode> nodes,
string? sourceNodeId,
string? targetNodeId,
string targetSide,
double bandY,
ElkPoint endBoundary,
double minClearance,
out List<ElkPoint> 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<ElkPoint>? 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<double> EnumerateUnderNodeBandEntryXs(
IReadOnlyList<ElkPoint> originalPath,
ElkPoint endBoundary,
IReadOnlyCollection<ElkPositionedNode> 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<double>
{
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<ElkPoint> 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<ElkPoint> BuildUnderNodeBandCandidatePath(
ElkPoint startBoundary,
ElkPoint sourceExterior,
double bandEntryX,
double bandY,
ElkPoint targetExterior,
ElkPoint targetBoundary)
{
var route = new List<ElkPoint> { 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<ElkPoint> candidate,
ElkPositionedNode sourceNode,
ElkPositionedNode targetNode,
IReadOnlyCollection<ElkPositionedNode> 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<ElkPoint> path,
IReadOnlyCollection<ElkPositionedNode> nodes,
string? sourceNodeId,
string? targetNodeId,
double minClearance,
out double bandY)
{
bandY = double.NaN;
var blockers = new Dictionary<string, ElkPositionedNode>(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<ElkPoint> path,
ElkPositionedNode sourceNode,
ElkPositionedNode targetNode,
double bandY)
{
const double coordinateTolerance = 0.5d;
var referenceXs = new List<double>
{
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<double>
{
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<string>(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<ElkPoint> originalPath,
IReadOnlyList<ElkPoint> candidate,
ElkPositionedNode targetNode,
string requestedTargetSide,
string preferredTargetSide,
double sideBias,
IReadOnlyCollection<ElkPositionedNode> 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<ElkPoint> 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<ElkPoint> path,
IReadOnlyCollection<ElkPositionedNode> nodes,
string? sourceNodeId,
string? targetNodeId,
double minClearance)
{
var blockers = new List<string>();
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 ? "<none>" : string.Join(" | ", blockers);
}
private static bool TryBuildGatewaySourceUnderNodeDropCandidate(
IReadOnlyList<ElkPoint> path,
ElkPositionedNode sourceNode,
ElkPositionedNode targetNode,
IReadOnlyCollection<ElkPositionedNode> nodes,
string? sourceNodeId,
string? targetNodeId,
double minClearance,
out List<ElkPoint> 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<ElkPoint> { 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<ElkPositionedNode> nodes,
string? sourceNodeId,
string? targetNodeId,
ElkPoint endBoundary,
double minClearance,
out List<ElkPoint> 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<ElkPoint>
{
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<ElkPoint> { 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<ElkPoint> path,
ElkPositionedNode sourceNode)
{
var prefix = ExtractGatewaySourceBandPrefix(path);
return !HasGatewaySourceExitBacktracking(prefix)
&& !HasGatewaySourceExitCurl(prefix)
&& !HasGatewaySourceDominantAxisDetour(prefix, sourceNode);
}
private static IReadOnlyList<ElkPoint> ExtractGatewaySourceBandPrefix(IReadOnlyList<ElkPoint> 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<ElkPoint> path,
ElkPositionedNode sourceNode,
ElkPositionedNode targetNode,
IReadOnlyCollection<ElkPositionedNode> nodes,
string? sourceNodeId,
string? targetNodeId,
bool requireUnderNodeImprovement,
double minClearance,
out List<ElkPoint> 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<ElkPoint> path,
IReadOnlyCollection<ElkPositionedNode> 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<ElkPositionedNode> 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<ElkPoint> 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<ElkPoint> path,
IReadOnlyCollection<ElkPositionedNode> 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<ElkPoint> left, IReadOnlyList<ElkPoint> right)
{
return left.Count != right.Count
|| !left.Zip(right, ElkEdgeRoutingGeometry.PointsEqual).All(equal => equal);
}
private static string CreatePathSignature(IReadOnlyList<ElkPoint> path)
{
return string.Join(";", path.Select(point => $"{point.X:F3},{point.Y:F3}"));
}
private static bool HasAcceptableGatewayBoundaryPath(
IReadOnlyList<ElkPoint> path,
IReadOnlyCollection<ElkPositionedNode> 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<ElkPoint> path,
IReadOnlyCollection<ElkPositionedNode> nodes,
string? sourceNodeId,
string? targetNodeId)
{
if (path.Count < 2)
{
return false;
}
var obstacles = nodes.Select(node => (
Left: node.X,
Top: node.Y,
Right: node.X + node.Width,
Bottom: node.Y + node.Height,
Id: node.Id)).ToArray();
for (var i = 0; i < path.Count - 1; i++)
{
if (SegmentCrossesObstacle(path[i], path[i + 1], obstacles, sourceNodeId ?? string.Empty, targetNodeId ?? string.Empty))
{
return true;
}
}
return false;
}
private static bool HasNodeObstacleCrossing(
IReadOnlyList<ElkPoint> path,
(double Left, double Top, double Right, double Bottom, string Id)[] nodeObstacles,
string? sourceNodeId,
string? targetNodeId)
{
if (path.Count < 2)
{
return false;
}
for (var i = 0; i < path.Count - 1; i++)
{
if (SegmentCrossesObstacle(path[i], path[i + 1], nodeObstacles, sourceNodeId ?? string.Empty, targetNodeId ?? string.Empty))
{
return true;
}
}
return false;
}
private static bool CanAcceptGatewayTargetRepair(
IReadOnlyList<ElkPoint> 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<ElkPoint> path,
ElkPositionedNode gatewayNode)
{
var maxDiagonalLength = (gatewayNode.Width + gatewayNode.Height) / 2d;
for (var i = 0; i < path.Count - 1; i++)
{
var start = path[i];
var end = path[i + 1];
var dx = Math.Abs(end.X - start.X);
var dy = Math.Abs(end.Y - start.Y);
if (dx <= 3d || dy <= 3d)
{
continue;
}
if (ElkEdgeRoutingGeometry.ComputeSegmentLength(start, end) > maxDiagonalLength)
{
return true;
}
}
return false;
}
private static int FindFirstGatewayExteriorPointIndex(
IReadOnlyList<ElkPoint> 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<ElkPoint> 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<ElkPoint> ForceGatewayExteriorTargetApproach(
IReadOnlyList<ElkPoint> 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<ElkPoint> 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<ElkPoint>();
AddUniquePoint(
candidates,
ElkShapeBoundaries.BuildGatewayDirectionalExteriorPoint(targetNode, boundaryPoint, exteriorAnchor, padding));
AddUniquePoint(
candidates,
ElkShapeBoundaries.BuildGatewayExteriorApproachPoint(targetNode, boundaryPoint, padding));
if (boundaryPoint.X <= centerX + 0.5d)
{
AddUniquePoint(
candidates,
new ElkPoint
{
X = targetNode.X - padding,
Y = boundaryPoint.Y,
});
}
if (boundaryPoint.X >= centerX - 0.5d)
{
AddUniquePoint(
candidates,
new ElkPoint
{
X = targetNode.X + targetNode.Width + padding,
Y = boundaryPoint.Y,
});
}
if (boundaryPoint.Y <= centerY + 0.5d)
{
AddUniquePoint(
candidates,
new ElkPoint
{
X = boundaryPoint.X,
Y = targetNode.Y - padding,
});
}
if (boundaryPoint.Y >= centerY - 0.5d)
{
AddUniquePoint(
candidates,
new ElkPoint
{
X = boundaryPoint.X,
Y = targetNode.Y + targetNode.Height + padding,
});
}
if (targetNode.Kind == "Decision")
{
if (boundaryPoint.X <= centerX + 0.5d)
{
var diagonalDx = Math.Abs(boundaryPoint.X - (targetNode.X - padding));
AddUniquePoint(
candidates,
new ElkPoint
{
X = targetNode.X - padding,
Y = boundaryPoint.Y - diagonalDx,
});
AddUniquePoint(
candidates,
new ElkPoint
{
X = targetNode.X - padding,
Y = boundaryPoint.Y + diagonalDx,
});
}
if (boundaryPoint.X >= centerX - 0.5d)
{
var diagonalDx = Math.Abs((targetNode.X + targetNode.Width + padding) - boundaryPoint.X);
AddUniquePoint(
candidates,
new ElkPoint
{
X = targetNode.X + targetNode.Width + padding,
Y = boundaryPoint.Y - diagonalDx,
});
AddUniquePoint(
candidates,
new ElkPoint
{
X = targetNode.X + targetNode.Width + padding,
Y = boundaryPoint.Y + diagonalDx,
});
}
if (boundaryPoint.Y <= centerY + 0.5d)
{
var diagonalDy = Math.Abs(boundaryPoint.Y - (targetNode.Y - padding));
AddUniquePoint(
candidates,
new ElkPoint
{
X = boundaryPoint.X - diagonalDy,
Y = targetNode.Y - padding,
});
AddUniquePoint(
candidates,
new ElkPoint
{
X = boundaryPoint.X + diagonalDy,
Y = targetNode.Y - padding,
});
}
if (boundaryPoint.Y >= centerY - 0.5d)
{
var diagonalDy = Math.Abs((targetNode.Y + targetNode.Height + padding) - boundaryPoint.Y);
AddUniquePoint(
candidates,
new ElkPoint
{
X = boundaryPoint.X - diagonalDy,
Y = targetNode.Y + targetNode.Height + padding,
});
AddUniquePoint(
candidates,
new ElkPoint
{
X = boundaryPoint.X + diagonalDy,
Y = targetNode.Y + targetNode.Height + padding,
});
}
}
return candidates
.Where(candidate => !ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(targetNode, candidate)
&& ElkShapeBoundaries.HasValidGatewayBoundaryAngle(targetNode, boundaryPoint, candidate))
.OrderBy(candidate => ScoreForcedGatewayExteriorApproachCandidate(targetNode, boundaryPoint, candidate, exteriorAnchor))
.ToArray();
}
private static double ScoreForcedGatewayExteriorApproachCandidate(
ElkPositionedNode targetNode,
ElkPoint boundaryPoint,
ElkPoint candidate,
ElkPoint exteriorAnchor)
{
var score = Math.Abs(candidate.X - exteriorAnchor.X) + Math.Abs(candidate.Y - exteriorAnchor.Y);
score += (Math.Abs(candidate.X - boundaryPoint.X) + Math.Abs(candidate.Y - boundaryPoint.Y)) * 0.25d;
var desiredDx = boundaryPoint.X - exteriorAnchor.X;
var desiredDy = boundaryPoint.Y - exteriorAnchor.Y;
var approachDx = candidate.X - boundaryPoint.X;
var approachDy = candidate.Y - boundaryPoint.Y;
if (Math.Abs(desiredDx) >= Math.Abs(desiredDy) * 1.15d
&& Math.Sign(approachDx) != 0
&& Math.Sign(approachDx) != Math.Sign(exteriorAnchor.X - boundaryPoint.X))
{
score += 10_000d;
}
if (Math.Abs(desiredDy) >= Math.Abs(desiredDx) * 1.15d
&& Math.Sign(approachDy) != 0
&& Math.Sign(approachDy) != Math.Sign(exteriorAnchor.Y - boundaryPoint.Y))
{
score += 10_000d;
}
var preferredExterior = ElkShapeBoundaries.BuildGatewayExteriorApproachPoint(targetNode, boundaryPoint);
if (ElkEdgeRoutingGeometry.PointsEqual(candidate, preferredExterior))
{
score -= 8d;
}
return score;
}
private static bool NeedsGatewayDiagonalStub(ElkPoint start, ElkPoint end)
{
var deltaX = Math.Abs(end.X - start.X);
var deltaY = Math.Abs(end.Y - start.Y);
if (deltaX < 3d || deltaY < 3d)
{
return false;
}
var ratio = deltaX / Math.Max(deltaY, 0.001d);
return ratio < 0.55d || ratio > 1.85d;
}
private static bool ShouldUseGatewayDiagonalStub(
ElkPositionedNode node,
ElkPoint start,
ElkPoint end)
{
return !ElkShapeBoundaries.IsNearGatewayVertex(node, start)
&& !ElkShapeBoundaries.IsNearGatewayVertex(node, end)
&& NeedsGatewayDiagonalStub(start, end);
}
private static List<ElkPoint> BuildGatewayExitStubbedPath(
IReadOnlyList<ElkPoint> path,
ElkPoint boundary,
ElkPoint anchor)
{
var stub = BuildGatewayDiagonalStubPoint(boundary, anchor);
var rebuilt = new List<ElkPoint> { 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<ElkPoint> BuildGatewayEntryStubbedPath(
IReadOnlyList<ElkPoint> 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<ElkPoint> 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<ElkPoint> 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<ElkPoint> ExtractFullPath(ElkRoutedEdge edge)
{
var path = new List<ElkPoint>();
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<ElkPoint> 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<ElkPoint>? TryBuildLocalObstacleSkirtBoundaryShortcut(
IReadOnlyList<ElkPoint> currentPath,
ElkPoint start,
ElkPoint end,
IReadOnlyCollection<ElkPositionedNode> 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<ElkPoint>? 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<double>();
AddUniqueCoordinate(candidateClearances, Math.Max(0d, obstaclePadding));
AddUniqueCoordinate(candidateClearances, Math.Min(Math.Max(0d, obstaclePadding), 24d));
AddUniqueCoordinate(candidateClearances, 8d);
AddUniqueCoordinate(candidateClearances, 0d);
candidateClearances.Sort((left, right) => right.CompareTo(left));
void ConsiderCandidate(
IReadOnlyList<ElkPoint> rawCandidate,
IReadOnlyList<(double Left, double Top, double Right, double Bottom, string Id)> obstacles)
{
var candidate = NormalizePathPoints(rawCandidate);
if (candidate.Count < 2)
{
return;
}
for (var i = 1; i < candidate.Count; i++)
{
if (SegmentCrossesObstacle(candidate[i - 1], candidate[i], obstacles.ToArray(), sourceNodeId, targetNodeId))
{
return;
}
}
if (sourceNode is not null)
{
if (ElkShapeBoundaries.IsGatewayShape(sourceNode)
&& !HasAcceptableGatewayBoundaryPath(
candidate,
nodes,
sourceNodeId,
targetNodeId,
sourceNode,
fromStart: true))
{
return;
}
if (!ElkShapeBoundaries.IsGatewayShape(sourceNode)
&& !HasValidBoundaryAngle(candidate[0], candidate[1], sourceNode))
{
return;
}
}
if (targetNode is not null
&& !ElkShapeBoundaries.IsGatewayShape(targetNode)
&& !HasValidBoundaryAngle(candidate[^1], candidate[^2], targetNode))
{
return;
}
if (targetNode is not null
&& !ElkShapeBoundaries.IsGatewayShape(targetNode)
&& HasTargetApproachBacktracking(candidate, targetNode))
{
return;
}
if (targetNode is not null
&& ElkShapeBoundaries.IsGatewayShape(targetNode)
&& (!CanAcceptGatewayTargetRepair(candidate, targetNode)
|| !HasAcceptableGatewayBoundaryPath(
candidate,
nodes,
sourceNodeId,
targetNodeId,
targetNode,
fromStart: false)))
{
return;
}
var underNodeSegments = CountUnderNodeSegments(
candidate,
nodes,
sourceNodeId,
targetNodeId,
minLineClearance);
var score =
(underNodeSegments * 100_000d)
+ ComputePathLength(candidate)
+ (Math.Max(0, candidate.Count - 2) * 4d);
if (score >= bestScore - 0.5d)
{
return;
}
bestScore = score;
bestPath = candidate;
}
static bool IsUsableForwardBridgeAxis(double startAxis, double endAxis, double candidateAxis)
{
const double tolerance = 0.5d;
var desiredDelta = endAxis - startAxis;
var candidateDelta = candidateAxis - startAxis;
if (Math.Abs(desiredDelta) <= tolerance
|| Math.Abs(candidateDelta) <= tolerance
|| Math.Sign(candidateDelta) != Math.Sign(desiredDelta))
{
return false;
}
var minAxis = Math.Min(startAxis, endAxis) + tolerance;
var maxAxis = Math.Max(startAxis, endAxis) - tolerance;
return candidateAxis >= minAxis && candidateAxis <= maxAxis;
}
static void AddForwardBridgeAxisCandidates(List<double> 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<double>();
AddUniqueCoordinate(sourceBridgeAxes, startAxis);
if (currentPath.Count >= 2 && !ElkEdgeRoutingGeometry.PointsEqual(currentPath[1], start))
{
var currentBridgeAxis = horizontalDominant ? currentPath[1].X : currentPath[1].Y;
if (IsUsableForwardBridgeAxis(startAxis, endAxis, currentBridgeAxis))
{
AddUniqueCoordinate(sourceBridgeAxes, currentBridgeAxis);
}
}
AddForwardBridgeAxisCandidates(sourceBridgeAxes, startAxis, endAxis);
var targetBridgeAxis = horizontalDominant ? end.X : end.Y;
ElkPoint? preservedGatewayTargetApproach = null;
double? preservedRectTargetApproachAxis = null;
if (targetNode is not null)
{
if (currentPath.Count >= 2
&& !ElkEdgeRoutingGeometry.PointsEqual(currentPath[^2], end))
{
if (ElkShapeBoundaries.IsGatewayShape(targetNode))
{
if (ElkShapeBoundaries.HasValidGatewayBoundaryAngle(targetNode, end, currentPath[^2]))
{
preservedGatewayTargetApproach = currentPath[^2];
targetBridgeAxis = horizontalDominant ? currentPath[^2].X : currentPath[^2].Y;
}
}
else if (HasValidBoundaryAngle(end, currentPath[^2], targetNode))
{
const double coordinateTolerance = 0.5d;
if (horizontalDominant && Math.Abs(currentPath[^2].X - end.X) <= coordinateTolerance)
{
preservedRectTargetApproachAxis = currentPath[^2].X;
}
else if (!horizontalDominant && Math.Abs(currentPath[^2].Y - end.Y) <= coordinateTolerance)
{
preservedRectTargetApproachAxis = currentPath[^2].Y;
}
}
}
else if (ElkShapeBoundaries.IsGatewayShape(targetNode))
{
var targetExterior = ElkShapeBoundaries.BuildGatewayDirectionalExteriorPoint(targetNode, end, start);
if (!ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(targetNode, targetExterior)
&& ElkShapeBoundaries.HasValidGatewayBoundaryAngle(targetNode, end, targetExterior))
{
targetBridgeAxis = horizontalDominant ? targetExterior.X : targetExterior.Y;
}
}
}
if (horizontalDominant)
{
foreach (var clearance in candidateClearances)
{
var obstacles = ExpandObstacles(rawObstacles, clearance);
var minX = Math.Min(start.X, end.X) + 0.5d;
var maxX = Math.Max(start.X, end.X) - 0.5d;
var corridorTop = Math.Min(start.Y, end.Y) - clearance;
var corridorBottom = Math.Max(start.Y, end.Y) + clearance;
var bypassYCandidates = new List<double> { start.Y, end.Y };
var cornerBridgeXCandidates = new List<double>();
foreach (var point in currentPath.Skip(1).Take(Math.Max(0, currentPath.Count - 2)))
{
AddUniqueCoordinate(bypassYCandidates, point.Y);
if (IsUsableForwardBridgeAxis(start.X, end.X, point.X))
{
AddUniqueCoordinate(cornerBridgeXCandidates, point.X);
}
}
foreach (var obstacle in obstacles)
{
if (string.Equals(obstacle.Id, sourceId, StringComparison.Ordinal)
|| string.Equals(obstacle.Id, targetId, StringComparison.Ordinal)
|| obstacle.Right <= minX
|| obstacle.Left >= maxX
|| obstacle.Bottom <= corridorTop
|| obstacle.Top >= corridorBottom)
{
continue;
}
AddUniqueCoordinate(bypassYCandidates, obstacle.Top);
AddUniqueCoordinate(bypassYCandidates, obstacle.Bottom);
if (IsUsableForwardBridgeAxis(start.X, end.X, obstacle.Left))
{
AddUniqueCoordinate(cornerBridgeXCandidates, obstacle.Left);
}
if (IsUsableForwardBridgeAxis(start.X, end.X, obstacle.Right))
{
AddUniqueCoordinate(cornerBridgeXCandidates, obstacle.Right);
}
}
foreach (var bypassY in bypassYCandidates)
{
foreach (var sourceBridgeAxis in sourceBridgeAxes)
{
ConsiderCandidate(
[
start,
new ElkPoint { X = sourceBridgeAxis, Y = start.Y },
new ElkPoint { X = sourceBridgeAxis, Y = bypassY },
end,
],
obstacles);
if (preservedRectTargetApproachAxis is double rectTargetApproachX
&& Math.Abs(bypassY - end.Y) > 0.5d)
{
ConsiderCandidate(
[
start,
new ElkPoint { X = sourceBridgeAxis, Y = start.Y },
new ElkPoint { X = sourceBridgeAxis, Y = bypassY },
new ElkPoint { X = rectTargetApproachX, Y = bypassY },
end,
],
obstacles);
}
foreach (var cornerBridgeX in cornerBridgeXCandidates)
{
if (!IsUsableForwardBridgeAxis(sourceBridgeAxis, end.X, cornerBridgeX))
{
continue;
}
ConsiderCandidate(
[
start,
new ElkPoint { X = sourceBridgeAxis, Y = start.Y },
new ElkPoint { X = sourceBridgeAxis, Y = bypassY },
new ElkPoint { X = cornerBridgeX, Y = bypassY },
end,
],
obstacles);
}
if (targetNode is not null
&& ElkShapeBoundaries.IsGatewayShape(targetNode)
&& Math.Abs(targetBridgeAxis - end.X) > 0.5d)
{
ConsiderCandidate(
[
start,
new ElkPoint { X = sourceBridgeAxis, Y = start.Y },
new ElkPoint { X = sourceBridgeAxis, Y = bypassY },
new ElkPoint { X = targetBridgeAxis, Y = bypassY },
end,
],
obstacles);
}
if (preservedGatewayTargetApproach is not null
&& !ElkEdgeRoutingGeometry.PointsEqual(
new ElkPoint { X = preservedGatewayTargetApproach.X, Y = bypassY },
preservedGatewayTargetApproach))
{
foreach (var cornerBridgeX in cornerBridgeXCandidates)
{
if (!IsUsableForwardBridgeAxis(sourceBridgeAxis, preservedGatewayTargetApproach.X, cornerBridgeX))
{
continue;
}
ConsiderCandidate(
[
start,
new ElkPoint { X = sourceBridgeAxis, Y = start.Y },
new ElkPoint { X = sourceBridgeAxis, Y = bypassY },
new ElkPoint { X = cornerBridgeX, Y = bypassY },
preservedGatewayTargetApproach,
end,
],
obstacles);
}
ConsiderCandidate(
[
start,
new ElkPoint { X = sourceBridgeAxis, Y = start.Y },
new ElkPoint { X = sourceBridgeAxis, Y = bypassY },
new ElkPoint { X = preservedGatewayTargetApproach.X, Y = bypassY },
preservedGatewayTargetApproach,
end,
],
obstacles);
}
}
}
}
}
else
{
foreach (var clearance in candidateClearances)
{
var obstacles = ExpandObstacles(rawObstacles, clearance);
var minY = Math.Min(start.Y, end.Y) + 0.5d;
var maxY = Math.Max(start.Y, end.Y) - 0.5d;
var corridorLeft = Math.Min(start.X, end.X) - clearance;
var corridorRight = Math.Max(start.X, end.X) + clearance;
var bypassXCandidates = new List<double> { start.X, end.X };
foreach (var point in currentPath.Skip(1).Take(Math.Max(0, currentPath.Count - 2)))
{
AddUniqueCoordinate(bypassXCandidates, point.X);
}
foreach (var obstacle in obstacles)
{
if (string.Equals(obstacle.Id, sourceId, StringComparison.Ordinal)
|| string.Equals(obstacle.Id, targetId, StringComparison.Ordinal)
|| obstacle.Bottom <= minY
|| obstacle.Top >= maxY
|| obstacle.Right <= corridorLeft
|| obstacle.Left >= corridorRight)
{
continue;
}
AddUniqueCoordinate(bypassXCandidates, obstacle.Left);
AddUniqueCoordinate(bypassXCandidates, obstacle.Right);
}
foreach (var bypassX in bypassXCandidates)
{
foreach (var sourceBridgeAxis in sourceBridgeAxes)
{
ConsiderCandidate(
[
start,
new ElkPoint { X = start.X, Y = sourceBridgeAxis },
new ElkPoint { X = bypassX, Y = sourceBridgeAxis },
end,
],
obstacles);
if (preservedRectTargetApproachAxis is double rectTargetApproachY
&& Math.Abs(bypassX - end.X) > 0.5d)
{
ConsiderCandidate(
[
start,
new ElkPoint { X = start.X, Y = sourceBridgeAxis },
new ElkPoint { X = bypassX, Y = sourceBridgeAxis },
new ElkPoint { X = bypassX, Y = rectTargetApproachY },
end,
],
obstacles);
}
if (targetNode is not null
&& ElkShapeBoundaries.IsGatewayShape(targetNode)
&& Math.Abs(targetBridgeAxis - end.Y) > 0.5d)
{
ConsiderCandidate(
[
start,
new ElkPoint { X = start.X, Y = sourceBridgeAxis },
new ElkPoint { X = bypassX, Y = sourceBridgeAxis },
new ElkPoint { X = bypassX, Y = targetBridgeAxis },
end,
],
obstacles);
}
if (preservedGatewayTargetApproach is not null
&& !ElkEdgeRoutingGeometry.PointsEqual(
new ElkPoint { X = bypassX, Y = preservedGatewayTargetApproach.Y },
preservedGatewayTargetApproach))
{
ConsiderCandidate(
[
start,
new ElkPoint { X = start.X, Y = sourceBridgeAxis },
new ElkPoint { X = bypassX, Y = sourceBridgeAxis },
new ElkPoint { X = bypassX, Y = preservedGatewayTargetApproach.Y },
preservedGatewayTargetApproach,
end,
],
obstacles);
}
}
}
}
}
return bestPath;
}
private static double ResolveMinLineClearance(IReadOnlyCollection<ElkPositionedNode> 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<ElkPoint> 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));
}
}