refactor: simplify ElkDiagramRenderer layout code
- Introduce LayoutContext to bundle 8 accumulator params into 1 object - Extract computeLayout (261 lines) into 6 focused sub-methods: buildNodeIndex, partitionNodes, createElkRoot, createElkEdges, postProcessDoTrySections, extractLayout - Consolidate duplicated DO_TRY handler iteration via orderedHandlerChildren - De-duplicate ELK root configuration (main + handler roots) - Add DO_TRY test cases for section ordering and uniform width - Clean up orphaned Javadoc comments No behavioral changes. 882 → 841 lines. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -185,48 +185,66 @@ public class ElkDiagramRenderer implements DiagramRenderer {
|
|||||||
// ----------------------------------------------------------------
|
// ----------------------------------------------------------------
|
||||||
|
|
||||||
private LayoutResult computeLayout(RouteGraph graph, Direction rootDirection) {
|
private LayoutResult computeLayout(RouteGraph graph, Direction rootDirection) {
|
||||||
ElkGraphFactory factory = ElkGraphFactory.eINSTANCE;
|
LayoutContext ctx = new LayoutContext(ElkGraphFactory.eINSTANCE);
|
||||||
|
|
||||||
// Create root node for main flow
|
// 1. Partition graph nodes into main flow vs handler sections
|
||||||
ElkNode rootNode = factory.createElkNode();
|
Map<String, RouteNode> nodeById = buildNodeIndex(graph);
|
||||||
rootNode.setIdentifier("root");
|
List<RouteNode> mainNodes = new ArrayList<>();
|
||||||
rootNode.setProperty(CoreOptions.ALGORITHM, "org.eclipse.elk.layered");
|
List<RouteNode> handlerNodes = new ArrayList<>();
|
||||||
rootNode.setProperty(CoreOptions.DIRECTION, rootDirection);
|
partitionNodes(graph, nodeById, mainNodes, handlerNodes);
|
||||||
rootNode.setProperty(CoreOptions.SPACING_NODE_NODE, NODE_SPACING);
|
|
||||||
rootNode.setProperty(CoreOptions.SPACING_EDGE_NODE, EDGE_SPACING);
|
|
||||||
rootNode.setProperty(CoreOptions.HIERARCHY_HANDLING, HierarchyHandling.INCLUDE_CHILDREN);
|
|
||||||
rootNode.setProperty(CoreOptions.EDGE_ROUTING, EdgeRouting.POLYLINE);
|
|
||||||
rootNode.setProperty(org.eclipse.elk.alg.layered.options.LayeredOptions.NODE_PLACEMENT_STRATEGY,
|
|
||||||
NodePlacementStrategy.LINEAR_SEGMENTS);
|
|
||||||
|
|
||||||
// Track which nodes are children of a compound (at any depth)
|
// 2. Build ELK graphs — main flow + separate handler roots
|
||||||
Set<String> childNodeIds = new HashSet<>();
|
ElkNode rootNode = createElkRoot("root", rootDirection, 1.0, ctx);
|
||||||
|
for (RouteNode rn : mainNodes) {
|
||||||
|
if (!ctx.elkNodeMap.containsKey(rn.getId())) {
|
||||||
|
createElkNodeRecursive(rn, rootNode, ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Create ELK nodes recursively — compounds contain their children
|
List<ElkNode> handlerRoots = new ArrayList<>();
|
||||||
Map<String, ElkNode> elkNodeMap = new HashMap<>();
|
for (RouteNode rn : handlerNodes) {
|
||||||
Map<String, Color> nodeColors = new HashMap<>();
|
ElkNode hr = createElkRoot("handler-root-" + rn.getId(), rootDirection, 0.5, ctx);
|
||||||
Set<String> compoundNodeIds = new HashSet<>();
|
createElkNodeRecursive(rn, hr, ctx);
|
||||||
|
handlerRoots.add(hr);
|
||||||
|
}
|
||||||
|
|
||||||
// Build a lookup from the flat nodes list (graph.getNodes() contains
|
// 3. Create ELK edges (filtering DO_TRY internals and cross-root edges)
|
||||||
// duplicates — children appear both nested AND top-level). Use this for
|
createElkEdges(graph, ctx);
|
||||||
// resolving edge endpoints to RouteNode objects.
|
|
||||||
|
// 4. Run layout engine
|
||||||
|
RecursiveGraphLayoutEngine engine = new RecursiveGraphLayoutEngine();
|
||||||
|
engine.layout(rootNode, new BasicProgressMonitor());
|
||||||
|
for (ElkNode hr : handlerRoots) {
|
||||||
|
engine.layout(hr, new BasicProgressMonitor());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Post-process: fix DO_TRY section ordering and widths
|
||||||
|
postProcessDoTrySections(ctx);
|
||||||
|
|
||||||
|
// 6. Extract positioned result
|
||||||
|
return extractLayout(graph, rootNode, handlerRoots, ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Build a lookup from the flat nodes list (deduplicates nested + top-level entries). */
|
||||||
|
private Map<String, RouteNode> buildNodeIndex(RouteGraph graph) {
|
||||||
Map<String, RouteNode> nodeById = new HashMap<>();
|
Map<String, RouteNode> nodeById = new HashMap<>();
|
||||||
if (graph.getNodes() != null) {
|
if (graph.getNodes() != null) {
|
||||||
for (RouteNode rn : graph.getNodes()) {
|
for (RouteNode rn : graph.getNodes()) {
|
||||||
nodeById.put(rn.getId(), rn);
|
nodeById.put(rn.getId(), rn);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return nodeById;
|
||||||
|
}
|
||||||
|
|
||||||
// Separate main flow from handler sections using graph structure:
|
/**
|
||||||
// - Start from graph.getRoot() and walk FLOW edges for main flow
|
* Separate main flow nodes from handler sections by walking FLOW edges
|
||||||
// - Handler sections are top-level nodes of HANDLER_SECTION_TYPES
|
* from the graph root. Handler sections (onException, onCompletion, etc.)
|
||||||
// This avoids the problem of the flat nodes list containing duplicates.
|
* are placed in their own list for independent layout.
|
||||||
|
*/
|
||||||
|
private void partitionNodes(RouteGraph graph, Map<String, RouteNode> nodeById,
|
||||||
|
List<RouteNode> mainNodes, List<RouteNode> handlerNodes) {
|
||||||
Set<String> mainNodeIds = new HashSet<>();
|
Set<String> mainNodeIds = new HashSet<>();
|
||||||
List<RouteNode> mainNodes = new ArrayList<>();
|
|
||||||
List<RouteNode> handlerNodes = new ArrayList<>();
|
|
||||||
|
|
||||||
// Collect main flow node IDs by walking from root along non-ERROR edges.
|
|
||||||
// graph.getRoot() provides the entry point; ERROR edges lead to handler sections.
|
|
||||||
if (graph.getRoot() != null && graph.getEdges() != null) {
|
if (graph.getRoot() != null && graph.getEdges() != null) {
|
||||||
mainNodeIds.add(graph.getRoot().getId());
|
mainNodeIds.add(graph.getRoot().getId());
|
||||||
boolean changed = true;
|
boolean changed = true;
|
||||||
@@ -235,7 +253,6 @@ public class ElkDiagramRenderer implements DiagramRenderer {
|
|||||||
for (RouteEdge re : graph.getEdges()) {
|
for (RouteEdge re : graph.getEdges()) {
|
||||||
if (re.getEdgeType() == RouteEdge.EdgeType.ERROR) continue;
|
if (re.getEdgeType() == RouteEdge.EdgeType.ERROR) continue;
|
||||||
if (mainNodeIds.contains(re.getSource()) && !mainNodeIds.contains(re.getTarget())) {
|
if (mainNodeIds.contains(re.getSource()) && !mainNodeIds.contains(re.getTarget())) {
|
||||||
// Don't follow edges INTO handler compounds
|
|
||||||
RouteNode target = nodeById.get(re.getTarget());
|
RouteNode target = nodeById.get(re.getTarget());
|
||||||
if (target != null && target.getType() != null
|
if (target != null && target.getType() != null
|
||||||
&& HANDLER_SECTION_TYPES.contains(target.getType())) {
|
&& HANDLER_SECTION_TYPES.contains(target.getType())) {
|
||||||
@@ -248,7 +265,6 @@ public class ElkDiagramRenderer implements DiagramRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build node lists — deduplicate (flat list has duplicates)
|
|
||||||
Set<String> seen = new HashSet<>();
|
Set<String> seen = new HashSet<>();
|
||||||
if (graph.getNodes() != null) {
|
if (graph.getNodes() != null) {
|
||||||
for (RouteNode rn : graph.getNodes()) {
|
for (RouteNode rn : graph.getNodes()) {
|
||||||
@@ -258,124 +274,91 @@ public class ElkDiagramRenderer implements DiagramRenderer {
|
|||||||
&& rn.getChildren() != null && !rn.getChildren().isEmpty()) {
|
&& rn.getChildren() != null && !rn.getChildren().isEmpty()) {
|
||||||
handlerNodes.add(rn);
|
handlerNodes.add(rn);
|
||||||
} else if (mainNodeIds.isEmpty() || mainNodeIds.contains(rn.getId())) {
|
} else if (mainNodeIds.isEmpty() || mainNodeIds.contains(rn.getId())) {
|
||||||
// When no root is available (legacy/test graphs), include all
|
|
||||||
// non-handler nodes as main flow (fallback to old behavior)
|
|
||||||
mainNodes.add(rn);
|
mainNodes.add(rn);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Process main flow nodes into the root ELK graph
|
/** Create a configured ELK root node for the layered algorithm. */
|
||||||
Set<String> doTryNodeIds = new HashSet<>();
|
private ElkNode createElkRoot(String identifier, Direction direction,
|
||||||
Map<String, List<String>> doTrySectionOrder = new LinkedHashMap<>();
|
double spacingScale, LayoutContext ctx) {
|
||||||
for (RouteNode rn : mainNodes) {
|
ElkNode root = ctx.factory.createElkNode();
|
||||||
if (!elkNodeMap.containsKey(rn.getId())) {
|
root.setIdentifier(identifier);
|
||||||
createElkNodeRecursive(rn, rootNode, factory, elkNodeMap, nodeColors,
|
root.setProperty(CoreOptions.ALGORITHM, "org.eclipse.elk.layered");
|
||||||
compoundNodeIds, childNodeIds, doTryNodeIds, doTrySectionOrder);
|
root.setProperty(CoreOptions.DIRECTION, direction);
|
||||||
|
root.setProperty(CoreOptions.SPACING_NODE_NODE, NODE_SPACING * spacingScale);
|
||||||
|
root.setProperty(CoreOptions.SPACING_EDGE_NODE, EDGE_SPACING * spacingScale);
|
||||||
|
root.setProperty(CoreOptions.HIERARCHY_HANDLING, HierarchyHandling.INCLUDE_CHILDREN);
|
||||||
|
root.setProperty(CoreOptions.EDGE_ROUTING, EdgeRouting.POLYLINE);
|
||||||
|
root.setProperty(org.eclipse.elk.alg.layered.options.LayeredOptions.NODE_PLACEMENT_STRATEGY,
|
||||||
|
NodePlacementStrategy.LINEAR_SEGMENTS);
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Create ELK edges, skipping DO_TRY-to-descendant and cross-root edges. */
|
||||||
|
private void createElkEdges(RouteGraph graph, LayoutContext ctx) {
|
||||||
|
if (graph.getEdges() == null) return;
|
||||||
|
for (RouteEdge re : graph.getEdges()) {
|
||||||
|
ElkNode sourceElk = ctx.elkNodeMap.get(re.getSource());
|
||||||
|
ElkNode targetElk = ctx.elkNodeMap.get(re.getTarget());
|
||||||
|
if (sourceElk == null || targetElk == null) continue;
|
||||||
|
|
||||||
|
// Skip edges from DO_TRY to its own children (keep continuation edges)
|
||||||
|
if (ctx.doTryNodeIds.contains(re.getSource()) && isDescendantOf(targetElk, sourceElk)) {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
// Skip edges that cross ELK root boundaries
|
||||||
|
if (getElkRoot(sourceElk) != getElkRoot(targetElk)) continue;
|
||||||
|
|
||||||
|
ElkEdge elkEdge = ctx.factory.createElkEdge();
|
||||||
|
elkEdge.setContainingNode(findCommonParent(sourceElk, targetElk));
|
||||||
|
elkEdge.getSources().add(sourceElk);
|
||||||
|
elkEdge.getTargets().add(targetElk);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Process handler sections into their OWN separate ELK root graphs
|
/**
|
||||||
List<ElkNode> handlerRoots = new ArrayList<>();
|
* Post-process DO_TRY compounds after layout: re-stack sections in correct
|
||||||
for (RouteNode rn : handlerNodes) {
|
* vertical order (try_body → doFinally → doCatch) and stretch to uniform width.
|
||||||
ElkNode handlerRoot = factory.createElkNode();
|
* ELK doesn't reliably order disconnected children within a compound.
|
||||||
handlerRoot.setIdentifier("handler-root-" + rn.getId());
|
*/
|
||||||
handlerRoot.setProperty(CoreOptions.ALGORITHM, "org.eclipse.elk.layered");
|
private void postProcessDoTrySections(LayoutContext ctx) {
|
||||||
handlerRoot.setProperty(CoreOptions.DIRECTION, rootDirection);
|
for (Map.Entry<String, List<String>> entry : ctx.doTrySectionOrder.entrySet()) {
|
||||||
handlerRoot.setProperty(CoreOptions.SPACING_NODE_NODE, NODE_SPACING * 0.5);
|
|
||||||
handlerRoot.setProperty(CoreOptions.SPACING_EDGE_NODE, EDGE_SPACING * 0.5);
|
|
||||||
handlerRoot.setProperty(CoreOptions.HIERARCHY_HANDLING, HierarchyHandling.INCLUDE_CHILDREN);
|
|
||||||
handlerRoot.setProperty(CoreOptions.EDGE_ROUTING, EdgeRouting.POLYLINE);
|
|
||||||
handlerRoot.setProperty(org.eclipse.elk.alg.layered.options.LayeredOptions.NODE_PLACEMENT_STRATEGY,
|
|
||||||
NodePlacementStrategy.LINEAR_SEGMENTS);
|
|
||||||
|
|
||||||
createElkNodeRecursive(rn, handlerRoot, factory, elkNodeMap, nodeColors,
|
|
||||||
compoundNodeIds, childNodeIds, doTryNodeIds, doTrySectionOrder);
|
|
||||||
handlerRoots.add(handlerRoot);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create ELK edges — skip edges that cross between different ELK root graphs
|
|
||||||
if (graph.getEdges() != null) {
|
|
||||||
for (RouteEdge re : graph.getEdges()) {
|
|
||||||
ElkNode sourceElk = elkNodeMap.get(re.getSource());
|
|
||||||
ElkNode targetElk = elkNodeMap.get(re.getTarget());
|
|
||||||
if (sourceElk == null || targetElk == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip edges from DO_TRY to its own children (entry + handler edges).
|
|
||||||
// Keep edges from DO_TRY to nodes OUTSIDE it (continuation edges).
|
|
||||||
if (doTryNodeIds.contains(re.getSource())) {
|
|
||||||
if (isDescendantOf(targetElk, sourceElk)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip edges that cross ELK root boundaries
|
|
||||||
ElkNode sourceRoot = getElkRoot(sourceElk);
|
|
||||||
ElkNode targetRoot = getElkRoot(targetElk);
|
|
||||||
if (sourceRoot != targetRoot) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
ElkNode containingNode = findCommonParent(sourceElk, targetElk);
|
|
||||||
ElkEdge elkEdge = factory.createElkEdge();
|
|
||||||
elkEdge.setContainingNode(containingNode);
|
|
||||||
elkEdge.getSources().add(sourceElk);
|
|
||||||
elkEdge.getTargets().add(targetElk);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run layout
|
|
||||||
RecursiveGraphLayoutEngine engine = new RecursiveGraphLayoutEngine();
|
|
||||||
engine.layout(rootNode, new BasicProgressMonitor());
|
|
||||||
for (ElkNode handlerRoot : handlerRoots) {
|
|
||||||
engine.layout(handlerRoot, new BasicProgressMonitor());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Post-process DO_TRY compounds: re-stack sections in correct order
|
|
||||||
// and stretch all sections to the same width
|
|
||||||
// (ELK doesn't reliably order disconnected children within a compound)
|
|
||||||
for (Map.Entry<String, List<String>> entry : doTrySectionOrder.entrySet()) {
|
|
||||||
List<String> orderedIds = entry.getValue();
|
List<String> orderedIds = entry.getValue();
|
||||||
List<ElkNode> sections = new ArrayList<>();
|
List<ElkNode> sections = new ArrayList<>();
|
||||||
for (String id : orderedIds) {
|
for (String id : orderedIds) {
|
||||||
ElkNode section = elkNodeMap.get(id);
|
ElkNode section = ctx.elkNodeMap.get(id);
|
||||||
if (section != null) sections.add(section);
|
if (section != null) sections.add(section);
|
||||||
}
|
}
|
||||||
if (sections.size() < 2) continue;
|
if (sections.size() < 2) continue;
|
||||||
|
|
||||||
// Re-stack in correct vertical order
|
|
||||||
double startY = sections.stream().mapToDouble(ElkNode::getY).min().orElse(0);
|
double startY = sections.stream().mapToDouble(ElkNode::getY).min().orElse(0);
|
||||||
double spacing = NODE_SPACING * 0.4; // matches DO_TRY spacing
|
double spacing = NODE_SPACING * 0.4;
|
||||||
double currentY = startY;
|
double currentY = startY;
|
||||||
for (ElkNode section : sections) {
|
for (ElkNode section : sections) {
|
||||||
section.setY(currentY);
|
section.setY(currentY);
|
||||||
currentY += section.getHeight() + spacing;
|
currentY += section.getHeight() + spacing;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stretch all sections to the widest one's width
|
|
||||||
double maxWidth = sections.stream().mapToDouble(ElkNode::getWidth).max().orElse(0);
|
double maxWidth = sections.stream().mapToDouble(ElkNode::getWidth).max().orElse(0);
|
||||||
for (ElkNode section : sections) {
|
for (ElkNode section : sections) {
|
||||||
section.setWidth(maxWidth);
|
section.setWidth(maxWidth);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Extract positioned nodes, edges, and bounding box from the ELK layout result. */
|
||||||
|
private LayoutResult extractLayout(RouteGraph graph, ElkNode rootNode,
|
||||||
|
List<ElkNode> handlerRoots, LayoutContext ctx) {
|
||||||
// Extract positioned nodes
|
// Extract positioned nodes
|
||||||
List<PositionedNode> positionedNodes = new ArrayList<>();
|
List<PositionedNode> positionedNodes = new ArrayList<>();
|
||||||
Map<String, CompoundInfo> compoundInfos = new HashMap<>();
|
|
||||||
|
|
||||||
if (graph.getNodes() != null) {
|
if (graph.getNodes() != null) {
|
||||||
for (RouteNode rn : graph.getNodes()) {
|
for (RouteNode rn : graph.getNodes()) {
|
||||||
if (childNodeIds.contains(rn.getId())) {
|
if (ctx.childNodeIds.contains(rn.getId())) continue;
|
||||||
continue;
|
ElkNode elkNode = ctx.elkNodeMap.get(rn.getId());
|
||||||
}
|
|
||||||
ElkNode elkNode = elkNodeMap.get(rn.getId());
|
|
||||||
if (elkNode == null) continue;
|
if (elkNode == null) continue;
|
||||||
|
positionedNodes.add(extractPositionedNode(rn, elkNode, getElkRoot(elkNode), ctx));
|
||||||
ElkNode coordRoot = getElkRoot(elkNode);
|
|
||||||
positionedNodes.add(extractPositionedNode(rn, elkNode, elkNodeMap,
|
|
||||||
compoundNodeIds, compoundInfos, coordRoot));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -388,25 +371,17 @@ public class ElkDiagramRenderer implements DiagramRenderer {
|
|||||||
for (ElkEdge elkEdge : allEdges) {
|
for (ElkEdge elkEdge : allEdges) {
|
||||||
String sourceId = elkEdge.getSources().isEmpty() ? "" : elkEdge.getSources().get(0).getIdentifier();
|
String sourceId = elkEdge.getSources().isEmpty() ? "" : elkEdge.getSources().get(0).getIdentifier();
|
||||||
String targetId = elkEdge.getTargets().isEmpty() ? "" : elkEdge.getTargets().get(0).getIdentifier();
|
String targetId = elkEdge.getTargets().isEmpty() ? "" : elkEdge.getTargets().get(0).getIdentifier();
|
||||||
|
|
||||||
ElkNode edgeRoot = getElkRoot(elkEdge.getContainingNode());
|
ElkNode edgeRoot = getElkRoot(elkEdge.getContainingNode());
|
||||||
|
|
||||||
List<double[]> points = new ArrayList<>();
|
List<double[]> points = new ArrayList<>();
|
||||||
for (ElkEdgeSection section : elkEdge.getSections()) {
|
for (ElkEdgeSection section : elkEdge.getSections()) {
|
||||||
points.add(new double[]{
|
double cx = getAbsoluteX(elkEdge.getContainingNode(), edgeRoot);
|
||||||
section.getStartX() + getAbsoluteX(elkEdge.getContainingNode(), edgeRoot),
|
double cy = getAbsoluteY(elkEdge.getContainingNode(), edgeRoot);
|
||||||
section.getStartY() + getAbsoluteY(elkEdge.getContainingNode(), edgeRoot)
|
points.add(new double[]{section.getStartX() + cx, section.getStartY() + cy});
|
||||||
});
|
|
||||||
for (ElkBendPoint bp : section.getBendPoints()) {
|
for (ElkBendPoint bp : section.getBendPoints()) {
|
||||||
points.add(new double[]{
|
points.add(new double[]{bp.getX() + cx, bp.getY() + cy});
|
||||||
bp.getX() + getAbsoluteX(elkEdge.getContainingNode(), edgeRoot),
|
|
||||||
bp.getY() + getAbsoluteY(elkEdge.getContainingNode(), edgeRoot)
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
points.add(new double[]{
|
points.add(new double[]{section.getEndX() + cx, section.getEndY() + cy});
|
||||||
section.getEndX() + getAbsoluteX(elkEdge.getContainingNode(), edgeRoot),
|
|
||||||
section.getEndY() + getAbsoluteY(elkEdge.getContainingNode(), edgeRoot)
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String label = "";
|
String label = "";
|
||||||
@@ -418,32 +393,24 @@ public class ElkDiagramRenderer implements DiagramRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
positionedEdges.add(new PositionedEdge(sourceId, targetId, label, points));
|
positionedEdges.add(new PositionedEdge(sourceId, targetId, label, points));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: normalization (shifting to 0,0 origin) is handled per-section
|
// Compute bounding box
|
||||||
// in the frontend's useDiagramData.ts, which separates main flow from
|
double totalWidth = 0, totalHeight = 0;
|
||||||
// handler sections and normalizes each independently.
|
|
||||||
|
|
||||||
// Compute bounding box from all positioned nodes and edges
|
|
||||||
double totalWidth = 0;
|
|
||||||
double totalHeight = 0;
|
|
||||||
for (PositionedNode pn : allNodes(positionedNodes)) {
|
for (PositionedNode pn : allNodes(positionedNodes)) {
|
||||||
double right = pn.x() + pn.width();
|
totalWidth = Math.max(totalWidth, pn.x() + pn.width());
|
||||||
double bottom = pn.y() + pn.height();
|
totalHeight = Math.max(totalHeight, pn.y() + pn.height());
|
||||||
if (right > totalWidth) totalWidth = right;
|
|
||||||
if (bottom > totalHeight) totalHeight = bottom;
|
|
||||||
}
|
}
|
||||||
for (PositionedEdge pe : positionedEdges) {
|
for (PositionedEdge pe : positionedEdges) {
|
||||||
for (double[] pt : pe.points()) {
|
for (double[] pt : pe.points()) {
|
||||||
if (pt[0] > totalWidth) totalWidth = pt[0];
|
totalWidth = Math.max(totalWidth, pt[0]);
|
||||||
if (pt[1] > totalHeight) totalHeight = pt[1];
|
totalHeight = Math.max(totalHeight, pt[1]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DiagramLayout layout = new DiagramLayout(totalWidth, totalHeight, positionedNodes, positionedEdges);
|
DiagramLayout layout = new DiagramLayout(totalWidth, totalHeight, positionedNodes, positionedEdges);
|
||||||
return new LayoutResult(layout, nodeColors, compoundInfos);
|
return new LayoutResult(layout, ctx.nodeColors, ctx.compoundInfos);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------
|
// ----------------------------------------------------------------
|
||||||
@@ -559,23 +526,20 @@ public class ElkDiagramRenderer implements DiagramRenderer {
|
|||||||
* with their children nested inside. Non-compound nodes become leaf nodes.
|
* with their children nested inside. Non-compound nodes become leaf nodes.
|
||||||
*/
|
*/
|
||||||
private void createElkNodeRecursive(
|
private void createElkNodeRecursive(
|
||||||
RouteNode rn, ElkNode parentElk, ElkGraphFactory factory,
|
RouteNode rn, ElkNode parentElk, LayoutContext ctx) {
|
||||||
Map<String, ElkNode> elkNodeMap, Map<String, Color> nodeColors,
|
|
||||||
Set<String> compoundNodeIds, Set<String> childNodeIds,
|
|
||||||
Set<String> doTryNodeIds, Map<String, List<String>> doTrySectionOrder) {
|
|
||||||
|
|
||||||
boolean isCompound = rn.getType() != null && COMPOUND_TYPES.contains(rn.getType())
|
boolean isCompound = rn.getType() != null && COMPOUND_TYPES.contains(rn.getType())
|
||||||
&& rn.getChildren() != null && !rn.getChildren().isEmpty();
|
&& rn.getChildren() != null && !rn.getChildren().isEmpty();
|
||||||
|
|
||||||
ElkNode elkNode = factory.createElkNode();
|
ElkNode elkNode = ctx.factory.createElkNode();
|
||||||
elkNode.setIdentifier(rn.getId());
|
elkNode.setIdentifier(rn.getId());
|
||||||
elkNode.setParent(parentElk);
|
elkNode.setParent(parentElk);
|
||||||
|
|
||||||
if (isCompound && rn.getType() == NodeType.DO_TRY) {
|
if (isCompound && rn.getType() == NodeType.DO_TRY) {
|
||||||
// DO_TRY: vertical container with a virtual _TRY_BODY wrapper for the try body
|
// DO_TRY: vertical container with a virtual _TRY_BODY wrapper for the try body
|
||||||
// and DO_CATCH/DO_FINALLY as separate children below
|
// and DO_CATCH/DO_FINALLY as separate children below
|
||||||
doTryNodeIds.add(rn.getId());
|
ctx.doTryNodeIds.add(rn.getId());
|
||||||
compoundNodeIds.add(rn.getId());
|
ctx.compoundNodeIds.add(rn.getId());
|
||||||
elkNode.setWidth(200);
|
elkNode.setWidth(200);
|
||||||
elkNode.setHeight(100);
|
elkNode.setHeight(100);
|
||||||
elkNode.setProperty(CoreOptions.ALGORITHM, "org.eclipse.elk.layered");
|
elkNode.setProperty(CoreOptions.ALGORITHM, "org.eclipse.elk.layered");
|
||||||
@@ -606,7 +570,7 @@ public class ElkDiagramRenderer implements DiagramRenderer {
|
|||||||
// Virtual _TRY_BODY wrapper
|
// Virtual _TRY_BODY wrapper
|
||||||
if (!tryBodyChildren.isEmpty()) {
|
if (!tryBodyChildren.isEmpty()) {
|
||||||
String wrapperId = rn.getId() + "._try_body";
|
String wrapperId = rn.getId() + "._try_body";
|
||||||
ElkNode wrapper = factory.createElkNode();
|
ElkNode wrapper = ctx.factory.createElkNode();
|
||||||
wrapper.setIdentifier(wrapperId);
|
wrapper.setIdentifier(wrapperId);
|
||||||
wrapper.setParent(elkNode);
|
wrapper.setParent(elkNode);
|
||||||
wrapper.setWidth(200);
|
wrapper.setWidth(200);
|
||||||
@@ -617,39 +581,26 @@ public class ElkDiagramRenderer implements DiagramRenderer {
|
|||||||
wrapper.setProperty(CoreOptions.SPACING_EDGE_NODE, EDGE_SPACING * 0.5);
|
wrapper.setProperty(CoreOptions.SPACING_EDGE_NODE, EDGE_SPACING * 0.5);
|
||||||
wrapper.setProperty(CoreOptions.PADDING,
|
wrapper.setProperty(CoreOptions.PADDING,
|
||||||
new org.eclipse.elk.core.math.ElkPadding(8, 8, 8, 8));
|
new org.eclipse.elk.core.math.ElkPadding(8, 8, 8, 8));
|
||||||
compoundNodeIds.add(wrapperId);
|
ctx.compoundNodeIds.add(wrapperId);
|
||||||
elkNodeMap.put(wrapperId, wrapper);
|
ctx.elkNodeMap.put(wrapperId, wrapper);
|
||||||
sectionOrder.add(wrapperId);
|
sectionOrder.add(wrapperId);
|
||||||
|
|
||||||
for (RouteNode child : tryBodyChildren) {
|
for (RouteNode child : tryBodyChildren) {
|
||||||
childNodeIds.add(child.getId());
|
ctx.childNodeIds.add(child.getId());
|
||||||
createElkNodeRecursive(child, wrapper, factory, elkNodeMap, nodeColors,
|
createElkNodeRecursive(child, wrapper, ctx);
|
||||||
compoundNodeIds, childNodeIds, doTryNodeIds, doTrySectionOrder);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DO_FINALLY sections (middle)
|
// Handler sections in order: DO_FINALLY (middle), then DO_CATCH (bottom)
|
||||||
for (RouteNode child : handlerChildren) {
|
for (RouteNode handler : orderedHandlerChildren(handlerChildren)) {
|
||||||
if (child.getType() == NodeType.DO_FINALLY) {
|
ctx.childNodeIds.add(handler.getId());
|
||||||
childNodeIds.add(child.getId());
|
createElkNodeRecursive(handler, elkNode, ctx);
|
||||||
createElkNodeRecursive(child, elkNode, factory, elkNodeMap, nodeColors,
|
sectionOrder.add(handler.getId());
|
||||||
compoundNodeIds, childNodeIds, doTryNodeIds, doTrySectionOrder);
|
|
||||||
sectionOrder.add(child.getId());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// DO_CATCH sections (bottom)
|
|
||||||
for (RouteNode child : handlerChildren) {
|
|
||||||
if (child.getType() == NodeType.DO_CATCH) {
|
|
||||||
childNodeIds.add(child.getId());
|
|
||||||
createElkNodeRecursive(child, elkNode, factory, elkNodeMap, nodeColors,
|
|
||||||
compoundNodeIds, childNodeIds, doTryNodeIds, doTrySectionOrder);
|
|
||||||
sectionOrder.add(child.getId());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
doTrySectionOrder.put(rn.getId(), sectionOrder);
|
ctx.doTrySectionOrder.put(rn.getId(), sectionOrder);
|
||||||
} else if (isCompound) {
|
} else if (isCompound) {
|
||||||
compoundNodeIds.add(rn.getId());
|
ctx.compoundNodeIds.add(rn.getId());
|
||||||
elkNode.setWidth(200);
|
elkNode.setWidth(200);
|
||||||
elkNode.setHeight(100);
|
elkNode.setHeight(100);
|
||||||
elkNode.setProperty(CoreOptions.ALGORITHM, "org.eclipse.elk.layered");
|
elkNode.setProperty(CoreOptions.ALGORITHM, "org.eclipse.elk.layered");
|
||||||
@@ -661,17 +612,16 @@ public class ElkDiagramRenderer implements DiagramRenderer {
|
|||||||
COMPOUND_SIDE_PADDING, COMPOUND_SIDE_PADDING, COMPOUND_SIDE_PADDING));
|
COMPOUND_SIDE_PADDING, COMPOUND_SIDE_PADDING, COMPOUND_SIDE_PADDING));
|
||||||
|
|
||||||
for (RouteNode child : rn.getChildren()) {
|
for (RouteNode child : rn.getChildren()) {
|
||||||
childNodeIds.add(child.getId());
|
ctx.childNodeIds.add(child.getId());
|
||||||
createElkNodeRecursive(child, elkNode, factory, elkNodeMap, nodeColors,
|
createElkNodeRecursive(child, elkNode, ctx);
|
||||||
compoundNodeIds, childNodeIds, doTryNodeIds, doTrySectionOrder);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
elkNode.setWidth(NODE_WIDTH);
|
elkNode.setWidth(NODE_WIDTH);
|
||||||
elkNode.setHeight(NODE_HEIGHT);
|
elkNode.setHeight(NODE_HEIGHT);
|
||||||
}
|
}
|
||||||
|
|
||||||
elkNodeMap.put(rn.getId(), elkNode);
|
ctx.elkNodeMap.put(rn.getId(), elkNode);
|
||||||
nodeColors.put(rn.getId(), colorForType(rn.getType()));
|
ctx.nodeColors.put(rn.getId(), colorForType(rn.getType()));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -679,29 +629,26 @@ public class ElkDiagramRenderer implements DiagramRenderer {
|
|||||||
* All coordinates are absolute (relative to the ELK root).
|
* All coordinates are absolute (relative to the ELK root).
|
||||||
*/
|
*/
|
||||||
private PositionedNode extractPositionedNode(
|
private PositionedNode extractPositionedNode(
|
||||||
RouteNode rn, ElkNode elkNode, Map<String, ElkNode> elkNodeMap,
|
RouteNode rn, ElkNode elkNode, ElkNode rootNode, LayoutContext ctx) {
|
||||||
Set<String> compoundNodeIds, Map<String, CompoundInfo> compoundInfos,
|
|
||||||
ElkNode rootNode) {
|
|
||||||
|
|
||||||
double absX = getAbsoluteX(elkNode, rootNode);
|
double absX = getAbsoluteX(elkNode, rootNode);
|
||||||
double absY = getAbsoluteY(elkNode, rootNode);
|
double absY = getAbsoluteY(elkNode, rootNode);
|
||||||
|
|
||||||
List<PositionedNode> children = List.of();
|
List<PositionedNode> children = List.of();
|
||||||
if (compoundNodeIds.contains(rn.getId()) && rn.getChildren() != null) {
|
if (ctx.compoundNodeIds.contains(rn.getId()) && rn.getChildren() != null) {
|
||||||
children = new ArrayList<>();
|
children = new ArrayList<>();
|
||||||
|
|
||||||
if (rn.getType() == NodeType.DO_TRY) {
|
if (rn.getType() == NodeType.DO_TRY) {
|
||||||
// DO_TRY: extract virtual _TRY_BODY wrapper first, then handler children
|
// DO_TRY: extract virtual _TRY_BODY wrapper first, then handler children
|
||||||
String wrapperId = rn.getId() + "._try_body";
|
String wrapperId = rn.getId() + "._try_body";
|
||||||
ElkNode wrapperElk = elkNodeMap.get(wrapperId);
|
ElkNode wrapperElk = ctx.elkNodeMap.get(wrapperId);
|
||||||
if (wrapperElk != null) {
|
if (wrapperElk != null) {
|
||||||
List<PositionedNode> wrapperChildren = new ArrayList<>();
|
List<PositionedNode> wrapperChildren = new ArrayList<>();
|
||||||
for (RouteNode child : rn.getChildren()) {
|
for (RouteNode child : rn.getChildren()) {
|
||||||
if (child.getType() != NodeType.DO_CATCH && child.getType() != NodeType.DO_FINALLY) {
|
if (child.getType() != NodeType.DO_CATCH && child.getType() != NodeType.DO_FINALLY) {
|
||||||
ElkNode childElk = elkNodeMap.get(child.getId());
|
ElkNode childElk = ctx.elkNodeMap.get(child.getId());
|
||||||
if (childElk != null) {
|
if (childElk != null) {
|
||||||
wrapperChildren.add(extractPositionedNode(child, childElk, elkNodeMap,
|
wrapperChildren.add(extractPositionedNode(child, childElk, rootNode, ctx));
|
||||||
compoundNodeIds, compoundInfos, rootNode));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -711,37 +658,24 @@ public class ElkDiagramRenderer implements DiagramRenderer {
|
|||||||
getAbsoluteY(wrapperElk, rootNode),
|
getAbsoluteY(wrapperElk, rootNode),
|
||||||
wrapperElk.getWidth(), wrapperElk.getHeight(),
|
wrapperElk.getWidth(), wrapperElk.getHeight(),
|
||||||
wrapperChildren));
|
wrapperChildren));
|
||||||
compoundInfos.put(wrapperId, new CompoundInfo(wrapperId, Color.WHITE));
|
ctx.compoundInfos.put(wrapperId, new CompoundInfo(wrapperId, Color.WHITE));
|
||||||
}
|
}
|
||||||
// Handler children: DO_FINALLY first, then DO_CATCH
|
// Handler children in order: DO_FINALLY first, then DO_CATCH
|
||||||
for (RouteNode child : rn.getChildren()) {
|
for (RouteNode handler : orderedHandlerChildren(rn.getChildren())) {
|
||||||
if (child.getType() == NodeType.DO_FINALLY) {
|
ElkNode childElk = ctx.elkNodeMap.get(handler.getId());
|
||||||
ElkNode childElk = elkNodeMap.get(child.getId());
|
if (childElk != null) {
|
||||||
if (childElk != null) {
|
children.add(extractPositionedNode(handler, childElk, rootNode, ctx));
|
||||||
children.add(extractPositionedNode(child, childElk, elkNodeMap,
|
|
||||||
compoundNodeIds, compoundInfos, rootNode));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (RouteNode child : rn.getChildren()) {
|
|
||||||
if (child.getType() == NodeType.DO_CATCH) {
|
|
||||||
ElkNode childElk = elkNodeMap.get(child.getId());
|
|
||||||
if (childElk != null) {
|
|
||||||
children.add(extractPositionedNode(child, childElk, elkNodeMap,
|
|
||||||
compoundNodeIds, compoundInfos, rootNode));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (RouteNode child : rn.getChildren()) {
|
for (RouteNode child : rn.getChildren()) {
|
||||||
ElkNode childElk = elkNodeMap.get(child.getId());
|
ElkNode childElk = ctx.elkNodeMap.get(child.getId());
|
||||||
if (childElk != null) {
|
if (childElk != null) {
|
||||||
children.add(extractPositionedNode(child, childElk, elkNodeMap,
|
children.add(extractPositionedNode(child, childElk, rootNode, ctx));
|
||||||
compoundNodeIds, compoundInfos, rootNode));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
compoundInfos.put(rn.getId(), new CompoundInfo(rn.getId(), colorForType(rn.getType())));
|
ctx.compoundInfos.put(rn.getId(), new CompoundInfo(rn.getId(), colorForType(rn.getType())));
|
||||||
}
|
}
|
||||||
|
|
||||||
return new PositionedNode(
|
return new PositionedNode(
|
||||||
@@ -758,7 +692,18 @@ public class ElkDiagramRenderer implements DiagramRenderer {
|
|||||||
// ELK graph helpers
|
// ELK graph helpers
|
||||||
// ----------------------------------------------------------------
|
// ----------------------------------------------------------------
|
||||||
|
|
||||||
/** Walk up to the top-level root of an ELK node's hierarchy. */
|
/** Return handler children in section order: DO_FINALLY first, then DO_CATCH. */
|
||||||
|
private static List<RouteNode> orderedHandlerChildren(List<RouteNode> children) {
|
||||||
|
List<RouteNode> ordered = new ArrayList<>();
|
||||||
|
for (RouteNode c : children) {
|
||||||
|
if (c.getType() == NodeType.DO_FINALLY) ordered.add(c);
|
||||||
|
}
|
||||||
|
for (RouteNode c : children) {
|
||||||
|
if (c.getType() == NodeType.DO_CATCH) ordered.add(c);
|
||||||
|
}
|
||||||
|
return ordered;
|
||||||
|
}
|
||||||
|
|
||||||
/** Check if 'child' is a descendant of 'ancestor' in the ELK node hierarchy. */
|
/** Check if 'child' is a descendant of 'ancestor' in the ELK node hierarchy. */
|
||||||
private boolean isDescendantOf(ElkNode child, ElkNode ancestor) {
|
private boolean isDescendantOf(ElkNode child, ElkNode ancestor) {
|
||||||
ElkNode current = child.getParent();
|
ElkNode current = child.getParent();
|
||||||
@@ -847,7 +792,6 @@ public class ElkDiagramRenderer implements DiagramRenderer {
|
|||||||
return all;
|
return all;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Recursively collect all node IDs from a tree. */
|
|
||||||
/** Collect IDs of all RouteNode descendants (for handler separation). */
|
/** Collect IDs of all RouteNode descendants (for handler separation). */
|
||||||
private void collectDescendantIds(List<RouteNode> nodes, Set<String> ids) {
|
private void collectDescendantIds(List<RouteNode> nodes, Set<String> ids) {
|
||||||
for (RouteNode n : nodes) {
|
for (RouteNode n : nodes) {
|
||||||
@@ -878,4 +822,20 @@ public class ElkDiagramRenderer implements DiagramRenderer {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
private record CompoundInfo(String nodeId, Color color) {}
|
private record CompoundInfo(String nodeId, Color color) {}
|
||||||
|
|
||||||
|
/** Mutable state accumulated during ELK graph construction and extraction. */
|
||||||
|
private static class LayoutContext {
|
||||||
|
final ElkGraphFactory factory;
|
||||||
|
final Map<String, ElkNode> elkNodeMap = new HashMap<>();
|
||||||
|
final Map<String, Color> nodeColors = new HashMap<>();
|
||||||
|
final Set<String> compoundNodeIds = new HashSet<>();
|
||||||
|
final Set<String> childNodeIds = new HashSet<>();
|
||||||
|
final Set<String> doTryNodeIds = new HashSet<>();
|
||||||
|
final Map<String, List<String>> doTrySectionOrder = new LinkedHashMap<>();
|
||||||
|
final Map<String, CompoundInfo> compoundInfos = new HashMap<>();
|
||||||
|
|
||||||
|
LayoutContext(ElkGraphFactory factory) {
|
||||||
|
this.factory = factory;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -172,6 +172,98 @@ class ElkDiagramRendererTest {
|
|||||||
assertEquals(2, choiceNode.children().size(), "Choice node should have 2 children (when, otherwise)");
|
assertEquals(2, choiceNode.children().size(), "Choice node should have 2 children (when, otherwise)");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a DO_TRY graph: from -> doTry(try: [process, log], doFinally: [cleanup], doCatch: [errorLog]) -> to
|
||||||
|
*/
|
||||||
|
private RouteGraph buildDoTryGraph() {
|
||||||
|
RouteGraph graph = new RouteGraph("try-catch-route");
|
||||||
|
graph.setExtractedAt(Instant.now());
|
||||||
|
graph.setVersion(1);
|
||||||
|
|
||||||
|
RouteNode from = new RouteNode("node-1", NodeType.ENDPOINT, "timer:tick");
|
||||||
|
RouteNode doTry = new RouteNode("node-2", NodeType.DO_TRY, "doTry");
|
||||||
|
RouteNode process = new RouteNode("node-3", NodeType.PROCESSOR, "process");
|
||||||
|
RouteNode log1 = new RouteNode("node-4", NodeType.LOG, "log:tryBody");
|
||||||
|
RouteNode doFinally = new RouteNode("node-5", NodeType.DO_FINALLY, "doFinally");
|
||||||
|
RouteNode cleanup = new RouteNode("node-6", NodeType.LOG, "log:cleanup");
|
||||||
|
RouteNode doCatch = new RouteNode("node-7", NodeType.DO_CATCH, "doCatch");
|
||||||
|
RouteNode errorLog = new RouteNode("node-8", NodeType.LOG, "log:error");
|
||||||
|
RouteNode to = new RouteNode("node-9", NodeType.TO, "log:done");
|
||||||
|
|
||||||
|
doFinally.setChildren(List.of(cleanup));
|
||||||
|
doCatch.setChildren(List.of(errorLog));
|
||||||
|
doTry.setChildren(List.of(process, log1, doFinally, doCatch));
|
||||||
|
|
||||||
|
graph.setRoot(from);
|
||||||
|
graph.setNodes(List.of(from, doTry, process, log1, doFinally, cleanup, doCatch, errorLog, to));
|
||||||
|
graph.setEdges(List.of(
|
||||||
|
new RouteEdge("node-1", "node-2", RouteEdge.EdgeType.FLOW),
|
||||||
|
new RouteEdge("node-2", "node-3", RouteEdge.EdgeType.FLOW),
|
||||||
|
new RouteEdge("node-3", "node-4", RouteEdge.EdgeType.FLOW),
|
||||||
|
new RouteEdge("node-2", "node-5", RouteEdge.EdgeType.FLOW),
|
||||||
|
new RouteEdge("node-5", "node-6", RouteEdge.EdgeType.FLOW),
|
||||||
|
new RouteEdge("node-2", "node-7", RouteEdge.EdgeType.FLOW),
|
||||||
|
new RouteEdge("node-7", "node-8", RouteEdge.EdgeType.FLOW),
|
||||||
|
new RouteEdge("node-2", "node-9", RouteEdge.EdgeType.FLOW)
|
||||||
|
));
|
||||||
|
|
||||||
|
return graph;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void layoutJson_doTryGraph_sectionsInCorrectOrder() {
|
||||||
|
DiagramLayout layout = renderer.layoutJson(buildDoTryGraph());
|
||||||
|
|
||||||
|
assertNotNull(layout);
|
||||||
|
|
||||||
|
// Find the DO_TRY compound node
|
||||||
|
PositionedNode doTryNode = layout.nodes().stream()
|
||||||
|
.filter(n -> "node-2".equals(n.id()))
|
||||||
|
.findFirst()
|
||||||
|
.orElseThrow(() -> new AssertionError("DO_TRY node not found"));
|
||||||
|
|
||||||
|
assertNotNull(doTryNode.children(), "DO_TRY should have children");
|
||||||
|
assertFalse(doTryNode.children().isEmpty(), "DO_TRY should have non-empty children");
|
||||||
|
|
||||||
|
// Find sections by ID pattern
|
||||||
|
PositionedNode tryBody = doTryNode.children().stream()
|
||||||
|
.filter(n -> n.id() != null && n.id().contains("._try_body"))
|
||||||
|
.findFirst().orElse(null);
|
||||||
|
PositionedNode finallySection = doTryNode.children().stream()
|
||||||
|
.filter(n -> "node-5".equals(n.id()))
|
||||||
|
.findFirst().orElse(null);
|
||||||
|
PositionedNode catchSection = doTryNode.children().stream()
|
||||||
|
.filter(n -> "node-7".equals(n.id()))
|
||||||
|
.findFirst().orElse(null);
|
||||||
|
|
||||||
|
assertNotNull(tryBody, "Try body wrapper should exist");
|
||||||
|
assertNotNull(finallySection, "doFinally section should exist");
|
||||||
|
assertNotNull(catchSection, "doCatch section should exist");
|
||||||
|
|
||||||
|
// Verify vertical order: tryBody.y < doFinally.y < doCatch.y
|
||||||
|
assertTrue(tryBody.y() < finallySection.y(),
|
||||||
|
"Try body (y=" + tryBody.y() + ") should be above doFinally (y=" + finallySection.y() + ")");
|
||||||
|
assertTrue(finallySection.y() < catchSection.y(),
|
||||||
|
"doFinally (y=" + finallySection.y() + ") should be above doCatch (y=" + catchSection.y() + ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void layoutJson_doTryGraph_sectionsHaveSameWidth() {
|
||||||
|
DiagramLayout layout = renderer.layoutJson(buildDoTryGraph());
|
||||||
|
|
||||||
|
PositionedNode doTryNode = layout.nodes().stream()
|
||||||
|
.filter(n -> "node-2".equals(n.id()))
|
||||||
|
.findFirst()
|
||||||
|
.orElseThrow(() -> new AssertionError("DO_TRY node not found"));
|
||||||
|
|
||||||
|
List<PositionedNode> sections = doTryNode.children();
|
||||||
|
double firstWidth = sections.get(0).width();
|
||||||
|
for (PositionedNode section : sections) {
|
||||||
|
assertEquals(firstWidth, section.width(), 0.1,
|
||||||
|
"All sections should have the same width, but " + section.id() + " differs");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void renderSvg_compoundGraph_producesValidSvg() {
|
void renderSvg_compoundGraph_producesValidSvg() {
|
||||||
String svg = renderer.renderSvg(buildCompoundGraph());
|
String svg = renderer.renderSvg(buildCompoundGraph());
|
||||||
|
|||||||
Reference in New Issue
Block a user