@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.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.
|
|
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
|
-
|
|
1564
|
-
const
|
|
1565
|
-
const
|
|
1566
|
-
const
|
|
1567
|
-
const
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
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
|
-
|
|
1764
|
+
ctx.fill();
|
|
1585
1765
|
}
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
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
|
-
|
|
1594
|
-
|
|
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
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
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
|
-
|
|
1622
|
-
this.
|
|
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
|
-
|
|
1631
|
-
|
|
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.
|
|
1635
|
-
this._size = 0;
|
|
1807
|
+
this.listeners.clear();
|
|
1636
1808
|
}
|
|
1637
1809
|
};
|
|
1638
1810
|
|
|
1639
|
-
// src/core/
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
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
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
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
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
if (
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
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
|
-
|
|
1779
|
-
|
|
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
|
-
|
|
1782
|
-
|
|
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
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
const
|
|
1789
|
-
const
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
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
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
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
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1916
|
+
this.children = null;
|
|
1857
1917
|
}
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
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
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
5586
|
-
if (
|
|
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.
|
|
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,
|