elksharp stabilization

This commit is contained in:
master
2026-03-24 08:38:09 +02:00
parent d788ee757e
commit 71edccd485
18 changed files with 6083 additions and 36 deletions

View File

@@ -170,6 +170,7 @@ internal static class ElkEdgePostProcessor
{
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++)
@@ -177,9 +178,12 @@ internal static class ElkEdgePostProcessor
var edge = edges[i];
var anyFixed = false;
var newSections = new List<ElkEdgeSection>();
var sectionList = edge.Sections.ToList();
foreach (var section in edge.Sections)
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);
@@ -205,6 +209,26 @@ internal static class ElkEdgePostProcessor
{
// 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 });
@@ -229,6 +253,275 @@ internal static class ElkEdgePostProcessor
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 = ElkEdgePostProcessor.IsRepeatCollectorLabel(edge.Label)
|| ElkEdgePostProcessor.HasCorridorBendPoints(edge, graphMinY, graphMaxY);
if (!preserveSourceExit
&& nodesById.TryGetValue(edge.SourceNodeId ?? string.Empty, out var sourceNode))
{
var sourceSide = ElkEdgeRoutingGeometry.ResolveBoundarySide(normalized[0], sourceNode);
var 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))
{
var targetSide = ElkEdgeRoutingGeometry.ResolveBoundarySide(normalized[^1], targetNode);
normalized = NormalizeEntryPath(normalized, targetNode, targetSide);
}
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 = ElkEdgePostProcessor.IsRepeatCollectorLabel(edge.Label)
|| ElkEdgePostProcessor.HasCorridorBendPoints(edge, graphMinY, graphMaxY);
if (preserveSourceExit
|| !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;
}
var sourceSide = ElkEdgeRoutingGeometry.ResolveBoundarySide(path[0], sourceNode);
var normalized = NormalizeExitPath(path, sourceNode, sourceSide);
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[] 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)
&& !HasValidBoundaryAngle(normalized[0], normalized[1], sourceNode))
{
var sourceSide = ElkEdgeRoutingGeometry.ResolveBoundarySide(normalized[0], sourceNode);
var sourceNormalized = NormalizeExitPath(normalized, 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];
var targetSide = ElkEdgeRoutingGeometry.ResolveBoundarySide(assignedEndpoint, targetNode);
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 (shortenedApproach.Count != normalized.Count
|| !shortenedApproach.Zip(normalized, ElkEdgeRoutingGeometry.PointsEqual).All(equal => equal))
{
if (HasClearBoundarySegments(shortenedApproach, nodes, edge.SourceNodeId, edge.TargetNodeId, false, 3))
{
normalized = shortenedApproach;
}
}
}
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 bool IsRepeatCollectorLabel(string? label)
{
if (string.IsNullOrWhiteSpace(label))
@@ -268,10 +561,8 @@ internal static class ElkEdgePostProcessor
(double Left, double Top, double Right, double Bottom, string Id)[] obstacles,
string sourceId, string targetId)
{
var segLen = Math.Sqrt(Math.Pow(p1.X - p2.X, 2) + Math.Pow(p1.Y - p2.Y, 2));
var isH = Math.Abs(p1.Y - p2.Y) < 2d;
var isV = Math.Abs(p1.X - p2.X) < 2d;
if (!isH && !isV) return segLen > 15d;
foreach (var ob in obstacles)
{
@@ -288,8 +579,555 @@ internal static class ElkEdgePostProcessor
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 rebuilt = new List<ElkPoint>
{
new() { X = sourceX, Y = path[0].Y },
};
var anchor = path[1];
var stubX = side == "left"
? sourceX - 24d
: sourceX + 24d;
if (Math.Abs(stubX - sourceX) > coordinateTolerance)
{
rebuilt.Add(new ElkPoint
{
X = stubX,
Y = path[0].Y,
});
}
if (Math.Abs(anchor.Y - path[0].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 verticalRebuilt = new List<ElkPoint>
{
new() { X = path[0].X, Y = sourceY },
};
var verticalAnchor = path[1];
var stubY = side == "top"
? sourceY - 24d
: sourceY + 24d;
if (Math.Abs(stubY - sourceY) > coordinateTolerance)
{
verticalRebuilt.Add(new ElkPoint
{
X = path[0].X,
Y = stubY,
});
}
if (Math.Abs(verticalAnchor.X - path[0].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 (side is "left" or "right")
{
var targetX = side == "left"
? targetNode.X
: targetNode.X + targetNode.Width;
var endpoint = explicitEndpoint ?? new ElkPoint { X = targetX, Y = path[^1].Y };
while (path.Count >= 3 && Math.Abs(path[^2].X - targetX) <= coordinateTolerance)
{
path.RemoveAt(path.Count - 2);
}
var anchor = path[^2];
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;
var verticalEndpoint = explicitEndpoint ?? new ElkPoint { X = path[^1].X, Y = targetY };
while (path.Count >= 3 && Math.Abs(path[^2].Y - targetY) <= coordinateTolerance)
{
path.RemoveAt(path.Count - 2);
}
var verticalAnchor = path[^2];
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 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 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 (restrictedEdgeIds is not null && !restrictedEdgeIds.Contains(edge.Id))
{
continue;
}
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 = ElkEdgeRoutingGeometry.ResolveBoundarySide(endpoint, 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;
}
var sideLength = side is "left" or "right"
? Math.Max(8d, targetNode.Height - 8d)
: Math.Max(8d, targetNode.Width - 8d);
var slotSpacing = group.Count > 1
? Math.Max(12d, Math.Min(minLineClearance, sideLength / (group.Count - 1)))
: 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));
result[sorted[i].EdgeId] = 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 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));
result[sorted[i].EdgeId] = new ElkPoint
{
X = slotX,
Y = side == "top" ? targetNode.Y : targetNode.Y + targetNode.Height,
};
}
}
}
return result;
}
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 (HasCorridorBendPoints(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)
{
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 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 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 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;
}
}