@fieldnotes/core 0.31.1 → 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/dist/index.d.cts CHANGED
@@ -200,6 +200,8 @@ interface ToolContext {
200
200
  activeLayerId?: string;
201
201
  isLayerVisible?: (layerId: string) => boolean;
202
202
  isLayerLocked?: (layerId: string) => boolean;
203
+ smartGuides?: boolean;
204
+ getVisibleRect?: () => Bounds;
203
205
  }
204
206
  interface PointerState {
205
207
  x: number;
@@ -410,6 +412,8 @@ interface ElementStyle {
410
412
  declare function styleToPatch(element: CanvasElement, style: ElementStyle): Partial<CanvasElement>;
411
413
  declare function getElementStyle(element: CanvasElement): ElementStyle;
412
414
 
415
+ type AlignEdge = 'left' | 'center-x' | 'right' | 'top' | 'middle' | 'bottom';
416
+ type DistributeAxis = 'horizontal' | 'vertical';
413
417
  interface GridInfo {
414
418
  gridType: 'square' | 'hex';
415
419
  hexOrientation: 'pointy' | 'flat';
@@ -458,6 +462,7 @@ declare class Viewport {
458
462
  private readonly marginViewport;
459
463
  private resizeObserver;
460
464
  private _snapToGrid;
465
+ private _smartGuides;
461
466
  private readonly _gridSize;
462
467
  private readonly renderLoop;
463
468
  private readonly domNodeManager;
@@ -472,6 +477,8 @@ declare class Viewport {
472
477
  get ctx(): CanvasRenderingContext2D | null;
473
478
  get snapToGrid(): boolean;
474
479
  setSnapToGrid(enabled: boolean): void;
480
+ get smartGuides(): boolean;
481
+ setSmartGuides(enabled: boolean): void;
475
482
  fitToContent(padding?: number): void;
476
483
  requestRender(): void;
477
484
  exportState(): CanvasState;
@@ -516,6 +523,10 @@ declare class Viewport {
516
523
  onSelectionChange(listener: () => void): () => void;
517
524
  getSelectionStyle(): ElementStyle | null;
518
525
  applyStyleToSelection(style: ElementStyle): void;
526
+ alignSelection(edge: AlignEdge): void;
527
+ distributeSelection(axis: DistributeAxis): void;
528
+ private boundedSelection;
529
+ private isMovable;
519
530
  getRenderStats(): RenderStatsSnapshot;
520
531
  logPerformance(intervalMs?: number): () => void;
521
532
  destroy(): void;
@@ -743,6 +754,9 @@ declare class SelectTool implements Tool {
743
754
  private ctx;
744
755
  private pendingSingleSelectId;
745
756
  private hasDragged;
757
+ private activeGuides;
758
+ private dragSnapTargets;
759
+ private dragVisibleRect;
746
760
  private resizeAspectRatio;
747
761
  private hoveredId;
748
762
  get selectedIds(): string[];
@@ -758,6 +772,7 @@ declare class SelectTool implements Tool {
758
772
  onPointerUp(_state: PointerState, ctx: ToolContext): void;
759
773
  onHover(state: PointerState, ctx: ToolContext): void;
760
774
  renderOverlay(canvasCtx: CanvasRenderingContext2D): void;
775
+ private renderGuideLines;
761
776
  private updateArrowsBoundTo;
762
777
  nudgeSelection(dx: number, dy: number, ctx: ToolContext): boolean;
763
778
  private updateHoverCursor;
@@ -973,6 +988,6 @@ declare class TemplateTool implements Tool {
973
988
  private notifyOptionsChange;
974
989
  }
975
990
 
976
- declare const VERSION = "0.31.1";
991
+ declare const VERSION = "0.33.0";
977
992
 
978
- export { type ActiveFormats, type ArrowElement, ArrowTool, type ArrowToolOptions, AutoSave, type AutoSaveOptions, type BackgroundOptions, type BackgroundPattern, type Binding, type Bounds, Camera, type CameraChangeInfo, type CameraOptions, type CanvasElement, type CanvasState, type Command, DEFAULT_NOTE_FONT_SIZE, ElementStore, type ElementStyle, type ElementType, type ElementUpdateEvent, EraserTool, type EraserToolOptions, type ExportImageOptions, type FontSizePreset, type GridElement, type GridInfo, HandTool, type HexOrientation, HistoryStack, type HistoryStackOptions, type HtmlElement, type ImageElement, ImageTool, type ImageToolOptions, type Layer, LayerManager, MeasureTool, type MeasureToolOptions, type Measurement, type NoteElement, NoteTool, type NoteToolOptions, PencilTool, type PencilToolOptions, type Point, type PointerState, type RenderStatsSnapshot, SelectTool, type ShapeElement, type ShapeKind, ShapeTool, type ShapeToolOptions, type ShortcutBindings, type ShortcutOptions, type ShortcutsApi, type Size, type StrokeElement, type StrokePoint, type TemplateElement, type TemplateShape, TemplateTool, type TemplateToolOptions, type TextElement, TextTool, type TextToolOptions, type Tool, type ToolContext, ToolManager, type ToolName, VERSION, Viewport, type ViewportOptions, boundsIntersect, createArrow, createGrid, createHtmlElement, createImage, createNote, createShape, createStroke, createTemplate, createText, drawHexPath, exportImage, getActiveFormats, getArrowBounds, getArrowControlPoint, getArrowMidpoint, getArrowTangentAngle, getBendFromPoint, getElementBounds, getElementStyle, getElementsBoundingBox, getHexCellsInCone, getHexCellsInLine, getHexCellsInRadius, getHexCellsInSquare, getHexDistance, isNearBezier, setFontSize, smartSnap, snapPoint, snapToHexCenter, styleToPatch, toggleBold, toggleItalic, toggleStrikethrough, toggleUnderline };
993
+ export { type ActiveFormats, type AlignEdge, type ArrowElement, ArrowTool, type ArrowToolOptions, AutoSave, type AutoSaveOptions, type BackgroundOptions, type BackgroundPattern, type Binding, type Bounds, Camera, type CameraChangeInfo, type CameraOptions, type CanvasElement, type CanvasState, type Command, DEFAULT_NOTE_FONT_SIZE, type DistributeAxis, ElementStore, type ElementStyle, type ElementType, type ElementUpdateEvent, EraserTool, type EraserToolOptions, type ExportImageOptions, type FontSizePreset, type GridElement, type GridInfo, HandTool, type HexOrientation, HistoryStack, type HistoryStackOptions, type HtmlElement, type ImageElement, ImageTool, type ImageToolOptions, type Layer, LayerManager, MeasureTool, type MeasureToolOptions, type Measurement, type NoteElement, NoteTool, type NoteToolOptions, PencilTool, type PencilToolOptions, type Point, type PointerState, type RenderStatsSnapshot, SelectTool, type ShapeElement, type ShapeKind, ShapeTool, type ShapeToolOptions, type ShortcutBindings, type ShortcutOptions, type ShortcutsApi, type Size, type StrokeElement, type StrokePoint, type TemplateElement, type TemplateShape, TemplateTool, type TemplateToolOptions, type TextElement, TextTool, type TextToolOptions, type Tool, type ToolContext, ToolManager, type ToolName, VERSION, Viewport, type ViewportOptions, boundsIntersect, createArrow, createGrid, createHtmlElement, createImage, createNote, createShape, createStroke, createTemplate, createText, drawHexPath, exportImage, getActiveFormats, getArrowBounds, getArrowControlPoint, getArrowMidpoint, getArrowTangentAngle, getBendFromPoint, getElementBounds, getElementStyle, getElementsBoundingBox, getHexCellsInCone, getHexCellsInLine, getHexCellsInRadius, getHexCellsInSquare, getHexDistance, isNearBezier, setFontSize, smartSnap, snapPoint, snapToHexCenter, styleToPatch, toggleBold, toggleItalic, toggleStrikethrough, toggleUnderline };
package/dist/index.d.ts CHANGED
@@ -200,6 +200,8 @@ interface ToolContext {
200
200
  activeLayerId?: string;
201
201
  isLayerVisible?: (layerId: string) => boolean;
202
202
  isLayerLocked?: (layerId: string) => boolean;
203
+ smartGuides?: boolean;
204
+ getVisibleRect?: () => Bounds;
203
205
  }
204
206
  interface PointerState {
205
207
  x: number;
@@ -410,6 +412,8 @@ interface ElementStyle {
410
412
  declare function styleToPatch(element: CanvasElement, style: ElementStyle): Partial<CanvasElement>;
411
413
  declare function getElementStyle(element: CanvasElement): ElementStyle;
412
414
 
415
+ type AlignEdge = 'left' | 'center-x' | 'right' | 'top' | 'middle' | 'bottom';
416
+ type DistributeAxis = 'horizontal' | 'vertical';
413
417
  interface GridInfo {
414
418
  gridType: 'square' | 'hex';
415
419
  hexOrientation: 'pointy' | 'flat';
@@ -458,6 +462,7 @@ declare class Viewport {
458
462
  private readonly marginViewport;
459
463
  private resizeObserver;
460
464
  private _snapToGrid;
465
+ private _smartGuides;
461
466
  private readonly _gridSize;
462
467
  private readonly renderLoop;
463
468
  private readonly domNodeManager;
@@ -472,6 +477,8 @@ declare class Viewport {
472
477
  get ctx(): CanvasRenderingContext2D | null;
473
478
  get snapToGrid(): boolean;
474
479
  setSnapToGrid(enabled: boolean): void;
480
+ get smartGuides(): boolean;
481
+ setSmartGuides(enabled: boolean): void;
475
482
  fitToContent(padding?: number): void;
476
483
  requestRender(): void;
477
484
  exportState(): CanvasState;
@@ -516,6 +523,10 @@ declare class Viewport {
516
523
  onSelectionChange(listener: () => void): () => void;
517
524
  getSelectionStyle(): ElementStyle | null;
518
525
  applyStyleToSelection(style: ElementStyle): void;
526
+ alignSelection(edge: AlignEdge): void;
527
+ distributeSelection(axis: DistributeAxis): void;
528
+ private boundedSelection;
529
+ private isMovable;
519
530
  getRenderStats(): RenderStatsSnapshot;
520
531
  logPerformance(intervalMs?: number): () => void;
521
532
  destroy(): void;
@@ -743,6 +754,9 @@ declare class SelectTool implements Tool {
743
754
  private ctx;
744
755
  private pendingSingleSelectId;
745
756
  private hasDragged;
757
+ private activeGuides;
758
+ private dragSnapTargets;
759
+ private dragVisibleRect;
746
760
  private resizeAspectRatio;
747
761
  private hoveredId;
748
762
  get selectedIds(): string[];
@@ -758,6 +772,7 @@ declare class SelectTool implements Tool {
758
772
  onPointerUp(_state: PointerState, ctx: ToolContext): void;
759
773
  onHover(state: PointerState, ctx: ToolContext): void;
760
774
  renderOverlay(canvasCtx: CanvasRenderingContext2D): void;
775
+ private renderGuideLines;
761
776
  private updateArrowsBoundTo;
762
777
  nudgeSelection(dx: number, dy: number, ctx: ToolContext): boolean;
763
778
  private updateHoverCursor;
@@ -973,6 +988,6 @@ declare class TemplateTool implements Tool {
973
988
  private notifyOptionsChange;
974
989
  }
975
990
 
976
- declare const VERSION = "0.31.1";
991
+ declare const VERSION = "0.33.0";
977
992
 
978
- export { type ActiveFormats, type ArrowElement, ArrowTool, type ArrowToolOptions, AutoSave, type AutoSaveOptions, type BackgroundOptions, type BackgroundPattern, type Binding, type Bounds, Camera, type CameraChangeInfo, type CameraOptions, type CanvasElement, type CanvasState, type Command, DEFAULT_NOTE_FONT_SIZE, ElementStore, type ElementStyle, type ElementType, type ElementUpdateEvent, EraserTool, type EraserToolOptions, type ExportImageOptions, type FontSizePreset, type GridElement, type GridInfo, HandTool, type HexOrientation, HistoryStack, type HistoryStackOptions, type HtmlElement, type ImageElement, ImageTool, type ImageToolOptions, type Layer, LayerManager, MeasureTool, type MeasureToolOptions, type Measurement, type NoteElement, NoteTool, type NoteToolOptions, PencilTool, type PencilToolOptions, type Point, type PointerState, type RenderStatsSnapshot, SelectTool, type ShapeElement, type ShapeKind, ShapeTool, type ShapeToolOptions, type ShortcutBindings, type ShortcutOptions, type ShortcutsApi, type Size, type StrokeElement, type StrokePoint, type TemplateElement, type TemplateShape, TemplateTool, type TemplateToolOptions, type TextElement, TextTool, type TextToolOptions, type Tool, type ToolContext, ToolManager, type ToolName, VERSION, Viewport, type ViewportOptions, boundsIntersect, createArrow, createGrid, createHtmlElement, createImage, createNote, createShape, createStroke, createTemplate, createText, drawHexPath, exportImage, getActiveFormats, getArrowBounds, getArrowControlPoint, getArrowMidpoint, getArrowTangentAngle, getBendFromPoint, getElementBounds, getElementStyle, getElementsBoundingBox, getHexCellsInCone, getHexCellsInLine, getHexCellsInRadius, getHexCellsInSquare, getHexDistance, isNearBezier, setFontSize, smartSnap, snapPoint, snapToHexCenter, styleToPatch, toggleBold, toggleItalic, toggleStrikethrough, toggleUnderline };
993
+ export { type ActiveFormats, type AlignEdge, type ArrowElement, ArrowTool, type ArrowToolOptions, AutoSave, type AutoSaveOptions, type BackgroundOptions, type BackgroundPattern, type Binding, type Bounds, Camera, type CameraChangeInfo, type CameraOptions, type CanvasElement, type CanvasState, type Command, DEFAULT_NOTE_FONT_SIZE, type DistributeAxis, ElementStore, type ElementStyle, type ElementType, type ElementUpdateEvent, EraserTool, type EraserToolOptions, type ExportImageOptions, type FontSizePreset, type GridElement, type GridInfo, HandTool, type HexOrientation, HistoryStack, type HistoryStackOptions, type HtmlElement, type ImageElement, ImageTool, type ImageToolOptions, type Layer, LayerManager, MeasureTool, type MeasureToolOptions, type Measurement, type NoteElement, NoteTool, type NoteToolOptions, PencilTool, type PencilToolOptions, type Point, type PointerState, type RenderStatsSnapshot, SelectTool, type ShapeElement, type ShapeKind, ShapeTool, type ShapeToolOptions, type ShortcutBindings, type ShortcutOptions, type ShortcutsApi, type Size, type StrokeElement, type StrokePoint, type TemplateElement, type TemplateShape, TemplateTool, type TemplateToolOptions, type TextElement, TextTool, type TextToolOptions, type Tool, type ToolContext, ToolManager, type ToolName, VERSION, Viewport, type ViewportOptions, boundsIntersect, createArrow, createGrid, createHtmlElement, createImage, createNote, createShape, createStroke, createTemplate, createText, drawHexPath, exportImage, getActiveFormats, getArrowBounds, getArrowControlPoint, getArrowMidpoint, getArrowTangentAngle, getBendFromPoint, getElementBounds, getElementStyle, getElementsBoundingBox, getHexCellsInCone, getHexCellsInLine, getHexCellsInRadius, getHexCellsInSquare, getHexDistance, isNearBezier, setFontSize, smartSnap, snapPoint, snapToHexCenter, styleToPatch, toggleBold, toggleItalic, toggleStrikethrough, toggleUnderline };
package/dist/index.js CHANGED
@@ -2270,6 +2270,23 @@ function distToBounds(point, bounds) {
2270
2270
  function findBoundArrows(elementId, store) {
2271
2271
  return store.getElementsByType("arrow").filter((a) => a.fromBinding?.elementId === elementId || a.toBinding?.elementId === elementId);
2272
2272
  }
2273
+ function updateArrowsBoundToElements(movedIds, store) {
2274
+ const movedNonArrowIds = /* @__PURE__ */ new Set();
2275
+ for (const id of movedIds) {
2276
+ const el = store.getById(id);
2277
+ if (el && el.type !== "arrow") movedNonArrowIds.add(id);
2278
+ }
2279
+ if (movedNonArrowIds.size === 0) return;
2280
+ const updated = /* @__PURE__ */ new Set();
2281
+ for (const id of movedNonArrowIds) {
2282
+ for (const ba of findBoundArrows(id, store)) {
2283
+ if (updated.has(ba.id)) continue;
2284
+ updated.add(ba.id);
2285
+ const updates = updateBoundArrow(ba, store);
2286
+ if (updates) store.update(ba.id, updates);
2287
+ }
2288
+ }
2289
+ }
2273
2290
  function updateBoundArrow(arrow, store) {
2274
2291
  if (!arrow.fromBinding && !arrow.toBinding) return null;
2275
2292
  const updates = {};
@@ -3697,6 +3714,19 @@ var NoteEditor = class {
3697
3714
  }
3698
3715
  };
3699
3716
 
3717
+ // src/elements/translate.ts
3718
+ function translateElementPatch(el, dx, dy) {
3719
+ const position = { x: el.position.x + dx, y: el.position.y + dy };
3720
+ if (el.type === "arrow") {
3721
+ return {
3722
+ position,
3723
+ from: { x: el.from.x + dx, y: el.from.y + dy },
3724
+ to: { x: el.to.x + dx, y: el.to.y + dy }
3725
+ };
3726
+ }
3727
+ return { position };
3728
+ }
3729
+
3700
3730
  // src/elements/arrow-label-editor.ts
3701
3731
  var ArrowLabelEditor = class {
3702
3732
  input = null;
@@ -5383,6 +5413,16 @@ function getElementStyle(element) {
5383
5413
  }
5384
5414
 
5385
5415
  // src/canvas/viewport.ts
5416
+ function unionBounds(list) {
5417
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
5418
+ for (const b of list) {
5419
+ minX = Math.min(minX, b.x);
5420
+ minY = Math.min(minY, b.y);
5421
+ maxX = Math.max(maxX, b.x + b.w);
5422
+ maxY = Math.max(maxY, b.y + b.h);
5423
+ }
5424
+ return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
5425
+ }
5386
5426
  var EMPTY_IDS = [];
5387
5427
  var ARROW_HIT_THRESHOLD = 10;
5388
5428
  function noop() {
@@ -5455,7 +5495,9 @@ var Viewport = class {
5455
5495
  gridSize: this._gridSize,
5456
5496
  activeLayerId: this.layerManager.activeLayerId,
5457
5497
  isLayerVisible: (id) => this.layerManager.isLayerVisible(id),
5458
- isLayerLocked: (id) => this.layerManager.isLayerLocked(id)
5498
+ isLayerLocked: (id) => this.layerManager.isLayerLocked(id),
5499
+ smartGuides: false,
5500
+ getVisibleRect: () => this.camera.getVisibleRect(this.canvasEl.clientWidth, this.canvasEl.clientHeight)
5459
5501
  };
5460
5502
  this.inputHandler = new InputHandler(this.wrapper, this.camera, {
5461
5503
  toolManager: this.toolManager,
@@ -5559,6 +5601,7 @@ var Viewport = class {
5559
5601
  marginViewport;
5560
5602
  resizeObserver = null;
5561
5603
  _snapToGrid = false;
5604
+ _smartGuides = false;
5562
5605
  _gridSize;
5563
5606
  renderLoop;
5564
5607
  domNodeManager;
@@ -5579,6 +5622,13 @@ var Viewport = class {
5579
5622
  this._snapToGrid = enabled;
5580
5623
  this.toolContext.snapToGrid = enabled;
5581
5624
  }
5625
+ get smartGuides() {
5626
+ return this._smartGuides;
5627
+ }
5628
+ setSmartGuides(enabled) {
5629
+ this._smartGuides = enabled;
5630
+ this.toolContext.smartGuides = enabled;
5631
+ }
5582
5632
  fitToContent(padding = 40) {
5583
5633
  if (this.wrapper.clientWidth === 0 || this.wrapper.clientHeight === 0) return;
5584
5634
  const visibleElements = this.store.getAll().filter((el) => this.layerManager.isLayerVisible(el.layerId));
@@ -5792,6 +5842,86 @@ var Viewport = class {
5792
5842
  }
5793
5843
  this.historyRecorder.commit();
5794
5844
  }
5845
+ alignSelection(edge) {
5846
+ const bounded = this.boundedSelection();
5847
+ if (bounded.length < 2) return;
5848
+ const B = unionBounds(bounded.map((e) => e.bounds));
5849
+ this.historyRecorder.begin();
5850
+ const moved = [];
5851
+ for (const { id, el, bounds: b } of bounded) {
5852
+ if (!this.isMovable(el)) continue;
5853
+ let dx = 0;
5854
+ let dy = 0;
5855
+ switch (edge) {
5856
+ case "left":
5857
+ dx = B.x - b.x;
5858
+ break;
5859
+ case "right":
5860
+ dx = B.x + B.w - (b.x + b.w);
5861
+ break;
5862
+ case "center-x":
5863
+ dx = B.x + B.w / 2 - (b.x + b.w / 2);
5864
+ break;
5865
+ case "top":
5866
+ dy = B.y - b.y;
5867
+ break;
5868
+ case "bottom":
5869
+ dy = B.y + B.h - (b.y + b.h);
5870
+ break;
5871
+ case "middle":
5872
+ dy = B.y + B.h / 2 - (b.y + b.h / 2);
5873
+ break;
5874
+ }
5875
+ if (dx === 0 && dy === 0) continue;
5876
+ this.store.update(id, translateElementPatch(el, dx, dy));
5877
+ moved.push(id);
5878
+ }
5879
+ updateArrowsBoundToElements(moved, this.store);
5880
+ this.historyRecorder.commit();
5881
+ this.requestRender();
5882
+ }
5883
+ distributeSelection(axis) {
5884
+ const bounded = this.boundedSelection();
5885
+ if (bounded.length < 3) return;
5886
+ const center = (b) => axis === "horizontal" ? b.x + b.w / 2 : b.y + b.h / 2;
5887
+ const sorted = [...bounded].sort((p, q) => center(p.bounds) - center(q.bounds));
5888
+ const first = sorted[0];
5889
+ const last = sorted[sorted.length - 1];
5890
+ if (!first || !last) return;
5891
+ const c0 = center(first.bounds);
5892
+ const cN = center(last.bounds);
5893
+ const n = sorted.length;
5894
+ this.historyRecorder.begin();
5895
+ const moved = [];
5896
+ for (let i = 1; i < n - 1; i++) {
5897
+ const item = sorted[i];
5898
+ if (!item || !this.isMovable(item.el)) continue;
5899
+ const target = c0 + i * (cN - c0) / (n - 1);
5900
+ const delta = target - center(item.bounds);
5901
+ if (delta === 0) continue;
5902
+ const [dx, dy] = axis === "horizontal" ? [delta, 0] : [0, delta];
5903
+ this.store.update(item.id, translateElementPatch(item.el, dx, dy));
5904
+ moved.push(item.id);
5905
+ }
5906
+ updateArrowsBoundToElements(moved, this.store);
5907
+ this.historyRecorder.commit();
5908
+ this.requestRender();
5909
+ }
5910
+ boundedSelection() {
5911
+ const out = [];
5912
+ for (const id of this.getSelectedIds()) {
5913
+ const el = this.store.getById(id);
5914
+ if (!el) continue;
5915
+ const bounds = getElementBounds(el);
5916
+ if (bounds) out.push({ id, el, bounds });
5917
+ }
5918
+ return out;
5919
+ }
5920
+ isMovable(el) {
5921
+ if (el.locked) return false;
5922
+ if (el.type === "arrow" && (el.fromBinding ?? el.toBinding)) return false;
5923
+ return true;
5924
+ }
5795
5925
  getRenderStats() {
5796
5926
  return this.renderLoop.getStats();
5797
5927
  }
@@ -6536,8 +6666,47 @@ function renderArrowHandles(canvasCtx, arrow, zoom) {
6536
6666
  }
6537
6667
  }
6538
6668
 
6669
+ // src/elements/snap-guides.ts
6670
+ function xAnchors(b) {
6671
+ return { lo: b.x, mid: b.x + b.w / 2, hi: b.x + b.w };
6672
+ }
6673
+ function yAnchors(b) {
6674
+ return { lo: b.y, mid: b.y + b.h / 2, hi: b.y + b.h };
6675
+ }
6676
+ function bestAxisSnap(moving, targets, anchorsFn, threshold) {
6677
+ let best = null;
6678
+ for (const t of targets) {
6679
+ const ta = anchorsFn(t);
6680
+ const pairs = [
6681
+ // colinear alignment: same-type edges/centers line up
6682
+ [ta.lo - moving.lo, ta.lo],
6683
+ [ta.mid - moving.mid, ta.mid],
6684
+ [ta.hi - moving.hi, ta.hi],
6685
+ // abutment: the moving box sits flush against the target's opposite edge
6686
+ [ta.lo - moving.hi, ta.lo],
6687
+ [ta.hi - moving.lo, ta.hi]
6688
+ ];
6689
+ for (const [delta, position] of pairs) {
6690
+ const abs = Math.abs(delta);
6691
+ if (abs <= threshold && (best === null || abs < Math.abs(best.delta))) {
6692
+ best = { delta, position };
6693
+ }
6694
+ }
6695
+ }
6696
+ return best;
6697
+ }
6698
+ function computeSnapGuides(moving, targets, threshold) {
6699
+ const xSnap = bestAxisSnap(xAnchors(moving), targets, xAnchors, threshold);
6700
+ const ySnap = bestAxisSnap(yAnchors(moving), targets, yAnchors, threshold);
6701
+ const guides = [];
6702
+ if (xSnap) guides.push({ axis: "x", position: xSnap.position });
6703
+ if (ySnap) guides.push({ axis: "y", position: ySnap.position });
6704
+ return { dx: xSnap?.delta ?? 0, dy: ySnap?.delta ?? 0, guides };
6705
+ }
6706
+
6539
6707
  // src/tools/select-tool.ts
6540
6708
  var HANDLE_SIZE = 8;
6709
+ var SNAP_PX = 6;
6541
6710
  var HANDLE_HIT_PADDING2 = 4;
6542
6711
  var SELECTION_PAD = 4;
6543
6712
  var MIN_ELEMENT_SIZE = 20;
@@ -6557,6 +6726,9 @@ var SelectTool = class {
6557
6726
  ctx = null;
6558
6727
  pendingSingleSelectId = null;
6559
6728
  hasDragged = false;
6729
+ activeGuides = [];
6730
+ dragSnapTargets = null;
6731
+ dragVisibleRect = null;
6560
6732
  resizeAspectRatio = 0;
6561
6733
  hoveredId = null;
6562
6734
  get selectedIds() {
@@ -6588,6 +6760,9 @@ var SelectTool = class {
6588
6760
  this.setSelectedIds([]);
6589
6761
  this.mode = { type: "idle" };
6590
6762
  this.hoveredId = null;
6763
+ this.activeGuides = [];
6764
+ this.dragSnapTargets = null;
6765
+ this.dragVisibleRect = null;
6591
6766
  ctx.setCursor?.("default");
6592
6767
  }
6593
6768
  snap(point, ctx) {
@@ -6596,6 +6771,8 @@ var SelectTool = class {
6596
6771
  onPointerDown(state, ctx) {
6597
6772
  this.ctx = ctx;
6598
6773
  this.setHovered(null, ctx);
6774
+ this.dragSnapTargets = null;
6775
+ this.dragVisibleRect = null;
6599
6776
  const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
6600
6777
  this.lastWorld = this.snap(world, ctx);
6601
6778
  this.currentWorld = world;
@@ -6696,6 +6873,31 @@ var SelectTool = class {
6696
6873
  const dx = snapped.x - this.lastWorld.x;
6697
6874
  const dy = snapped.y - this.lastWorld.y;
6698
6875
  this.lastWorld = snapped;
6876
+ let adjDx = dx;
6877
+ let adjDy = dy;
6878
+ this.activeGuides = [];
6879
+ if (ctx.smartGuides) {
6880
+ if (this.dragSnapTargets === null) {
6881
+ const selSet = new Set(this._selectedIds);
6882
+ this.dragVisibleRect = ctx.getVisibleRect?.() ?? null;
6883
+ const candidates = (this.dragVisibleRect ? ctx.store.queryRect(this.dragVisibleRect) : ctx.store.getAll()).filter((el) => !selSet.has(el.id) && el.type !== "grid");
6884
+ const targets = [];
6885
+ for (const el of candidates) {
6886
+ const b = getElementBounds(el);
6887
+ if (b) targets.push(b);
6888
+ }
6889
+ this.dragSnapTargets = targets;
6890
+ }
6891
+ const selectedEls = this._selectedIds.map((id) => ctx.store.getById(id)).filter((el) => !!el && !el.locked);
6892
+ const base = getElementsBoundingBox(selectedEls);
6893
+ if (base) {
6894
+ const moving = { x: base.x + dx, y: base.y + dy, w: base.w, h: base.h };
6895
+ const res = computeSnapGuides(moving, this.dragSnapTargets, SNAP_PX / ctx.camera.zoom);
6896
+ adjDx = dx + res.dx;
6897
+ adjDy = dy + res.dy;
6898
+ this.activeGuides = res.guides;
6899
+ }
6900
+ }
6699
6901
  for (const id of this._selectedIds) {
6700
6902
  const el = ctx.store.getById(id);
6701
6903
  if (!el || el.locked) continue;
@@ -6704,13 +6906,13 @@ var SelectTool = class {
6704
6906
  continue;
6705
6907
  }
6706
6908
  ctx.store.update(id, {
6707
- position: { x: el.position.x + dx, y: el.position.y + dy },
6708
- from: { x: el.from.x + dx, y: el.from.y + dy },
6709
- to: { x: el.to.x + dx, y: el.to.y + dy }
6909
+ position: { x: el.position.x + adjDx, y: el.position.y + adjDy },
6910
+ from: { x: el.from.x + adjDx, y: el.from.y + adjDy },
6911
+ to: { x: el.to.x + adjDx, y: el.to.y + adjDy }
6710
6912
  });
6711
- } else if (ctx.gridType && "size" in el) {
6712
- const centerX = el.position.x + el.size.w / 2 + dx;
6713
- const centerY = el.position.y + el.size.h / 2 + dy;
6913
+ } else if (!ctx.smartGuides && ctx.gridType && "size" in el) {
6914
+ const centerX = el.position.x + el.size.w / 2 + adjDx;
6915
+ const centerY = el.position.y + el.size.h / 2 + adjDy;
6714
6916
  const snappedCenter = this.snap({ x: centerX, y: centerY }, ctx);
6715
6917
  ctx.store.update(id, {
6716
6918
  position: {
@@ -6720,7 +6922,7 @@ var SelectTool = class {
6720
6922
  });
6721
6923
  } else {
6722
6924
  ctx.store.update(id, {
6723
- position: { x: el.position.x + dx, y: el.position.y + dy }
6925
+ position: { x: el.position.x + adjDx, y: el.position.y + adjDy }
6724
6926
  });
6725
6927
  }
6726
6928
  }
@@ -6750,6 +6952,10 @@ var SelectTool = class {
6750
6952
  this.hasDragged = false;
6751
6953
  const resizedNoteId = this.mode.type === "resizing" ? this.mode.elementId : null;
6752
6954
  this.mode = { type: "idle" };
6955
+ this.activeGuides = [];
6956
+ this.dragSnapTargets = null;
6957
+ this.dragVisibleRect = null;
6958
+ ctx.requestRender();
6753
6959
  ctx.setCursor?.("default");
6754
6960
  if (resizedNoteId !== null) {
6755
6961
  const el = ctx.store.getById(resizedNoteId);
@@ -6797,42 +7003,43 @@ var SelectTool = class {
6797
7003
  }
6798
7004
  }
6799
7005
  }
7006
+ this.renderGuideLines(canvasCtx);
6800
7007
  }
6801
- updateArrowsBoundTo(ids, ctx) {
6802
- const movedNonArrowIds = /* @__PURE__ */ new Set();
6803
- for (const id of ids) {
6804
- const el = ctx.store.getById(id);
6805
- if (el && el.type !== "arrow") movedNonArrowIds.add(id);
6806
- }
6807
- if (movedNonArrowIds.size === 0) return;
6808
- const updatedArrows = /* @__PURE__ */ new Set();
6809
- for (const id of movedNonArrowIds) {
6810
- const boundArrows = findBoundArrows(id, ctx.store);
6811
- for (const ba of boundArrows) {
6812
- if (updatedArrows.has(ba.id)) continue;
6813
- updatedArrows.add(ba.id);
6814
- const updates = updateBoundArrow(ba, ctx.store);
6815
- if (updates) ctx.store.update(ba.id, updates);
7008
+ renderGuideLines(canvasCtx) {
7009
+ if (this.mode.type !== "dragging" || !this.ctx || this.activeGuides.length === 0) return;
7010
+ const zoom = this.ctx.camera.zoom;
7011
+ const rect = this.dragVisibleRect;
7012
+ canvasCtx.save();
7013
+ canvasCtx.strokeStyle = "#FF4081";
7014
+ canvasCtx.lineWidth = 1 / zoom;
7015
+ canvasCtx.setLineDash([]);
7016
+ for (const g of this.activeGuides) {
7017
+ canvasCtx.beginPath();
7018
+ if (g.axis === "x") {
7019
+ const y0 = rect ? rect.y : this.currentWorld.y - 1e5;
7020
+ const y1 = rect ? rect.y + rect.h : this.currentWorld.y + 1e5;
7021
+ canvasCtx.moveTo(g.position, y0);
7022
+ canvasCtx.lineTo(g.position, y1);
7023
+ } else {
7024
+ const x0 = rect ? rect.x : this.currentWorld.x - 1e5;
7025
+ const x1 = rect ? rect.x + rect.w : this.currentWorld.x + 1e5;
7026
+ canvasCtx.moveTo(x0, g.position);
7027
+ canvasCtx.lineTo(x1, g.position);
6816
7028
  }
7029
+ canvasCtx.stroke();
6817
7030
  }
7031
+ canvasCtx.restore();
7032
+ }
7033
+ updateArrowsBoundTo(ids, ctx) {
7034
+ updateArrowsBoundToElements(ids, ctx.store);
6818
7035
  }
6819
7036
  nudgeSelection(dx, dy, ctx) {
6820
7037
  let moved = false;
6821
7038
  for (const id of this._selectedIds) {
6822
7039
  const el = ctx.store.getById(id);
6823
7040
  if (!el || el.locked) continue;
6824
- if (el.type === "arrow") {
6825
- if (el.fromBinding || el.toBinding) continue;
6826
- ctx.store.update(id, {
6827
- position: { x: el.position.x + dx, y: el.position.y + dy },
6828
- from: { x: el.from.x + dx, y: el.from.y + dy },
6829
- to: { x: el.to.x + dx, y: el.to.y + dy }
6830
- });
6831
- } else {
6832
- ctx.store.update(id, {
6833
- position: { x: el.position.x + dx, y: el.position.y + dy }
6834
- });
6835
- }
7041
+ if (el.type === "arrow" && (el.fromBinding || el.toBinding)) continue;
7042
+ ctx.store.update(id, translateElementPatch(el, dx, dy));
6836
7043
  moved = true;
6837
7044
  }
6838
7045
  if (moved) {
@@ -8024,7 +8231,7 @@ var TemplateTool = class {
8024
8231
  };
8025
8232
 
8026
8233
  // src/index.ts
8027
- var VERSION = "0.31.1";
8234
+ var VERSION = "0.33.0";
8028
8235
  export {
8029
8236
  ArrowTool,
8030
8237
  AutoSave,