@vanduo-oss/vd3-flowchart 1.4.0 → 1.5.0

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
@@ -398,11 +398,12 @@ function createToolbarButton({ action, label, icon, disabled = false }) {
398
398
  return button;
399
399
  }
400
400
  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;
401
+ var DEFAULT_MIN_SCALE = 0.35;
402
+ var DEFAULT_MAX_SCALE = 3;
403
+ var DEFAULT_MIN_NODE_SIZE = 56;
404
+ var DEFAULT_MAX_NODE_SIZE = 420;
405
+ var DEFAULT_WORLD_EXTENT = 12e3;
406
+ var WIDE_WORLD_EXTENT = 24e4;
406
407
  var MAX_NODES = 1e4;
407
408
  var MAX_EDGES = 1e4;
408
409
  var RESIZE_HANDLES = ["n", "ne", "e", "se", "s", "sw", "w", "nw"];
@@ -434,7 +435,7 @@ var EDGE_STROKE_PRESETS = [
434
435
  { id: "medium", label: "Medium", width: DEFAULT_EDGE_STROKE_WIDTH },
435
436
  { id: "bold", label: "Bold", width: 3.5 }
436
437
  ];
437
- var VD_FLOWCHART_VERSION = "1.4.0";
438
+ var VD_FLOWCHART_VERSION = "1.5.0";
438
439
  var FLOWCHART_DOCUMENT_VERSION = "1.3.0";
439
440
  var FLOWCHART_NODE_TYPES = [
440
441
  "rounded-rect",
@@ -575,6 +576,17 @@ function deepClone(value) {
575
576
  function formatNumber(value) {
576
577
  return Number(value.toFixed(2));
577
578
  }
579
+ function formatScale(value) {
580
+ return Number(toFiniteNumber(value, 1).toFixed(4));
581
+ }
582
+ function normalizeBoundPair(min, max, fallbackMin, fallbackMax) {
583
+ const low = Number(min);
584
+ const high = Number(max);
585
+ if (!Number.isFinite(low) || !Number.isFinite(high) || low <= 0 || high <= 0 || low >= high) {
586
+ return { min: fallbackMin, max: fallbackMax };
587
+ }
588
+ return { min: low, max: high };
589
+ }
578
590
  function sanitizeId(value) {
579
591
  if (value == null) return "";
580
592
  return String(value).trim();
@@ -632,21 +644,36 @@ function normalizeNodeType(type) {
632
644
  function getNodeSpec(type) {
633
645
  return DEFAULT_NODE_SPECS[normalizeNodeType(type)];
634
646
  }
635
- function getNodeSizeBounds(type) {
647
+ function nodeVisualScale(node) {
648
+ if (!node || node.type === "junction") return 1;
649
+ const spec = getNodeSpec(node.type);
650
+ const width = spec.width > 0 ? toFiniteNumber(node.width, spec.width) / spec.width : 1;
651
+ const height = spec.height > 0 ? toFiniteNumber(node.height, spec.height) / spec.height : 1;
652
+ return clamp(Math.min(1, width, height), 0.01, 1);
653
+ }
654
+ function edgeVisualScale(fromNode, toNode) {
655
+ if (fromNode && toNode) return Math.min(nodeVisualScale(fromNode), nodeVisualScale(toNode));
656
+ if (fromNode) return nodeVisualScale(fromNode);
657
+ if (toNode) return nodeVisualScale(toNode);
658
+ return 1;
659
+ }
660
+ function getNodeSizeBounds(type, sizeBounds) {
636
661
  const spec = getNodeSpec(type);
662
+ const min = sizeBounds?.min ?? DEFAULT_MIN_NODE_SIZE;
663
+ const max = sizeBounds?.max ?? DEFAULT_MAX_NODE_SIZE;
637
664
  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
665
+ minWidth: spec.minWidth ?? min,
666
+ minHeight: spec.minHeight ?? min,
667
+ maxWidth: spec.maxWidth ?? max,
668
+ maxHeight: spec.maxHeight ?? max
642
669
  };
643
670
  }
644
- function clampNodeWidth(type, width, fallback) {
645
- const bounds = getNodeSizeBounds(type);
671
+ function clampNodeWidth(type, width, fallback, sizeBounds) {
672
+ const bounds = getNodeSizeBounds(type, sizeBounds);
646
673
  return clamp(toFiniteNumber(width, fallback), bounds.minWidth, bounds.maxWidth);
647
674
  }
648
- function clampNodeHeight(type, height, fallback) {
649
- const bounds = getNodeSizeBounds(type);
675
+ function clampNodeHeight(type, height, fallback, sizeBounds) {
676
+ const bounds = getNodeSizeBounds(type, sizeBounds);
650
677
  return clamp(toFiniteNumber(height, fallback), bounds.minHeight, bounds.maxHeight);
651
678
  }
652
679
  function isNodeResizable(nodeOrType) {
@@ -670,14 +697,16 @@ function getStrokePresetWidth(presetId) {
670
697
  const preset = EDGE_STROKE_PRESETS.find((item) => item.id === presetId);
671
698
  return preset?.width ?? DEFAULT_EDGE_STROKE_WIDTH;
672
699
  }
673
- function normalizeViewport(viewport) {
700
+ function normalizeViewport(viewport, scaleBounds) {
701
+ const min = scaleBounds?.min ?? DEFAULT_MIN_SCALE;
702
+ const max = scaleBounds?.max ?? DEFAULT_MAX_SCALE;
674
703
  return {
675
704
  x: toFiniteNumber(viewport?.x, 0),
676
705
  y: toFiniteNumber(viewport?.y, 0),
677
- scale: clamp(toFiniteNumber(viewport?.scale, 1), MIN_SCALE, MAX_SCALE)
706
+ scale: clamp(formatScale(toFiniteNumber(viewport?.scale, 1)), min, max)
678
707
  };
679
708
  }
680
- function normalizeNode(rawNode, index, usedIds) {
709
+ function normalizeNode(rawNode, index, usedIds, sizeBounds) {
681
710
  const type = normalizeNodeType(rawNode?.type);
682
711
  const spec = getNodeSpec(type);
683
712
  const node = {
@@ -685,8 +714,8 @@ function normalizeNode(rawNode, index, usedIds) {
685
714
  type,
686
715
  x: toFiniteNumber(rawNode?.x, index * 28),
687
716
  y: toFiniteNumber(rawNode?.y, index * 18),
688
- width: clampNodeWidth(type, rawNode?.width, spec.width),
689
- height: clampNodeHeight(type, rawNode?.height, spec.height),
717
+ width: clampNodeWidth(type, rawNode?.width, spec.width, sizeBounds),
718
+ height: clampNodeHeight(type, rawNode?.height, spec.height, sizeBounds),
690
719
  text: rawNode?.text == null ? spec.text : String(rawNode.text),
691
720
  data: isPlainObject(rawNode?.data) ? deepClone(rawNode.data) : {}
692
721
  };
@@ -779,7 +808,7 @@ function isSupportedDocumentVersion(value) {
779
808
  if (parsed.minor === max.minor && parsed.patch > max.patch) return false;
780
809
  return true;
781
810
  }
782
- function normalizeDocument(input) {
811
+ function normalizeDocument(input, limits = {}) {
783
812
  let source = input;
784
813
  if (typeof source === "string") {
785
814
  try {
@@ -802,13 +831,13 @@ function normalizeDocument(input) {
802
831
  }
803
832
  }
804
833
  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));
834
+ const nodes = (Array.isArray(source.nodes) ? source.nodes : []).slice(0, MAX_NODES).map((node, index) => normalizeNode(node, index, usedNodeIds, limits.sizeBounds));
806
835
  const nodeIds = new Set(nodes.map((node) => node.id));
807
836
  const usedEdgeIds = /* @__PURE__ */ new Set();
808
837
  const edges = (Array.isArray(source.edges) ? source.edges : []).slice(0, MAX_EDGES).map((edge, index) => normalizeEdge(edge, index, nodeIds, usedEdgeIds)).filter(Boolean);
809
838
  return {
810
839
  version: FLOWCHART_DOCUMENT_VERSION,
811
- viewport: normalizeViewport(source.viewport),
840
+ viewport: normalizeViewport(source.viewport, limits.scaleBounds),
812
841
  nodes,
813
842
  edges
814
843
  };
@@ -1025,19 +1054,30 @@ function getPolylineLabelPoint(points) {
1025
1054
  const last = points[points.length - 1] || { x: 0, y: 0 };
1026
1055
  return { x: formatNumber(last.x), y: formatNumber(last.y) };
1027
1056
  }
1028
- function curveControlPoint(point, port, target, curvature) {
1057
+ function routeMetrics(edgeScale = 1) {
1058
+ const scale = clamp(toFiniteNumber(edgeScale, 1), 0.01, 1);
1059
+ return {
1060
+ scale,
1061
+ minCurveArm: MIN_CURVE_ARM * scale,
1062
+ maxCurveArm: MAX_CURVE_ARM * scale,
1063
+ stub: ORTHOGONAL_STUB_LENGTH * scale,
1064
+ corner: ORTHOGONAL_CORNER_RADIUS * scale,
1065
+ clearance: ORTHOGONAL_CLEARANCE * scale
1066
+ };
1067
+ }
1068
+ function curveControlPoint(point, port, target, curvature, metrics = routeMetrics(1)) {
1029
1069
  const normal = getPortNormal(port);
1030
1070
  const dx = target.x - point.x;
1031
1071
  const dy = target.y - point.y;
1032
1072
  const distance = Math.hypot(dx, dy) || 1;
1033
1073
  const align = (normal.x * dx + normal.y * dy) / distance;
1034
1074
  const ease = 0.55 + 0.45 * align;
1035
- const arm = clamp(distance * curvature * ease, MIN_CURVE_ARM, MAX_CURVE_ARM);
1075
+ const arm = clamp(distance * curvature * ease, metrics.minCurveArm, metrics.maxCurveArm);
1036
1076
  return { x: point.x + normal.x * arm, y: point.y + normal.y * arm };
1037
1077
  }
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);
1078
+ function buildCurvePath(fromPoint, toPoint, fromPort = "right", toPort = "left", metrics = routeMetrics(1)) {
1079
+ const controlA = curveControlPoint(fromPoint, fromPort, toPoint, EDGE_CURVATURE, metrics);
1080
+ const controlB = curveControlPoint(toPoint, toPort, fromPoint, EDGE_CURVATURE, metrics);
1041
1081
  const labelX = 0.125 * fromPoint.x + 0.375 * controlA.x + 0.375 * controlB.x + 0.125 * toPoint.x;
1042
1082
  const labelY = 0.125 * fromPoint.y + 0.375 * controlA.y + 0.375 * controlB.y + 0.125 * toPoint.y;
1043
1083
  return {
@@ -1099,11 +1139,11 @@ function getOrthogonalMidpoints(fromStub, toStub, fromPort, toPort) {
1099
1139
  }
1100
1140
  return [{ x: fromStub.x, y: toStub.y }];
1101
1141
  }
1102
- function getOrthogonalPoints(fromPoint, toPoint, fromPort, toPort, fromRect, toRect) {
1142
+ function getOrthogonalPoints(fromPoint, toPoint, fromPort, toPort, fromRect, toRect, metrics = routeMetrics(1)) {
1103
1143
  const fromNormal = getPortNormal(fromPort);
1104
1144
  const toNormal = getPortNormal(toPort);
1105
- const fromStub = offsetPoint(fromPoint, fromNormal, ORTHOGONAL_STUB_LENGTH);
1106
- const toStub = offsetPoint(toPoint, toNormal, ORTHOGONAL_STUB_LENGTH);
1145
+ const fromStub = offsetPoint(fromPoint, fromNormal, metrics.stub);
1146
+ const toStub = offsetPoint(toPoint, toNormal, metrics.stub);
1107
1147
  const sourceHorizontal = isHorizontalPort(fromPort);
1108
1148
  const targetHorizontal = isHorizontalPort(toPort);
1109
1149
  const mids = [];
@@ -1143,13 +1183,13 @@ function getOrthogonalPoints(fromPoint, toPoint, fromPort, toPort, fromRect, toR
1143
1183
  );
1144
1184
  return points;
1145
1185
  }
1146
- function buildOrthogonalPath(fromPoint, toPoint, fromPort = "right", toPort = "left", fromRect = null, toRect = null) {
1186
+ function buildOrthogonalPath(fromPoint, toPoint, fromPort = "right", toPort = "left", fromRect = null, toRect = null, metrics = routeMetrics(1)) {
1147
1187
  let points;
1148
1188
  if (fromRect && toRect) {
1149
- points = getOrthogonalPoints(fromPoint, toPoint, fromPort, toPort, fromRect, toRect);
1189
+ points = getOrthogonalPoints(fromPoint, toPoint, fromPort, toPort, fromRect, toRect, metrics);
1150
1190
  } else {
1151
- const fromStub = offsetPoint(fromPoint, getPortNormal(fromPort), ORTHOGONAL_STUB_LENGTH);
1152
- const toStub = offsetPoint(toPoint, getPortNormal(toPort), ORTHOGONAL_STUB_LENGTH);
1191
+ const fromStub = offsetPoint(fromPoint, getPortNormal(fromPort), metrics.stub);
1192
+ const toStub = offsetPoint(toPoint, getPortNormal(toPort), metrics.stub);
1153
1193
  points = [];
1154
1194
  [
1155
1195
  fromPoint,
@@ -1161,7 +1201,7 @@ function buildOrthogonalPath(fromPoint, toPoint, fromPort = "right", toPort = "l
1161
1201
  }
1162
1202
  const label = getPolylineLabelPoint(points);
1163
1203
  return {
1164
- d: buildRoundedPath(points, ORTHOGONAL_CORNER_RADIUS),
1204
+ d: buildRoundedPath(points, metrics.corner),
1165
1205
  labelX: label.x,
1166
1206
  labelY: label.y
1167
1207
  };
@@ -1172,6 +1212,7 @@ function buildEdgePath(edge, fromNode = null, toNode = null) {
1172
1212
  const fromPoint = edge?.fromPoint || (fromNode ? getPortPosition(fromNode, fromPort) : null);
1173
1213
  const toPoint = edge?.toPoint || (toNode ? getPortPosition(toNode, toPort) : null);
1174
1214
  const route = normalizeEdgeRoute(edge?.route);
1215
+ const metrics = routeMetrics(edgeVisualScale(fromNode, toNode));
1175
1216
  if (!fromPoint || !toPoint) {
1176
1217
  return { d: "", labelX: 0, labelY: 0 };
1177
1218
  }
@@ -1179,15 +1220,17 @@ function buildEdgePath(edge, fromNode = null, toNode = null) {
1179
1220
  return buildStraightPath(fromPoint, toPoint);
1180
1221
  }
1181
1222
  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);
1223
+ const fromRect = !edge?.fromPoint && fromNode ? getNodeRectBounds(fromNode, metrics.clearance) : null;
1224
+ const toRect = !edge?.toPoint && toNode ? getNodeRectBounds(toNode, metrics.clearance) : null;
1225
+ return buildOrthogonalPath(fromPoint, toPoint, fromPort, toPort, fromRect, toRect, metrics);
1185
1226
  }
1186
- return buildCurvePath(fromPoint, toPoint, fromPort, toPort);
1227
+ return buildCurvePath(fromPoint, toPoint, fromPort, toPort, metrics);
1187
1228
  }
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));
1229
+ function createArrowMarker(id, strokeWidth, reversed = false, edgeScale = 1) {
1230
+ const scale = clamp(toFiniteNumber(edgeScale, 1), 0.01, 1);
1231
+ const effective = Math.max(0.02, strokeWidth * scale);
1232
+ const size = formatNumber(Math.max(9 * scale, effective * 4.4));
1233
+ const refInset = formatNumber(Math.max(1.5 * scale, effective * 0.85));
1191
1234
  const marker = svgEl("marker", {
1192
1235
  id,
1193
1236
  markerWidth: size,
@@ -1205,9 +1248,11 @@ function createArrowMarker(id, strokeWidth, reversed = false) {
1205
1248
  );
1206
1249
  return marker;
1207
1250
  }
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));
1251
+ function createDotMarker(id, strokeWidth, edgeScale = 1) {
1252
+ const scale = clamp(toFiniteNumber(edgeScale, 1), 0.01, 1);
1253
+ const effective = Math.max(0.02, strokeWidth * scale);
1254
+ const size = formatNumber(Math.max(8 * scale, effective * 3.3));
1255
+ const radius = formatNumber(Math.max(2.75 * scale, effective * 1.45));
1211
1256
  const marker = svgEl("marker", {
1212
1257
  id,
1213
1258
  markerWidth: size,
@@ -1464,7 +1509,23 @@ var VdFlowchart = class {
1464
1509
  this.element = resolveElement(options.element || options.target);
1465
1510
  this.readonly = Boolean(options.readonly);
1466
1511
  this.gridSize = clamp(toFiniteNumber(options.gridSize, DEFAULT_GRID_SIZE), 12, 64);
1467
- this.documentData = normalizeDocument(options.data || {});
1512
+ const scaleBounds = normalizeBoundPair(
1513
+ options.minScale,
1514
+ options.maxScale,
1515
+ DEFAULT_MIN_SCALE,
1516
+ DEFAULT_MAX_SCALE
1517
+ );
1518
+ const sizeBounds = normalizeBoundPair(
1519
+ options.minNodeSize,
1520
+ options.maxNodeSize,
1521
+ DEFAULT_MIN_NODE_SIZE,
1522
+ DEFAULT_MAX_NODE_SIZE
1523
+ );
1524
+ this.minScale = scaleBounds.min;
1525
+ this.maxScale = scaleBounds.max;
1526
+ this.minNodeSize = sizeBounds.min;
1527
+ this.maxNodeSize = sizeBounds.max;
1528
+ this.documentData = normalizeDocument(options.data || {}, { scaleBounds, sizeBounds });
1468
1529
  this.listeners = {};
1469
1530
  this.selection = null;
1470
1531
  this.selectedNodeIds = /* @__PURE__ */ new Set();
@@ -1692,10 +1753,10 @@ var VdFlowchart = class {
1692
1753
  this.world = svgEl("g", { class: "vd-flowchart-world" });
1693
1754
  this.gridRect = svgEl("rect", {
1694
1755
  class: "vd-flowchart-grid",
1695
- x: -WORLD_EXTENT / 2,
1696
- y: -WORLD_EXTENT / 2,
1697
- width: WORLD_EXTENT,
1698
- height: WORLD_EXTENT,
1756
+ x: -DEFAULT_WORLD_EXTENT / 2,
1757
+ y: -DEFAULT_WORLD_EXTENT / 2,
1758
+ width: DEFAULT_WORLD_EXTENT,
1759
+ height: DEFAULT_WORLD_EXTENT,
1699
1760
  fill: `url(#${this.gridPatternId})`
1700
1761
  });
1701
1762
  this.edgesLayer = svgEl("g", { class: "vd-flowchart-edges" });
@@ -2478,7 +2539,7 @@ var VdFlowchart = class {
2478
2539
  const width = this.canvasEl.clientWidth || 800;
2479
2540
  const height = this.canvasEl.clientHeight || 560;
2480
2541
  this.scaleAround(
2481
- clamp(scale, MIN_SCALE, MAX_SCALE) / viewport.scale,
2542
+ clamp(formatScale(scale), this.minScale, this.maxScale) / viewport.scale,
2482
2543
  width / 2,
2483
2544
  height / 2,
2484
2545
  "viewport:zoom"
@@ -2513,7 +2574,8 @@ var VdFlowchart = class {
2513
2574
  const node = normalizeNode(
2514
2575
  { ...data, id: void 0, x: data.x + 24, y: data.y + 24 },
2515
2576
  this.documentData.nodes.length + index,
2516
- usedNodeIds
2577
+ usedNodeIds,
2578
+ this.nodeSizeBounds()
2517
2579
  );
2518
2580
  idMap.set(data.id, node.id);
2519
2581
  return node;
@@ -2545,7 +2607,8 @@ var VdFlowchart = class {
2545
2607
  const node = normalizeNode(
2546
2608
  { ...data, id: void 0, collapsed: void 0, x: data.x + 24, y: data.y + 24 },
2547
2609
  this.documentData.nodes.length,
2548
- usedIds
2610
+ usedIds,
2611
+ this.nodeSizeBounds()
2549
2612
  );
2550
2613
  this.documentData.nodes.push(node);
2551
2614
  this.select({ kind: "node", id: node.id });
@@ -3159,14 +3222,15 @@ var VdFlowchart = class {
3159
3222
  if (this.activeTool === "arrow") return true;
3160
3223
  return this.selection?.kind === "node" && this.selection.id === node.id && !this.hasMultiSelection();
3161
3224
  }
3162
- getMarkerId(markerType, position, strokeWidth) {
3225
+ getMarkerId(markerType, position, strokeWidth, edgeScale = 1) {
3163
3226
  const width = normalizeEdgeStrokeWidth(strokeWidth);
3164
- const key = `${markerType}:${position}:${width}`;
3227
+ const scale = clamp(formatNumber(Math.max(edgeScale, 0.01)), 0.01, 1);
3228
+ const key = `${markerType}:${position}:${width}:${scale}`;
3165
3229
  if (this.markerIds.has(key)) {
3166
3230
  return this.markerIds.get(key);
3167
3231
  }
3168
3232
  const markerId = nextId(`flowchart-${markerType}-${position}`);
3169
- const marker = markerType === "dot" ? createDotMarker(markerId, width) : createArrowMarker(markerId, width, position === "start");
3233
+ const marker = markerType === "dot" ? createDotMarker(markerId, width, scale) : createArrowMarker(markerId, width, position === "start", scale);
3170
3234
  this.markerDefs.appendChild(marker);
3171
3235
  this.markerIds.set(key, markerId);
3172
3236
  return markerId;
@@ -3174,8 +3238,15 @@ var VdFlowchart = class {
3174
3238
  getEdgeStrokeWidth(edge) {
3175
3239
  return normalizeEdgeStrokeWidth(edge?.strokeWidth);
3176
3240
  }
3177
- getEdgeHitStrokeWidth(edge) {
3178
- return formatNumber(Math.max(EDGE_HIT_STROKE_MIN, this.getEdgeStrokeWidth(edge) + 12));
3241
+ getEdgeHitStrokeWidth(edge, edgeScale = 1) {
3242
+ const visual = clamp(toFiniteNumber(edgeScale, 1), 0.01, 1);
3243
+ const base = this.getEdgeStrokeWidth(edge);
3244
+ const view = Math.max(this.documentData?.viewport?.scale || 1, 1e-4);
3245
+ const legacy = Math.max(EDGE_HIT_STROKE_MIN, base + 12);
3246
+ if (visual >= 0.999 && view >= DEFAULT_MIN_SCALE && view <= DEFAULT_MAX_SCALE) {
3247
+ return formatNumber(legacy);
3248
+ }
3249
+ return formatNumber(Math.max(base * visual, EDGE_HIT_STROKE_MIN / view));
3179
3250
  }
3180
3251
  findConnectionTarget(worldPoint, excludeNodeId, previousSnap = null, referencePoint = null) {
3181
3252
  const snapPadding = CONNECTION_SNAP_PADDING / this.documentData.viewport.scale;
@@ -3232,17 +3303,37 @@ var VdFlowchart = class {
3232
3303
  let width = original.width;
3233
3304
  let height = original.height;
3234
3305
  if (handle.includes("e")) {
3235
- width = clampNodeWidth(nextType, original.width + deltaX, original.width);
3306
+ width = clampNodeWidth(
3307
+ nextType,
3308
+ original.width + deltaX,
3309
+ original.width,
3310
+ this.nodeSizeBounds()
3311
+ );
3236
3312
  }
3237
3313
  if (handle.includes("s")) {
3238
- height = clampNodeHeight(nextType, original.height + deltaY, original.height);
3314
+ height = clampNodeHeight(
3315
+ nextType,
3316
+ original.height + deltaY,
3317
+ original.height,
3318
+ this.nodeSizeBounds()
3319
+ );
3239
3320
  }
3240
3321
  if (handle.includes("w")) {
3241
- width = clampNodeWidth(nextType, original.width - deltaX, original.width);
3322
+ width = clampNodeWidth(
3323
+ nextType,
3324
+ original.width - deltaX,
3325
+ original.width,
3326
+ this.nodeSizeBounds()
3327
+ );
3242
3328
  x = original.x + original.width - width;
3243
3329
  }
3244
3330
  if (handle.includes("n")) {
3245
- height = clampNodeHeight(nextType, original.height - deltaY, original.height);
3331
+ height = clampNodeHeight(
3332
+ nextType,
3333
+ original.height - deltaY,
3334
+ original.height,
3335
+ this.nodeSizeBounds()
3336
+ );
3246
3337
  y = original.y + original.height - height;
3247
3338
  }
3248
3339
  return {
@@ -3278,7 +3369,7 @@ var VdFlowchart = class {
3278
3369
  scaleAround(factor, localX, localY, reason) {
3279
3370
  const viewport = this.documentData.viewport;
3280
3371
  const anchor = this.localToWorld(localX, localY);
3281
- const nextScale = clamp(formatNumber(viewport.scale * factor), MIN_SCALE, MAX_SCALE);
3372
+ const nextScale = clamp(formatScale(viewport.scale * factor), this.minScale, this.maxScale);
3282
3373
  if (nextScale === viewport.scale) return;
3283
3374
  viewport.scale = nextScale;
3284
3375
  viewport.x = formatNumber(localX - anchor.x * nextScale);
@@ -3461,19 +3552,20 @@ var VdFlowchart = class {
3461
3552
  }
3462
3553
  const viewport = this.documentData.viewport;
3463
3554
  const scale = viewport.scale;
3555
+ const visual = nodeVisualScale(node);
3464
3556
  const textarea = this.textEditor.textarea;
3465
3557
  const width = node.width * scale;
3466
3558
  const height = node.height * scale;
3467
3559
  const metrics = getNodeFontMetrics(node);
3468
- const lineHeight = metrics.lineHeight * scale;
3560
+ const lineHeight = metrics.lineHeight * visual * scale;
3469
3561
  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);
3562
+ const horizontalPadding = (node.type === "textbox" ? 16 : 12) * visual * scale;
3563
+ const verticalPadding = node.type === "textbox" ? 12 * visual * scale : Math.max(4 * visual, (height - lineCount * lineHeight) / 2);
3472
3564
  textarea.style.left = `${formatNumber(viewport.x + node.x * scale)}px`;
3473
3565
  textarea.style.top = `${formatNumber(viewport.y + node.y * scale)}px`;
3474
3566
  textarea.style.width = `${formatNumber(width)}px`;
3475
3567
  textarea.style.height = `${formatNumber(height)}px`;
3476
- textarea.style.fontSize = `${formatNumber(metrics.fontSize * scale)}px`;
3568
+ textarea.style.fontSize = `${formatNumber(metrics.fontSize * visual * scale)}px`;
3477
3569
  textarea.style.lineHeight = `${formatNumber(lineHeight)}px`;
3478
3570
  textarea.style.padding = `${formatNumber(verticalPadding)}px ${formatNumber(horizontalPadding)}px`;
3479
3571
  }
@@ -3485,14 +3577,16 @@ var VdFlowchart = class {
3485
3577
  this.stopTextEdit({ commit: false });
3486
3578
  return;
3487
3579
  }
3580
+ const visual = edgeVisualScale(fromNode, toNode);
3488
3581
  const { labelX, labelY } = buildEdgePath(edge, fromNode, toNode);
3489
3582
  const viewport = this.documentData.viewport;
3490
3583
  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;
3584
+ const fontSize = Math.max(11 * visual, 13 * visual * viewport.scale);
3585
+ const width = clamp(textarea.value.length * fontSize * 0.62 + 28 * visual, 96 * visual, 280);
3586
+ const height = fontSize * 1.4 + 12 * visual;
3587
+ const labelOffset = 8 * visual;
3494
3588
  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`;
3589
+ textarea.style.top = `${formatNumber(viewport.y + (labelY - labelOffset) * viewport.scale - height / 2)}px`;
3496
3590
  textarea.style.width = `${formatNumber(width)}px`;
3497
3591
  textarea.style.height = `${formatNumber(height)}px`;
3498
3592
  textarea.style.fontSize = `${formatNumber(fontSize)}px`;
@@ -3501,6 +3595,7 @@ var VdFlowchart = class {
3501
3595
  }
3502
3596
  render(options = {}) {
3503
3597
  if (this.destroyed) return;
3598
+ this.syncGrid();
3504
3599
  const shouldRenderScene = options.scene !== false;
3505
3600
  const shouldRenderInspector = options.inspector !== false;
3506
3601
  const shouldRenderJson = options.json !== false;
@@ -3524,6 +3619,8 @@ var VdFlowchart = class {
3524
3619
  clearChildren(this.previewLayer);
3525
3620
  clearChildren(this.nodesLayer);
3526
3621
  clearChildren(this.overlayLayer);
3622
+ clearChildren(this.markerDefs);
3623
+ this.markerIds = /* @__PURE__ */ new Map();
3527
3624
  const hidden = this.getHiddenNodeIds();
3528
3625
  this.sceneHidden = hidden;
3529
3626
  this.sceneOutgoing = this.getOutgoingMap();
@@ -3587,17 +3684,18 @@ var VdFlowchart = class {
3587
3684
  }
3588
3685
  }
3589
3686
  }
3590
- applyEdgeMarkers(pathElement, edge) {
3687
+ applyEdgeMarkers(pathElement, edge, edgeScale = 1) {
3688
+ const width = this.getEdgeStrokeWidth(edge);
3591
3689
  if (edge.startMarker && edge.startMarker !== "none") {
3592
3690
  pathElement.setAttribute(
3593
3691
  "marker-start",
3594
- `url(#${this.getMarkerId(edge.startMarker, "start", this.getEdgeStrokeWidth(edge))})`
3692
+ `url(#${this.getMarkerId(edge.startMarker, "start", width, edgeScale)})`
3595
3693
  );
3596
3694
  }
3597
3695
  if (edge.endMarker && edge.endMarker !== "none") {
3598
3696
  pathElement.setAttribute(
3599
3697
  "marker-end",
3600
- `url(#${this.getMarkerId(edge.endMarker, "end", this.getEdgeStrokeWidth(edge))})`
3698
+ `url(#${this.getMarkerId(edge.endMarker, "end", width, edgeScale)})`
3601
3699
  );
3602
3700
  }
3603
3701
  }
@@ -3605,6 +3703,7 @@ var VdFlowchart = class {
3605
3703
  const fromNode = nodeMap.get(edge.from.nodeId);
3606
3704
  const toNode = nodeMap.get(edge.to.nodeId);
3607
3705
  if (!fromNode || !toNode) return null;
3706
+ const visual = edgeVisualScale(fromNode, toNode);
3608
3707
  const edgePath = buildEdgePath(edge, fromNode, toNode);
3609
3708
  const selected = this.selection?.kind === "edge" && this.selection.id === edge.id;
3610
3709
  const inReconnectMode = this.reconnectEdgeId === edge.id;
@@ -3612,13 +3711,14 @@ var VdFlowchart = class {
3612
3711
  class: `vd-flowchart-edge${selected ? " is-selected" : ""}${inReconnectMode ? " is-reconnecting" : ""}`,
3613
3712
  "data-edge-id": edge.id
3614
3713
  });
3615
- const strokeWidth = this.getEdgeStrokeWidth(edge);
3714
+ const baseStroke = this.getEdgeStrokeWidth(edge);
3715
+ const strokeWidth = visual < 0.999 ? formatNumber(Math.max(0.02, baseStroke * visual)) : baseStroke;
3616
3716
  if (selected) {
3617
3717
  group.appendChild(
3618
3718
  svgEl("path", {
3619
3719
  class: "vd-flowchart-edge-selection",
3620
3720
  d: edgePath.d,
3621
- "stroke-width": formatNumber(strokeWidth + 7)
3721
+ "stroke-width": visual < 0.999 ? formatNumber((baseStroke + 7) * visual) : formatNumber(strokeWidth + 7)
3622
3722
  })
3623
3723
  );
3624
3724
  }
@@ -3628,23 +3728,28 @@ var VdFlowchart = class {
3628
3728
  "stroke-width": strokeWidth,
3629
3729
  "data-edge-id": edge.id
3630
3730
  });
3631
- this.applyEdgeMarkers(visiblePath, edge);
3731
+ this.applyEdgeMarkers(visiblePath, edge, visual);
3632
3732
  const hitPath = svgEl("path", {
3633
3733
  class: "vd-flowchart-edge-hit",
3634
3734
  d: edgePath.d,
3635
- "stroke-width": this.getEdgeHitStrokeWidth(edge),
3735
+ "stroke-width": this.getEdgeHitStrokeWidth(edge, visual),
3636
3736
  "data-edge-id": edge.id
3637
3737
  });
3638
3738
  group.appendChild(visiblePath);
3639
3739
  group.appendChild(hitPath);
3640
3740
  if (edge.label && this.textEditor?.edgeId !== edge.id) {
3741
+ const labelOffset = visual < 0.999 ? formatNumber(8 * visual) : 8;
3641
3742
  const label = svgEl("text", {
3642
3743
  class: "vd-flowchart-edge-label",
3643
3744
  x: edgePath.labelX,
3644
- y: edgePath.labelY - 8,
3745
+ y: edgePath.labelY - labelOffset,
3645
3746
  "text-anchor": "middle",
3646
3747
  "data-edge-id": edge.id
3647
3748
  });
3749
+ if (visual < 0.999) {
3750
+ label.style.fontSize = `${formatNumber(Math.max(0.06, 12 * visual))}px`;
3751
+ label.style.strokeWidth = `${formatNumber(Math.max(0.03, 5 * visual))}px`;
3752
+ }
3648
3753
  label.textContent = edge.label;
3649
3754
  group.appendChild(label);
3650
3755
  }
@@ -3683,18 +3788,26 @@ var VdFlowchart = class {
3683
3788
  renderPreviewEdge(interaction) {
3684
3789
  const fromPort = interaction.fromPort || interaction.source?.port || "right";
3685
3790
  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
- });
3791
+ const fromNode = interaction.source?.nodeId ? this.findNode(interaction.source.nodeId) : null;
3792
+ const toNode = interaction.target?.nodeId ? this.findNode(interaction.target.nodeId) : null;
3793
+ const visual = edgeVisualScale(fromNode, toNode);
3794
+ const edgePath = buildEdgePath(
3795
+ {
3796
+ strokeWidth: interaction.strokeWidth,
3797
+ route: interaction.route,
3798
+ fromPoint: interaction.fromPoint,
3799
+ toPoint: interaction.toPoint,
3800
+ from: { port: fromPort },
3801
+ to: { port: toPort }
3802
+ },
3803
+ fromNode,
3804
+ toNode
3805
+ );
3806
+ const baseStroke = this.getEdgeStrokeWidth(interaction);
3694
3807
  return svgEl("path", {
3695
3808
  class: "vd-flowchart-preview-path",
3696
3809
  d: edgePath.d,
3697
- "stroke-width": this.getEdgeStrokeWidth(interaction)
3810
+ "stroke-width": visual < 0.999 ? formatNumber(Math.max(0.02, baseStroke * visual)) : baseStroke
3698
3811
  });
3699
3812
  }
3700
3813
  renderNode(node) {
@@ -3709,17 +3822,21 @@ var VdFlowchart = class {
3709
3822
  "data-node-id": node.id
3710
3823
  });
3711
3824
  const portsVisible = this.shouldShowNodePorts(node);
3825
+ const visual = nodeVisualScale(node);
3826
+ const corner = node.type === "rounded-rect" || node.type === "textbox" ? 16 : node.type === "junction" ? node.width / 2 : 6;
3712
3827
  const hitbox = svgEl("rect", {
3713
3828
  x: 0,
3714
3829
  y: 0,
3715
3830
  width: node.width,
3716
3831
  height: node.height,
3717
- rx: node.type === "rounded-rect" || node.type === "textbox" ? 16 : node.type === "junction" ? node.width / 2 : 6,
3832
+ rx: visual < 0.999 && node.type !== "junction" ? formatNumber(corner * visual) : corner,
3718
3833
  fill: "transparent",
3719
3834
  "data-node-id": node.id
3720
3835
  });
3721
3836
  group.appendChild(hitbox);
3722
- group.appendChild(this.renderNodeShape(node));
3837
+ const shape = this.renderNodeShape(node);
3838
+ this.applyScaledNodeShape(node, shape);
3839
+ group.appendChild(shape);
3723
3840
  group.appendChild(this.renderNodeText(node));
3724
3841
  const scale = this.documentData.viewport.scale || 1;
3725
3842
  if (soloSelected && !this.readonly && SELECTION_BOX_TYPES.has(node.type)) {
@@ -3911,6 +4028,21 @@ var VdFlowchart = class {
3911
4028
  });
3912
4029
  return group;
3913
4030
  }
4031
+ applyScaledNodeShape(node, shape) {
4032
+ const visual = nodeVisualScale(node);
4033
+ if (!shape || visual >= 0.999) return;
4034
+ const baseStroke = node.type === "junction" ? 1.25 : 1.5;
4035
+ shape.style.strokeWidth = String(formatNumber(Math.max(0.03, baseStroke * visual)));
4036
+ if (node.type === "textbox") {
4037
+ shape.style.strokeDasharray = `${formatNumber(8 * visual)} ${formatNumber(5 * visual)}`;
4038
+ }
4039
+ const baseRadius = node.type === "rounded-rect" ? 18 : node.type === "textbox" ? 14 : node.type === "rect" ? 2 : 0;
4040
+ if (baseRadius > 0) {
4041
+ const scaled = String(formatNumber(baseRadius * visual));
4042
+ shape.setAttribute("rx", scaled);
4043
+ shape.setAttribute("ry", scaled);
4044
+ }
4045
+ }
3914
4046
  renderNodeShape(node) {
3915
4047
  const baseClass = `vd-flowchart-node-shape vd-flowchart-node-shape--${node.type}`;
3916
4048
  if (node.type === "rounded-rect") {
@@ -3987,21 +4119,28 @@ var VdFlowchart = class {
3987
4119
  if (node.type === "junction" || !node.text) {
3988
4120
  return textElement;
3989
4121
  }
4122
+ const visual = nodeVisualScale(node);
4123
+ const metrics = getNodeFontMetrics(node);
4124
+ const lineHeight = visual < 0.999 ? Math.max(0.06, metrics.lineHeight * visual) : metrics.lineHeight;
4125
+ if (visual < 0.999) {
4126
+ textElement.style.fontSize = `${formatNumber(Math.max(0.05, metrics.fontSize * visual))}px`;
4127
+ }
4128
+ const equivalentWidth = visual < 0.999 ? node.width / visual : node.width;
3990
4129
  if (node.type === "textbox") {
3991
- const lines2 = wrapText(node.text, estimateChars(node.width - 32));
3992
- const { lineHeight: lineHeight2 } = getNodeFontMetrics(node);
4130
+ const lines2 = wrapText(node.text, estimateChars(equivalentWidth - 32));
4131
+ const padX = visual < 0.999 ? formatNumber(16 * visual) : 16;
4132
+ const startY2 = visual < 0.999 ? formatNumber(28 * visual) : 28;
3993
4133
  lines2.forEach((line, index) => {
3994
4134
  const span = svgEl("tspan", {
3995
- x: 16,
3996
- y: 28 + index * lineHeight2
4135
+ x: padX,
4136
+ y: visual < 0.999 ? formatNumber(startY2 + index * lineHeight) : startY2 + index * lineHeight
3997
4137
  });
3998
4138
  span.textContent = line;
3999
4139
  textElement.appendChild(span);
4000
4140
  });
4001
4141
  return textElement;
4002
4142
  }
4003
- const lines = wrapText(node.text, estimateChars(node.width));
4004
- const { lineHeight } = getNodeFontMetrics(node);
4143
+ const lines = wrapText(node.text, estimateChars(equivalentWidth));
4005
4144
  const totalHeight = (lines.length - 1) * lineHeight;
4006
4145
  const startY = node.height / 2 - totalHeight / 2;
4007
4146
  textElement.setAttribute("text-anchor", "middle");
@@ -4009,7 +4148,7 @@ var VdFlowchart = class {
4009
4148
  lines.forEach((line, index) => {
4010
4149
  const span = svgEl("tspan", {
4011
4150
  x: node.width / 2,
4012
- y: startY + index * lineHeight,
4151
+ y: visual < 0.999 ? formatNumber(startY + index * lineHeight) : startY + index * lineHeight,
4013
4152
  "text-anchor": "middle",
4014
4153
  "dominant-baseline": "middle"
4015
4154
  });
@@ -4061,12 +4200,12 @@ var VdFlowchart = class {
4061
4200
  textArea.setAttribute("data-field", "node-text");
4062
4201
  const widthInput = createElement("input", { value: node.width, type: "number" });
4063
4202
  widthInput.setAttribute("data-field", "node-width");
4064
- widthInput.setAttribute("min", String(MIN_NODE_SIZE));
4065
- widthInput.setAttribute("max", String(MAX_NODE_SIZE));
4203
+ widthInput.setAttribute("min", String(this.minNodeSize));
4204
+ widthInput.setAttribute("max", String(this.maxNodeSize));
4066
4205
  const heightInput = createElement("input", { value: node.height, type: "number" });
4067
4206
  heightInput.setAttribute("data-field", "node-height");
4068
- heightInput.setAttribute("min", String(MIN_NODE_SIZE));
4069
- heightInput.setAttribute("max", String(MAX_NODE_SIZE));
4207
+ heightInput.setAttribute("min", String(this.minNodeSize));
4208
+ heightInput.setAttribute("max", String(this.maxNodeSize));
4070
4209
  this.selectionFields.appendChild(createField("Type", typeSelect));
4071
4210
  if (isNodeTextEditable(node)) {
4072
4211
  this.selectionFields.appendChild(createField("Text", textArea));
@@ -4380,8 +4519,8 @@ var VdFlowchart = class {
4380
4519
  this.reconnectEdgeId = null;
4381
4520
  const currentViewport = deepClone(this.documentData.viewport);
4382
4521
  const previousSelection = this.selection;
4383
- this.documentData = normalizeDocument(snapshot);
4384
- this.documentData.viewport = normalizeViewport(currentViewport);
4522
+ this.documentData = normalizeDocument(snapshot, this.normalizationLimits());
4523
+ this.documentData.viewport = normalizeViewport(currentViewport, this.scaleBounds());
4385
4524
  this.selection = this.resolvePreservedSelection(previousSelection, true);
4386
4525
  this.render();
4387
4526
  this.emitChange(reason);
@@ -4458,7 +4597,8 @@ var VdFlowchart = class {
4458
4597
  y: partialNode.y == null ? fallbackY : partialNode.y
4459
4598
  },
4460
4599
  this.documentData.nodes.length,
4461
- usedIds
4600
+ usedIds,
4601
+ this.nodeSizeBounds()
4462
4602
  );
4463
4603
  this.documentData.nodes.push(node);
4464
4604
  this.select({ kind: "node", id: node.id });
@@ -4711,8 +4851,8 @@ var VdFlowchart = class {
4711
4851
  node.x = patch.x == null ? node.x : formatNumber(toFiniteNumber(patch.x, node.x));
4712
4852
  node.y = patch.y == null ? node.y : formatNumber(toFiniteNumber(patch.y, node.y));
4713
4853
  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);
4854
+ node.width = clampNodeWidth(nextType, patch.width, node.width, this.nodeSizeBounds());
4855
+ node.height = clampNodeHeight(nextType, patch.height, node.height, this.nodeSizeBounds());
4716
4856
  if (isPlainObject(patch.data)) {
4717
4857
  node.data = deepClone(patch.data);
4718
4858
  }
@@ -4901,8 +5041,45 @@ var VdFlowchart = class {
4901
5041
  if (this.selection.kind === "node") return this.removeNode(this.selection.id);
4902
5042
  return this.removeEdge(this.selection.id);
4903
5043
  }
5044
+ nodeSizeBounds() {
5045
+ return { min: this.minNodeSize, max: this.maxNodeSize };
5046
+ }
5047
+ scaleBounds() {
5048
+ return { min: this.minScale, max: this.maxScale };
5049
+ }
5050
+ normalizationLimits() {
5051
+ return { scaleBounds: this.scaleBounds(), sizeBounds: this.nodeSizeBounds() };
5052
+ }
5053
+ hasWideCamera() {
5054
+ return this.minScale < DEFAULT_MIN_SCALE || this.maxScale > DEFAULT_MAX_SCALE;
5055
+ }
5056
+ syncGrid() {
5057
+ const wide = this.hasWideCamera();
5058
+ const extent = wide ? WIDE_WORLD_EXTENT : DEFAULT_WORLD_EXTENT;
5059
+ this.gridRect?.setAttribute("x", String(-extent / 2));
5060
+ this.gridRect?.setAttribute("y", String(-extent / 2));
5061
+ this.gridRect?.setAttribute("width", String(extent));
5062
+ this.gridRect?.setAttribute("height", String(extent));
5063
+ const pattern = this.svg?.querySelector("pattern");
5064
+ const path = pattern?.querySelector("path");
5065
+ if (!pattern || !path) return;
5066
+ let tile = this.gridSize;
5067
+ if (wide) {
5068
+ const scale = Math.max(this.minScale, this.documentData?.viewport?.scale || 1);
5069
+ while (tile * scale < 12 && tile < extent / 8) tile *= 4;
5070
+ while (tile * scale > 48 && tile > 1.5) tile /= 4;
5071
+ tile = formatNumber(tile);
5072
+ path.setAttribute("vector-effect", "non-scaling-stroke");
5073
+ } else {
5074
+ path.removeAttribute("vector-effect");
5075
+ }
5076
+ path.setAttribute("stroke-width", "1");
5077
+ pattern.setAttribute("width", String(tile));
5078
+ pattern.setAttribute("height", String(tile));
5079
+ path.setAttribute("d", `M ${tile} 0 L 0 0 0 ${tile}`);
5080
+ }
4904
5081
  setViewport(viewport) {
4905
- this.documentData.viewport = normalizeViewport(viewport);
5082
+ this.documentData.viewport = normalizeViewport(viewport, this.scaleBounds());
4906
5083
  this.render({ inspector: false, json: true });
4907
5084
  this.emitViewportChange("viewport:set");
4908
5085
  return this;
@@ -4920,7 +5097,7 @@ var VdFlowchart = class {
4920
5097
  return this;
4921
5098
  }
4922
5099
  resetView() {
4923
- this.documentData.viewport = normalizeViewport({ x: 0, y: 0, scale: 1 });
5100
+ this.documentData.viewport = normalizeViewport({ x: 0, y: 0, scale: 1 }, this.scaleBounds());
4924
5101
  this.render({ inspector: false, json: true });
4925
5102
  this.emitViewportChange("viewport:reset");
4926
5103
  return this;
@@ -4936,30 +5113,64 @@ var VdFlowchart = class {
4936
5113
  const padding = 80;
4937
5114
  const contentWidth = Math.max(1, bounds.right - bounds.left);
4938
5115
  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
5116
+ const scale = formatScale(
5117
+ clamp(
5118
+ Math.min((width - padding * 2) / contentWidth, (height - padding * 2) / contentHeight),
5119
+ this.minScale,
5120
+ this.maxScale
5121
+ )
4943
5122
  );
4944
5123
  this.documentData.viewport = {
4945
5124
  x: formatNumber(width / 2 - (bounds.left + bounds.right) / 2 * scale),
4946
5125
  y: formatNumber(height / 2 - (bounds.top + bounds.bottom) / 2 * scale),
4947
- scale: formatNumber(scale)
5126
+ scale
4948
5127
  };
4949
5128
  this.render({ inspector: false, json: true });
4950
5129
  this.emitViewportChange("viewport:fit");
4951
5130
  return this;
4952
5131
  }
5132
+ fitNode(nodeId, padding = 48) {
5133
+ const node = this.findNode(nodeId);
5134
+ if (!node || this.isNodeHidden(node.id)) return this;
5135
+ const pad = Math.max(0, toFiniteNumber(padding, 48));
5136
+ const width = this.canvasEl.clientWidth || 800;
5137
+ const height = this.canvasEl.clientHeight || 560;
5138
+ const availableWidth = Math.max(1, width - pad * 2);
5139
+ const availableHeight = Math.max(1, height - pad * 2);
5140
+ const scale = formatScale(
5141
+ clamp(
5142
+ Math.min(
5143
+ availableWidth / Math.max(1, node.width),
5144
+ availableHeight / Math.max(1, node.height)
5145
+ ),
5146
+ this.minScale,
5147
+ this.maxScale
5148
+ )
5149
+ );
5150
+ const centerX = node.x + node.width / 2;
5151
+ const centerY = node.y + node.height / 2;
5152
+ this.documentData.viewport = {
5153
+ x: formatNumber(width / 2 - centerX * scale),
5154
+ y: formatNumber(height / 2 - centerY * scale),
5155
+ scale
5156
+ };
5157
+ this.render({ inspector: false, json: true });
5158
+ this.emitViewportChange("viewport:fit-node");
5159
+ return this;
5160
+ }
4953
5161
  clear() {
4954
5162
  this.stopTextEdit({ commit: false });
4955
5163
  this.activeTool = null;
4956
5164
  this.reconnectEdgeId = null;
4957
5165
  this.clipboard = null;
4958
- this.documentData = normalizeDocument({
4959
- nodes: [],
4960
- edges: [],
4961
- viewport: { x: 0, y: 0, scale: 1 }
4962
- });
5166
+ this.documentData = normalizeDocument(
5167
+ {
5168
+ nodes: [],
5169
+ edges: [],
5170
+ viewport: { x: 0, y: 0, scale: 1 }
5171
+ },
5172
+ this.normalizationLimits()
5173
+ );
4963
5174
  this.selection = null;
4964
5175
  this.render();
4965
5176
  this.emitChange("clear");
@@ -4994,6 +5205,33 @@ var VdFlowchart = class {
4994
5205
  if ("snapGuides" in options) this.snapGuides = options.snapGuides !== false;
4995
5206
  if ("minimap" in options) this.setMinimapEnabled(options.minimap !== false);
4996
5207
  if ("autoLayout" in options) this.autoLayout = Boolean(options.autoLayout);
5208
+ if ("minScale" in options || "maxScale" in options || "minNodeSize" in options || "maxNodeSize" in options) {
5209
+ const nextScale = normalizeBoundPair(
5210
+ "minScale" in options ? options.minScale : this.minScale,
5211
+ "maxScale" in options ? options.maxScale : this.maxScale,
5212
+ DEFAULT_MIN_SCALE,
5213
+ DEFAULT_MAX_SCALE
5214
+ );
5215
+ const nextSize = normalizeBoundPair(
5216
+ "minNodeSize" in options ? options.minNodeSize : this.minNodeSize,
5217
+ "maxNodeSize" in options ? options.maxNodeSize : this.maxNodeSize,
5218
+ DEFAULT_MIN_NODE_SIZE,
5219
+ DEFAULT_MAX_NODE_SIZE
5220
+ );
5221
+ this.minScale = nextScale.min;
5222
+ this.maxScale = nextScale.max;
5223
+ this.minNodeSize = nextSize.min;
5224
+ this.maxNodeSize = nextSize.max;
5225
+ const clamped = clamp(
5226
+ formatScale(this.documentData.viewport.scale),
5227
+ this.minScale,
5228
+ this.maxScale
5229
+ );
5230
+ if (clamped !== this.documentData.viewport.scale) {
5231
+ this.documentData.viewport.scale = clamped;
5232
+ this.pendingBoundsEvent = true;
5233
+ }
5234
+ }
4997
5235
  this.syncCanvasLabel();
4998
5236
  if (this.shortcutsHelpOpen) this.renderShortcutsHelp();
4999
5237
  if ("history" in options && options.history !== false !== this.historyEnabled) {
@@ -5011,11 +5249,15 @@ var VdFlowchart = class {
5011
5249
  }
5012
5250
  }
5013
5251
  this.render();
5252
+ if (this.pendingBoundsEvent) {
5253
+ this.pendingBoundsEvent = false;
5254
+ this.emitViewportChange("viewport:bounds");
5255
+ }
5014
5256
  this.updateHistoryButtons();
5015
5257
  return this;
5016
5258
  }
5017
5259
  load(data, options = {}) {
5018
- const documentData = normalizeDocument(data);
5260
+ const documentData = normalizeDocument(data, this.normalizationLimits());
5019
5261
  if (options.silent && JSON.stringify(documentData) === JSON.stringify(this.documentData)) {
5020
5262
  return this;
5021
5263
  }
@@ -5073,7 +5315,11 @@ var EDITOR_OPTION_KEYS = [
5073
5315
  "keyboardShortcuts",
5074
5316
  "snapGuides",
5075
5317
  "minimap",
5076
- "autoLayout"
5318
+ "autoLayout",
5319
+ "minScale",
5320
+ "maxScale",
5321
+ "minNodeSize",
5322
+ "maxNodeSize"
5077
5323
  ];
5078
5324
  var VdFlowchart2 = (0, import_vue.defineComponent)({
5079
5325
  name: "VdFlowchart",
@@ -5099,13 +5345,29 @@ var VdFlowchart2 = (0, import_vue.defineComponent)({
5099
5345
  /** Show the overview minimap in the canvas corner (default true). */
5100
5346
  minimap: { type: Boolean, default: true },
5101
5347
  /** Re-run the current layout after keyboard or handle insertions (default false). */
5102
- autoLayout: { type: Boolean, default: false }
5348
+ autoLayout: { type: Boolean, default: false },
5349
+ /** Lowest camera scale. Defaults to 0.35. Set it together with maxScale. */
5350
+ minScale: { type: Number, default: void 0 },
5351
+ /** Highest camera scale. Defaults to 3. Set it together with minScale. */
5352
+ maxScale: { type: Number, default: void 0 },
5353
+ /** Smallest resizable-node side. Defaults to 56. Set it together with maxNodeSize. */
5354
+ minNodeSize: { type: Number, default: void 0 },
5355
+ /** Largest resizable-node side. Defaults to 420. Set it together with minNodeSize. */
5356
+ maxNodeSize: { type: Number, default: void 0 }
5103
5357
  },
5104
5358
  emits: ["change", "select", "viewport", "connect", "ready"],
5105
5359
  setup(props, { emit, expose }) {
5106
5360
  const el = (0, import_vue.ref)(null);
5107
5361
  let instance = null;
5108
- const editorOptions = () => Object.fromEntries(EDITOR_OPTION_KEYS.map((key) => [key, props[key]]));
5362
+ const editorOptions = () => Object.fromEntries(
5363
+ EDITOR_OPTION_KEYS.flatMap((key) => {
5364
+ const value = props[key];
5365
+ if ((key === "minScale" || key === "maxScale" || key === "minNodeSize" || key === "maxNodeSize") && value == null) {
5366
+ return [];
5367
+ }
5368
+ return [[key, value]];
5369
+ })
5370
+ );
5109
5371
  const create = () => {
5110
5372
  instance = new VdFlowchart({
5111
5373
  element: el.value,
@@ -5147,7 +5409,8 @@ var VdFlowchart2 = (0, import_vue.defineComponent)({
5147
5409
  redo: () => instance?.redo(),
5148
5410
  canUndo: () => Boolean(instance?.canUndo()),
5149
5411
  canRedo: () => Boolean(instance?.canRedo()),
5150
- layout: (mode, options) => instance?.layout(mode, options)
5412
+ layout: (mode, options) => instance?.layout(mode, options),
5413
+ fitNode: (nodeId, padding) => instance?.fitNode(nodeId, padding)
5151
5414
  });
5152
5415
  return () => (0, import_vue.h)("div", { ref: el, class: "vd-flowchart" });
5153
5416
  }