@fieldnotes/core 0.7.0 → 0.8.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
@@ -60,6 +60,7 @@ __export(index_exports, {
60
60
  createShape: () => createShape,
61
61
  createStroke: () => createStroke,
62
62
  createText: () => createText,
63
+ exportImage: () => exportImage,
63
64
  exportState: () => exportState,
64
65
  findBindTarget: () => findBindTarget,
65
66
  findBoundArrows: () => findBoundArrows,
@@ -1769,7 +1770,7 @@ function createImage(input) {
1769
1770
  };
1770
1771
  }
1771
1772
  function createHtmlElement(input) {
1772
- return {
1773
+ const el = {
1773
1774
  id: createId("html"),
1774
1775
  type: "html",
1775
1776
  position: input.position,
@@ -1778,6 +1779,8 @@ function createHtmlElement(input) {
1778
1779
  layerId: input.layerId ?? "",
1779
1780
  size: input.size
1780
1781
  };
1782
+ if (input.domId) el.domId = input.domId;
1783
+ return el;
1781
1784
  }
1782
1785
  function createShape(input) {
1783
1786
  return {
@@ -1826,6 +1829,256 @@ function createText(input) {
1826
1829
  };
1827
1830
  }
1828
1831
 
1832
+ // src/canvas/export-image.ts
1833
+ function getStrokeBounds(el) {
1834
+ if (el.type !== "stroke") return null;
1835
+ if (el.points.length === 0) return null;
1836
+ let minX = Infinity;
1837
+ let minY = Infinity;
1838
+ let maxX = -Infinity;
1839
+ let maxY = -Infinity;
1840
+ for (const p of el.points) {
1841
+ const px = el.position.x + p.x;
1842
+ const py = el.position.y + p.y;
1843
+ minX = Math.min(minX, px);
1844
+ minY = Math.min(minY, py);
1845
+ maxX = Math.max(maxX, px);
1846
+ maxY = Math.max(maxY, py);
1847
+ }
1848
+ const pad = el.width / 2;
1849
+ return {
1850
+ x: minX - pad,
1851
+ y: minY - pad,
1852
+ w: maxX - minX + el.width,
1853
+ h: maxY - minY + el.width
1854
+ };
1855
+ }
1856
+ function getElementRect(el) {
1857
+ switch (el.type) {
1858
+ case "stroke":
1859
+ return getStrokeBounds(el);
1860
+ case "arrow": {
1861
+ const b = getArrowBounds(el.from, el.to, el.bend);
1862
+ const pad = el.width / 2 + 14;
1863
+ return { x: b.x - pad, y: b.y - pad, w: b.w + pad * 2, h: b.h + pad * 2 };
1864
+ }
1865
+ case "grid":
1866
+ return null;
1867
+ case "note":
1868
+ case "image":
1869
+ case "html":
1870
+ case "text":
1871
+ case "shape":
1872
+ if ("size" in el) {
1873
+ return { x: el.position.x, y: el.position.y, w: el.size.w, h: el.size.h };
1874
+ }
1875
+ return null;
1876
+ default:
1877
+ return null;
1878
+ }
1879
+ }
1880
+ function computeBounds(elements, padding) {
1881
+ let minX = Infinity;
1882
+ let minY = Infinity;
1883
+ let maxX = -Infinity;
1884
+ let maxY = -Infinity;
1885
+ let found = false;
1886
+ for (const el of elements) {
1887
+ const rect = getElementRect(el);
1888
+ if (!rect) continue;
1889
+ found = true;
1890
+ minX = Math.min(minX, rect.x);
1891
+ minY = Math.min(minY, rect.y);
1892
+ maxX = Math.max(maxX, rect.x + rect.w);
1893
+ maxY = Math.max(maxY, rect.y + rect.h);
1894
+ }
1895
+ if (!found) return null;
1896
+ return {
1897
+ x: minX - padding,
1898
+ y: minY - padding,
1899
+ w: maxX - minX + padding * 2,
1900
+ h: maxY - minY + padding * 2
1901
+ };
1902
+ }
1903
+ function renderNoteOnCanvas(ctx, note) {
1904
+ const { x, y } = note.position;
1905
+ const { w, h } = note.size;
1906
+ const r = 4;
1907
+ const pad = 8;
1908
+ ctx.save();
1909
+ ctx.fillStyle = note.backgroundColor;
1910
+ ctx.beginPath();
1911
+ ctx.moveTo(x + r, y);
1912
+ ctx.lineTo(x + w - r, y);
1913
+ ctx.arcTo(x + w, y, x + w, y + r, r);
1914
+ ctx.lineTo(x + w, y + h - r);
1915
+ ctx.arcTo(x + w, y + h, x + w - r, y + h, r);
1916
+ ctx.lineTo(x + r, y + h);
1917
+ ctx.arcTo(x, y + h, x, y + h - r, r);
1918
+ ctx.lineTo(x, y + r);
1919
+ ctx.arcTo(x, y, x + r, y, r);
1920
+ ctx.closePath();
1921
+ ctx.fill();
1922
+ if (note.text) {
1923
+ ctx.fillStyle = note.textColor;
1924
+ ctx.font = "14px system-ui, sans-serif";
1925
+ ctx.textBaseline = "top";
1926
+ wrapText(ctx, note.text, x + pad, y + pad, w - pad * 2, 18);
1927
+ }
1928
+ ctx.restore();
1929
+ }
1930
+ function renderTextOnCanvas(ctx, text) {
1931
+ if (!text.text) return;
1932
+ ctx.save();
1933
+ ctx.fillStyle = text.color;
1934
+ ctx.font = `${text.fontSize}px system-ui, sans-serif`;
1935
+ ctx.textBaseline = "top";
1936
+ ctx.textAlign = text.textAlign;
1937
+ const pad = 2;
1938
+ let textX = text.position.x + pad;
1939
+ if (text.textAlign === "center") {
1940
+ textX = text.position.x + text.size.w / 2;
1941
+ } else if (text.textAlign === "right") {
1942
+ textX = text.position.x + text.size.w - pad;
1943
+ }
1944
+ const lineHeight = text.fontSize * 1.4;
1945
+ const lines = text.text.split("\n");
1946
+ for (let i = 0; i < lines.length; i++) {
1947
+ const line = lines[i];
1948
+ if (line !== void 0) {
1949
+ ctx.fillText(line, textX, text.position.y + pad + i * lineHeight);
1950
+ }
1951
+ }
1952
+ ctx.restore();
1953
+ }
1954
+ function wrapText(ctx, text, x, y, maxWidth, lineHeight) {
1955
+ const words = text.split(" ");
1956
+ let line = "";
1957
+ let offsetY = 0;
1958
+ for (const word of words) {
1959
+ const testLine = line ? `${line} ${word}` : word;
1960
+ const metrics = ctx.measureText(testLine);
1961
+ if (metrics.width > maxWidth && line) {
1962
+ ctx.fillText(line, x, y + offsetY);
1963
+ line = word;
1964
+ offsetY += lineHeight;
1965
+ } else {
1966
+ line = testLine;
1967
+ }
1968
+ }
1969
+ if (line) {
1970
+ ctx.fillText(line, x, y + offsetY);
1971
+ }
1972
+ }
1973
+ function renderGridForBounds(ctx, grid, bounds) {
1974
+ const visibleBounds = {
1975
+ minX: bounds.x,
1976
+ minY: bounds.y,
1977
+ maxX: bounds.x + bounds.w,
1978
+ maxY: bounds.y + bounds.h
1979
+ };
1980
+ if (grid.gridType === "hex") {
1981
+ renderHexGrid(
1982
+ ctx,
1983
+ visibleBounds,
1984
+ grid.cellSize,
1985
+ grid.hexOrientation,
1986
+ grid.strokeColor,
1987
+ grid.strokeWidth,
1988
+ grid.opacity
1989
+ );
1990
+ } else {
1991
+ renderSquareGrid(
1992
+ ctx,
1993
+ visibleBounds,
1994
+ grid.cellSize,
1995
+ grid.strokeColor,
1996
+ grid.strokeWidth,
1997
+ grid.opacity
1998
+ );
1999
+ }
2000
+ }
2001
+ function loadImages(elements) {
2002
+ const imageElements = elements.filter(
2003
+ (el) => el.type === "image" && "src" in el
2004
+ );
2005
+ const cache = /* @__PURE__ */ new Map();
2006
+ if (imageElements.length === 0) return Promise.resolve(cache);
2007
+ return new Promise((resolve) => {
2008
+ let remaining = imageElements.length;
2009
+ const done = () => {
2010
+ remaining--;
2011
+ if (remaining <= 0) resolve(cache);
2012
+ };
2013
+ for (const el of imageElements) {
2014
+ const img = new Image();
2015
+ img.onload = () => {
2016
+ cache.set(el.id, img);
2017
+ done();
2018
+ };
2019
+ img.onerror = done;
2020
+ img.src = el.src;
2021
+ }
2022
+ });
2023
+ }
2024
+ async function exportImage(store, options = {}, layerManager) {
2025
+ const scale = options.scale ?? 2;
2026
+ const padding = options.padding ?? 0;
2027
+ const background = options.background ?? "#ffffff";
2028
+ const filter = options.filter;
2029
+ const allElements = store.getAll();
2030
+ let visibleElements = layerManager ? allElements.filter((el) => layerManager.isLayerVisible(el.layerId)) : allElements;
2031
+ if (filter) {
2032
+ visibleElements = visibleElements.filter(filter);
2033
+ }
2034
+ const bounds = computeBounds(visibleElements, padding);
2035
+ if (!bounds) return null;
2036
+ const imageCache = await loadImages(visibleElements);
2037
+ const canvas = document.createElement("canvas");
2038
+ canvas.width = Math.ceil(bounds.w * scale);
2039
+ canvas.height = Math.ceil(bounds.h * scale);
2040
+ const ctx = canvas.getContext("2d");
2041
+ if (!ctx) return null;
2042
+ ctx.scale(scale, scale);
2043
+ ctx.translate(-bounds.x, -bounds.y);
2044
+ ctx.fillStyle = background;
2045
+ ctx.fillRect(bounds.x, bounds.y, bounds.w, bounds.h);
2046
+ const renderer = new ElementRenderer();
2047
+ renderer.setStore(store);
2048
+ const grids = [];
2049
+ for (const el of visibleElements) {
2050
+ if (el.type === "grid") {
2051
+ grids.push(el);
2052
+ continue;
2053
+ }
2054
+ if (el.type === "note") {
2055
+ renderNoteOnCanvas(ctx, el);
2056
+ continue;
2057
+ }
2058
+ if (el.type === "text") {
2059
+ renderTextOnCanvas(ctx, el);
2060
+ continue;
2061
+ }
2062
+ if (el.type === "html") {
2063
+ continue;
2064
+ }
2065
+ if (el.type === "image") {
2066
+ const img = imageCache.get(el.id);
2067
+ if (img) {
2068
+ ctx.drawImage(img, el.position.x, el.position.y, el.size.w, el.size.h);
2069
+ }
2070
+ continue;
2071
+ }
2072
+ renderer.renderCanvasElement(ctx, el);
2073
+ }
2074
+ for (const grid of grids) {
2075
+ renderGridForBounds(ctx, grid, bounds);
2076
+ }
2077
+ return new Promise((resolve) => {
2078
+ canvas.toBlob((blob) => resolve(blob), "image/png");
2079
+ });
2080
+ }
2081
+
1829
2082
  // src/layers/layer-manager.ts
1830
2083
  var LayerManager = class {
1831
2084
  constructor(store) {
@@ -2087,6 +2340,9 @@ var Viewport = class {
2087
2340
  exportJSON() {
2088
2341
  return JSON.stringify(this.exportState());
2089
2342
  }
2343
+ async exportImage(options) {
2344
+ return exportImage(this.store, options, this.layerManager);
2345
+ }
2090
2346
  loadState(state) {
2091
2347
  this.historyRecorder.pause();
2092
2348
  this.noteEditor.destroy(this.store);
@@ -2095,6 +2351,7 @@ var Viewport = class {
2095
2351
  if (state.layers && state.layers.length > 0) {
2096
2352
  this.layerManager.loadSnapshot(state.layers);
2097
2353
  }
2354
+ this.reattachHtmlContent();
2098
2355
  this.history.clear();
2099
2356
  this.historyRecorder.resume();
2100
2357
  this.camera.moveTo(state.camera.position.x, state.camera.position.y);
@@ -2126,7 +2383,13 @@ var Viewport = class {
2126
2383
  return image.id;
2127
2384
  }
2128
2385
  addHtmlElement(dom, position, size = { w: 200, h: 150 }) {
2129
- const el = createHtmlElement({ position, size, layerId: this.layerManager.activeLayerId });
2386
+ const domId = dom.id || void 0;
2387
+ const el = createHtmlElement({
2388
+ position,
2389
+ size,
2390
+ domId,
2391
+ layerId: this.layerManager.activeLayerId
2392
+ });
2130
2393
  this.htmlContent.set(el.id, dom);
2131
2394
  this.historyRecorder.begin();
2132
2395
  this.store.add(el);
@@ -2495,6 +2758,16 @@ var Viewport = class {
2495
2758
  this.htmlContent.clear();
2496
2759
  this.requestRender();
2497
2760
  }
2761
+ reattachHtmlContent() {
2762
+ for (const el of this.store.getElementsByType("html")) {
2763
+ if (el.domId) {
2764
+ const dom = document.getElementById(el.domId);
2765
+ if (dom) {
2766
+ this.htmlContent.set(el.id, dom);
2767
+ }
2768
+ }
2769
+ }
2770
+ }
2498
2771
  createWrapper() {
2499
2772
  const el = document.createElement("div");
2500
2773
  Object.assign(el.style, {
@@ -2753,10 +3026,11 @@ function applyArrowHandleDrag(handle, elementId, world, ctx) {
2753
3026
  const el = ctx.store.getById(elementId);
2754
3027
  if (!el || el.type !== "arrow") return;
2755
3028
  const threshold = BIND_THRESHOLD / ctx.camera.zoom;
3029
+ const layerFilter = (candidate) => candidate.layerId === el.layerId;
2756
3030
  switch (handle) {
2757
3031
  case "start": {
2758
3032
  const excludeId = el.toBinding?.elementId;
2759
- const target = findBindTarget(world, ctx.store, threshold, excludeId);
3033
+ const target = findBindTarget(world, ctx.store, threshold, excludeId, layerFilter);
2760
3034
  if (target) {
2761
3035
  const center = getElementCenter(target);
2762
3036
  ctx.store.update(elementId, {
@@ -2775,7 +3049,7 @@ function applyArrowHandleDrag(handle, elementId, world, ctx) {
2775
3049
  }
2776
3050
  case "end": {
2777
3051
  const excludeId = el.fromBinding?.elementId;
2778
- const target = findBindTarget(world, ctx.store, threshold, excludeId);
3052
+ const target = findBindTarget(world, ctx.store, threshold, excludeId, layerFilter);
2779
3053
  if (target) {
2780
3054
  const center = getElementCenter(target);
2781
3055
  ctx.store.update(elementId, {
@@ -2804,7 +3078,8 @@ function getArrowHandleDragTarget(handle, elementId, world, ctx) {
2804
3078
  if (!el || el.type !== "arrow") return null;
2805
3079
  const threshold = BIND_THRESHOLD / ctx.camera.zoom;
2806
3080
  const excludeId = handle === "start" ? el.toBinding?.elementId : el.fromBinding?.elementId;
2807
- const target = findBindTarget(world, ctx.store, threshold, excludeId);
3081
+ const layerFilter = (candidate) => candidate.layerId === el.layerId;
3082
+ const target = findBindTarget(world, ctx.store, threshold, excludeId, layerFilter);
2808
3083
  if (!target) return null;
2809
3084
  return getElementBounds(target);
2810
3085
  }
@@ -3264,8 +3539,10 @@ var ArrowTool = class {
3264
3539
  if (options.width !== void 0) this.width = options.width;
3265
3540
  }
3266
3541
  layerFilter(ctx) {
3267
- if (!ctx.isLayerVisible && !ctx.isLayerLocked) return void 0;
3542
+ const activeLayerId = ctx.activeLayerId;
3543
+ if (!activeLayerId && !ctx.isLayerVisible && !ctx.isLayerLocked) return void 0;
3268
3544
  return (el) => {
3545
+ if (activeLayerId && el.layerId !== activeLayerId) return false;
3269
3546
  if (ctx.isLayerVisible && !ctx.isLayerVisible(el.layerId)) return false;
3270
3547
  if (ctx.isLayerLocked && ctx.isLayerLocked(el.layerId)) return false;
3271
3548
  return true;
@@ -3649,7 +3926,7 @@ var UpdateLayerCommand = class {
3649
3926
  };
3650
3927
 
3651
3928
  // src/index.ts
3652
- var VERSION = "0.6.1";
3929
+ var VERSION = "0.8.0";
3653
3930
  // Annotate the CommonJS export names for ESM import in node:
3654
3931
  0 && (module.exports = {
3655
3932
  AddElementCommand,
@@ -3692,6 +3969,7 @@ var VERSION = "0.6.1";
3692
3969
  createShape,
3693
3970
  createStroke,
3694
3971
  createText,
3972
+ exportImage,
3695
3973
  exportState,
3696
3974
  findBindTarget,
3697
3975
  findBoundArrows,