@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.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 {
@@ -1346,468 +1665,216 @@ var Background = class {
1346
1665
  adaptSpacing(baseSpacing, zoom) {
1347
1666
  let spacing = baseSpacing * zoom;
1348
1667
  while (spacing < MIN_PATTERN_SPACING) {
1349
- spacing *= 2;
1350
- }
1351
- return spacing;
1352
- }
1353
- renderDots(ctx, camera, width, height) {
1354
- const spacing = this.adaptSpacing(this.spacing, camera.zoom);
1355
- const offsetX = camera.position.x % spacing;
1356
- const offsetY = camera.position.y % spacing;
1357
- const radius = this.dotRadius * Math.min(camera.zoom, 2);
1358
- ctx.fillStyle = this.color;
1359
- ctx.beginPath();
1360
- for (let x = offsetX; x < width; x += spacing) {
1361
- for (let y = offsetY; y < height; y += spacing) {
1362
- ctx.moveTo(x + radius, y);
1363
- ctx.arc(x, y, radius, 0, Math.PI * 2);
1364
- }
1365
- }
1366
- ctx.fill();
1367
- }
1368
- renderGrid(ctx, camera, width, height) {
1369
- const spacing = this.adaptSpacing(this.spacing, camera.zoom);
1370
- const offsetX = camera.position.x % spacing;
1371
- const offsetY = camera.position.y % spacing;
1372
- const lineW = this.lineWidth * Math.min(camera.zoom, 2);
1373
- ctx.fillStyle = this.color;
1374
- for (let x = offsetX; x < width; x += spacing) {
1375
- ctx.fillRect(x, 0, lineW, height);
1376
- }
1377
- for (let y = offsetY; y < height; y += spacing) {
1378
- ctx.fillRect(0, y, width, lineW);
1379
- }
1380
- }
1381
- };
1382
-
1383
- // src/core/event-bus.ts
1384
- var EventBus = class {
1385
- listeners = /* @__PURE__ */ new Map();
1386
- on(event, listener) {
1387
- const existing = this.listeners.get(event);
1388
- if (existing) {
1389
- existing.add(listener);
1390
- } else {
1391
- const set = /* @__PURE__ */ new Set([listener]);
1392
- this.listeners.set(event, set);
1393
- }
1394
- return () => this.off(event, listener);
1395
- }
1396
- off(event, listener) {
1397
- this.listeners.get(event)?.delete(listener);
1398
- }
1399
- emit(event, data) {
1400
- this.listeners.get(event)?.forEach((listener) => {
1401
- try {
1402
- listener(data);
1403
- } catch (err) {
1404
- console.error(`[fieldnotes] listener error for "${String(event)}"`, err);
1405
- }
1406
- });
1407
- }
1408
- clear() {
1409
- this.listeners.clear();
1410
- }
1411
- };
1412
-
1413
- // src/core/quadtree.ts
1414
- var MAX_ITEMS = 8;
1415
- var MAX_DEPTH = 8;
1416
- function intersects(a, b) {
1417
- 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;
1418
- }
1419
- var QuadNode = class _QuadNode {
1420
- constructor(bounds, depth) {
1421
- this.bounds = bounds;
1422
- this.depth = depth;
1423
- }
1424
- items = [];
1425
- children = null;
1426
- insert(entry) {
1427
- if (this.children) {
1428
- const idx = this.getChildIndex(entry.bounds);
1429
- if (idx !== -1) {
1430
- const child = this.children[idx];
1431
- if (child) child.insert(entry);
1432
- return;
1433
- }
1434
- this.items.push(entry);
1435
- return;
1436
- }
1437
- this.items.push(entry);
1438
- if (this.items.length > MAX_ITEMS && this.depth < MAX_DEPTH) {
1439
- this.split();
1440
- }
1441
- }
1442
- remove(id) {
1443
- const idx = this.items.findIndex((e) => e.id === id);
1444
- if (idx !== -1) {
1445
- this.items.splice(idx, 1);
1446
- return true;
1447
- }
1448
- if (this.children) {
1449
- for (const child of this.children) {
1450
- if (child.remove(id)) {
1451
- this.collapseIfEmpty();
1452
- return true;
1453
- }
1454
- }
1455
- }
1456
- return false;
1457
- }
1458
- query(rect, result) {
1459
- if (!intersects(this.bounds, rect)) return;
1460
- for (const item of this.items) {
1461
- if (intersects(item.bounds, rect)) {
1462
- result.push(item.id);
1463
- }
1464
- }
1465
- if (this.children) {
1466
- for (const child of this.children) {
1467
- child.query(rect, result);
1468
- }
1469
- }
1470
- }
1471
- getChildIndex(itemBounds) {
1472
- const midX = this.bounds.x + this.bounds.w / 2;
1473
- const midY = this.bounds.y + this.bounds.h / 2;
1474
- const left = itemBounds.x >= this.bounds.x && itemBounds.x + itemBounds.w <= midX;
1475
- const right = itemBounds.x >= midX && itemBounds.x + itemBounds.w <= this.bounds.x + this.bounds.w;
1476
- const top = itemBounds.y >= this.bounds.y && itemBounds.y + itemBounds.h <= midY;
1477
- const bottom = itemBounds.y >= midY && itemBounds.y + itemBounds.h <= this.bounds.y + this.bounds.h;
1478
- if (left && top) return 0;
1479
- if (right && top) return 1;
1480
- if (left && bottom) return 2;
1481
- if (right && bottom) return 3;
1482
- return -1;
1668
+ spacing *= 2;
1669
+ }
1670
+ return spacing;
1483
1671
  }
1484
- split() {
1485
- const { x, y, w, h } = this.bounds;
1486
- const halfW = w / 2;
1487
- const halfH = h / 2;
1488
- const d = this.depth + 1;
1489
- this.children = [
1490
- new _QuadNode({ x, y, w: halfW, h: halfH }, d),
1491
- new _QuadNode({ x: x + halfW, y, w: halfW, h: halfH }, d),
1492
- new _QuadNode({ x, y: y + halfH, w: halfW, h: halfH }, d),
1493
- new _QuadNode({ x: x + halfW, y: y + halfH, w: halfW, h: halfH }, d)
1494
- ];
1495
- const remaining = [];
1496
- 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);
1672
+ renderDots(ctx, camera, width, height) {
1673
+ const spacing = this.adaptSpacing(this.spacing, camera.zoom);
1674
+ const offsetX = camera.position.x % spacing;
1675
+ const offsetY = camera.position.y % spacing;
1676
+ const radius = this.dotRadius * Math.min(camera.zoom, 2);
1677
+ ctx.fillStyle = this.color;
1678
+ ctx.beginPath();
1679
+ for (let x = offsetX; x < width; x += spacing) {
1680
+ for (let y = offsetY; y < height; y += spacing) {
1681
+ ctx.moveTo(x + radius, y);
1682
+ ctx.arc(x, y, radius, 0, Math.PI * 2);
1503
1683
  }
1504
1684
  }
1505
- this.items = remaining;
1685
+ ctx.fill();
1506
1686
  }
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;
1687
+ renderGrid(ctx, camera, width, height) {
1688
+ const spacing = this.adaptSpacing(this.spacing, camera.zoom);
1689
+ const offsetX = camera.position.x % spacing;
1690
+ const offsetY = camera.position.y % spacing;
1691
+ const lineW = this.lineWidth * Math.min(camera.zoom, 2);
1692
+ ctx.fillStyle = this.color;
1693
+ for (let x = offsetX; x < width; x += spacing) {
1694
+ ctx.fillRect(x, 0, lineW, height);
1513
1695
  }
1514
- if (totalItems <= MAX_ITEMS) {
1515
- for (const child of this.children) {
1516
- this.items.push(...child.items);
1517
- }
1518
- this.children = null;
1696
+ for (let y = offsetY; y < height; y += spacing) {
1697
+ ctx.fillRect(0, y, width, lineW);
1519
1698
  }
1520
1699
  }
1521
1700
  };
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--;
1701
+
1702
+ // src/core/event-bus.ts
1703
+ var EventBus = class {
1704
+ listeners = /* @__PURE__ */ new Map();
1705
+ on(event, listener) {
1706
+ const existing = this.listeners.get(event);
1707
+ if (existing) {
1708
+ existing.add(listener);
1709
+ } else {
1710
+ const set = /* @__PURE__ */ new Set([listener]);
1711
+ this.listeners.set(event, set);
1540
1712
  }
1713
+ return () => this.off(event, listener);
1541
1714
  }
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;
1715
+ off(event, listener) {
1716
+ this.listeners.get(event)?.delete(listener);
1550
1717
  }
1551
- queryPoint(point) {
1552
- return this.query({ x: point.x, y: point.y, w: 0, h: 0 });
1718
+ emit(event, data) {
1719
+ this.listeners.get(event)?.forEach((listener) => {
1720
+ try {
1721
+ listener(data);
1722
+ } catch (err) {
1723
+ console.error(`[fieldnotes] listener error for "${String(event)}"`, err);
1724
+ }
1725
+ });
1553
1726
  }
1554
1727
  clear() {
1555
- this.root = new QuadNode(this.worldBounds, 0);
1556
- this._size = 0;
1728
+ this.listeners.clear();
1557
1729
  }
1558
1730
  };
1559
1731
 
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;
1732
+ // src/core/quadtree.ts
1733
+ var MAX_ITEMS = 8;
1734
+ var MAX_DEPTH = 8;
1735
+ function intersects(a, b) {
1736
+ 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;
1665
1737
  }
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
- };
1738
+ var QuadNode = class _QuadNode {
1739
+ constructor(bounds, depth) {
1740
+ this.bounds = bounds;
1741
+ this.depth = depth;
1678
1742
  }
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;
1743
+ items = [];
1744
+ children = null;
1745
+ insert(entry) {
1746
+ if (this.children) {
1747
+ const idx = this.getChildIndex(entry.bounds);
1748
+ if (idx !== -1) {
1749
+ const child = this.children[idx];
1750
+ if (child) child.insert(entry);
1751
+ return;
1752
+ }
1753
+ this.items.push(entry);
1754
+ return;
1755
+ }
1756
+ this.items.push(entry);
1757
+ if (this.items.length > MAX_ITEMS && this.depth < MAX_DEPTH) {
1758
+ this.split();
1694
1759
  }
1695
- const bounds = { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
1696
- strokeBoundsCache.set(element, bounds);
1697
- return bounds;
1698
1760
  }
1699
- if (element.type === "arrow") {
1700
- return getArrowBoundsAnalytical(element.from, element.to, element.bend);
1761
+ remove(id) {
1762
+ const idx = this.items.findIndex((e) => e.id === id);
1763
+ if (idx !== -1) {
1764
+ this.items.splice(idx, 1);
1765
+ return true;
1766
+ }
1767
+ if (this.children) {
1768
+ for (const child of this.children) {
1769
+ if (child.remove(id)) {
1770
+ this.collapseIfEmpty();
1771
+ return true;
1772
+ }
1773
+ }
1774
+ }
1775
+ return false;
1701
1776
  }
1702
- if (element.type === "template") {
1703
- return getTemplateBounds(element);
1777
+ query(rect, result) {
1778
+ if (!intersects(this.bounds, rect)) return;
1779
+ for (const item of this.items) {
1780
+ if (intersects(item.bounds, rect)) {
1781
+ result.push(item.id);
1782
+ }
1783
+ }
1784
+ if (this.children) {
1785
+ for (const child of this.children) {
1786
+ child.query(rect, result);
1787
+ }
1788
+ }
1704
1789
  }
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
- };
1790
+ getChildIndex(itemBounds) {
1791
+ const midX = this.bounds.x + this.bounds.w / 2;
1792
+ const midY = this.bounds.y + this.bounds.h / 2;
1793
+ const left = itemBounds.x >= this.bounds.x && itemBounds.x + itemBounds.w <= midX;
1794
+ const right = itemBounds.x >= midX && itemBounds.x + itemBounds.w <= this.bounds.x + this.bounds.w;
1795
+ const top = itemBounds.y >= this.bounds.y && itemBounds.y + itemBounds.h <= midY;
1796
+ const bottom = itemBounds.y >= midY && itemBounds.y + itemBounds.h <= this.bounds.y + this.bounds.h;
1797
+ if (left && top) return 0;
1798
+ if (right && top) return 1;
1799
+ if (left && bottom) return 2;
1800
+ if (right && bottom) return 3;
1801
+ return -1;
1717
1802
  }
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;
1803
+ split() {
1804
+ const { x, y, w, h } = this.bounds;
1805
+ const halfW = w / 2;
1806
+ const halfH = h / 2;
1807
+ const d = this.depth + 1;
1808
+ this.children = [
1809
+ new _QuadNode({ x, y, w: halfW, h: halfH }, d),
1810
+ new _QuadNode({ x: x + halfW, y, w: halfW, h: halfH }, d),
1811
+ new _QuadNode({ x, y: y + halfH, w: halfW, h: halfH }, d),
1812
+ new _QuadNode({ x: x + halfW, y: y + halfH, w: halfW, h: halfH }, d)
1813
+ ];
1814
+ const remaining = [];
1815
+ for (const item of this.items) {
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
+ }
1731
1823
  }
1824
+ this.items = remaining;
1732
1825
  }
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;
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;
1741
1832
  }
1742
- }
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;
1833
+ if (totalItems <= MAX_ITEMS) {
1834
+ for (const child of this.children) {
1835
+ this.items.push(...child.items);
1776
1836
  }
1777
- return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
1837
+ this.children = null;
1778
1838
  }
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 };
1839
+ }
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);
1848
+ }
1849
+ get size() {
1850
+ return this._size;
1851
+ }
1852
+ insert(id, bounds) {
1853
+ this.root.insert({ id, bounds });
1854
+ this._size++;
1855
+ }
1856
+ remove(id) {
1857
+ if (this.root.remove(id)) {
1858
+ this._size--;
1798
1859
  }
1799
1860
  }
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
- }
1861
+ update(id, newBounds) {
1862
+ this.remove(id);
1863
+ this.insert(id, newBounds);
1864
+ }
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();
@@ -5133,6 +5189,10 @@ var Viewport = class {
5133
5189
  placeholder: options.placeholder
5134
5190
  });
5135
5191
  this.noteEditor.setOnStop((id) => this.onTextEditStop(id));
5192
+ this.noteEditor.setHistoryHooks(
5193
+ () => this.historyRecorder.begin(),
5194
+ () => this.historyRecorder.commit()
5195
+ );
5136
5196
  this.onHtmlElementMount = options.onHtmlElementMount;
5137
5197
  this.dropHandler = options.onDrop;
5138
5198
  this.history = new HistoryStack();
@@ -5149,6 +5209,7 @@ var Viewport = class {
5149
5209
  requestRender: () => this.requestRender(),
5150
5210
  switchTool: (name) => this.toolManager.setTool(name, this.toolContext),
5151
5211
  editElement: (id) => this.startEditingElement(id),
5212
+ fitNoteHeight: (id) => this.fitNoteHeight(id),
5152
5213
  setCursor: (cursor) => {
5153
5214
  this.wrapper.style.cursor = cursor;
5154
5215
  },
@@ -5483,31 +5544,38 @@ var Viewport = class {
5483
5544
  this.noteEditor.startEditing(node, id, this.store);
5484
5545
  }
5485
5546
  }
5547
+ fitNoteHeight(elementId) {
5548
+ const element = this.store.getById(elementId);
5549
+ if (!element || element.type !== "note") return;
5550
+ if (isNoteContentEmpty(element.text)) return;
5551
+ const node = this.domNodeManager.getNode(elementId);
5552
+ if (!node) return;
5553
+ const measured = node.scrollHeight;
5554
+ if (measured > element.size.h) {
5555
+ this.store.update(elementId, { size: { w: element.size.w, h: measured } });
5556
+ }
5557
+ }
5486
5558
  onTextEditStop(elementId) {
5487
5559
  const element = this.store.getById(elementId);
5488
5560
  if (!element) return;
5489
5561
  if (element.type === "note") {
5490
5562
  if (isNoteContentEmpty(element.text)) {
5491
- this.historyRecorder.begin();
5492
5563
  this.store.remove(elementId);
5493
- this.historyRecorder.commit();
5564
+ return;
5494
5565
  }
5566
+ this.fitNoteHeight(elementId);
5495
5567
  return;
5496
5568
  }
5497
5569
  if (element.type !== "text") return;
5498
5570
  if (!element.text || element.text.trim() === "") {
5499
- this.historyRecorder.begin();
5500
5571
  this.store.remove(elementId);
5501
- this.historyRecorder.commit();
5502
5572
  return;
5503
5573
  }
5504
5574
  const node = this.domNodeManager.getNode(elementId);
5505
5575
  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
- });
5576
+ const measured = node.scrollHeight;
5577
+ if (measured !== element.size.h) {
5578
+ this.store.update(elementId, { size: { w: element.size.w, h: measured } });
5511
5579
  }
5512
5580
  }
5513
5581
  }
@@ -6224,8 +6292,13 @@ var SelectTool = class {
6224
6292
  }
6225
6293
  this.pendingSingleSelectId = null;
6226
6294
  this.hasDragged = false;
6295
+ const resizedNoteId = this.mode.type === "resizing" ? this.mode.elementId : null;
6227
6296
  this.mode = { type: "idle" };
6228
6297
  ctx.setCursor?.("default");
6298
+ if (resizedNoteId !== null) {
6299
+ const el = ctx.store.getById(resizedNoteId);
6300
+ if (el?.type === "note") ctx.fitNoteHeight?.(resizedNoteId);
6301
+ }
6229
6302
  }
6230
6303
  onHover(state, ctx) {
6231
6304
  const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
@@ -7437,7 +7510,7 @@ var TemplateTool = class {
7437
7510
  };
7438
7511
 
7439
7512
  // src/index.ts
7440
- var VERSION = "0.25.0";
7513
+ var VERSION = "0.26.0";
7441
7514
  export {
7442
7515
  ArrowTool,
7443
7516
  AutoSave,