@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.cjs
CHANGED
|
@@ -45,6 +45,7 @@ __export(index_exports, {
|
|
|
45
45
|
UpdateElementCommand: () => UpdateElementCommand,
|
|
46
46
|
VERSION: () => VERSION,
|
|
47
47
|
Viewport: () => Viewport,
|
|
48
|
+
clearStaleBindings: () => clearStaleBindings,
|
|
48
49
|
createArrow: () => createArrow,
|
|
49
50
|
createHtmlElement: () => createHtmlElement,
|
|
50
51
|
createId: () => createId,
|
|
@@ -53,13 +54,21 @@ __export(index_exports, {
|
|
|
53
54
|
createStroke: () => createStroke,
|
|
54
55
|
createText: () => createText,
|
|
55
56
|
exportState: () => exportState,
|
|
57
|
+
findBindTarget: () => findBindTarget,
|
|
58
|
+
findBoundArrows: () => findBoundArrows,
|
|
56
59
|
getArrowBounds: () => getArrowBounds,
|
|
57
60
|
getArrowControlPoint: () => getArrowControlPoint,
|
|
58
61
|
getArrowMidpoint: () => getArrowMidpoint,
|
|
59
62
|
getArrowTangentAngle: () => getArrowTangentAngle,
|
|
60
63
|
getBendFromPoint: () => getBendFromPoint,
|
|
64
|
+
getEdgeIntersection: () => getEdgeIntersection,
|
|
65
|
+
getElementBounds: () => getElementBounds,
|
|
66
|
+
getElementCenter: () => getElementCenter,
|
|
67
|
+
isBindable: () => isBindable,
|
|
61
68
|
isNearBezier: () => isNearBezier,
|
|
62
|
-
parseState: () => parseState
|
|
69
|
+
parseState: () => parseState,
|
|
70
|
+
unbindArrow: () => unbindArrow,
|
|
71
|
+
updateBoundArrow: () => updateBoundArrow
|
|
63
72
|
});
|
|
64
73
|
module.exports = __toCommonJS(index_exports);
|
|
65
74
|
|
|
@@ -133,6 +142,7 @@ function validateState(data) {
|
|
|
133
142
|
validateElement(el);
|
|
134
143
|
migrateElement(el);
|
|
135
144
|
}
|
|
145
|
+
cleanBindings(obj["elements"]);
|
|
136
146
|
}
|
|
137
147
|
var VALID_TYPES = /* @__PURE__ */ new Set(["stroke", "note", "arrow", "image", "html", "text"]);
|
|
138
148
|
function validateElement(el) {
|
|
@@ -150,6 +160,20 @@ function validateElement(el) {
|
|
|
150
160
|
throw new Error("Invalid element: missing zIndex");
|
|
151
161
|
}
|
|
152
162
|
}
|
|
163
|
+
function cleanBindings(elements) {
|
|
164
|
+
const ids = new Set(elements.map((el) => el["id"]));
|
|
165
|
+
for (const el of elements) {
|
|
166
|
+
if (el["type"] !== "arrow") continue;
|
|
167
|
+
const fromBinding = el["fromBinding"];
|
|
168
|
+
if (fromBinding && !ids.has(fromBinding["elementId"])) {
|
|
169
|
+
el["fromBinding"] = void 0;
|
|
170
|
+
}
|
|
171
|
+
const toBinding = el["toBinding"];
|
|
172
|
+
if (toBinding && !ids.has(toBinding["elementId"])) {
|
|
173
|
+
el["toBinding"] = void 0;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
153
177
|
function migrateElement(obj) {
|
|
154
178
|
if (obj["type"] === "arrow" && typeof obj["bend"] !== "number") {
|
|
155
179
|
obj["bend"] = 0;
|
|
@@ -743,6 +767,134 @@ function isNearLine(point, a, b, threshold) {
|
|
|
743
767
|
return Math.hypot(point.x - projX, point.y - projY) <= threshold;
|
|
744
768
|
}
|
|
745
769
|
|
|
770
|
+
// src/elements/arrow-binding.ts
|
|
771
|
+
var BINDABLE_TYPES = /* @__PURE__ */ new Set(["note", "text", "image", "html"]);
|
|
772
|
+
function isBindable(element) {
|
|
773
|
+
return BINDABLE_TYPES.has(element.type);
|
|
774
|
+
}
|
|
775
|
+
function getElementCenter(element) {
|
|
776
|
+
if (!("size" in element)) {
|
|
777
|
+
throw new Error(`getElementCenter: element type "${element.type}" has no size`);
|
|
778
|
+
}
|
|
779
|
+
return {
|
|
780
|
+
x: element.position.x + element.size.w / 2,
|
|
781
|
+
y: element.position.y + element.size.h / 2
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
function getElementBounds(element) {
|
|
785
|
+
if (!("size" in element)) return null;
|
|
786
|
+
return {
|
|
787
|
+
x: element.position.x,
|
|
788
|
+
y: element.position.y,
|
|
789
|
+
w: element.size.w,
|
|
790
|
+
h: element.size.h
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
function getEdgeIntersection(bounds, outsidePoint) {
|
|
794
|
+
const cx = bounds.x + bounds.w / 2;
|
|
795
|
+
const cy = bounds.y + bounds.h / 2;
|
|
796
|
+
const dx = outsidePoint.x - cx;
|
|
797
|
+
const dy = outsidePoint.y - cy;
|
|
798
|
+
if (dx === 0 && dy === 0) return { x: cx, y: cy };
|
|
799
|
+
const halfW = bounds.w / 2;
|
|
800
|
+
const halfH = bounds.h / 2;
|
|
801
|
+
const scaleX = dx !== 0 ? halfW / Math.abs(dx) : Infinity;
|
|
802
|
+
const scaleY = dy !== 0 ? halfH / Math.abs(dy) : Infinity;
|
|
803
|
+
const scale = Math.min(scaleX, scaleY);
|
|
804
|
+
return {
|
|
805
|
+
x: cx + dx * scale,
|
|
806
|
+
y: cy + dy * scale
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
function findBindTarget(point, store, threshold, excludeId) {
|
|
810
|
+
let closest = null;
|
|
811
|
+
let closestDist = Infinity;
|
|
812
|
+
for (const el of store.getAll()) {
|
|
813
|
+
if (!isBindable(el)) continue;
|
|
814
|
+
if (excludeId && el.id === excludeId) continue;
|
|
815
|
+
const bounds = getElementBounds(el);
|
|
816
|
+
if (!bounds) continue;
|
|
817
|
+
const dist = distToBounds(point, bounds);
|
|
818
|
+
if (dist <= threshold && dist < closestDist) {
|
|
819
|
+
closest = el;
|
|
820
|
+
closestDist = dist;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
return closest;
|
|
824
|
+
}
|
|
825
|
+
function distToBounds(point, bounds) {
|
|
826
|
+
const clampedX = Math.max(bounds.x, Math.min(point.x, bounds.x + bounds.w));
|
|
827
|
+
const clampedY = Math.max(bounds.y, Math.min(point.y, bounds.y + bounds.h));
|
|
828
|
+
return Math.hypot(point.x - clampedX, point.y - clampedY);
|
|
829
|
+
}
|
|
830
|
+
function findBoundArrows(elementId, store) {
|
|
831
|
+
return store.getElementsByType("arrow").filter((a) => a.fromBinding?.elementId === elementId || a.toBinding?.elementId === elementId);
|
|
832
|
+
}
|
|
833
|
+
function updateBoundArrow(arrow, store) {
|
|
834
|
+
if (!arrow.fromBinding && !arrow.toBinding) return null;
|
|
835
|
+
const updates = {};
|
|
836
|
+
if (arrow.fromBinding) {
|
|
837
|
+
const el = store.getById(arrow.fromBinding.elementId);
|
|
838
|
+
if (el) {
|
|
839
|
+
const center = getElementCenter(el);
|
|
840
|
+
updates.from = center;
|
|
841
|
+
updates.position = center;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
if (arrow.toBinding) {
|
|
845
|
+
const el = store.getById(arrow.toBinding.elementId);
|
|
846
|
+
if (el) {
|
|
847
|
+
updates.to = getElementCenter(el);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
return Object.keys(updates).length > 0 ? updates : null;
|
|
851
|
+
}
|
|
852
|
+
function clearStaleBindings(arrow, store) {
|
|
853
|
+
const updates = {};
|
|
854
|
+
let hasUpdates = false;
|
|
855
|
+
if (arrow.fromBinding && !store.getById(arrow.fromBinding.elementId)) {
|
|
856
|
+
updates.fromBinding = void 0;
|
|
857
|
+
hasUpdates = true;
|
|
858
|
+
}
|
|
859
|
+
if (arrow.toBinding && !store.getById(arrow.toBinding.elementId)) {
|
|
860
|
+
updates.toBinding = void 0;
|
|
861
|
+
hasUpdates = true;
|
|
862
|
+
}
|
|
863
|
+
return hasUpdates ? updates : null;
|
|
864
|
+
}
|
|
865
|
+
function unbindArrow(arrow, store) {
|
|
866
|
+
const updates = {};
|
|
867
|
+
if (arrow.fromBinding) {
|
|
868
|
+
const el = store.getById(arrow.fromBinding.elementId);
|
|
869
|
+
const bounds = el ? getElementBounds(el) : null;
|
|
870
|
+
if (bounds) {
|
|
871
|
+
const angle = getArrowTangentAngle(arrow.from, arrow.to, arrow.bend, 0);
|
|
872
|
+
const rayTarget = {
|
|
873
|
+
x: arrow.from.x + Math.cos(angle) * 1e3,
|
|
874
|
+
y: arrow.from.y + Math.sin(angle) * 1e3
|
|
875
|
+
};
|
|
876
|
+
const edge = getEdgeIntersection(bounds, rayTarget);
|
|
877
|
+
updates.from = edge;
|
|
878
|
+
updates.position = edge;
|
|
879
|
+
}
|
|
880
|
+
updates.fromBinding = void 0;
|
|
881
|
+
}
|
|
882
|
+
if (arrow.toBinding) {
|
|
883
|
+
const el = store.getById(arrow.toBinding.elementId);
|
|
884
|
+
const bounds = el ? getElementBounds(el) : null;
|
|
885
|
+
if (bounds) {
|
|
886
|
+
const angle = getArrowTangentAngle(arrow.from, arrow.to, arrow.bend, 1);
|
|
887
|
+
const rayTarget = {
|
|
888
|
+
x: arrow.to.x - Math.cos(angle) * 1e3,
|
|
889
|
+
y: arrow.to.y - Math.sin(angle) * 1e3
|
|
890
|
+
};
|
|
891
|
+
updates.to = getEdgeIntersection(bounds, rayTarget);
|
|
892
|
+
}
|
|
893
|
+
updates.toBinding = void 0;
|
|
894
|
+
}
|
|
895
|
+
return updates;
|
|
896
|
+
}
|
|
897
|
+
|
|
746
898
|
// src/elements/stroke-smoothing.ts
|
|
747
899
|
var MIN_PRESSURE_SCALE = 0.2;
|
|
748
900
|
function pressureToWidth(pressure, baseWidth) {
|
|
@@ -821,6 +973,10 @@ var DOM_ELEMENT_TYPES = /* @__PURE__ */ new Set(["note", "image", "html", "text"
|
|
|
821
973
|
var ARROWHEAD_LENGTH = 12;
|
|
822
974
|
var ARROWHEAD_ANGLE = Math.PI / 6;
|
|
823
975
|
var ElementRenderer = class {
|
|
976
|
+
store = null;
|
|
977
|
+
setStore(store) {
|
|
978
|
+
this.store = store;
|
|
979
|
+
}
|
|
824
980
|
isDomElement(element) {
|
|
825
981
|
return DOM_ELEMENT_TYPES.has(element.type);
|
|
826
982
|
}
|
|
@@ -854,38 +1010,76 @@ var ElementRenderer = class {
|
|
|
854
1010
|
ctx.restore();
|
|
855
1011
|
}
|
|
856
1012
|
renderArrow(ctx, arrow) {
|
|
1013
|
+
const { visualFrom, visualTo } = this.getVisualEndpoints(arrow);
|
|
857
1014
|
ctx.save();
|
|
858
1015
|
ctx.strokeStyle = arrow.color;
|
|
859
1016
|
ctx.lineWidth = arrow.width;
|
|
860
1017
|
ctx.lineCap = "round";
|
|
1018
|
+
if (arrow.fromBinding || arrow.toBinding) {
|
|
1019
|
+
ctx.setLineDash([8, 4]);
|
|
1020
|
+
}
|
|
861
1021
|
ctx.beginPath();
|
|
862
|
-
ctx.moveTo(
|
|
1022
|
+
ctx.moveTo(visualFrom.x, visualFrom.y);
|
|
863
1023
|
if (arrow.bend !== 0) {
|
|
864
1024
|
const cp = getArrowControlPoint(arrow.from, arrow.to, arrow.bend);
|
|
865
|
-
ctx.quadraticCurveTo(cp.x, cp.y,
|
|
1025
|
+
ctx.quadraticCurveTo(cp.x, cp.y, visualTo.x, visualTo.y);
|
|
866
1026
|
} else {
|
|
867
|
-
ctx.lineTo(
|
|
1027
|
+
ctx.lineTo(visualTo.x, visualTo.y);
|
|
868
1028
|
}
|
|
869
1029
|
ctx.stroke();
|
|
870
|
-
this.renderArrowhead(ctx, arrow);
|
|
1030
|
+
this.renderArrowhead(ctx, arrow, visualTo);
|
|
871
1031
|
ctx.restore();
|
|
872
1032
|
}
|
|
873
|
-
renderArrowhead(ctx, arrow) {
|
|
1033
|
+
renderArrowhead(ctx, arrow, tip) {
|
|
874
1034
|
const angle = getArrowTangentAngle(arrow.from, arrow.to, arrow.bend, 1);
|
|
875
1035
|
ctx.beginPath();
|
|
876
|
-
ctx.moveTo(
|
|
1036
|
+
ctx.moveTo(tip.x, tip.y);
|
|
877
1037
|
ctx.lineTo(
|
|
878
|
-
|
|
879
|
-
|
|
1038
|
+
tip.x - ARROWHEAD_LENGTH * Math.cos(angle - ARROWHEAD_ANGLE),
|
|
1039
|
+
tip.y - ARROWHEAD_LENGTH * Math.sin(angle - ARROWHEAD_ANGLE)
|
|
880
1040
|
);
|
|
881
1041
|
ctx.lineTo(
|
|
882
|
-
|
|
883
|
-
|
|
1042
|
+
tip.x - ARROWHEAD_LENGTH * Math.cos(angle + ARROWHEAD_ANGLE),
|
|
1043
|
+
tip.y - ARROWHEAD_LENGTH * Math.sin(angle + ARROWHEAD_ANGLE)
|
|
884
1044
|
);
|
|
885
1045
|
ctx.closePath();
|
|
886
1046
|
ctx.fillStyle = arrow.color;
|
|
887
1047
|
ctx.fill();
|
|
888
1048
|
}
|
|
1049
|
+
getVisualEndpoints(arrow) {
|
|
1050
|
+
let visualFrom = arrow.from;
|
|
1051
|
+
let visualTo = arrow.to;
|
|
1052
|
+
if (!this.store) return { visualFrom, visualTo };
|
|
1053
|
+
if (arrow.fromBinding) {
|
|
1054
|
+
const el = this.store.getById(arrow.fromBinding.elementId);
|
|
1055
|
+
if (el) {
|
|
1056
|
+
const bounds = getElementBounds(el);
|
|
1057
|
+
if (bounds) {
|
|
1058
|
+
const tangentAngle = getArrowTangentAngle(arrow.from, arrow.to, arrow.bend, 0);
|
|
1059
|
+
const rayTarget = {
|
|
1060
|
+
x: arrow.from.x + Math.cos(tangentAngle) * 1e3,
|
|
1061
|
+
y: arrow.from.y + Math.sin(tangentAngle) * 1e3
|
|
1062
|
+
};
|
|
1063
|
+
visualFrom = getEdgeIntersection(bounds, rayTarget);
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
if (arrow.toBinding) {
|
|
1068
|
+
const el = this.store.getById(arrow.toBinding.elementId);
|
|
1069
|
+
if (el) {
|
|
1070
|
+
const bounds = getElementBounds(el);
|
|
1071
|
+
if (bounds) {
|
|
1072
|
+
const tangentAngle = getArrowTangentAngle(arrow.from, arrow.to, arrow.bend, 1);
|
|
1073
|
+
const rayTarget = {
|
|
1074
|
+
x: arrow.to.x - Math.cos(tangentAngle) * 1e3,
|
|
1075
|
+
y: arrow.to.y - Math.sin(tangentAngle) * 1e3
|
|
1076
|
+
};
|
|
1077
|
+
visualTo = getEdgeIntersection(bounds, rayTarget);
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
return { visualFrom, visualTo };
|
|
1082
|
+
}
|
|
889
1083
|
};
|
|
890
1084
|
|
|
891
1085
|
// src/elements/note-editor.ts
|
|
@@ -1256,7 +1450,7 @@ function createNote(input) {
|
|
|
1256
1450
|
};
|
|
1257
1451
|
}
|
|
1258
1452
|
function createArrow(input) {
|
|
1259
|
-
|
|
1453
|
+
const result = {
|
|
1260
1454
|
id: createId("arrow"),
|
|
1261
1455
|
type: "arrow",
|
|
1262
1456
|
position: input.position ?? { x: 0, y: 0 },
|
|
@@ -1268,6 +1462,9 @@ function createArrow(input) {
|
|
|
1268
1462
|
color: input.color ?? "#000000",
|
|
1269
1463
|
width: input.width ?? 2
|
|
1270
1464
|
};
|
|
1465
|
+
if (input.fromBinding) result.fromBinding = input.fromBinding;
|
|
1466
|
+
if (input.toBinding) result.toBinding = input.toBinding;
|
|
1467
|
+
return result;
|
|
1271
1468
|
}
|
|
1272
1469
|
function createImage(input) {
|
|
1273
1470
|
return {
|
|
@@ -1314,6 +1511,7 @@ var Viewport = class {
|
|
|
1314
1511
|
this.store = new ElementStore();
|
|
1315
1512
|
this.toolManager = new ToolManager();
|
|
1316
1513
|
this.renderer = new ElementRenderer();
|
|
1514
|
+
this.renderer.setStore(this.store);
|
|
1317
1515
|
this.noteEditor = new NoteEditor();
|
|
1318
1516
|
this.noteEditor.setOnStop((id) => this.onTextEditStop(id));
|
|
1319
1517
|
this.history = new HistoryStack();
|
|
@@ -1346,7 +1544,10 @@ var Viewport = class {
|
|
|
1346
1544
|
});
|
|
1347
1545
|
this.unsubStore = [
|
|
1348
1546
|
this.store.on("add", () => this.requestRender()),
|
|
1349
|
-
this.store.on("remove", (el) =>
|
|
1547
|
+
this.store.on("remove", (el) => {
|
|
1548
|
+
this.unbindArrowsFrom(el);
|
|
1549
|
+
this.removeDomNode(el.id);
|
|
1550
|
+
}),
|
|
1350
1551
|
this.store.on("update", () => this.requestRender()),
|
|
1351
1552
|
this.store.on("clear", () => this.clearDomNodes())
|
|
1352
1553
|
];
|
|
@@ -1721,6 +1922,40 @@ var Viewport = class {
|
|
|
1721
1922
|
}
|
|
1722
1923
|
}
|
|
1723
1924
|
}
|
|
1925
|
+
unbindArrowsFrom(removedElement) {
|
|
1926
|
+
const boundArrows = findBoundArrows(removedElement.id, this.store);
|
|
1927
|
+
const bounds = getElementBounds(removedElement);
|
|
1928
|
+
for (const arrow of boundArrows) {
|
|
1929
|
+
const updates = {};
|
|
1930
|
+
if (arrow.fromBinding?.elementId === removedElement.id) {
|
|
1931
|
+
updates.fromBinding = void 0;
|
|
1932
|
+
if (bounds) {
|
|
1933
|
+
const angle = getArrowTangentAngle(arrow.from, arrow.to, arrow.bend, 0);
|
|
1934
|
+
const rayTarget = {
|
|
1935
|
+
x: arrow.from.x + Math.cos(angle) * 1e3,
|
|
1936
|
+
y: arrow.from.y + Math.sin(angle) * 1e3
|
|
1937
|
+
};
|
|
1938
|
+
const edge = getEdgeIntersection(bounds, rayTarget);
|
|
1939
|
+
updates.from = edge;
|
|
1940
|
+
updates.position = edge;
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
if (arrow.toBinding?.elementId === removedElement.id) {
|
|
1944
|
+
updates.toBinding = void 0;
|
|
1945
|
+
if (bounds) {
|
|
1946
|
+
const angle = getArrowTangentAngle(arrow.from, arrow.to, arrow.bend, 1);
|
|
1947
|
+
const rayTarget = {
|
|
1948
|
+
x: arrow.to.x - Math.cos(angle) * 1e3,
|
|
1949
|
+
y: arrow.to.y - Math.sin(angle) * 1e3
|
|
1950
|
+
};
|
|
1951
|
+
updates.to = getEdgeIntersection(bounds, rayTarget);
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
if (Object.keys(updates).length > 0) {
|
|
1955
|
+
this.store.update(arrow.id, updates);
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1724
1959
|
removeDomNode(id) {
|
|
1725
1960
|
this.htmlContent.delete(id);
|
|
1726
1961
|
const node = this.domNodes.get(id);
|
|
@@ -1949,6 +2184,7 @@ var EraserTool = class {
|
|
|
1949
2184
|
};
|
|
1950
2185
|
|
|
1951
2186
|
// src/tools/arrow-handles.ts
|
|
2187
|
+
var BIND_THRESHOLD = 20;
|
|
1952
2188
|
var HANDLE_RADIUS = 5;
|
|
1953
2189
|
var HANDLE_HIT_PADDING = 4;
|
|
1954
2190
|
var ARROW_HANDLE_CURSORS = {
|
|
@@ -1989,18 +2225,44 @@ function hitTestArrowHandles(world, selectedIds, ctx) {
|
|
|
1989
2225
|
function applyArrowHandleDrag(handle, elementId, world, ctx) {
|
|
1990
2226
|
const el = ctx.store.getById(elementId);
|
|
1991
2227
|
if (!el || el.type !== "arrow") return;
|
|
2228
|
+
const threshold = BIND_THRESHOLD / ctx.camera.zoom;
|
|
1992
2229
|
switch (handle) {
|
|
1993
|
-
case "start":
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
2230
|
+
case "start": {
|
|
2231
|
+
const excludeId = el.toBinding?.elementId;
|
|
2232
|
+
const target = findBindTarget(world, ctx.store, threshold, excludeId);
|
|
2233
|
+
if (target) {
|
|
2234
|
+
const center = getElementCenter(target);
|
|
2235
|
+
ctx.store.update(elementId, {
|
|
2236
|
+
from: center,
|
|
2237
|
+
position: center,
|
|
2238
|
+
fromBinding: { elementId: target.id }
|
|
2239
|
+
});
|
|
2240
|
+
} else {
|
|
2241
|
+
ctx.store.update(elementId, {
|
|
2242
|
+
from: { x: world.x, y: world.y },
|
|
2243
|
+
position: { x: world.x, y: world.y },
|
|
2244
|
+
fromBinding: void 0
|
|
2245
|
+
});
|
|
2246
|
+
}
|
|
1998
2247
|
break;
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2248
|
+
}
|
|
2249
|
+
case "end": {
|
|
2250
|
+
const excludeId = el.fromBinding?.elementId;
|
|
2251
|
+
const target = findBindTarget(world, ctx.store, threshold, excludeId);
|
|
2252
|
+
if (target) {
|
|
2253
|
+
const center = getElementCenter(target);
|
|
2254
|
+
ctx.store.update(elementId, {
|
|
2255
|
+
to: center,
|
|
2256
|
+
toBinding: { elementId: target.id }
|
|
2257
|
+
});
|
|
2258
|
+
} else {
|
|
2259
|
+
ctx.store.update(elementId, {
|
|
2260
|
+
to: { x: world.x, y: world.y },
|
|
2261
|
+
toBinding: void 0
|
|
2262
|
+
});
|
|
2263
|
+
}
|
|
2003
2264
|
break;
|
|
2265
|
+
}
|
|
2004
2266
|
case "mid": {
|
|
2005
2267
|
const bend = getBendFromPoint(el.from, el.to, world);
|
|
2006
2268
|
ctx.store.update(elementId, { bend });
|
|
@@ -2009,6 +2271,16 @@ function applyArrowHandleDrag(handle, elementId, world, ctx) {
|
|
|
2009
2271
|
}
|
|
2010
2272
|
ctx.requestRender();
|
|
2011
2273
|
}
|
|
2274
|
+
function getArrowHandleDragTarget(handle, elementId, world, ctx) {
|
|
2275
|
+
if (handle === "mid") return null;
|
|
2276
|
+
const el = ctx.store.getById(elementId);
|
|
2277
|
+
if (!el || el.type !== "arrow") return null;
|
|
2278
|
+
const threshold = BIND_THRESHOLD / ctx.camera.zoom;
|
|
2279
|
+
const excludeId = handle === "start" ? el.toBinding?.elementId : el.fromBinding?.elementId;
|
|
2280
|
+
const target = findBindTarget(world, ctx.store, threshold, excludeId);
|
|
2281
|
+
if (!target) return null;
|
|
2282
|
+
return getElementBounds(target);
|
|
2283
|
+
}
|
|
2012
2284
|
function renderArrowHandles(canvasCtx, arrow, zoom) {
|
|
2013
2285
|
const radius = HANDLE_RADIUS / zoom;
|
|
2014
2286
|
const handles = getArrowHandlePositions(arrow);
|
|
@@ -2119,6 +2391,9 @@ var SelectTool = class {
|
|
|
2119
2391
|
const el = ctx.store.getById(id);
|
|
2120
2392
|
if (!el || el.locked) continue;
|
|
2121
2393
|
if (el.type === "arrow") {
|
|
2394
|
+
if (el.fromBinding || el.toBinding) {
|
|
2395
|
+
continue;
|
|
2396
|
+
}
|
|
2122
2397
|
ctx.store.update(id, {
|
|
2123
2398
|
position: { x: el.position.x + dx, y: el.position.y + dy },
|
|
2124
2399
|
from: { x: el.from.x + dx, y: el.from.y + dy },
|
|
@@ -2130,6 +2405,23 @@ var SelectTool = class {
|
|
|
2130
2405
|
});
|
|
2131
2406
|
}
|
|
2132
2407
|
}
|
|
2408
|
+
const movedNonArrowIds = /* @__PURE__ */ new Set();
|
|
2409
|
+
for (const id of this._selectedIds) {
|
|
2410
|
+
const el = ctx.store.getById(id);
|
|
2411
|
+
if (el && el.type !== "arrow") movedNonArrowIds.add(id);
|
|
2412
|
+
}
|
|
2413
|
+
if (movedNonArrowIds.size > 0) {
|
|
2414
|
+
const updatedArrows = /* @__PURE__ */ new Set();
|
|
2415
|
+
for (const id of movedNonArrowIds) {
|
|
2416
|
+
const boundArrows = findBoundArrows(id, ctx.store);
|
|
2417
|
+
for (const ba of boundArrows) {
|
|
2418
|
+
if (updatedArrows.has(ba.id)) continue;
|
|
2419
|
+
updatedArrows.add(ba.id);
|
|
2420
|
+
const updates = updateBoundArrow(ba, ctx.store);
|
|
2421
|
+
if (updates) ctx.store.update(ba.id, updates);
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2133
2425
|
ctx.requestRender();
|
|
2134
2426
|
return;
|
|
2135
2427
|
}
|
|
@@ -2158,6 +2450,22 @@ var SelectTool = class {
|
|
|
2158
2450
|
renderOverlay(canvasCtx) {
|
|
2159
2451
|
this.renderMarquee(canvasCtx);
|
|
2160
2452
|
this.renderSelectionBoxes(canvasCtx);
|
|
2453
|
+
if (this.mode.type === "arrow-handle" && this.ctx) {
|
|
2454
|
+
const target = getArrowHandleDragTarget(
|
|
2455
|
+
this.mode.handle,
|
|
2456
|
+
this.mode.elementId,
|
|
2457
|
+
this.currentWorld,
|
|
2458
|
+
this.ctx
|
|
2459
|
+
);
|
|
2460
|
+
if (target) {
|
|
2461
|
+
canvasCtx.save();
|
|
2462
|
+
canvasCtx.strokeStyle = "#2196F3";
|
|
2463
|
+
canvasCtx.lineWidth = 2 / this.ctx.camera.zoom;
|
|
2464
|
+
canvasCtx.setLineDash([]);
|
|
2465
|
+
canvasCtx.strokeRect(target.x, target.y, target.w, target.h);
|
|
2466
|
+
canvasCtx.restore();
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
2161
2469
|
}
|
|
2162
2470
|
updateHoverCursor(world, ctx) {
|
|
2163
2471
|
const arrowHit = hitTestArrowHandles(world, this._selectedIds, ctx);
|
|
@@ -2216,6 +2524,11 @@ var SelectTool = class {
|
|
|
2216
2524
|
position: { x, y },
|
|
2217
2525
|
size: { w, h }
|
|
2218
2526
|
});
|
|
2527
|
+
const boundArrows = findBoundArrows(this.mode.elementId, ctx.store);
|
|
2528
|
+
for (const ba of boundArrows) {
|
|
2529
|
+
const updates = updateBoundArrow(ba, ctx.store);
|
|
2530
|
+
if (updates) ctx.store.update(ba.id, updates);
|
|
2531
|
+
}
|
|
2219
2532
|
ctx.requestRender();
|
|
2220
2533
|
}
|
|
2221
2534
|
hitTestResizeHandle(world, ctx) {
|
|
@@ -2270,6 +2583,7 @@ var SelectTool = class {
|
|
|
2270
2583
|
if (!el) continue;
|
|
2271
2584
|
if (el.type === "arrow") {
|
|
2272
2585
|
renderArrowHandles(canvasCtx, el, zoom);
|
|
2586
|
+
this.renderBindingHighlights(canvasCtx, el, zoom);
|
|
2273
2587
|
continue;
|
|
2274
2588
|
}
|
|
2275
2589
|
const bounds = this.getElementBounds(el);
|
|
@@ -2299,6 +2613,26 @@ var SelectTool = class {
|
|
|
2299
2613
|
}
|
|
2300
2614
|
canvasCtx.restore();
|
|
2301
2615
|
}
|
|
2616
|
+
renderBindingHighlights(canvasCtx, arrow, zoom) {
|
|
2617
|
+
if (!this.ctx) return;
|
|
2618
|
+
if (!arrow.fromBinding && !arrow.toBinding) return;
|
|
2619
|
+
const pad = SELECTION_PAD / zoom;
|
|
2620
|
+
canvasCtx.save();
|
|
2621
|
+
canvasCtx.strokeStyle = "#2196F3";
|
|
2622
|
+
canvasCtx.lineWidth = 2 / zoom;
|
|
2623
|
+
canvasCtx.setLineDash([]);
|
|
2624
|
+
const drawn = /* @__PURE__ */ new Set();
|
|
2625
|
+
for (const binding of [arrow.fromBinding, arrow.toBinding]) {
|
|
2626
|
+
if (!binding || drawn.has(binding.elementId)) continue;
|
|
2627
|
+
drawn.add(binding.elementId);
|
|
2628
|
+
const target = this.ctx.store.getById(binding.elementId);
|
|
2629
|
+
if (!target) continue;
|
|
2630
|
+
const bounds = getElementBounds(target);
|
|
2631
|
+
if (!bounds) continue;
|
|
2632
|
+
canvasCtx.strokeRect(bounds.x - pad, bounds.y - pad, bounds.w + pad * 2, bounds.h + pad * 2);
|
|
2633
|
+
}
|
|
2634
|
+
canvasCtx.restore();
|
|
2635
|
+
}
|
|
2302
2636
|
getMarqueeRect() {
|
|
2303
2637
|
if (this.mode.type !== "marquee") return null;
|
|
2304
2638
|
const { start } = this.mode;
|
|
@@ -2372,6 +2706,7 @@ var SelectTool = class {
|
|
|
2372
2706
|
};
|
|
2373
2707
|
|
|
2374
2708
|
// src/tools/arrow-tool.ts
|
|
2709
|
+
var BIND_THRESHOLD2 = 20;
|
|
2375
2710
|
var ArrowTool = class {
|
|
2376
2711
|
name = "arrow";
|
|
2377
2712
|
drawing = false;
|
|
@@ -2379,6 +2714,9 @@ var ArrowTool = class {
|
|
|
2379
2714
|
end = { x: 0, y: 0 };
|
|
2380
2715
|
color;
|
|
2381
2716
|
width;
|
|
2717
|
+
fromBinding;
|
|
2718
|
+
fromTarget = null;
|
|
2719
|
+
toTarget = null;
|
|
2382
2720
|
constructor(options = {}) {
|
|
2383
2721
|
this.color = options.color ?? "#000000";
|
|
2384
2722
|
this.width = options.width ?? 2;
|
|
@@ -2389,12 +2727,34 @@ var ArrowTool = class {
|
|
|
2389
2727
|
}
|
|
2390
2728
|
onPointerDown(state, ctx) {
|
|
2391
2729
|
this.drawing = true;
|
|
2392
|
-
|
|
2730
|
+
const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
|
|
2731
|
+
const threshold = BIND_THRESHOLD2 / ctx.camera.zoom;
|
|
2732
|
+
const target = findBindTarget(world, ctx.store, threshold);
|
|
2733
|
+
if (target) {
|
|
2734
|
+
this.start = getElementCenter(target);
|
|
2735
|
+
this.fromBinding = { elementId: target.id };
|
|
2736
|
+
this.fromTarget = target;
|
|
2737
|
+
} else {
|
|
2738
|
+
this.start = world;
|
|
2739
|
+
this.fromBinding = void 0;
|
|
2740
|
+
this.fromTarget = null;
|
|
2741
|
+
}
|
|
2393
2742
|
this.end = { ...this.start };
|
|
2743
|
+
this.toTarget = null;
|
|
2394
2744
|
}
|
|
2395
2745
|
onPointerMove(state, ctx) {
|
|
2396
2746
|
if (!this.drawing) return;
|
|
2397
|
-
|
|
2747
|
+
const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
|
|
2748
|
+
const threshold = BIND_THRESHOLD2 / ctx.camera.zoom;
|
|
2749
|
+
const excludeId = this.fromBinding?.elementId;
|
|
2750
|
+
const target = findBindTarget(world, ctx.store, threshold, excludeId);
|
|
2751
|
+
if (target) {
|
|
2752
|
+
this.end = getElementCenter(target);
|
|
2753
|
+
this.toTarget = target;
|
|
2754
|
+
} else {
|
|
2755
|
+
this.end = world;
|
|
2756
|
+
this.toTarget = null;
|
|
2757
|
+
}
|
|
2398
2758
|
ctx.requestRender();
|
|
2399
2759
|
}
|
|
2400
2760
|
onPointerUp(_state, ctx) {
|
|
@@ -2404,16 +2764,40 @@ var ArrowTool = class {
|
|
|
2404
2764
|
const arrow = createArrow({
|
|
2405
2765
|
from: this.start,
|
|
2406
2766
|
to: this.end,
|
|
2767
|
+
position: this.start,
|
|
2407
2768
|
color: this.color,
|
|
2408
|
-
width: this.width
|
|
2769
|
+
width: this.width,
|
|
2770
|
+
fromBinding: this.fromBinding,
|
|
2771
|
+
toBinding: this.toTarget ? { elementId: this.toTarget.id } : void 0
|
|
2409
2772
|
});
|
|
2410
2773
|
ctx.store.add(arrow);
|
|
2774
|
+
this.fromTarget = null;
|
|
2775
|
+
this.toTarget = null;
|
|
2411
2776
|
ctx.requestRender();
|
|
2777
|
+
ctx.switchTool?.("select");
|
|
2412
2778
|
}
|
|
2413
2779
|
renderOverlay(ctx) {
|
|
2414
2780
|
if (!this.drawing) return;
|
|
2415
2781
|
if (this.start.x === this.end.x && this.start.y === this.end.y) return;
|
|
2416
2782
|
ctx.save();
|
|
2783
|
+
if (this.fromTarget) {
|
|
2784
|
+
const bounds = getElementBounds(this.fromTarget);
|
|
2785
|
+
if (bounds) {
|
|
2786
|
+
ctx.strokeStyle = "#2196F3";
|
|
2787
|
+
ctx.lineWidth = 2;
|
|
2788
|
+
ctx.setLineDash([]);
|
|
2789
|
+
ctx.strokeRect(bounds.x, bounds.y, bounds.w, bounds.h);
|
|
2790
|
+
}
|
|
2791
|
+
}
|
|
2792
|
+
if (this.toTarget) {
|
|
2793
|
+
const bounds = getElementBounds(this.toTarget);
|
|
2794
|
+
if (bounds) {
|
|
2795
|
+
ctx.strokeStyle = "#2196F3";
|
|
2796
|
+
ctx.lineWidth = 2;
|
|
2797
|
+
ctx.setLineDash([]);
|
|
2798
|
+
ctx.strokeRect(bounds.x, bounds.y, bounds.w, bounds.h);
|
|
2799
|
+
}
|
|
2800
|
+
}
|
|
2417
2801
|
ctx.strokeStyle = this.color;
|
|
2418
2802
|
ctx.lineWidth = this.width;
|
|
2419
2803
|
ctx.lineCap = "round";
|
|
@@ -2545,7 +2929,7 @@ var ImageTool = class {
|
|
|
2545
2929
|
};
|
|
2546
2930
|
|
|
2547
2931
|
// src/index.ts
|
|
2548
|
-
var VERSION = "0.3.
|
|
2932
|
+
var VERSION = "0.3.1";
|
|
2549
2933
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2550
2934
|
0 && (module.exports = {
|
|
2551
2935
|
AddElementCommand,
|
|
@@ -2573,6 +2957,7 @@ var VERSION = "0.3.0";
|
|
|
2573
2957
|
UpdateElementCommand,
|
|
2574
2958
|
VERSION,
|
|
2575
2959
|
Viewport,
|
|
2960
|
+
clearStaleBindings,
|
|
2576
2961
|
createArrow,
|
|
2577
2962
|
createHtmlElement,
|
|
2578
2963
|
createId,
|
|
@@ -2581,12 +2966,20 @@ var VERSION = "0.3.0";
|
|
|
2581
2966
|
createStroke,
|
|
2582
2967
|
createText,
|
|
2583
2968
|
exportState,
|
|
2969
|
+
findBindTarget,
|
|
2970
|
+
findBoundArrows,
|
|
2584
2971
|
getArrowBounds,
|
|
2585
2972
|
getArrowControlPoint,
|
|
2586
2973
|
getArrowMidpoint,
|
|
2587
2974
|
getArrowTangentAngle,
|
|
2588
2975
|
getBendFromPoint,
|
|
2976
|
+
getEdgeIntersection,
|
|
2977
|
+
getElementBounds,
|
|
2978
|
+
getElementCenter,
|
|
2979
|
+
isBindable,
|
|
2589
2980
|
isNearBezier,
|
|
2590
|
-
parseState
|
|
2981
|
+
parseState,
|
|
2982
|
+
unbindArrow,
|
|
2983
|
+
updateBoundArrow
|
|
2591
2984
|
});
|
|
2592
2985
|
//# sourceMappingURL=index.cjs.map
|