@vanduo-oss/vd3-cbun 1.3.2 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/README.md +24 -9
  3. package/SKILL.md +42 -15
  4. package/dist/charts/core.d.ts +4 -0
  5. package/dist/charts/index.cjs +197 -10
  6. package/dist/charts/index.cjs.map +3 -3
  7. package/dist/charts/index.js +197 -10
  8. package/dist/charts/index.js.map +3 -3
  9. package/dist/charts/vd3-charts.css +82 -0
  10. package/dist/charts/vue.d.ts +4 -0
  11. package/dist/code-editor/core.d.ts +19 -2
  12. package/dist/code-editor/highlight.cjs +1242 -0
  13. package/dist/code-editor/highlight.cjs.map +7 -0
  14. package/dist/code-editor/highlight.d.ts +6 -0
  15. package/dist/code-editor/highlight.js +1219 -0
  16. package/dist/code-editor/highlight.js.map +7 -0
  17. package/dist/code-editor/index.cjs +694 -70
  18. package/dist/code-editor/index.cjs.map +4 -4
  19. package/dist/code-editor/index.d.ts +2 -0
  20. package/dist/code-editor/index.js +694 -70
  21. package/dist/code-editor/index.js.map +4 -4
  22. package/dist/code-editor/vd3-code-editor.css +5 -0
  23. package/dist/draw/core.d.ts +7 -1
  24. package/dist/draw/index.cjs +470 -52
  25. package/dist/draw/index.cjs.map +2 -2
  26. package/dist/draw/index.js +470 -52
  27. package/dist/draw/index.js.map +2 -2
  28. package/dist/draw/vd3-draw.css +45 -0
  29. package/dist/draw/vue.d.ts +4 -0
  30. package/dist/hex-grid/core.d.ts +41 -0
  31. package/dist/hex-grid/index.cjs +308 -13
  32. package/dist/hex-grid/index.cjs.map +3 -3
  33. package/dist/hex-grid/index.d.ts +1 -0
  34. package/dist/hex-grid/index.js +308 -13
  35. package/dist/hex-grid/index.js.map +3 -3
  36. package/dist/hex-grid/vue.d.ts +4 -0
  37. package/dist/index.js +2 -2
  38. package/dist/index.js.map +2 -2
  39. package/dist/meta.json +177 -66
  40. package/package.json +15 -10
@@ -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,63 @@ 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
+ }
1150
+ this.render({ scene: false, overlay: true });
1151
+ return this;
1152
+ }
1153
+ setSnap(snap) {
1154
+ this.snap = Boolean(snap);
1155
+ this.options.snap = this.snap;
1156
+ return this;
1157
+ }
1158
+ setHistoryEnabled(enabled) {
1159
+ this.historyEnabled = Boolean(enabled);
1160
+ this.options.history = this.historyEnabled;
1161
+ if (!this.historyEnabled) {
1162
+ this.clearHistory();
1163
+ }
1164
+ return this;
1165
+ }
1166
+ setHistoryLimit(limit) {
1167
+ const num = Number(limit);
1168
+ this.historyLimit = Number.isFinite(num) && num > 0 ? num : 100;
1169
+ this.options.historyLimit = this.historyLimit;
1170
+ if (this.history.length > this.historyLimit + 1) {
1171
+ const drop = this.history.length - (this.historyLimit + 1);
1172
+ this.history.splice(0, drop);
1173
+ this.historyIndex = Math.max(0, this.historyIndex - drop);
1174
+ }
1175
+ return this;
1176
+ }
1040
1177
  // ── Tool + current style ─────────────────────────────────────────────────
1041
1178
  setTool(tool) {
1042
1179
  if (!DRAW_TOOLS.includes(tool)) return this;
1043
1180
  this.tool = tool;
1181
+ if (this.canvasEl) this.canvasEl.setAttribute("data-tool", tool);
1044
1182
  this._syncToolbar();
1045
1183
  return this;
1046
1184
  }
@@ -1074,7 +1212,7 @@ var VdDraw = class {
1074
1212
  }
1075
1213
  // ── Shape CRUD ─────────────────────────────────────────────────────────
1076
1214
  getShape(id) {
1077
- return this.documentData.shapes.find((s) => s.id === id) || null;
1215
+ return this._shapesById.get(id) || null;
1078
1216
  }
1079
1217
  getShapes() {
1080
1218
  return this.documentData.shapes.map((s) => deepClone(s));
@@ -1126,6 +1264,8 @@ var VdDraw = class {
1126
1264
  shape.text = typeof partial.text === "string" ? partial.text : "";
1127
1265
  }
1128
1266
  this.documentData.shapes.push(shape);
1267
+ this._shapesById.set(shape.id, shape);
1268
+ this._markDirty(shape.id);
1129
1269
  this.render();
1130
1270
  this._emitChange("shape:add", { shape: deepClone(shape), shapeId: shape.id });
1131
1271
  return shape;
@@ -1139,6 +1279,8 @@ var VdDraw = class {
1139
1279
  const shape = this.getShape(id);
1140
1280
  if (!shape || !patch) return null;
1141
1281
  Object.assign(shape, patch);
1282
+ this._shapesById.set(id, shape);
1283
+ this._markDirty(id);
1142
1284
  this.render();
1143
1285
  this._emitChange(options.reason || "shape:update", { shapeId: id }, options.reason ? id : null);
1144
1286
  return shape;
@@ -1147,7 +1289,9 @@ var VdDraw = class {
1147
1289
  const idx = this.documentData.shapes.findIndex((s) => s.id === id);
1148
1290
  if (idx === -1) return false;
1149
1291
  this.documentData.shapes.splice(idx, 1);
1292
+ this._shapesById.delete(id);
1150
1293
  this.selectedIds.delete(id);
1294
+ this._markDirty(id);
1151
1295
  this.render();
1152
1296
  this._emitChange("shape:delete", { shapeId: id });
1153
1297
  return true;
@@ -1214,11 +1358,13 @@ var VdDraw = class {
1214
1358
  _replaceShape(next) {
1215
1359
  const idx = this.documentData.shapes.findIndex((s) => s.id === next.id);
1216
1360
  if (idx !== -1) this.documentData.shapes[idx] = next;
1361
+ this._shapesById.set(next.id, next);
1217
1362
  }
1218
1363
  nudge(dx, dy) {
1219
1364
  const selected = this.getSelectedShapes();
1220
1365
  if (!selected.length) return this;
1221
1366
  for (const shape of selected) this._replaceShape(translateShape(shape, dx, dy));
1367
+ this._markAllDirty();
1222
1368
  this.render();
1223
1369
  this._emitChange(
1224
1370
  "shape:nudge",
@@ -1237,6 +1383,7 @@ var VdDraw = class {
1237
1383
  const scaled = scaleShape(shape, cur.x, cur.y, sx, sy);
1238
1384
  this._replaceShape(translateShape(scaled, target.x - cur.x, target.y - cur.y));
1239
1385
  }
1386
+ this._markAllDirty();
1240
1387
  this.render();
1241
1388
  this._emitChange("shape:resize", { shapeIds: [...this.selectedIds] });
1242
1389
  return this;
@@ -1244,8 +1391,10 @@ var VdDraw = class {
1244
1391
  deleteSelection() {
1245
1392
  if (this.selectedIds.size === 0) return false;
1246
1393
  const ids = [...this.selectedIds];
1394
+ for (const id of ids) this._shapesById.delete(id);
1247
1395
  this.documentData.shapes = this.documentData.shapes.filter((s) => !this.selectedIds.has(s.id));
1248
1396
  this.selectedIds.clear();
1397
+ this._markAllDirty();
1249
1398
  this.render();
1250
1399
  this._emitChange("shape:delete", { shapeIds: ids });
1251
1400
  return true;
@@ -1257,6 +1406,7 @@ var VdDraw = class {
1257
1406
  for (const shape of selected) {
1258
1407
  for (const key of allowed) if (key in patch) shape[key] = patch[key];
1259
1408
  }
1409
+ this._markAllDirty();
1260
1410
  this.render();
1261
1411
  this._emitChange(
1262
1412
  "shape:style",
@@ -1269,6 +1419,7 @@ var VdDraw = class {
1269
1419
  _reorder(mutator) {
1270
1420
  if (this.selectedIds.size === 0) return this;
1271
1421
  mutator();
1422
+ this._markAllDirty();
1272
1423
  this.render();
1273
1424
  this._emitChange("shape:reorder", { shapeIds: [...this.selectedIds] });
1274
1425
  return this;
@@ -1311,6 +1462,7 @@ var VdDraw = class {
1311
1462
  if (selected.length < 2) return this;
1312
1463
  const groupId = createId("grp");
1313
1464
  for (const shape of selected) shape.groupId = groupId;
1465
+ this._markAllDirty();
1314
1466
  this.render();
1315
1467
  this._emitChange("shape:group", { groupId, shapeIds: [...this.selectedIds] });
1316
1468
  return this;
@@ -1325,6 +1477,7 @@ var VdDraw = class {
1325
1477
  }
1326
1478
  }
1327
1479
  if (!changed) return this;
1480
+ this._markAllDirty();
1328
1481
  this.render();
1329
1482
  this._emitChange("shape:ungroup", { shapeIds: [...this.selectedIds] });
1330
1483
  return this;
@@ -1351,9 +1504,11 @@ var VdDraw = class {
1351
1504
  copy.groupId = groupRemap.get(copy.groupId);
1352
1505
  }
1353
1506
  this.documentData.shapes.push(copy);
1507
+ this._shapesById.set(copy.id, copy);
1354
1508
  newIds.push(copy.id);
1355
1509
  }
1356
1510
  this.selectedIds = new Set(newIds);
1511
+ this._markAllDirty();
1357
1512
  this.render();
1358
1513
  this._emitChange("shape:paste", { shapeIds: newIds });
1359
1514
  this._emitSelect();
@@ -1415,6 +1570,30 @@ var VdDraw = class {
1415
1570
  sticky: read("--vd-draw-sticky-fill", "#fdf3c4")
1416
1571
  };
1417
1572
  }
1573
+ _getArrowMarkerId(color, defsTarget = this.defsEl) {
1574
+ const isDefault = !color;
1575
+ const safeKey = isDefault ? "arrow" : "arrow-" + color.toLowerCase().replace(/[^a-z0-9_-]/g, "_");
1576
+ const id = this._svgId(safeKey);
1577
+ if (defsTarget && !defsTarget.querySelector(`#${id}`)) {
1578
+ const marker = createSvgEl("marker", {
1579
+ id,
1580
+ viewBox: "0 0 10 10",
1581
+ refX: 8,
1582
+ refY: 5,
1583
+ markerWidth: 7,
1584
+ markerHeight: 7,
1585
+ orient: "auto-start-reverse"
1586
+ });
1587
+ marker.appendChild(
1588
+ createSvgEl("path", {
1589
+ d: "M 0 0 L 10 5 L 0 10 z",
1590
+ fill: color || "var(--vd-draw-shape-stroke)"
1591
+ })
1592
+ );
1593
+ defsTarget.appendChild(marker);
1594
+ }
1595
+ return id;
1596
+ }
1418
1597
  toSVG() {
1419
1598
  const shapes = this.documentData.shapes;
1420
1599
  const bounds = boundsOfShapes(shapes) || { x: 0, y: 0, w: 100, h: 100 };
@@ -1432,10 +1611,14 @@ var VdDraw = class {
1432
1611
  height: round(vb.h),
1433
1612
  viewBox: `${round(vb.x)} ${round(vb.y)} ${round(vb.w)} ${round(vb.h)}`
1434
1613
  });
1614
+ const defs = createSvgEl("defs");
1435
1615
  for (const shape of shapes) {
1436
- const el = this._renderShapeEl(shape, { standalone: true, colors });
1616
+ const el = this._renderShapeEl(shape, { standalone: true, colors, defsTarget: defs });
1437
1617
  if (el) svg.appendChild(el);
1438
1618
  }
1619
+ if (defs.childNodes.length > 0) {
1620
+ svg.insertBefore(defs, svg.firstChild);
1621
+ }
1439
1622
  return new XMLSerializer().serializeToString(svg);
1440
1623
  }
1441
1624
  toPNG({ scale = 2 } = {}) {
@@ -1529,7 +1712,9 @@ var VdDraw = class {
1529
1712
  const viewport = { ...this.documentData.viewport };
1530
1713
  this.documentData = deepClone(snapshot);
1531
1714
  this.documentData.viewport = viewport;
1715
+ this._rebuildShapeIndex();
1532
1716
  this._syncSelectionValidity();
1717
+ this._markAllDirty();
1533
1718
  this.render();
1534
1719
  this.emit("change", { reason, document: this.toJSON() });
1535
1720
  this.emit("history", { reason, canUndo: this.canUndo(), canRedo: this.canRedo() });
@@ -1545,8 +1730,10 @@ var VdDraw = class {
1545
1730
  }
1546
1731
  load(data) {
1547
1732
  this.documentData = normalizeDocument(data);
1733
+ this._rebuildShapeIndex();
1548
1734
  this.selectedIds.clear();
1549
1735
  this._resetHistory();
1736
+ this._markAllDirty();
1550
1737
  this.render();
1551
1738
  this._emitChange("load");
1552
1739
  this._emitSelect();
@@ -1555,7 +1742,9 @@ var VdDraw = class {
1555
1742
  clear() {
1556
1743
  if (!this.documentData.shapes.length) return this;
1557
1744
  this.documentData.shapes = [];
1745
+ this._shapesById.clear();
1558
1746
  this.selectedIds.clear();
1747
+ this._markAllDirty();
1559
1748
  this.render();
1560
1749
  this._emitChange("clear");
1561
1750
  return this;
@@ -1580,10 +1769,16 @@ var VdDraw = class {
1580
1769
  }
1581
1770
  _positionTextEditor(shape, editor) {
1582
1771
  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`;
1772
+ const scale = vp.scale || 1;
1773
+ editor.style.left = `${vp.x + shape.x * scale}px`;
1774
+ editor.style.top = `${vp.y + shape.y * scale}px`;
1775
+ editor.style.width = `${shape.w * scale}px`;
1776
+ editor.style.height = `${shape.h * scale}px`;
1777
+ editor.style.fontSize = `${16 * scale}px`;
1778
+ editor.style.lineHeight = `${20 * scale}px`;
1779
+ if (shape.type === "sticky") {
1780
+ editor.style.background = shape.fill || "var(--vd-draw-sticky-fill)";
1781
+ }
1587
1782
  }
1588
1783
  stopTextEdit({ commit = true } = {}) {
1589
1784
  if (!this.textEditor) return;
@@ -1595,6 +1790,7 @@ var VdDraw = class {
1595
1790
  const shape = this.getShape(id);
1596
1791
  if (shape && shape.text !== value) {
1597
1792
  shape.text = value;
1793
+ this._markDirty(id);
1598
1794
  this.render();
1599
1795
  this._emitChange("shape:text", { shapeId: id }, `text:${id}`);
1600
1796
  }
@@ -1652,11 +1848,56 @@ var VdDraw = class {
1652
1848
  if (this.destroyed || event.button != null && event.button !== 0) return;
1653
1849
  this.canvasEl.focus();
1654
1850
  this.stopTextEdit({ commit: true });
1851
+ this._activePointers.set(event.pointerId, { clientX: event.clientX, clientY: event.clientY });
1852
+ if (this._activePointers.size === 2) {
1853
+ if (this.interaction) {
1854
+ const it = this.interaction;
1855
+ if (it.kind === "freehand" || it.kind === "create-line" || it.kind === "create-box") {
1856
+ this.documentData.shapes = this.documentData.shapes.filter((s) => s.id !== it.shapeId);
1857
+ this._shapesById.delete(it.shapeId);
1858
+ const el = this._shapeElements.get(it.shapeId);
1859
+ if (el) {
1860
+ el.remove();
1861
+ this._shapeElements.delete(it.shapeId);
1862
+ }
1863
+ this._scheduleRender({ scene: true, overlay: false });
1864
+ } else if (it.kind === "move" || it.kind === "resize") {
1865
+ for (const orig of it.originals) this._replaceShape(orig);
1866
+ this._scheduleRender({ scene: true });
1867
+ } else if (it.kind === "marquee") {
1868
+ clearChildren(this.marqueeLayer);
1869
+ }
1870
+ }
1871
+ const pts = [...this._activePointers.values()];
1872
+ const p1 = pts[0];
1873
+ const p2 = pts[1];
1874
+ const initialDistance = Math.hypot(p2.clientX - p1.clientX, p2.clientY - p1.clientY) || 1;
1875
+ const initialClientMid = {
1876
+ x: (p1.clientX + p2.clientX) / 2,
1877
+ y: (p1.clientY + p2.clientY) / 2
1878
+ };
1879
+ const initialLocalMid = this._clientToLocal(initialClientMid.x, initialClientMid.y);
1880
+ const initialVp = { ...this.documentData.viewport };
1881
+ const currentScale = initialVp.scale || 1;
1882
+ const worldMid = {
1883
+ x: (initialLocalMid.x - initialVp.x) / currentScale,
1884
+ y: (initialLocalMid.y - initialVp.y) / currentScale
1885
+ };
1886
+ this.interaction = {
1887
+ kind: "pinch",
1888
+ initialDistance,
1889
+ worldMid,
1890
+ initialScale: currentScale
1891
+ };
1892
+ if (typeof event.preventDefault === "function") event.preventDefault();
1893
+ return;
1894
+ }
1655
1895
  const world = this._clientToWorld(event.clientX, event.clientY);
1656
1896
  const shapeTarget = event.target.closest("[data-shape-id]");
1657
1897
  const handleTarget = event.target.closest("[data-handle]");
1658
1898
  if (this.tool === "hand") {
1659
1899
  this.interaction = this._beginPan(event);
1900
+ if (this.canvasEl) this.canvasEl.classList.add("vd-draw-panning");
1660
1901
  this._capture(event.pointerId);
1661
1902
  return;
1662
1903
  }
@@ -1667,6 +1908,15 @@ var VdDraw = class {
1667
1908
  return;
1668
1909
  }
1669
1910
  if (this.tool === "select") {
1911
+ if (event.detail === 2 && shapeTarget && !this.readonly) {
1912
+ const id = shapeTarget.getAttribute("data-shape-id");
1913
+ const shape = this.getShape(id);
1914
+ if (shape && (shape.type === "text" || shape.type === "sticky")) {
1915
+ this.select(id);
1916
+ this.startTextEdit(id);
1917
+ return;
1918
+ }
1919
+ }
1670
1920
  if (handleTarget && this.selectedIds.size) {
1671
1921
  this.interaction = {
1672
1922
  kind: "resize",
@@ -1730,6 +1980,7 @@ var VdDraw = class {
1730
1980
  points: [[round(world.x), round(world.y), pressure]]
1731
1981
  };
1732
1982
  this.documentData.shapes.push(shape2);
1983
+ this._shapesById.set(shape2.id, shape2);
1733
1984
  return { kind: "freehand", pointerId: event.pointerId, shapeId: shape2.id };
1734
1985
  }
1735
1986
  if (tool === "line") {
@@ -1747,6 +1998,7 @@ var VdDraw = class {
1747
1998
  opacity: this.style.opacity
1748
1999
  };
1749
2000
  this.documentData.shapes.push(shape2);
2001
+ this._shapesById.set(shape2.id, shape2);
1750
2002
  return { kind: "create-line", pointerId: event.pointerId, shapeId: shape2.id };
1751
2003
  }
1752
2004
  const type = tool;
@@ -1764,6 +2016,7 @@ var VdDraw = class {
1764
2016
  };
1765
2017
  if (type === "text" || type === "sticky") shape.text = "";
1766
2018
  this.documentData.shapes.push(shape);
2019
+ this._shapesById.set(shape.id, shape);
1767
2020
  return { kind: "create-box", pointerId: event.pointerId, shapeId: shape.id, start: world };
1768
2021
  }
1769
2022
  _applyErase(world) {
@@ -1778,16 +2031,44 @@ var VdDraw = class {
1778
2031
  changed = true;
1779
2032
  }
1780
2033
  }
1781
- if (changed) this.render({ scene: true, overlay: false });
2034
+ if (changed) {
2035
+ for (const id of it.erased) this._markDirty(id);
2036
+ this._scheduleRender({ scene: true, overlay: false });
2037
+ }
1782
2038
  }
1783
2039
  _handlePointerMove(event) {
2040
+ if (this._activePointers.has(event.pointerId)) {
2041
+ this._activePointers.set(event.pointerId, { clientX: event.clientX, clientY: event.clientY });
2042
+ }
1784
2043
  const it = this.interaction;
1785
2044
  if (!it) return;
2045
+ if (it.kind === "pinch") {
2046
+ if (this._activePointers.size >= 2) {
2047
+ if (typeof event.preventDefault === "function") event.preventDefault();
2048
+ const pts = [...this._activePointers.values()];
2049
+ const p1 = pts[0];
2050
+ const p2 = pts[1];
2051
+ const curDistance = Math.hypot(p2.clientX - p1.clientX, p2.clientY - p1.clientY) || 1;
2052
+ const curClientMid = {
2053
+ x: (p1.clientX + p2.clientX) / 2,
2054
+ y: (p1.clientY + p2.clientY) / 2
2055
+ };
2056
+ const curLocalMid = this._clientToLocal(curClientMid.x, curClientMid.y);
2057
+ const distRatio = curDistance / it.initialDistance;
2058
+ const newScale = clamp(it.initialScale * distRatio, MIN_SCALE, MAX_SCALE);
2059
+ const vp = this.documentData.viewport;
2060
+ vp.scale = newScale;
2061
+ vp.x = curLocalMid.x - it.worldMid.x * newScale;
2062
+ vp.y = curLocalMid.y - it.worldMid.y * newScale;
2063
+ this._scheduleRender({ scene: false });
2064
+ }
2065
+ return;
2066
+ }
1786
2067
  if (it.kind === "pan") {
1787
2068
  const vp = this.documentData.viewport;
1788
2069
  vp.x = it.startX + (event.clientX - it.startClientX);
1789
2070
  vp.y = it.startY + (event.clientY - it.startClientY);
1790
- this.render({ scene: false });
2071
+ this._scheduleRender({ scene: false });
1791
2072
  return;
1792
2073
  }
1793
2074
  const world = this._clientToWorld(event.clientX, event.clientY);
@@ -1805,8 +2086,9 @@ var VdDraw = class {
1805
2086
  if (snap.dx || snap.dy)
1806
2087
  for (const orig of it.originals)
1807
2088
  this._replaceShape(translateShape(orig, dx + snap.dx, dy + snap.dy));
2089
+ for (const orig of it.originals) this._markDirty(orig.id);
1808
2090
  this._renderGuides(snap.guides);
1809
- this.render({ scene: true, guides: false });
2091
+ this._scheduleRender({ scene: true, guides: false });
1810
2092
  return;
1811
2093
  }
1812
2094
  if (it.kind === "resize") {
@@ -1821,9 +2103,10 @@ var VdDraw = class {
1821
2103
  for (const orig of it.originals) {
1822
2104
  const scaled = scaleShape(orig, it.startBounds.x, it.startBounds.y, sx, sy);
1823
2105
  this._replaceShape(translateShape(scaled, t.x - it.startBounds.x, t.y - it.startBounds.y));
2106
+ this._markDirty(orig.id);
1824
2107
  }
1825
2108
  it.moved = true;
1826
- this.render();
2109
+ this._scheduleRender();
1827
2110
  return;
1828
2111
  }
1829
2112
  if (it.kind === "marquee") {
@@ -1834,8 +2117,14 @@ var VdDraw = class {
1834
2117
  if (it.kind === "freehand") {
1835
2118
  const shape = this.getShape(it.shapeId);
1836
2119
  if (shape) {
1837
- shape.points.push([round(world.x), round(world.y), event.pressure || 0.5]);
1838
- this.render({ scene: true });
2120
+ const raw = [round(world.x), round(world.y), event.pressure || 0.5];
2121
+ const preset = BRUSH_PRESETS[shape.brush] || BRUSH_PRESETS[DEFAULT_BRUSH];
2122
+ const smoothFactor = 1 - (preset.smoothing || 0.5) * 0.6;
2123
+ const prev = shape.points[shape.points.length - 1];
2124
+ const pt = prev ? smoothPoint(prev, raw, smoothFactor) : raw;
2125
+ appendAndSimplify(shape.points, pt);
2126
+ this._markDirty(it.shapeId);
2127
+ this._scheduleRender({ scene: true });
1839
2128
  }
1840
2129
  return;
1841
2130
  }
@@ -1843,7 +2132,8 @@ var VdDraw = class {
1843
2132
  const shape = this.getShape(it.shapeId);
1844
2133
  if (shape) {
1845
2134
  shape.points[1] = [round(world.x), round(world.y)];
1846
- this.render({ scene: true });
2135
+ this._markDirty(it.shapeId);
2136
+ this._scheduleRender({ scene: true });
1847
2137
  }
1848
2138
  return;
1849
2139
  }
@@ -1854,7 +2144,8 @@ var VdDraw = class {
1854
2144
  shape.y = round(Math.min(it.start.y, world.y));
1855
2145
  shape.w = round(Math.max(1, Math.abs(world.x - it.start.x)));
1856
2146
  shape.h = round(Math.max(1, Math.abs(world.y - it.start.y)));
1857
- this.render({ scene: true });
2147
+ this._markDirty(it.shapeId);
2148
+ this._scheduleRender({ scene: true });
1858
2149
  }
1859
2150
  }
1860
2151
  }
@@ -1873,9 +2164,18 @@ var VdDraw = class {
1873
2164
  return { x, y, w, h: h2 };
1874
2165
  }
1875
2166
  _handlePointerUp(event) {
2167
+ this._activePointers.delete(event.pointerId);
1876
2168
  const it = this.interaction;
1877
2169
  if (!it) return;
2170
+ if (it.kind === "pinch") {
2171
+ if (this._activePointers.size < 2) {
2172
+ this.interaction = null;
2173
+ this._emitViewportChange("viewport:pinch");
2174
+ }
2175
+ return;
2176
+ }
1878
2177
  this.interaction = null;
2178
+ if (this.canvasEl) this.canvasEl.classList.remove("vd-draw-panning");
1879
2179
  if (typeof this.canvasEl.releasePointerCapture === "function" && this.canvasEl.hasPointerCapture?.(event.pointerId)) {
1880
2180
  try {
1881
2181
  this.canvasEl.releasePointerCapture(event.pointerId);
@@ -1891,18 +2191,22 @@ var VdDraw = class {
1891
2191
  if (it.kind === "erase") {
1892
2192
  if (it.erased.size) {
1893
2193
  const ids = [...it.erased];
2194
+ for (const id of ids) this._shapesById.delete(id);
1894
2195
  this.documentData.shapes = this.documentData.shapes.filter((s) => !it.erased.has(s.id));
2196
+ this._markAllDirty();
1895
2197
  this.render();
1896
2198
  this._emitChange("shape:erase", { shapeIds: ids });
1897
2199
  }
1898
2200
  return;
1899
2201
  }
1900
2202
  if (it.kind === "move" && it.moved) {
2203
+ this._markAllDirty();
1901
2204
  this.render();
1902
2205
  this._emitChange("shape:move", { shapeIds: [...this.selectedIds] });
1903
2206
  return;
1904
2207
  }
1905
2208
  if (it.kind === "resize" && it.moved) {
2209
+ this._markAllDirty();
1906
2210
  this.render();
1907
2211
  this._emitChange("shape:resize", { shapeIds: [...this.selectedIds] });
1908
2212
  return;
@@ -1919,6 +2223,8 @@ var VdDraw = class {
1919
2223
  const b = shapeBounds(shape);
1920
2224
  if (it.kind !== "freehand" && b.w < 2 && b.h < 2 && shape.type !== "text" && shape.type !== "sticky") {
1921
2225
  this.documentData.shapes = this.documentData.shapes.filter((s) => s.id !== it.shapeId);
2226
+ this._shapesById.delete(it.shapeId);
2227
+ this._markAllDirty();
1922
2228
  this.render();
1923
2229
  return;
1924
2230
  }
@@ -1932,6 +2238,7 @@ var VdDraw = class {
1932
2238
  this.select(shape.id);
1933
2239
  this.setTool("select");
1934
2240
  }
2241
+ this._markDirty(shape.id);
1935
2242
  this.render();
1936
2243
  this._emitChange("shape:add", { shape: deepClone(shape), shapeId: shape.id });
1937
2244
  if (shape.type === "text" || shape.type === "sticky") this.startTextEdit(shape.id);
@@ -2008,26 +2315,91 @@ var VdDraw = class {
2008
2315
  if (this.destroyed) return;
2009
2316
  this.render({ scene: false });
2010
2317
  }
2318
+ // ── Dirty tracking ──────────────────────────────────────────────────────
2319
+ _markDirty(shapeId) {
2320
+ this._dirtyShapes.add(shapeId);
2321
+ }
2322
+ _markAllDirty() {
2323
+ this._allDirty = true;
2324
+ }
2325
+ // ── rAF-batched render ─────────────────────────────────────────────────
2326
+ _scheduleRender(flags = {}) {
2327
+ this._pendingFlags = {
2328
+ scene: (this._pendingFlags?.scene ?? false) || flags.scene !== false,
2329
+ overlay: (this._pendingFlags?.overlay ?? false) || flags.overlay !== false
2330
+ };
2331
+ if (this._rafId != null) return;
2332
+ this._rafId = (typeof requestAnimationFrame === "function" ? requestAnimationFrame : setTimeout)(() => {
2333
+ this._rafId = null;
2334
+ const f = this._pendingFlags || {};
2335
+ this._pendingFlags = null;
2336
+ this.render(f);
2337
+ });
2338
+ }
2011
2339
  // ── Rendering ────────────────────────────────────────────────────────────
2012
2340
  render(flags = {}) {
2013
2341
  if (this.destroyed) return;
2014
2342
  const { scene = true, overlay = true } = flags;
2015
2343
  const vp = this.documentData.viewport;
2016
2344
  this.world.setAttribute("transform", `matrix(${vp.scale} 0 0 ${vp.scale} ${vp.x} ${vp.y})`);
2345
+ if (this.textEditor) {
2346
+ const activeShape = this.getShape(this.textEditor.id);
2347
+ if (activeShape) this._positionTextEditor(activeShape, this.textEditor.el);
2348
+ }
2017
2349
  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
- }
2350
+ this._renderSceneIncremental();
2025
2351
  }
2026
2352
  if (overlay) this._renderOverlay();
2027
2353
  }
2354
+ _renderSceneIncremental() {
2355
+ const shapes = this.documentData.shapes;
2356
+ const erasing = this.interaction && this.interaction.kind === "erase" ? this.interaction.erased : null;
2357
+ const currentIds = /* @__PURE__ */ new Set();
2358
+ for (const shape of shapes) {
2359
+ if (erasing && erasing.has(shape.id)) continue;
2360
+ currentIds.add(shape.id);
2361
+ }
2362
+ for (const [id, el] of this._shapeElements) {
2363
+ if (!currentIds.has(id)) {
2364
+ el.remove();
2365
+ this._shapeElements.delete(id);
2366
+ }
2367
+ }
2368
+ const fullRebuild = this._allDirty;
2369
+ let prevEl = null;
2370
+ for (const shape of shapes) {
2371
+ if (!currentIds.has(shape.id)) continue;
2372
+ const needsUpdate = fullRebuild || this._dirtyShapes.has(shape.id);
2373
+ let el = this._shapeElements.get(shape.id);
2374
+ if (needsUpdate || !el) {
2375
+ const newEl = this._renderShapeEl(shape, {});
2376
+ if (!newEl) {
2377
+ if (el) {
2378
+ el.remove();
2379
+ this._shapeElements.delete(shape.id);
2380
+ }
2381
+ continue;
2382
+ }
2383
+ if (el) {
2384
+ el.replaceWith(newEl);
2385
+ } else {
2386
+ if (prevEl && prevEl.parentNode === this.shapesLayer) {
2387
+ prevEl.after(newEl);
2388
+ } else {
2389
+ this.shapesLayer.prepend(newEl);
2390
+ }
2391
+ }
2392
+ el = newEl;
2393
+ this._shapeElements.set(shape.id, el);
2394
+ }
2395
+ prevEl = el;
2396
+ }
2397
+ this._dirtyShapes.clear();
2398
+ this._allDirty = false;
2399
+ }
2028
2400
  // Colors are applied via inline `style` (which wins over the CSS class rules
2029
2401
  // and serializes self-contained), so a picked color always renders.
2030
- _renderShapeEl(shape, { standalone = false, colors = null }) {
2402
+ _renderShapeEl(shape, { standalone = false, colors = null, defsTarget = null } = {}) {
2031
2403
  let el = null;
2032
2404
  const setOpacity = (node) => {
2033
2405
  if (shape.opacity != null && shape.opacity !== 1)
@@ -2065,8 +2437,11 @@ var VdDraw = class {
2065
2437
  el.classList.add("vd-draw-shape");
2066
2438
  this._applyShapeStroke(el, shape, standalone, colors);
2067
2439
  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")})`);
2440
+ const strokeColor = shape.color || (standalone && colors ? colors.shapeStroke : "");
2441
+ const targetDefs = defsTarget || this.defsEl;
2442
+ const markerId = this._getArrowMarkerId(strokeColor, targetDefs);
2443
+ if (shape.arrowEnd) el.setAttribute("marker-end", `url(#${markerId})`);
2444
+ if (shape.arrowStart) el.setAttribute("marker-start", `url(#${markerId})`);
2070
2445
  } else if (shape.type === "text" || shape.type === "sticky") {
2071
2446
  el = createSvgEl("g");
2072
2447
  setOpacity(el);
@@ -2083,11 +2458,31 @@ var VdDraw = class {
2083
2458
  else if (standalone && colors) bg.style.fill = colors.sticky;
2084
2459
  el.appendChild(bg);
2085
2460
  }
2086
- const text = createSvgEl("text", { x: shape.x + 6, y: shape.y + 18 });
2461
+ const startX = (shape.x || 0) + (shape.type === "sticky" ? 10 : 6);
2462
+ const startY = (shape.y || 0) + (shape.type === "sticky" ? 14 : 18);
2463
+ const availWidth = shape.type === "sticky" ? Math.max(20, (shape.w || 160) - 20) : shape.w && shape.w > 20 ? shape.w - 12 : 0;
2464
+ const lines = wrapText(shape.text || "", availWidth);
2465
+ const text = createSvgEl("text", {
2466
+ x: startX,
2467
+ y: startY,
2468
+ "xml:space": "preserve"
2469
+ });
2087
2470
  text.classList.add("vd-draw-text");
2088
2471
  const fill = shape.color || (standalone && colors ? colors.text : "");
2089
2472
  if (fill) text.style.fill = fill;
2090
- text.textContent = shape.text || "";
2473
+ if (!lines.length) {
2474
+ text.textContent = "";
2475
+ } else {
2476
+ const lineHeight = 20;
2477
+ lines.forEach((lineText, idx) => {
2478
+ const tspan = createSvgEl("tspan", {
2479
+ x: startX,
2480
+ y: startY + idx * lineHeight
2481
+ });
2482
+ tspan.textContent = lineText;
2483
+ text.appendChild(tspan);
2484
+ });
2485
+ }
2091
2486
  el.appendChild(text);
2092
2487
  }
2093
2488
  if (el && !standalone) el.setAttribute("data-shape-id", shape.id);
@@ -2171,9 +2566,18 @@ var VdDraw = class {
2171
2566
  destroy() {
2172
2567
  if (this.destroyed) return;
2173
2568
  this.destroyed = true;
2569
+ if (this._rafId != null) {
2570
+ (typeof cancelAnimationFrame === "function" ? cancelAnimationFrame : clearTimeout)(
2571
+ this._rafId
2572
+ );
2573
+ this._rafId = null;
2574
+ }
2174
2575
  this.stopTextEdit({ commit: false });
2175
2576
  this._unbindEvents();
2176
2577
  this.listeners.clear();
2578
+ this._activePointers.clear();
2579
+ this._shapesById.clear();
2580
+ this._shapeElements.clear();
2177
2581
  if (this.element) this.element.replaceChildren();
2178
2582
  }
2179
2583
  };
@@ -2206,10 +2610,10 @@ var VdDraw2 = (0, import_vue.defineComponent)({
2206
2610
  setup(props, { emit, expose }) {
2207
2611
  const el = (0, import_vue.ref)(null);
2208
2612
  let instance = null;
2209
- const create = () => {
2613
+ const create = (savedDoc) => {
2210
2614
  instance = new VdDraw({
2211
2615
  element: el.value,
2212
- data: props.data,
2616
+ data: savedDoc || props.data,
2213
2617
  readonly: props.readonly,
2214
2618
  tool: props.tool,
2215
2619
  gridSize: props.gridSize,
@@ -2243,18 +2647,28 @@ var VdDraw2 = (0, import_vue.defineComponent)({
2243
2647
  (next) => instance?.setGridVisible(next)
2244
2648
  );
2245
2649
  (0, import_vue.watch)(
2246
- () => [
2247
- props.readonly,
2248
- props.gridSize,
2249
- props.snap,
2250
- props.autoFit,
2251
- props.history,
2252
- props.historyLimit
2253
- ],
2650
+ () => props.readonly,
2651
+ (next) => instance?.setReadonly(next)
2652
+ );
2653
+ (0, import_vue.watch)(
2654
+ () => props.snap,
2655
+ (next) => instance?.setSnap(next)
2656
+ );
2657
+ (0, import_vue.watch)(
2658
+ () => props.history,
2659
+ (next) => instance?.setHistoryEnabled(next)
2660
+ );
2661
+ (0, import_vue.watch)(
2662
+ () => props.historyLimit,
2663
+ (next) => instance?.setHistoryLimit(next)
2664
+ );
2665
+ (0, import_vue.watch)(
2666
+ () => [props.gridSize, props.autoFit],
2254
2667
  () => {
2255
2668
  if (!instance) return;
2669
+ const currentDoc = typeof instance.toJSON === "function" ? instance.toJSON() : void 0;
2256
2670
  instance.destroy();
2257
- create();
2671
+ create(currentDoc);
2258
2672
  }
2259
2673
  );
2260
2674
  (0, import_vue.onBeforeUnmount)(() => {
@@ -2266,6 +2680,10 @@ var VdDraw2 = (0, import_vue.defineComponent)({
2266
2680
  expose({
2267
2681
  getInstance: () => instance,
2268
2682
  setTool: (tool) => instance?.setTool(tool),
2683
+ setReadonly: (readonly) => instance?.setReadonly(readonly),
2684
+ setSnap: (snap) => instance?.setSnap(snap),
2685
+ setHistoryEnabled: (enabled) => instance?.setHistoryEnabled(enabled),
2686
+ setHistoryLimit: (limit) => instance?.setHistoryLimit(limit),
2269
2687
  undo: () => instance?.undo(),
2270
2688
  redo: () => instance?.redo(),
2271
2689
  canUndo: () => Boolean(instance?.canUndo()),