@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.d.cts CHANGED
@@ -447,15 +447,49 @@ declare class HistoryStack {
447
447
  private notifyChange;
448
448
  }
449
449
 
450
- interface ExportImageOptions {
450
+ type HtmlExportRenderer = (element: HtmlElement) => CanvasImageSource | null | Promise<CanvasImageSource | null>;
451
+ type HtmlExportErrorReason = 'unsupported' | 'timeout' | 'render' | 'encode';
452
+ interface HtmlExportError {
453
+ elementId: string;
454
+ htmlType?: string;
455
+ reason: HtmlExportErrorReason;
456
+ cause?: unknown;
457
+ }
458
+ interface HtmlExportOptions {
459
+ /** Converts an application-owned HTML embed into a ready canvas image source. */
460
+ renderHtml?: HtmlExportRenderer;
461
+ /** Maximum wait for each HTML renderer. Defaults to 10 seconds. */
462
+ htmlTimeoutMs?: number;
463
+ /** Called when an HTML embed cannot be rendered or encoded. The export continues. */
464
+ onHtmlError?: (error: HtmlExportError) => void;
465
+ }
466
+
467
+ interface ExportImageOptions extends ExportResourceOptions, HtmlExportOptions {
451
468
  scale?: number;
452
469
  padding?: number;
453
470
  background?: string;
454
471
  filter?: (element: CanvasElement) => boolean;
455
472
  }
473
+ type ExportAssetErrorReason = 'load' | 'timeout' | 'encode';
474
+ interface ExportAssetError {
475
+ elementId: string;
476
+ src: string;
477
+ reason: ExportAssetErrorReason;
478
+ cause?: unknown;
479
+ }
480
+ interface ExportResourceOptions {
481
+ /** Maximum wait for each image asset. Defaults to 10 seconds. */
482
+ imageTimeoutMs?: number;
483
+ /** Maximum width or height of any allocated export canvas. Defaults to 16,384. */
484
+ maxDimension?: number;
485
+ /** Maximum pixel count of any allocated export canvas. Defaults to 67,108,864. */
486
+ maxPixels?: number;
487
+ /** Called when an image cannot be loaded or embedded. The export continues. */
488
+ onAssetError?: (error: ExportAssetError) => void;
489
+ }
456
490
  declare function exportImage(store: ElementStore, options?: ExportImageOptions, layerManager?: LayerManager): Promise<Blob | null>;
457
491
 
458
- interface ExportSvgOptions {
492
+ interface ExportSvgOptions extends ExportResourceOptions, HtmlExportOptions {
459
493
  padding?: number;
460
494
  background?: string;
461
495
  filter?: (el: CanvasElement) => boolean;
@@ -1123,6 +1157,6 @@ declare class LaserTool implements Tool {
1123
1157
  private notifyOptionsChange;
1124
1158
  }
1125
1159
 
1126
- declare const VERSION = "0.50.8";
1160
+ declare const VERSION = "0.52.0";
1127
1161
 
1128
- export { type ActiveFormats, type AlignEdge, type ArrowElement, type ArrowStrokeStyle, ArrowTool, type ArrowToolOptions, AutoSave, type AutoSaveOptions, type BackgroundOptions, type BackgroundPattern, type Binding, type Bounds, Camera, type CameraChangeInfo, type CameraOptions, type CanvasElement, type CanvasState, type Command, DEFAULT_NOTE_FONT_SIZE, type DistributeAxis, type ElementChangeMeta, ElementStore, type ElementStyle, type ElementType, type ElementUpdateEvent, EraserTool, type EraserToolOptions, type ExportImageOptions, type ExportSvgOptions, type FontSizePreset, type GridElement, type GridInfo, HandTool, type HexOrientation, HistoryStack, type HistoryStackOptions, type HtmlElement, type ImageElement, ImageTool, type ImageToolOptions, IndexedDBAdapter, type IndexedDBAdapterOptions, LaserTool, type LaserToolOptions, type Layer, LayerManager, LocalStorageAdapter, MeasureTool, type MeasureToolOptions, type Measurement, MemoryAdapter, type NoteElement, NoteTool, type NoteToolOptions, PencilTool, type PencilToolOptions, type Point, type PointerState, type RenderStatsSnapshot, type RotateDirection, SelectTool, type ShapeElement, type ShapeKind, ShapeTool, type ShapeToolOptions, type ShortcutBindings, type ShortcutOptions, type ShortcutsApi, type Size, type StorageAdapter, type StrokeElement, type StrokePoint, type TemplateElement, type TemplateRenderStyle, type TemplateShape, TemplateTool, type TemplateToolOptions, type TextElement, TextTool, type TextToolOptions, type Tool, type ToolContext, ToolManager, type ToolName, VERSION, Viewport, type ViewportOptions, boundsIntersect, createArrow, createGrid, createHtmlElement, createImage, createNote, createShape, createStroke, createTemplate, createText, drawHexPath, exportImage, exportSvg, getActiveFormats, getArrowBounds, getArrowControlPoint, getArrowMidpoint, getArrowTangentAngle, getBendFromPoint, getElementBounds, getElementStyle, getElementsBoundingBox, getHexCellsInCone, getHexCellsInLine, getHexCellsInRadius, getHexCellsInRectangle, getHexCellsInSquare, getHexDistance, isNearBezier, setFontSize, smartSnap, snapPoint, snapToHexCenter, styleToPatch, toggleBold, toggleItalic, toggleStrikethrough, toggleUnderline };
1162
+ export { type ActiveFormats, type AlignEdge, type ArrowElement, type ArrowStrokeStyle, ArrowTool, type ArrowToolOptions, AutoSave, type AutoSaveOptions, type BackgroundOptions, type BackgroundPattern, type Binding, type Bounds, Camera, type CameraChangeInfo, type CameraOptions, type CanvasElement, type CanvasState, type Command, DEFAULT_NOTE_FONT_SIZE, type DistributeAxis, type ElementChangeMeta, ElementStore, type ElementStyle, type ElementType, type ElementUpdateEvent, EraserTool, type EraserToolOptions, type ExportAssetError, type ExportAssetErrorReason, type ExportImageOptions, type ExportResourceOptions, type ExportSvgOptions, type FontSizePreset, type GridElement, type GridInfo, HandTool, type HexOrientation, HistoryStack, type HistoryStackOptions, type HtmlElement, type HtmlExportError, type HtmlExportErrorReason, type HtmlExportOptions, type HtmlExportRenderer, type ImageElement, ImageTool, type ImageToolOptions, IndexedDBAdapter, type IndexedDBAdapterOptions, LaserTool, type LaserToolOptions, type Layer, LayerManager, LocalStorageAdapter, MeasureTool, type MeasureToolOptions, type Measurement, MemoryAdapter, type NoteElement, NoteTool, type NoteToolOptions, PencilTool, type PencilToolOptions, type Point, type PointerState, type RenderStatsSnapshot, type RotateDirection, SelectTool, type ShapeElement, type ShapeKind, ShapeTool, type ShapeToolOptions, type ShortcutBindings, type ShortcutOptions, type ShortcutsApi, type Size, type StorageAdapter, type StrokeElement, type StrokePoint, type TemplateElement, type TemplateRenderStyle, type TemplateShape, TemplateTool, type TemplateToolOptions, type TextElement, TextTool, type TextToolOptions, type Tool, type ToolContext, ToolManager, type ToolName, VERSION, Viewport, type ViewportOptions, boundsIntersect, createArrow, createGrid, createHtmlElement, createImage, createNote, createShape, createStroke, createTemplate, createText, drawHexPath, exportImage, exportSvg, getActiveFormats, getArrowBounds, getArrowControlPoint, getArrowMidpoint, getArrowTangentAngle, getBendFromPoint, getElementBounds, getElementStyle, getElementsBoundingBox, getHexCellsInCone, getHexCellsInLine, getHexCellsInRadius, getHexCellsInRectangle, getHexCellsInSquare, getHexDistance, isNearBezier, setFontSize, smartSnap, snapPoint, snapToHexCenter, styleToPatch, toggleBold, toggleItalic, toggleStrikethrough, toggleUnderline };
package/dist/index.d.ts CHANGED
@@ -447,15 +447,49 @@ declare class HistoryStack {
447
447
  private notifyChange;
448
448
  }
449
449
 
450
- interface ExportImageOptions {
450
+ type HtmlExportRenderer = (element: HtmlElement) => CanvasImageSource | null | Promise<CanvasImageSource | null>;
451
+ type HtmlExportErrorReason = 'unsupported' | 'timeout' | 'render' | 'encode';
452
+ interface HtmlExportError {
453
+ elementId: string;
454
+ htmlType?: string;
455
+ reason: HtmlExportErrorReason;
456
+ cause?: unknown;
457
+ }
458
+ interface HtmlExportOptions {
459
+ /** Converts an application-owned HTML embed into a ready canvas image source. */
460
+ renderHtml?: HtmlExportRenderer;
461
+ /** Maximum wait for each HTML renderer. Defaults to 10 seconds. */
462
+ htmlTimeoutMs?: number;
463
+ /** Called when an HTML embed cannot be rendered or encoded. The export continues. */
464
+ onHtmlError?: (error: HtmlExportError) => void;
465
+ }
466
+
467
+ interface ExportImageOptions extends ExportResourceOptions, HtmlExportOptions {
451
468
  scale?: number;
452
469
  padding?: number;
453
470
  background?: string;
454
471
  filter?: (element: CanvasElement) => boolean;
455
472
  }
473
+ type ExportAssetErrorReason = 'load' | 'timeout' | 'encode';
474
+ interface ExportAssetError {
475
+ elementId: string;
476
+ src: string;
477
+ reason: ExportAssetErrorReason;
478
+ cause?: unknown;
479
+ }
480
+ interface ExportResourceOptions {
481
+ /** Maximum wait for each image asset. Defaults to 10 seconds. */
482
+ imageTimeoutMs?: number;
483
+ /** Maximum width or height of any allocated export canvas. Defaults to 16,384. */
484
+ maxDimension?: number;
485
+ /** Maximum pixel count of any allocated export canvas. Defaults to 67,108,864. */
486
+ maxPixels?: number;
487
+ /** Called when an image cannot be loaded or embedded. The export continues. */
488
+ onAssetError?: (error: ExportAssetError) => void;
489
+ }
456
490
  declare function exportImage(store: ElementStore, options?: ExportImageOptions, layerManager?: LayerManager): Promise<Blob | null>;
457
491
 
458
- interface ExportSvgOptions {
492
+ interface ExportSvgOptions extends ExportResourceOptions, HtmlExportOptions {
459
493
  padding?: number;
460
494
  background?: string;
461
495
  filter?: (el: CanvasElement) => boolean;
@@ -1123,6 +1157,6 @@ declare class LaserTool implements Tool {
1123
1157
  private notifyOptionsChange;
1124
1158
  }
1125
1159
 
1126
- declare const VERSION = "0.50.8";
1160
+ declare const VERSION = "0.52.0";
1127
1161
 
1128
- export { type ActiveFormats, type AlignEdge, type ArrowElement, type ArrowStrokeStyle, ArrowTool, type ArrowToolOptions, AutoSave, type AutoSaveOptions, type BackgroundOptions, type BackgroundPattern, type Binding, type Bounds, Camera, type CameraChangeInfo, type CameraOptions, type CanvasElement, type CanvasState, type Command, DEFAULT_NOTE_FONT_SIZE, type DistributeAxis, type ElementChangeMeta, ElementStore, type ElementStyle, type ElementType, type ElementUpdateEvent, EraserTool, type EraserToolOptions, type ExportImageOptions, type ExportSvgOptions, type FontSizePreset, type GridElement, type GridInfo, HandTool, type HexOrientation, HistoryStack, type HistoryStackOptions, type HtmlElement, type ImageElement, ImageTool, type ImageToolOptions, IndexedDBAdapter, type IndexedDBAdapterOptions, LaserTool, type LaserToolOptions, type Layer, LayerManager, LocalStorageAdapter, MeasureTool, type MeasureToolOptions, type Measurement, MemoryAdapter, type NoteElement, NoteTool, type NoteToolOptions, PencilTool, type PencilToolOptions, type Point, type PointerState, type RenderStatsSnapshot, type RotateDirection, SelectTool, type ShapeElement, type ShapeKind, ShapeTool, type ShapeToolOptions, type ShortcutBindings, type ShortcutOptions, type ShortcutsApi, type Size, type StorageAdapter, type StrokeElement, type StrokePoint, type TemplateElement, type TemplateRenderStyle, type TemplateShape, TemplateTool, type TemplateToolOptions, type TextElement, TextTool, type TextToolOptions, type Tool, type ToolContext, ToolManager, type ToolName, VERSION, Viewport, type ViewportOptions, boundsIntersect, createArrow, createGrid, createHtmlElement, createImage, createNote, createShape, createStroke, createTemplate, createText, drawHexPath, exportImage, exportSvg, getActiveFormats, getArrowBounds, getArrowControlPoint, getArrowMidpoint, getArrowTangentAngle, getBendFromPoint, getElementBounds, getElementStyle, getElementsBoundingBox, getHexCellsInCone, getHexCellsInLine, getHexCellsInRadius, getHexCellsInRectangle, getHexCellsInSquare, getHexDistance, isNearBezier, setFontSize, smartSnap, snapPoint, snapToHexCenter, styleToPatch, toggleBold, toggleItalic, toggleStrikethrough, toggleUnderline };
1162
+ export { type ActiveFormats, type AlignEdge, type ArrowElement, type ArrowStrokeStyle, ArrowTool, type ArrowToolOptions, AutoSave, type AutoSaveOptions, type BackgroundOptions, type BackgroundPattern, type Binding, type Bounds, Camera, type CameraChangeInfo, type CameraOptions, type CanvasElement, type CanvasState, type Command, DEFAULT_NOTE_FONT_SIZE, type DistributeAxis, type ElementChangeMeta, ElementStore, type ElementStyle, type ElementType, type ElementUpdateEvent, EraserTool, type EraserToolOptions, type ExportAssetError, type ExportAssetErrorReason, type ExportImageOptions, type ExportResourceOptions, type ExportSvgOptions, type FontSizePreset, type GridElement, type GridInfo, HandTool, type HexOrientation, HistoryStack, type HistoryStackOptions, type HtmlElement, type HtmlExportError, type HtmlExportErrorReason, type HtmlExportOptions, type HtmlExportRenderer, type ImageElement, ImageTool, type ImageToolOptions, IndexedDBAdapter, type IndexedDBAdapterOptions, LaserTool, type LaserToolOptions, type Layer, LayerManager, LocalStorageAdapter, MeasureTool, type MeasureToolOptions, type Measurement, MemoryAdapter, type NoteElement, NoteTool, type NoteToolOptions, PencilTool, type PencilToolOptions, type Point, type PointerState, type RenderStatsSnapshot, type RotateDirection, SelectTool, type ShapeElement, type ShapeKind, ShapeTool, type ShapeToolOptions, type ShortcutBindings, type ShortcutOptions, type ShortcutsApi, type Size, type StorageAdapter, type StrokeElement, type StrokePoint, type TemplateElement, type TemplateRenderStyle, type TemplateShape, TemplateTool, type TemplateToolOptions, type TextElement, TextTool, type TextToolOptions, type Tool, type ToolContext, ToolManager, type ToolName, VERSION, Viewport, type ViewportOptions, boundsIntersect, createArrow, createGrid, createHtmlElement, createImage, createNote, createShape, createStroke, createTemplate, createText, drawHexPath, exportImage, exportSvg, getActiveFormats, getArrowBounds, getArrowControlPoint, getArrowMidpoint, getArrowTangentAngle, getBendFromPoint, getElementBounds, getElementStyle, getElementsBoundingBox, getHexCellsInCone, getHexCellsInLine, getHexCellsInRadius, getHexCellsInRectangle, getHexCellsInSquare, getHexDistance, isNearBezier, setFontSize, smartSnap, snapPoint, snapToHexCenter, styleToPatch, toggleBold, toggleItalic, toggleStrikethrough, toggleUnderline };
package/dist/index.js CHANGED
@@ -5355,7 +5355,71 @@ function renderTextOnCanvas(ctx, text) {
5355
5355
  ctx.restore();
5356
5356
  }
5357
5357
 
5358
+ // src/canvas/html-export.ts
5359
+ var DEFAULT_HTML_TIMEOUT_MS = 1e4;
5360
+ var TIMEOUT = /* @__PURE__ */ Symbol("html-export-timeout");
5361
+ function validateHtmlExportOptions(options) {
5362
+ const timeoutMs = options.htmlTimeoutMs ?? DEFAULT_HTML_TIMEOUT_MS;
5363
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
5364
+ throw new RangeError("htmlTimeoutMs must be a finite number greater than 0");
5365
+ }
5366
+ return timeoutMs;
5367
+ }
5368
+ async function renderHtmlElements(elements, options) {
5369
+ const timeoutMs = validateHtmlExportOptions(options);
5370
+ const sources = /* @__PURE__ */ new Map();
5371
+ await Promise.all(
5372
+ elements.map(async (element) => {
5373
+ if (!options.renderHtml) {
5374
+ options.onHtmlError?.({
5375
+ elementId: element.id,
5376
+ htmlType: element.htmlType,
5377
+ reason: "unsupported"
5378
+ });
5379
+ return;
5380
+ }
5381
+ let timer;
5382
+ try {
5383
+ const timeout = new Promise((resolve) => {
5384
+ timer = setTimeout(() => resolve(TIMEOUT), timeoutMs);
5385
+ });
5386
+ const result = await Promise.race([Promise.resolve(options.renderHtml(element)), timeout]);
5387
+ if (result === TIMEOUT) {
5388
+ options.onHtmlError?.({
5389
+ elementId: element.id,
5390
+ htmlType: element.htmlType,
5391
+ reason: "timeout"
5392
+ });
5393
+ return;
5394
+ }
5395
+ if (!result) {
5396
+ options.onHtmlError?.({
5397
+ elementId: element.id,
5398
+ htmlType: element.htmlType,
5399
+ reason: "render"
5400
+ });
5401
+ return;
5402
+ }
5403
+ sources.set(element.id, result);
5404
+ } catch (cause) {
5405
+ options.onHtmlError?.({
5406
+ elementId: element.id,
5407
+ htmlType: element.htmlType,
5408
+ reason: "render",
5409
+ cause
5410
+ });
5411
+ } finally {
5412
+ if (timer !== void 0) clearTimeout(timer);
5413
+ }
5414
+ })
5415
+ );
5416
+ return sources;
5417
+ }
5418
+
5358
5419
  // src/canvas/export-image.ts
5420
+ var DEFAULT_IMAGE_TIMEOUT_MS = 1e4;
5421
+ var DEFAULT_MAX_DIMENSION = 16384;
5422
+ var DEFAULT_MAX_PIXELS = 67108864;
5359
5423
  var center = (b) => ({ x: b.x + b.w / 2, y: b.y + b.h / 2 });
5360
5424
  function getStrokeBounds(el) {
5361
5425
  if (el.type !== "stroke") return null;
@@ -5465,12 +5529,53 @@ function renderGridForBounds(ctx, grid, bounds) {
5465
5529
  );
5466
5530
  }
5467
5531
  }
5468
- function loadImages(elements) {
5532
+ function positiveOption(value, fallback, name) {
5533
+ const resolved = value ?? fallback;
5534
+ if (!Number.isFinite(resolved) || resolved <= 0) {
5535
+ throw new RangeError(`${name} must be a finite number greater than 0`);
5536
+ }
5537
+ return resolved;
5538
+ }
5539
+ function nonNegativeOption(value, fallback, name) {
5540
+ const resolved = value ?? fallback;
5541
+ if (!Number.isFinite(resolved) || resolved < 0) {
5542
+ throw new RangeError(`${name} must be a finite number greater than or equal to 0`);
5543
+ }
5544
+ return resolved;
5545
+ }
5546
+ function assertExportSize(width, height, options) {
5547
+ const maxDimension = positiveOption(options.maxDimension, DEFAULT_MAX_DIMENSION, "maxDimension");
5548
+ const maxPixels = positiveOption(options.maxPixels, DEFAULT_MAX_PIXELS, "maxPixels");
5549
+ if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
5550
+ throw new RangeError("Export dimensions must be finite numbers greater than 0");
5551
+ }
5552
+ if (width > maxDimension || height > maxDimension) {
5553
+ throw new RangeError(
5554
+ `Export dimensions ${width}x${height} exceed the maximum dimension of ${maxDimension}`
5555
+ );
5556
+ }
5557
+ if (width * height > maxPixels) {
5558
+ throw new RangeError(
5559
+ `Export size ${width}x${height} exceeds the maximum of ${maxPixels} pixels`
5560
+ );
5561
+ }
5562
+ }
5563
+ function validateExportResourceOptions(options) {
5564
+ positiveOption(options.imageTimeoutMs, DEFAULT_IMAGE_TIMEOUT_MS, "imageTimeoutMs");
5565
+ positiveOption(options.maxDimension, DEFAULT_MAX_DIMENSION, "maxDimension");
5566
+ positiveOption(options.maxPixels, DEFAULT_MAX_PIXELS, "maxPixels");
5567
+ }
5568
+ function loadImages(elements, options = {}) {
5469
5569
  const imageElements = elements.filter(
5470
5570
  (el) => el.type === "image" && "src" in el
5471
5571
  );
5472
5572
  const cache3 = /* @__PURE__ */ new Map();
5473
5573
  if (imageElements.length === 0) return Promise.resolve(cache3);
5574
+ const timeoutMs = positiveOption(
5575
+ options.imageTimeoutMs,
5576
+ DEFAULT_IMAGE_TIMEOUT_MS,
5577
+ "imageTimeoutMs"
5578
+ );
5474
5579
  return new Promise((resolve) => {
5475
5580
  let remaining = imageElements.length;
5476
5581
  const done = () => {
@@ -5480,19 +5585,42 @@ function loadImages(elements) {
5480
5585
  for (const el of imageElements) {
5481
5586
  const img = new Image();
5482
5587
  img.crossOrigin = "anonymous";
5588
+ let settled = false;
5589
+ const timer = setTimeout(() => {
5590
+ if (settled) return;
5591
+ settled = true;
5592
+ img.onload = null;
5593
+ img.onerror = null;
5594
+ options.onAssetError?.({ elementId: el.id, src: el.src, reason: "timeout" });
5595
+ done();
5596
+ }, timeoutMs);
5597
+ const settle = () => {
5598
+ if (settled) return false;
5599
+ settled = true;
5600
+ clearTimeout(timer);
5601
+ img.onload = null;
5602
+ img.onerror = null;
5603
+ return true;
5604
+ };
5483
5605
  img.onload = () => {
5606
+ if (!settle()) return;
5484
5607
  cache3.set(el.id, img);
5485
5608
  done();
5486
5609
  };
5487
- img.onerror = done;
5488
- const sep = el.src.includes("?") ? "&" : "?";
5489
- img.src = `${el.src}${sep}_cors=1`;
5610
+ img.onerror = (cause) => {
5611
+ if (!settle()) return;
5612
+ options.onAssetError?.({ elementId: el.id, src: el.src, reason: "load", cause });
5613
+ done();
5614
+ };
5615
+ img.src = el.src;
5490
5616
  }
5491
5617
  });
5492
5618
  }
5493
5619
  async function exportImage(store, options = {}, layerManager) {
5494
- const scale = options.scale ?? 2;
5495
- const padding = options.padding ?? 0;
5620
+ const scale = positiveOption(options.scale, 2, "scale");
5621
+ const padding = nonNegativeOption(options.padding, 0, "padding");
5622
+ validateExportResourceOptions(options);
5623
+ validateHtmlExportOptions(options);
5496
5624
  const background = options.background ?? "#ffffff";
5497
5625
  const filter = options.filter;
5498
5626
  const allElements = store.getAll();
@@ -5502,10 +5630,15 @@ async function exportImage(store, options = {}, layerManager) {
5502
5630
  }
5503
5631
  const bounds = computeBounds(visibleElements, padding);
5504
5632
  if (!bounds) return null;
5505
- const imageCache = await loadImages(visibleElements);
5633
+ const width = Math.ceil(bounds.w * scale);
5634
+ const height = Math.ceil(bounds.h * scale);
5635
+ assertExportSize(width, height, options);
5636
+ const imageCache = await loadImages(visibleElements, options);
5637
+ const htmlElements = visibleElements.filter((el) => el.type === "html");
5638
+ const htmlSources = await renderHtmlElements(htmlElements, options);
5506
5639
  const canvas = document.createElement("canvas");
5507
- canvas.width = Math.ceil(bounds.w * scale);
5508
- canvas.height = Math.ceil(bounds.h * scale);
5640
+ canvas.width = width;
5641
+ canvas.height = height;
5509
5642
  const ctx = canvas.getContext("2d");
5510
5643
  if (!ctx) return null;
5511
5644
  ctx.scale(scale, scale);
@@ -5527,6 +5660,24 @@ async function exportImage(store, options = {}, layerManager) {
5527
5660
  return;
5528
5661
  }
5529
5662
  if (el.type === "html") {
5663
+ const source = htmlSources.get(el.id);
5664
+ if (!source) return;
5665
+ const b = getElementBounds(el);
5666
+ try {
5667
+ withRotation(
5668
+ target,
5669
+ el,
5670
+ b ? center(b) : el.position,
5671
+ () => target.drawImage(source, el.position.x, el.position.y, el.size.w, el.size.h)
5672
+ );
5673
+ } catch (cause) {
5674
+ options.onHtmlError?.({
5675
+ elementId: el.id,
5676
+ htmlType: el.htmlType,
5677
+ reason: "render",
5678
+ cause
5679
+ });
5680
+ }
5530
5681
  return;
5531
5682
  }
5532
5683
  if (el.type === "image") {
@@ -5682,20 +5833,23 @@ function emitArrow(arrow, store) {
5682
5833
  return out;
5683
5834
  }
5684
5835
  function emitImage(image, dataUri) {
5685
- const href = dataUri ?? image.src;
5836
+ const href = dataUri ?? ("src" in image ? image.src : "");
5686
5837
  if (!href) return "";
5687
5838
  const { x, y } = image.position;
5688
5839
  const { w, h } = image.size;
5689
5840
  return `<image href="${esc(href)}" x="${n(x)}" y="${n(y)}" width="${n(w)}" height="${n(h)}" />`;
5690
5841
  }
5691
- function emitText(text, rasterScale) {
5842
+ function emitText(text, rasterScale, resourceOptions) {
5692
5843
  if (!text.text) return "";
5693
5844
  const { x, y } = text.position;
5694
5845
  const { w, h } = text.size;
5695
5846
  if (typeof document === "undefined") return "";
5847
+ const width = Math.max(1, Math.ceil(w * rasterScale));
5848
+ const height = Math.max(1, Math.ceil(h * rasterScale));
5849
+ assertExportSize(width, height, resourceOptions);
5696
5850
  const canvas = document.createElement("canvas");
5697
- canvas.width = Math.max(1, Math.ceil(w * rasterScale));
5698
- canvas.height = Math.max(1, Math.ceil(h * rasterScale));
5851
+ canvas.width = width;
5852
+ canvas.height = height;
5699
5853
  const ctx = canvas.getContext("2d");
5700
5854
  if (!ctx) return "";
5701
5855
  ctx.scale(rasterScale, rasterScale);
@@ -5710,13 +5864,16 @@ function emitText(text, rasterScale) {
5710
5864
  if (!dataUri || !dataUri.startsWith("data:")) return "";
5711
5865
  return `<image href="${esc(dataUri)}" x="${n(x)}" y="${n(y)}" width="${n(w)}" height="${n(h)}" />`;
5712
5866
  }
5713
- function emitNote(note, rasterScale) {
5867
+ function emitNote(note, rasterScale, resourceOptions) {
5714
5868
  const { x, y } = note.position;
5715
5869
  const { w, h } = note.size;
5716
5870
  if (typeof document === "undefined") return emitNotePlaceholder(note);
5871
+ const width = Math.max(1, Math.ceil(w * rasterScale));
5872
+ const height = Math.max(1, Math.ceil(h * rasterScale));
5873
+ assertExportSize(width, height, resourceOptions);
5717
5874
  const canvas = document.createElement("canvas");
5718
- canvas.width = Math.max(1, Math.ceil(w * rasterScale));
5719
- canvas.height = Math.max(1, Math.ceil(h * rasterScale));
5875
+ canvas.width = width;
5876
+ canvas.height = height;
5720
5877
  const ctx = canvas.getContext("2d");
5721
5878
  if (!ctx) return emitNotePlaceholder(note);
5722
5879
  ctx.scale(rasterScale, rasterScale);
@@ -5877,8 +6034,10 @@ function emitHexTemplate(t, grid) {
5877
6034
  return `<path d="${d}" fill="${esc(t.fillColor)}" stroke="${esc(t.strokeColor)}" stroke-width="${n(t.strokeWidth)}" opacity="${n(t.opacity)}" />`;
5878
6035
  }
5879
6036
  async function exportSvg(store, options = {}, layerManager) {
5880
- const padding = options.padding ?? 0;
5881
- const rasterScale = options.rasterScale ?? 2;
6037
+ const padding = nonNegativeOption(options.padding, 0, "padding");
6038
+ const rasterScale = positiveOption(options.rasterScale, 2, "rasterScale");
6039
+ validateExportResourceOptions(options);
6040
+ validateHtmlExportOptions(options);
5882
6041
  const filter = options.filter;
5883
6042
  const allElements = store.getAll();
5884
6043
  let visibleElements = layerManager ? allElements.filter((el) => layerManager.isLayerVisible(el.layerId)) : allElements;
@@ -5887,11 +6046,15 @@ async function exportSvg(store, options = {}, layerManager) {
5887
6046
  if (!bounds) {
5888
6047
  return `<svg xmlns="http://www.w3.org/2000/svg" width="0" height="0" viewBox="0 0 0 0"></svg>`;
5889
6048
  }
6049
+ assertExportSize(Math.ceil(bounds.w), Math.ceil(bounds.h), options);
5890
6050
  const remoteImages = visibleElements.filter(
5891
6051
  (el) => el.type === "image" && !el.src.startsWith("data:")
5892
6052
  );
5893
- const imageCache = await loadImages(remoteImages);
5894
- const imageDataUris = encodeImages(visibleElements, imageCache, rasterScale);
6053
+ const imageCache = await loadImages(remoteImages, options);
6054
+ const imageDataUris = encodeImages(visibleElements, imageCache, rasterScale, options);
6055
+ const htmlElements = visibleElements.filter((el) => el.type === "html");
6056
+ const htmlSources = await renderHtmlElements(htmlElements, options);
6057
+ const htmlDataUris = encodeHtmlElements(htmlElements, htmlSources, rasterScale, options);
5895
6058
  const grids = visibleElements.filter((el) => el.type === "grid");
5896
6059
  const firstGrid = grids[0];
5897
6060
  let body = "";
@@ -5900,7 +6063,15 @@ async function exportSvg(store, options = {}, layerManager) {
5900
6063
  }
5901
6064
  const layerBodies = /* @__PURE__ */ new Map();
5902
6065
  for (const el of visibleElements) {
5903
- const emitted = emitElement(el, imageDataUris, rasterScale, firstGrid, store);
6066
+ const emitted = emitElement(
6067
+ el,
6068
+ imageDataUris,
6069
+ htmlDataUris,
6070
+ rasterScale,
6071
+ firstGrid,
6072
+ store,
6073
+ options
6074
+ );
5904
6075
  layerBodies.set(el.layerId, (layerBodies.get(el.layerId) ?? "") + emitted);
5905
6076
  }
5906
6077
  for (const [layerId, emitted] of layerBodies) {
@@ -5914,7 +6085,7 @@ async function exportSvg(store, options = {}, layerManager) {
5914
6085
  }
5915
6086
  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>`;
5916
6087
  }
5917
- function emitElement(el, imageDataUris, rasterScale, firstGrid, store) {
6088
+ function emitElement(el, imageDataUris, htmlDataUris, rasterScale, firstGrid, store, resourceOptions) {
5918
6089
  switch (el.type) {
5919
6090
  case "stroke":
5920
6091
  return withRotationSvg(el, emitStroke(el));
@@ -5925,20 +6096,71 @@ function emitElement(el, imageDataUris, rasterScale, firstGrid, store) {
5925
6096
  case "image":
5926
6097
  return withRotationSvg(el, emitImage(el, imageDataUris.get(el.id)));
5927
6098
  case "text":
5928
- return withRotationSvg(el, emitText(el, rasterScale));
6099
+ return withRotationSvg(el, emitText(el, rasterScale, resourceOptions));
5929
6100
  case "note":
5930
- return withRotationSvg(el, emitNote(el, rasterScale));
6101
+ return withRotationSvg(el, emitNote(el, rasterScale, resourceOptions));
5931
6102
  case "template":
5932
6103
  return emitTemplate(el, firstGrid);
5933
6104
  case "grid":
5934
6105
  return "";
5935
6106
  case "html":
5936
- return "";
6107
+ return withRotationSvg(el, emitImage(el, htmlDataUris.get(el.id)));
5937
6108
  default:
5938
6109
  return "";
5939
6110
  }
5940
6111
  }
5941
- function encodeImages(elements, imageCache, rasterScale) {
6112
+ function encodeHtmlElements(elements, sources, rasterScale, options) {
6113
+ const encoded = /* @__PURE__ */ new Map();
6114
+ for (const element of elements) {
6115
+ const source = sources.get(element.id);
6116
+ if (!source) continue;
6117
+ const width = Math.max(1, Math.ceil(element.size.w * rasterScale));
6118
+ const height = Math.max(1, Math.ceil(element.size.h * rasterScale));
6119
+ assertExportSize(width, height, options);
6120
+ if (typeof document === "undefined") {
6121
+ options.onHtmlError?.({
6122
+ elementId: element.id,
6123
+ htmlType: element.htmlType,
6124
+ reason: "encode"
6125
+ });
6126
+ continue;
6127
+ }
6128
+ const canvas = document.createElement("canvas");
6129
+ canvas.width = width;
6130
+ canvas.height = height;
6131
+ const ctx = canvas.getContext("2d");
6132
+ if (!ctx) {
6133
+ options.onHtmlError?.({
6134
+ elementId: element.id,
6135
+ htmlType: element.htmlType,
6136
+ reason: "encode"
6137
+ });
6138
+ continue;
6139
+ }
6140
+ try {
6141
+ ctx.drawImage(source, 0, 0, width, height);
6142
+ const dataUri = canvas.toDataURL();
6143
+ if (dataUri.startsWith("data:")) {
6144
+ encoded.set(element.id, dataUri);
6145
+ } else {
6146
+ options.onHtmlError?.({
6147
+ elementId: element.id,
6148
+ htmlType: element.htmlType,
6149
+ reason: "encode"
6150
+ });
6151
+ }
6152
+ } catch (cause) {
6153
+ options.onHtmlError?.({
6154
+ elementId: element.id,
6155
+ htmlType: element.htmlType,
6156
+ reason: "encode",
6157
+ cause
6158
+ });
6159
+ }
6160
+ }
6161
+ return encoded;
6162
+ }
6163
+ function encodeImages(elements, imageCache, rasterScale, resourceOptions) {
5942
6164
  const out = /* @__PURE__ */ new Map();
5943
6165
  for (const el of elements) {
5944
6166
  if (el.type !== "image") continue;
@@ -5948,16 +6170,24 @@ function encodeImages(elements, imageCache, rasterScale) {
5948
6170
  }
5949
6171
  const img = imageCache.get(el.id);
5950
6172
  if (!img || typeof document === "undefined") continue;
6173
+ const width = Math.max(1, Math.ceil(el.size.w * rasterScale));
6174
+ const height = Math.max(1, Math.ceil(el.size.h * rasterScale));
6175
+ assertExportSize(width, height, resourceOptions);
5951
6176
  const canvas = document.createElement("canvas");
5952
- canvas.width = Math.max(1, Math.ceil(el.size.w * rasterScale));
5953
- canvas.height = Math.max(1, Math.ceil(el.size.h * rasterScale));
6177
+ canvas.width = width;
6178
+ canvas.height = height;
5954
6179
  const ctx = canvas.getContext("2d");
5955
- if (!ctx) continue;
6180
+ if (!ctx) {
6181
+ resourceOptions.onAssetError?.({ elementId: el.id, src: el.src, reason: "encode" });
6182
+ continue;
6183
+ }
5956
6184
  try {
5957
6185
  ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
5958
6186
  const uri = canvas.toDataURL();
5959
6187
  if (uri.startsWith("data:")) out.set(el.id, uri);
5960
- } catch {
6188
+ else resourceOptions.onAssetError?.({ elementId: el.id, src: el.src, reason: "encode" });
6189
+ } catch (cause) {
6190
+ resourceOptions.onAssetError?.({ elementId: el.id, src: el.src, reason: "encode", cause });
5961
6191
  }
5962
6192
  }
5963
6193
  return out;
@@ -6506,7 +6736,7 @@ var RenderLoop = class {
6506
6736
  layerGroups = /* @__PURE__ */ new Map();
6507
6737
  gridCacheCanvas = null;
6508
6738
  gridCacheCtx = null;
6509
- lastGridRef = null;
6739
+ lastGridRefs = [];
6510
6740
  constructor(deps) {
6511
6741
  this.canvasEl = deps.canvasEl;
6512
6742
  this.camera = deps.camera;
@@ -6706,8 +6936,8 @@ var RenderLoop = class {
6706
6936
  }
6707
6937
  if (gridElements.length > 0) {
6708
6938
  const gridT0 = performance.now();
6709
- const gridRef = gridElements[0];
6710
- const gridDirty = this.gridCacheDirty || gridRef !== this.lastGridRef;
6939
+ const gridsChanged = gridElements.length !== this.lastGridRefs.length || gridElements.some((grid, index) => grid !== this.lastGridRefs[index]);
6940
+ const gridDirty = this.gridCacheDirty || gridsChanged;
6711
6941
  if (gridDirty) {
6712
6942
  this.ensureGridCache();
6713
6943
  if (this.gridCacheCtx && this.gridCacheCanvas) {
@@ -6735,7 +6965,7 @@ var RenderLoop = class {
6735
6965
  }
6736
6966
  }
6737
6967
  this.gridCacheDirty = false;
6738
- this.lastGridRef = gridRef;
6968
+ this.lastGridRefs = [...gridElements];
6739
6969
  }
6740
6970
  if (this.gridCacheCanvas) {
6741
6971
  const offset = this.marginViewport.compositeOffset(
@@ -6755,6 +6985,8 @@ var RenderLoop = class {
6755
6985
  }
6756
6986
  }
6757
6987
  gridMs = performance.now() - gridT0;
6988
+ } else {
6989
+ this.lastGridRefs = [];
6758
6990
  }
6759
6991
  const overlayT0 = performance.now();
6760
6992
  const activeTool = this.toolManager.activeTool;
@@ -10728,7 +10960,7 @@ var LaserTool = class {
10728
10960
  };
10729
10961
 
10730
10962
  // src/index.ts
10731
- var VERSION = "0.50.8";
10963
+ var VERSION = "0.52.0";
10732
10964
  export {
10733
10965
  ArrowTool,
10734
10966
  AutoSave,