@fieldnotes/core 0.51.0 → 0.52.1

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,7 +447,24 @@ declare class HistoryStack {
447
447
  private notifyChange;
448
448
  }
449
449
 
450
- interface ExportImageOptions extends ExportResourceOptions {
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;
@@ -472,7 +489,7 @@ interface ExportResourceOptions {
472
489
  }
473
490
  declare function exportImage(store: ElementStore, options?: ExportImageOptions, layerManager?: LayerManager): Promise<Blob | null>;
474
491
 
475
- interface ExportSvgOptions extends ExportResourceOptions {
492
+ interface ExportSvgOptions extends ExportResourceOptions, HtmlExportOptions {
476
493
  padding?: number;
477
494
  background?: string;
478
495
  filter?: (el: CanvasElement) => boolean;
@@ -555,6 +572,7 @@ declare class Viewport {
555
572
  readonly history: HistoryStack;
556
573
  readonly domLayer: HTMLDivElement;
557
574
  private readonly canvasEl;
575
+ private readonly paintStack;
558
576
  private readonly wrapper;
559
577
  private readonly unsubCamera;
560
578
  private readonly unsubToolChange;
@@ -1140,6 +1158,6 @@ declare class LaserTool implements Tool {
1140
1158
  private notifyOptionsChange;
1141
1159
  }
1142
1160
 
1143
- declare const VERSION = "0.51.0";
1161
+ declare const VERSION = "0.52.1";
1144
1162
 
1145
- 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 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 };
1163
+ 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,7 +447,24 @@ declare class HistoryStack {
447
447
  private notifyChange;
448
448
  }
449
449
 
450
- interface ExportImageOptions extends ExportResourceOptions {
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;
@@ -472,7 +489,7 @@ interface ExportResourceOptions {
472
489
  }
473
490
  declare function exportImage(store: ElementStore, options?: ExportImageOptions, layerManager?: LayerManager): Promise<Blob | null>;
474
491
 
475
- interface ExportSvgOptions extends ExportResourceOptions {
492
+ interface ExportSvgOptions extends ExportResourceOptions, HtmlExportOptions {
476
493
  padding?: number;
477
494
  background?: string;
478
495
  filter?: (el: CanvasElement) => boolean;
@@ -555,6 +572,7 @@ declare class Viewport {
555
572
  readonly history: HistoryStack;
556
573
  readonly domLayer: HTMLDivElement;
557
574
  private readonly canvasEl;
575
+ private readonly paintStack;
558
576
  private readonly wrapper;
559
577
  private readonly unsubCamera;
560
578
  private readonly unsubToolChange;
@@ -1140,6 +1158,6 @@ declare class LaserTool implements Tool {
1140
1158
  private notifyOptionsChange;
1141
1159
  }
1142
1160
 
1143
- declare const VERSION = "0.51.0";
1161
+ declare const VERSION = "0.52.1";
1144
1162
 
1145
- 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 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 };
1163
+ 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
@@ -4834,6 +4834,57 @@ function createDomLayer() {
4834
4834
  });
4835
4835
  return el;
4836
4836
  }
4837
+ function createPaintStack() {
4838
+ const el = createDomLayer();
4839
+ el.dataset["fieldnotesPaintStack"] = "";
4840
+ el.style.transformOrigin = "";
4841
+ return el;
4842
+ }
4843
+
4844
+ // src/canvas/hybrid-render-surface.ts
4845
+ var HybridRenderSurface = class {
4846
+ constructor(root) {
4847
+ this.root = root;
4848
+ }
4849
+ canvases = /* @__PURE__ */ new Map();
4850
+ beginFrame(activeOrders, width, height) {
4851
+ for (const [order, canvas] of this.canvases) {
4852
+ if (!activeOrders.has(order)) {
4853
+ canvas.remove();
4854
+ this.canvases.delete(order);
4855
+ }
4856
+ }
4857
+ for (const order of activeOrders) {
4858
+ const canvas = this.getCanvas(order);
4859
+ if (canvas.width !== width) canvas.width = width;
4860
+ if (canvas.height !== height) canvas.height = height;
4861
+ canvas.style.zIndex = String(order);
4862
+ }
4863
+ }
4864
+ getContext(order) {
4865
+ return this.getCanvas(order).getContext("2d");
4866
+ }
4867
+ clear() {
4868
+ for (const canvas of this.canvases.values()) canvas.remove();
4869
+ this.canvases.clear();
4870
+ }
4871
+ getCanvas(order) {
4872
+ let canvas = this.canvases.get(order);
4873
+ if (canvas) return canvas;
4874
+ canvas = document.createElement("canvas");
4875
+ canvas.dataset["paintOrder"] = String(order);
4876
+ Object.assign(canvas.style, {
4877
+ position: "absolute",
4878
+ inset: "0",
4879
+ width: "100%",
4880
+ height: "100%",
4881
+ pointerEvents: "none"
4882
+ });
4883
+ this.root.appendChild(canvas);
4884
+ this.canvases.set(order, canvas);
4885
+ return canvas;
4886
+ }
4887
+ };
4837
4888
 
4838
4889
  // src/elements/arrow-label-editor.ts
4839
4890
  var ArrowLabelEditor = class {
@@ -5355,6 +5406,67 @@ function renderTextOnCanvas(ctx, text) {
5355
5406
  ctx.restore();
5356
5407
  }
5357
5408
 
5409
+ // src/canvas/html-export.ts
5410
+ var DEFAULT_HTML_TIMEOUT_MS = 1e4;
5411
+ var TIMEOUT = /* @__PURE__ */ Symbol("html-export-timeout");
5412
+ function validateHtmlExportOptions(options) {
5413
+ const timeoutMs = options.htmlTimeoutMs ?? DEFAULT_HTML_TIMEOUT_MS;
5414
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
5415
+ throw new RangeError("htmlTimeoutMs must be a finite number greater than 0");
5416
+ }
5417
+ return timeoutMs;
5418
+ }
5419
+ async function renderHtmlElements(elements, options) {
5420
+ const timeoutMs = validateHtmlExportOptions(options);
5421
+ const sources = /* @__PURE__ */ new Map();
5422
+ await Promise.all(
5423
+ elements.map(async (element) => {
5424
+ if (!options.renderHtml) {
5425
+ options.onHtmlError?.({
5426
+ elementId: element.id,
5427
+ htmlType: element.htmlType,
5428
+ reason: "unsupported"
5429
+ });
5430
+ return;
5431
+ }
5432
+ let timer;
5433
+ try {
5434
+ const timeout = new Promise((resolve) => {
5435
+ timer = setTimeout(() => resolve(TIMEOUT), timeoutMs);
5436
+ });
5437
+ const result = await Promise.race([Promise.resolve(options.renderHtml(element)), timeout]);
5438
+ if (result === TIMEOUT) {
5439
+ options.onHtmlError?.({
5440
+ elementId: element.id,
5441
+ htmlType: element.htmlType,
5442
+ reason: "timeout"
5443
+ });
5444
+ return;
5445
+ }
5446
+ if (!result) {
5447
+ options.onHtmlError?.({
5448
+ elementId: element.id,
5449
+ htmlType: element.htmlType,
5450
+ reason: "render"
5451
+ });
5452
+ return;
5453
+ }
5454
+ sources.set(element.id, result);
5455
+ } catch (cause) {
5456
+ options.onHtmlError?.({
5457
+ elementId: element.id,
5458
+ htmlType: element.htmlType,
5459
+ reason: "render",
5460
+ cause
5461
+ });
5462
+ } finally {
5463
+ if (timer !== void 0) clearTimeout(timer);
5464
+ }
5465
+ })
5466
+ );
5467
+ return sources;
5468
+ }
5469
+
5358
5470
  // src/canvas/export-image.ts
5359
5471
  var DEFAULT_IMAGE_TIMEOUT_MS = 1e4;
5360
5472
  var DEFAULT_MAX_DIMENSION = 16384;
@@ -5559,6 +5671,7 @@ async function exportImage(store, options = {}, layerManager) {
5559
5671
  const scale = positiveOption(options.scale, 2, "scale");
5560
5672
  const padding = nonNegativeOption(options.padding, 0, "padding");
5561
5673
  validateExportResourceOptions(options);
5674
+ validateHtmlExportOptions(options);
5562
5675
  const background = options.background ?? "#ffffff";
5563
5676
  const filter = options.filter;
5564
5677
  const allElements = store.getAll();
@@ -5572,6 +5685,8 @@ async function exportImage(store, options = {}, layerManager) {
5572
5685
  const height = Math.ceil(bounds.h * scale);
5573
5686
  assertExportSize(width, height, options);
5574
5687
  const imageCache = await loadImages(visibleElements, options);
5688
+ const htmlElements = visibleElements.filter((el) => el.type === "html");
5689
+ const htmlSources = await renderHtmlElements(htmlElements, options);
5575
5690
  const canvas = document.createElement("canvas");
5576
5691
  canvas.width = width;
5577
5692
  canvas.height = height;
@@ -5596,6 +5711,24 @@ async function exportImage(store, options = {}, layerManager) {
5596
5711
  return;
5597
5712
  }
5598
5713
  if (el.type === "html") {
5714
+ const source = htmlSources.get(el.id);
5715
+ if (!source) return;
5716
+ const b = getElementBounds(el);
5717
+ try {
5718
+ withRotation(
5719
+ target,
5720
+ el,
5721
+ b ? center(b) : el.position,
5722
+ () => target.drawImage(source, el.position.x, el.position.y, el.size.w, el.size.h)
5723
+ );
5724
+ } catch (cause) {
5725
+ options.onHtmlError?.({
5726
+ elementId: el.id,
5727
+ htmlType: el.htmlType,
5728
+ reason: "render",
5729
+ cause
5730
+ });
5731
+ }
5599
5732
  return;
5600
5733
  }
5601
5734
  if (el.type === "image") {
@@ -5751,7 +5884,7 @@ function emitArrow(arrow, store) {
5751
5884
  return out;
5752
5885
  }
5753
5886
  function emitImage(image, dataUri) {
5754
- const href = dataUri ?? image.src;
5887
+ const href = dataUri ?? ("src" in image ? image.src : "");
5755
5888
  if (!href) return "";
5756
5889
  const { x, y } = image.position;
5757
5890
  const { w, h } = image.size;
@@ -5955,6 +6088,7 @@ async function exportSvg(store, options = {}, layerManager) {
5955
6088
  const padding = nonNegativeOption(options.padding, 0, "padding");
5956
6089
  const rasterScale = positiveOption(options.rasterScale, 2, "rasterScale");
5957
6090
  validateExportResourceOptions(options);
6091
+ validateHtmlExportOptions(options);
5958
6092
  const filter = options.filter;
5959
6093
  const allElements = store.getAll();
5960
6094
  let visibleElements = layerManager ? allElements.filter((el) => layerManager.isLayerVisible(el.layerId)) : allElements;
@@ -5969,6 +6103,9 @@ async function exportSvg(store, options = {}, layerManager) {
5969
6103
  );
5970
6104
  const imageCache = await loadImages(remoteImages, options);
5971
6105
  const imageDataUris = encodeImages(visibleElements, imageCache, rasterScale, options);
6106
+ const htmlElements = visibleElements.filter((el) => el.type === "html");
6107
+ const htmlSources = await renderHtmlElements(htmlElements, options);
6108
+ const htmlDataUris = encodeHtmlElements(htmlElements, htmlSources, rasterScale, options);
5972
6109
  const grids = visibleElements.filter((el) => el.type === "grid");
5973
6110
  const firstGrid = grids[0];
5974
6111
  let body = "";
@@ -5977,7 +6114,15 @@ async function exportSvg(store, options = {}, layerManager) {
5977
6114
  }
5978
6115
  const layerBodies = /* @__PURE__ */ new Map();
5979
6116
  for (const el of visibleElements) {
5980
- const emitted = emitElement(el, imageDataUris, rasterScale, firstGrid, store, options);
6117
+ const emitted = emitElement(
6118
+ el,
6119
+ imageDataUris,
6120
+ htmlDataUris,
6121
+ rasterScale,
6122
+ firstGrid,
6123
+ store,
6124
+ options
6125
+ );
5981
6126
  layerBodies.set(el.layerId, (layerBodies.get(el.layerId) ?? "") + emitted);
5982
6127
  }
5983
6128
  for (const [layerId, emitted] of layerBodies) {
@@ -5991,7 +6136,7 @@ async function exportSvg(store, options = {}, layerManager) {
5991
6136
  }
5992
6137
  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>`;
5993
6138
  }
5994
- function emitElement(el, imageDataUris, rasterScale, firstGrid, store, resourceOptions) {
6139
+ function emitElement(el, imageDataUris, htmlDataUris, rasterScale, firstGrid, store, resourceOptions) {
5995
6140
  switch (el.type) {
5996
6141
  case "stroke":
5997
6142
  return withRotationSvg(el, emitStroke(el));
@@ -6010,11 +6155,62 @@ function emitElement(el, imageDataUris, rasterScale, firstGrid, store, resourceO
6010
6155
  case "grid":
6011
6156
  return "";
6012
6157
  case "html":
6013
- return "";
6158
+ return withRotationSvg(el, emitImage(el, htmlDataUris.get(el.id)));
6014
6159
  default:
6015
6160
  return "";
6016
6161
  }
6017
6162
  }
6163
+ function encodeHtmlElements(elements, sources, rasterScale, options) {
6164
+ const encoded = /* @__PURE__ */ new Map();
6165
+ for (const element of elements) {
6166
+ const source = sources.get(element.id);
6167
+ if (!source) continue;
6168
+ const width = Math.max(1, Math.ceil(element.size.w * rasterScale));
6169
+ const height = Math.max(1, Math.ceil(element.size.h * rasterScale));
6170
+ assertExportSize(width, height, options);
6171
+ if (typeof document === "undefined") {
6172
+ options.onHtmlError?.({
6173
+ elementId: element.id,
6174
+ htmlType: element.htmlType,
6175
+ reason: "encode"
6176
+ });
6177
+ continue;
6178
+ }
6179
+ const canvas = document.createElement("canvas");
6180
+ canvas.width = width;
6181
+ canvas.height = height;
6182
+ const ctx = canvas.getContext("2d");
6183
+ if (!ctx) {
6184
+ options.onHtmlError?.({
6185
+ elementId: element.id,
6186
+ htmlType: element.htmlType,
6187
+ reason: "encode"
6188
+ });
6189
+ continue;
6190
+ }
6191
+ try {
6192
+ ctx.drawImage(source, 0, 0, width, height);
6193
+ const dataUri = canvas.toDataURL();
6194
+ if (dataUri.startsWith("data:")) {
6195
+ encoded.set(element.id, dataUri);
6196
+ } else {
6197
+ options.onHtmlError?.({
6198
+ elementId: element.id,
6199
+ htmlType: element.htmlType,
6200
+ reason: "encode"
6201
+ });
6202
+ }
6203
+ } catch (cause) {
6204
+ options.onHtmlError?.({
6205
+ elementId: element.id,
6206
+ htmlType: element.htmlType,
6207
+ reason: "encode",
6208
+ cause
6209
+ });
6210
+ }
6211
+ }
6212
+ return encoded;
6213
+ }
6018
6214
  function encodeImages(elements, imageCache, rasterScale, resourceOptions) {
6019
6215
  const out = /* @__PURE__ */ new Map();
6020
6216
  for (const el of elements) {
@@ -6307,6 +6503,7 @@ var DoubleTapDetector = class {
6307
6503
  // src/canvas/dom-node-manager.ts
6308
6504
  var DomNodeManager = class {
6309
6505
  domNodes = /* @__PURE__ */ new Map();
6506
+ strata = /* @__PURE__ */ new Map();
6310
6507
  htmlContent = /* @__PURE__ */ new Map();
6311
6508
  domLayer;
6312
6509
  onEditRequest;
@@ -6315,6 +6512,7 @@ var DomNodeManager = class {
6315
6512
  lastSyncedVersion = /* @__PURE__ */ new Map();
6316
6513
  lastSyncedZIndex = /* @__PURE__ */ new Map();
6317
6514
  lastSyncedOpacity = /* @__PURE__ */ new Map();
6515
+ cameraTransform = "";
6318
6516
  constructor(deps) {
6319
6517
  this.domLayer = deps.domLayer;
6320
6518
  this.onEditRequest = deps.onEditRequest;
@@ -6324,6 +6522,11 @@ var DomNodeManager = class {
6324
6522
  getNode(id) {
6325
6523
  return this.domNodes.get(id);
6326
6524
  }
6525
+ setCameraTransform(transform) {
6526
+ if (transform === this.cameraTransform) return;
6527
+ this.cameraTransform = transform;
6528
+ for (const stratum of this.strata.values()) stratum.style.transform = transform;
6529
+ }
6327
6530
  storeHtmlContent(elementId, dom) {
6328
6531
  this.htmlContent.set(elementId, dom);
6329
6532
  }
@@ -6351,7 +6554,7 @@ var DomNodeManager = class {
6351
6554
  position: "absolute",
6352
6555
  pointerEvents: "auto"
6353
6556
  });
6354
- this.domLayer.appendChild(node);
6557
+ this.getStratum(zIndex).appendChild(node);
6355
6558
  this.domNodes.set(element.id, node);
6356
6559
  } else if (this.getVersion) {
6357
6560
  const currentVersion = this.getVersion(element.id);
@@ -6362,6 +6565,15 @@ var DomNodeManager = class {
6362
6565
  return;
6363
6566
  }
6364
6567
  }
6568
+ if (node.parentElement !== this.getStratum(zIndex)) {
6569
+ const previousStratum = node.parentElement;
6570
+ this.getStratum(zIndex).appendChild(node);
6571
+ if (previousStratum?.childElementCount === 0) {
6572
+ const previousOrder = Number(previousStratum.dataset["paintOrder"]);
6573
+ previousStratum.remove();
6574
+ this.strata.delete(previousOrder);
6575
+ }
6576
+ }
6365
6577
  if (this.getVersion) {
6366
6578
  this.lastSyncedVersion.set(element.id, this.getVersion(element.id));
6367
6579
  this.lastSyncedZIndex.set(element.id, zIndex);
@@ -6383,7 +6595,10 @@ var DomNodeManager = class {
6383
6595
  }
6384
6596
  hideDomNode(id) {
6385
6597
  const node = this.domNodes.get(id);
6386
- if (node) node.style.display = "none";
6598
+ if (node) {
6599
+ node.style.display = "none";
6600
+ this.lastSyncedVersion.delete(id);
6601
+ }
6387
6602
  }
6388
6603
  removeDomNode(id) {
6389
6604
  this.htmlContent.delete(id);
@@ -6392,8 +6607,14 @@ var DomNodeManager = class {
6392
6607
  this.lastSyncedOpacity.delete(id);
6393
6608
  const node = this.domNodes.get(id);
6394
6609
  if (node) {
6610
+ const stratum = node.parentElement;
6395
6611
  node.remove();
6396
6612
  this.domNodes.delete(id);
6613
+ if (stratum?.childElementCount === 0) {
6614
+ const order = Number(stratum.dataset["paintOrder"]);
6615
+ stratum.remove();
6616
+ this.strata.delete(order);
6617
+ }
6397
6618
  }
6398
6619
  }
6399
6620
  clearDomNodes() {
@@ -6403,6 +6624,27 @@ var DomNodeManager = class {
6403
6624
  this.lastSyncedVersion.clear();
6404
6625
  this.lastSyncedZIndex.clear();
6405
6626
  this.lastSyncedOpacity.clear();
6627
+ for (const stratum of this.strata.values()) stratum.remove();
6628
+ this.strata.clear();
6629
+ }
6630
+ getStratum(order) {
6631
+ let stratum = this.strata.get(order);
6632
+ if (stratum) return stratum;
6633
+ stratum = document.createElement("div");
6634
+ stratum.dataset["paintOrder"] = String(order);
6635
+ Object.assign(stratum.style, {
6636
+ position: "absolute",
6637
+ inset: "0",
6638
+ width: "100%",
6639
+ height: "100%",
6640
+ pointerEvents: "none",
6641
+ transformOrigin: "0 0",
6642
+ transform: this.cameraTransform,
6643
+ zIndex: String(order)
6644
+ });
6645
+ this.domLayer.appendChild(stratum);
6646
+ this.strata.set(order, stratum);
6647
+ return stratum;
6406
6648
  }
6407
6649
  reattachHtmlContent(store) {
6408
6650
  for (const el of store.getElementsByType("html")) {
@@ -6584,6 +6826,7 @@ var RenderLoop = class {
6584
6826
  domNodeManager;
6585
6827
  layerCache;
6586
6828
  marginViewport;
6829
+ hybridSurface;
6587
6830
  activeDrawingLayerId = null;
6588
6831
  gridCacheDirty = true;
6589
6832
  // set on recenter/viewport-change; consumed by the grid block
@@ -6603,6 +6846,7 @@ var RenderLoop = class {
6603
6846
  this.domNodeManager = deps.domNodeManager;
6604
6847
  this.layerCache = deps.layerCache;
6605
6848
  this.marginViewport = deps.marginViewport;
6849
+ this.hybridSurface = deps.hybridSurface;
6606
6850
  }
6607
6851
  requestRender() {
6608
6852
  this.needsRender = true;
@@ -6723,9 +6967,19 @@ var RenderLoop = class {
6723
6967
  h: cullBounds.h + cullPad * 2
6724
6968
  };
6725
6969
  const allElements = this.store.getAll();
6970
+ const visibleElements = allElements.filter(
6971
+ (element) => this.layerManager.isLayerVisible(element.layerId)
6972
+ );
6973
+ const firstDomIndex = visibleElements.findIndex(
6974
+ (element) => this.renderer.isDomElement(element)
6975
+ );
6976
+ const hybridActive = firstDomIndex >= 0 && visibleElements.slice(firstDomIndex + 1).some((element) => !this.renderer.isDomElement(element));
6977
+ const hybridCanvasRuns = /* @__PURE__ */ new Map();
6978
+ const hybridOrders = /* @__PURE__ */ new Set();
6979
+ let activeHybridOrder = null;
6726
6980
  this.layerGroups.clear();
6727
6981
  const gridElements = [];
6728
- let domZIndex = 0;
6982
+ let paintOrder = 0;
6729
6983
  for (const element of allElements) {
6730
6984
  if (!this.layerManager.isLayerVisible(element.layerId)) {
6731
6985
  if (this.renderer.isDomElement(element)) {
@@ -6733,16 +6987,29 @@ var RenderLoop = class {
6733
6987
  }
6734
6988
  continue;
6735
6989
  }
6990
+ const order = ++paintOrder;
6736
6991
  if (this.renderer.isDomElement(element)) {
6992
+ activeHybridOrder = null;
6737
6993
  const layerOpacity = this.layerManager.getLayer?.(element.layerId)?.opacity ?? 1;
6738
6994
  const elBounds = getElementBounds(element);
6739
6995
  if (elBounds && !boundsIntersect(elBounds, cullingRect)) {
6740
6996
  this.domNodeManager.hideDomNode(element.id);
6741
6997
  } else {
6742
- this.domNodeManager.syncDomNode(element, domZIndex++, layerOpacity);
6998
+ this.domNodeManager.syncDomNode(element, order, layerOpacity);
6743
6999
  }
6744
7000
  continue;
6745
7001
  }
7002
+ if (hybridActive && paintOrder > firstDomIndex + 1 && element.type !== "grid") {
7003
+ activeHybridOrder ??= order;
7004
+ let run = hybridCanvasRuns.get(activeHybridOrder);
7005
+ if (!run) {
7006
+ run = [];
7007
+ hybridCanvasRuns.set(activeHybridOrder, run);
7008
+ hybridOrders.add(activeHybridOrder);
7009
+ }
7010
+ run.push(element);
7011
+ continue;
7012
+ }
6746
7013
  if (element.type === "grid") {
6747
7014
  gridElements.push(element);
6748
7015
  continue;
@@ -6754,6 +7021,10 @@ var RenderLoop = class {
6754
7021
  }
6755
7022
  group.push(element);
6756
7023
  }
7024
+ const activeTool = this.toolManager.activeTool;
7025
+ const overlayOrder = visibleElements.length + 1;
7026
+ if (hybridActive && activeTool?.renderOverlay) hybridOrders.add(overlayOrder);
7027
+ this.hybridSurface.beginFrame(hybridOrders, this.canvasEl.width, this.canvasEl.height);
6757
7028
  for (const [layerId, elements] of this.layerGroups) {
6758
7029
  const isActiveDrawingLayer = layerId === this.activeDrawingLayerId;
6759
7030
  const layerOpacity = this.layerManager.getLayer?.(layerId)?.opacity ?? 1;
@@ -6843,9 +7114,37 @@ var RenderLoop = class {
6843
7114
  } else {
6844
7115
  this.lastGridRefs = [];
6845
7116
  }
7117
+ for (const [order, elements] of hybridCanvasRuns) {
7118
+ const hybridCtx = this.hybridSurface.getContext(order);
7119
+ if (!hybridCtx) continue;
7120
+ hybridCtx.clearRect(0, 0, this.canvasEl.width, this.canvasEl.height);
7121
+ hybridCtx.save();
7122
+ hybridCtx.scale(dpr, dpr);
7123
+ hybridCtx.translate(this.camera.position.x, this.camera.position.y);
7124
+ hybridCtx.scale(this.camera.zoom, this.camera.zoom);
7125
+ for (const element of elements) {
7126
+ const elBounds = getElementBounds(element);
7127
+ if (elBounds && !boundsIntersect(elBounds, cullingRect)) continue;
7128
+ hybridCtx.save();
7129
+ hybridCtx.globalAlpha = this.layerManager.getLayer?.(element.layerId)?.opacity ?? 1;
7130
+ this.renderer.renderCanvasElement(hybridCtx, element);
7131
+ hybridCtx.restore();
7132
+ }
7133
+ hybridCtx.restore();
7134
+ }
6846
7135
  const overlayT0 = performance.now();
6847
- const activeTool = this.toolManager.activeTool;
6848
- if (activeTool?.renderOverlay) {
7136
+ if (hybridActive && activeTool?.renderOverlay) {
7137
+ const overlayCtx = this.hybridSurface.getContext(overlayOrder);
7138
+ if (overlayCtx) {
7139
+ overlayCtx.clearRect(0, 0, this.canvasEl.width, this.canvasEl.height);
7140
+ overlayCtx.save();
7141
+ overlayCtx.scale(dpr, dpr);
7142
+ overlayCtx.translate(this.camera.position.x, this.camera.position.y);
7143
+ overlayCtx.scale(this.camera.zoom, this.camera.zoom);
7144
+ activeTool.renderOverlay(overlayCtx);
7145
+ overlayCtx.restore();
7146
+ }
7147
+ } else if (activeTool?.renderOverlay) {
6849
7148
  activeTool.renderOverlay(ctx);
6850
7149
  }
6851
7150
  const overlayMs = performance.now() - overlayT0;
@@ -7596,8 +7895,11 @@ var Viewport = class {
7596
7895
  });
7597
7896
  this.wrapper = createWrapper();
7598
7897
  this.canvasEl = createCanvas();
7898
+ this.paintStack = createPaintStack();
7599
7899
  this.domLayer = createDomLayer();
7900
+ this.domLayer.style.zIndex = "2147483647";
7600
7901
  this.wrapper.appendChild(this.canvasEl);
7902
+ this.wrapper.appendChild(this.paintStack);
7601
7903
  this.wrapper.appendChild(this.domLayer);
7602
7904
  this.container.appendChild(this.wrapper);
7603
7905
  this.toolContext = {
@@ -7667,11 +7969,12 @@ var Viewport = class {
7667
7969
  this.minimap.scheduleDraw();
7668
7970
  }
7669
7971
  this.domNodeManager = new DomNodeManager({
7670
- domLayer: this.domLayer,
7972
+ domLayer: this.paintStack,
7671
7973
  onEditRequest: (id) => this.interactions.startEditingElement(id),
7672
7974
  isEditingElement: (id) => this.noteEditor.isEditing && this.noteEditor.editingElementId === id,
7673
7975
  getVersion: (id) => this.store.getVersion(id)
7674
7976
  });
7977
+ this.domNodeManager.setCameraTransform(this.camera.toCSSTransform());
7675
7978
  this.interactMode = new InteractMode({
7676
7979
  getNode: (id) => this.domNodeManager.getNode(id)
7677
7980
  });
@@ -7692,7 +7995,8 @@ var Viewport = class {
7692
7995
  layerManager: this.layerManager,
7693
7996
  domNodeManager: this.domNodeManager,
7694
7997
  layerCache,
7695
- marginViewport: this.marginViewport
7998
+ marginViewport: this.marginViewport,
7999
+ hybridSurface: new HybridRenderSurface(this.paintStack)
7696
8000
  });
7697
8001
  this.unsubCamera = this.camera.onChange(() => {
7698
8002
  this.applyCameraTransform();
@@ -7779,6 +8083,7 @@ var Viewport = class {
7779
8083
  history;
7780
8084
  domLayer;
7781
8085
  canvasEl;
8086
+ paintStack;
7782
8087
  wrapper;
7783
8088
  unsubCamera;
7784
8089
  unsubToolChange;
@@ -8134,7 +8439,9 @@ var Viewport = class {
8134
8439
  }
8135
8440
  }
8136
8441
  applyCameraTransform() {
8137
- this.domLayer.style.transform = this.camera.toCSSTransform();
8442
+ const transform = this.camera.toCSSTransform();
8443
+ this.domLayer.style.transform = transform;
8444
+ this.domNodeManager.setCameraTransform(transform);
8138
8445
  }
8139
8446
  syncCanvasSize() {
8140
8447
  const rect = this.container.getBoundingClientRect();
@@ -10815,7 +11122,7 @@ var LaserTool = class {
10815
11122
  };
10816
11123
 
10817
11124
  // src/index.ts
10818
- var VERSION = "0.51.0";
11125
+ var VERSION = "0.52.1";
10819
11126
  export {
10820
11127
  ArrowTool,
10821
11128
  AutoSave,