@fieldnotes/core 0.4.1 → 0.6.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
@@ -26,6 +26,7 @@ __export(index_exports, {
26
26
  Background: () => Background,
27
27
  BatchCommand: () => BatchCommand,
28
28
  Camera: () => Camera,
29
+ CreateLayerCommand: () => CreateLayerCommand,
29
30
  ElementRenderer: () => ElementRenderer,
30
31
  ElementStore: () => ElementStore,
31
32
  EraserTool: () => EraserTool,
@@ -35,15 +36,18 @@ __export(index_exports, {
35
36
  HistoryStack: () => HistoryStack,
36
37
  ImageTool: () => ImageTool,
37
38
  InputHandler: () => InputHandler,
39
+ LayerManager: () => LayerManager,
38
40
  NoteEditor: () => NoteEditor,
39
41
  NoteTool: () => NoteTool,
40
42
  PencilTool: () => PencilTool,
41
43
  RemoveElementCommand: () => RemoveElementCommand,
44
+ RemoveLayerCommand: () => RemoveLayerCommand,
42
45
  SelectTool: () => SelectTool,
43
46
  ShapeTool: () => ShapeTool,
44
47
  TextTool: () => TextTool,
45
48
  ToolManager: () => ToolManager,
46
49
  UpdateElementCommand: () => UpdateElementCommand,
50
+ UpdateLayerCommand: () => UpdateLayerCommand,
47
51
  VERSION: () => VERSION,
48
52
  Viewport: () => Viewport,
49
53
  clearStaleBindings: () => clearStaleBindings,
@@ -69,6 +73,7 @@ __export(index_exports, {
69
73
  isBindable: () => isBindable,
70
74
  isNearBezier: () => isNearBezier,
71
75
  parseState: () => parseState,
76
+ snapPoint: () => snapPoint,
72
77
  unbindArrow: () => unbindArrow,
73
78
  updateBoundArrow: () => updateBoundArrow
74
79
  });
@@ -99,15 +104,16 @@ var EventBus = class {
99
104
  };
100
105
 
101
106
  // src/core/state-serializer.ts
102
- var CURRENT_VERSION = 1;
103
- function exportState(elements, camera) {
107
+ var CURRENT_VERSION = 2;
108
+ function exportState(elements, camera, layers = []) {
104
109
  return {
105
110
  version: CURRENT_VERSION,
106
111
  camera: {
107
112
  position: { ...camera.position },
108
113
  zoom: camera.zoom
109
114
  },
110
- elements: elements.map((el) => structuredClone(el))
115
+ elements: elements.map((el) => structuredClone(el)),
116
+ layers: layers.map((l) => ({ ...l }))
111
117
  };
112
118
  }
113
119
  function parseState(json) {
@@ -145,6 +151,19 @@ function validateState(data) {
145
151
  migrateElement(el);
146
152
  }
147
153
  cleanBindings(obj["elements"]);
154
+ const layers = obj["layers"];
155
+ if (!Array.isArray(layers) || layers.length === 0) {
156
+ obj["layers"] = [
157
+ {
158
+ id: "default-layer",
159
+ name: "Layer 1",
160
+ visible: true,
161
+ locked: false,
162
+ order: 0,
163
+ opacity: 1
164
+ }
165
+ ];
166
+ }
148
167
  }
149
168
  var VALID_TYPES = /* @__PURE__ */ new Set(["stroke", "note", "arrow", "image", "html", "text", "shape"]);
150
169
  function validateElement(el) {
@@ -177,6 +196,9 @@ function cleanBindings(elements) {
177
196
  }
178
197
  }
179
198
  function migrateElement(obj) {
199
+ if (typeof obj["layerId"] !== "string") {
200
+ obj["layerId"] = "default-layer";
201
+ }
180
202
  if (obj["type"] === "arrow" && typeof obj["bend"] !== "number") {
181
203
  obj["bend"] = 0;
182
204
  }
@@ -195,6 +217,14 @@ function migrateElement(obj) {
195
217
  }
196
218
  }
197
219
 
220
+ // src/core/snap.ts
221
+ function snapPoint(point, gridSize) {
222
+ return {
223
+ x: Math.round(point.x / gridSize) * gridSize || 0,
224
+ y: Math.round(point.y / gridSize) * gridSize || 0
225
+ };
226
+ }
227
+
198
228
  // src/core/auto-save.ts
199
229
  var DEFAULT_KEY = "fieldnotes-autosave";
200
230
  var DEFAULT_DEBOUNCE_MS = 1e3;
@@ -204,9 +234,11 @@ var AutoSave = class {
204
234
  this.camera = camera;
205
235
  this.key = options.key ?? DEFAULT_KEY;
206
236
  this.debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS;
237
+ this.layerManager = options.layerManager;
207
238
  }
208
239
  key;
209
240
  debounceMs;
241
+ layerManager;
210
242
  timerId = null;
211
243
  unsubscribers = [];
212
244
  start() {
@@ -217,6 +249,9 @@ var AutoSave = class {
217
249
  this.store.on("update", schedule),
218
250
  this.camera.onChange(schedule)
219
251
  ];
252
+ if (this.layerManager) {
253
+ this.unsubscribers.push(this.layerManager.on("change", schedule));
254
+ }
220
255
  }
221
256
  stop() {
222
257
  this.cancelPending();
@@ -249,7 +284,8 @@ var AutoSave = class {
249
284
  }
250
285
  save() {
251
286
  if (typeof localStorage === "undefined") return;
252
- const state = exportState(this.store.snapshot(), this.camera);
287
+ const layers = this.layerManager?.snapshot() ?? [];
288
+ const state = exportState(this.store.snapshot(), this.camera, layers);
253
289
  localStorage.setItem(this.key, JSON.stringify(state));
254
290
  }
255
291
  };
@@ -626,11 +662,20 @@ var InputHandler = class {
626
662
  var ElementStore = class {
627
663
  elements = /* @__PURE__ */ new Map();
628
664
  bus = new EventBus();
665
+ layerOrderMap = /* @__PURE__ */ new Map();
629
666
  get count() {
630
667
  return this.elements.size;
631
668
  }
669
+ setLayerOrder(order) {
670
+ this.layerOrderMap = new Map(order);
671
+ }
632
672
  getAll() {
633
- return [...this.elements.values()].sort((a, b) => a.zIndex - b.zIndex);
673
+ return [...this.elements.values()].sort((a, b) => {
674
+ const layerA = this.layerOrderMap.get(a.layerId) ?? 0;
675
+ const layerB = this.layerOrderMap.get(b.layerId) ?? 0;
676
+ if (layerA !== layerB) return layerA - layerB;
677
+ return a.zIndex - b.zIndex;
678
+ });
634
679
  }
635
680
  getById(id) {
636
681
  return this.elements.get(id);
@@ -814,12 +859,13 @@ function getEdgeIntersection(bounds, outsidePoint) {
814
859
  y: cy + dy * scale
815
860
  };
816
861
  }
817
- function findBindTarget(point, store, threshold, excludeId) {
862
+ function findBindTarget(point, store, threshold, excludeId, filter) {
818
863
  let closest = null;
819
864
  let closestDist = Infinity;
820
865
  for (const el of store.getAll()) {
821
866
  if (!isBindable(el)) continue;
822
867
  if (excludeId && el.id === excludeId) continue;
868
+ if (filter && !filter(el)) continue;
823
869
  const bounds = getElementBounds(el);
824
870
  if (!bounds) continue;
825
871
  const dist = distToBounds(point, bounds);
@@ -977,14 +1023,19 @@ function smoothToSegments(points) {
977
1023
  }
978
1024
 
979
1025
  // src/elements/element-renderer.ts
980
- var DOM_ELEMENT_TYPES = /* @__PURE__ */ new Set(["note", "image", "html", "text"]);
1026
+ var DOM_ELEMENT_TYPES = /* @__PURE__ */ new Set(["note", "html", "text"]);
981
1027
  var ARROWHEAD_LENGTH = 12;
982
1028
  var ARROWHEAD_ANGLE = Math.PI / 6;
983
1029
  var ElementRenderer = class {
984
1030
  store = null;
1031
+ imageCache = /* @__PURE__ */ new Map();
1032
+ onImageLoad = null;
985
1033
  setStore(store) {
986
1034
  this.store = store;
987
1035
  }
1036
+ setOnImageLoad(callback) {
1037
+ this.onImageLoad = callback;
1038
+ }
988
1039
  isDomElement(element) {
989
1040
  return DOM_ELEMENT_TYPES.has(element.type);
990
1041
  }
@@ -999,6 +1050,9 @@ var ElementRenderer = class {
999
1050
  case "shape":
1000
1051
  this.renderShape(ctx, element);
1001
1052
  break;
1053
+ case "image":
1054
+ this.renderImage(ctx, element);
1055
+ break;
1002
1056
  }
1003
1057
  }
1004
1058
  renderStroke(ctx, stroke) {
@@ -1134,6 +1188,20 @@ var ElementRenderer = class {
1134
1188
  }
1135
1189
  }
1136
1190
  }
1191
+ renderImage(ctx, image) {
1192
+ const img = this.getImage(image.src);
1193
+ if (!img) return;
1194
+ ctx.drawImage(img, image.position.x, image.position.y, image.size.w, image.size.h);
1195
+ }
1196
+ getImage(src) {
1197
+ const cached = this.imageCache.get(src);
1198
+ if (cached) return cached.complete ? cached : null;
1199
+ const img = new Image();
1200
+ img.src = src;
1201
+ this.imageCache.set(src, img);
1202
+ img.onload = () => this.onImageLoad?.();
1203
+ return null;
1204
+ }
1137
1205
  };
1138
1206
 
1139
1207
  // src/elements/note-editor.ts
@@ -1485,6 +1553,7 @@ function createStroke(input) {
1485
1553
  position: input.position ?? { x: 0, y: 0 },
1486
1554
  zIndex: input.zIndex ?? 0,
1487
1555
  locked: input.locked ?? false,
1556
+ layerId: input.layerId ?? "",
1488
1557
  points: input.points,
1489
1558
  color: input.color ?? "#000000",
1490
1559
  width: input.width ?? 2,
@@ -1498,6 +1567,7 @@ function createNote(input) {
1498
1567
  position: input.position,
1499
1568
  zIndex: input.zIndex ?? 0,
1500
1569
  locked: input.locked ?? false,
1570
+ layerId: input.layerId ?? "",
1501
1571
  size: input.size ?? { w: 200, h: 100 },
1502
1572
  text: input.text ?? "",
1503
1573
  backgroundColor: input.backgroundColor ?? "#ffeb3b",
@@ -1511,6 +1581,7 @@ function createArrow(input) {
1511
1581
  position: input.position ?? { x: 0, y: 0 },
1512
1582
  zIndex: input.zIndex ?? 0,
1513
1583
  locked: input.locked ?? false,
1584
+ layerId: input.layerId ?? "",
1514
1585
  from: input.from,
1515
1586
  to: input.to,
1516
1587
  bend: input.bend ?? 0,
@@ -1528,6 +1599,7 @@ function createImage(input) {
1528
1599
  position: input.position,
1529
1600
  zIndex: input.zIndex ?? 0,
1530
1601
  locked: input.locked ?? false,
1602
+ layerId: input.layerId ?? "",
1531
1603
  size: input.size,
1532
1604
  src: input.src
1533
1605
  };
@@ -1539,6 +1611,7 @@ function createHtmlElement(input) {
1539
1611
  position: input.position,
1540
1612
  zIndex: input.zIndex ?? 0,
1541
1613
  locked: input.locked ?? false,
1614
+ layerId: input.layerId ?? "",
1542
1615
  size: input.size
1543
1616
  };
1544
1617
  }
@@ -1549,6 +1622,7 @@ function createShape(input) {
1549
1622
  position: input.position,
1550
1623
  zIndex: input.zIndex ?? 0,
1551
1624
  locked: input.locked ?? false,
1625
+ layerId: input.layerId ?? "",
1552
1626
  shape: input.shape ?? "rectangle",
1553
1627
  size: input.size,
1554
1628
  strokeColor: input.strokeColor ?? "#000000",
@@ -1563,6 +1637,7 @@ function createText(input) {
1563
1637
  position: input.position,
1564
1638
  zIndex: input.zIndex ?? 0,
1565
1639
  locked: input.locked ?? false,
1640
+ layerId: input.layerId ?? "",
1566
1641
  size: input.size ?? { w: 200, h: 28 },
1567
1642
  text: input.text ?? "",
1568
1643
  fontSize: input.fontSize ?? 16,
@@ -1571,16 +1646,168 @@ function createText(input) {
1571
1646
  };
1572
1647
  }
1573
1648
 
1649
+ // src/layers/layer-manager.ts
1650
+ var LayerManager = class {
1651
+ constructor(store) {
1652
+ this.store = store;
1653
+ const defaultLayer = {
1654
+ id: createId("layer"),
1655
+ name: "Layer 1",
1656
+ visible: true,
1657
+ locked: false,
1658
+ order: 0,
1659
+ opacity: 1
1660
+ };
1661
+ this.layers.set(defaultLayer.id, defaultLayer);
1662
+ this._activeLayerId = defaultLayer.id;
1663
+ this.syncLayerOrder();
1664
+ }
1665
+ layers = /* @__PURE__ */ new Map();
1666
+ _activeLayerId;
1667
+ bus = new EventBus();
1668
+ get activeLayer() {
1669
+ const layer = this.layers.get(this._activeLayerId);
1670
+ if (!layer) throw new Error("Active layer not found");
1671
+ return { ...layer };
1672
+ }
1673
+ get activeLayerId() {
1674
+ return this._activeLayerId;
1675
+ }
1676
+ getLayers() {
1677
+ return [...this.layers.values()].sort((a, b) => a.order - b.order).map((l) => ({ ...l }));
1678
+ }
1679
+ getLayer(id) {
1680
+ const layer = this.layers.get(id);
1681
+ return layer ? { ...layer } : void 0;
1682
+ }
1683
+ isLayerVisible(id) {
1684
+ return this.layers.get(id)?.visible ?? true;
1685
+ }
1686
+ isLayerLocked(id) {
1687
+ return this.layers.get(id)?.locked ?? false;
1688
+ }
1689
+ createLayer(name) {
1690
+ const maxOrder = Math.max(...[...this.layers.values()].map((l) => l.order), -1);
1691
+ const autoName = name ?? `Layer ${this.layers.size + 1}`;
1692
+ const layer = {
1693
+ id: createId("layer"),
1694
+ name: autoName,
1695
+ visible: true,
1696
+ locked: false,
1697
+ order: maxOrder + 1,
1698
+ opacity: 1
1699
+ };
1700
+ this.addLayerDirect(layer);
1701
+ return { ...layer };
1702
+ }
1703
+ removeLayer(id) {
1704
+ if (this.layers.size <= 1) {
1705
+ throw new Error("Cannot remove the last layer");
1706
+ }
1707
+ if (this._activeLayerId === id) {
1708
+ const remaining = [...this.layers.values()].filter((l) => l.id !== id).sort((a, b) => b.order - a.order);
1709
+ const fallback = remaining[0];
1710
+ if (fallback) this._activeLayerId = fallback.id;
1711
+ }
1712
+ const elements = this.store.getAll().filter((el) => el.layerId === id);
1713
+ for (const el of elements) {
1714
+ this.store.update(el.id, { layerId: this._activeLayerId });
1715
+ }
1716
+ this.removeLayerDirect(id);
1717
+ }
1718
+ renameLayer(id, name) {
1719
+ this.updateLayerDirect(id, { name });
1720
+ }
1721
+ reorderLayer(id, newOrder) {
1722
+ if (!this.layers.has(id)) return;
1723
+ this.updateLayerDirect(id, { order: newOrder });
1724
+ }
1725
+ setLayerVisible(id, visible) {
1726
+ if (!visible && id === this._activeLayerId) {
1727
+ const fallback = this.findFallbackLayer(id);
1728
+ if (!fallback) return false;
1729
+ this._activeLayerId = fallback.id;
1730
+ }
1731
+ this.updateLayerDirect(id, { visible });
1732
+ return true;
1733
+ }
1734
+ setLayerLocked(id, locked) {
1735
+ if (locked && id === this._activeLayerId) {
1736
+ const fallback = this.findFallbackLayer(id);
1737
+ if (!fallback) return false;
1738
+ this._activeLayerId = fallback.id;
1739
+ }
1740
+ this.updateLayerDirect(id, { locked });
1741
+ return true;
1742
+ }
1743
+ setActiveLayer(id) {
1744
+ if (!this.layers.has(id)) return;
1745
+ this._activeLayerId = id;
1746
+ this.bus.emit("change", null);
1747
+ }
1748
+ moveElementToLayer(elementId, layerId) {
1749
+ if (!this.layers.has(layerId)) return;
1750
+ this.store.update(elementId, { layerId });
1751
+ this.bus.emit("change", null);
1752
+ }
1753
+ snapshot() {
1754
+ return this.getLayers();
1755
+ }
1756
+ loadSnapshot(layers) {
1757
+ this.layers.clear();
1758
+ for (const layer of layers) {
1759
+ this.layers.set(layer.id, { ...layer });
1760
+ }
1761
+ const first = this.getLayers()[0];
1762
+ if (first) this._activeLayerId = first.id;
1763
+ this.syncLayerOrder();
1764
+ this.bus.emit("change", null);
1765
+ }
1766
+ on(event, callback) {
1767
+ return this.bus.on(event, callback);
1768
+ }
1769
+ addLayerDirect(layer) {
1770
+ this.layers.set(layer.id, { ...layer });
1771
+ this.syncLayerOrder();
1772
+ this.bus.emit("change", null);
1773
+ }
1774
+ removeLayerDirect(id) {
1775
+ this.layers.delete(id);
1776
+ this.syncLayerOrder();
1777
+ this.bus.emit("change", null);
1778
+ }
1779
+ updateLayerDirect(id, props) {
1780
+ const layer = this.layers.get(id);
1781
+ if (!layer) return;
1782
+ Object.assign(layer, props);
1783
+ if ("order" in props) this.syncLayerOrder();
1784
+ this.bus.emit("change", null);
1785
+ }
1786
+ syncLayerOrder() {
1787
+ const order = /* @__PURE__ */ new Map();
1788
+ for (const layer of this.layers.values()) {
1789
+ order.set(layer.id, layer.order);
1790
+ }
1791
+ this.store.setLayerOrder(order);
1792
+ }
1793
+ findFallbackLayer(excludeId) {
1794
+ return [...this.layers.values()].filter((l) => l.id !== excludeId && l.visible && !l.locked).sort((a, b) => b.order - a.order)[0];
1795
+ }
1796
+ };
1797
+
1574
1798
  // src/canvas/viewport.ts
1575
1799
  var Viewport = class {
1576
1800
  constructor(container, options = {}) {
1577
1801
  this.container = container;
1578
1802
  this.camera = new Camera(options.camera);
1579
1803
  this.background = new Background(options.background);
1804
+ this._gridSize = options.background?.spacing ?? 24;
1580
1805
  this.store = new ElementStore();
1806
+ this.layerManager = new LayerManager(this.store);
1581
1807
  this.toolManager = new ToolManager();
1582
1808
  this.renderer = new ElementRenderer();
1583
1809
  this.renderer.setStore(this.store);
1810
+ this.renderer.setOnImageLoad(() => this.requestRender());
1584
1811
  this.noteEditor = new NoteEditor();
1585
1812
  this.noteEditor.setOnStop((id) => this.onTextEditStop(id));
1586
1813
  this.history = new HistoryStack();
@@ -1599,7 +1826,12 @@ var Viewport = class {
1599
1826
  editElement: (id) => this.startEditingElement(id),
1600
1827
  setCursor: (cursor) => {
1601
1828
  this.wrapper.style.cursor = cursor;
1602
- }
1829
+ },
1830
+ snapToGrid: false,
1831
+ gridSize: this._gridSize,
1832
+ activeLayerId: this.layerManager.activeLayerId,
1833
+ isLayerVisible: (id) => this.layerManager.isLayerVisible(id),
1834
+ isLayerLocked: (id) => this.layerManager.isLayerLocked(id)
1603
1835
  };
1604
1836
  this.inputHandler = new InputHandler(this.wrapper, this.camera, {
1605
1837
  toolManager: this.toolManager,
@@ -1620,6 +1852,10 @@ var Viewport = class {
1620
1852
  this.store.on("update", () => this.requestRender()),
1621
1853
  this.store.on("clear", () => this.clearDomNodes())
1622
1854
  ];
1855
+ this.layerManager.on("change", () => {
1856
+ this.toolContext.activeLayerId = this.layerManager.activeLayerId;
1857
+ this.requestRender();
1858
+ });
1623
1859
  this.wrapper.addEventListener("dblclick", this.onDblClick);
1624
1860
  this.wrapper.addEventListener("dragover", this.onDragOver);
1625
1861
  this.wrapper.addEventListener("drop", this.onDrop);
@@ -1629,6 +1865,7 @@ var Viewport = class {
1629
1865
  }
1630
1866
  camera;
1631
1867
  store;
1868
+ layerManager;
1632
1869
  toolManager;
1633
1870
  history;
1634
1871
  domLayer;
@@ -1644,6 +1881,8 @@ var Viewport = class {
1644
1881
  toolContext;
1645
1882
  resizeObserver = null;
1646
1883
  animFrameId = 0;
1884
+ _snapToGrid = false;
1885
+ _gridSize;
1647
1886
  needsRender = true;
1648
1887
  domNodes = /* @__PURE__ */ new Map();
1649
1888
  htmlContent = /* @__PURE__ */ new Map();
@@ -1651,11 +1890,18 @@ var Viewport = class {
1651
1890
  get ctx() {
1652
1891
  return this.canvasEl.getContext("2d");
1653
1892
  }
1893
+ get snapToGrid() {
1894
+ return this._snapToGrid;
1895
+ }
1896
+ setSnapToGrid(enabled) {
1897
+ this._snapToGrid = enabled;
1898
+ this.toolContext.snapToGrid = enabled;
1899
+ }
1654
1900
  requestRender() {
1655
1901
  this.needsRender = true;
1656
1902
  }
1657
1903
  exportState() {
1658
- return exportState(this.store.snapshot(), this.camera);
1904
+ return exportState(this.store.snapshot(), this.camera, this.layerManager.snapshot());
1659
1905
  }
1660
1906
  exportJSON() {
1661
1907
  return JSON.stringify(this.exportState());
@@ -1665,6 +1911,9 @@ var Viewport = class {
1665
1911
  this.noteEditor.destroy(this.store);
1666
1912
  this.clearDomNodes();
1667
1913
  this.store.loadSnapshot(state.elements);
1914
+ if (state.layers && state.layers.length > 0) {
1915
+ this.layerManager.loadSnapshot(state.layers);
1916
+ }
1668
1917
  this.history.clear();
1669
1918
  this.historyRecorder.resume();
1670
1919
  this.camera.moveTo(state.camera.position.x, state.camera.position.y);
@@ -1688,7 +1937,7 @@ var Viewport = class {
1688
1937
  return result;
1689
1938
  }
1690
1939
  addImage(src, position, size = { w: 300, h: 200 }) {
1691
- const image = createImage({ position, size, src });
1940
+ const image = createImage({ position, size, src, layerId: this.layerManager.activeLayerId });
1692
1941
  this.historyRecorder.begin();
1693
1942
  this.store.add(image);
1694
1943
  this.historyRecorder.commit();
@@ -1696,7 +1945,7 @@ var Viewport = class {
1696
1945
  return image.id;
1697
1946
  }
1698
1947
  addHtmlElement(dom, position, size = { w: 200, h: 150 }) {
1699
- const el = createHtmlElement({ position, size });
1948
+ const el = createHtmlElement({ position, size, layerId: this.layerManager.activeLayerId });
1700
1949
  this.htmlContent.set(el.id, dom);
1701
1950
  this.historyRecorder.begin();
1702
1951
  this.store.add(el);
@@ -1739,9 +1988,17 @@ var Viewport = class {
1739
1988
  ctx.save();
1740
1989
  ctx.translate(this.camera.position.x, this.camera.position.y);
1741
1990
  ctx.scale(this.camera.zoom, this.camera.zoom);
1742
- for (const element of this.store.getAll()) {
1991
+ const allElements = this.store.getAll();
1992
+ let domZIndex = 0;
1993
+ for (const element of allElements) {
1994
+ if (!this.layerManager.isLayerVisible(element.layerId)) {
1995
+ if (this.renderer.isDomElement(element)) {
1996
+ this.hideDomNode(element.id);
1997
+ }
1998
+ continue;
1999
+ }
1743
2000
  if (this.renderer.isDomElement(element)) {
1744
- this.syncDomNode(element);
2001
+ this.syncDomNode(element, domZIndex++);
1745
2002
  } else {
1746
2003
  this.renderer.renderCanvasElement(ctx, element);
1747
2004
  }
@@ -1873,7 +2130,7 @@ var Viewport = class {
1873
2130
  reader.readAsDataURL(file);
1874
2131
  }
1875
2132
  };
1876
- syncDomNode(element) {
2133
+ syncDomNode(element, zIndex = 0) {
1877
2134
  let node = this.domNodes.get(element.id);
1878
2135
  if (!node) {
1879
2136
  node = document.createElement("div");
@@ -1887,10 +2144,12 @@ var Viewport = class {
1887
2144
  }
1888
2145
  const size = "size" in element ? element.size : null;
1889
2146
  Object.assign(node.style, {
2147
+ display: "block",
1890
2148
  left: `${element.position.x}px`,
1891
2149
  top: `${element.position.y}px`,
1892
2150
  width: size ? `${size.w}px` : "auto",
1893
- height: size ? `${size.h}px` : "auto"
2151
+ height: size ? `${size.h}px` : "auto",
2152
+ zIndex: String(zIndex)
1894
2153
  });
1895
2154
  this.renderDomContent(node, element);
1896
2155
  }
@@ -1925,26 +2184,6 @@ var Viewport = class {
1925
2184
  node.style.color = element.textColor;
1926
2185
  }
1927
2186
  }
1928
- if (element.type === "image") {
1929
- if (!node.dataset["initialized"]) {
1930
- node.dataset["initialized"] = "true";
1931
- const img = document.createElement("img");
1932
- img.src = element.src;
1933
- Object.assign(img.style, {
1934
- width: "100%",
1935
- height: "100%",
1936
- objectFit: "contain",
1937
- pointerEvents: "none"
1938
- });
1939
- img.draggable = false;
1940
- node.appendChild(img);
1941
- } else {
1942
- const img = node.querySelector("img");
1943
- if (img && img.src !== element.src) {
1944
- img.src = element.src;
1945
- }
1946
- }
1947
- }
1948
2187
  if (element.type === "html" && !node.dataset["initialized"]) {
1949
2188
  const content = this.htmlContent.get(element.id);
1950
2189
  if (content) {
@@ -2027,6 +2266,10 @@ var Viewport = class {
2027
2266
  }
2028
2267
  }
2029
2268
  }
2269
+ hideDomNode(id) {
2270
+ const node = this.domNodes.get(id);
2271
+ if (node) node.style.display = "none";
2272
+ }
2030
2273
  removeDomNode(id) {
2031
2274
  this.htmlContent.delete(id);
2032
2275
  const node = this.domNodes.get(id);
@@ -2173,7 +2416,8 @@ var PencilTool = class {
2173
2416
  const stroke = createStroke({
2174
2417
  points: simplified,
2175
2418
  color: this.color,
2176
- width: this.width
2419
+ width: this.width,
2420
+ layerId: ctx.activeLayerId ?? ""
2177
2421
  });
2178
2422
  ctx.store.add(stroke);
2179
2423
  this.points = [];
@@ -2237,6 +2481,8 @@ var EraserTool = class {
2237
2481
  const strokes = ctx.store.getElementsByType("stroke");
2238
2482
  let erased = false;
2239
2483
  for (const stroke of strokes) {
2484
+ if (ctx.isLayerVisible && !ctx.isLayerVisible(stroke.layerId)) continue;
2485
+ if (ctx.isLayerLocked && ctx.isLayerLocked(stroke.layerId)) continue;
2240
2486
  if (this.strokeIntersects(stroke, world)) {
2241
2487
  ctx.store.remove(stroke.id);
2242
2488
  erased = true;
@@ -2399,10 +2645,13 @@ var SelectTool = class {
2399
2645
  this.mode = { type: "idle" };
2400
2646
  ctx.setCursor?.("default");
2401
2647
  }
2648
+ snap(point, ctx) {
2649
+ return ctx.snapToGrid && ctx.gridSize ? snapPoint(point, ctx.gridSize) : point;
2650
+ }
2402
2651
  onPointerDown(state, ctx) {
2403
2652
  this.ctx = ctx;
2404
2653
  const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
2405
- this.lastWorld = world;
2654
+ this.lastWorld = this.snap(world, ctx);
2406
2655
  this.currentWorld = world;
2407
2656
  const arrowHit = hitTestArrowHandles(world, this._selectedIds, ctx);
2408
2657
  if (arrowHit) {
@@ -2455,9 +2704,10 @@ var SelectTool = class {
2455
2704
  }
2456
2705
  if (this.mode.type === "dragging" && this._selectedIds.length > 0) {
2457
2706
  ctx.setCursor?.("move");
2458
- const dx = world.x - this.lastWorld.x;
2459
- const dy = world.y - this.lastWorld.y;
2460
- this.lastWorld = world;
2707
+ const snapped = this.snap(world, ctx);
2708
+ const dx = snapped.x - this.lastWorld.x;
2709
+ const dy = snapped.y - this.lastWorld.y;
2710
+ this.lastWorld = snapped;
2461
2711
  for (const id of this._selectedIds) {
2462
2712
  const el = ctx.store.getById(id);
2463
2713
  if (!el || el.locked) continue;
@@ -2718,6 +2968,8 @@ var SelectTool = class {
2718
2968
  findElementsInRect(marquee, ctx) {
2719
2969
  const ids = [];
2720
2970
  for (const el of ctx.store.getAll()) {
2971
+ if (ctx.isLayerVisible && !ctx.isLayerVisible(el.layerId)) continue;
2972
+ if (ctx.isLayerLocked && ctx.isLayerLocked(el.layerId)) continue;
2721
2973
  const bounds = this.getElementBounds(el);
2722
2974
  if (bounds && this.rectsOverlap(marquee, bounds)) {
2723
2975
  ids.push(el.id);
@@ -2752,6 +3004,8 @@ var SelectTool = class {
2752
3004
  hitTest(world, ctx) {
2753
3005
  const elements = ctx.store.getAll().reverse();
2754
3006
  for (const el of elements) {
3007
+ if (ctx.isLayerVisible && !ctx.isLayerVisible(el.layerId)) continue;
3008
+ if (ctx.isLayerLocked && ctx.isLayerLocked(el.layerId)) continue;
2755
3009
  if (this.isInsideBounds(world, el)) return el;
2756
3010
  }
2757
3011
  return null;
@@ -2796,17 +3050,26 @@ var ArrowTool = class {
2796
3050
  if (options.color !== void 0) this.color = options.color;
2797
3051
  if (options.width !== void 0) this.width = options.width;
2798
3052
  }
3053
+ layerFilter(ctx) {
3054
+ if (!ctx.isLayerVisible && !ctx.isLayerLocked) return void 0;
3055
+ return (el) => {
3056
+ if (ctx.isLayerVisible && !ctx.isLayerVisible(el.layerId)) return false;
3057
+ if (ctx.isLayerLocked && ctx.isLayerLocked(el.layerId)) return false;
3058
+ return true;
3059
+ };
3060
+ }
2799
3061
  onPointerDown(state, ctx) {
2800
3062
  this.drawing = true;
2801
3063
  const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
2802
3064
  const threshold = BIND_THRESHOLD2 / ctx.camera.zoom;
2803
- const target = findBindTarget(world, ctx.store, threshold);
3065
+ const filter = this.layerFilter(ctx);
3066
+ const target = findBindTarget(world, ctx.store, threshold, void 0, filter);
2804
3067
  if (target) {
2805
3068
  this.start = getElementCenter(target);
2806
3069
  this.fromBinding = { elementId: target.id };
2807
3070
  this.fromTarget = target;
2808
3071
  } else {
2809
- this.start = world;
3072
+ this.start = ctx.snapToGrid && ctx.gridSize ? snapPoint(world, ctx.gridSize) : world;
2810
3073
  this.fromBinding = void 0;
2811
3074
  this.fromTarget = null;
2812
3075
  }
@@ -2818,12 +3081,13 @@ var ArrowTool = class {
2818
3081
  const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
2819
3082
  const threshold = BIND_THRESHOLD2 / ctx.camera.zoom;
2820
3083
  const excludeId = this.fromBinding?.elementId;
2821
- const target = findBindTarget(world, ctx.store, threshold, excludeId);
3084
+ const filter = this.layerFilter(ctx);
3085
+ const target = findBindTarget(world, ctx.store, threshold, excludeId, filter);
2822
3086
  if (target) {
2823
3087
  this.end = getElementCenter(target);
2824
3088
  this.toTarget = target;
2825
3089
  } else {
2826
- this.end = world;
3090
+ this.end = ctx.snapToGrid && ctx.gridSize ? snapPoint(world, ctx.gridSize) : world;
2827
3091
  this.toTarget = null;
2828
3092
  }
2829
3093
  ctx.requestRender();
@@ -2839,7 +3103,8 @@ var ArrowTool = class {
2839
3103
  color: this.color,
2840
3104
  width: this.width,
2841
3105
  fromBinding: this.fromBinding,
2842
- toBinding: this.toTarget ? { elementId: this.toTarget.id } : void 0
3106
+ toBinding: this.toTarget ? { elementId: this.toTarget.id } : void 0,
3107
+ layerId: ctx.activeLayerId ?? ""
2843
3108
  });
2844
3109
  ctx.store.add(arrow);
2845
3110
  this.fromTarget = null;
@@ -2918,12 +3183,16 @@ var NoteTool = class {
2918
3183
  onPointerMove(_state, _ctx) {
2919
3184
  }
2920
3185
  onPointerUp(state, ctx) {
2921
- const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
3186
+ let world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
3187
+ if (ctx.snapToGrid && ctx.gridSize) {
3188
+ world = snapPoint(world, ctx.gridSize);
3189
+ }
2922
3190
  const note = createNote({
2923
3191
  position: world,
2924
3192
  size: { ...this.size },
2925
3193
  backgroundColor: this.backgroundColor,
2926
- textColor: this.textColor
3194
+ textColor: this.textColor,
3195
+ layerId: ctx.activeLayerId ?? ""
2927
3196
  });
2928
3197
  ctx.store.add(note);
2929
3198
  ctx.requestRender();
@@ -2959,12 +3228,16 @@ var TextTool = class {
2959
3228
  onPointerMove(_state, _ctx) {
2960
3229
  }
2961
3230
  onPointerUp(state, ctx) {
2962
- const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
3231
+ let world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
3232
+ if (ctx.snapToGrid && ctx.gridSize) {
3233
+ world = snapPoint(world, ctx.gridSize);
3234
+ }
2963
3235
  const textEl = createText({
2964
3236
  position: world,
2965
3237
  fontSize: this.fontSize,
2966
3238
  color: this.color,
2967
- textAlign: this.textAlign
3239
+ textAlign: this.textAlign,
3240
+ layerId: ctx.activeLayerId ?? ""
2968
3241
  });
2969
3242
  ctx.store.add(textEl);
2970
3243
  ctx.requestRender();
@@ -3041,12 +3314,12 @@ var ShapeTool = class {
3041
3314
  }
3042
3315
  onPointerDown(state, ctx) {
3043
3316
  this.drawing = true;
3044
- this.start = ctx.camera.screenToWorld({ x: state.x, y: state.y });
3317
+ this.start = this.snap(ctx.camera.screenToWorld({ x: state.x, y: state.y }), ctx);
3045
3318
  this.end = { ...this.start };
3046
3319
  }
3047
3320
  onPointerMove(state, ctx) {
3048
3321
  if (!this.drawing) return;
3049
- this.end = ctx.camera.screenToWorld({ x: state.x, y: state.y });
3322
+ this.end = this.snap(ctx.camera.screenToWorld({ x: state.x, y: state.y }), ctx);
3050
3323
  ctx.requestRender();
3051
3324
  }
3052
3325
  onPointerUp(_state, ctx) {
@@ -3060,7 +3333,8 @@ var ShapeTool = class {
3060
3333
  shape: this.shape,
3061
3334
  strokeColor: this.strokeColor,
3062
3335
  strokeWidth: this.strokeWidth,
3063
- fillColor: this.fillColor
3336
+ fillColor: this.fillColor,
3337
+ layerId: ctx.activeLayerId ?? ""
3064
3338
  });
3065
3339
  ctx.store.add(shape);
3066
3340
  ctx.requestRender();
@@ -3110,6 +3384,9 @@ var ShapeTool = class {
3110
3384
  }
3111
3385
  return { position: { x, y }, size: { w, h } };
3112
3386
  }
3387
+ snap(point, ctx) {
3388
+ return ctx.snapToGrid && ctx.gridSize ? snapPoint(point, ctx.gridSize) : point;
3389
+ }
3113
3390
  onKeyDown = (e) => {
3114
3391
  if (e.key === "Shift") this.shiftHeld = true;
3115
3392
  };
@@ -3118,8 +3395,48 @@ var ShapeTool = class {
3118
3395
  };
3119
3396
  };
3120
3397
 
3398
+ // src/history/layer-commands.ts
3399
+ var CreateLayerCommand = class {
3400
+ constructor(manager, layer) {
3401
+ this.manager = manager;
3402
+ this.layer = layer;
3403
+ }
3404
+ execute(_store) {
3405
+ this.manager.addLayerDirect(this.layer);
3406
+ }
3407
+ undo(_store) {
3408
+ this.manager.removeLayerDirect(this.layer.id);
3409
+ }
3410
+ };
3411
+ var RemoveLayerCommand = class {
3412
+ constructor(manager, layer) {
3413
+ this.manager = manager;
3414
+ this.layer = layer;
3415
+ }
3416
+ execute(_store) {
3417
+ this.manager.removeLayerDirect(this.layer.id);
3418
+ }
3419
+ undo(_store) {
3420
+ this.manager.addLayerDirect(this.layer);
3421
+ }
3422
+ };
3423
+ var UpdateLayerCommand = class {
3424
+ constructor(manager, layerId, previous, current) {
3425
+ this.manager = manager;
3426
+ this.layerId = layerId;
3427
+ this.previous = previous;
3428
+ this.current = current;
3429
+ }
3430
+ execute(_store) {
3431
+ this.manager.updateLayerDirect(this.layerId, { ...this.current });
3432
+ }
3433
+ undo(_store) {
3434
+ this.manager.updateLayerDirect(this.layerId, { ...this.previous });
3435
+ }
3436
+ };
3437
+
3121
3438
  // src/index.ts
3122
- var VERSION = "0.4.1";
3439
+ var VERSION = "0.6.0";
3123
3440
  // Annotate the CommonJS export names for ESM import in node:
3124
3441
  0 && (module.exports = {
3125
3442
  AddElementCommand,
@@ -3128,6 +3445,7 @@ var VERSION = "0.4.1";
3128
3445
  Background,
3129
3446
  BatchCommand,
3130
3447
  Camera,
3448
+ CreateLayerCommand,
3131
3449
  ElementRenderer,
3132
3450
  ElementStore,
3133
3451
  EraserTool,
@@ -3137,15 +3455,18 @@ var VERSION = "0.4.1";
3137
3455
  HistoryStack,
3138
3456
  ImageTool,
3139
3457
  InputHandler,
3458
+ LayerManager,
3140
3459
  NoteEditor,
3141
3460
  NoteTool,
3142
3461
  PencilTool,
3143
3462
  RemoveElementCommand,
3463
+ RemoveLayerCommand,
3144
3464
  SelectTool,
3145
3465
  ShapeTool,
3146
3466
  TextTool,
3147
3467
  ToolManager,
3148
3468
  UpdateElementCommand,
3469
+ UpdateLayerCommand,
3149
3470
  VERSION,
3150
3471
  Viewport,
3151
3472
  clearStaleBindings,
@@ -3171,6 +3492,7 @@ var VERSION = "0.4.1";
3171
3492
  isBindable,
3172
3493
  isNearBezier,
3173
3494
  parseState,
3495
+ snapPoint,
3174
3496
  unbindArrow,
3175
3497
  updateBoundArrow
3176
3498
  });