@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
@@ -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,63 @@ 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
+ }
1119
+ this.render({ scene: false, overlay: true });
1120
+ return this;
1121
+ }
1122
+ setSnap(snap) {
1123
+ this.snap = Boolean(snap);
1124
+ this.options.snap = this.snap;
1125
+ return this;
1126
+ }
1127
+ setHistoryEnabled(enabled) {
1128
+ this.historyEnabled = Boolean(enabled);
1129
+ this.options.history = this.historyEnabled;
1130
+ if (!this.historyEnabled) {
1131
+ this.clearHistory();
1132
+ }
1133
+ return this;
1134
+ }
1135
+ setHistoryLimit(limit) {
1136
+ const num = Number(limit);
1137
+ this.historyLimit = Number.isFinite(num) && num > 0 ? num : 100;
1138
+ this.options.historyLimit = this.historyLimit;
1139
+ if (this.history.length > this.historyLimit + 1) {
1140
+ const drop = this.history.length - (this.historyLimit + 1);
1141
+ this.history.splice(0, drop);
1142
+ this.historyIndex = Math.max(0, this.historyIndex - drop);
1143
+ }
1144
+ return this;
1145
+ }
1009
1146
  // ── Tool + current style ─────────────────────────────────────────────────
1010
1147
  setTool(tool) {
1011
1148
  if (!DRAW_TOOLS.includes(tool)) return this;
1012
1149
  this.tool = tool;
1150
+ if (this.canvasEl) this.canvasEl.setAttribute("data-tool", tool);
1013
1151
  this._syncToolbar();
1014
1152
  return this;
1015
1153
  }
@@ -1043,7 +1181,7 @@ var VdDraw = class {
1043
1181
  }
1044
1182
  // ── Shape CRUD ─────────────────────────────────────────────────────────
1045
1183
  getShape(id) {
1046
- return this.documentData.shapes.find((s) => s.id === id) || null;
1184
+ return this._shapesById.get(id) || null;
1047
1185
  }
1048
1186
  getShapes() {
1049
1187
  return this.documentData.shapes.map((s) => deepClone(s));
@@ -1095,6 +1233,8 @@ var VdDraw = class {
1095
1233
  shape.text = typeof partial.text === "string" ? partial.text : "";
1096
1234
  }
1097
1235
  this.documentData.shapes.push(shape);
1236
+ this._shapesById.set(shape.id, shape);
1237
+ this._markDirty(shape.id);
1098
1238
  this.render();
1099
1239
  this._emitChange("shape:add", { shape: deepClone(shape), shapeId: shape.id });
1100
1240
  return shape;
@@ -1108,6 +1248,8 @@ var VdDraw = class {
1108
1248
  const shape = this.getShape(id);
1109
1249
  if (!shape || !patch) return null;
1110
1250
  Object.assign(shape, patch);
1251
+ this._shapesById.set(id, shape);
1252
+ this._markDirty(id);
1111
1253
  this.render();
1112
1254
  this._emitChange(options.reason || "shape:update", { shapeId: id }, options.reason ? id : null);
1113
1255
  return shape;
@@ -1116,7 +1258,9 @@ var VdDraw = class {
1116
1258
  const idx = this.documentData.shapes.findIndex((s) => s.id === id);
1117
1259
  if (idx === -1) return false;
1118
1260
  this.documentData.shapes.splice(idx, 1);
1261
+ this._shapesById.delete(id);
1119
1262
  this.selectedIds.delete(id);
1263
+ this._markDirty(id);
1120
1264
  this.render();
1121
1265
  this._emitChange("shape:delete", { shapeId: id });
1122
1266
  return true;
@@ -1183,11 +1327,13 @@ var VdDraw = class {
1183
1327
  _replaceShape(next) {
1184
1328
  const idx = this.documentData.shapes.findIndex((s) => s.id === next.id);
1185
1329
  if (idx !== -1) this.documentData.shapes[idx] = next;
1330
+ this._shapesById.set(next.id, next);
1186
1331
  }
1187
1332
  nudge(dx, dy) {
1188
1333
  const selected = this.getSelectedShapes();
1189
1334
  if (!selected.length) return this;
1190
1335
  for (const shape of selected) this._replaceShape(translateShape(shape, dx, dy));
1336
+ this._markAllDirty();
1191
1337
  this.render();
1192
1338
  this._emitChange(
1193
1339
  "shape:nudge",
@@ -1206,6 +1352,7 @@ var VdDraw = class {
1206
1352
  const scaled = scaleShape(shape, cur.x, cur.y, sx, sy);
1207
1353
  this._replaceShape(translateShape(scaled, target.x - cur.x, target.y - cur.y));
1208
1354
  }
1355
+ this._markAllDirty();
1209
1356
  this.render();
1210
1357
  this._emitChange("shape:resize", { shapeIds: [...this.selectedIds] });
1211
1358
  return this;
@@ -1213,8 +1360,10 @@ var VdDraw = class {
1213
1360
  deleteSelection() {
1214
1361
  if (this.selectedIds.size === 0) return false;
1215
1362
  const ids = [...this.selectedIds];
1363
+ for (const id of ids) this._shapesById.delete(id);
1216
1364
  this.documentData.shapes = this.documentData.shapes.filter((s) => !this.selectedIds.has(s.id));
1217
1365
  this.selectedIds.clear();
1366
+ this._markAllDirty();
1218
1367
  this.render();
1219
1368
  this._emitChange("shape:delete", { shapeIds: ids });
1220
1369
  return true;
@@ -1226,6 +1375,7 @@ var VdDraw = class {
1226
1375
  for (const shape of selected) {
1227
1376
  for (const key of allowed) if (key in patch) shape[key] = patch[key];
1228
1377
  }
1378
+ this._markAllDirty();
1229
1379
  this.render();
1230
1380
  this._emitChange(
1231
1381
  "shape:style",
@@ -1238,6 +1388,7 @@ var VdDraw = class {
1238
1388
  _reorder(mutator) {
1239
1389
  if (this.selectedIds.size === 0) return this;
1240
1390
  mutator();
1391
+ this._markAllDirty();
1241
1392
  this.render();
1242
1393
  this._emitChange("shape:reorder", { shapeIds: [...this.selectedIds] });
1243
1394
  return this;
@@ -1280,6 +1431,7 @@ var VdDraw = class {
1280
1431
  if (selected.length < 2) return this;
1281
1432
  const groupId = createId("grp");
1282
1433
  for (const shape of selected) shape.groupId = groupId;
1434
+ this._markAllDirty();
1283
1435
  this.render();
1284
1436
  this._emitChange("shape:group", { groupId, shapeIds: [...this.selectedIds] });
1285
1437
  return this;
@@ -1294,6 +1446,7 @@ var VdDraw = class {
1294
1446
  }
1295
1447
  }
1296
1448
  if (!changed) return this;
1449
+ this._markAllDirty();
1297
1450
  this.render();
1298
1451
  this._emitChange("shape:ungroup", { shapeIds: [...this.selectedIds] });
1299
1452
  return this;
@@ -1320,9 +1473,11 @@ var VdDraw = class {
1320
1473
  copy.groupId = groupRemap.get(copy.groupId);
1321
1474
  }
1322
1475
  this.documentData.shapes.push(copy);
1476
+ this._shapesById.set(copy.id, copy);
1323
1477
  newIds.push(copy.id);
1324
1478
  }
1325
1479
  this.selectedIds = new Set(newIds);
1480
+ this._markAllDirty();
1326
1481
  this.render();
1327
1482
  this._emitChange("shape:paste", { shapeIds: newIds });
1328
1483
  this._emitSelect();
@@ -1384,6 +1539,30 @@ var VdDraw = class {
1384
1539
  sticky: read("--vd-draw-sticky-fill", "#fdf3c4")
1385
1540
  };
1386
1541
  }
1542
+ _getArrowMarkerId(color, defsTarget = this.defsEl) {
1543
+ const isDefault = !color;
1544
+ const safeKey = isDefault ? "arrow" : "arrow-" + color.toLowerCase().replace(/[^a-z0-9_-]/g, "_");
1545
+ const id = this._svgId(safeKey);
1546
+ if (defsTarget && !defsTarget.querySelector(`#${id}`)) {
1547
+ const marker = createSvgEl("marker", {
1548
+ id,
1549
+ viewBox: "0 0 10 10",
1550
+ refX: 8,
1551
+ refY: 5,
1552
+ markerWidth: 7,
1553
+ markerHeight: 7,
1554
+ orient: "auto-start-reverse"
1555
+ });
1556
+ marker.appendChild(
1557
+ createSvgEl("path", {
1558
+ d: "M 0 0 L 10 5 L 0 10 z",
1559
+ fill: color || "var(--vd-draw-shape-stroke)"
1560
+ })
1561
+ );
1562
+ defsTarget.appendChild(marker);
1563
+ }
1564
+ return id;
1565
+ }
1387
1566
  toSVG() {
1388
1567
  const shapes = this.documentData.shapes;
1389
1568
  const bounds = boundsOfShapes(shapes) || { x: 0, y: 0, w: 100, h: 100 };
@@ -1401,10 +1580,14 @@ var VdDraw = class {
1401
1580
  height: round(vb.h),
1402
1581
  viewBox: `${round(vb.x)} ${round(vb.y)} ${round(vb.w)} ${round(vb.h)}`
1403
1582
  });
1583
+ const defs = createSvgEl("defs");
1404
1584
  for (const shape of shapes) {
1405
- const el = this._renderShapeEl(shape, { standalone: true, colors });
1585
+ const el = this._renderShapeEl(shape, { standalone: true, colors, defsTarget: defs });
1406
1586
  if (el) svg.appendChild(el);
1407
1587
  }
1588
+ if (defs.childNodes.length > 0) {
1589
+ svg.insertBefore(defs, svg.firstChild);
1590
+ }
1408
1591
  return new XMLSerializer().serializeToString(svg);
1409
1592
  }
1410
1593
  toPNG({ scale = 2 } = {}) {
@@ -1498,7 +1681,9 @@ var VdDraw = class {
1498
1681
  const viewport = { ...this.documentData.viewport };
1499
1682
  this.documentData = deepClone(snapshot);
1500
1683
  this.documentData.viewport = viewport;
1684
+ this._rebuildShapeIndex();
1501
1685
  this._syncSelectionValidity();
1686
+ this._markAllDirty();
1502
1687
  this.render();
1503
1688
  this.emit("change", { reason, document: this.toJSON() });
1504
1689
  this.emit("history", { reason, canUndo: this.canUndo(), canRedo: this.canRedo() });
@@ -1514,8 +1699,10 @@ var VdDraw = class {
1514
1699
  }
1515
1700
  load(data) {
1516
1701
  this.documentData = normalizeDocument(data);
1702
+ this._rebuildShapeIndex();
1517
1703
  this.selectedIds.clear();
1518
1704
  this._resetHistory();
1705
+ this._markAllDirty();
1519
1706
  this.render();
1520
1707
  this._emitChange("load");
1521
1708
  this._emitSelect();
@@ -1524,7 +1711,9 @@ var VdDraw = class {
1524
1711
  clear() {
1525
1712
  if (!this.documentData.shapes.length) return this;
1526
1713
  this.documentData.shapes = [];
1714
+ this._shapesById.clear();
1527
1715
  this.selectedIds.clear();
1716
+ this._markAllDirty();
1528
1717
  this.render();
1529
1718
  this._emitChange("clear");
1530
1719
  return this;
@@ -1549,10 +1738,16 @@ var VdDraw = class {
1549
1738
  }
1550
1739
  _positionTextEditor(shape, editor) {
1551
1740
  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`;
1741
+ const scale = vp.scale || 1;
1742
+ editor.style.left = `${vp.x + shape.x * scale}px`;
1743
+ editor.style.top = `${vp.y + shape.y * scale}px`;
1744
+ editor.style.width = `${shape.w * scale}px`;
1745
+ editor.style.height = `${shape.h * scale}px`;
1746
+ editor.style.fontSize = `${16 * scale}px`;
1747
+ editor.style.lineHeight = `${20 * scale}px`;
1748
+ if (shape.type === "sticky") {
1749
+ editor.style.background = shape.fill || "var(--vd-draw-sticky-fill)";
1750
+ }
1556
1751
  }
1557
1752
  stopTextEdit({ commit = true } = {}) {
1558
1753
  if (!this.textEditor) return;
@@ -1564,6 +1759,7 @@ var VdDraw = class {
1564
1759
  const shape = this.getShape(id);
1565
1760
  if (shape && shape.text !== value) {
1566
1761
  shape.text = value;
1762
+ this._markDirty(id);
1567
1763
  this.render();
1568
1764
  this._emitChange("shape:text", { shapeId: id }, `text:${id}`);
1569
1765
  }
@@ -1621,11 +1817,56 @@ var VdDraw = class {
1621
1817
  if (this.destroyed || event.button != null && event.button !== 0) return;
1622
1818
  this.canvasEl.focus();
1623
1819
  this.stopTextEdit({ commit: true });
1820
+ this._activePointers.set(event.pointerId, { clientX: event.clientX, clientY: event.clientY });
1821
+ if (this._activePointers.size === 2) {
1822
+ if (this.interaction) {
1823
+ const it = this.interaction;
1824
+ if (it.kind === "freehand" || it.kind === "create-line" || it.kind === "create-box") {
1825
+ this.documentData.shapes = this.documentData.shapes.filter((s) => s.id !== it.shapeId);
1826
+ this._shapesById.delete(it.shapeId);
1827
+ const el = this._shapeElements.get(it.shapeId);
1828
+ if (el) {
1829
+ el.remove();
1830
+ this._shapeElements.delete(it.shapeId);
1831
+ }
1832
+ this._scheduleRender({ scene: true, overlay: false });
1833
+ } else if (it.kind === "move" || it.kind === "resize") {
1834
+ for (const orig of it.originals) this._replaceShape(orig);
1835
+ this._scheduleRender({ scene: true });
1836
+ } else if (it.kind === "marquee") {
1837
+ clearChildren(this.marqueeLayer);
1838
+ }
1839
+ }
1840
+ const pts = [...this._activePointers.values()];
1841
+ const p1 = pts[0];
1842
+ const p2 = pts[1];
1843
+ const initialDistance = Math.hypot(p2.clientX - p1.clientX, p2.clientY - p1.clientY) || 1;
1844
+ const initialClientMid = {
1845
+ x: (p1.clientX + p2.clientX) / 2,
1846
+ y: (p1.clientY + p2.clientY) / 2
1847
+ };
1848
+ const initialLocalMid = this._clientToLocal(initialClientMid.x, initialClientMid.y);
1849
+ const initialVp = { ...this.documentData.viewport };
1850
+ const currentScale = initialVp.scale || 1;
1851
+ const worldMid = {
1852
+ x: (initialLocalMid.x - initialVp.x) / currentScale,
1853
+ y: (initialLocalMid.y - initialVp.y) / currentScale
1854
+ };
1855
+ this.interaction = {
1856
+ kind: "pinch",
1857
+ initialDistance,
1858
+ worldMid,
1859
+ initialScale: currentScale
1860
+ };
1861
+ if (typeof event.preventDefault === "function") event.preventDefault();
1862
+ return;
1863
+ }
1624
1864
  const world = this._clientToWorld(event.clientX, event.clientY);
1625
1865
  const shapeTarget = event.target.closest("[data-shape-id]");
1626
1866
  const handleTarget = event.target.closest("[data-handle]");
1627
1867
  if (this.tool === "hand") {
1628
1868
  this.interaction = this._beginPan(event);
1869
+ if (this.canvasEl) this.canvasEl.classList.add("vd-draw-panning");
1629
1870
  this._capture(event.pointerId);
1630
1871
  return;
1631
1872
  }
@@ -1636,6 +1877,15 @@ var VdDraw = class {
1636
1877
  return;
1637
1878
  }
1638
1879
  if (this.tool === "select") {
1880
+ if (event.detail === 2 && shapeTarget && !this.readonly) {
1881
+ const id = shapeTarget.getAttribute("data-shape-id");
1882
+ const shape = this.getShape(id);
1883
+ if (shape && (shape.type === "text" || shape.type === "sticky")) {
1884
+ this.select(id);
1885
+ this.startTextEdit(id);
1886
+ return;
1887
+ }
1888
+ }
1639
1889
  if (handleTarget && this.selectedIds.size) {
1640
1890
  this.interaction = {
1641
1891
  kind: "resize",
@@ -1699,6 +1949,7 @@ var VdDraw = class {
1699
1949
  points: [[round(world.x), round(world.y), pressure]]
1700
1950
  };
1701
1951
  this.documentData.shapes.push(shape2);
1952
+ this._shapesById.set(shape2.id, shape2);
1702
1953
  return { kind: "freehand", pointerId: event.pointerId, shapeId: shape2.id };
1703
1954
  }
1704
1955
  if (tool === "line") {
@@ -1716,6 +1967,7 @@ var VdDraw = class {
1716
1967
  opacity: this.style.opacity
1717
1968
  };
1718
1969
  this.documentData.shapes.push(shape2);
1970
+ this._shapesById.set(shape2.id, shape2);
1719
1971
  return { kind: "create-line", pointerId: event.pointerId, shapeId: shape2.id };
1720
1972
  }
1721
1973
  const type = tool;
@@ -1733,6 +1985,7 @@ var VdDraw = class {
1733
1985
  };
1734
1986
  if (type === "text" || type === "sticky") shape.text = "";
1735
1987
  this.documentData.shapes.push(shape);
1988
+ this._shapesById.set(shape.id, shape);
1736
1989
  return { kind: "create-box", pointerId: event.pointerId, shapeId: shape.id, start: world };
1737
1990
  }
1738
1991
  _applyErase(world) {
@@ -1747,16 +2000,44 @@ var VdDraw = class {
1747
2000
  changed = true;
1748
2001
  }
1749
2002
  }
1750
- if (changed) this.render({ scene: true, overlay: false });
2003
+ if (changed) {
2004
+ for (const id of it.erased) this._markDirty(id);
2005
+ this._scheduleRender({ scene: true, overlay: false });
2006
+ }
1751
2007
  }
1752
2008
  _handlePointerMove(event) {
2009
+ if (this._activePointers.has(event.pointerId)) {
2010
+ this._activePointers.set(event.pointerId, { clientX: event.clientX, clientY: event.clientY });
2011
+ }
1753
2012
  const it = this.interaction;
1754
2013
  if (!it) return;
2014
+ if (it.kind === "pinch") {
2015
+ if (this._activePointers.size >= 2) {
2016
+ if (typeof event.preventDefault === "function") event.preventDefault();
2017
+ const pts = [...this._activePointers.values()];
2018
+ const p1 = pts[0];
2019
+ const p2 = pts[1];
2020
+ const curDistance = Math.hypot(p2.clientX - p1.clientX, p2.clientY - p1.clientY) || 1;
2021
+ const curClientMid = {
2022
+ x: (p1.clientX + p2.clientX) / 2,
2023
+ y: (p1.clientY + p2.clientY) / 2
2024
+ };
2025
+ const curLocalMid = this._clientToLocal(curClientMid.x, curClientMid.y);
2026
+ const distRatio = curDistance / it.initialDistance;
2027
+ const newScale = clamp(it.initialScale * distRatio, MIN_SCALE, MAX_SCALE);
2028
+ const vp = this.documentData.viewport;
2029
+ vp.scale = newScale;
2030
+ vp.x = curLocalMid.x - it.worldMid.x * newScale;
2031
+ vp.y = curLocalMid.y - it.worldMid.y * newScale;
2032
+ this._scheduleRender({ scene: false });
2033
+ }
2034
+ return;
2035
+ }
1755
2036
  if (it.kind === "pan") {
1756
2037
  const vp = this.documentData.viewport;
1757
2038
  vp.x = it.startX + (event.clientX - it.startClientX);
1758
2039
  vp.y = it.startY + (event.clientY - it.startClientY);
1759
- this.render({ scene: false });
2040
+ this._scheduleRender({ scene: false });
1760
2041
  return;
1761
2042
  }
1762
2043
  const world = this._clientToWorld(event.clientX, event.clientY);
@@ -1774,8 +2055,9 @@ var VdDraw = class {
1774
2055
  if (snap.dx || snap.dy)
1775
2056
  for (const orig of it.originals)
1776
2057
  this._replaceShape(translateShape(orig, dx + snap.dx, dy + snap.dy));
2058
+ for (const orig of it.originals) this._markDirty(orig.id);
1777
2059
  this._renderGuides(snap.guides);
1778
- this.render({ scene: true, guides: false });
2060
+ this._scheduleRender({ scene: true, guides: false });
1779
2061
  return;
1780
2062
  }
1781
2063
  if (it.kind === "resize") {
@@ -1790,9 +2072,10 @@ var VdDraw = class {
1790
2072
  for (const orig of it.originals) {
1791
2073
  const scaled = scaleShape(orig, it.startBounds.x, it.startBounds.y, sx, sy);
1792
2074
  this._replaceShape(translateShape(scaled, t.x - it.startBounds.x, t.y - it.startBounds.y));
2075
+ this._markDirty(orig.id);
1793
2076
  }
1794
2077
  it.moved = true;
1795
- this.render();
2078
+ this._scheduleRender();
1796
2079
  return;
1797
2080
  }
1798
2081
  if (it.kind === "marquee") {
@@ -1803,8 +2086,14 @@ var VdDraw = class {
1803
2086
  if (it.kind === "freehand") {
1804
2087
  const shape = this.getShape(it.shapeId);
1805
2088
  if (shape) {
1806
- shape.points.push([round(world.x), round(world.y), event.pressure || 0.5]);
1807
- this.render({ scene: true });
2089
+ const raw = [round(world.x), round(world.y), event.pressure || 0.5];
2090
+ const preset = BRUSH_PRESETS[shape.brush] || BRUSH_PRESETS[DEFAULT_BRUSH];
2091
+ const smoothFactor = 1 - (preset.smoothing || 0.5) * 0.6;
2092
+ const prev = shape.points[shape.points.length - 1];
2093
+ const pt = prev ? smoothPoint(prev, raw, smoothFactor) : raw;
2094
+ appendAndSimplify(shape.points, pt);
2095
+ this._markDirty(it.shapeId);
2096
+ this._scheduleRender({ scene: true });
1808
2097
  }
1809
2098
  return;
1810
2099
  }
@@ -1812,7 +2101,8 @@ var VdDraw = class {
1812
2101
  const shape = this.getShape(it.shapeId);
1813
2102
  if (shape) {
1814
2103
  shape.points[1] = [round(world.x), round(world.y)];
1815
- this.render({ scene: true });
2104
+ this._markDirty(it.shapeId);
2105
+ this._scheduleRender({ scene: true });
1816
2106
  }
1817
2107
  return;
1818
2108
  }
@@ -1823,7 +2113,8 @@ var VdDraw = class {
1823
2113
  shape.y = round(Math.min(it.start.y, world.y));
1824
2114
  shape.w = round(Math.max(1, Math.abs(world.x - it.start.x)));
1825
2115
  shape.h = round(Math.max(1, Math.abs(world.y - it.start.y)));
1826
- this.render({ scene: true });
2116
+ this._markDirty(it.shapeId);
2117
+ this._scheduleRender({ scene: true });
1827
2118
  }
1828
2119
  }
1829
2120
  }
@@ -1842,9 +2133,18 @@ var VdDraw = class {
1842
2133
  return { x, y, w, h: h2 };
1843
2134
  }
1844
2135
  _handlePointerUp(event) {
2136
+ this._activePointers.delete(event.pointerId);
1845
2137
  const it = this.interaction;
1846
2138
  if (!it) return;
2139
+ if (it.kind === "pinch") {
2140
+ if (this._activePointers.size < 2) {
2141
+ this.interaction = null;
2142
+ this._emitViewportChange("viewport:pinch");
2143
+ }
2144
+ return;
2145
+ }
1847
2146
  this.interaction = null;
2147
+ if (this.canvasEl) this.canvasEl.classList.remove("vd-draw-panning");
1848
2148
  if (typeof this.canvasEl.releasePointerCapture === "function" && this.canvasEl.hasPointerCapture?.(event.pointerId)) {
1849
2149
  try {
1850
2150
  this.canvasEl.releasePointerCapture(event.pointerId);
@@ -1860,18 +2160,22 @@ var VdDraw = class {
1860
2160
  if (it.kind === "erase") {
1861
2161
  if (it.erased.size) {
1862
2162
  const ids = [...it.erased];
2163
+ for (const id of ids) this._shapesById.delete(id);
1863
2164
  this.documentData.shapes = this.documentData.shapes.filter((s) => !it.erased.has(s.id));
2165
+ this._markAllDirty();
1864
2166
  this.render();
1865
2167
  this._emitChange("shape:erase", { shapeIds: ids });
1866
2168
  }
1867
2169
  return;
1868
2170
  }
1869
2171
  if (it.kind === "move" && it.moved) {
2172
+ this._markAllDirty();
1870
2173
  this.render();
1871
2174
  this._emitChange("shape:move", { shapeIds: [...this.selectedIds] });
1872
2175
  return;
1873
2176
  }
1874
2177
  if (it.kind === "resize" && it.moved) {
2178
+ this._markAllDirty();
1875
2179
  this.render();
1876
2180
  this._emitChange("shape:resize", { shapeIds: [...this.selectedIds] });
1877
2181
  return;
@@ -1888,6 +2192,8 @@ var VdDraw = class {
1888
2192
  const b = shapeBounds(shape);
1889
2193
  if (it.kind !== "freehand" && b.w < 2 && b.h < 2 && shape.type !== "text" && shape.type !== "sticky") {
1890
2194
  this.documentData.shapes = this.documentData.shapes.filter((s) => s.id !== it.shapeId);
2195
+ this._shapesById.delete(it.shapeId);
2196
+ this._markAllDirty();
1891
2197
  this.render();
1892
2198
  return;
1893
2199
  }
@@ -1901,6 +2207,7 @@ var VdDraw = class {
1901
2207
  this.select(shape.id);
1902
2208
  this.setTool("select");
1903
2209
  }
2210
+ this._markDirty(shape.id);
1904
2211
  this.render();
1905
2212
  this._emitChange("shape:add", { shape: deepClone(shape), shapeId: shape.id });
1906
2213
  if (shape.type === "text" || shape.type === "sticky") this.startTextEdit(shape.id);
@@ -1977,26 +2284,91 @@ var VdDraw = class {
1977
2284
  if (this.destroyed) return;
1978
2285
  this.render({ scene: false });
1979
2286
  }
2287
+ // ── Dirty tracking ──────────────────────────────────────────────────────
2288
+ _markDirty(shapeId) {
2289
+ this._dirtyShapes.add(shapeId);
2290
+ }
2291
+ _markAllDirty() {
2292
+ this._allDirty = true;
2293
+ }
2294
+ // ── rAF-batched render ─────────────────────────────────────────────────
2295
+ _scheduleRender(flags = {}) {
2296
+ this._pendingFlags = {
2297
+ scene: (this._pendingFlags?.scene ?? false) || flags.scene !== false,
2298
+ overlay: (this._pendingFlags?.overlay ?? false) || flags.overlay !== false
2299
+ };
2300
+ if (this._rafId != null) return;
2301
+ this._rafId = (typeof requestAnimationFrame === "function" ? requestAnimationFrame : setTimeout)(() => {
2302
+ this._rafId = null;
2303
+ const f = this._pendingFlags || {};
2304
+ this._pendingFlags = null;
2305
+ this.render(f);
2306
+ });
2307
+ }
1980
2308
  // ── Rendering ────────────────────────────────────────────────────────────
1981
2309
  render(flags = {}) {
1982
2310
  if (this.destroyed) return;
1983
2311
  const { scene = true, overlay = true } = flags;
1984
2312
  const vp = this.documentData.viewport;
1985
2313
  this.world.setAttribute("transform", `matrix(${vp.scale} 0 0 ${vp.scale} ${vp.x} ${vp.y})`);
2314
+ if (this.textEditor) {
2315
+ const activeShape = this.getShape(this.textEditor.id);
2316
+ if (activeShape) this._positionTextEditor(activeShape, this.textEditor.el);
2317
+ }
1986
2318
  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
- }
2319
+ this._renderSceneIncremental();
1994
2320
  }
1995
2321
  if (overlay) this._renderOverlay();
1996
2322
  }
2323
+ _renderSceneIncremental() {
2324
+ const shapes = this.documentData.shapes;
2325
+ const erasing = this.interaction && this.interaction.kind === "erase" ? this.interaction.erased : null;
2326
+ const currentIds = /* @__PURE__ */ new Set();
2327
+ for (const shape of shapes) {
2328
+ if (erasing && erasing.has(shape.id)) continue;
2329
+ currentIds.add(shape.id);
2330
+ }
2331
+ for (const [id, el] of this._shapeElements) {
2332
+ if (!currentIds.has(id)) {
2333
+ el.remove();
2334
+ this._shapeElements.delete(id);
2335
+ }
2336
+ }
2337
+ const fullRebuild = this._allDirty;
2338
+ let prevEl = null;
2339
+ for (const shape of shapes) {
2340
+ if (!currentIds.has(shape.id)) continue;
2341
+ const needsUpdate = fullRebuild || this._dirtyShapes.has(shape.id);
2342
+ let el = this._shapeElements.get(shape.id);
2343
+ if (needsUpdate || !el) {
2344
+ const newEl = this._renderShapeEl(shape, {});
2345
+ if (!newEl) {
2346
+ if (el) {
2347
+ el.remove();
2348
+ this._shapeElements.delete(shape.id);
2349
+ }
2350
+ continue;
2351
+ }
2352
+ if (el) {
2353
+ el.replaceWith(newEl);
2354
+ } else {
2355
+ if (prevEl && prevEl.parentNode === this.shapesLayer) {
2356
+ prevEl.after(newEl);
2357
+ } else {
2358
+ this.shapesLayer.prepend(newEl);
2359
+ }
2360
+ }
2361
+ el = newEl;
2362
+ this._shapeElements.set(shape.id, el);
2363
+ }
2364
+ prevEl = el;
2365
+ }
2366
+ this._dirtyShapes.clear();
2367
+ this._allDirty = false;
2368
+ }
1997
2369
  // Colors are applied via inline `style` (which wins over the CSS class rules
1998
2370
  // and serializes self-contained), so a picked color always renders.
1999
- _renderShapeEl(shape, { standalone = false, colors = null }) {
2371
+ _renderShapeEl(shape, { standalone = false, colors = null, defsTarget = null } = {}) {
2000
2372
  let el = null;
2001
2373
  const setOpacity = (node) => {
2002
2374
  if (shape.opacity != null && shape.opacity !== 1)
@@ -2034,8 +2406,11 @@ var VdDraw = class {
2034
2406
  el.classList.add("vd-draw-shape");
2035
2407
  this._applyShapeStroke(el, shape, standalone, colors);
2036
2408
  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")})`);
2409
+ const strokeColor = shape.color || (standalone && colors ? colors.shapeStroke : "");
2410
+ const targetDefs = defsTarget || this.defsEl;
2411
+ const markerId = this._getArrowMarkerId(strokeColor, targetDefs);
2412
+ if (shape.arrowEnd) el.setAttribute("marker-end", `url(#${markerId})`);
2413
+ if (shape.arrowStart) el.setAttribute("marker-start", `url(#${markerId})`);
2039
2414
  } else if (shape.type === "text" || shape.type === "sticky") {
2040
2415
  el = createSvgEl("g");
2041
2416
  setOpacity(el);
@@ -2052,11 +2427,31 @@ var VdDraw = class {
2052
2427
  else if (standalone && colors) bg.style.fill = colors.sticky;
2053
2428
  el.appendChild(bg);
2054
2429
  }
2055
- const text = createSvgEl("text", { x: shape.x + 6, y: shape.y + 18 });
2430
+ const startX = (shape.x || 0) + (shape.type === "sticky" ? 10 : 6);
2431
+ const startY = (shape.y || 0) + (shape.type === "sticky" ? 14 : 18);
2432
+ const availWidth = shape.type === "sticky" ? Math.max(20, (shape.w || 160) - 20) : shape.w && shape.w > 20 ? shape.w - 12 : 0;
2433
+ const lines = wrapText(shape.text || "", availWidth);
2434
+ const text = createSvgEl("text", {
2435
+ x: startX,
2436
+ y: startY,
2437
+ "xml:space": "preserve"
2438
+ });
2056
2439
  text.classList.add("vd-draw-text");
2057
2440
  const fill = shape.color || (standalone && colors ? colors.text : "");
2058
2441
  if (fill) text.style.fill = fill;
2059
- text.textContent = shape.text || "";
2442
+ if (!lines.length) {
2443
+ text.textContent = "";
2444
+ } else {
2445
+ const lineHeight = 20;
2446
+ lines.forEach((lineText, idx) => {
2447
+ const tspan = createSvgEl("tspan", {
2448
+ x: startX,
2449
+ y: startY + idx * lineHeight
2450
+ });
2451
+ tspan.textContent = lineText;
2452
+ text.appendChild(tspan);
2453
+ });
2454
+ }
2060
2455
  el.appendChild(text);
2061
2456
  }
2062
2457
  if (el && !standalone) el.setAttribute("data-shape-id", shape.id);
@@ -2140,9 +2535,18 @@ var VdDraw = class {
2140
2535
  destroy() {
2141
2536
  if (this.destroyed) return;
2142
2537
  this.destroyed = true;
2538
+ if (this._rafId != null) {
2539
+ (typeof cancelAnimationFrame === "function" ? cancelAnimationFrame : clearTimeout)(
2540
+ this._rafId
2541
+ );
2542
+ this._rafId = null;
2543
+ }
2143
2544
  this.stopTextEdit({ commit: false });
2144
2545
  this._unbindEvents();
2145
2546
  this.listeners.clear();
2547
+ this._activePointers.clear();
2548
+ this._shapesById.clear();
2549
+ this._shapeElements.clear();
2146
2550
  if (this.element) this.element.replaceChildren();
2147
2551
  }
2148
2552
  };
@@ -2175,10 +2579,10 @@ var VdDraw2 = defineComponent({
2175
2579
  setup(props, { emit, expose }) {
2176
2580
  const el = ref(null);
2177
2581
  let instance = null;
2178
- const create = () => {
2582
+ const create = (savedDoc) => {
2179
2583
  instance = new VdDraw({
2180
2584
  element: el.value,
2181
- data: props.data,
2585
+ data: savedDoc || props.data,
2182
2586
  readonly: props.readonly,
2183
2587
  tool: props.tool,
2184
2588
  gridSize: props.gridSize,
@@ -2212,18 +2616,28 @@ var VdDraw2 = defineComponent({
2212
2616
  (next) => instance?.setGridVisible(next)
2213
2617
  );
2214
2618
  watch(
2215
- () => [
2216
- props.readonly,
2217
- props.gridSize,
2218
- props.snap,
2219
- props.autoFit,
2220
- props.history,
2221
- props.historyLimit
2222
- ],
2619
+ () => props.readonly,
2620
+ (next) => instance?.setReadonly(next)
2621
+ );
2622
+ watch(
2623
+ () => props.snap,
2624
+ (next) => instance?.setSnap(next)
2625
+ );
2626
+ watch(
2627
+ () => props.history,
2628
+ (next) => instance?.setHistoryEnabled(next)
2629
+ );
2630
+ watch(
2631
+ () => props.historyLimit,
2632
+ (next) => instance?.setHistoryLimit(next)
2633
+ );
2634
+ watch(
2635
+ () => [props.gridSize, props.autoFit],
2223
2636
  () => {
2224
2637
  if (!instance) return;
2638
+ const currentDoc = typeof instance.toJSON === "function" ? instance.toJSON() : void 0;
2225
2639
  instance.destroy();
2226
- create();
2640
+ create(currentDoc);
2227
2641
  }
2228
2642
  );
2229
2643
  onBeforeUnmount(() => {
@@ -2235,6 +2649,10 @@ var VdDraw2 = defineComponent({
2235
2649
  expose({
2236
2650
  getInstance: () => instance,
2237
2651
  setTool: (tool) => instance?.setTool(tool),
2652
+ setReadonly: (readonly) => instance?.setReadonly(readonly),
2653
+ setSnap: (snap) => instance?.setSnap(snap),
2654
+ setHistoryEnabled: (enabled) => instance?.setHistoryEnabled(enabled),
2655
+ setHistoryLimit: (limit) => instance?.setHistoryLimit(limit),
2238
2656
  undo: () => instance?.undo(),
2239
2657
  redo: () => instance?.redo(),
2240
2658
  canUndo: () => Boolean(instance?.canUndo()),