@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.js
CHANGED
|
@@ -509,6 +509,278 @@ function createId(prefix) {
|
|
|
509
509
|
return `${prefix}_${Date.now().toString(36)}_${(counter++).toString(36)}`;
|
|
510
510
|
}
|
|
511
511
|
|
|
512
|
+
// src/core/geometry.ts
|
|
513
|
+
function distSqToSegment(p, a, b) {
|
|
514
|
+
const abx = b.x - a.x;
|
|
515
|
+
const aby = b.y - a.y;
|
|
516
|
+
const apx = p.x - a.x;
|
|
517
|
+
const apy = p.y - a.y;
|
|
518
|
+
const lenSq = abx * abx + aby * aby;
|
|
519
|
+
if (lenSq === 0) {
|
|
520
|
+
return apx * apx + apy * apy;
|
|
521
|
+
}
|
|
522
|
+
const t = Math.max(0, Math.min(1, (apx * abx + apy * aby) / lenSq));
|
|
523
|
+
const dx = p.x - (a.x + t * abx);
|
|
524
|
+
const dy = p.y - (a.y + t * aby);
|
|
525
|
+
return dx * dx + dy * dy;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// src/elements/arrow-geometry.ts
|
|
529
|
+
function getArrowControlPoint(from, to, bend) {
|
|
530
|
+
const midX = (from.x + to.x) / 2;
|
|
531
|
+
const midY = (from.y + to.y) / 2;
|
|
532
|
+
if (bend === 0) return { x: midX, y: midY };
|
|
533
|
+
const dx = to.x - from.x;
|
|
534
|
+
const dy = to.y - from.y;
|
|
535
|
+
const len = Math.sqrt(dx * dx + dy * dy);
|
|
536
|
+
if (len === 0) return { x: midX, y: midY };
|
|
537
|
+
const perpX = -dy / len;
|
|
538
|
+
const perpY = dx / len;
|
|
539
|
+
return {
|
|
540
|
+
x: midX + perpX * bend,
|
|
541
|
+
y: midY + perpY * bend
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
function getArrowMidpoint(from, to, bend) {
|
|
545
|
+
const cp = getArrowControlPoint(from, to, bend);
|
|
546
|
+
return {
|
|
547
|
+
x: 0.25 * from.x + 0.5 * cp.x + 0.25 * to.x,
|
|
548
|
+
y: 0.25 * from.y + 0.5 * cp.y + 0.25 * to.y
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
function getBendFromPoint(from, to, dragPoint) {
|
|
552
|
+
const midX = (from.x + to.x) / 2;
|
|
553
|
+
const midY = (from.y + to.y) / 2;
|
|
554
|
+
const dx = to.x - from.x;
|
|
555
|
+
const dy = to.y - from.y;
|
|
556
|
+
const len = Math.sqrt(dx * dx + dy * dy);
|
|
557
|
+
if (len === 0) return 0;
|
|
558
|
+
const perpX = -dy / len;
|
|
559
|
+
const perpY = dx / len;
|
|
560
|
+
return (dragPoint.x - midX) * perpX + (dragPoint.y - midY) * perpY;
|
|
561
|
+
}
|
|
562
|
+
function getArrowTangentAngle(from, to, bend, t) {
|
|
563
|
+
const cp = getArrowControlPoint(from, to, bend);
|
|
564
|
+
const tangentX = 2 * (1 - t) * (cp.x - from.x) + 2 * t * (to.x - cp.x);
|
|
565
|
+
const tangentY = 2 * (1 - t) * (cp.y - from.y) + 2 * t * (to.y - cp.y);
|
|
566
|
+
return Math.atan2(tangentY, tangentX);
|
|
567
|
+
}
|
|
568
|
+
function isNearBezier(point, from, to, bend, threshold) {
|
|
569
|
+
if (bend === 0) return isNearLine(point, from, to, threshold);
|
|
570
|
+
const cp = getArrowControlPoint(from, to, bend);
|
|
571
|
+
const segments = 20;
|
|
572
|
+
for (let i = 0; i < segments; i++) {
|
|
573
|
+
const t0 = i / segments;
|
|
574
|
+
const t1 = (i + 1) / segments;
|
|
575
|
+
const a = bezierPoint(from, cp, to, t0);
|
|
576
|
+
const b = bezierPoint(from, cp, to, t1);
|
|
577
|
+
if (isNearLine(point, a, b, threshold)) return true;
|
|
578
|
+
}
|
|
579
|
+
return false;
|
|
580
|
+
}
|
|
581
|
+
function getArrowBounds(from, to, bend) {
|
|
582
|
+
if (bend === 0) {
|
|
583
|
+
const minX2 = Math.min(from.x, to.x);
|
|
584
|
+
const minY2 = Math.min(from.y, to.y);
|
|
585
|
+
return {
|
|
586
|
+
x: minX2,
|
|
587
|
+
y: minY2,
|
|
588
|
+
w: Math.abs(to.x - from.x),
|
|
589
|
+
h: Math.abs(to.y - from.y)
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
const cp = getArrowControlPoint(from, to, bend);
|
|
593
|
+
const steps = 20;
|
|
594
|
+
let minX = Math.min(from.x, to.x);
|
|
595
|
+
let minY = Math.min(from.y, to.y);
|
|
596
|
+
let maxX = Math.max(from.x, to.x);
|
|
597
|
+
let maxY = Math.max(from.y, to.y);
|
|
598
|
+
for (let i = 1; i < steps; i++) {
|
|
599
|
+
const t = i / steps;
|
|
600
|
+
const p = bezierPoint(from, cp, to, t);
|
|
601
|
+
if (p.x < minX) minX = p.x;
|
|
602
|
+
if (p.y < minY) minY = p.y;
|
|
603
|
+
if (p.x > maxX) maxX = p.x;
|
|
604
|
+
if (p.y > maxY) maxY = p.y;
|
|
605
|
+
}
|
|
606
|
+
return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
|
|
607
|
+
}
|
|
608
|
+
function bezierPoint(from, cp, to, t) {
|
|
609
|
+
const mt = 1 - t;
|
|
610
|
+
return {
|
|
611
|
+
x: mt * mt * from.x + 2 * mt * t * cp.x + t * t * to.x,
|
|
612
|
+
y: mt * mt * from.y + 2 * mt * t * cp.y + t * t * to.y
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
function isNearLine(point, a, b, threshold) {
|
|
616
|
+
return distSqToSegment(point, a, b) <= threshold * threshold;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// src/elements/element-bounds.ts
|
|
620
|
+
var strokeBoundsCache = /* @__PURE__ */ new WeakMap();
|
|
621
|
+
function getElementBounds(element) {
|
|
622
|
+
if (element.type === "grid") return null;
|
|
623
|
+
if ("size" in element) {
|
|
624
|
+
return {
|
|
625
|
+
x: element.position.x,
|
|
626
|
+
y: element.position.y,
|
|
627
|
+
w: element.size.w,
|
|
628
|
+
h: element.size.h
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
if (element.type === "stroke") {
|
|
632
|
+
if (element.points.length === 0) return null;
|
|
633
|
+
const cached = strokeBoundsCache.get(element);
|
|
634
|
+
if (cached) return cached;
|
|
635
|
+
let minX = Infinity;
|
|
636
|
+
let minY = Infinity;
|
|
637
|
+
let maxX = -Infinity;
|
|
638
|
+
let maxY = -Infinity;
|
|
639
|
+
for (const p of element.points) {
|
|
640
|
+
const px = p.x + element.position.x;
|
|
641
|
+
const py = p.y + element.position.y;
|
|
642
|
+
if (px < minX) minX = px;
|
|
643
|
+
if (py < minY) minY = py;
|
|
644
|
+
if (px > maxX) maxX = px;
|
|
645
|
+
if (py > maxY) maxY = py;
|
|
646
|
+
}
|
|
647
|
+
const bounds = { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
|
|
648
|
+
strokeBoundsCache.set(element, bounds);
|
|
649
|
+
return bounds;
|
|
650
|
+
}
|
|
651
|
+
if (element.type === "arrow") {
|
|
652
|
+
return getArrowBoundsAnalytical(element.from, element.to, element.bend);
|
|
653
|
+
}
|
|
654
|
+
if (element.type === "template") {
|
|
655
|
+
return getTemplateBounds(element);
|
|
656
|
+
}
|
|
657
|
+
return null;
|
|
658
|
+
}
|
|
659
|
+
function getArrowBoundsAnalytical(from, to, bend) {
|
|
660
|
+
if (bend === 0) {
|
|
661
|
+
const minX2 = Math.min(from.x, to.x);
|
|
662
|
+
const minY2 = Math.min(from.y, to.y);
|
|
663
|
+
return {
|
|
664
|
+
x: minX2,
|
|
665
|
+
y: minY2,
|
|
666
|
+
w: Math.abs(to.x - from.x),
|
|
667
|
+
h: Math.abs(to.y - from.y)
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
const cp = getArrowControlPoint(from, to, bend);
|
|
671
|
+
let minX = Math.min(from.x, to.x);
|
|
672
|
+
let maxX = Math.max(from.x, to.x);
|
|
673
|
+
let minY = Math.min(from.y, to.y);
|
|
674
|
+
let maxY = Math.max(from.y, to.y);
|
|
675
|
+
const tx = from.x - 2 * cp.x + to.x;
|
|
676
|
+
if (tx !== 0) {
|
|
677
|
+
const t = (from.x - cp.x) / tx;
|
|
678
|
+
if (t > 0 && t < 1) {
|
|
679
|
+
const mt = 1 - t;
|
|
680
|
+
const x = mt * mt * from.x + 2 * mt * t * cp.x + t * t * to.x;
|
|
681
|
+
if (x < minX) minX = x;
|
|
682
|
+
if (x > maxX) maxX = x;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
const ty = from.y - 2 * cp.y + to.y;
|
|
686
|
+
if (ty !== 0) {
|
|
687
|
+
const t = (from.y - cp.y) / ty;
|
|
688
|
+
if (t > 0 && t < 1) {
|
|
689
|
+
const mt = 1 - t;
|
|
690
|
+
const y = mt * mt * from.y + 2 * mt * t * cp.y + t * t * to.y;
|
|
691
|
+
if (y < minY) minY = y;
|
|
692
|
+
if (y > maxY) maxY = y;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
|
|
696
|
+
}
|
|
697
|
+
function getTemplateBounds(el) {
|
|
698
|
+
const { x: cx, y: cy } = el.position;
|
|
699
|
+
const r = el.radius;
|
|
700
|
+
switch (el.templateShape) {
|
|
701
|
+
case "circle":
|
|
702
|
+
return { x: cx - r, y: cy - r, w: 2 * r, h: 2 * r };
|
|
703
|
+
case "square":
|
|
704
|
+
return { x: cx - r / 2, y: cy - r / 2, w: r, h: r };
|
|
705
|
+
case "cone": {
|
|
706
|
+
const halfAngle = Math.atan(0.5);
|
|
707
|
+
const tipX = cx;
|
|
708
|
+
const tipY = cy;
|
|
709
|
+
const leftX = cx + r * Math.cos(el.angle - halfAngle);
|
|
710
|
+
const leftY = cy + r * Math.sin(el.angle - halfAngle);
|
|
711
|
+
const rightX = cx + r * Math.cos(el.angle + halfAngle);
|
|
712
|
+
const rightY = cy + r * Math.sin(el.angle + halfAngle);
|
|
713
|
+
const farX = cx + r * Math.cos(el.angle);
|
|
714
|
+
const farY = cy + r * Math.sin(el.angle);
|
|
715
|
+
const xs = [tipX, leftX, rightX, farX];
|
|
716
|
+
const ys = [tipY, leftY, rightY, farY];
|
|
717
|
+
let minX = Infinity;
|
|
718
|
+
let minY = Infinity;
|
|
719
|
+
let maxX = -Infinity;
|
|
720
|
+
let maxY = -Infinity;
|
|
721
|
+
for (let i = 0; i < xs.length; i++) {
|
|
722
|
+
const px = xs[i];
|
|
723
|
+
const py = ys[i];
|
|
724
|
+
if (px !== void 0 && px < minX) minX = px;
|
|
725
|
+
if (px !== void 0 && px > maxX) maxX = px;
|
|
726
|
+
if (py !== void 0 && py < minY) minY = py;
|
|
727
|
+
if (py !== void 0 && py > maxY) maxY = py;
|
|
728
|
+
}
|
|
729
|
+
return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
|
|
730
|
+
}
|
|
731
|
+
case "line": {
|
|
732
|
+
const halfW = r / 12;
|
|
733
|
+
const cos = Math.cos(el.angle);
|
|
734
|
+
const sin = Math.sin(el.angle);
|
|
735
|
+
const perpX = -sin * halfW;
|
|
736
|
+
const perpY = cos * halfW;
|
|
737
|
+
const x0 = cx + perpX;
|
|
738
|
+
const y0 = cy + perpY;
|
|
739
|
+
const x1 = cx + r * cos + perpX;
|
|
740
|
+
const y1 = cy + r * sin + perpY;
|
|
741
|
+
const x2 = cx + r * cos - perpX;
|
|
742
|
+
const y2 = cy + r * sin - perpY;
|
|
743
|
+
const x3 = cx - perpX;
|
|
744
|
+
const y3 = cy - perpY;
|
|
745
|
+
const minX = Math.min(x0, x1, x2, x3);
|
|
746
|
+
const minY = Math.min(y0, y1, y2, y3);
|
|
747
|
+
const maxX = Math.max(x0, x1, x2, x3);
|
|
748
|
+
const maxY = Math.max(y0, y1, y2, y3);
|
|
749
|
+
return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
function transferStrokeBounds(prev, next) {
|
|
754
|
+
if (prev.type !== "stroke" || next.type !== "stroke") return;
|
|
755
|
+
if (prev.points !== next.points) return;
|
|
756
|
+
if (prev.position.x !== next.position.x || prev.position.y !== next.position.y) return;
|
|
757
|
+
const bounds = strokeBoundsCache.get(prev);
|
|
758
|
+
if (bounds) strokeBoundsCache.set(next, bounds);
|
|
759
|
+
}
|
|
760
|
+
function boundsIntersect(a, b) {
|
|
761
|
+
return a.x <= b.x + b.w && a.x + a.w >= b.x && a.y <= b.y + b.h && a.y + a.h >= b.y;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// src/elements/bounds.ts
|
|
765
|
+
function getElementsBoundingBox(elements) {
|
|
766
|
+
let minX = Infinity;
|
|
767
|
+
let minY = Infinity;
|
|
768
|
+
let maxX = -Infinity;
|
|
769
|
+
let maxY = -Infinity;
|
|
770
|
+
let found = false;
|
|
771
|
+
for (const el of elements) {
|
|
772
|
+
const b = getElementBounds(el);
|
|
773
|
+
if (!b) continue;
|
|
774
|
+
found = true;
|
|
775
|
+
if (b.x < minX) minX = b.x;
|
|
776
|
+
if (b.y < minY) minY = b.y;
|
|
777
|
+
if (b.x + b.w > maxX) maxX = b.x + b.w;
|
|
778
|
+
if (b.y + b.h > maxY) maxY = b.y + b.h;
|
|
779
|
+
}
|
|
780
|
+
if (!found) return null;
|
|
781
|
+
return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
|
|
782
|
+
}
|
|
783
|
+
|
|
512
784
|
// src/canvas/keyboard-actions.ts
|
|
513
785
|
var KeyboardActions = class {
|
|
514
786
|
constructor(deps) {
|
|
@@ -614,8 +886,18 @@ var KeyboardActions = class {
|
|
|
614
886
|
if (this.clipboard.length === 0) return;
|
|
615
887
|
const sel = this.selectTool();
|
|
616
888
|
if (!sel) return;
|
|
889
|
+
const cursor = this.deps.getLastPointerWorld?.() ?? null;
|
|
890
|
+
if (cursor) {
|
|
891
|
+
const bbox = getElementsBoundingBox(this.clipboard);
|
|
892
|
+
if (bbox) {
|
|
893
|
+
const centerX = bbox.x + bbox.w / 2;
|
|
894
|
+
const centerY = bbox.y + bbox.h / 2;
|
|
895
|
+
this.insertClones(this.clipboard, { x: cursor.x - centerX, y: cursor.y - centerY }, sel);
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
}
|
|
617
899
|
this.pasteCount++;
|
|
618
|
-
this.insertClones(this.clipboard, this.pasteCount * 20, sel);
|
|
900
|
+
this.insertClones(this.clipboard, { x: this.pasteCount * 20, y: this.pasteCount * 20 }, sel);
|
|
619
901
|
}
|
|
620
902
|
duplicate() {
|
|
621
903
|
if (this.deps.isToolActive()) return;
|
|
@@ -628,7 +910,7 @@ var KeyboardActions = class {
|
|
|
628
910
|
if (el) source.push(el);
|
|
629
911
|
}
|
|
630
912
|
if (source.length === 0) return;
|
|
631
|
-
this.insertClones(source, 20, sel);
|
|
913
|
+
this.insertClones(source, { x: 20, y: 20 }, sel);
|
|
632
914
|
}
|
|
633
915
|
deselect() {
|
|
634
916
|
if (this.deps.isToolActive()) return;
|
|
@@ -699,11 +981,11 @@ var KeyboardActions = class {
|
|
|
699
981
|
const newId = idMap.get(el.id);
|
|
700
982
|
if (!newId) continue;
|
|
701
983
|
clone.id = newId;
|
|
702
|
-
clone.position = { x: clone.position.x + offset, y: clone.position.y + offset };
|
|
984
|
+
clone.position = { x: clone.position.x + offset.x, y: clone.position.y + offset.y };
|
|
703
985
|
if (clone.type === "arrow") {
|
|
704
986
|
const arrow = clone;
|
|
705
|
-
arrow.from = { x: arrow.from.x + offset, y: arrow.from.y + offset };
|
|
706
|
-
arrow.to = { x: arrow.to.x + offset, y: arrow.to.y + offset };
|
|
987
|
+
arrow.from = { x: arrow.from.x + offset.x, y: arrow.from.y + offset.y };
|
|
988
|
+
arrow.to = { x: arrow.to.x + offset.x, y: arrow.to.y + offset.y };
|
|
707
989
|
delete arrow.cachedControlPoint;
|
|
708
990
|
if (arrow.fromBinding) {
|
|
709
991
|
const newTarget = idMap.get(arrow.fromBinding.elementId);
|
|
@@ -749,6 +1031,9 @@ var DEFAULT_BINDINGS = [
|
|
|
749
1031
|
["z-front", ["mod+]"]],
|
|
750
1032
|
["z-back", ["mod+["]],
|
|
751
1033
|
["zoom-fit", ["shift+1"]],
|
|
1034
|
+
["zoom-in", ["mod+="]],
|
|
1035
|
+
["zoom-out", ["mod+-"]],
|
|
1036
|
+
["zoom-reset", ["mod+0"]],
|
|
752
1037
|
["nudge-left", ["arrowleft"]],
|
|
753
1038
|
["nudge-right", ["arrowright"]],
|
|
754
1039
|
["nudge-up", ["arrowup"]],
|
|
@@ -885,6 +1170,7 @@ var ShortcutMap = class {
|
|
|
885
1170
|
|
|
886
1171
|
// src/canvas/input-handler.ts
|
|
887
1172
|
var ZOOM_SENSITIVITY = 1e-3;
|
|
1173
|
+
var ZOOM_STEP = 1.2;
|
|
888
1174
|
var MIDDLE_BUTTON = 1;
|
|
889
1175
|
var NUDGE_DELTAS = {
|
|
890
1176
|
"nudge-left": [-1, 0],
|
|
@@ -906,7 +1192,8 @@ var InputHandler = class {
|
|
|
906
1192
|
getHistoryRecorder: () => this.historyRecorder,
|
|
907
1193
|
getHistoryStack: () => this.historyStack,
|
|
908
1194
|
isToolActive: () => this.isToolActive,
|
|
909
|
-
fitToContent: options.fitToContent
|
|
1195
|
+
fitToContent: options.fitToContent,
|
|
1196
|
+
getLastPointerWorld: () => this.lastPointerWorld()
|
|
910
1197
|
});
|
|
911
1198
|
this.shortcutMap = new ShortcutMap(options.shortcuts?.bindings);
|
|
912
1199
|
this.scope = options.shortcuts?.scope ?? "focus";
|
|
@@ -962,11 +1249,21 @@ var InputHandler = class {
|
|
|
962
1249
|
this.element.addEventListener("pointerdown", this.onPointerDown, opts);
|
|
963
1250
|
this.element.addEventListener("pointermove", this.onPointerMove, opts);
|
|
964
1251
|
this.element.addEventListener("pointerup", this.onPointerUp, opts);
|
|
965
|
-
this.element.addEventListener("pointerleave", this.
|
|
1252
|
+
this.element.addEventListener("pointerleave", this.onPointerLeave, opts);
|
|
966
1253
|
this.element.addEventListener("pointercancel", this.onPointerUp, opts);
|
|
967
1254
|
window.addEventListener("keydown", this.onKeyDown, opts);
|
|
968
1255
|
window.addEventListener("keyup", this.onKeyUp, opts);
|
|
969
1256
|
}
|
|
1257
|
+
viewportCenter() {
|
|
1258
|
+
const rect = this.element.getBoundingClientRect();
|
|
1259
|
+
return { x: rect.width / 2, y: rect.height / 2 };
|
|
1260
|
+
}
|
|
1261
|
+
zoomByFactor(factor) {
|
|
1262
|
+
this.camera.zoomAt(this.camera.zoom * factor, this.viewportCenter());
|
|
1263
|
+
}
|
|
1264
|
+
zoomToLevel(level) {
|
|
1265
|
+
this.camera.zoomAt(level, this.viewportCenter());
|
|
1266
|
+
}
|
|
970
1267
|
onWheel = (e) => {
|
|
971
1268
|
e.preventDefault();
|
|
972
1269
|
const rect = this.element.getBoundingClientRect();
|
|
@@ -1135,6 +1432,18 @@ var InputHandler = class {
|
|
|
1135
1432
|
e.preventDefault();
|
|
1136
1433
|
this.actions.zoomToFit();
|
|
1137
1434
|
return;
|
|
1435
|
+
case "zoom-in":
|
|
1436
|
+
e.preventDefault();
|
|
1437
|
+
this.zoomByFactor(ZOOM_STEP);
|
|
1438
|
+
return;
|
|
1439
|
+
case "zoom-out":
|
|
1440
|
+
e.preventDefault();
|
|
1441
|
+
this.zoomByFactor(1 / ZOOM_STEP);
|
|
1442
|
+
return;
|
|
1443
|
+
case "zoom-reset":
|
|
1444
|
+
e.preventDefault();
|
|
1445
|
+
this.zoomToLevel(1);
|
|
1446
|
+
return;
|
|
1138
1447
|
case "nudge-left":
|
|
1139
1448
|
case "nudge-right":
|
|
1140
1449
|
case "nudge-up":
|
|
@@ -1192,6 +1501,16 @@ var InputHandler = class {
|
|
|
1192
1501
|
midpoint(a, b) {
|
|
1193
1502
|
return { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
|
|
1194
1503
|
}
|
|
1504
|
+
lastPointerWorld() {
|
|
1505
|
+
const e = this.lastPointerEvent;
|
|
1506
|
+
if (!e) return null;
|
|
1507
|
+
const rect = this.element.getBoundingClientRect();
|
|
1508
|
+
return this.camera.screenToWorld({ x: e.clientX - rect.left, y: e.clientY - rect.top });
|
|
1509
|
+
}
|
|
1510
|
+
onPointerLeave = (e) => {
|
|
1511
|
+
this.lastPointerEvent = null;
|
|
1512
|
+
this.onPointerUp(e);
|
|
1513
|
+
};
|
|
1195
1514
|
toPointerState(e) {
|
|
1196
1515
|
const rect = this.element.getBoundingClientRect();
|
|
1197
1516
|
return {
|
|
@@ -1494,320 +1813,68 @@ var QuadNode = class _QuadNode {
|
|
|
1494
1813
|
];
|
|
1495
1814
|
const remaining = [];
|
|
1496
1815
|
for (const item of this.items) {
|
|
1497
|
-
const idx = this.getChildIndex(item.bounds);
|
|
1498
|
-
if (idx !== -1) {
|
|
1499
|
-
const target = this.children[idx];
|
|
1500
|
-
if (target) target.insert(item);
|
|
1501
|
-
} else {
|
|
1502
|
-
remaining.push(item);
|
|
1503
|
-
}
|
|
1504
|
-
}
|
|
1505
|
-
this.items = remaining;
|
|
1506
|
-
}
|
|
1507
|
-
collapseIfEmpty() {
|
|
1508
|
-
if (!this.children) return;
|
|
1509
|
-
let totalItems = this.items.length;
|
|
1510
|
-
for (const child of this.children) {
|
|
1511
|
-
if (child.children) return;
|
|
1512
|
-
totalItems += child.items.length;
|
|
1513
|
-
}
|
|
1514
|
-
if (totalItems <= MAX_ITEMS) {
|
|
1515
|
-
for (const child of this.children) {
|
|
1516
|
-
this.items.push(...child.items);
|
|
1517
|
-
}
|
|
1518
|
-
this.children = null;
|
|
1519
|
-
}
|
|
1520
|
-
}
|
|
1521
|
-
};
|
|
1522
|
-
var Quadtree = class {
|
|
1523
|
-
root;
|
|
1524
|
-
_size = 0;
|
|
1525
|
-
worldBounds;
|
|
1526
|
-
constructor(worldBounds) {
|
|
1527
|
-
this.worldBounds = worldBounds;
|
|
1528
|
-
this.root = new QuadNode(worldBounds, 0);
|
|
1529
|
-
}
|
|
1530
|
-
get size() {
|
|
1531
|
-
return this._size;
|
|
1532
|
-
}
|
|
1533
|
-
insert(id, bounds) {
|
|
1534
|
-
this.root.insert({ id, bounds });
|
|
1535
|
-
this._size++;
|
|
1536
|
-
}
|
|
1537
|
-
remove(id) {
|
|
1538
|
-
if (this.root.remove(id)) {
|
|
1539
|
-
this._size--;
|
|
1540
|
-
}
|
|
1541
|
-
}
|
|
1542
|
-
update(id, newBounds) {
|
|
1543
|
-
this.remove(id);
|
|
1544
|
-
this.insert(id, newBounds);
|
|
1545
|
-
}
|
|
1546
|
-
query(rect) {
|
|
1547
|
-
const result = [];
|
|
1548
|
-
this.root.query(rect, result);
|
|
1549
|
-
return result;
|
|
1550
|
-
}
|
|
1551
|
-
queryPoint(point) {
|
|
1552
|
-
return this.query({ x: point.x, y: point.y, w: 0, h: 0 });
|
|
1553
|
-
}
|
|
1554
|
-
clear() {
|
|
1555
|
-
this.root = new QuadNode(this.worldBounds, 0);
|
|
1556
|
-
this._size = 0;
|
|
1557
|
-
}
|
|
1558
|
-
};
|
|
1559
|
-
|
|
1560
|
-
// src/core/geometry.ts
|
|
1561
|
-
function distSqToSegment(p, a, b) {
|
|
1562
|
-
const abx = b.x - a.x;
|
|
1563
|
-
const aby = b.y - a.y;
|
|
1564
|
-
const apx = p.x - a.x;
|
|
1565
|
-
const apy = p.y - a.y;
|
|
1566
|
-
const lenSq = abx * abx + aby * aby;
|
|
1567
|
-
if (lenSq === 0) {
|
|
1568
|
-
return apx * apx + apy * apy;
|
|
1569
|
-
}
|
|
1570
|
-
const t = Math.max(0, Math.min(1, (apx * abx + apy * aby) / lenSq));
|
|
1571
|
-
const dx = p.x - (a.x + t * abx);
|
|
1572
|
-
const dy = p.y - (a.y + t * aby);
|
|
1573
|
-
return dx * dx + dy * dy;
|
|
1574
|
-
}
|
|
1575
|
-
|
|
1576
|
-
// src/elements/arrow-geometry.ts
|
|
1577
|
-
function getArrowControlPoint(from, to, bend) {
|
|
1578
|
-
const midX = (from.x + to.x) / 2;
|
|
1579
|
-
const midY = (from.y + to.y) / 2;
|
|
1580
|
-
if (bend === 0) return { x: midX, y: midY };
|
|
1581
|
-
const dx = to.x - from.x;
|
|
1582
|
-
const dy = to.y - from.y;
|
|
1583
|
-
const len = Math.sqrt(dx * dx + dy * dy);
|
|
1584
|
-
if (len === 0) return { x: midX, y: midY };
|
|
1585
|
-
const perpX = -dy / len;
|
|
1586
|
-
const perpY = dx / len;
|
|
1587
|
-
return {
|
|
1588
|
-
x: midX + perpX * bend,
|
|
1589
|
-
y: midY + perpY * bend
|
|
1590
|
-
};
|
|
1591
|
-
}
|
|
1592
|
-
function getArrowMidpoint(from, to, bend) {
|
|
1593
|
-
const cp = getArrowControlPoint(from, to, bend);
|
|
1594
|
-
return {
|
|
1595
|
-
x: 0.25 * from.x + 0.5 * cp.x + 0.25 * to.x,
|
|
1596
|
-
y: 0.25 * from.y + 0.5 * cp.y + 0.25 * to.y
|
|
1597
|
-
};
|
|
1598
|
-
}
|
|
1599
|
-
function getBendFromPoint(from, to, dragPoint) {
|
|
1600
|
-
const midX = (from.x + to.x) / 2;
|
|
1601
|
-
const midY = (from.y + to.y) / 2;
|
|
1602
|
-
const dx = to.x - from.x;
|
|
1603
|
-
const dy = to.y - from.y;
|
|
1604
|
-
const len = Math.sqrt(dx * dx + dy * dy);
|
|
1605
|
-
if (len === 0) return 0;
|
|
1606
|
-
const perpX = -dy / len;
|
|
1607
|
-
const perpY = dx / len;
|
|
1608
|
-
return (dragPoint.x - midX) * perpX + (dragPoint.y - midY) * perpY;
|
|
1609
|
-
}
|
|
1610
|
-
function getArrowTangentAngle(from, to, bend, t) {
|
|
1611
|
-
const cp = getArrowControlPoint(from, to, bend);
|
|
1612
|
-
const tangentX = 2 * (1 - t) * (cp.x - from.x) + 2 * t * (to.x - cp.x);
|
|
1613
|
-
const tangentY = 2 * (1 - t) * (cp.y - from.y) + 2 * t * (to.y - cp.y);
|
|
1614
|
-
return Math.atan2(tangentY, tangentX);
|
|
1615
|
-
}
|
|
1616
|
-
function isNearBezier(point, from, to, bend, threshold) {
|
|
1617
|
-
if (bend === 0) return isNearLine(point, from, to, threshold);
|
|
1618
|
-
const cp = getArrowControlPoint(from, to, bend);
|
|
1619
|
-
const segments = 20;
|
|
1620
|
-
for (let i = 0; i < segments; i++) {
|
|
1621
|
-
const t0 = i / segments;
|
|
1622
|
-
const t1 = (i + 1) / segments;
|
|
1623
|
-
const a = bezierPoint(from, cp, to, t0);
|
|
1624
|
-
const b = bezierPoint(from, cp, to, t1);
|
|
1625
|
-
if (isNearLine(point, a, b, threshold)) return true;
|
|
1626
|
-
}
|
|
1627
|
-
return false;
|
|
1628
|
-
}
|
|
1629
|
-
function getArrowBounds(from, to, bend) {
|
|
1630
|
-
if (bend === 0) {
|
|
1631
|
-
const minX2 = Math.min(from.x, to.x);
|
|
1632
|
-
const minY2 = Math.min(from.y, to.y);
|
|
1633
|
-
return {
|
|
1634
|
-
x: minX2,
|
|
1635
|
-
y: minY2,
|
|
1636
|
-
w: Math.abs(to.x - from.x),
|
|
1637
|
-
h: Math.abs(to.y - from.y)
|
|
1638
|
-
};
|
|
1639
|
-
}
|
|
1640
|
-
const cp = getArrowControlPoint(from, to, bend);
|
|
1641
|
-
const steps = 20;
|
|
1642
|
-
let minX = Math.min(from.x, to.x);
|
|
1643
|
-
let minY = Math.min(from.y, to.y);
|
|
1644
|
-
let maxX = Math.max(from.x, to.x);
|
|
1645
|
-
let maxY = Math.max(from.y, to.y);
|
|
1646
|
-
for (let i = 1; i < steps; i++) {
|
|
1647
|
-
const t = i / steps;
|
|
1648
|
-
const p = bezierPoint(from, cp, to, t);
|
|
1649
|
-
if (p.x < minX) minX = p.x;
|
|
1650
|
-
if (p.y < minY) minY = p.y;
|
|
1651
|
-
if (p.x > maxX) maxX = p.x;
|
|
1652
|
-
if (p.y > maxY) maxY = p.y;
|
|
1653
|
-
}
|
|
1654
|
-
return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
|
|
1655
|
-
}
|
|
1656
|
-
function bezierPoint(from, cp, to, t) {
|
|
1657
|
-
const mt = 1 - t;
|
|
1658
|
-
return {
|
|
1659
|
-
x: mt * mt * from.x + 2 * mt * t * cp.x + t * t * to.x,
|
|
1660
|
-
y: mt * mt * from.y + 2 * mt * t * cp.y + t * t * to.y
|
|
1661
|
-
};
|
|
1662
|
-
}
|
|
1663
|
-
function isNearLine(point, a, b, threshold) {
|
|
1664
|
-
return distSqToSegment(point, a, b) <= threshold * threshold;
|
|
1665
|
-
}
|
|
1666
|
-
|
|
1667
|
-
// src/elements/element-bounds.ts
|
|
1668
|
-
var strokeBoundsCache = /* @__PURE__ */ new WeakMap();
|
|
1669
|
-
function getElementBounds(element) {
|
|
1670
|
-
if (element.type === "grid") return null;
|
|
1671
|
-
if ("size" in element) {
|
|
1672
|
-
return {
|
|
1673
|
-
x: element.position.x,
|
|
1674
|
-
y: element.position.y,
|
|
1675
|
-
w: element.size.w,
|
|
1676
|
-
h: element.size.h
|
|
1677
|
-
};
|
|
1678
|
-
}
|
|
1679
|
-
if (element.type === "stroke") {
|
|
1680
|
-
if (element.points.length === 0) return null;
|
|
1681
|
-
const cached = strokeBoundsCache.get(element);
|
|
1682
|
-
if (cached) return cached;
|
|
1683
|
-
let minX = Infinity;
|
|
1684
|
-
let minY = Infinity;
|
|
1685
|
-
let maxX = -Infinity;
|
|
1686
|
-
let maxY = -Infinity;
|
|
1687
|
-
for (const p of element.points) {
|
|
1688
|
-
const px = p.x + element.position.x;
|
|
1689
|
-
const py = p.y + element.position.y;
|
|
1690
|
-
if (px < minX) minX = px;
|
|
1691
|
-
if (py < minY) minY = py;
|
|
1692
|
-
if (px > maxX) maxX = px;
|
|
1693
|
-
if (py > maxY) maxY = py;
|
|
1816
|
+
const idx = this.getChildIndex(item.bounds);
|
|
1817
|
+
if (idx !== -1) {
|
|
1818
|
+
const target = this.children[idx];
|
|
1819
|
+
if (target) target.insert(item);
|
|
1820
|
+
} else {
|
|
1821
|
+
remaining.push(item);
|
|
1822
|
+
}
|
|
1694
1823
|
}
|
|
1695
|
-
|
|
1696
|
-
strokeBoundsCache.set(element, bounds);
|
|
1697
|
-
return bounds;
|
|
1824
|
+
this.items = remaining;
|
|
1698
1825
|
}
|
|
1699
|
-
|
|
1700
|
-
|
|
1826
|
+
collapseIfEmpty() {
|
|
1827
|
+
if (!this.children) return;
|
|
1828
|
+
let totalItems = this.items.length;
|
|
1829
|
+
for (const child of this.children) {
|
|
1830
|
+
if (child.children) return;
|
|
1831
|
+
totalItems += child.items.length;
|
|
1832
|
+
}
|
|
1833
|
+
if (totalItems <= MAX_ITEMS) {
|
|
1834
|
+
for (const child of this.children) {
|
|
1835
|
+
this.items.push(...child.items);
|
|
1836
|
+
}
|
|
1837
|
+
this.children = null;
|
|
1838
|
+
}
|
|
1701
1839
|
}
|
|
1702
|
-
|
|
1703
|
-
|
|
1840
|
+
};
|
|
1841
|
+
var Quadtree = class {
|
|
1842
|
+
root;
|
|
1843
|
+
_size = 0;
|
|
1844
|
+
worldBounds;
|
|
1845
|
+
constructor(worldBounds) {
|
|
1846
|
+
this.worldBounds = worldBounds;
|
|
1847
|
+
this.root = new QuadNode(worldBounds, 0);
|
|
1704
1848
|
}
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
function getArrowBoundsAnalytical(from, to, bend) {
|
|
1708
|
-
if (bend === 0) {
|
|
1709
|
-
const minX2 = Math.min(from.x, to.x);
|
|
1710
|
-
const minY2 = Math.min(from.y, to.y);
|
|
1711
|
-
return {
|
|
1712
|
-
x: minX2,
|
|
1713
|
-
y: minY2,
|
|
1714
|
-
w: Math.abs(to.x - from.x),
|
|
1715
|
-
h: Math.abs(to.y - from.y)
|
|
1716
|
-
};
|
|
1849
|
+
get size() {
|
|
1850
|
+
return this._size;
|
|
1717
1851
|
}
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
let minY = Math.min(from.y, to.y);
|
|
1722
|
-
let maxY = Math.max(from.y, to.y);
|
|
1723
|
-
const tx = from.x - 2 * cp.x + to.x;
|
|
1724
|
-
if (tx !== 0) {
|
|
1725
|
-
const t = (from.x - cp.x) / tx;
|
|
1726
|
-
if (t > 0 && t < 1) {
|
|
1727
|
-
const mt = 1 - t;
|
|
1728
|
-
const x = mt * mt * from.x + 2 * mt * t * cp.x + t * t * to.x;
|
|
1729
|
-
if (x < minX) minX = x;
|
|
1730
|
-
if (x > maxX) maxX = x;
|
|
1731
|
-
}
|
|
1852
|
+
insert(id, bounds) {
|
|
1853
|
+
this.root.insert({ id, bounds });
|
|
1854
|
+
this._size++;
|
|
1732
1855
|
}
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
if (t > 0 && t < 1) {
|
|
1737
|
-
const mt = 1 - t;
|
|
1738
|
-
const y = mt * mt * from.y + 2 * mt * t * cp.y + t * t * to.y;
|
|
1739
|
-
if (y < minY) minY = y;
|
|
1740
|
-
if (y > maxY) maxY = y;
|
|
1856
|
+
remove(id) {
|
|
1857
|
+
if (this.root.remove(id)) {
|
|
1858
|
+
this._size--;
|
|
1741
1859
|
}
|
|
1742
1860
|
}
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
const { x: cx, y: cy } = el.position;
|
|
1747
|
-
const r = el.radius;
|
|
1748
|
-
switch (el.templateShape) {
|
|
1749
|
-
case "circle":
|
|
1750
|
-
return { x: cx - r, y: cy - r, w: 2 * r, h: 2 * r };
|
|
1751
|
-
case "square":
|
|
1752
|
-
return { x: cx - r / 2, y: cy - r / 2, w: r, h: r };
|
|
1753
|
-
case "cone": {
|
|
1754
|
-
const halfAngle = Math.atan(0.5);
|
|
1755
|
-
const tipX = cx;
|
|
1756
|
-
const tipY = cy;
|
|
1757
|
-
const leftX = cx + r * Math.cos(el.angle - halfAngle);
|
|
1758
|
-
const leftY = cy + r * Math.sin(el.angle - halfAngle);
|
|
1759
|
-
const rightX = cx + r * Math.cos(el.angle + halfAngle);
|
|
1760
|
-
const rightY = cy + r * Math.sin(el.angle + halfAngle);
|
|
1761
|
-
const farX = cx + r * Math.cos(el.angle);
|
|
1762
|
-
const farY = cy + r * Math.sin(el.angle);
|
|
1763
|
-
const xs = [tipX, leftX, rightX, farX];
|
|
1764
|
-
const ys = [tipY, leftY, rightY, farY];
|
|
1765
|
-
let minX = Infinity;
|
|
1766
|
-
let minY = Infinity;
|
|
1767
|
-
let maxX = -Infinity;
|
|
1768
|
-
let maxY = -Infinity;
|
|
1769
|
-
for (let i = 0; i < xs.length; i++) {
|
|
1770
|
-
const px = xs[i];
|
|
1771
|
-
const py = ys[i];
|
|
1772
|
-
if (px !== void 0 && px < minX) minX = px;
|
|
1773
|
-
if (px !== void 0 && px > maxX) maxX = px;
|
|
1774
|
-
if (py !== void 0 && py < minY) minY = py;
|
|
1775
|
-
if (py !== void 0 && py > maxY) maxY = py;
|
|
1776
|
-
}
|
|
1777
|
-
return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
|
|
1778
|
-
}
|
|
1779
|
-
case "line": {
|
|
1780
|
-
const halfW = r / 12;
|
|
1781
|
-
const cos = Math.cos(el.angle);
|
|
1782
|
-
const sin = Math.sin(el.angle);
|
|
1783
|
-
const perpX = -sin * halfW;
|
|
1784
|
-
const perpY = cos * halfW;
|
|
1785
|
-
const x0 = cx + perpX;
|
|
1786
|
-
const y0 = cy + perpY;
|
|
1787
|
-
const x1 = cx + r * cos + perpX;
|
|
1788
|
-
const y1 = cy + r * sin + perpY;
|
|
1789
|
-
const x2 = cx + r * cos - perpX;
|
|
1790
|
-
const y2 = cy + r * sin - perpY;
|
|
1791
|
-
const x3 = cx - perpX;
|
|
1792
|
-
const y3 = cy - perpY;
|
|
1793
|
-
const minX = Math.min(x0, x1, x2, x3);
|
|
1794
|
-
const minY = Math.min(y0, y1, y2, y3);
|
|
1795
|
-
const maxX = Math.max(x0, x1, x2, x3);
|
|
1796
|
-
const maxY = Math.max(y0, y1, y2, y3);
|
|
1797
|
-
return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
|
|
1798
|
-
}
|
|
1861
|
+
update(id, newBounds) {
|
|
1862
|
+
this.remove(id);
|
|
1863
|
+
this.insert(id, newBounds);
|
|
1799
1864
|
}
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
}
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1865
|
+
query(rect) {
|
|
1866
|
+
const result = [];
|
|
1867
|
+
this.root.query(rect, result);
|
|
1868
|
+
return result;
|
|
1869
|
+
}
|
|
1870
|
+
queryPoint(point) {
|
|
1871
|
+
return this.query({ x: point.x, y: point.y, w: 0, h: 0 });
|
|
1872
|
+
}
|
|
1873
|
+
clear() {
|
|
1874
|
+
this.root = new QuadNode(this.worldBounds, 0);
|
|
1875
|
+
this._size = 0;
|
|
1876
|
+
}
|
|
1877
|
+
};
|
|
1811
1878
|
|
|
1812
1879
|
// src/elements/stroke-smoothing.ts
|
|
1813
1880
|
var MIN_PRESSURE_SCALE = 0.2;
|
|
@@ -3429,6 +3496,8 @@ var NoteEditor = class {
|
|
|
3429
3496
|
inputHandler = null;
|
|
3430
3497
|
pendingEditId = null;
|
|
3431
3498
|
onStopCallback = null;
|
|
3499
|
+
beginHistory = null;
|
|
3500
|
+
commitHistory = null;
|
|
3432
3501
|
toolbar;
|
|
3433
3502
|
placeholder;
|
|
3434
3503
|
constructor(options) {
|
|
@@ -3444,6 +3513,10 @@ var NoteEditor = class {
|
|
|
3444
3513
|
setOnStop(callback) {
|
|
3445
3514
|
this.onStopCallback = callback;
|
|
3446
3515
|
}
|
|
3516
|
+
setHistoryHooks(begin, commit) {
|
|
3517
|
+
this.beginHistory = begin;
|
|
3518
|
+
this.commitHistory = commit;
|
|
3519
|
+
}
|
|
3447
3520
|
startEditing(node, elementId, store) {
|
|
3448
3521
|
if (this.editingId === elementId) return;
|
|
3449
3522
|
if (this.editingId) {
|
|
@@ -3475,18 +3548,21 @@ var NoteEditor = class {
|
|
|
3475
3548
|
this.editingNode.removeAttribute("data-fn-empty");
|
|
3476
3549
|
const text = sanitizeNoteHtml(this.editingNode.innerHTML);
|
|
3477
3550
|
const current = store.getById(this.editingId);
|
|
3478
|
-
|
|
3479
|
-
store.update(this.editingId, { text });
|
|
3480
|
-
}
|
|
3551
|
+
const textChanged = !!current && (current.type === "note" || current.type === "text") && current.text !== text;
|
|
3481
3552
|
this.editingNode.contentEditable = "false";
|
|
3482
3553
|
Object.assign(this.editingNode.style, {
|
|
3483
3554
|
userSelect: "none",
|
|
3484
3555
|
cursor: "default"
|
|
3485
3556
|
});
|
|
3486
3557
|
this.toolbar?.hide();
|
|
3558
|
+
this.beginHistory?.();
|
|
3559
|
+
if (textChanged) {
|
|
3560
|
+
store.update(this.editingId, { text });
|
|
3561
|
+
}
|
|
3487
3562
|
if (this.editingId && this.onStopCallback) {
|
|
3488
3563
|
this.onStopCallback(this.editingId);
|
|
3489
3564
|
}
|
|
3565
|
+
this.commitHistory?.();
|
|
3490
3566
|
this.editingId = null;
|
|
3491
3567
|
this.editingNode = null;
|
|
3492
3568
|
this.blurHandler = null;
|
|
@@ -3559,26 +3635,6 @@ var NoteEditor = class {
|
|
|
3559
3635
|
}
|
|
3560
3636
|
};
|
|
3561
3637
|
|
|
3562
|
-
// src/elements/bounds.ts
|
|
3563
|
-
function getElementsBoundingBox(elements) {
|
|
3564
|
-
let minX = Infinity;
|
|
3565
|
-
let minY = Infinity;
|
|
3566
|
-
let maxX = -Infinity;
|
|
3567
|
-
let maxY = -Infinity;
|
|
3568
|
-
let found = false;
|
|
3569
|
-
for (const el of elements) {
|
|
3570
|
-
const b = getElementBounds(el);
|
|
3571
|
-
if (!b) continue;
|
|
3572
|
-
found = true;
|
|
3573
|
-
if (b.x < minX) minX = b.x;
|
|
3574
|
-
if (b.y < minY) minY = b.y;
|
|
3575
|
-
if (b.x + b.w > maxX) maxX = b.x + b.w;
|
|
3576
|
-
if (b.y + b.h > maxY) maxY = b.y + b.h;
|
|
3577
|
-
}
|
|
3578
|
-
if (!found) return null;
|
|
3579
|
-
return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
|
|
3580
|
-
}
|
|
3581
|
-
|
|
3582
3638
|
// src/tools/tool-manager.ts
|
|
3583
3639
|
var ToolManager = class {
|
|
3584
3640
|
tools = /* @__PURE__ */ new Map();
|
|
@@ -5099,7 +5155,103 @@ var MarginViewport = class {
|
|
|
5099
5155
|
}
|
|
5100
5156
|
};
|
|
5101
5157
|
|
|
5158
|
+
// src/elements/element-style.ts
|
|
5159
|
+
function styleToPatch(element, style) {
|
|
5160
|
+
const { color, fillColor, strokeWidth, opacity, fontSize } = style;
|
|
5161
|
+
switch (element.type) {
|
|
5162
|
+
case "stroke":
|
|
5163
|
+
return {
|
|
5164
|
+
...color !== void 0 ? { color } : {},
|
|
5165
|
+
...strokeWidth !== void 0 ? { width: strokeWidth } : {},
|
|
5166
|
+
...opacity !== void 0 ? { opacity } : {}
|
|
5167
|
+
};
|
|
5168
|
+
case "arrow":
|
|
5169
|
+
return {
|
|
5170
|
+
...color !== void 0 ? { color } : {},
|
|
5171
|
+
...strokeWidth !== void 0 ? { width: strokeWidth } : {}
|
|
5172
|
+
};
|
|
5173
|
+
case "shape":
|
|
5174
|
+
return {
|
|
5175
|
+
...color !== void 0 ? { strokeColor: color } : {},
|
|
5176
|
+
...fillColor !== void 0 ? { fillColor } : {},
|
|
5177
|
+
...strokeWidth !== void 0 ? { strokeWidth } : {}
|
|
5178
|
+
};
|
|
5179
|
+
case "text":
|
|
5180
|
+
return {
|
|
5181
|
+
...color !== void 0 ? { color } : {},
|
|
5182
|
+
...fontSize !== void 0 ? { fontSize } : {}
|
|
5183
|
+
};
|
|
5184
|
+
case "note":
|
|
5185
|
+
return {
|
|
5186
|
+
...color !== void 0 ? { textColor: color } : {},
|
|
5187
|
+
...fillColor !== void 0 ? { backgroundColor: fillColor } : {},
|
|
5188
|
+
...fontSize !== void 0 ? { fontSize } : {}
|
|
5189
|
+
};
|
|
5190
|
+
case "grid":
|
|
5191
|
+
return {
|
|
5192
|
+
...color !== void 0 ? { strokeColor: color } : {},
|
|
5193
|
+
...strokeWidth !== void 0 ? { strokeWidth } : {},
|
|
5194
|
+
...opacity !== void 0 ? { opacity } : {}
|
|
5195
|
+
};
|
|
5196
|
+
case "template":
|
|
5197
|
+
return {
|
|
5198
|
+
...color !== void 0 ? { strokeColor: color } : {},
|
|
5199
|
+
...fillColor !== void 0 ? { fillColor } : {},
|
|
5200
|
+
...strokeWidth !== void 0 ? { strokeWidth } : {},
|
|
5201
|
+
...opacity !== void 0 ? { opacity } : {}
|
|
5202
|
+
};
|
|
5203
|
+
default:
|
|
5204
|
+
return {};
|
|
5205
|
+
}
|
|
5206
|
+
}
|
|
5207
|
+
function getElementStyle(element) {
|
|
5208
|
+
switch (element.type) {
|
|
5209
|
+
case "stroke":
|
|
5210
|
+
return { color: element.color, strokeWidth: element.width, opacity: element.opacity };
|
|
5211
|
+
case "arrow":
|
|
5212
|
+
return { color: element.color, strokeWidth: element.width };
|
|
5213
|
+
case "shape":
|
|
5214
|
+
return {
|
|
5215
|
+
color: element.strokeColor,
|
|
5216
|
+
fillColor: element.fillColor,
|
|
5217
|
+
strokeWidth: element.strokeWidth
|
|
5218
|
+
};
|
|
5219
|
+
case "text":
|
|
5220
|
+
return { color: element.color, fontSize: element.fontSize };
|
|
5221
|
+
case "note":
|
|
5222
|
+
return {
|
|
5223
|
+
color: element.textColor,
|
|
5224
|
+
fillColor: element.backgroundColor,
|
|
5225
|
+
...element.fontSize !== void 0 ? { fontSize: element.fontSize } : {}
|
|
5226
|
+
};
|
|
5227
|
+
case "grid":
|
|
5228
|
+
return {
|
|
5229
|
+
color: element.strokeColor,
|
|
5230
|
+
strokeWidth: element.strokeWidth,
|
|
5231
|
+
opacity: element.opacity
|
|
5232
|
+
};
|
|
5233
|
+
case "template":
|
|
5234
|
+
return {
|
|
5235
|
+
color: element.strokeColor,
|
|
5236
|
+
fillColor: element.fillColor,
|
|
5237
|
+
strokeWidth: element.strokeWidth,
|
|
5238
|
+
opacity: element.opacity
|
|
5239
|
+
};
|
|
5240
|
+
default:
|
|
5241
|
+
return {};
|
|
5242
|
+
}
|
|
5243
|
+
}
|
|
5244
|
+
|
|
5102
5245
|
// src/canvas/viewport.ts
|
|
5246
|
+
var EMPTY_IDS = [];
|
|
5247
|
+
function noop() {
|
|
5248
|
+
}
|
|
5249
|
+
function sharedValue(values) {
|
|
5250
|
+
const present = values.filter((v) => v !== void 0);
|
|
5251
|
+
if (present.length === 0) return void 0;
|
|
5252
|
+
const first = present[0];
|
|
5253
|
+
return present.every((v) => v === first) ? first : void 0;
|
|
5254
|
+
}
|
|
5103
5255
|
var Viewport = class {
|
|
5104
5256
|
constructor(container, options = {}) {
|
|
5105
5257
|
this.container = container;
|
|
@@ -5133,6 +5285,10 @@ var Viewport = class {
|
|
|
5133
5285
|
placeholder: options.placeholder
|
|
5134
5286
|
});
|
|
5135
5287
|
this.noteEditor.setOnStop((id) => this.onTextEditStop(id));
|
|
5288
|
+
this.noteEditor.setHistoryHooks(
|
|
5289
|
+
() => this.historyRecorder.begin(),
|
|
5290
|
+
() => this.historyRecorder.commit()
|
|
5291
|
+
);
|
|
5136
5292
|
this.onHtmlElementMount = options.onHtmlElementMount;
|
|
5137
5293
|
this.dropHandler = options.onDrop;
|
|
5138
5294
|
this.history = new HistoryStack();
|
|
@@ -5149,6 +5305,7 @@ var Viewport = class {
|
|
|
5149
5305
|
requestRender: () => this.requestRender(),
|
|
5150
5306
|
switchTool: (name) => this.toolManager.setTool(name, this.toolContext),
|
|
5151
5307
|
editElement: (id) => this.startEditingElement(id),
|
|
5308
|
+
fitNoteHeight: (id) => this.fitNoteHeight(id),
|
|
5152
5309
|
setCursor: (cursor) => {
|
|
5153
5310
|
this.wrapper.style.cursor = cursor;
|
|
5154
5311
|
},
|
|
@@ -5446,6 +5603,52 @@ var Viewport = class {
|
|
|
5446
5603
|
this.gridChangeListeners.delete(listener);
|
|
5447
5604
|
};
|
|
5448
5605
|
}
|
|
5606
|
+
getSelectTool() {
|
|
5607
|
+
return this.toolManager.getTool("select");
|
|
5608
|
+
}
|
|
5609
|
+
getSelectedIds() {
|
|
5610
|
+
return this.getSelectTool()?.selectedIds ?? EMPTY_IDS;
|
|
5611
|
+
}
|
|
5612
|
+
onSelectionChange(listener) {
|
|
5613
|
+
const tool = this.getSelectTool();
|
|
5614
|
+
return tool ? tool.onSelectionChange(listener) : noop;
|
|
5615
|
+
}
|
|
5616
|
+
getSelectionStyle() {
|
|
5617
|
+
const ids = this.getSelectedIds();
|
|
5618
|
+
if (ids.length === 0) return null;
|
|
5619
|
+
const styles = [];
|
|
5620
|
+
for (const id of ids) {
|
|
5621
|
+
const el = this.store.getById(id);
|
|
5622
|
+
if (el) styles.push(getElementStyle(el));
|
|
5623
|
+
}
|
|
5624
|
+
if (styles.length === 0) return null;
|
|
5625
|
+
const result = {};
|
|
5626
|
+
const color = sharedValue(styles.map((s) => s.color));
|
|
5627
|
+
if (color !== void 0) result.color = color;
|
|
5628
|
+
const fillColor = sharedValue(styles.map((s) => s.fillColor));
|
|
5629
|
+
if (fillColor !== void 0) result.fillColor = fillColor;
|
|
5630
|
+
const strokeWidth = sharedValue(styles.map((s) => s.strokeWidth));
|
|
5631
|
+
if (strokeWidth !== void 0) result.strokeWidth = strokeWidth;
|
|
5632
|
+
const opacity = sharedValue(styles.map((s) => s.opacity));
|
|
5633
|
+
if (opacity !== void 0) result.opacity = opacity;
|
|
5634
|
+
const fontSize = sharedValue(styles.map((s) => s.fontSize));
|
|
5635
|
+
if (fontSize !== void 0) result.fontSize = fontSize;
|
|
5636
|
+
return result;
|
|
5637
|
+
}
|
|
5638
|
+
applyStyleToSelection(style) {
|
|
5639
|
+
const ids = this.getSelectedIds();
|
|
5640
|
+
if (ids.length === 0) return;
|
|
5641
|
+
this.historyRecorder.begin();
|
|
5642
|
+
for (const id of ids) {
|
|
5643
|
+
const el = this.store.getById(id);
|
|
5644
|
+
if (!el) continue;
|
|
5645
|
+
const patch = styleToPatch(el, style);
|
|
5646
|
+
if (Object.keys(patch).length > 0) {
|
|
5647
|
+
this.store.update(id, patch);
|
|
5648
|
+
}
|
|
5649
|
+
}
|
|
5650
|
+
this.historyRecorder.commit();
|
|
5651
|
+
}
|
|
5449
5652
|
getRenderStats() {
|
|
5450
5653
|
return this.renderLoop.getStats();
|
|
5451
5654
|
}
|
|
@@ -5483,31 +5686,38 @@ var Viewport = class {
|
|
|
5483
5686
|
this.noteEditor.startEditing(node, id, this.store);
|
|
5484
5687
|
}
|
|
5485
5688
|
}
|
|
5689
|
+
fitNoteHeight(elementId) {
|
|
5690
|
+
const element = this.store.getById(elementId);
|
|
5691
|
+
if (!element || element.type !== "note") return;
|
|
5692
|
+
if (isNoteContentEmpty(element.text)) return;
|
|
5693
|
+
const node = this.domNodeManager.getNode(elementId);
|
|
5694
|
+
if (!node) return;
|
|
5695
|
+
const measured = node.scrollHeight;
|
|
5696
|
+
if (measured > element.size.h) {
|
|
5697
|
+
this.store.update(elementId, { size: { w: element.size.w, h: measured } });
|
|
5698
|
+
}
|
|
5699
|
+
}
|
|
5486
5700
|
onTextEditStop(elementId) {
|
|
5487
5701
|
const element = this.store.getById(elementId);
|
|
5488
5702
|
if (!element) return;
|
|
5489
5703
|
if (element.type === "note") {
|
|
5490
5704
|
if (isNoteContentEmpty(element.text)) {
|
|
5491
|
-
this.historyRecorder.begin();
|
|
5492
5705
|
this.store.remove(elementId);
|
|
5493
|
-
|
|
5706
|
+
return;
|
|
5494
5707
|
}
|
|
5708
|
+
this.fitNoteHeight(elementId);
|
|
5495
5709
|
return;
|
|
5496
5710
|
}
|
|
5497
5711
|
if (element.type !== "text") return;
|
|
5498
5712
|
if (!element.text || element.text.trim() === "") {
|
|
5499
|
-
this.historyRecorder.begin();
|
|
5500
5713
|
this.store.remove(elementId);
|
|
5501
|
-
this.historyRecorder.commit();
|
|
5502
5714
|
return;
|
|
5503
5715
|
}
|
|
5504
5716
|
const node = this.domNodeManager.getNode(elementId);
|
|
5505
5717
|
if (node && "size" in element) {
|
|
5506
|
-
const
|
|
5507
|
-
if (
|
|
5508
|
-
this.store.update(elementId, {
|
|
5509
|
-
size: { w: element.size.w, h: measuredHeight }
|
|
5510
|
-
});
|
|
5718
|
+
const measured = node.scrollHeight;
|
|
5719
|
+
if (measured !== element.size.h) {
|
|
5720
|
+
this.store.update(elementId, { size: { w: element.size.w, h: measured } });
|
|
5511
5721
|
}
|
|
5512
5722
|
}
|
|
5513
5723
|
}
|
|
@@ -6054,6 +6264,7 @@ var HANDLE_CURSORS = {
|
|
|
6054
6264
|
var SelectTool = class {
|
|
6055
6265
|
name = "select";
|
|
6056
6266
|
_selectedIds = [];
|
|
6267
|
+
selectionListeners = /* @__PURE__ */ new Set();
|
|
6057
6268
|
mode = { type: "idle" };
|
|
6058
6269
|
lastWorld = { x: 0, y: 0 };
|
|
6059
6270
|
currentWorld = { x: 0, y: 0 };
|
|
@@ -6063,10 +6274,22 @@ var SelectTool = class {
|
|
|
6063
6274
|
resizeAspectRatio = 0;
|
|
6064
6275
|
hoveredId = null;
|
|
6065
6276
|
get selectedIds() {
|
|
6066
|
-
return
|
|
6277
|
+
return this._selectedIds;
|
|
6067
6278
|
}
|
|
6068
|
-
|
|
6279
|
+
onSelectionChange(listener) {
|
|
6280
|
+
this.selectionListeners.add(listener);
|
|
6281
|
+
return () => {
|
|
6282
|
+
this.selectionListeners.delete(listener);
|
|
6283
|
+
};
|
|
6284
|
+
}
|
|
6285
|
+
setSelectedIds(ids) {
|
|
6286
|
+
const prev = this._selectedIds;
|
|
6287
|
+
if (prev.length === ids.length && prev.every((id, i) => id === ids[i])) return;
|
|
6069
6288
|
this._selectedIds = ids;
|
|
6289
|
+
for (const listener of this.selectionListeners) listener();
|
|
6290
|
+
}
|
|
6291
|
+
setSelection(ids) {
|
|
6292
|
+
this.setSelectedIds(ids);
|
|
6070
6293
|
this.ctx?.requestRender();
|
|
6071
6294
|
}
|
|
6072
6295
|
get isMarqueeActive() {
|
|
@@ -6076,7 +6299,7 @@ var SelectTool = class {
|
|
|
6076
6299
|
this.ctx = ctx;
|
|
6077
6300
|
}
|
|
6078
6301
|
onDeactivate(ctx) {
|
|
6079
|
-
this.
|
|
6302
|
+
this.setSelectedIds([]);
|
|
6080
6303
|
this.mode = { type: "idle" };
|
|
6081
6304
|
this.hoveredId = null;
|
|
6082
6305
|
ctx.setCursor?.("default");
|
|
@@ -6127,22 +6350,22 @@ var SelectTool = class {
|
|
|
6127
6350
|
const alreadySelected = this._selectedIds.includes(hit.id);
|
|
6128
6351
|
if (state.shiftKey) {
|
|
6129
6352
|
if (alreadySelected) {
|
|
6130
|
-
this.
|
|
6353
|
+
this.setSelectedIds(this._selectedIds.filter((id) => id !== hit.id));
|
|
6131
6354
|
this.mode = { type: "idle" };
|
|
6132
6355
|
} else {
|
|
6133
|
-
this.
|
|
6356
|
+
this.setSelectedIds([...this._selectedIds, hit.id]);
|
|
6134
6357
|
this.mode = hit.locked ? { type: "idle" } : { type: "dragging" };
|
|
6135
6358
|
}
|
|
6136
6359
|
} else {
|
|
6137
6360
|
if (!alreadySelected) {
|
|
6138
|
-
this.
|
|
6361
|
+
this.setSelectedIds([hit.id]);
|
|
6139
6362
|
} else if (this._selectedIds.length > 1) {
|
|
6140
6363
|
this.pendingSingleSelectId = hit.id;
|
|
6141
6364
|
}
|
|
6142
6365
|
this.mode = hit.locked ? { type: "idle" } : { type: "dragging" };
|
|
6143
6366
|
}
|
|
6144
6367
|
} else {
|
|
6145
|
-
this.
|
|
6368
|
+
this.setSelectedIds([]);
|
|
6146
6369
|
this.mode = { type: "marquee", start: world };
|
|
6147
6370
|
}
|
|
6148
6371
|
ctx.requestRender();
|
|
@@ -6215,17 +6438,22 @@ var SelectTool = class {
|
|
|
6215
6438
|
if (this.mode.type === "marquee") {
|
|
6216
6439
|
const rect = this.getMarqueeRect();
|
|
6217
6440
|
if (rect) {
|
|
6218
|
-
this.
|
|
6441
|
+
this.setSelectedIds(this.findElementsInRect(rect, ctx));
|
|
6219
6442
|
}
|
|
6220
6443
|
ctx.requestRender();
|
|
6221
6444
|
}
|
|
6222
6445
|
if (!this.hasDragged && this.pendingSingleSelectId !== null) {
|
|
6223
|
-
this.
|
|
6446
|
+
this.setSelectedIds([this.pendingSingleSelectId]);
|
|
6224
6447
|
}
|
|
6225
6448
|
this.pendingSingleSelectId = null;
|
|
6226
6449
|
this.hasDragged = false;
|
|
6450
|
+
const resizedNoteId = this.mode.type === "resizing" ? this.mode.elementId : null;
|
|
6227
6451
|
this.mode = { type: "idle" };
|
|
6228
6452
|
ctx.setCursor?.("default");
|
|
6453
|
+
if (resizedNoteId !== null) {
|
|
6454
|
+
const el = ctx.store.getById(resizedNoteId);
|
|
6455
|
+
if (el?.type === "note") ctx.fitNoteHeight?.(resizedNoteId);
|
|
6456
|
+
}
|
|
6229
6457
|
}
|
|
6230
6458
|
onHover(state, ctx) {
|
|
6231
6459
|
const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
|
|
@@ -7437,7 +7665,7 @@ var TemplateTool = class {
|
|
|
7437
7665
|
};
|
|
7438
7666
|
|
|
7439
7667
|
// src/index.ts
|
|
7440
|
-
var VERSION = "0.
|
|
7668
|
+
var VERSION = "0.27.0";
|
|
7441
7669
|
export {
|
|
7442
7670
|
ArrowTool,
|
|
7443
7671
|
AutoSave,
|
|
@@ -7478,6 +7706,7 @@ export {
|
|
|
7478
7706
|
getArrowTangentAngle,
|
|
7479
7707
|
getBendFromPoint,
|
|
7480
7708
|
getElementBounds,
|
|
7709
|
+
getElementStyle,
|
|
7481
7710
|
getElementsBoundingBox,
|
|
7482
7711
|
getHexCellsInCone,
|
|
7483
7712
|
getHexCellsInLine,
|
|
@@ -7489,6 +7718,7 @@ export {
|
|
|
7489
7718
|
smartSnap,
|
|
7490
7719
|
snapPoint,
|
|
7491
7720
|
snapToHexCenter,
|
|
7721
|
+
styleToPatch,
|
|
7492
7722
|
toggleBold,
|
|
7493
7723
|
toggleItalic,
|
|
7494
7724
|
toggleStrikethrough,
|