Improve rendering
This commit is contained in:
@@ -188,7 +188,7 @@ public sealed class WorkflowRenderSvgRenderer
|
||||
],
|
||||
trunkStyle,
|
||||
collectorStrokeWidth,
|
||||
highway.IsBackward ? null : trunkStyle.MarkerId,
|
||||
trunkStyle.MarkerId,
|
||||
collectorOpacity,
|
||||
highway.GroupId,
|
||||
IsCollector: true));
|
||||
@@ -251,7 +251,29 @@ public sealed class WorkflowRenderSvgRenderer
|
||||
}
|
||||
|
||||
var renderedPath = renderedPaths[pathIndex];
|
||||
var pathData = BuildRoundedEdgePath(renderedPath.Points, offsetX, offsetY, renderedPath.IsCollector ? 0d : 12d);
|
||||
var renderPoints = renderedPath.Points;
|
||||
if (!string.IsNullOrWhiteSpace(renderedPath.MarkerId) && renderPoints.Count >= 2)
|
||||
{
|
||||
var arrowLen = 5d * renderedPath.StrokeWidth;
|
||||
var last = renderPoints[^1];
|
||||
var prev = renderPoints[^2];
|
||||
var dx = last.X - prev.X;
|
||||
var dy = last.Y - prev.Y;
|
||||
var segLen = Math.Sqrt(dx * dx + dy * dy);
|
||||
if (segLen > arrowLen * 0.5d)
|
||||
{
|
||||
var pullback = Math.Min(arrowLen * 0.7d, segLen * 0.6d);
|
||||
var shortened = renderPoints.ToList();
|
||||
shortened[^1] = new WorkflowRenderPoint
|
||||
{
|
||||
X = last.X - (dx / segLen * pullback),
|
||||
Y = last.Y - (dy / segLen * pullback),
|
||||
};
|
||||
renderPoints = shortened;
|
||||
}
|
||||
}
|
||||
|
||||
var pathData = BuildRoundedEdgePath(renderPoints, offsetX, offsetY, renderedPath.IsCollector ? 0d : 12d);
|
||||
var markerAttribute = string.IsNullOrWhiteSpace(renderedPath.MarkerId)
|
||||
? string.Empty
|
||||
: $" marker-end=\"{renderedPath.MarkerId}\"";
|
||||
@@ -491,7 +513,7 @@ public sealed class WorkflowRenderSvgRenderer
|
||||
<text x="{Format(placement.CenterX)}" y="{Format(placement.Top + 14)}"
|
||||
text-anchor="middle"
|
||||
font-family="'Segoe UI', sans-serif"
|
||||
font-size="10.7"
|
||||
font-size="11.5"
|
||||
font-weight="700"
|
||||
fill="{placement.Style.LabelText}">{Encode(placement.Label)}</text>
|
||||
""");
|
||||
@@ -869,10 +891,11 @@ public sealed class WorkflowRenderSvgRenderer
|
||||
double anchorY;
|
||||
if (isErrorLabel && points.Count >= 2)
|
||||
{
|
||||
var sourcePoint = points[0];
|
||||
var secondPoint = points[Math.Min(1, points.Count - 1)];
|
||||
anchorX = (sourcePoint.X * 0.6d + secondPoint.X * 0.4d) + offsetX;
|
||||
anchorY = (sourcePoint.Y * 0.6d + secondPoint.Y * 0.4d) + offsetY;
|
||||
var longestSeg = ResolveLabelAnchorSegment(points);
|
||||
var segMidX = (longestSeg.Start.X + longestSeg.End.X) / 2d;
|
||||
var segMidY = (longestSeg.Start.Y + longestSeg.End.Y) / 2d;
|
||||
anchorX = segMidX + offsetX;
|
||||
anchorY = segMidY + offsetY;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1256,9 +1279,10 @@ public sealed class WorkflowRenderSvgRenderer
|
||||
group.First().FamilyKey,
|
||||
isBackward);
|
||||
})
|
||||
.Where(candidate => candidate.IsBackward || !string.Equals(candidate.TargetNode.Kind, "End", StringComparison.OrdinalIgnoreCase))
|
||||
.Where(candidate => candidate.IsBackward
|
||||
|| string.Equals(candidate.TargetNode.Kind, "End", StringComparison.OrdinalIgnoreCase))
|
||||
.Where(candidate =>
|
||||
candidate.EdgeIds.Count >= (string.Equals(candidate.TargetNode.Kind, "End", StringComparison.OrdinalIgnoreCase) ? 2 : 3))
|
||||
candidate.EdgeIds.Count >= (candidate.IsBackward ? 3 : 2))
|
||||
.ToArray();
|
||||
|
||||
foreach (var targetDirectionGroup in candidateGroups
|
||||
@@ -1291,20 +1315,45 @@ public sealed class WorkflowRenderSvgRenderer
|
||||
.FirstOrDefault();
|
||||
collectorY = preferredCollectorY
|
||||
?? (targetNode.Y - 42d - ((orderedGroups.Length - 1) * 4d));
|
||||
var requiredOverlapCount = candidate.EdgeIds.Count <= 2 ? candidate.EdgeIds.Count : (candidate.EdgeIds.Count / 2) + 1;
|
||||
if (!TryResolveHorizontalOverlapInterval(candidate.Edges, collectorY, requiredOverlapCount, out var sharedMinX, out var sharedMaxX))
|
||||
|
||||
// Ensure collectorY doesn't pass through any node
|
||||
var collectorMinX = candidate.Edges.Min(edge => edge.Sections.First().EndPoint.X);
|
||||
var collectorMaxX = candidate.Edges.Max(edge => edge.Sections.First().StartPoint.X);
|
||||
foreach (var obstacleNode in layout.Nodes)
|
||||
{
|
||||
collectorX = targetNode.X + (targetNode.Width / 2d)
|
||||
+ ResolveCenteredOffset(bandIndex, orderedGroups.Length, Math.Min(18d, targetNode.Width / 4d));
|
||||
targetX = collectorX;
|
||||
if (string.Equals(obstacleNode.Id, candidate.TargetId, StringComparison.Ordinal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (obstacleNode.X + obstacleNode.Width > collectorMinX
|
||||
&& obstacleNode.X < collectorMaxX
|
||||
&& collectorY > obstacleNode.Y - 24d
|
||||
&& collectorY < obstacleNode.Y + obstacleNode.Height + 24d)
|
||||
{
|
||||
collectorY = obstacleNode.Y - 36d;
|
||||
}
|
||||
}
|
||||
|
||||
var requiredOverlapCount = candidate.EdgeIds.Count <= 2 ? candidate.EdgeIds.Count : (candidate.EdgeIds.Count / 2) + 1;
|
||||
if (!TryResolveHorizontalOverlapInterval(candidate.Edges, collectorY, requiredOverlapCount, out var sharedMinX, out var sharedMaxX)
|
||||
|| Math.Abs(sharedMaxX - sharedMinX) < 40d)
|
||||
{
|
||||
collectorX = targetNode.X + (targetNode.Width / 2d) + 24d;
|
||||
targetX = targetNode.X + (targetNode.Width / 2d);
|
||||
}
|
||||
else
|
||||
{
|
||||
collectorX = sharedMaxX;
|
||||
targetX = sharedMinX;
|
||||
targetX = Math.Min(sharedMinX, targetNode.X + (targetNode.Width / 2d));
|
||||
}
|
||||
|
||||
targetY = targetNode.Y;
|
||||
if (Math.Abs(collectorY - targetY) < 28d)
|
||||
{
|
||||
collectorY = targetY - 32d;
|
||||
}
|
||||
|
||||
spreadPerEdge = 0d;
|
||||
|
||||
groups[groupId] = new HighwayGroup(
|
||||
|
||||
@@ -8,39 +8,39 @@ using StellaOps.Workflow.Renderer.Svg;
|
||||
namespace StellaOps.Workflow.Renderer.Tests;
|
||||
|
||||
[TestFixture]
|
||||
public class AssistantPrintInsisDocumentsRenderingTests
|
||||
public class DocumentProcessingWorkflowRenderingTests
|
||||
{
|
||||
private static WorkflowRenderGraph BuildAssistantPrintInsisDocumentsGraph()
|
||||
private static WorkflowRenderGraph BuildDocumentProcessingWorkflowGraph()
|
||||
{
|
||||
return new WorkflowRenderGraph
|
||||
{
|
||||
Id = "AssistantPrintInsisDocuments:1.0.0",
|
||||
Id = "DocumentProcessingWorkflow:1.0.0",
|
||||
Nodes =
|
||||
[
|
||||
new WorkflowRenderNode { Id = "start", Label = "Start", Kind = "Start", Width = 264, Height = 132 },
|
||||
new WorkflowRenderNode { Id = "start/1", Label = "Assign Business Reference", Kind = "BusinessReference", Width = 208, Height = 88 },
|
||||
new WorkflowRenderNode { Id = "start/2/split", Label = "Spin off async process", Kind = "Fork", Width = 176, Height = 124 },
|
||||
new WorkflowRenderNode { Id = "start/2/join", Label = "Spin off async process Join", Kind = "Join", Width = 176, Height = 124 },
|
||||
new WorkflowRenderNode { Id = "start/3", Label = "Load Notification Parameters", Kind = "TransportCall", Width = 208, Height = 88 },
|
||||
new WorkflowRenderNode { Id = "start/4/batched", Label = "Setting:\nnotificationParameters\nskipSystemNotification\ntoEmailsCount\nnotificationHasBody\nnotificationHasTitle", Kind = "SetState", Width = 224, Height = 104 },
|
||||
new WorkflowRenderNode { Id = "start/9", Label = "Has Notification Content", Kind = "Decision", Width = 188, Height = 132 },
|
||||
new WorkflowRenderNode { Id = "start/9/true/1", Label = "Send Private Note", Kind = "Decision", Width = 188, Height = 132 },
|
||||
new WorkflowRenderNode { Id = "start/9/true/1/true/1", Label = "Send Private Note", Kind = "TransportCall", Width = 208, Height = 88 },
|
||||
new WorkflowRenderNode { Id = "start/9/true/1/true/1/handled/1", Label = "Set notificationPrivateNoteFailed", Kind = "SetState", Width = 208, Height = 88 },
|
||||
new WorkflowRenderNode { Id = "start/9/true/2", Label = "Has Notification Emails", Kind = "Decision", Width = 188, Height = 132 },
|
||||
new WorkflowRenderNode { Id = "start/9/true/2/true/1", Label = "Send Notification Email", Kind = "TransportCall", Width = 208, Height = 88 },
|
||||
new WorkflowRenderNode { Id = "start/9/true/2/true/1/handled/1", Label = "Set notificationEmailFailed", Kind = "SetState", Width = 208, Height = 88 },
|
||||
new WorkflowRenderNode { Id = "start/1", Label = "Initialize Context", Kind = "BusinessReference", Width = 208, Height = 88 },
|
||||
new WorkflowRenderNode { Id = "start/2/split", Label = "Parallel Execution", Kind = "Fork", Width = 176, Height = 124 },
|
||||
new WorkflowRenderNode { Id = "start/2/join", Label = "Parallel Execution Join", Kind = "Join", Width = 176, Height = 124 },
|
||||
new WorkflowRenderNode { Id = "start/3", Label = "Load Configuration", Kind = "TransportCall", Width = 208, Height = 88 },
|
||||
new WorkflowRenderNode { Id = "start/4/batched", Label = "Setting:\nconfigParameters\nskipInternalNotification\nrecipientCount\nconfigHasBody\nconfigHasTitle", Kind = "SetState", Width = 224, Height = 104 },
|
||||
new WorkflowRenderNode { Id = "start/9", Label = "Evaluate Conditions", Kind = "Decision", Width = 188, Height = 132 },
|
||||
new WorkflowRenderNode { Id = "start/9/true/1", Label = "Internal Notification", Kind = "Decision", Width = 188, Height = 132 },
|
||||
new WorkflowRenderNode { Id = "start/9/true/1/true/1", Label = "Internal Notification", Kind = "TransportCall", Width = 208, Height = 88 },
|
||||
new WorkflowRenderNode { Id = "start/9/true/1/true/1/handled/1", Label = "Set internalNotificationFailed", Kind = "SetState", Width = 208, Height = 88 },
|
||||
new WorkflowRenderNode { Id = "start/9/true/2", Label = "Has Recipients", Kind = "Decision", Width = 188, Height = 132 },
|
||||
new WorkflowRenderNode { Id = "start/9/true/2/true/1", Label = "Email Dispatch", Kind = "TransportCall", Width = 208, Height = 88 },
|
||||
new WorkflowRenderNode { Id = "start/9/true/2/true/1/handled/1", Label = "Set emailDispatchFailed", Kind = "SetState", Width = 208, Height = 88 },
|
||||
new WorkflowRenderNode { Id = "end", Label = "End", Kind = "End", Width = 264, Height = 132 },
|
||||
new WorkflowRenderNode { Id = "start/2/branch-1/1", Label = "Generate Documents", Kind = "Repeat", Width = 208, Height = 88 },
|
||||
new WorkflowRenderNode { Id = "start/2/branch-1/1/body/1/batched", Label = "Setting:\nprintTimedOut\nprintGenerateFailed\nhasMissingDocuments", Kind = "SetState", Width = 224, Height = 104 },
|
||||
new WorkflowRenderNode { Id = "start/2/branch-1/1/body/4", Label = "Print Batch Documents", Kind = "TransportCall", Width = 208, Height = 88 },
|
||||
new WorkflowRenderNode { Id = "start/2/branch-1/1/body/4/failure/1", Label = "Attempt Again", Kind = "Decision", Width = 188, Height = 132 },
|
||||
new WorkflowRenderNode { Id = "start/2/branch-1/1/body/4/failure/1/true/1", Label = "Wait 5m", Kind = "Timer", Width = 208, Height = 88 },
|
||||
new WorkflowRenderNode { Id = "start/2/branch-1/1/body/4/failure/2", Label = "Set printGenerateFailed", Kind = "SetState", Width = 208, Height = 88 },
|
||||
new WorkflowRenderNode { Id = "start/2/branch-1/1/body/5", Label = "Print Batch Returned Result", Kind = "Decision", Width = 188, Height = 132 },
|
||||
new WorkflowRenderNode { Id = "start/2/branch-1/1/body/5/true/1", Label = "Print Batch Succeeded", Kind = "Decision", Width = 188, Height = 132 },
|
||||
new WorkflowRenderNode { Id = "start/2/branch-1/1/body/5/true/1/true/1/batched", Label = "Setting:\npolicyNo\nfiles\ndocsCount\nhasMissingDocuments", Kind = "SetState", Width = 224, Height = 104 },
|
||||
new WorkflowRenderNode { Id = "start/2/branch-1/1/body/4/timeout/1", Label = "Set printTimedOut", Kind = "SetState", Width = 208, Height = 88 },
|
||||
new WorkflowRenderNode { Id = "start/2/branch-1/1", Label = "Process Batch", Kind = "Repeat", Width = 208, Height = 88 },
|
||||
new WorkflowRenderNode { Id = "start/2/branch-1/1/body/1/batched", Label = "Setting:\nbatchTimedOut\nbatchGenerateFailed\nhasMissingItems", Kind = "SetState", Width = 224, Height = 104 },
|
||||
new WorkflowRenderNode { Id = "start/2/branch-1/1/body/4", Label = "Execute Batch", Kind = "TransportCall", Width = 208, Height = 88 },
|
||||
new WorkflowRenderNode { Id = "start/2/branch-1/1/body/4/failure/1", Label = "Retry Decision", Kind = "Decision", Width = 188, Height = 132 },
|
||||
new WorkflowRenderNode { Id = "start/2/branch-1/1/body/4/failure/1/true/1", Label = "Cooldown Timer", Kind = "Timer", Width = 208, Height = 88 },
|
||||
new WorkflowRenderNode { Id = "start/2/branch-1/1/body/4/failure/2", Label = "Set batchGenerateFailed", Kind = "SetState", Width = 208, Height = 88 },
|
||||
new WorkflowRenderNode { Id = "start/2/branch-1/1/body/5", Label = "Check Result", Kind = "Decision", Width = 188, Height = 132 },
|
||||
new WorkflowRenderNode { Id = "start/2/branch-1/1/body/5/true/1", Label = "Validate Success", Kind = "Decision", Width = 188, Height = 132 },
|
||||
new WorkflowRenderNode { Id = "start/2/branch-1/1/body/5/true/1/true/1/batched", Label = "Setting:\nitemId\nfiles\nitemsCount\nhasMissingItems", Kind = "SetState", Width = 224, Height = 104 },
|
||||
new WorkflowRenderNode { Id = "start/2/branch-1/1/body/4/timeout/1", Label = "Set batchTimedOut", Kind = "SetState", Width = 208, Height = 88 },
|
||||
],
|
||||
Edges =
|
||||
[
|
||||
@@ -85,9 +85,9 @@ public class AssistantPrintInsisDocumentsRenderingTests
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task AssistantPrintInsisDocuments_WhenLayoutOnly_ShouldProduceFinitePositions()
|
||||
public async Task DocumentProcessingWorkflow_WhenLayoutOnly_ShouldProduceFinitePositions()
|
||||
{
|
||||
var graph = BuildAssistantPrintInsisDocumentsGraph();
|
||||
var graph = BuildDocumentProcessingWorkflowGraph();
|
||||
var engine = new ElkSharpWorkflowRenderLayoutEngine();
|
||||
|
||||
var layout = await engine.LayoutAsync(graph, new WorkflowRenderLayoutRequest
|
||||
@@ -102,9 +102,9 @@ public class AssistantPrintInsisDocumentsRenderingTests
|
||||
|
||||
[Test]
|
||||
[Category("RenderingArtifacts")]
|
||||
public async Task AssistantPrintInsisDocuments_WhenRenderedWithElkSharp_ShouldProducePngWithZeroNodeCrossings()
|
||||
public async Task DocumentProcessingWorkflow_WhenRenderedWithElkSharp_ShouldProducePngWithZeroNodeCrossings()
|
||||
{
|
||||
var graph = BuildAssistantPrintInsisDocumentsGraph();
|
||||
var graph = BuildDocumentProcessingWorkflowGraph();
|
||||
var engine = new ElkSharpWorkflowRenderLayoutEngine();
|
||||
|
||||
var layout = await engine.LayoutAsync(graph, new WorkflowRenderLayoutRequest
|
||||
@@ -113,11 +113,11 @@ public class AssistantPrintInsisDocumentsRenderingTests
|
||||
});
|
||||
|
||||
var svgRenderer = new WorkflowRenderSvgRenderer();
|
||||
var svgDoc = svgRenderer.Render(layout, "AssistantPrintInsisDocuments [ElkSharp]");
|
||||
var svgDoc = svgRenderer.Render(layout, "DocumentProcessingWorkflow [ElkSharp]");
|
||||
|
||||
var outputDir = Path.Combine(
|
||||
Path.GetDirectoryName(typeof(AssistantPrintInsisDocumentsRenderingTests).Assembly.Location)!,
|
||||
"TestResults", "workflow-renderings", DateTime.Today.ToString("yyyyMMdd"), "AssistantPrintInsisDocuments");
|
||||
Path.GetDirectoryName(typeof(DocumentProcessingWorkflowRenderingTests).Assembly.Location)!,
|
||||
"TestResults", "workflow-renderings", DateTime.Today.ToString("yyyyMMdd"), "DocumentProcessingWorkflow");
|
||||
Directory.CreateDirectory(outputDir);
|
||||
|
||||
var svgPath = Path.Combine(outputDir, "elksharp.svg");
|
||||
Reference in New Issue
Block a user