@fieldnotes/core 0.51.0 → 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/README.md CHANGED
@@ -164,9 +164,30 @@ const blob = await viewport.exportImage({
164
164
  });
165
165
  ```
166
166
 
167
- HTML elements are currently excluded from image exports. Remote images are requested with anonymous
168
- CORS using their original URLs; failures are omitted from the result and reported through
169
- `onAssetError` when supplied.
167
+ Remote images are requested with anonymous CORS using their original URLs; failures are omitted from
168
+ the result and reported through `onAssetError` when supplied.
169
+
170
+ Application-owned HTML embeds require an explicit rasterization hook. Return a ready canvas-compatible
171
+ image source; Field Notes applies the element's size, rotation, paint order, and layer opacity in both
172
+ PNG and SVG exports:
173
+
174
+ ```typescript
175
+ const options = {
176
+ htmlTimeoutMs: 10_000,
177
+ renderHtml: async (element) => {
178
+ const node = document.querySelector(`[data-element-id="${element.id}"]`);
179
+ return node ? rasterizeToCanvas(node) : null; // application or library implementation
180
+ },
181
+ onHtmlError: ({ elementId, reason }) => {
182
+ console.warn(`Could not export HTML element ${elementId}: ${reason}`);
183
+ },
184
+ };
185
+
186
+ const png = await viewport.exportImage(options);
187
+ const svg = await viewport.exportSVG(options);
188
+ ```
189
+
190
+ Without `renderHtml`, embeds remain omitted and can be observed through `onHtmlError`.
170
191
 
171
192
  ## Performance Monitoring
172
193
 
package/dist/index.cjs CHANGED
@@ -5442,6 +5442,67 @@ 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
5446
5507
  var DEFAULT_IMAGE_TIMEOUT_MS = 1e4;
5447
5508
  var DEFAULT_MAX_DIMENSION = 16384;
@@ -5646,6 +5707,7 @@ async function exportImage(store, options = {}, layerManager) {
5646
5707
  const scale = positiveOption(options.scale, 2, "scale");
5647
5708
  const padding = nonNegativeOption(options.padding, 0, "padding");
5648
5709
  validateExportResourceOptions(options);
5710
+ validateHtmlExportOptions(options);
5649
5711
  const background = options.background ?? "#ffffff";
5650
5712
  const filter = options.filter;
5651
5713
  const allElements = store.getAll();
@@ -5659,6 +5721,8 @@ async function exportImage(store, options = {}, layerManager) {
5659
5721
  const height = Math.ceil(bounds.h * scale);
5660
5722
  assertExportSize(width, height, options);
5661
5723
  const imageCache = await loadImages(visibleElements, options);
5724
+ const htmlElements = visibleElements.filter((el) => el.type === "html");
5725
+ const htmlSources = await renderHtmlElements(htmlElements, options);
5662
5726
  const canvas = document.createElement("canvas");
5663
5727
  canvas.width = width;
5664
5728
  canvas.height = height;
@@ -5683,6 +5747,24 @@ async function exportImage(store, options = {}, layerManager) {
5683
5747
  return;
5684
5748
  }
5685
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
+ }
5686
5768
  return;
5687
5769
  }
5688
5770
  if (el.type === "image") {
@@ -5838,7 +5920,7 @@ function emitArrow(arrow, store) {
5838
5920
  return out;
5839
5921
  }
5840
5922
  function emitImage(image, dataUri) {
5841
- const href = dataUri ?? image.src;
5923
+ const href = dataUri ?? ("src" in image ? image.src : "");
5842
5924
  if (!href) return "";
5843
5925
  const { x, y } = image.position;
5844
5926
  const { w, h } = image.size;
@@ -6042,6 +6124,7 @@ async function exportSvg(store, options = {}, layerManager) {
6042
6124
  const padding = nonNegativeOption(options.padding, 0, "padding");
6043
6125
  const rasterScale = positiveOption(options.rasterScale, 2, "rasterScale");
6044
6126
  validateExportResourceOptions(options);
6127
+ validateHtmlExportOptions(options);
6045
6128
  const filter = options.filter;
6046
6129
  const allElements = store.getAll();
6047
6130
  let visibleElements = layerManager ? allElements.filter((el) => layerManager.isLayerVisible(el.layerId)) : allElements;
@@ -6056,6 +6139,9 @@ async function exportSvg(store, options = {}, layerManager) {
6056
6139
  );
6057
6140
  const imageCache = await loadImages(remoteImages, options);
6058
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);
6059
6145
  const grids = visibleElements.filter((el) => el.type === "grid");
6060
6146
  const firstGrid = grids[0];
6061
6147
  let body = "";
@@ -6064,7 +6150,15 @@ async function exportSvg(store, options = {}, layerManager) {
6064
6150
  }
6065
6151
  const layerBodies = /* @__PURE__ */ new Map();
6066
6152
  for (const el of visibleElements) {
6067
- const emitted = emitElement(el, imageDataUris, rasterScale, firstGrid, store, options);
6153
+ const emitted = emitElement(
6154
+ el,
6155
+ imageDataUris,
6156
+ htmlDataUris,
6157
+ rasterScale,
6158
+ firstGrid,
6159
+ store,
6160
+ options
6161
+ );
6068
6162
  layerBodies.set(el.layerId, (layerBodies.get(el.layerId) ?? "") + emitted);
6069
6163
  }
6070
6164
  for (const [layerId, emitted] of layerBodies) {
@@ -6078,7 +6172,7 @@ async function exportSvg(store, options = {}, layerManager) {
6078
6172
  }
6079
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>`;
6080
6174
  }
6081
- function emitElement(el, imageDataUris, rasterScale, firstGrid, store, resourceOptions) {
6175
+ function emitElement(el, imageDataUris, htmlDataUris, rasterScale, firstGrid, store, resourceOptions) {
6082
6176
  switch (el.type) {
6083
6177
  case "stroke":
6084
6178
  return withRotationSvg(el, emitStroke(el));
@@ -6097,11 +6191,62 @@ function emitElement(el, imageDataUris, rasterScale, firstGrid, store, resourceO
6097
6191
  case "grid":
6098
6192
  return "";
6099
6193
  case "html":
6100
- return "";
6194
+ return withRotationSvg(el, emitImage(el, htmlDataUris.get(el.id)));
6101
6195
  default:
6102
6196
  return "";
6103
6197
  }
6104
6198
  }
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
+ }
6105
6250
  function encodeImages(elements, imageCache, rasterScale, resourceOptions) {
6106
6251
  const out = /* @__PURE__ */ new Map();
6107
6252
  for (const el of elements) {
@@ -10902,7 +11047,7 @@ var LaserTool = class {
10902
11047
  };
10903
11048
 
10904
11049
  // src/index.ts
10905
- var VERSION = "0.51.0";
11050
+ var VERSION = "0.52.0";
10906
11051
  // Annotate the CommonJS export names for ESM import in node:
10907
11052
  0 && (module.exports = {
10908
11053
  ArrowTool,