elksharp stabilization
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user