@vanduo-oss/vd3-cbun 1.4.0 → 1.4.2

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.
@@ -257,6 +257,37 @@ function simplifyPoints(points, min = 1.2) {
257
257
  }
258
258
  return out;
259
259
  }
260
+ function smoothPoint(prev, next, factor = 0.5) {
261
+ const f = clamp(factor, 0.1, 1);
262
+ const x = round(prev[0] + (next[0] - prev[0]) * f);
263
+ const y = round(prev[1] + (next[1] - prev[1]) * f);
264
+ const hasPressure = prev.length >= 3 && prev[2] != null || next.length >= 3 && next[2] != null;
265
+ if (hasPressure) {
266
+ const pp = prev.length >= 3 && prev[2] != null ? prev[2] : 0.5;
267
+ const np = next.length >= 3 && next[2] != null ? next[2] : 0.5;
268
+ return [x, y, clamp(pp + (np - pp) * f, 0, 1)];
269
+ }
270
+ return [x, y];
271
+ }
272
+ function appendAndSimplify(points, point, minDist = 1.5, maxLen = 500) {
273
+ if (!points.length) {
274
+ points.push(roundPoint(point));
275
+ return points;
276
+ }
277
+ const last = points[points.length - 1];
278
+ if (Math.hypot(point[0] - last[0], point[1] - last[1]) < minDist) return points;
279
+ points.push(roundPoint(point));
280
+ if (points.length > maxLen) {
281
+ const first = points[0];
282
+ const end = points[points.length - 1];
283
+ const kept = [first];
284
+ for (let i = 2; i < points.length - 1; i += 2) kept.push(points[i]);
285
+ kept.push(end);
286
+ points.length = 0;
287
+ for (const p of kept) points.push(p);
288
+ }
289
+ return points;
290
+ }
260
291
  function streamlinePoints(points, streamline) {
261
292
  const first = points[0];
262
293
  const out = [[first[0], first[1], first[2] == null ? 0.5 : clamp(first[2], 0, 1)]];
@@ -471,6 +502,55 @@ function normalizeDocument(data) {
471
502
  shapes
472
503
  };
473
504
  }
505
+ function wrapText(text, maxWidth, charWidth = 8.8) {
506
+ if (typeof text !== "string" || text.length === 0) return [];
507
+ const rawLines = text.split(/\r?\n/);
508
+ if (!maxWidth || maxWidth <= 0 || !Number.isFinite(maxWidth)) {
509
+ return rawLines;
510
+ }
511
+ const maxChars = Math.max(1, Math.floor(maxWidth / charWidth));
512
+ const result = [];
513
+ for (const rawLine of rawLines) {
514
+ if (rawLine.length <= maxChars) {
515
+ result.push(rawLine);
516
+ continue;
517
+ }
518
+ const words = rawLine.split(" ");
519
+ let currentLine = "";
520
+ for (const word of words) {
521
+ if (!currentLine) {
522
+ if (word.length <= maxChars) {
523
+ currentLine = word;
524
+ } else {
525
+ let remaining = word;
526
+ while (remaining.length > maxChars) {
527
+ result.push(remaining.slice(0, maxChars));
528
+ remaining = remaining.slice(maxChars);
529
+ }
530
+ currentLine = remaining;
531
+ }
532
+ } else {
533
+ if (currentLine.length + 1 + word.length <= maxChars) {
534
+ currentLine += " " + word;
535
+ } else {
536
+ result.push(currentLine);
537
+ if (word.length <= maxChars) {
538
+ currentLine = word;
539
+ } else {
540
+ let remaining = word;
541
+ while (remaining.length > maxChars) {
542
+ result.push(remaining.slice(0, maxChars));
543
+ remaining = remaining.slice(maxChars);
544
+ }
545
+ currentLine = remaining;
546
+ }
547
+ }
548
+ }
549
+ }
550
+ if (currentLine) result.push(currentLine);
551
+ }
552
+ return result;
553
+ }
474
554
 
475
555
  // src/draw/core.js
476
556
  var SVG_NS = "http://www.w3.org/2000/svg";
@@ -596,6 +676,15 @@ var VdDraw = class {
596
676
  this.lastReason = null;
597
677
  this.lastTargetKey = null;
598
678
  this.listeners = /* @__PURE__ */ new Map();
679
+ this.options = { ...opts };
680
+ this._shapesById = /* @__PURE__ */ new Map();
681
+ this._rebuildShapeIndex();
682
+ this._activePointers = /* @__PURE__ */ new Map();
683
+ this._shapeElements = /* @__PURE__ */ new Map();
684
+ this._dirtyShapes = /* @__PURE__ */ new Set();
685
+ this._allDirty = true;
686
+ this._rafId = null;
687
+ this._pendingFlags = null;
599
688
  this._buildShell();
600
689
  if (typeof opts.color !== "string") this.style.color = this._resolveInk();
601
690
  this._bindEvents();
@@ -604,6 +693,12 @@ var VdDraw = class {
604
693
  this._syncStylePanel();
605
694
  this._scheduleReady();
606
695
  }
696
+ _rebuildShapeIndex() {
697
+ this._shapesById.clear();
698
+ for (const s of this.documentData.shapes) {
699
+ this._shapesById.set(s.id, s);
700
+ }
701
+ }
607
702
  _resolveElement(target) {
608
703
  if (!target) return null;
609
704
  if (typeof target === "string") return hasWindow() ? document.querySelector(target) : null;
@@ -637,11 +732,13 @@ var VdDraw = class {
637
732
  }
638
733
  this.canvasEl = document.createElement("div");
639
734
  this.canvasEl.className = "vd-draw-canvas";
735
+ this.canvasEl.setAttribute("data-tool", this.tool);
640
736
  this.canvasEl.tabIndex = 0;
641
737
  this.svg = createSvgEl("svg", { class: "vd-draw-svg" });
642
738
  this.svg.setAttribute("width", "100%");
643
739
  this.svg.setAttribute("height", "100%");
644
740
  const defs = createSvgEl("defs");
741
+ this.defsEl = defs;
645
742
  const gs = this.gridSize;
646
743
  this.gridPattern = createSvgEl("pattern", {
647
744
  id: this._svgId("grid"),
@@ -657,19 +754,7 @@ var VdDraw = class {
657
754
  });
658
755
  this.gridPattern.appendChild(this.gridPatternPath);
659
756
  defs.appendChild(this.gridPattern);
660
- const marker = createSvgEl("marker", {
661
- id: this._svgId("arrow"),
662
- viewBox: "0 0 10 10",
663
- refX: 8,
664
- refY: 5,
665
- markerWidth: 7,
666
- markerHeight: 7,
667
- orient: "auto-start-reverse"
668
- });
669
- marker.appendChild(
670
- createSvgEl("path", { d: "M 0 0 L 10 5 L 0 10 z", fill: "var(--vd-draw-shape-stroke)" })
671
- );
672
- defs.appendChild(marker);
757
+ this._getArrowMarkerId("", defs);
673
758
  this.svg.appendChild(defs);
674
759
  this.world = createSvgEl("g", { class: "vd-draw-world" });
675
760
  this.gridLayer = createSvgEl("rect", {
@@ -960,11 +1045,11 @@ var VdDraw = class {
960
1045
  this._emitViewportChange("viewport:reset");
961
1046
  return this;
962
1047
  }
963
- fitView() {
1048
+ fitView(padding = 40) {
964
1049
  const bounds = boundsOfShapes(this.documentData.shapes);
965
1050
  const rect = this.canvasEl.getBoundingClientRect();
966
1051
  if (!bounds || bounds.w === 0 || bounds.h === 0 || !rect.width || !rect.height) return this;
967
- const pad = 40;
1052
+ const pad = Number.isFinite(padding) ? Math.max(0, padding) : 40;
968
1053
  const scale = clamp(
969
1054
  Math.min((rect.width - pad * 2) / bounds.w, (rect.height - pad * 2) / bounds.h),
970
1055
  MIN_SCALE,
@@ -1006,10 +1091,65 @@ var VdDraw = class {
1006
1091
  toggleGrid() {
1007
1092
  return this.setGridVisible(!this.showGrid);
1008
1093
  }
1094
+ // ── Runtime options ──────────────────────────────────────────────────────
1095
+ setReadonly(readonly) {
1096
+ const next = Boolean(readonly);
1097
+ if (this.readonly === next) return this;
1098
+ this.readonly = next;
1099
+ this.options.readonly = next;
1100
+ if (this.element) {
1101
+ if (next) this.element.setAttribute("data-readonly", "true");
1102
+ else this.element.removeAttribute("data-readonly");
1103
+ }
1104
+ if (next) {
1105
+ this.stopTextEdit({ commit: true });
1106
+ this.deselect();
1107
+ if (this.toolbarEl) this.toolbarEl.style.display = "none";
1108
+ if (this.panelEl) this.panelEl.style.display = "none";
1109
+ } else {
1110
+ if (this.toolbarEl && !this.toolbarEl.children.length) {
1111
+ this._buildToolbar();
1112
+ }
1113
+ if (this.panelEl && !this.panelEl.children.length) {
1114
+ this._buildStylePanel();
1115
+ }
1116
+ if (this.toolbarEl) this.toolbarEl.style.display = "";
1117
+ if (this.panelEl) this.panelEl.style.display = "";
1118
+ this._syncToolbar();
1119
+ this._syncStylePanel();
1120
+ }
1121
+ this.render({ scene: false, overlay: true });
1122
+ return this;
1123
+ }
1124
+ setSnap(snap) {
1125
+ this.snap = Boolean(snap);
1126
+ this.options.snap = this.snap;
1127
+ return this;
1128
+ }
1129
+ setHistoryEnabled(enabled) {
1130
+ this.historyEnabled = Boolean(enabled);
1131
+ this.options.history = this.historyEnabled;
1132
+ if (!this.historyEnabled) {
1133
+ this.clearHistory();
1134
+ }
1135
+ return this;
1136
+ }
1137
+ setHistoryLimit(limit) {
1138
+ const num = Number(limit);
1139
+ this.historyLimit = Number.isFinite(num) && num > 0 ? num : 100;
1140
+ this.options.historyLimit = this.historyLimit;
1141
+ if (this.history.length > this.historyLimit + 1) {
1142
+ const drop = this.history.length - (this.historyLimit + 1);
1143
+ this.history.splice(0, drop);
1144
+ this.historyIndex = Math.max(0, this.historyIndex - drop);
1145
+ }
1146
+ return this;
1147
+ }
1009
1148
  // ── Tool + current style ─────────────────────────────────────────────────
1010
1149
  setTool(tool) {
1011
1150
  if (!DRAW_TOOLS.includes(tool)) return this;
1012
1151
  this.tool = tool;
1152
+ if (this.canvasEl) this.canvasEl.setAttribute("data-tool", tool);
1013
1153
  this._syncToolbar();
1014
1154
  return this;
1015
1155
  }
@@ -1043,7 +1183,7 @@ var VdDraw = class {
1043
1183
  }
1044
1184
  // ── Shape CRUD ─────────────────────────────────────────────────────────
1045
1185
  getShape(id) {
1046
- return this.documentData.shapes.find((s) => s.id === id) || null;
1186
+ return this._shapesById.get(id) || null;
1047
1187
  }
1048
1188
  getShapes() {
1049
1189
  return this.documentData.shapes.map((s) => deepClone(s));
@@ -1095,6 +1235,8 @@ var VdDraw = class {
1095
1235
  shape.text = typeof partial.text === "string" ? partial.text : "";
1096
1236
  }
1097
1237
  this.documentData.shapes.push(shape);
1238
+ this._shapesById.set(shape.id, shape);
1239
+ this._markDirty(shape.id);
1098
1240
  this.render();
1099
1241
  this._emitChange("shape:add", { shape: deepClone(shape), shapeId: shape.id });
1100
1242
  return shape;
@@ -1108,6 +1250,8 @@ var VdDraw = class {
1108
1250
  const shape = this.getShape(id);
1109
1251
  if (!shape || !patch) return null;
1110
1252
  Object.assign(shape, patch);
1253
+ this._shapesById.set(id, shape);
1254
+ this._markDirty(id);
1111
1255
  this.render();
1112
1256
  this._emitChange(options.reason || "shape:update", { shapeId: id }, options.reason ? id : null);
1113
1257
  return shape;
@@ -1116,7 +1260,9 @@ var VdDraw = class {
1116
1260
  const idx = this.documentData.shapes.findIndex((s) => s.id === id);
1117
1261
  if (idx === -1) return false;
1118
1262
  this.documentData.shapes.splice(idx, 1);
1263
+ this._shapesById.delete(id);
1119
1264
  this.selectedIds.delete(id);
1265
+ this._markDirty(id);
1120
1266
  this.render();
1121
1267
  this._emitChange("shape:delete", { shapeId: id });
1122
1268
  return true;
@@ -1183,11 +1329,13 @@ var VdDraw = class {
1183
1329
  _replaceShape(next) {
1184
1330
  const idx = this.documentData.shapes.findIndex((s) => s.id === next.id);
1185
1331
  if (idx !== -1) this.documentData.shapes[idx] = next;
1332
+ this._shapesById.set(next.id, next);
1186
1333
  }
1187
1334
  nudge(dx, dy) {
1188
1335
  const selected = this.getSelectedShapes();
1189
1336
  if (!selected.length) return this;
1190
1337
  for (const shape of selected) this._replaceShape(translateShape(shape, dx, dy));
1338
+ this._markAllDirty();
1191
1339
  this.render();
1192
1340
  this._emitChange(
1193
1341
  "shape:nudge",
@@ -1206,6 +1354,7 @@ var VdDraw = class {
1206
1354
  const scaled = scaleShape(shape, cur.x, cur.y, sx, sy);
1207
1355
  this._replaceShape(translateShape(scaled, target.x - cur.x, target.y - cur.y));
1208
1356
  }
1357
+ this._markAllDirty();
1209
1358
  this.render();
1210
1359
  this._emitChange("shape:resize", { shapeIds: [...this.selectedIds] });
1211
1360
  return this;
@@ -1213,8 +1362,10 @@ var VdDraw = class {
1213
1362
  deleteSelection() {
1214
1363
  if (this.selectedIds.size === 0) return false;
1215
1364
  const ids = [...this.selectedIds];
1365
+ for (const id of ids) this._shapesById.delete(id);
1216
1366
  this.documentData.shapes = this.documentData.shapes.filter((s) => !this.selectedIds.has(s.id));
1217
1367
  this.selectedIds.clear();
1368
+ this._markAllDirty();
1218
1369
  this.render();
1219
1370
  this._emitChange("shape:delete", { shapeIds: ids });
1220
1371
  return true;
@@ -1226,6 +1377,7 @@ var VdDraw = class {
1226
1377
  for (const shape of selected) {
1227
1378
  for (const key of allowed) if (key in patch) shape[key] = patch[key];
1228
1379
  }
1380
+ this._markAllDirty();
1229
1381
  this.render();
1230
1382
  this._emitChange(
1231
1383
  "shape:style",
@@ -1238,6 +1390,7 @@ var VdDraw = class {
1238
1390
  _reorder(mutator) {
1239
1391
  if (this.selectedIds.size === 0) return this;
1240
1392
  mutator();
1393
+ this._markAllDirty();
1241
1394
  this.render();
1242
1395
  this._emitChange("shape:reorder", { shapeIds: [...this.selectedIds] });
1243
1396
  return this;
@@ -1280,6 +1433,7 @@ var VdDraw = class {
1280
1433
  if (selected.length < 2) return this;
1281
1434
  const groupId = createId("grp");
1282
1435
  for (const shape of selected) shape.groupId = groupId;
1436
+ this._markAllDirty();
1283
1437
  this.render();
1284
1438
  this._emitChange("shape:group", { groupId, shapeIds: [...this.selectedIds] });
1285
1439
  return this;
@@ -1294,6 +1448,7 @@ var VdDraw = class {
1294
1448
  }
1295
1449
  }
1296
1450
  if (!changed) return this;
1451
+ this._markAllDirty();
1297
1452
  this.render();
1298
1453
  this._emitChange("shape:ungroup", { shapeIds: [...this.selectedIds] });
1299
1454
  return this;
@@ -1320,9 +1475,11 @@ var VdDraw = class {
1320
1475
  copy.groupId = groupRemap.get(copy.groupId);
1321
1476
  }
1322
1477
  this.documentData.shapes.push(copy);
1478
+ this._shapesById.set(copy.id, copy);
1323
1479
  newIds.push(copy.id);
1324
1480
  }
1325
1481
  this.selectedIds = new Set(newIds);
1482
+ this._markAllDirty();
1326
1483
  this.render();
1327
1484
  this._emitChange("shape:paste", { shapeIds: newIds });
1328
1485
  this._emitSelect();
@@ -1384,6 +1541,30 @@ var VdDraw = class {
1384
1541
  sticky: read("--vd-draw-sticky-fill", "#fdf3c4")
1385
1542
  };
1386
1543
  }
1544
+ _getArrowMarkerId(color, defsTarget = this.defsEl) {
1545
+ const isDefault = !color;
1546
+ const safeKey = isDefault ? "arrow" : "arrow-" + color.toLowerCase().replace(/[^a-z0-9_-]/g, "_");
1547
+ const id = this._svgId(safeKey);
1548
+ if (defsTarget && !defsTarget.querySelector(`#${id}`)) {
1549
+ const marker = createSvgEl("marker", {
1550
+ id,
1551
+ viewBox: "0 0 10 10",
1552
+ refX: 8,
1553
+ refY: 5,
1554
+ markerWidth: 7,
1555
+ markerHeight: 7,
1556
+ orient: "auto-start-reverse"
1557
+ });
1558
+ marker.appendChild(
1559
+ createSvgEl("path", {
1560
+ d: "M 0 0 L 10 5 L 0 10 z",
1561
+ fill: color || "var(--vd-draw-shape-stroke)"
1562
+ })
1563
+ );
1564
+ defsTarget.appendChild(marker);
1565
+ }
1566
+ return id;
1567
+ }
1387
1568
  toSVG() {
1388
1569
  const shapes = this.documentData.shapes;
1389
1570
  const bounds = boundsOfShapes(shapes) || { x: 0, y: 0, w: 100, h: 100 };
@@ -1401,10 +1582,14 @@ var VdDraw = class {
1401
1582
  height: round(vb.h),
1402
1583
  viewBox: `${round(vb.x)} ${round(vb.y)} ${round(vb.w)} ${round(vb.h)}`
1403
1584
  });
1585
+ const defs = createSvgEl("defs");
1404
1586
  for (const shape of shapes) {
1405
- const el = this._renderShapeEl(shape, { standalone: true, colors });
1587
+ const el = this._renderShapeEl(shape, { standalone: true, colors, defsTarget: defs });
1406
1588
  if (el) svg.appendChild(el);
1407
1589
  }
1590
+ if (defs.childNodes.length > 0) {
1591
+ svg.insertBefore(defs, svg.firstChild);
1592
+ }
1408
1593
  return new XMLSerializer().serializeToString(svg);
1409
1594
  }
1410
1595
  toPNG({ scale = 2 } = {}) {
@@ -1498,7 +1683,9 @@ var VdDraw = class {
1498
1683
  const viewport = { ...this.documentData.viewport };
1499
1684
  this.documentData = deepClone(snapshot);
1500
1685
  this.documentData.viewport = viewport;
1686
+ this._rebuildShapeIndex();
1501
1687
  this._syncSelectionValidity();
1688
+ this._markAllDirty();
1502
1689
  this.render();
1503
1690
  this.emit("change", { reason, document: this.toJSON() });
1504
1691
  this.emit("history", { reason, canUndo: this.canUndo(), canRedo: this.canRedo() });
@@ -1514,8 +1701,10 @@ var VdDraw = class {
1514
1701
  }
1515
1702
  load(data) {
1516
1703
  this.documentData = normalizeDocument(data);
1704
+ this._rebuildShapeIndex();
1517
1705
  this.selectedIds.clear();
1518
1706
  this._resetHistory();
1707
+ this._markAllDirty();
1519
1708
  this.render();
1520
1709
  this._emitChange("load");
1521
1710
  this._emitSelect();
@@ -1524,7 +1713,9 @@ var VdDraw = class {
1524
1713
  clear() {
1525
1714
  if (!this.documentData.shapes.length) return this;
1526
1715
  this.documentData.shapes = [];
1716
+ this._shapesById.clear();
1527
1717
  this.selectedIds.clear();
1718
+ this._markAllDirty();
1528
1719
  this.render();
1529
1720
  this._emitChange("clear");
1530
1721
  return this;
@@ -1549,10 +1740,16 @@ var VdDraw = class {
1549
1740
  }
1550
1741
  _positionTextEditor(shape, editor) {
1551
1742
  const vp = this.documentData.viewport;
1552
- editor.style.left = `${vp.x + shape.x * vp.scale}px`;
1553
- editor.style.top = `${vp.y + shape.y * vp.scale}px`;
1554
- editor.style.width = `${shape.w * vp.scale}px`;
1555
- editor.style.height = `${shape.h * vp.scale}px`;
1743
+ const scale = vp.scale || 1;
1744
+ editor.style.left = `${vp.x + shape.x * scale}px`;
1745
+ editor.style.top = `${vp.y + shape.y * scale}px`;
1746
+ editor.style.width = `${shape.w * scale}px`;
1747
+ editor.style.height = `${shape.h * scale}px`;
1748
+ editor.style.fontSize = `${16 * scale}px`;
1749
+ editor.style.lineHeight = `${20 * scale}px`;
1750
+ if (shape.type === "sticky") {
1751
+ editor.style.background = shape.fill || "var(--vd-draw-sticky-fill)";
1752
+ }
1556
1753
  }
1557
1754
  stopTextEdit({ commit = true } = {}) {
1558
1755
  if (!this.textEditor) return;
@@ -1564,6 +1761,7 @@ var VdDraw = class {
1564
1761
  const shape = this.getShape(id);
1565
1762
  if (shape && shape.text !== value) {
1566
1763
  shape.text = value;
1764
+ this._markDirty(id);
1567
1765
  this.render();
1568
1766
  this._emitChange("shape:text", { shapeId: id }, `text:${id}`);
1569
1767
  }
@@ -1621,11 +1819,65 @@ var VdDraw = class {
1621
1819
  if (this.destroyed || event.button != null && event.button !== 0) return;
1622
1820
  this.canvasEl.focus();
1623
1821
  this.stopTextEdit({ commit: true });
1822
+ this._activePointers.set(event.pointerId, { clientX: event.clientX, clientY: event.clientY });
1823
+ if (this._activePointers.size === 2) {
1824
+ if (this.interaction) {
1825
+ const it = this.interaction;
1826
+ if (it.kind === "freehand" || it.kind === "create-line" || it.kind === "create-box") {
1827
+ this.documentData.shapes = this.documentData.shapes.filter((s) => s.id !== it.shapeId);
1828
+ this._shapesById.delete(it.shapeId);
1829
+ const el = this._shapeElements.get(it.shapeId);
1830
+ if (el) {
1831
+ el.remove();
1832
+ this._shapeElements.delete(it.shapeId);
1833
+ }
1834
+ this._scheduleRender({ scene: true, overlay: false });
1835
+ } else if (it.kind === "move" || it.kind === "resize") {
1836
+ for (const orig of it.originals) {
1837
+ this._replaceShape(orig);
1838
+ this._markDirty(orig.id);
1839
+ }
1840
+ if (it.kind === "move") this._renderGuides([]);
1841
+ this._scheduleRender({ scene: true });
1842
+ } else if (it.kind === "erase") {
1843
+ for (const id of it.erased) this._markDirty(id);
1844
+ this._scheduleRender({ scene: true, overlay: false });
1845
+ } else if (it.kind === "pan") {
1846
+ if (this.canvasEl) this.canvasEl.classList.remove("vd-draw-panning");
1847
+ } else if (it.kind === "marquee") {
1848
+ clearChildren(this.marqueeLayer);
1849
+ }
1850
+ }
1851
+ const pts = [...this._activePointers.values()];
1852
+ const p1 = pts[0];
1853
+ const p2 = pts[1];
1854
+ const initialDistance = Math.hypot(p2.clientX - p1.clientX, p2.clientY - p1.clientY) || 1;
1855
+ const initialClientMid = {
1856
+ x: (p1.clientX + p2.clientX) / 2,
1857
+ y: (p1.clientY + p2.clientY) / 2
1858
+ };
1859
+ const initialLocalMid = this._clientToLocal(initialClientMid.x, initialClientMid.y);
1860
+ const initialVp = { ...this.documentData.viewport };
1861
+ const currentScale = initialVp.scale || 1;
1862
+ const worldMid = {
1863
+ x: (initialLocalMid.x - initialVp.x) / currentScale,
1864
+ y: (initialLocalMid.y - initialVp.y) / currentScale
1865
+ };
1866
+ this.interaction = {
1867
+ kind: "pinch",
1868
+ initialDistance,
1869
+ worldMid,
1870
+ initialScale: currentScale
1871
+ };
1872
+ if (typeof event.preventDefault === "function") event.preventDefault();
1873
+ return;
1874
+ }
1624
1875
  const world = this._clientToWorld(event.clientX, event.clientY);
1625
1876
  const shapeTarget = event.target.closest("[data-shape-id]");
1626
1877
  const handleTarget = event.target.closest("[data-handle]");
1627
1878
  if (this.tool === "hand") {
1628
1879
  this.interaction = this._beginPan(event);
1880
+ if (this.canvasEl) this.canvasEl.classList.add("vd-draw-panning");
1629
1881
  this._capture(event.pointerId);
1630
1882
  return;
1631
1883
  }
@@ -1636,6 +1888,15 @@ var VdDraw = class {
1636
1888
  return;
1637
1889
  }
1638
1890
  if (this.tool === "select") {
1891
+ if (event.detail === 2 && shapeTarget && !this.readonly) {
1892
+ const id = shapeTarget.getAttribute("data-shape-id");
1893
+ const shape = this.getShape(id);
1894
+ if (shape && (shape.type === "text" || shape.type === "sticky")) {
1895
+ this.select(id);
1896
+ this.startTextEdit(id);
1897
+ return;
1898
+ }
1899
+ }
1639
1900
  if (handleTarget && this.selectedIds.size) {
1640
1901
  this.interaction = {
1641
1902
  kind: "resize",
@@ -1699,6 +1960,7 @@ var VdDraw = class {
1699
1960
  points: [[round(world.x), round(world.y), pressure]]
1700
1961
  };
1701
1962
  this.documentData.shapes.push(shape2);
1963
+ this._shapesById.set(shape2.id, shape2);
1702
1964
  return { kind: "freehand", pointerId: event.pointerId, shapeId: shape2.id };
1703
1965
  }
1704
1966
  if (tool === "line") {
@@ -1716,6 +1978,7 @@ var VdDraw = class {
1716
1978
  opacity: this.style.opacity
1717
1979
  };
1718
1980
  this.documentData.shapes.push(shape2);
1981
+ this._shapesById.set(shape2.id, shape2);
1719
1982
  return { kind: "create-line", pointerId: event.pointerId, shapeId: shape2.id };
1720
1983
  }
1721
1984
  const type = tool;
@@ -1733,6 +1996,7 @@ var VdDraw = class {
1733
1996
  };
1734
1997
  if (type === "text" || type === "sticky") shape.text = "";
1735
1998
  this.documentData.shapes.push(shape);
1999
+ this._shapesById.set(shape.id, shape);
1736
2000
  return { kind: "create-box", pointerId: event.pointerId, shapeId: shape.id, start: world };
1737
2001
  }
1738
2002
  _applyErase(world) {
@@ -1747,16 +2011,44 @@ var VdDraw = class {
1747
2011
  changed = true;
1748
2012
  }
1749
2013
  }
1750
- if (changed) this.render({ scene: true, overlay: false });
2014
+ if (changed) {
2015
+ for (const id of it.erased) this._markDirty(id);
2016
+ this._scheduleRender({ scene: true, overlay: false });
2017
+ }
1751
2018
  }
1752
2019
  _handlePointerMove(event) {
2020
+ if (this._activePointers.has(event.pointerId)) {
2021
+ this._activePointers.set(event.pointerId, { clientX: event.clientX, clientY: event.clientY });
2022
+ }
1753
2023
  const it = this.interaction;
1754
2024
  if (!it) return;
2025
+ if (it.kind === "pinch") {
2026
+ if (this._activePointers.size >= 2) {
2027
+ if (typeof event.preventDefault === "function") event.preventDefault();
2028
+ const pts = [...this._activePointers.values()];
2029
+ const p1 = pts[0];
2030
+ const p2 = pts[1];
2031
+ const curDistance = Math.hypot(p2.clientX - p1.clientX, p2.clientY - p1.clientY) || 1;
2032
+ const curClientMid = {
2033
+ x: (p1.clientX + p2.clientX) / 2,
2034
+ y: (p1.clientY + p2.clientY) / 2
2035
+ };
2036
+ const curLocalMid = this._clientToLocal(curClientMid.x, curClientMid.y);
2037
+ const distRatio = curDistance / it.initialDistance;
2038
+ const newScale = clamp(it.initialScale * distRatio, MIN_SCALE, MAX_SCALE);
2039
+ const vp = this.documentData.viewport;
2040
+ vp.scale = newScale;
2041
+ vp.x = curLocalMid.x - it.worldMid.x * newScale;
2042
+ vp.y = curLocalMid.y - it.worldMid.y * newScale;
2043
+ this._scheduleRender({ scene: false });
2044
+ }
2045
+ return;
2046
+ }
1755
2047
  if (it.kind === "pan") {
1756
2048
  const vp = this.documentData.viewport;
1757
2049
  vp.x = it.startX + (event.clientX - it.startClientX);
1758
2050
  vp.y = it.startY + (event.clientY - it.startClientY);
1759
- this.render({ scene: false });
2051
+ this._scheduleRender({ scene: false });
1760
2052
  return;
1761
2053
  }
1762
2054
  const world = this._clientToWorld(event.clientX, event.clientY);
@@ -1774,8 +2066,9 @@ var VdDraw = class {
1774
2066
  if (snap.dx || snap.dy)
1775
2067
  for (const orig of it.originals)
1776
2068
  this._replaceShape(translateShape(orig, dx + snap.dx, dy + snap.dy));
2069
+ for (const orig of it.originals) this._markDirty(orig.id);
1777
2070
  this._renderGuides(snap.guides);
1778
- this.render({ scene: true, guides: false });
2071
+ this._scheduleRender({ scene: true, guides: false });
1779
2072
  return;
1780
2073
  }
1781
2074
  if (it.kind === "resize") {
@@ -1790,9 +2083,10 @@ var VdDraw = class {
1790
2083
  for (const orig of it.originals) {
1791
2084
  const scaled = scaleShape(orig, it.startBounds.x, it.startBounds.y, sx, sy);
1792
2085
  this._replaceShape(translateShape(scaled, t.x - it.startBounds.x, t.y - it.startBounds.y));
2086
+ this._markDirty(orig.id);
1793
2087
  }
1794
2088
  it.moved = true;
1795
- this.render();
2089
+ this._scheduleRender();
1796
2090
  return;
1797
2091
  }
1798
2092
  if (it.kind === "marquee") {
@@ -1803,8 +2097,14 @@ var VdDraw = class {
1803
2097
  if (it.kind === "freehand") {
1804
2098
  const shape = this.getShape(it.shapeId);
1805
2099
  if (shape) {
1806
- shape.points.push([round(world.x), round(world.y), event.pressure || 0.5]);
1807
- this.render({ scene: true });
2100
+ const raw = [round(world.x), round(world.y), event.pressure || 0.5];
2101
+ const preset = BRUSH_PRESETS[shape.brush] || BRUSH_PRESETS[DEFAULT_BRUSH];
2102
+ const smoothFactor = 1 - (preset.smoothing || 0.5) * 0.6;
2103
+ const prev = shape.points[shape.points.length - 1];
2104
+ const pt = prev ? smoothPoint(prev, raw, smoothFactor) : raw;
2105
+ appendAndSimplify(shape.points, pt);
2106
+ this._markDirty(it.shapeId);
2107
+ this._scheduleRender({ scene: true });
1808
2108
  }
1809
2109
  return;
1810
2110
  }
@@ -1812,7 +2112,8 @@ var VdDraw = class {
1812
2112
  const shape = this.getShape(it.shapeId);
1813
2113
  if (shape) {
1814
2114
  shape.points[1] = [round(world.x), round(world.y)];
1815
- this.render({ scene: true });
2115
+ this._markDirty(it.shapeId);
2116
+ this._scheduleRender({ scene: true });
1816
2117
  }
1817
2118
  return;
1818
2119
  }
@@ -1823,7 +2124,8 @@ var VdDraw = class {
1823
2124
  shape.y = round(Math.min(it.start.y, world.y));
1824
2125
  shape.w = round(Math.max(1, Math.abs(world.x - it.start.x)));
1825
2126
  shape.h = round(Math.max(1, Math.abs(world.y - it.start.y)));
1826
- this.render({ scene: true });
2127
+ this._markDirty(it.shapeId);
2128
+ this._scheduleRender({ scene: true });
1827
2129
  }
1828
2130
  }
1829
2131
  }
@@ -1842,9 +2144,19 @@ var VdDraw = class {
1842
2144
  return { x, y, w, h: h2 };
1843
2145
  }
1844
2146
  _handlePointerUp(event) {
2147
+ this._activePointers.delete(event.pointerId);
1845
2148
  const it = this.interaction;
1846
2149
  if (!it) return;
2150
+ if (it.kind === "pinch") {
2151
+ if (this._activePointers.size < 2) {
2152
+ this.interaction = null;
2153
+ if (this.canvasEl) this.canvasEl.classList.remove("vd-draw-panning");
2154
+ this._emitViewportChange("viewport:pinch");
2155
+ }
2156
+ return;
2157
+ }
1847
2158
  this.interaction = null;
2159
+ if (this.canvasEl) this.canvasEl.classList.remove("vd-draw-panning");
1848
2160
  if (typeof this.canvasEl.releasePointerCapture === "function" && this.canvasEl.hasPointerCapture?.(event.pointerId)) {
1849
2161
  try {
1850
2162
  this.canvasEl.releasePointerCapture(event.pointerId);
@@ -1860,18 +2172,22 @@ var VdDraw = class {
1860
2172
  if (it.kind === "erase") {
1861
2173
  if (it.erased.size) {
1862
2174
  const ids = [...it.erased];
2175
+ for (const id of ids) this._shapesById.delete(id);
1863
2176
  this.documentData.shapes = this.documentData.shapes.filter((s) => !it.erased.has(s.id));
2177
+ this._markAllDirty();
1864
2178
  this.render();
1865
2179
  this._emitChange("shape:erase", { shapeIds: ids });
1866
2180
  }
1867
2181
  return;
1868
2182
  }
1869
2183
  if (it.kind === "move" && it.moved) {
2184
+ this._markAllDirty();
1870
2185
  this.render();
1871
2186
  this._emitChange("shape:move", { shapeIds: [...this.selectedIds] });
1872
2187
  return;
1873
2188
  }
1874
2189
  if (it.kind === "resize" && it.moved) {
2190
+ this._markAllDirty();
1875
2191
  this.render();
1876
2192
  this._emitChange("shape:resize", { shapeIds: [...this.selectedIds] });
1877
2193
  return;
@@ -1888,6 +2204,8 @@ var VdDraw = class {
1888
2204
  const b = shapeBounds(shape);
1889
2205
  if (it.kind !== "freehand" && b.w < 2 && b.h < 2 && shape.type !== "text" && shape.type !== "sticky") {
1890
2206
  this.documentData.shapes = this.documentData.shapes.filter((s) => s.id !== it.shapeId);
2207
+ this._shapesById.delete(it.shapeId);
2208
+ this._markAllDirty();
1891
2209
  this.render();
1892
2210
  return;
1893
2211
  }
@@ -1901,6 +2219,7 @@ var VdDraw = class {
1901
2219
  this.select(shape.id);
1902
2220
  this.setTool("select");
1903
2221
  }
2222
+ this._markDirty(shape.id);
1904
2223
  this.render();
1905
2224
  this._emitChange("shape:add", { shape: deepClone(shape), shapeId: shape.id });
1906
2225
  if (shape.type === "text" || shape.type === "sticky") this.startTextEdit(shape.id);
@@ -1977,26 +2296,93 @@ var VdDraw = class {
1977
2296
  if (this.destroyed) return;
1978
2297
  this.render({ scene: false });
1979
2298
  }
2299
+ // ── Dirty tracking ──────────────────────────────────────────────────────
2300
+ _markDirty(shapeId) {
2301
+ this._dirtyShapes.add(shapeId);
2302
+ }
2303
+ _markAllDirty() {
2304
+ this._allDirty = true;
2305
+ }
2306
+ // ── rAF-batched render ─────────────────────────────────────────────────
2307
+ _scheduleRender(flags = {}) {
2308
+ this._pendingFlags = {
2309
+ scene: (this._pendingFlags?.scene ?? false) || flags.scene !== false,
2310
+ overlay: (this._pendingFlags?.overlay ?? false) || flags.overlay !== false
2311
+ };
2312
+ if (this._rafId != null) return;
2313
+ this._rafId = (typeof requestAnimationFrame === "function" ? requestAnimationFrame : setTimeout)(() => {
2314
+ this._rafId = null;
2315
+ const f = this._pendingFlags || {};
2316
+ this._pendingFlags = null;
2317
+ this.render(f);
2318
+ });
2319
+ }
1980
2320
  // ── Rendering ────────────────────────────────────────────────────────────
1981
2321
  render(flags = {}) {
1982
2322
  if (this.destroyed) return;
1983
2323
  const { scene = true, overlay = true } = flags;
1984
2324
  const vp = this.documentData.viewport;
1985
2325
  this.world.setAttribute("transform", `matrix(${vp.scale} 0 0 ${vp.scale} ${vp.x} ${vp.y})`);
2326
+ if (this.textEditor) {
2327
+ const activeShape = this.getShape(this.textEditor.id);
2328
+ if (activeShape) this._positionTextEditor(activeShape, this.textEditor.el);
2329
+ }
1986
2330
  if (scene) {
1987
- clearChildren(this.shapesLayer);
1988
- const erasing = this.interaction && this.interaction.kind === "erase" ? this.interaction.erased : null;
1989
- for (const shape of this.documentData.shapes) {
1990
- if (erasing && erasing.has(shape.id)) continue;
1991
- const el = this._renderShapeEl(shape, {});
1992
- if (el) this.shapesLayer.appendChild(el);
1993
- }
2331
+ this._renderSceneIncremental();
1994
2332
  }
1995
2333
  if (overlay) this._renderOverlay();
1996
2334
  }
2335
+ _renderSceneIncremental() {
2336
+ const shapes = this.documentData.shapes;
2337
+ const erasing = this.interaction && this.interaction.kind === "erase" ? this.interaction.erased : null;
2338
+ const currentIds = /* @__PURE__ */ new Set();
2339
+ for (const shape of shapes) {
2340
+ if (erasing && erasing.has(shape.id)) continue;
2341
+ currentIds.add(shape.id);
2342
+ }
2343
+ for (const [id, el] of this._shapeElements) {
2344
+ if (!currentIds.has(id)) {
2345
+ el.remove();
2346
+ this._shapeElements.delete(id);
2347
+ }
2348
+ }
2349
+ const fullRebuild = this._allDirty;
2350
+ let prevEl = null;
2351
+ for (const shape of shapes) {
2352
+ if (!currentIds.has(shape.id)) continue;
2353
+ const needsUpdate = fullRebuild || this._dirtyShapes.has(shape.id);
2354
+ let el = this._shapeElements.get(shape.id);
2355
+ if (needsUpdate || !el) {
2356
+ const newEl = this._renderShapeEl(shape, {});
2357
+ if (!newEl) {
2358
+ if (el) {
2359
+ el.remove();
2360
+ this._shapeElements.delete(shape.id);
2361
+ }
2362
+ continue;
2363
+ }
2364
+ if (el) {
2365
+ el.replaceWith(newEl);
2366
+ } else {
2367
+ if (prevEl && prevEl.parentNode === this.shapesLayer) {
2368
+ prevEl.after(newEl);
2369
+ } else {
2370
+ this.shapesLayer.prepend(newEl);
2371
+ }
2372
+ }
2373
+ el = newEl;
2374
+ this._shapeElements.set(shape.id, el);
2375
+ }
2376
+ const expected = prevEl ? prevEl.nextElementSibling : this.shapesLayer.firstElementChild;
2377
+ if (el !== expected) this.shapesLayer.insertBefore(el, expected);
2378
+ prevEl = el;
2379
+ }
2380
+ this._dirtyShapes.clear();
2381
+ this._allDirty = false;
2382
+ }
1997
2383
  // Colors are applied via inline `style` (which wins over the CSS class rules
1998
2384
  // and serializes self-contained), so a picked color always renders.
1999
- _renderShapeEl(shape, { standalone = false, colors = null }) {
2385
+ _renderShapeEl(shape, { standalone = false, colors = null, defsTarget = null } = {}) {
2000
2386
  let el = null;
2001
2387
  const setOpacity = (node) => {
2002
2388
  if (shape.opacity != null && shape.opacity !== 1)
@@ -2034,8 +2420,11 @@ var VdDraw = class {
2034
2420
  el.classList.add("vd-draw-shape");
2035
2421
  this._applyShapeStroke(el, shape, standalone, colors);
2036
2422
  setOpacity(el);
2037
- if (shape.arrowEnd) el.setAttribute("marker-end", `url(#${this._svgId("arrow")})`);
2038
- if (shape.arrowStart) el.setAttribute("marker-start", `url(#${this._svgId("arrow")})`);
2423
+ const strokeColor = shape.color || (standalone && colors ? colors.shapeStroke : "");
2424
+ const targetDefs = defsTarget || this.defsEl;
2425
+ const markerId = this._getArrowMarkerId(strokeColor, targetDefs);
2426
+ if (shape.arrowEnd) el.setAttribute("marker-end", `url(#${markerId})`);
2427
+ if (shape.arrowStart) el.setAttribute("marker-start", `url(#${markerId})`);
2039
2428
  } else if (shape.type === "text" || shape.type === "sticky") {
2040
2429
  el = createSvgEl("g");
2041
2430
  setOpacity(el);
@@ -2052,11 +2441,31 @@ var VdDraw = class {
2052
2441
  else if (standalone && colors) bg.style.fill = colors.sticky;
2053
2442
  el.appendChild(bg);
2054
2443
  }
2055
- const text = createSvgEl("text", { x: shape.x + 6, y: shape.y + 18 });
2444
+ const startX = (shape.x || 0) + (shape.type === "sticky" ? 10 : 6);
2445
+ const startY = (shape.y || 0) + (shape.type === "sticky" ? 14 : 18);
2446
+ const availWidth = shape.type === "sticky" ? Math.max(20, (shape.w || 160) - 20) : shape.w && shape.w > 20 ? shape.w - 12 : 0;
2447
+ const lines = wrapText(shape.text || "", availWidth);
2448
+ const text = createSvgEl("text", {
2449
+ x: startX,
2450
+ y: startY,
2451
+ "xml:space": "preserve"
2452
+ });
2056
2453
  text.classList.add("vd-draw-text");
2057
2454
  const fill = shape.color || (standalone && colors ? colors.text : "");
2058
2455
  if (fill) text.style.fill = fill;
2059
- text.textContent = shape.text || "";
2456
+ if (!lines.length) {
2457
+ text.textContent = "";
2458
+ } else {
2459
+ const lineHeight = 20;
2460
+ lines.forEach((lineText, idx) => {
2461
+ const tspan = createSvgEl("tspan", {
2462
+ x: startX,
2463
+ y: startY + idx * lineHeight
2464
+ });
2465
+ tspan.textContent = lineText;
2466
+ text.appendChild(tspan);
2467
+ });
2468
+ }
2060
2469
  el.appendChild(text);
2061
2470
  }
2062
2471
  if (el && !standalone) el.setAttribute("data-shape-id", shape.id);
@@ -2140,9 +2549,18 @@ var VdDraw = class {
2140
2549
  destroy() {
2141
2550
  if (this.destroyed) return;
2142
2551
  this.destroyed = true;
2552
+ if (this._rafId != null) {
2553
+ (typeof cancelAnimationFrame === "function" ? cancelAnimationFrame : clearTimeout)(
2554
+ this._rafId
2555
+ );
2556
+ this._rafId = null;
2557
+ }
2143
2558
  this.stopTextEdit({ commit: false });
2144
2559
  this._unbindEvents();
2145
2560
  this.listeners.clear();
2561
+ this._activePointers.clear();
2562
+ this._shapesById.clear();
2563
+ this._shapeElements.clear();
2146
2564
  if (this.element) this.element.replaceChildren();
2147
2565
  }
2148
2566
  };
@@ -2175,10 +2593,10 @@ var VdDraw2 = defineComponent({
2175
2593
  setup(props, { emit, expose }) {
2176
2594
  const el = ref(null);
2177
2595
  let instance = null;
2178
- const create = () => {
2596
+ const create = (savedDoc) => {
2179
2597
  instance = new VdDraw({
2180
2598
  element: el.value,
2181
- data: props.data,
2599
+ data: savedDoc || props.data,
2182
2600
  readonly: props.readonly,
2183
2601
  tool: props.tool,
2184
2602
  gridSize: props.gridSize,
@@ -2212,18 +2630,28 @@ var VdDraw2 = defineComponent({
2212
2630
  (next) => instance?.setGridVisible(next)
2213
2631
  );
2214
2632
  watch(
2215
- () => [
2216
- props.readonly,
2217
- props.gridSize,
2218
- props.snap,
2219
- props.autoFit,
2220
- props.history,
2221
- props.historyLimit
2222
- ],
2633
+ () => props.readonly,
2634
+ (next) => instance?.setReadonly(next)
2635
+ );
2636
+ watch(
2637
+ () => props.snap,
2638
+ (next) => instance?.setSnap(next)
2639
+ );
2640
+ watch(
2641
+ () => props.history,
2642
+ (next) => instance?.setHistoryEnabled(next)
2643
+ );
2644
+ watch(
2645
+ () => props.historyLimit,
2646
+ (next) => instance?.setHistoryLimit(next)
2647
+ );
2648
+ watch(
2649
+ () => [props.gridSize, props.autoFit],
2223
2650
  () => {
2224
2651
  if (!instance) return;
2652
+ const currentDoc = typeof instance.toJSON === "function" ? instance.toJSON() : void 0;
2225
2653
  instance.destroy();
2226
- create();
2654
+ create(currentDoc);
2227
2655
  }
2228
2656
  );
2229
2657
  onBeforeUnmount(() => {
@@ -2235,6 +2663,10 @@ var VdDraw2 = defineComponent({
2235
2663
  expose({
2236
2664
  getInstance: () => instance,
2237
2665
  setTool: (tool) => instance?.setTool(tool),
2666
+ setReadonly: (readonly) => instance?.setReadonly(readonly),
2667
+ setSnap: (snap) => instance?.setSnap(snap),
2668
+ setHistoryEnabled: (enabled) => instance?.setHistoryEnabled(enabled),
2669
+ setHistoryLimit: (limit) => instance?.setHistoryLimit(limit),
2238
2670
  undo: () => instance?.undo(),
2239
2671
  redo: () => instance?.redo(),
2240
2672
  canUndo: () => Boolean(instance?.canUndo()),