@fieldnotes/core 0.31.1 → 0.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -563,6 +563,22 @@ const unsub = viewport.onSelectionChange((ids) => {
563
563
  // call unsub() to unsubscribe
564
564
  ```
565
565
 
566
+ ## Aligning the Selection
567
+
568
+ Two methods on `Viewport` let you snap or space selected elements in one undo step.
569
+
570
+ - **`viewport.alignSelection(edge)`** — `edge`: `AlignEdge` = `'left' | 'center-x' | 'right' | 'top' | 'middle' | 'bottom'`; aligns every selected element to the corresponding edge or center of the selection's bounding box. Needs 2+ selected elements. Locked elements anchor the bounding box without moving.
571
+ - **`viewport.distributeSelection(axis)`** — `axis`: `DistributeAxis` = `'horizontal' | 'vertical'`; evenly spaces selected elements' centers along the axis. Needs 3+ selected elements. Locked elements anchor the span without moving.
572
+
573
+ ```typescript
574
+ viewport.alignSelection('left'); // flush left edges
575
+ viewport.alignSelection('center-x'); // center on vertical axis
576
+ viewport.alignSelection('middle'); // center on horizontal axis
577
+ viewport.distributeSelection('horizontal'); // equal horizontal spacing
578
+ ```
579
+
580
+ Grids are ignored by both operations.
581
+
566
582
  ## Built-in Interactions
567
583
 
568
584
  | Input | Action |
package/dist/index.cjs CHANGED
@@ -2351,6 +2351,23 @@ function distToBounds(point, bounds) {
2351
2351
  function findBoundArrows(elementId, store) {
2352
2352
  return store.getElementsByType("arrow").filter((a) => a.fromBinding?.elementId === elementId || a.toBinding?.elementId === elementId);
2353
2353
  }
2354
+ function updateArrowsBoundToElements(movedIds, store) {
2355
+ const movedNonArrowIds = /* @__PURE__ */ new Set();
2356
+ for (const id of movedIds) {
2357
+ const el = store.getById(id);
2358
+ if (el && el.type !== "arrow") movedNonArrowIds.add(id);
2359
+ }
2360
+ if (movedNonArrowIds.size === 0) return;
2361
+ const updated = /* @__PURE__ */ new Set();
2362
+ for (const id of movedNonArrowIds) {
2363
+ for (const ba of findBoundArrows(id, store)) {
2364
+ if (updated.has(ba.id)) continue;
2365
+ updated.add(ba.id);
2366
+ const updates = updateBoundArrow(ba, store);
2367
+ if (updates) store.update(ba.id, updates);
2368
+ }
2369
+ }
2370
+ }
2354
2371
  function updateBoundArrow(arrow, store) {
2355
2372
  if (!arrow.fromBinding && !arrow.toBinding) return null;
2356
2373
  const updates = {};
@@ -3778,6 +3795,19 @@ var NoteEditor = class {
3778
3795
  }
3779
3796
  };
3780
3797
 
3798
+ // src/elements/translate.ts
3799
+ function translateElementPatch(el, dx, dy) {
3800
+ const position = { x: el.position.x + dx, y: el.position.y + dy };
3801
+ if (el.type === "arrow") {
3802
+ return {
3803
+ position,
3804
+ from: { x: el.from.x + dx, y: el.from.y + dy },
3805
+ to: { x: el.to.x + dx, y: el.to.y + dy }
3806
+ };
3807
+ }
3808
+ return { position };
3809
+ }
3810
+
3781
3811
  // src/elements/arrow-label-editor.ts
3782
3812
  var ArrowLabelEditor = class {
3783
3813
  input = null;
@@ -5464,6 +5494,16 @@ function getElementStyle(element) {
5464
5494
  }
5465
5495
 
5466
5496
  // src/canvas/viewport.ts
5497
+ function unionBounds(list) {
5498
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
5499
+ for (const b of list) {
5500
+ minX = Math.min(minX, b.x);
5501
+ minY = Math.min(minY, b.y);
5502
+ maxX = Math.max(maxX, b.x + b.w);
5503
+ maxY = Math.max(maxY, b.y + b.h);
5504
+ }
5505
+ return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
5506
+ }
5467
5507
  var EMPTY_IDS = [];
5468
5508
  var ARROW_HIT_THRESHOLD = 10;
5469
5509
  function noop() {
@@ -5873,6 +5913,86 @@ var Viewport = class {
5873
5913
  }
5874
5914
  this.historyRecorder.commit();
5875
5915
  }
5916
+ alignSelection(edge) {
5917
+ const bounded = this.boundedSelection();
5918
+ if (bounded.length < 2) return;
5919
+ const B = unionBounds(bounded.map((e) => e.bounds));
5920
+ this.historyRecorder.begin();
5921
+ const moved = [];
5922
+ for (const { id, el, bounds: b } of bounded) {
5923
+ if (!this.isMovable(el)) continue;
5924
+ let dx = 0;
5925
+ let dy = 0;
5926
+ switch (edge) {
5927
+ case "left":
5928
+ dx = B.x - b.x;
5929
+ break;
5930
+ case "right":
5931
+ dx = B.x + B.w - (b.x + b.w);
5932
+ break;
5933
+ case "center-x":
5934
+ dx = B.x + B.w / 2 - (b.x + b.w / 2);
5935
+ break;
5936
+ case "top":
5937
+ dy = B.y - b.y;
5938
+ break;
5939
+ case "bottom":
5940
+ dy = B.y + B.h - (b.y + b.h);
5941
+ break;
5942
+ case "middle":
5943
+ dy = B.y + B.h / 2 - (b.y + b.h / 2);
5944
+ break;
5945
+ }
5946
+ if (dx === 0 && dy === 0) continue;
5947
+ this.store.update(id, translateElementPatch(el, dx, dy));
5948
+ moved.push(id);
5949
+ }
5950
+ updateArrowsBoundToElements(moved, this.store);
5951
+ this.historyRecorder.commit();
5952
+ this.requestRender();
5953
+ }
5954
+ distributeSelection(axis) {
5955
+ const bounded = this.boundedSelection();
5956
+ if (bounded.length < 3) return;
5957
+ const center = (b) => axis === "horizontal" ? b.x + b.w / 2 : b.y + b.h / 2;
5958
+ const sorted = [...bounded].sort((p, q) => center(p.bounds) - center(q.bounds));
5959
+ const first = sorted[0];
5960
+ const last = sorted[sorted.length - 1];
5961
+ if (!first || !last) return;
5962
+ const c0 = center(first.bounds);
5963
+ const cN = center(last.bounds);
5964
+ const n = sorted.length;
5965
+ this.historyRecorder.begin();
5966
+ const moved = [];
5967
+ for (let i = 1; i < n - 1; i++) {
5968
+ const item = sorted[i];
5969
+ if (!item || !this.isMovable(item.el)) continue;
5970
+ const target = c0 + i * (cN - c0) / (n - 1);
5971
+ const delta = target - center(item.bounds);
5972
+ if (delta === 0) continue;
5973
+ const [dx, dy] = axis === "horizontal" ? [delta, 0] : [0, delta];
5974
+ this.store.update(item.id, translateElementPatch(item.el, dx, dy));
5975
+ moved.push(item.id);
5976
+ }
5977
+ updateArrowsBoundToElements(moved, this.store);
5978
+ this.historyRecorder.commit();
5979
+ this.requestRender();
5980
+ }
5981
+ boundedSelection() {
5982
+ const out = [];
5983
+ for (const id of this.getSelectedIds()) {
5984
+ const el = this.store.getById(id);
5985
+ if (!el) continue;
5986
+ const bounds = getElementBounds(el);
5987
+ if (bounds) out.push({ id, el, bounds });
5988
+ }
5989
+ return out;
5990
+ }
5991
+ isMovable(el) {
5992
+ if (el.locked) return false;
5993
+ if (el.type === "arrow" && (el.fromBinding ?? el.toBinding)) return false;
5994
+ return true;
5995
+ }
5876
5996
  getRenderStats() {
5877
5997
  return this.renderLoop.getStats();
5878
5998
  }
@@ -6880,40 +7000,15 @@ var SelectTool = class {
6880
7000
  }
6881
7001
  }
6882
7002
  updateArrowsBoundTo(ids, ctx) {
6883
- const movedNonArrowIds = /* @__PURE__ */ new Set();
6884
- for (const id of ids) {
6885
- const el = ctx.store.getById(id);
6886
- if (el && el.type !== "arrow") movedNonArrowIds.add(id);
6887
- }
6888
- if (movedNonArrowIds.size === 0) return;
6889
- const updatedArrows = /* @__PURE__ */ new Set();
6890
- for (const id of movedNonArrowIds) {
6891
- const boundArrows = findBoundArrows(id, ctx.store);
6892
- for (const ba of boundArrows) {
6893
- if (updatedArrows.has(ba.id)) continue;
6894
- updatedArrows.add(ba.id);
6895
- const updates = updateBoundArrow(ba, ctx.store);
6896
- if (updates) ctx.store.update(ba.id, updates);
6897
- }
6898
- }
7003
+ updateArrowsBoundToElements(ids, ctx.store);
6899
7004
  }
6900
7005
  nudgeSelection(dx, dy, ctx) {
6901
7006
  let moved = false;
6902
7007
  for (const id of this._selectedIds) {
6903
7008
  const el = ctx.store.getById(id);
6904
7009
  if (!el || el.locked) continue;
6905
- if (el.type === "arrow") {
6906
- if (el.fromBinding || el.toBinding) continue;
6907
- ctx.store.update(id, {
6908
- position: { x: el.position.x + dx, y: el.position.y + dy },
6909
- from: { x: el.from.x + dx, y: el.from.y + dy },
6910
- to: { x: el.to.x + dx, y: el.to.y + dy }
6911
- });
6912
- } else {
6913
- ctx.store.update(id, {
6914
- position: { x: el.position.x + dx, y: el.position.y + dy }
6915
- });
6916
- }
7010
+ if (el.type === "arrow" && (el.fromBinding || el.toBinding)) continue;
7011
+ ctx.store.update(id, translateElementPatch(el, dx, dy));
6917
7012
  moved = true;
6918
7013
  }
6919
7014
  if (moved) {
@@ -8105,7 +8200,7 @@ var TemplateTool = class {
8105
8200
  };
8106
8201
 
8107
8202
  // src/index.ts
8108
- var VERSION = "0.31.1";
8203
+ var VERSION = "0.32.0";
8109
8204
  // Annotate the CommonJS export names for ESM import in node:
8110
8205
  0 && (module.exports = {
8111
8206
  ArrowTool,