@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 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(arrow.from.x, arrow.from.y);
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, arrow.to.x, arrow.to.y);
1025
+ ctx.quadraticCurveTo(cp.x, cp.y, visualTo.x, visualTo.y);
866
1026
  } else {
867
- ctx.lineTo(arrow.to.x, arrow.to.y);
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(arrow.to.x, arrow.to.y);
1036
+ ctx.moveTo(tip.x, tip.y);
877
1037
  ctx.lineTo(
878
- arrow.to.x - ARROWHEAD_LENGTH * Math.cos(angle - ARROWHEAD_ANGLE),
879
- arrow.to.y - ARROWHEAD_LENGTH * Math.sin(angle - ARROWHEAD_ANGLE)
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
- arrow.to.x - ARROWHEAD_LENGTH * Math.cos(angle + ARROWHEAD_ANGLE),
883
- arrow.to.y - ARROWHEAD_LENGTH * Math.sin(angle + ARROWHEAD_ANGLE)
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
- return {
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) => this.removeDomNode(el.id)),
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
- ctx.store.update(elementId, {
1995
- from: { x: world.x, y: world.y },
1996
- position: { x: world.x, y: world.y }
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
- case "end":
2000
- ctx.store.update(elementId, {
2001
- to: { x: world.x, y: world.y }
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
- this.start = ctx.camera.screenToWorld({ x: state.x, y: state.y });
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
- this.end = ctx.camera.screenToWorld({ x: state.x, y: state.y });
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.0";
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