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

Kusuma04-dev pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-kie-tools.git


The following commit(s) were added to refs/heads/main by this push:
     new 5fd92bf4b26 [incubator-kie-tools#4016] [BPMN Editor] Allow moving 
Sequence Flow labels by drag and drop (#4015)
5fd92bf4b26 is described below

commit 5fd92bf4b26c4596ef759787dd4fc7beda97ded9
Author: Mohammad Mustafa <[email protected]>
AuthorDate: Fri Oct 30 11:51:38 2026 +0300

    [incubator-kie-tools#4016] [BPMN Editor] Allow moving Sequence Flow labels 
by drag and drop (#4015)
    
    Co-authored-by: Mohammad Mustafa 
<[email protected]>
---
 .../bpmn-editor/src/clipboard/offsetPastedNodes.ts |   7 +
 packages/bpmn-editor/src/diagram/BpmnDiagram.tsx   | 121 +++++----
 packages/bpmn-editor/src/diagram/edges/Edges.css   |  16 ++
 packages/bpmn-editor/src/diagram/edges/Edges.tsx   | 225 +++++++++++++++-
 packages/bpmn-editor/src/mutations/addEdge.ts      |  15 ++
 .../bpmn-editor/src/mutations/addEdgeWaypoint.ts   |   5 +-
 .../src/mutations/deleteEdgeWaypoint.ts            |   5 +-
 .../src/mutations/repositionEdgeLabel.ts           | 300 +++++++++++++++++++++
 .../src/mutations/repositionEdgeWaypoint.ts        |  15 +-
 .../bpmn-editor/src/mutations/repositionNode.ts    |  35 ++-
 packages/bpmn-editor/src/mutations/resizeNode.ts   |  18 +-
 .../bpmn-editor/tests-e2e/__fixtures__/edges.ts    |  46 ++++
 .../tests-e2e/__fixtures__/jsonModel.ts            |  14 +
 .../flowElements/moveSequenceFlowLabel.spec.ts     |  95 +++++++
 14 files changed, 844 insertions(+), 73 deletions(-)

diff --git a/packages/bpmn-editor/src/clipboard/offsetPastedNodes.ts 
b/packages/bpmn-editor/src/clipboard/offsetPastedNodes.ts
index 4ca74b72fc4..4c7a3659e95 100644
--- a/packages/bpmn-editor/src/clipboard/offsetPastedNodes.ts
+++ b/packages/bpmn-editor/src/clipboard/offsetPastedNodes.ts
@@ -44,5 +44,12 @@ export function offsetPastedNodes(
       waypoint["@_x"] += offsetX;
       waypoint["@_y"] += offsetY;
     }
+
+    // Moved labels stay with their edge.
+    const labelBounds = edge["bpmndi:BPMNLabel"]?.["dc:Bounds"];
+    if (labelBounds) {
+      labelBounds["@_x"] += offsetX;
+      labelBounds["@_y"] += offsetY;
+    }
   }
 }
diff --git a/packages/bpmn-editor/src/diagram/BpmnDiagram.tsx 
b/packages/bpmn-editor/src/diagram/BpmnDiagram.tsx
index 9a85e8c9da0..a9b8a960bb5 100644
--- a/packages/bpmn-editor/src/diagram/BpmnDiagram.tsx
+++ b/packages/bpmn-editor/src/diagram/BpmnDiagram.tsx
@@ -63,6 +63,8 @@ import { moveNodesOutOfLane } from 
"../mutations/moveNodesOutOfLane";
 import { moveNodesOutOfSubProcess } from 
"../mutations/moveNodesOutOfSubProcess";
 import { makeBoundaryEvent } from "../mutations/makeBoundaryEvent";
 import { detachBoundaryEvent } from "../mutations/detachBoundaryEvent";
+import { getEdgeLabelBoundsAfterWaypointsChange } from 
"../mutations/repositionEdgeLabel";
+import { generateUuid } from 
"@kie-tools/xyflow-react-kie-diagram/dist/uuid/uuid";
 import { repositionEdgeWaypoint } from "../mutations/repositionEdgeWaypoint";
 import { repositionNode } from "../mutations/repositionNode";
 import { resizeNode } from "../mutations/resizeNode";
@@ -199,63 +201,68 @@ export function BpmnDiagram({
 
   const onNodeRepositioned = useCallback<
     OnNodeRepositioned<State, BpmnNodeType, BpmnDiagramNodeData, 
BpmnDiagramEdgeData>
-  >(({ state, node, controlWaypointsByEdge, newPosition, childNodeIds }) => {
-    console.log("BPMN EDITOR DIAGRAM: onNodeRepositioned");
-    const { delta } = repositionNode({
-      definitions: state.bpmn.model.definitions,
-      controlWaypointsByEdge,
-      __readonly_change: {
-        type: "absolute",
-        nodeType: node.type as BpmnNodeType,
-        selectedEdges: 
[...state.computed(state).getDiagramData().selectedEdgesById.keys()],
-        shapeIndex: node.data.shapeIndex,
-        sourceEdgeIndexes: state
-          .computed(state)
-          .getDiagramData()
-          .edges.flatMap((e) => (e.source === node.id && e.data?.bpmnEdge ? 
[e.data.bpmnEdgeIndex] : [])),
-        targetEdgeIndexes: state
-          .computed(state)
-          .getDiagramData()
-          .edges.flatMap((e) => (e.target === node.id && e.data?.bpmnEdge ? 
[e.data.bpmnEdgeIndex] : [])),
-        position: newPosition,
-      },
-    });
-
-    const allEdgeIds = state
-      .computed(state)
-      .getDiagramData()
-      .edges.map((e) => e.id); // Simulate all edges being selected
-
-    for (const nestedId of childNodeIds) {
-      const nestedNode = 
state.computed(state).getDiagramData().nodesById.get(nestedId);
-      if (!nestedNode) {
-        throw new Error("Can't reposition nested node with id " + nestedId);
-      }
-
-      repositionNode({
+  >(
+    ({ state, node, controlWaypointsByEdge, newPosition, childNodeIds }) => {
+      console.log("BPMN EDITOR DIAGRAM: onNodeRepositioned");
+      const { delta } = repositionNode({
+        __readonly_labelReferenceDefinitions: 
bpmnModelBeforeEditingRef.current.definitions,
         definitions: state.bpmn.model.definitions,
         controlWaypointsByEdge,
         __readonly_change: {
-          type: "offset",
-          nodeType: nestedNode.type as BpmnNodeType,
-          selectedEdges: allEdgeIds, // Makes sure all internal waypoints move 
too.
-          shapeIndex: nestedNode.data.shapeIndex,
+          type: "absolute",
+          nodeType: node.type as BpmnNodeType,
+          selectedEdges: 
[...state.computed(state).getDiagramData().selectedEdgesById.keys()],
+          shapeIndex: node.data.shapeIndex,
           sourceEdgeIndexes: state
             .computed(state)
             .getDiagramData()
-            .edges.flatMap((e) => (e.source === nestedId && e.data?.bpmnEdge ? 
[e.data.bpmnEdgeIndex] : [])),
+            .edges.flatMap((e) => (e.source === node.id && e.data?.bpmnEdge ? 
[e.data.bpmnEdgeIndex] : [])),
           targetEdgeIndexes: state
             .computed(state)
             .getDiagramData()
-            .edges.flatMap((e) => (e.target === nestedId && e.data?.bpmnEdge ? 
[e.data.bpmnEdgeIndex] : [])),
-          offset: {
-            deltaX: delta.x,
-            deltaY: delta.y,
-          },
+            .edges.flatMap((e) => (e.target === node.id && e.data?.bpmnEdge ? 
[e.data.bpmnEdgeIndex] : [])),
+          position: newPosition,
         },
       });
-    }
-  }, []);
+
+      const allEdgeIds = state
+        .computed(state)
+        .getDiagramData()
+        .edges.map((e) => e.id); // Simulate all edges being selected
+
+      for (const nestedId of childNodeIds) {
+        const nestedNode = 
state.computed(state).getDiagramData().nodesById.get(nestedId);
+        if (!nestedNode) {
+          throw new Error("Can't reposition nested node with id " + nestedId);
+        }
+
+        repositionNode({
+          __readonly_labelReferenceDefinitions: 
bpmnModelBeforeEditingRef.current.definitions,
+          definitions: state.bpmn.model.definitions,
+          controlWaypointsByEdge,
+          __readonly_change: {
+            type: "offset",
+            nodeType: nestedNode.type as BpmnNodeType,
+            selectedEdges: allEdgeIds, // Makes sure all internal waypoints 
move too.
+            shapeIndex: nestedNode.data.shapeIndex,
+            sourceEdgeIndexes: state
+              .computed(state)
+              .getDiagramData()
+              .edges.flatMap((e) => (e.source === nestedId && e.data?.bpmnEdge 
? [e.data.bpmnEdgeIndex] : [])),
+            targetEdgeIndexes: state
+              .computed(state)
+              .getDiagramData()
+              .edges.flatMap((e) => (e.target === nestedId && e.data?.bpmnEdge 
? [e.data.bpmnEdgeIndex] : [])),
+            offset: {
+              deltaX: delta.x,
+              deltaY: delta.y,
+            },
+          },
+        });
+      }
+    },
+    [bpmnModelBeforeEditingRef]
+  );
 
   const onNodeDeleted = useCallback<OnNodeDeleted<State, BpmnNodeType, 
BpmnDiagramNodeData, BpmnDiagramEdgeData>>(
     ({ state, node }) => {
@@ -362,6 +369,7 @@ export function BpmnDiagram({
     ({ state, node, newDimensions }) => {
       console.log("BPMN EDITOR DIAGRAM: onNodeResized");
       resizeNode({
+        __readonly_labelReferenceDefinitions: 
bpmnModelBeforeEditingRef.current.definitions,
         definitions: state.bpmn.model.definitions,
         __readonly_snapGrid: state.xyFlowReactKieDiagram.snapGrid,
         __readonly_change: {
@@ -383,7 +391,7 @@ export function BpmnDiagram({
         },
       });
     },
-    []
+    [bpmnModelBeforeEditingRef]
   );
 
   // edges
@@ -505,6 +513,22 @@ export function BpmnDiagram({
             newBpmnEdge["di:waypoint"]![newBpmnEdge["di:waypoint"]!.length - 
1],
           ];
         }
+
+        // Keep the reconnected edge's label.
+        const deletedLabel = deletedBpmnEdge?.["bpmndi:BPMNLabel"];
+        if (deletedLabel?.["dc:Bounds"]) {
+          newBpmnEdge["bpmndi:BPMNLabel"] = {
+            ...deletedLabel,
+            "@_id": generateUuid(),
+            "dc:Bounds": getEdgeLabelBoundsAfterWaypointsChange({
+              __readonly_labelBounds: deletedLabel["dc:Bounds"],
+              __readonly_previousWaypoints: deletedWaypoints,
+              __readonly_newWaypoints: newBpmnEdge["di:waypoint"],
+            }),
+          };
+        } else {
+          delete newBpmnEdge["bpmndi:BPMNLabel"];
+        }
       }
 
       return { id: newBpmnEdge["@_bpmnElement"]! };
@@ -541,6 +565,7 @@ export function BpmnDiagram({
       console.log("BPMN EDITOR DIAGRAM: onWaypointRepositioned");
       bpmnEditorStoreApi.setState((s) => {
         repositionEdgeWaypoint({
+          __readonly_labelReferenceDefinitions: 
bpmnModelBeforeEditingRef.current.definitions,
           definitions: s.bpmn.model.definitions,
           __readonly_edgeIndex: edgeIndex,
           __readonly_waypoint: waypoint,
@@ -548,7 +573,7 @@ export function BpmnDiagram({
         });
       });
     },
-    [bpmnEditorStoreApi]
+    [bpmnEditorStoreApi, bpmnModelBeforeEditingRef]
   );
 
   const onWaypointDeleted = useCallback<OnWaypointDeleted>(
diff --git a/packages/bpmn-editor/src/diagram/edges/Edges.css 
b/packages/bpmn-editor/src/diagram/edges/Edges.css
index ee4a3321180..c2b542490ca 100644
--- a/packages/bpmn-editor/src/diagram/edges/Edges.css
+++ b/packages/bpmn-editor/src/diagram/edges/Edges.css
@@ -51,3 +51,19 @@
   border-color: #006ba4;
   box-shadow: #006ba4 2px 2px 6px;
 }
+
+/* Centered on saved position. */
+.kie-bpmn-editor--floating-edge-label.pinned {
+  margin: 0;
+}
+
+/* Renderer disables pointer events. */
+.kie-bpmn-editor--floating-edge-label.movable {
+  pointer-events: all;
+  cursor: move;
+  touch-action: none;
+}
+
+.kie-bpmn-editor--floating-edge-label.dragging {
+  z-index: 1;
+}
diff --git a/packages/bpmn-editor/src/diagram/edges/Edges.tsx 
b/packages/bpmn-editor/src/diagram/edges/Edges.tsx
index aa9c6596624..91e2b6d5662 100644
--- a/packages/bpmn-editor/src/diagram/edges/Edges.tsx
+++ b/packages/bpmn-editor/src/diagram/edges/Edges.tsx
@@ -26,16 +26,17 @@ import { DEFAULT_INTRACTION_WIDTH } from 
"@kie-tools/xyflow-react-kie-diagram/di
 import { propsHaveSameValuesDeep } from 
"@kie-tools/xyflow-react-kie-diagram/dist/memoization/memoization";
 import { useIsHovered } from 
"@kie-tools/xyflow-react-kie-diagram/dist/reactExt/useIsHovered";
 import * as React from "react";
-import { useCallback, useMemo, useRef } from "react";
+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
 import * as RF from "reactflow";
 import { AssociationPath, SequenceFlowPath } from "./EdgeSvgs";
 import { BpmnDiagramEdgeData, MIN_NODE_SIZES } from "../BpmnDiagramDomain";
-import { useBpmnEditorStoreApi } from "../../store/StoreContext";
+import { useBpmnEditorStore, useBpmnEditorStoreApi } from 
"../../store/StoreContext";
 import {
   EditableNodeLabel,
   useEditableNodeLabel,
 } from "@kie-tools/xyflow-react-kie-diagram/dist/nodes/EditableNodeLabel";
 import { updateFlowElement } from "../../mutations/renameNode";
+import { getEdgeLabelCenter, repositionEdgeLabel } from 
"../../mutations/repositionEdgeLabel";
 import "./Edges.css";
 import { useBpmnEditorI18n } from "../../i18n";
 
@@ -84,12 +85,6 @@ export const SequenceFlowEdge = React.memo((props: 
RF.EdgeProps<BpmnDiagramEdgeD
     }
   }, [waypoints]);
 
-  const labelStyle = useMemo(() => {
-    return {
-      transform: 
`translate(${labelPosition["@_x"]}px,${labelPosition["@_y"]}px)`,
-    };
-  }, [labelPosition]);
-
   const bpmnEditorStoreApi = useBpmnEditorStoreApi();
 
   const id = props.data?.bpmnElement["@_id"] ?? props.id;
@@ -109,6 +104,191 @@ export const SequenceFlowEdge = React.memo((props: 
RF.EdgeProps<BpmnDiagramEdgeD
 
   const { isEditingLabel, setEditingLabel, triggerEditing: triggerEditingLabel 
} = useEditableNodeLabel(props.id);
 
+  // Label drag, saved on drop.
+
+  const isReadOnly = useBpmnEditorStore((s) => s.settings.isReadOnly);
+  const reactFlowStoreApi = RF.useStoreApi();
+  const labelRef = useRef<HTMLDivElement>(null);
+  const labelDragRef = useRef<EdgeLabelDrag | undefined>(undefined);
+  const [draggedLabelCenter, setDraggedLabelCenter] = useState<Point | 
undefined>(undefined);
+  const isDraggingLabel = draggedLabelCenter !== undefined;
+
+  const pinnedLabelCenter = useMemo(
+    () => 
getEdgeLabelCenter(props.data?.bpmnEdge?.["bpmndi:BPMNLabel"]?.["dc:Bounds"]),
+    [props.data?.bpmnEdge]
+  );
+
+  const labelCenter = draggedLabelCenter ?? pinnedLabelCenter;
+
+  const labelStyle = useMemo(() => {
+    if (labelCenter) {
+      return { transform: `translate(${labelCenter.x}px,${labelCenter.y}px) 
translate(-50%,-50%)` };
+    }
+
+    return {
+      transform: 
`translate(${labelPosition["@_x"]}px,${labelPosition["@_y"]}px)`,
+    };
+  }, [labelCenter, labelPosition]);
+
+  // screenToFlowPosition snaps to grid.
+  const getFlowPosition = useCallback(
+    (clientX: number, clientY: number): Point => {
+      const { transform, domNode } = reactFlowStoreApi.getState();
+      const domNodeRect = domNode?.getBoundingClientRect();
+      return {
+        x: (clientX - (domNodeRect?.left ?? 0) - transform[0]) / transform[2],
+        y: (clientY - (domNodeRect?.top ?? 0) - transform[1]) / transform[2],
+      };
+    },
+    [reactFlowStoreApi]
+  );
+
+  const onLabelPointerDown = useCallback(
+    (e: React.PointerEvent<HTMLDivElement>) => {
+      if (isReadOnly || isEditingLabel || e.button !== 0 || !labelRef.current) 
{
+        return;
+      }
+
+      // Multi-selection happens on click.
+      if (!reactFlowStoreApi.getState().multiSelectionActive) {
+        bpmnEditorStoreApi.setState((s) => {
+          const { _selectedNodes, _selectedEdges } = s.xyFlowReactKieDiagram;
+          if (_selectedNodes.length > 0 || _selectedEdges.length !== 1 || 
_selectedEdges[0] !== props.id) {
+            s.xyFlowReactKieDiagram._selectedNodes = [];
+            s.xyFlowReactKieDiagram._selectedEdges = [props.id];
+          }
+        });
+      }
+
+      const labelRect = labelRef.current.getBoundingClientRect();
+      labelDragRef.current = {
+        pointerId: e.pointerId,
+        startClientPosition: { x: e.clientX, y: e.clientY },
+        startPointerPosition: getFlowPosition(e.clientX, e.clientY),
+        startCenter: getFlowPosition(labelRect.left + labelRect.width / 2, 
labelRect.top + labelRect.height / 2),
+        width: labelRef.current.offsetWidth,
+        height: labelRef.current.offsetHeight,
+        hasMoved: false,
+      };
+
+      try {
+        e.currentTarget.setPointerCapture(e.pointerId);
+      } catch (error) {
+        // Inactive pointers can't be captured.
+      }
+    },
+    [bpmnEditorStoreApi, getFlowPosition, isEditingLabel, isReadOnly, 
props.id, reactFlowStoreApi]
+  );
+
+  const onLabelPointerMove = useCallback(
+    (e: React.PointerEvent<HTMLDivElement>) => {
+      const drag = labelDragRef.current;
+      if (!drag || drag.pointerId !== e.pointerId) {
+        return;
+      }
+
+      if (!drag.hasMoved) {
+        const distance = Math.hypot(e.clientX - drag.startClientPosition.x, 
e.clientY - drag.startClientPosition.y);
+        if (distance < EDGE_LABEL_DRAG_THRESHOLD_IN_PX) {
+          return; // Clicks never move labels.
+        }
+        drag.hasMoved = true;
+      }
+
+      setDraggedLabelCenter(getDraggedLabelCenter(drag, 
getFlowPosition(e.clientX, e.clientY)));
+    },
+    [getFlowPosition]
+  );
+
+  const edgeIndex = props.data?.bpmnEdgeIndex;
+
+  const onLabelPointerUp = useCallback(
+    (e: React.PointerEvent<HTMLDivElement>) => {
+      const drag = labelDragRef.current;
+      if (!drag || drag.pointerId !== e.pointerId) {
+        return;
+      }
+
+      labelDragRef.current = undefined;
+
+      const center = getDraggedLabelCenter(drag, getFlowPosition(e.clientX, 
e.clientY));
+      const isBackWherePinned =
+        !!pinnedLabelCenter &&
+        Math.abs(pinnedLabelCenter.x - center.x) < 0.01 &&
+        Math.abs(pinnedLabelCenter.y - center.y) < 0.01;
+
+      if (
+        !drag.hasMoved ||
+        isBackWherePinned ||
+        edgeIndex === undefined ||
+        !Number.isFinite(center.x) ||
+        !Number.isFinite(center.y)
+      ) {
+        setDraggedLabelCenter(undefined);
+        return;
+      }
+
+      // Avoids flicker until model updates.
+      setDraggedLabelCenter(center);
+      try {
+        bpmnEditorStoreApi.setState((s) => {
+          repositionEdgeLabel({
+            definitions: s.bpmn.model.definitions,
+            __readonly_edgeIndex: edgeIndex,
+            __readonly_bounds: {
+              "@_x": center.x - drag.width / 2,
+              "@_y": center.y - drag.height / 2,
+              "@_width": drag.width,
+              "@_height": drag.height,
+            },
+          });
+        });
+      } catch (error) {
+        setDraggedLabelCenter(undefined);
+        throw error;
+      }
+    },
+    [bpmnEditorStoreApi, edgeIndex, getFlowPosition, pinnedLabelCenter]
+  );
+
+  // Edge changed; clear dragged position.
+  useEffect(() => {
+    if (!labelDragRef.current) {
+      setDraggedLabelCenter(undefined);
+    }
+  }, [props.data?.bpmnEdge]);
+
+  const cancelLabelDrag = useCallback(() => {
+    labelDragRef.current = undefined;
+    setDraggedLabelCenter(undefined);
+  }, []);
+
+  // Also fired after pointerup.
+  const onLabelLostPointerCapture = useCallback(() => {
+    if (labelDragRef.current) {
+      cancelLabelDrag();
+    }
+  }, [cancelLabelDrag]);
+
+  // Escape cancels the drag.
+  useEffect(() => {
+    if (!isDraggingLabel) {
+      return;
+    }
+
+    const onKeyDown = (e: KeyboardEvent) => {
+      if (e.key === "Escape") {
+        e.stopPropagation();
+        cancelLabelDrag();
+      }
+    };
+
+    window.addEventListener("keydown", onKeyDown, true);
+    return () => {
+      window.removeEventListener("keydown", onKeyDown, true);
+    };
+  }, [cancelLabelDrag, isDraggingLabel]);
+
   const onDoubleClickEdge = useCallback(
     (e: React.MouseEvent) => {
       if (e.metaKey) {
@@ -143,8 +323,14 @@ export const SequenceFlowEdge = React.memo((props: 
RF.EdgeProps<BpmnDiagramEdgeD
         (!!props.data.bpmnElement["@_name"] || isEditingLabel) && (
           <RF.EdgeLabelRenderer>
             <div
+              ref={labelRef}
               style={labelStyle}
-              className={`kie-bpmn-editor--floating-edge-label 
edge-label-renderer__custom-edge nodrag nopan ${props.selected ? "selected" : 
""}`}
+              className={`kie-bpmn-editor--floating-edge-label 
edge-label-renderer__custom-edge nodrag nopan ${props.selected ? "selected" : 
""} ${labelCenter ? "pinned" : ""} ${isReadOnly ? "" : "movable"} 
${isDraggingLabel ? "dragging" : ""}`}
+              onPointerDown={isReadOnly ? undefined : onLabelPointerDown}
+              onPointerMove={isReadOnly ? undefined : onLabelPointerMove}
+              onPointerUp={isReadOnly ? undefined : onLabelPointerUp}
+              onPointerCancel={isReadOnly ? undefined : cancelLabelDrag}
+              onLostPointerCapture={isReadOnly ? undefined : 
onLabelLostPointerCapture}
             >
               <EditableNodeLabel
                 id={props.id}
@@ -238,3 +424,24 @@ export const AssociationEdge = React.memo((props: 
RF.EdgeProps<BpmnDiagramEdgeDa
 }, propsHaveSameValuesDeep);
 
 const NO_VALIDATION = () => true;
+
+const EDGE_LABEL_DRAG_THRESHOLD_IN_PX = 3;
+
+type Point = { x: number; y: number };
+
+type EdgeLabelDrag = {
+  pointerId: number;
+  startClientPosition: Point;
+  startPointerPosition: Point;
+  startCenter: Point;
+  width: number;
+  height: number;
+  hasMoved: boolean;
+};
+
+function getDraggedLabelCenter(drag: EdgeLabelDrag, pointerPosition: Point): 
Point {
+  return {
+    x: drag.startCenter.x + pointerPosition.x - drag.startPointerPosition.x,
+    y: drag.startCenter.y + pointerPosition.y - drag.startPointerPosition.y,
+  };
+}
diff --git a/packages/bpmn-editor/src/mutations/addEdge.ts 
b/packages/bpmn-editor/src/mutations/addEdge.ts
index 383af245097..015667e3c96 100644
--- a/packages/bpmn-editor/src/mutations/addEdge.ts
+++ b/packages/bpmn-editor/src/mutations/addEdge.ts
@@ -34,6 +34,7 @@ import { PositionalNodeHandleId } from 
"@kie-tools/xyflow-react-kie-diagram/dist
 import { AutoPositionedEdgeMarker } from 
"@kie-tools/xyflow-react-kie-diagram/dist/edges/AutoPositionedEdgeMarker";
 import { _checkIsValidConnection } from 
"@kie-tools/xyflow-react-kie-diagram/dist/graph/isValidConnection";
 import { addOrGetProcessAndDiagramElements } from 
"./addOrGetProcessAndDiagramElements";
+import { getEdgeLabelBoundsAfterWaypointsChange } from "./repositionEdgeLabel";
 import {
   getDiscreteAutoPositioningEdgeIdMarker,
   getPointForHandle,
@@ -284,6 +285,20 @@ export function addEdge({
     "di:waypoint": newWaypoints,
   };
 
+  // Keep label with kept waypoints.
+  const removedLabel = removedBpmnEdge?.["bpmndi:BPMNLabel"];
+  if (__readonly_keepWaypoints && removedLabel?.["dc:Bounds"]) {
+    newBpmnEdge["bpmndi:BPMNLabel"] = {
+      ...removedLabel,
+      "@_id": generateUuid(),
+      "dc:Bounds": getEdgeLabelBoundsAfterWaypointsChange({
+        __readonly_labelBounds: removedLabel["dc:Bounds"],
+        __readonly_previousWaypoints: removedBpmnEdge?.["di:waypoint"],
+        __readonly_newWaypoints: newWaypoints,
+      }),
+    };
+  }
+
   // Replace with the new one.
   diagramElements.push(newBpmnEdge);
 
diff --git a/packages/bpmn-editor/src/mutations/addEdgeWaypoint.ts 
b/packages/bpmn-editor/src/mutations/addEdgeWaypoint.ts
index 2df53dcd430..580f3701da0 100644
--- a/packages/bpmn-editor/src/mutations/addEdgeWaypoint.ts
+++ b/packages/bpmn-editor/src/mutations/addEdgeWaypoint.ts
@@ -21,6 +21,7 @@ import { BPMN20__tDefinitions } from 
"@kie-tools/bpmn-marshaller/dist/schemas/bp
 import { DC__Point } from 
"@kie-tools/xyflow-react-kie-diagram/dist/maths/model";
 import { Normalized } from "../normalization/normalize";
 import { addOrGetProcessAndDiagramElements } from 
"./addOrGetProcessAndDiagramElements";
+import { updateEdgeWaypointsKeepingLabelAttached } from 
"./repositionEdgeLabel";
 
 export function addEdgeWaypoint({
   definitions,
@@ -46,5 +47,7 @@ export function addEdgeWaypoint({
     );
   }
 
-  diagramElement["di:waypoint"]!.splice(__readonly_beforeIndex, 0, 
__readonly_waypoint);
+  updateEdgeWaypointsKeepingLabelAttached(diagramElement, () => {
+    diagramElement["di:waypoint"]!.splice(__readonly_beforeIndex, 0, 
__readonly_waypoint);
+  });
 }
diff --git a/packages/bpmn-editor/src/mutations/deleteEdgeWaypoint.ts 
b/packages/bpmn-editor/src/mutations/deleteEdgeWaypoint.ts
index 4c8f07e4214..3fbc0f4c0a4 100644
--- a/packages/bpmn-editor/src/mutations/deleteEdgeWaypoint.ts
+++ b/packages/bpmn-editor/src/mutations/deleteEdgeWaypoint.ts
@@ -20,6 +20,7 @@
 import { BPMN20__tDefinitions } from 
"@kie-tools/bpmn-marshaller/dist/schemas/bpmn-2_0/ts-gen/types";
 import { Normalized } from "../normalization/normalize";
 import { addOrGetProcessAndDiagramElements } from 
"./addOrGetProcessAndDiagramElements";
+import { updateEdgeWaypointsKeepingLabelAttached } from 
"./repositionEdgeLabel";
 
 export function deleteEdgeWaypoint({
   definitions,
@@ -43,5 +44,7 @@ export function deleteEdgeWaypoint({
     );
   }
 
-  diagramElement["di:waypoint"]!.splice(__readonly_waypointIndex, 1);
+  updateEdgeWaypointsKeepingLabelAttached(diagramElement, () => {
+    diagramElement["di:waypoint"]!.splice(__readonly_waypointIndex, 1);
+  });
 }
diff --git a/packages/bpmn-editor/src/mutations/repositionEdgeLabel.ts 
b/packages/bpmn-editor/src/mutations/repositionEdgeLabel.ts
new file mode 100644
index 00000000000..5157477e3f1
--- /dev/null
+++ b/packages/bpmn-editor/src/mutations/repositionEdgeLabel.ts
@@ -0,0 +1,300 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { BPMN20__tDefinitions } from 
"@kie-tools/bpmn-marshaller/dist/schemas/bpmn-2_0/ts-gen/types";
+import { DC__Bounds, DC__Point } from 
"@kie-tools/xyflow-react-kie-diagram/dist/maths/model";
+import { generateUuid } from 
"@kie-tools/xyflow-react-kie-diagram/dist/uuid/uuid";
+import { Normalized } from "../normalization/normalize";
+import { addOrGetProcessAndDiagramElements } from 
"./addOrGetProcessAndDiagramElements";
+
+/**
+ * Pins the label of a BPMNEdge to the given bounds. Labels without bounds are 
positioned automatically.
+ */
+export function repositionEdgeLabel({
+  definitions,
+  __readonly_edgeIndex,
+  __readonly_bounds,
+}: {
+  definitions: Normalized<BPMN20__tDefinitions>;
+  __readonly_edgeIndex: number;
+  __readonly_bounds: DC__Bounds;
+}) {
+  const { diagramElements } = addOrGetProcessAndDiagramElements({ definitions 
});
+
+  const diagramElement = diagramElements[__readonly_edgeIndex];
+  if (diagramElement?.__$$element !== "bpmndi:BPMNEdge") {
+    throw new Error("BPMN MUTATION: Can't reposition the label of an element 
that is not a BPMNEdge.");
+  }
+
+  if (!isFiniteBounds(__readonly_bounds)) {
+    throw new Error("BPMN MUTATION: Can't reposition the label of a BPMNEdge 
to invalid bounds.");
+  }
+
+  diagramElement["bpmndi:BPMNLabel"] ??= { "@_id": generateUuid() };
+  diagramElement["bpmndi:BPMNLabel"]["dc:Bounds"] = {
+    "@_x": __readonly_bounds["@_x"],
+    "@_y": __readonly_bounds["@_y"],
+    "@_width": __readonly_bounds["@_width"],
+    "@_height": __readonly_bounds["@_height"],
+  };
+}
+
+/**
+ * Runs `updateWaypoints` and then moves the edge's label (if it has explicit 
bounds) so that it stays at the same
+ * place relative to the edge path, like the label was attached to it.
+ *
+ * `referenceEdge` is the edge as it was before the ongoing gesture. 
Calculating from it doesn't accumulate errors during
+ * a drag. It's ignored when it doesn't match the current label anymore, as 
the label or the edge changed since then.
+ */
+export function updateEdgeWaypointsKeepingLabelAttached(
+  edge: EdgeWithLabel,
+  updateWaypoints: () => void,
+  referenceEdge?: EdgeWithLabel
+) {
+  const labelBounds = edge["bpmndi:BPMNLabel"]?.["dc:Bounds"];
+  if (!labelBounds) {
+    updateWaypoints();
+    return;
+  }
+
+  const source = referenceEdge && isReferenceOf(referenceEdge, edge) ? 
referenceEdge : edge;
+  const previousBounds = { ...source["bpmndi:BPMNLabel"]!["dc:Bounds"]! };
+  const previousWaypoints = copyWaypoints(source["di:waypoint"]);
+
+  updateWaypoints();
+
+  const newBounds = getEdgeLabelBoundsAfterWaypointsChange({
+    __readonly_labelBounds: previousBounds,
+    __readonly_previousWaypoints: previousWaypoints,
+    __readonly_newWaypoints: edge["di:waypoint"],
+  });
+
+  if (newBounds["@_x"] !== labelBounds["@_x"] || newBounds["@_y"] !== 
labelBounds["@_y"]) {
+    labelBounds["@_x"] = newBounds["@_x"];
+    labelBounds["@_y"] = newBounds["@_y"];
+  }
+}
+
+function isReferenceOf(referenceEdge: EdgeWithLabel, edge: EdgeWithLabel) {
+  const referenceBounds = referenceEdge["bpmndi:BPMNLabel"]?.["dc:Bounds"];
+  const labelBounds = edge["bpmndi:BPMNLabel"]?.["dc:Bounds"];
+  if (
+    !referenceBounds ||
+    !labelBounds ||
+    referenceEdge["bpmndi:BPMNLabel"]?.["@_id"] !== 
edge["bpmndi:BPMNLabel"]?.["@_id"] ||
+    referenceBounds["@_width"] !== labelBounds["@_width"] ||
+    referenceBounds["@_height"] !== labelBounds["@_height"]
+  ) {
+    return false;
+  }
+
+  // Reference must reproduce the label.
+  const expectedBounds = getEdgeLabelBoundsAfterWaypointsChange({
+    __readonly_labelBounds: referenceBounds,
+    __readonly_previousWaypoints: referenceEdge["di:waypoint"],
+    __readonly_newWaypoints: edge["di:waypoint"],
+  });
+
+  return (
+    Math.abs(expectedBounds["@_x"] - labelBounds["@_x"]) < REFERENCE_TOLERANCE 
&&
+    Math.abs(expectedBounds["@_y"] - labelBounds["@_y"]) < REFERENCE_TOLERANCE
+  );
+}
+
+/**
+ * Calculates where a label should go after the waypoints of its edge changed.
+ *
+ * The label center is projected on the previous path. The same spot is then 
found on the new path (same segment and
+ * relative position when the number of waypoints didn't change, same relative 
length otherwise), and the offset between
+ * the label center and the path is rotated by how much that segment rotated. 
Invalid inputs keep the label where it is.
+ */
+export function getEdgeLabelBoundsAfterWaypointsChange({
+  __readonly_labelBounds,
+  __readonly_previousWaypoints,
+  __readonly_newWaypoints,
+}: {
+  __readonly_labelBounds: DC__Bounds;
+  __readonly_previousWaypoints: DC__Point[] | undefined;
+  __readonly_newWaypoints: DC__Point[] | undefined;
+}): DC__Bounds {
+  const bounds = { ...__readonly_labelBounds };
+
+  const previousPath = toPath(__readonly_previousWaypoints);
+  const newPath = toPath(__readonly_newWaypoints);
+  const center = getEdgeLabelCenter(bounds);
+  if (!previousPath || !newPath || !center) {
+    return bounds;
+  }
+
+  const previousAnchor = getClosestPointOnPath(previousPath, center);
+  const newAnchor =
+    previousPath.length === newPath.length
+      ? getPointOnSegment(newPath, previousAnchor.segmentIndex, 
previousAnchor.segmentFraction)
+      : getPointAtLength(newPath, getPathLength(newPath) * 
previousAnchor.lengthFraction);
+
+  const previousAngle = getSegmentAngle(previousPath, 
previousAnchor.segmentIndex);
+  const newAngle = getSegmentAngle(newPath, newAnchor.segmentIndex);
+  const rotation = previousAngle === undefined || newAngle === undefined ? 0 : 
newAngle - previousAngle;
+  const offset = rotate({ x: center.x - previousAnchor.x, y: center.y - 
previousAnchor.y }, rotation);
+
+  const newX = newAnchor.x + offset.x - (center.x - bounds["@_x"]);
+  const newY = newAnchor.y + offset.y - (center.y - bounds["@_y"]);
+  if (!Number.isFinite(newX) || !Number.isFinite(newY)) {
+    return bounds;
+  }
+
+  bounds["@_x"] = newX;
+  bounds["@_y"] = newY;
+  return bounds;
+}
+
+/**
+ * Center of the label bounds, or `undefined` when the label has no usable 
position. Missing sizes count as zero.
+ */
+export function getEdgeLabelCenter(bounds: Partial<DC__Bounds> | undefined): 
Point | undefined {
+  if (!bounds || !Number.isFinite(bounds["@_x"]) || 
!Number.isFinite(bounds["@_y"])) {
+    return undefined;
+  }
+
+  return {
+    x: bounds["@_x"]! + (Number.isFinite(bounds["@_width"]) ? 
bounds["@_width"]! : 0) / 2,
+    y: bounds["@_y"]! + (Number.isFinite(bounds["@_height"]) ? 
bounds["@_height"]! : 0) / 2,
+  };
+}
+
+type Point = { x: number; y: number };
+type PointOnPath = Point & { segmentIndex: number; segmentFraction: number; 
lengthFraction: number };
+type EdgeWithLabel = {
+  "di:waypoint"?: DC__Point[];
+  "bpmndi:BPMNLabel"?: { "@_id"?: string; "dc:Bounds"?: DC__Bounds };
+};
+
+const REFERENCE_TOLERANCE = 0.01;
+
+function isFiniteBounds(bounds: DC__Bounds) {
+  return (
+    Number.isFinite(bounds["@_x"]) &&
+    Number.isFinite(bounds["@_y"]) &&
+    Number.isFinite(bounds["@_width"]) &&
+    Number.isFinite(bounds["@_height"])
+  );
+}
+
+function copyWaypoints(waypoints: DC__Point[] | undefined): DC__Point[] | 
undefined {
+  return waypoints?.map((w) => ({ "@_x": w["@_x"], "@_y": w["@_y"] }));
+}
+
+function toPath(waypoints: DC__Point[] | undefined): Point[] | undefined {
+  if (!waypoints || waypoints.length < 2) {
+    return undefined;
+  }
+
+  const path = waypoints.map((w) => ({ x: w?.["@_x"], y: w?.["@_y"] }));
+  return path.every((p) => Number.isFinite(p.x) && Number.isFinite(p.y)) ? 
path : undefined;
+}
+
+function getSegmentLength(path: Point[], segmentIndex: number) {
+  return Math.hypot(path[segmentIndex + 1].x - path[segmentIndex].x, 
path[segmentIndex + 1].y - path[segmentIndex].y);
+}
+
+function getPathLength(path: Point[]) {
+  let length = 0;
+  for (let i = 0; i < path.length - 1; i++) {
+    length += getSegmentLength(path, i);
+  }
+  return length;
+}
+
+function getSegmentAngle(path: Point[], segmentIndex: number) {
+  const a = path[segmentIndex];
+  const b = path[segmentIndex + 1];
+  return a.x === b.x && a.y === b.y ? undefined : Math.atan2(b.y - a.y, b.x - 
a.x);
+}
+
+function getClosestPointOnPath(path: Point[], point: Point): PointOnPath {
+  const totalLength = getPathLength(path);
+
+  let closest: PointOnPath = { ...path[0], segmentIndex: 0, segmentFraction: 
0, lengthFraction: 0 };
+  let closestDistance = Infinity;
+  let lengthBeforeSegment = 0;
+
+  for (let i = 0; i < path.length - 1; i++) {
+    const a = path[i];
+    const b = path[i + 1];
+    const dx = b.x - a.x;
+    const dy = b.y - a.y;
+    const squaredLength = dx * dx + dy * dy;
+    const segmentFraction =
+      squaredLength === 0 ? 0 : clamp(((point.x - a.x) * dx + (point.y - a.y) 
* dy) / squaredLength, 0, 1);
+
+    const x = a.x + segmentFraction * dx;
+    const y = a.y + segmentFraction * dy;
+    const distance = (point.x - x) * (point.x - x) + (point.y - y) * (point.y 
- y);
+
+    if (distance < closestDistance) {
+      closestDistance = distance;
+      closest = {
+        x,
+        y,
+        segmentIndex: i,
+        segmentFraction,
+        lengthFraction:
+          totalLength === 0 ? 0 : (lengthBeforeSegment + segmentFraction * 
Math.sqrt(squaredLength)) / totalLength,
+      };
+    }
+
+    lengthBeforeSegment += Math.sqrt(squaredLength);
+  }
+
+  return closest;
+}
+
+function getPointOnSegment(path: Point[], segmentIndex: number, 
segmentFraction: number): PointOnPath {
+  const a = path[segmentIndex];
+  const b = path[segmentIndex + 1];
+  return {
+    x: a.x + segmentFraction * (b.x - a.x),
+    y: a.y + segmentFraction * (b.y - a.y),
+    segmentIndex,
+    segmentFraction,
+    lengthFraction: 0, // Not needed.
+  };
+}
+
+function getPointAtLength(path: Point[], length: number): PointOnPath {
+  let remainingLength = Math.max(0, length);
+  for (let i = 0; i < path.length - 1; i++) {
+    const segmentLength = getSegmentLength(path, i);
+    if (remainingLength <= segmentLength || i === path.length - 2) {
+      return getPointOnSegment(path, i, segmentLength === 0 ? 0 : 
clamp(remainingLength / segmentLength, 0, 1));
+    }
+    remainingLength -= segmentLength;
+  }
+  return getPointOnSegment(path, 0, 0);
+}
+
+function rotate(vector: Point, angle: number): Point {
+  const cos = Math.cos(angle);
+  const sin = Math.sin(angle);
+  return { x: vector.x * cos - vector.y * sin, y: vector.x * sin + vector.y * 
cos };
+}
+
+function clamp(value: number, min: number, max: number) {
+  return Math.min(max, Math.max(min, value));
+}
diff --git a/packages/bpmn-editor/src/mutations/repositionEdgeWaypoint.ts 
b/packages/bpmn-editor/src/mutations/repositionEdgeWaypoint.ts
index cf4e143af06..fcaf570d393 100644
--- a/packages/bpmn-editor/src/mutations/repositionEdgeWaypoint.ts
+++ b/packages/bpmn-editor/src/mutations/repositionEdgeWaypoint.ts
@@ -20,20 +20,25 @@
 import { DC__Point } from 
"@kie-tools/xyflow-react-kie-diagram/dist/maths/model";
 import { Normalized } from "../normalization/normalize";
 import { addOrGetProcessAndDiagramElements } from 
"./addOrGetProcessAndDiagramElements";
+import { updateEdgeWaypointsKeepingLabelAttached } from 
"./repositionEdgeLabel";
 import { BPMN20__tDefinitions } from 
"@kie-tools/bpmn-marshaller/dist/schemas/bpmn-2_0/ts-gen/types";
 
 export function repositionEdgeWaypoint({
   definitions,
+  __readonly_labelReferenceDefinitions,
   __readonly_edgeIndex,
   __readonly_waypointIndex,
   __readonly_waypoint,
 }: {
   definitions: Normalized<BPMN20__tDefinitions>;
+  __readonly_labelReferenceDefinitions?: BPMN20__tDefinitions;
   __readonly_edgeIndex: number;
   __readonly_waypointIndex: number;
   __readonly_waypoint: DC__Point;
 }) {
   const { diagramElements } = addOrGetProcessAndDiagramElements({ definitions 
});
+  const referenceElements =
+    
__readonly_labelReferenceDefinitions?.["bpmndi:BPMNDiagram"]?.[0]?.["bpmndi:BPMNPlane"]["di:DiagramElement"];
 
   const diagramElement = diagramElements[__readonly_edgeIndex];
   if (diagramElement.__$$element !== "bpmndi:BPMNEdge") {
@@ -46,5 +51,13 @@ export function repositionEdgeWaypoint({
     );
   }
 
-  diagramElement["di:waypoint"]![__readonly_waypointIndex] = 
__readonly_waypoint;
+  updateEdgeWaypointsKeepingLabelAttached(
+    diagramElement,
+    () => {
+      diagramElement["di:waypoint"]![__readonly_waypointIndex] = 
__readonly_waypoint;
+    },
+    referenceElements?.find((e) => e.__$$element === "bpmndi:BPMNEdge" && 
e["@_id"] === diagramElement["@_id"]) as
+      | typeof diagramElement
+      | undefined
+  );
 }
diff --git a/packages/bpmn-editor/src/mutations/repositionNode.ts 
b/packages/bpmn-editor/src/mutations/repositionNode.ts
index 8fc19e52407..d009c5c8a15 100644
--- a/packages/bpmn-editor/src/mutations/repositionNode.ts
+++ b/packages/bpmn-editor/src/mutations/repositionNode.ts
@@ -23,13 +23,16 @@ import { DC__Shape } from 
"@kie-tools/xyflow-react-kie-diagram/dist/maths/model"
 import { BpmnNodeType } from "../diagram/BpmnDiagramDomain";
 import { Normalized } from "../normalization/normalize";
 import { addOrGetProcessAndDiagramElements } from 
"./addOrGetProcessAndDiagramElements";
+import { updateEdgeWaypointsKeepingLabelAttached } from 
"./repositionEdgeLabel";
 
 export function repositionNode({
   definitions,
+  __readonly_labelReferenceDefinitions,
   controlWaypointsByEdge,
   __readonly_change,
 }: {
   definitions: Normalized<BPMN20__tDefinitions>;
+  __readonly_labelReferenceDefinitions?: BPMN20__tDefinitions;
   /**
    * This will keep track of all waypoints that were updated, in the case 
where multiple nodes move together.
    * This will make sure we only move edges once, even though they might be 
source/target edges for multiple nodes.
@@ -53,6 +56,8 @@ export function repositionNode({
   );
 }) {
   const { diagramElements } = addOrGetProcessAndDiagramElements({ definitions 
});
+  const referenceElements =
+    
__readonly_labelReferenceDefinitions?.["bpmndi:BPMNDiagram"]?.[0]?.["bpmndi:BPMNPlane"]["di:DiagramElement"];
 
   const shape = diagramElements?.[__readonly_change.shapeIndex] as 
Normalized<DC__Shape> | undefined;
   const shapeBounds = shape?.["dc:Bounds"];
@@ -95,18 +100,26 @@ export function repositionNode({
       });
 
       controlWaypointsByEdge.set(edgeIndex, 
controlWaypointsByEdge.get(edgeIndex) ?? new Set());
-      for (const wi of waypointIndexes) {
-        const waypointsControl = controlWaypointsByEdge.get(edgeIndex)!;
-        if (waypointsControl.has(wi)) {
-          continue;
-        } else {
-          waypointsControl.add(wi);
-        }
+      updateEdgeWaypointsKeepingLabelAttached(
+        edge,
+        () => {
+          for (const wi of waypointIndexes) {
+            const waypointsControl = controlWaypointsByEdge.get(edgeIndex)!;
+            if (waypointsControl.has(wi)) {
+              continue;
+            } else {
+              waypointsControl.add(wi);
+            }
 
-        const w = edge["di:waypoint"][wi];
-        w["@_x"] += deltaX;
-        w["@_y"] += deltaY;
-      }
+            const w = edge["di:waypoint"][wi];
+            w["@_x"] += deltaX;
+            w["@_y"] += deltaY;
+          }
+        },
+        referenceElements?.find((e) => e.__$$element === "bpmndi:BPMNEdge" && 
e["@_id"] === edge["@_id"]) as
+          | typeof edge
+          | undefined
+      );
     }
   };
 
diff --git a/packages/bpmn-editor/src/mutations/resizeNode.ts 
b/packages/bpmn-editor/src/mutations/resizeNode.ts
index 121d922a962..b2f01413283 100644
--- a/packages/bpmn-editor/src/mutations/resizeNode.ts
+++ b/packages/bpmn-editor/src/mutations/resizeNode.ts
@@ -29,13 +29,16 @@ import { Normalized } from "../normalization/normalize";
 import { addOrGetProcessAndDiagramElements } from 
"./addOrGetProcessAndDiagramElements";
 import { visitFlowElementsAndArtifacts } from "./_elementVisitor";
 import { repositionNode } from "./repositionNode";
+import { updateEdgeWaypointsKeepingLabelAttached } from 
"./repositionEdgeLabel";
 
 export function resizeNode({
   definitions,
+  __readonly_labelReferenceDefinitions,
   __readonly_snapGrid,
   __readonly_change,
 }: {
   definitions: Normalized<BPMN20__tDefinitions>;
+  __readonly_labelReferenceDefinitions?: BPMN20__tDefinitions;
   __readonly_snapGrid: SnapGrid;
   __readonly_change: {
     bpmnElement: BpmnNodeElement;
@@ -49,6 +52,8 @@ export function resizeNode({
   const edgeIndexesAlreadyUpdated = new Set<number>();
 
   const { process, diagramElements } = addOrGetProcessAndDiagramElements({ 
definitions });
+  const referenceElements =
+    
__readonly_labelReferenceDefinitions?.["bpmndi:BPMNDiagram"]?.[0]?.["bpmndi:BPMNPlane"]["di:DiagramElement"];
 
   const shape = diagramElements?.[__readonly_change.shapeIndex] as 
Normalized<DC__Shape> | undefined;
   const shapeBounds = shape?.["dc:Bounds"];
@@ -103,6 +108,7 @@ export function resizeNode({
         }
 
         repositionNode({
+          __readonly_labelReferenceDefinitions,
           definitions,
           controlWaypointsByEdge: new Map(),
           __readonly_change: {
@@ -151,8 +157,16 @@ export function resizeNode({
       });
 
       const offset = offsetByPosition(getHandlePosition({ shapeBounds, 
waypoint }).handlePosition);
-      waypoint["@_x"] += offset.x;
-      waypoint["@_y"] += offset.y;
+      updateEdgeWaypointsKeepingLabelAttached(
+        edge,
+        () => {
+          waypoint["@_x"] += offset.x;
+          waypoint["@_y"] += offset.y;
+        },
+        referenceElements?.find((e) => e.__$$element === "bpmndi:BPMNEdge" && 
e["@_id"] === edge["@_id"]) as
+          | typeof edge
+          | undefined
+      );
     }
   };
 
diff --git a/packages/bpmn-editor/tests-e2e/__fixtures__/edges.ts 
b/packages/bpmn-editor/tests-e2e/__fixtures__/edges.ts
index 17b55d18f4a..961d8a92498 100644
--- a/packages/bpmn-editor/tests-e2e/__fixtures__/edges.ts
+++ b/packages/bpmn-editor/tests-e2e/__fixtures__/edges.ts
@@ -18,6 +18,7 @@
  */
 
 import { expect, Locator, Page } from "@playwright/test";
+import { BPMNDI__BPMNEdge } from 
"@kie-tools/bpmn-marshaller/dist/schemas/bpmn-2_0/ts-gen/types";
 import { Diagram } from "./diagram";
 import { Nodes } from "./nodes";
 
@@ -55,4 +56,49 @@ export class Edges {
     await edge.click();
     await this.diagram.get().press("Delete");
   }
+
+  public getLabel(args: { name: string }): Locator {
+    return this.diagram.get().getByText(args.name, { exact: true });
+  }
+
+  public async getLabelBounds(args: { name: string }) {
+    const box = await this.getLabel(args).boundingBox();
+    expect(box).not.toBeNull();
+    return box!;
+  }
+
+  public async getLabelCenterPosition(args: { name: string }): Promise<{ x: 
number; y: number }> {
+    const box = await this.getLabelBounds(args);
+    return {
+      x: box.x + box.width / 2,
+      y: box.y + box.height / 2,
+    };
+  }
+
+  public async moveLabel(args: { name: string; offset: { x: number; y: number 
} }) {
+    const { x, y } = await this.getLabelCenterPosition({ name: args.name });
+    await this.page.mouse.move(x, y);
+    await this.page.mouse.down();
+    await this.page.mouse.move(x + args.offset.x, y + args.offset.y, { steps: 
10 });
+    await this.page.mouse.up();
+  }
+
+  public getLabelDistanceToEdge(bpmnEdge: BPMNDI__BPMNEdge) {
+    const bounds = bpmnEdge["bpmndi:BPMNLabel"]!["dc:Bounds"]!;
+    const x = bounds["@_x"] + bounds["@_width"] / 2;
+    const y = bounds["@_y"] + bounds["@_height"] / 2;
+    const waypoints = bpmnEdge["di:waypoint"];
+
+    let distance = Infinity;
+    for (let i = 0; i < waypoints.length - 1; i++) {
+      const a = waypoints[i];
+      const dx = waypoints[i + 1]["@_x"] - a["@_x"];
+      const dy = waypoints[i + 1]["@_y"] - a["@_y"];
+      const squaredLength = dx * dx + dy * dy;
+      const t =
+        squaredLength === 0 ? 0 : Math.min(1, Math.max(0, ((x - a["@_x"]) * dx 
+ (y - a["@_y"]) * dy) / squaredLength));
+      distance = Math.min(distance, Math.hypot(x - a["@_x"] - t * dx, y - 
a["@_y"] - t * dy));
+    }
+    return distance;
+  }
 }
diff --git a/packages/bpmn-editor/tests-e2e/__fixtures__/jsonModel.ts 
b/packages/bpmn-editor/tests-e2e/__fixtures__/jsonModel.ts
index bd22c748c92..9a468e10bb3 100644
--- a/packages/bpmn-editor/tests-e2e/__fixtures__/jsonModel.ts
+++ b/packages/bpmn-editor/tests-e2e/__fixtures__/jsonModel.ts
@@ -248,4 +248,18 @@ export class JsonModel {
     }
     return undefined;
   }
+
+  public async getEdge(args: { diagramIndex?: number; bpmnElementId: string }) 
{
+    const edge = (await this.getPlane(args.diagramIndex ?? 
0))?.["di:DiagramElement"]?.find(
+      (e) => e.__$$element === "bpmndi:BPMNEdge" && e["@_bpmnElement"] === 
args.bpmnElementId
+    );
+    if (edge?.__$$element === "bpmndi:BPMNEdge") {
+      return edge;
+    }
+    return undefined;
+  }
+
+  public async getEdgeLabelBounds(args: { diagramIndex?: number; 
bpmnElementId: string }) {
+    return (await this.getEdge(args))?.["bpmndi:BPMNLabel"]?.["dc:Bounds"];
+  }
 }
diff --git 
a/packages/bpmn-editor/tests-e2e/flowElements/moveSequenceFlowLabel.spec.ts 
b/packages/bpmn-editor/tests-e2e/flowElements/moveSequenceFlowLabel.spec.ts
new file mode 100644
index 00000000000..9843136e152
--- /dev/null
+++ b/packages/bpmn-editor/tests-e2e/flowElements/moveSequenceFlowLabel.spec.ts
@@ -0,0 +1,95 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { test, expect } from "../__fixtures__/base";
+import { NodeType } from "../__fixtures__/nodes";
+
+test.beforeEach(async ({ editor }) => {
+  await editor.open();
+  await editor.setInitialProcessId();
+});
+
+test.describe("Move Sequence Flow label", () => {
+  test.beforeEach(async ({ palette, nodes, edges, diagram, 
sequenceFlowPropertiesPanel }) => {
+    await palette.dragNewNode({ type: NodeType.TASK, targetPosition: { x: 100, 
y: 100 }, thenRenameTo: "Task A" });
+    await diagram.resetFocus();
+    await palette.dragNewNode({ type: NodeType.TASK, targetPosition: { x: 400, 
y: 400 }, thenRenameTo: "Task B" });
+    await diagram.resetFocus();
+
+    await nodes.createSequenceFlow({ from: "Task A", to: "Task B" });
+
+    const edge = await edges.get({ from: "Task A", to: "Task B" });
+    await edge.click({ force: true });
+    await sequenceFlowPropertiesPanel.open();
+    await sequenceFlowPropertiesPanel.nameProperties.setName({ newName: 
"Approved" });
+    await sequenceFlowPropertiesPanel.close();
+    await diagram.resetFocus();
+
+    await expect(edges.getLabel({ name: "Approved" })).toBeVisible();
+  });
+
+  test("should move the label and save its position", async ({ edges, 
jsonModel }) => {
+    const bpmnElementId = (await jsonModel.getSequenceFlows())[0]["@_id"]!;
+    expect(await jsonModel.getEdgeLabelBounds({ bpmnElementId 
})).toBeUndefined();
+
+    const labelCenterBeforeMove = await edges.getLabelCenterPosition({ name: 
"Approved" });
+    await edges.moveLabel({ name: "Approved", offset: { x: 60, y: 40 } });
+
+    await expect.poll(() => jsonModel.getEdgeLabelBounds({ bpmnElementId 
})).toBeDefined();
+    const labelCenterAfterMove = await edges.getLabelCenterPosition({ name: 
"Approved" });
+    expect(labelCenterAfterMove.x - labelCenterBeforeMove.x).toBeCloseTo(60, 
0);
+    expect(labelCenterAfterMove.y - labelCenterBeforeMove.y).toBeCloseTo(40, 
0);
+
+    const boundsBeforeSecondMove = (await jsonModel.getEdgeLabelBounds({ 
bpmnElementId }))!;
+    await edges.moveLabel({ name: "Approved", offset: { x: -30, y: 20 } });
+
+    await expect
+      .poll(async () => (await jsonModel.getEdgeLabelBounds({ bpmnElementId 
}))?.["@_x"])
+      .toBeCloseTo(boundsBeforeSecondMove["@_x"] - 30, 0);
+    expect((await jsonModel.getEdgeLabelBounds({ bpmnElementId 
}))?.["@_y"]).toBeCloseTo(
+      boundsBeforeSecondMove["@_y"] + 20,
+      0
+    );
+  });
+
+  test("should keep the moved label next to its Sequence Flow when a connected 
node moves", async ({
+    edges,
+    nodes,
+    jsonModel,
+  }) => {
+    const bpmnElementId = (await jsonModel.getSequenceFlows())[0]["@_id"]!;
+    await edges.moveLabel({ name: "Approved", offset: { x: 40, y: 40 } });
+    await expect.poll(() => jsonModel.getEdgeLabelBounds({ bpmnElementId 
})).toBeDefined();
+    const edgeBeforeNodeMove = (await jsonModel.getEdge({ bpmnElementId }))!;
+
+    const taskBCenter = await nodes.getNodeCenterPosition({ name: "Task B" });
+    await nodes.dragNodeToPosition({ name: "Task B", toPosition: { x: 
taskBCenter.x + 200, y: taskBCenter.y } });
+
+    await expect
+      .poll(async () => (await jsonModel.getEdge({ bpmnElementId 
}))?.["di:waypoint"])
+      .not.toEqual(edgeBeforeNodeMove["di:waypoint"]);
+    const edgeAfterNodeMove = (await jsonModel.getEdge({ bpmnElementId }))!;
+
+    
expect(edgeAfterNodeMove["bpmndi:BPMNLabel"]).not.toEqual(edgeBeforeNodeMove["bpmndi:BPMNLabel"]);
+    expect(edges.getLabelDistanceToEdge(edgeAfterNodeMove)).toBeCloseTo(
+      edges.getLabelDistanceToEdge(edgeBeforeNodeMove),
+      0
+    );
+  });
+});


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to