@fieldnotes/core 0.32.0 → 0.34.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
@@ -20,6 +20,8 @@ interface BaseElement {
20
20
  zIndex: number;
21
21
  locked: boolean;
22
22
  layerId: string;
23
+ /** Optional flat group membership. Elements sharing a groupId select/move/delete as a unit. */
24
+ groupId?: string;
23
25
  }
24
26
  interface StrokeElement extends BaseElement {
25
27
  type: 'stroke';
@@ -200,6 +202,8 @@ interface ToolContext {
200
202
  activeLayerId?: string;
201
203
  isLayerVisible?: (layerId: string) => boolean;
202
204
  isLayerLocked?: (layerId: string) => boolean;
205
+ smartGuides?: boolean;
206
+ getVisibleRect?: () => Bounds;
203
207
  }
204
208
  interface PointerState {
205
209
  x: number;
@@ -460,6 +464,7 @@ declare class Viewport {
460
464
  private readonly marginViewport;
461
465
  private resizeObserver;
462
466
  private _snapToGrid;
467
+ private _smartGuides;
463
468
  private readonly _gridSize;
464
469
  private readonly renderLoop;
465
470
  private readonly domNodeManager;
@@ -474,6 +479,8 @@ declare class Viewport {
474
479
  get ctx(): CanvasRenderingContext2D | null;
475
480
  get snapToGrid(): boolean;
476
481
  setSnapToGrid(enabled: boolean): void;
482
+ get smartGuides(): boolean;
483
+ setSmartGuides(enabled: boolean): void;
477
484
  fitToContent(padding?: number): void;
478
485
  requestRender(): void;
479
486
  exportState(): CanvasState;
@@ -518,6 +525,8 @@ declare class Viewport {
518
525
  onSelectionChange(listener: () => void): () => void;
519
526
  getSelectionStyle(): ElementStyle | null;
520
527
  applyStyleToSelection(style: ElementStyle): void;
528
+ groupSelection(): void;
529
+ ungroupSelection(): void;
521
530
  alignSelection(edge: AlignEdge): void;
522
531
  distributeSelection(axis: DistributeAxis): void;
523
532
  private boundedSelection;
@@ -749,6 +758,9 @@ declare class SelectTool implements Tool {
749
758
  private ctx;
750
759
  private pendingSingleSelectId;
751
760
  private hasDragged;
761
+ private activeGuides;
762
+ private dragSnapTargets;
763
+ private dragVisibleRect;
752
764
  private resizeAspectRatio;
753
765
  private hoveredId;
754
766
  get selectedIds(): string[];
@@ -764,6 +776,7 @@ declare class SelectTool implements Tool {
764
776
  onPointerUp(_state: PointerState, ctx: ToolContext): void;
765
777
  onHover(state: PointerState, ctx: ToolContext): void;
766
778
  renderOverlay(canvasCtx: CanvasRenderingContext2D): void;
779
+ private renderGuideLines;
767
780
  private updateArrowsBoundTo;
768
781
  nudgeSelection(dx: number, dy: number, ctx: ToolContext): boolean;
769
782
  private updateHoverCursor;
@@ -979,6 +992,6 @@ declare class TemplateTool implements Tool {
979
992
  private notifyOptionsChange;
980
993
  }
981
994
 
982
- declare const VERSION = "0.32.0";
995
+ declare const VERSION = "0.34.0";
983
996
 
984
997
  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
@@ -20,6 +20,8 @@ interface BaseElement {
20
20
  zIndex: number;
21
21
  locked: boolean;
22
22
  layerId: string;
23
+ /** Optional flat group membership. Elements sharing a groupId select/move/delete as a unit. */
24
+ groupId?: string;
23
25
  }
24
26
  interface StrokeElement extends BaseElement {
25
27
  type: 'stroke';
@@ -200,6 +202,8 @@ interface ToolContext {
200
202
  activeLayerId?: string;
201
203
  isLayerVisible?: (layerId: string) => boolean;
202
204
  isLayerLocked?: (layerId: string) => boolean;
205
+ smartGuides?: boolean;
206
+ getVisibleRect?: () => Bounds;
203
207
  }
204
208
  interface PointerState {
205
209
  x: number;
@@ -460,6 +464,7 @@ declare class Viewport {
460
464
  private readonly marginViewport;
461
465
  private resizeObserver;
462
466
  private _snapToGrid;
467
+ private _smartGuides;
463
468
  private readonly _gridSize;
464
469
  private readonly renderLoop;
465
470
  private readonly domNodeManager;
@@ -474,6 +479,8 @@ declare class Viewport {
474
479
  get ctx(): CanvasRenderingContext2D | null;
475
480
  get snapToGrid(): boolean;
476
481
  setSnapToGrid(enabled: boolean): void;
482
+ get smartGuides(): boolean;
483
+ setSmartGuides(enabled: boolean): void;
477
484
  fitToContent(padding?: number): void;
478
485
  requestRender(): void;
479
486
  exportState(): CanvasState;
@@ -518,6 +525,8 @@ declare class Viewport {
518
525
  onSelectionChange(listener: () => void): () => void;
519
526
  getSelectionStyle(): ElementStyle | null;
520
527
  applyStyleToSelection(style: ElementStyle): void;
528
+ groupSelection(): void;
529
+ ungroupSelection(): void;
521
530
  alignSelection(edge: AlignEdge): void;
522
531
  distributeSelection(axis: DistributeAxis): void;
523
532
  private boundedSelection;
@@ -749,6 +758,9 @@ declare class SelectTool implements Tool {
749
758
  private ctx;
750
759
  private pendingSingleSelectId;
751
760
  private hasDragged;
761
+ private activeGuides;
762
+ private dragSnapTargets;
763
+ private dragVisibleRect;
752
764
  private resizeAspectRatio;
753
765
  private hoveredId;
754
766
  get selectedIds(): string[];
@@ -764,6 +776,7 @@ declare class SelectTool implements Tool {
764
776
  onPointerUp(_state: PointerState, ctx: ToolContext): void;
765
777
  onHover(state: PointerState, ctx: ToolContext): void;
766
778
  renderOverlay(canvasCtx: CanvasRenderingContext2D): void;
779
+ private renderGuideLines;
767
780
  private updateArrowsBoundTo;
768
781
  nudgeSelection(dx: number, dy: number, ctx: ToolContext): boolean;
769
782
  private updateHoverCursor;
@@ -979,6 +992,6 @@ declare class TemplateTool implements Tool {
979
992
  private notifyOptionsChange;
980
993
  }
981
994
 
982
- declare const VERSION = "0.32.0";
995
+ declare const VERSION = "0.34.0";
983
996
 
984
997
  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
@@ -940,6 +940,14 @@ var KeyboardActions = class {
940
940
  if (this.deps.isToolActive()) return;
941
941
  this.deps.fitToContent?.();
942
942
  }
943
+ group() {
944
+ if (this.deps.isToolActive()) return;
945
+ this.deps.group?.();
946
+ }
947
+ ungroup() {
948
+ if (this.deps.isToolActive()) return;
949
+ this.deps.ungroup?.();
950
+ }
943
951
  zOrder(operation) {
944
952
  if (this.deps.isToolActive()) return;
945
953
  this.flushPendingNudge();
@@ -973,6 +981,10 @@ var KeyboardActions = class {
973
981
  for (const el of source) {
974
982
  idMap.set(el.id, createId(el.type));
975
983
  }
984
+ const groupIdMap = /* @__PURE__ */ new Map();
985
+ for (const el of source) {
986
+ if (el.groupId && !groupIdMap.has(el.groupId)) groupIdMap.set(el.groupId, createId("group"));
987
+ }
976
988
  const newIds = [];
977
989
  const recorder = this.deps.getHistoryRecorder();
978
990
  recorder?.begin();
@@ -981,6 +993,7 @@ var KeyboardActions = class {
981
993
  const newId = idMap.get(el.id);
982
994
  if (!newId) continue;
983
995
  clone.id = newId;
996
+ if (clone.groupId) clone.groupId = groupIdMap.get(clone.groupId) ?? clone.groupId;
984
997
  clone.position = { x: clone.position.x + offset.x, y: clone.position.y + offset.y };
985
998
  if (clone.type === "arrow") {
986
999
  const arrow = clone;
@@ -1034,6 +1047,8 @@ var DEFAULT_BINDINGS = [
1034
1047
  ["zoom-in", ["mod+="]],
1035
1048
  ["zoom-out", ["mod+-"]],
1036
1049
  ["zoom-reset", ["mod+0"]],
1050
+ ["group", ["mod+g"]],
1051
+ ["ungroup", ["mod+shift+g"]],
1037
1052
  ["nudge-left", ["arrowleft"]],
1038
1053
  ["nudge-right", ["arrowright"]],
1039
1054
  ["nudge-up", ["arrowup"]],
@@ -1193,6 +1208,8 @@ var InputHandler = class {
1193
1208
  getHistoryStack: () => this.historyStack,
1194
1209
  isToolActive: () => this.isToolActive,
1195
1210
  fitToContent: options.fitToContent,
1211
+ group: options.group,
1212
+ ungroup: options.ungroup,
1196
1213
  getLastPointerWorld: () => this.lastPointerWorld()
1197
1214
  });
1198
1215
  this.shortcutMap = new ShortcutMap(options.shortcuts?.bindings);
@@ -1432,6 +1449,14 @@ var InputHandler = class {
1432
1449
  e.preventDefault();
1433
1450
  this.actions.zoomToFit();
1434
1451
  return;
1452
+ case "group":
1453
+ e.preventDefault();
1454
+ this.actions.group();
1455
+ return;
1456
+ case "ungroup":
1457
+ e.preventDefault();
1458
+ this.actions.ungroup();
1459
+ return;
1435
1460
  case "zoom-in":
1436
1461
  e.preventDefault();
1437
1462
  this.zoomByFactor(ZOOM_STEP);
@@ -3937,12 +3962,19 @@ var UpdateElementCommand = class {
3937
3962
  this.current = current;
3938
3963
  }
3939
3964
  execute(store) {
3940
- store.update(this.id, { ...this.current });
3965
+ store.update(this.id, diffPatch(this.previous, this.current));
3941
3966
  }
3942
3967
  undo(store) {
3943
- store.update(this.id, { ...this.previous });
3968
+ store.update(this.id, diffPatch(this.current, this.previous));
3944
3969
  }
3945
3970
  };
3971
+ function diffPatch(from, to) {
3972
+ const patch = { ...to };
3973
+ for (const key of Object.keys(from)) {
3974
+ if (!(key in to)) patch[key] = void 0;
3975
+ }
3976
+ return patch;
3977
+ }
3946
3978
  var BatchCommand = class {
3947
3979
  commands;
3948
3980
  constructor(commands) {
@@ -5495,7 +5527,9 @@ var Viewport = class {
5495
5527
  gridSize: this._gridSize,
5496
5528
  activeLayerId: this.layerManager.activeLayerId,
5497
5529
  isLayerVisible: (id) => this.layerManager.isLayerVisible(id),
5498
- isLayerLocked: (id) => this.layerManager.isLayerLocked(id)
5530
+ isLayerLocked: (id) => this.layerManager.isLayerLocked(id),
5531
+ smartGuides: false,
5532
+ getVisibleRect: () => this.camera.getVisibleRect(this.canvasEl.clientWidth, this.canvasEl.clientHeight)
5499
5533
  };
5500
5534
  this.inputHandler = new InputHandler(this.wrapper, this.camera, {
5501
5535
  toolManager: this.toolManager,
@@ -5503,6 +5537,8 @@ var Viewport = class {
5503
5537
  historyRecorder: this.historyRecorder,
5504
5538
  historyStack: this.history,
5505
5539
  fitToContent: () => this.fitToContent(),
5540
+ group: () => this.groupSelection(),
5541
+ ungroup: () => this.ungroupSelection(),
5506
5542
  shortcuts: options.shortcuts
5507
5543
  });
5508
5544
  this.domNodeManager = new DomNodeManager({
@@ -5599,6 +5635,7 @@ var Viewport = class {
5599
5635
  marginViewport;
5600
5636
  resizeObserver = null;
5601
5637
  _snapToGrid = false;
5638
+ _smartGuides = false;
5602
5639
  _gridSize;
5603
5640
  renderLoop;
5604
5641
  domNodeManager;
@@ -5619,6 +5656,13 @@ var Viewport = class {
5619
5656
  this._snapToGrid = enabled;
5620
5657
  this.toolContext.snapToGrid = enabled;
5621
5658
  }
5659
+ get smartGuides() {
5660
+ return this._smartGuides;
5661
+ }
5662
+ setSmartGuides(enabled) {
5663
+ this._smartGuides = enabled;
5664
+ this.toolContext.smartGuides = enabled;
5665
+ }
5622
5666
  fitToContent(padding = 40) {
5623
5667
  if (this.wrapper.clientWidth === 0 || this.wrapper.clientHeight === 0) return;
5624
5668
  const visibleElements = this.store.getAll().filter((el) => this.layerManager.isLayerVisible(el.layerId));
@@ -5832,6 +5876,26 @@ var Viewport = class {
5832
5876
  }
5833
5877
  this.historyRecorder.commit();
5834
5878
  }
5879
+ groupSelection() {
5880
+ const ids = this.getSelectedIds();
5881
+ if (ids.length < 2) return;
5882
+ const groupId = createId("group");
5883
+ this.historyRecorder.begin();
5884
+ for (const id of ids) {
5885
+ if (this.store.getById(id)) this.store.update(id, { groupId });
5886
+ }
5887
+ this.historyRecorder.commit();
5888
+ }
5889
+ ungroupSelection() {
5890
+ const ids = this.getSelectedIds();
5891
+ if (ids.length === 0) return;
5892
+ this.historyRecorder.begin();
5893
+ for (const id of ids) {
5894
+ const el = this.store.getById(id);
5895
+ if (el && el.groupId !== void 0) this.store.update(id, { groupId: void 0 });
5896
+ }
5897
+ this.historyRecorder.commit();
5898
+ }
5835
5899
  alignSelection(edge) {
5836
5900
  const bounded = this.boundedSelection();
5837
5901
  if (bounded.length < 2) return;
@@ -6541,6 +6605,26 @@ var EraserTool = class {
6541
6605
  }
6542
6606
  };
6543
6607
 
6608
+ // src/elements/group.ts
6609
+ function expandToGroups(ids, elements) {
6610
+ const byId = new Map(elements.map((e) => [e.id, e]));
6611
+ const groupIds = /* @__PURE__ */ new Set();
6612
+ for (const id of ids) {
6613
+ const g = byId.get(id)?.groupId;
6614
+ if (g) groupIds.add(g);
6615
+ }
6616
+ if (groupIds.size === 0) return ids;
6617
+ const idSet = new Set(ids);
6618
+ const result = [...ids];
6619
+ for (const el of elements) {
6620
+ if (el.groupId && groupIds.has(el.groupId) && !idSet.has(el.id)) {
6621
+ result.push(el.id);
6622
+ idSet.add(el.id);
6623
+ }
6624
+ }
6625
+ return result;
6626
+ }
6627
+
6544
6628
  // src/tools/arrow-handles.ts
6545
6629
  var BIND_THRESHOLD = 20;
6546
6630
  var HANDLE_RADIUS = 5;
@@ -6656,8 +6740,47 @@ function renderArrowHandles(canvasCtx, arrow, zoom) {
6656
6740
  }
6657
6741
  }
6658
6742
 
6743
+ // src/elements/snap-guides.ts
6744
+ function xAnchors(b) {
6745
+ return { lo: b.x, mid: b.x + b.w / 2, hi: b.x + b.w };
6746
+ }
6747
+ function yAnchors(b) {
6748
+ return { lo: b.y, mid: b.y + b.h / 2, hi: b.y + b.h };
6749
+ }
6750
+ function bestAxisSnap(moving, targets, anchorsFn, threshold) {
6751
+ let best = null;
6752
+ for (const t of targets) {
6753
+ const ta = anchorsFn(t);
6754
+ const pairs = [
6755
+ // colinear alignment: same-type edges/centers line up
6756
+ [ta.lo - moving.lo, ta.lo],
6757
+ [ta.mid - moving.mid, ta.mid],
6758
+ [ta.hi - moving.hi, ta.hi],
6759
+ // abutment: the moving box sits flush against the target's opposite edge
6760
+ [ta.lo - moving.hi, ta.lo],
6761
+ [ta.hi - moving.lo, ta.hi]
6762
+ ];
6763
+ for (const [delta, position] of pairs) {
6764
+ const abs = Math.abs(delta);
6765
+ if (abs <= threshold && (best === null || abs < Math.abs(best.delta))) {
6766
+ best = { delta, position };
6767
+ }
6768
+ }
6769
+ }
6770
+ return best;
6771
+ }
6772
+ function computeSnapGuides(moving, targets, threshold) {
6773
+ const xSnap = bestAxisSnap(xAnchors(moving), targets, xAnchors, threshold);
6774
+ const ySnap = bestAxisSnap(yAnchors(moving), targets, yAnchors, threshold);
6775
+ const guides = [];
6776
+ if (xSnap) guides.push({ axis: "x", position: xSnap.position });
6777
+ if (ySnap) guides.push({ axis: "y", position: ySnap.position });
6778
+ return { dx: xSnap?.delta ?? 0, dy: ySnap?.delta ?? 0, guides };
6779
+ }
6780
+
6659
6781
  // src/tools/select-tool.ts
6660
6782
  var HANDLE_SIZE = 8;
6783
+ var SNAP_PX = 6;
6661
6784
  var HANDLE_HIT_PADDING2 = 4;
6662
6785
  var SELECTION_PAD = 4;
6663
6786
  var MIN_ELEMENT_SIZE = 20;
@@ -6677,6 +6800,9 @@ var SelectTool = class {
6677
6800
  ctx = null;
6678
6801
  pendingSingleSelectId = null;
6679
6802
  hasDragged = false;
6803
+ activeGuides = [];
6804
+ dragSnapTargets = null;
6805
+ dragVisibleRect = null;
6680
6806
  resizeAspectRatio = 0;
6681
6807
  hoveredId = null;
6682
6808
  get selectedIds() {
@@ -6708,6 +6834,9 @@ var SelectTool = class {
6708
6834
  this.setSelectedIds([]);
6709
6835
  this.mode = { type: "idle" };
6710
6836
  this.hoveredId = null;
6837
+ this.activeGuides = [];
6838
+ this.dragSnapTargets = null;
6839
+ this.dragVisibleRect = null;
6711
6840
  ctx.setCursor?.("default");
6712
6841
  }
6713
6842
  snap(point, ctx) {
@@ -6716,6 +6845,8 @@ var SelectTool = class {
6716
6845
  onPointerDown(state, ctx) {
6717
6846
  this.ctx = ctx;
6718
6847
  this.setHovered(null, ctx);
6848
+ this.dragSnapTargets = null;
6849
+ this.dragVisibleRect = null;
6719
6850
  const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
6720
6851
  this.lastWorld = this.snap(world, ctx);
6721
6852
  this.currentWorld = world;
@@ -6759,18 +6890,20 @@ var SelectTool = class {
6759
6890
  this.hasDragged = false;
6760
6891
  const hit = this.hitTest(world, ctx);
6761
6892
  if (hit) {
6893
+ const all = ctx.store.getAll();
6762
6894
  const alreadySelected = this._selectedIds.includes(hit.id);
6763
6895
  if (state.shiftKey) {
6764
6896
  if (alreadySelected) {
6765
- this.setSelectedIds(this._selectedIds.filter((id) => id !== hit.id));
6897
+ const grp = new Set(expandToGroups([hit.id], all));
6898
+ this.setSelectedIds(this._selectedIds.filter((id) => !grp.has(id)));
6766
6899
  this.mode = { type: "idle" };
6767
6900
  } else {
6768
- this.setSelectedIds([...this._selectedIds, hit.id]);
6901
+ this.setSelectedIds(expandToGroups([...this._selectedIds, hit.id], all));
6769
6902
  this.mode = hit.locked ? { type: "idle" } : { type: "dragging" };
6770
6903
  }
6771
6904
  } else {
6772
6905
  if (!alreadySelected) {
6773
- this.setSelectedIds([hit.id]);
6906
+ this.setSelectedIds(expandToGroups([hit.id], all));
6774
6907
  } else if (this._selectedIds.length > 1) {
6775
6908
  this.pendingSingleSelectId = hit.id;
6776
6909
  }
@@ -6816,6 +6949,31 @@ var SelectTool = class {
6816
6949
  const dx = snapped.x - this.lastWorld.x;
6817
6950
  const dy = snapped.y - this.lastWorld.y;
6818
6951
  this.lastWorld = snapped;
6952
+ let adjDx = dx;
6953
+ let adjDy = dy;
6954
+ this.activeGuides = [];
6955
+ if (ctx.smartGuides) {
6956
+ if (this.dragSnapTargets === null) {
6957
+ const selSet = new Set(this._selectedIds);
6958
+ this.dragVisibleRect = ctx.getVisibleRect?.() ?? null;
6959
+ const candidates = (this.dragVisibleRect ? ctx.store.queryRect(this.dragVisibleRect) : ctx.store.getAll()).filter((el) => !selSet.has(el.id) && el.type !== "grid");
6960
+ const targets = [];
6961
+ for (const el of candidates) {
6962
+ const b = getElementBounds(el);
6963
+ if (b) targets.push(b);
6964
+ }
6965
+ this.dragSnapTargets = targets;
6966
+ }
6967
+ const selectedEls = this._selectedIds.map((id) => ctx.store.getById(id)).filter((el) => !!el && !el.locked);
6968
+ const base = getElementsBoundingBox(selectedEls);
6969
+ if (base) {
6970
+ const moving = { x: base.x + dx, y: base.y + dy, w: base.w, h: base.h };
6971
+ const res = computeSnapGuides(moving, this.dragSnapTargets, SNAP_PX / ctx.camera.zoom);
6972
+ adjDx = dx + res.dx;
6973
+ adjDy = dy + res.dy;
6974
+ this.activeGuides = res.guides;
6975
+ }
6976
+ }
6819
6977
  for (const id of this._selectedIds) {
6820
6978
  const el = ctx.store.getById(id);
6821
6979
  if (!el || el.locked) continue;
@@ -6824,13 +6982,13 @@ var SelectTool = class {
6824
6982
  continue;
6825
6983
  }
6826
6984
  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 }
6985
+ position: { x: el.position.x + adjDx, y: el.position.y + adjDy },
6986
+ from: { x: el.from.x + adjDx, y: el.from.y + adjDy },
6987
+ to: { x: el.to.x + adjDx, y: el.to.y + adjDy }
6830
6988
  });
6831
- } else if (ctx.gridType && "size" in el) {
6832
- const centerX = el.position.x + el.size.w / 2 + dx;
6833
- const centerY = el.position.y + el.size.h / 2 + dy;
6989
+ } else if (!ctx.smartGuides && ctx.gridType && "size" in el) {
6990
+ const centerX = el.position.x + el.size.w / 2 + adjDx;
6991
+ const centerY = el.position.y + el.size.h / 2 + adjDy;
6834
6992
  const snappedCenter = this.snap({ x: centerX, y: centerY }, ctx);
6835
6993
  ctx.store.update(id, {
6836
6994
  position: {
@@ -6840,7 +6998,7 @@ var SelectTool = class {
6840
6998
  });
6841
6999
  } else {
6842
7000
  ctx.store.update(id, {
6843
- position: { x: el.position.x + dx, y: el.position.y + dy }
7001
+ position: { x: el.position.x + adjDx, y: el.position.y + adjDy }
6844
7002
  });
6845
7003
  }
6846
7004
  }
@@ -6859,17 +7017,21 @@ var SelectTool = class {
6859
7017
  if (this.mode.type === "marquee") {
6860
7018
  const rect = this.getMarqueeRect();
6861
7019
  if (rect) {
6862
- this.setSelectedIds(this.findElementsInRect(rect, ctx));
7020
+ this.setSelectedIds(expandToGroups(this.findElementsInRect(rect, ctx), ctx.store.getAll()));
6863
7021
  }
6864
7022
  ctx.requestRender();
6865
7023
  }
6866
7024
  if (!this.hasDragged && this.pendingSingleSelectId !== null) {
6867
- this.setSelectedIds([this.pendingSingleSelectId]);
7025
+ this.setSelectedIds(expandToGroups([this.pendingSingleSelectId], ctx.store.getAll()));
6868
7026
  }
6869
7027
  this.pendingSingleSelectId = null;
6870
7028
  this.hasDragged = false;
6871
7029
  const resizedNoteId = this.mode.type === "resizing" ? this.mode.elementId : null;
6872
7030
  this.mode = { type: "idle" };
7031
+ this.activeGuides = [];
7032
+ this.dragSnapTargets = null;
7033
+ this.dragVisibleRect = null;
7034
+ ctx.requestRender();
6873
7035
  ctx.setCursor?.("default");
6874
7036
  if (resizedNoteId !== null) {
6875
7037
  const el = ctx.store.getById(resizedNoteId);
@@ -6917,6 +7079,32 @@ var SelectTool = class {
6917
7079
  }
6918
7080
  }
6919
7081
  }
7082
+ this.renderGuideLines(canvasCtx);
7083
+ }
7084
+ renderGuideLines(canvasCtx) {
7085
+ if (this.mode.type !== "dragging" || !this.ctx || this.activeGuides.length === 0) return;
7086
+ const zoom = this.ctx.camera.zoom;
7087
+ const rect = this.dragVisibleRect;
7088
+ canvasCtx.save();
7089
+ canvasCtx.strokeStyle = "#FF4081";
7090
+ canvasCtx.lineWidth = 1 / zoom;
7091
+ canvasCtx.setLineDash([]);
7092
+ for (const g of this.activeGuides) {
7093
+ canvasCtx.beginPath();
7094
+ if (g.axis === "x") {
7095
+ const y0 = rect ? rect.y : this.currentWorld.y - 1e5;
7096
+ const y1 = rect ? rect.y + rect.h : this.currentWorld.y + 1e5;
7097
+ canvasCtx.moveTo(g.position, y0);
7098
+ canvasCtx.lineTo(g.position, y1);
7099
+ } else {
7100
+ const x0 = rect ? rect.x : this.currentWorld.x - 1e5;
7101
+ const x1 = rect ? rect.x + rect.w : this.currentWorld.x + 1e5;
7102
+ canvasCtx.moveTo(x0, g.position);
7103
+ canvasCtx.lineTo(x1, g.position);
7104
+ }
7105
+ canvasCtx.stroke();
7106
+ }
7107
+ canvasCtx.restore();
6920
7108
  }
6921
7109
  updateArrowsBoundTo(ids, ctx) {
6922
7110
  updateArrowsBoundToElements(ids, ctx.store);
@@ -8119,7 +8307,7 @@ var TemplateTool = class {
8119
8307
  };
8120
8308
 
8121
8309
  // src/index.ts
8122
- var VERSION = "0.32.0";
8310
+ var VERSION = "0.34.0";
8123
8311
  export {
8124
8312
  ArrowTool,
8125
8313
  AutoSave,