Refactor ElkSharp routing sources into partial modules
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
# AGENTS.md · StellaOps.ElkSharp
|
||||
# AGENTS.md - StellaOps.ElkSharp
|
||||
|
||||
## Scope
|
||||
- Working directory: `src/__Libraries/StellaOps.ElkSharp/`
|
||||
@@ -22,13 +22,21 @@
|
||||
- Keep iterative diagnostics detailed enough to prove progress. The document-processing artifact test should emit a live progress log that shows baseline state, strategy starts, per-attempt scores, and adaptation decisions.
|
||||
- Iterative optimization work should focus on penalized edge or edge-cluster fixes, not whole-graph reroutes. Use whole-graph retries only as a fallback once diagnostics show the local repair path is unavailable.
|
||||
- Keep attempt 1 as the only full-strategy reroute. Attempt 2+ must target only the failed lanes or failed edge clusters, with shortest-path detours prioritized before broader quality cleanup.
|
||||
- Do not pad a local-repair iteration with generic high-severity edges that are not part of the currently failing rule set. The repair plan may add exact conflict peers for the same join/shared-lane/corridor problem, but it must not drift back toward a whole-graph reroute.
|
||||
- Local-repair iterations may build reroute candidates in parallel, but the merge back into the route must stay deterministic and keep the final per-edge apply order stable.
|
||||
- The selected layout must not backtrack inside the final target-approach window. Attempt 2+ shortest-path repair should try a direct orthogonal shortcut first and only fall back to a low-penalty 45-degree A* candidate when the orthogonal repair is blocked by other rules.
|
||||
- Keep small or protected graphs on the baseline route when the iterative sweep would risk established geometry contracts; reserve the multi-strategy path for larger congested graphs where it materially improves routing quality.
|
||||
- Keep per-attempt diagnostics granular enough to expose routing versus post-processing cost. Phase timings and route-pass counts are required evidence before widening the retry budget again.
|
||||
- Use cheap local geometry repair after routing to clean boundary-angle, target-side arrival-slot, and repeat-collector return-lane defects before escalating to more A* work. The selected layout must satisfy the node-side 90° entry/exit rule and must not leave repeat-collector lanes collapsed onto the same outer return lane.
|
||||
- Use cheap local geometry repair after routing to clean boundary-angle, target-side arrival-slot, and repeat-collector return-lane defects before escalating to more A* work. The selected layout must satisfy the node-side 90-degree entry/exit rule and must not leave repeat-collector lanes collapsed onto the same outer return lane.
|
||||
- Boundary joins must use a discrete side-slot lattice, not ad-hoc clustering. Rectangular nodes may use at most `3` evenly spread slots on `left`/`right` faces and at most `5` evenly spread slots on `top`/`bottom` faces; gateway faces may use only `1` or `2` centered face slots. Never allow more than one input/output to occupy the same resolved slot, and do not exempt singleton entries or preserved repeat/corridor exits from the lattice: a lone endpoint still has to land on its centered slot. Make scoring and final repair share the same realizable slot coordinates, and end winner refinement with a restabilization pass so late shared-lane or under-node cleanup cannot drift decision/branch exits back into mid-face clustering.
|
||||
- When shortest-path local repair evaluates obstacle-skirt candidates, include usable interior axes from the current path and a raw-clearance fallback before preserving a wider overshoot; otherwise the repair can miss an already-safe local lane and keep an unnecessary detour alive.
|
||||
- When local repair is restricted to a penalized subset, target-slot spacing must still be computed against the full peer set for that target/side so one repaired edge does not collapse back onto the unchanged arrivals beside it.
|
||||
- Decision/Fork/Join gateway nodes are polygonal, not rectangular. Keep their final landing logic gateway-specific: land on the real polygon boundary, derive target slots from polygon-face intersections instead of rectangular side slots, prefer short 45-degree diagonal stubs only on gateway side faces, never on gateway corner vertices, and do not apply rectangle-side highway or target-backtracking heuristics to gateway targets.
|
||||
- Selected layouts must not keep any lane below the node field, and any retained 45-degree segment must stay within one average node-shape length. Gateway tips are not valid final join points: source exits must leave from a face interior, and gateway-target join spreading/scoring must group arrivals by the landed boundary band rather than by the final diagonal direction alone.
|
||||
- Repeat-collector edges with preserved outer corridors are still subject to node-crossing repair. If a pre-corridor prefix crosses a node, reroute only that prefix into the preserved corridor instead of skipping the edge outright.
|
||||
- Do not replace corridor and backward-route behavior with generic rerouting unless the sprint explicitly changes that contract.
|
||||
- When touching proximity/highway logic, keep long applicable shared corridors distinct from short shared segments that must be spread apart.
|
||||
- Future A* performance work must precompute occupied grid cells or blocked segment masks and avoid expanding through cells already owned by non-terminal nodes or previously committed lanes. Derive intermediate grid spacing from approximately one third of the average service-task size instead of keeping a fixed dense lattice.
|
||||
- The A* router now precomputes node-obstacle blocked step masks per route so neighbor expansion does not rescan every node obstacle. Future performance work should extend that to precomputed lane-occupancy masks for previously committed edge lanes, so the router can skip already-owned space instead of only penalizing it after expansion. Derive intermediate grid spacing from approximately one third of the average service-task size instead of keeping a fixed dense lattice.
|
||||
- Keep `TopToBottom` behavior stable unless the sprint explicitly includes it.
|
||||
|
||||
## Testing
|
||||
|
||||
219
src/__Libraries/StellaOps.ElkSharp/ElkBoundarySlots.cs
Normal file
219
src/__Libraries/StellaOps.ElkSharp/ElkBoundarySlots.cs
Normal file
@@ -0,0 +1,219 @@
|
||||
namespace StellaOps.ElkSharp;
|
||||
|
||||
internal static class ElkBoundarySlots
|
||||
{
|
||||
private const double GatewayBoundaryInset = 4d;
|
||||
|
||||
internal static int ResolveBoundarySlotCapacity(ElkPositionedNode node, string side)
|
||||
{
|
||||
if (ElkShapeBoundaries.IsGatewayShape(node))
|
||||
{
|
||||
return side is "left" or "right" or "top" or "bottom" ? 2 : 1;
|
||||
}
|
||||
|
||||
return side switch
|
||||
{
|
||||
"left" or "right" => 3,
|
||||
"top" or "bottom" => 5,
|
||||
_ => 1,
|
||||
};
|
||||
}
|
||||
|
||||
internal static double[] BuildUniqueBoundarySlotCoordinates(
|
||||
ElkPositionedNode node,
|
||||
string side,
|
||||
int endpointCount)
|
||||
{
|
||||
var slotCount = Math.Max(1, Math.Min(Math.Max(1, endpointCount), ResolveBoundarySlotCapacity(node, side)));
|
||||
var (axisMin, axisMax) = ResolveBoundarySlotAxisRange(node, side);
|
||||
if (axisMax <= axisMin)
|
||||
{
|
||||
var midpoint = (axisMin + axisMax) / 2d;
|
||||
return Enumerable.Repeat(midpoint, slotCount).ToArray();
|
||||
}
|
||||
|
||||
if (slotCount == 1)
|
||||
{
|
||||
return [(axisMin + axisMax) / 2d];
|
||||
}
|
||||
|
||||
var axisSpan = axisMax - axisMin;
|
||||
if (ElkShapeBoundaries.IsGatewayShape(node))
|
||||
{
|
||||
return Enumerable.Range(0, slotCount)
|
||||
.Select(index => axisMin + (((index + 1d) * axisSpan) / (slotCount + 1d)))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
var axisStep = axisSpan / (slotCount - 1d);
|
||||
return Enumerable.Range(0, slotCount)
|
||||
.Select(index => axisMin + (index * axisStep))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
internal static double[] BuildAssignedBoundarySlotCoordinates(
|
||||
ElkPositionedNode node,
|
||||
string side,
|
||||
int endpointCount)
|
||||
{
|
||||
if (endpointCount <= 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var uniqueCoordinates = BuildUniqueBoundarySlotCoordinates(node, side, endpointCount);
|
||||
return Enumerable.Range(0, endpointCount)
|
||||
.Select(index => uniqueCoordinates[ResolveOrderedSlotIndex(index, endpointCount, uniqueCoordinates.Length)])
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
internal static double[] BuildAssignedBoundarySlotCoordinates(
|
||||
ElkPositionedNode node,
|
||||
string side,
|
||||
IReadOnlyList<double> orderedActualCoordinates)
|
||||
{
|
||||
if (orderedActualCoordinates.Count == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
return BuildAssignedBoundarySlotCoordinates(node, side, orderedActualCoordinates.Count);
|
||||
}
|
||||
|
||||
internal static double[] BuildAssignedBoundarySlotAxisCoordinates(
|
||||
ElkPositionedNode node,
|
||||
string side,
|
||||
int endpointCount)
|
||||
{
|
||||
var assignedCoordinates = BuildAssignedBoundarySlotCoordinates(node, side, endpointCount);
|
||||
if (assignedCoordinates.Length == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
return assignedCoordinates
|
||||
.Select(coordinate =>
|
||||
{
|
||||
var boundaryPoint = BuildBoundarySlotPoint(node, side, coordinate);
|
||||
return side is "left" or "right"
|
||||
? boundaryPoint.Y
|
||||
: boundaryPoint.X;
|
||||
})
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
internal static double[] BuildAssignedBoundarySlotAxisCoordinates(
|
||||
ElkPositionedNode node,
|
||||
string side,
|
||||
IReadOnlyList<double> orderedActualCoordinates)
|
||||
{
|
||||
var assignedCoordinates = BuildAssignedBoundarySlotCoordinates(node, side, orderedActualCoordinates);
|
||||
if (assignedCoordinates.Length == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
return assignedCoordinates
|
||||
.Select(coordinate =>
|
||||
{
|
||||
var boundaryPoint = BuildBoundarySlotPoint(node, side, coordinate);
|
||||
return side is "left" or "right"
|
||||
? boundaryPoint.Y
|
||||
: boundaryPoint.X;
|
||||
})
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
internal static double ResolveRequiredBoundarySlotGap(
|
||||
ElkPositionedNode node,
|
||||
string side,
|
||||
int endpointCount,
|
||||
double defaultGap)
|
||||
{
|
||||
var assignedCoordinates = BuildAssignedBoundarySlotAxisCoordinates(node, side, endpointCount);
|
||||
if (assignedCoordinates.Length < 2)
|
||||
{
|
||||
return defaultGap;
|
||||
}
|
||||
|
||||
var minPositiveGap = double.PositiveInfinity;
|
||||
var ordered = assignedCoordinates.OrderBy(value => value).ToArray();
|
||||
for (var i = 1; i < ordered.Length; i++)
|
||||
{
|
||||
var gap = ordered[i] - ordered[i - 1];
|
||||
if (gap > 0.5d)
|
||||
{
|
||||
minPositiveGap = Math.Min(minPositiveGap, gap);
|
||||
}
|
||||
}
|
||||
|
||||
if (double.IsInfinity(minPositiveGap))
|
||||
{
|
||||
return defaultGap;
|
||||
}
|
||||
|
||||
return Math.Min(defaultGap, minPositiveGap);
|
||||
}
|
||||
|
||||
internal static int ResolveOrderedSlotIndex(int orderedIndex, int entryCount, int slotCount)
|
||||
{
|
||||
if (slotCount <= 1 || entryCount <= 1)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (orderedIndex <= 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (orderedIndex >= entryCount - 1)
|
||||
{
|
||||
return slotCount - 1;
|
||||
}
|
||||
|
||||
return Math.Min(slotCount - 1, (int)Math.Floor((orderedIndex * (double)slotCount) / entryCount));
|
||||
}
|
||||
|
||||
internal static ElkPoint BuildBoundarySlotPoint(
|
||||
ElkPositionedNode node,
|
||||
string side,
|
||||
double slotCoordinate)
|
||||
{
|
||||
if (ElkShapeBoundaries.TryProjectGatewayBoundarySlot(node, side, slotCoordinate, out var gatewaySlot))
|
||||
{
|
||||
return gatewaySlot;
|
||||
}
|
||||
|
||||
return side switch
|
||||
{
|
||||
"left" => new ElkPoint { X = node.X, Y = slotCoordinate },
|
||||
"right" => new ElkPoint { X = node.X + node.Width, Y = slotCoordinate },
|
||||
"top" => new ElkPoint { X = slotCoordinate, Y = node.Y },
|
||||
"bottom" => new ElkPoint { X = slotCoordinate, Y = node.Y + node.Height },
|
||||
_ => new ElkPoint
|
||||
{
|
||||
X = node.X + (node.Width / 2d),
|
||||
Y = node.Y + (node.Height / 2d),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private static (double Min, double Max) ResolveBoundarySlotAxisRange(
|
||||
ElkPositionedNode node,
|
||||
string side)
|
||||
{
|
||||
if (ElkShapeBoundaries.IsGatewayShape(node))
|
||||
{
|
||||
return side is "left" or "right"
|
||||
? (node.Y + GatewayBoundaryInset, node.Y + node.Height - GatewayBoundaryInset)
|
||||
: (node.X + GatewayBoundaryInset, node.X + node.Width - GatewayBoundaryInset);
|
||||
}
|
||||
|
||||
var inset = side is "left" or "right"
|
||||
? Math.Min(24d, Math.Max(8d, node.Height / 4d))
|
||||
: Math.Min(24d, Math.Max(8d, node.Width / 4d));
|
||||
return side is "left" or "right"
|
||||
? (node.Y + inset, node.Y + node.Height - inset)
|
||||
: (node.X + inset, node.X + node.Width - inset);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
2619
src/__Libraries/StellaOps.ElkSharp/ElkEdgePostProcessor.UnderNode.cs
Normal file
2619
src/__Libraries/StellaOps.ElkSharp/ElkEdgePostProcessor.UnderNode.cs
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -34,27 +34,60 @@ internal static class ElkEdgePostProcessorCorridor
|
||||
|
||||
var corridorY = pts[firstCorridorIndex].Y;
|
||||
var isAboveCorridor = corridorY < graphMinY - 8d;
|
||||
var clearanceMargin = Math.Max(margin, 40d);
|
||||
var result = new List<ElkPoint>();
|
||||
|
||||
if (firstCorridorIndex > 0)
|
||||
{
|
||||
if (isAboveCorridor)
|
||||
{
|
||||
var preCorridorHasCrossing = false;
|
||||
for (var i = 0; i < firstCorridorIndex; i++)
|
||||
{
|
||||
var last = result.Count > 0 ? result[^1] : (ElkPoint?)null;
|
||||
if (last is null || Math.Abs(last.X - pts[i].X) > 0.01d || Math.Abs(last.Y - pts[i].Y) > 0.01d)
|
||||
if (!ElkEdgePostProcessor.SegmentCrossesObstacle(pts[i], pts[i + 1], obstacles, sourceId, targetId)
|
||||
&& !SegmentViolatesObstacleClearance(pts[i], pts[i + 1], obstacles, sourceId, targetId, clearanceMargin))
|
||||
{
|
||||
result.Add(pts[i]);
|
||||
continue;
|
||||
}
|
||||
|
||||
preCorridorHasCrossing = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (preCorridorHasCrossing)
|
||||
{
|
||||
var entryTarget = pts[firstCorridorIndex];
|
||||
var entryPath = ElkEdgePostProcessorAStar.RerouteWithGridAStar(
|
||||
section.StartPoint,
|
||||
entryTarget,
|
||||
obstacles,
|
||||
sourceId,
|
||||
targetId,
|
||||
margin);
|
||||
if (entryPath is not null && entryPath.Count >= 2)
|
||||
{
|
||||
result.AddRange(entryPath);
|
||||
}
|
||||
}
|
||||
|
||||
var entryX = result.Count > 0 ? result[^1].X : section.StartPoint.X;
|
||||
var entryY = result.Count > 0 ? result[^1].Y : section.StartPoint.Y;
|
||||
var safeEntryX = FindSafeVerticalX(entryX, entryY, corridorY, obstacles, sourceId, targetId);
|
||||
if (Math.Abs(safeEntryX - entryX) > 1d)
|
||||
if (result.Count == 0)
|
||||
{
|
||||
result.Add(new ElkPoint { X = safeEntryX, Y = corridorY });
|
||||
for (var i = 0; i < firstCorridorIndex; i++)
|
||||
{
|
||||
var last = result.Count > 0 ? result[^1] : (ElkPoint?)null;
|
||||
if (last is null || Math.Abs(last.X - pts[i].X) > 0.01d || Math.Abs(last.Y - pts[i].Y) > 0.01d)
|
||||
{
|
||||
result.Add(pts[i]);
|
||||
}
|
||||
}
|
||||
|
||||
var entryX = result.Count > 0 ? result[^1].X : section.StartPoint.X;
|
||||
var entryY = result.Count > 0 ? result[^1].Y : section.StartPoint.Y;
|
||||
var safeEntryX = FindSafeVerticalX(entryX, entryY, corridorY, obstacles, sourceId, targetId, clearanceMargin);
|
||||
if (Math.Abs(safeEntryX - entryX) > 1d)
|
||||
{
|
||||
result.Add(new ElkPoint { X = safeEntryX, Y = corridorY });
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -140,7 +173,8 @@ internal static class ElkEdgePostProcessorCorridor
|
||||
internal static double FindSafeVerticalX(
|
||||
double anchorX, double anchorY, double corridorY,
|
||||
(double Left, double Top, double Right, double Bottom, string Id)[] obstacles,
|
||||
string sourceId, string targetId)
|
||||
string sourceId, string targetId,
|
||||
double clearanceMargin = 0d)
|
||||
{
|
||||
var minY = Math.Min(anchorY, corridorY);
|
||||
var maxY = Math.Max(anchorY, corridorY);
|
||||
@@ -153,7 +187,10 @@ internal static class ElkEdgePostProcessorCorridor
|
||||
continue;
|
||||
}
|
||||
|
||||
if (anchorX > ob.Left && anchorX < ob.Right && maxY > ob.Top && minY < ob.Bottom)
|
||||
if (anchorX > ob.Left - clearanceMargin
|
||||
&& anchorX < ob.Right + clearanceMargin
|
||||
&& maxY > ob.Top - clearanceMargin
|
||||
&& minY < ob.Bottom + clearanceMargin)
|
||||
{
|
||||
blocked = true;
|
||||
break;
|
||||
@@ -167,16 +204,17 @@ internal static class ElkEdgePostProcessorCorridor
|
||||
|
||||
var candidateRight = anchorX;
|
||||
var candidateLeft = anchorX;
|
||||
var searchStep = Math.Max(24d, clearanceMargin * 0.5d);
|
||||
for (var attempt = 0; attempt < 20; attempt++)
|
||||
{
|
||||
candidateRight += 24d;
|
||||
if (!IsVerticalBlocked(candidateRight, minY, maxY, obstacles, sourceId, targetId))
|
||||
candidateRight += searchStep;
|
||||
if (!IsVerticalBlocked(candidateRight, minY, maxY, obstacles, sourceId, targetId, clearanceMargin))
|
||||
{
|
||||
return candidateRight;
|
||||
}
|
||||
|
||||
candidateLeft -= 24d;
|
||||
if (!IsVerticalBlocked(candidateLeft, minY, maxY, obstacles, sourceId, targetId))
|
||||
candidateLeft -= searchStep;
|
||||
if (!IsVerticalBlocked(candidateLeft, minY, maxY, obstacles, sourceId, targetId, clearanceMargin))
|
||||
{
|
||||
return candidateLeft;
|
||||
}
|
||||
@@ -188,7 +226,8 @@ internal static class ElkEdgePostProcessorCorridor
|
||||
private static bool IsVerticalBlocked(
|
||||
double x, double minY, double maxY,
|
||||
(double Left, double Top, double Right, double Bottom, string Id)[] obstacles,
|
||||
string sourceId, string targetId)
|
||||
string sourceId, string targetId,
|
||||
double clearanceMargin)
|
||||
{
|
||||
foreach (var ob in obstacles)
|
||||
{
|
||||
@@ -197,7 +236,66 @@ internal static class ElkEdgePostProcessorCorridor
|
||||
continue;
|
||||
}
|
||||
|
||||
if (x > ob.Left && x < ob.Right && maxY > ob.Top && minY < ob.Bottom)
|
||||
if (x > ob.Left - clearanceMargin
|
||||
&& x < ob.Right + clearanceMargin
|
||||
&& maxY > ob.Top - clearanceMargin
|
||||
&& minY < ob.Bottom + clearanceMargin)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool SegmentViolatesObstacleClearance(
|
||||
ElkPoint start,
|
||||
ElkPoint end,
|
||||
(double Left, double Top, double Right, double Bottom, string Id)[] obstacles,
|
||||
string sourceId,
|
||||
string targetId,
|
||||
double clearanceMargin)
|
||||
{
|
||||
if (clearanceMargin <= 0d)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var horizontal = Math.Abs(start.Y - end.Y) <= 0.5d;
|
||||
var vertical = Math.Abs(start.X - end.X) <= 0.5d;
|
||||
if (!horizontal && !vertical)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var ob in obstacles)
|
||||
{
|
||||
if (ob.Id == sourceId || ob.Id == targetId)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (horizontal)
|
||||
{
|
||||
var minX = Math.Min(start.X, end.X);
|
||||
var maxX = Math.Max(start.X, end.X);
|
||||
if (start.Y > ob.Top - clearanceMargin
|
||||
&& start.Y < ob.Bottom + clearanceMargin
|
||||
&& maxX > ob.Left - clearanceMargin
|
||||
&& minX < ob.Right + clearanceMargin)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
var minY = Math.Min(start.Y, end.Y);
|
||||
var maxY = Math.Max(start.Y, end.Y);
|
||||
if (start.X > ob.Left - clearanceMargin
|
||||
&& start.X < ob.Right + clearanceMargin
|
||||
&& maxY > ob.Top - clearanceMargin
|
||||
&& minY < ob.Bottom + clearanceMargin)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -48,6 +48,15 @@ internal static class ElkEdgeRouterAStar8Dir
|
||||
return null;
|
||||
}
|
||||
|
||||
var graphMaxY = obstacles.Length > 0
|
||||
? obstacles.Max(obstacle => obstacle.Bottom)
|
||||
: double.MaxValue;
|
||||
var disallowedBottomY = graphMaxY + 4d;
|
||||
var maxDiagonalStepLength = ResolveMaxDiagonalStepLength(obstacles);
|
||||
|
||||
var blockedSegments = BuildBlockedSegments(xArr, yArr, obstacles, sourceId, targetId);
|
||||
var softObstacleInfos = BuildSoftObstacleInfos(softObstacles);
|
||||
|
||||
var startIx = Array.BinarySearch(xArr, start.X);
|
||||
var startIy = Array.BinarySearch(yArr, start.Y);
|
||||
var endIx = Array.BinarySearch(xArr, end.X);
|
||||
@@ -59,41 +68,34 @@ internal static class ElkEdgeRouterAStar8Dir
|
||||
|
||||
bool IsBlockedOrthogonal(int ix1, int iy1, int ix2, int iy2)
|
||||
{
|
||||
var x1 = xArr[ix1];
|
||||
var y1 = yArr[iy1];
|
||||
var x2 = xArr[ix2];
|
||||
var y2 = yArr[iy2];
|
||||
foreach (var ob in obstacles)
|
||||
if (ix1 == ix2)
|
||||
{
|
||||
if (ob.Id == sourceId || ob.Id == targetId)
|
||||
var minIy = Math.Min(iy1, iy2);
|
||||
var maxIy = Math.Max(iy1, iy2);
|
||||
for (var iy = minIy; iy < maxIy; iy++)
|
||||
{
|
||||
continue;
|
||||
if (blockedSegments.IsVerticalBlocked(ix1, iy))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (ix1 == ix2)
|
||||
return false;
|
||||
}
|
||||
|
||||
if (iy1 == iy2)
|
||||
{
|
||||
var minIx = Math.Min(ix1, ix2);
|
||||
var maxIx = Math.Max(ix1, ix2);
|
||||
for (var ix = minIx; ix < maxIx; ix++)
|
||||
{
|
||||
if (x1 > ob.Left && x1 < ob.Right)
|
||||
if (blockedSegments.IsHorizontalBlocked(ix, iy1))
|
||||
{
|
||||
var minY = Math.Min(y1, y2);
|
||||
var maxY = Math.Max(y1, y2);
|
||||
if (maxY > ob.Top && minY < ob.Bottom)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (iy1 == iy2)
|
||||
{
|
||||
if (y1 > ob.Top && y1 < ob.Bottom)
|
||||
{
|
||||
var minX = Math.Min(x1, x2);
|
||||
var maxX = Math.Max(x1, x2);
|
||||
if (maxX > ob.Left && minX < ob.Right)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -152,18 +154,20 @@ internal static class ElkEdgeRouterAStar8Dir
|
||||
|
||||
var maxIterations = xCount * yCount * 12;
|
||||
var iterations = 0;
|
||||
var closed = new HashSet<int>();
|
||||
var closed = new bool[stateCount];
|
||||
|
||||
while (openSet.Count > 0 && iterations++ < maxIterations)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var current = openSet.Dequeue();
|
||||
if (!closed.Add(current))
|
||||
if (closed[current])
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
closed[current] = true;
|
||||
|
||||
var curDir = current % dirCount;
|
||||
var curIy = (current / dirCount) % yCount;
|
||||
var curIx = (current / dirCount) / yCount;
|
||||
@@ -182,6 +186,11 @@ internal static class ElkEdgeRouterAStar8Dir
|
||||
continue;
|
||||
}
|
||||
|
||||
if (yArr[curIy] > disallowedBottomY || yArr[ny] > disallowedBottomY)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var isDiagonal = Dx[d] != 0 && Dy[d] != 0;
|
||||
if (isDiagonal)
|
||||
{
|
||||
@@ -214,7 +223,12 @@ internal static class ElkEdgeRouterAStar8Dir
|
||||
{
|
||||
var ddx = xArr[nx] - xArr[curIx];
|
||||
var ddy = yArr[ny] - yArr[curIy];
|
||||
dist = Math.Sqrt(ddx * ddx + ddy * ddy) + routingParams.DiagonalPenalty;
|
||||
var diagonalStepLength = Math.Sqrt(ddx * ddx + ddy * ddy);
|
||||
if (diagonalStepLength > maxDiagonalStepLength)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
dist = diagonalStepLength + routingParams.DiagonalPenalty;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -223,7 +237,7 @@ internal static class ElkEdgeRouterAStar8Dir
|
||||
|
||||
var softCost = ComputeSoftObstacleCost(
|
||||
xArr[curIx], yArr[curIy], xArr[nx], yArr[ny],
|
||||
softObstacles, routingParams);
|
||||
softObstacleInfos, routingParams);
|
||||
|
||||
var tentativeG = gScore[current] + dist + bend + softCost;
|
||||
var neighborState = StateId(nx, ny, newDir);
|
||||
@@ -240,6 +254,20 @@ internal static class ElkEdgeRouterAStar8Dir
|
||||
return null;
|
||||
}
|
||||
|
||||
private static double ResolveMaxDiagonalStepLength(
|
||||
IReadOnlyCollection<(double Left, double Top, double Right, double Bottom, string Id)> obstacles)
|
||||
{
|
||||
if (obstacles.Count == 0)
|
||||
{
|
||||
return 256d;
|
||||
}
|
||||
|
||||
var averageWidth = obstacles.Average(obstacle => obstacle.Right - obstacle.Left);
|
||||
var averageHeight = obstacles.Average(obstacle => obstacle.Bottom - obstacle.Top);
|
||||
var averageShapeSize = (averageWidth + averageHeight) / 2d;
|
||||
return Math.Max(96d, averageShapeSize * 2d);
|
||||
}
|
||||
|
||||
private static double ComputeBendPenalty(int curDir, int newDir, double bendPenalty)
|
||||
{
|
||||
if (curDir == 0 || curDir == newDir)
|
||||
@@ -259,10 +287,10 @@ internal static class ElkEdgeRouterAStar8Dir
|
||||
|
||||
private static double ComputeSoftObstacleCost(
|
||||
double x1, double y1, double x2, double y2,
|
||||
IReadOnlyList<OrthogonalSoftObstacle> softObstacles,
|
||||
SoftObstacleInfo[] softObstacles,
|
||||
AStarRoutingParams routingParams)
|
||||
{
|
||||
if (routingParams.SoftObstacleWeight <= 0d || softObstacles.Count == 0)
|
||||
if (routingParams.SoftObstacleWeight <= 0d || softObstacles.Length == 0)
|
||||
{
|
||||
return 0d;
|
||||
}
|
||||
@@ -271,10 +299,26 @@ internal static class ElkEdgeRouterAStar8Dir
|
||||
var candidateEnd = new ElkPoint { X = x2, Y = y2 };
|
||||
var candidateIsH = Math.Abs(y2 - y1) < 2d;
|
||||
var candidateIsV = Math.Abs(x2 - x1) < 2d;
|
||||
var candidateMinX = Math.Min(x1, x2);
|
||||
var candidateMaxX = Math.Max(x1, x2);
|
||||
var candidateMinY = Math.Min(y1, y2);
|
||||
var candidateMaxY = Math.Max(y1, y2);
|
||||
var expandedMinX = candidateMinX - routingParams.SoftObstacleClearance;
|
||||
var expandedMaxX = candidateMaxX + routingParams.SoftObstacleClearance;
|
||||
var expandedMinY = candidateMinY - routingParams.SoftObstacleClearance;
|
||||
var expandedMaxY = candidateMaxY + routingParams.SoftObstacleClearance;
|
||||
var cost = 0d;
|
||||
|
||||
foreach (var obstacle in softObstacles)
|
||||
{
|
||||
if (expandedMaxX < obstacle.MinX
|
||||
|| expandedMinX > obstacle.MaxX
|
||||
|| expandedMaxY < obstacle.MinY
|
||||
|| expandedMinY > obstacle.MaxY)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ElkEdgeRoutingGeometry.SegmentsIntersect(candidateStart, candidateEnd, obstacle.Start, obstacle.End))
|
||||
{
|
||||
cost += 120d * routingParams.SoftObstacleWeight;
|
||||
@@ -284,7 +328,7 @@ internal static class ElkEdgeRouterAStar8Dir
|
||||
// Graduated proximity: closer = exponentially more expensive
|
||||
var dist = ComputeParallelDistance(
|
||||
x1, y1, x2, y2, candidateIsH, candidateIsV,
|
||||
obstacle.Start, obstacle.End,
|
||||
obstacle,
|
||||
routingParams.SoftObstacleClearance);
|
||||
|
||||
if (dist >= 0d)
|
||||
@@ -300,41 +344,202 @@ internal static class ElkEdgeRouterAStar8Dir
|
||||
private static double ComputeParallelDistance(
|
||||
double x1, double y1, double x2, double y2,
|
||||
bool candidateIsH, bool candidateIsV,
|
||||
ElkPoint obStart, ElkPoint obEnd,
|
||||
SoftObstacleInfo obstacle,
|
||||
double clearance)
|
||||
{
|
||||
var obIsH = Math.Abs(obStart.Y - obEnd.Y) < 2d;
|
||||
var obIsV = Math.Abs(obStart.X - obEnd.X) < 2d;
|
||||
|
||||
if (candidateIsH && obIsH)
|
||||
if (candidateIsH && obstacle.IsHorizontal)
|
||||
{
|
||||
var dist = Math.Abs(y1 - obStart.Y);
|
||||
var dist = Math.Abs(y1 - obstacle.Start.Y);
|
||||
if (dist >= clearance)
|
||||
{
|
||||
return -1d;
|
||||
}
|
||||
|
||||
var overlapMin = Math.Max(Math.Min(x1, x2), Math.Min(obStart.X, obEnd.X));
|
||||
var overlapMax = Math.Min(Math.Max(x1, x2), Math.Max(obStart.X, obEnd.X));
|
||||
var overlapMin = Math.Max(Math.Min(x1, x2), obstacle.MinX);
|
||||
var overlapMax = Math.Min(Math.Max(x1, x2), obstacle.MaxX);
|
||||
return overlapMax > overlapMin + 1d ? dist : -1d;
|
||||
}
|
||||
|
||||
if (candidateIsV && obIsV)
|
||||
if (candidateIsV && obstacle.IsVertical)
|
||||
{
|
||||
var dist = Math.Abs(x1 - obStart.X);
|
||||
var dist = Math.Abs(x1 - obstacle.Start.X);
|
||||
if (dist >= clearance)
|
||||
{
|
||||
return -1d;
|
||||
}
|
||||
|
||||
var overlapMin = Math.Max(Math.Min(y1, y2), Math.Min(obStart.Y, obEnd.Y));
|
||||
var overlapMax = Math.Min(Math.Max(y1, y2), Math.Max(obStart.Y, obEnd.Y));
|
||||
var overlapMin = Math.Max(Math.Min(y1, y2), obstacle.MinY);
|
||||
var overlapMax = Math.Min(Math.Max(y1, y2), obstacle.MaxY);
|
||||
return overlapMax > overlapMin + 1d ? dist : -1d;
|
||||
}
|
||||
|
||||
return -1d;
|
||||
}
|
||||
|
||||
private static BlockedSegments BuildBlockedSegments(
|
||||
double[] xArr,
|
||||
double[] yArr,
|
||||
(double Left, double Top, double Right, double Bottom, string Id)[] obstacles,
|
||||
string sourceId,
|
||||
string targetId)
|
||||
{
|
||||
var xCount = xArr.Length;
|
||||
var yCount = yArr.Length;
|
||||
var verticalBlocked = new bool[xCount * Math.Max(0, yCount - 1)];
|
||||
var horizontalBlocked = new bool[Math.Max(0, xCount - 1) * yCount];
|
||||
|
||||
foreach (var obstacle in obstacles)
|
||||
{
|
||||
if (obstacle.Id == sourceId || obstacle.Id == targetId)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var verticalXStart = Math.Max(0, LowerBoundExclusive(xArr, obstacle.Left));
|
||||
var verticalXEnd = Math.Min(xCount - 1, UpperBoundExclusive(xArr, obstacle.Right) - 1);
|
||||
if (verticalXStart <= verticalXEnd)
|
||||
{
|
||||
var verticalYStart = Math.Max(0, LowerBound(yArr, obstacle.Top) - 1);
|
||||
var verticalYEnd = Math.Min(yCount - 2, UpperBound(yArr, obstacle.Bottom) - 1);
|
||||
for (var ix = verticalXStart; ix <= verticalXEnd; ix++)
|
||||
{
|
||||
for (var iy = verticalYStart; iy <= verticalYEnd; iy++)
|
||||
{
|
||||
if (yArr[iy + 1] > obstacle.Top && yArr[iy] < obstacle.Bottom)
|
||||
{
|
||||
verticalBlocked[(ix * (yCount - 1)) + iy] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var horizontalYStart = Math.Max(0, LowerBoundExclusive(yArr, obstacle.Top));
|
||||
var horizontalYEnd = Math.Min(yCount - 1, UpperBoundExclusive(yArr, obstacle.Bottom) - 1);
|
||||
if (horizontalYStart <= horizontalYEnd)
|
||||
{
|
||||
var horizontalXStart = Math.Max(0, LowerBound(xArr, obstacle.Left) - 1);
|
||||
var horizontalXEnd = Math.Min(xCount - 2, UpperBound(xArr, obstacle.Right) - 1);
|
||||
for (var iy = horizontalYStart; iy <= horizontalYEnd; iy++)
|
||||
{
|
||||
for (var ix = horizontalXStart; ix <= horizontalXEnd; ix++)
|
||||
{
|
||||
if (xArr[ix + 1] > obstacle.Left && xArr[ix] < obstacle.Right)
|
||||
{
|
||||
horizontalBlocked[(ix * yCount) + iy] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new BlockedSegments(xCount, yCount, verticalBlocked, horizontalBlocked);
|
||||
}
|
||||
|
||||
private static SoftObstacleInfo[] BuildSoftObstacleInfos(IReadOnlyList<OrthogonalSoftObstacle> softObstacles)
|
||||
{
|
||||
if (softObstacles.Count == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var infos = new SoftObstacleInfo[softObstacles.Count];
|
||||
for (var i = 0; i < softObstacles.Count; i++)
|
||||
{
|
||||
var obstacle = softObstacles[i];
|
||||
infos[i] = new SoftObstacleInfo(
|
||||
obstacle.Start,
|
||||
obstacle.End,
|
||||
Math.Min(obstacle.Start.X, obstacle.End.X),
|
||||
Math.Max(obstacle.Start.X, obstacle.End.X),
|
||||
Math.Min(obstacle.Start.Y, obstacle.End.Y),
|
||||
Math.Max(obstacle.Start.Y, obstacle.End.Y),
|
||||
Math.Abs(obstacle.Start.Y - obstacle.End.Y) < 2d,
|
||||
Math.Abs(obstacle.Start.X - obstacle.End.X) < 2d);
|
||||
}
|
||||
|
||||
return infos;
|
||||
}
|
||||
|
||||
private static int LowerBound(double[] values, double target)
|
||||
{
|
||||
var low = 0;
|
||||
var high = values.Length;
|
||||
while (low < high)
|
||||
{
|
||||
var mid = low + ((high - low) / 2);
|
||||
if (values[mid] < target)
|
||||
{
|
||||
low = mid + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
high = mid;
|
||||
}
|
||||
}
|
||||
|
||||
return low;
|
||||
}
|
||||
|
||||
private static int UpperBound(double[] values, double target)
|
||||
{
|
||||
var low = 0;
|
||||
var high = values.Length;
|
||||
while (low < high)
|
||||
{
|
||||
var mid = low + ((high - low) / 2);
|
||||
if (values[mid] <= target)
|
||||
{
|
||||
low = mid + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
high = mid;
|
||||
}
|
||||
}
|
||||
|
||||
return low;
|
||||
}
|
||||
|
||||
private static int LowerBoundExclusive(double[] values, double target)
|
||||
{
|
||||
var low = 0;
|
||||
var high = values.Length;
|
||||
while (low < high)
|
||||
{
|
||||
var mid = low + ((high - low) / 2);
|
||||
if (values[mid] <= target)
|
||||
{
|
||||
low = mid + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
high = mid;
|
||||
}
|
||||
}
|
||||
|
||||
return low;
|
||||
}
|
||||
|
||||
private static int UpperBoundExclusive(double[] values, double target)
|
||||
{
|
||||
var low = 0;
|
||||
var high = values.Length;
|
||||
while (low < high)
|
||||
{
|
||||
var mid = low + ((high - low) / 2);
|
||||
if (values[mid] < target)
|
||||
{
|
||||
low = mid + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
high = mid;
|
||||
}
|
||||
}
|
||||
|
||||
return low;
|
||||
}
|
||||
|
||||
private static List<ElkPoint> ReconstructPath(
|
||||
int endState, int[] cameFrom,
|
||||
double[] xArr, double[] yArr,
|
||||
@@ -391,4 +596,41 @@ internal static class ElkEdgeRouterAStar8Dir
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private readonly record struct SoftObstacleInfo(
|
||||
ElkPoint Start,
|
||||
ElkPoint End,
|
||||
double MinX,
|
||||
double MaxX,
|
||||
double MinY,
|
||||
double MaxY,
|
||||
bool IsHorizontal,
|
||||
bool IsVertical);
|
||||
|
||||
private readonly record struct BlockedSegments(
|
||||
int XCount,
|
||||
int YCount,
|
||||
bool[] VerticalBlocked,
|
||||
bool[] HorizontalBlocked)
|
||||
{
|
||||
internal bool IsVerticalBlocked(int ix, int iy)
|
||||
{
|
||||
if (ix < 0 || ix >= XCount || iy < 0 || iy >= YCount - 1)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return VerticalBlocked[(ix * (YCount - 1)) + iy];
|
||||
}
|
||||
|
||||
internal bool IsHorizontalBlocked(int ix, int iy)
|
||||
{
|
||||
if (ix < 0 || ix >= XCount - 1 || iy < 0 || iy >= YCount)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return HorizontalBlocked[(ix * YCount) + iy];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,6 +164,11 @@ internal static class ElkEdgeRouterHighway
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ElkShapeBoundaries.IsGatewayShape(targetNode))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var path = ExtractFullPath(edge);
|
||||
if (path.Count < 2)
|
||||
{
|
||||
@@ -203,7 +208,12 @@ internal static class ElkEdgeRouterHighway
|
||||
|
||||
var pairMetrics = ComputePairMetrics(members);
|
||||
var actualGap = ComputeMinEndpointGap(members, side);
|
||||
var requiresSpread = (actualGap + CoordinateTolerance) < minLineClearance
|
||||
var requiredGap = ElkBoundarySlots.ResolveRequiredBoundarySlotGap(
|
||||
targetNode,
|
||||
side,
|
||||
members.Count,
|
||||
minLineClearance);
|
||||
var requiresSpread = (actualGap + CoordinateTolerance) < requiredGap
|
||||
&& !pairMetrics.AllPairsApplicable;
|
||||
if (!requiresSpread && pairMetrics.ShortestSharedRatio < MinHighwayRatio)
|
||||
{
|
||||
@@ -223,10 +233,10 @@ internal static class ElkEdgeRouterHighway
|
||||
Reason = requiresSpread
|
||||
? pairMetrics.HasSharedSegment && pairMetrics.ShortestSharedRatio < MinHighwayRatio
|
||||
? $"shared ratio {pairMetrics.ShortestSharedRatio:F2} < {MinHighwayRatio:F2}"
|
||||
: $"gap {actualGap:F0}px < clearance {minLineClearance:F0}px"
|
||||
: $"gap {actualGap:F0}px < required gap {requiredGap:F0}px"
|
||||
: pairMetrics.AllPairsApplicable
|
||||
? $"shared ratio {pairMetrics.ShortestSharedRatio:F2} >= {MinHighwayRatio:F2}"
|
||||
: $"gap {actualGap:F0}px >= clearance {minLineClearance:F0}px",
|
||||
: $"gap {actualGap:F0}px >= required gap {requiredGap:F0}px",
|
||||
};
|
||||
|
||||
return new GroupEvaluation(members, diagnostic);
|
||||
@@ -300,33 +310,7 @@ internal static class ElkEdgeRouterHighway
|
||||
int count,
|
||||
double minLineClearance)
|
||||
{
|
||||
if (count <= 1)
|
||||
{
|
||||
return
|
||||
[
|
||||
side is "left" or "right"
|
||||
? targetNode.Y + (targetNode.Height / 2d)
|
||||
: targetNode.X + (targetNode.Width / 2d),
|
||||
];
|
||||
}
|
||||
|
||||
var axisMin = side is "left" or "right"
|
||||
? targetNode.Y + BoundaryInset
|
||||
: targetNode.X + BoundaryInset;
|
||||
var axisMax = side is "left" or "right"
|
||||
? targetNode.Y + targetNode.Height - BoundaryInset
|
||||
: targetNode.X + targetNode.Width - BoundaryInset;
|
||||
var axisLength = Math.Max(8d, axisMax - axisMin);
|
||||
var spacing = Math.Max(
|
||||
MinimumSpreadSpacing,
|
||||
Math.Min(minLineClearance, axisLength / (count - 1)));
|
||||
var totalSpan = (count - 1) * spacing;
|
||||
var center = (axisMin + axisMax) / 2d;
|
||||
var start = Math.Max(axisMin, Math.Min(center - (totalSpan / 2d), axisMax - totalSpan));
|
||||
|
||||
return Enumerable.Range(0, count)
|
||||
.Select(index => Math.Min(axisMax, start + (index * spacing)))
|
||||
.ToArray();
|
||||
return ElkBoundarySlots.BuildAssignedBoundarySlotCoordinates(targetNode, side, count);
|
||||
}
|
||||
|
||||
private static List<ElkPoint> AdjustPathToTargetSlot(
|
||||
|
||||
@@ -0,0 +1,924 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
|
||||
namespace StellaOps.ElkSharp;
|
||||
|
||||
internal static partial class ElkEdgeRouterIterative
|
||||
{
|
||||
private static ElkRoutedEdge[] ApplyPostProcessing(
|
||||
ElkRoutedEdge[] edges,
|
||||
ElkPositionedNode[] nodes,
|
||||
ElkLayoutOptions layoutOptions)
|
||||
{
|
||||
var result = ElkEdgePostProcessor.AvoidNodeCrossings(edges, nodes, layoutOptions.Direction);
|
||||
result = ElkEdgePostProcessor.EliminateDiagonalSegments(result, nodes);
|
||||
result = ElkEdgePostProcessorSimplify.SimplifyEdgePaths(result, nodes);
|
||||
result = ElkEdgePostProcessorSimplify.TightenOuterCorridors(result, nodes);
|
||||
if (HighwayProcessingEnabled)
|
||||
{
|
||||
result = ElkEdgeRouterHighway.BreakShortHighways(result, nodes);
|
||||
}
|
||||
|
||||
result = ElkEdgePostProcessor.NormalizeBoundaryAngles(result, nodes);
|
||||
result = ElkEdgePostProcessor.AvoidNodeCrossings(result, nodes, layoutOptions.Direction);
|
||||
result = ElkEdgePostProcessorSimplify.SimplifyEdgePaths(result, nodes);
|
||||
result = ElkEdgePostProcessor.AvoidNodeCrossings(result, nodes, layoutOptions.Direction);
|
||||
result = ElkEdgePostProcessor.NormalizeBoundaryAngles(result, nodes);
|
||||
var serviceNodes = nodes.Where(n => n.Kind is not "Start" and not "End").ToArray();
|
||||
var minLineClearance = serviceNodes.Length > 0
|
||||
? Math.Min(serviceNodes.Average(n => n.Width), serviceNodes.Average(n => n.Height)) / 2d
|
||||
: 50d;
|
||||
result = ElkEdgePostProcessor.NormalizeSourceExitAngles(result, nodes);
|
||||
result = ElkEdgePostProcessor.SpreadSourceDepartureJoins(result, nodes, minLineClearance);
|
||||
result = ElkEdgePostProcessor.RepairBoundaryAnglesAndTargetApproaches(result, nodes, minLineClearance);
|
||||
result = ElkEdgePostProcessor.ElevateUnderNodeViolations(result, nodes, minLineClearance);
|
||||
result = ElkEdgePostProcessor.AvoidNodeCrossings(result, nodes, layoutOptions.Direction);
|
||||
result = ElkEdgePostProcessor.NormalizeBoundaryAngles(result, nodes);
|
||||
result = ElkEdgePostProcessor.NormalizeSourceExitAngles(result, nodes);
|
||||
result = ElkEdgePostProcessor.SpreadSourceDepartureJoins(result, nodes, minLineClearance);
|
||||
result = ElkEdgePostProcessor.RepairBoundaryAnglesAndTargetApproaches(result, nodes, minLineClearance);
|
||||
result = RestoreProtectedRepeatCollectorCorridors(result, edges, nodes);
|
||||
result = ElkEdgePostProcessor.AvoidNodeCrossings(result, nodes, layoutOptions.Direction);
|
||||
result = ElkEdgePostProcessor.FinalizeGatewayBoundaryGeometry(result, nodes);
|
||||
result = ElkEdgePostProcessor.AvoidNodeCrossings(result, nodes, layoutOptions.Direction);
|
||||
result = ElkEdgePostProcessor.FinalizeGatewayBoundaryGeometry(result, nodes);
|
||||
result = ElkEdgePostProcessor.RepairBoundaryAnglesAndTargetApproaches(result, nodes, minLineClearance);
|
||||
result = ElkEdgePostProcessor.SpreadTargetApproachJoins(result, nodes, minLineClearance);
|
||||
result = ElkEdgePostProcessor.NormalizeBoundaryAngles(result, nodes);
|
||||
result = ElkEdgePostProcessor.NormalizeSourceExitAngles(result, nodes);
|
||||
result = ElkEdgePostProcessor.SpreadSourceDepartureJoins(result, nodes, minLineClearance);
|
||||
result = ElkEdgePostProcessor.AvoidNodeCrossings(result, nodes, layoutOptions.Direction);
|
||||
result = ElkEdgePostProcessor.FinalizeGatewayBoundaryGeometry(result, nodes);
|
||||
result = ElkEdgePostProcessor.AvoidNodeCrossings(result, nodes, layoutOptions.Direction);
|
||||
result = ElkEdgePostProcessor.RepairBoundaryAnglesAndTargetApproaches(result, nodes, minLineClearance);
|
||||
result = ElkEdgePostProcessor.AvoidNodeCrossings(result, nodes, layoutOptions.Direction);
|
||||
result = ElkEdgePostProcessor.FinalizeGatewayBoundaryGeometry(result, nodes);
|
||||
result = ElkEdgePostProcessor.NormalizeBoundaryAngles(result, nodes);
|
||||
result = ElkEdgePostProcessor.NormalizeSourceExitAngles(result, nodes);
|
||||
result = ElkEdgePostProcessor.SpreadSourceDepartureJoins(result, nodes, minLineClearance);
|
||||
result = ElkEdgePostProcessor.AvoidNodeCrossings(result, nodes, layoutOptions.Direction);
|
||||
result = ElkEdgePostProcessor.FinalizeGatewayBoundaryGeometry(result, nodes);
|
||||
result = ElkEdgePostProcessor.SpreadTargetApproachJoins(result, nodes, minLineClearance);
|
||||
result = ElkEdgePostProcessor.NormalizeBoundaryAngles(result, nodes);
|
||||
result = ElkEdgePostProcessor.NormalizeSourceExitAngles(result, nodes);
|
||||
result = ElkEdgePostProcessor.SpreadSourceDepartureJoins(result, nodes, minLineClearance);
|
||||
result = ElkEdgePostProcessor.AvoidNodeCrossings(result, nodes, layoutOptions.Direction);
|
||||
result = ElkEdgePostProcessor.FinalizeGatewayBoundaryGeometry(result, nodes);
|
||||
result = ElkEdgePostProcessor.RepairBoundaryAnglesAndTargetApproaches(result, nodes, minLineClearance);
|
||||
result = ElkEdgePostProcessor.NormalizeBoundaryAngles(result, nodes);
|
||||
result = ElkEdgePostProcessor.NormalizeSourceExitAngles(result, nodes);
|
||||
result = ElkEdgePostProcessor.SpreadSourceDepartureJoins(result, nodes, minLineClearance);
|
||||
result = ElkEdgePostProcessor.FinalizeGatewayBoundaryGeometry(result, nodes);
|
||||
result = ElkEdgePostProcessor.NormalizeBoundaryAngles(result, nodes);
|
||||
result = ElkEdgePostProcessor.FinalizeGatewayBoundaryGeometry(result, nodes);
|
||||
result = ClampBelowGraphEdges(result, nodes);
|
||||
result = ElkEdgePostProcessor.NormalizeBoundaryAngles(result, nodes);
|
||||
result = ElkEdgePostProcessor.NormalizeSourceExitAngles(result, nodes);
|
||||
result = ElkEdgePostProcessor.RepairBoundaryAnglesAndTargetApproaches(result, nodes, minLineClearance);
|
||||
result = ElkEdgePostProcessor.SpreadTargetApproachJoins(result, nodes, minLineClearance);
|
||||
result = ElkEdgePostProcessor.NormalizeBoundaryAngles(result, nodes);
|
||||
result = ElkEdgePostProcessor.NormalizeSourceExitAngles(result, nodes);
|
||||
result = ElkEdgePostProcessor.RepairBoundaryAnglesAndTargetApproaches(result, nodes, minLineClearance);
|
||||
result = ElkEdgePostProcessor.SpreadTargetApproachJoins(result, nodes, minLineClearance);
|
||||
result = ElkEdgePostProcessor.NormalizeBoundaryAngles(result, nodes);
|
||||
result = ElkEdgePostProcessor.NormalizeSourceExitAngles(result, nodes);
|
||||
result = ElkEdgePostProcessor.RepairBoundaryAnglesAndTargetApproaches(result, nodes, minLineClearance);
|
||||
result = ElkEdgePostProcessor.NormalizeBoundaryAngles(result, nodes);
|
||||
result = ElkEdgePostProcessor.FinalizeDecisionTargetEntries(result, nodes);
|
||||
result = ElkEdgePostProcessor.NormalizeBoundaryAngles(result, nodes);
|
||||
result = ElkEdgePostProcessor.SpreadTargetApproachJoins(result, nodes, minLineClearance);
|
||||
result = ElkEdgePostProcessor.FinalizeGatewayBoundaryGeometry(result, nodes);
|
||||
result = ElkEdgePostProcessor.NormalizeBoundaryAngles(result, nodes);
|
||||
result = ElkEdgePostProcessor.SpreadTargetApproachJoins(result, nodes, minLineClearance);
|
||||
result = ElkEdgePostProcessor.FinalizeGatewayBoundaryGeometry(result, nodes);
|
||||
result = ElkEdgePostProcessor.FinalizeDecisionTargetEntries(result, nodes);
|
||||
result = ElkEdgePostProcessor.SpreadRectTargetApproachFeederBands(result, nodes, minLineClearance);
|
||||
result = ElkEdgePostProcessor.SeparateRepeatCollectorLocalLaneConflicts(result, nodes, minLineClearance);
|
||||
result = ClampBelowGraphEdges(result, nodes);
|
||||
result = ElkEdgePostProcessor.AvoidNodeCrossings(result, nodes, layoutOptions.Direction);
|
||||
result = ElkEdgePostProcessor.RepairBoundaryAnglesAndTargetApproaches(result, nodes, minLineClearance);
|
||||
result = ElkEdgePostProcessor.SpreadTargetApproachJoins(result, nodes, minLineClearance);
|
||||
result = ElkEdgePostProcessor.NormalizeSourceExitAngles(result, nodes);
|
||||
result = ElkEdgePostProcessor.SpreadSourceDepartureJoins(result, nodes, minLineClearance);
|
||||
result = ElkEdgePostProcessor.SpreadRectTargetApproachFeederBands(result, nodes, minLineClearance);
|
||||
result = ElkEdgePostProcessor.ElevateRepeatCollectorNodeClearanceViolations(result, nodes, minLineClearance);
|
||||
result = ElkRepeatCollectorCorridors.SeparateSharedLanes(result, nodes);
|
||||
result = ElkEdgePostProcessor.SeparateRepeatCollectorLocalLaneConflicts(result, nodes, minLineClearance);
|
||||
result = ElkEdgePostProcessor.SeparateSharedLaneConflicts(result, nodes, minLineClearance);
|
||||
result = ElkEdgePostProcessor.RepairBoundaryAnglesAndTargetApproaches(result, nodes, minLineClearance);
|
||||
result = ElkEdgePostProcessor.SpreadTargetApproachJoins(result, nodes, minLineClearance);
|
||||
result = ElkEdgePostProcessor.SeparateSharedLaneConflicts(result, nodes, minLineClearance);
|
||||
result = ElkEdgePostProcessor.PreferShortestBoundaryShortcuts(result, nodes);
|
||||
result = ElkEdgePostProcessor.NormalizeBoundaryAngles(result, nodes);
|
||||
result = ElkEdgePostProcessor.NormalizeSourceExitAngles(result, nodes);
|
||||
result = ElkEdgePostProcessor.SpreadSourceDepartureJoins(result, nodes, minLineClearance);
|
||||
result = ElkEdgePostProcessor.SeparateMixedNodeFaceLaneConflicts(result, nodes, minLineClearance);
|
||||
result = ElkEdgePostProcessor.RepairBoundaryAnglesAndTargetApproaches(result, nodes, minLineClearance);
|
||||
result = ElkEdgePostProcessor.SpreadTargetApproachJoins(result, nodes, minLineClearance);
|
||||
result = ElkEdgePostProcessor.NormalizeBoundaryAngles(result, nodes);
|
||||
result = ElkEdgePostProcessor.NormalizeSourceExitAngles(result, nodes);
|
||||
// The final hard-rule closure must end on lane separation so later
|
||||
// boundary slot normalizers cannot collapse a repaired handoff strip
|
||||
// back onto the same effective rail.
|
||||
result = ElkEdgePostProcessor.SeparateMixedNodeFaceLaneConflicts(result, nodes, minLineClearance);
|
||||
result = ElkEdgePostProcessor.SeparateSharedLaneConflicts(result, nodes, minLineClearance);
|
||||
result = ClampBelowGraphEdges(result, nodes);
|
||||
result = ElkEdgePostProcessor.SnapBoundarySlotAssignments(
|
||||
result,
|
||||
nodes,
|
||||
minLineClearance,
|
||||
enforceAllNodeEndpoints: true);
|
||||
result = ApplyPostSlotDetourClosure(result, nodes, minLineClearance);
|
||||
result = ElkEdgePostProcessor.SnapBoundarySlotAssignments(
|
||||
result,
|
||||
nodes,
|
||||
minLineClearance,
|
||||
enforceAllNodeEndpoints: true);
|
||||
|
||||
var score = ElkEdgeRoutingScoring.ComputeScore(result, nodes);
|
||||
var remainingBrokenHighways = HighwayProcessingEnabled
|
||||
? ElkEdgeRouterHighway.DetectRemainingBrokenHighways(result, nodes).Count
|
||||
: 0;
|
||||
var retryState = BuildRetryState(score, remainingBrokenHighways);
|
||||
if (retryState.RequiresBlockingRetry || retryState.RequiresLengthRetry)
|
||||
{
|
||||
var stabilized = ApplyTerminalRuleCleanupRound(
|
||||
result,
|
||||
nodes,
|
||||
layoutOptions.Direction,
|
||||
minLineClearance);
|
||||
var stabilizedScore = ElkEdgeRoutingScoring.ComputeScore(stabilized, nodes);
|
||||
var stabilizedBrokenHighways = HighwayProcessingEnabled
|
||||
? ElkEdgeRouterHighway.DetectRemainingBrokenHighways(stabilized, nodes).Count
|
||||
: 0;
|
||||
var stabilizedRetryState = BuildRetryState(stabilizedScore, stabilizedBrokenHighways);
|
||||
if (IsBetterBoundarySlotRepairCandidate(
|
||||
stabilizedScore,
|
||||
stabilizedRetryState,
|
||||
score,
|
||||
retryState))
|
||||
{
|
||||
result = stabilized;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static ElkRoutedEdge[] ApplyTerminalRuleCleanupRound(
|
||||
ElkRoutedEdge[] edges,
|
||||
ElkPositionedNode[] nodes,
|
||||
ElkLayoutDirection direction,
|
||||
double minLineClearance,
|
||||
IReadOnlyCollection<string>? restrictedEdgeIds = null)
|
||||
{
|
||||
var result = edges;
|
||||
result = ElkEdgePostProcessor.SeparateSharedLaneConflicts(result, nodes, minLineClearance, restrictedEdgeIds);
|
||||
result = ElkEdgePostProcessor.SeparateRepeatCollectorLocalLaneConflicts(result, nodes, minLineClearance, restrictedEdgeIds);
|
||||
result = ElkEdgePostProcessor.PreferShortestBoundaryShortcuts(result, nodes, restrictedEdgeIds);
|
||||
result = ElkEdgePostProcessor.NormalizeBoundaryAngles(result, nodes);
|
||||
result = ElkEdgePostProcessor.NormalizeSourceExitAngles(result, nodes);
|
||||
result = ElkEdgePostProcessor.ElevateRepeatCollectorNodeClearanceViolations(result, nodes, minLineClearance, restrictedEdgeIds);
|
||||
result = ElkRepeatCollectorCorridors.SeparateSharedLanes(result, nodes, restrictedEdgeIds);
|
||||
result = ElkEdgePostProcessor.SpreadSourceDepartureJoins(result, nodes, minLineClearance, restrictedEdgeIds);
|
||||
result = ElkEdgePostProcessor.SeparateMixedNodeFaceLaneConflicts(result, nodes, minLineClearance, restrictedEdgeIds);
|
||||
result = ElkEdgePostProcessor.RepairBoundaryAnglesAndTargetApproaches(result, nodes, minLineClearance, restrictedEdgeIds);
|
||||
result = ElkEdgePostProcessor.SpreadTargetApproachJoins(result, nodes, minLineClearance, restrictedEdgeIds);
|
||||
result = ElkEdgePostProcessor.SpreadRectTargetApproachFeederBands(result, nodes, minLineClearance, restrictedEdgeIds);
|
||||
result = ElkEdgePostProcessor.FinalizeDecisionTargetEntries(result, nodes, restrictedEdgeIds);
|
||||
result = ElkEdgePostProcessor.FinalizeGatewayBoundaryGeometry(result, nodes, restrictedEdgeIds);
|
||||
result = ClampBelowGraphEdges(result, nodes, restrictedEdgeIds);
|
||||
result = ElkEdgePostProcessor.AvoidNodeCrossings(result, nodes, direction, restrictedEdgeIds);
|
||||
result = ElkEdgePostProcessor.NormalizeBoundaryAngles(result, nodes);
|
||||
result = ElkEdgePostProcessor.NormalizeSourceExitAngles(result, nodes);
|
||||
result = ElkEdgePostProcessor.SeparateSharedLaneConflicts(result, nodes, minLineClearance, restrictedEdgeIds);
|
||||
result = ElkEdgePostProcessor.SpreadSourceDepartureJoins(result, nodes, minLineClearance, restrictedEdgeIds);
|
||||
result = ElkEdgePostProcessor.RepairBoundaryAnglesAndTargetApproaches(result, nodes, minLineClearance, restrictedEdgeIds);
|
||||
result = ElkEdgePostProcessor.SpreadTargetApproachJoins(result, nodes, minLineClearance, restrictedEdgeIds);
|
||||
result = ElkEdgePostProcessor.FinalizeGatewayBoundaryGeometry(result, nodes, restrictedEdgeIds);
|
||||
result = ClampBelowGraphEdges(result, nodes, restrictedEdgeIds);
|
||||
result = ElkEdgePostProcessor.AvoidNodeCrossings(result, nodes, direction, restrictedEdgeIds);
|
||||
result = ElkEdgePostProcessor.NormalizeBoundaryAngles(result, nodes);
|
||||
result = ElkEdgePostProcessor.NormalizeSourceExitAngles(result, nodes);
|
||||
// Final late-stage verification: source/target boundary normalization can collapse
|
||||
// lanes back onto the same node face, so restabilize the local geometry once more.
|
||||
result = ElkEdgePostProcessor.SpreadSourceDepartureJoins(result, nodes, minLineClearance, restrictedEdgeIds);
|
||||
result = ElkEdgePostProcessor.SeparateMixedNodeFaceLaneConflicts(result, nodes, minLineClearance, restrictedEdgeIds);
|
||||
result = ElkEdgePostProcessor.RepairBoundaryAnglesAndTargetApproaches(result, nodes, minLineClearance, restrictedEdgeIds);
|
||||
result = ElkEdgePostProcessor.SpreadTargetApproachJoins(result, nodes, minLineClearance, restrictedEdgeIds);
|
||||
result = ElkEdgePostProcessor.SpreadRectTargetApproachFeederBands(result, nodes, minLineClearance, restrictedEdgeIds);
|
||||
result = ElkEdgePostProcessor.SeparateSharedLaneConflicts(result, nodes, minLineClearance, restrictedEdgeIds);
|
||||
result = ClampBelowGraphEdges(result, nodes, restrictedEdgeIds);
|
||||
result = ElkEdgePostProcessor.AvoidNodeCrossings(result, nodes, direction, restrictedEdgeIds);
|
||||
result = ElkEdgePostProcessor.NormalizeBoundaryAngles(result, nodes);
|
||||
result = ElkEdgePostProcessor.NormalizeSourceExitAngles(result, nodes);
|
||||
result = ElkEdgePostProcessor.SpreadSourceDepartureJoins(result, nodes, minLineClearance, restrictedEdgeIds);
|
||||
result = ElkEdgePostProcessor.SeparateMixedNodeFaceLaneConflicts(result, nodes, minLineClearance, restrictedEdgeIds);
|
||||
result = ElkEdgePostProcessor.RepairBoundaryAnglesAndTargetApproaches(result, nodes, minLineClearance, restrictedEdgeIds);
|
||||
result = ElkEdgePostProcessor.SpreadTargetApproachJoins(result, nodes, minLineClearance, restrictedEdgeIds);
|
||||
result = ElkEdgePostProcessor.SpreadRectTargetApproachFeederBands(result, nodes, minLineClearance, restrictedEdgeIds);
|
||||
result = ElkEdgePostProcessor.SeparateSharedLaneConflicts(result, nodes, minLineClearance, restrictedEdgeIds);
|
||||
result = ElkEdgePostProcessor.NormalizeBoundaryAngles(result, nodes);
|
||||
result = ElkEdgePostProcessor.NormalizeSourceExitAngles(result, nodes);
|
||||
result = ElkEdgePostProcessor.RepairBoundaryAnglesAndTargetApproaches(result, nodes, minLineClearance, restrictedEdgeIds);
|
||||
result = ElkEdgePostProcessor.SpreadTargetApproachJoins(result, nodes, minLineClearance, restrictedEdgeIds);
|
||||
result = ElkEdgePostProcessor.SeparateMixedNodeFaceLaneConflicts(result, nodes, minLineClearance, restrictedEdgeIds);
|
||||
result = ElkEdgePostProcessor.SeparateSharedLaneConflicts(result, nodes, minLineClearance, restrictedEdgeIds);
|
||||
result = ElkEdgePostProcessor.ElevateRepeatCollectorNodeClearanceViolations(result, nodes, minLineClearance, restrictedEdgeIds);
|
||||
result = ElkEdgePostProcessor.ElevateUnderNodeViolations(result, nodes, minLineClearance, restrictedEdgeIds);
|
||||
result = ElkEdgePostProcessor.PreferShortestBoundaryShortcuts(result, nodes, restrictedEdgeIds);
|
||||
result = ElkEdgePostProcessor.RepairBoundaryAnglesAndTargetApproaches(result, nodes, minLineClearance, restrictedEdgeIds);
|
||||
result = ElkEdgePostProcessor.SpreadTargetApproachJoins(result, nodes, minLineClearance, restrictedEdgeIds);
|
||||
result = ElkEdgePostProcessor.SpreadRectTargetApproachFeederBands(result, nodes, minLineClearance, restrictedEdgeIds);
|
||||
result = ElkEdgePostProcessor.ElevateUnderNodeViolations(result, nodes, minLineClearance, restrictedEdgeIds);
|
||||
result = ElkEdgePostProcessor.FinalizeGatewayBoundaryGeometry(result, nodes, restrictedEdgeIds);
|
||||
result = ClampBelowGraphEdges(result, nodes, restrictedEdgeIds);
|
||||
result = ElkEdgePostProcessor.AvoidNodeCrossings(result, nodes, direction, restrictedEdgeIds);
|
||||
result = ElkEdgePostProcessor.NormalizeBoundaryAngles(result, nodes);
|
||||
result = ElkEdgePostProcessor.NormalizeSourceExitAngles(result, nodes);
|
||||
result = ElkEdgePostProcessor.RepairBoundaryAnglesAndTargetApproaches(result, nodes, minLineClearance, restrictedEdgeIds);
|
||||
result = ElkEdgePostProcessor.SpreadTargetApproachJoins(result, nodes, minLineClearance, restrictedEdgeIds);
|
||||
result = ElkEdgePostProcessor.SpreadRectTargetApproachFeederBands(result, nodes, minLineClearance, restrictedEdgeIds);
|
||||
result = ElkEdgePostProcessor.ElevateUnderNodeViolations(result, nodes, minLineClearance, restrictedEdgeIds);
|
||||
result = ElkEdgePostProcessor.NormalizeBoundaryAngles(result, nodes);
|
||||
// Final hard-rule restabilization after the last normalize pass: the final
|
||||
// boundary normalization can still pull target slots and horizontal lanes back
|
||||
// into a bad state, so re-apply the local rule fixers once more before scoring.
|
||||
result = ElkEdgePostProcessor.NormalizeSourceExitAngles(result, nodes);
|
||||
result = ElkEdgePostProcessor.RepairBoundaryAnglesAndTargetApproaches(result, nodes, minLineClearance, restrictedEdgeIds);
|
||||
result = ElkEdgePostProcessor.SpreadTargetApproachJoins(result, nodes, minLineClearance, restrictedEdgeIds);
|
||||
result = ElkEdgePostProcessor.SpreadRectTargetApproachFeederBands(result, nodes, minLineClearance, restrictedEdgeIds);
|
||||
result = ElkEdgePostProcessor.ElevateRepeatCollectorNodeClearanceViolations(result, nodes, minLineClearance, restrictedEdgeIds);
|
||||
result = ElkEdgePostProcessor.ElevateUnderNodeViolations(result, nodes, minLineClearance, restrictedEdgeIds);
|
||||
result = ClampBelowGraphEdges(result, nodes, restrictedEdgeIds);
|
||||
result = ElkEdgePostProcessor.AvoidNodeCrossings(result, nodes, direction, restrictedEdgeIds);
|
||||
result = ElkEdgePostProcessor.NormalizeBoundaryAngles(result, nodes);
|
||||
result = CloseRemainingTerminalViolations(result, nodes, direction, minLineClearance, restrictedEdgeIds);
|
||||
var lateDetourShortcuts = ElkEdgePostProcessor.PreferShortestBoundaryShortcuts(result, nodes, restrictedEdgeIds);
|
||||
result = ElkEdgeRoutingScoring.CountBoundarySlotViolations(result, nodes) > 0
|
||||
? ChoosePreferredBoundarySlotRepairLayout(result, lateDetourShortcuts, nodes)
|
||||
: ChoosePreferredHardRuleLayout(result, lateDetourShortcuts, nodes);
|
||||
result = ApplyFinalDetourPolish(result, nodes, minLineClearance, restrictedEdgeIds);
|
||||
result = ElkEdgePostProcessor.SnapBoundarySlotAssignments(
|
||||
result,
|
||||
nodes,
|
||||
minLineClearance,
|
||||
restrictedEdgeIds,
|
||||
enforceAllNodeEndpoints: true);
|
||||
result = ApplyPostSlotDetourClosure(result, nodes, minLineClearance, restrictedEdgeIds);
|
||||
result = ElkEdgePostProcessor.SnapBoundarySlotAssignments(
|
||||
result,
|
||||
nodes,
|
||||
minLineClearance,
|
||||
restrictedEdgeIds,
|
||||
enforceAllNodeEndpoints: true);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static ElkRoutedEdge[] ApplyFinalDetourPolish(
|
||||
ElkRoutedEdge[] edges,
|
||||
ElkPositionedNode[] nodes,
|
||||
double minLineClearance,
|
||||
IReadOnlyCollection<string>? restrictedEdgeIds)
|
||||
{
|
||||
var restrictedSet = restrictedEdgeIds is null
|
||||
? null
|
||||
: restrictedEdgeIds.ToHashSet(StringComparer.Ordinal);
|
||||
var result = edges;
|
||||
|
||||
for (var round = 0; round < 3; round++)
|
||||
{
|
||||
var detourSeverity = new Dictionary<string, int>(StringComparer.Ordinal);
|
||||
ElkEdgeRoutingScoring.CountExcessiveDetourViolations(result, nodes, detourSeverity, 10);
|
||||
if (detourSeverity.Count == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var currentScore = ElkEdgeRoutingScoring.ComputeScore(result, nodes);
|
||||
var currentRetryState = BuildRetryState(
|
||||
currentScore,
|
||||
HighwayProcessingEnabled
|
||||
? ElkEdgeRouterHighway.DetectRemainingBrokenHighways(result, nodes).Count
|
||||
: 0);
|
||||
|
||||
var improved = false;
|
||||
foreach (var edgeId in detourSeverity
|
||||
.OrderByDescending(pair => pair.Value)
|
||||
.ThenBy(pair => pair.Key, StringComparer.Ordinal)
|
||||
.Select(pair => pair.Key))
|
||||
{
|
||||
if (restrictedSet is not null && !restrictedSet.Contains(edgeId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var focused = (IReadOnlyCollection<string>)[edgeId];
|
||||
var candidateEdges = ComposeTransactionalFinalDetourCandidate(
|
||||
result,
|
||||
nodes,
|
||||
minLineClearance,
|
||||
focused);
|
||||
candidateEdges = ChoosePreferredHardRuleLayout(result, candidateEdges, nodes);
|
||||
if (ReferenceEquals(candidateEdges, result))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var candidateScore = ElkEdgeRoutingScoring.ComputeScore(candidateEdges, nodes);
|
||||
var candidateRetryState = BuildRetryState(
|
||||
candidateScore,
|
||||
HighwayProcessingEnabled
|
||||
? ElkEdgeRouterHighway.DetectRemainingBrokenHighways(candidateEdges, nodes).Count
|
||||
: 0);
|
||||
|
||||
var improvedDetours = candidateRetryState.ExcessiveDetourViolations < currentRetryState.ExcessiveDetourViolations;
|
||||
if (HasHardRuleRegression(candidateRetryState, currentRetryState)
|
||||
|| (!improvedDetours
|
||||
&& !IsBetterBoundarySlotRepairCandidate(
|
||||
candidateScore,
|
||||
candidateRetryState,
|
||||
currentScore,
|
||||
currentRetryState)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
result = candidateEdges;
|
||||
improved = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!improved)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static bool TryPromoteFinalDetourCandidate(
|
||||
ElkRoutedEdge[] baselineEdges,
|
||||
ElkRoutedEdge[] candidateEdges,
|
||||
ElkPositionedNode[] nodes,
|
||||
EdgeRoutingScore baselineScore,
|
||||
RoutingRetryState baselineRetryState,
|
||||
out ElkRoutedEdge[] promotedEdges)
|
||||
{
|
||||
promotedEdges = baselineEdges;
|
||||
if (ReferenceEquals(candidateEdges, baselineEdges))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var candidateScore = ElkEdgeRoutingScoring.ComputeScore(candidateEdges, nodes);
|
||||
var candidateRetryState = BuildRetryState(
|
||||
candidateScore,
|
||||
HighwayProcessingEnabled
|
||||
? ElkEdgeRouterHighway.DetectRemainingBrokenHighways(candidateEdges, nodes).Count
|
||||
: 0);
|
||||
|
||||
var improvedDetours = candidateRetryState.ExcessiveDetourViolations < baselineRetryState.ExcessiveDetourViolations;
|
||||
var improvedGatewaySource = candidateRetryState.GatewaySourceExitViolations < baselineRetryState.GatewaySourceExitViolations;
|
||||
if (HasHardRuleRegression(candidateRetryState, baselineRetryState)
|
||||
|| (!(improvedDetours || improvedGatewaySource)
|
||||
&& !IsBetterBoundarySlotRepairCandidate(
|
||||
candidateScore,
|
||||
candidateRetryState,
|
||||
baselineScore,
|
||||
baselineRetryState)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
promotedEdges = candidateEdges;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static ElkRoutedEdge[] ComposeTransactionalFinalDetourCandidate(
|
||||
ElkRoutedEdge[] baseline,
|
||||
ElkPositionedNode[] nodes,
|
||||
double minLineClearance,
|
||||
IReadOnlyCollection<string> focusedEdgeIds)
|
||||
{
|
||||
var candidate = ElkEdgePostProcessor.PreferShortestBoundaryShortcuts(baseline, nodes, focusedEdgeIds);
|
||||
if (ReferenceEquals(candidate, baseline))
|
||||
{
|
||||
return baseline;
|
||||
}
|
||||
|
||||
candidate = ElkEdgePostProcessor.RepairBoundaryAnglesAndTargetApproaches(candidate, nodes, minLineClearance, focusedEdgeIds);
|
||||
candidate = ElkEdgePostProcessor.SpreadTargetApproachJoins(candidate, nodes, minLineClearance, focusedEdgeIds);
|
||||
candidate = ElkEdgePostProcessor.SpreadRectTargetApproachFeederBands(candidate, nodes, minLineClearance, focusedEdgeIds);
|
||||
candidate = ElkEdgePostProcessor.ElevateUnderNodeViolations(candidate, nodes, minLineClearance, focusedEdgeIds);
|
||||
candidate = ClampBelowGraphEdges(candidate, nodes, focusedEdgeIds);
|
||||
candidate = ElkEdgePostProcessor.RepairBoundaryAnglesAndTargetApproaches(candidate, nodes, minLineClearance, focusedEdgeIds);
|
||||
candidate = ElkEdgePostProcessor.SpreadTargetApproachJoins(candidate, nodes, minLineClearance, focusedEdgeIds);
|
||||
candidate = ElkEdgePostProcessor.SpreadRectTargetApproachFeederBands(candidate, nodes, minLineClearance, focusedEdgeIds);
|
||||
candidate = ElkEdgePostProcessor.FinalizeDecisionTargetEntries(candidate, nodes, focusedEdgeIds);
|
||||
candidate = ElkEdgePostProcessor.FinalizeGatewayBoundaryGeometry(candidate, nodes, focusedEdgeIds);
|
||||
candidate = ElkEdgePostProcessor.PolishTargetPeerConflicts(candidate, nodes, minLineClearance, focusedEdgeIds);
|
||||
candidate = ElkEdgePostProcessor.SnapBoundarySlotAssignments(
|
||||
candidate,
|
||||
nodes,
|
||||
minLineClearance,
|
||||
focusedEdgeIds,
|
||||
enforceAllNodeEndpoints: true);
|
||||
candidate = ApplyPostSlotDetourClosure(candidate, nodes, minLineClearance, focusedEdgeIds);
|
||||
candidate = ApplyLateBoundarySlotRestabilization(candidate, nodes, minLineClearance, focusedEdgeIds);
|
||||
candidate = ElkEdgePostProcessor.SeparateMixedNodeFaceLaneConflicts(candidate, nodes, minLineClearance, focusedEdgeIds);
|
||||
candidate = ElkEdgePostProcessor.SeparateSharedLaneConflicts(candidate, nodes, minLineClearance, focusedEdgeIds);
|
||||
candidate = ElkEdgePostProcessor.SnapBoundarySlotAssignments(
|
||||
candidate,
|
||||
nodes,
|
||||
minLineClearance,
|
||||
focusedEdgeIds,
|
||||
enforceAllNodeEndpoints: true);
|
||||
candidate = ApplyPostSlotDetourClosure(candidate, nodes, minLineClearance, focusedEdgeIds);
|
||||
return candidate;
|
||||
}
|
||||
|
||||
internal static ElkRoutedEdge[] ApplyPostSlotDetourClosure(
|
||||
ElkRoutedEdge[] edges,
|
||||
ElkPositionedNode[] nodes,
|
||||
double minLineClearance,
|
||||
IReadOnlyCollection<string>? restrictedEdgeIds = null)
|
||||
{
|
||||
var candidate = ElkEdgePostProcessor.PreferShortestBoundaryShortcuts(edges, nodes, restrictedEdgeIds);
|
||||
if (ReferenceEquals(candidate, edges))
|
||||
{
|
||||
return edges;
|
||||
}
|
||||
|
||||
candidate = ElkEdgePostProcessor.NormalizeBoundaryAngles(candidate, nodes);
|
||||
candidate = ElkEdgePostProcessor.NormalizeSourceExitAngles(candidate, nodes);
|
||||
candidate = ElkEdgePostProcessor.SpreadSourceDepartureJoins(candidate, nodes, minLineClearance, restrictedEdgeIds);
|
||||
candidate = ElkEdgePostProcessor.RepairBoundaryAnglesAndTargetApproaches(candidate, nodes, minLineClearance, restrictedEdgeIds);
|
||||
candidate = ElkEdgePostProcessor.SpreadTargetApproachJoins(candidate, nodes, minLineClearance, restrictedEdgeIds);
|
||||
candidate = ElkEdgePostProcessor.SpreadRectTargetApproachFeederBands(candidate, nodes, minLineClearance, restrictedEdgeIds);
|
||||
candidate = ElkEdgePostProcessor.FinalizeDecisionTargetEntries(candidate, nodes, restrictedEdgeIds);
|
||||
candidate = ElkEdgePostProcessor.FinalizeGatewayBoundaryGeometry(candidate, nodes, restrictedEdgeIds);
|
||||
candidate = ElkEdgePostProcessor.PolishTargetPeerConflicts(candidate, nodes, minLineClearance, restrictedEdgeIds);
|
||||
candidate = ElkEdgePostProcessor.ElevateUnderNodeViolations(candidate, nodes, minLineClearance, restrictedEdgeIds);
|
||||
candidate = ClampBelowGraphEdges(candidate, nodes, restrictedEdgeIds);
|
||||
candidate = ElkEdgePostProcessor.NormalizeBoundaryAngles(candidate, nodes);
|
||||
candidate = ElkEdgePostProcessor.NormalizeSourceExitAngles(candidate, nodes);
|
||||
candidate = ElkEdgePostProcessor.SeparateMixedNodeFaceLaneConflicts(candidate, nodes, minLineClearance, restrictedEdgeIds);
|
||||
candidate = ElkEdgePostProcessor.SeparateSharedLaneConflicts(candidate, nodes, minLineClearance, restrictedEdgeIds);
|
||||
candidate = ElkEdgePostProcessor.SnapBoundarySlotAssignments(
|
||||
candidate,
|
||||
nodes,
|
||||
minLineClearance,
|
||||
restrictedEdgeIds,
|
||||
enforceAllNodeEndpoints: true);
|
||||
|
||||
return ElkEdgeRoutingScoring.CountBoundarySlotViolations(edges, nodes) > 0
|
||||
? ChoosePreferredBoundarySlotRepairLayout(edges, candidate, nodes)
|
||||
: ChoosePreferredHardRuleLayout(edges, candidate, nodes);
|
||||
}
|
||||
|
||||
private static ElkRoutedEdge[] CloseRemainingTerminalViolations(
|
||||
ElkRoutedEdge[] edges,
|
||||
ElkPositionedNode[] nodes,
|
||||
ElkLayoutDirection direction,
|
||||
double minLineClearance,
|
||||
IReadOnlyCollection<string>? restrictedEdgeIds)
|
||||
{
|
||||
var result = edges;
|
||||
var restrictedSet = restrictedEdgeIds is null
|
||||
? null
|
||||
: restrictedEdgeIds.ToHashSet(StringComparer.Ordinal);
|
||||
ElkLayoutDiagnostics.LogProgress(
|
||||
$"Terminal closure start: restricted={restrictedEdgeIds?.Count ?? 0}");
|
||||
|
||||
for (var round = 0; round < 4; round++)
|
||||
{
|
||||
ElkLayoutDiagnostics.LogProgress($"Terminal closure round {round + 1} pressure scan start");
|
||||
var severityByEdgeId = new Dictionary<string, int>(StringComparer.Ordinal);
|
||||
var previousHardPressure =
|
||||
ElkEdgeRoutingScoring.CountBadBoundaryAngles(result, nodes, severityByEdgeId, 10)
|
||||
+ ElkEdgeRoutingScoring.CountGatewaySourceExitViolations(result, nodes, severityByEdgeId, 10)
|
||||
+ ElkEdgeRoutingScoring.CountTargetApproachJoinViolations(result, nodes, severityByEdgeId, 10)
|
||||
+ ElkEdgeRoutingScoring.CountSharedLaneViolations(result, nodes, severityByEdgeId, 10)
|
||||
+ ElkEdgeRoutingScoring.CountBoundarySlotViolations(result, nodes, severityByEdgeId, 10)
|
||||
+ ElkEdgeRoutingScoring.CountBelowGraphViolations(result, nodes, severityByEdgeId, 10)
|
||||
+ ElkEdgeRoutingScoring.CountUnderNodeViolations(result, nodes, severityByEdgeId, 10);
|
||||
var previousLengthPressure = 0;
|
||||
if (previousHardPressure == 0)
|
||||
{
|
||||
previousLengthPressure =
|
||||
ElkEdgeRoutingScoring.CountTargetApproachBacktrackingViolations(result, nodes, severityByEdgeId, 10)
|
||||
+ ElkEdgeRoutingScoring.CountExcessiveDetourViolations(result, nodes, severityByEdgeId, 10);
|
||||
}
|
||||
ElkLayoutDiagnostics.LogProgress(
|
||||
$"Terminal closure round {round + 1} pressure scan done: hard={previousHardPressure} length={previousLengthPressure} severity={severityByEdgeId.Count}");
|
||||
|
||||
var previousScore = ElkEdgeRoutingScoring.ComputeScore(result, nodes);
|
||||
var previousRetryState = BuildRetryState(
|
||||
previousScore,
|
||||
HighwayProcessingEnabled
|
||||
? ElkEdgeRouterHighway.DetectRemainingBrokenHighways(result, nodes).Count
|
||||
: 0);
|
||||
ElkLayoutDiagnostics.LogProgress(
|
||||
$"Terminal closure round {round + 1} retry state ready: {DescribeRetryState(previousRetryState)}");
|
||||
if (previousHardPressure == 0 && previousLengthPressure == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var focusEdgeIds = severityByEdgeId.Keys
|
||||
.Where(edgeId => restrictedSet is null || restrictedSet.Contains(edgeId))
|
||||
.OrderBy(edgeId => edgeId, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
if (focusEdgeIds.Length == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
ElkLayoutDiagnostics.LogProgress(
|
||||
$"Terminal closure round {round + 1} focus ready: count={focusEdgeIds.Length}");
|
||||
|
||||
var focused = (IReadOnlyCollection<string>)focusEdgeIds;
|
||||
var candidate = result;
|
||||
if (previousHardPressure > 0
|
||||
&& ShouldPreferCompactFocusedTerminalClosure(previousRetryState, focusEdgeIds.Length))
|
||||
{
|
||||
ElkLayoutDiagnostics.LogProgress(
|
||||
$"Terminal closure round {round + 1} compact hard-pass start");
|
||||
candidate = ApplyCompactFocusedTerminalClosure(
|
||||
candidate,
|
||||
nodes,
|
||||
direction,
|
||||
minLineClearance,
|
||||
focused);
|
||||
ElkLayoutDiagnostics.LogProgress(
|
||||
$"Terminal closure round {round + 1} compact hard-pass complete");
|
||||
}
|
||||
else if (previousHardPressure > 0)
|
||||
{
|
||||
ElkLayoutDiagnostics.LogProgress($"Terminal closure round {round + 1} hard-pass block start");
|
||||
candidate = ApplyGuardedFocusedHardRulePass(candidate, nodes, current => ElkEdgePostProcessor.NormalizeBoundaryAngles(current, nodes));
|
||||
ElkLayoutDiagnostics.LogProgress($"Terminal closure round {round + 1} after normalize-boundary");
|
||||
candidate = ApplyGuardedFocusedHardRulePass(candidate, nodes, current => ElkEdgePostProcessor.NormalizeSourceExitAngles(current, nodes));
|
||||
ElkLayoutDiagnostics.LogProgress($"Terminal closure round {round + 1} after normalize-source-exit");
|
||||
candidate = ApplyGuardedFocusedHardRulePass(candidate, nodes, current => ElkEdgePostProcessor.SeparateSharedLaneConflicts(current, nodes, minLineClearance, focused));
|
||||
ElkLayoutDiagnostics.LogProgress($"Terminal closure round {round + 1} after separate-shared-lanes-1");
|
||||
candidate = ApplyGuardedFocusedHardRulePass(candidate, nodes, current => ElkEdgePostProcessor.SeparateRepeatCollectorLocalLaneConflicts(current, nodes, minLineClearance, focused));
|
||||
ElkLayoutDiagnostics.LogProgress($"Terminal closure round {round + 1} after separate-repeat-collector-1");
|
||||
candidate = ApplyGuardedFocusedHardRulePass(candidate, nodes, current => ElkRepeatCollectorCorridors.SeparateSharedLanes(current, nodes, focused));
|
||||
ElkLayoutDiagnostics.LogProgress($"Terminal closure round {round + 1} after repeat-collector-shared-lanes-1");
|
||||
candidate = ApplyGuardedFocusedHardRulePass(candidate, nodes, current => ElkEdgePostProcessor.SpreadSourceDepartureJoins(current, nodes, minLineClearance, focused));
|
||||
ElkLayoutDiagnostics.LogProgress($"Terminal closure round {round + 1} after spread-source-joins-1");
|
||||
candidate = ApplyGuardedFocusedHardRulePass(candidate, nodes, current => ElkEdgePostProcessor.SeparateMixedNodeFaceLaneConflicts(current, nodes, minLineClearance, focused));
|
||||
ElkLayoutDiagnostics.LogProgress($"Terminal closure round {round + 1} after separate-mixed-faces-1");
|
||||
candidate = ApplyGuardedFocusedHardRulePass(candidate, nodes, current => ElkEdgePostProcessor.ElevateUnderNodeViolations(current, nodes, minLineClearance, focused));
|
||||
ElkLayoutDiagnostics.LogProgress($"Terminal closure round {round + 1} after elevate-under-node-1");
|
||||
candidate = ApplyGuardedFocusedHardRulePass(candidate, nodes, current => ElkEdgePostProcessor.ElevateRepeatCollectorNodeClearanceViolations(current, nodes, minLineClearance, focused));
|
||||
ElkLayoutDiagnostics.LogProgress($"Terminal closure round {round + 1} after elevate-repeat-collector-clearance-1");
|
||||
candidate = ApplyGuardedFocusedHardRulePass(candidate, nodes, current => ElkEdgePostProcessor.PreferShortestBoundaryShortcuts(current, nodes, focused));
|
||||
ElkLayoutDiagnostics.LogProgress($"Terminal closure round {round + 1} after prefer-shortest-shortcuts-1");
|
||||
candidate = ApplyGuardedFocusedHardRulePass(candidate, nodes, current => ClampBelowGraphEdges(current, nodes, focused));
|
||||
ElkLayoutDiagnostics.LogProgress($"Terminal closure round {round + 1} after clamp-below-graph-1");
|
||||
candidate = ApplyGuardedFocusedHardRulePass(candidate, nodes, current => ElkEdgePostProcessor.RepairBoundaryAnglesAndTargetApproaches(current, nodes, minLineClearance, focused));
|
||||
ElkLayoutDiagnostics.LogProgress($"Terminal closure round {round + 1} after repair-boundary-target-1");
|
||||
candidate = ApplyGuardedFocusedHardRulePass(candidate, nodes, current => ElkEdgePostProcessor.SpreadTargetApproachJoins(current, nodes, minLineClearance, focused));
|
||||
ElkLayoutDiagnostics.LogProgress($"Terminal closure round {round + 1} after spread-target-joins-1");
|
||||
candidate = ApplyGuardedFocusedHardRulePass(candidate, nodes, current => ElkEdgePostProcessor.SpreadRectTargetApproachFeederBands(current, nodes, minLineClearance, focused));
|
||||
ElkLayoutDiagnostics.LogProgress($"Terminal closure round {round + 1} after spread-feeder-bands-1");
|
||||
candidate = ApplyGuardedFocusedHardRulePass(candidate, nodes, current => ElkEdgePostProcessor.FinalizeGatewayBoundaryGeometry(current, nodes, focused));
|
||||
ElkLayoutDiagnostics.LogProgress($"Terminal closure round {round + 1} after finalize-gateway-1");
|
||||
candidate = ApplyGuardedFocusedHardRulePass(candidate, nodes, current => ElkEdgePostProcessor.AvoidNodeCrossings(current, nodes, direction, focused));
|
||||
ElkLayoutDiagnostics.LogProgress($"Terminal closure round {round + 1} after avoid-node-crossings-1");
|
||||
candidate = ApplyGuardedFocusedHardRulePass(candidate, nodes, current => ElkEdgePostProcessor.ElevateUnderNodeViolations(current, nodes, minLineClearance, focused));
|
||||
ElkLayoutDiagnostics.LogProgress($"Terminal closure round {round + 1} after elevate-under-node-2");
|
||||
candidate = ApplyGuardedFocusedHardRulePass(candidate, nodes, current => ElkEdgePostProcessor.NormalizeBoundaryAngles(current, nodes));
|
||||
ElkLayoutDiagnostics.LogProgress($"Terminal closure round {round + 1} after normalize-boundary-2");
|
||||
candidate = ApplyGuardedFocusedHardRulePass(candidate, nodes, current => ElkEdgePostProcessor.NormalizeSourceExitAngles(current, nodes));
|
||||
ElkLayoutDiagnostics.LogProgress($"Terminal closure round {round + 1} after normalize-source-exit-2");
|
||||
candidate = ApplyGuardedFocusedHardRulePass(candidate, nodes, current => ElkEdgePostProcessor.SeparateSharedLaneConflicts(current, nodes, minLineClearance, focused));
|
||||
ElkLayoutDiagnostics.LogProgress($"Terminal closure round {round + 1} after separate-shared-lanes-2");
|
||||
candidate = ApplyGuardedFocusedHardRulePass(candidate, nodes, current => ElkEdgePostProcessor.SeparateRepeatCollectorLocalLaneConflicts(current, nodes, minLineClearance, focused));
|
||||
ElkLayoutDiagnostics.LogProgress($"Terminal closure round {round + 1} after separate-repeat-collector-2");
|
||||
candidate = ApplyGuardedFocusedHardRulePass(candidate, nodes, current => ElkRepeatCollectorCorridors.SeparateSharedLanes(current, nodes, focused));
|
||||
ElkLayoutDiagnostics.LogProgress($"Terminal closure round {round + 1} after repeat-collector-shared-lanes-2");
|
||||
candidate = ApplyGuardedFocusedHardRulePass(candidate, nodes, current => ElkEdgePostProcessor.NormalizeBoundaryAngles(current, nodes));
|
||||
ElkLayoutDiagnostics.LogProgress($"Terminal closure round {round + 1} after normalize-boundary-3");
|
||||
candidate = ApplyGuardedFocusedHardRulePass(candidate, nodes, current => ElkEdgePostProcessor.NormalizeSourceExitAngles(current, nodes));
|
||||
ElkLayoutDiagnostics.LogProgress($"Terminal closure round {round + 1} after normalize-source-exit-3");
|
||||
candidate = ApplyGuardedFocusedHardRulePass(candidate, nodes, current => ElkEdgePostProcessor.SpreadSourceDepartureJoins(current, nodes, minLineClearance, focused));
|
||||
ElkLayoutDiagnostics.LogProgress($"Terminal closure round {round + 1} after spread-source-joins-2");
|
||||
candidate = ApplyGuardedFocusedHardRulePass(candidate, nodes, current => ElkEdgePostProcessor.SeparateMixedNodeFaceLaneConflicts(current, nodes, minLineClearance, focused));
|
||||
ElkLayoutDiagnostics.LogProgress($"Terminal closure round {round + 1} after separate-mixed-faces-2");
|
||||
candidate = ApplyGuardedFocusedHardRulePass(candidate, nodes, current => ElkEdgePostProcessor.RepairBoundaryAnglesAndTargetApproaches(current, nodes, minLineClearance, focused));
|
||||
ElkLayoutDiagnostics.LogProgress($"Terminal closure round {round + 1} after repair-boundary-target-2");
|
||||
candidate = ApplyGuardedFocusedHardRulePass(candidate, nodes, current => ElkEdgePostProcessor.SpreadTargetApproachJoins(current, nodes, minLineClearance, focused));
|
||||
ElkLayoutDiagnostics.LogProgress($"Terminal closure round {round + 1} after spread-target-joins-2");
|
||||
candidate = ApplyGuardedFocusedHardRulePass(candidate, nodes, current => ElkEdgePostProcessor.SpreadRectTargetApproachFeederBands(current, nodes, minLineClearance, focused));
|
||||
ElkLayoutDiagnostics.LogProgress($"Terminal closure round {round + 1} after spread-feeder-bands-2");
|
||||
candidate = ApplyGuardedFocusedHardRulePass(candidate, nodes, current => ClampBelowGraphEdges(current, nodes, focused));
|
||||
ElkLayoutDiagnostics.LogProgress($"Terminal closure round {round + 1} after clamp-below-graph-2");
|
||||
candidate = ApplyGuardedFocusedHardRulePass(candidate, nodes, current => ElkEdgePostProcessor.AvoidNodeCrossings(current, nodes, direction, focused));
|
||||
ElkLayoutDiagnostics.LogProgress($"Terminal closure round {round + 1} after avoid-node-crossings-2");
|
||||
candidate = ApplyGuardedFocusedHardRulePass(candidate, nodes, current => ElkEdgePostProcessor.ElevateUnderNodeViolations(current, nodes, minLineClearance, focused));
|
||||
ElkLayoutDiagnostics.LogProgress($"Terminal closure round {round + 1} after elevate-under-node-3");
|
||||
candidate = ApplyGuardedFocusedHardRulePass(candidate, nodes, current => ElkEdgePostProcessor.NormalizeBoundaryAngles(current, nodes));
|
||||
ElkLayoutDiagnostics.LogProgress($"Terminal closure round {round + 1} after normalize-boundary-4");
|
||||
candidate = ApplyGuardedFocusedHardRulePass(candidate, nodes, current => ElkEdgePostProcessor.NormalizeSourceExitAngles(current, nodes));
|
||||
ElkLayoutDiagnostics.LogProgress($"Terminal closure round {round + 1} after normalize-source-exit-4");
|
||||
candidate = ApplyGuardedFocusedHardRulePass(candidate, nodes, current => ElkEdgePostProcessor.SpreadSourceDepartureJoins(current, nodes, minLineClearance, focused));
|
||||
ElkLayoutDiagnostics.LogProgress($"Terminal closure round {round + 1} after spread-source-joins-3");
|
||||
candidate = ApplyGuardedFocusedHardRulePass(candidate, nodes, current => ElkEdgePostProcessor.SeparateMixedNodeFaceLaneConflicts(current, nodes, minLineClearance, focused));
|
||||
ElkLayoutDiagnostics.LogProgress($"Terminal closure round {round + 1} after separate-mixed-faces-3");
|
||||
candidate = ApplyGuardedFocusedHardRulePass(candidate, nodes, current => ElkEdgePostProcessor.SeparateSharedLaneConflicts(current, nodes, minLineClearance, focused));
|
||||
ElkLayoutDiagnostics.LogProgress($"Terminal closure round {round + 1} after separate-shared-lanes-3");
|
||||
}
|
||||
else
|
||||
{
|
||||
ElkLayoutDiagnostics.LogProgress($"Terminal closure round {round + 1} length-pass block start");
|
||||
candidate = ApplyGuardedFocusedHardRulePass(candidate, nodes, current => ElkEdgePostProcessor.PreferShortestBoundaryShortcuts(current, nodes, focused));
|
||||
ElkLayoutDiagnostics.LogProgress($"Terminal closure round {round + 1} after prefer-shortest-shortcuts-length-1");
|
||||
candidate = ApplyGuardedFocusedHardRulePass(candidate, nodes, current => ElkEdgePostProcessor.RepairBoundaryAnglesAndTargetApproaches(current, nodes, minLineClearance, focused));
|
||||
ElkLayoutDiagnostics.LogProgress($"Terminal closure round {round + 1} after repair-boundary-target-length-1");
|
||||
candidate = ApplyGuardedFocusedHardRulePass(candidate, nodes, current => ElkEdgePostProcessor.SpreadTargetApproachJoins(current, nodes, minLineClearance, focused));
|
||||
ElkLayoutDiagnostics.LogProgress($"Terminal closure round {round + 1} after spread-target-joins-length-1");
|
||||
candidate = ApplyGuardedFocusedHardRulePass(candidate, nodes, current => ElkEdgePostProcessor.SpreadRectTargetApproachFeederBands(current, nodes, minLineClearance, focused));
|
||||
ElkLayoutDiagnostics.LogProgress($"Terminal closure round {round + 1} after spread-feeder-bands-length-1");
|
||||
candidate = ApplyGuardedFocusedHardRulePass(candidate, nodes, current => ElkEdgePostProcessor.FinalizeDecisionTargetEntries(current, nodes, focused));
|
||||
ElkLayoutDiagnostics.LogProgress($"Terminal closure round {round + 1} after finalize-decision-targets-length-1");
|
||||
candidate = ApplyGuardedFocusedHardRulePass(candidate, nodes, current => ElkEdgePostProcessor.FinalizeGatewayBoundaryGeometry(current, nodes, focused));
|
||||
ElkLayoutDiagnostics.LogProgress($"Terminal closure round {round + 1} after finalize-gateway-length-1");
|
||||
candidate = ApplyGuardedFocusedHardRulePass(candidate, nodes, current => ClampBelowGraphEdges(current, nodes, focused));
|
||||
ElkLayoutDiagnostics.LogProgress($"Terminal closure round {round + 1} after clamp-below-graph-length-1");
|
||||
candidate = ApplyGuardedFocusedHardRulePass(candidate, nodes, current => ElkEdgePostProcessor.AvoidNodeCrossings(current, nodes, direction, focused));
|
||||
ElkLayoutDiagnostics.LogProgress($"Terminal closure round {round + 1} after avoid-node-crossings-length-1");
|
||||
candidate = ApplyGuardedFocusedHardRulePass(candidate, nodes, current => ElkEdgePostProcessor.NormalizeBoundaryAngles(current, nodes));
|
||||
ElkLayoutDiagnostics.LogProgress($"Terminal closure round {round + 1} after normalize-boundary-length-1");
|
||||
candidate = ApplyGuardedFocusedHardRulePass(candidate, nodes, current => ElkEdgePostProcessor.NormalizeSourceExitAngles(current, nodes));
|
||||
ElkLayoutDiagnostics.LogProgress($"Terminal closure round {round + 1} after normalize-source-exit-length-1");
|
||||
candidate = ApplyGuardedFocusedHardRulePass(candidate, nodes, current => ElkEdgePostProcessor.PreferShortestBoundaryShortcuts(current, nodes, focused));
|
||||
ElkLayoutDiagnostics.LogProgress($"Terminal closure round {round + 1} after prefer-shortest-shortcuts-length-2");
|
||||
candidate = ApplyGuardedFocusedHardRulePass(candidate, nodes, current => ElkEdgePostProcessor.RepairBoundaryAnglesAndTargetApproaches(current, nodes, minLineClearance, focused));
|
||||
ElkLayoutDiagnostics.LogProgress($"Terminal closure round {round + 1} after repair-boundary-target-length-2");
|
||||
candidate = ApplyGuardedFocusedHardRulePass(candidate, nodes, current => ElkEdgePostProcessor.SpreadTargetApproachJoins(current, nodes, minLineClearance, focused));
|
||||
ElkLayoutDiagnostics.LogProgress($"Terminal closure round {round + 1} after spread-target-joins-length-2");
|
||||
candidate = ApplyGuardedFocusedHardRulePass(candidate, nodes, current => ElkEdgePostProcessor.FinalizeDecisionTargetEntries(current, nodes, focused));
|
||||
ElkLayoutDiagnostics.LogProgress($"Terminal closure round {round + 1} after finalize-decision-targets-length-2");
|
||||
candidate = ApplyGuardedFocusedHardRulePass(candidate, nodes, current => ElkEdgePostProcessor.FinalizeGatewayBoundaryGeometry(current, nodes, focused));
|
||||
ElkLayoutDiagnostics.LogProgress($"Terminal closure round {round + 1} after finalize-gateway-length-2");
|
||||
}
|
||||
|
||||
var currentHardPressure =
|
||||
ElkEdgeRoutingScoring.CountBadBoundaryAngles(candidate, nodes)
|
||||
+ ElkEdgeRoutingScoring.CountGatewaySourceExitViolations(candidate, nodes)
|
||||
+ ElkEdgeRoutingScoring.CountTargetApproachJoinViolations(candidate, nodes)
|
||||
+ ElkEdgeRoutingScoring.CountSharedLaneViolations(candidate, nodes)
|
||||
+ ElkEdgeRoutingScoring.CountBoundarySlotViolations(candidate, nodes)
|
||||
+ ElkEdgeRoutingScoring.CountBelowGraphViolations(candidate, nodes)
|
||||
+ ElkEdgeRoutingScoring.CountUnderNodeViolations(candidate, nodes);
|
||||
var currentLengthPressure =
|
||||
ElkEdgeRoutingScoring.CountTargetApproachBacktrackingViolations(candidate, nodes)
|
||||
+ ElkEdgeRoutingScoring.CountExcessiveDetourViolations(candidate, nodes);
|
||||
var candidateScore = ElkEdgeRoutingScoring.ComputeScore(candidate, nodes);
|
||||
var candidateRetryState = BuildRetryState(
|
||||
candidateScore,
|
||||
HighwayProcessingEnabled
|
||||
? ElkEdgeRouterHighway.DetectRemainingBrokenHighways(candidate, nodes).Count
|
||||
: 0);
|
||||
var improvedBoundarySlots = candidateRetryState.BoundarySlotViolations < previousRetryState.BoundarySlotViolations;
|
||||
var rejectedByRegression = improvedBoundarySlots
|
||||
? candidateScore.NodeCrossings > previousScore.NodeCrossings
|
||||
|| HasBlockingBoundarySlotPromotionRegression(candidateRetryState, previousRetryState)
|
||||
: HasHardRuleRegression(candidateRetryState, previousRetryState);
|
||||
var madeProgress = improvedBoundarySlots
|
||||
|| (previousHardPressure > 0
|
||||
? currentHardPressure < previousHardPressure
|
||||
: currentLengthPressure < previousLengthPressure);
|
||||
if (rejectedByRegression || !madeProgress)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
result = candidate;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static ElkRoutedEdge[] ApplyGuardedFocusedHardRulePass(
|
||||
ElkRoutedEdge[] current,
|
||||
ElkPositionedNode[] nodes,
|
||||
Func<ElkRoutedEdge[], ElkRoutedEdge[]> pass)
|
||||
{
|
||||
var candidate = pass(current);
|
||||
return ElkEdgeRoutingScoring.CountBoundarySlotViolations(current, nodes) > 0
|
||||
? ChoosePreferredBoundarySlotRepairLayout(current, candidate, nodes)
|
||||
: ChoosePreferredHardRuleLayout(current, candidate, nodes);
|
||||
}
|
||||
|
||||
private static ElkRoutedEdge[] ChoosePreferredBoundarySlotRepairLayout(
|
||||
ElkRoutedEdge[] baseline,
|
||||
ElkRoutedEdge[] candidate,
|
||||
ElkPositionedNode[] nodes)
|
||||
{
|
||||
if (ReferenceEquals(candidate, baseline))
|
||||
{
|
||||
return baseline;
|
||||
}
|
||||
|
||||
var baselineScore = ElkEdgeRoutingScoring.ComputeScore(baseline, nodes);
|
||||
var baselineRetryState = BuildRetryState(
|
||||
baselineScore,
|
||||
HighwayProcessingEnabled
|
||||
? ElkEdgeRouterHighway.DetectRemainingBrokenHighways(baseline, nodes).Count
|
||||
: 0);
|
||||
var candidateScore = ElkEdgeRoutingScoring.ComputeScore(candidate, nodes);
|
||||
var candidateRetryState = BuildRetryState(
|
||||
candidateScore,
|
||||
HighwayProcessingEnabled
|
||||
? ElkEdgeRouterHighway.DetectRemainingBrokenHighways(candidate, nodes).Count
|
||||
: 0);
|
||||
|
||||
if (!IsBetterBoundarySlotRepairCandidate(
|
||||
candidateScore,
|
||||
candidateRetryState,
|
||||
baselineScore,
|
||||
baselineRetryState))
|
||||
{
|
||||
return baseline;
|
||||
}
|
||||
|
||||
// Boundary-slot repair is staged ahead of other soft cleanups. Once a
|
||||
// candidate legitimately reduces boundary-slot violations without
|
||||
// introducing a blocking hard regression, keep it alive so the later
|
||||
// shared-lane / detour passes can recover any temporary soft tradeoff.
|
||||
if (candidateRetryState.BoundarySlotViolations < baselineRetryState.BoundarySlotViolations)
|
||||
{
|
||||
return candidate;
|
||||
}
|
||||
|
||||
var retryComparison = CompareRetryStates(candidateRetryState, baselineRetryState);
|
||||
if (retryComparison < 0)
|
||||
{
|
||||
return candidate;
|
||||
}
|
||||
|
||||
if (retryComparison > 0)
|
||||
{
|
||||
return baseline;
|
||||
}
|
||||
|
||||
if (candidateScore.NodeCrossings != baselineScore.NodeCrossings)
|
||||
{
|
||||
return candidateScore.NodeCrossings < baselineScore.NodeCrossings
|
||||
? candidate
|
||||
: baseline;
|
||||
}
|
||||
|
||||
return candidateScore.Value > baselineScore.Value
|
||||
? candidate
|
||||
: baseline;
|
||||
}
|
||||
|
||||
private static ElkRoutedEdge[] ChoosePreferredSharedLanePolishLayout(
|
||||
ElkRoutedEdge[] baseline,
|
||||
ElkRoutedEdge[] candidate,
|
||||
ElkPositionedNode[] nodes)
|
||||
{
|
||||
if (ReferenceEquals(candidate, baseline))
|
||||
{
|
||||
return baseline;
|
||||
}
|
||||
|
||||
var baselineScore = ElkEdgeRoutingScoring.ComputeScore(baseline, nodes);
|
||||
var baselineRetryState = BuildRetryState(
|
||||
baselineScore,
|
||||
HighwayProcessingEnabled
|
||||
? ElkEdgeRouterHighway.DetectRemainingBrokenHighways(baseline, nodes).Count
|
||||
: 0);
|
||||
var candidateScore = ElkEdgeRoutingScoring.ComputeScore(candidate, nodes);
|
||||
var candidateRetryState = BuildRetryState(
|
||||
candidateScore,
|
||||
HighwayProcessingEnabled
|
||||
? ElkEdgeRouterHighway.DetectRemainingBrokenHighways(candidate, nodes).Count
|
||||
: 0);
|
||||
|
||||
if (!IsBetterSharedLanePolishCandidate(
|
||||
candidateScore,
|
||||
candidateRetryState,
|
||||
baselineScore,
|
||||
baselineRetryState))
|
||||
{
|
||||
return baseline;
|
||||
}
|
||||
|
||||
if (candidateRetryState.SharedLaneViolations < baselineRetryState.SharedLaneViolations)
|
||||
{
|
||||
return candidate;
|
||||
}
|
||||
|
||||
var retryComparison = CompareRetryStates(candidateRetryState, baselineRetryState);
|
||||
if (retryComparison < 0)
|
||||
{
|
||||
return candidate;
|
||||
}
|
||||
|
||||
if (retryComparison > 0)
|
||||
{
|
||||
return baseline;
|
||||
}
|
||||
|
||||
if (candidateScore.NodeCrossings != baselineScore.NodeCrossings)
|
||||
{
|
||||
return candidateScore.NodeCrossings < baselineScore.NodeCrossings
|
||||
? candidate
|
||||
: baseline;
|
||||
}
|
||||
|
||||
return candidateScore.Value > baselineScore.Value
|
||||
? candidate
|
||||
: baseline;
|
||||
}
|
||||
|
||||
private static bool IsBetterBoundarySlotRepairCandidate(
|
||||
EdgeRoutingScore candidateScore,
|
||||
RoutingRetryState candidateRetryState,
|
||||
EdgeRoutingScore baselineScore,
|
||||
RoutingRetryState baselineRetryState)
|
||||
{
|
||||
if (candidateRetryState.BoundarySlotViolations < baselineRetryState.BoundarySlotViolations)
|
||||
{
|
||||
return candidateScore.NodeCrossings <= baselineScore.NodeCrossings
|
||||
&& !HasBlockingBoundarySlotPromotionRegression(candidateRetryState, baselineRetryState);
|
||||
}
|
||||
|
||||
return IsBetterCandidate(candidateScore, candidateRetryState, baselineScore, baselineRetryState);
|
||||
}
|
||||
|
||||
private static bool IsBetterSharedLanePolishCandidate(
|
||||
EdgeRoutingScore candidateScore,
|
||||
RoutingRetryState candidateRetryState,
|
||||
EdgeRoutingScore baselineScore,
|
||||
RoutingRetryState baselineRetryState)
|
||||
{
|
||||
if (candidateRetryState.SharedLaneViolations < baselineRetryState.SharedLaneViolations)
|
||||
{
|
||||
return candidateScore.NodeCrossings <= baselineScore.NodeCrossings
|
||||
&& !HasBlockingSharedLanePromotionRegression(candidateRetryState, baselineRetryState);
|
||||
}
|
||||
|
||||
return IsBetterCandidate(candidateScore, candidateRetryState, baselineScore, baselineRetryState);
|
||||
}
|
||||
|
||||
private static ElkRoutedEdge[] ChoosePreferredHardRuleLayout(
|
||||
ElkRoutedEdge[] baseline,
|
||||
ElkRoutedEdge[] candidate,
|
||||
ElkPositionedNode[] nodes)
|
||||
{
|
||||
if (ReferenceEquals(candidate, baseline))
|
||||
{
|
||||
return baseline;
|
||||
}
|
||||
|
||||
var baselineScore = ElkEdgeRoutingScoring.ComputeScore(baseline, nodes);
|
||||
var baselineRetryState = BuildRetryState(
|
||||
baselineScore,
|
||||
HighwayProcessingEnabled
|
||||
? ElkEdgeRouterHighway.DetectRemainingBrokenHighways(baseline, nodes).Count
|
||||
: 0);
|
||||
var candidateScore = ElkEdgeRoutingScoring.ComputeScore(candidate, nodes);
|
||||
var candidateRetryState = BuildRetryState(
|
||||
candidateScore,
|
||||
HighwayProcessingEnabled
|
||||
? ElkEdgeRouterHighway.DetectRemainingBrokenHighways(candidate, nodes).Count
|
||||
: 0);
|
||||
|
||||
if (HasHardRuleRegression(candidateRetryState, baselineRetryState))
|
||||
{
|
||||
return baseline;
|
||||
}
|
||||
|
||||
var retryComparison = CompareRetryStates(candidateRetryState, baselineRetryState);
|
||||
if (retryComparison < 0)
|
||||
{
|
||||
return candidate;
|
||||
}
|
||||
|
||||
if (retryComparison > 0)
|
||||
{
|
||||
return baseline;
|
||||
}
|
||||
|
||||
if (candidateScore.NodeCrossings != baselineScore.NodeCrossings)
|
||||
{
|
||||
return candidateScore.NodeCrossings < baselineScore.NodeCrossings
|
||||
? candidate
|
||||
: baseline;
|
||||
}
|
||||
|
||||
return candidateScore.Value > baselineScore.Value
|
||||
? candidate
|
||||
: baseline;
|
||||
}
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,42 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
|
||||
namespace StellaOps.ElkSharp;
|
||||
|
||||
internal static partial class ElkEdgeRouterIterative
|
||||
{
|
||||
private readonly record struct CandidateSolution(
|
||||
EdgeRoutingScore Score,
|
||||
RoutingRetryState RetryState,
|
||||
ElkRoutedEdge[] Edges,
|
||||
int StrategyIndex);
|
||||
|
||||
private readonly record struct StrategyWorkItem(
|
||||
int StrategyIndex,
|
||||
string StrategyName,
|
||||
RoutingStrategy Strategy);
|
||||
|
||||
private sealed record StrategyEvaluationResult(
|
||||
int StrategyIndex,
|
||||
IReadOnlyList<CandidateSolution> FallbackSolutions,
|
||||
CandidateSolution? ValidSolution,
|
||||
ElkIterativeStrategyDiagnostics Diagnostics);
|
||||
|
||||
private readonly record struct RepairPlan(
|
||||
int[] EdgeIndices,
|
||||
string[] EdgeIds,
|
||||
string[] PreferredShortestEdgeIds,
|
||||
string[] RouteRepairEdgeIds,
|
||||
string[] Reasons);
|
||||
|
||||
private sealed record RouteAllEdgesResult(
|
||||
ElkRoutedEdge[] Edges,
|
||||
ElkIterativeRouteDiagnostics Diagnostics);
|
||||
|
||||
private sealed record RepairEdgeBuildResult(
|
||||
ElkRoutedEdge Edge,
|
||||
int RoutedSections,
|
||||
int FallbackSections,
|
||||
bool WasSkipped);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -190,6 +190,70 @@ internal static class ElkEdgeRoutingGeometry
|
||||
return "bottom";
|
||||
}
|
||||
|
||||
internal static string ResolveBoundaryApproachSide(
|
||||
ElkPoint boundaryPoint,
|
||||
ElkPoint adjacentPoint,
|
||||
ElkPositionedNode node)
|
||||
{
|
||||
if (!ElkShapeBoundaries.IsGatewayShape(node))
|
||||
{
|
||||
return ResolveBoundarySide(boundaryPoint, node);
|
||||
}
|
||||
|
||||
var deltaX = boundaryPoint.X - adjacentPoint.X;
|
||||
var deltaY = boundaryPoint.Y - adjacentPoint.Y;
|
||||
var absDx = Math.Abs(deltaX);
|
||||
var absDy = Math.Abs(deltaY);
|
||||
if (absDx <= CoordinateTolerance && absDy > CoordinateTolerance)
|
||||
{
|
||||
return deltaY >= 0d ? "top" : "bottom";
|
||||
}
|
||||
|
||||
if (absDy <= CoordinateTolerance && absDx > CoordinateTolerance)
|
||||
{
|
||||
return deltaX >= 0d ? "left" : "right";
|
||||
}
|
||||
|
||||
if (absDx > absDy * 1.25d)
|
||||
{
|
||||
return deltaX >= 0d ? "left" : "right";
|
||||
}
|
||||
|
||||
if (absDy > absDx * 1.25d)
|
||||
{
|
||||
return deltaY >= 0d ? "top" : "bottom";
|
||||
}
|
||||
|
||||
return ResolveBoundarySide(boundaryPoint, node);
|
||||
}
|
||||
|
||||
internal static double ComputeParallelOverlapLength(
|
||||
ElkPoint a1,
|
||||
ElkPoint a2,
|
||||
ElkPoint b1,
|
||||
ElkPoint b2)
|
||||
{
|
||||
if (IsHorizontal(a1, a2) && IsHorizontal(b1, b2))
|
||||
{
|
||||
return OverlapLength(
|
||||
Math.Min(a1.X, a2.X),
|
||||
Math.Max(a1.X, a2.X),
|
||||
Math.Min(b1.X, b2.X),
|
||||
Math.Max(b1.X, b2.X));
|
||||
}
|
||||
|
||||
if (IsVertical(a1, a2) && IsVertical(b1, b2))
|
||||
{
|
||||
return OverlapLength(
|
||||
Math.Min(a1.Y, a2.Y),
|
||||
Math.Max(a1.Y, a2.Y),
|
||||
Math.Min(b1.Y, b2.Y),
|
||||
Math.Max(b1.Y, b2.Y));
|
||||
}
|
||||
|
||||
return 0d;
|
||||
}
|
||||
|
||||
internal static bool AreCollinearAndOverlapping(ElkPoint a1, ElkPoint a2, ElkPoint b1, ElkPoint b2)
|
||||
{
|
||||
if (IsHorizontal(a1, a2) && IsHorizontal(b1, b2) && Math.Abs(a1.Y - b1.Y) <= CoordinateTolerance)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
|
||||
namespace StellaOps.ElkSharp;
|
||||
@@ -38,6 +39,7 @@ internal sealed class ElkLayoutRunDiagnostics
|
||||
public List<ElkHighwayDiagnostics> DetectedHighways { get; } = [];
|
||||
public List<string> ProgressLog { get; } = [];
|
||||
public string? ProgressLogPath { get; set; }
|
||||
public string? SnapshotPath { get; set; }
|
||||
}
|
||||
|
||||
internal sealed class ElkHighwayDiagnostics
|
||||
@@ -58,11 +60,14 @@ internal sealed class ElkIterativeStrategyDiagnostics
|
||||
public int Attempts { get; set; }
|
||||
public double TotalDurationMs { get; set; }
|
||||
public EdgeRoutingScore? BestScore { get; set; }
|
||||
public required string Outcome { get; init; }
|
||||
public required string Outcome { get; set; }
|
||||
public double BendPenalty { get; init; }
|
||||
public double DiagonalPenalty { get; init; }
|
||||
public double SoftObstacleWeight { get; init; }
|
||||
|
||||
[System.Text.Json.Serialization.JsonIgnore]
|
||||
internal bool RegisteredLive { get; set; }
|
||||
|
||||
[System.Text.Json.Serialization.JsonIgnore]
|
||||
public ElkRoutedEdge[]? BestEdges { get; set; }
|
||||
|
||||
@@ -93,6 +98,8 @@ internal sealed class ElkIterativeRouteDiagnostics
|
||||
public int SoftObstacleSegments { get; init; }
|
||||
public IReadOnlyCollection<string> RepairedEdgeIds { get; init; } = [];
|
||||
public IReadOnlyCollection<string> RepairReasons { get; init; } = [];
|
||||
public string? BuilderMode { get; init; }
|
||||
public int BuilderParallelism { get; init; }
|
||||
}
|
||||
|
||||
internal sealed class ElkIterativePhaseDiagnostics
|
||||
@@ -135,6 +142,7 @@ internal sealed class ElkDiagnosticSectionPath
|
||||
internal static class ElkLayoutDiagnostics
|
||||
{
|
||||
private static readonly AsyncLocal<ElkLayoutRunDiagnostics?> CurrentDiagnostics = new();
|
||||
private static readonly JsonSerializerOptions SnapshotJsonOptions = new() { WriteIndented = true };
|
||||
|
||||
internal static ElkLayoutRunDiagnostics? Current => CurrentDiagnostics.Value;
|
||||
|
||||
@@ -175,6 +183,8 @@ internal static class ElkLayoutDiagnostics
|
||||
{
|
||||
File.AppendAllText(diagnostics.ProgressLogPath, line + Environment.NewLine);
|
||||
}
|
||||
|
||||
WriteSnapshotLocked(diagnostics);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,6 +199,7 @@ internal static class ElkLayoutDiagnostics
|
||||
lock (diagnostics.SyncRoot)
|
||||
{
|
||||
diagnostics.DetectedHighways.Add(diagnostic);
|
||||
WriteSnapshotLocked(diagnostics);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,6 +214,49 @@ internal static class ElkLayoutDiagnostics
|
||||
lock (diagnostics.SyncRoot)
|
||||
{
|
||||
diagnostics.Attempts.Add(attempt);
|
||||
WriteSnapshotLocked(diagnostics);
|
||||
}
|
||||
}
|
||||
|
||||
internal static void FlushSnapshot()
|
||||
{
|
||||
var diagnostics = CurrentDiagnostics.Value;
|
||||
if (diagnostics is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (diagnostics.SyncRoot)
|
||||
{
|
||||
WriteSnapshotLocked(diagnostics);
|
||||
}
|
||||
}
|
||||
|
||||
internal static void FlushSnapshot(ElkLayoutRunDiagnostics diagnostics)
|
||||
{
|
||||
lock (diagnostics.SyncRoot)
|
||||
{
|
||||
WriteSnapshotLocked(diagnostics);
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteSnapshotLocked(ElkLayoutRunDiagnostics diagnostics)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(diagnostics.SnapshotPath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var snapshotPath = diagnostics.SnapshotPath!;
|
||||
var snapshotDir = Path.GetDirectoryName(snapshotPath);
|
||||
if (!string.IsNullOrWhiteSpace(snapshotDir))
|
||||
{
|
||||
Directory.CreateDirectory(snapshotDir);
|
||||
}
|
||||
|
||||
var tempPath = snapshotPath + ".tmp";
|
||||
File.WriteAllText(tempPath, JsonSerializer.Serialize(diagnostics, SnapshotJsonOptions));
|
||||
File.Copy(tempPath, snapshotPath, overwrite: true);
|
||||
File.Delete(tempPath);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ internal readonly record struct GraphBounds(double MinX, double MinY, double Max
|
||||
|
||||
internal readonly record struct LayerBoundary(double MinX, double MaxX, double MinY, double MaxY);
|
||||
|
||||
internal readonly record struct NodePlacementGrid(double XStep, double YStep);
|
||||
|
||||
internal readonly record struct EdgeChannel(
|
||||
EdgeRouteMode RouteMode,
|
||||
int BackwardLane,
|
||||
@@ -52,12 +54,19 @@ internal readonly record struct EdgeRoutingScore(
|
||||
int BendCount,
|
||||
int TargetCongestion,
|
||||
int DiagonalCount,
|
||||
int BelowGraphViolations,
|
||||
int UnderNodeViolations,
|
||||
int LongDiagonalViolations,
|
||||
int EntryAngleViolations,
|
||||
int GatewaySourceExitViolations,
|
||||
int LabelProximityViolations,
|
||||
int RepeatCollectorCorridorViolations,
|
||||
int RepeatCollectorNodeClearanceViolations,
|
||||
int TargetApproachJoinViolations,
|
||||
int TargetApproachBacktrackingViolations,
|
||||
int ExcessiveDetourViolations,
|
||||
int SharedLaneViolations,
|
||||
int BoundarySlotViolations,
|
||||
int ProximityViolations,
|
||||
double TotalPathLength,
|
||||
double Value);
|
||||
@@ -65,24 +74,39 @@ internal readonly record struct EdgeRoutingScore(
|
||||
internal readonly record struct RoutingRetryState(
|
||||
int RemainingShortHighways,
|
||||
int RepeatCollectorCorridorViolations,
|
||||
int RepeatCollectorNodeClearanceViolations,
|
||||
int TargetApproachJoinViolations,
|
||||
int TargetApproachBacktrackingViolations,
|
||||
int ExcessiveDetourViolations,
|
||||
int SharedLaneViolations,
|
||||
int BoundarySlotViolations,
|
||||
int BelowGraphViolations,
|
||||
int UnderNodeViolations,
|
||||
int LongDiagonalViolations,
|
||||
int ProximityViolations,
|
||||
int EntryAngleViolations,
|
||||
int GatewaySourceExitViolations,
|
||||
int LabelProximityViolations,
|
||||
int EdgeCrossings)
|
||||
{
|
||||
internal int QualityViolationCount =>
|
||||
ProximityViolations + EntryAngleViolations + LabelProximityViolations;
|
||||
ProximityViolations + LabelProximityViolations;
|
||||
|
||||
internal bool RequiresQualityRetry => QualityViolationCount > 0;
|
||||
|
||||
internal int BlockingViolationCount =>
|
||||
RemainingShortHighways
|
||||
+ RepeatCollectorCorridorViolations
|
||||
+ RepeatCollectorNodeClearanceViolations
|
||||
+ TargetApproachJoinViolations
|
||||
+ TargetApproachBacktrackingViolations;
|
||||
+ TargetApproachBacktrackingViolations
|
||||
+ SharedLaneViolations
|
||||
+ BoundarySlotViolations
|
||||
+ BelowGraphViolations
|
||||
+ UnderNodeViolations
|
||||
+ LongDiagonalViolations
|
||||
+ EntryAngleViolations
|
||||
+ GatewaySourceExitViolations;
|
||||
|
||||
internal bool RequiresBlockingRetry => BlockingViolationCount > 0;
|
||||
|
||||
@@ -136,19 +160,29 @@ internal sealed class RoutingStrategy
|
||||
{
|
||||
var highwayPressure = Math.Min(retryState.RemainingShortHighways, 4);
|
||||
var collectorCorridorPressure = Math.Min(retryState.RepeatCollectorCorridorViolations, 4);
|
||||
var collectorClearancePressure = Math.Min(retryState.RepeatCollectorNodeClearanceViolations, 6);
|
||||
var targetJoinPressure = Math.Min(retryState.TargetApproachJoinViolations, 4);
|
||||
var backtrackingPressure = Math.Min(retryState.TargetApproachBacktrackingViolations, 4);
|
||||
var detourPressure = Math.Min(retryState.ExcessiveDetourViolations, 4);
|
||||
var sharedLanePressure = Math.Min(retryState.SharedLaneViolations, 6);
|
||||
var boundarySlotPressure = Math.Min(retryState.BoundarySlotViolations, 6);
|
||||
var underNodePressure = Math.Min(retryState.UnderNodeViolations, 6);
|
||||
var proximityPressure = Math.Min(retryState.ProximityViolations, 6);
|
||||
var entryPressure = Math.Min(retryState.EntryAngleViolations, 4);
|
||||
var gatewaySourcePressure = Math.Min(retryState.GatewaySourceExitViolations, 4);
|
||||
var labelPressure = Math.Min(retryState.LabelProximityViolations, 4);
|
||||
var crossingPressure = Math.Min(retryState.EdgeCrossings, 6);
|
||||
var clearanceStep = 4d
|
||||
+ (highwayPressure > 0 ? 8d : 0d)
|
||||
+ (collectorCorridorPressure > 0 ? 10d : 0d)
|
||||
+ (collectorClearancePressure > 0 ? 10d : 0d)
|
||||
+ (targetJoinPressure > 0 ? 12d : 0d)
|
||||
+ (backtrackingPressure > 0 ? 6d : 0d)
|
||||
+ (sharedLanePressure > 0 ? 12d : 0d)
|
||||
+ (boundarySlotPressure > 0 ? 12d : 0d)
|
||||
+ (underNodePressure > 0 ? 12d : 0d)
|
||||
+ (proximityPressure > 0 ? 10d : 0d)
|
||||
+ (gatewaySourcePressure > 0 ? 8d : 0d)
|
||||
+ (labelPressure > 0 ? 4d : 0d)
|
||||
+ (crossingPressure > 0 ? 3d : 0d);
|
||||
MinLineClearance = Math.Min(
|
||||
@@ -156,15 +190,15 @@ internal sealed class RoutingStrategy
|
||||
BaseLineClearance * 2d);
|
||||
|
||||
var bendPenalty = RoutingParams.BendPenalty;
|
||||
if (entryPressure > 0 || labelPressure > 0 || highwayPressure > 0 || collectorCorridorPressure > 0 || targetJoinPressure > 0)
|
||||
if (entryPressure > 0 || gatewaySourcePressure > 0 || labelPressure > 0 || highwayPressure > 0 || collectorCorridorPressure > 0 || collectorClearancePressure > 0 || targetJoinPressure > 0 || sharedLanePressure > 0 || boundarySlotPressure > 0 || underNodePressure > 0)
|
||||
{
|
||||
bendPenalty = Math.Min(bendPenalty + 40d, 800d);
|
||||
}
|
||||
else if (backtrackingPressure > 0 || detourPressure > 0 || proximityPressure > 0 || crossingPressure > 0)
|
||||
else if (backtrackingPressure > 0 || detourPressure > 0 || sharedLanePressure > 0 || boundarySlotPressure > 0 || underNodePressure > 0 || proximityPressure > 0 || crossingPressure > 0)
|
||||
{
|
||||
bendPenalty = Math.Max(
|
||||
80d,
|
||||
bendPenalty - (backtrackingPressure > 0 ? 80d : detourPressure > 0 ? 50d : 30d));
|
||||
bendPenalty - (backtrackingPressure > 0 ? 80d : detourPressure > 0 ? 50d : sharedLanePressure > 0 || boundarySlotPressure > 0 || underNodePressure > 0 ? 40d : 30d));
|
||||
}
|
||||
|
||||
var margin = RoutingParams.Margin;
|
||||
@@ -178,9 +212,14 @@ internal sealed class RoutingStrategy
|
||||
margin
|
||||
+ (highwayPressure > 0 ? 8d : 4d)
|
||||
+ (collectorCorridorPressure > 0 ? 8d : 0d)
|
||||
+ (collectorClearancePressure > 0 ? 8d : 0d)
|
||||
+ (targetJoinPressure > 0 ? 10d : 0d)
|
||||
+ (sharedLanePressure > 0 ? 10d : 0d)
|
||||
+ (boundarySlotPressure > 0 ? 10d : 0d)
|
||||
+ (underNodePressure > 0 ? 10d : 0d)
|
||||
+ (proximityPressure > 0 ? 6d : 0d)
|
||||
+ (entryPressure > 0 ? 3d : 0d),
|
||||
+ (entryPressure > 0 ? 3d : 0d)
|
||||
+ (gatewaySourcePressure > 0 ? 4d : 0d),
|
||||
BaseLineClearance * 2d);
|
||||
}
|
||||
|
||||
@@ -197,7 +236,9 @@ internal sealed class RoutingStrategy
|
||||
softObstacleWeight
|
||||
+ (highwayPressure > 0 ? 0.75d : 0.25d)
|
||||
+ (collectorCorridorPressure > 0 ? 0.75d : 0d)
|
||||
+ (collectorClearancePressure > 0 ? 0.75d : 0d)
|
||||
+ (targetJoinPressure > 0 ? 1.0d : 0d)
|
||||
+ (sharedLanePressure > 0 ? 1.0d : 0d)
|
||||
+ (proximityPressure > 0 ? 0.75d : 0d)
|
||||
+ (crossingPressure > 0 ? 0.5d : 0d),
|
||||
8d);
|
||||
@@ -216,7 +257,9 @@ internal sealed class RoutingStrategy
|
||||
softObstacleClearance
|
||||
+ (highwayPressure > 0 ? 8d : 4d)
|
||||
+ (collectorCorridorPressure > 0 ? 10d : 0d)
|
||||
+ (collectorClearancePressure > 0 ? 10d : 0d)
|
||||
+ (targetJoinPressure > 0 ? 16d : 0d)
|
||||
+ (sharedLanePressure > 0 ? 16d : 0d)
|
||||
+ (proximityPressure > 0 ? 10d : 0d)
|
||||
+ (labelPressure > 0 ? 4d : 0d)
|
||||
+ (crossingPressure > 0 ? 4d : 0d),
|
||||
@@ -238,6 +281,7 @@ internal sealed class RoutingStrategy
|
||||
- (highwayPressure > 0 ? 6d : 2d)
|
||||
- (collectorCorridorPressure > 0 ? 6d : 0d)
|
||||
- (targetJoinPressure > 0 ? 8d : 0d)
|
||||
- (sharedLanePressure > 0 ? 8d : 0d)
|
||||
- (proximityPressure > 0 ? 6d : 0d)
|
||||
- (entryPressure > 0 ? 4d : 0d)
|
||||
- (labelPressure > 0 ? 2d : 0d));
|
||||
|
||||
@@ -85,7 +85,7 @@ public sealed record EdgeRefinementOptions
|
||||
public sealed record IterativeRoutingOptions
|
||||
{
|
||||
public bool? Enabled { get; init; }
|
||||
public int MaxAdaptationsPerStrategy { get; init; } = 10;
|
||||
public int MaxAdaptationsPerStrategy { get; init; } = 100;
|
||||
public int RequiredValidSolutions { get; init; } = 10;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,23 @@ namespace StellaOps.ElkSharp;
|
||||
|
||||
internal static class ElkNodePlacement
|
||||
{
|
||||
internal static NodePlacementGrid ResolvePlacementGrid(IReadOnlyCollection<ElkNode> nodes)
|
||||
{
|
||||
var actualNodes = nodes
|
||||
.Where(node => node.Kind is not "Start" and not "End")
|
||||
.ToArray();
|
||||
if (actualNodes.Length == 0)
|
||||
{
|
||||
return new NodePlacementGrid(160d, 80d);
|
||||
}
|
||||
|
||||
var averageWidth = actualNodes.Average(node => node.Width);
|
||||
var averageHeight = actualNodes.Average(node => node.Height);
|
||||
return new NodePlacementGrid(
|
||||
XStep: Math.Max(64d, Math.Round(averageWidth / 8d) * 8d),
|
||||
YStep: Math.Max(48d, Math.Round(averageHeight / 8d) * 8d));
|
||||
}
|
||||
|
||||
internal static int ResolveOrderingIterationCount(
|
||||
ElkLayoutOptions options,
|
||||
int edgeCount,
|
||||
@@ -218,6 +235,7 @@ internal static class ElkNodePlacement
|
||||
sortedNodes,
|
||||
desiredCoordinates,
|
||||
nodeSpacing,
|
||||
0d,
|
||||
horizontal: direction == ElkLayoutDirection.LeftToRight);
|
||||
|
||||
for (var nodeIndex = 0; nodeIndex < sortedNodes.Length; nodeIndex++)
|
||||
@@ -231,10 +249,130 @@ internal static class ElkNodePlacement
|
||||
}
|
||||
}
|
||||
|
||||
internal static void AlignToPlacementGrid(
|
||||
Dictionary<string, ElkPositionedNode> positionedNodes,
|
||||
IReadOnlyList<ElkNode[]> layers,
|
||||
IReadOnlySet<string> dummyNodeIds,
|
||||
double nodeSpacing,
|
||||
NodePlacementGrid placementGrid,
|
||||
ElkLayoutDirection direction)
|
||||
{
|
||||
if (layers.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (direction == ElkLayoutDirection.LeftToRight)
|
||||
{
|
||||
foreach (var layer in layers)
|
||||
{
|
||||
var actualNodes = layer.Where(node => !dummyNodeIds.Contains(node.Id)).ToArray();
|
||||
if (actualNodes.Length == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var currentX = positionedNodes[actualNodes[0].Id].X;
|
||||
var snappedX = SnapToPlacementGrid(currentX, placementGrid.XStep);
|
||||
var deltaX = snappedX - currentX;
|
||||
if (Math.Abs(deltaX) > 0.01d)
|
||||
{
|
||||
foreach (var node in layer)
|
||||
{
|
||||
var pos = positionedNodes[node.Id];
|
||||
positionedNodes[node.Id] = ElkLayoutHelpers.CreatePositionedNode(
|
||||
node,
|
||||
pos.X + deltaX,
|
||||
pos.Y,
|
||||
direction);
|
||||
}
|
||||
}
|
||||
|
||||
var desiredY = actualNodes
|
||||
.Select(node => SnapToPlacementGrid(positionedNodes[node.Id].Y, placementGrid.YStep))
|
||||
.ToArray();
|
||||
EnforceLinearSpacing(actualNodes, desiredY, nodeSpacing, placementGrid.YStep, horizontal: true);
|
||||
for (var i = 0; i < actualNodes.Length; i++)
|
||||
{
|
||||
var current = positionedNodes[actualNodes[i].Id];
|
||||
positionedNodes[actualNodes[i].Id] = ElkLayoutHelpers.CreatePositionedNode(
|
||||
actualNodes[i],
|
||||
current.X,
|
||||
desiredY[i],
|
||||
direction);
|
||||
}
|
||||
}
|
||||
|
||||
var minY = positionedNodes.Values.Min(node => node.Y);
|
||||
if (minY < -0.01d)
|
||||
{
|
||||
var shift = SnapForwardToPlacementGrid(-minY, placementGrid.YStep);
|
||||
foreach (var nodeId in positionedNodes.Keys.ToArray())
|
||||
{
|
||||
var pos = positionedNodes[nodeId];
|
||||
positionedNodes[nodeId] = pos with { Y = pos.Y + shift };
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var layer in layers)
|
||||
{
|
||||
var actualNodes = layer.Where(node => !dummyNodeIds.Contains(node.Id)).ToArray();
|
||||
if (actualNodes.Length == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var currentY = positionedNodes[actualNodes[0].Id].Y;
|
||||
var snappedY = SnapToPlacementGrid(currentY, placementGrid.YStep);
|
||||
var deltaY = snappedY - currentY;
|
||||
if (Math.Abs(deltaY) > 0.01d)
|
||||
{
|
||||
foreach (var node in layer)
|
||||
{
|
||||
var pos = positionedNodes[node.Id];
|
||||
positionedNodes[node.Id] = ElkLayoutHelpers.CreatePositionedNode(
|
||||
node,
|
||||
pos.X,
|
||||
pos.Y + deltaY,
|
||||
direction);
|
||||
}
|
||||
}
|
||||
|
||||
var desiredX = actualNodes
|
||||
.Select(node => SnapToPlacementGrid(positionedNodes[node.Id].X, placementGrid.XStep))
|
||||
.ToArray();
|
||||
EnforceLinearSpacing(actualNodes, desiredX, nodeSpacing, placementGrid.XStep, horizontal: false);
|
||||
for (var i = 0; i < actualNodes.Length; i++)
|
||||
{
|
||||
var current = positionedNodes[actualNodes[i].Id];
|
||||
positionedNodes[actualNodes[i].Id] = ElkLayoutHelpers.CreatePositionedNode(
|
||||
actualNodes[i],
|
||||
desiredX[i],
|
||||
current.Y,
|
||||
direction);
|
||||
}
|
||||
}
|
||||
|
||||
var minX = positionedNodes.Values.Min(node => node.X);
|
||||
if (minX < -0.01d)
|
||||
{
|
||||
var shift = SnapForwardToPlacementGrid(-minX, placementGrid.XStep);
|
||||
foreach (var nodeId in positionedNodes.Keys.ToArray())
|
||||
{
|
||||
var pos = positionedNodes[nodeId];
|
||||
positionedNodes[nodeId] = pos with { X = pos.X + shift };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static void EnforceLinearSpacing(
|
||||
IReadOnlyList<ElkNode> layer,
|
||||
double[] desiredCoordinates,
|
||||
double spacing,
|
||||
double gridStep,
|
||||
bool horizontal)
|
||||
{
|
||||
for (var index = 1; index < layer.Count; index++)
|
||||
@@ -243,6 +381,7 @@ internal static class ElkNodePlacement
|
||||
desiredCoordinates[index] = Math.Max(
|
||||
desiredCoordinates[index],
|
||||
desiredCoordinates[index - 1] + extent + spacing);
|
||||
desiredCoordinates[index] = SnapForwardToPlacementGrid(desiredCoordinates[index], gridStep);
|
||||
}
|
||||
|
||||
for (var index = layer.Count - 2; index >= 0; index--)
|
||||
@@ -251,6 +390,7 @@ internal static class ElkNodePlacement
|
||||
desiredCoordinates[index] = Math.Min(
|
||||
desiredCoordinates[index],
|
||||
desiredCoordinates[index + 1] - extent - spacing);
|
||||
desiredCoordinates[index] = SnapBackwardToPlacementGrid(desiredCoordinates[index], gridStep);
|
||||
}
|
||||
|
||||
for (var index = 1; index < layer.Count; index++)
|
||||
@@ -259,6 +399,37 @@ internal static class ElkNodePlacement
|
||||
desiredCoordinates[index] = Math.Max(
|
||||
desiredCoordinates[index],
|
||||
desiredCoordinates[index - 1] + extent + spacing);
|
||||
desiredCoordinates[index] = SnapForwardToPlacementGrid(desiredCoordinates[index], gridStep);
|
||||
}
|
||||
}
|
||||
|
||||
internal static double SnapToPlacementGrid(double value, double gridStep)
|
||||
{
|
||||
if (gridStep <= 1d)
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
return Math.Round(value / gridStep) * gridStep;
|
||||
}
|
||||
|
||||
internal static double SnapForwardToPlacementGrid(double value, double gridStep)
|
||||
{
|
||||
if (gridStep <= 1d)
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
return Math.Ceiling(value / gridStep) * gridStep;
|
||||
}
|
||||
|
||||
internal static double SnapBackwardToPlacementGrid(double value, double gridStep)
|
||||
{
|
||||
if (gridStep <= 1d)
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
return Math.Floor(value / gridStep) * gridStep;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,9 @@ internal sealed class ElkRepeatCollectorCorridorGroup
|
||||
public required bool IsAbove { get; init; }
|
||||
public required double CorridorY { get; init; }
|
||||
public required string[] EdgeIds { get; init; }
|
||||
public required int ConflictPairCount { get; init; }
|
||||
public required double MinX { get; init; }
|
||||
public required double MaxX { get; init; }
|
||||
}
|
||||
|
||||
internal static class ElkRepeatCollectorCorridors
|
||||
@@ -22,13 +25,12 @@ internal static class ElkRepeatCollectorCorridors
|
||||
var count = 0;
|
||||
foreach (var group in groups)
|
||||
{
|
||||
var edgeCount = group.EdgeIds.Length;
|
||||
if (edgeCount < 2)
|
||||
if (group.ConflictPairCount <= 0 || group.EdgeIds.Length < 2)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
count += edgeCount * (edgeCount - 1) / 2;
|
||||
count += group.ConflictPairCount;
|
||||
if (severityByEdgeId is null)
|
||||
{
|
||||
continue;
|
||||
@@ -37,7 +39,7 @@ internal static class ElkRepeatCollectorCorridors
|
||||
foreach (var edgeId in group.EdgeIds)
|
||||
{
|
||||
severityByEdgeId[edgeId] = severityByEdgeId.GetValueOrDefault(edgeId)
|
||||
+ ((edgeCount - 1) * severityWeight);
|
||||
+ (severityWeight * Math.Max(1, group.ConflictPairCount));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,6 +57,10 @@ internal static class ElkRepeatCollectorCorridors
|
||||
|
||||
var graphMinY = nodes.Min(node => node.Y);
|
||||
var graphMaxY = nodes.Max(node => node.Y + node.Height);
|
||||
var serviceNodes = nodes.Where(node => node.Kind is not "Start" and not "End").ToArray();
|
||||
var minLineClearance = serviceNodes.Length > 0
|
||||
? Math.Min(serviceNodes.Average(node => node.Width), serviceNodes.Average(node => node.Height)) / 2d
|
||||
: 50d;
|
||||
var candidates = edges
|
||||
.Select(edge => CreateCandidate(edge, graphMinY, graphMaxY))
|
||||
.Where(candidate => candidate is not null)
|
||||
@@ -76,58 +82,38 @@ internal static class ElkRepeatCollectorCorridors
|
||||
StringComparer.Ordinal))
|
||||
{
|
||||
var bucket = groupedCandidates.ToArray();
|
||||
var visited = new bool[bucket.Length];
|
||||
var conflictPairs = 0;
|
||||
for (var i = 0; i < bucket.Length; i++)
|
||||
{
|
||||
if (visited[i])
|
||||
for (var j = i + 1; j < bucket.Length; j++)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var pending = new Queue<int>();
|
||||
var component = new List<CollectorCandidate>();
|
||||
pending.Enqueue(i);
|
||||
visited[i] = true;
|
||||
|
||||
while (pending.Count > 0)
|
||||
{
|
||||
var currentIndex = pending.Dequeue();
|
||||
var current = bucket[currentIndex];
|
||||
component.Add(current);
|
||||
|
||||
for (var j = 0; j < bucket.Length; j++)
|
||||
if (ConflictsOnOuterLane(bucket[i], bucket[j], minLineClearance))
|
||||
{
|
||||
if (visited[j] || currentIndex == j)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!SharesOuterLane(current, bucket[j]))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
visited[j] = true;
|
||||
pending.Enqueue(j);
|
||||
conflictPairs++;
|
||||
}
|
||||
}
|
||||
|
||||
if (component.Count < 2)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
groups.Add(new ElkRepeatCollectorCorridorGroup
|
||||
{
|
||||
TargetNodeId = component[0].TargetNodeId,
|
||||
IsAbove = component[0].IsAbove,
|
||||
CorridorY = component.Min(member => member.CorridorY),
|
||||
EdgeIds = component
|
||||
.Select(member => member.EdgeId)
|
||||
.OrderBy(edgeId => edgeId, StringComparer.Ordinal)
|
||||
.ToArray(),
|
||||
});
|
||||
}
|
||||
|
||||
if (conflictPairs <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
groups.Add(new ElkRepeatCollectorCorridorGroup
|
||||
{
|
||||
TargetNodeId = bucket[0].TargetNodeId,
|
||||
IsAbove = bucket[0].IsAbove,
|
||||
CorridorY = bucket[0].IsAbove
|
||||
? bucket.Min(member => member.CorridorY)
|
||||
: bucket.Max(member => member.CorridorY),
|
||||
EdgeIds = bucket
|
||||
.Select(member => member.EdgeId)
|
||||
.OrderBy(edgeId => edgeId, StringComparer.Ordinal)
|
||||
.ToArray(),
|
||||
ConflictPairCount = conflictPairs,
|
||||
MinX = bucket.Min(member => member.MinX),
|
||||
MaxX = bucket.Max(member => member.MaxX),
|
||||
});
|
||||
}
|
||||
|
||||
return groups;
|
||||
@@ -172,8 +158,15 @@ internal static class ElkRepeatCollectorCorridors
|
||||
.Select((edge, index) => new { edge, index, candidate = CreateCandidate(edge, graphMinY, graphMaxY) })
|
||||
.Where(item => item.candidate is not null
|
||||
&& group.EdgeIds.Contains(item.edge.Id, StringComparer.Ordinal))
|
||||
.Select(item => new RepairMember(item.index, item.edge.Id, item.candidate!.Value.CorridorY, item.candidate.Value.StartX))
|
||||
.OrderByDescending(member => member.StartX)
|
||||
.Select(item => new RepairMember(
|
||||
item.index,
|
||||
item.edge.Id,
|
||||
item.candidate!.Value.CorridorY,
|
||||
item.candidate.Value.StartX,
|
||||
item.candidate.Value.MinX,
|
||||
item.candidate.Value.MaxX))
|
||||
.OrderBy(member => member.CorridorY)
|
||||
.ThenByDescending(member => member.StartX)
|
||||
.ThenBy(member => member.EdgeId, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
if (members.Length < 2)
|
||||
@@ -181,16 +174,30 @@ internal static class ElkRepeatCollectorCorridors
|
||||
continue;
|
||||
}
|
||||
|
||||
var baseY = group.IsAbove
|
||||
? Math.Min(members.Min(member => member.CorridorY), graphMinY - 12d)
|
||||
: Math.Max(members.Max(member => member.CorridorY), graphMaxY + 12d);
|
||||
|
||||
for (var i = 0; i < members.Length; i++)
|
||||
var forbiddenBands = BuildForbiddenCorridorBands(
|
||||
nodes,
|
||||
members.Min(member => member.MinX),
|
||||
members.Max(member => member.MaxX),
|
||||
minLineClearance);
|
||||
if (group.IsAbove)
|
||||
{
|
||||
var assignedY = group.IsAbove
|
||||
? baseY - (laneGap * i)
|
||||
: baseY + (laneGap * i);
|
||||
result[members[i].Index] = RewriteOuterLane(result[members[i].Index], members[i].CorridorY, assignedY, graphMinY, graphMaxY, group.IsAbove);
|
||||
var nextY = Math.Min(members.Max(member => member.CorridorY), graphMinY - 12d);
|
||||
nextY = ResolveSafeCorridorY(nextY, group.IsAbove, laneGap, forbiddenBands);
|
||||
for (var i = members.Length - 1; i >= 0; i--)
|
||||
{
|
||||
result[members[i].Index] = RewriteOuterLane(result[members[i].Index], members[i].CorridorY, nextY, graphMinY, graphMaxY, group.IsAbove);
|
||||
nextY = ResolveSafeCorridorY(nextY - laneGap, group.IsAbove, laneGap, forbiddenBands);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var nextY = Math.Max(members.Min(member => member.CorridorY), graphMaxY + 12d);
|
||||
nextY = ResolveSafeCorridorY(nextY, group.IsAbove, laneGap, forbiddenBands);
|
||||
for (var i = 0; i < members.Length; i++)
|
||||
{
|
||||
result[members[i].Index] = RewriteOuterLane(result[members[i].Index], members[i].CorridorY, nextY, graphMinY, graphMaxY, group.IsAbove);
|
||||
nextY = ResolveSafeCorridorY(nextY + laneGap, group.IsAbove, laneGap, forbiddenBands);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,7 +247,10 @@ internal static class ElkRepeatCollectorCorridors
|
||||
};
|
||||
}
|
||||
|
||||
private static bool SharesOuterLane(CollectorCandidate left, CollectorCandidate right)
|
||||
private static bool ConflictsOnOuterLane(
|
||||
CollectorCandidate left,
|
||||
CollectorCandidate right,
|
||||
double minLineClearance)
|
||||
{
|
||||
if (!string.Equals(left.TargetNodeId, right.TargetNodeId, StringComparison.Ordinal)
|
||||
|| left.IsAbove != right.IsAbove)
|
||||
@@ -248,12 +258,12 @@ internal static class ElkRepeatCollectorCorridors
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Math.Abs(left.CorridorY - right.CorridorY) > CoordinateTolerance)
|
||||
if (Math.Min(left.MaxX, right.MaxX) - Math.Max(left.MinX, right.MinX) <= 1d)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return Math.Min(left.MaxX, right.MaxX) - Math.Max(left.MinX, right.MinX) > 1d;
|
||||
return Math.Abs(left.CorridorY - right.CorridorY) < minLineClearance - CoordinateTolerance;
|
||||
}
|
||||
|
||||
private static CollectorCandidate? CreateCandidate(
|
||||
@@ -316,6 +326,59 @@ internal static class ElkRepeatCollectorCorridors
|
||||
return best;
|
||||
}
|
||||
|
||||
private static (double Top, double Bottom)[] BuildForbiddenCorridorBands(
|
||||
IReadOnlyCollection<ElkPositionedNode> nodes,
|
||||
double spanMinX,
|
||||
double spanMaxX,
|
||||
double minLineClearance)
|
||||
{
|
||||
return nodes
|
||||
.Where(node => node.Kind is not "Start" and not "End")
|
||||
.Where(node => node.X + node.Width > spanMinX + CoordinateTolerance
|
||||
&& node.X < spanMaxX - CoordinateTolerance)
|
||||
.Select(node => (
|
||||
Top: node.Y - minLineClearance,
|
||||
Bottom: node.Y + node.Height + minLineClearance))
|
||||
.OrderBy(band => band.Top)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private static double ResolveSafeCorridorY(
|
||||
double candidateY,
|
||||
bool isAbove,
|
||||
double laneGap,
|
||||
IReadOnlyList<(double Top, double Bottom)> forbiddenBands)
|
||||
{
|
||||
if (forbiddenBands.Count == 0)
|
||||
{
|
||||
return candidateY;
|
||||
}
|
||||
|
||||
while (true)
|
||||
{
|
||||
var shifted = false;
|
||||
foreach (var band in forbiddenBands)
|
||||
{
|
||||
if (candidateY < band.Top - CoordinateTolerance
|
||||
|| candidateY > band.Bottom + CoordinateTolerance)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
candidateY = isAbove
|
||||
? band.Top - laneGap
|
||||
: band.Bottom + laneGap;
|
||||
shifted = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!shifted)
|
||||
{
|
||||
return candidateY;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static List<ElkPoint> ExtractPath(ElkRoutedEdge edge)
|
||||
{
|
||||
var path = new List<ElkPoint>();
|
||||
@@ -343,5 +406,11 @@ internal static class ElkRepeatCollectorCorridors
|
||||
double StartX,
|
||||
double Length);
|
||||
|
||||
private readonly record struct RepairMember(int Index, string EdgeId, double CorridorY, double StartX);
|
||||
private readonly record struct RepairMember(
|
||||
int Index,
|
||||
string EdgeId,
|
||||
double CorridorY,
|
||||
double StartX,
|
||||
double MinX,
|
||||
double MaxX);
|
||||
}
|
||||
|
||||
@@ -2,9 +2,17 @@ namespace StellaOps.ElkSharp;
|
||||
|
||||
internal static class ElkShapeBoundaries
|
||||
{
|
||||
private const double CoordinateTolerance = 0.5d;
|
||||
private const double GatewayVertexTolerance = 3d;
|
||||
|
||||
internal static bool IsGatewayShape(ElkPositionedNode node)
|
||||
{
|
||||
return node.Kind is "Decision" or "Fork" or "Join";
|
||||
}
|
||||
|
||||
internal static ElkPoint ProjectOntoShapeBoundary(ElkPositionedNode node, ElkPoint toward)
|
||||
{
|
||||
if (node.Kind is "Decision" or "Fork" or "Join")
|
||||
if (IsGatewayShape(node))
|
||||
{
|
||||
var cx = node.X + node.Width / 2d;
|
||||
var cy = node.Y + node.Height / 2d;
|
||||
@@ -16,6 +24,150 @@ internal static class ElkShapeBoundaries
|
||||
return ProjectOntoRectBoundary(node, toward);
|
||||
}
|
||||
|
||||
internal static bool TryProjectGatewayDiagonalBoundary(
|
||||
ElkPositionedNode node,
|
||||
ElkPoint anchor,
|
||||
ElkPoint fallbackBoundary,
|
||||
out ElkPoint boundaryPoint)
|
||||
{
|
||||
boundaryPoint = default!;
|
||||
if (!IsGatewayShape(node))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var candidates = new List<ElkPoint>();
|
||||
var projectedAnchor = ProjectOntoShapeBoundary(node, anchor);
|
||||
AddGatewayCandidate(node, candidates, projectedAnchor);
|
||||
AddGatewayCandidate(node, candidates, fallbackBoundary);
|
||||
AddGatewayCandidate(node, candidates, ProjectOntoShapeBoundary(node, fallbackBoundary));
|
||||
|
||||
foreach (var vertex in BuildGatewayBoundaryPoints(node))
|
||||
{
|
||||
AddGatewayCandidate(node, candidates, vertex);
|
||||
}
|
||||
|
||||
var centerX = node.X + (node.Width / 2d);
|
||||
var centerY = node.Y + (node.Height / 2d);
|
||||
var directionX = Math.Sign(centerX - anchor.X);
|
||||
var directionY = Math.Sign(centerY - anchor.Y);
|
||||
var diagonalDirections = new HashSet<(int X, int Y)>();
|
||||
if (directionX != 0 && directionY != 0)
|
||||
{
|
||||
diagonalDirections.Add((directionX, directionY));
|
||||
}
|
||||
|
||||
var fallbackDirectionX = Math.Sign(fallbackBoundary.X - anchor.X);
|
||||
var fallbackDirectionY = Math.Sign(fallbackBoundary.Y - anchor.Y);
|
||||
if (fallbackDirectionX != 0 && fallbackDirectionY != 0)
|
||||
{
|
||||
diagonalDirections.Add((fallbackDirectionX, fallbackDirectionY));
|
||||
}
|
||||
|
||||
foreach (var diagonalDirection in diagonalDirections)
|
||||
{
|
||||
if (TryIntersectGatewayRay(
|
||||
node,
|
||||
anchor.X,
|
||||
anchor.Y,
|
||||
diagonalDirection.X,
|
||||
diagonalDirection.Y,
|
||||
out var rayBoundary))
|
||||
{
|
||||
AddGatewayCandidate(node, candidates, rayBoundary);
|
||||
}
|
||||
}
|
||||
|
||||
var bestCandidate = default(ElkPoint?);
|
||||
var bestScore = double.PositiveInfinity;
|
||||
foreach (var candidate in candidates)
|
||||
{
|
||||
var score = ScoreGatewayBoundaryCandidate(node, anchor, projectedAnchor, candidate);
|
||||
if (score >= bestScore)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
bestScore = score;
|
||||
bestCandidate = candidate;
|
||||
}
|
||||
|
||||
if (bestCandidate is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
boundaryPoint = PreferGatewayEdgeInteriorBoundary(node, bestCandidate, anchor);
|
||||
return true;
|
||||
}
|
||||
|
||||
internal static bool HasValidGatewayBoundaryAngle(
|
||||
ElkPositionedNode node,
|
||||
ElkPoint boundaryPoint,
|
||||
ElkPoint adjacentPoint)
|
||||
{
|
||||
if (!IsGatewayShape(node))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var segDx = Math.Abs(boundaryPoint.X - adjacentPoint.X);
|
||||
var segDy = Math.Abs(boundaryPoint.Y - adjacentPoint.Y);
|
||||
if (segDx < 3d && segDy < 3d)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!IsPointOnGatewayBoundary(node, boundaryPoint, 2d))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (IsInsideNodeShapeInterior(node, adjacentPoint))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (IsDisallowedGatewayVertex(node, boundaryPoint))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (IsAllowedGatewayTipVertex(node, boundaryPoint))
|
||||
{
|
||||
return segDx > segDy * 3d;
|
||||
}
|
||||
|
||||
if (!TryGetGatewayBoundaryFace(node, boundaryPoint, out var faceStart, out var faceEnd))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var outwardVectorX = adjacentPoint.X - boundaryPoint.X;
|
||||
var outwardVectorY = adjacentPoint.Y - boundaryPoint.Y;
|
||||
var outwardLength = Math.Sqrt((outwardVectorX * outwardVectorX) + (outwardVectorY * outwardVectorY));
|
||||
if (outwardLength <= 0.001d)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var (normalX, normalY) = BuildGatewayFaceNormal(node, faceStart, faceEnd, boundaryPoint);
|
||||
var outwardDot = ((outwardVectorX / outwardLength) * normalX) + ((outwardVectorY / outwardLength) * normalY);
|
||||
var faceDx = Math.Abs(faceEnd.X - faceStart.X);
|
||||
var faceDy = Math.Abs(faceEnd.Y - faceStart.Y);
|
||||
var faceIsDiagonal = faceDx >= 3d && faceDy >= 3d;
|
||||
|
||||
if (faceIsDiagonal)
|
||||
{
|
||||
// Diamond-like faces can leave/arrive with a short 45-degree or orthogonal
|
||||
// stub as long as that stub moves outward from the face and does not land on
|
||||
// a corner vertex.
|
||||
return outwardDot >= 0.55d;
|
||||
}
|
||||
|
||||
return (segDx < 3d || segDy < 3d) && outwardDot >= 0.85d;
|
||||
}
|
||||
|
||||
internal static ElkPoint ProjectOntoRectBoundary(ElkPositionedNode node, ElkPoint toward)
|
||||
{
|
||||
var cx = node.X + node.Width / 2d;
|
||||
@@ -98,6 +250,22 @@ internal static class ElkShapeBoundaries
|
||||
BuildForkBoundaryPoints(node));
|
||||
}
|
||||
|
||||
internal static IReadOnlyList<ElkPoint> BuildGatewayBoundaryPoints(ElkPositionedNode node)
|
||||
{
|
||||
if (node.Kind == "Decision")
|
||||
{
|
||||
return
|
||||
[
|
||||
new ElkPoint { X = node.X + (node.Width / 2d), Y = node.Y },
|
||||
new ElkPoint { X = node.X + node.Width, Y = node.Y + (node.Height / 2d) },
|
||||
new ElkPoint { X = node.X + (node.Width / 2d), Y = node.Y + node.Height },
|
||||
new ElkPoint { X = node.X, Y = node.Y + (node.Height / 2d) },
|
||||
];
|
||||
}
|
||||
|
||||
return BuildForkBoundaryPoints(node);
|
||||
}
|
||||
|
||||
internal static IReadOnlyList<ElkPoint> BuildForkBoundaryPoints(ElkPositionedNode node)
|
||||
{
|
||||
var cornerInset = Math.Min(22d, Math.Max(6d, node.Width * 0.125d));
|
||||
@@ -188,4 +356,805 @@ internal static class ElkShapeBoundaries
|
||||
{
|
||||
return (ax * by) - (ay * bx);
|
||||
}
|
||||
|
||||
private static bool TryIntersectGatewayRay(
|
||||
ElkPositionedNode node,
|
||||
double originX,
|
||||
double originY,
|
||||
double deltaX,
|
||||
double deltaY,
|
||||
out ElkPoint boundaryPoint)
|
||||
{
|
||||
var polygon = BuildGatewayBoundaryPoints(node);
|
||||
var bestScale = double.PositiveInfinity;
|
||||
ElkPoint? bestPoint = null;
|
||||
for (var index = 0; index < polygon.Count; index++)
|
||||
{
|
||||
var start = polygon[index];
|
||||
var end = polygon[(index + 1) % polygon.Count];
|
||||
if (!TryIntersectRayWithSegment(originX, originY, deltaX, deltaY, start, end, out var scale, out var point))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (scale < bestScale)
|
||||
{
|
||||
bestScale = scale;
|
||||
bestPoint = point;
|
||||
}
|
||||
}
|
||||
|
||||
boundaryPoint = bestPoint ?? default!;
|
||||
return bestPoint is not null;
|
||||
}
|
||||
|
||||
private static void AddGatewayCandidate(
|
||||
ElkPositionedNode node,
|
||||
ICollection<ElkPoint> candidates,
|
||||
ElkPoint candidate)
|
||||
{
|
||||
if (!IsPointOnGatewayBoundary(node, candidate, 2d))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (candidates.Any(existing =>
|
||||
Math.Abs(existing.X - candidate.X) <= CoordinateTolerance
|
||||
&& Math.Abs(existing.Y - candidate.Y) <= CoordinateTolerance))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
candidates.Add(candidate);
|
||||
}
|
||||
|
||||
private static double ScoreGatewayBoundaryCandidate(
|
||||
ElkPositionedNode node,
|
||||
ElkPoint anchor,
|
||||
ElkPoint projectedAnchor,
|
||||
ElkPoint candidate)
|
||||
{
|
||||
var towardCenterX = (node.X + (node.Width / 2d)) - anchor.X;
|
||||
var towardCenterY = (node.Y + (node.Height / 2d)) - anchor.Y;
|
||||
var candidateDeltaX = candidate.X - anchor.X;
|
||||
var candidateDeltaY = candidate.Y - anchor.Y;
|
||||
var towardDot = (candidateDeltaX * towardCenterX) + (candidateDeltaY * towardCenterY);
|
||||
if (towardDot <= 0d)
|
||||
{
|
||||
return double.PositiveInfinity;
|
||||
}
|
||||
|
||||
var absDx = Math.Abs(candidateDeltaX);
|
||||
var absDy = Math.Abs(candidateDeltaY);
|
||||
var isDiagonal = absDx >= 3d && absDy >= 3d;
|
||||
var diagonalPenalty = isDiagonal
|
||||
? Math.Abs(absDx - absDy)
|
||||
: 10_000d;
|
||||
var projectedDistance = Math.Abs(candidate.X - projectedAnchor.X) + Math.Abs(candidate.Y - projectedAnchor.Y);
|
||||
var segmentLength = Math.Sqrt((candidateDeltaX * candidateDeltaX) + (candidateDeltaY * candidateDeltaY));
|
||||
var candidateNearVertex = IsNearGatewayVertex(node, candidate, GatewayVertexTolerance);
|
||||
var projectedNearVertex = IsNearGatewayVertex(node, projectedAnchor, GatewayVertexTolerance);
|
||||
var vertexPenalty = candidateNearVertex
|
||||
? projectedNearVertex
|
||||
? 4d
|
||||
: 24d
|
||||
: 0d;
|
||||
|
||||
return diagonalPenalty + (segmentLength * 0.05d) + (projectedDistance * 0.1d) + vertexPenalty;
|
||||
}
|
||||
|
||||
private static ElkPoint InterpolateAwayFromVertex(
|
||||
ElkPoint vertexPoint,
|
||||
ElkPoint adjacentVertex,
|
||||
double? forcedOffset = null)
|
||||
{
|
||||
var deltaX = adjacentVertex.X - vertexPoint.X;
|
||||
var deltaY = adjacentVertex.Y - vertexPoint.Y;
|
||||
var length = Math.Sqrt((deltaX * deltaX) + (deltaY * deltaY));
|
||||
if (length <= 0.001d)
|
||||
{
|
||||
return vertexPoint;
|
||||
}
|
||||
|
||||
var offset = forcedOffset ?? Math.Min(18d, Math.Max(10d, length * 0.2d));
|
||||
offset = Math.Min(Math.Max(length - 0.5d, 0.5d), offset);
|
||||
var scale = offset / length;
|
||||
return new ElkPoint
|
||||
{
|
||||
X = vertexPoint.X + (deltaX * scale),
|
||||
Y = vertexPoint.Y + (deltaY * scale),
|
||||
};
|
||||
}
|
||||
|
||||
private static bool IsPointOnGatewayBoundary(ElkPositionedNode node, ElkPoint point, double tolerance)
|
||||
{
|
||||
var polygon = BuildGatewayBoundaryPoints(node);
|
||||
for (var index = 0; index < polygon.Count; index++)
|
||||
{
|
||||
var start = polygon[index];
|
||||
var end = polygon[(index + 1) % polygon.Count];
|
||||
if (DistanceToSegment(point, start, end) <= tolerance)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
internal static bool IsNearGatewayVertex(ElkPositionedNode node, ElkPoint boundaryPoint, double tolerance = GatewayVertexTolerance)
|
||||
{
|
||||
foreach (var vertex in BuildGatewayBoundaryPoints(node))
|
||||
{
|
||||
if (Math.Abs(vertex.X - boundaryPoint.X) <= tolerance
|
||||
&& Math.Abs(vertex.Y - boundaryPoint.Y) <= tolerance)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
internal static bool IsAllowedGatewayTipVertex(
|
||||
ElkPositionedNode node,
|
||||
ElkPoint boundaryPoint,
|
||||
double tolerance = GatewayVertexTolerance)
|
||||
{
|
||||
// Gateway tips read as visually detached "pin" exits/entries in the renderer.
|
||||
// Keep all gateway joins on a face interior instead of permitting any tip vertex.
|
||||
return false;
|
||||
}
|
||||
|
||||
internal static bool IsInsideNodeBoundingBoxInterior(
|
||||
ElkPositionedNode node,
|
||||
ElkPoint point,
|
||||
double tolerance = CoordinateTolerance)
|
||||
{
|
||||
return point.X > node.X + tolerance
|
||||
&& point.X < node.X + node.Width - tolerance
|
||||
&& point.Y > node.Y + tolerance
|
||||
&& point.Y < node.Y + node.Height - tolerance;
|
||||
}
|
||||
|
||||
internal static bool IsInsideNodeShapeInterior(
|
||||
ElkPositionedNode node,
|
||||
ElkPoint point,
|
||||
double tolerance = CoordinateTolerance)
|
||||
{
|
||||
if (!IsGatewayShape(node))
|
||||
{
|
||||
return IsInsideNodeBoundingBoxInterior(node, point, tolerance);
|
||||
}
|
||||
|
||||
if (!IsInsideNodeBoundingBoxInterior(node, point, tolerance))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (IsPointOnGatewayBoundary(node, point, Math.Max(2d, tolerance * 2d)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var polygon = BuildGatewayBoundaryPoints(node);
|
||||
bool? hasPositiveSign = null;
|
||||
for (var index = 0; index < polygon.Count; index++)
|
||||
{
|
||||
var start = polygon[index];
|
||||
var end = polygon[(index + 1) % polygon.Count];
|
||||
var cross = Cross(end.X - start.X, end.Y - start.Y, point.X - start.X, point.Y - start.Y);
|
||||
if (Math.Abs(cross) <= tolerance)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var isPositive = cross > 0d;
|
||||
if (!hasPositiveSign.HasValue)
|
||||
{
|
||||
hasPositiveSign = isPositive;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (hasPositiveSign.Value != isPositive)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return hasPositiveSign.HasValue;
|
||||
}
|
||||
|
||||
internal static ElkPoint PreferGatewayEdgeInteriorBoundary(
|
||||
ElkPositionedNode node,
|
||||
ElkPoint boundaryPoint,
|
||||
ElkPoint anchor)
|
||||
{
|
||||
if (!IsGatewayShape(node) || !IsNearGatewayVertex(node, boundaryPoint))
|
||||
{
|
||||
return boundaryPoint;
|
||||
}
|
||||
|
||||
if (IsAllowedGatewayTipVertex(node, boundaryPoint))
|
||||
{
|
||||
return boundaryPoint;
|
||||
}
|
||||
|
||||
var polygon = BuildGatewayBoundaryPoints(node);
|
||||
var nearestVertexIndex = -1;
|
||||
var nearestVertexDistance = double.PositiveInfinity;
|
||||
for (var index = 0; index < polygon.Count; index++)
|
||||
{
|
||||
var vertex = polygon[index];
|
||||
var deltaX = boundaryPoint.X - vertex.X;
|
||||
var deltaY = boundaryPoint.Y - vertex.Y;
|
||||
var distance = Math.Sqrt((deltaX * deltaX) + (deltaY * deltaY));
|
||||
if (distance >= nearestVertexDistance)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
nearestVertexDistance = distance;
|
||||
nearestVertexIndex = index;
|
||||
}
|
||||
|
||||
if (nearestVertexIndex < 0)
|
||||
{
|
||||
return boundaryPoint;
|
||||
}
|
||||
|
||||
var vertexPoint = polygon[nearestVertexIndex];
|
||||
var previousVertex = polygon[(nearestVertexIndex - 1 + polygon.Count) % polygon.Count];
|
||||
var nextVertex = polygon[(nearestVertexIndex + 1) % polygon.Count];
|
||||
var projectedAnchor = ProjectOntoShapeBoundary(node, anchor);
|
||||
var candidates = new[]
|
||||
{
|
||||
InterpolateAwayFromVertex(vertexPoint, previousVertex),
|
||||
InterpolateAwayFromVertex(vertexPoint, nextVertex),
|
||||
};
|
||||
|
||||
var bestCandidate = boundaryPoint;
|
||||
var bestScore = double.PositiveInfinity;
|
||||
foreach (var candidate in candidates)
|
||||
{
|
||||
if (!IsPointOnGatewayBoundary(node, candidate, 2d))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var score = ScoreGatewayBoundaryCandidate(node, anchor, projectedAnchor, candidate);
|
||||
if (score >= bestScore)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
bestScore = score;
|
||||
bestCandidate = candidate;
|
||||
}
|
||||
|
||||
if (IsNearGatewayVertex(node, bestCandidate))
|
||||
{
|
||||
var forcedOffset = node.Kind == "Decision"
|
||||
? 18d
|
||||
: 14d;
|
||||
var forcedCandidates = new[]
|
||||
{
|
||||
InterpolateAwayFromVertex(vertexPoint, previousVertex, forcedOffset),
|
||||
InterpolateAwayFromVertex(vertexPoint, nextVertex, forcedOffset),
|
||||
};
|
||||
|
||||
foreach (var candidate in forcedCandidates)
|
||||
{
|
||||
if (!IsPointOnGatewayBoundary(node, candidate, 2.5d))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var score = ScoreGatewayBoundaryCandidate(node, anchor, projectedAnchor, candidate);
|
||||
if (score >= bestScore)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
bestScore = score;
|
||||
bestCandidate = candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return bestCandidate;
|
||||
}
|
||||
|
||||
internal static bool IsGatewayBoundaryPoint(
|
||||
ElkPositionedNode node,
|
||||
ElkPoint point,
|
||||
double tolerance = 2d)
|
||||
{
|
||||
return IsGatewayShape(node) && IsPointOnGatewayBoundary(node, point, tolerance);
|
||||
}
|
||||
|
||||
internal static bool TryProjectGatewayBoundarySlot(
|
||||
ElkPositionedNode node,
|
||||
string side,
|
||||
double slotCoordinate,
|
||||
out ElkPoint boundaryPoint)
|
||||
{
|
||||
boundaryPoint = default!;
|
||||
if (!IsGatewayShape(node))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var candidates = new List<ElkPoint>();
|
||||
var polygon = BuildGatewayBoundaryPoints(node);
|
||||
switch (side)
|
||||
{
|
||||
case "left":
|
||||
case "right":
|
||||
{
|
||||
var y = Math.Max(node.Y + 4d, Math.Min(node.Y + node.Height - 4d, slotCoordinate));
|
||||
for (var index = 0; index < polygon.Count; index++)
|
||||
{
|
||||
var start = polygon[index];
|
||||
var end = polygon[(index + 1) % polygon.Count];
|
||||
AddGatewaySlotIntersections(candidates, TryIntersectHorizontalSlot(start, end, y));
|
||||
}
|
||||
|
||||
if (candidates.Count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
boundaryPoint = side == "left"
|
||||
? candidates.OrderBy(point => point.X).ThenBy(point => point.Y).First()
|
||||
: candidates.OrderByDescending(point => point.X).ThenBy(point => point.Y).First();
|
||||
boundaryPoint = PreferGatewayEdgeInteriorBoundary(
|
||||
node,
|
||||
boundaryPoint,
|
||||
new ElkPoint
|
||||
{
|
||||
X = side == "left" ? node.X - 32d : node.X + node.Width + 32d,
|
||||
Y = y,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
case "top":
|
||||
case "bottom":
|
||||
{
|
||||
var x = Math.Max(node.X + 4d, Math.Min(node.X + node.Width - 4d, slotCoordinate));
|
||||
for (var index = 0; index < polygon.Count; index++)
|
||||
{
|
||||
var start = polygon[index];
|
||||
var end = polygon[(index + 1) % polygon.Count];
|
||||
AddGatewaySlotIntersections(candidates, TryIntersectVerticalSlot(start, end, x));
|
||||
}
|
||||
|
||||
if (candidates.Count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
boundaryPoint = side == "top"
|
||||
? candidates.OrderBy(point => point.Y).ThenBy(point => point.X).First()
|
||||
: candidates.OrderByDescending(point => point.Y).ThenBy(point => point.X).First();
|
||||
boundaryPoint = PreferGatewayEdgeInteriorBoundary(
|
||||
node,
|
||||
boundaryPoint,
|
||||
new ElkPoint
|
||||
{
|
||||
X = x,
|
||||
Y = side == "top" ? node.Y - 32d : node.Y + node.Height + 32d,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
internal static ElkPoint BuildGatewayExteriorApproachPoint(
|
||||
ElkPositionedNode node,
|
||||
ElkPoint boundaryPoint,
|
||||
double padding = 8d)
|
||||
{
|
||||
if (!IsGatewayShape(node)
|
||||
|| !TryGetGatewayBoundaryFace(node, boundaryPoint, out var faceStart, out var faceEnd))
|
||||
{
|
||||
return boundaryPoint;
|
||||
}
|
||||
|
||||
var (normalX, normalY) = BuildGatewayFaceNormal(node, faceStart, faceEnd, boundaryPoint);
|
||||
var exitDistance = ComputeRayExitDistanceFromBoundingBox(node, boundaryPoint, normalX, normalY);
|
||||
var offset = Math.Max(0.5d, exitDistance + padding);
|
||||
return new ElkPoint
|
||||
{
|
||||
X = boundaryPoint.X + (normalX * offset),
|
||||
Y = boundaryPoint.Y + (normalY * offset),
|
||||
};
|
||||
}
|
||||
|
||||
internal static ElkPoint BuildGatewayDirectionalExteriorPoint(
|
||||
ElkPositionedNode node,
|
||||
ElkPoint boundaryPoint,
|
||||
ElkPoint referencePoint,
|
||||
double padding = 8d)
|
||||
{
|
||||
if (!IsGatewayShape(node))
|
||||
{
|
||||
return boundaryPoint;
|
||||
}
|
||||
|
||||
var candidates = new List<ElkPoint>
|
||||
{
|
||||
BuildGatewayExteriorApproachPoint(node, boundaryPoint, padding),
|
||||
};
|
||||
|
||||
var horizontalDirection = Math.Sign(referencePoint.X - boundaryPoint.X);
|
||||
if (horizontalDirection != 0d)
|
||||
{
|
||||
candidates.Add(new ElkPoint
|
||||
{
|
||||
X = horizontalDirection > 0d
|
||||
? node.X + node.Width + padding
|
||||
: node.X - padding,
|
||||
Y = boundaryPoint.Y,
|
||||
});
|
||||
}
|
||||
|
||||
var verticalDirection = Math.Sign(referencePoint.Y - boundaryPoint.Y);
|
||||
if (verticalDirection != 0d)
|
||||
{
|
||||
candidates.Add(new ElkPoint
|
||||
{
|
||||
X = boundaryPoint.X,
|
||||
Y = verticalDirection > 0d
|
||||
? node.Y + node.Height + padding
|
||||
: node.Y - padding,
|
||||
});
|
||||
}
|
||||
|
||||
ElkPoint? bestCandidate = null;
|
||||
var bestScore = double.PositiveInfinity;
|
||||
foreach (var candidate in candidates)
|
||||
{
|
||||
if (IsInsideNodeBoundingBoxInterior(node, candidate)
|
||||
|| !HasValidGatewayBoundaryAngle(node, boundaryPoint, candidate))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var deltaX = candidate.X - boundaryPoint.X;
|
||||
var deltaY = candidate.Y - boundaryPoint.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);
|
||||
|
||||
if (Math.Abs(referencePoint.X - boundaryPoint.X) >= Math.Abs(referencePoint.Y - boundaryPoint.Y) * 1.2d)
|
||||
{
|
||||
if (Math.Sign(deltaX) != Math.Sign(referencePoint.X - boundaryPoint.X))
|
||||
{
|
||||
score += 10_000d;
|
||||
}
|
||||
|
||||
score += Math.Abs(deltaY) * 0.35d;
|
||||
}
|
||||
else if (Math.Abs(referencePoint.Y - boundaryPoint.Y) >= Math.Abs(referencePoint.X - boundaryPoint.X) * 1.2d)
|
||||
{
|
||||
if (Math.Sign(deltaY) != Math.Sign(referencePoint.Y - boundaryPoint.Y))
|
||||
{
|
||||
score += 10_000d;
|
||||
}
|
||||
|
||||
score += Math.Abs(deltaX) * 0.35d;
|
||||
}
|
||||
|
||||
if (score >= bestScore)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
bestScore = score;
|
||||
bestCandidate = candidate;
|
||||
}
|
||||
|
||||
return bestCandidate ?? BuildGatewayExteriorApproachPoint(node, boundaryPoint, padding);
|
||||
}
|
||||
|
||||
internal static ElkPoint BuildPreferredGatewaySourceExteriorPoint(
|
||||
ElkPositionedNode node,
|
||||
ElkPoint boundaryPoint,
|
||||
ElkPoint referencePoint,
|
||||
double padding = 8d)
|
||||
{
|
||||
if (!IsGatewayShape(node))
|
||||
{
|
||||
return boundaryPoint;
|
||||
}
|
||||
|
||||
var deltaX = referencePoint.X - boundaryPoint.X;
|
||||
var deltaY = referencePoint.Y - boundaryPoint.Y;
|
||||
if (node.Kind == "Decision"
|
||||
&& !IsNearGatewayVertex(node, boundaryPoint, 8d)
|
||||
&& TryGetGatewayBoundaryFace(node, boundaryPoint, out var faceStart, out var faceEnd))
|
||||
{
|
||||
var faceDx = Math.Abs(faceEnd.X - faceStart.X);
|
||||
var faceDy = Math.Abs(faceEnd.Y - faceStart.Y);
|
||||
var hasMaterialHorizontal = Math.Abs(deltaX) >= 12d;
|
||||
var hasMaterialVertical = Math.Abs(deltaY) >= 12d;
|
||||
var prefersDiagonalStub = hasMaterialHorizontal
|
||||
&& hasMaterialVertical
|
||||
&& Math.Abs(Math.Abs(deltaX) - Math.Abs(deltaY)) <= Math.Max(18d, Math.Min(Math.Abs(deltaX), Math.Abs(deltaY)) * 0.75d);
|
||||
if (faceDx >= 3d && faceDy >= 3d && prefersDiagonalStub)
|
||||
{
|
||||
var faceNormalCandidate = BuildGatewayExteriorApproachPoint(node, boundaryPoint, padding);
|
||||
if (!IsInsideNodeBoundingBoxInterior(node, faceNormalCandidate)
|
||||
&& HasValidGatewayBoundaryAngle(node, boundaryPoint, faceNormalCandidate))
|
||||
{
|
||||
return faceNormalCandidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var dominantHorizontal = Math.Abs(deltaX) >= Math.Abs(deltaY) * 1.15d;
|
||||
var dominantVertical = Math.Abs(deltaY) >= Math.Abs(deltaX) * 1.15d;
|
||||
|
||||
if (dominantHorizontal && Math.Sign(deltaX) != 0)
|
||||
{
|
||||
var horizontalCandidate = new ElkPoint
|
||||
{
|
||||
X = deltaX > 0d
|
||||
? node.X + node.Width + padding
|
||||
: node.X - padding,
|
||||
Y = boundaryPoint.Y,
|
||||
};
|
||||
if (!IsInsideNodeBoundingBoxInterior(node, horizontalCandidate)
|
||||
&& HasValidGatewayBoundaryAngle(node, boundaryPoint, horizontalCandidate))
|
||||
{
|
||||
return horizontalCandidate;
|
||||
}
|
||||
}
|
||||
|
||||
if (dominantVertical && Math.Sign(deltaY) != 0)
|
||||
{
|
||||
var verticalCandidate = new ElkPoint
|
||||
{
|
||||
X = boundaryPoint.X,
|
||||
Y = deltaY > 0d
|
||||
? node.Y + node.Height + padding
|
||||
: node.Y - padding,
|
||||
};
|
||||
if (!IsInsideNodeBoundingBoxInterior(node, verticalCandidate)
|
||||
&& HasValidGatewayBoundaryAngle(node, boundaryPoint, verticalCandidate))
|
||||
{
|
||||
return verticalCandidate;
|
||||
}
|
||||
}
|
||||
|
||||
return BuildGatewayDirectionalExteriorPoint(node, boundaryPoint, referencePoint, padding);
|
||||
}
|
||||
|
||||
private static void AddGatewaySlotIntersections(
|
||||
ICollection<ElkPoint> candidates,
|
||||
IEnumerable<ElkPoint> intersections)
|
||||
{
|
||||
foreach (var candidate in intersections)
|
||||
{
|
||||
if (candidates.Any(existing =>
|
||||
Math.Abs(existing.X - candidate.X) <= CoordinateTolerance
|
||||
&& Math.Abs(existing.Y - candidate.Y) <= CoordinateTolerance))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
candidates.Add(candidate);
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<ElkPoint> TryIntersectHorizontalSlot(
|
||||
ElkPoint start,
|
||||
ElkPoint end,
|
||||
double y)
|
||||
{
|
||||
if (Math.Abs(start.Y - end.Y) <= CoordinateTolerance)
|
||||
{
|
||||
if (Math.Abs(y - start.Y) > CoordinateTolerance)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
yield return new ElkPoint { X = start.X, Y = y };
|
||||
if (Math.Abs(end.X - start.X) > CoordinateTolerance)
|
||||
{
|
||||
yield return new ElkPoint { X = end.X, Y = y };
|
||||
}
|
||||
|
||||
yield break;
|
||||
}
|
||||
|
||||
var minY = Math.Min(start.Y, end.Y) - CoordinateTolerance;
|
||||
var maxY = Math.Max(start.Y, end.Y) + CoordinateTolerance;
|
||||
if (y < minY || y > maxY)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
var t = (y - start.Y) / (end.Y - start.Y);
|
||||
if (t < -CoordinateTolerance || t > 1d + CoordinateTolerance)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
yield return new ElkPoint
|
||||
{
|
||||
X = start.X + ((end.X - start.X) * t),
|
||||
Y = y,
|
||||
};
|
||||
}
|
||||
|
||||
private static IEnumerable<ElkPoint> TryIntersectVerticalSlot(
|
||||
ElkPoint start,
|
||||
ElkPoint end,
|
||||
double x)
|
||||
{
|
||||
if (Math.Abs(start.X - end.X) <= CoordinateTolerance)
|
||||
{
|
||||
if (Math.Abs(x - start.X) > CoordinateTolerance)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
yield return new ElkPoint { X = x, Y = start.Y };
|
||||
if (Math.Abs(end.Y - start.Y) > CoordinateTolerance)
|
||||
{
|
||||
yield return new ElkPoint { X = x, Y = end.Y };
|
||||
}
|
||||
|
||||
yield break;
|
||||
}
|
||||
|
||||
var minX = Math.Min(start.X, end.X) - CoordinateTolerance;
|
||||
var maxX = Math.Max(start.X, end.X) + CoordinateTolerance;
|
||||
if (x < minX || x > maxX)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
var t = (x - start.X) / (end.X - start.X);
|
||||
if (t < -CoordinateTolerance || t > 1d + CoordinateTolerance)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
yield return new ElkPoint
|
||||
{
|
||||
X = x,
|
||||
Y = start.Y + ((end.Y - start.Y) * t),
|
||||
};
|
||||
}
|
||||
|
||||
private static double DistanceToSegment(ElkPoint point, ElkPoint start, ElkPoint end)
|
||||
{
|
||||
var deltaX = end.X - start.X;
|
||||
var deltaY = end.Y - start.Y;
|
||||
var lengthSquared = (deltaX * deltaX) + (deltaY * deltaY);
|
||||
if (lengthSquared <= 0.001d)
|
||||
{
|
||||
return Math.Sqrt(((point.X - start.X) * (point.X - start.X)) + ((point.Y - start.Y) * (point.Y - start.Y)));
|
||||
}
|
||||
|
||||
var t = (((point.X - start.X) * deltaX) + ((point.Y - start.Y) * deltaY)) / lengthSquared;
|
||||
t = Math.Max(0d, Math.Min(1d, t));
|
||||
var projectionX = start.X + (t * deltaX);
|
||||
var projectionY = start.Y + (t * deltaY);
|
||||
var distanceX = point.X - projectionX;
|
||||
var distanceY = point.Y - projectionY;
|
||||
return Math.Sqrt((distanceX * distanceX) + (distanceY * distanceY));
|
||||
}
|
||||
|
||||
private static bool TryGetGatewayBoundaryFace(
|
||||
ElkPositionedNode node,
|
||||
ElkPoint boundaryPoint,
|
||||
out ElkPoint faceStart,
|
||||
out ElkPoint faceEnd)
|
||||
{
|
||||
faceStart = default!;
|
||||
faceEnd = default!;
|
||||
|
||||
var polygon = BuildGatewayBoundaryPoints(node);
|
||||
var bestDistance = double.PositiveInfinity;
|
||||
var bestIndex = -1;
|
||||
for (var index = 0; index < polygon.Count; index++)
|
||||
{
|
||||
var start = polygon[index];
|
||||
var end = polygon[(index + 1) % polygon.Count];
|
||||
var distance = DistanceToSegment(boundaryPoint, start, end);
|
||||
if (distance > 2d || distance >= bestDistance)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
bestDistance = distance;
|
||||
bestIndex = index;
|
||||
}
|
||||
|
||||
if (bestIndex < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
faceStart = polygon[bestIndex];
|
||||
faceEnd = polygon[(bestIndex + 1) % polygon.Count];
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsDisallowedGatewayVertex(
|
||||
ElkPositionedNode node,
|
||||
ElkPoint boundaryPoint)
|
||||
{
|
||||
return IsNearGatewayVertex(node, boundaryPoint, GatewayVertexTolerance)
|
||||
&& !IsAllowedGatewayTipVertex(node, boundaryPoint, GatewayVertexTolerance);
|
||||
}
|
||||
|
||||
private static (double X, double Y) BuildGatewayFaceNormal(
|
||||
ElkPositionedNode node,
|
||||
ElkPoint faceStart,
|
||||
ElkPoint faceEnd,
|
||||
ElkPoint boundaryPoint)
|
||||
{
|
||||
var deltaX = faceEnd.X - faceStart.X;
|
||||
var deltaY = faceEnd.Y - faceStart.Y;
|
||||
var length = Math.Sqrt((deltaX * deltaX) + (deltaY * deltaY));
|
||||
if (length <= 0.001d)
|
||||
{
|
||||
return (0d, -1d);
|
||||
}
|
||||
|
||||
var normalAX = deltaY / length;
|
||||
var normalAY = -deltaX / length;
|
||||
var normalBX = -normalAX;
|
||||
var normalBY = -normalAY;
|
||||
var centerX = node.X + (node.Width / 2d);
|
||||
var centerY = node.Y + (node.Height / 2d);
|
||||
var centerToBoundaryX = boundaryPoint.X - centerX;
|
||||
var centerToBoundaryY = boundaryPoint.Y - centerY;
|
||||
var dotA = (normalAX * centerToBoundaryX) + (normalAY * centerToBoundaryY);
|
||||
var dotB = (normalBX * centerToBoundaryX) + (normalBY * centerToBoundaryY);
|
||||
return dotA >= dotB
|
||||
? (normalAX, normalAY)
|
||||
: (normalBX, normalBY);
|
||||
}
|
||||
|
||||
private static double ComputeRayExitDistanceFromBoundingBox(
|
||||
ElkPositionedNode node,
|
||||
ElkPoint origin,
|
||||
double directionX,
|
||||
double directionY)
|
||||
{
|
||||
const double epsilon = 0.0001d;
|
||||
var bestDistance = double.PositiveInfinity;
|
||||
|
||||
if (directionX > epsilon)
|
||||
{
|
||||
bestDistance = Math.Min(bestDistance, (node.X + node.Width - origin.X) / directionX);
|
||||
}
|
||||
else if (directionX < -epsilon)
|
||||
{
|
||||
bestDistance = Math.Min(bestDistance, (node.X - origin.X) / directionX);
|
||||
}
|
||||
|
||||
if (directionY > epsilon)
|
||||
{
|
||||
bestDistance = Math.Min(bestDistance, (node.Y + node.Height - origin.Y) / directionY);
|
||||
}
|
||||
else if (directionY < -epsilon)
|
||||
{
|
||||
bestDistance = Math.Min(bestDistance, (node.Y - origin.Y) / directionY);
|
||||
}
|
||||
|
||||
if (double.IsInfinity(bestDistance) || bestDistance < 0d)
|
||||
{
|
||||
return 0d;
|
||||
}
|
||||
|
||||
return bestDistance;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +57,7 @@ public sealed class ElkSharpLayeredLayoutEngine : IElkLayoutEngine
|
||||
}
|
||||
|
||||
var placementIterations = ElkNodePlacement.ResolvePlacementIterationCount(options, allNodes.Count, layers.Length);
|
||||
var placementGrid = ElkNodePlacement.ResolvePlacementGrid(graph.Nodes);
|
||||
|
||||
var positionedNodes = new Dictionary<string, ElkPositionedNode>(StringComparer.Ordinal);
|
||||
var globalNodeWidth = graph.Nodes.Max(x => x.Width);
|
||||
@@ -67,14 +68,14 @@ public sealed class ElkSharpLayeredLayoutEngine : IElkLayoutEngine
|
||||
ElkSharpLayoutInitialPlacement.PlaceNodesLeftToRight(
|
||||
positionedNodes, layers, dummyResult, augmentedIncoming, augmentedOutgoing,
|
||||
augmentedNodesById, incomingNodeIds, outgoingNodeIds, nodesById,
|
||||
adaptiveNodeSpacing, options, placementIterations);
|
||||
adaptiveNodeSpacing, options, placementIterations, placementGrid);
|
||||
}
|
||||
else
|
||||
{
|
||||
ElkSharpLayoutInitialPlacement.PlaceNodesTopToBottom(
|
||||
positionedNodes, layers, dummyResult, augmentedIncoming, augmentedOutgoing,
|
||||
augmentedNodesById, incomingNodeIds, outgoingNodeIds, nodesById,
|
||||
globalNodeWidth, adaptiveNodeSpacing, options, placementIterations);
|
||||
globalNodeWidth, adaptiveNodeSpacing, options, placementIterations, placementGrid);
|
||||
}
|
||||
|
||||
var graphBounds = ElkGraphValidator.ComputeGraphBounds(positionedNodes.Values
|
||||
@@ -216,6 +217,7 @@ public sealed class ElkSharpLayeredLayoutEngine : IElkLayoutEngine
|
||||
routedEdges = ElkEdgePostProcessor.SnapAnchorsToNodeBoundary(routedEdges, finalNodes);
|
||||
// 2. Iterative multi-strategy optimizer (replaces refiner + avoid crossings + diag elim + simplify + tighten)
|
||||
routedEdges = ElkEdgeRouterIterative.Optimize(routedEdges, finalNodes, options, cancellationToken);
|
||||
ElkLayoutDiagnostics.LogProgress("ElkSharp layout optimize returned");
|
||||
|
||||
return Task.FromResult(new ElkLayoutResult
|
||||
{
|
||||
|
||||
@@ -8,13 +8,16 @@ internal static class ElkSharpLayoutInitialPlacement
|
||||
Dictionary<string, List<string>> augmentedOutgoing, Dictionary<string, ElkNode> augmentedNodesById,
|
||||
Dictionary<string, List<string>> incomingNodeIds, Dictionary<string, List<string>> outgoingNodeIds,
|
||||
Dictionary<string, ElkNode> nodesById, double adaptiveNodeSpacing,
|
||||
ElkLayoutOptions options, int placementIterations)
|
||||
ElkLayoutOptions options, int placementIterations, NodePlacementGrid placementGrid)
|
||||
{
|
||||
var globalNodeHeight = augmentedNodesById.Values
|
||||
.Where(n => !dummyResult.DummyNodeIds.Contains(n.Id))
|
||||
.Max(x => x.Height);
|
||||
var gridNodeSpacing = Math.Max(adaptiveNodeSpacing, placementGrid.YStep * 0.4d);
|
||||
var edgeDensityFactor = adaptiveNodeSpacing / options.NodeSpacing;
|
||||
var adaptiveLayerSpacing = options.LayerSpacing * Math.Min(1.15d, 0.92d + (Math.Max(0d, edgeDensityFactor - 1d) * 0.35d));
|
||||
var adaptiveLayerSpacing = Math.Max(
|
||||
options.LayerSpacing * Math.Min(1.15d, 0.92d + (Math.Max(0d, edgeDensityFactor - 1d) * 0.35d)),
|
||||
placementGrid.XStep * 0.45d);
|
||||
|
||||
var layerXPositions = new double[layers.Length];
|
||||
var currentX = 0d;
|
||||
@@ -53,7 +56,7 @@ internal static class ElkSharpLayoutInitialPlacement
|
||||
}
|
||||
else
|
||||
{
|
||||
desiredY[nodeIndex] = nodeIndex * (slotHeight + adaptiveNodeSpacing);
|
||||
desiredY[nodeIndex] = nodeIndex * (slotHeight + gridNodeSpacing);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,8 +65,8 @@ internal static class ElkSharpLayoutInitialPlacement
|
||||
var prevIsDummy = dummyResult.DummyNodeIds.Contains(layer[nodeIndex - 1].Id);
|
||||
var currIsDummy = dummyResult.DummyNodeIds.Contains(layer[nodeIndex].Id);
|
||||
var pairSpacing = (prevIsDummy && currIsDummy) ? 2d
|
||||
: (prevIsDummy || currIsDummy) ? Math.Min(adaptiveNodeSpacing, options.NodeSpacing * 0.5d)
|
||||
: adaptiveNodeSpacing;
|
||||
: (prevIsDummy || currIsDummy) ? Math.Min(gridNodeSpacing, options.NodeSpacing * 0.5d)
|
||||
: gridNodeSpacing;
|
||||
var minY = desiredY[nodeIndex - 1] + layer[nodeIndex - 1].Height + pairSpacing;
|
||||
if (desiredY[nodeIndex] < minY)
|
||||
{
|
||||
@@ -91,19 +94,19 @@ internal static class ElkSharpLayoutInitialPlacement
|
||||
|
||||
ElkNodePlacement.RefineHorizontalPlacement(positionedNodes, layers,
|
||||
incomingNodeIds, outgoingNodeIds, augmentedNodesById,
|
||||
options.NodeSpacing, placementIterations, options.Direction);
|
||||
gridNodeSpacing, placementIterations, options.Direction);
|
||||
|
||||
ElkNodePlacement.SnapOriginalPrimaryAxes(positionedNodes, layers,
|
||||
dummyResult.DummyNodeIds, incomingNodeIds, outgoingNodeIds,
|
||||
nodesById, options.NodeSpacing, options.Direction);
|
||||
nodesById, gridNodeSpacing, options.Direction);
|
||||
|
||||
ElkNodePlacementAlignment.CompactTowardIncomingFlow(positionedNodes, layers,
|
||||
dummyResult.DummyNodeIds, incomingNodeIds, nodesById,
|
||||
options.NodeSpacing, options.Direction);
|
||||
gridNodeSpacing, options.Direction);
|
||||
|
||||
ElkNodePlacement.SnapOriginalPrimaryAxes(positionedNodes, layers,
|
||||
dummyResult.DummyNodeIds, incomingNodeIds, outgoingNodeIds,
|
||||
nodesById, options.NodeSpacing, options.Direction);
|
||||
nodesById, gridNodeSpacing, options.Direction);
|
||||
|
||||
ElkNodePlacementPreferredCenter.AlignDummyNodesToFlow(positionedNodes, layers,
|
||||
dummyResult.DummyNodeIds, augmentedIncoming, augmentedOutgoing,
|
||||
@@ -123,7 +126,7 @@ internal static class ElkSharpLayoutInitialPlacement
|
||||
var pos = positionedNodes[nodeId];
|
||||
positionedNodes[nodeId] = ElkLayoutHelpers.CreatePositionedNode(
|
||||
augmentedNodesById[nodeId], pos.X, pos.Y - minNodeY, options.Direction);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,14 +136,15 @@ internal static class ElkSharpLayoutInitialPlacement
|
||||
Dictionary<string, List<string>> augmentedOutgoing, Dictionary<string, ElkNode> augmentedNodesById,
|
||||
Dictionary<string, List<string>> incomingNodeIds, Dictionary<string, List<string>> outgoingNodeIds,
|
||||
Dictionary<string, ElkNode> nodesById, double globalNodeWidth,
|
||||
double adaptiveNodeSpacing, ElkLayoutOptions options, int placementIterations)
|
||||
double adaptiveNodeSpacing, ElkLayoutOptions options, int placementIterations, NodePlacementGrid placementGrid)
|
||||
{
|
||||
var gridNodeSpacing = Math.Max(adaptiveNodeSpacing, placementGrid.XStep * 0.4d);
|
||||
var layerYPositions = new double[layers.Length];
|
||||
var currentY = 0d;
|
||||
for (var layerIndex = 0; layerIndex < layers.Length; layerIndex++)
|
||||
{
|
||||
layerYPositions[layerIndex] = currentY;
|
||||
currentY += layers[layerIndex].Max(x => x.Height) + options.LayerSpacing;
|
||||
currentY += layers[layerIndex].Max(x => x.Height) + Math.Max(options.LayerSpacing, placementGrid.YStep * 0.45d);
|
||||
}
|
||||
|
||||
var slotWidth = globalNodeWidth;
|
||||
@@ -172,7 +176,7 @@ internal static class ElkSharpLayoutInitialPlacement
|
||||
}
|
||||
else
|
||||
{
|
||||
desiredX[nodeIndex] = nodeIndex * (slotWidth + adaptiveNodeSpacing);
|
||||
desiredX[nodeIndex] = nodeIndex * (slotWidth + gridNodeSpacing);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,8 +185,8 @@ internal static class ElkSharpLayoutInitialPlacement
|
||||
var prevIsDummyX = dummyResult.DummyNodeIds.Contains(layer[nodeIndex - 1].Id);
|
||||
var currIsDummyX = dummyResult.DummyNodeIds.Contains(layer[nodeIndex].Id);
|
||||
var pairSpacingX = (prevIsDummyX && currIsDummyX) ? 2d
|
||||
: (prevIsDummyX || currIsDummyX) ? Math.Min(adaptiveNodeSpacing, options.NodeSpacing * 0.5d)
|
||||
: adaptiveNodeSpacing;
|
||||
: (prevIsDummyX || currIsDummyX) ? Math.Min(gridNodeSpacing, options.NodeSpacing * 0.5d)
|
||||
: gridNodeSpacing;
|
||||
var minX = desiredX[nodeIndex - 1] + layer[nodeIndex - 1].Width + pairSpacingX;
|
||||
if (desiredX[nodeIndex] < minX)
|
||||
{
|
||||
@@ -210,19 +214,19 @@ internal static class ElkSharpLayoutInitialPlacement
|
||||
|
||||
ElkNodePlacement.RefineVerticalPlacement(positionedNodes, layers,
|
||||
incomingNodeIds, outgoingNodeIds, augmentedNodesById,
|
||||
options.NodeSpacing, placementIterations, options.Direction);
|
||||
gridNodeSpacing, placementIterations, options.Direction);
|
||||
|
||||
ElkNodePlacement.SnapOriginalPrimaryAxes(positionedNodes, layers,
|
||||
dummyResult.DummyNodeIds, incomingNodeIds, outgoingNodeIds,
|
||||
nodesById, options.NodeSpacing, options.Direction);
|
||||
nodesById, gridNodeSpacing, options.Direction);
|
||||
|
||||
ElkNodePlacementAlignment.CompactTowardIncomingFlow(positionedNodes, layers,
|
||||
dummyResult.DummyNodeIds, incomingNodeIds, nodesById,
|
||||
options.NodeSpacing, options.Direction);
|
||||
gridNodeSpacing, options.Direction);
|
||||
|
||||
ElkNodePlacement.SnapOriginalPrimaryAxes(positionedNodes, layers,
|
||||
dummyResult.DummyNodeIds, incomingNodeIds, outgoingNodeIds,
|
||||
nodesById, options.NodeSpacing, options.Direction);
|
||||
nodesById, gridNodeSpacing, options.Direction);
|
||||
|
||||
ElkNodePlacementPreferredCenter.AlignDummyNodesToFlow(positionedNodes, layers,
|
||||
dummyResult.DummyNodeIds, augmentedIncoming, augmentedOutgoing,
|
||||
@@ -242,7 +246,7 @@ internal static class ElkSharpLayoutInitialPlacement
|
||||
var pos = positionedNodes[nodeId];
|
||||
positionedNodes[nodeId] = ElkLayoutHelpers.CreatePositionedNode(
|
||||
augmentedNodesById[nodeId], pos.X - minNodeX, pos.Y, options.Direction);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user