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:
master
2026-04-06 08:52:02 +03:00
parent e3e87942c7
commit 5d6435fdb2
41 changed files with 8334 additions and 246 deletions

View File

@@ -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()

View File

@@ -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);
}
}
}

View File

@@ -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()

View File

@@ -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);
}
}