@fieldnotes/core 0.33.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/README.md CHANGED
@@ -588,6 +588,22 @@ viewport.setSmartGuides(true); // enable
588
588
  viewport.setSmartGuides(false); // disable (default)
589
589
  ```
590
590
 
591
+ ## Grouping
592
+
593
+ Group elements so they select, move, delete, z-order, and align as a single unit.
594
+
595
+ - **`viewport.groupSelection()`** — groups the current selection under a new id.
596
+ - **`viewport.ungroupSelection()`** — dissolves any groups in the current selection.
597
+
598
+ Each is one undo step. Selecting any member selects its whole group, so to edit a single member individually, ungroup first. Pasting or duplicating a group keeps the copies grouped under a fresh id.
599
+
600
+ ```typescript
601
+ viewport.groupSelection(); // Ctrl/Cmd+G
602
+ viewport.ungroupSelection(); // Ctrl/Cmd+Shift+G
603
+ ```
604
+
605
+ The shortcuts are rebindable as `group` and `ungroup`.
606
+
591
607
  ## Built-in Interactions
592
608
 
593
609
  | Input | Action |
package/dist/index.cjs CHANGED
@@ -1021,6 +1021,14 @@ var KeyboardActions = class {
1021
1021
  if (this.deps.isToolActive()) return;
1022
1022
  this.deps.fitToContent?.();
1023
1023
  }
1024
+ group() {
1025
+ if (this.deps.isToolActive()) return;
1026
+ this.deps.group?.();
1027
+ }
1028
+ ungroup() {
1029
+ if (this.deps.isToolActive()) return;
1030
+ this.deps.ungroup?.();
1031
+ }
1024
1032
  zOrder(operation) {
1025
1033
  if (this.deps.isToolActive()) return;
1026
1034
  this.flushPendingNudge();
@@ -1054,6 +1062,10 @@ var KeyboardActions = class {
1054
1062
  for (const el of source) {
1055
1063
  idMap.set(el.id, createId(el.type));
1056
1064
  }
1065
+ const groupIdMap = /* @__PURE__ */ new Map();
1066
+ for (const el of source) {
1067
+ if (el.groupId && !groupIdMap.has(el.groupId)) groupIdMap.set(el.groupId, createId("group"));
1068
+ }
1057
1069
  const newIds = [];
1058
1070
  const recorder = this.deps.getHistoryRecorder();
1059
1071
  recorder?.begin();
@@ -1062,6 +1074,7 @@ var KeyboardActions = class {
1062
1074
  const newId = idMap.get(el.id);
1063
1075
  if (!newId) continue;
1064
1076
  clone.id = newId;
1077
+ if (clone.groupId) clone.groupId = groupIdMap.get(clone.groupId) ?? clone.groupId;
1065
1078
  clone.position = { x: clone.position.x + offset.x, y: clone.position.y + offset.y };
1066
1079
  if (clone.type === "arrow") {
1067
1080
  const arrow = clone;
@@ -1115,6 +1128,8 @@ var DEFAULT_BINDINGS = [
1115
1128
  ["zoom-in", ["mod+="]],
1116
1129
  ["zoom-out", ["mod+-"]],
1117
1130
  ["zoom-reset", ["mod+0"]],
1131
+ ["group", ["mod+g"]],
1132
+ ["ungroup", ["mod+shift+g"]],
1118
1133
  ["nudge-left", ["arrowleft"]],
1119
1134
  ["nudge-right", ["arrowright"]],
1120
1135
  ["nudge-up", ["arrowup"]],
@@ -1274,6 +1289,8 @@ var InputHandler = class {
1274
1289
  getHistoryStack: () => this.historyStack,
1275
1290
  isToolActive: () => this.isToolActive,
1276
1291
  fitToContent: options.fitToContent,
1292
+ group: options.group,
1293
+ ungroup: options.ungroup,
1277
1294
  getLastPointerWorld: () => this.lastPointerWorld()
1278
1295
  });
1279
1296
  this.shortcutMap = new ShortcutMap(options.shortcuts?.bindings);
@@ -1513,6 +1530,14 @@ var InputHandler = class {
1513
1530
  e.preventDefault();
1514
1531
  this.actions.zoomToFit();
1515
1532
  return;
1533
+ case "group":
1534
+ e.preventDefault();
1535
+ this.actions.group();
1536
+ return;
1537
+ case "ungroup":
1538
+ e.preventDefault();
1539
+ this.actions.ungroup();
1540
+ return;
1516
1541
  case "zoom-in":
1517
1542
  e.preventDefault();
1518
1543
  this.zoomByFactor(ZOOM_STEP);
@@ -4018,12 +4043,19 @@ var UpdateElementCommand = class {
4018
4043
  this.current = current;
4019
4044
  }
4020
4045
  execute(store) {
4021
- store.update(this.id, { ...this.current });
4046
+ store.update(this.id, diffPatch(this.previous, this.current));
4022
4047
  }
4023
4048
  undo(store) {
4024
- store.update(this.id, { ...this.previous });
4049
+ store.update(this.id, diffPatch(this.current, this.previous));
4025
4050
  }
4026
4051
  };
4052
+ function diffPatch(from, to) {
4053
+ const patch = { ...to };
4054
+ for (const key of Object.keys(from)) {
4055
+ if (!(key in to)) patch[key] = void 0;
4056
+ }
4057
+ return patch;
4058
+ }
4027
4059
  var BatchCommand = class {
4028
4060
  commands;
4029
4061
  constructor(commands) {
@@ -5586,6 +5618,8 @@ var Viewport = class {
5586
5618
  historyRecorder: this.historyRecorder,
5587
5619
  historyStack: this.history,
5588
5620
  fitToContent: () => this.fitToContent(),
5621
+ group: () => this.groupSelection(),
5622
+ ungroup: () => this.ungroupSelection(),
5589
5623
  shortcuts: options.shortcuts
5590
5624
  });
5591
5625
  this.domNodeManager = new DomNodeManager({
@@ -5923,6 +5957,26 @@ var Viewport = class {
5923
5957
  }
5924
5958
  this.historyRecorder.commit();
5925
5959
  }
5960
+ groupSelection() {
5961
+ const ids = this.getSelectedIds();
5962
+ if (ids.length < 2) return;
5963
+ const groupId = createId("group");
5964
+ this.historyRecorder.begin();
5965
+ for (const id of ids) {
5966
+ if (this.store.getById(id)) this.store.update(id, { groupId });
5967
+ }
5968
+ this.historyRecorder.commit();
5969
+ }
5970
+ ungroupSelection() {
5971
+ const ids = this.getSelectedIds();
5972
+ if (ids.length === 0) return;
5973
+ this.historyRecorder.begin();
5974
+ for (const id of ids) {
5975
+ const el = this.store.getById(id);
5976
+ if (el && el.groupId !== void 0) this.store.update(id, { groupId: void 0 });
5977
+ }
5978
+ this.historyRecorder.commit();
5979
+ }
5926
5980
  alignSelection(edge) {
5927
5981
  const bounded = this.boundedSelection();
5928
5982
  if (bounded.length < 2) return;
@@ -6632,6 +6686,26 @@ var EraserTool = class {
6632
6686
  }
6633
6687
  };
6634
6688
 
6689
+ // src/elements/group.ts
6690
+ function expandToGroups(ids, elements) {
6691
+ const byId = new Map(elements.map((e) => [e.id, e]));
6692
+ const groupIds = /* @__PURE__ */ new Set();
6693
+ for (const id of ids) {
6694
+ const g = byId.get(id)?.groupId;
6695
+ if (g) groupIds.add(g);
6696
+ }
6697
+ if (groupIds.size === 0) return ids;
6698
+ const idSet = new Set(ids);
6699
+ const result = [...ids];
6700
+ for (const el of elements) {
6701
+ if (el.groupId && groupIds.has(el.groupId) && !idSet.has(el.id)) {
6702
+ result.push(el.id);
6703
+ idSet.add(el.id);
6704
+ }
6705
+ }
6706
+ return result;
6707
+ }
6708
+
6635
6709
  // src/tools/arrow-handles.ts
6636
6710
  var BIND_THRESHOLD = 20;
6637
6711
  var HANDLE_RADIUS = 5;
@@ -6897,18 +6971,20 @@ var SelectTool = class {
6897
6971
  this.hasDragged = false;
6898
6972
  const hit = this.hitTest(world, ctx);
6899
6973
  if (hit) {
6974
+ const all = ctx.store.getAll();
6900
6975
  const alreadySelected = this._selectedIds.includes(hit.id);
6901
6976
  if (state.shiftKey) {
6902
6977
  if (alreadySelected) {
6903
- this.setSelectedIds(this._selectedIds.filter((id) => id !== hit.id));
6978
+ const grp = new Set(expandToGroups([hit.id], all));
6979
+ this.setSelectedIds(this._selectedIds.filter((id) => !grp.has(id)));
6904
6980
  this.mode = { type: "idle" };
6905
6981
  } else {
6906
- this.setSelectedIds([...this._selectedIds, hit.id]);
6982
+ this.setSelectedIds(expandToGroups([...this._selectedIds, hit.id], all));
6907
6983
  this.mode = hit.locked ? { type: "idle" } : { type: "dragging" };
6908
6984
  }
6909
6985
  } else {
6910
6986
  if (!alreadySelected) {
6911
- this.setSelectedIds([hit.id]);
6987
+ this.setSelectedIds(expandToGroups([hit.id], all));
6912
6988
  } else if (this._selectedIds.length > 1) {
6913
6989
  this.pendingSingleSelectId = hit.id;
6914
6990
  }
@@ -7022,12 +7098,12 @@ var SelectTool = class {
7022
7098
  if (this.mode.type === "marquee") {
7023
7099
  const rect = this.getMarqueeRect();
7024
7100
  if (rect) {
7025
- this.setSelectedIds(this.findElementsInRect(rect, ctx));
7101
+ this.setSelectedIds(expandToGroups(this.findElementsInRect(rect, ctx), ctx.store.getAll()));
7026
7102
  }
7027
7103
  ctx.requestRender();
7028
7104
  }
7029
7105
  if (!this.hasDragged && this.pendingSingleSelectId !== null) {
7030
- this.setSelectedIds([this.pendingSingleSelectId]);
7106
+ this.setSelectedIds(expandToGroups([this.pendingSingleSelectId], ctx.store.getAll()));
7031
7107
  }
7032
7108
  this.pendingSingleSelectId = null;
7033
7109
  this.hasDragged = false;
@@ -8312,7 +8388,7 @@ var TemplateTool = class {
8312
8388
  };
8313
8389
 
8314
8390
  // src/index.ts
8315
- var VERSION = "0.33.0";
8391
+ var VERSION = "0.34.0";
8316
8392
  // Annotate the CommonJS export names for ESM import in node:
8317
8393
  0 && (module.exports = {
8318
8394
  ArrowTool,