@fieldnotes/core 0.22.0 → 0.23.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
@@ -603,6 +603,8 @@ interface ViewportOptions {
603
603
  src: string;
604
604
  elementIds: string[];
605
605
  }) => void;
606
+ /** CSS-pixel margin cached beyond the viewport. Default `256`. Set `0` to disable. */
607
+ panBufferMargin?: number;
606
608
  }
607
609
  declare class Viewport {
608
610
  private readonly container;
@@ -622,6 +624,7 @@ declare class Viewport {
622
624
  private readonly noteEditor;
623
625
  private readonly historyRecorder;
624
626
  readonly toolContext: ToolContext;
627
+ private readonly marginViewport;
625
628
  private resizeObserver;
626
629
  private _snapToGrid;
627
630
  private readonly _gridSize;
@@ -708,11 +711,18 @@ declare class ElementRenderer {
708
711
  private canvasSize;
709
712
  private hexTileCache;
710
713
  private hexTileCacheKey;
714
+ private gridBoundsOverride;
711
715
  setStore(store: ElementStore): void;
712
716
  setOnImageLoad(callback: () => void): void;
713
717
  setOnImageError(callback: (src: string) => void): void;
714
718
  setCamera(camera: Camera): void;
715
719
  setCanvasSize(w: number, h: number): void;
720
+ setGridBoundsOverride(bounds: {
721
+ minX: number;
722
+ minY: number;
723
+ maxX: number;
724
+ maxY: number;
725
+ } | null): void;
716
726
  isDomElement(element: CanvasElement): boolean;
717
727
  renderCanvasElement(ctx: CanvasRenderingContext2D, element: CanvasElement): void;
718
728
  private renderStroke;
@@ -1251,6 +1261,6 @@ declare class UpdateLayerCommand implements Command {
1251
1261
  undo(_store: ElementStore): void;
1252
1262
  }
1253
1263
 
1254
- declare const VERSION = "0.22.0";
1264
+ declare const VERSION = "0.23.0";
1255
1265
 
1256
1266
  export { type ActiveFormats, AddElementCommand, type ArrowElement, ArrowTool, type ArrowToolOptions, AutoSave, type AutoSaveOptions, Background, type BackgroundOptions, type BackgroundPattern, BatchCommand, type Binding, type Bounds, Camera, type CameraChangeInfo, type CameraOptions, type CanvasElement, type CanvasState, type Command, CreateLayerCommand, DEFAULT_FONT_SIZE_PRESETS, DEFAULT_NOTE_FONT_SIZE, DoubleTapDetector, type DoubleTapDetectorOptions, ElementRenderer, ElementStore, type ElementType, type ElementUpdateEvent, EraserTool, type EraserToolOptions, EventBus, type ExportImageOptions, type FilterAction, type FilteredEvent, type FilteredUpEvent, type FontSizePreset, type GridElement, type GridInfo, HandTool, type HexOrientation, HistoryRecorder, HistoryStack, type HistoryStackOptions, type HtmlElement, type ImageElement, ImageTool, type ImageToolOptions, InputFilter, InputHandler, type InputHandlerOptions, type Layer, LayerManager, MeasureTool, type MeasureToolOptions, type Measurement, NoteEditor, type NoteEditorOptions, type NoteElement, NoteTool, type NoteToolOptions, NoteToolbar, PencilTool, type PencilToolOptions, type Point, type PointerState, Quadtree, RemoveElementCommand, RemoveLayerCommand, type RenderStatsSnapshot, SelectTool, type ShapeElement, type ShapeKind, ShapeTool, type ShapeToolOptions, type ShortcutBindings, type ShortcutOptions, type ShortcutsApi, type Size, type StrokeElement, type StrokePoint, type StyledRun, type TemplateElement, type TemplateShape, TemplateTool, type TemplateToolOptions, type TextElement, TextTool, type TextToolOptions, type Tool, type ToolContext, ToolManager, type ToolName, UpdateElementCommand, UpdateLayerCommand, VERSION, Viewport, type ViewportOptions, boundsIntersect, clearStaleBindings, createArrow, createGrid, createHtmlElement, createId, createImage, createNote, createShape, createStroke, createTemplate, createText, drawHexPath, exportImage, exportState, findBindTarget, findBoundArrows, getActiveFormats, getArrowBounds, getArrowControlPoint, getArrowMidpoint, getArrowTangentAngle, getBendFromPoint, getEdgeIntersection, getElementBounds, getElementCenter, getElementsBoundingBox, getHexCellsInCone, getHexCellsInLine, getHexCellsInRadius, getHexCellsInSquare, getHexDistance, isBindable, isNearBezier, isNoteContentEmpty, parseState, sanitizeNoteHtml, setFontSize, smartSnap, snapPoint, snapToHexCenter, toggleBold, toggleItalic, toggleStrikethrough, toggleUnderline, unbindArrow, updateBoundArrow };
package/dist/index.d.ts CHANGED
@@ -603,6 +603,8 @@ interface ViewportOptions {
603
603
  src: string;
604
604
  elementIds: string[];
605
605
  }) => void;
606
+ /** CSS-pixel margin cached beyond the viewport. Default `256`. Set `0` to disable. */
607
+ panBufferMargin?: number;
606
608
  }
607
609
  declare class Viewport {
608
610
  private readonly container;
@@ -622,6 +624,7 @@ declare class Viewport {
622
624
  private readonly noteEditor;
623
625
  private readonly historyRecorder;
624
626
  readonly toolContext: ToolContext;
627
+ private readonly marginViewport;
625
628
  private resizeObserver;
626
629
  private _snapToGrid;
627
630
  private readonly _gridSize;
@@ -708,11 +711,18 @@ declare class ElementRenderer {
708
711
  private canvasSize;
709
712
  private hexTileCache;
710
713
  private hexTileCacheKey;
714
+ private gridBoundsOverride;
711
715
  setStore(store: ElementStore): void;
712
716
  setOnImageLoad(callback: () => void): void;
713
717
  setOnImageError(callback: (src: string) => void): void;
714
718
  setCamera(camera: Camera): void;
715
719
  setCanvasSize(w: number, h: number): void;
720
+ setGridBoundsOverride(bounds: {
721
+ minX: number;
722
+ minY: number;
723
+ maxX: number;
724
+ maxY: number;
725
+ } | null): void;
716
726
  isDomElement(element: CanvasElement): boolean;
717
727
  renderCanvasElement(ctx: CanvasRenderingContext2D, element: CanvasElement): void;
718
728
  private renderStroke;
@@ -1251,6 +1261,6 @@ declare class UpdateLayerCommand implements Command {
1251
1261
  undo(_store: ElementStore): void;
1252
1262
  }
1253
1263
 
1254
- declare const VERSION = "0.22.0";
1264
+ declare const VERSION = "0.23.0";
1255
1265
 
1256
1266
  export { type ActiveFormats, AddElementCommand, type ArrowElement, ArrowTool, type ArrowToolOptions, AutoSave, type AutoSaveOptions, Background, type BackgroundOptions, type BackgroundPattern, BatchCommand, type Binding, type Bounds, Camera, type CameraChangeInfo, type CameraOptions, type CanvasElement, type CanvasState, type Command, CreateLayerCommand, DEFAULT_FONT_SIZE_PRESETS, DEFAULT_NOTE_FONT_SIZE, DoubleTapDetector, type DoubleTapDetectorOptions, ElementRenderer, ElementStore, type ElementType, type ElementUpdateEvent, EraserTool, type EraserToolOptions, EventBus, type ExportImageOptions, type FilterAction, type FilteredEvent, type FilteredUpEvent, type FontSizePreset, type GridElement, type GridInfo, HandTool, type HexOrientation, HistoryRecorder, HistoryStack, type HistoryStackOptions, type HtmlElement, type ImageElement, ImageTool, type ImageToolOptions, InputFilter, InputHandler, type InputHandlerOptions, type Layer, LayerManager, MeasureTool, type MeasureToolOptions, type Measurement, NoteEditor, type NoteEditorOptions, type NoteElement, NoteTool, type NoteToolOptions, NoteToolbar, PencilTool, type PencilToolOptions, type Point, type PointerState, Quadtree, RemoveElementCommand, RemoveLayerCommand, type RenderStatsSnapshot, SelectTool, type ShapeElement, type ShapeKind, ShapeTool, type ShapeToolOptions, type ShortcutBindings, type ShortcutOptions, type ShortcutsApi, type Size, type StrokeElement, type StrokePoint, type StyledRun, type TemplateElement, type TemplateShape, TemplateTool, type TemplateToolOptions, type TextElement, TextTool, type TextToolOptions, type Tool, type ToolContext, ToolManager, type ToolName, UpdateElementCommand, UpdateLayerCommand, VERSION, Viewport, type ViewportOptions, boundsIntersect, clearStaleBindings, createArrow, createGrid, createHtmlElement, createId, createImage, createNote, createShape, createStroke, createTemplate, createText, drawHexPath, exportImage, exportState, findBindTarget, findBoundArrows, getActiveFormats, getArrowBounds, getArrowControlPoint, getArrowMidpoint, getArrowTangentAngle, getBendFromPoint, getEdgeIntersection, getElementBounds, getElementCenter, getElementsBoundingBox, getHexCellsInCone, getHexCellsInLine, getHexCellsInRadius, getHexCellsInSquare, getHexDistance, isBindable, isNearBezier, isNoteContentEmpty, parseState, sanitizeNoteHtml, setFontSize, smartSnap, snapPoint, snapToHexCenter, toggleBold, toggleItalic, toggleStrikethrough, toggleUnderline, unbindArrow, updateBoundArrow };
package/dist/index.js CHANGED
@@ -2670,6 +2670,7 @@ var ElementRenderer = class {
2670
2670
  canvasSize = null;
2671
2671
  hexTileCache = null;
2672
2672
  hexTileCacheKey = "";
2673
+ gridBoundsOverride = null;
2673
2674
  setStore(store) {
2674
2675
  this.store = store;
2675
2676
  }
@@ -2685,6 +2686,9 @@ var ElementRenderer = class {
2685
2686
  setCanvasSize(w, h) {
2686
2687
  this.canvasSize = { w, h };
2687
2688
  }
2689
+ setGridBoundsOverride(bounds) {
2690
+ this.gridBoundsOverride = bounds;
2691
+ }
2688
2692
  isDomElement(element) {
2689
2693
  return DOM_ELEMENT_TYPES.has(element.type);
2690
2694
  }
@@ -2855,20 +2859,20 @@ var ElementRenderer = class {
2855
2859
  }
2856
2860
  }
2857
2861
  renderGrid(ctx, grid) {
2858
- if (!this.canvasSize) return;
2862
+ const canvasSize = this.canvasSize;
2863
+ if (!canvasSize) return;
2859
2864
  const cam = this.camera;
2860
2865
  if (!cam) return;
2861
- const topLeft = cam.screenToWorld({ x: 0, y: 0 });
2862
- const bottomRight = cam.screenToWorld({
2863
- x: this.canvasSize.w,
2864
- y: this.canvasSize.h
2865
- });
2866
- const bounds = {
2867
- minX: topLeft.x,
2868
- minY: topLeft.y,
2869
- maxX: bottomRight.x,
2870
- maxY: bottomRight.y
2871
- };
2866
+ const bounds = this.gridBoundsOverride ?? (() => {
2867
+ const topLeft = cam.screenToWorld({ x: 0, y: 0 });
2868
+ const bottomRight = cam.screenToWorld({ x: canvasSize.w, y: canvasSize.h });
2869
+ return {
2870
+ minX: topLeft.x,
2871
+ minY: topLeft.y,
2872
+ maxX: bottomRight.x,
2873
+ maxY: bottomRight.y
2874
+ };
2875
+ })();
2872
2876
  if (grid.gridType === "hex") {
2873
2877
  const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
2874
2878
  const scale = cam.zoom * dpr;
@@ -4727,19 +4731,14 @@ var RenderLoop = class {
4727
4731
  layerManager;
4728
4732
  domNodeManager;
4729
4733
  layerCache;
4734
+ marginViewport;
4730
4735
  activeDrawingLayerId = null;
4731
- lastZoom;
4732
- lastCamX;
4733
- lastCamY;
4736
+ gridCacheDirty = true;
4737
+ // set on recenter/viewport-change; consumed by the grid block
4734
4738
  stats = new RenderStats();
4735
4739
  layerGroups = /* @__PURE__ */ new Map();
4736
4740
  gridCacheCanvas = null;
4737
4741
  gridCacheCtx = null;
4738
- gridCacheZoom = -1;
4739
- gridCacheCamX = -Infinity;
4740
- gridCacheCamY = -Infinity;
4741
- gridCacheWidth = 0;
4742
- gridCacheHeight = 0;
4743
4742
  lastGridRef = null;
4744
4743
  constructor(deps) {
4745
4744
  this.canvasEl = deps.canvasEl;
@@ -4751,9 +4750,7 @@ var RenderLoop = class {
4751
4750
  this.layerManager = deps.layerManager;
4752
4751
  this.domNodeManager = deps.domNodeManager;
4753
4752
  this.layerCache = deps.layerCache;
4754
- this.lastZoom = deps.camera.zoom;
4755
- this.lastCamX = deps.camera.position.x;
4756
- this.lastCamY = deps.camera.position.y;
4753
+ this.marginViewport = deps.marginViewport;
4757
4754
  }
4758
4755
  requestRender() {
4759
4756
  this.needsRender = true;
@@ -4780,7 +4777,9 @@ var RenderLoop = class {
4780
4777
  setCanvasSize(width, height) {
4781
4778
  this.canvasEl.width = width;
4782
4779
  this.canvasEl.height = height;
4783
- this.layerCache.resize(this.canvasEl.clientWidth, this.canvasEl.clientHeight);
4780
+ const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
4781
+ this.marginViewport.setViewport(width / dpr, height / dpr, dpr);
4782
+ this.layerCache.resize();
4784
4783
  }
4785
4784
  setActiveDrawingLayer(layerId) {
4786
4785
  this.activeDrawingLayerId = layerId;
@@ -4794,30 +4793,29 @@ var RenderLoop = class {
4794
4793
  getStats() {
4795
4794
  return this.stats.getSnapshot();
4796
4795
  }
4797
- compositeLayerCache(ctx, layerId, dpr) {
4796
+ compositeLayerCache(ctx, layerId) {
4798
4797
  const cached = this.layerCache.getCanvas(layerId);
4798
+ const offset = this.marginViewport.compositeOffset(
4799
+ this.camera.position.x,
4800
+ this.camera.position.y
4801
+ );
4799
4802
  ctx.save();
4800
- ctx.scale(1 / this.camera.zoom, 1 / this.camera.zoom);
4801
- ctx.translate(-this.camera.position.x, -this.camera.position.y);
4802
- ctx.scale(1 / dpr, 1 / dpr);
4803
- ctx.drawImage(cached, 0, 0);
4803
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
4804
+ ctx.drawImage(cached, offset.x, offset.y);
4804
4805
  ctx.restore();
4805
4806
  }
4806
- ensureGridCache(cssWidth, cssHeight, dpr) {
4807
- if (this.gridCacheCanvas !== null && this.gridCacheWidth === cssWidth && this.gridCacheHeight === cssHeight) {
4807
+ ensureGridCache() {
4808
+ const w = this.marginViewport.physicalWidth();
4809
+ const h = this.marginViewport.physicalHeight();
4810
+ if (this.gridCacheCanvas !== null && this.gridCacheCanvas.width === w && this.gridCacheCanvas.height === h) {
4808
4811
  return;
4809
4812
  }
4810
- const physWidth = Math.round(cssWidth * dpr);
4811
- const physHeight = Math.round(cssHeight * dpr);
4812
4813
  if (typeof OffscreenCanvas !== "undefined") {
4813
- this.gridCacheCanvas = new OffscreenCanvas(
4814
- physWidth,
4815
- physHeight
4816
- );
4814
+ this.gridCacheCanvas = new OffscreenCanvas(w, h);
4817
4815
  } else if (typeof document !== "undefined") {
4818
4816
  const el = document.createElement("canvas");
4819
- el.width = physWidth;
4820
- el.height = physHeight;
4817
+ el.width = w;
4818
+ el.height = h;
4821
4819
  this.gridCacheCanvas = el;
4822
4820
  } else {
4823
4821
  this.gridCacheCanvas = null;
@@ -4836,14 +4834,14 @@ var RenderLoop = class {
4836
4834
  const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
4837
4835
  const cssWidth = this.canvasEl.clientWidth;
4838
4836
  const cssHeight = this.canvasEl.clientHeight;
4837
+ this.marginViewport.setViewport(cssWidth, cssHeight, dpr);
4839
4838
  const currentZoom = this.camera.zoom;
4840
4839
  const currentCamX = this.camera.position.x;
4841
4840
  const currentCamY = this.camera.position.y;
4842
- if (currentZoom !== this.lastZoom || currentCamX !== this.lastCamX || currentCamY !== this.lastCamY) {
4841
+ if (this.marginViewport.needsRecenter(currentCamX, currentCamY, currentZoom)) {
4842
+ this.marginViewport.recenter(currentCamX, currentCamY, currentZoom);
4843
4843
  this.layerCache.markAllDirty();
4844
- this.lastZoom = currentZoom;
4845
- this.lastCamX = currentCamX;
4846
- this.lastCamY = currentCamY;
4844
+ this.gridCacheDirty = true;
4847
4845
  }
4848
4846
  ctx.save();
4849
4847
  ctx.scale(dpr, dpr);
@@ -4862,13 +4860,13 @@ var RenderLoop = class {
4862
4860
  ctx.save();
4863
4861
  ctx.translate(this.camera.position.x, this.camera.position.y);
4864
4862
  ctx.scale(this.camera.zoom, this.camera.zoom);
4865
- const visibleRect = this.camera.getVisibleRect(cssWidth, cssHeight);
4866
- const margin = Math.max(visibleRect.w, visibleRect.h) * 0.1;
4863
+ const cullBounds = this.marginViewport.cachedWorldBounds();
4864
+ const cullPad = Math.max(cullBounds.w, cullBounds.h) * 0.05;
4867
4865
  const cullingRect = {
4868
- x: visibleRect.x - margin,
4869
- y: visibleRect.y - margin,
4870
- w: visibleRect.w + margin * 2,
4871
- h: visibleRect.h + margin * 2
4866
+ x: cullBounds.x - cullPad,
4867
+ y: cullBounds.y - cullPad,
4868
+ w: cullBounds.w + cullPad * 2,
4869
+ h: cullBounds.h + cullPad * 2
4872
4870
  };
4873
4871
  const allElements = this.store.getAll();
4874
4872
  this.layerGroups.clear();
@@ -4905,13 +4903,13 @@ var RenderLoop = class {
4905
4903
  const isActiveDrawingLayer = layerId === this.activeDrawingLayerId;
4906
4904
  if (!this.layerCache.isDirty(layerId)) {
4907
4905
  const compT0 = performance.now();
4908
- this.compositeLayerCache(ctx, layerId, dpr);
4906
+ this.compositeLayerCache(ctx, layerId);
4909
4907
  compositeMs += performance.now() - compT0;
4910
4908
  continue;
4911
4909
  }
4912
4910
  if (isActiveDrawingLayer) {
4913
4911
  const compT0 = performance.now();
4914
- this.compositeLayerCache(ctx, layerId, dpr);
4912
+ this.compositeLayerCache(ctx, layerId);
4915
4913
  compositeMs += performance.now() - compT0;
4916
4914
  continue;
4917
4915
  }
@@ -4921,9 +4919,7 @@ var RenderLoop = class {
4921
4919
  const offCanvas = this.layerCache.getCanvas(layerId);
4922
4920
  offCtx.clearRect(0, 0, offCanvas.width, offCanvas.height);
4923
4921
  offCtx.save();
4924
- offCtx.scale(dpr, dpr);
4925
- offCtx.translate(this.camera.position.x, this.camera.position.y);
4926
- offCtx.scale(this.camera.zoom, this.camera.zoom);
4922
+ this.marginViewport.applyRenderTransform(offCtx);
4927
4923
  for (const element of elements) {
4928
4924
  const elBounds = getElementBounds(element);
4929
4925
  if (elBounds && !boundsIntersect(elBounds, cullingRect)) continue;
@@ -4933,48 +4929,54 @@ var RenderLoop = class {
4933
4929
  this.layerCache.markClean(layerId);
4934
4930
  layersMs += performance.now() - layerT0;
4935
4931
  const compT0 = performance.now();
4936
- this.compositeLayerCache(ctx, layerId, dpr);
4932
+ this.compositeLayerCache(ctx, layerId);
4937
4933
  compositeMs += performance.now() - compT0;
4938
4934
  }
4939
4935
  }
4940
4936
  if (gridElements.length > 0) {
4941
4937
  const gridT0 = performance.now();
4942
4938
  const gridRef = gridElements[0];
4943
- const gridCacheHit = this.gridCacheCanvas !== null && currentZoom === this.gridCacheZoom && currentCamX === this.gridCacheCamX && currentCamY === this.gridCacheCamY && cssWidth === this.gridCacheWidth && cssHeight === this.gridCacheHeight && gridRef === this.lastGridRef;
4944
- if (gridCacheHit) {
4945
- ctx.save();
4946
- ctx.setTransform(1, 0, 0, 1, 0, 0);
4947
- ctx.drawImage(this.gridCacheCanvas, 0, 0);
4948
- ctx.restore();
4949
- } else {
4950
- this.ensureGridCache(cssWidth, cssHeight, dpr);
4939
+ const gridDirty = this.gridCacheDirty || gridRef !== this.lastGridRef;
4940
+ if (gridDirty) {
4941
+ this.ensureGridCache();
4951
4942
  if (this.gridCacheCtx && this.gridCacheCanvas) {
4943
+ const cb = this.marginViewport.cachedWorldBounds();
4944
+ this.renderer.setGridBoundsOverride({
4945
+ minX: cb.x,
4946
+ minY: cb.y,
4947
+ maxX: cb.x + cb.w,
4948
+ maxY: cb.y + cb.h
4949
+ });
4952
4950
  const gc = this.gridCacheCtx;
4953
4951
  gc.clearRect(0, 0, this.gridCacheCanvas.width, this.gridCacheCanvas.height);
4954
4952
  gc.save();
4955
- gc.scale(dpr, dpr);
4956
- gc.translate(currentCamX, currentCamY);
4957
- gc.scale(currentZoom, currentZoom);
4958
- for (const grid of gridElements) {
4959
- this.renderer.renderCanvasElement(gc, grid);
4960
- }
4961
- gc.restore();
4962
- ctx.save();
4963
- ctx.setTransform(1, 0, 0, 1, 0, 0);
4964
- ctx.drawImage(this.gridCacheCanvas, 0, 0);
4965
- ctx.restore();
4966
- } else {
4967
- for (const grid of gridElements) {
4968
- this.renderer.renderCanvasElement(ctx, grid);
4953
+ this.marginViewport.applyRenderTransform(gc);
4954
+ try {
4955
+ for (const grid of gridElements) {
4956
+ this.renderer.renderCanvasElement(gc, grid);
4957
+ }
4958
+ } finally {
4959
+ gc.restore();
4960
+ this.renderer.setGridBoundsOverride(null);
4969
4961
  }
4970
4962
  }
4971
- this.gridCacheZoom = currentZoom;
4972
- this.gridCacheCamX = currentCamX;
4973
- this.gridCacheCamY = currentCamY;
4974
- this.gridCacheWidth = cssWidth;
4975
- this.gridCacheHeight = cssHeight;
4963
+ this.gridCacheDirty = false;
4976
4964
  this.lastGridRef = gridRef;
4977
4965
  }
4966
+ if (this.gridCacheCanvas) {
4967
+ const offset = this.marginViewport.compositeOffset(
4968
+ this.camera.position.x,
4969
+ this.camera.position.y
4970
+ );
4971
+ ctx.save();
4972
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
4973
+ ctx.drawImage(this.gridCacheCanvas, offset.x, offset.y);
4974
+ ctx.restore();
4975
+ } else {
4976
+ for (const grid of gridElements) {
4977
+ this.renderer.renderCanvasElement(ctx, grid);
4978
+ }
4979
+ }
4978
4980
  gridMs = performance.now() - gridT0;
4979
4981
  }
4980
4982
  const overlayT0 = performance.now();
@@ -5006,15 +5008,11 @@ function createOffscreenCanvas(width, height) {
5006
5008
  return canvas;
5007
5009
  }
5008
5010
  var LayerCache = class {
5011
+ constructor(viewport) {
5012
+ this.viewport = viewport;
5013
+ }
5009
5014
  canvases = /* @__PURE__ */ new Map();
5010
5015
  dirtyFlags = /* @__PURE__ */ new Map();
5011
- width;
5012
- height;
5013
- constructor(width, height) {
5014
- const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
5015
- this.width = Math.round(width * dpr);
5016
- this.height = Math.round(height * dpr);
5017
- }
5018
5016
  isDirty(layerId) {
5019
5017
  return this.dirtyFlags.get(layerId) !== false;
5020
5018
  }
@@ -5032,7 +5030,7 @@ var LayerCache = class {
5032
5030
  getCanvas(layerId) {
5033
5031
  let canvas = this.canvases.get(layerId);
5034
5032
  if (!canvas) {
5035
- canvas = createOffscreenCanvas(this.width, this.height);
5033
+ canvas = createOffscreenCanvas(this.viewport.physicalWidth(), this.viewport.physicalHeight());
5036
5034
  this.canvases.set(layerId, canvas);
5037
5035
  this.dirtyFlags.set(layerId, true);
5038
5036
  }
@@ -5042,13 +5040,12 @@ var LayerCache = class {
5042
5040
  const canvas = this.getCanvas(layerId);
5043
5041
  return canvas.getContext("2d");
5044
5042
  }
5045
- resize(width, height) {
5046
- const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
5047
- this.width = Math.round(width * dpr);
5048
- this.height = Math.round(height * dpr);
5043
+ resize() {
5044
+ const w = this.viewport.physicalWidth();
5045
+ const h = this.viewport.physicalHeight();
5049
5046
  for (const [id, canvas] of this.canvases) {
5050
- canvas.width = this.width;
5051
- canvas.height = this.height;
5047
+ canvas.width = w;
5048
+ canvas.height = h;
5052
5049
  this.dirtyFlags.set(id, true);
5053
5050
  }
5054
5051
  }
@@ -5058,6 +5055,75 @@ var LayerCache = class {
5058
5055
  }
5059
5056
  };
5060
5057
 
5058
+ // src/canvas/margin-viewport.ts
5059
+ var MarginViewport = class {
5060
+ constructor(marginPx) {
5061
+ this.marginPx = marginPx;
5062
+ }
5063
+ cssW = 0;
5064
+ cssH = 0;
5065
+ dpr = 1;
5066
+ anchorCamX = 0;
5067
+ anchorCamY = 0;
5068
+ anchorZoom = Number.NaN;
5069
+ // sentinel → first needsRecenter is true
5070
+ viewportDirty = true;
5071
+ setMargin(marginPx) {
5072
+ if (marginPx !== this.marginPx) {
5073
+ this.marginPx = marginPx;
5074
+ this.viewportDirty = true;
5075
+ }
5076
+ }
5077
+ setViewport(cssW, cssH, dpr) {
5078
+ if (cssW !== this.cssW || cssH !== this.cssH || dpr !== this.dpr) {
5079
+ this.cssW = cssW;
5080
+ this.cssH = cssH;
5081
+ this.dpr = dpr;
5082
+ this.viewportDirty = true;
5083
+ }
5084
+ }
5085
+ physicalWidth() {
5086
+ return Math.round((this.cssW + 2 * this.marginPx) * this.dpr);
5087
+ }
5088
+ physicalHeight() {
5089
+ return Math.round((this.cssH + 2 * this.marginPx) * this.dpr);
5090
+ }
5091
+ needsRecenter(camX, camY, zoom) {
5092
+ return this.viewportDirty || zoom !== this.anchorZoom || Math.abs(camX - this.anchorCamX) > this.marginPx || Math.abs(camY - this.anchorCamY) > this.marginPx;
5093
+ }
5094
+ recenter(camX, camY, zoom) {
5095
+ this.anchorCamX = camX;
5096
+ this.anchorCamY = camY;
5097
+ this.anchorZoom = zoom;
5098
+ this.viewportDirty = false;
5099
+ }
5100
+ /** Applies dpr scale + anchor-relative world transform. setViewport must have been called first. */
5101
+ applyRenderTransform(ctx) {
5102
+ ctx.scale(this.dpr, this.dpr);
5103
+ ctx.translate(this.marginPx + this.anchorCamX, this.marginPx + this.anchorCamY);
5104
+ ctx.scale(this.anchorZoom, this.anchorZoom);
5105
+ }
5106
+ // Device-px destination for drawImage(cache, x, y).
5107
+ // A world point P sits in the cache at CSS x `margin + anchorCamX + P*zoom`; it must land on
5108
+ // screen at `camX + P*zoom`; so the blit offset is `camX - anchorCamX - margin` (CSS) * dpr.
5109
+ compositeOffset(camX, camY) {
5110
+ return {
5111
+ x: (camX - this.anchorCamX - this.marginPx) * this.dpr,
5112
+ y: (camY - this.anchorCamY - this.marginPx) * this.dpr
5113
+ };
5114
+ }
5115
+ // World-space bounds of the whole cached region at the anchor (cull rect for re-renders).
5116
+ cachedWorldBounds() {
5117
+ const z = this.anchorZoom;
5118
+ return {
5119
+ x: (-this.marginPx - this.anchorCamX) / z,
5120
+ y: (-this.marginPx - this.anchorCamY) / z,
5121
+ w: (this.cssW + 2 * this.marginPx) / z,
5122
+ h: (this.cssH + 2 * this.marginPx) / z
5123
+ };
5124
+ }
5125
+ };
5126
+
5061
5127
  // src/canvas/viewport.ts
5062
5128
  var Viewport = class {
5063
5129
  constructor(container, options = {}) {
@@ -5134,10 +5200,13 @@ var Viewport = class {
5134
5200
  this.interactMode = new InteractMode({
5135
5201
  getNode: (id) => this.domNodeManager.getNode(id)
5136
5202
  });
5137
- const layerCache = new LayerCache(
5203
+ this.marginViewport = new MarginViewport(options.panBufferMargin ?? 256);
5204
+ this.marginViewport.setViewport(
5138
5205
  this.canvasEl.clientWidth || 800,
5139
- this.canvasEl.clientHeight || 600
5206
+ this.canvasEl.clientHeight || 600,
5207
+ typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1
5140
5208
  );
5209
+ const layerCache = new LayerCache(this.marginViewport);
5141
5210
  this.renderLoop = new RenderLoop({
5142
5211
  canvasEl: this.canvasEl,
5143
5212
  camera: this.camera,
@@ -5147,7 +5216,8 @@ var Viewport = class {
5147
5216
  toolManager: this.toolManager,
5148
5217
  layerManager: this.layerManager,
5149
5218
  domNodeManager: this.domNodeManager,
5150
- layerCache
5219
+ layerCache,
5220
+ marginViewport: this.marginViewport
5151
5221
  });
5152
5222
  this.unsubCamera = this.camera.onChange(() => {
5153
5223
  this.applyCameraTransform();
@@ -5211,6 +5281,7 @@ var Viewport = class {
5211
5281
  noteEditor;
5212
5282
  historyRecorder;
5213
5283
  toolContext;
5284
+ marginViewport;
5214
5285
  resizeObserver = null;
5215
5286
  _snapToGrid = false;
5216
5287
  _gridSize;
@@ -7401,7 +7472,7 @@ var TemplateTool = class {
7401
7472
  };
7402
7473
 
7403
7474
  // src/index.ts
7404
- var VERSION = "0.22.0";
7475
+ var VERSION = "0.23.0";
7405
7476
  export {
7406
7477
  AddElementCommand,
7407
7478
  ArrowTool,