ElkSharp edge routing: boundary slots, gateway repairs, corridor spacing
Major edge routing improvements including corridor spacing, crossing reduction, focused gateway boundary repairs, setter families, and advanced restabilization. Adds workflow renderer tests for document-processing and artifact inspection. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -509,6 +509,7 @@ public partial class DocumentProcessingWorkflowRenderingTests
|
||||
$"subset [{string.Join(", ", result.Focus)}]: detour={result.Detour} gateway-source={result.GatewaySource} boundary-slots={result.BoundarySlots} entry={result.Entry} shared-lanes={result.SharedLanes}");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static string RenderLatestElkSharpArtifactForInspection()
|
||||
|
||||
@@ -908,8 +908,9 @@ public partial class DocumentProcessingWorkflowRenderingTests
|
||||
return false;
|
||||
}
|
||||
|
||||
var boundary = new ElkPoint { X = path[0].X, Y = path[0].Y };
|
||||
return ElkShapeBoundaries.IsNearGatewayVertex(ToElkNode(sourceNode), boundary);
|
||||
return ElkEdgePostProcessor.HasProblematicGatewaySourceVertexExit(
|
||||
path.Select(point => new ElkPoint { X = point.X, Y = point.Y }).ToArray(),
|
||||
ToElkNode(sourceNode));
|
||||
}
|
||||
|
||||
private static bool HasGatewaySourceScoringIssue(
|
||||
@@ -1315,4 +1316,4 @@ public partial class DocumentProcessingWorkflowRenderingTests
|
||||
|
||||
return ResolveBoundarySide(boundaryPoint, node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,6 +136,68 @@ public partial class ElkSharpEdgeRefinementTests
|
||||
ElkShapeBoundaries.IsGatewayBoundaryPoint(join, shifted).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Property("Intent", "Operational")]
|
||||
public void GatewayBoundaryHelpers_WhenJoinTargetUsesBottomHookIntoDiagonalFace_ShouldRebuildBottomEntry()
|
||||
{
|
||||
var source = new ElkPositionedNode
|
||||
{
|
||||
Id = "source",
|
||||
Label = "Process Batch",
|
||||
Kind = "Repeat",
|
||||
X = 992,
|
||||
Y = 268.311,
|
||||
Width = 208,
|
||||
Height = 88,
|
||||
};
|
||||
|
||||
var target = new ElkPositionedNode
|
||||
{
|
||||
Id = "join",
|
||||
Label = "Parallel Execution Join",
|
||||
Kind = "Join",
|
||||
X = 1290,
|
||||
Y = 188.733,
|
||||
Width = 176,
|
||||
Height = 124,
|
||||
};
|
||||
|
||||
var edge = new ElkRoutedEdge
|
||||
{
|
||||
Id = "edge/17",
|
||||
SourceNodeId = source.Id,
|
||||
TargetNodeId = target.Id,
|
||||
Sections =
|
||||
[
|
||||
new ElkEdgeSection
|
||||
{
|
||||
StartPoint = new ElkPoint { X = 1200, Y = 290.311 },
|
||||
EndPoint = new ElkPoint { X = 1302.152, Y = 280.561 },
|
||||
BendPoints =
|
||||
[
|
||||
new ElkPoint { X = 1302.152, Y = 290.311 },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
var originalSection = edge.Sections.Single();
|
||||
|
||||
ElkShapeBoundaries.HasValidGatewayBoundaryAngle(
|
||||
target,
|
||||
originalSection.EndPoint,
|
||||
originalSection.BendPoints.Single()).Should().BeFalse();
|
||||
|
||||
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);
|
||||
|
||||
ElkShapeBoundaries.HasValidGatewayBoundaryAngle(target, path[^1], path[^2]).Should().BeTrue();
|
||||
ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(target, path[^2]).Should().BeFalse();
|
||||
ElkShapeBoundaries.IsGatewayBoundaryPoint(target, path[^1]).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Property("Intent", "Operational")]
|
||||
public void GatewayBoundaryHelpers_WhenJoinSourceStartsAtTip_ShouldCountVertexExitViolation()
|
||||
|
||||
@@ -1603,4 +1603,228 @@ public partial class ElkSharpEdgeRefinementTests
|
||||
.Should()
|
||||
.BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Property("Intent", "Operational")]
|
||||
public void EndTerminalFamilyHelpers_WhenRepeatRoofFamilyOccupiesOuterBands_ShouldKeepEndRoofFamilyAboveIt()
|
||||
{
|
||||
var repeatSource = new ElkPositionedNode
|
||||
{
|
||||
Id = "start/2/branch-1/1/body/5",
|
||||
Label = "Check Result",
|
||||
Kind = "Decision",
|
||||
X = 3034,
|
||||
Y = 325.88,
|
||||
Width = 188,
|
||||
Height = 132,
|
||||
};
|
||||
|
||||
var repeatTarget = new ElkPositionedNode
|
||||
{
|
||||
Id = "start/2/branch-1/1",
|
||||
Label = "Process Batch",
|
||||
Kind = "Repeat",
|
||||
X = 992,
|
||||
Y = 268.31,
|
||||
Width = 208,
|
||||
Height = 88,
|
||||
};
|
||||
|
||||
var sourceFailure = new ElkPositionedNode
|
||||
{
|
||||
Id = "start/3",
|
||||
Label = "Load Configuration",
|
||||
Kind = "TransportCall",
|
||||
X = 1604,
|
||||
Y = 145.16,
|
||||
Width = 208,
|
||||
Height = 88,
|
||||
};
|
||||
|
||||
var sourceDefault = new ElkPositionedNode
|
||||
{
|
||||
Id = "start/9",
|
||||
Label = "Evaluate Conditions",
|
||||
Kind = "Decision",
|
||||
X = 2290,
|
||||
Y = 34.75,
|
||||
Width = 188,
|
||||
Height = 132,
|
||||
};
|
||||
|
||||
var end = new ElkPositionedNode
|
||||
{
|
||||
Id = "end",
|
||||
Label = "End",
|
||||
Kind = "End",
|
||||
X = 4864,
|
||||
Y = 364.87,
|
||||
Width = 264,
|
||||
Height = 132,
|
||||
};
|
||||
|
||||
var repeatReturn = new ElkRoutedEdge
|
||||
{
|
||||
Id = "edge/14",
|
||||
SourceNodeId = repeatSource.Id,
|
||||
TargetNodeId = repeatTarget.Id,
|
||||
Label = "repeat while state.printInsisAttempt eq 0",
|
||||
Sections =
|
||||
[
|
||||
new ElkEdgeSection
|
||||
{
|
||||
StartPoint = new ElkPoint { X = 3098, Y = 346.95 },
|
||||
EndPoint = new ElkPoint { X = 1069.33, Y = 268.31 },
|
||||
BendPoints =
|
||||
[
|
||||
new ElkPoint { X = 3098, Y = -202.7 },
|
||||
new ElkPoint { X = 1069.33, Y = -202.7 },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
var failureArrival = new ElkRoutedEdge
|
||||
{
|
||||
Id = "edge/20",
|
||||
SourceNodeId = sourceFailure.Id,
|
||||
TargetNodeId = end.Id,
|
||||
Label = "on failure / timeout",
|
||||
Sections =
|
||||
[
|
||||
new ElkEdgeSection
|
||||
{
|
||||
StartPoint = new ElkPoint { X = 1812, Y = 211.1552734375 },
|
||||
EndPoint = new ElkPoint { X = 4864, Y = 376.8660888671875 },
|
||||
BendPoints =
|
||||
[
|
||||
new ElkPoint { X = 1836, Y = 211.1552734375 },
|
||||
new ElkPoint { X = 1836, Y = -30.25 },
|
||||
new ElkPoint { X = 4759, Y = -30.25 },
|
||||
new ElkPoint { X = 4759, Y = 376.8660888671875 },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
var defaultArrival = new ElkRoutedEdge
|
||||
{
|
||||
Id = "edge/23",
|
||||
SourceNodeId = sourceDefault.Id,
|
||||
TargetNodeId = end.Id,
|
||||
Label = "default",
|
||||
Sections =
|
||||
[
|
||||
new ElkEdgeSection
|
||||
{
|
||||
StartPoint = new ElkPoint { X = 2464.06, Y = 110.54 },
|
||||
EndPoint = new ElkPoint { X = 4864, Y = 403.87 },
|
||||
BendPoints =
|
||||
[
|
||||
new ElkPoint { X = 2502, Y = 110.54 },
|
||||
new ElkPoint { X = 2502, Y = 16.75 },
|
||||
new ElkPoint { X = 4820, Y = 16.75 },
|
||||
new ElkPoint { X = 4820, Y = 403.87 },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
static double FindAboveGraphLaneY(ElkRoutedEdge edge, double graphMinY)
|
||||
{
|
||||
var path = ExtractPath(edge);
|
||||
var bestLength = double.NegativeInfinity;
|
||||
var bestY = double.NaN;
|
||||
for (var i = 0; i < path.Count - 1; i++)
|
||||
{
|
||||
if (Math.Abs(path[i].Y - path[i + 1].Y) > 0.5d
|
||||
|| path[i].Y >= graphMinY - 8d)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var length = Math.Abs(path[i + 1].X - path[i].X);
|
||||
if (length <= bestLength)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
bestLength = length;
|
||||
bestY = path[i].Y;
|
||||
}
|
||||
|
||||
double.IsNaN(bestY).Should().BeFalse();
|
||||
return bestY;
|
||||
}
|
||||
|
||||
var nodes = new[] { repeatSource, repeatTarget, sourceFailure, sourceDefault, end };
|
||||
var repaired = ElkEdgePostProcessor.DistributeEndTerminalLeftFaceTrunks(
|
||||
[repeatReturn, failureArrival, defaultArrival],
|
||||
nodes,
|
||||
53d);
|
||||
|
||||
var graphMinY = nodes.Min(node => node.Y);
|
||||
var repeatY = FindAboveGraphLaneY(repaired.Single(edge => edge.Id == "edge/14"), graphMinY);
|
||||
var repairedFailureY = FindAboveGraphLaneY(repaired.Single(edge => edge.Id == "edge/20"), graphMinY);
|
||||
var repairedDefaultY = FindAboveGraphLaneY(repaired.Single(edge => edge.Id == "edge/23"), graphMinY);
|
||||
|
||||
repairedFailureY.Should().BeLessThan(repeatY);
|
||||
repairedDefaultY.Should().BeLessThan(repeatY);
|
||||
|
||||
// The End edges' vertical exits at X within the repeat return's horizontal span
|
||||
// create 2 topologically unavoidable crossings. The repair must eliminate the
|
||||
// remaining approach-area crossings (baseline has 4).
|
||||
var repairedCrossings = ElkEdgeRoutingScoring.ComputeScore(repaired, nodes).EdgeCrossings;
|
||||
repairedCrossings.Should().BeLessThanOrEqualTo(2);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Property("Intent", "Operational")]
|
||||
public void LabelProximityScoring_WhenLongestAnchorSegmentIsLongEnough_ShouldIgnoreShortFirstStub()
|
||||
{
|
||||
var source = new ElkPositionedNode
|
||||
{
|
||||
Id = "source",
|
||||
Label = "Source",
|
||||
Kind = "TransportCall",
|
||||
X = 0,
|
||||
Y = 0,
|
||||
Width = 160,
|
||||
Height = 80,
|
||||
};
|
||||
|
||||
var target = new ElkPositionedNode
|
||||
{
|
||||
Id = "target",
|
||||
Label = "Target",
|
||||
Kind = "SetState",
|
||||
X = 420,
|
||||
Y = 260,
|
||||
Width = 176,
|
||||
Height = 88,
|
||||
};
|
||||
|
||||
var edge = new ElkRoutedEdge
|
||||
{
|
||||
Id = "edge/labeled",
|
||||
SourceNodeId = source.Id,
|
||||
TargetNodeId = target.Id,
|
||||
Label = "on failure / timeout",
|
||||
Sections =
|
||||
[
|
||||
new ElkEdgeSection
|
||||
{
|
||||
StartPoint = new ElkPoint { X = 160, Y = 40 },
|
||||
EndPoint = new ElkPoint { X = 420, Y = 304 },
|
||||
BendPoints =
|
||||
[
|
||||
new ElkPoint { X = 184, Y = 40 },
|
||||
new ElkPoint { X = 184, Y = 304 },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
ElkEdgeRoutingScoring.CountLabelProximityViolations([edge], [source, target]).Should().Be(0);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user