@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/README.md +503 -502
- package/dist/index.cjs +545 -472
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +545 -472
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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.
|
|
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
|
-
|
|
1485
|
-
const
|
|
1486
|
-
const
|
|
1487
|
-
const
|
|
1488
|
-
const
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
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
|
-
|
|
1685
|
+
ctx.fill();
|
|
1506
1686
|
}
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
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
|
-
|
|
1515
|
-
|
|
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
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
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
|
-
|
|
1543
|
-
this.
|
|
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
|
-
|
|
1552
|
-
|
|
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.
|
|
1556
|
-
this._size = 0;
|
|
1728
|
+
this.listeners.clear();
|
|
1557
1729
|
}
|
|
1558
1730
|
};
|
|
1559
1731
|
|
|
1560
|
-
// src/core/
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
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
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
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
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
if (
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
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
|
-
|
|
1700
|
-
|
|
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
|
-
|
|
1703
|
-
|
|
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
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
const
|
|
1710
|
-
const
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
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
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
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
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1837
|
+
this.children = null;
|
|
1778
1838
|
}
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
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
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
5507
|
-
if (
|
|
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.
|
|
7513
|
+
var VERSION = "0.26.0";
|
|
7441
7514
|
export {
|
|
7442
7515
|
ArrowTool,
|
|
7443
7516
|
AutoSave,
|