From 5306be3f2e01a20785b9d77cf31b5647247a627f Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Fri, 27 Mar 2026 20:03:20 +0100 Subject: [PATCH] fix: lay out handler sections in separate ELK graphs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../app/diagram/ElkDiagramRenderer.java | 54 +++++++++++++++++-- 1 file changed, 49 insertions(+), 5 deletions(-) diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/diagram/ElkDiagramRenderer.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/diagram/ElkDiagramRenderer.java index ae357cb3..1d2d9722 100644 --- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/diagram/ElkDiagramRenderer.java +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/diagram/ElkDiagramRenderer.java @@ -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 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 nodeColors = new HashMap<>(); Set 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 mainNodes = new ArrayList<>(); + List 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 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 positionedNodes = new ArrayList<>(); Map compoundInfos = new HashMap<>();