fix: lay out handler sections in separate ELK graphs
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 58s
CI / docker (push) Successful in 37s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 36s

ON_EXCEPTION, ON_COMPLETION, and ERROR_HANDLER compounds were included
in the same root ELK graph as the main flow. ELK's layered algorithm
offset the main flow nodes vertically to accommodate the handler
compounds, causing bent arrows between the ENDPOINT and first processor.

Now handler sections get their own independent ELK root graphs. The
frontend already separates and repositions them, so they just need
correct internal layout — not positioning relative to the main flow.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-03-27 20:03:20 +01:00
parent b0dcd0ac6b
commit 5306be3f2e

View File

@@ -107,6 +107,12 @@ public class ElkDiagramRenderer implements DiagramRenderer {
NodeType.ON_COMPLETION
);
/** Top-level handler types that are laid out in their own separate ELK graph
* to prevent them from affecting the main flow's node positioning. */
private static final Set<NodeType> HANDLER_SECTION_TYPES = EnumSet.of(
NodeType.ON_EXCEPTION, NodeType.ERROR_HANDLER, NodeType.ON_COMPLETION
);
public ElkDiagramRenderer() {
// Ensure the layered algorithm meta data provider is registered.
// LayoutMetaDataService uses ServiceLoader, but explicit registration
@@ -201,16 +207,49 @@ public class ElkDiagramRenderer implements DiagramRenderer {
Map<String, Color> nodeColors = new HashMap<>();
Set<String> compoundNodeIds = new HashSet<>();
// Process top-level nodes from the graph
// Separate handler sections (ON_EXCEPTION, ON_COMPLETION, ERROR_HANDLER)
// from main flow nodes. Handler sections are laid out in their own ELK
// graphs to prevent them from affecting the main flow's Y positioning.
List<RouteNode> mainNodes = new ArrayList<>();
List<RouteNode> handlerNodes = new ArrayList<>();
if (graph.getNodes() != null) {
for (RouteNode rn : graph.getNodes()) {
if (!elkNodeMap.containsKey(rn.getId())) {
createElkNodeRecursive(rn, rootNode, factory, elkNodeMap, nodeColors,
compoundNodeIds, childNodeIds);
if (rn.getType() != null && HANDLER_SECTION_TYPES.contains(rn.getType())
&& rn.getChildren() != null && !rn.getChildren().isEmpty()) {
handlerNodes.add(rn);
} else {
mainNodes.add(rn);
}
}
}
// Process main flow nodes into the root ELK graph
for (RouteNode rn : mainNodes) {
if (!elkNodeMap.containsKey(rn.getId())) {
createElkNodeRecursive(rn, rootNode, factory, elkNodeMap, nodeColors,
compoundNodeIds, childNodeIds);
}
}
// Process handler sections into their OWN separate ELK root graphs.
// This prevents them from affecting the main flow's Y positioning.
// Each handler gets its own independent layout.
List<ElkNode> handlerRoots = new ArrayList<>();
for (RouteNode rn : handlerNodes) {
ElkNode handlerRoot = factory.createElkNode();
handlerRoot.setIdentifier("handler-root-" + rn.getId());
handlerRoot.setProperty(CoreOptions.ALGORITHM, "org.eclipse.elk.layered");
handlerRoot.setProperty(CoreOptions.DIRECTION, rootDirection);
handlerRoot.setProperty(CoreOptions.SPACING_NODE_NODE, NODE_SPACING * 0.5);
handlerRoot.setProperty(CoreOptions.SPACING_EDGE_NODE, EDGE_SPACING * 0.5);
handlerRoot.setProperty(org.eclipse.elk.alg.layered.options.LayeredOptions.NODE_PLACEMENT_STRATEGY,
NodePlacementStrategy.LINEAR_SEGMENTS);
createElkNodeRecursive(rn, handlerRoot, factory, elkNodeMap, nodeColors,
compoundNodeIds, childNodeIds);
handlerRoots.add(handlerRoot);
}
// Create ELK edges
if (graph.getEdges() != null) {
for (RouteEdge re : graph.getEdges()) {
@@ -230,10 +269,15 @@ public class ElkDiagramRenderer implements DiagramRenderer {
}
}
// Run layout
// Run layout — main flow
RecursiveGraphLayoutEngine engine = new RecursiveGraphLayoutEngine();
engine.layout(rootNode, new BasicProgressMonitor());
// Run layout — each handler section independently
for (ElkNode handlerRoot : handlerRoots) {
engine.layout(handlerRoot, new BasicProgressMonitor());
}
// Extract results — only top-level nodes (children collected recursively)
List<PositionedNode> positionedNodes = new ArrayList<>();
Map<String, CompoundInfo> compoundInfos = new HashMap<>();