This is an automated email from the ASF dual-hosted git repository.

gnodet pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new f3ddfd578c3a chore(camel-diagram): Code cleanup and Unicode-safe text 
wrapping
f3ddfd578c3a is described below

commit f3ddfd578c3a64e46ab519329607c991ff0eeb8a
Author: Guillaume Nodet <[email protected]>
AuthorDate: Tue May 5 23:09:59 2026 +0200

    chore(camel-diagram): Code cleanup and Unicode-safe text wrapping
    
    - Fix Graphics2D resource leak in constructor (try-finally)
    - Replace int[] bounding box with Bounds class
    - Reduce visibility of internal-only APIs
    - Remove redundant instance resolveLabel wrapper
    - Extract getTopY() helper to deduplicate scope-check ternary
    - Share NODE_TEXT_PADDING via getter instead of duplicating constant
    - Named constants for magic numbers (DEFAULT_FONT_SIZE, DEFAULT_BOX_WIDTH)
    - Fix wrapLabel to use BreakIterator for grapheme-safe Unicode handling
      (emojis, combining marks, ZWJ sequences)
    - Add null-guard in JSON parsing
    
    Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
---
 .../apache/camel/diagram/RouteDiagramHelper.java   |   6 +-
 .../camel/diagram/RouteDiagramLayoutEngine.java    | 143 ++++++++++++++-------
 .../apache/camel/diagram/RouteDiagramRenderer.java |  37 ++++--
 .../org/apache/camel/diagram/RouteDiagramTest.java |  54 ++++++++
 .../commands/action/CamelRouteDiagramAction.java   |   2 +-
 5 files changed, 177 insertions(+), 65 deletions(-)

diff --git 
a/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramHelper.java
 
b/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramHelper.java
index 75dd4faeaa4f..c6ca5c5f39dc 100644
--- 
a/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramHelper.java
+++ 
b/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramHelper.java
@@ -48,7 +48,11 @@ public final class RouteDiagramHelper {
         }
 
         for (int i = 0; i < arr.size(); i++) {
-            JsonObject o = (JsonObject) arr.get(i);
+            Object item = arr.get(i);
+            if (!(item instanceof JsonObject)) {
+                continue;
+            }
+            JsonObject o = (JsonObject) item;
             RouteInfo route = new RouteInfo();
             route.routeId = o.getString("routeId");
             String source = o.getString("source");
diff --git 
a/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramLayoutEngine.java
 
b/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramLayoutEngine.java
index a139fab5aa52..e865f5dcec66 100644
--- 
a/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramLayoutEngine.java
+++ 
b/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramLayoutEngine.java
@@ -20,6 +20,7 @@ import java.awt.Font;
 import java.awt.FontMetrics;
 import java.awt.Graphics2D;
 import java.awt.image.BufferedImage;
+import java.text.BreakIterator;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Set;
@@ -30,13 +31,16 @@ import java.util.Set;
 public class RouteDiagramLayoutEngine {
 
     public static final int SCALE = 2;
-    public static final int H_GAP = 30 * SCALE;
+    private static final int H_GAP = 30 * SCALE;
     public static final int V_GAP = 40 * SCALE;
     public static final int PADDING = 30 * SCALE;
     public static final int SCOPE_BOX_PAD = 14 * SCALE;
-    public static final int LABEL_OFFSET = 24 * SCALE;
+    private static final int LABEL_OFFSET = 24 * SCALE;
     private static final int NODE_TEXT_PADDING = 16 * SCALE;
     private static final int MAX_WRAP_LINES = 3;
+    public static final int DEFAULT_FONT_SIZE = 12;
+    public static final int DEFAULT_BOX_WIDTH = 180;
+    private static final int DEFAULT_NODE_HEIGHT = 32;
 
     public enum NodeLabelMode {
         CODE,
@@ -58,8 +62,26 @@ public class RouteDiagramLayoutEngine {
     private static final Set<String> STRUCTURAL_TYPES = Set.of(
             "route", "from");
 
+    static class Bounds {
+        int minX, minY, maxX, maxY;
+
+        Bounds(int minX, int minY, int maxX, int maxY) {
+            this.minX = minX;
+            this.minY = minY;
+            this.maxX = maxX;
+            this.maxY = maxY;
+        }
+
+        void expand(Bounds other) {
+            minX = Math.min(minX, other.minX);
+            minY = Math.min(minY, other.minY);
+            maxX = Math.max(maxX, other.maxX);
+            maxY = Math.max(maxY, other.maxY);
+        }
+    }
+
     public RouteDiagramLayoutEngine() {
-        this(180, 12);
+        this(DEFAULT_BOX_WIDTH, DEFAULT_FONT_SIZE);
     }
 
     /**
@@ -77,13 +99,16 @@ public class RouteDiagramLayoutEngine {
      */
     public RouteDiagramLayoutEngine(int boxWidth, int fontSize, NodeLabelMode 
nodeLabelMode) {
         this.nodeWidth = boxWidth * SCALE;
-        this.baseNodeHeight = Math.max(32, fontSize * 32 / 12) * SCALE;
+        this.baseNodeHeight = Math.max(DEFAULT_NODE_HEIGHT, fontSize * 
DEFAULT_NODE_HEIGHT / DEFAULT_FONT_SIZE) * SCALE;
         this.nodeLabelMode = nodeLabelMode != null ? nodeLabelMode : 
NodeLabelMode.CODE;
         BufferedImage scratch = new BufferedImage(1, 1, 
BufferedImage.TYPE_INT_ARGB);
         Graphics2D g = scratch.createGraphics();
-        g.setFont(new Font("SansSerif", Font.PLAIN, fontSize * SCALE));
-        this.fontMetrics = g.getFontMetrics();
-        g.dispose();
+        try {
+            g.setFont(new Font("SansSerif", Font.PLAIN, fontSize * SCALE));
+            this.fontMetrics = g.getFontMetrics();
+        } finally {
+            g.dispose();
+        }
     }
 
     public int getNodeWidth() {
@@ -94,6 +119,10 @@ public class RouteDiagramLayoutEngine {
         return baseNodeHeight;
     }
 
+    public int getNodeTextPadding() {
+        return NODE_TEXT_PADDING;
+    }
+
     public static class NodeInfo {
         public String type;
         public String code;
@@ -189,7 +218,7 @@ public class RouteDiagramLayoutEngine {
         return code.replaceFirst("^\\.", "");
     }
 
-    List<String> wrapLabel(String label) {
+    private List<String> wrapLabel(String label) {
         int maxTextWidth = nodeWidth - NODE_TEXT_PADDING;
         if (fontMetrics.stringWidth(label) <= maxTextWidth) {
             return List.of(label);
@@ -203,37 +232,55 @@ public class RouteDiagramLayoutEngine {
                 break;
             }
             int breakAt = -1;
-            for (int i = 1; i < remaining.length(); i++) {
-                if (fontMetrics.stringWidth(remaining.substring(0, i + 1)) > 
maxTextWidth) {
+            BreakIterator bi = BreakIterator.getCharacterInstance();
+            bi.setText(remaining);
+            int start = bi.first();
+            int end = bi.next();
+            while (end != BreakIterator.DONE) {
+                String candidate = remaining.substring(0, end);
+                if (fontMetrics.stringWidth(candidate) > maxTextWidth) {
                     break;
                 }
-                char c = remaining.charAt(i);
+                char c = remaining.charAt(end - 1);
                 if (c == ' ' || c == ':' || c == '/' || c == '.' || c == ',' 
|| c == '&' || c == '?') {
-                    breakAt = i + 1;
+                    breakAt = end;
                 }
+                start = end;
+                end = bi.next();
             }
             if (breakAt <= 0) {
-                // hard break at last fitting character
-                breakAt = 1;
-                for (int i = 2; i < remaining.length(); i++) {
-                    if (fontMetrics.stringWidth(remaining.substring(0, i)) > 
maxTextWidth) {
+                breakAt = 0;
+                bi.setText(remaining);
+                start = bi.first();
+                end = bi.next();
+                while (end != BreakIterator.DONE) {
+                    String candidate = remaining.substring(0, end);
+                    if (fontMetrics.stringWidth(candidate) > maxTextWidth) {
                         break;
                     }
-                    breakAt = i;
+                    breakAt = end;
+                    start = end;
+                    end = bi.next();
+                }
+                if (breakAt <= 0) {
+                    breakAt = bi.first();
+                    end = bi.next();
+                    if (end != BreakIterator.DONE) {
+                        breakAt = end;
+                    }
                 }
             }
             lines.add(remaining.substring(0, breakAt));
             remaining = remaining.substring(breakAt).stripLeading();
         }
         if (!remaining.isEmpty()) {
-            // append overflow to last line for truncation by the renderer
             int lastIdx = lines.size() - 1;
             lines.set(lastIdx, lines.get(lastIdx) + remaining);
         }
         return lines;
     }
 
-    int computeNodeHeight(List<String> lines) {
+    private int computeNodeHeight(List<String> lines) {
         int lineCount = Math.min(lines.size(), MAX_WRAP_LINES);
         if (lineCount <= 1) {
             return baseNodeHeight;
@@ -242,10 +289,6 @@ public class RouteDiagramLayoutEngine {
         return baseNodeHeight + (lineCount - 1) * lineSpacing;
     }
 
-    List<String> resolveLabel(NodeInfo info) {
-        return resolveLabel(info, nodeLabelMode);
-    }
-
     public static List<String> resolveLabel(NodeInfo info, NodeLabelMode mode) 
{
         String code = cleanLabel(info.code);
         boolean hasDesc = info.description != null && 
!info.description.isBlank();
@@ -278,26 +321,26 @@ public class RouteDiagramLayoutEngine {
         computeSubtreeWidth(tree);
         assignPositions(tree, PADDING, startY + LABEL_OFFSET, 
tree.subtreeWidth, lr);
 
-        int[] extent = {
+        Bounds extent = new Bounds(
                 tree.layoutNode.x, tree.layoutNode.y,
-                tree.layoutNode.x + nodeWidth, tree.layoutNode.y + 
tree.layoutNode.height };
+                tree.layoutNode.x + nodeWidth, tree.layoutNode.y + 
tree.layoutNode.height);
         for (TreeNode child : tree.children) {
             expandBoundsForBox(child, extent, nodeWidth);
         }
 
-        if (extent[0] < PADDING) {
-            int shift = PADDING - extent[0];
+        if (extent.minX < PADDING) {
+            int shift = PADDING - extent.minX;
             for (LayoutNode ln : lr.nodes) {
                 ln.x += shift;
                 if (ln.connectFromMerge) {
                     ln.mergeCx += shift;
                 }
             }
-            extent[2] += shift;
+            extent.maxX += shift;
         }
 
-        lr.maxX = extent[2];
-        lr.maxY = Math.max(lr.maxY, extent[3]);
+        lr.maxX = extent.maxX;
+        lr.maxY = Math.max(lr.maxY, extent.maxY);
 
         return lr;
     }
@@ -335,7 +378,7 @@ public class RouteDiagramLayoutEngine {
         ln.type = node.info.type;
         ln.x = nodeX;
         ln.y = y;
-        ln.wrappedLines = resolveLabel(node.info).stream()
+        ln.wrappedLines = resolveLabel(node.info, nodeLabelMode).stream()
                 .flatMap(s -> wrapLabel(s).stream()).toList();
         ln.height = computeNodeHeight(ln.wrappedLines);
         ln.treeNode = node;
@@ -350,14 +393,14 @@ public class RouteDiagramLayoutEngine {
                     TreeNode prevSibling = parentNode.children.get(myIndex - 
1);
                     if (hasScope(prevSibling)) {
                         ln.connectFromMerge = true;
-                        int[] boxBounds = {
+                        Bounds boxBounds = new Bounds(
                                 prevSibling.layoutNode.x, 
prevSibling.layoutNode.y,
                                 prevSibling.layoutNode.x + nodeWidth,
-                                prevSibling.layoutNode.y + 
prevSibling.layoutNode.height };
+                                prevSibling.layoutNode.y + 
prevSibling.layoutNode.height);
                         for (TreeNode c : prevSibling.children) {
                             expandBoundsForBox(c, boxBounds, nodeWidth);
                         }
-                        ln.mergeY = boxBounds[3] + SCOPE_BOX_PAD;
+                        ln.mergeY = boxBounds.maxY + SCOPE_BOX_PAD;
                         ln.mergeCx = prevSibling.layoutNode.x + nodeWidth / 2;
                         ln.parentNode = prevSibling.layoutNode;
                     } else {
@@ -396,13 +439,13 @@ public class RouteDiagramLayoutEngine {
                 int adjustedY = hasScope(child) ? curY + SCOPE_BOX_PAD : curY;
                 assignPositions(child, x, adjustedY, availableWidth, lr);
                 if (hasScope(child)) {
-                    int[] cb = {
+                    Bounds cb = new Bounds(
                             child.layoutNode.x, child.layoutNode.y,
-                            child.layoutNode.x + nodeWidth, child.layoutNode.y 
+ child.layoutNode.height };
+                            child.layoutNode.x + nodeWidth, child.layoutNode.y 
+ child.layoutNode.height);
                     for (TreeNode c : child.children) {
                         expandBoundsForBox(c, cb, nodeWidth);
                     }
-                    curY = cb[3] + SCOPE_BOX_PAD + V_GAP;
+                    curY = cb.maxY + SCOPE_BOX_PAD + V_GAP;
                 } else {
                     curY = findMaxY(child) + V_GAP;
                 }
@@ -410,7 +453,7 @@ public class RouteDiagramLayoutEngine {
         }
     }
 
-    public static LayoutNode findLastLayoutNode(TreeNode node) {
+    private static LayoutNode findLastLayoutNode(TreeNode node) {
         if (node.children.isEmpty()) {
             return node.layoutNode;
         }
@@ -420,7 +463,7 @@ public class RouteDiagramLayoutEngine {
         return findLastLayoutNode(node.children.get(node.children.size() - 1));
     }
 
-    public static int findMaxY(TreeNode node) {
+    private static int findMaxY(TreeNode node) {
         int maxY = node.layoutNode != null ? node.layoutNode.y + 
node.layoutNode.height : 0;
         for (TreeNode child : node.children) {
             maxY = Math.max(maxY, findMaxY(child));
@@ -439,26 +482,26 @@ public class RouteDiagramLayoutEngine {
                 && !STRUCTURAL_TYPES.contains(node.info.type);
     }
 
-    public static void expandBoundsForBox(TreeNode node, int[] bounds, int 
nodeWidth) {
+    public static void expandBoundsForBox(TreeNode node, Bounds bounds, int 
nodeWidth) {
         boolean hasOwnBox = hasScope(node);
 
         if (hasOwnBox) {
-            int[] inner = {
+            Bounds inner = new Bounds(
                     node.layoutNode.x, node.layoutNode.y,
-                    node.layoutNode.x + nodeWidth, node.layoutNode.y + 
node.layoutNode.height };
+                    node.layoutNode.x + nodeWidth, node.layoutNode.y + 
node.layoutNode.height);
             for (TreeNode child : node.children) {
                 expandBoundsForBox(child, inner, nodeWidth);
             }
-            bounds[0] = Math.min(bounds[0], inner[0] - SCOPE_BOX_PAD);
-            bounds[1] = Math.min(bounds[1], inner[1] - SCOPE_BOX_PAD);
-            bounds[2] = Math.max(bounds[2], inner[2] + SCOPE_BOX_PAD);
-            bounds[3] = Math.max(bounds[3], inner[3] + SCOPE_BOX_PAD);
+            bounds.minX = Math.min(bounds.minX, inner.minX - SCOPE_BOX_PAD);
+            bounds.minY = Math.min(bounds.minY, inner.minY - SCOPE_BOX_PAD);
+            bounds.maxX = Math.max(bounds.maxX, inner.maxX + SCOPE_BOX_PAD);
+            bounds.maxY = Math.max(bounds.maxY, inner.maxY + SCOPE_BOX_PAD);
         } else {
             if (node.layoutNode != null) {
-                bounds[0] = Math.min(bounds[0], node.layoutNode.x);
-                bounds[1] = Math.min(bounds[1], node.layoutNode.y);
-                bounds[2] = Math.max(bounds[2], node.layoutNode.x + nodeWidth);
-                bounds[3] = Math.max(bounds[3], node.layoutNode.y + 
node.layoutNode.height);
+                bounds.minX = Math.min(bounds.minX, node.layoutNode.x);
+                bounds.minY = Math.min(bounds.minY, node.layoutNode.y);
+                bounds.maxX = Math.max(bounds.maxX, node.layoutNode.x + 
nodeWidth);
+                bounds.maxY = Math.max(bounds.maxY, node.layoutNode.y + 
node.layoutNode.height);
             }
             for (TreeNode child : node.children) {
                 expandBoundsForBox(child, bounds, nodeWidth);
diff --git 
a/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramRenderer.java
 
b/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramRenderer.java
index 2a04dfec5396..d3ea6e4ada84 100644
--- 
a/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramRenderer.java
+++ 
b/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramRenderer.java
@@ -28,6 +28,7 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
+import org.apache.camel.diagram.RouteDiagramLayoutEngine.Bounds;
 import org.apache.camel.diagram.RouteDiagramLayoutEngine.LayoutNode;
 import org.apache.camel.diagram.RouteDiagramLayoutEngine.LayoutRoute;
 import org.apache.camel.diagram.RouteDiagramLayoutEngine.RouteInfo;
@@ -71,14 +72,21 @@ public class RouteDiagramRenderer {
             "transparent", TRANSPARENT_COLORS);
 
     public RouteDiagramRenderer() {
-        this(180 * SCALE, 12 * SCALE);
+        this(RouteDiagramLayoutEngine.DEFAULT_BOX_WIDTH * SCALE,
+             RouteDiagramLayoutEngine.DEFAULT_FONT_SIZE * SCALE,
+             new RouteDiagramLayoutEngine().getNodeTextPadding());
     }
 
     public RouteDiagramRenderer(int nodeWidth, int fontSizeScaled) {
+        this(nodeWidth, fontSizeScaled, new RouteDiagramLayoutEngine(
+                nodeWidth / SCALE, fontSizeScaled / 
SCALE).getNodeTextPadding());
+    }
+
+    public RouteDiagramRenderer(int nodeWidth, int fontSizeScaled, int 
nodeTextPadding) {
         this.nodeWidth = nodeWidth;
         this.fontSizeNode = fontSizeScaled;
         this.fontSizeLabel = fontSizeScaled + 1 * SCALE;
-        this.nodeTextPadding = 16 * SCALE;
+        this.nodeTextPadding = nodeTextPadding;
     }
 
     public static class DiagramColors {
@@ -254,15 +262,17 @@ public class RouteDiagramRenderer {
 
     private void drawScopeBox(Graphics2D g, LayoutNode scopeNode, 
DiagramColors colors) {
         TreeNode tn = scopeNode.treeNode;
-        int[] bounds = { scopeNode.x, scopeNode.y, scopeNode.x + nodeWidth, 
scopeNode.y + scopeNode.height };
+        Bounds bounds = new Bounds(
+                scopeNode.x, scopeNode.y,
+                scopeNode.x + nodeWidth, scopeNode.y + scopeNode.height);
         for (TreeNode child : tn.children) {
             RouteDiagramLayoutEngine.expandBoundsForBox(child, bounds, 
nodeWidth);
         }
 
-        int boxX = bounds[0] - SCOPE_BOX_PAD;
-        int boxY = bounds[1] - SCOPE_BOX_PAD;
-        int boxW = bounds[2] - bounds[0] + 2 * SCOPE_BOX_PAD;
-        int boxH = bounds[3] - bounds[1] + 2 * SCOPE_BOX_PAD;
+        int boxX = bounds.minX - SCOPE_BOX_PAD;
+        int boxY = bounds.minY - SCOPE_BOX_PAD;
+        int boxW = bounds.maxX - bounds.minX + 2 * SCOPE_BOX_PAD;
+        int boxH = bounds.maxY - bounds.minY + 2 * SCOPE_BOX_PAD;
 
         Color c = colors.getArrow();
         g.setColor(new Color(c.getRed(), c.getGreen(), c.getBlue(), 140));
@@ -270,14 +280,17 @@ public class RouteDiagramRenderer {
         g.drawRoundRect(boxX, boxY, boxW, boxH, ARC, ARC);
     }
 
+    private int getTopY(LayoutNode node) {
+        return node.treeNode != null && 
RouteDiagramLayoutEngine.hasScope(node.treeNode)
+                ? node.y - SCOPE_BOX_PAD : node.y;
+    }
+
     private void drawArrowFromMerge(Graphics2D g, LayoutNode to, DiagramColors 
colors) {
         g.setColor(colors.getArrow());
         g.setStroke(new BasicStroke(STROKE_WIDTH));
 
         int toCx = to.x + nodeWidth / 2;
-        int toTy = to.treeNode != null && 
RouteDiagramLayoutEngine.hasScope(to.treeNode)
-                ? to.y - SCOPE_BOX_PAD
-                : to.y;
+        int toTy = getTopY(to);
         int mergeCx = to.mergeCx;
         int mergeY = to.mergeY;
 
@@ -332,9 +345,7 @@ public class RouteDiagramRenderer {
         int fromCx = from.x + nodeWidth / 2;
         int fromBy = from.y + from.height;
         int toCx = to.x + nodeWidth / 2;
-        int toTy = to.treeNode != null && 
RouteDiagramLayoutEngine.hasScope(to.treeNode)
-                ? to.y - SCOPE_BOX_PAD
-                : to.y;
+        int toTy = getTopY(to);
 
         if (fromCx == toCx) {
             g.drawLine(fromCx, fromBy, toCx, toTy - ARROW_SIZE / 2);
diff --git 
a/components/camel-diagram/src/test/java/org/apache/camel/diagram/RouteDiagramTest.java
 
b/components/camel-diagram/src/test/java/org/apache/camel/diagram/RouteDiagramTest.java
index 5c75685598fa..5bae36a04282 100644
--- 
a/components/camel-diagram/src/test/java/org/apache/camel/diagram/RouteDiagramTest.java
+++ 
b/components/camel-diagram/src/test/java/org/apache/camel/diagram/RouteDiagramTest.java
@@ -707,6 +707,60 @@ class RouteDiagramTest {
         assertTrue(image.getHeight() > 0);
     }
 
+    @Test
+    void testWrapLabelWithEmoji() {
+        RouteInfo route = new RouteInfo();
+        route.routeId = "route1";
+        route.nodes.add(node("to", "Send order 📦 to warehouse processing 
system queue", 0));
+
+        RouteDiagramLayoutEngine engine = new RouteDiagramLayoutEngine();
+        LayoutRoute lr = engine.layoutRoute(route, 
RouteDiagramLayoutEngine.PADDING);
+
+        LayoutNode n = lr.nodes.get(0);
+        String rejoined = String.join("", n.wrappedLines);
+        assertTrue(rejoined.contains("📦"), "Emoji must not be split across 
lines");
+        for (String line : n.wrappedLines) {
+            for (int i = 0; i < line.length(); i++) {
+                char c = line.charAt(i);
+                if (Character.isHighSurrogate(c)) {
+                    assertTrue(i + 1 < line.length() && 
Character.isLowSurrogate(line.charAt(i + 1)),
+                            "High surrogate at " + i + " must be followed by 
low surrogate in: " + line);
+                } else if (Character.isLowSurrogate(c)) {
+                    assertTrue(i > 0 && 
Character.isHighSurrogate(line.charAt(i - 1)),
+                            "Low surrogate at " + i + " must be preceded by 
high surrogate in: " + line);
+                }
+            }
+        }
+    }
+
+    @Test
+    void testWrapLabelWithCombiningMarks() {
+        RouteInfo route = new RouteInfo();
+        route.routeId = "route1";
+        route.nodes.add(node("to", "Résumé café order processing endpoint", 
0));
+
+        RouteDiagramLayoutEngine engine = new RouteDiagramLayoutEngine();
+        LayoutRoute lr = engine.layoutRoute(route, 
RouteDiagramLayoutEngine.PADDING);
+
+        LayoutNode n = lr.nodes.get(0);
+        String rejoined = String.join("", n.wrappedLines);
+        assertTrue(rejoined.contains("é"), "Combining accent must stay 
attached to base character");
+    }
+
+    @Test
+    void testWrapLabelWithZwjEmoji() {
+        RouteInfo route = new RouteInfo();
+        route.routeId = "route1";
+        route.nodes.add(node("to", "Family 👨‍👩‍👧‍👦 order processing", 0));
+
+        RouteDiagramLayoutEngine engine = new RouteDiagramLayoutEngine();
+        LayoutRoute lr = engine.layoutRoute(route, 
RouteDiagramLayoutEngine.PADDING);
+
+        LayoutNode n = lr.nodes.get(0);
+        String rejoined = String.join("", n.wrappedLines);
+        assertTrue(rejoined.contains("👨"), "ZWJ emoji must not be split");
+    }
+
     private static NodeInfo node(String type, String code, int level) {
         return node(type, code, level, null);
     }
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteDiagramAction.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteDiagramAction.java
index 3b84256441bc..79cbe5456ad4 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteDiagramAction.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteDiagramAction.java
@@ -191,7 +191,7 @@ public class CamelRouteDiagramAction extends 
ActionBaseCommand {
             NodeLabelMode labelMode = parseNodeLabelMode(nodeLabel);
             RouteDiagramLayoutEngine engine = new 
RouteDiagramLayoutEngine(boxWidth, fontSize, labelMode);
             RouteDiagramRenderer renderer = new RouteDiagramRenderer(
-                    engine.getNodeWidth(), fontSize * 
RouteDiagramLayoutEngine.SCALE);
+                    engine.getNodeWidth(), fontSize * 
RouteDiagramLayoutEngine.SCALE, engine.getNodeTextPadding());
 
             List<LayoutRoute> layoutRoutes = new ArrayList<>();
             int currentY = RouteDiagramLayoutEngine.PADDING;

Reply via email to