@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.
@@ -288,6 +288,37 @@ function simplifyPoints(points, min = 1.2) {
288
288
  }
289
289
  return out;
290
290
  }
291
+ function smoothPoint(prev, next, factor = 0.5) {
292
+ const f = clamp(factor, 0.1, 1);
293
+ const x = round(prev[0] + (next[0] - prev[0]) * f);
294
+ const y = round(prev[1] + (next[1] - prev[1]) * f);
295
+ const hasPressure = prev.length >= 3 && prev[2] != null || next.length >= 3 && next[2] != null;
296
+ if (hasPressure) {
297
+ const pp = prev.length >= 3 && prev[2] != null ? prev[2] : 0.5;
298
+ const np = next.length >= 3 && next[2] != null ? next[2] : 0.5;
299
+ return [x, y, clamp(pp + (np - pp) * f, 0, 1)];
300
+ }
301
+ return [x, y];
302
+ }
303
+ function appendAndSimplify(points, point, minDist = 1.5, maxLen = 500) {
304
+ if (!points.length) {
305
+ points.push(roundPoint(point));
306
+ return points;
307
+ }
308
+ const last = points[points.length - 1];
309
+ if (Math.hypot(point[0] - last[0], point[1] - last[1]) < minDist) return points;
310
+ points.push(roundPoint(point));
311
+ if (points.length > maxLen) {
312
+ const first = points[0];
313
+ const end = points[points.length - 1];
314
+ const kept = [first];
315
+ for (let i = 2; i < points.length - 1; i += 2) kept.push(points[i]);
316
+ kept.push(end);
317
+ points.length = 0;
318
+ for (const p of kept) points.push(p);
319
+ }
320
+ return points;
321
+ }
291
322
  function streamlinePoints(points, streamline) {
292
323
  const first = points[0];
293
324
  const out = [[first[0], first[1], first[2] == null ? 0.5 : clamp(first[2], 0, 1)]];
@@ -502,6 +533,55 @@ function normalizeDocument(data) {
502
533
  shapes
503
534
  };
504
535
  }
536
+ function wrapText(text, maxWidth, charWidth = 8.8) {
537
+ if (typeof text !== "string" || text.length === 0) return [];
538
+ const rawLines = text.split(/\r?\n/);
539
+ if (!maxWidth || maxWidth <= 0 || !Number.isFinite(maxWidth)) {
540
+ return rawLines;
541
+ }
542
+ const maxChars = Math.max(1, Math.floor(maxWidth / charWidth));
543
+ const result = [];
544
+ for (const rawLine of rawLines) {
545
+ if (rawLine.length <= maxChars) {
546
+ result.push(rawLine);
547
+ continue;
548
+ }
549
+ const words = rawLine.split(" ");
550
+ let currentLine = "";
551
+ for (const word of words) {
552
+ if (!currentLine) {
553
+ if (word.length <= maxChars) {
554
+ currentLine = word;
555
+ } else {
556
+ let remaining = word;
557
+ while (remaining.length > maxChars) {
558
+ result.push(remaining.slice(0, maxChars));
559
+ remaining = remaining.slice(maxChars);
560
+ }
561
+ currentLine = remaining;
562
+ }
563
+ } else {
564
+ if (currentLine.length + 1 + word.length <= maxChars) {
565
+ currentLine += " " + word;
566
+ } else {
567
+ result.push(currentLine);
568
+ if (word.length <= maxChars) {
569
+ currentLine = word;
570
+ } else {
571
+ let remaining = word;
572
+ while (remaining.length > maxChars) {
573
+ result.push(remaining.slice(0, maxChars));
574
+ remaining = remaining.slice(maxChars);
575
+ }
576
+ currentLine = remaining;
577
+ }
578
+ }
579
+ }
580
+ }
581
+ if (currentLine) result.push(currentLine);
582
+ }
583
+ return result;
584
+ }
505
585
 
506
586
  // src/draw/core.js
507
587
  var SVG_NS = "http://www.w3.org/2000/svg";
@@ -627,6 +707,15 @@ var VdDraw = class {
627
707
  this.lastReason = null;
628
708
  this.lastTargetKey = null;
629
709
  this.listeners = /* @__PURE__ */ new Map();
710
+ this.options = { ...opts };
711
+ this._shapesById = /* @__PURE__ */ new Map();
712
+ this._rebuildShapeIndex();
713
+ this._activePointers = /* @__PURE__ */ new Map();
714
+ this._shapeElements = /* @__PURE__ */ new Map();
715
+ this._dirtyShapes = /* @__PURE__ */ new Set();
716
+ this._allDirty = true;
717
+ this._rafId = null;
718
+ this._pendingFlags = null;
630
719
  this._buildShell();
631
720
  if (typeof opts.color !== "string") this.style.color = this._resolveInk();
632
721
  this._bindEvents();
@@ -635,6 +724,12 @@ var VdDraw = class {
635
724
  this._syncStylePanel();
636
725
  this._scheduleReady();
637
726
  }
727
+ _rebuildShapeIndex() {
728
+ this._shapesById.clear();
729
+ for (const s of this.documentData.shapes) {
730
+ this._shapesById.set(s.id, s);
731
+ }
732
+ }
638
733
  _resolveElement(target) {
639
734
  if (!target) return null;
640
735
  if (typeof target === "string") return hasWindow() ? document.querySelector(target) : null;
@@ -668,11 +763,13 @@ var VdDraw = class {
668
763
  }
669
764
  this.canvasEl = document.createElement("div");
670
765
  this.canvasEl.className = "vd-draw-canvas";
766
+ this.canvasEl.setAttribute("data-tool", this.tool);
671
767
  this.canvasEl.tabIndex = 0;
672
768
  this.svg = createSvgEl("svg", { class: "vd-draw-svg" });
673
769
  this.svg.setAttribute("width", "100%");
674
770
  this.svg.setAttribute("height", "100%");
675
771
  const defs = createSvgEl("defs");
772
+ this.defsEl = defs;
676
773
  const gs = this.gridSize;
677
774
  this.gridPattern = createSvgEl("pattern", {
678
775
  id: this._svgId("grid"),
@@ -688,19 +785,7 @@ var VdDraw = class {
688
785
  });
689
786
  this.gridPattern.appendChild(this.gridPatternPath);
690
787
  defs.appendChild(this.gridPattern);
691
- const marker = createSvgEl("marker", {
692
- id: this._svgId("arrow"),
693
- viewBox: "0 0 10 10",
694
- refX: 8,
695
- refY: 5,
696
- markerWidth: 7,
697
- markerHeight: 7,
698
- orient: "auto-start-reverse"
699
- });
700
- marker.appendChild(
701
- createSvgEl("path", { d: "M 0 0 L 10 5 L 0 10 z", fill: "var(--vd-draw-shape-stroke)" })
702
- );
703
- defs.appendChild(marker);
788
+ this._getArrowMarkerId("", defs);
704
789
  this.svg.appendChild(defs);
705
790
  this.world = createSvgEl("g", { class: "vd-draw-world" });
706
791
  this.gridLayer = createSvgEl("rect", {
@@ -991,11 +1076,11 @@ var VdDraw = class {
991
1076
  this._emitViewportChange("viewport:reset");
992
1077
  return this;
993
1078
  }
994
- fitView() {
1079
+ fitView(padding = 40) {
995
1080
  const bounds = boundsOfShapes(this.documentData.shapes);
996
1081
  const rect = this.canvasEl.getBoundingClientRect();
997
1082
  if (!bounds || bounds.w === 0 || bounds.h === 0 || !rect.width || !rect.height) return this;
998
- const pad = 40;
1083
+ const pad = Number.isFinite(padding) ? Math.max(0, padding) : 40;
999
1084
  const scale = clamp(
1000
1085
  Math.min((rect.width - pad * 2) / bounds.w, (rect.height - pad * 2) / bounds.h),
1001
1086
  MIN_SCALE,
@@ -1037,10 +1122,65 @@ var VdDraw = class {
1037
1122
  toggleGrid() {
1038
1123
  return this.setGridVisible(!this.showGrid);
1039
1124
  }
1125
+ // ── Runtime options ──────────────────────────────────────────────────────
1126
+ setReadonly(readonly) {
1127
+ const next = Boolean(readonly);
1128
+ if (this.readonly === next) return this;
1129
+ this.readonly = next;
1130
+ this.options.readonly = next;
1131
+ if (this.element) {
1132
+ if (next) this.element.setAttribute("data-readonly", "true");
1133
+ else this.element.removeAttribute("data-readonly");
1134
+ }
1135
+ if (next) {
1136
+ this.stopTextEdit({ commit: true });
1137
+ this.deselect();
1138
+ if (this.toolbarEl) this.toolbarEl.style.display = "none";
1139
+ if (this.panelEl) this.panelEl.style.display = "none";
1140
+ } else {
1141
+ if (this.toolbarEl && !this.toolbarEl.children.length) {
1142
+ this._buildToolbar();
1143
+ }
1144
+ if (this.panelEl && !this.panelEl.children.length) {
1145
+ this._buildStylePanel();
1146
+ }
1147
+ if (this.toolbarEl) this.toolbarEl.style.display = "";
1148
+ if (this.panelEl) this.panelEl.style.display = "";
1149
+ this._syncToolbar();
1150
+ this._syncStylePanel();
1151
+ }
1152
+ this.render({ scene: false, overlay: true });
1153
+ return this;
1154
+ }
1155
+ setSnap(snap) {
1156
+ this.snap = Boolean(snap);
1157
+ this.options.snap = this.snap;
1158
+ return this;
1159
+ }
1160
+ setHistoryEnabled(enabled) {
1161
+ this.historyEnabled = Boolean(enabled);
1162
+ this.options.history = this.historyEnabled;
1163
+ if (!this.historyEnabled) {
1164
+ this.clearHistory();
1165
+ }
1166
+ return this;
1167
+ }
1168
+ setHistoryLimit(limit) {
1169
+ const num = Number(limit);
1170
+ this.historyLimit = Number.isFinite(num) && num > 0 ? num : 100;
1171
+ this.options.historyLimit = this.historyLimit;
1172
+ if (this.history.length > this.historyLimit + 1) {
1173
+ const drop = this.history.length - (this.historyLimit + 1);
1174
+ this.history.splice(0, drop);
1175
+ this.historyIndex = Math.max(0, this.historyIndex - drop);
1176
+ }
1177
+ return this;
1178
+ }
1040
1179
  // ── Tool + current style ─────────────────────────────────────────────────
1041
1180
  setTool(tool) {
1042
1181
  if (!DRAW_TOOLS.includes(tool)) return this;
1043
1182
  this.tool = tool;
1183
+ if (this.canvasEl) this.canvasEl.setAttribute("data-tool", tool);
1044
1184
  this._syncToolbar();
1045
1185
  return this;
1046
1186
  }
@@ -1074,7 +1214,7 @@ var VdDraw = class {
1074
1214
  }
1075
1215
  // ── Shape CRUD ─────────────────────────────────────────────────────────
1076
1216
  getShape(id) {
1077
- return this.documentData.shapes.find((s) => s.id === id) || null;
1217
+ return this._shapesById.get(id) || null;
1078
1218
  }
1079
1219
  getShapes() {
1080
1220
  return this.documentData.shapes.map((s) => deepClone(s));
@@ -1126,6 +1266,8 @@ var VdDraw = class {
1126
1266
  shape.text = typeof partial.text === "string" ? partial.text : "";
1127
1267
  }
1128
1268
  this.documentData.shapes.push(shape);
1269
+ this._shapesById.set(shape.id, shape);
1270
+ this._markDirty(shape.id);
1129
1271
  this.render();
1130
1272
  this._emitChange("shape:add", { shape: deepClone(shape), shapeId: shape.id });
1131
1273
  return shape;
@@ -1139,6 +1281,8 @@ var VdDraw = class {
1139
1281
  const shape = this.getShape(id);
1140
1282
  if (!shape || !patch) return null;
1141
1283
  Object.assign(shape, patch);
1284
+ this._shapesById.set(id, shape);
1285
+ this._markDirty(id);
1142
1286
  this.render();
1143
1287
  this._emitChange(options.reason || "shape:update", { shapeId: id }, options.reason ? id : null);
1144
1288
  return shape;
@@ -1147,7 +1291,9 @@ var VdDraw = class {
1147
1291
  const idx = this.documentData.shapes.findIndex((s) => s.id === id);
1148
1292
  if (idx === -1) return false;
1149
1293
  this.documentData.shapes.splice(idx, 1);
1294
+ this._shapesById.delete(id);
1150
1295
  this.selectedIds.delete(id);
1296
+ this._markDirty(id);
1151
1297
  this.render();
1152
1298
  this._emitChange("shape:delete", { shapeId: id });
1153
1299
  return true;
@@ -1214,11 +1360,13 @@ var VdDraw = class {
1214
1360
  _replaceShape(next) {
1215
1361
  const idx = this.documentData.shapes.findIndex((s) => s.id === next.id);
1216
1362
  if (idx !== -1) this.documentData.shapes[idx] = next;
1363
+ this._shapesById.set(next.id, next);
1217
1364
  }
1218
1365
  nudge(dx, dy) {
1219
1366
  const selected = this.getSelectedShapes();
1220
1367
  if (!selected.length) return this;
1221
1368
  for (const shape of selected) this._replaceShape(translateShape(shape, dx, dy));
1369
+ this._markAllDirty();
1222
1370
  this.render();
1223
1371
  this._emitChange(
1224
1372
  "shape:nudge",
@@ -1237,6 +1385,7 @@ var VdDraw = class {
1237
1385
  const scaled = scaleShape(shape, cur.x, cur.y, sx, sy);
1238
1386
  this._replaceShape(translateShape(scaled, target.x - cur.x, target.y - cur.y));
1239
1387
  }
1388
+ this._markAllDirty();
1240
1389
  this.render();
1241
1390
  this._emitChange("shape:resize", { shapeIds: [...this.selectedIds] });
1242
1391
  return this;
@@ -1244,8 +1393,10 @@ var VdDraw = class {
1244
1393
  deleteSelection() {
1245
1394
  if (this.selectedIds.size === 0) return false;
1246
1395
  const ids = [...this.selectedIds];
1396
+ for (const id of ids) this._shapesById.delete(id);
1247
1397
  this.documentData.shapes = this.documentData.shapes.filter((s) => !this.selectedIds.has(s.id));
1248
1398
  this.selectedIds.clear();
1399
+ this._markAllDirty();
1249
1400
  this.render();
1250
1401
  this._emitChange("shape:delete", { shapeIds: ids });
1251
1402
  return true;
@@ -1257,6 +1408,7 @@ var VdDraw = class {
1257
1408
  for (const shape of selected) {
1258
1409
  for (const key of allowed) if (key in patch) shape[key] = patch[key];
1259
1410
  }
1411
+ this._markAllDirty();
1260
1412
  this.render();
1261
1413
  this._emitChange(
1262
1414
  "shape:style",
@@ -1269,6 +1421,7 @@ var VdDraw = class {
1269
1421
  _reorder(mutator) {
1270
1422
  if (this.selectedIds.size === 0) return this;
1271
1423
  mutator();
1424
+ this._markAllDirty();
1272
1425
  this.render();
1273
1426
  this._emitChange("shape:reorder", { shapeIds: [...this.selectedIds] });
1274
1427
  return this;
@@ -1311,6 +1464,7 @@ var VdDraw = class {
1311
1464
  if (selected.length < 2) return this;
1312
1465
  const groupId = createId("grp");
1313
1466
  for (const shape of selected) shape.groupId = groupId;
1467
+ this._markAllDirty();
1314
1468
  this.render();
1315
1469
  this._emitChange("shape:group", { groupId, shapeIds: [...this.selectedIds] });
1316
1470
  return this;
@@ -1325,6 +1479,7 @@ var VdDraw = class {
1325
1479
  }
1326
1480
  }
1327
1481
  if (!changed) return this;
1482
+ this._markAllDirty();
1328
1483
  this.render();
1329
1484
  this._emitChange("shape:ungroup", { shapeIds: [...this.selectedIds] });
1330
1485
  return this;
@@ -1351,9 +1506,11 @@ var VdDraw = class {
1351
1506
  copy.groupId = groupRemap.get(copy.groupId);
1352
1507
  }
1353
1508
  this.documentData.shapes.push(copy);
1509
+ this._shapesById.set(copy.id, copy);
1354
1510
  newIds.push(copy.id);
1355
1511
  }
1356
1512
  this.selectedIds = new Set(newIds);
1513
+ this._markAllDirty();
1357
1514
  this.render();
1358
1515
  this._emitChange("shape:paste", { shapeIds: newIds });
1359
1516
  this._emitSelect();
@@ -1415,6 +1572,30 @@ var VdDraw = class {
1415
1572
  sticky: read("--vd-draw-sticky-fill", "#fdf3c4")
1416
1573
  };
1417
1574
  }
1575
+ _getArrowMarkerId(color, defsTarget = this.defsEl) {
1576
+ const isDefault = !color;
1577
+ const safeKey = isDefault ? "arrow" : "arrow-" + color.toLowerCase().replace(/[^a-z0-9_-]/g, "_");
1578
+ const id = this._svgId(safeKey);
1579
+ if (defsTarget && !defsTarget.querySelector(`#${id}`)) {
1580
+ const marker = createSvgEl("marker", {
1581
+ id,
1582
+ viewBox: "0 0 10 10",
1583
+ refX: 8,
1584
+ refY: 5,
1585
+ markerWidth: 7,
1586
+ markerHeight: 7,
1587
+ orient: "auto-start-reverse"
1588
+ });
1589
+ marker.appendChild(
1590
+ createSvgEl("path", {
1591
+ d: "M 0 0 L 10 5 L 0 10 z",
1592
+ fill: color || "var(--vd-draw-shape-stroke)"
1593
+ })
1594
+ );
1595
+ defsTarget.appendChild(marker);
1596
+ }
1597
+ return id;
1598
+ }
1418
1599
  toSVG() {
1419
1600
  const shapes = this.documentData.shapes;
1420
1601
  const bounds = boundsOfShapes(shapes) || { x: 0, y: 0, w: 100, h: 100 };
@@ -1432,10 +1613,14 @@ var VdDraw = class {
1432
1613
  height: round(vb.h),
1433
1614
  viewBox: `${round(vb.x)} ${round(vb.y)} ${round(vb.w)} ${round(vb.h)}`
1434
1615
  });
1616
+ const defs = createSvgEl("defs");
1435
1617
  for (const shape of shapes) {
1436
- const el = this._renderShapeEl(shape, { standalone: true, colors });
1618
+ const el = this._renderShapeEl(shape, { standalone: true, colors, defsTarget: defs });
1437
1619
  if (el) svg.appendChild(el);
1438
1620
  }
1621
+ if (defs.childNodes.length > 0) {
1622
+ svg.insertBefore(defs, svg.firstChild);
1623
+ }
1439
1624
  return new XMLSerializer().serializeToString(svg);
1440
1625
  }
1441
1626
  toPNG({ scale = 2 } = {}) {
@@ -1529,7 +1714,9 @@ var VdDraw = class {
1529
1714
  const viewport = { ...this.documentData.viewport };
1530
1715
  this.documentData = deepClone(snapshot);
1531
1716
  this.documentData.viewport = viewport;
1717
+ this._rebuildShapeIndex();
1532
1718
  this._syncSelectionValidity();
1719
+ this._markAllDirty();
1533
1720
  this.render();
1534
1721
  this.emit("change", { reason, document: this.toJSON() });
1535
1722
  this.emit("history", { reason, canUndo: this.canUndo(), canRedo: this.canRedo() });
@@ -1545,8 +1732,10 @@ var VdDraw = class {
1545
1732
  }
1546
1733
  load(data) {
1547
1734
  this.documentData = normalizeDocument(data);
1735
+ this._rebuildShapeIndex();
1548
1736
  this.selectedIds.clear();
1549
1737
  this._resetHistory();
1738
+ this._markAllDirty();
1550
1739
  this.render();
1551
1740
  this._emitChange("load");
1552
1741
  this._emitSelect();
@@ -1555,7 +1744,9 @@ var VdDraw = class {
1555
1744
  clear() {
1556
1745
  if (!this.documentData.shapes.length) return this;
1557
1746
  this.documentData.shapes = [];
1747
+ this._shapesById.clear();
1558
1748
  this.selectedIds.clear();
1749
+ this._markAllDirty();
1559
1750
  this.render();
1560
1751
  this._emitChange("clear");
1561
1752
  return this;
@@ -1580,10 +1771,16 @@ var VdDraw = class {
1580
1771
  }
1581
1772
  _positionTextEditor(shape, editor) {
1582
1773
  const vp = this.documentData.viewport;
1583
- editor.style.left = `${vp.x + shape.x * vp.scale}px`;
1584
- editor.style.top = `${vp.y + shape.y * vp.scale}px`;
1585
- editor.style.width = `${shape.w * vp.scale}px`;
1586
- editor.style.height = `${shape.h * vp.scale}px`;
1774
+ const scale = vp.scale || 1;
1775
+ editor.style.left = `${vp.x + shape.x * scale}px`;
1776
+ editor.style.top = `${vp.y + shape.y * scale}px`;
1777
+ editor.style.width = `${shape.w * scale}px`;
1778
+ editor.style.height = `${shape.h * scale}px`;
1779
+ editor.style.fontSize = `${16 * scale}px`;
1780
+ editor.style.lineHeight = `${20 * scale}px`;
1781
+ if (shape.type === "sticky") {
1782
+ editor.style.background = shape.fill || "var(--vd-draw-sticky-fill)";
1783
+ }
1587
1784
  }
1588
1785
  stopTextEdit({ commit = true } = {}) {
1589
1786
  if (!this.textEditor) return;
@@ -1595,6 +1792,7 @@ var VdDraw = class {
1595
1792
  const shape = this.getShape(id);
1596
1793
  if (shape && shape.text !== value) {
1597
1794
  shape.text = value;
1795
+ this._markDirty(id);
1598
1796
  this.render();
1599
1797
  this._emitChange("shape:text", { shapeId: id }, `text:${id}`);
1600
1798
  }
@@ -1652,11 +1850,65 @@ var VdDraw = class {
1652
1850
  if (this.destroyed || event.button != null && event.button !== 0) return;
1653
1851
  this.canvasEl.focus();
1654
1852
  this.stopTextEdit({ commit: true });
1853
+ this._activePointers.set(event.pointerId, { clientX: event.clientX, clientY: event.clientY });
1854
+ if (this._activePointers.size === 2) {
1855
+ if (this.interaction) {
1856
+ const it = this.interaction;
1857
+ if (it.kind === "freehand" || it.kind === "create-line" || it.kind === "create-box") {
1858
+ this.documentData.shapes = this.documentData.shapes.filter((s) => s.id !== it.shapeId);
1859
+ this._shapesById.delete(it.shapeId);
1860
+ const el = this._shapeElements.get(it.shapeId);
1861
+ if (el) {
1862
+ el.remove();
1863
+ this._shapeElements.delete(it.shapeId);
1864
+ }
1865
+ this._scheduleRender({ scene: true, overlay: false });
1866
+ } else if (it.kind === "move" || it.kind === "resize") {
1867
+ for (const orig of it.originals) {
1868
+ this._replaceShape(orig);
1869
+ this._markDirty(orig.id);
1870
+ }
1871
+ if (it.kind === "move") this._renderGuides([]);
1872
+ this._scheduleRender({ scene: true });
1873
+ } else if (it.kind === "erase") {
1874
+ for (const id of it.erased) this._markDirty(id);
1875
+ this._scheduleRender({ scene: true, overlay: false });
1876
+ } else if (it.kind === "pan") {
1877
+ if (this.canvasEl) this.canvasEl.classList.remove("vd-draw-panning");
1878
+ } else if (it.kind === "marquee") {
1879
+ clearChildren(this.marqueeLayer);
1880
+ }
1881
+ }
1882
+ const pts = [...this._activePointers.values()];
1883
+ const p1 = pts[0];
1884
+ const p2 = pts[1];
1885
+ const initialDistance = Math.hypot(p2.clientX - p1.clientX, p2.clientY - p1.clientY) || 1;
1886
+ const initialClientMid = {
1887
+ x: (p1.clientX + p2.clientX) / 2,
1888
+ y: (p1.clientY + p2.clientY) / 2
1889
+ };
1890
+ const initialLocalMid = this._clientToLocal(initialClientMid.x, initialClientMid.y);
1891
+ const initialVp = { ...this.documentData.viewport };
1892
+ const currentScale = initialVp.scale || 1;
1893
+ const worldMid = {
1894
+ x: (initialLocalMid.x - initialVp.x) / currentScale,
1895
+ y: (initialLocalMid.y - initialVp.y) / currentScale
1896
+ };
1897
+ this.interaction = {
1898
+ kind: "pinch",
1899
+ initialDistance,
1900
+ worldMid,
1901
+ initialScale: currentScale
1902
+ };
1903
+ if (typeof event.preventDefault === "function") event.preventDefault();
1904
+ return;
1905
+ }
1655
1906
  const world = this._clientToWorld(event.clientX, event.clientY);
1656
1907
  const shapeTarget = event.target.closest("[data-shape-id]");
1657
1908
  const handleTarget = event.target.closest("[data-handle]");
1658
1909
  if (this.tool === "hand") {
1659
1910
  this.interaction = this._beginPan(event);
1911
+ if (this.canvasEl) this.canvasEl.classList.add("vd-draw-panning");
1660
1912
  this._capture(event.pointerId);
1661
1913
  return;
1662
1914
  }
@@ -1667,6 +1919,15 @@ var VdDraw = class {
1667
1919
  return;
1668
1920
  }
1669
1921
  if (this.tool === "select") {
1922
+ if (event.detail === 2 && shapeTarget && !this.readonly) {
1923
+ const id = shapeTarget.getAttribute("data-shape-id");
1924
+ const shape = this.getShape(id);
1925
+ if (shape && (shape.type === "text" || shape.type === "sticky")) {
1926
+ this.select(id);
1927
+ this.startTextEdit(id);
1928
+ return;
1929
+ }
1930
+ }
1670
1931
  if (handleTarget && this.selectedIds.size) {
1671
1932
  this.interaction = {
1672
1933
  kind: "resize",
@@ -1730,6 +1991,7 @@ var VdDraw = class {
1730
1991
  points: [[round(world.x), round(world.y), pressure]]
1731
1992
  };
1732
1993
  this.documentData.shapes.push(shape2);
1994
+ this._shapesById.set(shape2.id, shape2);
1733
1995
  return { kind: "freehand", pointerId: event.pointerId, shapeId: shape2.id };
1734
1996
  }
1735
1997
  if (tool === "line") {
@@ -1747,6 +2009,7 @@ var VdDraw = class {
1747
2009
  opacity: this.style.opacity
1748
2010
  };
1749
2011
  this.documentData.shapes.push(shape2);
2012
+ this._shapesById.set(shape2.id, shape2);
1750
2013
  return { kind: "create-line", pointerId: event.pointerId, shapeId: shape2.id };
1751
2014
  }
1752
2015
  const type = tool;
@@ -1764,6 +2027,7 @@ var VdDraw = class {
1764
2027
  };
1765
2028
  if (type === "text" || type === "sticky") shape.text = "";
1766
2029
  this.documentData.shapes.push(shape);
2030
+ this._shapesById.set(shape.id, shape);
1767
2031
  return { kind: "create-box", pointerId: event.pointerId, shapeId: shape.id, start: world };
1768
2032
  }
1769
2033
  _applyErase(world) {
@@ -1778,16 +2042,44 @@ var VdDraw = class {
1778
2042
  changed = true;
1779
2043
  }
1780
2044
  }
1781
- if (changed) this.render({ scene: true, overlay: false });
2045
+ if (changed) {
2046
+ for (const id of it.erased) this._markDirty(id);
2047
+ this._scheduleRender({ scene: true, overlay: false });
2048
+ }
1782
2049
  }
1783
2050
  _handlePointerMove(event) {
2051
+ if (this._activePointers.has(event.pointerId)) {
2052
+ this._activePointers.set(event.pointerId, { clientX: event.clientX, clientY: event.clientY });
2053
+ }
1784
2054
  const it = this.interaction;
1785
2055
  if (!it) return;
2056
+ if (it.kind === "pinch") {
2057
+ if (this._activePointers.size >= 2) {
2058
+ if (typeof event.preventDefault === "function") event.preventDefault();
2059
+ const pts = [...this._activePointers.values()];
2060
+ const p1 = pts[0];
2061
+ const p2 = pts[1];
2062
+ const curDistance = Math.hypot(p2.clientX - p1.clientX, p2.clientY - p1.clientY) || 1;
2063
+ const curClientMid = {
2064
+ x: (p1.clientX + p2.clientX) / 2,
2065
+ y: (p1.clientY + p2.clientY) / 2
2066
+ };
2067
+ const curLocalMid = this._clientToLocal(curClientMid.x, curClientMid.y);
2068
+ const distRatio = curDistance / it.initialDistance;
2069
+ const newScale = clamp(it.initialScale * distRatio, MIN_SCALE, MAX_SCALE);
2070
+ const vp = this.documentData.viewport;
2071
+ vp.scale = newScale;
2072
+ vp.x = curLocalMid.x - it.worldMid.x * newScale;
2073
+ vp.y = curLocalMid.y - it.worldMid.y * newScale;
2074
+ this._scheduleRender({ scene: false });
2075
+ }
2076
+ return;
2077
+ }
1786
2078
  if (it.kind === "pan") {
1787
2079
  const vp = this.documentData.viewport;
1788
2080
  vp.x = it.startX + (event.clientX - it.startClientX);
1789
2081
  vp.y = it.startY + (event.clientY - it.startClientY);
1790
- this.render({ scene: false });
2082
+ this._scheduleRender({ scene: false });
1791
2083
  return;
1792
2084
  }
1793
2085
  const world = this._clientToWorld(event.clientX, event.clientY);
@@ -1805,8 +2097,9 @@ var VdDraw = class {
1805
2097
  if (snap.dx || snap.dy)
1806
2098
  for (const orig of it.originals)
1807
2099
  this._replaceShape(translateShape(orig, dx + snap.dx, dy + snap.dy));
2100
+ for (const orig of it.originals) this._markDirty(orig.id);
1808
2101
  this._renderGuides(snap.guides);
1809
- this.render({ scene: true, guides: false });
2102
+ this._scheduleRender({ scene: true, guides: false });
1810
2103
  return;
1811
2104
  }
1812
2105
  if (it.kind === "resize") {
@@ -1821,9 +2114,10 @@ var VdDraw = class {
1821
2114
  for (const orig of it.originals) {
1822
2115
  const scaled = scaleShape(orig, it.startBounds.x, it.startBounds.y, sx, sy);
1823
2116
  this._replaceShape(translateShape(scaled, t.x - it.startBounds.x, t.y - it.startBounds.y));
2117
+ this._markDirty(orig.id);
1824
2118
  }
1825
2119
  it.moved = true;
1826
- this.render();
2120
+ this._scheduleRender();
1827
2121
  return;
1828
2122
  }
1829
2123
  if (it.kind === "marquee") {
@@ -1834,8 +2128,14 @@ var VdDraw = class {
1834
2128
  if (it.kind === "freehand") {
1835
2129
  const shape = this.getShape(it.shapeId);
1836
2130
  if (shape) {
1837
- shape.points.push([round(world.x), round(world.y), event.pressure || 0.5]);
1838
- this.render({ scene: true });
2131
+ const raw = [round(world.x), round(world.y), event.pressure || 0.5];
2132
+ const preset = BRUSH_PRESETS[shape.brush] || BRUSH_PRESETS[DEFAULT_BRUSH];
2133
+ const smoothFactor = 1 - (preset.smoothing || 0.5) * 0.6;
2134
+ const prev = shape.points[shape.points.length - 1];
2135
+ const pt = prev ? smoothPoint(prev, raw, smoothFactor) : raw;
2136
+ appendAndSimplify(shape.points, pt);
2137
+ this._markDirty(it.shapeId);
2138
+ this._scheduleRender({ scene: true });
1839
2139
  }
1840
2140
  return;
1841
2141
  }
@@ -1843,7 +2143,8 @@ var VdDraw = class {
1843
2143
  const shape = this.getShape(it.shapeId);
1844
2144
  if (shape) {
1845
2145
  shape.points[1] = [round(world.x), round(world.y)];
1846
- this.render({ scene: true });
2146
+ this._markDirty(it.shapeId);
2147
+ this._scheduleRender({ scene: true });
1847
2148
  }
1848
2149
  return;
1849
2150
  }
@@ -1854,7 +2155,8 @@ var VdDraw = class {
1854
2155
  shape.y = round(Math.min(it.start.y, world.y));
1855
2156
  shape.w = round(Math.max(1, Math.abs(world.x - it.start.x)));
1856
2157
  shape.h = round(Math.max(1, Math.abs(world.y - it.start.y)));
1857
- this.render({ scene: true });
2158
+ this._markDirty(it.shapeId);
2159
+ this._scheduleRender({ scene: true });
1858
2160
  }
1859
2161
  }
1860
2162
  }
@@ -1873,9 +2175,19 @@ var VdDraw = class {
1873
2175
  return { x, y, w, h: h2 };
1874
2176
  }
1875
2177
  _handlePointerUp(event) {
2178
+ this._activePointers.delete(event.pointerId);
1876
2179
  const it = this.interaction;
1877
2180
  if (!it) return;
2181
+ if (it.kind === "pinch") {
2182
+ if (this._activePointers.size < 2) {
2183
+ this.interaction = null;
2184
+ if (this.canvasEl) this.canvasEl.classList.remove("vd-draw-panning");
2185
+ this._emitViewportChange("viewport:pinch");
2186
+ }
2187
+ return;
2188
+ }
1878
2189
  this.interaction = null;
2190
+ if (this.canvasEl) this.canvasEl.classList.remove("vd-draw-panning");
1879
2191
  if (typeof this.canvasEl.releasePointerCapture === "function" && this.canvasEl.hasPointerCapture?.(event.pointerId)) {
1880
2192
  try {
1881
2193
  this.canvasEl.releasePointerCapture(event.pointerId);
@@ -1891,18 +2203,22 @@ var VdDraw = class {
1891
2203
  if (it.kind === "erase") {
1892
2204
  if (it.erased.size) {
1893
2205
  const ids = [...it.erased];
2206
+ for (const id of ids) this._shapesById.delete(id);
1894
2207
  this.documentData.shapes = this.documentData.shapes.filter((s) => !it.erased.has(s.id));
2208
+ this._markAllDirty();
1895
2209
  this.render();
1896
2210
  this._emitChange("shape:erase", { shapeIds: ids });
1897
2211
  }
1898
2212
  return;
1899
2213
  }
1900
2214
  if (it.kind === "move" && it.moved) {
2215
+ this._markAllDirty();
1901
2216
  this.render();
1902
2217
  this._emitChange("shape:move", { shapeIds: [...this.selectedIds] });
1903
2218
  return;
1904
2219
  }
1905
2220
  if (it.kind === "resize" && it.moved) {
2221
+ this._markAllDirty();
1906
2222
  this.render();
1907
2223
  this._emitChange("shape:resize", { shapeIds: [...this.selectedIds] });
1908
2224
  return;
@@ -1919,6 +2235,8 @@ var VdDraw = class {
1919
2235
  const b = shapeBounds(shape);
1920
2236
  if (it.kind !== "freehand" && b.w < 2 && b.h < 2 && shape.type !== "text" && shape.type !== "sticky") {
1921
2237
  this.documentData.shapes = this.documentData.shapes.filter((s) => s.id !== it.shapeId);
2238
+ this._shapesById.delete(it.shapeId);
2239
+ this._markAllDirty();
1922
2240
  this.render();
1923
2241
  return;
1924
2242
  }
@@ -1932,6 +2250,7 @@ var VdDraw = class {
1932
2250
  this.select(shape.id);
1933
2251
  this.setTool("select");
1934
2252
  }
2253
+ this._markDirty(shape.id);
1935
2254
  this.render();
1936
2255
  this._emitChange("shape:add", { shape: deepClone(shape), shapeId: shape.id });
1937
2256
  if (shape.type === "text" || shape.type === "sticky") this.startTextEdit(shape.id);
@@ -2008,26 +2327,93 @@ var VdDraw = class {
2008
2327
  if (this.destroyed) return;
2009
2328
  this.render({ scene: false });
2010
2329
  }
2330
+ // ── Dirty tracking ──────────────────────────────────────────────────────
2331
+ _markDirty(shapeId) {
2332
+ this._dirtyShapes.add(shapeId);
2333
+ }
2334
+ _markAllDirty() {
2335
+ this._allDirty = true;
2336
+ }
2337
+ // ── rAF-batched render ─────────────────────────────────────────────────
2338
+ _scheduleRender(flags = {}) {
2339
+ this._pendingFlags = {
2340
+ scene: (this._pendingFlags?.scene ?? false) || flags.scene !== false,
2341
+ overlay: (this._pendingFlags?.overlay ?? false) || flags.overlay !== false
2342
+ };
2343
+ if (this._rafId != null) return;
2344
+ this._rafId = (typeof requestAnimationFrame === "function" ? requestAnimationFrame : setTimeout)(() => {
2345
+ this._rafId = null;
2346
+ const f = this._pendingFlags || {};
2347
+ this._pendingFlags = null;
2348
+ this.render(f);
2349
+ });
2350
+ }
2011
2351
  // ── Rendering ────────────────────────────────────────────────────────────
2012
2352
  render(flags = {}) {
2013
2353
  if (this.destroyed) return;
2014
2354
  const { scene = true, overlay = true } = flags;
2015
2355
  const vp = this.documentData.viewport;
2016
2356
  this.world.setAttribute("transform", `matrix(${vp.scale} 0 0 ${vp.scale} ${vp.x} ${vp.y})`);
2357
+ if (this.textEditor) {
2358
+ const activeShape = this.getShape(this.textEditor.id);
2359
+ if (activeShape) this._positionTextEditor(activeShape, this.textEditor.el);
2360
+ }
2017
2361
  if (scene) {
2018
- clearChildren(this.shapesLayer);
2019
- const erasing = this.interaction && this.interaction.kind === "erase" ? this.interaction.erased : null;
2020
- for (const shape of this.documentData.shapes) {
2021
- if (erasing && erasing.has(shape.id)) continue;
2022
- const el = this._renderShapeEl(shape, {});
2023
- if (el) this.shapesLayer.appendChild(el);
2024
- }
2362
+ this._renderSceneIncremental();
2025
2363
  }
2026
2364
  if (overlay) this._renderOverlay();
2027
2365
  }
2366
+ _renderSceneIncremental() {
2367
+ const shapes = this.documentData.shapes;
2368
+ const erasing = this.interaction && this.interaction.kind === "erase" ? this.interaction.erased : null;
2369
+ const currentIds = /* @__PURE__ */ new Set();
2370
+ for (const shape of shapes) {
2371
+ if (erasing && erasing.has(shape.id)) continue;
2372
+ currentIds.add(shape.id);
2373
+ }
2374
+ for (const [id, el] of this._shapeElements) {
2375
+ if (!currentIds.has(id)) {
2376
+ el.remove();
2377
+ this._shapeElements.delete(id);
2378
+ }
2379
+ }
2380
+ const fullRebuild = this._allDirty;
2381
+ let prevEl = null;
2382
+ for (const shape of shapes) {
2383
+ if (!currentIds.has(shape.id)) continue;
2384
+ const needsUpdate = fullRebuild || this._dirtyShapes.has(shape.id);
2385
+ let el = this._shapeElements.get(shape.id);
2386
+ if (needsUpdate || !el) {
2387
+ const newEl = this._renderShapeEl(shape, {});
2388
+ if (!newEl) {
2389
+ if (el) {
2390
+ el.remove();
2391
+ this._shapeElements.delete(shape.id);
2392
+ }
2393
+ continue;
2394
+ }
2395
+ if (el) {
2396
+ el.replaceWith(newEl);
2397
+ } else {
2398
+ if (prevEl && prevEl.parentNode === this.shapesLayer) {
2399
+ prevEl.after(newEl);
2400
+ } else {
2401
+ this.shapesLayer.prepend(newEl);
2402
+ }
2403
+ }
2404
+ el = newEl;
2405
+ this._shapeElements.set(shape.id, el);
2406
+ }
2407
+ const expected = prevEl ? prevEl.nextElementSibling : this.shapesLayer.firstElementChild;
2408
+ if (el !== expected) this.shapesLayer.insertBefore(el, expected);
2409
+ prevEl = el;
2410
+ }
2411
+ this._dirtyShapes.clear();
2412
+ this._allDirty = false;
2413
+ }
2028
2414
  // Colors are applied via inline `style` (which wins over the CSS class rules
2029
2415
  // and serializes self-contained), so a picked color always renders.
2030
- _renderShapeEl(shape, { standalone = false, colors = null }) {
2416
+ _renderShapeEl(shape, { standalone = false, colors = null, defsTarget = null } = {}) {
2031
2417
  let el = null;
2032
2418
  const setOpacity = (node) => {
2033
2419
  if (shape.opacity != null && shape.opacity !== 1)
@@ -2065,8 +2451,11 @@ var VdDraw = class {
2065
2451
  el.classList.add("vd-draw-shape");
2066
2452
  this._applyShapeStroke(el, shape, standalone, colors);
2067
2453
  setOpacity(el);
2068
- if (shape.arrowEnd) el.setAttribute("marker-end", `url(#${this._svgId("arrow")})`);
2069
- if (shape.arrowStart) el.setAttribute("marker-start", `url(#${this._svgId("arrow")})`);
2454
+ const strokeColor = shape.color || (standalone && colors ? colors.shapeStroke : "");
2455
+ const targetDefs = defsTarget || this.defsEl;
2456
+ const markerId = this._getArrowMarkerId(strokeColor, targetDefs);
2457
+ if (shape.arrowEnd) el.setAttribute("marker-end", `url(#${markerId})`);
2458
+ if (shape.arrowStart) el.setAttribute("marker-start", `url(#${markerId})`);
2070
2459
  } else if (shape.type === "text" || shape.type === "sticky") {
2071
2460
  el = createSvgEl("g");
2072
2461
  setOpacity(el);
@@ -2083,11 +2472,31 @@ var VdDraw = class {
2083
2472
  else if (standalone && colors) bg.style.fill = colors.sticky;
2084
2473
  el.appendChild(bg);
2085
2474
  }
2086
- const text = createSvgEl("text", { x: shape.x + 6, y: shape.y + 18 });
2475
+ const startX = (shape.x || 0) + (shape.type === "sticky" ? 10 : 6);
2476
+ const startY = (shape.y || 0) + (shape.type === "sticky" ? 14 : 18);
2477
+ const availWidth = shape.type === "sticky" ? Math.max(20, (shape.w || 160) - 20) : shape.w && shape.w > 20 ? shape.w - 12 : 0;
2478
+ const lines = wrapText(shape.text || "", availWidth);
2479
+ const text = createSvgEl("text", {
2480
+ x: startX,
2481
+ y: startY,
2482
+ "xml:space": "preserve"
2483
+ });
2087
2484
  text.classList.add("vd-draw-text");
2088
2485
  const fill = shape.color || (standalone && colors ? colors.text : "");
2089
2486
  if (fill) text.style.fill = fill;
2090
- text.textContent = shape.text || "";
2487
+ if (!lines.length) {
2488
+ text.textContent = "";
2489
+ } else {
2490
+ const lineHeight = 20;
2491
+ lines.forEach((lineText, idx) => {
2492
+ const tspan = createSvgEl("tspan", {
2493
+ x: startX,
2494
+ y: startY + idx * lineHeight
2495
+ });
2496
+ tspan.textContent = lineText;
2497
+ text.appendChild(tspan);
2498
+ });
2499
+ }
2091
2500
  el.appendChild(text);
2092
2501
  }
2093
2502
  if (el && !standalone) el.setAttribute("data-shape-id", shape.id);
@@ -2171,9 +2580,18 @@ var VdDraw = class {
2171
2580
  destroy() {
2172
2581
  if (this.destroyed) return;
2173
2582
  this.destroyed = true;
2583
+ if (this._rafId != null) {
2584
+ (typeof cancelAnimationFrame === "function" ? cancelAnimationFrame : clearTimeout)(
2585
+ this._rafId
2586
+ );
2587
+ this._rafId = null;
2588
+ }
2174
2589
  this.stopTextEdit({ commit: false });
2175
2590
  this._unbindEvents();
2176
2591
  this.listeners.clear();
2592
+ this._activePointers.clear();
2593
+ this._shapesById.clear();
2594
+ this._shapeElements.clear();
2177
2595
  if (this.element) this.element.replaceChildren();
2178
2596
  }
2179
2597
  };
@@ -2206,10 +2624,10 @@ var VdDraw2 = (0, import_vue.defineComponent)({
2206
2624
  setup(props, { emit, expose }) {
2207
2625
  const el = (0, import_vue.ref)(null);
2208
2626
  let instance = null;
2209
- const create = () => {
2627
+ const create = (savedDoc) => {
2210
2628
  instance = new VdDraw({
2211
2629
  element: el.value,
2212
- data: props.data,
2630
+ data: savedDoc || props.data,
2213
2631
  readonly: props.readonly,
2214
2632
  tool: props.tool,
2215
2633
  gridSize: props.gridSize,
@@ -2243,18 +2661,28 @@ var VdDraw2 = (0, import_vue.defineComponent)({
2243
2661
  (next) => instance?.setGridVisible(next)
2244
2662
  );
2245
2663
  (0, import_vue.watch)(
2246
- () => [
2247
- props.readonly,
2248
- props.gridSize,
2249
- props.snap,
2250
- props.autoFit,
2251
- props.history,
2252
- props.historyLimit
2253
- ],
2664
+ () => props.readonly,
2665
+ (next) => instance?.setReadonly(next)
2666
+ );
2667
+ (0, import_vue.watch)(
2668
+ () => props.snap,
2669
+ (next) => instance?.setSnap(next)
2670
+ );
2671
+ (0, import_vue.watch)(
2672
+ () => props.history,
2673
+ (next) => instance?.setHistoryEnabled(next)
2674
+ );
2675
+ (0, import_vue.watch)(
2676
+ () => props.historyLimit,
2677
+ (next) => instance?.setHistoryLimit(next)
2678
+ );
2679
+ (0, import_vue.watch)(
2680
+ () => [props.gridSize, props.autoFit],
2254
2681
  () => {
2255
2682
  if (!instance) return;
2683
+ const currentDoc = typeof instance.toJSON === "function" ? instance.toJSON() : void 0;
2256
2684
  instance.destroy();
2257
- create();
2685
+ create(currentDoc);
2258
2686
  }
2259
2687
  );
2260
2688
  (0, import_vue.onBeforeUnmount)(() => {
@@ -2266,6 +2694,10 @@ var VdDraw2 = (0, import_vue.defineComponent)({
2266
2694
  expose({
2267
2695
  getInstance: () => instance,
2268
2696
  setTool: (tool) => instance?.setTool(tool),
2697
+ setReadonly: (readonly) => instance?.setReadonly(readonly),
2698
+ setSnap: (snap) => instance?.setSnap(snap),
2699
+ setHistoryEnabled: (enabled) => instance?.setHistoryEnabled(enabled),
2700
+ setHistoryLimit: (limit) => instance?.setHistoryLimit(limit),
2269
2701
  undo: () => instance?.undo(),
2270
2702
  redo: () => instance?.redo(),
2271
2703
  canUndo: () => Boolean(instance?.canUndo()),