@fieldnotes/core 0.25.0 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -59,6 +59,7 @@ __export(index_exports, {
59
59
  getArrowTangentAngle: () => getArrowTangentAngle,
60
60
  getBendFromPoint: () => getBendFromPoint,
61
61
  getElementBounds: () => getElementBounds,
62
+ getElementStyle: () => getElementStyle,
62
63
  getElementsBoundingBox: () => getElementsBoundingBox,
63
64
  getHexCellsInCone: () => getHexCellsInCone,
64
65
  getHexCellsInLine: () => getHexCellsInLine,
@@ -70,6 +71,7 @@ __export(index_exports, {
70
71
  smartSnap: () => smartSnap,
71
72
  snapPoint: () => snapPoint,
72
73
  snapToHexCenter: () => snapToHexCenter,
74
+ styleToPatch: () => styleToPatch,
73
75
  toggleBold: () => toggleBold,
74
76
  toggleItalic: () => toggleItalic,
75
77
  toggleStrikethrough: () => toggleStrikethrough,
@@ -588,6 +590,278 @@ function createId(prefix) {
588
590
  return `${prefix}_${Date.now().toString(36)}_${(counter++).toString(36)}`;
589
591
  }
590
592
 
593
+ // src/core/geometry.ts
594
+ function distSqToSegment(p, a, b) {
595
+ const abx = b.x - a.x;
596
+ const aby = b.y - a.y;
597
+ const apx = p.x - a.x;
598
+ const apy = p.y - a.y;
599
+ const lenSq = abx * abx + aby * aby;
600
+ if (lenSq === 0) {
601
+ return apx * apx + apy * apy;
602
+ }
603
+ const t = Math.max(0, Math.min(1, (apx * abx + apy * aby) / lenSq));
604
+ const dx = p.x - (a.x + t * abx);
605
+ const dy = p.y - (a.y + t * aby);
606
+ return dx * dx + dy * dy;
607
+ }
608
+
609
+ // src/elements/arrow-geometry.ts
610
+ function getArrowControlPoint(from, to, bend) {
611
+ const midX = (from.x + to.x) / 2;
612
+ const midY = (from.y + to.y) / 2;
613
+ if (bend === 0) return { x: midX, y: midY };
614
+ const dx = to.x - from.x;
615
+ const dy = to.y - from.y;
616
+ const len = Math.sqrt(dx * dx + dy * dy);
617
+ if (len === 0) return { x: midX, y: midY };
618
+ const perpX = -dy / len;
619
+ const perpY = dx / len;
620
+ return {
621
+ x: midX + perpX * bend,
622
+ y: midY + perpY * bend
623
+ };
624
+ }
625
+ function getArrowMidpoint(from, to, bend) {
626
+ const cp = getArrowControlPoint(from, to, bend);
627
+ return {
628
+ x: 0.25 * from.x + 0.5 * cp.x + 0.25 * to.x,
629
+ y: 0.25 * from.y + 0.5 * cp.y + 0.25 * to.y
630
+ };
631
+ }
632
+ function getBendFromPoint(from, to, dragPoint) {
633
+ const midX = (from.x + to.x) / 2;
634
+ const midY = (from.y + to.y) / 2;
635
+ const dx = to.x - from.x;
636
+ const dy = to.y - from.y;
637
+ const len = Math.sqrt(dx * dx + dy * dy);
638
+ if (len === 0) return 0;
639
+ const perpX = -dy / len;
640
+ const perpY = dx / len;
641
+ return (dragPoint.x - midX) * perpX + (dragPoint.y - midY) * perpY;
642
+ }
643
+ function getArrowTangentAngle(from, to, bend, t) {
644
+ const cp = getArrowControlPoint(from, to, bend);
645
+ const tangentX = 2 * (1 - t) * (cp.x - from.x) + 2 * t * (to.x - cp.x);
646
+ const tangentY = 2 * (1 - t) * (cp.y - from.y) + 2 * t * (to.y - cp.y);
647
+ return Math.atan2(tangentY, tangentX);
648
+ }
649
+ function isNearBezier(point, from, to, bend, threshold) {
650
+ if (bend === 0) return isNearLine(point, from, to, threshold);
651
+ const cp = getArrowControlPoint(from, to, bend);
652
+ const segments = 20;
653
+ for (let i = 0; i < segments; i++) {
654
+ const t0 = i / segments;
655
+ const t1 = (i + 1) / segments;
656
+ const a = bezierPoint(from, cp, to, t0);
657
+ const b = bezierPoint(from, cp, to, t1);
658
+ if (isNearLine(point, a, b, threshold)) return true;
659
+ }
660
+ return false;
661
+ }
662
+ function getArrowBounds(from, to, bend) {
663
+ if (bend === 0) {
664
+ const minX2 = Math.min(from.x, to.x);
665
+ const minY2 = Math.min(from.y, to.y);
666
+ return {
667
+ x: minX2,
668
+ y: minY2,
669
+ w: Math.abs(to.x - from.x),
670
+ h: Math.abs(to.y - from.y)
671
+ };
672
+ }
673
+ const cp = getArrowControlPoint(from, to, bend);
674
+ const steps = 20;
675
+ let minX = Math.min(from.x, to.x);
676
+ let minY = Math.min(from.y, to.y);
677
+ let maxX = Math.max(from.x, to.x);
678
+ let maxY = Math.max(from.y, to.y);
679
+ for (let i = 1; i < steps; i++) {
680
+ const t = i / steps;
681
+ const p = bezierPoint(from, cp, to, t);
682
+ if (p.x < minX) minX = p.x;
683
+ if (p.y < minY) minY = p.y;
684
+ if (p.x > maxX) maxX = p.x;
685
+ if (p.y > maxY) maxY = p.y;
686
+ }
687
+ return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
688
+ }
689
+ function bezierPoint(from, cp, to, t) {
690
+ const mt = 1 - t;
691
+ return {
692
+ x: mt * mt * from.x + 2 * mt * t * cp.x + t * t * to.x,
693
+ y: mt * mt * from.y + 2 * mt * t * cp.y + t * t * to.y
694
+ };
695
+ }
696
+ function isNearLine(point, a, b, threshold) {
697
+ return distSqToSegment(point, a, b) <= threshold * threshold;
698
+ }
699
+
700
+ // src/elements/element-bounds.ts
701
+ var strokeBoundsCache = /* @__PURE__ */ new WeakMap();
702
+ function getElementBounds(element) {
703
+ if (element.type === "grid") return null;
704
+ if ("size" in element) {
705
+ return {
706
+ x: element.position.x,
707
+ y: element.position.y,
708
+ w: element.size.w,
709
+ h: element.size.h
710
+ };
711
+ }
712
+ if (element.type === "stroke") {
713
+ if (element.points.length === 0) return null;
714
+ const cached = strokeBoundsCache.get(element);
715
+ if (cached) return cached;
716
+ let minX = Infinity;
717
+ let minY = Infinity;
718
+ let maxX = -Infinity;
719
+ let maxY = -Infinity;
720
+ for (const p of element.points) {
721
+ const px = p.x + element.position.x;
722
+ const py = p.y + element.position.y;
723
+ if (px < minX) minX = px;
724
+ if (py < minY) minY = py;
725
+ if (px > maxX) maxX = px;
726
+ if (py > maxY) maxY = py;
727
+ }
728
+ const bounds = { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
729
+ strokeBoundsCache.set(element, bounds);
730
+ return bounds;
731
+ }
732
+ if (element.type === "arrow") {
733
+ return getArrowBoundsAnalytical(element.from, element.to, element.bend);
734
+ }
735
+ if (element.type === "template") {
736
+ return getTemplateBounds(element);
737
+ }
738
+ return null;
739
+ }
740
+ function getArrowBoundsAnalytical(from, to, bend) {
741
+ if (bend === 0) {
742
+ const minX2 = Math.min(from.x, to.x);
743
+ const minY2 = Math.min(from.y, to.y);
744
+ return {
745
+ x: minX2,
746
+ y: minY2,
747
+ w: Math.abs(to.x - from.x),
748
+ h: Math.abs(to.y - from.y)
749
+ };
750
+ }
751
+ const cp = getArrowControlPoint(from, to, bend);
752
+ let minX = Math.min(from.x, to.x);
753
+ let maxX = Math.max(from.x, to.x);
754
+ let minY = Math.min(from.y, to.y);
755
+ let maxY = Math.max(from.y, to.y);
756
+ const tx = from.x - 2 * cp.x + to.x;
757
+ if (tx !== 0) {
758
+ const t = (from.x - cp.x) / tx;
759
+ if (t > 0 && t < 1) {
760
+ const mt = 1 - t;
761
+ const x = mt * mt * from.x + 2 * mt * t * cp.x + t * t * to.x;
762
+ if (x < minX) minX = x;
763
+ if (x > maxX) maxX = x;
764
+ }
765
+ }
766
+ const ty = from.y - 2 * cp.y + to.y;
767
+ if (ty !== 0) {
768
+ const t = (from.y - cp.y) / ty;
769
+ if (t > 0 && t < 1) {
770
+ const mt = 1 - t;
771
+ const y = mt * mt * from.y + 2 * mt * t * cp.y + t * t * to.y;
772
+ if (y < minY) minY = y;
773
+ if (y > maxY) maxY = y;
774
+ }
775
+ }
776
+ return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
777
+ }
778
+ function getTemplateBounds(el) {
779
+ const { x: cx, y: cy } = el.position;
780
+ const r = el.radius;
781
+ switch (el.templateShape) {
782
+ case "circle":
783
+ return { x: cx - r, y: cy - r, w: 2 * r, h: 2 * r };
784
+ case "square":
785
+ return { x: cx - r / 2, y: cy - r / 2, w: r, h: r };
786
+ case "cone": {
787
+ const halfAngle = Math.atan(0.5);
788
+ const tipX = cx;
789
+ const tipY = cy;
790
+ const leftX = cx + r * Math.cos(el.angle - halfAngle);
791
+ const leftY = cy + r * Math.sin(el.angle - halfAngle);
792
+ const rightX = cx + r * Math.cos(el.angle + halfAngle);
793
+ const rightY = cy + r * Math.sin(el.angle + halfAngle);
794
+ const farX = cx + r * Math.cos(el.angle);
795
+ const farY = cy + r * Math.sin(el.angle);
796
+ const xs = [tipX, leftX, rightX, farX];
797
+ const ys = [tipY, leftY, rightY, farY];
798
+ let minX = Infinity;
799
+ let minY = Infinity;
800
+ let maxX = -Infinity;
801
+ let maxY = -Infinity;
802
+ for (let i = 0; i < xs.length; i++) {
803
+ const px = xs[i];
804
+ const py = ys[i];
805
+ if (px !== void 0 && px < minX) minX = px;
806
+ if (px !== void 0 && px > maxX) maxX = px;
807
+ if (py !== void 0 && py < minY) minY = py;
808
+ if (py !== void 0 && py > maxY) maxY = py;
809
+ }
810
+ return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
811
+ }
812
+ case "line": {
813
+ const halfW = r / 12;
814
+ const cos = Math.cos(el.angle);
815
+ const sin = Math.sin(el.angle);
816
+ const perpX = -sin * halfW;
817
+ const perpY = cos * halfW;
818
+ const x0 = cx + perpX;
819
+ const y0 = cy + perpY;
820
+ const x1 = cx + r * cos + perpX;
821
+ const y1 = cy + r * sin + perpY;
822
+ const x2 = cx + r * cos - perpX;
823
+ const y2 = cy + r * sin - perpY;
824
+ const x3 = cx - perpX;
825
+ const y3 = cy - perpY;
826
+ const minX = Math.min(x0, x1, x2, x3);
827
+ const minY = Math.min(y0, y1, y2, y3);
828
+ const maxX = Math.max(x0, x1, x2, x3);
829
+ const maxY = Math.max(y0, y1, y2, y3);
830
+ return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
831
+ }
832
+ }
833
+ }
834
+ function transferStrokeBounds(prev, next) {
835
+ if (prev.type !== "stroke" || next.type !== "stroke") return;
836
+ if (prev.points !== next.points) return;
837
+ if (prev.position.x !== next.position.x || prev.position.y !== next.position.y) return;
838
+ const bounds = strokeBoundsCache.get(prev);
839
+ if (bounds) strokeBoundsCache.set(next, bounds);
840
+ }
841
+ function boundsIntersect(a, b) {
842
+ return a.x <= b.x + b.w && a.x + a.w >= b.x && a.y <= b.y + b.h && a.y + a.h >= b.y;
843
+ }
844
+
845
+ // src/elements/bounds.ts
846
+ function getElementsBoundingBox(elements) {
847
+ let minX = Infinity;
848
+ let minY = Infinity;
849
+ let maxX = -Infinity;
850
+ let maxY = -Infinity;
851
+ let found = false;
852
+ for (const el of elements) {
853
+ const b = getElementBounds(el);
854
+ if (!b) continue;
855
+ found = true;
856
+ if (b.x < minX) minX = b.x;
857
+ if (b.y < minY) minY = b.y;
858
+ if (b.x + b.w > maxX) maxX = b.x + b.w;
859
+ if (b.y + b.h > maxY) maxY = b.y + b.h;
860
+ }
861
+ if (!found) return null;
862
+ return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
863
+ }
864
+
591
865
  // src/canvas/keyboard-actions.ts
592
866
  var KeyboardActions = class {
593
867
  constructor(deps) {
@@ -693,8 +967,18 @@ var KeyboardActions = class {
693
967
  if (this.clipboard.length === 0) return;
694
968
  const sel = this.selectTool();
695
969
  if (!sel) return;
970
+ const cursor = this.deps.getLastPointerWorld?.() ?? null;
971
+ if (cursor) {
972
+ const bbox = getElementsBoundingBox(this.clipboard);
973
+ if (bbox) {
974
+ const centerX = bbox.x + bbox.w / 2;
975
+ const centerY = bbox.y + bbox.h / 2;
976
+ this.insertClones(this.clipboard, { x: cursor.x - centerX, y: cursor.y - centerY }, sel);
977
+ return;
978
+ }
979
+ }
696
980
  this.pasteCount++;
697
- this.insertClones(this.clipboard, this.pasteCount * 20, sel);
981
+ this.insertClones(this.clipboard, { x: this.pasteCount * 20, y: this.pasteCount * 20 }, sel);
698
982
  }
699
983
  duplicate() {
700
984
  if (this.deps.isToolActive()) return;
@@ -707,7 +991,7 @@ var KeyboardActions = class {
707
991
  if (el) source.push(el);
708
992
  }
709
993
  if (source.length === 0) return;
710
- this.insertClones(source, 20, sel);
994
+ this.insertClones(source, { x: 20, y: 20 }, sel);
711
995
  }
712
996
  deselect() {
713
997
  if (this.deps.isToolActive()) return;
@@ -778,11 +1062,11 @@ var KeyboardActions = class {
778
1062
  const newId = idMap.get(el.id);
779
1063
  if (!newId) continue;
780
1064
  clone.id = newId;
781
- clone.position = { x: clone.position.x + offset, y: clone.position.y + offset };
1065
+ clone.position = { x: clone.position.x + offset.x, y: clone.position.y + offset.y };
782
1066
  if (clone.type === "arrow") {
783
1067
  const arrow = clone;
784
- arrow.from = { x: arrow.from.x + offset, y: arrow.from.y + offset };
785
- arrow.to = { x: arrow.to.x + offset, y: arrow.to.y + offset };
1068
+ arrow.from = { x: arrow.from.x + offset.x, y: arrow.from.y + offset.y };
1069
+ arrow.to = { x: arrow.to.x + offset.x, y: arrow.to.y + offset.y };
786
1070
  delete arrow.cachedControlPoint;
787
1071
  if (arrow.fromBinding) {
788
1072
  const newTarget = idMap.get(arrow.fromBinding.elementId);
@@ -828,6 +1112,9 @@ var DEFAULT_BINDINGS = [
828
1112
  ["z-front", ["mod+]"]],
829
1113
  ["z-back", ["mod+["]],
830
1114
  ["zoom-fit", ["shift+1"]],
1115
+ ["zoom-in", ["mod+="]],
1116
+ ["zoom-out", ["mod+-"]],
1117
+ ["zoom-reset", ["mod+0"]],
831
1118
  ["nudge-left", ["arrowleft"]],
832
1119
  ["nudge-right", ["arrowright"]],
833
1120
  ["nudge-up", ["arrowup"]],
@@ -964,6 +1251,7 @@ var ShortcutMap = class {
964
1251
 
965
1252
  // src/canvas/input-handler.ts
966
1253
  var ZOOM_SENSITIVITY = 1e-3;
1254
+ var ZOOM_STEP = 1.2;
967
1255
  var MIDDLE_BUTTON = 1;
968
1256
  var NUDGE_DELTAS = {
969
1257
  "nudge-left": [-1, 0],
@@ -985,7 +1273,8 @@ var InputHandler = class {
985
1273
  getHistoryRecorder: () => this.historyRecorder,
986
1274
  getHistoryStack: () => this.historyStack,
987
1275
  isToolActive: () => this.isToolActive,
988
- fitToContent: options.fitToContent
1276
+ fitToContent: options.fitToContent,
1277
+ getLastPointerWorld: () => this.lastPointerWorld()
989
1278
  });
990
1279
  this.shortcutMap = new ShortcutMap(options.shortcuts?.bindings);
991
1280
  this.scope = options.shortcuts?.scope ?? "focus";
@@ -1041,11 +1330,21 @@ var InputHandler = class {
1041
1330
  this.element.addEventListener("pointerdown", this.onPointerDown, opts);
1042
1331
  this.element.addEventListener("pointermove", this.onPointerMove, opts);
1043
1332
  this.element.addEventListener("pointerup", this.onPointerUp, opts);
1044
- this.element.addEventListener("pointerleave", this.onPointerUp, opts);
1333
+ this.element.addEventListener("pointerleave", this.onPointerLeave, opts);
1045
1334
  this.element.addEventListener("pointercancel", this.onPointerUp, opts);
1046
1335
  window.addEventListener("keydown", this.onKeyDown, opts);
1047
1336
  window.addEventListener("keyup", this.onKeyUp, opts);
1048
1337
  }
1338
+ viewportCenter() {
1339
+ const rect = this.element.getBoundingClientRect();
1340
+ return { x: rect.width / 2, y: rect.height / 2 };
1341
+ }
1342
+ zoomByFactor(factor) {
1343
+ this.camera.zoomAt(this.camera.zoom * factor, this.viewportCenter());
1344
+ }
1345
+ zoomToLevel(level) {
1346
+ this.camera.zoomAt(level, this.viewportCenter());
1347
+ }
1049
1348
  onWheel = (e) => {
1050
1349
  e.preventDefault();
1051
1350
  const rect = this.element.getBoundingClientRect();
@@ -1214,6 +1513,18 @@ var InputHandler = class {
1214
1513
  e.preventDefault();
1215
1514
  this.actions.zoomToFit();
1216
1515
  return;
1516
+ case "zoom-in":
1517
+ e.preventDefault();
1518
+ this.zoomByFactor(ZOOM_STEP);
1519
+ return;
1520
+ case "zoom-out":
1521
+ e.preventDefault();
1522
+ this.zoomByFactor(1 / ZOOM_STEP);
1523
+ return;
1524
+ case "zoom-reset":
1525
+ e.preventDefault();
1526
+ this.zoomToLevel(1);
1527
+ return;
1217
1528
  case "nudge-left":
1218
1529
  case "nudge-right":
1219
1530
  case "nudge-up":
@@ -1271,6 +1582,16 @@ var InputHandler = class {
1271
1582
  midpoint(a, b) {
1272
1583
  return { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
1273
1584
  }
1585
+ lastPointerWorld() {
1586
+ const e = this.lastPointerEvent;
1587
+ if (!e) return null;
1588
+ const rect = this.element.getBoundingClientRect();
1589
+ return this.camera.screenToWorld({ x: e.clientX - rect.left, y: e.clientY - rect.top });
1590
+ }
1591
+ onPointerLeave = (e) => {
1592
+ this.lastPointerEvent = null;
1593
+ this.onPointerUp(e);
1594
+ };
1274
1595
  toPointerState(e) {
1275
1596
  const rect = this.element.getBoundingClientRect();
1276
1597
  return {
@@ -1573,320 +1894,68 @@ var QuadNode = class _QuadNode {
1573
1894
  ];
1574
1895
  const remaining = [];
1575
1896
  for (const item of this.items) {
1576
- const idx = this.getChildIndex(item.bounds);
1577
- if (idx !== -1) {
1578
- const target = this.children[idx];
1579
- if (target) target.insert(item);
1580
- } else {
1581
- remaining.push(item);
1582
- }
1583
- }
1584
- this.items = remaining;
1585
- }
1586
- collapseIfEmpty() {
1587
- if (!this.children) return;
1588
- let totalItems = this.items.length;
1589
- for (const child of this.children) {
1590
- if (child.children) return;
1591
- totalItems += child.items.length;
1592
- }
1593
- if (totalItems <= MAX_ITEMS) {
1594
- for (const child of this.children) {
1595
- this.items.push(...child.items);
1596
- }
1597
- this.children = null;
1598
- }
1599
- }
1600
- };
1601
- var Quadtree = class {
1602
- root;
1603
- _size = 0;
1604
- worldBounds;
1605
- constructor(worldBounds) {
1606
- this.worldBounds = worldBounds;
1607
- this.root = new QuadNode(worldBounds, 0);
1608
- }
1609
- get size() {
1610
- return this._size;
1611
- }
1612
- insert(id, bounds) {
1613
- this.root.insert({ id, bounds });
1614
- this._size++;
1615
- }
1616
- remove(id) {
1617
- if (this.root.remove(id)) {
1618
- this._size--;
1619
- }
1620
- }
1621
- update(id, newBounds) {
1622
- this.remove(id);
1623
- this.insert(id, newBounds);
1624
- }
1625
- query(rect) {
1626
- const result = [];
1627
- this.root.query(rect, result);
1628
- return result;
1629
- }
1630
- queryPoint(point) {
1631
- return this.query({ x: point.x, y: point.y, w: 0, h: 0 });
1632
- }
1633
- clear() {
1634
- this.root = new QuadNode(this.worldBounds, 0);
1635
- this._size = 0;
1636
- }
1637
- };
1638
-
1639
- // src/core/geometry.ts
1640
- function distSqToSegment(p, a, b) {
1641
- const abx = b.x - a.x;
1642
- const aby = b.y - a.y;
1643
- const apx = p.x - a.x;
1644
- const apy = p.y - a.y;
1645
- const lenSq = abx * abx + aby * aby;
1646
- if (lenSq === 0) {
1647
- return apx * apx + apy * apy;
1648
- }
1649
- const t = Math.max(0, Math.min(1, (apx * abx + apy * aby) / lenSq));
1650
- const dx = p.x - (a.x + t * abx);
1651
- const dy = p.y - (a.y + t * aby);
1652
- return dx * dx + dy * dy;
1653
- }
1654
-
1655
- // src/elements/arrow-geometry.ts
1656
- function getArrowControlPoint(from, to, bend) {
1657
- const midX = (from.x + to.x) / 2;
1658
- const midY = (from.y + to.y) / 2;
1659
- if (bend === 0) return { x: midX, y: midY };
1660
- const dx = to.x - from.x;
1661
- const dy = to.y - from.y;
1662
- const len = Math.sqrt(dx * dx + dy * dy);
1663
- if (len === 0) return { x: midX, y: midY };
1664
- const perpX = -dy / len;
1665
- const perpY = dx / len;
1666
- return {
1667
- x: midX + perpX * bend,
1668
- y: midY + perpY * bend
1669
- };
1670
- }
1671
- function getArrowMidpoint(from, to, bend) {
1672
- const cp = getArrowControlPoint(from, to, bend);
1673
- return {
1674
- x: 0.25 * from.x + 0.5 * cp.x + 0.25 * to.x,
1675
- y: 0.25 * from.y + 0.5 * cp.y + 0.25 * to.y
1676
- };
1677
- }
1678
- function getBendFromPoint(from, to, dragPoint) {
1679
- const midX = (from.x + to.x) / 2;
1680
- const midY = (from.y + to.y) / 2;
1681
- const dx = to.x - from.x;
1682
- const dy = to.y - from.y;
1683
- const len = Math.sqrt(dx * dx + dy * dy);
1684
- if (len === 0) return 0;
1685
- const perpX = -dy / len;
1686
- const perpY = dx / len;
1687
- return (dragPoint.x - midX) * perpX + (dragPoint.y - midY) * perpY;
1688
- }
1689
- function getArrowTangentAngle(from, to, bend, t) {
1690
- const cp = getArrowControlPoint(from, to, bend);
1691
- const tangentX = 2 * (1 - t) * (cp.x - from.x) + 2 * t * (to.x - cp.x);
1692
- const tangentY = 2 * (1 - t) * (cp.y - from.y) + 2 * t * (to.y - cp.y);
1693
- return Math.atan2(tangentY, tangentX);
1694
- }
1695
- function isNearBezier(point, from, to, bend, threshold) {
1696
- if (bend === 0) return isNearLine(point, from, to, threshold);
1697
- const cp = getArrowControlPoint(from, to, bend);
1698
- const segments = 20;
1699
- for (let i = 0; i < segments; i++) {
1700
- const t0 = i / segments;
1701
- const t1 = (i + 1) / segments;
1702
- const a = bezierPoint(from, cp, to, t0);
1703
- const b = bezierPoint(from, cp, to, t1);
1704
- if (isNearLine(point, a, b, threshold)) return true;
1705
- }
1706
- return false;
1707
- }
1708
- function getArrowBounds(from, to, bend) {
1709
- if (bend === 0) {
1710
- const minX2 = Math.min(from.x, to.x);
1711
- const minY2 = Math.min(from.y, to.y);
1712
- return {
1713
- x: minX2,
1714
- y: minY2,
1715
- w: Math.abs(to.x - from.x),
1716
- h: Math.abs(to.y - from.y)
1717
- };
1718
- }
1719
- const cp = getArrowControlPoint(from, to, bend);
1720
- const steps = 20;
1721
- let minX = Math.min(from.x, to.x);
1722
- let minY = Math.min(from.y, to.y);
1723
- let maxX = Math.max(from.x, to.x);
1724
- let maxY = Math.max(from.y, to.y);
1725
- for (let i = 1; i < steps; i++) {
1726
- const t = i / steps;
1727
- const p = bezierPoint(from, cp, to, t);
1728
- if (p.x < minX) minX = p.x;
1729
- if (p.y < minY) minY = p.y;
1730
- if (p.x > maxX) maxX = p.x;
1731
- if (p.y > maxY) maxY = p.y;
1732
- }
1733
- return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
1734
- }
1735
- function bezierPoint(from, cp, to, t) {
1736
- const mt = 1 - t;
1737
- return {
1738
- x: mt * mt * from.x + 2 * mt * t * cp.x + t * t * to.x,
1739
- y: mt * mt * from.y + 2 * mt * t * cp.y + t * t * to.y
1740
- };
1741
- }
1742
- function isNearLine(point, a, b, threshold) {
1743
- return distSqToSegment(point, a, b) <= threshold * threshold;
1744
- }
1745
-
1746
- // src/elements/element-bounds.ts
1747
- var strokeBoundsCache = /* @__PURE__ */ new WeakMap();
1748
- function getElementBounds(element) {
1749
- if (element.type === "grid") return null;
1750
- if ("size" in element) {
1751
- return {
1752
- x: element.position.x,
1753
- y: element.position.y,
1754
- w: element.size.w,
1755
- h: element.size.h
1756
- };
1757
- }
1758
- if (element.type === "stroke") {
1759
- if (element.points.length === 0) return null;
1760
- const cached = strokeBoundsCache.get(element);
1761
- if (cached) return cached;
1762
- let minX = Infinity;
1763
- let minY = Infinity;
1764
- let maxX = -Infinity;
1765
- let maxY = -Infinity;
1766
- for (const p of element.points) {
1767
- const px = p.x + element.position.x;
1768
- const py = p.y + element.position.y;
1769
- if (px < minX) minX = px;
1770
- if (py < minY) minY = py;
1771
- if (px > maxX) maxX = px;
1772
- if (py > maxY) maxY = py;
1897
+ const idx = this.getChildIndex(item.bounds);
1898
+ if (idx !== -1) {
1899
+ const target = this.children[idx];
1900
+ if (target) target.insert(item);
1901
+ } else {
1902
+ remaining.push(item);
1903
+ }
1773
1904
  }
1774
- const bounds = { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
1775
- strokeBoundsCache.set(element, bounds);
1776
- return bounds;
1905
+ this.items = remaining;
1777
1906
  }
1778
- if (element.type === "arrow") {
1779
- return getArrowBoundsAnalytical(element.from, element.to, element.bend);
1907
+ collapseIfEmpty() {
1908
+ if (!this.children) return;
1909
+ let totalItems = this.items.length;
1910
+ for (const child of this.children) {
1911
+ if (child.children) return;
1912
+ totalItems += child.items.length;
1913
+ }
1914
+ if (totalItems <= MAX_ITEMS) {
1915
+ for (const child of this.children) {
1916
+ this.items.push(...child.items);
1917
+ }
1918
+ this.children = null;
1919
+ }
1780
1920
  }
1781
- if (element.type === "template") {
1782
- return getTemplateBounds(element);
1921
+ };
1922
+ var Quadtree = class {
1923
+ root;
1924
+ _size = 0;
1925
+ worldBounds;
1926
+ constructor(worldBounds) {
1927
+ this.worldBounds = worldBounds;
1928
+ this.root = new QuadNode(worldBounds, 0);
1783
1929
  }
1784
- return null;
1785
- }
1786
- function getArrowBoundsAnalytical(from, to, bend) {
1787
- if (bend === 0) {
1788
- const minX2 = Math.min(from.x, to.x);
1789
- const minY2 = Math.min(from.y, to.y);
1790
- return {
1791
- x: minX2,
1792
- y: minY2,
1793
- w: Math.abs(to.x - from.x),
1794
- h: Math.abs(to.y - from.y)
1795
- };
1930
+ get size() {
1931
+ return this._size;
1796
1932
  }
1797
- const cp = getArrowControlPoint(from, to, bend);
1798
- let minX = Math.min(from.x, to.x);
1799
- let maxX = Math.max(from.x, to.x);
1800
- let minY = Math.min(from.y, to.y);
1801
- let maxY = Math.max(from.y, to.y);
1802
- const tx = from.x - 2 * cp.x + to.x;
1803
- if (tx !== 0) {
1804
- const t = (from.x - cp.x) / tx;
1805
- if (t > 0 && t < 1) {
1806
- const mt = 1 - t;
1807
- const x = mt * mt * from.x + 2 * mt * t * cp.x + t * t * to.x;
1808
- if (x < minX) minX = x;
1809
- if (x > maxX) maxX = x;
1810
- }
1933
+ insert(id, bounds) {
1934
+ this.root.insert({ id, bounds });
1935
+ this._size++;
1811
1936
  }
1812
- const ty = from.y - 2 * cp.y + to.y;
1813
- if (ty !== 0) {
1814
- const t = (from.y - cp.y) / ty;
1815
- if (t > 0 && t < 1) {
1816
- const mt = 1 - t;
1817
- const y = mt * mt * from.y + 2 * mt * t * cp.y + t * t * to.y;
1818
- if (y < minY) minY = y;
1819
- if (y > maxY) maxY = y;
1937
+ remove(id) {
1938
+ if (this.root.remove(id)) {
1939
+ this._size--;
1820
1940
  }
1821
1941
  }
1822
- return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
1823
- }
1824
- function getTemplateBounds(el) {
1825
- const { x: cx, y: cy } = el.position;
1826
- const r = el.radius;
1827
- switch (el.templateShape) {
1828
- case "circle":
1829
- return { x: cx - r, y: cy - r, w: 2 * r, h: 2 * r };
1830
- case "square":
1831
- return { x: cx - r / 2, y: cy - r / 2, w: r, h: r };
1832
- case "cone": {
1833
- const halfAngle = Math.atan(0.5);
1834
- const tipX = cx;
1835
- const tipY = cy;
1836
- const leftX = cx + r * Math.cos(el.angle - halfAngle);
1837
- const leftY = cy + r * Math.sin(el.angle - halfAngle);
1838
- const rightX = cx + r * Math.cos(el.angle + halfAngle);
1839
- const rightY = cy + r * Math.sin(el.angle + halfAngle);
1840
- const farX = cx + r * Math.cos(el.angle);
1841
- const farY = cy + r * Math.sin(el.angle);
1842
- const xs = [tipX, leftX, rightX, farX];
1843
- const ys = [tipY, leftY, rightY, farY];
1844
- let minX = Infinity;
1845
- let minY = Infinity;
1846
- let maxX = -Infinity;
1847
- let maxY = -Infinity;
1848
- for (let i = 0; i < xs.length; i++) {
1849
- const px = xs[i];
1850
- const py = ys[i];
1851
- if (px !== void 0 && px < minX) minX = px;
1852
- if (px !== void 0 && px > maxX) maxX = px;
1853
- if (py !== void 0 && py < minY) minY = py;
1854
- if (py !== void 0 && py > maxY) maxY = py;
1855
- }
1856
- return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
1857
- }
1858
- case "line": {
1859
- const halfW = r / 12;
1860
- const cos = Math.cos(el.angle);
1861
- const sin = Math.sin(el.angle);
1862
- const perpX = -sin * halfW;
1863
- const perpY = cos * halfW;
1864
- const x0 = cx + perpX;
1865
- const y0 = cy + perpY;
1866
- const x1 = cx + r * cos + perpX;
1867
- const y1 = cy + r * sin + perpY;
1868
- const x2 = cx + r * cos - perpX;
1869
- const y2 = cy + r * sin - perpY;
1870
- const x3 = cx - perpX;
1871
- const y3 = cy - perpY;
1872
- const minX = Math.min(x0, x1, x2, x3);
1873
- const minY = Math.min(y0, y1, y2, y3);
1874
- const maxX = Math.max(x0, x1, x2, x3);
1875
- const maxY = Math.max(y0, y1, y2, y3);
1876
- return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
1877
- }
1942
+ update(id, newBounds) {
1943
+ this.remove(id);
1944
+ this.insert(id, newBounds);
1878
1945
  }
1879
- }
1880
- function transferStrokeBounds(prev, next) {
1881
- if (prev.type !== "stroke" || next.type !== "stroke") return;
1882
- if (prev.points !== next.points) return;
1883
- if (prev.position.x !== next.position.x || prev.position.y !== next.position.y) return;
1884
- const bounds = strokeBoundsCache.get(prev);
1885
- if (bounds) strokeBoundsCache.set(next, bounds);
1886
- }
1887
- function boundsIntersect(a, b) {
1888
- return a.x <= b.x + b.w && a.x + a.w >= b.x && a.y <= b.y + b.h && a.y + a.h >= b.y;
1889
- }
1946
+ query(rect) {
1947
+ const result = [];
1948
+ this.root.query(rect, result);
1949
+ return result;
1950
+ }
1951
+ queryPoint(point) {
1952
+ return this.query({ x: point.x, y: point.y, w: 0, h: 0 });
1953
+ }
1954
+ clear() {
1955
+ this.root = new QuadNode(this.worldBounds, 0);
1956
+ this._size = 0;
1957
+ }
1958
+ };
1890
1959
 
1891
1960
  // src/elements/stroke-smoothing.ts
1892
1961
  var MIN_PRESSURE_SCALE = 0.2;
@@ -3508,6 +3577,8 @@ var NoteEditor = class {
3508
3577
  inputHandler = null;
3509
3578
  pendingEditId = null;
3510
3579
  onStopCallback = null;
3580
+ beginHistory = null;
3581
+ commitHistory = null;
3511
3582
  toolbar;
3512
3583
  placeholder;
3513
3584
  constructor(options) {
@@ -3523,6 +3594,10 @@ var NoteEditor = class {
3523
3594
  setOnStop(callback) {
3524
3595
  this.onStopCallback = callback;
3525
3596
  }
3597
+ setHistoryHooks(begin, commit) {
3598
+ this.beginHistory = begin;
3599
+ this.commitHistory = commit;
3600
+ }
3526
3601
  startEditing(node, elementId, store) {
3527
3602
  if (this.editingId === elementId) return;
3528
3603
  if (this.editingId) {
@@ -3554,18 +3629,21 @@ var NoteEditor = class {
3554
3629
  this.editingNode.removeAttribute("data-fn-empty");
3555
3630
  const text = sanitizeNoteHtml(this.editingNode.innerHTML);
3556
3631
  const current = store.getById(this.editingId);
3557
- if (current && (current.type === "note" || current.type === "text") && current.text !== text) {
3558
- store.update(this.editingId, { text });
3559
- }
3632
+ const textChanged = !!current && (current.type === "note" || current.type === "text") && current.text !== text;
3560
3633
  this.editingNode.contentEditable = "false";
3561
3634
  Object.assign(this.editingNode.style, {
3562
3635
  userSelect: "none",
3563
3636
  cursor: "default"
3564
3637
  });
3565
3638
  this.toolbar?.hide();
3639
+ this.beginHistory?.();
3640
+ if (textChanged) {
3641
+ store.update(this.editingId, { text });
3642
+ }
3566
3643
  if (this.editingId && this.onStopCallback) {
3567
3644
  this.onStopCallback(this.editingId);
3568
3645
  }
3646
+ this.commitHistory?.();
3569
3647
  this.editingId = null;
3570
3648
  this.editingNode = null;
3571
3649
  this.blurHandler = null;
@@ -3638,26 +3716,6 @@ var NoteEditor = class {
3638
3716
  }
3639
3717
  };
3640
3718
 
3641
- // src/elements/bounds.ts
3642
- function getElementsBoundingBox(elements) {
3643
- let minX = Infinity;
3644
- let minY = Infinity;
3645
- let maxX = -Infinity;
3646
- let maxY = -Infinity;
3647
- let found = false;
3648
- for (const el of elements) {
3649
- const b = getElementBounds(el);
3650
- if (!b) continue;
3651
- found = true;
3652
- if (b.x < minX) minX = b.x;
3653
- if (b.y < minY) minY = b.y;
3654
- if (b.x + b.w > maxX) maxX = b.x + b.w;
3655
- if (b.y + b.h > maxY) maxY = b.y + b.h;
3656
- }
3657
- if (!found) return null;
3658
- return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
3659
- }
3660
-
3661
3719
  // src/tools/tool-manager.ts
3662
3720
  var ToolManager = class {
3663
3721
  tools = /* @__PURE__ */ new Map();
@@ -5178,7 +5236,103 @@ var MarginViewport = class {
5178
5236
  }
5179
5237
  };
5180
5238
 
5239
+ // src/elements/element-style.ts
5240
+ function styleToPatch(element, style) {
5241
+ const { color, fillColor, strokeWidth, opacity, fontSize } = style;
5242
+ switch (element.type) {
5243
+ case "stroke":
5244
+ return {
5245
+ ...color !== void 0 ? { color } : {},
5246
+ ...strokeWidth !== void 0 ? { width: strokeWidth } : {},
5247
+ ...opacity !== void 0 ? { opacity } : {}
5248
+ };
5249
+ case "arrow":
5250
+ return {
5251
+ ...color !== void 0 ? { color } : {},
5252
+ ...strokeWidth !== void 0 ? { width: strokeWidth } : {}
5253
+ };
5254
+ case "shape":
5255
+ return {
5256
+ ...color !== void 0 ? { strokeColor: color } : {},
5257
+ ...fillColor !== void 0 ? { fillColor } : {},
5258
+ ...strokeWidth !== void 0 ? { strokeWidth } : {}
5259
+ };
5260
+ case "text":
5261
+ return {
5262
+ ...color !== void 0 ? { color } : {},
5263
+ ...fontSize !== void 0 ? { fontSize } : {}
5264
+ };
5265
+ case "note":
5266
+ return {
5267
+ ...color !== void 0 ? { textColor: color } : {},
5268
+ ...fillColor !== void 0 ? { backgroundColor: fillColor } : {},
5269
+ ...fontSize !== void 0 ? { fontSize } : {}
5270
+ };
5271
+ case "grid":
5272
+ return {
5273
+ ...color !== void 0 ? { strokeColor: color } : {},
5274
+ ...strokeWidth !== void 0 ? { strokeWidth } : {},
5275
+ ...opacity !== void 0 ? { opacity } : {}
5276
+ };
5277
+ case "template":
5278
+ return {
5279
+ ...color !== void 0 ? { strokeColor: color } : {},
5280
+ ...fillColor !== void 0 ? { fillColor } : {},
5281
+ ...strokeWidth !== void 0 ? { strokeWidth } : {},
5282
+ ...opacity !== void 0 ? { opacity } : {}
5283
+ };
5284
+ default:
5285
+ return {};
5286
+ }
5287
+ }
5288
+ function getElementStyle(element) {
5289
+ switch (element.type) {
5290
+ case "stroke":
5291
+ return { color: element.color, strokeWidth: element.width, opacity: element.opacity };
5292
+ case "arrow":
5293
+ return { color: element.color, strokeWidth: element.width };
5294
+ case "shape":
5295
+ return {
5296
+ color: element.strokeColor,
5297
+ fillColor: element.fillColor,
5298
+ strokeWidth: element.strokeWidth
5299
+ };
5300
+ case "text":
5301
+ return { color: element.color, fontSize: element.fontSize };
5302
+ case "note":
5303
+ return {
5304
+ color: element.textColor,
5305
+ fillColor: element.backgroundColor,
5306
+ ...element.fontSize !== void 0 ? { fontSize: element.fontSize } : {}
5307
+ };
5308
+ case "grid":
5309
+ return {
5310
+ color: element.strokeColor,
5311
+ strokeWidth: element.strokeWidth,
5312
+ opacity: element.opacity
5313
+ };
5314
+ case "template":
5315
+ return {
5316
+ color: element.strokeColor,
5317
+ fillColor: element.fillColor,
5318
+ strokeWidth: element.strokeWidth,
5319
+ opacity: element.opacity
5320
+ };
5321
+ default:
5322
+ return {};
5323
+ }
5324
+ }
5325
+
5181
5326
  // src/canvas/viewport.ts
5327
+ var EMPTY_IDS = [];
5328
+ function noop() {
5329
+ }
5330
+ function sharedValue(values) {
5331
+ const present = values.filter((v) => v !== void 0);
5332
+ if (present.length === 0) return void 0;
5333
+ const first = present[0];
5334
+ return present.every((v) => v === first) ? first : void 0;
5335
+ }
5182
5336
  var Viewport = class {
5183
5337
  constructor(container, options = {}) {
5184
5338
  this.container = container;
@@ -5212,6 +5366,10 @@ var Viewport = class {
5212
5366
  placeholder: options.placeholder
5213
5367
  });
5214
5368
  this.noteEditor.setOnStop((id) => this.onTextEditStop(id));
5369
+ this.noteEditor.setHistoryHooks(
5370
+ () => this.historyRecorder.begin(),
5371
+ () => this.historyRecorder.commit()
5372
+ );
5215
5373
  this.onHtmlElementMount = options.onHtmlElementMount;
5216
5374
  this.dropHandler = options.onDrop;
5217
5375
  this.history = new HistoryStack();
@@ -5228,6 +5386,7 @@ var Viewport = class {
5228
5386
  requestRender: () => this.requestRender(),
5229
5387
  switchTool: (name) => this.toolManager.setTool(name, this.toolContext),
5230
5388
  editElement: (id) => this.startEditingElement(id),
5389
+ fitNoteHeight: (id) => this.fitNoteHeight(id),
5231
5390
  setCursor: (cursor) => {
5232
5391
  this.wrapper.style.cursor = cursor;
5233
5392
  },
@@ -5525,6 +5684,52 @@ var Viewport = class {
5525
5684
  this.gridChangeListeners.delete(listener);
5526
5685
  };
5527
5686
  }
5687
+ getSelectTool() {
5688
+ return this.toolManager.getTool("select");
5689
+ }
5690
+ getSelectedIds() {
5691
+ return this.getSelectTool()?.selectedIds ?? EMPTY_IDS;
5692
+ }
5693
+ onSelectionChange(listener) {
5694
+ const tool = this.getSelectTool();
5695
+ return tool ? tool.onSelectionChange(listener) : noop;
5696
+ }
5697
+ getSelectionStyle() {
5698
+ const ids = this.getSelectedIds();
5699
+ if (ids.length === 0) return null;
5700
+ const styles = [];
5701
+ for (const id of ids) {
5702
+ const el = this.store.getById(id);
5703
+ if (el) styles.push(getElementStyle(el));
5704
+ }
5705
+ if (styles.length === 0) return null;
5706
+ const result = {};
5707
+ const color = sharedValue(styles.map((s) => s.color));
5708
+ if (color !== void 0) result.color = color;
5709
+ const fillColor = sharedValue(styles.map((s) => s.fillColor));
5710
+ if (fillColor !== void 0) result.fillColor = fillColor;
5711
+ const strokeWidth = sharedValue(styles.map((s) => s.strokeWidth));
5712
+ if (strokeWidth !== void 0) result.strokeWidth = strokeWidth;
5713
+ const opacity = sharedValue(styles.map((s) => s.opacity));
5714
+ if (opacity !== void 0) result.opacity = opacity;
5715
+ const fontSize = sharedValue(styles.map((s) => s.fontSize));
5716
+ if (fontSize !== void 0) result.fontSize = fontSize;
5717
+ return result;
5718
+ }
5719
+ applyStyleToSelection(style) {
5720
+ const ids = this.getSelectedIds();
5721
+ if (ids.length === 0) return;
5722
+ this.historyRecorder.begin();
5723
+ for (const id of ids) {
5724
+ const el = this.store.getById(id);
5725
+ if (!el) continue;
5726
+ const patch = styleToPatch(el, style);
5727
+ if (Object.keys(patch).length > 0) {
5728
+ this.store.update(id, patch);
5729
+ }
5730
+ }
5731
+ this.historyRecorder.commit();
5732
+ }
5528
5733
  getRenderStats() {
5529
5734
  return this.renderLoop.getStats();
5530
5735
  }
@@ -5562,31 +5767,38 @@ var Viewport = class {
5562
5767
  this.noteEditor.startEditing(node, id, this.store);
5563
5768
  }
5564
5769
  }
5770
+ fitNoteHeight(elementId) {
5771
+ const element = this.store.getById(elementId);
5772
+ if (!element || element.type !== "note") return;
5773
+ if (isNoteContentEmpty(element.text)) return;
5774
+ const node = this.domNodeManager.getNode(elementId);
5775
+ if (!node) return;
5776
+ const measured = node.scrollHeight;
5777
+ if (measured > element.size.h) {
5778
+ this.store.update(elementId, { size: { w: element.size.w, h: measured } });
5779
+ }
5780
+ }
5565
5781
  onTextEditStop(elementId) {
5566
5782
  const element = this.store.getById(elementId);
5567
5783
  if (!element) return;
5568
5784
  if (element.type === "note") {
5569
5785
  if (isNoteContentEmpty(element.text)) {
5570
- this.historyRecorder.begin();
5571
5786
  this.store.remove(elementId);
5572
- this.historyRecorder.commit();
5787
+ return;
5573
5788
  }
5789
+ this.fitNoteHeight(elementId);
5574
5790
  return;
5575
5791
  }
5576
5792
  if (element.type !== "text") return;
5577
5793
  if (!element.text || element.text.trim() === "") {
5578
- this.historyRecorder.begin();
5579
5794
  this.store.remove(elementId);
5580
- this.historyRecorder.commit();
5581
5795
  return;
5582
5796
  }
5583
5797
  const node = this.domNodeManager.getNode(elementId);
5584
5798
  if (node && "size" in element) {
5585
- const measuredHeight = node.scrollHeight;
5586
- if (measuredHeight !== element.size.h) {
5587
- this.store.update(elementId, {
5588
- size: { w: element.size.w, h: measuredHeight }
5589
- });
5799
+ const measured = node.scrollHeight;
5800
+ if (measured !== element.size.h) {
5801
+ this.store.update(elementId, { size: { w: element.size.w, h: measured } });
5590
5802
  }
5591
5803
  }
5592
5804
  }
@@ -6133,6 +6345,7 @@ var HANDLE_CURSORS = {
6133
6345
  var SelectTool = class {
6134
6346
  name = "select";
6135
6347
  _selectedIds = [];
6348
+ selectionListeners = /* @__PURE__ */ new Set();
6136
6349
  mode = { type: "idle" };
6137
6350
  lastWorld = { x: 0, y: 0 };
6138
6351
  currentWorld = { x: 0, y: 0 };
@@ -6142,10 +6355,22 @@ var SelectTool = class {
6142
6355
  resizeAspectRatio = 0;
6143
6356
  hoveredId = null;
6144
6357
  get selectedIds() {
6145
- return [...this._selectedIds];
6358
+ return this._selectedIds;
6146
6359
  }
6147
- setSelection(ids) {
6360
+ onSelectionChange(listener) {
6361
+ this.selectionListeners.add(listener);
6362
+ return () => {
6363
+ this.selectionListeners.delete(listener);
6364
+ };
6365
+ }
6366
+ setSelectedIds(ids) {
6367
+ const prev = this._selectedIds;
6368
+ if (prev.length === ids.length && prev.every((id, i) => id === ids[i])) return;
6148
6369
  this._selectedIds = ids;
6370
+ for (const listener of this.selectionListeners) listener();
6371
+ }
6372
+ setSelection(ids) {
6373
+ this.setSelectedIds(ids);
6149
6374
  this.ctx?.requestRender();
6150
6375
  }
6151
6376
  get isMarqueeActive() {
@@ -6155,7 +6380,7 @@ var SelectTool = class {
6155
6380
  this.ctx = ctx;
6156
6381
  }
6157
6382
  onDeactivate(ctx) {
6158
- this._selectedIds = [];
6383
+ this.setSelectedIds([]);
6159
6384
  this.mode = { type: "idle" };
6160
6385
  this.hoveredId = null;
6161
6386
  ctx.setCursor?.("default");
@@ -6206,22 +6431,22 @@ var SelectTool = class {
6206
6431
  const alreadySelected = this._selectedIds.includes(hit.id);
6207
6432
  if (state.shiftKey) {
6208
6433
  if (alreadySelected) {
6209
- this._selectedIds = this._selectedIds.filter((id) => id !== hit.id);
6434
+ this.setSelectedIds(this._selectedIds.filter((id) => id !== hit.id));
6210
6435
  this.mode = { type: "idle" };
6211
6436
  } else {
6212
- this._selectedIds = [...this._selectedIds, hit.id];
6437
+ this.setSelectedIds([...this._selectedIds, hit.id]);
6213
6438
  this.mode = hit.locked ? { type: "idle" } : { type: "dragging" };
6214
6439
  }
6215
6440
  } else {
6216
6441
  if (!alreadySelected) {
6217
- this._selectedIds = [hit.id];
6442
+ this.setSelectedIds([hit.id]);
6218
6443
  } else if (this._selectedIds.length > 1) {
6219
6444
  this.pendingSingleSelectId = hit.id;
6220
6445
  }
6221
6446
  this.mode = hit.locked ? { type: "idle" } : { type: "dragging" };
6222
6447
  }
6223
6448
  } else {
6224
- this._selectedIds = [];
6449
+ this.setSelectedIds([]);
6225
6450
  this.mode = { type: "marquee", start: world };
6226
6451
  }
6227
6452
  ctx.requestRender();
@@ -6294,17 +6519,22 @@ var SelectTool = class {
6294
6519
  if (this.mode.type === "marquee") {
6295
6520
  const rect = this.getMarqueeRect();
6296
6521
  if (rect) {
6297
- this._selectedIds = this.findElementsInRect(rect, ctx);
6522
+ this.setSelectedIds(this.findElementsInRect(rect, ctx));
6298
6523
  }
6299
6524
  ctx.requestRender();
6300
6525
  }
6301
6526
  if (!this.hasDragged && this.pendingSingleSelectId !== null) {
6302
- this._selectedIds = [this.pendingSingleSelectId];
6527
+ this.setSelectedIds([this.pendingSingleSelectId]);
6303
6528
  }
6304
6529
  this.pendingSingleSelectId = null;
6305
6530
  this.hasDragged = false;
6531
+ const resizedNoteId = this.mode.type === "resizing" ? this.mode.elementId : null;
6306
6532
  this.mode = { type: "idle" };
6307
6533
  ctx.setCursor?.("default");
6534
+ if (resizedNoteId !== null) {
6535
+ const el = ctx.store.getById(resizedNoteId);
6536
+ if (el?.type === "note") ctx.fitNoteHeight?.(resizedNoteId);
6537
+ }
6308
6538
  }
6309
6539
  onHover(state, ctx) {
6310
6540
  const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
@@ -7516,7 +7746,7 @@ var TemplateTool = class {
7516
7746
  };
7517
7747
 
7518
7748
  // src/index.ts
7519
- var VERSION = "0.25.0";
7749
+ var VERSION = "0.27.0";
7520
7750
  // Annotate the CommonJS export names for ESM import in node:
7521
7751
  0 && (module.exports = {
7522
7752
  ArrowTool,
@@ -7558,6 +7788,7 @@ var VERSION = "0.25.0";
7558
7788
  getArrowTangentAngle,
7559
7789
  getBendFromPoint,
7560
7790
  getElementBounds,
7791
+ getElementStyle,
7561
7792
  getElementsBoundingBox,
7562
7793
  getHexCellsInCone,
7563
7794
  getHexCellsInLine,
@@ -7569,6 +7800,7 @@ var VERSION = "0.25.0";
7569
7800
  smartSnap,
7570
7801
  snapPoint,
7571
7802
  snapToHexCenter,
7803
+ styleToPatch,
7572
7804
  toggleBold,
7573
7805
  toggleItalic,
7574
7806
  toggleStrikethrough,