1407 lines
51 KiB
C#
1407 lines
51 KiB
C#
namespace StellaOps.ElkSharp;
|
|
|
|
internal static partial 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))
|
|
{
|
|
if (!(ElkShapeBoundaries.IsGatewayShape(sourceNode)
|
|
&& ShouldPreserveSaturatedGatewaySourceFace(edge, edges, sourceNode, normalized)))
|
|
{
|
|
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;
|
|
}
|
|
}
|
|
|
|
if (ElkShapeBoundaries.IsGatewayShape(sourceNode)
|
|
&& ShouldPreserveSaturatedGatewaySourceFace(edge, edges, sourceNode, path))
|
|
{
|
|
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))
|
|
{
|
|
normalized = EnforceGatewaySourceExitQuality(
|
|
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();
|
|
var changedEdgeIds = new List<string>();
|
|
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])
|
|
|| HasShortGatewayTargetOrthogonalHook(repaired, targetNode))
|
|
&& targetNode.Kind == "Decision")
|
|
{
|
|
repaired = ForceDecisionExteriorTargetEntry(path, targetNode);
|
|
}
|
|
|
|
if ((!PathChanged(path, repaired)
|
|
|| !CanAcceptGatewayTargetRepair(repaired, targetNode)
|
|
|| HasShortGatewayTargetOrthogonalHook(repaired, targetNode))
|
|
&& targetNode.Kind == "Decision")
|
|
{
|
|
repaired = ForceDecisionDirectTargetEntry(path, targetNode);
|
|
}
|
|
|
|
if (!PathChanged(path, repaired)
|
|
|| !CanAcceptGatewayTargetRepair(repaired, targetNode)
|
|
|| HasShortGatewayTargetOrthogonalHook(repaired, targetNode))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
result[i] = BuildSingleSectionEdge(edge, repaired);
|
|
changedEdgeIds.Add(edge.Id);
|
|
}
|
|
|
|
if (changedEdgeIds.Count == 0)
|
|
{
|
|
return result;
|
|
}
|
|
|
|
var focusedEdgeIds = changedEdgeIds
|
|
.Distinct(StringComparer.Ordinal)
|
|
.OrderBy(edgeId => edgeId, StringComparer.Ordinal)
|
|
.ToArray();
|
|
var minLineClearance = ResolveMinLineClearance(nodes);
|
|
result = SpreadSourceDepartureJoins(result, nodes, minLineClearance, focusedEdgeIds);
|
|
result = SeparateMixedNodeFaceLaneConflicts(result, nodes, minLineClearance, focusedEdgeIds);
|
|
result = SeparateSharedLaneConflicts(result, nodes, minLineClearance, focusedEdgeIds);
|
|
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)
|
|
&& ElkEdgeRoutingScoring.CountUnderNodeViolations([edge], nodes) > 0)
|
|
{
|
|
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;
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
} |