@fieldnotes/core 0.32.0 → 0.33.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
@@ -579,6 +579,15 @@ viewport.distributeSelection('horizontal'); // equal horizontal spacing
579
579
 
580
580
  Grids are ignored by both operations.
581
581
 
582
+ ## Smart Alignment Guides
583
+
584
+ Call `viewport.setSmartGuides(true)` to enable drag-time alignment snapping. While dragging a selection, its edges and centers snap to the edges and centers of nearby visible elements (within 6 screen pixels), and guide lines are drawn at each matched alignment. Smart guides replace grid snapping for the duration of the drag; the result is still committed as a single undo step.
585
+
586
+ ```typescript
587
+ viewport.setSmartGuides(true); // enable
588
+ viewport.setSmartGuides(false); // disable (default)
589
+ ```
590
+
582
591
  ## Built-in Interactions
583
592
 
584
593
  | Input | Action |
package/dist/index.cjs CHANGED
@@ -5576,7 +5576,9 @@ var Viewport = class {
5576
5576
  gridSize: this._gridSize,
5577
5577
  activeLayerId: this.layerManager.activeLayerId,
5578
5578
  isLayerVisible: (id) => this.layerManager.isLayerVisible(id),
5579
- isLayerLocked: (id) => this.layerManager.isLayerLocked(id)
5579
+ isLayerLocked: (id) => this.layerManager.isLayerLocked(id),
5580
+ smartGuides: false,
5581
+ getVisibleRect: () => this.camera.getVisibleRect(this.canvasEl.clientWidth, this.canvasEl.clientHeight)
5580
5582
  };
5581
5583
  this.inputHandler = new InputHandler(this.wrapper, this.camera, {
5582
5584
  toolManager: this.toolManager,
@@ -5680,6 +5682,7 @@ var Viewport = class {
5680
5682
  marginViewport;
5681
5683
  resizeObserver = null;
5682
5684
  _snapToGrid = false;
5685
+ _smartGuides = false;
5683
5686
  _gridSize;
5684
5687
  renderLoop;
5685
5688
  domNodeManager;
@@ -5700,6 +5703,13 @@ var Viewport = class {
5700
5703
  this._snapToGrid = enabled;
5701
5704
  this.toolContext.snapToGrid = enabled;
5702
5705
  }
5706
+ get smartGuides() {
5707
+ return this._smartGuides;
5708
+ }
5709
+ setSmartGuides(enabled) {
5710
+ this._smartGuides = enabled;
5711
+ this.toolContext.smartGuides = enabled;
5712
+ }
5703
5713
  fitToContent(padding = 40) {
5704
5714
  if (this.wrapper.clientWidth === 0 || this.wrapper.clientHeight === 0) return;
5705
5715
  const visibleElements = this.store.getAll().filter((el) => this.layerManager.isLayerVisible(el.layerId));
@@ -6737,8 +6747,47 @@ function renderArrowHandles(canvasCtx, arrow, zoom) {
6737
6747
  }
6738
6748
  }
6739
6749
 
6750
+ // src/elements/snap-guides.ts
6751
+ function xAnchors(b) {
6752
+ return { lo: b.x, mid: b.x + b.w / 2, hi: b.x + b.w };
6753
+ }
6754
+ function yAnchors(b) {
6755
+ return { lo: b.y, mid: b.y + b.h / 2, hi: b.y + b.h };
6756
+ }
6757
+ function bestAxisSnap(moving, targets, anchorsFn, threshold) {
6758
+ let best = null;
6759
+ for (const t of targets) {
6760
+ const ta = anchorsFn(t);
6761
+ const pairs = [
6762
+ // colinear alignment: same-type edges/centers line up
6763
+ [ta.lo - moving.lo, ta.lo],
6764
+ [ta.mid - moving.mid, ta.mid],
6765
+ [ta.hi - moving.hi, ta.hi],
6766
+ // abutment: the moving box sits flush against the target's opposite edge
6767
+ [ta.lo - moving.hi, ta.lo],
6768
+ [ta.hi - moving.lo, ta.hi]
6769
+ ];
6770
+ for (const [delta, position] of pairs) {
6771
+ const abs = Math.abs(delta);
6772
+ if (abs <= threshold && (best === null || abs < Math.abs(best.delta))) {
6773
+ best = { delta, position };
6774
+ }
6775
+ }
6776
+ }
6777
+ return best;
6778
+ }
6779
+ function computeSnapGuides(moving, targets, threshold) {
6780
+ const xSnap = bestAxisSnap(xAnchors(moving), targets, xAnchors, threshold);
6781
+ const ySnap = bestAxisSnap(yAnchors(moving), targets, yAnchors, threshold);
6782
+ const guides = [];
6783
+ if (xSnap) guides.push({ axis: "x", position: xSnap.position });
6784
+ if (ySnap) guides.push({ axis: "y", position: ySnap.position });
6785
+ return { dx: xSnap?.delta ?? 0, dy: ySnap?.delta ?? 0, guides };
6786
+ }
6787
+
6740
6788
  // src/tools/select-tool.ts
6741
6789
  var HANDLE_SIZE = 8;
6790
+ var SNAP_PX = 6;
6742
6791
  var HANDLE_HIT_PADDING2 = 4;
6743
6792
  var SELECTION_PAD = 4;
6744
6793
  var MIN_ELEMENT_SIZE = 20;
@@ -6758,6 +6807,9 @@ var SelectTool = class {
6758
6807
  ctx = null;
6759
6808
  pendingSingleSelectId = null;
6760
6809
  hasDragged = false;
6810
+ activeGuides = [];
6811
+ dragSnapTargets = null;
6812
+ dragVisibleRect = null;
6761
6813
  resizeAspectRatio = 0;
6762
6814
  hoveredId = null;
6763
6815
  get selectedIds() {
@@ -6789,6 +6841,9 @@ var SelectTool = class {
6789
6841
  this.setSelectedIds([]);
6790
6842
  this.mode = { type: "idle" };
6791
6843
  this.hoveredId = null;
6844
+ this.activeGuides = [];
6845
+ this.dragSnapTargets = null;
6846
+ this.dragVisibleRect = null;
6792
6847
  ctx.setCursor?.("default");
6793
6848
  }
6794
6849
  snap(point, ctx) {
@@ -6797,6 +6852,8 @@ var SelectTool = class {
6797
6852
  onPointerDown(state, ctx) {
6798
6853
  this.ctx = ctx;
6799
6854
  this.setHovered(null, ctx);
6855
+ this.dragSnapTargets = null;
6856
+ this.dragVisibleRect = null;
6800
6857
  const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
6801
6858
  this.lastWorld = this.snap(world, ctx);
6802
6859
  this.currentWorld = world;
@@ -6897,6 +6954,31 @@ var SelectTool = class {
6897
6954
  const dx = snapped.x - this.lastWorld.x;
6898
6955
  const dy = snapped.y - this.lastWorld.y;
6899
6956
  this.lastWorld = snapped;
6957
+ let adjDx = dx;
6958
+ let adjDy = dy;
6959
+ this.activeGuides = [];
6960
+ if (ctx.smartGuides) {
6961
+ if (this.dragSnapTargets === null) {
6962
+ const selSet = new Set(this._selectedIds);
6963
+ this.dragVisibleRect = ctx.getVisibleRect?.() ?? null;
6964
+ const candidates = (this.dragVisibleRect ? ctx.store.queryRect(this.dragVisibleRect) : ctx.store.getAll()).filter((el) => !selSet.has(el.id) && el.type !== "grid");
6965
+ const targets = [];
6966
+ for (const el of candidates) {
6967
+ const b = getElementBounds(el);
6968
+ if (b) targets.push(b);
6969
+ }
6970
+ this.dragSnapTargets = targets;
6971
+ }
6972
+ const selectedEls = this._selectedIds.map((id) => ctx.store.getById(id)).filter((el) => !!el && !el.locked);
6973
+ const base = getElementsBoundingBox(selectedEls);
6974
+ if (base) {
6975
+ const moving = { x: base.x + dx, y: base.y + dy, w: base.w, h: base.h };
6976
+ const res = computeSnapGuides(moving, this.dragSnapTargets, SNAP_PX / ctx.camera.zoom);
6977
+ adjDx = dx + res.dx;
6978
+ adjDy = dy + res.dy;
6979
+ this.activeGuides = res.guides;
6980
+ }
6981
+ }
6900
6982
  for (const id of this._selectedIds) {
6901
6983
  const el = ctx.store.getById(id);
6902
6984
  if (!el || el.locked) continue;
@@ -6905,13 +6987,13 @@ var SelectTool = class {
6905
6987
  continue;
6906
6988
  }
6907
6989
  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 }
6990
+ position: { x: el.position.x + adjDx, y: el.position.y + adjDy },
6991
+ from: { x: el.from.x + adjDx, y: el.from.y + adjDy },
6992
+ to: { x: el.to.x + adjDx, y: el.to.y + adjDy }
6911
6993
  });
6912
- } else if (ctx.gridType && "size" in el) {
6913
- const centerX = el.position.x + el.size.w / 2 + dx;
6914
- const centerY = el.position.y + el.size.h / 2 + dy;
6994
+ } else if (!ctx.smartGuides && ctx.gridType && "size" in el) {
6995
+ const centerX = el.position.x + el.size.w / 2 + adjDx;
6996
+ const centerY = el.position.y + el.size.h / 2 + adjDy;
6915
6997
  const snappedCenter = this.snap({ x: centerX, y: centerY }, ctx);
6916
6998
  ctx.store.update(id, {
6917
6999
  position: {
@@ -6921,7 +7003,7 @@ var SelectTool = class {
6921
7003
  });
6922
7004
  } else {
6923
7005
  ctx.store.update(id, {
6924
- position: { x: el.position.x + dx, y: el.position.y + dy }
7006
+ position: { x: el.position.x + adjDx, y: el.position.y + adjDy }
6925
7007
  });
6926
7008
  }
6927
7009
  }
@@ -6951,6 +7033,10 @@ var SelectTool = class {
6951
7033
  this.hasDragged = false;
6952
7034
  const resizedNoteId = this.mode.type === "resizing" ? this.mode.elementId : null;
6953
7035
  this.mode = { type: "idle" };
7036
+ this.activeGuides = [];
7037
+ this.dragSnapTargets = null;
7038
+ this.dragVisibleRect = null;
7039
+ ctx.requestRender();
6954
7040
  ctx.setCursor?.("default");
6955
7041
  if (resizedNoteId !== null) {
6956
7042
  const el = ctx.store.getById(resizedNoteId);
@@ -6998,6 +7084,32 @@ var SelectTool = class {
6998
7084
  }
6999
7085
  }
7000
7086
  }
7087
+ this.renderGuideLines(canvasCtx);
7088
+ }
7089
+ renderGuideLines(canvasCtx) {
7090
+ if (this.mode.type !== "dragging" || !this.ctx || this.activeGuides.length === 0) return;
7091
+ const zoom = this.ctx.camera.zoom;
7092
+ const rect = this.dragVisibleRect;
7093
+ canvasCtx.save();
7094
+ canvasCtx.strokeStyle = "#FF4081";
7095
+ canvasCtx.lineWidth = 1 / zoom;
7096
+ canvasCtx.setLineDash([]);
7097
+ for (const g of this.activeGuides) {
7098
+ canvasCtx.beginPath();
7099
+ if (g.axis === "x") {
7100
+ const y0 = rect ? rect.y : this.currentWorld.y - 1e5;
7101
+ const y1 = rect ? rect.y + rect.h : this.currentWorld.y + 1e5;
7102
+ canvasCtx.moveTo(g.position, y0);
7103
+ canvasCtx.lineTo(g.position, y1);
7104
+ } else {
7105
+ const x0 = rect ? rect.x : this.currentWorld.x - 1e5;
7106
+ const x1 = rect ? rect.x + rect.w : this.currentWorld.x + 1e5;
7107
+ canvasCtx.moveTo(x0, g.position);
7108
+ canvasCtx.lineTo(x1, g.position);
7109
+ }
7110
+ canvasCtx.stroke();
7111
+ }
7112
+ canvasCtx.restore();
7001
7113
  }
7002
7114
  updateArrowsBoundTo(ids, ctx) {
7003
7115
  updateArrowsBoundToElements(ids, ctx.store);
@@ -8200,7 +8312,7 @@ var TemplateTool = class {
8200
8312
  };
8201
8313
 
8202
8314
  // src/index.ts
8203
- var VERSION = "0.32.0";
8315
+ var VERSION = "0.33.0";
8204
8316
  // Annotate the CommonJS export names for ESM import in node:
8205
8317
  0 && (module.exports = {
8206
8318
  ArrowTool,