@vanduo-oss/vd3-flowchart 1.4.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.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;
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);
121
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 }
@@ -398,11 +522,12 @@ function createToolbarButton({ action, label, icon, disabled = false }) {
398
522
  return button;
399
523
  }
400
524
  var DEFAULT_GRID_SIZE = 24;
401
- var MIN_SCALE = 0.35;
402
- var MAX_SCALE = 3;
403
- var MIN_NODE_SIZE = 56;
404
- var MAX_NODE_SIZE = 420;
405
- var WORLD_EXTENT = 12e3;
525
+ var DEFAULT_MIN_SCALE = 0.35;
526
+ var DEFAULT_MAX_SCALE = 3;
527
+ var DEFAULT_MIN_NODE_SIZE = 56;
528
+ var DEFAULT_MAX_NODE_SIZE = 420;
529
+ var DEFAULT_WORLD_EXTENT = 12e3;
530
+ var WIDE_WORLD_EXTENT = 24e4;
406
531
  var MAX_NODES = 1e4;
407
532
  var MAX_EDGES = 1e4;
408
533
  var RESIZE_HANDLES = ["n", "ne", "e", "se", "s", "sw", "w", "nw"];
@@ -434,7 +559,7 @@ var EDGE_STROKE_PRESETS = [
434
559
  { id: "medium", label: "Medium", width: DEFAULT_EDGE_STROKE_WIDTH },
435
560
  { id: "bold", label: "Bold", width: 3.5 }
436
561
  ];
437
- var VD_FLOWCHART_VERSION = "1.4.0";
562
+ var VD_FLOWCHART_VERSION = "1.5.1";
438
563
  var FLOWCHART_DOCUMENT_VERSION = "1.3.0";
439
564
  var FLOWCHART_NODE_TYPES = [
440
565
  "rounded-rect",
@@ -575,6 +700,17 @@ function deepClone(value) {
575
700
  function formatNumber(value) {
576
701
  return Number(value.toFixed(2));
577
702
  }
703
+ function formatScale(value) {
704
+ return Number(toFiniteNumber(value, 1).toFixed(4));
705
+ }
706
+ function normalizeBoundPair(min, max, fallbackMin, fallbackMax) {
707
+ const low = Number(min);
708
+ const high = Number(max);
709
+ if (!Number.isFinite(low) || !Number.isFinite(high) || low <= 0 || high <= 0 || low >= high) {
710
+ return { min: fallbackMin, max: fallbackMax };
711
+ }
712
+ return { min: low, max: high };
713
+ }
578
714
  function sanitizeId(value) {
579
715
  if (value == null) return "";
580
716
  return String(value).trim();
@@ -632,21 +768,36 @@ function normalizeNodeType(type) {
632
768
  function getNodeSpec(type) {
633
769
  return DEFAULT_NODE_SPECS[normalizeNodeType(type)];
634
770
  }
635
- function getNodeSizeBounds(type) {
771
+ function nodeVisualScale(node) {
772
+ if (!node || node.type === "junction") return 1;
773
+ const spec = getNodeSpec(node.type);
774
+ const width = spec.width > 0 ? toFiniteNumber(node.width, spec.width) / spec.width : 1;
775
+ const height = spec.height > 0 ? toFiniteNumber(node.height, spec.height) / spec.height : 1;
776
+ return clamp(Math.min(1, width, height), 0.01, 1);
777
+ }
778
+ function edgeVisualScale(fromNode, toNode) {
779
+ if (fromNode && toNode) return Math.min(nodeVisualScale(fromNode), nodeVisualScale(toNode));
780
+ if (fromNode) return nodeVisualScale(fromNode);
781
+ if (toNode) return nodeVisualScale(toNode);
782
+ return 1;
783
+ }
784
+ function getNodeSizeBounds(type, sizeBounds) {
636
785
  const spec = getNodeSpec(type);
786
+ const min = sizeBounds?.min ?? DEFAULT_MIN_NODE_SIZE;
787
+ const max = sizeBounds?.max ?? DEFAULT_MAX_NODE_SIZE;
637
788
  return {
638
- minWidth: spec.minWidth ?? MIN_NODE_SIZE,
639
- minHeight: spec.minHeight ?? MIN_NODE_SIZE,
640
- maxWidth: spec.maxWidth ?? MAX_NODE_SIZE,
641
- maxHeight: spec.maxHeight ?? MAX_NODE_SIZE
789
+ minWidth: spec.minWidth ?? min,
790
+ minHeight: spec.minHeight ?? min,
791
+ maxWidth: spec.maxWidth ?? max,
792
+ maxHeight: spec.maxHeight ?? max
642
793
  };
643
794
  }
644
- function clampNodeWidth(type, width, fallback) {
645
- const bounds = getNodeSizeBounds(type);
795
+ function clampNodeWidth(type, width, fallback, sizeBounds) {
796
+ const bounds = getNodeSizeBounds(type, sizeBounds);
646
797
  return clamp(toFiniteNumber(width, fallback), bounds.minWidth, bounds.maxWidth);
647
798
  }
648
- function clampNodeHeight(type, height, fallback) {
649
- const bounds = getNodeSizeBounds(type);
799
+ function clampNodeHeight(type, height, fallback, sizeBounds) {
800
+ const bounds = getNodeSizeBounds(type, sizeBounds);
650
801
  return clamp(toFiniteNumber(height, fallback), bounds.minHeight, bounds.maxHeight);
651
802
  }
652
803
  function isNodeResizable(nodeOrType) {
@@ -670,14 +821,16 @@ function getStrokePresetWidth(presetId) {
670
821
  const preset = EDGE_STROKE_PRESETS.find((item) => item.id === presetId);
671
822
  return preset?.width ?? DEFAULT_EDGE_STROKE_WIDTH;
672
823
  }
673
- function normalizeViewport(viewport) {
824
+ function normalizeViewport(viewport, scaleBounds) {
825
+ const min = scaleBounds?.min ?? DEFAULT_MIN_SCALE;
826
+ const max = scaleBounds?.max ?? DEFAULT_MAX_SCALE;
674
827
  return {
675
828
  x: toFiniteNumber(viewport?.x, 0),
676
829
  y: toFiniteNumber(viewport?.y, 0),
677
- scale: clamp(toFiniteNumber(viewport?.scale, 1), MIN_SCALE, MAX_SCALE)
830
+ scale: clamp(formatScale(toFiniteNumber(viewport?.scale, 1)), min, max)
678
831
  };
679
832
  }
680
- function normalizeNode(rawNode, index, usedIds) {
833
+ function normalizeNode(rawNode, index, usedIds, sizeBounds) {
681
834
  const type = normalizeNodeType(rawNode?.type);
682
835
  const spec = getNodeSpec(type);
683
836
  const node = {
@@ -685,8 +838,8 @@ function normalizeNode(rawNode, index, usedIds) {
685
838
  type,
686
839
  x: toFiniteNumber(rawNode?.x, index * 28),
687
840
  y: toFiniteNumber(rawNode?.y, index * 18),
688
- width: clampNodeWidth(type, rawNode?.width, spec.width),
689
- height: clampNodeHeight(type, rawNode?.height, spec.height),
841
+ width: clampNodeWidth(type, rawNode?.width, spec.width, sizeBounds),
842
+ height: clampNodeHeight(type, rawNode?.height, spec.height, sizeBounds),
690
843
  text: rawNode?.text == null ? spec.text : String(rawNode.text),
691
844
  data: isPlainObject(rawNode?.data) ? deepClone(rawNode.data) : {}
692
845
  };
@@ -779,7 +932,7 @@ function isSupportedDocumentVersion(value) {
779
932
  if (parsed.minor === max.minor && parsed.patch > max.patch) return false;
780
933
  return true;
781
934
  }
782
- function normalizeDocument(input) {
935
+ function normalizeDocument(input, limits = {}) {
783
936
  let source = input;
784
937
  if (typeof source === "string") {
785
938
  try {
@@ -802,13 +955,13 @@ function normalizeDocument(input) {
802
955
  }
803
956
  }
804
957
  const usedNodeIds = /* @__PURE__ */ new Set();
805
- const nodes = (Array.isArray(source.nodes) ? source.nodes : []).slice(0, MAX_NODES).map((node, index) => normalizeNode(node, index, usedNodeIds));
958
+ const nodes = (Array.isArray(source.nodes) ? source.nodes : []).slice(0, MAX_NODES).map((node, index) => normalizeNode(node, index, usedNodeIds, limits.sizeBounds));
806
959
  const nodeIds = new Set(nodes.map((node) => node.id));
807
960
  const usedEdgeIds = /* @__PURE__ */ new Set();
808
961
  const edges = (Array.isArray(source.edges) ? source.edges : []).slice(0, MAX_EDGES).map((edge, index) => normalizeEdge(edge, index, nodeIds, usedEdgeIds)).filter(Boolean);
809
962
  return {
810
963
  version: FLOWCHART_DOCUMENT_VERSION,
811
- viewport: normalizeViewport(source.viewport),
964
+ viewport: normalizeViewport(source.viewport, limits.scaleBounds),
812
965
  nodes,
813
966
  edges
814
967
  };
@@ -1025,19 +1178,30 @@ function getPolylineLabelPoint(points) {
1025
1178
  const last = points[points.length - 1] || { x: 0, y: 0 };
1026
1179
  return { x: formatNumber(last.x), y: formatNumber(last.y) };
1027
1180
  }
1028
- function curveControlPoint(point, port, target, curvature) {
1181
+ function routeMetrics(edgeScale = 1) {
1182
+ const scale = clamp(toFiniteNumber(edgeScale, 1), 0.01, 1);
1183
+ return {
1184
+ scale,
1185
+ minCurveArm: MIN_CURVE_ARM * scale,
1186
+ maxCurveArm: MAX_CURVE_ARM * scale,
1187
+ stub: ORTHOGONAL_STUB_LENGTH * scale,
1188
+ corner: ORTHOGONAL_CORNER_RADIUS * scale,
1189
+ clearance: ORTHOGONAL_CLEARANCE * scale
1190
+ };
1191
+ }
1192
+ function curveControlPoint(point, port, target, curvature, metrics = routeMetrics(1)) {
1029
1193
  const normal = getPortNormal(port);
1030
1194
  const dx = target.x - point.x;
1031
1195
  const dy = target.y - point.y;
1032
1196
  const distance = Math.hypot(dx, dy) || 1;
1033
1197
  const align = (normal.x * dx + normal.y * dy) / distance;
1034
1198
  const ease = 0.55 + 0.45 * align;
1035
- const arm = clamp(distance * curvature * ease, MIN_CURVE_ARM, MAX_CURVE_ARM);
1199
+ const arm = clamp(distance * curvature * ease, metrics.minCurveArm, metrics.maxCurveArm);
1036
1200
  return { x: point.x + normal.x * arm, y: point.y + normal.y * arm };
1037
1201
  }
1038
- function buildCurvePath(fromPoint, toPoint, fromPort = "right", toPort = "left") {
1039
- const controlA = curveControlPoint(fromPoint, fromPort, toPoint, EDGE_CURVATURE);
1040
- const controlB = curveControlPoint(toPoint, toPort, fromPoint, EDGE_CURVATURE);
1202
+ function buildCurvePath(fromPoint, toPoint, fromPort = "right", toPort = "left", metrics = routeMetrics(1)) {
1203
+ const controlA = curveControlPoint(fromPoint, fromPort, toPoint, EDGE_CURVATURE, metrics);
1204
+ const controlB = curveControlPoint(toPoint, toPort, fromPoint, EDGE_CURVATURE, metrics);
1041
1205
  const labelX = 0.125 * fromPoint.x + 0.375 * controlA.x + 0.375 * controlB.x + 0.125 * toPoint.x;
1042
1206
  const labelY = 0.125 * fromPoint.y + 0.375 * controlA.y + 0.375 * controlB.y + 0.125 * toPoint.y;
1043
1207
  return {
@@ -1099,13 +1263,40 @@ function getOrthogonalMidpoints(fromStub, toStub, fromPort, toPort) {
1099
1263
  }
1100
1264
  return [{ x: fromStub.x, y: toStub.y }];
1101
1265
  }
1102
- function getOrthogonalPoints(fromPoint, toPoint, fromPort, toPort, fromRect, toRect) {
1266
+ function getOrthogonalPoints(fromPoint, toPoint, fromPort, toPort, fromRect, toRect, metrics = routeMetrics(1)) {
1103
1267
  const fromNormal = getPortNormal(fromPort);
1104
1268
  const toNormal = getPortNormal(toPort);
1105
- const fromStub = offsetPoint(fromPoint, fromNormal, ORTHOGONAL_STUB_LENGTH);
1106
- const toStub = offsetPoint(toPoint, toNormal, ORTHOGONAL_STUB_LENGTH);
1107
1269
  const sourceHorizontal = isHorizontalPort(fromPort);
1108
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);
1109
1300
  const mids = [];
1110
1301
  if (sourceHorizontal && targetHorizontal) {
1111
1302
  const facing = fromNormal.x === -toNormal.x;
@@ -1143,13 +1334,13 @@ function getOrthogonalPoints(fromPoint, toPoint, fromPort, toPort, fromRect, toR
1143
1334
  );
1144
1335
  return points;
1145
1336
  }
1146
- function buildOrthogonalPath(fromPoint, toPoint, fromPort = "right", toPort = "left", fromRect = null, toRect = null) {
1337
+ function buildOrthogonalPath(fromPoint, toPoint, fromPort = "right", toPort = "left", fromRect = null, toRect = null, metrics = routeMetrics(1)) {
1147
1338
  let points;
1148
1339
  if (fromRect && toRect) {
1149
- points = getOrthogonalPoints(fromPoint, toPoint, fromPort, toPort, fromRect, toRect);
1340
+ points = getOrthogonalPoints(fromPoint, toPoint, fromPort, toPort, fromRect, toRect, metrics);
1150
1341
  } else {
1151
- const fromStub = offsetPoint(fromPoint, getPortNormal(fromPort), ORTHOGONAL_STUB_LENGTH);
1152
- const toStub = offsetPoint(toPoint, getPortNormal(toPort), ORTHOGONAL_STUB_LENGTH);
1342
+ const fromStub = offsetPoint(fromPoint, getPortNormal(fromPort), metrics.stub);
1343
+ const toStub = offsetPoint(toPoint, getPortNormal(toPort), metrics.stub);
1153
1344
  points = [];
1154
1345
  [
1155
1346
  fromPoint,
@@ -1161,7 +1352,7 @@ function buildOrthogonalPath(fromPoint, toPoint, fromPort = "right", toPort = "l
1161
1352
  }
1162
1353
  const label = getPolylineLabelPoint(points);
1163
1354
  return {
1164
- d: buildRoundedPath(points, ORTHOGONAL_CORNER_RADIUS),
1355
+ d: buildRoundedPath(points, metrics.corner),
1165
1356
  labelX: label.x,
1166
1357
  labelY: label.y
1167
1358
  };
@@ -1172,6 +1363,7 @@ function buildEdgePath(edge, fromNode = null, toNode = null) {
1172
1363
  const fromPoint = edge?.fromPoint || (fromNode ? getPortPosition(fromNode, fromPort) : null);
1173
1364
  const toPoint = edge?.toPoint || (toNode ? getPortPosition(toNode, toPort) : null);
1174
1365
  const route = normalizeEdgeRoute(edge?.route);
1366
+ const metrics = routeMetrics(edgeVisualScale(fromNode, toNode));
1175
1367
  if (!fromPoint || !toPoint) {
1176
1368
  return { d: "", labelX: 0, labelY: 0 };
1177
1369
  }
@@ -1179,15 +1371,17 @@ function buildEdgePath(edge, fromNode = null, toNode = null) {
1179
1371
  return buildStraightPath(fromPoint, toPoint);
1180
1372
  }
1181
1373
  if (route === "orthogonal") {
1182
- const fromRect = !edge?.fromPoint && fromNode ? getNodeRectBounds(fromNode, ORTHOGONAL_CLEARANCE) : null;
1183
- const toRect = !edge?.toPoint && toNode ? getNodeRectBounds(toNode, ORTHOGONAL_CLEARANCE) : null;
1184
- return buildOrthogonalPath(fromPoint, toPoint, fromPort, toPort, fromRect, toRect);
1374
+ const fromRect = !edge?.fromPoint && fromNode ? getNodeRectBounds(fromNode, metrics.clearance) : null;
1375
+ const toRect = !edge?.toPoint && toNode ? getNodeRectBounds(toNode, metrics.clearance) : null;
1376
+ return buildOrthogonalPath(fromPoint, toPoint, fromPort, toPort, fromRect, toRect, metrics);
1185
1377
  }
1186
- return buildCurvePath(fromPoint, toPoint, fromPort, toPort);
1378
+ return buildCurvePath(fromPoint, toPoint, fromPort, toPort, metrics);
1187
1379
  }
1188
- function createArrowMarker(id, strokeWidth, reversed = false) {
1189
- const size = formatNumber(Math.max(9, strokeWidth * 4.4));
1190
- const refInset = formatNumber(Math.max(1.5, strokeWidth * 0.85));
1380
+ function createArrowMarker(id, strokeWidth, reversed = false, edgeScale = 1) {
1381
+ const scale = clamp(toFiniteNumber(edgeScale, 1), 0.01, 1);
1382
+ const effective = Math.max(0.02, strokeWidth * scale);
1383
+ const size = formatNumber(Math.max(9 * scale, effective * 4.4));
1384
+ const refInset = formatNumber(Math.max(1.5 * scale, effective * 0.85));
1191
1385
  const marker = svgEl("marker", {
1192
1386
  id,
1193
1387
  markerWidth: size,
@@ -1205,9 +1399,11 @@ function createArrowMarker(id, strokeWidth, reversed = false) {
1205
1399
  );
1206
1400
  return marker;
1207
1401
  }
1208
- function createDotMarker(id, strokeWidth) {
1209
- const size = formatNumber(Math.max(8, strokeWidth * 3.3));
1210
- const radius = formatNumber(Math.max(2.75, strokeWidth * 1.45));
1402
+ function createDotMarker(id, strokeWidth, edgeScale = 1) {
1403
+ const scale = clamp(toFiniteNumber(edgeScale, 1), 0.01, 1);
1404
+ const effective = Math.max(0.02, strokeWidth * scale);
1405
+ const size = formatNumber(Math.max(8 * scale, effective * 3.3));
1406
+ const radius = formatNumber(Math.max(2.75 * scale, effective * 1.45));
1211
1407
  const marker = svgEl("marker", {
1212
1408
  id,
1213
1409
  markerWidth: size,
@@ -1464,7 +1660,23 @@ var VdFlowchart = class {
1464
1660
  this.element = resolveElement(options.element || options.target);
1465
1661
  this.readonly = Boolean(options.readonly);
1466
1662
  this.gridSize = clamp(toFiniteNumber(options.gridSize, DEFAULT_GRID_SIZE), 12, 64);
1467
- this.documentData = normalizeDocument(options.data || {});
1663
+ const scaleBounds = normalizeBoundPair(
1664
+ options.minScale,
1665
+ options.maxScale,
1666
+ DEFAULT_MIN_SCALE,
1667
+ DEFAULT_MAX_SCALE
1668
+ );
1669
+ const sizeBounds = normalizeBoundPair(
1670
+ options.minNodeSize,
1671
+ options.maxNodeSize,
1672
+ DEFAULT_MIN_NODE_SIZE,
1673
+ DEFAULT_MAX_NODE_SIZE
1674
+ );
1675
+ this.minScale = scaleBounds.min;
1676
+ this.maxScale = scaleBounds.max;
1677
+ this.minNodeSize = sizeBounds.min;
1678
+ this.maxNodeSize = sizeBounds.max;
1679
+ this.documentData = normalizeDocument(options.data || {}, { scaleBounds, sizeBounds });
1468
1680
  this.listeners = {};
1469
1681
  this.selection = null;
1470
1682
  this.selectedNodeIds = /* @__PURE__ */ new Set();
@@ -1507,6 +1719,7 @@ var VdFlowchart = class {
1507
1719
  this.handleClick = this.handleClick.bind(this);
1508
1720
  this.handleDoubleClick = this.handleDoubleClick.bind(this);
1509
1721
  this.handleWheel = this.handleWheel.bind(this);
1722
+ this.handleCanvasBlur = this.handleCanvasBlur.bind(this);
1510
1723
  this.handleKeyDown = this.handleKeyDown.bind(this);
1511
1724
  this.handleSelectionFieldInput = this.handleSelectionFieldInput.bind(this);
1512
1725
  this.handleSelectionFieldChange = this.handleSelectionFieldChange.bind(this);
@@ -1692,10 +1905,10 @@ var VdFlowchart = class {
1692
1905
  this.world = svgEl("g", { class: "vd-flowchart-world" });
1693
1906
  this.gridRect = svgEl("rect", {
1694
1907
  class: "vd-flowchart-grid",
1695
- x: -WORLD_EXTENT / 2,
1696
- y: -WORLD_EXTENT / 2,
1697
- width: WORLD_EXTENT,
1698
- height: WORLD_EXTENT,
1908
+ x: -DEFAULT_WORLD_EXTENT / 2,
1909
+ y: -DEFAULT_WORLD_EXTENT / 2,
1910
+ width: DEFAULT_WORLD_EXTENT,
1911
+ height: DEFAULT_WORLD_EXTENT,
1699
1912
  fill: `url(#${this.gridPatternId})`
1700
1913
  });
1701
1914
  this.edgesLayer = svgEl("g", { class: "vd-flowchart-edges" });
@@ -1919,6 +2132,7 @@ var VdFlowchart = class {
1919
2132
  this.canvasEl.addEventListener("click", this.handleClick);
1920
2133
  this.canvasEl.addEventListener("dblclick", this.handleDoubleClick);
1921
2134
  this.canvasEl.addEventListener("wheel", this.handleWheel, { passive: false });
2135
+ this.canvasEl.addEventListener("blur", this.handleCanvasBlur);
1922
2136
  this.root.addEventListener("keydown", this.handleKeyDown, true);
1923
2137
  window.addEventListener("pointerup", this.handlePointerUp);
1924
2138
  window.addEventListener("resize", this.handleResize);
@@ -1937,6 +2151,7 @@ var VdFlowchart = class {
1937
2151
  this.canvasEl.removeEventListener("click", this.handleClick);
1938
2152
  this.canvasEl.removeEventListener("dblclick", this.handleDoubleClick);
1939
2153
  this.canvasEl.removeEventListener("wheel", this.handleWheel);
2154
+ this.canvasEl.removeEventListener("blur", this.handleCanvasBlur);
1940
2155
  this.root.removeEventListener("keydown", this.handleKeyDown, true);
1941
2156
  window.removeEventListener("pointerup", this.handlePointerUp);
1942
2157
  window.removeEventListener("resize", this.handleResize);
@@ -1945,6 +2160,19 @@ var VdFlowchart = class {
1945
2160
  if (this.destroyed) return;
1946
2161
  this.render({ inspector: false, json: false });
1947
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
+ }
1948
2176
  updatePaletteState() {
1949
2177
  const arrowArmed = this.activeTool === "arrow";
1950
2178
  this.paletteGrid.querySelectorAll(".vd-flowchart-palette-btn").forEach((button) => {
@@ -2478,7 +2706,7 @@ var VdFlowchart = class {
2478
2706
  const width = this.canvasEl.clientWidth || 800;
2479
2707
  const height = this.canvasEl.clientHeight || 560;
2480
2708
  this.scaleAround(
2481
- clamp(scale, MIN_SCALE, MAX_SCALE) / viewport.scale,
2709
+ clamp(formatScale(scale), this.minScale, this.maxScale) / viewport.scale,
2482
2710
  width / 2,
2483
2711
  height / 2,
2484
2712
  "viewport:zoom"
@@ -2513,7 +2741,8 @@ var VdFlowchart = class {
2513
2741
  const node = normalizeNode(
2514
2742
  { ...data, id: void 0, x: data.x + 24, y: data.y + 24 },
2515
2743
  this.documentData.nodes.length + index,
2516
- usedNodeIds
2744
+ usedNodeIds,
2745
+ this.nodeSizeBounds()
2517
2746
  );
2518
2747
  idMap.set(data.id, node.id);
2519
2748
  return node;
@@ -2545,7 +2774,8 @@ var VdFlowchart = class {
2545
2774
  const node = normalizeNode(
2546
2775
  { ...data, id: void 0, collapsed: void 0, x: data.x + 24, y: data.y + 24 },
2547
2776
  this.documentData.nodes.length,
2548
- usedIds
2777
+ usedIds,
2778
+ this.nodeSizeBounds()
2549
2779
  );
2550
2780
  this.documentData.nodes.push(node);
2551
2781
  this.select({ kind: "node", id: node.id });
@@ -3159,14 +3389,15 @@ var VdFlowchart = class {
3159
3389
  if (this.activeTool === "arrow") return true;
3160
3390
  return this.selection?.kind === "node" && this.selection.id === node.id && !this.hasMultiSelection();
3161
3391
  }
3162
- getMarkerId(markerType, position, strokeWidth) {
3392
+ getMarkerId(markerType, position, strokeWidth, edgeScale = 1) {
3163
3393
  const width = normalizeEdgeStrokeWidth(strokeWidth);
3164
- const key = `${markerType}:${position}:${width}`;
3394
+ const scale = clamp(formatNumber(Math.max(edgeScale, 0.01)), 0.01, 1);
3395
+ const key = `${markerType}:${position}:${width}:${scale}`;
3165
3396
  if (this.markerIds.has(key)) {
3166
3397
  return this.markerIds.get(key);
3167
3398
  }
3168
3399
  const markerId = nextId(`flowchart-${markerType}-${position}`);
3169
- const marker = markerType === "dot" ? createDotMarker(markerId, width) : createArrowMarker(markerId, width, position === "start");
3400
+ const marker = markerType === "dot" ? createDotMarker(markerId, width, scale) : createArrowMarker(markerId, width, position === "start", scale);
3170
3401
  this.markerDefs.appendChild(marker);
3171
3402
  this.markerIds.set(key, markerId);
3172
3403
  return markerId;
@@ -3174,8 +3405,15 @@ var VdFlowchart = class {
3174
3405
  getEdgeStrokeWidth(edge) {
3175
3406
  return normalizeEdgeStrokeWidth(edge?.strokeWidth);
3176
3407
  }
3177
- getEdgeHitStrokeWidth(edge) {
3178
- return formatNumber(Math.max(EDGE_HIT_STROKE_MIN, this.getEdgeStrokeWidth(edge) + 12));
3408
+ getEdgeHitStrokeWidth(edge, edgeScale = 1) {
3409
+ const visual = clamp(toFiniteNumber(edgeScale, 1), 0.01, 1);
3410
+ const base = this.getEdgeStrokeWidth(edge);
3411
+ const view = Math.max(this.documentData?.viewport?.scale || 1, 1e-4);
3412
+ const legacy = Math.max(EDGE_HIT_STROKE_MIN, base + 12);
3413
+ if (visual >= 0.999 && view >= DEFAULT_MIN_SCALE && view <= DEFAULT_MAX_SCALE) {
3414
+ return formatNumber(legacy);
3415
+ }
3416
+ return formatNumber(Math.max(base * visual, EDGE_HIT_STROKE_MIN / view));
3179
3417
  }
3180
3418
  findConnectionTarget(worldPoint, excludeNodeId, previousSnap = null, referencePoint = null) {
3181
3419
  const snapPadding = CONNECTION_SNAP_PADDING / this.documentData.viewport.scale;
@@ -3232,17 +3470,37 @@ var VdFlowchart = class {
3232
3470
  let width = original.width;
3233
3471
  let height = original.height;
3234
3472
  if (handle.includes("e")) {
3235
- width = clampNodeWidth(nextType, original.width + deltaX, original.width);
3473
+ width = clampNodeWidth(
3474
+ nextType,
3475
+ original.width + deltaX,
3476
+ original.width,
3477
+ this.nodeSizeBounds()
3478
+ );
3236
3479
  }
3237
3480
  if (handle.includes("s")) {
3238
- height = clampNodeHeight(nextType, original.height + deltaY, original.height);
3481
+ height = clampNodeHeight(
3482
+ nextType,
3483
+ original.height + deltaY,
3484
+ original.height,
3485
+ this.nodeSizeBounds()
3486
+ );
3239
3487
  }
3240
3488
  if (handle.includes("w")) {
3241
- width = clampNodeWidth(nextType, original.width - deltaX, original.width);
3489
+ width = clampNodeWidth(
3490
+ nextType,
3491
+ original.width - deltaX,
3492
+ original.width,
3493
+ this.nodeSizeBounds()
3494
+ );
3242
3495
  x = original.x + original.width - width;
3243
3496
  }
3244
3497
  if (handle.includes("n")) {
3245
- height = clampNodeHeight(nextType, original.height - deltaY, original.height);
3498
+ height = clampNodeHeight(
3499
+ nextType,
3500
+ original.height - deltaY,
3501
+ original.height,
3502
+ this.nodeSizeBounds()
3503
+ );
3246
3504
  y = original.y + original.height - height;
3247
3505
  }
3248
3506
  return {
@@ -3278,7 +3536,7 @@ var VdFlowchart = class {
3278
3536
  scaleAround(factor, localX, localY, reason) {
3279
3537
  const viewport = this.documentData.viewport;
3280
3538
  const anchor = this.localToWorld(localX, localY);
3281
- const nextScale = clamp(formatNumber(viewport.scale * factor), MIN_SCALE, MAX_SCALE);
3539
+ const nextScale = clamp(formatScale(viewport.scale * factor), this.minScale, this.maxScale);
3282
3540
  if (nextScale === viewport.scale) return;
3283
3541
  viewport.scale = nextScale;
3284
3542
  viewport.x = formatNumber(localX - anchor.x * nextScale);
@@ -3370,7 +3628,7 @@ var VdFlowchart = class {
3370
3628
  if (event.key === "Escape" || event.key === "Enter") {
3371
3629
  event.preventDefault();
3372
3630
  this.stopTextEdit({ commit: event.key === "Enter" });
3373
- this.canvasEl.focus();
3631
+ this.focusCanvas({ quiet: true });
3374
3632
  }
3375
3633
  });
3376
3634
  textarea.addEventListener("blur", () => {
@@ -3397,7 +3655,7 @@ var VdFlowchart = class {
3397
3655
  if (event.key === "Escape") {
3398
3656
  event.preventDefault();
3399
3657
  this.stopTextEdit({ commit: false });
3400
- this.canvasEl.focus();
3658
+ this.focusCanvas({ quiet: true });
3401
3659
  return;
3402
3660
  }
3403
3661
  if (event.key === "Enter") {
@@ -3406,13 +3664,12 @@ var VdFlowchart = class {
3406
3664
  if (!commits) return;
3407
3665
  event.preventDefault();
3408
3666
  this.stopTextEdit({ commit: true });
3409
- this.canvasEl.focus();
3667
+ this.focusCanvas({ quiet: true });
3410
3668
  return;
3411
3669
  }
3412
3670
  if (event.key === "Tab" && mindmap && !event.shiftKey && !event.altKey && !event.ctrlKey) {
3413
3671
  event.preventDefault();
3414
3672
  this.stopTextEdit({ commit: true });
3415
- this.canvasEl.focus();
3416
3673
  this.insertBranchNode(node.id, { edit: true });
3417
3674
  }
3418
3675
  }
@@ -3461,19 +3718,20 @@ var VdFlowchart = class {
3461
3718
  }
3462
3719
  const viewport = this.documentData.viewport;
3463
3720
  const scale = viewport.scale;
3721
+ const visual = nodeVisualScale(node);
3464
3722
  const textarea = this.textEditor.textarea;
3465
3723
  const width = node.width * scale;
3466
3724
  const height = node.height * scale;
3467
3725
  const metrics = getNodeFontMetrics(node);
3468
- const lineHeight = metrics.lineHeight * scale;
3726
+ const lineHeight = metrics.lineHeight * visual * scale;
3469
3727
  const lineCount = Math.max(1, textarea.value.split(/\r?\n/).length);
3470
- const horizontalPadding = (node.type === "textbox" ? 16 : 12) * scale;
3471
- const verticalPadding = node.type === "textbox" ? 12 * scale : Math.max(4, (height - lineCount * lineHeight) / 2);
3728
+ const horizontalPadding = (node.type === "textbox" ? 16 : 12) * visual * scale;
3729
+ const verticalPadding = node.type === "textbox" ? 12 * visual * scale : Math.max(4 * visual, (height - lineCount * lineHeight) / 2);
3472
3730
  textarea.style.left = `${formatNumber(viewport.x + node.x * scale)}px`;
3473
3731
  textarea.style.top = `${formatNumber(viewport.y + node.y * scale)}px`;
3474
3732
  textarea.style.width = `${formatNumber(width)}px`;
3475
3733
  textarea.style.height = `${formatNumber(height)}px`;
3476
- textarea.style.fontSize = `${formatNumber(metrics.fontSize * scale)}px`;
3734
+ textarea.style.fontSize = `${formatNumber(metrics.fontSize * visual * scale)}px`;
3477
3735
  textarea.style.lineHeight = `${formatNumber(lineHeight)}px`;
3478
3736
  textarea.style.padding = `${formatNumber(verticalPadding)}px ${formatNumber(horizontalPadding)}px`;
3479
3737
  }
@@ -3485,14 +3743,16 @@ var VdFlowchart = class {
3485
3743
  this.stopTextEdit({ commit: false });
3486
3744
  return;
3487
3745
  }
3746
+ const visual = edgeVisualScale(fromNode, toNode);
3488
3747
  const { labelX, labelY } = buildEdgePath(edge, fromNode, toNode);
3489
3748
  const viewport = this.documentData.viewport;
3490
3749
  const textarea = this.textEditor.textarea;
3491
- const fontSize = Math.max(11, 13 * viewport.scale);
3492
- const width = clamp(textarea.value.length * fontSize * 0.62 + 28, 96, 280);
3493
- const height = fontSize * 1.4 + 12;
3750
+ const fontSize = Math.max(11 * visual, 13 * visual * viewport.scale);
3751
+ const width = clamp(textarea.value.length * fontSize * 0.62 + 28 * visual, 96 * visual, 280);
3752
+ const height = fontSize * 1.4 + 12 * visual;
3753
+ const labelOffset = 8 * visual;
3494
3754
  textarea.style.left = `${formatNumber(viewport.x + labelX * viewport.scale - width / 2)}px`;
3495
- textarea.style.top = `${formatNumber(viewport.y + (labelY - 8) * viewport.scale - height / 2)}px`;
3755
+ textarea.style.top = `${formatNumber(viewport.y + (labelY - labelOffset) * viewport.scale - height / 2)}px`;
3496
3756
  textarea.style.width = `${formatNumber(width)}px`;
3497
3757
  textarea.style.height = `${formatNumber(height)}px`;
3498
3758
  textarea.style.fontSize = `${formatNumber(fontSize)}px`;
@@ -3501,6 +3761,7 @@ var VdFlowchart = class {
3501
3761
  }
3502
3762
  render(options = {}) {
3503
3763
  if (this.destroyed) return;
3764
+ this.syncGrid();
3504
3765
  const shouldRenderScene = options.scene !== false;
3505
3766
  const shouldRenderInspector = options.inspector !== false;
3506
3767
  const shouldRenderJson = options.json !== false;
@@ -3524,6 +3785,8 @@ var VdFlowchart = class {
3524
3785
  clearChildren(this.previewLayer);
3525
3786
  clearChildren(this.nodesLayer);
3526
3787
  clearChildren(this.overlayLayer);
3788
+ clearChildren(this.markerDefs);
3789
+ this.markerIds = /* @__PURE__ */ new Map();
3527
3790
  const hidden = this.getHiddenNodeIds();
3528
3791
  this.sceneHidden = hidden;
3529
3792
  this.sceneOutgoing = this.getOutgoingMap();
@@ -3587,17 +3850,18 @@ var VdFlowchart = class {
3587
3850
  }
3588
3851
  }
3589
3852
  }
3590
- applyEdgeMarkers(pathElement, edge) {
3853
+ applyEdgeMarkers(pathElement, edge, edgeScale = 1) {
3854
+ const width = this.getEdgeStrokeWidth(edge);
3591
3855
  if (edge.startMarker && edge.startMarker !== "none") {
3592
3856
  pathElement.setAttribute(
3593
3857
  "marker-start",
3594
- `url(#${this.getMarkerId(edge.startMarker, "start", this.getEdgeStrokeWidth(edge))})`
3858
+ `url(#${this.getMarkerId(edge.startMarker, "start", width, edgeScale)})`
3595
3859
  );
3596
3860
  }
3597
3861
  if (edge.endMarker && edge.endMarker !== "none") {
3598
3862
  pathElement.setAttribute(
3599
3863
  "marker-end",
3600
- `url(#${this.getMarkerId(edge.endMarker, "end", this.getEdgeStrokeWidth(edge))})`
3864
+ `url(#${this.getMarkerId(edge.endMarker, "end", width, edgeScale)})`
3601
3865
  );
3602
3866
  }
3603
3867
  }
@@ -3605,6 +3869,7 @@ var VdFlowchart = class {
3605
3869
  const fromNode = nodeMap.get(edge.from.nodeId);
3606
3870
  const toNode = nodeMap.get(edge.to.nodeId);
3607
3871
  if (!fromNode || !toNode) return null;
3872
+ const visual = edgeVisualScale(fromNode, toNode);
3608
3873
  const edgePath = buildEdgePath(edge, fromNode, toNode);
3609
3874
  const selected = this.selection?.kind === "edge" && this.selection.id === edge.id;
3610
3875
  const inReconnectMode = this.reconnectEdgeId === edge.id;
@@ -3612,13 +3877,14 @@ var VdFlowchart = class {
3612
3877
  class: `vd-flowchart-edge${selected ? " is-selected" : ""}${inReconnectMode ? " is-reconnecting" : ""}`,
3613
3878
  "data-edge-id": edge.id
3614
3879
  });
3615
- const strokeWidth = this.getEdgeStrokeWidth(edge);
3880
+ const baseStroke = this.getEdgeStrokeWidth(edge);
3881
+ const strokeWidth = visual < 0.999 ? formatNumber(Math.max(0.02, baseStroke * visual)) : baseStroke;
3616
3882
  if (selected) {
3617
3883
  group.appendChild(
3618
3884
  svgEl("path", {
3619
3885
  class: "vd-flowchart-edge-selection",
3620
3886
  d: edgePath.d,
3621
- "stroke-width": formatNumber(strokeWidth + 7)
3887
+ "stroke-width": visual < 0.999 ? formatNumber((baseStroke + 7) * visual) : formatNumber(strokeWidth + 7)
3622
3888
  })
3623
3889
  );
3624
3890
  }
@@ -3628,23 +3894,28 @@ var VdFlowchart = class {
3628
3894
  "stroke-width": strokeWidth,
3629
3895
  "data-edge-id": edge.id
3630
3896
  });
3631
- this.applyEdgeMarkers(visiblePath, edge);
3897
+ this.applyEdgeMarkers(visiblePath, edge, visual);
3632
3898
  const hitPath = svgEl("path", {
3633
3899
  class: "vd-flowchart-edge-hit",
3634
3900
  d: edgePath.d,
3635
- "stroke-width": this.getEdgeHitStrokeWidth(edge),
3901
+ "stroke-width": this.getEdgeHitStrokeWidth(edge, visual),
3636
3902
  "data-edge-id": edge.id
3637
3903
  });
3638
3904
  group.appendChild(visiblePath);
3639
3905
  group.appendChild(hitPath);
3640
3906
  if (edge.label && this.textEditor?.edgeId !== edge.id) {
3907
+ const labelOffset = visual < 0.999 ? formatNumber(8 * visual) : 8;
3641
3908
  const label = svgEl("text", {
3642
3909
  class: "vd-flowchart-edge-label",
3643
3910
  x: edgePath.labelX,
3644
- y: edgePath.labelY - 8,
3911
+ y: edgePath.labelY - labelOffset,
3645
3912
  "text-anchor": "middle",
3646
3913
  "data-edge-id": edge.id
3647
3914
  });
3915
+ if (visual < 0.999) {
3916
+ label.style.fontSize = `${formatNumber(Math.max(0.06, 12 * visual))}px`;
3917
+ label.style.strokeWidth = `${formatNumber(Math.max(0.03, 5 * visual))}px`;
3918
+ }
3648
3919
  label.textContent = edge.label;
3649
3920
  group.appendChild(label);
3650
3921
  }
@@ -3683,18 +3954,26 @@ var VdFlowchart = class {
3683
3954
  renderPreviewEdge(interaction) {
3684
3955
  const fromPort = interaction.fromPort || interaction.source?.port || "right";
3685
3956
  const toPort = interaction.toPort || interaction.target?.port || "left";
3686
- const edgePath = buildEdgePath({
3687
- strokeWidth: interaction.strokeWidth,
3688
- route: interaction.route,
3689
- fromPoint: interaction.fromPoint,
3690
- toPoint: interaction.toPoint,
3691
- from: { port: fromPort },
3692
- to: { port: toPort }
3693
- });
3957
+ const fromNode = interaction.source?.nodeId ? this.findNode(interaction.source.nodeId) : null;
3958
+ const toNode = interaction.target?.nodeId ? this.findNode(interaction.target.nodeId) : null;
3959
+ const visual = edgeVisualScale(fromNode, toNode);
3960
+ const edgePath = buildEdgePath(
3961
+ {
3962
+ strokeWidth: interaction.strokeWidth,
3963
+ route: interaction.route,
3964
+ fromPoint: interaction.fromPoint,
3965
+ toPoint: interaction.toPoint,
3966
+ from: { port: fromPort },
3967
+ to: { port: toPort }
3968
+ },
3969
+ fromNode,
3970
+ toNode
3971
+ );
3972
+ const baseStroke = this.getEdgeStrokeWidth(interaction);
3694
3973
  return svgEl("path", {
3695
3974
  class: "vd-flowchart-preview-path",
3696
3975
  d: edgePath.d,
3697
- "stroke-width": this.getEdgeStrokeWidth(interaction)
3976
+ "stroke-width": visual < 0.999 ? formatNumber(Math.max(0.02, baseStroke * visual)) : baseStroke
3698
3977
  });
3699
3978
  }
3700
3979
  renderNode(node) {
@@ -3709,17 +3988,21 @@ var VdFlowchart = class {
3709
3988
  "data-node-id": node.id
3710
3989
  });
3711
3990
  const portsVisible = this.shouldShowNodePorts(node);
3991
+ const visual = nodeVisualScale(node);
3992
+ const corner = node.type === "rounded-rect" || node.type === "textbox" ? 16 : node.type === "junction" ? node.width / 2 : 6;
3712
3993
  const hitbox = svgEl("rect", {
3713
3994
  x: 0,
3714
3995
  y: 0,
3715
3996
  width: node.width,
3716
3997
  height: node.height,
3717
- rx: node.type === "rounded-rect" || node.type === "textbox" ? 16 : node.type === "junction" ? node.width / 2 : 6,
3998
+ rx: visual < 0.999 && node.type !== "junction" ? formatNumber(corner * visual) : corner,
3718
3999
  fill: "transparent",
3719
4000
  "data-node-id": node.id
3720
4001
  });
3721
4002
  group.appendChild(hitbox);
3722
- group.appendChild(this.renderNodeShape(node));
4003
+ const shape = this.renderNodeShape(node);
4004
+ this.applyScaledNodeShape(node, shape);
4005
+ group.appendChild(shape);
3723
4006
  group.appendChild(this.renderNodeText(node));
3724
4007
  const scale = this.documentData.viewport.scale || 1;
3725
4008
  if (soloSelected && !this.readonly && SELECTION_BOX_TYPES.has(node.type)) {
@@ -3911,6 +4194,21 @@ var VdFlowchart = class {
3911
4194
  });
3912
4195
  return group;
3913
4196
  }
4197
+ applyScaledNodeShape(node, shape) {
4198
+ const visual = nodeVisualScale(node);
4199
+ if (!shape || visual >= 0.999) return;
4200
+ const baseStroke = node.type === "junction" ? 1.25 : 1.5;
4201
+ shape.style.strokeWidth = String(formatNumber(Math.max(0.03, baseStroke * visual)));
4202
+ if (node.type === "textbox") {
4203
+ shape.style.strokeDasharray = `${formatNumber(8 * visual)} ${formatNumber(5 * visual)}`;
4204
+ }
4205
+ const baseRadius = node.type === "rounded-rect" ? 18 : node.type === "textbox" ? 14 : node.type === "rect" ? 2 : 0;
4206
+ if (baseRadius > 0) {
4207
+ const scaled = String(formatNumber(baseRadius * visual));
4208
+ shape.setAttribute("rx", scaled);
4209
+ shape.setAttribute("ry", scaled);
4210
+ }
4211
+ }
3914
4212
  renderNodeShape(node) {
3915
4213
  const baseClass = `vd-flowchart-node-shape vd-flowchart-node-shape--${node.type}`;
3916
4214
  if (node.type === "rounded-rect") {
@@ -3987,21 +4285,28 @@ var VdFlowchart = class {
3987
4285
  if (node.type === "junction" || !node.text) {
3988
4286
  return textElement;
3989
4287
  }
4288
+ const visual = nodeVisualScale(node);
4289
+ const metrics = getNodeFontMetrics(node);
4290
+ const lineHeight = visual < 0.999 ? Math.max(0.06, metrics.lineHeight * visual) : metrics.lineHeight;
4291
+ if (visual < 0.999) {
4292
+ textElement.style.fontSize = `${formatNumber(Math.max(0.05, metrics.fontSize * visual))}px`;
4293
+ }
4294
+ const equivalentWidth = visual < 0.999 ? node.width / visual : node.width;
3990
4295
  if (node.type === "textbox") {
3991
- const lines2 = wrapText(node.text, estimateChars(node.width - 32));
3992
- const { lineHeight: lineHeight2 } = getNodeFontMetrics(node);
4296
+ const lines2 = wrapText(node.text, estimateChars(equivalentWidth - 32));
4297
+ const padX = visual < 0.999 ? formatNumber(16 * visual) : 16;
4298
+ const startY2 = visual < 0.999 ? formatNumber(28 * visual) : 28;
3993
4299
  lines2.forEach((line, index) => {
3994
4300
  const span = svgEl("tspan", {
3995
- x: 16,
3996
- y: 28 + index * lineHeight2
4301
+ x: padX,
4302
+ y: visual < 0.999 ? formatNumber(startY2 + index * lineHeight) : startY2 + index * lineHeight
3997
4303
  });
3998
4304
  span.textContent = line;
3999
4305
  textElement.appendChild(span);
4000
4306
  });
4001
4307
  return textElement;
4002
4308
  }
4003
- const lines = wrapText(node.text, estimateChars(node.width));
4004
- const { lineHeight } = getNodeFontMetrics(node);
4309
+ const lines = wrapText(node.text, estimateChars(equivalentWidth));
4005
4310
  const totalHeight = (lines.length - 1) * lineHeight;
4006
4311
  const startY = node.height / 2 - totalHeight / 2;
4007
4312
  textElement.setAttribute("text-anchor", "middle");
@@ -4009,7 +4314,7 @@ var VdFlowchart = class {
4009
4314
  lines.forEach((line, index) => {
4010
4315
  const span = svgEl("tspan", {
4011
4316
  x: node.width / 2,
4012
- y: startY + index * lineHeight,
4317
+ y: visual < 0.999 ? formatNumber(startY + index * lineHeight) : startY + index * lineHeight,
4013
4318
  "text-anchor": "middle",
4014
4319
  "dominant-baseline": "middle"
4015
4320
  });
@@ -4061,12 +4366,12 @@ var VdFlowchart = class {
4061
4366
  textArea.setAttribute("data-field", "node-text");
4062
4367
  const widthInput = createElement("input", { value: node.width, type: "number" });
4063
4368
  widthInput.setAttribute("data-field", "node-width");
4064
- widthInput.setAttribute("min", String(MIN_NODE_SIZE));
4065
- widthInput.setAttribute("max", String(MAX_NODE_SIZE));
4369
+ widthInput.setAttribute("min", String(this.minNodeSize));
4370
+ widthInput.setAttribute("max", String(this.maxNodeSize));
4066
4371
  const heightInput = createElement("input", { value: node.height, type: "number" });
4067
4372
  heightInput.setAttribute("data-field", "node-height");
4068
- heightInput.setAttribute("min", String(MIN_NODE_SIZE));
4069
- heightInput.setAttribute("max", String(MAX_NODE_SIZE));
4373
+ heightInput.setAttribute("min", String(this.minNodeSize));
4374
+ heightInput.setAttribute("max", String(this.maxNodeSize));
4070
4375
  this.selectionFields.appendChild(createField("Type", typeSelect));
4071
4376
  if (isNodeTextEditable(node)) {
4072
4377
  this.selectionFields.appendChild(createField("Text", textArea));
@@ -4380,8 +4685,8 @@ var VdFlowchart = class {
4380
4685
  this.reconnectEdgeId = null;
4381
4686
  const currentViewport = deepClone(this.documentData.viewport);
4382
4687
  const previousSelection = this.selection;
4383
- this.documentData = normalizeDocument(snapshot);
4384
- this.documentData.viewport = normalizeViewport(currentViewport);
4688
+ this.documentData = normalizeDocument(snapshot, this.normalizationLimits());
4689
+ this.documentData.viewport = normalizeViewport(currentViewport, this.scaleBounds());
4385
4690
  this.selection = this.resolvePreservedSelection(previousSelection, true);
4386
4691
  this.render();
4387
4692
  this.emitChange(reason);
@@ -4458,7 +4763,8 @@ var VdFlowchart = class {
4458
4763
  y: partialNode.y == null ? fallbackY : partialNode.y
4459
4764
  },
4460
4765
  this.documentData.nodes.length,
4461
- usedIds
4766
+ usedIds,
4767
+ this.nodeSizeBounds()
4462
4768
  );
4463
4769
  this.documentData.nodes.push(node);
4464
4770
  this.select({ kind: "node", id: node.id });
@@ -4671,7 +4977,7 @@ var VdFlowchart = class {
4671
4977
  changed = true;
4672
4978
  }
4673
4979
  });
4674
- const rerouted = options.reroutePorts === false ? false : this.rerouteEdgePorts();
4980
+ const rerouted = options.reroutePorts === false ? false : this.rerouteEdgePorts(resolvedMode === "tree" ? options.direction || "right" : null);
4675
4981
  if (!changed && !rerouted) return this;
4676
4982
  this.render();
4677
4983
  this.emitChange("layout", { mode: resolvedMode });
@@ -4684,8 +4990,16 @@ var VdFlowchart = class {
4684
4990
  // Re-pick each edge's from/to port from the nodes' current centers, so a
4685
4991
  // freshly laid-out graph attaches connectors on sensible sides. Returns
4686
4992
  // whether any port actually changed.
4687
- rerouteEdgePorts() {
4993
+ rerouteEdgePorts(flowDirection = null) {
4688
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
+ }
4689
5003
  this.documentData.edges.forEach((edge) => {
4690
5004
  const fromNode = this.findNode(edge.from.nodeId);
4691
5005
  const toNode = this.findNode(edge.to.nodeId);
@@ -4695,8 +5009,82 @@ var VdFlowchart = class {
4695
5009
  y: fromNode.y + fromNode.height / 2
4696
5010
  };
4697
5011
  const toCenter = { x: toNode.x + toNode.width / 2, y: toNode.y + toNode.height / 2 };
4698
- const nextFrom = getPortByDirection(fromNode, toCenter).port;
4699
- 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
+ }
4700
5088
  if (nextFrom !== edge.from.port || nextTo !== edge.to.port) {
4701
5089
  edge.from.port = nextFrom;
4702
5090
  edge.to.port = nextTo;
@@ -4711,8 +5099,8 @@ var VdFlowchart = class {
4711
5099
  node.x = patch.x == null ? node.x : formatNumber(toFiniteNumber(patch.x, node.x));
4712
5100
  node.y = patch.y == null ? node.y : formatNumber(toFiniteNumber(patch.y, node.y));
4713
5101
  node.text = isNodeTextEditable(nextType) ? patch.text == null ? node.text : String(patch.text) : getNodeSpec(nextType).text;
4714
- node.width = clampNodeWidth(nextType, patch.width, node.width);
4715
- node.height = clampNodeHeight(nextType, patch.height, node.height);
5102
+ node.width = clampNodeWidth(nextType, patch.width, node.width, this.nodeSizeBounds());
5103
+ node.height = clampNodeHeight(nextType, patch.height, node.height, this.nodeSizeBounds());
4716
5104
  if (isPlainObject(patch.data)) {
4717
5105
  node.data = deepClone(patch.data);
4718
5106
  }
@@ -4901,8 +5289,45 @@ var VdFlowchart = class {
4901
5289
  if (this.selection.kind === "node") return this.removeNode(this.selection.id);
4902
5290
  return this.removeEdge(this.selection.id);
4903
5291
  }
5292
+ nodeSizeBounds() {
5293
+ return { min: this.minNodeSize, max: this.maxNodeSize };
5294
+ }
5295
+ scaleBounds() {
5296
+ return { min: this.minScale, max: this.maxScale };
5297
+ }
5298
+ normalizationLimits() {
5299
+ return { scaleBounds: this.scaleBounds(), sizeBounds: this.nodeSizeBounds() };
5300
+ }
5301
+ hasWideCamera() {
5302
+ return this.minScale < DEFAULT_MIN_SCALE || this.maxScale > DEFAULT_MAX_SCALE;
5303
+ }
5304
+ syncGrid() {
5305
+ const wide = this.hasWideCamera();
5306
+ const extent = wide ? WIDE_WORLD_EXTENT : DEFAULT_WORLD_EXTENT;
5307
+ this.gridRect?.setAttribute("x", String(-extent / 2));
5308
+ this.gridRect?.setAttribute("y", String(-extent / 2));
5309
+ this.gridRect?.setAttribute("width", String(extent));
5310
+ this.gridRect?.setAttribute("height", String(extent));
5311
+ const pattern = this.svg?.querySelector("pattern");
5312
+ const path = pattern?.querySelector("path");
5313
+ if (!pattern || !path) return;
5314
+ let tile = this.gridSize;
5315
+ if (wide) {
5316
+ const scale = Math.max(this.minScale, this.documentData?.viewport?.scale || 1);
5317
+ while (tile * scale < 12 && tile < extent / 8) tile *= 4;
5318
+ while (tile * scale > 48 && tile > 1.5) tile /= 4;
5319
+ tile = formatNumber(tile);
5320
+ path.setAttribute("vector-effect", "non-scaling-stroke");
5321
+ } else {
5322
+ path.removeAttribute("vector-effect");
5323
+ }
5324
+ path.setAttribute("stroke-width", "1");
5325
+ pattern.setAttribute("width", String(tile));
5326
+ pattern.setAttribute("height", String(tile));
5327
+ path.setAttribute("d", `M ${tile} 0 L 0 0 0 ${tile}`);
5328
+ }
4904
5329
  setViewport(viewport) {
4905
- this.documentData.viewport = normalizeViewport(viewport);
5330
+ this.documentData.viewport = normalizeViewport(viewport, this.scaleBounds());
4906
5331
  this.render({ inspector: false, json: true });
4907
5332
  this.emitViewportChange("viewport:set");
4908
5333
  return this;
@@ -4920,7 +5345,7 @@ var VdFlowchart = class {
4920
5345
  return this;
4921
5346
  }
4922
5347
  resetView() {
4923
- this.documentData.viewport = normalizeViewport({ x: 0, y: 0, scale: 1 });
5348
+ this.documentData.viewport = normalizeViewport({ x: 0, y: 0, scale: 1 }, this.scaleBounds());
4924
5349
  this.render({ inspector: false, json: true });
4925
5350
  this.emitViewportChange("viewport:reset");
4926
5351
  return this;
@@ -4936,30 +5361,64 @@ var VdFlowchart = class {
4936
5361
  const padding = 80;
4937
5362
  const contentWidth = Math.max(1, bounds.right - bounds.left);
4938
5363
  const contentHeight = Math.max(1, bounds.bottom - bounds.top);
4939
- const scale = clamp(
4940
- Math.min((width - padding * 2) / contentWidth, (height - padding * 2) / contentHeight),
4941
- MIN_SCALE,
4942
- MAX_SCALE
5364
+ const scale = formatScale(
5365
+ clamp(
5366
+ Math.min((width - padding * 2) / contentWidth, (height - padding * 2) / contentHeight),
5367
+ this.minScale,
5368
+ this.maxScale
5369
+ )
4943
5370
  );
4944
5371
  this.documentData.viewport = {
4945
5372
  x: formatNumber(width / 2 - (bounds.left + bounds.right) / 2 * scale),
4946
5373
  y: formatNumber(height / 2 - (bounds.top + bounds.bottom) / 2 * scale),
4947
- scale: formatNumber(scale)
5374
+ scale
4948
5375
  };
4949
5376
  this.render({ inspector: false, json: true });
4950
5377
  this.emitViewportChange("viewport:fit");
4951
5378
  return this;
4952
5379
  }
5380
+ fitNode(nodeId, padding = 48) {
5381
+ const node = this.findNode(nodeId);
5382
+ if (!node || this.isNodeHidden(node.id)) return this;
5383
+ const pad = Math.max(0, toFiniteNumber(padding, 48));
5384
+ const width = this.canvasEl.clientWidth || 800;
5385
+ const height = this.canvasEl.clientHeight || 560;
5386
+ const availableWidth = Math.max(1, width - pad * 2);
5387
+ const availableHeight = Math.max(1, height - pad * 2);
5388
+ const scale = formatScale(
5389
+ clamp(
5390
+ Math.min(
5391
+ availableWidth / Math.max(1, node.width),
5392
+ availableHeight / Math.max(1, node.height)
5393
+ ),
5394
+ this.minScale,
5395
+ this.maxScale
5396
+ )
5397
+ );
5398
+ const centerX = node.x + node.width / 2;
5399
+ const centerY = node.y + node.height / 2;
5400
+ this.documentData.viewport = {
5401
+ x: formatNumber(width / 2 - centerX * scale),
5402
+ y: formatNumber(height / 2 - centerY * scale),
5403
+ scale
5404
+ };
5405
+ this.render({ inspector: false, json: true });
5406
+ this.emitViewportChange("viewport:fit-node");
5407
+ return this;
5408
+ }
4953
5409
  clear() {
4954
5410
  this.stopTextEdit({ commit: false });
4955
5411
  this.activeTool = null;
4956
5412
  this.reconnectEdgeId = null;
4957
5413
  this.clipboard = null;
4958
- this.documentData = normalizeDocument({
4959
- nodes: [],
4960
- edges: [],
4961
- viewport: { x: 0, y: 0, scale: 1 }
4962
- });
5414
+ this.documentData = normalizeDocument(
5415
+ {
5416
+ nodes: [],
5417
+ edges: [],
5418
+ viewport: { x: 0, y: 0, scale: 1 }
5419
+ },
5420
+ this.normalizationLimits()
5421
+ );
4963
5422
  this.selection = null;
4964
5423
  this.render();
4965
5424
  this.emitChange("clear");
@@ -4994,6 +5453,33 @@ var VdFlowchart = class {
4994
5453
  if ("snapGuides" in options) this.snapGuides = options.snapGuides !== false;
4995
5454
  if ("minimap" in options) this.setMinimapEnabled(options.minimap !== false);
4996
5455
  if ("autoLayout" in options) this.autoLayout = Boolean(options.autoLayout);
5456
+ if ("minScale" in options || "maxScale" in options || "minNodeSize" in options || "maxNodeSize" in options) {
5457
+ const nextScale = normalizeBoundPair(
5458
+ "minScale" in options ? options.minScale : this.minScale,
5459
+ "maxScale" in options ? options.maxScale : this.maxScale,
5460
+ DEFAULT_MIN_SCALE,
5461
+ DEFAULT_MAX_SCALE
5462
+ );
5463
+ const nextSize = normalizeBoundPair(
5464
+ "minNodeSize" in options ? options.minNodeSize : this.minNodeSize,
5465
+ "maxNodeSize" in options ? options.maxNodeSize : this.maxNodeSize,
5466
+ DEFAULT_MIN_NODE_SIZE,
5467
+ DEFAULT_MAX_NODE_SIZE
5468
+ );
5469
+ this.minScale = nextScale.min;
5470
+ this.maxScale = nextScale.max;
5471
+ this.minNodeSize = nextSize.min;
5472
+ this.maxNodeSize = nextSize.max;
5473
+ const clamped = clamp(
5474
+ formatScale(this.documentData.viewport.scale),
5475
+ this.minScale,
5476
+ this.maxScale
5477
+ );
5478
+ if (clamped !== this.documentData.viewport.scale) {
5479
+ this.documentData.viewport.scale = clamped;
5480
+ this.pendingBoundsEvent = true;
5481
+ }
5482
+ }
4997
5483
  this.syncCanvasLabel();
4998
5484
  if (this.shortcutsHelpOpen) this.renderShortcutsHelp();
4999
5485
  if ("history" in options && options.history !== false !== this.historyEnabled) {
@@ -5011,11 +5497,15 @@ var VdFlowchart = class {
5011
5497
  }
5012
5498
  }
5013
5499
  this.render();
5500
+ if (this.pendingBoundsEvent) {
5501
+ this.pendingBoundsEvent = false;
5502
+ this.emitViewportChange("viewport:bounds");
5503
+ }
5014
5504
  this.updateHistoryButtons();
5015
5505
  return this;
5016
5506
  }
5017
5507
  load(data, options = {}) {
5018
- const documentData = normalizeDocument(data);
5508
+ const documentData = normalizeDocument(data, this.normalizationLimits());
5019
5509
  if (options.silent && JSON.stringify(documentData) === JSON.stringify(this.documentData)) {
5020
5510
  return this;
5021
5511
  }
@@ -5073,7 +5563,11 @@ var EDITOR_OPTION_KEYS = [
5073
5563
  "keyboardShortcuts",
5074
5564
  "snapGuides",
5075
5565
  "minimap",
5076
- "autoLayout"
5566
+ "autoLayout",
5567
+ "minScale",
5568
+ "maxScale",
5569
+ "minNodeSize",
5570
+ "maxNodeSize"
5077
5571
  ];
5078
5572
  var VdFlowchart2 = (0, import_vue.defineComponent)({
5079
5573
  name: "VdFlowchart",
@@ -5099,13 +5593,29 @@ var VdFlowchart2 = (0, import_vue.defineComponent)({
5099
5593
  /** Show the overview minimap in the canvas corner (default true). */
5100
5594
  minimap: { type: Boolean, default: true },
5101
5595
  /** Re-run the current layout after keyboard or handle insertions (default false). */
5102
- autoLayout: { type: Boolean, default: false }
5596
+ autoLayout: { type: Boolean, default: false },
5597
+ /** Lowest camera scale. Defaults to 0.35. Set it together with maxScale. */
5598
+ minScale: { type: Number, default: void 0 },
5599
+ /** Highest camera scale. Defaults to 3. Set it together with minScale. */
5600
+ maxScale: { type: Number, default: void 0 },
5601
+ /** Smallest resizable-node side. Defaults to 56. Set it together with maxNodeSize. */
5602
+ minNodeSize: { type: Number, default: void 0 },
5603
+ /** Largest resizable-node side. Defaults to 420. Set it together with minNodeSize. */
5604
+ maxNodeSize: { type: Number, default: void 0 }
5103
5605
  },
5104
5606
  emits: ["change", "select", "viewport", "connect", "ready"],
5105
5607
  setup(props, { emit, expose }) {
5106
5608
  const el = (0, import_vue.ref)(null);
5107
5609
  let instance = null;
5108
- const editorOptions = () => Object.fromEntries(EDITOR_OPTION_KEYS.map((key) => [key, props[key]]));
5610
+ const editorOptions = () => Object.fromEntries(
5611
+ EDITOR_OPTION_KEYS.flatMap((key) => {
5612
+ const value = props[key];
5613
+ if ((key === "minScale" || key === "maxScale" || key === "minNodeSize" || key === "maxNodeSize") && value == null) {
5614
+ return [];
5615
+ }
5616
+ return [[key, value]];
5617
+ })
5618
+ );
5109
5619
  const create = () => {
5110
5620
  instance = new VdFlowchart({
5111
5621
  element: el.value,
@@ -5147,7 +5657,8 @@ var VdFlowchart2 = (0, import_vue.defineComponent)({
5147
5657
  redo: () => instance?.redo(),
5148
5658
  canUndo: () => Boolean(instance?.canUndo()),
5149
5659
  canRedo: () => Boolean(instance?.canRedo()),
5150
- layout: (mode, options) => instance?.layout(mode, options)
5660
+ layout: (mode, options) => instance?.layout(mode, options),
5661
+ fitNode: (nodeId, padding) => instance?.fitNode(nodeId, padding)
5151
5662
  });
5152
5663
  return () => (0, import_vue.h)("div", { ref: el, class: "vd-flowchart" });
5153
5664
  }