@fieldnotes/core 0.50.8 → 0.52.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
@@ -5442,7 +5442,71 @@ function renderTextOnCanvas(ctx, text) {
5442
5442
  ctx.restore();
5443
5443
  }
5444
5444
 
5445
+ // src/canvas/html-export.ts
5446
+ var DEFAULT_HTML_TIMEOUT_MS = 1e4;
5447
+ var TIMEOUT = /* @__PURE__ */ Symbol("html-export-timeout");
5448
+ function validateHtmlExportOptions(options) {
5449
+ const timeoutMs = options.htmlTimeoutMs ?? DEFAULT_HTML_TIMEOUT_MS;
5450
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
5451
+ throw new RangeError("htmlTimeoutMs must be a finite number greater than 0");
5452
+ }
5453
+ return timeoutMs;
5454
+ }
5455
+ async function renderHtmlElements(elements, options) {
5456
+ const timeoutMs = validateHtmlExportOptions(options);
5457
+ const sources = /* @__PURE__ */ new Map();
5458
+ await Promise.all(
5459
+ elements.map(async (element) => {
5460
+ if (!options.renderHtml) {
5461
+ options.onHtmlError?.({
5462
+ elementId: element.id,
5463
+ htmlType: element.htmlType,
5464
+ reason: "unsupported"
5465
+ });
5466
+ return;
5467
+ }
5468
+ let timer;
5469
+ try {
5470
+ const timeout = new Promise((resolve) => {
5471
+ timer = setTimeout(() => resolve(TIMEOUT), timeoutMs);
5472
+ });
5473
+ const result = await Promise.race([Promise.resolve(options.renderHtml(element)), timeout]);
5474
+ if (result === TIMEOUT) {
5475
+ options.onHtmlError?.({
5476
+ elementId: element.id,
5477
+ htmlType: element.htmlType,
5478
+ reason: "timeout"
5479
+ });
5480
+ return;
5481
+ }
5482
+ if (!result) {
5483
+ options.onHtmlError?.({
5484
+ elementId: element.id,
5485
+ htmlType: element.htmlType,
5486
+ reason: "render"
5487
+ });
5488
+ return;
5489
+ }
5490
+ sources.set(element.id, result);
5491
+ } catch (cause) {
5492
+ options.onHtmlError?.({
5493
+ elementId: element.id,
5494
+ htmlType: element.htmlType,
5495
+ reason: "render",
5496
+ cause
5497
+ });
5498
+ } finally {
5499
+ if (timer !== void 0) clearTimeout(timer);
5500
+ }
5501
+ })
5502
+ );
5503
+ return sources;
5504
+ }
5505
+
5445
5506
  // src/canvas/export-image.ts
5507
+ var DEFAULT_IMAGE_TIMEOUT_MS = 1e4;
5508
+ var DEFAULT_MAX_DIMENSION = 16384;
5509
+ var DEFAULT_MAX_PIXELS = 67108864;
5446
5510
  var center = (b) => ({ x: b.x + b.w / 2, y: b.y + b.h / 2 });
5447
5511
  function getStrokeBounds(el) {
5448
5512
  if (el.type !== "stroke") return null;
@@ -5552,12 +5616,53 @@ function renderGridForBounds(ctx, grid, bounds) {
5552
5616
  );
5553
5617
  }
5554
5618
  }
5555
- function loadImages(elements) {
5619
+ function positiveOption(value, fallback, name) {
5620
+ const resolved = value ?? fallback;
5621
+ if (!Number.isFinite(resolved) || resolved <= 0) {
5622
+ throw new RangeError(`${name} must be a finite number greater than 0`);
5623
+ }
5624
+ return resolved;
5625
+ }
5626
+ function nonNegativeOption(value, fallback, name) {
5627
+ const resolved = value ?? fallback;
5628
+ if (!Number.isFinite(resolved) || resolved < 0) {
5629
+ throw new RangeError(`${name} must be a finite number greater than or equal to 0`);
5630
+ }
5631
+ return resolved;
5632
+ }
5633
+ function assertExportSize(width, height, options) {
5634
+ const maxDimension = positiveOption(options.maxDimension, DEFAULT_MAX_DIMENSION, "maxDimension");
5635
+ const maxPixels = positiveOption(options.maxPixels, DEFAULT_MAX_PIXELS, "maxPixels");
5636
+ if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
5637
+ throw new RangeError("Export dimensions must be finite numbers greater than 0");
5638
+ }
5639
+ if (width > maxDimension || height > maxDimension) {
5640
+ throw new RangeError(
5641
+ `Export dimensions ${width}x${height} exceed the maximum dimension of ${maxDimension}`
5642
+ );
5643
+ }
5644
+ if (width * height > maxPixels) {
5645
+ throw new RangeError(
5646
+ `Export size ${width}x${height} exceeds the maximum of ${maxPixels} pixels`
5647
+ );
5648
+ }
5649
+ }
5650
+ function validateExportResourceOptions(options) {
5651
+ positiveOption(options.imageTimeoutMs, DEFAULT_IMAGE_TIMEOUT_MS, "imageTimeoutMs");
5652
+ positiveOption(options.maxDimension, DEFAULT_MAX_DIMENSION, "maxDimension");
5653
+ positiveOption(options.maxPixels, DEFAULT_MAX_PIXELS, "maxPixels");
5654
+ }
5655
+ function loadImages(elements, options = {}) {
5556
5656
  const imageElements = elements.filter(
5557
5657
  (el) => el.type === "image" && "src" in el
5558
5658
  );
5559
5659
  const cache3 = /* @__PURE__ */ new Map();
5560
5660
  if (imageElements.length === 0) return Promise.resolve(cache3);
5661
+ const timeoutMs = positiveOption(
5662
+ options.imageTimeoutMs,
5663
+ DEFAULT_IMAGE_TIMEOUT_MS,
5664
+ "imageTimeoutMs"
5665
+ );
5561
5666
  return new Promise((resolve) => {
5562
5667
  let remaining = imageElements.length;
5563
5668
  const done = () => {
@@ -5567,19 +5672,42 @@ function loadImages(elements) {
5567
5672
  for (const el of imageElements) {
5568
5673
  const img = new Image();
5569
5674
  img.crossOrigin = "anonymous";
5675
+ let settled = false;
5676
+ const timer = setTimeout(() => {
5677
+ if (settled) return;
5678
+ settled = true;
5679
+ img.onload = null;
5680
+ img.onerror = null;
5681
+ options.onAssetError?.({ elementId: el.id, src: el.src, reason: "timeout" });
5682
+ done();
5683
+ }, timeoutMs);
5684
+ const settle = () => {
5685
+ if (settled) return false;
5686
+ settled = true;
5687
+ clearTimeout(timer);
5688
+ img.onload = null;
5689
+ img.onerror = null;
5690
+ return true;
5691
+ };
5570
5692
  img.onload = () => {
5693
+ if (!settle()) return;
5571
5694
  cache3.set(el.id, img);
5572
5695
  done();
5573
5696
  };
5574
- img.onerror = done;
5575
- const sep = el.src.includes("?") ? "&" : "?";
5576
- img.src = `${el.src}${sep}_cors=1`;
5697
+ img.onerror = (cause) => {
5698
+ if (!settle()) return;
5699
+ options.onAssetError?.({ elementId: el.id, src: el.src, reason: "load", cause });
5700
+ done();
5701
+ };
5702
+ img.src = el.src;
5577
5703
  }
5578
5704
  });
5579
5705
  }
5580
5706
  async function exportImage(store, options = {}, layerManager) {
5581
- const scale = options.scale ?? 2;
5582
- const padding = options.padding ?? 0;
5707
+ const scale = positiveOption(options.scale, 2, "scale");
5708
+ const padding = nonNegativeOption(options.padding, 0, "padding");
5709
+ validateExportResourceOptions(options);
5710
+ validateHtmlExportOptions(options);
5583
5711
  const background = options.background ?? "#ffffff";
5584
5712
  const filter = options.filter;
5585
5713
  const allElements = store.getAll();
@@ -5589,10 +5717,15 @@ async function exportImage(store, options = {}, layerManager) {
5589
5717
  }
5590
5718
  const bounds = computeBounds(visibleElements, padding);
5591
5719
  if (!bounds) return null;
5592
- const imageCache = await loadImages(visibleElements);
5720
+ const width = Math.ceil(bounds.w * scale);
5721
+ const height = Math.ceil(bounds.h * scale);
5722
+ assertExportSize(width, height, options);
5723
+ const imageCache = await loadImages(visibleElements, options);
5724
+ const htmlElements = visibleElements.filter((el) => el.type === "html");
5725
+ const htmlSources = await renderHtmlElements(htmlElements, options);
5593
5726
  const canvas = document.createElement("canvas");
5594
- canvas.width = Math.ceil(bounds.w * scale);
5595
- canvas.height = Math.ceil(bounds.h * scale);
5727
+ canvas.width = width;
5728
+ canvas.height = height;
5596
5729
  const ctx = canvas.getContext("2d");
5597
5730
  if (!ctx) return null;
5598
5731
  ctx.scale(scale, scale);
@@ -5614,6 +5747,24 @@ async function exportImage(store, options = {}, layerManager) {
5614
5747
  return;
5615
5748
  }
5616
5749
  if (el.type === "html") {
5750
+ const source = htmlSources.get(el.id);
5751
+ if (!source) return;
5752
+ const b = getElementBounds(el);
5753
+ try {
5754
+ withRotation(
5755
+ target,
5756
+ el,
5757
+ b ? center(b) : el.position,
5758
+ () => target.drawImage(source, el.position.x, el.position.y, el.size.w, el.size.h)
5759
+ );
5760
+ } catch (cause) {
5761
+ options.onHtmlError?.({
5762
+ elementId: el.id,
5763
+ htmlType: el.htmlType,
5764
+ reason: "render",
5765
+ cause
5766
+ });
5767
+ }
5617
5768
  return;
5618
5769
  }
5619
5770
  if (el.type === "image") {
@@ -5769,20 +5920,23 @@ function emitArrow(arrow, store) {
5769
5920
  return out;
5770
5921
  }
5771
5922
  function emitImage(image, dataUri) {
5772
- const href = dataUri ?? image.src;
5923
+ const href = dataUri ?? ("src" in image ? image.src : "");
5773
5924
  if (!href) return "";
5774
5925
  const { x, y } = image.position;
5775
5926
  const { w, h } = image.size;
5776
5927
  return `<image href="${esc(href)}" x="${n(x)}" y="${n(y)}" width="${n(w)}" height="${n(h)}" />`;
5777
5928
  }
5778
- function emitText(text, rasterScale) {
5929
+ function emitText(text, rasterScale, resourceOptions) {
5779
5930
  if (!text.text) return "";
5780
5931
  const { x, y } = text.position;
5781
5932
  const { w, h } = text.size;
5782
5933
  if (typeof document === "undefined") return "";
5934
+ const width = Math.max(1, Math.ceil(w * rasterScale));
5935
+ const height = Math.max(1, Math.ceil(h * rasterScale));
5936
+ assertExportSize(width, height, resourceOptions);
5783
5937
  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));
5938
+ canvas.width = width;
5939
+ canvas.height = height;
5786
5940
  const ctx = canvas.getContext("2d");
5787
5941
  if (!ctx) return "";
5788
5942
  ctx.scale(rasterScale, rasterScale);
@@ -5797,13 +5951,16 @@ function emitText(text, rasterScale) {
5797
5951
  if (!dataUri || !dataUri.startsWith("data:")) return "";
5798
5952
  return `<image href="${esc(dataUri)}" x="${n(x)}" y="${n(y)}" width="${n(w)}" height="${n(h)}" />`;
5799
5953
  }
5800
- function emitNote(note, rasterScale) {
5954
+ function emitNote(note, rasterScale, resourceOptions) {
5801
5955
  const { x, y } = note.position;
5802
5956
  const { w, h } = note.size;
5803
5957
  if (typeof document === "undefined") return emitNotePlaceholder(note);
5958
+ const width = Math.max(1, Math.ceil(w * rasterScale));
5959
+ const height = Math.max(1, Math.ceil(h * rasterScale));
5960
+ assertExportSize(width, height, resourceOptions);
5804
5961
  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));
5962
+ canvas.width = width;
5963
+ canvas.height = height;
5807
5964
  const ctx = canvas.getContext("2d");
5808
5965
  if (!ctx) return emitNotePlaceholder(note);
5809
5966
  ctx.scale(rasterScale, rasterScale);
@@ -5964,8 +6121,10 @@ function emitHexTemplate(t, grid) {
5964
6121
  return `<path d="${d}" fill="${esc(t.fillColor)}" stroke="${esc(t.strokeColor)}" stroke-width="${n(t.strokeWidth)}" opacity="${n(t.opacity)}" />`;
5965
6122
  }
5966
6123
  async function exportSvg(store, options = {}, layerManager) {
5967
- const padding = options.padding ?? 0;
5968
- const rasterScale = options.rasterScale ?? 2;
6124
+ const padding = nonNegativeOption(options.padding, 0, "padding");
6125
+ const rasterScale = positiveOption(options.rasterScale, 2, "rasterScale");
6126
+ validateExportResourceOptions(options);
6127
+ validateHtmlExportOptions(options);
5969
6128
  const filter = options.filter;
5970
6129
  const allElements = store.getAll();
5971
6130
  let visibleElements = layerManager ? allElements.filter((el) => layerManager.isLayerVisible(el.layerId)) : allElements;
@@ -5974,11 +6133,15 @@ async function exportSvg(store, options = {}, layerManager) {
5974
6133
  if (!bounds) {
5975
6134
  return `<svg xmlns="http://www.w3.org/2000/svg" width="0" height="0" viewBox="0 0 0 0"></svg>`;
5976
6135
  }
6136
+ assertExportSize(Math.ceil(bounds.w), Math.ceil(bounds.h), options);
5977
6137
  const remoteImages = visibleElements.filter(
5978
6138
  (el) => el.type === "image" && !el.src.startsWith("data:")
5979
6139
  );
5980
- const imageCache = await loadImages(remoteImages);
5981
- const imageDataUris = encodeImages(visibleElements, imageCache, rasterScale);
6140
+ const imageCache = await loadImages(remoteImages, options);
6141
+ const imageDataUris = encodeImages(visibleElements, imageCache, rasterScale, options);
6142
+ const htmlElements = visibleElements.filter((el) => el.type === "html");
6143
+ const htmlSources = await renderHtmlElements(htmlElements, options);
6144
+ const htmlDataUris = encodeHtmlElements(htmlElements, htmlSources, rasterScale, options);
5982
6145
  const grids = visibleElements.filter((el) => el.type === "grid");
5983
6146
  const firstGrid = grids[0];
5984
6147
  let body = "";
@@ -5987,7 +6150,15 @@ async function exportSvg(store, options = {}, layerManager) {
5987
6150
  }
5988
6151
  const layerBodies = /* @__PURE__ */ new Map();
5989
6152
  for (const el of visibleElements) {
5990
- const emitted = emitElement(el, imageDataUris, rasterScale, firstGrid, store);
6153
+ const emitted = emitElement(
6154
+ el,
6155
+ imageDataUris,
6156
+ htmlDataUris,
6157
+ rasterScale,
6158
+ firstGrid,
6159
+ store,
6160
+ options
6161
+ );
5991
6162
  layerBodies.set(el.layerId, (layerBodies.get(el.layerId) ?? "") + emitted);
5992
6163
  }
5993
6164
  for (const [layerId, emitted] of layerBodies) {
@@ -6001,7 +6172,7 @@ async function exportSvg(store, options = {}, layerManager) {
6001
6172
  }
6002
6173
  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
6174
  }
6004
- function emitElement(el, imageDataUris, rasterScale, firstGrid, store) {
6175
+ function emitElement(el, imageDataUris, htmlDataUris, rasterScale, firstGrid, store, resourceOptions) {
6005
6176
  switch (el.type) {
6006
6177
  case "stroke":
6007
6178
  return withRotationSvg(el, emitStroke(el));
@@ -6012,20 +6183,71 @@ function emitElement(el, imageDataUris, rasterScale, firstGrid, store) {
6012
6183
  case "image":
6013
6184
  return withRotationSvg(el, emitImage(el, imageDataUris.get(el.id)));
6014
6185
  case "text":
6015
- return withRotationSvg(el, emitText(el, rasterScale));
6186
+ return withRotationSvg(el, emitText(el, rasterScale, resourceOptions));
6016
6187
  case "note":
6017
- return withRotationSvg(el, emitNote(el, rasterScale));
6188
+ return withRotationSvg(el, emitNote(el, rasterScale, resourceOptions));
6018
6189
  case "template":
6019
6190
  return emitTemplate(el, firstGrid);
6020
6191
  case "grid":
6021
6192
  return "";
6022
6193
  case "html":
6023
- return "";
6194
+ return withRotationSvg(el, emitImage(el, htmlDataUris.get(el.id)));
6024
6195
  default:
6025
6196
  return "";
6026
6197
  }
6027
6198
  }
6028
- function encodeImages(elements, imageCache, rasterScale) {
6199
+ function encodeHtmlElements(elements, sources, rasterScale, options) {
6200
+ const encoded = /* @__PURE__ */ new Map();
6201
+ for (const element of elements) {
6202
+ const source = sources.get(element.id);
6203
+ if (!source) continue;
6204
+ const width = Math.max(1, Math.ceil(element.size.w * rasterScale));
6205
+ const height = Math.max(1, Math.ceil(element.size.h * rasterScale));
6206
+ assertExportSize(width, height, options);
6207
+ if (typeof document === "undefined") {
6208
+ options.onHtmlError?.({
6209
+ elementId: element.id,
6210
+ htmlType: element.htmlType,
6211
+ reason: "encode"
6212
+ });
6213
+ continue;
6214
+ }
6215
+ const canvas = document.createElement("canvas");
6216
+ canvas.width = width;
6217
+ canvas.height = height;
6218
+ const ctx = canvas.getContext("2d");
6219
+ if (!ctx) {
6220
+ options.onHtmlError?.({
6221
+ elementId: element.id,
6222
+ htmlType: element.htmlType,
6223
+ reason: "encode"
6224
+ });
6225
+ continue;
6226
+ }
6227
+ try {
6228
+ ctx.drawImage(source, 0, 0, width, height);
6229
+ const dataUri = canvas.toDataURL();
6230
+ if (dataUri.startsWith("data:")) {
6231
+ encoded.set(element.id, dataUri);
6232
+ } else {
6233
+ options.onHtmlError?.({
6234
+ elementId: element.id,
6235
+ htmlType: element.htmlType,
6236
+ reason: "encode"
6237
+ });
6238
+ }
6239
+ } catch (cause) {
6240
+ options.onHtmlError?.({
6241
+ elementId: element.id,
6242
+ htmlType: element.htmlType,
6243
+ reason: "encode",
6244
+ cause
6245
+ });
6246
+ }
6247
+ }
6248
+ return encoded;
6249
+ }
6250
+ function encodeImages(elements, imageCache, rasterScale, resourceOptions) {
6029
6251
  const out = /* @__PURE__ */ new Map();
6030
6252
  for (const el of elements) {
6031
6253
  if (el.type !== "image") continue;
@@ -6035,16 +6257,24 @@ function encodeImages(elements, imageCache, rasterScale) {
6035
6257
  }
6036
6258
  const img = imageCache.get(el.id);
6037
6259
  if (!img || typeof document === "undefined") continue;
6260
+ const width = Math.max(1, Math.ceil(el.size.w * rasterScale));
6261
+ const height = Math.max(1, Math.ceil(el.size.h * rasterScale));
6262
+ assertExportSize(width, height, resourceOptions);
6038
6263
  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));
6264
+ canvas.width = width;
6265
+ canvas.height = height;
6041
6266
  const ctx = canvas.getContext("2d");
6042
- if (!ctx) continue;
6267
+ if (!ctx) {
6268
+ resourceOptions.onAssetError?.({ elementId: el.id, src: el.src, reason: "encode" });
6269
+ continue;
6270
+ }
6043
6271
  try {
6044
6272
  ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
6045
6273
  const uri = canvas.toDataURL();
6046
6274
  if (uri.startsWith("data:")) out.set(el.id, uri);
6047
- } catch {
6275
+ else resourceOptions.onAssetError?.({ elementId: el.id, src: el.src, reason: "encode" });
6276
+ } catch (cause) {
6277
+ resourceOptions.onAssetError?.({ elementId: el.id, src: el.src, reason: "encode", cause });
6048
6278
  }
6049
6279
  }
6050
6280
  return out;
@@ -6593,7 +6823,7 @@ var RenderLoop = class {
6593
6823
  layerGroups = /* @__PURE__ */ new Map();
6594
6824
  gridCacheCanvas = null;
6595
6825
  gridCacheCtx = null;
6596
- lastGridRef = null;
6826
+ lastGridRefs = [];
6597
6827
  constructor(deps) {
6598
6828
  this.canvasEl = deps.canvasEl;
6599
6829
  this.camera = deps.camera;
@@ -6793,8 +7023,8 @@ var RenderLoop = class {
6793
7023
  }
6794
7024
  if (gridElements.length > 0) {
6795
7025
  const gridT0 = performance.now();
6796
- const gridRef = gridElements[0];
6797
- const gridDirty = this.gridCacheDirty || gridRef !== this.lastGridRef;
7026
+ const gridsChanged = gridElements.length !== this.lastGridRefs.length || gridElements.some((grid, index) => grid !== this.lastGridRefs[index]);
7027
+ const gridDirty = this.gridCacheDirty || gridsChanged;
6798
7028
  if (gridDirty) {
6799
7029
  this.ensureGridCache();
6800
7030
  if (this.gridCacheCtx && this.gridCacheCanvas) {
@@ -6822,7 +7052,7 @@ var RenderLoop = class {
6822
7052
  }
6823
7053
  }
6824
7054
  this.gridCacheDirty = false;
6825
- this.lastGridRef = gridRef;
7055
+ this.lastGridRefs = [...gridElements];
6826
7056
  }
6827
7057
  if (this.gridCacheCanvas) {
6828
7058
  const offset = this.marginViewport.compositeOffset(
@@ -6842,6 +7072,8 @@ var RenderLoop = class {
6842
7072
  }
6843
7073
  }
6844
7074
  gridMs = performance.now() - gridT0;
7075
+ } else {
7076
+ this.lastGridRefs = [];
6845
7077
  }
6846
7078
  const overlayT0 = performance.now();
6847
7079
  const activeTool = this.toolManager.activeTool;
@@ -10815,7 +11047,7 @@ var LaserTool = class {
10815
11047
  };
10816
11048
 
10817
11049
  // src/index.ts
10818
- var VERSION = "0.50.8";
11050
+ var VERSION = "0.52.0";
10819
11051
  // Annotate the CommonJS export names for ESM import in node:
10820
11052
  0 && (module.exports = {
10821
11053
  ArrowTool,