@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.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;
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);
85
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 }
@@ -362,11 +486,12 @@ function createToolbarButton({ action, label, icon, disabled = false }) {
362
486
  return button;
363
487
  }
364
488
  var DEFAULT_GRID_SIZE = 24;
365
- var MIN_SCALE = 0.35;
366
- var MAX_SCALE = 3;
367
- var MIN_NODE_SIZE = 56;
368
- var MAX_NODE_SIZE = 420;
369
- var WORLD_EXTENT = 12e3;
489
+ var DEFAULT_MIN_SCALE = 0.35;
490
+ var DEFAULT_MAX_SCALE = 3;
491
+ var DEFAULT_MIN_NODE_SIZE = 56;
492
+ var DEFAULT_MAX_NODE_SIZE = 420;
493
+ var DEFAULT_WORLD_EXTENT = 12e3;
494
+ var WIDE_WORLD_EXTENT = 24e4;
370
495
  var MAX_NODES = 1e4;
371
496
  var MAX_EDGES = 1e4;
372
497
  var RESIZE_HANDLES = ["n", "ne", "e", "se", "s", "sw", "w", "nw"];
@@ -398,7 +523,7 @@ var EDGE_STROKE_PRESETS = [
398
523
  { id: "medium", label: "Medium", width: DEFAULT_EDGE_STROKE_WIDTH },
399
524
  { id: "bold", label: "Bold", width: 3.5 }
400
525
  ];
401
- var VD_FLOWCHART_VERSION = "1.4.0";
526
+ var VD_FLOWCHART_VERSION = "1.5.1";
402
527
  var FLOWCHART_DOCUMENT_VERSION = "1.3.0";
403
528
  var FLOWCHART_NODE_TYPES = [
404
529
  "rounded-rect",
@@ -539,6 +664,17 @@ function deepClone(value) {
539
664
  function formatNumber(value) {
540
665
  return Number(value.toFixed(2));
541
666
  }
667
+ function formatScale(value) {
668
+ return Number(toFiniteNumber(value, 1).toFixed(4));
669
+ }
670
+ function normalizeBoundPair(min, max, fallbackMin, fallbackMax) {
671
+ const low = Number(min);
672
+ const high = Number(max);
673
+ if (!Number.isFinite(low) || !Number.isFinite(high) || low <= 0 || high <= 0 || low >= high) {
674
+ return { min: fallbackMin, max: fallbackMax };
675
+ }
676
+ return { min: low, max: high };
677
+ }
542
678
  function sanitizeId(value) {
543
679
  if (value == null) return "";
544
680
  return String(value).trim();
@@ -596,21 +732,36 @@ function normalizeNodeType(type) {
596
732
  function getNodeSpec(type) {
597
733
  return DEFAULT_NODE_SPECS[normalizeNodeType(type)];
598
734
  }
599
- function getNodeSizeBounds(type) {
735
+ function nodeVisualScale(node) {
736
+ if (!node || node.type === "junction") return 1;
737
+ const spec = getNodeSpec(node.type);
738
+ const width = spec.width > 0 ? toFiniteNumber(node.width, spec.width) / spec.width : 1;
739
+ const height = spec.height > 0 ? toFiniteNumber(node.height, spec.height) / spec.height : 1;
740
+ return clamp(Math.min(1, width, height), 0.01, 1);
741
+ }
742
+ function edgeVisualScale(fromNode, toNode) {
743
+ if (fromNode && toNode) return Math.min(nodeVisualScale(fromNode), nodeVisualScale(toNode));
744
+ if (fromNode) return nodeVisualScale(fromNode);
745
+ if (toNode) return nodeVisualScale(toNode);
746
+ return 1;
747
+ }
748
+ function getNodeSizeBounds(type, sizeBounds) {
600
749
  const spec = getNodeSpec(type);
750
+ const min = sizeBounds?.min ?? DEFAULT_MIN_NODE_SIZE;
751
+ const max = sizeBounds?.max ?? DEFAULT_MAX_NODE_SIZE;
601
752
  return {
602
- minWidth: spec.minWidth ?? MIN_NODE_SIZE,
603
- minHeight: spec.minHeight ?? MIN_NODE_SIZE,
604
- maxWidth: spec.maxWidth ?? MAX_NODE_SIZE,
605
- maxHeight: spec.maxHeight ?? MAX_NODE_SIZE
753
+ minWidth: spec.minWidth ?? min,
754
+ minHeight: spec.minHeight ?? min,
755
+ maxWidth: spec.maxWidth ?? max,
756
+ maxHeight: spec.maxHeight ?? max
606
757
  };
607
758
  }
608
- function clampNodeWidth(type, width, fallback) {
609
- const bounds = getNodeSizeBounds(type);
759
+ function clampNodeWidth(type, width, fallback, sizeBounds) {
760
+ const bounds = getNodeSizeBounds(type, sizeBounds);
610
761
  return clamp(toFiniteNumber(width, fallback), bounds.minWidth, bounds.maxWidth);
611
762
  }
612
- function clampNodeHeight(type, height, fallback) {
613
- const bounds = getNodeSizeBounds(type);
763
+ function clampNodeHeight(type, height, fallback, sizeBounds) {
764
+ const bounds = getNodeSizeBounds(type, sizeBounds);
614
765
  return clamp(toFiniteNumber(height, fallback), bounds.minHeight, bounds.maxHeight);
615
766
  }
616
767
  function isNodeResizable(nodeOrType) {
@@ -634,14 +785,16 @@ function getStrokePresetWidth(presetId) {
634
785
  const preset = EDGE_STROKE_PRESETS.find((item) => item.id === presetId);
635
786
  return preset?.width ?? DEFAULT_EDGE_STROKE_WIDTH;
636
787
  }
637
- function normalizeViewport(viewport) {
788
+ function normalizeViewport(viewport, scaleBounds) {
789
+ const min = scaleBounds?.min ?? DEFAULT_MIN_SCALE;
790
+ const max = scaleBounds?.max ?? DEFAULT_MAX_SCALE;
638
791
  return {
639
792
  x: toFiniteNumber(viewport?.x, 0),
640
793
  y: toFiniteNumber(viewport?.y, 0),
641
- scale: clamp(toFiniteNumber(viewport?.scale, 1), MIN_SCALE, MAX_SCALE)
794
+ scale: clamp(formatScale(toFiniteNumber(viewport?.scale, 1)), min, max)
642
795
  };
643
796
  }
644
- function normalizeNode(rawNode, index, usedIds) {
797
+ function normalizeNode(rawNode, index, usedIds, sizeBounds) {
645
798
  const type = normalizeNodeType(rawNode?.type);
646
799
  const spec = getNodeSpec(type);
647
800
  const node = {
@@ -649,8 +802,8 @@ function normalizeNode(rawNode, index, usedIds) {
649
802
  type,
650
803
  x: toFiniteNumber(rawNode?.x, index * 28),
651
804
  y: toFiniteNumber(rawNode?.y, index * 18),
652
- width: clampNodeWidth(type, rawNode?.width, spec.width),
653
- height: clampNodeHeight(type, rawNode?.height, spec.height),
805
+ width: clampNodeWidth(type, rawNode?.width, spec.width, sizeBounds),
806
+ height: clampNodeHeight(type, rawNode?.height, spec.height, sizeBounds),
654
807
  text: rawNode?.text == null ? spec.text : String(rawNode.text),
655
808
  data: isPlainObject(rawNode?.data) ? deepClone(rawNode.data) : {}
656
809
  };
@@ -743,7 +896,7 @@ function isSupportedDocumentVersion(value) {
743
896
  if (parsed.minor === max.minor && parsed.patch > max.patch) return false;
744
897
  return true;
745
898
  }
746
- function normalizeDocument(input) {
899
+ function normalizeDocument(input, limits = {}) {
747
900
  let source = input;
748
901
  if (typeof source === "string") {
749
902
  try {
@@ -766,13 +919,13 @@ function normalizeDocument(input) {
766
919
  }
767
920
  }
768
921
  const usedNodeIds = /* @__PURE__ */ new Set();
769
- const nodes = (Array.isArray(source.nodes) ? source.nodes : []).slice(0, MAX_NODES).map((node, index) => normalizeNode(node, index, usedNodeIds));
922
+ const nodes = (Array.isArray(source.nodes) ? source.nodes : []).slice(0, MAX_NODES).map((node, index) => normalizeNode(node, index, usedNodeIds, limits.sizeBounds));
770
923
  const nodeIds = new Set(nodes.map((node) => node.id));
771
924
  const usedEdgeIds = /* @__PURE__ */ new Set();
772
925
  const edges = (Array.isArray(source.edges) ? source.edges : []).slice(0, MAX_EDGES).map((edge, index) => normalizeEdge(edge, index, nodeIds, usedEdgeIds)).filter(Boolean);
773
926
  return {
774
927
  version: FLOWCHART_DOCUMENT_VERSION,
775
- viewport: normalizeViewport(source.viewport),
928
+ viewport: normalizeViewport(source.viewport, limits.scaleBounds),
776
929
  nodes,
777
930
  edges
778
931
  };
@@ -989,19 +1142,30 @@ function getPolylineLabelPoint(points) {
989
1142
  const last = points[points.length - 1] || { x: 0, y: 0 };
990
1143
  return { x: formatNumber(last.x), y: formatNumber(last.y) };
991
1144
  }
992
- function curveControlPoint(point, port, target, curvature) {
1145
+ function routeMetrics(edgeScale = 1) {
1146
+ const scale = clamp(toFiniteNumber(edgeScale, 1), 0.01, 1);
1147
+ return {
1148
+ scale,
1149
+ minCurveArm: MIN_CURVE_ARM * scale,
1150
+ maxCurveArm: MAX_CURVE_ARM * scale,
1151
+ stub: ORTHOGONAL_STUB_LENGTH * scale,
1152
+ corner: ORTHOGONAL_CORNER_RADIUS * scale,
1153
+ clearance: ORTHOGONAL_CLEARANCE * scale
1154
+ };
1155
+ }
1156
+ function curveControlPoint(point, port, target, curvature, metrics = routeMetrics(1)) {
993
1157
  const normal = getPortNormal(port);
994
1158
  const dx = target.x - point.x;
995
1159
  const dy = target.y - point.y;
996
1160
  const distance = Math.hypot(dx, dy) || 1;
997
1161
  const align = (normal.x * dx + normal.y * dy) / distance;
998
1162
  const ease = 0.55 + 0.45 * align;
999
- const arm = clamp(distance * curvature * ease, MIN_CURVE_ARM, MAX_CURVE_ARM);
1163
+ const arm = clamp(distance * curvature * ease, metrics.minCurveArm, metrics.maxCurveArm);
1000
1164
  return { x: point.x + normal.x * arm, y: point.y + normal.y * arm };
1001
1165
  }
1002
- function buildCurvePath(fromPoint, toPoint, fromPort = "right", toPort = "left") {
1003
- const controlA = curveControlPoint(fromPoint, fromPort, toPoint, EDGE_CURVATURE);
1004
- const controlB = curveControlPoint(toPoint, toPort, fromPoint, EDGE_CURVATURE);
1166
+ function buildCurvePath(fromPoint, toPoint, fromPort = "right", toPort = "left", metrics = routeMetrics(1)) {
1167
+ const controlA = curveControlPoint(fromPoint, fromPort, toPoint, EDGE_CURVATURE, metrics);
1168
+ const controlB = curveControlPoint(toPoint, toPort, fromPoint, EDGE_CURVATURE, metrics);
1005
1169
  const labelX = 0.125 * fromPoint.x + 0.375 * controlA.x + 0.375 * controlB.x + 0.125 * toPoint.x;
1006
1170
  const labelY = 0.125 * fromPoint.y + 0.375 * controlA.y + 0.375 * controlB.y + 0.125 * toPoint.y;
1007
1171
  return {
@@ -1063,13 +1227,40 @@ function getOrthogonalMidpoints(fromStub, toStub, fromPort, toPort) {
1063
1227
  }
1064
1228
  return [{ x: fromStub.x, y: toStub.y }];
1065
1229
  }
1066
- function getOrthogonalPoints(fromPoint, toPoint, fromPort, toPort, fromRect, toRect) {
1230
+ function getOrthogonalPoints(fromPoint, toPoint, fromPort, toPort, fromRect, toRect, metrics = routeMetrics(1)) {
1067
1231
  const fromNormal = getPortNormal(fromPort);
1068
1232
  const toNormal = getPortNormal(toPort);
1069
- const fromStub = offsetPoint(fromPoint, fromNormal, ORTHOGONAL_STUB_LENGTH);
1070
- const toStub = offsetPoint(toPoint, toNormal, ORTHOGONAL_STUB_LENGTH);
1071
1233
  const sourceHorizontal = isHorizontalPort(fromPort);
1072
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);
1073
1264
  const mids = [];
1074
1265
  if (sourceHorizontal && targetHorizontal) {
1075
1266
  const facing = fromNormal.x === -toNormal.x;
@@ -1107,13 +1298,13 @@ function getOrthogonalPoints(fromPoint, toPoint, fromPort, toPort, fromRect, toR
1107
1298
  );
1108
1299
  return points;
1109
1300
  }
1110
- function buildOrthogonalPath(fromPoint, toPoint, fromPort = "right", toPort = "left", fromRect = null, toRect = null) {
1301
+ function buildOrthogonalPath(fromPoint, toPoint, fromPort = "right", toPort = "left", fromRect = null, toRect = null, metrics = routeMetrics(1)) {
1111
1302
  let points;
1112
1303
  if (fromRect && toRect) {
1113
- points = getOrthogonalPoints(fromPoint, toPoint, fromPort, toPort, fromRect, toRect);
1304
+ points = getOrthogonalPoints(fromPoint, toPoint, fromPort, toPort, fromRect, toRect, metrics);
1114
1305
  } else {
1115
- const fromStub = offsetPoint(fromPoint, getPortNormal(fromPort), ORTHOGONAL_STUB_LENGTH);
1116
- const toStub = offsetPoint(toPoint, getPortNormal(toPort), ORTHOGONAL_STUB_LENGTH);
1306
+ const fromStub = offsetPoint(fromPoint, getPortNormal(fromPort), metrics.stub);
1307
+ const toStub = offsetPoint(toPoint, getPortNormal(toPort), metrics.stub);
1117
1308
  points = [];
1118
1309
  [
1119
1310
  fromPoint,
@@ -1125,7 +1316,7 @@ function buildOrthogonalPath(fromPoint, toPoint, fromPort = "right", toPort = "l
1125
1316
  }
1126
1317
  const label = getPolylineLabelPoint(points);
1127
1318
  return {
1128
- d: buildRoundedPath(points, ORTHOGONAL_CORNER_RADIUS),
1319
+ d: buildRoundedPath(points, metrics.corner),
1129
1320
  labelX: label.x,
1130
1321
  labelY: label.y
1131
1322
  };
@@ -1136,6 +1327,7 @@ function buildEdgePath(edge, fromNode = null, toNode = null) {
1136
1327
  const fromPoint = edge?.fromPoint || (fromNode ? getPortPosition(fromNode, fromPort) : null);
1137
1328
  const toPoint = edge?.toPoint || (toNode ? getPortPosition(toNode, toPort) : null);
1138
1329
  const route = normalizeEdgeRoute(edge?.route);
1330
+ const metrics = routeMetrics(edgeVisualScale(fromNode, toNode));
1139
1331
  if (!fromPoint || !toPoint) {
1140
1332
  return { d: "", labelX: 0, labelY: 0 };
1141
1333
  }
@@ -1143,15 +1335,17 @@ function buildEdgePath(edge, fromNode = null, toNode = null) {
1143
1335
  return buildStraightPath(fromPoint, toPoint);
1144
1336
  }
1145
1337
  if (route === "orthogonal") {
1146
- const fromRect = !edge?.fromPoint && fromNode ? getNodeRectBounds(fromNode, ORTHOGONAL_CLEARANCE) : null;
1147
- const toRect = !edge?.toPoint && toNode ? getNodeRectBounds(toNode, ORTHOGONAL_CLEARANCE) : null;
1148
- return buildOrthogonalPath(fromPoint, toPoint, fromPort, toPort, fromRect, toRect);
1338
+ const fromRect = !edge?.fromPoint && fromNode ? getNodeRectBounds(fromNode, metrics.clearance) : null;
1339
+ const toRect = !edge?.toPoint && toNode ? getNodeRectBounds(toNode, metrics.clearance) : null;
1340
+ return buildOrthogonalPath(fromPoint, toPoint, fromPort, toPort, fromRect, toRect, metrics);
1149
1341
  }
1150
- return buildCurvePath(fromPoint, toPoint, fromPort, toPort);
1342
+ return buildCurvePath(fromPoint, toPoint, fromPort, toPort, metrics);
1151
1343
  }
1152
- function createArrowMarker(id, strokeWidth, reversed = false) {
1153
- const size = formatNumber(Math.max(9, strokeWidth * 4.4));
1154
- const refInset = formatNumber(Math.max(1.5, strokeWidth * 0.85));
1344
+ function createArrowMarker(id, strokeWidth, reversed = false, edgeScale = 1) {
1345
+ const scale = clamp(toFiniteNumber(edgeScale, 1), 0.01, 1);
1346
+ const effective = Math.max(0.02, strokeWidth * scale);
1347
+ const size = formatNumber(Math.max(9 * scale, effective * 4.4));
1348
+ const refInset = formatNumber(Math.max(1.5 * scale, effective * 0.85));
1155
1349
  const marker = svgEl("marker", {
1156
1350
  id,
1157
1351
  markerWidth: size,
@@ -1169,9 +1363,11 @@ function createArrowMarker(id, strokeWidth, reversed = false) {
1169
1363
  );
1170
1364
  return marker;
1171
1365
  }
1172
- function createDotMarker(id, strokeWidth) {
1173
- const size = formatNumber(Math.max(8, strokeWidth * 3.3));
1174
- const radius = formatNumber(Math.max(2.75, strokeWidth * 1.45));
1366
+ function createDotMarker(id, strokeWidth, edgeScale = 1) {
1367
+ const scale = clamp(toFiniteNumber(edgeScale, 1), 0.01, 1);
1368
+ const effective = Math.max(0.02, strokeWidth * scale);
1369
+ const size = formatNumber(Math.max(8 * scale, effective * 3.3));
1370
+ const radius = formatNumber(Math.max(2.75 * scale, effective * 1.45));
1175
1371
  const marker = svgEl("marker", {
1176
1372
  id,
1177
1373
  markerWidth: size,
@@ -1428,7 +1624,23 @@ var VdFlowchart = class {
1428
1624
  this.element = resolveElement(options.element || options.target);
1429
1625
  this.readonly = Boolean(options.readonly);
1430
1626
  this.gridSize = clamp(toFiniteNumber(options.gridSize, DEFAULT_GRID_SIZE), 12, 64);
1431
- this.documentData = normalizeDocument(options.data || {});
1627
+ const scaleBounds = normalizeBoundPair(
1628
+ options.minScale,
1629
+ options.maxScale,
1630
+ DEFAULT_MIN_SCALE,
1631
+ DEFAULT_MAX_SCALE
1632
+ );
1633
+ const sizeBounds = normalizeBoundPair(
1634
+ options.minNodeSize,
1635
+ options.maxNodeSize,
1636
+ DEFAULT_MIN_NODE_SIZE,
1637
+ DEFAULT_MAX_NODE_SIZE
1638
+ );
1639
+ this.minScale = scaleBounds.min;
1640
+ this.maxScale = scaleBounds.max;
1641
+ this.minNodeSize = sizeBounds.min;
1642
+ this.maxNodeSize = sizeBounds.max;
1643
+ this.documentData = normalizeDocument(options.data || {}, { scaleBounds, sizeBounds });
1432
1644
  this.listeners = {};
1433
1645
  this.selection = null;
1434
1646
  this.selectedNodeIds = /* @__PURE__ */ new Set();
@@ -1471,6 +1683,7 @@ var VdFlowchart = class {
1471
1683
  this.handleClick = this.handleClick.bind(this);
1472
1684
  this.handleDoubleClick = this.handleDoubleClick.bind(this);
1473
1685
  this.handleWheel = this.handleWheel.bind(this);
1686
+ this.handleCanvasBlur = this.handleCanvasBlur.bind(this);
1474
1687
  this.handleKeyDown = this.handleKeyDown.bind(this);
1475
1688
  this.handleSelectionFieldInput = this.handleSelectionFieldInput.bind(this);
1476
1689
  this.handleSelectionFieldChange = this.handleSelectionFieldChange.bind(this);
@@ -1656,10 +1869,10 @@ var VdFlowchart = class {
1656
1869
  this.world = svgEl("g", { class: "vd-flowchart-world" });
1657
1870
  this.gridRect = svgEl("rect", {
1658
1871
  class: "vd-flowchart-grid",
1659
- x: -WORLD_EXTENT / 2,
1660
- y: -WORLD_EXTENT / 2,
1661
- width: WORLD_EXTENT,
1662
- height: WORLD_EXTENT,
1872
+ x: -DEFAULT_WORLD_EXTENT / 2,
1873
+ y: -DEFAULT_WORLD_EXTENT / 2,
1874
+ width: DEFAULT_WORLD_EXTENT,
1875
+ height: DEFAULT_WORLD_EXTENT,
1663
1876
  fill: `url(#${this.gridPatternId})`
1664
1877
  });
1665
1878
  this.edgesLayer = svgEl("g", { class: "vd-flowchart-edges" });
@@ -1883,6 +2096,7 @@ var VdFlowchart = class {
1883
2096
  this.canvasEl.addEventListener("click", this.handleClick);
1884
2097
  this.canvasEl.addEventListener("dblclick", this.handleDoubleClick);
1885
2098
  this.canvasEl.addEventListener("wheel", this.handleWheel, { passive: false });
2099
+ this.canvasEl.addEventListener("blur", this.handleCanvasBlur);
1886
2100
  this.root.addEventListener("keydown", this.handleKeyDown, true);
1887
2101
  window.addEventListener("pointerup", this.handlePointerUp);
1888
2102
  window.addEventListener("resize", this.handleResize);
@@ -1901,6 +2115,7 @@ var VdFlowchart = class {
1901
2115
  this.canvasEl.removeEventListener("click", this.handleClick);
1902
2116
  this.canvasEl.removeEventListener("dblclick", this.handleDoubleClick);
1903
2117
  this.canvasEl.removeEventListener("wheel", this.handleWheel);
2118
+ this.canvasEl.removeEventListener("blur", this.handleCanvasBlur);
1904
2119
  this.root.removeEventListener("keydown", this.handleKeyDown, true);
1905
2120
  window.removeEventListener("pointerup", this.handlePointerUp);
1906
2121
  window.removeEventListener("resize", this.handleResize);
@@ -1909,6 +2124,19 @@ var VdFlowchart = class {
1909
2124
  if (this.destroyed) return;
1910
2125
  this.render({ inspector: false, json: false });
1911
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
+ }
1912
2140
  updatePaletteState() {
1913
2141
  const arrowArmed = this.activeTool === "arrow";
1914
2142
  this.paletteGrid.querySelectorAll(".vd-flowchart-palette-btn").forEach((button) => {
@@ -2442,7 +2670,7 @@ var VdFlowchart = class {
2442
2670
  const width = this.canvasEl.clientWidth || 800;
2443
2671
  const height = this.canvasEl.clientHeight || 560;
2444
2672
  this.scaleAround(
2445
- clamp(scale, MIN_SCALE, MAX_SCALE) / viewport.scale,
2673
+ clamp(formatScale(scale), this.minScale, this.maxScale) / viewport.scale,
2446
2674
  width / 2,
2447
2675
  height / 2,
2448
2676
  "viewport:zoom"
@@ -2477,7 +2705,8 @@ var VdFlowchart = class {
2477
2705
  const node = normalizeNode(
2478
2706
  { ...data, id: void 0, x: data.x + 24, y: data.y + 24 },
2479
2707
  this.documentData.nodes.length + index,
2480
- usedNodeIds
2708
+ usedNodeIds,
2709
+ this.nodeSizeBounds()
2481
2710
  );
2482
2711
  idMap.set(data.id, node.id);
2483
2712
  return node;
@@ -2509,7 +2738,8 @@ var VdFlowchart = class {
2509
2738
  const node = normalizeNode(
2510
2739
  { ...data, id: void 0, collapsed: void 0, x: data.x + 24, y: data.y + 24 },
2511
2740
  this.documentData.nodes.length,
2512
- usedIds
2741
+ usedIds,
2742
+ this.nodeSizeBounds()
2513
2743
  );
2514
2744
  this.documentData.nodes.push(node);
2515
2745
  this.select({ kind: "node", id: node.id });
@@ -3123,14 +3353,15 @@ var VdFlowchart = class {
3123
3353
  if (this.activeTool === "arrow") return true;
3124
3354
  return this.selection?.kind === "node" && this.selection.id === node.id && !this.hasMultiSelection();
3125
3355
  }
3126
- getMarkerId(markerType, position, strokeWidth) {
3356
+ getMarkerId(markerType, position, strokeWidth, edgeScale = 1) {
3127
3357
  const width = normalizeEdgeStrokeWidth(strokeWidth);
3128
- const key = `${markerType}:${position}:${width}`;
3358
+ const scale = clamp(formatNumber(Math.max(edgeScale, 0.01)), 0.01, 1);
3359
+ const key = `${markerType}:${position}:${width}:${scale}`;
3129
3360
  if (this.markerIds.has(key)) {
3130
3361
  return this.markerIds.get(key);
3131
3362
  }
3132
3363
  const markerId = nextId(`flowchart-${markerType}-${position}`);
3133
- const marker = markerType === "dot" ? createDotMarker(markerId, width) : createArrowMarker(markerId, width, position === "start");
3364
+ const marker = markerType === "dot" ? createDotMarker(markerId, width, scale) : createArrowMarker(markerId, width, position === "start", scale);
3134
3365
  this.markerDefs.appendChild(marker);
3135
3366
  this.markerIds.set(key, markerId);
3136
3367
  return markerId;
@@ -3138,8 +3369,15 @@ var VdFlowchart = class {
3138
3369
  getEdgeStrokeWidth(edge) {
3139
3370
  return normalizeEdgeStrokeWidth(edge?.strokeWidth);
3140
3371
  }
3141
- getEdgeHitStrokeWidth(edge) {
3142
- return formatNumber(Math.max(EDGE_HIT_STROKE_MIN, this.getEdgeStrokeWidth(edge) + 12));
3372
+ getEdgeHitStrokeWidth(edge, edgeScale = 1) {
3373
+ const visual = clamp(toFiniteNumber(edgeScale, 1), 0.01, 1);
3374
+ const base = this.getEdgeStrokeWidth(edge);
3375
+ const view = Math.max(this.documentData?.viewport?.scale || 1, 1e-4);
3376
+ const legacy = Math.max(EDGE_HIT_STROKE_MIN, base + 12);
3377
+ if (visual >= 0.999 && view >= DEFAULT_MIN_SCALE && view <= DEFAULT_MAX_SCALE) {
3378
+ return formatNumber(legacy);
3379
+ }
3380
+ return formatNumber(Math.max(base * visual, EDGE_HIT_STROKE_MIN / view));
3143
3381
  }
3144
3382
  findConnectionTarget(worldPoint, excludeNodeId, previousSnap = null, referencePoint = null) {
3145
3383
  const snapPadding = CONNECTION_SNAP_PADDING / this.documentData.viewport.scale;
@@ -3196,17 +3434,37 @@ var VdFlowchart = class {
3196
3434
  let width = original.width;
3197
3435
  let height = original.height;
3198
3436
  if (handle.includes("e")) {
3199
- width = clampNodeWidth(nextType, original.width + deltaX, original.width);
3437
+ width = clampNodeWidth(
3438
+ nextType,
3439
+ original.width + deltaX,
3440
+ original.width,
3441
+ this.nodeSizeBounds()
3442
+ );
3200
3443
  }
3201
3444
  if (handle.includes("s")) {
3202
- height = clampNodeHeight(nextType, original.height + deltaY, original.height);
3445
+ height = clampNodeHeight(
3446
+ nextType,
3447
+ original.height + deltaY,
3448
+ original.height,
3449
+ this.nodeSizeBounds()
3450
+ );
3203
3451
  }
3204
3452
  if (handle.includes("w")) {
3205
- width = clampNodeWidth(nextType, original.width - deltaX, original.width);
3453
+ width = clampNodeWidth(
3454
+ nextType,
3455
+ original.width - deltaX,
3456
+ original.width,
3457
+ this.nodeSizeBounds()
3458
+ );
3206
3459
  x = original.x + original.width - width;
3207
3460
  }
3208
3461
  if (handle.includes("n")) {
3209
- height = clampNodeHeight(nextType, original.height - deltaY, original.height);
3462
+ height = clampNodeHeight(
3463
+ nextType,
3464
+ original.height - deltaY,
3465
+ original.height,
3466
+ this.nodeSizeBounds()
3467
+ );
3210
3468
  y = original.y + original.height - height;
3211
3469
  }
3212
3470
  return {
@@ -3242,7 +3500,7 @@ var VdFlowchart = class {
3242
3500
  scaleAround(factor, localX, localY, reason) {
3243
3501
  const viewport = this.documentData.viewport;
3244
3502
  const anchor = this.localToWorld(localX, localY);
3245
- const nextScale = clamp(formatNumber(viewport.scale * factor), MIN_SCALE, MAX_SCALE);
3503
+ const nextScale = clamp(formatScale(viewport.scale * factor), this.minScale, this.maxScale);
3246
3504
  if (nextScale === viewport.scale) return;
3247
3505
  viewport.scale = nextScale;
3248
3506
  viewport.x = formatNumber(localX - anchor.x * nextScale);
@@ -3334,7 +3592,7 @@ var VdFlowchart = class {
3334
3592
  if (event.key === "Escape" || event.key === "Enter") {
3335
3593
  event.preventDefault();
3336
3594
  this.stopTextEdit({ commit: event.key === "Enter" });
3337
- this.canvasEl.focus();
3595
+ this.focusCanvas({ quiet: true });
3338
3596
  }
3339
3597
  });
3340
3598
  textarea.addEventListener("blur", () => {
@@ -3361,7 +3619,7 @@ var VdFlowchart = class {
3361
3619
  if (event.key === "Escape") {
3362
3620
  event.preventDefault();
3363
3621
  this.stopTextEdit({ commit: false });
3364
- this.canvasEl.focus();
3622
+ this.focusCanvas({ quiet: true });
3365
3623
  return;
3366
3624
  }
3367
3625
  if (event.key === "Enter") {
@@ -3370,13 +3628,12 @@ var VdFlowchart = class {
3370
3628
  if (!commits) return;
3371
3629
  event.preventDefault();
3372
3630
  this.stopTextEdit({ commit: true });
3373
- this.canvasEl.focus();
3631
+ this.focusCanvas({ quiet: true });
3374
3632
  return;
3375
3633
  }
3376
3634
  if (event.key === "Tab" && mindmap && !event.shiftKey && !event.altKey && !event.ctrlKey) {
3377
3635
  event.preventDefault();
3378
3636
  this.stopTextEdit({ commit: true });
3379
- this.canvasEl.focus();
3380
3637
  this.insertBranchNode(node.id, { edit: true });
3381
3638
  }
3382
3639
  }
@@ -3425,19 +3682,20 @@ var VdFlowchart = class {
3425
3682
  }
3426
3683
  const viewport = this.documentData.viewport;
3427
3684
  const scale = viewport.scale;
3685
+ const visual = nodeVisualScale(node);
3428
3686
  const textarea = this.textEditor.textarea;
3429
3687
  const width = node.width * scale;
3430
3688
  const height = node.height * scale;
3431
3689
  const metrics = getNodeFontMetrics(node);
3432
- const lineHeight = metrics.lineHeight * scale;
3690
+ const lineHeight = metrics.lineHeight * visual * scale;
3433
3691
  const lineCount = Math.max(1, textarea.value.split(/\r?\n/).length);
3434
- const horizontalPadding = (node.type === "textbox" ? 16 : 12) * scale;
3435
- const verticalPadding = node.type === "textbox" ? 12 * scale : Math.max(4, (height - lineCount * lineHeight) / 2);
3692
+ const horizontalPadding = (node.type === "textbox" ? 16 : 12) * visual * scale;
3693
+ const verticalPadding = node.type === "textbox" ? 12 * visual * scale : Math.max(4 * visual, (height - lineCount * lineHeight) / 2);
3436
3694
  textarea.style.left = `${formatNumber(viewport.x + node.x * scale)}px`;
3437
3695
  textarea.style.top = `${formatNumber(viewport.y + node.y * scale)}px`;
3438
3696
  textarea.style.width = `${formatNumber(width)}px`;
3439
3697
  textarea.style.height = `${formatNumber(height)}px`;
3440
- textarea.style.fontSize = `${formatNumber(metrics.fontSize * scale)}px`;
3698
+ textarea.style.fontSize = `${formatNumber(metrics.fontSize * visual * scale)}px`;
3441
3699
  textarea.style.lineHeight = `${formatNumber(lineHeight)}px`;
3442
3700
  textarea.style.padding = `${formatNumber(verticalPadding)}px ${formatNumber(horizontalPadding)}px`;
3443
3701
  }
@@ -3449,14 +3707,16 @@ var VdFlowchart = class {
3449
3707
  this.stopTextEdit({ commit: false });
3450
3708
  return;
3451
3709
  }
3710
+ const visual = edgeVisualScale(fromNode, toNode);
3452
3711
  const { labelX, labelY } = buildEdgePath(edge, fromNode, toNode);
3453
3712
  const viewport = this.documentData.viewport;
3454
3713
  const textarea = this.textEditor.textarea;
3455
- const fontSize = Math.max(11, 13 * viewport.scale);
3456
- const width = clamp(textarea.value.length * fontSize * 0.62 + 28, 96, 280);
3457
- const height = fontSize * 1.4 + 12;
3714
+ const fontSize = Math.max(11 * visual, 13 * visual * viewport.scale);
3715
+ const width = clamp(textarea.value.length * fontSize * 0.62 + 28 * visual, 96 * visual, 280);
3716
+ const height = fontSize * 1.4 + 12 * visual;
3717
+ const labelOffset = 8 * visual;
3458
3718
  textarea.style.left = `${formatNumber(viewport.x + labelX * viewport.scale - width / 2)}px`;
3459
- textarea.style.top = `${formatNumber(viewport.y + (labelY - 8) * viewport.scale - height / 2)}px`;
3719
+ textarea.style.top = `${formatNumber(viewport.y + (labelY - labelOffset) * viewport.scale - height / 2)}px`;
3460
3720
  textarea.style.width = `${formatNumber(width)}px`;
3461
3721
  textarea.style.height = `${formatNumber(height)}px`;
3462
3722
  textarea.style.fontSize = `${formatNumber(fontSize)}px`;
@@ -3465,6 +3725,7 @@ var VdFlowchart = class {
3465
3725
  }
3466
3726
  render(options = {}) {
3467
3727
  if (this.destroyed) return;
3728
+ this.syncGrid();
3468
3729
  const shouldRenderScene = options.scene !== false;
3469
3730
  const shouldRenderInspector = options.inspector !== false;
3470
3731
  const shouldRenderJson = options.json !== false;
@@ -3488,6 +3749,8 @@ var VdFlowchart = class {
3488
3749
  clearChildren(this.previewLayer);
3489
3750
  clearChildren(this.nodesLayer);
3490
3751
  clearChildren(this.overlayLayer);
3752
+ clearChildren(this.markerDefs);
3753
+ this.markerIds = /* @__PURE__ */ new Map();
3491
3754
  const hidden = this.getHiddenNodeIds();
3492
3755
  this.sceneHidden = hidden;
3493
3756
  this.sceneOutgoing = this.getOutgoingMap();
@@ -3551,17 +3814,18 @@ var VdFlowchart = class {
3551
3814
  }
3552
3815
  }
3553
3816
  }
3554
- applyEdgeMarkers(pathElement, edge) {
3817
+ applyEdgeMarkers(pathElement, edge, edgeScale = 1) {
3818
+ const width = this.getEdgeStrokeWidth(edge);
3555
3819
  if (edge.startMarker && edge.startMarker !== "none") {
3556
3820
  pathElement.setAttribute(
3557
3821
  "marker-start",
3558
- `url(#${this.getMarkerId(edge.startMarker, "start", this.getEdgeStrokeWidth(edge))})`
3822
+ `url(#${this.getMarkerId(edge.startMarker, "start", width, edgeScale)})`
3559
3823
  );
3560
3824
  }
3561
3825
  if (edge.endMarker && edge.endMarker !== "none") {
3562
3826
  pathElement.setAttribute(
3563
3827
  "marker-end",
3564
- `url(#${this.getMarkerId(edge.endMarker, "end", this.getEdgeStrokeWidth(edge))})`
3828
+ `url(#${this.getMarkerId(edge.endMarker, "end", width, edgeScale)})`
3565
3829
  );
3566
3830
  }
3567
3831
  }
@@ -3569,6 +3833,7 @@ var VdFlowchart = class {
3569
3833
  const fromNode = nodeMap.get(edge.from.nodeId);
3570
3834
  const toNode = nodeMap.get(edge.to.nodeId);
3571
3835
  if (!fromNode || !toNode) return null;
3836
+ const visual = edgeVisualScale(fromNode, toNode);
3572
3837
  const edgePath = buildEdgePath(edge, fromNode, toNode);
3573
3838
  const selected = this.selection?.kind === "edge" && this.selection.id === edge.id;
3574
3839
  const inReconnectMode = this.reconnectEdgeId === edge.id;
@@ -3576,13 +3841,14 @@ var VdFlowchart = class {
3576
3841
  class: `vd-flowchart-edge${selected ? " is-selected" : ""}${inReconnectMode ? " is-reconnecting" : ""}`,
3577
3842
  "data-edge-id": edge.id
3578
3843
  });
3579
- const strokeWidth = this.getEdgeStrokeWidth(edge);
3844
+ const baseStroke = this.getEdgeStrokeWidth(edge);
3845
+ const strokeWidth = visual < 0.999 ? formatNumber(Math.max(0.02, baseStroke * visual)) : baseStroke;
3580
3846
  if (selected) {
3581
3847
  group.appendChild(
3582
3848
  svgEl("path", {
3583
3849
  class: "vd-flowchart-edge-selection",
3584
3850
  d: edgePath.d,
3585
- "stroke-width": formatNumber(strokeWidth + 7)
3851
+ "stroke-width": visual < 0.999 ? formatNumber((baseStroke + 7) * visual) : formatNumber(strokeWidth + 7)
3586
3852
  })
3587
3853
  );
3588
3854
  }
@@ -3592,23 +3858,28 @@ var VdFlowchart = class {
3592
3858
  "stroke-width": strokeWidth,
3593
3859
  "data-edge-id": edge.id
3594
3860
  });
3595
- this.applyEdgeMarkers(visiblePath, edge);
3861
+ this.applyEdgeMarkers(visiblePath, edge, visual);
3596
3862
  const hitPath = svgEl("path", {
3597
3863
  class: "vd-flowchart-edge-hit",
3598
3864
  d: edgePath.d,
3599
- "stroke-width": this.getEdgeHitStrokeWidth(edge),
3865
+ "stroke-width": this.getEdgeHitStrokeWidth(edge, visual),
3600
3866
  "data-edge-id": edge.id
3601
3867
  });
3602
3868
  group.appendChild(visiblePath);
3603
3869
  group.appendChild(hitPath);
3604
3870
  if (edge.label && this.textEditor?.edgeId !== edge.id) {
3871
+ const labelOffset = visual < 0.999 ? formatNumber(8 * visual) : 8;
3605
3872
  const label = svgEl("text", {
3606
3873
  class: "vd-flowchart-edge-label",
3607
3874
  x: edgePath.labelX,
3608
- y: edgePath.labelY - 8,
3875
+ y: edgePath.labelY - labelOffset,
3609
3876
  "text-anchor": "middle",
3610
3877
  "data-edge-id": edge.id
3611
3878
  });
3879
+ if (visual < 0.999) {
3880
+ label.style.fontSize = `${formatNumber(Math.max(0.06, 12 * visual))}px`;
3881
+ label.style.strokeWidth = `${formatNumber(Math.max(0.03, 5 * visual))}px`;
3882
+ }
3612
3883
  label.textContent = edge.label;
3613
3884
  group.appendChild(label);
3614
3885
  }
@@ -3647,18 +3918,26 @@ var VdFlowchart = class {
3647
3918
  renderPreviewEdge(interaction) {
3648
3919
  const fromPort = interaction.fromPort || interaction.source?.port || "right";
3649
3920
  const toPort = interaction.toPort || interaction.target?.port || "left";
3650
- const edgePath = buildEdgePath({
3651
- strokeWidth: interaction.strokeWidth,
3652
- route: interaction.route,
3653
- fromPoint: interaction.fromPoint,
3654
- toPoint: interaction.toPoint,
3655
- from: { port: fromPort },
3656
- to: { port: toPort }
3657
- });
3921
+ const fromNode = interaction.source?.nodeId ? this.findNode(interaction.source.nodeId) : null;
3922
+ const toNode = interaction.target?.nodeId ? this.findNode(interaction.target.nodeId) : null;
3923
+ const visual = edgeVisualScale(fromNode, toNode);
3924
+ const edgePath = buildEdgePath(
3925
+ {
3926
+ strokeWidth: interaction.strokeWidth,
3927
+ route: interaction.route,
3928
+ fromPoint: interaction.fromPoint,
3929
+ toPoint: interaction.toPoint,
3930
+ from: { port: fromPort },
3931
+ to: { port: toPort }
3932
+ },
3933
+ fromNode,
3934
+ toNode
3935
+ );
3936
+ const baseStroke = this.getEdgeStrokeWidth(interaction);
3658
3937
  return svgEl("path", {
3659
3938
  class: "vd-flowchart-preview-path",
3660
3939
  d: edgePath.d,
3661
- "stroke-width": this.getEdgeStrokeWidth(interaction)
3940
+ "stroke-width": visual < 0.999 ? formatNumber(Math.max(0.02, baseStroke * visual)) : baseStroke
3662
3941
  });
3663
3942
  }
3664
3943
  renderNode(node) {
@@ -3673,17 +3952,21 @@ var VdFlowchart = class {
3673
3952
  "data-node-id": node.id
3674
3953
  });
3675
3954
  const portsVisible = this.shouldShowNodePorts(node);
3955
+ const visual = nodeVisualScale(node);
3956
+ const corner = node.type === "rounded-rect" || node.type === "textbox" ? 16 : node.type === "junction" ? node.width / 2 : 6;
3676
3957
  const hitbox = svgEl("rect", {
3677
3958
  x: 0,
3678
3959
  y: 0,
3679
3960
  width: node.width,
3680
3961
  height: node.height,
3681
- rx: node.type === "rounded-rect" || node.type === "textbox" ? 16 : node.type === "junction" ? node.width / 2 : 6,
3962
+ rx: visual < 0.999 && node.type !== "junction" ? formatNumber(corner * visual) : corner,
3682
3963
  fill: "transparent",
3683
3964
  "data-node-id": node.id
3684
3965
  });
3685
3966
  group.appendChild(hitbox);
3686
- group.appendChild(this.renderNodeShape(node));
3967
+ const shape = this.renderNodeShape(node);
3968
+ this.applyScaledNodeShape(node, shape);
3969
+ group.appendChild(shape);
3687
3970
  group.appendChild(this.renderNodeText(node));
3688
3971
  const scale = this.documentData.viewport.scale || 1;
3689
3972
  if (soloSelected && !this.readonly && SELECTION_BOX_TYPES.has(node.type)) {
@@ -3875,6 +4158,21 @@ var VdFlowchart = class {
3875
4158
  });
3876
4159
  return group;
3877
4160
  }
4161
+ applyScaledNodeShape(node, shape) {
4162
+ const visual = nodeVisualScale(node);
4163
+ if (!shape || visual >= 0.999) return;
4164
+ const baseStroke = node.type === "junction" ? 1.25 : 1.5;
4165
+ shape.style.strokeWidth = String(formatNumber(Math.max(0.03, baseStroke * visual)));
4166
+ if (node.type === "textbox") {
4167
+ shape.style.strokeDasharray = `${formatNumber(8 * visual)} ${formatNumber(5 * visual)}`;
4168
+ }
4169
+ const baseRadius = node.type === "rounded-rect" ? 18 : node.type === "textbox" ? 14 : node.type === "rect" ? 2 : 0;
4170
+ if (baseRadius > 0) {
4171
+ const scaled = String(formatNumber(baseRadius * visual));
4172
+ shape.setAttribute("rx", scaled);
4173
+ shape.setAttribute("ry", scaled);
4174
+ }
4175
+ }
3878
4176
  renderNodeShape(node) {
3879
4177
  const baseClass = `vd-flowchart-node-shape vd-flowchart-node-shape--${node.type}`;
3880
4178
  if (node.type === "rounded-rect") {
@@ -3951,21 +4249,28 @@ var VdFlowchart = class {
3951
4249
  if (node.type === "junction" || !node.text) {
3952
4250
  return textElement;
3953
4251
  }
4252
+ const visual = nodeVisualScale(node);
4253
+ const metrics = getNodeFontMetrics(node);
4254
+ const lineHeight = visual < 0.999 ? Math.max(0.06, metrics.lineHeight * visual) : metrics.lineHeight;
4255
+ if (visual < 0.999) {
4256
+ textElement.style.fontSize = `${formatNumber(Math.max(0.05, metrics.fontSize * visual))}px`;
4257
+ }
4258
+ const equivalentWidth = visual < 0.999 ? node.width / visual : node.width;
3954
4259
  if (node.type === "textbox") {
3955
- const lines2 = wrapText(node.text, estimateChars(node.width - 32));
3956
- const { lineHeight: lineHeight2 } = getNodeFontMetrics(node);
4260
+ const lines2 = wrapText(node.text, estimateChars(equivalentWidth - 32));
4261
+ const padX = visual < 0.999 ? formatNumber(16 * visual) : 16;
4262
+ const startY2 = visual < 0.999 ? formatNumber(28 * visual) : 28;
3957
4263
  lines2.forEach((line, index) => {
3958
4264
  const span = svgEl("tspan", {
3959
- x: 16,
3960
- y: 28 + index * lineHeight2
4265
+ x: padX,
4266
+ y: visual < 0.999 ? formatNumber(startY2 + index * lineHeight) : startY2 + index * lineHeight
3961
4267
  });
3962
4268
  span.textContent = line;
3963
4269
  textElement.appendChild(span);
3964
4270
  });
3965
4271
  return textElement;
3966
4272
  }
3967
- const lines = wrapText(node.text, estimateChars(node.width));
3968
- const { lineHeight } = getNodeFontMetrics(node);
4273
+ const lines = wrapText(node.text, estimateChars(equivalentWidth));
3969
4274
  const totalHeight = (lines.length - 1) * lineHeight;
3970
4275
  const startY = node.height / 2 - totalHeight / 2;
3971
4276
  textElement.setAttribute("text-anchor", "middle");
@@ -3973,7 +4278,7 @@ var VdFlowchart = class {
3973
4278
  lines.forEach((line, index) => {
3974
4279
  const span = svgEl("tspan", {
3975
4280
  x: node.width / 2,
3976
- y: startY + index * lineHeight,
4281
+ y: visual < 0.999 ? formatNumber(startY + index * lineHeight) : startY + index * lineHeight,
3977
4282
  "text-anchor": "middle",
3978
4283
  "dominant-baseline": "middle"
3979
4284
  });
@@ -4025,12 +4330,12 @@ var VdFlowchart = class {
4025
4330
  textArea.setAttribute("data-field", "node-text");
4026
4331
  const widthInput = createElement("input", { value: node.width, type: "number" });
4027
4332
  widthInput.setAttribute("data-field", "node-width");
4028
- widthInput.setAttribute("min", String(MIN_NODE_SIZE));
4029
- widthInput.setAttribute("max", String(MAX_NODE_SIZE));
4333
+ widthInput.setAttribute("min", String(this.minNodeSize));
4334
+ widthInput.setAttribute("max", String(this.maxNodeSize));
4030
4335
  const heightInput = createElement("input", { value: node.height, type: "number" });
4031
4336
  heightInput.setAttribute("data-field", "node-height");
4032
- heightInput.setAttribute("min", String(MIN_NODE_SIZE));
4033
- heightInput.setAttribute("max", String(MAX_NODE_SIZE));
4337
+ heightInput.setAttribute("min", String(this.minNodeSize));
4338
+ heightInput.setAttribute("max", String(this.maxNodeSize));
4034
4339
  this.selectionFields.appendChild(createField("Type", typeSelect));
4035
4340
  if (isNodeTextEditable(node)) {
4036
4341
  this.selectionFields.appendChild(createField("Text", textArea));
@@ -4344,8 +4649,8 @@ var VdFlowchart = class {
4344
4649
  this.reconnectEdgeId = null;
4345
4650
  const currentViewport = deepClone(this.documentData.viewport);
4346
4651
  const previousSelection = this.selection;
4347
- this.documentData = normalizeDocument(snapshot);
4348
- this.documentData.viewport = normalizeViewport(currentViewport);
4652
+ this.documentData = normalizeDocument(snapshot, this.normalizationLimits());
4653
+ this.documentData.viewport = normalizeViewport(currentViewport, this.scaleBounds());
4349
4654
  this.selection = this.resolvePreservedSelection(previousSelection, true);
4350
4655
  this.render();
4351
4656
  this.emitChange(reason);
@@ -4422,7 +4727,8 @@ var VdFlowchart = class {
4422
4727
  y: partialNode.y == null ? fallbackY : partialNode.y
4423
4728
  },
4424
4729
  this.documentData.nodes.length,
4425
- usedIds
4730
+ usedIds,
4731
+ this.nodeSizeBounds()
4426
4732
  );
4427
4733
  this.documentData.nodes.push(node);
4428
4734
  this.select({ kind: "node", id: node.id });
@@ -4635,7 +4941,7 @@ var VdFlowchart = class {
4635
4941
  changed = true;
4636
4942
  }
4637
4943
  });
4638
- const rerouted = options.reroutePorts === false ? false : this.rerouteEdgePorts();
4944
+ const rerouted = options.reroutePorts === false ? false : this.rerouteEdgePorts(resolvedMode === "tree" ? options.direction || "right" : null);
4639
4945
  if (!changed && !rerouted) return this;
4640
4946
  this.render();
4641
4947
  this.emitChange("layout", { mode: resolvedMode });
@@ -4648,8 +4954,16 @@ var VdFlowchart = class {
4648
4954
  // Re-pick each edge's from/to port from the nodes' current centers, so a
4649
4955
  // freshly laid-out graph attaches connectors on sensible sides. Returns
4650
4956
  // whether any port actually changed.
4651
- rerouteEdgePorts() {
4957
+ rerouteEdgePorts(flowDirection = null) {
4652
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
+ }
4653
4967
  this.documentData.edges.forEach((edge) => {
4654
4968
  const fromNode = this.findNode(edge.from.nodeId);
4655
4969
  const toNode = this.findNode(edge.to.nodeId);
@@ -4659,8 +4973,82 @@ var VdFlowchart = class {
4659
4973
  y: fromNode.y + fromNode.height / 2
4660
4974
  };
4661
4975
  const toCenter = { x: toNode.x + toNode.width / 2, y: toNode.y + toNode.height / 2 };
4662
- const nextFrom = getPortByDirection(fromNode, toCenter).port;
4663
- 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
+ }
4664
5052
  if (nextFrom !== edge.from.port || nextTo !== edge.to.port) {
4665
5053
  edge.from.port = nextFrom;
4666
5054
  edge.to.port = nextTo;
@@ -4675,8 +5063,8 @@ var VdFlowchart = class {
4675
5063
  node.x = patch.x == null ? node.x : formatNumber(toFiniteNumber(patch.x, node.x));
4676
5064
  node.y = patch.y == null ? node.y : formatNumber(toFiniteNumber(patch.y, node.y));
4677
5065
  node.text = isNodeTextEditable(nextType) ? patch.text == null ? node.text : String(patch.text) : getNodeSpec(nextType).text;
4678
- node.width = clampNodeWidth(nextType, patch.width, node.width);
4679
- node.height = clampNodeHeight(nextType, patch.height, node.height);
5066
+ node.width = clampNodeWidth(nextType, patch.width, node.width, this.nodeSizeBounds());
5067
+ node.height = clampNodeHeight(nextType, patch.height, node.height, this.nodeSizeBounds());
4680
5068
  if (isPlainObject(patch.data)) {
4681
5069
  node.data = deepClone(patch.data);
4682
5070
  }
@@ -4865,8 +5253,45 @@ var VdFlowchart = class {
4865
5253
  if (this.selection.kind === "node") return this.removeNode(this.selection.id);
4866
5254
  return this.removeEdge(this.selection.id);
4867
5255
  }
5256
+ nodeSizeBounds() {
5257
+ return { min: this.minNodeSize, max: this.maxNodeSize };
5258
+ }
5259
+ scaleBounds() {
5260
+ return { min: this.minScale, max: this.maxScale };
5261
+ }
5262
+ normalizationLimits() {
5263
+ return { scaleBounds: this.scaleBounds(), sizeBounds: this.nodeSizeBounds() };
5264
+ }
5265
+ hasWideCamera() {
5266
+ return this.minScale < DEFAULT_MIN_SCALE || this.maxScale > DEFAULT_MAX_SCALE;
5267
+ }
5268
+ syncGrid() {
5269
+ const wide = this.hasWideCamera();
5270
+ const extent = wide ? WIDE_WORLD_EXTENT : DEFAULT_WORLD_EXTENT;
5271
+ this.gridRect?.setAttribute("x", String(-extent / 2));
5272
+ this.gridRect?.setAttribute("y", String(-extent / 2));
5273
+ this.gridRect?.setAttribute("width", String(extent));
5274
+ this.gridRect?.setAttribute("height", String(extent));
5275
+ const pattern = this.svg?.querySelector("pattern");
5276
+ const path = pattern?.querySelector("path");
5277
+ if (!pattern || !path) return;
5278
+ let tile = this.gridSize;
5279
+ if (wide) {
5280
+ const scale = Math.max(this.minScale, this.documentData?.viewport?.scale || 1);
5281
+ while (tile * scale < 12 && tile < extent / 8) tile *= 4;
5282
+ while (tile * scale > 48 && tile > 1.5) tile /= 4;
5283
+ tile = formatNumber(tile);
5284
+ path.setAttribute("vector-effect", "non-scaling-stroke");
5285
+ } else {
5286
+ path.removeAttribute("vector-effect");
5287
+ }
5288
+ path.setAttribute("stroke-width", "1");
5289
+ pattern.setAttribute("width", String(tile));
5290
+ pattern.setAttribute("height", String(tile));
5291
+ path.setAttribute("d", `M ${tile} 0 L 0 0 0 ${tile}`);
5292
+ }
4868
5293
  setViewport(viewport) {
4869
- this.documentData.viewport = normalizeViewport(viewport);
5294
+ this.documentData.viewport = normalizeViewport(viewport, this.scaleBounds());
4870
5295
  this.render({ inspector: false, json: true });
4871
5296
  this.emitViewportChange("viewport:set");
4872
5297
  return this;
@@ -4884,7 +5309,7 @@ var VdFlowchart = class {
4884
5309
  return this;
4885
5310
  }
4886
5311
  resetView() {
4887
- this.documentData.viewport = normalizeViewport({ x: 0, y: 0, scale: 1 });
5312
+ this.documentData.viewport = normalizeViewport({ x: 0, y: 0, scale: 1 }, this.scaleBounds());
4888
5313
  this.render({ inspector: false, json: true });
4889
5314
  this.emitViewportChange("viewport:reset");
4890
5315
  return this;
@@ -4900,30 +5325,64 @@ var VdFlowchart = class {
4900
5325
  const padding = 80;
4901
5326
  const contentWidth = Math.max(1, bounds.right - bounds.left);
4902
5327
  const contentHeight = Math.max(1, bounds.bottom - bounds.top);
4903
- const scale = clamp(
4904
- Math.min((width - padding * 2) / contentWidth, (height - padding * 2) / contentHeight),
4905
- MIN_SCALE,
4906
- MAX_SCALE
5328
+ const scale = formatScale(
5329
+ clamp(
5330
+ Math.min((width - padding * 2) / contentWidth, (height - padding * 2) / contentHeight),
5331
+ this.minScale,
5332
+ this.maxScale
5333
+ )
4907
5334
  );
4908
5335
  this.documentData.viewport = {
4909
5336
  x: formatNumber(width / 2 - (bounds.left + bounds.right) / 2 * scale),
4910
5337
  y: formatNumber(height / 2 - (bounds.top + bounds.bottom) / 2 * scale),
4911
- scale: formatNumber(scale)
5338
+ scale
4912
5339
  };
4913
5340
  this.render({ inspector: false, json: true });
4914
5341
  this.emitViewportChange("viewport:fit");
4915
5342
  return this;
4916
5343
  }
5344
+ fitNode(nodeId, padding = 48) {
5345
+ const node = this.findNode(nodeId);
5346
+ if (!node || this.isNodeHidden(node.id)) return this;
5347
+ const pad = Math.max(0, toFiniteNumber(padding, 48));
5348
+ const width = this.canvasEl.clientWidth || 800;
5349
+ const height = this.canvasEl.clientHeight || 560;
5350
+ const availableWidth = Math.max(1, width - pad * 2);
5351
+ const availableHeight = Math.max(1, height - pad * 2);
5352
+ const scale = formatScale(
5353
+ clamp(
5354
+ Math.min(
5355
+ availableWidth / Math.max(1, node.width),
5356
+ availableHeight / Math.max(1, node.height)
5357
+ ),
5358
+ this.minScale,
5359
+ this.maxScale
5360
+ )
5361
+ );
5362
+ const centerX = node.x + node.width / 2;
5363
+ const centerY = node.y + node.height / 2;
5364
+ this.documentData.viewport = {
5365
+ x: formatNumber(width / 2 - centerX * scale),
5366
+ y: formatNumber(height / 2 - centerY * scale),
5367
+ scale
5368
+ };
5369
+ this.render({ inspector: false, json: true });
5370
+ this.emitViewportChange("viewport:fit-node");
5371
+ return this;
5372
+ }
4917
5373
  clear() {
4918
5374
  this.stopTextEdit({ commit: false });
4919
5375
  this.activeTool = null;
4920
5376
  this.reconnectEdgeId = null;
4921
5377
  this.clipboard = null;
4922
- this.documentData = normalizeDocument({
4923
- nodes: [],
4924
- edges: [],
4925
- viewport: { x: 0, y: 0, scale: 1 }
4926
- });
5378
+ this.documentData = normalizeDocument(
5379
+ {
5380
+ nodes: [],
5381
+ edges: [],
5382
+ viewport: { x: 0, y: 0, scale: 1 }
5383
+ },
5384
+ this.normalizationLimits()
5385
+ );
4927
5386
  this.selection = null;
4928
5387
  this.render();
4929
5388
  this.emitChange("clear");
@@ -4958,6 +5417,33 @@ var VdFlowchart = class {
4958
5417
  if ("snapGuides" in options) this.snapGuides = options.snapGuides !== false;
4959
5418
  if ("minimap" in options) this.setMinimapEnabled(options.minimap !== false);
4960
5419
  if ("autoLayout" in options) this.autoLayout = Boolean(options.autoLayout);
5420
+ if ("minScale" in options || "maxScale" in options || "minNodeSize" in options || "maxNodeSize" in options) {
5421
+ const nextScale = normalizeBoundPair(
5422
+ "minScale" in options ? options.minScale : this.minScale,
5423
+ "maxScale" in options ? options.maxScale : this.maxScale,
5424
+ DEFAULT_MIN_SCALE,
5425
+ DEFAULT_MAX_SCALE
5426
+ );
5427
+ const nextSize = normalizeBoundPair(
5428
+ "minNodeSize" in options ? options.minNodeSize : this.minNodeSize,
5429
+ "maxNodeSize" in options ? options.maxNodeSize : this.maxNodeSize,
5430
+ DEFAULT_MIN_NODE_SIZE,
5431
+ DEFAULT_MAX_NODE_SIZE
5432
+ );
5433
+ this.minScale = nextScale.min;
5434
+ this.maxScale = nextScale.max;
5435
+ this.minNodeSize = nextSize.min;
5436
+ this.maxNodeSize = nextSize.max;
5437
+ const clamped = clamp(
5438
+ formatScale(this.documentData.viewport.scale),
5439
+ this.minScale,
5440
+ this.maxScale
5441
+ );
5442
+ if (clamped !== this.documentData.viewport.scale) {
5443
+ this.documentData.viewport.scale = clamped;
5444
+ this.pendingBoundsEvent = true;
5445
+ }
5446
+ }
4961
5447
  this.syncCanvasLabel();
4962
5448
  if (this.shortcutsHelpOpen) this.renderShortcutsHelp();
4963
5449
  if ("history" in options && options.history !== false !== this.historyEnabled) {
@@ -4975,11 +5461,15 @@ var VdFlowchart = class {
4975
5461
  }
4976
5462
  }
4977
5463
  this.render();
5464
+ if (this.pendingBoundsEvent) {
5465
+ this.pendingBoundsEvent = false;
5466
+ this.emitViewportChange("viewport:bounds");
5467
+ }
4978
5468
  this.updateHistoryButtons();
4979
5469
  return this;
4980
5470
  }
4981
5471
  load(data, options = {}) {
4982
- const documentData = normalizeDocument(data);
5472
+ const documentData = normalizeDocument(data, this.normalizationLimits());
4983
5473
  if (options.silent && JSON.stringify(documentData) === JSON.stringify(this.documentData)) {
4984
5474
  return this;
4985
5475
  }
@@ -5037,7 +5527,11 @@ var EDITOR_OPTION_KEYS = [
5037
5527
  "keyboardShortcuts",
5038
5528
  "snapGuides",
5039
5529
  "minimap",
5040
- "autoLayout"
5530
+ "autoLayout",
5531
+ "minScale",
5532
+ "maxScale",
5533
+ "minNodeSize",
5534
+ "maxNodeSize"
5041
5535
  ];
5042
5536
  var VdFlowchart2 = defineComponent({
5043
5537
  name: "VdFlowchart",
@@ -5063,13 +5557,29 @@ var VdFlowchart2 = defineComponent({
5063
5557
  /** Show the overview minimap in the canvas corner (default true). */
5064
5558
  minimap: { type: Boolean, default: true },
5065
5559
  /** Re-run the current layout after keyboard or handle insertions (default false). */
5066
- autoLayout: { type: Boolean, default: false }
5560
+ autoLayout: { type: Boolean, default: false },
5561
+ /** Lowest camera scale. Defaults to 0.35. Set it together with maxScale. */
5562
+ minScale: { type: Number, default: void 0 },
5563
+ /** Highest camera scale. Defaults to 3. Set it together with minScale. */
5564
+ maxScale: { type: Number, default: void 0 },
5565
+ /** Smallest resizable-node side. Defaults to 56. Set it together with maxNodeSize. */
5566
+ minNodeSize: { type: Number, default: void 0 },
5567
+ /** Largest resizable-node side. Defaults to 420. Set it together with minNodeSize. */
5568
+ maxNodeSize: { type: Number, default: void 0 }
5067
5569
  },
5068
5570
  emits: ["change", "select", "viewport", "connect", "ready"],
5069
5571
  setup(props, { emit, expose }) {
5070
5572
  const el = ref(null);
5071
5573
  let instance = null;
5072
- const editorOptions = () => Object.fromEntries(EDITOR_OPTION_KEYS.map((key) => [key, props[key]]));
5574
+ const editorOptions = () => Object.fromEntries(
5575
+ EDITOR_OPTION_KEYS.flatMap((key) => {
5576
+ const value = props[key];
5577
+ if ((key === "minScale" || key === "maxScale" || key === "minNodeSize" || key === "maxNodeSize") && value == null) {
5578
+ return [];
5579
+ }
5580
+ return [[key, value]];
5581
+ })
5582
+ );
5073
5583
  const create = () => {
5074
5584
  instance = new VdFlowchart({
5075
5585
  element: el.value,
@@ -5111,7 +5621,8 @@ var VdFlowchart2 = defineComponent({
5111
5621
  redo: () => instance?.redo(),
5112
5622
  canUndo: () => Boolean(instance?.canUndo()),
5113
5623
  canRedo: () => Boolean(instance?.canRedo()),
5114
- layout: (mode, options) => instance?.layout(mode, options)
5624
+ layout: (mode, options) => instance?.layout(mode, options),
5625
+ fitNode: (nodeId, padding) => instance?.fitNode(nodeId, padding)
5115
5626
  });
5116
5627
  return () => h("div", { ref: el, class: "vd-flowchart" });
5117
5628
  }