@fieldnotes/core 0.50.8 → 0.51.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
@@ -5443,6 +5443,9 @@ function renderTextOnCanvas(ctx, text) {
5443
5443
  }
5444
5444
 
5445
5445
  // src/canvas/export-image.ts
5446
+ var DEFAULT_IMAGE_TIMEOUT_MS = 1e4;
5447
+ var DEFAULT_MAX_DIMENSION = 16384;
5448
+ var DEFAULT_MAX_PIXELS = 67108864;
5446
5449
  var center = (b) => ({ x: b.x + b.w / 2, y: b.y + b.h / 2 });
5447
5450
  function getStrokeBounds(el) {
5448
5451
  if (el.type !== "stroke") return null;
@@ -5552,12 +5555,53 @@ function renderGridForBounds(ctx, grid, bounds) {
5552
5555
  );
5553
5556
  }
5554
5557
  }
5555
- function loadImages(elements) {
5558
+ function positiveOption(value, fallback, name) {
5559
+ const resolved = value ?? fallback;
5560
+ if (!Number.isFinite(resolved) || resolved <= 0) {
5561
+ throw new RangeError(`${name} must be a finite number greater than 0`);
5562
+ }
5563
+ return resolved;
5564
+ }
5565
+ function nonNegativeOption(value, fallback, name) {
5566
+ const resolved = value ?? fallback;
5567
+ if (!Number.isFinite(resolved) || resolved < 0) {
5568
+ throw new RangeError(`${name} must be a finite number greater than or equal to 0`);
5569
+ }
5570
+ return resolved;
5571
+ }
5572
+ function assertExportSize(width, height, options) {
5573
+ const maxDimension = positiveOption(options.maxDimension, DEFAULT_MAX_DIMENSION, "maxDimension");
5574
+ const maxPixels = positiveOption(options.maxPixels, DEFAULT_MAX_PIXELS, "maxPixels");
5575
+ if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
5576
+ throw new RangeError("Export dimensions must be finite numbers greater than 0");
5577
+ }
5578
+ if (width > maxDimension || height > maxDimension) {
5579
+ throw new RangeError(
5580
+ `Export dimensions ${width}x${height} exceed the maximum dimension of ${maxDimension}`
5581
+ );
5582
+ }
5583
+ if (width * height > maxPixels) {
5584
+ throw new RangeError(
5585
+ `Export size ${width}x${height} exceeds the maximum of ${maxPixels} pixels`
5586
+ );
5587
+ }
5588
+ }
5589
+ function validateExportResourceOptions(options) {
5590
+ positiveOption(options.imageTimeoutMs, DEFAULT_IMAGE_TIMEOUT_MS, "imageTimeoutMs");
5591
+ positiveOption(options.maxDimension, DEFAULT_MAX_DIMENSION, "maxDimension");
5592
+ positiveOption(options.maxPixels, DEFAULT_MAX_PIXELS, "maxPixels");
5593
+ }
5594
+ function loadImages(elements, options = {}) {
5556
5595
  const imageElements = elements.filter(
5557
5596
  (el) => el.type === "image" && "src" in el
5558
5597
  );
5559
5598
  const cache3 = /* @__PURE__ */ new Map();
5560
5599
  if (imageElements.length === 0) return Promise.resolve(cache3);
5600
+ const timeoutMs = positiveOption(
5601
+ options.imageTimeoutMs,
5602
+ DEFAULT_IMAGE_TIMEOUT_MS,
5603
+ "imageTimeoutMs"
5604
+ );
5561
5605
  return new Promise((resolve) => {
5562
5606
  let remaining = imageElements.length;
5563
5607
  const done = () => {
@@ -5567,19 +5611,41 @@ function loadImages(elements) {
5567
5611
  for (const el of imageElements) {
5568
5612
  const img = new Image();
5569
5613
  img.crossOrigin = "anonymous";
5614
+ let settled = false;
5615
+ const timer = setTimeout(() => {
5616
+ if (settled) return;
5617
+ settled = true;
5618
+ img.onload = null;
5619
+ img.onerror = null;
5620
+ options.onAssetError?.({ elementId: el.id, src: el.src, reason: "timeout" });
5621
+ done();
5622
+ }, timeoutMs);
5623
+ const settle = () => {
5624
+ if (settled) return false;
5625
+ settled = true;
5626
+ clearTimeout(timer);
5627
+ img.onload = null;
5628
+ img.onerror = null;
5629
+ return true;
5630
+ };
5570
5631
  img.onload = () => {
5632
+ if (!settle()) return;
5571
5633
  cache3.set(el.id, img);
5572
5634
  done();
5573
5635
  };
5574
- img.onerror = done;
5575
- const sep = el.src.includes("?") ? "&" : "?";
5576
- img.src = `${el.src}${sep}_cors=1`;
5636
+ img.onerror = (cause) => {
5637
+ if (!settle()) return;
5638
+ options.onAssetError?.({ elementId: el.id, src: el.src, reason: "load", cause });
5639
+ done();
5640
+ };
5641
+ img.src = el.src;
5577
5642
  }
5578
5643
  });
5579
5644
  }
5580
5645
  async function exportImage(store, options = {}, layerManager) {
5581
- const scale = options.scale ?? 2;
5582
- const padding = options.padding ?? 0;
5646
+ const scale = positiveOption(options.scale, 2, "scale");
5647
+ const padding = nonNegativeOption(options.padding, 0, "padding");
5648
+ validateExportResourceOptions(options);
5583
5649
  const background = options.background ?? "#ffffff";
5584
5650
  const filter = options.filter;
5585
5651
  const allElements = store.getAll();
@@ -5589,10 +5655,13 @@ async function exportImage(store, options = {}, layerManager) {
5589
5655
  }
5590
5656
  const bounds = computeBounds(visibleElements, padding);
5591
5657
  if (!bounds) return null;
5592
- const imageCache = await loadImages(visibleElements);
5658
+ const width = Math.ceil(bounds.w * scale);
5659
+ const height = Math.ceil(bounds.h * scale);
5660
+ assertExportSize(width, height, options);
5661
+ const imageCache = await loadImages(visibleElements, options);
5593
5662
  const canvas = document.createElement("canvas");
5594
- canvas.width = Math.ceil(bounds.w * scale);
5595
- canvas.height = Math.ceil(bounds.h * scale);
5663
+ canvas.width = width;
5664
+ canvas.height = height;
5596
5665
  const ctx = canvas.getContext("2d");
5597
5666
  if (!ctx) return null;
5598
5667
  ctx.scale(scale, scale);
@@ -5775,14 +5844,17 @@ function emitImage(image, dataUri) {
5775
5844
  const { w, h } = image.size;
5776
5845
  return `<image href="${esc(href)}" x="${n(x)}" y="${n(y)}" width="${n(w)}" height="${n(h)}" />`;
5777
5846
  }
5778
- function emitText(text, rasterScale) {
5847
+ function emitText(text, rasterScale, resourceOptions) {
5779
5848
  if (!text.text) return "";
5780
5849
  const { x, y } = text.position;
5781
5850
  const { w, h } = text.size;
5782
5851
  if (typeof document === "undefined") return "";
5852
+ const width = Math.max(1, Math.ceil(w * rasterScale));
5853
+ const height = Math.max(1, Math.ceil(h * rasterScale));
5854
+ assertExportSize(width, height, resourceOptions);
5783
5855
  const canvas = document.createElement("canvas");
5784
- canvas.width = Math.max(1, Math.ceil(w * rasterScale));
5785
- canvas.height = Math.max(1, Math.ceil(h * rasterScale));
5856
+ canvas.width = width;
5857
+ canvas.height = height;
5786
5858
  const ctx = canvas.getContext("2d");
5787
5859
  if (!ctx) return "";
5788
5860
  ctx.scale(rasterScale, rasterScale);
@@ -5797,13 +5869,16 @@ function emitText(text, rasterScale) {
5797
5869
  if (!dataUri || !dataUri.startsWith("data:")) return "";
5798
5870
  return `<image href="${esc(dataUri)}" x="${n(x)}" y="${n(y)}" width="${n(w)}" height="${n(h)}" />`;
5799
5871
  }
5800
- function emitNote(note, rasterScale) {
5872
+ function emitNote(note, rasterScale, resourceOptions) {
5801
5873
  const { x, y } = note.position;
5802
5874
  const { w, h } = note.size;
5803
5875
  if (typeof document === "undefined") return emitNotePlaceholder(note);
5876
+ const width = Math.max(1, Math.ceil(w * rasterScale));
5877
+ const height = Math.max(1, Math.ceil(h * rasterScale));
5878
+ assertExportSize(width, height, resourceOptions);
5804
5879
  const canvas = document.createElement("canvas");
5805
- canvas.width = Math.max(1, Math.ceil(w * rasterScale));
5806
- canvas.height = Math.max(1, Math.ceil(h * rasterScale));
5880
+ canvas.width = width;
5881
+ canvas.height = height;
5807
5882
  const ctx = canvas.getContext("2d");
5808
5883
  if (!ctx) return emitNotePlaceholder(note);
5809
5884
  ctx.scale(rasterScale, rasterScale);
@@ -5964,8 +6039,9 @@ function emitHexTemplate(t, grid) {
5964
6039
  return `<path d="${d}" fill="${esc(t.fillColor)}" stroke="${esc(t.strokeColor)}" stroke-width="${n(t.strokeWidth)}" opacity="${n(t.opacity)}" />`;
5965
6040
  }
5966
6041
  async function exportSvg(store, options = {}, layerManager) {
5967
- const padding = options.padding ?? 0;
5968
- const rasterScale = options.rasterScale ?? 2;
6042
+ const padding = nonNegativeOption(options.padding, 0, "padding");
6043
+ const rasterScale = positiveOption(options.rasterScale, 2, "rasterScale");
6044
+ validateExportResourceOptions(options);
5969
6045
  const filter = options.filter;
5970
6046
  const allElements = store.getAll();
5971
6047
  let visibleElements = layerManager ? allElements.filter((el) => layerManager.isLayerVisible(el.layerId)) : allElements;
@@ -5974,11 +6050,12 @@ async function exportSvg(store, options = {}, layerManager) {
5974
6050
  if (!bounds) {
5975
6051
  return `<svg xmlns="http://www.w3.org/2000/svg" width="0" height="0" viewBox="0 0 0 0"></svg>`;
5976
6052
  }
6053
+ assertExportSize(Math.ceil(bounds.w), Math.ceil(bounds.h), options);
5977
6054
  const remoteImages = visibleElements.filter(
5978
6055
  (el) => el.type === "image" && !el.src.startsWith("data:")
5979
6056
  );
5980
- const imageCache = await loadImages(remoteImages);
5981
- const imageDataUris = encodeImages(visibleElements, imageCache, rasterScale);
6057
+ const imageCache = await loadImages(remoteImages, options);
6058
+ const imageDataUris = encodeImages(visibleElements, imageCache, rasterScale, options);
5982
6059
  const grids = visibleElements.filter((el) => el.type === "grid");
5983
6060
  const firstGrid = grids[0];
5984
6061
  let body = "";
@@ -5987,7 +6064,7 @@ async function exportSvg(store, options = {}, layerManager) {
5987
6064
  }
5988
6065
  const layerBodies = /* @__PURE__ */ new Map();
5989
6066
  for (const el of visibleElements) {
5990
- const emitted = emitElement(el, imageDataUris, rasterScale, firstGrid, store);
6067
+ const emitted = emitElement(el, imageDataUris, rasterScale, firstGrid, store, options);
5991
6068
  layerBodies.set(el.layerId, (layerBodies.get(el.layerId) ?? "") + emitted);
5992
6069
  }
5993
6070
  for (const [layerId, emitted] of layerBodies) {
@@ -6001,7 +6078,7 @@ async function exportSvg(store, options = {}, layerManager) {
6001
6078
  }
6002
6079
  return `<svg xmlns="http://www.w3.org/2000/svg" width="${n(bounds.w)}" height="${n(bounds.h)}" viewBox="${n(bounds.x)} ${n(bounds.y)} ${n(bounds.w)} ${n(bounds.h)}">${body}</svg>`;
6003
6080
  }
6004
- function emitElement(el, imageDataUris, rasterScale, firstGrid, store) {
6081
+ function emitElement(el, imageDataUris, rasterScale, firstGrid, store, resourceOptions) {
6005
6082
  switch (el.type) {
6006
6083
  case "stroke":
6007
6084
  return withRotationSvg(el, emitStroke(el));
@@ -6012,9 +6089,9 @@ function emitElement(el, imageDataUris, rasterScale, firstGrid, store) {
6012
6089
  case "image":
6013
6090
  return withRotationSvg(el, emitImage(el, imageDataUris.get(el.id)));
6014
6091
  case "text":
6015
- return withRotationSvg(el, emitText(el, rasterScale));
6092
+ return withRotationSvg(el, emitText(el, rasterScale, resourceOptions));
6016
6093
  case "note":
6017
- return withRotationSvg(el, emitNote(el, rasterScale));
6094
+ return withRotationSvg(el, emitNote(el, rasterScale, resourceOptions));
6018
6095
  case "template":
6019
6096
  return emitTemplate(el, firstGrid);
6020
6097
  case "grid":
@@ -6025,7 +6102,7 @@ function emitElement(el, imageDataUris, rasterScale, firstGrid, store) {
6025
6102
  return "";
6026
6103
  }
6027
6104
  }
6028
- function encodeImages(elements, imageCache, rasterScale) {
6105
+ function encodeImages(elements, imageCache, rasterScale, resourceOptions) {
6029
6106
  const out = /* @__PURE__ */ new Map();
6030
6107
  for (const el of elements) {
6031
6108
  if (el.type !== "image") continue;
@@ -6035,16 +6112,24 @@ function encodeImages(elements, imageCache, rasterScale) {
6035
6112
  }
6036
6113
  const img = imageCache.get(el.id);
6037
6114
  if (!img || typeof document === "undefined") continue;
6115
+ const width = Math.max(1, Math.ceil(el.size.w * rasterScale));
6116
+ const height = Math.max(1, Math.ceil(el.size.h * rasterScale));
6117
+ assertExportSize(width, height, resourceOptions);
6038
6118
  const canvas = document.createElement("canvas");
6039
- canvas.width = Math.max(1, Math.ceil(el.size.w * rasterScale));
6040
- canvas.height = Math.max(1, Math.ceil(el.size.h * rasterScale));
6119
+ canvas.width = width;
6120
+ canvas.height = height;
6041
6121
  const ctx = canvas.getContext("2d");
6042
- if (!ctx) continue;
6122
+ if (!ctx) {
6123
+ resourceOptions.onAssetError?.({ elementId: el.id, src: el.src, reason: "encode" });
6124
+ continue;
6125
+ }
6043
6126
  try {
6044
6127
  ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
6045
6128
  const uri = canvas.toDataURL();
6046
6129
  if (uri.startsWith("data:")) out.set(el.id, uri);
6047
- } catch {
6130
+ else resourceOptions.onAssetError?.({ elementId: el.id, src: el.src, reason: "encode" });
6131
+ } catch (cause) {
6132
+ resourceOptions.onAssetError?.({ elementId: el.id, src: el.src, reason: "encode", cause });
6048
6133
  }
6049
6134
  }
6050
6135
  return out;
@@ -6593,7 +6678,7 @@ var RenderLoop = class {
6593
6678
  layerGroups = /* @__PURE__ */ new Map();
6594
6679
  gridCacheCanvas = null;
6595
6680
  gridCacheCtx = null;
6596
- lastGridRef = null;
6681
+ lastGridRefs = [];
6597
6682
  constructor(deps) {
6598
6683
  this.canvasEl = deps.canvasEl;
6599
6684
  this.camera = deps.camera;
@@ -6793,8 +6878,8 @@ var RenderLoop = class {
6793
6878
  }
6794
6879
  if (gridElements.length > 0) {
6795
6880
  const gridT0 = performance.now();
6796
- const gridRef = gridElements[0];
6797
- const gridDirty = this.gridCacheDirty || gridRef !== this.lastGridRef;
6881
+ const gridsChanged = gridElements.length !== this.lastGridRefs.length || gridElements.some((grid, index) => grid !== this.lastGridRefs[index]);
6882
+ const gridDirty = this.gridCacheDirty || gridsChanged;
6798
6883
  if (gridDirty) {
6799
6884
  this.ensureGridCache();
6800
6885
  if (this.gridCacheCtx && this.gridCacheCanvas) {
@@ -6822,7 +6907,7 @@ var RenderLoop = class {
6822
6907
  }
6823
6908
  }
6824
6909
  this.gridCacheDirty = false;
6825
- this.lastGridRef = gridRef;
6910
+ this.lastGridRefs = [...gridElements];
6826
6911
  }
6827
6912
  if (this.gridCacheCanvas) {
6828
6913
  const offset = this.marginViewport.compositeOffset(
@@ -6842,6 +6927,8 @@ var RenderLoop = class {
6842
6927
  }
6843
6928
  }
6844
6929
  gridMs = performance.now() - gridT0;
6930
+ } else {
6931
+ this.lastGridRefs = [];
6845
6932
  }
6846
6933
  const overlayT0 = performance.now();
6847
6934
  const activeTool = this.toolManager.activeTool;
@@ -10815,7 +10902,7 @@ var LaserTool = class {
10815
10902
  };
10816
10903
 
10817
10904
  // src/index.ts
10818
- var VERSION = "0.50.8";
10905
+ var VERSION = "0.51.0";
10819
10906
  // Annotate the CommonJS export names for ESM import in node:
10820
10907
  0 && (module.exports = {
10821
10908
  ArrowTool,