@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.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(arrow.from.x, arrow.from.y);
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, arrow.to.x, arrow.to.y);
951
+ ctx.quadraticCurveTo(cp.x, cp.y, visualTo.x, visualTo.y);
801
952
  } else {
802
- ctx.lineTo(arrow.to.x, arrow.to.y);
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(arrow.to.x, arrow.to.y);
962
+ ctx.moveTo(tip.x, tip.y);
812
963
  ctx.lineTo(
813
- arrow.to.x - ARROWHEAD_LENGTH * Math.cos(angle - ARROWHEAD_ANGLE),
814
- arrow.to.y - ARROWHEAD_LENGTH * Math.sin(angle - ARROWHEAD_ANGLE)
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
- arrow.to.x - ARROWHEAD_LENGTH * Math.cos(angle + ARROWHEAD_ANGLE),
818
- arrow.to.y - ARROWHEAD_LENGTH * Math.sin(angle + ARROWHEAD_ANGLE)
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
- return {
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) => this.removeDomNode(el.id)),
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
- ctx.store.update(elementId, {
1930
- from: { x: world.x, y: world.y },
1931
- position: { x: world.x, y: world.y }
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
- case "end":
1935
- ctx.store.update(elementId, {
1936
- to: { x: world.x, y: world.y }
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
- this.start = ctx.camera.screenToWorld({ x: state.x, y: state.y });
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
- this.end = ctx.camera.screenToWorld({ x: state.x, y: state.y });
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.0";
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