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