@fieldnotes/core 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +420 -27
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +27 -2
- package/dist/index.d.ts +27 -2
- package/dist/index.js +410 -26
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -68,6 +68,7 @@ function validateState(data) {
|
|
|
68
68
|
validateElement(el);
|
|
69
69
|
migrateElement(el);
|
|
70
70
|
}
|
|
71
|
+
cleanBindings(obj["elements"]);
|
|
71
72
|
}
|
|
72
73
|
var VALID_TYPES = /* @__PURE__ */ new Set(["stroke", "note", "arrow", "image", "html", "text"]);
|
|
73
74
|
function validateElement(el) {
|
|
@@ -85,6 +86,20 @@ function validateElement(el) {
|
|
|
85
86
|
throw new Error("Invalid element: missing zIndex");
|
|
86
87
|
}
|
|
87
88
|
}
|
|
89
|
+
function cleanBindings(elements) {
|
|
90
|
+
const ids = new Set(elements.map((el) => el["id"]));
|
|
91
|
+
for (const el of elements) {
|
|
92
|
+
if (el["type"] !== "arrow") continue;
|
|
93
|
+
const fromBinding = el["fromBinding"];
|
|
94
|
+
if (fromBinding && !ids.has(fromBinding["elementId"])) {
|
|
95
|
+
el["fromBinding"] = void 0;
|
|
96
|
+
}
|
|
97
|
+
const toBinding = el["toBinding"];
|
|
98
|
+
if (toBinding && !ids.has(toBinding["elementId"])) {
|
|
99
|
+
el["toBinding"] = void 0;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
88
103
|
function migrateElement(obj) {
|
|
89
104
|
if (obj["type"] === "arrow" && typeof obj["bend"] !== "number") {
|
|
90
105
|
obj["bend"] = 0;
|
|
@@ -678,6 +693,134 @@ function isNearLine(point, a, b, threshold) {
|
|
|
678
693
|
return Math.hypot(point.x - projX, point.y - projY) <= threshold;
|
|
679
694
|
}
|
|
680
695
|
|
|
696
|
+
// src/elements/arrow-binding.ts
|
|
697
|
+
var BINDABLE_TYPES = /* @__PURE__ */ new Set(["note", "text", "image", "html"]);
|
|
698
|
+
function isBindable(element) {
|
|
699
|
+
return BINDABLE_TYPES.has(element.type);
|
|
700
|
+
}
|
|
701
|
+
function getElementCenter(element) {
|
|
702
|
+
if (!("size" in element)) {
|
|
703
|
+
throw new Error(`getElementCenter: element type "${element.type}" has no size`);
|
|
704
|
+
}
|
|
705
|
+
return {
|
|
706
|
+
x: element.position.x + element.size.w / 2,
|
|
707
|
+
y: element.position.y + element.size.h / 2
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
function getElementBounds(element) {
|
|
711
|
+
if (!("size" in element)) return null;
|
|
712
|
+
return {
|
|
713
|
+
x: element.position.x,
|
|
714
|
+
y: element.position.y,
|
|
715
|
+
w: element.size.w,
|
|
716
|
+
h: element.size.h
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
function getEdgeIntersection(bounds, outsidePoint) {
|
|
720
|
+
const cx = bounds.x + bounds.w / 2;
|
|
721
|
+
const cy = bounds.y + bounds.h / 2;
|
|
722
|
+
const dx = outsidePoint.x - cx;
|
|
723
|
+
const dy = outsidePoint.y - cy;
|
|
724
|
+
if (dx === 0 && dy === 0) return { x: cx, y: cy };
|
|
725
|
+
const halfW = bounds.w / 2;
|
|
726
|
+
const halfH = bounds.h / 2;
|
|
727
|
+
const scaleX = dx !== 0 ? halfW / Math.abs(dx) : Infinity;
|
|
728
|
+
const scaleY = dy !== 0 ? halfH / Math.abs(dy) : Infinity;
|
|
729
|
+
const scale = Math.min(scaleX, scaleY);
|
|
730
|
+
return {
|
|
731
|
+
x: cx + dx * scale,
|
|
732
|
+
y: cy + dy * scale
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
function findBindTarget(point, store, threshold, excludeId) {
|
|
736
|
+
let closest = null;
|
|
737
|
+
let closestDist = Infinity;
|
|
738
|
+
for (const el of store.getAll()) {
|
|
739
|
+
if (!isBindable(el)) continue;
|
|
740
|
+
if (excludeId && el.id === excludeId) continue;
|
|
741
|
+
const bounds = getElementBounds(el);
|
|
742
|
+
if (!bounds) continue;
|
|
743
|
+
const dist = distToBounds(point, bounds);
|
|
744
|
+
if (dist <= threshold && dist < closestDist) {
|
|
745
|
+
closest = el;
|
|
746
|
+
closestDist = dist;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
return closest;
|
|
750
|
+
}
|
|
751
|
+
function distToBounds(point, bounds) {
|
|
752
|
+
const clampedX = Math.max(bounds.x, Math.min(point.x, bounds.x + bounds.w));
|
|
753
|
+
const clampedY = Math.max(bounds.y, Math.min(point.y, bounds.y + bounds.h));
|
|
754
|
+
return Math.hypot(point.x - clampedX, point.y - clampedY);
|
|
755
|
+
}
|
|
756
|
+
function findBoundArrows(elementId, store) {
|
|
757
|
+
return store.getElementsByType("arrow").filter((a) => a.fromBinding?.elementId === elementId || a.toBinding?.elementId === elementId);
|
|
758
|
+
}
|
|
759
|
+
function updateBoundArrow(arrow, store) {
|
|
760
|
+
if (!arrow.fromBinding && !arrow.toBinding) return null;
|
|
761
|
+
const updates = {};
|
|
762
|
+
if (arrow.fromBinding) {
|
|
763
|
+
const el = store.getById(arrow.fromBinding.elementId);
|
|
764
|
+
if (el) {
|
|
765
|
+
const center = getElementCenter(el);
|
|
766
|
+
updates.from = center;
|
|
767
|
+
updates.position = center;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
if (arrow.toBinding) {
|
|
771
|
+
const el = store.getById(arrow.toBinding.elementId);
|
|
772
|
+
if (el) {
|
|
773
|
+
updates.to = getElementCenter(el);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
return Object.keys(updates).length > 0 ? updates : null;
|
|
777
|
+
}
|
|
778
|
+
function clearStaleBindings(arrow, store) {
|
|
779
|
+
const updates = {};
|
|
780
|
+
let hasUpdates = false;
|
|
781
|
+
if (arrow.fromBinding && !store.getById(arrow.fromBinding.elementId)) {
|
|
782
|
+
updates.fromBinding = void 0;
|
|
783
|
+
hasUpdates = true;
|
|
784
|
+
}
|
|
785
|
+
if (arrow.toBinding && !store.getById(arrow.toBinding.elementId)) {
|
|
786
|
+
updates.toBinding = void 0;
|
|
787
|
+
hasUpdates = true;
|
|
788
|
+
}
|
|
789
|
+
return hasUpdates ? updates : null;
|
|
790
|
+
}
|
|
791
|
+
function unbindArrow(arrow, store) {
|
|
792
|
+
const updates = {};
|
|
793
|
+
if (arrow.fromBinding) {
|
|
794
|
+
const el = store.getById(arrow.fromBinding.elementId);
|
|
795
|
+
const bounds = el ? getElementBounds(el) : null;
|
|
796
|
+
if (bounds) {
|
|
797
|
+
const angle = getArrowTangentAngle(arrow.from, arrow.to, arrow.bend, 0);
|
|
798
|
+
const rayTarget = {
|
|
799
|
+
x: arrow.from.x + Math.cos(angle) * 1e3,
|
|
800
|
+
y: arrow.from.y + Math.sin(angle) * 1e3
|
|
801
|
+
};
|
|
802
|
+
const edge = getEdgeIntersection(bounds, rayTarget);
|
|
803
|
+
updates.from = edge;
|
|
804
|
+
updates.position = edge;
|
|
805
|
+
}
|
|
806
|
+
updates.fromBinding = void 0;
|
|
807
|
+
}
|
|
808
|
+
if (arrow.toBinding) {
|
|
809
|
+
const el = store.getById(arrow.toBinding.elementId);
|
|
810
|
+
const bounds = el ? getElementBounds(el) : null;
|
|
811
|
+
if (bounds) {
|
|
812
|
+
const angle = getArrowTangentAngle(arrow.from, arrow.to, arrow.bend, 1);
|
|
813
|
+
const rayTarget = {
|
|
814
|
+
x: arrow.to.x - Math.cos(angle) * 1e3,
|
|
815
|
+
y: arrow.to.y - Math.sin(angle) * 1e3
|
|
816
|
+
};
|
|
817
|
+
updates.to = getEdgeIntersection(bounds, rayTarget);
|
|
818
|
+
}
|
|
819
|
+
updates.toBinding = void 0;
|
|
820
|
+
}
|
|
821
|
+
return updates;
|
|
822
|
+
}
|
|
823
|
+
|
|
681
824
|
// src/elements/stroke-smoothing.ts
|
|
682
825
|
var MIN_PRESSURE_SCALE = 0.2;
|
|
683
826
|
function pressureToWidth(pressure, baseWidth) {
|
|
@@ -756,6 +899,10 @@ var DOM_ELEMENT_TYPES = /* @__PURE__ */ new Set(["note", "image", "html", "text"
|
|
|
756
899
|
var ARROWHEAD_LENGTH = 12;
|
|
757
900
|
var ARROWHEAD_ANGLE = Math.PI / 6;
|
|
758
901
|
var ElementRenderer = class {
|
|
902
|
+
store = null;
|
|
903
|
+
setStore(store) {
|
|
904
|
+
this.store = store;
|
|
905
|
+
}
|
|
759
906
|
isDomElement(element) {
|
|
760
907
|
return DOM_ELEMENT_TYPES.has(element.type);
|
|
761
908
|
}
|
|
@@ -789,38 +936,76 @@ var ElementRenderer = class {
|
|
|
789
936
|
ctx.restore();
|
|
790
937
|
}
|
|
791
938
|
renderArrow(ctx, arrow) {
|
|
939
|
+
const { visualFrom, visualTo } = this.getVisualEndpoints(arrow);
|
|
792
940
|
ctx.save();
|
|
793
941
|
ctx.strokeStyle = arrow.color;
|
|
794
942
|
ctx.lineWidth = arrow.width;
|
|
795
943
|
ctx.lineCap = "round";
|
|
944
|
+
if (arrow.fromBinding || arrow.toBinding) {
|
|
945
|
+
ctx.setLineDash([8, 4]);
|
|
946
|
+
}
|
|
796
947
|
ctx.beginPath();
|
|
797
|
-
ctx.moveTo(
|
|
948
|
+
ctx.moveTo(visualFrom.x, visualFrom.y);
|
|
798
949
|
if (arrow.bend !== 0) {
|
|
799
950
|
const cp = getArrowControlPoint(arrow.from, arrow.to, arrow.bend);
|
|
800
|
-
ctx.quadraticCurveTo(cp.x, cp.y,
|
|
951
|
+
ctx.quadraticCurveTo(cp.x, cp.y, visualTo.x, visualTo.y);
|
|
801
952
|
} else {
|
|
802
|
-
ctx.lineTo(
|
|
953
|
+
ctx.lineTo(visualTo.x, visualTo.y);
|
|
803
954
|
}
|
|
804
955
|
ctx.stroke();
|
|
805
|
-
this.renderArrowhead(ctx, arrow);
|
|
956
|
+
this.renderArrowhead(ctx, arrow, visualTo);
|
|
806
957
|
ctx.restore();
|
|
807
958
|
}
|
|
808
|
-
renderArrowhead(ctx, arrow) {
|
|
959
|
+
renderArrowhead(ctx, arrow, tip) {
|
|
809
960
|
const angle = getArrowTangentAngle(arrow.from, arrow.to, arrow.bend, 1);
|
|
810
961
|
ctx.beginPath();
|
|
811
|
-
ctx.moveTo(
|
|
962
|
+
ctx.moveTo(tip.x, tip.y);
|
|
812
963
|
ctx.lineTo(
|
|
813
|
-
|
|
814
|
-
|
|
964
|
+
tip.x - ARROWHEAD_LENGTH * Math.cos(angle - ARROWHEAD_ANGLE),
|
|
965
|
+
tip.y - ARROWHEAD_LENGTH * Math.sin(angle - ARROWHEAD_ANGLE)
|
|
815
966
|
);
|
|
816
967
|
ctx.lineTo(
|
|
817
|
-
|
|
818
|
-
|
|
968
|
+
tip.x - ARROWHEAD_LENGTH * Math.cos(angle + ARROWHEAD_ANGLE),
|
|
969
|
+
tip.y - ARROWHEAD_LENGTH * Math.sin(angle + ARROWHEAD_ANGLE)
|
|
819
970
|
);
|
|
820
971
|
ctx.closePath();
|
|
821
972
|
ctx.fillStyle = arrow.color;
|
|
822
973
|
ctx.fill();
|
|
823
974
|
}
|
|
975
|
+
getVisualEndpoints(arrow) {
|
|
976
|
+
let visualFrom = arrow.from;
|
|
977
|
+
let visualTo = arrow.to;
|
|
978
|
+
if (!this.store) return { visualFrom, visualTo };
|
|
979
|
+
if (arrow.fromBinding) {
|
|
980
|
+
const el = this.store.getById(arrow.fromBinding.elementId);
|
|
981
|
+
if (el) {
|
|
982
|
+
const bounds = getElementBounds(el);
|
|
983
|
+
if (bounds) {
|
|
984
|
+
const tangentAngle = getArrowTangentAngle(arrow.from, arrow.to, arrow.bend, 0);
|
|
985
|
+
const rayTarget = {
|
|
986
|
+
x: arrow.from.x + Math.cos(tangentAngle) * 1e3,
|
|
987
|
+
y: arrow.from.y + Math.sin(tangentAngle) * 1e3
|
|
988
|
+
};
|
|
989
|
+
visualFrom = getEdgeIntersection(bounds, rayTarget);
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
if (arrow.toBinding) {
|
|
994
|
+
const el = this.store.getById(arrow.toBinding.elementId);
|
|
995
|
+
if (el) {
|
|
996
|
+
const bounds = getElementBounds(el);
|
|
997
|
+
if (bounds) {
|
|
998
|
+
const tangentAngle = getArrowTangentAngle(arrow.from, arrow.to, arrow.bend, 1);
|
|
999
|
+
const rayTarget = {
|
|
1000
|
+
x: arrow.to.x - Math.cos(tangentAngle) * 1e3,
|
|
1001
|
+
y: arrow.to.y - Math.sin(tangentAngle) * 1e3
|
|
1002
|
+
};
|
|
1003
|
+
visualTo = getEdgeIntersection(bounds, rayTarget);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
return { visualFrom, visualTo };
|
|
1008
|
+
}
|
|
824
1009
|
};
|
|
825
1010
|
|
|
826
1011
|
// src/elements/note-editor.ts
|
|
@@ -1191,7 +1376,7 @@ function createNote(input) {
|
|
|
1191
1376
|
};
|
|
1192
1377
|
}
|
|
1193
1378
|
function createArrow(input) {
|
|
1194
|
-
|
|
1379
|
+
const result = {
|
|
1195
1380
|
id: createId("arrow"),
|
|
1196
1381
|
type: "arrow",
|
|
1197
1382
|
position: input.position ?? { x: 0, y: 0 },
|
|
@@ -1203,6 +1388,9 @@ function createArrow(input) {
|
|
|
1203
1388
|
color: input.color ?? "#000000",
|
|
1204
1389
|
width: input.width ?? 2
|
|
1205
1390
|
};
|
|
1391
|
+
if (input.fromBinding) result.fromBinding = input.fromBinding;
|
|
1392
|
+
if (input.toBinding) result.toBinding = input.toBinding;
|
|
1393
|
+
return result;
|
|
1206
1394
|
}
|
|
1207
1395
|
function createImage(input) {
|
|
1208
1396
|
return {
|
|
@@ -1249,6 +1437,7 @@ var Viewport = class {
|
|
|
1249
1437
|
this.store = new ElementStore();
|
|
1250
1438
|
this.toolManager = new ToolManager();
|
|
1251
1439
|
this.renderer = new ElementRenderer();
|
|
1440
|
+
this.renderer.setStore(this.store);
|
|
1252
1441
|
this.noteEditor = new NoteEditor();
|
|
1253
1442
|
this.noteEditor.setOnStop((id) => this.onTextEditStop(id));
|
|
1254
1443
|
this.history = new HistoryStack();
|
|
@@ -1281,7 +1470,10 @@ var Viewport = class {
|
|
|
1281
1470
|
});
|
|
1282
1471
|
this.unsubStore = [
|
|
1283
1472
|
this.store.on("add", () => this.requestRender()),
|
|
1284
|
-
this.store.on("remove", (el) =>
|
|
1473
|
+
this.store.on("remove", (el) => {
|
|
1474
|
+
this.unbindArrowsFrom(el);
|
|
1475
|
+
this.removeDomNode(el.id);
|
|
1476
|
+
}),
|
|
1285
1477
|
this.store.on("update", () => this.requestRender()),
|
|
1286
1478
|
this.store.on("clear", () => this.clearDomNodes())
|
|
1287
1479
|
];
|
|
@@ -1656,6 +1848,40 @@ var Viewport = class {
|
|
|
1656
1848
|
}
|
|
1657
1849
|
}
|
|
1658
1850
|
}
|
|
1851
|
+
unbindArrowsFrom(removedElement) {
|
|
1852
|
+
const boundArrows = findBoundArrows(removedElement.id, this.store);
|
|
1853
|
+
const bounds = getElementBounds(removedElement);
|
|
1854
|
+
for (const arrow of boundArrows) {
|
|
1855
|
+
const updates = {};
|
|
1856
|
+
if (arrow.fromBinding?.elementId === removedElement.id) {
|
|
1857
|
+
updates.fromBinding = void 0;
|
|
1858
|
+
if (bounds) {
|
|
1859
|
+
const angle = getArrowTangentAngle(arrow.from, arrow.to, arrow.bend, 0);
|
|
1860
|
+
const rayTarget = {
|
|
1861
|
+
x: arrow.from.x + Math.cos(angle) * 1e3,
|
|
1862
|
+
y: arrow.from.y + Math.sin(angle) * 1e3
|
|
1863
|
+
};
|
|
1864
|
+
const edge = getEdgeIntersection(bounds, rayTarget);
|
|
1865
|
+
updates.from = edge;
|
|
1866
|
+
updates.position = edge;
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
if (arrow.toBinding?.elementId === removedElement.id) {
|
|
1870
|
+
updates.toBinding = void 0;
|
|
1871
|
+
if (bounds) {
|
|
1872
|
+
const angle = getArrowTangentAngle(arrow.from, arrow.to, arrow.bend, 1);
|
|
1873
|
+
const rayTarget = {
|
|
1874
|
+
x: arrow.to.x - Math.cos(angle) * 1e3,
|
|
1875
|
+
y: arrow.to.y - Math.sin(angle) * 1e3
|
|
1876
|
+
};
|
|
1877
|
+
updates.to = getEdgeIntersection(bounds, rayTarget);
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
if (Object.keys(updates).length > 0) {
|
|
1881
|
+
this.store.update(arrow.id, updates);
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1659
1885
|
removeDomNode(id) {
|
|
1660
1886
|
this.htmlContent.delete(id);
|
|
1661
1887
|
const node = this.domNodes.get(id);
|
|
@@ -1884,6 +2110,7 @@ var EraserTool = class {
|
|
|
1884
2110
|
};
|
|
1885
2111
|
|
|
1886
2112
|
// src/tools/arrow-handles.ts
|
|
2113
|
+
var BIND_THRESHOLD = 20;
|
|
1887
2114
|
var HANDLE_RADIUS = 5;
|
|
1888
2115
|
var HANDLE_HIT_PADDING = 4;
|
|
1889
2116
|
var ARROW_HANDLE_CURSORS = {
|
|
@@ -1924,18 +2151,44 @@ function hitTestArrowHandles(world, selectedIds, ctx) {
|
|
|
1924
2151
|
function applyArrowHandleDrag(handle, elementId, world, ctx) {
|
|
1925
2152
|
const el = ctx.store.getById(elementId);
|
|
1926
2153
|
if (!el || el.type !== "arrow") return;
|
|
2154
|
+
const threshold = BIND_THRESHOLD / ctx.camera.zoom;
|
|
1927
2155
|
switch (handle) {
|
|
1928
|
-
case "start":
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
2156
|
+
case "start": {
|
|
2157
|
+
const excludeId = el.toBinding?.elementId;
|
|
2158
|
+
const target = findBindTarget(world, ctx.store, threshold, excludeId);
|
|
2159
|
+
if (target) {
|
|
2160
|
+
const center = getElementCenter(target);
|
|
2161
|
+
ctx.store.update(elementId, {
|
|
2162
|
+
from: center,
|
|
2163
|
+
position: center,
|
|
2164
|
+
fromBinding: { elementId: target.id }
|
|
2165
|
+
});
|
|
2166
|
+
} else {
|
|
2167
|
+
ctx.store.update(elementId, {
|
|
2168
|
+
from: { x: world.x, y: world.y },
|
|
2169
|
+
position: { x: world.x, y: world.y },
|
|
2170
|
+
fromBinding: void 0
|
|
2171
|
+
});
|
|
2172
|
+
}
|
|
1933
2173
|
break;
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
2174
|
+
}
|
|
2175
|
+
case "end": {
|
|
2176
|
+
const excludeId = el.fromBinding?.elementId;
|
|
2177
|
+
const target = findBindTarget(world, ctx.store, threshold, excludeId);
|
|
2178
|
+
if (target) {
|
|
2179
|
+
const center = getElementCenter(target);
|
|
2180
|
+
ctx.store.update(elementId, {
|
|
2181
|
+
to: center,
|
|
2182
|
+
toBinding: { elementId: target.id }
|
|
2183
|
+
});
|
|
2184
|
+
} else {
|
|
2185
|
+
ctx.store.update(elementId, {
|
|
2186
|
+
to: { x: world.x, y: world.y },
|
|
2187
|
+
toBinding: void 0
|
|
2188
|
+
});
|
|
2189
|
+
}
|
|
1938
2190
|
break;
|
|
2191
|
+
}
|
|
1939
2192
|
case "mid": {
|
|
1940
2193
|
const bend = getBendFromPoint(el.from, el.to, world);
|
|
1941
2194
|
ctx.store.update(elementId, { bend });
|
|
@@ -1944,6 +2197,16 @@ function applyArrowHandleDrag(handle, elementId, world, ctx) {
|
|
|
1944
2197
|
}
|
|
1945
2198
|
ctx.requestRender();
|
|
1946
2199
|
}
|
|
2200
|
+
function getArrowHandleDragTarget(handle, elementId, world, ctx) {
|
|
2201
|
+
if (handle === "mid") return null;
|
|
2202
|
+
const el = ctx.store.getById(elementId);
|
|
2203
|
+
if (!el || el.type !== "arrow") return null;
|
|
2204
|
+
const threshold = BIND_THRESHOLD / ctx.camera.zoom;
|
|
2205
|
+
const excludeId = handle === "start" ? el.toBinding?.elementId : el.fromBinding?.elementId;
|
|
2206
|
+
const target = findBindTarget(world, ctx.store, threshold, excludeId);
|
|
2207
|
+
if (!target) return null;
|
|
2208
|
+
return getElementBounds(target);
|
|
2209
|
+
}
|
|
1947
2210
|
function renderArrowHandles(canvasCtx, arrow, zoom) {
|
|
1948
2211
|
const radius = HANDLE_RADIUS / zoom;
|
|
1949
2212
|
const handles = getArrowHandlePositions(arrow);
|
|
@@ -2054,6 +2317,9 @@ var SelectTool = class {
|
|
|
2054
2317
|
const el = ctx.store.getById(id);
|
|
2055
2318
|
if (!el || el.locked) continue;
|
|
2056
2319
|
if (el.type === "arrow") {
|
|
2320
|
+
if (el.fromBinding || el.toBinding) {
|
|
2321
|
+
continue;
|
|
2322
|
+
}
|
|
2057
2323
|
ctx.store.update(id, {
|
|
2058
2324
|
position: { x: el.position.x + dx, y: el.position.y + dy },
|
|
2059
2325
|
from: { x: el.from.x + dx, y: el.from.y + dy },
|
|
@@ -2065,6 +2331,23 @@ var SelectTool = class {
|
|
|
2065
2331
|
});
|
|
2066
2332
|
}
|
|
2067
2333
|
}
|
|
2334
|
+
const movedNonArrowIds = /* @__PURE__ */ new Set();
|
|
2335
|
+
for (const id of this._selectedIds) {
|
|
2336
|
+
const el = ctx.store.getById(id);
|
|
2337
|
+
if (el && el.type !== "arrow") movedNonArrowIds.add(id);
|
|
2338
|
+
}
|
|
2339
|
+
if (movedNonArrowIds.size > 0) {
|
|
2340
|
+
const updatedArrows = /* @__PURE__ */ new Set();
|
|
2341
|
+
for (const id of movedNonArrowIds) {
|
|
2342
|
+
const boundArrows = findBoundArrows(id, ctx.store);
|
|
2343
|
+
for (const ba of boundArrows) {
|
|
2344
|
+
if (updatedArrows.has(ba.id)) continue;
|
|
2345
|
+
updatedArrows.add(ba.id);
|
|
2346
|
+
const updates = updateBoundArrow(ba, ctx.store);
|
|
2347
|
+
if (updates) ctx.store.update(ba.id, updates);
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2068
2351
|
ctx.requestRender();
|
|
2069
2352
|
return;
|
|
2070
2353
|
}
|
|
@@ -2093,6 +2376,22 @@ var SelectTool = class {
|
|
|
2093
2376
|
renderOverlay(canvasCtx) {
|
|
2094
2377
|
this.renderMarquee(canvasCtx);
|
|
2095
2378
|
this.renderSelectionBoxes(canvasCtx);
|
|
2379
|
+
if (this.mode.type === "arrow-handle" && this.ctx) {
|
|
2380
|
+
const target = getArrowHandleDragTarget(
|
|
2381
|
+
this.mode.handle,
|
|
2382
|
+
this.mode.elementId,
|
|
2383
|
+
this.currentWorld,
|
|
2384
|
+
this.ctx
|
|
2385
|
+
);
|
|
2386
|
+
if (target) {
|
|
2387
|
+
canvasCtx.save();
|
|
2388
|
+
canvasCtx.strokeStyle = "#2196F3";
|
|
2389
|
+
canvasCtx.lineWidth = 2 / this.ctx.camera.zoom;
|
|
2390
|
+
canvasCtx.setLineDash([]);
|
|
2391
|
+
canvasCtx.strokeRect(target.x, target.y, target.w, target.h);
|
|
2392
|
+
canvasCtx.restore();
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2096
2395
|
}
|
|
2097
2396
|
updateHoverCursor(world, ctx) {
|
|
2098
2397
|
const arrowHit = hitTestArrowHandles(world, this._selectedIds, ctx);
|
|
@@ -2151,6 +2450,11 @@ var SelectTool = class {
|
|
|
2151
2450
|
position: { x, y },
|
|
2152
2451
|
size: { w, h }
|
|
2153
2452
|
});
|
|
2453
|
+
const boundArrows = findBoundArrows(this.mode.elementId, ctx.store);
|
|
2454
|
+
for (const ba of boundArrows) {
|
|
2455
|
+
const updates = updateBoundArrow(ba, ctx.store);
|
|
2456
|
+
if (updates) ctx.store.update(ba.id, updates);
|
|
2457
|
+
}
|
|
2154
2458
|
ctx.requestRender();
|
|
2155
2459
|
}
|
|
2156
2460
|
hitTestResizeHandle(world, ctx) {
|
|
@@ -2205,6 +2509,7 @@ var SelectTool = class {
|
|
|
2205
2509
|
if (!el) continue;
|
|
2206
2510
|
if (el.type === "arrow") {
|
|
2207
2511
|
renderArrowHandles(canvasCtx, el, zoom);
|
|
2512
|
+
this.renderBindingHighlights(canvasCtx, el, zoom);
|
|
2208
2513
|
continue;
|
|
2209
2514
|
}
|
|
2210
2515
|
const bounds = this.getElementBounds(el);
|
|
@@ -2234,6 +2539,26 @@ var SelectTool = class {
|
|
|
2234
2539
|
}
|
|
2235
2540
|
canvasCtx.restore();
|
|
2236
2541
|
}
|
|
2542
|
+
renderBindingHighlights(canvasCtx, arrow, zoom) {
|
|
2543
|
+
if (!this.ctx) return;
|
|
2544
|
+
if (!arrow.fromBinding && !arrow.toBinding) return;
|
|
2545
|
+
const pad = SELECTION_PAD / zoom;
|
|
2546
|
+
canvasCtx.save();
|
|
2547
|
+
canvasCtx.strokeStyle = "#2196F3";
|
|
2548
|
+
canvasCtx.lineWidth = 2 / zoom;
|
|
2549
|
+
canvasCtx.setLineDash([]);
|
|
2550
|
+
const drawn = /* @__PURE__ */ new Set();
|
|
2551
|
+
for (const binding of [arrow.fromBinding, arrow.toBinding]) {
|
|
2552
|
+
if (!binding || drawn.has(binding.elementId)) continue;
|
|
2553
|
+
drawn.add(binding.elementId);
|
|
2554
|
+
const target = this.ctx.store.getById(binding.elementId);
|
|
2555
|
+
if (!target) continue;
|
|
2556
|
+
const bounds = getElementBounds(target);
|
|
2557
|
+
if (!bounds) continue;
|
|
2558
|
+
canvasCtx.strokeRect(bounds.x - pad, bounds.y - pad, bounds.w + pad * 2, bounds.h + pad * 2);
|
|
2559
|
+
}
|
|
2560
|
+
canvasCtx.restore();
|
|
2561
|
+
}
|
|
2237
2562
|
getMarqueeRect() {
|
|
2238
2563
|
if (this.mode.type !== "marquee") return null;
|
|
2239
2564
|
const { start } = this.mode;
|
|
@@ -2307,6 +2632,7 @@ var SelectTool = class {
|
|
|
2307
2632
|
};
|
|
2308
2633
|
|
|
2309
2634
|
// src/tools/arrow-tool.ts
|
|
2635
|
+
var BIND_THRESHOLD2 = 20;
|
|
2310
2636
|
var ArrowTool = class {
|
|
2311
2637
|
name = "arrow";
|
|
2312
2638
|
drawing = false;
|
|
@@ -2314,6 +2640,9 @@ var ArrowTool = class {
|
|
|
2314
2640
|
end = { x: 0, y: 0 };
|
|
2315
2641
|
color;
|
|
2316
2642
|
width;
|
|
2643
|
+
fromBinding;
|
|
2644
|
+
fromTarget = null;
|
|
2645
|
+
toTarget = null;
|
|
2317
2646
|
constructor(options = {}) {
|
|
2318
2647
|
this.color = options.color ?? "#000000";
|
|
2319
2648
|
this.width = options.width ?? 2;
|
|
@@ -2324,12 +2653,34 @@ var ArrowTool = class {
|
|
|
2324
2653
|
}
|
|
2325
2654
|
onPointerDown(state, ctx) {
|
|
2326
2655
|
this.drawing = true;
|
|
2327
|
-
|
|
2656
|
+
const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
|
|
2657
|
+
const threshold = BIND_THRESHOLD2 / ctx.camera.zoom;
|
|
2658
|
+
const target = findBindTarget(world, ctx.store, threshold);
|
|
2659
|
+
if (target) {
|
|
2660
|
+
this.start = getElementCenter(target);
|
|
2661
|
+
this.fromBinding = { elementId: target.id };
|
|
2662
|
+
this.fromTarget = target;
|
|
2663
|
+
} else {
|
|
2664
|
+
this.start = world;
|
|
2665
|
+
this.fromBinding = void 0;
|
|
2666
|
+
this.fromTarget = null;
|
|
2667
|
+
}
|
|
2328
2668
|
this.end = { ...this.start };
|
|
2669
|
+
this.toTarget = null;
|
|
2329
2670
|
}
|
|
2330
2671
|
onPointerMove(state, ctx) {
|
|
2331
2672
|
if (!this.drawing) return;
|
|
2332
|
-
|
|
2673
|
+
const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
|
|
2674
|
+
const threshold = BIND_THRESHOLD2 / ctx.camera.zoom;
|
|
2675
|
+
const excludeId = this.fromBinding?.elementId;
|
|
2676
|
+
const target = findBindTarget(world, ctx.store, threshold, excludeId);
|
|
2677
|
+
if (target) {
|
|
2678
|
+
this.end = getElementCenter(target);
|
|
2679
|
+
this.toTarget = target;
|
|
2680
|
+
} else {
|
|
2681
|
+
this.end = world;
|
|
2682
|
+
this.toTarget = null;
|
|
2683
|
+
}
|
|
2333
2684
|
ctx.requestRender();
|
|
2334
2685
|
}
|
|
2335
2686
|
onPointerUp(_state, ctx) {
|
|
@@ -2339,16 +2690,40 @@ var ArrowTool = class {
|
|
|
2339
2690
|
const arrow = createArrow({
|
|
2340
2691
|
from: this.start,
|
|
2341
2692
|
to: this.end,
|
|
2693
|
+
position: this.start,
|
|
2342
2694
|
color: this.color,
|
|
2343
|
-
width: this.width
|
|
2695
|
+
width: this.width,
|
|
2696
|
+
fromBinding: this.fromBinding,
|
|
2697
|
+
toBinding: this.toTarget ? { elementId: this.toTarget.id } : void 0
|
|
2344
2698
|
});
|
|
2345
2699
|
ctx.store.add(arrow);
|
|
2700
|
+
this.fromTarget = null;
|
|
2701
|
+
this.toTarget = null;
|
|
2346
2702
|
ctx.requestRender();
|
|
2703
|
+
ctx.switchTool?.("select");
|
|
2347
2704
|
}
|
|
2348
2705
|
renderOverlay(ctx) {
|
|
2349
2706
|
if (!this.drawing) return;
|
|
2350
2707
|
if (this.start.x === this.end.x && this.start.y === this.end.y) return;
|
|
2351
2708
|
ctx.save();
|
|
2709
|
+
if (this.fromTarget) {
|
|
2710
|
+
const bounds = getElementBounds(this.fromTarget);
|
|
2711
|
+
if (bounds) {
|
|
2712
|
+
ctx.strokeStyle = "#2196F3";
|
|
2713
|
+
ctx.lineWidth = 2;
|
|
2714
|
+
ctx.setLineDash([]);
|
|
2715
|
+
ctx.strokeRect(bounds.x, bounds.y, bounds.w, bounds.h);
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2718
|
+
if (this.toTarget) {
|
|
2719
|
+
const bounds = getElementBounds(this.toTarget);
|
|
2720
|
+
if (bounds) {
|
|
2721
|
+
ctx.strokeStyle = "#2196F3";
|
|
2722
|
+
ctx.lineWidth = 2;
|
|
2723
|
+
ctx.setLineDash([]);
|
|
2724
|
+
ctx.strokeRect(bounds.x, bounds.y, bounds.w, bounds.h);
|
|
2725
|
+
}
|
|
2726
|
+
}
|
|
2352
2727
|
ctx.strokeStyle = this.color;
|
|
2353
2728
|
ctx.lineWidth = this.width;
|
|
2354
2729
|
ctx.lineCap = "round";
|
|
@@ -2480,7 +2855,7 @@ var ImageTool = class {
|
|
|
2480
2855
|
};
|
|
2481
2856
|
|
|
2482
2857
|
// src/index.ts
|
|
2483
|
-
var VERSION = "0.3.
|
|
2858
|
+
var VERSION = "0.3.1";
|
|
2484
2859
|
export {
|
|
2485
2860
|
AddElementCommand,
|
|
2486
2861
|
ArrowTool,
|
|
@@ -2507,6 +2882,7 @@ export {
|
|
|
2507
2882
|
UpdateElementCommand,
|
|
2508
2883
|
VERSION,
|
|
2509
2884
|
Viewport,
|
|
2885
|
+
clearStaleBindings,
|
|
2510
2886
|
createArrow,
|
|
2511
2887
|
createHtmlElement,
|
|
2512
2888
|
createId,
|
|
@@ -2515,12 +2891,20 @@ export {
|
|
|
2515
2891
|
createStroke,
|
|
2516
2892
|
createText,
|
|
2517
2893
|
exportState,
|
|
2894
|
+
findBindTarget,
|
|
2895
|
+
findBoundArrows,
|
|
2518
2896
|
getArrowBounds,
|
|
2519
2897
|
getArrowControlPoint,
|
|
2520
2898
|
getArrowMidpoint,
|
|
2521
2899
|
getArrowTangentAngle,
|
|
2522
2900
|
getBendFromPoint,
|
|
2901
|
+
getEdgeIntersection,
|
|
2902
|
+
getElementBounds,
|
|
2903
|
+
getElementCenter,
|
|
2904
|
+
isBindable,
|
|
2523
2905
|
isNearBezier,
|
|
2524
|
-
parseState
|
|
2906
|
+
parseState,
|
|
2907
|
+
unbindArrow,
|
|
2908
|
+
updateBoundArrow
|
|
2525
2909
|
};
|
|
2526
2910
|
//# sourceMappingURL=index.js.map
|