13754 lines
516 KiB
C#
13754 lines
516 KiB
C#
namespace StellaOps.ElkSharp;
|
|
|
|
internal static class ElkEdgePostProcessor
|
|
{
|
|
private static readonly object UnderNodeDebugSync = new();
|
|
private const string ProtectedUnderNodeKindMarker = "protected-undernode";
|
|
|
|
internal static ElkRoutedEdge[] SnapAnchorsToNodeBoundary(
|
|
ElkRoutedEdge[] edges,
|
|
ElkPositionedNode[] nodes)
|
|
{
|
|
var nodesById = nodes.ToDictionary(n => n.Id, StringComparer.Ordinal);
|
|
var result = new ElkRoutedEdge[edges.Length];
|
|
for (var i = 0; i < edges.Length; i++)
|
|
{
|
|
var edge = edges[i];
|
|
var anyChanged = false;
|
|
var newSections = edge.Sections.ToList();
|
|
|
|
for (var s = 0; s < newSections.Count; s++)
|
|
{
|
|
var section = newSections[s];
|
|
var startFixed = false;
|
|
var endFixed = false;
|
|
var newStart = section.StartPoint;
|
|
var newEnd = section.EndPoint;
|
|
|
|
if (edge.SourceNodeId is not null && nodesById.TryGetValue(edge.SourceNodeId, out var srcNode) && s == 0)
|
|
{
|
|
if (newStart.X > srcNode.X + 1d && newStart.X < srcNode.X + srcNode.Width - 1d
|
|
&& newStart.Y > srcNode.Y + 1d && newStart.Y < srcNode.Y + srcNode.Height - 1d)
|
|
{
|
|
var target = section.BendPoints.Count > 0 ? section.BendPoints.First() : section.EndPoint;
|
|
newStart = ElkShapeBoundaries.ProjectOntoShapeBoundary(srcNode, target);
|
|
startFixed = true;
|
|
}
|
|
}
|
|
|
|
if (edge.TargetNodeId is not null && nodesById.TryGetValue(edge.TargetNodeId, out var tgtNode) && s == newSections.Count - 1)
|
|
{
|
|
var source = section.BendPoints.Count > 0 ? section.BendPoints.Last() : section.StartPoint;
|
|
var projected = ElkShapeBoundaries.ProjectOntoShapeBoundary(tgtNode, source);
|
|
if (Math.Abs(projected.X - newEnd.X) > 3d || Math.Abs(projected.Y - newEnd.Y) > 3d)
|
|
{
|
|
newEnd = projected;
|
|
endFixed = true;
|
|
}
|
|
}
|
|
|
|
if (startFixed || endFixed)
|
|
{
|
|
anyChanged = true;
|
|
newSections[s] = new ElkEdgeSection
|
|
{
|
|
StartPoint = newStart,
|
|
EndPoint = newEnd,
|
|
BendPoints = section.BendPoints,
|
|
};
|
|
}
|
|
}
|
|
|
|
result[i] = anyChanged
|
|
? new ElkRoutedEdge { Id = edge.Id, SourceNodeId = edge.SourceNodeId, TargetNodeId = edge.TargetNodeId, Label = edge.Label, Sections = newSections }
|
|
: edge;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
internal static ElkRoutedEdge[] ClearInternalRoutingMarkers(ElkRoutedEdge[] edges)
|
|
{
|
|
if (edges.Length == 0)
|
|
{
|
|
return edges;
|
|
}
|
|
|
|
var changed = false;
|
|
var result = new ElkRoutedEdge[edges.Length];
|
|
for (var i = 0; i < edges.Length; i++)
|
|
{
|
|
var edge = edges[i];
|
|
var cleanedKind = RemoveInternalKindMarker(edge.Kind, ProtectedUnderNodeKindMarker);
|
|
if (string.Equals(cleanedKind, edge.Kind, StringComparison.Ordinal))
|
|
{
|
|
result[i] = edge;
|
|
continue;
|
|
}
|
|
|
|
changed = true;
|
|
result[i] = CloneEdgeWithKind(edge, cleanedKind);
|
|
}
|
|
|
|
return changed ? result : edges;
|
|
}
|
|
|
|
internal static ElkRoutedEdge[] AvoidNodeCrossings(
|
|
ElkRoutedEdge[] edges,
|
|
ElkPositionedNode[] nodes,
|
|
ElkLayoutDirection direction,
|
|
IReadOnlyCollection<string>? restrictedEdgeIds = null)
|
|
{
|
|
if (direction != ElkLayoutDirection.LeftToRight || nodes.Length == 0)
|
|
{
|
|
return edges;
|
|
}
|
|
|
|
const double margin = 18d;
|
|
var obstacles = nodes.Select(n => (
|
|
Left: n.X - margin, Top: n.Y - margin,
|
|
Right: n.X + n.Width + margin, Bottom: n.Y + n.Height + margin,
|
|
Id: n.Id
|
|
)).ToArray();
|
|
var graphMinY = nodes.Min(n => n.Y);
|
|
var graphMaxY = nodes.Max(n => n.Y + n.Height);
|
|
var restrictedSet = restrictedEdgeIds is null
|
|
? null
|
|
: restrictedEdgeIds.ToHashSet(StringComparer.Ordinal);
|
|
|
|
var result = new ElkRoutedEdge[edges.Length];
|
|
for (var edgeIndex = 0; edgeIndex < edges.Length; edgeIndex++)
|
|
{
|
|
var edge = edges[edgeIndex];
|
|
if (restrictedSet is not null && !restrictedSet.Contains(edge.Id))
|
|
{
|
|
result[edgeIndex] = edge;
|
|
continue;
|
|
}
|
|
|
|
var sourceId = edge.SourceNodeId ?? "";
|
|
var targetId = edge.TargetNodeId ?? "";
|
|
|
|
var hasCorridorPts = HasCorridorBendPoints(edge, graphMinY, graphMaxY);
|
|
var hasCrossing = false;
|
|
foreach (var section in edge.Sections)
|
|
{
|
|
var pts = new List<ElkPoint> { section.StartPoint };
|
|
pts.AddRange(section.BendPoints);
|
|
pts.Add(section.EndPoint);
|
|
for (var i = 0; i < pts.Count - 1 && !hasCrossing; i++)
|
|
{
|
|
if (hasCorridorPts && IsCorridorSegment(pts[i], pts[i + 1], graphMinY, graphMaxY))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
hasCrossing = SegmentCrossesObstacle(pts[i], pts[i + 1], obstacles, sourceId, targetId);
|
|
}
|
|
}
|
|
|
|
if (!hasCrossing)
|
|
{
|
|
result[edgeIndex] = edge;
|
|
continue;
|
|
}
|
|
|
|
var hasCorridorPoints = hasCorridorPts;
|
|
var newSections = new List<ElkEdgeSection>(edge.Sections.Count);
|
|
foreach (var section in edge.Sections)
|
|
{
|
|
if (hasCorridorPoints)
|
|
{
|
|
var corridorRerouted = ElkEdgePostProcessorCorridor.ReroutePreservingCorridor(
|
|
section, obstacles, sourceId, targetId, margin, graphMinY, graphMaxY);
|
|
if (corridorRerouted is not null)
|
|
{
|
|
newSections.Add(corridorRerouted);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
var rerouted = ElkEdgePostProcessorAStar.RerouteWithGridAStar(
|
|
section.StartPoint, section.EndPoint,
|
|
obstacles, sourceId, targetId, margin);
|
|
|
|
if (rerouted is not null && rerouted.Count >= 2)
|
|
{
|
|
newSections.Add(new ElkEdgeSection
|
|
{
|
|
StartPoint = rerouted[0],
|
|
EndPoint = rerouted[^1],
|
|
BendPoints = rerouted.Skip(1).Take(rerouted.Count - 2).ToArray(),
|
|
});
|
|
}
|
|
else
|
|
{
|
|
newSections.Add(section);
|
|
}
|
|
}
|
|
|
|
result[edgeIndex] = new ElkRoutedEdge
|
|
{
|
|
Id = edge.Id,
|
|
SourceNodeId = edge.SourceNodeId,
|
|
TargetNodeId = edge.TargetNodeId,
|
|
Label = edge.Label,
|
|
Sections = newSections,
|
|
};
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
internal static ElkRoutedEdge[] EliminateDiagonalSegments(ElkRoutedEdge[] edges, ElkPositionedNode[] nodes)
|
|
{
|
|
var graphMinY = nodes.Length > 0 ? nodes.Min(n => n.Y) : 0d;
|
|
var graphMaxY = nodes.Length > 0 ? nodes.Max(n => n.Y + n.Height) : 0d;
|
|
var nodesById = nodes.ToDictionary(n => n.Id, StringComparer.Ordinal);
|
|
var obstacles = nodes.Select(n => (L: n.X - 4d, T: n.Y - 4d, R: n.X + n.Width + 4d, B: n.Y + n.Height + 4d, Id: n.Id)).ToArray();
|
|
var result = new ElkRoutedEdge[edges.Length];
|
|
for (var i = 0; i < edges.Length; i++)
|
|
{
|
|
var edge = edges[i];
|
|
var anyFixed = false;
|
|
var newSections = new List<ElkEdgeSection>();
|
|
var sectionList = edge.Sections.ToList();
|
|
|
|
for (var sIdx = 0; sIdx < sectionList.Count; sIdx++)
|
|
{
|
|
var section = sectionList[sIdx];
|
|
var isLastSection = sIdx == sectionList.Count - 1;
|
|
var pts = new List<ElkPoint> { section.StartPoint };
|
|
pts.AddRange(section.BendPoints);
|
|
pts.Add(section.EndPoint);
|
|
|
|
var fixedPts = new List<ElkPoint> { pts[0] };
|
|
for (var j = 1; j < pts.Count; j++)
|
|
{
|
|
var prev = fixedPts[^1];
|
|
var curr = pts[j];
|
|
var dx = Math.Abs(curr.X - prev.X);
|
|
var dy = Math.Abs(curr.Y - prev.Y);
|
|
if (dx > 3d && dy > 3d)
|
|
{
|
|
var prevIsCorridor = prev.Y < graphMinY - 8d || prev.Y > graphMaxY + 8d;
|
|
var currIsCorridor = curr.Y < graphMinY - 8d || curr.Y > graphMaxY + 8d;
|
|
var isBackwardSection = section.EndPoint.X < section.StartPoint.X - 1d;
|
|
if (prevIsCorridor)
|
|
{
|
|
fixedPts.Add(new ElkPoint { X = curr.X, Y = prev.Y });
|
|
anyFixed = true;
|
|
}
|
|
else if (currIsCorridor && isBackwardSection)
|
|
{
|
|
// Preserve diagonal for backward collector edges
|
|
}
|
|
else if (isLastSection && j == pts.Count - 1 && !isBackwardSection)
|
|
{
|
|
// Target approach: L-corner must be perpendicular to the entry side.
|
|
// Vertical side (left/right) → last segment horizontal (default).
|
|
// Horizontal side (top/bottom) → last segment vertical (flipped).
|
|
var targetNode = nodesById.GetValueOrDefault(edge.TargetNodeId ?? "");
|
|
var onHorizontalSide = targetNode is not null
|
|
&& (Math.Abs(curr.Y - targetNode.Y) < 2d
|
|
|| Math.Abs(curr.Y - (targetNode.Y + targetNode.Height)) < 2d);
|
|
if (onHorizontalSide)
|
|
{
|
|
fixedPts.Add(new ElkPoint { X = curr.X, Y = prev.Y });
|
|
}
|
|
else
|
|
{
|
|
fixedPts.Add(new ElkPoint { X = prev.X, Y = curr.Y });
|
|
}
|
|
|
|
anyFixed = true;
|
|
}
|
|
else
|
|
{
|
|
fixedPts.Add(new ElkPoint { X = prev.X, Y = curr.Y });
|
|
anyFixed = true;
|
|
}
|
|
}
|
|
fixedPts.Add(curr);
|
|
}
|
|
|
|
newSections.Add(new ElkEdgeSection
|
|
{
|
|
StartPoint = fixedPts[0],
|
|
EndPoint = fixedPts[^1],
|
|
BendPoints = fixedPts.Skip(1).Take(fixedPts.Count - 2).ToArray(),
|
|
});
|
|
}
|
|
|
|
result[i] = anyFixed
|
|
? new ElkRoutedEdge { Id = edge.Id, SourceNodeId = edge.SourceNodeId, TargetNodeId = edge.TargetNodeId, Label = edge.Label, Sections = newSections }
|
|
: edge;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
internal static ElkRoutedEdge[] NormalizeBoundaryAngles(
|
|
ElkRoutedEdge[] edges,
|
|
ElkPositionedNode[] nodes)
|
|
{
|
|
if (edges.Length == 0 || nodes.Length == 0)
|
|
{
|
|
return edges;
|
|
}
|
|
|
|
var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal);
|
|
var graphMinY = nodes.Min(node => node.Y);
|
|
var graphMaxY = nodes.Max(node => node.Y + node.Height);
|
|
var result = new ElkRoutedEdge[edges.Length];
|
|
for (var i = 0; i < edges.Length; i++)
|
|
{
|
|
var edge = edges[i];
|
|
var path = new List<ElkPoint>();
|
|
foreach (var section in edge.Sections)
|
|
{
|
|
if (path.Count == 0)
|
|
{
|
|
path.Add(section.StartPoint);
|
|
}
|
|
|
|
path.AddRange(section.BendPoints);
|
|
path.Add(section.EndPoint);
|
|
}
|
|
|
|
if (path.Count < 2)
|
|
{
|
|
result[i] = edge;
|
|
continue;
|
|
}
|
|
|
|
var normalized = path;
|
|
var preserveSourceExit = ShouldPreserveSourceExitGeometry(edge, graphMinY, graphMaxY);
|
|
if (!preserveSourceExit
|
|
&& nodesById.TryGetValue(edge.SourceNodeId ?? string.Empty, out var sourceNode))
|
|
{
|
|
List<ElkPoint> sourceNormalized;
|
|
if (ElkShapeBoundaries.IsGatewayShape(sourceNode))
|
|
{
|
|
sourceNormalized = NormalizeGatewayExitPath(normalized, sourceNode, nodes, edge.SourceNodeId, edge.TargetNodeId);
|
|
if (PathStartsAtDecisionVertex(sourceNormalized, sourceNode))
|
|
{
|
|
sourceNormalized = ForceDecisionSourceExitOffVertex(
|
|
sourceNormalized,
|
|
sourceNode,
|
|
nodes,
|
|
edge.SourceNodeId,
|
|
edge.TargetNodeId);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
var sourceSide = ElkEdgeRoutingGeometry.ResolveBoundarySide(normalized[0], sourceNode);
|
|
sourceNormalized = NormalizeExitPath(normalized, sourceNode, sourceSide);
|
|
}
|
|
|
|
if (HasClearSourceExitSegment(sourceNormalized, nodes, edge.SourceNodeId, edge.TargetNodeId))
|
|
{
|
|
normalized = sourceNormalized;
|
|
}
|
|
}
|
|
|
|
if (nodesById.TryGetValue(edge.TargetNodeId ?? string.Empty, out var targetNode))
|
|
{
|
|
if (ElkShapeBoundaries.IsGatewayShape(targetNode))
|
|
{
|
|
var gatewayNormalized = NormalizeGatewayEntryPath(normalized, targetNode, normalized[^1]);
|
|
if (HasAcceptableGatewayBoundaryPath(gatewayNormalized, nodes, edge.SourceNodeId, edge.TargetNodeId, targetNode, fromStart: false))
|
|
{
|
|
normalized = gatewayNormalized;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
var targetSide = ElkEdgeRoutingGeometry.ResolveBoundarySide(normalized[^1], targetNode);
|
|
ElkPoint? preferredEndpoint = null;
|
|
if (HasTargetApproachBacktracking(normalized, targetNode)
|
|
&& TryResolveNonGatewayBacktrackingEndpoint(normalized, targetNode, out var preferredSide, out var preferredBoundary))
|
|
{
|
|
targetSide = preferredSide;
|
|
preferredEndpoint = preferredBoundary;
|
|
}
|
|
|
|
normalized = NormalizeEntryPath(normalized, targetNode, targetSide, preferredEndpoint);
|
|
if (HasTargetApproachBacktracking(normalized, targetNode)
|
|
&& TryNormalizeNonGatewayBacktrackingEntry(normalized, targetNode, out var backtrackingRepair))
|
|
{
|
|
normalized = backtrackingRepair;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (normalized.Count == path.Count
|
|
&& normalized.Zip(path, ElkEdgeRoutingGeometry.PointsEqual).All(equal => equal))
|
|
{
|
|
result[i] = edge;
|
|
continue;
|
|
}
|
|
|
|
result[i] = new ElkRoutedEdge
|
|
{
|
|
Id = edge.Id,
|
|
SourceNodeId = edge.SourceNodeId,
|
|
TargetNodeId = edge.TargetNodeId,
|
|
SourcePortId = edge.SourcePortId,
|
|
TargetPortId = edge.TargetPortId,
|
|
Kind = edge.Kind,
|
|
Label = edge.Label,
|
|
Sections =
|
|
[
|
|
new ElkEdgeSection
|
|
{
|
|
StartPoint = normalized[0],
|
|
EndPoint = normalized[^1],
|
|
BendPoints = normalized.Count > 2
|
|
? normalized.Skip(1).Take(normalized.Count - 2).ToArray()
|
|
: [],
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
internal static ElkRoutedEdge[] NormalizeTargetEntryAngles(
|
|
ElkRoutedEdge[] edges,
|
|
ElkPositionedNode[] nodes)
|
|
{
|
|
return NormalizeBoundaryAngles(edges, nodes);
|
|
}
|
|
|
|
internal static ElkRoutedEdge[] NormalizeSourceExitAngles(
|
|
ElkRoutedEdge[] edges,
|
|
ElkPositionedNode[] nodes)
|
|
{
|
|
if (edges.Length == 0 || nodes.Length == 0)
|
|
{
|
|
return edges;
|
|
}
|
|
|
|
var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal);
|
|
var graphMinY = nodes.Min(node => node.Y);
|
|
var graphMaxY = nodes.Max(node => node.Y + node.Height);
|
|
var result = new ElkRoutedEdge[edges.Length];
|
|
for (var i = 0; i < edges.Length; i++)
|
|
{
|
|
var edge = edges[i];
|
|
var preserveSourceExit = ShouldPreserveSourceExitGeometry(edge, graphMinY, graphMaxY);
|
|
if (!nodesById.TryGetValue(edge.SourceNodeId ?? string.Empty, out var sourceNode))
|
|
{
|
|
result[i] = edge;
|
|
continue;
|
|
}
|
|
|
|
var path = new List<ElkPoint>();
|
|
foreach (var section in edge.Sections)
|
|
{
|
|
if (path.Count == 0)
|
|
{
|
|
path.Add(section.StartPoint);
|
|
}
|
|
|
|
path.AddRange(section.BendPoints);
|
|
path.Add(section.EndPoint);
|
|
}
|
|
|
|
if (path.Count < 2)
|
|
{
|
|
result[i] = edge;
|
|
continue;
|
|
}
|
|
|
|
if (preserveSourceExit)
|
|
{
|
|
var hasAcceptablePreservedExit = ElkShapeBoundaries.IsGatewayShape(sourceNode)
|
|
? HasAcceptableGatewayBoundaryPath(path, nodes, edge.SourceNodeId, edge.TargetNodeId, sourceNode, fromStart: true)
|
|
: HasValidBoundaryAngle(path[0], path[1], sourceNode);
|
|
if (hasAcceptablePreservedExit)
|
|
{
|
|
result[i] = edge;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
var sourceSide = ElkShapeBoundaries.IsGatewayShape(sourceNode)
|
|
? ElkEdgeRoutingGeometry.ResolveBoundarySide(path[0], sourceNode)
|
|
: ResolvePreferredRectSourceExitSide(path, sourceNode);
|
|
List<ElkPoint> normalized;
|
|
if (ElkShapeBoundaries.IsGatewayShape(sourceNode))
|
|
{
|
|
normalized = NormalizeGatewayExitPath(path, sourceNode, nodes, edge.SourceNodeId, edge.TargetNodeId);
|
|
}
|
|
else
|
|
{
|
|
var sourcePath = path
|
|
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
|
.ToList();
|
|
if (!HasValidBoundaryAngle(sourcePath[0], sourcePath[1], sourceNode))
|
|
{
|
|
sourcePath[0] = BuildRectBoundaryPointForSide(sourceNode, sourceSide, sourcePath[1]);
|
|
}
|
|
|
|
normalized = NormalizeExitPath(sourcePath, sourceNode, sourceSide);
|
|
}
|
|
if (ElkShapeBoundaries.IsGatewayShape(sourceNode)
|
|
&& PathStartsAtDecisionVertex(normalized, sourceNode))
|
|
{
|
|
normalized = ForceDecisionSourceExitOffVertex(
|
|
normalized,
|
|
sourceNode,
|
|
nodes,
|
|
edge.SourceNodeId,
|
|
edge.TargetNodeId);
|
|
}
|
|
if (ElkShapeBoundaries.IsGatewayShape(sourceNode))
|
|
{
|
|
if (!HasAcceptableGatewayBoundaryPath(normalized, nodes, edge.SourceNodeId, edge.TargetNodeId, sourceNode, fromStart: true))
|
|
{
|
|
result[i] = edge;
|
|
continue;
|
|
}
|
|
}
|
|
else if (!HasClearSourceExitSegment(normalized, nodes, edge.SourceNodeId, edge.TargetNodeId))
|
|
{
|
|
result[i] = edge;
|
|
continue;
|
|
}
|
|
|
|
if (normalized.Count == path.Count
|
|
&& normalized.Zip(path, ElkEdgeRoutingGeometry.PointsEqual).All(equal => equal))
|
|
{
|
|
result[i] = edge;
|
|
continue;
|
|
}
|
|
|
|
result[i] = new ElkRoutedEdge
|
|
{
|
|
Id = edge.Id,
|
|
SourceNodeId = edge.SourceNodeId,
|
|
TargetNodeId = edge.TargetNodeId,
|
|
SourcePortId = edge.SourcePortId,
|
|
TargetPortId = edge.TargetPortId,
|
|
Kind = edge.Kind,
|
|
Label = edge.Label,
|
|
Sections =
|
|
[
|
|
new ElkEdgeSection
|
|
{
|
|
StartPoint = normalized[0],
|
|
EndPoint = normalized[^1],
|
|
BendPoints = normalized.Count > 2
|
|
? normalized.Skip(1).Take(normalized.Count - 2).ToArray()
|
|
: [],
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
internal static ElkRoutedEdge[] FinalizeDecisionTargetEntries(
|
|
ElkRoutedEdge[] edges,
|
|
ElkPositionedNode[] nodes,
|
|
IReadOnlyCollection<string>? restrictedEdgeIds = null)
|
|
{
|
|
if (edges.Length == 0 || nodes.Length == 0)
|
|
{
|
|
return edges;
|
|
}
|
|
|
|
var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal);
|
|
var restrictedSet = restrictedEdgeIds is null
|
|
? null
|
|
: restrictedEdgeIds.ToHashSet(StringComparer.Ordinal);
|
|
var result = edges.ToArray();
|
|
for (var i = 0; i < result.Length; i++)
|
|
{
|
|
var edge = result[i];
|
|
if (restrictedSet is not null && !restrictedSet.Contains(edge.Id))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!nodesById.TryGetValue(edge.TargetNodeId ?? string.Empty, out var targetNode)
|
|
|| targetNode.Kind != "Decision")
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var path = ExtractFullPath(edge);
|
|
List<ElkPoint> repaired;
|
|
if (nodesById.TryGetValue(edge.SourceNodeId ?? string.Empty, out var sourceNode)
|
|
&& TryBuildPreferredGatewayTargetEntryPath(
|
|
path,
|
|
sourceNode,
|
|
targetNode,
|
|
nodes,
|
|
edge.SourceNodeId,
|
|
edge.TargetNodeId,
|
|
out var preferredGatewayTargetRepair))
|
|
{
|
|
repaired = preferredGatewayTargetRepair;
|
|
}
|
|
else
|
|
{
|
|
repaired = NormalizeGatewayEntryPath(path, targetNode, path[^1]);
|
|
}
|
|
|
|
if ((!CanAcceptGatewayTargetRepair(repaired, targetNode)
|
|
|| ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(targetNode, repaired[^2]))
|
|
&& targetNode.Kind == "Decision")
|
|
{
|
|
repaired = ForceDecisionExteriorTargetEntry(path, targetNode);
|
|
}
|
|
|
|
if ((!PathChanged(path, repaired) || !CanAcceptGatewayTargetRepair(repaired, targetNode))
|
|
&& targetNode.Kind == "Decision")
|
|
{
|
|
repaired = ForceDecisionDirectTargetEntry(path, targetNode);
|
|
}
|
|
|
|
if (!PathChanged(path, repaired)
|
|
|| !CanAcceptGatewayTargetRepair(repaired, targetNode))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
result[i] = BuildSingleSectionEdge(edge, repaired);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private static bool TryBuildPreferredGatewayTargetEntryPath(
|
|
IReadOnlyList<ElkPoint> path,
|
|
ElkPositionedNode sourceNode,
|
|
ElkPositionedNode targetNode,
|
|
IReadOnlyCollection<ElkPositionedNode> nodes,
|
|
string? sourceNodeId,
|
|
string? targetNodeId,
|
|
out List<ElkPoint> repaired)
|
|
{
|
|
repaired = [];
|
|
if (!ElkShapeBoundaries.IsGatewayShape(targetNode) || path.Count < 2)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var sourceCenterX = sourceNode.X + (sourceNode.Width / 2d);
|
|
var sourceCenterY = sourceNode.Y + (sourceNode.Height / 2d);
|
|
var targetCenterX = targetNode.X + (targetNode.Width / 2d);
|
|
var targetCenterY = targetNode.Y + (targetNode.Height / 2d);
|
|
var deltaX = targetCenterX - sourceCenterX;
|
|
var deltaY = targetCenterY - sourceCenterY;
|
|
var absDx = Math.Abs(deltaX);
|
|
var absDy = Math.Abs(deltaY);
|
|
var sameRowThreshold = Math.Max(40d, (sourceNode.Height + targetNode.Height) / 3d);
|
|
var sameColumnThreshold = Math.Max(40d, (sourceNode.Width + targetNode.Width) / 3d);
|
|
|
|
string? preferredSide = null;
|
|
double preferredSlotCoordinate = 0d;
|
|
if (((absDx >= absDy * 1.15d && absDy <= sameRowThreshold)
|
|
|| (absDx >= absDy * 1.75d))
|
|
&& Math.Sign(deltaX) != 0)
|
|
{
|
|
preferredSide = deltaX > 0d ? "left" : "right";
|
|
preferredSlotCoordinate = sourceCenterY;
|
|
}
|
|
else if (((absDy >= absDx * 1.15d && absDx <= sameColumnThreshold)
|
|
|| (absDy >= absDx * 1.75d))
|
|
&& Math.Sign(deltaY) != 0)
|
|
{
|
|
preferredSide = deltaY > 0d ? "top" : "bottom";
|
|
preferredSlotCoordinate = sourceCenterX;
|
|
}
|
|
|
|
if (preferredSide is null
|
|
|| !ElkShapeBoundaries.TryProjectGatewayBoundarySlot(targetNode, preferredSide, preferredSlotCoordinate, out var preferredBoundary))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var exteriorIndex = FindLastGatewayExteriorPointIndex(path, targetNode);
|
|
var exteriorAnchor = path[exteriorIndex];
|
|
preferredBoundary = ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(targetNode, preferredBoundary, exteriorAnchor);
|
|
|
|
var directPreferred = path.Take(exteriorIndex + 1)
|
|
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
|
.ToList();
|
|
if (directPreferred.Count == 0 || !ElkEdgeRoutingGeometry.PointsEqual(directPreferred[^1], exteriorAnchor))
|
|
{
|
|
directPreferred.Add(new ElkPoint { X = exteriorAnchor.X, Y = exteriorAnchor.Y });
|
|
}
|
|
|
|
directPreferred.Add(new ElkPoint { X = preferredBoundary.X, Y = preferredBoundary.Y });
|
|
var directNormalized = NormalizePathPoints(directPreferred);
|
|
if (directNormalized.Count >= 2
|
|
&& string.Equals(ElkEdgeRoutingGeometry.ResolveBoundarySide(directNormalized[^1], targetNode), preferredSide, StringComparison.Ordinal)
|
|
&& CanAcceptGatewayTargetRepair(directNormalized, targetNode)
|
|
&& HasAcceptableGatewayBoundaryPath(directNormalized, nodes, sourceNodeId, targetNodeId, targetNode, fromStart: false))
|
|
{
|
|
repaired = directNormalized;
|
|
return true;
|
|
}
|
|
|
|
var normalizedPreferred = NormalizeGatewayEntryPath(path, targetNode, preferredBoundary);
|
|
if (normalizedPreferred.Count < 2
|
|
|| !string.Equals(ElkEdgeRoutingGeometry.ResolveBoundarySide(normalizedPreferred[^1], targetNode), preferredSide, StringComparison.Ordinal)
|
|
|| !CanAcceptGatewayTargetRepair(normalizedPreferred, targetNode)
|
|
|| !HasAcceptableGatewayBoundaryPath(normalizedPreferred, nodes, sourceNodeId, targetNodeId, targetNode, fromStart: false))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
repaired = normalizedPreferred;
|
|
return true;
|
|
}
|
|
|
|
internal static ElkRoutedEdge[] PreferShortestBoundaryShortcuts(
|
|
ElkRoutedEdge[] edges,
|
|
ElkPositionedNode[] nodes,
|
|
IReadOnlyCollection<string>? restrictedEdgeIds = null)
|
|
{
|
|
if (edges.Length == 0 || nodes.Length == 0)
|
|
{
|
|
return edges;
|
|
}
|
|
|
|
var restrictedSet = restrictedEdgeIds is null
|
|
? null
|
|
: restrictedEdgeIds.ToHashSet(StringComparer.Ordinal);
|
|
var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal);
|
|
var graphMinY = nodes.Min(node => node.Y);
|
|
var graphMaxY = nodes.Max(node => node.Y + node.Height);
|
|
var minLineClearance = ResolveMinLineClearance(nodes);
|
|
var result = edges.ToArray();
|
|
|
|
for (var i = 0; i < result.Length; i++)
|
|
{
|
|
var edge = result[i];
|
|
if (restrictedSet is not null && !restrictedSet.Contains(edge.Id))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(edge.SourcePortId)
|
|
|| !string.IsNullOrWhiteSpace(edge.TargetPortId)
|
|
|| !nodesById.TryGetValue(edge.SourceNodeId ?? string.Empty, out var sourceNode)
|
|
|| !nodesById.TryGetValue(edge.TargetNodeId ?? string.Empty, out var targetNode))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(edge.Kind)
|
|
&& edge.Kind.StartsWith("backward|", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (HasProtectedUnderNodeGeometry(edge))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (IsRepeatCollectorLabel(edge.Label)
|
|
&& HasCorridorBendPoints(edge, graphMinY, graphMaxY))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var path = ExtractFullPath(edge);
|
|
if (path.Count < 2)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
List<ElkPoint>? bestShortcut = null;
|
|
var currentLength = ComputePathLength(path);
|
|
|
|
bool IsAcceptableShortcutCandidate(IReadOnlyList<ElkPoint> candidate)
|
|
{
|
|
if (candidate.Count < 2
|
|
|| HasNodeObstacleCrossing(candidate, nodes, edge.SourceNodeId, edge.TargetNodeId))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (ElkShapeBoundaries.IsGatewayShape(sourceNode))
|
|
{
|
|
if (!HasAcceptableGatewayBoundaryPath(
|
|
candidate,
|
|
nodes,
|
|
edge.SourceNodeId,
|
|
edge.TargetNodeId,
|
|
sourceNode,
|
|
fromStart: true))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
else if (!HasValidBoundaryAngle(candidate[0], candidate[1], sourceNode))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (ElkShapeBoundaries.IsGatewayShape(targetNode))
|
|
{
|
|
return CanAcceptGatewayTargetRepair(candidate, targetNode)
|
|
&& HasAcceptableGatewayBoundaryPath(
|
|
candidate,
|
|
nodes,
|
|
edge.SourceNodeId,
|
|
edge.TargetNodeId,
|
|
targetNode,
|
|
fromStart: false);
|
|
}
|
|
|
|
return !HasTargetApproachBacktracking(candidate, targetNode)
|
|
&& HasValidBoundaryAngle(candidate[^1], candidate[^2], targetNode);
|
|
}
|
|
|
|
void ConsiderShortcutCandidate(List<ElkPoint>? candidate)
|
|
{
|
|
if (candidate is null
|
|
|| ComputePathLength(candidate) + 16d >= currentLength
|
|
|| !IsAcceptableShortcutCandidate(candidate))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (bestShortcut is not null
|
|
&& ComputePathLength(candidate) + 0.5d >= ComputePathLength(bestShortcut))
|
|
{
|
|
return;
|
|
}
|
|
|
|
bestShortcut = candidate;
|
|
}
|
|
|
|
if (TryBuildDominantPreferredBoundaryShortcutPath(
|
|
sourceNode,
|
|
targetNode,
|
|
nodes,
|
|
edge.SourceNodeId,
|
|
edge.TargetNodeId,
|
|
out var dominantShortcut))
|
|
{
|
|
ConsiderShortcutCandidate(dominantShortcut);
|
|
}
|
|
|
|
if (TryBuildPreferredBoundaryShortcutPath(
|
|
sourceNode,
|
|
targetNode,
|
|
nodes,
|
|
edge.SourceNodeId,
|
|
edge.TargetNodeId,
|
|
out var preferredShortcut))
|
|
{
|
|
ConsiderShortcutCandidate(preferredShortcut);
|
|
}
|
|
|
|
var localSkirtShortcut = TryBuildLocalObstacleSkirtBoundaryShortcut(
|
|
path,
|
|
path[0],
|
|
path[^1],
|
|
nodes,
|
|
edge.SourceNodeId,
|
|
edge.TargetNodeId,
|
|
targetNode,
|
|
minLineClearance);
|
|
ConsiderShortcutCandidate(localSkirtShortcut);
|
|
|
|
if (bestShortcut is null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
result[i] = BuildSingleSectionEdge(edge, bestShortcut);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private static bool TryBuildDominantPreferredBoundaryShortcutPath(
|
|
ElkPositionedNode sourceNode,
|
|
ElkPositionedNode targetNode,
|
|
IReadOnlyCollection<ElkPositionedNode> nodes,
|
|
string? sourceNodeId,
|
|
string? targetNodeId,
|
|
out List<ElkPoint> shortcut)
|
|
{
|
|
shortcut = [];
|
|
var sourceCenterX = sourceNode.X + (sourceNode.Width / 2d);
|
|
var sourceCenterY = sourceNode.Y + (sourceNode.Height / 2d);
|
|
var targetCenterX = targetNode.X + (targetNode.Width / 2d);
|
|
var targetCenterY = targetNode.Y + (targetNode.Height / 2d);
|
|
var deltaX = targetCenterX - sourceCenterX;
|
|
var deltaY = targetCenterY - sourceCenterY;
|
|
var absDx = Math.Abs(deltaX);
|
|
var absDy = Math.Abs(deltaY);
|
|
if (absDx < 16d && absDy < 16d)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var horizontalDominant = absDx >= absDy;
|
|
var preferredSourceSide = horizontalDominant
|
|
? deltaX >= 0d ? "right" : "left"
|
|
: deltaY >= 0d ? "bottom" : "top";
|
|
var preferredTargetSide = horizontalDominant
|
|
? deltaX >= 0d ? "left" : "right"
|
|
: deltaY >= 0d ? "top" : "bottom";
|
|
|
|
return TryBuildPreferredBoundaryShortcutPath(
|
|
sourceNode,
|
|
targetNode,
|
|
preferredSourceSide,
|
|
preferredTargetSide,
|
|
nodes,
|
|
sourceNodeId,
|
|
targetNodeId,
|
|
out shortcut);
|
|
}
|
|
|
|
internal static bool TryBuildPreferredBoundaryShortcutPath(
|
|
ElkPositionedNode sourceNode,
|
|
ElkPositionedNode targetNode,
|
|
IReadOnlyCollection<ElkPositionedNode> nodes,
|
|
string? sourceNodeId,
|
|
string? targetNodeId,
|
|
out List<ElkPoint> shortcut)
|
|
{
|
|
shortcut = [];
|
|
var sourceCenterX = sourceNode.X + (sourceNode.Width / 2d);
|
|
var sourceCenterY = sourceNode.Y + (sourceNode.Height / 2d);
|
|
var targetCenterX = targetNode.X + (targetNode.Width / 2d);
|
|
var targetCenterY = targetNode.Y + (targetNode.Height / 2d);
|
|
var deltaX = targetCenterX - sourceCenterX;
|
|
var deltaY = targetCenterY - sourceCenterY;
|
|
var absDx = Math.Abs(deltaX);
|
|
var absDy = Math.Abs(deltaY);
|
|
if (absDx < 16d && absDy < 16d)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var horizontalDominant = absDx >= absDy;
|
|
var preferredSourceSide = horizontalDominant
|
|
? deltaX >= 0d ? "right" : "left"
|
|
: deltaY >= 0d ? "bottom" : "top";
|
|
var preferredTargetSide = horizontalDominant
|
|
? deltaX >= 0d ? "left" : "right"
|
|
: deltaY >= 0d ? "top" : "bottom";
|
|
|
|
if (!ElkShapeBoundaries.IsGatewayShape(sourceNode)
|
|
&& !ElkShapeBoundaries.IsGatewayShape(targetNode))
|
|
{
|
|
return TryBuildPreferredBoundaryShortcutPath(
|
|
sourceNode,
|
|
targetNode,
|
|
preferredSourceSide,
|
|
preferredTargetSide,
|
|
nodes,
|
|
sourceNodeId,
|
|
targetNodeId,
|
|
out shortcut);
|
|
}
|
|
|
|
var minLineClearance = ResolveMinLineClearance(nodes);
|
|
var bestScore = double.PositiveInfinity;
|
|
foreach (var candidateTargetSide in EnumeratePreferredShortcutTargetSides(preferredTargetSide))
|
|
{
|
|
if (!TryBuildPreferredBoundaryShortcutPath(
|
|
sourceNode,
|
|
targetNode,
|
|
preferredSourceSide,
|
|
candidateTargetSide,
|
|
nodes,
|
|
sourceNodeId,
|
|
targetNodeId,
|
|
out var candidate))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var score =
|
|
(CountUnderNodeSegments(candidate, nodes, sourceNodeId, targetNodeId, minLineClearance) * 100_000d)
|
|
+ ComputePathLength(candidate)
|
|
+ (Math.Max(0, candidate.Count - 2) * 4d);
|
|
if (!string.Equals(candidateTargetSide, preferredTargetSide, StringComparison.Ordinal))
|
|
{
|
|
score += 40d;
|
|
}
|
|
|
|
if (score >= bestScore)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
bestScore = score;
|
|
shortcut = candidate;
|
|
}
|
|
|
|
return shortcut.Count > 0;
|
|
}
|
|
|
|
internal static bool TryBuildPreferredBoundaryShortcutPath(
|
|
ElkPositionedNode sourceNode,
|
|
ElkPositionedNode targetNode,
|
|
string preferredSourceSide,
|
|
string preferredTargetSide,
|
|
IReadOnlyCollection<ElkPositionedNode> nodes,
|
|
string? sourceNodeId,
|
|
string? targetNodeId,
|
|
out List<ElkPoint> shortcut)
|
|
{
|
|
shortcut = [];
|
|
var start = BuildPreferredShortcutBoundaryPoint(sourceNode, preferredSourceSide, targetNode);
|
|
var end = BuildPreferredShortcutBoundaryPoint(targetNode, preferredTargetSide, sourceNode);
|
|
|
|
bool SegmentIsClear(ElkPoint from, ElkPoint to)
|
|
{
|
|
var obstacles = nodes.Select(node => (
|
|
Left: node.X,
|
|
Top: node.Y,
|
|
Right: node.X + node.Width,
|
|
Bottom: node.Y + node.Height,
|
|
Id: node.Id)).ToArray();
|
|
return !SegmentCrossesObstacle(from, to, obstacles, sourceNodeId, targetNodeId);
|
|
}
|
|
|
|
var prefix = new List<ElkPoint> { start };
|
|
var routeStart = start;
|
|
if (ElkShapeBoundaries.IsGatewayShape(sourceNode))
|
|
{
|
|
var sourceExterior = ElkShapeBoundaries.BuildPreferredGatewaySourceExteriorPoint(sourceNode, start, end);
|
|
if (ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(sourceNode, sourceExterior)
|
|
|| !ElkShapeBoundaries.HasValidGatewayBoundaryAngle(sourceNode, start, sourceExterior))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!SegmentIsClear(start, sourceExterior))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
prefix.Add(sourceExterior);
|
|
routeStart = sourceExterior;
|
|
}
|
|
|
|
var routeEnd = end;
|
|
if (ElkShapeBoundaries.IsGatewayShape(targetNode))
|
|
{
|
|
var targetExterior = ElkShapeBoundaries.BuildGatewayDirectionalExteriorPoint(targetNode, end, routeStart);
|
|
if (ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(targetNode, targetExterior)
|
|
|| !ElkShapeBoundaries.HasValidGatewayBoundaryAngle(targetNode, end, targetExterior))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
routeEnd = targetExterior;
|
|
}
|
|
|
|
List<ElkPoint>? bridge = null;
|
|
if (Math.Abs(routeStart.X - routeEnd.X) <= 0.5d || Math.Abs(routeStart.Y - routeEnd.Y) <= 0.5d)
|
|
{
|
|
if (SegmentIsClear(routeStart, routeEnd))
|
|
{
|
|
bridge =
|
|
[
|
|
routeStart,
|
|
routeEnd,
|
|
];
|
|
}
|
|
}
|
|
|
|
if (bridge is null)
|
|
{
|
|
var pivots = new[]
|
|
{
|
|
new ElkPoint { X = routeEnd.X, Y = routeStart.Y },
|
|
new ElkPoint { X = routeStart.X, Y = routeEnd.Y },
|
|
};
|
|
foreach (var pivot in pivots)
|
|
{
|
|
if (!SegmentIsClear(routeStart, pivot) || !SegmentIsClear(pivot, routeEnd))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
bridge =
|
|
[
|
|
routeStart,
|
|
pivot,
|
|
routeEnd,
|
|
];
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (bridge is null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var candidate = prefix
|
|
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
|
.ToList();
|
|
foreach (var point in bridge.Skip(1))
|
|
{
|
|
if (!ElkEdgeRoutingGeometry.PointsEqual(candidate[^1], point))
|
|
{
|
|
candidate.Add(new ElkPoint { X = point.X, Y = point.Y });
|
|
}
|
|
}
|
|
|
|
if (ElkShapeBoundaries.IsGatewayShape(targetNode)
|
|
&& !ElkEdgeRoutingGeometry.PointsEqual(candidate[^1], end))
|
|
{
|
|
candidate.Add(new ElkPoint { X = end.X, Y = end.Y });
|
|
}
|
|
|
|
shortcut = NormalizePathPoints(candidate);
|
|
if (shortcut.Count < 2)
|
|
{
|
|
shortcut = [];
|
|
return false;
|
|
}
|
|
|
|
if (ElkShapeBoundaries.IsGatewayShape(sourceNode))
|
|
{
|
|
if (!HasAcceptableGatewayBoundaryPath(shortcut, nodes, sourceNodeId, targetNodeId, sourceNode, fromStart: true))
|
|
{
|
|
shortcut = [];
|
|
return false;
|
|
}
|
|
}
|
|
else if (!HasValidBoundaryAngle(shortcut[0], shortcut[1], sourceNode))
|
|
{
|
|
shortcut = [];
|
|
return false;
|
|
}
|
|
|
|
if (ElkShapeBoundaries.IsGatewayShape(targetNode))
|
|
{
|
|
if (!CanAcceptGatewayTargetRepair(shortcut, targetNode)
|
|
|| !HasAcceptableGatewayBoundaryPath(shortcut, nodes, sourceNodeId, targetNodeId, targetNode, fromStart: false))
|
|
{
|
|
shortcut = [];
|
|
return false;
|
|
}
|
|
}
|
|
else if (HasTargetApproachBacktracking(shortcut, targetNode)
|
|
|| !HasValidBoundaryAngle(shortcut[^1], shortcut[^2], targetNode))
|
|
{
|
|
shortcut = [];
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private static ElkPoint BuildPreferredShortcutBoundaryPoint(
|
|
ElkPositionedNode node,
|
|
string side,
|
|
ElkPositionedNode otherNode)
|
|
{
|
|
var horizontalInset = Math.Min(24d, Math.Max(12d, node.Width / 4d));
|
|
var verticalInset = Math.Min(24d, Math.Max(12d, node.Height / 4d));
|
|
var otherCenterX = otherNode.X + (otherNode.Width / 2d);
|
|
var otherCenterY = otherNode.Y + (otherNode.Height / 2d);
|
|
var boundary = side switch
|
|
{
|
|
"left" => new ElkPoint
|
|
{
|
|
X = node.X,
|
|
Y = Math.Clamp(otherCenterY, node.Y + verticalInset, (node.Y + node.Height) - verticalInset),
|
|
},
|
|
"right" => new ElkPoint
|
|
{
|
|
X = node.X + node.Width,
|
|
Y = Math.Clamp(otherCenterY, node.Y + verticalInset, (node.Y + node.Height) - verticalInset),
|
|
},
|
|
"top" => new ElkPoint
|
|
{
|
|
X = Math.Clamp(otherCenterX, node.X + horizontalInset, (node.X + node.Width) - horizontalInset),
|
|
Y = node.Y,
|
|
},
|
|
_ => new ElkPoint
|
|
{
|
|
X = Math.Clamp(otherCenterX, node.X + horizontalInset, (node.X + node.Width) - horizontalInset),
|
|
Y = node.Y + node.Height,
|
|
},
|
|
};
|
|
|
|
if (!ElkShapeBoundaries.IsGatewayShape(node))
|
|
{
|
|
return boundary;
|
|
}
|
|
|
|
var referencePoint = side switch
|
|
{
|
|
"left" => new ElkPoint { X = node.X - Math.Max(24d, node.Width / 3d), Y = boundary.Y },
|
|
"right" => new ElkPoint { X = node.X + node.Width + Math.Max(24d, node.Width / 3d), Y = boundary.Y },
|
|
"top" => new ElkPoint { X = boundary.X, Y = node.Y - Math.Max(24d, node.Height / 3d) },
|
|
_ => new ElkPoint { X = boundary.X, Y = node.Y + node.Height + Math.Max(24d, node.Height / 3d) },
|
|
};
|
|
|
|
var projected = ElkShapeBoundaries.ProjectOntoShapeBoundary(node, referencePoint);
|
|
return ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(node, projected, referencePoint);
|
|
}
|
|
|
|
private static IEnumerable<string> EnumeratePreferredShortcutTargetSides(string preferredTargetSide)
|
|
{
|
|
var seen = new HashSet<string>(StringComparer.Ordinal);
|
|
|
|
bool Add(string side)
|
|
{
|
|
return side is "left" or "right" or "top" or "bottom"
|
|
&& seen.Add(side);
|
|
}
|
|
|
|
if (Add(preferredTargetSide))
|
|
{
|
|
yield return preferredTargetSide;
|
|
}
|
|
|
|
if (preferredTargetSide is "left" or "right")
|
|
{
|
|
if (Add("top"))
|
|
{
|
|
yield return "top";
|
|
}
|
|
|
|
if (Add("bottom"))
|
|
{
|
|
yield return "bottom";
|
|
}
|
|
|
|
var oppositeHorizontal = string.Equals(preferredTargetSide, "left", StringComparison.Ordinal)
|
|
? "right"
|
|
: "left";
|
|
if (Add(oppositeHorizontal))
|
|
{
|
|
yield return oppositeHorizontal;
|
|
}
|
|
|
|
yield break;
|
|
}
|
|
|
|
if (Add("left"))
|
|
{
|
|
yield return "left";
|
|
}
|
|
|
|
if (Add("right"))
|
|
{
|
|
yield return "right";
|
|
}
|
|
|
|
var oppositeVertical = string.Equals(preferredTargetSide, "top", StringComparison.Ordinal)
|
|
? "bottom"
|
|
: "top";
|
|
if (Add(oppositeVertical))
|
|
{
|
|
yield return oppositeVertical;
|
|
}
|
|
}
|
|
|
|
internal static ElkRoutedEdge[] RepairBoundaryAnglesAndTargetApproaches(
|
|
ElkRoutedEdge[] edges,
|
|
ElkPositionedNode[] nodes,
|
|
double minLineClearance,
|
|
IReadOnlyCollection<string>? restrictedEdgeIds = null)
|
|
{
|
|
if (edges.Length == 0 || nodes.Length == 0)
|
|
{
|
|
return edges;
|
|
}
|
|
|
|
var restrictedSet = restrictedEdgeIds is null
|
|
? null
|
|
: restrictedEdgeIds.ToHashSet(StringComparer.Ordinal);
|
|
var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal);
|
|
var graphMinY = nodes.Min(node => node.Y);
|
|
var graphMaxY = nodes.Max(node => node.Y + node.Height);
|
|
var targetSlots = ResolveTargetApproachSlots(edges, nodesById, graphMinY, graphMaxY, minLineClearance, restrictedSet);
|
|
var result = new ElkRoutedEdge[edges.Length];
|
|
|
|
for (var i = 0; i < edges.Length; i++)
|
|
{
|
|
var edge = edges[i];
|
|
if (restrictedSet is not null && !restrictedSet.Contains(edge.Id))
|
|
{
|
|
result[i] = edge;
|
|
continue;
|
|
}
|
|
|
|
var path = ExtractFullPath(edge);
|
|
if (path.Count < 2)
|
|
{
|
|
result[i] = edge;
|
|
continue;
|
|
}
|
|
|
|
var normalized = path
|
|
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
|
.ToList();
|
|
|
|
if (nodesById.TryGetValue(edge.SourceNodeId ?? string.Empty, out var sourceNode)
|
|
&& ElkShapeBoundaries.IsGatewayShape(sourceNode))
|
|
{
|
|
var preserveSourceExit = ShouldPreserveSourceExitGeometry(edge, graphMinY, graphMaxY);
|
|
if (!preserveSourceExit)
|
|
{
|
|
var gatewaySourceNormalized = NormalizeGatewayExitPath(normalized, sourceNode, nodes, edge.SourceNodeId, edge.TargetNodeId);
|
|
if (PathStartsAtDecisionVertex(gatewaySourceNormalized, sourceNode))
|
|
{
|
|
gatewaySourceNormalized = ForceDecisionSourceExitOffVertex(
|
|
gatewaySourceNormalized,
|
|
sourceNode,
|
|
nodes,
|
|
edge.SourceNodeId,
|
|
edge.TargetNodeId);
|
|
}
|
|
if (PathChanged(normalized, gatewaySourceNormalized)
|
|
&& HasAcceptableGatewayBoundaryPath(gatewaySourceNormalized, nodes, edge.SourceNodeId, edge.TargetNodeId, sourceNode, fromStart: true))
|
|
{
|
|
normalized = gatewaySourceNormalized;
|
|
}
|
|
}
|
|
}
|
|
else if (nodesById.TryGetValue(edge.SourceNodeId ?? string.Empty, out sourceNode)
|
|
&& !HasValidBoundaryAngle(normalized[0], normalized[1], sourceNode))
|
|
{
|
|
var sourceSide = ResolvePreferredRectSourceExitSide(normalized, sourceNode);
|
|
var sourcePath = normalized
|
|
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
|
.ToList();
|
|
sourcePath[0] = BuildRectBoundaryPointForSide(sourceNode, sourceSide, sourcePath[1]);
|
|
var sourceNormalized = NormalizeExitPath(sourcePath, sourceNode, sourceSide);
|
|
if (HasClearBoundarySegments(sourceNormalized, nodes, edge.SourceNodeId, edge.TargetNodeId, true, 3))
|
|
{
|
|
normalized = sourceNormalized;
|
|
}
|
|
}
|
|
|
|
if (nodesById.TryGetValue(edge.TargetNodeId ?? string.Empty, out var targetNode))
|
|
{
|
|
var assignedEndpoint = targetSlots.TryGetValue(edge.Id, out var slot)
|
|
? slot
|
|
: normalized[^1];
|
|
if (ElkShapeBoundaries.IsGatewayShape(targetNode))
|
|
{
|
|
List<ElkPoint>? preferredGatewayTargetNormalized = null;
|
|
if (nodesById.TryGetValue(edge.SourceNodeId ?? string.Empty, out var gatewaySourceNode)
|
|
&& TryBuildPreferredGatewayTargetEntryPath(
|
|
normalized,
|
|
gatewaySourceNode,
|
|
targetNode,
|
|
nodes,
|
|
edge.SourceNodeId,
|
|
edge.TargetNodeId,
|
|
out var preferredGatewayTargetRepair))
|
|
{
|
|
preferredGatewayTargetNormalized = preferredGatewayTargetRepair;
|
|
}
|
|
|
|
var gatewayTargetNormalized = NormalizeGatewayEntryPath(normalized, targetNode, assignedEndpoint);
|
|
if (gatewayTargetNormalized.Count >= 2
|
|
&& !ElkShapeBoundaries.HasValidGatewayBoundaryAngle(targetNode, gatewayTargetNormalized[^1], gatewayTargetNormalized[^2]))
|
|
{
|
|
var projectedBoundary = ElkShapeBoundaries.ProjectOntoShapeBoundary(targetNode, gatewayTargetNormalized[^2]);
|
|
projectedBoundary = ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(targetNode, projectedBoundary, gatewayTargetNormalized[^2]);
|
|
var projectedGatewayTargetNormalized = NormalizeGatewayEntryPath(normalized, targetNode, projectedBoundary);
|
|
if (PathChanged(gatewayTargetNormalized, projectedGatewayTargetNormalized)
|
|
&& HasAcceptableGatewayBoundaryPath(projectedGatewayTargetNormalized, nodes, edge.SourceNodeId, edge.TargetNodeId, targetNode, fromStart: false))
|
|
{
|
|
gatewayTargetNormalized = projectedGatewayTargetNormalized;
|
|
}
|
|
}
|
|
|
|
if (preferredGatewayTargetNormalized is not null
|
|
&& (gatewayTargetNormalized.Count < 2
|
|
|| NeedsGatewayTargetBoundaryRepair(gatewayTargetNormalized, targetNode)
|
|
|| !string.Equals(
|
|
ElkEdgeRoutingGeometry.ResolveBoundarySide(gatewayTargetNormalized[^1], targetNode),
|
|
ElkEdgeRoutingGeometry.ResolveBoundarySide(preferredGatewayTargetNormalized[^1], targetNode),
|
|
StringComparison.Ordinal)
|
|
|| ComputePathLength(preferredGatewayTargetNormalized) + 4d < ComputePathLength(gatewayTargetNormalized)
|
|
|| HasTargetApproachBacktracking(gatewayTargetNormalized, targetNode)))
|
|
{
|
|
gatewayTargetNormalized = preferredGatewayTargetNormalized;
|
|
}
|
|
|
|
if (PathChanged(normalized, gatewayTargetNormalized)
|
|
&& HasAcceptableGatewayBoundaryPath(gatewayTargetNormalized, nodes, edge.SourceNodeId, edge.TargetNodeId, targetNode, fromStart: false))
|
|
{
|
|
normalized = gatewayTargetNormalized;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (nodesById.TryGetValue(edge.SourceNodeId ?? string.Empty, out var shortcutSourceNode)
|
|
&& TryApplyPreferredBoundaryShortcut(
|
|
normalized,
|
|
shortcutSourceNode,
|
|
targetNode,
|
|
nodes,
|
|
edge.SourceNodeId,
|
|
edge.TargetNodeId,
|
|
requireUnderNodeImprovement: false,
|
|
minLineClearance,
|
|
out var preferredShortcut))
|
|
{
|
|
normalized = preferredShortcut;
|
|
}
|
|
|
|
var targetSide = ElkEdgeRoutingGeometry.ResolveBoundarySide(assignedEndpoint, targetNode);
|
|
if (IsOnWrongSideOfTarget(normalized[^2], targetNode, targetSide, 0.5d)
|
|
&& TryResolveNonGatewayBacktrackingEndpoint(normalized, targetNode, out var correctedSide, out var correctedBoundary))
|
|
{
|
|
targetSide = correctedSide;
|
|
assignedEndpoint = correctedBoundary;
|
|
}
|
|
|
|
if (HasTargetApproachBacktracking(normalized, targetNode)
|
|
&& TryResolveNonGatewayBacktrackingEndpoint(normalized, targetNode, out var preferredSide, out var preferredBoundary))
|
|
{
|
|
targetSide = preferredSide;
|
|
assignedEndpoint = preferredBoundary;
|
|
}
|
|
|
|
if (!HasValidBoundaryAngle(normalized[^1], normalized[^2], targetNode))
|
|
{
|
|
var preferredEntrySide = ResolvePreferredRectTargetEntrySide(normalized, targetNode);
|
|
if (!string.Equals(preferredEntrySide, targetSide, StringComparison.Ordinal))
|
|
{
|
|
targetSide = preferredEntrySide;
|
|
assignedEndpoint = BuildRectBoundaryPointForSide(targetNode, targetSide, normalized[^2]);
|
|
}
|
|
}
|
|
|
|
if (!ElkEdgeRoutingGeometry.PointsEqual(assignedEndpoint, normalized[^1])
|
|
|| !HasValidBoundaryAngle(normalized[^1], normalized[^2], targetNode))
|
|
{
|
|
var targetNormalized = NormalizeEntryPath(normalized, targetNode, targetSide, assignedEndpoint);
|
|
if (HasClearBoundarySegments(targetNormalized, nodes, edge.SourceNodeId, edge.TargetNodeId, false, 3))
|
|
{
|
|
normalized = targetNormalized;
|
|
}
|
|
}
|
|
|
|
var shortenedApproach = TrimTargetApproachBacktracking(normalized, targetNode, targetSide, assignedEndpoint);
|
|
if (PathChanged(normalized, shortenedApproach)
|
|
&& HasClearBoundarySegments(shortenedApproach, nodes, edge.SourceNodeId, edge.TargetNodeId, false, 3)
|
|
&& HasValidBoundaryAngle(shortenedApproach[^1], shortenedApproach[^2], targetNode))
|
|
{
|
|
normalized = shortenedApproach;
|
|
}
|
|
|
|
if (HasTargetApproachBacktracking(normalized, targetNode)
|
|
&& TryNormalizeNonGatewayBacktrackingEntry(normalized, targetNode, out var backtrackingRepair)
|
|
&& HasClearBoundarySegments(backtrackingRepair, nodes, edge.SourceNodeId, edge.TargetNodeId, false, 3)
|
|
&& HasValidBoundaryAngle(backtrackingRepair[^1], backtrackingRepair[^2], targetNode))
|
|
{
|
|
normalized = backtrackingRepair;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (normalized.Count == path.Count
|
|
&& normalized.Zip(path, ElkEdgeRoutingGeometry.PointsEqual).All(equal => equal))
|
|
{
|
|
result[i] = edge;
|
|
continue;
|
|
}
|
|
|
|
result[i] = BuildSingleSectionEdge(edge, normalized);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
internal static ElkRoutedEdge[] SpreadTargetApproachJoins(
|
|
ElkRoutedEdge[] edges,
|
|
ElkPositionedNode[] nodes,
|
|
double minLineClearance,
|
|
IReadOnlyCollection<string>? restrictedEdgeIds = null,
|
|
bool forceOutwardAxisSpacing = false)
|
|
{
|
|
if (edges.Length == 0 || nodes.Length == 0)
|
|
{
|
|
return edges;
|
|
}
|
|
|
|
var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal);
|
|
var graphMinY = nodes.Min(node => node.Y);
|
|
var graphMaxY = nodes.Max(node => node.Y + node.Height);
|
|
var restrictedSet = restrictedEdgeIds is null
|
|
? null
|
|
: restrictedEdgeIds.ToHashSet(StringComparer.Ordinal);
|
|
var result = edges.ToArray();
|
|
var groups = result
|
|
.Select((edge, index) => new
|
|
{
|
|
Edge = edge,
|
|
Index = index,
|
|
Path = ExtractFullPath(edge),
|
|
})
|
|
.Where(item => item.Path.Count >= 2
|
|
&& nodesById.TryGetValue(item.Edge.TargetNodeId ?? string.Empty, out _))
|
|
.GroupBy(
|
|
item =>
|
|
{
|
|
var targetNode = nodesById[item.Edge.TargetNodeId ?? string.Empty];
|
|
var side = ResolveTargetApproachSide(item.Path, targetNode);
|
|
return $"{targetNode.Id}|{side}";
|
|
},
|
|
StringComparer.Ordinal);
|
|
|
|
foreach (var group in groups)
|
|
{
|
|
var entries = group
|
|
.Select(item =>
|
|
{
|
|
var targetNode = nodesById[item.Edge.TargetNodeId ?? string.Empty];
|
|
var side = ResolveTargetApproachSide(item.Path, targetNode);
|
|
var endpoint = item.Path[^1];
|
|
return new
|
|
{
|
|
item.Edge,
|
|
item.Index,
|
|
item.Path,
|
|
TargetNode = targetNode,
|
|
Side = side,
|
|
Endpoint = endpoint,
|
|
};
|
|
})
|
|
.ToArray();
|
|
if (entries.Length < 2)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (restrictedSet is not null && !entries.Any(entry => restrictedSet.Contains(entry.Edge.Id)))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var joinEntries = entries
|
|
.Select(entry => (Path: (IReadOnlyList<ElkPoint>)entry.Path, Side: entry.Side))
|
|
.ToArray();
|
|
if (!GroupHasTargetApproachJoin(joinEntries, minLineClearance))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var targetNode = entries[0].TargetNode;
|
|
var side = entries[0].Side;
|
|
var isGatewayTarget = ElkShapeBoundaries.IsGatewayShape(targetNode);
|
|
var preserveBoundaryBandSlots = entries.Any(entry =>
|
|
HasProtectedUnderNodeGeometry(entry.Edge)
|
|
|| HasCorridorBendPoints(entry.Edge, graphMinY, graphMaxY));
|
|
var sorted = side is "left" or "right"
|
|
? entries.OrderBy(entry => entry.Endpoint.Y).ThenBy(entry => entry.Edge.Id, StringComparer.Ordinal).ToArray()
|
|
: entries.OrderBy(entry => entry.Endpoint.X).ThenBy(entry => entry.Edge.Id, StringComparer.Ordinal).ToArray();
|
|
var sideLength = side is "left" or "right"
|
|
? Math.Max(8d, targetNode.Height - 8d)
|
|
: Math.Max(8d, targetNode.Width - 8d);
|
|
var slotSpacing = sorted.Length > 1
|
|
? ResolveBoundaryJoinSlotSpacing(minLineClearance, sideLength, sorted.Length)
|
|
: 0d;
|
|
var totalSpan = (sorted.Length - 1) * slotSpacing;
|
|
var baseBoundaryCoordinate = side switch
|
|
{
|
|
"left" or "right" => entries.Min(entry => entry.Endpoint.Y),
|
|
"top" or "bottom" => entries.Min(entry => entry.Endpoint.X),
|
|
_ => double.NaN,
|
|
};
|
|
var currentApproachAxes = sorted
|
|
.Select(entry => ResolveSpreadableTargetApproachAxis(
|
|
entry.Path,
|
|
targetNode,
|
|
entry.Side,
|
|
minLineClearance))
|
|
.Where(axis => !double.IsNaN(axis))
|
|
.ToArray();
|
|
|
|
var baseApproachAxis = isGatewayTarget
|
|
? ResolveDefaultTargetApproachAxis(targetNode, side)
|
|
: currentApproachAxes.Length > 0
|
|
? forceOutwardAxisSpacing
|
|
? side switch
|
|
{
|
|
"left" or "top" => currentApproachAxes.Max(),
|
|
"right" or "bottom" => currentApproachAxes.Min(),
|
|
_ => ResolveDefaultTargetApproachAxis(targetNode, side),
|
|
}
|
|
: currentApproachAxes.Min()
|
|
: ResolveDefaultTargetApproachAxis(targetNode, side);
|
|
|
|
for (var i = 0; i < sorted.Length; i++)
|
|
{
|
|
var desiredBoundaryCoordinate = baseBoundaryCoordinate + (i * slotSpacing);
|
|
var desiredApproachAxis = ResolveDesiredTargetApproachAxis(
|
|
targetNode,
|
|
side,
|
|
baseApproachAxis,
|
|
slotSpacing,
|
|
i,
|
|
forceOutwardAxisSpacing);
|
|
|
|
if (isGatewayTarget)
|
|
{
|
|
ElkPoint slotPoint;
|
|
if (side is "left" or "right")
|
|
{
|
|
var centerY = targetNode.Y + (targetNode.Height / 2d);
|
|
var startY = Math.Max(targetNode.Y + 4d, centerY - (totalSpan / 2d));
|
|
var slotY = Math.Min(targetNode.Y + targetNode.Height - 4d, startY + (i * slotSpacing));
|
|
if (!ElkShapeBoundaries.TryProjectGatewayBoundarySlot(targetNode, side, slotY, out slotPoint))
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
var centerX = targetNode.X + (targetNode.Width / 2d);
|
|
var startX = Math.Max(targetNode.X + 4d, centerX - (totalSpan / 2d));
|
|
var slotX = Math.Min(targetNode.X + targetNode.Width - 4d, startX + (i * slotSpacing));
|
|
if (!ElkShapeBoundaries.TryProjectGatewayBoundarySlot(targetNode, side, slotX, out slotPoint))
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
|
|
var exteriorIndex = FindLastGatewayExteriorPointIndex(sorted[i].Path, targetNode);
|
|
var exteriorAnchor = sorted[i].Path[exteriorIndex];
|
|
var gatewayCandidate = TryBuildSlottedGatewayEntryPath(
|
|
sorted[i].Path,
|
|
targetNode,
|
|
exteriorIndex,
|
|
exteriorAnchor,
|
|
slotPoint)
|
|
?? NormalizeGatewayEntryPath(sorted[i].Path, targetNode, slotPoint);
|
|
var gatewayApproachAxis = double.IsNaN(desiredApproachAxis)
|
|
? ResolveTargetApproachAxisValue(gatewayCandidate, sorted[i].Side)
|
|
: desiredApproachAxis;
|
|
if (double.IsNaN(gatewayApproachAxis))
|
|
{
|
|
gatewayApproachAxis = ResolveDefaultTargetApproachAxis(targetNode, side);
|
|
}
|
|
|
|
var spreadGatewayCandidate = RewriteTargetApproachRun(
|
|
gatewayCandidate,
|
|
sorted[i].Side,
|
|
slotPoint,
|
|
gatewayApproachAxis);
|
|
if (PathChanged(gatewayCandidate, spreadGatewayCandidate)
|
|
&& CanAcceptGatewayTargetRepair(spreadGatewayCandidate, targetNode))
|
|
{
|
|
gatewayCandidate = spreadGatewayCandidate;
|
|
}
|
|
|
|
if (!PathChanged(sorted[i].Path, gatewayCandidate)
|
|
|| !CanAcceptGatewayTargetRepair(gatewayCandidate, targetNode))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
result[sorted[i].Index] = BuildSingleSectionEdge(sorted[i].Edge, gatewayCandidate);
|
|
continue;
|
|
}
|
|
|
|
ElkPoint desiredEndpoint;
|
|
if (side is "left" or "right")
|
|
{
|
|
var centerY = targetNode.Y + (targetNode.Height / 2d);
|
|
var startY = Math.Max(targetNode.Y + 4d, centerY - (totalSpan / 2d));
|
|
var slotY = preserveBoundaryBandSlots && !double.IsNaN(desiredBoundaryCoordinate)
|
|
? Math.Clamp(desiredBoundaryCoordinate, targetNode.Y + 4d, targetNode.Y + targetNode.Height - 4d)
|
|
: Math.Min(targetNode.Y + targetNode.Height - 4d, startY + (i * slotSpacing));
|
|
desiredEndpoint = new ElkPoint
|
|
{
|
|
X = side == "left" ? targetNode.X : targetNode.X + targetNode.Width,
|
|
Y = slotY,
|
|
};
|
|
}
|
|
else
|
|
{
|
|
var centerX = targetNode.X + (targetNode.Width / 2d);
|
|
var startX = Math.Max(targetNode.X + 4d, centerX - (totalSpan / 2d));
|
|
var slotX = preserveBoundaryBandSlots && !double.IsNaN(desiredBoundaryCoordinate)
|
|
? Math.Clamp(desiredBoundaryCoordinate, targetNode.X + 4d, targetNode.X + targetNode.Width - 4d)
|
|
: Math.Min(targetNode.X + targetNode.Width - 4d, startX + (i * slotSpacing));
|
|
desiredEndpoint = new ElkPoint
|
|
{
|
|
X = slotX,
|
|
Y = side == "top" ? targetNode.Y : targetNode.Y + targetNode.Height,
|
|
};
|
|
}
|
|
|
|
var currentRunAxis = ResolveTargetApproachAxisValue(sorted[i].Path, sorted[i].Side);
|
|
var preserveApproachBand = HasProtectedUnderNodeGeometry(sorted[i].Edge)
|
|
|| HasCorridorBendPoints(sorted[i].Edge, graphMinY, graphMaxY);
|
|
var desiredRunAxis = preserveApproachBand
|
|
? currentRunAxis
|
|
: double.IsNaN(desiredApproachAxis)
|
|
? currentRunAxis
|
|
: desiredApproachAxis;
|
|
if (double.IsNaN(desiredRunAxis))
|
|
{
|
|
desiredRunAxis = double.IsNaN(desiredApproachAxis)
|
|
? ResolveDefaultTargetApproachAxis(targetNode, side)
|
|
: desiredApproachAxis;
|
|
}
|
|
|
|
var candidate = RewriteTargetApproachRun(
|
|
sorted[i].Path,
|
|
sorted[i].Side,
|
|
desiredEndpoint,
|
|
desiredRunAxis);
|
|
if (!PathChanged(sorted[i].Path, candidate)
|
|
|| !HasClearBoundarySegments(candidate, nodes, sorted[i].Edge.SourceNodeId, sorted[i].Edge.TargetNodeId, false, 4))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
result[sorted[i].Index] = BuildSingleSectionEdge(sorted[i].Edge, candidate);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
internal static ElkRoutedEdge[] SpreadSourceDepartureJoins(
|
|
ElkRoutedEdge[] edges,
|
|
ElkPositionedNode[] nodes,
|
|
double minLineClearance,
|
|
IReadOnlyCollection<string>? restrictedEdgeIds = null)
|
|
{
|
|
if (edges.Length == 0 || nodes.Length == 0)
|
|
{
|
|
return edges;
|
|
}
|
|
|
|
var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal);
|
|
var graphMinY = nodes.Min(node => node.Y);
|
|
var graphMaxY = nodes.Max(node => node.Y + node.Height);
|
|
var restrictedSet = restrictedEdgeIds is null
|
|
? null
|
|
: restrictedEdgeIds.ToHashSet(StringComparer.Ordinal);
|
|
var result = edges.ToArray();
|
|
var groups = result
|
|
.Select((edge, index) => new
|
|
{
|
|
Edge = edge,
|
|
Index = index,
|
|
Path = ExtractFullPath(edge),
|
|
})
|
|
.Where(item => item.Path.Count >= 2
|
|
&& nodesById.TryGetValue(item.Edge.SourceNodeId ?? string.Empty, out _)
|
|
&& ShouldSpreadSourceDeparture(item.Edge, graphMinY, graphMaxY))
|
|
.GroupBy(
|
|
item =>
|
|
{
|
|
var sourceNode = nodesById[item.Edge.SourceNodeId ?? string.Empty];
|
|
var side = ResolveSourceDepartureSide(item.Path, sourceNode);
|
|
return $"{sourceNode.Id}|{side}";
|
|
},
|
|
StringComparer.Ordinal);
|
|
|
|
foreach (var group in groups)
|
|
{
|
|
var entries = group
|
|
.Select(item =>
|
|
{
|
|
var sourceNode = nodesById[item.Edge.SourceNodeId ?? string.Empty];
|
|
var side = ResolveSourceDepartureSide(item.Path, sourceNode);
|
|
return new
|
|
{
|
|
item.Edge,
|
|
item.Index,
|
|
item.Path,
|
|
SourceNode = sourceNode,
|
|
Side = side,
|
|
Boundary = item.Path[0],
|
|
TargetReference = side is "left" or "right"
|
|
? item.Path[^1].Y
|
|
: item.Path[^1].X,
|
|
PathLength = ComputePathLength(item.Path),
|
|
};
|
|
})
|
|
.ToArray();
|
|
if (entries.Length < 2)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (restrictedSet is not null && !entries.Any(entry => restrictedSet.Contains(entry.Edge.Id)))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var joinEntries = entries
|
|
.Select(entry => (Path: (IReadOnlyList<ElkPoint>)entry.Path, Side: entry.Side))
|
|
.ToArray();
|
|
if (!GroupHasSourceDepartureJoin(joinEntries, minLineClearance))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var sourceNode = entries[0].SourceNode;
|
|
var side = entries[0].Side;
|
|
var isGatewaySource = ElkShapeBoundaries.IsGatewayShape(sourceNode);
|
|
var boundaryCoordinate = side is "left" or "right"
|
|
? entries[0].Boundary.Y
|
|
: entries[0].Boundary.X;
|
|
var anchor = entries
|
|
.OrderBy(entry => Math.Abs(entry.TargetReference - boundaryCoordinate))
|
|
.ThenBy(entry => entry.PathLength)
|
|
.ThenBy(entry => entry.Edge.Id, StringComparer.Ordinal)
|
|
.First();
|
|
var sideLength = side is "left" or "right"
|
|
? Math.Max(8d, sourceNode.Height - 8d)
|
|
: Math.Max(8d, sourceNode.Width - 8d);
|
|
var slotSpacing = entries.Length > 1
|
|
? Math.Max(12d, Math.Min(minLineClearance, sideLength / entries.Length))
|
|
: 0d;
|
|
var minSlot = side is "left" or "right"
|
|
? sourceNode.Y + 4d
|
|
: sourceNode.X + 4d;
|
|
var maxSlot = side is "left" or "right"
|
|
? sourceNode.Y + sourceNode.Height - 4d
|
|
: sourceNode.X + sourceNode.Width - 4d;
|
|
|
|
var negativeEntries = entries
|
|
.Where(entry => !ReferenceEquals(entry, anchor) && entry.TargetReference < anchor.TargetReference)
|
|
.OrderByDescending(entry => entry.TargetReference)
|
|
.ThenBy(entry => entry.PathLength)
|
|
.ThenBy(entry => entry.Edge.Id, StringComparer.Ordinal)
|
|
.ToArray();
|
|
var positiveEntries = entries
|
|
.Where(entry => !ReferenceEquals(entry, anchor) && entry.TargetReference >= anchor.TargetReference)
|
|
.OrderBy(entry => entry.TargetReference)
|
|
.ThenBy(entry => entry.PathLength)
|
|
.ThenBy(entry => entry.Edge.Id, StringComparer.Ordinal)
|
|
.ToArray();
|
|
|
|
var desiredCoordinateByEdgeId = new Dictionary<string, double>(StringComparer.Ordinal)
|
|
{
|
|
[anchor.Edge.Id] = boundaryCoordinate,
|
|
};
|
|
var anchorDepartureAxis = TryExtractSourceDepartureRun(anchor.Path, side, out _, out var anchorRunEndIndex)
|
|
? side is "left" or "right"
|
|
? anchor.Path[anchorRunEndIndex].X
|
|
: anchor.Path[anchorRunEndIndex].Y
|
|
: side switch
|
|
{
|
|
"left" => sourceNode.X - 24d,
|
|
"right" => sourceNode.X + sourceNode.Width + 24d,
|
|
"top" => sourceNode.Y - 24d,
|
|
"bottom" => sourceNode.Y + sourceNode.Height + 24d,
|
|
_ => 0d,
|
|
};
|
|
var axisStep = Math.Max(12d, minLineClearance * 0.5d);
|
|
var desiredAxisByEdgeId = new Dictionary<string, double>(StringComparer.Ordinal)
|
|
{
|
|
[anchor.Edge.Id] = anchorDepartureAxis,
|
|
};
|
|
for (var i = 0; i < negativeEntries.Length; i++)
|
|
{
|
|
desiredCoordinateByEdgeId[negativeEntries[i].Edge.Id] = Math.Max(minSlot, boundaryCoordinate - ((i + 1) * slotSpacing));
|
|
desiredAxisByEdgeId[negativeEntries[i].Edge.Id] = anchorDepartureAxis;
|
|
}
|
|
|
|
for (var i = 0; i < positiveEntries.Length; i++)
|
|
{
|
|
desiredCoordinateByEdgeId[positiveEntries[i].Edge.Id] = Math.Min(maxSlot, boundaryCoordinate + ((i + 1) * slotSpacing));
|
|
desiredAxisByEdgeId[positiveEntries[i].Edge.Id] = anchorDepartureAxis;
|
|
}
|
|
|
|
foreach (var entry in entries)
|
|
{
|
|
if (!desiredCoordinateByEdgeId.TryGetValue(entry.Edge.Id, out var slotCoordinate))
|
|
{
|
|
continue;
|
|
}
|
|
if (!desiredAxisByEdgeId.TryGetValue(entry.Edge.Id, out var desiredAxis))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var originalCoordinate = side is "left" or "right"
|
|
? entry.Boundary.Y
|
|
: entry.Boundary.X;
|
|
var originalAxis = TryExtractSourceDepartureRun(entry.Path, side, out _, out var runEndIndex)
|
|
? side is "left" or "right"
|
|
? entry.Path[runEndIndex].X
|
|
: entry.Path[runEndIndex].Y
|
|
: desiredAxis;
|
|
if (Math.Abs(originalCoordinate - slotCoordinate) <= 0.5d
|
|
&& Math.Abs(originalAxis - desiredAxis) <= 0.5d)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
ElkPoint boundaryPoint;
|
|
if (isGatewaySource)
|
|
{
|
|
if (!ElkShapeBoundaries.TryProjectGatewayBoundarySlot(sourceNode, side, slotCoordinate, out boundaryPoint))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var continuation = entry.Path.Count > 1 ? entry.Path[1] : entry.Path[0];
|
|
boundaryPoint = ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(sourceNode, boundaryPoint, continuation);
|
|
}
|
|
else
|
|
{
|
|
boundaryPoint = side switch
|
|
{
|
|
"left" => new ElkPoint { X = sourceNode.X, Y = slotCoordinate },
|
|
"right" => new ElkPoint { X = sourceNode.X + sourceNode.Width, Y = slotCoordinate },
|
|
"top" => new ElkPoint { X = slotCoordinate, Y = sourceNode.Y },
|
|
"bottom" => new ElkPoint { X = slotCoordinate, Y = sourceNode.Y + sourceNode.Height },
|
|
_ => entry.Boundary,
|
|
};
|
|
}
|
|
|
|
var candidate = RewriteSourceDepartureRun(entry.Path, side, boundaryPoint, desiredAxis);
|
|
if (!PathChanged(entry.Path, candidate))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (isGatewaySource)
|
|
{
|
|
if (!HasAcceptableGatewayBoundaryPath(candidate, nodes, entry.Edge.SourceNodeId, entry.Edge.TargetNodeId, sourceNode, fromStart: true))
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (!HasClearBoundarySegments(candidate, nodes, entry.Edge.SourceNodeId, entry.Edge.TargetNodeId, true, 2)
|
|
|| !HasValidBoundaryAngle(candidate[0], candidate[1], sourceNode)
|
|
|| HasNodeObstacleCrossing(candidate, nodes, entry.Edge.SourceNodeId, entry.Edge.TargetNodeId))
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
|
|
result[entry.Index] = BuildSingleSectionEdge(entry.Edge, candidate);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
internal static ElkRoutedEdge[] SpreadRectTargetApproachFeederBands(
|
|
ElkRoutedEdge[] edges,
|
|
ElkPositionedNode[] nodes,
|
|
double minLineClearance,
|
|
IReadOnlyCollection<string>? restrictedEdgeIds = null)
|
|
{
|
|
if (edges.Length < 2 || nodes.Length == 0)
|
|
{
|
|
return edges;
|
|
}
|
|
|
|
var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal);
|
|
var restrictedSet = restrictedEdgeIds is null
|
|
? null
|
|
: restrictedEdgeIds.ToHashSet(StringComparer.Ordinal);
|
|
var result = edges.ToArray();
|
|
var groups = result
|
|
.Select((edge, index) => new
|
|
{
|
|
Edge = edge,
|
|
Index = index,
|
|
Path = ExtractFullPath(edge),
|
|
})
|
|
.Where(item => item.Path.Count >= 3
|
|
&& nodesById.TryGetValue(item.Edge.TargetNodeId ?? string.Empty, out var targetNode)
|
|
&& !ElkShapeBoundaries.IsGatewayShape(targetNode))
|
|
.GroupBy(
|
|
item =>
|
|
{
|
|
var targetNode = nodesById[item.Edge.TargetNodeId ?? string.Empty];
|
|
var side = ResolveTargetApproachSide(item.Path, targetNode);
|
|
return $"{targetNode.Id}|{side}";
|
|
},
|
|
StringComparer.Ordinal);
|
|
|
|
foreach (var group in groups)
|
|
{
|
|
var entries = group
|
|
.Select(item =>
|
|
{
|
|
var targetNode = nodesById[item.Edge.TargetNodeId ?? string.Empty];
|
|
var side = ResolveTargetApproachSide(item.Path, targetNode);
|
|
return TryExtractTargetApproachFeeder(item.Path, side, out var feeder)
|
|
? new
|
|
{
|
|
item.Edge,
|
|
item.Index,
|
|
item.Path,
|
|
TargetNode = targetNode,
|
|
Side = side,
|
|
Feeder = feeder,
|
|
}
|
|
: null;
|
|
})
|
|
.Where(entry => entry is not null)
|
|
.Select(entry => entry!)
|
|
.ToArray();
|
|
if (entries.Length < 2)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (restrictedSet is not null && !entries.Any(entry => restrictedSet.Contains(entry.Edge.Id)))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var hasConflict = false;
|
|
for (var i = 0; i < entries.Length && !hasConflict; i++)
|
|
{
|
|
for (var j = i + 1; j < entries.Length; j++)
|
|
{
|
|
if (ElkEdgeRoutingGeometry.AreParallelAndClose(
|
|
entries[i].Feeder.Start,
|
|
entries[i].Feeder.End,
|
|
entries[j].Feeder.Start,
|
|
entries[j].Feeder.End,
|
|
minLineClearance))
|
|
{
|
|
hasConflict = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!hasConflict)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var spacing = Math.Max(12d, minLineClearance + 4d);
|
|
var sorted = entries
|
|
.OrderBy(entry => entry.Feeder.BandCoordinate)
|
|
.ThenBy(entry => entry.Edge.Id, StringComparer.Ordinal)
|
|
.ToArray();
|
|
var baseBand = sorted[0].Side is "left" or "top"
|
|
? sorted.Max(entry => entry.Feeder.BandCoordinate)
|
|
: sorted.Min(entry => entry.Feeder.BandCoordinate);
|
|
|
|
for (var i = 0; i < sorted.Length; i++)
|
|
{
|
|
var desiredBand = ResolveDesiredTargetApproachAxis(
|
|
sorted[i].TargetNode,
|
|
sorted[i].Side,
|
|
baseBand,
|
|
spacing,
|
|
i,
|
|
forceOutwardFromBoundary: true);
|
|
|
|
if (Math.Abs(sorted[i].Feeder.BandCoordinate - desiredBand) <= 0.5d)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var candidate = RewriteTargetApproachFeederBand(sorted[i].Path, sorted[i].Side, desiredBand);
|
|
if (!PathChanged(sorted[i].Path, candidate)
|
|
|| !HasClearBoundarySegments(candidate, nodes, sorted[i].Edge.SourceNodeId, sorted[i].Edge.TargetNodeId, false, 4)
|
|
|| HasNodeObstacleCrossing(candidate, nodes, sorted[i].Edge.SourceNodeId, sorted[i].Edge.TargetNodeId))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
result[sorted[i].Index] = BuildSingleSectionEdge(sorted[i].Edge, candidate);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
internal static ElkRoutedEdge[] SeparateMixedNodeFaceLaneConflicts(
|
|
ElkRoutedEdge[] edges,
|
|
ElkPositionedNode[] nodes,
|
|
double minLineClearance,
|
|
IReadOnlyCollection<string>? restrictedEdgeIds = null)
|
|
{
|
|
if (edges.Length < 2 || nodes.Length == 0)
|
|
{
|
|
return edges;
|
|
}
|
|
|
|
var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal);
|
|
var graphMinY = nodes.Min(node => node.Y);
|
|
var graphMaxY = nodes.Max(node => node.Y + node.Height);
|
|
var restrictedSet = restrictedEdgeIds is null
|
|
? null
|
|
: restrictedEdgeIds.ToHashSet(StringComparer.Ordinal);
|
|
var result = edges.ToArray();
|
|
var entries = new List<(int Index, ElkRoutedEdge Edge, IReadOnlyList<ElkPoint> Path, ElkPositionedNode Node, string Side, bool IsOutgoing, ElkPoint Boundary, double BoundaryCoordinate, double AxisValue)>();
|
|
|
|
for (var index = 0; index < result.Length; index++)
|
|
{
|
|
var edge = result[index];
|
|
var path = ExtractFullPath(edge);
|
|
if (path.Count < 2)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (nodesById.TryGetValue(edge.SourceNodeId ?? string.Empty, out var sourceNode)
|
|
&& ShouldSpreadSourceDeparture(edge, graphMinY, graphMaxY))
|
|
{
|
|
var side = ResolveSourceDepartureSide(path, sourceNode);
|
|
var axisValue = TryExtractSourceDepartureRun(path, side, out _, out var runEndIndex)
|
|
? side is "left" or "right"
|
|
? path[runEndIndex].X
|
|
: path[runEndIndex].Y
|
|
: ResolveDefaultSourceDepartureAxis(sourceNode, side);
|
|
entries.Add((
|
|
index,
|
|
edge,
|
|
path,
|
|
sourceNode,
|
|
side,
|
|
true,
|
|
path[0],
|
|
side is "left" or "right" ? path[0].Y : path[0].X,
|
|
axisValue));
|
|
}
|
|
|
|
if (nodesById.TryGetValue(edge.TargetNodeId ?? string.Empty, out var targetNode)
|
|
&& ShouldSpreadTargetApproach(edge, graphMinY, graphMaxY))
|
|
{
|
|
var side = ResolveTargetApproachSide(path, targetNode);
|
|
var axisValue = ResolveTargetApproachAxisValue(path, side);
|
|
if (double.IsNaN(axisValue))
|
|
{
|
|
axisValue = side is "left" or "right" ? path[^1].Y : path[^1].X;
|
|
}
|
|
|
|
entries.Add((
|
|
index,
|
|
edge,
|
|
path,
|
|
targetNode,
|
|
side,
|
|
false,
|
|
path[^1],
|
|
side is "left" or "right" ? path[^1].Y : path[^1].X,
|
|
axisValue));
|
|
}
|
|
}
|
|
|
|
foreach (var group in entries.GroupBy(
|
|
entry => $"{entry.Node.Id}|{entry.Side}",
|
|
StringComparer.Ordinal))
|
|
{
|
|
var groupEntries = group.ToArray();
|
|
if (groupEntries.Length < 2
|
|
|| !groupEntries.Any(entry => entry.IsOutgoing)
|
|
|| !groupEntries.Any(entry => !entry.IsOutgoing)
|
|
|| !GroupHasMixedNodeFaceLaneConflict(groupEntries, minLineClearance))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (restrictedSet is not null && !groupEntries.Any(entry => restrictedSet.Contains(entry.Edge.Id)))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var node = groupEntries[0].Node;
|
|
var side = groupEntries[0].Side;
|
|
var sideLength = side is "left" or "right"
|
|
? Math.Max(8d, node.Height - 8d)
|
|
: Math.Max(8d, node.Width - 8d);
|
|
var slotSpacing = groupEntries.Length > 1
|
|
? groupEntries.Length == 2
|
|
? Math.Max(
|
|
12d,
|
|
Math.Min(
|
|
ResolveBoundaryJoinSlotSpacing(minLineClearance, sideLength, groupEntries.Length),
|
|
Math.Min(28d, minLineClearance * 0.45d)))
|
|
: ResolveBoundaryJoinSlotSpacing(minLineClearance, sideLength, groupEntries.Length)
|
|
: 0d;
|
|
var centerCoordinate = side is "left" or "right"
|
|
? node.Y + (node.Height / 2d)
|
|
: node.X + (node.Width / 2d);
|
|
var anchor = groupEntries
|
|
.OrderBy(entry => entry.IsOutgoing ? 0 : 1)
|
|
.ThenBy(entry => IsRepeatCollectorLabel(entry.Edge.Label) ? 1 : 0)
|
|
.ThenBy(entry => Math.Abs(entry.BoundaryCoordinate - centerCoordinate))
|
|
.ThenBy(entry => ComputePathLength(entry.Path))
|
|
.ThenBy(entry => entry.Edge.Id, StringComparer.Ordinal)
|
|
.First();
|
|
|
|
var minSlot = side is "left" or "right" ? node.Y + 4d : node.X + 4d;
|
|
var maxSlot = side is "left" or "right" ? node.Y + node.Height - 4d : node.X + node.Width - 4d;
|
|
var negativeEntries = groupEntries
|
|
.Where(entry => entry.Edge.Id != anchor.Edge.Id && entry.BoundaryCoordinate < anchor.BoundaryCoordinate)
|
|
.OrderByDescending(entry => entry.BoundaryCoordinate)
|
|
.ThenBy(entry => entry.IsOutgoing ? 0 : 1)
|
|
.ThenBy(entry => entry.Edge.Id, StringComparer.Ordinal)
|
|
.ToArray();
|
|
var positiveEntries = groupEntries
|
|
.Where(entry => entry.Edge.Id != anchor.Edge.Id && entry.BoundaryCoordinate >= anchor.BoundaryCoordinate)
|
|
.OrderBy(entry => entry.BoundaryCoordinate)
|
|
.ThenBy(entry => entry.IsOutgoing ? 0 : 1)
|
|
.ThenBy(entry => entry.Edge.Id, StringComparer.Ordinal)
|
|
.ToArray();
|
|
|
|
var desiredCoordinateByEdgeId = new Dictionary<string, double>(StringComparer.Ordinal)
|
|
{
|
|
[anchor.Edge.Id] = anchor.BoundaryCoordinate,
|
|
};
|
|
for (var i = 0; i < negativeEntries.Length; i++)
|
|
{
|
|
desiredCoordinateByEdgeId[negativeEntries[i].Edge.Id] = Math.Max(minSlot, anchor.BoundaryCoordinate - ((i + 1) * slotSpacing));
|
|
}
|
|
|
|
for (var i = 0; i < positiveEntries.Length; i++)
|
|
{
|
|
desiredCoordinateByEdgeId[positiveEntries[i].Edge.Id] = Math.Min(maxSlot, anchor.BoundaryCoordinate + ((i + 1) * slotSpacing));
|
|
}
|
|
|
|
foreach (var entry in groupEntries)
|
|
{
|
|
if (!desiredCoordinateByEdgeId.TryGetValue(entry.Edge.Id, out var desiredCoordinate)
|
|
|| Math.Abs(desiredCoordinate - entry.BoundaryCoordinate) <= 0.5d)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var bestEdge = result[entry.Index];
|
|
var currentGroupEdges = groupEntries
|
|
.Select(item => result[item.Index])
|
|
.ToArray();
|
|
var bestSharedLaneViolations = ElkEdgeRoutingScoring.CountSharedLaneViolations(currentGroupEdges, nodes);
|
|
var bestTargetJoinViolations = ElkEdgeRoutingScoring.CountTargetApproachJoinViolations(currentGroupEdges, nodes);
|
|
var bestBoundaryAngleViolations = ElkEdgeRoutingScoring.CountBadBoundaryAngles(currentGroupEdges, nodes);
|
|
var bestGatewaySourceExitViolations = ElkEdgeRoutingScoring.CountGatewaySourceExitViolations(currentGroupEdges, nodes);
|
|
var bestUnderNodeViolations = ElkEdgeRoutingScoring.CountUnderNodeViolations(currentGroupEdges, nodes);
|
|
var bestPathLength = ComputePathLength(entry.Path);
|
|
var prefersAlternateRepeatFace = !entry.IsOutgoing
|
|
&& !ElkShapeBoundaries.IsGatewayShape(entry.Node)
|
|
&& IsRepeatCollectorLabel(entry.Edge.Label)
|
|
&& groupEntries.Any(other => other.IsOutgoing);
|
|
var candidatePaths = new List<IReadOnlyList<ElkPoint>>();
|
|
var directCandidate = entry.IsOutgoing
|
|
? BuildMixedSourceFaceCandidate(entry.Path, entry.Node, side, desiredCoordinate, entry.AxisValue)
|
|
: BuildMixedTargetFaceCandidate(entry.Path, entry.Node, side, desiredCoordinate, entry.AxisValue);
|
|
AddUniquePathCandidate(candidatePaths, directCandidate);
|
|
var availableSpan = Math.Abs(desiredCoordinate - anchor.BoundaryCoordinate);
|
|
if ((prefersAlternateRepeatFace || availableSpan + 0.5d < minLineClearance)
|
|
&& TryBuildAlternateMixedFaceCandidate(entry, nodes, minLineClearance, out var alternateCandidate))
|
|
{
|
|
AddUniquePathCandidate(candidatePaths, alternateCandidate);
|
|
}
|
|
|
|
foreach (var candidate in candidatePaths)
|
|
{
|
|
if (!PathChanged(entry.Path, candidate)
|
|
|| HasNodeObstacleCrossing(candidate, nodes, entry.Edge.SourceNodeId, entry.Edge.TargetNodeId))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (entry.IsOutgoing)
|
|
{
|
|
if (ElkShapeBoundaries.IsGatewayShape(entry.Node))
|
|
{
|
|
if (!HasAcceptableGatewayBoundaryPath(candidate, nodes, entry.Edge.SourceNodeId, entry.Edge.TargetNodeId, entry.Node, fromStart: true))
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
else if (!HasClearBoundarySegments(candidate, nodes, entry.Edge.SourceNodeId, entry.Edge.TargetNodeId, true, 2)
|
|
|| !HasValidBoundaryAngle(candidate[0], candidate[1], entry.Node))
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (ElkShapeBoundaries.IsGatewayShape(entry.Node))
|
|
{
|
|
if (!CanAcceptGatewayTargetRepair(candidate, entry.Node)
|
|
|| !HasAcceptableGatewayBoundaryPath(candidate, nodes, entry.Edge.SourceNodeId, entry.Edge.TargetNodeId, entry.Node, fromStart: false))
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
else if (!HasClearBoundarySegments(candidate, nodes, entry.Edge.SourceNodeId, entry.Edge.TargetNodeId, false, 4)
|
|
|| !HasValidBoundaryAngle(candidate[^1], candidate[^2], entry.Node)
|
|
|| HasTargetApproachBacktracking(candidate, entry.Node))
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
|
|
var candidateEdge = BuildSingleSectionEdge(entry.Edge, candidate);
|
|
var candidateGroupEdges = groupEntries
|
|
.Select(item => item.Index == entry.Index ? candidateEdge : result[item.Index])
|
|
.ToArray();
|
|
var candidateSharedLaneViolations = ElkEdgeRoutingScoring.CountSharedLaneViolations(candidateGroupEdges, nodes);
|
|
var candidateTargetJoinViolations = ElkEdgeRoutingScoring.CountTargetApproachJoinViolations(candidateGroupEdges, nodes);
|
|
var candidateBoundaryAngleViolations = ElkEdgeRoutingScoring.CountBadBoundaryAngles(candidateGroupEdges, nodes);
|
|
var candidateGatewaySourceExitViolations = ElkEdgeRoutingScoring.CountGatewaySourceExitViolations(candidateGroupEdges, nodes);
|
|
var candidateUnderNodeViolations = ElkEdgeRoutingScoring.CountUnderNodeViolations(candidateGroupEdges, nodes);
|
|
var candidatePathLength = ComputePathLength(candidate);
|
|
|
|
if (!IsBetterMixedNodeFaceCandidate(
|
|
candidateSharedLaneViolations,
|
|
candidateTargetJoinViolations,
|
|
candidateBoundaryAngleViolations,
|
|
candidateGatewaySourceExitViolations,
|
|
candidateUnderNodeViolations,
|
|
candidatePathLength,
|
|
bestSharedLaneViolations,
|
|
bestTargetJoinViolations,
|
|
bestBoundaryAngleViolations,
|
|
bestGatewaySourceExitViolations,
|
|
bestUnderNodeViolations,
|
|
bestPathLength))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
bestEdge = candidateEdge;
|
|
bestSharedLaneViolations = candidateSharedLaneViolations;
|
|
bestTargetJoinViolations = candidateTargetJoinViolations;
|
|
bestBoundaryAngleViolations = candidateBoundaryAngleViolations;
|
|
bestGatewaySourceExitViolations = candidateGatewaySourceExitViolations;
|
|
bestUnderNodeViolations = candidateUnderNodeViolations;
|
|
bestPathLength = candidatePathLength;
|
|
}
|
|
|
|
result[entry.Index] = bestEdge;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
internal static ElkRoutedEdge[] SeparateRepeatCollectorLocalLaneConflicts(
|
|
ElkRoutedEdge[] edges,
|
|
ElkPositionedNode[] nodes,
|
|
double minLineClearance,
|
|
IReadOnlyCollection<string>? restrictedEdgeIds = null)
|
|
{
|
|
if (edges.Length < 2 || nodes.Length == 0)
|
|
{
|
|
return edges;
|
|
}
|
|
|
|
var result = edges.ToArray();
|
|
var graphMinY = nodes.Min(node => node.Y);
|
|
var graphMaxY = nodes.Max(node => node.Y + node.Height);
|
|
var restrictedSet = restrictedEdgeIds is null
|
|
? null
|
|
: restrictedEdgeIds.ToHashSet(StringComparer.Ordinal);
|
|
var nodeObstacles = nodes.Select(node => (
|
|
Left: node.X,
|
|
Top: node.Y,
|
|
Right: node.X + node.Width,
|
|
Bottom: node.Y + node.Height,
|
|
Id: node.Id)).ToArray();
|
|
|
|
for (var i = 0; i < result.Length; i++)
|
|
{
|
|
var edge = result[i];
|
|
if (restrictedSet is not null && !restrictedSet.Contains(edge.Id))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!IsRepeatCollectorLabel(edge.Label))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var path = ExtractFullPath(edge);
|
|
if (path.Count < 3)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
for (var segmentIndex = 0; segmentIndex < path.Count - 1; segmentIndex++)
|
|
{
|
|
var start = path[segmentIndex];
|
|
var end = path[segmentIndex + 1];
|
|
var isHorizontal = Math.Abs(start.Y - end.Y) <= 0.5d;
|
|
var isVertical = Math.Abs(start.X - end.X) <= 0.5d;
|
|
if (!isHorizontal && !isVertical)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var conflictFound = false;
|
|
var desiredCoordinate = 0d;
|
|
foreach (var otherEdge in result)
|
|
{
|
|
if (otherEdge.Id == edge.Id)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
foreach (var otherSegment in ElkEdgeRoutingGeometry.FlattenSegments(otherEdge))
|
|
{
|
|
if (!ElkEdgeRoutingGeometry.AreParallelAndClose(start, end, otherSegment.Start, otherSegment.End, minLineClearance))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (isHorizontal)
|
|
{
|
|
desiredCoordinate = start.Y <= otherSegment.Start.Y
|
|
? otherSegment.Start.Y - (minLineClearance + 4d)
|
|
: otherSegment.Start.Y + (minLineClearance + 4d);
|
|
}
|
|
else
|
|
{
|
|
desiredCoordinate = start.X <= otherSegment.Start.X
|
|
? otherSegment.Start.X - (minLineClearance + 4d)
|
|
: otherSegment.Start.X + (minLineClearance + 4d);
|
|
}
|
|
|
|
conflictFound = true;
|
|
break;
|
|
}
|
|
|
|
if (conflictFound)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!conflictFound)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var preferredCoordinate = desiredCoordinate;
|
|
var fallbackCoordinate = isHorizontal
|
|
? start.Y + (start.Y - desiredCoordinate)
|
|
: start.X + (start.X - desiredCoordinate);
|
|
foreach (var alternateCoordinate in new[] { preferredCoordinate, fallbackCoordinate }.Distinct())
|
|
{
|
|
var candidate = ShiftSingleOrthogonalRun(path, segmentIndex, alternateCoordinate);
|
|
if (!PathChanged(path, candidate)
|
|
|| HasNodeObstacleCrossing(candidate, nodes, edge.SourceNodeId, edge.TargetNodeId)
|
|
|| SegmentLeavesGraphBand(candidate, graphMinY, graphMaxY))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var crossesObstacle = false;
|
|
for (var candidateIndex = 0; candidateIndex < candidate.Count - 1; candidateIndex++)
|
|
{
|
|
if (!SegmentCrossesObstacle(candidate[candidateIndex], candidate[candidateIndex + 1], nodeObstacles, edge.SourceNodeId, edge.TargetNodeId))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
crossesObstacle = true;
|
|
break;
|
|
}
|
|
|
|
if (crossesObstacle)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var repairedEdge = BuildSingleSectionEdge(edge, candidate);
|
|
repairedEdge = RepairBoundaryAnglesAndTargetApproaches(
|
|
[repairedEdge],
|
|
nodes,
|
|
minLineClearance)[0];
|
|
var repairedPath = ExtractFullPath(repairedEdge);
|
|
if (HasNodeObstacleCrossing(repairedPath, nodes, edge.SourceNodeId, edge.TargetNodeId)
|
|
|| SegmentLeavesGraphBand(repairedPath, graphMinY, graphMaxY))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
result[i] = repairedEdge;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
internal static ElkRoutedEdge[] ElevateRepeatCollectorNodeClearanceViolations(
|
|
ElkRoutedEdge[] edges,
|
|
ElkPositionedNode[] nodes,
|
|
double minLineClearance,
|
|
IReadOnlyCollection<string>? restrictedEdgeIds = null)
|
|
{
|
|
if (edges.Length == 0 || nodes.Length == 0)
|
|
{
|
|
return edges;
|
|
}
|
|
|
|
var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal);
|
|
var graphMinY = nodes.Min(node => node.Y);
|
|
var corridorY = graphMinY - Math.Max(24d, minLineClearance * 0.6d);
|
|
var restrictedSet = restrictedEdgeIds is null
|
|
? null
|
|
: restrictedEdgeIds.ToHashSet(StringComparer.Ordinal);
|
|
var result = edges.ToArray();
|
|
|
|
for (var i = 0; i < result.Length; i++)
|
|
{
|
|
var edge = result[i];
|
|
if (restrictedSet is not null && !restrictedSet.Contains(edge.Id))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!IsRepeatCollectorLabel(edge.Label)
|
|
|| !nodesById.TryGetValue(edge.TargetNodeId ?? string.Empty, out var targetNode))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var path = ExtractFullPath(edge);
|
|
if (path.Count < 2
|
|
|| !HasRepeatCollectorNodeClearanceViolation(path, nodes, edge.SourceNodeId, edge.TargetNodeId, minLineClearance))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var targetApproachY = Math.Min(corridorY, targetNode.Y - 24d);
|
|
ElkPoint targetEndpoint;
|
|
if (ElkShapeBoundaries.IsGatewayShape(targetNode))
|
|
{
|
|
var slotCoordinate = Math.Max(targetNode.X + 4d, Math.Min(targetNode.X + targetNode.Width - 4d, path[^1].X));
|
|
if (!ElkShapeBoundaries.TryProjectGatewayBoundarySlot(targetNode, "top", slotCoordinate, out targetEndpoint))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
targetEndpoint = ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(
|
|
targetNode,
|
|
targetEndpoint,
|
|
new ElkPoint { X = targetEndpoint.X, Y = targetApproachY });
|
|
}
|
|
else
|
|
{
|
|
targetEndpoint = BuildRectBoundaryPointForSide(targetNode, "top", path[0]);
|
|
}
|
|
|
|
var rebuilt = new List<ElkPoint>
|
|
{
|
|
new() { X = path[0].X, Y = path[0].Y },
|
|
};
|
|
|
|
if (Math.Abs(rebuilt[^1].Y - corridorY) > 0.5d)
|
|
{
|
|
rebuilt.Add(new ElkPoint { X = rebuilt[^1].X, Y = corridorY });
|
|
}
|
|
|
|
if (Math.Abs(rebuilt[^1].X - targetEndpoint.X) > 0.5d)
|
|
{
|
|
rebuilt.Add(new ElkPoint { X = targetEndpoint.X, Y = rebuilt[^1].Y });
|
|
}
|
|
|
|
if (Math.Abs(rebuilt[^1].Y - targetApproachY) > 0.5d)
|
|
{
|
|
rebuilt.Add(new ElkPoint { X = targetEndpoint.X, Y = targetApproachY });
|
|
}
|
|
|
|
rebuilt.Add(targetEndpoint);
|
|
var candidate = NormalizePathPoints(rebuilt);
|
|
if (ElkShapeBoundaries.IsGatewayShape(targetNode))
|
|
{
|
|
candidate = NormalizeGatewayEntryPath(candidate, targetNode, targetEndpoint);
|
|
}
|
|
|
|
if (!PathChanged(path, candidate)
|
|
|| HasNodeObstacleCrossing(candidate, nodes, edge.SourceNodeId, edge.TargetNodeId)
|
|
|| HasRepeatCollectorNodeClearanceViolation(candidate, nodes, edge.SourceNodeId, edge.TargetNodeId, minLineClearance))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var repairedEdge = BuildSingleSectionEdge(edge, candidate);
|
|
repairedEdge = NormalizeSourceExitAngles([repairedEdge], nodes)[0];
|
|
var repairedPath = ExtractFullPath(repairedEdge);
|
|
if (repairedPath.Count < 2
|
|
|| HasNodeObstacleCrossing(repairedPath, nodes, edge.SourceNodeId, edge.TargetNodeId)
|
|
|| HasRepeatCollectorNodeClearanceViolation(repairedPath, nodes, edge.SourceNodeId, edge.TargetNodeId, minLineClearance)
|
|
|| (ElkShapeBoundaries.IsGatewayShape(targetNode)
|
|
? !CanAcceptGatewayTargetRepair(repairedPath, targetNode)
|
|
: !HasValidBoundaryAngle(repairedPath[^1], repairedPath[^2], targetNode)))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
result[i] = repairedEdge;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
internal static ElkRoutedEdge[] ElevateUnderNodeViolations(
|
|
ElkRoutedEdge[] edges,
|
|
ElkPositionedNode[] nodes,
|
|
double minLineClearance,
|
|
IReadOnlyCollection<string>? restrictedEdgeIds = null)
|
|
{
|
|
if (edges.Length == 0 || nodes.Length == 0)
|
|
{
|
|
return edges;
|
|
}
|
|
|
|
var restrictedSet = restrictedEdgeIds is null
|
|
? null
|
|
: restrictedEdgeIds.ToHashSet(StringComparer.Ordinal);
|
|
var result = edges.ToArray();
|
|
for (var i = 0; i < result.Length; i++)
|
|
{
|
|
var edge = result[i];
|
|
if (restrictedSet is not null && !restrictedSet.Contains(edge.Id))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var path = ExtractFullPath(edge);
|
|
if (path.Count < 2)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (TryResolveUnderNodeWithPreferredShortcut(
|
|
edge,
|
|
path,
|
|
nodes,
|
|
minLineClearance,
|
|
out var directRepair))
|
|
{
|
|
var currentLocalHardPressure = ComputeUnderNodeRepairLocalHardPressure(edge, nodes);
|
|
var repairedEdge = BuildSingleSectionEdge(edge, directRepair);
|
|
repairedEdge = ResolveUnderNodePeerTargetConflicts(
|
|
repairedEdge,
|
|
result,
|
|
i,
|
|
nodes,
|
|
minLineClearance);
|
|
var repairedPath = ExtractFullPath(repairedEdge);
|
|
var repairedUnderNodeSegments = CountUnderNodeSegments(repairedPath, nodes, edge.SourceNodeId, edge.TargetNodeId, minLineClearance);
|
|
var currentUnderNodeSegments = CountUnderNodeSegments(path, nodes, edge.SourceNodeId, edge.TargetNodeId, minLineClearance);
|
|
var repairedCrossesNode = HasNodeObstacleCrossing(repairedPath, nodes, edge.SourceNodeId, edge.TargetNodeId);
|
|
var repairedLocalHardPressure = ComputeUnderNodeRepairLocalHardPressure(repairedEdge, nodes);
|
|
WriteUnderNodeDebug(
|
|
edge.Id,
|
|
$"accept-check raw current={currentUnderNodeSegments} repaired={repairedUnderNodeSegments} crossing={repairedCrossesNode} local={repairedLocalHardPressure}/{currentLocalHardPressure} repaired={FormatPath(repairedPath)}");
|
|
if (repairedUnderNodeSegments < currentUnderNodeSegments
|
|
&& !repairedCrossesNode
|
|
&& repairedLocalHardPressure <= currentLocalHardPressure)
|
|
{
|
|
WriteUnderNodeDebug(edge.Id, "accept-check raw accepted");
|
|
result[i] = repairedUnderNodeSegments == 0
|
|
? ProtectUnderNodeGeometry(repairedEdge)
|
|
: repairedEdge;
|
|
continue;
|
|
}
|
|
|
|
repairedEdge = RepairBoundaryAnglesAndTargetApproaches(
|
|
[repairedEdge],
|
|
nodes,
|
|
minLineClearance)[0];
|
|
repairedEdge = FinalizeGatewayBoundaryGeometry([repairedEdge], nodes)[0];
|
|
repairedEdge = NormalizeBoundaryAngles([repairedEdge], nodes)[0];
|
|
repairedEdge = NormalizeSourceExitAngles([repairedEdge], nodes)[0];
|
|
repairedEdge = ResolveUnderNodePeerTargetConflicts(
|
|
repairedEdge,
|
|
result,
|
|
i,
|
|
nodes,
|
|
minLineClearance);
|
|
repairedPath = ExtractFullPath(repairedEdge);
|
|
repairedUnderNodeSegments = CountUnderNodeSegments(repairedPath, nodes, edge.SourceNodeId, edge.TargetNodeId, minLineClearance);
|
|
repairedCrossesNode = HasNodeObstacleCrossing(repairedPath, nodes, edge.SourceNodeId, edge.TargetNodeId);
|
|
repairedLocalHardPressure = ComputeUnderNodeRepairLocalHardPressure(repairedEdge, nodes);
|
|
WriteUnderNodeDebug(
|
|
edge.Id,
|
|
$"accept-check normalized current={currentUnderNodeSegments} repaired={repairedUnderNodeSegments} crossing={repairedCrossesNode} local={repairedLocalHardPressure}/{currentLocalHardPressure} repaired={FormatPath(repairedPath)}");
|
|
if (repairedUnderNodeSegments < currentUnderNodeSegments
|
|
&& !repairedCrossesNode
|
|
&& repairedLocalHardPressure <= currentLocalHardPressure)
|
|
{
|
|
WriteUnderNodeDebug(edge.Id, "accept-check normalized accepted");
|
|
result[i] = repairedUnderNodeSegments == 0
|
|
? ProtectUnderNodeGeometry(repairedEdge)
|
|
: repairedEdge;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
var lifted = TryLiftUnderNodeSegments(
|
|
path,
|
|
nodes,
|
|
edge.SourceNodeId,
|
|
edge.TargetNodeId,
|
|
minLineClearance);
|
|
if (!PathChanged(path, lifted))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var liftedEdge = BuildSingleSectionEdge(edge, lifted);
|
|
liftedEdge = NormalizeBoundaryAngles([liftedEdge], nodes)[0];
|
|
liftedEdge = NormalizeSourceExitAngles([liftedEdge], nodes)[0];
|
|
var liftedPath = ExtractFullPath(liftedEdge);
|
|
if (CountUnderNodeSegments(liftedPath, nodes, edge.SourceNodeId, edge.TargetNodeId, minLineClearance)
|
|
< CountUnderNodeSegments(path, nodes, edge.SourceNodeId, edge.TargetNodeId, minLineClearance)
|
|
&& !HasNodeObstacleCrossing(liftedPath, nodes, edge.SourceNodeId, edge.TargetNodeId))
|
|
{
|
|
result[i] = CountUnderNodeSegments(liftedPath, nodes, edge.SourceNodeId, edge.TargetNodeId, minLineClearance) == 0
|
|
? ProtectUnderNodeGeometry(liftedEdge)
|
|
: liftedEdge;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private static int ComputeUnderNodeRepairLocalHardPressure(
|
|
ElkRoutedEdge edge,
|
|
IReadOnlyCollection<ElkPositionedNode> nodes)
|
|
{
|
|
return ElkEdgeRoutingScoring.CountBelowGraphViolations([edge], nodes)
|
|
+ ElkEdgeRoutingScoring.CountUnderNodeViolations([edge], nodes)
|
|
+ ElkEdgeRoutingScoring.CountLongDiagonalViolations([edge], nodes)
|
|
+ ElkEdgeRoutingScoring.CountBadBoundaryAngles([edge], nodes)
|
|
+ ElkEdgeRoutingScoring.CountGatewaySourceExitViolations([edge], nodes)
|
|
+ ElkEdgeRoutingScoring.CountTargetApproachJoinViolations([edge], nodes)
|
|
+ ElkEdgeRoutingScoring.CountSharedLaneViolations([edge], nodes)
|
|
+ ElkEdgeRoutingScoring.CountTargetApproachBacktrackingViolations([edge], nodes)
|
|
+ ElkEdgeRoutingScoring.CountExcessiveDetourViolations([edge], nodes);
|
|
}
|
|
|
|
internal static ElkRoutedEdge[] PolishTargetPeerConflicts(
|
|
ElkRoutedEdge[] edges,
|
|
ElkPositionedNode[] nodes,
|
|
double minLineClearance,
|
|
IReadOnlyCollection<string>? restrictedEdgeIds = null)
|
|
{
|
|
if (edges.Length < 2 || nodes.Length == 0)
|
|
{
|
|
return edges;
|
|
}
|
|
|
|
var restrictedSet = restrictedEdgeIds is null
|
|
? null
|
|
: restrictedEdgeIds.ToHashSet(StringComparer.Ordinal);
|
|
var result = edges.ToArray();
|
|
for (var i = 0; i < result.Length; i++)
|
|
{
|
|
if (restrictedSet is not null && !restrictedSet.Contains(result[i].Id))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
result[i] = ResolveUnderNodePeerTargetConflicts(
|
|
result[i],
|
|
result,
|
|
i,
|
|
nodes,
|
|
minLineClearance);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private static ElkRoutedEdge ResolveUnderNodePeerTargetConflicts(
|
|
ElkRoutedEdge candidateEdge,
|
|
IReadOnlyList<ElkRoutedEdge> currentEdges,
|
|
int candidateIndex,
|
|
ElkPositionedNode[] nodes,
|
|
double minLineClearance)
|
|
{
|
|
if (TryPolishGatewayUnderNodeTargetPeerConflicts(
|
|
candidateEdge,
|
|
currentEdges,
|
|
candidateIndex,
|
|
nodes,
|
|
minLineClearance,
|
|
out var gatewayPolishedEdge))
|
|
{
|
|
return gatewayPolishedEdge;
|
|
}
|
|
|
|
return TryPolishRectUnderNodeTargetPeerConflicts(
|
|
candidateEdge,
|
|
currentEdges,
|
|
candidateIndex,
|
|
nodes,
|
|
minLineClearance,
|
|
out var polishedEdge)
|
|
? polishedEdge
|
|
: candidateEdge;
|
|
}
|
|
|
|
private static bool TryPolishGatewayUnderNodeTargetPeerConflicts(
|
|
ElkRoutedEdge candidateEdge,
|
|
IReadOnlyList<ElkRoutedEdge> currentEdges,
|
|
int candidateIndex,
|
|
ElkPositionedNode[] nodes,
|
|
double minLineClearance,
|
|
out ElkRoutedEdge polishedEdge)
|
|
{
|
|
polishedEdge = candidateEdge;
|
|
if (string.IsNullOrWhiteSpace(candidateEdge.TargetNodeId))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal);
|
|
if (!nodesById.TryGetValue(candidateEdge.TargetNodeId, out var targetNode)
|
|
|| !ElkShapeBoundaries.IsGatewayShape(targetNode))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
nodesById.TryGetValue(candidateEdge.SourceNodeId ?? string.Empty, out var sourceNode);
|
|
var peerEdges = currentEdges
|
|
.Where((edge, index) =>
|
|
index != candidateIndex
|
|
&& string.Equals(edge.TargetNodeId, candidateEdge.TargetNodeId, StringComparison.Ordinal))
|
|
.ToArray();
|
|
if (peerEdges.Length == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var path = ExtractFullPath(candidateEdge);
|
|
if (path.Count < 2)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var sourceNodeId = candidateEdge.SourceNodeId;
|
|
var targetNodeId = candidateEdge.TargetNodeId;
|
|
var currentBundle = peerEdges
|
|
.Append(candidateEdge)
|
|
.ToArray();
|
|
var currentTargetJoinViolations = ElkEdgeRoutingScoring.CountTargetApproachJoinViolations(currentBundle, nodes);
|
|
var currentSharedLaneViolations = ElkEdgeRoutingScoring.CountSharedLaneViolations(currentBundle, nodes);
|
|
var currentUnderNodeSegments = CountUnderNodeSegments(path, nodes, sourceNodeId, targetNodeId, minLineClearance);
|
|
var currentUnderNodeViolations = ElkEdgeRoutingScoring.CountUnderNodeViolations([candidateEdge], nodes);
|
|
var currentLocalHardPressure = ComputeUnderNodeRepairLocalHardPressure(candidateEdge, nodes);
|
|
var currentPathLength = ComputePathLength(path);
|
|
if (currentTargetJoinViolations == 0
|
|
&& currentSharedLaneViolations == 0
|
|
&& currentUnderNodeSegments == 0
|
|
&& currentUnderNodeViolations == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var bestEdge = default(ElkRoutedEdge);
|
|
var bestTargetJoinViolations = currentTargetJoinViolations;
|
|
var bestSharedLaneViolations = currentSharedLaneViolations;
|
|
var bestUnderNodeSegments = currentUnderNodeSegments;
|
|
var bestUnderNodeViolations = currentUnderNodeViolations;
|
|
var bestLocalHardPressure = currentLocalHardPressure;
|
|
var bestPathLength = currentPathLength;
|
|
|
|
foreach (var candidatePath in EnumerateGatewayUnderNodePeerConflictCandidates(
|
|
path,
|
|
targetNode,
|
|
sourceNode,
|
|
peerEdges,
|
|
nodes,
|
|
sourceNodeId,
|
|
targetNodeId,
|
|
minLineClearance))
|
|
{
|
|
if (!PathChanged(path, candidatePath)
|
|
|| candidatePath.Count < 2
|
|
|| HasNodeObstacleCrossing(candidatePath, nodes, sourceNodeId, targetNodeId)
|
|
|| !CanAcceptGatewayTargetRepair(candidatePath, targetNode)
|
|
|| !HasAcceptableGatewayBoundaryPath(candidatePath, nodes, sourceNodeId, targetNodeId, targetNode, fromStart: false))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var localCandidateEdge = BuildSingleSectionEdge(candidateEdge, candidatePath);
|
|
var localBundle = peerEdges
|
|
.Append(localCandidateEdge)
|
|
.ToArray();
|
|
var candidateTargetJoinViolations = ElkEdgeRoutingScoring.CountTargetApproachJoinViolations(localBundle, nodes);
|
|
var candidateSharedLaneViolations = ElkEdgeRoutingScoring.CountSharedLaneViolations(localBundle, nodes);
|
|
var candidateUnderNodeSegments = CountUnderNodeSegments(candidatePath, nodes, sourceNodeId, targetNodeId, minLineClearance);
|
|
var candidateUnderNodeViolations = ElkEdgeRoutingScoring.CountUnderNodeViolations([localCandidateEdge], nodes);
|
|
var candidateLocalHardPressure = ComputeUnderNodeRepairLocalHardPressure(localCandidateEdge, nodes);
|
|
var candidatePathLength = ComputePathLength(candidatePath);
|
|
|
|
if (!IsBetterGatewayUnderNodePeerConflictCandidate(
|
|
candidateTargetJoinViolations,
|
|
candidateSharedLaneViolations,
|
|
candidateUnderNodeSegments,
|
|
candidateUnderNodeViolations,
|
|
candidateLocalHardPressure,
|
|
candidatePathLength,
|
|
bestTargetJoinViolations,
|
|
bestSharedLaneViolations,
|
|
bestUnderNodeSegments,
|
|
bestUnderNodeViolations,
|
|
bestLocalHardPressure,
|
|
bestPathLength))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
bestEdge = localCandidateEdge;
|
|
bestTargetJoinViolations = candidateTargetJoinViolations;
|
|
bestSharedLaneViolations = candidateSharedLaneViolations;
|
|
bestUnderNodeSegments = candidateUnderNodeSegments;
|
|
bestUnderNodeViolations = candidateUnderNodeViolations;
|
|
bestLocalHardPressure = candidateLocalHardPressure;
|
|
bestPathLength = candidatePathLength;
|
|
}
|
|
|
|
if (bestEdge is null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
polishedEdge = bestEdge;
|
|
return true;
|
|
}
|
|
|
|
private static bool TryPolishRectUnderNodeTargetPeerConflicts(
|
|
ElkRoutedEdge candidateEdge,
|
|
IReadOnlyList<ElkRoutedEdge> currentEdges,
|
|
int candidateIndex,
|
|
ElkPositionedNode[] nodes,
|
|
double minLineClearance,
|
|
out ElkRoutedEdge polishedEdge)
|
|
{
|
|
polishedEdge = candidateEdge;
|
|
if (string.IsNullOrWhiteSpace(candidateEdge.TargetNodeId))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal);
|
|
if (!nodesById.TryGetValue(candidateEdge.TargetNodeId, out var targetNode)
|
|
|| ElkShapeBoundaries.IsGatewayShape(targetNode))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var peerEdges = currentEdges
|
|
.Where((edge, index) =>
|
|
index != candidateIndex
|
|
&& string.Equals(edge.TargetNodeId, candidateEdge.TargetNodeId, StringComparison.Ordinal))
|
|
.ToArray();
|
|
if (peerEdges.Length == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var currentBundle = peerEdges
|
|
.Append(candidateEdge)
|
|
.ToArray();
|
|
if (ElkEdgeRoutingScoring.CountTargetApproachJoinViolations(currentBundle, nodes) == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var path = ExtractFullPath(candidateEdge);
|
|
if (path.Count < 2)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var sourceNodeId = candidateEdge.SourceNodeId;
|
|
var targetNodeId = candidateEdge.TargetNodeId;
|
|
var currentUnderNodeSegments = CountUnderNodeSegments(path, nodes, sourceNodeId, targetNodeId, minLineClearance);
|
|
var currentSide = ResolveTargetApproachSide(path, targetNode);
|
|
var bestScore = double.PositiveInfinity;
|
|
ElkRoutedEdge? bestEdge = null;
|
|
|
|
foreach (var side in EnumerateRectTargetPeerConflictSides(path, targetNode, currentSide))
|
|
{
|
|
var axisCandidates = EnumerateRectTargetPeerConflictAxes(path, targetNode, side, minLineClearance).ToArray();
|
|
if (axisCandidates.Length == 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var boundaryCoordinates = EnumerateRectTargetPeerConflictBoundaryCoordinates(path, targetNode, side).ToArray();
|
|
if (boundaryCoordinates.Length == 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
foreach (var axis in axisCandidates)
|
|
{
|
|
foreach (var boundaryCoordinate in boundaryCoordinates)
|
|
{
|
|
var candidatePath = BuildMixedTargetFaceCandidate(path, targetNode, side, boundaryCoordinate, axis);
|
|
if (!PathChanged(path, candidatePath)
|
|
|| HasNodeObstacleCrossing(candidatePath, nodes, sourceNodeId, targetNodeId)
|
|
|| HasTargetApproachBacktracking(candidatePath, targetNode)
|
|
|| !HasValidBoundaryAngle(candidatePath[^1], candidatePath[^2], targetNode))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var candidateUnderNodeSegments = CountUnderNodeSegments(candidatePath, nodes, sourceNodeId, targetNodeId, minLineClearance);
|
|
if (candidateUnderNodeSegments > currentUnderNodeSegments)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var localCandidateEdge = BuildSingleSectionEdge(candidateEdge, candidatePath);
|
|
var localBundle = peerEdges
|
|
.Append(localCandidateEdge)
|
|
.ToArray();
|
|
if (ElkEdgeRoutingScoring.CountTargetApproachJoinViolations(localBundle, nodes) > 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var score = ComputeRectTargetPeerConflictPolishScore(candidatePath, currentSide, side);
|
|
if (score >= bestScore)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
bestScore = score;
|
|
bestEdge = localCandidateEdge;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (bestEdge is null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
polishedEdge = bestEdge;
|
|
return true;
|
|
}
|
|
|
|
private static IEnumerable<List<ElkPoint>> EnumerateGatewayUnderNodePeerConflictCandidates(
|
|
IReadOnlyList<ElkPoint> path,
|
|
ElkPositionedNode targetNode,
|
|
ElkPositionedNode? sourceNode,
|
|
IReadOnlyCollection<ElkRoutedEdge> peerEdges,
|
|
IReadOnlyCollection<ElkPositionedNode> nodes,
|
|
string? sourceNodeId,
|
|
string? targetNodeId,
|
|
double minLineClearance)
|
|
{
|
|
foreach (var side in EnumerateGatewayUnderNodePeerConflictSides(path, targetNode, peerEdges))
|
|
{
|
|
var slotCoordinates = EnumerateGatewayUnderNodePeerConflictSlotCoordinates(
|
|
path,
|
|
targetNode,
|
|
sourceNode,
|
|
peerEdges,
|
|
side,
|
|
minLineClearance)
|
|
.ToArray();
|
|
if (slotCoordinates.Length == 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
foreach (var slotCoordinate in slotCoordinates)
|
|
{
|
|
if (sourceNode is not null
|
|
&& ElkShapeBoundaries.TryProjectGatewayBoundarySlot(targetNode, side, slotCoordinate, out var bandBoundary)
|
|
&& TryBuildSafeHorizontalBandCandidate(
|
|
sourceNode,
|
|
targetNode,
|
|
nodes,
|
|
sourceNodeId,
|
|
targetNodeId,
|
|
path[0],
|
|
bandBoundary,
|
|
minLineClearance,
|
|
preferredSourceExterior: null,
|
|
out var bandCandidate))
|
|
{
|
|
yield return bandCandidate;
|
|
}
|
|
|
|
foreach (var axis in EnumerateGatewayUnderNodePeerConflictAxes(
|
|
path,
|
|
targetNode,
|
|
side,
|
|
nodes,
|
|
sourceNodeId,
|
|
targetNodeId,
|
|
minLineClearance))
|
|
{
|
|
yield return BuildMixedTargetFaceCandidate(path, targetNode, side, slotCoordinate, axis);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private static IEnumerable<string> EnumerateGatewayUnderNodePeerConflictSides(
|
|
IReadOnlyList<ElkPoint> path,
|
|
ElkPositionedNode targetNode,
|
|
IReadOnlyCollection<ElkRoutedEdge> peerEdges)
|
|
{
|
|
var seen = new HashSet<string>(StringComparer.Ordinal);
|
|
var currentSide = ResolveTargetApproachSide(path, targetNode);
|
|
var peerSides = peerEdges
|
|
.Select(edge => ExtractFullPath(edge))
|
|
.Where(peerPath => peerPath.Count >= 2)
|
|
.Select(peerPath => ResolveTargetApproachSide(peerPath, targetNode))
|
|
.ToHashSet(StringComparer.Ordinal);
|
|
|
|
foreach (var side in new[] { "top", "bottom", "right", "left" })
|
|
{
|
|
if (!string.Equals(side, currentSide, StringComparison.Ordinal)
|
|
&& !peerSides.Contains(side)
|
|
&& seen.Add(side))
|
|
{
|
|
yield return side;
|
|
}
|
|
}
|
|
|
|
if (seen.Add(currentSide))
|
|
{
|
|
yield return currentSide;
|
|
}
|
|
|
|
foreach (var side in new[] { "top", "bottom", "right", "left" })
|
|
{
|
|
if (seen.Add(side))
|
|
{
|
|
yield return side;
|
|
}
|
|
}
|
|
}
|
|
|
|
private static IEnumerable<double> EnumerateGatewayUnderNodePeerConflictSlotCoordinates(
|
|
IReadOnlyList<ElkPoint> path,
|
|
ElkPositionedNode targetNode,
|
|
ElkPositionedNode? sourceNode,
|
|
IReadOnlyCollection<ElkRoutedEdge> peerEdges,
|
|
string side,
|
|
double minLineClearance)
|
|
{
|
|
var coordinates = new List<double>();
|
|
var inset = 10d;
|
|
var spacing = Math.Max(14d, minLineClearance + 6d);
|
|
var centerX = targetNode.X + (targetNode.Width / 2d);
|
|
var centerY = targetNode.Y + (targetNode.Height / 2d);
|
|
var slotMinimum = side is "left" or "right" ? targetNode.Y + inset : targetNode.X + inset;
|
|
var slotMaximum = side is "left" or "right"
|
|
? targetNode.Y + targetNode.Height - inset
|
|
: targetNode.X + targetNode.Width - inset;
|
|
|
|
void AddClamped(double value)
|
|
{
|
|
AddUniqueCoordinate(coordinates, Math.Max(slotMinimum, Math.Min(slotMaximum, value)));
|
|
}
|
|
|
|
if (side is "left" or "right")
|
|
{
|
|
AddClamped(path[^1].Y);
|
|
foreach (var peer in peerEdges)
|
|
{
|
|
var peerPath = ExtractFullPath(peer);
|
|
if (peerPath.Count > 0)
|
|
{
|
|
AddClamped(peerPath[^1].Y - spacing);
|
|
AddClamped(peerPath[^1].Y + spacing);
|
|
AddClamped(peerPath[^1].Y);
|
|
}
|
|
}
|
|
|
|
if (sourceNode is not null)
|
|
{
|
|
AddClamped(sourceNode.Y + (sourceNode.Height / 2d));
|
|
}
|
|
|
|
AddClamped(centerY - spacing);
|
|
AddClamped(centerY);
|
|
AddClamped(centerY + spacing);
|
|
}
|
|
else
|
|
{
|
|
AddClamped(path[^1].X);
|
|
foreach (var peer in peerEdges)
|
|
{
|
|
var peerPath = ExtractFullPath(peer);
|
|
if (peerPath.Count > 0)
|
|
{
|
|
AddClamped(peerPath[^1].X - spacing);
|
|
AddClamped(peerPath[^1].X + spacing);
|
|
AddClamped(peerPath[^1].X);
|
|
}
|
|
}
|
|
|
|
if (sourceNode is not null)
|
|
{
|
|
AddClamped(sourceNode.X + (sourceNode.Width / 2d));
|
|
}
|
|
|
|
AddClamped(centerX - spacing);
|
|
AddClamped(centerX);
|
|
AddClamped(centerX + spacing);
|
|
}
|
|
|
|
foreach (var coordinate in coordinates.Take(8))
|
|
{
|
|
yield return coordinate;
|
|
}
|
|
}
|
|
|
|
private static IEnumerable<double> EnumerateGatewayUnderNodePeerConflictAxes(
|
|
IReadOnlyList<ElkPoint> path,
|
|
ElkPositionedNode targetNode,
|
|
string side,
|
|
IReadOnlyCollection<ElkPositionedNode> nodes,
|
|
string? sourceNodeId,
|
|
string? targetNodeId,
|
|
double minLineClearance)
|
|
{
|
|
var coordinates = new List<double>();
|
|
var currentAxis = ResolveTargetApproachAxisValue(path, side);
|
|
if (!double.IsNaN(currentAxis))
|
|
{
|
|
AddUniqueCoordinate(coordinates, currentAxis);
|
|
}
|
|
|
|
AddUniqueCoordinate(coordinates, ResolveDefaultTargetApproachAxis(targetNode, side));
|
|
|
|
var clearance = Math.Max(24d, minLineClearance * 0.6d);
|
|
if (side is "top" or "bottom")
|
|
{
|
|
var minX = Math.Min(path[0].X, targetNode.X);
|
|
var maxX = Math.Max(path[0].X, targetNode.X + targetNode.Width);
|
|
var blockers = nodes
|
|
.Where(node =>
|
|
!string.Equals(node.Id, sourceNodeId, StringComparison.Ordinal)
|
|
&& !string.Equals(node.Id, targetNodeId, StringComparison.Ordinal)
|
|
&& maxX > node.X + 0.5d
|
|
&& minX < node.X + node.Width - 0.5d)
|
|
.ToArray();
|
|
if (side == "top")
|
|
{
|
|
var highestBlockerY = blockers.Length > 0
|
|
? blockers.Min(node => node.Y)
|
|
: Math.Min(path[0].Y, targetNode.Y);
|
|
AddUniqueCoordinate(coordinates, Math.Min(targetNode.Y - 8d, highestBlockerY - clearance));
|
|
}
|
|
else
|
|
{
|
|
var lowestBlockerY = blockers.Length > 0
|
|
? blockers.Max(node => node.Y + node.Height)
|
|
: Math.Max(path[0].Y, targetNode.Y + targetNode.Height);
|
|
AddUniqueCoordinate(coordinates, Math.Max(targetNode.Y + targetNode.Height + 8d, lowestBlockerY + clearance));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
var minY = Math.Min(path[0].Y, targetNode.Y);
|
|
var maxY = Math.Max(path[0].Y, targetNode.Y + targetNode.Height);
|
|
var blockers = nodes
|
|
.Where(node =>
|
|
!string.Equals(node.Id, sourceNodeId, StringComparison.Ordinal)
|
|
&& !string.Equals(node.Id, targetNodeId, StringComparison.Ordinal)
|
|
&& maxY > node.Y + 0.5d
|
|
&& minY < node.Y + node.Height - 0.5d)
|
|
.ToArray();
|
|
if (side == "left")
|
|
{
|
|
var leftmostBlockerX = blockers.Length > 0
|
|
? blockers.Min(node => node.X)
|
|
: Math.Min(path[0].X, targetNode.X);
|
|
AddUniqueCoordinate(coordinates, Math.Min(targetNode.X - 8d, leftmostBlockerX - clearance));
|
|
}
|
|
else
|
|
{
|
|
var rightmostBlockerX = blockers.Length > 0
|
|
? blockers.Max(node => node.X + node.Width)
|
|
: Math.Max(path[0].X, targetNode.X + targetNode.Width);
|
|
AddUniqueCoordinate(coordinates, Math.Max(targetNode.X + targetNode.Width + 8d, rightmostBlockerX + clearance));
|
|
}
|
|
}
|
|
|
|
foreach (var coordinate in coordinates.Take(6))
|
|
{
|
|
yield return coordinate;
|
|
}
|
|
}
|
|
|
|
private static bool IsBetterGatewayUnderNodePeerConflictCandidate(
|
|
int candidateTargetJoinViolations,
|
|
int candidateSharedLaneViolations,
|
|
int candidateUnderNodeSegments,
|
|
int candidateUnderNodeViolations,
|
|
int candidateLocalHardPressure,
|
|
double candidatePathLength,
|
|
int currentTargetJoinViolations,
|
|
int currentSharedLaneViolations,
|
|
int currentUnderNodeSegments,
|
|
int currentUnderNodeViolations,
|
|
int currentLocalHardPressure,
|
|
double currentPathLength)
|
|
{
|
|
if (candidateTargetJoinViolations != currentTargetJoinViolations)
|
|
{
|
|
return candidateTargetJoinViolations < currentTargetJoinViolations;
|
|
}
|
|
|
|
if (candidateUnderNodeViolations != currentUnderNodeViolations)
|
|
{
|
|
return candidateUnderNodeViolations < currentUnderNodeViolations;
|
|
}
|
|
|
|
if (candidateUnderNodeSegments != currentUnderNodeSegments)
|
|
{
|
|
return candidateUnderNodeSegments < currentUnderNodeSegments;
|
|
}
|
|
|
|
if (candidateSharedLaneViolations != currentSharedLaneViolations)
|
|
{
|
|
return candidateSharedLaneViolations < currentSharedLaneViolations;
|
|
}
|
|
|
|
if (candidateLocalHardPressure != currentLocalHardPressure)
|
|
{
|
|
return candidateLocalHardPressure < currentLocalHardPressure;
|
|
}
|
|
|
|
return candidatePathLength + 0.5d < currentPathLength;
|
|
}
|
|
|
|
private static IEnumerable<string> EnumerateRectTargetPeerConflictSides(
|
|
IReadOnlyList<ElkPoint> path,
|
|
ElkPositionedNode targetNode,
|
|
string currentSide)
|
|
{
|
|
var seen = new HashSet<string>(StringComparer.Ordinal);
|
|
const double tolerance = 0.5d;
|
|
|
|
if (path.Any(point => point.Y < targetNode.Y - tolerance) && seen.Add("top"))
|
|
{
|
|
yield return "top";
|
|
}
|
|
|
|
if (path.Any(point => point.Y > targetNode.Y + targetNode.Height + tolerance) && seen.Add("bottom"))
|
|
{
|
|
yield return "bottom";
|
|
}
|
|
|
|
if (seen.Add(currentSide))
|
|
{
|
|
yield return currentSide;
|
|
}
|
|
}
|
|
|
|
private static IEnumerable<double> EnumerateRectTargetPeerConflictAxes(
|
|
IReadOnlyList<ElkPoint> path,
|
|
ElkPositionedNode targetNode,
|
|
string side,
|
|
double minLineClearance)
|
|
{
|
|
var coordinates = new List<double>();
|
|
var clearance = Math.Max(24d, minLineClearance * 0.6d);
|
|
const double tolerance = 0.5d;
|
|
|
|
switch (side)
|
|
{
|
|
case "top":
|
|
foreach (var value in path
|
|
.Select(point => point.Y)
|
|
.Where(coordinate => coordinate < targetNode.Y - tolerance)
|
|
.OrderByDescending(coordinate => coordinate))
|
|
{
|
|
AddUniqueCoordinate(coordinates, value);
|
|
}
|
|
|
|
AddUniqueCoordinate(coordinates, targetNode.Y - clearance);
|
|
break;
|
|
|
|
case "bottom":
|
|
foreach (var value in path
|
|
.Select(point => point.Y)
|
|
.Where(coordinate => coordinate > targetNode.Y + targetNode.Height + tolerance)
|
|
.OrderBy(coordinate => coordinate))
|
|
{
|
|
AddUniqueCoordinate(coordinates, value);
|
|
}
|
|
|
|
AddUniqueCoordinate(coordinates, targetNode.Y + targetNode.Height + clearance);
|
|
break;
|
|
|
|
case "left":
|
|
foreach (var value in path
|
|
.Select(point => point.X)
|
|
.Where(coordinate => coordinate < targetNode.X - tolerance)
|
|
.OrderByDescending(coordinate => coordinate))
|
|
{
|
|
AddUniqueCoordinate(coordinates, value);
|
|
}
|
|
|
|
AddUniqueCoordinate(coordinates, targetNode.X - clearance);
|
|
break;
|
|
|
|
case "right":
|
|
foreach (var value in path
|
|
.Select(point => point.X)
|
|
.Where(coordinate => coordinate > targetNode.X + targetNode.Width + tolerance)
|
|
.OrderBy(coordinate => coordinate))
|
|
{
|
|
AddUniqueCoordinate(coordinates, value);
|
|
}
|
|
|
|
AddUniqueCoordinate(coordinates, targetNode.X + targetNode.Width + clearance);
|
|
break;
|
|
}
|
|
|
|
foreach (var coordinate in coordinates.Take(6))
|
|
{
|
|
yield return coordinate;
|
|
}
|
|
}
|
|
|
|
private static IEnumerable<double> EnumerateRectTargetPeerConflictBoundaryCoordinates(
|
|
IReadOnlyList<ElkPoint> path,
|
|
ElkPositionedNode targetNode,
|
|
string side)
|
|
{
|
|
var coordinates = new List<double>();
|
|
var insetX = Math.Min(24d, Math.Max(8d, targetNode.Width / 4d));
|
|
var insetY = Math.Min(24d, Math.Max(8d, targetNode.Height / 4d));
|
|
|
|
if (side is "top" or "bottom")
|
|
{
|
|
var referenceX = path.Count > 1 ? path[^2].X : path[^1].X;
|
|
AddUniqueCoordinate(coordinates, referenceX);
|
|
AddUniqueCoordinate(coordinates, targetNode.X + insetX);
|
|
AddUniqueCoordinate(coordinates, targetNode.X + (targetNode.Width / 2d));
|
|
AddUniqueCoordinate(coordinates, targetNode.X + targetNode.Width - insetX);
|
|
|
|
foreach (var coordinate in coordinates
|
|
.OrderBy(value => Math.Abs(Math.Clamp(value, targetNode.X + insetX, (targetNode.X + targetNode.Width) - insetX) - referenceX))
|
|
.Take(6))
|
|
{
|
|
yield return coordinate;
|
|
}
|
|
|
|
yield break;
|
|
}
|
|
|
|
var referenceY = path[^1].Y;
|
|
AddUniqueCoordinate(coordinates, referenceY);
|
|
AddUniqueCoordinate(coordinates, targetNode.Y + insetY);
|
|
AddUniqueCoordinate(coordinates, targetNode.Y + (targetNode.Height / 2d));
|
|
AddUniqueCoordinate(coordinates, targetNode.Y + targetNode.Height - insetY);
|
|
|
|
foreach (var coordinate in coordinates
|
|
.OrderBy(value => Math.Abs(Math.Clamp(value, targetNode.Y + insetY, (targetNode.Y + targetNode.Height) - insetY) - referenceY))
|
|
.Take(6))
|
|
{
|
|
yield return coordinate;
|
|
}
|
|
}
|
|
|
|
private static double ComputeRectTargetPeerConflictPolishScore(
|
|
IReadOnlyList<ElkPoint> candidatePath,
|
|
string currentSide,
|
|
string candidateSide)
|
|
{
|
|
var score = ComputePathLength(candidatePath)
|
|
+ (Math.Max(0, candidatePath.Count - 2) * 8d);
|
|
if (!string.Equals(currentSide, candidateSide, StringComparison.Ordinal))
|
|
{
|
|
score += 12d;
|
|
}
|
|
|
|
return score;
|
|
}
|
|
|
|
internal static ElkRoutedEdge[] SeparateSharedLaneConflicts(
|
|
ElkRoutedEdge[] edges,
|
|
ElkPositionedNode[] nodes,
|
|
double minLineClearance,
|
|
IReadOnlyCollection<string>? restrictedEdgeIds = null)
|
|
{
|
|
if (edges.Length < 2 || nodes.Length == 0)
|
|
{
|
|
return edges;
|
|
}
|
|
|
|
var result = edges.ToArray();
|
|
var graphMinY = nodes.Min(node => node.Y);
|
|
var graphMaxY = nodes.Max(node => node.Y + node.Height);
|
|
var restrictedSet = restrictedEdgeIds is null
|
|
? null
|
|
: restrictedEdgeIds.ToHashSet(StringComparer.Ordinal);
|
|
var nodeObstacles = nodes.Select(node => (
|
|
Left: node.X,
|
|
Top: node.Y,
|
|
Right: node.X + node.Width,
|
|
Bottom: node.Y + node.Height,
|
|
Id: node.Id)).ToArray();
|
|
|
|
var conflicts = ElkEdgeRoutingScoring.DetectSharedLaneConflicts(result, nodes)
|
|
.Where(conflict => restrictedSet is null
|
|
|| restrictedSet.Contains(conflict.LeftEdgeId)
|
|
|| restrictedSet.Contains(conflict.RightEdgeId))
|
|
.Distinct()
|
|
.ToArray();
|
|
foreach (var conflict in conflicts)
|
|
{
|
|
var leftIndex = Array.FindIndex(result, edge => string.Equals(edge.Id, conflict.LeftEdgeId, StringComparison.Ordinal));
|
|
var rightIndex = Array.FindIndex(result, edge => string.Equals(edge.Id, conflict.RightEdgeId, StringComparison.Ordinal));
|
|
if (leftIndex < 0 || rightIndex < 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var leftEdge = result[leftIndex];
|
|
var rightEdge = result[rightIndex];
|
|
if (TryResolveSharedLaneByPairedNodeHandoffSlotRepair(
|
|
result,
|
|
leftIndex,
|
|
leftEdge,
|
|
rightIndex,
|
|
rightEdge,
|
|
nodes,
|
|
minLineClearance,
|
|
graphMinY,
|
|
graphMaxY,
|
|
out var pairedLeftEdge,
|
|
out var pairedRightEdge))
|
|
{
|
|
result[leftIndex] = pairedLeftEdge;
|
|
result[rightIndex] = pairedRightEdge;
|
|
continue;
|
|
}
|
|
|
|
var repairOrder = new[]
|
|
{
|
|
(Index: ComputePathLength(ExtractFullPath(leftEdge)) >= ComputePathLength(ExtractFullPath(rightEdge)) ? leftIndex : rightIndex,
|
|
Other: ComputePathLength(ExtractFullPath(leftEdge)) >= ComputePathLength(ExtractFullPath(rightEdge)) ? rightEdge : leftEdge),
|
|
(Index: ComputePathLength(ExtractFullPath(leftEdge)) >= ComputePathLength(ExtractFullPath(rightEdge)) ? rightIndex : leftIndex,
|
|
Other: ComputePathLength(ExtractFullPath(leftEdge)) >= ComputePathLength(ExtractFullPath(rightEdge)) ? leftEdge : rightEdge),
|
|
};
|
|
|
|
foreach (var repairCandidate in repairOrder)
|
|
{
|
|
if (TryResolveSharedLaneByAlternateRepeatFace(
|
|
result[repairCandidate.Index],
|
|
repairCandidate.Other,
|
|
nodes,
|
|
minLineClearance,
|
|
graphMinY,
|
|
graphMaxY,
|
|
out var alternateFaceEdge))
|
|
{
|
|
result[repairCandidate.Index] = alternateFaceEdge;
|
|
break;
|
|
}
|
|
|
|
if (TryResolveSharedLaneByDirectSourceSlotRepair(
|
|
result,
|
|
repairCandidate.Index,
|
|
result[repairCandidate.Index],
|
|
repairCandidate.Other,
|
|
nodes,
|
|
minLineClearance,
|
|
graphMinY,
|
|
graphMaxY,
|
|
out var directSourceSlotEdge))
|
|
{
|
|
result[repairCandidate.Index] = directSourceSlotEdge;
|
|
break;
|
|
}
|
|
|
|
if (TryResolveSharedLaneByDirectNodeHandoffSlotRepair(
|
|
result,
|
|
repairCandidate.Index,
|
|
result[repairCandidate.Index],
|
|
repairCandidate.Other,
|
|
nodes,
|
|
minLineClearance,
|
|
graphMinY,
|
|
graphMaxY,
|
|
out var directNodeHandoffEdge))
|
|
{
|
|
result[repairCandidate.Index] = directNodeHandoffEdge;
|
|
break;
|
|
}
|
|
|
|
if (TryResolveSharedLaneByFocusedSourceDepartureSpread(
|
|
result,
|
|
repairCandidate.Index,
|
|
result[repairCandidate.Index],
|
|
repairCandidate.Other,
|
|
nodes,
|
|
minLineClearance,
|
|
graphMinY,
|
|
graphMaxY,
|
|
out var sourceSpreadEdge))
|
|
{
|
|
result[repairCandidate.Index] = sourceSpreadEdge;
|
|
break;
|
|
}
|
|
|
|
if (TryResolveSharedLaneByFocusedMixedNodeFaceRepair(
|
|
result,
|
|
repairCandidate.Index,
|
|
result[repairCandidate.Index],
|
|
repairCandidate.Other,
|
|
nodes,
|
|
minLineClearance,
|
|
graphMinY,
|
|
graphMaxY,
|
|
out var mixedFaceEdge))
|
|
{
|
|
result[repairCandidate.Index] = mixedFaceEdge;
|
|
break;
|
|
}
|
|
|
|
if (!TrySeparateSharedLaneConflict(
|
|
result[repairCandidate.Index],
|
|
repairCandidate.Other,
|
|
nodes,
|
|
minLineClearance,
|
|
graphMinY,
|
|
graphMaxY,
|
|
nodeObstacles,
|
|
out var repairedEdge))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
result[repairCandidate.Index] = repairedEdge;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private static bool TryResolveSharedLaneByPairedNodeHandoffSlotRepair(
|
|
ElkRoutedEdge[] currentEdges,
|
|
int leftIndex,
|
|
ElkRoutedEdge leftEdge,
|
|
int rightIndex,
|
|
ElkRoutedEdge rightEdge,
|
|
ElkPositionedNode[] nodes,
|
|
double minLineClearance,
|
|
double graphMinY,
|
|
double graphMaxY,
|
|
out ElkRoutedEdge repairedLeftEdge,
|
|
out ElkRoutedEdge repairedRightEdge)
|
|
{
|
|
repairedLeftEdge = leftEdge;
|
|
repairedRightEdge = rightEdge;
|
|
var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal);
|
|
if (!TryResolveSharedLaneNodeHandoffContext(leftEdge, rightEdge, nodesById, graphMinY, graphMaxY, out var leftContext)
|
|
|| !TryResolveSharedLaneNodeHandoffContext(rightEdge, leftEdge, nodesById, graphMinY, graphMaxY, out var rightContext)
|
|
|| !string.Equals(leftContext.SharedNode.Id, rightContext.SharedNode.Id, StringComparison.Ordinal)
|
|
|| !string.Equals(leftContext.Side, rightContext.Side, StringComparison.Ordinal)
|
|
|| leftContext.IsOutgoing == rightContext.IsOutgoing)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var baselineConflicts = ElkEdgeRoutingScoring.DetectSharedLaneConflicts(currentEdges, nodes);
|
|
var baselineConflictCount = baselineConflicts.Count;
|
|
var baselineLeftConflictCount = baselineConflicts.Count(conflict =>
|
|
string.Equals(conflict.LeftEdgeId, leftEdge.Id, StringComparison.Ordinal)
|
|
|| string.Equals(conflict.RightEdgeId, leftEdge.Id, StringComparison.Ordinal));
|
|
var baselineRightConflictCount = baselineConflicts.Count(conflict =>
|
|
string.Equals(conflict.LeftEdgeId, rightEdge.Id, StringComparison.Ordinal)
|
|
|| string.Equals(conflict.RightEdgeId, rightEdge.Id, StringComparison.Ordinal));
|
|
var baselineCombinedPathLength = ComputePathLength(leftContext.Path) + ComputePathLength(rightContext.Path);
|
|
|
|
var peerCoordinates = CollectSharedLaneNodeFaceBoundaryCoordinates(
|
|
currentEdges,
|
|
leftContext.SharedNode,
|
|
leftContext.Side,
|
|
graphMinY,
|
|
graphMaxY,
|
|
leftEdge.Id);
|
|
var leftRepairCoordinates = EnumerateSharedLaneBoundaryRepairCoordinates(
|
|
leftContext.SharedNode,
|
|
leftContext.Side,
|
|
leftContext.CurrentBoundaryCoordinate,
|
|
minLineClearance,
|
|
peerCoordinates)
|
|
.ToArray();
|
|
var rightRepairCoordinates = EnumerateSharedLaneBoundaryRepairCoordinates(
|
|
rightContext.SharedNode,
|
|
rightContext.Side,
|
|
rightContext.CurrentBoundaryCoordinate,
|
|
minLineClearance,
|
|
peerCoordinates)
|
|
.ToArray();
|
|
|
|
ElkRoutedEdge? bestLeft = null;
|
|
ElkRoutedEdge? bestRight = null;
|
|
var bestConflictCount = baselineConflictCount;
|
|
var bestLeftConflictCount = baselineLeftConflictCount;
|
|
var bestRightConflictCount = baselineRightConflictCount;
|
|
var bestCombinedPathLength = baselineCombinedPathLength;
|
|
|
|
foreach (var leftCoordinate in leftRepairCoordinates)
|
|
{
|
|
var leftCandidatePath = leftContext.IsOutgoing
|
|
? BuildMixedSourceFaceCandidate(leftContext.Path, leftContext.SharedNode, leftContext.Side, leftCoordinate, leftContext.AxisValue)
|
|
: BuildMixedTargetFaceCandidate(leftContext.Path, leftContext.SharedNode, leftContext.Side, leftCoordinate, leftContext.AxisValue);
|
|
if (!IsValidSharedLaneBoundaryRepairCandidate(
|
|
leftEdge,
|
|
leftContext.Path,
|
|
leftCandidatePath,
|
|
leftContext.SharedNode,
|
|
leftContext.IsOutgoing,
|
|
nodes,
|
|
graphMinY,
|
|
graphMaxY))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
foreach (var rightCoordinate in rightRepairCoordinates)
|
|
{
|
|
var rightCandidatePath = rightContext.IsOutgoing
|
|
? BuildMixedSourceFaceCandidate(rightContext.Path, rightContext.SharedNode, rightContext.Side, rightCoordinate, rightContext.AxisValue)
|
|
: BuildMixedTargetFaceCandidate(rightContext.Path, rightContext.SharedNode, rightContext.Side, rightCoordinate, rightContext.AxisValue);
|
|
if (!IsValidSharedLaneBoundaryRepairCandidate(
|
|
rightEdge,
|
|
rightContext.Path,
|
|
rightCandidatePath,
|
|
rightContext.SharedNode,
|
|
rightContext.IsOutgoing,
|
|
nodes,
|
|
graphMinY,
|
|
graphMaxY))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var candidateLeft = BuildSingleSectionEdge(leftEdge, leftCandidatePath);
|
|
var candidateRight = BuildSingleSectionEdge(rightEdge, rightCandidatePath);
|
|
if (ElkEdgeRoutingScoring.DetectSharedLaneConflicts([candidateLeft, candidateRight], nodes).Count > 0
|
|
|| ComputeUnderNodeRepairLocalHardPressure(candidateLeft, nodes) > ComputeUnderNodeRepairLocalHardPressure(leftEdge, nodes)
|
|
|| ComputeUnderNodeRepairLocalHardPressure(candidateRight, nodes) > ComputeUnderNodeRepairLocalHardPressure(rightEdge, nodes))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var candidateEdges = currentEdges.ToArray();
|
|
candidateEdges[leftIndex] = candidateLeft;
|
|
candidateEdges[rightIndex] = candidateRight;
|
|
var candidateConflicts = ElkEdgeRoutingScoring.DetectSharedLaneConflicts(candidateEdges, nodes);
|
|
var candidateConflictCount = candidateConflicts.Count;
|
|
var candidateLeftConflictCount = candidateConflicts.Count(conflict =>
|
|
string.Equals(conflict.LeftEdgeId, leftEdge.Id, StringComparison.Ordinal)
|
|
|| string.Equals(conflict.RightEdgeId, leftEdge.Id, StringComparison.Ordinal));
|
|
var candidateRightConflictCount = candidateConflicts.Count(conflict =>
|
|
string.Equals(conflict.LeftEdgeId, rightEdge.Id, StringComparison.Ordinal)
|
|
|| string.Equals(conflict.RightEdgeId, rightEdge.Id, StringComparison.Ordinal));
|
|
if (candidateConflictCount > bestConflictCount
|
|
|| candidateLeftConflictCount > bestLeftConflictCount
|
|
|| candidateRightConflictCount > bestRightConflictCount)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var candidateCombinedPathLength = ComputePathLength(leftCandidatePath) + ComputePathLength(rightCandidatePath);
|
|
var isBetter =
|
|
candidateConflictCount < bestConflictCount
|
|
|| candidateLeftConflictCount < bestLeftConflictCount
|
|
|| candidateRightConflictCount < bestRightConflictCount
|
|
|| candidateCombinedPathLength + 0.5d < bestCombinedPathLength;
|
|
if (!isBetter)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
bestLeft = candidateLeft;
|
|
bestRight = candidateRight;
|
|
bestConflictCount = candidateConflictCount;
|
|
bestLeftConflictCount = candidateLeftConflictCount;
|
|
bestRightConflictCount = candidateRightConflictCount;
|
|
bestCombinedPathLength = candidateCombinedPathLength;
|
|
}
|
|
}
|
|
|
|
if (bestLeft is null || bestRight is null || bestConflictCount >= baselineConflictCount)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
repairedLeftEdge = bestLeft;
|
|
repairedRightEdge = bestRight;
|
|
return true;
|
|
}
|
|
|
|
private static bool TryResolveSharedLaneByDirectSourceSlotRepair(
|
|
ElkRoutedEdge[] currentEdges,
|
|
int repairIndex,
|
|
ElkRoutedEdge edge,
|
|
ElkRoutedEdge otherEdge,
|
|
ElkPositionedNode[] nodes,
|
|
double minLineClearance,
|
|
double graphMinY,
|
|
double graphMaxY,
|
|
out ElkRoutedEdge repairedEdge)
|
|
{
|
|
repairedEdge = edge;
|
|
if (string.IsNullOrWhiteSpace(edge.SourceNodeId)
|
|
|| !string.Equals(edge.SourceNodeId, otherEdge.SourceNodeId, StringComparison.Ordinal))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal);
|
|
if (!nodesById.TryGetValue(edge.SourceNodeId ?? string.Empty, out var sourceNode))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var path = ExtractFullPath(edge);
|
|
var otherPath = ExtractFullPath(otherEdge);
|
|
if (path.Count < 2
|
|
|| otherPath.Count < 2
|
|
|| !ShouldSpreadSourceDeparture(edge, graphMinY, graphMaxY)
|
|
|| !ShouldSpreadSourceDeparture(otherEdge, graphMinY, graphMaxY))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var side = ResolveSourceDepartureSide(path, sourceNode);
|
|
var otherSide = ResolveSourceDepartureSide(otherPath, sourceNode);
|
|
if (!string.Equals(side, otherSide, StringComparison.Ordinal))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var axisValue = TryExtractSourceDepartureRun(path, side, out _, out var runEndIndex)
|
|
? side is "left" or "right"
|
|
? path[runEndIndex].X
|
|
: path[runEndIndex].Y
|
|
: ResolveDefaultSourceDepartureAxis(sourceNode, side);
|
|
var currentBoundaryCoordinate = side is "left" or "right" ? path[0].Y : path[0].X;
|
|
var peerCoordinates = CollectSharedLaneSourceBoundaryCoordinates(
|
|
currentEdges,
|
|
sourceNode,
|
|
side,
|
|
graphMinY,
|
|
graphMaxY,
|
|
edge.Id);
|
|
|
|
foreach (var desiredCoordinate in EnumerateSharedLaneBoundaryRepairCoordinates(
|
|
sourceNode,
|
|
side,
|
|
currentBoundaryCoordinate,
|
|
minLineClearance,
|
|
peerCoordinates))
|
|
{
|
|
var candidatePath = BuildMixedSourceFaceCandidate(path, sourceNode, side, desiredCoordinate, axisValue);
|
|
if (!IsValidSharedLaneBoundaryRepairCandidate(
|
|
edge,
|
|
path,
|
|
candidatePath,
|
|
sourceNode,
|
|
isOutgoing: true,
|
|
nodes,
|
|
graphMinY,
|
|
graphMaxY))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var candidateEdges = currentEdges.ToArray();
|
|
candidateEdges[repairIndex] = BuildSingleSectionEdge(edge, candidatePath);
|
|
if (TryAcceptFocusedSharedLanePairRepair(
|
|
currentEdges,
|
|
candidateEdges,
|
|
repairIndex,
|
|
edge,
|
|
otherEdge,
|
|
nodes,
|
|
graphMinY,
|
|
graphMaxY,
|
|
out repairedEdge))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static bool TryResolveSharedLaneByDirectNodeHandoffSlotRepair(
|
|
ElkRoutedEdge[] currentEdges,
|
|
int repairIndex,
|
|
ElkRoutedEdge edge,
|
|
ElkRoutedEdge otherEdge,
|
|
ElkPositionedNode[] nodes,
|
|
double minLineClearance,
|
|
double graphMinY,
|
|
double graphMaxY,
|
|
out ElkRoutedEdge repairedEdge)
|
|
{
|
|
repairedEdge = edge;
|
|
var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal);
|
|
if (!TryResolveSharedLaneNodeHandoffContext(edge, otherEdge, nodesById, graphMinY, graphMaxY, out var context))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var peerCoordinates = CollectSharedLaneNodeFaceBoundaryCoordinates(
|
|
currentEdges,
|
|
context.SharedNode,
|
|
context.Side,
|
|
graphMinY,
|
|
graphMaxY,
|
|
edge.Id);
|
|
|
|
foreach (var desiredCoordinate in EnumerateSharedLaneBoundaryRepairCoordinates(
|
|
context.SharedNode,
|
|
context.Side,
|
|
context.CurrentBoundaryCoordinate,
|
|
minLineClearance,
|
|
peerCoordinates))
|
|
{
|
|
var candidatePath = context.IsOutgoing
|
|
? BuildMixedSourceFaceCandidate(context.Path, context.SharedNode, context.Side, desiredCoordinate, context.AxisValue)
|
|
: BuildMixedTargetFaceCandidate(context.Path, context.SharedNode, context.Side, desiredCoordinate, context.AxisValue);
|
|
if (!IsValidSharedLaneBoundaryRepairCandidate(
|
|
edge,
|
|
context.Path,
|
|
candidatePath,
|
|
context.SharedNode,
|
|
context.IsOutgoing,
|
|
nodes,
|
|
graphMinY,
|
|
graphMaxY))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var candidateEdges = currentEdges.ToArray();
|
|
candidateEdges[repairIndex] = BuildSingleSectionEdge(edge, candidatePath);
|
|
if (TryAcceptFocusedSharedLanePairRepair(
|
|
currentEdges,
|
|
candidateEdges,
|
|
repairIndex,
|
|
edge,
|
|
otherEdge,
|
|
nodes,
|
|
graphMinY,
|
|
graphMaxY,
|
|
out repairedEdge))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static bool TryResolveSharedLaneByFocusedSourceDepartureSpread(
|
|
ElkRoutedEdge[] currentEdges,
|
|
int repairIndex,
|
|
ElkRoutedEdge edge,
|
|
ElkRoutedEdge otherEdge,
|
|
ElkPositionedNode[] nodes,
|
|
double minLineClearance,
|
|
double graphMinY,
|
|
double graphMaxY,
|
|
out ElkRoutedEdge repairedEdge)
|
|
{
|
|
repairedEdge = edge;
|
|
if (string.IsNullOrWhiteSpace(edge.SourceNodeId)
|
|
|| !string.Equals(edge.SourceNodeId, otherEdge.SourceNodeId, StringComparison.Ordinal))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var focusedIds = new[] { edge.Id, otherEdge.Id };
|
|
var candidateEdges = SpreadSourceDepartureJoins(currentEdges, nodes, minLineClearance, focusedIds);
|
|
return TryAcceptFocusedSharedLanePairRepair(
|
|
currentEdges,
|
|
candidateEdges,
|
|
repairIndex,
|
|
edge,
|
|
otherEdge,
|
|
nodes,
|
|
graphMinY,
|
|
graphMaxY,
|
|
out repairedEdge);
|
|
}
|
|
|
|
private static bool TryResolveSharedLaneByFocusedMixedNodeFaceRepair(
|
|
ElkRoutedEdge[] currentEdges,
|
|
int repairIndex,
|
|
ElkRoutedEdge edge,
|
|
ElkRoutedEdge otherEdge,
|
|
ElkPositionedNode[] nodes,
|
|
double minLineClearance,
|
|
double graphMinY,
|
|
double graphMaxY,
|
|
out ElkRoutedEdge repairedEdge)
|
|
{
|
|
repairedEdge = edge;
|
|
var sharesIncomingOutgoingNode =
|
|
(!string.IsNullOrWhiteSpace(edge.TargetNodeId)
|
|
&& string.Equals(edge.TargetNodeId, otherEdge.SourceNodeId, StringComparison.Ordinal))
|
|
|| (!string.IsNullOrWhiteSpace(edge.SourceNodeId)
|
|
&& string.Equals(edge.SourceNodeId, otherEdge.TargetNodeId, StringComparison.Ordinal));
|
|
if (!sharesIncomingOutgoingNode)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var focusedIds = new[] { edge.Id, otherEdge.Id };
|
|
var candidateEdges = SeparateMixedNodeFaceLaneConflicts(currentEdges, nodes, minLineClearance, focusedIds);
|
|
return TryAcceptFocusedSharedLanePairRepair(
|
|
currentEdges,
|
|
candidateEdges,
|
|
repairIndex,
|
|
edge,
|
|
otherEdge,
|
|
nodes,
|
|
graphMinY,
|
|
graphMaxY,
|
|
out repairedEdge);
|
|
}
|
|
|
|
private static bool TryAcceptFocusedSharedLanePairRepair(
|
|
ElkRoutedEdge[] currentEdges,
|
|
ElkRoutedEdge[] candidateEdges,
|
|
int repairIndex,
|
|
ElkRoutedEdge edge,
|
|
ElkRoutedEdge otherEdge,
|
|
ElkPositionedNode[] nodes,
|
|
double graphMinY,
|
|
double graphMaxY,
|
|
out ElkRoutedEdge repairedEdge)
|
|
{
|
|
repairedEdge = edge;
|
|
if (repairIndex < 0 || repairIndex >= candidateEdges.Length)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var candidateEdge = candidateEdges[repairIndex];
|
|
var currentPath = ExtractFullPath(edge);
|
|
var candidatePath = ExtractFullPath(candidateEdge);
|
|
if (!PathChanged(currentPath, candidatePath)
|
|
|| HasNodeObstacleCrossing(candidatePath, nodes, edge.SourceNodeId, edge.TargetNodeId)
|
|
|| SegmentLeavesGraphBand(candidatePath, graphMinY, graphMaxY))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var currentSharedLaneConflicts = ElkEdgeRoutingScoring.DetectSharedLaneConflicts(currentEdges, nodes);
|
|
var candidateSharedLaneConflicts = ElkEdgeRoutingScoring.DetectSharedLaneConflicts(candidateEdges, nodes);
|
|
var currentSharedLaneCount = currentSharedLaneConflicts.Count;
|
|
var candidateSharedLaneCount = candidateSharedLaneConflicts.Count;
|
|
var currentEdgeSharedLaneCount = currentSharedLaneConflicts.Count(conflict =>
|
|
string.Equals(conflict.LeftEdgeId, edge.Id, StringComparison.Ordinal)
|
|
|| string.Equals(conflict.RightEdgeId, edge.Id, StringComparison.Ordinal));
|
|
var candidateEdgeSharedLaneCount = candidateSharedLaneConflicts.Count(conflict =>
|
|
string.Equals(conflict.LeftEdgeId, candidateEdge.Id, StringComparison.Ordinal)
|
|
|| string.Equals(conflict.RightEdgeId, candidateEdge.Id, StringComparison.Ordinal));
|
|
if (candidateSharedLaneCount > currentSharedLaneCount
|
|
|| candidateEdgeSharedLaneCount >= currentEdgeSharedLaneCount
|
|
|| ElkEdgeRoutingScoring.DetectSharedLaneConflicts([candidateEdge, otherEdge], nodes).Count > 0
|
|
|| ComputeUnderNodeRepairLocalHardPressure(candidateEdge, nodes) > ComputeUnderNodeRepairLocalHardPressure(edge, nodes))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
repairedEdge = candidateEdge;
|
|
return true;
|
|
}
|
|
|
|
private static IReadOnlyList<double> CollectSharedLaneSourceBoundaryCoordinates(
|
|
IReadOnlyCollection<ElkRoutedEdge> edges,
|
|
ElkPositionedNode sourceNode,
|
|
string side,
|
|
double graphMinY,
|
|
double graphMaxY,
|
|
string excludeEdgeId)
|
|
{
|
|
var coordinates = new List<double>();
|
|
foreach (var peerEdge in edges)
|
|
{
|
|
if (string.Equals(peerEdge.Id, excludeEdgeId, StringComparison.Ordinal)
|
|
|| !string.Equals(peerEdge.SourceNodeId, sourceNode.Id, StringComparison.Ordinal)
|
|
|| !ShouldSpreadSourceDeparture(peerEdge, graphMinY, graphMaxY))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var peerPath = ExtractFullPath(peerEdge);
|
|
if (peerPath.Count < 2)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var peerSide = ResolveSourceDepartureSide(peerPath, sourceNode);
|
|
if (!string.Equals(peerSide, side, StringComparison.Ordinal))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
AddUniqueCoordinate(coordinates, side is "left" or "right" ? peerPath[0].Y : peerPath[0].X);
|
|
}
|
|
|
|
return coordinates
|
|
.OrderBy(value => value)
|
|
.ToArray();
|
|
}
|
|
|
|
private static IReadOnlyList<double> CollectSharedLaneNodeFaceBoundaryCoordinates(
|
|
IReadOnlyCollection<ElkRoutedEdge> edges,
|
|
ElkPositionedNode node,
|
|
string side,
|
|
double graphMinY,
|
|
double graphMaxY,
|
|
string excludeEdgeId)
|
|
{
|
|
var coordinates = new List<double>();
|
|
foreach (var peerEdge in edges)
|
|
{
|
|
if (string.Equals(peerEdge.Id, excludeEdgeId, StringComparison.Ordinal))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var peerPath = ExtractFullPath(peerEdge);
|
|
if (peerPath.Count < 2)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (string.Equals(peerEdge.SourceNodeId, node.Id, StringComparison.Ordinal)
|
|
&& ShouldSpreadSourceDeparture(peerEdge, graphMinY, graphMaxY))
|
|
{
|
|
var peerSide = ResolveSourceDepartureSide(peerPath, node);
|
|
if (string.Equals(peerSide, side, StringComparison.Ordinal))
|
|
{
|
|
AddUniqueCoordinate(coordinates, side is "left" or "right" ? peerPath[0].Y : peerPath[0].X);
|
|
}
|
|
}
|
|
|
|
if (string.Equals(peerEdge.TargetNodeId, node.Id, StringComparison.Ordinal)
|
|
&& ShouldSpreadTargetApproach(peerEdge, graphMinY, graphMaxY))
|
|
{
|
|
var peerSide = ResolveTargetApproachSide(peerPath, node);
|
|
if (string.Equals(peerSide, side, StringComparison.Ordinal))
|
|
{
|
|
AddUniqueCoordinate(coordinates, side is "left" or "right" ? peerPath[^1].Y : peerPath[^1].X);
|
|
}
|
|
}
|
|
}
|
|
|
|
return coordinates
|
|
.OrderBy(value => value)
|
|
.ToArray();
|
|
}
|
|
|
|
private static IEnumerable<double> EnumerateSharedLaneBoundaryRepairCoordinates(
|
|
ElkPositionedNode node,
|
|
string side,
|
|
double currentCoordinate,
|
|
double minLineClearance,
|
|
IReadOnlyList<double> peerCoordinates)
|
|
{
|
|
var minSlot = side is "left" or "right" ? node.Y + 4d : node.X + 4d;
|
|
var maxSlot = side is "left" or "right" ? node.Y + node.Height - 4d : node.X + node.Width - 4d;
|
|
if (maxSlot <= minSlot + 0.5d)
|
|
{
|
|
yield break;
|
|
}
|
|
|
|
var sideLength = side is "left" or "right"
|
|
? Math.Max(8d, node.Height - 8d)
|
|
: Math.Max(8d, node.Width - 8d);
|
|
var spacing = ResolveBoundaryJoinSlotSpacing(
|
|
minLineClearance,
|
|
sideLength,
|
|
Math.Max(2, peerCoordinates.Count + 1));
|
|
// Direct pair repairs do not need full group-spacing. Trying a closer
|
|
// escape slot first keeps the edge away from the face corners and avoids
|
|
// manufacturing new boundary-angle / join defects while still clearing
|
|
// the shared-lane tolerance band.
|
|
var repairSpacing = Math.Max(
|
|
12d,
|
|
Math.Min(
|
|
spacing,
|
|
Math.Min(28d, minLineClearance * 0.45d)));
|
|
var candidates = new List<double>();
|
|
var sortedPeers = peerCoordinates
|
|
.OrderBy(value => value)
|
|
.ToArray();
|
|
|
|
AddUniqueCoordinate(candidates, Math.Max(minSlot, Math.Min(maxSlot, currentCoordinate - repairSpacing)));
|
|
AddUniqueCoordinate(candidates, Math.Max(minSlot, Math.Min(maxSlot, currentCoordinate + repairSpacing)));
|
|
AddUniqueCoordinate(candidates, Math.Max(minSlot, Math.Min(maxSlot, currentCoordinate - spacing)));
|
|
AddUniqueCoordinate(candidates, Math.Max(minSlot, Math.Min(maxSlot, currentCoordinate + spacing)));
|
|
AddUniqueCoordinate(candidates, minSlot);
|
|
AddUniqueCoordinate(candidates, maxSlot);
|
|
|
|
foreach (var peerCoordinate in sortedPeers)
|
|
{
|
|
AddUniqueCoordinate(candidates, Math.Max(minSlot, Math.Min(maxSlot, peerCoordinate - repairSpacing)));
|
|
AddUniqueCoordinate(candidates, Math.Max(minSlot, Math.Min(maxSlot, peerCoordinate + repairSpacing)));
|
|
AddUniqueCoordinate(candidates, Math.Max(minSlot, Math.Min(maxSlot, peerCoordinate - spacing)));
|
|
AddUniqueCoordinate(candidates, Math.Max(minSlot, Math.Min(maxSlot, peerCoordinate + spacing)));
|
|
}
|
|
|
|
for (var i = 0; i < sortedPeers.Length - 1; i++)
|
|
{
|
|
var midpoint = (sortedPeers[i] + sortedPeers[i + 1]) / 2d;
|
|
AddUniqueCoordinate(candidates, Math.Max(minSlot, Math.Min(maxSlot, midpoint)));
|
|
}
|
|
|
|
var laneTolerance = Math.Max(4d, Math.Min(12d, minLineClearance * 0.2d));
|
|
foreach (var coordinate in candidates
|
|
.Where(value => Math.Abs(value - currentCoordinate) > 0.5d)
|
|
.Select(value => new
|
|
{
|
|
Value = value,
|
|
TooCloseToPeer = sortedPeers.Any(peer => Math.Abs(peer - value) <= laneTolerance + 0.5d),
|
|
})
|
|
.OrderBy(item => item.TooCloseToPeer ? 1 : 0)
|
|
.ThenBy(item => Math.Abs(item.Value - currentCoordinate))
|
|
.ThenBy(item => item.Value))
|
|
{
|
|
yield return coordinate.Value;
|
|
}
|
|
}
|
|
|
|
private static void AddUniquePathCandidate(
|
|
ICollection<IReadOnlyList<ElkPoint>> candidates,
|
|
IReadOnlyList<ElkPoint> candidate)
|
|
{
|
|
if (candidates.Any(existing =>
|
|
existing.Count == candidate.Count
|
|
&& existing.Zip(candidate, ElkEdgeRoutingGeometry.PointsEqual).All(equal => equal)))
|
|
{
|
|
return;
|
|
}
|
|
|
|
candidates.Add(candidate);
|
|
}
|
|
|
|
private static bool IsBetterMixedNodeFaceCandidate(
|
|
int candidateSharedLaneViolations,
|
|
int candidateTargetJoinViolations,
|
|
int candidateBoundaryAngleViolations,
|
|
int candidateGatewaySourceExitViolations,
|
|
int candidateUnderNodeViolations,
|
|
double candidatePathLength,
|
|
int currentSharedLaneViolations,
|
|
int currentTargetJoinViolations,
|
|
int currentBoundaryAngleViolations,
|
|
int currentGatewaySourceExitViolations,
|
|
int currentUnderNodeViolations,
|
|
double currentPathLength)
|
|
{
|
|
if (candidateSharedLaneViolations != currentSharedLaneViolations)
|
|
{
|
|
return candidateSharedLaneViolations < currentSharedLaneViolations;
|
|
}
|
|
|
|
if (candidateTargetJoinViolations != currentTargetJoinViolations)
|
|
{
|
|
return candidateTargetJoinViolations < currentTargetJoinViolations;
|
|
}
|
|
|
|
if (candidateBoundaryAngleViolations != currentBoundaryAngleViolations)
|
|
{
|
|
return candidateBoundaryAngleViolations < currentBoundaryAngleViolations;
|
|
}
|
|
|
|
if (candidateGatewaySourceExitViolations != currentGatewaySourceExitViolations)
|
|
{
|
|
return candidateGatewaySourceExitViolations < currentGatewaySourceExitViolations;
|
|
}
|
|
|
|
if (candidateUnderNodeViolations != currentUnderNodeViolations)
|
|
{
|
|
return candidateUnderNodeViolations < currentUnderNodeViolations;
|
|
}
|
|
|
|
return candidatePathLength + 0.5d < currentPathLength;
|
|
}
|
|
|
|
private static bool TryResolveSharedLaneNodeHandoffContext(
|
|
ElkRoutedEdge edge,
|
|
ElkRoutedEdge otherEdge,
|
|
IReadOnlyDictionary<string, ElkPositionedNode> nodesById,
|
|
double graphMinY,
|
|
double graphMaxY,
|
|
out (ElkPositionedNode SharedNode, string Side, bool IsOutgoing, IReadOnlyList<ElkPoint> Path, double CurrentBoundaryCoordinate, double AxisValue) context)
|
|
{
|
|
context = default;
|
|
var path = ExtractFullPath(edge);
|
|
var otherPath = ExtractFullPath(otherEdge);
|
|
if (path.Count < 2 || otherPath.Count < 2)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(edge.TargetNodeId)
|
|
&& string.Equals(edge.TargetNodeId, otherEdge.SourceNodeId, StringComparison.Ordinal)
|
|
&& nodesById.TryGetValue(edge.TargetNodeId, out var incomingTargetNode)
|
|
&& ShouldSpreadTargetApproach(edge, graphMinY, graphMaxY)
|
|
&& ShouldSpreadSourceDeparture(otherEdge, graphMinY, graphMaxY))
|
|
{
|
|
var incomingSide = ResolveTargetApproachSide(path, incomingTargetNode);
|
|
var outgoingSide = ResolveSourceDepartureSide(otherPath, incomingTargetNode);
|
|
if (string.Equals(incomingSide, outgoingSide, StringComparison.Ordinal))
|
|
{
|
|
var axisValue = ResolveTargetApproachAxisValue(path, incomingSide);
|
|
if (double.IsNaN(axisValue))
|
|
{
|
|
axisValue = ResolveDefaultTargetApproachAxis(incomingTargetNode, incomingSide);
|
|
}
|
|
|
|
context = (
|
|
incomingTargetNode,
|
|
incomingSide,
|
|
IsOutgoing: false,
|
|
path,
|
|
incomingSide is "left" or "right" ? path[^1].Y : path[^1].X,
|
|
axisValue);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(edge.SourceNodeId)
|
|
&& string.Equals(edge.SourceNodeId, otherEdge.TargetNodeId, StringComparison.Ordinal)
|
|
&& nodesById.TryGetValue(edge.SourceNodeId, out var outgoingSourceNode)
|
|
&& ShouldSpreadSourceDeparture(edge, graphMinY, graphMaxY)
|
|
&& ShouldSpreadTargetApproach(otherEdge, graphMinY, graphMaxY))
|
|
{
|
|
var outgoingSide = ResolveSourceDepartureSide(path, outgoingSourceNode);
|
|
var incomingSide = ResolveTargetApproachSide(otherPath, outgoingSourceNode);
|
|
if (string.Equals(outgoingSide, incomingSide, StringComparison.Ordinal))
|
|
{
|
|
var axisValue = TryExtractSourceDepartureRun(path, outgoingSide, out _, out var runEndIndex)
|
|
? outgoingSide is "left" or "right"
|
|
? path[runEndIndex].X
|
|
: path[runEndIndex].Y
|
|
: ResolveDefaultSourceDepartureAxis(outgoingSourceNode, outgoingSide);
|
|
|
|
context = (
|
|
outgoingSourceNode,
|
|
outgoingSide,
|
|
IsOutgoing: true,
|
|
path,
|
|
outgoingSide is "left" or "right" ? path[0].Y : path[0].X,
|
|
axisValue);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static bool IsValidSharedLaneBoundaryRepairCandidate(
|
|
ElkRoutedEdge edge,
|
|
IReadOnlyList<ElkPoint> currentPath,
|
|
IReadOnlyList<ElkPoint> candidatePath,
|
|
ElkPositionedNode node,
|
|
bool isOutgoing,
|
|
IReadOnlyCollection<ElkPositionedNode> nodes,
|
|
double graphMinY,
|
|
double graphMaxY)
|
|
{
|
|
if (!PathChanged(currentPath, candidatePath)
|
|
|| HasNodeObstacleCrossing(candidatePath, nodes, edge.SourceNodeId, edge.TargetNodeId)
|
|
|| SegmentLeavesGraphBand(candidatePath, graphMinY, graphMaxY))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (isOutgoing)
|
|
{
|
|
if (ElkShapeBoundaries.IsGatewayShape(node))
|
|
{
|
|
return HasAcceptableGatewayBoundaryPath(candidatePath, nodes, edge.SourceNodeId, edge.TargetNodeId, node, fromStart: true);
|
|
}
|
|
|
|
return HasClearBoundarySegments(candidatePath, nodes, edge.SourceNodeId, edge.TargetNodeId, true, 2)
|
|
&& HasValidBoundaryAngle(candidatePath[0], candidatePath[1], node);
|
|
}
|
|
|
|
if (ElkShapeBoundaries.IsGatewayShape(node))
|
|
{
|
|
return CanAcceptGatewayTargetRepair(candidatePath, node)
|
|
&& HasAcceptableGatewayBoundaryPath(candidatePath, nodes, edge.SourceNodeId, edge.TargetNodeId, node, fromStart: false);
|
|
}
|
|
|
|
return HasClearBoundarySegments(candidatePath, nodes, edge.SourceNodeId, edge.TargetNodeId, false, 4)
|
|
&& HasValidBoundaryAngle(candidatePath[^1], candidatePath[^2], node)
|
|
&& !HasTargetApproachBacktracking(candidatePath, node);
|
|
}
|
|
|
|
private static bool TryResolveSharedLaneByAlternateRepeatFace(
|
|
ElkRoutedEdge edge,
|
|
ElkRoutedEdge otherEdge,
|
|
ElkPositionedNode[] nodes,
|
|
double minLineClearance,
|
|
double graphMinY,
|
|
double graphMaxY,
|
|
out ElkRoutedEdge repairedEdge)
|
|
{
|
|
repairedEdge = edge;
|
|
if (!IsRepeatCollectorLabel(edge.Label))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal);
|
|
if (!nodesById.TryGetValue(edge.TargetNodeId ?? string.Empty, out var targetNode)
|
|
|| !nodesById.TryGetValue(otherEdge.SourceNodeId ?? string.Empty, out var sourceNode)
|
|
|| !string.Equals(targetNode.Id, sourceNode.Id, StringComparison.Ordinal)
|
|
|| ElkShapeBoundaries.IsGatewayShape(targetNode))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var path = ExtractFullPath(edge);
|
|
var otherPath = ExtractFullPath(otherEdge);
|
|
if (path.Count < 2 || otherPath.Count < 2)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var incomingSide = ResolveTargetApproachSide(path, targetNode);
|
|
var outgoingSide = ResolveSourceDepartureSide(otherPath, sourceNode);
|
|
if (!string.Equals(incomingSide, outgoingSide, StringComparison.Ordinal))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var axisValue = ResolveTargetApproachAxisValue(path, incomingSide);
|
|
if (double.IsNaN(axisValue))
|
|
{
|
|
axisValue = incomingSide is "left" or "right" ? path[^1].Y : path[^1].X;
|
|
}
|
|
|
|
var incomingEntry = (
|
|
Index: 0,
|
|
Edge: edge,
|
|
Path: (IReadOnlyList<ElkPoint>)path,
|
|
Node: targetNode,
|
|
Side: incomingSide,
|
|
IsOutgoing: false,
|
|
Boundary: path[^1],
|
|
BoundaryCoordinate: incomingSide is "left" or "right" ? path[^1].Y : path[^1].X,
|
|
AxisValue: axisValue);
|
|
if (!TryBuildAlternateMixedFaceCandidate(incomingEntry, nodes, minLineClearance, out var candidate)
|
|
|| !PathChanged(path, candidate)
|
|
|| HasNodeObstacleCrossing(candidate, nodes, edge.SourceNodeId, edge.TargetNodeId)
|
|
|| SegmentLeavesGraphBand(candidate, graphMinY, graphMaxY)
|
|
|| !HasClearBoundarySegments(candidate, nodes, edge.SourceNodeId, edge.TargetNodeId, false, 4)
|
|
|| !HasValidBoundaryAngle(candidate[^1], candidate[^2], targetNode))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
repairedEdge = BuildSingleSectionEdge(edge, candidate);
|
|
var repairedPath = ExtractFullPath(repairedEdge);
|
|
if (!HasNodeObstacleCrossing(repairedPath, nodes, edge.SourceNodeId, edge.TargetNodeId)
|
|
&& !SegmentLeavesGraphBand(repairedPath, graphMinY, graphMaxY)
|
|
&& ElkEdgeRoutingScoring.DetectSharedLaneConflicts([repairedEdge, otherEdge], nodes).Count == 0)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
repairedEdge = RepairBoundaryAnglesAndTargetApproaches(
|
|
[repairedEdge],
|
|
nodes,
|
|
minLineClearance)[0];
|
|
repairedEdge = NormalizeSourceExitAngles([repairedEdge], nodes)[0];
|
|
repairedPath = ExtractFullPath(repairedEdge);
|
|
if (HasNodeObstacleCrossing(repairedPath, nodes, edge.SourceNodeId, edge.TargetNodeId)
|
|
|| SegmentLeavesGraphBand(repairedPath, graphMinY, graphMaxY)
|
|
|| ElkEdgeRoutingScoring.DetectSharedLaneConflicts([repairedEdge, otherEdge], nodes).Count > 0)
|
|
{
|
|
repairedEdge = edge;
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
internal static ElkRoutedEdge[] FinalizeGatewayBoundaryGeometry(
|
|
ElkRoutedEdge[] edges,
|
|
ElkPositionedNode[] nodes,
|
|
IReadOnlyCollection<string>? restrictedEdgeIds = null)
|
|
{
|
|
if (edges.Length == 0 || nodes.Length == 0)
|
|
{
|
|
return edges;
|
|
}
|
|
|
|
var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal);
|
|
var graphMinY = nodes.Min(node => node.Y);
|
|
var graphMaxY = nodes.Max(node => node.Y + node.Height);
|
|
var restrictedSet = restrictedEdgeIds is null
|
|
? null
|
|
: restrictedEdgeIds.ToHashSet(StringComparer.Ordinal);
|
|
var result = new ElkRoutedEdge[edges.Length];
|
|
|
|
for (var i = 0; i < edges.Length; i++)
|
|
{
|
|
var edge = edges[i];
|
|
if (restrictedSet is not null && !restrictedSet.Contains(edge.Id))
|
|
{
|
|
result[i] = edge;
|
|
continue;
|
|
}
|
|
|
|
var path = ExtractFullPath(edge);
|
|
if (path.Count < 2)
|
|
{
|
|
result[i] = edge;
|
|
continue;
|
|
}
|
|
|
|
var normalized = path
|
|
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
|
.ToList();
|
|
var changed = false;
|
|
|
|
var preserveSourceExit = ShouldPreserveSourceExitGeometry(edge, graphMinY, graphMaxY);
|
|
|
|
if (nodesById.TryGetValue(edge.SourceNodeId ?? string.Empty, out var sourceNode)
|
|
&& ElkShapeBoundaries.IsGatewayShape(sourceNode)
|
|
&& NeedsGatewaySourceBoundaryRepair(normalized, sourceNode))
|
|
{
|
|
var sourceRepaired = preserveSourceExit
|
|
? RepairProtectedGatewaySourceBoundaryPath(normalized, sourceNode, graphMinY, graphMaxY)
|
|
: RepairGatewaySourceBoundaryPath(
|
|
normalized,
|
|
sourceNode,
|
|
nodes,
|
|
edge.SourceNodeId,
|
|
edge.TargetNodeId);
|
|
if (PathChanged(normalized, sourceRepaired)
|
|
&& HasAcceptableGatewayBoundaryPath(sourceRepaired, nodes, edge.SourceNodeId, edge.TargetNodeId, sourceNode, fromStart: true))
|
|
{
|
|
normalized = sourceRepaired;
|
|
changed = true;
|
|
}
|
|
}
|
|
|
|
if (nodesById.TryGetValue(edge.TargetNodeId ?? string.Empty, out var targetNode)
|
|
&& ElkShapeBoundaries.IsGatewayShape(targetNode)
|
|
&& NeedsGatewayTargetBoundaryRepair(normalized, targetNode))
|
|
{
|
|
var targetRepaired = NormalizeGatewayEntryPath(normalized, targetNode, normalized[^1]);
|
|
if (PathChanged(normalized, targetRepaired)
|
|
&& CanAcceptGatewayTargetRepair(targetRepaired, targetNode))
|
|
{
|
|
normalized = targetRepaired;
|
|
changed = true;
|
|
}
|
|
}
|
|
|
|
if (nodesById.TryGetValue(edge.SourceNodeId ?? string.Empty, out sourceNode)
|
|
&& ElkShapeBoundaries.IsGatewayShape(sourceNode)
|
|
&& NeedsGatewaySourceBoundaryRepair(normalized, sourceNode))
|
|
{
|
|
var sourceRepaired = preserveSourceExit
|
|
? RepairProtectedGatewaySourceBoundaryPath(normalized, sourceNode, graphMinY, graphMaxY)
|
|
: RepairGatewaySourceBoundaryPath(
|
|
normalized,
|
|
sourceNode,
|
|
nodes,
|
|
edge.SourceNodeId,
|
|
edge.TargetNodeId);
|
|
if (PathChanged(normalized, sourceRepaired)
|
|
&& HasAcceptableGatewayBoundaryPath(sourceRepaired, nodes, edge.SourceNodeId, edge.TargetNodeId, sourceNode, fromStart: true))
|
|
{
|
|
normalized = sourceRepaired;
|
|
changed = true;
|
|
}
|
|
}
|
|
|
|
if (nodesById.TryGetValue(edge.TargetNodeId ?? string.Empty, out targetNode)
|
|
&& !ElkShapeBoundaries.IsGatewayShape(targetNode)
|
|
&& normalized.Count >= 2
|
|
&& !HasValidBoundaryAngle(normalized[^1], normalized[^2], targetNode))
|
|
{
|
|
var targetSide = ElkEdgeRoutingGeometry.ResolveBoundarySide(normalized[^1], targetNode);
|
|
var targetRepaired = NormalizeEntryPath(normalized, targetNode, targetSide);
|
|
if (HasClearBoundarySegments(targetRepaired, nodes, edge.SourceNodeId, edge.TargetNodeId, false, 3))
|
|
{
|
|
normalized = targetRepaired;
|
|
changed = true;
|
|
}
|
|
}
|
|
|
|
if (nodesById.TryGetValue(edge.TargetNodeId ?? string.Empty, out targetNode)
|
|
&& !ElkShapeBoundaries.IsGatewayShape(targetNode)
|
|
&& HasTargetApproachBacktracking(normalized, targetNode)
|
|
&& TryNormalizeNonGatewayBacktrackingEntry(normalized, targetNode, out var backtrackingRepair))
|
|
{
|
|
normalized = backtrackingRepair;
|
|
changed = true;
|
|
}
|
|
|
|
if (nodesById.TryGetValue(edge.TargetNodeId ?? string.Empty, out targetNode)
|
|
&& ElkShapeBoundaries.IsGatewayShape(targetNode)
|
|
&& NeedsGatewayTargetBoundaryRepair(normalized, targetNode))
|
|
{
|
|
var targetRepaired = NormalizeGatewayEntryPath(normalized, targetNode, normalized[^1]);
|
|
if (PathChanged(normalized, targetRepaired)
|
|
&& CanAcceptGatewayTargetRepair(targetRepaired, targetNode))
|
|
{
|
|
normalized = targetRepaired;
|
|
changed = true;
|
|
}
|
|
}
|
|
|
|
if (nodesById.TryGetValue(edge.SourceNodeId ?? string.Empty, out sourceNode)
|
|
&& ElkShapeBoundaries.IsGatewayShape(sourceNode))
|
|
{
|
|
if (preserveSourceExit)
|
|
{
|
|
var protectedExitFixed = TryBuildProtectedGatewaySourcePath(
|
|
normalized,
|
|
sourceNode,
|
|
nodes,
|
|
edge.SourceNodeId,
|
|
edge.TargetNodeId);
|
|
if (PathChanged(normalized, protectedExitFixed)
|
|
&& HasAcceptableGatewayBoundaryPath(protectedExitFixed, nodes, edge.SourceNodeId, edge.TargetNodeId, sourceNode, fromStart: true)
|
|
&& !HasGatewaySourceExitBacktracking(protectedExitFixed)
|
|
&& !HasGatewaySourceExitCurl(protectedExitFixed))
|
|
{
|
|
normalized = protectedExitFixed;
|
|
changed = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
var directExitFixed = TryBuildDirectGatewaySourcePath(
|
|
normalized,
|
|
sourceNode,
|
|
nodes,
|
|
edge.SourceNodeId,
|
|
edge.TargetNodeId);
|
|
if (PathChanged(normalized, directExitFixed)
|
|
&& HasAcceptableGatewayBoundaryPath(directExitFixed, nodes, edge.SourceNodeId, edge.TargetNodeId, sourceNode, fromStart: true)
|
|
&& !HasGatewaySourceExitBacktracking(directExitFixed)
|
|
&& !HasGatewaySourceExitCurl(directExitFixed)
|
|
&& !HasGatewaySourceDominantAxisDetour(directExitFixed, sourceNode)
|
|
&& !HasGatewaySourcePreferredFaceMismatch(directExitFixed, sourceNode))
|
|
{
|
|
normalized = directExitFixed;
|
|
changed = true;
|
|
}
|
|
|
|
if (sourceNode.Kind == "Decision")
|
|
{
|
|
var diagonalExitFixed = ForceDecisionDiagonalSourceExit(
|
|
normalized,
|
|
sourceNode,
|
|
nodes,
|
|
edge.SourceNodeId,
|
|
edge.TargetNodeId);
|
|
if (PathChanged(normalized, diagonalExitFixed)
|
|
&& HasAcceptableGatewayBoundaryPath(diagonalExitFixed, nodes, edge.SourceNodeId, edge.TargetNodeId, sourceNode, fromStart: true)
|
|
&& HasClearBoundarySegments(diagonalExitFixed, nodes, edge.SourceNodeId, edge.TargetNodeId, true, Math.Min(4, diagonalExitFixed.Count - 1))
|
|
&& !HasGatewaySourceExitBacktracking(diagonalExitFixed)
|
|
&& !HasGatewaySourceExitCurl(diagonalExitFixed))
|
|
{
|
|
normalized = diagonalExitFixed;
|
|
changed = true;
|
|
}
|
|
}
|
|
|
|
var faceFixed = FixGatewaySourcePreferredFace(normalized, sourceNode);
|
|
if (PathChanged(normalized, faceFixed)
|
|
&& HasAcceptableGatewayBoundaryPath(faceFixed, nodes, edge.SourceNodeId, edge.TargetNodeId, sourceNode, fromStart: true))
|
|
{
|
|
normalized = faceFixed;
|
|
changed = true;
|
|
}
|
|
|
|
var curlFixed = FixGatewaySourceExitCurl(normalized, sourceNode);
|
|
if (PathChanged(normalized, curlFixed)
|
|
&& HasAcceptableGatewayBoundaryPath(curlFixed, nodes, edge.SourceNodeId, edge.TargetNodeId, sourceNode, fromStart: true)
|
|
&& !HasGatewaySourceExitCurl(curlFixed))
|
|
{
|
|
normalized = curlFixed;
|
|
changed = true;
|
|
}
|
|
|
|
var dominantAxisFixed = FixGatewaySourceDominantAxisDetour(normalized, sourceNode);
|
|
if (PathChanged(normalized, dominantAxisFixed)
|
|
&& HasAcceptableGatewayBoundaryPath(dominantAxisFixed, nodes, edge.SourceNodeId, edge.TargetNodeId, sourceNode, fromStart: true))
|
|
{
|
|
normalized = dominantAxisFixed;
|
|
changed = true;
|
|
}
|
|
|
|
if (HasGatewaySourcePreferredFaceMismatch(normalized, sourceNode))
|
|
{
|
|
var forceAligned = ForceGatewaySourcePreferredFaceAlignment(normalized, sourceNode);
|
|
if (PathChanged(normalized, forceAligned)
|
|
&& HasAcceptableGatewayBoundaryPath(forceAligned, nodes, edge.SourceNodeId, edge.TargetNodeId, sourceNode, fromStart: true))
|
|
{
|
|
normalized = forceAligned;
|
|
changed = true;
|
|
}
|
|
}
|
|
|
|
var finalDirectExit = TryBuildDirectGatewaySourcePath(
|
|
normalized,
|
|
sourceNode,
|
|
nodes,
|
|
edge.SourceNodeId,
|
|
edge.TargetNodeId);
|
|
if (PathChanged(normalized, finalDirectExit)
|
|
&& HasAcceptableGatewayBoundaryPath(finalDirectExit, nodes, edge.SourceNodeId, edge.TargetNodeId, sourceNode, fromStart: true)
|
|
&& !HasGatewaySourceExitBacktracking(finalDirectExit)
|
|
&& !HasGatewaySourceExitCurl(finalDirectExit)
|
|
&& !HasGatewaySourceDominantAxisDetour(finalDirectExit, sourceNode)
|
|
&& !HasGatewaySourcePreferredFaceMismatch(finalDirectExit, sourceNode))
|
|
{
|
|
normalized = finalDirectExit;
|
|
changed = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (nodesById.TryGetValue(edge.TargetNodeId ?? string.Empty, out targetNode)
|
|
&& !ElkShapeBoundaries.IsGatewayShape(targetNode)
|
|
&& normalized.Count >= 2
|
|
&& !HasValidBoundaryAngle(normalized[^1], normalized[^2], targetNode))
|
|
{
|
|
var targetSide = ElkEdgeRoutingGeometry.ResolveBoundarySide(normalized[^1], targetNode);
|
|
var targetRepaired = NormalizeEntryPath(normalized, targetNode, targetSide);
|
|
if (HasClearBoundarySegments(targetRepaired, nodes, edge.SourceNodeId, edge.TargetNodeId, false, 3))
|
|
{
|
|
normalized = targetRepaired;
|
|
changed = true;
|
|
}
|
|
}
|
|
|
|
if (nodesById.TryGetValue(edge.TargetNodeId ?? string.Empty, out targetNode)
|
|
&& ElkShapeBoundaries.IsGatewayShape(targetNode)
|
|
&& normalized.Count >= 2
|
|
&& !HasValidBoundaryAngle(normalized[^1], normalized[^2], targetNode))
|
|
{
|
|
var lateTargetRepair = NormalizeGatewayEntryPath(normalized, targetNode, normalized[^1]);
|
|
if (PathChanged(normalized, lateTargetRepair)
|
|
&& CanAcceptGatewayTargetRepair(lateTargetRepair, targetNode))
|
|
{
|
|
normalized = lateTargetRepair;
|
|
changed = true;
|
|
}
|
|
else if (targetNode.Kind == "Decision")
|
|
{
|
|
var directDecisionRepair = ForceDecisionDirectTargetEntry(normalized, targetNode);
|
|
if (PathChanged(normalized, directDecisionRepair)
|
|
&& CanAcceptGatewayTargetRepair(directDecisionRepair, targetNode))
|
|
{
|
|
normalized = directDecisionRepair;
|
|
changed = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (nodesById.TryGetValue(edge.TargetNodeId ?? string.Empty, out targetNode)
|
|
&& ElkShapeBoundaries.IsGatewayShape(targetNode)
|
|
&& normalized.Count >= 2
|
|
&& !HasValidBoundaryAngle(normalized[^1], normalized[^2], targetNode))
|
|
{
|
|
var forcedGatewayStub = ForceGatewayTargetBoundaryStub(normalized, targetNode);
|
|
if (PathChanged(normalized, forcedGatewayStub)
|
|
&& CanAcceptGatewayTargetRepair(forcedGatewayStub, targetNode))
|
|
{
|
|
normalized = forcedGatewayStub;
|
|
changed = true;
|
|
}
|
|
}
|
|
|
|
if (nodesById.TryGetValue(edge.SourceNodeId ?? string.Empty, out sourceNode)
|
|
&& ElkShapeBoundaries.IsGatewayShape(sourceNode))
|
|
{
|
|
var lateSourceRepaired = RepairGatewaySourceBoundaryPath(
|
|
normalized,
|
|
sourceNode,
|
|
nodes,
|
|
edge.SourceNodeId,
|
|
edge.TargetNodeId);
|
|
if (PathChanged(normalized, lateSourceRepaired)
|
|
&& HasAcceptableGatewayBoundaryPath(lateSourceRepaired, nodes, edge.SourceNodeId, edge.TargetNodeId, sourceNode, fromStart: true)
|
|
&& !HasGatewaySourceExitBacktracking(lateSourceRepaired)
|
|
&& !HasGatewaySourceExitCurl(lateSourceRepaired)
|
|
&& !HasGatewaySourceDominantAxisDetour(lateSourceRepaired, sourceNode)
|
|
&& !HasGatewaySourcePreferredFaceMismatch(lateSourceRepaired, sourceNode))
|
|
{
|
|
normalized = lateSourceRepaired;
|
|
changed = true;
|
|
}
|
|
}
|
|
|
|
if (nodesById.TryGetValue(edge.TargetNodeId ?? string.Empty, out targetNode))
|
|
{
|
|
if (ElkShapeBoundaries.IsGatewayShape(targetNode))
|
|
{
|
|
if (NeedsGatewayTargetBoundaryRepair(normalized, targetNode))
|
|
{
|
|
var finalGatewayTargetRepair = NormalizeGatewayEntryPath(normalized, targetNode, normalized[^1]);
|
|
if (PathChanged(normalized, finalGatewayTargetRepair)
|
|
&& CanAcceptGatewayTargetRepair(finalGatewayTargetRepair, targetNode))
|
|
{
|
|
normalized = finalGatewayTargetRepair;
|
|
changed = true;
|
|
}
|
|
else if (targetNode.Kind == "Decision")
|
|
{
|
|
var directDecisionRepair = ForceDecisionDirectTargetEntry(normalized, targetNode);
|
|
if (PathChanged(normalized, directDecisionRepair)
|
|
&& CanAcceptGatewayTargetRepair(directDecisionRepair, targetNode))
|
|
{
|
|
normalized = directDecisionRepair;
|
|
changed = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (normalized.Count >= 2)
|
|
{
|
|
if (!HasValidBoundaryAngle(normalized[^1], normalized[^2], targetNode))
|
|
{
|
|
var finalTargetSide = ElkEdgeRoutingGeometry.ResolveBoundarySide(normalized[^1], targetNode);
|
|
var finalTargetRepair = NormalizeEntryPath(normalized, targetNode, finalTargetSide);
|
|
if (HasClearBoundarySegments(finalTargetRepair, nodes, edge.SourceNodeId, edge.TargetNodeId, false, 3))
|
|
{
|
|
normalized = finalTargetRepair;
|
|
changed = true;
|
|
}
|
|
}
|
|
|
|
if (HasTargetApproachBacktracking(normalized, targetNode)
|
|
&& TryNormalizeNonGatewayBacktrackingEntry(normalized, targetNode, out var finalBacktrackingRepair)
|
|
&& HasClearBoundarySegments(finalBacktrackingRepair, nodes, edge.SourceNodeId, edge.TargetNodeId, false, 3)
|
|
&& HasValidBoundaryAngle(finalBacktrackingRepair[^1], finalBacktrackingRepair[^2], targetNode))
|
|
{
|
|
normalized = finalBacktrackingRepair;
|
|
changed = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (nodesById.TryGetValue(edge.SourceNodeId ?? string.Empty, out sourceNode)
|
|
&& ElkShapeBoundaries.IsGatewayShape(sourceNode))
|
|
{
|
|
var finalSourceRepair = EnforceGatewaySourceExitQuality(
|
|
normalized,
|
|
sourceNode,
|
|
nodes,
|
|
edge.SourceNodeId,
|
|
edge.TargetNodeId);
|
|
if (PathChanged(normalized, finalSourceRepair))
|
|
{
|
|
normalized = finalSourceRepair;
|
|
changed = true;
|
|
}
|
|
}
|
|
|
|
if (nodesById.TryGetValue(edge.SourceNodeId ?? string.Empty, out sourceNode)
|
|
&& ElkShapeBoundaries.IsGatewayShape(sourceNode)
|
|
&& nodesById.TryGetValue(edge.TargetNodeId ?? string.Empty, out targetNode)
|
|
&& !ElkShapeBoundaries.IsGatewayShape(targetNode)
|
|
&& normalized.Count >= 2
|
|
&& TryRealignNonGatewayTargetBoundarySlot(
|
|
normalized,
|
|
targetNode,
|
|
nodes,
|
|
edge.SourceNodeId,
|
|
edge.TargetNodeId,
|
|
out var finalSourceDrivenTargetRepair)
|
|
&& PathChanged(normalized, finalSourceDrivenTargetRepair))
|
|
{
|
|
normalized = finalSourceDrivenTargetRepair;
|
|
changed = true;
|
|
}
|
|
|
|
result[i] = changed
|
|
? BuildSingleSectionEdge(edge, normalized)
|
|
: edge;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private static List<ElkPoint> FixGatewaySourcePreferredFace(
|
|
IReadOnlyList<ElkPoint> sourcePath,
|
|
ElkPositionedNode sourceNode)
|
|
{
|
|
if (!HasGatewaySourcePreferredFaceMismatch(sourcePath, sourceNode))
|
|
{
|
|
return sourcePath.Select(point => new ElkPoint { X = point.X, Y = point.Y }).ToList();
|
|
}
|
|
|
|
var path = sourcePath
|
|
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
|
.ToList();
|
|
var firstExteriorIndex = FindFirstGatewayExteriorPointIndex(path, sourceNode);
|
|
var continuationIndex = FindGatewaySourceCurlRecoveryIndex(path, firstExteriorIndex)
|
|
?? FindPreferredGatewayExitContinuationIndex(path, sourceNode, firstExteriorIndex);
|
|
var continuationPoint = path[continuationIndex];
|
|
if (!TryResolvePreferredGatewaySourceBoundary(sourceNode, continuationPoint, path[^1], out var preferredBoundary))
|
|
{
|
|
return path;
|
|
}
|
|
|
|
return BuildGatewaySourceRepairPath(
|
|
path,
|
|
sourceNode,
|
|
preferredBoundary,
|
|
continuationPoint,
|
|
continuationIndex,
|
|
path[^1]);
|
|
}
|
|
|
|
private static bool HasGatewaySourcePreferredFaceMismatch(
|
|
IReadOnlyList<ElkPoint> path,
|
|
ElkPositionedNode sourceNode)
|
|
{
|
|
if (!ElkShapeBoundaries.IsGatewayShape(sourceNode) || path.Count < 2)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var centerX = sourceNode.X + (sourceNode.Width / 2d);
|
|
var centerY = sourceNode.Y + (sourceNode.Height / 2d);
|
|
var desiredDx = path[^1].X - centerX;
|
|
var desiredDy = path[^1].Y - centerY;
|
|
var boundaryDx = path[0].X - centerX;
|
|
var boundaryDy = path[0].Y - centerY;
|
|
|
|
if (Math.Abs(desiredDx) >= Math.Abs(desiredDy) * 1.25d)
|
|
{
|
|
return Math.Sign(boundaryDx) != Math.Sign(desiredDx)
|
|
|| Math.Abs(boundaryDy) > sourceNode.Height * 0.28d;
|
|
}
|
|
|
|
if (Math.Abs(desiredDy) >= Math.Abs(desiredDx) * 1.25d)
|
|
{
|
|
return Math.Sign(boundaryDy) != Math.Sign(desiredDy)
|
|
|| Math.Abs(boundaryDx) > sourceNode.Width * 0.28d;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static List<ElkPoint> FixGatewaySourceExitCurl(
|
|
IReadOnlyList<ElkPoint> sourcePath,
|
|
ElkPositionedNode sourceNode)
|
|
{
|
|
if (!ElkShapeBoundaries.IsGatewayShape(sourceNode) || sourcePath.Count < 3)
|
|
{
|
|
return sourcePath.Select(point => new ElkPoint { X = point.X, Y = point.Y }).ToList();
|
|
}
|
|
|
|
var sample = sourcePath
|
|
.Take(Math.Min(sourcePath.Count, 6))
|
|
.ToArray();
|
|
var desiredDx = sourcePath[^1].X - sourcePath[0].X;
|
|
var desiredDy = sourcePath[^1].Y - sourcePath[0].Y;
|
|
if (!HasGatewaySourceExitCurl(sample))
|
|
{
|
|
return sourcePath.Select(point => new ElkPoint { X = point.X, Y = point.Y }).ToList();
|
|
}
|
|
|
|
var path = sourcePath
|
|
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
|
.ToList();
|
|
var firstExteriorIndex = FindFirstGatewayExteriorPointIndex(path, sourceNode);
|
|
var continuationIndex = FindGatewaySourceCurlRecoveryIndex(path, firstExteriorIndex)
|
|
?? FindPreferredGatewayExitContinuationIndex(path, sourceNode, firstExteriorIndex);
|
|
var continuationPoint = path[continuationIndex];
|
|
var boundary = sourceNode.Kind == "Decision"
|
|
? ResolveDecisionSourceExitBoundary(sourceNode, continuationPoint, continuationPoint)
|
|
: ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(
|
|
sourceNode,
|
|
ElkShapeBoundaries.ProjectOntoShapeBoundary(sourceNode, continuationPoint),
|
|
continuationPoint);
|
|
var continuationAligned = BuildGatewaySourceRepairPath(
|
|
path,
|
|
sourceNode,
|
|
boundary,
|
|
continuationPoint,
|
|
continuationIndex,
|
|
continuationPoint);
|
|
if (PathChanged(path, continuationAligned)
|
|
&& !HasGatewaySourceExitBacktracking(continuationAligned)
|
|
&& !HasGatewaySourceExitCurl(continuationAligned))
|
|
{
|
|
return continuationAligned;
|
|
}
|
|
|
|
var collapsedCurl = TryBuildGatewaySourceDominantAxisShortcut(path, sourceNode, path[0]);
|
|
if (collapsedCurl is not null
|
|
&& PathChanged(path, collapsedCurl)
|
|
&& !HasGatewaySourceExitBacktracking(collapsedCurl)
|
|
&& !HasGatewaySourceExitCurl(collapsedCurl))
|
|
{
|
|
return collapsedCurl;
|
|
}
|
|
|
|
const double axisTolerance = 4d;
|
|
var rebuilt = path;
|
|
var dominantHorizontal = Math.Abs(desiredDx) >= Math.Abs(desiredDy) * 1.15d && Math.Sign(desiredDx) != 0;
|
|
var dominantVertical = Math.Abs(desiredDy) >= Math.Abs(desiredDx) * 1.15d && Math.Sign(desiredDy) != 0;
|
|
|
|
if (dominantHorizontal && Math.Abs(rebuilt[1].Y - rebuilt[0].Y) <= axisTolerance)
|
|
{
|
|
rebuilt[1] = new ElkPoint
|
|
{
|
|
X = rebuilt[1].X,
|
|
Y = rebuilt[0].Y,
|
|
};
|
|
}
|
|
else if (dominantVertical && Math.Abs(rebuilt[1].X - rebuilt[0].X) <= axisTolerance)
|
|
{
|
|
rebuilt[1] = new ElkPoint
|
|
{
|
|
X = rebuilt[0].X,
|
|
Y = rebuilt[1].Y,
|
|
};
|
|
}
|
|
|
|
return NormalizePathPoints(rebuilt);
|
|
}
|
|
|
|
private static List<ElkPoint> FixGatewaySourceDominantAxisDetour(
|
|
IReadOnlyList<ElkPoint> sourcePath,
|
|
ElkPositionedNode sourceNode)
|
|
{
|
|
var path = sourcePath
|
|
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
|
.ToList();
|
|
if (!HasGatewaySourceDominantAxisDetour(path, sourceNode))
|
|
{
|
|
return path;
|
|
}
|
|
|
|
var boundary = path[0];
|
|
var desiredDx = path[^1].X - boundary.X;
|
|
var desiredDy = path[^1].Y - boundary.Y;
|
|
var dominantHorizontal = Math.Abs(desiredDx) >= Math.Abs(desiredDy) * 1.15d && Math.Sign(desiredDx) != 0;
|
|
var dominantVertical = Math.Abs(desiredDy) >= Math.Abs(desiredDx) * 1.15d && Math.Sign(desiredDy) != 0;
|
|
var firstExteriorIndex = FindFirstGatewayExteriorPointIndex(path, sourceNode);
|
|
var boundaryReferencePoint = path[firstExteriorIndex];
|
|
if (!TryResolvePreferredGatewaySourceBoundary(sourceNode, boundaryReferencePoint, path[^1], out var preferredBoundary))
|
|
{
|
|
return path;
|
|
}
|
|
|
|
var localContinuationPoint = path[firstExteriorIndex];
|
|
var localRepair = new List<ElkPoint> { preferredBoundary };
|
|
if (!ElkEdgeRoutingGeometry.PointsEqual(localRepair[^1], localContinuationPoint))
|
|
{
|
|
AppendGatewayOrthogonalCorner(
|
|
localRepair,
|
|
localRepair[^1],
|
|
localContinuationPoint,
|
|
firstExteriorIndex + 1 < path.Count ? path[firstExteriorIndex + 1] : null,
|
|
preferHorizontalFromReference: ShouldPreferHorizontalGatewayExit(localRepair[^1], localContinuationPoint));
|
|
if (!ElkEdgeRoutingGeometry.PointsEqual(localRepair[^1], localContinuationPoint))
|
|
{
|
|
localRepair.Add(localContinuationPoint);
|
|
}
|
|
}
|
|
|
|
for (var i = firstExteriorIndex + 1; i < path.Count; i++)
|
|
{
|
|
localRepair.Add(path[i]);
|
|
}
|
|
|
|
localRepair = NormalizePathPoints(localRepair);
|
|
if (PathChanged(path, localRepair)
|
|
&& !HasGatewaySourceExitBacktracking(localRepair)
|
|
&& !HasGatewaySourceExitCurl(localRepair)
|
|
&& !HasGatewaySourceDominantAxisDetour(localRepair, sourceNode)
|
|
&& !HasGatewaySourcePreferredFaceMismatch(localRepair, sourceNode))
|
|
{
|
|
return localRepair;
|
|
}
|
|
|
|
var dominantAxisShortcut = TryBuildGatewaySourceDominantAxisShortcut(path, sourceNode, preferredBoundary);
|
|
if (dominantAxisShortcut is not null
|
|
&& PathChanged(path, dominantAxisShortcut)
|
|
&& !HasGatewaySourceExitBacktracking(dominantAxisShortcut)
|
|
&& !HasGatewaySourceExitCurl(dominantAxisShortcut)
|
|
&& !HasGatewaySourceDominantAxisDetour(dominantAxisShortcut, sourceNode)
|
|
&& !HasGatewaySourcePreferredFaceMismatch(dominantAxisShortcut, sourceNode))
|
|
{
|
|
return dominantAxisShortcut;
|
|
}
|
|
|
|
var preferredContinuationIndex = FindPreferredGatewayExitContinuationIndex(path, sourceNode, firstExteriorIndex);
|
|
var candidateContinuationIndices = new[]
|
|
{
|
|
firstExteriorIndex,
|
|
Math.Min(path.Count - 1, firstExteriorIndex + 1),
|
|
Math.Min(path.Count - 1, firstExteriorIndex + 2),
|
|
preferredContinuationIndex,
|
|
}
|
|
.Distinct()
|
|
.Where(index => index >= firstExteriorIndex && index < path.Count)
|
|
.ToArray();
|
|
|
|
List<ElkPoint>? bestCandidate = null;
|
|
var bestScore = double.PositiveInfinity;
|
|
foreach (var continuationIndex in candidateContinuationIndices)
|
|
{
|
|
var continuationCandidates = new List<ElkPoint>
|
|
{
|
|
path[continuationIndex],
|
|
};
|
|
if (dominantHorizontal)
|
|
{
|
|
AddUniquePoint(
|
|
continuationCandidates,
|
|
new ElkPoint
|
|
{
|
|
X = path[continuationIndex].X,
|
|
Y = preferredBoundary.Y,
|
|
});
|
|
}
|
|
else if (dominantVertical)
|
|
{
|
|
AddUniquePoint(
|
|
continuationCandidates,
|
|
new ElkPoint
|
|
{
|
|
X = preferredBoundary.X,
|
|
Y = path[continuationIndex].Y,
|
|
});
|
|
}
|
|
|
|
foreach (var continuationPoint in continuationCandidates)
|
|
{
|
|
var candidate = BuildGatewaySourceRepairPath(
|
|
path,
|
|
sourceNode,
|
|
preferredBoundary,
|
|
continuationPoint,
|
|
continuationIndex,
|
|
continuationPoint);
|
|
if (!PathChanged(path, candidate))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var score = ComputePathLength(candidate);
|
|
if (!ElkEdgeRoutingGeometry.PointsEqual(continuationPoint, path[continuationIndex]))
|
|
{
|
|
score -= 18d;
|
|
}
|
|
|
|
if (HasGatewaySourceExitBacktracking(candidate)
|
|
|| HasGatewaySourceExitCurl(candidate))
|
|
{
|
|
score += 100_000d;
|
|
}
|
|
|
|
if (HasGatewaySourceDominantAxisDetour(candidate, sourceNode))
|
|
{
|
|
score += 50_000d;
|
|
}
|
|
|
|
if (HasGatewaySourcePreferredFaceMismatch(candidate, sourceNode))
|
|
{
|
|
score += 25_000d;
|
|
}
|
|
|
|
if (score >= bestScore)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
bestScore = score;
|
|
bestCandidate = candidate;
|
|
}
|
|
}
|
|
|
|
return bestCandidate ?? path;
|
|
}
|
|
|
|
private static List<ElkPoint>? TryBuildGatewaySourceDominantAxisShortcut(
|
|
IReadOnlyList<ElkPoint> path,
|
|
ElkPositionedNode sourceNode,
|
|
ElkPoint preferredBoundary)
|
|
{
|
|
if (path.Count < 4)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var desiredDx = path[^1].X - preferredBoundary.X;
|
|
var desiredDy = path[^1].Y - preferredBoundary.Y;
|
|
var dominantHorizontal = Math.Abs(desiredDx) >= Math.Abs(desiredDy) * 1.15d && Math.Sign(desiredDx) != 0;
|
|
var dominantVertical = Math.Abs(desiredDy) >= Math.Abs(desiredDx) * 1.15d && Math.Sign(desiredDy) != 0;
|
|
if (!dominantHorizontal && !dominantVertical)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
List<ElkPoint>? bestCandidate = null;
|
|
var bestLength = double.PositiveInfinity;
|
|
var maxShortcutIndex = Math.Min(path.Count - 2, 3);
|
|
for (var shortcutIndex = 1; shortcutIndex <= maxShortcutIndex; shortcutIndex++)
|
|
{
|
|
var shortcutAnchor = path[shortcutIndex];
|
|
var shortcutPoint = dominantHorizontal
|
|
? new ElkPoint { X = shortcutAnchor.X, Y = preferredBoundary.Y }
|
|
: new ElkPoint { X = preferredBoundary.X, Y = shortcutAnchor.Y };
|
|
if (ElkEdgeRoutingGeometry.PointsEqual(preferredBoundary, shortcutPoint))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var candidate = new List<ElkPoint> { preferredBoundary, shortcutPoint };
|
|
for (var i = shortcutIndex + 1; i < path.Count; i++)
|
|
{
|
|
candidate.Add(path[i]);
|
|
}
|
|
|
|
candidate = NormalizePathPoints(candidate);
|
|
if (HasGatewaySourceExitBacktracking(candidate)
|
|
|| HasGatewaySourceExitCurl(candidate)
|
|
|| HasGatewaySourceDominantAxisDetour(candidate, sourceNode)
|
|
|| HasGatewaySourcePreferredFaceMismatch(candidate, sourceNode))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var length = ComputePathLength(candidate);
|
|
if (length >= bestLength)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
bestLength = length;
|
|
bestCandidate = candidate;
|
|
}
|
|
|
|
return bestCandidate;
|
|
}
|
|
|
|
private static bool HasGatewaySourceDominantAxisDetour(
|
|
IReadOnlyList<ElkPoint> path,
|
|
ElkPositionedNode sourceNode)
|
|
{
|
|
if (!ElkShapeBoundaries.IsGatewayShape(sourceNode) || path.Count < 3)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
const double coordinateTolerance = 0.5d;
|
|
var centerX = sourceNode.X + (sourceNode.Width / 2d);
|
|
var centerY = sourceNode.Y + (sourceNode.Height / 2d);
|
|
var desiredDx = path[^1].X - centerX;
|
|
var desiredDy = path[^1].Y - centerY;
|
|
var dominantHorizontal = Math.Abs(desiredDx) >= Math.Abs(desiredDy) * 1.25d && Math.Sign(desiredDx) != 0;
|
|
var dominantVertical = Math.Abs(desiredDy) >= Math.Abs(desiredDx) * 1.25d && Math.Sign(desiredDy) != 0;
|
|
if (!dominantHorizontal && !dominantVertical)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var boundary = path[0];
|
|
var adjacent = path[1];
|
|
var firstDx = adjacent.X - boundary.X;
|
|
var firstDy = adjacent.Y - boundary.Y;
|
|
if (dominantHorizontal)
|
|
{
|
|
if (Math.Sign(firstDx) != Math.Sign(desiredDx) || Math.Abs(firstDx) <= coordinateTolerance)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (Math.Abs(firstDy) > Math.Max(12d, Math.Abs(firstDx) + 6d))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return Math.Abs(firstDy) > Math.Max(24d, Math.Abs(desiredDy) + 12d)
|
|
&& Math.Abs(firstDy) > Math.Abs(firstDx) * 1.25d;
|
|
}
|
|
|
|
if (Math.Sign(firstDy) != Math.Sign(desiredDy) || Math.Abs(firstDy) <= coordinateTolerance)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (Math.Abs(firstDx) > Math.Max(12d, Math.Abs(firstDy) + 6d))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return Math.Abs(firstDx) > Math.Max(24d, Math.Abs(desiredDx) + 12d)
|
|
&& Math.Abs(firstDx) > Math.Abs(firstDy) * 1.25d;
|
|
}
|
|
|
|
private static List<ElkPoint> ForceGatewaySourcePreferredFaceAlignment(
|
|
IReadOnlyList<ElkPoint> sourcePath,
|
|
ElkPositionedNode sourceNode)
|
|
{
|
|
var path = sourcePath
|
|
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
|
.ToList();
|
|
if (!ElkShapeBoundaries.IsGatewayShape(sourceNode) || path.Count < 2)
|
|
{
|
|
return path;
|
|
}
|
|
|
|
var centerX = sourceNode.X + (sourceNode.Width / 2d);
|
|
var centerY = sourceNode.Y + (sourceNode.Height / 2d);
|
|
var desiredDx = path[^1].X - centerX;
|
|
var desiredDy = path[^1].Y - centerY;
|
|
var dominantHorizontal = Math.Abs(desiredDx) >= Math.Abs(desiredDy) * 1.25d && Math.Sign(desiredDx) != 0;
|
|
var dominantVertical = Math.Abs(desiredDy) >= Math.Abs(desiredDx) * 1.25d && Math.Sign(desiredDy) != 0;
|
|
if (!dominantHorizontal && !dominantVertical)
|
|
{
|
|
return path;
|
|
}
|
|
|
|
var preferredSide = dominantHorizontal
|
|
? desiredDx >= 0d ? "right" : "left"
|
|
: desiredDy >= 0d ? "bottom" : "top";
|
|
var slotCoordinate = dominantHorizontal
|
|
? centerY + Math.Clamp(path[^1].Y - centerY, -sourceNode.Height * 0.18d, sourceNode.Height * 0.18d)
|
|
: centerX + Math.Clamp(path[^1].X - centerX, -sourceNode.Width * 0.18d, sourceNode.Width * 0.18d);
|
|
if (!ElkShapeBoundaries.TryProjectGatewayBoundarySlot(sourceNode, preferredSide, slotCoordinate, out var preferredBoundary))
|
|
{
|
|
return path;
|
|
}
|
|
|
|
preferredBoundary = ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(sourceNode, preferredBoundary, path[^1]);
|
|
var firstExteriorIndex = FindFirstGatewayExteriorPointIndex(path, sourceNode);
|
|
var continuationPoint = path[firstExteriorIndex];
|
|
var adjacentPoint = ElkShapeBoundaries.BuildPreferredGatewaySourceExteriorPoint(sourceNode, preferredBoundary, continuationPoint);
|
|
|
|
if (dominantHorizontal)
|
|
{
|
|
var candidateX = continuationPoint.X;
|
|
if (Math.Sign(candidateX - preferredBoundary.X) != Math.Sign(desiredDx)
|
|
|| Math.Abs(candidateX - preferredBoundary.X) <= 0.5d)
|
|
{
|
|
candidateX = desiredDx >= 0d
|
|
? sourceNode.X + sourceNode.Width + 8d
|
|
: sourceNode.X - 8d;
|
|
}
|
|
|
|
adjacentPoint = new ElkPoint
|
|
{
|
|
X = candidateX,
|
|
Y = preferredBoundary.Y,
|
|
};
|
|
}
|
|
else if (dominantVertical)
|
|
{
|
|
var candidateY = continuationPoint.Y;
|
|
if (Math.Sign(candidateY - preferredBoundary.Y) != Math.Sign(desiredDy)
|
|
|| Math.Abs(candidateY - preferredBoundary.Y) <= 0.5d)
|
|
{
|
|
candidateY = desiredDy >= 0d
|
|
? sourceNode.Y + sourceNode.Height + 8d
|
|
: sourceNode.Y - 8d;
|
|
}
|
|
|
|
adjacentPoint = new ElkPoint
|
|
{
|
|
X = preferredBoundary.X,
|
|
Y = candidateY,
|
|
};
|
|
}
|
|
|
|
if (ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(sourceNode, adjacentPoint))
|
|
{
|
|
adjacentPoint = ElkShapeBoundaries.BuildPreferredGatewaySourceExteriorPoint(sourceNode, preferredBoundary, continuationPoint);
|
|
}
|
|
|
|
var rebuilt = new List<ElkPoint> { preferredBoundary };
|
|
if (!ElkEdgeRoutingGeometry.PointsEqual(rebuilt[^1], adjacentPoint))
|
|
{
|
|
rebuilt.Add(adjacentPoint);
|
|
}
|
|
|
|
if (!ElkEdgeRoutingGeometry.PointsEqual(rebuilt[^1], continuationPoint))
|
|
{
|
|
AppendGatewayOrthogonalCorner(
|
|
rebuilt,
|
|
rebuilt[^1],
|
|
continuationPoint,
|
|
firstExteriorIndex + 1 < path.Count ? path[firstExteriorIndex + 1] : null,
|
|
preferHorizontalFromReference: ShouldPreferHorizontalGatewayExit(rebuilt[^1], continuationPoint));
|
|
if (!ElkEdgeRoutingGeometry.PointsEqual(rebuilt[^1], continuationPoint))
|
|
{
|
|
rebuilt.Add(continuationPoint);
|
|
}
|
|
}
|
|
|
|
for (var i = firstExteriorIndex + 1; i < path.Count; i++)
|
|
{
|
|
rebuilt.Add(path[i]);
|
|
}
|
|
|
|
return NormalizePathPoints(rebuilt);
|
|
}
|
|
|
|
internal static bool IsRepeatCollectorLabel(string? label)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(label))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var normalized = label.Trim().ToLowerInvariant();
|
|
return normalized.StartsWith("repeat ", StringComparison.Ordinal)
|
|
|| normalized.Equals("body", StringComparison.Ordinal);
|
|
}
|
|
|
|
private static bool ShouldPreserveSourceExitGeometry(
|
|
ElkRoutedEdge edge,
|
|
double graphMinY,
|
|
double graphMaxY)
|
|
{
|
|
if (HasProtectedUnderNodeGeometry(edge))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (!HasCorridorBendPoints(edge, graphMinY, graphMaxY))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return IsRepeatCollectorLabel(edge.Label)
|
|
|| (!string.IsNullOrWhiteSpace(edge.Kind)
|
|
&& edge.Kind.StartsWith("backward|", StringComparison.OrdinalIgnoreCase));
|
|
}
|
|
|
|
internal static bool IsCorridorSegment(ElkPoint p1, ElkPoint p2, double graphMinY, double graphMaxY)
|
|
{
|
|
return p1.Y < graphMinY - 8d || p1.Y > graphMaxY + 8d
|
|
|| p2.Y < graphMinY - 8d || p2.Y > graphMaxY + 8d;
|
|
}
|
|
|
|
internal static bool HasCorridorBendPoints(ElkRoutedEdge edge, double graphMinY, double graphMaxY)
|
|
{
|
|
foreach (var section in edge.Sections)
|
|
{
|
|
foreach (var bp in section.BendPoints)
|
|
{
|
|
if (bp.Y < graphMinY - 8d || bp.Y > graphMaxY + 8d)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
internal static bool SegmentCrossesObstacle(
|
|
ElkPoint p1, ElkPoint p2,
|
|
(double Left, double Top, double Right, double Bottom, string Id)[] obstacles,
|
|
string sourceId, string targetId)
|
|
{
|
|
var isH = Math.Abs(p1.Y - p2.Y) < 2d;
|
|
var isV = Math.Abs(p1.X - p2.X) < 2d;
|
|
|
|
foreach (var ob in obstacles)
|
|
{
|
|
if (ob.Id == sourceId || ob.Id == targetId) continue;
|
|
if (isH && p1.Y > ob.Top && p1.Y < ob.Bottom)
|
|
{
|
|
var minX = Math.Min(p1.X, p2.X);
|
|
var maxX = Math.Max(p1.X, p2.X);
|
|
if (maxX > ob.Left && minX < ob.Right) return true;
|
|
}
|
|
else if (isV && p1.X > ob.Left && p1.X < ob.Right)
|
|
{
|
|
var minY = Math.Min(p1.Y, p2.Y);
|
|
var maxY = Math.Max(p1.Y, p2.Y);
|
|
if (maxY > ob.Top && minY < ob.Bottom) return true;
|
|
}
|
|
else if (!isH && !isV)
|
|
{
|
|
// Diagonal segment: check actual intersection with obstacle rectangle
|
|
if (ElkEdgeRoutingGeometry.SegmentsIntersect(p1, p2,
|
|
new ElkPoint { X = ob.Left, Y = ob.Top }, new ElkPoint { X = ob.Right, Y = ob.Top })
|
|
|| ElkEdgeRoutingGeometry.SegmentsIntersect(p1, p2,
|
|
new ElkPoint { X = ob.Right, Y = ob.Top }, new ElkPoint { X = ob.Right, Y = ob.Bottom })
|
|
|| ElkEdgeRoutingGeometry.SegmentsIntersect(p1, p2,
|
|
new ElkPoint { X = ob.Right, Y = ob.Bottom }, new ElkPoint { X = ob.Left, Y = ob.Bottom })
|
|
|| ElkEdgeRoutingGeometry.SegmentsIntersect(p1, p2,
|
|
new ElkPoint { X = ob.Left, Y = ob.Bottom }, new ElkPoint { X = ob.Left, Y = ob.Top }))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static bool HasClearSourceExitSegment(
|
|
IReadOnlyList<ElkPoint> path,
|
|
IReadOnlyCollection<ElkPositionedNode> nodes,
|
|
string? sourceNodeId,
|
|
string? targetNodeId)
|
|
{
|
|
return HasClearBoundarySegments(path, nodes, sourceNodeId, targetNodeId, true, 2);
|
|
}
|
|
|
|
private static List<ElkPoint> NormalizeExitPath(
|
|
IReadOnlyList<ElkPoint> sourcePath,
|
|
ElkPositionedNode sourceNode,
|
|
string side)
|
|
{
|
|
const double coordinateTolerance = 0.5d;
|
|
var path = sourcePath
|
|
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
|
.ToList();
|
|
if (path.Count < 2)
|
|
{
|
|
return path;
|
|
}
|
|
|
|
if (side is "left" or "right")
|
|
{
|
|
var sourceX = side == "left"
|
|
? sourceNode.X
|
|
: sourceNode.X + sourceNode.Width;
|
|
while (path.Count >= 3 && Math.Abs(path[1].X - sourceX) <= coordinateTolerance)
|
|
{
|
|
path.RemoveAt(1);
|
|
}
|
|
|
|
var anchor = path[1];
|
|
var boundaryPoint = BuildRectBoundaryPointForSide(sourceNode, side, anchor);
|
|
var rebuilt = new List<ElkPoint>
|
|
{
|
|
new() { X = sourceX, Y = boundaryPoint.Y },
|
|
};
|
|
var stubX = side == "left"
|
|
? Math.Min(sourceX - 24d, anchor.X)
|
|
: Math.Max(sourceX + 24d, anchor.X);
|
|
if (Math.Abs(stubX - sourceX) > coordinateTolerance)
|
|
{
|
|
rebuilt.Add(new ElkPoint
|
|
{
|
|
X = stubX,
|
|
Y = boundaryPoint.Y,
|
|
});
|
|
}
|
|
|
|
if (Math.Abs(anchor.Y - boundaryPoint.Y) > coordinateTolerance)
|
|
{
|
|
rebuilt.Add(new ElkPoint { X = stubX, Y = anchor.Y });
|
|
}
|
|
|
|
if (Math.Abs(anchor.X - stubX) > coordinateTolerance)
|
|
{
|
|
rebuilt.Add(new ElkPoint { X = anchor.X, Y = anchor.Y });
|
|
}
|
|
|
|
rebuilt.AddRange(path.Skip(2));
|
|
return NormalizePathPoints(rebuilt);
|
|
}
|
|
|
|
var sourceY = side == "top"
|
|
? sourceNode.Y
|
|
: sourceNode.Y + sourceNode.Height;
|
|
while (path.Count >= 3 && Math.Abs(path[1].Y - sourceY) <= coordinateTolerance)
|
|
{
|
|
path.RemoveAt(1);
|
|
}
|
|
|
|
var verticalAnchor = path[1];
|
|
var verticalBoundaryPoint = BuildRectBoundaryPointForSide(sourceNode, side, verticalAnchor);
|
|
var verticalRebuilt = new List<ElkPoint>
|
|
{
|
|
new() { X = verticalBoundaryPoint.X, Y = sourceY },
|
|
};
|
|
var stubY = side == "top"
|
|
? Math.Min(sourceY - 24d, verticalAnchor.Y)
|
|
: Math.Max(sourceY + 24d, verticalAnchor.Y);
|
|
if (Math.Abs(stubY - sourceY) > coordinateTolerance)
|
|
{
|
|
verticalRebuilt.Add(new ElkPoint
|
|
{
|
|
X = verticalBoundaryPoint.X,
|
|
Y = stubY,
|
|
});
|
|
}
|
|
|
|
if (Math.Abs(verticalAnchor.X - verticalBoundaryPoint.X) > coordinateTolerance)
|
|
{
|
|
verticalRebuilt.Add(new ElkPoint { X = verticalAnchor.X, Y = stubY });
|
|
}
|
|
|
|
if (Math.Abs(verticalAnchor.Y - stubY) > coordinateTolerance)
|
|
{
|
|
verticalRebuilt.Add(new ElkPoint { X = verticalAnchor.X, Y = verticalAnchor.Y });
|
|
}
|
|
|
|
verticalRebuilt.AddRange(path.Skip(2));
|
|
return NormalizePathPoints(verticalRebuilt);
|
|
}
|
|
|
|
private static List<ElkPoint> NormalizeEntryPath(
|
|
IReadOnlyList<ElkPoint> sourcePath,
|
|
ElkPositionedNode targetNode,
|
|
string side)
|
|
{
|
|
return NormalizeEntryPath(sourcePath, targetNode, side, null);
|
|
}
|
|
|
|
private static List<ElkPoint> NormalizeEntryPath(
|
|
IReadOnlyList<ElkPoint> sourcePath,
|
|
ElkPositionedNode targetNode,
|
|
string side,
|
|
ElkPoint? explicitEndpoint)
|
|
{
|
|
const double coordinateTolerance = 0.5d;
|
|
var path = sourcePath
|
|
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
|
.ToList();
|
|
if (path.Count < 2)
|
|
{
|
|
return path;
|
|
}
|
|
|
|
if (HasTargetApproachBacktracking(path, targetNode)
|
|
&& TryResolveNonGatewayBacktrackingEndpoint(path, targetNode, out var correctedSide, out var correctedEndpoint))
|
|
{
|
|
side = correctedSide;
|
|
explicitEndpoint = correctedEndpoint;
|
|
}
|
|
|
|
if (side is "left" or "right")
|
|
{
|
|
var targetX = side == "left"
|
|
? targetNode.X
|
|
: targetNode.X + targetNode.Width;
|
|
while (path.Count >= 3 && Math.Abs(path[^2].X - targetX) <= coordinateTolerance)
|
|
{
|
|
path.RemoveAt(path.Count - 2);
|
|
}
|
|
|
|
var anchor = path[^2];
|
|
var endpoint = explicitEndpoint ?? BuildRectBoundaryPointForSide(targetNode, side, anchor);
|
|
var rebuilt = path.Take(path.Count - 2).ToList();
|
|
if (rebuilt.Count == 0 || !ElkEdgeRoutingGeometry.PointsEqual(rebuilt[^1], anchor))
|
|
{
|
|
rebuilt.Add(anchor);
|
|
}
|
|
|
|
var stubX = side == "left"
|
|
? targetX - 24d
|
|
: targetX + 24d;
|
|
if (Math.Abs(anchor.X - stubX) > coordinateTolerance)
|
|
{
|
|
rebuilt.Add(new ElkPoint { X = stubX, Y = anchor.Y });
|
|
}
|
|
|
|
if (Math.Abs(anchor.Y - endpoint.Y) > coordinateTolerance)
|
|
{
|
|
rebuilt.Add(new ElkPoint { X = stubX, Y = endpoint.Y });
|
|
}
|
|
|
|
rebuilt.Add(endpoint);
|
|
return NormalizePathPoints(rebuilt);
|
|
}
|
|
|
|
var targetY = side == "top"
|
|
? targetNode.Y
|
|
: targetNode.Y + targetNode.Height;
|
|
while (path.Count >= 3 && Math.Abs(path[^2].Y - targetY) <= coordinateTolerance)
|
|
{
|
|
path.RemoveAt(path.Count - 2);
|
|
}
|
|
|
|
var verticalAnchor = path[^2];
|
|
var verticalEndpoint = explicitEndpoint ?? BuildRectBoundaryPointForSide(targetNode, side, verticalAnchor);
|
|
var verticalRebuilt = path.Take(path.Count - 2).ToList();
|
|
if (verticalRebuilt.Count == 0 || !ElkEdgeRoutingGeometry.PointsEqual(verticalRebuilt[^1], verticalAnchor))
|
|
{
|
|
verticalRebuilt.Add(verticalAnchor);
|
|
}
|
|
|
|
var stubY = side == "top"
|
|
? targetY - 24d
|
|
: targetY + 24d;
|
|
if (Math.Abs(verticalAnchor.X - verticalEndpoint.X) > coordinateTolerance)
|
|
{
|
|
verticalRebuilt.Add(new ElkPoint { X = verticalEndpoint.X, Y = verticalAnchor.Y });
|
|
}
|
|
|
|
if (Math.Abs(verticalAnchor.Y - stubY) > coordinateTolerance)
|
|
{
|
|
verticalRebuilt.Add(new ElkPoint { X = verticalEndpoint.X, Y = stubY });
|
|
}
|
|
|
|
verticalRebuilt.Add(verticalEndpoint);
|
|
return NormalizePathPoints(verticalRebuilt);
|
|
}
|
|
|
|
private static string ResolvePreferredRectSourceExitSide(
|
|
IReadOnlyList<ElkPoint> path,
|
|
ElkPositionedNode sourceNode)
|
|
{
|
|
var currentSide = ElkEdgeRoutingGeometry.ResolveBoundarySide(path[0], sourceNode);
|
|
if (path.Count < 2)
|
|
{
|
|
return currentSide;
|
|
}
|
|
|
|
var overallDeltaX = path[^1].X - path[0].X;
|
|
var overallDeltaY = path[^1].Y - path[0].Y;
|
|
var overallAbsDx = Math.Abs(overallDeltaX);
|
|
var overallAbsDy = Math.Abs(overallDeltaY);
|
|
var sameRowThreshold = Math.Max(24d, sourceNode.Height / 3d);
|
|
var sameColumnThreshold = Math.Max(24d, sourceNode.Width / 3d);
|
|
if (overallAbsDx >= overallAbsDy * 1.15d
|
|
&& overallAbsDy <= sameRowThreshold
|
|
&& Math.Sign(overallDeltaX) != 0)
|
|
{
|
|
return overallDeltaX > 0d ? "right" : "left";
|
|
}
|
|
|
|
if (overallAbsDy >= overallAbsDx * 1.15d
|
|
&& overallAbsDx <= sameColumnThreshold
|
|
&& Math.Sign(overallDeltaY) != 0)
|
|
{
|
|
return overallDeltaY > 0d ? "bottom" : "top";
|
|
}
|
|
|
|
if (HasValidBoundaryAngle(path[0], path[1], sourceNode))
|
|
{
|
|
return currentSide;
|
|
}
|
|
|
|
var deltaX = path[1].X - path[0].X;
|
|
var deltaY = path[1].Y - path[0].Y;
|
|
var absDx = Math.Abs(deltaX);
|
|
var absDy = Math.Abs(deltaY);
|
|
if (absDx >= absDy * 1.15d && Math.Sign(deltaX) != 0)
|
|
{
|
|
return deltaX > 0d ? "right" : "left";
|
|
}
|
|
|
|
if (absDy >= absDx * 1.15d && Math.Sign(deltaY) != 0)
|
|
{
|
|
return deltaY > 0d ? "bottom" : "top";
|
|
}
|
|
|
|
return currentSide;
|
|
}
|
|
|
|
private static string ResolvePreferredRectTargetEntrySide(
|
|
IReadOnlyList<ElkPoint> path,
|
|
ElkPositionedNode targetNode)
|
|
{
|
|
var currentSide = ElkEdgeRoutingGeometry.ResolveBoundarySide(path[^1], targetNode);
|
|
if (path.Count < 2)
|
|
{
|
|
return currentSide;
|
|
}
|
|
|
|
var overallDeltaX = path[^1].X - path[0].X;
|
|
var overallDeltaY = path[^1].Y - path[0].Y;
|
|
var overallAbsDx = Math.Abs(overallDeltaX);
|
|
var overallAbsDy = Math.Abs(overallDeltaY);
|
|
var sameRowThreshold = Math.Max(24d, targetNode.Height / 3d);
|
|
var sameColumnThreshold = Math.Max(24d, targetNode.Width / 3d);
|
|
if (overallAbsDx >= overallAbsDy * 1.15d
|
|
&& overallAbsDy <= sameRowThreshold
|
|
&& Math.Sign(overallDeltaX) != 0)
|
|
{
|
|
return overallDeltaX > 0d ? "left" : "right";
|
|
}
|
|
|
|
if (overallAbsDy >= overallAbsDx * 1.15d
|
|
&& overallAbsDx <= sameColumnThreshold
|
|
&& Math.Sign(overallDeltaY) != 0)
|
|
{
|
|
return overallDeltaY > 0d ? "top" : "bottom";
|
|
}
|
|
|
|
if (HasValidBoundaryAngle(path[^1], path[^2], targetNode))
|
|
{
|
|
return currentSide;
|
|
}
|
|
|
|
var deltaX = path[^1].X - path[^2].X;
|
|
var deltaY = path[^1].Y - path[^2].Y;
|
|
var absDx = Math.Abs(deltaX);
|
|
var absDy = Math.Abs(deltaY);
|
|
if (absDx >= absDy * 1.15d && Math.Sign(deltaX) != 0)
|
|
{
|
|
return deltaX > 0d ? "left" : "right";
|
|
}
|
|
|
|
if (absDy >= absDx * 1.15d && Math.Sign(deltaY) != 0)
|
|
{
|
|
return deltaY > 0d ? "top" : "bottom";
|
|
}
|
|
|
|
return currentSide;
|
|
}
|
|
|
|
private static ElkPoint BuildRectBoundaryPointForSide(
|
|
ElkPositionedNode node,
|
|
string side,
|
|
ElkPoint referencePoint)
|
|
{
|
|
var insetX = Math.Min(24d, Math.Max(8d, node.Width / 4d));
|
|
var insetY = Math.Min(24d, Math.Max(8d, node.Height / 4d));
|
|
return side switch
|
|
{
|
|
"left" => new ElkPoint
|
|
{
|
|
X = node.X,
|
|
Y = Math.Clamp(referencePoint.Y, node.Y + insetY, (node.Y + node.Height) - insetY),
|
|
},
|
|
"right" => new ElkPoint
|
|
{
|
|
X = node.X + node.Width,
|
|
Y = Math.Clamp(referencePoint.Y, node.Y + insetY, (node.Y + node.Height) - insetY),
|
|
},
|
|
"top" => new ElkPoint
|
|
{
|
|
X = Math.Clamp(referencePoint.X, node.X + insetX, (node.X + node.Width) - insetX),
|
|
Y = node.Y,
|
|
},
|
|
"bottom" => new ElkPoint
|
|
{
|
|
X = Math.Clamp(referencePoint.X, node.X + insetX, (node.X + node.Width) - insetX),
|
|
Y = node.Y + node.Height,
|
|
},
|
|
_ => ElkShapeBoundaries.ProjectOntoShapeBoundary(node, referencePoint),
|
|
};
|
|
}
|
|
|
|
private static List<ElkPoint> NormalizeGatewayExitPath(
|
|
IReadOnlyList<ElkPoint> sourcePath,
|
|
ElkPositionedNode sourceNode,
|
|
IReadOnlyCollection<ElkPositionedNode> nodes,
|
|
string? sourceNodeId,
|
|
string? targetNodeId)
|
|
{
|
|
var path = sourcePath
|
|
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
|
.ToList();
|
|
if (path.Count < 2)
|
|
{
|
|
return path;
|
|
}
|
|
|
|
var firstExteriorIndex = FindFirstGatewayExteriorPointIndex(path, sourceNode);
|
|
var firstContinuationIndex = FindPreferredGatewayExitContinuationIndex(path, sourceNode, firstExteriorIndex);
|
|
var forceLocalExitRepair = path.Count > 2 && ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(sourceNode, path[1]);
|
|
var minContinuationIndex = forceLocalExitRepair
|
|
? firstExteriorIndex
|
|
: firstContinuationIndex;
|
|
var maxContinuationIndex = forceLocalExitRepair
|
|
? Math.Min(path.Count - 1, firstExteriorIndex + 1)
|
|
: path.Count - 1;
|
|
var exitReferences = CollectGatewayExitReferencePoints(path, nodes, targetNodeId, firstContinuationIndex);
|
|
List<ElkPoint>? bestCandidate = null;
|
|
var bestScore = double.PositiveInfinity;
|
|
|
|
foreach (var exitReference in exitReferences)
|
|
{
|
|
foreach (var boundary in ResolveGatewayExitBoundaryCandidates(sourceNode, exitReference))
|
|
{
|
|
for (var continuationIndex = minContinuationIndex; continuationIndex <= maxContinuationIndex; continuationIndex++)
|
|
{
|
|
if (ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(sourceNode, path[continuationIndex]))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var continuationReference = path[continuationIndex];
|
|
foreach (var exteriorApproach in ResolveGatewayExteriorApproachCandidates(sourceNode, boundary, continuationReference))
|
|
{
|
|
var candidate = BuildGatewayExitCandidate(path, boundary, exteriorApproach, continuationIndex);
|
|
if (!HasAcceptableGatewayBoundaryPath(candidate, nodes, sourceNodeId, targetNodeId, sourceNode, fromStart: true)
|
|
|| !HasClearBoundarySegments(candidate, nodes, sourceNodeId, targetNodeId, true, Math.Min(4, candidate.Count - 1))
|
|
|| HasGatewaySourceExitBacktracking(candidate)
|
|
|| HasGatewaySourceExitCurl(candidate))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var score = ScoreGatewayExitCandidate(candidate, exitReference, continuationIndex, path, sourceNode);
|
|
if (score >= bestScore)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
bestScore = score;
|
|
bestCandidate = candidate;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (bestCandidate is not null)
|
|
{
|
|
return ForceDecisionDiagonalSourceExit(bestCandidate, sourceNode, nodes, sourceNodeId, targetNodeId);
|
|
}
|
|
|
|
var exteriorAnchor = path[firstContinuationIndex];
|
|
var fallbackReference = exitReferences[0];
|
|
var fallbackBoundaryCandidates = ResolveGatewayExitBoundaryCandidates(sourceNode, fallbackReference).ToArray();
|
|
if (fallbackBoundaryCandidates.Length == 0)
|
|
{
|
|
fallbackBoundaryCandidates =
|
|
[
|
|
ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(
|
|
sourceNode,
|
|
ElkShapeBoundaries.ProjectOntoShapeBoundary(sourceNode, fallbackReference),
|
|
fallbackReference),
|
|
];
|
|
}
|
|
|
|
List<ElkPoint>? fallbackPath = null;
|
|
var fallbackScore = double.PositiveInfinity;
|
|
foreach (var fallbackBoundary in fallbackBoundaryCandidates)
|
|
{
|
|
var candidate = BuildGatewayFallbackExitPath(path, sourceNode, fallbackBoundary, exteriorAnchor, firstContinuationIndex);
|
|
var candidateScore = ScoreGatewayExitCandidate(candidate, fallbackReference, firstContinuationIndex, path, sourceNode);
|
|
if (!HasAcceptableGatewayBoundaryPath(candidate, nodes, sourceNodeId, targetNodeId, sourceNode, fromStart: true))
|
|
{
|
|
candidateScore += 5_000d;
|
|
}
|
|
|
|
if (candidateScore >= fallbackScore)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
fallbackScore = candidateScore;
|
|
fallbackPath = candidate;
|
|
}
|
|
|
|
if (fallbackPath is not null)
|
|
{
|
|
return ForceDecisionDiagonalSourceExit(fallbackPath, sourceNode, nodes, sourceNodeId, targetNodeId);
|
|
}
|
|
|
|
if (sourceNode.Kind == "Decision"
|
|
&& path.Count >= 3
|
|
&& ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(sourceNode, path[1]))
|
|
{
|
|
var continuationIndex = Math.Max(firstExteriorIndex, Math.Min(path.Count - 1, 2));
|
|
var continuationPoint = path[continuationIndex];
|
|
var directBoundary = ResolveDecisionSourceExitBoundary(sourceNode, continuationPoint, path[^1]);
|
|
var directApproach = ElkShapeBoundaries.BuildPreferredGatewaySourceExteriorPoint(sourceNode, directBoundary, continuationPoint);
|
|
var directRepair = BuildGatewayExitCandidate(path, directBoundary, directApproach, continuationIndex);
|
|
if (HasAcceptableGatewayBoundaryPath(directRepair, nodes, sourceNodeId, targetNodeId, sourceNode, fromStart: true)
|
|
&& HasClearBoundarySegments(directRepair, nodes, sourceNodeId, targetNodeId, true, Math.Min(4, directRepair.Count - 1))
|
|
&& !HasGatewaySourceExitBacktracking(directRepair)
|
|
&& !HasGatewaySourceExitCurl(directRepair))
|
|
{
|
|
return ForceDecisionDiagonalSourceExit(directRepair, sourceNode, nodes, sourceNodeId, targetNodeId);
|
|
}
|
|
}
|
|
|
|
return ForceDecisionDiagonalSourceExit(path, sourceNode, nodes, sourceNodeId, targetNodeId);
|
|
}
|
|
|
|
private static List<ElkPoint> CollectGatewayExitReferencePoints(
|
|
IReadOnlyList<ElkPoint> path,
|
|
IReadOnlyCollection<ElkPositionedNode> nodes,
|
|
string? targetNodeId,
|
|
int firstContinuationIndex)
|
|
{
|
|
var references = new List<ElkPoint>();
|
|
if (!string.IsNullOrWhiteSpace(targetNodeId))
|
|
{
|
|
var targetNode = nodes.FirstOrDefault(node => string.Equals(node.Id, targetNodeId, StringComparison.Ordinal));
|
|
if (targetNode is not null)
|
|
{
|
|
AddUniquePoint(references, new ElkPoint
|
|
{
|
|
X = targetNode.X + (targetNode.Width / 2d),
|
|
Y = targetNode.Y + (targetNode.Height / 2d),
|
|
});
|
|
}
|
|
}
|
|
|
|
var maxReferenceIndex = Math.Min(path.Count - 1, firstContinuationIndex + 4);
|
|
for (var i = firstContinuationIndex; i <= maxReferenceIndex; i++)
|
|
{
|
|
AddUniquePoint(references, path[i]);
|
|
}
|
|
|
|
AddUniquePoint(references, path[^1]);
|
|
if (references.Count == 0)
|
|
{
|
|
references.Add(path[^1]);
|
|
}
|
|
|
|
return references;
|
|
}
|
|
|
|
private static IEnumerable<ElkPoint> ResolveGatewayExitBoundaryCandidates(
|
|
ElkPositionedNode sourceNode,
|
|
ElkPoint exitReference)
|
|
{
|
|
var candidates = new List<ElkPoint>();
|
|
AddUniquePoint(
|
|
candidates,
|
|
ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(
|
|
sourceNode,
|
|
ElkShapeBoundaries.ProjectOntoShapeBoundary(sourceNode, exitReference),
|
|
exitReference));
|
|
|
|
foreach (var side in EnumeratePreferredGatewayExitSides(sourceNode, exitReference))
|
|
{
|
|
var centerX = sourceNode.X + (sourceNode.Width / 2d);
|
|
var centerY = sourceNode.Y + (sourceNode.Height / 2d);
|
|
var slotCoordinate = side is "left" or "right"
|
|
? centerY + Math.Clamp(exitReference.Y - centerY, -sourceNode.Height * 0.18d, sourceNode.Height * 0.18d)
|
|
: centerX + Math.Clamp(exitReference.X - centerX, -sourceNode.Width * 0.18d, sourceNode.Width * 0.18d);
|
|
if (!ElkShapeBoundaries.TryProjectGatewayBoundarySlot(sourceNode, side, slotCoordinate, out var slotBoundary))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
AddUniquePoint(
|
|
candidates,
|
|
ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(sourceNode, slotBoundary, exitReference));
|
|
}
|
|
|
|
return candidates;
|
|
}
|
|
|
|
private static IEnumerable<string> EnumeratePreferredGatewayExitSides(
|
|
ElkPositionedNode sourceNode,
|
|
ElkPoint exitReference)
|
|
{
|
|
var centerX = sourceNode.X + (sourceNode.Width / 2d);
|
|
var centerY = sourceNode.Y + (sourceNode.Height / 2d);
|
|
var deltaX = exitReference.X - centerX;
|
|
var deltaY = exitReference.Y - centerY;
|
|
var absDx = Math.Abs(deltaX);
|
|
var absDy = Math.Abs(deltaY);
|
|
var primary = absDx >= absDy
|
|
? (deltaX >= 0d ? "right" : "left")
|
|
: (deltaY >= 0d ? "bottom" : "top");
|
|
yield return primary;
|
|
|
|
if (absDx > 0.5d && absDy > 0.5d)
|
|
{
|
|
var secondary = primary is "left" or "right"
|
|
? (deltaY >= 0d ? "bottom" : "top")
|
|
: (deltaX >= 0d ? "right" : "left");
|
|
if (!string.Equals(primary, secondary, StringComparison.Ordinal))
|
|
{
|
|
yield return secondary;
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void AddUniquePoint(ICollection<ElkPoint> points, ElkPoint point)
|
|
{
|
|
if (points.Any(existing => ElkEdgeRoutingGeometry.PointsEqual(existing, point)))
|
|
{
|
|
return;
|
|
}
|
|
|
|
points.Add(point);
|
|
}
|
|
|
|
private static List<ElkPoint> BuildGatewayExitCandidate(
|
|
IReadOnlyList<ElkPoint> path,
|
|
ElkPoint boundary,
|
|
ElkPoint exteriorApproach,
|
|
int continuationIndex)
|
|
{
|
|
var rebuilt = new List<ElkPoint> { boundary };
|
|
if (!ElkEdgeRoutingGeometry.PointsEqual(boundary, exteriorApproach))
|
|
{
|
|
rebuilt.Add(exteriorApproach);
|
|
}
|
|
|
|
var continuationPoint = path[continuationIndex];
|
|
if (!ElkEdgeRoutingGeometry.PointsEqual(rebuilt[^1], continuationPoint))
|
|
{
|
|
var preferHorizontal = ShouldPreferHorizontalGatewayExit(rebuilt[^1], continuationPoint);
|
|
AppendGatewayOrthogonalCorner(
|
|
rebuilt,
|
|
rebuilt[^1],
|
|
continuationPoint,
|
|
continuationIndex + 1 < path.Count ? path[continuationIndex + 1] : null,
|
|
preferHorizontalFromReference: preferHorizontal);
|
|
if (!ElkEdgeRoutingGeometry.PointsEqual(rebuilt[^1], continuationPoint))
|
|
{
|
|
rebuilt.Add(continuationPoint);
|
|
}
|
|
}
|
|
|
|
for (var i = continuationIndex + 1; i < path.Count; i++)
|
|
{
|
|
rebuilt.Add(path[i]);
|
|
}
|
|
|
|
return NormalizePathPoints(rebuilt);
|
|
}
|
|
|
|
private static List<ElkPoint> BuildGatewayFallbackExitPath(
|
|
IReadOnlyList<ElkPoint> path,
|
|
ElkPositionedNode sourceNode,
|
|
ElkPoint boundary,
|
|
ElkPoint exteriorAnchor,
|
|
int continuationIndex)
|
|
{
|
|
var exteriorApproach = ElkShapeBoundaries.BuildPreferredGatewaySourceExteriorPoint(sourceNode, boundary, exteriorAnchor);
|
|
|
|
var rebuilt = new List<ElkPoint> { boundary };
|
|
if (!ElkEdgeRoutingGeometry.PointsEqual(boundary, exteriorApproach))
|
|
{
|
|
rebuilt.Add(exteriorApproach);
|
|
}
|
|
|
|
AppendGatewayOrthogonalCorner(
|
|
rebuilt,
|
|
rebuilt[^1],
|
|
exteriorAnchor,
|
|
continuationIndex + 1 < path.Count ? path[continuationIndex + 1] : null,
|
|
preferHorizontalFromReference: ShouldPreferHorizontalGatewayExit(rebuilt[^1], exteriorAnchor));
|
|
if (!ElkEdgeRoutingGeometry.PointsEqual(rebuilt[^1], exteriorAnchor))
|
|
{
|
|
rebuilt.Add(exteriorAnchor);
|
|
}
|
|
|
|
for (var i = continuationIndex + 1; i < path.Count; i++)
|
|
{
|
|
rebuilt.Add(path[i]);
|
|
}
|
|
|
|
return NormalizePathPoints(rebuilt);
|
|
}
|
|
|
|
private static bool PathStartsAtDecisionVertex(
|
|
IReadOnlyList<ElkPoint> path,
|
|
ElkPositionedNode sourceNode)
|
|
{
|
|
return sourceNode.Kind == "Decision"
|
|
&& path.Count >= 2
|
|
&& ElkShapeBoundaries.IsNearGatewayVertex(sourceNode, path[0]);
|
|
}
|
|
|
|
private static List<ElkPoint> ForceDecisionSourceExitOffVertex(
|
|
IReadOnlyList<ElkPoint> sourcePath,
|
|
ElkPositionedNode sourceNode,
|
|
IReadOnlyCollection<ElkPositionedNode> nodes,
|
|
string? sourceNodeId,
|
|
string? targetNodeId)
|
|
{
|
|
var path = sourcePath
|
|
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
|
.ToList();
|
|
if (path.Count < 3 || sourceNode.Kind != "Decision")
|
|
{
|
|
return path;
|
|
}
|
|
|
|
var continuationIndex = Math.Min(path.Count - 1, 2);
|
|
var reference = path[^1];
|
|
var boundary = ResolveDecisionSourceExitBoundary(sourceNode, path[continuationIndex], reference);
|
|
if (ElkShapeBoundaries.IsNearGatewayVertex(sourceNode, boundary))
|
|
{
|
|
return path;
|
|
}
|
|
|
|
var continuationPoint = path[continuationIndex];
|
|
var exteriorApproach = ElkShapeBoundaries.BuildPreferredGatewaySourceExteriorPoint(sourceNode, boundary, continuationPoint);
|
|
var rebuilt = new List<ElkPoint> { boundary };
|
|
if (!ElkEdgeRoutingGeometry.PointsEqual(boundary, exteriorApproach))
|
|
{
|
|
rebuilt.Add(exteriorApproach);
|
|
}
|
|
|
|
AppendGatewayOrthogonalCorner(
|
|
rebuilt,
|
|
rebuilt[^1],
|
|
continuationPoint,
|
|
continuationIndex + 1 < path.Count ? path[continuationIndex + 1] : null,
|
|
preferHorizontalFromReference: ShouldPreferHorizontalGatewayExit(rebuilt[^1], continuationPoint));
|
|
if (!ElkEdgeRoutingGeometry.PointsEqual(rebuilt[^1], continuationPoint))
|
|
{
|
|
rebuilt.Add(continuationPoint);
|
|
}
|
|
|
|
for (var i = continuationIndex + 1; i < path.Count; i++)
|
|
{
|
|
rebuilt.Add(path[i]);
|
|
}
|
|
|
|
return NormalizePathPoints(rebuilt);
|
|
}
|
|
|
|
private static ElkPoint ResolveDecisionSourceExitBoundary(
|
|
ElkPositionedNode sourceNode,
|
|
ElkPoint continuationPoint,
|
|
ElkPoint reference)
|
|
{
|
|
var projectedReference = ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(
|
|
sourceNode,
|
|
ElkShapeBoundaries.ProjectOntoShapeBoundary(sourceNode, reference),
|
|
reference);
|
|
var projectedContinuation = ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(
|
|
sourceNode,
|
|
ElkShapeBoundaries.ProjectOntoShapeBoundary(sourceNode, continuationPoint),
|
|
continuationPoint);
|
|
var candidates = new List<ElkPoint>();
|
|
AddDecisionBoundaryCandidate(candidates, projectedReference);
|
|
AddDecisionBoundaryCandidate(candidates, projectedContinuation);
|
|
foreach (var side in EnumeratePreferredGatewaySourceSides(sourceNode, continuationPoint, reference))
|
|
{
|
|
foreach (var slotBoundary in ResolveGatewaySourceBoundarySlotCandidates(sourceNode, side, continuationPoint, reference))
|
|
{
|
|
AddDecisionBoundaryCandidate(candidates, slotBoundary);
|
|
}
|
|
}
|
|
|
|
var bestCandidate = projectedReference;
|
|
var bestScore = double.PositiveInfinity;
|
|
foreach (var candidate in candidates)
|
|
{
|
|
var score = ScoreDecisionSourceExitBoundaryCandidate(
|
|
sourceNode,
|
|
candidate,
|
|
projectedReference,
|
|
projectedContinuation,
|
|
continuationPoint,
|
|
reference);
|
|
if (score >= bestScore)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
bestScore = score;
|
|
bestCandidate = candidate;
|
|
}
|
|
|
|
return bestCandidate;
|
|
}
|
|
|
|
private static void AddDecisionBoundaryCandidate(
|
|
ICollection<ElkPoint> candidates,
|
|
ElkPoint candidate)
|
|
{
|
|
if (candidates.Any(existing => ElkEdgeRoutingGeometry.PointsEqual(existing, candidate)))
|
|
{
|
|
return;
|
|
}
|
|
|
|
candidates.Add(candidate);
|
|
}
|
|
|
|
private static double ScoreDecisionSourceExitBoundaryCandidate(
|
|
ElkPositionedNode sourceNode,
|
|
ElkPoint candidate,
|
|
ElkPoint projectedReference,
|
|
ElkPoint projectedContinuation,
|
|
ElkPoint continuationPoint,
|
|
ElkPoint reference)
|
|
{
|
|
var centerX = sourceNode.X + (sourceNode.Width / 2d);
|
|
var centerY = sourceNode.Y + (sourceNode.Height / 2d);
|
|
var desiredDx = reference.X - centerX;
|
|
var desiredDy = reference.Y - centerY;
|
|
var candidateDx = candidate.X - centerX;
|
|
var candidateDy = candidate.Y - centerY;
|
|
|
|
var score = Math.Abs(candidate.X - projectedReference.X) + Math.Abs(candidate.Y - projectedReference.Y);
|
|
score += (Math.Abs(candidate.X - projectedContinuation.X) + Math.Abs(candidate.Y - projectedContinuation.Y)) * 0.35d;
|
|
|
|
var preferredSides = EnumeratePreferredGatewaySourceSides(sourceNode, continuationPoint, reference).ToArray();
|
|
if (preferredSides.Length > 0
|
|
&& !IsBoundaryOnGatewaySourceSide(sourceNode, candidate, preferredSides[0]))
|
|
{
|
|
score += preferredSides[0] is "left" or "right"
|
|
? 12_000d
|
|
: 8_000d;
|
|
}
|
|
else if (preferredSides.Length > 0 && preferredSides[0] is "left" or "right")
|
|
{
|
|
score -= Math.Abs(candidateDx) * 0.4d;
|
|
}
|
|
|
|
if (preferredSides.Length > 1
|
|
&& !IsBoundaryOnGatewaySourceSide(sourceNode, candidate, preferredSides[0])
|
|
&& !IsBoundaryOnGatewaySourceSide(sourceNode, candidate, preferredSides[1]))
|
|
{
|
|
score += 4_000d;
|
|
}
|
|
|
|
var dominantHorizontal = Math.Abs(desiredDx) >= Math.Abs(desiredDy) * 1.25d;
|
|
var dominantVertical = Math.Abs(desiredDy) >= Math.Abs(desiredDx) * 1.25d;
|
|
if (dominantHorizontal)
|
|
{
|
|
if (Math.Sign(candidateDx) != Math.Sign(desiredDx))
|
|
{
|
|
score += 10_000d;
|
|
}
|
|
|
|
if (Math.Abs(candidateDy) > sourceNode.Height * 0.28d)
|
|
{
|
|
score += 25_000d;
|
|
}
|
|
|
|
score += Math.Abs(candidateDy) * 6d;
|
|
}
|
|
else if (dominantVertical)
|
|
{
|
|
if (Math.Sign(candidateDy) != Math.Sign(desiredDy))
|
|
{
|
|
score += 10_000d;
|
|
}
|
|
|
|
if (Math.Abs(candidateDx) > sourceNode.Width * 0.28d)
|
|
{
|
|
score += 25_000d;
|
|
}
|
|
|
|
score += Math.Abs(candidateDx) * 6d;
|
|
}
|
|
else
|
|
{
|
|
score += (Math.Abs(candidateDx - desiredDx) + Math.Abs(candidateDy - desiredDy)) * 0.08d;
|
|
}
|
|
|
|
if (ElkShapeBoundaries.IsNearGatewayVertex(sourceNode, candidate, 8d))
|
|
{
|
|
score += 4_000d;
|
|
}
|
|
|
|
var exterior = ElkShapeBoundaries.BuildPreferredGatewaySourceExteriorPoint(sourceNode, candidate, continuationPoint);
|
|
score += (Math.Abs(exterior.X - continuationPoint.X) + Math.Abs(exterior.Y - continuationPoint.Y)) * 0.04d;
|
|
return score;
|
|
}
|
|
|
|
private static bool ShouldPreferHorizontalGatewayExit(ElkPoint from, ElkPoint to)
|
|
{
|
|
return Math.Abs(to.X - from.X) >= Math.Abs(to.Y - from.Y);
|
|
}
|
|
|
|
private static bool CanUseDirectGatewayContinuation(ElkPoint from, ElkPoint to)
|
|
{
|
|
const double coordinateTolerance = 0.5d;
|
|
var deltaX = Math.Abs(to.X - from.X);
|
|
var deltaY = Math.Abs(to.Y - from.Y);
|
|
if (deltaX <= coordinateTolerance || deltaY <= coordinateTolerance)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
var length = Math.Sqrt((deltaX * deltaX) + (deltaY * deltaY));
|
|
return length <= 36d
|
|
&& Math.Abs(deltaX - deltaY) <= Math.Max(6d, Math.Min(deltaX, deltaY) * 0.45d);
|
|
}
|
|
|
|
private static void AddUniqueCoordinate(ICollection<double> coordinates, double value)
|
|
{
|
|
if (coordinates.Any(existing => Math.Abs(existing - value) <= 0.5d))
|
|
{
|
|
return;
|
|
}
|
|
|
|
coordinates.Add(value);
|
|
}
|
|
|
|
private static double ScoreGatewayExitCandidate(
|
|
IReadOnlyList<ElkPoint> candidate,
|
|
ElkPoint exitReference,
|
|
int continuationIndex,
|
|
IReadOnlyList<ElkPoint> originalPath,
|
|
ElkPositionedNode sourceNode)
|
|
{
|
|
var score = ComputePathLength(candidate);
|
|
score += Math.Max(0, candidate.Count - 2) * 3d;
|
|
score += continuationIndex * 2d;
|
|
score += (Math.Abs(candidate[0].X - exitReference.X) + Math.Abs(candidate[0].Y - exitReference.Y)) * 0.1d;
|
|
if (continuationIndex < originalPath.Count)
|
|
{
|
|
score += (Math.Abs(candidate[1].X - originalPath[continuationIndex].X)
|
|
+ Math.Abs(candidate[1].Y - originalPath[continuationIndex].Y)) * 0.02d;
|
|
}
|
|
|
|
score += ScoreGatewayExitProgress(sourceNode, candidate, exitReference);
|
|
if (HasGatewaySourceDominantAxisDetour(candidate, sourceNode))
|
|
{
|
|
score += 50_000d;
|
|
}
|
|
|
|
if (HasGatewaySourcePreferredFaceMismatch(candidate, sourceNode))
|
|
{
|
|
score += 25_000d;
|
|
}
|
|
|
|
if (NeedsDecisionSourcePreferredFaceRepair(candidate, sourceNode))
|
|
{
|
|
score += 15_000d;
|
|
}
|
|
|
|
return score;
|
|
}
|
|
|
|
private static double ScoreGatewayExitProgress(
|
|
ElkPositionedNode sourceNode,
|
|
IReadOnlyList<ElkPoint> candidate,
|
|
ElkPoint exitReference)
|
|
{
|
|
if (candidate.Count < 2)
|
|
{
|
|
return 0d;
|
|
}
|
|
|
|
var boundary = candidate[0];
|
|
var next = candidate[1];
|
|
var score = 0d;
|
|
if (ElkShapeBoundaries.IsNearGatewayVertex(sourceNode, boundary))
|
|
{
|
|
score += sourceNode.Kind == "Decision"
|
|
? 5_000d
|
|
: 1_500d;
|
|
}
|
|
|
|
var startDistance = Math.Abs(boundary.X - exitReference.X) + Math.Abs(boundary.Y - exitReference.Y);
|
|
var nextDistance = Math.Abs(next.X - exitReference.X) + Math.Abs(next.Y - exitReference.Y);
|
|
if (nextDistance > startDistance + 0.5d)
|
|
{
|
|
score += (nextDistance - startDistance) * 6d;
|
|
}
|
|
|
|
var totalDx = exitReference.X - boundary.X;
|
|
var totalDy = exitReference.Y - boundary.Y;
|
|
var firstDx = next.X - boundary.X;
|
|
var firstDy = next.Y - boundary.Y;
|
|
var absTotalDx = Math.Abs(totalDx);
|
|
var absTotalDy = Math.Abs(totalDy);
|
|
var absFirstDx = Math.Abs(firstDx);
|
|
var absFirstDy = Math.Abs(firstDy);
|
|
const double coordinateTolerance = 0.5d;
|
|
|
|
if (sourceNode.Kind == "Decision"
|
|
&& (absFirstDx <= coordinateTolerance || absFirstDy <= coordinateTolerance))
|
|
{
|
|
score += ElkShapeBoundaries.IsNearGatewayVertex(sourceNode, boundary)
|
|
? 350d
|
|
: 0d;
|
|
}
|
|
|
|
if (absTotalDx >= absTotalDy * 1.25d)
|
|
{
|
|
if (absFirstDx <= coordinateTolerance || Math.Sign(firstDx) != Math.Sign(totalDx))
|
|
{
|
|
score += 600d;
|
|
}
|
|
else if (absFirstDy > absFirstDx * 1.25d)
|
|
{
|
|
score += 120d;
|
|
}
|
|
}
|
|
else if (absTotalDy >= absTotalDx * 1.25d)
|
|
{
|
|
if (absFirstDy <= coordinateTolerance || Math.Sign(firstDy) != Math.Sign(totalDy))
|
|
{
|
|
score += 600d;
|
|
}
|
|
else if (absFirstDx > absFirstDy * 1.25d)
|
|
{
|
|
score += 120d;
|
|
}
|
|
}
|
|
|
|
return score;
|
|
}
|
|
|
|
private static int FindPreferredGatewayExitContinuationIndex(
|
|
IReadOnlyList<ElkPoint> path,
|
|
ElkPositionedNode sourceNode,
|
|
int firstExteriorIndex)
|
|
{
|
|
if (path.Count <= firstExteriorIndex + 1)
|
|
{
|
|
return firstExteriorIndex;
|
|
}
|
|
|
|
var firstContinuation = path[firstExteriorIndex];
|
|
var finalTarget = path[^1];
|
|
var start = path[0];
|
|
var firstDx = firstContinuation.X - start.X;
|
|
var firstDy = firstContinuation.Y - start.Y;
|
|
var desiredDx = finalTarget.X - start.X;
|
|
var desiredDy = finalTarget.Y - start.Y;
|
|
const double coordinateTolerance = 0.5d;
|
|
|
|
var bestIndex = firstExteriorIndex;
|
|
var bestScore = ScoreGatewayExitContinuationPoint(path[firstExteriorIndex], start, finalTarget, firstExteriorIndex, sourceNode.Kind, coordinateTolerance);
|
|
for (var i = firstExteriorIndex + 1; i < path.Count; i++)
|
|
{
|
|
if (!ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(sourceNode, path[i]))
|
|
{
|
|
var score = ScoreGatewayExitContinuationPoint(path[i], start, finalTarget, i, sourceNode.Kind, coordinateTolerance);
|
|
if (score < bestScore)
|
|
{
|
|
bestScore = score;
|
|
bestIndex = i;
|
|
}
|
|
}
|
|
}
|
|
|
|
return bestIndex;
|
|
}
|
|
|
|
private static int? FindGatewaySourceCurlRecoveryIndex(
|
|
IReadOnlyList<ElkPoint> path,
|
|
int firstExteriorIndex)
|
|
{
|
|
if (!HasGatewaySourceExitCurl(path) || path.Count <= firstExteriorIndex + 1)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
const double coordinateTolerance = 0.5d;
|
|
var start = path[0];
|
|
var finalTarget = path[^1];
|
|
var desiredDx = finalTarget.X - start.X;
|
|
var desiredDy = finalTarget.Y - start.Y;
|
|
for (var i = firstExteriorIndex + 1; i < path.Count; i++)
|
|
{
|
|
var point = path[i];
|
|
var deltaX = point.X - start.X;
|
|
var deltaY = point.Y - start.Y;
|
|
if (IsGatewayExitAxisAlignedWithDesiredDirection(deltaX, desiredDx, coordinateTolerance)
|
|
&& IsGatewayExitAxisAlignedWithDesiredDirection(deltaY, desiredDy, coordinateTolerance))
|
|
{
|
|
return i;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static bool IsGatewayExitAxisAlignedWithDesiredDirection(
|
|
double delta,
|
|
double desiredDelta,
|
|
double tolerance)
|
|
{
|
|
if (Math.Abs(desiredDelta) <= tolerance || Math.Abs(delta) <= tolerance)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return Math.Sign(delta) == Math.Sign(desiredDelta);
|
|
}
|
|
|
|
private static double ScoreGatewayExitContinuationPoint(
|
|
ElkPoint point,
|
|
ElkPoint start,
|
|
ElkPoint finalTarget,
|
|
int index,
|
|
string sourceKind,
|
|
double tolerance)
|
|
{
|
|
var desiredDx = finalTarget.X - start.X;
|
|
var desiredDy = finalTarget.Y - start.Y;
|
|
var deltaX = point.X - start.X;
|
|
var deltaY = point.Y - start.Y;
|
|
var score = index * 4d;
|
|
|
|
if (Math.Abs(desiredDx) >= Math.Abs(desiredDy) * 1.25d)
|
|
{
|
|
if (Math.Sign(deltaX) != Math.Sign(desiredDx) || Math.Abs(deltaX) <= tolerance)
|
|
{
|
|
score += 1_000d;
|
|
}
|
|
|
|
if (Math.Abs(desiredDy) <= tolerance)
|
|
{
|
|
score += Math.Abs(point.Y - start.Y) * 1.25d;
|
|
}
|
|
else
|
|
{
|
|
if (Math.Sign(deltaY) != Math.Sign(desiredDy) && Math.Abs(deltaY) > tolerance)
|
|
{
|
|
score += 1_600d;
|
|
}
|
|
|
|
if (desiredDy > tolerance && point.Y > finalTarget.Y + tolerance)
|
|
{
|
|
score += 4_000d;
|
|
}
|
|
else if (desiredDy < -tolerance && point.Y < finalTarget.Y - tolerance)
|
|
{
|
|
score += 4_000d;
|
|
}
|
|
|
|
score += Math.Abs(point.Y - finalTarget.Y) * 2.4d;
|
|
}
|
|
|
|
score -= Math.Abs(deltaX) * 0.2d;
|
|
}
|
|
else if (Math.Abs(desiredDy) >= Math.Abs(desiredDx) * 1.25d)
|
|
{
|
|
if (Math.Sign(deltaY) != Math.Sign(desiredDy) || Math.Abs(deltaY) <= tolerance)
|
|
{
|
|
score += 1_000d;
|
|
}
|
|
|
|
if (Math.Abs(desiredDx) <= tolerance)
|
|
{
|
|
score += Math.Abs(point.X - start.X) * 1.25d;
|
|
}
|
|
else
|
|
{
|
|
if (Math.Sign(deltaX) != Math.Sign(desiredDx) && Math.Abs(deltaX) > tolerance)
|
|
{
|
|
score += 1_600d;
|
|
}
|
|
|
|
if (desiredDx > tolerance && point.X > finalTarget.X + tolerance)
|
|
{
|
|
score += 4_000d;
|
|
}
|
|
else if (desiredDx < -tolerance && point.X < finalTarget.X - tolerance)
|
|
{
|
|
score += 4_000d;
|
|
}
|
|
|
|
score += Math.Abs(point.X - finalTarget.X) * 2.4d;
|
|
}
|
|
|
|
score -= Math.Abs(deltaY) * 0.2d;
|
|
}
|
|
else
|
|
{
|
|
if (Math.Sign(deltaX) != Math.Sign(desiredDx))
|
|
{
|
|
score += 500d;
|
|
}
|
|
|
|
if (Math.Sign(deltaY) != Math.Sign(desiredDy))
|
|
{
|
|
score += 500d;
|
|
}
|
|
|
|
score -= (Math.Abs(deltaX) + Math.Abs(deltaY)) * 0.12d;
|
|
}
|
|
|
|
if (sourceKind == "Decision"
|
|
&& (Math.Abs(deltaX) <= tolerance || Math.Abs(deltaY) <= tolerance))
|
|
{
|
|
score += 120d;
|
|
}
|
|
|
|
return score;
|
|
}
|
|
|
|
private static bool HasGatewaySourceExitBacktracking(IReadOnlyList<ElkPoint> path)
|
|
{
|
|
if (path.Count < 4)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var reference = path[^1];
|
|
var desiredDx = reference.X - path[0].X;
|
|
var desiredDy = reference.Y - path[0].Y;
|
|
var sampleCount = Math.Min(path.Count, 6);
|
|
var absDx = Math.Abs(desiredDx);
|
|
var absDy = Math.Abs(desiredDy);
|
|
if (absDx >= absDy * 1.35d)
|
|
{
|
|
return HasAxisReversalFromStart(path.Take(sampleCount).Select(point => point.X), desiredDx);
|
|
}
|
|
|
|
if (absDy >= absDx * 1.35d)
|
|
{
|
|
return HasAxisReversalFromStart(path.Take(sampleCount).Select(point => point.Y), desiredDy);
|
|
}
|
|
|
|
return HasAxisReversalFromStart(path.Take(sampleCount).Select(point => point.X), desiredDx)
|
|
&& HasAxisReversalFromStart(path.Take(sampleCount).Select(point => point.Y), desiredDy);
|
|
}
|
|
|
|
private static bool HasGatewaySourceExitCurl(IReadOnlyList<ElkPoint> path)
|
|
{
|
|
if (path.Count < 4)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var sampleCount = Math.Min(path.Count, 6);
|
|
var desiredDx = path[^1].X - path[0].X;
|
|
var desiredDy = path[^1].Y - path[0].Y;
|
|
return HasAxisReversalFromStart(path.Take(sampleCount).Select(point => point.X), desiredDx)
|
|
|| HasAxisReversalFromStart(path.Take(sampleCount).Select(point => point.Y), desiredDy);
|
|
}
|
|
|
|
private static bool NeedsGatewaySourceBoundaryRepair(
|
|
IReadOnlyList<ElkPoint> path,
|
|
ElkPositionedNode sourceNode)
|
|
{
|
|
if (path.Count < 2)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return ElkShapeBoundaries.IsNearGatewayVertex(sourceNode, path[0])
|
|
|| ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(sourceNode, path[1])
|
|
|| !ElkShapeBoundaries.HasValidGatewayBoundaryAngle(sourceNode, path[0], path[1])
|
|
|| NeedsDecisionSourcePreferredFaceRepair(path, sourceNode)
|
|
|| HasGatewaySourceExitCurl(path)
|
|
|| HasGatewaySourceExitBacktracking(path);
|
|
}
|
|
|
|
private static List<ElkPoint> ForceDecisionDiagonalSourceExit(
|
|
IReadOnlyList<ElkPoint> sourcePath,
|
|
ElkPositionedNode sourceNode,
|
|
IReadOnlyCollection<ElkPositionedNode> nodes,
|
|
string? sourceNodeId,
|
|
string? targetNodeId)
|
|
{
|
|
var path = sourcePath
|
|
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
|
.ToList();
|
|
if (sourceNode.Kind != "Decision" || path.Count < 3)
|
|
{
|
|
return path;
|
|
}
|
|
|
|
if (ElkShapeBoundaries.IsNearGatewayVertex(sourceNode, path[0], 8d))
|
|
{
|
|
return path;
|
|
}
|
|
|
|
var firstDx = path[1].X - path[0].X;
|
|
var firstDy = path[1].Y - path[0].Y;
|
|
var startsAxisAligned = Math.Abs(firstDx) <= 0.5d || Math.Abs(firstDy) <= 0.5d;
|
|
if (!startsAxisAligned)
|
|
{
|
|
return path;
|
|
}
|
|
|
|
var referenceDx = path[^1].X - path[0].X;
|
|
var referenceDy = path[^1].Y - path[0].Y;
|
|
if (Math.Abs(referenceDx) <= 24d
|
|
|| Math.Abs(referenceDy) <= 24d
|
|
|| Math.Abs(referenceDx) > Math.Abs(referenceDy) * 3d
|
|
|| Math.Abs(referenceDy) > Math.Abs(referenceDx) * 3d)
|
|
{
|
|
return path;
|
|
}
|
|
|
|
var firstExteriorIndex = FindFirstGatewayExteriorPointIndex(path, sourceNode);
|
|
var continuationIndex = FindPreferredGatewayExitContinuationIndex(path, sourceNode, firstExteriorIndex);
|
|
var continuationPoint = path[continuationIndex];
|
|
var boundary = ResolveDecisionSourceExitBoundary(sourceNode, continuationPoint, path[^1]);
|
|
var diagonalExterior = ElkShapeBoundaries.BuildGatewayExteriorApproachPoint(sourceNode, boundary);
|
|
if (ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(sourceNode, diagonalExterior)
|
|
|| !ElkShapeBoundaries.HasValidGatewayBoundaryAngle(sourceNode, boundary, diagonalExterior))
|
|
{
|
|
return path;
|
|
}
|
|
|
|
var candidate = BuildGatewayExitCandidate(path, boundary, diagonalExterior, continuationIndex);
|
|
if (!PathChanged(path, candidate))
|
|
{
|
|
return path;
|
|
}
|
|
|
|
if (!HasAcceptableGatewayBoundaryPath(candidate, nodes, sourceNodeId, targetNodeId, sourceNode, fromStart: true)
|
|
|| HasGatewaySourceExitBacktracking(candidate)
|
|
|| HasGatewaySourceExitCurl(candidate))
|
|
{
|
|
return path;
|
|
}
|
|
|
|
return ComputePathLength(candidate) <= ComputePathLength(path) + 2d
|
|
? candidate
|
|
: path;
|
|
}
|
|
|
|
private static bool NeedsDecisionSourcePreferredFaceRepair(
|
|
IReadOnlyList<ElkPoint> path,
|
|
ElkPositionedNode sourceNode)
|
|
{
|
|
if (sourceNode.Kind != "Decision" || path.Count < 3)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var firstExteriorIndex = FindFirstGatewayExteriorPointIndex(path, sourceNode);
|
|
var continuationIndex = FindPreferredGatewayExitContinuationIndex(path, sourceNode, firstExteriorIndex);
|
|
var continuationPoint = path[continuationIndex];
|
|
var preferredBoundary = ResolveDecisionSourceExitBoundary(sourceNode, continuationPoint, path[^1]);
|
|
if (ElkEdgeRoutingGeometry.PointsEqual(path[0], preferredBoundary))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return ElkShapeBoundaries.IsNearGatewayVertex(sourceNode, path[0], 8d)
|
|
|| ElkEdgeRoutingGeometry.ComputeSegmentLength(path[0], preferredBoundary) > 6d;
|
|
}
|
|
|
|
private static bool NeedsGatewayTargetBoundaryRepair(
|
|
IReadOnlyList<ElkPoint> path,
|
|
ElkPositionedNode targetNode)
|
|
{
|
|
if (path.Count < 2)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return ElkShapeBoundaries.IsNearGatewayVertex(targetNode, path[^1])
|
|
|| ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(targetNode, path[^2])
|
|
|| !ElkShapeBoundaries.HasValidGatewayBoundaryAngle(targetNode, path[^1], path[^2]);
|
|
}
|
|
|
|
private static List<ElkPoint> RepairGatewaySourceBoundaryPath(
|
|
IReadOnlyList<ElkPoint> sourcePath,
|
|
ElkPositionedNode sourceNode,
|
|
IReadOnlyCollection<ElkPositionedNode> nodes,
|
|
string? sourceNodeId,
|
|
string? targetNodeId)
|
|
{
|
|
var directSourceCandidate = TryBuildDirectGatewaySourcePath(
|
|
sourcePath,
|
|
sourceNode,
|
|
nodes,
|
|
sourceNodeId,
|
|
targetNodeId);
|
|
if (PathChanged(sourcePath, directSourceCandidate)
|
|
&& HasAcceptableGatewayBoundaryPath(directSourceCandidate, nodes, sourceNodeId, targetNodeId, sourceNode, fromStart: true)
|
|
&& !HasGatewaySourceExitBacktracking(directSourceCandidate)
|
|
&& !HasGatewaySourceExitCurl(directSourceCandidate)
|
|
&& !HasGatewaySourceDominantAxisDetour(directSourceCandidate, sourceNode)
|
|
&& !HasGatewaySourcePreferredFaceMismatch(directSourceCandidate, sourceNode))
|
|
{
|
|
return directSourceCandidate;
|
|
}
|
|
|
|
var normalizedCandidate = NormalizeGatewayExitPath(
|
|
sourcePath,
|
|
sourceNode,
|
|
nodes,
|
|
sourceNodeId,
|
|
targetNodeId);
|
|
if (PathChanged(sourcePath, normalizedCandidate)
|
|
&& HasAcceptableGatewayBoundaryPath(normalizedCandidate, nodes, sourceNodeId, targetNodeId, sourceNode, fromStart: true)
|
|
&& !HasGatewaySourceExitBacktracking(normalizedCandidate)
|
|
&& !HasGatewaySourceExitCurl(normalizedCandidate)
|
|
&& !HasGatewaySourceDominantAxisDetour(normalizedCandidate, sourceNode)
|
|
&& !HasGatewaySourcePreferredFaceMismatch(normalizedCandidate, sourceNode))
|
|
{
|
|
return normalizedCandidate;
|
|
}
|
|
|
|
var blockerEscapeCandidate = TryBuildGatewaySourceDominantBlockerEscapePath(
|
|
sourcePath,
|
|
sourceNode,
|
|
nodes,
|
|
sourceNodeId,
|
|
targetNodeId);
|
|
if (PathChanged(sourcePath, blockerEscapeCandidate)
|
|
&& HasAcceptableGatewayBoundaryPath(blockerEscapeCandidate, nodes, sourceNodeId, targetNodeId, sourceNode, fromStart: true)
|
|
&& !HasGatewaySourceExitBacktracking(blockerEscapeCandidate)
|
|
&& !HasGatewaySourceExitCurl(blockerEscapeCandidate)
|
|
&& !HasGatewaySourceDominantAxisDetour(blockerEscapeCandidate, sourceNode)
|
|
&& !HasGatewaySourcePreferredFaceMismatch(blockerEscapeCandidate, sourceNode))
|
|
{
|
|
return blockerEscapeCandidate;
|
|
}
|
|
|
|
var path = sourcePath
|
|
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
|
.ToList();
|
|
if (path.Count < 2)
|
|
{
|
|
return path;
|
|
}
|
|
|
|
var firstExteriorIndex = FindFirstGatewayExteriorPointIndex(path, sourceNode);
|
|
var continuationIndex = FindPreferredGatewayExitContinuationIndex(path, sourceNode, firstExteriorIndex);
|
|
|
|
var continuationPoint = path[continuationIndex];
|
|
ElkPoint boundary;
|
|
if (sourceNode.Kind == "Decision")
|
|
{
|
|
boundary = ResolveDecisionSourceExitBoundary(sourceNode, continuationPoint, path[^1]);
|
|
}
|
|
else
|
|
{
|
|
boundary = ElkShapeBoundaries.ProjectOntoShapeBoundary(sourceNode, continuationPoint);
|
|
boundary = ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(sourceNode, boundary, continuationPoint);
|
|
}
|
|
|
|
var normalized = BuildGatewaySourceRepairPath(
|
|
path,
|
|
sourceNode,
|
|
boundary,
|
|
continuationPoint,
|
|
continuationIndex,
|
|
path[^1]);
|
|
return HasAcceptableGatewayBoundaryPath(normalized, nodes, sourceNodeId, targetNodeId, sourceNode, fromStart: true)
|
|
? normalized
|
|
: path;
|
|
}
|
|
|
|
private static List<ElkPoint> TryBuildGatewaySourceDominantBlockerEscapePath(
|
|
IReadOnlyList<ElkPoint> sourcePath,
|
|
ElkPositionedNode sourceNode,
|
|
IReadOnlyCollection<ElkPositionedNode> nodes,
|
|
string? sourceNodeId,
|
|
string? targetNodeId)
|
|
{
|
|
var path = sourcePath
|
|
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
|
.ToList();
|
|
if (sourceNode.Kind != "Decision"
|
|
|| path.Count < 3
|
|
|| !HasGatewaySourceDominantAxisDetour(path, sourceNode))
|
|
{
|
|
return path;
|
|
}
|
|
|
|
var centerX = sourceNode.X + (sourceNode.Width / 2d);
|
|
var centerY = sourceNode.Y + (sourceNode.Height / 2d);
|
|
var desiredDx = path[^1].X - centerX;
|
|
var desiredDy = path[^1].Y - centerY;
|
|
var dominantVertical = Math.Abs(desiredDy) >= Math.Abs(desiredDx) * 1.25d && Math.Sign(desiredDy) != 0;
|
|
if (!dominantVertical)
|
|
{
|
|
return path;
|
|
}
|
|
|
|
var clearance = 24d;
|
|
var direction = Math.Sign(desiredDy);
|
|
var targetNode = string.IsNullOrWhiteSpace(targetNodeId)
|
|
? null
|
|
: nodes.FirstOrDefault(node => string.Equals(node.Id, targetNodeId, StringComparison.Ordinal));
|
|
var firstExteriorIndex = FindFirstGatewayExteriorPointIndex(path, sourceNode);
|
|
List<ElkPoint>? bestCandidate = null;
|
|
var bestScore = double.PositiveInfinity;
|
|
|
|
foreach (var continuationIndex in EnumerateGatewayDirectRepairContinuationIndices(path, sourceNode, firstExteriorIndex))
|
|
{
|
|
var anchorX = path[continuationIndex].X;
|
|
if (Math.Abs(anchorX - path[0].X) <= 3d)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var dominantReference = new ElkPoint { X = anchorX, Y = path[^1].Y };
|
|
if (!TryResolvePreferredGatewaySourceBoundary(sourceNode, dominantReference, path[^1], out var provisionalBoundary))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var stemBlockers = nodes
|
|
.Where(node =>
|
|
!string.Equals(node.Id, sourceNodeId, StringComparison.Ordinal)
|
|
&& !string.Equals(node.Id, targetNodeId, StringComparison.Ordinal)
|
|
&& provisionalBoundary.X > node.X + 0.5d
|
|
&& provisionalBoundary.X < node.X + node.Width - 0.5d
|
|
&& (direction > 0d
|
|
? node.Y > provisionalBoundary.Y + 0.5d && node.Y < path[^1].Y - 0.5d
|
|
: node.Y + node.Height < provisionalBoundary.Y - 0.5d && node.Y + node.Height > path[^1].Y + 0.5d))
|
|
.OrderBy(node => direction > 0d ? node.Y : -(node.Y + node.Height))
|
|
.ToArray();
|
|
if (stemBlockers.Length == 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
foreach (var blocker in stemBlockers)
|
|
{
|
|
var escapeY = direction > 0d
|
|
? blocker.Y - clearance
|
|
: blocker.Y + blocker.Height + clearance;
|
|
if (direction > 0d)
|
|
{
|
|
if (escapeY <= provisionalBoundary.Y + 8d || escapeY >= blocker.Y - 0.5d)
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
else if (escapeY >= provisionalBoundary.Y - 8d || escapeY <= blocker.Y + blocker.Height + 0.5d)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var continuationPoint = new ElkPoint { X = anchorX, Y = escapeY };
|
|
var boundary = provisionalBoundary;
|
|
|
|
var candidate = BuildGatewaySourceRepairPath(
|
|
path,
|
|
sourceNode,
|
|
boundary,
|
|
continuationPoint,
|
|
continuationIndex,
|
|
continuationPoint);
|
|
if (!PathChanged(path, candidate)
|
|
|| !HasAcceptableGatewayBoundaryPath(candidate, nodes, sourceNodeId, targetNodeId, sourceNode, fromStart: true)
|
|
|| !HasClearBoundarySegments(candidate, nodes, sourceNodeId, targetNodeId, true, Math.Min(4, candidate.Count - 1))
|
|
|| (targetNode is not null
|
|
&& (ElkShapeBoundaries.IsGatewayShape(targetNode)
|
|
? !HasAcceptableGatewayBoundaryPath(candidate, nodes, sourceNodeId, targetNodeId, targetNode, fromStart: false)
|
|
: candidate.Count < 2 || !HasValidBoundaryAngle(candidate[^1], candidate[^2], targetNode)))
|
|
|| HasGatewaySourceExitBacktracking(candidate)
|
|
|| HasGatewaySourceExitCurl(candidate)
|
|
|| HasGatewaySourceDominantAxisDetour(candidate, sourceNode)
|
|
|| HasGatewaySourcePreferredFaceMismatch(candidate, sourceNode))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var score = ScoreGatewayDirectRepairCandidate(path, candidate, sourceNode, continuationIndex);
|
|
if (score >= bestScore)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
bestScore = score;
|
|
bestCandidate = candidate;
|
|
}
|
|
}
|
|
|
|
if (bestCandidate is null)
|
|
{
|
|
return path;
|
|
}
|
|
|
|
return IsMaterialGatewaySourceRepairImprovement(path, bestCandidate)
|
|
|| IsGatewaySourceGeometryRepairImprovement(path, bestCandidate, sourceNode)
|
|
? bestCandidate
|
|
: path;
|
|
}
|
|
|
|
private static List<ElkPoint> RepairProtectedGatewaySourceBoundaryPath(
|
|
IReadOnlyList<ElkPoint> sourcePath,
|
|
ElkPositionedNode sourceNode,
|
|
double graphMinY,
|
|
double graphMaxY)
|
|
{
|
|
var path = sourcePath
|
|
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
|
.ToList();
|
|
if (path.Count < 3)
|
|
{
|
|
return path;
|
|
}
|
|
|
|
var corridorIndex = -1;
|
|
for (var i = 1; i < path.Count; i++)
|
|
{
|
|
if (path[i].Y < graphMinY - 8d || path[i].Y > graphMaxY + 8d)
|
|
{
|
|
corridorIndex = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (corridorIndex < 1)
|
|
{
|
|
return path;
|
|
}
|
|
|
|
var corridorPoint = path[corridorIndex];
|
|
var boundary = sourceNode.Kind == "Decision"
|
|
? ResolveDecisionSourceExitBoundary(sourceNode, corridorPoint, corridorPoint)
|
|
: ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(
|
|
sourceNode,
|
|
ElkShapeBoundaries.ProjectOntoShapeBoundary(sourceNode, corridorPoint),
|
|
corridorPoint);
|
|
|
|
return BuildGatewaySourceRepairPath(
|
|
path,
|
|
sourceNode,
|
|
boundary,
|
|
corridorPoint,
|
|
corridorIndex,
|
|
corridorPoint);
|
|
}
|
|
|
|
private static List<ElkPoint> TryBuildProtectedGatewaySourcePath(
|
|
IReadOnlyList<ElkPoint> sourcePath,
|
|
ElkPositionedNode sourceNode,
|
|
IReadOnlyCollection<ElkPositionedNode> nodes,
|
|
string? sourceNodeId,
|
|
string? targetNodeId)
|
|
{
|
|
var graphMinY = nodes.Min(node => node.Y);
|
|
var graphMaxY = nodes.Max(node => node.Y + node.Height);
|
|
var candidate = RepairProtectedGatewaySourceBoundaryPath(sourcePath, sourceNode, graphMinY, graphMaxY);
|
|
if (!PathChanged(sourcePath, candidate))
|
|
{
|
|
return sourcePath.Select(point => new ElkPoint { X = point.X, Y = point.Y }).ToList();
|
|
}
|
|
|
|
if (!TryNormalizeTargetBoundaryAfterSourceRepair(
|
|
candidate,
|
|
nodes,
|
|
sourceNodeId,
|
|
targetNodeId,
|
|
out candidate))
|
|
{
|
|
return sourcePath.Select(point => new ElkPoint { X = point.X, Y = point.Y }).ToList();
|
|
}
|
|
|
|
return candidate;
|
|
}
|
|
|
|
private static List<ElkPoint> BuildGatewaySourceRepairPath(
|
|
IReadOnlyList<ElkPoint> path,
|
|
ElkPositionedNode sourceNode,
|
|
ElkPoint boundary,
|
|
ElkPoint continuationPoint,
|
|
int continuationIndex,
|
|
ElkPoint referencePoint)
|
|
{
|
|
List<ElkPoint>? bestCandidate = null;
|
|
var bestScore = double.PositiveInfinity;
|
|
|
|
foreach (var exteriorApproach in ResolveGatewayExteriorApproachCandidates(sourceNode, boundary, referencePoint))
|
|
{
|
|
foreach (var preferDirectContinuation in new[] { true, false })
|
|
{
|
|
var rebuilt = new List<ElkPoint> { boundary };
|
|
if (!ElkEdgeRoutingGeometry.PointsEqual(boundary, exteriorApproach))
|
|
{
|
|
rebuilt.Add(exteriorApproach);
|
|
}
|
|
|
|
if (!ElkEdgeRoutingGeometry.PointsEqual(rebuilt[^1], continuationPoint))
|
|
{
|
|
var allowDirectContinuation = preferDirectContinuation
|
|
&& CanUseDirectGatewayContinuation(rebuilt[^1], continuationPoint);
|
|
if (!allowDirectContinuation)
|
|
{
|
|
var curlRecoveryCorner = ResolveGatewaySourceCurlRecoveryCorner(path, rebuilt[^1], continuationPoint);
|
|
if (curlRecoveryCorner is not null)
|
|
{
|
|
rebuilt.Add(curlRecoveryCorner);
|
|
}
|
|
else
|
|
{
|
|
AppendGatewayOrthogonalCorner(
|
|
rebuilt,
|
|
rebuilt[^1],
|
|
continuationPoint,
|
|
continuationIndex + 1 < path.Count ? path[continuationIndex + 1] : null,
|
|
preferHorizontalFromReference: ShouldPreferHorizontalGatewayExit(rebuilt[^1], continuationPoint));
|
|
}
|
|
}
|
|
|
|
if (!ElkEdgeRoutingGeometry.PointsEqual(rebuilt[^1], continuationPoint))
|
|
{
|
|
rebuilt.Add(continuationPoint);
|
|
}
|
|
}
|
|
|
|
for (var i = continuationIndex + 1; i < path.Count; i++)
|
|
{
|
|
rebuilt.Add(path[i]);
|
|
}
|
|
|
|
var candidate = NormalizePathPoints(rebuilt);
|
|
candidate = SnapGatewaySourceStubToDominantAxis(candidate, sourceNode, referencePoint);
|
|
var score = ComputePathLength(candidate) + ScoreGatewayExitProgress(sourceNode, candidate, referencePoint);
|
|
if (preferDirectContinuation)
|
|
{
|
|
score -= 6d;
|
|
}
|
|
|
|
if (HasGatewaySourceExitBacktracking(candidate)
|
|
|| HasGatewaySourceExitCurl(candidate))
|
|
{
|
|
score += 100_000d;
|
|
}
|
|
|
|
if (HasGatewaySourceDominantAxisDetour(candidate, sourceNode))
|
|
{
|
|
score += 100_000d;
|
|
}
|
|
|
|
if (HasGatewaySourcePreferredFaceMismatch(candidate, sourceNode))
|
|
{
|
|
score += 50_000d;
|
|
}
|
|
|
|
if (NeedsDecisionSourcePreferredFaceRepair(candidate, sourceNode))
|
|
{
|
|
score += 50_000d;
|
|
}
|
|
|
|
if (score >= bestScore)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
bestScore = score;
|
|
bestCandidate = candidate;
|
|
}
|
|
}
|
|
|
|
return bestCandidate
|
|
?? path.Select(point => new ElkPoint { X = point.X, Y = point.Y }).ToList();
|
|
}
|
|
|
|
private static ElkPoint? ResolveGatewaySourceCurlRecoveryCorner(
|
|
IReadOnlyList<ElkPoint> path,
|
|
ElkPoint from,
|
|
ElkPoint to)
|
|
{
|
|
const double coordinateTolerance = 0.5d;
|
|
if (!HasGatewaySourceExitCurl(path)
|
|
|| Math.Abs(from.X - to.X) <= coordinateTolerance
|
|
|| Math.Abs(from.Y - to.Y) <= coordinateTolerance)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var desiredDx = path[^1].X - path[0].X;
|
|
var desiredDy = path[^1].Y - path[0].Y;
|
|
if (Math.Abs(desiredDx) >= Math.Abs(desiredDy) * 1.25d
|
|
&& Math.Abs(desiredDy) > coordinateTolerance
|
|
&& Math.Sign(to.Y - from.Y) == Math.Sign(desiredDy))
|
|
{
|
|
return new ElkPoint { X = from.X, Y = to.Y };
|
|
}
|
|
|
|
if (Math.Abs(desiredDy) >= Math.Abs(desiredDx) * 1.25d
|
|
&& Math.Abs(desiredDx) > coordinateTolerance
|
|
&& Math.Sign(to.X - from.X) == Math.Sign(desiredDx))
|
|
{
|
|
return new ElkPoint { X = to.X, Y = from.Y };
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static List<ElkPoint> TryBuildDirectGatewaySourcePath(
|
|
IReadOnlyList<ElkPoint> sourcePath,
|
|
ElkPositionedNode sourceNode,
|
|
IReadOnlyCollection<ElkPositionedNode> nodes,
|
|
string? sourceNodeId,
|
|
string? targetNodeId)
|
|
{
|
|
var path = sourcePath
|
|
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
|
.ToList();
|
|
if (path.Count < 2 || !ElkShapeBoundaries.IsGatewayShape(sourceNode))
|
|
{
|
|
return path;
|
|
}
|
|
|
|
var firstExteriorIndex = FindFirstGatewayExteriorPointIndex(path, sourceNode);
|
|
List<ElkPoint>? bestCandidate = null;
|
|
var bestScore = double.PositiveInfinity;
|
|
foreach (var continuationIndex in EnumerateGatewayDirectRepairContinuationIndices(path, sourceNode, firstExteriorIndex))
|
|
{
|
|
var continuationPoint = path[continuationIndex];
|
|
var boundaryCandidates = new List<ElkPoint>();
|
|
if (TryResolvePreferredGatewaySourceBoundary(sourceNode, continuationPoint, path[^1], out var preferredBoundary))
|
|
{
|
|
AddUniquePoint(boundaryCandidates, preferredBoundary);
|
|
}
|
|
|
|
AddUniquePoint(
|
|
boundaryCandidates,
|
|
ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(
|
|
sourceNode,
|
|
ElkShapeBoundaries.ProjectOntoShapeBoundary(sourceNode, continuationPoint),
|
|
continuationPoint));
|
|
|
|
foreach (var candidateBoundary in ResolveGatewayExitBoundaryCandidates(sourceNode, path[^1]))
|
|
{
|
|
AddUniquePoint(boundaryCandidates, candidateBoundary);
|
|
}
|
|
|
|
foreach (var boundaryCandidate in boundaryCandidates)
|
|
{
|
|
var candidate = BuildGatewaySourceRepairPath(
|
|
path,
|
|
sourceNode,
|
|
boundaryCandidate,
|
|
continuationPoint,
|
|
continuationIndex,
|
|
path[^1]);
|
|
|
|
if (!PathChanged(path, candidate))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!HasAcceptableGatewayBoundaryPath(candidate, nodes, sourceNodeId, targetNodeId, sourceNode, fromStart: true)
|
|
|| !HasClearBoundarySegments(candidate, nodes, sourceNodeId, targetNodeId, true, Math.Min(4, candidate.Count - 1))
|
|
|| HasGatewaySourceExitBacktracking(candidate)
|
|
|| HasGatewaySourceExitCurl(candidate)
|
|
|| HasGatewaySourceDominantAxisDetour(candidate, sourceNode)
|
|
|| HasGatewaySourcePreferredFaceMismatch(candidate, sourceNode))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var score = ScoreGatewayDirectRepairCandidate(path, candidate, sourceNode, continuationIndex);
|
|
if (score >= bestScore)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
bestScore = score;
|
|
bestCandidate = candidate;
|
|
}
|
|
}
|
|
|
|
if (bestCandidate is null)
|
|
{
|
|
return path;
|
|
}
|
|
|
|
if (!IsMaterialGatewaySourceRepairImprovement(path, bestCandidate)
|
|
&& !IsGatewaySourceGeometryRepairImprovement(path, bestCandidate, sourceNode))
|
|
{
|
|
return path;
|
|
}
|
|
|
|
return bestCandidate;
|
|
}
|
|
|
|
internal static bool HasClearGatewaySourceDirectRepairOpportunity(
|
|
IReadOnlyList<ElkPoint> sourcePath,
|
|
ElkPositionedNode sourceNode,
|
|
IReadOnlyCollection<ElkPositionedNode> nodes,
|
|
string? sourceNodeId,
|
|
string? targetNodeId)
|
|
{
|
|
if (!ElkShapeBoundaries.IsGatewayShape(sourceNode) || sourcePath.Count < 2)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var candidate = TryBuildDirectGatewaySourcePath(
|
|
sourcePath,
|
|
sourceNode,
|
|
nodes,
|
|
sourceNodeId,
|
|
targetNodeId);
|
|
return IsMaterialGatewaySourceRepairImprovement(sourcePath, candidate);
|
|
}
|
|
|
|
internal static bool HasClearGatewaySourceScoringOpportunity(
|
|
IReadOnlyList<ElkPoint> sourcePath,
|
|
ElkPositionedNode sourceNode,
|
|
IReadOnlyCollection<ElkPositionedNode> nodes,
|
|
string? sourceNodeId,
|
|
string? targetNodeId)
|
|
{
|
|
if (!ElkShapeBoundaries.IsGatewayShape(sourceNode) || sourcePath.Count < 2)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var candidate = HasProtectedGatewaySourceCorridorPath(sourcePath, nodes)
|
|
? TryBuildProtectedGatewaySourcePath(
|
|
sourcePath,
|
|
sourceNode,
|
|
nodes,
|
|
sourceNodeId,
|
|
targetNodeId)
|
|
: TryBuildDirectGatewaySourcePath(
|
|
sourcePath,
|
|
sourceNode,
|
|
nodes,
|
|
sourceNodeId,
|
|
targetNodeId);
|
|
if (!IsMaterialGatewaySourceRepairImprovement(sourcePath, candidate)
|
|
|| !HasAcceptableGatewayBoundaryPath(candidate, nodes, sourceNodeId, targetNodeId, sourceNode, fromStart: true))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!TryNormalizeTargetBoundaryAfterSourceRepair(
|
|
candidate,
|
|
nodes,
|
|
sourceNodeId,
|
|
targetNodeId,
|
|
out candidate))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (HasGatewaySourceExitBacktracking(candidate)
|
|
|| HasGatewaySourceExitCurl(candidate)
|
|
|| HasGatewaySourceDominantAxisDetour(candidate, sourceNode)
|
|
|| HasGatewaySourcePreferredFaceMismatch(candidate, sourceNode)
|
|
|| HasGatewaySourceLeadIntoDominantBlocker(
|
|
candidate,
|
|
sourceNode,
|
|
nodes,
|
|
sourceNodeId,
|
|
targetNodeId))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var lengthGain = ComputePathLength(sourcePath) - ComputePathLength(candidate);
|
|
var originalBends = Math.Max(0, sourcePath.Count - 2);
|
|
var candidateBends = Math.Max(0, candidate.Count - 2);
|
|
if (lengthGain < 12d && candidateBends >= originalBends)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return IsMaterialGatewaySourceRepairImprovement(sourcePath, candidate);
|
|
}
|
|
|
|
private static List<ElkPoint> EnforceGatewaySourceExitQuality(
|
|
IReadOnlyList<ElkPoint> sourcePath,
|
|
ElkPositionedNode sourceNode,
|
|
IReadOnlyCollection<ElkPositionedNode> nodes,
|
|
string? sourceNodeId,
|
|
string? targetNodeId)
|
|
{
|
|
var path = sourcePath
|
|
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
|
.ToList();
|
|
if (!ElkShapeBoundaries.IsGatewayShape(sourceNode) || path.Count < 2)
|
|
{
|
|
return path;
|
|
}
|
|
|
|
var centerX = sourceNode.X + (sourceNode.Width / 2d);
|
|
var centerY = sourceNode.Y + (sourceNode.Height / 2d);
|
|
var desiredDx = path[^1].X - centerX;
|
|
var desiredDy = path[^1].Y - centerY;
|
|
var dominantHorizontal = Math.Abs(desiredDx) >= Math.Abs(desiredDy) * 1.25d && Math.Sign(desiredDx) != 0;
|
|
var dominantVertical = Math.Abs(desiredDy) >= Math.Abs(desiredDx) * 1.25d && Math.Sign(desiredDy) != 0;
|
|
var allowDominantAxisRepair = sourceNode.Kind is not "Decision" || dominantHorizontal || dominantVertical;
|
|
var scoringCandidate = HasProtectedGatewaySourceCorridorPath(path, nodes)
|
|
? TryBuildProtectedGatewaySourcePath(
|
|
path,
|
|
sourceNode,
|
|
nodes,
|
|
sourceNodeId,
|
|
targetNodeId)
|
|
: TryBuildDirectGatewaySourcePath(
|
|
path,
|
|
sourceNode,
|
|
nodes,
|
|
sourceNodeId,
|
|
targetNodeId);
|
|
var directDominantCandidate = TryBuildDirectDominantGatewaySourcePath(path, sourceNode, nodes, sourceNodeId, targetNodeId);
|
|
var hasDominantDirectOpportunity = allowDominantAxisRepair
|
|
&& PathChanged(path, directDominantCandidate)
|
|
&& ComputePathLength(directDominantCandidate) + 1d < ComputePathLength(path);
|
|
var requiresRepair = HasGatewaySourceExitBacktracking(path)
|
|
|| HasGatewaySourceExitCurl(path)
|
|
|| hasDominantDirectOpportunity
|
|
|| (allowDominantAxisRepair && HasGatewaySourcePreferredFaceMismatch(path, sourceNode))
|
|
|| (allowDominantAxisRepair && HasGatewaySourceDominantAxisDetour(path, sourceNode))
|
|
|| (allowDominantAxisRepair && HasClearGatewaySourceScoringOpportunity(path, sourceNode, nodes, sourceNodeId, targetNodeId));
|
|
if (!requiresRepair)
|
|
{
|
|
return path;
|
|
}
|
|
|
|
List<ElkPoint>? bestCandidate = null;
|
|
var bestScore = double.PositiveInfinity;
|
|
|
|
void ConsiderCandidate(IReadOnlyList<ElkPoint> rawCandidate)
|
|
{
|
|
if (!PathChanged(path, rawCandidate))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var candidate = rawCandidate
|
|
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
|
.ToList();
|
|
if (!TryNormalizeTargetBoundaryAfterSourceRepair(
|
|
candidate,
|
|
nodes,
|
|
sourceNodeId,
|
|
targetNodeId,
|
|
out candidate))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!HasAcceptableGatewayBoundaryPath(candidate, nodes, sourceNodeId, targetNodeId, sourceNode, fromStart: true)
|
|
|| HasGatewaySourceExitBacktracking(candidate)
|
|
|| HasGatewaySourceExitCurl(candidate)
|
|
|| HasGatewaySourceDominantAxisDetour(candidate, sourceNode)
|
|
|| HasGatewaySourcePreferredFaceMismatch(candidate, sourceNode)
|
|
|| HasGatewaySourceLeadIntoDominantBlocker(candidate, sourceNode, nodes, sourceNodeId, targetNodeId)
|
|
|| HasClearGatewaySourceScoringOpportunity(candidate, sourceNode, nodes, sourceNodeId, targetNodeId))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var score = ComputePathLength(candidate) + (Math.Max(0, candidate.Count - 2) * 6d);
|
|
if (score >= bestScore)
|
|
{
|
|
return;
|
|
}
|
|
|
|
bestScore = score;
|
|
bestCandidate = candidate;
|
|
}
|
|
|
|
ConsiderCandidate(scoringCandidate);
|
|
ConsiderCandidate(directDominantCandidate);
|
|
ConsiderCandidate(TryBuildDirectGatewaySourcePath(path, sourceNode, nodes, sourceNodeId, targetNodeId));
|
|
ConsiderCandidate(ForceGatewaySourcePreferredFaceAlignment(path, sourceNode));
|
|
ConsiderCandidate(FixGatewaySourceDominantAxisDetour(path, sourceNode));
|
|
ConsiderCandidate(NormalizeGatewayExitPath(path, sourceNode, nodes, sourceNodeId, targetNodeId));
|
|
|
|
return bestCandidate ?? path;
|
|
}
|
|
|
|
private static List<ElkPoint> TryBuildDirectDominantGatewaySourcePath(
|
|
IReadOnlyList<ElkPoint> sourcePath,
|
|
ElkPositionedNode sourceNode,
|
|
IReadOnlyCollection<ElkPositionedNode> nodes,
|
|
string? sourceNodeId,
|
|
string? targetNodeId)
|
|
{
|
|
var path = sourcePath
|
|
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
|
.ToList();
|
|
if (!ElkShapeBoundaries.IsGatewayShape(sourceNode)
|
|
|| path.Count < 2
|
|
|| string.IsNullOrWhiteSpace(targetNodeId))
|
|
{
|
|
return path;
|
|
}
|
|
|
|
var targetNode = nodes.FirstOrDefault(node => string.Equals(node.Id, targetNodeId, StringComparison.Ordinal));
|
|
if (targetNode is null || ElkShapeBoundaries.IsGatewayShape(targetNode))
|
|
{
|
|
return path;
|
|
}
|
|
|
|
var centerX = sourceNode.X + (sourceNode.Width / 2d);
|
|
var centerY = sourceNode.Y + (sourceNode.Height / 2d);
|
|
var targetCenterX = targetNode.X + (targetNode.Width / 2d);
|
|
var targetCenterY = targetNode.Y + (targetNode.Height / 2d);
|
|
var desiredDx = targetCenterX - centerX;
|
|
var desiredDy = targetCenterY - centerY;
|
|
var dominantHorizontal = Math.Abs(desiredDx) >= Math.Abs(desiredDy) * 1.25d && Math.Sign(desiredDx) != 0;
|
|
var dominantVertical = Math.Abs(desiredDy) >= Math.Abs(desiredDx) * 1.25d && Math.Sign(desiredDy) != 0;
|
|
if (!dominantHorizontal && !dominantVertical)
|
|
{
|
|
return path;
|
|
}
|
|
|
|
var firstExteriorIndex = FindFirstGatewayExteriorPointIndex(path, sourceNode);
|
|
var continuationIndex = FindPreferredGatewayExitContinuationIndex(path, sourceNode, firstExteriorIndex);
|
|
var continuationPoint = path[continuationIndex];
|
|
if (!TryResolvePreferredGatewaySourceBoundary(sourceNode, continuationPoint, path[^1], out var boundary))
|
|
{
|
|
return path;
|
|
}
|
|
|
|
var targetEndpoint = dominantHorizontal
|
|
? new ElkPoint
|
|
{
|
|
X = desiredDx >= 0d ? targetNode.X : targetNode.X + targetNode.Width,
|
|
Y = Math.Clamp(boundary.Y, targetNode.Y, targetNode.Y + targetNode.Height),
|
|
}
|
|
: new ElkPoint
|
|
{
|
|
X = Math.Clamp(boundary.X, targetNode.X, targetNode.X + targetNode.Width),
|
|
Y = desiredDy >= 0d ? targetNode.Y : targetNode.Y + targetNode.Height,
|
|
};
|
|
|
|
var obstacles = nodes
|
|
.Select(node => (
|
|
Left: node.X,
|
|
Top: node.Y,
|
|
Right: node.X + node.Width,
|
|
Bottom: node.Y + node.Height,
|
|
Id: node.Id))
|
|
.ToArray();
|
|
if (SegmentCrossesObstacle(boundary, targetEndpoint, obstacles, sourceNodeId ?? string.Empty, targetNodeId))
|
|
{
|
|
var bypassCandidate = TryBuildDominantAxisGatewaySourceBypassPath(
|
|
sourceNode,
|
|
targetNode,
|
|
boundary,
|
|
targetEndpoint,
|
|
obstacles,
|
|
sourceNodeId,
|
|
targetNodeId,
|
|
dominantHorizontal,
|
|
desiredDx,
|
|
desiredDy);
|
|
return bypassCandidate ?? path;
|
|
}
|
|
|
|
var rebuilt = new List<ElkPoint> { boundary };
|
|
var gatewayStub = dominantHorizontal
|
|
? new ElkPoint
|
|
{
|
|
X = boundary.X + (desiredDx >= 0d ? 24d : -24d),
|
|
Y = boundary.Y,
|
|
}
|
|
: new ElkPoint
|
|
{
|
|
X = boundary.X,
|
|
Y = boundary.Y + (desiredDy >= 0d ? 24d : -24d),
|
|
};
|
|
if (!ElkEdgeRoutingGeometry.PointsEqual(rebuilt[^1], gatewayStub))
|
|
{
|
|
rebuilt.Add(gatewayStub);
|
|
}
|
|
|
|
AppendNonGatewayTargetBoundaryApproach(rebuilt, targetNode, targetEndpoint);
|
|
|
|
return NormalizePathPoints(rebuilt);
|
|
}
|
|
|
|
private static List<ElkPoint>? TryBuildDominantAxisGatewaySourceBypassPath(
|
|
ElkPositionedNode sourceNode,
|
|
ElkPositionedNode targetNode,
|
|
ElkPoint boundary,
|
|
ElkPoint targetEndpoint,
|
|
(double Left, double Top, double Right, double Bottom, string Id)[] obstacles,
|
|
string? sourceNodeId,
|
|
string? targetNodeId,
|
|
bool dominantHorizontal,
|
|
double desiredDx,
|
|
double desiredDy)
|
|
{
|
|
const double padding = 8d;
|
|
const double coordinateTolerance = 0.5d;
|
|
var sourceId = sourceNodeId ?? string.Empty;
|
|
var targetId = targetNodeId ?? string.Empty;
|
|
List<ElkPoint>? bestCandidate = null;
|
|
var bestScore = double.PositiveInfinity;
|
|
|
|
void ConsiderCandidate(List<ElkPoint> rawCandidate)
|
|
{
|
|
var candidate = NormalizePathPoints(rawCandidate);
|
|
if (candidate.Count < 2
|
|
|| !IsPathClearOfObstacles(candidate, obstacles, sourceId, targetId)
|
|
|| !HasValidBoundaryAngle(candidate[^1], candidate[^2], targetNode)
|
|
|| HasGatewaySourceExitBacktracking(candidate)
|
|
|| HasGatewaySourceExitCurl(candidate)
|
|
|| HasGatewaySourceDominantAxisDetour(candidate, sourceNode)
|
|
|| HasGatewaySourcePreferredFaceMismatch(candidate, sourceNode))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var score = ComputePathLength(candidate) + (Math.Max(0, candidate.Count - 2) * 6d);
|
|
if (score >= bestScore)
|
|
{
|
|
return;
|
|
}
|
|
|
|
bestScore = score;
|
|
bestCandidate = candidate;
|
|
}
|
|
|
|
if (dominantHorizontal)
|
|
{
|
|
var movingRight = desiredDx >= 0d;
|
|
var firstBlocker = obstacles
|
|
.Where(ob => !string.Equals(ob.Id, sourceId, StringComparison.Ordinal)
|
|
&& !string.Equals(ob.Id, targetId, StringComparison.Ordinal)
|
|
&& boundary.Y > ob.Top + coordinateTolerance
|
|
&& boundary.Y < ob.Bottom - coordinateTolerance
|
|
&& (movingRight
|
|
? ob.Left > boundary.X + coordinateTolerance && ob.Left < targetEndpoint.X - coordinateTolerance
|
|
: ob.Right < boundary.X - coordinateTolerance && ob.Right > targetEndpoint.X + coordinateTolerance))
|
|
.OrderBy(ob => movingRight ? ob.Left : -ob.Right)
|
|
.FirstOrDefault();
|
|
if (string.IsNullOrWhiteSpace(firstBlocker.Id))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var axisX = movingRight
|
|
? firstBlocker.Left - padding
|
|
: firstBlocker.Right + padding;
|
|
if (movingRight ? axisX <= boundary.X + 2d : axisX >= boundary.X - 2d)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var bypassYCandidates = new List<double>();
|
|
AddUniqueCoordinate(bypassYCandidates, targetEndpoint.Y);
|
|
AddUniqueCoordinate(bypassYCandidates, firstBlocker.Top - padding);
|
|
AddUniqueCoordinate(bypassYCandidates, firstBlocker.Bottom + padding);
|
|
foreach (var bypassY in bypassYCandidates)
|
|
{
|
|
var diagonalLead = new List<ElkPoint> { boundary };
|
|
var diagonalLeadPoint = new ElkPoint { X = axisX, Y = bypassY };
|
|
if (!ElkEdgeRoutingGeometry.PointsEqual(diagonalLead[^1], diagonalLeadPoint))
|
|
{
|
|
diagonalLead.Add(diagonalLeadPoint);
|
|
}
|
|
|
|
AppendNonGatewayTargetBoundaryApproach(diagonalLead, targetNode, targetEndpoint);
|
|
ConsiderCandidate(diagonalLead);
|
|
|
|
var rebuilt = new List<ElkPoint>
|
|
{
|
|
boundary,
|
|
new() { X = axisX, Y = boundary.Y },
|
|
};
|
|
|
|
if (Math.Abs(rebuilt[^1].Y - bypassY) > coordinateTolerance)
|
|
{
|
|
rebuilt.Add(new ElkPoint { X = axisX, Y = bypassY });
|
|
}
|
|
|
|
AppendNonGatewayTargetBoundaryApproach(rebuilt, targetNode, targetEndpoint);
|
|
|
|
ConsiderCandidate(rebuilt);
|
|
}
|
|
|
|
return bestCandidate;
|
|
}
|
|
|
|
var movingDown = desiredDy >= 0d;
|
|
var verticalBlocker = obstacles
|
|
.Where(ob => !string.Equals(ob.Id, sourceId, StringComparison.Ordinal)
|
|
&& !string.Equals(ob.Id, targetId, StringComparison.Ordinal)
|
|
&& boundary.X > ob.Left + coordinateTolerance
|
|
&& boundary.X < ob.Right - coordinateTolerance
|
|
&& (movingDown
|
|
? ob.Top > boundary.Y + coordinateTolerance && ob.Top < targetEndpoint.Y - coordinateTolerance
|
|
: ob.Bottom < boundary.Y - coordinateTolerance && ob.Bottom > targetEndpoint.Y + coordinateTolerance))
|
|
.OrderBy(ob => movingDown ? ob.Top : -ob.Bottom)
|
|
.FirstOrDefault();
|
|
if (string.IsNullOrWhiteSpace(verticalBlocker.Id))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var axisY = movingDown
|
|
? verticalBlocker.Top - padding
|
|
: verticalBlocker.Bottom + padding;
|
|
if (movingDown ? axisY <= boundary.Y + 2d : axisY >= boundary.Y - 2d)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var bypassXCandidates = new List<double>();
|
|
AddUniqueCoordinate(bypassXCandidates, targetEndpoint.X);
|
|
AddUniqueCoordinate(bypassXCandidates, verticalBlocker.Left - padding);
|
|
AddUniqueCoordinate(bypassXCandidates, verticalBlocker.Right + padding);
|
|
foreach (var bypassX in bypassXCandidates)
|
|
{
|
|
var diagonalLead = new List<ElkPoint> { boundary };
|
|
var diagonalLeadPoint = new ElkPoint { X = bypassX, Y = axisY };
|
|
if (!ElkEdgeRoutingGeometry.PointsEqual(diagonalLead[^1], diagonalLeadPoint))
|
|
{
|
|
diagonalLead.Add(diagonalLeadPoint);
|
|
}
|
|
|
|
AppendNonGatewayTargetBoundaryApproach(diagonalLead, targetNode, targetEndpoint);
|
|
ConsiderCandidate(diagonalLead);
|
|
|
|
var rebuilt = new List<ElkPoint>
|
|
{
|
|
boundary,
|
|
new() { X = boundary.X, Y = axisY },
|
|
};
|
|
|
|
if (Math.Abs(rebuilt[^1].X - bypassX) > coordinateTolerance)
|
|
{
|
|
rebuilt.Add(new ElkPoint { X = bypassX, Y = axisY });
|
|
}
|
|
|
|
AppendNonGatewayTargetBoundaryApproach(rebuilt, targetNode, targetEndpoint);
|
|
|
|
ConsiderCandidate(rebuilt);
|
|
}
|
|
|
|
return bestCandidate;
|
|
}
|
|
|
|
private static bool IsPathClearOfObstacles(
|
|
IReadOnlyList<ElkPoint> path,
|
|
(double Left, double Top, double Right, double Bottom, string Id)[] obstacles,
|
|
string sourceId,
|
|
string targetId)
|
|
{
|
|
for (var i = 0; i < path.Count - 1; i++)
|
|
{
|
|
if (SegmentCrossesObstacle(path[i], path[i + 1], obstacles, sourceId, targetId))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private static void AppendNonGatewayTargetBoundaryApproach(
|
|
ICollection<ElkPoint> rawPoints,
|
|
ElkPositionedNode targetNode,
|
|
ElkPoint targetEndpoint)
|
|
{
|
|
var rebuilt = rawPoints as List<ElkPoint>;
|
|
if (rebuilt is null || rebuilt.Count == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var targetSide = ElkEdgeRoutingGeometry.ResolveBoundarySide(targetEndpoint, targetNode);
|
|
ElkPoint approachPoint;
|
|
switch (targetSide)
|
|
{
|
|
case "left":
|
|
approachPoint = new ElkPoint { X = targetEndpoint.X - 24d, Y = targetEndpoint.Y };
|
|
break;
|
|
case "right":
|
|
approachPoint = new ElkPoint { X = targetEndpoint.X + 24d, Y = targetEndpoint.Y };
|
|
break;
|
|
case "top":
|
|
approachPoint = new ElkPoint { X = targetEndpoint.X, Y = targetEndpoint.Y - 24d };
|
|
break;
|
|
case "bottom":
|
|
approachPoint = new ElkPoint { X = targetEndpoint.X, Y = targetEndpoint.Y + 24d };
|
|
break;
|
|
default:
|
|
approachPoint = targetEndpoint;
|
|
break;
|
|
}
|
|
|
|
if (!ElkEdgeRoutingGeometry.PointsEqual(rebuilt[^1], approachPoint))
|
|
{
|
|
AppendGatewayOrthogonalCorner(
|
|
rebuilt,
|
|
rebuilt[^1],
|
|
approachPoint,
|
|
null,
|
|
preferHorizontalFromReference: targetSide is "top" or "bottom");
|
|
|
|
if (!ElkEdgeRoutingGeometry.PointsEqual(rebuilt[^1], approachPoint))
|
|
{
|
|
rebuilt.Add(approachPoint);
|
|
}
|
|
}
|
|
|
|
if (!ElkEdgeRoutingGeometry.PointsEqual(rebuilt[^1], targetEndpoint))
|
|
{
|
|
rebuilt.Add(targetEndpoint);
|
|
}
|
|
}
|
|
|
|
private static bool HasGatewaySourceLeadIntoDominantBlocker(
|
|
IReadOnlyList<ElkPoint> path,
|
|
ElkPositionedNode sourceNode,
|
|
IReadOnlyCollection<ElkPositionedNode> nodes,
|
|
string? sourceNodeId,
|
|
string? targetNodeId)
|
|
{
|
|
if (!ElkShapeBoundaries.IsGatewayShape(sourceNode) || path.Count < 2)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
const double tolerance = 0.5d;
|
|
var centerX = sourceNode.X + (sourceNode.Width / 2d);
|
|
var centerY = sourceNode.Y + (sourceNode.Height / 2d);
|
|
var desiredDx = path[^1].X - centerX;
|
|
var desiredDy = path[^1].Y - centerY;
|
|
var dominantHorizontal = Math.Abs(desiredDx) >= Math.Abs(desiredDy) * 1.25d && Math.Sign(desiredDx) != 0;
|
|
var dominantVertical = Math.Abs(desiredDy) >= Math.Abs(desiredDx) * 1.25d && Math.Sign(desiredDy) != 0;
|
|
if (!dominantHorizontal && !dominantVertical)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var boundary = path[0];
|
|
var adjacent = path[1];
|
|
if (dominantHorizontal)
|
|
{
|
|
var movingRight = desiredDx > 0d;
|
|
var blocker = nodes
|
|
.Where(node => !string.Equals(node.Id, sourceNodeId, StringComparison.Ordinal)
|
|
&& !string.Equals(node.Id, targetNodeId, StringComparison.Ordinal)
|
|
&& boundary.Y > node.Y + tolerance
|
|
&& boundary.Y < node.Y + node.Height - tolerance
|
|
&& (movingRight
|
|
? node.X > boundary.X + tolerance && node.X < path[^1].X - tolerance
|
|
: node.X + node.Width < boundary.X - tolerance && node.X + node.Width > path[^1].X + tolerance))
|
|
.OrderBy(node => movingRight ? node.X : -(node.X + node.Width))
|
|
.FirstOrDefault();
|
|
if (blocker is null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return adjacent.Y > blocker.Y + tolerance
|
|
&& adjacent.Y < blocker.Y + blocker.Height - tolerance;
|
|
}
|
|
|
|
var movingDown = desiredDy > 0d;
|
|
var verticalBlocker = nodes
|
|
.Where(node => !string.Equals(node.Id, sourceNodeId, StringComparison.Ordinal)
|
|
&& !string.Equals(node.Id, targetNodeId, StringComparison.Ordinal)
|
|
&& boundary.X > node.X + tolerance
|
|
&& boundary.X < node.X + node.Width - tolerance
|
|
&& (movingDown
|
|
? node.Y > boundary.Y + tolerance && node.Y < path[^1].Y - tolerance
|
|
: node.Y + node.Height < boundary.Y - tolerance && node.Y + node.Height > path[^1].Y + tolerance))
|
|
.OrderBy(node => movingDown ? node.Y : -(node.Y + node.Height))
|
|
.FirstOrDefault();
|
|
if (verticalBlocker is null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return adjacent.X > verticalBlocker.X + tolerance
|
|
&& adjacent.X < verticalBlocker.X + verticalBlocker.Width - tolerance;
|
|
}
|
|
|
|
private static bool TryNormalizeTargetBoundaryAfterSourceRepair(
|
|
IReadOnlyList<ElkPoint> candidatePath,
|
|
IReadOnlyCollection<ElkPositionedNode> nodes,
|
|
string? sourceNodeId,
|
|
string? targetNodeId,
|
|
out List<ElkPoint> normalized)
|
|
{
|
|
normalized = candidatePath
|
|
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
|
.ToList();
|
|
if (string.IsNullOrWhiteSpace(targetNodeId) || normalized.Count < 2)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
var targetNode = nodes.FirstOrDefault(node => string.Equals(node.Id, targetNodeId, StringComparison.Ordinal));
|
|
if (targetNode is null)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (ElkShapeBoundaries.IsGatewayShape(targetNode))
|
|
{
|
|
if (!NeedsGatewayTargetBoundaryRepair(normalized, targetNode))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
var repairedTargetCandidate = NormalizeGatewayEntryPath(normalized, targetNode, normalized[^1]);
|
|
if (!CanAcceptGatewayTargetRepair(repairedTargetCandidate, targetNode))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
normalized = repairedTargetCandidate;
|
|
return true;
|
|
}
|
|
|
|
if (TryRealignNonGatewayTargetBoundarySlot(normalized, targetNode, nodes, sourceNodeId, targetNodeId, out var realignedTargetCandidate))
|
|
{
|
|
normalized = realignedTargetCandidate;
|
|
}
|
|
|
|
if (HasValidBoundaryAngle(normalized[^1], normalized[^2], targetNode))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
var targetSide = ElkEdgeRoutingGeometry.ResolveBoundarySide(normalized[^1], targetNode);
|
|
var repairedNonGatewayTarget = NormalizeEntryPath(normalized, targetNode, targetSide);
|
|
if (!HasClearBoundarySegments(repairedNonGatewayTarget, nodes, sourceNodeId, targetNodeId, false, 3)
|
|
|| !HasValidBoundaryAngle(repairedNonGatewayTarget[^1], repairedNonGatewayTarget[^2], targetNode))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
normalized = repairedNonGatewayTarget;
|
|
return true;
|
|
}
|
|
|
|
private static bool TryRealignNonGatewayTargetBoundarySlot(
|
|
IReadOnlyList<ElkPoint> path,
|
|
ElkPositionedNode targetNode,
|
|
IReadOnlyCollection<ElkPositionedNode> nodes,
|
|
string? sourceNodeId,
|
|
string? targetNodeId,
|
|
out List<ElkPoint> realigned)
|
|
{
|
|
realigned = path
|
|
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
|
.ToList();
|
|
if (path.Count < 2)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var side = ElkEdgeRoutingGeometry.ResolveBoundarySide(path[^1], targetNode);
|
|
if (side is not "left" and not "right" and not "top" and not "bottom")
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var approach = path[^2];
|
|
var candidateEndpoint = side switch
|
|
{
|
|
"left" => new ElkPoint
|
|
{
|
|
X = targetNode.X,
|
|
Y = Math.Clamp(approach.Y, targetNode.Y, targetNode.Y + targetNode.Height),
|
|
},
|
|
"right" => new ElkPoint
|
|
{
|
|
X = targetNode.X + targetNode.Width,
|
|
Y = Math.Clamp(approach.Y, targetNode.Y, targetNode.Y + targetNode.Height),
|
|
},
|
|
"top" => new ElkPoint
|
|
{
|
|
X = Math.Clamp(approach.X, targetNode.X, targetNode.X + targetNode.Width),
|
|
Y = targetNode.Y,
|
|
},
|
|
"bottom" => new ElkPoint
|
|
{
|
|
X = Math.Clamp(approach.X, targetNode.X, targetNode.X + targetNode.Width),
|
|
Y = targetNode.Y + targetNode.Height,
|
|
},
|
|
_ => path[^1],
|
|
};
|
|
|
|
if (ElkEdgeRoutingGeometry.PointsEqual(candidateEndpoint, path[^1]))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
realigned[^1] = candidateEndpoint;
|
|
realigned = NormalizePathPoints(realigned);
|
|
if (!HasClearBoundarySegments(realigned, nodes, sourceNodeId, targetNodeId, false, 3)
|
|
|| !HasValidBoundaryAngle(realigned[^1], realigned[^2], targetNode))
|
|
{
|
|
realigned = path
|
|
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
|
.ToList();
|
|
return false;
|
|
}
|
|
|
|
var originalLength = ComputePathLength(path);
|
|
var realignedLength = ComputePathLength(realigned);
|
|
if (realignedLength + 0.5d < originalLength)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
var directlyAligned = side is "left" or "right"
|
|
? Math.Abs(realigned[^1].Y - approach.Y) <= 0.6d
|
|
: Math.Abs(realigned[^1].X - approach.X) <= 0.6d;
|
|
return directlyAligned && realignedLength <= originalLength + 0.5d;
|
|
}
|
|
|
|
private static IEnumerable<int> EnumerateGatewayDirectRepairContinuationIndices(
|
|
IReadOnlyList<ElkPoint> path,
|
|
ElkPositionedNode sourceNode,
|
|
int firstExteriorIndex)
|
|
{
|
|
if (path.Count <= firstExteriorIndex)
|
|
{
|
|
yield return firstExteriorIndex;
|
|
yield break;
|
|
}
|
|
|
|
var preferredIndex = FindPreferredGatewayExitContinuationIndex(path, sourceNode, firstExteriorIndex);
|
|
var curlRecoveryIndex = FindGatewaySourceCurlRecoveryIndex(path, firstExteriorIndex);
|
|
var seen = new HashSet<int>();
|
|
var candidates = new[]
|
|
{
|
|
firstExteriorIndex,
|
|
Math.Min(path.Count - 1, firstExteriorIndex + 1),
|
|
Math.Min(path.Count - 1, firstExteriorIndex + 2),
|
|
preferredIndex,
|
|
Math.Min(path.Count - 1, preferredIndex + 1),
|
|
curlRecoveryIndex ?? -1,
|
|
curlRecoveryIndex is int recoveryIndex
|
|
? Math.Min(path.Count - 1, recoveryIndex + 1)
|
|
: -1,
|
|
};
|
|
|
|
foreach (var candidate in candidates)
|
|
{
|
|
if (candidate < firstExteriorIndex
|
|
|| candidate >= path.Count
|
|
|| ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(sourceNode, path[candidate])
|
|
|| !seen.Add(candidate))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
yield return candidate;
|
|
}
|
|
}
|
|
|
|
private static double ScoreGatewayDirectRepairCandidate(
|
|
IReadOnlyList<ElkPoint> originalPath,
|
|
IReadOnlyList<ElkPoint> candidate,
|
|
ElkPositionedNode sourceNode,
|
|
int continuationIndex)
|
|
{
|
|
var score = ComputePathLength(candidate)
|
|
+ (Math.Max(0, candidate.Count - 2) * 4d)
|
|
+ (continuationIndex * 6d)
|
|
+ ScoreGatewayExitProgress(sourceNode, candidate, originalPath[^1]);
|
|
if (HasGatewaySourceExitBacktracking(candidate)
|
|
|| HasGatewaySourceExitCurl(candidate))
|
|
{
|
|
score += 100_000d;
|
|
}
|
|
|
|
if (HasGatewaySourceDominantAxisDetour(candidate, sourceNode))
|
|
{
|
|
score += 50_000d;
|
|
}
|
|
|
|
if (HasGatewaySourcePreferredFaceMismatch(candidate, sourceNode))
|
|
{
|
|
score += 25_000d;
|
|
}
|
|
|
|
return score;
|
|
}
|
|
|
|
private static bool IsMaterialGatewaySourceRepairImprovement(
|
|
IReadOnlyList<ElkPoint> originalPath,
|
|
IReadOnlyList<ElkPoint> candidate)
|
|
{
|
|
if (!PathChanged(originalPath, candidate))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var originalLength = ComputePathLength(originalPath);
|
|
var candidateLength = ComputePathLength(candidate);
|
|
var originalBends = Math.Max(0, originalPath.Count - 2);
|
|
var candidateBends = Math.Max(0, candidate.Count - 2);
|
|
var lengthGain = originalLength - candidateLength;
|
|
if (originalPath.Count <= 3
|
|
&& lengthGain < 24d
|
|
&& candidateBends <= originalBends)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (lengthGain > 4d)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (lengthGain > 1d && candidateBends <= originalBends)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (candidateBends + 1 < originalBends
|
|
&& candidateLength <= originalLength + 4d)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return candidateBends < originalBends
|
|
&& candidateLength <= originalLength + 1d;
|
|
}
|
|
|
|
private static bool IsGatewaySourceGeometryRepairImprovement(
|
|
IReadOnlyList<ElkPoint> originalPath,
|
|
IReadOnlyList<ElkPoint> candidate,
|
|
ElkPositionedNode sourceNode)
|
|
{
|
|
if (!PathChanged(originalPath, candidate))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var originalHasGeometryDefect = NeedsGatewaySourceBoundaryRepair(originalPath, sourceNode)
|
|
|| HasGatewaySourceExitBacktracking(originalPath)
|
|
|| HasGatewaySourceExitCurl(originalPath)
|
|
|| HasGatewaySourceDominantAxisDetour(originalPath, sourceNode)
|
|
|| HasGatewaySourcePreferredFaceMismatch(originalPath, sourceNode)
|
|
|| NeedsDecisionSourcePreferredFaceRepair(originalPath, sourceNode);
|
|
if (!originalHasGeometryDefect)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var candidateIsClean = !NeedsGatewaySourceBoundaryRepair(candidate, sourceNode)
|
|
&& !HasGatewaySourceExitBacktracking(candidate)
|
|
&& !HasGatewaySourceExitCurl(candidate)
|
|
&& !HasGatewaySourceDominantAxisDetour(candidate, sourceNode)
|
|
&& !HasGatewaySourcePreferredFaceMismatch(candidate, sourceNode)
|
|
&& !NeedsDecisionSourcePreferredFaceRepair(candidate, sourceNode);
|
|
if (!candidateIsClean)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var originalLength = ComputePathLength(originalPath);
|
|
var candidateLength = ComputePathLength(candidate);
|
|
return candidateLength <= originalLength + 120d;
|
|
}
|
|
|
|
private static bool TryResolvePreferredGatewaySourceBoundary(
|
|
ElkPositionedNode sourceNode,
|
|
ElkPoint referencePoint,
|
|
out ElkPoint boundary)
|
|
{
|
|
return TryResolvePreferredGatewaySourceBoundary(
|
|
sourceNode,
|
|
referencePoint,
|
|
referencePoint,
|
|
out boundary);
|
|
}
|
|
|
|
private static bool TryResolvePreferredGatewaySourceBoundary(
|
|
ElkPositionedNode sourceNode,
|
|
ElkPoint continuationPoint,
|
|
ElkPoint referencePoint,
|
|
out ElkPoint boundary)
|
|
{
|
|
boundary = default!;
|
|
if (!ElkShapeBoundaries.IsGatewayShape(sourceNode))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (sourceNode.Kind == "Decision")
|
|
{
|
|
boundary = ResolveDecisionSourceExitBoundary(sourceNode, continuationPoint, referencePoint);
|
|
return true;
|
|
}
|
|
|
|
foreach (var preferredSide in EnumeratePreferredGatewaySourceSides(sourceNode, continuationPoint, referencePoint))
|
|
{
|
|
foreach (var candidate in ResolveGatewaySourceBoundarySlotCandidates(sourceNode, preferredSide, continuationPoint, referencePoint))
|
|
{
|
|
boundary = candidate;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
boundary = sourceNode.Kind == "Decision"
|
|
? ResolveDecisionSourceExitBoundary(sourceNode, continuationPoint, referencePoint)
|
|
: ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(
|
|
sourceNode,
|
|
ElkShapeBoundaries.ProjectOntoShapeBoundary(sourceNode, continuationPoint),
|
|
continuationPoint);
|
|
return true;
|
|
}
|
|
|
|
private static bool HasProtectedGatewaySourceCorridorPath(
|
|
IReadOnlyList<ElkPoint> path,
|
|
IReadOnlyCollection<ElkPositionedNode> nodes)
|
|
{
|
|
if (path.Count < 3 || nodes.Count == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var graphMinY = nodes.Min(node => node.Y);
|
|
var graphMaxY = nodes.Max(node => node.Y + node.Height);
|
|
return path.Skip(1).Any(point => point.Y < graphMinY - 8d || point.Y > graphMaxY + 8d);
|
|
}
|
|
|
|
private static List<ElkPoint> SnapGatewaySourceStubToDominantAxis(
|
|
IReadOnlyList<ElkPoint> sourcePath,
|
|
ElkPositionedNode sourceNode,
|
|
ElkPoint referencePoint)
|
|
{
|
|
if (!ElkShapeBoundaries.IsGatewayShape(sourceNode) || sourcePath.Count < 2)
|
|
{
|
|
return sourcePath.Select(point => new ElkPoint { X = point.X, Y = point.Y }).ToList();
|
|
}
|
|
|
|
const double axisTolerance = 4d;
|
|
var boundary = sourcePath[0];
|
|
var adjacent = sourcePath[1];
|
|
var desiredDx = referencePoint.X - boundary.X;
|
|
var desiredDy = referencePoint.Y - boundary.Y;
|
|
var dominantHorizontal = Math.Abs(desiredDx) >= Math.Abs(desiredDy) * 1.15d && Math.Sign(desiredDx) != 0;
|
|
var dominantVertical = Math.Abs(desiredDy) >= Math.Abs(desiredDx) * 1.15d && Math.Sign(desiredDy) != 0;
|
|
|
|
if (!dominantHorizontal && !dominantVertical)
|
|
{
|
|
return sourcePath.Select(point => new ElkPoint { X = point.X, Y = point.Y }).ToList();
|
|
}
|
|
|
|
var snapped = sourcePath
|
|
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
|
.ToList();
|
|
|
|
if (dominantHorizontal && Math.Abs(adjacent.Y - boundary.Y) <= axisTolerance)
|
|
{
|
|
snapped[1] = new ElkPoint
|
|
{
|
|
X = adjacent.X,
|
|
Y = boundary.Y,
|
|
};
|
|
}
|
|
else if (dominantVertical && Math.Abs(adjacent.X - boundary.X) <= axisTolerance)
|
|
{
|
|
snapped[1] = new ElkPoint
|
|
{
|
|
X = boundary.X,
|
|
Y = adjacent.Y,
|
|
};
|
|
}
|
|
|
|
return NormalizePathPoints(snapped);
|
|
}
|
|
|
|
private static bool HasAxisReversalFromStart(IEnumerable<double> values, double desiredDelta)
|
|
{
|
|
const double tolerance = 0.5d;
|
|
var distinctValues = new List<double>();
|
|
foreach (var value in values)
|
|
{
|
|
if (distinctValues.Count == 0 || Math.Abs(distinctValues[^1] - value) > tolerance)
|
|
{
|
|
distinctValues.Add(value);
|
|
}
|
|
}
|
|
|
|
if (distinctValues.Count < 3)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var nonZeroDirections = new List<int>();
|
|
for (var i = 1; i < distinctValues.Count; i++)
|
|
{
|
|
var delta = distinctValues[i] - distinctValues[i - 1];
|
|
if (Math.Abs(delta) <= tolerance)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
nonZeroDirections.Add(Math.Sign(delta));
|
|
}
|
|
|
|
if (nonZeroDirections.Count < 2)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (Math.Abs(desiredDelta) <= tolerance)
|
|
{
|
|
return nonZeroDirections.Distinct().Count() > 1;
|
|
}
|
|
|
|
var desiredSign = Math.Sign(desiredDelta);
|
|
var sawOpposite = false;
|
|
foreach (var direction in nonZeroDirections)
|
|
{
|
|
if (direction == desiredSign)
|
|
{
|
|
if (sawOpposite)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
sawOpposite = true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static List<ElkPoint> NormalizeGatewayEntryPath(
|
|
IReadOnlyList<ElkPoint> sourcePath,
|
|
ElkPositionedNode targetNode,
|
|
ElkPoint assignedEndpoint)
|
|
{
|
|
var path = sourcePath
|
|
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
|
.ToList();
|
|
if (path.Count < 2)
|
|
{
|
|
return path;
|
|
}
|
|
|
|
var exteriorIndex = FindLastGatewayExteriorPointIndex(path, targetNode);
|
|
var exteriorAnchor = path[exteriorIndex];
|
|
var actualAdjacent = path[^2];
|
|
var assignedApproach = ElkShapeBoundaries.IsGatewayBoundaryPoint(targetNode, assignedEndpoint)
|
|
? ElkShapeBoundaries.BuildGatewayDirectionalExteriorPoint(targetNode, assignedEndpoint, exteriorAnchor)
|
|
: assignedEndpoint;
|
|
ElkPoint boundary;
|
|
var assignedEndpointUsable = ElkShapeBoundaries.IsGatewayBoundaryPoint(targetNode, assignedEndpoint)
|
|
&& !ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(targetNode, assignedApproach)
|
|
&& ElkShapeBoundaries.HasValidGatewayBoundaryAngle(targetNode, assignedEndpoint, assignedApproach)
|
|
&& !ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(targetNode, exteriorAnchor);
|
|
if (assignedEndpointUsable)
|
|
{
|
|
boundary = assignedEndpoint;
|
|
}
|
|
else
|
|
{
|
|
var boundaryCandidates = ResolveGatewayEntryBoundaryCandidates(targetNode, exteriorAnchor, assignedEndpoint).ToArray();
|
|
boundary = boundaryCandidates.Length > 0
|
|
? boundaryCandidates
|
|
.OrderBy(candidate => ScoreGatewayEntryBoundaryCandidate(targetNode, candidate, exteriorAnchor, assignedEndpoint))
|
|
.First()
|
|
: ElkShapeBoundaries.ProjectOntoShapeBoundary(targetNode, exteriorAnchor);
|
|
|
|
if (!ElkShapeBoundaries.IsGatewayBoundaryPoint(targetNode, boundary)
|
|
|| !ElkShapeBoundaries.HasValidGatewayBoundaryAngle(targetNode, boundary, exteriorAnchor))
|
|
{
|
|
var fallbackBoundary = ElkShapeBoundaries.ProjectOntoShapeBoundary(targetNode, exteriorAnchor);
|
|
if (!ElkShapeBoundaries.TryProjectGatewayDiagonalBoundary(targetNode, exteriorAnchor, fallbackBoundary, out boundary))
|
|
{
|
|
boundary = fallbackBoundary;
|
|
}
|
|
}
|
|
}
|
|
|
|
boundary = ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(targetNode, boundary, exteriorAnchor);
|
|
var directEntryCandidate = TryBuildDirectGatewayTargetEntry(
|
|
path,
|
|
targetNode,
|
|
exteriorIndex,
|
|
exteriorAnchor,
|
|
boundary,
|
|
assignedEndpoint);
|
|
if (ShouldPreferDirectGatewayTargetEntry(
|
|
directEntryCandidate,
|
|
targetNode,
|
|
assignedEndpoint,
|
|
preserveAssignedSlot: assignedEndpointUsable))
|
|
{
|
|
return directEntryCandidate!;
|
|
}
|
|
|
|
var exteriorApproach = ElkShapeBoundaries.BuildGatewayDirectionalExteriorPoint(targetNode, boundary, exteriorAnchor);
|
|
var rebuilt = path.Take(exteriorIndex + 1).ToList();
|
|
if (rebuilt.Count == 0 || !ElkEdgeRoutingGeometry.PointsEqual(rebuilt[^1], exteriorAnchor))
|
|
{
|
|
rebuilt.Add(exteriorAnchor);
|
|
}
|
|
|
|
AppendGatewayTargetOrthogonalCorner(
|
|
rebuilt,
|
|
rebuilt[^1],
|
|
exteriorApproach,
|
|
rebuilt.Count >= 2 ? rebuilt[^2] : null,
|
|
preferHorizontalFromReference: ShouldPreferHorizontalGatewayExit(rebuilt[^1], exteriorApproach),
|
|
targetNode);
|
|
if (!ElkEdgeRoutingGeometry.PointsEqual(rebuilt[^1], exteriorApproach))
|
|
{
|
|
rebuilt.Add(exteriorApproach);
|
|
}
|
|
|
|
rebuilt.Add(boundary);
|
|
var normalizedRebuilt = NormalizePathPoints(rebuilt);
|
|
if (normalizedRebuilt.Count >= 2
|
|
&& ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(targetNode, normalizedRebuilt[^2]))
|
|
{
|
|
var repairedAnchorIndex = FindLastGatewayExteriorPointIndex(normalizedRebuilt, targetNode);
|
|
var repairedAnchor = normalizedRebuilt[repairedAnchorIndex];
|
|
var repairedApproach = ElkShapeBoundaries.BuildGatewayDirectionalExteriorPoint(targetNode, boundary, repairedAnchor);
|
|
var repaired = normalizedRebuilt.Take(repairedAnchorIndex + 1).ToList();
|
|
if (repaired.Count == 0 || !ElkEdgeRoutingGeometry.PointsEqual(repaired[^1], repairedAnchor))
|
|
{
|
|
repaired.Add(repairedAnchor);
|
|
}
|
|
|
|
AppendGatewayTargetOrthogonalCorner(
|
|
repaired,
|
|
repaired[^1],
|
|
repairedApproach,
|
|
repaired.Count >= 2 ? repaired[^2] : null,
|
|
preferHorizontalFromReference: ShouldPreferHorizontalGatewayExit(repaired[^1], repairedApproach),
|
|
targetNode);
|
|
if (!ElkEdgeRoutingGeometry.PointsEqual(repaired[^1], repairedApproach))
|
|
{
|
|
repaired.Add(repairedApproach);
|
|
}
|
|
|
|
repaired.Add(boundary);
|
|
normalizedRebuilt = NormalizePathPoints(repaired);
|
|
}
|
|
|
|
if (normalizedRebuilt.Count >= 2
|
|
&& (ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(targetNode, normalizedRebuilt[^2])
|
|
|| !ElkShapeBoundaries.HasValidGatewayBoundaryAngle(targetNode, normalizedRebuilt[^1], normalizedRebuilt[^2])))
|
|
{
|
|
normalizedRebuilt = ForceGatewayExteriorTargetApproach(
|
|
normalizedRebuilt,
|
|
targetNode,
|
|
boundary);
|
|
}
|
|
|
|
normalizedRebuilt = PreferGatewayDiagonalTargetEntry(normalizedRebuilt, targetNode);
|
|
if (normalizedRebuilt.Count >= 2
|
|
&& !ElkShapeBoundaries.HasValidGatewayBoundaryAngle(targetNode, normalizedRebuilt[^1], normalizedRebuilt[^2]))
|
|
{
|
|
var slottedGatewayTargetRepair = TryBuildSlottedGatewayEntryPath(path, targetNode, exteriorIndex, exteriorAnchor);
|
|
if (slottedGatewayTargetRepair is not null
|
|
&& ElkShapeBoundaries.HasValidGatewayBoundaryAngle(targetNode, slottedGatewayTargetRepair[^1], slottedGatewayTargetRepair[^2]))
|
|
{
|
|
normalizedRebuilt = slottedGatewayTargetRepair;
|
|
}
|
|
}
|
|
|
|
var preserveAssignedSlot = assignedEndpointUsable;
|
|
if (directEntryCandidate is not null
|
|
&& (!preserveAssignedSlot
|
|
|| ElkEdgeRoutingGeometry.ComputeSegmentLength(directEntryCandidate[^1], assignedEndpoint) <= 6d)
|
|
&& (normalizedRebuilt.Count < 2
|
|
|| !ElkShapeBoundaries.HasValidGatewayBoundaryAngle(targetNode, normalizedRebuilt[^1], normalizedRebuilt[^2])
|
|
|| ComputePathLength(directEntryCandidate) <= ComputePathLength(normalizedRebuilt) + 2d
|
|
|| directEntryCandidate.Count < normalizedRebuilt.Count))
|
|
{
|
|
normalizedRebuilt = directEntryCandidate;
|
|
}
|
|
|
|
normalizedRebuilt = CollapseGatewayTargetTailIfPossible(normalizedRebuilt, targetNode);
|
|
|
|
return normalizedRebuilt;
|
|
}
|
|
|
|
private static List<ElkPoint> ForceGatewayTargetBoundaryStub(
|
|
IReadOnlyList<ElkPoint> sourcePath,
|
|
ElkPositionedNode targetNode)
|
|
{
|
|
var path = sourcePath
|
|
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
|
.ToList();
|
|
if (path.Count < 2)
|
|
{
|
|
return path;
|
|
}
|
|
|
|
var boundary = path[^1];
|
|
var exteriorAnchor = path[^2];
|
|
var exteriorApproach = ElkShapeBoundaries.BuildGatewayDirectionalExteriorPoint(targetNode, boundary, exteriorAnchor);
|
|
if (ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(targetNode, exteriorApproach)
|
|
|| !ElkShapeBoundaries.HasValidGatewayBoundaryAngle(targetNode, boundary, exteriorApproach))
|
|
{
|
|
return path;
|
|
}
|
|
|
|
var rebuilt = path.Take(path.Count - 1)
|
|
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
|
.ToList();
|
|
if (rebuilt.Count == 0 || !ElkEdgeRoutingGeometry.PointsEqual(rebuilt[^1], exteriorAnchor))
|
|
{
|
|
rebuilt.Add(exteriorAnchor);
|
|
}
|
|
|
|
AppendGatewayTargetOrthogonalCorner(
|
|
rebuilt,
|
|
rebuilt[^1],
|
|
exteriorApproach,
|
|
rebuilt.Count >= 2 ? rebuilt[^2] : null,
|
|
preferHorizontalFromReference: ShouldPreferHorizontalGatewayExit(rebuilt[^1], exteriorApproach),
|
|
targetNode);
|
|
if (!ElkEdgeRoutingGeometry.PointsEqual(rebuilt[^1], exteriorApproach))
|
|
{
|
|
rebuilt.Add(exteriorApproach);
|
|
}
|
|
|
|
rebuilt.Add(boundary);
|
|
var normalized = NormalizePathPoints(rebuilt);
|
|
return normalized.Count >= 2
|
|
&& !ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(targetNode, normalized[^2])
|
|
&& ElkShapeBoundaries.HasValidGatewayBoundaryAngle(targetNode, normalized[^1], normalized[^2])
|
|
? normalized
|
|
: path;
|
|
}
|
|
|
|
private static List<ElkPoint>? TryBuildSlottedGatewayEntryPath(
|
|
IReadOnlyList<ElkPoint> sourcePath,
|
|
ElkPositionedNode targetNode,
|
|
int exteriorIndex,
|
|
ElkPoint exteriorAnchor)
|
|
{
|
|
if (!ElkShapeBoundaries.IsGatewayShape(targetNode))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var centerX = targetNode.X + (targetNode.Width / 2d);
|
|
var centerY = targetNode.Y + (targetNode.Height / 2d);
|
|
var deltaX = exteriorAnchor.X - centerX;
|
|
var deltaY = exteriorAnchor.Y - centerY;
|
|
string side;
|
|
double slotCoordinate;
|
|
if (Math.Abs(deltaX) >= Math.Abs(deltaY) * 1.15d)
|
|
{
|
|
side = deltaX <= 0d ? "left" : "right";
|
|
slotCoordinate = exteriorAnchor.Y;
|
|
}
|
|
else
|
|
{
|
|
side = deltaY <= 0d ? "top" : "bottom";
|
|
slotCoordinate = exteriorAnchor.X;
|
|
}
|
|
|
|
if (!ElkShapeBoundaries.TryProjectGatewayBoundarySlot(targetNode, side, slotCoordinate, out var boundary))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return TryBuildSlottedGatewayEntryPath(sourcePath, targetNode, exteriorIndex, exteriorAnchor, boundary);
|
|
}
|
|
|
|
private static List<ElkPoint>? TryBuildSlottedGatewayEntryPath(
|
|
IReadOnlyList<ElkPoint> sourcePath,
|
|
ElkPositionedNode targetNode,
|
|
int exteriorIndex,
|
|
ElkPoint exteriorAnchor,
|
|
ElkPoint boundary)
|
|
{
|
|
if (!ElkShapeBoundaries.IsGatewayShape(targetNode))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
boundary = ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(targetNode, boundary, exteriorAnchor);
|
|
var exteriorApproach = ElkShapeBoundaries.BuildGatewayDirectionalExteriorPoint(targetNode, boundary, exteriorAnchor);
|
|
var rebuilt = sourcePath.Take(exteriorIndex + 1)
|
|
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
|
.ToList();
|
|
if (rebuilt.Count == 0 || !ElkEdgeRoutingGeometry.PointsEqual(rebuilt[^1], exteriorAnchor))
|
|
{
|
|
rebuilt.Add(exteriorAnchor);
|
|
}
|
|
|
|
AppendGatewayTargetOrthogonalCorner(
|
|
rebuilt,
|
|
rebuilt[^1],
|
|
exteriorApproach,
|
|
rebuilt.Count >= 2 ? rebuilt[^2] : null,
|
|
preferHorizontalFromReference: ShouldPreferHorizontalGatewayExit(rebuilt[^1], exteriorApproach),
|
|
targetNode);
|
|
if (!ElkEdgeRoutingGeometry.PointsEqual(rebuilt[^1], exteriorApproach))
|
|
{
|
|
rebuilt.Add(exteriorApproach);
|
|
}
|
|
|
|
rebuilt.Add(boundary);
|
|
var normalized = NormalizePathPoints(rebuilt);
|
|
if (normalized.Count >= 2
|
|
&& (ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(targetNode, normalized[^2])
|
|
|| !ElkShapeBoundaries.HasValidGatewayBoundaryAngle(targetNode, normalized[^1], normalized[^2])))
|
|
{
|
|
normalized = ForceGatewayExteriorTargetApproach(normalized, targetNode, boundary);
|
|
}
|
|
|
|
return normalized.Count >= 2
|
|
&& !ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(targetNode, normalized[^2])
|
|
&& ElkShapeBoundaries.HasValidGatewayBoundaryAngle(targetNode, normalized[^1], normalized[^2])
|
|
? normalized
|
|
: null;
|
|
}
|
|
|
|
private static List<ElkPoint>? TryBuildDirectGatewayTargetEntry(
|
|
IReadOnlyList<ElkPoint> sourcePath,
|
|
ElkPositionedNode targetNode,
|
|
int exteriorIndex,
|
|
ElkPoint exteriorAnchor,
|
|
ElkPoint boundaryPoint,
|
|
ElkPoint assignedEndpoint)
|
|
{
|
|
if (!ElkShapeBoundaries.IsGatewayShape(targetNode))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var prefix = sourcePath.Take(exteriorIndex + 1)
|
|
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
|
.ToList();
|
|
if (prefix.Count == 0 || !ElkEdgeRoutingGeometry.PointsEqual(prefix[^1], exteriorAnchor))
|
|
{
|
|
prefix.Add(new ElkPoint { X = exteriorAnchor.X, Y = exteriorAnchor.Y });
|
|
}
|
|
|
|
var bestPath = default(List<ElkPoint>);
|
|
var bestScore = double.PositiveInfinity;
|
|
foreach (var candidate in ResolveDirectGatewayTargetBoundaryCandidates(targetNode, exteriorAnchor, boundaryPoint, assignedEndpoint))
|
|
{
|
|
if (ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(targetNode, exteriorAnchor)
|
|
|| !ElkShapeBoundaries.HasValidGatewayBoundaryAngle(targetNode, candidate, exteriorAnchor))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var rebuilt = prefix
|
|
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
|
.ToList();
|
|
rebuilt.Add(candidate);
|
|
|
|
var normalized = NormalizePathPoints(rebuilt);
|
|
if (normalized.Count < 2
|
|
|| ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(targetNode, normalized[^2])
|
|
|| !ElkShapeBoundaries.HasValidGatewayBoundaryAngle(targetNode, normalized[^1], normalized[^2]))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var score = ComputePathLength(normalized);
|
|
score += Math.Abs(candidate.X - boundaryPoint.X) + Math.Abs(candidate.Y - boundaryPoint.Y);
|
|
if (ElkShapeBoundaries.IsNearGatewayVertex(targetNode, candidate, 8d))
|
|
{
|
|
score += 1_000d;
|
|
}
|
|
|
|
if (score >= bestScore)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
bestScore = score;
|
|
bestPath = normalized;
|
|
}
|
|
|
|
return bestPath;
|
|
}
|
|
|
|
private static List<ElkPoint> ForceDecisionDirectTargetEntry(
|
|
IReadOnlyList<ElkPoint> sourcePath,
|
|
ElkPositionedNode targetNode)
|
|
{
|
|
if (targetNode.Kind != "Decision" || sourcePath.Count < 3)
|
|
{
|
|
return sourcePath
|
|
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
|
.ToList();
|
|
}
|
|
|
|
var anchor = sourcePath[^3];
|
|
if (ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(targetNode, anchor))
|
|
{
|
|
return sourcePath
|
|
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
|
.ToList();
|
|
}
|
|
|
|
var boundary = ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(
|
|
targetNode,
|
|
ElkShapeBoundaries.ProjectOntoShapeBoundary(targetNode, anchor),
|
|
anchor);
|
|
if (ElkShapeBoundaries.TryProjectGatewayDiagonalBoundary(targetNode, anchor, boundary, out var diagonalBoundary))
|
|
{
|
|
boundary = ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(targetNode, diagonalBoundary, anchor);
|
|
}
|
|
|
|
var rebuilt = sourcePath.Take(sourcePath.Count - 2)
|
|
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
|
.ToList();
|
|
rebuilt.Add(boundary);
|
|
var normalized = NormalizePathPoints(rebuilt);
|
|
return CanAcceptGatewayTargetRepair(normalized, targetNode)
|
|
? normalized
|
|
: sourcePath
|
|
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
|
.ToList();
|
|
}
|
|
|
|
private static List<ElkPoint> ForceDecisionExteriorTargetEntry(
|
|
IReadOnlyList<ElkPoint> sourcePath,
|
|
ElkPositionedNode targetNode)
|
|
{
|
|
var current = sourcePath
|
|
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
|
.ToList();
|
|
if (targetNode.Kind != "Decision" || current.Count < 2)
|
|
{
|
|
return current;
|
|
}
|
|
|
|
var exteriorIndex = FindLastGatewayExteriorPointIndex(current, targetNode);
|
|
var exteriorAnchor = current[exteriorIndex];
|
|
if (ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(targetNode, exteriorAnchor))
|
|
{
|
|
return current;
|
|
}
|
|
|
|
var projectedBoundary = ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(
|
|
targetNode,
|
|
ElkShapeBoundaries.ProjectOntoShapeBoundary(targetNode, exteriorAnchor),
|
|
exteriorAnchor);
|
|
|
|
List<ElkPoint>? bestPath = null;
|
|
var bestScore = double.PositiveInfinity;
|
|
foreach (var boundary in ResolveDirectGatewayTargetBoundaryCandidates(
|
|
targetNode,
|
|
exteriorAnchor,
|
|
projectedBoundary,
|
|
projectedBoundary))
|
|
{
|
|
foreach (var exteriorApproach in ResolveForcedGatewayExteriorApproachCandidates(
|
|
targetNode,
|
|
boundary,
|
|
exteriorAnchor))
|
|
{
|
|
var rebuilt = current.Take(exteriorIndex + 1)
|
|
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
|
.ToList();
|
|
if (rebuilt.Count == 0 || !ElkEdgeRoutingGeometry.PointsEqual(rebuilt[^1], exteriorAnchor))
|
|
{
|
|
rebuilt.Add(new ElkPoint { X = exteriorAnchor.X, Y = exteriorAnchor.Y });
|
|
}
|
|
|
|
AppendGatewayTargetOrthogonalCorner(
|
|
rebuilt,
|
|
rebuilt[^1],
|
|
exteriorApproach,
|
|
rebuilt.Count >= 2 ? rebuilt[^2] : null,
|
|
preferHorizontalFromReference: ShouldPreferHorizontalGatewayExit(rebuilt[^1], exteriorApproach),
|
|
targetNode);
|
|
if (!ElkEdgeRoutingGeometry.PointsEqual(rebuilt[^1], exteriorApproach))
|
|
{
|
|
rebuilt.Add(new ElkPoint { X = exteriorApproach.X, Y = exteriorApproach.Y });
|
|
}
|
|
|
|
rebuilt.Add(new ElkPoint { X = boundary.X, Y = boundary.Y });
|
|
var normalized = NormalizePathPoints(rebuilt);
|
|
if (!CanAcceptGatewayTargetRepair(normalized, targetNode))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var score = ComputePathLength(normalized);
|
|
if (score >= bestScore)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
bestScore = score;
|
|
bestPath = normalized;
|
|
}
|
|
}
|
|
|
|
return bestPath ?? current;
|
|
}
|
|
|
|
private static bool ShouldPreferDirectGatewayTargetEntry(
|
|
IReadOnlyList<ElkPoint>? candidate,
|
|
ElkPositionedNode targetNode,
|
|
ElkPoint assignedEndpoint,
|
|
bool preserveAssignedSlot)
|
|
{
|
|
if (candidate is null || candidate.Count < 2)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(targetNode, candidate[^2])
|
|
|| !ElkShapeBoundaries.HasValidGatewayBoundaryAngle(targetNode, candidate[^1], candidate[^2]))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!preserveAssignedSlot)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
var endpointDelta = ElkEdgeRoutingGeometry.ComputeSegmentLength(candidate[^1], assignedEndpoint);
|
|
if (endpointDelta <= 6d)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// Decision targets can still prefer a direct face entry, but join/fork
|
|
// targets must honor materially different assigned slots so target-side
|
|
// lane separation survives the normalization pass.
|
|
return targetNode.Kind == "Decision";
|
|
}
|
|
|
|
private static List<ElkPoint> CollapseGatewayTargetTailIfPossible(
|
|
IReadOnlyList<ElkPoint> sourcePath,
|
|
ElkPositionedNode targetNode)
|
|
{
|
|
if (!ElkShapeBoundaries.IsGatewayShape(targetNode) || sourcePath.Count < 3)
|
|
{
|
|
return sourcePath
|
|
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
|
.ToList();
|
|
}
|
|
|
|
var current = sourcePath
|
|
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
|
.ToList();
|
|
var boundary = current[^1];
|
|
for (var anchorIndex = current.Count - 3; anchorIndex >= 0; anchorIndex--)
|
|
{
|
|
var anchor = current[anchorIndex];
|
|
if (ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(targetNode, anchor))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
foreach (var candidateBoundary in ResolveDirectGatewayTargetBoundaryCandidates(targetNode, anchor, boundary, boundary))
|
|
{
|
|
var rebuilt = current.Take(anchorIndex + 1)
|
|
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
|
.ToList();
|
|
rebuilt.Add(candidateBoundary);
|
|
var normalized = NormalizePathPoints(rebuilt);
|
|
if (normalized.Count >= 2
|
|
&& CanAcceptGatewayTargetRepair(normalized, targetNode))
|
|
{
|
|
return normalized;
|
|
}
|
|
}
|
|
}
|
|
|
|
return current;
|
|
}
|
|
|
|
private static IEnumerable<ElkPoint> ResolveDirectGatewayTargetBoundaryCandidates(
|
|
ElkPositionedNode targetNode,
|
|
ElkPoint exteriorAnchor,
|
|
ElkPoint boundaryPoint,
|
|
ElkPoint assignedEndpoint)
|
|
{
|
|
var candidates = new List<ElkPoint>();
|
|
AddUniquePoint(candidates, boundaryPoint);
|
|
|
|
var projectedBoundary = ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(
|
|
targetNode,
|
|
ElkShapeBoundaries.ProjectOntoShapeBoundary(targetNode, exteriorAnchor),
|
|
exteriorAnchor);
|
|
AddUniquePoint(candidates, projectedBoundary);
|
|
|
|
if (ElkShapeBoundaries.IsGatewayBoundaryPoint(targetNode, assignedEndpoint))
|
|
{
|
|
AddUniquePoint(
|
|
candidates,
|
|
ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(targetNode, assignedEndpoint, exteriorAnchor));
|
|
}
|
|
|
|
if (ElkShapeBoundaries.TryProjectGatewayDiagonalBoundary(targetNode, exteriorAnchor, projectedBoundary, out var diagonalProjected))
|
|
{
|
|
AddUniquePoint(
|
|
candidates,
|
|
ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(targetNode, diagonalProjected, exteriorAnchor));
|
|
}
|
|
|
|
if (ElkShapeBoundaries.TryProjectGatewayDiagonalBoundary(targetNode, exteriorAnchor, boundaryPoint, out var diagonalBoundary))
|
|
{
|
|
AddUniquePoint(
|
|
candidates,
|
|
ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(targetNode, diagonalBoundary, exteriorAnchor));
|
|
}
|
|
|
|
return candidates;
|
|
}
|
|
|
|
private static List<ElkPoint> PreferGatewayDiagonalTargetEntry(
|
|
IReadOnlyList<ElkPoint> sourcePath,
|
|
ElkPositionedNode targetNode)
|
|
{
|
|
var path = sourcePath
|
|
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
|
.ToList();
|
|
if (!ElkShapeBoundaries.IsGatewayShape(targetNode) || path.Count < 3)
|
|
{
|
|
return path;
|
|
}
|
|
|
|
const double tolerance = 0.5d;
|
|
var boundary = path[^1];
|
|
var adjacent = path[^2];
|
|
var previous = path[^3];
|
|
var lastOrthogonal = Math.Abs(boundary.X - adjacent.X) <= tolerance || Math.Abs(boundary.Y - adjacent.Y) <= tolerance;
|
|
var previousOrthogonal = path.Count == 3
|
|
|| Math.Abs(adjacent.X - previous.X) <= tolerance
|
|
|| Math.Abs(adjacent.Y - previous.Y) <= tolerance;
|
|
if (!lastOrthogonal || !previousOrthogonal)
|
|
{
|
|
return path;
|
|
}
|
|
|
|
if (Math.Abs(boundary.X - previous.X) <= tolerance
|
|
|| Math.Abs(boundary.Y - previous.Y) <= tolerance
|
|
|| ElkShapeBoundaries.IsNearGatewayVertex(targetNode, boundary, 8d)
|
|
|| !ElkShapeBoundaries.HasValidGatewayBoundaryAngle(targetNode, boundary, previous))
|
|
{
|
|
var projectedBoundary = ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(
|
|
targetNode,
|
|
ElkShapeBoundaries.ProjectOntoShapeBoundary(targetNode, previous),
|
|
previous);
|
|
if (ElkShapeBoundaries.TryProjectGatewayDiagonalBoundary(targetNode, previous, projectedBoundary, out var diagonalBoundary))
|
|
{
|
|
projectedBoundary = ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(targetNode, diagonalBoundary, previous);
|
|
}
|
|
|
|
boundary = projectedBoundary;
|
|
}
|
|
|
|
if (ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(targetNode, previous)
|
|
|| !ElkShapeBoundaries.HasValidGatewayBoundaryAngle(targetNode, boundary, previous))
|
|
{
|
|
return path;
|
|
}
|
|
|
|
var rebuilt = path.Take(path.Count - 2)
|
|
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
|
.ToList();
|
|
rebuilt.Add(new ElkPoint { X = boundary.X, Y = boundary.Y });
|
|
return NormalizePathPoints(rebuilt);
|
|
}
|
|
|
|
private static IEnumerable<ElkPoint> ResolveGatewayEntryBoundaryCandidates(
|
|
ElkPositionedNode targetNode,
|
|
ElkPoint exteriorAnchor,
|
|
ElkPoint assignedEndpoint)
|
|
{
|
|
var candidates = new List<ElkPoint>();
|
|
AddUniquePoint(
|
|
candidates,
|
|
ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(
|
|
targetNode,
|
|
ElkShapeBoundaries.ProjectOntoShapeBoundary(targetNode, exteriorAnchor),
|
|
exteriorAnchor));
|
|
|
|
var projectedAssigned = ElkShapeBoundaries.ProjectOntoShapeBoundary(targetNode, assignedEndpoint);
|
|
AddUniquePoint(
|
|
candidates,
|
|
ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(targetNode, projectedAssigned, exteriorAnchor));
|
|
|
|
if (ElkShapeBoundaries.IsGatewayBoundaryPoint(targetNode, assignedEndpoint))
|
|
{
|
|
AddUniquePoint(
|
|
candidates,
|
|
ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(targetNode, assignedEndpoint, exteriorAnchor));
|
|
}
|
|
|
|
foreach (var side in EnumeratePreferredGatewayEntrySides(targetNode, exteriorAnchor))
|
|
{
|
|
var centerX = targetNode.X + (targetNode.Width / 2d);
|
|
var centerY = targetNode.Y + (targetNode.Height / 2d);
|
|
var slotCoordinate = side is "left" or "right"
|
|
? centerY + Math.Clamp(exteriorAnchor.Y - centerY, -targetNode.Height * 0.18d, targetNode.Height * 0.18d)
|
|
: centerX + Math.Clamp(exteriorAnchor.X - centerX, -targetNode.Width * 0.18d, targetNode.Width * 0.18d);
|
|
if (!ElkShapeBoundaries.TryProjectGatewayBoundarySlot(targetNode, side, slotCoordinate, out var slotBoundary))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
AddUniquePoint(
|
|
candidates,
|
|
ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(targetNode, slotBoundary, exteriorAnchor));
|
|
}
|
|
|
|
if (ElkShapeBoundaries.TryProjectGatewayDiagonalBoundary(targetNode, exteriorAnchor, projectedAssigned, out var diagonalBoundary))
|
|
{
|
|
AddUniquePoint(
|
|
candidates,
|
|
ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(targetNode, diagonalBoundary, exteriorAnchor));
|
|
}
|
|
|
|
return candidates;
|
|
}
|
|
|
|
private static IEnumerable<ElkPoint> ResolveGatewayExteriorApproachCandidates(
|
|
ElkPositionedNode node,
|
|
ElkPoint boundary,
|
|
ElkPoint referencePoint,
|
|
double padding = 8d)
|
|
{
|
|
var candidates = new List<ElkPoint>();
|
|
AddUniquePoint(
|
|
candidates,
|
|
ElkShapeBoundaries.BuildPreferredGatewaySourceExteriorPoint(node, boundary, referencePoint, padding));
|
|
var faceNormalCandidate = ElkShapeBoundaries.BuildGatewayExteriorApproachPoint(node, boundary, padding);
|
|
AddUniquePoint(candidates, faceNormalCandidate);
|
|
|
|
var horizontalDirection = Math.Sign(referencePoint.X - boundary.X);
|
|
if (horizontalDirection != 0d)
|
|
{
|
|
AddUniquePoint(
|
|
candidates,
|
|
new ElkPoint
|
|
{
|
|
X = horizontalDirection > 0d
|
|
? node.X + node.Width + padding
|
|
: node.X - padding,
|
|
Y = boundary.Y,
|
|
});
|
|
}
|
|
|
|
var verticalDirection = Math.Sign(referencePoint.Y - boundary.Y);
|
|
if (verticalDirection != 0d)
|
|
{
|
|
AddUniquePoint(
|
|
candidates,
|
|
new ElkPoint
|
|
{
|
|
X = boundary.X,
|
|
Y = verticalDirection > 0d
|
|
? node.Y + node.Height + padding
|
|
: node.Y - padding,
|
|
});
|
|
}
|
|
|
|
return candidates
|
|
.Where(candidate => !ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(node, candidate)
|
|
&& ElkShapeBoundaries.HasValidGatewayBoundaryAngle(node, boundary, candidate))
|
|
.OrderBy(candidate => ScoreGatewayExteriorApproachCandidate(node, boundary, candidate, referencePoint))
|
|
.ToArray();
|
|
}
|
|
|
|
private static IEnumerable<string> EnumeratePreferredGatewaySourceSides(
|
|
ElkPositionedNode sourceNode,
|
|
ElkPoint continuationPoint,
|
|
ElkPoint referencePoint)
|
|
{
|
|
var centerX = sourceNode.X + (sourceNode.Width / 2d);
|
|
var centerY = sourceNode.Y + (sourceNode.Height / 2d);
|
|
var continuationDx = continuationPoint.X - centerX;
|
|
var continuationDy = continuationPoint.Y - centerY;
|
|
var referenceDx = referencePoint.X - centerX;
|
|
var referenceDy = referencePoint.Y - centerY;
|
|
var effectiveDx = Math.Abs(continuationDx) > 12d ? continuationDx : referenceDx;
|
|
var effectiveDy = Math.Abs(continuationDy) > 12d ? continuationDy : referenceDy;
|
|
var absDx = Math.Abs(effectiveDx);
|
|
var absDy = Math.Abs(effectiveDy);
|
|
|
|
var primary = absDx > 12d && (absDx >= absDy * 0.55d || absDy < 20d)
|
|
? effectiveDx >= 0d ? "right" : "left"
|
|
: absDy > 12d
|
|
? effectiveDy >= 0d ? "bottom" : "top"
|
|
: Math.Abs(referenceDx) >= Math.Abs(referenceDy)
|
|
? referenceDx >= 0d ? "right" : "left"
|
|
: referenceDy >= 0d ? "bottom" : "top";
|
|
yield return primary;
|
|
|
|
string? secondary = null;
|
|
if (primary is "left" or "right")
|
|
{
|
|
if (absDy > 12d)
|
|
{
|
|
secondary = effectiveDy >= 0d ? "bottom" : "top";
|
|
}
|
|
}
|
|
else if (absDx > 12d)
|
|
{
|
|
secondary = effectiveDx >= 0d ? "right" : "left";
|
|
}
|
|
|
|
if (secondary is not null
|
|
&& !string.Equals(primary, secondary, StringComparison.Ordinal))
|
|
{
|
|
yield return secondary;
|
|
}
|
|
|
|
var referencePrimary = Math.Abs(referenceDx) > 12d && Math.Abs(referenceDx) >= Math.Abs(referenceDy) * 0.55d
|
|
? referenceDx >= 0d ? "right" : "left"
|
|
: Math.Abs(referenceDy) > 12d
|
|
? referenceDy >= 0d ? "bottom" : "top"
|
|
: null;
|
|
if (referencePrimary is not null
|
|
&& !string.Equals(referencePrimary, primary, StringComparison.Ordinal)
|
|
&& !string.Equals(referencePrimary, secondary, StringComparison.Ordinal))
|
|
{
|
|
yield return referencePrimary;
|
|
}
|
|
}
|
|
|
|
private static bool TryProjectGatewaySourceBoundarySlot(
|
|
ElkPositionedNode sourceNode,
|
|
string side,
|
|
ElkPoint continuationPoint,
|
|
ElkPoint referencePoint,
|
|
out ElkPoint boundary)
|
|
{
|
|
boundary = default!;
|
|
var slotCoordinate = ResolveGatewaySourceSlotCoordinate(sourceNode, side, continuationPoint, referencePoint);
|
|
if (!ElkShapeBoundaries.TryProjectGatewayBoundarySlot(sourceNode, side, slotCoordinate, out boundary))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
boundary = ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(sourceNode, boundary, continuationPoint);
|
|
return true;
|
|
}
|
|
|
|
private static IEnumerable<ElkPoint> ResolveGatewaySourceBoundarySlotCandidates(
|
|
ElkPositionedNode sourceNode,
|
|
string side,
|
|
ElkPoint continuationPoint,
|
|
ElkPoint referencePoint)
|
|
{
|
|
var candidates = new List<ElkPoint>();
|
|
foreach (var slotCoordinate in EnumerateGatewaySourceSlotCoordinates(sourceNode, side, continuationPoint, referencePoint))
|
|
{
|
|
if (!ElkShapeBoundaries.TryProjectGatewayBoundarySlot(sourceNode, side, slotCoordinate, out var boundary))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
boundary = ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(sourceNode, boundary, continuationPoint);
|
|
AddUniquePoint(candidates, boundary);
|
|
}
|
|
|
|
return candidates;
|
|
}
|
|
|
|
private static double ResolveGatewaySourceSlotCoordinate(
|
|
ElkPositionedNode sourceNode,
|
|
string side,
|
|
ElkPoint continuationPoint,
|
|
ElkPoint referencePoint)
|
|
{
|
|
var centerX = sourceNode.X + (sourceNode.Width / 2d);
|
|
var centerY = sourceNode.Y + (sourceNode.Height / 2d);
|
|
var referenceDx = referencePoint.X - centerX;
|
|
var referenceDy = referencePoint.Y - centerY;
|
|
var dominantHorizontal = Math.Abs(referenceDx) >= Math.Abs(referenceDy) * 1.25d;
|
|
var dominantVertical = Math.Abs(referenceDy) >= Math.Abs(referenceDx) * 1.25d;
|
|
|
|
return side is "left" or "right"
|
|
? Math.Clamp(
|
|
dominantHorizontal
|
|
? centerY + Math.Clamp(referenceDy, -sourceNode.Height * 0.18d, sourceNode.Height * 0.18d)
|
|
: Math.Abs(continuationPoint.Y - centerY) > 2d
|
|
? continuationPoint.Y
|
|
: referencePoint.Y,
|
|
sourceNode.Y + 4d,
|
|
sourceNode.Y + sourceNode.Height - 4d)
|
|
: Math.Clamp(
|
|
dominantVertical
|
|
? centerX + Math.Clamp(referenceDx, -sourceNode.Width * 0.18d, sourceNode.Width * 0.18d)
|
|
: Math.Abs(continuationPoint.X - centerX) > 2d
|
|
? continuationPoint.X
|
|
: referencePoint.X,
|
|
sourceNode.X + 4d,
|
|
sourceNode.X + sourceNode.Width - 4d);
|
|
}
|
|
|
|
private static IEnumerable<double> EnumerateGatewaySourceSlotCoordinates(
|
|
ElkPositionedNode sourceNode,
|
|
string side,
|
|
ElkPoint continuationPoint,
|
|
ElkPoint referencePoint)
|
|
{
|
|
var primary = ResolveGatewaySourceSlotCoordinate(sourceNode, side, continuationPoint, referencePoint);
|
|
yield return primary;
|
|
|
|
var center = side is "left" or "right"
|
|
? sourceNode.Y + (sourceNode.Height / 2d)
|
|
: sourceNode.X + (sourceNode.Width / 2d);
|
|
if (Math.Abs(center - primary) > 1d)
|
|
{
|
|
yield return center;
|
|
}
|
|
|
|
var alternate = side is "left" or "right"
|
|
? Math.Clamp(referencePoint.Y, sourceNode.Y + 4d, sourceNode.Y + sourceNode.Height - 4d)
|
|
: Math.Clamp(referencePoint.X, sourceNode.X + 4d, sourceNode.X + sourceNode.Width - 4d);
|
|
if (Math.Abs(alternate - primary) > 1d)
|
|
{
|
|
yield return alternate;
|
|
}
|
|
|
|
var blended = side is "left" or "right"
|
|
? Math.Clamp((continuationPoint.Y + referencePoint.Y) / 2d, sourceNode.Y + 4d, sourceNode.Y + sourceNode.Height - 4d)
|
|
: Math.Clamp((continuationPoint.X + referencePoint.X) / 2d, sourceNode.X + 4d, sourceNode.X + sourceNode.Width - 4d);
|
|
if (Math.Abs(blended - primary) > 1d && Math.Abs(blended - alternate) > 1d && Math.Abs(blended - center) > 1d)
|
|
{
|
|
yield return blended;
|
|
}
|
|
}
|
|
|
|
private static bool IsBoundaryOnGatewaySourceSide(
|
|
ElkPositionedNode sourceNode,
|
|
ElkPoint boundary,
|
|
string side)
|
|
{
|
|
var centerX = sourceNode.X + (sourceNode.Width / 2d);
|
|
var centerY = sourceNode.Y + (sourceNode.Height / 2d);
|
|
var deltaX = boundary.X - centerX;
|
|
var deltaY = boundary.Y - centerY;
|
|
|
|
return side switch
|
|
{
|
|
"right" => deltaX > 0d && Math.Abs(deltaX) >= Math.Abs(deltaY) * 0.35d,
|
|
"left" => deltaX < 0d && Math.Abs(deltaX) >= Math.Abs(deltaY) * 0.35d,
|
|
"bottom" => deltaY > 0d && Math.Abs(deltaY) >= Math.Abs(deltaX) * 0.35d,
|
|
"top" => deltaY < 0d && Math.Abs(deltaY) >= Math.Abs(deltaX) * 0.35d,
|
|
_ => false,
|
|
};
|
|
}
|
|
|
|
private static IEnumerable<string> EnumeratePreferredGatewayEntrySides(
|
|
ElkPositionedNode targetNode,
|
|
ElkPoint exteriorAnchor)
|
|
{
|
|
var centerX = targetNode.X + (targetNode.Width / 2d);
|
|
var centerY = targetNode.Y + (targetNode.Height / 2d);
|
|
var deltaX = exteriorAnchor.X - centerX;
|
|
var deltaY = exteriorAnchor.Y - centerY;
|
|
var absDx = Math.Abs(deltaX);
|
|
var absDy = Math.Abs(deltaY);
|
|
var primary = absDx >= absDy
|
|
? (deltaX >= 0d ? "right" : "left")
|
|
: (deltaY >= 0d ? "bottom" : "top");
|
|
yield return primary;
|
|
|
|
if (absDx > 0.5d && absDy > 0.5d)
|
|
{
|
|
var secondary = primary is "left" or "right"
|
|
? (deltaY >= 0d ? "bottom" : "top")
|
|
: (deltaX >= 0d ? "right" : "left");
|
|
if (!string.Equals(primary, secondary, StringComparison.Ordinal))
|
|
{
|
|
yield return secondary;
|
|
}
|
|
}
|
|
}
|
|
|
|
private static double ScoreGatewayEntryBoundaryCandidate(
|
|
ElkPositionedNode targetNode,
|
|
ElkPoint candidate,
|
|
ElkPoint exteriorAnchor,
|
|
ElkPoint assignedEndpoint)
|
|
{
|
|
if (!ElkShapeBoundaries.IsGatewayBoundaryPoint(targetNode, candidate))
|
|
{
|
|
return double.PositiveInfinity;
|
|
}
|
|
|
|
var exteriorApproach = ElkShapeBoundaries.BuildGatewayDirectionalExteriorPoint(targetNode, candidate, exteriorAnchor);
|
|
if (!ElkShapeBoundaries.HasValidGatewayBoundaryAngle(targetNode, candidate, exteriorApproach))
|
|
{
|
|
return double.PositiveInfinity;
|
|
}
|
|
|
|
var centerX = targetNode.X + (targetNode.Width / 2d);
|
|
var centerY = targetNode.Y + (targetNode.Height / 2d);
|
|
var desiredDx = exteriorAnchor.X - centerX;
|
|
var desiredDy = exteriorAnchor.Y - centerY;
|
|
var candidateDx = candidate.X - centerX;
|
|
var candidateDy = candidate.Y - centerY;
|
|
var score = Math.Abs(candidate.X - exteriorAnchor.X) + Math.Abs(candidate.Y - exteriorAnchor.Y);
|
|
score += (Math.Abs(candidate.X - assignedEndpoint.X) + Math.Abs(candidate.Y - assignedEndpoint.Y)) * 0.2d;
|
|
score += (Math.Abs(exteriorApproach.X - exteriorAnchor.X) + Math.Abs(exteriorApproach.Y - exteriorAnchor.Y)) * 0.05d;
|
|
|
|
var dominantHorizontal = Math.Abs(desiredDx) >= Math.Abs(desiredDy) * 1.25d;
|
|
var dominantVertical = Math.Abs(desiredDy) >= Math.Abs(desiredDx) * 1.25d;
|
|
if (dominantHorizontal)
|
|
{
|
|
if (Math.Sign(candidateDx) != Math.Sign(desiredDx))
|
|
{
|
|
score += 10_000d;
|
|
}
|
|
|
|
score += Math.Abs(candidateDy) * 6d;
|
|
}
|
|
else if (dominantVertical)
|
|
{
|
|
if (Math.Sign(candidateDy) != Math.Sign(desiredDy))
|
|
{
|
|
score += 10_000d;
|
|
}
|
|
|
|
score += Math.Abs(candidateDx) * 6d;
|
|
}
|
|
else
|
|
{
|
|
score += (Math.Abs(candidateDx - desiredDx) + Math.Abs(candidateDy - desiredDy)) * 0.08d;
|
|
}
|
|
|
|
if (ElkShapeBoundaries.IsNearGatewayVertex(targetNode, candidate, 8d))
|
|
{
|
|
score += 4_000d;
|
|
}
|
|
|
|
return score;
|
|
}
|
|
|
|
private static double ScoreGatewayExteriorApproachCandidate(
|
|
ElkPositionedNode node,
|
|
ElkPoint boundary,
|
|
ElkPoint candidate,
|
|
ElkPoint referencePoint)
|
|
{
|
|
var deltaX = candidate.X - boundary.X;
|
|
var deltaY = candidate.Y - boundary.Y;
|
|
var moveLength = Math.Sqrt((deltaX * deltaX) + (deltaY * deltaY));
|
|
var referenceDistance = Math.Abs(referencePoint.X - candidate.X) + Math.Abs(referencePoint.Y - candidate.Y);
|
|
var score = moveLength + (referenceDistance * 0.1d);
|
|
var dominantHorizontal = Math.Abs(referencePoint.X - boundary.X) >= Math.Abs(referencePoint.Y - boundary.Y) * 1.2d;
|
|
var dominantVertical = Math.Abs(referencePoint.Y - boundary.Y) >= Math.Abs(referencePoint.X - boundary.X) * 1.2d;
|
|
|
|
if (node.Kind == "Decision"
|
|
&& !ElkShapeBoundaries.IsNearGatewayVertex(node, boundary, 8d))
|
|
{
|
|
var preferredCandidate = ElkShapeBoundaries.BuildGatewayExteriorApproachPoint(node, boundary);
|
|
var isAxisAlignedStub = Math.Abs(deltaX) <= 0.5d || Math.Abs(deltaY) <= 0.5d;
|
|
if (!dominantHorizontal && !dominantVertical)
|
|
{
|
|
if (ElkEdgeRoutingGeometry.PointsEqual(candidate, preferredCandidate))
|
|
{
|
|
score -= 220d;
|
|
}
|
|
else
|
|
{
|
|
if (isAxisAlignedStub)
|
|
{
|
|
score += 120d;
|
|
}
|
|
|
|
score += Math.Abs(Math.Abs(deltaX) - Math.Abs(deltaY)) * 0.25d;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (dominantHorizontal)
|
|
{
|
|
if (Math.Abs(deltaX) <= 0.5d || Math.Sign(deltaX) != Math.Sign(referencePoint.X - boundary.X))
|
|
{
|
|
score += 8_000d;
|
|
}
|
|
|
|
if (Math.Abs(deltaY) <= 0.5d)
|
|
{
|
|
score -= 120d;
|
|
}
|
|
|
|
score += Math.Abs(deltaY) * 8d;
|
|
}
|
|
else if (dominantVertical)
|
|
{
|
|
if (Math.Abs(deltaY) <= 0.5d || Math.Sign(deltaY) != Math.Sign(referencePoint.Y - boundary.Y))
|
|
{
|
|
score += 8_000d;
|
|
}
|
|
|
|
if (Math.Abs(deltaX) <= 0.5d)
|
|
{
|
|
score -= 120d;
|
|
}
|
|
|
|
score += Math.Abs(deltaX) * 8d;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (dominantHorizontal)
|
|
{
|
|
if (Math.Sign(deltaX) != Math.Sign(referencePoint.X - boundary.X))
|
|
{
|
|
score += 10_000d;
|
|
}
|
|
|
|
score += Math.Abs(deltaY) * 0.35d;
|
|
}
|
|
else if (dominantVertical)
|
|
{
|
|
if (Math.Sign(deltaY) != Math.Sign(referencePoint.Y - boundary.Y))
|
|
{
|
|
score += 10_000d;
|
|
}
|
|
|
|
score += Math.Abs(deltaX) * 0.35d;
|
|
}
|
|
|
|
return score;
|
|
}
|
|
|
|
private static List<ElkPoint> TrimTargetApproachBacktracking(
|
|
IReadOnlyList<ElkPoint> sourcePath,
|
|
ElkPositionedNode targetNode,
|
|
string side,
|
|
ElkPoint explicitEndpoint)
|
|
{
|
|
if (sourcePath.Count < 4)
|
|
{
|
|
return sourcePath
|
|
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
|
.ToList();
|
|
}
|
|
|
|
const double tolerance = 0.5d;
|
|
var startIndex = Math.Max(0, sourcePath.Count - 5);
|
|
var firstOffendingIndex = -1;
|
|
for (var i = startIndex; i < sourcePath.Count - 1; i++)
|
|
{
|
|
if (IsOnWrongSideOfTarget(sourcePath[i], targetNode, side, tolerance))
|
|
{
|
|
firstOffendingIndex = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (firstOffendingIndex < 0)
|
|
{
|
|
return sourcePath
|
|
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
|
.ToList();
|
|
}
|
|
|
|
var trimmed = sourcePath
|
|
.Take(Math.Max(1, firstOffendingIndex))
|
|
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
|
.ToList();
|
|
if (trimmed.Count == 0 || !ElkEdgeRoutingGeometry.PointsEqual(trimmed[^1], explicitEndpoint))
|
|
{
|
|
trimmed.Add(explicitEndpoint);
|
|
}
|
|
|
|
return NormalizeEntryPath(trimmed, targetNode, side, explicitEndpoint);
|
|
}
|
|
|
|
private static bool TryNormalizeNonGatewayBacktrackingEntry(
|
|
IReadOnlyList<ElkPoint> sourcePath,
|
|
ElkPositionedNode targetNode,
|
|
out List<ElkPoint> repairedPath)
|
|
{
|
|
repairedPath = sourcePath
|
|
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
|
.ToList();
|
|
if (sourcePath.Count < 2)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!TryResolveNonGatewayBacktrackingEndpoint(sourcePath, targetNode, out var side, out var endpoint))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var candidate = NormalizeEntryPath(sourcePath, targetNode, side, endpoint);
|
|
if (HasTargetApproachBacktracking(candidate, targetNode))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
repairedPath = candidate;
|
|
return true;
|
|
}
|
|
|
|
private static bool TryResolveNonGatewayBacktrackingEndpoint(
|
|
IReadOnlyList<ElkPoint> sourcePath,
|
|
ElkPositionedNode targetNode,
|
|
out string side,
|
|
out ElkPoint endpoint)
|
|
{
|
|
side = string.Empty;
|
|
endpoint = default!;
|
|
if (sourcePath.Count < 2)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var anchor = sourcePath[^2];
|
|
var centerX = targetNode.X + (targetNode.Width / 2d);
|
|
var centerY = targetNode.Y + (targetNode.Height / 2d);
|
|
var deltaX = anchor.X - centerX;
|
|
var deltaY = anchor.Y - centerY;
|
|
var dominantHorizontal = Math.Abs(deltaX) >= Math.Abs(deltaY) * 1.15d;
|
|
side = dominantHorizontal
|
|
? (deltaX <= 0d ? "left" : "right")
|
|
: (deltaY <= 0d ? "top" : "bottom");
|
|
|
|
if (side is "left" or "right")
|
|
{
|
|
endpoint = new ElkPoint
|
|
{
|
|
X = side == "left" ? targetNode.X : targetNode.X + targetNode.Width,
|
|
Y = Math.Clamp(anchor.Y, targetNode.Y + 4d, targetNode.Y + targetNode.Height - 4d),
|
|
};
|
|
}
|
|
else
|
|
{
|
|
endpoint = new ElkPoint
|
|
{
|
|
X = Math.Clamp(anchor.X, targetNode.X + 4d, targetNode.X + targetNode.Width - 4d),
|
|
Y = side == "top" ? targetNode.Y : targetNode.Y + targetNode.Height,
|
|
};
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private static bool HasTargetApproachBacktracking(
|
|
IReadOnlyList<ElkPoint> path,
|
|
ElkPositionedNode targetNode)
|
|
{
|
|
if (path.Count < 3 || ElkShapeBoundaries.IsGatewayShape(targetNode))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var side = ElkEdgeRoutingGeometry.ResolveBoundarySide(path[^1], targetNode);
|
|
if (side is not "left" and not "right" and not "top" and not "bottom")
|
|
{
|
|
return false;
|
|
}
|
|
|
|
const double tolerance = 0.5d;
|
|
if (HasShortOrthogonalTargetHook(path, targetNode, side, tolerance))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
var startIndex = Math.Max(0, path.Count - (side is "left" or "right" ? 4 : 3));
|
|
var axisValues = new List<double>(path.Count - startIndex);
|
|
for (var i = startIndex; i < path.Count; i++)
|
|
{
|
|
var value = side is "left" or "right"
|
|
? path[i].X
|
|
: path[i].Y;
|
|
if (axisValues.Count == 0 || Math.Abs(axisValues[^1] - value) > tolerance)
|
|
{
|
|
axisValues.Add(value);
|
|
}
|
|
}
|
|
|
|
if (axisValues.Count < 3)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var targetAxis = side switch
|
|
{
|
|
"left" => targetNode.X,
|
|
"right" => targetNode.X + targetNode.Width,
|
|
"top" => targetNode.Y,
|
|
"bottom" => targetNode.Y + targetNode.Height,
|
|
_ => double.NaN,
|
|
};
|
|
|
|
var overshootsTargetSide = side switch
|
|
{
|
|
"left" or "top" => axisValues.Any(value => value > targetAxis + tolerance),
|
|
"right" or "bottom" => axisValues.Any(value => value < targetAxis - tolerance),
|
|
_ => false,
|
|
};
|
|
if (overshootsTargetSide)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
var expectsIncreasing = side is "left" or "top";
|
|
var sawProgress = false;
|
|
for (var i = 1; i < axisValues.Count; i++)
|
|
{
|
|
var delta = axisValues[i] - axisValues[i - 1];
|
|
if (Math.Abs(delta) <= tolerance)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (expectsIncreasing)
|
|
{
|
|
if (delta > tolerance)
|
|
{
|
|
sawProgress = true;
|
|
}
|
|
else if (sawProgress)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (delta < -tolerance)
|
|
{
|
|
sawProgress = true;
|
|
}
|
|
else if (sawProgress)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static bool HasShortOrthogonalTargetHook(
|
|
IReadOnlyList<ElkPoint> path,
|
|
ElkPositionedNode targetNode,
|
|
string side,
|
|
double tolerance)
|
|
{
|
|
if (path.Count < 3)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var boundaryPoint = path[^1];
|
|
var runStartIndex = path.Count - 2;
|
|
if (side is "left" or "right")
|
|
{
|
|
while (runStartIndex > 0 && Math.Abs(path[runStartIndex - 1].Y - boundaryPoint.Y) <= tolerance)
|
|
{
|
|
runStartIndex--;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
while (runStartIndex > 0 && Math.Abs(path[runStartIndex - 1].X - boundaryPoint.X) <= tolerance)
|
|
{
|
|
runStartIndex--;
|
|
}
|
|
}
|
|
|
|
if (runStartIndex == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var overallDeltaX = path[^1].X - path[0].X;
|
|
var overallDeltaY = path[^1].Y - path[0].Y;
|
|
var overallAbsDx = Math.Abs(overallDeltaX);
|
|
var overallAbsDy = Math.Abs(overallDeltaY);
|
|
var sameRowThreshold = Math.Max(24d, targetNode.Height / 3d);
|
|
var sameColumnThreshold = Math.Max(24d, targetNode.Width / 3d);
|
|
var looksHorizontal = overallAbsDx >= overallAbsDy * 1.15d
|
|
&& overallAbsDy <= sameRowThreshold
|
|
&& Math.Sign(overallDeltaX) != 0;
|
|
var looksVertical = overallAbsDy >= overallAbsDx * 1.15d
|
|
&& overallAbsDx <= sameColumnThreshold
|
|
&& Math.Sign(overallDeltaY) != 0;
|
|
var contradictsDominantApproach = side switch
|
|
{
|
|
"left" or "right" => looksVertical,
|
|
"top" or "bottom" => looksHorizontal,
|
|
_ => false,
|
|
};
|
|
if (!contradictsDominantApproach)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var runStart = path[runStartIndex];
|
|
var boundaryDepth = side is "left" or "right"
|
|
? Math.Abs(boundaryPoint.X - runStart.X)
|
|
: Math.Abs(boundaryPoint.Y - runStart.Y);
|
|
var requiredDepth = side is "left" or "right"
|
|
? targetNode.Width
|
|
: targetNode.Height;
|
|
if (boundaryDepth + tolerance >= requiredDepth)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var predecessor = path[runStartIndex - 1];
|
|
var predecessorDx = Math.Abs(runStart.X - predecessor.X);
|
|
var predecessorDy = Math.Abs(runStart.Y - predecessor.Y);
|
|
return side switch
|
|
{
|
|
"left" or "right" => predecessorDy > predecessorDx * 3d,
|
|
"top" or "bottom" => predecessorDx > predecessorDy * 3d,
|
|
_ => false,
|
|
};
|
|
}
|
|
|
|
private static bool IsOnWrongSideOfTarget(
|
|
ElkPoint point,
|
|
ElkPositionedNode targetNode,
|
|
string side,
|
|
double tolerance)
|
|
{
|
|
return side switch
|
|
{
|
|
"left" => point.X > targetNode.X + tolerance,
|
|
"right" => point.X < (targetNode.X + targetNode.Width) - tolerance,
|
|
"top" => point.Y > targetNode.Y + tolerance,
|
|
"bottom" => point.Y < (targetNode.Y + targetNode.Height) - tolerance,
|
|
_ => false,
|
|
};
|
|
}
|
|
|
|
private static Dictionary<string, ElkPoint> ResolveTargetApproachSlots(
|
|
IReadOnlyCollection<ElkRoutedEdge> edges,
|
|
IReadOnlyDictionary<string, ElkPositionedNode> nodesById,
|
|
double graphMinY,
|
|
double graphMaxY,
|
|
double minLineClearance,
|
|
IReadOnlySet<string>? restrictedEdgeIds)
|
|
{
|
|
var result = new Dictionary<string, ElkPoint>(StringComparer.Ordinal);
|
|
var groups = new Dictionary<string, List<(string EdgeId, ElkPoint Endpoint)>>(StringComparer.Ordinal);
|
|
|
|
foreach (var edge in edges)
|
|
{
|
|
if (!ShouldSpreadTargetApproach(edge, graphMinY, graphMaxY)
|
|
|| !nodesById.TryGetValue(edge.TargetNodeId ?? string.Empty, out var targetNode))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var path = ExtractFullPath(edge);
|
|
if (path.Count < 2)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var endpoint = path[^1];
|
|
var side = ResolveTargetApproachSide(path, targetNode);
|
|
var key = $"{targetNode.Id}|{side}";
|
|
if (!groups.TryGetValue(key, out var group))
|
|
{
|
|
group = [];
|
|
groups[key] = group;
|
|
}
|
|
|
|
group.Add((edge.Id, endpoint));
|
|
}
|
|
|
|
foreach (var (key, group) in groups)
|
|
{
|
|
if (group.Count < 2)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var separator = key.IndexOf('|', StringComparison.Ordinal);
|
|
var targetId = key[..separator];
|
|
var side = key[(separator + 1)..];
|
|
if (!nodesById.TryGetValue(targetId, out var targetNode))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (restrictedEdgeIds is not null
|
|
&& !group.Any(item => restrictedEdgeIds.Contains(item.EdgeId)))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var sideLength = side is "left" or "right"
|
|
? Math.Max(8d, targetNode.Height - 8d)
|
|
: Math.Max(8d, targetNode.Width - 8d);
|
|
var slotSpacing = group.Count > 1
|
|
? ResolveBoundaryJoinSlotSpacing(minLineClearance, sideLength, group.Count)
|
|
: 0d;
|
|
var totalSpan = (group.Count - 1) * slotSpacing;
|
|
|
|
if (side is "left" or "right")
|
|
{
|
|
var centerY = targetNode.Y + (targetNode.Height / 2d);
|
|
var startY = Math.Max(targetNode.Y + 4d, centerY - (totalSpan / 2d));
|
|
var sorted = group.OrderBy(item => item.Endpoint.Y).ToArray();
|
|
for (var i = 0; i < sorted.Length; i++)
|
|
{
|
|
var slotY = Math.Min(targetNode.Y + targetNode.Height - 4d, startY + (i * slotSpacing));
|
|
if (restrictedEdgeIds is not null && !restrictedEdgeIds.Contains(sorted[i].EdgeId))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var slotPoint = new ElkPoint
|
|
{
|
|
X = side == "left" ? targetNode.X : targetNode.X + targetNode.Width,
|
|
Y = slotY,
|
|
};
|
|
if (ElkShapeBoundaries.TryProjectGatewayBoundarySlot(targetNode, side, slotY, out var gatewaySlot))
|
|
{
|
|
slotPoint = gatewaySlot;
|
|
}
|
|
|
|
result[sorted[i].EdgeId] = slotPoint;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
var centerX = targetNode.X + (targetNode.Width / 2d);
|
|
var startX = Math.Max(targetNode.X + 4d, centerX - (totalSpan / 2d));
|
|
var sorted = group.OrderBy(item => item.Endpoint.X).ToArray();
|
|
for (var i = 0; i < sorted.Length; i++)
|
|
{
|
|
var slotX = Math.Min(targetNode.X + targetNode.Width - 4d, startX + (i * slotSpacing));
|
|
if (restrictedEdgeIds is not null && !restrictedEdgeIds.Contains(sorted[i].EdgeId))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var slotPoint = new ElkPoint
|
|
{
|
|
X = slotX,
|
|
Y = side == "top" ? targetNode.Y : targetNode.Y + targetNode.Height,
|
|
};
|
|
if (ElkShapeBoundaries.TryProjectGatewayBoundarySlot(targetNode, side, slotX, out var gatewaySlot))
|
|
{
|
|
slotPoint = gatewaySlot;
|
|
}
|
|
|
|
result[sorted[i].EdgeId] = slotPoint;
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private static bool GroupHasTargetApproachJoin(
|
|
IReadOnlyList<(IReadOnlyList<ElkPoint> Path, string Side)> entries,
|
|
double minLineClearance)
|
|
{
|
|
for (var i = 0; i < entries.Count; i++)
|
|
{
|
|
var left = entries[i];
|
|
if (!TryExtractTargetApproachRun(left.Path, left.Side, out var leftRunStartIndex, out var leftRunEndIndex))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var leftStart = left.Path[leftRunStartIndex];
|
|
var leftEnd = left.Path[leftRunEndIndex];
|
|
for (var j = i + 1; j < entries.Count; j++)
|
|
{
|
|
var right = entries[j];
|
|
if (!string.Equals(left.Side, right.Side, StringComparison.Ordinal)
|
|
|| !TryExtractTargetApproachRun(right.Path, right.Side, out var rightRunStartIndex, out var rightRunEndIndex))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var rightStart = right.Path[rightRunStartIndex];
|
|
var rightEnd = right.Path[rightRunEndIndex];
|
|
if (ElkEdgeRoutingGeometry.AreParallelAndClose(leftStart, leftEnd, rightStart, rightEnd, minLineClearance))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static double ResolveBoundaryJoinSlotSpacing(
|
|
double minLineClearance,
|
|
double sideLength,
|
|
int entryCount)
|
|
{
|
|
if (entryCount <= 1)
|
|
{
|
|
return 0d;
|
|
}
|
|
|
|
// Keep slot spacing slightly above the violation threshold so a final
|
|
// normalize pass does not collapse two target lanes back into the same
|
|
// effective rail by a fraction of a pixel.
|
|
var desiredSpacing = minLineClearance + 6d;
|
|
return Math.Max(12d, Math.Min(desiredSpacing, sideLength / (entryCount - 1)));
|
|
}
|
|
|
|
private static string ResolveTargetApproachSide(
|
|
IReadOnlyList<ElkPoint> path,
|
|
ElkPositionedNode targetNode)
|
|
{
|
|
if (ElkShapeBoundaries.IsGatewayShape(targetNode))
|
|
{
|
|
if (path.Count >= 2)
|
|
{
|
|
return ElkEdgeRoutingGeometry.ResolveBoundaryApproachSide(path[^1], path[^2], targetNode);
|
|
}
|
|
|
|
return ElkEdgeRoutingGeometry.ResolveBoundarySide(path[^1], targetNode);
|
|
}
|
|
|
|
if (path.Count < 2)
|
|
{
|
|
return ElkEdgeRoutingGeometry.ResolveBoundarySide(path[^1], targetNode);
|
|
}
|
|
|
|
return ElkEdgeRoutingGeometry.ResolveBoundaryApproachSide(path[^1], path[^2], targetNode);
|
|
}
|
|
|
|
private static double ResolveTargetApproachAxisValue(
|
|
IReadOnlyList<ElkPoint> path,
|
|
string side)
|
|
{
|
|
if (!TryExtractTargetApproachRun(path, side, out var runStartIndex, out _))
|
|
{
|
|
return double.NaN;
|
|
}
|
|
|
|
return side switch
|
|
{
|
|
"left" or "right" => path[runStartIndex].X,
|
|
"top" or "bottom" => path[runStartIndex].Y,
|
|
_ => double.NaN,
|
|
};
|
|
}
|
|
|
|
private static double ResolveSpreadableTargetApproachAxis(
|
|
IReadOnlyList<ElkPoint> path,
|
|
ElkPositionedNode targetNode,
|
|
string side,
|
|
double minLineClearance)
|
|
{
|
|
if (!TryExtractTargetApproachRun(path, side, out var runStartIndex, out _))
|
|
{
|
|
return double.NaN;
|
|
}
|
|
|
|
var rawAxis = ResolveTargetApproachAxisValue(path, side);
|
|
if (double.IsNaN(rawAxis))
|
|
{
|
|
return double.NaN;
|
|
}
|
|
|
|
var maxOffset = Math.Max(
|
|
Math.Max(targetNode.Width, targetNode.Height),
|
|
(minLineClearance * 2d) + 16d);
|
|
|
|
return side switch
|
|
{
|
|
"left" => runStartIndex == 0
|
|
? Math.Max(rawAxis, targetNode.X - maxOffset)
|
|
: Math.Max(rawAxis, targetNode.X - maxOffset),
|
|
"right" => runStartIndex == 0
|
|
? Math.Min(rawAxis, targetNode.X + targetNode.Width + maxOffset)
|
|
: Math.Min(rawAxis, targetNode.X + targetNode.Width + maxOffset),
|
|
"top" => runStartIndex == 0
|
|
? Math.Max(rawAxis, targetNode.Y - maxOffset)
|
|
: Math.Max(rawAxis, targetNode.Y - maxOffset),
|
|
"bottom" => runStartIndex == 0
|
|
? Math.Min(rawAxis, targetNode.Y + targetNode.Height + maxOffset)
|
|
: Math.Min(rawAxis, targetNode.Y + targetNode.Height + maxOffset),
|
|
_ => rawAxis,
|
|
};
|
|
}
|
|
|
|
private static string ResolveSourceDepartureSide(
|
|
IReadOnlyList<ElkPoint> path,
|
|
ElkPositionedNode sourceNode)
|
|
{
|
|
if (path.Count < 2)
|
|
{
|
|
return ElkEdgeRoutingGeometry.ResolveBoundarySide(path[0], sourceNode);
|
|
}
|
|
|
|
return ElkEdgeRoutingGeometry.ResolveBoundaryApproachSide(path[0], path[1], sourceNode);
|
|
}
|
|
|
|
private static double ResolveDefaultSourceDepartureAxis(
|
|
ElkPositionedNode sourceNode,
|
|
string side)
|
|
{
|
|
return side switch
|
|
{
|
|
"left" => sourceNode.X - 24d,
|
|
"right" => sourceNode.X + sourceNode.Width + 24d,
|
|
"top" => sourceNode.Y - 24d,
|
|
"bottom" => sourceNode.Y + sourceNode.Height + 24d,
|
|
_ => 0d,
|
|
};
|
|
}
|
|
|
|
private static double ResolveDefaultTargetApproachAxis(
|
|
ElkPositionedNode targetNode,
|
|
string side)
|
|
{
|
|
return side switch
|
|
{
|
|
"left" => targetNode.X - 24d,
|
|
"right" => targetNode.X + targetNode.Width + 24d,
|
|
"top" => targetNode.Y - 24d,
|
|
"bottom" => targetNode.Y + targetNode.Height + 24d,
|
|
_ => double.NaN,
|
|
};
|
|
}
|
|
|
|
private static double ResolveDesiredTargetApproachAxis(
|
|
ElkPositionedNode targetNode,
|
|
string side,
|
|
double baseApproachAxis,
|
|
double slotSpacing,
|
|
int slotIndex,
|
|
bool forceOutwardFromBoundary = false)
|
|
{
|
|
var originAxis = double.IsNaN(baseApproachAxis)
|
|
? ResolveDefaultTargetApproachAxis(targetNode, side)
|
|
: baseApproachAxis;
|
|
|
|
var axis = forceOutwardFromBoundary
|
|
? side switch
|
|
{
|
|
"left" or "top" => originAxis - (slotIndex * slotSpacing),
|
|
"right" or "bottom" => originAxis + (slotIndex * slotSpacing),
|
|
_ => originAxis,
|
|
}
|
|
: originAxis + (slotIndex * slotSpacing);
|
|
|
|
return side switch
|
|
{
|
|
"left" => Math.Min(axis, targetNode.X - 8d),
|
|
"right" => Math.Max(axis, targetNode.X + targetNode.Width + 8d),
|
|
"top" => Math.Min(axis, targetNode.Y - 8d),
|
|
"bottom" => Math.Max(axis, targetNode.Y + targetNode.Height + 8d),
|
|
_ => axis,
|
|
};
|
|
}
|
|
|
|
private static bool GroupHasMixedNodeFaceLaneConflict(
|
|
IReadOnlyList<(int Index, ElkRoutedEdge Edge, IReadOnlyList<ElkPoint> Path, ElkPositionedNode Node, string Side, bool IsOutgoing, ElkPoint Boundary, double BoundaryCoordinate, double AxisValue)> entries,
|
|
double minLineClearance)
|
|
{
|
|
for (var i = 0; i < entries.Count; i++)
|
|
{
|
|
for (var j = i + 1; j < entries.Count; j++)
|
|
{
|
|
if (entries[i].IsOutgoing == entries[j].IsOutgoing
|
|
|| !string.Equals(entries[i].Side, entries[j].Side, StringComparison.Ordinal))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var outgoing = entries[i].IsOutgoing ? entries[i] : entries[j];
|
|
var incoming = entries[i].IsOutgoing ? entries[j] : entries[i];
|
|
if (!TryExtractSourceDepartureRun(outgoing.Path, outgoing.Side, out _, out var outgoingRunEndIndex)
|
|
|| !TryExtractTargetApproachRun(incoming.Path, incoming.Side, out var incomingRunStartIndex, out _))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (ElkEdgeRoutingGeometry.AreParallelAndClose(
|
|
outgoing.Path[0],
|
|
outgoing.Path[outgoingRunEndIndex],
|
|
incoming.Path[incomingRunStartIndex],
|
|
incoming.Path[^1],
|
|
minLineClearance))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static List<ElkPoint> BuildMixedSourceFaceCandidate(
|
|
IReadOnlyList<ElkPoint> path,
|
|
ElkPositionedNode sourceNode,
|
|
string side,
|
|
double desiredCoordinate,
|
|
double axisValue)
|
|
{
|
|
ElkPoint boundaryPoint;
|
|
if (ElkShapeBoundaries.IsGatewayShape(sourceNode))
|
|
{
|
|
if (!ElkShapeBoundaries.TryProjectGatewayBoundarySlot(sourceNode, side, desiredCoordinate, out boundaryPoint))
|
|
{
|
|
return path.Select(point => new ElkPoint { X = point.X, Y = point.Y }).ToList();
|
|
}
|
|
|
|
var continuation = path.Count > 1 ? path[1] : path[0];
|
|
boundaryPoint = ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(sourceNode, boundaryPoint, continuation);
|
|
}
|
|
else
|
|
{
|
|
boundaryPoint = side switch
|
|
{
|
|
"left" => new ElkPoint { X = sourceNode.X, Y = desiredCoordinate },
|
|
"right" => new ElkPoint { X = sourceNode.X + sourceNode.Width, Y = desiredCoordinate },
|
|
"top" => new ElkPoint { X = desiredCoordinate, Y = sourceNode.Y },
|
|
"bottom" => new ElkPoint { X = desiredCoordinate, Y = sourceNode.Y + sourceNode.Height },
|
|
_ => path[0],
|
|
};
|
|
}
|
|
|
|
return RewriteSourceDepartureRun(
|
|
path,
|
|
side,
|
|
boundaryPoint,
|
|
double.IsNaN(axisValue) ? ResolveDefaultSourceDepartureAxis(sourceNode, side) : axisValue);
|
|
}
|
|
|
|
private static List<ElkPoint> BuildMixedTargetFaceCandidate(
|
|
IReadOnlyList<ElkPoint> path,
|
|
ElkPositionedNode targetNode,
|
|
string side,
|
|
double desiredCoordinate,
|
|
double axisValue)
|
|
{
|
|
ElkPoint desiredEndpoint;
|
|
if (ElkShapeBoundaries.IsGatewayShape(targetNode))
|
|
{
|
|
if (!ElkShapeBoundaries.TryProjectGatewayBoundarySlot(targetNode, side, desiredCoordinate, out desiredEndpoint))
|
|
{
|
|
return path.Select(point => new ElkPoint { X = point.X, Y = point.Y }).ToList();
|
|
}
|
|
return BuildTargetApproachCandidatePath(
|
|
path,
|
|
targetNode,
|
|
side,
|
|
desiredEndpoint,
|
|
axisValue);
|
|
}
|
|
|
|
desiredEndpoint = side switch
|
|
{
|
|
"left" => new ElkPoint { X = targetNode.X, Y = desiredCoordinate },
|
|
"right" => new ElkPoint { X = targetNode.X + targetNode.Width, Y = desiredCoordinate },
|
|
"top" => new ElkPoint { X = desiredCoordinate, Y = targetNode.Y },
|
|
"bottom" => new ElkPoint { X = desiredCoordinate, Y = targetNode.Y + targetNode.Height },
|
|
_ => path[^1],
|
|
};
|
|
|
|
return BuildTargetApproachCandidatePath(
|
|
path,
|
|
targetNode,
|
|
side,
|
|
desiredEndpoint,
|
|
axisValue);
|
|
}
|
|
|
|
private static List<ElkPoint> BuildTargetApproachCandidatePath(
|
|
IReadOnlyList<ElkPoint> path,
|
|
ElkPositionedNode targetNode,
|
|
string side,
|
|
ElkPoint desiredEndpoint,
|
|
double axisValue)
|
|
{
|
|
List<ElkPoint> normalized;
|
|
if (ElkShapeBoundaries.IsGatewayShape(targetNode))
|
|
{
|
|
var exteriorIndex = FindLastGatewayExteriorPointIndex(path, targetNode);
|
|
var exteriorAnchor = path[exteriorIndex];
|
|
normalized = TryBuildSlottedGatewayEntryPath(
|
|
path,
|
|
targetNode,
|
|
exteriorIndex,
|
|
exteriorAnchor,
|
|
desiredEndpoint)
|
|
?? NormalizeGatewayEntryPath(path, targetNode, desiredEndpoint);
|
|
}
|
|
else
|
|
{
|
|
normalized = NormalizeEntryPath(path, targetNode, side, desiredEndpoint);
|
|
}
|
|
var targetAxis = double.IsNaN(axisValue)
|
|
? ResolveDefaultTargetApproachAxis(targetNode, side)
|
|
: axisValue;
|
|
|
|
if (!TryExtractTargetApproachFeeder(normalized, side, out _))
|
|
{
|
|
return normalized;
|
|
}
|
|
|
|
var rewritten = RewriteTargetApproachRun(
|
|
normalized,
|
|
side,
|
|
desiredEndpoint,
|
|
targetAxis);
|
|
if (!PathChanged(normalized, rewritten))
|
|
{
|
|
return normalized;
|
|
}
|
|
|
|
if (ElkShapeBoundaries.IsGatewayShape(targetNode)
|
|
&& !CanAcceptGatewayTargetRepair(rewritten, targetNode))
|
|
{
|
|
return normalized;
|
|
}
|
|
|
|
return rewritten;
|
|
}
|
|
|
|
private static bool TryBuildAlternateMixedFaceCandidate(
|
|
(int Index, ElkRoutedEdge Edge, IReadOnlyList<ElkPoint> Path, ElkPositionedNode Node, string Side, bool IsOutgoing, ElkPoint Boundary, double BoundaryCoordinate, double AxisValue) entry,
|
|
IReadOnlyCollection<ElkPositionedNode> nodes,
|
|
double minLineClearance,
|
|
out List<ElkPoint> candidate)
|
|
{
|
|
candidate = entry.Path
|
|
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
|
.ToList();
|
|
if (ElkShapeBoundaries.IsGatewayShape(entry.Node)
|
|
|| string.IsNullOrWhiteSpace(entry.Edge.Label)
|
|
|| !IsRepeatCollectorLabel(entry.Edge.Label))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var alternateSide = entry.Side switch
|
|
{
|
|
"left" or "right" => "top",
|
|
"top" or "bottom" => "right",
|
|
_ => string.Empty,
|
|
};
|
|
if (string.IsNullOrWhiteSpace(alternateSide))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (entry.IsOutgoing)
|
|
{
|
|
var sourcePath = entry.Path
|
|
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
|
.ToList();
|
|
var toward = sourcePath.Count > 1 ? sourcePath[1] : sourcePath[0];
|
|
sourcePath[0] = BuildRectBoundaryPointForSide(entry.Node, alternateSide, toward);
|
|
candidate = NormalizeExitPath(sourcePath, entry.Node, alternateSide);
|
|
return true;
|
|
}
|
|
|
|
var explicitEndpoint = BuildRectBoundaryPointForSide(entry.Node, alternateSide, entry.Path[^2]);
|
|
var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal);
|
|
if (nodesById.TryGetValue(entry.Edge.SourceNodeId ?? string.Empty, out var sourceNode)
|
|
&& (alternateSide is "top" or "bottom")
|
|
&& TryBuildSafeHorizontalBandCandidate(
|
|
sourceNode,
|
|
entry.Node,
|
|
nodes,
|
|
entry.Edge.SourceNodeId,
|
|
entry.Edge.TargetNodeId,
|
|
entry.Path[0],
|
|
explicitEndpoint,
|
|
minLineClearance,
|
|
preferredSourceExterior: null,
|
|
out var bandCandidate))
|
|
{
|
|
candidate = bandCandidate;
|
|
return true;
|
|
}
|
|
|
|
candidate = NormalizeEntryPath(entry.Path, entry.Node, alternateSide, explicitEndpoint);
|
|
return true;
|
|
}
|
|
|
|
private static bool TryBuildSafeHorizontalBandCandidate(
|
|
ElkPositionedNode sourceNode,
|
|
ElkPositionedNode targetNode,
|
|
IReadOnlyCollection<ElkPositionedNode> nodes,
|
|
string? sourceNodeId,
|
|
string? targetNodeId,
|
|
ElkPoint startBoundary,
|
|
ElkPoint endBoundary,
|
|
double minClearance,
|
|
ElkPoint? preferredSourceExterior,
|
|
out List<ElkPoint> candidate)
|
|
{
|
|
candidate = [];
|
|
|
|
var route = new List<ElkPoint>
|
|
{
|
|
new() { X = startBoundary.X, Y = startBoundary.Y },
|
|
};
|
|
|
|
var routeStart = route[0];
|
|
if (ElkShapeBoundaries.IsGatewayShape(sourceNode))
|
|
{
|
|
var gatewayExteriorCandidates = new List<ElkPoint>();
|
|
if (preferredSourceExterior is { } preferredExterior)
|
|
{
|
|
gatewayExteriorCandidates.Add(preferredExterior);
|
|
}
|
|
|
|
gatewayExteriorCandidates.Add(ElkShapeBoundaries.BuildPreferredGatewaySourceExteriorPoint(sourceNode, startBoundary, endBoundary));
|
|
gatewayExteriorCandidates.Add(ElkShapeBoundaries.BuildGatewayExteriorApproachPoint(sourceNode, startBoundary));
|
|
|
|
ElkPoint? sourceExterior = null;
|
|
foreach (var exteriorCandidate in gatewayExteriorCandidates)
|
|
{
|
|
if (ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(sourceNode, exteriorCandidate)
|
|
|| !ElkShapeBoundaries.HasValidGatewayBoundaryAngle(sourceNode, startBoundary, exteriorCandidate))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
sourceExterior = exteriorCandidate;
|
|
break;
|
|
}
|
|
|
|
if (sourceExterior is null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!ElkEdgeRoutingGeometry.PointsEqual(route[^1], sourceExterior))
|
|
{
|
|
route.Add(sourceExterior);
|
|
routeStart = sourceExterior;
|
|
}
|
|
}
|
|
|
|
var clearance = Math.Max(24d, minClearance * 0.6d);
|
|
var minX = Math.Min(routeStart.X, endBoundary.X);
|
|
var maxX = Math.Max(routeStart.X, endBoundary.X);
|
|
var graphMinY = nodes.Min(node => node.Y);
|
|
var blockers = nodes
|
|
.Where(node =>
|
|
!string.Equals(node.Id, sourceNodeId, StringComparison.Ordinal)
|
|
&& !string.Equals(node.Id, targetNodeId, StringComparison.Ordinal)
|
|
&& maxX > node.X + 0.5d
|
|
&& minX < node.X + node.Width - 0.5d
|
|
&& node.Y <= Math.Max(routeStart.Y, endBoundary.Y) + clearance)
|
|
.ToArray();
|
|
var baseY = Math.Min(Math.Min(routeStart.Y, endBoundary.Y), targetNode.Y);
|
|
if (blockers.Length > 0)
|
|
{
|
|
baseY = Math.Min(baseY, blockers.Min(node => node.Y));
|
|
}
|
|
|
|
var bandY = Math.Max(graphMinY - 72d, baseY - clearance);
|
|
if (bandY >= Math.Min(routeStart.Y, endBoundary.Y) - 0.5d)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (Math.Abs(route[^1].Y - bandY) > 0.5d)
|
|
{
|
|
route.Add(new ElkPoint { X = route[^1].X, Y = bandY });
|
|
}
|
|
|
|
if (Math.Abs(route[^1].X - endBoundary.X) > 0.5d)
|
|
{
|
|
route.Add(new ElkPoint { X = endBoundary.X, Y = bandY });
|
|
}
|
|
|
|
if (Math.Abs(route[^1].Y - endBoundary.Y) > 0.5d)
|
|
{
|
|
route.Add(new ElkPoint { X = endBoundary.X, Y = endBoundary.Y });
|
|
}
|
|
|
|
candidate = NormalizePathPoints(route);
|
|
if (candidate.Count < 2 || HasNodeObstacleCrossing(candidate, nodes, sourceNodeId, targetNodeId))
|
|
{
|
|
candidate = [];
|
|
return false;
|
|
}
|
|
|
|
if (ElkShapeBoundaries.IsGatewayShape(sourceNode))
|
|
{
|
|
if (!HasAcceptableGatewayBoundaryPath(candidate, nodes, sourceNodeId, targetNodeId, sourceNode, fromStart: true))
|
|
{
|
|
candidate = [];
|
|
return false;
|
|
}
|
|
}
|
|
else if (!HasValidBoundaryAngle(candidate[0], candidate[1], sourceNode))
|
|
{
|
|
candidate = [];
|
|
return false;
|
|
}
|
|
|
|
if (ElkShapeBoundaries.IsGatewayShape(targetNode))
|
|
{
|
|
if (!CanAcceptGatewayTargetRepair(candidate, targetNode)
|
|
|| !HasAcceptableGatewayBoundaryPath(candidate, nodes, sourceNodeId, targetNodeId, targetNode, fromStart: false))
|
|
{
|
|
candidate = [];
|
|
return false;
|
|
}
|
|
}
|
|
else if (HasTargetApproachBacktracking(candidate, targetNode)
|
|
|| !HasValidBoundaryAngle(candidate[^1], candidate[^2], targetNode))
|
|
{
|
|
candidate = [];
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private static List<ElkPoint> RewriteTargetApproachRun(
|
|
IReadOnlyList<ElkPoint> path,
|
|
string side,
|
|
ElkPoint endpoint,
|
|
double desiredAxis)
|
|
{
|
|
if (!TryExtractTargetApproachRun(path, side, out var runStartIndex, out _))
|
|
{
|
|
return path
|
|
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
|
.ToList();
|
|
}
|
|
|
|
var prefixEndExclusive = runStartIndex;
|
|
if (runStartIndex > 0 && !IsOrthogonal(path[runStartIndex - 1], path[runStartIndex]))
|
|
{
|
|
prefixEndExclusive = runStartIndex + 1;
|
|
}
|
|
else if (prefixEndExclusive < 2 && path.Count > 2)
|
|
{
|
|
// Preserve the initial source-exit stub while spreading only the target-side run.
|
|
prefixEndExclusive = 2;
|
|
}
|
|
|
|
var rebuilt = path.Take(prefixEndExclusive)
|
|
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
|
.ToList();
|
|
if (rebuilt.Count == 0)
|
|
{
|
|
rebuilt.Add(new ElkPoint { X = path[0].X, Y = path[0].Y });
|
|
}
|
|
|
|
const double coordinateTolerance = 0.5d;
|
|
if (side is "top" or "bottom")
|
|
{
|
|
var approachY = double.IsNaN(desiredAxis) ? rebuilt[^1].Y : desiredAxis;
|
|
|
|
if (Math.Abs(rebuilt[^1].Y - approachY) > coordinateTolerance)
|
|
{
|
|
rebuilt.Add(new ElkPoint { X = rebuilt[^1].X, Y = approachY });
|
|
}
|
|
|
|
if (Math.Abs(rebuilt[^1].X - endpoint.X) > coordinateTolerance)
|
|
{
|
|
rebuilt.Add(new ElkPoint { X = endpoint.X, Y = approachY });
|
|
}
|
|
|
|
if (Math.Abs(rebuilt[^1].Y - endpoint.Y) > coordinateTolerance)
|
|
{
|
|
rebuilt.Add(new ElkPoint { X = endpoint.X, Y = endpoint.Y });
|
|
}
|
|
}
|
|
else
|
|
{
|
|
var approachX = double.IsNaN(desiredAxis) ? rebuilt[^1].X : desiredAxis;
|
|
|
|
if (Math.Abs(rebuilt[^1].X - approachX) > coordinateTolerance)
|
|
{
|
|
rebuilt.Add(new ElkPoint { X = approachX, Y = rebuilt[^1].Y });
|
|
}
|
|
|
|
if (Math.Abs(rebuilt[^1].Y - endpoint.Y) > coordinateTolerance)
|
|
{
|
|
rebuilt.Add(new ElkPoint { X = approachX, Y = endpoint.Y });
|
|
}
|
|
|
|
if (Math.Abs(rebuilt[^1].X - endpoint.X) > coordinateTolerance)
|
|
{
|
|
rebuilt.Add(new ElkPoint { X = endpoint.X, Y = endpoint.Y });
|
|
}
|
|
}
|
|
|
|
if (!ElkEdgeRoutingGeometry.PointsEqual(rebuilt[^1], endpoint))
|
|
{
|
|
rebuilt.Add(new ElkPoint { X = endpoint.X, Y = endpoint.Y });
|
|
}
|
|
|
|
return NormalizePathPoints(rebuilt);
|
|
}
|
|
|
|
private static bool TryExtractTargetApproachFeeder(
|
|
IReadOnlyList<ElkPoint> path,
|
|
string side,
|
|
out (ElkPoint Start, ElkPoint End, double BandCoordinate) feeder)
|
|
{
|
|
feeder = default;
|
|
if (!TryExtractTargetApproachRun(path, side, out var runStartIndex, out _)
|
|
|| runStartIndex < 1)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var start = path[runStartIndex - 1];
|
|
var end = path[runStartIndex];
|
|
const double coordinateTolerance = 0.5d;
|
|
if (side is "top" or "bottom")
|
|
{
|
|
if (Math.Abs(start.Y - end.Y) > coordinateTolerance)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
feeder = (start, end, start.Y);
|
|
return true;
|
|
}
|
|
|
|
if (Math.Abs(start.X - end.X) > coordinateTolerance)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
feeder = (start, end, start.X);
|
|
return true;
|
|
}
|
|
|
|
private static List<ElkPoint> RewriteTargetApproachFeederBand(
|
|
IReadOnlyList<ElkPoint> path,
|
|
string side,
|
|
double desiredBand)
|
|
{
|
|
if (!TryExtractTargetApproachRun(path, side, out var runStartIndex, out var runEndIndex)
|
|
|| runStartIndex < 1)
|
|
{
|
|
return path
|
|
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
|
.ToList();
|
|
}
|
|
|
|
var prefix = path.Take(runStartIndex)
|
|
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
|
.ToList();
|
|
if (prefix.Count == 0)
|
|
{
|
|
prefix.Add(new ElkPoint { X = path[0].X, Y = path[0].Y });
|
|
}
|
|
|
|
var endpoint = path[^1];
|
|
const double coordinateTolerance = 0.5d;
|
|
if (side is "top" or "bottom")
|
|
{
|
|
if (Math.Abs(prefix[^1].Y - desiredBand) > coordinateTolerance)
|
|
{
|
|
prefix.Add(new ElkPoint { X = prefix[^1].X, Y = desiredBand });
|
|
}
|
|
|
|
var approachAxis = path[runEndIndex].X;
|
|
if (Math.Abs(prefix[^1].X - approachAxis) > coordinateTolerance)
|
|
{
|
|
prefix.Add(new ElkPoint { X = approachAxis, Y = desiredBand });
|
|
}
|
|
|
|
if (Math.Abs(prefix[^1].Y - endpoint.Y) > coordinateTolerance)
|
|
{
|
|
prefix.Add(new ElkPoint { X = approachAxis, Y = endpoint.Y });
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (Math.Abs(prefix[^1].X - desiredBand) > coordinateTolerance)
|
|
{
|
|
prefix.Add(new ElkPoint { X = desiredBand, Y = prefix[^1].Y });
|
|
}
|
|
|
|
var approachAxis = path[runEndIndex].Y;
|
|
if (Math.Abs(prefix[^1].Y - approachAxis) > coordinateTolerance)
|
|
{
|
|
prefix.Add(new ElkPoint { X = desiredBand, Y = approachAxis });
|
|
}
|
|
|
|
if (Math.Abs(prefix[^1].X - endpoint.X) > coordinateTolerance)
|
|
{
|
|
prefix.Add(new ElkPoint { X = endpoint.X, Y = approachAxis });
|
|
}
|
|
}
|
|
|
|
prefix.Add(new ElkPoint { X = endpoint.X, Y = endpoint.Y });
|
|
return NormalizePathPoints(prefix);
|
|
}
|
|
|
|
private static List<ElkPoint> ShiftSingleOrthogonalRun(
|
|
IReadOnlyList<ElkPoint> path,
|
|
int segmentIndex,
|
|
double desiredCoordinate)
|
|
{
|
|
var candidate = path
|
|
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
|
.ToList();
|
|
if (segmentIndex < 0 || segmentIndex >= candidate.Count - 1)
|
|
{
|
|
return candidate;
|
|
}
|
|
|
|
var start = candidate[segmentIndex];
|
|
var end = candidate[segmentIndex + 1];
|
|
if (Math.Abs(start.Y - end.Y) <= 0.5d)
|
|
{
|
|
var original = start.Y;
|
|
for (var i = 0; i < candidate.Count; i++)
|
|
{
|
|
if (Math.Abs(candidate[i].Y - original) <= 0.5d)
|
|
{
|
|
candidate[i] = new ElkPoint { X = candidate[i].X, Y = desiredCoordinate };
|
|
}
|
|
}
|
|
}
|
|
else if (Math.Abs(start.X - end.X) <= 0.5d)
|
|
{
|
|
var original = start.X;
|
|
for (var i = 0; i < candidate.Count; i++)
|
|
{
|
|
if (Math.Abs(candidate[i].X - original) <= 0.5d)
|
|
{
|
|
candidate[i] = new ElkPoint { X = desiredCoordinate, Y = candidate[i].Y };
|
|
}
|
|
}
|
|
}
|
|
|
|
return NormalizePathPoints(candidate);
|
|
}
|
|
|
|
private static List<ElkPoint> ShiftStraightOrthogonalPath(
|
|
IReadOnlyList<ElkPoint> path,
|
|
double desiredCoordinate)
|
|
{
|
|
var candidate = path
|
|
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
|
.ToList();
|
|
if (candidate.Count != 2)
|
|
{
|
|
return candidate;
|
|
}
|
|
|
|
var start = candidate[0];
|
|
var end = candidate[1];
|
|
if (Math.Abs(start.Y - end.Y) <= 0.5d)
|
|
{
|
|
return NormalizePathPoints(
|
|
[
|
|
new ElkPoint { X = start.X, Y = start.Y },
|
|
new ElkPoint { X = start.X, Y = desiredCoordinate },
|
|
new ElkPoint { X = end.X, Y = desiredCoordinate },
|
|
new ElkPoint { X = end.X, Y = end.Y },
|
|
]);
|
|
}
|
|
|
|
if (Math.Abs(start.X - end.X) <= 0.5d)
|
|
{
|
|
return NormalizePathPoints(
|
|
[
|
|
new ElkPoint { X = start.X, Y = start.Y },
|
|
new ElkPoint { X = desiredCoordinate, Y = start.Y },
|
|
new ElkPoint { X = desiredCoordinate, Y = end.Y },
|
|
new ElkPoint { X = end.X, Y = end.Y },
|
|
]);
|
|
}
|
|
|
|
return candidate;
|
|
}
|
|
|
|
private static double[] ResolveLaneShiftCoordinates(
|
|
ElkPoint start,
|
|
ElkPoint end,
|
|
ElkPoint otherStart,
|
|
ElkPoint otherEnd,
|
|
double minLineClearance)
|
|
{
|
|
var offset = minLineClearance + 4d;
|
|
if (Math.Abs(start.Y - end.Y) <= 0.5d && Math.Abs(otherStart.Y - otherEnd.Y) <= 0.5d)
|
|
{
|
|
var lower = otherStart.Y - offset;
|
|
var upper = otherStart.Y + offset;
|
|
return start.Y <= otherStart.Y
|
|
? [lower, upper]
|
|
: [upper, lower];
|
|
}
|
|
|
|
if (Math.Abs(start.X - end.X) <= 0.5d && Math.Abs(otherStart.X - otherEnd.X) <= 0.5d)
|
|
{
|
|
var lower = otherStart.X - offset;
|
|
var upper = otherStart.X + offset;
|
|
return start.X <= otherStart.X
|
|
? [lower, upper]
|
|
: [upper, lower];
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
private static bool SegmentLeavesGraphBand(
|
|
IReadOnlyList<ElkPoint> path,
|
|
double graphMinY,
|
|
double graphMaxY)
|
|
{
|
|
return path.Any(point => point.Y < graphMinY - 96d || point.Y > graphMaxY + 96d);
|
|
}
|
|
|
|
private static bool TrySeparateSharedLaneConflict(
|
|
ElkRoutedEdge edge,
|
|
ElkRoutedEdge otherEdge,
|
|
ElkPositionedNode[] nodes,
|
|
double minLineClearance,
|
|
double graphMinY,
|
|
double graphMaxY,
|
|
(double Left, double Top, double Right, double Bottom, string Id)[] nodeObstacles,
|
|
out ElkRoutedEdge repairedEdge)
|
|
{
|
|
repairedEdge = edge;
|
|
var path = ExtractFullPath(edge);
|
|
if (path.Count < 2)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal);
|
|
var originalTargetSide = nodesById.TryGetValue(edge.TargetNodeId ?? string.Empty, out var targetNode)
|
|
&& !ElkShapeBoundaries.IsGatewayShape(targetNode)
|
|
? ResolveTargetApproachSide(path, targetNode)
|
|
: null;
|
|
|
|
for (var segmentIndex = 0; segmentIndex < path.Count - 1; segmentIndex++)
|
|
{
|
|
var start = path[segmentIndex];
|
|
var end = path[segmentIndex + 1];
|
|
var isHorizontal = Math.Abs(start.Y - end.Y) <= 0.5d;
|
|
var isVertical = Math.Abs(start.X - end.X) <= 0.5d;
|
|
if (!isHorizontal && !isVertical)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
foreach (var otherSegment in ElkEdgeRoutingGeometry.FlattenSegments(otherEdge))
|
|
{
|
|
if (!SegmentsShareLane(
|
|
start,
|
|
end,
|
|
otherSegment.Start,
|
|
otherSegment.End,
|
|
minLineClearance))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
foreach (var alternateCoordinate in ResolveLaneShiftCoordinates(
|
|
start,
|
|
end,
|
|
otherSegment.Start,
|
|
otherSegment.End,
|
|
minLineClearance))
|
|
{
|
|
var candidate = path.Count == 2
|
|
? ShiftStraightOrthogonalPath(path, alternateCoordinate)
|
|
: ShiftSingleOrthogonalRun(path, segmentIndex, alternateCoordinate);
|
|
if (!PathChanged(path, candidate)
|
|
|| (originalTargetSide is not null
|
|
&& ResolveTargetApproachSide(candidate, targetNode) != originalTargetSide)
|
|
|| HasNodeObstacleCrossing(candidate, nodes, edge.SourceNodeId, edge.TargetNodeId)
|
|
|| SegmentLeavesGraphBand(candidate, graphMinY, graphMaxY))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var crossesObstacle = false;
|
|
for (var candidateIndex = 0; candidateIndex < candidate.Count - 1; candidateIndex++)
|
|
{
|
|
if (!SegmentCrossesObstacle(candidate[candidateIndex], candidate[candidateIndex + 1], nodeObstacles, edge.SourceNodeId, edge.TargetNodeId))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
crossesObstacle = true;
|
|
break;
|
|
}
|
|
|
|
if (crossesObstacle)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
repairedEdge = BuildSingleSectionEdge(edge, candidate);
|
|
repairedEdge = RepairBoundaryAnglesAndTargetApproaches(
|
|
[repairedEdge],
|
|
nodes,
|
|
minLineClearance)[0];
|
|
repairedEdge = NormalizeSourceExitAngles([repairedEdge], nodes)[0];
|
|
var repairedPath = ExtractFullPath(repairedEdge);
|
|
if ((originalTargetSide is not null
|
|
&& ResolveTargetApproachSide(repairedPath, targetNode) != originalTargetSide)
|
|
|| HasNodeObstacleCrossing(repairedPath, nodes, edge.SourceNodeId, edge.TargetNodeId)
|
|
|| SegmentLeavesGraphBand(repairedPath, graphMinY, graphMaxY))
|
|
{
|
|
repairedEdge = edge;
|
|
continue;
|
|
}
|
|
|
|
if (ElkEdgeRoutingScoring.DetectSharedLaneConflicts([repairedEdge, otherEdge], nodes).Count > 0)
|
|
{
|
|
repairedEdge = edge;
|
|
continue;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
repairedEdge = edge;
|
|
return false;
|
|
}
|
|
|
|
private static bool SegmentsShareLane(
|
|
ElkPoint leftStart,
|
|
ElkPoint leftEnd,
|
|
ElkPoint rightStart,
|
|
ElkPoint rightEnd,
|
|
double minLineClearance)
|
|
{
|
|
var laneTolerance = Math.Max(4d, Math.Min(12d, minLineClearance * 0.2d));
|
|
var minSharedLength = Math.Max(24d, minLineClearance * 0.4d);
|
|
if (Math.Abs(leftStart.Y - leftEnd.Y) <= 0.5d
|
|
&& Math.Abs(rightStart.Y - rightEnd.Y) <= 0.5d
|
|
&& Math.Abs(leftStart.Y - rightStart.Y) <= laneTolerance)
|
|
{
|
|
var leftMinX = Math.Min(leftStart.X, leftEnd.X);
|
|
var leftMaxX = Math.Max(leftStart.X, leftEnd.X);
|
|
var rightMinX = Math.Min(rightStart.X, rightEnd.X);
|
|
var rightMaxX = Math.Max(rightStart.X, rightEnd.X);
|
|
return Math.Min(leftMaxX, rightMaxX) - Math.Max(leftMinX, rightMinX) >= minSharedLength;
|
|
}
|
|
|
|
if (Math.Abs(leftStart.X - leftEnd.X) <= 0.5d
|
|
&& Math.Abs(rightStart.X - rightEnd.X) <= 0.5d
|
|
&& Math.Abs(leftStart.X - rightStart.X) <= laneTolerance)
|
|
{
|
|
var leftMinY = Math.Min(leftStart.Y, leftEnd.Y);
|
|
var leftMaxY = Math.Max(leftStart.Y, leftEnd.Y);
|
|
var rightMinY = Math.Min(rightStart.Y, rightEnd.Y);
|
|
var rightMaxY = Math.Max(rightStart.Y, rightEnd.Y);
|
|
return Math.Min(leftMaxY, rightMaxY) - Math.Max(leftMinY, rightMinY) >= minSharedLength;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static List<ElkPoint> RewriteSourceDepartureRun(
|
|
IReadOnlyList<ElkPoint> path,
|
|
string side,
|
|
ElkPoint boundaryPoint,
|
|
double desiredAxis)
|
|
{
|
|
if (!TryExtractSourceDepartureRun(path, side, out _, out var runEndIndex))
|
|
{
|
|
return path
|
|
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
|
.ToList();
|
|
}
|
|
|
|
var suffixStartIndex = runEndIndex + 1;
|
|
if (suffixStartIndex >= path.Count)
|
|
{
|
|
return path
|
|
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
|
.ToList();
|
|
}
|
|
|
|
const double coordinateTolerance = 0.5d;
|
|
var suffixStart = path[suffixStartIndex];
|
|
var rebuilt = new List<ElkPoint>
|
|
{
|
|
new() { X = boundaryPoint.X, Y = boundaryPoint.Y },
|
|
};
|
|
|
|
if (side is "left" or "right")
|
|
{
|
|
if (Math.Abs(rebuilt[^1].X - desiredAxis) > coordinateTolerance)
|
|
{
|
|
rebuilt.Add(new ElkPoint { X = desiredAxis, Y = rebuilt[^1].Y });
|
|
}
|
|
|
|
if (Math.Abs(rebuilt[^1].Y - suffixStart.Y) > coordinateTolerance)
|
|
{
|
|
rebuilt.Add(new ElkPoint { X = desiredAxis, Y = suffixStart.Y });
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (Math.Abs(rebuilt[^1].Y - desiredAxis) > coordinateTolerance)
|
|
{
|
|
rebuilt.Add(new ElkPoint { X = rebuilt[^1].X, Y = desiredAxis });
|
|
}
|
|
|
|
if (Math.Abs(rebuilt[^1].X - suffixStart.X) > coordinateTolerance)
|
|
{
|
|
rebuilt.Add(new ElkPoint { X = suffixStart.X, Y = desiredAxis });
|
|
}
|
|
}
|
|
|
|
for (var i = suffixStartIndex; i < path.Count; i++)
|
|
{
|
|
var point = path[i];
|
|
if (!ElkEdgeRoutingGeometry.PointsEqual(rebuilt[^1], point))
|
|
{
|
|
rebuilt.Add(new ElkPoint { X = point.X, Y = point.Y });
|
|
}
|
|
}
|
|
|
|
return NormalizePathPoints(rebuilt);
|
|
}
|
|
|
|
private static bool TryExtractTargetApproachRun(
|
|
IReadOnlyList<ElkPoint> path,
|
|
string side,
|
|
out int runStartIndex,
|
|
out int runEndIndex)
|
|
{
|
|
runStartIndex = -1;
|
|
runEndIndex = -1;
|
|
if (path.Count < 2 || side is not ("left" or "right" or "top" or "bottom"))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
const double coordinateTolerance = 0.5d;
|
|
runEndIndex = path.Count - 1;
|
|
if (side is "top" or "bottom")
|
|
{
|
|
var axis = path[runEndIndex].X;
|
|
runStartIndex = runEndIndex;
|
|
while (runStartIndex > 0 && Math.Abs(path[runStartIndex - 1].X - axis) <= coordinateTolerance)
|
|
{
|
|
runStartIndex--;
|
|
}
|
|
|
|
return runEndIndex >= runStartIndex;
|
|
}
|
|
|
|
var xAxis = path[runEndIndex].Y;
|
|
runStartIndex = runEndIndex;
|
|
while (runStartIndex > 0 && Math.Abs(path[runStartIndex - 1].Y - xAxis) <= coordinateTolerance)
|
|
{
|
|
runStartIndex--;
|
|
}
|
|
|
|
return runEndIndex >= runStartIndex;
|
|
}
|
|
|
|
private static bool TryExtractSourceDepartureRun(
|
|
IReadOnlyList<ElkPoint> path,
|
|
string side,
|
|
out int runStartIndex,
|
|
out int runEndIndex)
|
|
{
|
|
runStartIndex = -1;
|
|
runEndIndex = -1;
|
|
if (path.Count < 2 || side is not ("left" or "right" or "top" or "bottom"))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
const double coordinateTolerance = 0.5d;
|
|
runStartIndex = 0;
|
|
runEndIndex = 1;
|
|
if (side is "left" or "right")
|
|
{
|
|
var axis = path[1].Y;
|
|
if (Math.Abs(path[0].Y - axis) > coordinateTolerance)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
while (runEndIndex + 1 < path.Count && Math.Abs(path[runEndIndex + 1].Y - axis) <= coordinateTolerance)
|
|
{
|
|
runEndIndex++;
|
|
}
|
|
|
|
return runEndIndex > runStartIndex;
|
|
}
|
|
|
|
var xAxis = path[1].X;
|
|
if (Math.Abs(path[0].X - xAxis) > coordinateTolerance)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
while (runEndIndex + 1 < path.Count && Math.Abs(path[runEndIndex + 1].X - xAxis) <= coordinateTolerance)
|
|
{
|
|
runEndIndex++;
|
|
}
|
|
|
|
return runEndIndex > runStartIndex;
|
|
}
|
|
|
|
private static bool GroupHasSourceDepartureJoin(
|
|
IReadOnlyList<(IReadOnlyList<ElkPoint> Path, string Side)> entries,
|
|
double minLineClearance)
|
|
{
|
|
for (var i = 0; i < entries.Count; i++)
|
|
{
|
|
var left = entries[i];
|
|
var leftSegments = FlattenSegmentsNearStart(left.Path, 3);
|
|
for (var j = i + 1; j < entries.Count; j++)
|
|
{
|
|
var right = entries[j];
|
|
if (!string.Equals(left.Side, right.Side, StringComparison.Ordinal))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var rightSegments = FlattenSegmentsNearStart(right.Path, 3);
|
|
foreach (var leftSegment in leftSegments)
|
|
{
|
|
foreach (var rightSegment in rightSegments)
|
|
{
|
|
if (!ElkEdgeRoutingGeometry.AreParallelAndClose(
|
|
leftSegment.Start,
|
|
leftSegment.End,
|
|
rightSegment.Start,
|
|
rightSegment.End,
|
|
minLineClearance))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var overlap = ElkEdgeRoutingGeometry.ComputeSharedSegmentLength(
|
|
leftSegment.Start,
|
|
leftSegment.End,
|
|
rightSegment.Start,
|
|
rightSegment.End);
|
|
if (overlap > 8d)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static bool HasRepeatCollectorNodeClearanceViolation(
|
|
IReadOnlyList<ElkPoint> path,
|
|
IReadOnlyCollection<ElkPositionedNode> nodes,
|
|
string? sourceNodeId,
|
|
string? targetNodeId,
|
|
double minClearance)
|
|
{
|
|
for (var i = 0; i < path.Count - 1; i++)
|
|
{
|
|
var start = path[i];
|
|
var end = path[i + 1];
|
|
var horizontal = Math.Abs(start.Y - end.Y) < 2d;
|
|
var vertical = Math.Abs(start.X - end.X) < 2d;
|
|
if (!horizontal && !vertical)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
foreach (var node in nodes)
|
|
{
|
|
if (string.Equals(node.Id, sourceNodeId, StringComparison.Ordinal)
|
|
|| string.Equals(node.Id, targetNodeId, StringComparison.Ordinal))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (horizontal)
|
|
{
|
|
var overlapX = Math.Max(start.X, end.X) > node.X
|
|
&& Math.Min(start.X, end.X) < node.X + node.Width;
|
|
if (!overlapX)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var distance = Math.Min(Math.Abs(start.Y - node.Y), Math.Abs(start.Y - (node.Y + node.Height)));
|
|
if (distance > 0.5d && distance < minClearance)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
var overlapY = Math.Max(start.Y, end.Y) > node.Y
|
|
&& Math.Min(start.Y, end.Y) < node.Y + node.Height;
|
|
if (!overlapY)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var verticalDistance = Math.Min(Math.Abs(start.X - node.X), Math.Abs(start.X - (node.X + node.Width)));
|
|
if (verticalDistance > 0.5d && verticalDistance < minClearance)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static List<ElkPoint> TryLiftUnderNodeSegments(
|
|
IReadOnlyList<ElkPoint> path,
|
|
IReadOnlyCollection<ElkPositionedNode> nodes,
|
|
string? sourceNodeId,
|
|
string? targetNodeId,
|
|
double minClearance)
|
|
{
|
|
var current = path
|
|
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
|
.ToList();
|
|
for (var pass = 0; pass < 6; pass++)
|
|
{
|
|
var changed = false;
|
|
for (var segmentIndex = 0; segmentIndex < current.Count - 1; segmentIndex++)
|
|
{
|
|
if (!TryResolveUnderNodeBlockers(
|
|
current[segmentIndex],
|
|
current[segmentIndex + 1],
|
|
nodes,
|
|
sourceNodeId,
|
|
targetNodeId,
|
|
minClearance,
|
|
out var blockers))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var minX = Math.Min(current[segmentIndex].X, current[segmentIndex + 1].X);
|
|
var maxX = Math.Max(current[segmentIndex].X, current[segmentIndex + 1].X);
|
|
var maxRelevantDistance = Math.Max(minClearance * 1.75d, 96d);
|
|
var overlappingNodes = nodes
|
|
.Where(node =>
|
|
!string.Equals(node.Id, sourceNodeId, StringComparison.Ordinal)
|
|
&& !string.Equals(node.Id, targetNodeId, StringComparison.Ordinal)
|
|
&& maxX > node.X + 0.5d
|
|
&& minX < node.X + node.Width - 0.5d)
|
|
.Where(node =>
|
|
{
|
|
var distanceBelowNode = current[segmentIndex].Y - (node.Y + node.Height);
|
|
return distanceBelowNode > 0.5d && distanceBelowNode < maxRelevantDistance;
|
|
})
|
|
.ToArray();
|
|
var liftY = (overlappingNodes.Length > 0 ? overlappingNodes.Min(node => node.Y) : blockers.Min(node => node.Y))
|
|
- Math.Max(24d, minClearance * 0.6d);
|
|
if (liftY >= current[segmentIndex].Y - 0.5d)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var rebuilt = new List<ElkPoint>(current.Count + 2);
|
|
rebuilt.AddRange(current.Take(segmentIndex + 1).Select(point => new ElkPoint { X = point.X, Y = point.Y }));
|
|
rebuilt.Add(new ElkPoint { X = current[segmentIndex].X, Y = liftY });
|
|
rebuilt.Add(new ElkPoint { X = current[segmentIndex + 1].X, Y = liftY });
|
|
rebuilt.AddRange(current.Skip(segmentIndex + 1).Select(point => new ElkPoint { X = point.X, Y = point.Y }));
|
|
|
|
var candidate = NormalizePathPoints(rebuilt);
|
|
if (!PathChanged(current, candidate)
|
|
|| HasNodeObstacleCrossing(candidate, nodes, sourceNodeId, targetNodeId)
|
|
|| CountUnderNodeSegments(candidate, nodes, sourceNodeId, targetNodeId, minClearance)
|
|
>= CountUnderNodeSegments(current, nodes, sourceNodeId, targetNodeId, minClearance))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
current = candidate;
|
|
changed = true;
|
|
break;
|
|
}
|
|
|
|
if (!changed)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
return current;
|
|
}
|
|
|
|
private static bool TryResolveUnderNodeWithPreferredShortcut(
|
|
ElkRoutedEdge edge,
|
|
IReadOnlyList<ElkPoint> path,
|
|
IReadOnlyCollection<ElkPositionedNode> nodes,
|
|
double minClearance,
|
|
out List<ElkPoint> repairedPath)
|
|
{
|
|
repairedPath = path
|
|
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
|
.ToList();
|
|
if (path.Count < 2)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal);
|
|
if (!nodesById.TryGetValue(edge.SourceNodeId ?? string.Empty, out var sourceNode)
|
|
|| !nodesById.TryGetValue(edge.TargetNodeId ?? string.Empty, out var targetNode))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (TryApplyPreferredBoundaryShortcut(
|
|
path,
|
|
sourceNode,
|
|
targetNode,
|
|
nodes,
|
|
edge.SourceNodeId,
|
|
edge.TargetNodeId,
|
|
requireUnderNodeImprovement: true,
|
|
minClearance,
|
|
out repairedPath))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (TryBuildBestUnderNodeBandCandidate(
|
|
path,
|
|
sourceNode,
|
|
targetNode,
|
|
nodes,
|
|
edge.SourceNodeId,
|
|
edge.TargetNodeId,
|
|
minClearance,
|
|
edge.Id,
|
|
out repairedPath))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
var targetSide = ResolveTargetApproachSide(path, targetNode);
|
|
if (!IsRepeatCollectorLabel(edge.Label)
|
|
&& TryBuildGatewaySourceUnderNodeDropCandidate(
|
|
path,
|
|
sourceNode,
|
|
targetNode,
|
|
nodes,
|
|
edge.SourceNodeId,
|
|
edge.TargetNodeId,
|
|
minClearance,
|
|
out repairedPath))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (TryBuildSafeGatewaySourceBandCandidate(
|
|
sourceNode,
|
|
targetNode,
|
|
nodes,
|
|
edge.SourceNodeId,
|
|
edge.TargetNodeId,
|
|
path[^1],
|
|
minClearance,
|
|
out repairedPath))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (targetSide is "top" or "bottom"
|
|
&& TryBuildSafeHorizontalBandCandidate(
|
|
sourceNode,
|
|
targetNode,
|
|
nodes,
|
|
edge.SourceNodeId,
|
|
edge.TargetNodeId,
|
|
path[0],
|
|
path[^1],
|
|
minClearance,
|
|
preferredSourceExterior: path.Count > 1 ? path[1] : null,
|
|
out repairedPath))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
repairedPath = path
|
|
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
|
.ToList();
|
|
return false;
|
|
}
|
|
|
|
private static bool TryBuildBestUnderNodeBandCandidate(
|
|
IReadOnlyList<ElkPoint> path,
|
|
ElkPositionedNode sourceNode,
|
|
ElkPositionedNode targetNode,
|
|
IReadOnlyCollection<ElkPositionedNode> nodes,
|
|
string? sourceNodeId,
|
|
string? targetNodeId,
|
|
double minClearance,
|
|
string? debugEdgeId,
|
|
out List<ElkPoint> candidate)
|
|
{
|
|
candidate = [];
|
|
if (path.Count < 2
|
|
|| !TryResolveUnderNodeBand(path, nodes, sourceNodeId, targetNodeId, minClearance, out var bandY))
|
|
{
|
|
WriteUnderNodeDebug(debugEdgeId, $"band-unavailable path={FormatPath(path)}");
|
|
return false;
|
|
}
|
|
|
|
var currentUnderNodeSegments = CountUnderNodeSegments(path, nodes, sourceNodeId, targetNodeId, minClearance);
|
|
if (currentUnderNodeSegments == 0)
|
|
{
|
|
WriteUnderNodeDebug(debugEdgeId, $"band-skip no-under-node path={FormatPath(path)}");
|
|
return false;
|
|
}
|
|
|
|
WriteUnderNodeDebug(debugEdgeId, $"band-try y={bandY:F2} under={currentUnderNodeSegments} path={FormatPath(path)}");
|
|
|
|
var bestScore = double.PositiveInfinity;
|
|
List<ElkPoint>? bestCandidate = null;
|
|
var preferredTargetSide = ResolveTargetApproachSide(path, targetNode);
|
|
foreach (var (side, endBoundary, sideBias) in EnumerateUnderNodeBandTargetBoundaries(path, sourceNode, targetNode, bandY))
|
|
{
|
|
if (!TryBuildExplicitUnderNodeBandCandidate(
|
|
path,
|
|
sourceNode,
|
|
targetNode,
|
|
nodes,
|
|
sourceNodeId,
|
|
targetNodeId,
|
|
side,
|
|
bandY,
|
|
endBoundary,
|
|
minClearance,
|
|
out var bandCandidate))
|
|
{
|
|
WriteUnderNodeDebug(debugEdgeId, $"band-reject build side={side} end={FormatPoint(endBoundary)}");
|
|
continue;
|
|
}
|
|
|
|
if (!PathChanged(path, bandCandidate))
|
|
{
|
|
WriteUnderNodeDebug(debugEdgeId, $"band-reject unchanged side={side} candidate={FormatPath(bandCandidate)}");
|
|
continue;
|
|
}
|
|
|
|
var candidateUnderNodeSegments = CountUnderNodeSegments(bandCandidate, nodes, sourceNodeId, targetNodeId, minClearance);
|
|
if (candidateUnderNodeSegments >= currentUnderNodeSegments)
|
|
{
|
|
WriteUnderNodeDebug(debugEdgeId, $"band-reject no-improvement side={side} under={candidateUnderNodeSegments} blockers={FormatUnderNodeBlockers(bandCandidate, nodes, sourceNodeId, targetNodeId, minClearance)} candidate={FormatPath(bandCandidate)}");
|
|
continue;
|
|
}
|
|
|
|
if (ComputePathLength(bandCandidate) > ComputePathLength(path) + 260d)
|
|
{
|
|
WriteUnderNodeDebug(debugEdgeId, $"band-reject long side={side} candidate={FormatPath(bandCandidate)}");
|
|
continue;
|
|
}
|
|
|
|
var score = ScoreUnderNodeBandCandidate(
|
|
path,
|
|
bandCandidate,
|
|
targetNode,
|
|
side,
|
|
preferredTargetSide,
|
|
sideBias,
|
|
nodes,
|
|
sourceNodeId,
|
|
targetNodeId,
|
|
minClearance);
|
|
if (score >= bestScore)
|
|
{
|
|
WriteUnderNodeDebug(debugEdgeId, $"band-reject score side={side} score={score:F2} candidate={FormatPath(bandCandidate)}");
|
|
continue;
|
|
}
|
|
|
|
WriteUnderNodeDebug(debugEdgeId, $"band-candidate side={side} score={score:F2} candidate={FormatPath(bandCandidate)}");
|
|
bestScore = score;
|
|
bestCandidate = bandCandidate;
|
|
}
|
|
|
|
if (bestCandidate is null)
|
|
{
|
|
WriteUnderNodeDebug(debugEdgeId, "band-result none");
|
|
return false;
|
|
}
|
|
|
|
WriteUnderNodeDebug(debugEdgeId, $"band-result selected score={bestScore:F2} candidate={FormatPath(bestCandidate)}");
|
|
candidate = bestCandidate;
|
|
return true;
|
|
}
|
|
|
|
private static bool TryBuildExplicitUnderNodeBandCandidate(
|
|
IReadOnlyList<ElkPoint> originalPath,
|
|
ElkPositionedNode sourceNode,
|
|
ElkPositionedNode targetNode,
|
|
IReadOnlyCollection<ElkPositionedNode> nodes,
|
|
string? sourceNodeId,
|
|
string? targetNodeId,
|
|
string targetSide,
|
|
double bandY,
|
|
ElkPoint endBoundary,
|
|
double minClearance,
|
|
out List<ElkPoint> candidate)
|
|
{
|
|
candidate = [];
|
|
if (originalPath.Count < 2)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var currentUnderNodeSegments = CountUnderNodeSegments(originalPath, nodes, sourceNodeId, targetNodeId, minClearance);
|
|
if (currentUnderNodeSegments == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var bestLength = double.PositiveInfinity;
|
|
List<ElkPoint>? bestCandidate = null;
|
|
foreach (var bandEntryX in EnumerateUnderNodeBandEntryXs(originalPath, endBoundary, nodes, sourceNodeId, targetNodeId, minClearance))
|
|
{
|
|
var candidateBandY = bandY;
|
|
for (var refinement = 0; refinement < 4; refinement++)
|
|
{
|
|
if (!TryResolveUnderNodeBandTargetGeometry(
|
|
targetNode,
|
|
targetSide,
|
|
endBoundary,
|
|
candidateBandY,
|
|
minClearance,
|
|
out var targetBoundary,
|
|
out var targetExterior))
|
|
{
|
|
break;
|
|
}
|
|
|
|
if (!TryResolveUnderNodeBandSourceGeometry(
|
|
originalPath,
|
|
sourceNode,
|
|
targetBoundary,
|
|
bandEntryX,
|
|
candidateBandY,
|
|
out var startBoundary,
|
|
out var sourceExterior))
|
|
{
|
|
break;
|
|
}
|
|
|
|
var route = BuildUnderNodeBandCandidatePath(
|
|
startBoundary,
|
|
sourceExterior,
|
|
bandEntryX,
|
|
candidateBandY,
|
|
targetExterior,
|
|
targetBoundary);
|
|
var bandCandidate = NormalizePathPoints(route);
|
|
if (!IsUsableUnderNodeBandCandidate(
|
|
bandCandidate,
|
|
sourceNode,
|
|
targetNode,
|
|
nodes,
|
|
sourceNodeId,
|
|
targetNodeId))
|
|
{
|
|
break;
|
|
}
|
|
|
|
var candidateUnderNodeSegments = CountUnderNodeSegments(bandCandidate, nodes, sourceNodeId, targetNodeId, minClearance);
|
|
if (candidateUnderNodeSegments == 0)
|
|
{
|
|
var candidateLength = ComputePathLength(bandCandidate);
|
|
if (candidateLength < bestLength)
|
|
{
|
|
bestLength = candidateLength;
|
|
bestCandidate = bandCandidate;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
if (!TryResolveUnderNodeBand(
|
|
bandCandidate,
|
|
nodes,
|
|
sourceNodeId,
|
|
targetNodeId,
|
|
minClearance,
|
|
out var refinedBandY)
|
|
|| Math.Abs(refinedBandY - candidateBandY) <= 0.5d)
|
|
{
|
|
break;
|
|
}
|
|
|
|
candidateBandY = refinedBandY;
|
|
}
|
|
}
|
|
|
|
if (bestCandidate is null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
candidate = bestCandidate;
|
|
return true;
|
|
}
|
|
|
|
private static IEnumerable<double> EnumerateUnderNodeBandEntryXs(
|
|
IReadOnlyList<ElkPoint> originalPath,
|
|
ElkPoint endBoundary,
|
|
IReadOnlyCollection<ElkPositionedNode> nodes,
|
|
string? sourceNodeId,
|
|
string? targetNodeId,
|
|
double minClearance)
|
|
{
|
|
var clearance = Math.Max(24d, minClearance * 0.6d);
|
|
var preferredX = originalPath.Count > 1 ? originalPath[1].X : originalPath[0].X;
|
|
var coordinates = new List<double>
|
|
{
|
|
preferredX,
|
|
originalPath[0].X,
|
|
endBoundary.X,
|
|
};
|
|
|
|
var minX = Math.Min(preferredX, endBoundary.X) - (clearance * 2d);
|
|
var maxX = Math.Max(preferredX, endBoundary.X) + (clearance * 2d);
|
|
foreach (var node in nodes)
|
|
{
|
|
if (string.Equals(node.Id, sourceNodeId, StringComparison.Ordinal)
|
|
|| string.Equals(node.Id, targetNodeId, StringComparison.Ordinal)
|
|
|| node.X > maxX
|
|
|| node.X + node.Width < minX)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
AddUniqueCoordinate(coordinates, node.X - clearance);
|
|
AddUniqueCoordinate(coordinates, node.X + node.Width + clearance);
|
|
}
|
|
|
|
foreach (var coordinate in coordinates
|
|
.OrderBy(value => Math.Abs(value - preferredX))
|
|
.ThenBy(value => Math.Abs(value - endBoundary.X))
|
|
.Take(10))
|
|
{
|
|
yield return coordinate;
|
|
}
|
|
}
|
|
|
|
private static bool TryResolveUnderNodeBandTargetGeometry(
|
|
ElkPositionedNode targetNode,
|
|
string targetSide,
|
|
ElkPoint preferredBoundary,
|
|
double bandY,
|
|
double minClearance,
|
|
out ElkPoint targetBoundary,
|
|
out ElkPoint targetExterior)
|
|
{
|
|
targetBoundary = default!;
|
|
targetExterior = default!;
|
|
|
|
var clearance = Math.Max(24d, minClearance * 0.6d);
|
|
var axisCoordinate = targetSide is "top" or "bottom"
|
|
? preferredBoundary.X
|
|
: preferredBoundary.Y;
|
|
if (!TryBuildUnderNodeBandTargetBoundary(targetNode, targetSide, axisCoordinate, bandY, out targetBoundary))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var targetAnchor = targetSide switch
|
|
{
|
|
"left" => new ElkPoint { X = targetBoundary.X - clearance, Y = targetBoundary.Y },
|
|
"right" => new ElkPoint { X = targetBoundary.X + clearance, Y = targetBoundary.Y },
|
|
_ => new ElkPoint { X = targetBoundary.X, Y = bandY },
|
|
};
|
|
|
|
if (ElkShapeBoundaries.IsGatewayShape(targetNode))
|
|
{
|
|
targetBoundary = ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(targetNode, targetBoundary, targetAnchor);
|
|
targetExterior = ElkShapeBoundaries.BuildGatewayDirectionalExteriorPoint(targetNode, targetBoundary, targetAnchor);
|
|
return !ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(targetNode, targetExterior)
|
|
&& ElkShapeBoundaries.HasValidGatewayBoundaryAngle(targetNode, targetBoundary, targetExterior);
|
|
}
|
|
|
|
targetExterior = targetSide switch
|
|
{
|
|
"left" => new ElkPoint { X = targetBoundary.X - clearance, Y = targetBoundary.Y },
|
|
"right" => new ElkPoint { X = targetBoundary.X + clearance, Y = targetBoundary.Y },
|
|
"top" => new ElkPoint { X = targetBoundary.X, Y = targetBoundary.Y - clearance },
|
|
"bottom" => new ElkPoint { X = targetBoundary.X, Y = targetBoundary.Y + clearance },
|
|
_ => targetBoundary,
|
|
};
|
|
return true;
|
|
}
|
|
|
|
private static bool TryResolveUnderNodeBandSourceGeometry(
|
|
IReadOnlyList<ElkPoint> originalPath,
|
|
ElkPositionedNode sourceNode,
|
|
ElkPoint targetBoundary,
|
|
double bandEntryX,
|
|
double bandY,
|
|
out ElkPoint startBoundary,
|
|
out ElkPoint sourceExterior)
|
|
{
|
|
startBoundary = new ElkPoint { X = originalPath[0].X, Y = originalPath[0].Y };
|
|
sourceExterior = startBoundary;
|
|
if (!ElkShapeBoundaries.IsGatewayShape(sourceNode))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
var bandEntry = new ElkPoint { X = bandEntryX, Y = bandY };
|
|
sourceExterior = ElkShapeBoundaries.BuildPreferredGatewaySourceExteriorPoint(sourceNode, startBoundary, bandEntry);
|
|
var canReuseCurrentBoundary = CanReuseGatewayBoundaryForBandRoute(
|
|
sourceNode,
|
|
startBoundary,
|
|
sourceExterior,
|
|
bandEntry);
|
|
if (!canReuseCurrentBoundary)
|
|
{
|
|
if (!TryResolvePreferredGatewaySourceBoundary(sourceNode, bandEntry, targetBoundary, out startBoundary))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
sourceExterior = ElkShapeBoundaries.BuildPreferredGatewaySourceExteriorPoint(sourceNode, startBoundary, bandEntry);
|
|
if (ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(sourceNode, sourceExterior)
|
|
|| !ElkShapeBoundaries.HasValidGatewayBoundaryAngle(sourceNode, startBoundary, sourceExterior))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private static List<ElkPoint> BuildUnderNodeBandCandidatePath(
|
|
ElkPoint startBoundary,
|
|
ElkPoint sourceExterior,
|
|
double bandEntryX,
|
|
double bandY,
|
|
ElkPoint targetExterior,
|
|
ElkPoint targetBoundary)
|
|
{
|
|
var route = new List<ElkPoint> { startBoundary };
|
|
if (!ElkEdgeRoutingGeometry.PointsEqual(route[^1], sourceExterior))
|
|
{
|
|
route.Add(sourceExterior);
|
|
}
|
|
|
|
if (Math.Abs(route[^1].X - bandEntryX) > 0.5d)
|
|
{
|
|
route.Add(new ElkPoint { X = bandEntryX, Y = route[^1].Y });
|
|
}
|
|
|
|
if (Math.Abs(route[^1].Y - bandY) > 0.5d)
|
|
{
|
|
route.Add(new ElkPoint { X = bandEntryX, Y = bandY });
|
|
}
|
|
|
|
if (Math.Abs(route[^1].X - targetExterior.X) > 0.5d)
|
|
{
|
|
route.Add(new ElkPoint { X = targetExterior.X, Y = bandY });
|
|
}
|
|
|
|
if (Math.Abs(route[^1].Y - targetExterior.Y) > 0.5d)
|
|
{
|
|
route.Add(new ElkPoint { X = targetExterior.X, Y = targetExterior.Y });
|
|
}
|
|
|
|
if (!ElkEdgeRoutingGeometry.PointsEqual(route[^1], targetExterior))
|
|
{
|
|
route.Add(targetExterior);
|
|
}
|
|
|
|
if (!ElkEdgeRoutingGeometry.PointsEqual(route[^1], targetBoundary))
|
|
{
|
|
route.Add(targetBoundary);
|
|
}
|
|
|
|
return route;
|
|
}
|
|
|
|
private static bool IsUsableUnderNodeBandCandidate(
|
|
IReadOnlyList<ElkPoint> candidate,
|
|
ElkPositionedNode sourceNode,
|
|
ElkPositionedNode targetNode,
|
|
IReadOnlyCollection<ElkPositionedNode> nodes,
|
|
string? sourceNodeId,
|
|
string? targetNodeId)
|
|
{
|
|
if (candidate.Count < 2
|
|
|| HasNodeObstacleCrossing(candidate, nodes, sourceNodeId, targetNodeId))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (ElkShapeBoundaries.IsGatewayShape(sourceNode))
|
|
{
|
|
if (!HasAcceptableGatewayBoundaryPath(candidate, nodes, sourceNodeId, targetNodeId, sourceNode, fromStart: true)
|
|
|| !HasCleanGatewaySourceBandPath(candidate, sourceNode))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
else if (!HasValidBoundaryAngle(candidate[0], candidate[1], sourceNode))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (ElkShapeBoundaries.IsGatewayShape(targetNode))
|
|
{
|
|
return CanAcceptGatewayTargetRepair(candidate, targetNode)
|
|
&& HasAcceptableGatewayBoundaryPath(candidate, nodes, sourceNodeId, targetNodeId, targetNode, fromStart: false);
|
|
}
|
|
|
|
return !HasTargetApproachBacktracking(candidate, targetNode)
|
|
&& HasValidBoundaryAngle(candidate[^1], candidate[^2], targetNode);
|
|
}
|
|
|
|
private static bool TryResolveUnderNodeBand(
|
|
IReadOnlyList<ElkPoint> path,
|
|
IReadOnlyCollection<ElkPositionedNode> nodes,
|
|
string? sourceNodeId,
|
|
string? targetNodeId,
|
|
double minClearance,
|
|
out double bandY)
|
|
{
|
|
bandY = double.NaN;
|
|
var blockers = new Dictionary<string, ElkPositionedNode>(StringComparer.Ordinal);
|
|
for (var i = 0; i < path.Count - 1; i++)
|
|
{
|
|
if (!TryResolveUnderNodeBlockers(
|
|
path[i],
|
|
path[i + 1],
|
|
nodes,
|
|
sourceNodeId,
|
|
targetNodeId,
|
|
minClearance,
|
|
out var segmentBlockers))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
foreach (var blocker in segmentBlockers)
|
|
{
|
|
blockers[blocker.Id] = blocker;
|
|
}
|
|
}
|
|
|
|
if (blockers.Count == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var clearance = Math.Max(24d, minClearance * 0.6d);
|
|
var graphMinY = nodes.Min(node => node.Y);
|
|
bandY = Math.Max(graphMinY - 72d, blockers.Values.Min(node => node.Y) - clearance);
|
|
return true;
|
|
}
|
|
|
|
private static IEnumerable<(string Side, ElkPoint Boundary, double SideBias)> EnumerateUnderNodeBandTargetBoundaries(
|
|
IReadOnlyList<ElkPoint> path,
|
|
ElkPositionedNode sourceNode,
|
|
ElkPositionedNode targetNode,
|
|
double bandY)
|
|
{
|
|
const double coordinateTolerance = 0.5d;
|
|
var referenceXs = new List<double>
|
|
{
|
|
path[0].X,
|
|
path[^1].X,
|
|
path.Count > 1 ? path[1].X : path[0].X,
|
|
path.Count > 1 ? path[^2].X : path[^1].X,
|
|
sourceNode.X + (sourceNode.Width / 2d),
|
|
targetNode.X + (targetNode.Width / 2d),
|
|
};
|
|
var referenceYs = new List<double>
|
|
{
|
|
path[0].Y,
|
|
path[^1].Y,
|
|
path.Count > 1 ? path[1].Y : path[0].Y,
|
|
path.Count > 1 ? path[^2].Y : path[^1].Y,
|
|
sourceNode.Y + (sourceNode.Height / 2d),
|
|
targetNode.Y + (targetNode.Height / 2d),
|
|
};
|
|
|
|
var sides = new List<(string Side, double SideBias)>();
|
|
if (bandY < targetNode.Y - coordinateTolerance)
|
|
{
|
|
sides.Add(("top", -200d));
|
|
}
|
|
|
|
if (bandY > targetNode.Y + targetNode.Height + coordinateTolerance)
|
|
{
|
|
sides.Add(("bottom", -200d));
|
|
}
|
|
|
|
if (path[0].X <= targetNode.X - coordinateTolerance
|
|
|| path[^1].X <= targetNode.X - coordinateTolerance)
|
|
{
|
|
sides.Add(("left", -120d));
|
|
}
|
|
|
|
if (path[0].X >= targetNode.X + targetNode.Width + coordinateTolerance
|
|
|| path[^1].X >= targetNode.X + targetNode.Width + coordinateTolerance)
|
|
{
|
|
sides.Add(("right", -120d));
|
|
}
|
|
|
|
if (sides.Count == 0)
|
|
{
|
|
yield break;
|
|
}
|
|
|
|
var seenBoundaries = new HashSet<string>(StringComparer.Ordinal);
|
|
foreach (var (side, sideBias) in sides)
|
|
{
|
|
var coordinates = side is "top" or "bottom" ? referenceXs : referenceYs;
|
|
foreach (var coordinate in coordinates)
|
|
{
|
|
if (!TryBuildUnderNodeBandTargetBoundary(targetNode, side, coordinate, bandY, out var boundary))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var key = $"{side}|{Math.Round(boundary.X, 2):F2}|{Math.Round(boundary.Y, 2):F2}";
|
|
if (!seenBoundaries.Add(key))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
yield return (side, boundary, sideBias);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static bool TryBuildUnderNodeBandTargetBoundary(
|
|
ElkPositionedNode targetNode,
|
|
string side,
|
|
double axisCoordinate,
|
|
double bandY,
|
|
out ElkPoint boundary)
|
|
{
|
|
boundary = default!;
|
|
var referencePoint = side switch
|
|
{
|
|
"top" or "bottom" => new ElkPoint { X = axisCoordinate, Y = bandY },
|
|
"left" => new ElkPoint { X = targetNode.X - 24d, Y = axisCoordinate },
|
|
"right" => new ElkPoint { X = targetNode.X + targetNode.Width + 24d, Y = axisCoordinate },
|
|
_ => new ElkPoint { X = axisCoordinate, Y = bandY },
|
|
};
|
|
|
|
if (ElkShapeBoundaries.IsGatewayShape(targetNode))
|
|
{
|
|
var slotCoordinate = side is "top" or "bottom"
|
|
? referencePoint.X
|
|
: referencePoint.Y;
|
|
if (!ElkShapeBoundaries.TryProjectGatewayBoundarySlot(targetNode, side, slotCoordinate, out boundary))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
boundary = ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(targetNode, boundary, referencePoint);
|
|
return true;
|
|
}
|
|
|
|
boundary = BuildRectBoundaryPointForSide(targetNode, side, referencePoint);
|
|
return true;
|
|
}
|
|
|
|
private static double ScoreUnderNodeBandCandidate(
|
|
IReadOnlyList<ElkPoint> originalPath,
|
|
IReadOnlyList<ElkPoint> candidate,
|
|
ElkPositionedNode targetNode,
|
|
string requestedTargetSide,
|
|
string preferredTargetSide,
|
|
double sideBias,
|
|
IReadOnlyCollection<ElkPositionedNode> nodes,
|
|
string? sourceNodeId,
|
|
string? targetNodeId,
|
|
double minClearance)
|
|
{
|
|
var candidateUnderNodeSegments = CountUnderNodeSegments(candidate, nodes, sourceNodeId, targetNodeId, minClearance);
|
|
var score = (candidateUnderNodeSegments * 100_000d)
|
|
+ ComputePathLength(candidate)
|
|
+ (Math.Max(0, candidate.Count - 2) * 8d)
|
|
+ sideBias;
|
|
|
|
var actualTargetSide = ResolveTargetApproachSide(candidate, targetNode);
|
|
if (!string.Equals(actualTargetSide, preferredTargetSide, StringComparison.Ordinal))
|
|
{
|
|
score += 1_000d;
|
|
}
|
|
|
|
if (!string.Equals(actualTargetSide, requestedTargetSide, StringComparison.Ordinal))
|
|
{
|
|
score += 350d;
|
|
}
|
|
|
|
if (ComputePathLength(candidate) > ComputePathLength(originalPath))
|
|
{
|
|
score += (ComputePathLength(candidate) - ComputePathLength(originalPath)) * 0.5d;
|
|
}
|
|
|
|
return score;
|
|
}
|
|
|
|
private static void WriteUnderNodeDebug(string? edgeId, string message)
|
|
{
|
|
if (edgeId is not ("edge/9" or "edge/25"))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var path = System.IO.Path.Combine(System.AppContext.BaseDirectory, "elksharp.undernode-debug.log");
|
|
lock (UnderNodeDebugSync)
|
|
{
|
|
System.IO.File.AppendAllText(path, $"[{System.DateTime.UtcNow:O}] {edgeId} {message}{System.Environment.NewLine}");
|
|
}
|
|
}
|
|
|
|
private static string FormatPath(IReadOnlyList<ElkPoint> path)
|
|
{
|
|
return string.Join(" -> ", path.Select(FormatPoint));
|
|
}
|
|
|
|
private static string FormatPoint(ElkPoint point)
|
|
{
|
|
return $"({point.X:F2},{point.Y:F2})";
|
|
}
|
|
|
|
private static string FormatUnderNodeBlockers(
|
|
IReadOnlyList<ElkPoint> path,
|
|
IReadOnlyCollection<ElkPositionedNode> nodes,
|
|
string? sourceNodeId,
|
|
string? targetNodeId,
|
|
double minClearance)
|
|
{
|
|
var blockers = new List<string>();
|
|
for (var i = 0; i < path.Count - 1; i++)
|
|
{
|
|
if (!TryResolveUnderNodeBlockers(path[i], path[i + 1], nodes, sourceNodeId, targetNodeId, minClearance, out var segmentBlockers))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
blockers.Add($"{FormatPoint(path[i])}->{FormatPoint(path[i + 1])}:{string.Join(",", segmentBlockers.Select(node => node.Id))}");
|
|
}
|
|
|
|
return blockers.Count == 0 ? "<none>" : string.Join(" | ", blockers);
|
|
}
|
|
|
|
private static bool TryBuildGatewaySourceUnderNodeDropCandidate(
|
|
IReadOnlyList<ElkPoint> path,
|
|
ElkPositionedNode sourceNode,
|
|
ElkPositionedNode targetNode,
|
|
IReadOnlyCollection<ElkPositionedNode> nodes,
|
|
string? sourceNodeId,
|
|
string? targetNodeId,
|
|
double minClearance,
|
|
out List<ElkPoint> candidate)
|
|
{
|
|
candidate = [];
|
|
if (!ElkShapeBoundaries.IsGatewayShape(sourceNode)
|
|
|| path.Count < 2)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
ElkPositionedNode[] blockers = [];
|
|
for (var i = 0; i < path.Count - 1; i++)
|
|
{
|
|
if (TryResolveUnderNodeBlockers(
|
|
path[i],
|
|
path[i + 1],
|
|
nodes,
|
|
sourceNodeId,
|
|
targetNodeId,
|
|
minClearance,
|
|
out blockers))
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (blockers.Length == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (path[1].Y <= path[0].Y + 0.5d)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var targetSide =
|
|
targetNode.X >= sourceNode.X + sourceNode.Width - 0.5d ? "left" :
|
|
targetNode.X + targetNode.Width <= sourceNode.X + 0.5d ? "right" :
|
|
ResolveTargetApproachSide(path, targetNode);
|
|
if (targetSide is not "left" and not "right")
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var currentUnderNodeSegments = CountUnderNodeSegments(path, nodes, sourceNodeId, targetNodeId, minClearance);
|
|
if (currentUnderNodeSegments == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var clearance = Math.Max(24d, minClearance * 0.6d);
|
|
var graphMinY = nodes.Min(node => node.Y);
|
|
var bandY = blockers.Min(node => node.Y) - clearance;
|
|
if (bandY <= graphMinY - 96d)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var targetAnchor = targetSide == "left"
|
|
? new ElkPoint { X = targetNode.X - clearance, Y = bandY }
|
|
: new ElkPoint { X = targetNode.X + targetNode.Width + clearance, Y = bandY };
|
|
|
|
ElkPoint targetBoundary;
|
|
ElkPoint targetExterior;
|
|
if (ElkShapeBoundaries.IsGatewayShape(targetNode))
|
|
{
|
|
if (!ElkShapeBoundaries.TryProjectGatewayBoundarySlot(targetNode, targetSide, bandY, out targetBoundary))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
targetBoundary = ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(targetNode, targetBoundary, targetAnchor);
|
|
targetExterior = ElkShapeBoundaries.BuildGatewayDirectionalExteriorPoint(targetNode, targetBoundary, targetAnchor);
|
|
}
|
|
else
|
|
{
|
|
targetBoundary = BuildRectBoundaryPointForSide(targetNode, targetSide, targetAnchor);
|
|
targetExterior = new ElkPoint
|
|
{
|
|
X = targetSide == "left"
|
|
? targetBoundary.X - clearance
|
|
: targetBoundary.X + clearance,
|
|
Y = targetBoundary.Y,
|
|
};
|
|
}
|
|
|
|
var bandEntry = new ElkPoint { X = targetExterior.X, Y = bandY };
|
|
var startBoundary = path[0];
|
|
var sourceExterior = ElkShapeBoundaries.BuildPreferredGatewaySourceExteriorPoint(sourceNode, startBoundary, bandEntry);
|
|
var canReuseCurrentBoundary = CanReuseGatewayBoundaryForBandRoute(
|
|
sourceNode,
|
|
startBoundary,
|
|
sourceExterior,
|
|
bandEntry);
|
|
if (!canReuseCurrentBoundary)
|
|
{
|
|
if (!TryResolvePreferredGatewaySourceBoundary(sourceNode, bandEntry, targetBoundary, out startBoundary))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
sourceExterior = ElkShapeBoundaries.BuildPreferredGatewaySourceExteriorPoint(sourceNode, startBoundary, bandEntry);
|
|
if (ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(sourceNode, sourceExterior)
|
|
|| !ElkShapeBoundaries.HasValidGatewayBoundaryAngle(sourceNode, startBoundary, sourceExterior))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
var route = new List<ElkPoint> { startBoundary };
|
|
if (!ElkEdgeRoutingGeometry.PointsEqual(route[^1], sourceExterior))
|
|
{
|
|
route.Add(sourceExterior);
|
|
}
|
|
|
|
if (Math.Abs(route[^1].Y - bandY) > 0.5d)
|
|
{
|
|
route.Add(new ElkPoint { X = route[^1].X, Y = bandY });
|
|
}
|
|
|
|
if (Math.Abs(route[^1].X - targetExterior.X) > 0.5d)
|
|
{
|
|
route.Add(new ElkPoint { X = targetExterior.X, Y = bandY });
|
|
}
|
|
|
|
if (Math.Abs(route[^1].Y - targetExterior.Y) > 0.5d)
|
|
{
|
|
route.Add(new ElkPoint { X = targetExterior.X, Y = targetExterior.Y });
|
|
}
|
|
|
|
if (!ElkEdgeRoutingGeometry.PointsEqual(route[^1], targetExterior))
|
|
{
|
|
route.Add(targetExterior);
|
|
}
|
|
|
|
route.Add(targetBoundary);
|
|
candidate = NormalizePathPoints(route);
|
|
if (candidate.Count < 2
|
|
|| HasNodeObstacleCrossing(candidate, nodes, sourceNodeId, targetNodeId)
|
|
|| !HasAcceptableGatewayBoundaryPath(candidate, nodes, sourceNodeId, targetNodeId, sourceNode, fromStart: true)
|
|
|| !HasCleanGatewaySourceBandPath(candidate, sourceNode)
|
|
|| HasTargetApproachBacktracking(candidate, targetNode)
|
|
|| (ElkShapeBoundaries.IsGatewayShape(targetNode)
|
|
? !HasAcceptableGatewayBoundaryPath(candidate, nodes, sourceNodeId, targetNodeId, targetNode, fromStart: false)
|
|
: !HasValidBoundaryAngle(candidate[^1], candidate[^2], targetNode))
|
|
|| CountUnderNodeSegments(candidate, nodes, sourceNodeId, targetNodeId, minClearance) >= currentUnderNodeSegments)
|
|
{
|
|
candidate = [];
|
|
return false;
|
|
}
|
|
|
|
if (ComputePathLength(candidate) > ComputePathLength(path) + 220d)
|
|
{
|
|
candidate = [];
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private static bool TryBuildSafeGatewaySourceBandCandidate(
|
|
ElkPositionedNode sourceNode,
|
|
ElkPositionedNode targetNode,
|
|
IReadOnlyCollection<ElkPositionedNode> nodes,
|
|
string? sourceNodeId,
|
|
string? targetNodeId,
|
|
ElkPoint endBoundary,
|
|
double minClearance,
|
|
out List<ElkPoint> candidate)
|
|
{
|
|
candidate = [];
|
|
if (!ElkShapeBoundaries.IsGatewayShape(sourceNode))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var clearance = Math.Max(24d, minClearance * 0.6d);
|
|
var graphMinY = nodes.Min(node => node.Y);
|
|
var minX = Math.Min(sourceNode.X, endBoundary.X);
|
|
var maxX = Math.Max(sourceNode.X + sourceNode.Width, endBoundary.X);
|
|
var blockers = nodes
|
|
.Where(node =>
|
|
!string.Equals(node.Id, sourceNodeId, StringComparison.Ordinal)
|
|
&& !string.Equals(node.Id, targetNodeId, StringComparison.Ordinal)
|
|
&& maxX > node.X + 0.5d
|
|
&& minX < node.X + node.Width - 0.5d
|
|
&& node.Y <= Math.Max(sourceNode.Y + sourceNode.Height, endBoundary.Y) + clearance)
|
|
.ToArray();
|
|
|
|
var baseY = Math.Min(targetNode.Y, sourceNode.Y);
|
|
if (blockers.Length > 0)
|
|
{
|
|
baseY = Math.Min(baseY, blockers.Min(node => node.Y));
|
|
}
|
|
|
|
var bandY = Math.Max(graphMinY - 72d, baseY - clearance);
|
|
var continuationPoint = new ElkPoint { X = endBoundary.X, Y = bandY };
|
|
if (!TryResolvePreferredGatewaySourceBoundary(sourceNode, continuationPoint, endBoundary, out var startBoundary))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var sourceExterior = ElkShapeBoundaries.BuildPreferredGatewaySourceExteriorPoint(sourceNode, startBoundary, continuationPoint);
|
|
if (ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(sourceNode, sourceExterior)
|
|
|| !ElkShapeBoundaries.HasValidGatewayBoundaryAngle(sourceNode, startBoundary, sourceExterior))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (bandY >= Math.Min(sourceExterior.Y, endBoundary.Y) - 0.5d)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var route = new List<ElkPoint>
|
|
{
|
|
new() { X = startBoundary.X, Y = startBoundary.Y },
|
|
new() { X = sourceExterior.X, Y = sourceExterior.Y },
|
|
};
|
|
|
|
if (Math.Abs(route[^1].Y - bandY) > 0.5d)
|
|
{
|
|
route.Add(new ElkPoint { X = route[^1].X, Y = bandY });
|
|
}
|
|
|
|
if (Math.Abs(route[^1].X - endBoundary.X) > 0.5d)
|
|
{
|
|
route.Add(new ElkPoint { X = endBoundary.X, Y = bandY });
|
|
}
|
|
|
|
if (Math.Abs(route[^1].Y - endBoundary.Y) > 0.5d)
|
|
{
|
|
route.Add(new ElkPoint { X = endBoundary.X, Y = endBoundary.Y });
|
|
}
|
|
|
|
candidate = NormalizePathPoints(route);
|
|
if (candidate.Count < 2 || HasNodeObstacleCrossing(candidate, nodes, sourceNodeId, targetNodeId))
|
|
{
|
|
candidate = [];
|
|
return false;
|
|
}
|
|
|
|
if (!HasAcceptableGatewayBoundaryPath(candidate, nodes, sourceNodeId, targetNodeId, sourceNode, fromStart: true))
|
|
{
|
|
candidate = [];
|
|
return false;
|
|
}
|
|
|
|
if (!HasCleanGatewaySourceBandPath(candidate, sourceNode))
|
|
{
|
|
candidate = [];
|
|
return false;
|
|
}
|
|
|
|
if (ElkShapeBoundaries.IsGatewayShape(targetNode))
|
|
{
|
|
if (!CanAcceptGatewayTargetRepair(candidate, targetNode)
|
|
|| !HasAcceptableGatewayBoundaryPath(candidate, nodes, sourceNodeId, targetNodeId, targetNode, fromStart: false))
|
|
{
|
|
candidate = [];
|
|
return false;
|
|
}
|
|
}
|
|
else if (HasTargetApproachBacktracking(candidate, targetNode)
|
|
|| !HasValidBoundaryAngle(candidate[^1], candidate[^2], targetNode))
|
|
{
|
|
candidate = [];
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private static bool CanReuseGatewayBoundaryForBandRoute(
|
|
ElkPositionedNode sourceNode,
|
|
ElkPoint startBoundary,
|
|
ElkPoint sourceExterior,
|
|
ElkPoint bandEntry)
|
|
{
|
|
if (!ElkShapeBoundaries.IsGatewayShape(sourceNode)
|
|
|| !ElkShapeBoundaries.IsGatewayBoundaryPoint(sourceNode, startBoundary)
|
|
|| ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(sourceNode, sourceExterior)
|
|
|| !ElkShapeBoundaries.HasValidGatewayBoundaryAngle(sourceNode, startBoundary, sourceExterior))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var prefix = new List<ElkPoint> { startBoundary };
|
|
if (!ElkEdgeRoutingGeometry.PointsEqual(prefix[^1], sourceExterior))
|
|
{
|
|
prefix.Add(sourceExterior);
|
|
}
|
|
|
|
if (Math.Abs(prefix[^1].X - bandEntry.X) > 0.5d)
|
|
{
|
|
prefix.Add(new ElkPoint { X = bandEntry.X, Y = prefix[^1].Y });
|
|
}
|
|
|
|
if (Math.Abs(prefix[^1].Y - bandEntry.Y) > 0.5d)
|
|
{
|
|
prefix.Add(new ElkPoint { X = bandEntry.X, Y = bandEntry.Y });
|
|
}
|
|
|
|
prefix = NormalizePathPoints(prefix);
|
|
return HasCleanGatewaySourceBandPath(prefix, sourceNode);
|
|
}
|
|
|
|
private static bool HasCleanGatewaySourceBandPath(
|
|
IReadOnlyList<ElkPoint> path,
|
|
ElkPositionedNode sourceNode)
|
|
{
|
|
var prefix = ExtractGatewaySourceBandPrefix(path);
|
|
return !HasGatewaySourceExitBacktracking(prefix)
|
|
&& !HasGatewaySourceExitCurl(prefix)
|
|
&& !HasGatewaySourceDominantAxisDetour(prefix, sourceNode);
|
|
}
|
|
|
|
private static IReadOnlyList<ElkPoint> ExtractGatewaySourceBandPrefix(IReadOnlyList<ElkPoint> path)
|
|
{
|
|
if (path.Count < 4)
|
|
{
|
|
return path;
|
|
}
|
|
|
|
var bandY = path.Min(point => point.Y);
|
|
var bandIndex = -1;
|
|
for (var i = 1; i < path.Count; i++)
|
|
{
|
|
if (Math.Abs(path[i].Y - bandY) <= 0.5d)
|
|
{
|
|
bandIndex = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (bandIndex < 1)
|
|
{
|
|
bandIndex = Math.Min(path.Count - 1, 3);
|
|
}
|
|
|
|
return NormalizePathPoints(
|
|
path.Take(bandIndex + 1)
|
|
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
|
.ToList());
|
|
}
|
|
|
|
private static bool TryApplyPreferredBoundaryShortcut(
|
|
IReadOnlyList<ElkPoint> path,
|
|
ElkPositionedNode sourceNode,
|
|
ElkPositionedNode targetNode,
|
|
IReadOnlyCollection<ElkPositionedNode> nodes,
|
|
string? sourceNodeId,
|
|
string? targetNodeId,
|
|
bool requireUnderNodeImprovement,
|
|
double minClearance,
|
|
out List<ElkPoint> repairedPath)
|
|
{
|
|
repairedPath = path
|
|
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
|
.ToList();
|
|
if (!TryBuildPreferredBoundaryShortcutPath(
|
|
sourceNode,
|
|
targetNode,
|
|
nodes,
|
|
sourceNodeId,
|
|
targetNodeId,
|
|
out var shortcut))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (HasNodeObstacleCrossing(shortcut, nodes, sourceNodeId, targetNodeId))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (ElkShapeBoundaries.IsGatewayShape(sourceNode))
|
|
{
|
|
if (!HasAcceptableGatewayBoundaryPath(shortcut, nodes, sourceNodeId, targetNodeId, sourceNode, fromStart: true))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
else if (shortcut.Count < 2 || !HasValidBoundaryAngle(shortcut[0], shortcut[1], sourceNode))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (ElkShapeBoundaries.IsGatewayShape(targetNode))
|
|
{
|
|
if (!CanAcceptGatewayTargetRepair(shortcut, targetNode)
|
|
|| !HasAcceptableGatewayBoundaryPath(shortcut, nodes, sourceNodeId, targetNodeId, targetNode, fromStart: false))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
else if (shortcut.Count < 2
|
|
|| HasTargetApproachBacktracking(shortcut, targetNode)
|
|
|| !HasValidBoundaryAngle(shortcut[^1], shortcut[^2], targetNode))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var currentUnderNodeSegments = CountUnderNodeSegments(path, nodes, sourceNodeId, targetNodeId, minClearance);
|
|
var shortcutUnderNodeSegments = CountUnderNodeSegments(shortcut, nodes, sourceNodeId, targetNodeId, minClearance);
|
|
if (requireUnderNodeImprovement && shortcutUnderNodeSegments >= currentUnderNodeSegments)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var currentLength = ComputePathLength(path);
|
|
var shortcutLength = ComputePathLength(shortcut);
|
|
var boundaryInvalid = ElkShapeBoundaries.IsGatewayShape(targetNode)
|
|
? NeedsGatewayTargetBoundaryRepair(path, targetNode)
|
|
: path.Count >= 2 && !HasValidBoundaryAngle(path[^1], path[^2], targetNode);
|
|
var underNodeImproved = shortcutUnderNodeSegments < currentUnderNodeSegments;
|
|
if (!underNodeImproved
|
|
&& !boundaryInvalid
|
|
&& shortcutLength > currentLength - 8d)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
repairedPath = shortcut;
|
|
return true;
|
|
}
|
|
|
|
private static int CountUnderNodeSegments(
|
|
IReadOnlyList<ElkPoint> path,
|
|
IReadOnlyCollection<ElkPositionedNode> nodes,
|
|
string? sourceNodeId,
|
|
string? targetNodeId,
|
|
double minClearance)
|
|
{
|
|
var count = 0;
|
|
for (var i = 0; i < path.Count - 1; i++)
|
|
{
|
|
if (TryResolveUnderNodeBlockers(
|
|
path[i],
|
|
path[i + 1],
|
|
nodes,
|
|
sourceNodeId,
|
|
targetNodeId,
|
|
minClearance,
|
|
out _))
|
|
{
|
|
count++;
|
|
}
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
private static bool TryResolveUnderNodeBlockers(
|
|
ElkPoint start,
|
|
ElkPoint end,
|
|
IReadOnlyCollection<ElkPositionedNode> nodes,
|
|
string? sourceNodeId,
|
|
string? targetNodeId,
|
|
double minClearance,
|
|
out ElkPositionedNode[] blockers)
|
|
{
|
|
blockers = [];
|
|
if (Math.Abs(start.Y - end.Y) > 2d)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var minX = Math.Min(start.X, end.X);
|
|
var maxX = Math.Max(start.X, end.X);
|
|
blockers = nodes
|
|
.Where(node =>
|
|
!string.Equals(node.Id, sourceNodeId, StringComparison.Ordinal)
|
|
&& !string.Equals(node.Id, targetNodeId, StringComparison.Ordinal)
|
|
&& maxX > node.X + 0.5d
|
|
&& minX < node.X + node.Width - 0.5d)
|
|
.Where(node =>
|
|
{
|
|
var distanceBelowNode = start.Y - (node.Y + node.Height);
|
|
return distanceBelowNode > 0.5d && distanceBelowNode < minClearance;
|
|
})
|
|
.ToArray();
|
|
|
|
return blockers.Length > 0;
|
|
}
|
|
|
|
private static IReadOnlyList<(ElkPoint Start, ElkPoint End)> FlattenSegmentsNearStart(
|
|
IReadOnlyList<ElkPoint> path,
|
|
int maxSegmentsFromStart)
|
|
{
|
|
if (path.Count < 2 || maxSegmentsFromStart <= 0)
|
|
{
|
|
return [];
|
|
}
|
|
|
|
var segments = new List<(ElkPoint Start, ElkPoint End)>(Math.Min(path.Count - 1, maxSegmentsFromStart));
|
|
var segmentCount = Math.Min(path.Count - 1, maxSegmentsFromStart);
|
|
for (var i = 0; i < segmentCount; i++)
|
|
{
|
|
segments.Add((path[i], path[i + 1]));
|
|
}
|
|
|
|
return segments;
|
|
}
|
|
|
|
private static bool IsOrthogonal(ElkPoint start, ElkPoint end)
|
|
{
|
|
return Math.Abs(start.X - end.X) <= 0.5d
|
|
|| Math.Abs(start.Y - end.Y) <= 0.5d;
|
|
}
|
|
|
|
private static bool ShouldSpreadTargetApproach(
|
|
ElkRoutedEdge edge,
|
|
double graphMinY,
|
|
double graphMaxY)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(edge.SourcePortId) || !string.IsNullOrWhiteSpace(edge.TargetPortId))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(edge.Kind)
|
|
&& edge.Kind.StartsWith("backward|", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (IsRepeatCollectorLabel(edge.Label))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (HasProtectedUnderNodeGeometry(edge))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (HasCorridorBendPoints(edge, graphMinY, graphMaxY))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private static bool ShouldSpreadSourceDeparture(
|
|
ElkRoutedEdge edge,
|
|
double graphMinY,
|
|
double graphMaxY)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(edge.SourcePortId) || !string.IsNullOrWhiteSpace(edge.TargetPortId))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(edge.Kind)
|
|
&& edge.Kind.StartsWith("backward|", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (ShouldPreserveSourceExitGeometry(edge, graphMinY, graphMaxY))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private static bool HasClearBoundarySegments(
|
|
IReadOnlyList<ElkPoint> path,
|
|
IReadOnlyCollection<ElkPositionedNode> nodes,
|
|
string? sourceNodeId,
|
|
string? targetNodeId,
|
|
bool fromStart,
|
|
int segmentCount)
|
|
{
|
|
if (path.Count < 2)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
var obstacles = nodes.Select(node => (
|
|
Left: node.X,
|
|
Top: node.Y,
|
|
Right: node.X + node.Width,
|
|
Bottom: node.Y + node.Height,
|
|
Id: node.Id)).ToArray();
|
|
if (fromStart)
|
|
{
|
|
var maxIndex = Math.Min(path.Count - 1, segmentCount);
|
|
for (var i = 0; i < maxIndex; i++)
|
|
{
|
|
if (SegmentCrossesObstacle(path[i], path[i + 1], obstacles, sourceNodeId ?? string.Empty, targetNodeId ?? string.Empty))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
var startIndex = Math.Max(0, path.Count - 1 - segmentCount);
|
|
for (var i = startIndex; i < path.Count - 1; i++)
|
|
{
|
|
if (SegmentCrossesObstacle(path[i], path[i + 1], obstacles, sourceNodeId ?? string.Empty, targetNodeId ?? string.Empty))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private static bool HasValidBoundaryAngle(
|
|
ElkPoint boundaryPoint,
|
|
ElkPoint adjacentPoint,
|
|
ElkPositionedNode node)
|
|
{
|
|
if (ElkShapeBoundaries.IsGatewayShape(node))
|
|
{
|
|
return ElkShapeBoundaries.HasValidGatewayBoundaryAngle(node, boundaryPoint, adjacentPoint);
|
|
}
|
|
|
|
var segDx = Math.Abs(boundaryPoint.X - adjacentPoint.X);
|
|
var segDy = Math.Abs(boundaryPoint.Y - adjacentPoint.Y);
|
|
if (segDx < 3d && segDy < 3d)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
var side = ElkEdgeRoutingGeometry.ResolveBoundarySide(boundaryPoint, node);
|
|
var validForVerticalSide = segDx > segDy * 3d;
|
|
var validForHorizontalSide = segDy > segDx * 3d;
|
|
return side switch
|
|
{
|
|
"left" or "right" => validForVerticalSide,
|
|
"top" or "bottom" => validForHorizontalSide,
|
|
_ => true,
|
|
};
|
|
}
|
|
|
|
private static bool PathChanged(IReadOnlyList<ElkPoint> left, IReadOnlyList<ElkPoint> right)
|
|
{
|
|
return left.Count != right.Count
|
|
|| !left.Zip(right, ElkEdgeRoutingGeometry.PointsEqual).All(equal => equal);
|
|
}
|
|
|
|
private static bool HasAcceptableGatewayBoundaryPath(
|
|
IReadOnlyList<ElkPoint> path,
|
|
IReadOnlyCollection<ElkPositionedNode> nodes,
|
|
string? sourceNodeId,
|
|
string? targetNodeId,
|
|
ElkPositionedNode gatewayNode,
|
|
bool fromStart)
|
|
{
|
|
if (path.Count < 2)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var boundaryPoint = fromStart ? path[0] : path[^1];
|
|
var adjacentPoint = fromStart ? path[1] : path[^2];
|
|
if (!ElkShapeBoundaries.HasValidGatewayBoundaryAngle(gatewayNode, boundaryPoint, adjacentPoint))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return HasClearBoundarySegments(path, nodes, sourceNodeId, targetNodeId, fromStart, 1)
|
|
&& !HasExcessiveGatewayDiagonalLength(path, gatewayNode)
|
|
&& !HasNodeObstacleCrossing(path, nodes, sourceNodeId, targetNodeId);
|
|
}
|
|
|
|
private static bool HasNodeObstacleCrossing(
|
|
IReadOnlyList<ElkPoint> path,
|
|
IReadOnlyCollection<ElkPositionedNode> nodes,
|
|
string? sourceNodeId,
|
|
string? targetNodeId)
|
|
{
|
|
if (path.Count < 2)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var obstacles = nodes.Select(node => (
|
|
Left: node.X,
|
|
Top: node.Y,
|
|
Right: node.X + node.Width,
|
|
Bottom: node.Y + node.Height,
|
|
Id: node.Id)).ToArray();
|
|
for (var i = 0; i < path.Count - 1; i++)
|
|
{
|
|
if (SegmentCrossesObstacle(path[i], path[i + 1], obstacles, sourceNodeId ?? string.Empty, targetNodeId ?? string.Empty))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static bool CanAcceptGatewayTargetRepair(
|
|
IReadOnlyList<ElkPoint> path,
|
|
ElkPositionedNode targetNode)
|
|
{
|
|
return path.Count >= 2
|
|
&& !ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(targetNode, path[^2])
|
|
&& !HasExcessiveGatewayDiagonalLength(path, targetNode)
|
|
&& ElkShapeBoundaries.HasValidGatewayBoundaryAngle(targetNode, path[^1], path[^2]);
|
|
}
|
|
|
|
private static bool HasExcessiveGatewayDiagonalLength(
|
|
IReadOnlyList<ElkPoint> path,
|
|
ElkPositionedNode gatewayNode)
|
|
{
|
|
var maxDiagonalLength = Math.Max(96d, gatewayNode.Width + gatewayNode.Height);
|
|
for (var i = 0; i < path.Count - 1; i++)
|
|
{
|
|
var start = path[i];
|
|
var end = path[i + 1];
|
|
var dx = Math.Abs(end.X - start.X);
|
|
var dy = Math.Abs(end.Y - start.Y);
|
|
if (dx <= 3d || dy <= 3d)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (ElkEdgeRoutingGeometry.ComputeSegmentLength(start, end) > maxDiagonalLength)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static int FindFirstGatewayExteriorPointIndex(
|
|
IReadOnlyList<ElkPoint> path,
|
|
ElkPositionedNode node)
|
|
{
|
|
for (var i = 1; i < path.Count; i++)
|
|
{
|
|
if (!ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(node, path[i]))
|
|
{
|
|
return i;
|
|
}
|
|
}
|
|
|
|
return Math.Min(1, path.Count - 1);
|
|
}
|
|
|
|
private static int FindLastGatewayExteriorPointIndex(
|
|
IReadOnlyList<ElkPoint> path,
|
|
ElkPositionedNode node)
|
|
{
|
|
for (var i = path.Count - 2; i >= 0; i--)
|
|
{
|
|
if (!ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(node, path[i]))
|
|
{
|
|
return i;
|
|
}
|
|
}
|
|
|
|
return Math.Max(0, path.Count - 2);
|
|
}
|
|
|
|
private static List<ElkPoint> ForceGatewayExteriorTargetApproach(
|
|
IReadOnlyList<ElkPoint> sourcePath,
|
|
ElkPositionedNode targetNode,
|
|
ElkPoint boundaryPoint)
|
|
{
|
|
var path = sourcePath
|
|
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
|
.ToList();
|
|
if (path.Count < 2)
|
|
{
|
|
return path;
|
|
}
|
|
|
|
var exteriorIndex = FindLastGatewayExteriorPointIndex(path, targetNode);
|
|
var exteriorAnchor = path[exteriorIndex];
|
|
var boundary = ElkShapeBoundaries.IsGatewayBoundaryPoint(targetNode, boundaryPoint)
|
|
? boundaryPoint
|
|
: ElkShapeBoundaries.ProjectOntoShapeBoundary(targetNode, exteriorAnchor);
|
|
boundary = ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(targetNode, boundary, exteriorAnchor);
|
|
|
|
var candidates = ResolveForcedGatewayExteriorApproachCandidates(targetNode, boundary, exteriorAnchor).ToArray();
|
|
if (candidates.Length == 0)
|
|
{
|
|
return path;
|
|
}
|
|
|
|
var prefix = path.Take(exteriorIndex + 1)
|
|
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
|
.ToList();
|
|
if (prefix.Count == 0 || !ElkEdgeRoutingGeometry.PointsEqual(prefix[^1], exteriorAnchor))
|
|
{
|
|
prefix.Add(exteriorAnchor);
|
|
}
|
|
|
|
foreach (var candidate in candidates)
|
|
{
|
|
var rebuilt = prefix
|
|
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
|
.ToList();
|
|
AppendGatewayTargetOrthogonalCorner(
|
|
rebuilt,
|
|
rebuilt[^1],
|
|
candidate,
|
|
rebuilt.Count >= 2 ? rebuilt[^2] : null,
|
|
preferHorizontalFromReference: ShouldPreferHorizontalGatewayExit(rebuilt[^1], candidate),
|
|
targetNode);
|
|
if (!ElkEdgeRoutingGeometry.PointsEqual(rebuilt[^1], candidate))
|
|
{
|
|
rebuilt.Add(candidate);
|
|
}
|
|
|
|
rebuilt.Add(boundary);
|
|
var normalized = NormalizePathPoints(rebuilt);
|
|
if (normalized.Count < 2
|
|
|| ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(targetNode, normalized[^2])
|
|
|| !ElkShapeBoundaries.HasValidGatewayBoundaryAngle(targetNode, normalized[^1], normalized[^2]))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
return normalized;
|
|
}
|
|
|
|
return path;
|
|
}
|
|
|
|
private static IEnumerable<ElkPoint> ResolveForcedGatewayExteriorApproachCandidates(
|
|
ElkPositionedNode targetNode,
|
|
ElkPoint boundaryPoint,
|
|
ElkPoint exteriorAnchor)
|
|
{
|
|
const double padding = 8d;
|
|
var centerX = targetNode.X + (targetNode.Width / 2d);
|
|
var centerY = targetNode.Y + (targetNode.Height / 2d);
|
|
var candidates = new List<ElkPoint>();
|
|
|
|
AddUniquePoint(
|
|
candidates,
|
|
ElkShapeBoundaries.BuildGatewayDirectionalExteriorPoint(targetNode, boundaryPoint, exteriorAnchor, padding));
|
|
AddUniquePoint(
|
|
candidates,
|
|
ElkShapeBoundaries.BuildGatewayExteriorApproachPoint(targetNode, boundaryPoint, padding));
|
|
|
|
if (boundaryPoint.X <= centerX + 0.5d)
|
|
{
|
|
AddUniquePoint(
|
|
candidates,
|
|
new ElkPoint
|
|
{
|
|
X = targetNode.X - padding,
|
|
Y = boundaryPoint.Y,
|
|
});
|
|
}
|
|
|
|
if (boundaryPoint.X >= centerX - 0.5d)
|
|
{
|
|
AddUniquePoint(
|
|
candidates,
|
|
new ElkPoint
|
|
{
|
|
X = targetNode.X + targetNode.Width + padding,
|
|
Y = boundaryPoint.Y,
|
|
});
|
|
}
|
|
|
|
if (boundaryPoint.Y <= centerY + 0.5d)
|
|
{
|
|
AddUniquePoint(
|
|
candidates,
|
|
new ElkPoint
|
|
{
|
|
X = boundaryPoint.X,
|
|
Y = targetNode.Y - padding,
|
|
});
|
|
}
|
|
|
|
if (boundaryPoint.Y >= centerY - 0.5d)
|
|
{
|
|
AddUniquePoint(
|
|
candidates,
|
|
new ElkPoint
|
|
{
|
|
X = boundaryPoint.X,
|
|
Y = targetNode.Y + targetNode.Height + padding,
|
|
});
|
|
}
|
|
|
|
return candidates
|
|
.Where(candidate => !ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(targetNode, candidate)
|
|
&& ElkShapeBoundaries.HasValidGatewayBoundaryAngle(targetNode, boundaryPoint, candidate))
|
|
.OrderBy(candidate => ScoreForcedGatewayExteriorApproachCandidate(targetNode, boundaryPoint, candidate, exteriorAnchor))
|
|
.ToArray();
|
|
}
|
|
|
|
private static double ScoreForcedGatewayExteriorApproachCandidate(
|
|
ElkPositionedNode targetNode,
|
|
ElkPoint boundaryPoint,
|
|
ElkPoint candidate,
|
|
ElkPoint exteriorAnchor)
|
|
{
|
|
var score = Math.Abs(candidate.X - exteriorAnchor.X) + Math.Abs(candidate.Y - exteriorAnchor.Y);
|
|
score += (Math.Abs(candidate.X - boundaryPoint.X) + Math.Abs(candidate.Y - boundaryPoint.Y)) * 0.25d;
|
|
|
|
var desiredDx = boundaryPoint.X - exteriorAnchor.X;
|
|
var desiredDy = boundaryPoint.Y - exteriorAnchor.Y;
|
|
var approachDx = candidate.X - boundaryPoint.X;
|
|
var approachDy = candidate.Y - boundaryPoint.Y;
|
|
|
|
if (Math.Abs(desiredDx) >= Math.Abs(desiredDy) * 1.15d
|
|
&& Math.Sign(approachDx) != 0
|
|
&& Math.Sign(approachDx) != Math.Sign(exteriorAnchor.X - boundaryPoint.X))
|
|
{
|
|
score += 10_000d;
|
|
}
|
|
|
|
if (Math.Abs(desiredDy) >= Math.Abs(desiredDx) * 1.15d
|
|
&& Math.Sign(approachDy) != 0
|
|
&& Math.Sign(approachDy) != Math.Sign(exteriorAnchor.Y - boundaryPoint.Y))
|
|
{
|
|
score += 10_000d;
|
|
}
|
|
|
|
var preferredExterior = ElkShapeBoundaries.BuildGatewayExteriorApproachPoint(targetNode, boundaryPoint);
|
|
if (ElkEdgeRoutingGeometry.PointsEqual(candidate, preferredExterior))
|
|
{
|
|
score -= 8d;
|
|
}
|
|
|
|
return score;
|
|
}
|
|
|
|
private static bool NeedsGatewayDiagonalStub(ElkPoint start, ElkPoint end)
|
|
{
|
|
var deltaX = Math.Abs(end.X - start.X);
|
|
var deltaY = Math.Abs(end.Y - start.Y);
|
|
if (deltaX < 3d || deltaY < 3d)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var ratio = deltaX / Math.Max(deltaY, 0.001d);
|
|
return ratio < 0.55d || ratio > 1.85d;
|
|
}
|
|
|
|
private static bool ShouldUseGatewayDiagonalStub(
|
|
ElkPositionedNode node,
|
|
ElkPoint start,
|
|
ElkPoint end)
|
|
{
|
|
return !ElkShapeBoundaries.IsNearGatewayVertex(node, start)
|
|
&& !ElkShapeBoundaries.IsNearGatewayVertex(node, end)
|
|
&& NeedsGatewayDiagonalStub(start, end);
|
|
}
|
|
|
|
private static List<ElkPoint> BuildGatewayExitStubbedPath(
|
|
IReadOnlyList<ElkPoint> path,
|
|
ElkPoint boundary,
|
|
ElkPoint anchor)
|
|
{
|
|
var stub = BuildGatewayDiagonalStubPoint(boundary, anchor);
|
|
var rebuilt = new List<ElkPoint> { boundary, stub };
|
|
AppendGatewayOrthogonalCorner(
|
|
rebuilt,
|
|
stub,
|
|
anchor,
|
|
path.Count > 2 ? path[2] : null,
|
|
preferHorizontalFromReference: true);
|
|
rebuilt.Add(anchor);
|
|
rebuilt.AddRange(path.Skip(2));
|
|
return NormalizePathPoints(rebuilt);
|
|
}
|
|
|
|
private static List<ElkPoint> BuildGatewayEntryStubbedPath(
|
|
IReadOnlyList<ElkPoint> path,
|
|
ElkPoint anchor,
|
|
ElkPoint boundary)
|
|
{
|
|
var stub = BuildGatewayDiagonalStubPoint(boundary, anchor);
|
|
var rebuilt = path.Take(path.Count - 2).ToList();
|
|
if (rebuilt.Count == 0 || !ElkEdgeRoutingGeometry.PointsEqual(rebuilt[^1], anchor))
|
|
{
|
|
rebuilt.Add(anchor);
|
|
}
|
|
|
|
AppendGatewayOrthogonalCorner(
|
|
rebuilt,
|
|
anchor,
|
|
stub,
|
|
rebuilt.Count >= 2 ? rebuilt[^2] : null,
|
|
preferHorizontalFromReference: false);
|
|
rebuilt.Add(stub);
|
|
rebuilt.Add(boundary);
|
|
return NormalizePathPoints(rebuilt);
|
|
}
|
|
|
|
private static ElkPoint BuildGatewayDiagonalStubPoint(ElkPoint boundary, ElkPoint anchor)
|
|
{
|
|
var deltaX = Math.Abs(anchor.X - boundary.X);
|
|
var deltaY = Math.Abs(anchor.Y - boundary.Y);
|
|
var stubLength = Math.Min(24d, Math.Max(12d, Math.Min(deltaX, deltaY) * 0.5d));
|
|
return new ElkPoint
|
|
{
|
|
X = boundary.X + (Math.Sign(anchor.X - boundary.X) * stubLength),
|
|
Y = boundary.Y + (Math.Sign(anchor.Y - boundary.Y) * stubLength),
|
|
};
|
|
}
|
|
|
|
private static void AppendGatewayOrthogonalCorner(
|
|
IList<ElkPoint> points,
|
|
ElkPoint from,
|
|
ElkPoint to,
|
|
ElkPoint? referencePoint,
|
|
bool preferHorizontalFromReference)
|
|
{
|
|
const double coordinateTolerance = 0.5d;
|
|
if (Math.Abs(from.X - to.X) <= coordinateTolerance || Math.Abs(from.Y - to.Y) <= coordinateTolerance)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var cornerA = new ElkPoint { X = to.X, Y = from.Y };
|
|
var cornerB = new ElkPoint { X = from.X, Y = to.Y };
|
|
var scoreA = ScoreGatewayOrthogonalCorner(cornerA, from, to, referencePoint, preferHorizontalFromReference);
|
|
var scoreB = ScoreGatewayOrthogonalCorner(cornerB, from, to, referencePoint, !preferHorizontalFromReference);
|
|
points.Add(scoreA <= scoreB ? cornerA : cornerB);
|
|
}
|
|
|
|
private static void AppendGatewayTargetOrthogonalCorner(
|
|
IList<ElkPoint> points,
|
|
ElkPoint from,
|
|
ElkPoint to,
|
|
ElkPoint? referencePoint,
|
|
bool preferHorizontalFromReference,
|
|
ElkPositionedNode targetNode)
|
|
{
|
|
const double coordinateTolerance = 0.5d;
|
|
if (Math.Abs(from.X - to.X) <= coordinateTolerance || Math.Abs(from.Y - to.Y) <= coordinateTolerance)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var cornerA = new ElkPoint { X = to.X, Y = from.Y };
|
|
var cornerB = new ElkPoint { X = from.X, Y = to.Y };
|
|
var scoreA = ScoreGatewayOrthogonalCorner(cornerA, from, to, referencePoint, preferHorizontalFromReference);
|
|
var scoreB = ScoreGatewayOrthogonalCorner(cornerB, from, to, referencePoint, !preferHorizontalFromReference);
|
|
if (ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(targetNode, cornerA))
|
|
{
|
|
scoreA += 100_000d;
|
|
}
|
|
|
|
if (ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(targetNode, cornerB))
|
|
{
|
|
scoreB += 100_000d;
|
|
}
|
|
|
|
points.Add(scoreA <= scoreB ? cornerA : cornerB);
|
|
}
|
|
|
|
private static double ScoreGatewayOrthogonalCorner(
|
|
ElkPoint corner,
|
|
ElkPoint from,
|
|
ElkPoint to,
|
|
ElkPoint? referencePoint,
|
|
bool preferHorizontalFirst)
|
|
{
|
|
const double coordinateTolerance = 0.5d;
|
|
var score = preferHorizontalFirst ? 0d : 1d;
|
|
var totalDx = to.X - from.X;
|
|
var totalDy = to.Y - from.Y;
|
|
var firstDx = corner.X - from.X;
|
|
var firstDy = corner.Y - from.Y;
|
|
var secondDx = to.X - corner.X;
|
|
var secondDy = to.Y - corner.Y;
|
|
|
|
if (Math.Abs(firstDx) > coordinateTolerance && Math.Abs(totalDx) > coordinateTolerance && Math.Sign(firstDx) != Math.Sign(totalDx))
|
|
{
|
|
score += 50d;
|
|
}
|
|
|
|
if (Math.Abs(firstDy) > coordinateTolerance && Math.Abs(totalDy) > coordinateTolerance && Math.Sign(firstDy) != Math.Sign(totalDy))
|
|
{
|
|
score += 50d;
|
|
}
|
|
|
|
if (Math.Abs(secondDx) > coordinateTolerance && Math.Abs(totalDx) > coordinateTolerance && Math.Sign(secondDx) != Math.Sign(totalDx))
|
|
{
|
|
score += 25d;
|
|
}
|
|
|
|
if (Math.Abs(secondDy) > coordinateTolerance && Math.Abs(totalDy) > coordinateTolerance && Math.Sign(secondDy) != Math.Sign(totalDy))
|
|
{
|
|
score += 25d;
|
|
}
|
|
|
|
if (referencePoint is not null)
|
|
{
|
|
var reference = referencePoint;
|
|
score += (Math.Abs(corner.X - reference.X) + Math.Abs(corner.Y - reference.Y)) * 0.02d;
|
|
if (Math.Abs(reference.Y - from.Y) <= coordinateTolerance)
|
|
{
|
|
score -= Math.Abs(corner.Y - from.Y) <= coordinateTolerance ? 1d : 0d;
|
|
}
|
|
else if (Math.Abs(reference.X - from.X) <= coordinateTolerance)
|
|
{
|
|
score -= Math.Abs(corner.X - from.X) <= coordinateTolerance ? 1d : 0d;
|
|
}
|
|
}
|
|
|
|
return score;
|
|
}
|
|
|
|
private static List<ElkPoint> ExtractFullPath(ElkRoutedEdge edge)
|
|
{
|
|
var path = new List<ElkPoint>();
|
|
foreach (var section in edge.Sections)
|
|
{
|
|
if (path.Count == 0)
|
|
{
|
|
path.Add(section.StartPoint);
|
|
}
|
|
|
|
path.AddRange(section.BendPoints);
|
|
path.Add(section.EndPoint);
|
|
}
|
|
|
|
return path;
|
|
}
|
|
|
|
private static double ComputePathLength(IReadOnlyList<ElkPoint> points)
|
|
{
|
|
var length = 0d;
|
|
for (var i = 1; i < points.Count; i++)
|
|
{
|
|
length += ElkEdgeRoutingGeometry.ComputeSegmentLength(points[i - 1], points[i]);
|
|
}
|
|
|
|
return length;
|
|
}
|
|
|
|
private static List<ElkPoint>? TryBuildLocalObstacleSkirtBoundaryShortcut(
|
|
IReadOnlyList<ElkPoint> currentPath,
|
|
ElkPoint start,
|
|
ElkPoint end,
|
|
IReadOnlyCollection<ElkPositionedNode> nodes,
|
|
string? sourceNodeId,
|
|
string? targetNodeId,
|
|
ElkPositionedNode? targetNode,
|
|
double obstaclePadding)
|
|
{
|
|
var rawObstacles = nodes.Select(node => (
|
|
Left: node.X,
|
|
Top: node.Y,
|
|
Right: node.X + node.Width,
|
|
Bottom: node.Y + node.Height,
|
|
Id: node.Id)).ToArray();
|
|
var sourceId = sourceNodeId ?? string.Empty;
|
|
var targetId = targetNodeId ?? string.Empty;
|
|
var sourceNode = nodes.FirstOrDefault(node => string.Equals(node.Id, sourceId, StringComparison.Ordinal));
|
|
var minLineClearance = ResolveMinLineClearance(nodes);
|
|
List<ElkPoint>? bestPath = null;
|
|
var bestScore = double.MaxValue;
|
|
|
|
static (double Left, double Top, double Right, double Bottom, string Id)[] ExpandObstacles(
|
|
IReadOnlyList<(double Left, double Top, double Right, double Bottom, string Id)> obstacles,
|
|
double clearance)
|
|
{
|
|
return obstacles
|
|
.Select(obstacle => (
|
|
Left: obstacle.Left - clearance,
|
|
Top: obstacle.Top - clearance,
|
|
Right: obstacle.Right + clearance,
|
|
Bottom: obstacle.Bottom + clearance,
|
|
obstacle.Id))
|
|
.ToArray();
|
|
}
|
|
|
|
var candidateClearances = new List<double>();
|
|
AddUniqueCoordinate(candidateClearances, Math.Max(0d, obstaclePadding));
|
|
AddUniqueCoordinate(candidateClearances, Math.Min(Math.Max(0d, obstaclePadding), 24d));
|
|
AddUniqueCoordinate(candidateClearances, 8d);
|
|
candidateClearances.Sort((left, right) => right.CompareTo(left));
|
|
|
|
void ConsiderCandidate(
|
|
IReadOnlyList<ElkPoint> rawCandidate,
|
|
IReadOnlyList<(double Left, double Top, double Right, double Bottom, string Id)> obstacles)
|
|
{
|
|
var candidate = NormalizePathPoints(rawCandidate);
|
|
if (candidate.Count < 2)
|
|
{
|
|
return;
|
|
}
|
|
|
|
for (var i = 1; i < candidate.Count; i++)
|
|
{
|
|
if (SegmentCrossesObstacle(candidate[i - 1], candidate[i], obstacles.ToArray(), sourceNodeId, targetNodeId))
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (sourceNode is not null)
|
|
{
|
|
if (ElkShapeBoundaries.IsGatewayShape(sourceNode)
|
|
&& !HasAcceptableGatewayBoundaryPath(
|
|
candidate,
|
|
nodes,
|
|
sourceNodeId,
|
|
targetNodeId,
|
|
sourceNode,
|
|
fromStart: true))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!ElkShapeBoundaries.IsGatewayShape(sourceNode)
|
|
&& !HasValidBoundaryAngle(candidate[0], candidate[1], sourceNode))
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (targetNode is not null
|
|
&& !ElkShapeBoundaries.IsGatewayShape(targetNode)
|
|
&& HasTargetApproachBacktracking(candidate, targetNode))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (targetNode is not null
|
|
&& ElkShapeBoundaries.IsGatewayShape(targetNode)
|
|
&& (!CanAcceptGatewayTargetRepair(candidate, targetNode)
|
|
|| !HasAcceptableGatewayBoundaryPath(
|
|
candidate,
|
|
nodes,
|
|
sourceNodeId,
|
|
targetNodeId,
|
|
targetNode,
|
|
fromStart: false)))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var underNodeSegments = CountUnderNodeSegments(
|
|
candidate,
|
|
nodes,
|
|
sourceNodeId,
|
|
targetNodeId,
|
|
minLineClearance);
|
|
var score =
|
|
(underNodeSegments * 100_000d)
|
|
+ ComputePathLength(candidate)
|
|
+ (Math.Max(0, candidate.Count - 2) * 4d);
|
|
if (score >= bestScore - 0.5d)
|
|
{
|
|
return;
|
|
}
|
|
|
|
bestScore = score;
|
|
bestPath = candidate;
|
|
}
|
|
|
|
static bool IsUsableForwardBridgeAxis(double startAxis, double endAxis, double candidateAxis)
|
|
{
|
|
const double tolerance = 0.5d;
|
|
var desiredDelta = endAxis - startAxis;
|
|
var candidateDelta = candidateAxis - startAxis;
|
|
if (Math.Abs(desiredDelta) <= tolerance
|
|
|| Math.Abs(candidateDelta) <= tolerance
|
|
|| Math.Sign(candidateDelta) != Math.Sign(desiredDelta))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var minAxis = Math.Min(startAxis, endAxis) + tolerance;
|
|
var maxAxis = Math.Max(startAxis, endAxis) - tolerance;
|
|
return candidateAxis >= minAxis && candidateAxis <= maxAxis;
|
|
}
|
|
|
|
static void AddForwardBridgeAxisCandidates(List<double> axes, double startAxis, double endAxis)
|
|
{
|
|
var desiredDelta = endAxis - startAxis;
|
|
if (Math.Abs(desiredDelta) <= 1d)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var midpoint = startAxis + (desiredDelta / 2d);
|
|
if (IsUsableForwardBridgeAxis(startAxis, endAxis, midpoint))
|
|
{
|
|
AddUniqueCoordinate(axes, midpoint);
|
|
}
|
|
|
|
var forwardStep = startAxis + (Math.Sign(desiredDelta) * Math.Min(48d, Math.Abs(desiredDelta) / 2d));
|
|
if (IsUsableForwardBridgeAxis(startAxis, endAxis, forwardStep))
|
|
{
|
|
AddUniqueCoordinate(axes, forwardStep);
|
|
}
|
|
}
|
|
|
|
var horizontalDominant = Math.Abs(end.X - start.X) >= Math.Abs(end.Y - start.Y);
|
|
var startAxis = horizontalDominant ? start.X : start.Y;
|
|
var endAxis = horizontalDominant ? end.X : end.Y;
|
|
var sourceBridgeAxes = new List<double>();
|
|
AddUniqueCoordinate(sourceBridgeAxes, startAxis);
|
|
if (currentPath.Count >= 2 && !ElkEdgeRoutingGeometry.PointsEqual(currentPath[1], start))
|
|
{
|
|
var currentBridgeAxis = horizontalDominant ? currentPath[1].X : currentPath[1].Y;
|
|
if (IsUsableForwardBridgeAxis(startAxis, endAxis, currentBridgeAxis))
|
|
{
|
|
AddUniqueCoordinate(sourceBridgeAxes, currentBridgeAxis);
|
|
}
|
|
}
|
|
AddForwardBridgeAxisCandidates(sourceBridgeAxes, startAxis, endAxis);
|
|
var targetBridgeAxis = horizontalDominant ? end.X : end.Y;
|
|
ElkPoint? preservedGatewayTargetApproach = null;
|
|
if (targetNode is not null && ElkShapeBoundaries.IsGatewayShape(targetNode))
|
|
{
|
|
if (currentPath.Count >= 2
|
|
&& !ElkEdgeRoutingGeometry.PointsEqual(currentPath[^2], end)
|
|
&& ElkShapeBoundaries.HasValidGatewayBoundaryAngle(targetNode, end, currentPath[^2]))
|
|
{
|
|
preservedGatewayTargetApproach = currentPath[^2];
|
|
targetBridgeAxis = horizontalDominant ? currentPath[^2].X : currentPath[^2].Y;
|
|
}
|
|
else
|
|
{
|
|
var targetExterior = ElkShapeBoundaries.BuildGatewayDirectionalExteriorPoint(targetNode, end, start);
|
|
if (!ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(targetNode, targetExterior)
|
|
&& ElkShapeBoundaries.HasValidGatewayBoundaryAngle(targetNode, end, targetExterior))
|
|
{
|
|
targetBridgeAxis = horizontalDominant ? targetExterior.X : targetExterior.Y;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (horizontalDominant)
|
|
{
|
|
foreach (var clearance in candidateClearances)
|
|
{
|
|
var obstacles = ExpandObstacles(rawObstacles, clearance);
|
|
var minX = Math.Min(start.X, end.X) + 0.5d;
|
|
var maxX = Math.Max(start.X, end.X) - 0.5d;
|
|
var corridorTop = Math.Min(start.Y, end.Y) - clearance;
|
|
var corridorBottom = Math.Max(start.Y, end.Y) + clearance;
|
|
var bypassYCandidates = new List<double> { start.Y, end.Y };
|
|
var cornerBridgeXCandidates = new List<double>();
|
|
|
|
foreach (var obstacle in obstacles)
|
|
{
|
|
if (string.Equals(obstacle.Id, sourceId, StringComparison.Ordinal)
|
|
|| string.Equals(obstacle.Id, targetId, StringComparison.Ordinal)
|
|
|| obstacle.Right <= minX
|
|
|| obstacle.Left >= maxX
|
|
|| obstacle.Bottom <= corridorTop
|
|
|| obstacle.Top >= corridorBottom)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
AddUniqueCoordinate(bypassYCandidates, obstacle.Top);
|
|
AddUniqueCoordinate(bypassYCandidates, obstacle.Bottom);
|
|
if (IsUsableForwardBridgeAxis(start.X, end.X, obstacle.Left))
|
|
{
|
|
AddUniqueCoordinate(cornerBridgeXCandidates, obstacle.Left);
|
|
}
|
|
|
|
if (IsUsableForwardBridgeAxis(start.X, end.X, obstacle.Right))
|
|
{
|
|
AddUniqueCoordinate(cornerBridgeXCandidates, obstacle.Right);
|
|
}
|
|
}
|
|
|
|
foreach (var bypassY in bypassYCandidates)
|
|
{
|
|
foreach (var sourceBridgeAxis in sourceBridgeAxes)
|
|
{
|
|
ConsiderCandidate(
|
|
[
|
|
start,
|
|
new ElkPoint { X = sourceBridgeAxis, Y = start.Y },
|
|
new ElkPoint { X = sourceBridgeAxis, Y = bypassY },
|
|
end,
|
|
],
|
|
obstacles);
|
|
|
|
foreach (var cornerBridgeX in cornerBridgeXCandidates)
|
|
{
|
|
if (!IsUsableForwardBridgeAxis(sourceBridgeAxis, end.X, cornerBridgeX))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
ConsiderCandidate(
|
|
[
|
|
start,
|
|
new ElkPoint { X = sourceBridgeAxis, Y = start.Y },
|
|
new ElkPoint { X = sourceBridgeAxis, Y = bypassY },
|
|
new ElkPoint { X = cornerBridgeX, Y = bypassY },
|
|
end,
|
|
],
|
|
obstacles);
|
|
}
|
|
|
|
if (targetNode is not null
|
|
&& ElkShapeBoundaries.IsGatewayShape(targetNode)
|
|
&& Math.Abs(targetBridgeAxis - end.X) > 0.5d)
|
|
{
|
|
ConsiderCandidate(
|
|
[
|
|
start,
|
|
new ElkPoint { X = sourceBridgeAxis, Y = start.Y },
|
|
new ElkPoint { X = sourceBridgeAxis, Y = bypassY },
|
|
new ElkPoint { X = targetBridgeAxis, Y = bypassY },
|
|
end,
|
|
],
|
|
obstacles);
|
|
}
|
|
|
|
if (preservedGatewayTargetApproach is not null
|
|
&& !ElkEdgeRoutingGeometry.PointsEqual(
|
|
new ElkPoint { X = preservedGatewayTargetApproach.X, Y = bypassY },
|
|
preservedGatewayTargetApproach))
|
|
{
|
|
foreach (var cornerBridgeX in cornerBridgeXCandidates)
|
|
{
|
|
if (!IsUsableForwardBridgeAxis(sourceBridgeAxis, preservedGatewayTargetApproach.X, cornerBridgeX))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
ConsiderCandidate(
|
|
[
|
|
start,
|
|
new ElkPoint { X = sourceBridgeAxis, Y = start.Y },
|
|
new ElkPoint { X = sourceBridgeAxis, Y = bypassY },
|
|
new ElkPoint { X = cornerBridgeX, Y = bypassY },
|
|
preservedGatewayTargetApproach,
|
|
end,
|
|
],
|
|
obstacles);
|
|
}
|
|
|
|
ConsiderCandidate(
|
|
[
|
|
start,
|
|
new ElkPoint { X = sourceBridgeAxis, Y = start.Y },
|
|
new ElkPoint { X = sourceBridgeAxis, Y = bypassY },
|
|
new ElkPoint { X = preservedGatewayTargetApproach.X, Y = bypassY },
|
|
preservedGatewayTargetApproach,
|
|
end,
|
|
],
|
|
obstacles);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
foreach (var clearance in candidateClearances)
|
|
{
|
|
var obstacles = ExpandObstacles(rawObstacles, clearance);
|
|
var minY = Math.Min(start.Y, end.Y) + 0.5d;
|
|
var maxY = Math.Max(start.Y, end.Y) - 0.5d;
|
|
var corridorLeft = Math.Min(start.X, end.X) - clearance;
|
|
var corridorRight = Math.Max(start.X, end.X) + clearance;
|
|
var bypassXCandidates = new List<double> { start.X, end.X };
|
|
|
|
foreach (var obstacle in obstacles)
|
|
{
|
|
if (string.Equals(obstacle.Id, sourceId, StringComparison.Ordinal)
|
|
|| string.Equals(obstacle.Id, targetId, StringComparison.Ordinal)
|
|
|| obstacle.Bottom <= minY
|
|
|| obstacle.Top >= maxY
|
|
|| obstacle.Right <= corridorLeft
|
|
|| obstacle.Left >= corridorRight)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
AddUniqueCoordinate(bypassXCandidates, obstacle.Left);
|
|
AddUniqueCoordinate(bypassXCandidates, obstacle.Right);
|
|
}
|
|
|
|
foreach (var bypassX in bypassXCandidates)
|
|
{
|
|
foreach (var sourceBridgeAxis in sourceBridgeAxes)
|
|
{
|
|
ConsiderCandidate(
|
|
[
|
|
start,
|
|
new ElkPoint { X = start.X, Y = sourceBridgeAxis },
|
|
new ElkPoint { X = bypassX, Y = sourceBridgeAxis },
|
|
end,
|
|
],
|
|
obstacles);
|
|
|
|
if (targetNode is not null
|
|
&& ElkShapeBoundaries.IsGatewayShape(targetNode)
|
|
&& Math.Abs(targetBridgeAxis - end.Y) > 0.5d)
|
|
{
|
|
ConsiderCandidate(
|
|
[
|
|
start,
|
|
new ElkPoint { X = start.X, Y = sourceBridgeAxis },
|
|
new ElkPoint { X = bypassX, Y = sourceBridgeAxis },
|
|
new ElkPoint { X = bypassX, Y = targetBridgeAxis },
|
|
end,
|
|
],
|
|
obstacles);
|
|
}
|
|
|
|
if (preservedGatewayTargetApproach is not null
|
|
&& !ElkEdgeRoutingGeometry.PointsEqual(
|
|
new ElkPoint { X = bypassX, Y = preservedGatewayTargetApproach.Y },
|
|
preservedGatewayTargetApproach))
|
|
{
|
|
ConsiderCandidate(
|
|
[
|
|
start,
|
|
new ElkPoint { X = start.X, Y = sourceBridgeAxis },
|
|
new ElkPoint { X = bypassX, Y = sourceBridgeAxis },
|
|
new ElkPoint { X = bypassX, Y = preservedGatewayTargetApproach.Y },
|
|
preservedGatewayTargetApproach,
|
|
end,
|
|
],
|
|
obstacles);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return bestPath;
|
|
}
|
|
|
|
private static double ResolveMinLineClearance(IReadOnlyCollection<ElkPositionedNode> nodes)
|
|
{
|
|
var serviceNodes = nodes.Where(node => node.Kind is not "Start" and not "End").ToArray();
|
|
return serviceNodes.Length > 0
|
|
? Math.Min(serviceNodes.Average(node => node.Width), serviceNodes.Average(node => node.Height)) / 2d
|
|
: 50d;
|
|
}
|
|
|
|
private static ElkRoutedEdge BuildSingleSectionEdge(
|
|
ElkRoutedEdge edge,
|
|
IReadOnlyList<ElkPoint> path)
|
|
{
|
|
return new ElkRoutedEdge
|
|
{
|
|
Id = edge.Id,
|
|
SourceNodeId = edge.SourceNodeId,
|
|
TargetNodeId = edge.TargetNodeId,
|
|
SourcePortId = edge.SourcePortId,
|
|
TargetPortId = edge.TargetPortId,
|
|
Kind = edge.Kind,
|
|
Label = edge.Label,
|
|
Sections =
|
|
[
|
|
new ElkEdgeSection
|
|
{
|
|
StartPoint = path[0],
|
|
EndPoint = path[^1],
|
|
BendPoints = path.Count > 2
|
|
? path.Skip(1).Take(path.Count - 2).ToArray()
|
|
: [],
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
private static bool HasProtectedUnderNodeGeometry(ElkRoutedEdge edge)
|
|
{
|
|
return ContainsInternalKindMarker(edge.Kind, ProtectedUnderNodeKindMarker);
|
|
}
|
|
|
|
private static ElkRoutedEdge ProtectUnderNodeGeometry(ElkRoutedEdge edge)
|
|
{
|
|
if (HasProtectedUnderNodeGeometry(edge))
|
|
{
|
|
return edge;
|
|
}
|
|
|
|
return CloneEdgeWithKind(edge, AppendInternalKindMarker(edge.Kind, ProtectedUnderNodeKindMarker));
|
|
}
|
|
|
|
private static ElkRoutedEdge CloneEdgeWithKind(ElkRoutedEdge edge, string? kind)
|
|
{
|
|
return new ElkRoutedEdge
|
|
{
|
|
Id = edge.Id,
|
|
SourceNodeId = edge.SourceNodeId,
|
|
TargetNodeId = edge.TargetNodeId,
|
|
SourcePortId = edge.SourcePortId,
|
|
TargetPortId = edge.TargetPortId,
|
|
Kind = kind,
|
|
Label = edge.Label,
|
|
Sections = edge.Sections
|
|
.Select(section => new ElkEdgeSection
|
|
{
|
|
StartPoint = section.StartPoint,
|
|
EndPoint = section.EndPoint,
|
|
BendPoints = section.BendPoints.ToArray(),
|
|
})
|
|
.ToArray(),
|
|
};
|
|
}
|
|
|
|
private static bool ContainsInternalKindMarker(string? kind, string marker)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(kind))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return kind
|
|
.Split('|', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
|
.Any(part => string.Equals(part, marker, StringComparison.OrdinalIgnoreCase));
|
|
}
|
|
|
|
private static string AppendInternalKindMarker(string? kind, string marker)
|
|
{
|
|
if (ContainsInternalKindMarker(kind, marker))
|
|
{
|
|
return kind!;
|
|
}
|
|
|
|
return string.IsNullOrWhiteSpace(kind)
|
|
? marker
|
|
: $"{kind}|{marker}";
|
|
}
|
|
|
|
private static string? RemoveInternalKindMarker(string? kind, string marker)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(kind))
|
|
{
|
|
return kind;
|
|
}
|
|
|
|
var parts = kind
|
|
.Split('|', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
|
.Where(part => !string.Equals(part, marker, StringComparison.OrdinalIgnoreCase))
|
|
.ToArray();
|
|
if (parts.Length == 0)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return string.Join("|", parts);
|
|
}
|
|
|
|
private static List<ElkPoint> NormalizePathPoints(IReadOnlyList<ElkPoint> points)
|
|
{
|
|
const double coordinateTolerance = 0.5d;
|
|
var deduped = new List<ElkPoint>();
|
|
foreach (var point in points)
|
|
{
|
|
if (deduped.Count == 0 || !ElkEdgeRoutingGeometry.PointsEqual(deduped[^1], point))
|
|
{
|
|
deduped.Add(point);
|
|
}
|
|
}
|
|
|
|
if (deduped.Count <= 2)
|
|
{
|
|
return deduped;
|
|
}
|
|
|
|
var simplified = new List<ElkPoint> { deduped[0] };
|
|
for (var i = 1; i < deduped.Count - 1; i++)
|
|
{
|
|
var previous = simplified[^1];
|
|
var current = deduped[i];
|
|
var next = deduped[i + 1];
|
|
var sameX = Math.Abs(previous.X - current.X) <= coordinateTolerance
|
|
&& Math.Abs(current.X - next.X) <= coordinateTolerance;
|
|
var sameY = Math.Abs(previous.Y - current.Y) <= coordinateTolerance
|
|
&& Math.Abs(current.Y - next.Y) <= coordinateTolerance;
|
|
if (!sameX && !sameY)
|
|
{
|
|
simplified.Add(current);
|
|
}
|
|
}
|
|
|
|
simplified.Add(deduped[^1]);
|
|
return simplified;
|
|
}
|
|
}
|