@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/README.md CHANGED
@@ -80,7 +80,8 @@ card.querySelector('button').addEventListener('click', () => {
80
80
  const elementId = viewport.addHtmlElement(card, { x: 100, y: 200 }, { w: 250, h: 150 });
81
81
  ```
82
82
 
83
- HTML elements pan, zoom, and resize with the canvas. They use a **two-mode interaction model**:
83
+ HTML elements pan, zoom, resize, and interleave by layer/`zIndex` with canvas-rendered content. They
84
+ use a **two-mode interaction model**:
84
85
 
85
86
  - **Default** — the element can be selected, dragged, and resized like any other element
86
87
  - **Double-click** — enters interact mode, making buttons, inputs, and links work
@@ -164,9 +165,30 @@ const blob = await viewport.exportImage({
164
165
  });
165
166
  ```
166
167
 
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.
168
+ Remote images are requested with anonymous CORS using their original URLs; failures are omitted from
169
+ the result and reported through `onAssetError` when supplied.
170
+
171
+ Application-owned HTML embeds require an explicit rasterization hook. Return a ready canvas-compatible
172
+ image source; Field Notes applies the element's size, rotation, paint order, and layer opacity in both
173
+ PNG and SVG exports:
174
+
175
+ ```typescript
176
+ const options = {
177
+ htmlTimeoutMs: 10_000,
178
+ renderHtml: async (element) => {
179
+ const node = document.querySelector(`[data-element-id="${element.id}"]`);
180
+ return node ? rasterizeToCanvas(node) : null; // application or library implementation
181
+ },
182
+ onHtmlError: ({ elementId, reason }) => {
183
+ console.warn(`Could not export HTML element ${elementId}: ${reason}`);
184
+ },
185
+ };
186
+
187
+ const png = await viewport.exportImage(options);
188
+ const svg = await viewport.exportSVG(options);
189
+ ```
190
+
191
+ Without `renderHtml`, embeds remain omitted and can be observed through `onHtmlError`.
170
192
 
171
193
  ## Performance Monitoring
172
194
 
package/dist/index.cjs CHANGED
@@ -4921,6 +4921,57 @@ function createDomLayer() {
4921
4921
  });
4922
4922
  return el;
4923
4923
  }
4924
+ function createPaintStack() {
4925
+ const el = createDomLayer();
4926
+ el.dataset["fieldnotesPaintStack"] = "";
4927
+ el.style.transformOrigin = "";
4928
+ return el;
4929
+ }
4930
+
4931
+ // src/canvas/hybrid-render-surface.ts
4932
+ var HybridRenderSurface = class {
4933
+ constructor(root) {
4934
+ this.root = root;
4935
+ }
4936
+ canvases = /* @__PURE__ */ new Map();
4937
+ beginFrame(activeOrders, width, height) {
4938
+ for (const [order, canvas] of this.canvases) {
4939
+ if (!activeOrders.has(order)) {
4940
+ canvas.remove();
4941
+ this.canvases.delete(order);
4942
+ }
4943
+ }
4944
+ for (const order of activeOrders) {
4945
+ const canvas = this.getCanvas(order);
4946
+ if (canvas.width !== width) canvas.width = width;
4947
+ if (canvas.height !== height) canvas.height = height;
4948
+ canvas.style.zIndex = String(order);
4949
+ }
4950
+ }
4951
+ getContext(order) {
4952
+ return this.getCanvas(order).getContext("2d");
4953
+ }
4954
+ clear() {
4955
+ for (const canvas of this.canvases.values()) canvas.remove();
4956
+ this.canvases.clear();
4957
+ }
4958
+ getCanvas(order) {
4959
+ let canvas = this.canvases.get(order);
4960
+ if (canvas) return canvas;
4961
+ canvas = document.createElement("canvas");
4962
+ canvas.dataset["paintOrder"] = String(order);
4963
+ Object.assign(canvas.style, {
4964
+ position: "absolute",
4965
+ inset: "0",
4966
+ width: "100%",
4967
+ height: "100%",
4968
+ pointerEvents: "none"
4969
+ });
4970
+ this.root.appendChild(canvas);
4971
+ this.canvases.set(order, canvas);
4972
+ return canvas;
4973
+ }
4974
+ };
4924
4975
 
4925
4976
  // src/elements/arrow-label-editor.ts
4926
4977
  var ArrowLabelEditor = class {
@@ -5442,6 +5493,67 @@ function renderTextOnCanvas(ctx, text) {
5442
5493
  ctx.restore();
5443
5494
  }
5444
5495
 
5496
+ // src/canvas/html-export.ts
5497
+ var DEFAULT_HTML_TIMEOUT_MS = 1e4;
5498
+ var TIMEOUT = /* @__PURE__ */ Symbol("html-export-timeout");
5499
+ function validateHtmlExportOptions(options) {
5500
+ const timeoutMs = options.htmlTimeoutMs ?? DEFAULT_HTML_TIMEOUT_MS;
5501
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
5502
+ throw new RangeError("htmlTimeoutMs must be a finite number greater than 0");
5503
+ }
5504
+ return timeoutMs;
5505
+ }
5506
+ async function renderHtmlElements(elements, options) {
5507
+ const timeoutMs = validateHtmlExportOptions(options);
5508
+ const sources = /* @__PURE__ */ new Map();
5509
+ await Promise.all(
5510
+ elements.map(async (element) => {
5511
+ if (!options.renderHtml) {
5512
+ options.onHtmlError?.({
5513
+ elementId: element.id,
5514
+ htmlType: element.htmlType,
5515
+ reason: "unsupported"
5516
+ });
5517
+ return;
5518
+ }
5519
+ let timer;
5520
+ try {
5521
+ const timeout = new Promise((resolve) => {
5522
+ timer = setTimeout(() => resolve(TIMEOUT), timeoutMs);
5523
+ });
5524
+ const result = await Promise.race([Promise.resolve(options.renderHtml(element)), timeout]);
5525
+ if (result === TIMEOUT) {
5526
+ options.onHtmlError?.({
5527
+ elementId: element.id,
5528
+ htmlType: element.htmlType,
5529
+ reason: "timeout"
5530
+ });
5531
+ return;
5532
+ }
5533
+ if (!result) {
5534
+ options.onHtmlError?.({
5535
+ elementId: element.id,
5536
+ htmlType: element.htmlType,
5537
+ reason: "render"
5538
+ });
5539
+ return;
5540
+ }
5541
+ sources.set(element.id, result);
5542
+ } catch (cause) {
5543
+ options.onHtmlError?.({
5544
+ elementId: element.id,
5545
+ htmlType: element.htmlType,
5546
+ reason: "render",
5547
+ cause
5548
+ });
5549
+ } finally {
5550
+ if (timer !== void 0) clearTimeout(timer);
5551
+ }
5552
+ })
5553
+ );
5554
+ return sources;
5555
+ }
5556
+
5445
5557
  // src/canvas/export-image.ts
5446
5558
  var DEFAULT_IMAGE_TIMEOUT_MS = 1e4;
5447
5559
  var DEFAULT_MAX_DIMENSION = 16384;
@@ -5646,6 +5758,7 @@ async function exportImage(store, options = {}, layerManager) {
5646
5758
  const scale = positiveOption(options.scale, 2, "scale");
5647
5759
  const padding = nonNegativeOption(options.padding, 0, "padding");
5648
5760
  validateExportResourceOptions(options);
5761
+ validateHtmlExportOptions(options);
5649
5762
  const background = options.background ?? "#ffffff";
5650
5763
  const filter = options.filter;
5651
5764
  const allElements = store.getAll();
@@ -5659,6 +5772,8 @@ async function exportImage(store, options = {}, layerManager) {
5659
5772
  const height = Math.ceil(bounds.h * scale);
5660
5773
  assertExportSize(width, height, options);
5661
5774
  const imageCache = await loadImages(visibleElements, options);
5775
+ const htmlElements = visibleElements.filter((el) => el.type === "html");
5776
+ const htmlSources = await renderHtmlElements(htmlElements, options);
5662
5777
  const canvas = document.createElement("canvas");
5663
5778
  canvas.width = width;
5664
5779
  canvas.height = height;
@@ -5683,6 +5798,24 @@ async function exportImage(store, options = {}, layerManager) {
5683
5798
  return;
5684
5799
  }
5685
5800
  if (el.type === "html") {
5801
+ const source = htmlSources.get(el.id);
5802
+ if (!source) return;
5803
+ const b = getElementBounds(el);
5804
+ try {
5805
+ withRotation(
5806
+ target,
5807
+ el,
5808
+ b ? center(b) : el.position,
5809
+ () => target.drawImage(source, el.position.x, el.position.y, el.size.w, el.size.h)
5810
+ );
5811
+ } catch (cause) {
5812
+ options.onHtmlError?.({
5813
+ elementId: el.id,
5814
+ htmlType: el.htmlType,
5815
+ reason: "render",
5816
+ cause
5817
+ });
5818
+ }
5686
5819
  return;
5687
5820
  }
5688
5821
  if (el.type === "image") {
@@ -5838,7 +5971,7 @@ function emitArrow(arrow, store) {
5838
5971
  return out;
5839
5972
  }
5840
5973
  function emitImage(image, dataUri) {
5841
- const href = dataUri ?? image.src;
5974
+ const href = dataUri ?? ("src" in image ? image.src : "");
5842
5975
  if (!href) return "";
5843
5976
  const { x, y } = image.position;
5844
5977
  const { w, h } = image.size;
@@ -6042,6 +6175,7 @@ async function exportSvg(store, options = {}, layerManager) {
6042
6175
  const padding = nonNegativeOption(options.padding, 0, "padding");
6043
6176
  const rasterScale = positiveOption(options.rasterScale, 2, "rasterScale");
6044
6177
  validateExportResourceOptions(options);
6178
+ validateHtmlExportOptions(options);
6045
6179
  const filter = options.filter;
6046
6180
  const allElements = store.getAll();
6047
6181
  let visibleElements = layerManager ? allElements.filter((el) => layerManager.isLayerVisible(el.layerId)) : allElements;
@@ -6056,6 +6190,9 @@ async function exportSvg(store, options = {}, layerManager) {
6056
6190
  );
6057
6191
  const imageCache = await loadImages(remoteImages, options);
6058
6192
  const imageDataUris = encodeImages(visibleElements, imageCache, rasterScale, options);
6193
+ const htmlElements = visibleElements.filter((el) => el.type === "html");
6194
+ const htmlSources = await renderHtmlElements(htmlElements, options);
6195
+ const htmlDataUris = encodeHtmlElements(htmlElements, htmlSources, rasterScale, options);
6059
6196
  const grids = visibleElements.filter((el) => el.type === "grid");
6060
6197
  const firstGrid = grids[0];
6061
6198
  let body = "";
@@ -6064,7 +6201,15 @@ async function exportSvg(store, options = {}, layerManager) {
6064
6201
  }
6065
6202
  const layerBodies = /* @__PURE__ */ new Map();
6066
6203
  for (const el of visibleElements) {
6067
- const emitted = emitElement(el, imageDataUris, rasterScale, firstGrid, store, options);
6204
+ const emitted = emitElement(
6205
+ el,
6206
+ imageDataUris,
6207
+ htmlDataUris,
6208
+ rasterScale,
6209
+ firstGrid,
6210
+ store,
6211
+ options
6212
+ );
6068
6213
  layerBodies.set(el.layerId, (layerBodies.get(el.layerId) ?? "") + emitted);
6069
6214
  }
6070
6215
  for (const [layerId, emitted] of layerBodies) {
@@ -6078,7 +6223,7 @@ async function exportSvg(store, options = {}, layerManager) {
6078
6223
  }
6079
6224
  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
6225
  }
6081
- function emitElement(el, imageDataUris, rasterScale, firstGrid, store, resourceOptions) {
6226
+ function emitElement(el, imageDataUris, htmlDataUris, rasterScale, firstGrid, store, resourceOptions) {
6082
6227
  switch (el.type) {
6083
6228
  case "stroke":
6084
6229
  return withRotationSvg(el, emitStroke(el));
@@ -6097,11 +6242,62 @@ function emitElement(el, imageDataUris, rasterScale, firstGrid, store, resourceO
6097
6242
  case "grid":
6098
6243
  return "";
6099
6244
  case "html":
6100
- return "";
6245
+ return withRotationSvg(el, emitImage(el, htmlDataUris.get(el.id)));
6101
6246
  default:
6102
6247
  return "";
6103
6248
  }
6104
6249
  }
6250
+ function encodeHtmlElements(elements, sources, rasterScale, options) {
6251
+ const encoded = /* @__PURE__ */ new Map();
6252
+ for (const element of elements) {
6253
+ const source = sources.get(element.id);
6254
+ if (!source) continue;
6255
+ const width = Math.max(1, Math.ceil(element.size.w * rasterScale));
6256
+ const height = Math.max(1, Math.ceil(element.size.h * rasterScale));
6257
+ assertExportSize(width, height, options);
6258
+ if (typeof document === "undefined") {
6259
+ options.onHtmlError?.({
6260
+ elementId: element.id,
6261
+ htmlType: element.htmlType,
6262
+ reason: "encode"
6263
+ });
6264
+ continue;
6265
+ }
6266
+ const canvas = document.createElement("canvas");
6267
+ canvas.width = width;
6268
+ canvas.height = height;
6269
+ const ctx = canvas.getContext("2d");
6270
+ if (!ctx) {
6271
+ options.onHtmlError?.({
6272
+ elementId: element.id,
6273
+ htmlType: element.htmlType,
6274
+ reason: "encode"
6275
+ });
6276
+ continue;
6277
+ }
6278
+ try {
6279
+ ctx.drawImage(source, 0, 0, width, height);
6280
+ const dataUri = canvas.toDataURL();
6281
+ if (dataUri.startsWith("data:")) {
6282
+ encoded.set(element.id, dataUri);
6283
+ } else {
6284
+ options.onHtmlError?.({
6285
+ elementId: element.id,
6286
+ htmlType: element.htmlType,
6287
+ reason: "encode"
6288
+ });
6289
+ }
6290
+ } catch (cause) {
6291
+ options.onHtmlError?.({
6292
+ elementId: element.id,
6293
+ htmlType: element.htmlType,
6294
+ reason: "encode",
6295
+ cause
6296
+ });
6297
+ }
6298
+ }
6299
+ return encoded;
6300
+ }
6105
6301
  function encodeImages(elements, imageCache, rasterScale, resourceOptions) {
6106
6302
  const out = /* @__PURE__ */ new Map();
6107
6303
  for (const el of elements) {
@@ -6394,6 +6590,7 @@ var DoubleTapDetector = class {
6394
6590
  // src/canvas/dom-node-manager.ts
6395
6591
  var DomNodeManager = class {
6396
6592
  domNodes = /* @__PURE__ */ new Map();
6593
+ strata = /* @__PURE__ */ new Map();
6397
6594
  htmlContent = /* @__PURE__ */ new Map();
6398
6595
  domLayer;
6399
6596
  onEditRequest;
@@ -6402,6 +6599,7 @@ var DomNodeManager = class {
6402
6599
  lastSyncedVersion = /* @__PURE__ */ new Map();
6403
6600
  lastSyncedZIndex = /* @__PURE__ */ new Map();
6404
6601
  lastSyncedOpacity = /* @__PURE__ */ new Map();
6602
+ cameraTransform = "";
6405
6603
  constructor(deps) {
6406
6604
  this.domLayer = deps.domLayer;
6407
6605
  this.onEditRequest = deps.onEditRequest;
@@ -6411,6 +6609,11 @@ var DomNodeManager = class {
6411
6609
  getNode(id) {
6412
6610
  return this.domNodes.get(id);
6413
6611
  }
6612
+ setCameraTransform(transform) {
6613
+ if (transform === this.cameraTransform) return;
6614
+ this.cameraTransform = transform;
6615
+ for (const stratum of this.strata.values()) stratum.style.transform = transform;
6616
+ }
6414
6617
  storeHtmlContent(elementId, dom) {
6415
6618
  this.htmlContent.set(elementId, dom);
6416
6619
  }
@@ -6438,7 +6641,7 @@ var DomNodeManager = class {
6438
6641
  position: "absolute",
6439
6642
  pointerEvents: "auto"
6440
6643
  });
6441
- this.domLayer.appendChild(node);
6644
+ this.getStratum(zIndex).appendChild(node);
6442
6645
  this.domNodes.set(element.id, node);
6443
6646
  } else if (this.getVersion) {
6444
6647
  const currentVersion = this.getVersion(element.id);
@@ -6449,6 +6652,15 @@ var DomNodeManager = class {
6449
6652
  return;
6450
6653
  }
6451
6654
  }
6655
+ if (node.parentElement !== this.getStratum(zIndex)) {
6656
+ const previousStratum = node.parentElement;
6657
+ this.getStratum(zIndex).appendChild(node);
6658
+ if (previousStratum?.childElementCount === 0) {
6659
+ const previousOrder = Number(previousStratum.dataset["paintOrder"]);
6660
+ previousStratum.remove();
6661
+ this.strata.delete(previousOrder);
6662
+ }
6663
+ }
6452
6664
  if (this.getVersion) {
6453
6665
  this.lastSyncedVersion.set(element.id, this.getVersion(element.id));
6454
6666
  this.lastSyncedZIndex.set(element.id, zIndex);
@@ -6470,7 +6682,10 @@ var DomNodeManager = class {
6470
6682
  }
6471
6683
  hideDomNode(id) {
6472
6684
  const node = this.domNodes.get(id);
6473
- if (node) node.style.display = "none";
6685
+ if (node) {
6686
+ node.style.display = "none";
6687
+ this.lastSyncedVersion.delete(id);
6688
+ }
6474
6689
  }
6475
6690
  removeDomNode(id) {
6476
6691
  this.htmlContent.delete(id);
@@ -6479,8 +6694,14 @@ var DomNodeManager = class {
6479
6694
  this.lastSyncedOpacity.delete(id);
6480
6695
  const node = this.domNodes.get(id);
6481
6696
  if (node) {
6697
+ const stratum = node.parentElement;
6482
6698
  node.remove();
6483
6699
  this.domNodes.delete(id);
6700
+ if (stratum?.childElementCount === 0) {
6701
+ const order = Number(stratum.dataset["paintOrder"]);
6702
+ stratum.remove();
6703
+ this.strata.delete(order);
6704
+ }
6484
6705
  }
6485
6706
  }
6486
6707
  clearDomNodes() {
@@ -6490,6 +6711,27 @@ var DomNodeManager = class {
6490
6711
  this.lastSyncedVersion.clear();
6491
6712
  this.lastSyncedZIndex.clear();
6492
6713
  this.lastSyncedOpacity.clear();
6714
+ for (const stratum of this.strata.values()) stratum.remove();
6715
+ this.strata.clear();
6716
+ }
6717
+ getStratum(order) {
6718
+ let stratum = this.strata.get(order);
6719
+ if (stratum) return stratum;
6720
+ stratum = document.createElement("div");
6721
+ stratum.dataset["paintOrder"] = String(order);
6722
+ Object.assign(stratum.style, {
6723
+ position: "absolute",
6724
+ inset: "0",
6725
+ width: "100%",
6726
+ height: "100%",
6727
+ pointerEvents: "none",
6728
+ transformOrigin: "0 0",
6729
+ transform: this.cameraTransform,
6730
+ zIndex: String(order)
6731
+ });
6732
+ this.domLayer.appendChild(stratum);
6733
+ this.strata.set(order, stratum);
6734
+ return stratum;
6493
6735
  }
6494
6736
  reattachHtmlContent(store) {
6495
6737
  for (const el of store.getElementsByType("html")) {
@@ -6671,6 +6913,7 @@ var RenderLoop = class {
6671
6913
  domNodeManager;
6672
6914
  layerCache;
6673
6915
  marginViewport;
6916
+ hybridSurface;
6674
6917
  activeDrawingLayerId = null;
6675
6918
  gridCacheDirty = true;
6676
6919
  // set on recenter/viewport-change; consumed by the grid block
@@ -6690,6 +6933,7 @@ var RenderLoop = class {
6690
6933
  this.domNodeManager = deps.domNodeManager;
6691
6934
  this.layerCache = deps.layerCache;
6692
6935
  this.marginViewport = deps.marginViewport;
6936
+ this.hybridSurface = deps.hybridSurface;
6693
6937
  }
6694
6938
  requestRender() {
6695
6939
  this.needsRender = true;
@@ -6810,9 +7054,19 @@ var RenderLoop = class {
6810
7054
  h: cullBounds.h + cullPad * 2
6811
7055
  };
6812
7056
  const allElements = this.store.getAll();
7057
+ const visibleElements = allElements.filter(
7058
+ (element) => this.layerManager.isLayerVisible(element.layerId)
7059
+ );
7060
+ const firstDomIndex = visibleElements.findIndex(
7061
+ (element) => this.renderer.isDomElement(element)
7062
+ );
7063
+ const hybridActive = firstDomIndex >= 0 && visibleElements.slice(firstDomIndex + 1).some((element) => !this.renderer.isDomElement(element));
7064
+ const hybridCanvasRuns = /* @__PURE__ */ new Map();
7065
+ const hybridOrders = /* @__PURE__ */ new Set();
7066
+ let activeHybridOrder = null;
6813
7067
  this.layerGroups.clear();
6814
7068
  const gridElements = [];
6815
- let domZIndex = 0;
7069
+ let paintOrder = 0;
6816
7070
  for (const element of allElements) {
6817
7071
  if (!this.layerManager.isLayerVisible(element.layerId)) {
6818
7072
  if (this.renderer.isDomElement(element)) {
@@ -6820,16 +7074,29 @@ var RenderLoop = class {
6820
7074
  }
6821
7075
  continue;
6822
7076
  }
7077
+ const order = ++paintOrder;
6823
7078
  if (this.renderer.isDomElement(element)) {
7079
+ activeHybridOrder = null;
6824
7080
  const layerOpacity = this.layerManager.getLayer?.(element.layerId)?.opacity ?? 1;
6825
7081
  const elBounds = getElementBounds(element);
6826
7082
  if (elBounds && !boundsIntersect(elBounds, cullingRect)) {
6827
7083
  this.domNodeManager.hideDomNode(element.id);
6828
7084
  } else {
6829
- this.domNodeManager.syncDomNode(element, domZIndex++, layerOpacity);
7085
+ this.domNodeManager.syncDomNode(element, order, layerOpacity);
6830
7086
  }
6831
7087
  continue;
6832
7088
  }
7089
+ if (hybridActive && paintOrder > firstDomIndex + 1 && element.type !== "grid") {
7090
+ activeHybridOrder ??= order;
7091
+ let run = hybridCanvasRuns.get(activeHybridOrder);
7092
+ if (!run) {
7093
+ run = [];
7094
+ hybridCanvasRuns.set(activeHybridOrder, run);
7095
+ hybridOrders.add(activeHybridOrder);
7096
+ }
7097
+ run.push(element);
7098
+ continue;
7099
+ }
6833
7100
  if (element.type === "grid") {
6834
7101
  gridElements.push(element);
6835
7102
  continue;
@@ -6841,6 +7108,10 @@ var RenderLoop = class {
6841
7108
  }
6842
7109
  group.push(element);
6843
7110
  }
7111
+ const activeTool = this.toolManager.activeTool;
7112
+ const overlayOrder = visibleElements.length + 1;
7113
+ if (hybridActive && activeTool?.renderOverlay) hybridOrders.add(overlayOrder);
7114
+ this.hybridSurface.beginFrame(hybridOrders, this.canvasEl.width, this.canvasEl.height);
6844
7115
  for (const [layerId, elements] of this.layerGroups) {
6845
7116
  const isActiveDrawingLayer = layerId === this.activeDrawingLayerId;
6846
7117
  const layerOpacity = this.layerManager.getLayer?.(layerId)?.opacity ?? 1;
@@ -6930,9 +7201,37 @@ var RenderLoop = class {
6930
7201
  } else {
6931
7202
  this.lastGridRefs = [];
6932
7203
  }
7204
+ for (const [order, elements] of hybridCanvasRuns) {
7205
+ const hybridCtx = this.hybridSurface.getContext(order);
7206
+ if (!hybridCtx) continue;
7207
+ hybridCtx.clearRect(0, 0, this.canvasEl.width, this.canvasEl.height);
7208
+ hybridCtx.save();
7209
+ hybridCtx.scale(dpr, dpr);
7210
+ hybridCtx.translate(this.camera.position.x, this.camera.position.y);
7211
+ hybridCtx.scale(this.camera.zoom, this.camera.zoom);
7212
+ for (const element of elements) {
7213
+ const elBounds = getElementBounds(element);
7214
+ if (elBounds && !boundsIntersect(elBounds, cullingRect)) continue;
7215
+ hybridCtx.save();
7216
+ hybridCtx.globalAlpha = this.layerManager.getLayer?.(element.layerId)?.opacity ?? 1;
7217
+ this.renderer.renderCanvasElement(hybridCtx, element);
7218
+ hybridCtx.restore();
7219
+ }
7220
+ hybridCtx.restore();
7221
+ }
6933
7222
  const overlayT0 = performance.now();
6934
- const activeTool = this.toolManager.activeTool;
6935
- if (activeTool?.renderOverlay) {
7223
+ if (hybridActive && activeTool?.renderOverlay) {
7224
+ const overlayCtx = this.hybridSurface.getContext(overlayOrder);
7225
+ if (overlayCtx) {
7226
+ overlayCtx.clearRect(0, 0, this.canvasEl.width, this.canvasEl.height);
7227
+ overlayCtx.save();
7228
+ overlayCtx.scale(dpr, dpr);
7229
+ overlayCtx.translate(this.camera.position.x, this.camera.position.y);
7230
+ overlayCtx.scale(this.camera.zoom, this.camera.zoom);
7231
+ activeTool.renderOverlay(overlayCtx);
7232
+ overlayCtx.restore();
7233
+ }
7234
+ } else if (activeTool?.renderOverlay) {
6936
7235
  activeTool.renderOverlay(ctx);
6937
7236
  }
6938
7237
  const overlayMs = performance.now() - overlayT0;
@@ -7683,8 +7982,11 @@ var Viewport = class {
7683
7982
  });
7684
7983
  this.wrapper = createWrapper();
7685
7984
  this.canvasEl = createCanvas();
7985
+ this.paintStack = createPaintStack();
7686
7986
  this.domLayer = createDomLayer();
7987
+ this.domLayer.style.zIndex = "2147483647";
7687
7988
  this.wrapper.appendChild(this.canvasEl);
7989
+ this.wrapper.appendChild(this.paintStack);
7688
7990
  this.wrapper.appendChild(this.domLayer);
7689
7991
  this.container.appendChild(this.wrapper);
7690
7992
  this.toolContext = {
@@ -7754,11 +8056,12 @@ var Viewport = class {
7754
8056
  this.minimap.scheduleDraw();
7755
8057
  }
7756
8058
  this.domNodeManager = new DomNodeManager({
7757
- domLayer: this.domLayer,
8059
+ domLayer: this.paintStack,
7758
8060
  onEditRequest: (id) => this.interactions.startEditingElement(id),
7759
8061
  isEditingElement: (id) => this.noteEditor.isEditing && this.noteEditor.editingElementId === id,
7760
8062
  getVersion: (id) => this.store.getVersion(id)
7761
8063
  });
8064
+ this.domNodeManager.setCameraTransform(this.camera.toCSSTransform());
7762
8065
  this.interactMode = new InteractMode({
7763
8066
  getNode: (id) => this.domNodeManager.getNode(id)
7764
8067
  });
@@ -7779,7 +8082,8 @@ var Viewport = class {
7779
8082
  layerManager: this.layerManager,
7780
8083
  domNodeManager: this.domNodeManager,
7781
8084
  layerCache,
7782
- marginViewport: this.marginViewport
8085
+ marginViewport: this.marginViewport,
8086
+ hybridSurface: new HybridRenderSurface(this.paintStack)
7783
8087
  });
7784
8088
  this.unsubCamera = this.camera.onChange(() => {
7785
8089
  this.applyCameraTransform();
@@ -7866,6 +8170,7 @@ var Viewport = class {
7866
8170
  history;
7867
8171
  domLayer;
7868
8172
  canvasEl;
8173
+ paintStack;
7869
8174
  wrapper;
7870
8175
  unsubCamera;
7871
8176
  unsubToolChange;
@@ -8221,7 +8526,9 @@ var Viewport = class {
8221
8526
  }
8222
8527
  }
8223
8528
  applyCameraTransform() {
8224
- this.domLayer.style.transform = this.camera.toCSSTransform();
8529
+ const transform = this.camera.toCSSTransform();
8530
+ this.domLayer.style.transform = transform;
8531
+ this.domNodeManager.setCameraTransform(transform);
8225
8532
  }
8226
8533
  syncCanvasSize() {
8227
8534
  const rect = this.container.getBoundingClientRect();
@@ -10902,7 +11209,7 @@ var LaserTool = class {
10902
11209
  };
10903
11210
 
10904
11211
  // src/index.ts
10905
- var VERSION = "0.51.0";
11212
+ var VERSION = "0.52.1";
10906
11213
  // Annotate the CommonJS export names for ESM import in node:
10907
11214
  0 && (module.exports = {
10908
11215
  ArrowTool,