@fieldnotes/core 0.57.0 → 0.59.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.cjs CHANGED
@@ -37,6 +37,7 @@ __export(index_exports, {
37
37
  MEASURE_PRESENCE_KIND: () => MEASURE_PRESENCE_KIND,
38
38
  MeasureTool: () => MeasureTool,
39
39
  MemoryAdapter: () => MemoryAdapter,
40
+ MinimapController: () => MinimapController,
40
41
  NoteTool: () => NoteTool,
41
42
  PING_PRESENCE_KIND: () => PING_PRESENCE_KIND,
42
43
  PencilTool: () => PencilTool,
@@ -4796,88 +4797,247 @@ function miniToWorld(t, p) {
4796
4797
  return { x: (p.x - t.offsetX) / t.scale, y: (p.y - t.offsetY) / t.scale };
4797
4798
  }
4798
4799
 
4799
- // src/canvas/minimap.ts
4800
- var WIDTH = 200;
4801
- var HEIGHT = 140;
4802
- var MARGIN = 16;
4803
- var PADDING = 8;
4800
+ // src/canvas/minimap-controller.ts
4801
+ var DEFAULT_WIDTH = 200;
4802
+ var DEFAULT_HEIGHT = 140;
4803
+ var DEFAULT_PADDING = 8;
4804
+ var DEFAULT_DEBOUNCE_MS2 = 200;
4805
+ var DEFAULT_VIEWPORT_STROKE = "#3b82f6";
4804
4806
  var NEUTRAL = "rgba(100,116,139,0.6)";
4805
- var VIEWPORT_STROKE = "#3b82f6";
4807
+ var DOM_FALLBACK_TYPES = /* @__PURE__ */ new Set(["note", "text", "html"]);
4806
4808
  function elementColor(el) {
4807
4809
  return "color" in el && typeof el.color === "string" ? el.color : NEUTRAL;
4808
4810
  }
4809
- var Minimap = class {
4810
- constructor(deps) {
4811
- this.deps = deps;
4812
- const canvas = document.createElement("canvas");
4813
- canvas.width = WIDTH;
4814
- canvas.height = HEIGHT;
4815
- Object.assign(canvas.style, {
4816
- position: "absolute",
4817
- right: `${MARGIN}px`,
4818
- bottom: `${MARGIN}px`,
4819
- width: `${WIDTH}px`,
4820
- height: `${HEIGHT}px`,
4821
- background: "rgba(255,255,255,0.85)",
4822
- border: "1px solid rgba(0,0,0,0.15)",
4823
- borderRadius: "4px",
4824
- touchAction: "none",
4825
- cursor: "pointer",
4826
- zIndex: "10"
4827
- });
4828
- canvas.addEventListener("pointerdown", this.onPointerDown);
4829
- canvas.addEventListener("pointermove", this.onPointerMove);
4830
- canvas.addEventListener("pointerup", this.onPointerUp);
4831
- this.deps.container.appendChild(canvas);
4811
+ function sameBounds(a, b) {
4812
+ return a.x === b.x && a.y === b.y && a.w === b.w && a.h === b.h;
4813
+ }
4814
+ var MinimapController = class {
4815
+ constructor(viewport, canvas, options = {}) {
4816
+ this.viewport = viewport;
4832
4817
  this.canvas = canvas;
4818
+ this.width = options.width ?? DEFAULT_WIDTH;
4819
+ this.height = options.height ?? DEFAULT_HEIGHT;
4820
+ this.padding = options.padding ?? DEFAULT_PADDING;
4821
+ this.background = options.background ?? null;
4822
+ this.viewportStroke = options.viewportStroke ?? DEFAULT_VIEWPORT_STROKE;
4823
+ this.debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS2;
4824
+ this.interactive = options.interactive !== false;
4825
+ this.requestFrame = options.requestFrame ?? ((cb) => typeof requestAnimationFrame !== "undefined" ? requestAnimationFrame(cb) : 0);
4826
+ this.cancelFrame = options.cancelFrame ?? ((id) => {
4827
+ if (typeof cancelAnimationFrame !== "undefined") cancelAnimationFrame(id);
4828
+ });
4829
+ this.renderer.setStore(viewport.store);
4830
+ this.renderer.setOnImageLoad(() => this.markSceneDirty());
4831
+ this.applyCanvasSize();
4832
+ const onScene = () => this.markSceneDirty();
4833
+ this.unsubs.push(
4834
+ viewport.store.on("add", onScene),
4835
+ viewport.store.on("remove", onScene),
4836
+ viewport.store.on("update", onScene),
4837
+ viewport.store.on("clear", onScene),
4838
+ viewport.layerManager.on("change", onScene),
4839
+ viewport.camera.onChange(() => this.onViewChanged()),
4840
+ viewport.onResize(() => this.onViewChanged())
4841
+ );
4842
+ if (this.interactive) {
4843
+ canvas.style.touchAction = "none";
4844
+ canvas.style.cursor = "pointer";
4845
+ canvas.addEventListener("pointerdown", this.onPointerDown);
4846
+ canvas.addEventListener("pointermove", this.onPointerMove);
4847
+ canvas.addEventListener("pointerup", this.onPointerUp);
4848
+ canvas.addEventListener("pointercancel", this.onPointerEnd);
4849
+ canvas.addEventListener("lostpointercapture", this.onPointerEnd);
4850
+ }
4851
+ this.renderScene();
4852
+ this.requestDraw();
4833
4853
  }
4834
- canvas;
4835
- rafId = null;
4854
+ width;
4855
+ height;
4856
+ padding;
4857
+ background;
4858
+ viewportStroke;
4859
+ debounceMs;
4860
+ interactive;
4861
+ requestFrame;
4862
+ cancelFrame;
4863
+ renderer = new ElementRenderer();
4864
+ scene = null;
4865
+ frameId = null;
4866
+ debounceTimer = null;
4836
4867
  dragging = false;
4837
- scheduleDraw() {
4838
- if (this.rafId !== null) return;
4839
- this.rafId = this.deps.requestFrame(this.draw);
4868
+ disposed = false;
4869
+ unsubs = [];
4870
+ setSize(width, height) {
4871
+ if (this.disposed) return;
4872
+ this.width = width;
4873
+ this.height = height;
4874
+ this.applyCanvasSize();
4875
+ this.clearDebounce();
4876
+ this.renderScene();
4877
+ this.requestDraw();
4840
4878
  }
4841
- destroy() {
4842
- if (this.rafId !== null) {
4843
- this.deps.cancelFrame(this.rafId);
4844
- this.rafId = null;
4879
+ requestDraw() {
4880
+ if (this.disposed || this.frameId !== null) return;
4881
+ this.frameId = this.requestFrame(this.draw);
4882
+ }
4883
+ dispose() {
4884
+ if (this.disposed) return;
4885
+ this.disposed = true;
4886
+ this.clearDebounce();
4887
+ if (this.frameId !== null) {
4888
+ this.cancelFrame(this.frameId);
4889
+ this.frameId = null;
4890
+ }
4891
+ for (const unsub of this.unsubs.splice(0)) unsub();
4892
+ if (this.interactive) {
4893
+ this.canvas.removeEventListener("pointerdown", this.onPointerDown);
4894
+ this.canvas.removeEventListener("pointermove", this.onPointerMove);
4895
+ this.canvas.removeEventListener("pointerup", this.onPointerUp);
4896
+ this.canvas.removeEventListener("pointercancel", this.onPointerEnd);
4897
+ this.canvas.removeEventListener("lostpointercapture", this.onPointerEnd);
4898
+ }
4899
+ }
4900
+ clearDebounce() {
4901
+ if (this.debounceTimer !== null) {
4902
+ clearTimeout(this.debounceTimer);
4903
+ this.debounceTimer = null;
4904
+ }
4905
+ }
4906
+ dpr() {
4907
+ return typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
4908
+ }
4909
+ applyCanvasSize() {
4910
+ const dpr = this.dpr();
4911
+ this.canvas.width = Math.max(1, Math.round(this.width * dpr));
4912
+ this.canvas.height = Math.max(1, Math.round(this.height * dpr));
4913
+ }
4914
+ // Single source for both mapping bounds and rendering: layer-visible,
4915
+ // grids excluded. (getElementBounds already returns null for grids, so they
4916
+ // cannot extend the bounding box today — this filter makes the invariant
4917
+ // structural instead of relying on that special case.)
4918
+ sceneElements() {
4919
+ return this.viewport.store.getAll().filter((el) => el.type !== "grid" && this.viewport.layerManager.isLayerVisible(el.layerId));
4920
+ }
4921
+ currentMapping() {
4922
+ const viewportRect = this.viewport.getVisibleRect();
4923
+ const content = getElementsBoundingBox(this.sceneElements());
4924
+ return content ? unionBounds(content, viewportRect) : viewportRect;
4925
+ }
4926
+ onViewChanged() {
4927
+ if (this.disposed) return;
4928
+ const mapping = this.currentMapping();
4929
+ if (!this.scene || !sameBounds(mapping, this.scene.mapping)) {
4930
+ this.markSceneDirty();
4845
4931
  }
4846
- this.canvas.removeEventListener("pointerdown", this.onPointerDown);
4847
- this.canvas.removeEventListener("pointermove", this.onPointerMove);
4848
- this.canvas.removeEventListener("pointerup", this.onPointerUp);
4849
- this.canvas.remove();
4932
+ this.requestDraw();
4850
4933
  }
4851
- currentTransform() {
4852
- const viewport = this.deps.getViewportRect();
4853
- const content = this.deps.getContentBounds();
4854
- const mapping = content ? unionBounds(content, viewport) : viewport;
4855
- return computeMinimapTransform(mapping, WIDTH, HEIGHT, PADDING);
4934
+ markSceneDirty() {
4935
+ if (this.disposed) return;
4936
+ this.clearDebounce();
4937
+ this.debounceTimer = setTimeout(() => {
4938
+ this.debounceTimer = null;
4939
+ this.renderScene();
4940
+ this.requestDraw();
4941
+ }, this.debounceMs);
4942
+ }
4943
+ renderScene() {
4944
+ if (this.disposed) return;
4945
+ const dpr = this.dpr();
4946
+ const mapping = this.currentMapping();
4947
+ const transform = computeMinimapTransform(mapping, this.width, this.height, this.padding);
4948
+ const sceneCanvas = document.createElement("canvas");
4949
+ sceneCanvas.width = Math.max(1, Math.round(this.width * dpr));
4950
+ sceneCanvas.height = Math.max(1, Math.round(this.height * dpr));
4951
+ const ctx = sceneCanvas.getContext("2d");
4952
+ if (!ctx) return;
4953
+ const byLayer = /* @__PURE__ */ new Map();
4954
+ for (const el of this.sceneElements()) {
4955
+ let list = byLayer.get(el.layerId);
4956
+ if (!list) {
4957
+ list = [];
4958
+ byLayer.set(el.layerId, list);
4959
+ }
4960
+ list.push(el);
4961
+ }
4962
+ for (const layer of this.viewport.layerManager.getLayers()) {
4963
+ const els = byLayer.get(layer.id);
4964
+ if (!els || els.length === 0) continue;
4965
+ const opacity = layer.opacity;
4966
+ if (opacity >= 1) {
4967
+ this.renderLayerElements(ctx, els, transform, dpr);
4968
+ continue;
4969
+ }
4970
+ const layerCanvas = document.createElement("canvas");
4971
+ layerCanvas.width = sceneCanvas.width;
4972
+ layerCanvas.height = sceneCanvas.height;
4973
+ const layerCtx = layerCanvas.getContext("2d");
4974
+ if (!layerCtx) continue;
4975
+ this.renderLayerElements(layerCtx, els, transform, dpr);
4976
+ ctx.save();
4977
+ ctx.globalAlpha = opacity;
4978
+ ctx.drawImage(layerCanvas, 0, 0);
4979
+ ctx.restore();
4980
+ }
4981
+ this.scene = { canvas: sceneCanvas, transform, mapping };
4982
+ }
4983
+ renderLayerElements(ctx, elements, t, dpr) {
4984
+ for (const el of elements) {
4985
+ if (DOM_FALLBACK_TYPES.has(el.type)) {
4986
+ const b = getElementBounds(el);
4987
+ if (!b) continue;
4988
+ const tl = worldToMini(t, { x: b.x, y: b.y });
4989
+ ctx.save();
4990
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
4991
+ ctx.fillStyle = elementColor(el);
4992
+ ctx.fillRect(tl.x, tl.y, Math.max(1, b.w * t.scale), Math.max(1, b.h * t.scale));
4993
+ ctx.restore();
4994
+ continue;
4995
+ }
4996
+ ctx.save();
4997
+ ctx.setTransform(dpr * t.scale, 0, 0, dpr * t.scale, dpr * t.offsetX, dpr * t.offsetY);
4998
+ this.renderer.renderCanvasElement(ctx, el);
4999
+ ctx.restore();
5000
+ }
4856
5001
  }
4857
5002
  draw = () => {
4858
- this.rafId = null;
5003
+ this.frameId = null;
5004
+ if (this.disposed) return;
4859
5005
  const ctx = this.canvas.getContext("2d");
4860
5006
  if (!ctx) return;
4861
- const t = this.currentTransform();
4862
- const viewport = this.deps.getViewportRect();
4863
- ctx.clearRect(0, 0, WIDTH, HEIGHT);
4864
- for (const el of this.deps.getElements()) {
4865
- const b = getElementBounds(el);
4866
- if (!b) continue;
4867
- const tl = worldToMini(t, { x: b.x, y: b.y });
4868
- ctx.fillStyle = elementColor(el);
4869
- ctx.fillRect(tl.x, tl.y, Math.max(1, b.w * t.scale), Math.max(1, b.h * t.scale));
4870
- }
4871
- const vtl = worldToMini(t, { x: viewport.x, y: viewport.y });
4872
- ctx.strokeStyle = VIEWPORT_STROKE;
5007
+ const dpr = this.dpr();
5008
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
5009
+ ctx.clearRect(0, 0, this.width, this.height);
5010
+ if (this.background) {
5011
+ ctx.fillStyle = this.background;
5012
+ ctx.fillRect(0, 0, this.width, this.height);
5013
+ }
5014
+ const scene = this.scene;
5015
+ if (!scene) return;
5016
+ ctx.drawImage(scene.canvas, 0, 0, this.width, this.height);
5017
+ const viewportRect = this.viewport.getVisibleRect();
5018
+ const tl = worldToMini(scene.transform, { x: viewportRect.x, y: viewportRect.y });
5019
+ ctx.strokeStyle = this.viewportStroke;
4873
5020
  ctx.lineWidth = 1.5;
4874
- ctx.strokeRect(vtl.x, vtl.y, viewport.w * t.scale, viewport.h * t.scale);
5021
+ ctx.strokeRect(
5022
+ tl.x,
5023
+ tl.y,
5024
+ viewportRect.w * scene.transform.scale,
5025
+ viewportRect.h * scene.transform.scale
5026
+ );
4875
5027
  };
5028
+ navTransform() {
5029
+ return this.scene?.transform ?? computeMinimapTransform(this.currentMapping(), this.width, this.height, this.padding);
5030
+ }
4876
5031
  navigateFromEvent(e) {
4877
5032
  const rect = this.canvas.getBoundingClientRect();
4878
- const point = { x: e.clientX - rect.left, y: e.clientY - rect.top };
4879
- const world = miniToWorld(this.currentTransform(), point);
4880
- this.deps.navigateTo(world);
5033
+ const scaleX = rect.width > 0 ? this.width / rect.width : 1;
5034
+ const scaleY = rect.height > 0 ? this.height / rect.height : 1;
5035
+ const point = {
5036
+ x: (e.clientX - rect.left) * scaleX,
5037
+ y: (e.clientY - rect.top) * scaleY
5038
+ };
5039
+ const world = miniToWorld(this.navTransform(), point);
5040
+ this.viewport.centerCameraAt(world);
4881
5041
  }
4882
5042
  onPointerDown = (e) => {
4883
5043
  e.stopPropagation();
@@ -4898,6 +5058,45 @@ var Minimap = class {
4898
5058
  } catch {
4899
5059
  }
4900
5060
  };
5061
+ // pointercancel / lostpointercapture: a browser gesture or interrupted touch
5062
+ // can end the interaction without a pointerup — the drag must not stick.
5063
+ onPointerEnd = () => {
5064
+ this.dragging = false;
5065
+ };
5066
+ };
5067
+
5068
+ // src/canvas/minimap.ts
5069
+ var WIDTH = 200;
5070
+ var HEIGHT = 140;
5071
+ var MARGIN = 16;
5072
+ var Minimap = class {
5073
+ canvas;
5074
+ controller;
5075
+ constructor(container, viewport) {
5076
+ const canvas = document.createElement("canvas");
5077
+ Object.assign(canvas.style, {
5078
+ position: "absolute",
5079
+ right: `${MARGIN}px`,
5080
+ bottom: `${MARGIN}px`,
5081
+ width: `${WIDTH}px`,
5082
+ height: `${HEIGHT}px`,
5083
+ background: "rgba(255,255,255,0.85)",
5084
+ border: "1px solid rgba(0,0,0,0.15)",
5085
+ borderRadius: "4px",
5086
+ zIndex: "10"
5087
+ });
5088
+ canvas.dataset.fieldnotesMinimap = "true";
5089
+ container.appendChild(canvas);
5090
+ this.canvas = canvas;
5091
+ this.controller = new MinimapController(viewport, canvas, { width: WIDTH, height: HEIGHT });
5092
+ }
5093
+ scheduleDraw() {
5094
+ this.controller.requestDraw();
5095
+ }
5096
+ destroy() {
5097
+ this.controller.dispose();
5098
+ this.canvas.remove();
5099
+ }
4901
5100
  };
4902
5101
 
4903
5102
  // src/canvas/viewport-dom.ts
@@ -5656,6 +5855,19 @@ function computeBounds(elements, padding) {
5656
5855
  h: maxY - minY + padding * 2
5657
5856
  };
5658
5857
  }
5858
+ function resolveExportBounds(region, elements, padding) {
5859
+ if (!region) return computeBounds(elements, padding);
5860
+ const finite = Number.isFinite(region.x) && Number.isFinite(region.y) && Number.isFinite(region.w) && Number.isFinite(region.h);
5861
+ if (!finite || region.w <= 0 || region.h <= 0) {
5862
+ throw new RangeError("region must have finite coordinates and positive w/h");
5863
+ }
5864
+ return {
5865
+ x: region.x - padding,
5866
+ y: region.y - padding,
5867
+ w: region.w + padding * 2,
5868
+ h: region.h + padding * 2
5869
+ };
5870
+ }
5659
5871
  function renderGridForBounds(ctx, grid, bounds) {
5660
5872
  const visibleBounds = {
5661
5873
  minX: bounds.x,
@@ -5698,6 +5910,29 @@ function nonNegativeOption(value, fallback, name) {
5698
5910
  }
5699
5911
  return resolved;
5700
5912
  }
5913
+ function fitExportScale(bounds, requestedScale, options) {
5914
+ const maxDimension = positiveOption(options.maxDimension, DEFAULT_MAX_DIMENSION, "maxDimension");
5915
+ const maxPixels = positiveOption(options.maxPixels, DEFAULT_MAX_PIXELS, "maxPixels");
5916
+ const fits = (scale) => {
5917
+ const w = Math.ceil(bounds.w * scale);
5918
+ const h = Math.ceil(bounds.h * scale);
5919
+ return w <= maxDimension && h <= maxDimension && w * h <= maxPixels;
5920
+ };
5921
+ if (fits(requestedScale)) return requestedScale;
5922
+ const analytical = Math.min(
5923
+ maxDimension / Math.max(bounds.w, bounds.h),
5924
+ Math.sqrt(maxPixels) / Math.sqrt(bounds.w) / Math.sqrt(bounds.h)
5925
+ );
5926
+ let hi = Math.min(requestedScale, analytical);
5927
+ if (fits(hi)) return hi;
5928
+ let lo = 0;
5929
+ for (let i = 0; i < 40; i++) {
5930
+ const mid = (lo + hi) / 2;
5931
+ if (fits(mid)) lo = mid;
5932
+ else hi = mid;
5933
+ }
5934
+ return lo;
5935
+ }
5701
5936
  function assertExportSize(width, height, options) {
5702
5937
  const maxDimension = positiveOption(options.maxDimension, DEFAULT_MAX_DIMENSION, "maxDimension");
5703
5938
  const maxPixels = positiveOption(options.maxPixels, DEFAULT_MAX_PIXELS, "maxPixels");
@@ -5772,10 +6007,23 @@ function loadImages(elements, options = {}) {
5772
6007
  });
5773
6008
  }
5774
6009
  async function exportImage(store, options = {}, layerManager) {
5775
- const scale = positiveOption(options.scale, 2, "scale");
6010
+ const requestedScale = positiveOption(options.scale, 2, "scale");
6011
+ const scaleMode = options.scaleMode ?? "exact";
6012
+ if (scaleMode !== "exact" && scaleMode !== "fit") {
6013
+ throw new RangeError(`scaleMode must be 'exact' or 'fit'`);
6014
+ }
5776
6015
  const padding = nonNegativeOption(options.padding, 0, "padding");
5777
6016
  validateExportResourceOptions(options);
5778
6017
  validateHtmlExportOptions(options);
6018
+ const format = options.format ?? "png";
6019
+ if (format !== "png" && format !== "jpeg") {
6020
+ throw new RangeError(`format must be 'png' or 'jpeg'`);
6021
+ }
6022
+ if (options.quality !== void 0) {
6023
+ if (!Number.isFinite(options.quality) || options.quality <= 0 || options.quality > 1) {
6024
+ throw new RangeError("quality must be a finite number in (0, 1]");
6025
+ }
6026
+ }
5779
6027
  const background = options.background ?? "#ffffff";
5780
6028
  const filter = options.filter;
5781
6029
  const allElements = store.getAll();
@@ -5783,8 +6031,9 @@ async function exportImage(store, options = {}, layerManager) {
5783
6031
  if (filter) {
5784
6032
  visibleElements = visibleElements.filter(filter);
5785
6033
  }
5786
- const bounds = computeBounds(visibleElements, padding);
6034
+ const bounds = resolveExportBounds(options.region, visibleElements, padding);
5787
6035
  if (!bounds) return null;
6036
+ const scale = scaleMode === "fit" ? fitExportScale(bounds, requestedScale, options) : requestedScale;
5788
6037
  const width = Math.ceil(bounds.w * scale);
5789
6038
  const height = Math.ceil(bounds.h * scale);
5790
6039
  assertExportSize(width, height, options);
@@ -5886,8 +6135,9 @@ async function exportImage(store, options = {}, layerManager) {
5886
6135
  renderGridForBounds(ctx, grid, bounds);
5887
6136
  ctx.restore();
5888
6137
  }
6138
+ const mimeType = format === "jpeg" ? "image/jpeg" : "image/png";
5889
6139
  return new Promise((resolve) => {
5890
- canvas.toBlob((blob) => resolve(blob), "image/png");
6140
+ canvas.toBlob((blob) => resolve(blob), mimeType, options.quality);
5891
6141
  });
5892
6142
  }
5893
6143
 
@@ -8079,25 +8329,7 @@ var Viewport = class {
8079
8329
  }
8080
8330
  this.unsubToolChange = this.toolManager.onChange(() => this.contextMenu?.close());
8081
8331
  if (options.minimap) {
8082
- const visibleEls = () => this.store.getAll().filter((el) => this.layerManager.isLayerVisible(el.layerId));
8083
- this.minimap = new Minimap({
8084
- container: this.wrapper,
8085
- getElements: visibleEls,
8086
- getContentBounds: () => getElementsBoundingBox(visibleEls()),
8087
- getViewportRect: () => this.camera.getVisibleRect(this.canvasEl.clientWidth, this.canvasEl.clientHeight),
8088
- navigateTo: (w) => {
8089
- const z = this.camera.zoom;
8090
- this.camera.moveTo(
8091
- this.canvasEl.clientWidth / 2 - w.x * z,
8092
- this.canvasEl.clientHeight / 2 - w.y * z
8093
- );
8094
- },
8095
- requestFrame: (cb) => typeof requestAnimationFrame !== "undefined" ? requestAnimationFrame(cb) : 0,
8096
- cancelFrame: (id) => {
8097
- if (typeof cancelAnimationFrame !== "undefined") cancelAnimationFrame(id);
8098
- }
8099
- });
8100
- this.minimap.scheduleDraw();
8332
+ this.minimap = new Minimap(this.wrapper, this);
8101
8333
  }
8102
8334
  this.domNodeManager = new DomNodeManager({
8103
8335
  domLayer: this.paintStack,
@@ -8133,7 +8365,6 @@ var Viewport = class {
8133
8365
  this.applyCameraTransform();
8134
8366
  this.noteEditor.updateToolbarPosition();
8135
8367
  this.contextMenu?.close();
8136
- this.minimap?.scheduleDraw();
8137
8368
  this.requestRender();
8138
8369
  });
8139
8370
  this.gridController = new GridController({
@@ -8148,7 +8379,6 @@ var Viewport = class {
8148
8379
  this.store.on("add", (el) => {
8149
8380
  if (el.type === "grid") this.gridController.syncContext();
8150
8381
  this.renderLoop.markLayerDirty(el.layerId);
8151
- this.minimap?.scheduleDraw();
8152
8382
  this.requestRender();
8153
8383
  }),
8154
8384
  this.store.on("remove", (el) => {
@@ -8156,7 +8386,6 @@ var Viewport = class {
8156
8386
  this.unbindArrowsFrom(el);
8157
8387
  this.domNodeManager.removeDomNode(el.id);
8158
8388
  this.renderLoop.markLayerDirty(el.layerId);
8159
- this.minimap?.scheduleDraw();
8160
8389
  this.requestRender();
8161
8390
  }),
8162
8391
  this.store.on("update", ({ previous, current }) => {
@@ -8165,21 +8394,18 @@ var Viewport = class {
8165
8394
  if (previous.layerId !== current.layerId) {
8166
8395
  this.renderLoop.markLayerDirty(previous.layerId);
8167
8396
  }
8168
- this.minimap?.scheduleDraw();
8169
8397
  this.requestRender();
8170
8398
  }),
8171
8399
  this.store.on("clear", () => {
8172
8400
  this.domNodeManager.clearDomNodes();
8173
8401
  this.renderLoop.markAllLayersDirty();
8174
8402
  this.gridController.syncContext();
8175
- this.minimap?.scheduleDraw();
8176
8403
  this.requestRender();
8177
8404
  })
8178
8405
  ];
8179
8406
  this.layerManager.on("change", () => {
8180
8407
  this.toolContext.activeLayerId = this.layerManager.activeLayerId;
8181
8408
  this.renderLoop.markAllLayersDirty();
8182
- this.minimap?.scheduleDraw();
8183
8409
  this.requestRender();
8184
8410
  });
8185
8411
  this.interactions = new ViewportInteractions({
@@ -8243,6 +8469,7 @@ var Viewport = class {
8243
8469
  contextMenu = null;
8244
8470
  minimap = null;
8245
8471
  htmlRenderers = /* @__PURE__ */ new Map();
8472
+ resizeListeners = /* @__PURE__ */ new Set();
8246
8473
  get ctx() {
8247
8474
  return this.canvasEl.getContext("2d");
8248
8475
  }
@@ -8267,6 +8494,27 @@ var Viewport = class {
8267
8494
  if (!bbox) return;
8268
8495
  this.camera.fitToContent(bbox, this.wrapper.clientWidth, this.wrapper.clientHeight, padding);
8269
8496
  }
8497
+ /** World-space rectangle currently visible through the canvas. */
8498
+ getVisibleRect() {
8499
+ return this.camera.getVisibleRect(this.canvasEl.clientWidth, this.canvasEl.clientHeight);
8500
+ }
8501
+ /** Centers the camera on a world point without changing zoom. */
8502
+ centerCameraAt(world) {
8503
+ const z = this.camera.zoom;
8504
+ this.camera.moveTo(
8505
+ this.canvasEl.clientWidth / 2 - world.x * z,
8506
+ this.canvasEl.clientHeight / 2 - world.y * z
8507
+ );
8508
+ }
8509
+ /**
8510
+ * Notifies after the host container resizes (ResizeObserver-driven). A resize
8511
+ * changes the visible world rect without a camera event; overlays such as the
8512
+ * minimap subscribe to stay current. Returns an idempotent unsubscribe.
8513
+ */
8514
+ onResize(listener) {
8515
+ this.resizeListeners.add(listener);
8516
+ return () => this.resizeListeners.delete(listener);
8517
+ }
8270
8518
  requestRender() {
8271
8519
  this.renderLoop.requestRender();
8272
8520
  }
@@ -8571,6 +8819,7 @@ var Viewport = class {
8571
8819
  this.unsubStore.forEach((fn) => fn());
8572
8820
  this.resizeObserver?.disconnect();
8573
8821
  this.resizeObserver = null;
8822
+ this.resizeListeners.clear();
8574
8823
  this.wrapper.remove();
8575
8824
  }
8576
8825
  stopInteracting() {
@@ -8620,6 +8869,7 @@ var Viewport = class {
8620
8869
  const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
8621
8870
  this.renderLoop.setCanvasSize(rect.width * dpr, rect.height * dpr);
8622
8871
  this.requestRender();
8872
+ this.resizeListeners.forEach((fn) => fn());
8623
8873
  }
8624
8874
  observeResize() {
8625
8875
  if (typeof ResizeObserver === "undefined") return;
@@ -8659,7 +8909,7 @@ function toLaserTrailPresence(emission) {
8659
8909
  };
8660
8910
  }
8661
8911
  var DEFAULT_COLOR = "#ff3b30";
8662
- var DEFAULT_WIDTH = 4;
8912
+ var DEFAULT_WIDTH2 = 4;
8663
8913
  var DEFAULT_FADE_MS = 1200;
8664
8914
  var DEFAULT_MAX_POINTS = 512;
8665
8915
  var RemoteLaserOverlay = class {
@@ -8675,7 +8925,7 @@ var RemoteLaserOverlay = class {
8675
8925
  constructor(host, options = {}) {
8676
8926
  this.host = host;
8677
8927
  this.color = options.color ?? DEFAULT_COLOR;
8678
- this.width = options.width ?? DEFAULT_WIDTH;
8928
+ this.width = options.width ?? DEFAULT_WIDTH2;
8679
8929
  this.fadeMs = options.fadeMs ?? DEFAULT_FADE_MS;
8680
8930
  this.maxPointsPerSender = options.maxPointsPerSender ?? DEFAULT_MAX_POINTS;
8681
8931
  this.unregister = host.registerOverlay((ctx) => this.renderTrails(ctx));
@@ -11904,7 +12154,7 @@ var TemplateTool = class {
11904
12154
 
11905
12155
  // src/tools/laser-tool.ts
11906
12156
  var DEFAULT_COLOR5 = "#ff3b30";
11907
- var DEFAULT_WIDTH2 = 4;
12157
+ var DEFAULT_WIDTH3 = 4;
11908
12158
  var DEFAULT_FADE_MS3 = 1200;
11909
12159
  var LaserTool = class {
11910
12160
  name;
@@ -11920,7 +12170,7 @@ var LaserTool = class {
11920
12170
  constructor(options = {}) {
11921
12171
  this.name = options.name ?? "laser";
11922
12172
  this.color = options.color ?? DEFAULT_COLOR5;
11923
- this.width = options.width ?? DEFAULT_WIDTH2;
12173
+ this.width = options.width ?? DEFAULT_WIDTH3;
11924
12174
  this.fadeMs = options.fadeMs ?? DEFAULT_FADE_MS3;
11925
12175
  }
11926
12176
  now() {
@@ -12167,7 +12417,7 @@ var PingTool = class {
12167
12417
  };
12168
12418
 
12169
12419
  // src/index.ts
12170
- var VERSION = "0.57.0";
12420
+ var VERSION = "0.59.0";
12171
12421
  // Annotate the CommonJS export names for ESM import in node:
12172
12422
  0 && (module.exports = {
12173
12423
  ArrowTool,
@@ -12187,6 +12437,7 @@ var VERSION = "0.57.0";
12187
12437
  MEASURE_PRESENCE_KIND,
12188
12438
  MeasureTool,
12189
12439
  MemoryAdapter,
12440
+ MinimapController,
12190
12441
  NoteTool,
12191
12442
  PING_PRESENCE_KIND,
12192
12443
  PencilTool,