Complete ElkSharp document rendering cleanup and source decomposition
- Fix target-join (edge/4+edge/17): gateway face overflow redirect to left tip - Fix under-node (edge/14,15,20): push-first corridor reroute instead of top corridor - Fix boundary-slots (4->0): snap after gateway polish reordering - Fix gateway corner diagonals (2->0): post-pipeline straightening pass - Fix gateway interior adjacent: polygon-aware IsInsideNodeShapeInterior - Fix gateway source face mismatch (2->0): per-edge redirect with lenient validation - Fix gateway source scoring (5->0): per-edge scoring candidate application - Fix edge-node crossing (1->0): push horizontal segment above blocking node - Decompose 7 oversized files (~20K lines) into 55+ partials under 400 lines each - Archive sprints 004 (document cleanup), 005 (decomposition), 007 (render speed) All 44+ document-processing artifact assertions pass. Hybrid deterministic mode documented as recommended path for LeftToRight layouts. Tests verified: StraightExit 2/2, BoundarySlotOffenders 2/2, HybridDeterministicMode 3/3, DocumentProcessingWorkflow artifact 1/1. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -677,8 +677,12 @@ public partial class DocumentProcessingWorkflowRenderingTests
|
||||
}
|
||||
|
||||
var adjacent = fromSource ? path[1] : path[^2];
|
||||
return ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(
|
||||
ToElkNode(node),
|
||||
var elkNode = ToElkNode(node);
|
||||
// Use the polygon-aware interior check for gateway shapes so that
|
||||
// points outside the diamond polygon but inside the rectangular
|
||||
// bounding box are not flagged as interior violations.
|
||||
return ElkShapeBoundaries.IsInsideNodeShapeInterior(
|
||||
elkNode,
|
||||
new ElkPoint { X = adjacent.X, Y = adjacent.Y });
|
||||
}
|
||||
|
||||
|
||||
@@ -2732,4 +2732,4 @@ public partial class ElkSharpEdgeRefinementTests
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,509 @@
|
||||
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_WhenLateCleanupLeavesSourceAndTargetOffLattice_ShouldSnapAssignedSlots()
|
||||
{
|
||||
var source = new ElkPositionedNode
|
||||
{
|
||||
Id = "source",
|
||||
Label = "Internal Notification",
|
||||
Kind = "TransportCall",
|
||||
X = 3034,
|
||||
Y = 653.352783203125,
|
||||
Width = 208,
|
||||
Height = 88,
|
||||
};
|
||||
var handled = new ElkPositionedNode
|
||||
{
|
||||
Id = "handled",
|
||||
Label = "Set internalNotificationFailed",
|
||||
Kind = "SetState",
|
||||
X = 3406,
|
||||
Y = 653.352783203125,
|
||||
Width = 208,
|
||||
Height = 88,
|
||||
};
|
||||
var hasRecipients = new ElkPositionedNode
|
||||
{
|
||||
Id = "hasRecipients",
|
||||
Label = "Has Recipients",
|
||||
Kind = "Decision",
|
||||
X = 3778,
|
||||
Y = 631.352783203125,
|
||||
Width = 188,
|
||||
Height = 132,
|
||||
};
|
||||
var sourceA = new ElkPositionedNode
|
||||
{
|
||||
Id = "source-a",
|
||||
Label = "Source A",
|
||||
Kind = "TransportCall",
|
||||
X = 2100,
|
||||
Y = 420,
|
||||
Width = 208,
|
||||
Height = 132,
|
||||
};
|
||||
var sourceB = new ElkPositionedNode
|
||||
{
|
||||
Id = "source-b",
|
||||
Label = "Source B",
|
||||
Kind = "TransportCall",
|
||||
X = 2362,
|
||||
Y = 330,
|
||||
Width = 208,
|
||||
Height = 132,
|
||||
};
|
||||
var target = new ElkPositionedNode
|
||||
{
|
||||
Id = "target",
|
||||
Label = "Set batchGenerateFailed",
|
||||
Kind = "SetState",
|
||||
X = 2662,
|
||||
Y = 479.4360656738281,
|
||||
Width = 208,
|
||||
Height = 88,
|
||||
};
|
||||
|
||||
var sourceSlots = ElkBoundarySlots.BuildAssignedBoundarySlotCoordinates(source, "right", 2);
|
||||
var targetSlots = ElkBoundarySlots.BuildAssignedBoundarySlotCoordinates(target, "left", 2);
|
||||
var outgoingDirect = new ElkRoutedEdge
|
||||
{
|
||||
Id = "edge/26",
|
||||
SourceNodeId = source.Id,
|
||||
TargetNodeId = handled.Id,
|
||||
Sections =
|
||||
[
|
||||
new ElkEdgeSection
|
||||
{
|
||||
StartPoint = new ElkPoint { X = 3242, Y = 697.352783203125 },
|
||||
EndPoint = new ElkPoint { X = 3406, Y = 697.352783203125 },
|
||||
BendPoints = [],
|
||||
},
|
||||
],
|
||||
};
|
||||
var outgoingBranch = new ElkRoutedEdge
|
||||
{
|
||||
Id = "edge/27",
|
||||
SourceNodeId = source.Id,
|
||||
TargetNodeId = hasRecipients.Id,
|
||||
Sections =
|
||||
[
|
||||
new ElkEdgeSection
|
||||
{
|
||||
StartPoint = new ElkPoint { X = 3242, Y = 697.352783203125 },
|
||||
EndPoint = new ElkPoint { X = 3806.7594033898904, Y = 717.5455557960269 },
|
||||
BendPoints =
|
||||
[
|
||||
new ElkPoint { X = 3266, Y = 697.352783203125 },
|
||||
new ElkPoint { X = 3266, Y = 828.1806263316762 },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
var incomingDirect = new ElkRoutedEdge
|
||||
{
|
||||
Id = "edge/direct",
|
||||
SourceNodeId = sourceA.Id,
|
||||
TargetNodeId = target.Id,
|
||||
Sections =
|
||||
[
|
||||
new ElkEdgeSection
|
||||
{
|
||||
StartPoint = new ElkPoint { X = sourceA.X + sourceA.Width, Y = 523.4360656738281 },
|
||||
EndPoint = new ElkPoint { X = target.X, Y = 523.4360656738281 },
|
||||
BendPoints = [],
|
||||
},
|
||||
],
|
||||
};
|
||||
var incomingElbow = new ElkRoutedEdge
|
||||
{
|
||||
Id = "edge/elbowed",
|
||||
SourceNodeId = sourceB.Id,
|
||||
TargetNodeId = target.Id,
|
||||
Sections =
|
||||
[
|
||||
new ElkEdgeSection
|
||||
{
|
||||
StartPoint = new ElkPoint { X = sourceB.X + sourceB.Width, Y = 390 },
|
||||
EndPoint = new ElkPoint { X = target.X, Y = targetSlots[1] },
|
||||
BendPoints =
|
||||
[
|
||||
new ElkPoint { X = 2550, Y = 390 },
|
||||
new ElkPoint { X = 2550, Y = targetSlots[1] },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
var edges = new[] { outgoingDirect, outgoingBranch, incomingDirect, incomingElbow };
|
||||
var nodes = new[] { source, handled, hasRecipients, sourceA, sourceB, target };
|
||||
ElkEdgeRoutingScoring.CountBoundarySlotViolations(edges, nodes)
|
||||
.Should().BeGreaterThan(0);
|
||||
|
||||
var repaired = ElkEdgePostProcessor.SnapBoundarySlotAssignments(edges, nodes, 53d);
|
||||
ElkEdgeRoutingScoring.CountBoundarySlotViolations(repaired, nodes)
|
||||
.Should().Be(0);
|
||||
repaired.Single(edge => edge.Id == "edge/26").Sections.Single().StartPoint.Y.Should().Be(sourceSlots[0]);
|
||||
repaired.Single(edge => edge.Id == "edge/27").Sections.Single().StartPoint.Y.Should().Be(sourceSlots[1]);
|
||||
repaired.Single(edge => edge.Id == "edge/direct").Sections.Single().EndPoint.Y.Should().Be(targetSlots[0]);
|
||||
repaired.Single(edge => edge.Id == "edge/elbowed").Sections.Single().EndPoint.Y.Should().Be(targetSlots[1]);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Property("Intent", "Operational")]
|
||||
public void MixedNodeFaceHelpers_WhenIncomingAndOutgoingEdgesShareTheSameFaceLane_ShouldSeparateThem()
|
||||
{
|
||||
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 = 3439.84, Y = 267.421640625 },
|
||||
EndPoint = new ElkPoint { X = 1200, Y = 267.421640625 },
|
||||
BendPoints = [],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
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 = 1308.0475075276013, Y = 222.88924788024843 },
|
||||
BendPoints =
|
||||
[
|
||||
new ElkPoint { X = 1282.5912611218437, Y = 269.181640625 },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
var nodes = new[] { process, validateSuccess, join };
|
||||
ElkEdgeRoutingScoring.CountSharedLaneViolations([incoming, outgoing], nodes)
|
||||
.Should().Be(1);
|
||||
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);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Property("Intent", "Operational")]
|
||||
public void BoundarySlotHelpers_WhenGatewaySourceExitsShareARightFace_ShouldSnapToDistinctGatewaySlots()
|
||||
{
|
||||
var retryDecision = new ElkPositionedNode
|
||||
{
|
||||
Id = "retry",
|
||||
Label = "Retry Decision",
|
||||
Kind = "Decision",
|
||||
X = 1976,
|
||||
Y = 413.9718017578125,
|
||||
Width = 188,
|
||||
Height = 132,
|
||||
};
|
||||
|
||||
var cooldownTimer = new ElkPositionedNode
|
||||
{
|
||||
Id = "cooldown",
|
||||
Label = "Cooldown Timer",
|
||||
Kind = "Timer",
|
||||
X = 2290,
|
||||
Y = 457.1265563964844,
|
||||
Width = 208,
|
||||
Height = 88,
|
||||
};
|
||||
|
||||
var setBatchGenerateFailed = new ElkPositionedNode
|
||||
{
|
||||
Id = "failed",
|
||||
Label = "Set batchGenerateFailed",
|
||||
Kind = "SetState",
|
||||
X = 2662,
|
||||
Y = 479.4360656738281,
|
||||
Width = 208,
|
||||
Height = 88,
|
||||
};
|
||||
|
||||
var whenNotExceeded = new ElkRoutedEdge
|
||||
{
|
||||
Id = "edge/8",
|
||||
SourceNodeId = retryDecision.Id,
|
||||
TargetNodeId = cooldownTimer.Id,
|
||||
Label = "when notstate.printInsisAttempt gt 2",
|
||||
Sections =
|
||||
[
|
||||
new ElkEdgeSection
|
||||
{
|
||||
StartPoint = new ElkPoint { X = 2148.8381635762003, Y = 490.61734648090606 },
|
||||
EndPoint = new ElkPoint { X = 2290, Y = 490.61734648090606 },
|
||||
BendPoints = [],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
var defaultExit = new ElkRoutedEdge
|
||||
{
|
||||
Id = "edge/9",
|
||||
SourceNodeId = retryDecision.Id,
|
||||
TargetNodeId = setBatchGenerateFailed.Id,
|
||||
Label = "default",
|
||||
Sections =
|
||||
[
|
||||
new ElkEdgeSection
|
||||
{
|
||||
StartPoint = new ElkPoint { X = 2151.2098609190243, Y = 488.95211217636984 },
|
||||
EndPoint = new ElkPoint { X = 2662, Y = 545.4360656738281 },
|
||||
BendPoints =
|
||||
[
|
||||
new ElkPoint { X = 2172, Y = 488.95211217636984 },
|
||||
new ElkPoint { X = 2172, Y = 545.4360656738281 },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
var nodes = new[] { retryDecision, cooldownTimer, setBatchGenerateFailed };
|
||||
ElkEdgeRoutingScoring.CountBoundarySlotViolations([whenNotExceeded, defaultExit], nodes)
|
||||
.Should().BeGreaterThan(0);
|
||||
|
||||
var repaired = ElkEdgePostProcessor.SnapBoundarySlotAssignments([whenNotExceeded, defaultExit], nodes, 53d);
|
||||
ElkEdgeRoutingScoring.CountBoundarySlotViolations(repaired, nodes)
|
||||
.Should().Be(0);
|
||||
|
||||
var expectedSlots = ElkBoundarySlots.BuildAssignedBoundarySlotAxisCoordinates(retryDecision, "right", 2);
|
||||
repaired.Select(edge => edge.Sections.Single().StartPoint.Y)
|
||||
.OrderBy(value => value)
|
||||
.Should().Equal(expectedSlots.OrderBy(value => value));
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Property("Intent", "Operational")]
|
||||
public void BoundarySlotHelpers_WhenGatewayRightFaceSlotMustRejoinSafeUpperLane_ShouldSnapWithoutCrossingBlocker()
|
||||
{
|
||||
var retryDecision = new ElkPositionedNode
|
||||
{
|
||||
Id = "retry",
|
||||
Label = "Retry Decision",
|
||||
Kind = "Decision",
|
||||
X = 1976,
|
||||
Y = 413.9718017578125,
|
||||
Width = 188,
|
||||
Height = 132,
|
||||
};
|
||||
|
||||
var cooldownTimer = new ElkPositionedNode
|
||||
{
|
||||
Id = "cooldown",
|
||||
Label = "Cooldown Timer",
|
||||
Kind = "Timer",
|
||||
X = 2290,
|
||||
Y = 457.1265563964844,
|
||||
Width = 208,
|
||||
Height = 88,
|
||||
};
|
||||
|
||||
var setBatchGenerateFailed = new ElkPositionedNode
|
||||
{
|
||||
Id = "failed",
|
||||
Label = "Set batchGenerateFailed",
|
||||
Kind = "SetState",
|
||||
X = 2662,
|
||||
Y = 479.4360656738281,
|
||||
Width = 208,
|
||||
Height = 88,
|
||||
};
|
||||
|
||||
var whenNotExceeded = new ElkRoutedEdge
|
||||
{
|
||||
Id = "edge/8",
|
||||
SourceNodeId = retryDecision.Id,
|
||||
TargetNodeId = cooldownTimer.Id,
|
||||
Label = "when notstate.printInsisAttempt gt 2",
|
||||
Sections =
|
||||
[
|
||||
new ElkEdgeSection
|
||||
{
|
||||
StartPoint = new ElkPoint { X = 2148.8381635762003, Y = 490.61734648090606 },
|
||||
EndPoint = new ElkPoint { X = 2290, Y = 490.61734648090606 },
|
||||
BendPoints = [],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
var defaultExit = new ElkRoutedEdge
|
||||
{
|
||||
Id = "edge/9",
|
||||
SourceNodeId = retryDecision.Id,
|
||||
TargetNodeId = setBatchGenerateFailed.Id,
|
||||
Label = "default",
|
||||
Sections =
|
||||
[
|
||||
new ElkEdgeSection
|
||||
{
|
||||
StartPoint = new ElkPoint { X = 2103.8402987865636, Y = 437.73227685820864 },
|
||||
EndPoint = new ElkPoint { X = 2598.726806640625, Y = 479.4360656738281 },
|
||||
BendPoints =
|
||||
[
|
||||
new ElkPoint { X = 2598.726806640625, Y = 437.73227685820864 },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
var nodes = new[] { retryDecision, cooldownTimer, setBatchGenerateFailed };
|
||||
ElkEdgeRoutingScoring.CountBoundarySlotViolations([whenNotExceeded, defaultExit], nodes)
|
||||
.Should().BeGreaterThan(0);
|
||||
|
||||
var repaired = ElkEdgePostProcessor.SnapBoundarySlotAssignments(
|
||||
[whenNotExceeded, defaultExit],
|
||||
nodes,
|
||||
53d,
|
||||
enforceAllNodeEndpoints: true);
|
||||
var repairedEdge8Path = ExtractPath(repaired.Single(edge => edge.Id == "edge/8"));
|
||||
var repairedEdge9Path = ExtractPath(repaired.Single(edge => edge.Id == "edge/9"));
|
||||
var repairedDefaultPath = ExtractPath(repaired.Single(edge => edge.Id == "edge/9"));
|
||||
var pathText = string.Join(" -> ", repairedDefaultPath.Select(point => $"({point.X:F3},{point.Y:F3})"));
|
||||
|
||||
ElkEdgeRoutingScoring.CountBoundarySlotViolations(repaired, nodes)
|
||||
.Should().Be(0, pathText);
|
||||
ElkEdgeRoutingScoring.CountGatewaySourceExitViolations(repaired, nodes)
|
||||
.Should().Be(0, pathText);
|
||||
ElkEdgeRoutingScoring.CountEdgeNodeCrossings(repaired, nodes, null)
|
||||
.Should().Be(0, pathText);
|
||||
|
||||
var expectedSlots = ElkBoundarySlots.BuildAssignedBoundarySlotAxisCoordinates(retryDecision, "right", 2)
|
||||
.OrderBy(value => value)
|
||||
.ToArray();
|
||||
repairedDefaultPath[0].Y.Should().BeApproximately(expectedSlots[0], 0.5d, pathText);
|
||||
repairedDefaultPath.Any(point => point.Y < cooldownTimer.Y - 0.5d)
|
||||
.Should().BeTrue(pathText);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Property("Intent", "Operational")]
|
||||
public void BoundarySlotHelpers_WhenStrictSnapSeesSingleRepeatCorridorExit_ShouldCenterTheDepartureSlot()
|
||||
{
|
||||
var repeatDecision = new ElkPositionedNode
|
||||
{
|
||||
Id = "repeat",
|
||||
Label = "Repeat Decision",
|
||||
Kind = "Decision",
|
||||
X = 3000,
|
||||
Y = 320,
|
||||
Width = 188,
|
||||
Height = 132,
|
||||
};
|
||||
|
||||
var target = new ElkPositionedNode
|
||||
{
|
||||
Id = "target",
|
||||
Label = "Internal Discussion",
|
||||
Kind = "TransportCall",
|
||||
X = 3420,
|
||||
Y = 340,
|
||||
Width = 208,
|
||||
Height = 88,
|
||||
};
|
||||
|
||||
var repeatReturn = new ElkRoutedEdge
|
||||
{
|
||||
Id = "edge/repeat-corridor",
|
||||
SourceNodeId = repeatDecision.Id,
|
||||
TargetNodeId = target.Id,
|
||||
Label = "repeat while state.printInsisAttempt eq 0",
|
||||
Sections =
|
||||
[
|
||||
new ElkEdgeSection
|
||||
{
|
||||
StartPoint = new ElkPoint { X = 3188, Y = 348 },
|
||||
EndPoint = new ElkPoint { X = 3420, Y = 384 },
|
||||
BendPoints =
|
||||
[
|
||||
new ElkPoint { X = 3228, Y = 348 },
|
||||
new ElkPoint { X = 3228, Y = 240 },
|
||||
new ElkPoint { X = 3420, Y = 240 },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
var nodes = new[] { repeatDecision, target };
|
||||
ElkEdgeRoutingScoring.CountBoundarySlotViolations([repeatReturn], nodes)
|
||||
.Should().Be(1);
|
||||
|
||||
var repaired = ElkEdgePostProcessor.SnapBoundarySlotAssignments(
|
||||
[repeatReturn],
|
||||
nodes,
|
||||
53d,
|
||||
enforceAllNodeEndpoints: true);
|
||||
|
||||
ElkEdgeRoutingScoring.CountBoundarySlotViolations(repaired, nodes)
|
||||
.Should().Be(0);
|
||||
|
||||
var expectedStartY = ElkBoundarySlots.BuildAssignedBoundarySlotAxisCoordinates(repeatDecision, "right", 1).Single();
|
||||
repaired.Single().Sections.Single().StartPoint.Y.Should().BeApproximately(expectedStartY, 0.5d);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,392 @@
|
||||
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 GatewayBoundaryHelpers_WhenDominantAxisExitIsBlocked_ShouldNotCountAsBoundaryAngleViolation()
|
||||
{
|
||||
var source = new ElkPositionedNode
|
||||
{
|
||||
Id = "source",
|
||||
Label = "Internal Notification",
|
||||
Kind = "Decision",
|
||||
X = 2557,
|
||||
Y = 543.9360656738281,
|
||||
Width = 188,
|
||||
Height = 132,
|
||||
};
|
||||
|
||||
var blocker = new ElkPositionedNode
|
||||
{
|
||||
Id = "blocker",
|
||||
Label = "Set internalNotificationFailed",
|
||||
Kind = "SetState",
|
||||
X = 3206,
|
||||
Y = 561.352783203125,
|
||||
Width = 208,
|
||||
Height = 88,
|
||||
};
|
||||
|
||||
var target = new ElkPositionedNode
|
||||
{
|
||||
Id = "target",
|
||||
Label = "Has Recipients",
|
||||
Kind = "Decision",
|
||||
X = 3578,
|
||||
Y = 539.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 = 2711.16, Y = 633.70 },
|
||||
EndPoint = new ElkPoint { X = 3658.87, Y = 662.13 },
|
||||
BendPoints =
|
||||
[
|
||||
new ElkPoint { X = 2745.41, Y = 682.48 },
|
||||
new ElkPoint { X = 3658.87, Y = 682.48 },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
var repaired = ElkEdgePostProcessor.FinalizeGatewayBoundaryGeometry([edge], [source, blocker, target]);
|
||||
var violations = ElkEdgeRoutingScoring.CountBadBoundaryAngles(repaired, [source, blocker, target]);
|
||||
|
||||
violations.Should().Be(0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Property("Intent", "Operational")]
|
||||
public void GatewayBoundaryHelpers_WhenGatewaySourceHasShorterClearExit_ShouldExposeOpportunityAndShortenPath()
|
||||
{
|
||||
var source = new ElkPositionedNode
|
||||
{
|
||||
Id = "source",
|
||||
Label = "Internal Notification",
|
||||
Kind = "Decision",
|
||||
X = 2557,
|
||||
Y = 543.9360656738281,
|
||||
Width = 188,
|
||||
Height = 132,
|
||||
};
|
||||
|
||||
var target = new ElkPositionedNode
|
||||
{
|
||||
Id = "target",
|
||||
Label = "Has Recipients",
|
||||
Kind = "Decision",
|
||||
X = 3578,
|
||||
Y = 539.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 = 2711.16, Y = 633.70 },
|
||||
EndPoint = new ElkPoint { X = 3658.87, Y = 662.13 },
|
||||
BendPoints =
|
||||
[
|
||||
new ElkPoint { X = 2745.41, Y = 682.48 },
|
||||
new ElkPoint { X = 3658.87, Y = 682.48 },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
var originalSection = edge.Sections.Single();
|
||||
var originalPath = new List<ElkPoint> { originalSection.StartPoint };
|
||||
originalPath.AddRange(originalSection.BendPoints);
|
||||
originalPath.Add(originalSection.EndPoint);
|
||||
|
||||
ElkEdgePostProcessor.HasClearGatewaySourceDirectRepairOpportunity(
|
||||
originalPath,
|
||||
source,
|
||||
[source, target],
|
||||
edge.SourceNodeId,
|
||||
edge.TargetNodeId).Should().BeTrue();
|
||||
|
||||
var repaired = ElkEdgePostProcessor.FinalizeGatewayBoundaryGeometry([edge], [source, target]);
|
||||
var section = repaired[0].Sections.Single();
|
||||
var path = new List<ElkPoint> { section.StartPoint };
|
||||
path.AddRange(section.BendPoints);
|
||||
path.Add(section.EndPoint);
|
||||
|
||||
ComputePathLength(path).Should().BeLessThan(ComputePathLength(originalPath));
|
||||
ElkEdgeRoutingScoring.CountBadBoundaryAngles(repaired, [source, target]).Should().Be(0);
|
||||
|
||||
static double ComputePathLength(IReadOnlyList<ElkPoint> points)
|
||||
{
|
||||
var length = 0d;
|
||||
for (var i = 1; i < points.Count; i++)
|
||||
{
|
||||
var dx = points[i].X - points[i - 1].X;
|
||||
var dy = points[i].Y - points[i - 1].Y;
|
||||
length += Math.Sqrt((dx * dx) + (dy * dy));
|
||||
}
|
||||
|
||||
return length;
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Property("Intent", "Operational")]
|
||||
public void GatewayBoundaryHelpers_WhenDecisionSourceNeedsVerticalStemForValidExit_ShouldKeepBoundaryRepair()
|
||||
{
|
||||
var source = new ElkPositionedNode
|
||||
{
|
||||
Id = "source",
|
||||
Label = "Evaluate Conditions",
|
||||
Kind = "Decision",
|
||||
X = 2290,
|
||||
Y = 32.25,
|
||||
Width = 188,
|
||||
Height = 132,
|
||||
};
|
||||
|
||||
var target = new ElkPositionedNode
|
||||
{
|
||||
Id = "target",
|
||||
Label = "Internal Notification",
|
||||
Kind = "Decision",
|
||||
X = 2662,
|
||||
Y = 639.4360656738281,
|
||||
Width = 188,
|
||||
Height = 132,
|
||||
};
|
||||
|
||||
var edge = new ElkRoutedEdge
|
||||
{
|
||||
Id = "edge/22",
|
||||
SourceNodeId = source.Id,
|
||||
TargetNodeId = target.Id,
|
||||
Label = "when state.notificationHasBody",
|
||||
Sections =
|
||||
[
|
||||
new ElkEdgeSection
|
||||
{
|
||||
StartPoint = new ElkPoint { X = 2409.45679088221, Y = 172.25 },
|
||||
EndPoint = new ElkPoint { X = 2693.4842064714944, Y = 683.3301334704383 },
|
||||
BendPoints =
|
||||
[
|
||||
new ElkPoint { X = 2546.7272727272725, Y = 172.25 },
|
||||
new ElkPoint { X = 2546.7272727272725, Y = 631.4360656738281 },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
ElkEdgeRoutingScoring.CountBadBoundaryAngles([edge], [source, target]).Should().Be(1);
|
||||
|
||||
var repaired = ElkEdgePostProcessor.FinalizeGatewayBoundaryGeometry([edge], [source, target]);
|
||||
var section = repaired[0].Sections.Single();
|
||||
var path = new List<ElkPoint> { section.StartPoint };
|
||||
path.AddRange(section.BendPoints);
|
||||
path.Add(section.EndPoint);
|
||||
var originalFirstBend = edge.Sections.Single().BendPoints.First();
|
||||
|
||||
ElkEdgeRoutingScoring.CountBadBoundaryAngles(repaired, [source, target]).Should().Be(0);
|
||||
ElkEdgeRoutingScoring.CountGatewaySourceExitViolations(repaired, [source, target]).Should().Be(0);
|
||||
path[1].Y.Should().BeGreaterThan(path[0].Y + 3d);
|
||||
ElkEdgeRoutingGeometry.PointsEqual(path[1], originalFirstBend).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Property("Intent", "Operational")]
|
||||
public void GatewayBoundaryHelpers_WhenDecisionSourceVerticalStemWouldHitLocalBlocker_ShouldEscapeBeforeBlocker()
|
||||
{
|
||||
var source = new ElkPositionedNode
|
||||
{
|
||||
Id = "source",
|
||||
Label = "Evaluate Conditions",
|
||||
Kind = "Decision",
|
||||
X = 2290,
|
||||
Y = 32.25,
|
||||
Width = 188,
|
||||
Height = 132,
|
||||
};
|
||||
|
||||
var blocker = new ElkPositionedNode
|
||||
{
|
||||
Id = "blocker",
|
||||
Label = "Delay Notification",
|
||||
Kind = "Timer",
|
||||
X = 2290,
|
||||
Y = 457.1265563964844,
|
||||
Width = 208,
|
||||
Height = 88,
|
||||
};
|
||||
|
||||
var target = new ElkPositionedNode
|
||||
{
|
||||
Id = "target",
|
||||
Label = "Internal Notification",
|
||||
Kind = "Decision",
|
||||
X = 2662,
|
||||
Y = 639.4360656738281,
|
||||
Width = 188,
|
||||
Height = 132,
|
||||
};
|
||||
|
||||
var edge = new ElkRoutedEdge
|
||||
{
|
||||
Id = "edge/22",
|
||||
SourceNodeId = source.Id,
|
||||
TargetNodeId = target.Id,
|
||||
Label = "when state.notificationHasBody",
|
||||
Sections =
|
||||
[
|
||||
new ElkEdgeSection
|
||||
{
|
||||
StartPoint = new ElkPoint { X = 2409.45679088221, Y = 172.25 },
|
||||
EndPoint = new ElkPoint { X = 2693.4842064714944, Y = 683.3301334704383 },
|
||||
BendPoints =
|
||||
[
|
||||
new ElkPoint { X = 2546.7272727272725, Y = 172.25 },
|
||||
new ElkPoint { X = 2546.7272727272725, Y = 631.4360656738281 },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
var repaired = ElkEdgePostProcessor.FinalizeGatewayBoundaryGeometry([edge], [source, blocker, target]);
|
||||
var section = repaired[0].Sections.Single();
|
||||
var path = new List<ElkPoint> { section.StartPoint };
|
||||
path.AddRange(section.BendPoints);
|
||||
path.Add(section.EndPoint);
|
||||
TestContext.Out.WriteLine(
|
||||
$"{string.Join(" -> ", path.Select(point => $"({point.X:F3},{point.Y:F3})"))} "
|
||||
+ $"boundary={ElkEdgeRoutingScoring.CountBadBoundaryAngles(repaired, [source, blocker, target])} "
|
||||
+ $"gateway-source={ElkEdgeRoutingScoring.CountGatewaySourceExitViolations(repaired, [source, blocker, target])}");
|
||||
|
||||
ElkEdgeRoutingScoring.CountBadBoundaryAngles(repaired, [source, blocker, target]).Should().Be(0);
|
||||
ElkEdgeRoutingScoring.CountGatewaySourceExitViolations(repaired, [source, blocker, target]).Should().Be(0);
|
||||
path[1].Y.Should().BeGreaterThan(path[0].Y + 3d);
|
||||
path.Should().Contain(point => point.X > blocker.X + blocker.Width + 8d && point.Y < blocker.Y - 0.5d);
|
||||
CrossesRectObstacle(path, blocker).Should().BeFalse();
|
||||
|
||||
static bool CrossesRectObstacle(IReadOnlyList<ElkPoint> polyline, ElkPositionedNode node)
|
||||
{
|
||||
const double tolerance = 0.5d;
|
||||
for (var i = 0; i < polyline.Count - 1; i++)
|
||||
{
|
||||
var start = polyline[i];
|
||||
var end = polyline[i + 1];
|
||||
if (Math.Abs(start.Y - end.Y) <= tolerance)
|
||||
{
|
||||
if (start.Y > node.Y + tolerance
|
||||
&& start.Y < node.Y + node.Height - tolerance
|
||||
&& Math.Max(start.X, end.X) > node.X + tolerance
|
||||
&& Math.Min(start.X, end.X) < node.X + node.Width - tolerance)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else if (Math.Abs(start.X - end.X) <= tolerance)
|
||||
{
|
||||
if (start.X > node.X + tolerance
|
||||
&& start.X < node.X + node.Width - tolerance
|
||||
&& Math.Max(start.Y, end.Y) > node.Y + tolerance
|
||||
&& Math.Min(start.Y, end.Y) < node.Y + node.Height - tolerance)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Property("Intent", "Operational")]
|
||||
public void GatewayBoundaryHelpers_WhenUpperGatewayArrivalsCollapseOntoSameLane_ShouldCountJoinAndSharedLaneViolations()
|
||||
{
|
||||
var target = new ElkPositionedNode
|
||||
{
|
||||
Id = "target",
|
||||
Label = "Has Recipients",
|
||||
Kind = "Decision",
|
||||
X = 3578,
|
||||
Y = 539.352783203125,
|
||||
Width = 188,
|
||||
Height = 132,
|
||||
};
|
||||
|
||||
var leftArrival = new ElkRoutedEdge
|
||||
{
|
||||
Id = "edge/25",
|
||||
SourceNodeId = "source-a",
|
||||
TargetNodeId = target.Id,
|
||||
Label = "default",
|
||||
Sections =
|
||||
[
|
||||
new ElkEdgeSection
|
||||
{
|
||||
StartPoint = new ElkPoint { X = 2738.472294662938, Y = 605.352783203125 },
|
||||
EndPoint = new ElkPoint { X = 3586.8910474656404, Y = 599.1101328549094 },
|
||||
BendPoints =
|
||||
[
|
||||
new ElkPoint { X = 2738.472294662938, Y = 535.9360656738281 },
|
||||
new ElkPoint { X = 3586.8910474656404, Y = 535.9360656738281 },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
var rightArrival = new ElkRoutedEdge
|
||||
{
|
||||
Id = "edge/28",
|
||||
SourceNodeId = "source-b",
|
||||
TargetNodeId = target.Id,
|
||||
Sections =
|
||||
[
|
||||
new ElkEdgeSection
|
||||
{
|
||||
StartPoint = new ElkPoint { X = 3414, Y = 605.352783203125 },
|
||||
EndPoint = new ElkPoint { X = 3682.79150671785, Y = 546.9297985582112 },
|
||||
BendPoints =
|
||||
[
|
||||
new ElkPoint { X = 3438, Y = 605.352783203125 },
|
||||
new ElkPoint { X = 3438, Y = 532.8054790069142 },
|
||||
new ElkPoint { X = 3682.79150671785, Y = 532.8054790069142 },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
var nodes = new[] { target };
|
||||
ElkEdgeRoutingScoring.CountTargetApproachJoinViolations([leftArrival, rightArrival], nodes)
|
||||
.Should().Be(1);
|
||||
ElkEdgeRoutingScoring.CountSharedLaneViolations([leftArrival, rightArrival], nodes)
|
||||
.Should().Be(1);
|
||||
}
|
||||
}
|
||||
@@ -11,385 +11,6 @@ namespace StellaOps.Workflow.Renderer.Tests;
|
||||
|
||||
public partial class ElkSharpEdgeRefinementTests
|
||||
{
|
||||
[Test]
|
||||
[Property("Intent", "Operational")]
|
||||
public void GatewayBoundaryHelpers_WhenDominantAxisExitIsBlocked_ShouldNotCountAsBoundaryAngleViolation()
|
||||
{
|
||||
var source = new ElkPositionedNode
|
||||
{
|
||||
Id = "source",
|
||||
Label = "Internal Notification",
|
||||
Kind = "Decision",
|
||||
X = 2557,
|
||||
Y = 543.9360656738281,
|
||||
Width = 188,
|
||||
Height = 132,
|
||||
};
|
||||
|
||||
var blocker = new ElkPositionedNode
|
||||
{
|
||||
Id = "blocker",
|
||||
Label = "Set internalNotificationFailed",
|
||||
Kind = "SetState",
|
||||
X = 3206,
|
||||
Y = 561.352783203125,
|
||||
Width = 208,
|
||||
Height = 88,
|
||||
};
|
||||
|
||||
var target = new ElkPositionedNode
|
||||
{
|
||||
Id = "target",
|
||||
Label = "Has Recipients",
|
||||
Kind = "Decision",
|
||||
X = 3578,
|
||||
Y = 539.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 = 2711.16, Y = 633.70 },
|
||||
EndPoint = new ElkPoint { X = 3658.87, Y = 662.13 },
|
||||
BendPoints =
|
||||
[
|
||||
new ElkPoint { X = 2745.41, Y = 682.48 },
|
||||
new ElkPoint { X = 3658.87, Y = 682.48 },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
var repaired = ElkEdgePostProcessor.FinalizeGatewayBoundaryGeometry([edge], [source, blocker, target]);
|
||||
var violations = ElkEdgeRoutingScoring.CountBadBoundaryAngles(repaired, [source, blocker, target]);
|
||||
|
||||
violations.Should().Be(0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Property("Intent", "Operational")]
|
||||
public void GatewayBoundaryHelpers_WhenGatewaySourceHasShorterClearExit_ShouldExposeOpportunityAndShortenPath()
|
||||
{
|
||||
var source = new ElkPositionedNode
|
||||
{
|
||||
Id = "source",
|
||||
Label = "Internal Notification",
|
||||
Kind = "Decision",
|
||||
X = 2557,
|
||||
Y = 543.9360656738281,
|
||||
Width = 188,
|
||||
Height = 132,
|
||||
};
|
||||
|
||||
var target = new ElkPositionedNode
|
||||
{
|
||||
Id = "target",
|
||||
Label = "Has Recipients",
|
||||
Kind = "Decision",
|
||||
X = 3578,
|
||||
Y = 539.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 = 2711.16, Y = 633.70 },
|
||||
EndPoint = new ElkPoint { X = 3658.87, Y = 662.13 },
|
||||
BendPoints =
|
||||
[
|
||||
new ElkPoint { X = 2745.41, Y = 682.48 },
|
||||
new ElkPoint { X = 3658.87, Y = 682.48 },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
var originalSection = edge.Sections.Single();
|
||||
var originalPath = new List<ElkPoint> { originalSection.StartPoint };
|
||||
originalPath.AddRange(originalSection.BendPoints);
|
||||
originalPath.Add(originalSection.EndPoint);
|
||||
|
||||
ElkEdgePostProcessor.HasClearGatewaySourceDirectRepairOpportunity(
|
||||
originalPath,
|
||||
source,
|
||||
[source, target],
|
||||
edge.SourceNodeId,
|
||||
edge.TargetNodeId).Should().BeTrue();
|
||||
|
||||
var repaired = ElkEdgePostProcessor.FinalizeGatewayBoundaryGeometry([edge], [source, target]);
|
||||
var section = repaired[0].Sections.Single();
|
||||
var path = new List<ElkPoint> { section.StartPoint };
|
||||
path.AddRange(section.BendPoints);
|
||||
path.Add(section.EndPoint);
|
||||
|
||||
ComputePathLength(path).Should().BeLessThan(ComputePathLength(originalPath));
|
||||
ElkEdgeRoutingScoring.CountBadBoundaryAngles(repaired, [source, target]).Should().Be(0);
|
||||
|
||||
static double ComputePathLength(IReadOnlyList<ElkPoint> points)
|
||||
{
|
||||
var length = 0d;
|
||||
for (var i = 1; i < points.Count; i++)
|
||||
{
|
||||
var dx = points[i].X - points[i - 1].X;
|
||||
var dy = points[i].Y - points[i - 1].Y;
|
||||
length += Math.Sqrt((dx * dx) + (dy * dy));
|
||||
}
|
||||
|
||||
return length;
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Property("Intent", "Operational")]
|
||||
public void GatewayBoundaryHelpers_WhenDecisionSourceNeedsVerticalStemForValidExit_ShouldKeepBoundaryRepair()
|
||||
{
|
||||
var source = new ElkPositionedNode
|
||||
{
|
||||
Id = "source",
|
||||
Label = "Evaluate Conditions",
|
||||
Kind = "Decision",
|
||||
X = 2290,
|
||||
Y = 32.25,
|
||||
Width = 188,
|
||||
Height = 132,
|
||||
};
|
||||
|
||||
var target = new ElkPositionedNode
|
||||
{
|
||||
Id = "target",
|
||||
Label = "Internal Notification",
|
||||
Kind = "Decision",
|
||||
X = 2662,
|
||||
Y = 639.4360656738281,
|
||||
Width = 188,
|
||||
Height = 132,
|
||||
};
|
||||
|
||||
var edge = new ElkRoutedEdge
|
||||
{
|
||||
Id = "edge/22",
|
||||
SourceNodeId = source.Id,
|
||||
TargetNodeId = target.Id,
|
||||
Label = "when state.notificationHasBody",
|
||||
Sections =
|
||||
[
|
||||
new ElkEdgeSection
|
||||
{
|
||||
StartPoint = new ElkPoint { X = 2409.45679088221, Y = 172.25 },
|
||||
EndPoint = new ElkPoint { X = 2693.4842064714944, Y = 683.3301334704383 },
|
||||
BendPoints =
|
||||
[
|
||||
new ElkPoint { X = 2546.7272727272725, Y = 172.25 },
|
||||
new ElkPoint { X = 2546.7272727272725, Y = 631.4360656738281 },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
ElkEdgeRoutingScoring.CountBadBoundaryAngles([edge], [source, target]).Should().Be(1);
|
||||
|
||||
var repaired = ElkEdgePostProcessor.FinalizeGatewayBoundaryGeometry([edge], [source, target]);
|
||||
var section = repaired[0].Sections.Single();
|
||||
var path = new List<ElkPoint> { section.StartPoint };
|
||||
path.AddRange(section.BendPoints);
|
||||
path.Add(section.EndPoint);
|
||||
var originalFirstBend = edge.Sections.Single().BendPoints.First();
|
||||
|
||||
ElkEdgeRoutingScoring.CountBadBoundaryAngles(repaired, [source, target]).Should().Be(0);
|
||||
ElkEdgeRoutingScoring.CountGatewaySourceExitViolations(repaired, [source, target]).Should().Be(0);
|
||||
path[1].Y.Should().BeGreaterThan(path[0].Y + 3d);
|
||||
ElkEdgeRoutingGeometry.PointsEqual(path[1], originalFirstBend).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Property("Intent", "Operational")]
|
||||
public void GatewayBoundaryHelpers_WhenDecisionSourceVerticalStemWouldHitLocalBlocker_ShouldEscapeBeforeBlocker()
|
||||
{
|
||||
var source = new ElkPositionedNode
|
||||
{
|
||||
Id = "source",
|
||||
Label = "Evaluate Conditions",
|
||||
Kind = "Decision",
|
||||
X = 2290,
|
||||
Y = 32.25,
|
||||
Width = 188,
|
||||
Height = 132,
|
||||
};
|
||||
|
||||
var blocker = new ElkPositionedNode
|
||||
{
|
||||
Id = "blocker",
|
||||
Label = "Delay Notification",
|
||||
Kind = "Timer",
|
||||
X = 2290,
|
||||
Y = 457.1265563964844,
|
||||
Width = 208,
|
||||
Height = 88,
|
||||
};
|
||||
|
||||
var target = new ElkPositionedNode
|
||||
{
|
||||
Id = "target",
|
||||
Label = "Internal Notification",
|
||||
Kind = "Decision",
|
||||
X = 2662,
|
||||
Y = 639.4360656738281,
|
||||
Width = 188,
|
||||
Height = 132,
|
||||
};
|
||||
|
||||
var edge = new ElkRoutedEdge
|
||||
{
|
||||
Id = "edge/22",
|
||||
SourceNodeId = source.Id,
|
||||
TargetNodeId = target.Id,
|
||||
Label = "when state.notificationHasBody",
|
||||
Sections =
|
||||
[
|
||||
new ElkEdgeSection
|
||||
{
|
||||
StartPoint = new ElkPoint { X = 2409.45679088221, Y = 172.25 },
|
||||
EndPoint = new ElkPoint { X = 2693.4842064714944, Y = 683.3301334704383 },
|
||||
BendPoints =
|
||||
[
|
||||
new ElkPoint { X = 2546.7272727272725, Y = 172.25 },
|
||||
new ElkPoint { X = 2546.7272727272725, Y = 631.4360656738281 },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
var repaired = ElkEdgePostProcessor.FinalizeGatewayBoundaryGeometry([edge], [source, blocker, target]);
|
||||
var section = repaired[0].Sections.Single();
|
||||
var path = new List<ElkPoint> { section.StartPoint };
|
||||
path.AddRange(section.BendPoints);
|
||||
path.Add(section.EndPoint);
|
||||
TestContext.Out.WriteLine(
|
||||
$"{string.Join(" -> ", path.Select(point => $"({point.X:F3},{point.Y:F3})"))} "
|
||||
+ $"boundary={ElkEdgeRoutingScoring.CountBadBoundaryAngles(repaired, [source, blocker, target])} "
|
||||
+ $"gateway-source={ElkEdgeRoutingScoring.CountGatewaySourceExitViolations(repaired, [source, blocker, target])}");
|
||||
|
||||
ElkEdgeRoutingScoring.CountBadBoundaryAngles(repaired, [source, blocker, target]).Should().Be(0);
|
||||
ElkEdgeRoutingScoring.CountGatewaySourceExitViolations(repaired, [source, blocker, target]).Should().Be(0);
|
||||
path[1].Y.Should().BeGreaterThan(path[0].Y + 3d);
|
||||
path.Should().Contain(point => point.X > blocker.X + blocker.Width + 8d && point.Y < blocker.Y - 0.5d);
|
||||
CrossesRectObstacle(path, blocker).Should().BeFalse();
|
||||
|
||||
static bool CrossesRectObstacle(IReadOnlyList<ElkPoint> polyline, ElkPositionedNode node)
|
||||
{
|
||||
const double tolerance = 0.5d;
|
||||
for (var i = 0; i < polyline.Count - 1; i++)
|
||||
{
|
||||
var start = polyline[i];
|
||||
var end = polyline[i + 1];
|
||||
if (Math.Abs(start.Y - end.Y) <= tolerance)
|
||||
{
|
||||
if (start.Y > node.Y + tolerance
|
||||
&& start.Y < node.Y + node.Height - tolerance
|
||||
&& Math.Max(start.X, end.X) > node.X + tolerance
|
||||
&& Math.Min(start.X, end.X) < node.X + node.Width - tolerance)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else if (Math.Abs(start.X - end.X) <= tolerance)
|
||||
{
|
||||
if (start.X > node.X + tolerance
|
||||
&& start.X < node.X + node.Width - tolerance
|
||||
&& Math.Max(start.Y, end.Y) > node.Y + tolerance
|
||||
&& Math.Min(start.Y, end.Y) < node.Y + node.Height - tolerance)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Property("Intent", "Operational")]
|
||||
public void GatewayBoundaryHelpers_WhenUpperGatewayArrivalsCollapseOntoSameLane_ShouldCountJoinAndSharedLaneViolations()
|
||||
{
|
||||
var target = new ElkPositionedNode
|
||||
{
|
||||
Id = "target",
|
||||
Label = "Has Recipients",
|
||||
Kind = "Decision",
|
||||
X = 3578,
|
||||
Y = 539.352783203125,
|
||||
Width = 188,
|
||||
Height = 132,
|
||||
};
|
||||
|
||||
var leftArrival = new ElkRoutedEdge
|
||||
{
|
||||
Id = "edge/25",
|
||||
SourceNodeId = "source-a",
|
||||
TargetNodeId = target.Id,
|
||||
Label = "default",
|
||||
Sections =
|
||||
[
|
||||
new ElkEdgeSection
|
||||
{
|
||||
StartPoint = new ElkPoint { X = 2738.472294662938, Y = 605.352783203125 },
|
||||
EndPoint = new ElkPoint { X = 3586.8910474656404, Y = 599.1101328549094 },
|
||||
BendPoints =
|
||||
[
|
||||
new ElkPoint { X = 2738.472294662938, Y = 535.9360656738281 },
|
||||
new ElkPoint { X = 3586.8910474656404, Y = 535.9360656738281 },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
var rightArrival = new ElkRoutedEdge
|
||||
{
|
||||
Id = "edge/28",
|
||||
SourceNodeId = "source-b",
|
||||
TargetNodeId = target.Id,
|
||||
Sections =
|
||||
[
|
||||
new ElkEdgeSection
|
||||
{
|
||||
StartPoint = new ElkPoint { X = 3414, Y = 605.352783203125 },
|
||||
EndPoint = new ElkPoint { X = 3682.79150671785, Y = 546.9297985582112 },
|
||||
BendPoints =
|
||||
[
|
||||
new ElkPoint { X = 3438, Y = 605.352783203125 },
|
||||
new ElkPoint { X = 3438, Y = 532.8054790069142 },
|
||||
new ElkPoint { X = 3682.79150671785, Y = 532.8054790069142 },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
var nodes = new[] { target };
|
||||
ElkEdgeRoutingScoring.CountTargetApproachJoinViolations([leftArrival, rightArrival], nodes)
|
||||
.Should().Be(1);
|
||||
ElkEdgeRoutingScoring.CountSharedLaneViolations([leftArrival, rightArrival], nodes)
|
||||
.Should().Be(1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Property("Intent", "Operational")]
|
||||
public void SourceDepartureHelpers_WhenOutgoingEdgesShareTheSameDepartureLane_ShouldSpreadOnlyTheConflictingPeer()
|
||||
@@ -496,500 +117,4 @@ public partial class ElkSharpEdgeRefinementTests
|
||||
repairedBranching.StartPoint.Y.Should().Be(assignedSourceSlots[1]);
|
||||
repairedBranchingPoints.Should().NotBeEquivalentTo(originalBranchingPoints);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Property("Intent", "Operational")]
|
||||
public void BoundarySlotHelpers_WhenLateCleanupLeavesSourceAndTargetOffLattice_ShouldSnapAssignedSlots()
|
||||
{
|
||||
var source = new ElkPositionedNode
|
||||
{
|
||||
Id = "source",
|
||||
Label = "Internal Notification",
|
||||
Kind = "TransportCall",
|
||||
X = 3034,
|
||||
Y = 653.352783203125,
|
||||
Width = 208,
|
||||
Height = 88,
|
||||
};
|
||||
var handled = new ElkPositionedNode
|
||||
{
|
||||
Id = "handled",
|
||||
Label = "Set internalNotificationFailed",
|
||||
Kind = "SetState",
|
||||
X = 3406,
|
||||
Y = 653.352783203125,
|
||||
Width = 208,
|
||||
Height = 88,
|
||||
};
|
||||
var hasRecipients = new ElkPositionedNode
|
||||
{
|
||||
Id = "hasRecipients",
|
||||
Label = "Has Recipients",
|
||||
Kind = "Decision",
|
||||
X = 3778,
|
||||
Y = 631.352783203125,
|
||||
Width = 188,
|
||||
Height = 132,
|
||||
};
|
||||
var sourceA = new ElkPositionedNode
|
||||
{
|
||||
Id = "source-a",
|
||||
Label = "Source A",
|
||||
Kind = "TransportCall",
|
||||
X = 2100,
|
||||
Y = 420,
|
||||
Width = 208,
|
||||
Height = 132,
|
||||
};
|
||||
var sourceB = new ElkPositionedNode
|
||||
{
|
||||
Id = "source-b",
|
||||
Label = "Source B",
|
||||
Kind = "TransportCall",
|
||||
X = 2362,
|
||||
Y = 330,
|
||||
Width = 208,
|
||||
Height = 132,
|
||||
};
|
||||
var target = new ElkPositionedNode
|
||||
{
|
||||
Id = "target",
|
||||
Label = "Set batchGenerateFailed",
|
||||
Kind = "SetState",
|
||||
X = 2662,
|
||||
Y = 479.4360656738281,
|
||||
Width = 208,
|
||||
Height = 88,
|
||||
};
|
||||
|
||||
var sourceSlots = ElkBoundarySlots.BuildAssignedBoundarySlotCoordinates(source, "right", 2);
|
||||
var targetSlots = ElkBoundarySlots.BuildAssignedBoundarySlotCoordinates(target, "left", 2);
|
||||
var outgoingDirect = new ElkRoutedEdge
|
||||
{
|
||||
Id = "edge/26",
|
||||
SourceNodeId = source.Id,
|
||||
TargetNodeId = handled.Id,
|
||||
Sections =
|
||||
[
|
||||
new ElkEdgeSection
|
||||
{
|
||||
StartPoint = new ElkPoint { X = 3242, Y = 697.352783203125 },
|
||||
EndPoint = new ElkPoint { X = 3406, Y = 697.352783203125 },
|
||||
BendPoints = [],
|
||||
},
|
||||
],
|
||||
};
|
||||
var outgoingBranch = new ElkRoutedEdge
|
||||
{
|
||||
Id = "edge/27",
|
||||
SourceNodeId = source.Id,
|
||||
TargetNodeId = hasRecipients.Id,
|
||||
Sections =
|
||||
[
|
||||
new ElkEdgeSection
|
||||
{
|
||||
StartPoint = new ElkPoint { X = 3242, Y = 697.352783203125 },
|
||||
EndPoint = new ElkPoint { X = 3806.7594033898904, Y = 717.5455557960269 },
|
||||
BendPoints =
|
||||
[
|
||||
new ElkPoint { X = 3266, Y = 697.352783203125 },
|
||||
new ElkPoint { X = 3266, Y = 828.1806263316762 },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
var incomingDirect = new ElkRoutedEdge
|
||||
{
|
||||
Id = "edge/direct",
|
||||
SourceNodeId = sourceA.Id,
|
||||
TargetNodeId = target.Id,
|
||||
Sections =
|
||||
[
|
||||
new ElkEdgeSection
|
||||
{
|
||||
StartPoint = new ElkPoint { X = sourceA.X + sourceA.Width, Y = 523.4360656738281 },
|
||||
EndPoint = new ElkPoint { X = target.X, Y = 523.4360656738281 },
|
||||
BendPoints = [],
|
||||
},
|
||||
],
|
||||
};
|
||||
var incomingElbow = new ElkRoutedEdge
|
||||
{
|
||||
Id = "edge/elbowed",
|
||||
SourceNodeId = sourceB.Id,
|
||||
TargetNodeId = target.Id,
|
||||
Sections =
|
||||
[
|
||||
new ElkEdgeSection
|
||||
{
|
||||
StartPoint = new ElkPoint { X = sourceB.X + sourceB.Width, Y = 390 },
|
||||
EndPoint = new ElkPoint { X = target.X, Y = targetSlots[1] },
|
||||
BendPoints =
|
||||
[
|
||||
new ElkPoint { X = 2550, Y = 390 },
|
||||
new ElkPoint { X = 2550, Y = targetSlots[1] },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
var edges = new[] { outgoingDirect, outgoingBranch, incomingDirect, incomingElbow };
|
||||
var nodes = new[] { source, handled, hasRecipients, sourceA, sourceB, target };
|
||||
ElkEdgeRoutingScoring.CountBoundarySlotViolations(edges, nodes)
|
||||
.Should().BeGreaterThan(0);
|
||||
|
||||
var repaired = ElkEdgePostProcessor.SnapBoundarySlotAssignments(edges, nodes, 53d);
|
||||
ElkEdgeRoutingScoring.CountBoundarySlotViolations(repaired, nodes)
|
||||
.Should().Be(0);
|
||||
repaired.Single(edge => edge.Id == "edge/26").Sections.Single().StartPoint.Y.Should().Be(sourceSlots[0]);
|
||||
repaired.Single(edge => edge.Id == "edge/27").Sections.Single().StartPoint.Y.Should().Be(sourceSlots[1]);
|
||||
repaired.Single(edge => edge.Id == "edge/direct").Sections.Single().EndPoint.Y.Should().Be(targetSlots[0]);
|
||||
repaired.Single(edge => edge.Id == "edge/elbowed").Sections.Single().EndPoint.Y.Should().Be(targetSlots[1]);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Property("Intent", "Operational")]
|
||||
public void MixedNodeFaceHelpers_WhenIncomingAndOutgoingEdgesShareTheSameFaceLane_ShouldSeparateThem()
|
||||
{
|
||||
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 = 3439.84, Y = 267.421640625 },
|
||||
EndPoint = new ElkPoint { X = 1200, Y = 267.421640625 },
|
||||
BendPoints = [],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
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 = 1308.0475075276013, Y = 222.88924788024843 },
|
||||
BendPoints =
|
||||
[
|
||||
new ElkPoint { X = 1282.5912611218437, Y = 269.181640625 },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
var nodes = new[] { process, validateSuccess, join };
|
||||
ElkEdgeRoutingScoring.CountSharedLaneViolations([incoming, outgoing], nodes)
|
||||
.Should().Be(1);
|
||||
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);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Property("Intent", "Operational")]
|
||||
public void BoundarySlotHelpers_WhenGatewaySourceExitsShareARightFace_ShouldSnapToDistinctGatewaySlots()
|
||||
{
|
||||
var retryDecision = new ElkPositionedNode
|
||||
{
|
||||
Id = "retry",
|
||||
Label = "Retry Decision",
|
||||
Kind = "Decision",
|
||||
X = 1976,
|
||||
Y = 413.9718017578125,
|
||||
Width = 188,
|
||||
Height = 132,
|
||||
};
|
||||
|
||||
var cooldownTimer = new ElkPositionedNode
|
||||
{
|
||||
Id = "cooldown",
|
||||
Label = "Cooldown Timer",
|
||||
Kind = "Timer",
|
||||
X = 2290,
|
||||
Y = 457.1265563964844,
|
||||
Width = 208,
|
||||
Height = 88,
|
||||
};
|
||||
|
||||
var setBatchGenerateFailed = new ElkPositionedNode
|
||||
{
|
||||
Id = "failed",
|
||||
Label = "Set batchGenerateFailed",
|
||||
Kind = "SetState",
|
||||
X = 2662,
|
||||
Y = 479.4360656738281,
|
||||
Width = 208,
|
||||
Height = 88,
|
||||
};
|
||||
|
||||
var whenNotExceeded = new ElkRoutedEdge
|
||||
{
|
||||
Id = "edge/8",
|
||||
SourceNodeId = retryDecision.Id,
|
||||
TargetNodeId = cooldownTimer.Id,
|
||||
Label = "when notstate.printInsisAttempt gt 2",
|
||||
Sections =
|
||||
[
|
||||
new ElkEdgeSection
|
||||
{
|
||||
StartPoint = new ElkPoint { X = 2148.8381635762003, Y = 490.61734648090606 },
|
||||
EndPoint = new ElkPoint { X = 2290, Y = 490.61734648090606 },
|
||||
BendPoints = [],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
var defaultExit = new ElkRoutedEdge
|
||||
{
|
||||
Id = "edge/9",
|
||||
SourceNodeId = retryDecision.Id,
|
||||
TargetNodeId = setBatchGenerateFailed.Id,
|
||||
Label = "default",
|
||||
Sections =
|
||||
[
|
||||
new ElkEdgeSection
|
||||
{
|
||||
StartPoint = new ElkPoint { X = 2151.2098609190243, Y = 488.95211217636984 },
|
||||
EndPoint = new ElkPoint { X = 2662, Y = 545.4360656738281 },
|
||||
BendPoints =
|
||||
[
|
||||
new ElkPoint { X = 2172, Y = 488.95211217636984 },
|
||||
new ElkPoint { X = 2172, Y = 545.4360656738281 },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
var nodes = new[] { retryDecision, cooldownTimer, setBatchGenerateFailed };
|
||||
ElkEdgeRoutingScoring.CountBoundarySlotViolations([whenNotExceeded, defaultExit], nodes)
|
||||
.Should().BeGreaterThan(0);
|
||||
|
||||
var repaired = ElkEdgePostProcessor.SnapBoundarySlotAssignments([whenNotExceeded, defaultExit], nodes, 53d);
|
||||
ElkEdgeRoutingScoring.CountBoundarySlotViolations(repaired, nodes)
|
||||
.Should().Be(0);
|
||||
|
||||
var expectedSlots = ElkBoundarySlots.BuildAssignedBoundarySlotAxisCoordinates(retryDecision, "right", 2);
|
||||
repaired.Select(edge => edge.Sections.Single().StartPoint.Y)
|
||||
.OrderBy(value => value)
|
||||
.Should().Equal(expectedSlots.OrderBy(value => value));
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Property("Intent", "Operational")]
|
||||
public void BoundarySlotHelpers_WhenGatewayRightFaceSlotMustRejoinSafeUpperLane_ShouldSnapWithoutCrossingBlocker()
|
||||
{
|
||||
var retryDecision = new ElkPositionedNode
|
||||
{
|
||||
Id = "retry",
|
||||
Label = "Retry Decision",
|
||||
Kind = "Decision",
|
||||
X = 1976,
|
||||
Y = 413.9718017578125,
|
||||
Width = 188,
|
||||
Height = 132,
|
||||
};
|
||||
|
||||
var cooldownTimer = new ElkPositionedNode
|
||||
{
|
||||
Id = "cooldown",
|
||||
Label = "Cooldown Timer",
|
||||
Kind = "Timer",
|
||||
X = 2290,
|
||||
Y = 457.1265563964844,
|
||||
Width = 208,
|
||||
Height = 88,
|
||||
};
|
||||
|
||||
var setBatchGenerateFailed = new ElkPositionedNode
|
||||
{
|
||||
Id = "failed",
|
||||
Label = "Set batchGenerateFailed",
|
||||
Kind = "SetState",
|
||||
X = 2662,
|
||||
Y = 479.4360656738281,
|
||||
Width = 208,
|
||||
Height = 88,
|
||||
};
|
||||
|
||||
var whenNotExceeded = new ElkRoutedEdge
|
||||
{
|
||||
Id = "edge/8",
|
||||
SourceNodeId = retryDecision.Id,
|
||||
TargetNodeId = cooldownTimer.Id,
|
||||
Label = "when notstate.printInsisAttempt gt 2",
|
||||
Sections =
|
||||
[
|
||||
new ElkEdgeSection
|
||||
{
|
||||
StartPoint = new ElkPoint { X = 2148.8381635762003, Y = 490.61734648090606 },
|
||||
EndPoint = new ElkPoint { X = 2290, Y = 490.61734648090606 },
|
||||
BendPoints = [],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
var defaultExit = new ElkRoutedEdge
|
||||
{
|
||||
Id = "edge/9",
|
||||
SourceNodeId = retryDecision.Id,
|
||||
TargetNodeId = setBatchGenerateFailed.Id,
|
||||
Label = "default",
|
||||
Sections =
|
||||
[
|
||||
new ElkEdgeSection
|
||||
{
|
||||
StartPoint = new ElkPoint { X = 2103.8402987865636, Y = 437.73227685820864 },
|
||||
EndPoint = new ElkPoint { X = 2598.726806640625, Y = 479.4360656738281 },
|
||||
BendPoints =
|
||||
[
|
||||
new ElkPoint { X = 2598.726806640625, Y = 437.73227685820864 },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
var nodes = new[] { retryDecision, cooldownTimer, setBatchGenerateFailed };
|
||||
ElkEdgeRoutingScoring.CountBoundarySlotViolations([whenNotExceeded, defaultExit], nodes)
|
||||
.Should().BeGreaterThan(0);
|
||||
|
||||
var repaired = ElkEdgePostProcessor.SnapBoundarySlotAssignments(
|
||||
[whenNotExceeded, defaultExit],
|
||||
nodes,
|
||||
53d,
|
||||
enforceAllNodeEndpoints: true);
|
||||
var repairedEdge8Path = ExtractPath(repaired.Single(edge => edge.Id == "edge/8"));
|
||||
var repairedEdge9Path = ExtractPath(repaired.Single(edge => edge.Id == "edge/9"));
|
||||
var repairedDefaultPath = ExtractPath(repaired.Single(edge => edge.Id == "edge/9"));
|
||||
var pathText = string.Join(" -> ", repairedDefaultPath.Select(point => $"({point.X:F3},{point.Y:F3})"));
|
||||
|
||||
ElkEdgeRoutingScoring.CountBoundarySlotViolations(repaired, nodes)
|
||||
.Should().Be(0, pathText);
|
||||
ElkEdgeRoutingScoring.CountGatewaySourceExitViolations(repaired, nodes)
|
||||
.Should().Be(0, pathText);
|
||||
ElkEdgeRoutingScoring.CountEdgeNodeCrossings(repaired, nodes, null)
|
||||
.Should().Be(0, pathText);
|
||||
|
||||
var expectedSlots = ElkBoundarySlots.BuildAssignedBoundarySlotAxisCoordinates(retryDecision, "right", 2)
|
||||
.OrderBy(value => value)
|
||||
.ToArray();
|
||||
repairedDefaultPath[0].Y.Should().BeApproximately(expectedSlots[0], 0.5d, pathText);
|
||||
repairedDefaultPath.Any(point => point.Y < cooldownTimer.Y - 0.5d)
|
||||
.Should().BeTrue(pathText);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Property("Intent", "Operational")]
|
||||
public void BoundarySlotHelpers_WhenStrictSnapSeesSingleRepeatCorridorExit_ShouldCenterTheDepartureSlot()
|
||||
{
|
||||
var repeatDecision = new ElkPositionedNode
|
||||
{
|
||||
Id = "repeat",
|
||||
Label = "Repeat Decision",
|
||||
Kind = "Decision",
|
||||
X = 3000,
|
||||
Y = 320,
|
||||
Width = 188,
|
||||
Height = 132,
|
||||
};
|
||||
|
||||
var target = new ElkPositionedNode
|
||||
{
|
||||
Id = "target",
|
||||
Label = "Internal Discussion",
|
||||
Kind = "TransportCall",
|
||||
X = 3420,
|
||||
Y = 340,
|
||||
Width = 208,
|
||||
Height = 88,
|
||||
};
|
||||
|
||||
var repeatReturn = new ElkRoutedEdge
|
||||
{
|
||||
Id = "edge/repeat-corridor",
|
||||
SourceNodeId = repeatDecision.Id,
|
||||
TargetNodeId = target.Id,
|
||||
Label = "repeat while state.printInsisAttempt eq 0",
|
||||
Sections =
|
||||
[
|
||||
new ElkEdgeSection
|
||||
{
|
||||
StartPoint = new ElkPoint { X = 3188, Y = 348 },
|
||||
EndPoint = new ElkPoint { X = 3420, Y = 384 },
|
||||
BendPoints =
|
||||
[
|
||||
new ElkPoint { X = 3228, Y = 348 },
|
||||
new ElkPoint { X = 3228, Y = 240 },
|
||||
new ElkPoint { X = 3420, Y = 240 },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
var nodes = new[] { repeatDecision, target };
|
||||
ElkEdgeRoutingScoring.CountBoundarySlotViolations([repeatReturn], nodes)
|
||||
.Should().Be(1);
|
||||
|
||||
var repaired = ElkEdgePostProcessor.SnapBoundarySlotAssignments(
|
||||
[repeatReturn],
|
||||
nodes,
|
||||
53d,
|
||||
enforceAllNodeEndpoints: true);
|
||||
|
||||
ElkEdgeRoutingScoring.CountBoundarySlotViolations(repaired, nodes)
|
||||
.Should().Be(0);
|
||||
|
||||
var expectedStartY = ElkBoundarySlots.BuildAssignedBoundarySlotAxisCoordinates(repeatDecision, "right", 1).Single();
|
||||
repaired.Single().Sections.Single().StartPoint.Y.Should().BeApproximately(expectedStartY, 0.5d);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user