Files
git.stella-ops.org/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterIterative.LocalRepair.ShortestPaths.cs

234 lines
8.3 KiB
C#

using System.Collections.Concurrent;
using System.Diagnostics;
using System.Globalization;
namespace StellaOps.ElkSharp;
internal static partial class ElkEdgeRouterIterative
{
private static List<ElkPoint>? TryBuildPreferredSideShortcut(
ElkPositionedNode sourceNode,
ElkPositionedNode targetNode,
IReadOnlyCollection<ElkPositionedNode> nodes,
string sourceId,
string targetId)
{
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 null;
}
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";
var start = BuildPreferredBoundaryPoint(sourceNode, preferredSourceSide, targetNode);
var end = BuildPreferredBoundaryPoint(targetNode, preferredTargetSide, sourceNode);
return TryBuildShortestOrthogonalPath(start, end, nodes, sourceId, targetId, targetNode, 0d);
}
private static ElkPoint BuildPreferredBoundaryPoint(
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 List<ElkPoint>? TryBuildShortestOrthogonalPath(
ElkPoint start,
ElkPoint end,
IReadOnlyCollection<ElkPositionedNode> nodes,
string sourceId,
string targetId,
ElkPositionedNode? targetNode,
double obstaclePadding)
{
var rawObstacles = nodes.Select(node => (
Left: node.X - obstaclePadding,
Top: node.Y - obstaclePadding,
Right: node.X + node.Width + obstaclePadding,
Bottom: node.Y + node.Height + obstaclePadding,
Id: node.Id)).ToArray();
bool SegmentIsClear(ElkPoint from, ElkPoint to) =>
!ElkEdgePostProcessor.SegmentCrossesObstacle(from, to, rawObstacles, sourceId, targetId);
if (Math.Abs(start.X - end.X) < 0.5d || Math.Abs(start.Y - end.Y) < 0.5d)
{
return SegmentIsClear(start, end)
? [start, end]
: null;
}
foreach (var pivot in EnumerateOrthogonalShortcutPivots(start, end, targetNode))
{
if (!SegmentIsClear(start, pivot) || !SegmentIsClear(pivot, end))
{
continue;
}
return NormalizePolyline([start, pivot, end]);
}
return null;
}
private static IEnumerable<ElkPoint> EnumerateOrthogonalShortcutPivots(
ElkPoint start,
ElkPoint end,
ElkPositionedNode? targetNode)
{
var targetSide = targetNode is null
? string.Empty
: ElkEdgeRoutingGeometry.ResolveBoundarySide(end, targetNode);
var preferred = targetSide is "left" or "right"
? new ElkPoint { X = start.X, Y = end.Y }
: new ElkPoint { X = end.X, Y = start.Y };
var alternate = targetSide is "left" or "right"
? new ElkPoint { X = end.X, Y = start.Y }
: new ElkPoint { X = start.X, Y = end.Y };
yield return preferred;
if (!ElkEdgeRoutingGeometry.PointsEqual(preferred, alternate))
{
yield return alternate;
}
}
private static IEnumerable<ElkPoint> EnumerateShortestRepairEndpoints(
ElkPoint start,
ElkPoint currentEnd,
ElkPositionedNode? targetNode)
{
var endpoints = new List<ElkPoint>();
void AddCandidate(ElkPoint candidate)
{
if (!endpoints.Any(existing => ElkEdgeRoutingGeometry.PointsEqual(existing, candidate)))
{
endpoints.Add(candidate);
}
}
AddCandidate(currentEnd);
if (targetNode is null)
{
return endpoints;
}
if (ElkShapeBoundaries.IsGatewayShape(targetNode))
{
if (ElkShapeBoundaries.TryProjectGatewayBoundarySlot(targetNode, "left", start.Y, out var left))
{
AddCandidate(left);
}
if (ElkShapeBoundaries.TryProjectGatewayBoundarySlot(targetNode, "right", start.Y, out var right))
{
AddCandidate(right);
}
if (ElkShapeBoundaries.TryProjectGatewayBoundarySlot(targetNode, "top", start.X, out var top))
{
AddCandidate(top);
}
if (ElkShapeBoundaries.TryProjectGatewayBoundarySlot(targetNode, "bottom", start.X, out var bottom))
{
AddCandidate(bottom);
}
return endpoints;
}
var horizontalInset = Math.Min(24d, Math.Max(12d, targetNode.Width / 4d));
var verticalInset = Math.Min(24d, Math.Max(12d, targetNode.Height / 4d));
var candidateEndpoints = new[]
{
new ElkPoint
{
X = targetNode.X,
Y = Math.Clamp(start.Y, targetNode.Y + verticalInset, (targetNode.Y + targetNode.Height) - verticalInset),
},
new ElkPoint
{
X = targetNode.X + targetNode.Width,
Y = Math.Clamp(start.Y, targetNode.Y + verticalInset, (targetNode.Y + targetNode.Height) - verticalInset),
},
new ElkPoint
{
X = Math.Clamp(start.X, targetNode.X + horizontalInset, (targetNode.X + targetNode.Width) - horizontalInset),
Y = targetNode.Y,
},
new ElkPoint
{
X = Math.Clamp(start.X, targetNode.X + horizontalInset, (targetNode.X + targetNode.Width) - horizontalInset),
Y = targetNode.Y + targetNode.Height,
},
};
foreach (var candidate in candidateEndpoints)
{
AddCandidate(candidate);
}
return endpoints;
}
}