@fieldnotes/core 0.14.0 → 0.16.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
@@ -1084,6 +1084,14 @@ var InputHandler = class {
1084
1084
  e.preventDefault();
1085
1085
  this.handlePaste();
1086
1086
  }
1087
+ if (e.key === "]") {
1088
+ e.preventDefault();
1089
+ this.handleZOrder(e.ctrlKey || e.metaKey ? "front" : "forward");
1090
+ }
1091
+ if (e.key === "[") {
1092
+ e.preventDefault();
1093
+ this.handleZOrder(e.ctrlKey || e.metaKey ? "back" : "backward");
1094
+ }
1087
1095
  };
1088
1096
  onKeyUp = (e) => {
1089
1097
  if (e.key === " ") {
@@ -1260,6 +1268,33 @@ var InputHandler = class {
1260
1268
  selectTool.setSelection(newIds);
1261
1269
  this.toolContext.requestRender();
1262
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
+ }
1263
1298
  cancelToolIfActive(e) {
1264
1299
  if (this.isToolActive) {
1265
1300
  this.dispatchToolUp(e);
@@ -1556,9 +1591,13 @@ var ElementStore = class {
1556
1591
  layerOrderMap = /* @__PURE__ */ new Map();
1557
1592
  spatialIndex = new Quadtree({ x: -1e5, y: -1e5, w: 2e5, h: 2e5 });
1558
1593
  sortedCache = null;
1594
+ _versions = /* @__PURE__ */ new Map();
1559
1595
  get count() {
1560
1596
  return this.elements.size;
1561
1597
  }
1598
+ getVersion(id) {
1599
+ return this._versions.get(id) ?? -1;
1600
+ }
1562
1601
  setLayerOrder(order) {
1563
1602
  this.layerOrderMap = new Map(order);
1564
1603
  this.sortedCache = null;
@@ -1583,6 +1622,7 @@ var ElementStore = class {
1583
1622
  }
1584
1623
  add(element) {
1585
1624
  this.sortedCache = null;
1625
+ this._versions.set(element.id, 0);
1586
1626
  this.elements.set(element.id, element);
1587
1627
  const bounds = getElementBounds(element);
1588
1628
  if (bounds) this.spatialIndex.insert(element.id, bounds);
@@ -1592,6 +1632,7 @@ var ElementStore = class {
1592
1632
  const existing = this.elements.get(id);
1593
1633
  if (!existing) return;
1594
1634
  this.sortedCache = null;
1635
+ this._versions.set(id, (this._versions.get(id) ?? 0) + 1);
1595
1636
  const updated = { ...existing, ...partial, id: existing.id, type: existing.type };
1596
1637
  if (updated.type === "arrow") {
1597
1638
  const arrow = updated;
@@ -1611,12 +1652,14 @@ var ElementStore = class {
1611
1652
  const element = this.elements.get(id);
1612
1653
  if (!element) return;
1613
1654
  this.sortedCache = null;
1655
+ this._versions.delete(id);
1614
1656
  this.elements.delete(id);
1615
1657
  this.spatialIndex.remove(id);
1616
1658
  this.bus.emit("remove", element);
1617
1659
  }
1618
1660
  clear() {
1619
1661
  this.sortedCache = null;
1662
+ this._versions.clear();
1620
1663
  this.elements.clear();
1621
1664
  this.spatialIndex.clear();
1622
1665
  this.bus.emit("clear", null);
@@ -1626,10 +1669,12 @@ var ElementStore = class {
1626
1669
  }
1627
1670
  loadSnapshot(elements) {
1628
1671
  this.sortedCache = null;
1672
+ this._versions.clear();
1629
1673
  this.elements.clear();
1630
1674
  this.spatialIndex.clear();
1631
1675
  for (const el of elements) {
1632
1676
  this.elements.set(el.id, el);
1677
+ this._versions.set(el.id, 0);
1633
1678
  const bounds = getElementBounds(el);
1634
1679
  if (bounds) this.spatialIndex.insert(el.id, bounds);
1635
1680
  }
@@ -1638,6 +1683,54 @@ var ElementStore = class {
1638
1683
  this.bus.emit("add", el);
1639
1684
  }
1640
1685
  }
1686
+ bringToFront(id) {
1687
+ const el = this.elements.get(id);
1688
+ if (!el) return;
1689
+ const siblings = [...this.elements.values()].filter(
1690
+ (e) => e.layerId === el.layerId && e.id !== id
1691
+ );
1692
+ if (siblings.length === 0) return;
1693
+ const maxZ = Math.max(...siblings.map((e) => e.zIndex));
1694
+ if (el.zIndex >= maxZ) return;
1695
+ this.update(id, { zIndex: maxZ + 1 });
1696
+ }
1697
+ sendToBack(id) {
1698
+ const el = this.elements.get(id);
1699
+ if (!el) return;
1700
+ const siblings = [...this.elements.values()].filter(
1701
+ (e) => e.layerId === el.layerId && e.id !== id
1702
+ );
1703
+ if (siblings.length === 0) return;
1704
+ const minZ = Math.min(...siblings.map((e) => e.zIndex));
1705
+ if (el.zIndex <= minZ) return;
1706
+ this.update(id, { zIndex: minZ - 1 });
1707
+ }
1708
+ bringForward(id) {
1709
+ const el = this.elements.get(id);
1710
+ if (!el) return;
1711
+ const sorted = [...this.elements.values()].filter((e) => e.layerId === el.layerId).sort((a, b) => a.zIndex - b.zIndex);
1712
+ const idx = sorted.findIndex((e) => e.id === id);
1713
+ if (idx < 0 || idx >= sorted.length - 1) return;
1714
+ const next = sorted[idx + 1];
1715
+ if (!next) return;
1716
+ const myZ = el.zIndex;
1717
+ const nextZ = next.zIndex;
1718
+ this.update(id, { zIndex: nextZ });
1719
+ this.update(next.id, { zIndex: myZ });
1720
+ }
1721
+ sendBackward(id) {
1722
+ const el = this.elements.get(id);
1723
+ if (!el) return;
1724
+ const sorted = [...this.elements.values()].filter((e) => e.layerId === el.layerId).sort((a, b) => a.zIndex - b.zIndex);
1725
+ const idx = sorted.findIndex((e) => e.id === id);
1726
+ if (idx <= 0) return;
1727
+ const prev = sorted[idx - 1];
1728
+ if (!prev) return;
1729
+ const myZ = el.zIndex;
1730
+ const prevZ = prev.zIndex;
1731
+ this.update(id, { zIndex: prevZ });
1732
+ this.update(prev.id, { zIndex: myZ });
1733
+ }
1641
1734
  queryRect(rect) {
1642
1735
  const ids = this.spatialIndex.query(rect);
1643
1736
  const elements = [];
@@ -3309,16 +3402,64 @@ var BatchCommand = class {
3309
3402
  }
3310
3403
  };
3311
3404
 
3405
+ // src/history/layer-commands.ts
3406
+ var CreateLayerCommand = class {
3407
+ constructor(manager, layer) {
3408
+ this.manager = manager;
3409
+ this.layer = layer;
3410
+ }
3411
+ execute(_store) {
3412
+ this.manager.addLayerDirect(this.layer);
3413
+ }
3414
+ undo(_store) {
3415
+ this.manager.removeLayerDirect(this.layer.id);
3416
+ }
3417
+ };
3418
+ var RemoveLayerCommand = class {
3419
+ constructor(manager, layer) {
3420
+ this.manager = manager;
3421
+ this.layer = layer;
3422
+ }
3423
+ execute(_store) {
3424
+ this.manager.removeLayerDirect(this.layer.id);
3425
+ }
3426
+ undo(_store) {
3427
+ this.manager.addLayerDirect(this.layer);
3428
+ }
3429
+ };
3430
+ var UpdateLayerCommand = class {
3431
+ constructor(manager, layerId, previous, current) {
3432
+ this.manager = manager;
3433
+ this.layerId = layerId;
3434
+ this.previous = previous;
3435
+ this.current = current;
3436
+ }
3437
+ execute(_store) {
3438
+ this.manager.updateLayerDirect(this.layerId, { ...this.current });
3439
+ }
3440
+ undo(_store) {
3441
+ this.manager.updateLayerDirect(this.layerId, { ...this.previous });
3442
+ }
3443
+ };
3444
+
3312
3445
  // src/history/history-recorder.ts
3313
3446
  var HistoryRecorder = class {
3314
- constructor(store, stack) {
3447
+ constructor(store, stack, layerManager) {
3315
3448
  this.store = store;
3316
3449
  this.stack = stack;
3450
+ this.layerManager = layerManager;
3317
3451
  this.unsubscribers = [
3318
3452
  store.on("add", (el) => this.onAdd(el)),
3319
3453
  store.on("remove", (el) => this.onRemove(el)),
3320
3454
  store.on("update", ({ previous, current }) => this.onUpdate(previous, current))
3321
3455
  ];
3456
+ if (layerManager) {
3457
+ this.unsubscribers.push(
3458
+ layerManager.on("create", (layer) => this.onLayerCreate(layer)),
3459
+ layerManager.on("remove", (layer) => this.onLayerRemove(layer)),
3460
+ layerManager.on("update", ({ previous, current }) => this.onLayerUpdate(previous, current))
3461
+ );
3462
+ }
3322
3463
  }
3323
3464
  recording = true;
3324
3465
  transaction = null;
@@ -3379,6 +3520,21 @@ var HistoryRecorder = class {
3379
3520
  this.stack.push(new UpdateElementCommand(current.id, previous, current));
3380
3521
  }
3381
3522
  }
3523
+ onLayerCreate(layer) {
3524
+ if (!this.recording) return;
3525
+ if (!this.layerManager) return;
3526
+ this.record(new CreateLayerCommand(this.layerManager, layer));
3527
+ }
3528
+ onLayerRemove(layer) {
3529
+ if (!this.recording) return;
3530
+ if (!this.layerManager) return;
3531
+ this.record(new RemoveLayerCommand(this.layerManager, layer));
3532
+ }
3533
+ onLayerUpdate(previous, current) {
3534
+ if (!this.recording) return;
3535
+ if (!this.layerManager) return;
3536
+ this.record(new UpdateLayerCommand(this.layerManager, current.id, previous, current));
3537
+ }
3382
3538
  flushUpdateSnapshots() {
3383
3539
  const commands = [];
3384
3540
  for (const [id, previous] of this.updateSnapshots) {
@@ -3811,18 +3967,23 @@ var LayerManager = class {
3811
3967
  addLayerDirect(layer) {
3812
3968
  this.layers.set(layer.id, { ...layer });
3813
3969
  this.syncLayerOrder();
3970
+ this.bus.emit("create", { ...layer });
3814
3971
  this.bus.emit("change", null);
3815
3972
  }
3816
3973
  removeLayerDirect(id) {
3974
+ const layer = this.layers.get(id);
3817
3975
  this.layers.delete(id);
3818
3976
  this.syncLayerOrder();
3977
+ if (layer) this.bus.emit("remove", { ...layer });
3819
3978
  this.bus.emit("change", null);
3820
3979
  }
3821
3980
  updateLayerDirect(id, props) {
3822
3981
  const layer = this.layers.get(id);
3823
3982
  if (!layer) return;
3983
+ const previous = { ...layer };
3824
3984
  Object.assign(layer, props);
3825
3985
  if ("order" in props) this.syncLayerOrder();
3986
+ this.bus.emit("update", { previous, current: { ...layer } });
3826
3987
  this.bus.emit("change", null);
3827
3988
  }
3828
3989
  syncLayerOrder() {
@@ -3900,10 +4061,14 @@ var DomNodeManager = class {
3900
4061
  domLayer;
3901
4062
  onEditRequest;
3902
4063
  isEditingElement;
4064
+ getVersion;
4065
+ lastSyncedVersion = /* @__PURE__ */ new Map();
4066
+ lastSyncedZIndex = /* @__PURE__ */ new Map();
3903
4067
  constructor(deps) {
3904
4068
  this.domLayer = deps.domLayer;
3905
4069
  this.onEditRequest = deps.onEditRequest;
3906
4070
  this.isEditingElement = deps.isEditingElement;
4071
+ this.getVersion = deps.getVersion ?? null;
3907
4072
  }
3908
4073
  getNode(id) {
3909
4074
  return this.domNodes.get(id);
@@ -3911,6 +4076,20 @@ var DomNodeManager = class {
3911
4076
  storeHtmlContent(elementId, dom) {
3912
4077
  this.htmlContent.set(elementId, dom);
3913
4078
  }
4079
+ hasContent(elementId) {
4080
+ return this.htmlContent.has(elementId);
4081
+ }
4082
+ resetHtmlContent(elementId) {
4083
+ this.htmlContent.delete(elementId);
4084
+ this.lastSyncedVersion.delete(elementId);
4085
+ this.lastSyncedZIndex.delete(elementId);
4086
+ const node = this.domNodes.get(elementId);
4087
+ if (!node) return;
4088
+ while (node.firstChild) {
4089
+ node.removeChild(node.firstChild);
4090
+ }
4091
+ delete node.dataset["initialized"];
4092
+ }
3914
4093
  syncDomNode(element, zIndex = 0) {
3915
4094
  let node = this.domNodes.get(element.id);
3916
4095
  if (!node) {
@@ -3922,6 +4101,17 @@ var DomNodeManager = class {
3922
4101
  });
3923
4102
  this.domLayer.appendChild(node);
3924
4103
  this.domNodes.set(element.id, node);
4104
+ } else if (this.getVersion) {
4105
+ const currentVersion = this.getVersion(element.id);
4106
+ const lastVersion = this.lastSyncedVersion.get(element.id);
4107
+ const lastZ = this.lastSyncedZIndex.get(element.id);
4108
+ if (lastVersion === currentVersion && lastZ === zIndex) {
4109
+ return;
4110
+ }
4111
+ }
4112
+ if (this.getVersion) {
4113
+ this.lastSyncedVersion.set(element.id, this.getVersion(element.id));
4114
+ this.lastSyncedZIndex.set(element.id, zIndex);
3925
4115
  }
3926
4116
  const size = "size" in element ? element.size : null;
3927
4117
  Object.assign(node.style, {
@@ -3940,6 +4130,8 @@ var DomNodeManager = class {
3940
4130
  }
3941
4131
  removeDomNode(id) {
3942
4132
  this.htmlContent.delete(id);
4133
+ this.lastSyncedVersion.delete(id);
4134
+ this.lastSyncedZIndex.delete(id);
3943
4135
  const node = this.domNodes.get(id);
3944
4136
  if (node) {
3945
4137
  node.remove();
@@ -3950,6 +4142,8 @@ var DomNodeManager = class {
3950
4142
  this.domNodes.forEach((node) => node.remove());
3951
4143
  this.domNodes.clear();
3952
4144
  this.htmlContent.clear();
4145
+ this.lastSyncedVersion.clear();
4146
+ this.lastSyncedZIndex.clear();
3953
4147
  }
3954
4148
  reattachHtmlContent(store) {
3955
4149
  for (const el of store.getElementsByType("html")) {
@@ -4437,8 +4631,10 @@ var Viewport = class {
4437
4631
  toolbar: options.toolbar
4438
4632
  });
4439
4633
  this.noteEditor.setOnStop((id) => this.onTextEditStop(id));
4634
+ this.onHtmlElementMount = options.onHtmlElementMount;
4635
+ this.dropHandler = options.onDrop;
4440
4636
  this.history = new HistoryStack();
4441
- this.historyRecorder = new HistoryRecorder(this.store, this.history);
4637
+ this.historyRecorder = new HistoryRecorder(this.store, this.history, this.layerManager);
4442
4638
  this.wrapper = this.createWrapper();
4443
4639
  this.canvasEl = this.createCanvas();
4444
4640
  this.domLayer = this.createDomLayer();
@@ -4469,7 +4665,8 @@ var Viewport = class {
4469
4665
  this.domNodeManager = new DomNodeManager({
4470
4666
  domLayer: this.domLayer,
4471
4667
  onEditRequest: (id) => this.startEditingElement(id),
4472
- isEditingElement: (id) => this.noteEditor.isEditing && this.noteEditor.editingElementId === id
4668
+ isEditingElement: (id) => this.noteEditor.isEditing && this.noteEditor.editingElementId === id,
4669
+ getVersion: (id) => this.store.getVersion(id)
4473
4670
  });
4474
4671
  this.interactMode = new InteractMode({
4475
4672
  getNode: (id) => this.domNodeManager.getNode(id)
@@ -4557,6 +4754,8 @@ var Viewport = class {
4557
4754
  renderLoop;
4558
4755
  domNodeManager;
4559
4756
  interactMode;
4757
+ onHtmlElementMount;
4758
+ dropHandler;
4560
4759
  gridChangeListeners = /* @__PURE__ */ new Set();
4561
4760
  doubleTapDetector = new DoubleTapDetector();
4562
4761
  tapDownX = 0;
@@ -4600,6 +4799,22 @@ var Viewport = class {
4600
4799
  this.layerManager.setActiveLayer(state.activeLayerId);
4601
4800
  }
4602
4801
  this.domNodeManager.reattachHtmlContent(this.store);
4802
+ if (this.onHtmlElementMount) {
4803
+ for (const el of this.store.getElementsByType("html")) {
4804
+ if (!this.domNodeManager.hasContent(el.id)) {
4805
+ this.domNodeManager.syncDomNode(el);
4806
+ const node = this.domNodeManager.getNode(el.id);
4807
+ if (node) {
4808
+ this.onHtmlElementMount(el.id, el.domId, node);
4809
+ node.dataset["initialized"] = "true";
4810
+ Object.assign(node.style, {
4811
+ overflow: "hidden",
4812
+ pointerEvents: el.interactive ? "auto" : "none"
4813
+ });
4814
+ }
4815
+ }
4816
+ }
4817
+ }
4603
4818
  this.history.clear();
4604
4819
  this.historyRecorder.resume();
4605
4820
  this.camera.moveTo(state.camera.position.x, state.camera.position.y);
@@ -4645,6 +4860,19 @@ var Viewport = class {
4645
4860
  this.requestRender();
4646
4861
  return el.id;
4647
4862
  }
4863
+ removeLayer(id) {
4864
+ this.historyRecorder.begin();
4865
+ this.layerManager.removeLayer(id);
4866
+ this.historyRecorder.commit();
4867
+ }
4868
+ updateHtmlElement(id, newContent) {
4869
+ const el = this.store.getById(id);
4870
+ if (!el) throw new Error(`Element not found: ${id}`);
4871
+ if (el.type !== "html") throw new Error(`Element ${id} is not an HTML element`);
4872
+ this.domNodeManager.resetHtmlContent(id);
4873
+ this.domNodeManager.storeHtmlContent(id, newContent);
4874
+ this.requestRender();
4875
+ }
4648
4876
  addGrid(input) {
4649
4877
  const existing = this.store.getElementsByType("grid")[0];
4650
4878
  this.historyRecorder.begin();
@@ -4796,17 +5024,21 @@ var Viewport = class {
4796
5024
  };
4797
5025
  onDrop = (e) => {
4798
5026
  e.preventDefault();
5027
+ const rect = this.wrapper.getBoundingClientRect();
5028
+ const screenPos = { x: e.clientX - rect.left, y: e.clientY - rect.top };
5029
+ const worldPos = this.camera.screenToWorld(screenPos);
5030
+ if (this.dropHandler) {
5031
+ this.dropHandler(e, worldPos);
5032
+ return;
5033
+ }
4799
5034
  const files = e.dataTransfer?.files;
4800
5035
  if (!files) return;
4801
- const rect = this.wrapper.getBoundingClientRect();
4802
5036
  for (const file of files) {
4803
5037
  if (!file.type.startsWith("image/")) continue;
4804
5038
  const reader = new FileReader();
4805
5039
  reader.onload = () => {
4806
5040
  const src = reader.result;
4807
5041
  if (typeof src !== "string") return;
4808
- const screenPos = { x: e.clientX - rect.left, y: e.clientY - rect.top };
4809
- const worldPos = this.camera.screenToWorld(screenPos);
4810
5042
  this.addImage(src, worldPos);
4811
5043
  };
4812
5044
  reader.readAsDataURL(file);
@@ -5292,6 +5524,7 @@ var SelectTool = class {
5292
5524
  ctx = null;
5293
5525
  pendingSingleSelectId = null;
5294
5526
  hasDragged = false;
5527
+ resizeAspectRatio = 0;
5295
5528
  get selectedIds() {
5296
5529
  return [...this._selectedIds];
5297
5530
  }
@@ -5337,7 +5570,8 @@ var SelectTool = class {
5337
5570
  const resizeHit = this.hitTestResizeHandle(world, ctx);
5338
5571
  if (resizeHit) {
5339
5572
  const el = ctx.store.getById(resizeHit.elementId);
5340
- if (el) {
5573
+ if (el && "size" in el) {
5574
+ this.resizeAspectRatio = el.size.h > 0 ? el.size.w / el.size.h : 0;
5341
5575
  this.mode = {
5342
5576
  type: "resizing",
5343
5577
  elementId: resizeHit.elementId,
@@ -5389,7 +5623,7 @@ var SelectTool = class {
5389
5623
  }
5390
5624
  if (this.mode.type === "resizing") {
5391
5625
  ctx.setCursor?.(HANDLE_CURSORS[this.mode.handle]);
5392
- this.handleResize(world, ctx);
5626
+ this.handleResize(world, ctx, state.shiftKey);
5393
5627
  return;
5394
5628
  }
5395
5629
  if (this.mode.type === "dragging" && this._selectedIds.length > 0) {
@@ -5513,7 +5747,7 @@ var SelectTool = class {
5513
5747
  const hit = this.hitTest(world, ctx);
5514
5748
  ctx.setCursor?.(hit ? "move" : "default");
5515
5749
  }
5516
- handleResize(world, ctx) {
5750
+ handleResize(world, ctx, shiftKey = false) {
5517
5751
  if (this.mode.type !== "resizing") return;
5518
5752
  const el = ctx.store.getById(this.mode.elementId);
5519
5753
  if (!el || !("size" in el) || el.locked) return;
@@ -5544,6 +5778,21 @@ var SelectTool = class {
5544
5778
  h -= dy;
5545
5779
  break;
5546
5780
  }
5781
+ if (shiftKey && this.resizeAspectRatio > 0) {
5782
+ const absDw = Math.abs(w - el.size.w);
5783
+ const absDh = Math.abs(h - el.size.h);
5784
+ if (absDw >= absDh) {
5785
+ h = w / this.resizeAspectRatio;
5786
+ } else {
5787
+ w = h * this.resizeAspectRatio;
5788
+ }
5789
+ if (handle === "nw" || handle === "sw") {
5790
+ x = el.position.x + el.size.w - w;
5791
+ }
5792
+ if (handle === "nw" || handle === "ne") {
5793
+ y = el.position.y + el.size.h - h;
5794
+ }
5795
+ }
5547
5796
  if (w < MIN_ELEMENT_SIZE) {
5548
5797
  if (handle === "nw" || handle === "sw") x = el.position.x + el.size.w - MIN_ELEMENT_SIZE;
5549
5798
  w = MIN_ELEMENT_SIZE;
@@ -6606,48 +6855,8 @@ var TemplateTool = class {
6606
6855
  }
6607
6856
  };
6608
6857
 
6609
- // src/history/layer-commands.ts
6610
- var CreateLayerCommand = class {
6611
- constructor(manager, layer) {
6612
- this.manager = manager;
6613
- this.layer = layer;
6614
- }
6615
- execute(_store) {
6616
- this.manager.addLayerDirect(this.layer);
6617
- }
6618
- undo(_store) {
6619
- this.manager.removeLayerDirect(this.layer.id);
6620
- }
6621
- };
6622
- var RemoveLayerCommand = class {
6623
- constructor(manager, layer) {
6624
- this.manager = manager;
6625
- this.layer = layer;
6626
- }
6627
- execute(_store) {
6628
- this.manager.removeLayerDirect(this.layer.id);
6629
- }
6630
- undo(_store) {
6631
- this.manager.addLayerDirect(this.layer);
6632
- }
6633
- };
6634
- var UpdateLayerCommand = class {
6635
- constructor(manager, layerId, previous, current) {
6636
- this.manager = manager;
6637
- this.layerId = layerId;
6638
- this.previous = previous;
6639
- this.current = current;
6640
- }
6641
- execute(_store) {
6642
- this.manager.updateLayerDirect(this.layerId, { ...this.current });
6643
- }
6644
- undo(_store) {
6645
- this.manager.updateLayerDirect(this.layerId, { ...this.previous });
6646
- }
6647
- };
6648
-
6649
6858
  // src/index.ts
6650
- var VERSION = "0.14.0";
6859
+ var VERSION = "0.16.0";
6651
6860
  // Annotate the CommonJS export names for ESM import in node:
6652
6861
  0 && (module.exports = {
6653
6862
  AddElementCommand,
package/dist/index.d.cts CHANGED
@@ -204,7 +204,9 @@ declare class ElementStore {
204
204
  private layerOrderMap;
205
205
  private spatialIndex;
206
206
  private sortedCache;
207
+ private _versions;
207
208
  get count(): number;
209
+ getVersion(id: string): number;
208
210
  setLayerOrder(order: Map<string, number>): void;
209
211
  getAll(): CanvasElement[];
210
212
  getById(id: string): CanvasElement | undefined;
@@ -217,6 +219,10 @@ declare class ElementStore {
217
219
  clear(): void;
218
220
  snapshot(): CanvasElement[];
219
221
  loadSnapshot(elements: CanvasElement[]): void;
222
+ bringToFront(id: string): void;
223
+ sendToBack(id: string): void;
224
+ bringForward(id: string): void;
225
+ sendBackward(id: string): void;
220
226
  queryRect(rect: Bounds): CanvasElement[];
221
227
  queryPoint(point: Point): CanvasElement[];
222
228
  on<K extends keyof ElementStoreEvents>(event: K, listener: (data: ElementStoreEvents[K]) => void): () => void;
@@ -264,6 +270,15 @@ declare function snapPoint(point: Point, gridSize: number): Point;
264
270
  declare function snapToHexCenter(point: Point, cellSize: number, orientation: HexOrientation): Point;
265
271
  declare function smartSnap(point: Point, ctx: ToolContext): Point;
266
272
 
273
+ interface LayerManagerEvents {
274
+ change: null;
275
+ create: Layer;
276
+ remove: Layer;
277
+ update: {
278
+ previous: Layer;
279
+ current: Layer;
280
+ };
281
+ }
267
282
  declare class LayerManager {
268
283
  private readonly store;
269
284
  private layers;
@@ -287,7 +302,7 @@ declare class LayerManager {
287
302
  moveElementToLayer(elementId: string, layerId: string): void;
288
303
  snapshot(): Layer[];
289
304
  loadSnapshot(layers: Layer[]): void;
290
- on(event: 'change', callback: () => void): () => void;
305
+ on<K extends keyof LayerManagerEvents>(event: K, callback: (data: LayerManagerEvents[K]) => void): () => void;
291
306
  addLayerDirect(layer: Layer): void;
292
307
  removeLayerDirect(id: string): void;
293
308
  updateLayerDirect(id: string, props: Omit<Partial<Layer>, 'id'>): void;
@@ -393,11 +408,12 @@ declare class HistoryStack {
393
408
  declare class HistoryRecorder {
394
409
  private readonly store;
395
410
  private readonly stack;
411
+ private readonly layerManager?;
396
412
  private recording;
397
413
  private transaction;
398
414
  private updateSnapshots;
399
415
  private unsubscribers;
400
- constructor(store: ElementStore, stack: HistoryStack);
416
+ constructor(store: ElementStore, stack: HistoryStack, layerManager?: LayerManager | undefined);
401
417
  pause(): void;
402
418
  resume(): void;
403
419
  begin(): void;
@@ -408,6 +424,9 @@ declare class HistoryRecorder {
408
424
  private onAdd;
409
425
  private onRemove;
410
426
  private onUpdate;
427
+ private onLayerCreate;
428
+ private onLayerRemove;
429
+ private onLayerUpdate;
411
430
  private flushUpdateSnapshots;
412
431
  }
413
432
 
@@ -462,6 +481,7 @@ declare class InputHandler {
462
481
  private handleRedo;
463
482
  private handleCopy;
464
483
  private handlePaste;
484
+ private handleZOrder;
465
485
  private cancelToolIfActive;
466
486
  }
467
487
 
@@ -552,6 +572,11 @@ interface ViewportOptions {
552
572
  background?: BackgroundOptions;
553
573
  fontSizePresets?: FontSizePreset[];
554
574
  toolbar?: boolean;
575
+ onHtmlElementMount?: (elementId: string, domId: string | undefined, container: HTMLDivElement) => void;
576
+ onDrop?: (event: DragEvent, worldPosition: {
577
+ x: number;
578
+ y: number;
579
+ }) => void;
555
580
  }
556
581
  declare class Viewport {
557
582
  private readonly container;
@@ -577,6 +602,8 @@ declare class Viewport {
577
602
  private readonly renderLoop;
578
603
  private readonly domNodeManager;
579
604
  private readonly interactMode;
605
+ private readonly onHtmlElementMount?;
606
+ private readonly dropHandler?;
580
607
  private readonly gridChangeListeners;
581
608
  private readonly doubleTapDetector;
582
609
  private tapDownX;
@@ -607,6 +634,8 @@ declare class Viewport {
607
634
  w: number;
608
635
  h: number;
609
636
  }): string;
637
+ removeLayer(id: string): void;
638
+ updateHtmlElement(id: string, newContent: HTMLElement): void;
610
639
  addGrid(input: {
611
640
  gridType?: 'square' | 'hex';
612
641
  hexOrientation?: 'pointy' | 'flat';
@@ -935,6 +964,7 @@ declare class SelectTool implements Tool {
935
964
  private ctx;
936
965
  private pendingSingleSelectId;
937
966
  private hasDragged;
967
+ private resizeAspectRatio;
938
968
  get selectedIds(): string[];
939
969
  setSelection(ids: string[]): void;
940
970
  get isMarqueeActive(): boolean;
@@ -1181,6 +1211,6 @@ declare class UpdateLayerCommand implements Command {
1181
1211
  undo(_store: ElementStore): void;
1182
1212
  }
1183
1213
 
1184
- declare const VERSION = "0.14.0";
1214
+ declare const VERSION = "0.16.0";
1185
1215
 
1186
1216
  export { type ActiveFormats, AddElementCommand, type ArrowElement, ArrowTool, type ArrowToolOptions, AutoSave, type AutoSaveOptions, Background, type BackgroundOptions, type BackgroundPattern, BatchCommand, type Binding, type Bounds, Camera, type CameraChangeInfo, type CameraOptions, type CanvasElement, type CanvasState, type Command, CreateLayerCommand, DEFAULT_FONT_SIZE_PRESETS, DEFAULT_NOTE_FONT_SIZE, DoubleTapDetector, type DoubleTapDetectorOptions, ElementRenderer, ElementStore, type ElementType, type ElementUpdateEvent, EraserTool, type EraserToolOptions, EventBus, type ExportImageOptions, type FilterAction, type FilteredEvent, type FilteredUpEvent, type FontSizePreset, type GridElement, type GridInfo, HandTool, type HexOrientation, HistoryRecorder, HistoryStack, type HistoryStackOptions, type HtmlElement, type ImageElement, ImageTool, type ImageToolOptions, InputFilter, InputHandler, type Layer, LayerManager, MeasureTool, type MeasureToolOptions, type Measurement, NoteEditor, type NoteEditorOptions, type NoteElement, NoteTool, type NoteToolOptions, NoteToolbar, PencilTool, type PencilToolOptions, type Point, type PointerState, Quadtree, RemoveElementCommand, RemoveLayerCommand, type RenderStatsSnapshot, SelectTool, type ShapeElement, type ShapeKind, ShapeTool, type ShapeToolOptions, type Size, type StrokeElement, type StrokePoint, type StyledRun, type TemplateElement, type TemplateShape, TemplateTool, type TemplateToolOptions, type TextElement, TextTool, type TextToolOptions, type Tool, type ToolContext, ToolManager, type ToolName, UpdateElementCommand, UpdateLayerCommand, VERSION, Viewport, type ViewportOptions, boundsIntersect, clearStaleBindings, createArrow, createGrid, createHtmlElement, createId, createImage, createNote, createShape, createStroke, createTemplate, createText, drawHexPath, exportImage, exportState, findBindTarget, findBoundArrows, getActiveFormats, getArrowBounds, getArrowControlPoint, getArrowMidpoint, getArrowTangentAngle, getBendFromPoint, getEdgeIntersection, getElementBounds, getElementCenter, getElementsBoundingBox, getHexCellsInCone, getHexCellsInLine, getHexCellsInRadius, getHexCellsInSquare, getHexDistance, isBindable, isNearBezier, parseState, sanitizeNoteHtml, setFontSize, smartSnap, snapPoint, snapToHexCenter, toggleBold, toggleItalic, toggleStrikethrough, toggleUnderline, unbindArrow, updateBoundArrow };
package/dist/index.d.ts CHANGED
@@ -204,7 +204,9 @@ declare class ElementStore {
204
204
  private layerOrderMap;
205
205
  private spatialIndex;
206
206
  private sortedCache;
207
+ private _versions;
207
208
  get count(): number;
209
+ getVersion(id: string): number;
208
210
  setLayerOrder(order: Map<string, number>): void;
209
211
  getAll(): CanvasElement[];
210
212
  getById(id: string): CanvasElement | undefined;
@@ -217,6 +219,10 @@ declare class ElementStore {
217
219
  clear(): void;
218
220
  snapshot(): CanvasElement[];
219
221
  loadSnapshot(elements: CanvasElement[]): void;
222
+ bringToFront(id: string): void;
223
+ sendToBack(id: string): void;
224
+ bringForward(id: string): void;
225
+ sendBackward(id: string): void;
220
226
  queryRect(rect: Bounds): CanvasElement[];
221
227
  queryPoint(point: Point): CanvasElement[];
222
228
  on<K extends keyof ElementStoreEvents>(event: K, listener: (data: ElementStoreEvents[K]) => void): () => void;
@@ -264,6 +270,15 @@ declare function snapPoint(point: Point, gridSize: number): Point;
264
270
  declare function snapToHexCenter(point: Point, cellSize: number, orientation: HexOrientation): Point;
265
271
  declare function smartSnap(point: Point, ctx: ToolContext): Point;
266
272
 
273
+ interface LayerManagerEvents {
274
+ change: null;
275
+ create: Layer;
276
+ remove: Layer;
277
+ update: {
278
+ previous: Layer;
279
+ current: Layer;
280
+ };
281
+ }
267
282
  declare class LayerManager {
268
283
  private readonly store;
269
284
  private layers;
@@ -287,7 +302,7 @@ declare class LayerManager {
287
302
  moveElementToLayer(elementId: string, layerId: string): void;
288
303
  snapshot(): Layer[];
289
304
  loadSnapshot(layers: Layer[]): void;
290
- on(event: 'change', callback: () => void): () => void;
305
+ on<K extends keyof LayerManagerEvents>(event: K, callback: (data: LayerManagerEvents[K]) => void): () => void;
291
306
  addLayerDirect(layer: Layer): void;
292
307
  removeLayerDirect(id: string): void;
293
308
  updateLayerDirect(id: string, props: Omit<Partial<Layer>, 'id'>): void;
@@ -393,11 +408,12 @@ declare class HistoryStack {
393
408
  declare class HistoryRecorder {
394
409
  private readonly store;
395
410
  private readonly stack;
411
+ private readonly layerManager?;
396
412
  private recording;
397
413
  private transaction;
398
414
  private updateSnapshots;
399
415
  private unsubscribers;
400
- constructor(store: ElementStore, stack: HistoryStack);
416
+ constructor(store: ElementStore, stack: HistoryStack, layerManager?: LayerManager | undefined);
401
417
  pause(): void;
402
418
  resume(): void;
403
419
  begin(): void;
@@ -408,6 +424,9 @@ declare class HistoryRecorder {
408
424
  private onAdd;
409
425
  private onRemove;
410
426
  private onUpdate;
427
+ private onLayerCreate;
428
+ private onLayerRemove;
429
+ private onLayerUpdate;
411
430
  private flushUpdateSnapshots;
412
431
  }
413
432
 
@@ -462,6 +481,7 @@ declare class InputHandler {
462
481
  private handleRedo;
463
482
  private handleCopy;
464
483
  private handlePaste;
484
+ private handleZOrder;
465
485
  private cancelToolIfActive;
466
486
  }
467
487
 
@@ -552,6 +572,11 @@ interface ViewportOptions {
552
572
  background?: BackgroundOptions;
553
573
  fontSizePresets?: FontSizePreset[];
554
574
  toolbar?: boolean;
575
+ onHtmlElementMount?: (elementId: string, domId: string | undefined, container: HTMLDivElement) => void;
576
+ onDrop?: (event: DragEvent, worldPosition: {
577
+ x: number;
578
+ y: number;
579
+ }) => void;
555
580
  }
556
581
  declare class Viewport {
557
582
  private readonly container;
@@ -577,6 +602,8 @@ declare class Viewport {
577
602
  private readonly renderLoop;
578
603
  private readonly domNodeManager;
579
604
  private readonly interactMode;
605
+ private readonly onHtmlElementMount?;
606
+ private readonly dropHandler?;
580
607
  private readonly gridChangeListeners;
581
608
  private readonly doubleTapDetector;
582
609
  private tapDownX;
@@ -607,6 +634,8 @@ declare class Viewport {
607
634
  w: number;
608
635
  h: number;
609
636
  }): string;
637
+ removeLayer(id: string): void;
638
+ updateHtmlElement(id: string, newContent: HTMLElement): void;
610
639
  addGrid(input: {
611
640
  gridType?: 'square' | 'hex';
612
641
  hexOrientation?: 'pointy' | 'flat';
@@ -935,6 +964,7 @@ declare class SelectTool implements Tool {
935
964
  private ctx;
936
965
  private pendingSingleSelectId;
937
966
  private hasDragged;
967
+ private resizeAspectRatio;
938
968
  get selectedIds(): string[];
939
969
  setSelection(ids: string[]): void;
940
970
  get isMarqueeActive(): boolean;
@@ -1181,6 +1211,6 @@ declare class UpdateLayerCommand implements Command {
1181
1211
  undo(_store: ElementStore): void;
1182
1212
  }
1183
1213
 
1184
- declare const VERSION = "0.14.0";
1214
+ declare const VERSION = "0.16.0";
1185
1215
 
1186
1216
  export { type ActiveFormats, AddElementCommand, type ArrowElement, ArrowTool, type ArrowToolOptions, AutoSave, type AutoSaveOptions, Background, type BackgroundOptions, type BackgroundPattern, BatchCommand, type Binding, type Bounds, Camera, type CameraChangeInfo, type CameraOptions, type CanvasElement, type CanvasState, type Command, CreateLayerCommand, DEFAULT_FONT_SIZE_PRESETS, DEFAULT_NOTE_FONT_SIZE, DoubleTapDetector, type DoubleTapDetectorOptions, ElementRenderer, ElementStore, type ElementType, type ElementUpdateEvent, EraserTool, type EraserToolOptions, EventBus, type ExportImageOptions, type FilterAction, type FilteredEvent, type FilteredUpEvent, type FontSizePreset, type GridElement, type GridInfo, HandTool, type HexOrientation, HistoryRecorder, HistoryStack, type HistoryStackOptions, type HtmlElement, type ImageElement, ImageTool, type ImageToolOptions, InputFilter, InputHandler, type Layer, LayerManager, MeasureTool, type MeasureToolOptions, type Measurement, NoteEditor, type NoteEditorOptions, type NoteElement, NoteTool, type NoteToolOptions, NoteToolbar, PencilTool, type PencilToolOptions, type Point, type PointerState, Quadtree, RemoveElementCommand, RemoveLayerCommand, type RenderStatsSnapshot, SelectTool, type ShapeElement, type ShapeKind, ShapeTool, type ShapeToolOptions, type Size, type StrokeElement, type StrokePoint, type StyledRun, type TemplateElement, type TemplateShape, TemplateTool, type TemplateToolOptions, type TextElement, TextTool, type TextToolOptions, type Tool, type ToolContext, ToolManager, type ToolName, UpdateElementCommand, UpdateLayerCommand, VERSION, Viewport, type ViewportOptions, boundsIntersect, clearStaleBindings, createArrow, createGrid, createHtmlElement, createId, createImage, createNote, createShape, createStroke, createTemplate, createText, drawHexPath, exportImage, exportState, findBindTarget, findBoundArrows, getActiveFormats, getArrowBounds, getArrowControlPoint, getArrowMidpoint, getArrowTangentAngle, getBendFromPoint, getEdgeIntersection, getElementBounds, getElementCenter, getElementsBoundingBox, getHexCellsInCone, getHexCellsInLine, getHexCellsInRadius, getHexCellsInSquare, getHexDistance, isBindable, isNearBezier, parseState, sanitizeNoteHtml, setFontSize, smartSnap, snapPoint, snapToHexCenter, toggleBold, toggleItalic, toggleStrikethrough, toggleUnderline, unbindArrow, updateBoundArrow };
package/dist/index.js CHANGED
@@ -975,6 +975,14 @@ var InputHandler = class {
975
975
  e.preventDefault();
976
976
  this.handlePaste();
977
977
  }
978
+ if (e.key === "]") {
979
+ e.preventDefault();
980
+ this.handleZOrder(e.ctrlKey || e.metaKey ? "front" : "forward");
981
+ }
982
+ if (e.key === "[") {
983
+ e.preventDefault();
984
+ this.handleZOrder(e.ctrlKey || e.metaKey ? "back" : "backward");
985
+ }
978
986
  };
979
987
  onKeyUp = (e) => {
980
988
  if (e.key === " ") {
@@ -1151,6 +1159,33 @@ var InputHandler = class {
1151
1159
  selectTool.setSelection(newIds);
1152
1160
  this.toolContext.requestRender();
1153
1161
  }
1162
+ handleZOrder(operation) {
1163
+ if (!this.toolManager || !this.toolContext) return;
1164
+ const tool = this.toolManager.activeTool;
1165
+ if (tool?.name !== "select") return;
1166
+ const selectTool = tool;
1167
+ const ids = selectTool.selectedIds;
1168
+ if (ids.length === 0) return;
1169
+ this.historyRecorder?.begin();
1170
+ for (const id of ids) {
1171
+ switch (operation) {
1172
+ case "forward":
1173
+ this.toolContext.store.bringForward(id);
1174
+ break;
1175
+ case "backward":
1176
+ this.toolContext.store.sendBackward(id);
1177
+ break;
1178
+ case "front":
1179
+ this.toolContext.store.bringToFront(id);
1180
+ break;
1181
+ case "back":
1182
+ this.toolContext.store.sendToBack(id);
1183
+ break;
1184
+ }
1185
+ }
1186
+ this.historyRecorder?.commit();
1187
+ this.toolContext.requestRender();
1188
+ }
1154
1189
  cancelToolIfActive(e) {
1155
1190
  if (this.isToolActive) {
1156
1191
  this.dispatchToolUp(e);
@@ -1447,9 +1482,13 @@ var ElementStore = class {
1447
1482
  layerOrderMap = /* @__PURE__ */ new Map();
1448
1483
  spatialIndex = new Quadtree({ x: -1e5, y: -1e5, w: 2e5, h: 2e5 });
1449
1484
  sortedCache = null;
1485
+ _versions = /* @__PURE__ */ new Map();
1450
1486
  get count() {
1451
1487
  return this.elements.size;
1452
1488
  }
1489
+ getVersion(id) {
1490
+ return this._versions.get(id) ?? -1;
1491
+ }
1453
1492
  setLayerOrder(order) {
1454
1493
  this.layerOrderMap = new Map(order);
1455
1494
  this.sortedCache = null;
@@ -1474,6 +1513,7 @@ var ElementStore = class {
1474
1513
  }
1475
1514
  add(element) {
1476
1515
  this.sortedCache = null;
1516
+ this._versions.set(element.id, 0);
1477
1517
  this.elements.set(element.id, element);
1478
1518
  const bounds = getElementBounds(element);
1479
1519
  if (bounds) this.spatialIndex.insert(element.id, bounds);
@@ -1483,6 +1523,7 @@ var ElementStore = class {
1483
1523
  const existing = this.elements.get(id);
1484
1524
  if (!existing) return;
1485
1525
  this.sortedCache = null;
1526
+ this._versions.set(id, (this._versions.get(id) ?? 0) + 1);
1486
1527
  const updated = { ...existing, ...partial, id: existing.id, type: existing.type };
1487
1528
  if (updated.type === "arrow") {
1488
1529
  const arrow = updated;
@@ -1502,12 +1543,14 @@ var ElementStore = class {
1502
1543
  const element = this.elements.get(id);
1503
1544
  if (!element) return;
1504
1545
  this.sortedCache = null;
1546
+ this._versions.delete(id);
1505
1547
  this.elements.delete(id);
1506
1548
  this.spatialIndex.remove(id);
1507
1549
  this.bus.emit("remove", element);
1508
1550
  }
1509
1551
  clear() {
1510
1552
  this.sortedCache = null;
1553
+ this._versions.clear();
1511
1554
  this.elements.clear();
1512
1555
  this.spatialIndex.clear();
1513
1556
  this.bus.emit("clear", null);
@@ -1517,10 +1560,12 @@ var ElementStore = class {
1517
1560
  }
1518
1561
  loadSnapshot(elements) {
1519
1562
  this.sortedCache = null;
1563
+ this._versions.clear();
1520
1564
  this.elements.clear();
1521
1565
  this.spatialIndex.clear();
1522
1566
  for (const el of elements) {
1523
1567
  this.elements.set(el.id, el);
1568
+ this._versions.set(el.id, 0);
1524
1569
  const bounds = getElementBounds(el);
1525
1570
  if (bounds) this.spatialIndex.insert(el.id, bounds);
1526
1571
  }
@@ -1529,6 +1574,54 @@ var ElementStore = class {
1529
1574
  this.bus.emit("add", el);
1530
1575
  }
1531
1576
  }
1577
+ bringToFront(id) {
1578
+ const el = this.elements.get(id);
1579
+ if (!el) return;
1580
+ const siblings = [...this.elements.values()].filter(
1581
+ (e) => e.layerId === el.layerId && e.id !== id
1582
+ );
1583
+ if (siblings.length === 0) return;
1584
+ const maxZ = Math.max(...siblings.map((e) => e.zIndex));
1585
+ if (el.zIndex >= maxZ) return;
1586
+ this.update(id, { zIndex: maxZ + 1 });
1587
+ }
1588
+ sendToBack(id) {
1589
+ const el = this.elements.get(id);
1590
+ if (!el) return;
1591
+ const siblings = [...this.elements.values()].filter(
1592
+ (e) => e.layerId === el.layerId && e.id !== id
1593
+ );
1594
+ if (siblings.length === 0) return;
1595
+ const minZ = Math.min(...siblings.map((e) => e.zIndex));
1596
+ if (el.zIndex <= minZ) return;
1597
+ this.update(id, { zIndex: minZ - 1 });
1598
+ }
1599
+ bringForward(id) {
1600
+ const el = this.elements.get(id);
1601
+ if (!el) return;
1602
+ const sorted = [...this.elements.values()].filter((e) => e.layerId === el.layerId).sort((a, b) => a.zIndex - b.zIndex);
1603
+ const idx = sorted.findIndex((e) => e.id === id);
1604
+ if (idx < 0 || idx >= sorted.length - 1) return;
1605
+ const next = sorted[idx + 1];
1606
+ if (!next) return;
1607
+ const myZ = el.zIndex;
1608
+ const nextZ = next.zIndex;
1609
+ this.update(id, { zIndex: nextZ });
1610
+ this.update(next.id, { zIndex: myZ });
1611
+ }
1612
+ sendBackward(id) {
1613
+ const el = this.elements.get(id);
1614
+ if (!el) return;
1615
+ const sorted = [...this.elements.values()].filter((e) => e.layerId === el.layerId).sort((a, b) => a.zIndex - b.zIndex);
1616
+ const idx = sorted.findIndex((e) => e.id === id);
1617
+ if (idx <= 0) return;
1618
+ const prev = sorted[idx - 1];
1619
+ if (!prev) return;
1620
+ const myZ = el.zIndex;
1621
+ const prevZ = prev.zIndex;
1622
+ this.update(id, { zIndex: prevZ });
1623
+ this.update(prev.id, { zIndex: myZ });
1624
+ }
1532
1625
  queryRect(rect) {
1533
1626
  const ids = this.spatialIndex.query(rect);
1534
1627
  const elements = [];
@@ -3200,16 +3293,64 @@ var BatchCommand = class {
3200
3293
  }
3201
3294
  };
3202
3295
 
3296
+ // src/history/layer-commands.ts
3297
+ var CreateLayerCommand = class {
3298
+ constructor(manager, layer) {
3299
+ this.manager = manager;
3300
+ this.layer = layer;
3301
+ }
3302
+ execute(_store) {
3303
+ this.manager.addLayerDirect(this.layer);
3304
+ }
3305
+ undo(_store) {
3306
+ this.manager.removeLayerDirect(this.layer.id);
3307
+ }
3308
+ };
3309
+ var RemoveLayerCommand = class {
3310
+ constructor(manager, layer) {
3311
+ this.manager = manager;
3312
+ this.layer = layer;
3313
+ }
3314
+ execute(_store) {
3315
+ this.manager.removeLayerDirect(this.layer.id);
3316
+ }
3317
+ undo(_store) {
3318
+ this.manager.addLayerDirect(this.layer);
3319
+ }
3320
+ };
3321
+ var UpdateLayerCommand = class {
3322
+ constructor(manager, layerId, previous, current) {
3323
+ this.manager = manager;
3324
+ this.layerId = layerId;
3325
+ this.previous = previous;
3326
+ this.current = current;
3327
+ }
3328
+ execute(_store) {
3329
+ this.manager.updateLayerDirect(this.layerId, { ...this.current });
3330
+ }
3331
+ undo(_store) {
3332
+ this.manager.updateLayerDirect(this.layerId, { ...this.previous });
3333
+ }
3334
+ };
3335
+
3203
3336
  // src/history/history-recorder.ts
3204
3337
  var HistoryRecorder = class {
3205
- constructor(store, stack) {
3338
+ constructor(store, stack, layerManager) {
3206
3339
  this.store = store;
3207
3340
  this.stack = stack;
3341
+ this.layerManager = layerManager;
3208
3342
  this.unsubscribers = [
3209
3343
  store.on("add", (el) => this.onAdd(el)),
3210
3344
  store.on("remove", (el) => this.onRemove(el)),
3211
3345
  store.on("update", ({ previous, current }) => this.onUpdate(previous, current))
3212
3346
  ];
3347
+ if (layerManager) {
3348
+ this.unsubscribers.push(
3349
+ layerManager.on("create", (layer) => this.onLayerCreate(layer)),
3350
+ layerManager.on("remove", (layer) => this.onLayerRemove(layer)),
3351
+ layerManager.on("update", ({ previous, current }) => this.onLayerUpdate(previous, current))
3352
+ );
3353
+ }
3213
3354
  }
3214
3355
  recording = true;
3215
3356
  transaction = null;
@@ -3270,6 +3411,21 @@ var HistoryRecorder = class {
3270
3411
  this.stack.push(new UpdateElementCommand(current.id, previous, current));
3271
3412
  }
3272
3413
  }
3414
+ onLayerCreate(layer) {
3415
+ if (!this.recording) return;
3416
+ if (!this.layerManager) return;
3417
+ this.record(new CreateLayerCommand(this.layerManager, layer));
3418
+ }
3419
+ onLayerRemove(layer) {
3420
+ if (!this.recording) return;
3421
+ if (!this.layerManager) return;
3422
+ this.record(new RemoveLayerCommand(this.layerManager, layer));
3423
+ }
3424
+ onLayerUpdate(previous, current) {
3425
+ if (!this.recording) return;
3426
+ if (!this.layerManager) return;
3427
+ this.record(new UpdateLayerCommand(this.layerManager, current.id, previous, current));
3428
+ }
3273
3429
  flushUpdateSnapshots() {
3274
3430
  const commands = [];
3275
3431
  for (const [id, previous] of this.updateSnapshots) {
@@ -3702,18 +3858,23 @@ var LayerManager = class {
3702
3858
  addLayerDirect(layer) {
3703
3859
  this.layers.set(layer.id, { ...layer });
3704
3860
  this.syncLayerOrder();
3861
+ this.bus.emit("create", { ...layer });
3705
3862
  this.bus.emit("change", null);
3706
3863
  }
3707
3864
  removeLayerDirect(id) {
3865
+ const layer = this.layers.get(id);
3708
3866
  this.layers.delete(id);
3709
3867
  this.syncLayerOrder();
3868
+ if (layer) this.bus.emit("remove", { ...layer });
3710
3869
  this.bus.emit("change", null);
3711
3870
  }
3712
3871
  updateLayerDirect(id, props) {
3713
3872
  const layer = this.layers.get(id);
3714
3873
  if (!layer) return;
3874
+ const previous = { ...layer };
3715
3875
  Object.assign(layer, props);
3716
3876
  if ("order" in props) this.syncLayerOrder();
3877
+ this.bus.emit("update", { previous, current: { ...layer } });
3717
3878
  this.bus.emit("change", null);
3718
3879
  }
3719
3880
  syncLayerOrder() {
@@ -3791,10 +3952,14 @@ var DomNodeManager = class {
3791
3952
  domLayer;
3792
3953
  onEditRequest;
3793
3954
  isEditingElement;
3955
+ getVersion;
3956
+ lastSyncedVersion = /* @__PURE__ */ new Map();
3957
+ lastSyncedZIndex = /* @__PURE__ */ new Map();
3794
3958
  constructor(deps) {
3795
3959
  this.domLayer = deps.domLayer;
3796
3960
  this.onEditRequest = deps.onEditRequest;
3797
3961
  this.isEditingElement = deps.isEditingElement;
3962
+ this.getVersion = deps.getVersion ?? null;
3798
3963
  }
3799
3964
  getNode(id) {
3800
3965
  return this.domNodes.get(id);
@@ -3802,6 +3967,20 @@ var DomNodeManager = class {
3802
3967
  storeHtmlContent(elementId, dom) {
3803
3968
  this.htmlContent.set(elementId, dom);
3804
3969
  }
3970
+ hasContent(elementId) {
3971
+ return this.htmlContent.has(elementId);
3972
+ }
3973
+ resetHtmlContent(elementId) {
3974
+ this.htmlContent.delete(elementId);
3975
+ this.lastSyncedVersion.delete(elementId);
3976
+ this.lastSyncedZIndex.delete(elementId);
3977
+ const node = this.domNodes.get(elementId);
3978
+ if (!node) return;
3979
+ while (node.firstChild) {
3980
+ node.removeChild(node.firstChild);
3981
+ }
3982
+ delete node.dataset["initialized"];
3983
+ }
3805
3984
  syncDomNode(element, zIndex = 0) {
3806
3985
  let node = this.domNodes.get(element.id);
3807
3986
  if (!node) {
@@ -3813,6 +3992,17 @@ var DomNodeManager = class {
3813
3992
  });
3814
3993
  this.domLayer.appendChild(node);
3815
3994
  this.domNodes.set(element.id, node);
3995
+ } else if (this.getVersion) {
3996
+ const currentVersion = this.getVersion(element.id);
3997
+ const lastVersion = this.lastSyncedVersion.get(element.id);
3998
+ const lastZ = this.lastSyncedZIndex.get(element.id);
3999
+ if (lastVersion === currentVersion && lastZ === zIndex) {
4000
+ return;
4001
+ }
4002
+ }
4003
+ if (this.getVersion) {
4004
+ this.lastSyncedVersion.set(element.id, this.getVersion(element.id));
4005
+ this.lastSyncedZIndex.set(element.id, zIndex);
3816
4006
  }
3817
4007
  const size = "size" in element ? element.size : null;
3818
4008
  Object.assign(node.style, {
@@ -3831,6 +4021,8 @@ var DomNodeManager = class {
3831
4021
  }
3832
4022
  removeDomNode(id) {
3833
4023
  this.htmlContent.delete(id);
4024
+ this.lastSyncedVersion.delete(id);
4025
+ this.lastSyncedZIndex.delete(id);
3834
4026
  const node = this.domNodes.get(id);
3835
4027
  if (node) {
3836
4028
  node.remove();
@@ -3841,6 +4033,8 @@ var DomNodeManager = class {
3841
4033
  this.domNodes.forEach((node) => node.remove());
3842
4034
  this.domNodes.clear();
3843
4035
  this.htmlContent.clear();
4036
+ this.lastSyncedVersion.clear();
4037
+ this.lastSyncedZIndex.clear();
3844
4038
  }
3845
4039
  reattachHtmlContent(store) {
3846
4040
  for (const el of store.getElementsByType("html")) {
@@ -4328,8 +4522,10 @@ var Viewport = class {
4328
4522
  toolbar: options.toolbar
4329
4523
  });
4330
4524
  this.noteEditor.setOnStop((id) => this.onTextEditStop(id));
4525
+ this.onHtmlElementMount = options.onHtmlElementMount;
4526
+ this.dropHandler = options.onDrop;
4331
4527
  this.history = new HistoryStack();
4332
- this.historyRecorder = new HistoryRecorder(this.store, this.history);
4528
+ this.historyRecorder = new HistoryRecorder(this.store, this.history, this.layerManager);
4333
4529
  this.wrapper = this.createWrapper();
4334
4530
  this.canvasEl = this.createCanvas();
4335
4531
  this.domLayer = this.createDomLayer();
@@ -4360,7 +4556,8 @@ var Viewport = class {
4360
4556
  this.domNodeManager = new DomNodeManager({
4361
4557
  domLayer: this.domLayer,
4362
4558
  onEditRequest: (id) => this.startEditingElement(id),
4363
- isEditingElement: (id) => this.noteEditor.isEditing && this.noteEditor.editingElementId === id
4559
+ isEditingElement: (id) => this.noteEditor.isEditing && this.noteEditor.editingElementId === id,
4560
+ getVersion: (id) => this.store.getVersion(id)
4364
4561
  });
4365
4562
  this.interactMode = new InteractMode({
4366
4563
  getNode: (id) => this.domNodeManager.getNode(id)
@@ -4448,6 +4645,8 @@ var Viewport = class {
4448
4645
  renderLoop;
4449
4646
  domNodeManager;
4450
4647
  interactMode;
4648
+ onHtmlElementMount;
4649
+ dropHandler;
4451
4650
  gridChangeListeners = /* @__PURE__ */ new Set();
4452
4651
  doubleTapDetector = new DoubleTapDetector();
4453
4652
  tapDownX = 0;
@@ -4491,6 +4690,22 @@ var Viewport = class {
4491
4690
  this.layerManager.setActiveLayer(state.activeLayerId);
4492
4691
  }
4493
4692
  this.domNodeManager.reattachHtmlContent(this.store);
4693
+ if (this.onHtmlElementMount) {
4694
+ for (const el of this.store.getElementsByType("html")) {
4695
+ if (!this.domNodeManager.hasContent(el.id)) {
4696
+ this.domNodeManager.syncDomNode(el);
4697
+ const node = this.domNodeManager.getNode(el.id);
4698
+ if (node) {
4699
+ this.onHtmlElementMount(el.id, el.domId, node);
4700
+ node.dataset["initialized"] = "true";
4701
+ Object.assign(node.style, {
4702
+ overflow: "hidden",
4703
+ pointerEvents: el.interactive ? "auto" : "none"
4704
+ });
4705
+ }
4706
+ }
4707
+ }
4708
+ }
4494
4709
  this.history.clear();
4495
4710
  this.historyRecorder.resume();
4496
4711
  this.camera.moveTo(state.camera.position.x, state.camera.position.y);
@@ -4536,6 +4751,19 @@ var Viewport = class {
4536
4751
  this.requestRender();
4537
4752
  return el.id;
4538
4753
  }
4754
+ removeLayer(id) {
4755
+ this.historyRecorder.begin();
4756
+ this.layerManager.removeLayer(id);
4757
+ this.historyRecorder.commit();
4758
+ }
4759
+ updateHtmlElement(id, newContent) {
4760
+ const el = this.store.getById(id);
4761
+ if (!el) throw new Error(`Element not found: ${id}`);
4762
+ if (el.type !== "html") throw new Error(`Element ${id} is not an HTML element`);
4763
+ this.domNodeManager.resetHtmlContent(id);
4764
+ this.domNodeManager.storeHtmlContent(id, newContent);
4765
+ this.requestRender();
4766
+ }
4539
4767
  addGrid(input) {
4540
4768
  const existing = this.store.getElementsByType("grid")[0];
4541
4769
  this.historyRecorder.begin();
@@ -4687,17 +4915,21 @@ var Viewport = class {
4687
4915
  };
4688
4916
  onDrop = (e) => {
4689
4917
  e.preventDefault();
4918
+ const rect = this.wrapper.getBoundingClientRect();
4919
+ const screenPos = { x: e.clientX - rect.left, y: e.clientY - rect.top };
4920
+ const worldPos = this.camera.screenToWorld(screenPos);
4921
+ if (this.dropHandler) {
4922
+ this.dropHandler(e, worldPos);
4923
+ return;
4924
+ }
4690
4925
  const files = e.dataTransfer?.files;
4691
4926
  if (!files) return;
4692
- const rect = this.wrapper.getBoundingClientRect();
4693
4927
  for (const file of files) {
4694
4928
  if (!file.type.startsWith("image/")) continue;
4695
4929
  const reader = new FileReader();
4696
4930
  reader.onload = () => {
4697
4931
  const src = reader.result;
4698
4932
  if (typeof src !== "string") return;
4699
- const screenPos = { x: e.clientX - rect.left, y: e.clientY - rect.top };
4700
- const worldPos = this.camera.screenToWorld(screenPos);
4701
4933
  this.addImage(src, worldPos);
4702
4934
  };
4703
4935
  reader.readAsDataURL(file);
@@ -5183,6 +5415,7 @@ var SelectTool = class {
5183
5415
  ctx = null;
5184
5416
  pendingSingleSelectId = null;
5185
5417
  hasDragged = false;
5418
+ resizeAspectRatio = 0;
5186
5419
  get selectedIds() {
5187
5420
  return [...this._selectedIds];
5188
5421
  }
@@ -5228,7 +5461,8 @@ var SelectTool = class {
5228
5461
  const resizeHit = this.hitTestResizeHandle(world, ctx);
5229
5462
  if (resizeHit) {
5230
5463
  const el = ctx.store.getById(resizeHit.elementId);
5231
- if (el) {
5464
+ if (el && "size" in el) {
5465
+ this.resizeAspectRatio = el.size.h > 0 ? el.size.w / el.size.h : 0;
5232
5466
  this.mode = {
5233
5467
  type: "resizing",
5234
5468
  elementId: resizeHit.elementId,
@@ -5280,7 +5514,7 @@ var SelectTool = class {
5280
5514
  }
5281
5515
  if (this.mode.type === "resizing") {
5282
5516
  ctx.setCursor?.(HANDLE_CURSORS[this.mode.handle]);
5283
- this.handleResize(world, ctx);
5517
+ this.handleResize(world, ctx, state.shiftKey);
5284
5518
  return;
5285
5519
  }
5286
5520
  if (this.mode.type === "dragging" && this._selectedIds.length > 0) {
@@ -5404,7 +5638,7 @@ var SelectTool = class {
5404
5638
  const hit = this.hitTest(world, ctx);
5405
5639
  ctx.setCursor?.(hit ? "move" : "default");
5406
5640
  }
5407
- handleResize(world, ctx) {
5641
+ handleResize(world, ctx, shiftKey = false) {
5408
5642
  if (this.mode.type !== "resizing") return;
5409
5643
  const el = ctx.store.getById(this.mode.elementId);
5410
5644
  if (!el || !("size" in el) || el.locked) return;
@@ -5435,6 +5669,21 @@ var SelectTool = class {
5435
5669
  h -= dy;
5436
5670
  break;
5437
5671
  }
5672
+ if (shiftKey && this.resizeAspectRatio > 0) {
5673
+ const absDw = Math.abs(w - el.size.w);
5674
+ const absDh = Math.abs(h - el.size.h);
5675
+ if (absDw >= absDh) {
5676
+ h = w / this.resizeAspectRatio;
5677
+ } else {
5678
+ w = h * this.resizeAspectRatio;
5679
+ }
5680
+ if (handle === "nw" || handle === "sw") {
5681
+ x = el.position.x + el.size.w - w;
5682
+ }
5683
+ if (handle === "nw" || handle === "ne") {
5684
+ y = el.position.y + el.size.h - h;
5685
+ }
5686
+ }
5438
5687
  if (w < MIN_ELEMENT_SIZE) {
5439
5688
  if (handle === "nw" || handle === "sw") x = el.position.x + el.size.w - MIN_ELEMENT_SIZE;
5440
5689
  w = MIN_ELEMENT_SIZE;
@@ -6497,48 +6746,8 @@ var TemplateTool = class {
6497
6746
  }
6498
6747
  };
6499
6748
 
6500
- // src/history/layer-commands.ts
6501
- var CreateLayerCommand = class {
6502
- constructor(manager, layer) {
6503
- this.manager = manager;
6504
- this.layer = layer;
6505
- }
6506
- execute(_store) {
6507
- this.manager.addLayerDirect(this.layer);
6508
- }
6509
- undo(_store) {
6510
- this.manager.removeLayerDirect(this.layer.id);
6511
- }
6512
- };
6513
- var RemoveLayerCommand = class {
6514
- constructor(manager, layer) {
6515
- this.manager = manager;
6516
- this.layer = layer;
6517
- }
6518
- execute(_store) {
6519
- this.manager.removeLayerDirect(this.layer.id);
6520
- }
6521
- undo(_store) {
6522
- this.manager.addLayerDirect(this.layer);
6523
- }
6524
- };
6525
- var UpdateLayerCommand = class {
6526
- constructor(manager, layerId, previous, current) {
6527
- this.manager = manager;
6528
- this.layerId = layerId;
6529
- this.previous = previous;
6530
- this.current = current;
6531
- }
6532
- execute(_store) {
6533
- this.manager.updateLayerDirect(this.layerId, { ...this.current });
6534
- }
6535
- undo(_store) {
6536
- this.manager.updateLayerDirect(this.layerId, { ...this.previous });
6537
- }
6538
- };
6539
-
6540
6749
  // src/index.ts
6541
- var VERSION = "0.14.0";
6750
+ var VERSION = "0.16.0";
6542
6751
  export {
6543
6752
  AddElementCommand,
6544
6753
  ArrowTool,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fieldnotes/core",
3
- "version": "0.14.0",
3
+ "version": "0.16.0",
4
4
  "description": "Vanilla TypeScript infinite canvas engine",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",