@vanduo-oss/vd3-flowchart 1.5.0 → 1.5.1

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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,24 @@
2
2
 
3
3
  All notable changes to `@vanduo-oss/vd3-flowchart` are documented here.
4
4
 
5
+ ## 1.5.1 — 2026-11-01
6
+
7
+ ### Fixed
8
+
9
+ - Inline text editors for shapes use transparent chrome so circles and diamonds
10
+ keep their SVG silhouette while typing. Textbox, edge, and label editors keep
11
+ an opaque panel.
12
+ - Dark theme idle node strokes lean on the accent color so outlines stay visible
13
+ without hover or selection. Hosts may still override `--vd-flowchart-node-stroke`.
14
+ - The diagram canvas no longer draws a full-viewport focus ring. Mindmap Tab that
15
+ opens a new branch editor skips the intermediate canvas focus flash.
16
+ - Tree layout measures node size. Rank and sibling gaps stay clear of the boxes,
17
+ and a decision with one deep branch keeps that branch on the main spine.
18
+ - Facing orthogonal connectors shorten their stubs when two shapes sit closer
19
+ than two full stubs, so the route does not loop around the pair.
20
+ - Tree layout re-picks ports from the flow: diamond side branches leave the side
21
+ ports, and a reverse loop uses the quieter side of the spine.
22
+
5
23
  ## 1.5.0 — 2026-10-25
6
24
 
7
25
  ### Added
package/README.md CHANGED
@@ -1,12 +1,14 @@
1
1
  # @vanduo-oss/vd3-flowchart
2
2
 
3
+ <a href="https://snyk.io/?utm_source=open-source&utm_medium=pg-ptr&utm_campaign=ref-2501-osp&utm_content=pg-cta"><img src="https://raw.githubusercontent.com/vanduo-oss/vd3-flowchart/main/assets/scanned-by-snyk.png" alt="Scanned by Snyk" width="206" height="141"></a>
4
+
3
5
  [![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
4
6
 
5
7
  Vanduo **flowchart** for Vue 3: a node/edge editor with undo/redo, layout
6
8
  modes, and a framework-agnostic core. Extracted 1-to-1 from
7
9
  `@vanduo-oss/vd3-cbun` flowchart **1.2.0**.
8
10
 
9
- **Status: 1.5.0.** `VD_FLOWCHART_VERSION` tracks the package release.
11
+ **Status: 1.5.1.** `VD_FLOWCHART_VERSION` tracks the package release.
10
12
  `FLOWCHART_DOCUMENT_VERSION` (currently `1.3.0`) is the value `toJSON()`
11
13
  writes. Do not reset either independently without a compatibility plan.
12
14
 
@@ -50,7 +52,7 @@ if anything but `vue` is externalized. The package declares
50
52
 
51
53
  ## Version policy
52
54
 
53
- `package.json` version **is** `VD_FLOWCHART_VERSION` (`1.5.0`). Bump those
55
+ `package.json` version **is** `VD_FLOWCHART_VERSION` (`1.5.1`). Bump those
54
56
  together for a package release. Keep `FLOWCHART_DOCUMENT_VERSION` unchanged
55
57
  unless the serialized schema changes; add fixtures and explicit compatibility
56
58
  rules for a format change. Unversioned 1.x documents through 1.3.0 still load;
@@ -144,6 +146,9 @@ Tokens consumed include `--vd-bg-primary`, `--vd-bg-secondary`,
144
146
 
145
147
  ## Security
146
148
 
149
+ - **Open-source scanning** — this repo is monitored with
150
+ [Snyk](https://snyk.io/?utm_source=open-source&utm_medium=pg-ptr&utm_campaign=ref-2501-osp&utm_content=pg-cta)
151
+ (thanks for their support of open source).
147
152
  - **No bundled dependencies** — beyond the `vue` peer, nothing is bundled; the
148
153
  build fails if any other module is externalized or any `node_modules` input
149
154
  is bundled.
package/dist/index.cjs CHANGED
@@ -44,6 +44,8 @@ var DIRECTIONS = {
44
44
  down: { main: "y", sign: 1 },
45
45
  up: { main: "y", sign: -1 }
46
46
  };
47
+ var DEFAULT_LEVEL_EDGE_GAP = 88;
48
+ var DEFAULT_SIBLING_EDGE_GAP = 64;
47
49
  function num(value, fallback) {
48
50
  const next = Number(value);
49
51
  return Number.isFinite(next) ? next : fallback;
@@ -68,60 +70,182 @@ function pickRoots(nodes, indegree, optRoot) {
68
70
  }
69
71
  function spanningForest(allIds, roots, children) {
70
72
  const visited = /* @__PURE__ */ new Set();
71
- const depth = /* @__PURE__ */ new Map();
72
- const tree = /* @__PURE__ */ new Map();
73
+ const active = /* @__PURE__ */ new Set();
73
74
  const forestRoots = [];
74
- const walk = (id, d) => {
75
+ const forwardParents = new Map(allIds.map((id) => [id, []]));
76
+ const dfs = (id) => {
75
77
  visited.add(id);
76
- depth.set(id, d);
77
- const kids = [];
78
+ active.add(id);
78
79
  (children.get(id) || []).forEach((child) => {
79
- if (!visited.has(child)) {
80
- kids.push(child);
81
- walk(child, d + 1);
82
- }
80
+ if (active.has(child)) return;
81
+ const list = forwardParents.get(child);
82
+ if (list && !list.includes(id)) list.push(id);
83
+ if (!visited.has(child)) dfs(child);
83
84
  });
84
- tree.set(id, kids);
85
+ active.delete(id);
85
86
  };
86
87
  roots.forEach((root) => {
87
88
  if (!visited.has(root)) {
88
89
  forestRoots.push(root);
89
- walk(root, 0);
90
+ dfs(root);
90
91
  }
91
92
  });
92
93
  allIds.forEach((id) => {
93
94
  if (!visited.has(id)) {
94
95
  forestRoots.push(id);
95
- walk(id, 0);
96
+ dfs(id);
96
97
  }
97
98
  });
99
+ const depth = new Map(allIds.map((id) => [id, 0]));
100
+ const limit = Math.max(1, allIds.length);
101
+ for (let pass = 0; pass < limit; pass += 1) {
102
+ let changed = false;
103
+ for (const id of allIds) {
104
+ const parents = forwardParents.get(id) || [];
105
+ for (const parent of parents) {
106
+ const nextDepth = Math.min(limit, (depth.get(parent) || 0) + 1);
107
+ if (nextDepth > (depth.get(id) || 0)) {
108
+ depth.set(id, nextDepth);
109
+ changed = true;
110
+ }
111
+ }
112
+ }
113
+ if (!changed) break;
114
+ }
115
+ const tree = new Map(allIds.map((id) => [id, []]));
116
+ const rootSet = new Set(forestRoots);
117
+ const chosenParent = /* @__PURE__ */ new Map();
118
+ for (const id of allIds) {
119
+ if (rootSet.has(id)) continue;
120
+ const parents = forwardParents.get(id) || [];
121
+ if (!parents.length) continue;
122
+ let bestParent = parents[0];
123
+ let bestDepth = depth.get(bestParent) ?? -1;
124
+ for (let index = 1; index < parents.length; index += 1) {
125
+ const candidate = parents[index];
126
+ const candidateDepth = depth.get(candidate) ?? -1;
127
+ if (candidateDepth > bestDepth) {
128
+ bestDepth = candidateDepth;
129
+ bestParent = candidate;
130
+ }
131
+ }
132
+ chosenParent.set(id, bestParent);
133
+ }
134
+ const placedChild = /* @__PURE__ */ new Set();
135
+ for (const parentId of allIds) {
136
+ const out = tree.get(parentId);
137
+ for (const childId of children.get(parentId) || []) {
138
+ if (chosenParent.get(childId) === parentId && !placedChild.has(childId)) {
139
+ placedChild.add(childId);
140
+ out.push(childId);
141
+ }
142
+ }
143
+ }
98
144
  return { depth, tree, forestRoots };
99
145
  }
100
146
  function treeCenters(forestRoots, tree, depth, sizeOf, options) {
101
147
  const dir = DIRECTIONS[options.direction] || DIRECTIONS.right;
102
- const levelGap = num(options.levelGap, 220);
103
- const siblingGap = num(options.siblingGap, 140);
148
+ const hasExplicitLevel = Number.isFinite(Number(options.levelGap));
149
+ const hasExplicitSibling = Number.isFinite(Number(options.siblingGap));
150
+ const explicitLevelGap = num(options.levelGap, 220);
151
+ const explicitSiblingGap = num(options.siblingGap, 140);
152
+ const edgeSiblingGap = hasExplicitSibling ? Math.max(16, explicitSiblingGap - 96) : DEFAULT_SIBLING_EDGE_GAP;
153
+ const crossSpan = (id) => {
154
+ const size = sizeOf(id);
155
+ return dir.main === "x" ? size.height : size.width;
156
+ };
157
+ const mainSpan = (id) => {
158
+ const size = sizeOf(id);
159
+ return dir.main === "x" ? size.width : size.height;
160
+ };
161
+ const subtreeSpan = /* @__PURE__ */ new Map();
162
+ const subtreeCount = /* @__PURE__ */ new Map();
163
+ const measure = (id) => {
164
+ const selfSpan = crossSpan(id);
165
+ const kids = tree.get(id) || [];
166
+ if (!kids.length) {
167
+ subtreeSpan.set(id, selfSpan);
168
+ subtreeCount.set(id, 1);
169
+ return selfSpan;
170
+ }
171
+ let childTotal = 0;
172
+ let totalCount = 1;
173
+ kids.forEach((kid, index) => {
174
+ if (index > 0) childTotal += edgeSiblingGap;
175
+ childTotal += measure(kid);
176
+ totalCount += subtreeCount.get(kid) || 1;
177
+ });
178
+ const span = Math.max(selfSpan, childTotal);
179
+ subtreeSpan.set(id, span);
180
+ subtreeCount.set(id, totalCount);
181
+ return span;
182
+ };
183
+ forestRoots.forEach(measure);
104
184
  const crossOf = /* @__PURE__ */ new Map();
105
- let cursor = 0;
106
- const place = (id) => {
185
+ const place = (id, startCross) => {
186
+ const span = subtreeSpan.get(id) || crossSpan(id);
107
187
  const kids = tree.get(id) || [];
108
188
  if (!kids.length) {
109
- crossOf.set(id, cursor);
110
- cursor += siblingGap;
111
- return crossOf.get(id);
189
+ const center2 = startCross + span / 2;
190
+ crossOf.set(id, center2);
191
+ return center2;
192
+ }
193
+ let childTotal = 0;
194
+ const kidSpans = kids.map((kid, index) => {
195
+ const kidSpan = subtreeSpan.get(kid) || crossSpan(kid);
196
+ if (index > 0) childTotal += edgeSiblingGap;
197
+ childTotal += kidSpan;
198
+ return kidSpan;
199
+ });
200
+ let cursor = startCross + (span - childTotal) / 2;
201
+ const childCenters = kids.map((kid, index) => {
202
+ const kidSpan = kidSpans[index];
203
+ const center2 = place(kid, cursor);
204
+ cursor += kidSpan + edgeSiblingGap;
205
+ return center2;
206
+ });
207
+ if (kids.length >= 2) {
208
+ const counts = kids.map((kid) => subtreeCount.get(kid) || 1);
209
+ const maxCount = Math.max(...counts);
210
+ const minCount = Math.min(...counts);
211
+ if (maxCount > minCount) {
212
+ const primaryIndex = counts.indexOf(maxCount);
213
+ const center2 = childCenters[primaryIndex];
214
+ crossOf.set(id, center2);
215
+ return center2;
216
+ }
112
217
  }
113
- const childCenters = kids.map(place);
114
218
  const center = (childCenters[0] + childCenters[childCenters.length - 1]) / 2;
115
219
  crossOf.set(id, center);
116
220
  return center;
117
221
  };
118
- forestRoots.forEach((root) => {
119
- place(root);
120
- cursor += siblingGap;
222
+ let rootCursor = 0;
223
+ forestRoots.forEach((root, index) => {
224
+ if (index > 0) rootCursor += edgeSiblingGap;
225
+ const span = subtreeSpan.get(root) || crossSpan(root);
226
+ place(root, rootCursor);
227
+ rootCursor += span;
121
228
  });
229
+ let maxDepth = 0;
230
+ const maxMainAtDepth = /* @__PURE__ */ new Map();
231
+ crossOf.forEach((_cross, id) => {
232
+ const d = depth.get(id) || 0;
233
+ if (d > maxDepth) maxDepth = d;
234
+ const current = maxMainAtDepth.get(d) || 0;
235
+ const span = mainSpan(id);
236
+ if (span > current) maxMainAtDepth.set(d, span);
237
+ });
238
+ const mainCenterAtDepth = /* @__PURE__ */ new Map([[0, 0]]);
239
+ for (let d = 1; d <= maxDepth; d += 1) {
240
+ const prevCenter = mainCenterAtDepth.get(d - 1) || 0;
241
+ const prevSpan = maxMainAtDepth.get(d - 1) || 160;
242
+ const currSpan = maxMainAtDepth.get(d) || 160;
243
+ const step = hasExplicitLevel ? Math.max(explicitLevelGap, (prevSpan + currSpan) / 2 + 24) : (prevSpan + currSpan) / 2 + DEFAULT_LEVEL_EDGE_GAP;
244
+ mainCenterAtDepth.set(d, prevCenter + step * dir.sign);
245
+ }
122
246
  const centers = /* @__PURE__ */ new Map();
123
247
  crossOf.forEach((cross, id) => {
124
- const main = (depth.get(id) || 0) * levelGap * dir.sign;
248
+ const main = mainCenterAtDepth.get(depth.get(id) || 0) || 0;
125
249
  centers.set(
126
250
  id,
127
251
  dir.main === "x" ? { centerX: main, centerY: cross } : { centerX: cross, centerY: main }
@@ -435,7 +559,7 @@ var EDGE_STROKE_PRESETS = [
435
559
  { id: "medium", label: "Medium", width: DEFAULT_EDGE_STROKE_WIDTH },
436
560
  { id: "bold", label: "Bold", width: 3.5 }
437
561
  ];
438
- var VD_FLOWCHART_VERSION = "1.5.0";
562
+ var VD_FLOWCHART_VERSION = "1.5.1";
439
563
  var FLOWCHART_DOCUMENT_VERSION = "1.3.0";
440
564
  var FLOWCHART_NODE_TYPES = [
441
565
  "rounded-rect",
@@ -1142,10 +1266,37 @@ function getOrthogonalMidpoints(fromStub, toStub, fromPort, toPort) {
1142
1266
  function getOrthogonalPoints(fromPoint, toPoint, fromPort, toPort, fromRect, toRect, metrics = routeMetrics(1)) {
1143
1267
  const fromNormal = getPortNormal(fromPort);
1144
1268
  const toNormal = getPortNormal(toPort);
1145
- const fromStub = offsetPoint(fromPoint, fromNormal, metrics.stub);
1146
- const toStub = offsetPoint(toPoint, toNormal, metrics.stub);
1147
1269
  const sourceHorizontal = isHorizontalPort(fromPort);
1148
1270
  const targetHorizontal = isHorizontalPort(toPort);
1271
+ const minStub = 4 * (metrics.scale || 1);
1272
+ let fromStubLen = metrics.stub;
1273
+ let toStubLen = metrics.stub;
1274
+ if (sourceHorizontal && targetHorizontal && fromNormal.x === -toNormal.x) {
1275
+ const span = (toPoint.x - fromPoint.x) * fromNormal.x;
1276
+ if (span > 0) {
1277
+ const fitted = Math.min(metrics.stub, span * 0.45, span / 2);
1278
+ fromStubLen = fitted;
1279
+ toStubLen = fitted;
1280
+ }
1281
+ } else if (!sourceHorizontal && !targetHorizontal && fromNormal.y === -toNormal.y) {
1282
+ const span = (toPoint.y - fromPoint.y) * fromNormal.y;
1283
+ if (span > 0) {
1284
+ const fitted = Math.min(metrics.stub, span * 0.45, span / 2);
1285
+ fromStubLen = fitted;
1286
+ toStubLen = fitted;
1287
+ }
1288
+ } else if (sourceHorizontal !== targetHorizontal) {
1289
+ const spanFrom = sourceHorizontal ? (toPoint.x - fromPoint.x) * fromNormal.x : (toPoint.y - fromPoint.y) * fromNormal.y;
1290
+ const spanTo = targetHorizontal ? (fromPoint.x - toPoint.x) * toNormal.x : (fromPoint.y - toPoint.y) * toNormal.y;
1291
+ if (spanFrom > 0 && spanFrom < metrics.stub) {
1292
+ fromStubLen = Math.min(spanFrom / 2, Math.max(minStub, spanFrom * 0.5));
1293
+ }
1294
+ if (spanTo > 0 && spanTo < metrics.stub) {
1295
+ toStubLen = Math.min(spanTo / 2, Math.max(minStub, spanTo * 0.5));
1296
+ }
1297
+ }
1298
+ const fromStub = offsetPoint(fromPoint, fromNormal, fromStubLen);
1299
+ const toStub = offsetPoint(toPoint, toNormal, toStubLen);
1149
1300
  const mids = [];
1150
1301
  if (sourceHorizontal && targetHorizontal) {
1151
1302
  const facing = fromNormal.x === -toNormal.x;
@@ -1568,6 +1719,7 @@ var VdFlowchart = class {
1568
1719
  this.handleClick = this.handleClick.bind(this);
1569
1720
  this.handleDoubleClick = this.handleDoubleClick.bind(this);
1570
1721
  this.handleWheel = this.handleWheel.bind(this);
1722
+ this.handleCanvasBlur = this.handleCanvasBlur.bind(this);
1571
1723
  this.handleKeyDown = this.handleKeyDown.bind(this);
1572
1724
  this.handleSelectionFieldInput = this.handleSelectionFieldInput.bind(this);
1573
1725
  this.handleSelectionFieldChange = this.handleSelectionFieldChange.bind(this);
@@ -1980,6 +2132,7 @@ var VdFlowchart = class {
1980
2132
  this.canvasEl.addEventListener("click", this.handleClick);
1981
2133
  this.canvasEl.addEventListener("dblclick", this.handleDoubleClick);
1982
2134
  this.canvasEl.addEventListener("wheel", this.handleWheel, { passive: false });
2135
+ this.canvasEl.addEventListener("blur", this.handleCanvasBlur);
1983
2136
  this.root.addEventListener("keydown", this.handleKeyDown, true);
1984
2137
  window.addEventListener("pointerup", this.handlePointerUp);
1985
2138
  window.addEventListener("resize", this.handleResize);
@@ -1998,6 +2151,7 @@ var VdFlowchart = class {
1998
2151
  this.canvasEl.removeEventListener("click", this.handleClick);
1999
2152
  this.canvasEl.removeEventListener("dblclick", this.handleDoubleClick);
2000
2153
  this.canvasEl.removeEventListener("wheel", this.handleWheel);
2154
+ this.canvasEl.removeEventListener("blur", this.handleCanvasBlur);
2001
2155
  this.root.removeEventListener("keydown", this.handleKeyDown, true);
2002
2156
  window.removeEventListener("pointerup", this.handlePointerUp);
2003
2157
  window.removeEventListener("resize", this.handleResize);
@@ -2006,6 +2160,19 @@ var VdFlowchart = class {
2006
2160
  if (this.destroyed) return;
2007
2161
  this.render({ inspector: false, json: false });
2008
2162
  }
2163
+ /**
2164
+ * Focus the diagram canvas. Quiet focus returns from an inline editor without
2165
+ * painting the full-canvas `:focus-visible` ring (see `.is-quiet-focus`).
2166
+ */
2167
+ focusCanvas(options = {}) {
2168
+ if (!this.canvasEl || this.destroyed) return;
2169
+ const quiet = options.quiet === true;
2170
+ this.canvasEl.classList.toggle("is-quiet-focus", quiet);
2171
+ this.canvasEl.focus({ preventScroll: true });
2172
+ }
2173
+ handleCanvasBlur() {
2174
+ this.canvasEl?.classList.remove("is-quiet-focus");
2175
+ }
2009
2176
  updatePaletteState() {
2010
2177
  const arrowArmed = this.activeTool === "arrow";
2011
2178
  this.paletteGrid.querySelectorAll(".vd-flowchart-palette-btn").forEach((button) => {
@@ -3461,7 +3628,7 @@ var VdFlowchart = class {
3461
3628
  if (event.key === "Escape" || event.key === "Enter") {
3462
3629
  event.preventDefault();
3463
3630
  this.stopTextEdit({ commit: event.key === "Enter" });
3464
- this.canvasEl.focus();
3631
+ this.focusCanvas({ quiet: true });
3465
3632
  }
3466
3633
  });
3467
3634
  textarea.addEventListener("blur", () => {
@@ -3488,7 +3655,7 @@ var VdFlowchart = class {
3488
3655
  if (event.key === "Escape") {
3489
3656
  event.preventDefault();
3490
3657
  this.stopTextEdit({ commit: false });
3491
- this.canvasEl.focus();
3658
+ this.focusCanvas({ quiet: true });
3492
3659
  return;
3493
3660
  }
3494
3661
  if (event.key === "Enter") {
@@ -3497,13 +3664,12 @@ var VdFlowchart = class {
3497
3664
  if (!commits) return;
3498
3665
  event.preventDefault();
3499
3666
  this.stopTextEdit({ commit: true });
3500
- this.canvasEl.focus();
3667
+ this.focusCanvas({ quiet: true });
3501
3668
  return;
3502
3669
  }
3503
3670
  if (event.key === "Tab" && mindmap && !event.shiftKey && !event.altKey && !event.ctrlKey) {
3504
3671
  event.preventDefault();
3505
3672
  this.stopTextEdit({ commit: true });
3506
- this.canvasEl.focus();
3507
3673
  this.insertBranchNode(node.id, { edit: true });
3508
3674
  }
3509
3675
  }
@@ -4811,7 +4977,7 @@ var VdFlowchart = class {
4811
4977
  changed = true;
4812
4978
  }
4813
4979
  });
4814
- const rerouted = options.reroutePorts === false ? false : this.rerouteEdgePorts();
4980
+ const rerouted = options.reroutePorts === false ? false : this.rerouteEdgePorts(resolvedMode === "tree" ? options.direction || "right" : null);
4815
4981
  if (!changed && !rerouted) return this;
4816
4982
  this.render();
4817
4983
  this.emitChange("layout", { mode: resolvedMode });
@@ -4824,8 +4990,16 @@ var VdFlowchart = class {
4824
4990
  // Re-pick each edge's from/to port from the nodes' current centers, so a
4825
4991
  // freshly laid-out graph attaches connectors on sensible sides. Returns
4826
4992
  // whether any port actually changed.
4827
- rerouteEdgePorts() {
4993
+ rerouteEdgePorts(flowDirection = null) {
4828
4994
  let changed = false;
4995
+ const flow = flowDirection === "down" || flowDirection === "up" || flowDirection === "left" || flowDirection === "right" ? flowDirection : null;
4996
+ const outDegree = /* @__PURE__ */ new Map();
4997
+ if (flow) {
4998
+ this.documentData.edges.forEach((edge) => {
4999
+ const fromId = edge?.from?.nodeId;
5000
+ if (fromId) outDegree.set(fromId, (outDegree.get(fromId) || 0) + 1);
5001
+ });
5002
+ }
4829
5003
  this.documentData.edges.forEach((edge) => {
4830
5004
  const fromNode = this.findNode(edge.from.nodeId);
4831
5005
  const toNode = this.findNode(edge.to.nodeId);
@@ -4835,8 +5009,82 @@ var VdFlowchart = class {
4835
5009
  y: fromNode.y + fromNode.height / 2
4836
5010
  };
4837
5011
  const toCenter = { x: toNode.x + toNode.width / 2, y: toNode.y + toNode.height / 2 };
4838
- const nextFrom = getPortByDirection(fromNode, toCenter).port;
4839
- const nextTo = getPortByDirection(toNode, fromCenter).port;
5012
+ let nextFrom;
5013
+ let nextTo;
5014
+ if (flow) {
5015
+ const dx = toCenter.x - fromCenter.x;
5016
+ const dy = toCenter.y - fromCenter.y;
5017
+ const vertical = flow === "down" || flow === "up";
5018
+ const sign = flow === "up" || flow === "left" ? -1 : 1;
5019
+ const mainDelta = (vertical ? dy : dx) * sign;
5020
+ const crossDelta = vertical ? dx : dy;
5021
+ const isDiamondBranch = fromNode.type === "diamond" && (outDegree.get(fromNode.id) || 0) >= 2;
5022
+ if (mainDelta < -8) {
5023
+ const allNodes = this.documentData.nodes;
5024
+ if (vertical) {
5025
+ const minX = Math.min(fromNode.x, toNode.x);
5026
+ const maxRight = Math.max(fromNode.x + fromNode.width, toNode.x + toNode.width);
5027
+ const minY = Math.min(fromCenter.y, toCenter.y) - 8;
5028
+ const maxY = Math.max(fromCenter.y, toCenter.y) + 8;
5029
+ let rightCount = 0;
5030
+ let leftCount = 0;
5031
+ allNodes.forEach((n) => {
5032
+ if (!n || n.id === fromNode.id || n.id === toNode.id) return;
5033
+ const ny = n.y + n.height / 2;
5034
+ if (ny < minY || ny > maxY) return;
5035
+ const nx = n.x + n.width / 2;
5036
+ if (nx >= maxRight - 20) rightCount += 1;
5037
+ if (nx <= minX + 20) leftCount += 1;
5038
+ });
5039
+ const side = leftCount < rightCount ? "left" : rightCount < leftCount ? "right" : crossDelta < -8 ? "left" : "right";
5040
+ nextFrom = side;
5041
+ nextTo = side;
5042
+ } else {
5043
+ const minY = Math.min(fromNode.y, toNode.y);
5044
+ const maxBottom = Math.max(fromNode.y + fromNode.height, toNode.y + toNode.height);
5045
+ const minX = Math.min(fromCenter.x, toCenter.x) - 8;
5046
+ const maxX = Math.max(fromCenter.x, toCenter.x) + 8;
5047
+ let bottomCount = 0;
5048
+ let topCount = 0;
5049
+ allNodes.forEach((n) => {
5050
+ if (!n || n.id === fromNode.id || n.id === toNode.id) return;
5051
+ const nx = n.x + n.width / 2;
5052
+ if (nx < minX || nx > maxX) return;
5053
+ const ny = n.y + n.height / 2;
5054
+ if (ny >= maxBottom - 20) bottomCount += 1;
5055
+ if (ny <= minY + 20) topCount += 1;
5056
+ });
5057
+ const side = topCount < bottomCount ? "top" : bottomCount < topCount ? "bottom" : crossDelta < -8 ? "top" : "bottom";
5058
+ nextFrom = side;
5059
+ nextTo = side;
5060
+ }
5061
+ } else if (mainDelta >= 12) {
5062
+ if (vertical) {
5063
+ nextTo = sign > 0 ? "top" : "bottom";
5064
+ if (isDiamondBranch && Math.abs(crossDelta) >= fromNode.width * 0.45) {
5065
+ nextFrom = crossDelta > 0 ? "right" : "left";
5066
+ } else {
5067
+ nextFrom = sign > 0 ? "bottom" : "top";
5068
+ }
5069
+ } else {
5070
+ nextTo = sign > 0 ? "left" : "right";
5071
+ if (isDiamondBranch && Math.abs(crossDelta) >= fromNode.height * 0.45) {
5072
+ nextFrom = crossDelta > 0 ? "bottom" : "top";
5073
+ } else {
5074
+ nextFrom = sign > 0 ? "right" : "left";
5075
+ }
5076
+ }
5077
+ } else if (vertical) {
5078
+ nextFrom = dx >= 0 ? "right" : "left";
5079
+ nextTo = dx >= 0 ? "left" : "right";
5080
+ } else {
5081
+ nextFrom = dy >= 0 ? "bottom" : "top";
5082
+ nextTo = dy >= 0 ? "top" : "bottom";
5083
+ }
5084
+ } else {
5085
+ nextFrom = getPortByDirection(fromNode, toCenter).port;
5086
+ nextTo = getPortByDirection(toNode, fromCenter).port;
5087
+ }
4840
5088
  if (nextFrom !== edge.from.port || nextTo !== edge.to.port) {
4841
5089
  edge.from.port = nextFrom;
4842
5090
  edge.to.port = nextTo;