@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.js CHANGED
@@ -23,15 +23,16 @@ var EventBus = class {
23
23
  };
24
24
 
25
25
  // src/core/state-serializer.ts
26
- var CURRENT_VERSION = 1;
27
- function exportState(elements, camera) {
26
+ var CURRENT_VERSION = 2;
27
+ function exportState(elements, camera, layers = []) {
28
28
  return {
29
29
  version: CURRENT_VERSION,
30
30
  camera: {
31
31
  position: { ...camera.position },
32
32
  zoom: camera.zoom
33
33
  },
34
- elements: elements.map((el) => structuredClone(el))
34
+ elements: elements.map((el) => structuredClone(el)),
35
+ layers: layers.map((l) => ({ ...l }))
35
36
  };
36
37
  }
37
38
  function parseState(json) {
@@ -69,6 +70,19 @@ function validateState(data) {
69
70
  migrateElement(el);
70
71
  }
71
72
  cleanBindings(obj["elements"]);
73
+ const layers = obj["layers"];
74
+ if (!Array.isArray(layers) || layers.length === 0) {
75
+ obj["layers"] = [
76
+ {
77
+ id: "default-layer",
78
+ name: "Layer 1",
79
+ visible: true,
80
+ locked: false,
81
+ order: 0,
82
+ opacity: 1
83
+ }
84
+ ];
85
+ }
72
86
  }
73
87
  var VALID_TYPES = /* @__PURE__ */ new Set(["stroke", "note", "arrow", "image", "html", "text", "shape"]);
74
88
  function validateElement(el) {
@@ -101,6 +115,9 @@ function cleanBindings(elements) {
101
115
  }
102
116
  }
103
117
  function migrateElement(obj) {
118
+ if (typeof obj["layerId"] !== "string") {
119
+ obj["layerId"] = "default-layer";
120
+ }
104
121
  if (obj["type"] === "arrow" && typeof obj["bend"] !== "number") {
105
122
  obj["bend"] = 0;
106
123
  }
@@ -119,6 +136,14 @@ function migrateElement(obj) {
119
136
  }
120
137
  }
121
138
 
139
+ // src/core/snap.ts
140
+ function snapPoint(point, gridSize) {
141
+ return {
142
+ x: Math.round(point.x / gridSize) * gridSize || 0,
143
+ y: Math.round(point.y / gridSize) * gridSize || 0
144
+ };
145
+ }
146
+
122
147
  // src/core/auto-save.ts
123
148
  var DEFAULT_KEY = "fieldnotes-autosave";
124
149
  var DEFAULT_DEBOUNCE_MS = 1e3;
@@ -128,9 +153,11 @@ var AutoSave = class {
128
153
  this.camera = camera;
129
154
  this.key = options.key ?? DEFAULT_KEY;
130
155
  this.debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS;
156
+ this.layerManager = options.layerManager;
131
157
  }
132
158
  key;
133
159
  debounceMs;
160
+ layerManager;
134
161
  timerId = null;
135
162
  unsubscribers = [];
136
163
  start() {
@@ -141,6 +168,9 @@ var AutoSave = class {
141
168
  this.store.on("update", schedule),
142
169
  this.camera.onChange(schedule)
143
170
  ];
171
+ if (this.layerManager) {
172
+ this.unsubscribers.push(this.layerManager.on("change", schedule));
173
+ }
144
174
  }
145
175
  stop() {
146
176
  this.cancelPending();
@@ -173,7 +203,8 @@ var AutoSave = class {
173
203
  }
174
204
  save() {
175
205
  if (typeof localStorage === "undefined") return;
176
- const state = exportState(this.store.snapshot(), this.camera);
206
+ const layers = this.layerManager?.snapshot() ?? [];
207
+ const state = exportState(this.store.snapshot(), this.camera, layers);
177
208
  localStorage.setItem(this.key, JSON.stringify(state));
178
209
  }
179
210
  };
@@ -550,11 +581,20 @@ var InputHandler = class {
550
581
  var ElementStore = class {
551
582
  elements = /* @__PURE__ */ new Map();
552
583
  bus = new EventBus();
584
+ layerOrderMap = /* @__PURE__ */ new Map();
553
585
  get count() {
554
586
  return this.elements.size;
555
587
  }
588
+ setLayerOrder(order) {
589
+ this.layerOrderMap = new Map(order);
590
+ }
556
591
  getAll() {
557
- return [...this.elements.values()].sort((a, b) => a.zIndex - b.zIndex);
592
+ return [...this.elements.values()].sort((a, b) => {
593
+ const layerA = this.layerOrderMap.get(a.layerId) ?? 0;
594
+ const layerB = this.layerOrderMap.get(b.layerId) ?? 0;
595
+ if (layerA !== layerB) return layerA - layerB;
596
+ return a.zIndex - b.zIndex;
597
+ });
558
598
  }
559
599
  getById(id) {
560
600
  return this.elements.get(id);
@@ -738,12 +778,13 @@ function getEdgeIntersection(bounds, outsidePoint) {
738
778
  y: cy + dy * scale
739
779
  };
740
780
  }
741
- function findBindTarget(point, store, threshold, excludeId) {
781
+ function findBindTarget(point, store, threshold, excludeId, filter) {
742
782
  let closest = null;
743
783
  let closestDist = Infinity;
744
784
  for (const el of store.getAll()) {
745
785
  if (!isBindable(el)) continue;
746
786
  if (excludeId && el.id === excludeId) continue;
787
+ if (filter && !filter(el)) continue;
747
788
  const bounds = getElementBounds(el);
748
789
  if (!bounds) continue;
749
790
  const dist = distToBounds(point, bounds);
@@ -901,14 +942,19 @@ function smoothToSegments(points) {
901
942
  }
902
943
 
903
944
  // src/elements/element-renderer.ts
904
- var DOM_ELEMENT_TYPES = /* @__PURE__ */ new Set(["note", "image", "html", "text"]);
945
+ var DOM_ELEMENT_TYPES = /* @__PURE__ */ new Set(["note", "html", "text"]);
905
946
  var ARROWHEAD_LENGTH = 12;
906
947
  var ARROWHEAD_ANGLE = Math.PI / 6;
907
948
  var ElementRenderer = class {
908
949
  store = null;
950
+ imageCache = /* @__PURE__ */ new Map();
951
+ onImageLoad = null;
909
952
  setStore(store) {
910
953
  this.store = store;
911
954
  }
955
+ setOnImageLoad(callback) {
956
+ this.onImageLoad = callback;
957
+ }
912
958
  isDomElement(element) {
913
959
  return DOM_ELEMENT_TYPES.has(element.type);
914
960
  }
@@ -923,6 +969,9 @@ var ElementRenderer = class {
923
969
  case "shape":
924
970
  this.renderShape(ctx, element);
925
971
  break;
972
+ case "image":
973
+ this.renderImage(ctx, element);
974
+ break;
926
975
  }
927
976
  }
928
977
  renderStroke(ctx, stroke) {
@@ -1058,6 +1107,20 @@ var ElementRenderer = class {
1058
1107
  }
1059
1108
  }
1060
1109
  }
1110
+ renderImage(ctx, image) {
1111
+ const img = this.getImage(image.src);
1112
+ if (!img) return;
1113
+ ctx.drawImage(img, image.position.x, image.position.y, image.size.w, image.size.h);
1114
+ }
1115
+ getImage(src) {
1116
+ const cached = this.imageCache.get(src);
1117
+ if (cached) return cached.complete ? cached : null;
1118
+ const img = new Image();
1119
+ img.src = src;
1120
+ this.imageCache.set(src, img);
1121
+ img.onload = () => this.onImageLoad?.();
1122
+ return null;
1123
+ }
1061
1124
  };
1062
1125
 
1063
1126
  // src/elements/note-editor.ts
@@ -1409,6 +1472,7 @@ function createStroke(input) {
1409
1472
  position: input.position ?? { x: 0, y: 0 },
1410
1473
  zIndex: input.zIndex ?? 0,
1411
1474
  locked: input.locked ?? false,
1475
+ layerId: input.layerId ?? "",
1412
1476
  points: input.points,
1413
1477
  color: input.color ?? "#000000",
1414
1478
  width: input.width ?? 2,
@@ -1422,6 +1486,7 @@ function createNote(input) {
1422
1486
  position: input.position,
1423
1487
  zIndex: input.zIndex ?? 0,
1424
1488
  locked: input.locked ?? false,
1489
+ layerId: input.layerId ?? "",
1425
1490
  size: input.size ?? { w: 200, h: 100 },
1426
1491
  text: input.text ?? "",
1427
1492
  backgroundColor: input.backgroundColor ?? "#ffeb3b",
@@ -1435,6 +1500,7 @@ function createArrow(input) {
1435
1500
  position: input.position ?? { x: 0, y: 0 },
1436
1501
  zIndex: input.zIndex ?? 0,
1437
1502
  locked: input.locked ?? false,
1503
+ layerId: input.layerId ?? "",
1438
1504
  from: input.from,
1439
1505
  to: input.to,
1440
1506
  bend: input.bend ?? 0,
@@ -1452,6 +1518,7 @@ function createImage(input) {
1452
1518
  position: input.position,
1453
1519
  zIndex: input.zIndex ?? 0,
1454
1520
  locked: input.locked ?? false,
1521
+ layerId: input.layerId ?? "",
1455
1522
  size: input.size,
1456
1523
  src: input.src
1457
1524
  };
@@ -1463,6 +1530,7 @@ function createHtmlElement(input) {
1463
1530
  position: input.position,
1464
1531
  zIndex: input.zIndex ?? 0,
1465
1532
  locked: input.locked ?? false,
1533
+ layerId: input.layerId ?? "",
1466
1534
  size: input.size
1467
1535
  };
1468
1536
  }
@@ -1473,6 +1541,7 @@ function createShape(input) {
1473
1541
  position: input.position,
1474
1542
  zIndex: input.zIndex ?? 0,
1475
1543
  locked: input.locked ?? false,
1544
+ layerId: input.layerId ?? "",
1476
1545
  shape: input.shape ?? "rectangle",
1477
1546
  size: input.size,
1478
1547
  strokeColor: input.strokeColor ?? "#000000",
@@ -1487,6 +1556,7 @@ function createText(input) {
1487
1556
  position: input.position,
1488
1557
  zIndex: input.zIndex ?? 0,
1489
1558
  locked: input.locked ?? false,
1559
+ layerId: input.layerId ?? "",
1490
1560
  size: input.size ?? { w: 200, h: 28 },
1491
1561
  text: input.text ?? "",
1492
1562
  fontSize: input.fontSize ?? 16,
@@ -1495,16 +1565,168 @@ function createText(input) {
1495
1565
  };
1496
1566
  }
1497
1567
 
1568
+ // src/layers/layer-manager.ts
1569
+ var LayerManager = class {
1570
+ constructor(store) {
1571
+ this.store = store;
1572
+ const defaultLayer = {
1573
+ id: createId("layer"),
1574
+ name: "Layer 1",
1575
+ visible: true,
1576
+ locked: false,
1577
+ order: 0,
1578
+ opacity: 1
1579
+ };
1580
+ this.layers.set(defaultLayer.id, defaultLayer);
1581
+ this._activeLayerId = defaultLayer.id;
1582
+ this.syncLayerOrder();
1583
+ }
1584
+ layers = /* @__PURE__ */ new Map();
1585
+ _activeLayerId;
1586
+ bus = new EventBus();
1587
+ get activeLayer() {
1588
+ const layer = this.layers.get(this._activeLayerId);
1589
+ if (!layer) throw new Error("Active layer not found");
1590
+ return { ...layer };
1591
+ }
1592
+ get activeLayerId() {
1593
+ return this._activeLayerId;
1594
+ }
1595
+ getLayers() {
1596
+ return [...this.layers.values()].sort((a, b) => a.order - b.order).map((l) => ({ ...l }));
1597
+ }
1598
+ getLayer(id) {
1599
+ const layer = this.layers.get(id);
1600
+ return layer ? { ...layer } : void 0;
1601
+ }
1602
+ isLayerVisible(id) {
1603
+ return this.layers.get(id)?.visible ?? true;
1604
+ }
1605
+ isLayerLocked(id) {
1606
+ return this.layers.get(id)?.locked ?? false;
1607
+ }
1608
+ createLayer(name) {
1609
+ const maxOrder = Math.max(...[...this.layers.values()].map((l) => l.order), -1);
1610
+ const autoName = name ?? `Layer ${this.layers.size + 1}`;
1611
+ const layer = {
1612
+ id: createId("layer"),
1613
+ name: autoName,
1614
+ visible: true,
1615
+ locked: false,
1616
+ order: maxOrder + 1,
1617
+ opacity: 1
1618
+ };
1619
+ this.addLayerDirect(layer);
1620
+ return { ...layer };
1621
+ }
1622
+ removeLayer(id) {
1623
+ if (this.layers.size <= 1) {
1624
+ throw new Error("Cannot remove the last layer");
1625
+ }
1626
+ if (this._activeLayerId === id) {
1627
+ const remaining = [...this.layers.values()].filter((l) => l.id !== id).sort((a, b) => b.order - a.order);
1628
+ const fallback = remaining[0];
1629
+ if (fallback) this._activeLayerId = fallback.id;
1630
+ }
1631
+ const elements = this.store.getAll().filter((el) => el.layerId === id);
1632
+ for (const el of elements) {
1633
+ this.store.update(el.id, { layerId: this._activeLayerId });
1634
+ }
1635
+ this.removeLayerDirect(id);
1636
+ }
1637
+ renameLayer(id, name) {
1638
+ this.updateLayerDirect(id, { name });
1639
+ }
1640
+ reorderLayer(id, newOrder) {
1641
+ if (!this.layers.has(id)) return;
1642
+ this.updateLayerDirect(id, { order: newOrder });
1643
+ }
1644
+ setLayerVisible(id, visible) {
1645
+ if (!visible && id === this._activeLayerId) {
1646
+ const fallback = this.findFallbackLayer(id);
1647
+ if (!fallback) return false;
1648
+ this._activeLayerId = fallback.id;
1649
+ }
1650
+ this.updateLayerDirect(id, { visible });
1651
+ return true;
1652
+ }
1653
+ setLayerLocked(id, locked) {
1654
+ if (locked && id === this._activeLayerId) {
1655
+ const fallback = this.findFallbackLayer(id);
1656
+ if (!fallback) return false;
1657
+ this._activeLayerId = fallback.id;
1658
+ }
1659
+ this.updateLayerDirect(id, { locked });
1660
+ return true;
1661
+ }
1662
+ setActiveLayer(id) {
1663
+ if (!this.layers.has(id)) return;
1664
+ this._activeLayerId = id;
1665
+ this.bus.emit("change", null);
1666
+ }
1667
+ moveElementToLayer(elementId, layerId) {
1668
+ if (!this.layers.has(layerId)) return;
1669
+ this.store.update(elementId, { layerId });
1670
+ this.bus.emit("change", null);
1671
+ }
1672
+ snapshot() {
1673
+ return this.getLayers();
1674
+ }
1675
+ loadSnapshot(layers) {
1676
+ this.layers.clear();
1677
+ for (const layer of layers) {
1678
+ this.layers.set(layer.id, { ...layer });
1679
+ }
1680
+ const first = this.getLayers()[0];
1681
+ if (first) this._activeLayerId = first.id;
1682
+ this.syncLayerOrder();
1683
+ this.bus.emit("change", null);
1684
+ }
1685
+ on(event, callback) {
1686
+ return this.bus.on(event, callback);
1687
+ }
1688
+ addLayerDirect(layer) {
1689
+ this.layers.set(layer.id, { ...layer });
1690
+ this.syncLayerOrder();
1691
+ this.bus.emit("change", null);
1692
+ }
1693
+ removeLayerDirect(id) {
1694
+ this.layers.delete(id);
1695
+ this.syncLayerOrder();
1696
+ this.bus.emit("change", null);
1697
+ }
1698
+ updateLayerDirect(id, props) {
1699
+ const layer = this.layers.get(id);
1700
+ if (!layer) return;
1701
+ Object.assign(layer, props);
1702
+ if ("order" in props) this.syncLayerOrder();
1703
+ this.bus.emit("change", null);
1704
+ }
1705
+ syncLayerOrder() {
1706
+ const order = /* @__PURE__ */ new Map();
1707
+ for (const layer of this.layers.values()) {
1708
+ order.set(layer.id, layer.order);
1709
+ }
1710
+ this.store.setLayerOrder(order);
1711
+ }
1712
+ findFallbackLayer(excludeId) {
1713
+ return [...this.layers.values()].filter((l) => l.id !== excludeId && l.visible && !l.locked).sort((a, b) => b.order - a.order)[0];
1714
+ }
1715
+ };
1716
+
1498
1717
  // src/canvas/viewport.ts
1499
1718
  var Viewport = class {
1500
1719
  constructor(container, options = {}) {
1501
1720
  this.container = container;
1502
1721
  this.camera = new Camera(options.camera);
1503
1722
  this.background = new Background(options.background);
1723
+ this._gridSize = options.background?.spacing ?? 24;
1504
1724
  this.store = new ElementStore();
1725
+ this.layerManager = new LayerManager(this.store);
1505
1726
  this.toolManager = new ToolManager();
1506
1727
  this.renderer = new ElementRenderer();
1507
1728
  this.renderer.setStore(this.store);
1729
+ this.renderer.setOnImageLoad(() => this.requestRender());
1508
1730
  this.noteEditor = new NoteEditor();
1509
1731
  this.noteEditor.setOnStop((id) => this.onTextEditStop(id));
1510
1732
  this.history = new HistoryStack();
@@ -1523,7 +1745,12 @@ var Viewport = class {
1523
1745
  editElement: (id) => this.startEditingElement(id),
1524
1746
  setCursor: (cursor) => {
1525
1747
  this.wrapper.style.cursor = cursor;
1526
- }
1748
+ },
1749
+ snapToGrid: false,
1750
+ gridSize: this._gridSize,
1751
+ activeLayerId: this.layerManager.activeLayerId,
1752
+ isLayerVisible: (id) => this.layerManager.isLayerVisible(id),
1753
+ isLayerLocked: (id) => this.layerManager.isLayerLocked(id)
1527
1754
  };
1528
1755
  this.inputHandler = new InputHandler(this.wrapper, this.camera, {
1529
1756
  toolManager: this.toolManager,
@@ -1544,6 +1771,10 @@ var Viewport = class {
1544
1771
  this.store.on("update", () => this.requestRender()),
1545
1772
  this.store.on("clear", () => this.clearDomNodes())
1546
1773
  ];
1774
+ this.layerManager.on("change", () => {
1775
+ this.toolContext.activeLayerId = this.layerManager.activeLayerId;
1776
+ this.requestRender();
1777
+ });
1547
1778
  this.wrapper.addEventListener("dblclick", this.onDblClick);
1548
1779
  this.wrapper.addEventListener("dragover", this.onDragOver);
1549
1780
  this.wrapper.addEventListener("drop", this.onDrop);
@@ -1553,6 +1784,7 @@ var Viewport = class {
1553
1784
  }
1554
1785
  camera;
1555
1786
  store;
1787
+ layerManager;
1556
1788
  toolManager;
1557
1789
  history;
1558
1790
  domLayer;
@@ -1568,6 +1800,8 @@ var Viewport = class {
1568
1800
  toolContext;
1569
1801
  resizeObserver = null;
1570
1802
  animFrameId = 0;
1803
+ _snapToGrid = false;
1804
+ _gridSize;
1571
1805
  needsRender = true;
1572
1806
  domNodes = /* @__PURE__ */ new Map();
1573
1807
  htmlContent = /* @__PURE__ */ new Map();
@@ -1575,11 +1809,18 @@ var Viewport = class {
1575
1809
  get ctx() {
1576
1810
  return this.canvasEl.getContext("2d");
1577
1811
  }
1812
+ get snapToGrid() {
1813
+ return this._snapToGrid;
1814
+ }
1815
+ setSnapToGrid(enabled) {
1816
+ this._snapToGrid = enabled;
1817
+ this.toolContext.snapToGrid = enabled;
1818
+ }
1578
1819
  requestRender() {
1579
1820
  this.needsRender = true;
1580
1821
  }
1581
1822
  exportState() {
1582
- return exportState(this.store.snapshot(), this.camera);
1823
+ return exportState(this.store.snapshot(), this.camera, this.layerManager.snapshot());
1583
1824
  }
1584
1825
  exportJSON() {
1585
1826
  return JSON.stringify(this.exportState());
@@ -1589,6 +1830,9 @@ var Viewport = class {
1589
1830
  this.noteEditor.destroy(this.store);
1590
1831
  this.clearDomNodes();
1591
1832
  this.store.loadSnapshot(state.elements);
1833
+ if (state.layers && state.layers.length > 0) {
1834
+ this.layerManager.loadSnapshot(state.layers);
1835
+ }
1592
1836
  this.history.clear();
1593
1837
  this.historyRecorder.resume();
1594
1838
  this.camera.moveTo(state.camera.position.x, state.camera.position.y);
@@ -1612,7 +1856,7 @@ var Viewport = class {
1612
1856
  return result;
1613
1857
  }
1614
1858
  addImage(src, position, size = { w: 300, h: 200 }) {
1615
- const image = createImage({ position, size, src });
1859
+ const image = createImage({ position, size, src, layerId: this.layerManager.activeLayerId });
1616
1860
  this.historyRecorder.begin();
1617
1861
  this.store.add(image);
1618
1862
  this.historyRecorder.commit();
@@ -1620,7 +1864,7 @@ var Viewport = class {
1620
1864
  return image.id;
1621
1865
  }
1622
1866
  addHtmlElement(dom, position, size = { w: 200, h: 150 }) {
1623
- const el = createHtmlElement({ position, size });
1867
+ const el = createHtmlElement({ position, size, layerId: this.layerManager.activeLayerId });
1624
1868
  this.htmlContent.set(el.id, dom);
1625
1869
  this.historyRecorder.begin();
1626
1870
  this.store.add(el);
@@ -1663,9 +1907,17 @@ var Viewport = class {
1663
1907
  ctx.save();
1664
1908
  ctx.translate(this.camera.position.x, this.camera.position.y);
1665
1909
  ctx.scale(this.camera.zoom, this.camera.zoom);
1666
- for (const element of this.store.getAll()) {
1910
+ const allElements = this.store.getAll();
1911
+ let domZIndex = 0;
1912
+ for (const element of allElements) {
1913
+ if (!this.layerManager.isLayerVisible(element.layerId)) {
1914
+ if (this.renderer.isDomElement(element)) {
1915
+ this.hideDomNode(element.id);
1916
+ }
1917
+ continue;
1918
+ }
1667
1919
  if (this.renderer.isDomElement(element)) {
1668
- this.syncDomNode(element);
1920
+ this.syncDomNode(element, domZIndex++);
1669
1921
  } else {
1670
1922
  this.renderer.renderCanvasElement(ctx, element);
1671
1923
  }
@@ -1797,7 +2049,7 @@ var Viewport = class {
1797
2049
  reader.readAsDataURL(file);
1798
2050
  }
1799
2051
  };
1800
- syncDomNode(element) {
2052
+ syncDomNode(element, zIndex = 0) {
1801
2053
  let node = this.domNodes.get(element.id);
1802
2054
  if (!node) {
1803
2055
  node = document.createElement("div");
@@ -1811,10 +2063,12 @@ var Viewport = class {
1811
2063
  }
1812
2064
  const size = "size" in element ? element.size : null;
1813
2065
  Object.assign(node.style, {
2066
+ display: "block",
1814
2067
  left: `${element.position.x}px`,
1815
2068
  top: `${element.position.y}px`,
1816
2069
  width: size ? `${size.w}px` : "auto",
1817
- height: size ? `${size.h}px` : "auto"
2070
+ height: size ? `${size.h}px` : "auto",
2071
+ zIndex: String(zIndex)
1818
2072
  });
1819
2073
  this.renderDomContent(node, element);
1820
2074
  }
@@ -1849,26 +2103,6 @@ var Viewport = class {
1849
2103
  node.style.color = element.textColor;
1850
2104
  }
1851
2105
  }
1852
- if (element.type === "image") {
1853
- if (!node.dataset["initialized"]) {
1854
- node.dataset["initialized"] = "true";
1855
- const img = document.createElement("img");
1856
- img.src = element.src;
1857
- Object.assign(img.style, {
1858
- width: "100%",
1859
- height: "100%",
1860
- objectFit: "contain",
1861
- pointerEvents: "none"
1862
- });
1863
- img.draggable = false;
1864
- node.appendChild(img);
1865
- } else {
1866
- const img = node.querySelector("img");
1867
- if (img && img.src !== element.src) {
1868
- img.src = element.src;
1869
- }
1870
- }
1871
- }
1872
2106
  if (element.type === "html" && !node.dataset["initialized"]) {
1873
2107
  const content = this.htmlContent.get(element.id);
1874
2108
  if (content) {
@@ -1951,6 +2185,10 @@ var Viewport = class {
1951
2185
  }
1952
2186
  }
1953
2187
  }
2188
+ hideDomNode(id) {
2189
+ const node = this.domNodes.get(id);
2190
+ if (node) node.style.display = "none";
2191
+ }
1954
2192
  removeDomNode(id) {
1955
2193
  this.htmlContent.delete(id);
1956
2194
  const node = this.domNodes.get(id);
@@ -2097,7 +2335,8 @@ var PencilTool = class {
2097
2335
  const stroke = createStroke({
2098
2336
  points: simplified,
2099
2337
  color: this.color,
2100
- width: this.width
2338
+ width: this.width,
2339
+ layerId: ctx.activeLayerId ?? ""
2101
2340
  });
2102
2341
  ctx.store.add(stroke);
2103
2342
  this.points = [];
@@ -2161,6 +2400,8 @@ var EraserTool = class {
2161
2400
  const strokes = ctx.store.getElementsByType("stroke");
2162
2401
  let erased = false;
2163
2402
  for (const stroke of strokes) {
2403
+ if (ctx.isLayerVisible && !ctx.isLayerVisible(stroke.layerId)) continue;
2404
+ if (ctx.isLayerLocked && ctx.isLayerLocked(stroke.layerId)) continue;
2164
2405
  if (this.strokeIntersects(stroke, world)) {
2165
2406
  ctx.store.remove(stroke.id);
2166
2407
  erased = true;
@@ -2323,10 +2564,13 @@ var SelectTool = class {
2323
2564
  this.mode = { type: "idle" };
2324
2565
  ctx.setCursor?.("default");
2325
2566
  }
2567
+ snap(point, ctx) {
2568
+ return ctx.snapToGrid && ctx.gridSize ? snapPoint(point, ctx.gridSize) : point;
2569
+ }
2326
2570
  onPointerDown(state, ctx) {
2327
2571
  this.ctx = ctx;
2328
2572
  const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
2329
- this.lastWorld = world;
2573
+ this.lastWorld = this.snap(world, ctx);
2330
2574
  this.currentWorld = world;
2331
2575
  const arrowHit = hitTestArrowHandles(world, this._selectedIds, ctx);
2332
2576
  if (arrowHit) {
@@ -2379,9 +2623,10 @@ var SelectTool = class {
2379
2623
  }
2380
2624
  if (this.mode.type === "dragging" && this._selectedIds.length > 0) {
2381
2625
  ctx.setCursor?.("move");
2382
- const dx = world.x - this.lastWorld.x;
2383
- const dy = world.y - this.lastWorld.y;
2384
- this.lastWorld = world;
2626
+ const snapped = this.snap(world, ctx);
2627
+ const dx = snapped.x - this.lastWorld.x;
2628
+ const dy = snapped.y - this.lastWorld.y;
2629
+ this.lastWorld = snapped;
2385
2630
  for (const id of this._selectedIds) {
2386
2631
  const el = ctx.store.getById(id);
2387
2632
  if (!el || el.locked) continue;
@@ -2642,6 +2887,8 @@ var SelectTool = class {
2642
2887
  findElementsInRect(marquee, ctx) {
2643
2888
  const ids = [];
2644
2889
  for (const el of ctx.store.getAll()) {
2890
+ if (ctx.isLayerVisible && !ctx.isLayerVisible(el.layerId)) continue;
2891
+ if (ctx.isLayerLocked && ctx.isLayerLocked(el.layerId)) continue;
2645
2892
  const bounds = this.getElementBounds(el);
2646
2893
  if (bounds && this.rectsOverlap(marquee, bounds)) {
2647
2894
  ids.push(el.id);
@@ -2676,6 +2923,8 @@ var SelectTool = class {
2676
2923
  hitTest(world, ctx) {
2677
2924
  const elements = ctx.store.getAll().reverse();
2678
2925
  for (const el of elements) {
2926
+ if (ctx.isLayerVisible && !ctx.isLayerVisible(el.layerId)) continue;
2927
+ if (ctx.isLayerLocked && ctx.isLayerLocked(el.layerId)) continue;
2679
2928
  if (this.isInsideBounds(world, el)) return el;
2680
2929
  }
2681
2930
  return null;
@@ -2720,17 +2969,26 @@ var ArrowTool = class {
2720
2969
  if (options.color !== void 0) this.color = options.color;
2721
2970
  if (options.width !== void 0) this.width = options.width;
2722
2971
  }
2972
+ layerFilter(ctx) {
2973
+ if (!ctx.isLayerVisible && !ctx.isLayerLocked) return void 0;
2974
+ return (el) => {
2975
+ if (ctx.isLayerVisible && !ctx.isLayerVisible(el.layerId)) return false;
2976
+ if (ctx.isLayerLocked && ctx.isLayerLocked(el.layerId)) return false;
2977
+ return true;
2978
+ };
2979
+ }
2723
2980
  onPointerDown(state, ctx) {
2724
2981
  this.drawing = true;
2725
2982
  const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
2726
2983
  const threshold = BIND_THRESHOLD2 / ctx.camera.zoom;
2727
- const target = findBindTarget(world, ctx.store, threshold);
2984
+ const filter = this.layerFilter(ctx);
2985
+ const target = findBindTarget(world, ctx.store, threshold, void 0, filter);
2728
2986
  if (target) {
2729
2987
  this.start = getElementCenter(target);
2730
2988
  this.fromBinding = { elementId: target.id };
2731
2989
  this.fromTarget = target;
2732
2990
  } else {
2733
- this.start = world;
2991
+ this.start = ctx.snapToGrid && ctx.gridSize ? snapPoint(world, ctx.gridSize) : world;
2734
2992
  this.fromBinding = void 0;
2735
2993
  this.fromTarget = null;
2736
2994
  }
@@ -2742,12 +3000,13 @@ var ArrowTool = class {
2742
3000
  const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
2743
3001
  const threshold = BIND_THRESHOLD2 / ctx.camera.zoom;
2744
3002
  const excludeId = this.fromBinding?.elementId;
2745
- const target = findBindTarget(world, ctx.store, threshold, excludeId);
3003
+ const filter = this.layerFilter(ctx);
3004
+ const target = findBindTarget(world, ctx.store, threshold, excludeId, filter);
2746
3005
  if (target) {
2747
3006
  this.end = getElementCenter(target);
2748
3007
  this.toTarget = target;
2749
3008
  } else {
2750
- this.end = world;
3009
+ this.end = ctx.snapToGrid && ctx.gridSize ? snapPoint(world, ctx.gridSize) : world;
2751
3010
  this.toTarget = null;
2752
3011
  }
2753
3012
  ctx.requestRender();
@@ -2763,7 +3022,8 @@ var ArrowTool = class {
2763
3022
  color: this.color,
2764
3023
  width: this.width,
2765
3024
  fromBinding: this.fromBinding,
2766
- toBinding: this.toTarget ? { elementId: this.toTarget.id } : void 0
3025
+ toBinding: this.toTarget ? { elementId: this.toTarget.id } : void 0,
3026
+ layerId: ctx.activeLayerId ?? ""
2767
3027
  });
2768
3028
  ctx.store.add(arrow);
2769
3029
  this.fromTarget = null;
@@ -2842,12 +3102,16 @@ var NoteTool = class {
2842
3102
  onPointerMove(_state, _ctx) {
2843
3103
  }
2844
3104
  onPointerUp(state, ctx) {
2845
- const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
3105
+ let world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
3106
+ if (ctx.snapToGrid && ctx.gridSize) {
3107
+ world = snapPoint(world, ctx.gridSize);
3108
+ }
2846
3109
  const note = createNote({
2847
3110
  position: world,
2848
3111
  size: { ...this.size },
2849
3112
  backgroundColor: this.backgroundColor,
2850
- textColor: this.textColor
3113
+ textColor: this.textColor,
3114
+ layerId: ctx.activeLayerId ?? ""
2851
3115
  });
2852
3116
  ctx.store.add(note);
2853
3117
  ctx.requestRender();
@@ -2883,12 +3147,16 @@ var TextTool = class {
2883
3147
  onPointerMove(_state, _ctx) {
2884
3148
  }
2885
3149
  onPointerUp(state, ctx) {
2886
- const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
3150
+ let world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
3151
+ if (ctx.snapToGrid && ctx.gridSize) {
3152
+ world = snapPoint(world, ctx.gridSize);
3153
+ }
2887
3154
  const textEl = createText({
2888
3155
  position: world,
2889
3156
  fontSize: this.fontSize,
2890
3157
  color: this.color,
2891
- textAlign: this.textAlign
3158
+ textAlign: this.textAlign,
3159
+ layerId: ctx.activeLayerId ?? ""
2892
3160
  });
2893
3161
  ctx.store.add(textEl);
2894
3162
  ctx.requestRender();
@@ -2965,12 +3233,12 @@ var ShapeTool = class {
2965
3233
  }
2966
3234
  onPointerDown(state, ctx) {
2967
3235
  this.drawing = true;
2968
- this.start = ctx.camera.screenToWorld({ x: state.x, y: state.y });
3236
+ this.start = this.snap(ctx.camera.screenToWorld({ x: state.x, y: state.y }), ctx);
2969
3237
  this.end = { ...this.start };
2970
3238
  }
2971
3239
  onPointerMove(state, ctx) {
2972
3240
  if (!this.drawing) return;
2973
- this.end = ctx.camera.screenToWorld({ x: state.x, y: state.y });
3241
+ this.end = this.snap(ctx.camera.screenToWorld({ x: state.x, y: state.y }), ctx);
2974
3242
  ctx.requestRender();
2975
3243
  }
2976
3244
  onPointerUp(_state, ctx) {
@@ -2984,7 +3252,8 @@ var ShapeTool = class {
2984
3252
  shape: this.shape,
2985
3253
  strokeColor: this.strokeColor,
2986
3254
  strokeWidth: this.strokeWidth,
2987
- fillColor: this.fillColor
3255
+ fillColor: this.fillColor,
3256
+ layerId: ctx.activeLayerId ?? ""
2988
3257
  });
2989
3258
  ctx.store.add(shape);
2990
3259
  ctx.requestRender();
@@ -3034,6 +3303,9 @@ var ShapeTool = class {
3034
3303
  }
3035
3304
  return { position: { x, y }, size: { w, h } };
3036
3305
  }
3306
+ snap(point, ctx) {
3307
+ return ctx.snapToGrid && ctx.gridSize ? snapPoint(point, ctx.gridSize) : point;
3308
+ }
3037
3309
  onKeyDown = (e) => {
3038
3310
  if (e.key === "Shift") this.shiftHeld = true;
3039
3311
  };
@@ -3042,8 +3314,48 @@ var ShapeTool = class {
3042
3314
  };
3043
3315
  };
3044
3316
 
3317
+ // src/history/layer-commands.ts
3318
+ var CreateLayerCommand = class {
3319
+ constructor(manager, layer) {
3320
+ this.manager = manager;
3321
+ this.layer = layer;
3322
+ }
3323
+ execute(_store) {
3324
+ this.manager.addLayerDirect(this.layer);
3325
+ }
3326
+ undo(_store) {
3327
+ this.manager.removeLayerDirect(this.layer.id);
3328
+ }
3329
+ };
3330
+ var RemoveLayerCommand = class {
3331
+ constructor(manager, layer) {
3332
+ this.manager = manager;
3333
+ this.layer = layer;
3334
+ }
3335
+ execute(_store) {
3336
+ this.manager.removeLayerDirect(this.layer.id);
3337
+ }
3338
+ undo(_store) {
3339
+ this.manager.addLayerDirect(this.layer);
3340
+ }
3341
+ };
3342
+ var UpdateLayerCommand = class {
3343
+ constructor(manager, layerId, previous, current) {
3344
+ this.manager = manager;
3345
+ this.layerId = layerId;
3346
+ this.previous = previous;
3347
+ this.current = current;
3348
+ }
3349
+ execute(_store) {
3350
+ this.manager.updateLayerDirect(this.layerId, { ...this.current });
3351
+ }
3352
+ undo(_store) {
3353
+ this.manager.updateLayerDirect(this.layerId, { ...this.previous });
3354
+ }
3355
+ };
3356
+
3045
3357
  // src/index.ts
3046
- var VERSION = "0.4.1";
3358
+ var VERSION = "0.6.0";
3047
3359
  export {
3048
3360
  AddElementCommand,
3049
3361
  ArrowTool,
@@ -3051,6 +3363,7 @@ export {
3051
3363
  Background,
3052
3364
  BatchCommand,
3053
3365
  Camera,
3366
+ CreateLayerCommand,
3054
3367
  ElementRenderer,
3055
3368
  ElementStore,
3056
3369
  EraserTool,
@@ -3060,15 +3373,18 @@ export {
3060
3373
  HistoryStack,
3061
3374
  ImageTool,
3062
3375
  InputHandler,
3376
+ LayerManager,
3063
3377
  NoteEditor,
3064
3378
  NoteTool,
3065
3379
  PencilTool,
3066
3380
  RemoveElementCommand,
3381
+ RemoveLayerCommand,
3067
3382
  SelectTool,
3068
3383
  ShapeTool,
3069
3384
  TextTool,
3070
3385
  ToolManager,
3071
3386
  UpdateElementCommand,
3387
+ UpdateLayerCommand,
3072
3388
  VERSION,
3073
3389
  Viewport,
3074
3390
  clearStaleBindings,
@@ -3094,6 +3410,7 @@ export {
3094
3410
  isBindable,
3095
3411
  isNearBezier,
3096
3412
  parseState,
3413
+ snapPoint,
3097
3414
  unbindArrow,
3098
3415
  updateBoundArrow
3099
3416
  };