@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.js CHANGED
@@ -509,6 +509,278 @@ function createId(prefix) {
509
509
  return `${prefix}_${Date.now().toString(36)}_${(counter++).toString(36)}`;
510
510
  }
511
511
 
512
+ // src/core/geometry.ts
513
+ function distSqToSegment(p, a, b) {
514
+ const abx = b.x - a.x;
515
+ const aby = b.y - a.y;
516
+ const apx = p.x - a.x;
517
+ const apy = p.y - a.y;
518
+ const lenSq = abx * abx + aby * aby;
519
+ if (lenSq === 0) {
520
+ return apx * apx + apy * apy;
521
+ }
522
+ const t = Math.max(0, Math.min(1, (apx * abx + apy * aby) / lenSq));
523
+ const dx = p.x - (a.x + t * abx);
524
+ const dy = p.y - (a.y + t * aby);
525
+ return dx * dx + dy * dy;
526
+ }
527
+
528
+ // src/elements/arrow-geometry.ts
529
+ function getArrowControlPoint(from, to, bend) {
530
+ const midX = (from.x + to.x) / 2;
531
+ const midY = (from.y + to.y) / 2;
532
+ if (bend === 0) return { x: midX, y: midY };
533
+ const dx = to.x - from.x;
534
+ const dy = to.y - from.y;
535
+ const len = Math.sqrt(dx * dx + dy * dy);
536
+ if (len === 0) return { x: midX, y: midY };
537
+ const perpX = -dy / len;
538
+ const perpY = dx / len;
539
+ return {
540
+ x: midX + perpX * bend,
541
+ y: midY + perpY * bend
542
+ };
543
+ }
544
+ function getArrowMidpoint(from, to, bend) {
545
+ const cp = getArrowControlPoint(from, to, bend);
546
+ return {
547
+ x: 0.25 * from.x + 0.5 * cp.x + 0.25 * to.x,
548
+ y: 0.25 * from.y + 0.5 * cp.y + 0.25 * to.y
549
+ };
550
+ }
551
+ function getBendFromPoint(from, to, dragPoint) {
552
+ const midX = (from.x + to.x) / 2;
553
+ const midY = (from.y + to.y) / 2;
554
+ const dx = to.x - from.x;
555
+ const dy = to.y - from.y;
556
+ const len = Math.sqrt(dx * dx + dy * dy);
557
+ if (len === 0) return 0;
558
+ const perpX = -dy / len;
559
+ const perpY = dx / len;
560
+ return (dragPoint.x - midX) * perpX + (dragPoint.y - midY) * perpY;
561
+ }
562
+ function getArrowTangentAngle(from, to, bend, t) {
563
+ const cp = getArrowControlPoint(from, to, bend);
564
+ const tangentX = 2 * (1 - t) * (cp.x - from.x) + 2 * t * (to.x - cp.x);
565
+ const tangentY = 2 * (1 - t) * (cp.y - from.y) + 2 * t * (to.y - cp.y);
566
+ return Math.atan2(tangentY, tangentX);
567
+ }
568
+ function isNearBezier(point, from, to, bend, threshold) {
569
+ if (bend === 0) return isNearLine(point, from, to, threshold);
570
+ const cp = getArrowControlPoint(from, to, bend);
571
+ const segments = 20;
572
+ for (let i = 0; i < segments; i++) {
573
+ const t0 = i / segments;
574
+ const t1 = (i + 1) / segments;
575
+ const a = bezierPoint(from, cp, to, t0);
576
+ const b = bezierPoint(from, cp, to, t1);
577
+ if (isNearLine(point, a, b, threshold)) return true;
578
+ }
579
+ return false;
580
+ }
581
+ function getArrowBounds(from, to, bend) {
582
+ if (bend === 0) {
583
+ const minX2 = Math.min(from.x, to.x);
584
+ const minY2 = Math.min(from.y, to.y);
585
+ return {
586
+ x: minX2,
587
+ y: minY2,
588
+ w: Math.abs(to.x - from.x),
589
+ h: Math.abs(to.y - from.y)
590
+ };
591
+ }
592
+ const cp = getArrowControlPoint(from, to, bend);
593
+ const steps = 20;
594
+ let minX = Math.min(from.x, to.x);
595
+ let minY = Math.min(from.y, to.y);
596
+ let maxX = Math.max(from.x, to.x);
597
+ let maxY = Math.max(from.y, to.y);
598
+ for (let i = 1; i < steps; i++) {
599
+ const t = i / steps;
600
+ const p = bezierPoint(from, cp, to, t);
601
+ if (p.x < minX) minX = p.x;
602
+ if (p.y < minY) minY = p.y;
603
+ if (p.x > maxX) maxX = p.x;
604
+ if (p.y > maxY) maxY = p.y;
605
+ }
606
+ return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
607
+ }
608
+ function bezierPoint(from, cp, to, t) {
609
+ const mt = 1 - t;
610
+ return {
611
+ x: mt * mt * from.x + 2 * mt * t * cp.x + t * t * to.x,
612
+ y: mt * mt * from.y + 2 * mt * t * cp.y + t * t * to.y
613
+ };
614
+ }
615
+ function isNearLine(point, a, b, threshold) {
616
+ return distSqToSegment(point, a, b) <= threshold * threshold;
617
+ }
618
+
619
+ // src/elements/element-bounds.ts
620
+ var strokeBoundsCache = /* @__PURE__ */ new WeakMap();
621
+ function getElementBounds(element) {
622
+ if (element.type === "grid") return null;
623
+ if ("size" in element) {
624
+ return {
625
+ x: element.position.x,
626
+ y: element.position.y,
627
+ w: element.size.w,
628
+ h: element.size.h
629
+ };
630
+ }
631
+ if (element.type === "stroke") {
632
+ if (element.points.length === 0) return null;
633
+ const cached = strokeBoundsCache.get(element);
634
+ if (cached) return cached;
635
+ let minX = Infinity;
636
+ let minY = Infinity;
637
+ let maxX = -Infinity;
638
+ let maxY = -Infinity;
639
+ for (const p of element.points) {
640
+ const px = p.x + element.position.x;
641
+ const py = p.y + element.position.y;
642
+ if (px < minX) minX = px;
643
+ if (py < minY) minY = py;
644
+ if (px > maxX) maxX = px;
645
+ if (py > maxY) maxY = py;
646
+ }
647
+ const bounds = { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
648
+ strokeBoundsCache.set(element, bounds);
649
+ return bounds;
650
+ }
651
+ if (element.type === "arrow") {
652
+ return getArrowBoundsAnalytical(element.from, element.to, element.bend);
653
+ }
654
+ if (element.type === "template") {
655
+ return getTemplateBounds(element);
656
+ }
657
+ return null;
658
+ }
659
+ function getArrowBoundsAnalytical(from, to, bend) {
660
+ if (bend === 0) {
661
+ const minX2 = Math.min(from.x, to.x);
662
+ const minY2 = Math.min(from.y, to.y);
663
+ return {
664
+ x: minX2,
665
+ y: minY2,
666
+ w: Math.abs(to.x - from.x),
667
+ h: Math.abs(to.y - from.y)
668
+ };
669
+ }
670
+ const cp = getArrowControlPoint(from, to, bend);
671
+ let minX = Math.min(from.x, to.x);
672
+ let maxX = Math.max(from.x, to.x);
673
+ let minY = Math.min(from.y, to.y);
674
+ let maxY = Math.max(from.y, to.y);
675
+ const tx = from.x - 2 * cp.x + to.x;
676
+ if (tx !== 0) {
677
+ const t = (from.x - cp.x) / tx;
678
+ if (t > 0 && t < 1) {
679
+ const mt = 1 - t;
680
+ const x = mt * mt * from.x + 2 * mt * t * cp.x + t * t * to.x;
681
+ if (x < minX) minX = x;
682
+ if (x > maxX) maxX = x;
683
+ }
684
+ }
685
+ const ty = from.y - 2 * cp.y + to.y;
686
+ if (ty !== 0) {
687
+ const t = (from.y - cp.y) / ty;
688
+ if (t > 0 && t < 1) {
689
+ const mt = 1 - t;
690
+ const y = mt * mt * from.y + 2 * mt * t * cp.y + t * t * to.y;
691
+ if (y < minY) minY = y;
692
+ if (y > maxY) maxY = y;
693
+ }
694
+ }
695
+ return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
696
+ }
697
+ function getTemplateBounds(el) {
698
+ const { x: cx, y: cy } = el.position;
699
+ const r = el.radius;
700
+ switch (el.templateShape) {
701
+ case "circle":
702
+ return { x: cx - r, y: cy - r, w: 2 * r, h: 2 * r };
703
+ case "square":
704
+ return { x: cx - r / 2, y: cy - r / 2, w: r, h: r };
705
+ case "cone": {
706
+ const halfAngle = Math.atan(0.5);
707
+ const tipX = cx;
708
+ const tipY = cy;
709
+ const leftX = cx + r * Math.cos(el.angle - halfAngle);
710
+ const leftY = cy + r * Math.sin(el.angle - halfAngle);
711
+ const rightX = cx + r * Math.cos(el.angle + halfAngle);
712
+ const rightY = cy + r * Math.sin(el.angle + halfAngle);
713
+ const farX = cx + r * Math.cos(el.angle);
714
+ const farY = cy + r * Math.sin(el.angle);
715
+ const xs = [tipX, leftX, rightX, farX];
716
+ const ys = [tipY, leftY, rightY, farY];
717
+ let minX = Infinity;
718
+ let minY = Infinity;
719
+ let maxX = -Infinity;
720
+ let maxY = -Infinity;
721
+ for (let i = 0; i < xs.length; i++) {
722
+ const px = xs[i];
723
+ const py = ys[i];
724
+ if (px !== void 0 && px < minX) minX = px;
725
+ if (px !== void 0 && px > maxX) maxX = px;
726
+ if (py !== void 0 && py < minY) minY = py;
727
+ if (py !== void 0 && py > maxY) maxY = py;
728
+ }
729
+ return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
730
+ }
731
+ case "line": {
732
+ const halfW = r / 12;
733
+ const cos = Math.cos(el.angle);
734
+ const sin = Math.sin(el.angle);
735
+ const perpX = -sin * halfW;
736
+ const perpY = cos * halfW;
737
+ const x0 = cx + perpX;
738
+ const y0 = cy + perpY;
739
+ const x1 = cx + r * cos + perpX;
740
+ const y1 = cy + r * sin + perpY;
741
+ const x2 = cx + r * cos - perpX;
742
+ const y2 = cy + r * sin - perpY;
743
+ const x3 = cx - perpX;
744
+ const y3 = cy - perpY;
745
+ const minX = Math.min(x0, x1, x2, x3);
746
+ const minY = Math.min(y0, y1, y2, y3);
747
+ const maxX = Math.max(x0, x1, x2, x3);
748
+ const maxY = Math.max(y0, y1, y2, y3);
749
+ return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
750
+ }
751
+ }
752
+ }
753
+ function transferStrokeBounds(prev, next) {
754
+ if (prev.type !== "stroke" || next.type !== "stroke") return;
755
+ if (prev.points !== next.points) return;
756
+ if (prev.position.x !== next.position.x || prev.position.y !== next.position.y) return;
757
+ const bounds = strokeBoundsCache.get(prev);
758
+ if (bounds) strokeBoundsCache.set(next, bounds);
759
+ }
760
+ function boundsIntersect(a, b) {
761
+ 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;
762
+ }
763
+
764
+ // src/elements/bounds.ts
765
+ function getElementsBoundingBox(elements) {
766
+ let minX = Infinity;
767
+ let minY = Infinity;
768
+ let maxX = -Infinity;
769
+ let maxY = -Infinity;
770
+ let found = false;
771
+ for (const el of elements) {
772
+ const b = getElementBounds(el);
773
+ if (!b) continue;
774
+ found = true;
775
+ if (b.x < minX) minX = b.x;
776
+ if (b.y < minY) minY = b.y;
777
+ if (b.x + b.w > maxX) maxX = b.x + b.w;
778
+ if (b.y + b.h > maxY) maxY = b.y + b.h;
779
+ }
780
+ if (!found) return null;
781
+ return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
782
+ }
783
+
512
784
  // src/canvas/keyboard-actions.ts
513
785
  var KeyboardActions = class {
514
786
  constructor(deps) {
@@ -614,8 +886,18 @@ var KeyboardActions = class {
614
886
  if (this.clipboard.length === 0) return;
615
887
  const sel = this.selectTool();
616
888
  if (!sel) return;
889
+ const cursor = this.deps.getLastPointerWorld?.() ?? null;
890
+ if (cursor) {
891
+ const bbox = getElementsBoundingBox(this.clipboard);
892
+ if (bbox) {
893
+ const centerX = bbox.x + bbox.w / 2;
894
+ const centerY = bbox.y + bbox.h / 2;
895
+ this.insertClones(this.clipboard, { x: cursor.x - centerX, y: cursor.y - centerY }, sel);
896
+ return;
897
+ }
898
+ }
617
899
  this.pasteCount++;
618
- this.insertClones(this.clipboard, this.pasteCount * 20, sel);
900
+ this.insertClones(this.clipboard, { x: this.pasteCount * 20, y: this.pasteCount * 20 }, sel);
619
901
  }
620
902
  duplicate() {
621
903
  if (this.deps.isToolActive()) return;
@@ -628,7 +910,7 @@ var KeyboardActions = class {
628
910
  if (el) source.push(el);
629
911
  }
630
912
  if (source.length === 0) return;
631
- this.insertClones(source, 20, sel);
913
+ this.insertClones(source, { x: 20, y: 20 }, sel);
632
914
  }
633
915
  deselect() {
634
916
  if (this.deps.isToolActive()) return;
@@ -699,11 +981,11 @@ var KeyboardActions = class {
699
981
  const newId = idMap.get(el.id);
700
982
  if (!newId) continue;
701
983
  clone.id = newId;
702
- clone.position = { x: clone.position.x + offset, y: clone.position.y + offset };
984
+ clone.position = { x: clone.position.x + offset.x, y: clone.position.y + offset.y };
703
985
  if (clone.type === "arrow") {
704
986
  const arrow = clone;
705
- arrow.from = { x: arrow.from.x + offset, y: arrow.from.y + offset };
706
- arrow.to = { x: arrow.to.x + offset, y: arrow.to.y + offset };
987
+ arrow.from = { x: arrow.from.x + offset.x, y: arrow.from.y + offset.y };
988
+ arrow.to = { x: arrow.to.x + offset.x, y: arrow.to.y + offset.y };
707
989
  delete arrow.cachedControlPoint;
708
990
  if (arrow.fromBinding) {
709
991
  const newTarget = idMap.get(arrow.fromBinding.elementId);
@@ -749,6 +1031,9 @@ var DEFAULT_BINDINGS = [
749
1031
  ["z-front", ["mod+]"]],
750
1032
  ["z-back", ["mod+["]],
751
1033
  ["zoom-fit", ["shift+1"]],
1034
+ ["zoom-in", ["mod+="]],
1035
+ ["zoom-out", ["mod+-"]],
1036
+ ["zoom-reset", ["mod+0"]],
752
1037
  ["nudge-left", ["arrowleft"]],
753
1038
  ["nudge-right", ["arrowright"]],
754
1039
  ["nudge-up", ["arrowup"]],
@@ -885,6 +1170,7 @@ var ShortcutMap = class {
885
1170
 
886
1171
  // src/canvas/input-handler.ts
887
1172
  var ZOOM_SENSITIVITY = 1e-3;
1173
+ var ZOOM_STEP = 1.2;
888
1174
  var MIDDLE_BUTTON = 1;
889
1175
  var NUDGE_DELTAS = {
890
1176
  "nudge-left": [-1, 0],
@@ -906,7 +1192,8 @@ var InputHandler = class {
906
1192
  getHistoryRecorder: () => this.historyRecorder,
907
1193
  getHistoryStack: () => this.historyStack,
908
1194
  isToolActive: () => this.isToolActive,
909
- fitToContent: options.fitToContent
1195
+ fitToContent: options.fitToContent,
1196
+ getLastPointerWorld: () => this.lastPointerWorld()
910
1197
  });
911
1198
  this.shortcutMap = new ShortcutMap(options.shortcuts?.bindings);
912
1199
  this.scope = options.shortcuts?.scope ?? "focus";
@@ -962,11 +1249,21 @@ var InputHandler = class {
962
1249
  this.element.addEventListener("pointerdown", this.onPointerDown, opts);
963
1250
  this.element.addEventListener("pointermove", this.onPointerMove, opts);
964
1251
  this.element.addEventListener("pointerup", this.onPointerUp, opts);
965
- this.element.addEventListener("pointerleave", this.onPointerUp, opts);
1252
+ this.element.addEventListener("pointerleave", this.onPointerLeave, opts);
966
1253
  this.element.addEventListener("pointercancel", this.onPointerUp, opts);
967
1254
  window.addEventListener("keydown", this.onKeyDown, opts);
968
1255
  window.addEventListener("keyup", this.onKeyUp, opts);
969
1256
  }
1257
+ viewportCenter() {
1258
+ const rect = this.element.getBoundingClientRect();
1259
+ return { x: rect.width / 2, y: rect.height / 2 };
1260
+ }
1261
+ zoomByFactor(factor) {
1262
+ this.camera.zoomAt(this.camera.zoom * factor, this.viewportCenter());
1263
+ }
1264
+ zoomToLevel(level) {
1265
+ this.camera.zoomAt(level, this.viewportCenter());
1266
+ }
970
1267
  onWheel = (e) => {
971
1268
  e.preventDefault();
972
1269
  const rect = this.element.getBoundingClientRect();
@@ -1135,6 +1432,18 @@ var InputHandler = class {
1135
1432
  e.preventDefault();
1136
1433
  this.actions.zoomToFit();
1137
1434
  return;
1435
+ case "zoom-in":
1436
+ e.preventDefault();
1437
+ this.zoomByFactor(ZOOM_STEP);
1438
+ return;
1439
+ case "zoom-out":
1440
+ e.preventDefault();
1441
+ this.zoomByFactor(1 / ZOOM_STEP);
1442
+ return;
1443
+ case "zoom-reset":
1444
+ e.preventDefault();
1445
+ this.zoomToLevel(1);
1446
+ return;
1138
1447
  case "nudge-left":
1139
1448
  case "nudge-right":
1140
1449
  case "nudge-up":
@@ -1192,6 +1501,16 @@ var InputHandler = class {
1192
1501
  midpoint(a, b) {
1193
1502
  return { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
1194
1503
  }
1504
+ lastPointerWorld() {
1505
+ const e = this.lastPointerEvent;
1506
+ if (!e) return null;
1507
+ const rect = this.element.getBoundingClientRect();
1508
+ return this.camera.screenToWorld({ x: e.clientX - rect.left, y: e.clientY - rect.top });
1509
+ }
1510
+ onPointerLeave = (e) => {
1511
+ this.lastPointerEvent = null;
1512
+ this.onPointerUp(e);
1513
+ };
1195
1514
  toPointerState(e) {
1196
1515
  const rect = this.element.getBoundingClientRect();
1197
1516
  return {
@@ -1494,320 +1813,68 @@ var QuadNode = class _QuadNode {
1494
1813
  ];
1495
1814
  const remaining = [];
1496
1815
  for (const item of this.items) {
1497
- const idx = this.getChildIndex(item.bounds);
1498
- if (idx !== -1) {
1499
- const target = this.children[idx];
1500
- if (target) target.insert(item);
1501
- } else {
1502
- remaining.push(item);
1503
- }
1504
- }
1505
- this.items = remaining;
1506
- }
1507
- collapseIfEmpty() {
1508
- if (!this.children) return;
1509
- let totalItems = this.items.length;
1510
- for (const child of this.children) {
1511
- if (child.children) return;
1512
- totalItems += child.items.length;
1513
- }
1514
- if (totalItems <= MAX_ITEMS) {
1515
- for (const child of this.children) {
1516
- this.items.push(...child.items);
1517
- }
1518
- this.children = null;
1519
- }
1520
- }
1521
- };
1522
- var Quadtree = class {
1523
- root;
1524
- _size = 0;
1525
- worldBounds;
1526
- constructor(worldBounds) {
1527
- this.worldBounds = worldBounds;
1528
- this.root = new QuadNode(worldBounds, 0);
1529
- }
1530
- get size() {
1531
- return this._size;
1532
- }
1533
- insert(id, bounds) {
1534
- this.root.insert({ id, bounds });
1535
- this._size++;
1536
- }
1537
- remove(id) {
1538
- if (this.root.remove(id)) {
1539
- this._size--;
1540
- }
1541
- }
1542
- update(id, newBounds) {
1543
- this.remove(id);
1544
- this.insert(id, newBounds);
1545
- }
1546
- query(rect) {
1547
- const result = [];
1548
- this.root.query(rect, result);
1549
- return result;
1550
- }
1551
- queryPoint(point) {
1552
- return this.query({ x: point.x, y: point.y, w: 0, h: 0 });
1553
- }
1554
- clear() {
1555
- this.root = new QuadNode(this.worldBounds, 0);
1556
- this._size = 0;
1557
- }
1558
- };
1559
-
1560
- // src/core/geometry.ts
1561
- function distSqToSegment(p, a, b) {
1562
- const abx = b.x - a.x;
1563
- const aby = b.y - a.y;
1564
- const apx = p.x - a.x;
1565
- const apy = p.y - a.y;
1566
- const lenSq = abx * abx + aby * aby;
1567
- if (lenSq === 0) {
1568
- return apx * apx + apy * apy;
1569
- }
1570
- const t = Math.max(0, Math.min(1, (apx * abx + apy * aby) / lenSq));
1571
- const dx = p.x - (a.x + t * abx);
1572
- const dy = p.y - (a.y + t * aby);
1573
- return dx * dx + dy * dy;
1574
- }
1575
-
1576
- // src/elements/arrow-geometry.ts
1577
- function getArrowControlPoint(from, to, bend) {
1578
- const midX = (from.x + to.x) / 2;
1579
- const midY = (from.y + to.y) / 2;
1580
- if (bend === 0) return { x: midX, y: midY };
1581
- const dx = to.x - from.x;
1582
- const dy = to.y - from.y;
1583
- const len = Math.sqrt(dx * dx + dy * dy);
1584
- if (len === 0) return { x: midX, y: midY };
1585
- const perpX = -dy / len;
1586
- const perpY = dx / len;
1587
- return {
1588
- x: midX + perpX * bend,
1589
- y: midY + perpY * bend
1590
- };
1591
- }
1592
- function getArrowMidpoint(from, to, bend) {
1593
- const cp = getArrowControlPoint(from, to, bend);
1594
- return {
1595
- x: 0.25 * from.x + 0.5 * cp.x + 0.25 * to.x,
1596
- y: 0.25 * from.y + 0.5 * cp.y + 0.25 * to.y
1597
- };
1598
- }
1599
- function getBendFromPoint(from, to, dragPoint) {
1600
- const midX = (from.x + to.x) / 2;
1601
- const midY = (from.y + to.y) / 2;
1602
- const dx = to.x - from.x;
1603
- const dy = to.y - from.y;
1604
- const len = Math.sqrt(dx * dx + dy * dy);
1605
- if (len === 0) return 0;
1606
- const perpX = -dy / len;
1607
- const perpY = dx / len;
1608
- return (dragPoint.x - midX) * perpX + (dragPoint.y - midY) * perpY;
1609
- }
1610
- function getArrowTangentAngle(from, to, bend, t) {
1611
- const cp = getArrowControlPoint(from, to, bend);
1612
- const tangentX = 2 * (1 - t) * (cp.x - from.x) + 2 * t * (to.x - cp.x);
1613
- const tangentY = 2 * (1 - t) * (cp.y - from.y) + 2 * t * (to.y - cp.y);
1614
- return Math.atan2(tangentY, tangentX);
1615
- }
1616
- function isNearBezier(point, from, to, bend, threshold) {
1617
- if (bend === 0) return isNearLine(point, from, to, threshold);
1618
- const cp = getArrowControlPoint(from, to, bend);
1619
- const segments = 20;
1620
- for (let i = 0; i < segments; i++) {
1621
- const t0 = i / segments;
1622
- const t1 = (i + 1) / segments;
1623
- const a = bezierPoint(from, cp, to, t0);
1624
- const b = bezierPoint(from, cp, to, t1);
1625
- if (isNearLine(point, a, b, threshold)) return true;
1626
- }
1627
- return false;
1628
- }
1629
- function getArrowBounds(from, to, bend) {
1630
- if (bend === 0) {
1631
- const minX2 = Math.min(from.x, to.x);
1632
- const minY2 = Math.min(from.y, to.y);
1633
- return {
1634
- x: minX2,
1635
- y: minY2,
1636
- w: Math.abs(to.x - from.x),
1637
- h: Math.abs(to.y - from.y)
1638
- };
1639
- }
1640
- const cp = getArrowControlPoint(from, to, bend);
1641
- const steps = 20;
1642
- let minX = Math.min(from.x, to.x);
1643
- let minY = Math.min(from.y, to.y);
1644
- let maxX = Math.max(from.x, to.x);
1645
- let maxY = Math.max(from.y, to.y);
1646
- for (let i = 1; i < steps; i++) {
1647
- const t = i / steps;
1648
- const p = bezierPoint(from, cp, to, t);
1649
- if (p.x < minX) minX = p.x;
1650
- if (p.y < minY) minY = p.y;
1651
- if (p.x > maxX) maxX = p.x;
1652
- if (p.y > maxY) maxY = p.y;
1653
- }
1654
- return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
1655
- }
1656
- function bezierPoint(from, cp, to, t) {
1657
- const mt = 1 - t;
1658
- return {
1659
- x: mt * mt * from.x + 2 * mt * t * cp.x + t * t * to.x,
1660
- y: mt * mt * from.y + 2 * mt * t * cp.y + t * t * to.y
1661
- };
1662
- }
1663
- function isNearLine(point, a, b, threshold) {
1664
- return distSqToSegment(point, a, b) <= threshold * threshold;
1665
- }
1666
-
1667
- // src/elements/element-bounds.ts
1668
- var strokeBoundsCache = /* @__PURE__ */ new WeakMap();
1669
- function getElementBounds(element) {
1670
- if (element.type === "grid") return null;
1671
- if ("size" in element) {
1672
- return {
1673
- x: element.position.x,
1674
- y: element.position.y,
1675
- w: element.size.w,
1676
- h: element.size.h
1677
- };
1678
- }
1679
- if (element.type === "stroke") {
1680
- if (element.points.length === 0) return null;
1681
- const cached = strokeBoundsCache.get(element);
1682
- if (cached) return cached;
1683
- let minX = Infinity;
1684
- let minY = Infinity;
1685
- let maxX = -Infinity;
1686
- let maxY = -Infinity;
1687
- for (const p of element.points) {
1688
- const px = p.x + element.position.x;
1689
- const py = p.y + element.position.y;
1690
- if (px < minX) minX = px;
1691
- if (py < minY) minY = py;
1692
- if (px > maxX) maxX = px;
1693
- if (py > maxY) maxY = py;
1816
+ const idx = this.getChildIndex(item.bounds);
1817
+ if (idx !== -1) {
1818
+ const target = this.children[idx];
1819
+ if (target) target.insert(item);
1820
+ } else {
1821
+ remaining.push(item);
1822
+ }
1694
1823
  }
1695
- const bounds = { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
1696
- strokeBoundsCache.set(element, bounds);
1697
- return bounds;
1824
+ this.items = remaining;
1698
1825
  }
1699
- if (element.type === "arrow") {
1700
- return getArrowBoundsAnalytical(element.from, element.to, element.bend);
1826
+ collapseIfEmpty() {
1827
+ if (!this.children) return;
1828
+ let totalItems = this.items.length;
1829
+ for (const child of this.children) {
1830
+ if (child.children) return;
1831
+ totalItems += child.items.length;
1832
+ }
1833
+ if (totalItems <= MAX_ITEMS) {
1834
+ for (const child of this.children) {
1835
+ this.items.push(...child.items);
1836
+ }
1837
+ this.children = null;
1838
+ }
1701
1839
  }
1702
- if (element.type === "template") {
1703
- return getTemplateBounds(element);
1840
+ };
1841
+ var Quadtree = class {
1842
+ root;
1843
+ _size = 0;
1844
+ worldBounds;
1845
+ constructor(worldBounds) {
1846
+ this.worldBounds = worldBounds;
1847
+ this.root = new QuadNode(worldBounds, 0);
1704
1848
  }
1705
- return null;
1706
- }
1707
- function getArrowBoundsAnalytical(from, to, bend) {
1708
- if (bend === 0) {
1709
- const minX2 = Math.min(from.x, to.x);
1710
- const minY2 = Math.min(from.y, to.y);
1711
- return {
1712
- x: minX2,
1713
- y: minY2,
1714
- w: Math.abs(to.x - from.x),
1715
- h: Math.abs(to.y - from.y)
1716
- };
1849
+ get size() {
1850
+ return this._size;
1717
1851
  }
1718
- const cp = getArrowControlPoint(from, to, bend);
1719
- let minX = Math.min(from.x, to.x);
1720
- let maxX = Math.max(from.x, to.x);
1721
- let minY = Math.min(from.y, to.y);
1722
- let maxY = Math.max(from.y, to.y);
1723
- const tx = from.x - 2 * cp.x + to.x;
1724
- if (tx !== 0) {
1725
- const t = (from.x - cp.x) / tx;
1726
- if (t > 0 && t < 1) {
1727
- const mt = 1 - t;
1728
- const x = mt * mt * from.x + 2 * mt * t * cp.x + t * t * to.x;
1729
- if (x < minX) minX = x;
1730
- if (x > maxX) maxX = x;
1731
- }
1852
+ insert(id, bounds) {
1853
+ this.root.insert({ id, bounds });
1854
+ this._size++;
1732
1855
  }
1733
- const ty = from.y - 2 * cp.y + to.y;
1734
- if (ty !== 0) {
1735
- const t = (from.y - cp.y) / ty;
1736
- if (t > 0 && t < 1) {
1737
- const mt = 1 - t;
1738
- const y = mt * mt * from.y + 2 * mt * t * cp.y + t * t * to.y;
1739
- if (y < minY) minY = y;
1740
- if (y > maxY) maxY = y;
1856
+ remove(id) {
1857
+ if (this.root.remove(id)) {
1858
+ this._size--;
1741
1859
  }
1742
1860
  }
1743
- return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
1744
- }
1745
- function getTemplateBounds(el) {
1746
- const { x: cx, y: cy } = el.position;
1747
- const r = el.radius;
1748
- switch (el.templateShape) {
1749
- case "circle":
1750
- return { x: cx - r, y: cy - r, w: 2 * r, h: 2 * r };
1751
- case "square":
1752
- return { x: cx - r / 2, y: cy - r / 2, w: r, h: r };
1753
- case "cone": {
1754
- const halfAngle = Math.atan(0.5);
1755
- const tipX = cx;
1756
- const tipY = cy;
1757
- const leftX = cx + r * Math.cos(el.angle - halfAngle);
1758
- const leftY = cy + r * Math.sin(el.angle - halfAngle);
1759
- const rightX = cx + r * Math.cos(el.angle + halfAngle);
1760
- const rightY = cy + r * Math.sin(el.angle + halfAngle);
1761
- const farX = cx + r * Math.cos(el.angle);
1762
- const farY = cy + r * Math.sin(el.angle);
1763
- const xs = [tipX, leftX, rightX, farX];
1764
- const ys = [tipY, leftY, rightY, farY];
1765
- let minX = Infinity;
1766
- let minY = Infinity;
1767
- let maxX = -Infinity;
1768
- let maxY = -Infinity;
1769
- for (let i = 0; i < xs.length; i++) {
1770
- const px = xs[i];
1771
- const py = ys[i];
1772
- if (px !== void 0 && px < minX) minX = px;
1773
- if (px !== void 0 && px > maxX) maxX = px;
1774
- if (py !== void 0 && py < minY) minY = py;
1775
- if (py !== void 0 && py > maxY) maxY = py;
1776
- }
1777
- return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
1778
- }
1779
- case "line": {
1780
- const halfW = r / 12;
1781
- const cos = Math.cos(el.angle);
1782
- const sin = Math.sin(el.angle);
1783
- const perpX = -sin * halfW;
1784
- const perpY = cos * halfW;
1785
- const x0 = cx + perpX;
1786
- const y0 = cy + perpY;
1787
- const x1 = cx + r * cos + perpX;
1788
- const y1 = cy + r * sin + perpY;
1789
- const x2 = cx + r * cos - perpX;
1790
- const y2 = cy + r * sin - perpY;
1791
- const x3 = cx - perpX;
1792
- const y3 = cy - perpY;
1793
- const minX = Math.min(x0, x1, x2, x3);
1794
- const minY = Math.min(y0, y1, y2, y3);
1795
- const maxX = Math.max(x0, x1, x2, x3);
1796
- const maxY = Math.max(y0, y1, y2, y3);
1797
- return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
1798
- }
1861
+ update(id, newBounds) {
1862
+ this.remove(id);
1863
+ this.insert(id, newBounds);
1799
1864
  }
1800
- }
1801
- function transferStrokeBounds(prev, next) {
1802
- if (prev.type !== "stroke" || next.type !== "stroke") return;
1803
- if (prev.points !== next.points) return;
1804
- if (prev.position.x !== next.position.x || prev.position.y !== next.position.y) return;
1805
- const bounds = strokeBoundsCache.get(prev);
1806
- if (bounds) strokeBoundsCache.set(next, bounds);
1807
- }
1808
- function boundsIntersect(a, b) {
1809
- 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;
1810
- }
1865
+ query(rect) {
1866
+ const result = [];
1867
+ this.root.query(rect, result);
1868
+ return result;
1869
+ }
1870
+ queryPoint(point) {
1871
+ return this.query({ x: point.x, y: point.y, w: 0, h: 0 });
1872
+ }
1873
+ clear() {
1874
+ this.root = new QuadNode(this.worldBounds, 0);
1875
+ this._size = 0;
1876
+ }
1877
+ };
1811
1878
 
1812
1879
  // src/elements/stroke-smoothing.ts
1813
1880
  var MIN_PRESSURE_SCALE = 0.2;
@@ -3429,6 +3496,8 @@ var NoteEditor = class {
3429
3496
  inputHandler = null;
3430
3497
  pendingEditId = null;
3431
3498
  onStopCallback = null;
3499
+ beginHistory = null;
3500
+ commitHistory = null;
3432
3501
  toolbar;
3433
3502
  placeholder;
3434
3503
  constructor(options) {
@@ -3444,6 +3513,10 @@ var NoteEditor = class {
3444
3513
  setOnStop(callback) {
3445
3514
  this.onStopCallback = callback;
3446
3515
  }
3516
+ setHistoryHooks(begin, commit) {
3517
+ this.beginHistory = begin;
3518
+ this.commitHistory = commit;
3519
+ }
3447
3520
  startEditing(node, elementId, store) {
3448
3521
  if (this.editingId === elementId) return;
3449
3522
  if (this.editingId) {
@@ -3475,18 +3548,21 @@ var NoteEditor = class {
3475
3548
  this.editingNode.removeAttribute("data-fn-empty");
3476
3549
  const text = sanitizeNoteHtml(this.editingNode.innerHTML);
3477
3550
  const current = store.getById(this.editingId);
3478
- if (current && (current.type === "note" || current.type === "text") && current.text !== text) {
3479
- store.update(this.editingId, { text });
3480
- }
3551
+ const textChanged = !!current && (current.type === "note" || current.type === "text") && current.text !== text;
3481
3552
  this.editingNode.contentEditable = "false";
3482
3553
  Object.assign(this.editingNode.style, {
3483
3554
  userSelect: "none",
3484
3555
  cursor: "default"
3485
3556
  });
3486
3557
  this.toolbar?.hide();
3558
+ this.beginHistory?.();
3559
+ if (textChanged) {
3560
+ store.update(this.editingId, { text });
3561
+ }
3487
3562
  if (this.editingId && this.onStopCallback) {
3488
3563
  this.onStopCallback(this.editingId);
3489
3564
  }
3565
+ this.commitHistory?.();
3490
3566
  this.editingId = null;
3491
3567
  this.editingNode = null;
3492
3568
  this.blurHandler = null;
@@ -3559,26 +3635,6 @@ var NoteEditor = class {
3559
3635
  }
3560
3636
  };
3561
3637
 
3562
- // src/elements/bounds.ts
3563
- function getElementsBoundingBox(elements) {
3564
- let minX = Infinity;
3565
- let minY = Infinity;
3566
- let maxX = -Infinity;
3567
- let maxY = -Infinity;
3568
- let found = false;
3569
- for (const el of elements) {
3570
- const b = getElementBounds(el);
3571
- if (!b) continue;
3572
- found = true;
3573
- if (b.x < minX) minX = b.x;
3574
- if (b.y < minY) minY = b.y;
3575
- if (b.x + b.w > maxX) maxX = b.x + b.w;
3576
- if (b.y + b.h > maxY) maxY = b.y + b.h;
3577
- }
3578
- if (!found) return null;
3579
- return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
3580
- }
3581
-
3582
3638
  // src/tools/tool-manager.ts
3583
3639
  var ToolManager = class {
3584
3640
  tools = /* @__PURE__ */ new Map();
@@ -5099,7 +5155,103 @@ var MarginViewport = class {
5099
5155
  }
5100
5156
  };
5101
5157
 
5158
+ // src/elements/element-style.ts
5159
+ function styleToPatch(element, style) {
5160
+ const { color, fillColor, strokeWidth, opacity, fontSize } = style;
5161
+ switch (element.type) {
5162
+ case "stroke":
5163
+ return {
5164
+ ...color !== void 0 ? { color } : {},
5165
+ ...strokeWidth !== void 0 ? { width: strokeWidth } : {},
5166
+ ...opacity !== void 0 ? { opacity } : {}
5167
+ };
5168
+ case "arrow":
5169
+ return {
5170
+ ...color !== void 0 ? { color } : {},
5171
+ ...strokeWidth !== void 0 ? { width: strokeWidth } : {}
5172
+ };
5173
+ case "shape":
5174
+ return {
5175
+ ...color !== void 0 ? { strokeColor: color } : {},
5176
+ ...fillColor !== void 0 ? { fillColor } : {},
5177
+ ...strokeWidth !== void 0 ? { strokeWidth } : {}
5178
+ };
5179
+ case "text":
5180
+ return {
5181
+ ...color !== void 0 ? { color } : {},
5182
+ ...fontSize !== void 0 ? { fontSize } : {}
5183
+ };
5184
+ case "note":
5185
+ return {
5186
+ ...color !== void 0 ? { textColor: color } : {},
5187
+ ...fillColor !== void 0 ? { backgroundColor: fillColor } : {},
5188
+ ...fontSize !== void 0 ? { fontSize } : {}
5189
+ };
5190
+ case "grid":
5191
+ return {
5192
+ ...color !== void 0 ? { strokeColor: color } : {},
5193
+ ...strokeWidth !== void 0 ? { strokeWidth } : {},
5194
+ ...opacity !== void 0 ? { opacity } : {}
5195
+ };
5196
+ case "template":
5197
+ return {
5198
+ ...color !== void 0 ? { strokeColor: color } : {},
5199
+ ...fillColor !== void 0 ? { fillColor } : {},
5200
+ ...strokeWidth !== void 0 ? { strokeWidth } : {},
5201
+ ...opacity !== void 0 ? { opacity } : {}
5202
+ };
5203
+ default:
5204
+ return {};
5205
+ }
5206
+ }
5207
+ function getElementStyle(element) {
5208
+ switch (element.type) {
5209
+ case "stroke":
5210
+ return { color: element.color, strokeWidth: element.width, opacity: element.opacity };
5211
+ case "arrow":
5212
+ return { color: element.color, strokeWidth: element.width };
5213
+ case "shape":
5214
+ return {
5215
+ color: element.strokeColor,
5216
+ fillColor: element.fillColor,
5217
+ strokeWidth: element.strokeWidth
5218
+ };
5219
+ case "text":
5220
+ return { color: element.color, fontSize: element.fontSize };
5221
+ case "note":
5222
+ return {
5223
+ color: element.textColor,
5224
+ fillColor: element.backgroundColor,
5225
+ ...element.fontSize !== void 0 ? { fontSize: element.fontSize } : {}
5226
+ };
5227
+ case "grid":
5228
+ return {
5229
+ color: element.strokeColor,
5230
+ strokeWidth: element.strokeWidth,
5231
+ opacity: element.opacity
5232
+ };
5233
+ case "template":
5234
+ return {
5235
+ color: element.strokeColor,
5236
+ fillColor: element.fillColor,
5237
+ strokeWidth: element.strokeWidth,
5238
+ opacity: element.opacity
5239
+ };
5240
+ default:
5241
+ return {};
5242
+ }
5243
+ }
5244
+
5102
5245
  // src/canvas/viewport.ts
5246
+ var EMPTY_IDS = [];
5247
+ function noop() {
5248
+ }
5249
+ function sharedValue(values) {
5250
+ const present = values.filter((v) => v !== void 0);
5251
+ if (present.length === 0) return void 0;
5252
+ const first = present[0];
5253
+ return present.every((v) => v === first) ? first : void 0;
5254
+ }
5103
5255
  var Viewport = class {
5104
5256
  constructor(container, options = {}) {
5105
5257
  this.container = container;
@@ -5133,6 +5285,10 @@ var Viewport = class {
5133
5285
  placeholder: options.placeholder
5134
5286
  });
5135
5287
  this.noteEditor.setOnStop((id) => this.onTextEditStop(id));
5288
+ this.noteEditor.setHistoryHooks(
5289
+ () => this.historyRecorder.begin(),
5290
+ () => this.historyRecorder.commit()
5291
+ );
5136
5292
  this.onHtmlElementMount = options.onHtmlElementMount;
5137
5293
  this.dropHandler = options.onDrop;
5138
5294
  this.history = new HistoryStack();
@@ -5149,6 +5305,7 @@ var Viewport = class {
5149
5305
  requestRender: () => this.requestRender(),
5150
5306
  switchTool: (name) => this.toolManager.setTool(name, this.toolContext),
5151
5307
  editElement: (id) => this.startEditingElement(id),
5308
+ fitNoteHeight: (id) => this.fitNoteHeight(id),
5152
5309
  setCursor: (cursor) => {
5153
5310
  this.wrapper.style.cursor = cursor;
5154
5311
  },
@@ -5446,6 +5603,52 @@ var Viewport = class {
5446
5603
  this.gridChangeListeners.delete(listener);
5447
5604
  };
5448
5605
  }
5606
+ getSelectTool() {
5607
+ return this.toolManager.getTool("select");
5608
+ }
5609
+ getSelectedIds() {
5610
+ return this.getSelectTool()?.selectedIds ?? EMPTY_IDS;
5611
+ }
5612
+ onSelectionChange(listener) {
5613
+ const tool = this.getSelectTool();
5614
+ return tool ? tool.onSelectionChange(listener) : noop;
5615
+ }
5616
+ getSelectionStyle() {
5617
+ const ids = this.getSelectedIds();
5618
+ if (ids.length === 0) return null;
5619
+ const styles = [];
5620
+ for (const id of ids) {
5621
+ const el = this.store.getById(id);
5622
+ if (el) styles.push(getElementStyle(el));
5623
+ }
5624
+ if (styles.length === 0) return null;
5625
+ const result = {};
5626
+ const color = sharedValue(styles.map((s) => s.color));
5627
+ if (color !== void 0) result.color = color;
5628
+ const fillColor = sharedValue(styles.map((s) => s.fillColor));
5629
+ if (fillColor !== void 0) result.fillColor = fillColor;
5630
+ const strokeWidth = sharedValue(styles.map((s) => s.strokeWidth));
5631
+ if (strokeWidth !== void 0) result.strokeWidth = strokeWidth;
5632
+ const opacity = sharedValue(styles.map((s) => s.opacity));
5633
+ if (opacity !== void 0) result.opacity = opacity;
5634
+ const fontSize = sharedValue(styles.map((s) => s.fontSize));
5635
+ if (fontSize !== void 0) result.fontSize = fontSize;
5636
+ return result;
5637
+ }
5638
+ applyStyleToSelection(style) {
5639
+ const ids = this.getSelectedIds();
5640
+ if (ids.length === 0) return;
5641
+ this.historyRecorder.begin();
5642
+ for (const id of ids) {
5643
+ const el = this.store.getById(id);
5644
+ if (!el) continue;
5645
+ const patch = styleToPatch(el, style);
5646
+ if (Object.keys(patch).length > 0) {
5647
+ this.store.update(id, patch);
5648
+ }
5649
+ }
5650
+ this.historyRecorder.commit();
5651
+ }
5449
5652
  getRenderStats() {
5450
5653
  return this.renderLoop.getStats();
5451
5654
  }
@@ -5483,31 +5686,38 @@ var Viewport = class {
5483
5686
  this.noteEditor.startEditing(node, id, this.store);
5484
5687
  }
5485
5688
  }
5689
+ fitNoteHeight(elementId) {
5690
+ const element = this.store.getById(elementId);
5691
+ if (!element || element.type !== "note") return;
5692
+ if (isNoteContentEmpty(element.text)) return;
5693
+ const node = this.domNodeManager.getNode(elementId);
5694
+ if (!node) return;
5695
+ const measured = node.scrollHeight;
5696
+ if (measured > element.size.h) {
5697
+ this.store.update(elementId, { size: { w: element.size.w, h: measured } });
5698
+ }
5699
+ }
5486
5700
  onTextEditStop(elementId) {
5487
5701
  const element = this.store.getById(elementId);
5488
5702
  if (!element) return;
5489
5703
  if (element.type === "note") {
5490
5704
  if (isNoteContentEmpty(element.text)) {
5491
- this.historyRecorder.begin();
5492
5705
  this.store.remove(elementId);
5493
- this.historyRecorder.commit();
5706
+ return;
5494
5707
  }
5708
+ this.fitNoteHeight(elementId);
5495
5709
  return;
5496
5710
  }
5497
5711
  if (element.type !== "text") return;
5498
5712
  if (!element.text || element.text.trim() === "") {
5499
- this.historyRecorder.begin();
5500
5713
  this.store.remove(elementId);
5501
- this.historyRecorder.commit();
5502
5714
  return;
5503
5715
  }
5504
5716
  const node = this.domNodeManager.getNode(elementId);
5505
5717
  if (node && "size" in element) {
5506
- const measuredHeight = node.scrollHeight;
5507
- if (measuredHeight !== element.size.h) {
5508
- this.store.update(elementId, {
5509
- size: { w: element.size.w, h: measuredHeight }
5510
- });
5718
+ const measured = node.scrollHeight;
5719
+ if (measured !== element.size.h) {
5720
+ this.store.update(elementId, { size: { w: element.size.w, h: measured } });
5511
5721
  }
5512
5722
  }
5513
5723
  }
@@ -6054,6 +6264,7 @@ var HANDLE_CURSORS = {
6054
6264
  var SelectTool = class {
6055
6265
  name = "select";
6056
6266
  _selectedIds = [];
6267
+ selectionListeners = /* @__PURE__ */ new Set();
6057
6268
  mode = { type: "idle" };
6058
6269
  lastWorld = { x: 0, y: 0 };
6059
6270
  currentWorld = { x: 0, y: 0 };
@@ -6063,10 +6274,22 @@ var SelectTool = class {
6063
6274
  resizeAspectRatio = 0;
6064
6275
  hoveredId = null;
6065
6276
  get selectedIds() {
6066
- return [...this._selectedIds];
6277
+ return this._selectedIds;
6067
6278
  }
6068
- setSelection(ids) {
6279
+ onSelectionChange(listener) {
6280
+ this.selectionListeners.add(listener);
6281
+ return () => {
6282
+ this.selectionListeners.delete(listener);
6283
+ };
6284
+ }
6285
+ setSelectedIds(ids) {
6286
+ const prev = this._selectedIds;
6287
+ if (prev.length === ids.length && prev.every((id, i) => id === ids[i])) return;
6069
6288
  this._selectedIds = ids;
6289
+ for (const listener of this.selectionListeners) listener();
6290
+ }
6291
+ setSelection(ids) {
6292
+ this.setSelectedIds(ids);
6070
6293
  this.ctx?.requestRender();
6071
6294
  }
6072
6295
  get isMarqueeActive() {
@@ -6076,7 +6299,7 @@ var SelectTool = class {
6076
6299
  this.ctx = ctx;
6077
6300
  }
6078
6301
  onDeactivate(ctx) {
6079
- this._selectedIds = [];
6302
+ this.setSelectedIds([]);
6080
6303
  this.mode = { type: "idle" };
6081
6304
  this.hoveredId = null;
6082
6305
  ctx.setCursor?.("default");
@@ -6127,22 +6350,22 @@ var SelectTool = class {
6127
6350
  const alreadySelected = this._selectedIds.includes(hit.id);
6128
6351
  if (state.shiftKey) {
6129
6352
  if (alreadySelected) {
6130
- this._selectedIds = this._selectedIds.filter((id) => id !== hit.id);
6353
+ this.setSelectedIds(this._selectedIds.filter((id) => id !== hit.id));
6131
6354
  this.mode = { type: "idle" };
6132
6355
  } else {
6133
- this._selectedIds = [...this._selectedIds, hit.id];
6356
+ this.setSelectedIds([...this._selectedIds, hit.id]);
6134
6357
  this.mode = hit.locked ? { type: "idle" } : { type: "dragging" };
6135
6358
  }
6136
6359
  } else {
6137
6360
  if (!alreadySelected) {
6138
- this._selectedIds = [hit.id];
6361
+ this.setSelectedIds([hit.id]);
6139
6362
  } else if (this._selectedIds.length > 1) {
6140
6363
  this.pendingSingleSelectId = hit.id;
6141
6364
  }
6142
6365
  this.mode = hit.locked ? { type: "idle" } : { type: "dragging" };
6143
6366
  }
6144
6367
  } else {
6145
- this._selectedIds = [];
6368
+ this.setSelectedIds([]);
6146
6369
  this.mode = { type: "marquee", start: world };
6147
6370
  }
6148
6371
  ctx.requestRender();
@@ -6215,17 +6438,22 @@ var SelectTool = class {
6215
6438
  if (this.mode.type === "marquee") {
6216
6439
  const rect = this.getMarqueeRect();
6217
6440
  if (rect) {
6218
- this._selectedIds = this.findElementsInRect(rect, ctx);
6441
+ this.setSelectedIds(this.findElementsInRect(rect, ctx));
6219
6442
  }
6220
6443
  ctx.requestRender();
6221
6444
  }
6222
6445
  if (!this.hasDragged && this.pendingSingleSelectId !== null) {
6223
- this._selectedIds = [this.pendingSingleSelectId];
6446
+ this.setSelectedIds([this.pendingSingleSelectId]);
6224
6447
  }
6225
6448
  this.pendingSingleSelectId = null;
6226
6449
  this.hasDragged = false;
6450
+ const resizedNoteId = this.mode.type === "resizing" ? this.mode.elementId : null;
6227
6451
  this.mode = { type: "idle" };
6228
6452
  ctx.setCursor?.("default");
6453
+ if (resizedNoteId !== null) {
6454
+ const el = ctx.store.getById(resizedNoteId);
6455
+ if (el?.type === "note") ctx.fitNoteHeight?.(resizedNoteId);
6456
+ }
6229
6457
  }
6230
6458
  onHover(state, ctx) {
6231
6459
  const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
@@ -7437,7 +7665,7 @@ var TemplateTool = class {
7437
7665
  };
7438
7666
 
7439
7667
  // src/index.ts
7440
- var VERSION = "0.25.0";
7668
+ var VERSION = "0.27.0";
7441
7669
  export {
7442
7670
  ArrowTool,
7443
7671
  AutoSave,
@@ -7478,6 +7706,7 @@ export {
7478
7706
  getArrowTangentAngle,
7479
7707
  getBendFromPoint,
7480
7708
  getElementBounds,
7709
+ getElementStyle,
7481
7710
  getElementsBoundingBox,
7482
7711
  getHexCellsInCone,
7483
7712
  getHexCellsInLine,
@@ -7489,6 +7718,7 @@ export {
7489
7718
  smartSnap,
7490
7719
  snapPoint,
7491
7720
  snapToHexCenter,
7721
+ styleToPatch,
7492
7722
  toggleBold,
7493
7723
  toggleItalic,
7494
7724
  toggleStrikethrough,