ElkSharp: gateway face overflow redirect, under-node push-first routing, boundary-slot snap
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,517 @@
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
|
||||
using NUnit.Framework;
|
||||
|
||||
using StellaOps.ElkSharp;
|
||||
using StellaOps.Workflow.Abstractions;
|
||||
|
||||
namespace StellaOps.Workflow.Renderer.Tests;
|
||||
|
||||
public partial class DocumentProcessingWorkflowRenderingTests
|
||||
{
|
||||
[Test]
|
||||
public void DocumentProcessingWorkflow_WhenInspectingLatestElkSharpArtifact_ShouldReportBoundarySlotOffenders()
|
||||
{
|
||||
var workflowRenderingsDirectory = Path.Combine(
|
||||
Path.GetDirectoryName(typeof(DocumentProcessingWorkflowRenderingTests).Assembly.Location)!,
|
||||
"TestResults",
|
||||
"workflow-renderings");
|
||||
var outputDir = Directory.GetDirectories(workflowRenderingsDirectory)
|
||||
.OrderByDescending(path => Path.GetFileName(path), StringComparer.Ordinal)
|
||||
.Select(path => Path.Combine(path, "DocumentProcessingWorkflow"))
|
||||
.First(Directory.Exists);
|
||||
var jsonPath = Path.Combine(outputDir, "elksharp.json");
|
||||
Assert.That(File.Exists(jsonPath), Is.True);
|
||||
|
||||
var layout = JsonSerializer.Deserialize<WorkflowRenderLayoutResult>(
|
||||
File.ReadAllText(jsonPath),
|
||||
new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
});
|
||||
Assert.That(layout, Is.Not.Null);
|
||||
|
||||
var elkNodes = layout!.Nodes.Select(node => new ElkPositionedNode
|
||||
{
|
||||
Id = node.Id,
|
||||
Label = node.Label,
|
||||
Kind = node.Kind,
|
||||
X = node.X,
|
||||
Y = node.Y,
|
||||
Width = node.Width,
|
||||
Height = node.Height,
|
||||
}).ToArray();
|
||||
var elkEdges = layout.Edges.Select(edge => new ElkRoutedEdge
|
||||
{
|
||||
Id = edge.Id,
|
||||
SourceNodeId = edge.SourceNodeId,
|
||||
TargetNodeId = edge.TargetNodeId,
|
||||
Kind = edge.Kind,
|
||||
Label = edge.Label,
|
||||
Sections = edge.Sections.Select(section => new ElkEdgeSection
|
||||
{
|
||||
StartPoint = new ElkPoint { X = section.StartPoint.X, Y = section.StartPoint.Y },
|
||||
EndPoint = new ElkPoint { X = section.EndPoint.X, Y = section.EndPoint.Y },
|
||||
BendPoints = section.BendPoints.Select(point => new ElkPoint { X = point.X, Y = point.Y }).ToArray(),
|
||||
}).ToArray(),
|
||||
}).ToArray();
|
||||
|
||||
var severityByEdgeId = new Dictionary<string, int>(StringComparer.Ordinal);
|
||||
var count = ElkEdgeRoutingScoring.CountBoundarySlotViolations(elkEdges, elkNodes, severityByEdgeId, 1);
|
||||
TestContext.Out.WriteLine($"boundary-slot artifact count: {count}");
|
||||
foreach (var offender in severityByEdgeId.OrderBy(pair => pair.Key, StringComparer.Ordinal))
|
||||
{
|
||||
var edge = elkEdges.Single(candidate => candidate.Id == offender.Key);
|
||||
TestContext.Out.WriteLine(
|
||||
$"{offender.Key}: {string.Join(" -> ", ExtractElkPath(edge).Select(point => $"({point.X:F3},{point.Y:F3})"))}");
|
||||
}
|
||||
|
||||
var serviceNodes = elkNodes.Where(node => node.Kind is not "Start" and not "End").ToArray();
|
||||
var minLineClearance = serviceNodes.Length > 0
|
||||
? Math.Min(serviceNodes.Average(node => node.Width), serviceNodes.Average(node => node.Height)) / 2d
|
||||
: 50d;
|
||||
var nodesById = elkNodes.ToDictionary(node => node.Id, StringComparer.Ordinal);
|
||||
var graphMinY = elkNodes.Min(node => node.Y);
|
||||
var graphMaxY = elkNodes.Max(node => node.Y + node.Height);
|
||||
var (sourceSlots, targetSlots) = ElkEdgePostProcessor.ResolveCombinedBoundarySlots(
|
||||
elkEdges,
|
||||
nodesById,
|
||||
graphMinY,
|
||||
graphMaxY,
|
||||
restrictedEdgeIds: null,
|
||||
enforceAllNodeEndpoints: true);
|
||||
if (sourceSlots.TryGetValue("edge/25", out var sourceSlot))
|
||||
{
|
||||
TestContext.Out.WriteLine(
|
||||
$"edge/25 source-slot: side={sourceSlot.Side} boundary=({sourceSlot.Boundary.X:F3},{sourceSlot.Boundary.Y:F3})");
|
||||
}
|
||||
|
||||
if (targetSlots.TryGetValue("edge/25", out var targetSlot))
|
||||
{
|
||||
TestContext.Out.WriteLine(
|
||||
$"edge/25 target-slot: side={targetSlot.Side} boundary=({targetSlot.Boundary.X:F3},{targetSlot.Boundary.Y:F3})");
|
||||
|
||||
var edge25 = elkEdges.Single(edge => edge.Id == "edge/25");
|
||||
var edge25Path = ExtractElkPath(edge25);
|
||||
var buildTargetApproachCandidatePath = typeof(ElkEdgePostProcessor).GetMethod(
|
||||
"BuildTargetApproachCandidatePath",
|
||||
BindingFlags.NonPublic | BindingFlags.Static);
|
||||
Assert.That(buildTargetApproachCandidatePath, Is.Not.Null);
|
||||
var reflectedTargetCandidate = (List<ElkPoint>)buildTargetApproachCandidatePath!.Invoke(
|
||||
null,
|
||||
[edge25Path, elkNodes.Single(node => node.Id == edge25.TargetNodeId), targetSlot.Side, targetSlot.Boundary, edge25Path[2].Y])!;
|
||||
TestContext.Out.WriteLine(
|
||||
$"edge/25 reflected-target-candidate: {string.Join(" -> ", reflectedTargetCandidate.Select(point => $"({point.X:F3},{point.Y:F3})"))}");
|
||||
}
|
||||
|
||||
if (targetSlots.TryGetValue("edge/5", out var edge5TargetSlot))
|
||||
{
|
||||
TestContext.Out.WriteLine(
|
||||
$"edge/5 target-slot: side={edge5TargetSlot.Side} boundary=({edge5TargetSlot.Boundary.X:F3},{edge5TargetSlot.Boundary.Y:F3})");
|
||||
|
||||
var edge5 = elkEdges.Single(edge => edge.Id == "edge/5");
|
||||
var edge5Path = ExtractElkPath(edge5);
|
||||
var targetNode = elkNodes.Single(node => node.Id == edge5.TargetNodeId);
|
||||
var postProcessorType = typeof(ElkEdgePostProcessor);
|
||||
var buildTargetApproachCandidatePath = postProcessorType.GetMethod(
|
||||
"BuildTargetApproachCandidatePath",
|
||||
BindingFlags.NonPublic | BindingFlags.Static);
|
||||
var resolveTargetApproachAxisValue = postProcessorType.GetMethod(
|
||||
"ResolveTargetApproachAxisValue",
|
||||
BindingFlags.NonPublic | BindingFlags.Static);
|
||||
var resolveDefaultTargetApproachAxis = postProcessorType.GetMethod(
|
||||
"ResolveDefaultTargetApproachAxis",
|
||||
BindingFlags.NonPublic | BindingFlags.Static);
|
||||
var normalizeEntryPath = postProcessorType
|
||||
.GetMethods(BindingFlags.NonPublic | BindingFlags.Static)
|
||||
.Single(method =>
|
||||
method.Name == "NormalizeEntryPath"
|
||||
&& method.GetParameters().Length == 4);
|
||||
var canAcceptGatewayTargetRepair = postProcessorType.GetMethod(
|
||||
"CanAcceptGatewayTargetRepair",
|
||||
BindingFlags.NonPublic | BindingFlags.Static);
|
||||
var hasAcceptableGatewayBoundaryPath = postProcessorType.GetMethod(
|
||||
"HasAcceptableGatewayBoundaryPath",
|
||||
BindingFlags.NonPublic | BindingFlags.Static);
|
||||
var resolveTargetApproachSide = postProcessorType.GetMethod(
|
||||
"ResolveTargetApproachSide",
|
||||
BindingFlags.NonPublic | BindingFlags.Static);
|
||||
var isAcceptableStrictBoundarySlotCandidate = postProcessorType.GetMethod(
|
||||
"IsAcceptableStrictBoundarySlotCandidate",
|
||||
BindingFlags.NonPublic | BindingFlags.Static);
|
||||
Assert.That(buildTargetApproachCandidatePath, Is.Not.Null);
|
||||
Assert.That(resolveTargetApproachAxisValue, Is.Not.Null);
|
||||
Assert.That(resolveDefaultTargetApproachAxis, Is.Not.Null);
|
||||
Assert.That(canAcceptGatewayTargetRepair, Is.Not.Null);
|
||||
Assert.That(hasAcceptableGatewayBoundaryPath, Is.Not.Null);
|
||||
Assert.That(resolveTargetApproachSide, Is.Not.Null);
|
||||
Assert.That(isAcceptableStrictBoundarySlotCandidate, Is.Not.Null);
|
||||
|
||||
var desiredTargetAxis = (double)resolveTargetApproachAxisValue!.Invoke(
|
||||
null,
|
||||
[edge5Path, edge5TargetSlot.Side])!;
|
||||
if (double.IsNaN(desiredTargetAxis))
|
||||
{
|
||||
desiredTargetAxis = (double)resolveDefaultTargetApproachAxis!.Invoke(
|
||||
null,
|
||||
[targetNode, edge5TargetSlot.Side])!;
|
||||
}
|
||||
|
||||
var reflectedTargetCandidate = (List<ElkPoint>)buildTargetApproachCandidatePath!.Invoke(
|
||||
null,
|
||||
[edge5Path, targetNode, edge5TargetSlot.Side, edge5TargetSlot.Boundary, desiredTargetAxis])!;
|
||||
var strictTargetCandidate = (List<ElkPoint>)normalizeEntryPath.Invoke(
|
||||
null,
|
||||
[edge5Path, targetNode, edge5TargetSlot.Side, edge5TargetSlot.Boundary])!;
|
||||
var reflectedTargetCandidateEdge = new ElkRoutedEdge[]
|
||||
{
|
||||
edge5 with
|
||||
{
|
||||
Sections =
|
||||
[
|
||||
new ElkEdgeSection
|
||||
{
|
||||
StartPoint = new ElkPoint { X = reflectedTargetCandidate[0].X, Y = reflectedTargetCandidate[0].Y },
|
||||
EndPoint = new ElkPoint { X = reflectedTargetCandidate[^1].X, Y = reflectedTargetCandidate[^1].Y },
|
||||
BendPoints = reflectedTargetCandidate.Skip(1).SkipLast(1)
|
||||
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
||||
.ToArray(),
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
TestContext.Out.WriteLine(
|
||||
$"edge/5 reflected-target-candidate: {string.Join(" -> ", reflectedTargetCandidate.Select(point => $"({point.X:F3},{point.Y:F3})"))}");
|
||||
TestContext.Out.WriteLine(
|
||||
$"edge/5 reflected-target-candidate accepts-gateway={canAcceptGatewayTargetRepair!.Invoke(null, [reflectedTargetCandidate, targetNode])} boundary={hasAcceptableGatewayBoundaryPath!.Invoke(null, [reflectedTargetCandidate, elkNodes, edge5.SourceNodeId, edge5.TargetNodeId, targetNode, false])} strict={isAcceptableStrictBoundarySlotCandidate!.Invoke(null, [edge5, edge5Path, reflectedTargetCandidate, targetNode, false, elkNodes, graphMinY, graphMaxY])} side={resolveTargetApproachSide!.Invoke(null, [reflectedTargetCandidate, targetNode])}");
|
||||
TestContext.Out.WriteLine(
|
||||
$"edge/5 reflected-target-candidate score: boundary={ElkEdgeRoutingScoring.CountBoundarySlotViolations(reflectedTargetCandidateEdge, elkNodes)} long-diagonal={ElkEdgeRoutingScoring.CountLongDiagonalViolations(reflectedTargetCandidateEdge, elkNodes)}");
|
||||
TestContext.Out.WriteLine(
|
||||
$"edge/5 strict-target-candidate: {string.Join(" -> ", strictTargetCandidate.Select(point => $"({point.X:F3},{point.Y:F3})"))}");
|
||||
TestContext.Out.WriteLine(
|
||||
$"edge/5 strict-target-candidate accepts-gateway={canAcceptGatewayTargetRepair.Invoke(null, [strictTargetCandidate, targetNode])} boundary={hasAcceptableGatewayBoundaryPath.Invoke(null, [strictTargetCandidate, elkNodes, edge5.SourceNodeId, edge5.TargetNodeId, targetNode, false])} strict={isAcceptableStrictBoundarySlotCandidate.Invoke(null, [edge5, edge5Path, strictTargetCandidate, targetNode, false, elkNodes, graphMinY, graphMaxY])} side={resolveTargetApproachSide.Invoke(null, [strictTargetCandidate, targetNode])}");
|
||||
}
|
||||
|
||||
var snapped = ElkEdgePostProcessor.SnapBoundarySlotAssignments(elkEdges, elkNodes, minLineClearance);
|
||||
var snappedSeverityByEdgeId = new Dictionary<string, int>(StringComparer.Ordinal);
|
||||
var snappedCount = ElkEdgeRoutingScoring.CountBoundarySlotViolations(snapped, elkNodes, snappedSeverityByEdgeId, 1);
|
||||
TestContext.Out.WriteLine($"boundary-slot snapped count: {snappedCount}");
|
||||
if (snappedSeverityByEdgeId.TryGetValue("edge/25", out _))
|
||||
{
|
||||
var snappedEdge = snapped.Single(candidate => candidate.Id == "edge/25");
|
||||
TestContext.Out.WriteLine(
|
||||
$"edge/25 snapped: {string.Join(" -> ", ExtractElkPath(snappedEdge).Select(point => $"({point.X:F3},{point.Y:F3})"))}");
|
||||
}
|
||||
|
||||
var edge5FocusedSnap = ElkEdgePostProcessor.SnapBoundarySlotAssignments(
|
||||
elkEdges,
|
||||
elkNodes,
|
||||
minLineClearance,
|
||||
["edge/5"],
|
||||
enforceAllNodeEndpoints: true);
|
||||
var edge5FocusedSnapPath = ExtractElkPath(edge5FocusedSnap.Single(edge => edge.Id == "edge/5"));
|
||||
TestContext.Out.WriteLine(
|
||||
$"edge/5 focused-snapped: {string.Join(" -> ", edge5FocusedSnapPath.Select(point => $"({point.X:F3},{point.Y:F3})"))}");
|
||||
TestContext.Out.WriteLine(
|
||||
$"edge/5 focused-snapped score: boundary={ElkEdgeRoutingScoring.CountBoundarySlotViolations(edge5FocusedSnap, elkNodes)} long-diagonal={ElkEdgeRoutingScoring.CountLongDiagonalViolations(edge5FocusedSnap, elkNodes)}");
|
||||
|
||||
var detourSeverityByEdgeId = new Dictionary<string, int>(StringComparer.Ordinal);
|
||||
var detourCount = ElkEdgeRoutingScoring.CountExcessiveDetourViolations(elkEdges, elkNodes, detourSeverityByEdgeId, 1);
|
||||
TestContext.Out.WriteLine($"detour artifact count: {detourCount}");
|
||||
foreach (var offender in detourSeverityByEdgeId.OrderByDescending(pair => pair.Value).ThenBy(pair => pair.Key, StringComparer.Ordinal))
|
||||
{
|
||||
var edge = elkEdges.Single(candidate => candidate.Id == offender.Key);
|
||||
TestContext.Out.WriteLine(
|
||||
$"{offender.Key} detour={offender.Value}: {string.Join(" -> ", ExtractElkPath(edge).Select(point => $"({point.X:F3},{point.Y:F3})"))}");
|
||||
|
||||
var shortcutCandidate = ElkEdgePostProcessor.PreferShortestBoundaryShortcuts([edge], elkNodes)[0];
|
||||
if (!ExtractElkPath(shortcutCandidate).SequenceEqual(ExtractElkPath(edge), ElkPointComparer.Instance))
|
||||
{
|
||||
var shortcutDetourSeverity = new Dictionary<string, int>(StringComparer.Ordinal);
|
||||
var shortcutGatewaySeverity = new Dictionary<string, int>(StringComparer.Ordinal);
|
||||
var shortcutDetourCount = ElkEdgeRoutingScoring.CountExcessiveDetourViolations(
|
||||
[shortcutCandidate],
|
||||
elkNodes,
|
||||
shortcutDetourSeverity,
|
||||
1);
|
||||
var shortcutGatewayCount = ElkEdgeRoutingScoring.CountGatewaySourceExitViolations(
|
||||
[shortcutCandidate],
|
||||
elkNodes,
|
||||
shortcutGatewaySeverity,
|
||||
1);
|
||||
TestContext.Out.WriteLine(
|
||||
$" shortcut -> detour={shortcutDetourCount} gateway-source={shortcutGatewayCount}: {string.Join(" -> ", ExtractElkPath(shortcutCandidate).Select(point => $"({point.X:F3},{point.Y:F3})"))}");
|
||||
}
|
||||
else
|
||||
{
|
||||
TestContext.Out.WriteLine(" shortcut -> unchanged");
|
||||
}
|
||||
}
|
||||
|
||||
var gatewaySourceSeverityByEdgeId = new Dictionary<string, int>(StringComparer.Ordinal);
|
||||
var gatewaySourceCount = ElkEdgeRoutingScoring.CountGatewaySourceExitViolations(elkEdges, elkNodes, gatewaySourceSeverityByEdgeId, 1);
|
||||
TestContext.Out.WriteLine($"gateway-source artifact count: {gatewaySourceCount}");
|
||||
foreach (var offender in gatewaySourceSeverityByEdgeId.OrderByDescending(pair => pair.Value).ThenBy(pair => pair.Key, StringComparer.Ordinal))
|
||||
{
|
||||
var edge = elkEdges.Single(candidate => candidate.Id == offender.Key);
|
||||
TestContext.Out.WriteLine(
|
||||
$"{offender.Key} gateway-source={offender.Value}: {string.Join(" -> ", ExtractElkPath(edge).Select(point => $"({point.X:F3},{point.Y:F3})"))}");
|
||||
}
|
||||
|
||||
var sharedLaneConflicts = ElkEdgeRoutingScoring.DetectSharedLaneConflicts(elkEdges, elkNodes)
|
||||
.Distinct()
|
||||
.OrderBy(conflict => conflict.LeftEdgeId, StringComparer.Ordinal)
|
||||
.ThenBy(conflict => conflict.RightEdgeId, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
TestContext.Out.WriteLine($"shared-lane artifact count: {sharedLaneConflicts.Length}");
|
||||
foreach (var conflict in sharedLaneConflicts)
|
||||
{
|
||||
TestContext.Out.WriteLine($"shared-lane artifact pair: {conflict.LeftEdgeId}+{conflict.RightEdgeId}");
|
||||
}
|
||||
|
||||
var layoutNodesById = layout.Nodes.ToDictionary(node => node.Id, StringComparer.Ordinal);
|
||||
var gatewayCornerDiagonalOffenders = layout.Edges
|
||||
.Where(edge =>
|
||||
HasGatewayCornerDiagonal(edge, layoutNodesById[edge.SourceNodeId], fromSource: true)
|
||||
|| HasGatewayCornerDiagonal(edge, layoutNodesById[edge.TargetNodeId], fromSource: false))
|
||||
.Select(edge => edge.Id)
|
||||
.OrderBy(edgeId => edgeId, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
TestContext.Out.WriteLine($"gateway-corner artifact count: {gatewayCornerDiagonalOffenders.Length}");
|
||||
foreach (var edgeId in gatewayCornerDiagonalOffenders)
|
||||
{
|
||||
TestContext.Out.WriteLine($"gateway-corner artifact edge: {edgeId}");
|
||||
}
|
||||
|
||||
var gatewayInteriorAdjacentOffenders = layout.Edges
|
||||
.Where(edge =>
|
||||
HasGatewayInteriorAdjacentPoint(edge, layoutNodesById[edge.SourceNodeId], fromSource: true)
|
||||
|| HasGatewayInteriorAdjacentPoint(edge, layoutNodesById[edge.TargetNodeId], fromSource: false))
|
||||
.Select(edge => edge.Id)
|
||||
.OrderBy(edgeId => edgeId, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
TestContext.Out.WriteLine($"gateway-interior-adjacent artifact count: {gatewayInteriorAdjacentOffenders.Length}");
|
||||
foreach (var edgeId in gatewayInteriorAdjacentOffenders)
|
||||
{
|
||||
TestContext.Out.WriteLine($"gateway-interior-adjacent artifact edge: {edgeId}");
|
||||
}
|
||||
|
||||
var gatewaySourceCurlOffenders = layout.Edges
|
||||
.Where(edge => HasGatewaySourceExitCurl(edge, layoutNodesById[edge.SourceNodeId]))
|
||||
.Select(edge => edge.Id)
|
||||
.OrderBy(edgeId => edgeId, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
TestContext.Out.WriteLine($"gateway-source-curl artifact count: {gatewaySourceCurlOffenders.Length}");
|
||||
foreach (var edgeId in gatewaySourceCurlOffenders)
|
||||
{
|
||||
TestContext.Out.WriteLine($"gateway-source-curl artifact edge: {edgeId}");
|
||||
}
|
||||
|
||||
var gatewaySourceFaceMismatchOffenders = layout.Edges
|
||||
.Where(edge => HasGatewaySourcePreferredFaceMismatch(edge, layoutNodesById[edge.SourceNodeId], layout.Nodes))
|
||||
.Select(edge => edge.Id)
|
||||
.OrderBy(edgeId => edgeId, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
TestContext.Out.WriteLine($"gateway-source-face-mismatch artifact count: {gatewaySourceFaceMismatchOffenders.Length}");
|
||||
foreach (var edgeId in gatewaySourceFaceMismatchOffenders)
|
||||
{
|
||||
TestContext.Out.WriteLine($"gateway-source-face-mismatch artifact edge: {edgeId}");
|
||||
}
|
||||
|
||||
var gatewaySourceDetourOffenders = layout.Edges
|
||||
.Where(edge => HasGatewaySourceDominantAxisDetour(edge, layoutNodesById[edge.SourceNodeId], layout.Nodes))
|
||||
.Select(edge => edge.Id)
|
||||
.OrderBy(edgeId => edgeId, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
TestContext.Out.WriteLine($"gateway-source-detour artifact count: {gatewaySourceDetourOffenders.Length}");
|
||||
foreach (var edgeId in gatewaySourceDetourOffenders)
|
||||
{
|
||||
TestContext.Out.WriteLine($"gateway-source-detour artifact edge: {edgeId}");
|
||||
}
|
||||
|
||||
var gatewaySourceVertexExitOffenders = layout.Edges
|
||||
.Where(edge => HasGatewaySourceVertexExit(edge, layoutNodesById[edge.SourceNodeId]))
|
||||
.Select(edge => edge.Id)
|
||||
.OrderBy(edgeId => edgeId, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
TestContext.Out.WriteLine($"gateway-source-vertex-exit artifact count: {gatewaySourceVertexExitOffenders.Length}");
|
||||
foreach (var edgeId in gatewaySourceVertexExitOffenders)
|
||||
{
|
||||
TestContext.Out.WriteLine($"gateway-source-vertex-exit artifact edge: {edgeId}");
|
||||
}
|
||||
|
||||
var gatewaySourceScoringOffenders = layout.Edges
|
||||
.Where(edge => HasGatewaySourceScoringIssue(edge, layout.Nodes))
|
||||
.Select(edge => edge.Id)
|
||||
.OrderBy(edgeId => edgeId, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
TestContext.Out.WriteLine($"gateway-source-scoring artifact count: {gatewaySourceScoringOffenders.Length}");
|
||||
foreach (var edgeId in gatewaySourceScoringOffenders)
|
||||
{
|
||||
TestContext.Out.WriteLine($"gateway-source-scoring artifact edge: {edgeId}");
|
||||
}
|
||||
|
||||
var loadConfigurationNode = layout.Nodes.Single(node => node.Id == "start/3");
|
||||
var processBatchLoopOffenders = layout.Edges
|
||||
.Where(edge => edge.TargetNodeId == "start/2/branch-1/1"
|
||||
&& edge.Label.StartsWith("repeat while", StringComparison.Ordinal)
|
||||
&& HasNearNodeClearanceViolation(edge, loadConfigurationNode, 40d))
|
||||
.Select(edge => edge.Id)
|
||||
.OrderBy(edgeId => edgeId, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
TestContext.Out.WriteLine($"process-batch-clearance artifact count: {processBatchLoopOffenders.Length}");
|
||||
foreach (var edgeId in processBatchLoopOffenders)
|
||||
{
|
||||
TestContext.Out.WriteLine($"process-batch-clearance artifact edge: {edgeId}");
|
||||
}
|
||||
|
||||
var crossings = 0;
|
||||
foreach (var node in layout.Nodes)
|
||||
{
|
||||
foreach (var edge in layout.Edges)
|
||||
{
|
||||
if (edge.SourceNodeId == node.Id || edge.TargetNodeId == node.Id) continue;
|
||||
foreach (var section in edge.Sections)
|
||||
{
|
||||
var points = new List<WorkflowRenderPoint> { section.StartPoint };
|
||||
points.AddRange(section.BendPoints);
|
||||
points.Add(section.EndPoint);
|
||||
for (var i = 0; i < points.Count - 1; i++)
|
||||
{
|
||||
var start = points[i];
|
||||
var end = points[i + 1];
|
||||
if (Math.Abs(start.Y - end.Y) < 2d && start.Y > node.Y && start.Y < node.Y + node.Height)
|
||||
{
|
||||
if (Math.Max(start.X, end.X) > node.X && Math.Min(start.X, end.X) < node.X + node.Width)
|
||||
{
|
||||
crossings++;
|
||||
}
|
||||
}
|
||||
else if (Math.Abs(start.X - end.X) < 2d && start.X > node.X && start.X < node.X + node.Width)
|
||||
{
|
||||
if (Math.Max(start.Y, end.Y) > node.Y && Math.Min(start.Y, end.Y) < node.Y + node.Height)
|
||||
{
|
||||
crossings++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
TestContext.Out.WriteLine($"edge-node-crossing artifact count: {crossings}");
|
||||
|
||||
var focusedSharedLaneCandidate = ElkEdgePostProcessor.SeparateSharedLaneConflicts(
|
||||
elkEdges,
|
||||
elkNodes,
|
||||
minLineClearance,
|
||||
["edge/15", "edge/17", "edge/35"]);
|
||||
TestContext.Out.WriteLine(
|
||||
$"focused shared-lane candidate counts: detour={ElkEdgeRoutingScoring.CountExcessiveDetourViolations(focusedSharedLaneCandidate, elkNodes)} gateway-source={ElkEdgeRoutingScoring.CountGatewaySourceExitViolations(focusedSharedLaneCandidate, elkNodes)} boundary-slots={ElkEdgeRoutingScoring.CountBoundarySlotViolations(focusedSharedLaneCandidate, elkNodes)} entry={ElkEdgeRoutingScoring.CountBadEntryAngles(focusedSharedLaneCandidate, elkNodes)} shared-lanes={ElkEdgeRoutingScoring.CountSharedLaneViolations(focusedSharedLaneCandidate, elkNodes)}");
|
||||
foreach (var edgeId in new[] { "edge/15", "edge/17", "edge/35" })
|
||||
{
|
||||
var currentEdge = elkEdges.Single(edge => edge.Id == edgeId);
|
||||
var candidateEdge = focusedSharedLaneCandidate.Single(edge => edge.Id == edgeId);
|
||||
TestContext.Out.WriteLine(
|
||||
$"focused shared-lane {edgeId} current: {string.Join(" -> ", ExtractElkPath(currentEdge).Select(point => $"({point.X:F3},{point.Y:F3})"))}");
|
||||
TestContext.Out.WriteLine(
|
||||
$"focused shared-lane {edgeId} candidate: {string.Join(" -> ", ExtractElkPath(candidateEdge).Select(point => $"({point.X:F3},{point.Y:F3})"))}");
|
||||
}
|
||||
|
||||
var combinedFocus = detourSeverityByEdgeId.Keys
|
||||
.Concat(gatewaySourceSeverityByEdgeId.Keys)
|
||||
.OrderBy(edgeId => edgeId, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
if (combinedFocus.Length > 0)
|
||||
{
|
||||
var batchCandidate = ElkEdgePostProcessor.PreferShortestBoundaryShortcuts(elkEdges, elkNodes, combinedFocus);
|
||||
batchCandidate = ElkEdgePostProcessor.RepairBoundaryAnglesAndTargetApproaches(batchCandidate, elkNodes, minLineClearance, combinedFocus);
|
||||
batchCandidate = ElkEdgePostProcessor.SpreadTargetApproachJoins(batchCandidate, elkNodes, minLineClearance, combinedFocus);
|
||||
batchCandidate = ElkEdgePostProcessor.SpreadRectTargetApproachFeederBands(batchCandidate, elkNodes, minLineClearance, combinedFocus);
|
||||
batchCandidate = ElkEdgePostProcessor.ElevateUnderNodeViolations(batchCandidate, elkNodes, minLineClearance, combinedFocus);
|
||||
batchCandidate = ElkEdgePostProcessor.FinalizeDecisionTargetEntries(batchCandidate, elkNodes, combinedFocus);
|
||||
batchCandidate = ElkEdgePostProcessor.FinalizeGatewayBoundaryGeometry(batchCandidate, elkNodes, combinedFocus);
|
||||
batchCandidate = ElkEdgePostProcessor.PolishTargetPeerConflicts(batchCandidate, elkNodes, minLineClearance, combinedFocus);
|
||||
batchCandidate = ElkEdgePostProcessor.SnapBoundarySlotAssignments(
|
||||
batchCandidate,
|
||||
elkNodes,
|
||||
minLineClearance,
|
||||
combinedFocus,
|
||||
enforceAllNodeEndpoints: true);
|
||||
|
||||
var batchDetourCount = ElkEdgeRoutingScoring.CountExcessiveDetourViolations(batchCandidate, elkNodes);
|
||||
var batchGatewaySourceCount = ElkEdgeRoutingScoring.CountGatewaySourceExitViolations(batchCandidate, elkNodes);
|
||||
var batchBoundarySlotCount = ElkEdgeRoutingScoring.CountBoundarySlotViolations(batchCandidate, elkNodes);
|
||||
var batchEntryCount = ElkEdgeRoutingScoring.CountBadEntryAngles(batchCandidate, elkNodes);
|
||||
var batchSharedLaneCount = ElkEdgeRoutingScoring.CountSharedLaneViolations(batchCandidate, elkNodes);
|
||||
TestContext.Out.WriteLine(
|
||||
$"batch-candidate counts: detour={batchDetourCount} gateway-source={batchGatewaySourceCount} boundary-slots={batchBoundarySlotCount} entry={batchEntryCount} shared-lanes={batchSharedLaneCount}");
|
||||
|
||||
foreach (var focusEdgeId in combinedFocus)
|
||||
{
|
||||
var focusedCandidate = ElkEdgePostProcessor.PreferShortestBoundaryShortcuts(elkEdges, elkNodes, [focusEdgeId]);
|
||||
focusedCandidate = ElkEdgePostProcessor.RepairBoundaryAnglesAndTargetApproaches(focusedCandidate, elkNodes, minLineClearance, [focusEdgeId]);
|
||||
focusedCandidate = ElkEdgePostProcessor.SpreadTargetApproachJoins(focusedCandidate, elkNodes, minLineClearance, [focusEdgeId]);
|
||||
focusedCandidate = ElkEdgePostProcessor.SpreadRectTargetApproachFeederBands(focusedCandidate, elkNodes, minLineClearance, [focusEdgeId]);
|
||||
focusedCandidate = ElkEdgePostProcessor.ElevateUnderNodeViolations(focusedCandidate, elkNodes, minLineClearance, [focusEdgeId]);
|
||||
focusedCandidate = ElkEdgePostProcessor.FinalizeDecisionTargetEntries(focusedCandidate, elkNodes, [focusEdgeId]);
|
||||
focusedCandidate = ElkEdgePostProcessor.FinalizeGatewayBoundaryGeometry(focusedCandidate, elkNodes, [focusEdgeId]);
|
||||
focusedCandidate = ElkEdgePostProcessor.PolishTargetPeerConflicts(focusedCandidate, elkNodes, minLineClearance, [focusEdgeId]);
|
||||
focusedCandidate = ElkEdgePostProcessor.SnapBoundarySlotAssignments(
|
||||
focusedCandidate,
|
||||
elkNodes,
|
||||
minLineClearance,
|
||||
[focusEdgeId],
|
||||
enforceAllNodeEndpoints: true);
|
||||
|
||||
TestContext.Out.WriteLine(
|
||||
$"focused {focusEdgeId}: detour={ElkEdgeRoutingScoring.CountExcessiveDetourViolations(focusedCandidate, elkNodes)} gateway-source={ElkEdgeRoutingScoring.CountGatewaySourceExitViolations(focusedCandidate, elkNodes)} boundary-slots={ElkEdgeRoutingScoring.CountBoundarySlotViolations(focusedCandidate, elkNodes)} entry={ElkEdgeRoutingScoring.CountBadEntryAngles(focusedCandidate, elkNodes)} shared-lanes={ElkEdgeRoutingScoring.CountSharedLaneViolations(focusedCandidate, elkNodes)}");
|
||||
}
|
||||
|
||||
var subsetResults = new List<(string[] Focus, int Detour, int GatewaySource, int BoundarySlots, int Entry, int SharedLanes)>();
|
||||
for (var mask = 1; mask < (1 << combinedFocus.Length); mask++)
|
||||
{
|
||||
var subset = combinedFocus
|
||||
.Where((_, index) => (mask & (1 << index)) != 0)
|
||||
.ToArray();
|
||||
var subsetCandidate = ElkEdgePostProcessor.PreferShortestBoundaryShortcuts(elkEdges, elkNodes, subset);
|
||||
subsetCandidate = ElkEdgePostProcessor.RepairBoundaryAnglesAndTargetApproaches(subsetCandidate, elkNodes, minLineClearance, subset);
|
||||
subsetCandidate = ElkEdgePostProcessor.SpreadTargetApproachJoins(subsetCandidate, elkNodes, minLineClearance, subset);
|
||||
subsetCandidate = ElkEdgePostProcessor.SpreadRectTargetApproachFeederBands(subsetCandidate, elkNodes, minLineClearance, subset);
|
||||
subsetCandidate = ElkEdgePostProcessor.ElevateUnderNodeViolations(subsetCandidate, elkNodes, minLineClearance, subset);
|
||||
subsetCandidate = ElkEdgePostProcessor.FinalizeDecisionTargetEntries(subsetCandidate, elkNodes, subset);
|
||||
subsetCandidate = ElkEdgePostProcessor.FinalizeGatewayBoundaryGeometry(subsetCandidate, elkNodes, subset);
|
||||
subsetCandidate = ElkEdgePostProcessor.PolishTargetPeerConflicts(subsetCandidate, elkNodes, minLineClearance, subset);
|
||||
subsetCandidate = ElkEdgePostProcessor.SnapBoundarySlotAssignments(
|
||||
subsetCandidate,
|
||||
elkNodes,
|
||||
minLineClearance,
|
||||
subset,
|
||||
enforceAllNodeEndpoints: true);
|
||||
|
||||
subsetResults.Add((
|
||||
subset,
|
||||
ElkEdgeRoutingScoring.CountExcessiveDetourViolations(subsetCandidate, elkNodes),
|
||||
ElkEdgeRoutingScoring.CountGatewaySourceExitViolations(subsetCandidate, elkNodes),
|
||||
ElkEdgeRoutingScoring.CountBoundarySlotViolations(subsetCandidate, elkNodes),
|
||||
ElkEdgeRoutingScoring.CountBadEntryAngles(subsetCandidate, elkNodes),
|
||||
ElkEdgeRoutingScoring.CountSharedLaneViolations(subsetCandidate, elkNodes)));
|
||||
}
|
||||
|
||||
foreach (var result in subsetResults
|
||||
.OrderBy(item => item.BoundarySlots)
|
||||
.ThenBy(item => item.GatewaySource)
|
||||
.ThenBy(item => item.Detour)
|
||||
.ThenBy(item => item.Entry)
|
||||
.ThenBy(item => item.SharedLanes)
|
||||
.ThenBy(item => item.Focus.Length)
|
||||
.ThenBy(item => string.Join(",", item.Focus), StringComparer.Ordinal)
|
||||
.Take(12))
|
||||
{
|
||||
TestContext.Out.WriteLine(
|
||||
$"subset [{string.Join(", ", result.Focus)}]: detour={result.Detour} gateway-source={result.GatewaySource} boundary-slots={result.BoundarySlots} entry={result.Entry} shared-lanes={result.SharedLanes}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
using System.Text.Json;
|
||||
|
||||
using NUnit.Framework;
|
||||
|
||||
using StellaOps.ElkSharp;
|
||||
using StellaOps.Workflow.Abstractions;
|
||||
using StellaOps.Workflow.Renderer.ElkSharp;
|
||||
using StellaOps.Workflow.Renderer.Svg;
|
||||
|
||||
namespace StellaOps.Workflow.Renderer.Tests;
|
||||
|
||||
public partial class DocumentProcessingWorkflowRenderingTests
|
||||
{
|
||||
[Test]
|
||||
[Category("RenderingArtifacts")]
|
||||
public async Task DocumentProcessingWorkflow_WhenRenderedWithElkSharp_ShouldProducePngWithZeroNodeCrossings()
|
||||
{
|
||||
var graph = BuildDocumentProcessingWorkflowGraph();
|
||||
var engine = new ElkSharpWorkflowRenderLayoutEngine();
|
||||
var outputDir = Path.Combine(
|
||||
Path.GetDirectoryName(typeof(DocumentProcessingWorkflowRenderingTests).Assembly.Location)!,
|
||||
"TestResults", "workflow-renderings", DateTime.Today.ToString("yyyyMMdd"), "DocumentProcessingWorkflow");
|
||||
Directory.CreateDirectory(outputDir);
|
||||
|
||||
using var diagnosticsCapture = ElkLayoutDiagnostics.BeginCapture();
|
||||
var progressLogPath = Path.Combine(outputDir, "elksharp.progress.log");
|
||||
if (File.Exists(progressLogPath))
|
||||
{
|
||||
File.Delete(progressLogPath);
|
||||
}
|
||||
|
||||
var diagnosticsPath = Path.Combine(outputDir, "elksharp.refinement-diagnostics.json");
|
||||
if (File.Exists(diagnosticsPath))
|
||||
{
|
||||
File.Delete(diagnosticsPath);
|
||||
}
|
||||
|
||||
diagnosticsCapture.Diagnostics.ProgressLogPath = progressLogPath;
|
||||
diagnosticsCapture.Diagnostics.SnapshotPath = diagnosticsPath;
|
||||
var layout = await engine.LayoutAsync(graph, new WorkflowRenderLayoutRequest
|
||||
{
|
||||
Direction = WorkflowRenderLayoutDirection.LeftToRight,
|
||||
});
|
||||
|
||||
var svgRenderer = new WorkflowRenderSvgRenderer();
|
||||
var svgDoc = svgRenderer.Render(layout, "DocumentProcessingWorkflow [ElkSharp]");
|
||||
|
||||
var svgPath = Path.Combine(outputDir, "elksharp.svg");
|
||||
await File.WriteAllTextAsync(svgPath, svgDoc.Svg);
|
||||
|
||||
var jsonPath = Path.Combine(outputDir, "elksharp.json");
|
||||
await File.WriteAllTextAsync(jsonPath, JsonSerializer.Serialize(layout, new JsonSerializerOptions { WriteIndented = true }));
|
||||
|
||||
await File.WriteAllTextAsync(
|
||||
diagnosticsPath,
|
||||
JsonSerializer.Serialize(diagnosticsCapture.Diagnostics, new JsonSerializerOptions { WriteIndented = true }));
|
||||
|
||||
WorkflowRenderPngExporter? pngExporter = null;
|
||||
string? pngPath = null;
|
||||
try
|
||||
{
|
||||
pngPath = Path.Combine(outputDir, "elksharp.png");
|
||||
pngExporter = new WorkflowRenderPngExporter();
|
||||
await pngExporter.ExportAsync(svgDoc, pngPath, scale: 2f);
|
||||
TestContext.Out.WriteLine($"PNG generated at: {pngPath}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
TestContext.Out.WriteLine($"PNG export failed (non-fatal): {ex.Message}");
|
||||
TestContext.Out.WriteLine($"SVG available at: {svgPath}");
|
||||
}
|
||||
|
||||
TestContext.Out.WriteLine($"SVG: {svgPath}");
|
||||
TestContext.Out.WriteLine($"JSON: {jsonPath}");
|
||||
TestContext.Out.WriteLine($"Diagnostics: {diagnosticsPath}");
|
||||
TestContext.Out.WriteLine($"Progress log: {progressLogPath}");
|
||||
|
||||
// Render every iteration of every strategy as SVG only
|
||||
var variantsDir = Path.Combine(outputDir, "strategy-variants");
|
||||
Directory.CreateDirectory(variantsDir);
|
||||
foreach (var stratDiag in diagnosticsCapture.Diagnostics.IterativeStrategies)
|
||||
{
|
||||
foreach (var attemptDiag in stratDiag.AttemptDetails)
|
||||
{
|
||||
if (attemptDiag.Edges is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var attemptLayout = BuildVariantLayout(layout, attemptDiag.Edges);
|
||||
var sc = attemptDiag.Score;
|
||||
var attemptLabel = $"S{stratDiag.StrategyIndex} {stratDiag.OrderingName} att{attemptDiag.Attempt} [{attemptDiag.Outcome}] " +
|
||||
$"nc={sc.NodeCrossings} ec={sc.EdgeCrossings} bends={sc.BendCount} diag={sc.DiagonalCount} " +
|
||||
$"bg={sc.BelowGraphViolations} un={sc.UnderNodeViolations} ld={sc.LongDiagonalViolations} " +
|
||||
$"ea={sc.EntryAngleViolations} lbl={sc.LabelProximityViolations} tj={sc.TargetApproachJoinViolations} " +
|
||||
$"sl={sc.SharedLaneViolations} bs={sc.BoundarySlotViolations} " +
|
||||
$"tb={sc.TargetApproachBacktrackingViolations} det={sc.ExcessiveDetourViolations} score={sc.Value:F0}";
|
||||
var attemptSvg = svgRenderer.Render(attemptLayout, attemptLabel);
|
||||
await File.WriteAllTextAsync(
|
||||
Path.Combine(variantsDir, $"s{stratDiag.StrategyIndex:D2}-{stratDiag.OrderingName}-att{attemptDiag.Attempt:D2}.svg"),
|
||||
attemptSvg.Svg);
|
||||
}
|
||||
|
||||
if (stratDiag.BestEdges is not null)
|
||||
{
|
||||
var bestLayout = BuildVariantLayout(layout, stratDiag.BestEdges);
|
||||
var bestSc = stratDiag.BestScore;
|
||||
var bestLabel = $"S{stratDiag.StrategyIndex} {stratDiag.OrderingName} BEST [{stratDiag.Outcome}] " +
|
||||
$"nc={bestSc?.NodeCrossings} ec={bestSc?.EdgeCrossings} bends={bestSc?.BendCount} diag={bestSc?.DiagonalCount} " +
|
||||
$"bg={bestSc?.BelowGraphViolations} un={bestSc?.UnderNodeViolations} ld={bestSc?.LongDiagonalViolations} " +
|
||||
$"ea={bestSc?.EntryAngleViolations} lbl={bestSc?.LabelProximityViolations} tj={bestSc?.TargetApproachJoinViolations} " +
|
||||
$"sl={bestSc?.SharedLaneViolations} bs={bestSc?.BoundarySlotViolations} " +
|
||||
$"tb={bestSc?.TargetApproachBacktrackingViolations} det={bestSc?.ExcessiveDetourViolations} score={bestSc?.Value:F0}";
|
||||
var bestSvg = svgRenderer.Render(bestLayout, bestLabel);
|
||||
await File.WriteAllTextAsync(
|
||||
Path.Combine(variantsDir, $"s{stratDiag.StrategyIndex:D2}-{stratDiag.OrderingName}-BEST.svg"),
|
||||
bestSvg.Svg);
|
||||
}
|
||||
}
|
||||
|
||||
TestContext.Out.WriteLine($"Strategy variants: {variantsDir}");
|
||||
|
||||
var localRepairAttempts = diagnosticsCapture.Diagnostics.IterativeStrategies
|
||||
.SelectMany(strategy => strategy.AttemptDetails)
|
||||
.Where(attempt => attempt.Attempt > 1
|
||||
&& string.Equals(attempt.RouteDiagnostics?.Mode, "local-repair", StringComparison.Ordinal))
|
||||
.ToArray();
|
||||
Assert.That(localRepairAttempts, Is.Not.Empty, "Expected later attempts to use targeted local repair.");
|
||||
Assert.That(
|
||||
localRepairAttempts.All(attempt => attempt.RouteDiagnostics!.RoutedEdges < attempt.RouteDiagnostics.TotalEdges),
|
||||
Is.True,
|
||||
"Local repair attempts must reroute only the penalized subset of edges.");
|
||||
Assert.That(diagnosticsCapture.Diagnostics.FinalBrokenShortHighwayCount, Is.EqualTo(0), "Final selected layout must not keep broken short highways.");
|
||||
Assert.That(diagnosticsCapture.Diagnostics.FinalScore?.RepeatCollectorCorridorViolations, Is.EqualTo(0), "Repeat collector outer lanes must remain separated.");
|
||||
var boundaryAngleOffenders = layout.Edges
|
||||
.SelectMany(edge => GetBoundaryAngleViolations(edge, layout.Nodes))
|
||||
.ToArray();
|
||||
TestContext.Out.WriteLine($"Boundary angle offenders: {(boundaryAngleOffenders.Length == 0 ? "<none>" : string.Join(", ", boundaryAngleOffenders))}");
|
||||
var targetJoinOffenders = GetTargetApproachJoinOffenders(layout.Edges, layout.Nodes).ToArray();
|
||||
TestContext.Out.WriteLine($"Target join offenders: {(targetJoinOffenders.Length == 0 ? "<none>" : string.Join(", ", targetJoinOffenders))}");
|
||||
var elkNodes = layout.Nodes.Select(node => new ElkPositionedNode
|
||||
{
|
||||
Id = node.Id,
|
||||
Label = node.Label,
|
||||
Kind = node.Kind,
|
||||
X = node.X,
|
||||
Y = node.Y,
|
||||
Width = node.Width,
|
||||
Height = node.Height,
|
||||
}).ToArray();
|
||||
var elkEdges = layout.Edges.Select(edge => new ElkRoutedEdge
|
||||
{
|
||||
Id = edge.Id,
|
||||
SourceNodeId = edge.SourceNodeId,
|
||||
TargetNodeId = edge.TargetNodeId,
|
||||
Kind = edge.Kind,
|
||||
Label = edge.Label,
|
||||
Sections = edge.Sections.Select(section => new ElkEdgeSection
|
||||
{
|
||||
StartPoint = new ElkPoint { X = section.StartPoint.X, Y = section.StartPoint.Y },
|
||||
EndPoint = new ElkPoint { X = section.EndPoint.X, Y = section.EndPoint.Y },
|
||||
BendPoints = section.BendPoints.Select(point => new ElkPoint { X = point.X, Y = point.Y }).ToArray(),
|
||||
}).ToArray(),
|
||||
}).ToArray();
|
||||
var sharedLaneOffenders = ElkEdgeRoutingScoring.DetectSharedLaneConflicts(elkEdges, elkNodes)
|
||||
.Select(conflict => $"{conflict.LeftEdgeId}+{conflict.RightEdgeId}")
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
TestContext.Out.WriteLine($"Shared lane offenders: {(sharedLaneOffenders.Length == 0 ? "<none>" : string.Join(", ", sharedLaneOffenders))}");
|
||||
var belowGraphOffenders = GetBelowGraphOffenders(layout.Edges, layout.Nodes).ToArray();
|
||||
TestContext.Out.WriteLine($"Below-graph offenders: {(belowGraphOffenders.Length == 0 ? "<none>" : string.Join(", ", belowGraphOffenders))}");
|
||||
var underNodeOffenders = GetUnderNodeOffenders(layout.Edges, layout.Nodes).ToArray();
|
||||
TestContext.Out.WriteLine($"Under-node offenders: {(underNodeOffenders.Length == 0 ? "<none>" : string.Join(", ", underNodeOffenders))}");
|
||||
var longDiagonalOffenders = GetLongDiagonalOffenders(layout.Edges, layout.Nodes).ToArray();
|
||||
TestContext.Out.WriteLine($"Long-diagonal offenders: {(longDiagonalOffenders.Length == 0 ? "<none>" : string.Join(", ", longDiagonalOffenders))}");
|
||||
var gatewaySourceScoringOffenders = layout.Edges
|
||||
.Where(edge => HasGatewaySourceScoringIssue(edge, layout.Nodes))
|
||||
.Select(edge => edge.Id)
|
||||
.ToArray();
|
||||
Assert.That(diagnosticsCapture.Diagnostics.FinalScore?.EntryAngleViolations, Is.EqualTo(0), "Selected layout must satisfy the node-side entry/exit angle rule.");
|
||||
Assert.That(targetJoinOffenders, Is.Empty, "Selected layout must not leave visually collapsed target-side approach joins.");
|
||||
Assert.That(diagnosticsCapture.Diagnostics.FinalScore?.TargetApproachJoinViolations, Is.EqualTo(0), "Selected layout must not keep disallowed target-side joins.");
|
||||
Assert.That(diagnosticsCapture.Diagnostics.FinalScore?.SharedLaneViolations, Is.EqualTo(0), "Selected layout must not keep same-lane occupancy outside explicit corridor/highway exceptions.");
|
||||
Assert.That(diagnosticsCapture.Diagnostics.FinalScore?.BoundarySlotViolations, Is.EqualTo(0), "Selected layout must not concentrate more than one edge onto the same discrete side slot or leave side endpoints off the evenly spread slot lattice.");
|
||||
Assert.That(diagnosticsCapture.Diagnostics.FinalScore?.BelowGraphViolations, Is.EqualTo(0), "Selected layout must not route any lane below the node field.");
|
||||
Assert.That(diagnosticsCapture.Diagnostics.FinalScore?.UnderNodeViolations, Is.EqualTo(0), "Selected layout must not keep horizontal lanes tucked underneath other nodes.");
|
||||
Assert.That(diagnosticsCapture.Diagnostics.FinalScore?.LongDiagonalViolations, Is.EqualTo(0), "Selected layout must not keep overlong 45-degree segments.");
|
||||
Assert.That(diagnosticsCapture.Diagnostics.FinalScore?.TargetApproachBacktrackingViolations, Is.EqualTo(0), "Selected layout must not overshoot a target side and curl back near the final approach.");
|
||||
Assert.That(diagnosticsCapture.Diagnostics.FinalScore?.ExcessiveDetourViolations, Is.EqualTo(0), "Selected layout must not keep shortest-path violations after the retry budget is exhausted.");
|
||||
var gatewayCornerDiagonalCount = layout.Edges.Count(edge =>
|
||||
HasGatewayCornerDiagonal(edge, layout.Nodes.Single(node => node.Id == edge.SourceNodeId), fromSource: true)
|
||||
|| HasGatewayCornerDiagonal(edge, layout.Nodes.Single(node => node.Id == edge.TargetNodeId), fromSource: false));
|
||||
Assert.That(gatewayCornerDiagonalCount, Is.EqualTo(0), "Gateway diagonal stubs may land on side faces, not on gateway corner vertices.");
|
||||
var gatewayInteriorAdjacentCount = layout.Edges.Count(edge =>
|
||||
HasGatewayInteriorAdjacentPoint(edge, layout.Nodes.Single(node => node.Id == edge.SourceNodeId), fromSource: true)
|
||||
|| HasGatewayInteriorAdjacentPoint(edge, layout.Nodes.Single(node => node.Id == edge.TargetNodeId), fromSource: false));
|
||||
Assert.That(gatewayInteriorAdjacentCount, Is.EqualTo(0), "Gateway joins must use an exterior face-approach point instead of gluing the boundary back to an interior rectangular anchor.");
|
||||
var gatewaySourceCurlCount = layout.Edges.Count(edge =>
|
||||
HasGatewaySourceExitCurl(edge, layout.Nodes.Single(node => node.Id == edge.SourceNodeId)));
|
||||
var gatewaySourceCurlOffenders = layout.Edges
|
||||
.Where(edge => HasGatewaySourceExitCurl(edge, layout.Nodes.Single(node => node.Id == edge.SourceNodeId)))
|
||||
.Select(edge => edge.Id)
|
||||
.ToArray();
|
||||
Assert.That(gatewaySourceCurlCount, Is.EqualTo(0), "Gateway source exits must leave from the downstream-facing side without curling away and back.");
|
||||
var gatewaySourceFaceMismatchCount = layout.Edges.Count(edge =>
|
||||
HasGatewaySourcePreferredFaceMismatch(edge, layout.Nodes.Single(node => node.Id == edge.SourceNodeId), layout.Nodes));
|
||||
var gatewaySourceFaceMismatchOffenders = layout.Edges
|
||||
.Where(edge => HasGatewaySourcePreferredFaceMismatch(edge, layout.Nodes.Single(node => node.Id == edge.SourceNodeId), layout.Nodes))
|
||||
.Select(edge => edge.Id)
|
||||
.ToArray();
|
||||
Assert.That(gatewaySourceFaceMismatchCount, Is.EqualTo(0), "Gateway source exits must leave from the dominant downstream-facing face instead of drifting onto an upper or lower face.");
|
||||
var gatewaySourceDetourCount = layout.Edges.Count(edge =>
|
||||
HasGatewaySourceDominantAxisDetour(edge, layout.Nodes.Single(node => node.Id == edge.SourceNodeId), layout.Nodes));
|
||||
var gatewaySourceDetourOffenders = layout.Edges
|
||||
.Where(edge => HasGatewaySourceDominantAxisDetour(edge, layout.Nodes.Single(node => node.Id == edge.SourceNodeId), layout.Nodes))
|
||||
.Select(edge => edge.Id)
|
||||
.ToArray();
|
||||
Assert.That(gatewaySourceDetourCount, Is.EqualTo(0), "Gateway source exits must not leave on the non-dominant axis when a direct dominant-axis exit is available.");
|
||||
var gatewaySourceVertexExitCount = layout.Edges.Count(edge =>
|
||||
HasGatewaySourceVertexExit(edge, layout.Nodes.Single(node => node.Id == edge.SourceNodeId)));
|
||||
var gatewaySourceVertexExitOffenders = layout.Edges
|
||||
.Where(edge => HasGatewaySourceVertexExit(edge, layout.Nodes.Single(node => node.Id == edge.SourceNodeId)))
|
||||
.Select(edge => edge.Id)
|
||||
.ToArray();
|
||||
Assert.That(gatewaySourceVertexExitCount, Is.EqualTo(0), "Gateway source exits must leave from a face interior, not from a gateway tip/corner.");
|
||||
Assert.That(gatewaySourceScoringOffenders, Is.Empty, "Gateway source exits must not leave a shorter clean downstream-facing repair opportunity unused.");
|
||||
var loadConfigurationNode = layout.Nodes.Single(node => node.Id == "start/3");
|
||||
var processBatchLoops = layout.Edges
|
||||
.Where(edge => edge.TargetNodeId == "start/2/branch-1/1"
|
||||
&& edge.Label.StartsWith("repeat while", StringComparison.Ordinal))
|
||||
.ToArray();
|
||||
Assert.That(
|
||||
processBatchLoops.All(edge => !HasNearNodeClearanceViolation(edge, loadConfigurationNode, 40d)),
|
||||
Is.True,
|
||||
"Repeat-return lanes into Process Batch must stay outside the Load Configuration clearance band.");
|
||||
|
||||
static WorkflowRenderLayoutResult BuildVariantLayout(WorkflowRenderLayoutResult baseLayout, ElkRoutedEdge[] edges)
|
||||
{
|
||||
return new WorkflowRenderLayoutResult
|
||||
{
|
||||
GraphId = baseLayout.GraphId,
|
||||
Nodes = baseLayout.Nodes,
|
||||
Edges = edges.Select(e => new WorkflowRenderRoutedEdge
|
||||
{
|
||||
Id = e.Id,
|
||||
SourceNodeId = e.SourceNodeId,
|
||||
TargetNodeId = e.TargetNodeId,
|
||||
Kind = e.Kind,
|
||||
Label = e.Label,
|
||||
Sections = e.Sections.Select(s => new WorkflowRenderEdgeSection
|
||||
{
|
||||
StartPoint = new WorkflowRenderPoint { X = s.StartPoint.X, Y = s.StartPoint.Y },
|
||||
EndPoint = new WorkflowRenderPoint { X = s.EndPoint.X, Y = s.EndPoint.Y },
|
||||
BendPoints = s.BendPoints.Select(p => new WorkflowRenderPoint { X = p.X, Y = p.Y }).ToArray(),
|
||||
}).ToArray(),
|
||||
}).ToArray(),
|
||||
};
|
||||
}
|
||||
|
||||
// Verify zero edge-node crossings
|
||||
var crossings = 0;
|
||||
foreach (var node in layout.Nodes)
|
||||
{
|
||||
foreach (var edge in layout.Edges)
|
||||
{
|
||||
if (edge.SourceNodeId == node.Id || edge.TargetNodeId == node.Id) continue;
|
||||
foreach (var section in edge.Sections)
|
||||
{
|
||||
var pts = new List<WorkflowRenderPoint> { section.StartPoint };
|
||||
pts.AddRange(section.BendPoints);
|
||||
pts.Add(section.EndPoint);
|
||||
for (var i = 0; i < pts.Count - 1; i++)
|
||||
{
|
||||
var p1 = pts[i];
|
||||
var p2 = pts[i + 1];
|
||||
if (Math.Abs(p1.Y - p2.Y) < 2 && p1.Y > node.Y && p1.Y < node.Y + node.Height)
|
||||
{
|
||||
if (Math.Max(p1.X, p2.X) > node.X && Math.Min(p1.X, p2.X) < node.X + node.Width)
|
||||
crossings++;
|
||||
}
|
||||
else if (Math.Abs(p1.X - p2.X) < 2 && p1.X > node.X && p1.X < node.X + node.Width)
|
||||
{
|
||||
if (Math.Max(p1.Y, p2.Y) > node.Y && Math.Min(p1.Y, p2.Y) < node.Y + node.Height)
|
||||
crossings++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TestContext.Out.WriteLine($"Edge-node crossings: {crossings}");
|
||||
Assert.That(crossings, Is.EqualTo(0), "No edges should cross through node shapes");
|
||||
}
|
||||
}
|
||||
@@ -283,690 +283,4 @@ public partial class DocumentProcessingWorkflowRenderingTests
|
||||
Is.EqualTo(0),
|
||||
$"Selected layout must keep decision source exits on the discrete boundary-slot lattice after winner refinement. Offenders: {string.Join(", ", severityByEdgeId.OrderBy(entry => entry.Key, StringComparer.Ordinal).Select(entry => entry.Key))}");
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Category("RenderingArtifacts")]
|
||||
public async Task DocumentProcessingWorkflow_WhenRenderedWithElkSharp_ShouldProducePngWithZeroNodeCrossings()
|
||||
{
|
||||
var graph = BuildDocumentProcessingWorkflowGraph();
|
||||
var engine = new ElkSharpWorkflowRenderLayoutEngine();
|
||||
var outputDir = Path.Combine(
|
||||
Path.GetDirectoryName(typeof(DocumentProcessingWorkflowRenderingTests).Assembly.Location)!,
|
||||
"TestResults", "workflow-renderings", DateTime.Today.ToString("yyyyMMdd"), "DocumentProcessingWorkflow");
|
||||
Directory.CreateDirectory(outputDir);
|
||||
|
||||
using var diagnosticsCapture = ElkLayoutDiagnostics.BeginCapture();
|
||||
var progressLogPath = Path.Combine(outputDir, "elksharp.progress.log");
|
||||
if (File.Exists(progressLogPath))
|
||||
{
|
||||
File.Delete(progressLogPath);
|
||||
}
|
||||
|
||||
var diagnosticsPath = Path.Combine(outputDir, "elksharp.refinement-diagnostics.json");
|
||||
if (File.Exists(diagnosticsPath))
|
||||
{
|
||||
File.Delete(diagnosticsPath);
|
||||
}
|
||||
|
||||
diagnosticsCapture.Diagnostics.ProgressLogPath = progressLogPath;
|
||||
diagnosticsCapture.Diagnostics.SnapshotPath = diagnosticsPath;
|
||||
var layout = await engine.LayoutAsync(graph, new WorkflowRenderLayoutRequest
|
||||
{
|
||||
Direction = WorkflowRenderLayoutDirection.LeftToRight,
|
||||
});
|
||||
|
||||
var svgRenderer = new WorkflowRenderSvgRenderer();
|
||||
var svgDoc = svgRenderer.Render(layout, "DocumentProcessingWorkflow [ElkSharp]");
|
||||
|
||||
var svgPath = Path.Combine(outputDir, "elksharp.svg");
|
||||
await File.WriteAllTextAsync(svgPath, svgDoc.Svg);
|
||||
|
||||
var jsonPath = Path.Combine(outputDir, "elksharp.json");
|
||||
await File.WriteAllTextAsync(jsonPath, JsonSerializer.Serialize(layout, new JsonSerializerOptions { WriteIndented = true }));
|
||||
|
||||
await File.WriteAllTextAsync(
|
||||
diagnosticsPath,
|
||||
JsonSerializer.Serialize(diagnosticsCapture.Diagnostics, new JsonSerializerOptions { WriteIndented = true }));
|
||||
|
||||
WorkflowRenderPngExporter? pngExporter = null;
|
||||
string? pngPath = null;
|
||||
try
|
||||
{
|
||||
pngPath = Path.Combine(outputDir, "elksharp.png");
|
||||
pngExporter = new WorkflowRenderPngExporter();
|
||||
await pngExporter.ExportAsync(svgDoc, pngPath, scale: 2f);
|
||||
TestContext.Out.WriteLine($"PNG generated at: {pngPath}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
TestContext.Out.WriteLine($"PNG export failed (non-fatal): {ex.Message}");
|
||||
TestContext.Out.WriteLine($"SVG available at: {svgPath}");
|
||||
}
|
||||
|
||||
TestContext.Out.WriteLine($"SVG: {svgPath}");
|
||||
TestContext.Out.WriteLine($"JSON: {jsonPath}");
|
||||
TestContext.Out.WriteLine($"Diagnostics: {diagnosticsPath}");
|
||||
TestContext.Out.WriteLine($"Progress log: {progressLogPath}");
|
||||
|
||||
// Render every iteration of every strategy as SVG only
|
||||
var variantsDir = Path.Combine(outputDir, "strategy-variants");
|
||||
Directory.CreateDirectory(variantsDir);
|
||||
foreach (var stratDiag in diagnosticsCapture.Diagnostics.IterativeStrategies)
|
||||
{
|
||||
foreach (var attemptDiag in stratDiag.AttemptDetails)
|
||||
{
|
||||
if (attemptDiag.Edges is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var attemptLayout = BuildVariantLayout(layout, attemptDiag.Edges);
|
||||
var sc = attemptDiag.Score;
|
||||
var attemptLabel = $"S{stratDiag.StrategyIndex} {stratDiag.OrderingName} att{attemptDiag.Attempt} [{attemptDiag.Outcome}] " +
|
||||
$"nc={sc.NodeCrossings} ec={sc.EdgeCrossings} bends={sc.BendCount} diag={sc.DiagonalCount} " +
|
||||
$"bg={sc.BelowGraphViolations} un={sc.UnderNodeViolations} ld={sc.LongDiagonalViolations} " +
|
||||
$"ea={sc.EntryAngleViolations} lbl={sc.LabelProximityViolations} tj={sc.TargetApproachJoinViolations} " +
|
||||
$"sl={sc.SharedLaneViolations} bs={sc.BoundarySlotViolations} " +
|
||||
$"tb={sc.TargetApproachBacktrackingViolations} det={sc.ExcessiveDetourViolations} score={sc.Value:F0}";
|
||||
var attemptSvg = svgRenderer.Render(attemptLayout, attemptLabel);
|
||||
await File.WriteAllTextAsync(
|
||||
Path.Combine(variantsDir, $"s{stratDiag.StrategyIndex:D2}-{stratDiag.OrderingName}-att{attemptDiag.Attempt:D2}.svg"),
|
||||
attemptSvg.Svg);
|
||||
}
|
||||
|
||||
if (stratDiag.BestEdges is not null)
|
||||
{
|
||||
var bestLayout = BuildVariantLayout(layout, stratDiag.BestEdges);
|
||||
var bestSc = stratDiag.BestScore;
|
||||
var bestLabel = $"S{stratDiag.StrategyIndex} {stratDiag.OrderingName} BEST [{stratDiag.Outcome}] " +
|
||||
$"nc={bestSc?.NodeCrossings} ec={bestSc?.EdgeCrossings} bends={bestSc?.BendCount} diag={bestSc?.DiagonalCount} " +
|
||||
$"bg={bestSc?.BelowGraphViolations} un={bestSc?.UnderNodeViolations} ld={bestSc?.LongDiagonalViolations} " +
|
||||
$"ea={bestSc?.EntryAngleViolations} lbl={bestSc?.LabelProximityViolations} tj={bestSc?.TargetApproachJoinViolations} " +
|
||||
$"sl={bestSc?.SharedLaneViolations} bs={bestSc?.BoundarySlotViolations} " +
|
||||
$"tb={bestSc?.TargetApproachBacktrackingViolations} det={bestSc?.ExcessiveDetourViolations} score={bestSc?.Value:F0}";
|
||||
var bestSvg = svgRenderer.Render(bestLayout, bestLabel);
|
||||
await File.WriteAllTextAsync(
|
||||
Path.Combine(variantsDir, $"s{stratDiag.StrategyIndex:D2}-{stratDiag.OrderingName}-BEST.svg"),
|
||||
bestSvg.Svg);
|
||||
}
|
||||
}
|
||||
|
||||
TestContext.Out.WriteLine($"Strategy variants: {variantsDir}");
|
||||
|
||||
var localRepairAttempts = diagnosticsCapture.Diagnostics.IterativeStrategies
|
||||
.SelectMany(strategy => strategy.AttemptDetails)
|
||||
.Where(attempt => attempt.Attempt > 1
|
||||
&& string.Equals(attempt.RouteDiagnostics?.Mode, "local-repair", StringComparison.Ordinal))
|
||||
.ToArray();
|
||||
Assert.That(localRepairAttempts, Is.Not.Empty, "Expected later attempts to use targeted local repair.");
|
||||
Assert.That(
|
||||
localRepairAttempts.All(attempt => attempt.RouteDiagnostics!.RoutedEdges < attempt.RouteDiagnostics.TotalEdges),
|
||||
Is.True,
|
||||
"Local repair attempts must reroute only the penalized subset of edges.");
|
||||
Assert.That(diagnosticsCapture.Diagnostics.FinalBrokenShortHighwayCount, Is.EqualTo(0), "Final selected layout must not keep broken short highways.");
|
||||
Assert.That(diagnosticsCapture.Diagnostics.FinalScore?.RepeatCollectorCorridorViolations, Is.EqualTo(0), "Repeat collector outer lanes must remain separated.");
|
||||
var boundaryAngleOffenders = layout.Edges
|
||||
.SelectMany(edge => GetBoundaryAngleViolations(edge, layout.Nodes))
|
||||
.ToArray();
|
||||
TestContext.Out.WriteLine($"Boundary angle offenders: {(boundaryAngleOffenders.Length == 0 ? "<none>" : string.Join(", ", boundaryAngleOffenders))}");
|
||||
var targetJoinOffenders = GetTargetApproachJoinOffenders(layout.Edges, layout.Nodes).ToArray();
|
||||
TestContext.Out.WriteLine($"Target join offenders: {(targetJoinOffenders.Length == 0 ? "<none>" : string.Join(", ", targetJoinOffenders))}");
|
||||
var elkNodes = layout.Nodes.Select(node => new ElkPositionedNode
|
||||
{
|
||||
Id = node.Id,
|
||||
Label = node.Label,
|
||||
Kind = node.Kind,
|
||||
X = node.X,
|
||||
Y = node.Y,
|
||||
Width = node.Width,
|
||||
Height = node.Height,
|
||||
}).ToArray();
|
||||
var elkEdges = layout.Edges.Select(edge => new ElkRoutedEdge
|
||||
{
|
||||
Id = edge.Id,
|
||||
SourceNodeId = edge.SourceNodeId,
|
||||
TargetNodeId = edge.TargetNodeId,
|
||||
Kind = edge.Kind,
|
||||
Label = edge.Label,
|
||||
Sections = edge.Sections.Select(section => new ElkEdgeSection
|
||||
{
|
||||
StartPoint = new ElkPoint { X = section.StartPoint.X, Y = section.StartPoint.Y },
|
||||
EndPoint = new ElkPoint { X = section.EndPoint.X, Y = section.EndPoint.Y },
|
||||
BendPoints = section.BendPoints.Select(point => new ElkPoint { X = point.X, Y = point.Y }).ToArray(),
|
||||
}).ToArray(),
|
||||
}).ToArray();
|
||||
var sharedLaneOffenders = ElkEdgeRoutingScoring.DetectSharedLaneConflicts(elkEdges, elkNodes)
|
||||
.Select(conflict => $"{conflict.LeftEdgeId}+{conflict.RightEdgeId}")
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
TestContext.Out.WriteLine($"Shared lane offenders: {(sharedLaneOffenders.Length == 0 ? "<none>" : string.Join(", ", sharedLaneOffenders))}");
|
||||
var belowGraphOffenders = GetBelowGraphOffenders(layout.Edges, layout.Nodes).ToArray();
|
||||
TestContext.Out.WriteLine($"Below-graph offenders: {(belowGraphOffenders.Length == 0 ? "<none>" : string.Join(", ", belowGraphOffenders))}");
|
||||
var underNodeOffenders = GetUnderNodeOffenders(layout.Edges, layout.Nodes).ToArray();
|
||||
TestContext.Out.WriteLine($"Under-node offenders: {(underNodeOffenders.Length == 0 ? "<none>" : string.Join(", ", underNodeOffenders))}");
|
||||
var longDiagonalOffenders = GetLongDiagonalOffenders(layout.Edges, layout.Nodes).ToArray();
|
||||
TestContext.Out.WriteLine($"Long-diagonal offenders: {(longDiagonalOffenders.Length == 0 ? "<none>" : string.Join(", ", longDiagonalOffenders))}");
|
||||
var gatewaySourceScoringOffenders = layout.Edges
|
||||
.Where(edge => HasGatewaySourceScoringIssue(edge, layout.Nodes))
|
||||
.Select(edge => edge.Id)
|
||||
.ToArray();
|
||||
Assert.That(diagnosticsCapture.Diagnostics.FinalScore?.EntryAngleViolations, Is.EqualTo(0), "Selected layout must satisfy the node-side entry/exit angle rule.");
|
||||
Assert.That(targetJoinOffenders, Is.Empty, "Selected layout must not leave visually collapsed target-side approach joins.");
|
||||
Assert.That(diagnosticsCapture.Diagnostics.FinalScore?.TargetApproachJoinViolations, Is.EqualTo(0), "Selected layout must not keep disallowed target-side joins.");
|
||||
Assert.That(diagnosticsCapture.Diagnostics.FinalScore?.SharedLaneViolations, Is.EqualTo(0), "Selected layout must not keep same-lane occupancy outside explicit corridor/highway exceptions.");
|
||||
Assert.That(diagnosticsCapture.Diagnostics.FinalScore?.BoundarySlotViolations, Is.EqualTo(0), "Selected layout must not concentrate more than one edge onto the same discrete side slot or leave side endpoints off the evenly spread slot lattice.");
|
||||
Assert.That(diagnosticsCapture.Diagnostics.FinalScore?.BelowGraphViolations, Is.EqualTo(0), "Selected layout must not route any lane below the node field.");
|
||||
Assert.That(diagnosticsCapture.Diagnostics.FinalScore?.UnderNodeViolations, Is.EqualTo(0), "Selected layout must not keep horizontal lanes tucked underneath other nodes.");
|
||||
Assert.That(diagnosticsCapture.Diagnostics.FinalScore?.LongDiagonalViolations, Is.EqualTo(0), "Selected layout must not keep overlong 45-degree segments.");
|
||||
Assert.That(diagnosticsCapture.Diagnostics.FinalScore?.TargetApproachBacktrackingViolations, Is.EqualTo(0), "Selected layout must not overshoot a target side and curl back near the final approach.");
|
||||
Assert.That(diagnosticsCapture.Diagnostics.FinalScore?.ExcessiveDetourViolations, Is.EqualTo(0), "Selected layout must not keep shortest-path violations after the retry budget is exhausted.");
|
||||
var gatewayCornerDiagonalCount = layout.Edges.Count(edge =>
|
||||
HasGatewayCornerDiagonal(edge, layout.Nodes.Single(node => node.Id == edge.SourceNodeId), fromSource: true)
|
||||
|| HasGatewayCornerDiagonal(edge, layout.Nodes.Single(node => node.Id == edge.TargetNodeId), fromSource: false));
|
||||
Assert.That(gatewayCornerDiagonalCount, Is.EqualTo(0), "Gateway diagonal stubs may land on side faces, not on gateway corner vertices.");
|
||||
var gatewayInteriorAdjacentCount = layout.Edges.Count(edge =>
|
||||
HasGatewayInteriorAdjacentPoint(edge, layout.Nodes.Single(node => node.Id == edge.SourceNodeId), fromSource: true)
|
||||
|| HasGatewayInteriorAdjacentPoint(edge, layout.Nodes.Single(node => node.Id == edge.TargetNodeId), fromSource: false));
|
||||
Assert.That(gatewayInteriorAdjacentCount, Is.EqualTo(0), "Gateway joins must use an exterior face-approach point instead of gluing the boundary back to an interior rectangular anchor.");
|
||||
var gatewaySourceCurlCount = layout.Edges.Count(edge =>
|
||||
HasGatewaySourceExitCurl(edge, layout.Nodes.Single(node => node.Id == edge.SourceNodeId)));
|
||||
var gatewaySourceCurlOffenders = layout.Edges
|
||||
.Where(edge => HasGatewaySourceExitCurl(edge, layout.Nodes.Single(node => node.Id == edge.SourceNodeId)))
|
||||
.Select(edge => edge.Id)
|
||||
.ToArray();
|
||||
Assert.That(gatewaySourceCurlCount, Is.EqualTo(0), "Gateway source exits must leave from the downstream-facing side without curling away and back.");
|
||||
var gatewaySourceFaceMismatchCount = layout.Edges.Count(edge =>
|
||||
HasGatewaySourcePreferredFaceMismatch(edge, layout.Nodes.Single(node => node.Id == edge.SourceNodeId), layout.Nodes));
|
||||
var gatewaySourceFaceMismatchOffenders = layout.Edges
|
||||
.Where(edge => HasGatewaySourcePreferredFaceMismatch(edge, layout.Nodes.Single(node => node.Id == edge.SourceNodeId), layout.Nodes))
|
||||
.Select(edge => edge.Id)
|
||||
.ToArray();
|
||||
Assert.That(gatewaySourceFaceMismatchCount, Is.EqualTo(0), "Gateway source exits must leave from the dominant downstream-facing face instead of drifting onto an upper or lower face.");
|
||||
var gatewaySourceDetourCount = layout.Edges.Count(edge =>
|
||||
HasGatewaySourceDominantAxisDetour(edge, layout.Nodes.Single(node => node.Id == edge.SourceNodeId), layout.Nodes));
|
||||
var gatewaySourceDetourOffenders = layout.Edges
|
||||
.Where(edge => HasGatewaySourceDominantAxisDetour(edge, layout.Nodes.Single(node => node.Id == edge.SourceNodeId), layout.Nodes))
|
||||
.Select(edge => edge.Id)
|
||||
.ToArray();
|
||||
Assert.That(gatewaySourceDetourCount, Is.EqualTo(0), "Gateway source exits must not leave on the non-dominant axis when a direct dominant-axis exit is available.");
|
||||
var gatewaySourceVertexExitCount = layout.Edges.Count(edge =>
|
||||
HasGatewaySourceVertexExit(edge, layout.Nodes.Single(node => node.Id == edge.SourceNodeId)));
|
||||
var gatewaySourceVertexExitOffenders = layout.Edges
|
||||
.Where(edge => HasGatewaySourceVertexExit(edge, layout.Nodes.Single(node => node.Id == edge.SourceNodeId)))
|
||||
.Select(edge => edge.Id)
|
||||
.ToArray();
|
||||
Assert.That(gatewaySourceVertexExitCount, Is.EqualTo(0), "Gateway source exits must leave from a face interior, not from a gateway tip/corner.");
|
||||
Assert.That(gatewaySourceScoringOffenders, Is.Empty, "Gateway source exits must not leave a shorter clean downstream-facing repair opportunity unused.");
|
||||
var loadConfigurationNode = layout.Nodes.Single(node => node.Id == "start/3");
|
||||
var processBatchLoops = layout.Edges
|
||||
.Where(edge => edge.TargetNodeId == "start/2/branch-1/1"
|
||||
&& edge.Label.StartsWith("repeat while", StringComparison.Ordinal))
|
||||
.ToArray();
|
||||
Assert.That(
|
||||
processBatchLoops.All(edge => !HasNearNodeClearanceViolation(edge, loadConfigurationNode, 40d)),
|
||||
Is.True,
|
||||
"Repeat-return lanes into Process Batch must stay outside the Load Configuration clearance band.");
|
||||
|
||||
static WorkflowRenderLayoutResult BuildVariantLayout(WorkflowRenderLayoutResult baseLayout, ElkRoutedEdge[] edges)
|
||||
{
|
||||
return new WorkflowRenderLayoutResult
|
||||
{
|
||||
GraphId = baseLayout.GraphId,
|
||||
Nodes = baseLayout.Nodes,
|
||||
Edges = edges.Select(e => new WorkflowRenderRoutedEdge
|
||||
{
|
||||
Id = e.Id,
|
||||
SourceNodeId = e.SourceNodeId,
|
||||
TargetNodeId = e.TargetNodeId,
|
||||
Kind = e.Kind,
|
||||
Label = e.Label,
|
||||
Sections = e.Sections.Select(s => new WorkflowRenderEdgeSection
|
||||
{
|
||||
StartPoint = new WorkflowRenderPoint { X = s.StartPoint.X, Y = s.StartPoint.Y },
|
||||
EndPoint = new WorkflowRenderPoint { X = s.EndPoint.X, Y = s.EndPoint.Y },
|
||||
BendPoints = s.BendPoints.Select(p => new WorkflowRenderPoint { X = p.X, Y = p.Y }).ToArray(),
|
||||
}).ToArray(),
|
||||
}).ToArray(),
|
||||
};
|
||||
}
|
||||
|
||||
// Verify zero edge-node crossings
|
||||
var crossings = 0;
|
||||
foreach (var node in layout.Nodes)
|
||||
{
|
||||
foreach (var edge in layout.Edges)
|
||||
{
|
||||
if (edge.SourceNodeId == node.Id || edge.TargetNodeId == node.Id) continue;
|
||||
foreach (var section in edge.Sections)
|
||||
{
|
||||
var pts = new List<WorkflowRenderPoint> { section.StartPoint };
|
||||
pts.AddRange(section.BendPoints);
|
||||
pts.Add(section.EndPoint);
|
||||
for (var i = 0; i < pts.Count - 1; i++)
|
||||
{
|
||||
var p1 = pts[i];
|
||||
var p2 = pts[i + 1];
|
||||
if (Math.Abs(p1.Y - p2.Y) < 2 && p1.Y > node.Y && p1.Y < node.Y + node.Height)
|
||||
{
|
||||
if (Math.Max(p1.X, p2.X) > node.X && Math.Min(p1.X, p2.X) < node.X + node.Width)
|
||||
crossings++;
|
||||
}
|
||||
else if (Math.Abs(p1.X - p2.X) < 2 && p1.X > node.X && p1.X < node.X + node.Width)
|
||||
{
|
||||
if (Math.Max(p1.Y, p2.Y) > node.Y && Math.Min(p1.Y, p2.Y) < node.Y + node.Height)
|
||||
crossings++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TestContext.Out.WriteLine($"Edge-node crossings: {crossings}");
|
||||
Assert.That(crossings, Is.EqualTo(0), "No edges should cross through node shapes");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DocumentProcessingWorkflow_WhenInspectingLatestElkSharpArtifact_ShouldReportBoundarySlotOffenders()
|
||||
{
|
||||
var workflowRenderingsDirectory = Path.Combine(
|
||||
Path.GetDirectoryName(typeof(DocumentProcessingWorkflowRenderingTests).Assembly.Location)!,
|
||||
"TestResults",
|
||||
"workflow-renderings");
|
||||
var outputDir = Directory.GetDirectories(workflowRenderingsDirectory)
|
||||
.OrderByDescending(path => Path.GetFileName(path), StringComparer.Ordinal)
|
||||
.Select(path => Path.Combine(path, "DocumentProcessingWorkflow"))
|
||||
.First(Directory.Exists);
|
||||
var jsonPath = Path.Combine(outputDir, "elksharp.json");
|
||||
Assert.That(File.Exists(jsonPath), Is.True);
|
||||
|
||||
var layout = JsonSerializer.Deserialize<WorkflowRenderLayoutResult>(
|
||||
File.ReadAllText(jsonPath),
|
||||
new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
});
|
||||
Assert.That(layout, Is.Not.Null);
|
||||
|
||||
var elkNodes = layout!.Nodes.Select(node => new ElkPositionedNode
|
||||
{
|
||||
Id = node.Id,
|
||||
Label = node.Label,
|
||||
Kind = node.Kind,
|
||||
X = node.X,
|
||||
Y = node.Y,
|
||||
Width = node.Width,
|
||||
Height = node.Height,
|
||||
}).ToArray();
|
||||
var elkEdges = layout.Edges.Select(edge => new ElkRoutedEdge
|
||||
{
|
||||
Id = edge.Id,
|
||||
SourceNodeId = edge.SourceNodeId,
|
||||
TargetNodeId = edge.TargetNodeId,
|
||||
Kind = edge.Kind,
|
||||
Label = edge.Label,
|
||||
Sections = edge.Sections.Select(section => new ElkEdgeSection
|
||||
{
|
||||
StartPoint = new ElkPoint { X = section.StartPoint.X, Y = section.StartPoint.Y },
|
||||
EndPoint = new ElkPoint { X = section.EndPoint.X, Y = section.EndPoint.Y },
|
||||
BendPoints = section.BendPoints.Select(point => new ElkPoint { X = point.X, Y = point.Y }).ToArray(),
|
||||
}).ToArray(),
|
||||
}).ToArray();
|
||||
|
||||
var severityByEdgeId = new Dictionary<string, int>(StringComparer.Ordinal);
|
||||
var count = ElkEdgeRoutingScoring.CountBoundarySlotViolations(elkEdges, elkNodes, severityByEdgeId, 1);
|
||||
TestContext.Out.WriteLine($"boundary-slot artifact count: {count}");
|
||||
foreach (var offender in severityByEdgeId.OrderBy(pair => pair.Key, StringComparer.Ordinal))
|
||||
{
|
||||
var edge = elkEdges.Single(candidate => candidate.Id == offender.Key);
|
||||
TestContext.Out.WriteLine(
|
||||
$"{offender.Key}: {string.Join(" -> ", ExtractElkPath(edge).Select(point => $"({point.X:F3},{point.Y:F3})"))}");
|
||||
}
|
||||
|
||||
var serviceNodes = elkNodes.Where(node => node.Kind is not "Start" and not "End").ToArray();
|
||||
var minLineClearance = serviceNodes.Length > 0
|
||||
? Math.Min(serviceNodes.Average(node => node.Width), serviceNodes.Average(node => node.Height)) / 2d
|
||||
: 50d;
|
||||
var nodesById = elkNodes.ToDictionary(node => node.Id, StringComparer.Ordinal);
|
||||
var graphMinY = elkNodes.Min(node => node.Y);
|
||||
var graphMaxY = elkNodes.Max(node => node.Y + node.Height);
|
||||
var (sourceSlots, targetSlots) = ElkEdgePostProcessor.ResolveCombinedBoundarySlots(
|
||||
elkEdges,
|
||||
nodesById,
|
||||
graphMinY,
|
||||
graphMaxY,
|
||||
restrictedEdgeIds: null,
|
||||
enforceAllNodeEndpoints: true);
|
||||
if (sourceSlots.TryGetValue("edge/25", out var sourceSlot))
|
||||
{
|
||||
TestContext.Out.WriteLine(
|
||||
$"edge/25 source-slot: side={sourceSlot.Side} boundary=({sourceSlot.Boundary.X:F3},{sourceSlot.Boundary.Y:F3})");
|
||||
}
|
||||
|
||||
if (targetSlots.TryGetValue("edge/25", out var targetSlot))
|
||||
{
|
||||
TestContext.Out.WriteLine(
|
||||
$"edge/25 target-slot: side={targetSlot.Side} boundary=({targetSlot.Boundary.X:F3},{targetSlot.Boundary.Y:F3})");
|
||||
|
||||
var edge25 = elkEdges.Single(edge => edge.Id == "edge/25");
|
||||
var edge25Path = ExtractElkPath(edge25);
|
||||
var buildTargetApproachCandidatePath = typeof(ElkEdgePostProcessor).GetMethod(
|
||||
"BuildTargetApproachCandidatePath",
|
||||
BindingFlags.NonPublic | BindingFlags.Static);
|
||||
Assert.That(buildTargetApproachCandidatePath, Is.Not.Null);
|
||||
var reflectedTargetCandidate = (List<ElkPoint>)buildTargetApproachCandidatePath!.Invoke(
|
||||
null,
|
||||
[edge25Path, elkNodes.Single(node => node.Id == edge25.TargetNodeId), targetSlot.Side, targetSlot.Boundary, edge25Path[2].Y])!;
|
||||
TestContext.Out.WriteLine(
|
||||
$"edge/25 reflected-target-candidate: {string.Join(" -> ", reflectedTargetCandidate.Select(point => $"({point.X:F3},{point.Y:F3})"))}");
|
||||
}
|
||||
|
||||
var snapped = ElkEdgePostProcessor.SnapBoundarySlotAssignments(elkEdges, elkNodes, minLineClearance);
|
||||
var snappedSeverityByEdgeId = new Dictionary<string, int>(StringComparer.Ordinal);
|
||||
var snappedCount = ElkEdgeRoutingScoring.CountBoundarySlotViolations(snapped, elkNodes, snappedSeverityByEdgeId, 1);
|
||||
TestContext.Out.WriteLine($"boundary-slot snapped count: {snappedCount}");
|
||||
if (snappedSeverityByEdgeId.TryGetValue("edge/25", out _))
|
||||
{
|
||||
var snappedEdge = snapped.Single(candidate => candidate.Id == "edge/25");
|
||||
TestContext.Out.WriteLine(
|
||||
$"edge/25 snapped: {string.Join(" -> ", ExtractElkPath(snappedEdge).Select(point => $"({point.X:F3},{point.Y:F3})"))}");
|
||||
}
|
||||
|
||||
var detourSeverityByEdgeId = new Dictionary<string, int>(StringComparer.Ordinal);
|
||||
var detourCount = ElkEdgeRoutingScoring.CountExcessiveDetourViolations(elkEdges, elkNodes, detourSeverityByEdgeId, 1);
|
||||
TestContext.Out.WriteLine($"detour artifact count: {detourCount}");
|
||||
foreach (var offender in detourSeverityByEdgeId.OrderByDescending(pair => pair.Value).ThenBy(pair => pair.Key, StringComparer.Ordinal))
|
||||
{
|
||||
var edge = elkEdges.Single(candidate => candidate.Id == offender.Key);
|
||||
TestContext.Out.WriteLine(
|
||||
$"{offender.Key} detour={offender.Value}: {string.Join(" -> ", ExtractElkPath(edge).Select(point => $"({point.X:F3},{point.Y:F3})"))}");
|
||||
|
||||
var shortcutCandidate = ElkEdgePostProcessor.PreferShortestBoundaryShortcuts([edge], elkNodes)[0];
|
||||
if (!ExtractElkPath(shortcutCandidate).SequenceEqual(ExtractElkPath(edge), ElkPointComparer.Instance))
|
||||
{
|
||||
var shortcutDetourSeverity = new Dictionary<string, int>(StringComparer.Ordinal);
|
||||
var shortcutGatewaySeverity = new Dictionary<string, int>(StringComparer.Ordinal);
|
||||
var shortcutDetourCount = ElkEdgeRoutingScoring.CountExcessiveDetourViolations(
|
||||
[shortcutCandidate],
|
||||
elkNodes,
|
||||
shortcutDetourSeverity,
|
||||
1);
|
||||
var shortcutGatewayCount = ElkEdgeRoutingScoring.CountGatewaySourceExitViolations(
|
||||
[shortcutCandidate],
|
||||
elkNodes,
|
||||
shortcutGatewaySeverity,
|
||||
1);
|
||||
TestContext.Out.WriteLine(
|
||||
$" shortcut -> detour={shortcutDetourCount} gateway-source={shortcutGatewayCount}: {string.Join(" -> ", ExtractElkPath(shortcutCandidate).Select(point => $"({point.X:F3},{point.Y:F3})"))}");
|
||||
}
|
||||
else
|
||||
{
|
||||
TestContext.Out.WriteLine(" shortcut -> unchanged");
|
||||
}
|
||||
}
|
||||
|
||||
var gatewaySourceSeverityByEdgeId = new Dictionary<string, int>(StringComparer.Ordinal);
|
||||
var gatewaySourceCount = ElkEdgeRoutingScoring.CountGatewaySourceExitViolations(elkEdges, elkNodes, gatewaySourceSeverityByEdgeId, 1);
|
||||
TestContext.Out.WriteLine($"gateway-source artifact count: {gatewaySourceCount}");
|
||||
foreach (var offender in gatewaySourceSeverityByEdgeId.OrderByDescending(pair => pair.Value).ThenBy(pair => pair.Key, StringComparer.Ordinal))
|
||||
{
|
||||
var edge = elkEdges.Single(candidate => candidate.Id == offender.Key);
|
||||
TestContext.Out.WriteLine(
|
||||
$"{offender.Key} gateway-source={offender.Value}: {string.Join(" -> ", ExtractElkPath(edge).Select(point => $"({point.X:F3},{point.Y:F3})"))}");
|
||||
}
|
||||
|
||||
var sharedLaneConflicts = ElkEdgeRoutingScoring.DetectSharedLaneConflicts(elkEdges, elkNodes)
|
||||
.Distinct()
|
||||
.OrderBy(conflict => conflict.LeftEdgeId, StringComparer.Ordinal)
|
||||
.ThenBy(conflict => conflict.RightEdgeId, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
TestContext.Out.WriteLine($"shared-lane artifact count: {sharedLaneConflicts.Length}");
|
||||
foreach (var conflict in sharedLaneConflicts)
|
||||
{
|
||||
TestContext.Out.WriteLine($"shared-lane artifact pair: {conflict.LeftEdgeId}+{conflict.RightEdgeId}");
|
||||
}
|
||||
|
||||
var layoutNodesById = layout.Nodes.ToDictionary(node => node.Id, StringComparer.Ordinal);
|
||||
var gatewayCornerDiagonalOffenders = layout.Edges
|
||||
.Where(edge =>
|
||||
HasGatewayCornerDiagonal(edge, layoutNodesById[edge.SourceNodeId], fromSource: true)
|
||||
|| HasGatewayCornerDiagonal(edge, layoutNodesById[edge.TargetNodeId], fromSource: false))
|
||||
.Select(edge => edge.Id)
|
||||
.OrderBy(edgeId => edgeId, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
TestContext.Out.WriteLine($"gateway-corner artifact count: {gatewayCornerDiagonalOffenders.Length}");
|
||||
foreach (var edgeId in gatewayCornerDiagonalOffenders)
|
||||
{
|
||||
TestContext.Out.WriteLine($"gateway-corner artifact edge: {edgeId}");
|
||||
}
|
||||
|
||||
var gatewayInteriorAdjacentOffenders = layout.Edges
|
||||
.Where(edge =>
|
||||
HasGatewayInteriorAdjacentPoint(edge, layoutNodesById[edge.SourceNodeId], fromSource: true)
|
||||
|| HasGatewayInteriorAdjacentPoint(edge, layoutNodesById[edge.TargetNodeId], fromSource: false))
|
||||
.Select(edge => edge.Id)
|
||||
.OrderBy(edgeId => edgeId, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
TestContext.Out.WriteLine($"gateway-interior-adjacent artifact count: {gatewayInteriorAdjacentOffenders.Length}");
|
||||
foreach (var edgeId in gatewayInteriorAdjacentOffenders)
|
||||
{
|
||||
TestContext.Out.WriteLine($"gateway-interior-adjacent artifact edge: {edgeId}");
|
||||
}
|
||||
|
||||
var gatewaySourceCurlOffenders = layout.Edges
|
||||
.Where(edge => HasGatewaySourceExitCurl(edge, layoutNodesById[edge.SourceNodeId]))
|
||||
.Select(edge => edge.Id)
|
||||
.OrderBy(edgeId => edgeId, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
TestContext.Out.WriteLine($"gateway-source-curl artifact count: {gatewaySourceCurlOffenders.Length}");
|
||||
foreach (var edgeId in gatewaySourceCurlOffenders)
|
||||
{
|
||||
TestContext.Out.WriteLine($"gateway-source-curl artifact edge: {edgeId}");
|
||||
}
|
||||
|
||||
var gatewaySourceFaceMismatchOffenders = layout.Edges
|
||||
.Where(edge => HasGatewaySourcePreferredFaceMismatch(edge, layoutNodesById[edge.SourceNodeId], layout.Nodes))
|
||||
.Select(edge => edge.Id)
|
||||
.OrderBy(edgeId => edgeId, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
TestContext.Out.WriteLine($"gateway-source-face-mismatch artifact count: {gatewaySourceFaceMismatchOffenders.Length}");
|
||||
foreach (var edgeId in gatewaySourceFaceMismatchOffenders)
|
||||
{
|
||||
TestContext.Out.WriteLine($"gateway-source-face-mismatch artifact edge: {edgeId}");
|
||||
}
|
||||
|
||||
var gatewaySourceDetourOffenders = layout.Edges
|
||||
.Where(edge => HasGatewaySourceDominantAxisDetour(edge, layoutNodesById[edge.SourceNodeId], layout.Nodes))
|
||||
.Select(edge => edge.Id)
|
||||
.OrderBy(edgeId => edgeId, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
TestContext.Out.WriteLine($"gateway-source-detour artifact count: {gatewaySourceDetourOffenders.Length}");
|
||||
foreach (var edgeId in gatewaySourceDetourOffenders)
|
||||
{
|
||||
TestContext.Out.WriteLine($"gateway-source-detour artifact edge: {edgeId}");
|
||||
}
|
||||
|
||||
var gatewaySourceVertexExitOffenders = layout.Edges
|
||||
.Where(edge => HasGatewaySourceVertexExit(edge, layoutNodesById[edge.SourceNodeId]))
|
||||
.Select(edge => edge.Id)
|
||||
.OrderBy(edgeId => edgeId, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
TestContext.Out.WriteLine($"gateway-source-vertex-exit artifact count: {gatewaySourceVertexExitOffenders.Length}");
|
||||
foreach (var edgeId in gatewaySourceVertexExitOffenders)
|
||||
{
|
||||
TestContext.Out.WriteLine($"gateway-source-vertex-exit artifact edge: {edgeId}");
|
||||
}
|
||||
|
||||
var gatewaySourceScoringOffenders = layout.Edges
|
||||
.Where(edge => HasGatewaySourceScoringIssue(edge, layout.Nodes))
|
||||
.Select(edge => edge.Id)
|
||||
.OrderBy(edgeId => edgeId, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
TestContext.Out.WriteLine($"gateway-source-scoring artifact count: {gatewaySourceScoringOffenders.Length}");
|
||||
foreach (var edgeId in gatewaySourceScoringOffenders)
|
||||
{
|
||||
TestContext.Out.WriteLine($"gateway-source-scoring artifact edge: {edgeId}");
|
||||
}
|
||||
|
||||
var loadConfigurationNode = layout.Nodes.Single(node => node.Id == "start/3");
|
||||
var processBatchLoopOffenders = layout.Edges
|
||||
.Where(edge => edge.TargetNodeId == "start/2/branch-1/1"
|
||||
&& edge.Label.StartsWith("repeat while", StringComparison.Ordinal)
|
||||
&& HasNearNodeClearanceViolation(edge, loadConfigurationNode, 40d))
|
||||
.Select(edge => edge.Id)
|
||||
.OrderBy(edgeId => edgeId, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
TestContext.Out.WriteLine($"process-batch-clearance artifact count: {processBatchLoopOffenders.Length}");
|
||||
foreach (var edgeId in processBatchLoopOffenders)
|
||||
{
|
||||
TestContext.Out.WriteLine($"process-batch-clearance artifact edge: {edgeId}");
|
||||
}
|
||||
|
||||
var crossings = 0;
|
||||
foreach (var node in layout.Nodes)
|
||||
{
|
||||
foreach (var edge in layout.Edges)
|
||||
{
|
||||
if (edge.SourceNodeId == node.Id || edge.TargetNodeId == node.Id) continue;
|
||||
foreach (var section in edge.Sections)
|
||||
{
|
||||
var points = new List<WorkflowRenderPoint> { section.StartPoint };
|
||||
points.AddRange(section.BendPoints);
|
||||
points.Add(section.EndPoint);
|
||||
for (var i = 0; i < points.Count - 1; i++)
|
||||
{
|
||||
var start = points[i];
|
||||
var end = points[i + 1];
|
||||
if (Math.Abs(start.Y - end.Y) < 2d && start.Y > node.Y && start.Y < node.Y + node.Height)
|
||||
{
|
||||
if (Math.Max(start.X, end.X) > node.X && Math.Min(start.X, end.X) < node.X + node.Width)
|
||||
{
|
||||
crossings++;
|
||||
}
|
||||
}
|
||||
else if (Math.Abs(start.X - end.X) < 2d && start.X > node.X && start.X < node.X + node.Width)
|
||||
{
|
||||
if (Math.Max(start.Y, end.Y) > node.Y && Math.Min(start.Y, end.Y) < node.Y + node.Height)
|
||||
{
|
||||
crossings++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
TestContext.Out.WriteLine($"edge-node-crossing artifact count: {crossings}");
|
||||
|
||||
var focusedSharedLaneCandidate = ElkEdgePostProcessor.SeparateSharedLaneConflicts(
|
||||
elkEdges,
|
||||
elkNodes,
|
||||
minLineClearance,
|
||||
["edge/15", "edge/17", "edge/35"]);
|
||||
TestContext.Out.WriteLine(
|
||||
$"focused shared-lane candidate counts: detour={ElkEdgeRoutingScoring.CountExcessiveDetourViolations(focusedSharedLaneCandidate, elkNodes)} gateway-source={ElkEdgeRoutingScoring.CountGatewaySourceExitViolations(focusedSharedLaneCandidate, elkNodes)} boundary-slots={ElkEdgeRoutingScoring.CountBoundarySlotViolations(focusedSharedLaneCandidate, elkNodes)} entry={ElkEdgeRoutingScoring.CountBadEntryAngles(focusedSharedLaneCandidate, elkNodes)} shared-lanes={ElkEdgeRoutingScoring.CountSharedLaneViolations(focusedSharedLaneCandidate, elkNodes)}");
|
||||
foreach (var edgeId in new[] { "edge/15", "edge/17", "edge/35" })
|
||||
{
|
||||
var currentEdge = elkEdges.Single(edge => edge.Id == edgeId);
|
||||
var candidateEdge = focusedSharedLaneCandidate.Single(edge => edge.Id == edgeId);
|
||||
TestContext.Out.WriteLine(
|
||||
$"focused shared-lane {edgeId} current: {string.Join(" -> ", ExtractElkPath(currentEdge).Select(point => $"({point.X:F3},{point.Y:F3})"))}");
|
||||
TestContext.Out.WriteLine(
|
||||
$"focused shared-lane {edgeId} candidate: {string.Join(" -> ", ExtractElkPath(candidateEdge).Select(point => $"({point.X:F3},{point.Y:F3})"))}");
|
||||
}
|
||||
|
||||
var combinedFocus = detourSeverityByEdgeId.Keys
|
||||
.Concat(gatewaySourceSeverityByEdgeId.Keys)
|
||||
.OrderBy(edgeId => edgeId, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
if (combinedFocus.Length > 0)
|
||||
{
|
||||
var batchCandidate = ElkEdgePostProcessor.PreferShortestBoundaryShortcuts(elkEdges, elkNodes, combinedFocus);
|
||||
batchCandidate = ElkEdgePostProcessor.RepairBoundaryAnglesAndTargetApproaches(batchCandidate, elkNodes, minLineClearance, combinedFocus);
|
||||
batchCandidate = ElkEdgePostProcessor.SpreadTargetApproachJoins(batchCandidate, elkNodes, minLineClearance, combinedFocus);
|
||||
batchCandidate = ElkEdgePostProcessor.SpreadRectTargetApproachFeederBands(batchCandidate, elkNodes, minLineClearance, combinedFocus);
|
||||
batchCandidate = ElkEdgePostProcessor.ElevateUnderNodeViolations(batchCandidate, elkNodes, minLineClearance, combinedFocus);
|
||||
batchCandidate = ElkEdgePostProcessor.FinalizeDecisionTargetEntries(batchCandidate, elkNodes, combinedFocus);
|
||||
batchCandidate = ElkEdgePostProcessor.FinalizeGatewayBoundaryGeometry(batchCandidate, elkNodes, combinedFocus);
|
||||
batchCandidate = ElkEdgePostProcessor.PolishTargetPeerConflicts(batchCandidate, elkNodes, minLineClearance, combinedFocus);
|
||||
batchCandidate = ElkEdgePostProcessor.SnapBoundarySlotAssignments(
|
||||
batchCandidate,
|
||||
elkNodes,
|
||||
minLineClearance,
|
||||
combinedFocus,
|
||||
enforceAllNodeEndpoints: true);
|
||||
|
||||
var batchDetourCount = ElkEdgeRoutingScoring.CountExcessiveDetourViolations(batchCandidate, elkNodes);
|
||||
var batchGatewaySourceCount = ElkEdgeRoutingScoring.CountGatewaySourceExitViolations(batchCandidate, elkNodes);
|
||||
var batchBoundarySlotCount = ElkEdgeRoutingScoring.CountBoundarySlotViolations(batchCandidate, elkNodes);
|
||||
var batchEntryCount = ElkEdgeRoutingScoring.CountBadEntryAngles(batchCandidate, elkNodes);
|
||||
var batchSharedLaneCount = ElkEdgeRoutingScoring.CountSharedLaneViolations(batchCandidate, elkNodes);
|
||||
TestContext.Out.WriteLine(
|
||||
$"batch-candidate counts: detour={batchDetourCount} gateway-source={batchGatewaySourceCount} boundary-slots={batchBoundarySlotCount} entry={batchEntryCount} shared-lanes={batchSharedLaneCount}");
|
||||
|
||||
foreach (var focusEdgeId in combinedFocus)
|
||||
{
|
||||
var focusedCandidate = ElkEdgePostProcessor.PreferShortestBoundaryShortcuts(elkEdges, elkNodes, [focusEdgeId]);
|
||||
focusedCandidate = ElkEdgePostProcessor.RepairBoundaryAnglesAndTargetApproaches(focusedCandidate, elkNodes, minLineClearance, [focusEdgeId]);
|
||||
focusedCandidate = ElkEdgePostProcessor.SpreadTargetApproachJoins(focusedCandidate, elkNodes, minLineClearance, [focusEdgeId]);
|
||||
focusedCandidate = ElkEdgePostProcessor.SpreadRectTargetApproachFeederBands(focusedCandidate, elkNodes, minLineClearance, [focusEdgeId]);
|
||||
focusedCandidate = ElkEdgePostProcessor.ElevateUnderNodeViolations(focusedCandidate, elkNodes, minLineClearance, [focusEdgeId]);
|
||||
focusedCandidate = ElkEdgePostProcessor.FinalizeDecisionTargetEntries(focusedCandidate, elkNodes, [focusEdgeId]);
|
||||
focusedCandidate = ElkEdgePostProcessor.FinalizeGatewayBoundaryGeometry(focusedCandidate, elkNodes, [focusEdgeId]);
|
||||
focusedCandidate = ElkEdgePostProcessor.PolishTargetPeerConflicts(focusedCandidate, elkNodes, minLineClearance, [focusEdgeId]);
|
||||
focusedCandidate = ElkEdgePostProcessor.SnapBoundarySlotAssignments(
|
||||
focusedCandidate,
|
||||
elkNodes,
|
||||
minLineClearance,
|
||||
[focusEdgeId],
|
||||
enforceAllNodeEndpoints: true);
|
||||
|
||||
TestContext.Out.WriteLine(
|
||||
$"focused {focusEdgeId}: detour={ElkEdgeRoutingScoring.CountExcessiveDetourViolations(focusedCandidate, elkNodes)} gateway-source={ElkEdgeRoutingScoring.CountGatewaySourceExitViolations(focusedCandidate, elkNodes)} boundary-slots={ElkEdgeRoutingScoring.CountBoundarySlotViolations(focusedCandidate, elkNodes)} entry={ElkEdgeRoutingScoring.CountBadEntryAngles(focusedCandidate, elkNodes)} shared-lanes={ElkEdgeRoutingScoring.CountSharedLaneViolations(focusedCandidate, elkNodes)}");
|
||||
}
|
||||
|
||||
var subsetResults = new List<(string[] Focus, int Detour, int GatewaySource, int BoundarySlots, int Entry, int SharedLanes)>();
|
||||
for (var mask = 1; mask < (1 << combinedFocus.Length); mask++)
|
||||
{
|
||||
var subset = combinedFocus
|
||||
.Where((_, index) => (mask & (1 << index)) != 0)
|
||||
.ToArray();
|
||||
var subsetCandidate = ElkEdgePostProcessor.PreferShortestBoundaryShortcuts(elkEdges, elkNodes, subset);
|
||||
subsetCandidate = ElkEdgePostProcessor.RepairBoundaryAnglesAndTargetApproaches(subsetCandidate, elkNodes, minLineClearance, subset);
|
||||
subsetCandidate = ElkEdgePostProcessor.SpreadTargetApproachJoins(subsetCandidate, elkNodes, minLineClearance, subset);
|
||||
subsetCandidate = ElkEdgePostProcessor.SpreadRectTargetApproachFeederBands(subsetCandidate, elkNodes, minLineClearance, subset);
|
||||
subsetCandidate = ElkEdgePostProcessor.ElevateUnderNodeViolations(subsetCandidate, elkNodes, minLineClearance, subset);
|
||||
subsetCandidate = ElkEdgePostProcessor.FinalizeDecisionTargetEntries(subsetCandidate, elkNodes, subset);
|
||||
subsetCandidate = ElkEdgePostProcessor.FinalizeGatewayBoundaryGeometry(subsetCandidate, elkNodes, subset);
|
||||
subsetCandidate = ElkEdgePostProcessor.PolishTargetPeerConflicts(subsetCandidate, elkNodes, minLineClearance, subset);
|
||||
subsetCandidate = ElkEdgePostProcessor.SnapBoundarySlotAssignments(
|
||||
subsetCandidate,
|
||||
elkNodes,
|
||||
minLineClearance,
|
||||
subset,
|
||||
enforceAllNodeEndpoints: true);
|
||||
|
||||
subsetResults.Add((
|
||||
subset,
|
||||
ElkEdgeRoutingScoring.CountExcessiveDetourViolations(subsetCandidate, elkNodes),
|
||||
ElkEdgeRoutingScoring.CountGatewaySourceExitViolations(subsetCandidate, elkNodes),
|
||||
ElkEdgeRoutingScoring.CountBoundarySlotViolations(subsetCandidate, elkNodes),
|
||||
ElkEdgeRoutingScoring.CountBadEntryAngles(subsetCandidate, elkNodes),
|
||||
ElkEdgeRoutingScoring.CountSharedLaneViolations(subsetCandidate, elkNodes)));
|
||||
}
|
||||
|
||||
foreach (var result in subsetResults
|
||||
.OrderBy(item => item.BoundarySlots)
|
||||
.ThenBy(item => item.GatewaySource)
|
||||
.ThenBy(item => item.Detour)
|
||||
.ThenBy(item => item.Entry)
|
||||
.ThenBy(item => item.SharedLanes)
|
||||
.ThenBy(item => item.Focus.Length)
|
||||
.ThenBy(item => string.Join(",", item.Focus), StringComparer.Ordinal)
|
||||
.Take(12))
|
||||
{
|
||||
TestContext.Out.WriteLine(
|
||||
$"subset [{string.Join(", ", result.Focus)}]: detour={result.Detour} gateway-source={result.GatewaySource} boundary-slots={result.BoundarySlots} entry={result.Entry} shared-lanes={result.SharedLanes}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -185,6 +185,164 @@ public partial class ElkSharpEdgeRefinementTests
|
||||
ElkEdgeRoutingScoring.CountGatewaySourceVertexExitViolations([edge], [join, target]).Should().Be(1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Property("Intent", "Operational")]
|
||||
public void GatewayBoundaryHelpers_WhenDecisionSourceLeavesFromTipOnStraightExit_ShouldRepairOffVertex()
|
||||
{
|
||||
var source = new ElkPositionedNode
|
||||
{
|
||||
Id = "decision",
|
||||
Label = "Check Result",
|
||||
Kind = "Decision",
|
||||
X = 3406,
|
||||
Y = 225.182,
|
||||
Width = 188,
|
||||
Height = 132,
|
||||
};
|
||||
var target = new ElkPositionedNode
|
||||
{
|
||||
Id = "task",
|
||||
Label = "Set printBatched",
|
||||
Kind = "SetState",
|
||||
X = 3778,
|
||||
Y = 239.182,
|
||||
Width = 224,
|
||||
Height = 104,
|
||||
};
|
||||
var tip = new ElkPoint
|
||||
{
|
||||
X = source.X + source.Width,
|
||||
Y = source.Y + (source.Height / 2d),
|
||||
};
|
||||
var edge = new ElkRoutedEdge
|
||||
{
|
||||
Id = "edge/16",
|
||||
SourceNodeId = source.Id,
|
||||
TargetNodeId = target.Id,
|
||||
Sections =
|
||||
[
|
||||
new ElkEdgeSection
|
||||
{
|
||||
StartPoint = tip,
|
||||
EndPoint = new ElkPoint { X = target.X, Y = tip.Y },
|
||||
BendPoints = [],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
ElkEdgeRoutingScoring.CountGatewaySourceVertexExitViolations([edge], [source, target]).Should().Be(1);
|
||||
|
||||
var helper = typeof(ElkEdgePostProcessor).GetMethod(
|
||||
"ForceDecisionSourceExitOffVertex",
|
||||
BindingFlags.Static | BindingFlags.NonPublic)!;
|
||||
var repairedPath = ((IReadOnlyList<ElkPoint>)helper.Invoke(
|
||||
null,
|
||||
[new List<ElkPoint> { tip, new ElkPoint { X = target.X, Y = tip.Y } }, source, new[] { source, target }, source.Id, target.Id])!)
|
||||
.ToList();
|
||||
var repaired = new[]
|
||||
{
|
||||
new ElkRoutedEdge
|
||||
{
|
||||
Id = edge.Id,
|
||||
SourceNodeId = edge.SourceNodeId,
|
||||
TargetNodeId = edge.TargetNodeId,
|
||||
Sections =
|
||||
[
|
||||
new ElkEdgeSection
|
||||
{
|
||||
StartPoint = repairedPath[0],
|
||||
EndPoint = repairedPath[^1],
|
||||
BendPoints = repairedPath.Skip(1).Take(Math.Max(0, repairedPath.Count - 2)).ToArray(),
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
ElkEdgeRoutingScoring.CountGatewaySourceVertexExitViolations(repaired, [source, target]).Should().Be(0);
|
||||
ElkEdgeRoutingScoring.CountBoundarySlotViolations(repaired, [source, target]).Should().Be(0);
|
||||
ElkShapeBoundaries.IsNearGatewayVertex(source, repairedPath[0]).Should().BeFalse();
|
||||
repairedPath[1].X.Should().BeGreaterThan(repairedPath[0].X + 3d);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Property("Intent", "Operational")]
|
||||
public void GatewayBoundaryHelpers_WhenJoinSourceLeavesFromTipOnStraightExit_ShouldRepairOffVertex()
|
||||
{
|
||||
var source = new ElkPositionedNode
|
||||
{
|
||||
Id = "join",
|
||||
Label = "Parallel Execution Join",
|
||||
Kind = "Join",
|
||||
X = 1290,
|
||||
Y = 116.591,
|
||||
Width = 176,
|
||||
Height = 124,
|
||||
};
|
||||
var target = new ElkPositionedNode
|
||||
{
|
||||
Id = "task",
|
||||
Label = "Load Configuration",
|
||||
Kind = "TransportCall",
|
||||
X = 1604,
|
||||
Y = 134.591,
|
||||
Width = 208,
|
||||
Height = 88,
|
||||
};
|
||||
var tip = new ElkPoint
|
||||
{
|
||||
X = source.X + source.Width,
|
||||
Y = source.Y + (source.Height / 2d),
|
||||
};
|
||||
var edge = new ElkRoutedEdge
|
||||
{
|
||||
Id = "edge/19",
|
||||
SourceNodeId = source.Id,
|
||||
TargetNodeId = target.Id,
|
||||
Sections =
|
||||
[
|
||||
new ElkEdgeSection
|
||||
{
|
||||
StartPoint = tip,
|
||||
EndPoint = new ElkPoint { X = target.X, Y = tip.Y },
|
||||
BendPoints = [],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
ElkEdgeRoutingScoring.CountGatewaySourceVertexExitViolations([edge], [source, target]).Should().Be(1);
|
||||
|
||||
var helper = typeof(ElkEdgePostProcessor).GetMethod(
|
||||
"ForceDecisionSourceExitOffVertex",
|
||||
BindingFlags.Static | BindingFlags.NonPublic)!;
|
||||
var repairedPath = ((IReadOnlyList<ElkPoint>)helper.Invoke(
|
||||
null,
|
||||
[new List<ElkPoint> { tip, new ElkPoint { X = target.X, Y = tip.Y } }, source, new[] { source, target }, source.Id, target.Id])!)
|
||||
.ToList();
|
||||
var repaired = new[]
|
||||
{
|
||||
new ElkRoutedEdge
|
||||
{
|
||||
Id = edge.Id,
|
||||
SourceNodeId = edge.SourceNodeId,
|
||||
TargetNodeId = edge.TargetNodeId,
|
||||
Sections =
|
||||
[
|
||||
new ElkEdgeSection
|
||||
{
|
||||
StartPoint = repairedPath[0],
|
||||
EndPoint = repairedPath[^1],
|
||||
BendPoints = repairedPath.Skip(1).Take(Math.Max(0, repairedPath.Count - 2)).ToArray(),
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
ElkEdgeRoutingScoring.CountGatewaySourceVertexExitViolations(repaired, [source, target]).Should().Be(0);
|
||||
ElkEdgeRoutingScoring.CountBoundarySlotViolations(repaired, [source, target]).Should().Be(0);
|
||||
ElkShapeBoundaries.IsNearGatewayVertex(source, repairedPath[0]).Should().BeFalse();
|
||||
repairedPath[1].X.Should().BeGreaterThan(repairedPath[0].X + 3d);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Property("Intent", "Operational")]
|
||||
public void GatewayBoundaryHelpers_WhenTwoEdgesArriveNearParallelIntoJoin_ShouldCountTargetJoinViolation()
|
||||
@@ -1186,6 +1344,24 @@ public partial class ElkSharpEdgeRefinementTests
|
||||
var composeTransactionalFinalDetourCandidate = typeof(ElkEdgeRouterIterative).GetMethod(
|
||||
"ComposeTransactionalFinalDetourCandidate",
|
||||
BindingFlags.Static | BindingFlags.NonPublic)!;
|
||||
var expandWinningSolutionFocus = typeof(ElkEdgeRouterIterative).GetMethod(
|
||||
"ExpandWinningSolutionFocus",
|
||||
BindingFlags.Static | BindingFlags.NonPublic)!;
|
||||
var buildFinalBoundarySlotCandidate = typeof(ElkEdgeRouterIterative).GetMethod(
|
||||
"BuildFinalBoundarySlotCandidate",
|
||||
BindingFlags.Static | BindingFlags.NonPublic)!;
|
||||
var buildFinalRestabilizedCandidate = typeof(ElkEdgeRouterIterative).GetMethod(
|
||||
"BuildFinalRestabilizedCandidate",
|
||||
BindingFlags.Static | BindingFlags.NonPublic)!;
|
||||
var buildRetryState = typeof(ElkEdgeRouterIterative).GetMethod(
|
||||
"BuildRetryState",
|
||||
BindingFlags.Static | BindingFlags.NonPublic)!;
|
||||
var compareRetryStates = typeof(ElkEdgeRouterIterative).GetMethod(
|
||||
"CompareRetryStates",
|
||||
BindingFlags.Static | BindingFlags.NonPublic)!;
|
||||
var hasHardRuleRegression = typeof(ElkEdgeRouterIterative).GetMethod(
|
||||
"HasHardRuleRegression",
|
||||
BindingFlags.Static | BindingFlags.NonPublic)!;
|
||||
|
||||
var offenders = elkEdges
|
||||
.Where(edge =>
|
||||
@@ -1223,22 +1399,97 @@ public partial class ElkSharpEdgeRefinementTests
|
||||
var finalizedBoundary = ElkEdgeRoutingScoring.CountBadBoundaryAngles(finalized, elkNodes);
|
||||
var finalizedGatewaySource = ElkEdgeRoutingScoring.CountGatewaySourceExitViolations(finalized, elkNodes);
|
||||
var edgeSpecificDebug = string.Empty;
|
||||
if (edge.Id is "edge/7" or "edge/27")
|
||||
if (edge.Id is "edge/7" or "edge/9" or "edge/20" or "edge/27")
|
||||
{
|
||||
var targetNode = elkNodes.Single(node => node.Id == edge.TargetNodeId);
|
||||
var expandedFocusEdgeIds = ((IEnumerable<string>)expandWinningSolutionFocus.Invoke(
|
||||
null,
|
||||
new object?[] { elkEdges, new[] { edge.Id } })!)
|
||||
.ToArray();
|
||||
var focusedLayoutRepair = ElkEdgePostProcessor.PreferShortestBoundaryShortcuts(elkEdges, elkNodes, [edge.Id]);
|
||||
var transactionalLayoutRepair = (ElkRoutedEdge[])composeTransactionalFinalDetourCandidate.Invoke(
|
||||
null,
|
||||
new object?[] { elkEdges, elkNodes, 52.7d, new[] { edge.Id } })!;
|
||||
var focusedExpandedSnapLayoutRepair = ElkEdgePostProcessor.SnapBoundarySlotAssignments(
|
||||
focusedLayoutRepair,
|
||||
elkNodes,
|
||||
52.7d,
|
||||
expandedFocusEdgeIds,
|
||||
enforceAllNodeEndpoints: true);
|
||||
var focusedExpandedSnapBacktrackingRepairLayout = focusedExpandedSnapLayoutRepair;
|
||||
var expandedTransactionalLayoutRepair = (ElkRoutedEdge[])composeTransactionalFinalDetourCandidate.Invoke(
|
||||
null,
|
||||
new object?[] { elkEdges, elkNodes, 52.7d, expandedFocusEdgeIds })!;
|
||||
var focusedExpandedRestabilizedLayoutRepair = (ElkRoutedEdge[])buildFinalRestabilizedCandidate.Invoke(
|
||||
null,
|
||||
new object?[]
|
||||
{
|
||||
focusedLayoutRepair,
|
||||
elkNodes,
|
||||
ElkLayoutDirection.LeftToRight,
|
||||
52.7d,
|
||||
expandedFocusEdgeIds,
|
||||
})!;
|
||||
var pairedTransactionalLayoutRepair = edge.Id == "edge/27"
|
||||
? (ElkRoutedEdge[])composeTransactionalFinalDetourCandidate.Invoke(
|
||||
null,
|
||||
new object?[] { elkEdges, elkNodes, 52.7d, new[] { "edge/27", "edge/28" } })!
|
||||
: transactionalLayoutRepair;
|
||||
var focusedExpandedSnapNewOffenderRevertLayout = focusedExpandedSnapLayoutRepair;
|
||||
var baselineScore = ElkEdgeRoutingScoring.ComputeScore(elkEdges, elkNodes);
|
||||
var focusedLayoutScore = ElkEdgeRoutingScoring.ComputeScore(focusedLayoutRepair, elkNodes);
|
||||
var transactionalLayoutScore = ElkEdgeRoutingScoring.ComputeScore(transactionalLayoutRepair, elkNodes);
|
||||
var focusedExpandedSnapLayoutScore = ElkEdgeRoutingScoring.ComputeScore(focusedExpandedSnapLayoutRepair, elkNodes);
|
||||
var focusedExpandedSnapBacktrackingRepairLayoutScore = focusedExpandedSnapLayoutScore;
|
||||
var focusedExpandedSnapNewOffenderRevertLayoutScore = focusedExpandedSnapLayoutScore;
|
||||
var expandedTransactionalLayoutScore = ElkEdgeRoutingScoring.ComputeScore(expandedTransactionalLayoutRepair, elkNodes);
|
||||
var focusedExpandedRestabilizedLayoutScore = ElkEdgeRoutingScoring.ComputeScore(focusedExpandedRestabilizedLayoutRepair, elkNodes);
|
||||
var pairedTransactionalLayoutScore = ElkEdgeRoutingScoring.ComputeScore(pairedTransactionalLayoutRepair, elkNodes);
|
||||
var focusedBoundarySlotCandidate = (ElkRoutedEdge[])buildFinalBoundarySlotCandidate.Invoke(
|
||||
null,
|
||||
new object?[]
|
||||
{
|
||||
focusedLayoutRepair,
|
||||
elkNodes,
|
||||
ElkLayoutDirection.LeftToRight,
|
||||
52.7d,
|
||||
new[] { edge.Id },
|
||||
false,
|
||||
})!;
|
||||
var focusedBoundarySlotScore = ElkEdgeRoutingScoring.ComputeScore(focusedBoundarySlotCandidate, elkNodes);
|
||||
var baselineBrokenHighways = ElkEdgeRouterHighway.DetectRemainingBrokenHighways(elkEdges, elkNodes).Count;
|
||||
var focusedBrokenHighways = ElkEdgeRouterHighway.DetectRemainingBrokenHighways(focusedLayoutRepair, elkNodes).Count;
|
||||
var transactionalBrokenHighways = ElkEdgeRouterHighway.DetectRemainingBrokenHighways(transactionalLayoutRepair, elkNodes).Count;
|
||||
var focusedExpandedSnapBrokenHighways = ElkEdgeRouterHighway.DetectRemainingBrokenHighways(focusedExpandedSnapLayoutRepair, elkNodes).Count;
|
||||
var focusedExpandedSnapBacktrackingRepairBrokenHighways = focusedExpandedSnapBrokenHighways;
|
||||
var focusedExpandedSnapNewOffenderRevertBrokenHighways = focusedExpandedSnapBrokenHighways;
|
||||
var expandedTransactionalBrokenHighways = ElkEdgeRouterHighway.DetectRemainingBrokenHighways(expandedTransactionalLayoutRepair, elkNodes).Count;
|
||||
var focusedExpandedRestabilizedBrokenHighways = ElkEdgeRouterHighway.DetectRemainingBrokenHighways(focusedExpandedRestabilizedLayoutRepair, elkNodes).Count;
|
||||
var pairedTransactionalBrokenHighways = ElkEdgeRouterHighway.DetectRemainingBrokenHighways(pairedTransactionalLayoutRepair, elkNodes).Count;
|
||||
var focusedBoundarySlotBrokenHighways = ElkEdgeRouterHighway.DetectRemainingBrokenHighways(focusedBoundarySlotCandidate, elkNodes).Count;
|
||||
var baselineRetryState = (RoutingRetryState)buildRetryState.Invoke(
|
||||
null,
|
||||
new object?[] { baselineScore, baselineBrokenHighways })!;
|
||||
var focusedLayoutRetryState = (RoutingRetryState)buildRetryState.Invoke(
|
||||
null,
|
||||
new object?[] { focusedLayoutScore, focusedBrokenHighways })!;
|
||||
var transactionalLayoutRetryState = (RoutingRetryState)buildRetryState.Invoke(
|
||||
null,
|
||||
new object?[] { transactionalLayoutScore, transactionalBrokenHighways })!;
|
||||
var focusedExpandedSnapLayoutRetryState = (RoutingRetryState)buildRetryState.Invoke(
|
||||
null,
|
||||
new object?[] { focusedExpandedSnapLayoutScore, focusedExpandedSnapBrokenHighways })!;
|
||||
var focusedExpandedSnapBacktrackingRepairLayoutRetryState = focusedExpandedSnapLayoutRetryState;
|
||||
var focusedExpandedSnapNewOffenderRevertLayoutRetryState = focusedExpandedSnapLayoutRetryState;
|
||||
var expandedTransactionalLayoutRetryState = (RoutingRetryState)buildRetryState.Invoke(
|
||||
null,
|
||||
new object?[] { expandedTransactionalLayoutScore, expandedTransactionalBrokenHighways })!;
|
||||
var focusedExpandedRestabilizedLayoutRetryState = (RoutingRetryState)buildRetryState.Invoke(
|
||||
null,
|
||||
new object?[] { focusedExpandedRestabilizedLayoutScore, focusedExpandedRestabilizedBrokenHighways })!;
|
||||
var pairedTransactionalLayoutRetryState = (RoutingRetryState)buildRetryState.Invoke(
|
||||
null,
|
||||
new object?[] { pairedTransactionalLayoutScore, pairedTransactionalBrokenHighways })!;
|
||||
var peerConflictLayoutRepair = edge.Id == "edge/27"
|
||||
? focusedLayoutRepair.ToArray()
|
||||
: focusedLayoutRepair;
|
||||
@@ -1265,8 +1516,110 @@ public partial class ElkSharpEdgeRefinementTests
|
||||
}
|
||||
}
|
||||
var peerConflictLayoutScore = ElkEdgeRoutingScoring.ComputeScore(peerConflictLayoutRepair, elkNodes);
|
||||
var baselineBacktrackingSeverity = new Dictionary<string, int>(StringComparer.Ordinal);
|
||||
ElkEdgeRoutingScoring.CountTargetApproachBacktrackingViolations(
|
||||
elkEdges,
|
||||
elkNodes,
|
||||
baselineBacktrackingSeverity,
|
||||
1);
|
||||
var focusedJoinSeverity = new Dictionary<string, int>(StringComparer.Ordinal);
|
||||
ElkEdgeRoutingScoring.CountTargetApproachJoinViolations(focusedLayoutRepair, elkNodes, focusedJoinSeverity, 1);
|
||||
var expandedTransactionalBacktrackingSeverity = new Dictionary<string, int>(StringComparer.Ordinal);
|
||||
ElkEdgeRoutingScoring.CountTargetApproachBacktrackingViolations(
|
||||
expandedTransactionalLayoutRepair,
|
||||
elkNodes,
|
||||
expandedTransactionalBacktrackingSeverity,
|
||||
1);
|
||||
var focusedExpandedSnapBacktrackingSeverity = new Dictionary<string, int>(StringComparer.Ordinal);
|
||||
ElkEdgeRoutingScoring.CountTargetApproachBacktrackingViolations(
|
||||
focusedExpandedSnapLayoutRepair,
|
||||
elkNodes,
|
||||
focusedExpandedSnapBacktrackingSeverity,
|
||||
1);
|
||||
if (focusedExpandedSnapBacktrackingSeverity.Count > 0)
|
||||
{
|
||||
focusedExpandedSnapBacktrackingRepairLayout = ElkEdgePostProcessor.NormalizeBoundaryAngles(
|
||||
ElkEdgePostProcessor.RepairBoundaryAnglesAndTargetApproaches(
|
||||
focusedExpandedSnapLayoutRepair,
|
||||
elkNodes,
|
||||
52.7d,
|
||||
focusedExpandedSnapBacktrackingSeverity.Keys
|
||||
.OrderBy(id => id, StringComparer.Ordinal)
|
||||
.ToArray()),
|
||||
elkNodes);
|
||||
focusedExpandedSnapBacktrackingRepairLayoutScore = ElkEdgeRoutingScoring.ComputeScore(
|
||||
focusedExpandedSnapBacktrackingRepairLayout,
|
||||
elkNodes);
|
||||
focusedExpandedSnapBacktrackingRepairBrokenHighways = ElkEdgeRouterHighway.DetectRemainingBrokenHighways(
|
||||
focusedExpandedSnapBacktrackingRepairLayout,
|
||||
elkNodes).Count;
|
||||
focusedExpandedSnapBacktrackingRepairLayoutRetryState = (RoutingRetryState)buildRetryState.Invoke(
|
||||
null,
|
||||
new object?[]
|
||||
{
|
||||
focusedExpandedSnapBacktrackingRepairLayoutScore,
|
||||
focusedExpandedSnapBacktrackingRepairBrokenHighways,
|
||||
})!;
|
||||
}
|
||||
var focusedExpandedSnapBacktrackingRepairSeverity = new Dictionary<string, int>(StringComparer.Ordinal);
|
||||
ElkEdgeRoutingScoring.CountTargetApproachBacktrackingViolations(
|
||||
focusedExpandedSnapBacktrackingRepairLayout,
|
||||
elkNodes,
|
||||
focusedExpandedSnapBacktrackingRepairSeverity,
|
||||
1);
|
||||
var focusedExpandedSnapNewOffenderEdgeIds = focusedExpandedSnapBacktrackingSeverity.Keys
|
||||
.Except(baselineBacktrackingSeverity.Keys, StringComparer.Ordinal)
|
||||
.OrderBy(id => id, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
if (focusedExpandedSnapNewOffenderEdgeIds.Length > 0)
|
||||
{
|
||||
focusedExpandedSnapNewOffenderRevertLayout = focusedExpandedSnapLayoutRepair.ToArray();
|
||||
foreach (var revertEdgeId in focusedExpandedSnapNewOffenderEdgeIds)
|
||||
{
|
||||
var revertIndex = Array.FindIndex(
|
||||
focusedExpandedSnapNewOffenderRevertLayout,
|
||||
candidate => string.Equals(candidate.Id, revertEdgeId, StringComparison.Ordinal));
|
||||
if (revertIndex < 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
focusedExpandedSnapNewOffenderRevertLayout[revertIndex] = focusedLayoutRepair.Single(candidate =>
|
||||
string.Equals(candidate.Id, revertEdgeId, StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
focusedExpandedSnapNewOffenderRevertLayoutScore = ElkEdgeRoutingScoring.ComputeScore(
|
||||
focusedExpandedSnapNewOffenderRevertLayout,
|
||||
elkNodes);
|
||||
focusedExpandedSnapNewOffenderRevertBrokenHighways = ElkEdgeRouterHighway.DetectRemainingBrokenHighways(
|
||||
focusedExpandedSnapNewOffenderRevertLayout,
|
||||
elkNodes).Count;
|
||||
focusedExpandedSnapNewOffenderRevertLayoutRetryState = (RoutingRetryState)buildRetryState.Invoke(
|
||||
null,
|
||||
new object?[]
|
||||
{
|
||||
focusedExpandedSnapNewOffenderRevertLayoutScore,
|
||||
focusedExpandedSnapNewOffenderRevertBrokenHighways,
|
||||
})!;
|
||||
}
|
||||
var focusedExpandedSnapNewOffenderRevertSeverity = new Dictionary<string, int>(StringComparer.Ordinal);
|
||||
ElkEdgeRoutingScoring.CountTargetApproachBacktrackingViolations(
|
||||
focusedExpandedSnapNewOffenderRevertLayout,
|
||||
elkNodes,
|
||||
focusedExpandedSnapNewOffenderRevertSeverity,
|
||||
1);
|
||||
var focusedExpandedRestabilizedBacktrackingSeverity = new Dictionary<string, int>(StringComparer.Ordinal);
|
||||
ElkEdgeRoutingScoring.CountTargetApproachBacktrackingViolations(
|
||||
focusedExpandedRestabilizedLayoutRepair,
|
||||
elkNodes,
|
||||
focusedExpandedRestabilizedBacktrackingSeverity,
|
||||
1);
|
||||
var focusedExpandedRestabilizedJoinSeverity = new Dictionary<string, int>(StringComparer.Ordinal);
|
||||
ElkEdgeRoutingScoring.CountTargetApproachJoinViolations(
|
||||
focusedExpandedRestabilizedLayoutRepair,
|
||||
elkNodes,
|
||||
focusedExpandedRestabilizedJoinSeverity,
|
||||
1);
|
||||
var focusedLayoutSection = focusedLayoutRepair.Single(candidate => candidate.Id == edge.Id).Sections.Single();
|
||||
var focusedLayoutPath = focusedLayoutSection.BendPoints
|
||||
.Prepend(focusedLayoutSection.StartPoint)
|
||||
@@ -1279,6 +1632,36 @@ public partial class ElkSharpEdgeRefinementTests
|
||||
.Append(transactionalLayoutSection.EndPoint)
|
||||
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
||||
.ToArray();
|
||||
var focusedExpandedSnapLayoutSection = focusedExpandedSnapLayoutRepair.Single(candidate => candidate.Id == edge.Id).Sections.Single();
|
||||
var focusedExpandedSnapLayoutPath = focusedExpandedSnapLayoutSection.BendPoints
|
||||
.Prepend(focusedExpandedSnapLayoutSection.StartPoint)
|
||||
.Append(focusedExpandedSnapLayoutSection.EndPoint)
|
||||
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
||||
.ToArray();
|
||||
var focusedExpandedSnapBacktrackingRepairLayoutSection = focusedExpandedSnapBacktrackingRepairLayout.Single(candidate => candidate.Id == edge.Id).Sections.Single();
|
||||
var focusedExpandedSnapBacktrackingRepairLayoutPath = focusedExpandedSnapBacktrackingRepairLayoutSection.BendPoints
|
||||
.Prepend(focusedExpandedSnapBacktrackingRepairLayoutSection.StartPoint)
|
||||
.Append(focusedExpandedSnapBacktrackingRepairLayoutSection.EndPoint)
|
||||
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
||||
.ToArray();
|
||||
var focusedExpandedSnapNewOffenderRevertLayoutSection = focusedExpandedSnapNewOffenderRevertLayout.Single(candidate => candidate.Id == edge.Id).Sections.Single();
|
||||
var focusedExpandedSnapNewOffenderRevertLayoutPath = focusedExpandedSnapNewOffenderRevertLayoutSection.BendPoints
|
||||
.Prepend(focusedExpandedSnapNewOffenderRevertLayoutSection.StartPoint)
|
||||
.Append(focusedExpandedSnapNewOffenderRevertLayoutSection.EndPoint)
|
||||
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
||||
.ToArray();
|
||||
var expandedTransactionalLayoutSection = expandedTransactionalLayoutRepair.Single(candidate => candidate.Id == edge.Id).Sections.Single();
|
||||
var expandedTransactionalLayoutPath = expandedTransactionalLayoutSection.BendPoints
|
||||
.Prepend(expandedTransactionalLayoutSection.StartPoint)
|
||||
.Append(expandedTransactionalLayoutSection.EndPoint)
|
||||
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
||||
.ToArray();
|
||||
var focusedExpandedRestabilizedLayoutSection = focusedExpandedRestabilizedLayoutRepair.Single(candidate => candidate.Id == edge.Id).Sections.Single();
|
||||
var focusedExpandedRestabilizedLayoutPath = focusedExpandedRestabilizedLayoutSection.BendPoints
|
||||
.Prepend(focusedExpandedRestabilizedLayoutSection.StartPoint)
|
||||
.Append(focusedExpandedRestabilizedLayoutSection.EndPoint)
|
||||
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
||||
.ToArray();
|
||||
var peerConflictLayoutSection = peerConflictLayoutRepair.Single(candidate => candidate.Id == edge.Id).Sections.Single();
|
||||
var peerConflictLayoutPath = peerConflictLayoutSection.BendPoints
|
||||
.Prepend(peerConflictLayoutSection.StartPoint)
|
||||
@@ -1291,6 +1674,21 @@ public partial class ElkSharpEdgeRefinementTests
|
||||
var chosenTransactionalLayout = (ElkRoutedEdge[])choosePreferredHardRuleLayout.Invoke(
|
||||
null,
|
||||
new object?[] { elkEdges, transactionalLayoutRepair, elkNodes })!;
|
||||
var chosenFocusedExpandedSnapLayout = (ElkRoutedEdge[])choosePreferredHardRuleLayout.Invoke(
|
||||
null,
|
||||
new object?[] { elkEdges, focusedExpandedSnapLayoutRepair, elkNodes })!;
|
||||
var chosenFocusedExpandedSnapBacktrackingRepairLayout = (ElkRoutedEdge[])choosePreferredHardRuleLayout.Invoke(
|
||||
null,
|
||||
new object?[] { elkEdges, focusedExpandedSnapBacktrackingRepairLayout, elkNodes })!;
|
||||
var chosenFocusedExpandedSnapNewOffenderRevertLayout = (ElkRoutedEdge[])choosePreferredHardRuleLayout.Invoke(
|
||||
null,
|
||||
new object?[] { elkEdges, focusedExpandedSnapNewOffenderRevertLayout, elkNodes })!;
|
||||
var chosenExpandedTransactionalLayout = (ElkRoutedEdge[])choosePreferredHardRuleLayout.Invoke(
|
||||
null,
|
||||
new object?[] { elkEdges, expandedTransactionalLayoutRepair, elkNodes })!;
|
||||
var chosenFocusedExpandedRestabilizedLayout = (ElkRoutedEdge[])choosePreferredHardRuleLayout.Invoke(
|
||||
null,
|
||||
new object?[] { elkEdges, focusedExpandedRestabilizedLayoutRepair, elkNodes })!;
|
||||
var chosenPairedTransactionalLayout = (ElkRoutedEdge[])choosePreferredHardRuleLayout.Invoke(
|
||||
null,
|
||||
new object?[] { elkEdges, pairedTransactionalLayoutRepair, elkNodes })!;
|
||||
@@ -1326,8 +1724,13 @@ public partial class ElkSharpEdgeRefinementTests
|
||||
};
|
||||
edgeSpecificDebug += $" | chooser={(ReferenceEquals(chosenLayout, focusedLayoutRepair) ? "candidate" : "baseline")}";
|
||||
edgeSpecificDebug += $" | baseline-score entry={baselineScore.EntryAngleViolations},"
|
||||
+ $" nc={baselineScore.NodeCrossings},"
|
||||
+ $" short={baselineBrokenHighways},"
|
||||
+ $" gateway-source={baselineScore.GatewaySourceExitViolations},"
|
||||
+ $" collector-corridor={baselineScore.RepeatCollectorCorridorViolations},"
|
||||
+ $" collector-clearance={baselineScore.RepeatCollectorNodeClearanceViolations},"
|
||||
+ $" shared={baselineScore.SharedLaneViolations},"
|
||||
+ $" boundary={baselineScore.BoundarySlotViolations},"
|
||||
+ $" joins={baselineScore.TargetApproachJoinViolations},"
|
||||
+ $" backtracking={baselineScore.TargetApproachBacktrackingViolations},"
|
||||
+ $" detour={baselineScore.ExcessiveDetourViolations},"
|
||||
@@ -1336,10 +1739,16 @@ public partial class ElkSharpEdgeRefinementTests
|
||||
+ $" longdiag={baselineScore.LongDiagonalViolations},"
|
||||
+ $" proximity={baselineScore.ProximityViolations},"
|
||||
+ $" label={baselineScore.LabelProximityViolations},"
|
||||
+ $" crossings={baselineScore.EdgeCrossings}";
|
||||
+ $" crossings={baselineScore.EdgeCrossings},"
|
||||
+ $" value={baselineScore.Value:F0}";
|
||||
edgeSpecificDebug += $" | focused-score entry={focusedLayoutScore.EntryAngleViolations},"
|
||||
+ $" nc={focusedLayoutScore.NodeCrossings},"
|
||||
+ $" short={focusedBrokenHighways},"
|
||||
+ $" gateway-source={focusedLayoutScore.GatewaySourceExitViolations},"
|
||||
+ $" collector-corridor={focusedLayoutScore.RepeatCollectorCorridorViolations},"
|
||||
+ $" collector-clearance={focusedLayoutScore.RepeatCollectorNodeClearanceViolations},"
|
||||
+ $" shared={focusedLayoutScore.SharedLaneViolations},"
|
||||
+ $" boundary={focusedLayoutScore.BoundarySlotViolations},"
|
||||
+ $" joins={focusedLayoutScore.TargetApproachJoinViolations},"
|
||||
+ $" backtracking={focusedLayoutScore.TargetApproachBacktrackingViolations},"
|
||||
+ $" detour={focusedLayoutScore.ExcessiveDetourViolations},"
|
||||
@@ -1348,11 +1757,19 @@ public partial class ElkSharpEdgeRefinementTests
|
||||
+ $" longdiag={focusedLayoutScore.LongDiagonalViolations},"
|
||||
+ $" proximity={focusedLayoutScore.ProximityViolations},"
|
||||
+ $" label={focusedLayoutScore.LabelProximityViolations},"
|
||||
+ $" crossings={focusedLayoutScore.EdgeCrossings}";
|
||||
+ $" crossings={focusedLayoutScore.EdgeCrossings},"
|
||||
+ $" value={focusedLayoutScore.Value:F0}";
|
||||
edgeSpecificDebug += $" | focused-compare retry={compareRetryStates.Invoke(null, new object?[] { focusedLayoutRetryState, baselineRetryState })},"
|
||||
+ $" hard={(bool)hasHardRuleRegression.Invoke(null, new object?[] { focusedLayoutRetryState, baselineRetryState })!}";
|
||||
edgeSpecificDebug += $" | transactional-chooser={(ReferenceEquals(chosenTransactionalLayout, transactionalLayoutRepair) ? "candidate" : "baseline")}";
|
||||
edgeSpecificDebug += $" | transactional-score entry={transactionalLayoutScore.EntryAngleViolations},"
|
||||
+ $" nc={transactionalLayoutScore.NodeCrossings},"
|
||||
+ $" short={transactionalBrokenHighways},"
|
||||
+ $" gateway-source={transactionalLayoutScore.GatewaySourceExitViolations},"
|
||||
+ $" collector-corridor={transactionalLayoutScore.RepeatCollectorCorridorViolations},"
|
||||
+ $" collector-clearance={transactionalLayoutScore.RepeatCollectorNodeClearanceViolations},"
|
||||
+ $" shared={transactionalLayoutScore.SharedLaneViolations},"
|
||||
+ $" boundary={transactionalLayoutScore.BoundarySlotViolations},"
|
||||
+ $" joins={transactionalLayoutScore.TargetApproachJoinViolations},"
|
||||
+ $" backtracking={transactionalLayoutScore.TargetApproachBacktrackingViolations},"
|
||||
+ $" detour={transactionalLayoutScore.ExcessiveDetourViolations},"
|
||||
@@ -1361,11 +1778,132 @@ public partial class ElkSharpEdgeRefinementTests
|
||||
+ $" longdiag={transactionalLayoutScore.LongDiagonalViolations},"
|
||||
+ $" proximity={transactionalLayoutScore.ProximityViolations},"
|
||||
+ $" label={transactionalLayoutScore.LabelProximityViolations},"
|
||||
+ $" crossings={transactionalLayoutScore.EdgeCrossings}";
|
||||
+ $" crossings={transactionalLayoutScore.EdgeCrossings},"
|
||||
+ $" value={transactionalLayoutScore.Value:F0}";
|
||||
edgeSpecificDebug += $" | transactional-compare retry={compareRetryStates.Invoke(null, new object?[] { transactionalLayoutRetryState, baselineRetryState })},"
|
||||
+ $" hard={(bool)hasHardRuleRegression.Invoke(null, new object?[] { transactionalLayoutRetryState, baselineRetryState })!}";
|
||||
edgeSpecificDebug += $" | focused-expanded-snap-chooser={(ReferenceEquals(chosenFocusedExpandedSnapLayout, focusedExpandedSnapLayoutRepair) ? "candidate" : "baseline")}";
|
||||
edgeSpecificDebug += $" | focused-expanded-snap-score entry={focusedExpandedSnapLayoutScore.EntryAngleViolations},"
|
||||
+ $" nc={focusedExpandedSnapLayoutScore.NodeCrossings},"
|
||||
+ $" short={focusedExpandedSnapBrokenHighways},"
|
||||
+ $" gateway-source={focusedExpandedSnapLayoutScore.GatewaySourceExitViolations},"
|
||||
+ $" collector-corridor={focusedExpandedSnapLayoutScore.RepeatCollectorCorridorViolations},"
|
||||
+ $" collector-clearance={focusedExpandedSnapLayoutScore.RepeatCollectorNodeClearanceViolations},"
|
||||
+ $" shared={focusedExpandedSnapLayoutScore.SharedLaneViolations},"
|
||||
+ $" boundary={focusedExpandedSnapLayoutScore.BoundarySlotViolations},"
|
||||
+ $" joins={focusedExpandedSnapLayoutScore.TargetApproachJoinViolations},"
|
||||
+ $" backtracking={focusedExpandedSnapLayoutScore.TargetApproachBacktrackingViolations},"
|
||||
+ $" detour={focusedExpandedSnapLayoutScore.ExcessiveDetourViolations},"
|
||||
+ $" below={focusedExpandedSnapLayoutScore.BelowGraphViolations},"
|
||||
+ $" under={focusedExpandedSnapLayoutScore.UnderNodeViolations},"
|
||||
+ $" longdiag={focusedExpandedSnapLayoutScore.LongDiagonalViolations},"
|
||||
+ $" proximity={focusedExpandedSnapLayoutScore.ProximityViolations},"
|
||||
+ $" label={focusedExpandedSnapLayoutScore.LabelProximityViolations},"
|
||||
+ $" crossings={focusedExpandedSnapLayoutScore.EdgeCrossings},"
|
||||
+ $" value={focusedExpandedSnapLayoutScore.Value:F0}";
|
||||
edgeSpecificDebug += $" | focused-expanded-snap-compare retry={compareRetryStates.Invoke(null, new object?[] { focusedExpandedSnapLayoutRetryState, baselineRetryState })},"
|
||||
+ $" hard={(bool)hasHardRuleRegression.Invoke(null, new object?[] { focusedExpandedSnapLayoutRetryState, baselineRetryState })!}";
|
||||
edgeSpecificDebug += $" | focused-expanded-snap-backtracking-edges=[{string.Join(", ", focusedExpandedSnapBacktrackingSeverity.Keys.OrderBy(id => id, StringComparer.Ordinal))}]";
|
||||
edgeSpecificDebug += $" | focused-expanded-snap-backtracking-repair-chooser={(ReferenceEquals(chosenFocusedExpandedSnapBacktrackingRepairLayout, focusedExpandedSnapBacktrackingRepairLayout) ? "candidate" : "baseline")}";
|
||||
edgeSpecificDebug += $" | focused-expanded-snap-backtracking-repair-score entry={focusedExpandedSnapBacktrackingRepairLayoutScore.EntryAngleViolations},"
|
||||
+ $" nc={focusedExpandedSnapBacktrackingRepairLayoutScore.NodeCrossings},"
|
||||
+ $" short={focusedExpandedSnapBacktrackingRepairBrokenHighways},"
|
||||
+ $" gateway-source={focusedExpandedSnapBacktrackingRepairLayoutScore.GatewaySourceExitViolations},"
|
||||
+ $" collector-corridor={focusedExpandedSnapBacktrackingRepairLayoutScore.RepeatCollectorCorridorViolations},"
|
||||
+ $" collector-clearance={focusedExpandedSnapBacktrackingRepairLayoutScore.RepeatCollectorNodeClearanceViolations},"
|
||||
+ $" shared={focusedExpandedSnapBacktrackingRepairLayoutScore.SharedLaneViolations},"
|
||||
+ $" boundary={focusedExpandedSnapBacktrackingRepairLayoutScore.BoundarySlotViolations},"
|
||||
+ $" joins={focusedExpandedSnapBacktrackingRepairLayoutScore.TargetApproachJoinViolations},"
|
||||
+ $" backtracking={focusedExpandedSnapBacktrackingRepairLayoutScore.TargetApproachBacktrackingViolations},"
|
||||
+ $" detour={focusedExpandedSnapBacktrackingRepairLayoutScore.ExcessiveDetourViolations},"
|
||||
+ $" below={focusedExpandedSnapBacktrackingRepairLayoutScore.BelowGraphViolations},"
|
||||
+ $" under={focusedExpandedSnapBacktrackingRepairLayoutScore.UnderNodeViolations},"
|
||||
+ $" longdiag={focusedExpandedSnapBacktrackingRepairLayoutScore.LongDiagonalViolations},"
|
||||
+ $" proximity={focusedExpandedSnapBacktrackingRepairLayoutScore.ProximityViolations},"
|
||||
+ $" label={focusedExpandedSnapBacktrackingRepairLayoutScore.LabelProximityViolations},"
|
||||
+ $" crossings={focusedExpandedSnapBacktrackingRepairLayoutScore.EdgeCrossings},"
|
||||
+ $" value={focusedExpandedSnapBacktrackingRepairLayoutScore.Value:F0}";
|
||||
edgeSpecificDebug += $" | focused-expanded-snap-backtracking-repair-compare retry={compareRetryStates.Invoke(null, new object?[] { focusedExpandedSnapBacktrackingRepairLayoutRetryState, baselineRetryState })},"
|
||||
+ $" hard={(bool)hasHardRuleRegression.Invoke(null, new object?[] { focusedExpandedSnapBacktrackingRepairLayoutRetryState, baselineRetryState })!}";
|
||||
edgeSpecificDebug += $" | focused-expanded-snap-backtracking-repair-edges=[{string.Join(", ", focusedExpandedSnapBacktrackingRepairSeverity.Keys.OrderBy(id => id, StringComparer.Ordinal))}]";
|
||||
edgeSpecificDebug += $" | focused-expanded-snap-new-offender-revert-chooser={(ReferenceEquals(chosenFocusedExpandedSnapNewOffenderRevertLayout, focusedExpandedSnapNewOffenderRevertLayout) ? "candidate" : "baseline")}";
|
||||
edgeSpecificDebug += $" | focused-expanded-snap-new-offender-revert-score entry={focusedExpandedSnapNewOffenderRevertLayoutScore.EntryAngleViolations},"
|
||||
+ $" nc={focusedExpandedSnapNewOffenderRevertLayoutScore.NodeCrossings},"
|
||||
+ $" short={focusedExpandedSnapNewOffenderRevertBrokenHighways},"
|
||||
+ $" gateway-source={focusedExpandedSnapNewOffenderRevertLayoutScore.GatewaySourceExitViolations},"
|
||||
+ $" collector-corridor={focusedExpandedSnapNewOffenderRevertLayoutScore.RepeatCollectorCorridorViolations},"
|
||||
+ $" collector-clearance={focusedExpandedSnapNewOffenderRevertLayoutScore.RepeatCollectorNodeClearanceViolations},"
|
||||
+ $" shared={focusedExpandedSnapNewOffenderRevertLayoutScore.SharedLaneViolations},"
|
||||
+ $" boundary={focusedExpandedSnapNewOffenderRevertLayoutScore.BoundarySlotViolations},"
|
||||
+ $" joins={focusedExpandedSnapNewOffenderRevertLayoutScore.TargetApproachJoinViolations},"
|
||||
+ $" backtracking={focusedExpandedSnapNewOffenderRevertLayoutScore.TargetApproachBacktrackingViolations},"
|
||||
+ $" detour={focusedExpandedSnapNewOffenderRevertLayoutScore.ExcessiveDetourViolations},"
|
||||
+ $" below={focusedExpandedSnapNewOffenderRevertLayoutScore.BelowGraphViolations},"
|
||||
+ $" under={focusedExpandedSnapNewOffenderRevertLayoutScore.UnderNodeViolations},"
|
||||
+ $" longdiag={focusedExpandedSnapNewOffenderRevertLayoutScore.LongDiagonalViolations},"
|
||||
+ $" proximity={focusedExpandedSnapNewOffenderRevertLayoutScore.ProximityViolations},"
|
||||
+ $" label={focusedExpandedSnapNewOffenderRevertLayoutScore.LabelProximityViolations},"
|
||||
+ $" crossings={focusedExpandedSnapNewOffenderRevertLayoutScore.EdgeCrossings},"
|
||||
+ $" value={focusedExpandedSnapNewOffenderRevertLayoutScore.Value:F0}";
|
||||
edgeSpecificDebug += $" | focused-expanded-snap-new-offender-revert-compare retry={compareRetryStates.Invoke(null, new object?[] { focusedExpandedSnapNewOffenderRevertLayoutRetryState, baselineRetryState })},"
|
||||
+ $" hard={(bool)hasHardRuleRegression.Invoke(null, new object?[] { focusedExpandedSnapNewOffenderRevertLayoutRetryState, baselineRetryState })!}";
|
||||
edgeSpecificDebug += $" | focused-expanded-snap-new-offender-edges=[{string.Join(", ", focusedExpandedSnapNewOffenderEdgeIds)}]";
|
||||
edgeSpecificDebug += $" | focused-expanded-snap-new-offender-revert-backtracking-edges=[{string.Join(", ", focusedExpandedSnapNewOffenderRevertSeverity.Keys.OrderBy(id => id, StringComparer.Ordinal))}]";
|
||||
edgeSpecificDebug += $" | expanded-transactional-focus=[{string.Join(", ", expandedFocusEdgeIds)}]";
|
||||
edgeSpecificDebug += $" | expanded-transactional-chooser={(ReferenceEquals(chosenExpandedTransactionalLayout, expandedTransactionalLayoutRepair) ? "candidate" : "baseline")}";
|
||||
edgeSpecificDebug += $" | expanded-transactional-score entry={expandedTransactionalLayoutScore.EntryAngleViolations},"
|
||||
+ $" nc={expandedTransactionalLayoutScore.NodeCrossings},"
|
||||
+ $" short={expandedTransactionalBrokenHighways},"
|
||||
+ $" gateway-source={expandedTransactionalLayoutScore.GatewaySourceExitViolations},"
|
||||
+ $" collector-corridor={expandedTransactionalLayoutScore.RepeatCollectorCorridorViolations},"
|
||||
+ $" collector-clearance={expandedTransactionalLayoutScore.RepeatCollectorNodeClearanceViolations},"
|
||||
+ $" shared={expandedTransactionalLayoutScore.SharedLaneViolations},"
|
||||
+ $" boundary={expandedTransactionalLayoutScore.BoundarySlotViolations},"
|
||||
+ $" joins={expandedTransactionalLayoutScore.TargetApproachJoinViolations},"
|
||||
+ $" backtracking={expandedTransactionalLayoutScore.TargetApproachBacktrackingViolations},"
|
||||
+ $" detour={expandedTransactionalLayoutScore.ExcessiveDetourViolations},"
|
||||
+ $" below={expandedTransactionalLayoutScore.BelowGraphViolations},"
|
||||
+ $" under={expandedTransactionalLayoutScore.UnderNodeViolations},"
|
||||
+ $" longdiag={expandedTransactionalLayoutScore.LongDiagonalViolations},"
|
||||
+ $" proximity={expandedTransactionalLayoutScore.ProximityViolations},"
|
||||
+ $" label={expandedTransactionalLayoutScore.LabelProximityViolations},"
|
||||
+ $" crossings={expandedTransactionalLayoutScore.EdgeCrossings},"
|
||||
+ $" value={expandedTransactionalLayoutScore.Value:F0}";
|
||||
edgeSpecificDebug += $" | expanded-transactional-compare retry={compareRetryStates.Invoke(null, new object?[] { expandedTransactionalLayoutRetryState, baselineRetryState })},"
|
||||
+ $" hard={(bool)hasHardRuleRegression.Invoke(null, new object?[] { expandedTransactionalLayoutRetryState, baselineRetryState })!}";
|
||||
edgeSpecificDebug += $" | expanded-transactional-backtracking-edges=[{string.Join(", ", expandedTransactionalBacktrackingSeverity.Keys.OrderBy(id => id, StringComparer.Ordinal))}]";
|
||||
edgeSpecificDebug += $" | focused-expanded-restabilized-chooser={(ReferenceEquals(chosenFocusedExpandedRestabilizedLayout, focusedExpandedRestabilizedLayoutRepair) ? "candidate" : "baseline")}";
|
||||
edgeSpecificDebug += $" | focused-expanded-restabilized-score entry={focusedExpandedRestabilizedLayoutScore.EntryAngleViolations},"
|
||||
+ $" nc={focusedExpandedRestabilizedLayoutScore.NodeCrossings},"
|
||||
+ $" short={focusedExpandedRestabilizedBrokenHighways},"
|
||||
+ $" gateway-source={focusedExpandedRestabilizedLayoutScore.GatewaySourceExitViolations},"
|
||||
+ $" collector-corridor={focusedExpandedRestabilizedLayoutScore.RepeatCollectorCorridorViolations},"
|
||||
+ $" collector-clearance={focusedExpandedRestabilizedLayoutScore.RepeatCollectorNodeClearanceViolations},"
|
||||
+ $" shared={focusedExpandedRestabilizedLayoutScore.SharedLaneViolations},"
|
||||
+ $" boundary={focusedExpandedRestabilizedLayoutScore.BoundarySlotViolations},"
|
||||
+ $" joins={focusedExpandedRestabilizedLayoutScore.TargetApproachJoinViolations},"
|
||||
+ $" backtracking={focusedExpandedRestabilizedLayoutScore.TargetApproachBacktrackingViolations},"
|
||||
+ $" detour={focusedExpandedRestabilizedLayoutScore.ExcessiveDetourViolations},"
|
||||
+ $" below={focusedExpandedRestabilizedLayoutScore.BelowGraphViolations},"
|
||||
+ $" under={focusedExpandedRestabilizedLayoutScore.UnderNodeViolations},"
|
||||
+ $" longdiag={focusedExpandedRestabilizedLayoutScore.LongDiagonalViolations},"
|
||||
+ $" proximity={focusedExpandedRestabilizedLayoutScore.ProximityViolations},"
|
||||
+ $" label={focusedExpandedRestabilizedLayoutScore.LabelProximityViolations},"
|
||||
+ $" crossings={focusedExpandedRestabilizedLayoutScore.EdgeCrossings},"
|
||||
+ $" value={focusedExpandedRestabilizedLayoutScore.Value:F0}";
|
||||
edgeSpecificDebug += $" | focused-expanded-restabilized-compare retry={compareRetryStates.Invoke(null, new object?[] { focusedExpandedRestabilizedLayoutRetryState, baselineRetryState })},"
|
||||
+ $" hard={(bool)hasHardRuleRegression.Invoke(null, new object?[] { focusedExpandedRestabilizedLayoutRetryState, baselineRetryState })!}";
|
||||
edgeSpecificDebug += $" | focused-expanded-restabilized-backtracking-edges=[{string.Join(", ", focusedExpandedRestabilizedBacktrackingSeverity.Keys.OrderBy(id => id, StringComparer.Ordinal))}]";
|
||||
edgeSpecificDebug += $" | focused-expanded-restabilized-join-edges=[{string.Join(", ", focusedExpandedRestabilizedJoinSeverity.Keys.OrderBy(id => id, StringComparer.Ordinal))}]";
|
||||
edgeSpecificDebug += $" | paired-transactional-chooser={(ReferenceEquals(chosenPairedTransactionalLayout, pairedTransactionalLayoutRepair) ? "candidate" : "baseline")}";
|
||||
edgeSpecificDebug += $" | paired-transactional-score entry={pairedTransactionalLayoutScore.EntryAngleViolations},"
|
||||
+ $" nc={pairedTransactionalLayoutScore.NodeCrossings},"
|
||||
+ $" short={pairedTransactionalBrokenHighways},"
|
||||
+ $" gateway-source={pairedTransactionalLayoutScore.GatewaySourceExitViolations},"
|
||||
+ $" collector-corridor={pairedTransactionalLayoutScore.RepeatCollectorCorridorViolations},"
|
||||
+ $" collector-clearance={pairedTransactionalLayoutScore.RepeatCollectorNodeClearanceViolations},"
|
||||
+ $" shared={pairedTransactionalLayoutScore.SharedLaneViolations},"
|
||||
+ $" boundary={pairedTransactionalLayoutScore.BoundarySlotViolations},"
|
||||
+ $" joins={pairedTransactionalLayoutScore.TargetApproachJoinViolations},"
|
||||
+ $" backtracking={pairedTransactionalLayoutScore.TargetApproachBacktrackingViolations},"
|
||||
+ $" detour={pairedTransactionalLayoutScore.ExcessiveDetourViolations},"
|
||||
@@ -1374,7 +1912,10 @@ public partial class ElkSharpEdgeRefinementTests
|
||||
+ $" longdiag={pairedTransactionalLayoutScore.LongDiagonalViolations},"
|
||||
+ $" proximity={pairedTransactionalLayoutScore.ProximityViolations},"
|
||||
+ $" label={pairedTransactionalLayoutScore.LabelProximityViolations},"
|
||||
+ $" crossings={pairedTransactionalLayoutScore.EdgeCrossings}";
|
||||
+ $" crossings={pairedTransactionalLayoutScore.EdgeCrossings},"
|
||||
+ $" value={pairedTransactionalLayoutScore.Value:F0}";
|
||||
edgeSpecificDebug += $" | paired-transactional-compare retry={compareRetryStates.Invoke(null, new object?[] { pairedTransactionalLayoutRetryState, baselineRetryState })},"
|
||||
+ $" hard={(bool)hasHardRuleRegression.Invoke(null, new object?[] { pairedTransactionalLayoutRetryState, baselineRetryState })!}";
|
||||
edgeSpecificDebug += $" | peer-conflict-score entry={peerConflictLayoutScore.EntryAngleViolations},"
|
||||
+ $" gateway-source={peerConflictLayoutScore.GatewaySourceExitViolations},"
|
||||
+ $" shared={peerConflictLayoutScore.SharedLaneViolations},"
|
||||
@@ -1389,8 +1930,18 @@ public partial class ElkSharpEdgeRefinementTests
|
||||
+ $" crossings={peerConflictLayoutScore.EdgeCrossings}";
|
||||
edgeSpecificDebug += $" | focused-layout detour={ElkEdgeRoutingScoring.CountExcessiveDetourViolations(focusedLayoutRepair, elkNodes)},"
|
||||
+ $" shared={ElkEdgeRoutingScoring.CountSharedLaneViolations(focusedLayoutRepair, elkNodes)},"
|
||||
+ $" boundary={ElkEdgeRoutingScoring.CountBoundarySlotViolations(focusedLayoutRepair, elkNodes)},"
|
||||
+ $" gateway-source={ElkEdgeRoutingScoring.CountGatewaySourceExitViolations(focusedLayoutRepair, elkNodes)}:"
|
||||
+ $" {DescribePath(focusedLayoutPath)}";
|
||||
edgeSpecificDebug += $" | focused-boundary-candidate short={focusedBoundarySlotBrokenHighways},"
|
||||
+ $" boundary={focusedBoundarySlotScore.BoundarySlotViolations},"
|
||||
+ $" joins={focusedBoundarySlotScore.TargetApproachJoinViolations},"
|
||||
+ $" backtracking={focusedBoundarySlotScore.TargetApproachBacktrackingViolations},"
|
||||
+ $" detour={focusedBoundarySlotScore.ExcessiveDetourViolations},"
|
||||
+ $" under={focusedBoundarySlotScore.UnderNodeViolations},"
|
||||
+ $" proximity={focusedBoundarySlotScore.ProximityViolations},"
|
||||
+ $" label={focusedBoundarySlotScore.LabelProximityViolations},"
|
||||
+ $" crossings={focusedBoundarySlotScore.EdgeCrossings}";
|
||||
edgeSpecificDebug += $" | focused-join-edges=[{string.Join(", ", focusedJoinSeverity.Keys.OrderBy(id => id, StringComparer.Ordinal))}]";
|
||||
edgeSpecificDebug += $" | transactional-layout detour={ElkEdgeRoutingScoring.CountExcessiveDetourViolations(transactionalLayoutRepair, elkNodes)},"
|
||||
+ $" shared={ElkEdgeRoutingScoring.CountSharedLaneViolations(transactionalLayoutRepair, elkNodes)},"
|
||||
@@ -1398,6 +1949,36 @@ public partial class ElkSharpEdgeRefinementTests
|
||||
+ $" under={ElkEdgeRoutingScoring.CountUnderNodeViolations(transactionalLayoutRepair, elkNodes)},"
|
||||
+ $" joins={ElkEdgeRoutingScoring.CountTargetApproachJoinViolations(transactionalLayoutRepair, elkNodes)}:"
|
||||
+ $" {DescribePath(transactionalLayoutPath)}";
|
||||
edgeSpecificDebug += $" | focused-expanded-snap-layout detour={ElkEdgeRoutingScoring.CountExcessiveDetourViolations(focusedExpandedSnapLayoutRepair, elkNodes)},"
|
||||
+ $" backtracking={ElkEdgeRoutingScoring.CountTargetApproachBacktrackingViolations(focusedExpandedSnapLayoutRepair, elkNodes)},"
|
||||
+ $" shared={ElkEdgeRoutingScoring.CountSharedLaneViolations(focusedExpandedSnapLayoutRepair, elkNodes)},"
|
||||
+ $" gateway-source={ElkEdgeRoutingScoring.CountGatewaySourceExitViolations(focusedExpandedSnapLayoutRepair, elkNodes)},"
|
||||
+ $" joins={ElkEdgeRoutingScoring.CountTargetApproachJoinViolations(focusedExpandedSnapLayoutRepair, elkNodes)}:"
|
||||
+ $" {DescribePath(focusedExpandedSnapLayoutPath)}";
|
||||
edgeSpecificDebug += $" | focused-expanded-snap-backtracking-repair-layout detour={ElkEdgeRoutingScoring.CountExcessiveDetourViolations(focusedExpandedSnapBacktrackingRepairLayout, elkNodes)},"
|
||||
+ $" backtracking={ElkEdgeRoutingScoring.CountTargetApproachBacktrackingViolations(focusedExpandedSnapBacktrackingRepairLayout, elkNodes)},"
|
||||
+ $" shared={ElkEdgeRoutingScoring.CountSharedLaneViolations(focusedExpandedSnapBacktrackingRepairLayout, elkNodes)},"
|
||||
+ $" gateway-source={ElkEdgeRoutingScoring.CountGatewaySourceExitViolations(focusedExpandedSnapBacktrackingRepairLayout, elkNodes)},"
|
||||
+ $" joins={ElkEdgeRoutingScoring.CountTargetApproachJoinViolations(focusedExpandedSnapBacktrackingRepairLayout, elkNodes)}:"
|
||||
+ $" {DescribePath(focusedExpandedSnapBacktrackingRepairLayoutPath)}";
|
||||
edgeSpecificDebug += $" | focused-expanded-snap-new-offender-revert-layout detour={ElkEdgeRoutingScoring.CountExcessiveDetourViolations(focusedExpandedSnapNewOffenderRevertLayout, elkNodes)},"
|
||||
+ $" backtracking={ElkEdgeRoutingScoring.CountTargetApproachBacktrackingViolations(focusedExpandedSnapNewOffenderRevertLayout, elkNodes)},"
|
||||
+ $" shared={ElkEdgeRoutingScoring.CountSharedLaneViolations(focusedExpandedSnapNewOffenderRevertLayout, elkNodes)},"
|
||||
+ $" gateway-source={ElkEdgeRoutingScoring.CountGatewaySourceExitViolations(focusedExpandedSnapNewOffenderRevertLayout, elkNodes)},"
|
||||
+ $" joins={ElkEdgeRoutingScoring.CountTargetApproachJoinViolations(focusedExpandedSnapNewOffenderRevertLayout, elkNodes)}:"
|
||||
+ $" {DescribePath(focusedExpandedSnapNewOffenderRevertLayoutPath)}";
|
||||
edgeSpecificDebug += $" | expanded-transactional-layout detour={ElkEdgeRoutingScoring.CountExcessiveDetourViolations(expandedTransactionalLayoutRepair, elkNodes)},"
|
||||
+ $" backtracking={ElkEdgeRoutingScoring.CountTargetApproachBacktrackingViolations(expandedTransactionalLayoutRepair, elkNodes)},"
|
||||
+ $" shared={ElkEdgeRoutingScoring.CountSharedLaneViolations(expandedTransactionalLayoutRepair, elkNodes)},"
|
||||
+ $" gateway-source={ElkEdgeRoutingScoring.CountGatewaySourceExitViolations(expandedTransactionalLayoutRepair, elkNodes)},"
|
||||
+ $" joins={ElkEdgeRoutingScoring.CountTargetApproachJoinViolations(expandedTransactionalLayoutRepair, elkNodes)}:"
|
||||
+ $" {DescribePath(expandedTransactionalLayoutPath)}";
|
||||
edgeSpecificDebug += $" | focused-expanded-restabilized-layout detour={ElkEdgeRoutingScoring.CountExcessiveDetourViolations(focusedExpandedRestabilizedLayoutRepair, elkNodes)},"
|
||||
+ $" backtracking={ElkEdgeRoutingScoring.CountTargetApproachBacktrackingViolations(focusedExpandedRestabilizedLayoutRepair, elkNodes)},"
|
||||
+ $" shared={ElkEdgeRoutingScoring.CountSharedLaneViolations(focusedExpandedRestabilizedLayoutRepair, elkNodes)},"
|
||||
+ $" gateway-source={ElkEdgeRoutingScoring.CountGatewaySourceExitViolations(focusedExpandedRestabilizedLayoutRepair, elkNodes)},"
|
||||
+ $" joins={ElkEdgeRoutingScoring.CountTargetApproachJoinViolations(focusedExpandedRestabilizedLayoutRepair, elkNodes)}:"
|
||||
+ $" {DescribePath(focusedExpandedRestabilizedLayoutPath)}";
|
||||
if (edge.Id == "edge/27")
|
||||
{
|
||||
var pairedTransactionalLayoutSection = pairedTransactionalLayoutRepair.Single(candidate => candidate.Id == edge.Id).Sections.Single();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,903 @@
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
|
||||
using StellaOps.ElkSharp;
|
||||
using StellaOps.Workflow.Abstractions;
|
||||
|
||||
namespace StellaOps.Workflow.Renderer.Tests;
|
||||
|
||||
public partial class ElkSharpEdgeRefinementTests
|
||||
{
|
||||
[Test]
|
||||
[Property("Intent", "Operational")]
|
||||
public void BoundarySlotHelpers_WhenGatewayTargetSingletonTopSlotNeedsSnap_ShouldPreserveOrthogonalFeeder()
|
||||
{
|
||||
var source = new ElkPositionedNode
|
||||
{
|
||||
Id = "source",
|
||||
Label = "Internal Notification",
|
||||
Kind = "Decision",
|
||||
X = 2662,
|
||||
Y = 639.4360656738281,
|
||||
Width = 188,
|
||||
Height = 132,
|
||||
};
|
||||
var target = new ElkPositionedNode
|
||||
{
|
||||
Id = "target",
|
||||
Label = "Has Recipients",
|
||||
Kind = "Decision",
|
||||
X = 3778,
|
||||
Y = 631.352783203125,
|
||||
Width = 188,
|
||||
Height = 132,
|
||||
};
|
||||
var edge = new ElkRoutedEdge
|
||||
{
|
||||
Id = "edge/25",
|
||||
SourceNodeId = source.Id,
|
||||
TargetNodeId = target.Id,
|
||||
Label = "default",
|
||||
Sections =
|
||||
[
|
||||
new ElkEdgeSection
|
||||
{
|
||||
StartPoint = new ElkPoint { X = 2741.2685655585256, Y = 649.7794132603952 },
|
||||
EndPoint = new ElkPoint { X = 3849.7988202419156, Y = 646.940845586461 },
|
||||
BendPoints =
|
||||
[
|
||||
new ElkPoint { X = 2741.2685655585256, Y = 631.4360656738281 },
|
||||
new ElkPoint { X = 3849.7988202419156, Y = 631.4360656738281 },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
var nodes = new[] { source, target };
|
||||
ElkEdgeRoutingScoring.CountBoundarySlotViolations([edge], nodes)
|
||||
.Should().Be(1);
|
||||
|
||||
var repaired = ElkEdgePostProcessor.SnapBoundarySlotAssignments(
|
||||
[edge],
|
||||
nodes,
|
||||
53d,
|
||||
enforceAllNodeEndpoints: true);
|
||||
|
||||
ElkEdgeRoutingScoring.CountBoundarySlotViolations(repaired, nodes)
|
||||
.Should().Be(0);
|
||||
|
||||
var repairedPath = ExtractPath(repaired.Single());
|
||||
repairedPath.Should().HaveCount(4);
|
||||
repairedPath[1].Y.Should().BeApproximately(repairedPath[2].Y, 0.5d);
|
||||
repairedPath[2].X.Should().BeApproximately(repairedPath[3].X, 0.5d);
|
||||
|
||||
var expectedEnd = ElkBoundarySlots.BuildBoundarySlotPoint(
|
||||
target,
|
||||
"top",
|
||||
ElkBoundarySlots.BuildAssignedBoundarySlotCoordinates(target, "top", 1).Single());
|
||||
repairedPath[^1].X.Should().BeApproximately(expectedEnd.X, 0.5d);
|
||||
repairedPath[^1].Y.Should().BeApproximately(expectedEnd.Y, 0.5d);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Property("Intent", "Operational")]
|
||||
public void BoundarySlotHelpers_WhenGatewayTargetSingletonLeftSlotNeedsSnap_ShouldReplaceLongDiagonalWithOrthogonalFeeder()
|
||||
{
|
||||
var source = new ElkPositionedNode
|
||||
{
|
||||
Id = "source",
|
||||
Label = "Execute Batch",
|
||||
Kind = "TransportCall",
|
||||
X = 1604,
|
||||
Y = 320.5908203125,
|
||||
Width = 208,
|
||||
Height = 88,
|
||||
};
|
||||
var target = new ElkPositionedNode
|
||||
{
|
||||
Id = "target",
|
||||
Label = "Retry Decision",
|
||||
Kind = "Decision",
|
||||
X = 1976,
|
||||
Y = 420.5908203125,
|
||||
Width = 188,
|
||||
Height = 132,
|
||||
};
|
||||
var edge = new ElkRoutedEdge
|
||||
{
|
||||
Id = "edge/5",
|
||||
SourceNodeId = source.Id,
|
||||
TargetNodeId = target.Id,
|
||||
Label = "on failure",
|
||||
Sections =
|
||||
[
|
||||
new ElkEdgeSection
|
||||
{
|
||||
StartPoint = new ElkPoint { X = 1812, Y = 386.5908203125 },
|
||||
EndPoint = new ElkPoint { X = 1986.4506603822254, Y = 479.25312259732056 },
|
||||
BendPoints =
|
||||
[
|
||||
new ElkPoint { X = 1860, Y = 386.5908203125 },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
var nodes = new[] { source, target };
|
||||
ElkEdgeRoutingScoring.CountBoundarySlotViolations([edge], nodes)
|
||||
.Should().BeGreaterThan(0);
|
||||
ElkEdgeRoutingScoring.CountLongDiagonalViolations([edge], nodes)
|
||||
.Should().Be(1);
|
||||
|
||||
var repaired = ElkEdgePostProcessor.SnapBoundarySlotAssignments(
|
||||
[edge],
|
||||
nodes,
|
||||
53d,
|
||||
enforceAllNodeEndpoints: true);
|
||||
|
||||
ElkEdgeRoutingScoring.CountBoundarySlotViolations(repaired, nodes)
|
||||
.Should().Be(0);
|
||||
ElkEdgeRoutingScoring.CountLongDiagonalViolations(repaired, nodes)
|
||||
.Should().Be(0);
|
||||
|
||||
var repairedPath = ExtractPath(repaired.Single());
|
||||
repairedPath.Should().HaveCountGreaterOrEqualTo(4);
|
||||
Math.Abs(repairedPath[^2].Y - repairedPath[^1].Y).Should().BeLessThan(0.5d);
|
||||
repairedPath.Zip(repairedPath.Skip(1), (start, end) =>
|
||||
Math.Abs(start.X - end.X) <= 0.5d && Math.Abs(start.Y - end.Y) > 0.5d)
|
||||
.Should()
|
||||
.Contain(isVertical => isVertical);
|
||||
|
||||
var expectedEnd = ElkBoundarySlots.BuildBoundarySlotPoint(
|
||||
target,
|
||||
"left",
|
||||
ElkBoundarySlots.BuildAssignedBoundarySlotCoordinates(target, "left", 1).Single());
|
||||
repairedPath[^1].X.Should().BeApproximately(expectedEnd.X, 0.5d);
|
||||
repairedPath[^1].Y.Should().BeApproximately(expectedEnd.Y, 0.5d);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Property("Intent", "Operational")]
|
||||
public void BoundarySlotHelpers_WhenGatewayTargetSingletonLeftSlotUsesFallbackAxis_ShouldKeepAssignedFace()
|
||||
{
|
||||
var source = new ElkPositionedNode
|
||||
{
|
||||
Id = "source",
|
||||
Label = "Execute Batch",
|
||||
Kind = "TransportCall",
|
||||
X = 1604,
|
||||
Y = 320.5908203125,
|
||||
Width = 208,
|
||||
Height = 88,
|
||||
};
|
||||
var target = new ElkPositionedNode
|
||||
{
|
||||
Id = "target",
|
||||
Label = "Retry Decision",
|
||||
Kind = "Decision",
|
||||
X = 1976,
|
||||
Y = 420.5908203125,
|
||||
Width = 188,
|
||||
Height = 132,
|
||||
};
|
||||
var edge = new ElkRoutedEdge
|
||||
{
|
||||
Id = "edge/5",
|
||||
SourceNodeId = source.Id,
|
||||
TargetNodeId = target.Id,
|
||||
Label = "on failure",
|
||||
Sections =
|
||||
[
|
||||
new ElkEdgeSection
|
||||
{
|
||||
StartPoint = new ElkPoint { X = 1812, Y = 386.5908203125 },
|
||||
EndPoint = new ElkPoint { X = 1986.4506603822254, Y = 479.25312259732056 },
|
||||
BendPoints =
|
||||
[
|
||||
new ElkPoint { X = 1860, Y = 386.5908203125 },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
var path = ExtractPath(edge);
|
||||
var desiredEndpoint = ElkBoundarySlots.BuildBoundarySlotPoint(
|
||||
target,
|
||||
"left",
|
||||
ElkBoundarySlots.BuildAssignedBoundarySlotCoordinates(target, "left", 1).Single());
|
||||
var postProcessorType = typeof(ElkEdgePostProcessor);
|
||||
var resolveTargetApproachAxisValue = postProcessorType.GetMethod(
|
||||
"ResolveTargetApproachAxisValue",
|
||||
BindingFlags.NonPublic | BindingFlags.Static);
|
||||
var buildTargetApproachCandidatePath = postProcessorType.GetMethod(
|
||||
"BuildTargetApproachCandidatePath",
|
||||
BindingFlags.NonPublic | BindingFlags.Static);
|
||||
var canAcceptGatewayTargetRepair = postProcessorType.GetMethod(
|
||||
"CanAcceptGatewayTargetRepair",
|
||||
BindingFlags.NonPublic | BindingFlags.Static);
|
||||
var resolveTargetApproachSide = postProcessorType.GetMethod(
|
||||
"ResolveTargetApproachSide",
|
||||
BindingFlags.NonPublic | BindingFlags.Static);
|
||||
|
||||
resolveTargetApproachAxisValue.Should().NotBeNull();
|
||||
buildTargetApproachCandidatePath.Should().NotBeNull();
|
||||
canAcceptGatewayTargetRepair.Should().NotBeNull();
|
||||
resolveTargetApproachSide.Should().NotBeNull();
|
||||
|
||||
var targetAxis = (double)resolveTargetApproachAxisValue!.Invoke(null, [path, "left"])!;
|
||||
var candidate = (List<ElkPoint>)buildTargetApproachCandidatePath!.Invoke(
|
||||
null,
|
||||
[path, target, "left", desiredEndpoint, targetAxis])!;
|
||||
var candidateEdge = new ElkRoutedEdge
|
||||
{
|
||||
Id = edge.Id,
|
||||
SourceNodeId = edge.SourceNodeId,
|
||||
TargetNodeId = edge.TargetNodeId,
|
||||
Label = edge.Label,
|
||||
Sections =
|
||||
[
|
||||
new ElkEdgeSection
|
||||
{
|
||||
StartPoint = new ElkPoint { X = candidate[0].X, Y = candidate[0].Y },
|
||||
EndPoint = new ElkPoint { X = candidate[^1].X, Y = candidate[^1].Y },
|
||||
BendPoints = candidate.Skip(1).SkipLast(1)
|
||||
.Select(point => new ElkPoint { X = point.X, Y = point.Y })
|
||||
.ToArray(),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
resolveTargetApproachSide.Invoke(null, [candidate, target]).Should().Be("left");
|
||||
canAcceptGatewayTargetRepair.Invoke(null, [candidate, target]).Should().Be(true);
|
||||
ElkEdgeRoutingScoring.CountBoundarySlotViolations([candidateEdge], [source, target]).Should().Be(0);
|
||||
ElkEdgeRoutingScoring.CountLongDiagonalViolations([candidateEdge], [source, target]).Should().Be(0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Property("Intent", "Operational")]
|
||||
public void BoundarySlotHelpers_WhenLateSlotSnapTouchesMixedNodeFaceHandoff_ShouldKeepDistinctFaceSlots()
|
||||
{
|
||||
var process = new ElkPositionedNode
|
||||
{
|
||||
Id = "process",
|
||||
Label = "Process Batch",
|
||||
Kind = "Repeat",
|
||||
X = 992,
|
||||
Y = 247.181640625,
|
||||
Width = 208,
|
||||
Height = 88,
|
||||
};
|
||||
|
||||
var validateSuccess = new ElkPositionedNode
|
||||
{
|
||||
Id = "validate",
|
||||
Label = "Validate Success",
|
||||
Kind = "Decision",
|
||||
X = 3406,
|
||||
Y = 225.181640625,
|
||||
Width = 188,
|
||||
Height = 132,
|
||||
};
|
||||
|
||||
var join = new ElkPositionedNode
|
||||
{
|
||||
Id = "join",
|
||||
Label = "Parallel Execution Join",
|
||||
Kind = "Join",
|
||||
X = 1290,
|
||||
Y = 116.5908203125,
|
||||
Width = 176,
|
||||
Height = 124,
|
||||
};
|
||||
|
||||
var incoming = new ElkRoutedEdge
|
||||
{
|
||||
Id = "edge/in",
|
||||
SourceNodeId = validateSuccess.Id,
|
||||
TargetNodeId = process.Id,
|
||||
Label = "repeat while state.printInsisAttempt eq 0",
|
||||
Sections =
|
||||
[
|
||||
new ElkEdgeSection
|
||||
{
|
||||
StartPoint = new ElkPoint { X = 3420.7314344414744, Y = 301.5249882115671 },
|
||||
EndPoint = new ElkPoint { X = 1200, Y = 269.181640625 },
|
||||
BendPoints =
|
||||
[
|
||||
new ElkPoint { X = 3398, Y = 301.5249882115671 },
|
||||
new ElkPoint { X = 3398, Y = -87.75 },
|
||||
new ElkPoint { X = 1224, Y = -87.75 },
|
||||
new ElkPoint { X = 1224, Y = 269.181640625 },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
var outgoing = new ElkRoutedEdge
|
||||
{
|
||||
Id = "edge/out",
|
||||
SourceNodeId = process.Id,
|
||||
TargetNodeId = join.Id,
|
||||
Sections =
|
||||
[
|
||||
new ElkEdgeSection
|
||||
{
|
||||
StartPoint = new ElkPoint { X = 1200, Y = 269.181640625 },
|
||||
EndPoint = new ElkPoint { X = 1305.1127839415904, Y = 215.68583544185816 },
|
||||
BendPoints =
|
||||
[
|
||||
new ElkPoint { X = 1282.5912611218437, Y = 269.181640625 },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
var body = new ElkRoutedEdge
|
||||
{
|
||||
Id = "edge/body",
|
||||
SourceNodeId = process.Id,
|
||||
TargetNodeId = "body",
|
||||
Label = "body",
|
||||
Sections =
|
||||
[
|
||||
new ElkEdgeSection
|
||||
{
|
||||
StartPoint = new ElkPoint { X = 1200, Y = 291.181640625 },
|
||||
EndPoint = new ElkPoint { X = 1290, Y = 347.61603420489007 },
|
||||
BendPoints =
|
||||
[
|
||||
new ElkPoint { X = 1224, Y = 291.181640625 },
|
||||
new ElkPoint { X = 1224, Y = 347.61603420489007 },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
var bodyNode = new ElkPositionedNode
|
||||
{
|
||||
Id = "body",
|
||||
Label = "Body",
|
||||
Kind = "SetState",
|
||||
X = 1290,
|
||||
Y = 312.5908203125,
|
||||
Width = 224,
|
||||
Height = 104,
|
||||
};
|
||||
|
||||
var nodes = new[] { process, validateSuccess, join, bodyNode };
|
||||
ElkEdgeRoutingScoring.CountSharedLaneViolations([incoming, outgoing, body], nodes)
|
||||
.Should().Be(1);
|
||||
ElkEdgeRoutingScoring.CountBoundarySlotViolations([incoming, outgoing, body], nodes)
|
||||
.Should().BeGreaterThan(0);
|
||||
|
||||
var repaired = ElkEdgePostProcessor.SeparateMixedNodeFaceLaneConflicts([incoming, outgoing, body], nodes, 53d);
|
||||
repaired = ElkEdgePostProcessor.NormalizeBoundaryAngles(repaired, nodes);
|
||||
repaired = ElkEdgePostProcessor.NormalizeSourceExitAngles(repaired, nodes);
|
||||
repaired = ElkEdgePostProcessor.SnapBoundarySlotAssignments(repaired, nodes, 53d);
|
||||
ElkEdgeRoutingScoring.CountSharedLaneViolations(repaired, nodes)
|
||||
.Should().Be(0);
|
||||
ElkEdgeRoutingScoring.CountBoundarySlotViolations(repaired, nodes)
|
||||
.Should().Be(0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Property("Intent", "Operational")]
|
||||
public void MixedNodeFaceHelpers_WhenMixedTopFaceEntriesAreOffLatticeWithoutLaneConflict_ShouldSnapToDiscreteSlots()
|
||||
{
|
||||
var process = new ElkPositionedNode
|
||||
{
|
||||
Id = "process",
|
||||
Label = "Process Batch",
|
||||
Kind = "Repeat",
|
||||
X = 992,
|
||||
Y = 247.181640625,
|
||||
Width = 208,
|
||||
Height = 88,
|
||||
};
|
||||
|
||||
var upstream = new ElkPositionedNode
|
||||
{
|
||||
Id = "upstream",
|
||||
Label = "Upstream",
|
||||
Kind = "Task",
|
||||
X = 1012,
|
||||
Y = 56,
|
||||
Width = 160,
|
||||
Height = 88,
|
||||
};
|
||||
|
||||
var join = new ElkPositionedNode
|
||||
{
|
||||
Id = "join",
|
||||
Label = "Parallel Execution Join",
|
||||
Kind = "Join",
|
||||
X = 1290,
|
||||
Y = 116.5908203125,
|
||||
Width = 176,
|
||||
Height = 124,
|
||||
};
|
||||
|
||||
var incoming = new ElkRoutedEdge
|
||||
{
|
||||
Id = "edge/in-top",
|
||||
SourceNodeId = upstream.Id,
|
||||
TargetNodeId = process.Id,
|
||||
Sections =
|
||||
[
|
||||
new ElkEdgeSection
|
||||
{
|
||||
StartPoint = new ElkPoint { X = 1040, Y = 144 },
|
||||
EndPoint = new ElkPoint { X = 1040, Y = 247.181640625 },
|
||||
BendPoints =
|
||||
[
|
||||
new ElkPoint { X = 1040, Y = 188 },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
var outgoing = new ElkRoutedEdge
|
||||
{
|
||||
Id = "edge/out-top",
|
||||
SourceNodeId = process.Id,
|
||||
TargetNodeId = join.Id,
|
||||
Sections =
|
||||
[
|
||||
new ElkEdgeSection
|
||||
{
|
||||
StartPoint = new ElkPoint { X = 1140, Y = 247.181640625 },
|
||||
EndPoint = new ElkPoint { X = 1290, Y = 222.88924788024843 },
|
||||
BendPoints =
|
||||
[
|
||||
new ElkPoint { X = 1140, Y = 180 },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
var nodes = new[] { process, upstream, join };
|
||||
ElkEdgeRoutingScoring.CountSharedLaneViolations([incoming, outgoing], nodes)
|
||||
.Should().Be(0);
|
||||
ElkEdgeRoutingScoring.CountBoundarySlotViolations([incoming, outgoing], nodes)
|
||||
.Should().BeGreaterThan(0);
|
||||
|
||||
var repaired = ElkEdgePostProcessor.SeparateMixedNodeFaceLaneConflicts([incoming, outgoing], nodes, 53d);
|
||||
repaired = ElkEdgePostProcessor.NormalizeBoundaryAngles(repaired, nodes);
|
||||
repaired = ElkEdgePostProcessor.NormalizeSourceExitAngles(repaired, nodes);
|
||||
|
||||
ElkEdgeRoutingScoring.CountSharedLaneViolations(repaired, nodes)
|
||||
.Should().Be(0);
|
||||
ElkEdgeRoutingScoring.CountBoundarySlotViolations(repaired, nodes)
|
||||
.Should().Be(0);
|
||||
|
||||
var repairedIncoming = repaired.Single(edge => edge.Id == "edge/in-top").Sections.Single();
|
||||
var repairedOutgoing = repaired.Single(edge => edge.Id == "edge/out-top").Sections.Single();
|
||||
repairedIncoming.EndPoint.X.Should().NotBe(1040d);
|
||||
repairedOutgoing.StartPoint.X.Should().NotBe(1140d);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Property("Intent", "Operational")]
|
||||
public void BoundarySlotHelpers_WhenDecisionSourceSlotsNeedLateRestabilization_ShouldRepairGatewayExitsAndDefaultDetours()
|
||||
{
|
||||
var evaluateConditions = new ElkPositionedNode
|
||||
{
|
||||
Id = "start/9",
|
||||
Label = "Evaluate Conditions",
|
||||
Kind = "Decision",
|
||||
X = 2290,
|
||||
Y = 32.25,
|
||||
Width = 188,
|
||||
Height = 132,
|
||||
};
|
||||
|
||||
var internalNotification = new ElkPositionedNode
|
||||
{
|
||||
Id = "start/9/true/1",
|
||||
Label = "Internal Notification",
|
||||
Kind = "Decision",
|
||||
X = 2662,
|
||||
Y = 639.4360656738281,
|
||||
Width = 188,
|
||||
Height = 132,
|
||||
};
|
||||
|
||||
var hasRecipients = new ElkPositionedNode
|
||||
{
|
||||
Id = "start/9/true/2",
|
||||
Label = "Has Recipients",
|
||||
Kind = "Decision",
|
||||
X = 3778,
|
||||
Y = 631.352783203125,
|
||||
Width = 188,
|
||||
Height = 132,
|
||||
};
|
||||
|
||||
var end = new ElkPositionedNode
|
||||
{
|
||||
Id = "end",
|
||||
Label = "End",
|
||||
Kind = "End",
|
||||
X = 4864,
|
||||
Y = 331.8013916015625,
|
||||
Width = 264,
|
||||
Height = 132,
|
||||
};
|
||||
|
||||
var toInternalNotification = new ElkRoutedEdge
|
||||
{
|
||||
Id = "edge/22",
|
||||
SourceNodeId = evaluateConditions.Id,
|
||||
TargetNodeId = internalNotification.Id,
|
||||
Label = "when state.notificationHasBody",
|
||||
Sections =
|
||||
[
|
||||
new ElkEdgeSection
|
||||
{
|
||||
StartPoint = new ElkPoint { X = 2416.700665574371, Y = 141.28995821373923 },
|
||||
EndPoint = new ElkPoint { X = 2726, Y = 660.4998954610621 },
|
||||
BendPoints =
|
||||
[
|
||||
new ElkPoint { X = 2437.418589349298, Y = 170.79730419621083 },
|
||||
new ElkPoint { X = 2573.636363636364, Y = 170.79730419621083 },
|
||||
new ElkPoint { X = 2573.636363636364, Y = 660.4998954610621 },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
var defaultToEnd = new ElkRoutedEdge
|
||||
{
|
||||
Id = "edge/23",
|
||||
SourceNodeId = evaluateConditions.Id,
|
||||
TargetNodeId = end.Id,
|
||||
Label = "default",
|
||||
Sections =
|
||||
[
|
||||
new ElkEdgeSection
|
||||
{
|
||||
StartPoint = new ElkPoint { X = 2466.977224619808, Y = 105.98939547970912 },
|
||||
EndPoint = new ElkPoint { X = 4888, Y = 331.8013916015625 },
|
||||
BendPoints =
|
||||
[
|
||||
new ElkPoint { X = 4888, Y = 105.98939547970912 },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
var defaultToRecipients = new ElkRoutedEdge
|
||||
{
|
||||
Id = "edge/25",
|
||||
SourceNodeId = internalNotification.Id,
|
||||
TargetNodeId = hasRecipients.Id,
|
||||
Label = "default",
|
||||
Sections =
|
||||
[
|
||||
new ElkEdgeSection
|
||||
{
|
||||
StartPoint = new ElkPoint { X = 2838.4874461780896, Y = 697.352783203125 },
|
||||
EndPoint = new ElkPoint { X = 3849.7988202419156, Y = 646.940845586461 },
|
||||
BendPoints =
|
||||
[
|
||||
new ElkPoint { X = 2838.4874461780896, Y = 631.4360656738281 },
|
||||
new ElkPoint { X = 3849.7988202419156, Y = 631.4360656738281 },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
var recipientsDefaultToEnd = new ElkRoutedEdge
|
||||
{
|
||||
Id = "edge/30",
|
||||
SourceNodeId = hasRecipients.Id,
|
||||
TargetNodeId = end.Id,
|
||||
Label = "default",
|
||||
Sections =
|
||||
[
|
||||
new ElkEdgeSection
|
||||
{
|
||||
StartPoint = new ElkPoint { X = 3936.565656565657, Y = 718.0194498697916 },
|
||||
EndPoint = new ElkPoint { X = 4864, Y = 355.8013916015625 },
|
||||
BendPoints =
|
||||
[
|
||||
new ElkPoint { X = 3974, Y = 718.0194498697916 },
|
||||
new ElkPoint { X = 3974, Y = 625.039776972473 },
|
||||
new ElkPoint { X = 4840, Y = 625.039776972473 },
|
||||
new ElkPoint { X = 4840, Y = 355.8013916015625 },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
var nodes = new[] { evaluateConditions, internalNotification, hasRecipients, end };
|
||||
var edges = new[] { toInternalNotification, defaultToEnd, defaultToRecipients, recipientsDefaultToEnd };
|
||||
var initialBoundarySlots = ElkEdgeRoutingScoring.CountBoundarySlotViolations(edges, nodes);
|
||||
var initialGatewaySource = ElkEdgeRoutingScoring.CountGatewaySourceExitViolations(edges, nodes);
|
||||
var initialEntry = ElkEdgeRoutingScoring.CountBadBoundaryAngles(edges, nodes);
|
||||
var initialDetour = ElkEdgeRoutingScoring.CountExcessiveDetourViolations(edges, nodes);
|
||||
initialBoundarySlots.Should().BeGreaterThan(0);
|
||||
initialGatewaySource.Should().BeGreaterThan(0);
|
||||
initialDetour.Should().BeGreaterThanOrEqualTo(0);
|
||||
|
||||
var repaired = ElkEdgeRouterIterative.BuildFinalBoundarySlotCandidate(
|
||||
edges,
|
||||
nodes,
|
||||
ElkLayoutDirection.LeftToRight,
|
||||
53d,
|
||||
["edge/22", "edge/23", "edge/25", "edge/30"]);
|
||||
var repairedGatewaySource = ElkEdgeRoutingScoring.CountGatewaySourceExitViolations(repaired, nodes);
|
||||
var repairedRecipientsDefaultToEnd = repaired.Single(edge => edge.Id == "edge/30");
|
||||
var repairedRecipientsDefaultToEndPath = ExtractPath(repairedRecipientsDefaultToEnd);
|
||||
var hasScoringCandidate = ElkEdgePostProcessor.TryBuildGatewaySourceScoringCandidate(
|
||||
repairedRecipientsDefaultToEndPath,
|
||||
hasRecipients,
|
||||
nodes,
|
||||
repairedRecipientsDefaultToEnd.SourceNodeId,
|
||||
repairedRecipientsDefaultToEnd.TargetNodeId,
|
||||
out var repairedRecipientsDefaultToEndScoringCandidate);
|
||||
var scoringCandidateText = hasScoringCandidate
|
||||
? string.Join(" -> ", repairedRecipientsDefaultToEndScoringCandidate.Select(point => $"({point.X:0.###},{point.Y:0.###})"))
|
||||
: "<none>";
|
||||
if (repairedGatewaySource > 0)
|
||||
{
|
||||
foreach (var edge in repaired)
|
||||
{
|
||||
var path = ExtractPath(edge);
|
||||
TestContext.WriteLine($"{edge.Id}: {string.Join(" -> ", path.Select(point => $"({point.X:0.###},{point.Y:0.###})"))}");
|
||||
}
|
||||
}
|
||||
|
||||
ElkEdgeRoutingScoring.CountBoundarySlotViolations(repaired, nodes)
|
||||
.Should().Be(0);
|
||||
repairedGatewaySource.Should().Be(0);
|
||||
ElkEdgePostProcessor.HasClearGatewaySourceScoringOpportunity(
|
||||
repairedRecipientsDefaultToEndPath,
|
||||
hasRecipients,
|
||||
nodes,
|
||||
repairedRecipientsDefaultToEnd.SourceNodeId,
|
||||
repairedRecipientsDefaultToEnd.TargetNodeId)
|
||||
.Should()
|
||||
.BeFalse(
|
||||
$"edge/30 path: {string.Join(" -> ", repairedRecipientsDefaultToEndPath.Select(point => $"({point.X:0.###},{point.Y:0.###})"))}; " +
|
||||
$"scoring candidate: {scoringCandidateText}");
|
||||
ElkEdgeRoutingScoring.CountBadBoundaryAngles(repaired, nodes)
|
||||
.Should().BeLessThanOrEqualTo(initialEntry);
|
||||
ElkEdgeRoutingScoring.CountExcessiveDetourViolations(repaired, nodes)
|
||||
.Should().Be(0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Property("Intent", "Operational")]
|
||||
public void BoundarySlotHelpers_WhenLateSlottingReintroducesMixedFaceAndEntryViolations_ShouldRestabilizeWinnerGeometry()
|
||||
{
|
||||
var process = new ElkPositionedNode
|
||||
{
|
||||
Id = "start/2/branch-1/1",
|
||||
Label = "Process Batch",
|
||||
Kind = "Repeat",
|
||||
X = 992,
|
||||
Y = 247.181640625,
|
||||
Width = 208,
|
||||
Height = 88,
|
||||
};
|
||||
|
||||
var validateSuccess = new ElkPositionedNode
|
||||
{
|
||||
Id = "start/2/branch-1/1/body/5/true/1",
|
||||
Label = "Validate Success",
|
||||
Kind = "Decision",
|
||||
X = 3406,
|
||||
Y = 225.181640625,
|
||||
Width = 188,
|
||||
Height = 132,
|
||||
};
|
||||
|
||||
var join = new ElkPositionedNode
|
||||
{
|
||||
Id = "start/2/join",
|
||||
Label = "Parallel Execution Join",
|
||||
Kind = "Join",
|
||||
X = 1290,
|
||||
Y = 116.5908203125,
|
||||
Width = 176,
|
||||
Height = 124,
|
||||
};
|
||||
|
||||
var internalNotification = new ElkPositionedNode
|
||||
{
|
||||
Id = "start/9/true/1/true/1",
|
||||
Label = "Internal Notification",
|
||||
Kind = "TransportCall",
|
||||
X = 3034,
|
||||
Y = 653.352783203125,
|
||||
Width = 208,
|
||||
Height = 88,
|
||||
};
|
||||
|
||||
var handled = new ElkPositionedNode
|
||||
{
|
||||
Id = "start/9/true/1/true/1/handled/1",
|
||||
Label = "Set internalNotificationFailed",
|
||||
Kind = "SetState",
|
||||
X = 3406,
|
||||
Y = 653.352783203125,
|
||||
Width = 208,
|
||||
Height = 88,
|
||||
};
|
||||
|
||||
var hasRecipients = new ElkPositionedNode
|
||||
{
|
||||
Id = "start/9/true/2",
|
||||
Label = "Has Recipients",
|
||||
Kind = "Decision",
|
||||
X = 3778,
|
||||
Y = 631.352783203125,
|
||||
Width = 188,
|
||||
Height = 132,
|
||||
};
|
||||
|
||||
var end = new ElkPositionedNode
|
||||
{
|
||||
Id = "end",
|
||||
Label = "End",
|
||||
Kind = "End",
|
||||
X = 4864,
|
||||
Y = 331.8013916015625,
|
||||
Width = 264,
|
||||
Height = 132,
|
||||
};
|
||||
|
||||
var repeatReturn = new ElkRoutedEdge
|
||||
{
|
||||
Id = "edge/15",
|
||||
SourceNodeId = validateSuccess.Id,
|
||||
TargetNodeId = process.Id,
|
||||
Label = "repeat while state.printInsisAttempt eq 0",
|
||||
Sections =
|
||||
[
|
||||
new ElkEdgeSection
|
||||
{
|
||||
StartPoint = new ElkPoint { X = 3420.73143444147, Y = 301.524988211567 },
|
||||
EndPoint = new ElkPoint { X = 1200, Y = 269.181640625 },
|
||||
BendPoints =
|
||||
[
|
||||
new ElkPoint { X = 3398, Y = 301.524988211567 },
|
||||
new ElkPoint { X = 3398, Y = -190.659090909091 },
|
||||
new ElkPoint { X = 1224, Y = -190.659090909091 },
|
||||
new ElkPoint { X = 1224, Y = 269.181640625 },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
var joinExit = new ElkRoutedEdge
|
||||
{
|
||||
Id = "edge/17",
|
||||
SourceNodeId = process.Id,
|
||||
TargetNodeId = join.Id,
|
||||
Sections =
|
||||
[
|
||||
new ElkEdgeSection
|
||||
{
|
||||
StartPoint = new ElkPoint { X = 1200, Y = 269.181640625 },
|
||||
EndPoint = new ElkPoint { X = 1298.10526315789, Y = 198.485557154605 },
|
||||
BendPoints =
|
||||
[
|
||||
new ElkPoint { X = 1248, Y = 269.181640625 },
|
||||
new ElkPoint { X = 1248, Y = 248.5908203125 },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
var directFailure = new ElkRoutedEdge
|
||||
{
|
||||
Id = "edge/26",
|
||||
SourceNodeId = internalNotification.Id,
|
||||
TargetNodeId = handled.Id,
|
||||
Label = "on failure / timeout",
|
||||
Sections =
|
||||
[
|
||||
new ElkEdgeSection
|
||||
{
|
||||
StartPoint = new ElkPoint { X = 3242, Y = 675.352783203125 },
|
||||
EndPoint = new ElkPoint { X = 3406, Y = 697.352783203125 },
|
||||
BendPoints =
|
||||
[
|
||||
new ElkPoint { X = 3406, Y = 675.352783203125 },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
var notificationToRecipients = new ElkRoutedEdge
|
||||
{
|
||||
Id = "edge/27",
|
||||
SourceNodeId = internalNotification.Id,
|
||||
TargetNodeId = hasRecipients.Id,
|
||||
Sections =
|
||||
[
|
||||
new ElkEdgeSection
|
||||
{
|
||||
StartPoint = new ElkPoint { X = 3242, Y = 719.352783203125 },
|
||||
EndPoint = new ElkPoint { X = 3792.73143444147, Y = 707.696130789692 },
|
||||
BendPoints =
|
||||
[
|
||||
new ElkPoint { X = 3253.63636363636, Y = 719.352783203125 },
|
||||
new ElkPoint { X = 3253.63636363636, Y = 574.708792946555 },
|
||||
new ElkPoint { X = 3814.59988578289, Y = 574.708792946555 },
|
||||
new ElkPoint { X = 3814.59988578289, Y = 769.900087399336 },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
var recipientsToEnd = new ElkRoutedEdge
|
||||
{
|
||||
Id = "edge/30",
|
||||
SourceNodeId = hasRecipients.Id,
|
||||
TargetNodeId = end.Id,
|
||||
Label = "default",
|
||||
Sections =
|
||||
[
|
||||
new ElkEdgeSection
|
||||
{
|
||||
StartPoint = new ElkPoint { X = 3940.02631578947, Y = 679.115941097862 },
|
||||
EndPoint = new ElkPoint { X = 4864, Y = 439.801391601563 },
|
||||
BendPoints =
|
||||
[
|
||||
new ElkPoint { X = 3974, Y = 679.115941097862 },
|
||||
new ElkPoint { X = 3974, Y = 439.801391601563 },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
var nodes = new[] { process, validateSuccess, join, internalNotification, handled, hasRecipients, end };
|
||||
var edges = new[] { repeatReturn, joinExit, directFailure, notificationToRecipients, recipientsToEnd };
|
||||
var initialSharedLanes = ElkEdgeRoutingScoring.CountSharedLaneViolations(edges, nodes);
|
||||
var initialEntry = ElkEdgeRoutingScoring.CountBadBoundaryAngles(edges, nodes);
|
||||
var initialBoundarySlots = ElkEdgeRoutingScoring.CountBoundarySlotViolations(edges, nodes);
|
||||
var initialDetour = ElkEdgeRoutingScoring.CountExcessiveDetourViolations(edges, nodes);
|
||||
|
||||
initialSharedLanes.Should().BeGreaterThan(0);
|
||||
initialEntry.Should().BeGreaterThan(0);
|
||||
|
||||
var mixedFaceOnly = ElkEdgePostProcessor.SeparateMixedNodeFaceLaneConflicts(
|
||||
edges,
|
||||
nodes,
|
||||
53d,
|
||||
["edge/15", "edge/17"]);
|
||||
ElkEdgeRoutingScoring.CountSharedLaneViolations(mixedFaceOnly, nodes)
|
||||
.Should().Be(0);
|
||||
var terminalOnly = ElkEdgePostProcessor.RepairBoundaryAnglesAndTargetApproaches(
|
||||
edges,
|
||||
nodes,
|
||||
53d,
|
||||
["edge/26", "edge/27", "edge/30"]);
|
||||
terminalOnly = ElkEdgePostProcessor.NormalizeBoundaryAngles(terminalOnly, nodes);
|
||||
ElkEdgeRoutingScoring.CountBadBoundaryAngles(terminalOnly, nodes)
|
||||
.Should().Be(0);
|
||||
|
||||
var repaired = ElkEdgeRouterIterative.BuildFinalRestabilizedCandidate(
|
||||
edges,
|
||||
nodes,
|
||||
ElkLayoutDirection.LeftToRight,
|
||||
53d,
|
||||
["edge/15", "edge/17", "edge/26", "edge/27", "edge/30"]);
|
||||
|
||||
ElkEdgeRoutingScoring.CountSharedLaneViolations(repaired, nodes)
|
||||
.Should().BeLessThan(initialSharedLanes);
|
||||
var repairedBadAngles = new Dictionary<string, int>(StringComparer.Ordinal);
|
||||
ElkEdgeRoutingScoring.CountBadBoundaryAngles(repaired, nodes, repairedBadAngles, 10)
|
||||
.Should().BeLessThan(initialEntry, $"remaining bad-angle edges: {string.Join(", ", repairedBadAngles.Keys.OrderBy(id => id, StringComparer.Ordinal))}");
|
||||
ElkEdgeRoutingScoring.CountBoundarySlotViolations(repaired, nodes)
|
||||
.Should().BeLessThanOrEqualTo(initialBoundarySlots);
|
||||
ElkEdgeRoutingScoring.CountExcessiveDetourViolations(repaired, nodes)
|
||||
.Should().BeLessThanOrEqualTo(initialDetour);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user