@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.js CHANGED
@@ -362,11 +362,12 @@ function createToolbarButton({ action, label, icon, disabled = false }) {
362
362
  return button;
363
363
  }
364
364
  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;
365
+ var DEFAULT_MIN_SCALE = 0.35;
366
+ var DEFAULT_MAX_SCALE = 3;
367
+ var DEFAULT_MIN_NODE_SIZE = 56;
368
+ var DEFAULT_MAX_NODE_SIZE = 420;
369
+ var DEFAULT_WORLD_EXTENT = 12e3;
370
+ var WIDE_WORLD_EXTENT = 24e4;
370
371
  var MAX_NODES = 1e4;
371
372
  var MAX_EDGES = 1e4;
372
373
  var RESIZE_HANDLES = ["n", "ne", "e", "se", "s", "sw", "w", "nw"];
@@ -398,7 +399,7 @@ var EDGE_STROKE_PRESETS = [
398
399
  { id: "medium", label: "Medium", width: DEFAULT_EDGE_STROKE_WIDTH },
399
400
  { id: "bold", label: "Bold", width: 3.5 }
400
401
  ];
401
- var VD_FLOWCHART_VERSION = "1.4.0";
402
+ var VD_FLOWCHART_VERSION = "1.5.0";
402
403
  var FLOWCHART_DOCUMENT_VERSION = "1.3.0";
403
404
  var FLOWCHART_NODE_TYPES = [
404
405
  "rounded-rect",
@@ -539,6 +540,17 @@ function deepClone(value) {
539
540
  function formatNumber(value) {
540
541
  return Number(value.toFixed(2));
541
542
  }
543
+ function formatScale(value) {
544
+ return Number(toFiniteNumber(value, 1).toFixed(4));
545
+ }
546
+ function normalizeBoundPair(min, max, fallbackMin, fallbackMax) {
547
+ const low = Number(min);
548
+ const high = Number(max);
549
+ if (!Number.isFinite(low) || !Number.isFinite(high) || low <= 0 || high <= 0 || low >= high) {
550
+ return { min: fallbackMin, max: fallbackMax };
551
+ }
552
+ return { min: low, max: high };
553
+ }
542
554
  function sanitizeId(value) {
543
555
  if (value == null) return "";
544
556
  return String(value).trim();
@@ -596,21 +608,36 @@ function normalizeNodeType(type) {
596
608
  function getNodeSpec(type) {
597
609
  return DEFAULT_NODE_SPECS[normalizeNodeType(type)];
598
610
  }
599
- function getNodeSizeBounds(type) {
611
+ function nodeVisualScale(node) {
612
+ if (!node || node.type === "junction") return 1;
613
+ const spec = getNodeSpec(node.type);
614
+ const width = spec.width > 0 ? toFiniteNumber(node.width, spec.width) / spec.width : 1;
615
+ const height = spec.height > 0 ? toFiniteNumber(node.height, spec.height) / spec.height : 1;
616
+ return clamp(Math.min(1, width, height), 0.01, 1);
617
+ }
618
+ function edgeVisualScale(fromNode, toNode) {
619
+ if (fromNode && toNode) return Math.min(nodeVisualScale(fromNode), nodeVisualScale(toNode));
620
+ if (fromNode) return nodeVisualScale(fromNode);
621
+ if (toNode) return nodeVisualScale(toNode);
622
+ return 1;
623
+ }
624
+ function getNodeSizeBounds(type, sizeBounds) {
600
625
  const spec = getNodeSpec(type);
626
+ const min = sizeBounds?.min ?? DEFAULT_MIN_NODE_SIZE;
627
+ const max = sizeBounds?.max ?? DEFAULT_MAX_NODE_SIZE;
601
628
  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
629
+ minWidth: spec.minWidth ?? min,
630
+ minHeight: spec.minHeight ?? min,
631
+ maxWidth: spec.maxWidth ?? max,
632
+ maxHeight: spec.maxHeight ?? max
606
633
  };
607
634
  }
608
- function clampNodeWidth(type, width, fallback) {
609
- const bounds = getNodeSizeBounds(type);
635
+ function clampNodeWidth(type, width, fallback, sizeBounds) {
636
+ const bounds = getNodeSizeBounds(type, sizeBounds);
610
637
  return clamp(toFiniteNumber(width, fallback), bounds.minWidth, bounds.maxWidth);
611
638
  }
612
- function clampNodeHeight(type, height, fallback) {
613
- const bounds = getNodeSizeBounds(type);
639
+ function clampNodeHeight(type, height, fallback, sizeBounds) {
640
+ const bounds = getNodeSizeBounds(type, sizeBounds);
614
641
  return clamp(toFiniteNumber(height, fallback), bounds.minHeight, bounds.maxHeight);
615
642
  }
616
643
  function isNodeResizable(nodeOrType) {
@@ -634,14 +661,16 @@ function getStrokePresetWidth(presetId) {
634
661
  const preset = EDGE_STROKE_PRESETS.find((item) => item.id === presetId);
635
662
  return preset?.width ?? DEFAULT_EDGE_STROKE_WIDTH;
636
663
  }
637
- function normalizeViewport(viewport) {
664
+ function normalizeViewport(viewport, scaleBounds) {
665
+ const min = scaleBounds?.min ?? DEFAULT_MIN_SCALE;
666
+ const max = scaleBounds?.max ?? DEFAULT_MAX_SCALE;
638
667
  return {
639
668
  x: toFiniteNumber(viewport?.x, 0),
640
669
  y: toFiniteNumber(viewport?.y, 0),
641
- scale: clamp(toFiniteNumber(viewport?.scale, 1), MIN_SCALE, MAX_SCALE)
670
+ scale: clamp(formatScale(toFiniteNumber(viewport?.scale, 1)), min, max)
642
671
  };
643
672
  }
644
- function normalizeNode(rawNode, index, usedIds) {
673
+ function normalizeNode(rawNode, index, usedIds, sizeBounds) {
645
674
  const type = normalizeNodeType(rawNode?.type);
646
675
  const spec = getNodeSpec(type);
647
676
  const node = {
@@ -649,8 +678,8 @@ function normalizeNode(rawNode, index, usedIds) {
649
678
  type,
650
679
  x: toFiniteNumber(rawNode?.x, index * 28),
651
680
  y: toFiniteNumber(rawNode?.y, index * 18),
652
- width: clampNodeWidth(type, rawNode?.width, spec.width),
653
- height: clampNodeHeight(type, rawNode?.height, spec.height),
681
+ width: clampNodeWidth(type, rawNode?.width, spec.width, sizeBounds),
682
+ height: clampNodeHeight(type, rawNode?.height, spec.height, sizeBounds),
654
683
  text: rawNode?.text == null ? spec.text : String(rawNode.text),
655
684
  data: isPlainObject(rawNode?.data) ? deepClone(rawNode.data) : {}
656
685
  };
@@ -743,7 +772,7 @@ function isSupportedDocumentVersion(value) {
743
772
  if (parsed.minor === max.minor && parsed.patch > max.patch) return false;
744
773
  return true;
745
774
  }
746
- function normalizeDocument(input) {
775
+ function normalizeDocument(input, limits = {}) {
747
776
  let source = input;
748
777
  if (typeof source === "string") {
749
778
  try {
@@ -766,13 +795,13 @@ function normalizeDocument(input) {
766
795
  }
767
796
  }
768
797
  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));
798
+ const nodes = (Array.isArray(source.nodes) ? source.nodes : []).slice(0, MAX_NODES).map((node, index) => normalizeNode(node, index, usedNodeIds, limits.sizeBounds));
770
799
  const nodeIds = new Set(nodes.map((node) => node.id));
771
800
  const usedEdgeIds = /* @__PURE__ */ new Set();
772
801
  const edges = (Array.isArray(source.edges) ? source.edges : []).slice(0, MAX_EDGES).map((edge, index) => normalizeEdge(edge, index, nodeIds, usedEdgeIds)).filter(Boolean);
773
802
  return {
774
803
  version: FLOWCHART_DOCUMENT_VERSION,
775
- viewport: normalizeViewport(source.viewport),
804
+ viewport: normalizeViewport(source.viewport, limits.scaleBounds),
776
805
  nodes,
777
806
  edges
778
807
  };
@@ -989,19 +1018,30 @@ function getPolylineLabelPoint(points) {
989
1018
  const last = points[points.length - 1] || { x: 0, y: 0 };
990
1019
  return { x: formatNumber(last.x), y: formatNumber(last.y) };
991
1020
  }
992
- function curveControlPoint(point, port, target, curvature) {
1021
+ function routeMetrics(edgeScale = 1) {
1022
+ const scale = clamp(toFiniteNumber(edgeScale, 1), 0.01, 1);
1023
+ return {
1024
+ scale,
1025
+ minCurveArm: MIN_CURVE_ARM * scale,
1026
+ maxCurveArm: MAX_CURVE_ARM * scale,
1027
+ stub: ORTHOGONAL_STUB_LENGTH * scale,
1028
+ corner: ORTHOGONAL_CORNER_RADIUS * scale,
1029
+ clearance: ORTHOGONAL_CLEARANCE * scale
1030
+ };
1031
+ }
1032
+ function curveControlPoint(point, port, target, curvature, metrics = routeMetrics(1)) {
993
1033
  const normal = getPortNormal(port);
994
1034
  const dx = target.x - point.x;
995
1035
  const dy = target.y - point.y;
996
1036
  const distance = Math.hypot(dx, dy) || 1;
997
1037
  const align = (normal.x * dx + normal.y * dy) / distance;
998
1038
  const ease = 0.55 + 0.45 * align;
999
- const arm = clamp(distance * curvature * ease, MIN_CURVE_ARM, MAX_CURVE_ARM);
1039
+ const arm = clamp(distance * curvature * ease, metrics.minCurveArm, metrics.maxCurveArm);
1000
1040
  return { x: point.x + normal.x * arm, y: point.y + normal.y * arm };
1001
1041
  }
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);
1042
+ function buildCurvePath(fromPoint, toPoint, fromPort = "right", toPort = "left", metrics = routeMetrics(1)) {
1043
+ const controlA = curveControlPoint(fromPoint, fromPort, toPoint, EDGE_CURVATURE, metrics);
1044
+ const controlB = curveControlPoint(toPoint, toPort, fromPoint, EDGE_CURVATURE, metrics);
1005
1045
  const labelX = 0.125 * fromPoint.x + 0.375 * controlA.x + 0.375 * controlB.x + 0.125 * toPoint.x;
1006
1046
  const labelY = 0.125 * fromPoint.y + 0.375 * controlA.y + 0.375 * controlB.y + 0.125 * toPoint.y;
1007
1047
  return {
@@ -1063,11 +1103,11 @@ function getOrthogonalMidpoints(fromStub, toStub, fromPort, toPort) {
1063
1103
  }
1064
1104
  return [{ x: fromStub.x, y: toStub.y }];
1065
1105
  }
1066
- function getOrthogonalPoints(fromPoint, toPoint, fromPort, toPort, fromRect, toRect) {
1106
+ function getOrthogonalPoints(fromPoint, toPoint, fromPort, toPort, fromRect, toRect, metrics = routeMetrics(1)) {
1067
1107
  const fromNormal = getPortNormal(fromPort);
1068
1108
  const toNormal = getPortNormal(toPort);
1069
- const fromStub = offsetPoint(fromPoint, fromNormal, ORTHOGONAL_STUB_LENGTH);
1070
- const toStub = offsetPoint(toPoint, toNormal, ORTHOGONAL_STUB_LENGTH);
1109
+ const fromStub = offsetPoint(fromPoint, fromNormal, metrics.stub);
1110
+ const toStub = offsetPoint(toPoint, toNormal, metrics.stub);
1071
1111
  const sourceHorizontal = isHorizontalPort(fromPort);
1072
1112
  const targetHorizontal = isHorizontalPort(toPort);
1073
1113
  const mids = [];
@@ -1107,13 +1147,13 @@ function getOrthogonalPoints(fromPoint, toPoint, fromPort, toPort, fromRect, toR
1107
1147
  );
1108
1148
  return points;
1109
1149
  }
1110
- function buildOrthogonalPath(fromPoint, toPoint, fromPort = "right", toPort = "left", fromRect = null, toRect = null) {
1150
+ function buildOrthogonalPath(fromPoint, toPoint, fromPort = "right", toPort = "left", fromRect = null, toRect = null, metrics = routeMetrics(1)) {
1111
1151
  let points;
1112
1152
  if (fromRect && toRect) {
1113
- points = getOrthogonalPoints(fromPoint, toPoint, fromPort, toPort, fromRect, toRect);
1153
+ points = getOrthogonalPoints(fromPoint, toPoint, fromPort, toPort, fromRect, toRect, metrics);
1114
1154
  } else {
1115
- const fromStub = offsetPoint(fromPoint, getPortNormal(fromPort), ORTHOGONAL_STUB_LENGTH);
1116
- const toStub = offsetPoint(toPoint, getPortNormal(toPort), ORTHOGONAL_STUB_LENGTH);
1155
+ const fromStub = offsetPoint(fromPoint, getPortNormal(fromPort), metrics.stub);
1156
+ const toStub = offsetPoint(toPoint, getPortNormal(toPort), metrics.stub);
1117
1157
  points = [];
1118
1158
  [
1119
1159
  fromPoint,
@@ -1125,7 +1165,7 @@ function buildOrthogonalPath(fromPoint, toPoint, fromPort = "right", toPort = "l
1125
1165
  }
1126
1166
  const label = getPolylineLabelPoint(points);
1127
1167
  return {
1128
- d: buildRoundedPath(points, ORTHOGONAL_CORNER_RADIUS),
1168
+ d: buildRoundedPath(points, metrics.corner),
1129
1169
  labelX: label.x,
1130
1170
  labelY: label.y
1131
1171
  };
@@ -1136,6 +1176,7 @@ function buildEdgePath(edge, fromNode = null, toNode = null) {
1136
1176
  const fromPoint = edge?.fromPoint || (fromNode ? getPortPosition(fromNode, fromPort) : null);
1137
1177
  const toPoint = edge?.toPoint || (toNode ? getPortPosition(toNode, toPort) : null);
1138
1178
  const route = normalizeEdgeRoute(edge?.route);
1179
+ const metrics = routeMetrics(edgeVisualScale(fromNode, toNode));
1139
1180
  if (!fromPoint || !toPoint) {
1140
1181
  return { d: "", labelX: 0, labelY: 0 };
1141
1182
  }
@@ -1143,15 +1184,17 @@ function buildEdgePath(edge, fromNode = null, toNode = null) {
1143
1184
  return buildStraightPath(fromPoint, toPoint);
1144
1185
  }
1145
1186
  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);
1187
+ const fromRect = !edge?.fromPoint && fromNode ? getNodeRectBounds(fromNode, metrics.clearance) : null;
1188
+ const toRect = !edge?.toPoint && toNode ? getNodeRectBounds(toNode, metrics.clearance) : null;
1189
+ return buildOrthogonalPath(fromPoint, toPoint, fromPort, toPort, fromRect, toRect, metrics);
1149
1190
  }
1150
- return buildCurvePath(fromPoint, toPoint, fromPort, toPort);
1191
+ return buildCurvePath(fromPoint, toPoint, fromPort, toPort, metrics);
1151
1192
  }
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));
1193
+ function createArrowMarker(id, strokeWidth, reversed = false, edgeScale = 1) {
1194
+ const scale = clamp(toFiniteNumber(edgeScale, 1), 0.01, 1);
1195
+ const effective = Math.max(0.02, strokeWidth * scale);
1196
+ const size = formatNumber(Math.max(9 * scale, effective * 4.4));
1197
+ const refInset = formatNumber(Math.max(1.5 * scale, effective * 0.85));
1155
1198
  const marker = svgEl("marker", {
1156
1199
  id,
1157
1200
  markerWidth: size,
@@ -1169,9 +1212,11 @@ function createArrowMarker(id, strokeWidth, reversed = false) {
1169
1212
  );
1170
1213
  return marker;
1171
1214
  }
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));
1215
+ function createDotMarker(id, strokeWidth, edgeScale = 1) {
1216
+ const scale = clamp(toFiniteNumber(edgeScale, 1), 0.01, 1);
1217
+ const effective = Math.max(0.02, strokeWidth * scale);
1218
+ const size = formatNumber(Math.max(8 * scale, effective * 3.3));
1219
+ const radius = formatNumber(Math.max(2.75 * scale, effective * 1.45));
1175
1220
  const marker = svgEl("marker", {
1176
1221
  id,
1177
1222
  markerWidth: size,
@@ -1428,7 +1473,23 @@ var VdFlowchart = class {
1428
1473
  this.element = resolveElement(options.element || options.target);
1429
1474
  this.readonly = Boolean(options.readonly);
1430
1475
  this.gridSize = clamp(toFiniteNumber(options.gridSize, DEFAULT_GRID_SIZE), 12, 64);
1431
- this.documentData = normalizeDocument(options.data || {});
1476
+ const scaleBounds = normalizeBoundPair(
1477
+ options.minScale,
1478
+ options.maxScale,
1479
+ DEFAULT_MIN_SCALE,
1480
+ DEFAULT_MAX_SCALE
1481
+ );
1482
+ const sizeBounds = normalizeBoundPair(
1483
+ options.minNodeSize,
1484
+ options.maxNodeSize,
1485
+ DEFAULT_MIN_NODE_SIZE,
1486
+ DEFAULT_MAX_NODE_SIZE
1487
+ );
1488
+ this.minScale = scaleBounds.min;
1489
+ this.maxScale = scaleBounds.max;
1490
+ this.minNodeSize = sizeBounds.min;
1491
+ this.maxNodeSize = sizeBounds.max;
1492
+ this.documentData = normalizeDocument(options.data || {}, { scaleBounds, sizeBounds });
1432
1493
  this.listeners = {};
1433
1494
  this.selection = null;
1434
1495
  this.selectedNodeIds = /* @__PURE__ */ new Set();
@@ -1656,10 +1717,10 @@ var VdFlowchart = class {
1656
1717
  this.world = svgEl("g", { class: "vd-flowchart-world" });
1657
1718
  this.gridRect = svgEl("rect", {
1658
1719
  class: "vd-flowchart-grid",
1659
- x: -WORLD_EXTENT / 2,
1660
- y: -WORLD_EXTENT / 2,
1661
- width: WORLD_EXTENT,
1662
- height: WORLD_EXTENT,
1720
+ x: -DEFAULT_WORLD_EXTENT / 2,
1721
+ y: -DEFAULT_WORLD_EXTENT / 2,
1722
+ width: DEFAULT_WORLD_EXTENT,
1723
+ height: DEFAULT_WORLD_EXTENT,
1663
1724
  fill: `url(#${this.gridPatternId})`
1664
1725
  });
1665
1726
  this.edgesLayer = svgEl("g", { class: "vd-flowchart-edges" });
@@ -2442,7 +2503,7 @@ var VdFlowchart = class {
2442
2503
  const width = this.canvasEl.clientWidth || 800;
2443
2504
  const height = this.canvasEl.clientHeight || 560;
2444
2505
  this.scaleAround(
2445
- clamp(scale, MIN_SCALE, MAX_SCALE) / viewport.scale,
2506
+ clamp(formatScale(scale), this.minScale, this.maxScale) / viewport.scale,
2446
2507
  width / 2,
2447
2508
  height / 2,
2448
2509
  "viewport:zoom"
@@ -2477,7 +2538,8 @@ var VdFlowchart = class {
2477
2538
  const node = normalizeNode(
2478
2539
  { ...data, id: void 0, x: data.x + 24, y: data.y + 24 },
2479
2540
  this.documentData.nodes.length + index,
2480
- usedNodeIds
2541
+ usedNodeIds,
2542
+ this.nodeSizeBounds()
2481
2543
  );
2482
2544
  idMap.set(data.id, node.id);
2483
2545
  return node;
@@ -2509,7 +2571,8 @@ var VdFlowchart = class {
2509
2571
  const node = normalizeNode(
2510
2572
  { ...data, id: void 0, collapsed: void 0, x: data.x + 24, y: data.y + 24 },
2511
2573
  this.documentData.nodes.length,
2512
- usedIds
2574
+ usedIds,
2575
+ this.nodeSizeBounds()
2513
2576
  );
2514
2577
  this.documentData.nodes.push(node);
2515
2578
  this.select({ kind: "node", id: node.id });
@@ -3123,14 +3186,15 @@ var VdFlowchart = class {
3123
3186
  if (this.activeTool === "arrow") return true;
3124
3187
  return this.selection?.kind === "node" && this.selection.id === node.id && !this.hasMultiSelection();
3125
3188
  }
3126
- getMarkerId(markerType, position, strokeWidth) {
3189
+ getMarkerId(markerType, position, strokeWidth, edgeScale = 1) {
3127
3190
  const width = normalizeEdgeStrokeWidth(strokeWidth);
3128
- const key = `${markerType}:${position}:${width}`;
3191
+ const scale = clamp(formatNumber(Math.max(edgeScale, 0.01)), 0.01, 1);
3192
+ const key = `${markerType}:${position}:${width}:${scale}`;
3129
3193
  if (this.markerIds.has(key)) {
3130
3194
  return this.markerIds.get(key);
3131
3195
  }
3132
3196
  const markerId = nextId(`flowchart-${markerType}-${position}`);
3133
- const marker = markerType === "dot" ? createDotMarker(markerId, width) : createArrowMarker(markerId, width, position === "start");
3197
+ const marker = markerType === "dot" ? createDotMarker(markerId, width, scale) : createArrowMarker(markerId, width, position === "start", scale);
3134
3198
  this.markerDefs.appendChild(marker);
3135
3199
  this.markerIds.set(key, markerId);
3136
3200
  return markerId;
@@ -3138,8 +3202,15 @@ var VdFlowchart = class {
3138
3202
  getEdgeStrokeWidth(edge) {
3139
3203
  return normalizeEdgeStrokeWidth(edge?.strokeWidth);
3140
3204
  }
3141
- getEdgeHitStrokeWidth(edge) {
3142
- return formatNumber(Math.max(EDGE_HIT_STROKE_MIN, this.getEdgeStrokeWidth(edge) + 12));
3205
+ getEdgeHitStrokeWidth(edge, edgeScale = 1) {
3206
+ const visual = clamp(toFiniteNumber(edgeScale, 1), 0.01, 1);
3207
+ const base = this.getEdgeStrokeWidth(edge);
3208
+ const view = Math.max(this.documentData?.viewport?.scale || 1, 1e-4);
3209
+ const legacy = Math.max(EDGE_HIT_STROKE_MIN, base + 12);
3210
+ if (visual >= 0.999 && view >= DEFAULT_MIN_SCALE && view <= DEFAULT_MAX_SCALE) {
3211
+ return formatNumber(legacy);
3212
+ }
3213
+ return formatNumber(Math.max(base * visual, EDGE_HIT_STROKE_MIN / view));
3143
3214
  }
3144
3215
  findConnectionTarget(worldPoint, excludeNodeId, previousSnap = null, referencePoint = null) {
3145
3216
  const snapPadding = CONNECTION_SNAP_PADDING / this.documentData.viewport.scale;
@@ -3196,17 +3267,37 @@ var VdFlowchart = class {
3196
3267
  let width = original.width;
3197
3268
  let height = original.height;
3198
3269
  if (handle.includes("e")) {
3199
- width = clampNodeWidth(nextType, original.width + deltaX, original.width);
3270
+ width = clampNodeWidth(
3271
+ nextType,
3272
+ original.width + deltaX,
3273
+ original.width,
3274
+ this.nodeSizeBounds()
3275
+ );
3200
3276
  }
3201
3277
  if (handle.includes("s")) {
3202
- height = clampNodeHeight(nextType, original.height + deltaY, original.height);
3278
+ height = clampNodeHeight(
3279
+ nextType,
3280
+ original.height + deltaY,
3281
+ original.height,
3282
+ this.nodeSizeBounds()
3283
+ );
3203
3284
  }
3204
3285
  if (handle.includes("w")) {
3205
- width = clampNodeWidth(nextType, original.width - deltaX, original.width);
3286
+ width = clampNodeWidth(
3287
+ nextType,
3288
+ original.width - deltaX,
3289
+ original.width,
3290
+ this.nodeSizeBounds()
3291
+ );
3206
3292
  x = original.x + original.width - width;
3207
3293
  }
3208
3294
  if (handle.includes("n")) {
3209
- height = clampNodeHeight(nextType, original.height - deltaY, original.height);
3295
+ height = clampNodeHeight(
3296
+ nextType,
3297
+ original.height - deltaY,
3298
+ original.height,
3299
+ this.nodeSizeBounds()
3300
+ );
3210
3301
  y = original.y + original.height - height;
3211
3302
  }
3212
3303
  return {
@@ -3242,7 +3333,7 @@ var VdFlowchart = class {
3242
3333
  scaleAround(factor, localX, localY, reason) {
3243
3334
  const viewport = this.documentData.viewport;
3244
3335
  const anchor = this.localToWorld(localX, localY);
3245
- const nextScale = clamp(formatNumber(viewport.scale * factor), MIN_SCALE, MAX_SCALE);
3336
+ const nextScale = clamp(formatScale(viewport.scale * factor), this.minScale, this.maxScale);
3246
3337
  if (nextScale === viewport.scale) return;
3247
3338
  viewport.scale = nextScale;
3248
3339
  viewport.x = formatNumber(localX - anchor.x * nextScale);
@@ -3425,19 +3516,20 @@ var VdFlowchart = class {
3425
3516
  }
3426
3517
  const viewport = this.documentData.viewport;
3427
3518
  const scale = viewport.scale;
3519
+ const visual = nodeVisualScale(node);
3428
3520
  const textarea = this.textEditor.textarea;
3429
3521
  const width = node.width * scale;
3430
3522
  const height = node.height * scale;
3431
3523
  const metrics = getNodeFontMetrics(node);
3432
- const lineHeight = metrics.lineHeight * scale;
3524
+ const lineHeight = metrics.lineHeight * visual * scale;
3433
3525
  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);
3526
+ const horizontalPadding = (node.type === "textbox" ? 16 : 12) * visual * scale;
3527
+ const verticalPadding = node.type === "textbox" ? 12 * visual * scale : Math.max(4 * visual, (height - lineCount * lineHeight) / 2);
3436
3528
  textarea.style.left = `${formatNumber(viewport.x + node.x * scale)}px`;
3437
3529
  textarea.style.top = `${formatNumber(viewport.y + node.y * scale)}px`;
3438
3530
  textarea.style.width = `${formatNumber(width)}px`;
3439
3531
  textarea.style.height = `${formatNumber(height)}px`;
3440
- textarea.style.fontSize = `${formatNumber(metrics.fontSize * scale)}px`;
3532
+ textarea.style.fontSize = `${formatNumber(metrics.fontSize * visual * scale)}px`;
3441
3533
  textarea.style.lineHeight = `${formatNumber(lineHeight)}px`;
3442
3534
  textarea.style.padding = `${formatNumber(verticalPadding)}px ${formatNumber(horizontalPadding)}px`;
3443
3535
  }
@@ -3449,14 +3541,16 @@ var VdFlowchart = class {
3449
3541
  this.stopTextEdit({ commit: false });
3450
3542
  return;
3451
3543
  }
3544
+ const visual = edgeVisualScale(fromNode, toNode);
3452
3545
  const { labelX, labelY } = buildEdgePath(edge, fromNode, toNode);
3453
3546
  const viewport = this.documentData.viewport;
3454
3547
  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;
3548
+ const fontSize = Math.max(11 * visual, 13 * visual * viewport.scale);
3549
+ const width = clamp(textarea.value.length * fontSize * 0.62 + 28 * visual, 96 * visual, 280);
3550
+ const height = fontSize * 1.4 + 12 * visual;
3551
+ const labelOffset = 8 * visual;
3458
3552
  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`;
3553
+ textarea.style.top = `${formatNumber(viewport.y + (labelY - labelOffset) * viewport.scale - height / 2)}px`;
3460
3554
  textarea.style.width = `${formatNumber(width)}px`;
3461
3555
  textarea.style.height = `${formatNumber(height)}px`;
3462
3556
  textarea.style.fontSize = `${formatNumber(fontSize)}px`;
@@ -3465,6 +3559,7 @@ var VdFlowchart = class {
3465
3559
  }
3466
3560
  render(options = {}) {
3467
3561
  if (this.destroyed) return;
3562
+ this.syncGrid();
3468
3563
  const shouldRenderScene = options.scene !== false;
3469
3564
  const shouldRenderInspector = options.inspector !== false;
3470
3565
  const shouldRenderJson = options.json !== false;
@@ -3488,6 +3583,8 @@ var VdFlowchart = class {
3488
3583
  clearChildren(this.previewLayer);
3489
3584
  clearChildren(this.nodesLayer);
3490
3585
  clearChildren(this.overlayLayer);
3586
+ clearChildren(this.markerDefs);
3587
+ this.markerIds = /* @__PURE__ */ new Map();
3491
3588
  const hidden = this.getHiddenNodeIds();
3492
3589
  this.sceneHidden = hidden;
3493
3590
  this.sceneOutgoing = this.getOutgoingMap();
@@ -3551,17 +3648,18 @@ var VdFlowchart = class {
3551
3648
  }
3552
3649
  }
3553
3650
  }
3554
- applyEdgeMarkers(pathElement, edge) {
3651
+ applyEdgeMarkers(pathElement, edge, edgeScale = 1) {
3652
+ const width = this.getEdgeStrokeWidth(edge);
3555
3653
  if (edge.startMarker && edge.startMarker !== "none") {
3556
3654
  pathElement.setAttribute(
3557
3655
  "marker-start",
3558
- `url(#${this.getMarkerId(edge.startMarker, "start", this.getEdgeStrokeWidth(edge))})`
3656
+ `url(#${this.getMarkerId(edge.startMarker, "start", width, edgeScale)})`
3559
3657
  );
3560
3658
  }
3561
3659
  if (edge.endMarker && edge.endMarker !== "none") {
3562
3660
  pathElement.setAttribute(
3563
3661
  "marker-end",
3564
- `url(#${this.getMarkerId(edge.endMarker, "end", this.getEdgeStrokeWidth(edge))})`
3662
+ `url(#${this.getMarkerId(edge.endMarker, "end", width, edgeScale)})`
3565
3663
  );
3566
3664
  }
3567
3665
  }
@@ -3569,6 +3667,7 @@ var VdFlowchart = class {
3569
3667
  const fromNode = nodeMap.get(edge.from.nodeId);
3570
3668
  const toNode = nodeMap.get(edge.to.nodeId);
3571
3669
  if (!fromNode || !toNode) return null;
3670
+ const visual = edgeVisualScale(fromNode, toNode);
3572
3671
  const edgePath = buildEdgePath(edge, fromNode, toNode);
3573
3672
  const selected = this.selection?.kind === "edge" && this.selection.id === edge.id;
3574
3673
  const inReconnectMode = this.reconnectEdgeId === edge.id;
@@ -3576,13 +3675,14 @@ var VdFlowchart = class {
3576
3675
  class: `vd-flowchart-edge${selected ? " is-selected" : ""}${inReconnectMode ? " is-reconnecting" : ""}`,
3577
3676
  "data-edge-id": edge.id
3578
3677
  });
3579
- const strokeWidth = this.getEdgeStrokeWidth(edge);
3678
+ const baseStroke = this.getEdgeStrokeWidth(edge);
3679
+ const strokeWidth = visual < 0.999 ? formatNumber(Math.max(0.02, baseStroke * visual)) : baseStroke;
3580
3680
  if (selected) {
3581
3681
  group.appendChild(
3582
3682
  svgEl("path", {
3583
3683
  class: "vd-flowchart-edge-selection",
3584
3684
  d: edgePath.d,
3585
- "stroke-width": formatNumber(strokeWidth + 7)
3685
+ "stroke-width": visual < 0.999 ? formatNumber((baseStroke + 7) * visual) : formatNumber(strokeWidth + 7)
3586
3686
  })
3587
3687
  );
3588
3688
  }
@@ -3592,23 +3692,28 @@ var VdFlowchart = class {
3592
3692
  "stroke-width": strokeWidth,
3593
3693
  "data-edge-id": edge.id
3594
3694
  });
3595
- this.applyEdgeMarkers(visiblePath, edge);
3695
+ this.applyEdgeMarkers(visiblePath, edge, visual);
3596
3696
  const hitPath = svgEl("path", {
3597
3697
  class: "vd-flowchart-edge-hit",
3598
3698
  d: edgePath.d,
3599
- "stroke-width": this.getEdgeHitStrokeWidth(edge),
3699
+ "stroke-width": this.getEdgeHitStrokeWidth(edge, visual),
3600
3700
  "data-edge-id": edge.id
3601
3701
  });
3602
3702
  group.appendChild(visiblePath);
3603
3703
  group.appendChild(hitPath);
3604
3704
  if (edge.label && this.textEditor?.edgeId !== edge.id) {
3705
+ const labelOffset = visual < 0.999 ? formatNumber(8 * visual) : 8;
3605
3706
  const label = svgEl("text", {
3606
3707
  class: "vd-flowchart-edge-label",
3607
3708
  x: edgePath.labelX,
3608
- y: edgePath.labelY - 8,
3709
+ y: edgePath.labelY - labelOffset,
3609
3710
  "text-anchor": "middle",
3610
3711
  "data-edge-id": edge.id
3611
3712
  });
3713
+ if (visual < 0.999) {
3714
+ label.style.fontSize = `${formatNumber(Math.max(0.06, 12 * visual))}px`;
3715
+ label.style.strokeWidth = `${formatNumber(Math.max(0.03, 5 * visual))}px`;
3716
+ }
3612
3717
  label.textContent = edge.label;
3613
3718
  group.appendChild(label);
3614
3719
  }
@@ -3647,18 +3752,26 @@ var VdFlowchart = class {
3647
3752
  renderPreviewEdge(interaction) {
3648
3753
  const fromPort = interaction.fromPort || interaction.source?.port || "right";
3649
3754
  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
- });
3755
+ const fromNode = interaction.source?.nodeId ? this.findNode(interaction.source.nodeId) : null;
3756
+ const toNode = interaction.target?.nodeId ? this.findNode(interaction.target.nodeId) : null;
3757
+ const visual = edgeVisualScale(fromNode, toNode);
3758
+ const edgePath = buildEdgePath(
3759
+ {
3760
+ strokeWidth: interaction.strokeWidth,
3761
+ route: interaction.route,
3762
+ fromPoint: interaction.fromPoint,
3763
+ toPoint: interaction.toPoint,
3764
+ from: { port: fromPort },
3765
+ to: { port: toPort }
3766
+ },
3767
+ fromNode,
3768
+ toNode
3769
+ );
3770
+ const baseStroke = this.getEdgeStrokeWidth(interaction);
3658
3771
  return svgEl("path", {
3659
3772
  class: "vd-flowchart-preview-path",
3660
3773
  d: edgePath.d,
3661
- "stroke-width": this.getEdgeStrokeWidth(interaction)
3774
+ "stroke-width": visual < 0.999 ? formatNumber(Math.max(0.02, baseStroke * visual)) : baseStroke
3662
3775
  });
3663
3776
  }
3664
3777
  renderNode(node) {
@@ -3673,17 +3786,21 @@ var VdFlowchart = class {
3673
3786
  "data-node-id": node.id
3674
3787
  });
3675
3788
  const portsVisible = this.shouldShowNodePorts(node);
3789
+ const visual = nodeVisualScale(node);
3790
+ const corner = node.type === "rounded-rect" || node.type === "textbox" ? 16 : node.type === "junction" ? node.width / 2 : 6;
3676
3791
  const hitbox = svgEl("rect", {
3677
3792
  x: 0,
3678
3793
  y: 0,
3679
3794
  width: node.width,
3680
3795
  height: node.height,
3681
- rx: node.type === "rounded-rect" || node.type === "textbox" ? 16 : node.type === "junction" ? node.width / 2 : 6,
3796
+ rx: visual < 0.999 && node.type !== "junction" ? formatNumber(corner * visual) : corner,
3682
3797
  fill: "transparent",
3683
3798
  "data-node-id": node.id
3684
3799
  });
3685
3800
  group.appendChild(hitbox);
3686
- group.appendChild(this.renderNodeShape(node));
3801
+ const shape = this.renderNodeShape(node);
3802
+ this.applyScaledNodeShape(node, shape);
3803
+ group.appendChild(shape);
3687
3804
  group.appendChild(this.renderNodeText(node));
3688
3805
  const scale = this.documentData.viewport.scale || 1;
3689
3806
  if (soloSelected && !this.readonly && SELECTION_BOX_TYPES.has(node.type)) {
@@ -3875,6 +3992,21 @@ var VdFlowchart = class {
3875
3992
  });
3876
3993
  return group;
3877
3994
  }
3995
+ applyScaledNodeShape(node, shape) {
3996
+ const visual = nodeVisualScale(node);
3997
+ if (!shape || visual >= 0.999) return;
3998
+ const baseStroke = node.type === "junction" ? 1.25 : 1.5;
3999
+ shape.style.strokeWidth = String(formatNumber(Math.max(0.03, baseStroke * visual)));
4000
+ if (node.type === "textbox") {
4001
+ shape.style.strokeDasharray = `${formatNumber(8 * visual)} ${formatNumber(5 * visual)}`;
4002
+ }
4003
+ const baseRadius = node.type === "rounded-rect" ? 18 : node.type === "textbox" ? 14 : node.type === "rect" ? 2 : 0;
4004
+ if (baseRadius > 0) {
4005
+ const scaled = String(formatNumber(baseRadius * visual));
4006
+ shape.setAttribute("rx", scaled);
4007
+ shape.setAttribute("ry", scaled);
4008
+ }
4009
+ }
3878
4010
  renderNodeShape(node) {
3879
4011
  const baseClass = `vd-flowchart-node-shape vd-flowchart-node-shape--${node.type}`;
3880
4012
  if (node.type === "rounded-rect") {
@@ -3951,21 +4083,28 @@ var VdFlowchart = class {
3951
4083
  if (node.type === "junction" || !node.text) {
3952
4084
  return textElement;
3953
4085
  }
4086
+ const visual = nodeVisualScale(node);
4087
+ const metrics = getNodeFontMetrics(node);
4088
+ const lineHeight = visual < 0.999 ? Math.max(0.06, metrics.lineHeight * visual) : metrics.lineHeight;
4089
+ if (visual < 0.999) {
4090
+ textElement.style.fontSize = `${formatNumber(Math.max(0.05, metrics.fontSize * visual))}px`;
4091
+ }
4092
+ const equivalentWidth = visual < 0.999 ? node.width / visual : node.width;
3954
4093
  if (node.type === "textbox") {
3955
- const lines2 = wrapText(node.text, estimateChars(node.width - 32));
3956
- const { lineHeight: lineHeight2 } = getNodeFontMetrics(node);
4094
+ const lines2 = wrapText(node.text, estimateChars(equivalentWidth - 32));
4095
+ const padX = visual < 0.999 ? formatNumber(16 * visual) : 16;
4096
+ const startY2 = visual < 0.999 ? formatNumber(28 * visual) : 28;
3957
4097
  lines2.forEach((line, index) => {
3958
4098
  const span = svgEl("tspan", {
3959
- x: 16,
3960
- y: 28 + index * lineHeight2
4099
+ x: padX,
4100
+ y: visual < 0.999 ? formatNumber(startY2 + index * lineHeight) : startY2 + index * lineHeight
3961
4101
  });
3962
4102
  span.textContent = line;
3963
4103
  textElement.appendChild(span);
3964
4104
  });
3965
4105
  return textElement;
3966
4106
  }
3967
- const lines = wrapText(node.text, estimateChars(node.width));
3968
- const { lineHeight } = getNodeFontMetrics(node);
4107
+ const lines = wrapText(node.text, estimateChars(equivalentWidth));
3969
4108
  const totalHeight = (lines.length - 1) * lineHeight;
3970
4109
  const startY = node.height / 2 - totalHeight / 2;
3971
4110
  textElement.setAttribute("text-anchor", "middle");
@@ -3973,7 +4112,7 @@ var VdFlowchart = class {
3973
4112
  lines.forEach((line, index) => {
3974
4113
  const span = svgEl("tspan", {
3975
4114
  x: node.width / 2,
3976
- y: startY + index * lineHeight,
4115
+ y: visual < 0.999 ? formatNumber(startY + index * lineHeight) : startY + index * lineHeight,
3977
4116
  "text-anchor": "middle",
3978
4117
  "dominant-baseline": "middle"
3979
4118
  });
@@ -4025,12 +4164,12 @@ var VdFlowchart = class {
4025
4164
  textArea.setAttribute("data-field", "node-text");
4026
4165
  const widthInput = createElement("input", { value: node.width, type: "number" });
4027
4166
  widthInput.setAttribute("data-field", "node-width");
4028
- widthInput.setAttribute("min", String(MIN_NODE_SIZE));
4029
- widthInput.setAttribute("max", String(MAX_NODE_SIZE));
4167
+ widthInput.setAttribute("min", String(this.minNodeSize));
4168
+ widthInput.setAttribute("max", String(this.maxNodeSize));
4030
4169
  const heightInput = createElement("input", { value: node.height, type: "number" });
4031
4170
  heightInput.setAttribute("data-field", "node-height");
4032
- heightInput.setAttribute("min", String(MIN_NODE_SIZE));
4033
- heightInput.setAttribute("max", String(MAX_NODE_SIZE));
4171
+ heightInput.setAttribute("min", String(this.minNodeSize));
4172
+ heightInput.setAttribute("max", String(this.maxNodeSize));
4034
4173
  this.selectionFields.appendChild(createField("Type", typeSelect));
4035
4174
  if (isNodeTextEditable(node)) {
4036
4175
  this.selectionFields.appendChild(createField("Text", textArea));
@@ -4344,8 +4483,8 @@ var VdFlowchart = class {
4344
4483
  this.reconnectEdgeId = null;
4345
4484
  const currentViewport = deepClone(this.documentData.viewport);
4346
4485
  const previousSelection = this.selection;
4347
- this.documentData = normalizeDocument(snapshot);
4348
- this.documentData.viewport = normalizeViewport(currentViewport);
4486
+ this.documentData = normalizeDocument(snapshot, this.normalizationLimits());
4487
+ this.documentData.viewport = normalizeViewport(currentViewport, this.scaleBounds());
4349
4488
  this.selection = this.resolvePreservedSelection(previousSelection, true);
4350
4489
  this.render();
4351
4490
  this.emitChange(reason);
@@ -4422,7 +4561,8 @@ var VdFlowchart = class {
4422
4561
  y: partialNode.y == null ? fallbackY : partialNode.y
4423
4562
  },
4424
4563
  this.documentData.nodes.length,
4425
- usedIds
4564
+ usedIds,
4565
+ this.nodeSizeBounds()
4426
4566
  );
4427
4567
  this.documentData.nodes.push(node);
4428
4568
  this.select({ kind: "node", id: node.id });
@@ -4675,8 +4815,8 @@ var VdFlowchart = class {
4675
4815
  node.x = patch.x == null ? node.x : formatNumber(toFiniteNumber(patch.x, node.x));
4676
4816
  node.y = patch.y == null ? node.y : formatNumber(toFiniteNumber(patch.y, node.y));
4677
4817
  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);
4818
+ node.width = clampNodeWidth(nextType, patch.width, node.width, this.nodeSizeBounds());
4819
+ node.height = clampNodeHeight(nextType, patch.height, node.height, this.nodeSizeBounds());
4680
4820
  if (isPlainObject(patch.data)) {
4681
4821
  node.data = deepClone(patch.data);
4682
4822
  }
@@ -4865,8 +5005,45 @@ var VdFlowchart = class {
4865
5005
  if (this.selection.kind === "node") return this.removeNode(this.selection.id);
4866
5006
  return this.removeEdge(this.selection.id);
4867
5007
  }
5008
+ nodeSizeBounds() {
5009
+ return { min: this.minNodeSize, max: this.maxNodeSize };
5010
+ }
5011
+ scaleBounds() {
5012
+ return { min: this.minScale, max: this.maxScale };
5013
+ }
5014
+ normalizationLimits() {
5015
+ return { scaleBounds: this.scaleBounds(), sizeBounds: this.nodeSizeBounds() };
5016
+ }
5017
+ hasWideCamera() {
5018
+ return this.minScale < DEFAULT_MIN_SCALE || this.maxScale > DEFAULT_MAX_SCALE;
5019
+ }
5020
+ syncGrid() {
5021
+ const wide = this.hasWideCamera();
5022
+ const extent = wide ? WIDE_WORLD_EXTENT : DEFAULT_WORLD_EXTENT;
5023
+ this.gridRect?.setAttribute("x", String(-extent / 2));
5024
+ this.gridRect?.setAttribute("y", String(-extent / 2));
5025
+ this.gridRect?.setAttribute("width", String(extent));
5026
+ this.gridRect?.setAttribute("height", String(extent));
5027
+ const pattern = this.svg?.querySelector("pattern");
5028
+ const path = pattern?.querySelector("path");
5029
+ if (!pattern || !path) return;
5030
+ let tile = this.gridSize;
5031
+ if (wide) {
5032
+ const scale = Math.max(this.minScale, this.documentData?.viewport?.scale || 1);
5033
+ while (tile * scale < 12 && tile < extent / 8) tile *= 4;
5034
+ while (tile * scale > 48 && tile > 1.5) tile /= 4;
5035
+ tile = formatNumber(tile);
5036
+ path.setAttribute("vector-effect", "non-scaling-stroke");
5037
+ } else {
5038
+ path.removeAttribute("vector-effect");
5039
+ }
5040
+ path.setAttribute("stroke-width", "1");
5041
+ pattern.setAttribute("width", String(tile));
5042
+ pattern.setAttribute("height", String(tile));
5043
+ path.setAttribute("d", `M ${tile} 0 L 0 0 0 ${tile}`);
5044
+ }
4868
5045
  setViewport(viewport) {
4869
- this.documentData.viewport = normalizeViewport(viewport);
5046
+ this.documentData.viewport = normalizeViewport(viewport, this.scaleBounds());
4870
5047
  this.render({ inspector: false, json: true });
4871
5048
  this.emitViewportChange("viewport:set");
4872
5049
  return this;
@@ -4884,7 +5061,7 @@ var VdFlowchart = class {
4884
5061
  return this;
4885
5062
  }
4886
5063
  resetView() {
4887
- this.documentData.viewport = normalizeViewport({ x: 0, y: 0, scale: 1 });
5064
+ this.documentData.viewport = normalizeViewport({ x: 0, y: 0, scale: 1 }, this.scaleBounds());
4888
5065
  this.render({ inspector: false, json: true });
4889
5066
  this.emitViewportChange("viewport:reset");
4890
5067
  return this;
@@ -4900,30 +5077,64 @@ var VdFlowchart = class {
4900
5077
  const padding = 80;
4901
5078
  const contentWidth = Math.max(1, bounds.right - bounds.left);
4902
5079
  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
5080
+ const scale = formatScale(
5081
+ clamp(
5082
+ Math.min((width - padding * 2) / contentWidth, (height - padding * 2) / contentHeight),
5083
+ this.minScale,
5084
+ this.maxScale
5085
+ )
4907
5086
  );
4908
5087
  this.documentData.viewport = {
4909
5088
  x: formatNumber(width / 2 - (bounds.left + bounds.right) / 2 * scale),
4910
5089
  y: formatNumber(height / 2 - (bounds.top + bounds.bottom) / 2 * scale),
4911
- scale: formatNumber(scale)
5090
+ scale
4912
5091
  };
4913
5092
  this.render({ inspector: false, json: true });
4914
5093
  this.emitViewportChange("viewport:fit");
4915
5094
  return this;
4916
5095
  }
5096
+ fitNode(nodeId, padding = 48) {
5097
+ const node = this.findNode(nodeId);
5098
+ if (!node || this.isNodeHidden(node.id)) return this;
5099
+ const pad = Math.max(0, toFiniteNumber(padding, 48));
5100
+ const width = this.canvasEl.clientWidth || 800;
5101
+ const height = this.canvasEl.clientHeight || 560;
5102
+ const availableWidth = Math.max(1, width - pad * 2);
5103
+ const availableHeight = Math.max(1, height - pad * 2);
5104
+ const scale = formatScale(
5105
+ clamp(
5106
+ Math.min(
5107
+ availableWidth / Math.max(1, node.width),
5108
+ availableHeight / Math.max(1, node.height)
5109
+ ),
5110
+ this.minScale,
5111
+ this.maxScale
5112
+ )
5113
+ );
5114
+ const centerX = node.x + node.width / 2;
5115
+ const centerY = node.y + node.height / 2;
5116
+ this.documentData.viewport = {
5117
+ x: formatNumber(width / 2 - centerX * scale),
5118
+ y: formatNumber(height / 2 - centerY * scale),
5119
+ scale
5120
+ };
5121
+ this.render({ inspector: false, json: true });
5122
+ this.emitViewportChange("viewport:fit-node");
5123
+ return this;
5124
+ }
4917
5125
  clear() {
4918
5126
  this.stopTextEdit({ commit: false });
4919
5127
  this.activeTool = null;
4920
5128
  this.reconnectEdgeId = null;
4921
5129
  this.clipboard = null;
4922
- this.documentData = normalizeDocument({
4923
- nodes: [],
4924
- edges: [],
4925
- viewport: { x: 0, y: 0, scale: 1 }
4926
- });
5130
+ this.documentData = normalizeDocument(
5131
+ {
5132
+ nodes: [],
5133
+ edges: [],
5134
+ viewport: { x: 0, y: 0, scale: 1 }
5135
+ },
5136
+ this.normalizationLimits()
5137
+ );
4927
5138
  this.selection = null;
4928
5139
  this.render();
4929
5140
  this.emitChange("clear");
@@ -4958,6 +5169,33 @@ var VdFlowchart = class {
4958
5169
  if ("snapGuides" in options) this.snapGuides = options.snapGuides !== false;
4959
5170
  if ("minimap" in options) this.setMinimapEnabled(options.minimap !== false);
4960
5171
  if ("autoLayout" in options) this.autoLayout = Boolean(options.autoLayout);
5172
+ if ("minScale" in options || "maxScale" in options || "minNodeSize" in options || "maxNodeSize" in options) {
5173
+ const nextScale = normalizeBoundPair(
5174
+ "minScale" in options ? options.minScale : this.minScale,
5175
+ "maxScale" in options ? options.maxScale : this.maxScale,
5176
+ DEFAULT_MIN_SCALE,
5177
+ DEFAULT_MAX_SCALE
5178
+ );
5179
+ const nextSize = normalizeBoundPair(
5180
+ "minNodeSize" in options ? options.minNodeSize : this.minNodeSize,
5181
+ "maxNodeSize" in options ? options.maxNodeSize : this.maxNodeSize,
5182
+ DEFAULT_MIN_NODE_SIZE,
5183
+ DEFAULT_MAX_NODE_SIZE
5184
+ );
5185
+ this.minScale = nextScale.min;
5186
+ this.maxScale = nextScale.max;
5187
+ this.minNodeSize = nextSize.min;
5188
+ this.maxNodeSize = nextSize.max;
5189
+ const clamped = clamp(
5190
+ formatScale(this.documentData.viewport.scale),
5191
+ this.minScale,
5192
+ this.maxScale
5193
+ );
5194
+ if (clamped !== this.documentData.viewport.scale) {
5195
+ this.documentData.viewport.scale = clamped;
5196
+ this.pendingBoundsEvent = true;
5197
+ }
5198
+ }
4961
5199
  this.syncCanvasLabel();
4962
5200
  if (this.shortcutsHelpOpen) this.renderShortcutsHelp();
4963
5201
  if ("history" in options && options.history !== false !== this.historyEnabled) {
@@ -4975,11 +5213,15 @@ var VdFlowchart = class {
4975
5213
  }
4976
5214
  }
4977
5215
  this.render();
5216
+ if (this.pendingBoundsEvent) {
5217
+ this.pendingBoundsEvent = false;
5218
+ this.emitViewportChange("viewport:bounds");
5219
+ }
4978
5220
  this.updateHistoryButtons();
4979
5221
  return this;
4980
5222
  }
4981
5223
  load(data, options = {}) {
4982
- const documentData = normalizeDocument(data);
5224
+ const documentData = normalizeDocument(data, this.normalizationLimits());
4983
5225
  if (options.silent && JSON.stringify(documentData) === JSON.stringify(this.documentData)) {
4984
5226
  return this;
4985
5227
  }
@@ -5037,7 +5279,11 @@ var EDITOR_OPTION_KEYS = [
5037
5279
  "keyboardShortcuts",
5038
5280
  "snapGuides",
5039
5281
  "minimap",
5040
- "autoLayout"
5282
+ "autoLayout",
5283
+ "minScale",
5284
+ "maxScale",
5285
+ "minNodeSize",
5286
+ "maxNodeSize"
5041
5287
  ];
5042
5288
  var VdFlowchart2 = defineComponent({
5043
5289
  name: "VdFlowchart",
@@ -5063,13 +5309,29 @@ var VdFlowchart2 = defineComponent({
5063
5309
  /** Show the overview minimap in the canvas corner (default true). */
5064
5310
  minimap: { type: Boolean, default: true },
5065
5311
  /** Re-run the current layout after keyboard or handle insertions (default false). */
5066
- autoLayout: { type: Boolean, default: false }
5312
+ autoLayout: { type: Boolean, default: false },
5313
+ /** Lowest camera scale. Defaults to 0.35. Set it together with maxScale. */
5314
+ minScale: { type: Number, default: void 0 },
5315
+ /** Highest camera scale. Defaults to 3. Set it together with minScale. */
5316
+ maxScale: { type: Number, default: void 0 },
5317
+ /** Smallest resizable-node side. Defaults to 56. Set it together with maxNodeSize. */
5318
+ minNodeSize: { type: Number, default: void 0 },
5319
+ /** Largest resizable-node side. Defaults to 420. Set it together with minNodeSize. */
5320
+ maxNodeSize: { type: Number, default: void 0 }
5067
5321
  },
5068
5322
  emits: ["change", "select", "viewport", "connect", "ready"],
5069
5323
  setup(props, { emit, expose }) {
5070
5324
  const el = ref(null);
5071
5325
  let instance = null;
5072
- const editorOptions = () => Object.fromEntries(EDITOR_OPTION_KEYS.map((key) => [key, props[key]]));
5326
+ const editorOptions = () => Object.fromEntries(
5327
+ EDITOR_OPTION_KEYS.flatMap((key) => {
5328
+ const value = props[key];
5329
+ if ((key === "minScale" || key === "maxScale" || key === "minNodeSize" || key === "maxNodeSize") && value == null) {
5330
+ return [];
5331
+ }
5332
+ return [[key, value]];
5333
+ })
5334
+ );
5073
5335
  const create = () => {
5074
5336
  instance = new VdFlowchart({
5075
5337
  element: el.value,
@@ -5111,7 +5373,8 @@ var VdFlowchart2 = defineComponent({
5111
5373
  redo: () => instance?.redo(),
5112
5374
  canUndo: () => Boolean(instance?.canUndo()),
5113
5375
  canRedo: () => Boolean(instance?.canRedo()),
5114
- layout: (mode, options) => instance?.layout(mode, options)
5376
+ layout: (mode, options) => instance?.layout(mode, options),
5377
+ fitNode: (nodeId, padding) => instance?.fitNode(nodeId, padding)
5115
5378
  });
5116
5379
  return () => h("div", { ref: el, class: "vd-flowchart" });
5117
5380
  }