@fieldnotes/core 0.25.0 → 0.26.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
@@ -588,6 +588,278 @@ function createId(prefix) {
588
588
  return `${prefix}_${Date.now().toString(36)}_${(counter++).toString(36)}`;
589
589
  }
590
590
 
591
+ // src/core/geometry.ts
592
+ function distSqToSegment(p, a, b) {
593
+ const abx = b.x - a.x;
594
+ const aby = b.y - a.y;
595
+ const apx = p.x - a.x;
596
+ const apy = p.y - a.y;
597
+ const lenSq = abx * abx + aby * aby;
598
+ if (lenSq === 0) {
599
+ return apx * apx + apy * apy;
600
+ }
601
+ const t = Math.max(0, Math.min(1, (apx * abx + apy * aby) / lenSq));
602
+ const dx = p.x - (a.x + t * abx);
603
+ const dy = p.y - (a.y + t * aby);
604
+ return dx * dx + dy * dy;
605
+ }
606
+
607
+ // src/elements/arrow-geometry.ts
608
+ function getArrowControlPoint(from, to, bend) {
609
+ const midX = (from.x + to.x) / 2;
610
+ const midY = (from.y + to.y) / 2;
611
+ if (bend === 0) return { x: midX, y: midY };
612
+ const dx = to.x - from.x;
613
+ const dy = to.y - from.y;
614
+ const len = Math.sqrt(dx * dx + dy * dy);
615
+ if (len === 0) return { x: midX, y: midY };
616
+ const perpX = -dy / len;
617
+ const perpY = dx / len;
618
+ return {
619
+ x: midX + perpX * bend,
620
+ y: midY + perpY * bend
621
+ };
622
+ }
623
+ function getArrowMidpoint(from, to, bend) {
624
+ const cp = getArrowControlPoint(from, to, bend);
625
+ return {
626
+ x: 0.25 * from.x + 0.5 * cp.x + 0.25 * to.x,
627
+ y: 0.25 * from.y + 0.5 * cp.y + 0.25 * to.y
628
+ };
629
+ }
630
+ function getBendFromPoint(from, to, dragPoint) {
631
+ const midX = (from.x + to.x) / 2;
632
+ const midY = (from.y + to.y) / 2;
633
+ const dx = to.x - from.x;
634
+ const dy = to.y - from.y;
635
+ const len = Math.sqrt(dx * dx + dy * dy);
636
+ if (len === 0) return 0;
637
+ const perpX = -dy / len;
638
+ const perpY = dx / len;
639
+ return (dragPoint.x - midX) * perpX + (dragPoint.y - midY) * perpY;
640
+ }
641
+ function getArrowTangentAngle(from, to, bend, t) {
642
+ const cp = getArrowControlPoint(from, to, bend);
643
+ const tangentX = 2 * (1 - t) * (cp.x - from.x) + 2 * t * (to.x - cp.x);
644
+ const tangentY = 2 * (1 - t) * (cp.y - from.y) + 2 * t * (to.y - cp.y);
645
+ return Math.atan2(tangentY, tangentX);
646
+ }
647
+ function isNearBezier(point, from, to, bend, threshold) {
648
+ if (bend === 0) return isNearLine(point, from, to, threshold);
649
+ const cp = getArrowControlPoint(from, to, bend);
650
+ const segments = 20;
651
+ for (let i = 0; i < segments; i++) {
652
+ const t0 = i / segments;
653
+ const t1 = (i + 1) / segments;
654
+ const a = bezierPoint(from, cp, to, t0);
655
+ const b = bezierPoint(from, cp, to, t1);
656
+ if (isNearLine(point, a, b, threshold)) return true;
657
+ }
658
+ return false;
659
+ }
660
+ function getArrowBounds(from, to, bend) {
661
+ if (bend === 0) {
662
+ const minX2 = Math.min(from.x, to.x);
663
+ const minY2 = Math.min(from.y, to.y);
664
+ return {
665
+ x: minX2,
666
+ y: minY2,
667
+ w: Math.abs(to.x - from.x),
668
+ h: Math.abs(to.y - from.y)
669
+ };
670
+ }
671
+ const cp = getArrowControlPoint(from, to, bend);
672
+ const steps = 20;
673
+ let minX = Math.min(from.x, to.x);
674
+ let minY = Math.min(from.y, to.y);
675
+ let maxX = Math.max(from.x, to.x);
676
+ let maxY = Math.max(from.y, to.y);
677
+ for (let i = 1; i < steps; i++) {
678
+ const t = i / steps;
679
+ const p = bezierPoint(from, cp, to, t);
680
+ if (p.x < minX) minX = p.x;
681
+ if (p.y < minY) minY = p.y;
682
+ if (p.x > maxX) maxX = p.x;
683
+ if (p.y > maxY) maxY = p.y;
684
+ }
685
+ return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
686
+ }
687
+ function bezierPoint(from, cp, to, t) {
688
+ const mt = 1 - t;
689
+ return {
690
+ x: mt * mt * from.x + 2 * mt * t * cp.x + t * t * to.x,
691
+ y: mt * mt * from.y + 2 * mt * t * cp.y + t * t * to.y
692
+ };
693
+ }
694
+ function isNearLine(point, a, b, threshold) {
695
+ return distSqToSegment(point, a, b) <= threshold * threshold;
696
+ }
697
+
698
+ // src/elements/element-bounds.ts
699
+ var strokeBoundsCache = /* @__PURE__ */ new WeakMap();
700
+ function getElementBounds(element) {
701
+ if (element.type === "grid") return null;
702
+ if ("size" in element) {
703
+ return {
704
+ x: element.position.x,
705
+ y: element.position.y,
706
+ w: element.size.w,
707
+ h: element.size.h
708
+ };
709
+ }
710
+ if (element.type === "stroke") {
711
+ if (element.points.length === 0) return null;
712
+ const cached = strokeBoundsCache.get(element);
713
+ if (cached) return cached;
714
+ let minX = Infinity;
715
+ let minY = Infinity;
716
+ let maxX = -Infinity;
717
+ let maxY = -Infinity;
718
+ for (const p of element.points) {
719
+ const px = p.x + element.position.x;
720
+ const py = p.y + element.position.y;
721
+ if (px < minX) minX = px;
722
+ if (py < minY) minY = py;
723
+ if (px > maxX) maxX = px;
724
+ if (py > maxY) maxY = py;
725
+ }
726
+ const bounds = { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
727
+ strokeBoundsCache.set(element, bounds);
728
+ return bounds;
729
+ }
730
+ if (element.type === "arrow") {
731
+ return getArrowBoundsAnalytical(element.from, element.to, element.bend);
732
+ }
733
+ if (element.type === "template") {
734
+ return getTemplateBounds(element);
735
+ }
736
+ return null;
737
+ }
738
+ function getArrowBoundsAnalytical(from, to, bend) {
739
+ if (bend === 0) {
740
+ const minX2 = Math.min(from.x, to.x);
741
+ const minY2 = Math.min(from.y, to.y);
742
+ return {
743
+ x: minX2,
744
+ y: minY2,
745
+ w: Math.abs(to.x - from.x),
746
+ h: Math.abs(to.y - from.y)
747
+ };
748
+ }
749
+ const cp = getArrowControlPoint(from, to, bend);
750
+ let minX = Math.min(from.x, to.x);
751
+ let maxX = Math.max(from.x, to.x);
752
+ let minY = Math.min(from.y, to.y);
753
+ let maxY = Math.max(from.y, to.y);
754
+ const tx = from.x - 2 * cp.x + to.x;
755
+ if (tx !== 0) {
756
+ const t = (from.x - cp.x) / tx;
757
+ if (t > 0 && t < 1) {
758
+ const mt = 1 - t;
759
+ const x = mt * mt * from.x + 2 * mt * t * cp.x + t * t * to.x;
760
+ if (x < minX) minX = x;
761
+ if (x > maxX) maxX = x;
762
+ }
763
+ }
764
+ const ty = from.y - 2 * cp.y + to.y;
765
+ if (ty !== 0) {
766
+ const t = (from.y - cp.y) / ty;
767
+ if (t > 0 && t < 1) {
768
+ const mt = 1 - t;
769
+ const y = mt * mt * from.y + 2 * mt * t * cp.y + t * t * to.y;
770
+ if (y < minY) minY = y;
771
+ if (y > maxY) maxY = y;
772
+ }
773
+ }
774
+ return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
775
+ }
776
+ function getTemplateBounds(el) {
777
+ const { x: cx, y: cy } = el.position;
778
+ const r = el.radius;
779
+ switch (el.templateShape) {
780
+ case "circle":
781
+ return { x: cx - r, y: cy - r, w: 2 * r, h: 2 * r };
782
+ case "square":
783
+ return { x: cx - r / 2, y: cy - r / 2, w: r, h: r };
784
+ case "cone": {
785
+ const halfAngle = Math.atan(0.5);
786
+ const tipX = cx;
787
+ const tipY = cy;
788
+ const leftX = cx + r * Math.cos(el.angle - halfAngle);
789
+ const leftY = cy + r * Math.sin(el.angle - halfAngle);
790
+ const rightX = cx + r * Math.cos(el.angle + halfAngle);
791
+ const rightY = cy + r * Math.sin(el.angle + halfAngle);
792
+ const farX = cx + r * Math.cos(el.angle);
793
+ const farY = cy + r * Math.sin(el.angle);
794
+ const xs = [tipX, leftX, rightX, farX];
795
+ const ys = [tipY, leftY, rightY, farY];
796
+ let minX = Infinity;
797
+ let minY = Infinity;
798
+ let maxX = -Infinity;
799
+ let maxY = -Infinity;
800
+ for (let i = 0; i < xs.length; i++) {
801
+ const px = xs[i];
802
+ const py = ys[i];
803
+ if (px !== void 0 && px < minX) minX = px;
804
+ if (px !== void 0 && px > maxX) maxX = px;
805
+ if (py !== void 0 && py < minY) minY = py;
806
+ if (py !== void 0 && py > maxY) maxY = py;
807
+ }
808
+ return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
809
+ }
810
+ case "line": {
811
+ const halfW = r / 12;
812
+ const cos = Math.cos(el.angle);
813
+ const sin = Math.sin(el.angle);
814
+ const perpX = -sin * halfW;
815
+ const perpY = cos * halfW;
816
+ const x0 = cx + perpX;
817
+ const y0 = cy + perpY;
818
+ const x1 = cx + r * cos + perpX;
819
+ const y1 = cy + r * sin + perpY;
820
+ const x2 = cx + r * cos - perpX;
821
+ const y2 = cy + r * sin - perpY;
822
+ const x3 = cx - perpX;
823
+ const y3 = cy - perpY;
824
+ const minX = Math.min(x0, x1, x2, x3);
825
+ const minY = Math.min(y0, y1, y2, y3);
826
+ const maxX = Math.max(x0, x1, x2, x3);
827
+ const maxY = Math.max(y0, y1, y2, y3);
828
+ return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
829
+ }
830
+ }
831
+ }
832
+ function transferStrokeBounds(prev, next) {
833
+ if (prev.type !== "stroke" || next.type !== "stroke") return;
834
+ if (prev.points !== next.points) return;
835
+ if (prev.position.x !== next.position.x || prev.position.y !== next.position.y) return;
836
+ const bounds = strokeBoundsCache.get(prev);
837
+ if (bounds) strokeBoundsCache.set(next, bounds);
838
+ }
839
+ function boundsIntersect(a, b) {
840
+ 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;
841
+ }
842
+
843
+ // src/elements/bounds.ts
844
+ function getElementsBoundingBox(elements) {
845
+ let minX = Infinity;
846
+ let minY = Infinity;
847
+ let maxX = -Infinity;
848
+ let maxY = -Infinity;
849
+ let found = false;
850
+ for (const el of elements) {
851
+ const b = getElementBounds(el);
852
+ if (!b) continue;
853
+ found = true;
854
+ if (b.x < minX) minX = b.x;
855
+ if (b.y < minY) minY = b.y;
856
+ if (b.x + b.w > maxX) maxX = b.x + b.w;
857
+ if (b.y + b.h > maxY) maxY = b.y + b.h;
858
+ }
859
+ if (!found) return null;
860
+ return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
861
+ }
862
+
591
863
  // src/canvas/keyboard-actions.ts
592
864
  var KeyboardActions = class {
593
865
  constructor(deps) {
@@ -693,8 +965,18 @@ var KeyboardActions = class {
693
965
  if (this.clipboard.length === 0) return;
694
966
  const sel = this.selectTool();
695
967
  if (!sel) return;
968
+ const cursor = this.deps.getLastPointerWorld?.() ?? null;
969
+ if (cursor) {
970
+ const bbox = getElementsBoundingBox(this.clipboard);
971
+ if (bbox) {
972
+ const centerX = bbox.x + bbox.w / 2;
973
+ const centerY = bbox.y + bbox.h / 2;
974
+ this.insertClones(this.clipboard, { x: cursor.x - centerX, y: cursor.y - centerY }, sel);
975
+ return;
976
+ }
977
+ }
696
978
  this.pasteCount++;
697
- this.insertClones(this.clipboard, this.pasteCount * 20, sel);
979
+ this.insertClones(this.clipboard, { x: this.pasteCount * 20, y: this.pasteCount * 20 }, sel);
698
980
  }
699
981
  duplicate() {
700
982
  if (this.deps.isToolActive()) return;
@@ -707,7 +989,7 @@ var KeyboardActions = class {
707
989
  if (el) source.push(el);
708
990
  }
709
991
  if (source.length === 0) return;
710
- this.insertClones(source, 20, sel);
992
+ this.insertClones(source, { x: 20, y: 20 }, sel);
711
993
  }
712
994
  deselect() {
713
995
  if (this.deps.isToolActive()) return;
@@ -778,11 +1060,11 @@ var KeyboardActions = class {
778
1060
  const newId = idMap.get(el.id);
779
1061
  if (!newId) continue;
780
1062
  clone.id = newId;
781
- clone.position = { x: clone.position.x + offset, y: clone.position.y + offset };
1063
+ clone.position = { x: clone.position.x + offset.x, y: clone.position.y + offset.y };
782
1064
  if (clone.type === "arrow") {
783
1065
  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 };
1066
+ arrow.from = { x: arrow.from.x + offset.x, y: arrow.from.y + offset.y };
1067
+ arrow.to = { x: arrow.to.x + offset.x, y: arrow.to.y + offset.y };
786
1068
  delete arrow.cachedControlPoint;
787
1069
  if (arrow.fromBinding) {
788
1070
  const newTarget = idMap.get(arrow.fromBinding.elementId);
@@ -828,6 +1110,9 @@ var DEFAULT_BINDINGS = [
828
1110
  ["z-front", ["mod+]"]],
829
1111
  ["z-back", ["mod+["]],
830
1112
  ["zoom-fit", ["shift+1"]],
1113
+ ["zoom-in", ["mod+="]],
1114
+ ["zoom-out", ["mod+-"]],
1115
+ ["zoom-reset", ["mod+0"]],
831
1116
  ["nudge-left", ["arrowleft"]],
832
1117
  ["nudge-right", ["arrowright"]],
833
1118
  ["nudge-up", ["arrowup"]],
@@ -964,6 +1249,7 @@ var ShortcutMap = class {
964
1249
 
965
1250
  // src/canvas/input-handler.ts
966
1251
  var ZOOM_SENSITIVITY = 1e-3;
1252
+ var ZOOM_STEP = 1.2;
967
1253
  var MIDDLE_BUTTON = 1;
968
1254
  var NUDGE_DELTAS = {
969
1255
  "nudge-left": [-1, 0],
@@ -985,7 +1271,8 @@ var InputHandler = class {
985
1271
  getHistoryRecorder: () => this.historyRecorder,
986
1272
  getHistoryStack: () => this.historyStack,
987
1273
  isToolActive: () => this.isToolActive,
988
- fitToContent: options.fitToContent
1274
+ fitToContent: options.fitToContent,
1275
+ getLastPointerWorld: () => this.lastPointerWorld()
989
1276
  });
990
1277
  this.shortcutMap = new ShortcutMap(options.shortcuts?.bindings);
991
1278
  this.scope = options.shortcuts?.scope ?? "focus";
@@ -1041,11 +1328,21 @@ var InputHandler = class {
1041
1328
  this.element.addEventListener("pointerdown", this.onPointerDown, opts);
1042
1329
  this.element.addEventListener("pointermove", this.onPointerMove, opts);
1043
1330
  this.element.addEventListener("pointerup", this.onPointerUp, opts);
1044
- this.element.addEventListener("pointerleave", this.onPointerUp, opts);
1331
+ this.element.addEventListener("pointerleave", this.onPointerLeave, opts);
1045
1332
  this.element.addEventListener("pointercancel", this.onPointerUp, opts);
1046
1333
  window.addEventListener("keydown", this.onKeyDown, opts);
1047
1334
  window.addEventListener("keyup", this.onKeyUp, opts);
1048
1335
  }
1336
+ viewportCenter() {
1337
+ const rect = this.element.getBoundingClientRect();
1338
+ return { x: rect.width / 2, y: rect.height / 2 };
1339
+ }
1340
+ zoomByFactor(factor) {
1341
+ this.camera.zoomAt(this.camera.zoom * factor, this.viewportCenter());
1342
+ }
1343
+ zoomToLevel(level) {
1344
+ this.camera.zoomAt(level, this.viewportCenter());
1345
+ }
1049
1346
  onWheel = (e) => {
1050
1347
  e.preventDefault();
1051
1348
  const rect = this.element.getBoundingClientRect();
@@ -1214,6 +1511,18 @@ var InputHandler = class {
1214
1511
  e.preventDefault();
1215
1512
  this.actions.zoomToFit();
1216
1513
  return;
1514
+ case "zoom-in":
1515
+ e.preventDefault();
1516
+ this.zoomByFactor(ZOOM_STEP);
1517
+ return;
1518
+ case "zoom-out":
1519
+ e.preventDefault();
1520
+ this.zoomByFactor(1 / ZOOM_STEP);
1521
+ return;
1522
+ case "zoom-reset":
1523
+ e.preventDefault();
1524
+ this.zoomToLevel(1);
1525
+ return;
1217
1526
  case "nudge-left":
1218
1527
  case "nudge-right":
1219
1528
  case "nudge-up":
@@ -1271,6 +1580,16 @@ var InputHandler = class {
1271
1580
  midpoint(a, b) {
1272
1581
  return { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
1273
1582
  }
1583
+ lastPointerWorld() {
1584
+ const e = this.lastPointerEvent;
1585
+ if (!e) return null;
1586
+ const rect = this.element.getBoundingClientRect();
1587
+ return this.camera.screenToWorld({ x: e.clientX - rect.left, y: e.clientY - rect.top });
1588
+ }
1589
+ onPointerLeave = (e) => {
1590
+ this.lastPointerEvent = null;
1591
+ this.onPointerUp(e);
1592
+ };
1274
1593
  toPointerState(e) {
1275
1594
  const rect = this.element.getBoundingClientRect();
1276
1595
  return {
@@ -1425,468 +1744,216 @@ var Background = class {
1425
1744
  adaptSpacing(baseSpacing, zoom) {
1426
1745
  let spacing = baseSpacing * zoom;
1427
1746
  while (spacing < MIN_PATTERN_SPACING) {
1428
- spacing *= 2;
1429
- }
1430
- return spacing;
1431
- }
1432
- renderDots(ctx, camera, width, height) {
1433
- const spacing = this.adaptSpacing(this.spacing, camera.zoom);
1434
- const offsetX = camera.position.x % spacing;
1435
- const offsetY = camera.position.y % spacing;
1436
- const radius = this.dotRadius * Math.min(camera.zoom, 2);
1437
- ctx.fillStyle = this.color;
1438
- ctx.beginPath();
1439
- for (let x = offsetX; x < width; x += spacing) {
1440
- for (let y = offsetY; y < height; y += spacing) {
1441
- ctx.moveTo(x + radius, y);
1442
- ctx.arc(x, y, radius, 0, Math.PI * 2);
1443
- }
1444
- }
1445
- ctx.fill();
1446
- }
1447
- renderGrid(ctx, camera, width, height) {
1448
- const spacing = this.adaptSpacing(this.spacing, camera.zoom);
1449
- const offsetX = camera.position.x % spacing;
1450
- const offsetY = camera.position.y % spacing;
1451
- const lineW = this.lineWidth * Math.min(camera.zoom, 2);
1452
- ctx.fillStyle = this.color;
1453
- for (let x = offsetX; x < width; x += spacing) {
1454
- ctx.fillRect(x, 0, lineW, height);
1455
- }
1456
- for (let y = offsetY; y < height; y += spacing) {
1457
- ctx.fillRect(0, y, width, lineW);
1458
- }
1459
- }
1460
- };
1461
-
1462
- // src/core/event-bus.ts
1463
- var EventBus = class {
1464
- listeners = /* @__PURE__ */ new Map();
1465
- on(event, listener) {
1466
- const existing = this.listeners.get(event);
1467
- if (existing) {
1468
- existing.add(listener);
1469
- } else {
1470
- const set = /* @__PURE__ */ new Set([listener]);
1471
- this.listeners.set(event, set);
1472
- }
1473
- return () => this.off(event, listener);
1474
- }
1475
- off(event, listener) {
1476
- this.listeners.get(event)?.delete(listener);
1477
- }
1478
- emit(event, data) {
1479
- this.listeners.get(event)?.forEach((listener) => {
1480
- try {
1481
- listener(data);
1482
- } catch (err) {
1483
- console.error(`[fieldnotes] listener error for "${String(event)}"`, err);
1484
- }
1485
- });
1486
- }
1487
- clear() {
1488
- this.listeners.clear();
1489
- }
1490
- };
1491
-
1492
- // src/core/quadtree.ts
1493
- var MAX_ITEMS = 8;
1494
- var MAX_DEPTH = 8;
1495
- function intersects(a, b) {
1496
- 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;
1497
- }
1498
- var QuadNode = class _QuadNode {
1499
- constructor(bounds, depth) {
1500
- this.bounds = bounds;
1501
- this.depth = depth;
1502
- }
1503
- items = [];
1504
- children = null;
1505
- insert(entry) {
1506
- if (this.children) {
1507
- const idx = this.getChildIndex(entry.bounds);
1508
- if (idx !== -1) {
1509
- const child = this.children[idx];
1510
- if (child) child.insert(entry);
1511
- return;
1512
- }
1513
- this.items.push(entry);
1514
- return;
1515
- }
1516
- this.items.push(entry);
1517
- if (this.items.length > MAX_ITEMS && this.depth < MAX_DEPTH) {
1518
- this.split();
1519
- }
1520
- }
1521
- remove(id) {
1522
- const idx = this.items.findIndex((e) => e.id === id);
1523
- if (idx !== -1) {
1524
- this.items.splice(idx, 1);
1525
- return true;
1526
- }
1527
- if (this.children) {
1528
- for (const child of this.children) {
1529
- if (child.remove(id)) {
1530
- this.collapseIfEmpty();
1531
- return true;
1532
- }
1533
- }
1534
- }
1535
- return false;
1536
- }
1537
- query(rect, result) {
1538
- if (!intersects(this.bounds, rect)) return;
1539
- for (const item of this.items) {
1540
- if (intersects(item.bounds, rect)) {
1541
- result.push(item.id);
1542
- }
1543
- }
1544
- if (this.children) {
1545
- for (const child of this.children) {
1546
- child.query(rect, result);
1547
- }
1548
- }
1549
- }
1550
- getChildIndex(itemBounds) {
1551
- const midX = this.bounds.x + this.bounds.w / 2;
1552
- const midY = this.bounds.y + this.bounds.h / 2;
1553
- const left = itemBounds.x >= this.bounds.x && itemBounds.x + itemBounds.w <= midX;
1554
- const right = itemBounds.x >= midX && itemBounds.x + itemBounds.w <= this.bounds.x + this.bounds.w;
1555
- const top = itemBounds.y >= this.bounds.y && itemBounds.y + itemBounds.h <= midY;
1556
- const bottom = itemBounds.y >= midY && itemBounds.y + itemBounds.h <= this.bounds.y + this.bounds.h;
1557
- if (left && top) return 0;
1558
- if (right && top) return 1;
1559
- if (left && bottom) return 2;
1560
- if (right && bottom) return 3;
1561
- return -1;
1747
+ spacing *= 2;
1748
+ }
1749
+ return spacing;
1562
1750
  }
1563
- split() {
1564
- const { x, y, w, h } = this.bounds;
1565
- const halfW = w / 2;
1566
- const halfH = h / 2;
1567
- const d = this.depth + 1;
1568
- this.children = [
1569
- new _QuadNode({ x, y, w: halfW, h: halfH }, d),
1570
- new _QuadNode({ x: x + halfW, y, w: halfW, h: halfH }, d),
1571
- new _QuadNode({ x, y: y + halfH, w: halfW, h: halfH }, d),
1572
- new _QuadNode({ x: x + halfW, y: y + halfH, w: halfW, h: halfH }, d)
1573
- ];
1574
- const remaining = [];
1575
- 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);
1751
+ renderDots(ctx, camera, width, height) {
1752
+ const spacing = this.adaptSpacing(this.spacing, camera.zoom);
1753
+ const offsetX = camera.position.x % spacing;
1754
+ const offsetY = camera.position.y % spacing;
1755
+ const radius = this.dotRadius * Math.min(camera.zoom, 2);
1756
+ ctx.fillStyle = this.color;
1757
+ ctx.beginPath();
1758
+ for (let x = offsetX; x < width; x += spacing) {
1759
+ for (let y = offsetY; y < height; y += spacing) {
1760
+ ctx.moveTo(x + radius, y);
1761
+ ctx.arc(x, y, radius, 0, Math.PI * 2);
1582
1762
  }
1583
1763
  }
1584
- this.items = remaining;
1764
+ ctx.fill();
1585
1765
  }
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;
1766
+ renderGrid(ctx, camera, width, height) {
1767
+ const spacing = this.adaptSpacing(this.spacing, camera.zoom);
1768
+ const offsetX = camera.position.x % spacing;
1769
+ const offsetY = camera.position.y % spacing;
1770
+ const lineW = this.lineWidth * Math.min(camera.zoom, 2);
1771
+ ctx.fillStyle = this.color;
1772
+ for (let x = offsetX; x < width; x += spacing) {
1773
+ ctx.fillRect(x, 0, lineW, height);
1592
1774
  }
1593
- if (totalItems <= MAX_ITEMS) {
1594
- for (const child of this.children) {
1595
- this.items.push(...child.items);
1596
- }
1597
- this.children = null;
1775
+ for (let y = offsetY; y < height; y += spacing) {
1776
+ ctx.fillRect(0, y, width, lineW);
1598
1777
  }
1599
1778
  }
1600
1779
  };
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--;
1780
+
1781
+ // src/core/event-bus.ts
1782
+ var EventBus = class {
1783
+ listeners = /* @__PURE__ */ new Map();
1784
+ on(event, listener) {
1785
+ const existing = this.listeners.get(event);
1786
+ if (existing) {
1787
+ existing.add(listener);
1788
+ } else {
1789
+ const set = /* @__PURE__ */ new Set([listener]);
1790
+ this.listeners.set(event, set);
1619
1791
  }
1792
+ return () => this.off(event, listener);
1620
1793
  }
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;
1794
+ off(event, listener) {
1795
+ this.listeners.get(event)?.delete(listener);
1629
1796
  }
1630
- queryPoint(point) {
1631
- return this.query({ x: point.x, y: point.y, w: 0, h: 0 });
1797
+ emit(event, data) {
1798
+ this.listeners.get(event)?.forEach((listener) => {
1799
+ try {
1800
+ listener(data);
1801
+ } catch (err) {
1802
+ console.error(`[fieldnotes] listener error for "${String(event)}"`, err);
1803
+ }
1804
+ });
1632
1805
  }
1633
1806
  clear() {
1634
- this.root = new QuadNode(this.worldBounds, 0);
1635
- this._size = 0;
1807
+ this.listeners.clear();
1636
1808
  }
1637
1809
  };
1638
1810
 
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;
1811
+ // src/core/quadtree.ts
1812
+ var MAX_ITEMS = 8;
1813
+ var MAX_DEPTH = 8;
1814
+ function intersects(a, b) {
1815
+ 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;
1744
1816
  }
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
- };
1817
+ var QuadNode = class _QuadNode {
1818
+ constructor(bounds, depth) {
1819
+ this.bounds = bounds;
1820
+ this.depth = depth;
1757
1821
  }
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;
1822
+ items = [];
1823
+ children = null;
1824
+ insert(entry) {
1825
+ if (this.children) {
1826
+ const idx = this.getChildIndex(entry.bounds);
1827
+ if (idx !== -1) {
1828
+ const child = this.children[idx];
1829
+ if (child) child.insert(entry);
1830
+ return;
1831
+ }
1832
+ this.items.push(entry);
1833
+ return;
1834
+ }
1835
+ this.items.push(entry);
1836
+ if (this.items.length > MAX_ITEMS && this.depth < MAX_DEPTH) {
1837
+ this.split();
1773
1838
  }
1774
- const bounds = { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
1775
- strokeBoundsCache.set(element, bounds);
1776
- return bounds;
1777
1839
  }
1778
- if (element.type === "arrow") {
1779
- return getArrowBoundsAnalytical(element.from, element.to, element.bend);
1840
+ remove(id) {
1841
+ const idx = this.items.findIndex((e) => e.id === id);
1842
+ if (idx !== -1) {
1843
+ this.items.splice(idx, 1);
1844
+ return true;
1845
+ }
1846
+ if (this.children) {
1847
+ for (const child of this.children) {
1848
+ if (child.remove(id)) {
1849
+ this.collapseIfEmpty();
1850
+ return true;
1851
+ }
1852
+ }
1853
+ }
1854
+ return false;
1780
1855
  }
1781
- if (element.type === "template") {
1782
- return getTemplateBounds(element);
1856
+ query(rect, result) {
1857
+ if (!intersects(this.bounds, rect)) return;
1858
+ for (const item of this.items) {
1859
+ if (intersects(item.bounds, rect)) {
1860
+ result.push(item.id);
1861
+ }
1862
+ }
1863
+ if (this.children) {
1864
+ for (const child of this.children) {
1865
+ child.query(rect, result);
1866
+ }
1867
+ }
1783
1868
  }
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
- };
1869
+ getChildIndex(itemBounds) {
1870
+ const midX = this.bounds.x + this.bounds.w / 2;
1871
+ const midY = this.bounds.y + this.bounds.h / 2;
1872
+ const left = itemBounds.x >= this.bounds.x && itemBounds.x + itemBounds.w <= midX;
1873
+ const right = itemBounds.x >= midX && itemBounds.x + itemBounds.w <= this.bounds.x + this.bounds.w;
1874
+ const top = itemBounds.y >= this.bounds.y && itemBounds.y + itemBounds.h <= midY;
1875
+ const bottom = itemBounds.y >= midY && itemBounds.y + itemBounds.h <= this.bounds.y + this.bounds.h;
1876
+ if (left && top) return 0;
1877
+ if (right && top) return 1;
1878
+ if (left && bottom) return 2;
1879
+ if (right && bottom) return 3;
1880
+ return -1;
1796
1881
  }
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;
1882
+ split() {
1883
+ const { x, y, w, h } = this.bounds;
1884
+ const halfW = w / 2;
1885
+ const halfH = h / 2;
1886
+ const d = this.depth + 1;
1887
+ this.children = [
1888
+ new _QuadNode({ x, y, w: halfW, h: halfH }, d),
1889
+ new _QuadNode({ x: x + halfW, y, w: halfW, h: halfH }, d),
1890
+ new _QuadNode({ x, y: y + halfH, w: halfW, h: halfH }, d),
1891
+ new _QuadNode({ x: x + halfW, y: y + halfH, w: halfW, h: halfH }, d)
1892
+ ];
1893
+ const remaining = [];
1894
+ for (const item of this.items) {
1895
+ const idx = this.getChildIndex(item.bounds);
1896
+ if (idx !== -1) {
1897
+ const target = this.children[idx];
1898
+ if (target) target.insert(item);
1899
+ } else {
1900
+ remaining.push(item);
1901
+ }
1810
1902
  }
1903
+ this.items = remaining;
1811
1904
  }
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;
1905
+ collapseIfEmpty() {
1906
+ if (!this.children) return;
1907
+ let totalItems = this.items.length;
1908
+ for (const child of this.children) {
1909
+ if (child.children) return;
1910
+ totalItems += child.items.length;
1820
1911
  }
1821
- }
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;
1912
+ if (totalItems <= MAX_ITEMS) {
1913
+ for (const child of this.children) {
1914
+ this.items.push(...child.items);
1855
1915
  }
1856
- return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
1916
+ this.children = null;
1857
1917
  }
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 };
1918
+ }
1919
+ };
1920
+ var Quadtree = class {
1921
+ root;
1922
+ _size = 0;
1923
+ worldBounds;
1924
+ constructor(worldBounds) {
1925
+ this.worldBounds = worldBounds;
1926
+ this.root = new QuadNode(worldBounds, 0);
1927
+ }
1928
+ get size() {
1929
+ return this._size;
1930
+ }
1931
+ insert(id, bounds) {
1932
+ this.root.insert({ id, bounds });
1933
+ this._size++;
1934
+ }
1935
+ remove(id) {
1936
+ if (this.root.remove(id)) {
1937
+ this._size--;
1877
1938
  }
1878
1939
  }
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
- }
1940
+ update(id, newBounds) {
1941
+ this.remove(id);
1942
+ this.insert(id, newBounds);
1943
+ }
1944
+ query(rect) {
1945
+ const result = [];
1946
+ this.root.query(rect, result);
1947
+ return result;
1948
+ }
1949
+ queryPoint(point) {
1950
+ return this.query({ x: point.x, y: point.y, w: 0, h: 0 });
1951
+ }
1952
+ clear() {
1953
+ this.root = new QuadNode(this.worldBounds, 0);
1954
+ this._size = 0;
1955
+ }
1956
+ };
1890
1957
 
1891
1958
  // src/elements/stroke-smoothing.ts
1892
1959
  var MIN_PRESSURE_SCALE = 0.2;
@@ -3508,6 +3575,8 @@ var NoteEditor = class {
3508
3575
  inputHandler = null;
3509
3576
  pendingEditId = null;
3510
3577
  onStopCallback = null;
3578
+ beginHistory = null;
3579
+ commitHistory = null;
3511
3580
  toolbar;
3512
3581
  placeholder;
3513
3582
  constructor(options) {
@@ -3523,6 +3592,10 @@ var NoteEditor = class {
3523
3592
  setOnStop(callback) {
3524
3593
  this.onStopCallback = callback;
3525
3594
  }
3595
+ setHistoryHooks(begin, commit) {
3596
+ this.beginHistory = begin;
3597
+ this.commitHistory = commit;
3598
+ }
3526
3599
  startEditing(node, elementId, store) {
3527
3600
  if (this.editingId === elementId) return;
3528
3601
  if (this.editingId) {
@@ -3554,18 +3627,21 @@ var NoteEditor = class {
3554
3627
  this.editingNode.removeAttribute("data-fn-empty");
3555
3628
  const text = sanitizeNoteHtml(this.editingNode.innerHTML);
3556
3629
  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
- }
3630
+ const textChanged = !!current && (current.type === "note" || current.type === "text") && current.text !== text;
3560
3631
  this.editingNode.contentEditable = "false";
3561
3632
  Object.assign(this.editingNode.style, {
3562
3633
  userSelect: "none",
3563
3634
  cursor: "default"
3564
3635
  });
3565
3636
  this.toolbar?.hide();
3637
+ this.beginHistory?.();
3638
+ if (textChanged) {
3639
+ store.update(this.editingId, { text });
3640
+ }
3566
3641
  if (this.editingId && this.onStopCallback) {
3567
3642
  this.onStopCallback(this.editingId);
3568
3643
  }
3644
+ this.commitHistory?.();
3569
3645
  this.editingId = null;
3570
3646
  this.editingNode = null;
3571
3647
  this.blurHandler = null;
@@ -3638,26 +3714,6 @@ var NoteEditor = class {
3638
3714
  }
3639
3715
  };
3640
3716
 
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
3717
  // src/tools/tool-manager.ts
3662
3718
  var ToolManager = class {
3663
3719
  tools = /* @__PURE__ */ new Map();
@@ -5212,6 +5268,10 @@ var Viewport = class {
5212
5268
  placeholder: options.placeholder
5213
5269
  });
5214
5270
  this.noteEditor.setOnStop((id) => this.onTextEditStop(id));
5271
+ this.noteEditor.setHistoryHooks(
5272
+ () => this.historyRecorder.begin(),
5273
+ () => this.historyRecorder.commit()
5274
+ );
5215
5275
  this.onHtmlElementMount = options.onHtmlElementMount;
5216
5276
  this.dropHandler = options.onDrop;
5217
5277
  this.history = new HistoryStack();
@@ -5228,6 +5288,7 @@ var Viewport = class {
5228
5288
  requestRender: () => this.requestRender(),
5229
5289
  switchTool: (name) => this.toolManager.setTool(name, this.toolContext),
5230
5290
  editElement: (id) => this.startEditingElement(id),
5291
+ fitNoteHeight: (id) => this.fitNoteHeight(id),
5231
5292
  setCursor: (cursor) => {
5232
5293
  this.wrapper.style.cursor = cursor;
5233
5294
  },
@@ -5562,31 +5623,38 @@ var Viewport = class {
5562
5623
  this.noteEditor.startEditing(node, id, this.store);
5563
5624
  }
5564
5625
  }
5626
+ fitNoteHeight(elementId) {
5627
+ const element = this.store.getById(elementId);
5628
+ if (!element || element.type !== "note") return;
5629
+ if (isNoteContentEmpty(element.text)) return;
5630
+ const node = this.domNodeManager.getNode(elementId);
5631
+ if (!node) return;
5632
+ const measured = node.scrollHeight;
5633
+ if (measured > element.size.h) {
5634
+ this.store.update(elementId, { size: { w: element.size.w, h: measured } });
5635
+ }
5636
+ }
5565
5637
  onTextEditStop(elementId) {
5566
5638
  const element = this.store.getById(elementId);
5567
5639
  if (!element) return;
5568
5640
  if (element.type === "note") {
5569
5641
  if (isNoteContentEmpty(element.text)) {
5570
- this.historyRecorder.begin();
5571
5642
  this.store.remove(elementId);
5572
- this.historyRecorder.commit();
5643
+ return;
5573
5644
  }
5645
+ this.fitNoteHeight(elementId);
5574
5646
  return;
5575
5647
  }
5576
5648
  if (element.type !== "text") return;
5577
5649
  if (!element.text || element.text.trim() === "") {
5578
- this.historyRecorder.begin();
5579
5650
  this.store.remove(elementId);
5580
- this.historyRecorder.commit();
5581
5651
  return;
5582
5652
  }
5583
5653
  const node = this.domNodeManager.getNode(elementId);
5584
5654
  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
- });
5655
+ const measured = node.scrollHeight;
5656
+ if (measured !== element.size.h) {
5657
+ this.store.update(elementId, { size: { w: element.size.w, h: measured } });
5590
5658
  }
5591
5659
  }
5592
5660
  }
@@ -6303,8 +6371,13 @@ var SelectTool = class {
6303
6371
  }
6304
6372
  this.pendingSingleSelectId = null;
6305
6373
  this.hasDragged = false;
6374
+ const resizedNoteId = this.mode.type === "resizing" ? this.mode.elementId : null;
6306
6375
  this.mode = { type: "idle" };
6307
6376
  ctx.setCursor?.("default");
6377
+ if (resizedNoteId !== null) {
6378
+ const el = ctx.store.getById(resizedNoteId);
6379
+ if (el?.type === "note") ctx.fitNoteHeight?.(resizedNoteId);
6380
+ }
6308
6381
  }
6309
6382
  onHover(state, ctx) {
6310
6383
  const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
@@ -7516,7 +7589,7 @@ var TemplateTool = class {
7516
7589
  };
7517
7590
 
7518
7591
  // src/index.ts
7519
- var VERSION = "0.25.0";
7592
+ var VERSION = "0.26.0";
7520
7593
  // Annotate the CommonJS export names for ESM import in node:
7521
7594
  0 && (module.exports = {
7522
7595
  ArrowTool,