@sapui5/sap.suite.ui.commons 1.136.11 → 1.136.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/package.json +1 -1
  2. package/src/sap/suite/ui/commons/.library +1 -1
  3. package/src/sap/suite/ui/commons/AriaProperties.js +1 -1
  4. package/src/sap/suite/ui/commons/CalculationBuilder.js +1 -1
  5. package/src/sap/suite/ui/commons/CalculationBuilderExpression.js +1 -1
  6. package/src/sap/suite/ui/commons/CalculationBuilderFunction.js +1 -1
  7. package/src/sap/suite/ui/commons/CalculationBuilderGroup.js +1 -1
  8. package/src/sap/suite/ui/commons/CalculationBuilderItem.js +1 -1
  9. package/src/sap/suite/ui/commons/CalculationBuilderValidationResult.js +1 -1
  10. package/src/sap/suite/ui/commons/CalculationBuilderVariable.js +1 -1
  11. package/src/sap/suite/ui/commons/CloudFilePicker.js +1 -1
  12. package/src/sap/suite/ui/commons/MicroProcessFlow.js +1 -1
  13. package/src/sap/suite/ui/commons/MicroProcessFlowItem.js +1 -1
  14. package/src/sap/suite/ui/commons/TimelineRenderManager.js +8 -4
  15. package/src/sap/suite/ui/commons/flexibility/changeHandler/PropertyChangeMapper.js +1 -1
  16. package/src/sap/suite/ui/commons/imageeditor/CropCustomShapeHistoryItem.js +1 -1
  17. package/src/sap/suite/ui/commons/imageeditor/CropEllipseHistoryItem.js +1 -1
  18. package/src/sap/suite/ui/commons/imageeditor/CropRectangleHistoryItem.js +1 -1
  19. package/src/sap/suite/ui/commons/imageeditor/CustomSizeItem.js +1 -1
  20. package/src/sap/suite/ui/commons/imageeditor/FilterHistoryItem.js +1 -1
  21. package/src/sap/suite/ui/commons/imageeditor/FlipHistoryItem.js +1 -1
  22. package/src/sap/suite/ui/commons/imageeditor/HistoryItem.js +1 -1
  23. package/src/sap/suite/ui/commons/imageeditor/ImageEditor.js +1 -1
  24. package/src/sap/suite/ui/commons/imageeditor/ImageEditorContainer.js +1 -1
  25. package/src/sap/suite/ui/commons/imageeditor/ImageEditorResponsiveContainer.js +1 -1
  26. package/src/sap/suite/ui/commons/imageeditor/ResizeHistoryItem.js +1 -1
  27. package/src/sap/suite/ui/commons/imageeditor/RotateHistoryItem.js +1 -1
  28. package/src/sap/suite/ui/commons/library.js +100 -3
  29. package/src/sap/suite/ui/commons/messagebundle.properties +73 -12
  30. package/src/sap/suite/ui/commons/messagebundle_ar.properties +1 -1
  31. package/src/sap/suite/ui/commons/messagebundle_en_US_saprigi.properties +39 -5
  32. package/src/sap/suite/ui/commons/messagebundle_fr_CA.properties +1 -1
  33. package/src/sap/suite/ui/commons/messagebundle_hu.properties +1 -1
  34. package/src/sap/suite/ui/commons/messagebundle_id.properties +2 -2
  35. package/src/sap/suite/ui/commons/messagebundle_it.properties +1 -1
  36. package/src/sap/suite/ui/commons/messagebundle_pt.properties +1 -1
  37. package/src/sap/suite/ui/commons/messagebundle_ru.properties +1 -1
  38. package/src/sap/suite/ui/commons/networkgraph/ElementBase.js +19 -1
  39. package/src/sap/suite/ui/commons/networkgraph/Graph.js +590 -48
  40. package/src/sap/suite/ui/commons/networkgraph/GraphMap.js +25 -3
  41. package/src/sap/suite/ui/commons/networkgraph/GraphRenderer.js +19 -8
  42. package/src/sap/suite/ui/commons/networkgraph/KeyboardNavigator.js +367 -12
  43. package/src/sap/suite/ui/commons/networkgraph/Line.js +814 -22
  44. package/src/sap/suite/ui/commons/networkgraph/Node.js +573 -79
  45. package/src/sap/suite/ui/commons/networkgraph/Tooltip.js +4 -0
  46. package/src/sap/suite/ui/commons/networkgraph/Utils.js +249 -10
  47. package/src/sap/suite/ui/commons/networkgraph/layout/NoopLayout.js +77 -7
  48. package/src/sap/suite/ui/commons/networkgraph/util/ConnectionPathUtils.js +1174 -0
  49. package/src/sap/suite/ui/commons/networkgraph/util/CreateConnectionPopover.js +374 -0
  50. package/src/sap/suite/ui/commons/networkgraph/util/DependencyLayoutHelper.js +1017 -0
  51. package/src/sap/suite/ui/commons/networkgraph/util/DragDropManager.js +721 -0
  52. package/src/sap/suite/ui/commons/networkgraph/util/PortManager.js +582 -0
  53. package/src/sap/suite/ui/commons/statusindicator/Circle.js +1 -1
  54. package/src/sap/suite/ui/commons/statusindicator/CustomShape.js +1 -1
  55. package/src/sap/suite/ui/commons/statusindicator/DiscreteThreshold.js +1 -1
  56. package/src/sap/suite/ui/commons/statusindicator/FillingOption.js +1 -1
  57. package/src/sap/suite/ui/commons/statusindicator/LibraryShape.js +1 -1
  58. package/src/sap/suite/ui/commons/statusindicator/Path.js +1 -1
  59. package/src/sap/suite/ui/commons/statusindicator/PropertyThreshold.js +1 -1
  60. package/src/sap/suite/ui/commons/statusindicator/Rectangle.js +1 -1
  61. package/src/sap/suite/ui/commons/statusindicator/Shape.js +1 -1
  62. package/src/sap/suite/ui/commons/statusindicator/ShapeGroup.js +1 -1
  63. package/src/sap/suite/ui/commons/statusindicator/SimpleShape.js +1 -1
  64. package/src/sap/suite/ui/commons/statusindicator/StatusIndicator.js +1 -1
  65. package/src/sap/suite/ui/commons/taccount/TAccount.js +1 -1
  66. package/src/sap/suite/ui/commons/taccount/TAccountGroup.js +1 -1
  67. package/src/sap/suite/ui/commons/taccount/TAccountItem.js +1 -1
  68. package/src/sap/suite/ui/commons/taccount/TAccountItemProperty.js +1 -1
  69. package/src/sap/suite/ui/commons/taccount/TAccountPanel.js +1 -1
  70. package/src/sap/suite/ui/commons/themes/base/NetworkGraph.less +26 -13
  71. package/src/sap/suite/ui/commons/themes/base/NetworkGroup.less +4 -0
  72. package/src/sap/suite/ui/commons/themes/base/NetworkLine.less +58 -13
  73. package/src/sap/suite/ui/commons/themes/base/NetworkNode.less +251 -47
  74. package/src/sap/suite/ui/commons/themes/base/SemanticColorMixins.less +55 -0
@@ -0,0 +1,1174 @@
1
+ /*!
2
+ *
3
+ SAP UI development toolkit for HTML5 (SAPUI5)
4
+ (c) Copyright 2009-2015 SAP SE. All rights reserved
5
+
6
+ */
7
+ sap.ui.define([
8
+ "sap/suite/ui/commons/library"
9
+ ], function (library) {
10
+ "use strict";
11
+ const ConnectionType = library.networkgraph.ConnectionType;
12
+ const GRID_SIZE = 54; // Size of each grid cell
13
+ const MIN_EDGE_LENGTH = 44; // Minimum length of an edge to avoid too short lines
14
+ /**
15
+ * Utility class for calculating connection paths between nodes in network graphs.
16
+ * This utility provides a method to normalize lines and calculate path coordinates
17
+ * based on different connection types.
18
+ *
19
+ * @namespace sap.suite.ui.commons.networkgraph.util.ConnectionPathUtils
20
+ * @public
21
+ * @since 1.136.19
22
+ */
23
+ var ConnectionPathUtils = {
24
+ /**
25
+ * Gets the effective connection type for path calculation based on RTL mode.
26
+ * In RTL mode, semantic directions are reversed since nodes are already mirrored.
27
+ *
28
+ * @param {string} sConnectionType - The configured connection type
29
+ * @param {boolean} bIsRTL - Whether RTL mode is enabled
30
+ * @returns {string} The effective connection type to use for calculations
31
+ * @private
32
+ */
33
+ _getEffectiveConnectionType: function (sConnectionType, bIsRTL) {
34
+ if (!bIsRTL) {
35
+ return sConnectionType;
36
+ }
37
+
38
+ // In RTL mode, semantic directions are reversed (nodes are already mirrored)
39
+ const rtlMapping = {
40
+ [ConnectionType.RightToLeft]: ConnectionType.LeftToRight,
41
+ [ConnectionType.LeftToRight]: ConnectionType.RightToLeft,
42
+ [ConnectionType.LeftToLeft]: ConnectionType.RightToRight,
43
+ [ConnectionType.RightToRight]: ConnectionType.LeftToLeft
44
+ };
45
+
46
+ return rtlMapping[sConnectionType] || sConnectionType;
47
+ },
48
+
49
+ /**
50
+ * Normalizes lines in a network graph by calculating path coordinates
51
+ * between source and target nodes based on connection types.
52
+ *
53
+ * @param {sap.suite.ui.commons.networkgraph.Graph} oGraph - The network graph instance
54
+ * @param {object} [mConfig] - Configuration object
55
+ * @param {number} [mConfig.gridSize=50] - Size of each grid cell
56
+ * @param {number} [mConfig.minEdgeLength=20] - Minimum length of an edge
57
+ * @public
58
+ */
59
+ normalizeLines: function (oGraph, mConfig) {
60
+ const config = Object.assign({
61
+ gridSize: GRID_SIZE,
62
+ minEdgeLength: MIN_EDGE_LENGTH
63
+ }, mConfig || {});
64
+
65
+ // Enable multiple directed arrows feature when using ConnectionPathUtils
66
+ oGraph._enableMultipleDirectedArrows = true;
67
+
68
+ // Detect RTL mode from graph
69
+ const bIsRTL = oGraph._bIsRtl || false;
70
+
71
+ // Iterate over all lines in the graph
72
+ oGraph.getLines().forEach((oLine) => {
73
+ const configuredConnectionType = oLine.getConnectionType(); // Get the connection type, if not configured at Line level then the default is Right to Left from source to target.
74
+ // In RTL mode, use effective connection type (semantically reversed)
75
+ const effectiveConnectionType = this._getEffectiveConnectionType(configuredConnectionType, bIsRTL);
76
+ const oSourceNode = oLine.getFromNode();
77
+ const oTargetNode = oLine.getToNode();
78
+ // Get anchor coordinates for source and target nodes, usually the center of the respective sides
79
+ const oSourceAnchor = this._getNodeAnchorCoordinates(oLine.getFromNode(), effectiveConnectionType, "source");
80
+ const oTargetAnchor = this._getNodeAnchorCoordinates(oLine.getToNode(), effectiveConnectionType, "target");
81
+ const mParams = {
82
+ sourceAnchor: oSourceAnchor,
83
+ targetAnchor: oTargetAnchor,
84
+ sourceNode: oSourceNode,
85
+ targetNode: oTargetNode,
86
+ sConnectionType: effectiveConnectionType,
87
+ gridSize: config.gridSize,
88
+ minEdgeLength: config.minEdgeLength
89
+ };
90
+ // Calculate the path coordinates between the source and target anchors
91
+ const aCoordinates = this._calculatePathCoordinates(mParams);
92
+ // set the anchor points of the source and target nodes on the line as source and target for drawing the path.
93
+ oLine.setSource({
94
+ x: oSourceAnchor.x,
95
+ y: oSourceAnchor.y
96
+ });
97
+ oLine.setTarget({
98
+ x: oTargetAnchor.x,
99
+ y: oTargetAnchor.y
100
+ });
101
+ // Set the calculated bend points on the line
102
+ oLine.clearBends();
103
+ // Set the calculated bend points on the line, determined by the connection type.
104
+ if (aCoordinates && aCoordinates.length) {
105
+ aCoordinates.forEach((oBend) => {
106
+ oLine.addBend(oBend);
107
+ });
108
+ }
109
+ });
110
+ },
111
+ /**
112
+ * Calculates path coordinates between source and target anchors based on connection type.
113
+ *
114
+ * @param {object} mParams - Parameters for path calculation
115
+ * @param {object} mParams.sourceAnchor - Source anchor coordinates {x, y}
116
+ * @param {object} mParams.targetAnchor - Target anchor coordinates {x, y}
117
+ * @param {object} mParams.sourceNode - Source node object
118
+ * @param {object} mParams.targetNode - Target node object
119
+ * @param {string} mParams.sConnectionType - Connection type
120
+ * @param {number} [mParams.gridSize=50] - Grid size for calculations
121
+ * @param {number} [mParams.minEdgeLength=20] - Minimum edge length
122
+ * @returns {Array<object>} Array of bend point coordinates
123
+ * @private
124
+ */
125
+ _calculatePathCoordinates: function (mParams) {
126
+ const { sConnectionType } = mParams;
127
+ switch (sConnectionType) {
128
+ case ConnectionType.RightToLeft:
129
+ return this._handleRightToLeftConnection(mParams);
130
+ case ConnectionType.LeftToRight:
131
+ return this._handleLeftToRightConnection(mParams);
132
+ case ConnectionType.LeftToLeft:
133
+ return this._handleLeftToLeftConnection(mParams);
134
+ case ConnectionType.RightToRight:
135
+ return this._handleRightToRightConnection(mParams);
136
+ default:
137
+ // default RightToLeft case
138
+ // This is a fallback for any unrecognized connection type
139
+ return this._handleRightToLeftConnection(mParams);
140
+ }
141
+ },
142
+ /**
143
+ * ╔══════════════════════════════════════════════════════════════════════════════╗
144
+ * ║ RIGHT-TO-LEFT CONNECTION HANDLER ║
145
+ * ╠══════════════════════════════════════════════════════════════════════════════╣
146
+ * ║ ║
147
+ * ║ [SRC NODE]→ ········→ ←[TGT NODE] ║
148
+ * ║ ║
149
+ * ║ Calculates optimal path from right anchor of source to left anchor of ║
150
+ * ║ target, creating L-shaped, U-shaped, Z-bend, or S-shaped connections ║
151
+ * ║ based on node positions and grid constraints. ║
152
+ * ║ ║
153
+ * ╚══════════════════════════════════════════════════════════════════════════════╝
154
+ */
155
+ /**
156
+ * Handles RightToLeft connection path calculation.
157
+ * Connection flows from the right side of source node to the left side of target node.
158
+ *
159
+ * @param {object} mParams - Parameters for path calculation
160
+ * @returns {Array<object>} Array of bend point coordinates
161
+ * @private
162
+ */
163
+ _handleRightToLeftConnection: function (mParams) {
164
+ const { sourceNode, gridSize = GRID_SIZE, minEdgeLength = MIN_EDGE_LENGTH } = mParams;
165
+ const { x1, y1, x2, y2, AX1, AY1, AX2, AY2, dx } = this._transformSourceTargetCoordinates(mParams);
166
+
167
+ // CASE 1: Source and target nodes are on the same horizontal line (y1 === y2)
168
+ if (y1 === y2) {
169
+ if (x2 > x1) {
170
+ // SCENARIO 1A: Direct horizontal connection (source right → target left)
171
+ // Check if anchors are aligned and connection pattern allows straight line
172
+ const anchorVerticalOffset = Math.abs(AY1 - AY2);
173
+ const shouldUseStraightLine = this._shouldUseHorizontalStraightLine(mParams.sourceNode, mParams.targetNode, mParams.sConnectionType);
174
+
175
+ if (shouldUseStraightLine && anchorVerticalOffset <= 5) {
176
+ // Visual: [SRC]----→[TGT] (perfect alignment)
177
+ return [];
178
+ } else if (anchorVerticalOffset > 5) {
179
+ // Visual: [SRC]→ ←[TGT] (offset anchors - add horizontal correction)
180
+ // | |
181
+ // └─────┘
182
+ const midX = AX1 + (AX2 - AX1) / 2;
183
+ return [
184
+ { x: midX, y: AY1 },
185
+ { x: midX, y: AY2 }
186
+ ];
187
+ } else {
188
+ // Multiple connections - use straight line anyway for now
189
+ return [];
190
+ }
191
+ } else if (x1 >= x2) {
192
+ // SCENARIO 1B: Backward horizontal connection (source overlaps/behind target)
193
+ // Visual: [TGT] [SRC]
194
+ // ↑----→ |
195
+ // | |
196
+ // ←------↓
197
+ // Create U-shaped path going around both nodes
198
+ const x3 = this.getRightGridIntersectionPoint(AX1, AY1, gridSize, minEdgeLength).x;
199
+ const x4 = this.getLeftGridIntersectionPoint(AX2, AY2, gridSize, minEdgeLength).x;
200
+ const y3 = this.getBottomGridIntersectionPoint(x3, y1 + sourceNode._iHeight, gridSize, minEdgeLength).y;
201
+
202
+ return [
203
+ { x: x3, y: AY1 }, // Right from source
204
+ { x: x3, y: y3 }, // Down below both nodes
205
+ { x: x4, y: y3 }, // Left to target column
206
+ { x: x4, y: AY2 } // Up to target
207
+ ];
208
+ }
209
+ } else if (y1 != y2) {
210
+ if (x1 <= x2) {
211
+ // SCENARIO 2A: Source is to the left or aligned with target
212
+ if (dx >= minEdgeLength + sourceNode._iWidth) {
213
+ if (Math.abs(x1 + sourceNode._iWidth - x2) <= gridSize) {
214
+ // SCENARIO 2A1: Nodes are close - use midpoint connection
215
+ // Visual: [SRC]→ ←[TGT]
216
+ // | |
217
+ // ↓ ↑
218
+ // (midpoint)
219
+ const x3 = AX1 + (x2 - (x1 + sourceNode._iWidth)) / 2;
220
+ return [{ x: x3, y: AY1 }, { x: x3, y: AY2 }];
221
+ } else {
222
+ // SCENARIO 2A2: Nodes are far apart - use grid-aligned Z-bend
223
+ // Visual: [SRC]→ |
224
+ // |
225
+ // ↓
226
+ // ←[TGT]
227
+ const x3 = this.getRightGridIntersectionPoint(x1 + sourceNode._iWidth, AY1, gridSize, minEdgeLength).x;
228
+ return [{ x: x3, y: AY1 }, { x: x3, y: AY2 }];
229
+ }
230
+ } else if (dx <= minEdgeLength + sourceNode._iWidth) {
231
+ // SCENARIO 2B: Nodes are too close - create S-shaped path
232
+ if (y1 < y2) {
233
+ // SCENARIO 2B1: Source above target - S-bend going down
234
+ // Visual: [SRC]→ |
235
+ // |
236
+ // ↓----→ |
237
+ // |
238
+ // ↓
239
+ // ←[TGT]
240
+ const x3 = this.getRightGridIntersectionPoint(x1 + sourceNode._iWidth, y1, gridSize, minEdgeLength).x;
241
+ const x4 = this.getLeftGridIntersectionPoint(x2, AY2, gridSize, minEdgeLength).x;
242
+ const y3 = this.getBottomGridIntersectionPoint(x3, y1 + sourceNode._iHeight, gridSize, minEdgeLength).y;
243
+ return [{ x: x3, y: AY1 }, { x: x3, y: y3 }, { x: x4, y: y3 }, { x: x4, y: AY2 }];
244
+ } else if (y1 > y2) {
245
+ // SCENARIO 2B2: Source below target - S-bend going up
246
+ // Visual: ←[TGT]
247
+ // ↑
248
+ // ↑----→ |
249
+ // |
250
+ // |
251
+ // [SRC]→ |
252
+ const x3 = this.getRightGridIntersectionPoint(AX1, AY2, gridSize, minEdgeLength).x;
253
+ const x4 = this.getLeftGridIntersectionPoint(AX2, AY2, gridSize, minEdgeLength).x;
254
+ const y3 = this.getTopGridIntersectionPoint(x3, y1, gridSize, minEdgeLength).y;
255
+ return [{ x: x3, y: AY1 }, { x: x3, y: y3 }, { x: x4, y: y3 }, { x: x4, y: AY2 }];
256
+ }
257
+ }
258
+ } else if (x1 > x2) {
259
+ // SCENARIO 3: Source is to the right of target - create S-shaped path
260
+ if (y1 < y2) {
261
+ // SCENARIO 3A: Source above and right of target - S-bend down and left
262
+ // Visual: [SRC]→ |
263
+ // |
264
+ // ↓
265
+ // ←[TGT] ←---------
266
+ const x3 = this.getRightGridIntersectionPoint(AX1, AY1, gridSize, minEdgeLength).x;
267
+ const x4 = this.getLeftGridIntersectionPoint(AX2, AY2, gridSize, minEdgeLength).x;
268
+ const y3 = this.getBottomGridIntersectionPoint(x3, y1 + sourceNode._iHeight, gridSize, minEdgeLength).y;
269
+ return [
270
+ { x: x3, y: AY1 }, // Right from source
271
+ { x: x3, y: y3 }, // Down past both nodes
272
+ { x: x4, y: y3 }, // Left to target column
273
+ { x: x4, y: AY2 } // Up to target
274
+ ];
275
+ } else if (y1 > y2) {
276
+ // SCENARIO 3B: Source below and right of target - S-bend up and left
277
+ // Visual: ←[TGT] ←---------
278
+ // ↑
279
+ // |
280
+ // [SRC]→ |
281
+ const x3 = this.getRightGridIntersectionPoint(AX1, AY1, gridSize, minEdgeLength).x;
282
+ const x4 = this.getLeftGridIntersectionPoint(AX2, AY2, gridSize, minEdgeLength).x;
283
+ const y3 = this.getTopGridIntersectionPoint(x3, y1, gridSize, minEdgeLength).y;
284
+ return [{ x: x3, y: AY1 }, { x: x3, y: y3 }, { x: x4, y: y3 }, { x: x4, y: AY2 }];
285
+ }
286
+ }
287
+ }
288
+ // DEFAULT: No bends needed (direct connection possible)
289
+ return [];
290
+ },
291
+ /**
292
+ * ╔══════════════════════════════════════════════════════════════════════════════╗
293
+ * ║ LEFT-TO-RIGHT CONNECTION HANDLER ║
294
+ * ╠══════════════════════════════════════════════════════════════════════════════╣
295
+ * ║ ║
296
+ * ║ ←[SRC NODE] ········→ [TGT NODE]→ ║
297
+ * ║ ║
298
+ * ║ Calculates optimal path from left anchor of source to right anchor of ║
299
+ * ║ target, creating direct, U-shaped, Z-bend, or S-shaped connections ║
300
+ * ║ based on node positions and grid alignment. ║
301
+ * ║ ║
302
+ * ╚══════════════════════════════════════════════════════════════════════════════╝
303
+ */
304
+ /**
305
+ * Handles LeftToRight connection path calculation.
306
+ * Connection flows from the left side of source node to the right side of target node.
307
+ *
308
+ * @param {object} mParams - Parameters for path calculation
309
+ * @returns {Array<object>} Array of bend point coordinates
310
+ * @private
311
+ */
312
+ _handleLeftToRightConnection: function (mParams) {
313
+ const { sourceNode, targetNode, gridSize = GRID_SIZE, minEdgeLength = MIN_EDGE_LENGTH } = mParams;
314
+ const { x1, y1, x2, y2, AX1, AY1, AX2, AY2, dx } = this._transformSourceTargetCoordinates(mParams);
315
+ // CASE 1: Source and target nodes are on the same horizontal line (y1 === y2)
316
+ if (y1 === y2) {
317
+ if (x1 > x2) {
318
+ // SCENARIO 1A: Direct horizontal connection (source left → target right, but source is actually to the right)
319
+ // Check if anchors are aligned and connection pattern allows straight line
320
+ const anchorVerticalOffset = Math.abs(AY1 - AY2);
321
+ const shouldUseStraightLine = this._shouldUseHorizontalStraightLine(mParams.sourceNode, mParams.targetNode, mParams.sConnectionType);
322
+
323
+ if (shouldUseStraightLine && anchorVerticalOffset <= 5) {
324
+ // Visual: [TGT]←----[SRC] (perfect alignment)
325
+ return [];
326
+ } else if (anchorVerticalOffset > 5) {
327
+ // Visual: ←[SRC] [TGT]→ (offset anchors - add horizontal correction)
328
+ // | |
329
+ // └───────────┘
330
+ const midX = AX1 + (AX2 - AX1) / 2;
331
+ return [
332
+ { x: midX, y: AY1 },
333
+ { x: midX, y: AY2 }
334
+ ];
335
+ } else {
336
+ // Multiple connections - use straight line anyway for now
337
+ return [];
338
+ }
339
+ } else if (x1 <= x2) {
340
+ // SCENARIO 1B: Forward horizontal connection but needs U-shape (source overlaps/in front of target)
341
+ // Visual: [SRC] [TGT]
342
+ // ↓----→ |
343
+ // | |
344
+ // ↑------↓
345
+ // Create U-shaped path going around both nodes
346
+ const x3 = this.getLeftGridIntersectionPoint(AX1, AY1, gridSize, minEdgeLength).x;
347
+ const x4 = this.getRightGridIntersectionPoint(AX2, AY2, gridSize, minEdgeLength).x;
348
+ const y3 = this.getBottomGridIntersectionPoint(x3, y1 + sourceNode._iHeight, gridSize, minEdgeLength).y;
349
+
350
+ return [
351
+ { x: x3, y: AY1 }, // Right from source
352
+ { x: x3, y: y3 }, // Down below both nodes
353
+ { x: x4, y: y3 }, // Left to target column
354
+ { x: x4, y: AY2 } // Up to target
355
+ ];
356
+ }
357
+ } else if (y1 != y2) {
358
+ // CASE 2: Source and target nodes are on different horizontal lines (y1 != y2)
359
+ if (x1 >= x2) {
360
+ // SCENARIO 2A: Source is to the right or aligned with target
361
+ if (dx >= minEdgeLength + targetNode._iWidth) {
362
+ if (Math.abs(x1 - (x2 + targetNode._iWidth)) <= gridSize) {
363
+ // SCENARIO 2A1: Nodes are close - use midpoint connection
364
+ // Visual: ←[SRC] [TGT]→
365
+ // | |
366
+ // ↓ ↑
367
+ // (midpoint)
368
+ const x3 = (AX1 + AX2) / 2;
369
+ return [{ x: x3, y: AY1 }, { x: x3, y: AY2 }];
370
+ } else {
371
+ // SCENARIO 2A2: Nodes are far apart - use grid-aligned Z-bend
372
+ // Visual: ←[SRC] |
373
+ // |
374
+ // ↓
375
+ // [TGT]→
376
+ const x3 = this.getLeftGridIntersectionPoint(AX1, AY1, gridSize, minEdgeLength).x;
377
+ return [{ x: x3, y: AY1 }, { x: x3, y: AY2 }];
378
+ }
379
+ } else if (dx <= minEdgeLength + targetNode._iWidth) {
380
+ // SCENARIO 2B: Nodes are too close - create S-shaped path
381
+ if (y1 < y2) {
382
+ // SCENARIO 2B1: Source above target - S-bend going down
383
+ // Visual: ←[SRC] |
384
+ // |
385
+ // ←---- ↓
386
+ // |
387
+ // ↓
388
+ // [TGT]→
389
+ const x3 = this.getLeftGridIntersectionPoint(AX1, AY1, gridSize, minEdgeLength).x;
390
+ const x4 = this.getRightGridIntersectionPoint(AX2, AY2, gridSize, minEdgeLength).x;
391
+ const y3 = this.getBottomGridIntersectionPoint(x3, y1 + sourceNode._iHeight, gridSize, minEdgeLength).y;
392
+ return [{ x: x3, y: AY1 }, { x: x3, y: y3 }, { x: x4, y: y3 }, { x: x4, y: AY2 }];
393
+ } else if (y1 > y2) {
394
+ // SCENARIO 2B2: Source below target - S-bend going up
395
+ // Visual: [TGT]→
396
+ // ↑
397
+ // ←------ |
398
+ // |
399
+ // |
400
+ // ←[SRC] |
401
+ const x3 = this.getLeftGridIntersectionPoint(AX1, AY2, gridSize, minEdgeLength).x;
402
+ const x4 = this.getRightGridIntersectionPoint(AX2, AY2, gridSize, minEdgeLength).x;
403
+ const y3 = this.getTopGridIntersectionPoint(x3, y1, gridSize, minEdgeLength).y;
404
+ return [{ x: x3, y: AY1 }, { x: x3, y: y3 }, { x: x4, y: y3 }, { x: x4, y: AY2 }];
405
+ }
406
+ }
407
+ } else if (x1 < x2) {
408
+ // SCENARIO 3: Source is to the left of target - create S-shaped path
409
+ if (y1 < y2) {
410
+ // SCENARIO 3A: Source above and left of target - S-bend down and right
411
+ // Visual: ←[SRC] |
412
+ // |
413
+ // ↓
414
+ // --------→[TGT]
415
+ const x3 = this.getLeftGridIntersectionPoint(AX1, AY1, gridSize, minEdgeLength).x;
416
+ const x4 = this.getRightGridIntersectionPoint(AX2, AY2, gridSize, minEdgeLength).x;
417
+ const y3 = this.getBottomGridIntersectionPoint(x3, y1 + sourceNode._iHeight, gridSize, minEdgeLength).y;
418
+ return [{ x: x3, y: AY1 }, { x: x3, y: y3 }, { x: x4, y: y3 }, { x: x4, y: AY2 }];
419
+ } else if (y1 > y2) {
420
+ // SCENARIO 3B: Source below and left of target - S-bend up and right
421
+ // Visual: --------→[TGT]
422
+ // ↑
423
+ // |
424
+ // ←[SRC] |
425
+ const x3 = this.getLeftGridIntersectionPoint(AX1, AY1, gridSize, minEdgeLength).x;
426
+ const x4 = this.getRightGridIntersectionPoint(AX2, AY2, gridSize, minEdgeLength).x;
427
+ const y3 = this.getTopGridIntersectionPoint(x3, y1, gridSize, minEdgeLength).y;
428
+ return [{ x: x3, y: AY1 }, { x: x3, y: y3 }, { x: x4, y: y3 }, { x: x4, y: AY2 }];
429
+ }
430
+ }
431
+ }
432
+ // DEFAULT: No bends needed (direct connection possible)
433
+ return [];
434
+ },
435
+ /**
436
+ * ╔══════════════════════════════════════════════════════════════════════════════╗
437
+ * ║ LEFT-TO-LEFT CONNECTION HANDLER ║
438
+ * ╠══════════════════════════════════════════════════════════════════════════════╣
439
+ * ║ ║
440
+ * ║ ←[SRC NODE] ········→ ←[TGT NODE] ║
441
+ * ║ ║
442
+ * ║ Calculates optimal path from left anchor of source to left anchor of ║
443
+ * ║ target, creating C-shaped or stepped connections that route around ║
444
+ * ║ the left side of both nodes to avoid overlaps. ║
445
+ * ║ ║
446
+ * ╚══════════════════════════════════════════════════════════════════════════════╝
447
+ */
448
+ /**
449
+ * Handles LeftToLeft connection path calculation.
450
+ * Connection flows from the left side of source node to the left side of target node.
451
+ *
452
+ * @param {object} mParams - Parameters for path calculation
453
+ * @returns {Array<object>} Array of bend point coordinates
454
+ * @private
455
+ */
456
+ _handleLeftToLeftConnection: function (mParams) {
457
+ const { sourceNode, targetNode, gridSize = GRID_SIZE, minEdgeLength = MIN_EDGE_LENGTH } = mParams;
458
+ const { x1, y1, x2, y2, AX1, AY1, AX2, AY2 } = this._transformSourceTargetCoordinates(mParams);
459
+ // CASE 1: Source and target nodes are on the same horizontal line (y1 === y2)
460
+ if (y1 === y2) {
461
+ if (x1 > x2) {
462
+ // SCENARIO 1A: Source to the right of target - create C-shaped path on left side
463
+ // Visual: ←[TGT] ←[SRC]
464
+ // | |
465
+ // ↓ |
466
+ // ←-----------↓
467
+ // Check if nodes are close enough for midpoint, else use grid-aligned path
468
+ const x3 = Math.abs(x1 - x2 + targetNode._iWidth) <= gridSize ?
469
+ (AX1 + (x2 + targetNode._iWidth)) / 2 :
470
+ this.getLeftGridIntersectionPoint(AX1, AY1, gridSize, minEdgeLength).x;
471
+ const x4 = this.getLeftGridIntersectionPoint(AX2, AY2, gridSize, minEdgeLength).x;
472
+ const y3 = y1 <= gridSize ? this.getBottomGridIntersectionPoint(x3, y1 + sourceNode._iHeight, gridSize, minEdgeLength).y : this.getTopGridIntersectionPoint(x3, y1, gridSize, minEdgeLength).y;
473
+ return [
474
+ { x: x3, y: AY1 }, // Left from source
475
+ { x: x3, y: y3 }, // Down/up to avoid nodes
476
+ { x: x4, y: y3 }, // Across to target column
477
+ { x: x4, y: AY2 } // Up/down to target
478
+ ];
479
+ } else if (x1 <= x2) {
480
+ // SCENARIO 1B: Source to the left of target - create C-shaped path on left side
481
+ // Visual: ←[SRC] ←[TGT]
482
+ // | |
483
+ // ↓ |
484
+ // →-------------↓
485
+ const x3 = this.getLeftGridIntersectionPoint(AX1, AY1, gridSize, minEdgeLength).x;
486
+ const x4 = Math.abs(x1 + sourceNode._iWidth - x2) <= gridSize ?
487
+ (AX2 + x2) / 2 :
488
+ this.getLeftGridIntersectionPoint(AX2, AY2, gridSize, minEdgeLength).x;
489
+ const y3 = y1 <= gridSize ? this.getBottomGridIntersectionPoint(x3, y1 + sourceNode._iHeight, gridSize, minEdgeLength).y : this.getTopGridIntersectionPoint(x3, y1, gridSize, minEdgeLength).y;
490
+ return [{ x: x3, y: AY1 }, { x: x3, y: y3 }, { x: x4, y: y3 }, { x: x4, y: AY2 }];
491
+ }
492
+ } else if (y1 != y2) {
493
+ // CASE 2: Source and target nodes are on different horizontal lines (y1 != y2)
494
+ /**
495
+ * When the source and target nodes are not on the same horizontal line (y1 != y2)
496
+ * Then nodes can be placed in either x1 > x2 or x1 < x2
497
+ * with these positions there can be two cases:
498
+ * 1. y1 < y2: source node is above the target node
499
+ * 2. y1 > y2: source node is below the target node
500
+ * In both cases we need to calculate the vertical bend points based on node's y so that the lines do not overlap with the target node.
501
+ * If x1 < x2 and y1 < y2 then we know that source node is above the target node and to the left of it. Based on AY2 we can calculate above or below bend points.
502
+ */
503
+ if (x1 > x2) {
504
+ // SCENARIO 2A: Source is to the right of target
505
+ if (y1 < y2) {
506
+ // SCENARIO 2A1: Source above and right of target
507
+ if (AY1 < y2) {
508
+ // Simple vertical connection if source anchor is above target node
509
+ // Visual: ←[SRC]
510
+ // |
511
+ // |
512
+ // ↓
513
+ // ←[TGT]
514
+ const x4 = this.getLeftGridIntersectionPoint(AX2, AY2, gridSize, minEdgeLength).x;
515
+ return [{ x: x4, y: AY1 }, { x: x4, y: AY2 }];
516
+ } else if (AY1 >= y2) {
517
+ // Need to avoid target node - create stepped path
518
+ // Visual: ←[SRC]
519
+ // |
520
+ // ←[TGT] ←-----↓
521
+ const iDiff = Math.abs(AX1 - (x2 + targetNode._iWidth));
522
+ let x3 = null;
523
+ if (iDiff <= gridSize) {
524
+ x3 = iDiff < minEdgeLength ? (AX1 + (x2 + targetNode._iWidth) / 2) : AX1 - minEdgeLength;
525
+ } else {
526
+ x3 = this.getLeftGridIntersectionPoint(AX1, AY1, gridSize, minEdgeLength).x;
527
+ }
528
+ const x4 = this.getLeftGridIntersectionPoint(AX2, AY2, gridSize, minEdgeLength).x;
529
+ const y3 = this.getTopGridIntersectionPoint(x4, y2, gridSize, minEdgeLength).y;
530
+ return [{ x: x3, y: AY1 }, { x: x3, y: y3 }, { x: x4, y: y3 }, { x: x4, y: AY2 }];
531
+ }
532
+ } else if (y1 > y2) {
533
+ // SCENARIO 2A2: Source below and right of target
534
+ if (AY1 > y2 + targetNode._iHeight) {
535
+ // Simple vertical connection if source anchor is below target node
536
+ // Visual: ←[TGT]
537
+ // |
538
+ // |
539
+ // ↑
540
+ // ←[SRC]
541
+ const x4 = this.getLeftGridIntersectionPoint(AX2, AY2, gridSize, minEdgeLength).x;
542
+ return [{ x: x4, y: AY1 }, { x: x4, y: AY2 }];
543
+ } else if (AY1 <= y2 + targetNode._iHeight) {
544
+ // Need to avoid target node - create stepped path
545
+ // Visual: ←[TGT] ←-----↑
546
+ // |
547
+ // ←[SRC]
548
+ const iDiff = Math.abs(AX1 - (x2 + targetNode._iWidth));
549
+ let x3 = null;
550
+ if (iDiff <= gridSize) {
551
+ x3 = iDiff < AX1 + minEdgeLength ? (AX1 + (x2 + targetNode._iWidth) / 2) : AX1 + minEdgeLength;
552
+ } else {
553
+ x3 = this.getLeftGridIntersectionPoint(AX1, AY1, gridSize, minEdgeLength).x;
554
+ }
555
+ const x4 = this.getLeftGridIntersectionPoint(AX2, AY2, gridSize, minEdgeLength).x;
556
+ const y3 = this.getBottomGridIntersectionPoint(x4, y2 + targetNode._iHeight, gridSize, minEdgeLength).y;
557
+ return [{ x: x3, y: AY1 }, { x: x3, y: y3 }, { x: x4, y: y3 }, { x: x4, y: AY2 }];
558
+ }
559
+ }
560
+ } else if (x1 <= x2) {
561
+ // SCENARIO 2B: Source is to the left of target
562
+ if (y1 < y2) {
563
+ // SCENARIO 2B1: Source above and left of target
564
+ // Visual: ←[SRC]
565
+ // |
566
+ // ↓-------→ |
567
+ // ↓
568
+ // ←[TGT]
569
+ const x3 = this.getLeftGridIntersectionPoint(AX1, AY1, gridSize, minEdgeLength).x;
570
+ const iDiff = Math.abs(x1 + sourceNode._iWidth - x2);
571
+ let x4 = null;
572
+ if (iDiff <= gridSize) {
573
+ x4 = iDiff < minEdgeLength ? (AX2 + (x1 + sourceNode._iWidth)) / 2 : AX2 + minEdgeLength;
574
+ } else {
575
+ x4 = this.getLeftGridIntersectionPoint(AX2, AY2, gridSize, minEdgeLength).x;
576
+ }
577
+ const y3 = (AY2 > (y1 + sourceNode._iHeight)) ? AY2 : this.getBottomGridIntersectionPoint(x3, y1 + sourceNode._iHeight, gridSize, minEdgeLength).y;
578
+ const aCoordinates = AY2 > (y1 + sourceNode._iHeight) ? [{ x: x3, y: AY1 }, { x: x3, y: y3 }] : [{ x: x3, y: AY1 }, { x: x3, y: y3 }, { x: x4, y: y3 }, { x: x4, y: AY2 }];
579
+ return aCoordinates;
580
+ } else if (y1 > y2) {
581
+ // SCENARIO 2B2: Source below and left of target
582
+ // Visual: ←[TGT]
583
+ // ↑
584
+ // ←-------→ |
585
+ // |
586
+ // ←[SRC]
587
+ const x3 = this.getLeftGridIntersectionPoint(AX1, AY2, gridSize, minEdgeLength).x;
588
+ const iDiff = Math.abs(x1 + sourceNode._iWidth - x2);
589
+ let x4 = null;
590
+ if (iDiff <= gridSize) {
591
+ x4 = iDiff < minEdgeLength ? (AX2 + (x1 + sourceNode._iWidth)) / 2 : AX2 + minEdgeLength;
592
+ } else {
593
+ x4 = this.getLeftGridIntersectionPoint(AX2, AY2, gridSize, minEdgeLength).x;
594
+ }
595
+ const y3 = (AY2 < y1) ? AY2 : this.getTopGridIntersectionPoint(x3, y1, gridSize, minEdgeLength).y;
596
+ const aCoordinates = AY2 < y1 ? [{ x: x3, y: AY1 }, { x: x3, y: y3 }] : [{ x: x3, y: AY1 }, { x: x3, y: y3 }, { x: x4, y: y3 }, { x: x4, y: AY2 }];
597
+ return aCoordinates;
598
+ }
599
+ }
600
+ }
601
+ // DEFAULT: No bends needed (direct connection possible)
602
+ return [];
603
+ },
604
+ /**
605
+ * ╔══════════════════════════════════════════════════════════════════════════════╗
606
+ * ║ RIGHT-TO-RIGHT CONNECTION HANDLER ║
607
+ * ╠══════════════════════════════════════════════════════════════════════════════╣
608
+ * ║ ║
609
+ * ║ [SRC NODE]→ ········→ [TGT NODE]→ ║
610
+ * ║ ║
611
+ * ║ Calculates optimal path from right anchor of source to right anchor of ║
612
+ * ║ target, creating C-shaped or stepped connections that route around ║
613
+ * ║ the right side of both nodes to avoid overlaps. ║
614
+ * ║ ║
615
+ * ╚══════════════════════════════════════════════════════════════════════════════╝
616
+ */
617
+ /**
618
+ * Handles RightToRight connection path calculation.
619
+ * Connection flows from the right side of source node to the right side of target node.
620
+ *
621
+ * @param {object} mParams - Parameters for path calculation
622
+ * @returns {Array<object>} Array of bend point coordinates
623
+ * @private
624
+ */
625
+ _handleRightToRightConnection: function (mParams) {
626
+ const { sourceNode, targetNode, gridSize = GRID_SIZE, minEdgeLength = MIN_EDGE_LENGTH } = mParams;
627
+ const { x1, y1, x2, y2, AX1, AY1, AX2, AY2 } = this._transformSourceTargetCoordinates(mParams);
628
+ // CASE 1: Source and target nodes are on the same horizontal line (y1 === y2)
629
+ if (y1 === y2) {
630
+ if (x1 > x2) {
631
+ // SCENARIO 1A: Source to the right of target - create C-shaped path on right side
632
+ // Visual: [TGT]→ [SRC]→
633
+ // | |
634
+ // ↓ |
635
+ // →---------↓
636
+ const x3 = this.getRightGridIntersectionPoint(AX1, AY1, gridSize, minEdgeLength).x;
637
+ const iDiff = Math.abs(x1 - AX2);
638
+ const x4 = iDiff <= gridSize ? (x1 + x2) / 2 : this.getRightGridIntersectionPoint(AX2, AY2, gridSize, minEdgeLength).x;
639
+ const y3 = y1 < gridSize ? this.getBottomGridIntersectionPoint(x3, y1 + sourceNode._iHeight, gridSize, minEdgeLength).y : this.getTopGridIntersectionPoint(x3, y1, gridSize, minEdgeLength).y;
640
+ return [{ x: x3, y: AY1 }, { x: x3, y: y3 }, { x: x4, y: y3 }, { x: x4, y: AY2 }];
641
+ } else if (x1 <= x2) {
642
+ // SCENARIO 1B: Source to the left of target - create C-shaped path on right side
643
+ // Visual: [SRC]→ [TGT]→
644
+ // | |
645
+ // ↓ |
646
+ // →-----------------↓
647
+ const iDff = Math.abs(AX1 - x2);
648
+ const x3 = iDff <= gridSize ? (AX1 + x2) / 2 : this.getRightGridIntersectionPoint(AX1, AY1, gridSize, minEdgeLength).x;
649
+ const x4 = this.getRightGridIntersectionPoint(AX2, AY2, gridSize, minEdgeLength).x;
650
+ const y3 = y1 < gridSize ? this.getBottomGridIntersectionPoint(x3, y1 + sourceNode._iHeight, gridSize, minEdgeLength).y : this.getTopGridIntersectionPoint(x3, y1, gridSize, minEdgeLength).y;
651
+ return [{ x: x3, y: AY1 }, { x: x3, y: y3 }, { x: x4, y: y3 }, { x: x4, y: AY2 }];
652
+ }
653
+ } else if (y1 != y2) {
654
+ // CASE 2: Source and target nodes are on different horizontal lines (y1 != y2)
655
+ if (x1 > x2) {
656
+ // SCENARIO 2A: Source is to the right of target
657
+ if (y1 < y2) {
658
+ // SCENARIO 2A1: Source above and right of target
659
+ // Visual: [SRC]→
660
+ // |
661
+ // ↓--------→ |
662
+ // ↓
663
+ // [TGT]→
664
+ const x3 = this.getRightGridIntersectionPoint(AX1, AY1, gridSize, minEdgeLength).x;
665
+ const iDiff = Math.abs(AX2 - x1);
666
+ let x4 = null;
667
+ if (iDiff <= gridSize) {
668
+ x4 = iDiff < minEdgeLength ? (AX2 + x1) / 2 : AX2 + minEdgeLength;
669
+ } else {
670
+ x4 = this.getRightGridIntersectionPoint(AX2, AY2, gridSize, minEdgeLength).x;
671
+ }
672
+ const y3 = y1 <= gridSize ? this.getBottomGridIntersectionPoint(x3, y1 + sourceNode._iHeight, gridSize, minEdgeLength).y : this.getTopGridIntersectionPoint(x3, y1, gridSize, minEdgeLength).y;
673
+ if (AY2 > y1 + sourceNode._iHeight) {
674
+ // Simple vertical connection if target anchor is below source node
675
+ // Visual: [SRC]→
676
+ // |
677
+ // ↓
678
+ // [TGT]→
679
+ const aCords = AX2 > AX1 ? [{x: x4, y: AY1}, {x: x4, y: AY2}] : [{ x: x3, y: AY1 }, { x: x3, y: AY2 }];
680
+ return aCords;
681
+ } else {
682
+ // Need to avoid target node - create stepped path
683
+ // Visual: [SRC]→
684
+ // |
685
+ // ↓---- →
686
+ // |
687
+ // [TGT]→
688
+ return [{ x: x3, y: AY1 }, { x: x3, y: y3 }, { x: x4, y: y3 }, { x: x4, y: AY2 }];
689
+ }
690
+ } else if (y1 > y2) {
691
+ // SCENARIO 2A2: Source below and right of target
692
+ // Visual: [TGT]→
693
+ // ↑
694
+ // ↑--------→ |
695
+ // |
696
+ // [SRC]→
697
+ const x3 = this.getRightGridIntersectionPoint(AX1, AY1, gridSize, minEdgeLength).x;
698
+ const iDiff = Math.abs(AX2 - x1);
699
+ let x4 = null;
700
+ if (iDiff <= gridSize) {
701
+ x4 = iDiff < minEdgeLength ? (AX2 + x1) / 2 : AX2 + minEdgeLength;
702
+ } else {
703
+ x4 = this.getRightGridIntersectionPoint(AX2, AY2, gridSize, minEdgeLength).x;
704
+ }
705
+ const y3 = y2 <= gridSize ? this.getBottomGridIntersectionPoint(x3, y1 + sourceNode._iHeight, gridSize, minEdgeLength) : this.getTopGridIntersectionPoint(x4, y2, gridSize, minEdgeLength).y;
706
+ if (AY1 > y2 + targetNode._iHeight) {
707
+ // Simple vertical connection if target anchor is below source node
708
+ // Visual: [SRC]→
709
+ // |
710
+ // ↓
711
+ // [TGT]→
712
+ const aCords = AX2 > AX1 ? [{x: x4, y: AY1}, {x: x4, y: AY2}] : [{ x: x3, y: AY1 }, { x: x3, y: AY2 }];
713
+ return aCords;
714
+ } else {
715
+ // Need to avoid target node - create stepped path
716
+ // Visual: [SRC]→
717
+ // |
718
+ // ↓---- →
719
+ // |
720
+ // [TGT]→
721
+ return [{ x: x3, y: AY1 }, { x: x3, y: y3 }, { x: x4, y: y3 }, { x: x4, y: AY2 }];
722
+ }
723
+ }
724
+ } else if (x1 <= x2) {
725
+ // SCENARIO 2B: Source is to the left of target
726
+ if (y1 < y2) {
727
+ // SCENARIO 2B1: Source above and left of target
728
+ // Visual: [SRC]→
729
+ // |
730
+ // ↓----------→ |
731
+ // ↓
732
+ // [TGT]→
733
+ const x3 = this.getRightGridIntersectionPoint(AX1, AY1, gridSize, minEdgeLength).x;
734
+ const iDiff = Math.abs(x2 - AX1);
735
+ let x4 = null;
736
+ if (iDiff <= gridSize && !(AY2 > y1 + sourceNode._iHeight)) {
737
+ x4 = iDiff < minEdgeLength ? (x2 + AX1) / 2 : AX1 - minEdgeLength;
738
+ } else {
739
+ x4 = this.getRightGridIntersectionPoint(AX2, AY2, gridSize, minEdgeLength).x;
740
+ }
741
+ const y3 = y1 <= gridSize ? this.getBottomGridIntersectionPoint(x3, y1 + sourceNode._iHeight, gridSize, minEdgeLength).y : this.getTopGridIntersectionPoint(x3, y1, gridSize, minEdgeLength).y;
742
+ if (AY2 > y1 + sourceNode._iHeight) {
743
+ // Simple vertical connection if target anchor is below source node
744
+ // Visual: [SRC]→
745
+ // |
746
+ // ↓
747
+ // [TGT]→
748
+ const aCords = AX2 > AX1 ? [{x: x4, y: AY1}, {x: x4, y: AY2}] : [{ x: x3, y: AY1 }, { x: x3, y: AY2 }];
749
+ return aCords;
750
+ } else {
751
+ // Need to avoid target node - create stepped path
752
+ // Visual: [SRC]→
753
+ // |
754
+ // ↓---- →
755
+ // |
756
+ // [TGT]→
757
+ return [{ x: x3, y: AY1 }, { x: x3, y: y3 }, { x: x4, y: y3 }, { x: x4, y: AY2 }];
758
+ }
759
+ // const aCoordinates = (AY2 > y1 + sourceNode._iHeight) ? [{ x: x4, y: AY1 }, { x: x4, y: AY2 }] : [{ x: x3, y: AY1 }, { x: x3, y: y3 }, { x: x4, y: y3 }, { x: x4, y: AY2 }];
760
+ // return aCoordinates;
761
+ } else if (y1 > y2) {
762
+ // SCENARIO 2B2: Source below and left of target
763
+ // Visual: [TGT]→
764
+ // ↑
765
+ // ↑----------→ |
766
+ // |
767
+ // [SRC]→
768
+ const x3 = this.getRightGridIntersectionPoint(AX1, AY1, gridSize, minEdgeLength).x;
769
+ const iDiff = Math.abs(x2 - AX1);
770
+ let x4 = null;
771
+ if (iDiff <= gridSize && !(AY1 > y2 + targetNode._iHeight)) {
772
+ x4 = iDiff < minEdgeLength ? (x2 + AX1) / 2 : AX1 - minEdgeLength;
773
+ } else {
774
+ x4 = this.getRightGridIntersectionPoint(AX2, AY2, gridSize, minEdgeLength).x;
775
+ }
776
+ const y3 = y2 <= gridSize ? this.getBottomGridIntersectionPoint(x3, y1 + sourceNode._iHeight, gridSize, minEdgeLength) : this.getTopGridIntersectionPoint(x4, y2, gridSize, minEdgeLength).y;
777
+ if (AY1 > y2 + targetNode._iHeight) {
778
+ // Simple vertical connection if target anchor is below source node
779
+ // Visual: [SRC]→
780
+ // |
781
+ // ↓
782
+ // [TGT]→
783
+ const aCords = AX2 > AX1 ? [{x: x4, y: AY1}, {x: x4, y: AY2}] : [{ x: x3, y: AY1 }, { x: x3, y: AY2 }];
784
+ return aCords;
785
+ } else {
786
+ // Need to avoid target node - create stepped path
787
+ // Visual: [SRC]→
788
+ // |
789
+ // ↓---- →
790
+ // |
791
+ // [TGT]→
792
+ return [{ x: x3, y: AY1 }, { x: x3, y: y3 }, { x: x4, y: y3 }, { x: x4, y: AY2 }];
793
+ }
794
+ // const aCoordinates = (AY1 > y2 + targetNode._iHeight) ? [{ x: x4, y: AY1 }, { x: x4, y: AY2 }] : [{ x: x3, y: AY1 }, { x: x3, y: y3 }, { x: x4, y: y3 }, { x: x4, y: AY2 }];
795
+ // return aCoordinates;
796
+ }
797
+ }
798
+ }
799
+ // DEFAULT: No bends needed (direct connection possible)
800
+ return [];
801
+ },
802
+ /**
803
+ * Calculates the right grid intersection point.
804
+ *
805
+ * @param {number} x - X coordinate
806
+ * @param {number} y - Y coordinate
807
+ * @param {number} [gridSize=50] - Size of grid cells
808
+ * @param {number} [minEdgeLength=20] - Minimum edge length
809
+ * @returns {object} Intersection point {x, y}
810
+ * @public
811
+ */
812
+ getRightGridIntersectionPoint: function (x, y, gridSize = GRID_SIZE, minEdgeLength = MIN_EDGE_LENGTH) {
813
+ let xIntersection = Math.ceil(x / gridSize) * gridSize;
814
+ if (xIntersection - x < minEdgeLength) {
815
+ xIntersection += gridSize; // Ensure the intersection is at least MIN_EDGE_LENGTH away
816
+ }
817
+ return { x: xIntersection, y: y };
818
+ },
819
+ /**
820
+ * Calculates the left grid intersection point.
821
+ *
822
+ * @param {number} x - X coordinate
823
+ * @param {number} y - Y coordinate
824
+ * @param {number} [gridSize=50] - Size of grid cells
825
+ * @param {number} [minEdgeLength=20] - Minimum edge length
826
+ * @returns {object} Intersection point {x, y}
827
+ * @public
828
+ */
829
+ getLeftGridIntersectionPoint: function (x, y, gridSize = GRID_SIZE, minEdgeLength = MIN_EDGE_LENGTH) {
830
+ let xIntersection = Math.floor(x / gridSize) * gridSize;
831
+ if (x - xIntersection < minEdgeLength) {
832
+ xIntersection -= gridSize; // Ensure the intersection is at least MIN_EDGE_LENGTH away
833
+ }
834
+ return { x: xIntersection, y: y };
835
+ },
836
+ /**
837
+ * Calculates the top grid intersection point.
838
+ *
839
+ * @param {number} x - X coordinate
840
+ * @param {number} y - Y coordinate
841
+ * @param {number} [gridSize=50] - Size of grid cells
842
+ * @param {number} [minEdgeLength=20] - Minimum edge length
843
+ * @returns {object} Intersection point {x, y}
844
+ * @public
845
+ */
846
+ getTopGridIntersectionPoint: function (x, y, gridSize = GRID_SIZE, minEdgeLength = MIN_EDGE_LENGTH) {
847
+ let yIntersection = Math.floor(y / gridSize) * gridSize;
848
+ if (y - yIntersection < minEdgeLength) {
849
+ yIntersection -= gridSize; // Ensure the intersection is at least MIN_EDGE_LENGTH away
850
+ }
851
+ return { x: x, y: yIntersection };
852
+ },
853
+ /**
854
+ * Calculates the bottom grid intersection point.
855
+ *
856
+ * @param {number} x - X coordinate
857
+ * @param {number} y - Y coordinate
858
+ * @param {number} [gridSize=50] - Size of grid cells
859
+ * @param {number} [minEdgeLength=20] - Minimum edge length
860
+ * @returns {object} Intersection point {x, y}
861
+ * @public
862
+ */
863
+ getBottomGridIntersectionPoint: function (x, y, gridSize = GRID_SIZE, minEdgeLength = MIN_EDGE_LENGTH) {
864
+ let yIntersection = Math.ceil(y / gridSize) * gridSize;
865
+ if (yIntersection - y < minEdgeLength) {
866
+ yIntersection += gridSize; // Ensure the intersection is at least MIN_EDGE_LENGTH away
867
+ }
868
+ return { x: x, y: yIntersection };
869
+ },
870
+ /**
871
+ * Gets the anchor coordinates for a node based on connection type and anchor type.
872
+ * Enhanced to handle center positioning for single connections and offset positioning for mixed connections.
873
+ *
874
+ * @param {object} oNode - The node object
875
+ * @param {string} sConnectionType - Connection type
876
+ * @param {string} sAnchorType - Anchor type ("source" or "target")
877
+ * @returns {object} Anchor coordinates {x, y}
878
+ * @private
879
+ */
880
+ _getNodeAnchorCoordinates: function (oNode, sConnectionType, sAnchorType) {
881
+ const x = oNode.getX();
882
+ const y = oNode.getY();
883
+ const width = oNode._iWidth;
884
+ const height = oNode._iHeight;
885
+ const centerY = y + (height / 2);
886
+
887
+ // Enhanced anchor calculation with connection pattern analysis
888
+ const oConnectionAnalysis = this._analyzeNodeConnectionPatterns(oNode);
889
+ // Determine the port position based on the connection type and analysis
890
+ switch (sConnectionType) {
891
+ case "LeftToLeft":
892
+ if (sAnchorType === "source") {
893
+ // Source anchor on left side
894
+ return this._calculateEnhancedAnchor({
895
+ node: { x, y, width, height, centerY },
896
+ side: "left",
897
+ analysis: oConnectionAnalysis,
898
+ anchorType: sAnchorType
899
+ });
900
+ } else {
901
+ // Target anchor on left side
902
+ return this._calculateEnhancedAnchor({
903
+ node: { x, y, width, height, centerY },
904
+ side: "left",
905
+ analysis: oConnectionAnalysis,
906
+ anchorType: sAnchorType
907
+ });
908
+ }
909
+ case "LeftToRight":
910
+ if (sAnchorType === "source") {
911
+ // Source anchor on left side
912
+ return this._calculateEnhancedAnchor({
913
+ node: { x, y, width, height, centerY },
914
+ side: "left",
915
+ analysis: oConnectionAnalysis,
916
+ anchorType: sAnchorType
917
+ });
918
+ } else {
919
+ // Target anchor on right side
920
+ return this._calculateEnhancedAnchor({
921
+ node: { x, y, width, height, centerY },
922
+ side: "right",
923
+ analysis: oConnectionAnalysis,
924
+ anchorType: sAnchorType
925
+ });
926
+ }
927
+ case "RightToLeft":
928
+ if (sAnchorType === "source") {
929
+ // Source anchor on right side
930
+ return this._calculateEnhancedAnchor({
931
+ node: { x, y, width, height, centerY },
932
+ side: "right",
933
+ analysis: oConnectionAnalysis,
934
+ anchorType: sAnchorType
935
+ });
936
+ } else {
937
+ // Target anchor on left side
938
+ return this._calculateEnhancedAnchor({
939
+ node: { x, y, width, height, centerY },
940
+ side: "left",
941
+ analysis: oConnectionAnalysis,
942
+ anchorType: sAnchorType
943
+ });
944
+ }
945
+ case "RightToRight":
946
+ if (sAnchorType === "source") {
947
+ // Source anchor on right side
948
+ return this._calculateEnhancedAnchor({
949
+ node: { x, y, width, height, centerY },
950
+ side: "right",
951
+ analysis: oConnectionAnalysis,
952
+ anchorType: sAnchorType
953
+ });
954
+ } else {
955
+ // Target anchor on right side
956
+ return this._calculateEnhancedAnchor({
957
+ node: { x, y, width, height, centerY },
958
+ side: "right",
959
+ analysis: oConnectionAnalysis,
960
+ anchorType: sAnchorType
961
+ });
962
+ }
963
+ default:
964
+ // Default RightToLeft case
965
+ if (sAnchorType === "source") {
966
+ return this._calculateEnhancedAnchor({
967
+ node: { x, y, width, height, centerY },
968
+ side: "right",
969
+ analysis: oConnectionAnalysis,
970
+ anchorType: sAnchorType
971
+ });
972
+ } else {
973
+ return this._calculateEnhancedAnchor({
974
+ node: { x, y, width, height, centerY },
975
+ side: "left",
976
+ analysis: oConnectionAnalysis,
977
+ anchorType: sAnchorType
978
+ });
979
+ }
980
+ }
981
+ },
982
+ /**
983
+ * Transforms source and target coordinates into a normalized format for calculations.
984
+ *
985
+ * @param {object} mParams - Parameters containing node and anchor information
986
+ * @returns {object} Transformed coordinates object
987
+ * @private
988
+ */
989
+ _transformSourceTargetCoordinates: function (mParams) {
990
+ return {
991
+ x1: mParams.sourceNode.getX(), // x coordinate of the source node
992
+ y1: mParams.sourceNode.getY(), // y coordinate of the source node
993
+ x2: mParams.targetNode.getX(), // x cordinate of the target node
994
+ y2: mParams.targetNode.getY(), // y coordinate of the target node
995
+ AX1: mParams.sourceAnchor.x, // x coordinate of the source anchor
996
+ AY1: mParams.sourceAnchor.y, // y coordinate of the source anchor
997
+ AX2: mParams.targetAnchor.x, // x coordinate of the target anchor
998
+ AY2: mParams.targetAnchor.y, // y coordinate of the target anchor
999
+ connectionType: mParams.connectionType, // type of the connection
1000
+ dx: Math.abs(mParams.targetNode.getX() - mParams.sourceNode.getX()), // horizontal distance between source and target nodes
1001
+ dy: Math.abs(mParams.targetNode.getY() - mParams.sourceNode.getY()) // vertical distance between source and target nodes
1002
+ };
1003
+ },
1004
+ /**
1005
+ * Analyzes the connection patterns for a node to determine optimal anchor placement
1006
+ * @param {object} oNode - The node to analyze
1007
+ * @returns {object} Analysis object with connection counts per side
1008
+ * @private
1009
+ */
1010
+ _analyzeNodeConnectionPatterns: function (oNode) {
1011
+ if (!oNode || !oNode.getParent || !oNode.getParent()) {
1012
+ return { leftIncoming: 0, leftOutgoing: 0, rightIncoming: 0, rightOutgoing: 0 };
1013
+ }
1014
+
1015
+ const oGraph = oNode.getParent();
1016
+ const aLines = oGraph.getLines();
1017
+ const sNodeKey = oNode.getKey();
1018
+
1019
+ let leftIncoming = 0, leftOutgoing = 0, rightIncoming = 0, rightOutgoing = 0;
1020
+
1021
+ aLines.forEach(function(oLine) {
1022
+ if (!oLine.getVisible() || oLine._isIgnored()) {
1023
+ return;
1024
+ }
1025
+
1026
+ const sFromKey = oLine.getFrom();
1027
+ const sToKey = oLine.getTo();
1028
+ const sLineConnectionType = oLine.getConnectionType();
1029
+
1030
+ // Check if this line involves our node
1031
+ if (sFromKey === sNodeKey) {
1032
+ // Outgoing connection from our node
1033
+ switch (sLineConnectionType) {
1034
+ case "LeftToLeft":
1035
+ case "LeftToRight":
1036
+ leftOutgoing++;
1037
+ break;
1038
+ case "RightToLeft":
1039
+ case "RightToRight":
1040
+ rightOutgoing++;
1041
+ break;
1042
+ default:
1043
+ // Default to right for unrecognized connection types
1044
+ rightOutgoing++;
1045
+ break;
1046
+ }
1047
+ } else if (sToKey === sNodeKey) {
1048
+ // Incoming connection to our node
1049
+ switch (sLineConnectionType) {
1050
+ case "LeftToLeft":
1051
+ case "RightToLeft":
1052
+ leftIncoming++;
1053
+ break;
1054
+ case "LeftToRight":
1055
+ case "RightToRight":
1056
+ rightIncoming++;
1057
+ break;
1058
+ default:
1059
+ // Default to left for unrecognized connection types
1060
+ leftIncoming++;
1061
+ break;
1062
+ }
1063
+ }
1064
+ });
1065
+
1066
+ return {
1067
+ leftIncoming: leftIncoming,
1068
+ leftOutgoing: leftOutgoing,
1069
+ rightIncoming: rightIncoming,
1070
+ rightOutgoing: rightOutgoing
1071
+ };
1072
+ },
1073
+
1074
+ /**
1075
+ * Calculates enhanced anchor position based on connection analysis
1076
+ * @param {object} mParams - Parameters for anchor calculation
1077
+ * @param {object} mParams.node - Node position and size info
1078
+ * @param {string} mParams.side - Side of the node ("left" or "right")
1079
+ * @param {object} mParams.analysis - Connection pattern analysis
1080
+ * @param {string} mParams.anchorType - Anchor type ("source" or "target")
1081
+ * @returns {object} Anchor coordinates {x, y}
1082
+ * @private
1083
+ */
1084
+ _calculateEnhancedAnchor: function (mParams) {
1085
+ const { node, side, analysis, anchorType } = mParams;
1086
+ const { x, y, width, height, centerY } = node;
1087
+
1088
+ const offsetDistance = height * 0.15; // 15% of node height for offset positioning
1089
+
1090
+ // Determine base X coordinate for the side
1091
+ const baseX = side === "left" ? x : (x + width);
1092
+
1093
+ // Get connection counts for this side
1094
+ const sideIncoming = side === "left" ? analysis.leftIncoming : analysis.rightIncoming;
1095
+ const sideOutgoing = side === "left" ? analysis.leftOutgoing : analysis.rightOutgoing;
1096
+
1097
+ // Calculate Y position based on connection patterns
1098
+ let anchorY = centerY; // Default to center
1099
+
1100
+ // ENHANCEMENT 1: Single connection type on this side -> use center
1101
+ if ((sideIncoming === 0 && sideOutgoing === 1) || (sideIncoming === 1 && sideOutgoing === 0)) {
1102
+ anchorY = centerY; // Center position for single connection
1103
+ } else if (sideIncoming > 0 && sideOutgoing > 0) {
1104
+ // ENHANCEMENT 2: Mixed connections on this side -> offset positioning
1105
+ if (anchorType === "target") {
1106
+ // Incoming connections (targets) go above center
1107
+ anchorY = centerY - offsetDistance;
1108
+ } else {
1109
+ // Outgoing connections (sources) go below center
1110
+ anchorY = centerY + offsetDistance;
1111
+ }
1112
+ } else {
1113
+ // Multiple connections of same type -> use center (for now)
1114
+ anchorY = centerY;
1115
+ }
1116
+
1117
+ // Ensure anchor stays within node boundaries
1118
+ const minY = y + (height * 0.1); // 10% margin from top
1119
+ const maxY = y + (height * 0.9); // 10% margin from bottom
1120
+ anchorY = Math.max(minY, Math.min(maxY, anchorY));
1121
+
1122
+ return {
1123
+ x: baseX,
1124
+ y: Math.floor(anchorY)
1125
+ };
1126
+ },
1127
+
1128
+ /**
1129
+ * Determines if a horizontal connection should use a straight line based on connection patterns.
1130
+ * Returns true only when both nodes have single dedicated connections (source has single outgoing, target has single incoming).
1131
+ *
1132
+ * @param {object} sourceNode - The source node
1133
+ * @param {object} targetNode - The target node
1134
+ * @param {string} connectionType - The connection type
1135
+ * @returns {boolean} True if straight line should be used, false if bends are needed
1136
+ * @private
1137
+ */
1138
+ _shouldUseHorizontalStraightLine: function (sourceNode, targetNode, connectionType) {
1139
+ const sourceAnalysis = this._analyzeNodeConnectionPatterns(sourceNode);
1140
+ const targetAnalysis = this._analyzeNodeConnectionPatterns(targetNode);
1141
+
1142
+ // Determine which sides are involved based on connection type
1143
+ let sourceOutgoingCount, targetIncomingCount;
1144
+
1145
+ switch (connectionType) {
1146
+ case "RightToLeft":
1147
+ sourceOutgoingCount = sourceAnalysis.rightOutgoing;
1148
+ targetIncomingCount = targetAnalysis.leftIncoming;
1149
+ break;
1150
+ case "LeftToRight":
1151
+ sourceOutgoingCount = sourceAnalysis.leftOutgoing;
1152
+ targetIncomingCount = targetAnalysis.rightIncoming;
1153
+ break;
1154
+ case "LeftToLeft":
1155
+ sourceOutgoingCount = sourceAnalysis.leftOutgoing;
1156
+ targetIncomingCount = targetAnalysis.leftIncoming;
1157
+ break;
1158
+ case "RightToRight":
1159
+ sourceOutgoingCount = sourceAnalysis.rightOutgoing;
1160
+ targetIncomingCount = targetAnalysis.rightIncoming;
1161
+ break;
1162
+ default:
1163
+ // Default to RightToLeft
1164
+ sourceOutgoingCount = sourceAnalysis.rightOutgoing;
1165
+ targetIncomingCount = targetAnalysis.leftIncoming;
1166
+ break;
1167
+ }
1168
+
1169
+ // Allow straight line only when both nodes have single dedicated connections
1170
+ return (sourceOutgoingCount === 1 && targetIncomingCount === 1);
1171
+ }
1172
+ };
1173
+ return ConnectionPathUtils;
1174
+ });