@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.js CHANGED
@@ -4695,88 +4695,247 @@ function miniToWorld(t, p) {
4695
4695
  return { x: (p.x - t.offsetX) / t.scale, y: (p.y - t.offsetY) / t.scale };
4696
4696
  }
4697
4697
 
4698
- // src/canvas/minimap.ts
4699
- var WIDTH = 200;
4700
- var HEIGHT = 140;
4701
- var MARGIN = 16;
4702
- var PADDING = 8;
4698
+ // src/canvas/minimap-controller.ts
4699
+ var DEFAULT_WIDTH = 200;
4700
+ var DEFAULT_HEIGHT = 140;
4701
+ var DEFAULT_PADDING = 8;
4702
+ var DEFAULT_DEBOUNCE_MS2 = 200;
4703
+ var DEFAULT_VIEWPORT_STROKE = "#3b82f6";
4703
4704
  var NEUTRAL = "rgba(100,116,139,0.6)";
4704
- var VIEWPORT_STROKE = "#3b82f6";
4705
+ var DOM_FALLBACK_TYPES = /* @__PURE__ */ new Set(["note", "text", "html"]);
4705
4706
  function elementColor(el) {
4706
4707
  return "color" in el && typeof el.color === "string" ? el.color : NEUTRAL;
4707
4708
  }
4708
- var Minimap = class {
4709
- constructor(deps) {
4710
- this.deps = deps;
4711
- const canvas = document.createElement("canvas");
4712
- canvas.width = WIDTH;
4713
- canvas.height = HEIGHT;
4714
- Object.assign(canvas.style, {
4715
- position: "absolute",
4716
- right: `${MARGIN}px`,
4717
- bottom: `${MARGIN}px`,
4718
- width: `${WIDTH}px`,
4719
- height: `${HEIGHT}px`,
4720
- background: "rgba(255,255,255,0.85)",
4721
- border: "1px solid rgba(0,0,0,0.15)",
4722
- borderRadius: "4px",
4723
- touchAction: "none",
4724
- cursor: "pointer",
4725
- zIndex: "10"
4726
- });
4727
- canvas.addEventListener("pointerdown", this.onPointerDown);
4728
- canvas.addEventListener("pointermove", this.onPointerMove);
4729
- canvas.addEventListener("pointerup", this.onPointerUp);
4730
- this.deps.container.appendChild(canvas);
4709
+ function sameBounds(a, b) {
4710
+ return a.x === b.x && a.y === b.y && a.w === b.w && a.h === b.h;
4711
+ }
4712
+ var MinimapController = class {
4713
+ constructor(viewport, canvas, options = {}) {
4714
+ this.viewport = viewport;
4731
4715
  this.canvas = canvas;
4716
+ this.width = options.width ?? DEFAULT_WIDTH;
4717
+ this.height = options.height ?? DEFAULT_HEIGHT;
4718
+ this.padding = options.padding ?? DEFAULT_PADDING;
4719
+ this.background = options.background ?? null;
4720
+ this.viewportStroke = options.viewportStroke ?? DEFAULT_VIEWPORT_STROKE;
4721
+ this.debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS2;
4722
+ this.interactive = options.interactive !== false;
4723
+ this.requestFrame = options.requestFrame ?? ((cb) => typeof requestAnimationFrame !== "undefined" ? requestAnimationFrame(cb) : 0);
4724
+ this.cancelFrame = options.cancelFrame ?? ((id) => {
4725
+ if (typeof cancelAnimationFrame !== "undefined") cancelAnimationFrame(id);
4726
+ });
4727
+ this.renderer.setStore(viewport.store);
4728
+ this.renderer.setOnImageLoad(() => this.markSceneDirty());
4729
+ this.applyCanvasSize();
4730
+ const onScene = () => this.markSceneDirty();
4731
+ this.unsubs.push(
4732
+ viewport.store.on("add", onScene),
4733
+ viewport.store.on("remove", onScene),
4734
+ viewport.store.on("update", onScene),
4735
+ viewport.store.on("clear", onScene),
4736
+ viewport.layerManager.on("change", onScene),
4737
+ viewport.camera.onChange(() => this.onViewChanged()),
4738
+ viewport.onResize(() => this.onViewChanged())
4739
+ );
4740
+ if (this.interactive) {
4741
+ canvas.style.touchAction = "none";
4742
+ canvas.style.cursor = "pointer";
4743
+ canvas.addEventListener("pointerdown", this.onPointerDown);
4744
+ canvas.addEventListener("pointermove", this.onPointerMove);
4745
+ canvas.addEventListener("pointerup", this.onPointerUp);
4746
+ canvas.addEventListener("pointercancel", this.onPointerEnd);
4747
+ canvas.addEventListener("lostpointercapture", this.onPointerEnd);
4748
+ }
4749
+ this.renderScene();
4750
+ this.requestDraw();
4732
4751
  }
4733
- canvas;
4734
- rafId = null;
4752
+ width;
4753
+ height;
4754
+ padding;
4755
+ background;
4756
+ viewportStroke;
4757
+ debounceMs;
4758
+ interactive;
4759
+ requestFrame;
4760
+ cancelFrame;
4761
+ renderer = new ElementRenderer();
4762
+ scene = null;
4763
+ frameId = null;
4764
+ debounceTimer = null;
4735
4765
  dragging = false;
4736
- scheduleDraw() {
4737
- if (this.rafId !== null) return;
4738
- this.rafId = this.deps.requestFrame(this.draw);
4766
+ disposed = false;
4767
+ unsubs = [];
4768
+ setSize(width, height) {
4769
+ if (this.disposed) return;
4770
+ this.width = width;
4771
+ this.height = height;
4772
+ this.applyCanvasSize();
4773
+ this.clearDebounce();
4774
+ this.renderScene();
4775
+ this.requestDraw();
4739
4776
  }
4740
- destroy() {
4741
- if (this.rafId !== null) {
4742
- this.deps.cancelFrame(this.rafId);
4743
- this.rafId = null;
4777
+ requestDraw() {
4778
+ if (this.disposed || this.frameId !== null) return;
4779
+ this.frameId = this.requestFrame(this.draw);
4780
+ }
4781
+ dispose() {
4782
+ if (this.disposed) return;
4783
+ this.disposed = true;
4784
+ this.clearDebounce();
4785
+ if (this.frameId !== null) {
4786
+ this.cancelFrame(this.frameId);
4787
+ this.frameId = null;
4788
+ }
4789
+ for (const unsub of this.unsubs.splice(0)) unsub();
4790
+ if (this.interactive) {
4791
+ this.canvas.removeEventListener("pointerdown", this.onPointerDown);
4792
+ this.canvas.removeEventListener("pointermove", this.onPointerMove);
4793
+ this.canvas.removeEventListener("pointerup", this.onPointerUp);
4794
+ this.canvas.removeEventListener("pointercancel", this.onPointerEnd);
4795
+ this.canvas.removeEventListener("lostpointercapture", this.onPointerEnd);
4796
+ }
4797
+ }
4798
+ clearDebounce() {
4799
+ if (this.debounceTimer !== null) {
4800
+ clearTimeout(this.debounceTimer);
4801
+ this.debounceTimer = null;
4802
+ }
4803
+ }
4804
+ dpr() {
4805
+ return typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
4806
+ }
4807
+ applyCanvasSize() {
4808
+ const dpr = this.dpr();
4809
+ this.canvas.width = Math.max(1, Math.round(this.width * dpr));
4810
+ this.canvas.height = Math.max(1, Math.round(this.height * dpr));
4811
+ }
4812
+ // Single source for both mapping bounds and rendering: layer-visible,
4813
+ // grids excluded. (getElementBounds already returns null for grids, so they
4814
+ // cannot extend the bounding box today — this filter makes the invariant
4815
+ // structural instead of relying on that special case.)
4816
+ sceneElements() {
4817
+ return this.viewport.store.getAll().filter((el) => el.type !== "grid" && this.viewport.layerManager.isLayerVisible(el.layerId));
4818
+ }
4819
+ currentMapping() {
4820
+ const viewportRect = this.viewport.getVisibleRect();
4821
+ const content = getElementsBoundingBox(this.sceneElements());
4822
+ return content ? unionBounds(content, viewportRect) : viewportRect;
4823
+ }
4824
+ onViewChanged() {
4825
+ if (this.disposed) return;
4826
+ const mapping = this.currentMapping();
4827
+ if (!this.scene || !sameBounds(mapping, this.scene.mapping)) {
4828
+ this.markSceneDirty();
4744
4829
  }
4745
- this.canvas.removeEventListener("pointerdown", this.onPointerDown);
4746
- this.canvas.removeEventListener("pointermove", this.onPointerMove);
4747
- this.canvas.removeEventListener("pointerup", this.onPointerUp);
4748
- this.canvas.remove();
4830
+ this.requestDraw();
4749
4831
  }
4750
- currentTransform() {
4751
- const viewport = this.deps.getViewportRect();
4752
- const content = this.deps.getContentBounds();
4753
- const mapping = content ? unionBounds(content, viewport) : viewport;
4754
- return computeMinimapTransform(mapping, WIDTH, HEIGHT, PADDING);
4832
+ markSceneDirty() {
4833
+ if (this.disposed) return;
4834
+ this.clearDebounce();
4835
+ this.debounceTimer = setTimeout(() => {
4836
+ this.debounceTimer = null;
4837
+ this.renderScene();
4838
+ this.requestDraw();
4839
+ }, this.debounceMs);
4840
+ }
4841
+ renderScene() {
4842
+ if (this.disposed) return;
4843
+ const dpr = this.dpr();
4844
+ const mapping = this.currentMapping();
4845
+ const transform = computeMinimapTransform(mapping, this.width, this.height, this.padding);
4846
+ const sceneCanvas = document.createElement("canvas");
4847
+ sceneCanvas.width = Math.max(1, Math.round(this.width * dpr));
4848
+ sceneCanvas.height = Math.max(1, Math.round(this.height * dpr));
4849
+ const ctx = sceneCanvas.getContext("2d");
4850
+ if (!ctx) return;
4851
+ const byLayer = /* @__PURE__ */ new Map();
4852
+ for (const el of this.sceneElements()) {
4853
+ let list = byLayer.get(el.layerId);
4854
+ if (!list) {
4855
+ list = [];
4856
+ byLayer.set(el.layerId, list);
4857
+ }
4858
+ list.push(el);
4859
+ }
4860
+ for (const layer of this.viewport.layerManager.getLayers()) {
4861
+ const els = byLayer.get(layer.id);
4862
+ if (!els || els.length === 0) continue;
4863
+ const opacity = layer.opacity;
4864
+ if (opacity >= 1) {
4865
+ this.renderLayerElements(ctx, els, transform, dpr);
4866
+ continue;
4867
+ }
4868
+ const layerCanvas = document.createElement("canvas");
4869
+ layerCanvas.width = sceneCanvas.width;
4870
+ layerCanvas.height = sceneCanvas.height;
4871
+ const layerCtx = layerCanvas.getContext("2d");
4872
+ if (!layerCtx) continue;
4873
+ this.renderLayerElements(layerCtx, els, transform, dpr);
4874
+ ctx.save();
4875
+ ctx.globalAlpha = opacity;
4876
+ ctx.drawImage(layerCanvas, 0, 0);
4877
+ ctx.restore();
4878
+ }
4879
+ this.scene = { canvas: sceneCanvas, transform, mapping };
4880
+ }
4881
+ renderLayerElements(ctx, elements, t, dpr) {
4882
+ for (const el of elements) {
4883
+ if (DOM_FALLBACK_TYPES.has(el.type)) {
4884
+ const b = getElementBounds(el);
4885
+ if (!b) continue;
4886
+ const tl = worldToMini(t, { x: b.x, y: b.y });
4887
+ ctx.save();
4888
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
4889
+ ctx.fillStyle = elementColor(el);
4890
+ ctx.fillRect(tl.x, tl.y, Math.max(1, b.w * t.scale), Math.max(1, b.h * t.scale));
4891
+ ctx.restore();
4892
+ continue;
4893
+ }
4894
+ ctx.save();
4895
+ ctx.setTransform(dpr * t.scale, 0, 0, dpr * t.scale, dpr * t.offsetX, dpr * t.offsetY);
4896
+ this.renderer.renderCanvasElement(ctx, el);
4897
+ ctx.restore();
4898
+ }
4755
4899
  }
4756
4900
  draw = () => {
4757
- this.rafId = null;
4901
+ this.frameId = null;
4902
+ if (this.disposed) return;
4758
4903
  const ctx = this.canvas.getContext("2d");
4759
4904
  if (!ctx) return;
4760
- const t = this.currentTransform();
4761
- const viewport = this.deps.getViewportRect();
4762
- ctx.clearRect(0, 0, WIDTH, HEIGHT);
4763
- for (const el of this.deps.getElements()) {
4764
- const b = getElementBounds(el);
4765
- if (!b) continue;
4766
- const tl = worldToMini(t, { x: b.x, y: b.y });
4767
- ctx.fillStyle = elementColor(el);
4768
- ctx.fillRect(tl.x, tl.y, Math.max(1, b.w * t.scale), Math.max(1, b.h * t.scale));
4769
- }
4770
- const vtl = worldToMini(t, { x: viewport.x, y: viewport.y });
4771
- ctx.strokeStyle = VIEWPORT_STROKE;
4905
+ const dpr = this.dpr();
4906
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
4907
+ ctx.clearRect(0, 0, this.width, this.height);
4908
+ if (this.background) {
4909
+ ctx.fillStyle = this.background;
4910
+ ctx.fillRect(0, 0, this.width, this.height);
4911
+ }
4912
+ const scene = this.scene;
4913
+ if (!scene) return;
4914
+ ctx.drawImage(scene.canvas, 0, 0, this.width, this.height);
4915
+ const viewportRect = this.viewport.getVisibleRect();
4916
+ const tl = worldToMini(scene.transform, { x: viewportRect.x, y: viewportRect.y });
4917
+ ctx.strokeStyle = this.viewportStroke;
4772
4918
  ctx.lineWidth = 1.5;
4773
- ctx.strokeRect(vtl.x, vtl.y, viewport.w * t.scale, viewport.h * t.scale);
4919
+ ctx.strokeRect(
4920
+ tl.x,
4921
+ tl.y,
4922
+ viewportRect.w * scene.transform.scale,
4923
+ viewportRect.h * scene.transform.scale
4924
+ );
4774
4925
  };
4926
+ navTransform() {
4927
+ return this.scene?.transform ?? computeMinimapTransform(this.currentMapping(), this.width, this.height, this.padding);
4928
+ }
4775
4929
  navigateFromEvent(e) {
4776
4930
  const rect = this.canvas.getBoundingClientRect();
4777
- const point = { x: e.clientX - rect.left, y: e.clientY - rect.top };
4778
- const world = miniToWorld(this.currentTransform(), point);
4779
- this.deps.navigateTo(world);
4931
+ const scaleX = rect.width > 0 ? this.width / rect.width : 1;
4932
+ const scaleY = rect.height > 0 ? this.height / rect.height : 1;
4933
+ const point = {
4934
+ x: (e.clientX - rect.left) * scaleX,
4935
+ y: (e.clientY - rect.top) * scaleY
4936
+ };
4937
+ const world = miniToWorld(this.navTransform(), point);
4938
+ this.viewport.centerCameraAt(world);
4780
4939
  }
4781
4940
  onPointerDown = (e) => {
4782
4941
  e.stopPropagation();
@@ -4797,6 +4956,45 @@ var Minimap = class {
4797
4956
  } catch {
4798
4957
  }
4799
4958
  };
4959
+ // pointercancel / lostpointercapture: a browser gesture or interrupted touch
4960
+ // can end the interaction without a pointerup — the drag must not stick.
4961
+ onPointerEnd = () => {
4962
+ this.dragging = false;
4963
+ };
4964
+ };
4965
+
4966
+ // src/canvas/minimap.ts
4967
+ var WIDTH = 200;
4968
+ var HEIGHT = 140;
4969
+ var MARGIN = 16;
4970
+ var Minimap = class {
4971
+ canvas;
4972
+ controller;
4973
+ constructor(container, viewport) {
4974
+ const canvas = document.createElement("canvas");
4975
+ Object.assign(canvas.style, {
4976
+ position: "absolute",
4977
+ right: `${MARGIN}px`,
4978
+ bottom: `${MARGIN}px`,
4979
+ width: `${WIDTH}px`,
4980
+ height: `${HEIGHT}px`,
4981
+ background: "rgba(255,255,255,0.85)",
4982
+ border: "1px solid rgba(0,0,0,0.15)",
4983
+ borderRadius: "4px",
4984
+ zIndex: "10"
4985
+ });
4986
+ canvas.dataset.fieldnotesMinimap = "true";
4987
+ container.appendChild(canvas);
4988
+ this.canvas = canvas;
4989
+ this.controller = new MinimapController(viewport, canvas, { width: WIDTH, height: HEIGHT });
4990
+ }
4991
+ scheduleDraw() {
4992
+ this.controller.requestDraw();
4993
+ }
4994
+ destroy() {
4995
+ this.controller.dispose();
4996
+ this.canvas.remove();
4997
+ }
4800
4998
  };
4801
4999
 
4802
5000
  // src/canvas/viewport-dom.ts
@@ -5555,6 +5753,19 @@ function computeBounds(elements, padding) {
5555
5753
  h: maxY - minY + padding * 2
5556
5754
  };
5557
5755
  }
5756
+ function resolveExportBounds(region, elements, padding) {
5757
+ if (!region) return computeBounds(elements, padding);
5758
+ const finite = Number.isFinite(region.x) && Number.isFinite(region.y) && Number.isFinite(region.w) && Number.isFinite(region.h);
5759
+ if (!finite || region.w <= 0 || region.h <= 0) {
5760
+ throw new RangeError("region must have finite coordinates and positive w/h");
5761
+ }
5762
+ return {
5763
+ x: region.x - padding,
5764
+ y: region.y - padding,
5765
+ w: region.w + padding * 2,
5766
+ h: region.h + padding * 2
5767
+ };
5768
+ }
5558
5769
  function renderGridForBounds(ctx, grid, bounds) {
5559
5770
  const visibleBounds = {
5560
5771
  minX: bounds.x,
@@ -5597,6 +5808,29 @@ function nonNegativeOption(value, fallback, name) {
5597
5808
  }
5598
5809
  return resolved;
5599
5810
  }
5811
+ function fitExportScale(bounds, requestedScale, options) {
5812
+ const maxDimension = positiveOption(options.maxDimension, DEFAULT_MAX_DIMENSION, "maxDimension");
5813
+ const maxPixels = positiveOption(options.maxPixels, DEFAULT_MAX_PIXELS, "maxPixels");
5814
+ const fits = (scale) => {
5815
+ const w = Math.ceil(bounds.w * scale);
5816
+ const h = Math.ceil(bounds.h * scale);
5817
+ return w <= maxDimension && h <= maxDimension && w * h <= maxPixels;
5818
+ };
5819
+ if (fits(requestedScale)) return requestedScale;
5820
+ const analytical = Math.min(
5821
+ maxDimension / Math.max(bounds.w, bounds.h),
5822
+ Math.sqrt(maxPixels) / Math.sqrt(bounds.w) / Math.sqrt(bounds.h)
5823
+ );
5824
+ let hi = Math.min(requestedScale, analytical);
5825
+ if (fits(hi)) return hi;
5826
+ let lo = 0;
5827
+ for (let i = 0; i < 40; i++) {
5828
+ const mid = (lo + hi) / 2;
5829
+ if (fits(mid)) lo = mid;
5830
+ else hi = mid;
5831
+ }
5832
+ return lo;
5833
+ }
5600
5834
  function assertExportSize(width, height, options) {
5601
5835
  const maxDimension = positiveOption(options.maxDimension, DEFAULT_MAX_DIMENSION, "maxDimension");
5602
5836
  const maxPixels = positiveOption(options.maxPixels, DEFAULT_MAX_PIXELS, "maxPixels");
@@ -5671,10 +5905,23 @@ function loadImages(elements, options = {}) {
5671
5905
  });
5672
5906
  }
5673
5907
  async function exportImage(store, options = {}, layerManager) {
5674
- const scale = positiveOption(options.scale, 2, "scale");
5908
+ const requestedScale = positiveOption(options.scale, 2, "scale");
5909
+ const scaleMode = options.scaleMode ?? "exact";
5910
+ if (scaleMode !== "exact" && scaleMode !== "fit") {
5911
+ throw new RangeError(`scaleMode must be 'exact' or 'fit'`);
5912
+ }
5675
5913
  const padding = nonNegativeOption(options.padding, 0, "padding");
5676
5914
  validateExportResourceOptions(options);
5677
5915
  validateHtmlExportOptions(options);
5916
+ const format = options.format ?? "png";
5917
+ if (format !== "png" && format !== "jpeg") {
5918
+ throw new RangeError(`format must be 'png' or 'jpeg'`);
5919
+ }
5920
+ if (options.quality !== void 0) {
5921
+ if (!Number.isFinite(options.quality) || options.quality <= 0 || options.quality > 1) {
5922
+ throw new RangeError("quality must be a finite number in (0, 1]");
5923
+ }
5924
+ }
5678
5925
  const background = options.background ?? "#ffffff";
5679
5926
  const filter = options.filter;
5680
5927
  const allElements = store.getAll();
@@ -5682,8 +5929,9 @@ async function exportImage(store, options = {}, layerManager) {
5682
5929
  if (filter) {
5683
5930
  visibleElements = visibleElements.filter(filter);
5684
5931
  }
5685
- const bounds = computeBounds(visibleElements, padding);
5932
+ const bounds = resolveExportBounds(options.region, visibleElements, padding);
5686
5933
  if (!bounds) return null;
5934
+ const scale = scaleMode === "fit" ? fitExportScale(bounds, requestedScale, options) : requestedScale;
5687
5935
  const width = Math.ceil(bounds.w * scale);
5688
5936
  const height = Math.ceil(bounds.h * scale);
5689
5937
  assertExportSize(width, height, options);
@@ -5785,8 +6033,9 @@ async function exportImage(store, options = {}, layerManager) {
5785
6033
  renderGridForBounds(ctx, grid, bounds);
5786
6034
  ctx.restore();
5787
6035
  }
6036
+ const mimeType = format === "jpeg" ? "image/jpeg" : "image/png";
5788
6037
  return new Promise((resolve) => {
5789
- canvas.toBlob((blob) => resolve(blob), "image/png");
6038
+ canvas.toBlob((blob) => resolve(blob), mimeType, options.quality);
5790
6039
  });
5791
6040
  }
5792
6041
 
@@ -7978,25 +8227,7 @@ var Viewport = class {
7978
8227
  }
7979
8228
  this.unsubToolChange = this.toolManager.onChange(() => this.contextMenu?.close());
7980
8229
  if (options.minimap) {
7981
- const visibleEls = () => this.store.getAll().filter((el) => this.layerManager.isLayerVisible(el.layerId));
7982
- this.minimap = new Minimap({
7983
- container: this.wrapper,
7984
- getElements: visibleEls,
7985
- getContentBounds: () => getElementsBoundingBox(visibleEls()),
7986
- getViewportRect: () => this.camera.getVisibleRect(this.canvasEl.clientWidth, this.canvasEl.clientHeight),
7987
- navigateTo: (w) => {
7988
- const z = this.camera.zoom;
7989
- this.camera.moveTo(
7990
- this.canvasEl.clientWidth / 2 - w.x * z,
7991
- this.canvasEl.clientHeight / 2 - w.y * z
7992
- );
7993
- },
7994
- requestFrame: (cb) => typeof requestAnimationFrame !== "undefined" ? requestAnimationFrame(cb) : 0,
7995
- cancelFrame: (id) => {
7996
- if (typeof cancelAnimationFrame !== "undefined") cancelAnimationFrame(id);
7997
- }
7998
- });
7999
- this.minimap.scheduleDraw();
8230
+ this.minimap = new Minimap(this.wrapper, this);
8000
8231
  }
8001
8232
  this.domNodeManager = new DomNodeManager({
8002
8233
  domLayer: this.paintStack,
@@ -8032,7 +8263,6 @@ var Viewport = class {
8032
8263
  this.applyCameraTransform();
8033
8264
  this.noteEditor.updateToolbarPosition();
8034
8265
  this.contextMenu?.close();
8035
- this.minimap?.scheduleDraw();
8036
8266
  this.requestRender();
8037
8267
  });
8038
8268
  this.gridController = new GridController({
@@ -8047,7 +8277,6 @@ var Viewport = class {
8047
8277
  this.store.on("add", (el) => {
8048
8278
  if (el.type === "grid") this.gridController.syncContext();
8049
8279
  this.renderLoop.markLayerDirty(el.layerId);
8050
- this.minimap?.scheduleDraw();
8051
8280
  this.requestRender();
8052
8281
  }),
8053
8282
  this.store.on("remove", (el) => {
@@ -8055,7 +8284,6 @@ var Viewport = class {
8055
8284
  this.unbindArrowsFrom(el);
8056
8285
  this.domNodeManager.removeDomNode(el.id);
8057
8286
  this.renderLoop.markLayerDirty(el.layerId);
8058
- this.minimap?.scheduleDraw();
8059
8287
  this.requestRender();
8060
8288
  }),
8061
8289
  this.store.on("update", ({ previous, current }) => {
@@ -8064,21 +8292,18 @@ var Viewport = class {
8064
8292
  if (previous.layerId !== current.layerId) {
8065
8293
  this.renderLoop.markLayerDirty(previous.layerId);
8066
8294
  }
8067
- this.minimap?.scheduleDraw();
8068
8295
  this.requestRender();
8069
8296
  }),
8070
8297
  this.store.on("clear", () => {
8071
8298
  this.domNodeManager.clearDomNodes();
8072
8299
  this.renderLoop.markAllLayersDirty();
8073
8300
  this.gridController.syncContext();
8074
- this.minimap?.scheduleDraw();
8075
8301
  this.requestRender();
8076
8302
  })
8077
8303
  ];
8078
8304
  this.layerManager.on("change", () => {
8079
8305
  this.toolContext.activeLayerId = this.layerManager.activeLayerId;
8080
8306
  this.renderLoop.markAllLayersDirty();
8081
- this.minimap?.scheduleDraw();
8082
8307
  this.requestRender();
8083
8308
  });
8084
8309
  this.interactions = new ViewportInteractions({
@@ -8142,6 +8367,7 @@ var Viewport = class {
8142
8367
  contextMenu = null;
8143
8368
  minimap = null;
8144
8369
  htmlRenderers = /* @__PURE__ */ new Map();
8370
+ resizeListeners = /* @__PURE__ */ new Set();
8145
8371
  get ctx() {
8146
8372
  return this.canvasEl.getContext("2d");
8147
8373
  }
@@ -8166,6 +8392,27 @@ var Viewport = class {
8166
8392
  if (!bbox) return;
8167
8393
  this.camera.fitToContent(bbox, this.wrapper.clientWidth, this.wrapper.clientHeight, padding);
8168
8394
  }
8395
+ /** World-space rectangle currently visible through the canvas. */
8396
+ getVisibleRect() {
8397
+ return this.camera.getVisibleRect(this.canvasEl.clientWidth, this.canvasEl.clientHeight);
8398
+ }
8399
+ /** Centers the camera on a world point without changing zoom. */
8400
+ centerCameraAt(world) {
8401
+ const z = this.camera.zoom;
8402
+ this.camera.moveTo(
8403
+ this.canvasEl.clientWidth / 2 - world.x * z,
8404
+ this.canvasEl.clientHeight / 2 - world.y * z
8405
+ );
8406
+ }
8407
+ /**
8408
+ * Notifies after the host container resizes (ResizeObserver-driven). A resize
8409
+ * changes the visible world rect without a camera event; overlays such as the
8410
+ * minimap subscribe to stay current. Returns an idempotent unsubscribe.
8411
+ */
8412
+ onResize(listener) {
8413
+ this.resizeListeners.add(listener);
8414
+ return () => this.resizeListeners.delete(listener);
8415
+ }
8169
8416
  requestRender() {
8170
8417
  this.renderLoop.requestRender();
8171
8418
  }
@@ -8470,6 +8717,7 @@ var Viewport = class {
8470
8717
  this.unsubStore.forEach((fn) => fn());
8471
8718
  this.resizeObserver?.disconnect();
8472
8719
  this.resizeObserver = null;
8720
+ this.resizeListeners.clear();
8473
8721
  this.wrapper.remove();
8474
8722
  }
8475
8723
  stopInteracting() {
@@ -8519,6 +8767,7 @@ var Viewport = class {
8519
8767
  const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
8520
8768
  this.renderLoop.setCanvasSize(rect.width * dpr, rect.height * dpr);
8521
8769
  this.requestRender();
8770
+ this.resizeListeners.forEach((fn) => fn());
8522
8771
  }
8523
8772
  observeResize() {
8524
8773
  if (typeof ResizeObserver === "undefined") return;
@@ -8558,7 +8807,7 @@ function toLaserTrailPresence(emission) {
8558
8807
  };
8559
8808
  }
8560
8809
  var DEFAULT_COLOR = "#ff3b30";
8561
- var DEFAULT_WIDTH = 4;
8810
+ var DEFAULT_WIDTH2 = 4;
8562
8811
  var DEFAULT_FADE_MS = 1200;
8563
8812
  var DEFAULT_MAX_POINTS = 512;
8564
8813
  var RemoteLaserOverlay = class {
@@ -8574,7 +8823,7 @@ var RemoteLaserOverlay = class {
8574
8823
  constructor(host, options = {}) {
8575
8824
  this.host = host;
8576
8825
  this.color = options.color ?? DEFAULT_COLOR;
8577
- this.width = options.width ?? DEFAULT_WIDTH;
8826
+ this.width = options.width ?? DEFAULT_WIDTH2;
8578
8827
  this.fadeMs = options.fadeMs ?? DEFAULT_FADE_MS;
8579
8828
  this.maxPointsPerSender = options.maxPointsPerSender ?? DEFAULT_MAX_POINTS;
8580
8829
  this.unregister = host.registerOverlay((ctx) => this.renderTrails(ctx));
@@ -11803,7 +12052,7 @@ var TemplateTool = class {
11803
12052
 
11804
12053
  // src/tools/laser-tool.ts
11805
12054
  var DEFAULT_COLOR5 = "#ff3b30";
11806
- var DEFAULT_WIDTH2 = 4;
12055
+ var DEFAULT_WIDTH3 = 4;
11807
12056
  var DEFAULT_FADE_MS3 = 1200;
11808
12057
  var LaserTool = class {
11809
12058
  name;
@@ -11819,7 +12068,7 @@ var LaserTool = class {
11819
12068
  constructor(options = {}) {
11820
12069
  this.name = options.name ?? "laser";
11821
12070
  this.color = options.color ?? DEFAULT_COLOR5;
11822
- this.width = options.width ?? DEFAULT_WIDTH2;
12071
+ this.width = options.width ?? DEFAULT_WIDTH3;
11823
12072
  this.fadeMs = options.fadeMs ?? DEFAULT_FADE_MS3;
11824
12073
  }
11825
12074
  now() {
@@ -12066,7 +12315,7 @@ var PingTool = class {
12066
12315
  };
12067
12316
 
12068
12317
  // src/index.ts
12069
- var VERSION = "0.57.0";
12318
+ var VERSION = "0.59.0";
12070
12319
  export {
12071
12320
  ArrowTool,
12072
12321
  AutoSave,
@@ -12085,6 +12334,7 @@ export {
12085
12334
  MEASURE_PRESENCE_KIND,
12086
12335
  MeasureTool,
12087
12336
  MemoryAdapter,
12337
+ MinimapController,
12088
12338
  NoteTool,
12089
12339
  PING_PRESENCE_KIND,
12090
12340
  PencilTool,