@sapui5/sap.suite.ui.commons 1.136.12 → 1.136.15

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 (117) 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 +39 -5
  31. package/src/sap/suite/ui/commons/messagebundle_bg.properties +39 -5
  32. package/src/sap/suite/ui/commons/messagebundle_ca.properties +39 -5
  33. package/src/sap/suite/ui/commons/messagebundle_cnr.properties +39 -5
  34. package/src/sap/suite/ui/commons/messagebundle_cs.properties +38 -4
  35. package/src/sap/suite/ui/commons/messagebundle_cy.properties +39 -5
  36. package/src/sap/suite/ui/commons/messagebundle_da.properties +38 -4
  37. package/src/sap/suite/ui/commons/messagebundle_de.properties +39 -5
  38. package/src/sap/suite/ui/commons/messagebundle_el.properties +39 -5
  39. package/src/sap/suite/ui/commons/messagebundle_en.properties +39 -5
  40. package/src/sap/suite/ui/commons/messagebundle_en_GB.properties +39 -5
  41. package/src/sap/suite/ui/commons/messagebundle_en_US_sappsd.properties +99 -28
  42. package/src/sap/suite/ui/commons/messagebundle_en_US_saprigi.properties +39 -5
  43. package/src/sap/suite/ui/commons/messagebundle_en_US_saptrc.properties +600 -537
  44. package/src/sap/suite/ui/commons/messagebundle_es.properties +39 -5
  45. package/src/sap/suite/ui/commons/messagebundle_es_MX.properties +38 -4
  46. package/src/sap/suite/ui/commons/messagebundle_et.properties +38 -4
  47. package/src/sap/suite/ui/commons/messagebundle_fi.properties +38 -4
  48. package/src/sap/suite/ui/commons/messagebundle_fr.properties +39 -5
  49. package/src/sap/suite/ui/commons/messagebundle_fr_CA.properties +40 -6
  50. package/src/sap/suite/ui/commons/messagebundle_hi.properties +39 -5
  51. package/src/sap/suite/ui/commons/messagebundle_hr.properties +39 -5
  52. package/src/sap/suite/ui/commons/messagebundle_hu.properties +39 -5
  53. package/src/sap/suite/ui/commons/messagebundle_id.properties +39 -5
  54. package/src/sap/suite/ui/commons/messagebundle_it.properties +44 -10
  55. package/src/sap/suite/ui/commons/messagebundle_iw.properties +39 -5
  56. package/src/sap/suite/ui/commons/messagebundle_ja.properties +39 -5
  57. package/src/sap/suite/ui/commons/messagebundle_kk.properties +38 -4
  58. package/src/sap/suite/ui/commons/messagebundle_ko.properties +38 -4
  59. package/src/sap/suite/ui/commons/messagebundle_lt.properties +39 -5
  60. package/src/sap/suite/ui/commons/messagebundle_lv.properties +39 -5
  61. package/src/sap/suite/ui/commons/messagebundle_mk.properties +39 -5
  62. package/src/sap/suite/ui/commons/messagebundle_ms.properties +39 -5
  63. package/src/sap/suite/ui/commons/messagebundle_nl.properties +38 -4
  64. package/src/sap/suite/ui/commons/messagebundle_no.properties +39 -5
  65. package/src/sap/suite/ui/commons/messagebundle_pl.properties +39 -5
  66. package/src/sap/suite/ui/commons/messagebundle_pt.properties +39 -5
  67. package/src/sap/suite/ui/commons/messagebundle_pt_PT.properties +39 -5
  68. package/src/sap/suite/ui/commons/messagebundle_ro.properties +39 -5
  69. package/src/sap/suite/ui/commons/messagebundle_ru.properties +39 -5
  70. package/src/sap/suite/ui/commons/messagebundle_sh.properties +39 -5
  71. package/src/sap/suite/ui/commons/messagebundle_sk.properties +39 -5
  72. package/src/sap/suite/ui/commons/messagebundle_sl.properties +38 -4
  73. package/src/sap/suite/ui/commons/messagebundle_sr.properties +39 -5
  74. package/src/sap/suite/ui/commons/messagebundle_sv.properties +38 -4
  75. package/src/sap/suite/ui/commons/messagebundle_th.properties +39 -5
  76. package/src/sap/suite/ui/commons/messagebundle_tr.properties +39 -5
  77. package/src/sap/suite/ui/commons/messagebundle_uk.properties +38 -4
  78. package/src/sap/suite/ui/commons/messagebundle_vi.properties +39 -5
  79. package/src/sap/suite/ui/commons/messagebundle_zh_CN.properties +39 -5
  80. package/src/sap/suite/ui/commons/messagebundle_zh_TW.properties +39 -5
  81. package/src/sap/suite/ui/commons/networkgraph/ElementBase.js +19 -1
  82. package/src/sap/suite/ui/commons/networkgraph/Graph.js +562 -45
  83. package/src/sap/suite/ui/commons/networkgraph/GraphMap.js +25 -3
  84. package/src/sap/suite/ui/commons/networkgraph/GraphRenderer.js +19 -8
  85. package/src/sap/suite/ui/commons/networkgraph/KeyboardNavigator.js +367 -12
  86. package/src/sap/suite/ui/commons/networkgraph/Line.js +814 -22
  87. package/src/sap/suite/ui/commons/networkgraph/Node.js +573 -79
  88. package/src/sap/suite/ui/commons/networkgraph/Tooltip.js +4 -0
  89. package/src/sap/suite/ui/commons/networkgraph/Utils.js +249 -10
  90. package/src/sap/suite/ui/commons/networkgraph/layout/NoopLayout.js +77 -7
  91. package/src/sap/suite/ui/commons/networkgraph/util/ConnectionPathUtils.js +1420 -0
  92. package/src/sap/suite/ui/commons/networkgraph/util/CreateConnectionPopover.js +374 -0
  93. package/src/sap/suite/ui/commons/networkgraph/util/DependencyLayoutHelper.js +1032 -0
  94. package/src/sap/suite/ui/commons/networkgraph/util/DragDropManager.js +721 -0
  95. package/src/sap/suite/ui/commons/networkgraph/util/PortManager.js +582 -0
  96. package/src/sap/suite/ui/commons/statusindicator/Circle.js +1 -1
  97. package/src/sap/suite/ui/commons/statusindicator/CustomShape.js +1 -1
  98. package/src/sap/suite/ui/commons/statusindicator/DiscreteThreshold.js +1 -1
  99. package/src/sap/suite/ui/commons/statusindicator/FillingOption.js +1 -1
  100. package/src/sap/suite/ui/commons/statusindicator/LibraryShape.js +1 -1
  101. package/src/sap/suite/ui/commons/statusindicator/Path.js +1 -1
  102. package/src/sap/suite/ui/commons/statusindicator/PropertyThreshold.js +1 -1
  103. package/src/sap/suite/ui/commons/statusindicator/Rectangle.js +1 -1
  104. package/src/sap/suite/ui/commons/statusindicator/Shape.js +1 -1
  105. package/src/sap/suite/ui/commons/statusindicator/ShapeGroup.js +1 -1
  106. package/src/sap/suite/ui/commons/statusindicator/SimpleShape.js +1 -1
  107. package/src/sap/suite/ui/commons/statusindicator/StatusIndicator.js +1 -1
  108. package/src/sap/suite/ui/commons/taccount/TAccount.js +1 -1
  109. package/src/sap/suite/ui/commons/taccount/TAccountGroup.js +1 -1
  110. package/src/sap/suite/ui/commons/taccount/TAccountItem.js +1 -1
  111. package/src/sap/suite/ui/commons/taccount/TAccountItemProperty.js +1 -1
  112. package/src/sap/suite/ui/commons/taccount/TAccountPanel.js +1 -1
  113. package/src/sap/suite/ui/commons/themes/base/NetworkGraph.less +26 -13
  114. package/src/sap/suite/ui/commons/themes/base/NetworkGroup.less +4 -0
  115. package/src/sap/suite/ui/commons/themes/base/NetworkLine.less +58 -13
  116. package/src/sap/suite/ui/commons/themes/base/NetworkNode.less +251 -47
  117. package/src/sap/suite/ui/commons/themes/base/SemanticColorMixins.less +55 -0
@@ -0,0 +1,1420 @@
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
+ const bEnhanced = oGraph.getEnableEnhancedLineRouting ? oGraph.getEnableEnhancedLineRouting() : false;
71
+
72
+ try {
73
+ if (bEnhanced) {
74
+ this._routingRowCache = this._buildRoutingRowCache(oGraph.getNodes(), config.gridSize);
75
+
76
+ // Precompute per-node connection counts once to avoid rescanning all lines on each call.
77
+ // Keys are normalized to strings on both write and read — node keys may be numeric
78
+ // while Line#getFrom/getTo return them as-is, and Map.get() uses strict key identity.
79
+ this._analysisCache = new Map();
80
+ oGraph.getNodes().forEach(function(oNode) {
81
+ this._analysisCache.set(String(oNode.getKey()), { leftIncoming: 0, leftOutgoing: 0, rightIncoming: 0, rightOutgoing: 0 });
82
+ }, this);
83
+ oGraph.getLines().forEach(function(oLine) {
84
+ if (!oLine.getVisible() || oLine._isIgnored()) { return; }
85
+ var sFrom = oLine.getFrom();
86
+ var sTo = oLine.getTo();
87
+ var sType = oLine.getConnectionType();
88
+ var oFrom = this._analysisCache.get(String(sFrom));
89
+ var oTo = this._analysisCache.get(String(sTo));
90
+ if (oFrom) {
91
+ switch (sType) {
92
+ case "LeftToLeft": case "LeftToRight": oFrom.leftOutgoing++; break;
93
+ default: oFrom.rightOutgoing++; break;
94
+ }
95
+ }
96
+ if (oTo) {
97
+ switch (sType) {
98
+ case "LeftToLeft": case "RightToLeft": oTo.leftIncoming++; break;
99
+ default: oTo.rightIncoming++; break;
100
+ }
101
+ }
102
+ }, this);
103
+ }
104
+ // Iterate over all lines in the graph
105
+ oGraph.getLines().forEach((oLine) => {
106
+ 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.
107
+ // In RTL mode, use effective connection type (semantically reversed)
108
+ const effectiveConnectionType = this._getEffectiveConnectionType(configuredConnectionType, bIsRTL);
109
+ const oSourceNode = oLine.getFromNode();
110
+ const oTargetNode = oLine.getToNode();
111
+ // Get anchor coordinates for source and target nodes, usually the center of the respective sides
112
+ const oSourceAnchor = this._getNodeAnchorCoordinates(oLine.getFromNode(), effectiveConnectionType, "source");
113
+ const oTargetAnchor = this._getNodeAnchorCoordinates(oLine.getToNode(), effectiveConnectionType, "target");
114
+
115
+ const mParams = {
116
+ sourceAnchor: oSourceAnchor,
117
+ targetAnchor: oTargetAnchor,
118
+ sourceNode: oSourceNode,
119
+ targetNode: oTargetNode,
120
+ sConnectionType: effectiveConnectionType,
121
+ gridSize: config.gridSize,
122
+ minEdgeLength: config.minEdgeLength,
123
+ bEnhanced: bEnhanced
124
+ };
125
+ // Calculate the path coordinates between the source and target anchors
126
+ const aCoordinates = this._calculatePathCoordinates(mParams);
127
+ // set the anchor points of the source and target nodes on the line as source and target for drawing the path.
128
+ oLine.setSource({
129
+ x: oSourceAnchor.x,
130
+ y: oSourceAnchor.y
131
+ });
132
+ oLine.setTarget({
133
+ x: oTargetAnchor.x,
134
+ y: oTargetAnchor.y
135
+ });
136
+ // Set the calculated bend points on the line
137
+ oLine.clearBends();
138
+ // Set the calculated bend points on the line, determined by the connection type.
139
+ if (aCoordinates && aCoordinates.length) {
140
+ aCoordinates.forEach((oBend) => {
141
+ oLine.addBend(oBend);
142
+ });
143
+ }
144
+ });
145
+ } finally {
146
+ this._routingRowCache = null;
147
+ this._analysisCache = null;
148
+ }
149
+
150
+ if (bEnhanced) {
151
+ this._resolveLineNodeCollisions(oGraph.getLines(), oGraph.getNodes(), config.gridSize);
152
+ }
153
+ },
154
+ /**
155
+ * Calculates path coordinates between source and target anchors based on connection type.
156
+ *
157
+ * @param {object} mParams - Parameters for path calculation
158
+ * @param {object} mParams.sourceAnchor - Source anchor coordinates {x, y}
159
+ * @param {object} mParams.targetAnchor - Target anchor coordinates {x, y}
160
+ * @param {object} mParams.sourceNode - Source node object
161
+ * @param {object} mParams.targetNode - Target node object
162
+ * @param {string} mParams.sConnectionType - Connection type
163
+ * @param {number} [mParams.gridSize=50] - Grid size for calculations
164
+ * @param {number} [mParams.minEdgeLength=20] - Minimum edge length
165
+ * @returns {Array<object>} Array of bend point coordinates
166
+ * @private
167
+ */
168
+ _calculatePathCoordinates: function (mParams) {
169
+ const { sConnectionType } = mParams;
170
+ switch (sConnectionType) {
171
+ case ConnectionType.RightToLeft:
172
+ return this._handleRightToLeftConnection(mParams);
173
+ case ConnectionType.LeftToRight:
174
+ return this._handleLeftToRightConnection(mParams);
175
+ case ConnectionType.LeftToLeft:
176
+ return this._handleLeftToLeftConnection(mParams);
177
+ case ConnectionType.RightToRight:
178
+ return this._handleRightToRightConnection(mParams);
179
+ default:
180
+ // default RightToLeft case
181
+ // This is a fallback for any unrecognized connection type
182
+ return this._handleRightToLeftConnection(mParams);
183
+ }
184
+ },
185
+ /**
186
+ * ╔══════════════════════════════════════════════════════════════════════════════╗
187
+ * ║ RIGHT-TO-LEFT CONNECTION HANDLER ║
188
+ * ╠══════════════════════════════════════════════════════════════════════════════╣
189
+ * ║ ║
190
+ * ║ [SRC NODE]→ ········→ ←[TGT NODE] ║
191
+ * ║ ║
192
+ * ║ Calculates optimal path from right anchor of source to left anchor of ║
193
+ * ║ target, creating L-shaped, U-shaped, Z-bend, or S-shaped connections ║
194
+ * ║ based on node positions and grid constraints. ║
195
+ * ║ ║
196
+ * ╚══════════════════════════════════════════════════════════════════════════════╝
197
+ */
198
+ /**
199
+ * Handles RightToLeft connection path calculation.
200
+ * Connection flows from the right side of source node to the left side of target node.
201
+ *
202
+ * @param {object} mParams - Parameters for path calculation
203
+ * @returns {Array<object>} Array of bend point coordinates
204
+ * @private
205
+ */
206
+ _handleRightToLeftConnection: function (mParams) {
207
+ const { sourceNode, gridSize = GRID_SIZE, minEdgeLength = MIN_EDGE_LENGTH, bEnhanced } = mParams;
208
+ const { x1, y1, x2, y2, AX1, AY1, AX2, AY2, dx } = this._transformSourceTargetCoordinates(mParams);
209
+
210
+ // CASE 1: Source and target nodes are on the same horizontal line (y1 === y2)
211
+ if (y1 === y2) {
212
+ if (x2 > x1) {
213
+ // SCENARIO 1A: Direct horizontal connection (source right → target left)
214
+ // Check if anchors are aligned and connection pattern allows straight line
215
+ const anchorVerticalOffset = Math.abs(AY1 - AY2);
216
+ const shouldUseStraightLine = this._shouldUseHorizontalStraightLine(mParams.sourceNode, mParams.targetNode, mParams.sConnectionType);
217
+
218
+ if (shouldUseStraightLine && anchorVerticalOffset <= 5) {
219
+ // Visual: [SRC]----→[TGT] (perfect alignment)
220
+ return [];
221
+ } else if (anchorVerticalOffset > 5) {
222
+ // Offset anchors due to different node heights in the same row.
223
+ // For skip-layer (intermediate nodes exist), route using corridor to avoid
224
+ // the vertical Z-bend passing through intermediate nodes.
225
+ if (bEnhanced) {
226
+ const iNodeWidthOff = sourceNode._iWidth || 0;
227
+ const iHorizontalGapOff = x2 - (x1 + iNodeWidthOff);
228
+ if (iHorizontalGapOff > Math.max(iNodeWidthOff, gridSize)) {
229
+ const xDrop = this.getRightGridIntersectionPoint(x1 + iNodeWidthOff, AY1, gridSize, minEdgeLength).x;
230
+ const xRise = this.getLeftGridIntersectionPoint(AX2, AY2, gridSize, minEdgeLength).x;
231
+ const y3 = this._getRoutingYBelow(sourceNode, gridSize);
232
+ return [{ x: xDrop, y: AY1 }, { x: xDrop, y: y3 }, { x: xRise, y: y3 }, { x: xRise, y: AY2 }];
233
+ }
234
+ }
235
+ // Adjacent nodes — simple Z-bend through the gap between them
236
+ const midX = AX1 + (AX2 - AX1) / 2;
237
+ return [
238
+ { x: midX, y: AY1 },
239
+ { x: midX, y: AY2 }
240
+ ];
241
+ } else {
242
+ // Multiple connections from same side.
243
+ if (bEnhanced) {
244
+ // If the gap is large enough to suggest intermediate nodes, route below
245
+ // to avoid the line passing through them visually.
246
+ const iNodeWidth = sourceNode._iWidth || 0;
247
+ const iHorizontalGap = x2 - (x1 + iNodeWidth);
248
+ if (iHorizontalGap > Math.max(iNodeWidth, gridSize)) {
249
+ const xDrop = this.getRightGridIntersectionPoint(x1 + iNodeWidth, AY1, gridSize, minEdgeLength).x;
250
+ const xRise = this.getLeftGridIntersectionPoint(AX2, AY2, gridSize, minEdgeLength).x;
251
+ const y3 = this._getRoutingYBelow(sourceNode, gridSize);
252
+ return [{ x: xDrop, y: AY1 }, { x: xDrop, y: y3 }, { x: xRise, y: y3 }, { x: xRise, y: AY2 }];
253
+ }
254
+ }
255
+ return [];
256
+ }
257
+ } else if (x1 >= x2) {
258
+ // SCENARIO 1B: Backward horizontal connection (source overlaps/behind target)
259
+ // Visual: [TGT] [SRC]
260
+ // ↑----→ |
261
+ // | |
262
+ // ←------↓
263
+ // Create U-shaped path going around both nodes
264
+ const x3 = this.getRightGridIntersectionPoint(AX1, AY1, gridSize, minEdgeLength).x;
265
+ const x4 = this.getLeftGridIntersectionPoint(AX2, AY2, gridSize, minEdgeLength).x;
266
+ const y3 = bEnhanced ? this._getRoutingYBelow(sourceNode, gridSize) : this.getBottomGridIntersectionPoint(x3, y1 + sourceNode._iHeight, gridSize, minEdgeLength).y;
267
+
268
+ return [
269
+ { x: x3, y: AY1 }, // Right from source
270
+ { x: x3, y: y3 }, // Down below both nodes
271
+ { x: x4, y: y3 }, // Left to target column
272
+ { x: x4, y: AY2 } // Up to target
273
+ ];
274
+ }
275
+ } else if (y1 != y2) {
276
+ if (x1 <= x2) {
277
+ // SCENARIO 2A: Source is to the left or aligned with target
278
+ if (dx >= minEdgeLength + sourceNode._iWidth) {
279
+ if (Math.abs(x1 + sourceNode._iWidth - x2) <= gridSize) {
280
+ // SCENARIO 2A1: Nodes are close - use midpoint connection
281
+ // Visual: [SRC]→ ←[TGT]
282
+ // | |
283
+ // ↓ ↑
284
+ // (midpoint)
285
+ const x3 = AX1 + (x2 - (x1 + sourceNode._iWidth)) / 2;
286
+ return [{ x: x3, y: AY1 }, { x: x3, y: AY2 }];
287
+ } else {
288
+ // SCENARIO 2A2: Nodes are far apart
289
+ // If the horizontal gap is larger than one node width, intermediate nodes
290
+ // likely occupy the space — route the vertical segments at source/target
291
+ // column boundaries to avoid passing through them.
292
+ const iGap = x2 - (x1 + sourceNode._iWidth);
293
+ if (bEnhanced && iGap > sourceNode._iWidth) {
294
+ // SCENARIO 2A2-SKIP: Skip-layer connection.
295
+ // First go right to the nearest grid point (so the line visually exits the
296
+ // source from the center-right side), then drop/rise outside intermediate
297
+ // columns, then go horizontally to the target.
298
+ // Visual (y1 ≤ y2): [SRC]→——| Visual (y1 > y2): [SRC]→——|
299
+ // | ↑
300
+ // ↓————————→ | ←———————————|
301
+ // ↑
302
+ // ←[TGT]
303
+ const xDrop = this.getRightGridIntersectionPoint(x1 + sourceNode._iWidth, AY1, gridSize, minEdgeLength).x;
304
+ const xRise = this.getLeftGridIntersectionPoint(AX2, AY2, gridSize, minEdgeLength).x;
305
+ let y3;
306
+ if (y1 <= y2) {
307
+ y3 = this._getRoutingYBelow(sourceNode, gridSize);
308
+ } else {
309
+ y3 = this.getTopGridIntersectionPoint(xDrop, Math.min(y1, y2), gridSize, minEdgeLength).y;
310
+ if (y3 <= 0) {
311
+ y3 = this._getRoutingYBelow(sourceNode, gridSize);
312
+ }
313
+ }
314
+ return [{ x: xDrop, y: AY1 }, { x: xDrop, y: y3 }, { x: xRise, y: y3 }, { x: xRise, y: AY2 }];
315
+ } else {
316
+ // SCENARIO 2A2: Adjacent layers — grid-aligned Z-bend
317
+ // Visual: [SRC]→ |
318
+ // |
319
+ // ↓
320
+ // ←[TGT]
321
+ const x3 = this.getRightGridIntersectionPoint(x1 + sourceNode._iWidth, AY1, gridSize, minEdgeLength).x;
322
+ return [{ x: x3, y: AY1 }, { x: x3, y: AY2 }];
323
+ }
324
+ }
325
+ } else if (dx <= minEdgeLength + sourceNode._iWidth) {
326
+ // SCENARIO 2B: Nodes are too close - create S-shaped path
327
+ if (y1 < y2) {
328
+ // SCENARIO 2B1: Source above target - S-bend going down
329
+ // Visual: [SRC]→ |
330
+ // |
331
+ // ↓----→ |
332
+ // |
333
+ // ↓
334
+ // ←[TGT]
335
+ const x3 = this.getRightGridIntersectionPoint(x1 + sourceNode._iWidth, y1, gridSize, minEdgeLength).x;
336
+ const x4 = this.getLeftGridIntersectionPoint(x2, AY2, gridSize, minEdgeLength).x;
337
+ const y3 = bEnhanced ? this._getRoutingYBelow(sourceNode, gridSize) : this.getBottomGridIntersectionPoint(x3, y1 + sourceNode._iHeight, gridSize, minEdgeLength).y;
338
+ return [{ x: x3, y: AY1 }, { x: x3, y: y3 }, { x: x4, y: y3 }, { x: x4, y: AY2 }];
339
+ } else if (y1 > y2) {
340
+ // SCENARIO 2B2: Source below target - S-bend going up
341
+ // Visual: ←[TGT]
342
+ // ↑
343
+ // ↑----→ |
344
+ // |
345
+ // |
346
+ // [SRC]→ |
347
+ const x3 = this.getRightGridIntersectionPoint(AX1, AY2, gridSize, minEdgeLength).x;
348
+ const x4 = this.getLeftGridIntersectionPoint(AX2, AY2, gridSize, minEdgeLength).x;
349
+ const y3 = this.getTopGridIntersectionPoint(x3, y1, gridSize, minEdgeLength).y;
350
+ return [{ x: x3, y: AY1 }, { x: x3, y: y3 }, { x: x4, y: y3 }, { x: x4, y: AY2 }];
351
+ }
352
+ }
353
+ } else if (x1 > x2) {
354
+ // SCENARIO 3: Source is to the right of target - create S-shaped path
355
+ if (y1 < y2) {
356
+ // SCENARIO 3A: Source above and right of target - S-bend down and left
357
+ // Visual: [SRC]→ |
358
+ // |
359
+ // ↓
360
+ // ←[TGT] ←---------
361
+ const x3 = this.getRightGridIntersectionPoint(AX1, AY1, gridSize, minEdgeLength).x;
362
+ const x4 = this.getLeftGridIntersectionPoint(AX2, AY2, gridSize, minEdgeLength).x;
363
+ const y3 = bEnhanced ? this._getRoutingYBelow(sourceNode, gridSize) : this.getBottomGridIntersectionPoint(x3, y1 + sourceNode._iHeight, gridSize, minEdgeLength).y;
364
+ return [
365
+ { x: x3, y: AY1 }, // Right from source
366
+ { x: x3, y: y3 }, // Down past both nodes
367
+ { x: x4, y: y3 }, // Left to target column
368
+ { x: x4, y: AY2 } // Up to target
369
+ ];
370
+ } else if (y1 > y2) {
371
+ // SCENARIO 3B: Source below and right of target - S-bend up and left
372
+ // Visual: ←[TGT] ←---------
373
+ // ↑
374
+ // |
375
+ // [SRC]→ |
376
+ const x3 = this.getRightGridIntersectionPoint(AX1, AY1, gridSize, minEdgeLength).x;
377
+ const x4 = this.getLeftGridIntersectionPoint(AX2, AY2, gridSize, minEdgeLength).x;
378
+ const y3 = this.getTopGridIntersectionPoint(x3, y1, gridSize, minEdgeLength).y;
379
+ return [{ x: x3, y: AY1 }, { x: x3, y: y3 }, { x: x4, y: y3 }, { x: x4, y: AY2 }];
380
+ }
381
+ }
382
+ }
383
+ // DEFAULT: No bends needed (direct connection possible)
384
+ return [];
385
+ },
386
+ /**
387
+ * ╔══════════════════════════════════════════════════════════════════════════════╗
388
+ * ║ LEFT-TO-RIGHT CONNECTION HANDLER ║
389
+ * ╠══════════════════════════════════════════════════════════════════════════════╣
390
+ * ║ ║
391
+ * ║ ←[SRC NODE] ········→ [TGT NODE]→ ║
392
+ * ║ ║
393
+ * ║ Calculates optimal path from left anchor of source to right anchor of ║
394
+ * ║ target, creating direct, U-shaped, Z-bend, or S-shaped connections ║
395
+ * ║ based on node positions and grid alignment. ║
396
+ * ║ ║
397
+ * ╚══════════════════════════════════════════════════════════════════════════════╝
398
+ */
399
+ /**
400
+ * Handles LeftToRight connection path calculation.
401
+ * Connection flows from the left side of source node to the right side of target node.
402
+ *
403
+ * @param {object} mParams - Parameters for path calculation
404
+ * @returns {Array<object>} Array of bend point coordinates
405
+ * @private
406
+ */
407
+ _handleLeftToRightConnection: function (mParams) {
408
+ const { sourceNode, targetNode, gridSize = GRID_SIZE, minEdgeLength = MIN_EDGE_LENGTH, bEnhanced } = mParams;
409
+ const { x1, y1, x2, y2, AX1, AY1, AX2, AY2, dx } = this._transformSourceTargetCoordinates(mParams);
410
+ // CASE 1: Source and target nodes are on the same horizontal line (y1 === y2)
411
+ if (y1 === y2) {
412
+ if (x1 > x2) {
413
+ // SCENARIO 1A: Direct horizontal connection (source left → target right, but source is actually to the right)
414
+ // Check if anchors are aligned and connection pattern allows straight line
415
+ const anchorVerticalOffset = Math.abs(AY1 - AY2);
416
+ const shouldUseStraightLine = this._shouldUseHorizontalStraightLine(mParams.sourceNode, mParams.targetNode, mParams.sConnectionType);
417
+
418
+ if (shouldUseStraightLine && anchorVerticalOffset <= 5) {
419
+ // Visual: [TGT]←----[SRC] (perfect alignment)
420
+ return [];
421
+ } else if (anchorVerticalOffset > 5) {
422
+ // Visual: ←[SRC] [TGT]→ (offset anchors - add horizontal correction)
423
+ // | |
424
+ // └───────────┘
425
+ const midX = AX1 + (AX2 - AX1) / 2;
426
+ return [
427
+ { x: midX, y: AY1 },
428
+ { x: midX, y: AY2 }
429
+ ];
430
+ } else {
431
+ // Multiple connections - use straight line anyway for now
432
+ return [];
433
+ }
434
+ } else if (x1 <= x2) {
435
+ // SCENARIO 1B: Forward horizontal connection but needs U-shape (source overlaps/in front of target)
436
+ // Visual: [SRC] [TGT]
437
+ // ↓----→ |
438
+ // | |
439
+ // ↑------↓
440
+ // Create U-shaped path going around both nodes
441
+ const x3 = this.getLeftGridIntersectionPoint(AX1, AY1, gridSize, minEdgeLength).x;
442
+ const x4 = this.getRightGridIntersectionPoint(AX2, AY2, gridSize, minEdgeLength).x;
443
+ const y3 = bEnhanced ? this._getRoutingYBelow(sourceNode, gridSize) : this.getBottomGridIntersectionPoint(x3, y1 + sourceNode._iHeight, gridSize, minEdgeLength).y;
444
+
445
+ return [
446
+ { x: x3, y: AY1 }, // Right from source
447
+ { x: x3, y: y3 }, // Down below both nodes
448
+ { x: x4, y: y3 }, // Left to target column
449
+ { x: x4, y: AY2 } // Up to target
450
+ ];
451
+ }
452
+ } else if (y1 != y2) {
453
+ // CASE 2: Source and target nodes are on different horizontal lines (y1 != y2)
454
+ if (x1 >= x2) {
455
+ // SCENARIO 2A: Source is to the right or aligned with target
456
+ if (dx >= minEdgeLength + targetNode._iWidth) {
457
+ if (Math.abs(x1 - (x2 + targetNode._iWidth)) <= gridSize) {
458
+ // SCENARIO 2A1: Nodes are close - use midpoint connection
459
+ // Visual: ←[SRC] [TGT]→
460
+ // | |
461
+ // ↓ ↑
462
+ // (midpoint)
463
+ const x3 = (AX1 + AX2) / 2;
464
+ return [{ x: x3, y: AY1 }, { x: x3, y: AY2 }];
465
+ } else {
466
+ // SCENARIO 2A2: Nodes are far apart - use grid-aligned Z-bend
467
+ // Visual: ←[SRC] |
468
+ // |
469
+ // ↓
470
+ // [TGT]→
471
+ const x3 = this.getLeftGridIntersectionPoint(AX1, AY1, gridSize, minEdgeLength).x;
472
+ return [{ x: x3, y: AY1 }, { x: x3, y: AY2 }];
473
+ }
474
+ } else if (dx <= minEdgeLength + targetNode._iWidth) {
475
+ // SCENARIO 2B: Nodes are too close - create S-shaped path
476
+ if (y1 < y2) {
477
+ // SCENARIO 2B1: Source above target - S-bend going down
478
+ // Visual: ←[SRC] |
479
+ // |
480
+ // ←---- ↓
481
+ // |
482
+ // ↓
483
+ // [TGT]→
484
+ const x3 = this.getLeftGridIntersectionPoint(AX1, AY1, gridSize, minEdgeLength).x;
485
+ const x4 = this.getRightGridIntersectionPoint(AX2, AY2, gridSize, minEdgeLength).x;
486
+ const y3 = bEnhanced ? this._getRoutingYBelow(sourceNode, gridSize) : this.getBottomGridIntersectionPoint(x3, y1 + sourceNode._iHeight, gridSize, minEdgeLength).y;
487
+ return [{ x: x3, y: AY1 }, { x: x3, y: y3 }, { x: x4, y: y3 }, { x: x4, y: AY2 }];
488
+ } else if (y1 > y2) {
489
+ // SCENARIO 2B2: Source below target - S-bend going up
490
+ // Visual: [TGT]→
491
+ // ↑
492
+ // ←------ |
493
+ // |
494
+ // |
495
+ // ←[SRC] |
496
+ const x3 = this.getLeftGridIntersectionPoint(AX1, AY2, gridSize, minEdgeLength).x;
497
+ const x4 = this.getRightGridIntersectionPoint(AX2, AY2, gridSize, minEdgeLength).x;
498
+ const y3 = this.getTopGridIntersectionPoint(x3, y1, gridSize, minEdgeLength).y;
499
+ return [{ x: x3, y: AY1 }, { x: x3, y: y3 }, { x: x4, y: y3 }, { x: x4, y: AY2 }];
500
+ }
501
+ }
502
+ } else if (x1 < x2) {
503
+ // SCENARIO 3: Source is to the left of target - create S-shaped path
504
+ if (y1 < y2) {
505
+ // SCENARIO 3A: Source above and left of target - S-bend down and right
506
+ // Visual: ←[SRC] |
507
+ // |
508
+ // ↓
509
+ // --------→[TGT]
510
+ const x3 = this.getLeftGridIntersectionPoint(AX1, AY1, gridSize, minEdgeLength).x;
511
+ const x4 = this.getRightGridIntersectionPoint(AX2, AY2, gridSize, minEdgeLength).x;
512
+ const y3 = bEnhanced ? this._getRoutingYBelow(sourceNode, gridSize) : this.getBottomGridIntersectionPoint(x3, y1 + sourceNode._iHeight, gridSize, minEdgeLength).y;
513
+ return [{ x: x3, y: AY1 }, { x: x3, y: y3 }, { x: x4, y: y3 }, { x: x4, y: AY2 }];
514
+ } else if (y1 > y2) {
515
+ // SCENARIO 3B: Source below and left of target - S-bend up and right
516
+ // Visual: --------→[TGT]
517
+ // ↑
518
+ // |
519
+ // ←[SRC] |
520
+ const x3 = this.getLeftGridIntersectionPoint(AX1, AY1, gridSize, minEdgeLength).x;
521
+ const x4 = this.getRightGridIntersectionPoint(AX2, AY2, gridSize, minEdgeLength).x;
522
+ const y3 = this.getTopGridIntersectionPoint(x3, y1, gridSize, minEdgeLength).y;
523
+ return [{ x: x3, y: AY1 }, { x: x3, y: y3 }, { x: x4, y: y3 }, { x: x4, y: AY2 }];
524
+ }
525
+ }
526
+ }
527
+ // DEFAULT: No bends needed (direct connection possible)
528
+ return [];
529
+ },
530
+ /**
531
+ * ╔══════════════════════════════════════════════════════════════════════════════╗
532
+ * ║ LEFT-TO-LEFT CONNECTION HANDLER ║
533
+ * ╠══════════════════════════════════════════════════════════════════════════════╣
534
+ * ║ ║
535
+ * ║ ←[SRC NODE] ········→ ←[TGT NODE] ║
536
+ * ║ ║
537
+ * ║ Calculates optimal path from left anchor of source to left anchor of ║
538
+ * ║ target, creating C-shaped or stepped connections that route around ║
539
+ * ║ the left side of both nodes to avoid overlaps. ║
540
+ * ║ ║
541
+ * ╚══════════════════════════════════════════════════════════════════════════════╝
542
+ */
543
+ /**
544
+ * Handles LeftToLeft connection path calculation.
545
+ * Connection flows from the left side of source node to the left side of target node.
546
+ *
547
+ * @param {object} mParams - Parameters for path calculation
548
+ * @returns {Array<object>} Array of bend point coordinates
549
+ * @private
550
+ */
551
+ _handleLeftToLeftConnection: function (mParams) {
552
+ const { sourceNode, targetNode, gridSize = GRID_SIZE, minEdgeLength = MIN_EDGE_LENGTH, bEnhanced } = mParams;
553
+ const { x1, y1, x2, y2, AX1, AY1, AX2, AY2 } = this._transformSourceTargetCoordinates(mParams);
554
+ // CASE 1: Source and target nodes are on the same horizontal line (y1 === y2)
555
+ if (y1 === y2) {
556
+ if (x1 > x2) {
557
+ // SCENARIO 1A: Source to the right of target - create C-shaped path on left side
558
+ // Visual: ←[TGT] ←[SRC]
559
+ // | |
560
+ // ↓ |
561
+ // ←-----------↓
562
+ // Check if nodes are close enough for midpoint, else use grid-aligned path
563
+ const x3 = Math.abs(x1 - x2 + targetNode._iWidth) <= gridSize ?
564
+ (AX1 + (x2 + targetNode._iWidth)) / 2 :
565
+ this.getLeftGridIntersectionPoint(AX1, AY1, gridSize, minEdgeLength).x;
566
+ const x4 = this.getLeftGridIntersectionPoint(AX2, AY2, gridSize, minEdgeLength).x;
567
+ const y3 = y1 <= gridSize ? (bEnhanced ? this._getRoutingYBelow(sourceNode, gridSize) : this.getBottomGridIntersectionPoint(x3, y1 + sourceNode._iHeight, gridSize, minEdgeLength).y) : this.getTopGridIntersectionPoint(x3, y1, gridSize, minEdgeLength).y;
568
+ return [
569
+ { x: x3, y: AY1 }, // Left from source
570
+ { x: x3, y: y3 }, // Down/up to avoid nodes
571
+ { x: x4, y: y3 }, // Across to target column
572
+ { x: x4, y: AY2 } // Up/down to target
573
+ ];
574
+ } else if (x1 <= x2) {
575
+ // SCENARIO 1B: Source to the left of target - create C-shaped path on left side
576
+ // Visual: ←[SRC] ←[TGT]
577
+ // | |
578
+ // ↓ |
579
+ // →-------------↓
580
+ const x3 = this.getLeftGridIntersectionPoint(AX1, AY1, gridSize, minEdgeLength).x;
581
+ const x4 = Math.abs(x1 + sourceNode._iWidth - x2) <= gridSize ?
582
+ (AX2 + x2) / 2 :
583
+ this.getLeftGridIntersectionPoint(AX2, AY2, gridSize, minEdgeLength).x;
584
+ const y3 = y1 <= gridSize ? (bEnhanced ? this._getRoutingYBelow(sourceNode, gridSize) : this.getBottomGridIntersectionPoint(x3, y1 + sourceNode._iHeight, gridSize, minEdgeLength).y) : this.getTopGridIntersectionPoint(x3, y1, gridSize, minEdgeLength).y;
585
+ return [{ x: x3, y: AY1 }, { x: x3, y: y3 }, { x: x4, y: y3 }, { x: x4, y: AY2 }];
586
+ }
587
+ } else if (y1 != y2) {
588
+ // CASE 2: Source and target nodes are on different horizontal lines (y1 != y2)
589
+ /**
590
+ * When the source and target nodes are not on the same horizontal line (y1 != y2)
591
+ * Then nodes can be placed in either x1 > x2 or x1 < x2
592
+ * with these positions there can be two cases:
593
+ * 1. y1 < y2: source node is above the target node
594
+ * 2. y1 > y2: source node is below the target node
595
+ * 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.
596
+ * 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.
597
+ */
598
+ if (x1 > x2) {
599
+ // SCENARIO 2A: Source is to the right of target
600
+ if (y1 < y2) {
601
+ // SCENARIO 2A1: Source above and right of target
602
+ if (AY1 < y2) {
603
+ // Simple vertical connection if source anchor is above target node
604
+ // Visual: ←[SRC]
605
+ // |
606
+ // |
607
+ // ↓
608
+ // ←[TGT]
609
+ const x4 = this.getLeftGridIntersectionPoint(AX2, AY2, gridSize, minEdgeLength).x;
610
+ return [{ x: x4, y: AY1 }, { x: x4, y: AY2 }];
611
+ } else if (AY1 >= y2) {
612
+ // Need to avoid target node - create stepped path
613
+ // Visual: ←[SRC]
614
+ // |
615
+ // ←[TGT] ←-----↓
616
+ const iDiff = Math.abs(AX1 - (x2 + targetNode._iWidth));
617
+ let x3 = null;
618
+ if (iDiff <= gridSize) {
619
+ x3 = iDiff < minEdgeLength ? (AX1 + (x2 + targetNode._iWidth) / 2) : AX1 - minEdgeLength;
620
+ } else {
621
+ x3 = this.getLeftGridIntersectionPoint(AX1, AY1, gridSize, minEdgeLength).x;
622
+ }
623
+ const x4 = this.getLeftGridIntersectionPoint(AX2, AY2, gridSize, minEdgeLength).x;
624
+ const y3 = this.getTopGridIntersectionPoint(x4, y2, gridSize, minEdgeLength).y;
625
+ return [{ x: x3, y: AY1 }, { x: x3, y: y3 }, { x: x4, y: y3 }, { x: x4, y: AY2 }];
626
+ }
627
+ } else if (y1 > y2) {
628
+ // SCENARIO 2A2: Source below and right of target
629
+ if (AY1 > y2 + targetNode._iHeight) {
630
+ // Simple vertical connection if source anchor is below target node
631
+ // Visual: ←[TGT]
632
+ // |
633
+ // |
634
+ // ↑
635
+ // ←[SRC]
636
+ const x4 = this.getLeftGridIntersectionPoint(AX2, AY2, gridSize, minEdgeLength).x;
637
+ return [{ x: x4, y: AY1 }, { x: x4, y: AY2 }];
638
+ } else if (AY1 <= y2 + targetNode._iHeight) {
639
+ // Need to avoid target node - create stepped path
640
+ // Visual: ←[TGT] ←-----↑
641
+ // |
642
+ // ←[SRC]
643
+ const iDiff = Math.abs(AX1 - (x2 + targetNode._iWidth));
644
+ let x3 = null;
645
+ if (iDiff <= gridSize) {
646
+ x3 = iDiff < AX1 + minEdgeLength ? (AX1 + (x2 + targetNode._iWidth) / 2) : AX1 + minEdgeLength;
647
+ } else {
648
+ x3 = this.getLeftGridIntersectionPoint(AX1, AY1, gridSize, minEdgeLength).x;
649
+ }
650
+ const x4 = this.getLeftGridIntersectionPoint(AX2, AY2, gridSize, minEdgeLength).x;
651
+ const y3 = bEnhanced ? this._getRoutingYBelow(targetNode, gridSize) : this.getBottomGridIntersectionPoint(x4, y2 + targetNode._iHeight, gridSize, minEdgeLength).y;
652
+ return [{ x: x3, y: AY1 }, { x: x3, y: y3 }, { x: x4, y: y3 }, { x: x4, y: AY2 }];
653
+ }
654
+ }
655
+ } else if (x1 <= x2) {
656
+ // SCENARIO 2B: Source is to the left of target
657
+ if (y1 < y2) {
658
+ // SCENARIO 2B1: Source above and left of target
659
+ // Visual: ←[SRC]
660
+ // |
661
+ // ↓-------→ |
662
+ // ↓
663
+ // ←[TGT]
664
+ const x3 = this.getLeftGridIntersectionPoint(AX1, AY1, gridSize, minEdgeLength).x;
665
+ const iDiff = Math.abs(x1 + sourceNode._iWidth - x2);
666
+ let x4 = null;
667
+ if (iDiff <= gridSize) {
668
+ x4 = iDiff < minEdgeLength ? (AX2 + (x1 + sourceNode._iWidth)) / 2 : AX2 + minEdgeLength;
669
+ } else {
670
+ x4 = this.getLeftGridIntersectionPoint(AX2, AY2, gridSize, minEdgeLength).x;
671
+ }
672
+ const y3 = (AY2 > (y1 + sourceNode._iHeight)) ? AY2 : (bEnhanced ? this._getRoutingYBelow(sourceNode, gridSize) : this.getBottomGridIntersectionPoint(x3, y1 + sourceNode._iHeight, gridSize, minEdgeLength).y);
673
+ 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 }];
674
+ return aCoordinates;
675
+ } else if (y1 > y2) {
676
+ // SCENARIO 2B2: Source below and left of target
677
+ // Visual: ←[TGT]
678
+ // ↑
679
+ // ←-------→ |
680
+ // |
681
+ // ←[SRC]
682
+ const x3 = this.getLeftGridIntersectionPoint(AX1, AY2, gridSize, minEdgeLength).x;
683
+ const iDiff = Math.abs(x1 + sourceNode._iWidth - x2);
684
+ let x4 = null;
685
+ if (iDiff <= gridSize) {
686
+ x4 = iDiff < minEdgeLength ? (AX2 + (x1 + sourceNode._iWidth)) / 2 : AX2 + minEdgeLength;
687
+ } else {
688
+ x4 = this.getLeftGridIntersectionPoint(AX2, AY2, gridSize, minEdgeLength).x;
689
+ }
690
+ const y3 = (AY2 < y1) ? AY2 : this.getTopGridIntersectionPoint(x3, y1, gridSize, minEdgeLength).y;
691
+ 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 }];
692
+ return aCoordinates;
693
+ }
694
+ }
695
+ }
696
+ // DEFAULT: No bends needed (direct connection possible)
697
+ return [];
698
+ },
699
+ /**
700
+ * ╔══════════════════════════════════════════════════════════════════════════════╗
701
+ * ║ RIGHT-TO-RIGHT CONNECTION HANDLER ║
702
+ * ╠══════════════════════════════════════════════════════════════════════════════╣
703
+ * ║ ║
704
+ * ║ [SRC NODE]→ ········→ [TGT NODE]→ ║
705
+ * ║ ║
706
+ * ║ Calculates optimal path from right anchor of source to right anchor of ║
707
+ * ║ target, creating C-shaped or stepped connections that route around ║
708
+ * ║ the right side of both nodes to avoid overlaps. ║
709
+ * ║ ║
710
+ * ╚══════════════════════════════════════════════════════════════════════════════╝
711
+ */
712
+ /**
713
+ * Handles RightToRight connection path calculation.
714
+ * Connection flows from the right side of source node to the right side of target node.
715
+ *
716
+ * @param {object} mParams - Parameters for path calculation
717
+ * @returns {Array<object>} Array of bend point coordinates
718
+ * @private
719
+ */
720
+ _handleRightToRightConnection: function (mParams) {
721
+ const { sourceNode, targetNode, gridSize = GRID_SIZE, minEdgeLength = MIN_EDGE_LENGTH, bEnhanced } = mParams;
722
+ const { x1, y1, x2, y2, AX1, AY1, AX2, AY2 } = this._transformSourceTargetCoordinates(mParams);
723
+ // CASE 1: Source and target nodes are on the same horizontal line (y1 === y2)
724
+ if (y1 === y2) {
725
+ if (x1 > x2) {
726
+ // SCENARIO 1A: Source to the right of target - create C-shaped path on right side
727
+ // Visual: [TGT]→ [SRC]→
728
+ // | |
729
+ // ↓ |
730
+ // →---------↓
731
+ const x3 = this.getRightGridIntersectionPoint(AX1, AY1, gridSize, minEdgeLength).x;
732
+ const iDiff = Math.abs(x1 - AX2);
733
+ const x4 = iDiff <= gridSize ? (x1 + x2) / 2 : this.getRightGridIntersectionPoint(AX2, AY2, gridSize, minEdgeLength).x;
734
+ const y3 = y1 < gridSize ? (bEnhanced ? this._getRoutingYBelow(sourceNode, gridSize) : this.getBottomGridIntersectionPoint(x3, y1 + sourceNode._iHeight, gridSize, minEdgeLength).y) : this.getTopGridIntersectionPoint(x3, y1, gridSize, minEdgeLength).y;
735
+ return [{ x: x3, y: AY1 }, { x: x3, y: y3 }, { x: x4, y: y3 }, { x: x4, y: AY2 }];
736
+ } else if (x1 <= x2) {
737
+ // SCENARIO 1B: Source to the left of target - create C-shaped path on right side
738
+ // Visual: [SRC]→ [TGT]→
739
+ // | |
740
+ // ↓ |
741
+ // →-----------------↓
742
+ const iDff = Math.abs(AX1 - x2);
743
+ const x3 = iDff <= gridSize ? (AX1 + x2) / 2 : this.getRightGridIntersectionPoint(AX1, AY1, gridSize, minEdgeLength).x;
744
+ const x4 = this.getRightGridIntersectionPoint(AX2, AY2, gridSize, minEdgeLength).x;
745
+ const y3 = y1 < gridSize ? (bEnhanced ? this._getRoutingYBelow(sourceNode, gridSize) : this.getBottomGridIntersectionPoint(x3, y1 + sourceNode._iHeight, gridSize, minEdgeLength).y) : this.getTopGridIntersectionPoint(x3, y1, gridSize, minEdgeLength).y;
746
+ return [{ x: x3, y: AY1 }, { x: x3, y: y3 }, { x: x4, y: y3 }, { x: x4, y: AY2 }];
747
+ }
748
+ } else if (y1 != y2) {
749
+ // CASE 2: Source and target nodes are on different horizontal lines (y1 != y2)
750
+ if (x1 > x2) {
751
+ // SCENARIO 2A: Source is to the right of target
752
+ if (y1 < y2) {
753
+ // SCENARIO 2A1: Source above and right of target
754
+ // Visual: [SRC]→
755
+ // |
756
+ // ↓--------→ |
757
+ // ↓
758
+ // [TGT]→
759
+ const x3 = this.getRightGridIntersectionPoint(AX1, AY1, gridSize, minEdgeLength).x;
760
+ const iDiff = Math.abs(AX2 - x1);
761
+ let x4 = null;
762
+ if (iDiff <= gridSize) {
763
+ x4 = iDiff < minEdgeLength ? (AX2 + x1) / 2 : AX2 + minEdgeLength;
764
+ } else {
765
+ x4 = this.getRightGridIntersectionPoint(AX2, AY2, gridSize, minEdgeLength).x;
766
+ }
767
+ const y3 = y1 <= gridSize ? (bEnhanced ? this._getRoutingYBelow(sourceNode, gridSize) : this.getBottomGridIntersectionPoint(x3, y1 + sourceNode._iHeight, gridSize, minEdgeLength).y) : this.getTopGridIntersectionPoint(x3, y1, gridSize, minEdgeLength).y;
768
+ if (AY2 > y1 + sourceNode._iHeight) {
769
+ // Simple vertical connection if target anchor is below source node
770
+ // Visual: [SRC]→
771
+ // |
772
+ // ↓
773
+ // [TGT]→
774
+ const aCords = AX2 > AX1 ? [{x: x4, y: AY1}, {x: x4, y: AY2}] : [{ x: x3, y: AY1 }, { x: x3, y: AY2 }];
775
+ return aCords;
776
+ } else {
777
+ // Need to avoid target node - create stepped path
778
+ // Visual: [SRC]→
779
+ // |
780
+ // ↓---- →
781
+ // |
782
+ // [TGT]→
783
+ return [{ x: x3, y: AY1 }, { x: x3, y: y3 }, { x: x4, y: y3 }, { x: x4, y: AY2 }];
784
+ }
785
+ } else if (y1 > y2) {
786
+ // SCENARIO 2A2: Source below and right of target
787
+ // Visual: [TGT]→
788
+ // ↑
789
+ // ↑--------→ |
790
+ // |
791
+ // [SRC]→
792
+ const x3 = this.getRightGridIntersectionPoint(AX1, AY1, gridSize, minEdgeLength).x;
793
+ const iDiff = Math.abs(AX2 - x1);
794
+ let x4 = null;
795
+ if (iDiff <= gridSize) {
796
+ x4 = iDiff < minEdgeLength ? (AX2 + x1) / 2 : AX2 + minEdgeLength;
797
+ } else {
798
+ x4 = this.getRightGridIntersectionPoint(AX2, AY2, gridSize, minEdgeLength).x;
799
+ }
800
+ const y3 = y2 <= gridSize ? (bEnhanced ? this._getRoutingYBelow(sourceNode, gridSize) : this.getBottomGridIntersectionPoint(x3, y1 + sourceNode._iHeight, gridSize, minEdgeLength)) : this.getTopGridIntersectionPoint(x4, y2, gridSize, minEdgeLength).y;
801
+ if (AY1 > y2 + targetNode._iHeight) {
802
+ // Simple vertical connection if target anchor is below source node
803
+ // Visual: [SRC]→
804
+ // |
805
+ // ↓
806
+ // [TGT]→
807
+ const aCords = AX2 > AX1 ? [{x: x4, y: AY1}, {x: x4, y: AY2}] : [{ x: x3, y: AY1 }, { x: x3, y: AY2 }];
808
+ return aCords;
809
+ } else {
810
+ // Need to avoid target node - create stepped path
811
+ // Visual: [SRC]→
812
+ // |
813
+ // ↓---- →
814
+ // |
815
+ // [TGT]→
816
+ return [{ x: x3, y: AY1 }, { x: x3, y: y3 }, { x: x4, y: y3 }, { x: x4, y: AY2 }];
817
+ }
818
+ }
819
+ } else if (x1 <= x2) {
820
+ // SCENARIO 2B: Source is to the left of target
821
+ if (y1 < y2) {
822
+ // SCENARIO 2B1: Source above and left of target
823
+ // Visual: [SRC]→
824
+ // |
825
+ // ↓----------→ |
826
+ // ↓
827
+ // [TGT]→
828
+ const x3 = this.getRightGridIntersectionPoint(AX1, AY1, gridSize, minEdgeLength).x;
829
+ const iDiff = Math.abs(x2 - AX1);
830
+ let x4 = null;
831
+ if (iDiff <= gridSize && !(AY2 > y1 + sourceNode._iHeight)) {
832
+ x4 = iDiff < minEdgeLength ? (x2 + AX1) / 2 : AX1 - minEdgeLength;
833
+ } else {
834
+ x4 = this.getRightGridIntersectionPoint(AX2, AY2, gridSize, minEdgeLength).x;
835
+ }
836
+ const y3 = y1 <= gridSize ? (bEnhanced ? this._getRoutingYBelow(sourceNode, gridSize) : this.getBottomGridIntersectionPoint(x3, y1 + sourceNode._iHeight, gridSize, minEdgeLength).y) : this.getTopGridIntersectionPoint(x3, y1, gridSize, minEdgeLength).y;
837
+ if (AY2 > y1 + sourceNode._iHeight) {
838
+ // Simple vertical connection if target anchor is below source node
839
+ // Visual: [SRC]→
840
+ // |
841
+ // ↓
842
+ // [TGT]→
843
+ const aCords = AX2 > AX1 ? [{x: x4, y: AY1}, {x: x4, y: AY2}] : [{ x: x3, y: AY1 }, { x: x3, y: AY2 }];
844
+ return aCords;
845
+ } else {
846
+ // Need to avoid target node - create stepped path
847
+ // Visual: [SRC]→
848
+ // |
849
+ // ↓---- →
850
+ // |
851
+ // [TGT]→
852
+ return [{ x: x3, y: AY1 }, { x: x3, y: y3 }, { x: x4, y: y3 }, { x: x4, y: AY2 }];
853
+ }
854
+ // 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 }];
855
+ // return aCoordinates;
856
+ } else if (y1 > y2) {
857
+ // SCENARIO 2B2: Source below and left of target
858
+ // Visual: [TGT]→
859
+ // ↑
860
+ // ↑----------→ |
861
+ // |
862
+ // [SRC]→
863
+ const x3 = this.getRightGridIntersectionPoint(AX1, AY1, gridSize, minEdgeLength).x;
864
+ const iDiff = Math.abs(x2 - AX1);
865
+ let x4 = null;
866
+ if (iDiff <= gridSize && !(AY1 > y2 + targetNode._iHeight)) {
867
+ x4 = iDiff < minEdgeLength ? (x2 + AX1) / 2 : AX1 - minEdgeLength;
868
+ } else {
869
+ x4 = this.getRightGridIntersectionPoint(AX2, AY2, gridSize, minEdgeLength).x;
870
+ }
871
+ const y3 = y2 <= gridSize ? (bEnhanced ? this._getRoutingYBelow(sourceNode, gridSize) : this.getBottomGridIntersectionPoint(x3, y1 + sourceNode._iHeight, gridSize, minEdgeLength)) : this.getTopGridIntersectionPoint(x4, y2, gridSize, minEdgeLength).y;
872
+ if (AY1 > y2 + targetNode._iHeight) {
873
+ // Simple vertical connection if target anchor is below source node
874
+ // Visual: [SRC]→
875
+ // |
876
+ // ↓
877
+ // [TGT]→
878
+ const aCords = AX2 > AX1 ? [{x: x4, y: AY1}, {x: x4, y: AY2}] : [{ x: x3, y: AY1 }, { x: x3, y: AY2 }];
879
+ return aCords;
880
+ } else {
881
+ // Need to avoid target node - create stepped path
882
+ // Visual: [SRC]→
883
+ // |
884
+ // ↓---- →
885
+ // |
886
+ // [TGT]→
887
+ return [{ x: x3, y: AY1 }, { x: x3, y: y3 }, { x: x4, y: y3 }, { x: x4, y: AY2 }];
888
+ }
889
+ // 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 }];
890
+ // return aCoordinates;
891
+ }
892
+ }
893
+ }
894
+ // DEFAULT: No bends needed (direct connection possible)
895
+ return [];
896
+ },
897
+ /**
898
+ * Calculates the right grid intersection point.
899
+ *
900
+ * @param {number} x - X coordinate
901
+ * @param {number} y - Y coordinate
902
+ * @param {number} [gridSize=50] - Size of grid cells
903
+ * @param {number} [minEdgeLength=20] - Minimum edge length
904
+ * @returns {object} Intersection point {x, y}
905
+ * @public
906
+ */
907
+ getRightGridIntersectionPoint: function (x, y, gridSize = GRID_SIZE, minEdgeLength = MIN_EDGE_LENGTH) {
908
+ let xIntersection = Math.ceil(x / gridSize) * gridSize;
909
+ if (xIntersection - x < minEdgeLength) {
910
+ xIntersection += gridSize; // Ensure the intersection is at least MIN_EDGE_LENGTH away
911
+ }
912
+ return { x: xIntersection, y: y };
913
+ },
914
+ /**
915
+ * Calculates the left grid intersection point.
916
+ *
917
+ * @param {number} x - X coordinate
918
+ * @param {number} y - Y coordinate
919
+ * @param {number} [gridSize=50] - Size of grid cells
920
+ * @param {number} [minEdgeLength=20] - Minimum edge length
921
+ * @returns {object} Intersection point {x, y}
922
+ * @public
923
+ */
924
+ getLeftGridIntersectionPoint: function (x, y, gridSize = GRID_SIZE, minEdgeLength = MIN_EDGE_LENGTH) {
925
+ let xIntersection = Math.floor(x / gridSize) * gridSize;
926
+ if (x - xIntersection < minEdgeLength) {
927
+ xIntersection -= gridSize; // Ensure the intersection is at least MIN_EDGE_LENGTH away
928
+ }
929
+ return { x: xIntersection, y: y };
930
+ },
931
+ /**
932
+ * Calculates the top grid intersection point.
933
+ *
934
+ * @param {number} x - X coordinate
935
+ * @param {number} y - Y coordinate
936
+ * @param {number} [gridSize=50] - Size of grid cells
937
+ * @param {number} [minEdgeLength=20] - Minimum edge length
938
+ * @returns {object} Intersection point {x, y}
939
+ * @public
940
+ */
941
+ getTopGridIntersectionPoint: function (x, y, gridSize = GRID_SIZE, minEdgeLength = MIN_EDGE_LENGTH) {
942
+ let yIntersection = Math.floor(y / gridSize) * gridSize;
943
+ if (y - yIntersection < minEdgeLength) {
944
+ yIntersection -= gridSize; // Ensure the intersection is at least MIN_EDGE_LENGTH away
945
+ }
946
+ return { x: x, y: yIntersection };
947
+ },
948
+ /**
949
+ * Calculates the bottom grid intersection point.
950
+ *
951
+ * @param {number} x - X coordinate
952
+ * @param {number} y - Y coordinate
953
+ * @param {number} [gridSize=50] - Size of grid cells
954
+ * @param {number} [minEdgeLength=20] - Minimum edge length
955
+ * @returns {object} Intersection point {x, y}
956
+ * @public
957
+ */
958
+ getBottomGridIntersectionPoint: function (x, y, gridSize = GRID_SIZE, minEdgeLength = MIN_EDGE_LENGTH) {
959
+ let yIntersection = Math.ceil(y / gridSize) * gridSize;
960
+ if (yIntersection - y < minEdgeLength) {
961
+ yIntersection += gridSize; // Ensure the intersection is at least MIN_EDGE_LENGTH away
962
+ }
963
+ return { x: x, y: yIntersection };
964
+ },
965
+ /**
966
+ * Post-processing pass: detects horizontal line segments that pass through a node's
967
+ * bounding box and shifts them below the collision.
968
+ * Only interior segments are adjusted, source and target anchors are never moved.
969
+ *
970
+ * @param {sap.suite.ui.commons.networkgraph.Line[]} aLines
971
+ * @param {sap.suite.ui.commons.networkgraph.Node[]} aNodes
972
+ * @param {number} gridSize
973
+ * @private
974
+ */
975
+ _resolveLineNodeCollisions: function(aLines, aNodes, gridSize) {
976
+ var aBounds = aNodes.map(function(oNode) {
977
+ return {
978
+ x: oNode.getX(),
979
+ y: oNode.getY(),
980
+ r: oNode.getX() + (oNode._iWidth || 0),
981
+ b: oNode.getY() + (oNode._iHeight || 0)
982
+ };
983
+ }).filter(function(b) { return b.r > b.x && b.b > b.y; });
984
+
985
+ aLines.forEach(function(oLine) {
986
+ if (oLine.getVisible && !oLine.getVisible()) { return; }
987
+ if (oLine._isIgnored && oLine._isIgnored()) { return; }
988
+
989
+ var aBends = oLine.getBends();
990
+ var aPoints = [{ x: oLine.getSource().getX(), y: oLine.getSource().getY() }];
991
+ aBends.forEach(function(oBend) { aPoints.push({ x: oBend.getX(), y: oBend.getY() }); });
992
+ aPoints.push({ x: oLine.getTarget().getX(), y: oLine.getTarget().getY() });
993
+
994
+ var iPointCount = aPoints.length;
995
+ var bChanged = false;
996
+
997
+ // Only check interior segments — never move source (0) or target (iPointCount-1)
998
+ for (var i = 1; i < iPointCount - 2; i++) {
999
+ var p1 = aPoints[i], p2 = aPoints[i + 1];
1000
+ if (p1.y !== p2.y) { continue; } // skip vertical segments
1001
+
1002
+ var y = p1.y;
1003
+ var xMin = Math.min(p1.x, p2.x);
1004
+ var xMax = Math.max(p1.x, p2.x);
1005
+
1006
+ var iMaxBottom = -Infinity;
1007
+ var bCollision = false;
1008
+ aBounds.forEach(function(oBound) {
1009
+ if (y > oBound.y && y <= oBound.b && xMin < oBound.r && xMax > oBound.x) {
1010
+ bCollision = true;
1011
+ if (oBound.b > iMaxBottom) { iMaxBottom = oBound.b; }
1012
+ }
1013
+ });
1014
+
1015
+ if (bCollision) {
1016
+ var iNewY = iMaxBottom + Math.round(gridSize * 0.5);
1017
+ aPoints[i].y = iNewY;
1018
+ aPoints[i + 1].y = iNewY;
1019
+ bChanged = true;
1020
+ }
1021
+ }
1022
+
1023
+ if (bChanged) {
1024
+ oLine.clearBends();
1025
+ for (var j = 1; j < iPointCount - 1; j++) {
1026
+ oLine.addBend(aPoints[j]);
1027
+ }
1028
+ }
1029
+ });
1030
+ },
1031
+
1032
+ /**
1033
+ * Builds a WeakMap of per-node row boundary data used by _getRoutingYBelow.
1034
+ * Uses two passes to avoid the single-pass bug where iRowBottom is not yet finalised
1035
+ * when a next-row candidate is evaluated.
1036
+ *
1037
+ * @param {object[]} aNodes - All graph nodes
1038
+ * @param {number} gridSize - Grid size used for same-row proximity threshold
1039
+ * @returns {WeakMap} node → { rowBottom, nextRowTop }
1040
+ * @private
1041
+ */
1042
+ _buildRoutingRowCache: function(aNodes, gridSize) {
1043
+ var mCache = new WeakMap();
1044
+ if (!aNodes.length) { return mCache; }
1045
+
1046
+ // Pass 1: group nodes by row (center within gridSize/2) and compute rowBottom per group.
1047
+ // The representative centerY is intentionally kept as the first node's center, a deliberate
1048
+ // approximation that avoids updating all prior comparisons when new nodes join.
1049
+ var aRows = [];
1050
+ aNodes.forEach(function(oNode) {
1051
+ var h = oNode._iHeight || 0;
1052
+ var centerY = oNode.getY() + h / 2;
1053
+ var oRow = null;
1054
+ for (var i = 0; i < aRows.length; i++) {
1055
+ if (Math.abs(aRows[i].centerY - centerY) < gridSize / 2) {
1056
+ oRow = aRows[i];
1057
+ break;
1058
+ }
1059
+ }
1060
+ if (oRow) {
1061
+ oRow.nodes.push(oNode);
1062
+ oRow.rowBottom = Math.max(oRow.rowBottom, oNode.getY() + h);
1063
+ oRow.rowTop = Math.min(oRow.rowTop, oNode.getY());
1064
+ } else {
1065
+ aRows.push({ centerY: centerY, nodes: [oNode], rowBottom: oNode.getY() + h, rowTop: oNode.getY() });
1066
+ }
1067
+ });
1068
+
1069
+ // Pass 2: for each row, find the nearest next rowTop. rowTop isn't monotonic with
1070
+ // rowBottom if the heights vary, so the scan can't stop at the first match. It must
1071
+ // keep the minimum qualifying rowTop across all subsequent rows.
1072
+ aRows.sort(function(a, b) { return a.rowBottom - b.rowBottom; });
1073
+ aRows.forEach(function(oRow, iRowIdx) {
1074
+ var iNextRowTop = Infinity;
1075
+ for (var k = iRowIdx + 1; k < aRows.length; k++) {
1076
+ if (aRows[k].rowTop > oRow.rowBottom && aRows[k].rowTop < iNextRowTop) {
1077
+ iNextRowTop = aRows[k].rowTop;
1078
+ }
1079
+ }
1080
+ oRow.nodes.forEach(function(oNode) {
1081
+ mCache.set(oNode, { rowBottom: oRow.rowBottom, nextRowTop: iNextRowTop });
1082
+ });
1083
+ });
1084
+
1085
+ return mCache;
1086
+ },
1087
+
1088
+ /**
1089
+ * Returns the Y coordinate of the horizontal routing corridor below a node's row.
1090
+ * Uses pre-computed row boundary data from _buildRoutingRowCache (O(1) lookup).
1091
+ * Falls back to node-bottom + 0.75 * gridSize when called outside normalizeLines.
1092
+ *
1093
+ * @param {object} oNode - Source/target node
1094
+ * @param {number} gridSize - Grid size for fallback spacing
1095
+ * @returns {number} Y coordinate of the routing corridor
1096
+ * @private
1097
+ */
1098
+ _getRoutingYBelow: function(oNode, gridSize) {
1099
+ var oData = this._routingRowCache && this._routingRowCache.get(oNode);
1100
+ var iRowBottom = oData ? oData.rowBottom : oNode.getY() + (oNode._iHeight || 0);
1101
+ var iNextRowTop = oData ? oData.nextRowTop : Infinity;
1102
+
1103
+ if (iNextRowTop === Infinity) {
1104
+ return iRowBottom + Math.round(gridSize * 0.75);
1105
+ }
1106
+ return Math.round((iRowBottom + iNextRowTop) / 2);
1107
+ },
1108
+
1109
+ /**
1110
+ * Gets the anchor coordinates for a node based on connection type and anchor type.
1111
+ * Enhanced to handle center positioning for single connections and offset positioning for mixed connections.
1112
+ *
1113
+ * @param {object} oNode - The node object
1114
+ * @param {string} sConnectionType - Connection type
1115
+ * @param {string} sAnchorType - Anchor type ("source" or "target")
1116
+ * @returns {object} Anchor coordinates {x, y}
1117
+ * @private
1118
+ */
1119
+ _getNodeAnchorCoordinates: function (oNode, sConnectionType, sAnchorType) {
1120
+ const x = oNode.getX();
1121
+ const y = oNode.getY();
1122
+ const width = oNode._iWidth;
1123
+ const height = oNode._iHeight;
1124
+ const centerY = y + (height / 2);
1125
+
1126
+ // Enhanced anchor calculation with connection pattern analysis
1127
+ const oConnectionAnalysis = this._analyzeNodeConnectionPatterns(oNode);
1128
+ // Determine the port position based on the connection type and analysis
1129
+ switch (sConnectionType) {
1130
+ case "LeftToLeft":
1131
+ if (sAnchorType === "source") {
1132
+ // Source anchor on left side
1133
+ return this._calculateEnhancedAnchor({
1134
+ node: { x, y, width, height, centerY },
1135
+ side: "left",
1136
+ analysis: oConnectionAnalysis,
1137
+ anchorType: sAnchorType
1138
+ });
1139
+ } else {
1140
+ // Target anchor on left side
1141
+ return this._calculateEnhancedAnchor({
1142
+ node: { x, y, width, height, centerY },
1143
+ side: "left",
1144
+ analysis: oConnectionAnalysis,
1145
+ anchorType: sAnchorType
1146
+ });
1147
+ }
1148
+ case "LeftToRight":
1149
+ if (sAnchorType === "source") {
1150
+ // Source anchor on left side
1151
+ return this._calculateEnhancedAnchor({
1152
+ node: { x, y, width, height, centerY },
1153
+ side: "left",
1154
+ analysis: oConnectionAnalysis,
1155
+ anchorType: sAnchorType
1156
+ });
1157
+ } else {
1158
+ // Target anchor on right side
1159
+ return this._calculateEnhancedAnchor({
1160
+ node: { x, y, width, height, centerY },
1161
+ side: "right",
1162
+ analysis: oConnectionAnalysis,
1163
+ anchorType: sAnchorType
1164
+ });
1165
+ }
1166
+ case "RightToLeft":
1167
+ if (sAnchorType === "source") {
1168
+ // Source anchor on right side
1169
+ return this._calculateEnhancedAnchor({
1170
+ node: { x, y, width, height, centerY },
1171
+ side: "right",
1172
+ analysis: oConnectionAnalysis,
1173
+ anchorType: sAnchorType
1174
+ });
1175
+ } else {
1176
+ // Target anchor on left side
1177
+ return this._calculateEnhancedAnchor({
1178
+ node: { x, y, width, height, centerY },
1179
+ side: "left",
1180
+ analysis: oConnectionAnalysis,
1181
+ anchorType: sAnchorType
1182
+ });
1183
+ }
1184
+ case "RightToRight":
1185
+ if (sAnchorType === "source") {
1186
+ // Source anchor on right side
1187
+ return this._calculateEnhancedAnchor({
1188
+ node: { x, y, width, height, centerY },
1189
+ side: "right",
1190
+ analysis: oConnectionAnalysis,
1191
+ anchorType: sAnchorType
1192
+ });
1193
+ } else {
1194
+ // Target anchor on right side
1195
+ return this._calculateEnhancedAnchor({
1196
+ node: { x, y, width, height, centerY },
1197
+ side: "right",
1198
+ analysis: oConnectionAnalysis,
1199
+ anchorType: sAnchorType
1200
+ });
1201
+ }
1202
+ default:
1203
+ // Default RightToLeft case
1204
+ if (sAnchorType === "source") {
1205
+ return this._calculateEnhancedAnchor({
1206
+ node: { x, y, width, height, centerY },
1207
+ side: "right",
1208
+ analysis: oConnectionAnalysis,
1209
+ anchorType: sAnchorType
1210
+ });
1211
+ } else {
1212
+ return this._calculateEnhancedAnchor({
1213
+ node: { x, y, width, height, centerY },
1214
+ side: "left",
1215
+ analysis: oConnectionAnalysis,
1216
+ anchorType: sAnchorType
1217
+ });
1218
+ }
1219
+ }
1220
+ },
1221
+ /**
1222
+ * Transforms source and target coordinates into a normalized format for calculations.
1223
+ *
1224
+ * @param {object} mParams - Parameters containing node and anchor information
1225
+ * @returns {object} Transformed coordinates object
1226
+ * @private
1227
+ */
1228
+ _transformSourceTargetCoordinates: function (mParams) {
1229
+ return {
1230
+ x1: mParams.sourceNode.getX(), // x coordinate of the source node
1231
+ y1: mParams.sourceNode.getY(), // y coordinate of the source node
1232
+ x2: mParams.targetNode.getX(), // x cordinate of the target node
1233
+ y2: mParams.targetNode.getY(), // y coordinate of the target node
1234
+ AX1: mParams.sourceAnchor.x, // x coordinate of the source anchor
1235
+ AY1: mParams.sourceAnchor.y, // y coordinate of the source anchor
1236
+ AX2: mParams.targetAnchor.x, // x coordinate of the target anchor
1237
+ AY2: mParams.targetAnchor.y, // y coordinate of the target anchor
1238
+ connectionType: mParams.connectionType, // type of the connection
1239
+ dx: Math.abs(mParams.targetNode.getX() - mParams.sourceNode.getX()), // horizontal distance between source and target nodes
1240
+ dy: Math.abs(mParams.targetNode.getY() - mParams.sourceNode.getY()) // vertical distance between source and target nodes
1241
+ };
1242
+ },
1243
+ /**
1244
+ * Analyzes the connection patterns for a node to determine optimal anchor placement
1245
+ * @param {object} oNode - The node to analyze
1246
+ * @returns {object} Analysis object with connection counts per side
1247
+ * @private
1248
+ */
1249
+ _analyzeNodeConnectionPatterns: function (oNode) {
1250
+ if (!oNode || !oNode.getParent || !oNode.getParent()) {
1251
+ return { leftIncoming: 0, leftOutgoing: 0, rightIncoming: 0, rightOutgoing: 0 };
1252
+ }
1253
+
1254
+ if (this._analysisCache) {
1255
+ var sCachedKey = oNode.getKey && oNode.getKey();
1256
+ if (sCachedKey !== undefined && this._analysisCache.has(String(sCachedKey))) {
1257
+ return this._analysisCache.get(String(sCachedKey));
1258
+ }
1259
+ }
1260
+
1261
+ const oGraph = oNode.getParent();
1262
+ const aLines = oGraph.getLines();
1263
+ const sNodeKey = oNode.getKey();
1264
+
1265
+ let leftIncoming = 0, leftOutgoing = 0, rightIncoming = 0, rightOutgoing = 0;
1266
+
1267
+ aLines.forEach(function(oLine) {
1268
+ if (!oLine.getVisible() || oLine._isIgnored()) {
1269
+ return;
1270
+ }
1271
+
1272
+ const sFromKey = oLine.getFrom();
1273
+ const sToKey = oLine.getTo();
1274
+ const sLineConnectionType = oLine.getConnectionType();
1275
+
1276
+ // Check if this line involves our node
1277
+ if (sFromKey === sNodeKey) {
1278
+ // Outgoing connection from our node
1279
+ switch (sLineConnectionType) {
1280
+ case "LeftToLeft":
1281
+ case "LeftToRight":
1282
+ leftOutgoing++;
1283
+ break;
1284
+ case "RightToLeft":
1285
+ case "RightToRight":
1286
+ rightOutgoing++;
1287
+ break;
1288
+ default:
1289
+ // Default to right for unrecognized connection types
1290
+ rightOutgoing++;
1291
+ break;
1292
+ }
1293
+ } else if (sToKey === sNodeKey) {
1294
+ // Incoming connection to our node
1295
+ switch (sLineConnectionType) {
1296
+ case "LeftToLeft":
1297
+ case "RightToLeft":
1298
+ leftIncoming++;
1299
+ break;
1300
+ case "LeftToRight":
1301
+ case "RightToRight":
1302
+ rightIncoming++;
1303
+ break;
1304
+ default:
1305
+ // Default to left for unrecognized connection types
1306
+ leftIncoming++;
1307
+ break;
1308
+ }
1309
+ }
1310
+ });
1311
+
1312
+ return {
1313
+ leftIncoming: leftIncoming,
1314
+ leftOutgoing: leftOutgoing,
1315
+ rightIncoming: rightIncoming,
1316
+ rightOutgoing: rightOutgoing
1317
+ };
1318
+ },
1319
+
1320
+ /**
1321
+ * Calculates enhanced anchor position based on connection analysis
1322
+ * @param {object} mParams - Parameters for anchor calculation
1323
+ * @param {object} mParams.node - Node position and size info
1324
+ * @param {string} mParams.side - Side of the node ("left" or "right")
1325
+ * @param {object} mParams.analysis - Connection pattern analysis
1326
+ * @param {string} mParams.anchorType - Anchor type ("source" or "target")
1327
+ * @returns {object} Anchor coordinates {x, y}
1328
+ * @private
1329
+ */
1330
+ _calculateEnhancedAnchor: function (mParams) {
1331
+ const { node, side, analysis, anchorType } = mParams;
1332
+ const { x, y, width, height, centerY } = node;
1333
+
1334
+ const offsetDistance = height * 0.15; // 15% of node height for offset positioning
1335
+
1336
+ // Determine base X coordinate for the side
1337
+ const baseX = side === "left" ? x : (x + width);
1338
+
1339
+ // Get connection counts for this side
1340
+ const sideIncoming = side === "left" ? analysis.leftIncoming : analysis.rightIncoming;
1341
+ const sideOutgoing = side === "left" ? analysis.leftOutgoing : analysis.rightOutgoing;
1342
+
1343
+ // Calculate Y position based on connection patterns
1344
+ let anchorY = centerY; // Default to center
1345
+
1346
+ // ENHANCEMENT 1: Single connection type on this side -> use center
1347
+ if ((sideIncoming === 0 && sideOutgoing === 1) || (sideIncoming === 1 && sideOutgoing === 0)) {
1348
+ anchorY = centerY; // Center position for single connection
1349
+ } else if (sideIncoming > 0 && sideOutgoing > 0) {
1350
+ // ENHANCEMENT 2: Mixed connections on this side -> offset positioning
1351
+ if (anchorType === "target") {
1352
+ // Incoming connections (targets) go above center
1353
+ anchorY = centerY - offsetDistance;
1354
+ } else {
1355
+ // Outgoing connections (sources) go below center
1356
+ anchorY = centerY + offsetDistance;
1357
+ }
1358
+ } else {
1359
+ // Multiple connections of same type -> use center (for now)
1360
+ anchorY = centerY;
1361
+ }
1362
+
1363
+ // Ensure anchor stays within node boundaries
1364
+ const minY = y + (height * 0.1); // 10% margin from top
1365
+ const maxY = y + (height * 0.9); // 10% margin from bottom
1366
+ anchorY = Math.max(minY, Math.min(maxY, anchorY));
1367
+
1368
+ return {
1369
+ x: baseX,
1370
+ y: Math.floor(anchorY)
1371
+ };
1372
+ },
1373
+
1374
+ /**
1375
+ * Determines if a horizontal connection should use a straight line based on connection patterns.
1376
+ * Returns true only when both nodes have single dedicated connections (source has single outgoing, target has single incoming).
1377
+ *
1378
+ * @param {object} sourceNode - The source node
1379
+ * @param {object} targetNode - The target node
1380
+ * @param {string} connectionType - The connection type
1381
+ * @returns {boolean} True if straight line should be used, false if bends are needed
1382
+ * @private
1383
+ */
1384
+ _shouldUseHorizontalStraightLine: function (sourceNode, targetNode, connectionType) {
1385
+ const sourceAnalysis = this._analyzeNodeConnectionPatterns(sourceNode);
1386
+ const targetAnalysis = this._analyzeNodeConnectionPatterns(targetNode);
1387
+
1388
+ // Determine which sides are involved based on connection type
1389
+ let sourceOutgoingCount, targetIncomingCount;
1390
+
1391
+ switch (connectionType) {
1392
+ case "RightToLeft":
1393
+ sourceOutgoingCount = sourceAnalysis.rightOutgoing;
1394
+ targetIncomingCount = targetAnalysis.leftIncoming;
1395
+ break;
1396
+ case "LeftToRight":
1397
+ sourceOutgoingCount = sourceAnalysis.leftOutgoing;
1398
+ targetIncomingCount = targetAnalysis.rightIncoming;
1399
+ break;
1400
+ case "LeftToLeft":
1401
+ sourceOutgoingCount = sourceAnalysis.leftOutgoing;
1402
+ targetIncomingCount = targetAnalysis.leftIncoming;
1403
+ break;
1404
+ case "RightToRight":
1405
+ sourceOutgoingCount = sourceAnalysis.rightOutgoing;
1406
+ targetIncomingCount = targetAnalysis.rightIncoming;
1407
+ break;
1408
+ default:
1409
+ // Default to RightToLeft
1410
+ sourceOutgoingCount = sourceAnalysis.rightOutgoing;
1411
+ targetIncomingCount = targetAnalysis.leftIncoming;
1412
+ break;
1413
+ }
1414
+
1415
+ // Allow straight line only when both nodes have single dedicated connections
1416
+ return (sourceOutgoingCount === 1 && targetIncomingCount === 1);
1417
+ }
1418
+ };
1419
+ return ConnectionPathUtils;
1420
+ });