2619 lines
92 KiB
C#
2619 lines
92 KiB
C#
namespace StellaOps.ElkSharp;
|
|
|
|
internal static partial class ElkEdgePostProcessor
|
|
{
|
|
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 string CreatePathSignature(IReadOnlyList<ElkPoint> path)
|
|
{
|
|
return string.Join(";", path.Select(point => $"{point.X:F3},{point.Y:F3}"));
|
|
}
|
|
|
|
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 HasNodeObstacleCrossing(
|
|
IReadOnlyList<ElkPoint> path,
|
|
(double Left, double Top, double Right, double Bottom, string Id)[] nodeObstacles,
|
|
string? sourceNodeId,
|
|
string? targetNodeId)
|
|
{
|
|
if (path.Count < 2)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
for (var i = 0; i < path.Count - 1; i++)
|
|
{
|
|
if (SegmentCrossesObstacle(path[i], path[i + 1], nodeObstacles, 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 = (gatewayNode.Width + gatewayNode.Height) / 2d;
|
|
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,
|
|
});
|
|
}
|
|
|
|
if (targetNode.Kind == "Decision")
|
|
{
|
|
if (boundaryPoint.X <= centerX + 0.5d)
|
|
{
|
|
var diagonalDx = Math.Abs(boundaryPoint.X - (targetNode.X - padding));
|
|
AddUniquePoint(
|
|
candidates,
|
|
new ElkPoint
|
|
{
|
|
X = targetNode.X - padding,
|
|
Y = boundaryPoint.Y - diagonalDx,
|
|
});
|
|
AddUniquePoint(
|
|
candidates,
|
|
new ElkPoint
|
|
{
|
|
X = targetNode.X - padding,
|
|
Y = boundaryPoint.Y + diagonalDx,
|
|
});
|
|
}
|
|
|
|
if (boundaryPoint.X >= centerX - 0.5d)
|
|
{
|
|
var diagonalDx = Math.Abs((targetNode.X + targetNode.Width + padding) - boundaryPoint.X);
|
|
AddUniquePoint(
|
|
candidates,
|
|
new ElkPoint
|
|
{
|
|
X = targetNode.X + targetNode.Width + padding,
|
|
Y = boundaryPoint.Y - diagonalDx,
|
|
});
|
|
AddUniquePoint(
|
|
candidates,
|
|
new ElkPoint
|
|
{
|
|
X = targetNode.X + targetNode.Width + padding,
|
|
Y = boundaryPoint.Y + diagonalDx,
|
|
});
|
|
}
|
|
|
|
if (boundaryPoint.Y <= centerY + 0.5d)
|
|
{
|
|
var diagonalDy = Math.Abs(boundaryPoint.Y - (targetNode.Y - padding));
|
|
AddUniquePoint(
|
|
candidates,
|
|
new ElkPoint
|
|
{
|
|
X = boundaryPoint.X - diagonalDy,
|
|
Y = targetNode.Y - padding,
|
|
});
|
|
AddUniquePoint(
|
|
candidates,
|
|
new ElkPoint
|
|
{
|
|
X = boundaryPoint.X + diagonalDy,
|
|
Y = targetNode.Y - padding,
|
|
});
|
|
}
|
|
|
|
if (boundaryPoint.Y >= centerY - 0.5d)
|
|
{
|
|
var diagonalDy = Math.Abs((targetNode.Y + targetNode.Height + padding) - boundaryPoint.Y);
|
|
AddUniquePoint(
|
|
candidates,
|
|
new ElkPoint
|
|
{
|
|
X = boundaryPoint.X - diagonalDy,
|
|
Y = targetNode.Y + targetNode.Height + padding,
|
|
});
|
|
AddUniquePoint(
|
|
candidates,
|
|
new ElkPoint
|
|
{
|
|
X = boundaryPoint.X + diagonalDy,
|
|
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);
|
|
AddUniqueCoordinate(candidateClearances, 0d);
|
|
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)
|
|
&& !HasValidBoundaryAngle(candidate[^1], candidate[^2], targetNode))
|
|
{
|
|
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;
|
|
double? preservedRectTargetApproachAxis = null;
|
|
if (targetNode is not null)
|
|
{
|
|
if (currentPath.Count >= 2
|
|
&& !ElkEdgeRoutingGeometry.PointsEqual(currentPath[^2], end))
|
|
{
|
|
if (ElkShapeBoundaries.IsGatewayShape(targetNode))
|
|
{
|
|
if (ElkShapeBoundaries.HasValidGatewayBoundaryAngle(targetNode, end, currentPath[^2]))
|
|
{
|
|
preservedGatewayTargetApproach = currentPath[^2];
|
|
targetBridgeAxis = horizontalDominant ? currentPath[^2].X : currentPath[^2].Y;
|
|
}
|
|
}
|
|
else if (HasValidBoundaryAngle(end, currentPath[^2], targetNode))
|
|
{
|
|
const double coordinateTolerance = 0.5d;
|
|
if (horizontalDominant && Math.Abs(currentPath[^2].X - end.X) <= coordinateTolerance)
|
|
{
|
|
preservedRectTargetApproachAxis = currentPath[^2].X;
|
|
}
|
|
else if (!horizontalDominant && Math.Abs(currentPath[^2].Y - end.Y) <= coordinateTolerance)
|
|
{
|
|
preservedRectTargetApproachAxis = currentPath[^2].Y;
|
|
}
|
|
}
|
|
}
|
|
else if (ElkShapeBoundaries.IsGatewayShape(targetNode))
|
|
{
|
|
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 point in currentPath.Skip(1).Take(Math.Max(0, currentPath.Count - 2)))
|
|
{
|
|
AddUniqueCoordinate(bypassYCandidates, point.Y);
|
|
if (IsUsableForwardBridgeAxis(start.X, end.X, point.X))
|
|
{
|
|
AddUniqueCoordinate(cornerBridgeXCandidates, point.X);
|
|
}
|
|
}
|
|
|
|
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);
|
|
|
|
if (preservedRectTargetApproachAxis is double rectTargetApproachX
|
|
&& Math.Abs(bypassY - end.Y) > 0.5d)
|
|
{
|
|
ConsiderCandidate(
|
|
[
|
|
start,
|
|
new ElkPoint { X = sourceBridgeAxis, Y = start.Y },
|
|
new ElkPoint { X = sourceBridgeAxis, Y = bypassY },
|
|
new ElkPoint { X = rectTargetApproachX, 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 point in currentPath.Skip(1).Take(Math.Max(0, currentPath.Count - 2)))
|
|
{
|
|
AddUniqueCoordinate(bypassXCandidates, point.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 (preservedRectTargetApproachAxis is double rectTargetApproachY
|
|
&& Math.Abs(bypassX - end.X) > 0.5d)
|
|
{
|
|
ConsiderCandidate(
|
|
[
|
|
start,
|
|
new ElkPoint { X = start.X, Y = sourceBridgeAxis },
|
|
new ElkPoint { X = bypassX, Y = sourceBridgeAxis },
|
|
new ElkPoint { X = bypassX, Y = rectTargetApproachY },
|
|
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));
|
|
}
|
|
|
|
} |