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

13754 lines
516 KiB
C#

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