@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/dist/index.js CHANGED
@@ -8,6 +8,8 @@ var DIRECTIONS = {
8
8
  down: { main: "y", sign: 1 },
9
9
  up: { main: "y", sign: -1 }
10
10
  };
11
+ var DEFAULT_LEVEL_EDGE_GAP = 88;
12
+ var DEFAULT_SIBLING_EDGE_GAP = 64;
11
13
  function num(value, fallback) {
12
14
  const next = Number(value);
13
15
  return Number.isFinite(next) ? next : fallback;
@@ -32,60 +34,182 @@ function pickRoots(nodes, indegree, optRoot) {
32
34
  }
33
35
  function spanningForest(allIds, roots, children) {
34
36
  const visited = /* @__PURE__ */ new Set();
35
- const depth = /* @__PURE__ */ new Map();
36
- const tree = /* @__PURE__ */ new Map();
37
+ const active = /* @__PURE__ */ new Set();
37
38
  const forestRoots = [];
38
- const walk = (id, d) => {
39
+ const forwardParents = new Map(allIds.map((id) => [id, []]));
40
+ const dfs = (id) => {
39
41
  visited.add(id);
40
- depth.set(id, d);
41
- const kids = [];
42
+ active.add(id);
42
43
  (children.get(id) || []).forEach((child) => {
43
- if (!visited.has(child)) {
44
- kids.push(child);
45
- walk(child, d + 1);
46
- }
44
+ if (active.has(child)) return;
45
+ const list = forwardParents.get(child);
46
+ if (list && !list.includes(id)) list.push(id);
47
+ if (!visited.has(child)) dfs(child);
47
48
  });
48
- tree.set(id, kids);
49
+ active.delete(id);
49
50
  };
50
51
  roots.forEach((root) => {
51
52
  if (!visited.has(root)) {
52
53
  forestRoots.push(root);
53
- walk(root, 0);
54
+ dfs(root);
54
55
  }
55
56
  });
56
57
  allIds.forEach((id) => {
57
58
  if (!visited.has(id)) {
58
59
  forestRoots.push(id);
59
- walk(id, 0);
60
+ dfs(id);
60
61
  }
61
62
  });
63
+ const depth = new Map(allIds.map((id) => [id, 0]));
64
+ const limit = Math.max(1, allIds.length);
65
+ for (let pass = 0; pass < limit; pass += 1) {
66
+ let changed = false;
67
+ for (const id of allIds) {
68
+ const parents = forwardParents.get(id) || [];
69
+ for (const parent of parents) {
70
+ const nextDepth = Math.min(limit, (depth.get(parent) || 0) + 1);
71
+ if (nextDepth > (depth.get(id) || 0)) {
72
+ depth.set(id, nextDepth);
73
+ changed = true;
74
+ }
75
+ }
76
+ }
77
+ if (!changed) break;
78
+ }
79
+ const tree = new Map(allIds.map((id) => [id, []]));
80
+ const rootSet = new Set(forestRoots);
81
+ const chosenParent = /* @__PURE__ */ new Map();
82
+ for (const id of allIds) {
83
+ if (rootSet.has(id)) continue;
84
+ const parents = forwardParents.get(id) || [];
85
+ if (!parents.length) continue;
86
+ let bestParent = parents[0];
87
+ let bestDepth = depth.get(bestParent) ?? -1;
88
+ for (let index = 1; index < parents.length; index += 1) {
89
+ const candidate = parents[index];
90
+ const candidateDepth = depth.get(candidate) ?? -1;
91
+ if (candidateDepth > bestDepth) {
92
+ bestDepth = candidateDepth;
93
+ bestParent = candidate;
94
+ }
95
+ }
96
+ chosenParent.set(id, bestParent);
97
+ }
98
+ const placedChild = /* @__PURE__ */ new Set();
99
+ for (const parentId of allIds) {
100
+ const out = tree.get(parentId);
101
+ for (const childId of children.get(parentId) || []) {
102
+ if (chosenParent.get(childId) === parentId && !placedChild.has(childId)) {
103
+ placedChild.add(childId);
104
+ out.push(childId);
105
+ }
106
+ }
107
+ }
62
108
  return { depth, tree, forestRoots };
63
109
  }
64
110
  function treeCenters(forestRoots, tree, depth, sizeOf, options) {
65
111
  const dir = DIRECTIONS[options.direction] || DIRECTIONS.right;
66
- const levelGap = num(options.levelGap, 220);
67
- const siblingGap = num(options.siblingGap, 140);
112
+ const hasExplicitLevel = Number.isFinite(Number(options.levelGap));
113
+ const hasExplicitSibling = Number.isFinite(Number(options.siblingGap));
114
+ const explicitLevelGap = num(options.levelGap, 220);
115
+ const explicitSiblingGap = num(options.siblingGap, 140);
116
+ const edgeSiblingGap = hasExplicitSibling ? Math.max(16, explicitSiblingGap - 96) : DEFAULT_SIBLING_EDGE_GAP;
117
+ const crossSpan = (id) => {
118
+ const size = sizeOf(id);
119
+ return dir.main === "x" ? size.height : size.width;
120
+ };
121
+ const mainSpan = (id) => {
122
+ const size = sizeOf(id);
123
+ return dir.main === "x" ? size.width : size.height;
124
+ };
125
+ const subtreeSpan = /* @__PURE__ */ new Map();
126
+ const subtreeCount = /* @__PURE__ */ new Map();
127
+ const measure = (id) => {
128
+ const selfSpan = crossSpan(id);
129
+ const kids = tree.get(id) || [];
130
+ if (!kids.length) {
131
+ subtreeSpan.set(id, selfSpan);
132
+ subtreeCount.set(id, 1);
133
+ return selfSpan;
134
+ }
135
+ let childTotal = 0;
136
+ let totalCount = 1;
137
+ kids.forEach((kid, index) => {
138
+ if (index > 0) childTotal += edgeSiblingGap;
139
+ childTotal += measure(kid);
140
+ totalCount += subtreeCount.get(kid) || 1;
141
+ });
142
+ const span = Math.max(selfSpan, childTotal);
143
+ subtreeSpan.set(id, span);
144
+ subtreeCount.set(id, totalCount);
145
+ return span;
146
+ };
147
+ forestRoots.forEach(measure);
68
148
  const crossOf = /* @__PURE__ */ new Map();
69
- let cursor = 0;
70
- const place = (id) => {
149
+ const place = (id, startCross) => {
150
+ const span = subtreeSpan.get(id) || crossSpan(id);
71
151
  const kids = tree.get(id) || [];
72
152
  if (!kids.length) {
73
- crossOf.set(id, cursor);
74
- cursor += siblingGap;
75
- return crossOf.get(id);
153
+ const center2 = startCross + span / 2;
154
+ crossOf.set(id, center2);
155
+ return center2;
156
+ }
157
+ let childTotal = 0;
158
+ const kidSpans = kids.map((kid, index) => {
159
+ const kidSpan = subtreeSpan.get(kid) || crossSpan(kid);
160
+ if (index > 0) childTotal += edgeSiblingGap;
161
+ childTotal += kidSpan;
162
+ return kidSpan;
163
+ });
164
+ let cursor = startCross + (span - childTotal) / 2;
165
+ const childCenters = kids.map((kid, index) => {
166
+ const kidSpan = kidSpans[index];
167
+ const center2 = place(kid, cursor);
168
+ cursor += kidSpan + edgeSiblingGap;
169
+ return center2;
170
+ });
171
+ if (kids.length >= 2) {
172
+ const counts = kids.map((kid) => subtreeCount.get(kid) || 1);
173
+ const maxCount = Math.max(...counts);
174
+ const minCount = Math.min(...counts);
175
+ if (maxCount > minCount) {
176
+ const primaryIndex = counts.indexOf(maxCount);
177
+ const center2 = childCenters[primaryIndex];
178
+ crossOf.set(id, center2);
179
+ return center2;
180
+ }
76
181
  }
77
- const childCenters = kids.map(place);
78
182
  const center = (childCenters[0] + childCenters[childCenters.length - 1]) / 2;
79
183
  crossOf.set(id, center);
80
184
  return center;
81
185
  };
82
- forestRoots.forEach((root) => {
83
- place(root);
84
- cursor += siblingGap;
186
+ let rootCursor = 0;
187
+ forestRoots.forEach((root, index) => {
188
+ if (index > 0) rootCursor += edgeSiblingGap;
189
+ const span = subtreeSpan.get(root) || crossSpan(root);
190
+ place(root, rootCursor);
191
+ rootCursor += span;
85
192
  });
193
+ let maxDepth = 0;
194
+ const maxMainAtDepth = /* @__PURE__ */ new Map();
195
+ crossOf.forEach((_cross, id) => {
196
+ const d = depth.get(id) || 0;
197
+ if (d > maxDepth) maxDepth = d;
198
+ const current = maxMainAtDepth.get(d) || 0;
199
+ const span = mainSpan(id);
200
+ if (span > current) maxMainAtDepth.set(d, span);
201
+ });
202
+ const mainCenterAtDepth = /* @__PURE__ */ new Map([[0, 0]]);
203
+ for (let d = 1; d <= maxDepth; d += 1) {
204
+ const prevCenter = mainCenterAtDepth.get(d - 1) || 0;
205
+ const prevSpan = maxMainAtDepth.get(d - 1) || 160;
206
+ const currSpan = maxMainAtDepth.get(d) || 160;
207
+ const step = hasExplicitLevel ? Math.max(explicitLevelGap, (prevSpan + currSpan) / 2 + 24) : (prevSpan + currSpan) / 2 + DEFAULT_LEVEL_EDGE_GAP;
208
+ mainCenterAtDepth.set(d, prevCenter + step * dir.sign);
209
+ }
86
210
  const centers = /* @__PURE__ */ new Map();
87
211
  crossOf.forEach((cross, id) => {
88
- const main = (depth.get(id) || 0) * levelGap * dir.sign;
212
+ const main = mainCenterAtDepth.get(depth.get(id) || 0) || 0;
89
213
  centers.set(
90
214
  id,
91
215
  dir.main === "x" ? { centerX: main, centerY: cross } : { centerX: cross, centerY: main }
@@ -399,7 +523,7 @@ var EDGE_STROKE_PRESETS = [
399
523
  { id: "medium", label: "Medium", width: DEFAULT_EDGE_STROKE_WIDTH },
400
524
  { id: "bold", label: "Bold", width: 3.5 }
401
525
  ];
402
- var VD_FLOWCHART_VERSION = "1.5.0";
526
+ var VD_FLOWCHART_VERSION = "1.5.1";
403
527
  var FLOWCHART_DOCUMENT_VERSION = "1.3.0";
404
528
  var FLOWCHART_NODE_TYPES = [
405
529
  "rounded-rect",
@@ -1106,10 +1230,37 @@ function getOrthogonalMidpoints(fromStub, toStub, fromPort, toPort) {
1106
1230
  function getOrthogonalPoints(fromPoint, toPoint, fromPort, toPort, fromRect, toRect, metrics = routeMetrics(1)) {
1107
1231
  const fromNormal = getPortNormal(fromPort);
1108
1232
  const toNormal = getPortNormal(toPort);
1109
- const fromStub = offsetPoint(fromPoint, fromNormal, metrics.stub);
1110
- const toStub = offsetPoint(toPoint, toNormal, metrics.stub);
1111
1233
  const sourceHorizontal = isHorizontalPort(fromPort);
1112
1234
  const targetHorizontal = isHorizontalPort(toPort);
1235
+ const minStub = 4 * (metrics.scale || 1);
1236
+ let fromStubLen = metrics.stub;
1237
+ let toStubLen = metrics.stub;
1238
+ if (sourceHorizontal && targetHorizontal && fromNormal.x === -toNormal.x) {
1239
+ const span = (toPoint.x - fromPoint.x) * fromNormal.x;
1240
+ if (span > 0) {
1241
+ const fitted = Math.min(metrics.stub, span * 0.45, span / 2);
1242
+ fromStubLen = fitted;
1243
+ toStubLen = fitted;
1244
+ }
1245
+ } else if (!sourceHorizontal && !targetHorizontal && fromNormal.y === -toNormal.y) {
1246
+ const span = (toPoint.y - fromPoint.y) * fromNormal.y;
1247
+ if (span > 0) {
1248
+ const fitted = Math.min(metrics.stub, span * 0.45, span / 2);
1249
+ fromStubLen = fitted;
1250
+ toStubLen = fitted;
1251
+ }
1252
+ } else if (sourceHorizontal !== targetHorizontal) {
1253
+ const spanFrom = sourceHorizontal ? (toPoint.x - fromPoint.x) * fromNormal.x : (toPoint.y - fromPoint.y) * fromNormal.y;
1254
+ const spanTo = targetHorizontal ? (fromPoint.x - toPoint.x) * toNormal.x : (fromPoint.y - toPoint.y) * toNormal.y;
1255
+ if (spanFrom > 0 && spanFrom < metrics.stub) {
1256
+ fromStubLen = Math.min(spanFrom / 2, Math.max(minStub, spanFrom * 0.5));
1257
+ }
1258
+ if (spanTo > 0 && spanTo < metrics.stub) {
1259
+ toStubLen = Math.min(spanTo / 2, Math.max(minStub, spanTo * 0.5));
1260
+ }
1261
+ }
1262
+ const fromStub = offsetPoint(fromPoint, fromNormal, fromStubLen);
1263
+ const toStub = offsetPoint(toPoint, toNormal, toStubLen);
1113
1264
  const mids = [];
1114
1265
  if (sourceHorizontal && targetHorizontal) {
1115
1266
  const facing = fromNormal.x === -toNormal.x;
@@ -1532,6 +1683,7 @@ var VdFlowchart = class {
1532
1683
  this.handleClick = this.handleClick.bind(this);
1533
1684
  this.handleDoubleClick = this.handleDoubleClick.bind(this);
1534
1685
  this.handleWheel = this.handleWheel.bind(this);
1686
+ this.handleCanvasBlur = this.handleCanvasBlur.bind(this);
1535
1687
  this.handleKeyDown = this.handleKeyDown.bind(this);
1536
1688
  this.handleSelectionFieldInput = this.handleSelectionFieldInput.bind(this);
1537
1689
  this.handleSelectionFieldChange = this.handleSelectionFieldChange.bind(this);
@@ -1944,6 +2096,7 @@ var VdFlowchart = class {
1944
2096
  this.canvasEl.addEventListener("click", this.handleClick);
1945
2097
  this.canvasEl.addEventListener("dblclick", this.handleDoubleClick);
1946
2098
  this.canvasEl.addEventListener("wheel", this.handleWheel, { passive: false });
2099
+ this.canvasEl.addEventListener("blur", this.handleCanvasBlur);
1947
2100
  this.root.addEventListener("keydown", this.handleKeyDown, true);
1948
2101
  window.addEventListener("pointerup", this.handlePointerUp);
1949
2102
  window.addEventListener("resize", this.handleResize);
@@ -1962,6 +2115,7 @@ var VdFlowchart = class {
1962
2115
  this.canvasEl.removeEventListener("click", this.handleClick);
1963
2116
  this.canvasEl.removeEventListener("dblclick", this.handleDoubleClick);
1964
2117
  this.canvasEl.removeEventListener("wheel", this.handleWheel);
2118
+ this.canvasEl.removeEventListener("blur", this.handleCanvasBlur);
1965
2119
  this.root.removeEventListener("keydown", this.handleKeyDown, true);
1966
2120
  window.removeEventListener("pointerup", this.handlePointerUp);
1967
2121
  window.removeEventListener("resize", this.handleResize);
@@ -1970,6 +2124,19 @@ var VdFlowchart = class {
1970
2124
  if (this.destroyed) return;
1971
2125
  this.render({ inspector: false, json: false });
1972
2126
  }
2127
+ /**
2128
+ * Focus the diagram canvas. Quiet focus returns from an inline editor without
2129
+ * painting the full-canvas `:focus-visible` ring (see `.is-quiet-focus`).
2130
+ */
2131
+ focusCanvas(options = {}) {
2132
+ if (!this.canvasEl || this.destroyed) return;
2133
+ const quiet = options.quiet === true;
2134
+ this.canvasEl.classList.toggle("is-quiet-focus", quiet);
2135
+ this.canvasEl.focus({ preventScroll: true });
2136
+ }
2137
+ handleCanvasBlur() {
2138
+ this.canvasEl?.classList.remove("is-quiet-focus");
2139
+ }
1973
2140
  updatePaletteState() {
1974
2141
  const arrowArmed = this.activeTool === "arrow";
1975
2142
  this.paletteGrid.querySelectorAll(".vd-flowchart-palette-btn").forEach((button) => {
@@ -3425,7 +3592,7 @@ var VdFlowchart = class {
3425
3592
  if (event.key === "Escape" || event.key === "Enter") {
3426
3593
  event.preventDefault();
3427
3594
  this.stopTextEdit({ commit: event.key === "Enter" });
3428
- this.canvasEl.focus();
3595
+ this.focusCanvas({ quiet: true });
3429
3596
  }
3430
3597
  });
3431
3598
  textarea.addEventListener("blur", () => {
@@ -3452,7 +3619,7 @@ var VdFlowchart = class {
3452
3619
  if (event.key === "Escape") {
3453
3620
  event.preventDefault();
3454
3621
  this.stopTextEdit({ commit: false });
3455
- this.canvasEl.focus();
3622
+ this.focusCanvas({ quiet: true });
3456
3623
  return;
3457
3624
  }
3458
3625
  if (event.key === "Enter") {
@@ -3461,13 +3628,12 @@ var VdFlowchart = class {
3461
3628
  if (!commits) return;
3462
3629
  event.preventDefault();
3463
3630
  this.stopTextEdit({ commit: true });
3464
- this.canvasEl.focus();
3631
+ this.focusCanvas({ quiet: true });
3465
3632
  return;
3466
3633
  }
3467
3634
  if (event.key === "Tab" && mindmap && !event.shiftKey && !event.altKey && !event.ctrlKey) {
3468
3635
  event.preventDefault();
3469
3636
  this.stopTextEdit({ commit: true });
3470
- this.canvasEl.focus();
3471
3637
  this.insertBranchNode(node.id, { edit: true });
3472
3638
  }
3473
3639
  }
@@ -4775,7 +4941,7 @@ var VdFlowchart = class {
4775
4941
  changed = true;
4776
4942
  }
4777
4943
  });
4778
- const rerouted = options.reroutePorts === false ? false : this.rerouteEdgePorts();
4944
+ const rerouted = options.reroutePorts === false ? false : this.rerouteEdgePorts(resolvedMode === "tree" ? options.direction || "right" : null);
4779
4945
  if (!changed && !rerouted) return this;
4780
4946
  this.render();
4781
4947
  this.emitChange("layout", { mode: resolvedMode });
@@ -4788,8 +4954,16 @@ var VdFlowchart = class {
4788
4954
  // Re-pick each edge's from/to port from the nodes' current centers, so a
4789
4955
  // freshly laid-out graph attaches connectors on sensible sides. Returns
4790
4956
  // whether any port actually changed.
4791
- rerouteEdgePorts() {
4957
+ rerouteEdgePorts(flowDirection = null) {
4792
4958
  let changed = false;
4959
+ const flow = flowDirection === "down" || flowDirection === "up" || flowDirection === "left" || flowDirection === "right" ? flowDirection : null;
4960
+ const outDegree = /* @__PURE__ */ new Map();
4961
+ if (flow) {
4962
+ this.documentData.edges.forEach((edge) => {
4963
+ const fromId = edge?.from?.nodeId;
4964
+ if (fromId) outDegree.set(fromId, (outDegree.get(fromId) || 0) + 1);
4965
+ });
4966
+ }
4793
4967
  this.documentData.edges.forEach((edge) => {
4794
4968
  const fromNode = this.findNode(edge.from.nodeId);
4795
4969
  const toNode = this.findNode(edge.to.nodeId);
@@ -4799,8 +4973,82 @@ var VdFlowchart = class {
4799
4973
  y: fromNode.y + fromNode.height / 2
4800
4974
  };
4801
4975
  const toCenter = { x: toNode.x + toNode.width / 2, y: toNode.y + toNode.height / 2 };
4802
- const nextFrom = getPortByDirection(fromNode, toCenter).port;
4803
- const nextTo = getPortByDirection(toNode, fromCenter).port;
4976
+ let nextFrom;
4977
+ let nextTo;
4978
+ if (flow) {
4979
+ const dx = toCenter.x - fromCenter.x;
4980
+ const dy = toCenter.y - fromCenter.y;
4981
+ const vertical = flow === "down" || flow === "up";
4982
+ const sign = flow === "up" || flow === "left" ? -1 : 1;
4983
+ const mainDelta = (vertical ? dy : dx) * sign;
4984
+ const crossDelta = vertical ? dx : dy;
4985
+ const isDiamondBranch = fromNode.type === "diamond" && (outDegree.get(fromNode.id) || 0) >= 2;
4986
+ if (mainDelta < -8) {
4987
+ const allNodes = this.documentData.nodes;
4988
+ if (vertical) {
4989
+ const minX = Math.min(fromNode.x, toNode.x);
4990
+ const maxRight = Math.max(fromNode.x + fromNode.width, toNode.x + toNode.width);
4991
+ const minY = Math.min(fromCenter.y, toCenter.y) - 8;
4992
+ const maxY = Math.max(fromCenter.y, toCenter.y) + 8;
4993
+ let rightCount = 0;
4994
+ let leftCount = 0;
4995
+ allNodes.forEach((n) => {
4996
+ if (!n || n.id === fromNode.id || n.id === toNode.id) return;
4997
+ const ny = n.y + n.height / 2;
4998
+ if (ny < minY || ny > maxY) return;
4999
+ const nx = n.x + n.width / 2;
5000
+ if (nx >= maxRight - 20) rightCount += 1;
5001
+ if (nx <= minX + 20) leftCount += 1;
5002
+ });
5003
+ const side = leftCount < rightCount ? "left" : rightCount < leftCount ? "right" : crossDelta < -8 ? "left" : "right";
5004
+ nextFrom = side;
5005
+ nextTo = side;
5006
+ } else {
5007
+ const minY = Math.min(fromNode.y, toNode.y);
5008
+ const maxBottom = Math.max(fromNode.y + fromNode.height, toNode.y + toNode.height);
5009
+ const minX = Math.min(fromCenter.x, toCenter.x) - 8;
5010
+ const maxX = Math.max(fromCenter.x, toCenter.x) + 8;
5011
+ let bottomCount = 0;
5012
+ let topCount = 0;
5013
+ allNodes.forEach((n) => {
5014
+ if (!n || n.id === fromNode.id || n.id === toNode.id) return;
5015
+ const nx = n.x + n.width / 2;
5016
+ if (nx < minX || nx > maxX) return;
5017
+ const ny = n.y + n.height / 2;
5018
+ if (ny >= maxBottom - 20) bottomCount += 1;
5019
+ if (ny <= minY + 20) topCount += 1;
5020
+ });
5021
+ const side = topCount < bottomCount ? "top" : bottomCount < topCount ? "bottom" : crossDelta < -8 ? "top" : "bottom";
5022
+ nextFrom = side;
5023
+ nextTo = side;
5024
+ }
5025
+ } else if (mainDelta >= 12) {
5026
+ if (vertical) {
5027
+ nextTo = sign > 0 ? "top" : "bottom";
5028
+ if (isDiamondBranch && Math.abs(crossDelta) >= fromNode.width * 0.45) {
5029
+ nextFrom = crossDelta > 0 ? "right" : "left";
5030
+ } else {
5031
+ nextFrom = sign > 0 ? "bottom" : "top";
5032
+ }
5033
+ } else {
5034
+ nextTo = sign > 0 ? "left" : "right";
5035
+ if (isDiamondBranch && Math.abs(crossDelta) >= fromNode.height * 0.45) {
5036
+ nextFrom = crossDelta > 0 ? "bottom" : "top";
5037
+ } else {
5038
+ nextFrom = sign > 0 ? "right" : "left";
5039
+ }
5040
+ }
5041
+ } else if (vertical) {
5042
+ nextFrom = dx >= 0 ? "right" : "left";
5043
+ nextTo = dx >= 0 ? "left" : "right";
5044
+ } else {
5045
+ nextFrom = dy >= 0 ? "bottom" : "top";
5046
+ nextTo = dy >= 0 ? "top" : "bottom";
5047
+ }
5048
+ } else {
5049
+ nextFrom = getPortByDirection(fromNode, toCenter).port;
5050
+ nextTo = getPortByDirection(toNode, fromCenter).port;
5051
+ }
4804
5052
  if (nextFrom !== edge.from.port || nextTo !== edge.to.port) {
4805
5053
  edge.from.port = nextFrom;
4806
5054
  edge.to.port = nextTo;