Refactor ElkSharp hybrid routing and document speed path

This commit is contained in:
master
2026-03-29 19:33:46 +03:00
parent 7d6bc2b0ab
commit e8f7ad7652
89 changed files with 13280 additions and 10732 deletions

View File

@@ -0,0 +1,92 @@
# Sprint 20260329-007 - ElkSharp Document Render Speed
## Topic & Scope
- Reduce the actual document-processing render path to a sub-15-second generation target for the current document fixture.
- Promote the deterministic hybrid iterative router into the real `LeftToRight` workflow-rendering path without changing Sugiyama-owned node geometry.
- Increase safe parallel repair-candidate construction to use all available CPU cores while preserving deterministic commit order.
- Working directory: `src/__Libraries/StellaOps.ElkSharp/`.
- Expected evidence: renderer-path config changes, focused speed measurements on the document-processing tests, and updated sprint execution logs.
## Dependencies & Concurrency
- Depends on `docs/implplan/SPRINT_20260329_006_ElkSharp_hybrid_iterative_routing.md` for the hybrid router baseline.
- Safe cross-module edits for this sprint are limited to:
- `src/Workflow/__Libraries/StellaOps.Workflow.Renderer.ElkSharp/`
- `src/Workflow/__Tests/StellaOps.Workflow.Renderer.Tests/`
- `docs/workflow/`
- `docs/implplan/`
## Documentation Prerequisites
- `docs/code-of-conduct/CODE_OF_CONDUCT.md`
- `docs/code-of-conduct/TESTING_PRACTICES.md`
- `docs/workflow/ENGINE.md`
- `src/__Libraries/StellaOps.ElkSharp/AGENTS.md`
- `docs/implplan/SPRINT_20260329_006_ElkSharp_hybrid_iterative_routing.md`
## Delivery Tracker
### TASK-001 - Route actual document renders through the hybrid deterministic path
Status: DONE
Dependency: none
Owners: Implementer
Task description:
- Make the workflow renderer use the hybrid iterative router by default for `LeftToRight` document renders so real document-processing output stops paying the legacy multi-strategy brute-force cost.
- Preserve `TopToBottom` behavior and keep Sugiyama-owned geometry contracts unchanged.
Completion criteria:
- [x] `ElkSharpWorkflowRenderLayoutEngine` explicitly configures hybrid deterministic iterative routing for `LeftToRight` renders
- [x] `TopToBottom` stays on the legacy path
- [x] Focused hybrid parity tests still pass after the renderer-path change
### TASK-002 - Use full-core parallel repair-candidate builds safely
Status: DONE
Dependency: TASK-001
Owners: Implementer
Task description:
- Remove the artificial low CPU cap in iterative local-repair candidate building.
- Keep build work parallel only for independent candidate construction; merge/apply order must remain deterministic.
Completion criteria:
- [x] Iterative config accepts a full-core parallel repair-build budget
- [x] Parallel repair-build scheduling honors the requested budget up to available processor count
- [x] Deterministic hybrid parity tests still pass with the higher parallelism budget
### TASK-003 - Reduce the remaining winner-terminal speed hotspot
Status: DOING
Dependency: TASK-002
Owners: Implementer
Task description:
- Re-measure the actual document-processing layout path after the hybrid/default and full-core changes.
- If the document is still above target, optimize the winner post-slot hard-rule fallback and terminal-closure path without weakening hard-rule quality gates.
Completion criteria:
- [x] The layout-only document-processing test completes in under 15 seconds
- [x] Any new winner-terminal/finalization optimization keeps deterministic ordering and focused regression coverage intact
- [ ] The rendered document artifact test passes on the same fast-path configuration
- [x] Sprint execution log records before/after timings for the document-processing path
## Execution Log
| Date (UTC) | Update | Owner |
| --- | --- | --- |
| 2026-03-29 | Sprint created to track actual document-processing render speed work after hybrid routing landed behind an opt-in flag. | Implementer |
| 2026-03-29 | Workflow renderer now defaults `LeftToRight` document renders to hybrid deterministic iterative routing with `MaxRepairWaves=1` and `MaxParallelRepairBuilds=Environment.ProcessorCount`; focused hybrid/renderer tests stayed green (`5/5`). | Implementer |
| 2026-03-29 | Real layout-only document test hit `14.49s` wall time (`15.12s` total test time) on the reverted low-wave hybrid path, meeting the raw layout-only speed target for that fixture. | Implementer |
| 2026-03-29 | Document-quality checks still failed on the same low-wave path: `DocumentProcessingWorkflow_WhenLaidOutWithElkSharp_ShouldNotBacktrackIntoCheckResult`, `...ShouldKeepDecisionSourceExitsOnDiscreteBoundarySlots`, and `...ShouldProducePngWithZeroNodeCrossings` due to residual entry-angle / target-approach / boundary-slot defects. | Implementer |
| 2026-03-29 | Added hybrid attempt diagnostics so artifact tests now observe targeted local-repair attempts in hybrid mode; experimental exact-restabilization speed fixes were reverted after they pushed layout-only runtime back above target without clearing the geometry regressions. | Implementer |
| 2026-03-29 | Narrowed the hybrid low-wave winner-refinement path: focused terminal closure now caps rounds for restricted passes, shared-lane polish uses lean hybrid cleanup in low-wave mode, hybrid post-slot focus sets are capped deterministically, and tiny restricted boundary-slot repairs now stop after normalization instead of paying full detour closure. | Implementer |
| 2026-03-29 | Current speed gate on the actual document fixture is `13.81s` wall time (`14.36s` total test time) for `DocumentProcessingWorkflow_WhenLayoutOnly_ShouldProduceFinitePositions`; focused guards `...ShouldNotBacktrackIntoCheckResult` and `...ShouldKeepDecisionSourceExitsOnDiscreteBoundarySlots` both still pass on the same fast path. | Implementer |
| 2026-03-29 | The rendered artifact regression is faster on the same configuration (optimize phase ~`11.4s`, total test `16.27s`) but still fails with the pre-existing geometry defects: entry angles `2`, boundary angles on `edge/7` and `edge/27`, target join `edge/32+edge/33`, shared lane `edge/3+edge/4`, and under-node `edge/15`, `edge/20`, `edge/25`. | Implementer |
## Decisions & Risks
- The speed target is for the real document-processing render path, not synthetic hybrid-only stress tests.
- Hybrid mode may become the default only for `LeftToRight` workflow renders in this sprint; `TopToBottom` remains on the legacy path until parity is explicitly proven.
- Full-core usage applies to candidate construction only. Candidate commits and final route application must remain single-threaded and deterministic.
- The current best-performing configuration uses one hybrid wave and full-core candidate builds. That path is fast enough on the layout-only document test but still violates boundary-slot, target-approach backtracking, and entry-angle quality checks on the document fixture.
- The current best-performing configuration now meets the `<15s` layout-only document gate by using a narrower low-wave winner-refinement path, but the rendered artifact test still fails on the same entry-angle, target-join, shared-lane, and under-node offenders.
- Attempt-level hybrid diagnostics are now emitted into `ElkLayoutDiagnostics.IterativeStrategies`, restoring visibility into targeted local-repair activity for artifact tests.
- Reintroducing broader exact restabilization in the winner-terminal path raised the real document runtime into the `34s+` range without clearing the remaining geometry regressions, so that branch was reverted rather than left in the workspace.
- Tiny focused boundary-slot repairs now use an ultra-lean restricted path that stops after normalization. This is acceptable for the speed gate because the remaining quality failures are unchanged, but it is still a tradeoff that should be revisited once the artifact-quality defects are fixed.
## Next Checkpoints
- Keep the single-wave hybrid renderer path and full-core local-repair budget in place while targeting the residual artifact offenders `edge/7`, `edge/27`, `edge/32+edge/33`, `edge/3+edge/4`, and `edge/15`/`edge/20`/`edge/25` with narrower exact repairs.
- Re-run the rendered artifact regression together with the layout-only speed gate and the two focused document guards after each targeted fix.
- Only close the sprint once the document fixture satisfies both the `<15s` layout-only gate and the document-quality checks under the same configuration.

View File

@@ -23,31 +23,48 @@ public sealed class ElkSharpWorkflowRenderLayoutEngine : INamedWorkflowRenderGra
ArgumentNullException.ThrowIfNull(graph);
request ??= new WorkflowRenderLayoutRequest();
var direction = request.Direction == WorkflowRenderLayoutDirection.LeftToRight
? ElkLayoutDirection.LeftToRight
: ElkLayoutDirection.TopToBottom;
var effort = request.Effort switch
{
WorkflowRenderLayoutEffort.Draft => ElkLayoutEffort.Draft,
WorkflowRenderLayoutEffort.Balanced => ElkLayoutEffort.Balanced,
_ => ElkLayoutEffort.Best,
};
var elkGraph = new ElkGraph
{
Id = graph.Id,
Nodes = graph.Nodes.Select(MapNode).ToArray(),
Edges = graph.Edges.Select(MapEdge).ToArray(),
};
var layoutOptions = new ElkLayoutOptions
{
Direction = direction,
NodeSpacing = request.NodeSpacing,
LayerSpacing = request.LayerSpacing,
Effort = effort,
OrderingIterations = request.OrderingIterations,
PlacementIterations = request.PlacementIterations,
};
if (direction == ElkLayoutDirection.LeftToRight
&& effort == ElkLayoutEffort.Best)
{
layoutOptions = layoutOptions with
{
IterativeRouting = new IterativeRoutingOptions
{
Enabled = true,
Mode = IterativeRoutingMode.HybridDeterministic,
MaxRepairWaves = 1,
MaxParallelRepairBuilds = Math.Max(1, Environment.ProcessorCount),
},
};
}
var elkResult = await elkLayoutEngine.LayoutAsync(
elkGraph,
new ElkLayoutOptions
{
Direction = request.Direction == WorkflowRenderLayoutDirection.LeftToRight
? ElkLayoutDirection.LeftToRight
: ElkLayoutDirection.TopToBottom,
NodeSpacing = request.NodeSpacing,
LayerSpacing = request.LayerSpacing,
Effort = request.Effort switch
{
WorkflowRenderLayoutEffort.Draft => ElkLayoutEffort.Draft,
WorkflowRenderLayoutEffort.Balanced => ElkLayoutEffort.Balanced,
_ => ElkLayoutEffort.Best,
},
OrderingIterations = request.OrderingIterations,
PlacementIterations = request.PlacementIterations,
},
layoutOptions,
cancellationToken);
return new WorkflowRenderLayoutResult

View File

@@ -0,0 +1,282 @@
namespace StellaOps.ElkSharp;
internal static partial class ElkCompoundLayout
{
private static ElkRoutedEdge[] InsertCompoundBoundaryCrossings(
IReadOnlyCollection<ElkRoutedEdge> routedEdges,
IReadOnlyDictionary<string, ElkPositionedNode> positionedNodes,
ElkCompoundHierarchy hierarchy)
{
return routedEdges
.Select(edge =>
{
var path = ExtractPath(edge);
if (path.Count < 2)
{
return edge;
}
var lcaNodeId = hierarchy.GetLowestCommonAncestor(edge.SourceNodeId, edge.TargetNodeId);
var sourceAncestorIds = hierarchy.GetAncestorIdsNearestFirst(edge.SourceNodeId)
.TakeWhile(ancestorNodeId => !string.Equals(ancestorNodeId, lcaNodeId, StringComparison.Ordinal))
.ToArray();
var targetAncestorIds = hierarchy.GetAncestorIdsNearestFirst(edge.TargetNodeId)
.TakeWhile(ancestorNodeId => !string.Equals(ancestorNodeId, lcaNodeId, StringComparison.Ordinal))
.ToArray();
if (sourceAncestorIds.Length == 0 && targetAncestorIds.Length == 0)
{
return edge;
}
var rebuiltPath = path.ToList();
var startSegmentIndex = 0;
foreach (var ancestorNodeId in sourceAncestorIds)
{
if (!positionedNodes.TryGetValue(ancestorNodeId, out var ancestorNode))
{
continue;
}
if (!TryFindBoundaryTransitionFromStart(rebuiltPath, ancestorNode, startSegmentIndex, out var insertIndex, out var boundaryPoint))
{
continue;
}
rebuiltPath.Insert(insertIndex, boundaryPoint);
startSegmentIndex = insertIndex;
}
var endSegmentIndex = rebuiltPath.Count - 2;
foreach (var ancestorNodeId in targetAncestorIds)
{
if (!positionedNodes.TryGetValue(ancestorNodeId, out var ancestorNode))
{
continue;
}
if (!TryFindBoundaryTransitionFromEnd(rebuiltPath, ancestorNode, endSegmentIndex, out var insertIndex, out var boundaryPoint))
{
continue;
}
rebuiltPath.Insert(insertIndex, boundaryPoint);
endSegmentIndex = insertIndex - 2;
}
return BuildEdgeWithPath(edge, rebuiltPath);
})
.ToArray();
}
private static List<ElkPoint> ExtractPath(ElkRoutedEdge edge)
{
var path = new List<ElkPoint>();
foreach (var section in edge.Sections)
{
AppendPoint(path, section.StartPoint);
foreach (var bendPoint in section.BendPoints)
{
AppendPoint(path, bendPoint);
}
AppendPoint(path, section.EndPoint);
}
return path;
}
private static ElkRoutedEdge BuildEdgeWithPath(ElkRoutedEdge edge, IReadOnlyList<ElkPoint> path)
{
var normalizedPath = NormalizePath(path);
if (normalizedPath.Count < 2)
{
return edge;
}
return edge with
{
Sections =
[
new ElkEdgeSection
{
StartPoint = normalizedPath[0],
EndPoint = normalizedPath[^1],
BendPoints = normalizedPath.Count > 2
? normalizedPath.Skip(1).Take(normalizedPath.Count - 2).ToArray()
: [],
},
],
};
}
private static bool TryFindBoundaryTransitionFromStart(
IReadOnlyList<ElkPoint> path,
ElkPositionedNode boundaryNode,
int startSegmentIndex,
out int insertIndex,
out ElkPoint boundaryPoint)
{
insertIndex = -1;
boundaryPoint = default!;
for (var segmentIndex = Math.Max(0, startSegmentIndex); segmentIndex < path.Count - 1; segmentIndex++)
{
var from = path[segmentIndex];
var to = path[segmentIndex + 1];
if (!IsInsideOrOn(boundaryNode, from) || IsInsideOrOn(boundaryNode, to))
{
continue;
}
if (!TryResolveBoundaryTransition(boundaryNode, from, to, exitBoundary: true, out boundaryPoint))
{
continue;
}
insertIndex = segmentIndex + 1;
return true;
}
return false;
}
private static bool TryFindBoundaryTransitionFromEnd(
IReadOnlyList<ElkPoint> path,
ElkPositionedNode boundaryNode,
int startSegmentIndex,
out int insertIndex,
out ElkPoint boundaryPoint)
{
insertIndex = -1;
boundaryPoint = default!;
for (var segmentIndex = Math.Min(startSegmentIndex, path.Count - 2); segmentIndex >= 0; segmentIndex--)
{
var from = path[segmentIndex];
var to = path[segmentIndex + 1];
if (IsInsideOrOn(boundaryNode, from) || !IsInsideOrOn(boundaryNode, to))
{
continue;
}
if (!TryResolveBoundaryTransition(boundaryNode, from, to, exitBoundary: false, out boundaryPoint))
{
continue;
}
insertIndex = segmentIndex + 1;
return true;
}
return false;
}
private static bool TryResolveBoundaryTransition(
ElkPositionedNode boundaryNode,
ElkPoint from,
ElkPoint to,
bool exitBoundary,
out ElkPoint boundaryPoint)
{
boundaryPoint = default!;
if (!Clip(boundaryNode, from, to, out var enterScale, out var exitScale))
{
return false;
}
var scale = exitBoundary ? exitScale : enterScale;
scale = Math.Clamp(scale, 0d, 1d);
boundaryPoint = new ElkPoint
{
X = from.X + ((to.X - from.X) * scale),
Y = from.Y + ((to.Y - from.Y) * scale),
};
return true;
}
private static bool Clip(
ElkPositionedNode node,
ElkPoint from,
ElkPoint to,
out double enterScale,
out double exitScale)
{
enterScale = 0d;
exitScale = 1d;
var deltaX = to.X - from.X;
var deltaY = to.Y - from.Y;
return ClipTest(-deltaX, from.X - node.X, ref enterScale, ref exitScale)
&& ClipTest(deltaX, (node.X + node.Width) - from.X, ref enterScale, ref exitScale)
&& ClipTest(-deltaY, from.Y - node.Y, ref enterScale, ref exitScale)
&& ClipTest(deltaY, (node.Y + node.Height) - from.Y, ref enterScale, ref exitScale);
static bool ClipTest(double p, double q, ref double enter, ref double exit)
{
if (Math.Abs(p) <= 0.0001d)
{
return q >= -0.0001d;
}
var ratio = q / p;
if (p < 0d)
{
if (ratio > exit)
{
return false;
}
if (ratio > enter)
{
enter = ratio;
}
}
else
{
if (ratio < enter)
{
return false;
}
if (ratio < exit)
{
exit = ratio;
}
}
return true;
}
}
private static bool IsInsideOrOn(ElkPositionedNode node, ElkPoint point)
{
const double tolerance = 0.01d;
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;
}
private static List<ElkPoint> NormalizePath(IReadOnlyList<ElkPoint> path)
{
var normalized = new List<ElkPoint>(path.Count);
foreach (var point in path)
{
AppendPoint(normalized, point);
}
return normalized;
}
private static void AppendPoint(ICollection<ElkPoint> path, ElkPoint point)
{
if (path.Count > 0 && path.Last() is { } previousPoint && AreSamePoint(previousPoint, point))
{
return;
}
path.Add(new ElkPoint { X = point.X, Y = point.Y });
}
private static bool AreSamePoint(ElkPoint left, ElkPoint right) =>
Math.Abs(left.X - right.X) <= 0.01d
&& Math.Abs(left.Y - right.Y) <= 0.01d;
}

View File

@@ -0,0 +1,157 @@
namespace StellaOps.ElkSharp;
internal static partial class ElkCompoundLayout
{
private static ElkNode[][] OptimizeLayerOrderingForHierarchy(
ElkNode[][] initialLayers,
IReadOnlyDictionary<string, List<string>> incomingNodeIds,
IReadOnlyDictionary<string, List<string>> outgoingNodeIds,
IReadOnlyDictionary<string, int> inputOrder,
int iterationCount,
ElkCompoundHierarchy hierarchy,
IReadOnlySet<string> dummyNodeIds)
{
var layers = initialLayers
.Select(layer => layer.ToList())
.ToArray();
var effectiveIterations = Math.Max(1, iterationCount);
for (var iteration = 0; iteration < effectiveIterations; iteration++)
{
for (var layerIndex = 1; layerIndex < layers.Length; layerIndex++)
{
OrderLayerWithHierarchy(layers, layerIndex, incomingNodeIds, inputOrder, hierarchy, dummyNodeIds);
}
for (var layerIndex = layers.Length - 2; layerIndex >= 0; layerIndex--)
{
OrderLayerWithHierarchy(layers, layerIndex, outgoingNodeIds, inputOrder, hierarchy, dummyNodeIds);
}
}
return layers
.Select(layer => layer.ToArray())
.ToArray();
}
private static void OrderLayerWithHierarchy(
IReadOnlyList<List<ElkNode>> layers,
int layerIndex,
IReadOnlyDictionary<string, List<string>> adjacentNodeIds,
IReadOnlyDictionary<string, int> inputOrder,
ElkCompoundHierarchy hierarchy,
IReadOnlySet<string> dummyNodeIds)
{
var currentLayer = layers[layerIndex];
if (currentLayer.Count == 0)
{
return;
}
var positions = ElkNodeOrdering.BuildNodeOrderPositions(layers);
var layerNodesById = currentLayer.ToDictionary(node => node.Id, StringComparer.Ordinal);
var realNodeIdsInLayer = currentLayer
.Where(node => !dummyNodeIds.Contains(node.Id))
.Select(node => node.Id)
.ToHashSet(StringComparer.Ordinal);
var orderedRealIds = OrderNodesForSubtree(
null,
hierarchy,
realNodeIdsInLayer,
adjacentNodeIds,
positions,
inputOrder);
var orderedDummies = currentLayer
.Where(node => dummyNodeIds.Contains(node.Id))
.OrderBy(node => ElkNodeOrdering.ResolveOrderingRank(node.Id, adjacentNodeIds, positions))
.ThenBy(node => positions[node.Id])
.ThenBy(node => inputOrder[node.Id])
.ToArray();
currentLayer.Clear();
foreach (var nodeId in orderedRealIds)
{
currentLayer.Add(layerNodesById[nodeId]);
}
currentLayer.AddRange(orderedDummies);
}
private static List<string> OrderNodesForSubtree(
string? parentNodeId,
ElkCompoundHierarchy hierarchy,
IReadOnlySet<string> realNodeIdsInLayer,
IReadOnlyDictionary<string, List<string>> adjacentNodeIds,
IReadOnlyDictionary<string, int> positions,
IReadOnlyDictionary<string, int> inputOrder)
{
var blocks = new List<HierarchyOrderBlock>();
foreach (var childNodeId in hierarchy.GetChildIds(parentNodeId))
{
if (realNodeIdsInLayer.Contains(childNodeId))
{
blocks.Add(BuildBlock([childNodeId]));
continue;
}
if (!hierarchy.IsCompoundNode(childNodeId))
{
continue;
}
var descendantNodeIds = OrderNodesForSubtree(
childNodeId,
hierarchy,
realNodeIdsInLayer,
adjacentNodeIds,
positions,
inputOrder);
if (descendantNodeIds.Count == 0)
{
continue;
}
blocks.Add(BuildBlock(descendantNodeIds));
}
return blocks
.OrderBy(block => block.Rank)
.ThenBy(block => block.MinCurrentPosition)
.ThenBy(block => block.MinInputOrder)
.SelectMany(block => block.NodeIds)
.ToList();
HierarchyOrderBlock BuildBlock(IReadOnlyList<string> nodeIds)
{
var ranks = new List<double>();
foreach (var nodeId in nodeIds)
{
var nodeRank = ElkNodeOrdering.ResolveOrderingRank(nodeId, adjacentNodeIds, positions);
if (!double.IsInfinity(nodeRank))
{
ranks.Add(nodeRank);
}
}
ranks.Sort();
var rank = ranks.Count switch
{
0 => double.PositiveInfinity,
_ when ranks.Count % 2 == 1 => ranks[ranks.Count / 2],
_ => (ranks[(ranks.Count / 2) - 1] + ranks[ranks.Count / 2]) / 2d,
};
return new HierarchyOrderBlock(
nodeIds,
rank,
nodeIds.Min(nodeId => positions.GetValueOrDefault(nodeId, int.MaxValue)),
nodeIds.Min(nodeId => inputOrder.GetValueOrDefault(nodeId, int.MaxValue)));
}
}
private readonly record struct HierarchyOrderBlock(
IReadOnlyList<string> NodeIds,
double Rank,
int MinCurrentPosition,
int MinInputOrder);
}

View File

@@ -0,0 +1,130 @@
namespace StellaOps.ElkSharp;
internal static partial class ElkCompoundLayout
{
private static Dictionary<string, ElkPositionedNode> BuildCompoundPositionedNodes(
IReadOnlyCollection<ElkNode> graphNodes,
ElkCompoundHierarchy hierarchy,
IReadOnlyDictionary<string, ElkPositionedNode> positionedVisibleNodes,
ElkLayoutOptions options)
{
var nodesById = graphNodes.ToDictionary(node => node.Id, StringComparer.Ordinal);
var compoundNodes = new Dictionary<string, ElkPositionedNode>(StringComparer.Ordinal);
foreach (var pair in positionedVisibleNodes)
{
if (!hierarchy.IsLayoutVisibleNode(pair.Key))
{
continue;
}
compoundNodes[pair.Key] = pair.Value;
}
foreach (var nodeId in hierarchy.GetNonLeafNodeIdsByDescendingDepth())
{
var childBounds = hierarchy.GetChildIds(nodeId)
.Select(childNodeId => compoundNodes[childNodeId])
.ToArray();
var contentLeft = childBounds.Min(node => node.X);
var contentTop = childBounds.Min(node => node.Y);
var contentRight = childBounds.Max(node => node.X + node.Width);
var contentBottom = childBounds.Max(node => node.Y + node.Height);
var desiredWidth = (contentRight - contentLeft) + (options.CompoundPadding * 2d);
var desiredHeight = (contentBottom - contentTop) + options.CompoundHeaderHeight + (options.CompoundPadding * 2d);
var width = Math.Max(nodesById[nodeId].Width, desiredWidth);
var height = Math.Max(nodesById[nodeId].Height, desiredHeight);
var x = contentLeft - options.CompoundPadding - ((width - desiredWidth) / 2d);
var y = contentTop - options.CompoundHeaderHeight - options.CompoundPadding - ((height - desiredHeight) / 2d);
compoundNodes[nodeId] = ElkLayoutHelpers.CreatePositionedNode(
nodesById[nodeId],
x,
y,
options.Direction) with
{
Width = width,
Height = height,
};
}
return compoundNodes;
}
private static bool TryResolveNegativeCoordinateShift(
IReadOnlyDictionary<string, ElkPositionedNode> positionedNodes,
out double shiftX,
out double shiftY)
{
shiftX = 0d;
shiftY = 0d;
if (positionedNodes.Count == 0)
{
return false;
}
var minX = positionedNodes.Values.Min(node => node.X);
var minY = positionedNodes.Values.Min(node => node.Y);
if (minX >= -0.01d && minY >= -0.01d)
{
return false;
}
shiftX = minX < 0d ? -minX : 0d;
shiftY = minY < 0d ? -minY : 0d;
return shiftX > 0d || shiftY > 0d;
}
private static Dictionary<string, ElkPositionedNode> ShiftNodes(
IReadOnlyCollection<ElkNode> sourceNodes,
IReadOnlyDictionary<string, ElkPositionedNode> positionedNodes,
double shiftX,
double shiftY,
ElkLayoutDirection direction)
{
var sourceNodesById = sourceNodes.ToDictionary(node => node.Id, StringComparer.Ordinal);
return positionedNodes.ToDictionary(
pair => pair.Key,
pair =>
{
var shifted = ElkLayoutHelpers.CreatePositionedNode(
sourceNodesById[pair.Key],
pair.Value.X + shiftX,
pair.Value.Y + shiftY,
direction);
return shifted with
{
Width = pair.Value.Width,
Height = pair.Value.Height,
};
},
StringComparer.Ordinal);
}
private static ElkRoutedEdge[] ShiftEdges(
IReadOnlyCollection<ElkRoutedEdge> routedEdges,
double shiftX,
double shiftY)
{
return routedEdges
.Select(edge => new ElkRoutedEdge
{
Id = edge.Id,
SourceNodeId = edge.SourceNodeId,
TargetNodeId = edge.TargetNodeId,
SourcePortId = edge.SourcePortId,
TargetPortId = edge.TargetPortId,
Kind = edge.Kind,
Label = edge.Label,
Sections = edge.Sections.Select(section => new ElkEdgeSection
{
StartPoint = new ElkPoint { X = section.StartPoint.X + shiftX, Y = section.StartPoint.Y + shiftY },
EndPoint = new ElkPoint { X = section.EndPoint.X + shiftX, Y = section.EndPoint.Y + shiftY },
BendPoints = section.BendPoints
.Select(point => new ElkPoint { X = point.X + shiftX, Y = point.Y + shiftY })
.ToArray(),
}).ToArray(),
})
.ToArray();
}
}

View File

@@ -1,6 +1,5 @@
namespace StellaOps.ElkSharp;
internal static class ElkCompoundLayout
internal static partial class ElkCompoundLayout
{
internal static ElkLayoutResult Layout(
ElkGraph graph,
@@ -298,560 +297,4 @@ internal static class ElkCompoundLayout
};
}
private static ElkNode[][] OptimizeLayerOrderingForHierarchy(
ElkNode[][] initialLayers,
IReadOnlyDictionary<string, List<string>> incomingNodeIds,
IReadOnlyDictionary<string, List<string>> outgoingNodeIds,
IReadOnlyDictionary<string, int> inputOrder,
int iterationCount,
ElkCompoundHierarchy hierarchy,
IReadOnlySet<string> dummyNodeIds)
{
var layers = initialLayers
.Select(layer => layer.ToList())
.ToArray();
var effectiveIterations = Math.Max(1, iterationCount);
for (var iteration = 0; iteration < effectiveIterations; iteration++)
{
for (var layerIndex = 1; layerIndex < layers.Length; layerIndex++)
{
OrderLayerWithHierarchy(layers, layerIndex, incomingNodeIds, inputOrder, hierarchy, dummyNodeIds);
}
for (var layerIndex = layers.Length - 2; layerIndex >= 0; layerIndex--)
{
OrderLayerWithHierarchy(layers, layerIndex, outgoingNodeIds, inputOrder, hierarchy, dummyNodeIds);
}
}
return layers
.Select(layer => layer.ToArray())
.ToArray();
}
private static void OrderLayerWithHierarchy(
IReadOnlyList<List<ElkNode>> layers,
int layerIndex,
IReadOnlyDictionary<string, List<string>> adjacentNodeIds,
IReadOnlyDictionary<string, int> inputOrder,
ElkCompoundHierarchy hierarchy,
IReadOnlySet<string> dummyNodeIds)
{
var currentLayer = layers[layerIndex];
if (currentLayer.Count == 0)
{
return;
}
var positions = ElkNodeOrdering.BuildNodeOrderPositions(layers);
var layerNodesById = currentLayer.ToDictionary(node => node.Id, StringComparer.Ordinal);
var realNodeIdsInLayer = currentLayer
.Where(node => !dummyNodeIds.Contains(node.Id))
.Select(node => node.Id)
.ToHashSet(StringComparer.Ordinal);
var orderedRealIds = OrderNodesForSubtree(
null,
hierarchy,
realNodeIdsInLayer,
adjacentNodeIds,
positions,
inputOrder);
var orderedDummies = currentLayer
.Where(node => dummyNodeIds.Contains(node.Id))
.OrderBy(node => ElkNodeOrdering.ResolveOrderingRank(node.Id, adjacentNodeIds, positions))
.ThenBy(node => positions[node.Id])
.ThenBy(node => inputOrder[node.Id])
.ToArray();
currentLayer.Clear();
foreach (var nodeId in orderedRealIds)
{
currentLayer.Add(layerNodesById[nodeId]);
}
currentLayer.AddRange(orderedDummies);
}
private static List<string> OrderNodesForSubtree(
string? parentNodeId,
ElkCompoundHierarchy hierarchy,
IReadOnlySet<string> realNodeIdsInLayer,
IReadOnlyDictionary<string, List<string>> adjacentNodeIds,
IReadOnlyDictionary<string, int> positions,
IReadOnlyDictionary<string, int> inputOrder)
{
var blocks = new List<HierarchyOrderBlock>();
foreach (var childNodeId in hierarchy.GetChildIds(parentNodeId))
{
if (realNodeIdsInLayer.Contains(childNodeId))
{
blocks.Add(BuildBlock([childNodeId]));
continue;
}
if (!hierarchy.IsCompoundNode(childNodeId))
{
continue;
}
var descendantNodeIds = OrderNodesForSubtree(
childNodeId,
hierarchy,
realNodeIdsInLayer,
adjacentNodeIds,
positions,
inputOrder);
if (descendantNodeIds.Count == 0)
{
continue;
}
blocks.Add(BuildBlock(descendantNodeIds));
}
return blocks
.OrderBy(block => block.Rank)
.ThenBy(block => block.MinCurrentPosition)
.ThenBy(block => block.MinInputOrder)
.SelectMany(block => block.NodeIds)
.ToList();
HierarchyOrderBlock BuildBlock(IReadOnlyList<string> nodeIds)
{
var ranks = new List<double>();
foreach (var nodeId in nodeIds)
{
var nodeRank = ElkNodeOrdering.ResolveOrderingRank(nodeId, adjacentNodeIds, positions);
if (!double.IsInfinity(nodeRank))
{
ranks.Add(nodeRank);
}
}
ranks.Sort();
var rank = ranks.Count switch
{
0 => double.PositiveInfinity,
_ when ranks.Count % 2 == 1 => ranks[ranks.Count / 2],
_ => (ranks[(ranks.Count / 2) - 1] + ranks[ranks.Count / 2]) / 2d,
};
return new HierarchyOrderBlock(
nodeIds,
rank,
nodeIds.Min(nodeId => positions.GetValueOrDefault(nodeId, int.MaxValue)),
nodeIds.Min(nodeId => inputOrder.GetValueOrDefault(nodeId, int.MaxValue)));
}
}
private static ElkRoutedEdge[] InsertCompoundBoundaryCrossings(
IReadOnlyCollection<ElkRoutedEdge> routedEdges,
IReadOnlyDictionary<string, ElkPositionedNode> positionedNodes,
ElkCompoundHierarchy hierarchy)
{
return routedEdges
.Select(edge =>
{
var path = ExtractPath(edge);
if (path.Count < 2)
{
return edge;
}
var lcaNodeId = hierarchy.GetLowestCommonAncestor(edge.SourceNodeId, edge.TargetNodeId);
var sourceAncestorIds = hierarchy.GetAncestorIdsNearestFirst(edge.SourceNodeId)
.TakeWhile(ancestorNodeId => !string.Equals(ancestorNodeId, lcaNodeId, StringComparison.Ordinal))
.ToArray();
var targetAncestorIds = hierarchy.GetAncestorIdsNearestFirst(edge.TargetNodeId)
.TakeWhile(ancestorNodeId => !string.Equals(ancestorNodeId, lcaNodeId, StringComparison.Ordinal))
.ToArray();
if (sourceAncestorIds.Length == 0 && targetAncestorIds.Length == 0)
{
return edge;
}
var rebuiltPath = path.ToList();
var startSegmentIndex = 0;
foreach (var ancestorNodeId in sourceAncestorIds)
{
if (!positionedNodes.TryGetValue(ancestorNodeId, out var ancestorNode))
{
continue;
}
if (!TryFindBoundaryTransitionFromStart(rebuiltPath, ancestorNode, startSegmentIndex, out var insertIndex, out var boundaryPoint))
{
continue;
}
rebuiltPath.Insert(insertIndex, boundaryPoint);
startSegmentIndex = insertIndex;
}
var endSegmentIndex = rebuiltPath.Count - 2;
foreach (var ancestorNodeId in targetAncestorIds)
{
if (!positionedNodes.TryGetValue(ancestorNodeId, out var ancestorNode))
{
continue;
}
if (!TryFindBoundaryTransitionFromEnd(rebuiltPath, ancestorNode, endSegmentIndex, out var insertIndex, out var boundaryPoint))
{
continue;
}
rebuiltPath.Insert(insertIndex, boundaryPoint);
endSegmentIndex = insertIndex - 2;
}
return BuildEdgeWithPath(edge, rebuiltPath);
})
.ToArray();
}
private static List<ElkPoint> ExtractPath(ElkRoutedEdge edge)
{
var path = new List<ElkPoint>();
foreach (var section in edge.Sections)
{
AppendPoint(path, section.StartPoint);
foreach (var bendPoint in section.BendPoints)
{
AppendPoint(path, bendPoint);
}
AppendPoint(path, section.EndPoint);
}
return path;
}
private static ElkRoutedEdge BuildEdgeWithPath(ElkRoutedEdge edge, IReadOnlyList<ElkPoint> path)
{
var normalizedPath = NormalizePath(path);
if (normalizedPath.Count < 2)
{
return edge;
}
return edge with
{
Sections =
[
new ElkEdgeSection
{
StartPoint = normalizedPath[0],
EndPoint = normalizedPath[^1],
BendPoints = normalizedPath.Count > 2
? normalizedPath.Skip(1).Take(normalizedPath.Count - 2).ToArray()
: [],
},
],
};
}
private static bool TryFindBoundaryTransitionFromStart(
IReadOnlyList<ElkPoint> path,
ElkPositionedNode boundaryNode,
int startSegmentIndex,
out int insertIndex,
out ElkPoint boundaryPoint)
{
insertIndex = -1;
boundaryPoint = default!;
for (var segmentIndex = Math.Max(0, startSegmentIndex); segmentIndex < path.Count - 1; segmentIndex++)
{
var from = path[segmentIndex];
var to = path[segmentIndex + 1];
if (!IsInsideOrOn(boundaryNode, from) || IsInsideOrOn(boundaryNode, to))
{
continue;
}
if (!TryResolveBoundaryTransition(boundaryNode, from, to, exitBoundary: true, out boundaryPoint))
{
continue;
}
insertIndex = segmentIndex + 1;
return true;
}
return false;
}
private static bool TryFindBoundaryTransitionFromEnd(
IReadOnlyList<ElkPoint> path,
ElkPositionedNode boundaryNode,
int startSegmentIndex,
out int insertIndex,
out ElkPoint boundaryPoint)
{
insertIndex = -1;
boundaryPoint = default!;
for (var segmentIndex = Math.Min(startSegmentIndex, path.Count - 2); segmentIndex >= 0; segmentIndex--)
{
var from = path[segmentIndex];
var to = path[segmentIndex + 1];
if (IsInsideOrOn(boundaryNode, from) || !IsInsideOrOn(boundaryNode, to))
{
continue;
}
if (!TryResolveBoundaryTransition(boundaryNode, from, to, exitBoundary: false, out boundaryPoint))
{
continue;
}
insertIndex = segmentIndex + 1;
return true;
}
return false;
}
private static bool TryResolveBoundaryTransition(
ElkPositionedNode boundaryNode,
ElkPoint from,
ElkPoint to,
bool exitBoundary,
out ElkPoint boundaryPoint)
{
boundaryPoint = default!;
if (!Clip(boundaryNode, from, to, out var enterScale, out var exitScale))
{
return false;
}
var scale = exitBoundary ? exitScale : enterScale;
scale = Math.Clamp(scale, 0d, 1d);
boundaryPoint = new ElkPoint
{
X = from.X + ((to.X - from.X) * scale),
Y = from.Y + ((to.Y - from.Y) * scale),
};
return true;
}
private static bool Clip(
ElkPositionedNode node,
ElkPoint from,
ElkPoint to,
out double enterScale,
out double exitScale)
{
enterScale = 0d;
exitScale = 1d;
var deltaX = to.X - from.X;
var deltaY = to.Y - from.Y;
return ClipTest(-deltaX, from.X - node.X, ref enterScale, ref exitScale)
&& ClipTest(deltaX, (node.X + node.Width) - from.X, ref enterScale, ref exitScale)
&& ClipTest(-deltaY, from.Y - node.Y, ref enterScale, ref exitScale)
&& ClipTest(deltaY, (node.Y + node.Height) - from.Y, ref enterScale, ref exitScale);
static bool ClipTest(double p, double q, ref double enter, ref double exit)
{
if (Math.Abs(p) <= 0.0001d)
{
return q >= -0.0001d;
}
var ratio = q / p;
if (p < 0d)
{
if (ratio > exit)
{
return false;
}
if (ratio > enter)
{
enter = ratio;
}
}
else
{
if (ratio < enter)
{
return false;
}
if (ratio < exit)
{
exit = ratio;
}
}
return true;
}
}
private static bool IsInsideOrOn(ElkPositionedNode node, ElkPoint point)
{
const double tolerance = 0.01d;
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;
}
private static List<ElkPoint> NormalizePath(IReadOnlyList<ElkPoint> path)
{
var normalized = new List<ElkPoint>(path.Count);
foreach (var point in path)
{
AppendPoint(normalized, point);
}
return normalized;
}
private static void AppendPoint(ICollection<ElkPoint> path, ElkPoint point)
{
if (path.Count > 0 && path.Last() is { } previousPoint && AreSamePoint(previousPoint, point))
{
return;
}
path.Add(new ElkPoint { X = point.X, Y = point.Y });
}
private static bool AreSamePoint(ElkPoint left, ElkPoint right) =>
Math.Abs(left.X - right.X) <= 0.01d
&& Math.Abs(left.Y - right.Y) <= 0.01d;
private static Dictionary<string, ElkPositionedNode> BuildCompoundPositionedNodes(
IReadOnlyCollection<ElkNode> graphNodes,
ElkCompoundHierarchy hierarchy,
IReadOnlyDictionary<string, ElkPositionedNode> positionedVisibleNodes,
ElkLayoutOptions options)
{
var nodesById = graphNodes.ToDictionary(node => node.Id, StringComparer.Ordinal);
var compoundNodes = new Dictionary<string, ElkPositionedNode>(StringComparer.Ordinal);
foreach (var pair in positionedVisibleNodes)
{
if (!hierarchy.IsLayoutVisibleNode(pair.Key))
{
continue;
}
compoundNodes[pair.Key] = pair.Value;
}
foreach (var nodeId in hierarchy.GetNonLeafNodeIdsByDescendingDepth())
{
var childBounds = hierarchy.GetChildIds(nodeId)
.Select(childNodeId => compoundNodes[childNodeId])
.ToArray();
var contentLeft = childBounds.Min(node => node.X);
var contentTop = childBounds.Min(node => node.Y);
var contentRight = childBounds.Max(node => node.X + node.Width);
var contentBottom = childBounds.Max(node => node.Y + node.Height);
var desiredWidth = (contentRight - contentLeft) + (options.CompoundPadding * 2d);
var desiredHeight = (contentBottom - contentTop) + options.CompoundHeaderHeight + (options.CompoundPadding * 2d);
var width = Math.Max(nodesById[nodeId].Width, desiredWidth);
var height = Math.Max(nodesById[nodeId].Height, desiredHeight);
var x = contentLeft - options.CompoundPadding - ((width - desiredWidth) / 2d);
var y = contentTop - options.CompoundHeaderHeight - options.CompoundPadding - ((height - desiredHeight) / 2d);
compoundNodes[nodeId] = ElkLayoutHelpers.CreatePositionedNode(
nodesById[nodeId],
x,
y,
options.Direction) with
{
Width = width,
Height = height,
};
}
return compoundNodes;
}
private static bool TryResolveNegativeCoordinateShift(
IReadOnlyDictionary<string, ElkPositionedNode> positionedNodes,
out double shiftX,
out double shiftY)
{
shiftX = 0d;
shiftY = 0d;
if (positionedNodes.Count == 0)
{
return false;
}
var minX = positionedNodes.Values.Min(node => node.X);
var minY = positionedNodes.Values.Min(node => node.Y);
if (minX >= -0.01d && minY >= -0.01d)
{
return false;
}
shiftX = minX < 0d ? -minX : 0d;
shiftY = minY < 0d ? -minY : 0d;
return shiftX > 0d || shiftY > 0d;
}
private static Dictionary<string, ElkPositionedNode> ShiftNodes(
IReadOnlyCollection<ElkNode> sourceNodes,
IReadOnlyDictionary<string, ElkPositionedNode> positionedNodes,
double shiftX,
double shiftY,
ElkLayoutDirection direction)
{
var sourceNodesById = sourceNodes.ToDictionary(node => node.Id, StringComparer.Ordinal);
return positionedNodes.ToDictionary(
pair => pair.Key,
pair =>
{
var shifted = ElkLayoutHelpers.CreatePositionedNode(
sourceNodesById[pair.Key],
pair.Value.X + shiftX,
pair.Value.Y + shiftY,
direction);
return shifted with
{
Width = pair.Value.Width,
Height = pair.Value.Height,
};
},
StringComparer.Ordinal);
}
private static ElkRoutedEdge[] ShiftEdges(
IReadOnlyCollection<ElkRoutedEdge> routedEdges,
double shiftX,
double shiftY)
{
return routedEdges
.Select(edge => new ElkRoutedEdge
{
Id = edge.Id,
SourceNodeId = edge.SourceNodeId,
TargetNodeId = edge.TargetNodeId,
SourcePortId = edge.SourcePortId,
TargetPortId = edge.TargetPortId,
Kind = edge.Kind,
Label = edge.Label,
Sections = edge.Sections.Select(section => new ElkEdgeSection
{
StartPoint = new ElkPoint { X = section.StartPoint.X + shiftX, Y = section.StartPoint.Y + shiftY },
EndPoint = new ElkPoint { X = section.EndPoint.X + shiftX, Y = section.EndPoint.Y + shiftY },
BendPoints = section.BendPoints
.Select(point => new ElkPoint { X = point.X + shiftX, Y = point.Y + shiftY })
.ToArray(),
}).ToArray(),
})
.ToArray();
}
private readonly record struct HierarchyOrderBlock(
IReadOnlyList<string> NodeIds,
double Rank,
int MinCurrentPosition,
int MinInputOrder);
}

View File

@@ -1585,6 +1585,7 @@ internal static partial class ElkEdgePostProcessor
return path.Count >= 2
&& !ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(targetNode, path[^2])
&& !HasExcessiveGatewayDiagonalLength(path, targetNode)
&& !HasShortGatewayTargetOrthogonalHook(path, targetNode)
&& ElkShapeBoundaries.HasValidGatewayBoundaryAngle(targetNode, path[^1], path[^2]);
}
@@ -2616,4 +2617,4 @@ internal static partial class ElkEdgePostProcessor
return CloneEdgeWithKind(edge, AppendInternalKindMarker(edge.Kind, ProtectedUnderNodeKindMarker));
}
}
}

View File

@@ -0,0 +1,144 @@
namespace StellaOps.ElkSharp;
internal static partial 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,
double clearanceMargin = 0d)
{
var minY = Math.Min(anchorY, corridorY);
var maxY = Math.Max(anchorY, corridorY);
var blocked = false;
foreach (var obstacle in obstacles)
{
if (obstacle.Id == sourceId || obstacle.Id == targetId)
{
continue;
}
if (anchorX > obstacle.Left - clearanceMargin
&& anchorX < obstacle.Right + clearanceMargin
&& maxY > obstacle.Top - clearanceMargin
&& minY < obstacle.Bottom + clearanceMargin)
{
blocked = true;
break;
}
}
if (!blocked)
{
return anchorX;
}
var candidateRight = anchorX;
var candidateLeft = anchorX;
var searchStep = Math.Max(24d, clearanceMargin * 0.5d);
for (var attempt = 0; attempt < 20; attempt++)
{
candidateRight += searchStep;
if (!IsVerticalBlocked(candidateRight, minY, maxY, obstacles, sourceId, targetId, clearanceMargin))
{
return candidateRight;
}
candidateLeft -= searchStep;
if (!IsVerticalBlocked(candidateLeft, minY, maxY, obstacles, sourceId, targetId, clearanceMargin))
{
return candidateLeft;
}
}
return anchorX;
}
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,
double clearanceMargin)
{
foreach (var obstacle in obstacles)
{
if (obstacle.Id == sourceId || obstacle.Id == targetId)
{
continue;
}
if (x > obstacle.Left - clearanceMargin
&& x < obstacle.Right + clearanceMargin
&& maxY > obstacle.Top - clearanceMargin
&& minY < obstacle.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 obstacle in obstacles)
{
if (obstacle.Id == sourceId || obstacle.Id == targetId)
{
continue;
}
if (horizontal)
{
var minX = Math.Min(start.X, end.X);
var maxX = Math.Max(start.X, end.X);
if (start.Y > obstacle.Top - clearanceMargin
&& start.Y < obstacle.Bottom + clearanceMargin
&& maxX > obstacle.Left - clearanceMargin
&& minX < obstacle.Right + clearanceMargin)
{
return true;
}
continue;
}
var minY = Math.Min(start.Y, end.Y);
var maxY = Math.Max(start.Y, end.Y);
if (start.X > obstacle.Left - clearanceMargin
&& start.X < obstacle.Right + clearanceMargin
&& maxY > obstacle.Top - clearanceMargin
&& minY < obstacle.Bottom + clearanceMargin)
{
return true;
}
}
return false;
}
}

View File

@@ -1,22 +1,25 @@
namespace StellaOps.ElkSharp;
internal static class ElkEdgePostProcessorCorridor
internal static partial class ElkEdgePostProcessorCorridor
{
internal static ElkEdgeSection? ReroutePreservingCorridor(
ElkEdgeSection section,
(double Left, double Top, double Right, double Bottom, string Id)[] obstacles,
string sourceId, string targetId, double margin,
double graphMinY, double graphMaxY)
string sourceId,
string targetId,
double margin,
double graphMinY,
double graphMaxY)
{
var pts = new List<ElkPoint> { section.StartPoint };
pts.AddRange(section.BendPoints);
pts.Add(section.EndPoint);
var points = new List<ElkPoint> { section.StartPoint };
points.AddRange(section.BendPoints);
points.Add(section.EndPoint);
var firstCorridorIndex = -1;
var lastCorridorIndex = -1;
for (var i = 0; i < pts.Count; i++)
for (var i = 0; i < points.Count; i++)
{
if (pts[i].Y < graphMinY - 8d || pts[i].Y > graphMaxY + 8d)
if (points[i].Y < graphMinY - 8d || points[i].Y > graphMaxY + 8d)
{
if (firstCorridorIndex < 0)
{
@@ -27,12 +30,12 @@ internal static class ElkEdgePostProcessorCorridor
}
}
if (firstCorridorIndex < 0 || firstCorridorIndex == 0 && lastCorridorIndex == pts.Count - 1)
if (firstCorridorIndex < 0 || firstCorridorIndex == 0 && lastCorridorIndex == points.Count - 1)
{
return null;
}
var corridorY = pts[firstCorridorIndex].Y;
var corridorY = points[firstCorridorIndex].Y;
var isAboveCorridor = corridorY < graphMinY - 8d;
var clearanceMargin = Math.Max(margin, 40d);
var result = new List<ElkPoint>();
@@ -44,8 +47,8 @@ internal static class ElkEdgePostProcessorCorridor
var preCorridorHasCrossing = false;
for (var i = 0; i < firstCorridorIndex; i++)
{
if (!ElkEdgePostProcessor.SegmentCrossesObstacle(pts[i], pts[i + 1], obstacles, sourceId, targetId)
&& !SegmentViolatesObstacleClearance(pts[i], pts[i + 1], obstacles, sourceId, targetId, clearanceMargin))
if (!ElkEdgePostProcessor.SegmentCrossesObstacle(points[i], points[i + 1], obstacles, sourceId, targetId)
&& !SegmentViolatesObstacleClearance(points[i], points[i + 1], obstacles, sourceId, targetId, clearanceMargin))
{
continue;
}
@@ -56,7 +59,7 @@ internal static class ElkEdgePostProcessorCorridor
if (preCorridorHasCrossing)
{
var entryTarget = pts[firstCorridorIndex];
var entryTarget = points[firstCorridorIndex];
var entryPath = ElkEdgePostProcessorAStar.RerouteWithGridAStar(
section.StartPoint,
entryTarget,
@@ -75,9 +78,9 @@ internal static class ElkEdgePostProcessorCorridor
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 (last is null || Math.Abs(last.X - points[i].X) > 0.01d || Math.Abs(last.Y - points[i].Y) > 0.01d)
{
result.Add(pts[i]);
result.Add(points[i]);
}
}
@@ -92,7 +95,7 @@ internal static class ElkEdgePostProcessorCorridor
}
else
{
var entryTarget = pts[firstCorridorIndex];
var entryTarget = points[firstCorridorIndex];
var entryPath = ElkEdgePostProcessorAStar.RerouteWithGridAStar(
section.StartPoint, entryTarget, obstacles, sourceId, targetId, margin);
if (entryPath is not null && entryPath.Count >= 2)
@@ -103,41 +106,41 @@ internal static class ElkEdgePostProcessorCorridor
{
for (var i = 0; i <= firstCorridorIndex; i++)
{
result.Add(pts[i]);
result.Add(points[i]);
}
}
}
}
else
{
result.Add(pts[0]);
result.Add(points[0]);
}
for (var i = firstCorridorIndex; i <= lastCorridorIndex; 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 (last is null || Math.Abs(last.X - points[i].X) > 0.01d || Math.Abs(last.Y - points[i].Y) > 0.01d)
{
result.Add(pts[i]);
result.Add(points[i]);
}
}
if (lastCorridorIndex < pts.Count - 1)
if (lastCorridorIndex < points.Count - 1)
{
if (isAboveCorridor)
{
for (var i = lastCorridorIndex + 1; i < pts.Count; i++)
for (var i = lastCorridorIndex + 1; i < points.Count; 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 (last is null || Math.Abs(last.X - points[i].X) > 0.01d || Math.Abs(last.Y - points[i].Y) > 0.01d)
{
result.Add(pts[i]);
result.Add(points[i]);
}
}
}
else
{
var exitSource = pts[lastCorridorIndex];
var exitSource = points[lastCorridorIndex];
var exitPath = ElkEdgePostProcessorAStar.RerouteWithGridAStar(
exitSource, section.EndPoint, obstacles, sourceId, targetId, margin);
if (exitPath is not null && exitPath.Count >= 2)
@@ -149,9 +152,9 @@ internal static class ElkEdgePostProcessorCorridor
}
else
{
for (var i = lastCorridorIndex + 1; i < pts.Count; i++)
for (var i = lastCorridorIndex + 1; i < points.Count; i++)
{
result.Add(pts[i]);
result.Add(points[i]);
}
}
}
@@ -169,138 +172,4 @@ internal static class ElkEdgePostProcessorCorridor
BendPoints = result.Skip(1).Take(result.Count - 2).ToArray(),
};
}
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,
double clearanceMargin = 0d)
{
var minY = Math.Min(anchorY, corridorY);
var maxY = Math.Max(anchorY, corridorY);
var blocked = false;
foreach (var ob in obstacles)
{
if (ob.Id == sourceId || ob.Id == targetId)
{
continue;
}
if (anchorX > ob.Left - clearanceMargin
&& anchorX < ob.Right + clearanceMargin
&& maxY > ob.Top - clearanceMargin
&& minY < ob.Bottom + clearanceMargin)
{
blocked = true;
break;
}
}
if (!blocked)
{
return anchorX;
}
var candidateRight = anchorX;
var candidateLeft = anchorX;
var searchStep = Math.Max(24d, clearanceMargin * 0.5d);
for (var attempt = 0; attempt < 20; attempt++)
{
candidateRight += searchStep;
if (!IsVerticalBlocked(candidateRight, minY, maxY, obstacles, sourceId, targetId, clearanceMargin))
{
return candidateRight;
}
candidateLeft -= searchStep;
if (!IsVerticalBlocked(candidateLeft, minY, maxY, obstacles, sourceId, targetId, clearanceMargin))
{
return candidateLeft;
}
}
return anchorX;
}
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,
double clearanceMargin)
{
foreach (var ob in obstacles)
{
if (ob.Id == sourceId || ob.Id == targetId)
{
continue;
}
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;
}
}
return false;
}
}

View File

@@ -0,0 +1,214 @@
namespace StellaOps.ElkSharp;
internal static partial class ElkEdgePostProcessorSimplify
{
internal static ElkRoutedEdge[] TightenOuterCorridors(
ElkRoutedEdge[] edges,
ElkPositionedNode[] nodes)
{
if (nodes.Length == 0) return edges;
var graphMinY = nodes.Min(n => n.Y);
var graphMaxY = nodes.Max(n => n.Y + n.Height);
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;
var minMargin = Math.Max(12d, minLineClearance + 4d);
var laneGap = Math.Max(8d, minLineClearance + 4d);
var outerEdges = new List<(int Index, double CorridorY, bool IsAbove)>();
for (var i = 0; i < edges.Length; i++)
{
var aboveYs = new List<double>();
var belowYs = new List<double>();
foreach (var section in edges[i].Sections)
{
foreach (var bp in section.BendPoints)
{
if (bp.Y < graphMinY - 8d)
{
aboveYs.Add(bp.Y);
}
else if (bp.Y > graphMaxY + 8d)
{
belowYs.Add(bp.Y);
}
}
}
if (aboveYs.Count > 0)
{
outerEdges.Add((i, aboveYs.Min(), true));
}
if (belowYs.Count > 0)
{
outerEdges.Add((i, belowYs.Max(), false));
}
}
if (outerEdges.Count == 0) return edges;
NormalizeCorridorYValues(outerEdges, edges, graphMinY, graphMaxY);
var aboveLanes = outerEdges.Where(e => e.IsAbove)
.GroupBy(e => Math.Round(e.CorridorY, 1))
.OrderBy(g => g.Key)
.ToArray();
var belowLanes = outerEdges.Where(e => !e.IsAbove)
.GroupBy(e => Math.Round(e.CorridorY, 1))
.OrderByDescending(g => g.Key)
.ToArray();
var result = edges.ToArray();
var shifts = new Dictionary<int, double>();
for (var lane = 0; lane < aboveLanes.Length; lane++)
{
var targetY = graphMinY - minMargin - (lane * laneGap);
var currentY = aboveLanes[lane].Key;
var shift = targetY - currentY;
if (Math.Abs(shift) > 2d)
{
foreach (var entry in aboveLanes[lane])
{
shifts[entry.Index] = shift;
}
}
}
for (var lane = 0; lane < belowLanes.Length; lane++)
{
var targetY = graphMaxY + minMargin + (lane * laneGap);
var currentY = belowLanes[lane].Key;
var shift = targetY - currentY;
if (Math.Abs(shift) > 2d)
{
foreach (var entry in belowLanes[lane])
{
shifts[entry.Index] = shift;
}
}
}
foreach (var (edgeIndex, shift) in shifts)
{
var edge = result[edgeIndex];
var newSections = new List<ElkEdgeSection>();
foreach (var section in edge.Sections)
{
var newBendPoints = section.BendPoints.Select(bp =>
{
if ((shift < 0 && bp.Y < graphMinY - 4d) || (shift > 0 && bp.Y > graphMaxY + 4d)
|| (shift > 0 && bp.Y < graphMinY - 4d) || (shift < 0 && bp.Y > graphMaxY + 4d))
{
return new ElkPoint { X = bp.X, Y = bp.Y + shift };
}
return bp;
}).ToArray();
newSections.Add(new ElkEdgeSection
{
StartPoint = section.StartPoint,
EndPoint = section.EndPoint,
BendPoints = newBendPoints,
});
}
result[edgeIndex] = new ElkRoutedEdge
{
Id = edge.Id,
SourceNodeId = edge.SourceNodeId,
TargetNodeId = edge.TargetNodeId,
Label = edge.Label,
Sections = newSections,
};
}
return result;
}
private static void NormalizeCorridorYValues(
List<(int Index, double CorridorY, bool IsAbove)> outerEdges,
ElkRoutedEdge[] edges,
double graphMinY,
double graphMaxY)
{
const double mergeThreshold = 6d;
var groups = new List<List<int>>();
var sorted = outerEdges.OrderBy(e => e.CorridorY).ToArray();
foreach (var entry in sorted)
{
var merged = false;
foreach (var group in groups)
{
var groupY = outerEdges[group[0]].CorridorY;
if (Math.Abs(entry.CorridorY - groupY) <= mergeThreshold && entry.IsAbove == outerEdges[group[0]].IsAbove)
{
group.Add(outerEdges.IndexOf(entry));
merged = true;
break;
}
}
if (!merged)
{
groups.Add([outerEdges.IndexOf(entry)]);
}
}
foreach (var group in groups)
{
if (group.Count <= 1)
{
continue;
}
var targetY = outerEdges[group[0]].CorridorY;
for (var gi = 1; gi < group.Count; gi++)
{
var idx = group[gi];
var edgeIndex = outerEdges[idx].Index;
var currentY = outerEdges[idx].CorridorY;
var shift = targetY - currentY;
if (Math.Abs(shift) < 0.5d)
{
continue;
}
var edge = edges[edgeIndex];
var newSections = new List<ElkEdgeSection>();
foreach (var section in edge.Sections)
{
var newBendPoints = section.BendPoints.Select(bp =>
{
if (Math.Abs(bp.Y - currentY) < 2d)
{
return new ElkPoint { X = bp.X, Y = targetY };
}
return bp;
}).ToArray();
newSections.Add(new ElkEdgeSection
{
StartPoint = section.StartPoint,
EndPoint = section.EndPoint,
BendPoints = newBendPoints,
});
}
edges[edgeIndex] = new ElkRoutedEdge
{
Id = edge.Id,
SourceNodeId = edge.SourceNodeId,
TargetNodeId = edge.TargetNodeId,
Label = edge.Label,
Sections = newSections,
};
outerEdges[idx] = (edgeIndex, targetY, outerEdges[idx].IsAbove);
}
}
}
}

View File

@@ -0,0 +1,132 @@
namespace StellaOps.ElkSharp;
internal static partial class ElkEdgePostProcessorSimplify
{
internal static bool SegmentClearsObstacles(
ElkPoint p1, ElkPoint p2,
(double L, double T, double R, double B, string Id)[] obstacles,
HashSet<string> excludeIds)
{
var isH = Math.Abs(p1.Y - p2.Y) < 1d;
var isV = Math.Abs(p1.X - p2.X) < 1d;
if (!isH && !isV) return true;
foreach (var ob in obstacles)
{
if (excludeIds.Contains(ob.Id)) continue;
if (isH && p1.Y > ob.T && p1.Y < ob.B)
{
if (Math.Max(p1.X, p2.X) > ob.L && Math.Min(p1.X, p2.X) < ob.R) return false;
}
else if (isV && p1.X > ob.L && p1.X < ob.R)
{
if (Math.Max(p1.Y, p2.Y) > ob.T && Math.Min(p1.Y, p2.Y) < ob.B) return false;
}
}
return true;
}
private static bool TryApplyOrthogonalShortcut(
List<ElkPoint> points,
(double L, double T, double R, double B, string Id)[] obstacles,
HashSet<string> excludeIds)
{
if (points.Count < 4)
{
return false;
}
for (var startIndex = 0; startIndex < points.Count - 2; startIndex++)
{
for (var endIndex = points.Count - 1; endIndex >= startIndex + 2; endIndex--)
{
var start = points[startIndex];
var end = points[endIndex];
var existingLength = ComputeSubpathLength(points, startIndex, endIndex);
foreach (var shortcut in BuildShortcutCandidates(start, end))
{
if (shortcut.Count < 2)
{
continue;
}
if (!ShortcutClearsObstacles(shortcut, obstacles, excludeIds))
{
continue;
}
var shortcutLength = ComputePathLength(shortcut);
if (shortcutLength >= existingLength - 8d)
{
continue;
}
points.RemoveRange(startIndex + 1, endIndex - startIndex - 1);
if (shortcut.Count > 2)
{
points.InsertRange(startIndex + 1, shortcut.Skip(1).Take(shortcut.Count - 2));
}
return true;
}
}
}
return false;
}
private static IReadOnlyList<List<ElkPoint>> BuildShortcutCandidates(ElkPoint start, ElkPoint end)
{
var candidates = new List<List<ElkPoint>>();
if (Math.Abs(start.X - end.X) < 1d || Math.Abs(start.Y - end.Y) < 1d)
{
candidates.Add([start, end]);
return candidates;
}
var corner1 = new ElkPoint { X = start.X, Y = end.Y };
var corner2 = new ElkPoint { X = end.X, Y = start.Y };
candidates.Add([start, corner1, end]);
candidates.Add([start, corner2, end]);
return candidates;
}
private static bool ShortcutClearsObstacles(
IReadOnlyList<ElkPoint> shortcut,
(double L, double T, double R, double B, string Id)[] obstacles,
HashSet<string> excludeIds)
{
for (var i = 0; i < shortcut.Count - 1; i++)
{
if (!SegmentClearsObstacles(shortcut[i], shortcut[i + 1], obstacles, excludeIds))
{
return false;
}
}
return true;
}
private static double ComputeSubpathLength(IReadOnlyList<ElkPoint> points, int startIndex, int endIndex)
{
var length = 0d;
for (var i = startIndex; i < endIndex; i++)
{
length += ElkEdgeRoutingGeometry.ComputeSegmentLength(points[i], points[i + 1]);
}
return length;
}
private static double ComputePathLength(IReadOnlyList<ElkPoint> points)
{
var length = 0d;
for (var i = 0; i < points.Count - 1; i++)
{
length += ElkEdgeRoutingGeometry.ComputeSegmentLength(points[i], points[i + 1]);
}
return length;
}
}

View File

@@ -1,6 +1,6 @@
namespace StellaOps.ElkSharp;
internal static class ElkEdgePostProcessorSimplify
internal static partial class ElkEdgePostProcessorSimplify
{
internal static ElkRoutedEdge[] SimplifyEdgePaths(
ElkRoutedEdge[] edges,
@@ -108,341 +108,4 @@ internal static class ElkEdgePostProcessorSimplify
return result;
}
internal static ElkRoutedEdge[] TightenOuterCorridors(
ElkRoutedEdge[] edges,
ElkPositionedNode[] nodes)
{
if (nodes.Length == 0) return edges;
var graphMinY = nodes.Min(n => n.Y);
var graphMaxY = nodes.Max(n => n.Y + n.Height);
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;
var minMargin = Math.Max(12d, minLineClearance + 4d);
var laneGap = Math.Max(8d, minLineClearance + 4d);
var outerEdges = new List<(int Index, double CorridorY, bool IsAbove)>();
for (var i = 0; i < edges.Length; i++)
{
var aboveYs = new List<double>();
var belowYs = new List<double>();
foreach (var section in edges[i].Sections)
{
foreach (var bp in section.BendPoints)
{
if (bp.Y < graphMinY - 8d)
{
aboveYs.Add(bp.Y);
}
else if (bp.Y > graphMaxY + 8d)
{
belowYs.Add(bp.Y);
}
}
}
if (aboveYs.Count > 0)
{
outerEdges.Add((i, aboveYs.Min(), true));
}
if (belowYs.Count > 0)
{
outerEdges.Add((i, belowYs.Max(), false));
}
}
if (outerEdges.Count == 0) return edges;
NormalizeCorridorYValues(outerEdges, edges, graphMinY, graphMaxY);
var aboveLanes = outerEdges.Where(e => e.IsAbove)
.GroupBy(e => Math.Round(e.CorridorY, 1))
.OrderBy(g => g.Key)
.ToArray();
var belowLanes = outerEdges.Where(e => !e.IsAbove)
.GroupBy(e => Math.Round(e.CorridorY, 1))
.OrderByDescending(g => g.Key)
.ToArray();
var result = edges.ToArray();
var shifts = new Dictionary<int, double>();
for (var lane = 0; lane < aboveLanes.Length; lane++)
{
var targetY = graphMinY - minMargin - (lane * laneGap);
var currentY = aboveLanes[lane].Key;
var shift = targetY - currentY;
if (Math.Abs(shift) > 2d)
{
foreach (var entry in aboveLanes[lane])
{
shifts[entry.Index] = shift;
}
}
}
for (var lane = 0; lane < belowLanes.Length; lane++)
{
var targetY = graphMaxY + minMargin + (lane * laneGap);
var currentY = belowLanes[lane].Key;
var shift = targetY - currentY;
if (Math.Abs(shift) > 2d)
{
foreach (var entry in belowLanes[lane])
{
shifts[entry.Index] = shift;
}
}
}
foreach (var (edgeIndex, shift) in shifts)
{
var edge = result[edgeIndex];
var boundary = shift > 0 ? graphMaxY : graphMinY;
var newSections = new List<ElkEdgeSection>();
foreach (var section in edge.Sections)
{
var newBendPoints = section.BendPoints.Select(bp =>
{
if ((shift < 0 && bp.Y < graphMinY - 4d) || (shift > 0 && bp.Y > graphMaxY + 4d)
|| (shift > 0 && bp.Y < graphMinY - 4d) || (shift < 0 && bp.Y > graphMaxY + 4d))
{
return new ElkPoint { X = bp.X, Y = bp.Y + shift };
}
return bp;
}).ToArray();
newSections.Add(new ElkEdgeSection
{
StartPoint = section.StartPoint,
EndPoint = section.EndPoint,
BendPoints = newBendPoints,
});
}
result[edgeIndex] = new ElkRoutedEdge
{
Id = edge.Id,
SourceNodeId = edge.SourceNodeId,
TargetNodeId = edge.TargetNodeId,
Label = edge.Label,
Sections = newSections,
};
}
return result;
}
internal static bool SegmentClearsObstacles(
ElkPoint p1, ElkPoint p2,
(double L, double T, double R, double B, string Id)[] obstacles,
HashSet<string> excludeIds)
{
var isH = Math.Abs(p1.Y - p2.Y) < 1d;
var isV = Math.Abs(p1.X - p2.X) < 1d;
if (!isH && !isV) return true;
foreach (var ob in obstacles)
{
if (excludeIds.Contains(ob.Id)) continue;
if (isH && p1.Y > ob.T && p1.Y < ob.B)
{
if (Math.Max(p1.X, p2.X) > ob.L && Math.Min(p1.X, p2.X) < ob.R) return false;
}
else if (isV && p1.X > ob.L && p1.X < ob.R)
{
if (Math.Max(p1.Y, p2.Y) > ob.T && Math.Min(p1.Y, p2.Y) < ob.B) return false;
}
}
return true;
}
private static bool TryApplyOrthogonalShortcut(
List<ElkPoint> points,
(double L, double T, double R, double B, string Id)[] obstacles,
HashSet<string> excludeIds)
{
if (points.Count < 4)
{
return false;
}
for (var startIndex = 0; startIndex < points.Count - 2; startIndex++)
{
for (var endIndex = points.Count - 1; endIndex >= startIndex + 2; endIndex--)
{
var start = points[startIndex];
var end = points[endIndex];
var existingLength = ComputeSubpathLength(points, startIndex, endIndex);
foreach (var shortcut in BuildShortcutCandidates(start, end))
{
if (shortcut.Count < 2)
{
continue;
}
if (!ShortcutClearsObstacles(shortcut, obstacles, excludeIds))
{
continue;
}
var shortcutLength = ComputePathLength(shortcut);
if (shortcutLength >= existingLength - 8d)
{
continue;
}
points.RemoveRange(startIndex + 1, endIndex - startIndex - 1);
if (shortcut.Count > 2)
{
points.InsertRange(startIndex + 1, shortcut.Skip(1).Take(shortcut.Count - 2));
}
return true;
}
}
}
return false;
}
private static IReadOnlyList<List<ElkPoint>> BuildShortcutCandidates(ElkPoint start, ElkPoint end)
{
var candidates = new List<List<ElkPoint>>();
if (Math.Abs(start.X - end.X) < 1d || Math.Abs(start.Y - end.Y) < 1d)
{
candidates.Add([start, end]);
return candidates;
}
var corner1 = new ElkPoint { X = start.X, Y = end.Y };
var corner2 = new ElkPoint { X = end.X, Y = start.Y };
candidates.Add([start, corner1, end]);
candidates.Add([start, corner2, end]);
return candidates;
}
private static bool ShortcutClearsObstacles(
IReadOnlyList<ElkPoint> shortcut,
(double L, double T, double R, double B, string Id)[] obstacles,
HashSet<string> excludeIds)
{
for (var i = 0; i < shortcut.Count - 1; i++)
{
if (!SegmentClearsObstacles(shortcut[i], shortcut[i + 1], obstacles, excludeIds))
{
return false;
}
}
return true;
}
private static double ComputeSubpathLength(IReadOnlyList<ElkPoint> points, int startIndex, int endIndex)
{
var length = 0d;
for (var i = startIndex; i < endIndex; i++)
{
length += ElkEdgeRoutingGeometry.ComputeSegmentLength(points[i], points[i + 1]);
}
return length;
}
private static double ComputePathLength(IReadOnlyList<ElkPoint> points)
{
var length = 0d;
for (var i = 0; i < points.Count - 1; i++)
{
length += ElkEdgeRoutingGeometry.ComputeSegmentLength(points[i], points[i + 1]);
}
return length;
}
private static void NormalizeCorridorYValues(
List<(int Index, double CorridorY, bool IsAbove)> outerEdges,
ElkRoutedEdge[] edges,
double graphMinY, double graphMaxY)
{
const double mergeThreshold = 6d;
var groups = new List<List<int>>();
var sorted = outerEdges.OrderBy(e => e.CorridorY).ToArray();
foreach (var entry in sorted)
{
var merged = false;
foreach (var group in groups)
{
var groupY = outerEdges[group[0]].CorridorY;
if (Math.Abs(entry.CorridorY - groupY) <= mergeThreshold && entry.IsAbove == outerEdges[group[0]].IsAbove)
{
group.Add(outerEdges.IndexOf(entry));
merged = true;
break;
}
}
if (!merged)
{
groups.Add([outerEdges.IndexOf(entry)]);
}
}
foreach (var group in groups)
{
if (group.Count <= 1)
{
continue;
}
var targetY = outerEdges[group[0]].CorridorY;
for (var gi = 1; gi < group.Count; gi++)
{
var idx = group[gi];
var edgeIndex = outerEdges[idx].Index;
var currentY = outerEdges[idx].CorridorY;
var shift = targetY - currentY;
if (Math.Abs(shift) < 0.5d)
{
continue;
}
var edge = edges[edgeIndex];
var newSections = new List<ElkEdgeSection>();
foreach (var section in edge.Sections)
{
var newBendPoints = section.BendPoints.Select(bp =>
{
if (Math.Abs(bp.Y - currentY) < 2d)
{
return new ElkPoint { X = bp.X, Y = targetY };
}
return bp;
}).ToArray();
newSections.Add(new ElkEdgeSection
{
StartPoint = section.StartPoint,
EndPoint = section.EndPoint,
BendPoints = newBendPoints,
});
}
edges[edgeIndex] = new ElkRoutedEdge
{
Id = edge.Id,
SourceNodeId = edge.SourceNodeId,
TargetNodeId = edge.TargetNodeId,
Label = edge.Label,
Sections = newSections,
};
outerEdges[idx] = (edgeIndex, targetY, outerEdges[idx].IsAbove);
}
}
}
}

View File

@@ -0,0 +1,89 @@
namespace StellaOps.ElkSharp;
internal static partial class ElkEdgeRouteRefiner
{
private static bool IsBetterCandidate(EdgeRoutingScore candidate, EdgeRoutingScore baseline)
{
if (candidate.NodeCrossings != baseline.NodeCrossings)
{
return candidate.NodeCrossings < baseline.NodeCrossings;
}
if (candidate.EdgeCrossings != baseline.EdgeCrossings)
{
return candidate.EdgeCrossings < baseline.EdgeCrossings;
}
return candidate.Value > baseline.Value + 0.01d;
}
private static bool CanRefineEdge(ElkRoutedEdge edge, IReadOnlyCollection<ElkPositionedNode> nodes)
{
if (!string.IsNullOrWhiteSpace(edge.SourcePortId) || !string.IsNullOrWhiteSpace(edge.TargetPortId))
{
return false;
}
if (!string.IsNullOrWhiteSpace(edge.Kind)
&& edge.Kind.StartsWith("backward|", StringComparison.OrdinalIgnoreCase))
{
return false;
}
if (nodes.Count == 0)
{
return false;
}
var graphMinY = nodes.Min(node => node.Y);
var graphMaxY = nodes.Max(node => node.Y + node.Height);
if (ElkEdgePostProcessor.HasCorridorBendPoints(edge, graphMinY, graphMaxY))
{
return false;
}
return !ElkEdgePostProcessor.IsRepeatCollectorLabel(edge.Label);
}
private static IReadOnlyList<OrthogonalSoftObstacle> BuildSoftObstacles(
IReadOnlyCollection<ElkRoutedEdge> edges,
string excludedEdgeId)
{
return ElkEdgeRoutingGeometry.FlattenSegments(edges)
.Where(segment => !string.Equals(segment.EdgeId, excludedEdgeId, StringComparison.Ordinal))
.Select(segment => new OrthogonalSoftObstacle(segment.Start, segment.End))
.ToArray();
}
private static IEnumerable<OrthogonalAStarOptions> BuildTrials(EdgeRefinementOptions options)
{
foreach (var template in TrialTemplates)
{
var softObstacleWeight = options.SoftObstacleWeight <= 0d
? 0d
: Math.Max(options.SoftObstacleWeight, template.SoftObstacleWeight);
yield return new OrthogonalAStarOptions(
Math.Max(options.BaseObstacleMargin, template.Margin),
template.BendPenalty,
softObstacleWeight,
Math.Max(8d, options.SoftObstacleClearance));
}
}
private static EdgeRefinementOptions ResolveOptions(ElkLayoutOptions layoutOptions)
{
var requested = layoutOptions.EdgeRefinement ?? new EdgeRefinementOptions();
var enabled = requested.Enabled ?? layoutOptions.Effort == ElkLayoutEffort.Best;
return new EdgeRefinementOptions
{
Enabled = enabled,
MaxGlobalPasses = Math.Max(0, requested.MaxGlobalPasses),
MaxTrialsPerProblemEdge = Math.Max(1, requested.MaxTrialsPerProblemEdge),
MaxProblemEdgesPerPass = Math.Max(1, requested.MaxProblemEdgesPerPass),
BaseObstacleMargin = Math.Max(8d, requested.BaseObstacleMargin),
SoftObstacleWeight = Math.Max(0d, requested.SoftObstacleWeight),
SoftObstacleClearance = Math.Max(8d, requested.SoftObstacleClearance),
};
}
}

View File

@@ -1,6 +1,6 @@
namespace StellaOps.ElkSharp;
internal static class ElkEdgeRouteRefiner
internal static partial class ElkEdgeRouteRefiner
{
private static readonly OrthogonalAStarOptions[] TrialTemplates =
[
@@ -251,89 +251,4 @@ internal static class ElkEdgeRouteRefiner
improvedScore = bestLocalScore;
return true;
}
private static bool IsBetterCandidate(EdgeRoutingScore candidate, EdgeRoutingScore baseline)
{
if (candidate.NodeCrossings != baseline.NodeCrossings)
{
return candidate.NodeCrossings < baseline.NodeCrossings;
}
if (candidate.EdgeCrossings != baseline.EdgeCrossings)
{
return candidate.EdgeCrossings < baseline.EdgeCrossings;
}
return candidate.Value > baseline.Value + 0.01d;
}
private static bool CanRefineEdge(ElkRoutedEdge edge, IReadOnlyCollection<ElkPositionedNode> nodes)
{
if (!string.IsNullOrWhiteSpace(edge.SourcePortId) || !string.IsNullOrWhiteSpace(edge.TargetPortId))
{
return false;
}
if (!string.IsNullOrWhiteSpace(edge.Kind)
&& edge.Kind.StartsWith("backward|", StringComparison.OrdinalIgnoreCase))
{
return false;
}
if (nodes.Count == 0)
{
return false;
}
var graphMinY = nodes.Min(node => node.Y);
var graphMaxY = nodes.Max(node => node.Y + node.Height);
if (ElkEdgePostProcessor.HasCorridorBendPoints(edge, graphMinY, graphMaxY))
{
return false;
}
return !ElkEdgePostProcessor.IsRepeatCollectorLabel(edge.Label);
}
private static IReadOnlyList<OrthogonalSoftObstacle> BuildSoftObstacles(
IReadOnlyCollection<ElkRoutedEdge> edges,
string excludedEdgeId)
{
return ElkEdgeRoutingGeometry.FlattenSegments(edges)
.Where(segment => !string.Equals(segment.EdgeId, excludedEdgeId, StringComparison.Ordinal))
.Select(segment => new OrthogonalSoftObstacle(segment.Start, segment.End))
.ToArray();
}
private static IEnumerable<OrthogonalAStarOptions> BuildTrials(EdgeRefinementOptions options)
{
foreach (var template in TrialTemplates)
{
var softObstacleWeight = options.SoftObstacleWeight <= 0d
? 0d
: Math.Max(options.SoftObstacleWeight, template.SoftObstacleWeight);
yield return new OrthogonalAStarOptions(
Math.Max(options.BaseObstacleMargin, template.Margin),
template.BendPenalty,
softObstacleWeight,
Math.Max(8d, options.SoftObstacleClearance));
}
}
private static EdgeRefinementOptions ResolveOptions(ElkLayoutOptions layoutOptions)
{
var requested = layoutOptions.EdgeRefinement ?? new EdgeRefinementOptions();
var enabled = requested.Enabled ?? layoutOptions.Effort == ElkLayoutEffort.Best;
return new EdgeRefinementOptions
{
Enabled = enabled,
MaxGlobalPasses = Math.Max(0, requested.MaxGlobalPasses),
MaxTrialsPerProblemEdge = Math.Max(1, requested.MaxTrialsPerProblemEdge),
MaxProblemEdgesPerPass = Math.Max(1, requested.MaxProblemEdgesPerPass),
BaseObstacleMargin = Math.Max(8d, requested.BaseObstacleMargin),
SoftObstacleWeight = Math.Max(0d, requested.SoftObstacleWeight),
SoftObstacleClearance = Math.Max(8d, requested.SoftObstacleClearance),
};
}
}

View File

@@ -0,0 +1,175 @@
namespace StellaOps.ElkSharp;
internal static partial class ElkEdgeRouter
{
internal static Dictionary<string, ElkRoutedEdge> ReconstructDummyEdges(
IReadOnlyCollection<ElkEdge> originalEdges,
DummyNodeResult dummyResult,
IReadOnlyDictionary<string, ElkPositionedNode> positionedNodes,
IReadOnlyDictionary<string, ElkNode> augmentedNodesById,
ElkLayoutDirection direction,
GraphBounds graphBounds,
IReadOnlyDictionary<string, EdgeChannel> edgeChannels,
IReadOnlyDictionary<string, LayerBoundary> layerBoundariesByNodeId)
{
var edgesWithChains = originalEdges
.Where(edge => dummyResult.EdgeDummyChains.ContainsKey(edge.Id))
.ToArray();
var incomingByTarget = edgesWithChains
.GroupBy(edge => edge.TargetNodeId, StringComparer.Ordinal)
.ToDictionary(group => group.Key, group => group.OrderBy(edge =>
{
var sourceNode = positionedNodes[edge.SourceNodeId];
return direction == ElkLayoutDirection.LeftToRight
? sourceNode.Y + (sourceNode.Height / 2d)
: sourceNode.X + (sourceNode.Width / 2d);
}).ToArray(), StringComparer.Ordinal);
var outgoingBySource = edgesWithChains
.GroupBy(edge => edge.SourceNodeId, StringComparer.Ordinal)
.ToDictionary(group => group.Key, group => group.OrderBy(edge =>
{
var targetNode = positionedNodes[edge.TargetNodeId];
return direction == ElkLayoutDirection.LeftToRight
? targetNode.Y + (targetNode.Height / 2d)
: targetNode.X + (targetNode.Width / 2d);
}).ToArray(), StringComparer.Ordinal);
var reconstructed = new Dictionary<string, ElkRoutedEdge>(StringComparer.Ordinal);
foreach (var edge in edgesWithChains)
{
if (!dummyResult.EdgeDummyChains.TryGetValue(edge.Id, out var chain) || chain.Count == 0)
{
continue;
}
var channel = edgeChannels.GetValueOrDefault(edge.Id);
var useCorridorRouting = channel.RouteMode != EdgeRouteMode.Direct
|| !string.IsNullOrWhiteSpace(edge.SourcePortId)
|| !string.IsNullOrWhiteSpace(edge.TargetPortId);
if (useCorridorRouting)
{
reconstructed[edge.Id] = RouteEdge(
edge,
augmentedNodesById,
positionedNodes,
direction,
graphBounds,
channel,
layerBoundariesByNodeId);
continue;
}
var sourceNode = positionedNodes[edge.SourceNodeId];
var targetNode = positionedNodes[edge.TargetNodeId];
var sourceGroup = outgoingBySource.GetValueOrDefault(edge.SourceNodeId);
var targetGroup = incomingByTarget.GetValueOrDefault(edge.TargetNodeId);
if (ShouldRouteLongEdgeViaDirectRouter(edge, sourceNode, targetNode, sourceGroup, targetGroup, positionedNodes, direction))
{
reconstructed[edge.Id] = RouteEdge(
edge,
augmentedNodesById,
positionedNodes,
direction,
graphBounds,
channel,
layerBoundariesByNodeId);
continue;
}
var bendPoints = new List<ElkPoint>();
foreach (var dummyId in chain)
{
if (positionedNodes.TryGetValue(dummyId, out var dummyPosition))
{
var centerY = dummyPosition.Y + (dummyPosition.Height / 2d);
if (channel.RouteMode == EdgeRouteMode.Direct
&& (centerY > graphBounds.MaxY + 8d || centerY < graphBounds.MinY - 8d))
{
continue;
}
bendPoints.Add(new ElkPoint
{
X = dummyPosition.X + (dummyPosition.Width / 2d),
Y = centerY,
});
}
}
var sourceExitY = ElkEdgeRouterGrouping.ResolveGroupedAnchorCoordinate(sourceNode, edge, sourceGroup, positionedNodes, isSource: true, direction);
var targetEntryY = ElkEdgeRouterGrouping.ResolveGroupedAnchorCoordinate(targetNode, edge, targetGroup, positionedNodes, isSource: false, direction);
var targetCenter = new ElkPoint
{
X = targetNode.X + (targetNode.Width / 2d),
Y = targetNode.Y + (targetNode.Height / 2d),
};
var sourceAnchor = ElkEdgeRouterAnchors.ComputeSmartAnchor(
sourceNode,
targetCenter,
true,
sourceExitY,
sourceGroup?.Length ?? 1,
direction);
var targetAnchor = ElkEdgeRouterAnchors.ComputeSmartAnchor(
targetNode,
bendPoints.Count > 0 ? bendPoints[^1] : null,
false,
targetEntryY,
targetGroup?.Length ?? 1,
direction);
if (direction == ElkLayoutDirection.LeftToRight
&& (string.Equals(targetNode.Kind, "End", StringComparison.OrdinalIgnoreCase)
|| string.Equals(targetNode.Kind, "Join", StringComparison.OrdinalIgnoreCase)))
{
var sourceCenterY = sourceNode.Y + (sourceNode.Height / 2d);
var targetCenterY = targetNode.Y + (targetNode.Height / 2d);
var verticalOffset = sourceCenterY - targetCenterY;
var sideThreshold = targetNode.Height * 0.4d;
if (verticalOffset < -sideThreshold)
{
targetAnchor = ElkEdgeRouterAnchors.ResolvePreferredAnchorPoint(
targetNode, sourceNode.X + sourceNode.Width, targetNode.Y - 256d, "NORTH", direction);
var approachOffset = 24d;
var approachX = targetNode.X + (targetNode.Width / 2d);
bendPoints.Add(new ElkPoint { X = approachX, Y = targetNode.Y - approachOffset });
}
else if (verticalOffset > sideThreshold)
{
targetAnchor = ElkEdgeRouterAnchors.ResolvePreferredAnchorPoint(
targetNode, sourceNode.X + sourceNode.Width, targetNode.Y + targetNode.Height + 256d, "SOUTH", direction);
var approachOffset = 24d;
var approachX = targetNode.X + (targetNode.Width / 2d);
bendPoints.Add(new ElkPoint { X = approachX, Y = targetNode.Y + targetNode.Height + approachOffset });
}
}
reconstructed[edge.Id] = new ElkRoutedEdge
{
Id = edge.Id,
SourceNodeId = edge.SourceNodeId,
TargetNodeId = edge.TargetNodeId,
SourcePortId = edge.SourcePortId,
TargetPortId = edge.TargetPortId,
Kind = edge.Kind,
Label = edge.Label,
Sections =
[
new ElkEdgeSection
{
StartPoint = sourceAnchor,
EndPoint = targetAnchor,
BendPoints = bendPoints,
},
],
};
}
return reconstructed;
}
}

View File

@@ -1,6 +1,6 @@
namespace StellaOps.ElkSharp;
internal static class ElkEdgeRouter
internal static partial class ElkEdgeRouter
{
internal static ElkRoutedEdge RouteEdge(
ElkEdge edge,
@@ -143,167 +143,6 @@ internal static class ElkEdgeRouter
};
}
internal static Dictionary<string, ElkRoutedEdge> ReconstructDummyEdges(
IReadOnlyCollection<ElkEdge> originalEdges,
DummyNodeResult dummyResult,
IReadOnlyDictionary<string, ElkPositionedNode> positionedNodes,
IReadOnlyDictionary<string, ElkNode> augmentedNodesById,
ElkLayoutDirection direction,
GraphBounds graphBounds,
IReadOnlyDictionary<string, EdgeChannel> edgeChannels,
IReadOnlyDictionary<string, LayerBoundary> layerBoundariesByNodeId)
{
var edgesWithChains = originalEdges
.Where(e => dummyResult.EdgeDummyChains.ContainsKey(e.Id))
.ToArray();
var incomingByTarget = edgesWithChains
.GroupBy(e => e.TargetNodeId, StringComparer.Ordinal)
.ToDictionary(g => g.Key, g => g.OrderBy(e =>
{
var s = positionedNodes[e.SourceNodeId];
return direction == ElkLayoutDirection.LeftToRight
? s.Y + (s.Height / 2d)
: s.X + (s.Width / 2d);
}).ToArray(), StringComparer.Ordinal);
var outgoingBySource = edgesWithChains
.GroupBy(e => e.SourceNodeId, StringComparer.Ordinal)
.ToDictionary(g => g.Key, g => g.OrderBy(e =>
{
var t = positionedNodes[e.TargetNodeId];
return direction == ElkLayoutDirection.LeftToRight
? t.Y + (t.Height / 2d)
: t.X + (t.Width / 2d);
}).ToArray(), StringComparer.Ordinal);
var reconstructed = new Dictionary<string, ElkRoutedEdge>(StringComparer.Ordinal);
foreach (var edge in edgesWithChains)
{
if (!dummyResult.EdgeDummyChains.TryGetValue(edge.Id, out var chain) || chain.Count == 0)
{
continue;
}
var channel = edgeChannels.GetValueOrDefault(edge.Id);
var useCorridorRouting = channel.RouteMode != EdgeRouteMode.Direct
|| !string.IsNullOrWhiteSpace(edge.SourcePortId)
|| !string.IsNullOrWhiteSpace(edge.TargetPortId);
if (useCorridorRouting)
{
reconstructed[edge.Id] = RouteEdge(
edge,
augmentedNodesById,
positionedNodes,
direction,
graphBounds,
channel,
layerBoundariesByNodeId);
continue;
}
var sourceNode = positionedNodes[edge.SourceNodeId];
var targetNode = positionedNodes[edge.TargetNodeId];
var sourceGroup = outgoingBySource.GetValueOrDefault(edge.SourceNodeId);
var targetGroup = incomingByTarget.GetValueOrDefault(edge.TargetNodeId);
if (ShouldRouteLongEdgeViaDirectRouter(edge, sourceNode, targetNode, sourceGroup, targetGroup, positionedNodes, direction))
{
reconstructed[edge.Id] = RouteEdge(
edge,
augmentedNodesById,
positionedNodes,
direction,
graphBounds,
channel,
layerBoundariesByNodeId);
continue;
}
var bendPoints = new List<ElkPoint>();
foreach (var dummyId in chain)
{
if (positionedNodes.TryGetValue(dummyId, out var dummyPos))
{
var centerY = dummyPos.Y + (dummyPos.Height / 2d);
if (channel.RouteMode == EdgeRouteMode.Direct
&& (centerY > graphBounds.MaxY + 8d || centerY < graphBounds.MinY - 8d))
{
continue;
}
bendPoints.Add(new ElkPoint
{
X = dummyPos.X + (dummyPos.Width / 2d),
Y = centerY,
});
}
}
var sourceExitY = ElkEdgeRouterGrouping.ResolveGroupedAnchorCoordinate(sourceNode, edge, sourceGroup, positionedNodes, isSource: true, direction);
var targetEntryY = ElkEdgeRouterGrouping.ResolveGroupedAnchorCoordinate(targetNode, edge, targetGroup, positionedNodes, isSource: false, direction);
var targetCenter = new ElkPoint
{
X = targetNode.X + (targetNode.Width / 2d),
Y = targetNode.Y + (targetNode.Height / 2d),
};
var sourceAnchor = ElkEdgeRouterAnchors.ComputeSmartAnchor(sourceNode, targetCenter,
true, sourceExitY, sourceGroup?.Length ?? 1, direction);
var targetAnchor = ElkEdgeRouterAnchors.ComputeSmartAnchor(targetNode, bendPoints.Count > 0 ? bendPoints[^1] : null,
false, targetEntryY, targetGroup?.Length ?? 1, direction);
if (direction == ElkLayoutDirection.LeftToRight
&& (string.Equals(targetNode.Kind, "End", StringComparison.OrdinalIgnoreCase)
|| string.Equals(targetNode.Kind, "Join", StringComparison.OrdinalIgnoreCase)))
{
var sourceCenterY = sourceNode.Y + (sourceNode.Height / 2d);
var targetCenterY = targetNode.Y + (targetNode.Height / 2d);
var verticalOffset = sourceCenterY - targetCenterY;
var sideThreshold = targetNode.Height * 0.4d;
if (verticalOffset < -sideThreshold)
{
targetAnchor = ElkEdgeRouterAnchors.ResolvePreferredAnchorPoint(
targetNode, sourceNode.X + sourceNode.Width, targetNode.Y - 256d, "NORTH", direction);
var approachOffset = 24d;
var approachX = targetNode.X + (targetNode.Width / 2d);
bendPoints.Add(new ElkPoint { X = approachX, Y = targetNode.Y - approachOffset });
}
else if (verticalOffset > sideThreshold)
{
targetAnchor = ElkEdgeRouterAnchors.ResolvePreferredAnchorPoint(
targetNode, sourceNode.X + sourceNode.Width, targetNode.Y + targetNode.Height + 256d, "SOUTH", direction);
var approachOffset = 24d;
var approachX = targetNode.X + (targetNode.Width / 2d);
bendPoints.Add(new ElkPoint { X = approachX, Y = targetNode.Y + targetNode.Height + approachOffset });
}
}
reconstructed[edge.Id] = new ElkRoutedEdge
{
Id = edge.Id,
SourceNodeId = edge.SourceNodeId,
TargetNodeId = edge.TargetNodeId,
SourcePortId = edge.SourcePortId,
TargetPortId = edge.TargetPortId,
Kind = edge.Kind,
Label = edge.Label,
Sections =
[
new ElkEdgeSection
{
StartPoint = sourceAnchor,
EndPoint = targetAnchor,
BendPoints = bendPoints,
},
],
};
}
return reconstructed;
}
internal static bool ShouldRouteLongEdgeViaDirectRouter(
ElkEdge edge,
ElkPositionedNode sourceNode,

View File

@@ -0,0 +1,135 @@
namespace StellaOps.ElkSharp;
internal static partial class ElkEdgeRouterAStar8Dir
{
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)
{
return 0d;
}
if ((curDir <= 2 && newDir <= 2) || (curDir >= 3 && newDir >= 3))
{
return bendPenalty;
}
return bendPenalty / 2d;
}
private static double ComputeSoftObstacleCost(
double x1,
double y1,
double x2,
double y2,
SoftObstacleInfo[] softObstacles,
AStarRoutingParams routingParams)
{
if (routingParams.SoftObstacleWeight <= 0d || softObstacles.Length == 0)
{
return 0d;
}
var candidateStart = new ElkPoint { X = x1, Y = y1 };
var candidateEnd = new ElkPoint { X = x2, Y = y2 };
var candidateIsHorizontal = Math.Abs(y2 - y1) < 2d;
var candidateIsVertical = 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;
continue;
}
var distance = ComputeParallelDistance(
x1,
y1,
x2,
y2,
candidateIsHorizontal,
candidateIsVertical,
obstacle,
routingParams.SoftObstacleClearance);
if (distance >= 0d)
{
var factor = 1d - (distance / routingParams.SoftObstacleClearance);
cost += 60d * factor * factor * routingParams.SoftObstacleWeight;
}
}
return cost;
}
private static double ComputeParallelDistance(
double x1,
double y1,
double x2,
double y2,
bool candidateIsHorizontal,
bool candidateIsVertical,
SoftObstacleInfo obstacle,
double clearance)
{
if (candidateIsHorizontal && obstacle.IsHorizontal)
{
var distance = Math.Abs(y1 - obstacle.Start.Y);
if (distance >= clearance)
{
return -1d;
}
var overlapMin = Math.Max(Math.Min(x1, x2), obstacle.MinX);
var overlapMax = Math.Min(Math.Max(x1, x2), obstacle.MaxX);
return overlapMax > overlapMin + 1d ? distance : -1d;
}
if (candidateIsVertical && obstacle.IsVertical)
{
var distance = Math.Abs(x1 - obstacle.Start.X);
if (distance >= clearance)
{
return -1d;
}
var overlapMin = Math.Max(Math.Min(y1, y2), obstacle.MinY);
var overlapMax = Math.Min(Math.Max(y1, y2), obstacle.MaxY);
return overlapMax > overlapMin + 1d ? distance : -1d;
}
return -1d;
}
}

View File

@@ -0,0 +1,225 @@
namespace StellaOps.ElkSharp;
internal static partial class ElkEdgeRouterAStar8Dir
{
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 void AddIntermediateLines(SortedSet<double> coords, double spacing)
{
var arr = coords.ToArray();
for (var i = 0; i < arr.Length - 1; i++)
{
var gap = arr[i + 1] - arr[i];
if (gap <= spacing * 2d)
{
continue;
}
var count = (int)(gap / spacing);
var step = gap / (count + 1);
for (var j = 1; j <= count; j++)
{
coords.Add(arr[i] + j * step);
}
}
}
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];
}
}
}

View File

@@ -1,11 +1,11 @@
namespace StellaOps.ElkSharp;
internal static class ElkEdgeRouterAStar8Dir
internal static partial class ElkEdgeRouterAStar8Dir
{
// E, W, S, N, NE, SW, SE, NW
private static readonly int[] Dx = [1, -1, 0, 0, 1, -1, 1, -1];
private static readonly int[] Dy = [0, 0, 1, -1, -1, 1, 1, -1];
// Direction codes: 1=horizontal, 2=vertical, 3=diagonal45(NE/SW), 4=diagonal135(SE/NW)
// Direction codes: 1=horizontal, 2=vertical, 3=diagonal45 (NE/SW), 4=diagonal135 (SE/NW)
private static readonly int[] DirCodes = [1, 1, 2, 2, 3, 3, 4, 4];
internal static List<ElkPoint>? Route(
@@ -20,17 +20,17 @@ internal static class ElkEdgeRouterAStar8Dir
{
var xs = new SortedSet<double> { start.X, end.X };
var ys = new SortedSet<double> { start.Y, end.Y };
foreach (var ob in obstacles)
foreach (var obstacle in obstacles)
{
if (ob.Id == sourceId || ob.Id == targetId)
if (obstacle.Id == sourceId || obstacle.Id == targetId)
{
continue;
}
xs.Add(ob.Left - routingParams.Margin);
xs.Add(ob.Right + routingParams.Margin);
ys.Add(ob.Top - routingParams.Margin);
ys.Add(ob.Bottom + routingParams.Margin);
xs.Add(obstacle.Left - routingParams.Margin);
xs.Add(obstacle.Right + routingParams.Margin);
ys.Add(obstacle.Top - routingParams.Margin);
ys.Add(obstacle.Bottom + routingParams.Margin);
}
if (routingParams.IntermediateGridSpacing > 0d)
@@ -108,30 +108,27 @@ internal static class ElkEdgeRouterAStar8Dir
var cameFrom = new int[stateCount];
Array.Fill(cameFrom, -1);
// Side-aware entry angle: block moves parallel to the target's entry side
// Vertical side (left/right) → block vertical (dir=2), force horizontal approach
// Horizontal side (top/bottom) → block horizontal (dir=1), force vertical approach
var blockedEntryDir = 0;
if (routingParams.EnforceEntryAngle)
{
foreach (var ob in obstacles)
foreach (var obstacle in obstacles)
{
if (ob.Id != targetId)
if (obstacle.Id != targetId)
{
continue;
}
var nodeLeft = ob.Left + routingParams.Margin;
var nodeRight = ob.Right - routingParams.Margin;
var nodeTop = ob.Top + routingParams.Margin;
var nodeBottom = ob.Bottom - routingParams.Margin;
var nodeLeft = obstacle.Left + routingParams.Margin;
var nodeRight = obstacle.Right - routingParams.Margin;
var nodeTop = obstacle.Top + routingParams.Margin;
var nodeBottom = obstacle.Bottom - routingParams.Margin;
if (Math.Abs(end.X - nodeLeft) < 2d || Math.Abs(end.X - nodeRight) < 2d)
{
blockedEntryDir = 2; // vertical side → block vertical
blockedEntryDir = 2;
}
else if (Math.Abs(end.Y - nodeTop) < 2d || Math.Abs(end.Y - nodeBottom) < 2d)
{
blockedEntryDir = 1; // horizontal side → block horizontal
blockedEntryDir = 1;
}
break;
@@ -200,23 +197,16 @@ internal static class ElkEdgeRouterAStar8Dir
continue;
}
}
else
{
if (IsBlockedOrthogonal(curIx, curIy, nx, ny))
{
continue;
}
}
var newDir = DirCodes[d];
// Side-aware entry angle: block parallel moves into end cell
if (blockedEntryDir > 0 && nx == endIx && ny == endIy && newDir == blockedEntryDir)
else if (IsBlockedOrthogonal(curIx, curIy, nx, ny))
{
continue;
}
var bend = ComputeBendPenalty(curDir, newDir, routingParams.BendPenalty);
var newDir = DirCodes[d];
if (blockedEntryDir > 0 && nx == endIx && ny == endIy && newDir == blockedEntryDir)
{
continue;
}
double dist;
if (isDiagonal)
@@ -228,6 +218,7 @@ internal static class ElkEdgeRouterAStar8Dir
{
continue;
}
dist = diagonalStepLength + routingParams.DiagonalPenalty;
}
else
@@ -235,10 +226,10 @@ internal static class ElkEdgeRouterAStar8Dir
dist = Math.Abs(xArr[nx] - xArr[curIx]) + Math.Abs(yArr[ny] - yArr[curIy]);
}
var bend = ComputeBendPenalty(curDir, newDir, routingParams.BendPenalty);
var softCost = ComputeSoftObstacleCost(
xArr[curIx], yArr[curIy], xArr[nx], yArr[ny],
softObstacleInfos, routingParams);
var tentativeG = gScore[current] + dist + bend + softCost;
var neighborState = StateId(nx, ny, newDir);
@@ -254,317 +245,33 @@ 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)
{
return 0d;
}
// H↔V = 90° bend, diag↔diag (opposite types) = 90° bend
if ((curDir <= 2 && newDir <= 2) || (curDir >= 3 && newDir >= 3))
{
return bendPenalty;
}
// ortho↔diag = 45° bend
return bendPenalty / 2d;
}
private static double ComputeSoftObstacleCost(
double x1, double y1, double x2, double y2,
SoftObstacleInfo[] softObstacles,
AStarRoutingParams routingParams)
{
if (routingParams.SoftObstacleWeight <= 0d || softObstacles.Length == 0)
{
return 0d;
}
var candidateStart = new ElkPoint { X = x1, Y = y1 };
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;
continue;
}
// Graduated proximity: closer = exponentially more expensive
var dist = ComputeParallelDistance(
x1, y1, x2, y2, candidateIsH, candidateIsV,
obstacle,
routingParams.SoftObstacleClearance);
if (dist >= 0d)
{
var factor = 1d - (dist / routingParams.SoftObstacleClearance);
cost += 60d * factor * factor * routingParams.SoftObstacleWeight;
}
}
return cost;
}
private static double ComputeParallelDistance(
double x1, double y1, double x2, double y2,
bool candidateIsH, bool candidateIsV,
SoftObstacleInfo obstacle,
double clearance)
{
if (candidateIsH && obstacle.IsHorizontal)
{
var dist = Math.Abs(y1 - obstacle.Start.Y);
if (dist >= clearance)
{
return -1d;
}
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 && obstacle.IsVertical)
{
var dist = Math.Abs(x1 - obstacle.Start.X);
if (dist >= clearance)
{
return -1d;
}
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(
private static List<ElkPoint> ReconstructPath(
int endState,
int[] cameFrom,
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,
int yCount, int dirCount)
int yCount,
int dirCount)
{
var path = new List<ElkPoint>();
var state = endState;
while (state >= 0)
{
var sIy = (state / dirCount) % yCount;
var sIx = (state / dirCount) / yCount;
path.Add(new ElkPoint { X = xArr[sIx], Y = yArr[sIy] });
var stateY = (state / dirCount) % yCount;
var stateX = (state / dirCount) / yCount;
path.Add(new ElkPoint { X = xArr[stateX], Y = yArr[stateY] });
state = cameFrom[state];
}
path.Reverse();
// Simplify: remove collinear points (same direction between consecutive segments)
var simplified = new List<ElkPoint> { path[0] };
for (var i = 1; i < path.Count - 1; i++)
{
var prev = simplified[^1];
var previous = simplified[^1];
var next = path[i + 1];
var dx1 = Math.Sign(path[i].X - prev.X);
var dy1 = Math.Sign(path[i].Y - prev.Y);
var dx1 = Math.Sign(path[i].X - previous.X);
var dy1 = Math.Sign(path[i].Y - previous.Y);
var dx2 = Math.Sign(next.X - path[i].X);
var dy2 = Math.Sign(next.Y - path[i].Y);
if (dx1 != dx2 || dy1 != dy2)
@@ -576,61 +283,4 @@ internal static class ElkEdgeRouterAStar8Dir
simplified.Add(path[^1]);
return simplified;
}
private static void AddIntermediateLines(SortedSet<double> coords, double spacing)
{
var arr = coords.ToArray();
for (var i = 0; i < arr.Length - 1; i++)
{
var gap = arr[i + 1] - arr[i];
if (gap <= spacing * 2d)
{
continue;
}
var count = (int)(gap / spacing);
var step = gap / (count + 1);
for (var j = 1; j <= count; j++)
{
coords.Add(arr[i] + j * step);
}
}
}
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];
}
}
}

View File

@@ -0,0 +1,194 @@
namespace StellaOps.ElkSharp;
internal static partial class ElkEdgeRouterHighway
{
private static Dictionary<string, List<int>> BuildTargetSideGroups(
IReadOnlyList<ElkRoutedEdge> edges,
IReadOnlyDictionary<string, ElkPositionedNode> nodesById,
double graphMinY,
double graphMaxY)
{
var edgesByTargetSide = new Dictionary<string, List<int>>(StringComparer.Ordinal);
for (var i = 0; i < edges.Count; i++)
{
var edge = edges[i];
if (!ShouldProcessEdge(edge, graphMinY, graphMaxY))
{
continue;
}
if (!nodesById.TryGetValue(edge.TargetNodeId ?? string.Empty, out var targetNode))
{
continue;
}
if (ElkShapeBoundaries.IsGatewayShape(targetNode))
{
continue;
}
var path = ExtractFullPath(edge);
if (path.Count < 2)
{
continue;
}
var side = ElkEdgeRoutingGeometry.ResolveBoundarySide(path[^1], targetNode);
var key = $"{targetNode.Id}|{side}";
if (!edgesByTargetSide.TryGetValue(key, out var list))
{
list = [];
edgesByTargetSide[key] = list;
}
list.Add(i);
}
return edgesByTargetSide;
}
private static GroupEvaluation? EvaluateTargetSideGroup(
IReadOnlyList<ElkRoutedEdge> edges,
IReadOnlyList<int> edgeIndices,
ElkPositionedNode targetNode,
string side,
double minLineClearance)
{
var members = edgeIndices
.Select(index => CreateMember(edges[index], index, side))
.Where(member => member.Path.Count >= 2)
.OrderBy(member => member.EdgeId, StringComparer.Ordinal)
.ToList();
if (members.Count < 2)
{
return null;
}
var pairMetrics = ComputePairMetrics(members);
var actualGap = ComputeMinEndpointGap(members);
var requiredGap = ElkBoundarySlots.ResolveRequiredBoundarySlotGap(
targetNode,
side,
members.Count,
minLineClearance);
var requiresSpread = (actualGap + CoordinateTolerance) < requiredGap
&& !pairMetrics.AllPairsApplicable;
if (!requiresSpread && pairMetrics.ShortestSharedRatio < MinHighwayRatio)
{
requiresSpread = pairMetrics.HasSharedSegment;
}
var diagnostic = new ElkHighwayDiagnostics
{
TargetNodeId = targetNode.Id,
SharedAxis = side,
SharedCoord = Math.Round(members.Average(member => member.EndpointCoord), 1),
EdgeIds = members.Select(member => member.EdgeId).ToArray(),
MinRatio = pairMetrics.HasSharedSegment
? Math.Round(pairMetrics.ShortestSharedRatio, 3)
: 0d,
WasBroken = requiresSpread,
Reason = requiresSpread
? pairMetrics.HasSharedSegment && pairMetrics.ShortestSharedRatio < MinHighwayRatio
? $"shared ratio {pairMetrics.ShortestSharedRatio:F2} < {MinHighwayRatio:F2}"
: $"gap {actualGap:F0}px < required gap {requiredGap:F0}px"
: pairMetrics.AllPairsApplicable
? $"shared ratio {pairMetrics.ShortestSharedRatio:F2} >= {MinHighwayRatio:F2}"
: $"gap {actualGap:F0}px >= required gap {requiredGap:F0}px",
};
return new GroupEvaluation(members, diagnostic);
}
private static HighwayPairMetrics ComputePairMetrics(IReadOnlyList<HighwayMember> members)
{
var shortestSharedRatio = double.MaxValue;
var hasSharedSegment = false;
var allPairsApplicable = true;
for (var i = 0; i < members.Count; i++)
{
for (var j = i + 1; j < members.Count; j++)
{
var sharedLength = ElkEdgeRoutingGeometry.ComputeLongestSharedApproachSegmentLength(
members[i].Path,
members[j].Path);
if (sharedLength <= 1d)
{
allPairsApplicable = false;
continue;
}
hasSharedSegment = true;
var shortestPath = Math.Min(members[i].PathLength, members[j].PathLength);
if (shortestPath <= 1d)
{
allPairsApplicable = false;
continue;
}
var ratio = sharedLength / shortestPath;
shortestSharedRatio = Math.Min(shortestSharedRatio, ratio);
if (ratio < MinHighwayRatio)
{
allPairsApplicable = false;
}
}
}
return new HighwayPairMetrics(
HasSharedSegment: hasSharedSegment,
AllPairsApplicable: allPairsApplicable && hasSharedSegment,
ShortestSharedRatio: hasSharedSegment ? shortestSharedRatio : 0d);
}
private static double ComputeMinEndpointGap(IReadOnlyList<HighwayMember> members)
{
var coords = members
.Select(member => member.EndpointCoord)
.OrderBy(value => value)
.ToArray();
if (coords.Length < 2)
{
return double.MaxValue;
}
var minGap = double.MaxValue;
for (var i = 1; i < coords.Length; i++)
{
minGap = Math.Min(minGap, coords[i] - coords[i - 1]);
}
return minGap;
}
private static HighwayMember CreateMember(ElkRoutedEdge edge, int index, string side)
{
var path = ExtractFullPath(edge);
var endpointCoord = side is "left" or "right"
? path[^1].Y
: path[^1].X;
return new HighwayMember(
Index: index,
EdgeId: edge.Id,
Path: path,
PathLength: ElkEdgeRoutingGeometry.ComputePathLength(edge),
EndpointCoord: endpointCoord);
}
private readonly record struct HighwayMember(
int Index,
string EdgeId,
List<ElkPoint> Path,
double PathLength,
double EndpointCoord);
private readonly record struct HighwayPairMetrics(
bool HasSharedSegment,
bool AllPairsApplicable,
double ShortestSharedRatio);
private readonly record struct GroupEvaluation(
IReadOnlyList<HighwayMember> Members,
ElkHighwayDiagnostics Diagnostic);
}

View File

@@ -0,0 +1,170 @@
namespace StellaOps.ElkSharp;
internal static partial class ElkEdgeRouterHighway
{
private static double[] BuildSlotCoordinates(
ElkPositionedNode targetNode,
string side,
int count,
double minLineClearance)
{
return ElkBoundarySlots.BuildAssignedBoundarySlotCoordinates(targetNode, side, count);
}
private static List<ElkPoint> AdjustPathToTargetSlot(
IReadOnlyList<ElkPoint> path,
ElkPositionedNode targetNode,
string side,
double slotCoord,
double minLineClearance)
{
var adjusted = path
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
.ToList();
if (adjusted.Count < 2)
{
return adjusted;
}
if (side is "left" or "right")
{
var targetX = side == "left"
? targetNode.X
: targetNode.X + targetNode.Width;
while (adjusted.Count >= 3 && Math.Abs(adjusted[^2].X - targetX) <= CoordinateTolerance)
{
adjusted.RemoveAt(adjusted.Count - 2);
}
var anchor = adjusted[^2];
var rebuilt = adjusted.Take(adjusted.Count - 1).ToList();
if (Math.Abs(anchor.X - targetX) <= CoordinateTolerance)
{
var offset = Math.Max(24d, minLineClearance / 2d);
rebuilt.Add(new ElkPoint
{
X = side == "left" ? targetX - offset : targetX + offset,
Y = anchor.Y,
});
anchor = rebuilt[^1];
}
if (Math.Abs(anchor.Y - slotCoord) > CoordinateTolerance)
{
rebuilt.Add(new ElkPoint { X = anchor.X, Y = slotCoord });
}
rebuilt.Add(new ElkPoint { X = targetX, Y = slotCoord });
return NormalizePath(rebuilt);
}
var targetY = side == "top"
? targetNode.Y
: targetNode.Y + targetNode.Height;
while (adjusted.Count >= 3 && Math.Abs(adjusted[^2].Y - targetY) <= CoordinateTolerance)
{
adjusted.RemoveAt(adjusted.Count - 2);
}
var verticalAnchor = adjusted[^2];
var verticalRebuilt = adjusted.Take(adjusted.Count - 1).ToList();
if (Math.Abs(verticalAnchor.Y - targetY) <= CoordinateTolerance)
{
var offset = Math.Max(24d, minLineClearance / 2d);
verticalRebuilt.Add(new ElkPoint
{
X = verticalAnchor.X,
Y = side == "top" ? targetY - offset : targetY + offset,
});
verticalAnchor = verticalRebuilt[^1];
}
if (Math.Abs(verticalAnchor.X - slotCoord) > CoordinateTolerance)
{
verticalRebuilt.Add(new ElkPoint { X = slotCoord, Y = verticalAnchor.Y });
}
verticalRebuilt.Add(new ElkPoint { X = slotCoord, Y = targetY });
return NormalizePath(verticalRebuilt);
}
private static List<ElkPoint> NormalizePath(IReadOnlyList<ElkPoint> path)
{
var deduped = new List<ElkPoint>();
foreach (var point in path)
{
if (deduped.Count == 0 || !ElkEdgeRoutingGeometry.PointsEqual(deduped[^1], point))
{
deduped.Add(point);
}
}
if (deduped.Count <= 2)
{
return deduped;
}
var simplified = new List<ElkPoint> { deduped[0] };
for (var i = 1; i < deduped.Count - 1; i++)
{
var previous = simplified[^1];
var current = deduped[i];
var next = deduped[i + 1];
var sameX = Math.Abs(previous.X - current.X) <= CoordinateTolerance
&& Math.Abs(current.X - next.X) <= CoordinateTolerance;
var sameY = Math.Abs(previous.Y - current.Y) <= CoordinateTolerance
&& Math.Abs(current.Y - next.Y) <= CoordinateTolerance;
if (!sameX && !sameY)
{
simplified.Add(current);
}
}
simplified.Add(deduped[^1]);
return simplified;
}
private static void WriteBackPath(ElkRoutedEdge[] result, int edgeIndex, IReadOnlyList<ElkPoint> path)
{
var edge = result[edgeIndex];
result[edgeIndex] = new ElkRoutedEdge
{
Id = edge.Id,
SourceNodeId = edge.SourceNodeId,
TargetNodeId = edge.TargetNodeId,
SourcePortId = edge.SourcePortId,
TargetPortId = edge.TargetPortId,
Kind = edge.Kind,
Label = edge.Label,
Sections =
[
new ElkEdgeSection
{
StartPoint = path[0],
EndPoint = path[^1],
BendPoints = path.Count > 2
? path.Skip(1).Take(path.Count - 2).ToArray()
: [],
},
],
};
}
private static List<ElkPoint> ExtractFullPath(ElkRoutedEdge edge)
{
var path = new List<ElkPoint>();
foreach (var section in edge.Sections)
{
if (path.Count == 0)
{
path.Add(section.StartPoint);
}
path.AddRange(section.BendPoints);
path.Add(section.EndPoint);
}
return path;
}
}

View File

@@ -1,6 +1,6 @@
namespace StellaOps.ElkSharp;
internal static class ElkEdgeRouterHighway
internal static partial class ElkEdgeRouterHighway
{
private const double MinHighwayRatio = 2d / 5d;
private const double BoundaryInset = 4d;
@@ -143,366 +143,4 @@ internal static class ElkEdgeRouterHighway
WriteBackPath(result, ordered[i].Index, adjustedPath);
}
}
private static Dictionary<string, List<int>> BuildTargetSideGroups(
IReadOnlyList<ElkRoutedEdge> edges,
IReadOnlyDictionary<string, ElkPositionedNode> nodesById,
double graphMinY,
double graphMaxY)
{
var edgesByTargetSide = new Dictionary<string, List<int>>(StringComparer.Ordinal);
for (var i = 0; i < edges.Count; i++)
{
var edge = edges[i];
if (!ShouldProcessEdge(edge, graphMinY, graphMaxY))
{
continue;
}
if (!nodesById.TryGetValue(edge.TargetNodeId ?? string.Empty, out var targetNode))
{
continue;
}
if (ElkShapeBoundaries.IsGatewayShape(targetNode))
{
continue;
}
var path = ExtractFullPath(edge);
if (path.Count < 2)
{
continue;
}
var side = ElkEdgeRoutingGeometry.ResolveBoundarySide(path[^1], targetNode);
var key = $"{targetNode.Id}|{side}";
if (!edgesByTargetSide.TryGetValue(key, out var list))
{
list = [];
edgesByTargetSide[key] = list;
}
list.Add(i);
}
return edgesByTargetSide;
}
private static GroupEvaluation? EvaluateTargetSideGroup(
IReadOnlyList<ElkRoutedEdge> edges,
IReadOnlyList<int> edgeIndices,
ElkPositionedNode targetNode,
string side,
double minLineClearance)
{
var members = edgeIndices
.Select(index => CreateMember(edges[index], index, targetNode, side))
.Where(member => member.Path.Count >= 2)
.OrderBy(member => member.EdgeId, StringComparer.Ordinal)
.ToList();
if (members.Count < 2)
{
return null;
}
var pairMetrics = ComputePairMetrics(members);
var actualGap = ComputeMinEndpointGap(members, side);
var requiredGap = ElkBoundarySlots.ResolveRequiredBoundarySlotGap(
targetNode,
side,
members.Count,
minLineClearance);
var requiresSpread = (actualGap + CoordinateTolerance) < requiredGap
&& !pairMetrics.AllPairsApplicable;
if (!requiresSpread && pairMetrics.ShortestSharedRatio < MinHighwayRatio)
{
requiresSpread = pairMetrics.HasSharedSegment;
}
var diagnostic = new ElkHighwayDiagnostics
{
TargetNodeId = targetNode.Id,
SharedAxis = side,
SharedCoord = Math.Round(members.Average(member => member.EndpointCoord), 1),
EdgeIds = members.Select(member => member.EdgeId).ToArray(),
MinRatio = pairMetrics.HasSharedSegment
? Math.Round(pairMetrics.ShortestSharedRatio, 3)
: 0d,
WasBroken = requiresSpread,
Reason = requiresSpread
? pairMetrics.HasSharedSegment && pairMetrics.ShortestSharedRatio < MinHighwayRatio
? $"shared ratio {pairMetrics.ShortestSharedRatio:F2} < {MinHighwayRatio:F2}"
: $"gap {actualGap:F0}px < required gap {requiredGap:F0}px"
: pairMetrics.AllPairsApplicable
? $"shared ratio {pairMetrics.ShortestSharedRatio:F2} >= {MinHighwayRatio:F2}"
: $"gap {actualGap:F0}px >= required gap {requiredGap:F0}px",
};
return new GroupEvaluation(members, diagnostic);
}
private static HighwayPairMetrics ComputePairMetrics(IReadOnlyList<HighwayMember> members)
{
var shortestSharedRatio = double.MaxValue;
var hasSharedSegment = false;
var allPairsApplicable = true;
for (var i = 0; i < members.Count; i++)
{
for (var j = i + 1; j < members.Count; j++)
{
var sharedLength = ElkEdgeRoutingGeometry.ComputeLongestSharedApproachSegmentLength(
members[i].Path,
members[j].Path);
if (sharedLength <= 1d)
{
allPairsApplicable = false;
continue;
}
hasSharedSegment = true;
var shortestPath = Math.Min(members[i].PathLength, members[j].PathLength);
if (shortestPath <= 1d)
{
allPairsApplicable = false;
continue;
}
var ratio = sharedLength / shortestPath;
shortestSharedRatio = Math.Min(shortestSharedRatio, ratio);
if (ratio < MinHighwayRatio)
{
allPairsApplicable = false;
}
}
}
return new HighwayPairMetrics(
HasSharedSegment: hasSharedSegment,
AllPairsApplicable: allPairsApplicable && hasSharedSegment,
ShortestSharedRatio: hasSharedSegment ? shortestSharedRatio : 0d);
}
private static double ComputeMinEndpointGap(IReadOnlyList<HighwayMember> members, string side)
{
var coords = members
.Select(member => member.EndpointCoord)
.OrderBy(value => value)
.ToArray();
if (coords.Length < 2)
{
return double.MaxValue;
}
var minGap = double.MaxValue;
for (var i = 1; i < coords.Length; i++)
{
minGap = Math.Min(minGap, coords[i] - coords[i - 1]);
}
return minGap;
}
private static double[] BuildSlotCoordinates(
ElkPositionedNode targetNode,
string side,
int count,
double minLineClearance)
{
return ElkBoundarySlots.BuildAssignedBoundarySlotCoordinates(targetNode, side, count);
}
private static List<ElkPoint> AdjustPathToTargetSlot(
IReadOnlyList<ElkPoint> path,
ElkPositionedNode targetNode,
string side,
double slotCoord,
double minLineClearance)
{
var adjusted = path
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
.ToList();
if (adjusted.Count < 2)
{
return adjusted;
}
if (side is "left" or "right")
{
var targetX = side == "left"
? targetNode.X
: targetNode.X + targetNode.Width;
while (adjusted.Count >= 3 && Math.Abs(adjusted[^2].X - targetX) <= CoordinateTolerance)
{
adjusted.RemoveAt(adjusted.Count - 2);
}
var anchor = adjusted[^2];
var rebuilt = adjusted.Take(adjusted.Count - 1).ToList();
if (Math.Abs(anchor.X - targetX) <= CoordinateTolerance)
{
var offset = Math.Max(24d, minLineClearance / 2d);
rebuilt.Add(new ElkPoint
{
X = side == "left" ? targetX - offset : targetX + offset,
Y = anchor.Y,
});
anchor = rebuilt[^1];
}
if (Math.Abs(anchor.Y - slotCoord) > CoordinateTolerance)
{
rebuilt.Add(new ElkPoint { X = anchor.X, Y = slotCoord });
}
rebuilt.Add(new ElkPoint { X = targetX, Y = slotCoord });
return NormalizePath(rebuilt);
}
var targetY = side == "top"
? targetNode.Y
: targetNode.Y + targetNode.Height;
while (adjusted.Count >= 3 && Math.Abs(adjusted[^2].Y - targetY) <= CoordinateTolerance)
{
adjusted.RemoveAt(adjusted.Count - 2);
}
var verticalAnchor = adjusted[^2];
var verticalRebuilt = adjusted.Take(adjusted.Count - 1).ToList();
if (Math.Abs(verticalAnchor.Y - targetY) <= CoordinateTolerance)
{
var offset = Math.Max(24d, minLineClearance / 2d);
verticalRebuilt.Add(new ElkPoint
{
X = verticalAnchor.X,
Y = side == "top" ? targetY - offset : targetY + offset,
});
verticalAnchor = verticalRebuilt[^1];
}
if (Math.Abs(verticalAnchor.X - slotCoord) > CoordinateTolerance)
{
verticalRebuilt.Add(new ElkPoint { X = slotCoord, Y = verticalAnchor.Y });
}
verticalRebuilt.Add(new ElkPoint { X = slotCoord, Y = targetY });
return NormalizePath(verticalRebuilt);
}
private static List<ElkPoint> NormalizePath(IReadOnlyList<ElkPoint> path)
{
var deduped = new List<ElkPoint>();
foreach (var point in path)
{
if (deduped.Count == 0 || !ElkEdgeRoutingGeometry.PointsEqual(deduped[^1], point))
{
deduped.Add(point);
}
}
if (deduped.Count <= 2)
{
return deduped;
}
var simplified = new List<ElkPoint> { deduped[0] };
for (var i = 1; i < deduped.Count - 1; i++)
{
var previous = simplified[^1];
var current = deduped[i];
var next = deduped[i + 1];
var sameX = Math.Abs(previous.X - current.X) <= CoordinateTolerance
&& Math.Abs(current.X - next.X) <= CoordinateTolerance;
var sameY = Math.Abs(previous.Y - current.Y) <= CoordinateTolerance
&& Math.Abs(current.Y - next.Y) <= CoordinateTolerance;
if (!sameX && !sameY)
{
simplified.Add(current);
}
}
simplified.Add(deduped[^1]);
return simplified;
}
private static HighwayMember CreateMember(
ElkRoutedEdge edge,
int index,
ElkPositionedNode targetNode,
string side)
{
var path = ExtractFullPath(edge);
var endpointCoord = side is "left" or "right"
? path[^1].Y
: path[^1].X;
return new HighwayMember(
Index: index,
EdgeId: edge.Id,
Edge: edge,
Path: path,
PathLength: ElkEdgeRoutingGeometry.ComputePathLength(edge),
EndpointCoord: endpointCoord);
}
private static void WriteBackPath(ElkRoutedEdge[] result, int edgeIndex, IReadOnlyList<ElkPoint> path)
{
var edge = result[edgeIndex];
result[edgeIndex] = new ElkRoutedEdge
{
Id = edge.Id,
SourceNodeId = edge.SourceNodeId,
TargetNodeId = edge.TargetNodeId,
SourcePortId = edge.SourcePortId,
TargetPortId = edge.TargetPortId,
Kind = edge.Kind,
Label = edge.Label,
Sections =
[
new ElkEdgeSection
{
StartPoint = path[0],
EndPoint = path[^1],
BendPoints = path.Count > 2
? path.Skip(1).Take(path.Count - 2).ToArray()
: [],
},
],
};
}
private static List<ElkPoint> ExtractFullPath(ElkRoutedEdge edge)
{
var path = new List<ElkPoint>();
foreach (var section in edge.Sections)
{
if (path.Count == 0)
{
path.Add(section.StartPoint);
}
path.AddRange(section.BendPoints);
path.Add(section.EndPoint);
}
return path;
}
private readonly record struct HighwayMember(
int Index,
string EdgeId,
ElkRoutedEdge Edge,
List<ElkPoint> Path,
double PathLength,
double EndpointCoord);
private readonly record struct HighwayPairMetrics(
bool HasSharedSegment,
bool AllPairsApplicable,
double ShortestSharedRatio);
private readonly record struct GroupEvaluation(
IReadOnlyList<HighwayMember> Members,
ElkHighwayDiagnostics Diagnostic);
}

View File

@@ -0,0 +1,274 @@
namespace StellaOps.ElkSharp;
internal static partial class ElkEdgeRouterIterative
{
private static double ScoreProtectedCollectorGatewaySourceExitCandidate(
IReadOnlyList<ElkPoint> path,
ElkPositionedNode sourceNode,
ElkPoint exitReference)
{
var score = 0d;
if (path.Count < 2)
{
return double.PositiveInfinity;
}
var boundary = path[0];
var adjacent = path[1];
var centerX = sourceNode.X + (sourceNode.Width / 2d);
var centerY = sourceNode.Y + (sourceNode.Height / 2d);
var desiredDx = exitReference.X - centerX;
var desiredDy = exitReference.Y - centerY;
var boundaryDx = boundary.X - centerX;
var boundaryDy = boundary.Y - centerY;
var firstDx = adjacent.X - boundary.X;
var firstDy = adjacent.Y - boundary.Y;
const double tolerance = 0.5d;
var dominantHorizontal = Math.Abs(desiredDx) >= Math.Abs(desiredDy) * 1.15d && Math.Sign(desiredDx) != 0;
var dominantVertical = Math.Abs(desiredDy) >= Math.Abs(desiredDx) * 1.15d && Math.Sign(desiredDy) != 0;
if (dominantHorizontal)
{
if (Math.Sign(firstDx) != Math.Sign(desiredDx) || Math.Abs(firstDx) <= tolerance)
{
score += 100_000d;
}
if (Math.Abs(boundaryDy) > sourceNode.Height * 0.28d)
{
score += 25_000d;
}
score += Math.Abs(boundaryDy) * 6d;
}
else if (dominantVertical)
{
if (Math.Sign(firstDy) != Math.Sign(desiredDy) || Math.Abs(firstDy) <= tolerance)
{
score += 100_000d;
}
if (Math.Abs(boundaryDx) > sourceNode.Width * 0.28d)
{
score += 25_000d;
}
score += Math.Abs(boundaryDx) * 6d;
}
if (ElkShapeBoundaries.IsNearGatewayVertex(sourceNode, boundary, 8d))
{
score += 4_000d;
}
var length = 0d;
for (var i = 1; i < path.Count; i++)
{
length += ElkEdgeRoutingGeometry.ComputeSegmentLength(path[i - 1], path[i]);
}
return score
+ length
+ (Math.Max(0, path.Count - 2) * 6d);
}
private static List<ElkPoint> NormalizeProtectedCollectorTail(
IReadOnlyList<ElkPoint> path,
double graphMinY,
double graphMaxY)
{
if (path.Count < 5)
{
return path.Select(point => new ElkPoint { X = point.X, Y = point.Y }).ToList();
}
const double corridorTolerance = 8d;
const double coordinateTolerance = 0.5d;
var firstCorridorIndex = -1;
var lastCorridorIndex = -1;
for (var i = 0; i < path.Count; i++)
{
if (path[i].Y < graphMinY - corridorTolerance || path[i].Y > graphMaxY + corridorTolerance)
{
if (firstCorridorIndex < 0)
{
firstCorridorIndex = i;
}
lastCorridorIndex = i;
}
}
if (firstCorridorIndex < 0
|| lastCorridorIndex <= firstCorridorIndex
|| lastCorridorIndex >= path.Count - 1
|| path[firstCorridorIndex].Y > graphMinY - corridorTolerance)
{
return path.Select(point => new ElkPoint { X = point.X, Y = point.Y }).ToList();
}
var desiredDx = path[^1].X - path[0].X;
if (Math.Abs(desiredDx) <= coordinateTolerance)
{
return path.Select(point => new ElkPoint { X = point.X, Y = point.Y }).ToList();
}
var preCorridorHorizontalIndex = -1;
for (var i = 1; i < lastCorridorIndex; i++)
{
if (Math.Abs(path[i].Y - path[i + 1].Y) > coordinateTolerance
|| Math.Abs(path[i].X - path[i + 1].X) <= coordinateTolerance)
{
continue;
}
var horizontalDelta = path[i + 1].X - path[i].X;
if (Math.Sign(horizontalDelta) == 0 || Math.Sign(horizontalDelta) == Math.Sign(desiredDx))
{
continue;
}
preCorridorHorizontalIndex = i;
break;
}
if (preCorridorHorizontalIndex >= 0)
{
var rebuiltPrefix = path
.Take(preCorridorHorizontalIndex + 1)
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
.ToList();
var rewrittenCorridorY = path[lastCorridorIndex].Y;
var rewrittenReentryPoint = path[lastCorridorIndex + 1];
if (Math.Abs(rebuiltPrefix[^1].Y - rewrittenCorridorY) > coordinateTolerance)
{
rebuiltPrefix.Add(new ElkPoint
{
X = rebuiltPrefix[^1].X,
Y = rewrittenCorridorY,
});
}
if (Math.Abs(rebuiltPrefix[^1].X - rewrittenReentryPoint.X) > coordinateTolerance)
{
rebuiltPrefix.Add(new ElkPoint
{
X = rewrittenReentryPoint.X,
Y = rewrittenCorridorY,
});
}
for (var i = lastCorridorIndex + 1; i < path.Count; i++)
{
rebuiltPrefix.Add(new ElkPoint { X = path[i].X, Y = path[i].Y });
}
return NormalizeCollectorPoints(rebuiltPrefix);
}
var firstHorizontalIndex = -1;
for (var i = firstCorridorIndex; i < lastCorridorIndex; i++)
{
if (Math.Abs(path[i].Y - path[i + 1].Y) <= coordinateTolerance
&& Math.Abs(path[i].X - path[i + 1].X) > coordinateTolerance)
{
firstHorizontalIndex = i;
break;
}
}
if (firstHorizontalIndex < 0)
{
return path.Select(point => new ElkPoint { X = point.X, Y = point.Y }).ToList();
}
var firstHorizontalDelta = path[firstHorizontalIndex + 1].X - path[firstHorizontalIndex].X;
if (Math.Sign(firstHorizontalDelta) == 0 || Math.Sign(firstHorizontalDelta) == Math.Sign(desiredDx))
{
return path.Select(point => new ElkPoint { X = point.X, Y = point.Y }).ToList();
}
var rebuilt = path
.Take(firstCorridorIndex + 1)
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
.ToList();
var targetCorridorY = path[lastCorridorIndex].Y;
var reentryPoint = path[lastCorridorIndex + 1];
if (Math.Abs(rebuilt[^1].Y - targetCorridorY) > coordinateTolerance)
{
rebuilt.Add(new ElkPoint
{
X = rebuilt[^1].X,
Y = targetCorridorY,
});
}
if (Math.Abs(rebuilt[^1].X - reentryPoint.X) > coordinateTolerance)
{
rebuilt.Add(new ElkPoint
{
X = reentryPoint.X,
Y = targetCorridorY,
});
}
for (var i = lastCorridorIndex + 1; i < path.Count; i++)
{
rebuilt.Add(new ElkPoint { X = path[i].X, Y = path[i].Y });
}
return NormalizeCollectorPoints(rebuilt);
}
private static ElkPoint? BuildOrthogonalCollectorCorner(ElkPoint from, ElkPoint to)
{
const double tolerance = 0.5d;
if (Math.Abs(from.X - to.X) <= tolerance || Math.Abs(from.Y - to.Y) <= tolerance)
{
return null;
}
return Math.Abs(to.Y - from.Y) >= Math.Abs(to.X - from.X)
? new ElkPoint { X = from.X, Y = to.Y }
: new ElkPoint { X = to.X, Y = from.Y };
}
private static List<ElkPoint> NormalizeCollectorPoints(IReadOnlyList<ElkPoint> points)
{
const double coordinateTolerance = 0.5d;
var deduped = new List<ElkPoint>();
foreach (var point in points)
{
if (deduped.Count == 0 || !ElkEdgeRoutingGeometry.PointsEqual(deduped[^1], point))
{
deduped.Add(point);
}
}
if (deduped.Count <= 2)
{
return deduped;
}
var simplified = new List<ElkPoint> { deduped[0] };
for (var i = 1; i < deduped.Count - 1; i++)
{
var previous = simplified[^1];
var current = deduped[i];
var next = deduped[i + 1];
var sameX = Math.Abs(previous.X - current.X) <= coordinateTolerance
&& Math.Abs(current.X - next.X) <= coordinateTolerance;
var sameY = Math.Abs(previous.Y - current.Y) <= coordinateTolerance
&& Math.Abs(current.Y - next.Y) <= coordinateTolerance;
if (!sameX && !sameY)
{
simplified.Add(current);
}
}
simplified.Add(deduped[^1]);
return simplified;
}
}

View File

@@ -0,0 +1,220 @@
namespace StellaOps.ElkSharp;
internal static partial class ElkEdgeRouterIterative
{
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;
}
}

View File

@@ -0,0 +1,207 @@
namespace StellaOps.ElkSharp;
internal static partial class ElkEdgeRouterIterative
{
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);
}
}

View File

@@ -0,0 +1,49 @@
namespace StellaOps.ElkSharp;
internal static partial class ElkEdgeRouterIterative
{
private static ElkRoutedEdge[] ApplyLeanHybridBaselinePostProcessing(
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);
}
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.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 = ApplyHybridTerminalRuleCleanupRound(
result,
nodes,
layoutOptions.Direction,
minLineClearance);
result = ChoosePreferredBoundarySlotRepairLayout(
result,
ApplyPostSlotDetourClosure(result, nodes, minLineClearance),
nodes);
result = ChoosePreferredBoundarySlotRepairLayout(
result,
ElkEdgePostProcessor.SnapBoundarySlotAssignments(
result,
nodes,
minLineClearance,
enforceAllNodeEndpoints: true),
nodes);
return result;
}
}

View File

@@ -0,0 +1,236 @@
namespace StellaOps.ElkSharp;
internal static partial class ElkEdgeRouterIterative
{
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);
var maxRounds = restrictedSet switch
{
{ Count: > 0 and <= MaxLateRestabilizedClosureFocusEdges } => 1,
{ Count: > 0 } => 2,
_ => 4,
};
ElkLayoutDiagnostics.LogProgress(
$"Terminal closure start: restricted={restrictedEdgeIds?.Count ?? 0} rounds={maxRounds}");
for (var round = 0; round < maxRounds; 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;
}
}

View File

@@ -0,0 +1,39 @@
namespace StellaOps.ElkSharp;
internal static partial class ElkEdgeRouterIterative
{
private static ElkRoutedEdge[] ApplyHybridTerminalRuleCleanupRound(
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.SnapBoundarySlotAssignments(
result,
nodes,
minLineClearance,
restrictedEdgeIds,
enforceAllNodeEndpoints: true);
return result;
}
}

View File

@@ -0,0 +1,114 @@
namespace StellaOps.ElkSharp;
internal static partial class ElkEdgeRouterIterative
{
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;
}
}

View File

@@ -9,8 +9,14 @@ internal static partial class ElkEdgeRouterIterative
private static ElkRoutedEdge[] ApplyPostProcessing(
ElkRoutedEdge[] edges,
ElkPositionedNode[] nodes,
ElkLayoutOptions layoutOptions)
ElkLayoutOptions layoutOptions,
bool preferHybridTerminalCleanup = false)
{
if (preferHybridTerminalCleanup)
{
return ApplyLeanHybridBaselinePostProcessing(edges, nodes, layoutOptions);
}
var result = ElkEdgePostProcessor.AvoidNodeCrossings(edges, nodes, layoutOptions.Direction);
result = ElkEdgePostProcessor.EliminateDiagonalSegments(result, nodes);
result = ElkEdgePostProcessorSimplify.SimplifyEdgePaths(result, nodes);
@@ -143,11 +149,17 @@ internal static partial class ElkEdgeRouterIterative
var retryState = BuildRetryState(score, remainingBrokenHighways);
if (retryState.RequiresBlockingRetry || retryState.RequiresLengthRetry)
{
var stabilized = ApplyTerminalRuleCleanupRound(
result,
nodes,
layoutOptions.Direction,
minLineClearance);
var stabilized = preferHybridTerminalCleanup
? ApplyHybridTerminalRuleCleanupRound(
result,
nodes,
layoutOptions.Direction,
minLineClearance)
: ApplyTerminalRuleCleanupRound(
result,
nodes,
layoutOptions.Direction,
minLineClearance);
var stabilizedScore = ElkEdgeRoutingScoring.ComputeScore(stabilized, nodes);
var stabilizedBrokenHighways = HighwayProcessingEnabled
? ElkEdgeRouterHighway.DetectRemainingBrokenHighways(stabilized, nodes).Count
@@ -165,760 +177,4 @@ internal static partial class ElkEdgeRouterIterative
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;
}
}
}

View File

@@ -0,0 +1,153 @@
namespace StellaOps.ElkSharp;
internal static partial class ElkEdgeRouterIterative
{
private static List<ElkPoint> NormalizePolyline(IReadOnlyList<ElkPoint> points)
{
var result = new List<ElkPoint>(points.Count);
foreach (var point in points)
{
if (result.Count == 0 || !ElkEdgeRoutingGeometry.PointsEqual(result[^1], point))
{
result.Add(point);
}
}
if (result.Count < 3)
{
return result;
}
var collapsed = new List<ElkPoint> { result[0] };
for (var i = 1; i < result.Count - 1; i++)
{
var previous = collapsed[^1];
var current = result[i];
var next = result[i + 1];
var sameX = Math.Abs(previous.X - current.X) < 0.5d && Math.Abs(current.X - next.X) < 0.5d;
var sameY = Math.Abs(previous.Y - current.Y) < 0.5d && Math.Abs(current.Y - next.Y) < 0.5d;
if (!sameX && !sameY)
{
collapsed.Add(current);
}
}
collapsed.Add(result[^1]);
return collapsed;
}
private static void AddUniqueCoordinate(ICollection<double> coordinates, double candidate, double tolerance = 0.5d)
{
foreach (var coordinate in coordinates)
{
if (Math.Abs(coordinate - candidate) <= tolerance)
{
return;
}
}
coordinates.Add(candidate);
}
private static (double Left, double Top, double Right, double Bottom, string Id)[] BuildObstacles(
ElkPositionedNode[] nodes,
double margin)
{
return nodes.Select(node => (
Left: node.X - margin,
Top: node.Y - margin,
Right: node.X + node.Width + margin,
Bottom: node.Y + node.Height + margin,
Id: node.Id
)).ToArray();
}
private static ElkRoutedEdge[] ClampBelowGraphEdges(
ElkRoutedEdge[] edges,
ElkPositionedNode[] nodes,
IReadOnlyCollection<string>? restrictedEdgeIds = null)
{
if (edges.Length == 0 || nodes.Length == 0)
{
return edges;
}
var graphMaxY = nodes.Max(node => node.Y + node.Height);
var limitY = graphMaxY + 4d;
var obstacles = BuildObstacles(nodes, 0d);
var restrictedSet = restrictedEdgeIds is null
? null
: restrictedEdgeIds.ToHashSet(StringComparer.Ordinal);
var result = edges.ToArray();
for (var i = 0; i < result.Length; i++)
{
var edge = result[i];
if (restrictedSet is not null && !restrictedSet.Contains(edge.Id))
{
continue;
}
var path = ExtractPath(edge)
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
.ToList();
if (path.Count < 3)
{
continue;
}
var changed = false;
for (var pointIndex = 1; pointIndex < path.Count - 1; pointIndex++)
{
if (path[pointIndex].Y <= limitY)
{
continue;
}
path[pointIndex] = new ElkPoint
{
X = path[pointIndex].X,
Y = limitY,
};
changed = true;
}
if (!changed)
{
continue;
}
var normalized = NormalizePolyline(path);
var candidate = new ElkRoutedEdge
{
Id = edge.Id,
SourceNodeId = edge.SourceNodeId,
TargetNodeId = edge.TargetNodeId,
SourcePortId = edge.SourcePortId,
TargetPortId = edge.TargetPortId,
Kind = edge.Kind,
Label = edge.Label,
Sections =
[
new ElkEdgeSection
{
StartPoint = normalized[0],
EndPoint = normalized[^1],
BendPoints = normalized.Count > 2
? normalized.Skip(1).Take(normalized.Count - 2).ToArray()
: [],
},
],
};
if (EdgeCrossesNode(candidate, obstacles))
{
continue;
}
result[i] = candidate;
}
return result;
}
}

View File

@@ -0,0 +1,522 @@
namespace StellaOps.ElkSharp;
internal static partial class ElkEdgeRouterIterative
{
private readonly record struct HybridRepairBatch(
string[] EdgeIds,
string[] ConflictKeys);
private static CandidateSolution OptimizeHybrid(
ElkRoutedEdge[] baselineProcessed,
EdgeRoutingScore baselineProcessedScore,
RoutingRetryState baselineRetryState,
ElkPositionedNode[] nodes,
ElkLayoutOptions layoutOptions,
IterativeRoutingConfig config,
double minLineClearance,
CancellationToken cancellationToken)
{
var strategy = BuildHybridStrategy(baselineProcessed, nodes, baselineProcessedScore, baselineRetryState, minLineClearance);
var current = new CandidateSolution(baselineProcessedScore, baselineRetryState, baselineProcessed, 0);
var diagnostics = ElkLayoutDiagnostics.Current;
var attemptCounter = 0;
ElkIterativeStrategyDiagnostics? liveStrategyDiagnostics = null;
if (diagnostics is not null)
{
liveStrategyDiagnostics = new ElkIterativeStrategyDiagnostics
{
StrategyIndex = 1,
OrderingName = "hybrid",
Attempts = 0,
TotalDurationMs = 0d,
BestScore = baselineProcessedScore,
Outcome = $"baseline({DescribeRetryState(baselineRetryState)})",
RegisteredLive = true,
BestEdges = baselineProcessed,
};
lock (diagnostics.SyncRoot)
{
diagnostics.IterativeStrategies.Add(liveStrategyDiagnostics);
}
ElkLayoutDiagnostics.FlushSnapshot(diagnostics);
}
ElkLayoutDiagnostics.LogProgress(
$"Hybrid routing start: score={current.Score.Value:F0} retry={DescribeRetryState(current.RetryState)} waves={config.MaxRepairWaves}");
for (var wave = 0; wave < config.MaxRepairWaves; wave++)
{
cancellationToken.ThrowIfCancellationRequested();
if (!current.RetryState.RequiresPrimaryRetry && current.Score.EdgeCrossings == 0)
{
ElkLayoutDiagnostics.LogProgress($"Hybrid routing converged before wave {wave + 1}");
break;
}
var repairPlan = BuildRepairPlan(
current.Edges,
nodes,
current.Score,
current.RetryState,
strategy,
wave + 1);
if (repairPlan is null || repairPlan.Value.EdgeIds.Length == 0)
{
ElkLayoutDiagnostics.LogProgress($"Hybrid routing wave {wave + 1}: no repair plan");
break;
}
var batches = BuildHybridRepairBatches(current.Edges, nodes, repairPlan.Value);
if (batches.Count == 0)
{
ElkLayoutDiagnostics.LogProgress($"Hybrid routing wave {wave + 1}: no independent repair batches");
break;
}
var waveImproved = false;
for (var batchIndex = 0; batchIndex < batches.Count; batchIndex++)
{
cancellationToken.ThrowIfCancellationRequested();
var batch = batches[batchIndex];
var batchPlan = BuildHybridBatchPlan(repairPlan.Value, batch.EdgeIds);
if (batchPlan.EdgeIds.Length == 0)
{
continue;
}
ElkLayoutDiagnostics.LogProgress(
$"Hybrid routing wave {wave + 1} batch {batchIndex + 1}/{batches.Count}: " +
$"edges=[{string.Join(", ", batchPlan.EdgeIds)}] keys=[{string.Join(", ", batch.ConflictKeys)}]");
var attemptStopwatch = System.Diagnostics.Stopwatch.StartNew();
if (!TryApplyHybridRepairBatch(
current,
nodes,
layoutOptions.Direction,
config,
strategy,
batchPlan,
cancellationToken,
out var promoted,
out var attempted,
out var routeDiagnostics))
{
attemptStopwatch.Stop();
attemptCounter++;
RecordHybridAttempt(
diagnostics,
liveStrategyDiagnostics,
attemptCounter,
attemptStopwatch.Elapsed.TotalMilliseconds,
attempted.Score,
BuildHybridAttemptOutcome(attempted.RetryState, improved: false),
routeDiagnostics,
attempted.Edges);
continue;
}
attemptStopwatch.Stop();
attemptCounter++;
RecordHybridAttempt(
diagnostics,
liveStrategyDiagnostics,
attemptCounter,
attemptStopwatch.Elapsed.TotalMilliseconds,
attempted.Score,
BuildHybridAttemptOutcome(attempted.RetryState, improved: true),
routeDiagnostics,
attempted.Edges);
current = promoted;
waveImproved = true;
}
if (waveImproved)
{
ElkLayoutDiagnostics.LogProgress(
$"Hybrid routing wave {wave + 1} improved: score={current.Score.Value:F0} retry={DescribeRetryState(current.RetryState)}");
continue;
}
if (!current.RetryState.RequiresPrimaryRetry && current.Score.EdgeCrossings == 0)
{
break;
}
ElkLayoutDiagnostics.LogProgress(
$"Hybrid routing wave {wave + 1} stalled; adapting strategy for retry={DescribeRetryState(current.RetryState)}");
strategy.AdaptForViolations(current.Score, wave, current.RetryState);
}
if (config.MaxRepairWaves <= 1
&& (current.RetryState.RequiresPrimaryRetry || current.Score.EdgeCrossings > 0))
{
var focusedRepairStopwatch = System.Diagnostics.Stopwatch.StartNew();
var focusedRepair = TryApplyVerifiedIssueRepairRound(
current.Edges,
nodes,
config.ObstacleMargin,
strategy,
current.RetryState,
layoutOptions.Direction,
cancellationToken,
config.MaxParallelRepairBuilds,
trustIndependentParallelBuilds: true,
useHybridCleanup: true);
focusedRepairStopwatch.Stop();
if (focusedRepair is { } repaired)
{
var improved = IsBetterBoundarySlotRepairCandidate(
repaired.Score,
repaired.RetryState,
current.Score,
current.RetryState);
attemptCounter++;
RecordHybridAttempt(
diagnostics,
liveStrategyDiagnostics,
attemptCounter,
focusedRepairStopwatch.Elapsed.TotalMilliseconds,
repaired.Score,
BuildHybridAttemptOutcome(repaired.RetryState, improved),
repaired.RouteDiagnostics,
repaired.Edges);
if (improved)
{
current = current with
{
Score = repaired.Score,
RetryState = repaired.RetryState,
Edges = repaired.Edges,
};
ElkLayoutDiagnostics.LogProgress(
$"Hybrid focused post-wave repair improved: score={current.Score.Value:F0} retry={DescribeRetryState(current.RetryState)}");
}
}
}
current = RefineHybridWinningSolution(
current,
nodes,
layoutOptions.Direction,
minLineClearance,
preferLowWaveRuntimePolish: config.MaxRepairWaves <= 2);
if (liveStrategyDiagnostics is not null)
{
lock (diagnostics!.SyncRoot)
{
liveStrategyDiagnostics.Attempts = attemptCounter;
liveStrategyDiagnostics.BestScore = current.Score;
liveStrategyDiagnostics.BestEdges = current.Edges;
liveStrategyDiagnostics.Outcome = current.RetryState.RequiresPrimaryRetry
? $"retry({DescribeRetryState(current.RetryState)})"
: "valid";
}
ElkLayoutDiagnostics.FlushSnapshot(diagnostics!);
}
ElkLayoutDiagnostics.LogProgress(
$"Hybrid routing complete: score={current.Score.Value:F0} retry={DescribeRetryState(current.RetryState)}");
return current;
}
private static RoutingStrategy BuildHybridStrategy(
ElkRoutedEdge[] edges,
ElkPositionedNode[] nodes,
EdgeRoutingScore baselineScore,
RoutingRetryState baselineRetryState,
double minLineClearance)
{
var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal);
var connectionCount = new Dictionary<string, int>(StringComparer.Ordinal);
foreach (var edge in edges)
{
var sourceId = edge.SourceNodeId ?? string.Empty;
var targetId = edge.TargetNodeId ?? string.Empty;
connectionCount[sourceId] = connectionCount.GetValueOrDefault(sourceId) + 1;
connectionCount[targetId] = connectionCount.GetValueOrDefault(targetId) + 1;
}
var useConnectedOrdering = baselineRetryState.RequiresBlockingRetry
|| baselineRetryState.SharedLaneViolations > 0
|| baselineRetryState.TargetApproachJoinViolations > 0;
var edgeOrder = useConnectedOrdering
? OrderByMostConnectedFirst(edges, connectionCount)
: OrderByLongestFirst(edges, nodesById);
var routingParams = baselineRetryState.RequiresBlockingRetry
? new AStarRoutingParams(18d, 400d, 600d, 3.0d, minLineClearance, 40d, true)
: new AStarRoutingParams(18d, 200d, 500d, 2.0d, minLineClearance, 40d, true);
var strategy = new RoutingStrategy
{
EdgeOrder = edgeOrder,
BaseLineClearance = minLineClearance,
MinLineClearance = minLineClearance,
RoutingParams = routingParams,
};
if (baselineRetryState.RequiresPrimaryRetry || baselineScore.EdgeCrossings > 0)
{
strategy.AdaptForViolations(baselineScore, 0, baselineRetryState);
}
return strategy;
}
private static IReadOnlyList<HybridRepairBatch> BuildHybridRepairBatches(
IReadOnlyCollection<ElkRoutedEdge> edges,
IReadOnlyCollection<ElkPositionedNode> nodes,
RepairPlan repairPlan)
{
var edgesById = edges.ToDictionary(edge => edge.Id, StringComparer.Ordinal);
var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal);
var orderedBatches = new List<(List<string> EdgeIds, HashSet<string> ConflictKeys)>();
foreach (var edgeId in repairPlan.EdgeIds)
{
if (!edgesById.TryGetValue(edgeId, out var edge))
{
continue;
}
var conflictKeys = GetHybridConflictKeys(edge, nodesById);
var assigned = false;
foreach (var batch in orderedBatches)
{
if (batch.ConflictKeys.Overlaps(conflictKeys))
{
continue;
}
batch.EdgeIds.Add(edgeId);
batch.ConflictKeys.UnionWith(conflictKeys);
assigned = true;
break;
}
if (assigned)
{
continue;
}
orderedBatches.Add((
EdgeIds: [edgeId],
ConflictKeys: new HashSet<string>(conflictKeys, StringComparer.Ordinal)));
}
return orderedBatches
.Select(batch => new HybridRepairBatch(
batch.EdgeIds.ToArray(),
batch.ConflictKeys.OrderBy(key => key, StringComparer.Ordinal).ToArray()))
.ToArray();
}
private static string[] GetHybridConflictKeys(
ElkRoutedEdge edge,
IReadOnlyDictionary<string, ElkPositionedNode> nodesById)
{
var keys = new HashSet<string>(StringComparer.Ordinal);
var sourceId = edge.SourceNodeId ?? string.Empty;
var targetId = edge.TargetNodeId ?? string.Empty;
if (!string.IsNullOrEmpty(sourceId))
{
keys.Add($"source:{sourceId}");
}
if (!string.IsNullOrEmpty(targetId))
{
keys.Add($"target:{targetId}");
}
if (!string.IsNullOrEmpty(sourceId) && !string.IsNullOrEmpty(targetId))
{
keys.Add($"pair:{sourceId}->{targetId}");
}
var path = ExtractPath(edge);
if (path.Count >= 2
&& nodesById.TryGetValue(sourceId, out var sourceNode))
{
var sourceSide = ElkEdgeRoutingGeometry.ResolveBoundaryApproachSide(path[0], path[1], sourceNode);
keys.Add($"source-side:{sourceId}:{sourceSide}");
if (ElkShapeBoundaries.IsGatewayShape(sourceNode))
{
keys.Add($"gateway-source:{sourceId}");
}
}
if (path.Count >= 2
&& nodesById.TryGetValue(targetId, out var targetNode))
{
var previousPoint = path[^2];
var targetSide = ResolveEntrySide(path[^1], previousPoint, targetNode);
keys.Add($"target-side:{targetId}:{targetSide}");
if (ElkShapeBoundaries.IsGatewayShape(targetNode))
{
keys.Add($"gateway-target:{targetId}");
}
}
if (ElkEdgePostProcessor.IsRepeatCollectorLabel(edge.Label))
{
keys.Add($"collector:{sourceId}:{targetId}");
}
return keys.OrderBy(key => key, StringComparer.Ordinal).ToArray();
}
private static RepairPlan BuildHybridBatchPlan(RepairPlan repairPlan, IReadOnlyCollection<string> batchEdgeIds)
{
var batchEdgeSet = batchEdgeIds.ToHashSet(StringComparer.Ordinal);
var edgeIds = repairPlan.EdgeIds.Where(batchEdgeSet.Contains).ToArray();
var preferredShortestEdgeIds = repairPlan.PreferredShortestEdgeIds.Where(batchEdgeSet.Contains).ToArray();
var routeRepairEdgeIds = repairPlan.RouteRepairEdgeIds.Where(batchEdgeSet.Contains).ToArray();
var edgeIndexById = new Dictionary<string, int>(StringComparer.Ordinal);
for (var i = 0; i < repairPlan.EdgeIds.Length && i < repairPlan.EdgeIndices.Length; i++)
{
edgeIndexById[repairPlan.EdgeIds[i]] = repairPlan.EdgeIndices[i];
}
var edgeIndices = edgeIds
.Select(edgeId => edgeIndexById.GetValueOrDefault(edgeId, -1))
.Where(index => index >= 0)
.ToArray();
return new RepairPlan(
edgeIndices,
edgeIds,
preferredShortestEdgeIds,
routeRepairEdgeIds,
repairPlan.Reasons);
}
private static bool TryApplyHybridRepairBatch(
CandidateSolution current,
ElkPositionedNode[] nodes,
ElkLayoutDirection direction,
IterativeRoutingConfig config,
RoutingStrategy strategy,
RepairPlan repairPlan,
CancellationToken cancellationToken,
out CandidateSolution promoted,
out CandidateSolution attempted,
out ElkIterativeRouteDiagnostics routeDiagnostics)
{
promoted = current;
attempted = current;
var restrictedEdgeIds = repairPlan.EdgeIds;
var routed = RepairPenalizedEdges(
current.Edges,
nodes,
config.ObstacleMargin,
strategy,
repairPlan,
cancellationToken,
config.MaxParallelRepairBuilds,
trustIndependentParallelBuilds: true);
routeDiagnostics = routed.Diagnostics;
var candidateEdges = ApplyHybridTerminalRuleCleanupRound(
routed.Edges,
nodes,
direction,
strategy.MinLineClearance,
restrictedEdgeIds);
candidateEdges = ChoosePreferredHardRuleLayout(current.Edges, candidateEdges, nodes);
var candidateScore = ElkEdgeRoutingScoring.ComputeScore(candidateEdges, nodes);
var remainingBrokenHighways = HighwayProcessingEnabled
? ElkEdgeRouterHighway.DetectRemainingBrokenHighways(candidateEdges, nodes).Count
: 0;
var candidateRetryState = BuildRetryState(candidateScore, remainingBrokenHighways);
if (candidateRetryState.RequiresBlockingRetry || candidateRetryState.RequiresLengthRetry)
{
var focusedRepair = TryApplyVerifiedIssueRepairRound(
candidateEdges,
nodes,
config.ObstacleMargin,
strategy,
candidateRetryState,
direction,
cancellationToken,
config.MaxParallelRepairBuilds,
trustIndependentParallelBuilds: true,
useHybridCleanup: true);
if (focusedRepair is { } repaired
&& IsBetterBoundarySlotRepairCandidate(
repaired.Score,
repaired.RetryState,
candidateScore,
candidateRetryState))
{
candidateEdges = repaired.Edges;
candidateScore = repaired.Score;
remainingBrokenHighways = repaired.RemainingBrokenHighways;
candidateRetryState = repaired.RetryState;
}
}
attempted = current with
{
Score = candidateScore,
RetryState = candidateRetryState,
Edges = candidateEdges,
};
if (!IsBetterBoundarySlotRepairCandidate(
candidateScore,
candidateRetryState,
current.Score,
current.RetryState))
{
return false;
}
promoted = attempted;
return true;
}
private static void RecordHybridAttempt(
ElkLayoutRunDiagnostics? diagnostics,
ElkIterativeStrategyDiagnostics? liveStrategyDiagnostics,
int attempt,
double durationMs,
EdgeRoutingScore score,
string outcome,
ElkIterativeRouteDiagnostics routeDiagnostics,
ElkRoutedEdge[] edges)
{
if (diagnostics is null || liveStrategyDiagnostics is null)
{
return;
}
var attemptDiagnostics = new ElkIterativeAttemptDiagnostics
{
Attempt = attempt,
TotalDurationMs = Math.Round(durationMs, 3),
Score = score,
Outcome = outcome,
RouteDiagnostics = routeDiagnostics,
Edges = edges,
};
lock (diagnostics.SyncRoot)
{
liveStrategyDiagnostics.Attempts = attempt;
liveStrategyDiagnostics.BestScore = score;
liveStrategyDiagnostics.AttemptDetails.Add(attemptDiagnostics);
}
ElkLayoutDiagnostics.FlushSnapshot(diagnostics);
}
private static string BuildHybridAttemptOutcome(RoutingRetryState retryState, bool improved)
{
if (!retryState.RequiresPrimaryRetry)
{
return improved ? "valid" : "rejected-valid";
}
var retry = $"retry({DescribeRetryState(retryState)})";
return improved ? retry : $"rejected-{retry}";
}
}

View File

@@ -0,0 +1,303 @@
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Globalization;
namespace StellaOps.ElkSharp;
internal static partial class ElkEdgeRouterIterative
{
private static bool HasTargetApproachBacktracking(
IReadOnlyList<ElkPoint> path,
ElkPositionedNode targetNode)
{
if (path.Count < 3)
{
return false;
}
if (ElkShapeBoundaries.IsGatewayShape(targetNode))
{
return HasGatewayTargetApproachBacktracking(path, targetNode);
}
var side = ElkEdgeRoutingGeometry.ResolveBoundarySide(path[^1], targetNode);
if (side is not "left" and not "right" and not "top" and not "bottom")
{
return false;
}
const double tolerance = 0.5d;
if (HasShortOrthogonalTargetHook(path, targetNode, side, tolerance))
{
return true;
}
var startIndex = Math.Max(
0,
path.Count - (side is "left" or "right" ? 4 : 3));
var axisValues = new List<double>(path.Count - startIndex);
for (var i = startIndex; i < path.Count; i++)
{
var value = side is "left" or "right"
? path[i].X
: path[i].Y;
if (axisValues.Count == 0 || Math.Abs(axisValues[^1] - value) > tolerance)
{
axisValues.Add(value);
}
}
if (axisValues.Count < 3)
{
return false;
}
var targetAxis = side switch
{
"left" => targetNode.X,
"right" => targetNode.X + targetNode.Width,
"top" => targetNode.Y,
"bottom" => targetNode.Y + targetNode.Height,
_ => double.NaN,
};
var overshootsTargetSide = side switch
{
"left" or "top" => axisValues.Any(value => value > targetAxis + tolerance),
"right" or "bottom" => axisValues.Any(value => value < targetAxis - tolerance),
_ => false,
};
if (overshootsTargetSide)
{
return true;
}
var expectsIncreasing = side is "left" or "top";
var sawProgress = false;
for (var i = 1; i < axisValues.Count; i++)
{
var delta = axisValues[i] - axisValues[i - 1];
if (Math.Abs(delta) <= tolerance)
{
continue;
}
if (expectsIncreasing)
{
if (delta > tolerance)
{
sawProgress = true;
}
else if (sawProgress)
{
return true;
}
}
else
{
if (delta < -tolerance)
{
sawProgress = true;
}
else if (sawProgress)
{
return true;
}
}
}
return false;
}
private static bool HasGatewayTargetApproachBacktracking(
IReadOnlyList<ElkPoint> path,
ElkPositionedNode targetNode)
{
if (path.Count < 4)
{
return false;
}
if (HasShortGatewayTargetOrthogonalHook(path, targetNode))
{
return true;
}
const double tolerance = 0.5d;
var startIndex = Math.Max(0, path.Count - 4);
var nearEnd = path.Skip(startIndex).ToArray();
if (nearEnd.Length < 3)
{
return false;
}
var orthStart = nearEnd[^2];
var orthPrev = nearEnd.Length >= 4
? nearEnd[^3]
: nearEnd[0];
var horizontalApproach = Math.Abs(orthPrev.Y - orthStart.Y) <= tolerance
&& Math.Abs(orthPrev.X - orthStart.X) > tolerance;
var verticalApproach = Math.Abs(orthPrev.X - orthStart.X) <= tolerance
&& Math.Abs(orthPrev.Y - orthStart.Y) > tolerance;
if (!horizontalApproach && !verticalApproach)
{
return false;
}
var axisValues = new List<double>(nearEnd.Length);
foreach (var point in nearEnd)
{
var value = horizontalApproach
? point.X
: point.Y;
if (axisValues.Count == 0 || Math.Abs(axisValues[^1] - value) > tolerance)
{
axisValues.Add(value);
}
}
if (axisValues.Count < 3)
{
return false;
}
var targetAxis = horizontalApproach
? nearEnd[^1].X
: nearEnd[^1].Y;
var previousDistance = Math.Abs(axisValues[0] - targetAxis);
var sawProgress = false;
for (var i = 1; i < axisValues.Count; i++)
{
var currentDistance = Math.Abs(axisValues[i] - targetAxis);
if (currentDistance + tolerance < previousDistance)
{
sawProgress = true;
}
else if (sawProgress && currentDistance > previousDistance + tolerance)
{
return true;
}
previousDistance = currentDistance;
}
return false;
}
private static bool HasShortGatewayTargetOrthogonalHook(
IReadOnlyList<ElkPoint> path,
ElkPositionedNode targetNode)
{
if (path.Count < 3)
{
return false;
}
const double tolerance = 0.5d;
var boundaryPoint = path[^1];
var exteriorPoint = path[^2];
var finalDx = Math.Abs(boundaryPoint.X - exteriorPoint.X);
var finalDy = Math.Abs(boundaryPoint.Y - exteriorPoint.Y);
var finalIsHorizontal = finalDx > tolerance && finalDy <= tolerance;
var finalIsVertical = finalDy > tolerance && finalDx <= tolerance;
if (!finalIsHorizontal && !finalIsVertical)
{
return false;
}
var finalStubLength = finalIsHorizontal ? finalDx : finalDy;
var requiredDepth = Math.Min(targetNode.Width, targetNode.Height);
if (finalStubLength + tolerance >= requiredDepth)
{
return false;
}
var predecessor = path[^3];
var predecessorDx = Math.Abs(exteriorPoint.X - predecessor.X);
var predecessorDy = Math.Abs(exteriorPoint.Y - predecessor.Y);
const double minimumApproachSpan = 24d;
return finalIsHorizontal
? predecessorDy >= minimumApproachSpan && predecessorDy > predecessorDx * 3d
: predecessorDx >= minimumApproachSpan && predecessorDx > predecessorDy * 3d;
}
private static bool HasShortOrthogonalTargetHook(
IReadOnlyList<ElkPoint> path,
ElkPositionedNode targetNode,
string side,
double tolerance)
{
if (path.Count < 3)
{
return false;
}
var boundaryPoint = path[^1];
var runStartIndex = path.Count - 2;
if (side is "left" or "right")
{
while (runStartIndex > 0 && Math.Abs(path[runStartIndex - 1].Y - boundaryPoint.Y) <= tolerance)
{
runStartIndex--;
}
}
else
{
while (runStartIndex > 0 && Math.Abs(path[runStartIndex - 1].X - boundaryPoint.X) <= tolerance)
{
runStartIndex--;
}
}
if (runStartIndex == 0)
{
return false;
}
var overallDeltaX = path[^1].X - path[0].X;
var overallDeltaY = path[^1].Y - path[0].Y;
var overallAbsDx = Math.Abs(overallDeltaX);
var overallAbsDy = Math.Abs(overallDeltaY);
var sameRowThreshold = Math.Max(24d, targetNode.Height / 3d);
var sameColumnThreshold = Math.Max(24d, targetNode.Width / 3d);
var looksHorizontal = overallAbsDx >= overallAbsDy * 1.15d
&& overallAbsDy <= sameRowThreshold
&& Math.Sign(overallDeltaX) != 0;
var looksVertical = overallAbsDy >= overallAbsDx * 1.15d
&& overallAbsDx <= sameColumnThreshold
&& Math.Sign(overallDeltaY) != 0;
var contradictsDominantApproach = side switch
{
"left" or "right" => looksVertical,
"top" or "bottom" => looksHorizontal,
_ => false,
};
if (!contradictsDominantApproach)
{
return false;
}
var runStart = path[runStartIndex];
var boundaryDepth = side is "left" or "right"
? Math.Abs(boundaryPoint.X - runStart.X)
: Math.Abs(boundaryPoint.Y - runStart.Y);
var requiredDepth = side is "left" or "right"
? targetNode.Width
: targetNode.Height;
if (boundaryDepth + tolerance >= requiredDepth)
{
return false;
}
var predecessor = path[runStartIndex - 1];
var predecessorDx = Math.Abs(runStart.X - predecessor.X);
var predecessorDy = Math.Abs(runStart.Y - predecessor.Y);
return side switch
{
"left" or "right" => predecessorDy > predecessorDx * 3d,
"top" or "bottom" => predecessorDx > predecessorDy * 3d,
_ => false,
};
}
}

View File

@@ -0,0 +1,278 @@
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Globalization;
namespace StellaOps.ElkSharp;
internal static partial class ElkEdgeRouterIterative
{
private static ElkRoutedEdge[] RestoreProtectedRepeatCollectorCorridors(
ElkRoutedEdge[] candidateEdges,
ElkRoutedEdge[] referenceEdges,
ElkPositionedNode[] nodes)
{
if (candidateEdges.Length == 0 || referenceEdges.Length == 0 || nodes.Length == 0)
{
return candidateEdges;
}
var graphMinY = nodes.Min(node => node.Y);
var graphMaxY = nodes.Max(node => node.Y + node.Height);
var minLineClearance = ResolveMinLineClearance(nodes);
var obstacles = BuildObstacles(nodes, 0d);
var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal);
var restoredIds = new List<string>();
var result = candidateEdges.ToArray();
for (var i = 0; i < result.Length && i < referenceEdges.Length; i++)
{
var reference = referenceEdges[i];
if (!ElkEdgePostProcessor.IsRepeatCollectorLabel(reference.Label)
|| !ElkEdgePostProcessor.HasCorridorBendPoints(reference, graphMinY, graphMaxY)
|| ElkEdgePostProcessor.HasCorridorBendPoints(result[i], graphMinY, graphMaxY)
|| EdgeCrossesNode(reference, obstacles)
|| EdgeViolatesNodeClearance(reference, nodes, minLineClearance))
{
continue;
}
var restored = reference;
if (nodesById.TryGetValue(reference.SourceNodeId ?? string.Empty, out var sourceNode)
&& ElkShapeBoundaries.IsGatewayShape(sourceNode))
{
restored = AlignProtectedCollectorGatewaySourceExit(restored, sourceNode, graphMinY, graphMaxY);
}
result[i] = restored;
restoredIds.Add(restored.Id);
}
return restoredIds.Count > 0
? ElkRepeatCollectorCorridors.SeparateSharedLanes(result, nodes, restoredIds)
: result;
}
private static bool EdgeViolatesNodeClearance(
ElkRoutedEdge edge,
IReadOnlyCollection<ElkPositionedNode> nodes,
double minLineClearance)
{
if (minLineClearance <= 0d)
{
return false;
}
foreach (var segment in ElkEdgeRoutingGeometry.FlattenSegments(edge))
{
var horizontal = Math.Abs(segment.Start.Y - segment.End.Y) <= 0.5d;
var vertical = Math.Abs(segment.Start.X - segment.End.X) <= 0.5d;
if (!horizontal && !vertical)
{
continue;
}
foreach (var node in nodes)
{
if (node.Id == edge.SourceNodeId || node.Id == edge.TargetNodeId)
{
continue;
}
if (horizontal)
{
var minX = Math.Min(segment.Start.X, segment.End.X);
var maxX = Math.Max(segment.Start.X, segment.End.X);
if (maxX <= node.X || minX >= node.X + node.Width)
{
continue;
}
var distance = Math.Min(
Math.Abs(segment.Start.Y - node.Y),
Math.Abs(segment.Start.Y - (node.Y + node.Height)));
if (distance > 0.5d && distance < minLineClearance)
{
return true;
}
continue;
}
var minY = Math.Min(segment.Start.Y, segment.End.Y);
var maxY = Math.Max(segment.Start.Y, segment.End.Y);
if (maxY <= node.Y || minY >= node.Y + node.Height)
{
continue;
}
var horizontalDistance = Math.Min(
Math.Abs(segment.Start.X - node.X),
Math.Abs(segment.Start.X - (node.X + node.Width)));
if (horizontalDistance > 0.5d && horizontalDistance < minLineClearance)
{
return true;
}
}
}
return false;
}
private static bool EdgeCrossesNode(
ElkRoutedEdge edge,
(double Left, double Top, double Right, double Bottom, string Id)[] obstacles)
{
foreach (var segment in ElkEdgeRoutingGeometry.FlattenSegments(edge))
{
if (ElkEdgePostProcessor.SegmentCrossesObstacle(
segment.Start,
segment.End,
obstacles,
edge.SourceNodeId,
edge.TargetNodeId))
{
return true;
}
}
return false;
}
private static ElkRoutedEdge AlignProtectedCollectorGatewaySourceExit(
ElkRoutedEdge edge,
ElkPositionedNode sourceNode,
double graphMinY,
double graphMaxY)
{
var path = new List<ElkPoint>();
foreach (var section in edge.Sections)
{
if (path.Count == 0)
{
path.Add(section.StartPoint);
}
path.AddRange(section.BendPoints);
path.Add(section.EndPoint);
}
if (path.Count < 2)
{
return edge;
}
var corridorIndex = 1;
for (var i = 1; i < path.Count; i++)
{
if (path[i].Y < graphMinY - 8d || path[i].Y > graphMaxY + 8d)
{
corridorIndex = i;
break;
}
}
var corridorPoint = path[corridorIndex];
var boundaryReferences = sourceNode.Kind == "Decision"
? new[]
{
(BoundaryReference: path[^1], ExitReference: path[^1]),
(BoundaryReference: corridorPoint, ExitReference: corridorPoint),
(BoundaryReference: corridorPoint, ExitReference: path[^1]),
}
: new[]
{
(BoundaryReference: corridorPoint, ExitReference: corridorPoint),
};
List<ElkPoint>? rebuilt = null;
var bestScore = double.PositiveInfinity;
foreach (var (boundaryReference, exitReference) in boundaryReferences)
{
var boundary = ElkShapeBoundaries.ProjectOntoShapeBoundary(sourceNode, boundaryReference);
boundary = ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(sourceNode, boundary, boundaryReference);
var exteriorApproach = ElkShapeBoundaries.BuildPreferredGatewaySourceExteriorPoint(sourceNode, boundary, exitReference);
var desiredExitDx = exitReference.X - boundary.X;
var desiredExitDy = exitReference.Y - boundary.Y;
if (Math.Abs(desiredExitDx) >= Math.Abs(desiredExitDy) * 1.15d && Math.Sign(desiredExitDx) != 0)
{
exteriorApproach = new ElkPoint
{
X = desiredExitDx > 0d
? sourceNode.X + sourceNode.Width + 8d
: sourceNode.X - 8d,
Y = boundary.Y,
};
}
else if (Math.Abs(desiredExitDy) >= Math.Abs(desiredExitDx) * 1.15d && Math.Sign(desiredExitDy) != 0)
{
exteriorApproach = new ElkPoint
{
X = boundary.X,
Y = desiredExitDy > 0d
? sourceNode.Y + sourceNode.Height + 8d
: sourceNode.Y - 8d,
};
}
var candidate = new List<ElkPoint> { boundary };
if (!ElkEdgeRoutingGeometry.PointsEqual(boundary, exteriorApproach))
{
candidate.Add(exteriorApproach);
}
if (!ElkEdgeRoutingGeometry.PointsEqual(candidate[^1], corridorPoint))
{
var corner = BuildOrthogonalCollectorCorner(candidate[^1], corridorPoint);
if (corner is not null && !ElkEdgeRoutingGeometry.PointsEqual(candidate[^1], corner))
{
candidate.Add(corner);
}
if (!ElkEdgeRoutingGeometry.PointsEqual(candidate[^1], corridorPoint))
{
candidate.Add(corridorPoint);
}
}
for (var i = corridorIndex + 1; i < path.Count; i++)
{
candidate.Add(path[i]);
}
candidate = NormalizeProtectedCollectorTail(candidate, graphMinY, graphMaxY);
var score = ScoreProtectedCollectorGatewaySourceExitCandidate(candidate, sourceNode, exitReference);
if (score >= bestScore)
{
continue;
}
bestScore = score;
rebuilt = candidate;
}
rebuilt ??= NormalizeProtectedCollectorTail(path, graphMinY, graphMaxY);
return new ElkRoutedEdge
{
Id = edge.Id,
SourceNodeId = edge.SourceNodeId,
TargetNodeId = edge.TargetNodeId,
SourcePortId = edge.SourcePortId,
TargetPortId = edge.TargetPortId,
Kind = edge.Kind,
Label = edge.Label,
Sections =
[
new ElkEdgeSection
{
StartPoint = rebuilt[0],
EndPoint = rebuilt[^1],
BendPoints = rebuilt.Count > 2
? rebuilt.Skip(1).Take(rebuilt.Count - 2).ToArray()
: [],
},
],
};
}
}

View File

@@ -0,0 +1,270 @@
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Globalization;
namespace StellaOps.ElkSharp;
internal static partial class ElkEdgeRouterIterative
{
private static bool CanRepairEdgeLocally(
ElkRoutedEdge edge,
IReadOnlyCollection<ElkPositionedNode> nodes,
double graphMinY,
double graphMaxY)
{
if (ShouldRouteEdge(edge, graphMinY, graphMaxY))
{
return true;
}
if (!string.IsNullOrWhiteSpace(edge.SourcePortId) || !string.IsNullOrWhiteSpace(edge.TargetPortId))
{
return false;
}
if (!string.IsNullOrWhiteSpace(edge.Kind)
&& edge.Kind.StartsWith("backward|", StringComparison.OrdinalIgnoreCase))
{
return false;
}
if (ElkEdgePostProcessor.IsRepeatCollectorLabel(edge.Label))
{
return false;
}
return ElkEdgePostProcessor.HasCorridorBendPoints(edge, graphMinY, graphMaxY)
&& HasClearOrthogonalShortcut(edge, nodes);
}
private static bool CanRouteSelectedRepairEdge(
ElkRoutedEdge edge,
double graphMinY,
double graphMaxY,
IReadOnlySet<string> routeRepairEdgeIds)
{
if (ShouldRouteEdge(edge, graphMinY, graphMaxY))
{
return true;
}
if (!routeRepairEdgeIds.Contains(edge.Id))
{
return false;
}
if (!string.IsNullOrWhiteSpace(edge.SourcePortId) || !string.IsNullOrWhiteSpace(edge.TargetPortId))
{
return false;
}
if (!string.IsNullOrWhiteSpace(edge.Kind)
&& edge.Kind.StartsWith("backward|", StringComparison.OrdinalIgnoreCase))
{
return false;
}
return ElkEdgePostProcessor.IsRepeatCollectorLabel(edge.Label)
&& !ElkEdgePostProcessor.HasCorridorBendPoints(edge, graphMinY, graphMaxY);
}
private static bool HasClearOrthogonalShortcut(
ElkRoutedEdge edge,
IReadOnlyCollection<ElkPositionedNode> nodes)
{
var firstSection = edge.Sections.FirstOrDefault();
var lastSection = edge.Sections.LastOrDefault();
if (firstSection is null || lastSection is null)
{
return false;
}
var start = firstSection.StartPoint;
var end = lastSection.EndPoint;
var obstacles = nodes.Select(node => (
Left: node.X,
Top: node.Y,
Right: node.X + node.Width,
Bottom: node.Y + node.Height,
Id: node.Id)).ToArray();
bool SegmentIsClear(ElkPoint from, ElkPoint to) =>
!ElkEdgePostProcessor.SegmentCrossesObstacle(
from,
to,
obstacles,
edge.SourceNodeId,
edge.TargetNodeId);
if (Math.Abs(start.X - end.X) < 2d || Math.Abs(start.Y - end.Y) < 2d)
{
return SegmentIsClear(start, end);
}
var horizontalThenVertical = new ElkPoint { X = end.X, Y = start.Y };
if (SegmentIsClear(start, horizontalThenVertical) && SegmentIsClear(horizontalThenVertical, end))
{
return true;
}
var verticalThenHorizontal = new ElkPoint { X = start.X, Y = end.Y };
return SegmentIsClear(start, verticalThenHorizontal)
&& SegmentIsClear(verticalThenHorizontal, end);
}
private static int CountRepeatCollectorNodeClearanceViolations(
IReadOnlyCollection<ElkRoutedEdge> edges,
IReadOnlyCollection<ElkPositionedNode> nodes,
Dictionary<string, int>? severityByEdgeId,
int severityWeight = 1)
{
if (edges.Count == 0 || nodes.Count == 0)
{
return 0;
}
var serviceNodes = nodes.Where(node => node.Kind is not "Start" and not "End").ToArray();
var minClearance = serviceNodes.Length > 0
? Math.Min(serviceNodes.Average(node => node.Width), serviceNodes.Average(node => node.Height)) / 2d
: 50d;
var graphMinY = nodes.Min(node => node.Y);
var graphMaxY = nodes.Max(node => node.Y + node.Height);
var count = 0;
foreach (var edge in edges)
{
if (!ElkEdgePostProcessor.IsRepeatCollectorLabel(edge.Label))
{
continue;
}
var edgeViolations = 0;
foreach (var segment in ElkEdgeRoutingGeometry.FlattenSegments(edge))
{
var horizontal = Math.Abs(segment.Start.Y - segment.End.Y) < 2d;
var vertical = Math.Abs(segment.Start.X - segment.End.X) < 2d;
if (!horizontal && !vertical)
{
continue;
}
foreach (var node in nodes)
{
if (node.Id == edge.SourceNodeId || node.Id == edge.TargetNodeId)
{
continue;
}
if (horizontal)
{
var overlapX = Math.Max(segment.Start.X, segment.End.X) > node.X
&& Math.Min(segment.Start.X, segment.End.X) < node.X + node.Width;
if (!overlapX)
{
continue;
}
var distance = Math.Min(
Math.Abs(segment.Start.Y - node.Y),
Math.Abs(segment.Start.Y - (node.Y + node.Height)));
if (distance > 0.5d && distance < minClearance)
{
edgeViolations++;
}
}
else
{
var overlapY = Math.Max(segment.Start.Y, segment.End.Y) > node.Y
&& Math.Min(segment.Start.Y, segment.End.Y) < node.Y + node.Height;
if (!overlapY)
{
continue;
}
var distance = Math.Min(
Math.Abs(segment.Start.X - node.X),
Math.Abs(segment.Start.X - (node.X + node.Width)));
if (distance > 0.5d && distance < minClearance)
{
edgeViolations++;
}
}
}
}
if (edgeViolations <= 0)
{
continue;
}
count += edgeViolations;
if (severityByEdgeId is not null)
{
severityByEdgeId[edge.Id] = severityByEdgeId.GetValueOrDefault(edge.Id) + (edgeViolations * severityWeight);
}
}
return count;
}
private static (ElkPoint StartPoint, ElkPoint EndPoint) ResolveRoutingEndpoints(
ElkPoint startPoint,
ElkPoint endPoint,
string? sourceNodeId,
string? targetNodeId,
IReadOnlyDictionary<string, ElkPositionedNode> nodesById)
{
var adjustedStart = startPoint;
if (nodesById.TryGetValue(sourceNodeId ?? string.Empty, out var sourceNode)
&& ElkShapeBoundaries.IsGatewayShape(sourceNode))
{
adjustedStart = ResolveGatewayRoutingDeparturePoint(sourceNode, endPoint, startPoint);
}
var adjustedEnd = endPoint;
if (nodesById.TryGetValue(targetNodeId ?? string.Empty, out var targetNode)
&& ElkShapeBoundaries.IsGatewayShape(targetNode))
{
adjustedEnd = ResolveGatewayRoutingApproachPoint(targetNode, adjustedStart, endPoint);
}
return (adjustedStart, adjustedEnd);
}
private static ElkPoint ResolveGatewayRoutingBoundary(
ElkPositionedNode node,
ElkPoint referencePoint)
{
var boundary = ElkShapeBoundaries.ProjectOntoShapeBoundary(node, referencePoint);
return ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(node, boundary, referencePoint);
}
private static ElkPoint ResolveGatewayRoutingDeparturePoint(
ElkPositionedNode node,
ElkPoint referencePoint,
ElkPoint preferredBoundary)
{
var boundary = ElkShapeBoundaries.IsGatewayBoundaryPoint(node, preferredBoundary)
? ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(node, preferredBoundary, referencePoint)
: ResolveGatewayRoutingBoundary(node, referencePoint);
var exteriorDeparture = ElkShapeBoundaries.BuildGatewayDirectionalExteriorPoint(node, boundary, referencePoint);
return ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(node, exteriorDeparture)
? boundary
: exteriorDeparture;
}
private static ElkPoint ResolveGatewayRoutingApproachPoint(
ElkPositionedNode node,
ElkPoint referencePoint,
ElkPoint preferredBoundary)
{
var boundary = ElkShapeBoundaries.IsGatewayBoundaryPoint(node, preferredBoundary)
? ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(node, preferredBoundary, referencePoint)
: ResolveGatewayRoutingBoundary(node, referencePoint);
var exteriorApproach = ElkShapeBoundaries.BuildGatewayDirectionalExteriorPoint(node, boundary, referencePoint);
return ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(node, exteriorApproach)
? boundary
: exteriorApproach;
}
}

View File

@@ -0,0 +1,124 @@
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Globalization;
namespace StellaOps.ElkSharp;
internal static partial class ElkEdgeRouterIterative
{
private static Dictionary<string, ElkPoint> SpreadTargetEndpoints(
ElkRoutedEdge[] edges,
Dictionary<string, ElkPositionedNode> nodesById,
double graphMinY,
double graphMaxY,
double minLineClearance)
{
var result = new Dictionary<string, ElkPoint>(StringComparer.Ordinal);
// Group routable edges by target + entry side
var groups = new Dictionary<string, List<(string EdgeId, ElkPoint OrigEnd)>>(StringComparer.Ordinal);
foreach (var edge in edges)
{
if (!ShouldRouteEdge(edge, graphMinY, graphMaxY))
{
continue;
}
var lastSection = edge.Sections.LastOrDefault();
if (lastSection is null || !nodesById.TryGetValue(edge.TargetNodeId ?? "", out var targetNode))
{
continue;
}
var ep = lastSection.EndPoint;
var adjacentPoint = lastSection.BendPoints.LastOrDefault() ?? lastSection.StartPoint;
var side = ResolveEntrySide(ep, adjacentPoint, targetNode);
var key = $"{edge.TargetNodeId}|{side}";
if (!groups.TryGetValue(key, out var list))
{
list = [];
groups[key] = list;
}
list.Add((edge.Id, ep));
}
// For each group with 2+ edges on the same side, spread them
foreach (var (key, group) in groups)
{
if (group.Count < 2)
{
continue;
}
var parts = key.Split('|');
if (!nodesById.TryGetValue(parts[0], out var node))
{
continue;
}
var side = parts[1];
var sorted = side is "left" or "right"
? group.OrderBy(g => g.OrigEnd.Y).ThenBy(g => g.EdgeId, StringComparer.Ordinal).ToList()
: group.OrderBy(g => g.OrigEnd.X).ThenBy(g => g.EdgeId, StringComparer.Ordinal).ToList();
var assignedSlotCoordinates = ElkBoundarySlots.BuildAssignedBoundarySlotCoordinates(node, side, sorted.Count);
if (side is "left" or "right")
{
for (var i = 0; i < sorted.Count; i++)
{
result[sorted[i].EdgeId] = new ElkPoint
{
X = sorted[i].OrigEnd.X,
Y = assignedSlotCoordinates[i],
};
}
}
else
{
for (var i = 0; i < sorted.Count; i++)
{
result[sorted[i].EdgeId] = new ElkPoint
{
X = assignedSlotCoordinates[i],
Y = sorted[i].OrigEnd.Y,
};
}
}
}
return result;
}
private static string ResolveEntrySide(ElkPoint endpoint, ElkPoint adjacentPoint, ElkPositionedNode node)
{
return ElkEdgeRoutingGeometry.ResolveBoundaryApproachSide(endpoint, adjacentPoint, node);
}
private static bool ShouldRouteEdge(ElkRoutedEdge edge, double graphMinY, double graphMaxY)
{
// Skip port-anchored edges (their anchors are fixed)
if (!string.IsNullOrWhiteSpace(edge.SourcePortId) || !string.IsNullOrWhiteSpace(edge.TargetPortId))
{
return false;
}
// Skip backward edges (routed through external corridors)
if (!string.IsNullOrWhiteSpace(edge.Kind)
&& edge.Kind.StartsWith("backward|", StringComparison.OrdinalIgnoreCase))
{
return false;
}
// Skip edges with corridor bend points (already routed outside graph bounds)
if (ElkEdgePostProcessor.HasCorridorBendPoints(edge, graphMinY, graphMaxY))
{
return false;
}
// Skip repeat collector labels
return !ElkEdgePostProcessor.IsRepeatCollectorLabel(edge.Label);
}
}

View File

@@ -0,0 +1,167 @@
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Globalization;
namespace StellaOps.ElkSharp;
internal static partial class ElkEdgeRouterIterative
{
private static List<ElkPoint>? TryBuildLocalObstacleSkirtPath(
ElkPoint start,
ElkPoint end,
IReadOnlyCollection<ElkPositionedNode> nodes,
string sourceId,
string targetId,
ElkPositionedNode? targetNode,
double obstaclePadding)
{
var obstacles = nodes.Select(node => (
Left: node.X - obstaclePadding,
Top: node.Y - obstaclePadding,
Right: node.X + node.Width + obstaclePadding,
Bottom: node.Y + node.Height + obstaclePadding,
Id: node.Id)).ToArray();
List<ElkPoint>? bestPath = null;
var bestScore = double.MaxValue;
bool SegmentIsClear(ElkPoint from, ElkPoint to) =>
!ElkEdgePostProcessor.SegmentCrossesObstacle(from, to, obstacles, sourceId, targetId);
void ConsiderCandidate(IReadOnlyList<ElkPoint> rawCandidate)
{
var candidate = NormalizePolyline(rawCandidate);
if (candidate.Count < 2)
{
return;
}
for (var i = 1; i < candidate.Count; i++)
{
if (!SegmentIsClear(candidate[i - 1], candidate[i]))
{
return;
}
}
if (targetNode is not null
&& !ElkShapeBoundaries.IsGatewayShape(targetNode)
&& HasTargetApproachBacktracking(candidate, targetNode))
{
return;
}
var score = ComputePolylineLength(candidate) + (Math.Max(0, candidate.Count - 2) * 4d);
if (score >= bestScore - 0.5d)
{
return;
}
bestScore = score;
bestPath = candidate;
}
var horizontalDominant = Math.Abs(end.X - start.X) >= Math.Abs(end.Y - start.Y);
if (horizontalDominant)
{
var targetBridgeX = end.X;
if (targetNode is not null && ElkShapeBoundaries.IsGatewayShape(targetNode))
{
targetBridgeX = ResolveGatewayRoutingApproachPoint(targetNode, start, end).X;
}
var minX = Math.Min(start.X, end.X) + 0.5d;
var maxX = Math.Max(start.X, end.X) - 0.5d;
var corridorTop = Math.Min(start.Y, end.Y) - obstaclePadding;
var corridorBottom = Math.Max(start.Y, end.Y) + obstaclePadding;
var bypassYCandidates = new List<double> { start.Y, end.Y };
foreach (var obstacle in obstacles)
{
if (string.Equals(obstacle.Id, sourceId, StringComparison.Ordinal)
|| string.Equals(obstacle.Id, targetId, StringComparison.Ordinal)
|| obstacle.Right <= minX
|| obstacle.Left >= maxX
|| obstacle.Bottom <= corridorTop
|| obstacle.Top >= corridorBottom)
{
continue;
}
AddUniqueCoordinate(bypassYCandidates, obstacle.Top);
AddUniqueCoordinate(bypassYCandidates, obstacle.Bottom);
}
foreach (var bypassY in bypassYCandidates)
{
ConsiderCandidate(
[
start,
new ElkPoint { X = start.X, Y = bypassY },
new ElkPoint { X = targetBridgeX, Y = bypassY },
end,
]);
}
}
else
{
var targetBridgeY = end.Y;
if (targetNode is not null && ElkShapeBoundaries.IsGatewayShape(targetNode))
{
targetBridgeY = ResolveGatewayRoutingApproachPoint(targetNode, start, end).Y;
}
var minY = Math.Min(start.Y, end.Y) + 0.5d;
var maxY = Math.Max(start.Y, end.Y) - 0.5d;
var corridorLeft = Math.Min(start.X, end.X) - obstaclePadding;
var corridorRight = Math.Max(start.X, end.X) + obstaclePadding;
var bypassXCandidates = new List<double> { start.X, end.X };
foreach (var obstacle in obstacles)
{
if (string.Equals(obstacle.Id, sourceId, StringComparison.Ordinal)
|| string.Equals(obstacle.Id, targetId, StringComparison.Ordinal)
|| obstacle.Bottom <= minY
|| obstacle.Top >= maxY
|| obstacle.Right <= corridorLeft
|| obstacle.Left >= corridorRight)
{
continue;
}
AddUniqueCoordinate(bypassXCandidates, obstacle.Left);
AddUniqueCoordinate(bypassXCandidates, obstacle.Right);
}
foreach (var bypassX in bypassXCandidates)
{
ConsiderCandidate(
[
start,
new ElkPoint { X = bypassX, Y = start.Y },
new ElkPoint { X = bypassX, Y = targetBridgeY },
end,
]);
}
}
return bestPath;
}
private static double ComputePolylineLength(IReadOnlyList<ElkPoint> points)
{
var length = 0d;
for (var i = 1; i < points.Count; i++)
{
length += ElkEdgeRoutingGeometry.ComputeSegmentLength(points[i - 1], points[i]);
}
return length;
}
private static double ResolveMinLineClearance(IReadOnlyCollection<ElkPositionedNode> nodes)
{
var serviceNodes = nodes.Where(node => node.Kind is not "Start" and not "End").ToArray();
return serviceNodes.Length > 0
? Math.Min(serviceNodes.Average(node => node.Width), serviceNodes.Average(node => node.Height)) / 2d
: 50d;
}
}

View File

@@ -0,0 +1,74 @@
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Globalization;
namespace StellaOps.ElkSharp;
internal static partial class ElkEdgeRouterIterative
{
private static int[] OrderByLongestFirst(
ElkRoutedEdge[] edges,
Dictionary<string, ElkPositionedNode> nodesById)
{
return Enumerable.Range(0, edges.Length)
.OrderByDescending(i =>
{
if (!nodesById.TryGetValue(edges[i].SourceNodeId ?? "", out var s)
|| !nodesById.TryGetValue(edges[i].TargetNodeId ?? "", out var t))
{
return 0d;
}
var dx = (t.X + t.Width / 2d) - (s.X + s.Width / 2d);
var dy = (t.Y + t.Height / 2d) - (s.Y + s.Height / 2d);
return Math.Sqrt(dx * dx + dy * dy);
})
.ToArray();
}
private static int[] OrderByShortestFirst(
ElkRoutedEdge[] edges,
Dictionary<string, ElkPositionedNode> nodesById)
{
return Enumerable.Range(0, edges.Length)
.OrderBy(i =>
{
if (!nodesById.TryGetValue(edges[i].SourceNodeId ?? "", out var s)
|| !nodesById.TryGetValue(edges[i].TargetNodeId ?? "", out var t))
{
return 0d;
}
var dx = (t.X + t.Width / 2d) - (s.X + s.Width / 2d);
var dy = (t.Y + t.Height / 2d) - (s.Y + s.Height / 2d);
return Math.Sqrt(dx * dx + dy * dy);
})
.ToArray();
}
private static int[] OrderByMostConnectedFirst(
ElkRoutedEdge[] edges,
Dictionary<string, int> connectionCount)
{
return Enumerable.Range(0, edges.Length)
.OrderByDescending(i =>
connectionCount.GetValueOrDefault(edges[i].SourceNodeId ?? "")
+ connectionCount.GetValueOrDefault(edges[i].TargetNodeId ?? ""))
.ToArray();
}
private static int[] OrderByReverse(ElkRoutedEdge[] edges)
{
return Enumerable.Range(0, edges.Length).Reverse().ToArray();
}
private static void Shuffle(int[] array, Random rng)
{
for (var i = array.Length - 1; i > 0; i--)
{
var j = rng.Next(i + 1);
(array[i], array[j]) = (array[j], array[i]);
}
}
}

View File

@@ -0,0 +1,121 @@
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Globalization;
namespace StellaOps.ElkSharp;
internal static partial class ElkEdgeRouterIterative
{
private static List<ElkPoint>? ChooseBetterLocalRepairCandidate(
ElkRoutedEdge originalEdge,
IReadOnlyCollection<ElkPositionedNode> nodes,
List<ElkPoint>? primaryCandidate,
List<ElkPoint>? secondaryCandidate)
{
if (primaryCandidate is null || primaryCandidate.Count < 2)
{
return secondaryCandidate;
}
if (secondaryCandidate is null || secondaryCandidate.Count < 2)
{
return primaryCandidate;
}
var primaryEdge = BuildCandidateRepairEdge(
originalEdge.SourceNodeId,
originalEdge.TargetNodeId,
null,
null,
primaryCandidate);
var secondaryEdge = BuildCandidateRepairEdge(
originalEdge.SourceNodeId,
originalEdge.TargetNodeId,
null,
null,
secondaryCandidate);
return CompareSingleEdgeRepairQuality(secondaryEdge, primaryEdge, nodes) < 0
? secondaryCandidate
: primaryCandidate;
}
private static ElkRoutedEdge BuildCandidateRepairEdge(
string? sourceNodeId,
string? targetNodeId,
ElkPositionedNode? sourceNode,
ElkPositionedNode? targetNode,
IReadOnlyList<ElkPoint> path)
{
return new ElkRoutedEdge
{
Id = "__candidate__",
SourceNodeId = sourceNodeId,
TargetNodeId = targetNodeId,
Kind = string.Empty,
Label = string.Empty,
Sections =
[
new ElkEdgeSection
{
StartPoint = path[0],
EndPoint = path[^1],
BendPoints = path.Count > 2
? path.Skip(1).Take(path.Count - 2).ToArray()
: [],
},
],
};
}
private static int CompareSingleEdgeRepairQuality(
ElkRoutedEdge left,
ElkRoutedEdge right,
IReadOnlyCollection<ElkPositionedNode> nodes)
{
var leftBlocking = CountSingleEdgeBlockingViolations(left, nodes);
var rightBlocking = CountSingleEdgeBlockingViolations(right, nodes);
if (leftBlocking != rightBlocking)
{
return leftBlocking.CompareTo(rightBlocking);
}
var leftDetour = ElkEdgeRoutingScoring.CountExcessiveDetourViolations([left], nodes);
var rightDetour = ElkEdgeRoutingScoring.CountExcessiveDetourViolations([right], nodes);
if (leftDetour != rightDetour)
{
return leftDetour.CompareTo(rightDetour);
}
var leftLength = ComputePolylineLength(ExtractCandidatePath(left));
var rightLength = ComputePolylineLength(ExtractCandidatePath(right));
return leftLength.CompareTo(rightLength);
}
private static int CountSingleEdgeBlockingViolations(
ElkRoutedEdge edge,
IReadOnlyCollection<ElkPositionedNode> nodes)
{
return ElkEdgeRoutingScoring.CountUnderNodeViolations([edge], nodes)
+ ElkEdgeRoutingScoring.CountBelowGraphViolations([edge], nodes)
+ ElkEdgeRoutingScoring.CountBadBoundaryAngles([edge], nodes)
+ ElkEdgeRoutingScoring.CountGatewaySourceExitViolations([edge], nodes);
}
private static List<ElkPoint> ExtractCandidatePath(ElkRoutedEdge edge)
{
var path = new List<ElkPoint>();
foreach (var section in edge.Sections)
{
if (path.Count == 0)
{
path.Add(section.StartPoint);
}
path.AddRange(section.BendPoints);
path.Add(section.EndPoint);
}
return path;
}
}

View File

@@ -0,0 +1,140 @@
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Globalization;
namespace StellaOps.ElkSharp;
internal static partial class ElkEdgeRouterIterative
{
private static bool ShouldRetryForEdgeCrossings(
RoutingRetryState retryState,
int attempt,
int maxAdaptationsPerStrategy)
{
if (retryState.RequiresPrimaryRetry || retryState.EdgeCrossings <= 0)
{
return false;
}
return attempt < Math.Max(0, maxAdaptationsPerStrategy - 1);
}
private static int DetermineAdaptiveAttemptBudget(
RoutingRetryState retryState,
int maxAdaptationsPerStrategy)
{
var boundedMaximum = Math.Clamp(maxAdaptationsPerStrategy, 1, 12);
if (retryState.RequiresBlockingRetry)
{
var complexBlocking =
retryState.UnderNodeViolations > 0
|| retryState.SharedLaneViolations > 0
|| retryState.TargetApproachJoinViolations > 0
|| retryState.GatewaySourceExitViolations > 0;
return Math.Min(boundedMaximum, complexBlocking ? 6 : 5);
}
if (retryState.RequiresLengthRetry)
{
return Math.Min(boundedMaximum, 4);
}
if (retryState.RequiresQualityRetry || retryState.EdgeCrossings > 0)
{
return Math.Min(boundedMaximum, 3);
}
return 1;
}
private static bool ShouldStopForStagnation(int stagnantAttempts, int attempt, int maxAdaptationsPerStrategy)
{
if (stagnantAttempts <= 0 || attempt < 2)
{
return false;
}
var stagnationBudget = Math.Min(Math.Max(4, maxAdaptationsPerStrategy / 12), 8);
return stagnantAttempts >= stagnationBudget;
}
private static bool ShouldRetryForPrimaryViolations(
RoutingRetryState retryState,
int attempt,
int maxAdaptationsPerStrategy)
{
if (!retryState.RequiresPrimaryRetry)
{
return false;
}
return attempt < Math.Max(0, maxAdaptationsPerStrategy - 1);
}
private static int DetermineTargetValidSolutionCount(
RoutingRetryState baselineRetryState,
IterativeRoutingConfig config)
{
if (!baselineRetryState.RequiresPrimaryRetry)
{
return config.RequiredValidSolutions;
}
if (baselineRetryState.RequiresBlockingRetry || baselineRetryState.RequiresLengthRetry)
{
return Math.Min(config.RequiredValidSolutions, 3);
}
return Math.Min(config.RequiredValidSolutions, 2);
}
private static int DetermineStrategySearchBudget(
RoutingRetryState baselineRetryState,
IterativeRoutingConfig config)
{
if (!baselineRetryState.RequiresPrimaryRetry)
{
return int.MaxValue;
}
var severity = baselineRetryState.PrimaryViolationCount + baselineRetryState.EdgeCrossings;
var minimumBudget = baselineRetryState.RequiresBlockingRetry || baselineRetryState.RequiresLengthRetry
? 8
: 6;
var severityBudget = severity >= 20
? 8
: severity >= 10
? 7
: 6;
return Math.Min(
OrderingNames.Length,
Math.Max(minimumBudget, severityBudget));
}
private static bool ShouldKeepBaselineSolution(
IReadOnlyCollection<ElkRoutedEdge> baselineEdges,
IReadOnlyCollection<ElkPositionedNode> nodes,
RoutingRetryState baselineRetryState)
{
if (baselineRetryState.RepeatCollectorCorridorViolations > 0)
{
return false;
}
var hasProtectedEdgeContract = baselineEdges.Any(edge =>
!string.IsNullOrWhiteSpace(edge.SourcePortId)
|| !string.IsNullOrWhiteSpace(edge.TargetPortId)
|| (!string.IsNullOrWhiteSpace(edge.Kind)
&& edge.Kind.StartsWith("backward|", StringComparison.OrdinalIgnoreCase)));
if (baselineEdges.Count <= 8
|| nodes.Count <= 8
|| hasProtectedEdgeContract)
{
return true;
}
return !baselineRetryState.RequiresPrimaryRetry && baselineEdges.Count <= 12;
}
}

View File

@@ -0,0 +1,212 @@
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Globalization;
namespace StellaOps.ElkSharp;
internal static partial class ElkEdgeRouterIterative
{
private static bool IsBetterCandidate(
EdgeRoutingScore candidate,
RoutingRetryState candidateRetryState,
EdgeRoutingScore best,
RoutingRetryState bestRetryState)
{
if (HasHardRuleRegression(candidateRetryState, bestRetryState))
{
return false;
}
var retryComparison = CompareRetryStates(candidateRetryState, bestRetryState);
if (retryComparison != 0)
{
return retryComparison < 0;
}
if (candidate.NodeCrossings != best.NodeCrossings)
{
return candidate.NodeCrossings < best.NodeCrossings;
}
return candidate.Value > best.Value;
}
private static bool HasHardRuleRegression(RoutingRetryState candidate, RoutingRetryState baseline)
{
return candidate.RemainingShortHighways > baseline.RemainingShortHighways
|| candidate.RepeatCollectorCorridorViolations > baseline.RepeatCollectorCorridorViolations
|| candidate.RepeatCollectorNodeClearanceViolations > baseline.RepeatCollectorNodeClearanceViolations
|| candidate.BelowGraphViolations > baseline.BelowGraphViolations
|| candidate.UnderNodeViolations > baseline.UnderNodeViolations
|| candidate.LongDiagonalViolations > baseline.LongDiagonalViolations
|| candidate.EntryAngleViolations > baseline.EntryAngleViolations
|| candidate.GatewaySourceExitViolations > baseline.GatewaySourceExitViolations
|| candidate.SharedLaneViolations > baseline.SharedLaneViolations
|| candidate.BoundarySlotViolations > baseline.BoundarySlotViolations
|| candidate.TargetApproachJoinViolations > baseline.TargetApproachJoinViolations
|| candidate.TargetApproachBacktrackingViolations > baseline.TargetApproachBacktrackingViolations
|| candidate.ExcessiveDetourViolations > baseline.ExcessiveDetourViolations;
}
private static bool HasBlockingSharedLanePromotionRegression(
RoutingRetryState candidate,
RoutingRetryState baseline)
{
var allowsTemporaryBoundarySlotTrade =
candidate.SharedLaneViolations < baseline.SharedLaneViolations
&& baseline.BoundarySlotViolations > 0
&& candidate.BoundarySlotViolations <= baseline.BoundarySlotViolations + 1;
return candidate.RemainingShortHighways > baseline.RemainingShortHighways
|| candidate.RepeatCollectorCorridorViolations > baseline.RepeatCollectorCorridorViolations
|| candidate.RepeatCollectorNodeClearanceViolations > baseline.RepeatCollectorNodeClearanceViolations
|| candidate.BelowGraphViolations > baseline.BelowGraphViolations
|| candidate.UnderNodeViolations > baseline.UnderNodeViolations
|| candidate.LongDiagonalViolations > baseline.LongDiagonalViolations
|| candidate.EntryAngleViolations > baseline.EntryAngleViolations
|| candidate.GatewaySourceExitViolations > baseline.GatewaySourceExitViolations
|| candidate.SharedLaneViolations > baseline.SharedLaneViolations
|| (!allowsTemporaryBoundarySlotTrade
&& candidate.BoundarySlotViolations > baseline.BoundarySlotViolations)
|| candidate.TargetApproachJoinViolations > baseline.TargetApproachJoinViolations
|| candidate.TargetApproachBacktrackingViolations > baseline.TargetApproachBacktrackingViolations
|| candidate.ExcessiveDetourViolations > baseline.ExcessiveDetourViolations;
}
private static int CompareRetryStates(RoutingRetryState left, RoutingRetryState right)
{
if (left.RemainingShortHighways != right.RemainingShortHighways)
{
return left.RemainingShortHighways.CompareTo(right.RemainingShortHighways);
}
if (left.RepeatCollectorCorridorViolations != right.RepeatCollectorCorridorViolations)
{
return left.RepeatCollectorCorridorViolations.CompareTo(right.RepeatCollectorCorridorViolations);
}
if (left.RepeatCollectorNodeClearanceViolations != right.RepeatCollectorNodeClearanceViolations)
{
return left.RepeatCollectorNodeClearanceViolations.CompareTo(right.RepeatCollectorNodeClearanceViolations);
}
if (left.BelowGraphViolations != right.BelowGraphViolations)
{
return left.BelowGraphViolations.CompareTo(right.BelowGraphViolations);
}
if (left.UnderNodeViolations != right.UnderNodeViolations)
{
return left.UnderNodeViolations.CompareTo(right.UnderNodeViolations);
}
if (left.LongDiagonalViolations != right.LongDiagonalViolations)
{
return left.LongDiagonalViolations.CompareTo(right.LongDiagonalViolations);
}
if (left.EntryAngleViolations != right.EntryAngleViolations)
{
return left.EntryAngleViolations.CompareTo(right.EntryAngleViolations);
}
if (left.GatewaySourceExitViolations != right.GatewaySourceExitViolations)
{
return left.GatewaySourceExitViolations.CompareTo(right.GatewaySourceExitViolations);
}
if (left.SharedLaneViolations != right.SharedLaneViolations)
{
return left.SharedLaneViolations.CompareTo(right.SharedLaneViolations);
}
if (left.BoundarySlotViolations != right.BoundarySlotViolations)
{
return left.BoundarySlotViolations.CompareTo(right.BoundarySlotViolations);
}
if (left.TargetApproachJoinViolations != right.TargetApproachJoinViolations)
{
return left.TargetApproachJoinViolations.CompareTo(right.TargetApproachJoinViolations);
}
if (left.TargetApproachBacktrackingViolations != right.TargetApproachBacktrackingViolations)
{
return left.TargetApproachBacktrackingViolations.CompareTo(right.TargetApproachBacktrackingViolations);
}
if (left.ExcessiveDetourViolations != right.ExcessiveDetourViolations)
{
return left.ExcessiveDetourViolations.CompareTo(right.ExcessiveDetourViolations);
}
if (left.ProximityViolations != right.ProximityViolations)
{
return left.ProximityViolations.CompareTo(right.ProximityViolations);
}
if (left.LabelProximityViolations != right.LabelProximityViolations)
{
return left.LabelProximityViolations.CompareTo(right.LabelProximityViolations);
}
return left.EdgeCrossings.CompareTo(right.EdgeCrossings);
}
private static CandidateSolution SelectBestValidSolution(
IReadOnlyList<CandidateSolution> solutions)
{
var best = solutions[0];
for (var i = 1; i < solutions.Count; i++)
{
var candidate = solutions[i];
if (IsBetterBoundarySlotRepairCandidate(
candidate.Score,
candidate.RetryState,
best.Score,
best.RetryState)
|| candidate.Score.Value > best.Score.Value
|| (Math.Abs(candidate.Score.Value - best.Score.Value) < 0.001d
&& CompareRetryStates(candidate.RetryState, best.RetryState) < 0)
|| (Math.Abs(candidate.Score.Value - best.Score.Value) < 0.001d
&& CompareRetryStates(candidate.RetryState, best.RetryState) == 0
&& candidate.Score.EdgeCrossings < best.Score.EdgeCrossings))
{
best = candidate;
}
}
return best;
}
private static CandidateSolution SelectBestFallbackSolution(
IReadOnlyList<CandidateSolution> solutions)
{
var best = solutions[0];
for (var i = 1; i < solutions.Count; i++)
{
var candidate = solutions[i];
if (IsBetterBoundarySlotRepairCandidate(
candidate.Score,
candidate.RetryState,
best.Score,
best.RetryState)
|| candidate.Score.NodeCrossings < best.Score.NodeCrossings
|| (candidate.Score.NodeCrossings == best.Score.NodeCrossings
&& CompareRetryStates(candidate.RetryState, best.RetryState) < 0)
|| (candidate.Score.NodeCrossings == best.Score.NodeCrossings
&& CompareRetryStates(candidate.RetryState, best.RetryState) == 0
&& candidate.Score.EdgeCrossings < best.Score.EdgeCrossings)
|| (candidate.Score.NodeCrossings == best.Score.NodeCrossings
&& CompareRetryStates(candidate.RetryState, best.RetryState) == 0
&& candidate.Score.EdgeCrossings == best.Score.EdgeCrossings
&& candidate.Score.Value > best.Score.Value))
{
best = candidate;
}
}
return best;
}
}

View File

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

View File

@@ -0,0 +1,220 @@
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Globalization;
namespace StellaOps.ElkSharp;
internal static partial class ElkEdgeRouterIterative
{
private static List<ElkPoint>? TryRouteShortestRepair(
ElkPoint start,
ElkPoint end,
IReadOnlyCollection<ElkPositionedNode> nodes,
(double Left, double Top, double Right, double Bottom, string Id)[] obstacles,
string sourceId,
string targetId,
ElkPositionedNode? sourceNode,
ElkPositionedNode? targetNode,
AStarRoutingParams routingParams,
IReadOnlyList<OrthogonalSoftObstacle> softObstacles,
CancellationToken cancellationToken)
{
if (sourceNode is not null && targetNode is not null)
{
if (ElkEdgePostProcessor.TryBuildPreferredBoundaryShortcutPath(
sourceNode,
targetNode,
nodes,
sourceId,
targetId,
out var preferredShortcut)
&& (targetNode is null || !HasTargetApproachBacktracking(preferredShortcut, targetNode)))
{
return preferredShortcut;
}
}
var candidateEndpoints = EnumerateShortestRepairEndpoints(start, end, targetNode).ToArray();
var minLineClearance = ResolveMinLineClearance(nodes);
var shortcutObstaclePadding = Math.Max(12d, Math.Min(routingParams.Margin, Math.Max(18d, minLineClearance - 4d)));
List<ElkPoint>? bestPath = null;
var bestLength = double.MaxValue;
foreach (var candidateEnd in candidateEndpoints)
{
var orthogonalShortcut = TryBuildShortestOrthogonalPath(
start,
candidateEnd,
nodes,
sourceId,
targetId,
targetNode,
shortcutObstaclePadding);
if (orthogonalShortcut is null
|| (targetNode is not null && HasTargetApproachBacktracking(orthogonalShortcut, targetNode)))
{
continue;
}
var shortcutLength = ComputePolylineLength(orthogonalShortcut);
if (shortcutLength < bestLength - 0.5d)
{
bestPath = orthogonalShortcut;
bestLength = shortcutLength;
}
}
foreach (var candidateEnd in candidateEndpoints)
{
var localSkirtShortcut = TryBuildLocalObstacleSkirtPath(
start,
candidateEnd,
nodes,
sourceId,
targetId,
targetNode,
shortcutObstaclePadding);
if (localSkirtShortcut is null
|| (targetNode is not null && HasTargetApproachBacktracking(localSkirtShortcut, targetNode)))
{
continue;
}
var shortcutLength = ComputePolylineLength(localSkirtShortcut);
if (shortcutLength < bestLength - 0.5d)
{
bestPath = localSkirtShortcut;
bestLength = shortcutLength;
}
}
if (bestPath is not null)
{
return bestPath;
}
var shortestParams = routingParams with
{
Margin = Math.Max(shortcutObstaclePadding, Math.Min(routingParams.Margin, minLineClearance)),
BendPenalty = Math.Min(routingParams.BendPenalty, 80d),
DiagonalPenalty = Math.Min(routingParams.DiagonalPenalty, 40d),
SoftObstacleWeight = Math.Max(0.25d, routingParams.SoftObstacleWeight * 0.35d),
SoftObstacleClearance = Math.Max(Math.Max(18d, minLineClearance * 0.6d), routingParams.SoftObstacleClearance * 0.5d),
IntermediateGridSpacing = Math.Max(12d, routingParams.IntermediateGridSpacing - 8d),
};
var shortestObstacles = nodes
.Select(node => (
Left: node.X - shortestParams.Margin,
Top: node.Y - shortestParams.Margin,
Right: node.X + node.Width + shortestParams.Margin,
Bottom: node.Y + node.Height + shortestParams.Margin,
Id: node.Id))
.ToArray();
foreach (var candidateEnd in candidateEndpoints)
{
var diagonalPath = ElkEdgeRouterAStar8Dir.Route(
start,
candidateEnd,
shortestObstacles,
sourceId,
targetId,
shortestParams,
[],
cancellationToken);
if (diagonalPath is null
|| (targetNode is not null && HasTargetApproachBacktracking(diagonalPath, targetNode)))
{
continue;
}
var pathLength = ComputePolylineLength(diagonalPath);
if (pathLength < bestLength - 0.5d)
{
bestPath = diagonalPath;
bestLength = pathLength;
}
}
return bestPath;
}
private static List<ElkPoint>? TryRouteAggressiveRepair(
ElkPoint start,
ElkPoint end,
IReadOnlyCollection<ElkPositionedNode> nodes,
(double Left, double Top, double Right, double Bottom, string Id)[] obstacles,
string sourceId,
string targetId,
ElkPositionedNode? sourceNode,
ElkPositionedNode? targetNode,
AStarRoutingParams routingParams,
CancellationToken cancellationToken)
{
var candidateEndpoints = EnumerateShortestRepairEndpoints(start, end, targetNode).ToArray();
if (candidateEndpoints.Length == 0)
{
return null;
}
var minLineClearance = ResolveMinLineClearance(nodes);
var aggressiveParams = routingParams with
{
Margin = Math.Max(10d, Math.Min(routingParams.Margin, Math.Max(14d, minLineClearance * 0.45d))),
BendPenalty = Math.Min(routingParams.BendPenalty, 60d),
DiagonalPenalty = Math.Min(routingParams.DiagonalPenalty, 35d),
SoftObstacleWeight = 0d,
SoftObstacleClearance = 0d,
IntermediateGridSpacing = Math.Max(12d, Math.Min(routingParams.IntermediateGridSpacing, Math.Max(12d, minLineClearance * 0.45d))),
};
var aggressiveObstacles = obstacles
.Select(obstacle => (
Left: obstacle.Left + Math.Min(0d, aggressiveParams.Margin - routingParams.Margin),
Top: obstacle.Top + Math.Min(0d, aggressiveParams.Margin - routingParams.Margin),
Right: obstacle.Right - Math.Min(0d, aggressiveParams.Margin - routingParams.Margin),
Bottom: obstacle.Bottom - Math.Min(0d, aggressiveParams.Margin - routingParams.Margin),
obstacle.Id))
.ToArray();
List<ElkPoint>? bestPath = null;
ElkRoutedEdge? bestEdge = null;
foreach (var candidateEnd in candidateEndpoints)
{
var candidate = ElkEdgeRouterAStar8Dir.Route(
start,
candidateEnd,
aggressiveObstacles,
sourceId,
targetId,
aggressiveParams,
[],
cancellationToken);
if (candidate is null)
{
continue;
}
if (targetNode is not null && HasTargetApproachBacktracking(candidate, targetNode))
{
continue;
}
var candidateEdge = BuildCandidateRepairEdge(
sourceId,
targetId,
sourceNode,
targetNode,
candidate);
if (bestPath is null || CompareSingleEdgeRepairQuality(candidateEdge, bestEdge!, nodes) < 0)
{
bestPath = candidate;
bestEdge = candidateEdge;
}
}
return bestPath;
}
}

View File

@@ -0,0 +1,644 @@
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Globalization;
namespace StellaOps.ElkSharp;
internal static partial class ElkEdgeRouterIterative
{
private static StrategyEvaluationResult EvaluateStrategy(
StrategyWorkItem workItem,
ElkRoutedEdge[] baselineEdges,
ElkPositionedNode[] nodes,
ElkLayoutOptions layoutOptions,
IterativeRoutingConfig config,
CancellationToken cancellationToken,
ElkLayoutRunDiagnostics? diagnostics)
{
using var diagnosticsScope = diagnostics is null
? null
: ElkLayoutDiagnostics.Attach(diagnostics);
const int maxAllowedNodeCrossings = 0;
var strategy = workItem.Strategy;
ElkLayoutDiagnostics.LogProgress(
$"Strategy {workItem.StrategyIndex} [{workItem.StrategyName}] start: bend={strategy.RoutingParams.BendPenalty:F0} " +
$"diag={strategy.RoutingParams.DiagonalPenalty:F0} soft={strategy.RoutingParams.SoftObstacleWeight:F2} " +
$"clearance={strategy.MinLineClearance:F1}");
var bestAttemptScore = (EdgeRoutingScore?)null;
ElkRoutedEdge[]? bestAttemptEdges = null;
var bestAttemptRetryState = new RoutingRetryState(
RemainingShortHighways: int.MaxValue,
RepeatCollectorCorridorViolations: int.MaxValue,
RepeatCollectorNodeClearanceViolations: int.MaxValue,
TargetApproachJoinViolations: int.MaxValue,
TargetApproachBacktrackingViolations: int.MaxValue,
ExcessiveDetourViolations: int.MaxValue,
SharedLaneViolations: int.MaxValue,
BoundarySlotViolations: int.MaxValue,
BelowGraphViolations: int.MaxValue,
UnderNodeViolations: int.MaxValue,
LongDiagonalViolations: int.MaxValue,
ProximityViolations: int.MaxValue,
EntryAngleViolations: int.MaxValue,
GatewaySourceExitViolations: int.MaxValue,
LabelProximityViolations: int.MaxValue,
EdgeCrossings: int.MaxValue);
var repairSeedScore = (EdgeRoutingScore?)null;
ElkRoutedEdge[]? repairSeedEdges = null;
var repairSeedRetryState = new RoutingRetryState(
RemainingShortHighways: int.MaxValue,
RepeatCollectorCorridorViolations: int.MaxValue,
RepeatCollectorNodeClearanceViolations: int.MaxValue,
TargetApproachJoinViolations: int.MaxValue,
TargetApproachBacktrackingViolations: int.MaxValue,
ExcessiveDetourViolations: int.MaxValue,
SharedLaneViolations: int.MaxValue,
BoundarySlotViolations: int.MaxValue,
BelowGraphViolations: int.MaxValue,
UnderNodeViolations: int.MaxValue,
LongDiagonalViolations: int.MaxValue,
ProximityViolations: int.MaxValue,
EntryAngleViolations: int.MaxValue,
GatewaySourceExitViolations: int.MaxValue,
LabelProximityViolations: int.MaxValue,
EdgeCrossings: int.MaxValue);
var attemptDetails = new List<ElkIterativeAttemptDiagnostics>();
var fallbackSolutions = new List<CandidateSolution>();
CandidateSolution? validSolution = null;
var outcome = "no-valid";
var attempts = 0;
var maxAttempts = config.MaxAdaptationsPerStrategy;
var stagnantAttempts = 0;
string? lastPlateauFingerprint = null;
var repeatedPlateauFingerprintCount = 0;
var recentBlockingCycleFingerprints = new List<string>(4);
string? lastPlannedRepairFocusFingerprint = null;
var repeatedPlannedRepairFocusCount = 0;
string? lastRepairFocusFingerprint = null;
var repeatedRepairFocusCount = 0;
var hasLastAttemptState = false;
var lastAttemptRetryState = new RoutingRetryState(
RemainingShortHighways: int.MaxValue,
RepeatCollectorCorridorViolations: int.MaxValue,
RepeatCollectorNodeClearanceViolations: int.MaxValue,
TargetApproachJoinViolations: int.MaxValue,
TargetApproachBacktrackingViolations: int.MaxValue,
ExcessiveDetourViolations: int.MaxValue,
SharedLaneViolations: int.MaxValue,
BoundarySlotViolations: int.MaxValue,
BelowGraphViolations: int.MaxValue,
UnderNodeViolations: int.MaxValue,
LongDiagonalViolations: int.MaxValue,
ProximityViolations: int.MaxValue,
EntryAngleViolations: int.MaxValue,
GatewaySourceExitViolations: int.MaxValue,
LabelProximityViolations: int.MaxValue,
EdgeCrossings: int.MaxValue);
var lastAttemptNodeCrossings = int.MaxValue;
var consecutiveNonImprovingAttempts = 0;
var strategyStopwatch = Stopwatch.StartNew();
ElkIterativeStrategyDiagnostics? liveStrategyDiagnostics = null;
if (diagnostics is not null)
{
liveStrategyDiagnostics = new ElkIterativeStrategyDiagnostics
{
StrategyIndex = workItem.StrategyIndex,
OrderingName = workItem.StrategyName,
Attempts = 0,
TotalDurationMs = 0,
BestScore = null,
Outcome = "running",
BendPenalty = workItem.Strategy.RoutingParams.BendPenalty,
DiagonalPenalty = workItem.Strategy.RoutingParams.DiagonalPenalty,
SoftObstacleWeight = workItem.Strategy.RoutingParams.SoftObstacleWeight,
RegisteredLive = true,
};
lock (diagnostics.SyncRoot)
{
diagnostics.IterativeStrategies.Add(liveStrategyDiagnostics);
}
ElkLayoutDiagnostics.FlushSnapshot(diagnostics);
}
for (var attempt = 0; attempt < maxAttempts; attempt++)
{
cancellationToken.ThrowIfCancellationRequested();
attempts++;
var attemptStopwatch = Stopwatch.StartNew();
var phaseTimings = new List<ElkIterativePhaseDiagnostics>();
ElkLayoutDiagnostics.LogProgress(
$"Strategy {workItem.StrategyIndex} [{workItem.StrategyName}] attempt {attempt + 1} start");
T MeasurePhase<T>(string phaseName, Func<T> action)
{
var phaseStopwatch = Stopwatch.StartNew();
var value = action();
phaseStopwatch.Stop();
phaseTimings.Add(new ElkIterativePhaseDiagnostics
{
Phase = phaseName,
DurationMs = Math.Round(phaseStopwatch.Elapsed.TotalMilliseconds, 3),
});
return value;
}
RepairPlan? repairPlan = null;
RouteAllEdgesResult? routeResult;
if (attempt == 0 || repairSeedEdges is null || repairSeedScore is null)
{
routeResult = MeasurePhase(
"route-all-edges",
() => RouteAllEdges(baselineEdges, nodes, config.ObstacleMargin, strategy, cancellationToken));
}
else
{
repairPlan = MeasurePhase(
"select-repair-targets",
() => BuildRepairPlan(repairSeedEdges, nodes, repairSeedScore.Value, repairSeedRetryState, strategy, attempt));
if (repairPlan is null)
{
outcome = $"no-repair-targets({DescribeRetryState(repairSeedRetryState)})@attempt{attempt + 1}";
ElkLayoutDiagnostics.LogProgress(
$"Strategy {workItem.StrategyIndex} [{workItem.StrategyName}] {outcome}");
break;
}
var plannedRepairFocusFingerprint = BuildRepairFocusFingerprint(repairPlan);
if (!string.IsNullOrEmpty(plannedRepairFocusFingerprint))
{
if (string.Equals(plannedRepairFocusFingerprint, lastPlannedRepairFocusFingerprint, StringComparison.Ordinal))
{
repeatedPlannedRepairFocusCount++;
}
else
{
lastPlannedRepairFocusFingerprint = plannedRepairFocusFingerprint;
repeatedPlannedRepairFocusCount = 0;
}
if (repeatedPlannedRepairFocusCount >= 1 && attempt >= 2)
{
outcome = $"stalled-same-repair-plan({DescribeRetryState(repairSeedRetryState)})@attempt{attempt + 1}";
ElkLayoutDiagnostics.LogProgress(
$"Strategy {workItem.StrategyIndex} [{workItem.StrategyName}] {outcome}");
break;
}
}
else
{
lastPlannedRepairFocusFingerprint = null;
repeatedPlannedRepairFocusCount = 0;
}
ElkLayoutDiagnostics.LogProgress(
$"Strategy {workItem.StrategyIndex} [{workItem.StrategyName}] attempt {attempt + 1} local-repair " +
$"edges=[{string.Join(", ", repairPlan.Value.EdgeIds)}] reasons=[{string.Join(", ", repairPlan.Value.Reasons)}]");
routeResult = MeasurePhase(
"route-penalized-edges",
() => RepairPenalizedEdges(
repairSeedEdges,
nodes,
config.ObstacleMargin,
strategy,
repairPlan.Value,
cancellationToken,
config.MaxParallelRepairBuilds));
}
if (routeResult is null)
{
outcome = "route-failed";
ElkLayoutDiagnostics.LogProgress(
$"Strategy {workItem.StrategyIndex} [{workItem.StrategyName}] {outcome}@attempt{attempt + 1}");
break;
}
var candidateEdges = routeResult.Edges;
var scopedCleanupEdgeIds = routeResult.Diagnostics?.Mode == "local-repair"
? routeResult.Diagnostics.RepairedEdgeIds.ToArray()
: null;
if (scopedCleanupEdgeIds is { Length: > 0 })
{
candidateEdges = MeasurePhase(
"targeted-terminal-rule-cleanup",
() => ApplyTerminalRuleCleanupRound(
candidateEdges,
nodes,
layoutOptions.Direction,
strategy.MinLineClearance,
scopedCleanupEdgeIds));
}
else
{
candidateEdges = MeasurePhase(
"snap-anchors",
() => ElkEdgePostProcessor.SnapAnchorsToNodeBoundary(candidateEdges, nodes));
candidateEdges = MeasurePhase(
"eliminate-diagonals",
() => ElkEdgePostProcessor.EliminateDiagonalSegments(candidateEdges, nodes));
candidateEdges = MeasurePhase(
"avoid-node-crossings-1",
() => ElkEdgePostProcessor.AvoidNodeCrossings(candidateEdges, nodes, layoutOptions.Direction));
candidateEdges = MeasurePhase(
"simplify-1",
() => ElkEdgePostProcessorSimplify.SimplifyEdgePaths(candidateEdges, nodes));
candidateEdges = MeasurePhase(
"tighten-corridors",
() => ElkEdgePostProcessorSimplify.TightenOuterCorridors(candidateEdges, nodes));
if (HighwayProcessingEnabled)
{
candidateEdges = MeasurePhase(
"break-short-highways",
() => ElkEdgeRouterHighway.BreakShortHighways(candidateEdges, nodes));
}
candidateEdges = MeasurePhase(
"terminal-rule-cleanup",
() => ApplyTerminalRuleCleanupRound(
candidateEdges,
nodes,
layoutOptions.Direction,
strategy.MinLineClearance));
}
var score = MeasurePhase(
"compute-score",
() => ElkEdgeRoutingScoring.ComputeScore(candidateEdges, nodes));
var remainingBrokenHighways = HighwayProcessingEnabled
? MeasurePhase(
"detect-broken-highways",
() => ElkEdgeRouterHighway.DetectRemainingBrokenHighways(candidateEdges, nodes).Count)
: 0;
var retryState = BuildRetryState(score, remainingBrokenHighways);
if (retryState.RequiresBlockingRetry || retryState.RequiresLengthRetry)
{
var focusedRepair = MeasurePhase(
"repair-verified-issues",
() => TryApplyVerifiedIssueRepairRound(
candidateEdges,
nodes,
config.ObstacleMargin,
strategy,
retryState,
layoutOptions.Direction,
cancellationToken,
config.MaxParallelRepairBuilds));
if (focusedRepair is { } repaired
&& IsBetterBoundarySlotRepairCandidate(
repaired.Score,
repaired.RetryState,
score,
retryState))
{
candidateEdges = repaired.Edges;
score = repaired.Score;
remainingBrokenHighways = repaired.RemainingBrokenHighways;
retryState = repaired.RetryState;
}
}
if (retryState.RequiresBlockingRetry || retryState.RequiresLengthRetry)
{
var stabilizedEdges = MeasurePhase(
"stabilize-terminal-rules",
() => ApplyTerminalRuleCleanupRound(
candidateEdges,
nodes,
layoutOptions.Direction,
strategy.MinLineClearance,
scopedCleanupEdgeIds));
var stabilizedScore = MeasurePhase(
"compute-score-stabilized",
() => ElkEdgeRoutingScoring.ComputeScore(stabilizedEdges, nodes));
var stabilizedBrokenHighways = HighwayProcessingEnabled
? MeasurePhase(
"detect-broken-highways-stabilized",
() => ElkEdgeRouterHighway.DetectRemainingBrokenHighways(stabilizedEdges, nodes).Count)
: 0;
var stabilizedRetryState = BuildRetryState(stabilizedScore, stabilizedBrokenHighways);
if (IsBetterBoundarySlotRepairCandidate(
stabilizedScore,
stabilizedRetryState,
score,
retryState))
{
candidateEdges = stabilizedEdges;
score = stabilizedScore;
remainingBrokenHighways = stabilizedBrokenHighways;
retryState = stabilizedRetryState;
}
}
if (attempt == 0)
{
maxAttempts = DetermineAdaptiveAttemptBudget(retryState, config.MaxAdaptationsPerStrategy);
ElkLayoutDiagnostics.LogProgress(
$"Strategy {workItem.StrategyIndex} [{workItem.StrategyName}] attempt-budget={maxAttempts}");
}
ElkLayoutDiagnostics.LogProgress(
$"Strategy {workItem.StrategyIndex} [{workItem.StrategyName}] attempt {attempt + 1} " +
$"score={score.Value:F0} retry={DescribeRetryState(retryState)}");
var candidate = new CandidateSolution(score, retryState, candidateEdges, workItem.StrategyIndex);
fallbackSolutions.Add(candidate);
repairSeedScore = score;
repairSeedEdges = candidateEdges;
repairSeedRetryState = retryState;
var repairFocusFingerprint = BuildRepairFocusFingerprint(repairPlan);
if (!string.IsNullOrEmpty(repairFocusFingerprint))
{
if (string.Equals(repairFocusFingerprint, lastRepairFocusFingerprint, StringComparison.Ordinal))
{
repeatedRepairFocusCount++;
}
else
{
lastRepairFocusFingerprint = repairFocusFingerprint;
repeatedRepairFocusCount = 0;
}
if (repeatedRepairFocusCount >= 1 && attempt >= 3)
{
outcome = $"stalled-same-focus({DescribeRetryState(retryState)})@attempt{attempt + 1}";
ElkLayoutDiagnostics.LogProgress(
$"Strategy {workItem.StrategyIndex} [{workItem.StrategyName}] {outcome}");
break;
}
}
else
{
lastRepairFocusFingerprint = null;
repeatedRepairFocusCount = 0;
}
var plateauFingerprint = BuildPlateauFingerprint(retryState, repairPlan);
if (string.Equals(plateauFingerprint, lastPlateauFingerprint, StringComparison.Ordinal))
{
repeatedPlateauFingerprintCount++;
}
else
{
lastPlateauFingerprint = plateauFingerprint;
repeatedPlateauFingerprintCount = 0;
}
if (repeatedPlateauFingerprintCount >= 2 && attempt >= 3)
{
outcome = $"stalled-repeat({DescribeRetryState(retryState)})@attempt{attempt + 1}";
ElkLayoutDiagnostics.LogProgress(
$"Strategy {workItem.StrategyIndex} [{workItem.StrategyName}] {outcome}");
break;
}
var blockingCycleFingerprint = BuildBlockingCycleFingerprint(retryState, repairPlan);
if (ShouldStopForBlockingCycle(
recentBlockingCycleFingerprints,
blockingCycleFingerprint,
retryState,
bestAttemptRetryState,
attempt))
{
outcome = $"stalled-cycle({DescribeRetryState(retryState)})@attempt{attempt + 1}";
ElkLayoutDiagnostics.LogProgress(
$"Strategy {workItem.StrategyIndex} [{workItem.StrategyName}] {outcome}");
break;
}
AppendRecentFingerprint(recentBlockingCycleFingerprints, blockingCycleFingerprint, 4);
if (hasLastAttemptState)
{
var retryStateComparison = CompareRetryStates(retryState, lastAttemptRetryState);
var improvedBoundarySlotsSinceLast =
retryState.BoundarySlotViolations < lastAttemptRetryState.BoundarySlotViolations
&& score.NodeCrossings <= lastAttemptNodeCrossings
&& !HasBlockingBoundarySlotPromotionRegression(retryState, lastAttemptRetryState);
if (!improvedBoundarySlotsSinceLast
&& retryStateComparison >= 0
&& score.NodeCrossings >= lastAttemptNodeCrossings)
{
consecutiveNonImprovingAttempts++;
}
else
{
consecutiveNonImprovingAttempts = 0;
}
if (consecutiveNonImprovingAttempts >= 2 && attempt >= 3)
{
outcome = $"stalled-no-progress({DescribeRetryState(retryState)})@attempt{attempt + 1}";
ElkLayoutDiagnostics.LogProgress(
$"Strategy {workItem.StrategyIndex} [{workItem.StrategyName}] {outcome}");
break;
}
}
hasLastAttemptState = true;
lastAttemptRetryState = retryState;
lastAttemptNodeCrossings = score.NodeCrossings;
var improvedAttempt = bestAttemptScore is null
|| IsBetterBoundarySlotRepairCandidate(
score,
retryState,
bestAttemptScore.Value,
bestAttemptRetryState);
var improvedRuleState = bestAttemptScore is null
|| (retryState.BoundarySlotViolations < bestAttemptRetryState.BoundarySlotViolations
&& score.NodeCrossings <= bestAttemptScore.Value.NodeCrossings
&& !HasBlockingBoundarySlotPromotionRegression(retryState, bestAttemptRetryState))
|| CompareRetryStates(retryState, bestAttemptRetryState) < 0
|| score.NodeCrossings < bestAttemptScore.Value.NodeCrossings;
if (improvedAttempt)
{
bestAttemptScore = score;
bestAttemptEdges = candidateEdges;
bestAttemptRetryState = retryState;
stagnantAttempts = improvedRuleState
? 0
: stagnantAttempts + 1;
if (ShouldStopForStagnation(stagnantAttempts, attempt, config.MaxAdaptationsPerStrategy))
{
outcome = $"stalled({DescribeRetryState(retryState)})@attempt{attempt + 1}";
ElkLayoutDiagnostics.LogProgress(
$"Strategy {workItem.StrategyIndex} [{workItem.StrategyName}] {outcome}");
break;
}
}
else
{
stagnantAttempts++;
if (ShouldStopForStagnation(stagnantAttempts, attempt, config.MaxAdaptationsPerStrategy))
{
outcome = $"stalled({DescribeRetryState(retryState)})@attempt{attempt + 1}";
ElkLayoutDiagnostics.LogProgress(
$"Strategy {workItem.StrategyIndex} [{workItem.StrategyName}] {outcome}");
break;
}
}
var attemptOutcome = score.NodeCrossings > maxAllowedNodeCrossings
? $"hard-violation(nc={score.NodeCrossings}>{maxAllowedNodeCrossings})"
: retryState.RequiresPrimaryRetry
? $"retry({DescribeRetryState(retryState)})"
: ShouldRetryForEdgeCrossings(retryState, attempt, config.MaxAdaptationsPerStrategy)
? $"retry(edge-crossings={retryState.EdgeCrossings})"
: "valid";
if (diagnostics is not null)
{
attemptStopwatch.Stop();
var attemptDiagnostics = new ElkIterativeAttemptDiagnostics
{
Attempt = attempt + 1,
TotalDurationMs = Math.Round(attemptStopwatch.Elapsed.TotalMilliseconds, 3),
Score = score,
Outcome = attemptOutcome,
RouteDiagnostics = routeResult.Diagnostics,
Edges = candidateEdges,
};
attemptDiagnostics.PhaseTimings.AddRange(phaseTimings);
attemptDetails.Add(attemptDiagnostics);
if (liveStrategyDiagnostics is not null)
{
lock (diagnostics.SyncRoot)
{
liveStrategyDiagnostics.Attempts = attempts;
liveStrategyDiagnostics.TotalDurationMs = Math.Round(strategyStopwatch.Elapsed.TotalMilliseconds, 3);
liveStrategyDiagnostics.BestScore = bestAttemptScore;
liveStrategyDiagnostics.Outcome = attemptOutcome;
liveStrategyDiagnostics.AttemptDetails.Add(attemptDiagnostics);
}
ElkLayoutDiagnostics.FlushSnapshot(diagnostics);
}
}
if (score.NodeCrossings > maxAllowedNodeCrossings)
{
outcome = $"{attemptOutcome}@attempt{attempt + 1}";
ElkLayoutDiagnostics.LogProgress(
$"Strategy {workItem.StrategyIndex} [{workItem.StrategyName}] adapting after node crossing violation");
strategy.AdaptForViolations(score, attempt, retryState);
continue;
}
if (retryState.RemainingShortHighways > 0
|| retryState.RepeatCollectorCorridorViolations > 0
|| retryState.RepeatCollectorNodeClearanceViolations > 0
|| retryState.TargetApproachJoinViolations > 0
|| retryState.TargetApproachBacktrackingViolations > 0
|| retryState.SharedLaneViolations > 0
|| retryState.BelowGraphViolations > 0
|| retryState.UnderNodeViolations > 0
|| retryState.LongDiagonalViolations > 0
|| retryState.EntryAngleViolations > 0
|| retryState.GatewaySourceExitViolations > 0)
{
if (ShouldRetryForPrimaryViolations(retryState, attempt, config.MaxAdaptationsPerStrategy))
{
outcome = $"{attemptOutcome}@attempt{attempt + 1}";
ElkLayoutDiagnostics.LogProgress(
$"Strategy {workItem.StrategyIndex} [{workItem.StrategyName}] adapting for blocking violations");
strategy.AdaptForViolations(score, attempt, retryState);
continue;
}
outcome = $"invalid({DescribeRetryState(retryState)})@attempt{attempt + 1}";
ElkLayoutDiagnostics.LogProgress(
$"Strategy {workItem.StrategyIndex} [{workItem.StrategyName}] {outcome}");
break;
}
if (retryState.RequiresLengthRetry)
{
if (ShouldRetryForPrimaryViolations(retryState, attempt, config.MaxAdaptationsPerStrategy))
{
outcome = $"{attemptOutcome}@attempt{attempt + 1}";
ElkLayoutDiagnostics.LogProgress(
$"Strategy {workItem.StrategyIndex} [{workItem.StrategyName}] adapting for shortest-path / detour violations");
strategy.AdaptForViolations(score, attempt, retryState);
continue;
}
outcome = $"invalid({DescribeRetryState(retryState)})@attempt{attempt + 1}";
ElkLayoutDiagnostics.LogProgress(
$"Strategy {workItem.StrategyIndex} [{workItem.StrategyName}] {outcome}");
break;
}
if (retryState.RequiresQualityRetry)
{
if (ShouldRetryForPrimaryViolations(retryState, attempt, config.MaxAdaptationsPerStrategy))
{
outcome = $"{attemptOutcome}@attempt{attempt + 1}";
ElkLayoutDiagnostics.LogProgress(
$"Strategy {workItem.StrategyIndex} [{workItem.StrategyName}] adapting for quality/length violations");
strategy.AdaptForViolations(score, attempt, retryState);
continue;
}
}
if (ShouldRetryForEdgeCrossings(retryState, attempt, config.MaxAdaptationsPerStrategy))
{
outcome = $"{attemptOutcome}@attempt{attempt + 1}";
ElkLayoutDiagnostics.LogProgress(
$"Strategy {workItem.StrategyIndex} [{workItem.StrategyName}] adapting for edge crossings");
strategy.AdaptForViolations(score, attempt, retryState);
continue;
}
var residualSoftViolations = retryState.RequiresQualityRetry || retryState.EdgeCrossings > 0;
outcome = residualSoftViolations
? $"valid-soft({DescribeRetryState(retryState)})@attempt{attempt + 1}"
: $"valid@attempt{attempt + 1}";
ElkLayoutDiagnostics.LogProgress(
$"Strategy {workItem.StrategyIndex} [{workItem.StrategyName}] {outcome}");
validSolution = candidate;
break;
}
var stratDiag = new ElkIterativeStrategyDiagnostics
{
StrategyIndex = workItem.StrategyIndex,
OrderingName = workItem.StrategyName,
Attempts = attempts,
TotalDurationMs = Math.Round(strategyStopwatch.Elapsed.TotalMilliseconds, 3),
BestScore = bestAttemptScore,
Outcome = outcome,
BendPenalty = workItem.Strategy.RoutingParams.BendPenalty,
DiagonalPenalty = workItem.Strategy.RoutingParams.DiagonalPenalty,
SoftObstacleWeight = workItem.Strategy.RoutingParams.SoftObstacleWeight,
BestEdges = bestAttemptEdges,
};
stratDiag.AttemptDetails.AddRange(attemptDetails);
if (liveStrategyDiagnostics is not null && diagnostics is not null)
{
lock (diagnostics.SyncRoot)
{
liveStrategyDiagnostics.Attempts = attempts;
liveStrategyDiagnostics.TotalDurationMs = Math.Round(strategyStopwatch.Elapsed.TotalMilliseconds, 3);
liveStrategyDiagnostics.BestScore = bestAttemptScore;
liveStrategyDiagnostics.Outcome = outcome;
liveStrategyDiagnostics.BestEdges = bestAttemptEdges;
}
ElkLayoutDiagnostics.FlushSnapshot(diagnostics);
}
return new StrategyEvaluationResult(
workItem.StrategyIndex,
fallbackSolutions,
validSolution,
stratDiag);
}
}

View File

@@ -0,0 +1,206 @@
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Globalization;
namespace StellaOps.ElkSharp;
internal static partial class ElkEdgeRouterIterative
{
private static int DetermineRepairEdgeBudget(
RoutingRetryState retryState,
bool detourPriority,
int seededEdgeCount,
int mandatoryEdgeCount)
{
if (detourPriority)
{
return Math.Clamp(Math.Max(2, seededEdgeCount), 2, 6);
}
var budget = retryState.RequiresBlockingRetry
? 3
: retryState.RequiresLengthRetry
? 2
: 2;
if (retryState.ProximityViolations >= 6 || retryState.EdgeCrossings >= 8)
{
budget++;
}
budget = Math.Max(budget, Math.Min(6, Math.Max(2, seededEdgeCount)));
return Math.Clamp(budget, 2, 6);
}
private static int DetermineMandatoryFocusBudget(
int mandatoryEdgeCount,
int repairBudget,
int seededEdgeCount)
{
if (mandatoryEdgeCount <= 0)
{
return 0;
}
if (mandatoryEdgeCount <= repairBudget)
{
return mandatoryEdgeCount;
}
var focusBudget = Math.Max(2, Math.Max(seededEdgeCount, repairBudget - 1));
return Math.Min(mandatoryEdgeCount, Math.Clamp(focusBudget, 2, 6));
}
private static IEnumerable<string> RotateOrderedEdgeIds(
IReadOnlyList<string> edgeIds,
int attempt)
{
if (edgeIds.Count == 0)
{
yield break;
}
var offset = edgeIds.Count == 0
? 0
: Math.Max(0, attempt - 1) % edgeIds.Count;
for (var i = 0; i < edgeIds.Count; i++)
{
yield return edgeIds[(offset + i) % edgeIds.Count];
}
}
private static string BuildPlateauFingerprint(
RoutingRetryState retryState,
RepairPlan? repairPlan)
{
if (repairPlan is null)
{
return $"full::{DescribeRetryState(retryState)}";
}
return string.Create(
CultureInfo.InvariantCulture,
$"{DescribeRetryState(retryState)}::{string.Join(",", repairPlan.Value.EdgeIds.OrderBy(id => id, StringComparer.Ordinal))}");
}
private static string BuildBlockingCycleFingerprint(
RoutingRetryState retryState,
RepairPlan? repairPlan)
{
var classes = new List<string>(8);
if (retryState.RemainingShortHighways > 0)
{
classes.Add("short-highways");
}
if (retryState.RepeatCollectorCorridorViolations > 0)
{
classes.Add("collector-corridors");
}
if (retryState.RepeatCollectorNodeClearanceViolations > 0)
{
classes.Add("collector-clearance");
}
if (retryState.TargetApproachJoinViolations > 0)
{
classes.Add("target-joins");
}
if (retryState.TargetApproachBacktrackingViolations > 0)
{
classes.Add("approach-backtracking");
}
if (retryState.SharedLaneViolations > 0)
{
classes.Add("shared-lanes");
}
if (retryState.BelowGraphViolations > 0)
{
classes.Add("below-graph");
}
if (retryState.UnderNodeViolations > 0)
{
classes.Add("under-node");
}
if (retryState.LongDiagonalViolations > 0)
{
classes.Add("long-diagonal");
}
if (retryState.EntryAngleViolations > 0)
{
classes.Add("entry");
}
if (retryState.GatewaySourceExitViolations > 0)
{
classes.Add("gateway-source");
}
if (retryState.ExcessiveDetourViolations > 0)
{
classes.Add("detour");
}
var repairReasons = repairPlan is null
? "full"
: string.Join(",", repairPlan.Value.Reasons.OrderBy(reason => reason, StringComparer.Ordinal));
return string.Create(
CultureInfo.InvariantCulture,
$"{string.Join("|", classes)}::{repairReasons}");
}
private static bool ShouldStopForBlockingCycle(
IReadOnlyList<string> recentFingerprints,
string fingerprint,
RoutingRetryState retryState,
RoutingRetryState bestRetryState,
int attempt)
{
if (attempt < 3 || recentFingerprints.Count < 3)
{
return false;
}
var repeatCount = recentFingerprints.Count(item => string.Equals(item, fingerprint, StringComparison.Ordinal));
return repeatCount >= 2
&& retryState.BlockingViolationCount >= bestRetryState.BlockingViolationCount
&& retryState.LengthViolationCount >= bestRetryState.LengthViolationCount;
}
private static void AppendRecentFingerprint(
List<string> recentFingerprints,
string fingerprint,
int capacity)
{
if (capacity <= 0)
{
return;
}
if (recentFingerprints.Count >= capacity)
{
recentFingerprints.RemoveAt(0);
}
recentFingerprints.Add(fingerprint);
}
private static string? BuildRepairFocusFingerprint(RepairPlan? repairPlan)
{
if (repairPlan is null)
{
return null;
}
return string.Create(
CultureInfo.InvariantCulture,
$"{string.Join(",", repairPlan.Value.Reasons.OrderBy(reason => reason, StringComparer.Ordinal))}::{string.Join(",", repairPlan.Value.EdgeIds.OrderBy(id => id, StringComparer.Ordinal))}");
}
}

View File

@@ -0,0 +1,226 @@
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Globalization;
namespace StellaOps.ElkSharp;
internal static partial class ElkEdgeRouterIterative
{
private static int DetermineRepairBuildParallelism(int repairEdgeCount, int maxParallelRepairBuilds)
{
if (repairEdgeCount <= 1)
{
return 1;
}
var requestedParallelism = Math.Clamp(
maxParallelRepairBuilds,
1,
Math.Max(1, Environment.ProcessorCount));
return Math.Min(repairEdgeCount, requestedParallelism);
}
private static bool CanParallelizeRepairBuilds(
IReadOnlyList<int> orderedRepairIndices,
IReadOnlyList<ElkRoutedEdge> existingEdges)
{
if (orderedRepairIndices.Count <= 1)
{
return false;
}
var seenLockKeys = new HashSet<string>(StringComparer.Ordinal);
foreach (var edgeIndex in orderedRepairIndices)
{
if (edgeIndex < 0 || edgeIndex >= existingEdges.Count)
{
continue;
}
foreach (var lockKey in GetRepairBuildLockKeys(existingEdges[edgeIndex]))
{
if (!seenLockKeys.Add(lockKey))
{
return false;
}
}
}
return true;
}
private static RepairEdgeBuildResult BuildRepairEdgeResult(
int edgeIndex,
ElkRoutedEdge[] existingEdges,
ElkPositionedNode[] nodes,
(double Left, double Top, double Right, double Bottom, string Id)[] obstacles,
IReadOnlyDictionary<string, ElkPoint> spreadEndpoints,
IReadOnlyDictionary<string, ElkPositionedNode> nodesById,
IReadOnlyList<OrthogonalSoftObstacle> softObstacles,
IReadOnlySet<string> routeRepairEdgeIdSet,
IReadOnlySet<string> preferredShortestEdgeIdSet,
IReadOnlyCollection<string> repairReasons,
double graphMinY,
double graphMaxY,
RoutingStrategy strategy,
CancellationToken cancellationToken)
{
var edge = existingEdges[edgeIndex];
if (!CanRouteSelectedRepairEdge(edge, graphMinY, graphMaxY, routeRepairEdgeIdSet))
{
return new RepairEdgeBuildResult(edge, 0, 0, true);
}
var sourceNode = nodesById.GetValueOrDefault(edge.SourceNodeId ?? string.Empty);
var targetNode = nodesById.GetValueOrDefault(edge.TargetNodeId ?? string.Empty);
var aggressiveRepairNeeded = routeRepairEdgeIdSet.Contains(edge.Id)
&& repairReasons.Any(static reason =>
reason is "under-node"
or "entry"
or "gateway-source-exit"
or "target-joins"
or "below-graph");
var newSections = new List<ElkEdgeSection>(edge.Sections.Count);
var routedSections = 0;
var fallbackSections = 0;
foreach (var section in edge.Sections)
{
var endPoint = spreadEndpoints.TryGetValue(edge.Id, out var spread)
? spread
: section.EndPoint;
var (startPoint, adjustedEndPoint) = ResolveRoutingEndpoints(
section.StartPoint,
endPoint,
edge.SourceNodeId,
edge.TargetNodeId,
nodesById);
List<ElkPoint>? rerouted = null;
if (preferredShortestEdgeIdSet.Contains(edge.Id))
{
rerouted = TryRouteShortestRepair(
startPoint,
adjustedEndPoint,
nodes,
obstacles,
edge.SourceNodeId ?? string.Empty,
edge.TargetNodeId ?? string.Empty,
sourceNode,
targetNode,
strategy.RoutingParams,
softObstacles,
cancellationToken);
}
if (aggressiveRepairNeeded)
{
var aggressiveReroute = TryRouteAggressiveRepair(
startPoint,
adjustedEndPoint,
nodes,
obstacles,
edge.SourceNodeId ?? string.Empty,
edge.TargetNodeId ?? string.Empty,
sourceNode,
targetNode,
strategy.RoutingParams,
cancellationToken);
rerouted = ChooseBetterLocalRepairCandidate(
edge,
nodes,
rerouted,
aggressiveReroute);
}
rerouted ??= ElkEdgeRouterAStar8Dir.Route(
startPoint,
adjustedEndPoint,
obstacles,
edge.SourceNodeId ?? string.Empty,
edge.TargetNodeId ?? string.Empty,
strategy.RoutingParams,
softObstacles,
cancellationToken);
if (rerouted is not null && rerouted.Count >= 2)
{
routedSections++;
newSections.Add(new ElkEdgeSection
{
StartPoint = rerouted[0],
EndPoint = rerouted[^1],
BendPoints = rerouted.Skip(1).Take(rerouted.Count - 2).ToArray(),
});
}
else
{
fallbackSections++;
newSections.Add(section);
}
}
return new RepairEdgeBuildResult(
new ElkRoutedEdge
{
Id = edge.Id,
SourceNodeId = edge.SourceNodeId,
TargetNodeId = edge.TargetNodeId,
SourcePortId = edge.SourcePortId,
TargetPortId = edge.TargetPortId,
Kind = edge.Kind,
Label = edge.Label,
Sections = newSections,
},
routedSections,
fallbackSections,
false);
}
private static string[] GetRepairBuildLockKeys(ElkRoutedEdge edge)
{
return new[]
{
$"source:{edge.SourceNodeId ?? string.Empty}",
$"target:{edge.TargetNodeId ?? string.Empty}",
}
.Where(static key => !key.EndsWith(':'))
.Distinct(StringComparer.Ordinal)
.OrderBy(static key => key, StringComparer.Ordinal)
.ToArray();
}
private static void ExecuteWithRepairBuildLocks(
ConcurrentDictionary<string, object> lockRegistry,
IReadOnlyList<string> lockKeys,
Action action)
{
static void ExecuteLocked(
IReadOnlyList<object> locks,
int index,
Action action)
{
if (index >= locks.Count)
{
action();
return;
}
lock (locks[index])
{
ExecuteLocked(locks, index + 1, action);
}
}
if (lockKeys.Count == 0)
{
action();
return;
}
var locks = lockKeys
.Select(key => lockRegistry.GetOrAdd(key, static _ => new object()))
.ToArray();
ExecuteLocked(locks, 0, action);
}
}

View File

@@ -0,0 +1,205 @@
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Globalization;
namespace StellaOps.ElkSharp;
internal static partial class ElkEdgeRouterIterative
{
private static RouteAllEdgesResult RepairPenalizedEdges(
ElkRoutedEdge[] existingEdges,
ElkPositionedNode[] nodes,
double baseObstacleMargin,
RoutingStrategy strategy,
RepairPlan repairPlan,
CancellationToken cancellationToken,
int maxParallelRepairBuilds,
bool trustIndependentParallelBuilds = false)
{
var routedEdges = new ElkRoutedEdge[existingEdges.Length];
Array.Copy(existingEdges, routedEdges, existingEdges.Length);
var obstacleMargin = Math.Max(
baseObstacleMargin,
Math.Max(strategy.MinLineClearance + 4d, strategy.RoutingParams.Margin));
var obstacles = BuildObstacles(nodes, obstacleMargin);
var graphMinY = nodes.Length > 0 ? nodes.Min(n => n.Y) : 0d;
var graphMaxY = nodes.Length > 0 ? nodes.Max(n => n.Y + n.Height) : 0d;
var nodesById = nodes.ToDictionary(n => n.Id, StringComparer.Ordinal);
var routedEdgeCount = 0;
var skippedEdgeCount = 0;
var routedSectionCount = 0;
var fallbackSectionCount = 0;
var repairSet = repairPlan.EdgeIndices.ToHashSet();
var routeRepairEdgeIdSet = repairPlan.RouteRepairEdgeIds.ToHashSet(StringComparer.Ordinal);
var collectorRepairSet = repairPlan.Reasons.Contains("collector-corridors", StringComparer.Ordinal)
? repairSet
.Where(edgeIndex => edgeIndex >= 0
&& edgeIndex < existingEdges.Length
&& ElkEdgePostProcessor.IsRepeatCollectorLabel(existingEdges[edgeIndex].Label))
.Where(edgeIndex => !routeRepairEdgeIdSet.Contains(existingEdges[edgeIndex].Id))
.ToHashSet()
: [];
var preferredShortestEdgeIdSet = repairPlan.PreferredShortestEdgeIds.ToHashSet(StringComparer.Ordinal);
if (collectorRepairSet.Count > 0)
{
var collectorEdgeIds = collectorRepairSet
.Select(edgeIndex => existingEdges[edgeIndex].Id)
.ToArray();
routedEdges = ElkRepeatCollectorCorridors.SeparateSharedLanes(routedEdges, nodes, collectorEdgeIds);
routedEdgeCount += collectorRepairSet.Count;
}
var aStarRepairSet = repairSet
.Where(edgeIndex => !collectorRepairSet.Contains(edgeIndex))
.ToHashSet();
var spreadEndpoints = SpreadTargetEndpoints(existingEdges, nodesById, graphMinY, graphMaxY, strategy.MinLineClearance);
var softObstacles = new List<OrthogonalSoftObstacle>();
for (var edgeIndex = 0; edgeIndex < existingEdges.Length; edgeIndex++)
{
if (aStarRepairSet.Contains(edgeIndex))
{
continue;
}
foreach (var segment in ElkEdgeRoutingGeometry.FlattenSegments(routedEdges[edgeIndex]))
{
softObstacles.Add(new OrthogonalSoftObstacle(segment.Start, segment.End));
}
}
var orderedRepairIndices = strategy.EdgeOrder
.Where(aStarRepairSet.Contains)
.Concat(aStarRepairSet.Where(edgeIndex => !strategy.EdgeOrder.Contains(edgeIndex)))
.Distinct()
.ToArray();
var repairBuilderParallelism = (trustIndependentParallelBuilds || CanParallelizeRepairBuilds(orderedRepairIndices, existingEdges))
? DetermineRepairBuildParallelism(orderedRepairIndices.Length, maxParallelRepairBuilds)
: 1;
var builtRepairResults = new ConcurrentDictionary<int, RepairEdgeBuildResult>();
var repairBuildLocks = new ConcurrentDictionary<string, object>(StringComparer.Ordinal);
if (repairBuilderParallelism > 1 && orderedRepairIndices.Length > 1)
{
var immutableSoftObstacles = softObstacles.ToArray();
var parallelOptions = new ParallelOptions
{
CancellationToken = cancellationToken,
MaxDegreeOfParallelism = repairBuilderParallelism,
};
Parallel.ForEach(
orderedRepairIndices,
parallelOptions,
edgeIndex =>
{
if (edgeIndex < 0 || edgeIndex >= existingEdges.Length)
{
return;
}
var edge = existingEdges[edgeIndex];
var lockKeys = trustIndependentParallelBuilds
? []
: GetRepairBuildLockKeys(edge);
ExecuteWithRepairBuildLocks(
repairBuildLocks,
lockKeys,
() =>
{
builtRepairResults[edgeIndex] = BuildRepairEdgeResult(
edgeIndex,
existingEdges,
nodes,
obstacles,
spreadEndpoints,
nodesById,
immutableSoftObstacles,
routeRepairEdgeIdSet,
preferredShortestEdgeIdSet,
repairPlan.Reasons,
graphMinY,
graphMaxY,
strategy,
cancellationToken);
});
});
}
foreach (var edgeIndex in orderedRepairIndices)
{
cancellationToken.ThrowIfCancellationRequested();
if (edgeIndex < 0 || edgeIndex >= existingEdges.Length)
{
continue;
}
var buildResult = builtRepairResults.TryGetValue(edgeIndex, out var parallelBuildResult)
? parallelBuildResult
: BuildRepairEdgeResult(
edgeIndex,
existingEdges,
nodes,
obstacles,
spreadEndpoints,
nodesById,
softObstacles,
routeRepairEdgeIdSet,
preferredShortestEdgeIdSet,
repairPlan.Reasons,
graphMinY,
graphMaxY,
strategy,
cancellationToken);
if (buildResult.WasSkipped)
{
skippedEdgeCount++;
foreach (var segment in ElkEdgeRoutingGeometry.FlattenSegments(buildResult.Edge))
{
softObstacles.Add(new OrthogonalSoftObstacle(segment.Start, segment.End));
}
continue;
}
routedSectionCount += buildResult.RoutedSections;
fallbackSectionCount += buildResult.FallbackSections;
routedEdgeCount++;
routedEdges[edgeIndex] = buildResult.Edge;
foreach (var segment in ElkEdgeRoutingGeometry.FlattenSegments(routedEdges[edgeIndex]))
{
softObstacles.Add(new OrthogonalSoftObstacle(segment.Start, segment.End));
}
}
var repeatRouteRepairIds = repairPlan.RouteRepairEdgeIds
.Where(edgeId => routedEdges.Any(edge =>
string.Equals(edge.Id, edgeId, StringComparison.Ordinal)
&& ElkEdgePostProcessor.IsRepeatCollectorLabel(edge.Label)))
.ToArray();
if (repeatRouteRepairIds.Length > 0)
{
routedEdges = ElkRepeatCollectorCorridors.SeparateSharedLanes(routedEdges, nodes, repeatRouteRepairIds);
}
return new RouteAllEdgesResult(
routedEdges,
new ElkIterativeRouteDiagnostics
{
Mode = "local-repair",
TotalEdges = existingEdges.Length,
RoutedEdges = routedEdgeCount,
SkippedEdges = skippedEdgeCount,
RoutedSections = routedSectionCount,
FallbackSections = fallbackSectionCount,
SoftObstacleSegments = softObstacles.Count,
RepairedEdgeIds = repairPlan.EdgeIds,
RepairReasons = repairPlan.Reasons,
BuilderMode = repairBuilderParallelism > 1 ? "parallel-locked-local-build" : "sequential-local-build",
BuilderParallelism = repairBuilderParallelism,
});
}
}

View File

@@ -0,0 +1,215 @@
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Globalization;
namespace StellaOps.ElkSharp;
internal static partial class ElkEdgeRouterIterative
{
private static string[] ExpandRepeatCollectorRepairSet(
IReadOnlyCollection<string> selectedEdgeIds,
IReadOnlyCollection<ElkRoutedEdge> edges,
IReadOnlyCollection<ElkPositionedNode> nodes)
{
var selected = selectedEdgeIds.ToHashSet(StringComparer.Ordinal);
foreach (var group in ElkRepeatCollectorCorridors.DetectSharedLaneGroups(edges, nodes))
{
if (!group.EdgeIds.Any(selected.Contains))
{
continue;
}
foreach (var edgeId in group.EdgeIds)
{
selected.Add(edgeId);
}
}
return selected
.OrderBy(edgeId => edgeId, StringComparer.Ordinal)
.ToArray();
}
private static string[] ExpandTargetApproachJoinRepairSet(
IReadOnlyCollection<string> selectedEdgeIds,
IReadOnlyCollection<ElkRoutedEdge> edges,
IReadOnlyCollection<ElkPositionedNode> nodes,
double minLineClearance)
{
var selected = selectedEdgeIds.ToHashSet(StringComparer.Ordinal);
var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal);
var edgeArray = edges.ToArray();
foreach (var group in edgeArray.GroupBy(edge => edge.TargetNodeId ?? string.Empty, StringComparer.Ordinal))
{
if (string.IsNullOrWhiteSpace(group.Key)
|| !nodesById.TryGetValue(group.Key, out var targetNode))
{
continue;
}
var targetEdges = group.ToArray();
for (var i = 0; i < targetEdges.Length; i++)
{
var leftEdge = targetEdges[i];
var leftPath = ExtractPath(leftEdge);
if (leftPath.Count < 2)
{
continue;
}
var leftSide = ElkEdgeRoutingGeometry.ResolveBoundaryApproachSide(leftPath[^1], leftPath[^2], targetNode);
for (var j = i + 1; j < targetEdges.Length; j++)
{
var rightEdge = targetEdges[j];
var rightPath = ExtractPath(rightEdge);
if (rightPath.Count < 2)
{
continue;
}
var rightSide = ElkEdgeRoutingGeometry.ResolveBoundaryApproachSide(rightPath[^1], rightPath[^2], targetNode);
if (!string.Equals(leftSide, rightSide, StringComparison.Ordinal))
{
continue;
}
if (!HasTargetApproachJoinPair(leftPath, rightPath, minLineClearance))
{
continue;
}
selected.Add(leftEdge.Id);
selected.Add(rightEdge.Id);
}
}
}
return selected
.OrderBy(edgeId => edgeId, StringComparer.Ordinal)
.ToArray();
}
private static string[] ExpandSharedLaneRepairSet(
IReadOnlyCollection<string> selectedEdgeIds,
IReadOnlyCollection<ElkRoutedEdge> edges,
IReadOnlyCollection<ElkPositionedNode> nodes)
{
var selected = selectedEdgeIds.ToHashSet(StringComparer.Ordinal);
foreach (var (leftEdgeId, rightEdgeId) in ElkEdgeRoutingScoring.DetectSharedLaneConflicts(edges, nodes))
{
if (!selected.Contains(leftEdgeId) && !selected.Contains(rightEdgeId))
{
continue;
}
selected.Add(leftEdgeId);
selected.Add(rightEdgeId);
}
return selected
.OrderBy(edgeId => edgeId, StringComparer.Ordinal)
.ToArray();
}
private static string[] ExpandUnderNodeRepairSet(
IReadOnlyCollection<string> selectedEdgeIds,
IReadOnlyCollection<ElkRoutedEdge> edges,
IReadOnlyCollection<ElkPositionedNode> nodes)
{
var selected = selectedEdgeIds.ToHashSet(StringComparer.Ordinal);
foreach (var edge in edges)
{
if (ElkEdgeRoutingScoring.CountUnderNodeViolations([edge], nodes) > 0)
{
selected.Add(edge.Id);
}
}
return selected
.OrderBy(edgeId => edgeId, StringComparer.Ordinal)
.ToArray();
}
private static List<ElkPoint> ExtractPath(ElkRoutedEdge edge)
{
var path = new List<ElkPoint>();
foreach (var section in edge.Sections)
{
if (path.Count == 0)
{
path.Add(section.StartPoint);
}
path.AddRange(section.BendPoints);
path.Add(section.EndPoint);
}
return path;
}
private static bool HasTargetApproachJoinPair(
IReadOnlyList<ElkPoint> leftPath,
IReadOnlyList<ElkPoint> rightPath,
double minLineClearance,
int maxSegmentsFromEnd = 3)
{
var leftSegments = FlattenSegmentsNearEnd(leftPath, maxSegmentsFromEnd);
var rightSegments = FlattenSegmentsNearEnd(rightPath, maxSegmentsFromEnd);
foreach (var leftSegment in leftSegments)
{
foreach (var rightSegment in rightSegments)
{
if (!ElkEdgeRoutingGeometry.AreParallelAndClose(
leftSegment.Start,
leftSegment.End,
rightSegment.Start,
rightSegment.End,
minLineClearance))
{
continue;
}
var overlap = ElkEdgeRoutingGeometry.ComputeSharedSegmentLength(
leftSegment.Start,
leftSegment.End,
rightSegment.Start,
rightSegment.End);
if (overlap > 8d)
{
return true;
}
var leftLength = ElkEdgeRoutingGeometry.ComputeSegmentLength(leftSegment.Start, leftSegment.End);
var rightLength = ElkEdgeRoutingGeometry.ComputeSegmentLength(rightSegment.Start, rightSegment.End);
if (Math.Min(leftLength, rightLength) > 8d)
{
return true;
}
}
}
return false;
}
private static IReadOnlyList<RoutedEdgeSegment> FlattenSegmentsNearEnd(
IReadOnlyList<ElkPoint> path,
int maxSegmentsFromEnd)
{
if (path.Count < 2 || maxSegmentsFromEnd <= 0)
{
return [];
}
var startIndex = Math.Max(0, path.Count - (maxSegmentsFromEnd + 1));
var segments = new List<RoutedEdgeSegment>();
for (var i = startIndex; i < path.Count - 1; i++)
{
segments.Add(new RoutedEdgeSegment(string.Empty, path[i], path[i + 1]));
}
return segments;
}
}

View File

@@ -0,0 +1,373 @@
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Globalization;
namespace StellaOps.ElkSharp;
internal static partial class ElkEdgeRouterIterative
{
private static RepairPlan? BuildRepairPlan(
ElkRoutedEdge[] edges,
ElkPositionedNode[] nodes,
EdgeRoutingScore score,
RoutingRetryState retryState,
RoutingStrategy strategy,
int attempt)
{
if (edges.Length == 0)
{
return null;
}
var severityByEdgeId = new Dictionary<string, int>(StringComparer.Ordinal);
var preferredShortestEdgeIds = new HashSet<string>(StringComparer.Ordinal);
var routeRepairEdgeIds = new HashSet<string>(StringComparer.Ordinal);
var mandatoryEdgeIds = new HashSet<string>(StringComparer.Ordinal);
var severityByReason = new Dictionary<string, Dictionary<string, int>>(StringComparer.Ordinal);
var reasons = new List<string>();
var prioritizeBlockingAndLengthOnly = retryState.RequiresBlockingRetry || retryState.RequiresLengthRetry;
void AddReason(string reason)
{
if (!reasons.Contains(reason, StringComparer.Ordinal))
{
reasons.Add(reason);
}
}
void AddEdgeIds(
IEnumerable<string> edgeIds,
int severity,
string reason,
bool requiresRouteRepair = false,
bool mandatoryRepair = false)
{
AddReason(reason);
if (!severityByReason.TryGetValue(reason, out var reasonSeverity))
{
reasonSeverity = new Dictionary<string, int>(StringComparer.Ordinal);
severityByReason[reason] = reasonSeverity;
}
foreach (var edgeId in edgeIds)
{
severityByEdgeId[edgeId] = severityByEdgeId.GetValueOrDefault(edgeId) + severity;
reasonSeverity[edgeId] = reasonSeverity.GetValueOrDefault(edgeId) + severity;
if (requiresRouteRepair)
{
routeRepairEdgeIds.Add(edgeId);
}
if (mandatoryRepair)
{
mandatoryEdgeIds.Add(edgeId);
}
}
}
void MergeSeverity(
Dictionary<string, int> metricSeverity,
string reason,
bool preferShortestRepair = false,
bool requiresRouteRepair = false,
bool mandatoryRepair = false)
{
if (metricSeverity.Count == 0)
{
return;
}
AddReason(reason);
if (!severityByReason.TryGetValue(reason, out var reasonSeverity))
{
reasonSeverity = new Dictionary<string, int>(StringComparer.Ordinal);
severityByReason[reason] = reasonSeverity;
}
foreach (var (edgeId, severity) in metricSeverity)
{
severityByEdgeId[edgeId] = severityByEdgeId.GetValueOrDefault(edgeId) + severity;
reasonSeverity[edgeId] = reasonSeverity.GetValueOrDefault(edgeId) + severity;
if (preferShortestRepair || requiresRouteRepair)
{
routeRepairEdgeIds.Add(edgeId);
}
if (mandatoryRepair)
{
mandatoryEdgeIds.Add(edgeId);
}
if (preferShortestRepair)
{
preferredShortestEdgeIds.Add(edgeId);
}
}
}
if (attempt == 1 && retryState.RequiresLengthRetry && !retryState.RequiresBlockingRetry)
{
if (retryState.TargetApproachBacktrackingViolations > 0)
{
var backtrackingSeverity = new Dictionary<string, int>(StringComparer.Ordinal);
ElkEdgeRoutingScoring.CountTargetApproachBacktrackingViolations(edges, nodes, backtrackingSeverity, 2_250);
MergeSeverity(backtrackingSeverity, "approach-backtracking", preferShortestRepair: true, mandatoryRepair: true);
}
if (retryState.ExcessiveDetourViolations > 0)
{
var detourSeverity = new Dictionary<string, int>(StringComparer.Ordinal);
ElkEdgeRoutingScoring.CountExcessiveDetourViolations(edges, nodes, detourSeverity, 2_000);
MergeSeverity(detourSeverity, "detour-priority", preferShortestRepair: true, mandatoryRepair: true);
}
}
else
{
if (score.NodeCrossings > 0)
{
var nodeCrossingSeverity = new Dictionary<string, int>(StringComparer.Ordinal);
ElkEdgeRoutingScoring.CountEdgeNodeCrossings(edges, nodes, nodeCrossingSeverity, 2_500);
MergeSeverity(nodeCrossingSeverity, "node-crossings", requiresRouteRepair: true, mandatoryRepair: true);
}
if (retryState.RemainingShortHighways > 0)
{
var brokenHighways = ElkEdgeRouterHighway.DetectRemainingBrokenHighways(edges, nodes);
AddEdgeIds(
brokenHighways.SelectMany(highway => highway.EdgeIds).Distinct(StringComparer.Ordinal),
2_000,
"short-highways",
requiresRouteRepair: true,
mandatoryRepair: true);
}
if (retryState.RepeatCollectorCorridorViolations > 0)
{
var collectorSeverity = new Dictionary<string, int>(StringComparer.Ordinal);
ElkEdgeRoutingScoring.CountRepeatCollectorCorridorViolations(edges, nodes, collectorSeverity, 2_000);
MergeSeverity(collectorSeverity, "collector-corridors", requiresRouteRepair: true, mandatoryRepair: true);
}
if (retryState.RepeatCollectorNodeClearanceViolations > 0)
{
var collectorClearanceSeverity = new Dictionary<string, int>(StringComparer.Ordinal);
ElkEdgeRoutingScoring.CountRepeatCollectorNodeClearanceViolations(edges, nodes, collectorClearanceSeverity, 2_000);
MergeSeverity(collectorClearanceSeverity, "collector-clearance", requiresRouteRepair: true, mandatoryRepair: true);
}
if (retryState.TargetApproachJoinViolations > 0)
{
var targetJoinSeverity = new Dictionary<string, int>(StringComparer.Ordinal);
ElkEdgeRoutingScoring.CountTargetApproachJoinViolations(edges, nodes, targetJoinSeverity, 1_500);
MergeSeverity(targetJoinSeverity, "target-joins", requiresRouteRepair: true, mandatoryRepair: true);
}
if (retryState.TargetApproachBacktrackingViolations > 0)
{
var backtrackingSeverity = new Dictionary<string, int>(StringComparer.Ordinal);
ElkEdgeRoutingScoring.CountTargetApproachBacktrackingViolations(edges, nodes, backtrackingSeverity, 1_600);
MergeSeverity(backtrackingSeverity, "approach-backtracking", preferShortestRepair: true, requiresRouteRepair: true, mandatoryRepair: true);
}
if (retryState.BelowGraphViolations > 0)
{
var belowGraphSeverity = new Dictionary<string, int>(StringComparer.Ordinal);
ElkEdgeRoutingScoring.CountBelowGraphViolations(edges, nodes, belowGraphSeverity, 2_500);
MergeSeverity(belowGraphSeverity, "below-graph", preferShortestRepair: true, requiresRouteRepair: true, mandatoryRepair: true);
}
if (retryState.UnderNodeViolations > 0)
{
var underNodeSeverity = new Dictionary<string, int>(StringComparer.Ordinal);
ElkEdgeRoutingScoring.CountUnderNodeViolations(edges, nodes, underNodeSeverity, 2_500);
MergeSeverity(underNodeSeverity, "under-node", preferShortestRepair: true, requiresRouteRepair: true, mandatoryRepair: true);
}
if (retryState.LongDiagonalViolations > 0)
{
var longDiagonalSeverity = new Dictionary<string, int>(StringComparer.Ordinal);
ElkEdgeRoutingScoring.CountLongDiagonalViolations(edges, nodes, longDiagonalSeverity, 2_250);
MergeSeverity(longDiagonalSeverity, "long-diagonal", preferShortestRepair: true, requiresRouteRepair: true, mandatoryRepair: true);
}
if (retryState.ExcessiveDetourViolations > 0)
{
var detourSeverity = new Dictionary<string, int>(StringComparer.Ordinal);
ElkEdgeRoutingScoring.CountExcessiveDetourViolations(edges, nodes, detourSeverity, 1_250);
MergeSeverity(detourSeverity, "detour", preferShortestRepair: true, requiresRouteRepair: true, mandatoryRepair: true);
}
if (retryState.SharedLaneViolations > 0)
{
var sharedLaneSeverity = new Dictionary<string, int>(StringComparer.Ordinal);
ElkEdgeRoutingScoring.CountSharedLaneViolations(edges, nodes, sharedLaneSeverity, 2_000);
MergeSeverity(sharedLaneSeverity, "shared-lanes", requiresRouteRepair: true, mandatoryRepair: true);
}
if (retryState.BoundarySlotViolations > 0)
{
var boundarySlotSeverity = new Dictionary<string, int>(StringComparer.Ordinal);
ElkEdgeRoutingScoring.CountBoundarySlotViolations(edges, nodes, boundarySlotSeverity, 2_000);
MergeSeverity(boundarySlotSeverity, "boundary-slots", requiresRouteRepair: true, mandatoryRepair: true);
}
if (!prioritizeBlockingAndLengthOnly && retryState.ProximityViolations > 0)
{
var proximitySeverity = new Dictionary<string, int>(StringComparer.Ordinal);
ElkEdgeRoutingScoring.CountProximityViolations(edges, nodes, proximitySeverity, 350);
MergeSeverity(proximitySeverity, "proximity", requiresRouteRepair: true);
}
if (retryState.EntryAngleViolations > 0)
{
var entrySeverity = new Dictionary<string, int>(StringComparer.Ordinal);
ElkEdgeRoutingScoring.CountBadBoundaryAngles(edges, nodes, entrySeverity, 450);
MergeSeverity(entrySeverity, "entry", requiresRouteRepair: true, mandatoryRepair: true);
}
if (retryState.GatewaySourceExitViolations > 0)
{
var gatewaySourceSeverity = new Dictionary<string, int>(StringComparer.Ordinal);
ElkEdgeRoutingScoring.CountGatewaySourceExitViolations(edges, nodes, gatewaySourceSeverity, 2_500);
MergeSeverity(gatewaySourceSeverity, "gateway-source-exit", preferShortestRepair: true, requiresRouteRepair: true, mandatoryRepair: true);
}
if (!prioritizeBlockingAndLengthOnly && retryState.LabelProximityViolations > 0)
{
var labelSeverity = new Dictionary<string, int>(StringComparer.Ordinal);
ElkEdgeRoutingScoring.CountLabelProximityViolations(edges, nodes, labelSeverity, 300);
MergeSeverity(labelSeverity, "label", requiresRouteRepair: true);
}
if (!prioritizeBlockingAndLengthOnly && retryState.EdgeCrossings > 0)
{
var edgeCrossingSeverity = new Dictionary<string, int>(StringComparer.Ordinal);
ElkEdgeRoutingScoring.CountEdgeEdgeCrossings(edges, edgeCrossingSeverity, 200);
MergeSeverity(edgeCrossingSeverity, "edge-crossings", requiresRouteRepair: true);
}
}
if (severityByEdgeId.Count == 0)
{
return null;
}
var orderRankByEdgeId = strategy.EdgeOrder
.Select((edgeIndex, rank) => new { edgeIndex, rank })
.Where(item => item.edgeIndex >= 0 && item.edgeIndex < edges.Length)
.ToDictionary(item => edges[item.edgeIndex].Id, item => item.rank, StringComparer.Ordinal);
var seedSelectedEdgeIds = new List<string>();
foreach (var reason in reasons)
{
if (!severityByReason.TryGetValue(reason, out var reasonSeverity)
|| reasonSeverity.Count == 0)
{
continue;
}
var picksForReason = reason is "detour" or "detour-priority" or "approach-backtracking"
? 2
: 1;
var rankedReasonEdgeIds = reasonSeverity
.OrderByDescending(pair => pair.Value)
.ThenBy(pair => orderRankByEdgeId.GetValueOrDefault(pair.Key, int.MaxValue))
.ThenBy(pair => pair.Key, StringComparer.Ordinal)
.Select(pair => pair.Key)
.ToArray();
foreach (var edgeId in RotateOrderedEdgeIds(rankedReasonEdgeIds, attempt))
{
if (seedSelectedEdgeIds.Contains(edgeId, StringComparer.Ordinal))
{
continue;
}
seedSelectedEdgeIds.Add(edgeId);
if (--picksForReason == 0)
{
break;
}
}
}
var maxEdgeRepairs = DetermineRepairEdgeBudget(
retryState,
attempt == 1 && retryState.RequiresLengthRetry,
seedSelectedEdgeIds.Count,
mandatoryEdgeIds.Count);
var orderedEdgeIds = severityByEdgeId
.OrderByDescending(pair => pair.Value)
.ThenBy(pair => orderRankByEdgeId.GetValueOrDefault(pair.Key, int.MaxValue))
.ThenBy(pair => pair.Key, StringComparer.Ordinal)
.Select(pair => pair.Key)
.ToArray();
var prioritizedShortestEdgeIds = orderedEdgeIds
.Where(preferredShortestEdgeIds.Contains)
.Take(Math.Min(Math.Max(2, seedSelectedEdgeIds.Count), maxEdgeRepairs))
.ToArray();
var orderedMandatoryEdgeIds = orderedEdgeIds
.Where(mandatoryEdgeIds.Contains)
.ToArray();
var mandatoryFocusBudget = DetermineMandatoryFocusBudget(
orderedMandatoryEdgeIds.Length,
maxEdgeRepairs,
seedSelectedEdgeIds.Count);
var focusedMandatoryEdgeIds = RotateOrderedEdgeIds(orderedMandatoryEdgeIds, attempt)
.Take(mandatoryFocusBudget)
.ToArray();
var effectiveEdgeRepairBudget = Math.Max(maxEdgeRepairs, focusedMandatoryEdgeIds.Length);
var selectedEdgeIds = focusedMandatoryEdgeIds
.Concat(seedSelectedEdgeIds)
.Concat(prioritizedShortestEdgeIds)
.Distinct(StringComparer.Ordinal)
.Take(effectiveEdgeRepairBudget)
.ToArray();
if (reasons.Contains("collector-corridors", StringComparer.Ordinal))
{
selectedEdgeIds = ExpandRepeatCollectorRepairSet(selectedEdgeIds, edges, nodes);
}
if (reasons.Contains("target-joins", StringComparer.Ordinal))
{
selectedEdgeIds = ExpandTargetApproachJoinRepairSet(selectedEdgeIds, edges, nodes, strategy.MinLineClearance);
}
if (reasons.Contains("under-node", StringComparer.Ordinal))
{
selectedEdgeIds = ExpandUnderNodeRepairSet(selectedEdgeIds, edges, nodes);
}
if (reasons.Contains("shared-lanes", StringComparer.Ordinal))
{
selectedEdgeIds = ExpandSharedLaneRepairSet(selectedEdgeIds, edges, nodes);
}
if (selectedEdgeIds.Length == 0)
{
return null;
}
var preferredSelectedEdgeIds = selectedEdgeIds
.Where(preferredShortestEdgeIds.Contains)
.ToArray();
var routeRepairSelectedEdgeIds = selectedEdgeIds
.Where(routeRepairEdgeIds.Contains)
.ToArray();
var edgeIndices = selectedEdgeIds
.Select(edgeId => Array.FindIndex(edges, edge => string.Equals(edge.Id, edgeId, StringComparison.Ordinal)))
.Where(edgeIndex => edgeIndex >= 0)
.ToArray();
if (edgeIndices.Length == 0)
{
return null;
}
return new RepairPlan(
edgeIndices,
selectedEdgeIds,
preferredSelectedEdgeIds,
routeRepairSelectedEdgeIds,
reasons.ToArray());
}
}

View File

@@ -0,0 +1,134 @@
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Globalization;
namespace StellaOps.ElkSharp;
internal static partial class ElkEdgeRouterIterative
{
private static RouteAllEdgesResult? RouteAllEdges(
ElkRoutedEdge[] existingEdges,
ElkPositionedNode[] nodes,
double baseObstacleMargin,
RoutingStrategy strategy,
CancellationToken cancellationToken)
{
var routedEdges = new ElkRoutedEdge[existingEdges.Length];
Array.Copy(existingEdges, routedEdges, existingEdges.Length);
var obstacleMargin = Math.Max(
baseObstacleMargin,
Math.Max(strategy.MinLineClearance + 4d, strategy.RoutingParams.Margin));
var obstacles = BuildObstacles(nodes, obstacleMargin);
var graphMinY = nodes.Length > 0 ? nodes.Min(n => n.Y) : 0d;
var graphMaxY = nodes.Length > 0 ? nodes.Max(n => n.Y + n.Height) : 0d;
var nodesById = nodes.ToDictionary(n => n.Id, StringComparer.Ordinal);
var routedEdgeCount = 0;
var skippedEdgeCount = 0;
var routedSectionCount = 0;
var fallbackSectionCount = 0;
// Spread endpoints: distribute edges arriving at the same target side
var spreadEndpoints = SpreadTargetEndpoints(existingEdges, nodesById, graphMinY, graphMaxY, strategy.MinLineClearance);
var softObstacles = new List<OrthogonalSoftObstacle>();
foreach (var edgeIndex in strategy.EdgeOrder)
{
if (edgeIndex < 0 || edgeIndex >= existingEdges.Length)
{
continue;
}
cancellationToken.ThrowIfCancellationRequested();
var edge = existingEdges[edgeIndex];
// Skip edges that need special routing (backward, ports, corridors, collectors)
if (!CanRepairEdgeLocally(edge, nodes, graphMinY, graphMaxY))
{
skippedEdgeCount++;
foreach (var segment in ElkEdgeRoutingGeometry.FlattenSegments(edge))
{
softObstacles.Add(new OrthogonalSoftObstacle(segment.Start, segment.End));
}
continue;
}
var newSections = new List<ElkEdgeSection>(edge.Sections.Count);
foreach (var section in edge.Sections)
{
var endPoint = spreadEndpoints.TryGetValue(edge.Id, out var spread)
? spread
: section.EndPoint;
var (startPoint, adjustedEndPoint) = ResolveRoutingEndpoints(
section.StartPoint,
endPoint,
edge.SourceNodeId,
edge.TargetNodeId,
nodesById);
var rerouted = ElkEdgeRouterAStar8Dir.Route(
startPoint,
adjustedEndPoint,
obstacles,
edge.SourceNodeId ?? "",
edge.TargetNodeId ?? "",
strategy.RoutingParams,
softObstacles,
cancellationToken);
if (rerouted is not null && rerouted.Count >= 2)
{
routedSectionCount++;
newSections.Add(new ElkEdgeSection
{
StartPoint = rerouted[0],
EndPoint = rerouted[^1],
BendPoints = rerouted.Skip(1).Take(rerouted.Count - 2).ToArray(),
});
}
else
{
fallbackSectionCount++;
newSections.Add(section);
}
}
routedEdgeCount++;
routedEdges[edgeIndex] = new ElkRoutedEdge
{
Id = edge.Id,
SourceNodeId = edge.SourceNodeId,
TargetNodeId = edge.TargetNodeId,
SourcePortId = edge.SourcePortId,
TargetPortId = edge.TargetPortId,
Kind = edge.Kind,
Label = edge.Label,
Sections = newSections,
};
foreach (var segment in ElkEdgeRoutingGeometry.FlattenSegments(routedEdges[edgeIndex]))
{
softObstacles.Add(new OrthogonalSoftObstacle(segment.Start, segment.End));
}
}
return new RouteAllEdgesResult(
routedEdges,
new ElkIterativeRouteDiagnostics
{
Mode = "full-strategy",
TotalEdges = existingEdges.Length,
RoutedEdges = routedEdgeCount,
SkippedEdges = skippedEdgeCount,
RoutedSections = routedSectionCount,
FallbackSections = fallbackSectionCount,
SoftObstacleSegments = softObstacles.Count,
});
}
}

View File

@@ -0,0 +1,65 @@
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Globalization;
namespace StellaOps.ElkSharp;
internal static partial class ElkEdgeRouterIterative
{
private static (ElkRoutedEdge[] Edges, EdgeRoutingScore Score, RoutingRetryState RetryState, int RemainingBrokenHighways, ElkIterativeRouteDiagnostics RouteDiagnostics)? TryApplyVerifiedIssueRepairRound(
ElkRoutedEdge[] edges,
ElkPositionedNode[] nodes,
double baseObstacleMargin,
RoutingStrategy strategy,
RoutingRetryState retryState,
ElkLayoutDirection direction,
CancellationToken cancellationToken,
int maxParallelRepairBuilds,
bool trustIndependentParallelBuilds = false,
bool useHybridCleanup = false)
{
var score = ElkEdgeRoutingScoring.ComputeScore(edges, nodes);
var focusedPlan = BuildRepairPlan(
edges,
nodes,
score,
retryState,
strategy,
int.MaxValue);
if (focusedPlan is null || focusedPlan.Value.EdgeIds.Length == 0)
{
return null;
}
var reroutedResult = RepairPenalizedEdges(
edges,
nodes,
baseObstacleMargin,
strategy,
focusedPlan.Value,
cancellationToken,
maxParallelRepairBuilds,
trustIndependentParallelBuilds);
var rerouted = reroutedResult.Edges;
var cleaned = useHybridCleanup
? ApplyHybridTerminalRuleCleanupRound(
rerouted,
nodes,
direction,
strategy.MinLineClearance,
focusedPlan.Value.EdgeIds)
: ApplyTerminalRuleCleanupRound(
rerouted,
nodes,
direction,
strategy.MinLineClearance,
focusedPlan.Value.EdgeIds);
var cleanedScore = ElkEdgeRoutingScoring.ComputeScore(cleaned, nodes);
var remainingBrokenHighways = HighwayProcessingEnabled
? ElkEdgeRouterHighway.DetectRemainingBrokenHighways(cleaned, nodes).Count
: 0;
var cleanedRetryState = BuildRetryState(cleanedScore, remainingBrokenHighways);
return (cleaned, cleanedScore, cleanedRetryState, remainingBrokenHighways, reroutedResult.Diagnostics);
}
}

View File

@@ -0,0 +1,262 @@
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Globalization;
namespace StellaOps.ElkSharp;
internal static partial class ElkEdgeRouterIterative
{
private static CandidateSolution ApplyFinalBoundarySlotPolish(
CandidateSolution solution,
ElkPositionedNode[] nodes,
ElkLayoutDirection direction,
double minLineClearance,
int maxRounds = 3)
{
var current = solution;
for (var round = 0; round < maxRounds; round++)
{
var boundarySlotSeverity = new Dictionary<string, int>(StringComparer.Ordinal);
ElkEdgeRoutingScoring.CountBoundarySlotViolations(current.Edges, nodes, boundarySlotSeverity, 10);
if (boundarySlotSeverity.Count == 0)
{
break;
}
var batchedRootEdgeIds = boundarySlotSeverity
.OrderByDescending(pair => pair.Value)
.ThenBy(pair => pair.Key, StringComparer.Ordinal)
.Take(MaxWinnerPolishBatchedRootEdges)
.Select(pair => pair.Key)
.ToArray();
var batchedFocusEdgeIds = ExpandWinningSolutionFocus(current.Edges, batchedRootEdgeIds).ToArray();
if (batchedFocusEdgeIds.Length > 0)
{
var batchedCandidateEdges = BuildFinalBoundarySlotCandidate(
current.Edges,
nodes,
direction,
minLineClearance,
batchedFocusEdgeIds,
allowLateRestabilizedClosure: false);
if (TryPromoteFinalBoundarySlotCandidate(current, batchedCandidateEdges, nodes, out var batchedPromoted))
{
current = batchedPromoted;
continue;
}
}
var improved = false;
foreach (var edgeId in boundarySlotSeverity
.OrderByDescending(pair => pair.Value)
.ThenBy(pair => pair.Key, StringComparer.Ordinal)
.Select(pair => pair.Key))
{
var focusEdgeIds = ExpandWinningSolutionFocus(current.Edges, [edgeId]).ToArray();
if (focusEdgeIds.Length == 0)
{
continue;
}
var candidateEdges = BuildFinalBoundarySlotCandidate(
current.Edges,
nodes,
direction,
minLineClearance,
focusEdgeIds,
allowLateRestabilizedClosure: false);
if (!TryPromoteFinalBoundarySlotCandidate(current, candidateEdges, nodes, out var promoted))
{
continue;
}
current = promoted;
improved = true;
break;
}
if (!improved)
{
break;
}
}
return current;
}
private static CandidateSolution ApplyFinalPostSlotHardRulePolish(
CandidateSolution solution,
ElkPositionedNode[] nodes,
ElkLayoutDirection direction,
double minLineClearance,
int maxRounds = 3)
{
var current = solution;
for (var round = 0; round < maxRounds; round++)
{
var severityByEdgeId = new Dictionary<string, int>(StringComparer.Ordinal);
var pressure =
ElkEdgeRoutingScoring.CountBadBoundaryAngles(current.Edges, nodes, severityByEdgeId, 10)
+ ElkEdgeRoutingScoring.CountGatewaySourceExitViolations(current.Edges, nodes, severityByEdgeId, 10)
+ ElkEdgeRoutingScoring.CountTargetApproachJoinViolations(current.Edges, nodes, severityByEdgeId, 10)
+ ElkEdgeRoutingScoring.CountSharedLaneViolations(current.Edges, nodes, severityByEdgeId, 10)
+ ElkEdgeRoutingScoring.CountBoundarySlotViolations(current.Edges, nodes, severityByEdgeId, 10)
+ ElkEdgeRoutingScoring.CountTargetApproachBacktrackingViolations(current.Edges, nodes, severityByEdgeId, 10)
+ ElkEdgeRoutingScoring.CountExcessiveDetourViolations(current.Edges, nodes, severityByEdgeId, 10)
+ ElkEdgeRoutingScoring.CountBelowGraphViolations(current.Edges, nodes, severityByEdgeId, 10)
+ ElkEdgeRoutingScoring.CountUnderNodeViolations(current.Edges, nodes, severityByEdgeId, 10);
ElkLayoutDiagnostics.LogProgress(
$"Winner post-slot hard-rule round {round + 1} start: pressure={pressure} retry={DescribeRetryState(current.RetryState)} focus={severityByEdgeId.Count}");
if (pressure == 0)
{
break;
}
var preferFastTerminalOnly = ShouldPreferFastTerminalOnlyHardRuleClosure(current.RetryState);
var batchedRootEdgeIds = severityByEdgeId
.OrderByDescending(pair => pair.Value)
.ThenBy(pair => pair.Key, StringComparer.Ordinal)
.Take(MaxWinnerPolishBatchedRootEdges)
.Select(pair => pair.Key)
.ToArray();
var batchedFocusEdgeIds = preferFastTerminalOnly
? batchedRootEdgeIds
: ExpandWinningSolutionFocus(current.Edges, batchedRootEdgeIds).ToArray();
if (batchedFocusEdgeIds.Length > 0)
{
if (preferFastTerminalOnly)
{
ElkLayoutDiagnostics.LogProgress(
$"Winner post-slot hard-rule round {round + 1} fast-terminal focus=[{string.Join(", ", batchedFocusEdgeIds)}]");
var quickBatchedCandidateEdges = BuildFastTerminalOnlyHardRuleCandidate(
current.Edges,
nodes,
direction,
minLineClearance,
batchedFocusEdgeIds);
if (TryPromoteFinalHardRuleCandidate(current, quickBatchedCandidateEdges, nodes, out var quickBatchedPromoted))
{
current = quickBatchedPromoted;
continue;
}
var focusedTerminalClosureEdges = CloseRemainingTerminalViolations(
current.Edges,
nodes,
direction,
minLineClearance,
batchedFocusEdgeIds);
if (TryPromoteFinalHardRuleCandidate(current, focusedTerminalClosureEdges, nodes, out var focusedTerminalPromoted))
{
current = focusedTerminalPromoted;
continue;
}
var exactRestabilizedEdges = BuildFinalRestabilizedCandidate(
current.Edges,
nodes,
direction,
minLineClearance,
batchedFocusEdgeIds);
if (TryPromoteFinalHardRuleCandidate(current, exactRestabilizedEdges, nodes, out var exactRestabilizedPromoted))
{
current = exactRestabilizedPromoted;
continue;
}
var quickBatchedScore = ElkEdgeRoutingScoring.ComputeScore(quickBatchedCandidateEdges, nodes);
var quickBatchedRetryState = BuildRetryState(
quickBatchedScore,
HighwayProcessingEnabled
? ElkEdgeRouterHighway.DetectRemainingBrokenHighways(quickBatchedCandidateEdges, nodes).Count
: 0);
var changedFocusEdgeIds = batchedFocusEdgeIds
.Where(edgeId => HasEdgeGeometryChanged(current.Edges, quickBatchedCandidateEdges, edgeId))
.ToArray();
ElkLayoutDiagnostics.LogProgress(
$"Winner post-slot hard-rule round {round + 1} fast-terminal made no promotion: candidate={DescribeRetryState(quickBatchedRetryState)} changed=[{string.Join(", ", changedFocusEdgeIds)}]");
}
else
{
ElkLayoutDiagnostics.LogProgress(
$"Winner post-slot hard-rule round {round + 1} full-restabilize focus=[{string.Join(", ", batchedFocusEdgeIds)}]");
var batchedCandidateEdges = BuildFinalRestabilizedCandidate(
current.Edges,
nodes,
direction,
minLineClearance,
batchedFocusEdgeIds);
if (TryPromoteFinalHardRuleCandidate(current, batchedCandidateEdges, nodes, out var batchedPromoted))
{
current = batchedPromoted;
continue;
}
}
}
var orderedSeverityEdgeIds = severityByEdgeId
.OrderByDescending(pair => pair.Value)
.ThenBy(pair => pair.Key, StringComparer.Ordinal)
.Select(pair => pair.Key)
.ToArray();
var improved = false;
if (preferFastTerminalOnly)
{
var candidateEdgeIds = orderedSeverityEdgeIds
.Take(MaxWinnerPolishFastTerminalSingleEdgeCandidates)
.ToArray();
ElkLayoutDiagnostics.LogProgress(
$"Winner post-slot hard-rule round {round + 1} evaluating {candidateEdgeIds.Length}/{orderedSeverityEdgeIds.Length} fast-terminal single-edge candidates in parallel");
if (TryPromoteFastTerminalCandidates(
current,
nodes,
direction,
minLineClearance,
candidateEdgeIds,
out var parallelPromoted))
{
current = parallelPromoted;
improved = true;
}
}
else
{
foreach (var edgeId in orderedSeverityEdgeIds)
{
var focusEdgeIds = ExpandWinningSolutionFocus(current.Edges, [edgeId]).ToArray();
if (focusEdgeIds.Length == 0)
{
continue;
}
var candidateEdges = BuildFinalRestabilizedCandidate(
current.Edges,
nodes,
direction,
minLineClearance,
focusEdgeIds);
if (!TryPromoteFinalHardRuleCandidate(current, candidateEdges, nodes, out var promoted))
{
continue;
}
current = promoted;
improved = true;
break;
}
}
if (!improved)
{
break;
}
}
return current;
}
}

View File

@@ -0,0 +1,75 @@
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Globalization;
namespace StellaOps.ElkSharp;
internal static partial class ElkEdgeRouterIterative
{
private static CandidateSolution ApplyWinnerDetourPolish(
CandidateSolution solution,
ElkPositionedNode[] nodes,
double minLineClearance)
{
var focusSeverity = new Dictionary<string, int>(StringComparer.Ordinal);
ElkEdgeRoutingScoring.CountExcessiveDetourViolations(solution.Edges, nodes, focusSeverity, 10);
ElkEdgeRoutingScoring.CountGatewaySourceExitViolations(solution.Edges, nodes, focusSeverity, 10);
if (focusSeverity.Count > 0)
{
var batchedRootEdgeIds = focusSeverity
.OrderByDescending(pair => pair.Value)
.ThenBy(pair => pair.Key, StringComparer.Ordinal)
.Take(Math.Min(focusSeverity.Count, MaxWinnerPolishBatchedRootEdges + 2))
.Select(pair => pair.Key)
.ToArray();
var batchedFocusEdgeIds = ExpandWinningSolutionFocus(solution.Edges, batchedRootEdgeIds).ToArray();
if (batchedFocusEdgeIds.Length > 0)
{
var batchedCandidateEdges = ComposeTransactionalFinalDetourCandidate(
solution.Edges,
nodes,
minLineClearance,
batchedFocusEdgeIds);
batchedCandidateEdges = ChoosePreferredHardRuleLayout(solution.Edges, batchedCandidateEdges, nodes);
if (TryPromoteFinalDetourCandidate(
solution.Edges,
batchedCandidateEdges,
nodes,
solution.Score,
solution.RetryState,
out var batchedPromotedEdges))
{
var batchedPromotedScore = ElkEdgeRoutingScoring.ComputeScore(batchedPromotedEdges, nodes);
var batchedPromotedRetryState = BuildRetryState(
batchedPromotedScore,
HighwayProcessingEnabled
? ElkEdgeRouterHighway.DetectRemainingBrokenHighways(batchedPromotedEdges, nodes).Count
: 0);
solution = new CandidateSolution(
batchedPromotedScore,
batchedPromotedRetryState,
batchedPromotedEdges,
solution.StrategyIndex);
}
}
}
var candidateEdges = ApplyFinalDetourPolish(solution.Edges, nodes, minLineClearance, restrictedEdgeIds: null);
if (ReferenceEquals(candidateEdges, solution.Edges))
{
return solution;
}
var candidateScore = ElkEdgeRoutingScoring.ComputeScore(candidateEdges, nodes);
var candidateRetryState = BuildRetryState(
candidateScore,
HighwayProcessingEnabled
? ElkEdgeRouterHighway.DetectRemainingBrokenHighways(candidateEdges, nodes).Count
: 0);
return new CandidateSolution(candidateScore, candidateRetryState, candidateEdges, solution.StrategyIndex);
}
}

View File

@@ -0,0 +1,58 @@
namespace StellaOps.ElkSharp;
internal static partial class ElkEdgeRouterIterative
{
private static bool TryPromoteFastTerminalCandidates(
CandidateSolution current,
ElkPositionedNode[] nodes,
ElkLayoutDirection direction,
double minLineClearance,
IReadOnlyList<string> orderedEdgeIds,
out CandidateSolution promoted)
{
promoted = current;
if (orderedEdgeIds.Count == 0)
{
return false;
}
var candidatePromotions = new CandidateSolution?[orderedEdgeIds.Count];
var parallelOptions = new ParallelOptions
{
MaxDegreeOfParallelism = Math.Min(
orderedEdgeIds.Count,
Math.Max(1, Environment.ProcessorCount)),
};
Parallel.For(
0,
orderedEdgeIds.Count,
parallelOptions,
index =>
{
var candidateEdges = BuildFastTerminalOnlyHardRuleCandidate(
current.Edges,
nodes,
direction,
minLineClearance,
[orderedEdgeIds[index]]);
if (TryPromoteFinalHardRuleCandidate(current, candidateEdges, nodes, out var candidatePromoted))
{
candidatePromotions[index] = candidatePromoted;
}
});
for (var index = 0; index < candidatePromotions.Length; index++)
{
if (candidatePromotions[index] is not { } candidatePromoted)
{
continue;
}
promoted = candidatePromoted;
return true;
}
return false;
}
}

View File

@@ -0,0 +1,228 @@
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Globalization;
namespace StellaOps.ElkSharp;
internal static partial class ElkEdgeRouterIterative
{
internal static ElkRoutedEdge[] BuildFinalBoundarySlotCandidate(
ElkRoutedEdge[] edges,
ElkPositionedNode[] nodes,
ElkLayoutDirection direction,
double minLineClearance,
IReadOnlyCollection<string>? restrictedEdgeIds = null,
bool allowLateRestabilizedClosure = true)
{
var focusEdgeIds = restrictedEdgeIds?.Count > 0
? restrictedEdgeIds
: edges
.Select(edge => edge.Id)
.OrderBy(edgeId => edgeId, StringComparer.Ordinal)
.ToArray();
var useLeanRestrictedBoundarySlotPass =
restrictedEdgeIds?.Count > 0
&& (!allowLateRestabilizedClosure
|| restrictedEdgeIds.Count <= 2);
var useUltraLeanRestrictedBoundarySlotPass =
restrictedEdgeIds?.Count > 0
&& restrictedEdgeIds.Count <= MaxWinnerPolishBatchedRootEdges + 1;
ElkLayoutDiagnostics.LogProgress(
$"Boundary-slot candidate start: focus={focusEdgeIds.Count} allowLateRestabilizedClosure={allowLateRestabilizedClosure}");
var best = edges;
var candidate = ElkEdgePostProcessor.SnapBoundarySlotAssignments(
edges,
nodes,
minLineClearance,
restrictedEdgeIds,
enforceAllNodeEndpoints: true);
best = ChoosePreferredBoundarySlotRepairLayout(best, candidate, nodes);
ElkLayoutDiagnostics.LogProgress("Boundary-slot candidate after initial snap");
var terminalClosureCandidate = useLeanRestrictedBoundarySlotPass
? ApplyHybridTerminalRuleCleanupRound(
candidate,
nodes,
direction,
minLineClearance,
restrictedEdgeIds)
: CloseRemainingTerminalViolations(
candidate,
nodes,
direction,
minLineClearance,
restrictedEdgeIds);
candidate = ChoosePreferredBoundarySlotRepairLayout(candidate, terminalClosureCandidate, nodes);
best = ChoosePreferredBoundarySlotRepairLayout(best, candidate, nodes);
ElkLayoutDiagnostics.LogProgress("Boundary-slot candidate after terminal closure");
if (focusEdgeIds.Count > 0)
{
candidate = ElkEdgePostProcessor.SeparateMixedNodeFaceLaneConflicts(
candidate,
nodes,
minLineClearance,
focusEdgeIds);
candidate = ElkEdgePostProcessor.SeparateSharedLaneConflicts(
candidate,
nodes,
minLineClearance,
focusEdgeIds);
best = ChoosePreferredBoundarySlotRepairLayout(best, candidate, nodes);
ElkLayoutDiagnostics.LogProgress("Boundary-slot candidate after face/shared-lane separation");
}
candidate = ClampBelowGraphEdges(candidate, nodes, restrictedEdgeIds);
candidate = ElkEdgePostProcessor.NormalizeBoundaryAngles(candidate, nodes);
candidate = ElkEdgePostProcessor.NormalizeSourceExitAngles(candidate, nodes);
candidate = ElkEdgePostProcessor.SnapBoundarySlotAssignments(
candidate,
nodes,
minLineClearance,
restrictedEdgeIds,
enforceAllNodeEndpoints: true);
best = ChoosePreferredBoundarySlotRepairLayout(best, candidate, nodes);
ElkLayoutDiagnostics.LogProgress("Boundary-slot candidate after normalization snap");
if (useUltraLeanRestrictedBoundarySlotPass)
{
ElkLayoutDiagnostics.LogProgress("Boundary-slot candidate complete (ultra-lean restricted path)");
return ChoosePreferredBoundarySlotRepairLayout(best, candidate, nodes);
}
candidate = ApplyPostSlotDetourClosure(candidate, nodes, minLineClearance, restrictedEdgeIds);
best = ChoosePreferredBoundarySlotRepairLayout(best, candidate, nodes);
ElkLayoutDiagnostics.LogProgress("Boundary-slot candidate after detour closure");
candidate = ElkEdgePostProcessor.SnapBoundarySlotAssignments(
candidate,
nodes,
minLineClearance,
restrictedEdgeIds,
enforceAllNodeEndpoints: true);
best = ChoosePreferredBoundarySlotRepairLayout(best, candidate, nodes);
ElkLayoutDiagnostics.LogProgress("Boundary-slot candidate after detour snap");
if (useLeanRestrictedBoundarySlotPass)
{
ElkLayoutDiagnostics.LogProgress("Boundary-slot candidate complete (lean restricted path)");
return ChoosePreferredBoundarySlotRepairLayout(best, candidate, nodes);
}
if (restrictedEdgeIds?.Count > 0)
{
candidate = ChoosePreferredBoundarySlotRepairLayout(
candidate,
CloseRemainingTerminalViolations(candidate, nodes, direction, minLineClearance, restrictedEdgeIds),
nodes);
best = ChoosePreferredBoundarySlotRepairLayout(best, candidate, nodes);
candidate = ElkEdgePostProcessor.SnapBoundarySlotAssignments(
candidate,
nodes,
minLineClearance,
restrictedEdgeIds,
enforceAllNodeEndpoints: true);
best = ChoosePreferredBoundarySlotRepairLayout(best, candidate, nodes);
ElkLayoutDiagnostics.LogProgress("Boundary-slot candidate after restricted terminal recheck");
}
candidate = ApplyLateBoundarySlotRestabilization(
candidate,
nodes,
minLineClearance,
focusEdgeIds);
best = ChoosePreferredBoundarySlotRepairLayout(best, candidate, nodes);
ElkLayoutDiagnostics.LogProgress("Boundary-slot candidate after late restabilization");
candidate = ChoosePreferredBoundarySlotRepairLayout(
candidate,
ElkEdgePostProcessor.SnapBoundarySlotAssignments(
candidate,
nodes,
minLineClearance,
restrictedEdgeIds,
enforceAllNodeEndpoints: true),
nodes);
best = ChoosePreferredBoundarySlotRepairLayout(best, candidate, nodes);
ElkLayoutDiagnostics.LogProgress("Boundary-slot candidate after late restabilization snap");
if (focusEdgeIds.Count > 0)
{
var lateHardRuleCandidate = ApplyAggressiveSharedLaneClosure(
candidate,
nodes,
direction,
minLineClearance,
focusEdgeIds);
lateHardRuleCandidate = ElkEdgePostProcessor.SnapBoundarySlotAssignments(
lateHardRuleCandidate,
nodes,
minLineClearance,
restrictedEdgeIds,
enforceAllNodeEndpoints: true);
candidate = ChoosePreferredHardRuleLayout(candidate, lateHardRuleCandidate, nodes);
candidate = ChoosePreferredBoundarySlotRepairLayout(
candidate,
ElkEdgePostProcessor.SnapBoundarySlotAssignments(
candidate,
nodes,
minLineClearance,
restrictedEdgeIds,
enforceAllNodeEndpoints: true),
nodes);
best = ChoosePreferredBoundarySlotRepairLayout(best, candidate, nodes);
ElkLayoutDiagnostics.LogProgress("Boundary-slot candidate after late hard-rule closure");
}
if (focusEdgeIds.Count > 0
&& (ElkEdgeRoutingScoring.CountBoundarySlotViolations(candidate, nodes) > 0
|| ElkEdgeRoutingScoring.CountGatewaySourceExitViolations(candidate, nodes) > 0))
{
var lateClosureCandidate = ApplyLateFocusedBoundarySlotClosure(
candidate,
nodes,
direction,
minLineClearance,
focusEdgeIds,
restrictedEdgeIds);
candidate = ChoosePreferredBoundarySlotRepairLayout(candidate, lateClosureCandidate, nodes);
candidate = ChoosePreferredBoundarySlotRepairLayout(
candidate,
ElkEdgePostProcessor.SnapBoundarySlotAssignments(
candidate,
nodes,
minLineClearance,
restrictedEdgeIds,
enforceAllNodeEndpoints: true),
nodes);
best = ChoosePreferredBoundarySlotRepairLayout(best, candidate, nodes);
ElkLayoutDiagnostics.LogProgress("Boundary-slot candidate after focused late closure");
}
if (allowLateRestabilizedClosure
&& focusEdgeIds.Count > 0
&& focusEdgeIds.Count <= MaxLateRestabilizedClosureFocusEdges)
{
var stagedLateRestabilizedCandidate = ElkEdgePostProcessor.SnapBoundarySlotAssignments(
edges,
nodes,
minLineClearance,
restrictedEdgeIds,
enforceAllNodeEndpoints: true);
stagedLateRestabilizedCandidate = BuildFinalRestabilizedCandidate(
stagedLateRestabilizedCandidate,
nodes,
direction,
minLineClearance,
restrictedEdgeIds);
stagedLateRestabilizedCandidate = ChoosePreferredBoundarySlotRepairLayout(
stagedLateRestabilizedCandidate,
ElkEdgePostProcessor.SnapBoundarySlotAssignments(
stagedLateRestabilizedCandidate,
nodes,
minLineClearance,
restrictedEdgeIds,
enforceAllNodeEndpoints: true),
nodes);
candidate = ChoosePreferredBoundarySlotRepairLayout(candidate, stagedLateRestabilizedCandidate, nodes);
best = ChoosePreferredBoundarySlotRepairLayout(best, candidate, nodes);
ElkLayoutDiagnostics.LogProgress("Boundary-slot candidate after staged late restabilized closure");
}
ElkLayoutDiagnostics.LogProgress("Boundary-slot candidate complete");
return ChoosePreferredBoundarySlotRepairLayout(best, candidate, nodes);
}
}

View File

@@ -0,0 +1,213 @@
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Globalization;
namespace StellaOps.ElkSharp;
internal static partial class ElkEdgeRouterIterative
{
internal static ElkRoutedEdge[] BuildFinalRestabilizedCandidate(
ElkRoutedEdge[] edges,
ElkPositionedNode[] nodes,
ElkLayoutDirection direction,
double minLineClearance,
IReadOnlyCollection<string>? restrictedEdgeIds = null)
{
var focusEdgeIds = restrictedEdgeIds?.Count > 0
? restrictedEdgeIds
: edges
.Select(edge => edge.Id)
.OrderBy(edgeId => edgeId, StringComparer.Ordinal)
.ToArray();
var focusEdgeSet = focusEdgeIds.ToHashSet(StringComparer.Ordinal);
var candidate = ChoosePreferredHardRuleLayout(
edges,
CloseRemainingTerminalViolations(edges, nodes, direction, minLineClearance, restrictedEdgeIds),
nodes);
if (focusEdgeIds.Count > 0)
{
candidate = ChoosePreferredHardRuleLayout(
candidate,
ApplyAggressiveSharedLaneClosure(
candidate,
nodes,
direction,
minLineClearance,
focusEdgeIds),
nodes);
}
candidate = BuildFinalBoundarySlotCandidate(
candidate,
nodes,
direction,
minLineClearance,
restrictedEdgeIds,
allowLateRestabilizedClosure: false);
if (focusEdgeIds.Count > 0)
{
candidate = ChoosePreferredHardRuleLayout(
candidate,
ApplyAggressiveSharedLaneClosure(
candidate,
nodes,
direction,
minLineClearance,
focusEdgeIds),
nodes);
}
candidate = ChoosePreferredBoundarySlotRepairLayout(
candidate,
ElkEdgePostProcessor.SnapBoundarySlotAssignments(
candidate,
nodes,
minLineClearance,
restrictedEdgeIds,
enforceAllNodeEndpoints: true),
nodes);
candidate = ChoosePreferredBoundarySlotRepairLayout(
candidate,
CloseRemainingTerminalViolations(candidate, nodes, direction, minLineClearance, restrictedEdgeIds),
nodes);
if (focusEdgeIds.Count > 0)
{
var lateHardRuleCandidate = ApplyAggressiveSharedLaneClosure(
candidate,
nodes,
direction,
minLineClearance,
focusEdgeIds);
lateHardRuleCandidate = CloseRemainingTerminalViolations(
lateHardRuleCandidate,
nodes,
direction,
minLineClearance,
focusEdgeIds);
candidate = ChoosePreferredHardRuleLayout(candidate, lateHardRuleCandidate, nodes);
var remainingBadAngles = new Dictionary<string, int>(StringComparer.Ordinal);
ElkEdgeRoutingScoring.CountBadBoundaryAngles(candidate, nodes, remainingBadAngles, 10);
var remainingTerminalFocus = ExpandWinningSolutionFocus(
candidate,
remainingBadAngles.Keys.Where(focusEdgeSet.Contains))
.Where(focusEdgeSet.Contains)
.OrderBy(edgeId => edgeId, StringComparer.Ordinal)
.ToArray();
if (remainingTerminalFocus.Length > 0)
{
var terminalFocus = (IReadOnlyCollection<string>)remainingTerminalFocus;
var lateTerminalCandidate = ElkEdgePostProcessor.RepairBoundaryAnglesAndTargetApproaches(
candidate,
nodes,
minLineClearance,
terminalFocus);
lateTerminalCandidate = BuildFinalBoundarySlotCandidate(
lateTerminalCandidate,
nodes,
direction,
minLineClearance,
terminalFocus,
allowLateRestabilizedClosure: false);
lateTerminalCandidate = CloseRemainingTerminalViolations(
lateTerminalCandidate,
nodes,
direction,
minLineClearance,
terminalFocus);
candidate = ChoosePreferredBoundarySlotRepairLayout(candidate, lateTerminalCandidate, nodes);
}
}
candidate = ChoosePreferredBoundarySlotRepairLayout(
candidate,
ElkEdgePostProcessor.SnapBoundarySlotAssignments(
candidate,
nodes,
minLineClearance,
restrictedEdgeIds,
enforceAllNodeEndpoints: true),
nodes);
candidate = ChoosePreferredBoundarySlotRepairLayout(
candidate,
ApplyPostSlotDetourClosure(candidate, nodes, minLineClearance, restrictedEdgeIds),
nodes);
candidate = ChoosePreferredBoundarySlotRepairLayout(
candidate,
ElkEdgePostProcessor.SnapBoundarySlotAssignments(
candidate,
nodes,
minLineClearance,
restrictedEdgeIds,
enforceAllNodeEndpoints: true),
nodes);
candidate = ApplyLateBoundarySlotRestabilization(
candidate,
nodes,
minLineClearance,
focusEdgeIds);
candidate = ChoosePreferredBoundarySlotRepairLayout(
candidate,
ElkEdgePostProcessor.SnapBoundarySlotAssignments(
candidate,
nodes,
minLineClearance,
restrictedEdgeIds,
enforceAllNodeEndpoints: true),
nodes);
if (focusEdgeIds.Count > 0)
{
var finalSharedLaneCandidate = ElkEdgePostProcessor.SeparateSharedLaneConflicts(
candidate,
nodes,
minLineClearance,
focusEdgeIds);
candidate = ChoosePreferredSharedLanePolishLayout(candidate, finalSharedLaneCandidate, nodes);
var finalSourceJoinCandidate = ElkEdgePostProcessor.SpreadSourceDepartureJoins(
finalSharedLaneCandidate,
nodes,
minLineClearance,
focusEdgeIds);
candidate = ChoosePreferredSharedLanePolishLayout(candidate, finalSourceJoinCandidate, nodes);
var finalTargetJoinCandidate = ElkEdgePostProcessor.SpreadTargetApproachJoins(
finalSourceJoinCandidate,
nodes,
minLineClearance,
focusEdgeIds,
forceOutwardAxisSpacing: true);
candidate = ChoosePreferredSharedLanePolishLayout(candidate, finalTargetJoinCandidate, nodes);
var finalAggressiveCandidate = ApplyAggressiveSharedLaneClosure(
candidate,
nodes,
direction,
minLineClearance,
focusEdgeIds);
candidate = ChoosePreferredSharedLanePolishLayout(candidate, finalAggressiveCandidate, nodes);
var forcedFinalSharedLaneCandidate = ElkEdgePostProcessor.SeparateSharedLaneConflicts(
candidate,
nodes,
minLineClearance,
focusEdgeIds);
var baselineSharedLaneViolations = ElkEdgeRoutingScoring.CountSharedLaneViolations(candidate, nodes);
var forcedSharedLaneViolations = ElkEdgeRoutingScoring.CountSharedLaneViolations(forcedFinalSharedLaneCandidate, nodes);
if (forcedSharedLaneViolations < baselineSharedLaneViolations)
{
var baselineScore = ElkEdgeRoutingScoring.ComputeScore(candidate, nodes);
var forcedScore = ElkEdgeRoutingScoring.ComputeScore(forcedFinalSharedLaneCandidate, nodes);
if (forcedScore.NodeCrossings <= baselineScore.NodeCrossings)
{
candidate = forcedFinalSharedLaneCandidate;
}
}
}
return candidate;
}
}

View File

@@ -0,0 +1,127 @@
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Globalization;
namespace StellaOps.ElkSharp;
internal static partial class ElkEdgeRouterIterative
{
private static IEnumerable<string> ExpandWinningSolutionFocus(
IReadOnlyCollection<ElkRoutedEdge> edges,
IEnumerable<string> focusEdgeIds)
{
var edgesById = edges.ToDictionary(edge => edge.Id, StringComparer.Ordinal);
var expanded = new HashSet<string>(StringComparer.Ordinal);
foreach (var edgeId in focusEdgeIds)
{
if (!expanded.Add(edgeId) || !edgesById.TryGetValue(edgeId, out var edge))
{
continue;
}
foreach (var peer in edges)
{
if (string.Equals(peer.Id, edge.Id, StringComparison.Ordinal))
{
continue;
}
if (string.Equals(peer.SourceNodeId, edge.SourceNodeId, StringComparison.Ordinal)
|| string.Equals(peer.TargetNodeId, edge.TargetNodeId, StringComparison.Ordinal)
|| string.Equals(peer.SourceNodeId, edge.TargetNodeId, StringComparison.Ordinal)
|| string.Equals(peer.TargetNodeId, edge.SourceNodeId, StringComparison.Ordinal))
{
expanded.Add(peer.Id);
}
}
}
return expanded.OrderBy(edgeId => edgeId, StringComparer.Ordinal);
}
private static IEnumerable<string> ExpandSharedLanePolishFocus(
IReadOnlyCollection<ElkRoutedEdge> edges,
IReadOnlyCollection<ElkPositionedNode> nodes,
string focusEdgeId)
{
var edgesById = edges.ToDictionary(edge => edge.Id, StringComparer.Ordinal);
if (!edgesById.TryGetValue(focusEdgeId, out var focusEdge))
{
return [];
}
var focusedEdgeIds = new HashSet<string>(StringComparer.Ordinal)
{
focusEdgeId,
};
var sharedNodeIds = new HashSet<string>(StringComparer.Ordinal);
foreach (var (leftEdgeId, rightEdgeId) in ElkEdgeRoutingScoring.DetectSharedLaneConflicts(edges, nodes))
{
string partnerEdgeId;
if (string.Equals(leftEdgeId, focusEdgeId, StringComparison.Ordinal))
{
partnerEdgeId = rightEdgeId;
}
else if (string.Equals(rightEdgeId, focusEdgeId, StringComparison.Ordinal))
{
partnerEdgeId = leftEdgeId;
}
else
{
continue;
}
if (!edgesById.TryGetValue(partnerEdgeId, out var partnerEdge))
{
continue;
}
focusedEdgeIds.Add(partnerEdgeId);
CollectSharedConflictNodeIds(focusEdge, partnerEdge, sharedNodeIds);
}
if (sharedNodeIds.Count == 0)
{
return ExpandWinningSolutionFocus(edges, [focusEdgeId]);
}
foreach (var edge in edges)
{
if (focusedEdgeIds.Contains(edge.Id))
{
continue;
}
if ((edge.SourceNodeId is not null && sharedNodeIds.Contains(edge.SourceNodeId))
|| (edge.TargetNodeId is not null && sharedNodeIds.Contains(edge.TargetNodeId)))
{
focusedEdgeIds.Add(edge.Id);
}
}
return focusedEdgeIds.OrderBy(edgeId => edgeId, StringComparer.Ordinal);
}
private static void CollectSharedConflictNodeIds(
ElkRoutedEdge edge,
ElkRoutedEdge partner,
ISet<string> sharedNodeIds)
{
if (edge.SourceNodeId is not null
&& (string.Equals(edge.SourceNodeId, partner.SourceNodeId, StringComparison.Ordinal)
|| string.Equals(edge.SourceNodeId, partner.TargetNodeId, StringComparison.Ordinal)))
{
sharedNodeIds.Add(edge.SourceNodeId);
}
if (edge.TargetNodeId is not null
&& (string.Equals(edge.TargetNodeId, partner.SourceNodeId, StringComparison.Ordinal)
|| string.Equals(edge.TargetNodeId, partner.TargetNodeId, StringComparison.Ordinal)))
{
sharedNodeIds.Add(edge.TargetNodeId);
}
}
}

View File

@@ -0,0 +1,86 @@
namespace StellaOps.ElkSharp;
internal static partial class ElkEdgeRouterIterative
{
private static CandidateSolution RefineHybridWinningSolution(
CandidateSolution best,
ElkPositionedNode[] nodes,
ElkLayoutDirection direction,
double minLineClearance,
bool preferLowWaveRuntimePolish = false)
{
static string DescribeSolution(CandidateSolution solution)
{
return $"score={solution.Score.Value:F0} retry={DescribeRetryState(solution.RetryState)}";
}
var current = best;
ElkLayoutDiagnostics.LogProgress($"Hybrid winner refinement start: {DescribeSolution(current)}");
if (current.RetryState.UnderNodeViolations > 0)
{
current = ApplyFinalDirectUnderNodePolish(current, nodes, minLineClearance);
ElkLayoutDiagnostics.LogProgress($"Hybrid winner refinement after under-node polish: {DescribeSolution(current)}");
}
if (current.RetryState.UnderNodeViolations > 0
|| current.RetryState.TargetApproachJoinViolations > 0)
{
current = ApplyFinalProtectedLocalBundlePolish(current, nodes, minLineClearance);
ElkLayoutDiagnostics.LogProgress($"Hybrid winner refinement after local-bundle polish: {DescribeSolution(current)}");
}
if (current.RetryState.SharedLaneViolations > 0
|| current.RetryState.TargetApproachJoinViolations > 0)
{
current = ApplyFinalSharedLanePolish(
current,
nodes,
direction,
minLineClearance,
preferLeanTerminalCleanup: preferLowWaveRuntimePolish);
ElkLayoutDiagnostics.LogProgress($"Hybrid winner refinement after shared-lane polish: {DescribeSolution(current)}");
}
if (current.RetryState.BoundarySlotViolations > 0
|| current.RetryState.GatewaySourceExitViolations > 0
|| current.RetryState.EntryAngleViolations > 0)
{
current = ApplyFinalBoundarySlotPolish(current, nodes, direction, minLineClearance, maxRounds: 1);
ElkLayoutDiagnostics.LogProgress($"Hybrid winner refinement after boundary-slot polish: {DescribeSolution(current)}");
}
if (current.RetryState.ExcessiveDetourViolations > 0
|| (!preferLowWaveRuntimePolish && current.RetryState.GatewaySourceExitViolations > 0))
{
current = ApplyWinnerDetourPolish(current, nodes, minLineClearance);
ElkLayoutDiagnostics.LogProgress($"Hybrid winner refinement after detour polish: {DescribeSolution(current)}");
}
if (HasHybridHardRulePressure(current.RetryState))
{
current = preferLowWaveRuntimePolish
? ApplyHybridLeanPostSlotHardRulePolish(current, nodes, direction, minLineClearance)
: ApplyFinalPostSlotHardRulePolish(current, nodes, direction, minLineClearance, maxRounds: 1);
ElkLayoutDiagnostics.LogProgress($"Hybrid winner refinement after post-slot hard-rule polish: {DescribeSolution(current)}");
}
return current;
}
private static bool HasHybridHardRulePressure(RoutingRetryState retryState)
{
return retryState.RemainingShortHighways > 0
|| retryState.RepeatCollectorCorridorViolations > 0
|| retryState.RepeatCollectorNodeClearanceViolations > 0
|| retryState.TargetApproachJoinViolations > 0
|| retryState.TargetApproachBacktrackingViolations > 0
|| retryState.ExcessiveDetourViolations > 0
|| retryState.SharedLaneViolations > 0
|| retryState.BoundarySlotViolations > 0
|| retryState.BelowGraphViolations > 0
|| retryState.UnderNodeViolations > 0
|| retryState.EntryAngleViolations > 0
|| retryState.GatewaySourceExitViolations > 0;
}
}

View File

@@ -0,0 +1,664 @@
namespace StellaOps.ElkSharp;
internal static partial class ElkEdgeRouterIterative
{
private static ElkRoutedEdge[] ApplyHybridFocusedResidualTerminalRecovery(
ElkRoutedEdge[] edges,
ElkPositionedNode[] nodes,
ElkLayoutDirection direction,
double minLineClearance)
{
var boundarySlotSeverity = new Dictionary<string, int>(StringComparer.Ordinal);
var backtrackingSeverity = new Dictionary<string, int>(StringComparer.Ordinal);
var entrySeverity = new Dictionary<string, int>(StringComparer.Ordinal);
var boundaryAngleSeverity = new Dictionary<string, int>(StringComparer.Ordinal);
var gatewaySourceSeverity = new Dictionary<string, int>(StringComparer.Ordinal);
var targetJoinSeverity = new Dictionary<string, int>(StringComparer.Ordinal);
var severityByEdgeId = new Dictionary<string, int>(StringComparer.Ordinal);
var pressure =
ElkEdgeRoutingScoring.CountBoundarySlotViolations(edges, nodes, severityByEdgeId, 40)
+ ElkEdgeRoutingScoring.CountTargetApproachBacktrackingViolations(edges, nodes, severityByEdgeId, 40)
+ ElkEdgeRoutingScoring.CountBadEntryAngles(edges, nodes, severityByEdgeId, 20)
+ ElkEdgeRoutingScoring.CountBadBoundaryAngles(edges, nodes, severityByEdgeId, 10)
+ ElkEdgeRoutingScoring.CountGatewaySourceExitViolations(edges, nodes, severityByEdgeId, 10)
+ ElkEdgeRoutingScoring.CountTargetApproachJoinViolations(edges, nodes, severityByEdgeId, 10);
ElkEdgeRoutingScoring.CountBoundarySlotViolations(edges, nodes, boundarySlotSeverity, 40);
ElkEdgeRoutingScoring.CountTargetApproachBacktrackingViolations(edges, nodes, backtrackingSeverity, 40);
ElkEdgeRoutingScoring.CountBadEntryAngles(edges, nodes, entrySeverity, 20);
ElkEdgeRoutingScoring.CountBadBoundaryAngles(edges, nodes, boundaryAngleSeverity, 10);
ElkEdgeRoutingScoring.CountGatewaySourceExitViolations(edges, nodes, gatewaySourceSeverity, 10);
ElkEdgeRoutingScoring.CountTargetApproachJoinViolations(edges, nodes, targetJoinSeverity, 10);
if (pressure == 0 || severityByEdgeId.Count == 0)
{
return edges;
}
var boundaryRootEdgeIds = BuildHybridResidualCategoryRootEdgeIds(boundarySlotSeverity, gatewaySourceSeverity);
var approachRootEdgeIds = BuildHybridResidualApproachRootEdgeIds(
backtrackingSeverity,
entrySeverity,
boundaryAngleSeverity,
targetJoinSeverity);
var boundaryFocusEdgeIds = ExpandHybridResidualBoundaryFocus(edges, boundaryRootEdgeIds);
var rootEdgeIds = boundaryRootEdgeIds
.Concat(approachRootEdgeIds)
.Distinct(StringComparer.Ordinal)
.OrderBy(edgeId => edgeId, StringComparer.Ordinal)
.ToArray();
if (boundaryFocusEdgeIds.Length == 0 && approachRootEdgeIds.Length == 0)
{
return edges;
}
ElkLayoutDiagnostics.LogProgress(
$"Hybrid low-wave residual recovery start: roots=[{string.Join(", ", rootEdgeIds)}] boundary-focus={boundaryFocusEdgeIds.Length} approach-roots={approachRootEdgeIds.Length}");
var candidate = edges;
if (boundaryFocusEdgeIds.Length > 0)
{
candidate = ChoosePreferredBoundarySlotRepairLayout(
candidate,
BuildFinalBoundarySlotCandidate(
candidate,
nodes,
direction,
minLineClearance,
boundaryFocusEdgeIds,
allowLateRestabilizedClosure: false),
nodes);
if (boundaryRootEdgeIds.Length > 0
&& (ElkEdgeRoutingScoring.CountBoundarySlotViolations(candidate, nodes) > 0
|| ElkEdgeRoutingScoring.CountGatewaySourceExitViolations(candidate, nodes) > 0))
{
foreach (var boundaryRootEdgeId in boundaryRootEdgeIds)
{
var exactBoundaryFocusEdgeIds = ExpandHybridResidualBoundaryFocus(candidate, [boundaryRootEdgeId]);
if (exactBoundaryFocusEdgeIds.Length == 0)
{
continue;
}
ElkLayoutDiagnostics.LogProgress(
$"Hybrid residual boundary root {boundaryRootEdgeId}: focus=[{string.Join(", ", exactBoundaryFocusEdgeIds)}]");
candidate = ChoosePreferredBoundarySlotRepairLayout(
candidate,
BuildFinalBoundarySlotCandidate(
BuildHybridResidualSingleEdgeRepairCandidate(
candidate,
nodes,
minLineClearance,
boundaryRootEdgeId,
["boundary-slots", "gateway-source-exit"]),
nodes,
direction,
minLineClearance,
exactBoundaryFocusEdgeIds,
allowLateRestabilizedClosure: false),
nodes);
if (ElkEdgeRoutingScoring.CountBoundarySlotViolations(candidate, nodes) == 0
&& ElkEdgeRoutingScoring.CountGatewaySourceExitViolations(candidate, nodes) == 0)
{
break;
}
}
}
}
foreach (var approachRootEdgeId in approachRootEdgeIds)
{
if (boundaryRootEdgeIds.Contains(approachRootEdgeId, StringComparer.Ordinal))
{
continue;
}
ElkLayoutDiagnostics.LogProgress(
$"Hybrid residual approach root {approachRootEdgeId}: focus=[{approachRootEdgeId}]");
var singleEdgeCandidate = BuildHybridResidualSingleEdgeRepairCandidate(
candidate,
nodes,
minLineClearance,
approachRootEdgeId,
["approach-backtracking", "entry"]);
candidate = ChoosePreferredFocusedApproachRepairLayout(
candidate,
singleEdgeCandidate,
nodes,
approachRootEdgeId);
if (!HasFocusedApproachResidualViolations(candidate, nodes, approachRootEdgeId))
{
continue;
}
if (IsGatewayTargetEdge(candidate, nodes, approachRootEdgeId))
{
continue;
}
candidate = ChoosePreferredFocusedApproachRepairLayout(
candidate,
BuildHybridResidualApproachCandidate(
candidate,
nodes,
minLineClearance,
[approachRootEdgeId]),
nodes,
approachRootEdgeId);
if (ElkEdgeRoutingScoring.CountTargetApproachBacktrackingViolations(candidate, nodes) == 0
&& ElkEdgeRoutingScoring.CountBadEntryAngles(candidate, nodes) == 0
&& ElkEdgeRoutingScoring.CountBadBoundaryAngles(candidate, nodes) == 0
&& ElkEdgeRoutingScoring.CountTargetApproachJoinViolations(candidate, nodes) == 0)
{
break;
}
}
if (boundaryFocusEdgeIds.Length > 0
&& ElkEdgeRoutingScoring.CountBoundarySlotViolations(candidate, nodes) > 0)
{
candidate = ChoosePreferredBoundarySlotRepairLayout(
candidate,
ElkEdgePostProcessor.SnapBoundarySlotAssignments(
candidate,
nodes,
minLineClearance,
boundaryFocusEdgeIds,
enforceAllNodeEndpoints: true),
nodes);
}
return ChoosePreferredBoundarySlotRepairLayout(edges, candidate, nodes);
}
private static ElkRoutedEdge[] BuildHybridResidualApproachCandidate(
ElkRoutedEdge[] edges,
ElkPositionedNode[] nodes,
double minLineClearance,
string[] focusEdgeIds)
{
var candidate = ElkEdgePostProcessor.PreferShortestBoundaryShortcuts(edges, nodes, focusEdgeIds);
candidate = ElkEdgePostProcessor.RepairBoundaryAnglesAndTargetApproaches(candidate, nodes, minLineClearance, focusEdgeIds);
candidate = ElkEdgePostProcessor.SpreadTargetApproachJoins(candidate, nodes, minLineClearance, focusEdgeIds);
candidate = ElkEdgePostProcessor.SpreadRectTargetApproachFeederBands(candidate, nodes, minLineClearance, focusEdgeIds);
candidate = ElkEdgePostProcessor.FinalizeDecisionTargetEntries(candidate, nodes, focusEdgeIds);
candidate = ElkEdgePostProcessor.FinalizeGatewayBoundaryGeometry(candidate, nodes, focusEdgeIds);
candidate = ApplyPostSlotDetourClosure(candidate, nodes, minLineClearance, focusEdgeIds);
candidate = ElkEdgePostProcessor.NormalizeBoundaryAngles(candidate, nodes);
candidate = ElkEdgePostProcessor.NormalizeSourceExitAngles(candidate, nodes);
return candidate;
}
private static ElkRoutedEdge[] BuildHybridResidualSingleEdgeRepairCandidate(
ElkRoutedEdge[] edges,
ElkPositionedNode[] nodes,
double minLineClearance,
string edgeId,
IReadOnlyCollection<string> repairReasons)
{
var edgeIndex = Array.FindIndex(edges, edge => string.Equals(edge.Id, edgeId, StringComparison.Ordinal));
if (edgeIndex < 0)
{
return edges;
}
var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal);
var graphMinY = nodes.Length > 0 ? nodes.Min(node => node.Y) : 0d;
var graphMaxY = nodes.Length > 0 ? nodes.Max(node => node.Y + node.Height) : 0d;
var strategy = new RoutingStrategy
{
EdgeOrder = [edgeIndex],
BaseLineClearance = minLineClearance,
MinLineClearance = minLineClearance,
RoutingParams = new AStarRoutingParams(18d, 200d, 500d, 2.0d, minLineClearance, 40d, true),
};
var obstacleMargin = Math.Max(
strategy.MinLineClearance + 4d,
strategy.RoutingParams.Margin);
var obstacles = BuildObstacles(nodes, obstacleMargin);
var spreadEndpoints = SpreadTargetEndpoints(edges, nodesById, graphMinY, graphMaxY, strategy.MinLineClearance);
var softObstacles = edges
.Where(edge => !string.Equals(edge.Id, edgeId, StringComparison.Ordinal))
.SelectMany(ElkEdgeRoutingGeometry.FlattenSegments)
.Select(segment => new OrthogonalSoftObstacle(segment.Start, segment.End))
.ToArray();
var buildResult = BuildRepairEdgeResult(
edgeIndex,
edges,
nodes,
obstacles,
spreadEndpoints,
nodesById,
softObstacles,
new HashSet<string>([edgeId], StringComparer.Ordinal),
new HashSet<string>([edgeId], StringComparer.Ordinal),
repairReasons,
graphMinY,
graphMaxY,
strategy,
CancellationToken.None);
if (buildResult.WasSkipped || buildResult.RoutedSections == 0)
{
return edges;
}
var candidate = edges.ToArray();
candidate[edgeIndex] = buildResult.Edge;
return candidate;
}
private static ElkRoutedEdge[] ApplyHybridFinalGatewayTargetEntryCleanup(
ElkRoutedEdge[] edges,
ElkPositionedNode[] nodes)
{
var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal);
var severityByEdgeId = new Dictionary<string, int>(StringComparer.Ordinal);
ElkEdgeRoutingScoring.CountTargetApproachBacktrackingViolations(edges, nodes, severityByEdgeId, 40);
ElkEdgeRoutingScoring.CountBadEntryAngles(edges, nodes, severityByEdgeId, 20);
var focusEdgeIds = severityByEdgeId
.OrderByDescending(pair => pair.Value)
.ThenBy(pair => pair.Key, StringComparer.Ordinal)
.Select(pair => pair.Key)
.Where(edgeId =>
{
var edge = edges.FirstOrDefault(candidate => string.Equals(candidate.Id, edgeId, StringComparison.Ordinal));
return edge is not null
&& nodesById.TryGetValue(edge.TargetNodeId ?? string.Empty, out var targetNode)
&& ElkShapeBoundaries.IsGatewayShape(targetNode);
})
.Take(2)
.ToArray();
if (focusEdgeIds.Length == 0)
{
return edges;
}
var candidate = edges;
foreach (var focusEdgeId in focusEdgeIds)
{
var repaired = ElkEdgePostProcessor.FinalizeDecisionTargetEntries(candidate, nodes, [focusEdgeId]);
repaired = ElkEdgePostProcessor.FinalizeGatewayBoundaryGeometry(repaired, nodes, [focusEdgeId]);
repaired = ElkEdgePostProcessor.NormalizeBoundaryAngles(repaired, nodes);
repaired = ElkEdgePostProcessor.NormalizeSourceExitAngles(repaired, nodes);
candidate = ChoosePreferredFocusedApproachRepairLayout(candidate, repaired, nodes, focusEdgeId);
}
return candidate;
}
private static ElkRoutedEdge[] ChoosePreferredFocusedApproachRepairLayout(
ElkRoutedEdge[] baseline,
ElkRoutedEdge[] candidate,
ElkPositionedNode[] nodes,
string focusEdgeId)
{
if (ReferenceEquals(candidate, baseline))
{
return baseline;
}
var baselinePenalty = CountFocusedApproachResidualPenalty(baseline, nodes, focusEdgeId);
var candidatePenalty = CountFocusedApproachResidualPenalty(candidate, nodes, focusEdgeId);
if (candidatePenalty < baselinePenalty)
{
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)
&& candidateScore.NodeCrossings <= baselineScore.NodeCrossings)
{
return candidate;
}
}
return ChoosePreferredBoundarySlotRepairLayout(baseline, candidate, nodes);
}
private static bool HasFocusedApproachResidualViolations(
ElkRoutedEdge[] edges,
ElkPositionedNode[] nodes,
string focusEdgeId)
{
return CountFocusedApproachResidualPenalty(edges, nodes, focusEdgeId) > 0;
}
private static bool IsGatewayTargetEdge(
ElkRoutedEdge[] edges,
ElkPositionedNode[] nodes,
string focusEdgeId)
{
var edge = edges.FirstOrDefault(candidate => string.Equals(candidate.Id, focusEdgeId, StringComparison.Ordinal));
if (edge is null)
{
return false;
}
var targetNode = nodes.FirstOrDefault(node => string.Equals(node.Id, edge.TargetNodeId, StringComparison.Ordinal));
return targetNode is not null && ElkShapeBoundaries.IsGatewayShape(targetNode);
}
private static int CountFocusedApproachResidualPenalty(
ElkRoutedEdge[] edges,
ElkPositionedNode[] nodes,
string focusEdgeId)
{
var edge = edges.FirstOrDefault(candidate => string.Equals(candidate.Id, focusEdgeId, StringComparison.Ordinal));
if (edge is null)
{
return int.MaxValue / 4;
}
return (ElkEdgeRoutingScoring.CountTargetApproachBacktrackingViolations([edge], nodes) * 100)
+ (ElkEdgeRoutingScoring.CountBadEntryAngles([edge], nodes) * 40)
+ (ElkEdgeRoutingScoring.CountBadBoundaryAngles([edge], nodes) * 20)
+ (ElkEdgeRoutingScoring.CountTargetApproachJoinViolations([edge], nodes) * 10);
}
private static string[] ExpandHybridResidualBoundaryFocus(
IReadOnlyCollection<ElkRoutedEdge> edges,
IReadOnlyList<string> rootEdgeIds)
{
if (rootEdgeIds.Count == 0)
{
return [];
}
var edgesById = edges.ToDictionary(edge => edge.Id, StringComparer.Ordinal);
var focused = new HashSet<string>(StringComparer.Ordinal);
var ordered = new List<string>(MaxLateRestabilizedClosureFocusEdges);
foreach (var rootEdgeId in rootEdgeIds)
{
if (!focused.Add(rootEdgeId))
{
continue;
}
ordered.Add(rootEdgeId);
}
foreach (var rootEdgeId in rootEdgeIds)
{
if (!edgesById.TryGetValue(rootEdgeId, out var rootEdge))
{
continue;
}
AddSharedEndpointPeers(
edges,
rootEdge,
focused,
ordered,
includeSharedSource: true,
includeSharedTarget: true);
AddDefaultDownstreamChain(edgesById, edges, rootEdge.TargetNodeId, focused, ordered);
}
return ordered.ToArray();
}
private static string[] ExpandHybridResidualApproachFocus(
IReadOnlyCollection<ElkRoutedEdge> edges,
IReadOnlyList<string> rootEdgeIds)
{
if (rootEdgeIds.Count == 0)
{
return [];
}
var edgesById = edges.ToDictionary(edge => edge.Id, StringComparer.Ordinal);
var focused = new HashSet<string>(StringComparer.Ordinal);
var ordered = new List<string>(MaxLateRestabilizedClosureFocusEdges);
foreach (var rootEdgeId in rootEdgeIds)
{
if (!focused.Add(rootEdgeId))
{
continue;
}
ordered.Add(rootEdgeId);
}
foreach (var rootEdgeId in rootEdgeIds)
{
if (!edgesById.TryGetValue(rootEdgeId, out var rootEdge))
{
continue;
}
AddSharedEndpointPeers(
edges,
rootEdge,
focused,
ordered,
includeSharedSource: true,
includeSharedTarget: true);
AddOutgoingTargetEdges(edges, rootEdge.TargetNodeId, focused, ordered);
}
return ordered.ToArray();
}
private static string[] BuildHybridResidualCategoryRootEdgeIds(
params IReadOnlyDictionary<string, int>[] severities)
{
var ordered = new List<string>(Math.Min(2, MaxWinnerPolishBatchedRootEdges));
var seen = new HashSet<string>(StringComparer.Ordinal);
foreach (var severity in severities)
{
if (ordered.Count >= Math.Min(2, MaxWinnerPolishBatchedRootEdges))
{
break;
}
AddTopSeverityEdge(severity, seen, ordered);
}
return ordered.ToArray();
}
private static string[] BuildHybridResidualApproachRootEdgeIds(
IReadOnlyDictionary<string, int> backtrackingSeverity,
IReadOnlyDictionary<string, int> entrySeverity,
IReadOnlyDictionary<string, int> boundaryAngleSeverity,
IReadOnlyDictionary<string, int> targetJoinSeverity)
{
const int maxResidualApproachRoots = 2;
var weightedSeverity = new Dictionary<string, int>(StringComparer.Ordinal);
AddWeightedSeverity(weightedSeverity, backtrackingSeverity, 100);
AddWeightedSeverity(weightedSeverity, entrySeverity, 40);
AddWeightedSeverity(weightedSeverity, boundaryAngleSeverity, 20);
AddWeightedSeverity(weightedSeverity, targetJoinSeverity, 10);
return weightedSeverity
.OrderByDescending(pair => pair.Value)
.ThenBy(pair => pair.Key, StringComparer.Ordinal)
.Take(maxResidualApproachRoots)
.Select(pair => pair.Key)
.ToArray();
}
private static void AddTopSeverityEdge(
IReadOnlyDictionary<string, int> severity,
ISet<string> seen,
IList<string> ordered)
{
foreach (var edgeId in severity
.OrderByDescending(pair => pair.Value)
.ThenBy(pair => pair.Key, StringComparer.Ordinal)
.Select(pair => pair.Key))
{
if (!seen.Add(edgeId))
{
continue;
}
ordered.Add(edgeId);
return;
}
}
private static void AddTopSeverityEdges(
IReadOnlyDictionary<string, int> severity,
ISet<string> seen,
IList<string> ordered,
int take,
int maxTotal = int.MaxValue)
{
foreach (var edgeId in severity
.OrderByDescending(pair => pair.Value)
.ThenBy(pair => pair.Key, StringComparer.Ordinal)
.Select(pair => pair.Key))
{
if (ordered.Count >= maxTotal)
{
return;
}
if (!seen.Add(edgeId))
{
continue;
}
ordered.Add(edgeId);
if (--take <= 0)
{
return;
}
}
}
private static void AddWeightedSeverity(
IDictionary<string, int> weightedSeverity,
IReadOnlyDictionary<string, int> severity,
int weight)
{
foreach (var pair in severity)
{
weightedSeverity[pair.Key] = weightedSeverity.TryGetValue(pair.Key, out var current)
? current + (pair.Value * weight)
: pair.Value * weight;
}
}
private static void AddSharedEndpointPeers(
IReadOnlyCollection<ElkRoutedEdge> edges,
ElkRoutedEdge rootEdge,
ISet<string> focused,
IList<string> ordered,
bool includeSharedSource,
bool includeSharedTarget)
{
foreach (var peer in edges.OrderBy(edge => edge.Id, StringComparer.Ordinal))
{
if (ordered.Count >= MaxLateRestabilizedClosureFocusEdges
|| string.Equals(peer.Id, rootEdge.Id, StringComparison.Ordinal))
{
continue;
}
if (includeSharedSource
&& string.Equals(peer.SourceNodeId, rootEdge.SourceNodeId, StringComparison.Ordinal)
&& focused.Add(peer.Id))
{
ordered.Add(peer.Id);
continue;
}
if (includeSharedTarget
&& string.Equals(peer.TargetNodeId, rootEdge.TargetNodeId, StringComparison.Ordinal)
&& focused.Add(peer.Id))
{
ordered.Add(peer.Id);
}
}
}
private static void AddOutgoingTargetEdges(
IReadOnlyCollection<ElkRoutedEdge> edges,
string? nodeId,
ISet<string> focused,
IList<string> ordered)
{
if (string.IsNullOrWhiteSpace(nodeId))
{
return;
}
foreach (var peer in edges
.Where(edge => string.Equals(edge.SourceNodeId, nodeId, StringComparison.Ordinal))
.OrderBy(edge => edge.Id, StringComparer.Ordinal))
{
if (!focused.Add(peer.Id))
{
continue;
}
ordered.Add(peer.Id);
if (ordered.Count >= MaxLateRestabilizedClosureFocusEdges)
{
return;
}
}
}
private static void AddDefaultDownstreamChain(
IReadOnlyDictionary<string, ElkRoutedEdge> edgesById,
IReadOnlyCollection<ElkRoutedEdge> edges,
string? nodeId,
ISet<string> focused,
IList<string> ordered)
{
for (var depth = 0; depth < 2 && !string.IsNullOrWhiteSpace(nodeId); depth++)
{
var nextEdge = edges
.Where(edge => string.Equals(edge.SourceNodeId, nodeId, StringComparison.Ordinal))
.OrderBy(edge => IsDefaultLikeEdge(edge) ? 0 : 1)
.ThenBy(edge => edge.Id, StringComparer.Ordinal)
.FirstOrDefault();
if (nextEdge is null)
{
return;
}
if (focused.Add(nextEdge.Id))
{
ordered.Add(nextEdge.Id);
if (ordered.Count >= MaxLateRestabilizedClosureFocusEdges)
{
return;
}
}
nodeId = nextEdge.TargetNodeId;
if (nodeId is not null && edgesById.ContainsKey(nextEdge.Id) && !IsDefaultLikeEdge(nextEdge))
{
return;
}
}
}
private static bool IsDefaultLikeEdge(ElkRoutedEdge edge)
{
if (string.IsNullOrWhiteSpace(edge.Label))
{
return true;
}
return string.Equals(edge.Label.Trim(), "default", StringComparison.OrdinalIgnoreCase);
}
}

View File

@@ -0,0 +1,155 @@
namespace StellaOps.ElkSharp;
internal static partial class ElkEdgeRouterIterative
{
private static CandidateSolution ApplyHybridLeanPostSlotHardRulePolish(
CandidateSolution solution,
ElkPositionedNode[] nodes,
ElkLayoutDirection direction,
double minLineClearance)
{
var severityByEdgeId = new Dictionary<string, int>(StringComparer.Ordinal);
var pressure =
ElkEdgeRoutingScoring.CountBadBoundaryAngles(solution.Edges, nodes, severityByEdgeId, 10)
+ ElkEdgeRoutingScoring.CountGatewaySourceExitViolations(solution.Edges, nodes, severityByEdgeId, 10)
+ ElkEdgeRoutingScoring.CountTargetApproachJoinViolations(solution.Edges, nodes, severityByEdgeId, 10)
+ ElkEdgeRoutingScoring.CountSharedLaneViolations(solution.Edges, nodes, severityByEdgeId, 10)
+ ElkEdgeRoutingScoring.CountBoundarySlotViolations(solution.Edges, nodes, severityByEdgeId, 10)
+ ElkEdgeRoutingScoring.CountTargetApproachBacktrackingViolations(solution.Edges, nodes, severityByEdgeId, 10)
+ ElkEdgeRoutingScoring.CountExcessiveDetourViolations(solution.Edges, nodes, severityByEdgeId, 10)
+ ElkEdgeRoutingScoring.CountBelowGraphViolations(solution.Edges, nodes, severityByEdgeId, 10)
+ ElkEdgeRoutingScoring.CountUnderNodeViolations(solution.Edges, nodes, severityByEdgeId, 10);
ElkLayoutDiagnostics.LogProgress(
$"Hybrid lean post-slot hard-rule start: pressure={pressure} retry={DescribeRetryState(solution.RetryState)} focus={severityByEdgeId.Count}");
if (pressure == 0 || severityByEdgeId.Count == 0)
{
return solution;
}
var rootEdgeIds = severityByEdgeId
.OrderByDescending(pair => pair.Value)
.ThenBy(pair => pair.Key, StringComparer.Ordinal)
.Take(MaxWinnerPolishBatchedRootEdges)
.Select(pair => pair.Key)
.ToArray();
var focusEdgeIds = ExpandHybridLeanPostSlotFocus(solution.Edges, rootEdgeIds);
if (focusEdgeIds.Length == 0)
{
return solution;
}
var candidateEdges = BuildFinalBoundarySlotCandidate(
solution.Edges,
nodes,
direction,
minLineClearance,
focusEdgeIds,
allowLateRestabilizedClosure: false);
candidateEdges = ChoosePreferredHardRuleLayout(
solution.Edges,
candidateEdges,
nodes);
if (solution.RetryState.SharedLaneViolations > 0
|| solution.RetryState.TargetApproachJoinViolations > 0
|| solution.RetryState.UnderNodeViolations > 0
|| solution.RetryState.BoundarySlotViolations > 0
|| solution.RetryState.GatewaySourceExitViolations > 0)
{
candidateEdges = ChoosePreferredHardRuleLayout(
candidateEdges,
ApplyAggressiveSharedLaneClosure(
candidateEdges,
nodes,
direction,
minLineClearance,
focusEdgeIds),
nodes);
}
var shouldRunFocusedTerminalCleanup =
focusEdgeIds.Length <= MaxWinnerPolishBatchedRootEdges
&& (ElkEdgeRoutingScoring.CountSharedLaneViolations(candidateEdges, nodes) > 0
|| ElkEdgeRoutingScoring.CountTargetApproachJoinViolations(candidateEdges, nodes) > 0
|| ElkEdgeRoutingScoring.CountUnderNodeViolations(candidateEdges, nodes) > 0
|| ElkEdgeRoutingScoring.CountGatewaySourceExitViolations(candidateEdges, nodes) > 0);
if (shouldRunFocusedTerminalCleanup)
{
candidateEdges = ChoosePreferredBoundarySlotRepairLayout(
candidateEdges,
ApplyHybridTerminalRuleCleanupRound(
candidateEdges,
nodes,
direction,
minLineClearance,
focusEdgeIds),
nodes);
}
if (ElkEdgeRoutingScoring.CountBoundarySlotViolations(candidateEdges, nodes) > 0
|| ElkEdgeRoutingScoring.CountTargetApproachBacktrackingViolations(candidateEdges, nodes) > 0
|| ElkEdgeRoutingScoring.CountBadEntryAngles(candidateEdges, nodes) > 0)
{
candidateEdges = ApplyHybridFocusedResidualTerminalRecovery(
candidateEdges,
nodes,
direction,
minLineClearance);
}
if (ElkEdgeRoutingScoring.CountTargetApproachBacktrackingViolations(candidateEdges, nodes) > 0
|| ElkEdgeRoutingScoring.CountBadEntryAngles(candidateEdges, nodes) > 0)
{
candidateEdges = ApplyHybridFinalGatewayTargetEntryCleanup(candidateEdges, nodes);
}
return TryPromoteFinalHardRuleCandidate(solution, candidateEdges, nodes, out var promoted)
? promoted
: solution;
}
private static string[] ExpandHybridLeanPostSlotFocus(
IReadOnlyCollection<ElkRoutedEdge> edges,
IReadOnlyList<string> rootEdgeIds)
{
const int maxFocusedEdges = 5;
if (rootEdgeIds.Count == 0)
{
return [];
}
var edgesById = edges.ToDictionary(edge => edge.Id, StringComparer.Ordinal);
var focused = new HashSet<string>(StringComparer.Ordinal);
var ordered = new List<string>(Math.Max(rootEdgeIds.Count, maxFocusedEdges));
foreach (var rootEdgeId in rootEdgeIds)
{
if (!focused.Add(rootEdgeId))
{
continue;
}
ordered.Add(rootEdgeId);
}
foreach (var rootEdgeId in rootEdgeIds)
{
if (ordered.Count >= maxFocusedEdges
|| !edgesById.TryGetValue(rootEdgeId, out var rootEdge))
{
continue;
}
AddSharedEndpointPeers(
edges,
rootEdge,
focused,
ordered,
includeSharedSource: true,
includeSharedTarget: true);
}
return ordered
.Take(maxFocusedEdges)
.ToArray();
}
}

View File

@@ -0,0 +1,170 @@
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Globalization;
namespace StellaOps.ElkSharp;
internal static partial class ElkEdgeRouterIterative
{
private static ElkRoutedEdge[] ApplyLateBoundarySlotRestabilization(
ElkRoutedEdge[] edges,
ElkPositionedNode[] nodes,
double minLineClearance,
IReadOnlyCollection<string> focusEdgeIds)
{
if (focusEdgeIds.Count == 0)
{
return edges;
}
var candidate = ElkEdgePostProcessor.SeparateMixedNodeFaceLaneConflicts(
edges,
nodes,
minLineClearance,
focusEdgeIds);
ElkLayoutDiagnostics.LogProgress("Late boundary-slot restabilization after mixed-face separation");
candidate = ElkEdgePostProcessor.SeparateRepeatCollectorLocalLaneConflicts(
candidate,
nodes,
minLineClearance,
focusEdgeIds);
ElkLayoutDiagnostics.LogProgress("Late boundary-slot restabilization after collector separation");
candidate = ElkEdgePostProcessor.SpreadSourceDepartureJoins(
candidate,
nodes,
minLineClearance,
focusEdgeIds);
ElkLayoutDiagnostics.LogProgress("Late boundary-slot restabilization after source-join spread");
candidate = ElkEdgePostProcessor.SeparateSharedLaneConflicts(
candidate,
nodes,
minLineClearance,
focusEdgeIds);
ElkLayoutDiagnostics.LogProgress("Late boundary-slot restabilization after shared-lane separation");
candidate = ElkEdgePostProcessor.RepairBoundaryAnglesAndTargetApproaches(
candidate,
nodes,
minLineClearance,
focusEdgeIds);
ElkLayoutDiagnostics.LogProgress("Late boundary-slot restabilization after boundary/target repair");
candidate = ElkEdgePostProcessor.SpreadTargetApproachJoins(
candidate,
nodes,
minLineClearance,
focusEdgeIds);
ElkLayoutDiagnostics.LogProgress("Late boundary-slot restabilization after target-join spread");
candidate = ElkEdgePostProcessor.SpreadRectTargetApproachFeederBands(
candidate,
nodes,
minLineClearance,
focusEdgeIds);
ElkLayoutDiagnostics.LogProgress("Late boundary-slot restabilization after feeder-band spread");
candidate = ElkEdgePostProcessor.FinalizeGatewayBoundaryGeometry(candidate, nodes, focusEdgeIds);
ElkLayoutDiagnostics.LogProgress("Late boundary-slot restabilization after gateway finalize");
candidate = ElkEdgePostProcessor.NormalizeBoundaryAngles(candidate, nodes);
candidate = ElkEdgePostProcessor.NormalizeSourceExitAngles(candidate, nodes);
candidate = ElkEdgePostProcessor.SnapBoundarySlotAssignments(
candidate,
nodes,
minLineClearance,
focusEdgeIds,
enforceAllNodeEndpoints: true);
ElkLayoutDiagnostics.LogProgress("Late boundary-slot restabilization after first normalization snap");
candidate = ElkEdgePostProcessor.SeparateRepeatCollectorLocalLaneConflicts(
candidate,
nodes,
minLineClearance,
focusEdgeIds);
ElkLayoutDiagnostics.LogProgress("Late boundary-slot restabilization after second collector separation");
candidate = ElkEdgePostProcessor.SpreadSourceDepartureJoins(
candidate,
nodes,
minLineClearance,
focusEdgeIds);
ElkLayoutDiagnostics.LogProgress("Late boundary-slot restabilization after second source-join spread");
candidate = ElkEdgePostProcessor.SeparateMixedNodeFaceLaneConflicts(
candidate,
nodes,
minLineClearance,
focusEdgeIds);
ElkLayoutDiagnostics.LogProgress("Late boundary-slot restabilization after second mixed-face separation");
candidate = ElkEdgePostProcessor.SeparateSharedLaneConflicts(
candidate,
nodes,
minLineClearance,
focusEdgeIds);
ElkLayoutDiagnostics.LogProgress("Late boundary-slot restabilization after second shared-lane separation");
candidate = ElkEdgePostProcessor.RepairBoundaryAnglesAndTargetApproaches(
candidate,
nodes,
minLineClearance,
focusEdgeIds);
ElkLayoutDiagnostics.LogProgress("Late boundary-slot restabilization after second boundary/target repair");
candidate = ElkEdgePostProcessor.SpreadTargetApproachJoins(
candidate,
nodes,
minLineClearance,
focusEdgeIds);
ElkLayoutDiagnostics.LogProgress("Late boundary-slot restabilization after second target-join spread");
candidate = ElkEdgePostProcessor.SpreadRectTargetApproachFeederBands(
candidate,
nodes,
minLineClearance,
focusEdgeIds);
ElkLayoutDiagnostics.LogProgress("Late boundary-slot restabilization after second feeder-band spread");
candidate = ElkEdgePostProcessor.FinalizeGatewayBoundaryGeometry(candidate, nodes, focusEdgeIds);
ElkLayoutDiagnostics.LogProgress("Late boundary-slot restabilization after second gateway finalize");
candidate = ApplyPostSlotDetourClosure(candidate, nodes, minLineClearance, focusEdgeIds);
ElkLayoutDiagnostics.LogProgress("Late boundary-slot restabilization after detour closure");
candidate = ElkEdgePostProcessor.NormalizeBoundaryAngles(candidate, nodes);
candidate = ElkEdgePostProcessor.NormalizeSourceExitAngles(candidate, nodes);
candidate = ElkEdgePostProcessor.SnapBoundarySlotAssignments(
candidate,
nodes,
minLineClearance,
focusEdgeIds,
enforceAllNodeEndpoints: true);
ElkLayoutDiagnostics.LogProgress("Late boundary-slot restabilization complete");
return ChoosePreferredBoundarySlotRepairLayout(edges, candidate, nodes);
}
private static ElkRoutedEdge[] ApplyLateFocusedBoundarySlotClosure(
ElkRoutedEdge[] edges,
ElkPositionedNode[] nodes,
ElkLayoutDirection direction,
double minLineClearance,
IReadOnlyCollection<string> focusEdgeIds,
IReadOnlyCollection<string>? restrictedEdgeIds)
{
var candidate = ChoosePreferredHardRuleLayout(
edges,
ApplyAggressiveSharedLaneClosure(edges, nodes, direction, minLineClearance, focusEdgeIds),
nodes);
candidate = ChoosePreferredBoundarySlotRepairLayout(
candidate,
ElkEdgePostProcessor.SnapBoundarySlotAssignments(
candidate,
nodes,
minLineClearance,
restrictedEdgeIds,
enforceAllNodeEndpoints: true),
nodes);
candidate = ChoosePreferredBoundarySlotRepairLayout(
candidate,
CloseRemainingTerminalViolations(candidate, nodes, direction, minLineClearance, restrictedEdgeIds),
nodes);
candidate = ApplyLateBoundarySlotRestabilization(candidate, nodes, minLineClearance, focusEdgeIds);
candidate = ChoosePreferredBoundarySlotRepairLayout(
candidate,
ElkEdgePostProcessor.SnapBoundarySlotAssignments(
candidate,
nodes,
minLineClearance,
restrictedEdgeIds,
enforceAllNodeEndpoints: true),
nodes);
return candidate;
}
}

View File

@@ -0,0 +1,165 @@
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Globalization;
namespace StellaOps.ElkSharp;
internal static partial class ElkEdgeRouterIterative
{
private static bool TryPromoteFinalBoundarySlotCandidate(
CandidateSolution current,
ElkRoutedEdge[] candidateEdges,
ElkPositionedNode[] nodes,
out CandidateSolution promoted)
{
promoted = current;
var candidateScore = ElkEdgeRoutingScoring.ComputeScore(candidateEdges, nodes);
var candidateRetryState = BuildRetryState(
candidateScore,
HighwayProcessingEnabled
? ElkEdgeRouterHighway.DetectRemainingBrokenHighways(candidateEdges, nodes).Count
: 0);
if (!IsBetterBoundarySlotRepairCandidate(
candidateScore,
candidateRetryState,
current.Score,
current.RetryState))
{
return false;
}
promoted = current with
{
Score = candidateScore,
RetryState = candidateRetryState,
Edges = candidateEdges,
};
return true;
}
private static bool HasBlockingBoundarySlotPromotionRegression(
RoutingRetryState candidate,
RoutingRetryState baseline)
{
return candidate.RemainingShortHighways > baseline.RemainingShortHighways
|| candidate.RepeatCollectorCorridorViolations > baseline.RepeatCollectorCorridorViolations
|| candidate.RepeatCollectorNodeClearanceViolations > baseline.RepeatCollectorNodeClearanceViolations
|| candidate.BelowGraphViolations > baseline.BelowGraphViolations
|| candidate.UnderNodeViolations > baseline.UnderNodeViolations;
}
private static bool HasEdgeGeometryChanged(
IReadOnlyList<ElkRoutedEdge> baselineEdges,
IReadOnlyList<ElkRoutedEdge> candidateEdges,
string edgeId)
{
var baseline = baselineEdges.FirstOrDefault(edge => string.Equals(edge.Id, edgeId, StringComparison.Ordinal));
var candidate = candidateEdges.FirstOrDefault(edge => string.Equals(edge.Id, edgeId, StringComparison.Ordinal));
if (baseline is null || candidate is null)
{
return false;
}
var baselinePath = ExtractEdgePath(baseline);
var candidatePath = ExtractEdgePath(candidate);
if (baselinePath.Count != candidatePath.Count)
{
return true;
}
for (var i = 0; i < baselinePath.Count; i++)
{
if (!ElkEdgeRoutingGeometry.PointsEqual(baselinePath[i], candidatePath[i]))
{
return true;
}
}
return false;
}
private static List<ElkPoint> ExtractEdgePath(ElkRoutedEdge edge)
{
var path = new List<ElkPoint>();
foreach (var section in edge.Sections)
{
if (path.Count == 0 || !ElkEdgeRoutingGeometry.PointsEqual(path[^1], section.StartPoint))
{
path.Add(section.StartPoint);
}
foreach (var bendPoint in section.BendPoints)
{
if (path.Count == 0 || !ElkEdgeRoutingGeometry.PointsEqual(path[^1], bendPoint))
{
path.Add(bendPoint);
}
}
if (path.Count == 0 || !ElkEdgeRoutingGeometry.PointsEqual(path[^1], section.EndPoint))
{
path.Add(section.EndPoint);
}
}
return path;
}
private static bool TryPromoteFinalHardRuleCandidate(
CandidateSolution current,
ElkRoutedEdge[] candidateEdges,
ElkPositionedNode[] nodes,
out CandidateSolution promoted)
{
promoted = current;
var candidateScore = ElkEdgeRoutingScoring.ComputeScore(candidateEdges, nodes);
var candidateRetryState = BuildRetryState(
candidateScore,
HighwayProcessingEnabled
? ElkEdgeRouterHighway.DetectRemainingBrokenHighways(candidateEdges, nodes).Count
: 0);
if (!IsBetterBoundarySlotRepairCandidate(
candidateScore,
candidateRetryState,
current.Score,
current.RetryState))
{
return false;
}
promoted = current with
{
Score = candidateScore,
RetryState = candidateRetryState,
Edges = candidateEdges,
};
return true;
}
private static ElkRoutedEdge[] ApplyAggressiveSharedLaneClosure(
ElkRoutedEdge[] edges,
ElkPositionedNode[] nodes,
ElkLayoutDirection direction,
double minLineClearance,
IReadOnlyCollection<string> focusEdgeIds)
{
var result = edges;
result = ElkEdgePostProcessor.SeparateSharedLaneConflicts(result, nodes, minLineClearance, focusEdgeIds);
result = ElkEdgePostProcessor.SeparateRepeatCollectorLocalLaneConflicts(result, nodes, minLineClearance, focusEdgeIds);
result = ElkRepeatCollectorCorridors.SeparateSharedLanes(result, nodes, focusEdgeIds);
result = ElkEdgePostProcessor.SpreadSourceDepartureJoins(result, nodes, minLineClearance, focusEdgeIds);
result = ElkEdgePostProcessor.SeparateMixedNodeFaceLaneConflicts(result, nodes, minLineClearance, focusEdgeIds);
result = ElkEdgePostProcessor.SpreadTargetApproachJoins(result, nodes, minLineClearance, focusEdgeIds, forceOutwardAxisSpacing: true);
result = ElkEdgePostProcessor.SpreadRectTargetApproachFeederBands(result, nodes, minLineClearance, focusEdgeIds);
result = ElkEdgePostProcessor.RepairBoundaryAnglesAndTargetApproaches(result, nodes, minLineClearance, focusEdgeIds);
result = ElkEdgePostProcessor.FinalizeGatewayBoundaryGeometry(result, nodes, focusEdgeIds);
result = ClampBelowGraphEdges(result, nodes, focusEdgeIds);
result = ElkEdgePostProcessor.AvoidNodeCrossings(result, nodes, direction, focusEdgeIds);
result = ElkEdgePostProcessor.NormalizeBoundaryAngles(result, nodes);
result = ElkEdgePostProcessor.NormalizeSourceExitAngles(result, nodes);
result = ElkEdgePostProcessor.SeparateMixedNodeFaceLaneConflicts(result, nodes, minLineClearance, focusEdgeIds);
result = ElkEdgePostProcessor.SeparateSharedLaneConflicts(result, nodes, minLineClearance, focusEdgeIds);
return result;
}
}

View File

@@ -0,0 +1,146 @@
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Globalization;
namespace StellaOps.ElkSharp;
internal static partial class ElkEdgeRouterIterative
{
private static CandidateSolution ApplyFinalSharedLanePolish(
CandidateSolution solution,
ElkPositionedNode[] nodes,
ElkLayoutDirection direction,
double minLineClearance,
bool preferLeanTerminalCleanup = false)
{
var current = solution;
if (current.RetryState.SharedLaneViolations <= 0)
{
return current;
}
var maxRounds = preferLeanTerminalCleanup ? 1 : 3;
for (var round = 0; round < maxRounds; round++)
{
var sharedLaneSeverity = new Dictionary<string, int>(StringComparer.Ordinal);
ElkEdgeRoutingScoring.CountSharedLaneViolations(current.Edges, nodes, sharedLaneSeverity, 10);
if (sharedLaneSeverity.Count == 0)
{
break;
}
var improved = false;
foreach (var edgeId in sharedLaneSeverity
.OrderByDescending(pair => pair.Value)
.ThenBy(pair => pair.Key, StringComparer.Ordinal)
.Select(pair => pair.Key))
{
var focusEdgeIds = ExpandSharedLanePolishFocus(current.Edges, nodes, edgeId).ToArray();
if (focusEdgeIds.Length == 0)
{
continue;
}
var directCandidate = ElkEdgePostProcessor.SeparateSharedLaneConflicts(
current.Edges,
nodes,
minLineClearance,
focusEdgeIds);
var directClosureCandidate = preferLeanTerminalCleanup
? ApplyHybridTerminalRuleCleanupRound(
directCandidate,
nodes,
direction,
minLineClearance,
focusEdgeIds)
: CloseRemainingTerminalViolations(
directCandidate,
nodes,
direction,
minLineClearance,
focusEdgeIds);
var closureCandidate = preferLeanTerminalCleanup
? ApplyHybridTerminalRuleCleanupRound(
current.Edges,
nodes,
direction,
minLineClearance,
focusEdgeIds)
: CloseRemainingTerminalViolations(
current.Edges,
nodes,
direction,
minLineClearance,
focusEdgeIds);
var aggressiveCandidate = ApplyAggressiveSharedLaneClosure(
current.Edges,
nodes,
direction,
minLineClearance,
focusEdgeIds);
var directRetryState = BuildRetryState(
ElkEdgeRoutingScoring.ComputeScore(directCandidate, nodes),
HighwayProcessingEnabled
? ElkEdgeRouterHighway.DetectRemainingBrokenHighways(directCandidate, nodes).Count
: 0);
var directClosureRetryState = BuildRetryState(
ElkEdgeRoutingScoring.ComputeScore(directClosureCandidate, nodes),
HighwayProcessingEnabled
? ElkEdgeRouterHighway.DetectRemainingBrokenHighways(directClosureCandidate, nodes).Count
: 0);
var closureRetryState = BuildRetryState(
ElkEdgeRoutingScoring.ComputeScore(closureCandidate, nodes),
HighwayProcessingEnabled
? ElkEdgeRouterHighway.DetectRemainingBrokenHighways(closureCandidate, nodes).Count
: 0);
var aggressiveRetryState = BuildRetryState(
ElkEdgeRoutingScoring.ComputeScore(aggressiveCandidate, nodes),
HighwayProcessingEnabled
? ElkEdgeRouterHighway.DetectRemainingBrokenHighways(aggressiveCandidate, nodes).Count
: 0);
ElkLayoutDiagnostics.LogProgress(
$"Winner shared-lane focus edge={edgeId} focus=[{string.Join(", ", focusEdgeIds)}] " +
$"direct={DescribeRetryState(directRetryState)} " +
$"direct-closure={DescribeRetryState(directClosureRetryState)} " +
$"closure={DescribeRetryState(closureRetryState)} " +
$"aggressive={DescribeRetryState(aggressiveRetryState)}");
var candidateEdges = ChoosePreferredSharedLanePolishLayout(directCandidate, directClosureCandidate, nodes);
candidateEdges = ChoosePreferredSharedLanePolishLayout(candidateEdges, closureCandidate, nodes);
candidateEdges = ChoosePreferredSharedLanePolishLayout(candidateEdges, aggressiveCandidate, nodes);
candidateEdges = ChoosePreferredSharedLanePolishLayout(current.Edges, candidateEdges, nodes);
var candidateScore = ElkEdgeRoutingScoring.ComputeScore(candidateEdges, nodes);
var candidateRetryState = BuildRetryState(
candidateScore,
HighwayProcessingEnabled
? ElkEdgeRouterHighway.DetectRemainingBrokenHighways(candidateEdges, nodes).Count
: 0);
var improvedSharedLanes = candidateRetryState.SharedLaneViolations < current.RetryState.SharedLaneViolations;
if (HasBlockingSharedLanePromotionRegression(candidateRetryState, current.RetryState)
|| (!improvedSharedLanes
&& !IsBetterCandidate(candidateScore, candidateRetryState, current.Score, current.RetryState)))
{
continue;
}
current = current with
{
Score = candidateScore,
RetryState = candidateRetryState,
Edges = candidateEdges,
};
improved = true;
break;
}
if (!improved)
{
break;
}
}
return current;
}
}

View File

@@ -0,0 +1,149 @@
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Globalization;
namespace StellaOps.ElkSharp;
internal static partial class ElkEdgeRouterIterative
{
private static bool ShouldPreferFastTerminalOnlyHardRuleClosure(RoutingRetryState retryState)
{
return retryState.RemainingShortHighways == 0
&& retryState.RepeatCollectorCorridorViolations == 0
&& retryState.RepeatCollectorNodeClearanceViolations == 0
&& retryState.TargetApproachBacktrackingViolations == 0
&& retryState.ExcessiveDetourViolations == 0
&& retryState.BoundarySlotViolations == 0
&& retryState.BelowGraphViolations == 0
&& retryState.UnderNodeViolations == 0
&& retryState.LongDiagonalViolations == 0
&& retryState.SharedLaneViolations <= 2
&& (retryState.TargetApproachJoinViolations > 0
|| retryState.EntryAngleViolations > 0
|| retryState.GatewaySourceExitViolations > 0
|| retryState.SharedLaneViolations > 0);
}
private static ElkRoutedEdge[] BuildFastTerminalOnlyHardRuleCandidate(
ElkRoutedEdge[] edges,
ElkPositionedNode[] nodes,
ElkLayoutDirection direction,
double minLineClearance,
IReadOnlyCollection<string> restrictedEdgeIds)
{
var candidate = edges;
candidate = ChoosePreferredHardRuleLayout(
candidate,
ElkEdgePostProcessor.SeparateSharedLaneConflicts(
candidate,
nodes,
minLineClearance,
restrictedEdgeIds),
nodes);
candidate = ChoosePreferredHardRuleLayout(
candidate,
ElkEdgePostProcessor.SpreadSourceDepartureJoins(
candidate,
nodes,
minLineClearance,
restrictedEdgeIds),
nodes);
candidate = ChoosePreferredHardRuleLayout(
candidate,
ElkEdgePostProcessor.RepairBoundaryAnglesAndTargetApproaches(
candidate,
nodes,
minLineClearance,
restrictedEdgeIds),
nodes);
candidate = ChoosePreferredHardRuleLayout(
candidate,
ElkEdgePostProcessor.SpreadTargetApproachJoins(
candidate,
nodes,
minLineClearance,
restrictedEdgeIds,
forceOutwardAxisSpacing: true),
nodes);
candidate = ChoosePreferredHardRuleLayout(
candidate,
ElkEdgePostProcessor.NormalizeBoundaryAngles(candidate, nodes),
nodes);
candidate = ChoosePreferredHardRuleLayout(
candidate,
ElkEdgePostProcessor.NormalizeSourceExitAngles(candidate, nodes),
nodes);
candidate = ChoosePreferredHardRuleLayout(
candidate,
ElkEdgePostProcessor.SeparateSharedLaneConflicts(
candidate,
nodes,
minLineClearance,
restrictedEdgeIds),
nodes);
return candidate;
}
private static bool ShouldPreferCompactFocusedTerminalClosure(
RoutingRetryState retryState,
int focusEdgeCount)
{
return focusEdgeCount <= 4
&& retryState.BoundarySlotViolations == 0
&& retryState.BelowGraphViolations == 0
&& retryState.UnderNodeViolations == 0
&& retryState.GatewaySourceExitViolations == 0
&& retryState.EntryAngleViolations == 0
&& retryState.TargetApproachBacktrackingViolations == 0
&& retryState.ExcessiveDetourViolations == 0
&& retryState.TargetApproachJoinViolations <= 1
&& retryState.SharedLaneViolations <= 1
&& (retryState.TargetApproachJoinViolations > 0
|| retryState.SharedLaneViolations > 0);
}
private static ElkRoutedEdge[] ApplyCompactFocusedTerminalClosure(
ElkRoutedEdge[] edges,
ElkPositionedNode[] nodes,
ElkLayoutDirection direction,
double minLineClearance,
IReadOnlyCollection<string> focusedEdgeIds)
{
var candidate = edges;
candidate = ApplyGuardedFocusedHardRulePass(
candidate,
nodes,
current => ElkEdgePostProcessor.SeparateSharedLaneConflicts(current, nodes, minLineClearance, focusedEdgeIds));
candidate = ApplyGuardedFocusedHardRulePass(
candidate,
nodes,
current => ElkEdgePostProcessor.SpreadSourceDepartureJoins(current, nodes, minLineClearance, focusedEdgeIds));
candidate = ApplyGuardedFocusedHardRulePass(
candidate,
nodes,
current => ElkEdgePostProcessor.RepairBoundaryAnglesAndTargetApproaches(current, nodes, minLineClearance, focusedEdgeIds));
candidate = ApplyGuardedFocusedHardRulePass(
candidate,
nodes,
current => ElkEdgePostProcessor.SpreadTargetApproachJoins(
current,
nodes,
minLineClearance,
focusedEdgeIds,
forceOutwardAxisSpacing: true));
candidate = ApplyGuardedFocusedHardRulePass(
candidate,
nodes,
current => ElkEdgePostProcessor.SeparateSharedLaneConflicts(current, nodes, minLineClearance, focusedEdgeIds));
candidate = ApplyGuardedFocusedHardRulePass(
candidate,
nodes,
current => ElkEdgePostProcessor.NormalizeBoundaryAngles(current, nodes));
candidate = ApplyGuardedFocusedHardRulePass(
candidate,
nodes,
current => ElkEdgePostProcessor.NormalizeSourceExitAngles(current, nodes));
return candidate;
}
}

View File

@@ -0,0 +1,154 @@
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Globalization;
namespace StellaOps.ElkSharp;
internal static partial class ElkEdgeRouterIterative
{
private static CandidateSolution ApplyFinalDirectUnderNodePolish(
CandidateSolution solution,
ElkPositionedNode[] nodes,
double minLineClearance)
{
var current = solution;
var underNodeSeverity = new Dictionary<string, int>(StringComparer.Ordinal);
ElkEdgeRoutingScoring.CountUnderNodeViolations(current.Edges, nodes, underNodeSeverity, 10);
if (underNodeSeverity.Count == 0)
{
return current;
}
foreach (var edgeId in underNodeSeverity
.OrderByDescending(pair => pair.Value)
.ThenBy(pair => pair.Key, StringComparer.Ordinal)
.Select(pair => pair.Key))
{
var candidateEdges = ElkEdgePostProcessor.ElevateUnderNodeViolations(
current.Edges,
nodes,
minLineClearance,
[edgeId]);
var candidateScore = ElkEdgeRoutingScoring.ComputeScore(candidateEdges, nodes);
var candidateRetryState = BuildRetryState(
candidateScore,
HighwayProcessingEnabled
? ElkEdgeRouterHighway.DetectRemainingBrokenHighways(candidateEdges, nodes).Count
: 0);
if (!IsBetterCandidate(candidateScore, candidateRetryState, current.Score, current.RetryState))
{
continue;
}
current = current with
{
Score = candidateScore,
RetryState = candidateRetryState,
Edges = candidateEdges,
};
}
return current;
}
private static CandidateSolution ApplyFinalProtectedLocalBundlePolish(
CandidateSolution solution,
ElkPositionedNode[] nodes,
double minLineClearance)
{
var current = solution;
var underNodeSeverity = new Dictionary<string, int>(StringComparer.Ordinal);
ElkEdgeRoutingScoring.CountUnderNodeViolations(current.Edges, nodes, underNodeSeverity, 10);
var focusSeverity = new Dictionary<string, int>(underNodeSeverity, StringComparer.Ordinal);
ElkEdgeRoutingScoring.CountTargetApproachJoinViolations(current.Edges, nodes, focusSeverity, 10);
if (focusSeverity.Count == 0)
{
return current;
}
foreach (var edgeId in focusSeverity
.OrderByDescending(pair => pair.Value)
.ThenBy(pair => pair.Key, StringComparer.Ordinal)
.Select(pair => pair.Key))
{
var focusEdgeIds = ExpandWinningSolutionFocus(current.Edges, [edgeId]).ToArray();
if (focusEdgeIds.Length == 0)
{
continue;
}
var focusedRepairSeeds = focusEdgeIds
.Where(underNodeSeverity.ContainsKey)
.OrderBy(id => id, StringComparer.Ordinal)
.ToArray();
if (focusedRepairSeeds.Length == 0)
{
focusedRepairSeeds = [edgeId];
}
var candidateEdges = ElkEdgePostProcessor.ElevateUnderNodeViolations(
current.Edges,
nodes,
minLineClearance,
focusedRepairSeeds);
candidateEdges = ElkEdgePostProcessor.SpreadTargetApproachJoins(
candidateEdges,
nodes,
minLineClearance,
focusEdgeIds,
forceOutwardAxisSpacing: true);
candidateEdges = ElkEdgePostProcessor.SpreadRectTargetApproachFeederBands(
candidateEdges,
nodes,
minLineClearance,
focusEdgeIds);
candidateEdges = ElkEdgePostProcessor.SeparateSharedLaneConflicts(
candidateEdges,
nodes,
minLineClearance,
focusEdgeIds);
candidateEdges = ElkEdgePostProcessor.ElevateUnderNodeViolations(
candidateEdges,
nodes,
minLineClearance,
focusEdgeIds);
candidateEdges = ElkEdgePostProcessor.SpreadTargetApproachJoins(
candidateEdges,
nodes,
minLineClearance,
focusEdgeIds,
forceOutwardAxisSpacing: true);
candidateEdges = ElkEdgePostProcessor.SpreadRectTargetApproachFeederBands(
candidateEdges,
nodes,
minLineClearance,
focusEdgeIds);
candidateEdges = ChoosePreferredHardRuleLayout(current.Edges, candidateEdges, nodes);
var candidateScore = ElkEdgeRoutingScoring.ComputeScore(candidateEdges, nodes);
var candidateRetryState = BuildRetryState(
candidateScore,
HighwayProcessingEnabled
? ElkEdgeRouterHighway.DetectRemainingBrokenHighways(candidateEdges, nodes).Count
: 0);
if (!IsBetterCandidate(candidateScore, candidateRetryState, current.Score, current.RetryState))
{
continue;
}
current = current with
{
Score = candidateScore,
RetryState = candidateRetryState,
Edges = candidateEdges,
};
underNodeSeverity.Clear();
ElkEdgeRoutingScoring.CountUnderNodeViolations(current.Edges, nodes, underNodeSeverity, 10);
}
return current;
}
}

View File

@@ -8,6 +8,7 @@ internal static partial class ElkEdgeRouterIterative
{
private static readonly bool HighwayProcessingEnabled = true;
private const int MaxWinnerPolishBatchedRootEdges = 4;
private const int MaxWinnerPolishFastTerminalSingleEdgeCandidates = 8;
private const int MaxLateRestabilizedClosureFocusEdges = 8;
private static readonly string[] OrderingNames =
@@ -31,7 +32,6 @@ internal static partial class ElkEdgeRouterIterative
return ApplyPostProcessing(baselineEdges, nodes, layoutOptions);
}
// Rule: minimum line clearance = average service-task dimension
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
@@ -41,8 +41,12 @@ internal static partial class ElkEdgeRouterIterative
var validSolutions = new List<CandidateSolution>();
var fallbackSolutions = new List<CandidateSolution>();
// Strategy 0: baseline (existing routing with post-processing)
var baselineProcessed = ApplyPostProcessing(baselineEdges, nodes, layoutOptions);
var baselineProcessed = ApplyPostProcessing(
baselineEdges,
nodes,
layoutOptions,
preferHybridTerminalCleanup: config.Mode == IterativeRoutingMode.HybridDeterministic
&& config.MaxRepairWaves <= 2);
var baselineProcessedScore = ElkEdgeRoutingScoring.ComputeScore(baselineProcessed, nodes);
var baselineBrokenHighways = HighwayProcessingEnabled
? ElkEdgeRouterHighway.DetectRemainingBrokenHighways(baselineProcessed, nodes)
@@ -91,6 +95,32 @@ internal static partial class ElkEdgeRouterIterative
return ElkEdgePostProcessor.ClearInternalRoutingMarkers(baselineProcessed);
}
if (config.Mode == IterativeRoutingMode.HybridDeterministic)
{
var hybridBest = OptimizeHybrid(
baselineProcessed,
baselineProcessedScore,
baselineRetryState,
nodes,
layoutOptions,
config,
minLineClearance,
cancellationToken);
if (diagnostics is not null)
{
diagnostics.SelectedStrategyIndex = 1;
diagnostics.FinalScore = hybridBest.Score;
diagnostics.FinalBrokenShortHighwayCount = HighwayProcessingEnabled
? ElkEdgeRouterHighway.DetectRemainingBrokenHighways(hybridBest.Edges, nodes).Count
: 0;
ElkLayoutDiagnostics.FlushSnapshot(diagnostics);
}
var cleanedHybridEdges = ElkEdgePostProcessor.ClearInternalRoutingMarkers(hybridBest.Edges);
ElkLayoutDiagnostics.LogProgress("Hybrid refinement markers cleared");
return cleanedHybridEdges;
}
var strategyInputs = GenerateStrategies(baselineEdges, nodes, config, minLineClearance)
.Take(maxStrategiesToAttempt)
.Select((strategy, zeroBasedIndex) => new StrategyWorkItem(
@@ -154,426 +184,6 @@ internal static partial class ElkEdgeRouterIterative
return cleanedEdges;
}
private static double ScoreProtectedCollectorGatewaySourceExitCandidate(
IReadOnlyList<ElkPoint> path,
ElkPositionedNode sourceNode,
ElkPoint exitReference)
{
var score = 0d;
if (path.Count < 2)
{
return double.PositiveInfinity;
}
var boundary = path[0];
var adjacent = path[1];
var centerX = sourceNode.X + (sourceNode.Width / 2d);
var centerY = sourceNode.Y + (sourceNode.Height / 2d);
var desiredDx = exitReference.X - centerX;
var desiredDy = exitReference.Y - centerY;
var boundaryDx = boundary.X - centerX;
var boundaryDy = boundary.Y - centerY;
var firstDx = adjacent.X - boundary.X;
var firstDy = adjacent.Y - boundary.Y;
const double tolerance = 0.5d;
var dominantHorizontal = Math.Abs(desiredDx) >= Math.Abs(desiredDy) * 1.15d && Math.Sign(desiredDx) != 0;
var dominantVertical = Math.Abs(desiredDy) >= Math.Abs(desiredDx) * 1.15d && Math.Sign(desiredDy) != 0;
if (dominantHorizontal)
{
if (Math.Sign(firstDx) != Math.Sign(desiredDx) || Math.Abs(firstDx) <= tolerance)
{
score += 100_000d;
}
if (Math.Abs(boundaryDy) > sourceNode.Height * 0.28d)
{
score += 25_000d;
}
score += Math.Abs(boundaryDy) * 6d;
}
else if (dominantVertical)
{
if (Math.Sign(firstDy) != Math.Sign(desiredDy) || Math.Abs(firstDy) <= tolerance)
{
score += 100_000d;
}
if (Math.Abs(boundaryDx) > sourceNode.Width * 0.28d)
{
score += 25_000d;
}
score += Math.Abs(boundaryDx) * 6d;
}
if (ElkShapeBoundaries.IsNearGatewayVertex(sourceNode, boundary, 8d))
{
score += 4_000d;
}
var length = 0d;
for (var i = 1; i < path.Count; i++)
{
length += ElkEdgeRoutingGeometry.ComputeSegmentLength(path[i - 1], path[i]);
}
return score
+ length
+ (Math.Max(0, path.Count - 2) * 6d);
}
private static List<ElkPoint> NormalizeProtectedCollectorTail(
IReadOnlyList<ElkPoint> path,
double graphMinY,
double graphMaxY)
{
if (path.Count < 5)
{
return path.Select(point => new ElkPoint { X = point.X, Y = point.Y }).ToList();
}
const double corridorTolerance = 8d;
const double coordinateTolerance = 0.5d;
var firstCorridorIndex = -1;
var lastCorridorIndex = -1;
for (var i = 0; i < path.Count; i++)
{
if (path[i].Y < graphMinY - corridorTolerance || path[i].Y > graphMaxY + corridorTolerance)
{
if (firstCorridorIndex < 0)
{
firstCorridorIndex = i;
}
lastCorridorIndex = i;
}
}
if (firstCorridorIndex < 0
|| lastCorridorIndex <= firstCorridorIndex
|| lastCorridorIndex >= path.Count - 1
|| path[firstCorridorIndex].Y > graphMinY - corridorTolerance)
{
return path.Select(point => new ElkPoint { X = point.X, Y = point.Y }).ToList();
}
var desiredDx = path[^1].X - path[0].X;
if (Math.Abs(desiredDx) <= coordinateTolerance)
{
return path.Select(point => new ElkPoint { X = point.X, Y = point.Y }).ToList();
}
var preCorridorHorizontalIndex = -1;
for (var i = 1; i < lastCorridorIndex; i++)
{
if (Math.Abs(path[i].Y - path[i + 1].Y) > coordinateTolerance
|| Math.Abs(path[i].X - path[i + 1].X) <= coordinateTolerance)
{
continue;
}
var horizontalDelta = path[i + 1].X - path[i].X;
if (Math.Sign(horizontalDelta) == 0 || Math.Sign(horizontalDelta) == Math.Sign(desiredDx))
{
continue;
}
preCorridorHorizontalIndex = i;
break;
}
if (preCorridorHorizontalIndex >= 0)
{
var rebuiltPrefix = path
.Take(preCorridorHorizontalIndex + 1)
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
.ToList();
var rewrittenCorridorY = path[lastCorridorIndex].Y;
var rewrittenReentryPoint = path[lastCorridorIndex + 1];
if (Math.Abs(rebuiltPrefix[^1].Y - rewrittenCorridorY) > coordinateTolerance)
{
rebuiltPrefix.Add(new ElkPoint
{
X = rebuiltPrefix[^1].X,
Y = rewrittenCorridorY,
});
}
if (Math.Abs(rebuiltPrefix[^1].X - rewrittenReentryPoint.X) > coordinateTolerance)
{
rebuiltPrefix.Add(new ElkPoint
{
X = rewrittenReentryPoint.X,
Y = rewrittenCorridorY,
});
}
for (var i = lastCorridorIndex + 1; i < path.Count; i++)
{
rebuiltPrefix.Add(new ElkPoint { X = path[i].X, Y = path[i].Y });
}
return NormalizeCollectorPoints(rebuiltPrefix);
}
var firstHorizontalIndex = -1;
for (var i = firstCorridorIndex; i < lastCorridorIndex; i++)
{
if (Math.Abs(path[i].Y - path[i + 1].Y) <= coordinateTolerance
&& Math.Abs(path[i].X - path[i + 1].X) > coordinateTolerance)
{
firstHorizontalIndex = i;
break;
}
}
if (firstHorizontalIndex < 0)
{
return path.Select(point => new ElkPoint { X = point.X, Y = point.Y }).ToList();
}
var firstHorizontalDelta = path[firstHorizontalIndex + 1].X - path[firstHorizontalIndex].X;
if (Math.Sign(firstHorizontalDelta) == 0 || Math.Sign(firstHorizontalDelta) == Math.Sign(desiredDx))
{
return path.Select(point => new ElkPoint { X = point.X, Y = point.Y }).ToList();
}
var rebuilt = path
.Take(firstCorridorIndex + 1)
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
.ToList();
var targetCorridorY = path[lastCorridorIndex].Y;
var reentryPoint = path[lastCorridorIndex + 1];
if (Math.Abs(rebuilt[^1].Y - targetCorridorY) > coordinateTolerance)
{
rebuilt.Add(new ElkPoint
{
X = rebuilt[^1].X,
Y = targetCorridorY,
});
}
if (Math.Abs(rebuilt[^1].X - reentryPoint.X) > coordinateTolerance)
{
rebuilt.Add(new ElkPoint
{
X = reentryPoint.X,
Y = targetCorridorY,
});
}
for (var i = lastCorridorIndex + 1; i < path.Count; i++)
{
rebuilt.Add(new ElkPoint { X = path[i].X, Y = path[i].Y });
}
return NormalizeCollectorPoints(rebuilt);
}
private static ElkPoint? BuildOrthogonalCollectorCorner(ElkPoint from, ElkPoint to)
{
const double tolerance = 0.5d;
if (Math.Abs(from.X - to.X) <= tolerance || Math.Abs(from.Y - to.Y) <= tolerance)
{
return null;
}
return Math.Abs(to.Y - from.Y) >= Math.Abs(to.X - from.X)
? new ElkPoint { X = from.X, Y = to.Y }
: new ElkPoint { X = to.X, Y = from.Y };
}
private static List<ElkPoint> NormalizeCollectorPoints(IReadOnlyList<ElkPoint> points)
{
const double coordinateTolerance = 0.5d;
var deduped = new List<ElkPoint>();
foreach (var point in points)
{
if (deduped.Count == 0 || !ElkEdgeRoutingGeometry.PointsEqual(deduped[^1], point))
{
deduped.Add(point);
}
}
if (deduped.Count <= 2)
{
return deduped;
}
var simplified = new List<ElkPoint> { deduped[0] };
for (var i = 1; i < deduped.Count - 1; i++)
{
var previous = simplified[^1];
var current = deduped[i];
var next = deduped[i + 1];
var sameX = Math.Abs(previous.X - current.X) <= coordinateTolerance
&& Math.Abs(current.X - next.X) <= coordinateTolerance;
var sameY = Math.Abs(previous.Y - current.Y) <= coordinateTolerance
&& Math.Abs(current.Y - next.Y) <= coordinateTolerance;
if (!sameX && !sameY)
{
simplified.Add(current);
}
}
simplified.Add(deduped[^1]);
return simplified;
}
private static List<ElkPoint> NormalizePolyline(IReadOnlyList<ElkPoint> points)
{
var result = new List<ElkPoint>(points.Count);
foreach (var point in points)
{
if (result.Count == 0 || !ElkEdgeRoutingGeometry.PointsEqual(result[^1], point))
{
result.Add(point);
}
}
if (result.Count < 3)
{
return result;
}
var collapsed = new List<ElkPoint> { result[0] };
for (var i = 1; i < result.Count - 1; i++)
{
var previous = collapsed[^1];
var current = result[i];
var next = result[i + 1];
var sameX = Math.Abs(previous.X - current.X) < 0.5d && Math.Abs(current.X - next.X) < 0.5d;
var sameY = Math.Abs(previous.Y - current.Y) < 0.5d && Math.Abs(current.Y - next.Y) < 0.5d;
if (!sameX && !sameY)
{
collapsed.Add(current);
}
}
collapsed.Add(result[^1]);
return collapsed;
}
private static void AddUniqueCoordinate(ICollection<double> coordinates, double candidate, double tolerance = 0.5d)
{
foreach (var coordinate in coordinates)
{
if (Math.Abs(coordinate - candidate) <= tolerance)
{
return;
}
}
coordinates.Add(candidate);
}
private static (double Left, double Top, double Right, double Bottom, string Id)[] BuildObstacles(
ElkPositionedNode[] nodes,
double margin)
{
return nodes.Select(n => (
Left: n.X - margin,
Top: n.Y - margin,
Right: n.X + n.Width + margin,
Bottom: n.Y + n.Height + margin,
Id: n.Id
)).ToArray();
}
private static ElkRoutedEdge[] ClampBelowGraphEdges(
ElkRoutedEdge[] edges,
ElkPositionedNode[] nodes,
IReadOnlyCollection<string>? restrictedEdgeIds = null)
{
if (edges.Length == 0 || nodes.Length == 0)
{
return edges;
}
var graphMinY = nodes.Min(node => node.Y);
var graphMaxY = nodes.Max(node => node.Y + node.Height);
var limitY = graphMaxY + 4d;
var obstacles = BuildObstacles(nodes, 0d);
var restrictedSet = restrictedEdgeIds is null
? null
: restrictedEdgeIds.ToHashSet(StringComparer.Ordinal);
var result = edges.ToArray();
for (var i = 0; i < result.Length; i++)
{
var edge = result[i];
if (restrictedSet is not null && !restrictedSet.Contains(edge.Id))
{
continue;
}
var path = ExtractPath(edge)
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
.ToList();
if (path.Count < 3)
{
continue;
}
var changed = false;
for (var pointIndex = 1; pointIndex < path.Count - 1; pointIndex++)
{
if (path[pointIndex].Y <= limitY)
{
continue;
}
path[pointIndex] = new ElkPoint
{
X = path[pointIndex].X,
Y = limitY,
};
changed = true;
}
if (!changed)
{
continue;
}
var normalized = NormalizePolyline(path);
var candidate = new ElkRoutedEdge
{
Id = edge.Id,
SourceNodeId = edge.SourceNodeId,
TargetNodeId = edge.TargetNodeId,
SourcePortId = edge.SourcePortId,
TargetPortId = edge.TargetPortId,
Kind = edge.Kind,
Label = edge.Label,
Sections =
[
new ElkEdgeSection
{
StartPoint = normalized[0],
EndPoint = normalized[^1],
BendPoints = normalized.Count > 2
? normalized.Skip(1).Take(normalized.Count - 2).ToArray()
: [],
},
],
};
if (EdgeCrossesNode(candidate, obstacles))
{
continue;
}
result[i] = candidate;
}
return result;
}
private static IterativeRoutingConfig ResolveConfig(ElkLayoutOptions layoutOptions)
{
var requested = layoutOptions.IterativeRouting ?? new IterativeRoutingOptions();
@@ -581,9 +191,14 @@ internal static partial class ElkEdgeRouterIterative
return new IterativeRoutingConfig(
Enabled: enabled,
Mode: requested.Mode,
MaxAdaptationsPerStrategy: Math.Clamp(requested.MaxAdaptationsPerStrategy, 1, 100),
RequiredValidSolutions: Math.Max(1, requested.RequiredValidSolutions),
MaxRepairWaves: Math.Clamp(requested.MaxRepairWaves, 1, 12),
MaxParallelRepairBuilds: Math.Clamp(
requested.MaxParallelRepairBuilds,
1,
Math.Max(1, Environment.ProcessorCount)),
ObstacleMargin: 18d);
}
}
}

View File

@@ -0,0 +1,186 @@
namespace StellaOps.ElkSharp;
internal static partial class ElkEdgeRoutingGeometry
{
internal static bool AreParallelAndClose(
ElkPoint a1,
ElkPoint a2,
ElkPoint b1,
ElkPoint b2,
double clearance)
{
if (IsHorizontal(a1, a2) && IsHorizontal(b1, b2))
{
return Math.Abs(a1.Y - b1.Y) <= clearance
&& 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)) > 1d;
}
if (IsVertical(a1, a2) && IsVertical(b1, b2))
{
return Math.Abs(a1.X - b1.X) <= clearance
&& 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)) > 1d;
}
return false;
}
internal static double ComputeSharedSegmentLength(
ElkPoint a1,
ElkPoint a2,
ElkPoint b1,
ElkPoint b2)
{
if (IsHorizontal(a1, a2) && IsHorizontal(b1, b2) && Math.Abs(a1.Y - b1.Y) <= CoordinateTolerance)
{
return Math.Max(0d, 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) && Math.Abs(a1.X - b1.X) <= CoordinateTolerance)
{
return Math.Max(0d, 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 string ResolveBoundarySide(ElkPoint point, ElkPositionedNode node)
{
var distLeft = Math.Abs(point.X - node.X);
var distRight = Math.Abs(point.X - (node.X + node.Width));
var distTop = Math.Abs(point.Y - node.Y);
var distBottom = Math.Abs(point.Y - (node.Y + node.Height));
var min = Math.Min(Math.Min(distLeft, distRight), Math.Min(distTop, distBottom));
if (Math.Abs(min - distLeft) <= CoordinateTolerance)
{
return "left";
}
if (Math.Abs(min - distRight) <= CoordinateTolerance)
{
return "right";
}
if (Math.Abs(min - distTop) <= CoordinateTolerance)
{
return "top";
}
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)
{
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)) > 1d;
}
if (IsVertical(a1, a2) && IsVertical(b1, b2) && Math.Abs(a1.X - b1.X) <= CoordinateTolerance)
{
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)) > 1d;
}
return false;
}
private static bool IsHorizontal(ElkPoint start, ElkPoint end) => Math.Abs(start.Y - end.Y) <= CoordinateTolerance;
private static bool IsVertical(ElkPoint start, ElkPoint end) => Math.Abs(start.X - end.X) <= CoordinateTolerance;
private static IReadOnlyList<RoutedEdgeSegment> FlattenSegmentsNearEnd(
IReadOnlyList<ElkPoint> path,
int maxSegmentsFromEnd)
{
if (path.Count < 2 || maxSegmentsFromEnd <= 0)
{
return [];
}
var startIndex = Math.Max(0, path.Count - (maxSegmentsFromEnd + 1));
var segments = new List<RoutedEdgeSegment>();
for (var i = startIndex; i < path.Count - 1; i++)
{
segments.Add(new RoutedEdgeSegment(string.Empty, path[i], path[i + 1]));
}
return segments;
}
private static double OverlapLength(double firstMin, double firstMax, double secondMin, double secondMax)
{
return Math.Min(firstMax, secondMax) - Math.Max(firstMin, secondMin);
}
}

View File

@@ -0,0 +1,87 @@
namespace StellaOps.ElkSharp;
internal static partial class ElkEdgeRoutingGeometry
{
internal static bool SegmentsIntersect(ElkPoint a1, ElkPoint a2, ElkPoint b1, ElkPoint b2)
{
if (ShareEndpoint(a1, a2, b1, b2))
{
return false;
}
if (AreCollinearAndOverlapping(a1, a2, b1, b2))
{
return false;
}
if (IsHorizontal(a1, a2) && IsVertical(b1, b2))
{
return IntersectsOrthogonal(a1, a2, b1, b2);
}
if (IsVertical(a1, a2) && IsHorizontal(b1, b2))
{
return IntersectsOrthogonal(b1, b2, a1, a2);
}
return SegmentsIntersectGeneral(a1, a2, b1, b2);
}
private static bool IntersectsOrthogonal(ElkPoint horizontalStart, ElkPoint horizontalEnd, ElkPoint verticalStart, ElkPoint verticalEnd)
{
var minHorizontalX = Math.Min(horizontalStart.X, horizontalEnd.X);
var maxHorizontalX = Math.Max(horizontalStart.X, horizontalEnd.X);
var minVerticalY = Math.Min(verticalStart.Y, verticalEnd.Y);
var maxVerticalY = Math.Max(verticalStart.Y, verticalEnd.Y);
return verticalStart.X > minHorizontalX + CoordinateTolerance
&& verticalStart.X < maxHorizontalX - CoordinateTolerance
&& horizontalStart.Y > minVerticalY + CoordinateTolerance
&& horizontalStart.Y < maxVerticalY - CoordinateTolerance;
}
private static bool ShareEndpoint(ElkPoint a1, ElkPoint a2, ElkPoint b1, ElkPoint b2)
{
return PointsEqual(a1, b1)
|| PointsEqual(a1, b2)
|| PointsEqual(a2, b1)
|| PointsEqual(a2, b2);
}
private static bool SegmentsIntersectGeneral(ElkPoint a1, ElkPoint a2, ElkPoint b1, ElkPoint b2)
{
var o1 = Orientation(a1, a2, b1);
var o2 = Orientation(a1, a2, b2);
var o3 = Orientation(b1, b2, a1);
var o4 = Orientation(b1, b2, a2);
if (o1 != o2 && o3 != o4)
{
return true;
}
return o1 == 0 && OnSegment(a1, b1, a2)
|| o2 == 0 && OnSegment(a1, b2, a2)
|| o3 == 0 && OnSegment(b1, a1, b2)
|| o4 == 0 && OnSegment(b1, a2, b2);
}
private static int Orientation(ElkPoint start, ElkPoint middle, ElkPoint end)
{
var value = ((middle.Y - start.Y) * (end.X - middle.X)) - ((middle.X - start.X) * (end.Y - middle.Y));
if (Math.Abs(value) <= CoordinateTolerance)
{
return 0;
}
return value > 0 ? 1 : 2;
}
private static bool OnSegment(ElkPoint start, ElkPoint point, ElkPoint end)
{
return point.X <= Math.Max(start.X, end.X) + CoordinateTolerance
&& point.X >= Math.Min(start.X, end.X) - CoordinateTolerance
&& point.Y <= Math.Max(start.Y, end.Y) + CoordinateTolerance
&& point.Y >= Math.Min(start.Y, end.Y) - CoordinateTolerance;
}
}

View File

@@ -1,6 +1,6 @@
namespace StellaOps.ElkSharp;
internal static class ElkEdgeRoutingGeometry
internal static partial class ElkEdgeRoutingGeometry
{
private const double CoordinateTolerance = 0.5d;
@@ -45,80 +45,6 @@ internal static class ElkEdgeRoutingGeometry
return Math.Sqrt((dx * dx) + (dy * dy));
}
internal static bool SegmentsIntersect(ElkPoint a1, ElkPoint a2, ElkPoint b1, ElkPoint b2)
{
if (ShareEndpoint(a1, a2, b1, b2))
{
return false;
}
if (AreCollinearAndOverlapping(a1, a2, b1, b2))
{
return false;
}
if (IsHorizontal(a1, a2) && IsVertical(b1, b2))
{
return IntersectsOrthogonal(a1, a2, b1, b2);
}
if (IsVertical(a1, a2) && IsHorizontal(b1, b2))
{
return IntersectsOrthogonal(b1, b2, a1, a2);
}
return SegmentsIntersectGeneral(a1, a2, b1, b2);
}
internal static bool AreParallelAndClose(
ElkPoint a1,
ElkPoint a2,
ElkPoint b1,
ElkPoint b2,
double clearance)
{
if (IsHorizontal(a1, a2) && IsHorizontal(b1, b2))
{
return Math.Abs(a1.Y - b1.Y) <= clearance
&& 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)) > 1d;
}
if (IsVertical(a1, a2) && IsVertical(b1, b2))
{
return Math.Abs(a1.X - b1.X) <= clearance
&& 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)) > 1d;
}
return false;
}
internal static double ComputeSharedSegmentLength(
ElkPoint a1,
ElkPoint a2,
ElkPoint b1,
ElkPoint b2)
{
if (IsHorizontal(a1, a2) && IsHorizontal(b1, b2) && Math.Abs(a1.Y - b1.Y) <= CoordinateTolerance)
{
return Math.Max(0d, 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) && Math.Abs(a1.X - b1.X) <= CoordinateTolerance)
{
return Math.Max(0d, 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 double ComputeLongestSharedSegmentLength(ElkRoutedEdge left, ElkRoutedEdge right)
{
var leftSegments = FlattenSegments(left);
@@ -164,111 +90,6 @@ internal static class ElkEdgeRoutingGeometry
return longest;
}
internal static string ResolveBoundarySide(ElkPoint point, ElkPositionedNode node)
{
var distLeft = Math.Abs(point.X - node.X);
var distRight = Math.Abs(point.X - (node.X + node.Width));
var distTop = Math.Abs(point.Y - node.Y);
var distBottom = Math.Abs(point.Y - (node.Y + node.Height));
var min = Math.Min(Math.Min(distLeft, distRight), Math.Min(distTop, distBottom));
if (Math.Abs(min - distLeft) <= CoordinateTolerance)
{
return "left";
}
if (Math.Abs(min - distRight) <= CoordinateTolerance)
{
return "right";
}
if (Math.Abs(min - distTop) <= CoordinateTolerance)
{
return "top";
}
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)
{
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)) > 1d;
}
if (IsVertical(a1, a2) && IsVertical(b1, b2) && Math.Abs(a1.X - b1.X) <= CoordinateTolerance)
{
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)) > 1d;
}
return false;
}
internal static ElkPoint ResolveApproachPoint(ElkRoutedEdge edge)
{
var lastSection = edge.Sections.Last();
@@ -285,90 +106,4 @@ internal static class ElkEdgeRoutingGeometry
return Math.Abs(left.X - right.X) <= CoordinateTolerance
&& Math.Abs(left.Y - right.Y) <= CoordinateTolerance;
}
private static bool IsHorizontal(ElkPoint start, ElkPoint end) => Math.Abs(start.Y - end.Y) <= CoordinateTolerance;
private static bool IsVertical(ElkPoint start, ElkPoint end) => Math.Abs(start.X - end.X) <= CoordinateTolerance;
private static IReadOnlyList<RoutedEdgeSegment> FlattenSegmentsNearEnd(
IReadOnlyList<ElkPoint> path,
int maxSegmentsFromEnd)
{
if (path.Count < 2 || maxSegmentsFromEnd <= 0)
{
return [];
}
var startIndex = Math.Max(0, path.Count - (maxSegmentsFromEnd + 1));
var segments = new List<RoutedEdgeSegment>();
for (var i = startIndex; i < path.Count - 1; i++)
{
segments.Add(new RoutedEdgeSegment(string.Empty, path[i], path[i + 1]));
}
return segments;
}
private static bool IntersectsOrthogonal(ElkPoint horizontalStart, ElkPoint horizontalEnd, ElkPoint verticalStart, ElkPoint verticalEnd)
{
var minHorizontalX = Math.Min(horizontalStart.X, horizontalEnd.X);
var maxHorizontalX = Math.Max(horizontalStart.X, horizontalEnd.X);
var minVerticalY = Math.Min(verticalStart.Y, verticalEnd.Y);
var maxVerticalY = Math.Max(verticalStart.Y, verticalEnd.Y);
return verticalStart.X > minHorizontalX + CoordinateTolerance
&& verticalStart.X < maxHorizontalX - CoordinateTolerance
&& horizontalStart.Y > minVerticalY + CoordinateTolerance
&& horizontalStart.Y < maxVerticalY - CoordinateTolerance;
}
private static bool ShareEndpoint(ElkPoint a1, ElkPoint a2, ElkPoint b1, ElkPoint b2)
{
return PointsEqual(a1, b1)
|| PointsEqual(a1, b2)
|| PointsEqual(a2, b1)
|| PointsEqual(a2, b2);
}
private static bool SegmentsIntersectGeneral(ElkPoint a1, ElkPoint a2, ElkPoint b1, ElkPoint b2)
{
var o1 = Orientation(a1, a2, b1);
var o2 = Orientation(a1, a2, b2);
var o3 = Orientation(b1, b2, a1);
var o4 = Orientation(b1, b2, a2);
if (o1 != o2 && o3 != o4)
{
return true;
}
return o1 == 0 && OnSegment(a1, b1, a2)
|| o2 == 0 && OnSegment(a1, b2, a2)
|| o3 == 0 && OnSegment(b1, a1, b2)
|| o4 == 0 && OnSegment(b1, a2, b2);
}
private static int Orientation(ElkPoint start, ElkPoint middle, ElkPoint end)
{
var value = ((middle.Y - start.Y) * (end.X - middle.X)) - ((middle.X - start.X) * (end.Y - middle.Y));
if (Math.Abs(value) <= CoordinateTolerance)
{
return 0;
}
return value > 0 ? 1 : 2;
}
private static bool OnSegment(ElkPoint start, ElkPoint point, ElkPoint end)
{
return point.X <= Math.Max(start.X, end.X) + CoordinateTolerance
&& point.X >= Math.Min(start.X, end.X) - CoordinateTolerance
&& point.Y <= Math.Max(start.Y, end.Y) + CoordinateTolerance
&& point.Y >= Math.Min(start.Y, end.Y) - CoordinateTolerance;
}
private static double OverlapLength(double firstMin, double firstMax, double secondMin, double secondMax)
{
return Math.Min(firstMax, secondMax) - Math.Max(firstMin, secondMin);
}
}

View File

@@ -1960,7 +1960,7 @@ internal static class ElkEdgeRoutingScoring
if (ElkShapeBoundaries.IsGatewayShape(targetNode))
{
return HasGatewayTargetApproachBacktracking(path);
return HasGatewayTargetApproachBacktracking(path, targetNode);
}
var side = ElkEdgeRoutingGeometry.ResolveBoundarySide(path[^1], targetNode);
@@ -1970,6 +1970,11 @@ internal static class ElkEdgeRoutingScoring
}
const double tolerance = 0.5d;
if (HasShortOrthogonalTargetHook(path, targetNode, side, tolerance))
{
return true;
}
var startIndex = Math.Max(
0,
path.Count - (side is "left" or "right" ? 4 : 3));
@@ -2047,6 +2052,85 @@ internal static class ElkEdgeRoutingScoring
return false;
}
private static bool HasShortOrthogonalTargetHook(
IReadOnlyList<ElkPoint> path,
ElkPositionedNode targetNode,
string side,
double tolerance)
{
if (path.Count < 3)
{
return false;
}
var boundaryPoint = path[^1];
var runStartIndex = path.Count - 2;
if (side is "left" or "right")
{
while (runStartIndex > 0 && Math.Abs(path[runStartIndex - 1].Y - boundaryPoint.Y) <= tolerance)
{
runStartIndex--;
}
}
else
{
while (runStartIndex > 0 && Math.Abs(path[runStartIndex - 1].X - boundaryPoint.X) <= tolerance)
{
runStartIndex--;
}
}
if (runStartIndex == 0)
{
return false;
}
var overallDeltaX = path[^1].X - path[0].X;
var overallDeltaY = path[^1].Y - path[0].Y;
var overallAbsDx = Math.Abs(overallDeltaX);
var overallAbsDy = Math.Abs(overallDeltaY);
var sameRowThreshold = Math.Max(24d, targetNode.Height / 3d);
var sameColumnThreshold = Math.Max(24d, targetNode.Width / 3d);
var looksHorizontal = overallAbsDx >= overallAbsDy * 1.15d
&& overallAbsDy <= sameRowThreshold
&& Math.Sign(overallDeltaX) != 0;
var looksVertical = overallAbsDy >= overallAbsDx * 1.15d
&& overallAbsDx <= sameColumnThreshold
&& Math.Sign(overallDeltaY) != 0;
var contradictsDominantApproach = side switch
{
"left" or "right" => looksVertical,
"top" or "bottom" => looksHorizontal,
_ => false,
};
if (!contradictsDominantApproach)
{
return false;
}
var runStart = path[runStartIndex];
var boundaryDepth = side is "left" or "right"
? Math.Abs(boundaryPoint.X - runStart.X)
: Math.Abs(boundaryPoint.Y - runStart.Y);
var requiredDepth = side is "left" or "right"
? targetNode.Width
: targetNode.Height;
if (boundaryDepth + tolerance >= requiredDepth)
{
return false;
}
var predecessor = path[runStartIndex - 1];
var predecessorDx = Math.Abs(runStart.X - predecessor.X);
var predecessorDy = Math.Abs(runStart.Y - predecessor.Y);
return side switch
{
"left" or "right" => predecessorDy > predecessorDx * 3d,
"top" or "bottom" => predecessorDx > predecessorDy * 3d,
_ => false,
};
}
private static bool HasGatewaySourceExitIssue(
IReadOnlyList<ElkPoint> path,
ElkPositionedNode sourceNode,
@@ -2234,13 +2318,20 @@ internal static class ElkEdgeRoutingScoring
return false;
}
private static bool HasGatewayTargetApproachBacktracking(IReadOnlyList<ElkPoint> path)
private static bool HasGatewayTargetApproachBacktracking(
IReadOnlyList<ElkPoint> path,
ElkPositionedNode targetNode)
{
if (path.Count < 4)
{
return false;
}
if (HasShortGatewayTargetOrthogonalHook(path, targetNode))
{
return true;
}
const double tolerance = 0.5d;
var startIndex = Math.Max(0, path.Count - 4);
var nearEnd = path.Skip(startIndex).ToArray();
@@ -2304,6 +2395,43 @@ internal static class ElkEdgeRoutingScoring
return false;
}
private static bool HasShortGatewayTargetOrthogonalHook(
IReadOnlyList<ElkPoint> path,
ElkPositionedNode targetNode)
{
if (path.Count < 3)
{
return false;
}
const double tolerance = 0.5d;
var boundaryPoint = path[^1];
var exteriorPoint = path[^2];
var finalDx = Math.Abs(boundaryPoint.X - exteriorPoint.X);
var finalDy = Math.Abs(boundaryPoint.Y - exteriorPoint.Y);
var finalIsHorizontal = finalDx > tolerance && finalDy <= tolerance;
var finalIsVertical = finalDy > tolerance && finalDx <= tolerance;
if (!finalIsHorizontal && !finalIsVertical)
{
return false;
}
var finalStubLength = finalIsHorizontal ? finalDx : finalDy;
var requiredDepth = Math.Min(targetNode.Width, targetNode.Height);
if (finalStubLength + tolerance >= requiredDepth)
{
return false;
}
var predecessor = path[^3];
var predecessorDx = Math.Abs(exteriorPoint.X - predecessor.X);
var predecessorDy = Math.Abs(exteriorPoint.Y - predecessor.Y);
const double minimumApproachSpan = 24d;
return finalIsHorizontal
? predecessorDy >= minimumApproachSpan && predecessorDy > predecessorDx * 3d
: predecessorDx >= minimumApproachSpan && predecessorDx > predecessorDy * 3d;
}
private static IReadOnlyList<RoutedEdgeSegment> FlattenSegmentsNearEnd(
IReadOnlyList<ElkPoint> path,
int maxSegmentsFromEnd)

View File

@@ -0,0 +1,80 @@
namespace StellaOps.ElkSharp;
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 + LabelProximityViolations;
internal bool RequiresQualityRetry => QualityViolationCount > 0;
internal int BlockingViolationCount =>
RemainingShortHighways
+ RepeatCollectorCorridorViolations
+ RepeatCollectorNodeClearanceViolations
+ TargetApproachJoinViolations
+ TargetApproachBacktrackingViolations
+ SharedLaneViolations
+ BoundarySlotViolations
+ BelowGraphViolations
+ UnderNodeViolations
+ LongDiagonalViolations
+ EntryAngleViolations
+ GatewaySourceExitViolations
+ LabelProximityViolations;
internal bool RequiresBlockingRetry => BlockingViolationCount > 0;
internal int LengthViolationCount =>
ExcessiveDetourViolations;
internal bool RequiresLengthRetry => LengthViolationCount > 0;
internal int PrimaryViolationCount =>
BlockingViolationCount + LengthViolationCount + QualityViolationCount;
internal bool RequiresPrimaryRetry => PrimaryViolationCount > 0;
}
internal readonly record struct EdgeRoutingIssue(
string EdgeId,
int Severity);
internal readonly record struct RoutedEdgeSegment(
string EdgeId,
ElkPoint Start,
ElkPoint End);
internal readonly record struct OrthogonalAStarOptions(
double Margin,
double BendPenalty,
double SoftObstacleWeight,
double SoftObstacleClearance);
internal readonly record struct OrthogonalSoftObstacle(
ElkPoint Start,
ElkPoint End);
internal readonly record struct AStarRoutingParams(
double Margin,
double BendPenalty,
double DiagonalPenalty,
double SoftObstacleWeight,
double SoftObstacleClearance,
double IntermediateGridSpacing,
bool EnforceEntryAngle);

View File

@@ -0,0 +1,159 @@
namespace StellaOps.ElkSharp;
internal sealed class RoutingStrategy
{
internal int[] EdgeOrder { get; init; } = [];
internal double BaseLineClearance { get; init; }
internal double MinLineClearance { get; set; }
internal AStarRoutingParams RoutingParams { get; set; }
internal void AdaptForViolations(EdgeRoutingScore score, int attempt, RoutingRetryState retryState)
{
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(
Math.Max(MinLineClearance, BaseLineClearance) + clearanceStep,
BaseLineClearance * 2d);
var bendPenalty = RoutingParams.BendPenalty;
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 || sharedLanePressure > 0 || boundarySlotPressure > 0 || underNodePressure > 0 || proximityPressure > 0 || crossingPressure > 0)
{
bendPenalty = Math.Max(
80d,
bendPenalty - (backtrackingPressure > 0 ? 80d : detourPressure > 0 ? 50d : sharedLanePressure > 0 || boundarySlotPressure > 0 || underNodePressure > 0 ? 40d : 30d));
}
var margin = RoutingParams.Margin;
if (backtrackingPressure > 0 || detourPressure > 0)
{
margin = Math.Max(12d, margin - (backtrackingPressure > 0 ? 6d : 4d));
}
else
{
margin = Math.Min(
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)
+ (gatewaySourcePressure > 0 ? 4d : 0d),
BaseLineClearance * 2d);
}
var softObstacleWeight = RoutingParams.SoftObstacleWeight;
if (backtrackingPressure > 0 || detourPressure > 0)
{
softObstacleWeight = Math.Max(
0.5d,
softObstacleWeight - (backtrackingPressure > 0 ? 1.0d : 0.75d));
}
else
{
softObstacleWeight = Math.Min(
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);
}
var softObstacleClearance = RoutingParams.SoftObstacleClearance;
if (backtrackingPressure > 0 || detourPressure > 0)
{
softObstacleClearance = Math.Max(
BaseLineClearance * 0.6d,
softObstacleClearance - (backtrackingPressure > 0 ? 14d : 10d));
}
else
{
softObstacleClearance = Math.Min(
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),
BaseLineClearance * 2d);
}
var intermediateGridSpacing = RoutingParams.IntermediateGridSpacing;
if (backtrackingPressure > 0 || detourPressure > 0)
{
intermediateGridSpacing = Math.Max(
12d,
intermediateGridSpacing - (backtrackingPressure > 0 ? 10d : 6d));
}
else
{
intermediateGridSpacing = Math.Max(
12d,
intermediateGridSpacing
- (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));
}
RoutingParams = RoutingParams with
{
Margin = margin,
BendPenalty = bendPenalty,
SoftObstacleWeight = softObstacleWeight,
SoftObstacleClearance = softObstacleClearance,
IntermediateGridSpacing = intermediateGridSpacing,
};
}
}
internal readonly record struct IterativeRoutingConfig(
bool Enabled,
IterativeRoutingMode Mode,
int MaxAdaptationsPerStrategy,
int RequiredValidSolutions,
int MaxRepairWaves,
int MaxParallelRepairBuilds,
double ObstacleMargin);

View File

@@ -70,236 +70,3 @@ internal readonly record struct EdgeRoutingScore(
int ProximityViolations,
double TotalPathLength,
double Value);
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 + LabelProximityViolations;
internal bool RequiresQualityRetry => QualityViolationCount > 0;
internal int BlockingViolationCount =>
RemainingShortHighways
+ RepeatCollectorCorridorViolations
+ RepeatCollectorNodeClearanceViolations
+ TargetApproachJoinViolations
+ TargetApproachBacktrackingViolations
+ SharedLaneViolations
+ BoundarySlotViolations
+ BelowGraphViolations
+ UnderNodeViolations
+ LongDiagonalViolations
+ EntryAngleViolations
+ GatewaySourceExitViolations;
internal bool RequiresBlockingRetry => BlockingViolationCount > 0;
internal int LengthViolationCount =>
ExcessiveDetourViolations;
internal bool RequiresLengthRetry => LengthViolationCount > 0;
internal int PrimaryViolationCount =>
BlockingViolationCount + LengthViolationCount + QualityViolationCount;
internal bool RequiresPrimaryRetry => PrimaryViolationCount > 0;
}
internal readonly record struct EdgeRoutingIssue(
string EdgeId,
int Severity);
internal readonly record struct RoutedEdgeSegment(
string EdgeId,
ElkPoint Start,
ElkPoint End);
internal readonly record struct OrthogonalAStarOptions(
double Margin,
double BendPenalty,
double SoftObstacleWeight,
double SoftObstacleClearance);
internal readonly record struct OrthogonalSoftObstacle(
ElkPoint Start,
ElkPoint End);
internal readonly record struct AStarRoutingParams(
double Margin,
double BendPenalty,
double DiagonalPenalty,
double SoftObstacleWeight,
double SoftObstacleClearance,
double IntermediateGridSpacing,
bool EnforceEntryAngle);
internal sealed class RoutingStrategy
{
internal int[] EdgeOrder { get; init; } = [];
internal double BaseLineClearance { get; init; }
internal double MinLineClearance { get; set; }
internal AStarRoutingParams RoutingParams { get; set; }
internal void AdaptForViolations(EdgeRoutingScore score, int attempt, RoutingRetryState retryState)
{
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(
Math.Max(MinLineClearance, BaseLineClearance) + clearanceStep,
BaseLineClearance * 2d);
var bendPenalty = RoutingParams.BendPenalty;
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 || sharedLanePressure > 0 || boundarySlotPressure > 0 || underNodePressure > 0 || proximityPressure > 0 || crossingPressure > 0)
{
bendPenalty = Math.Max(
80d,
bendPenalty - (backtrackingPressure > 0 ? 80d : detourPressure > 0 ? 50d : sharedLanePressure > 0 || boundarySlotPressure > 0 || underNodePressure > 0 ? 40d : 30d));
}
var margin = RoutingParams.Margin;
if (backtrackingPressure > 0 || detourPressure > 0)
{
margin = Math.Max(12d, margin - (backtrackingPressure > 0 ? 6d : 4d));
}
else
{
margin = Math.Min(
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)
+ (gatewaySourcePressure > 0 ? 4d : 0d),
BaseLineClearance * 2d);
}
var softObstacleWeight = RoutingParams.SoftObstacleWeight;
if (backtrackingPressure > 0 || detourPressure > 0)
{
softObstacleWeight = Math.Max(
0.5d,
softObstacleWeight - (backtrackingPressure > 0 ? 1.0d : 0.75d));
}
else
{
softObstacleWeight = Math.Min(
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);
}
var softObstacleClearance = RoutingParams.SoftObstacleClearance;
if (backtrackingPressure > 0 || detourPressure > 0)
{
softObstacleClearance = Math.Max(
BaseLineClearance * 0.6d,
softObstacleClearance - (backtrackingPressure > 0 ? 14d : 10d));
}
else
{
softObstacleClearance = Math.Min(
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),
BaseLineClearance * 2d);
}
var intermediateGridSpacing = RoutingParams.IntermediateGridSpacing;
if (backtrackingPressure > 0 || detourPressure > 0)
{
intermediateGridSpacing = Math.Max(
12d,
intermediateGridSpacing - (backtrackingPressure > 0 ? 10d : 6d));
}
else
{
intermediateGridSpacing = Math.Max(
12d,
intermediateGridSpacing
- (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));
}
RoutingParams = RoutingParams with
{
Margin = margin,
BendPenalty = bendPenalty,
SoftObstacleWeight = softObstacleWeight,
SoftObstacleClearance = softObstacleClearance,
IntermediateGridSpacing = intermediateGridSpacing,
};
}
}
internal readonly record struct IterativeRoutingConfig(
bool Enabled,
int MaxAdaptationsPerStrategy,
int RequiredValidSolutions,
double ObstacleMargin);

View File

@@ -87,8 +87,17 @@ public sealed record EdgeRefinementOptions
public sealed record IterativeRoutingOptions
{
public bool? Enabled { get; init; }
public IterativeRoutingMode Mode { get; init; } = IterativeRoutingMode.LegacyMultiStrategy;
public int MaxAdaptationsPerStrategy { get; init; } = 100;
public int RequiredValidSolutions { get; init; } = 10;
public int MaxRepairWaves { get; init; } = 4;
public int MaxParallelRepairBuilds { get; init; } = 4;
}
public enum IterativeRoutingMode
{
LegacyMultiStrategy = 0,
HybridDeterministic = 1,
}
public sealed record ElkPoint

View File

@@ -0,0 +1,188 @@
namespace StellaOps.ElkSharp;
internal static partial 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++)
{
var extent = horizontal ? layer[index - 1].Height : layer[index - 1].Width;
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--)
{
var extent = horizontal ? layer[index].Height : layer[index].Width;
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++)
{
var extent = horizontal ? layer[index - 1].Height : layer[index - 1].Width;
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;
}
}

View File

@@ -0,0 +1,196 @@
namespace StellaOps.ElkSharp;
internal static partial class ElkNodePlacement
{
internal static void RefineHorizontalPlacement(
Dictionary<string, ElkPositionedNode> positionedNodes,
IReadOnlyList<ElkNode[]> layers,
IReadOnlyDictionary<string, List<string>> incomingNodeIds,
IReadOnlyDictionary<string, List<string>> outgoingNodeIds,
IReadOnlyDictionary<string, ElkNode> nodesById,
double nodeSpacing,
int iterationCount,
ElkLayoutDirection direction)
{
if (iterationCount <= 0)
{
return;
}
for (var iteration = 0; iteration < iterationCount; iteration++)
{
var layerIndices = iteration % 2 == 0
? Enumerable.Range(0, layers.Count)
: Enumerable.Range(0, layers.Count).Reverse();
foreach (var layerIndex in layerIndices)
{
var layer = layers[layerIndex];
if (layer.Length == 0)
{
continue;
}
var desiredY = new double[layer.Length];
for (var nodeIndex = 0; nodeIndex < layer.Length; nodeIndex++)
{
var node = layer[nodeIndex];
var preferredCenter = ElkNodePlacementPreferredCenter.ResolvePreferredCenter(
node.Id,
incomingNodeIds,
outgoingNodeIds,
positionedNodes,
horizontal: true);
desiredY[nodeIndex] = preferredCenter.HasValue
? preferredCenter.Value - (node.Height / 2d)
: positionedNodes[node.Id].Y;
}
for (var nodeIndex = 1; nodeIndex < layer.Length; nodeIndex++)
{
var minY = desiredY[nodeIndex - 1] + layer[nodeIndex - 1].Height + nodeSpacing;
if (desiredY[nodeIndex] < minY)
{
desiredY[nodeIndex] = minY;
}
}
for (var nodeIndex = 0; nodeIndex < layer.Length; nodeIndex++)
{
var current = positionedNodes[layer[nodeIndex].Id];
positionedNodes[layer[nodeIndex].Id] = ElkLayoutHelpers.CreatePositionedNode(
nodesById[layer[nodeIndex].Id],
current.X,
desiredY[nodeIndex],
direction);
}
}
}
}
internal static void RefineVerticalPlacement(
Dictionary<string, ElkPositionedNode> positionedNodes,
IReadOnlyList<ElkNode[]> layers,
IReadOnlyDictionary<string, List<string>> incomingNodeIds,
IReadOnlyDictionary<string, List<string>> outgoingNodeIds,
IReadOnlyDictionary<string, ElkNode> nodesById,
double nodeSpacing,
int iterationCount,
ElkLayoutDirection direction)
{
if (iterationCount <= 0)
{
return;
}
for (var iteration = 0; iteration < iterationCount; iteration++)
{
var layerIndices = iteration % 2 == 0
? Enumerable.Range(0, layers.Count)
: Enumerable.Range(0, layers.Count).Reverse();
foreach (var layerIndex in layerIndices)
{
var layer = layers[layerIndex];
if (layer.Length == 0)
{
continue;
}
var desiredX = new double[layer.Length];
for (var nodeIndex = 0; nodeIndex < layer.Length; nodeIndex++)
{
var node = layer[nodeIndex];
var preferredCenter = ElkNodePlacementPreferredCenter.ResolvePreferredCenter(
node.Id,
incomingNodeIds,
outgoingNodeIds,
positionedNodes,
horizontal: false);
desiredX[nodeIndex] = preferredCenter.HasValue
? preferredCenter.Value - (node.Width / 2d)
: positionedNodes[node.Id].X;
}
for (var nodeIndex = 1; nodeIndex < layer.Length; nodeIndex++)
{
var minX = desiredX[nodeIndex - 1] + layer[nodeIndex - 1].Width + nodeSpacing;
if (desiredX[nodeIndex] < minX)
{
desiredX[nodeIndex] = minX;
}
}
for (var nodeIndex = 0; nodeIndex < layer.Length; nodeIndex++)
{
var current = positionedNodes[layer[nodeIndex].Id];
positionedNodes[layer[nodeIndex].Id] = ElkLayoutHelpers.CreatePositionedNode(
nodesById[layer[nodeIndex].Id],
desiredX[nodeIndex],
current.Y,
direction);
}
}
}
}
internal static void SnapOriginalPrimaryAxes(
Dictionary<string, ElkPositionedNode> positionedNodes,
IReadOnlyList<ElkNode[]> layers,
IReadOnlySet<string> dummyNodeIds,
IReadOnlyDictionary<string, List<string>> incomingNodeIds,
IReadOnlyDictionary<string, List<string>> outgoingNodeIds,
IReadOnlyDictionary<string, ElkNode> originalNodesById,
double nodeSpacing,
ElkLayoutDirection direction)
{
for (var iteration = 0; iteration < 3; iteration++)
{
foreach (var layer in layers)
{
var actualNodes = layer
.Where(node => !dummyNodeIds.Contains(node.Id) && originalNodesById.ContainsKey(node.Id))
.Select(node => originalNodesById[node.Id])
.ToArray();
if (actualNodes.Length == 0)
{
continue;
}
var nodeDesiredPairs = new (ElkNode Node, double Desired)[actualNodes.Length];
for (var nodeIndex = 0; nodeIndex < actualNodes.Length; nodeIndex++)
{
var positioned = positionedNodes[actualNodes[nodeIndex].Id];
var preferredCenter = ElkNodePlacementPreferredCenter.ResolveOriginalPreferredCenter(
actualNodes[nodeIndex].Id,
incomingNodeIds,
outgoingNodeIds,
positionedNodes,
horizontal: direction == ElkLayoutDirection.LeftToRight);
nodeDesiredPairs[nodeIndex] = (actualNodes[nodeIndex], preferredCenter.HasValue
? preferredCenter.Value - ((direction == ElkLayoutDirection.LeftToRight ? positioned.Height : positioned.Width) / 2d)
: (direction == ElkLayoutDirection.LeftToRight ? positioned.Y : positioned.X));
}
Array.Sort(nodeDesiredPairs, (a, b) => a.Desired.CompareTo(b.Desired));
var sortedNodes = nodeDesiredPairs.Select(p => p.Node).ToArray();
var desiredCoordinates = nodeDesiredPairs.Select(p => p.Desired).ToArray();
EnforceLinearSpacing(
sortedNodes,
desiredCoordinates,
nodeSpacing,
0d,
horizontal: direction == ElkLayoutDirection.LeftToRight);
for (var nodeIndex = 0; nodeIndex < sortedNodes.Length; nodeIndex++)
{
var current = positionedNodes[sortedNodes[nodeIndex].Id];
positionedNodes[sortedNodes[nodeIndex].Id] = direction == ElkLayoutDirection.LeftToRight
? ElkLayoutHelpers.CreatePositionedNode(sortedNodes[nodeIndex], current.X, desiredCoordinates[nodeIndex], direction)
: ElkLayoutHelpers.CreatePositionedNode(sortedNodes[nodeIndex], desiredCoordinates[nodeIndex], current.Y, direction);
}
}
}
}
}

View File

@@ -1,6 +1,6 @@
namespace StellaOps.ElkSharp;
internal static class ElkNodePlacement
internal static partial class ElkNodePlacement
{
internal static NodePlacementGrid ResolvePlacementGrid(IReadOnlyCollection<ElkNode> nodes)
{
@@ -57,379 +57,4 @@ internal static class ElkNodePlacement
};
}
internal static void RefineHorizontalPlacement(
Dictionary<string, ElkPositionedNode> positionedNodes,
IReadOnlyList<ElkNode[]> layers,
IReadOnlyDictionary<string, List<string>> incomingNodeIds,
IReadOnlyDictionary<string, List<string>> outgoingNodeIds,
IReadOnlyDictionary<string, ElkNode> nodesById,
double nodeSpacing,
int iterationCount,
ElkLayoutDirection direction)
{
if (iterationCount <= 0)
{
return;
}
for (var iteration = 0; iteration < iterationCount; iteration++)
{
var layerIndices = iteration % 2 == 0
? Enumerable.Range(0, layers.Count)
: Enumerable.Range(0, layers.Count).Reverse();
foreach (var layerIndex in layerIndices)
{
var layer = layers[layerIndex];
if (layer.Length == 0)
{
continue;
}
var desiredY = new double[layer.Length];
for (var nodeIndex = 0; nodeIndex < layer.Length; nodeIndex++)
{
var node = layer[nodeIndex];
var preferredCenter = ElkNodePlacementPreferredCenter.ResolvePreferredCenter(
node.Id,
incomingNodeIds,
outgoingNodeIds,
positionedNodes,
horizontal: true);
desiredY[nodeIndex] = preferredCenter.HasValue
? preferredCenter.Value - (node.Height / 2d)
: positionedNodes[node.Id].Y;
}
for (var nodeIndex = 1; nodeIndex < layer.Length; nodeIndex++)
{
var minY = desiredY[nodeIndex - 1] + layer[nodeIndex - 1].Height + nodeSpacing;
if (desiredY[nodeIndex] < minY)
{
desiredY[nodeIndex] = minY;
}
}
for (var nodeIndex = 0; nodeIndex < layer.Length; nodeIndex++)
{
var current = positionedNodes[layer[nodeIndex].Id];
positionedNodes[layer[nodeIndex].Id] = ElkLayoutHelpers.CreatePositionedNode(
nodesById[layer[nodeIndex].Id],
current.X,
desiredY[nodeIndex],
direction);
}
}
}
}
internal static void RefineVerticalPlacement(
Dictionary<string, ElkPositionedNode> positionedNodes,
IReadOnlyList<ElkNode[]> layers,
IReadOnlyDictionary<string, List<string>> incomingNodeIds,
IReadOnlyDictionary<string, List<string>> outgoingNodeIds,
IReadOnlyDictionary<string, ElkNode> nodesById,
double nodeSpacing,
int iterationCount,
ElkLayoutDirection direction)
{
if (iterationCount <= 0)
{
return;
}
for (var iteration = 0; iteration < iterationCount; iteration++)
{
var layerIndices = iteration % 2 == 0
? Enumerable.Range(0, layers.Count)
: Enumerable.Range(0, layers.Count).Reverse();
foreach (var layerIndex in layerIndices)
{
var layer = layers[layerIndex];
if (layer.Length == 0)
{
continue;
}
var desiredX = new double[layer.Length];
for (var nodeIndex = 0; nodeIndex < layer.Length; nodeIndex++)
{
var node = layer[nodeIndex];
var preferredCenter = ElkNodePlacementPreferredCenter.ResolvePreferredCenter(
node.Id,
incomingNodeIds,
outgoingNodeIds,
positionedNodes,
horizontal: false);
desiredX[nodeIndex] = preferredCenter.HasValue
? preferredCenter.Value - (node.Width / 2d)
: positionedNodes[node.Id].X;
}
for (var nodeIndex = 1; nodeIndex < layer.Length; nodeIndex++)
{
var minX = desiredX[nodeIndex - 1] + layer[nodeIndex - 1].Width + nodeSpacing;
if (desiredX[nodeIndex] < minX)
{
desiredX[nodeIndex] = minX;
}
}
for (var nodeIndex = 0; nodeIndex < layer.Length; nodeIndex++)
{
var current = positionedNodes[layer[nodeIndex].Id];
positionedNodes[layer[nodeIndex].Id] = ElkLayoutHelpers.CreatePositionedNode(
nodesById[layer[nodeIndex].Id],
desiredX[nodeIndex],
current.Y,
direction);
}
}
}
}
internal static void SnapOriginalPrimaryAxes(
Dictionary<string, ElkPositionedNode> positionedNodes,
IReadOnlyList<ElkNode[]> layers,
IReadOnlySet<string> dummyNodeIds,
IReadOnlyDictionary<string, List<string>> incomingNodeIds,
IReadOnlyDictionary<string, List<string>> outgoingNodeIds,
IReadOnlyDictionary<string, ElkNode> originalNodesById,
double nodeSpacing,
ElkLayoutDirection direction)
{
for (var iteration = 0; iteration < 3; iteration++)
{
foreach (var layer in layers)
{
var actualNodes = layer
.Where(node => !dummyNodeIds.Contains(node.Id) && originalNodesById.ContainsKey(node.Id))
.Select(node => originalNodesById[node.Id])
.ToArray();
if (actualNodes.Length == 0)
{
continue;
}
var nodeDesiredPairs = new (ElkNode Node, double Desired)[actualNodes.Length];
for (var nodeIndex = 0; nodeIndex < actualNodes.Length; nodeIndex++)
{
var positioned = positionedNodes[actualNodes[nodeIndex].Id];
var preferredCenter = ElkNodePlacementPreferredCenter.ResolveOriginalPreferredCenter(
actualNodes[nodeIndex].Id,
incomingNodeIds,
outgoingNodeIds,
positionedNodes,
horizontal: direction == ElkLayoutDirection.LeftToRight);
nodeDesiredPairs[nodeIndex] = (actualNodes[nodeIndex], preferredCenter.HasValue
? preferredCenter.Value - ((direction == ElkLayoutDirection.LeftToRight ? positioned.Height : positioned.Width) / 2d)
: (direction == ElkLayoutDirection.LeftToRight ? positioned.Y : positioned.X));
}
Array.Sort(nodeDesiredPairs, (a, b) => a.Desired.CompareTo(b.Desired));
var sortedNodes = nodeDesiredPairs.Select(p => p.Node).ToArray();
var desiredCoordinates = nodeDesiredPairs.Select(p => p.Desired).ToArray();
EnforceLinearSpacing(
sortedNodes,
desiredCoordinates,
nodeSpacing,
0d,
horizontal: direction == ElkLayoutDirection.LeftToRight);
for (var nodeIndex = 0; nodeIndex < sortedNodes.Length; nodeIndex++)
{
var current = positionedNodes[sortedNodes[nodeIndex].Id];
positionedNodes[sortedNodes[nodeIndex].Id] = direction == ElkLayoutDirection.LeftToRight
? ElkLayoutHelpers.CreatePositionedNode(sortedNodes[nodeIndex], current.X, desiredCoordinates[nodeIndex], direction)
: ElkLayoutHelpers.CreatePositionedNode(sortedNodes[nodeIndex], desiredCoordinates[nodeIndex], current.Y, direction);
}
}
}
}
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++)
{
var extent = horizontal ? layer[index - 1].Height : layer[index - 1].Width;
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--)
{
var extent = horizontal ? layer[index].Height : layer[index].Width;
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++)
{
var extent = horizontal ? layer[index - 1].Height : layer[index - 1].Width;
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;
}
}

View File

@@ -0,0 +1,171 @@
namespace StellaOps.ElkSharp;
internal static partial class ElkRepeatCollectorCorridors
{
private static bool ConflictsOnOuterLane(
CollectorCandidate left,
CollectorCandidate right,
double minLineClearance)
{
if (!string.Equals(left.TargetNodeId, right.TargetNodeId, StringComparison.Ordinal)
|| left.IsAbove != right.IsAbove)
{
return false;
}
if (Math.Min(left.MaxX, right.MaxX) - Math.Max(left.MinX, right.MinX) <= 1d)
{
return false;
}
return Math.Abs(left.CorridorY - right.CorridorY) < minLineClearance - CoordinateTolerance;
}
private static CollectorCandidate? CreateCandidate(
ElkRoutedEdge edge,
double graphMinY,
double graphMaxY)
{
if (!ElkEdgePostProcessor.IsRepeatCollectorLabel(edge.Label)
|| !ElkEdgePostProcessor.HasCorridorBendPoints(edge, graphMinY, graphMaxY)
|| string.IsNullOrWhiteSpace(edge.TargetNodeId))
{
return null;
}
var path = ExtractPath(edge);
if (path.Count < 2)
{
return null;
}
CollectorCandidate? best = null;
for (var i = 0; i < path.Count - 1; i++)
{
var start = path[i];
var end = path[i + 1];
if (Math.Abs(start.Y - end.Y) > CoordinateTolerance)
{
continue;
}
var y = (start.Y + end.Y) / 2d;
var isAbove = y < graphMinY - 8d;
var isBelow = y > graphMaxY + 8d;
if (!isAbove && !isBelow)
{
continue;
}
var length = Math.Abs(end.X - start.X);
if (length <= 1d)
{
continue;
}
var candidate = new CollectorCandidate(
edge.Id,
edge.TargetNodeId!,
isAbove,
y,
Math.Min(start.X, end.X),
Math.Max(start.X, end.X),
path[0].X,
length);
if (best is null || candidate.Length > best.Value.Length)
{
best = candidate;
}
}
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>();
foreach (var section in edge.Sections)
{
if (path.Count == 0)
{
path.Add(section.StartPoint);
}
path.AddRange(section.BendPoints);
path.Add(section.EndPoint);
}
return path;
}
private readonly record struct CollectorCandidate(
string EdgeId,
string TargetNodeId,
bool IsAbove,
double CorridorY,
double MinX,
double MaxX,
double StartX,
double Length);
private readonly record struct RepairMember(
int Index,
string EdgeId,
double CorridorY,
double StartX,
double MinX,
double MaxX);
}

View File

@@ -0,0 +1,47 @@
namespace StellaOps.ElkSharp;
internal static partial class ElkRepeatCollectorCorridors
{
private static ElkRoutedEdge RewriteOuterLane(
ElkRoutedEdge edge,
double currentY,
double assignedY,
double graphMinY,
double graphMaxY,
bool isAbove)
{
if (Math.Abs(currentY - assignedY) <= CoordinateTolerance)
{
return edge;
}
bool ShouldShift(ElkPoint point) =>
Math.Abs(point.Y - currentY) <= CoordinateTolerance
&& (isAbove
? point.Y < graphMinY - 8d
: point.Y > graphMaxY + 8d);
ElkPoint Shift(ElkPoint point) => ShouldShift(point)
? new ElkPoint { X = point.X, Y = assignedY }
: point;
return new ElkRoutedEdge
{
Id = edge.Id,
SourceNodeId = edge.SourceNodeId,
TargetNodeId = edge.TargetNodeId,
SourcePortId = edge.SourcePortId,
TargetPortId = edge.TargetPortId,
Kind = edge.Kind,
Label = edge.Label,
Sections = edge.Sections
.Select(section => new ElkEdgeSection
{
StartPoint = Shift(section.StartPoint),
EndPoint = Shift(section.EndPoint),
BendPoints = section.BendPoints.Select(Shift).ToArray(),
})
.ToArray(),
};
}
}

View File

@@ -11,7 +11,7 @@ internal sealed class ElkRepeatCollectorCorridorGroup
public required double MaxX { get; init; }
}
internal static class ElkRepeatCollectorCorridors
internal static partial class ElkRepeatCollectorCorridors
{
private const double CoordinateTolerance = 0.5d;
@@ -203,214 +203,4 @@ internal static class ElkRepeatCollectorCorridors
return result;
}
private static ElkRoutedEdge RewriteOuterLane(
ElkRoutedEdge edge,
double currentY,
double assignedY,
double graphMinY,
double graphMaxY,
bool isAbove)
{
if (Math.Abs(currentY - assignedY) <= CoordinateTolerance)
{
return edge;
}
bool ShouldShift(ElkPoint point) =>
Math.Abs(point.Y - currentY) <= CoordinateTolerance
&& (isAbove
? point.Y < graphMinY - 8d
: point.Y > graphMaxY + 8d);
ElkPoint Shift(ElkPoint point) => ShouldShift(point)
? new ElkPoint { X = point.X, Y = assignedY }
: point;
return new ElkRoutedEdge
{
Id = edge.Id,
SourceNodeId = edge.SourceNodeId,
TargetNodeId = edge.TargetNodeId,
SourcePortId = edge.SourcePortId,
TargetPortId = edge.TargetPortId,
Kind = edge.Kind,
Label = edge.Label,
Sections = edge.Sections
.Select(section => new ElkEdgeSection
{
StartPoint = Shift(section.StartPoint),
EndPoint = Shift(section.EndPoint),
BendPoints = section.BendPoints.Select(Shift).ToArray(),
})
.ToArray(),
};
}
private static bool ConflictsOnOuterLane(
CollectorCandidate left,
CollectorCandidate right,
double minLineClearance)
{
if (!string.Equals(left.TargetNodeId, right.TargetNodeId, StringComparison.Ordinal)
|| left.IsAbove != right.IsAbove)
{
return false;
}
if (Math.Min(left.MaxX, right.MaxX) - Math.Max(left.MinX, right.MinX) <= 1d)
{
return false;
}
return Math.Abs(left.CorridorY - right.CorridorY) < minLineClearance - CoordinateTolerance;
}
private static CollectorCandidate? CreateCandidate(
ElkRoutedEdge edge,
double graphMinY,
double graphMaxY)
{
if (!ElkEdgePostProcessor.IsRepeatCollectorLabel(edge.Label)
|| !ElkEdgePostProcessor.HasCorridorBendPoints(edge, graphMinY, graphMaxY)
|| string.IsNullOrWhiteSpace(edge.TargetNodeId))
{
return null;
}
var path = ExtractPath(edge);
if (path.Count < 2)
{
return null;
}
CollectorCandidate? best = null;
for (var i = 0; i < path.Count - 1; i++)
{
var start = path[i];
var end = path[i + 1];
if (Math.Abs(start.Y - end.Y) > CoordinateTolerance)
{
continue;
}
var y = (start.Y + end.Y) / 2d;
var isAbove = y < graphMinY - 8d;
var isBelow = y > graphMaxY + 8d;
if (!isAbove && !isBelow)
{
continue;
}
var length = Math.Abs(end.X - start.X);
if (length <= 1d)
{
continue;
}
var candidate = new CollectorCandidate(
edge.Id,
edge.TargetNodeId!,
isAbove,
y,
Math.Min(start.X, end.X),
Math.Max(start.X, end.X),
path[0].X,
length);
if (best is null || candidate.Length > best.Value.Length)
{
best = candidate;
}
}
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>();
foreach (var section in edge.Sections)
{
if (path.Count == 0)
{
path.Add(section.StartPoint);
}
path.AddRange(section.BendPoints);
path.Add(section.EndPoint);
}
return path;
}
private readonly record struct CollectorCandidate(
string EdgeId,
string TargetNodeId,
bool IsAbove,
double CorridorY,
double MinX,
double MaxX,
double StartX,
double Length);
private readonly record struct RepairMember(
int Index,
string EdgeId,
double CorridorY,
double StartX,
double MinX,
double MaxX);
}

View File

@@ -0,0 +1,182 @@
namespace StellaOps.ElkSharp;
internal static partial class ElkShapeBoundaries
{
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;
}
}
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),
};
}
}

View File

@@ -0,0 +1,131 @@
namespace StellaOps.ElkSharp;
internal static partial class ElkShapeBoundaries
{
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;
}
}

View File

@@ -0,0 +1,185 @@
namespace StellaOps.ElkSharp;
internal static partial class ElkShapeBoundaries
{
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);
}
}

View File

@@ -0,0 +1,300 @@
namespace StellaOps.ElkSharp;
internal static partial class ElkShapeBoundaries
{
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;
}
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);
}
}

View File

@@ -0,0 +1,243 @@
namespace StellaOps.ElkSharp;
internal static partial class ElkShapeBoundaries
{
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 ElkPoint IntersectPolygonBoundary(
double originX,
double originY,
double deltaX,
double deltaY,
IReadOnlyList<ElkPoint> polygon)
{
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;
}
}
return bestPoint ?? new ElkPoint
{
X = originX + deltaX,
Y = originY + deltaY,
};
}
internal static bool TryIntersectRayWithSegment(
double originX,
double originY,
double deltaX,
double deltaY,
ElkPoint segmentStart,
ElkPoint segmentEnd,
out double scale,
out ElkPoint point)
{
scale = double.PositiveInfinity;
point = default!;
var segmentDeltaX = segmentEnd.X - segmentStart.X;
var segmentDeltaY = segmentEnd.Y - segmentStart.Y;
var denominator = Cross(deltaX, deltaY, segmentDeltaX, segmentDeltaY);
if (Math.Abs(denominator) <= 0.001d)
{
return false;
}
var relativeX = segmentStart.X - originX;
var relativeY = segmentStart.Y - originY;
var rayScale = Cross(relativeX, relativeY, segmentDeltaX, segmentDeltaY) / denominator;
var segmentScale = Cross(relativeX, relativeY, deltaX, deltaY) / denominator;
if (rayScale < 0d || segmentScale < 0d || segmentScale > 1d)
{
return false;
}
scale = rayScale;
point = new ElkPoint
{
X = originX + (deltaX * rayScale),
Y = originY + (deltaY * rayScale),
};
return true;
}
internal static double Cross(double ax, double ay, double bx, double by)
{
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;
}
}

File diff suppressed because it is too large Load Diff