@fieldnotes/core 0.41.0 → 0.43.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
@@ -1554,6 +1554,81 @@ var KeyboardHandler = class {
1554
1554
  }
1555
1555
  };
1556
1556
 
1557
+ // src/canvas/pan-inertia.ts
1558
+ var VELOCITY_WINDOW_MS = 60;
1559
+ var FRICTION = 0.92;
1560
+ var MIN_START_SPEED = 2;
1561
+ var MIN_STOP_SPEED = 0.3;
1562
+ var PanInertia = class {
1563
+ constructor(deps) {
1564
+ this.deps = deps;
1565
+ }
1566
+ samples = [];
1567
+ vx = 0;
1568
+ vy = 0;
1569
+ rafId = null;
1570
+ sample(dx, dy) {
1571
+ const t = this.deps.now();
1572
+ this.samples.push({ dx, dy, t });
1573
+ this.prune(t);
1574
+ }
1575
+ release() {
1576
+ if (!this.deps.enabled()) {
1577
+ this.reset();
1578
+ return;
1579
+ }
1580
+ const t = this.deps.now();
1581
+ this.prune(t);
1582
+ const recent = this.samples;
1583
+ if (recent.length === 0) {
1584
+ this.reset();
1585
+ return;
1586
+ }
1587
+ let sx = 0;
1588
+ let sy = 0;
1589
+ for (const s of recent) {
1590
+ sx += s.dx;
1591
+ sy += s.dy;
1592
+ }
1593
+ this.vx = sx / recent.length;
1594
+ this.vy = sy / recent.length;
1595
+ this.samples = [];
1596
+ if (Math.hypot(this.vx, this.vy) < MIN_START_SPEED) {
1597
+ this.vx = 0;
1598
+ this.vy = 0;
1599
+ return;
1600
+ }
1601
+ this.rafId = this.deps.requestFrame(this.step);
1602
+ }
1603
+ cancel() {
1604
+ this.reset();
1605
+ }
1606
+ step = () => {
1607
+ if (this.rafId === null) return;
1608
+ this.deps.pan(this.vx, this.vy);
1609
+ this.vx *= FRICTION;
1610
+ this.vy *= FRICTION;
1611
+ if (Math.hypot(this.vx, this.vy) >= MIN_STOP_SPEED) {
1612
+ this.rafId = this.deps.requestFrame(this.step);
1613
+ } else {
1614
+ this.reset();
1615
+ }
1616
+ };
1617
+ prune(now) {
1618
+ const cutoff = now - VELOCITY_WINDOW_MS;
1619
+ this.samples = this.samples.filter((s) => s.t >= cutoff);
1620
+ }
1621
+ reset() {
1622
+ if (this.rafId !== null) {
1623
+ this.deps.cancelFrame(this.rafId);
1624
+ this.rafId = null;
1625
+ }
1626
+ this.samples = [];
1627
+ this.vx = 0;
1628
+ this.vy = 0;
1629
+ }
1630
+ };
1631
+
1557
1632
  // src/canvas/input-handler.ts
1558
1633
  var ZOOM_SENSITIVITY = 1e-3;
1559
1634
  var MIDDLE_BUTTON = 1;
@@ -1579,6 +1654,16 @@ var InputHandler = class {
1579
1654
  getLastPointerWorld: () => this.lastPointerWorld()
1580
1655
  });
1581
1656
  this.openContextMenu = options.openContextMenu;
1657
+ this.panInertiaEnabled = options.panInertia ?? true;
1658
+ this.panInertia = new PanInertia({
1659
+ pan: (dx, dy) => this.camera.pan(dx, dy),
1660
+ now: () => typeof performance !== "undefined" ? performance.now() : Date.now(),
1661
+ requestFrame: (cb) => typeof requestAnimationFrame !== "undefined" ? requestAnimationFrame(cb) : 0,
1662
+ cancelFrame: (id) => {
1663
+ if (typeof cancelAnimationFrame !== "undefined") cancelAnimationFrame(id);
1664
+ },
1665
+ enabled: () => this.panInertiaEnabled
1666
+ });
1582
1667
  this.scope = options.shortcuts?.scope ?? "focus";
1583
1668
  this.keyboard = new KeyboardHandler({
1584
1669
  element: this.element,
@@ -1628,6 +1713,8 @@ var InputHandler = class {
1628
1713
  keyboard;
1629
1714
  scope;
1630
1715
  openContextMenu;
1716
+ panInertia;
1717
+ panInertiaEnabled;
1631
1718
  setToolManager(toolManager, toolContext) {
1632
1719
  this.toolManager = toolManager;
1633
1720
  this.toolContext = toolContext;
@@ -1639,6 +1726,7 @@ var InputHandler = class {
1639
1726
  return this.keyboard.shortcuts;
1640
1727
  }
1641
1728
  destroy() {
1729
+ this.panInertia.cancel();
1642
1730
  this.actions.dispose();
1643
1731
  this.abortController.abort();
1644
1732
  this.inputFilter.reset();
@@ -1662,6 +1750,7 @@ var InputHandler = class {
1662
1750
  }
1663
1751
  onWheel = (e) => {
1664
1752
  e.preventDefault();
1753
+ this.panInertia.cancel();
1665
1754
  const rect = this.element.getBoundingClientRect();
1666
1755
  const zoomFactor = 1 - e.deltaY * ZOOM_SENSITIVITY;
1667
1756
  const newZoom = this.camera.zoom * zoomFactor;
@@ -1671,6 +1760,7 @@ var InputHandler = class {
1671
1760
  });
1672
1761
  };
1673
1762
  onPointerDown = (e) => {
1763
+ this.panInertia.cancel();
1674
1764
  this.focusSelf();
1675
1765
  this.activePointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
1676
1766
  this.element.setPointerCapture?.(e.pointerId);
@@ -1711,6 +1801,7 @@ var InputHandler = class {
1711
1801
  const dy = e.clientY - this.lastPointer.y;
1712
1802
  this.lastPointer = { x: e.clientX, y: e.clientY };
1713
1803
  this.camera.pan(dx, dy);
1804
+ this.panInertia.sample(dx, dy);
1714
1805
  return;
1715
1806
  }
1716
1807
  if (e.pointerType === "pen" && !this.activePointers.has(e.pointerId)) {
@@ -1741,6 +1832,7 @@ var InputHandler = class {
1741
1832
  }
1742
1833
  if (this.isPanning && this.activePointers.size === 0) {
1743
1834
  this.isPanning = false;
1835
+ this.panInertia.release();
1744
1836
  }
1745
1837
  const upResult = this.inputFilter.filterUp(e);
1746
1838
  if (this.isToolActive) {
@@ -1761,6 +1853,7 @@ var InputHandler = class {
1761
1853
  return this.actions.hasClipboard();
1762
1854
  }
1763
1855
  startPinch() {
1856
+ this.panInertia.cancel();
1764
1857
  this.cancelLongPress();
1765
1858
  this.inputFilter.reset();
1766
1859
  this.deferredDown = null;
@@ -1782,6 +1875,7 @@ var InputHandler = class {
1782
1875
  const dx = center2.x - this.lastPointer.x;
1783
1876
  const dy = center2.y - this.lastPointer.y;
1784
1877
  this.camera.pan(dx, dy);
1878
+ this.panInertia.sample(dx, dy);
1785
1879
  this.lastPinchDistance = dist;
1786
1880
  this.lastPinchCenter = center2;
1787
1881
  this.lastPointer = { ...center2 };
@@ -4266,6 +4360,135 @@ var ContextMenu = class {
4266
4360
  }
4267
4361
  };
4268
4362
 
4363
+ // src/canvas/minimap-transform.ts
4364
+ function unionBounds(a, b) {
4365
+ const minX = Math.min(a.x, b.x);
4366
+ const minY = Math.min(a.y, b.y);
4367
+ const maxX = Math.max(a.x + a.w, b.x + b.w);
4368
+ const maxY = Math.max(a.y + a.h, b.y + b.h);
4369
+ return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
4370
+ }
4371
+ function computeMinimapTransform(mapping, miniW, miniH, padding) {
4372
+ const availW = Math.max(1, miniW - 2 * padding);
4373
+ const availH = Math.max(1, miniH - 2 * padding);
4374
+ const w = Math.max(mapping.w, 1);
4375
+ const h = Math.max(mapping.h, 1);
4376
+ const scale = Math.min(availW / w, availH / h);
4377
+ const offsetX = (miniW - mapping.w * scale) / 2 - mapping.x * scale;
4378
+ const offsetY = (miniH - mapping.h * scale) / 2 - mapping.y * scale;
4379
+ return { scale, offsetX, offsetY };
4380
+ }
4381
+ function worldToMini(t, p) {
4382
+ return { x: p.x * t.scale + t.offsetX, y: p.y * t.scale + t.offsetY };
4383
+ }
4384
+ function miniToWorld(t, p) {
4385
+ return { x: (p.x - t.offsetX) / t.scale, y: (p.y - t.offsetY) / t.scale };
4386
+ }
4387
+
4388
+ // src/canvas/minimap.ts
4389
+ var WIDTH = 200;
4390
+ var HEIGHT = 140;
4391
+ var MARGIN = 16;
4392
+ var PADDING = 8;
4393
+ var NEUTRAL = "rgba(100,116,139,0.6)";
4394
+ var VIEWPORT_STROKE = "#3b82f6";
4395
+ function elementColor(el) {
4396
+ return "color" in el && typeof el.color === "string" ? el.color : NEUTRAL;
4397
+ }
4398
+ var Minimap = class {
4399
+ constructor(deps) {
4400
+ this.deps = deps;
4401
+ const canvas = document.createElement("canvas");
4402
+ canvas.width = WIDTH;
4403
+ canvas.height = HEIGHT;
4404
+ Object.assign(canvas.style, {
4405
+ position: "absolute",
4406
+ right: `${MARGIN}px`,
4407
+ bottom: `${MARGIN}px`,
4408
+ width: `${WIDTH}px`,
4409
+ height: `${HEIGHT}px`,
4410
+ background: "rgba(255,255,255,0.85)",
4411
+ border: "1px solid rgba(0,0,0,0.15)",
4412
+ borderRadius: "4px",
4413
+ touchAction: "none",
4414
+ cursor: "pointer",
4415
+ zIndex: "10"
4416
+ });
4417
+ canvas.addEventListener("pointerdown", this.onPointerDown);
4418
+ canvas.addEventListener("pointermove", this.onPointerMove);
4419
+ canvas.addEventListener("pointerup", this.onPointerUp);
4420
+ this.deps.container.appendChild(canvas);
4421
+ this.canvas = canvas;
4422
+ }
4423
+ canvas;
4424
+ rafId = null;
4425
+ dragging = false;
4426
+ scheduleDraw() {
4427
+ if (this.rafId !== null) return;
4428
+ this.rafId = this.deps.requestFrame(this.draw);
4429
+ }
4430
+ destroy() {
4431
+ if (this.rafId !== null) {
4432
+ this.deps.cancelFrame(this.rafId);
4433
+ this.rafId = null;
4434
+ }
4435
+ this.canvas.removeEventListener("pointerdown", this.onPointerDown);
4436
+ this.canvas.removeEventListener("pointermove", this.onPointerMove);
4437
+ this.canvas.removeEventListener("pointerup", this.onPointerUp);
4438
+ this.canvas.remove();
4439
+ }
4440
+ currentTransform() {
4441
+ const viewport = this.deps.getViewportRect();
4442
+ const content = this.deps.getContentBounds();
4443
+ const mapping = content ? unionBounds(content, viewport) : viewport;
4444
+ return computeMinimapTransform(mapping, WIDTH, HEIGHT, PADDING);
4445
+ }
4446
+ draw = () => {
4447
+ this.rafId = null;
4448
+ const ctx = this.canvas.getContext("2d");
4449
+ if (!ctx) return;
4450
+ const t = this.currentTransform();
4451
+ const viewport = this.deps.getViewportRect();
4452
+ ctx.clearRect(0, 0, WIDTH, HEIGHT);
4453
+ for (const el of this.deps.getElements()) {
4454
+ const b = getElementBounds(el);
4455
+ if (!b) continue;
4456
+ const tl = worldToMini(t, { x: b.x, y: b.y });
4457
+ ctx.fillStyle = elementColor(el);
4458
+ ctx.fillRect(tl.x, tl.y, Math.max(1, b.w * t.scale), Math.max(1, b.h * t.scale));
4459
+ }
4460
+ const vtl = worldToMini(t, { x: viewport.x, y: viewport.y });
4461
+ ctx.strokeStyle = VIEWPORT_STROKE;
4462
+ ctx.lineWidth = 1.5;
4463
+ ctx.strokeRect(vtl.x, vtl.y, viewport.w * t.scale, viewport.h * t.scale);
4464
+ };
4465
+ navigateFromEvent(e) {
4466
+ const rect = this.canvas.getBoundingClientRect();
4467
+ const point = { x: e.clientX - rect.left, y: e.clientY - rect.top };
4468
+ const world = miniToWorld(this.currentTransform(), point);
4469
+ this.deps.navigateTo(world);
4470
+ }
4471
+ onPointerDown = (e) => {
4472
+ e.stopPropagation();
4473
+ e.preventDefault();
4474
+ this.dragging = true;
4475
+ this.canvas.setPointerCapture?.(e.pointerId);
4476
+ this.navigateFromEvent(e);
4477
+ };
4478
+ onPointerMove = (e) => {
4479
+ if (!this.dragging) return;
4480
+ e.stopPropagation();
4481
+ this.navigateFromEvent(e);
4482
+ };
4483
+ onPointerUp = (e) => {
4484
+ this.dragging = false;
4485
+ try {
4486
+ this.canvas.releasePointerCapture(e.pointerId);
4487
+ } catch {
4488
+ }
4489
+ };
4490
+ };
4491
+
4269
4492
  // src/canvas/viewport-dom.ts
4270
4493
  function createWrapper() {
4271
4494
  const el = document.createElement("div");
@@ -6391,7 +6614,7 @@ function getElementStyle(element) {
6391
6614
  }
6392
6615
 
6393
6616
  // src/canvas/selection-ops.ts
6394
- function unionBounds(list) {
6617
+ function unionBounds2(list) {
6395
6618
  let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
6396
6619
  for (const b of list) {
6397
6620
  minX = Math.min(minX, b.x);
@@ -6486,7 +6709,7 @@ var SelectionOps = class {
6486
6709
  align(edge) {
6487
6710
  const bounded = this.boundedSelection();
6488
6711
  if (bounded.length < 2) return;
6489
- const B = unionBounds(bounded.map((e) => e.bounds));
6712
+ const B = unionBounds2(bounded.map((e) => e.bounds));
6490
6713
  this.deps.recorder.begin();
6491
6714
  const moved = [];
6492
6715
  for (const { id, el, bounds: b } of bounded) {
@@ -6883,7 +7106,8 @@ var Viewport = class {
6883
7106
  shortcuts: options.shortcuts,
6884
7107
  addImage: (src, world) => this.addImage(src, world),
6885
7108
  getCenteredWorld: () => this.centeredPosition({ w: 300, h: 200 }),
6886
- onPaste: options.onPaste
7109
+ onPaste: options.onPaste,
7110
+ panInertia: options.panInertia
6887
7111
  });
6888
7112
  if (options.contextMenu !== false) {
6889
7113
  this.contextMenu = new ContextMenu({
@@ -6892,6 +7116,27 @@ var Viewport = class {
6892
7116
  });
6893
7117
  }
6894
7118
  this.unsubToolChange = this.toolManager.onChange(() => this.contextMenu?.close());
7119
+ if (options.minimap) {
7120
+ const visibleEls = () => this.store.getAll().filter((el) => this.layerManager.isLayerVisible(el.layerId));
7121
+ this.minimap = new Minimap({
7122
+ container: this.wrapper,
7123
+ getElements: visibleEls,
7124
+ getContentBounds: () => getElementsBoundingBox(visibleEls()),
7125
+ getViewportRect: () => this.camera.getVisibleRect(this.canvasEl.clientWidth, this.canvasEl.clientHeight),
7126
+ navigateTo: (w) => {
7127
+ const z = this.camera.zoom;
7128
+ this.camera.moveTo(
7129
+ this.canvasEl.clientWidth / 2 - w.x * z,
7130
+ this.canvasEl.clientHeight / 2 - w.y * z
7131
+ );
7132
+ },
7133
+ requestFrame: (cb) => typeof requestAnimationFrame !== "undefined" ? requestAnimationFrame(cb) : 0,
7134
+ cancelFrame: (id) => {
7135
+ if (typeof cancelAnimationFrame !== "undefined") cancelAnimationFrame(id);
7136
+ }
7137
+ });
7138
+ this.minimap.scheduleDraw();
7139
+ }
6895
7140
  this.domNodeManager = new DomNodeManager({
6896
7141
  domLayer: this.domLayer,
6897
7142
  onEditRequest: (id) => this.interactions.startEditingElement(id),
@@ -6924,6 +7169,7 @@ var Viewport = class {
6924
7169
  this.applyCameraTransform();
6925
7170
  this.noteEditor.updateToolbarPosition();
6926
7171
  this.contextMenu?.close();
7172
+ this.minimap?.scheduleDraw();
6927
7173
  this.requestRender();
6928
7174
  });
6929
7175
  this.gridController = new GridController({
@@ -6938,6 +7184,7 @@ var Viewport = class {
6938
7184
  this.store.on("add", (el) => {
6939
7185
  if (el.type === "grid") this.gridController.syncContext();
6940
7186
  this.renderLoop.markLayerDirty(el.layerId);
7187
+ this.minimap?.scheduleDraw();
6941
7188
  this.requestRender();
6942
7189
  }),
6943
7190
  this.store.on("remove", (el) => {
@@ -6945,6 +7192,7 @@ var Viewport = class {
6945
7192
  this.unbindArrowsFrom(el);
6946
7193
  this.domNodeManager.removeDomNode(el.id);
6947
7194
  this.renderLoop.markLayerDirty(el.layerId);
7195
+ this.minimap?.scheduleDraw();
6948
7196
  this.requestRender();
6949
7197
  }),
6950
7198
  this.store.on("update", ({ previous, current }) => {
@@ -6953,17 +7201,20 @@ var Viewport = class {
6953
7201
  if (previous.layerId !== current.layerId) {
6954
7202
  this.renderLoop.markLayerDirty(previous.layerId);
6955
7203
  }
7204
+ this.minimap?.scheduleDraw();
6956
7205
  this.requestRender();
6957
7206
  }),
6958
7207
  this.store.on("clear", () => {
6959
7208
  this.domNodeManager.clearDomNodes();
6960
7209
  this.renderLoop.markAllLayersDirty();
6961
7210
  this.gridController.syncContext();
7211
+ this.minimap?.scheduleDraw();
6962
7212
  this.requestRender();
6963
7213
  })
6964
7214
  ];
6965
7215
  this.layerManager.on("change", () => {
6966
7216
  this.toolContext.activeLayerId = this.layerManager.activeLayerId;
7217
+ this.minimap?.scheduleDraw();
6967
7218
  this.requestRender();
6968
7219
  });
6969
7220
  this.interactions = new ViewportInteractions({
@@ -7023,6 +7274,7 @@ var Viewport = class {
7023
7274
  gridController;
7024
7275
  interactions;
7025
7276
  contextMenu = null;
7277
+ minimap = null;
7026
7278
  htmlRenderers = /* @__PURE__ */ new Map();
7027
7279
  get ctx() {
7028
7280
  return this.canvasEl.getContext("2d");
@@ -7296,6 +7548,7 @@ var Viewport = class {
7296
7548
  this.arrowLabelEditor.cancel();
7297
7549
  this.historyRecorder.destroy();
7298
7550
  this.contextMenu?.dispose();
7551
+ this.minimap?.destroy();
7299
7552
  this.wrapper.removeEventListener("pointerdown", this.interactions.onTapDown);
7300
7553
  this.wrapper.removeEventListener("pointerup", this.interactions.onDoubleTap);
7301
7554
  this.wrapper.removeEventListener("dragover", this.interactions.onDragOver);
@@ -9798,7 +10051,7 @@ var LaserTool = class {
9798
10051
  };
9799
10052
 
9800
10053
  // src/index.ts
9801
- var VERSION = "0.41.0";
10054
+ var VERSION = "0.43.0";
9802
10055
  // Annotate the CommonJS export names for ESM import in node:
9803
10056
  0 && (module.exports = {
9804
10057
  ArrowTool,