@fieldnotes/core 0.15.0 → 0.17.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.cjs CHANGED
@@ -920,9 +920,228 @@ function createId(prefix) {
920
920
  return `${prefix}_${Date.now().toString(36)}_${(counter++).toString(36)}`;
921
921
  }
922
922
 
923
+ // src/canvas/keyboard-actions.ts
924
+ var KeyboardActions = class {
925
+ constructor(deps) {
926
+ this.deps = deps;
927
+ }
928
+ clipboard = [];
929
+ pasteCount = 0;
930
+ nudgeTimer = null;
931
+ dispose() {
932
+ this.flushPendingNudge();
933
+ }
934
+ selectTool() {
935
+ const tm = this.deps.getToolManager();
936
+ const ctx = this.deps.getToolContext();
937
+ if (!tm || !ctx) return null;
938
+ const tool = tm.activeTool;
939
+ if (tool?.name !== "select") return null;
940
+ return { tool, ctx };
941
+ }
942
+ nudge(dx, dy, byCell) {
943
+ if (this.deps.isToolActive()) return false;
944
+ const sel = this.selectTool();
945
+ if (!sel) return false;
946
+ if (sel.tool.selectedIds.length === 0) return false;
947
+ const step = byCell ? sel.ctx.gridSize ?? 10 : 1;
948
+ if (this.nudgeTimer === null) {
949
+ this.deps.getHistoryRecorder()?.begin();
950
+ } else {
951
+ clearTimeout(this.nudgeTimer);
952
+ }
953
+ const moved = sel.tool.nudgeSelection(dx * step, dy * step, sel.ctx);
954
+ this.nudgeTimer = setTimeout(() => this.flushPendingNudge(), 400);
955
+ return moved;
956
+ }
957
+ flushPendingNudge() {
958
+ if (this.nudgeTimer === null) return;
959
+ clearTimeout(this.nudgeTimer);
960
+ this.nudgeTimer = null;
961
+ this.deps.getHistoryRecorder()?.commit();
962
+ }
963
+ deleteSelected() {
964
+ this.flushPendingNudge();
965
+ const sel = this.selectTool();
966
+ if (!sel) return;
967
+ const ids = sel.tool.selectedIds;
968
+ if (ids.length === 0) return;
969
+ const recorder = this.deps.getHistoryRecorder();
970
+ recorder?.begin();
971
+ for (const id of ids) {
972
+ sel.ctx.store.remove(id);
973
+ }
974
+ recorder?.commit();
975
+ sel.ctx.requestRender();
976
+ }
977
+ undo() {
978
+ this.flushPendingNudge();
979
+ const ctx = this.deps.getToolContext();
980
+ const stack = this.deps.getHistoryStack();
981
+ if (!stack || !ctx) return;
982
+ const recorder = this.deps.getHistoryRecorder();
983
+ recorder?.pause();
984
+ stack.undo(ctx.store);
985
+ recorder?.resume();
986
+ ctx.requestRender();
987
+ }
988
+ redo() {
989
+ this.flushPendingNudge();
990
+ const ctx = this.deps.getToolContext();
991
+ const stack = this.deps.getHistoryStack();
992
+ if (!stack || !ctx) return;
993
+ const recorder = this.deps.getHistoryRecorder();
994
+ recorder?.pause();
995
+ stack.redo(ctx.store);
996
+ recorder?.resume();
997
+ ctx.requestRender();
998
+ }
999
+ copy() {
1000
+ if (this.deps.isToolActive()) return;
1001
+ const sel = this.selectTool();
1002
+ if (!sel) return;
1003
+ const ids = sel.tool.selectedIds;
1004
+ if (ids.length === 0) return;
1005
+ this.clipboard = [];
1006
+ for (const id of ids) {
1007
+ const el = sel.ctx.store.getById(id);
1008
+ if (el) this.clipboard.push(structuredClone(el));
1009
+ }
1010
+ this.pasteCount = 0;
1011
+ }
1012
+ paste() {
1013
+ this.flushPendingNudge();
1014
+ if (this.clipboard.length === 0 || this.deps.isToolActive()) return;
1015
+ const sel = this.selectTool();
1016
+ if (!sel) return;
1017
+ this.pasteCount++;
1018
+ this.insertClones(this.clipboard, this.pasteCount * 20, sel);
1019
+ }
1020
+ duplicate() {
1021
+ this.flushPendingNudge();
1022
+ if (this.deps.isToolActive()) return;
1023
+ const sel = this.selectTool();
1024
+ if (!sel) return;
1025
+ const source = [];
1026
+ for (const id of sel.tool.selectedIds) {
1027
+ const el = sel.ctx.store.getById(id);
1028
+ if (el) source.push(el);
1029
+ }
1030
+ if (source.length === 0) return;
1031
+ this.insertClones(source, 20, sel);
1032
+ }
1033
+ deselect() {
1034
+ if (this.deps.isToolActive()) return;
1035
+ const sel = this.selectTool();
1036
+ if (!sel) return;
1037
+ if (sel.tool.selectedIds.length === 0) return;
1038
+ sel.tool.setSelection([]);
1039
+ sel.ctx.requestRender();
1040
+ }
1041
+ selectAll() {
1042
+ if (this.deps.isToolActive()) return;
1043
+ const tm = this.deps.getToolManager();
1044
+ const ctx = this.deps.getToolContext();
1045
+ if (!tm || !ctx) return;
1046
+ if (tm.activeTool?.name !== "select") {
1047
+ ctx.switchTool?.("select");
1048
+ }
1049
+ const sel = this.selectTool();
1050
+ if (!sel) return;
1051
+ const ids = sel.ctx.store.getAll().filter(
1052
+ (el) => !el.locked && (sel.ctx.isLayerVisible?.(el.layerId) ?? true) && !(sel.ctx.isLayerLocked?.(el.layerId) ?? false)
1053
+ ).map((el) => el.id);
1054
+ sel.tool.setSelection(ids);
1055
+ sel.ctx.requestRender();
1056
+ }
1057
+ zoomToFit() {
1058
+ if (this.deps.isToolActive()) return;
1059
+ this.deps.fitToContent?.();
1060
+ }
1061
+ zOrder(operation) {
1062
+ this.flushPendingNudge();
1063
+ const sel = this.selectTool();
1064
+ if (!sel) return;
1065
+ const ids = sel.tool.selectedIds;
1066
+ if (ids.length === 0) return;
1067
+ const recorder = this.deps.getHistoryRecorder();
1068
+ recorder?.begin();
1069
+ for (const id of ids) {
1070
+ switch (operation) {
1071
+ case "forward":
1072
+ sel.ctx.store.bringForward(id);
1073
+ break;
1074
+ case "backward":
1075
+ sel.ctx.store.sendBackward(id);
1076
+ break;
1077
+ case "front":
1078
+ sel.ctx.store.bringToFront(id);
1079
+ break;
1080
+ case "back":
1081
+ sel.ctx.store.sendToBack(id);
1082
+ break;
1083
+ }
1084
+ }
1085
+ recorder?.commit();
1086
+ sel.ctx.requestRender();
1087
+ }
1088
+ insertClones(source, offset, sel) {
1089
+ const idMap = /* @__PURE__ */ new Map();
1090
+ for (const el of source) {
1091
+ idMap.set(el.id, createId(el.type));
1092
+ }
1093
+ const newIds = [];
1094
+ const recorder = this.deps.getHistoryRecorder();
1095
+ recorder?.begin();
1096
+ for (const el of source) {
1097
+ const clone = structuredClone(el);
1098
+ const newId = idMap.get(el.id);
1099
+ if (!newId) continue;
1100
+ clone.id = newId;
1101
+ clone.position = { x: clone.position.x + offset, y: clone.position.y + offset };
1102
+ if (clone.type === "arrow") {
1103
+ const arrow = clone;
1104
+ arrow.from = { x: arrow.from.x + offset, y: arrow.from.y + offset };
1105
+ arrow.to = { x: arrow.to.x + offset, y: arrow.to.y + offset };
1106
+ delete arrow.cachedControlPoint;
1107
+ if (arrow.fromBinding) {
1108
+ const newTarget = idMap.get(arrow.fromBinding.elementId);
1109
+ if (newTarget) {
1110
+ arrow.fromBinding = { elementId: newTarget };
1111
+ } else {
1112
+ delete arrow.fromBinding;
1113
+ }
1114
+ }
1115
+ if (arrow.toBinding) {
1116
+ const newTarget = idMap.get(arrow.toBinding.elementId);
1117
+ if (newTarget) {
1118
+ arrow.toBinding = { elementId: newTarget };
1119
+ } else {
1120
+ delete arrow.toBinding;
1121
+ }
1122
+ }
1123
+ }
1124
+ if (sel.ctx.activeLayerId) {
1125
+ clone.layerId = sel.ctx.activeLayerId;
1126
+ }
1127
+ sel.ctx.store.add(clone);
1128
+ newIds.push(clone.id);
1129
+ }
1130
+ recorder?.commit();
1131
+ sel.tool.setSelection(newIds);
1132
+ sel.ctx.requestRender();
1133
+ }
1134
+ };
1135
+
923
1136
  // src/canvas/input-handler.ts
924
1137
  var ZOOM_SENSITIVITY = 1e-3;
925
1138
  var MIDDLE_BUTTON = 1;
1139
+ var NUDGE_KEYS = {
1140
+ ArrowLeft: [-1, 0],
1141
+ ArrowRight: [1, 0],
1142
+ ArrowUp: [0, -1],
1143
+ ArrowDown: [0, 1]
1144
+ };
926
1145
  var InputHandler = class {
927
1146
  constructor(element, camera, options = {}) {
928
1147
  this.element = element;
@@ -931,6 +1150,14 @@ var InputHandler = class {
931
1150
  this.toolContext = options.toolContext ?? null;
932
1151
  this.historyRecorder = options.historyRecorder ?? null;
933
1152
  this.historyStack = options.historyStack ?? null;
1153
+ this.actions = new KeyboardActions({
1154
+ getToolManager: () => this.toolManager,
1155
+ getToolContext: () => this.toolContext,
1156
+ getHistoryRecorder: () => this.historyRecorder,
1157
+ getHistoryStack: () => this.historyStack,
1158
+ isToolActive: () => this.isToolActive,
1159
+ fitToContent: options.fitToContent
1160
+ });
934
1161
  this.element.style.touchAction = "none";
935
1162
  this.bind();
936
1163
  }
@@ -949,13 +1176,16 @@ var InputHandler = class {
949
1176
  inputFilter = new InputFilter();
950
1177
  deferredDown = null;
951
1178
  abortController = new AbortController();
952
- clipboard = [];
953
- pasteCount = 0;
1179
+ actions;
954
1180
  setToolManager(toolManager, toolContext) {
955
1181
  this.toolManager = toolManager;
956
1182
  this.toolContext = toolContext;
957
1183
  }
1184
+ flushPendingHistory() {
1185
+ this.actions.flushPendingNudge();
1186
+ }
958
1187
  destroy() {
1188
+ this.actions.dispose();
959
1189
  this.abortController.abort();
960
1190
  this.inputFilter.reset();
961
1191
  this.deferredDown = null;
@@ -1061,36 +1291,61 @@ var InputHandler = class {
1061
1291
  }
1062
1292
  };
1063
1293
  onKeyDown = (e) => {
1064
- if (e.target?.isContentEditable) return;
1294
+ const target = e.target;
1295
+ if (target?.isContentEditable) return;
1296
+ const tag = target?.tagName;
1297
+ if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return;
1065
1298
  if (e.key === " ") {
1066
1299
  this.spaceHeld = true;
1067
1300
  }
1068
1301
  if (e.key === "Delete" || e.key === "Backspace") {
1069
- this.deleteSelected();
1302
+ this.actions.deleteSelected();
1303
+ }
1304
+ if (e.key === "Escape") {
1305
+ this.actions.deselect();
1070
1306
  }
1071
1307
  if ((e.ctrlKey || e.metaKey) && e.key === "z" && !e.shiftKey) {
1072
1308
  e.preventDefault();
1073
- this.handleUndo();
1309
+ this.actions.undo();
1074
1310
  }
1075
1311
  if ((e.ctrlKey || e.metaKey) && (e.key === "y" || e.key === "z" && e.shiftKey)) {
1076
1312
  e.preventDefault();
1077
- this.handleRedo();
1313
+ this.actions.redo();
1314
+ }
1315
+ if ((e.ctrlKey || e.metaKey) && e.key === "a") {
1316
+ e.preventDefault();
1317
+ this.actions.selectAll();
1078
1318
  }
1079
1319
  if ((e.ctrlKey || e.metaKey) && e.key === "c") {
1080
1320
  e.preventDefault();
1081
- this.handleCopy();
1321
+ this.actions.copy();
1082
1322
  }
1083
1323
  if ((e.ctrlKey || e.metaKey) && e.key === "v") {
1084
1324
  e.preventDefault();
1085
- this.handlePaste();
1325
+ this.actions.paste();
1326
+ }
1327
+ if ((e.ctrlKey || e.metaKey) && e.key === "d") {
1328
+ e.preventDefault();
1329
+ this.actions.duplicate();
1086
1330
  }
1087
1331
  if (e.key === "]") {
1088
1332
  e.preventDefault();
1089
- this.handleZOrder(e.ctrlKey || e.metaKey ? "front" : "forward");
1333
+ this.actions.zOrder(e.ctrlKey || e.metaKey ? "front" : "forward");
1090
1334
  }
1091
1335
  if (e.key === "[") {
1092
1336
  e.preventDefault();
1093
- this.handleZOrder(e.ctrlKey || e.metaKey ? "back" : "backward");
1337
+ this.actions.zOrder(e.ctrlKey || e.metaKey ? "back" : "backward");
1338
+ }
1339
+ if (e.shiftKey && e.code === "Digit1" && !e.ctrlKey && !e.metaKey && !e.altKey) {
1340
+ e.preventDefault();
1341
+ this.actions.zoomToFit();
1342
+ }
1343
+ const nudgeDelta = NUDGE_KEYS[e.key];
1344
+ if (nudgeDelta) {
1345
+ const [dx, dy] = nudgeDelta;
1346
+ if (this.actions.nudge(dx, dy, e.shiftKey)) {
1347
+ e.preventDefault();
1348
+ }
1094
1349
  }
1095
1350
  };
1096
1351
  onKeyUp = (e) => {
@@ -1154,6 +1409,7 @@ var InputHandler = class {
1154
1409
  }
1155
1410
  dispatchToolDown(e) {
1156
1411
  if (!this.toolManager || !this.toolContext) return;
1412
+ this.actions.flushPendingNudge();
1157
1413
  this.historyRecorder?.begin();
1158
1414
  this.isToolActive = true;
1159
1415
  this.toolManager.handlePointerDown(this.toPointerState(e), this.toolContext);
@@ -1174,127 +1430,6 @@ var InputHandler = class {
1174
1430
  this.toolManager.handlePointerUp(this.toPointerState(e), this.toolContext);
1175
1431
  this.historyRecorder?.commit();
1176
1432
  }
1177
- deleteSelected() {
1178
- if (!this.toolManager || !this.toolContext) return;
1179
- const tool = this.toolManager.activeTool;
1180
- if (tool?.name !== "select") return;
1181
- const selectTool = tool;
1182
- const ids = selectTool.selectedIds;
1183
- if (ids.length === 0) return;
1184
- this.historyRecorder?.begin();
1185
- for (const id of ids) {
1186
- this.toolContext.store.remove(id);
1187
- }
1188
- this.historyRecorder?.commit();
1189
- this.toolContext.requestRender();
1190
- }
1191
- handleUndo() {
1192
- if (!this.historyStack || !this.toolContext) return;
1193
- this.historyRecorder?.pause();
1194
- this.historyStack.undo(this.toolContext.store);
1195
- this.historyRecorder?.resume();
1196
- this.toolContext.requestRender();
1197
- }
1198
- handleRedo() {
1199
- if (!this.historyStack || !this.toolContext) return;
1200
- this.historyRecorder?.pause();
1201
- this.historyStack.redo(this.toolContext.store);
1202
- this.historyRecorder?.resume();
1203
- this.toolContext.requestRender();
1204
- }
1205
- handleCopy() {
1206
- if (!this.toolManager || !this.toolContext || this.isToolActive) return;
1207
- const tool = this.toolManager.activeTool;
1208
- if (tool?.name !== "select") return;
1209
- const selectTool = tool;
1210
- const ids = selectTool.selectedIds;
1211
- if (ids.length === 0) return;
1212
- this.clipboard = [];
1213
- for (const id of ids) {
1214
- const el = this.toolContext.store.getById(id);
1215
- if (el) this.clipboard.push(structuredClone(el));
1216
- }
1217
- this.pasteCount = 0;
1218
- }
1219
- handlePaste() {
1220
- if (!this.toolManager || !this.toolContext || this.clipboard.length === 0 || this.isToolActive)
1221
- return;
1222
- const tool = this.toolManager.activeTool;
1223
- if (tool?.name !== "select") return;
1224
- const selectTool = tool;
1225
- this.pasteCount++;
1226
- const offset = this.pasteCount * 20;
1227
- const idMap = /* @__PURE__ */ new Map();
1228
- for (const el of this.clipboard) {
1229
- idMap.set(el.id, createId(el.type));
1230
- }
1231
- const newIds = [];
1232
- this.historyRecorder?.begin();
1233
- for (const el of this.clipboard) {
1234
- const clone = structuredClone(el);
1235
- const newId = idMap.get(el.id);
1236
- if (!newId) continue;
1237
- clone.id = newId;
1238
- clone.position = { x: clone.position.x + offset, y: clone.position.y + offset };
1239
- if (clone.type === "arrow") {
1240
- const arrow = clone;
1241
- arrow.from = { x: arrow.from.x + offset, y: arrow.from.y + offset };
1242
- arrow.to = { x: arrow.to.x + offset, y: arrow.to.y + offset };
1243
- delete arrow.cachedControlPoint;
1244
- if (arrow.fromBinding) {
1245
- const newTarget = idMap.get(arrow.fromBinding.elementId);
1246
- if (newTarget) {
1247
- arrow.fromBinding = { elementId: newTarget };
1248
- } else {
1249
- delete arrow.fromBinding;
1250
- }
1251
- }
1252
- if (arrow.toBinding) {
1253
- const newTarget = idMap.get(arrow.toBinding.elementId);
1254
- if (newTarget) {
1255
- arrow.toBinding = { elementId: newTarget };
1256
- } else {
1257
- delete arrow.toBinding;
1258
- }
1259
- }
1260
- }
1261
- if (this.toolContext.activeLayerId) {
1262
- clone.layerId = this.toolContext.activeLayerId;
1263
- }
1264
- this.toolContext.store.add(clone);
1265
- newIds.push(clone.id);
1266
- }
1267
- this.historyRecorder?.commit();
1268
- selectTool.setSelection(newIds);
1269
- this.toolContext.requestRender();
1270
- }
1271
- handleZOrder(operation) {
1272
- if (!this.toolManager || !this.toolContext) return;
1273
- const tool = this.toolManager.activeTool;
1274
- if (tool?.name !== "select") return;
1275
- const selectTool = tool;
1276
- const ids = selectTool.selectedIds;
1277
- if (ids.length === 0) return;
1278
- this.historyRecorder?.begin();
1279
- for (const id of ids) {
1280
- switch (operation) {
1281
- case "forward":
1282
- this.toolContext.store.bringForward(id);
1283
- break;
1284
- case "backward":
1285
- this.toolContext.store.sendBackward(id);
1286
- break;
1287
- case "front":
1288
- this.toolContext.store.bringToFront(id);
1289
- break;
1290
- case "back":
1291
- this.toolContext.store.sendToBack(id);
1292
- break;
1293
- }
1294
- }
1295
- this.historyRecorder?.commit();
1296
- this.toolContext.requestRender();
1297
- }
1298
1433
  cancelToolIfActive(e) {
1299
1434
  if (this.isToolActive) {
1300
1435
  this.dispatchToolUp(e);
@@ -3247,6 +3382,26 @@ var NoteEditor = class {
3247
3382
  }
3248
3383
  };
3249
3384
 
3385
+ // src/elements/bounds.ts
3386
+ function getElementsBoundingBox(elements) {
3387
+ let minX = Infinity;
3388
+ let minY = Infinity;
3389
+ let maxX = -Infinity;
3390
+ let maxY = -Infinity;
3391
+ let found = false;
3392
+ for (const el of elements) {
3393
+ const b = getElementBounds(el);
3394
+ if (!b) continue;
3395
+ found = true;
3396
+ if (b.x < minX) minX = b.x;
3397
+ if (b.y < minY) minY = b.y;
3398
+ if (b.x + b.w > maxX) maxX = b.x + b.w;
3399
+ if (b.y + b.h > maxY) maxY = b.y + b.h;
3400
+ }
3401
+ if (!found) return null;
3402
+ return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
3403
+ }
3404
+
3250
3405
  // src/tools/tool-manager.ts
3251
3406
  var ToolManager = class {
3252
3407
  tools = /* @__PURE__ */ new Map();
@@ -3402,16 +3557,64 @@ var BatchCommand = class {
3402
3557
  }
3403
3558
  };
3404
3559
 
3560
+ // src/history/layer-commands.ts
3561
+ var CreateLayerCommand = class {
3562
+ constructor(manager, layer) {
3563
+ this.manager = manager;
3564
+ this.layer = layer;
3565
+ }
3566
+ execute(_store) {
3567
+ this.manager.addLayerDirect(this.layer);
3568
+ }
3569
+ undo(_store) {
3570
+ this.manager.removeLayerDirect(this.layer.id);
3571
+ }
3572
+ };
3573
+ var RemoveLayerCommand = class {
3574
+ constructor(manager, layer) {
3575
+ this.manager = manager;
3576
+ this.layer = layer;
3577
+ }
3578
+ execute(_store) {
3579
+ this.manager.removeLayerDirect(this.layer.id);
3580
+ }
3581
+ undo(_store) {
3582
+ this.manager.addLayerDirect(this.layer);
3583
+ }
3584
+ };
3585
+ var UpdateLayerCommand = class {
3586
+ constructor(manager, layerId, previous, current) {
3587
+ this.manager = manager;
3588
+ this.layerId = layerId;
3589
+ this.previous = previous;
3590
+ this.current = current;
3591
+ }
3592
+ execute(_store) {
3593
+ this.manager.updateLayerDirect(this.layerId, { ...this.current });
3594
+ }
3595
+ undo(_store) {
3596
+ this.manager.updateLayerDirect(this.layerId, { ...this.previous });
3597
+ }
3598
+ };
3599
+
3405
3600
  // src/history/history-recorder.ts
3406
3601
  var HistoryRecorder = class {
3407
- constructor(store, stack) {
3602
+ constructor(store, stack, layerManager) {
3408
3603
  this.store = store;
3409
3604
  this.stack = stack;
3605
+ this.layerManager = layerManager;
3410
3606
  this.unsubscribers = [
3411
3607
  store.on("add", (el) => this.onAdd(el)),
3412
3608
  store.on("remove", (el) => this.onRemove(el)),
3413
3609
  store.on("update", ({ previous, current }) => this.onUpdate(previous, current))
3414
3610
  ];
3611
+ if (layerManager) {
3612
+ this.unsubscribers.push(
3613
+ layerManager.on("create", (layer) => this.onLayerCreate(layer)),
3614
+ layerManager.on("remove", (layer) => this.onLayerRemove(layer)),
3615
+ layerManager.on("update", ({ previous, current }) => this.onLayerUpdate(previous, current))
3616
+ );
3617
+ }
3415
3618
  }
3416
3619
  recording = true;
3417
3620
  transaction = null;
@@ -3424,6 +3627,9 @@ var HistoryRecorder = class {
3424
3627
  this.recording = true;
3425
3628
  }
3426
3629
  begin() {
3630
+ if (this.transaction !== null) {
3631
+ this.commit();
3632
+ }
3427
3633
  this.transaction = [];
3428
3634
  this.updateSnapshots.clear();
3429
3635
  }
@@ -3472,6 +3678,21 @@ var HistoryRecorder = class {
3472
3678
  this.stack.push(new UpdateElementCommand(current.id, previous, current));
3473
3679
  }
3474
3680
  }
3681
+ onLayerCreate(layer) {
3682
+ if (!this.recording) return;
3683
+ if (!this.layerManager) return;
3684
+ this.record(new CreateLayerCommand(this.layerManager, layer));
3685
+ }
3686
+ onLayerRemove(layer) {
3687
+ if (!this.recording) return;
3688
+ if (!this.layerManager) return;
3689
+ this.record(new RemoveLayerCommand(this.layerManager, layer));
3690
+ }
3691
+ onLayerUpdate(previous, current) {
3692
+ if (!this.recording) return;
3693
+ if (!this.layerManager) return;
3694
+ this.record(new UpdateLayerCommand(this.layerManager, current.id, previous, current));
3695
+ }
3475
3696
  flushUpdateSnapshots() {
3476
3697
  const commands = [];
3477
3698
  for (const [id, previous] of this.updateSnapshots) {
@@ -3904,18 +4125,23 @@ var LayerManager = class {
3904
4125
  addLayerDirect(layer) {
3905
4126
  this.layers.set(layer.id, { ...layer });
3906
4127
  this.syncLayerOrder();
4128
+ this.bus.emit("create", { ...layer });
3907
4129
  this.bus.emit("change", null);
3908
4130
  }
3909
4131
  removeLayerDirect(id) {
4132
+ const layer = this.layers.get(id);
3910
4133
  this.layers.delete(id);
3911
4134
  this.syncLayerOrder();
4135
+ if (layer) this.bus.emit("remove", { ...layer });
3912
4136
  this.bus.emit("change", null);
3913
4137
  }
3914
4138
  updateLayerDirect(id, props) {
3915
4139
  const layer = this.layers.get(id);
3916
4140
  if (!layer) return;
4141
+ const previous = { ...layer };
3917
4142
  Object.assign(layer, props);
3918
4143
  if ("order" in props) this.syncLayerOrder();
4144
+ this.bus.emit("update", { previous, current: { ...layer } });
3919
4145
  this.bus.emit("change", null);
3920
4146
  }
3921
4147
  syncLayerOrder() {
@@ -4008,6 +4234,20 @@ var DomNodeManager = class {
4008
4234
  storeHtmlContent(elementId, dom) {
4009
4235
  this.htmlContent.set(elementId, dom);
4010
4236
  }
4237
+ hasContent(elementId) {
4238
+ return this.htmlContent.has(elementId);
4239
+ }
4240
+ resetHtmlContent(elementId) {
4241
+ this.htmlContent.delete(elementId);
4242
+ this.lastSyncedVersion.delete(elementId);
4243
+ this.lastSyncedZIndex.delete(elementId);
4244
+ const node = this.domNodes.get(elementId);
4245
+ if (!node) return;
4246
+ while (node.firstChild) {
4247
+ node.removeChild(node.firstChild);
4248
+ }
4249
+ delete node.dataset["initialized"];
4250
+ }
4011
4251
  syncDomNode(element, zIndex = 0) {
4012
4252
  let node = this.domNodes.get(element.id);
4013
4253
  if (!node) {
@@ -4549,8 +4789,10 @@ var Viewport = class {
4549
4789
  toolbar: options.toolbar
4550
4790
  });
4551
4791
  this.noteEditor.setOnStop((id) => this.onTextEditStop(id));
4792
+ this.onHtmlElementMount = options.onHtmlElementMount;
4793
+ this.dropHandler = options.onDrop;
4552
4794
  this.history = new HistoryStack();
4553
- this.historyRecorder = new HistoryRecorder(this.store, this.history);
4795
+ this.historyRecorder = new HistoryRecorder(this.store, this.history, this.layerManager);
4554
4796
  this.wrapper = this.createWrapper();
4555
4797
  this.canvasEl = this.createCanvas();
4556
4798
  this.domLayer = this.createDomLayer();
@@ -4576,7 +4818,8 @@ var Viewport = class {
4576
4818
  toolManager: this.toolManager,
4577
4819
  toolContext: this.toolContext,
4578
4820
  historyRecorder: this.historyRecorder,
4579
- historyStack: this.history
4821
+ historyStack: this.history,
4822
+ fitToContent: () => this.fitToContent()
4580
4823
  });
4581
4824
  this.domNodeManager = new DomNodeManager({
4582
4825
  domLayer: this.domLayer,
@@ -4670,6 +4913,8 @@ var Viewport = class {
4670
4913
  renderLoop;
4671
4914
  domNodeManager;
4672
4915
  interactMode;
4916
+ onHtmlElementMount;
4917
+ dropHandler;
4673
4918
  gridChangeListeners = /* @__PURE__ */ new Set();
4674
4919
  doubleTapDetector = new DoubleTapDetector();
4675
4920
  tapDownX = 0;
@@ -4684,6 +4929,13 @@ var Viewport = class {
4684
4929
  this._snapToGrid = enabled;
4685
4930
  this.toolContext.snapToGrid = enabled;
4686
4931
  }
4932
+ fitToContent(padding = 40) {
4933
+ if (this.wrapper.clientWidth === 0 || this.wrapper.clientHeight === 0) return;
4934
+ const visibleElements = this.store.getAll().filter((el) => this.layerManager.isLayerVisible(el.layerId));
4935
+ const bbox = getElementsBoundingBox(visibleElements);
4936
+ if (!bbox) return;
4937
+ this.camera.fitToContent(bbox, this.wrapper.clientWidth, this.wrapper.clientHeight, padding);
4938
+ }
4687
4939
  requestRender() {
4688
4940
  this.renderLoop.requestRender();
4689
4941
  }
@@ -4702,6 +4954,7 @@ var Viewport = class {
4702
4954
  return exportImage(this.store, options, this.layerManager);
4703
4955
  }
4704
4956
  loadState(state) {
4957
+ this.inputHandler.flushPendingHistory();
4705
4958
  this.historyRecorder.pause();
4706
4959
  this.noteEditor.destroy(this.store);
4707
4960
  this.domNodeManager.clearDomNodes();
@@ -4713,6 +4966,22 @@ var Viewport = class {
4713
4966
  this.layerManager.setActiveLayer(state.activeLayerId);
4714
4967
  }
4715
4968
  this.domNodeManager.reattachHtmlContent(this.store);
4969
+ if (this.onHtmlElementMount) {
4970
+ for (const el of this.store.getElementsByType("html")) {
4971
+ if (!this.domNodeManager.hasContent(el.id)) {
4972
+ this.domNodeManager.syncDomNode(el);
4973
+ const node = this.domNodeManager.getNode(el.id);
4974
+ if (node) {
4975
+ this.onHtmlElementMount(el.id, el.domId, node);
4976
+ node.dataset["initialized"] = "true";
4977
+ Object.assign(node.style, {
4978
+ overflow: "hidden",
4979
+ pointerEvents: el.interactive ? "auto" : "none"
4980
+ });
4981
+ }
4982
+ }
4983
+ }
4984
+ }
4716
4985
  this.history.clear();
4717
4986
  this.historyRecorder.resume();
4718
4987
  this.camera.moveTo(state.camera.position.x, state.camera.position.y);
@@ -4722,6 +4991,7 @@ var Viewport = class {
4722
4991
  this.loadState(parseState(json));
4723
4992
  }
4724
4993
  undo() {
4994
+ this.inputHandler.flushPendingHistory();
4725
4995
  this.historyRecorder.pause();
4726
4996
  const result = this.history.undo(this.store);
4727
4997
  this.historyRecorder.resume();
@@ -4729,6 +4999,7 @@ var Viewport = class {
4729
4999
  return result;
4730
5000
  }
4731
5001
  redo() {
5002
+ this.inputHandler.flushPendingHistory();
4732
5003
  this.historyRecorder.pause();
4733
5004
  const result = this.history.redo(this.store);
4734
5005
  this.historyRecorder.resume();
@@ -4758,6 +5029,19 @@ var Viewport = class {
4758
5029
  this.requestRender();
4759
5030
  return el.id;
4760
5031
  }
5032
+ removeLayer(id) {
5033
+ this.historyRecorder.begin();
5034
+ this.layerManager.removeLayer(id);
5035
+ this.historyRecorder.commit();
5036
+ }
5037
+ updateHtmlElement(id, newContent) {
5038
+ const el = this.store.getById(id);
5039
+ if (!el) throw new Error(`Element not found: ${id}`);
5040
+ if (el.type !== "html") throw new Error(`Element ${id} is not an HTML element`);
5041
+ this.domNodeManager.resetHtmlContent(id);
5042
+ this.domNodeManager.storeHtmlContent(id, newContent);
5043
+ this.requestRender();
5044
+ }
4761
5045
  addGrid(input) {
4762
5046
  const existing = this.store.getElementsByType("grid")[0];
4763
5047
  this.historyRecorder.begin();
@@ -4909,17 +5193,21 @@ var Viewport = class {
4909
5193
  };
4910
5194
  onDrop = (e) => {
4911
5195
  e.preventDefault();
5196
+ const rect = this.wrapper.getBoundingClientRect();
5197
+ const screenPos = { x: e.clientX - rect.left, y: e.clientY - rect.top };
5198
+ const worldPos = this.camera.screenToWorld(screenPos);
5199
+ if (this.dropHandler) {
5200
+ this.dropHandler(e, worldPos);
5201
+ return;
5202
+ }
4912
5203
  const files = e.dataTransfer?.files;
4913
5204
  if (!files) return;
4914
- const rect = this.wrapper.getBoundingClientRect();
4915
5205
  for (const file of files) {
4916
5206
  if (!file.type.startsWith("image/")) continue;
4917
5207
  const reader = new FileReader();
4918
5208
  reader.onload = () => {
4919
5209
  const src = reader.result;
4920
5210
  if (typeof src !== "string") return;
4921
- const screenPos = { x: e.clientX - rect.left, y: e.clientY - rect.top };
4922
- const worldPos = this.camera.screenToWorld(screenPos);
4923
5211
  this.addImage(src, worldPos);
4924
5212
  };
4925
5213
  reader.readAsDataURL(file);
@@ -5031,26 +5319,6 @@ var Viewport = class {
5031
5319
  }
5032
5320
  };
5033
5321
 
5034
- // src/elements/bounds.ts
5035
- function getElementsBoundingBox(elements) {
5036
- let minX = Infinity;
5037
- let minY = Infinity;
5038
- let maxX = -Infinity;
5039
- let maxY = -Infinity;
5040
- let found = false;
5041
- for (const el of elements) {
5042
- const b = getElementBounds(el);
5043
- if (!b) continue;
5044
- found = true;
5045
- if (b.x < minX) minX = b.x;
5046
- if (b.y < minY) minY = b.y;
5047
- if (b.x + b.w > maxX) maxX = b.x + b.w;
5048
- if (b.y + b.h > maxY) maxY = b.y + b.h;
5049
- }
5050
- if (!found) return null;
5051
- return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
5052
- }
5053
-
5054
5322
  // src/tools/hand-tool.ts
5055
5323
  var HandTool = class {
5056
5324
  name = "hand";
@@ -5542,23 +5810,7 @@ var SelectTool = class {
5542
5810
  });
5543
5811
  }
5544
5812
  }
5545
- const movedNonArrowIds = /* @__PURE__ */ new Set();
5546
- for (const id of this._selectedIds) {
5547
- const el = ctx.store.getById(id);
5548
- if (el && el.type !== "arrow") movedNonArrowIds.add(id);
5549
- }
5550
- if (movedNonArrowIds.size > 0) {
5551
- const updatedArrows = /* @__PURE__ */ new Set();
5552
- for (const id of movedNonArrowIds) {
5553
- const boundArrows = findBoundArrows(id, ctx.store);
5554
- for (const ba of boundArrows) {
5555
- if (updatedArrows.has(ba.id)) continue;
5556
- updatedArrows.add(ba.id);
5557
- const updates = updateBoundArrow(ba, ctx.store);
5558
- if (updates) ctx.store.update(ba.id, updates);
5559
- }
5560
- }
5561
- }
5813
+ this.updateArrowsBoundTo(this._selectedIds, ctx);
5562
5814
  ctx.requestRender();
5563
5815
  return;
5564
5816
  }
@@ -5609,6 +5861,49 @@ var SelectTool = class {
5609
5861
  }
5610
5862
  }
5611
5863
  }
5864
+ updateArrowsBoundTo(ids, ctx) {
5865
+ const movedNonArrowIds = /* @__PURE__ */ new Set();
5866
+ for (const id of ids) {
5867
+ const el = ctx.store.getById(id);
5868
+ if (el && el.type !== "arrow") movedNonArrowIds.add(id);
5869
+ }
5870
+ if (movedNonArrowIds.size === 0) return;
5871
+ const updatedArrows = /* @__PURE__ */ new Set();
5872
+ for (const id of movedNonArrowIds) {
5873
+ const boundArrows = findBoundArrows(id, ctx.store);
5874
+ for (const ba of boundArrows) {
5875
+ if (updatedArrows.has(ba.id)) continue;
5876
+ updatedArrows.add(ba.id);
5877
+ const updates = updateBoundArrow(ba, ctx.store);
5878
+ if (updates) ctx.store.update(ba.id, updates);
5879
+ }
5880
+ }
5881
+ }
5882
+ nudgeSelection(dx, dy, ctx) {
5883
+ let moved = false;
5884
+ for (const id of this._selectedIds) {
5885
+ const el = ctx.store.getById(id);
5886
+ if (!el || el.locked) continue;
5887
+ if (el.type === "arrow") {
5888
+ if (el.fromBinding || el.toBinding) continue;
5889
+ ctx.store.update(id, {
5890
+ position: { x: el.position.x + dx, y: el.position.y + dy },
5891
+ from: { x: el.from.x + dx, y: el.from.y + dy },
5892
+ to: { x: el.to.x + dx, y: el.to.y + dy }
5893
+ });
5894
+ } else {
5895
+ ctx.store.update(id, {
5896
+ position: { x: el.position.x + dx, y: el.position.y + dy }
5897
+ });
5898
+ }
5899
+ moved = true;
5900
+ }
5901
+ if (moved) {
5902
+ this.updateArrowsBoundTo(this._selectedIds, ctx);
5903
+ ctx.requestRender();
5904
+ }
5905
+ return moved;
5906
+ }
5612
5907
  updateHoverCursor(world, ctx) {
5613
5908
  const arrowHit = hitTestArrowHandles(world, this._selectedIds, ctx);
5614
5909
  if (arrowHit) {
@@ -5686,11 +5981,7 @@ var SelectTool = class {
5686
5981
  position: { x, y },
5687
5982
  size: { w, h }
5688
5983
  });
5689
- const boundArrows = findBoundArrows(this.mode.elementId, ctx.store);
5690
- for (const ba of boundArrows) {
5691
- const updates = updateBoundArrow(ba, ctx.store);
5692
- if (updates) ctx.store.update(ba.id, updates);
5693
- }
5984
+ this.updateArrowsBoundTo([this.mode.elementId], ctx);
5694
5985
  ctx.requestRender();
5695
5986
  }
5696
5987
  hitTestResizeHandle(world, ctx) {
@@ -6736,48 +7027,8 @@ var TemplateTool = class {
6736
7027
  }
6737
7028
  };
6738
7029
 
6739
- // src/history/layer-commands.ts
6740
- var CreateLayerCommand = class {
6741
- constructor(manager, layer) {
6742
- this.manager = manager;
6743
- this.layer = layer;
6744
- }
6745
- execute(_store) {
6746
- this.manager.addLayerDirect(this.layer);
6747
- }
6748
- undo(_store) {
6749
- this.manager.removeLayerDirect(this.layer.id);
6750
- }
6751
- };
6752
- var RemoveLayerCommand = class {
6753
- constructor(manager, layer) {
6754
- this.manager = manager;
6755
- this.layer = layer;
6756
- }
6757
- execute(_store) {
6758
- this.manager.removeLayerDirect(this.layer.id);
6759
- }
6760
- undo(_store) {
6761
- this.manager.addLayerDirect(this.layer);
6762
- }
6763
- };
6764
- var UpdateLayerCommand = class {
6765
- constructor(manager, layerId, previous, current) {
6766
- this.manager = manager;
6767
- this.layerId = layerId;
6768
- this.previous = previous;
6769
- this.current = current;
6770
- }
6771
- execute(_store) {
6772
- this.manager.updateLayerDirect(this.layerId, { ...this.current });
6773
- }
6774
- undo(_store) {
6775
- this.manager.updateLayerDirect(this.layerId, { ...this.previous });
6776
- }
6777
- };
6778
-
6779
7030
  // src/index.ts
6780
- var VERSION = "0.15.0";
7031
+ var VERSION = "0.17.0";
6781
7032
  // Annotate the CommonJS export names for ESM import in node:
6782
7033
  0 && (module.exports = {
6783
7034
  AddElementCommand,