Refactor ElkSharp hybrid routing and document speed path
This commit is contained in:
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
157
src/__Libraries/StellaOps.ElkSharp/ElkCompoundLayout.Ordering.cs
Normal file
157
src/__Libraries/StellaOps.ElkSharp/ElkCompoundLayout.Ordering.cs
Normal 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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
170
src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterHighway.Paths.cs
Normal file
170
src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterHighway.Paths.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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}";
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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()
|
||||
: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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))}");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
80
src/__Libraries/StellaOps.ElkSharp/ElkLayoutTypes.Retry.cs
Normal file
80
src/__Libraries/StellaOps.ElkSharp/ElkLayoutTypes.Retry.cs
Normal 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);
|
||||
159
src/__Libraries/StellaOps.ElkSharp/ElkLayoutTypes.Strategy.cs
Normal file
159
src/__Libraries/StellaOps.ElkSharp/ElkLayoutTypes.Strategy.cs
Normal 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);
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
188
src/__Libraries/StellaOps.ElkSharp/ElkNodePlacement.Grid.cs
Normal file
188
src/__Libraries/StellaOps.ElkSharp/ElkNodePlacement.Grid.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user