@fieldnotes/core 0.25.0 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +74 -1
- package/dist/index.cjs +586 -354
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +22 -2
- package/dist/index.d.ts +22 -2
- package/dist/index.js +584 -354
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -59,6 +59,7 @@ __export(index_exports, {
|
|
|
59
59
|
getArrowTangentAngle: () => getArrowTangentAngle,
|
|
60
60
|
getBendFromPoint: () => getBendFromPoint,
|
|
61
61
|
getElementBounds: () => getElementBounds,
|
|
62
|
+
getElementStyle: () => getElementStyle,
|
|
62
63
|
getElementsBoundingBox: () => getElementsBoundingBox,
|
|
63
64
|
getHexCellsInCone: () => getHexCellsInCone,
|
|
64
65
|
getHexCellsInLine: () => getHexCellsInLine,
|
|
@@ -70,6 +71,7 @@ __export(index_exports, {
|
|
|
70
71
|
smartSnap: () => smartSnap,
|
|
71
72
|
snapPoint: () => snapPoint,
|
|
72
73
|
snapToHexCenter: () => snapToHexCenter,
|
|
74
|
+
styleToPatch: () => styleToPatch,
|
|
73
75
|
toggleBold: () => toggleBold,
|
|
74
76
|
toggleItalic: () => toggleItalic,
|
|
75
77
|
toggleStrikethrough: () => toggleStrikethrough,
|
|
@@ -588,6 +590,278 @@ function createId(prefix) {
|
|
|
588
590
|
return `${prefix}_${Date.now().toString(36)}_${(counter++).toString(36)}`;
|
|
589
591
|
}
|
|
590
592
|
|
|
593
|
+
// src/core/geometry.ts
|
|
594
|
+
function distSqToSegment(p, a, b) {
|
|
595
|
+
const abx = b.x - a.x;
|
|
596
|
+
const aby = b.y - a.y;
|
|
597
|
+
const apx = p.x - a.x;
|
|
598
|
+
const apy = p.y - a.y;
|
|
599
|
+
const lenSq = abx * abx + aby * aby;
|
|
600
|
+
if (lenSq === 0) {
|
|
601
|
+
return apx * apx + apy * apy;
|
|
602
|
+
}
|
|
603
|
+
const t = Math.max(0, Math.min(1, (apx * abx + apy * aby) / lenSq));
|
|
604
|
+
const dx = p.x - (a.x + t * abx);
|
|
605
|
+
const dy = p.y - (a.y + t * aby);
|
|
606
|
+
return dx * dx + dy * dy;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// src/elements/arrow-geometry.ts
|
|
610
|
+
function getArrowControlPoint(from, to, bend) {
|
|
611
|
+
const midX = (from.x + to.x) / 2;
|
|
612
|
+
const midY = (from.y + to.y) / 2;
|
|
613
|
+
if (bend === 0) return { x: midX, y: midY };
|
|
614
|
+
const dx = to.x - from.x;
|
|
615
|
+
const dy = to.y - from.y;
|
|
616
|
+
const len = Math.sqrt(dx * dx + dy * dy);
|
|
617
|
+
if (len === 0) return { x: midX, y: midY };
|
|
618
|
+
const perpX = -dy / len;
|
|
619
|
+
const perpY = dx / len;
|
|
620
|
+
return {
|
|
621
|
+
x: midX + perpX * bend,
|
|
622
|
+
y: midY + perpY * bend
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
function getArrowMidpoint(from, to, bend) {
|
|
626
|
+
const cp = getArrowControlPoint(from, to, bend);
|
|
627
|
+
return {
|
|
628
|
+
x: 0.25 * from.x + 0.5 * cp.x + 0.25 * to.x,
|
|
629
|
+
y: 0.25 * from.y + 0.5 * cp.y + 0.25 * to.y
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
function getBendFromPoint(from, to, dragPoint) {
|
|
633
|
+
const midX = (from.x + to.x) / 2;
|
|
634
|
+
const midY = (from.y + to.y) / 2;
|
|
635
|
+
const dx = to.x - from.x;
|
|
636
|
+
const dy = to.y - from.y;
|
|
637
|
+
const len = Math.sqrt(dx * dx + dy * dy);
|
|
638
|
+
if (len === 0) return 0;
|
|
639
|
+
const perpX = -dy / len;
|
|
640
|
+
const perpY = dx / len;
|
|
641
|
+
return (dragPoint.x - midX) * perpX + (dragPoint.y - midY) * perpY;
|
|
642
|
+
}
|
|
643
|
+
function getArrowTangentAngle(from, to, bend, t) {
|
|
644
|
+
const cp = getArrowControlPoint(from, to, bend);
|
|
645
|
+
const tangentX = 2 * (1 - t) * (cp.x - from.x) + 2 * t * (to.x - cp.x);
|
|
646
|
+
const tangentY = 2 * (1 - t) * (cp.y - from.y) + 2 * t * (to.y - cp.y);
|
|
647
|
+
return Math.atan2(tangentY, tangentX);
|
|
648
|
+
}
|
|
649
|
+
function isNearBezier(point, from, to, bend, threshold) {
|
|
650
|
+
if (bend === 0) return isNearLine(point, from, to, threshold);
|
|
651
|
+
const cp = getArrowControlPoint(from, to, bend);
|
|
652
|
+
const segments = 20;
|
|
653
|
+
for (let i = 0; i < segments; i++) {
|
|
654
|
+
const t0 = i / segments;
|
|
655
|
+
const t1 = (i + 1) / segments;
|
|
656
|
+
const a = bezierPoint(from, cp, to, t0);
|
|
657
|
+
const b = bezierPoint(from, cp, to, t1);
|
|
658
|
+
if (isNearLine(point, a, b, threshold)) return true;
|
|
659
|
+
}
|
|
660
|
+
return false;
|
|
661
|
+
}
|
|
662
|
+
function getArrowBounds(from, to, bend) {
|
|
663
|
+
if (bend === 0) {
|
|
664
|
+
const minX2 = Math.min(from.x, to.x);
|
|
665
|
+
const minY2 = Math.min(from.y, to.y);
|
|
666
|
+
return {
|
|
667
|
+
x: minX2,
|
|
668
|
+
y: minY2,
|
|
669
|
+
w: Math.abs(to.x - from.x),
|
|
670
|
+
h: Math.abs(to.y - from.y)
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
const cp = getArrowControlPoint(from, to, bend);
|
|
674
|
+
const steps = 20;
|
|
675
|
+
let minX = Math.min(from.x, to.x);
|
|
676
|
+
let minY = Math.min(from.y, to.y);
|
|
677
|
+
let maxX = Math.max(from.x, to.x);
|
|
678
|
+
let maxY = Math.max(from.y, to.y);
|
|
679
|
+
for (let i = 1; i < steps; i++) {
|
|
680
|
+
const t = i / steps;
|
|
681
|
+
const p = bezierPoint(from, cp, to, t);
|
|
682
|
+
if (p.x < minX) minX = p.x;
|
|
683
|
+
if (p.y < minY) minY = p.y;
|
|
684
|
+
if (p.x > maxX) maxX = p.x;
|
|
685
|
+
if (p.y > maxY) maxY = p.y;
|
|
686
|
+
}
|
|
687
|
+
return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
|
|
688
|
+
}
|
|
689
|
+
function bezierPoint(from, cp, to, t) {
|
|
690
|
+
const mt = 1 - t;
|
|
691
|
+
return {
|
|
692
|
+
x: mt * mt * from.x + 2 * mt * t * cp.x + t * t * to.x,
|
|
693
|
+
y: mt * mt * from.y + 2 * mt * t * cp.y + t * t * to.y
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
function isNearLine(point, a, b, threshold) {
|
|
697
|
+
return distSqToSegment(point, a, b) <= threshold * threshold;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// src/elements/element-bounds.ts
|
|
701
|
+
var strokeBoundsCache = /* @__PURE__ */ new WeakMap();
|
|
702
|
+
function getElementBounds(element) {
|
|
703
|
+
if (element.type === "grid") return null;
|
|
704
|
+
if ("size" in element) {
|
|
705
|
+
return {
|
|
706
|
+
x: element.position.x,
|
|
707
|
+
y: element.position.y,
|
|
708
|
+
w: element.size.w,
|
|
709
|
+
h: element.size.h
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
if (element.type === "stroke") {
|
|
713
|
+
if (element.points.length === 0) return null;
|
|
714
|
+
const cached = strokeBoundsCache.get(element);
|
|
715
|
+
if (cached) return cached;
|
|
716
|
+
let minX = Infinity;
|
|
717
|
+
let minY = Infinity;
|
|
718
|
+
let maxX = -Infinity;
|
|
719
|
+
let maxY = -Infinity;
|
|
720
|
+
for (const p of element.points) {
|
|
721
|
+
const px = p.x + element.position.x;
|
|
722
|
+
const py = p.y + element.position.y;
|
|
723
|
+
if (px < minX) minX = px;
|
|
724
|
+
if (py < minY) minY = py;
|
|
725
|
+
if (px > maxX) maxX = px;
|
|
726
|
+
if (py > maxY) maxY = py;
|
|
727
|
+
}
|
|
728
|
+
const bounds = { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
|
|
729
|
+
strokeBoundsCache.set(element, bounds);
|
|
730
|
+
return bounds;
|
|
731
|
+
}
|
|
732
|
+
if (element.type === "arrow") {
|
|
733
|
+
return getArrowBoundsAnalytical(element.from, element.to, element.bend);
|
|
734
|
+
}
|
|
735
|
+
if (element.type === "template") {
|
|
736
|
+
return getTemplateBounds(element);
|
|
737
|
+
}
|
|
738
|
+
return null;
|
|
739
|
+
}
|
|
740
|
+
function getArrowBoundsAnalytical(from, to, bend) {
|
|
741
|
+
if (bend === 0) {
|
|
742
|
+
const minX2 = Math.min(from.x, to.x);
|
|
743
|
+
const minY2 = Math.min(from.y, to.y);
|
|
744
|
+
return {
|
|
745
|
+
x: minX2,
|
|
746
|
+
y: minY2,
|
|
747
|
+
w: Math.abs(to.x - from.x),
|
|
748
|
+
h: Math.abs(to.y - from.y)
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
const cp = getArrowControlPoint(from, to, bend);
|
|
752
|
+
let minX = Math.min(from.x, to.x);
|
|
753
|
+
let maxX = Math.max(from.x, to.x);
|
|
754
|
+
let minY = Math.min(from.y, to.y);
|
|
755
|
+
let maxY = Math.max(from.y, to.y);
|
|
756
|
+
const tx = from.x - 2 * cp.x + to.x;
|
|
757
|
+
if (tx !== 0) {
|
|
758
|
+
const t = (from.x - cp.x) / tx;
|
|
759
|
+
if (t > 0 && t < 1) {
|
|
760
|
+
const mt = 1 - t;
|
|
761
|
+
const x = mt * mt * from.x + 2 * mt * t * cp.x + t * t * to.x;
|
|
762
|
+
if (x < minX) minX = x;
|
|
763
|
+
if (x > maxX) maxX = x;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
const ty = from.y - 2 * cp.y + to.y;
|
|
767
|
+
if (ty !== 0) {
|
|
768
|
+
const t = (from.y - cp.y) / ty;
|
|
769
|
+
if (t > 0 && t < 1) {
|
|
770
|
+
const mt = 1 - t;
|
|
771
|
+
const y = mt * mt * from.y + 2 * mt * t * cp.y + t * t * to.y;
|
|
772
|
+
if (y < minY) minY = y;
|
|
773
|
+
if (y > maxY) maxY = y;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
|
|
777
|
+
}
|
|
778
|
+
function getTemplateBounds(el) {
|
|
779
|
+
const { x: cx, y: cy } = el.position;
|
|
780
|
+
const r = el.radius;
|
|
781
|
+
switch (el.templateShape) {
|
|
782
|
+
case "circle":
|
|
783
|
+
return { x: cx - r, y: cy - r, w: 2 * r, h: 2 * r };
|
|
784
|
+
case "square":
|
|
785
|
+
return { x: cx - r / 2, y: cy - r / 2, w: r, h: r };
|
|
786
|
+
case "cone": {
|
|
787
|
+
const halfAngle = Math.atan(0.5);
|
|
788
|
+
const tipX = cx;
|
|
789
|
+
const tipY = cy;
|
|
790
|
+
const leftX = cx + r * Math.cos(el.angle - halfAngle);
|
|
791
|
+
const leftY = cy + r * Math.sin(el.angle - halfAngle);
|
|
792
|
+
const rightX = cx + r * Math.cos(el.angle + halfAngle);
|
|
793
|
+
const rightY = cy + r * Math.sin(el.angle + halfAngle);
|
|
794
|
+
const farX = cx + r * Math.cos(el.angle);
|
|
795
|
+
const farY = cy + r * Math.sin(el.angle);
|
|
796
|
+
const xs = [tipX, leftX, rightX, farX];
|
|
797
|
+
const ys = [tipY, leftY, rightY, farY];
|
|
798
|
+
let minX = Infinity;
|
|
799
|
+
let minY = Infinity;
|
|
800
|
+
let maxX = -Infinity;
|
|
801
|
+
let maxY = -Infinity;
|
|
802
|
+
for (let i = 0; i < xs.length; i++) {
|
|
803
|
+
const px = xs[i];
|
|
804
|
+
const py = ys[i];
|
|
805
|
+
if (px !== void 0 && px < minX) minX = px;
|
|
806
|
+
if (px !== void 0 && px > maxX) maxX = px;
|
|
807
|
+
if (py !== void 0 && py < minY) minY = py;
|
|
808
|
+
if (py !== void 0 && py > maxY) maxY = py;
|
|
809
|
+
}
|
|
810
|
+
return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
|
|
811
|
+
}
|
|
812
|
+
case "line": {
|
|
813
|
+
const halfW = r / 12;
|
|
814
|
+
const cos = Math.cos(el.angle);
|
|
815
|
+
const sin = Math.sin(el.angle);
|
|
816
|
+
const perpX = -sin * halfW;
|
|
817
|
+
const perpY = cos * halfW;
|
|
818
|
+
const x0 = cx + perpX;
|
|
819
|
+
const y0 = cy + perpY;
|
|
820
|
+
const x1 = cx + r * cos + perpX;
|
|
821
|
+
const y1 = cy + r * sin + perpY;
|
|
822
|
+
const x2 = cx + r * cos - perpX;
|
|
823
|
+
const y2 = cy + r * sin - perpY;
|
|
824
|
+
const x3 = cx - perpX;
|
|
825
|
+
const y3 = cy - perpY;
|
|
826
|
+
const minX = Math.min(x0, x1, x2, x3);
|
|
827
|
+
const minY = Math.min(y0, y1, y2, y3);
|
|
828
|
+
const maxX = Math.max(x0, x1, x2, x3);
|
|
829
|
+
const maxY = Math.max(y0, y1, y2, y3);
|
|
830
|
+
return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
function transferStrokeBounds(prev, next) {
|
|
835
|
+
if (prev.type !== "stroke" || next.type !== "stroke") return;
|
|
836
|
+
if (prev.points !== next.points) return;
|
|
837
|
+
if (prev.position.x !== next.position.x || prev.position.y !== next.position.y) return;
|
|
838
|
+
const bounds = strokeBoundsCache.get(prev);
|
|
839
|
+
if (bounds) strokeBoundsCache.set(next, bounds);
|
|
840
|
+
}
|
|
841
|
+
function boundsIntersect(a, b) {
|
|
842
|
+
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;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// src/elements/bounds.ts
|
|
846
|
+
function getElementsBoundingBox(elements) {
|
|
847
|
+
let minX = Infinity;
|
|
848
|
+
let minY = Infinity;
|
|
849
|
+
let maxX = -Infinity;
|
|
850
|
+
let maxY = -Infinity;
|
|
851
|
+
let found = false;
|
|
852
|
+
for (const el of elements) {
|
|
853
|
+
const b = getElementBounds(el);
|
|
854
|
+
if (!b) continue;
|
|
855
|
+
found = true;
|
|
856
|
+
if (b.x < minX) minX = b.x;
|
|
857
|
+
if (b.y < minY) minY = b.y;
|
|
858
|
+
if (b.x + b.w > maxX) maxX = b.x + b.w;
|
|
859
|
+
if (b.y + b.h > maxY) maxY = b.y + b.h;
|
|
860
|
+
}
|
|
861
|
+
if (!found) return null;
|
|
862
|
+
return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
|
|
863
|
+
}
|
|
864
|
+
|
|
591
865
|
// src/canvas/keyboard-actions.ts
|
|
592
866
|
var KeyboardActions = class {
|
|
593
867
|
constructor(deps) {
|
|
@@ -693,8 +967,18 @@ var KeyboardActions = class {
|
|
|
693
967
|
if (this.clipboard.length === 0) return;
|
|
694
968
|
const sel = this.selectTool();
|
|
695
969
|
if (!sel) return;
|
|
970
|
+
const cursor = this.deps.getLastPointerWorld?.() ?? null;
|
|
971
|
+
if (cursor) {
|
|
972
|
+
const bbox = getElementsBoundingBox(this.clipboard);
|
|
973
|
+
if (bbox) {
|
|
974
|
+
const centerX = bbox.x + bbox.w / 2;
|
|
975
|
+
const centerY = bbox.y + bbox.h / 2;
|
|
976
|
+
this.insertClones(this.clipboard, { x: cursor.x - centerX, y: cursor.y - centerY }, sel);
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
}
|
|
696
980
|
this.pasteCount++;
|
|
697
|
-
this.insertClones(this.clipboard, this.pasteCount * 20, sel);
|
|
981
|
+
this.insertClones(this.clipboard, { x: this.pasteCount * 20, y: this.pasteCount * 20 }, sel);
|
|
698
982
|
}
|
|
699
983
|
duplicate() {
|
|
700
984
|
if (this.deps.isToolActive()) return;
|
|
@@ -707,7 +991,7 @@ var KeyboardActions = class {
|
|
|
707
991
|
if (el) source.push(el);
|
|
708
992
|
}
|
|
709
993
|
if (source.length === 0) return;
|
|
710
|
-
this.insertClones(source, 20, sel);
|
|
994
|
+
this.insertClones(source, { x: 20, y: 20 }, sel);
|
|
711
995
|
}
|
|
712
996
|
deselect() {
|
|
713
997
|
if (this.deps.isToolActive()) return;
|
|
@@ -778,11 +1062,11 @@ var KeyboardActions = class {
|
|
|
778
1062
|
const newId = idMap.get(el.id);
|
|
779
1063
|
if (!newId) continue;
|
|
780
1064
|
clone.id = newId;
|
|
781
|
-
clone.position = { x: clone.position.x + offset, y: clone.position.y + offset };
|
|
1065
|
+
clone.position = { x: clone.position.x + offset.x, y: clone.position.y + offset.y };
|
|
782
1066
|
if (clone.type === "arrow") {
|
|
783
1067
|
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 };
|
|
1068
|
+
arrow.from = { x: arrow.from.x + offset.x, y: arrow.from.y + offset.y };
|
|
1069
|
+
arrow.to = { x: arrow.to.x + offset.x, y: arrow.to.y + offset.y };
|
|
786
1070
|
delete arrow.cachedControlPoint;
|
|
787
1071
|
if (arrow.fromBinding) {
|
|
788
1072
|
const newTarget = idMap.get(arrow.fromBinding.elementId);
|
|
@@ -828,6 +1112,9 @@ var DEFAULT_BINDINGS = [
|
|
|
828
1112
|
["z-front", ["mod+]"]],
|
|
829
1113
|
["z-back", ["mod+["]],
|
|
830
1114
|
["zoom-fit", ["shift+1"]],
|
|
1115
|
+
["zoom-in", ["mod+="]],
|
|
1116
|
+
["zoom-out", ["mod+-"]],
|
|
1117
|
+
["zoom-reset", ["mod+0"]],
|
|
831
1118
|
["nudge-left", ["arrowleft"]],
|
|
832
1119
|
["nudge-right", ["arrowright"]],
|
|
833
1120
|
["nudge-up", ["arrowup"]],
|
|
@@ -964,6 +1251,7 @@ var ShortcutMap = class {
|
|
|
964
1251
|
|
|
965
1252
|
// src/canvas/input-handler.ts
|
|
966
1253
|
var ZOOM_SENSITIVITY = 1e-3;
|
|
1254
|
+
var ZOOM_STEP = 1.2;
|
|
967
1255
|
var MIDDLE_BUTTON = 1;
|
|
968
1256
|
var NUDGE_DELTAS = {
|
|
969
1257
|
"nudge-left": [-1, 0],
|
|
@@ -985,7 +1273,8 @@ var InputHandler = class {
|
|
|
985
1273
|
getHistoryRecorder: () => this.historyRecorder,
|
|
986
1274
|
getHistoryStack: () => this.historyStack,
|
|
987
1275
|
isToolActive: () => this.isToolActive,
|
|
988
|
-
fitToContent: options.fitToContent
|
|
1276
|
+
fitToContent: options.fitToContent,
|
|
1277
|
+
getLastPointerWorld: () => this.lastPointerWorld()
|
|
989
1278
|
});
|
|
990
1279
|
this.shortcutMap = new ShortcutMap(options.shortcuts?.bindings);
|
|
991
1280
|
this.scope = options.shortcuts?.scope ?? "focus";
|
|
@@ -1041,11 +1330,21 @@ var InputHandler = class {
|
|
|
1041
1330
|
this.element.addEventListener("pointerdown", this.onPointerDown, opts);
|
|
1042
1331
|
this.element.addEventListener("pointermove", this.onPointerMove, opts);
|
|
1043
1332
|
this.element.addEventListener("pointerup", this.onPointerUp, opts);
|
|
1044
|
-
this.element.addEventListener("pointerleave", this.
|
|
1333
|
+
this.element.addEventListener("pointerleave", this.onPointerLeave, opts);
|
|
1045
1334
|
this.element.addEventListener("pointercancel", this.onPointerUp, opts);
|
|
1046
1335
|
window.addEventListener("keydown", this.onKeyDown, opts);
|
|
1047
1336
|
window.addEventListener("keyup", this.onKeyUp, opts);
|
|
1048
1337
|
}
|
|
1338
|
+
viewportCenter() {
|
|
1339
|
+
const rect = this.element.getBoundingClientRect();
|
|
1340
|
+
return { x: rect.width / 2, y: rect.height / 2 };
|
|
1341
|
+
}
|
|
1342
|
+
zoomByFactor(factor) {
|
|
1343
|
+
this.camera.zoomAt(this.camera.zoom * factor, this.viewportCenter());
|
|
1344
|
+
}
|
|
1345
|
+
zoomToLevel(level) {
|
|
1346
|
+
this.camera.zoomAt(level, this.viewportCenter());
|
|
1347
|
+
}
|
|
1049
1348
|
onWheel = (e) => {
|
|
1050
1349
|
e.preventDefault();
|
|
1051
1350
|
const rect = this.element.getBoundingClientRect();
|
|
@@ -1214,6 +1513,18 @@ var InputHandler = class {
|
|
|
1214
1513
|
e.preventDefault();
|
|
1215
1514
|
this.actions.zoomToFit();
|
|
1216
1515
|
return;
|
|
1516
|
+
case "zoom-in":
|
|
1517
|
+
e.preventDefault();
|
|
1518
|
+
this.zoomByFactor(ZOOM_STEP);
|
|
1519
|
+
return;
|
|
1520
|
+
case "zoom-out":
|
|
1521
|
+
e.preventDefault();
|
|
1522
|
+
this.zoomByFactor(1 / ZOOM_STEP);
|
|
1523
|
+
return;
|
|
1524
|
+
case "zoom-reset":
|
|
1525
|
+
e.preventDefault();
|
|
1526
|
+
this.zoomToLevel(1);
|
|
1527
|
+
return;
|
|
1217
1528
|
case "nudge-left":
|
|
1218
1529
|
case "nudge-right":
|
|
1219
1530
|
case "nudge-up":
|
|
@@ -1271,6 +1582,16 @@ var InputHandler = class {
|
|
|
1271
1582
|
midpoint(a, b) {
|
|
1272
1583
|
return { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
|
|
1273
1584
|
}
|
|
1585
|
+
lastPointerWorld() {
|
|
1586
|
+
const e = this.lastPointerEvent;
|
|
1587
|
+
if (!e) return null;
|
|
1588
|
+
const rect = this.element.getBoundingClientRect();
|
|
1589
|
+
return this.camera.screenToWorld({ x: e.clientX - rect.left, y: e.clientY - rect.top });
|
|
1590
|
+
}
|
|
1591
|
+
onPointerLeave = (e) => {
|
|
1592
|
+
this.lastPointerEvent = null;
|
|
1593
|
+
this.onPointerUp(e);
|
|
1594
|
+
};
|
|
1274
1595
|
toPointerState(e) {
|
|
1275
1596
|
const rect = this.element.getBoundingClientRect();
|
|
1276
1597
|
return {
|
|
@@ -1573,320 +1894,68 @@ var QuadNode = class _QuadNode {
|
|
|
1573
1894
|
];
|
|
1574
1895
|
const remaining = [];
|
|
1575
1896
|
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);
|
|
1582
|
-
}
|
|
1583
|
-
}
|
|
1584
|
-
this.items = remaining;
|
|
1585
|
-
}
|
|
1586
|
-
collapseIfEmpty() {
|
|
1587
|
-
if (!this.children) return;
|
|
1588
|
-
let totalItems = this.items.length;
|
|
1589
|
-
for (const child of this.children) {
|
|
1590
|
-
if (child.children) return;
|
|
1591
|
-
totalItems += child.items.length;
|
|
1592
|
-
}
|
|
1593
|
-
if (totalItems <= MAX_ITEMS) {
|
|
1594
|
-
for (const child of this.children) {
|
|
1595
|
-
this.items.push(...child.items);
|
|
1596
|
-
}
|
|
1597
|
-
this.children = null;
|
|
1598
|
-
}
|
|
1599
|
-
}
|
|
1600
|
-
};
|
|
1601
|
-
var Quadtree = class {
|
|
1602
|
-
root;
|
|
1603
|
-
_size = 0;
|
|
1604
|
-
worldBounds;
|
|
1605
|
-
constructor(worldBounds) {
|
|
1606
|
-
this.worldBounds = worldBounds;
|
|
1607
|
-
this.root = new QuadNode(worldBounds, 0);
|
|
1608
|
-
}
|
|
1609
|
-
get size() {
|
|
1610
|
-
return this._size;
|
|
1611
|
-
}
|
|
1612
|
-
insert(id, bounds) {
|
|
1613
|
-
this.root.insert({ id, bounds });
|
|
1614
|
-
this._size++;
|
|
1615
|
-
}
|
|
1616
|
-
remove(id) {
|
|
1617
|
-
if (this.root.remove(id)) {
|
|
1618
|
-
this._size--;
|
|
1619
|
-
}
|
|
1620
|
-
}
|
|
1621
|
-
update(id, newBounds) {
|
|
1622
|
-
this.remove(id);
|
|
1623
|
-
this.insert(id, newBounds);
|
|
1624
|
-
}
|
|
1625
|
-
query(rect) {
|
|
1626
|
-
const result = [];
|
|
1627
|
-
this.root.query(rect, result);
|
|
1628
|
-
return result;
|
|
1629
|
-
}
|
|
1630
|
-
queryPoint(point) {
|
|
1631
|
-
return this.query({ x: point.x, y: point.y, w: 0, h: 0 });
|
|
1632
|
-
}
|
|
1633
|
-
clear() {
|
|
1634
|
-
this.root = new QuadNode(this.worldBounds, 0);
|
|
1635
|
-
this._size = 0;
|
|
1636
|
-
}
|
|
1637
|
-
};
|
|
1638
|
-
|
|
1639
|
-
// src/core/geometry.ts
|
|
1640
|
-
function distSqToSegment(p, a, b) {
|
|
1641
|
-
const abx = b.x - a.x;
|
|
1642
|
-
const aby = b.y - a.y;
|
|
1643
|
-
const apx = p.x - a.x;
|
|
1644
|
-
const apy = p.y - a.y;
|
|
1645
|
-
const lenSq = abx * abx + aby * aby;
|
|
1646
|
-
if (lenSq === 0) {
|
|
1647
|
-
return apx * apx + apy * apy;
|
|
1648
|
-
}
|
|
1649
|
-
const t = Math.max(0, Math.min(1, (apx * abx + apy * aby) / lenSq));
|
|
1650
|
-
const dx = p.x - (a.x + t * abx);
|
|
1651
|
-
const dy = p.y - (a.y + t * aby);
|
|
1652
|
-
return dx * dx + dy * dy;
|
|
1653
|
-
}
|
|
1654
|
-
|
|
1655
|
-
// src/elements/arrow-geometry.ts
|
|
1656
|
-
function getArrowControlPoint(from, to, bend) {
|
|
1657
|
-
const midX = (from.x + to.x) / 2;
|
|
1658
|
-
const midY = (from.y + to.y) / 2;
|
|
1659
|
-
if (bend === 0) return { x: midX, y: midY };
|
|
1660
|
-
const dx = to.x - from.x;
|
|
1661
|
-
const dy = to.y - from.y;
|
|
1662
|
-
const len = Math.sqrt(dx * dx + dy * dy);
|
|
1663
|
-
if (len === 0) return { x: midX, y: midY };
|
|
1664
|
-
const perpX = -dy / len;
|
|
1665
|
-
const perpY = dx / len;
|
|
1666
|
-
return {
|
|
1667
|
-
x: midX + perpX * bend,
|
|
1668
|
-
y: midY + perpY * bend
|
|
1669
|
-
};
|
|
1670
|
-
}
|
|
1671
|
-
function getArrowMidpoint(from, to, bend) {
|
|
1672
|
-
const cp = getArrowControlPoint(from, to, bend);
|
|
1673
|
-
return {
|
|
1674
|
-
x: 0.25 * from.x + 0.5 * cp.x + 0.25 * to.x,
|
|
1675
|
-
y: 0.25 * from.y + 0.5 * cp.y + 0.25 * to.y
|
|
1676
|
-
};
|
|
1677
|
-
}
|
|
1678
|
-
function getBendFromPoint(from, to, dragPoint) {
|
|
1679
|
-
const midX = (from.x + to.x) / 2;
|
|
1680
|
-
const midY = (from.y + to.y) / 2;
|
|
1681
|
-
const dx = to.x - from.x;
|
|
1682
|
-
const dy = to.y - from.y;
|
|
1683
|
-
const len = Math.sqrt(dx * dx + dy * dy);
|
|
1684
|
-
if (len === 0) return 0;
|
|
1685
|
-
const perpX = -dy / len;
|
|
1686
|
-
const perpY = dx / len;
|
|
1687
|
-
return (dragPoint.x - midX) * perpX + (dragPoint.y - midY) * perpY;
|
|
1688
|
-
}
|
|
1689
|
-
function getArrowTangentAngle(from, to, bend, t) {
|
|
1690
|
-
const cp = getArrowControlPoint(from, to, bend);
|
|
1691
|
-
const tangentX = 2 * (1 - t) * (cp.x - from.x) + 2 * t * (to.x - cp.x);
|
|
1692
|
-
const tangentY = 2 * (1 - t) * (cp.y - from.y) + 2 * t * (to.y - cp.y);
|
|
1693
|
-
return Math.atan2(tangentY, tangentX);
|
|
1694
|
-
}
|
|
1695
|
-
function isNearBezier(point, from, to, bend, threshold) {
|
|
1696
|
-
if (bend === 0) return isNearLine(point, from, to, threshold);
|
|
1697
|
-
const cp = getArrowControlPoint(from, to, bend);
|
|
1698
|
-
const segments = 20;
|
|
1699
|
-
for (let i = 0; i < segments; i++) {
|
|
1700
|
-
const t0 = i / segments;
|
|
1701
|
-
const t1 = (i + 1) / segments;
|
|
1702
|
-
const a = bezierPoint(from, cp, to, t0);
|
|
1703
|
-
const b = bezierPoint(from, cp, to, t1);
|
|
1704
|
-
if (isNearLine(point, a, b, threshold)) return true;
|
|
1705
|
-
}
|
|
1706
|
-
return false;
|
|
1707
|
-
}
|
|
1708
|
-
function getArrowBounds(from, to, bend) {
|
|
1709
|
-
if (bend === 0) {
|
|
1710
|
-
const minX2 = Math.min(from.x, to.x);
|
|
1711
|
-
const minY2 = Math.min(from.y, to.y);
|
|
1712
|
-
return {
|
|
1713
|
-
x: minX2,
|
|
1714
|
-
y: minY2,
|
|
1715
|
-
w: Math.abs(to.x - from.x),
|
|
1716
|
-
h: Math.abs(to.y - from.y)
|
|
1717
|
-
};
|
|
1718
|
-
}
|
|
1719
|
-
const cp = getArrowControlPoint(from, to, bend);
|
|
1720
|
-
const steps = 20;
|
|
1721
|
-
let minX = Math.min(from.x, to.x);
|
|
1722
|
-
let minY = Math.min(from.y, to.y);
|
|
1723
|
-
let maxX = Math.max(from.x, to.x);
|
|
1724
|
-
let maxY = Math.max(from.y, to.y);
|
|
1725
|
-
for (let i = 1; i < steps; i++) {
|
|
1726
|
-
const t = i / steps;
|
|
1727
|
-
const p = bezierPoint(from, cp, to, t);
|
|
1728
|
-
if (p.x < minX) minX = p.x;
|
|
1729
|
-
if (p.y < minY) minY = p.y;
|
|
1730
|
-
if (p.x > maxX) maxX = p.x;
|
|
1731
|
-
if (p.y > maxY) maxY = p.y;
|
|
1732
|
-
}
|
|
1733
|
-
return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
|
|
1734
|
-
}
|
|
1735
|
-
function bezierPoint(from, cp, to, t) {
|
|
1736
|
-
const mt = 1 - t;
|
|
1737
|
-
return {
|
|
1738
|
-
x: mt * mt * from.x + 2 * mt * t * cp.x + t * t * to.x,
|
|
1739
|
-
y: mt * mt * from.y + 2 * mt * t * cp.y + t * t * to.y
|
|
1740
|
-
};
|
|
1741
|
-
}
|
|
1742
|
-
function isNearLine(point, a, b, threshold) {
|
|
1743
|
-
return distSqToSegment(point, a, b) <= threshold * threshold;
|
|
1744
|
-
}
|
|
1745
|
-
|
|
1746
|
-
// src/elements/element-bounds.ts
|
|
1747
|
-
var strokeBoundsCache = /* @__PURE__ */ new WeakMap();
|
|
1748
|
-
function getElementBounds(element) {
|
|
1749
|
-
if (element.type === "grid") return null;
|
|
1750
|
-
if ("size" in element) {
|
|
1751
|
-
return {
|
|
1752
|
-
x: element.position.x,
|
|
1753
|
-
y: element.position.y,
|
|
1754
|
-
w: element.size.w,
|
|
1755
|
-
h: element.size.h
|
|
1756
|
-
};
|
|
1757
|
-
}
|
|
1758
|
-
if (element.type === "stroke") {
|
|
1759
|
-
if (element.points.length === 0) return null;
|
|
1760
|
-
const cached = strokeBoundsCache.get(element);
|
|
1761
|
-
if (cached) return cached;
|
|
1762
|
-
let minX = Infinity;
|
|
1763
|
-
let minY = Infinity;
|
|
1764
|
-
let maxX = -Infinity;
|
|
1765
|
-
let maxY = -Infinity;
|
|
1766
|
-
for (const p of element.points) {
|
|
1767
|
-
const px = p.x + element.position.x;
|
|
1768
|
-
const py = p.y + element.position.y;
|
|
1769
|
-
if (px < minX) minX = px;
|
|
1770
|
-
if (py < minY) minY = py;
|
|
1771
|
-
if (px > maxX) maxX = px;
|
|
1772
|
-
if (py > maxY) maxY = py;
|
|
1897
|
+
const idx = this.getChildIndex(item.bounds);
|
|
1898
|
+
if (idx !== -1) {
|
|
1899
|
+
const target = this.children[idx];
|
|
1900
|
+
if (target) target.insert(item);
|
|
1901
|
+
} else {
|
|
1902
|
+
remaining.push(item);
|
|
1903
|
+
}
|
|
1773
1904
|
}
|
|
1774
|
-
|
|
1775
|
-
strokeBoundsCache.set(element, bounds);
|
|
1776
|
-
return bounds;
|
|
1905
|
+
this.items = remaining;
|
|
1777
1906
|
}
|
|
1778
|
-
|
|
1779
|
-
|
|
1907
|
+
collapseIfEmpty() {
|
|
1908
|
+
if (!this.children) return;
|
|
1909
|
+
let totalItems = this.items.length;
|
|
1910
|
+
for (const child of this.children) {
|
|
1911
|
+
if (child.children) return;
|
|
1912
|
+
totalItems += child.items.length;
|
|
1913
|
+
}
|
|
1914
|
+
if (totalItems <= MAX_ITEMS) {
|
|
1915
|
+
for (const child of this.children) {
|
|
1916
|
+
this.items.push(...child.items);
|
|
1917
|
+
}
|
|
1918
|
+
this.children = null;
|
|
1919
|
+
}
|
|
1780
1920
|
}
|
|
1781
|
-
|
|
1782
|
-
|
|
1921
|
+
};
|
|
1922
|
+
var Quadtree = class {
|
|
1923
|
+
root;
|
|
1924
|
+
_size = 0;
|
|
1925
|
+
worldBounds;
|
|
1926
|
+
constructor(worldBounds) {
|
|
1927
|
+
this.worldBounds = worldBounds;
|
|
1928
|
+
this.root = new QuadNode(worldBounds, 0);
|
|
1783
1929
|
}
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
function getArrowBoundsAnalytical(from, to, bend) {
|
|
1787
|
-
if (bend === 0) {
|
|
1788
|
-
const minX2 = Math.min(from.x, to.x);
|
|
1789
|
-
const minY2 = Math.min(from.y, to.y);
|
|
1790
|
-
return {
|
|
1791
|
-
x: minX2,
|
|
1792
|
-
y: minY2,
|
|
1793
|
-
w: Math.abs(to.x - from.x),
|
|
1794
|
-
h: Math.abs(to.y - from.y)
|
|
1795
|
-
};
|
|
1930
|
+
get size() {
|
|
1931
|
+
return this._size;
|
|
1796
1932
|
}
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
let minY = Math.min(from.y, to.y);
|
|
1801
|
-
let maxY = Math.max(from.y, to.y);
|
|
1802
|
-
const tx = from.x - 2 * cp.x + to.x;
|
|
1803
|
-
if (tx !== 0) {
|
|
1804
|
-
const t = (from.x - cp.x) / tx;
|
|
1805
|
-
if (t > 0 && t < 1) {
|
|
1806
|
-
const mt = 1 - t;
|
|
1807
|
-
const x = mt * mt * from.x + 2 * mt * t * cp.x + t * t * to.x;
|
|
1808
|
-
if (x < minX) minX = x;
|
|
1809
|
-
if (x > maxX) maxX = x;
|
|
1810
|
-
}
|
|
1933
|
+
insert(id, bounds) {
|
|
1934
|
+
this.root.insert({ id, bounds });
|
|
1935
|
+
this._size++;
|
|
1811
1936
|
}
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
if (t > 0 && t < 1) {
|
|
1816
|
-
const mt = 1 - t;
|
|
1817
|
-
const y = mt * mt * from.y + 2 * mt * t * cp.y + t * t * to.y;
|
|
1818
|
-
if (y < minY) minY = y;
|
|
1819
|
-
if (y > maxY) maxY = y;
|
|
1937
|
+
remove(id) {
|
|
1938
|
+
if (this.root.remove(id)) {
|
|
1939
|
+
this._size--;
|
|
1820
1940
|
}
|
|
1821
1941
|
}
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
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;
|
|
1855
|
-
}
|
|
1856
|
-
return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
|
|
1857
|
-
}
|
|
1858
|
-
case "line": {
|
|
1859
|
-
const halfW = r / 12;
|
|
1860
|
-
const cos = Math.cos(el.angle);
|
|
1861
|
-
const sin = Math.sin(el.angle);
|
|
1862
|
-
const perpX = -sin * halfW;
|
|
1863
|
-
const perpY = cos * halfW;
|
|
1864
|
-
const x0 = cx + perpX;
|
|
1865
|
-
const y0 = cy + perpY;
|
|
1866
|
-
const x1 = cx + r * cos + perpX;
|
|
1867
|
-
const y1 = cy + r * sin + perpY;
|
|
1868
|
-
const x2 = cx + r * cos - perpX;
|
|
1869
|
-
const y2 = cy + r * sin - perpY;
|
|
1870
|
-
const x3 = cx - perpX;
|
|
1871
|
-
const y3 = cy - perpY;
|
|
1872
|
-
const minX = Math.min(x0, x1, x2, x3);
|
|
1873
|
-
const minY = Math.min(y0, y1, y2, y3);
|
|
1874
|
-
const maxX = Math.max(x0, x1, x2, x3);
|
|
1875
|
-
const maxY = Math.max(y0, y1, y2, y3);
|
|
1876
|
-
return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
|
|
1877
|
-
}
|
|
1942
|
+
update(id, newBounds) {
|
|
1943
|
+
this.remove(id);
|
|
1944
|
+
this.insert(id, newBounds);
|
|
1878
1945
|
}
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
}
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1946
|
+
query(rect) {
|
|
1947
|
+
const result = [];
|
|
1948
|
+
this.root.query(rect, result);
|
|
1949
|
+
return result;
|
|
1950
|
+
}
|
|
1951
|
+
queryPoint(point) {
|
|
1952
|
+
return this.query({ x: point.x, y: point.y, w: 0, h: 0 });
|
|
1953
|
+
}
|
|
1954
|
+
clear() {
|
|
1955
|
+
this.root = new QuadNode(this.worldBounds, 0);
|
|
1956
|
+
this._size = 0;
|
|
1957
|
+
}
|
|
1958
|
+
};
|
|
1890
1959
|
|
|
1891
1960
|
// src/elements/stroke-smoothing.ts
|
|
1892
1961
|
var MIN_PRESSURE_SCALE = 0.2;
|
|
@@ -3508,6 +3577,8 @@ var NoteEditor = class {
|
|
|
3508
3577
|
inputHandler = null;
|
|
3509
3578
|
pendingEditId = null;
|
|
3510
3579
|
onStopCallback = null;
|
|
3580
|
+
beginHistory = null;
|
|
3581
|
+
commitHistory = null;
|
|
3511
3582
|
toolbar;
|
|
3512
3583
|
placeholder;
|
|
3513
3584
|
constructor(options) {
|
|
@@ -3523,6 +3594,10 @@ var NoteEditor = class {
|
|
|
3523
3594
|
setOnStop(callback) {
|
|
3524
3595
|
this.onStopCallback = callback;
|
|
3525
3596
|
}
|
|
3597
|
+
setHistoryHooks(begin, commit) {
|
|
3598
|
+
this.beginHistory = begin;
|
|
3599
|
+
this.commitHistory = commit;
|
|
3600
|
+
}
|
|
3526
3601
|
startEditing(node, elementId, store) {
|
|
3527
3602
|
if (this.editingId === elementId) return;
|
|
3528
3603
|
if (this.editingId) {
|
|
@@ -3554,18 +3629,21 @@ var NoteEditor = class {
|
|
|
3554
3629
|
this.editingNode.removeAttribute("data-fn-empty");
|
|
3555
3630
|
const text = sanitizeNoteHtml(this.editingNode.innerHTML);
|
|
3556
3631
|
const current = store.getById(this.editingId);
|
|
3557
|
-
|
|
3558
|
-
store.update(this.editingId, { text });
|
|
3559
|
-
}
|
|
3632
|
+
const textChanged = !!current && (current.type === "note" || current.type === "text") && current.text !== text;
|
|
3560
3633
|
this.editingNode.contentEditable = "false";
|
|
3561
3634
|
Object.assign(this.editingNode.style, {
|
|
3562
3635
|
userSelect: "none",
|
|
3563
3636
|
cursor: "default"
|
|
3564
3637
|
});
|
|
3565
3638
|
this.toolbar?.hide();
|
|
3639
|
+
this.beginHistory?.();
|
|
3640
|
+
if (textChanged) {
|
|
3641
|
+
store.update(this.editingId, { text });
|
|
3642
|
+
}
|
|
3566
3643
|
if (this.editingId && this.onStopCallback) {
|
|
3567
3644
|
this.onStopCallback(this.editingId);
|
|
3568
3645
|
}
|
|
3646
|
+
this.commitHistory?.();
|
|
3569
3647
|
this.editingId = null;
|
|
3570
3648
|
this.editingNode = null;
|
|
3571
3649
|
this.blurHandler = null;
|
|
@@ -3638,26 +3716,6 @@ var NoteEditor = class {
|
|
|
3638
3716
|
}
|
|
3639
3717
|
};
|
|
3640
3718
|
|
|
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
3719
|
// src/tools/tool-manager.ts
|
|
3662
3720
|
var ToolManager = class {
|
|
3663
3721
|
tools = /* @__PURE__ */ new Map();
|
|
@@ -5178,7 +5236,103 @@ var MarginViewport = class {
|
|
|
5178
5236
|
}
|
|
5179
5237
|
};
|
|
5180
5238
|
|
|
5239
|
+
// src/elements/element-style.ts
|
|
5240
|
+
function styleToPatch(element, style) {
|
|
5241
|
+
const { color, fillColor, strokeWidth, opacity, fontSize } = style;
|
|
5242
|
+
switch (element.type) {
|
|
5243
|
+
case "stroke":
|
|
5244
|
+
return {
|
|
5245
|
+
...color !== void 0 ? { color } : {},
|
|
5246
|
+
...strokeWidth !== void 0 ? { width: strokeWidth } : {},
|
|
5247
|
+
...opacity !== void 0 ? { opacity } : {}
|
|
5248
|
+
};
|
|
5249
|
+
case "arrow":
|
|
5250
|
+
return {
|
|
5251
|
+
...color !== void 0 ? { color } : {},
|
|
5252
|
+
...strokeWidth !== void 0 ? { width: strokeWidth } : {}
|
|
5253
|
+
};
|
|
5254
|
+
case "shape":
|
|
5255
|
+
return {
|
|
5256
|
+
...color !== void 0 ? { strokeColor: color } : {},
|
|
5257
|
+
...fillColor !== void 0 ? { fillColor } : {},
|
|
5258
|
+
...strokeWidth !== void 0 ? { strokeWidth } : {}
|
|
5259
|
+
};
|
|
5260
|
+
case "text":
|
|
5261
|
+
return {
|
|
5262
|
+
...color !== void 0 ? { color } : {},
|
|
5263
|
+
...fontSize !== void 0 ? { fontSize } : {}
|
|
5264
|
+
};
|
|
5265
|
+
case "note":
|
|
5266
|
+
return {
|
|
5267
|
+
...color !== void 0 ? { textColor: color } : {},
|
|
5268
|
+
...fillColor !== void 0 ? { backgroundColor: fillColor } : {},
|
|
5269
|
+
...fontSize !== void 0 ? { fontSize } : {}
|
|
5270
|
+
};
|
|
5271
|
+
case "grid":
|
|
5272
|
+
return {
|
|
5273
|
+
...color !== void 0 ? { strokeColor: color } : {},
|
|
5274
|
+
...strokeWidth !== void 0 ? { strokeWidth } : {},
|
|
5275
|
+
...opacity !== void 0 ? { opacity } : {}
|
|
5276
|
+
};
|
|
5277
|
+
case "template":
|
|
5278
|
+
return {
|
|
5279
|
+
...color !== void 0 ? { strokeColor: color } : {},
|
|
5280
|
+
...fillColor !== void 0 ? { fillColor } : {},
|
|
5281
|
+
...strokeWidth !== void 0 ? { strokeWidth } : {},
|
|
5282
|
+
...opacity !== void 0 ? { opacity } : {}
|
|
5283
|
+
};
|
|
5284
|
+
default:
|
|
5285
|
+
return {};
|
|
5286
|
+
}
|
|
5287
|
+
}
|
|
5288
|
+
function getElementStyle(element) {
|
|
5289
|
+
switch (element.type) {
|
|
5290
|
+
case "stroke":
|
|
5291
|
+
return { color: element.color, strokeWidth: element.width, opacity: element.opacity };
|
|
5292
|
+
case "arrow":
|
|
5293
|
+
return { color: element.color, strokeWidth: element.width };
|
|
5294
|
+
case "shape":
|
|
5295
|
+
return {
|
|
5296
|
+
color: element.strokeColor,
|
|
5297
|
+
fillColor: element.fillColor,
|
|
5298
|
+
strokeWidth: element.strokeWidth
|
|
5299
|
+
};
|
|
5300
|
+
case "text":
|
|
5301
|
+
return { color: element.color, fontSize: element.fontSize };
|
|
5302
|
+
case "note":
|
|
5303
|
+
return {
|
|
5304
|
+
color: element.textColor,
|
|
5305
|
+
fillColor: element.backgroundColor,
|
|
5306
|
+
...element.fontSize !== void 0 ? { fontSize: element.fontSize } : {}
|
|
5307
|
+
};
|
|
5308
|
+
case "grid":
|
|
5309
|
+
return {
|
|
5310
|
+
color: element.strokeColor,
|
|
5311
|
+
strokeWidth: element.strokeWidth,
|
|
5312
|
+
opacity: element.opacity
|
|
5313
|
+
};
|
|
5314
|
+
case "template":
|
|
5315
|
+
return {
|
|
5316
|
+
color: element.strokeColor,
|
|
5317
|
+
fillColor: element.fillColor,
|
|
5318
|
+
strokeWidth: element.strokeWidth,
|
|
5319
|
+
opacity: element.opacity
|
|
5320
|
+
};
|
|
5321
|
+
default:
|
|
5322
|
+
return {};
|
|
5323
|
+
}
|
|
5324
|
+
}
|
|
5325
|
+
|
|
5181
5326
|
// src/canvas/viewport.ts
|
|
5327
|
+
var EMPTY_IDS = [];
|
|
5328
|
+
function noop() {
|
|
5329
|
+
}
|
|
5330
|
+
function sharedValue(values) {
|
|
5331
|
+
const present = values.filter((v) => v !== void 0);
|
|
5332
|
+
if (present.length === 0) return void 0;
|
|
5333
|
+
const first = present[0];
|
|
5334
|
+
return present.every((v) => v === first) ? first : void 0;
|
|
5335
|
+
}
|
|
5182
5336
|
var Viewport = class {
|
|
5183
5337
|
constructor(container, options = {}) {
|
|
5184
5338
|
this.container = container;
|
|
@@ -5212,6 +5366,10 @@ var Viewport = class {
|
|
|
5212
5366
|
placeholder: options.placeholder
|
|
5213
5367
|
});
|
|
5214
5368
|
this.noteEditor.setOnStop((id) => this.onTextEditStop(id));
|
|
5369
|
+
this.noteEditor.setHistoryHooks(
|
|
5370
|
+
() => this.historyRecorder.begin(),
|
|
5371
|
+
() => this.historyRecorder.commit()
|
|
5372
|
+
);
|
|
5215
5373
|
this.onHtmlElementMount = options.onHtmlElementMount;
|
|
5216
5374
|
this.dropHandler = options.onDrop;
|
|
5217
5375
|
this.history = new HistoryStack();
|
|
@@ -5228,6 +5386,7 @@ var Viewport = class {
|
|
|
5228
5386
|
requestRender: () => this.requestRender(),
|
|
5229
5387
|
switchTool: (name) => this.toolManager.setTool(name, this.toolContext),
|
|
5230
5388
|
editElement: (id) => this.startEditingElement(id),
|
|
5389
|
+
fitNoteHeight: (id) => this.fitNoteHeight(id),
|
|
5231
5390
|
setCursor: (cursor) => {
|
|
5232
5391
|
this.wrapper.style.cursor = cursor;
|
|
5233
5392
|
},
|
|
@@ -5525,6 +5684,52 @@ var Viewport = class {
|
|
|
5525
5684
|
this.gridChangeListeners.delete(listener);
|
|
5526
5685
|
};
|
|
5527
5686
|
}
|
|
5687
|
+
getSelectTool() {
|
|
5688
|
+
return this.toolManager.getTool("select");
|
|
5689
|
+
}
|
|
5690
|
+
getSelectedIds() {
|
|
5691
|
+
return this.getSelectTool()?.selectedIds ?? EMPTY_IDS;
|
|
5692
|
+
}
|
|
5693
|
+
onSelectionChange(listener) {
|
|
5694
|
+
const tool = this.getSelectTool();
|
|
5695
|
+
return tool ? tool.onSelectionChange(listener) : noop;
|
|
5696
|
+
}
|
|
5697
|
+
getSelectionStyle() {
|
|
5698
|
+
const ids = this.getSelectedIds();
|
|
5699
|
+
if (ids.length === 0) return null;
|
|
5700
|
+
const styles = [];
|
|
5701
|
+
for (const id of ids) {
|
|
5702
|
+
const el = this.store.getById(id);
|
|
5703
|
+
if (el) styles.push(getElementStyle(el));
|
|
5704
|
+
}
|
|
5705
|
+
if (styles.length === 0) return null;
|
|
5706
|
+
const result = {};
|
|
5707
|
+
const color = sharedValue(styles.map((s) => s.color));
|
|
5708
|
+
if (color !== void 0) result.color = color;
|
|
5709
|
+
const fillColor = sharedValue(styles.map((s) => s.fillColor));
|
|
5710
|
+
if (fillColor !== void 0) result.fillColor = fillColor;
|
|
5711
|
+
const strokeWidth = sharedValue(styles.map((s) => s.strokeWidth));
|
|
5712
|
+
if (strokeWidth !== void 0) result.strokeWidth = strokeWidth;
|
|
5713
|
+
const opacity = sharedValue(styles.map((s) => s.opacity));
|
|
5714
|
+
if (opacity !== void 0) result.opacity = opacity;
|
|
5715
|
+
const fontSize = sharedValue(styles.map((s) => s.fontSize));
|
|
5716
|
+
if (fontSize !== void 0) result.fontSize = fontSize;
|
|
5717
|
+
return result;
|
|
5718
|
+
}
|
|
5719
|
+
applyStyleToSelection(style) {
|
|
5720
|
+
const ids = this.getSelectedIds();
|
|
5721
|
+
if (ids.length === 0) return;
|
|
5722
|
+
this.historyRecorder.begin();
|
|
5723
|
+
for (const id of ids) {
|
|
5724
|
+
const el = this.store.getById(id);
|
|
5725
|
+
if (!el) continue;
|
|
5726
|
+
const patch = styleToPatch(el, style);
|
|
5727
|
+
if (Object.keys(patch).length > 0) {
|
|
5728
|
+
this.store.update(id, patch);
|
|
5729
|
+
}
|
|
5730
|
+
}
|
|
5731
|
+
this.historyRecorder.commit();
|
|
5732
|
+
}
|
|
5528
5733
|
getRenderStats() {
|
|
5529
5734
|
return this.renderLoop.getStats();
|
|
5530
5735
|
}
|
|
@@ -5562,31 +5767,38 @@ var Viewport = class {
|
|
|
5562
5767
|
this.noteEditor.startEditing(node, id, this.store);
|
|
5563
5768
|
}
|
|
5564
5769
|
}
|
|
5770
|
+
fitNoteHeight(elementId) {
|
|
5771
|
+
const element = this.store.getById(elementId);
|
|
5772
|
+
if (!element || element.type !== "note") return;
|
|
5773
|
+
if (isNoteContentEmpty(element.text)) return;
|
|
5774
|
+
const node = this.domNodeManager.getNode(elementId);
|
|
5775
|
+
if (!node) return;
|
|
5776
|
+
const measured = node.scrollHeight;
|
|
5777
|
+
if (measured > element.size.h) {
|
|
5778
|
+
this.store.update(elementId, { size: { w: element.size.w, h: measured } });
|
|
5779
|
+
}
|
|
5780
|
+
}
|
|
5565
5781
|
onTextEditStop(elementId) {
|
|
5566
5782
|
const element = this.store.getById(elementId);
|
|
5567
5783
|
if (!element) return;
|
|
5568
5784
|
if (element.type === "note") {
|
|
5569
5785
|
if (isNoteContentEmpty(element.text)) {
|
|
5570
|
-
this.historyRecorder.begin();
|
|
5571
5786
|
this.store.remove(elementId);
|
|
5572
|
-
|
|
5787
|
+
return;
|
|
5573
5788
|
}
|
|
5789
|
+
this.fitNoteHeight(elementId);
|
|
5574
5790
|
return;
|
|
5575
5791
|
}
|
|
5576
5792
|
if (element.type !== "text") return;
|
|
5577
5793
|
if (!element.text || element.text.trim() === "") {
|
|
5578
|
-
this.historyRecorder.begin();
|
|
5579
5794
|
this.store.remove(elementId);
|
|
5580
|
-
this.historyRecorder.commit();
|
|
5581
5795
|
return;
|
|
5582
5796
|
}
|
|
5583
5797
|
const node = this.domNodeManager.getNode(elementId);
|
|
5584
5798
|
if (node && "size" in element) {
|
|
5585
|
-
const
|
|
5586
|
-
if (
|
|
5587
|
-
this.store.update(elementId, {
|
|
5588
|
-
size: { w: element.size.w, h: measuredHeight }
|
|
5589
|
-
});
|
|
5799
|
+
const measured = node.scrollHeight;
|
|
5800
|
+
if (measured !== element.size.h) {
|
|
5801
|
+
this.store.update(elementId, { size: { w: element.size.w, h: measured } });
|
|
5590
5802
|
}
|
|
5591
5803
|
}
|
|
5592
5804
|
}
|
|
@@ -6133,6 +6345,7 @@ var HANDLE_CURSORS = {
|
|
|
6133
6345
|
var SelectTool = class {
|
|
6134
6346
|
name = "select";
|
|
6135
6347
|
_selectedIds = [];
|
|
6348
|
+
selectionListeners = /* @__PURE__ */ new Set();
|
|
6136
6349
|
mode = { type: "idle" };
|
|
6137
6350
|
lastWorld = { x: 0, y: 0 };
|
|
6138
6351
|
currentWorld = { x: 0, y: 0 };
|
|
@@ -6142,10 +6355,22 @@ var SelectTool = class {
|
|
|
6142
6355
|
resizeAspectRatio = 0;
|
|
6143
6356
|
hoveredId = null;
|
|
6144
6357
|
get selectedIds() {
|
|
6145
|
-
return
|
|
6358
|
+
return this._selectedIds;
|
|
6146
6359
|
}
|
|
6147
|
-
|
|
6360
|
+
onSelectionChange(listener) {
|
|
6361
|
+
this.selectionListeners.add(listener);
|
|
6362
|
+
return () => {
|
|
6363
|
+
this.selectionListeners.delete(listener);
|
|
6364
|
+
};
|
|
6365
|
+
}
|
|
6366
|
+
setSelectedIds(ids) {
|
|
6367
|
+
const prev = this._selectedIds;
|
|
6368
|
+
if (prev.length === ids.length && prev.every((id, i) => id === ids[i])) return;
|
|
6148
6369
|
this._selectedIds = ids;
|
|
6370
|
+
for (const listener of this.selectionListeners) listener();
|
|
6371
|
+
}
|
|
6372
|
+
setSelection(ids) {
|
|
6373
|
+
this.setSelectedIds(ids);
|
|
6149
6374
|
this.ctx?.requestRender();
|
|
6150
6375
|
}
|
|
6151
6376
|
get isMarqueeActive() {
|
|
@@ -6155,7 +6380,7 @@ var SelectTool = class {
|
|
|
6155
6380
|
this.ctx = ctx;
|
|
6156
6381
|
}
|
|
6157
6382
|
onDeactivate(ctx) {
|
|
6158
|
-
this.
|
|
6383
|
+
this.setSelectedIds([]);
|
|
6159
6384
|
this.mode = { type: "idle" };
|
|
6160
6385
|
this.hoveredId = null;
|
|
6161
6386
|
ctx.setCursor?.("default");
|
|
@@ -6206,22 +6431,22 @@ var SelectTool = class {
|
|
|
6206
6431
|
const alreadySelected = this._selectedIds.includes(hit.id);
|
|
6207
6432
|
if (state.shiftKey) {
|
|
6208
6433
|
if (alreadySelected) {
|
|
6209
|
-
this.
|
|
6434
|
+
this.setSelectedIds(this._selectedIds.filter((id) => id !== hit.id));
|
|
6210
6435
|
this.mode = { type: "idle" };
|
|
6211
6436
|
} else {
|
|
6212
|
-
this.
|
|
6437
|
+
this.setSelectedIds([...this._selectedIds, hit.id]);
|
|
6213
6438
|
this.mode = hit.locked ? { type: "idle" } : { type: "dragging" };
|
|
6214
6439
|
}
|
|
6215
6440
|
} else {
|
|
6216
6441
|
if (!alreadySelected) {
|
|
6217
|
-
this.
|
|
6442
|
+
this.setSelectedIds([hit.id]);
|
|
6218
6443
|
} else if (this._selectedIds.length > 1) {
|
|
6219
6444
|
this.pendingSingleSelectId = hit.id;
|
|
6220
6445
|
}
|
|
6221
6446
|
this.mode = hit.locked ? { type: "idle" } : { type: "dragging" };
|
|
6222
6447
|
}
|
|
6223
6448
|
} else {
|
|
6224
|
-
this.
|
|
6449
|
+
this.setSelectedIds([]);
|
|
6225
6450
|
this.mode = { type: "marquee", start: world };
|
|
6226
6451
|
}
|
|
6227
6452
|
ctx.requestRender();
|
|
@@ -6294,17 +6519,22 @@ var SelectTool = class {
|
|
|
6294
6519
|
if (this.mode.type === "marquee") {
|
|
6295
6520
|
const rect = this.getMarqueeRect();
|
|
6296
6521
|
if (rect) {
|
|
6297
|
-
this.
|
|
6522
|
+
this.setSelectedIds(this.findElementsInRect(rect, ctx));
|
|
6298
6523
|
}
|
|
6299
6524
|
ctx.requestRender();
|
|
6300
6525
|
}
|
|
6301
6526
|
if (!this.hasDragged && this.pendingSingleSelectId !== null) {
|
|
6302
|
-
this.
|
|
6527
|
+
this.setSelectedIds([this.pendingSingleSelectId]);
|
|
6303
6528
|
}
|
|
6304
6529
|
this.pendingSingleSelectId = null;
|
|
6305
6530
|
this.hasDragged = false;
|
|
6531
|
+
const resizedNoteId = this.mode.type === "resizing" ? this.mode.elementId : null;
|
|
6306
6532
|
this.mode = { type: "idle" };
|
|
6307
6533
|
ctx.setCursor?.("default");
|
|
6534
|
+
if (resizedNoteId !== null) {
|
|
6535
|
+
const el = ctx.store.getById(resizedNoteId);
|
|
6536
|
+
if (el?.type === "note") ctx.fitNoteHeight?.(resizedNoteId);
|
|
6537
|
+
}
|
|
6308
6538
|
}
|
|
6309
6539
|
onHover(state, ctx) {
|
|
6310
6540
|
const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
|
|
@@ -7516,7 +7746,7 @@ var TemplateTool = class {
|
|
|
7516
7746
|
};
|
|
7517
7747
|
|
|
7518
7748
|
// src/index.ts
|
|
7519
|
-
var VERSION = "0.
|
|
7749
|
+
var VERSION = "0.27.0";
|
|
7520
7750
|
// Annotate the CommonJS export names for ESM import in node:
|
|
7521
7751
|
0 && (module.exports = {
|
|
7522
7752
|
ArrowTool,
|
|
@@ -7558,6 +7788,7 @@ var VERSION = "0.25.0";
|
|
|
7558
7788
|
getArrowTangentAngle,
|
|
7559
7789
|
getBendFromPoint,
|
|
7560
7790
|
getElementBounds,
|
|
7791
|
+
getElementStyle,
|
|
7561
7792
|
getElementsBoundingBox,
|
|
7562
7793
|
getHexCellsInCone,
|
|
7563
7794
|
getHexCellsInLine,
|
|
@@ -7569,6 +7800,7 @@ var VERSION = "0.25.0";
|
|
|
7569
7800
|
smartSnap,
|
|
7570
7801
|
snapPoint,
|
|
7571
7802
|
snapToHexCenter,
|
|
7803
|
+
styleToPatch,
|
|
7572
7804
|
toggleBold,
|
|
7573
7805
|
toggleItalic,
|
|
7574
7806
|
toggleStrikethrough,
|