@fieldnotes/core 0.22.0 → 0.24.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/README.md +15 -0
- package/dist/index.cjs +212 -131
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +13 -2
- package/dist/index.d.ts +13 -2
- package/dist/index.js +212 -131
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -413,6 +413,21 @@ new Viewport(container, {
|
|
|
413
413
|
});
|
|
414
414
|
```
|
|
415
415
|
|
|
416
|
+
### ViewportOptions reference
|
|
417
|
+
|
|
418
|
+
- `camera?: CameraOptions` — `minZoom` / `maxZoom` (defaults `0.1` / `10`).
|
|
419
|
+
- `background?: BackgroundOptions` — `pattern`, `spacing`, `color`.
|
|
420
|
+
- `fontSizePresets?: FontSizePreset[]` — custom font-size steps for the note toolbar.
|
|
421
|
+
- `toolbar?: boolean` — show/hide the note formatting toolbar (default `true`).
|
|
422
|
+
- `placeholder?: string` — placeholder text shown in empty notes.
|
|
423
|
+
- `shortcuts?: ShortcutOptions` — seed the keyboard shortcut table with custom bindings.
|
|
424
|
+
- `onHtmlElementMount?` — called after `loadState` for HTML elements that need content injected.
|
|
425
|
+
- `onDrop?` — called for every drop event; replaces the built-in image-drop handling.
|
|
426
|
+
- `onImageError?` — called when an image element fails to load.
|
|
427
|
+
- `panBufferMargin?: number` (default `256`) — CSS-pixel margin cached beyond the viewport so
|
|
428
|
+
small pans re-composite instead of re-rasterizing the layers and grid. Larger = more pan
|
|
429
|
+
reuse, more memory per layer. Set `0` to disable (exact-viewport caches) on memory-tight hosts.
|
|
430
|
+
|
|
416
431
|
### Tool Options
|
|
417
432
|
|
|
418
433
|
```typescript
|
package/dist/index.cjs
CHANGED
|
@@ -1033,16 +1033,17 @@ var KeyboardActions = class {
|
|
|
1033
1033
|
this.pasteCount = 0;
|
|
1034
1034
|
}
|
|
1035
1035
|
paste() {
|
|
1036
|
+
if (this.deps.isToolActive()) return;
|
|
1036
1037
|
this.flushPendingNudge();
|
|
1037
|
-
if (this.clipboard.length === 0
|
|
1038
|
+
if (this.clipboard.length === 0) return;
|
|
1038
1039
|
const sel = this.selectTool();
|
|
1039
1040
|
if (!sel) return;
|
|
1040
1041
|
this.pasteCount++;
|
|
1041
1042
|
this.insertClones(this.clipboard, this.pasteCount * 20, sel);
|
|
1042
1043
|
}
|
|
1043
1044
|
duplicate() {
|
|
1044
|
-
this.flushPendingNudge();
|
|
1045
1045
|
if (this.deps.isToolActive()) return;
|
|
1046
|
+
this.flushPendingNudge();
|
|
1046
1047
|
const sel = this.selectTool();
|
|
1047
1048
|
if (!sel) return;
|
|
1048
1049
|
const source = [];
|
|
@@ -1226,6 +1227,11 @@ function parseBinding(binding) {
|
|
|
1226
1227
|
throw new Error(`Invalid shortcut binding "${binding}": unknown modifier "${part}"`);
|
|
1227
1228
|
}
|
|
1228
1229
|
}
|
|
1230
|
+
if (parsed.mod && (parsed.ctrl || parsed.meta)) {
|
|
1231
|
+
throw new Error(
|
|
1232
|
+
`Invalid shortcut binding "${binding}": "mod" already means Ctrl or Cmd; don't combine it with ctrl/meta`
|
|
1233
|
+
);
|
|
1234
|
+
}
|
|
1229
1235
|
return parsed;
|
|
1230
1236
|
}
|
|
1231
1237
|
function bindingMatches(p, e, allowShift) {
|
|
@@ -1369,6 +1375,10 @@ var InputHandler = class {
|
|
|
1369
1375
|
this.inputFilter.reset();
|
|
1370
1376
|
this.deferredDown = null;
|
|
1371
1377
|
this.lastPointerEvent = null;
|
|
1378
|
+
if (this.scope === "focus") {
|
|
1379
|
+
this.element.removeAttribute("tabindex");
|
|
1380
|
+
this.element.style.outline = "";
|
|
1381
|
+
}
|
|
1372
1382
|
}
|
|
1373
1383
|
bind() {
|
|
1374
1384
|
const opts = { signal: this.abortController.signal };
|
|
@@ -1699,6 +1709,22 @@ var DoubleTapDetector = class {
|
|
|
1699
1709
|
}
|
|
1700
1710
|
};
|
|
1701
1711
|
|
|
1712
|
+
// src/core/geometry.ts
|
|
1713
|
+
function distSqToSegment(p, a, b) {
|
|
1714
|
+
const abx = b.x - a.x;
|
|
1715
|
+
const aby = b.y - a.y;
|
|
1716
|
+
const apx = p.x - a.x;
|
|
1717
|
+
const apy = p.y - a.y;
|
|
1718
|
+
const lenSq = abx * abx + aby * aby;
|
|
1719
|
+
if (lenSq === 0) {
|
|
1720
|
+
return apx * apx + apy * apy;
|
|
1721
|
+
}
|
|
1722
|
+
const t = Math.max(0, Math.min(1, (apx * abx + apy * aby) / lenSq));
|
|
1723
|
+
const dx = p.x - (a.x + t * abx);
|
|
1724
|
+
const dy = p.y - (a.y + t * aby);
|
|
1725
|
+
return dx * dx + dy * dy;
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1702
1728
|
// src/elements/arrow-geometry.ts
|
|
1703
1729
|
function getArrowControlPoint(from, to, bend) {
|
|
1704
1730
|
const midX = (from.x + to.x) / 2;
|
|
@@ -1787,16 +1813,7 @@ function bezierPoint(from, cp, to, t) {
|
|
|
1787
1813
|
};
|
|
1788
1814
|
}
|
|
1789
1815
|
function isNearLine(point, a, b, threshold) {
|
|
1790
|
-
|
|
1791
|
-
const dy = b.y - a.y;
|
|
1792
|
-
const lenSq = dx * dx + dy * dy;
|
|
1793
|
-
if (lenSq === 0) {
|
|
1794
|
-
return Math.hypot(point.x - a.x, point.y - a.y) <= threshold;
|
|
1795
|
-
}
|
|
1796
|
-
const t = Math.max(0, Math.min(1, ((point.x - a.x) * dx + (point.y - a.y) * dy) / lenSq));
|
|
1797
|
-
const projX = a.x + t * dx;
|
|
1798
|
-
const projY = a.y + t * dy;
|
|
1799
|
-
return Math.hypot(point.x - projX, point.y - projY) <= threshold;
|
|
1816
|
+
return distSqToSegment(point, a, b) <= threshold * threshold;
|
|
1800
1817
|
}
|
|
1801
1818
|
|
|
1802
1819
|
// src/elements/element-bounds.ts
|
|
@@ -2780,6 +2797,7 @@ var ElementRenderer = class {
|
|
|
2780
2797
|
canvasSize = null;
|
|
2781
2798
|
hexTileCache = null;
|
|
2782
2799
|
hexTileCacheKey = "";
|
|
2800
|
+
gridBoundsOverride = null;
|
|
2783
2801
|
setStore(store) {
|
|
2784
2802
|
this.store = store;
|
|
2785
2803
|
}
|
|
@@ -2795,6 +2813,9 @@ var ElementRenderer = class {
|
|
|
2795
2813
|
setCanvasSize(w, h) {
|
|
2796
2814
|
this.canvasSize = { w, h };
|
|
2797
2815
|
}
|
|
2816
|
+
setGridBoundsOverride(bounds) {
|
|
2817
|
+
this.gridBoundsOverride = bounds;
|
|
2818
|
+
}
|
|
2798
2819
|
isDomElement(element) {
|
|
2799
2820
|
return DOM_ELEMENT_TYPES.has(element.type);
|
|
2800
2821
|
}
|
|
@@ -2965,20 +2986,20 @@ var ElementRenderer = class {
|
|
|
2965
2986
|
}
|
|
2966
2987
|
}
|
|
2967
2988
|
renderGrid(ctx, grid) {
|
|
2968
|
-
|
|
2989
|
+
const canvasSize = this.canvasSize;
|
|
2990
|
+
if (!canvasSize) return;
|
|
2969
2991
|
const cam = this.camera;
|
|
2970
2992
|
if (!cam) return;
|
|
2971
|
-
const
|
|
2972
|
-
|
|
2973
|
-
x:
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
};
|
|
2993
|
+
const bounds = this.gridBoundsOverride ?? (() => {
|
|
2994
|
+
const topLeft = cam.screenToWorld({ x: 0, y: 0 });
|
|
2995
|
+
const bottomRight = cam.screenToWorld({ x: canvasSize.w, y: canvasSize.h });
|
|
2996
|
+
return {
|
|
2997
|
+
minX: topLeft.x,
|
|
2998
|
+
minY: topLeft.y,
|
|
2999
|
+
maxX: bottomRight.x,
|
|
3000
|
+
maxY: bottomRight.y
|
|
3001
|
+
};
|
|
3002
|
+
})();
|
|
2982
3003
|
if (grid.gridType === "hex") {
|
|
2983
3004
|
const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
|
|
2984
3005
|
const scale = cam.zoom * dpr;
|
|
@@ -3223,9 +3244,9 @@ var ElementRenderer = class {
|
|
|
3223
3244
|
});
|
|
3224
3245
|
}
|
|
3225
3246
|
};
|
|
3226
|
-
img.onerror = () => {
|
|
3247
|
+
img.onerror = (event) => {
|
|
3227
3248
|
this.imageCache.set(src, "failed");
|
|
3228
|
-
this.onImageError?.(src);
|
|
3249
|
+
this.onImageError?.(src, event);
|
|
3229
3250
|
this.onImageLoad?.();
|
|
3230
3251
|
};
|
|
3231
3252
|
return null;
|
|
@@ -3650,7 +3671,10 @@ var NoteEditor = class {
|
|
|
3650
3671
|
this.editingNode.removeAttribute("data-fn-placeholder");
|
|
3651
3672
|
this.editingNode.removeAttribute("data-fn-empty");
|
|
3652
3673
|
const text = sanitizeNoteHtml(this.editingNode.innerHTML);
|
|
3653
|
-
store.
|
|
3674
|
+
const current = store.getById(this.editingId);
|
|
3675
|
+
if (current && (current.type === "note" || current.type === "text") && current.text !== text) {
|
|
3676
|
+
store.update(this.editingId, { text });
|
|
3677
|
+
}
|
|
3654
3678
|
this.editingNode.contentEditable = "false";
|
|
3655
3679
|
Object.assign(this.editingNode.style, {
|
|
3656
3680
|
userSelect: "none",
|
|
@@ -4837,19 +4861,14 @@ var RenderLoop = class {
|
|
|
4837
4861
|
layerManager;
|
|
4838
4862
|
domNodeManager;
|
|
4839
4863
|
layerCache;
|
|
4864
|
+
marginViewport;
|
|
4840
4865
|
activeDrawingLayerId = null;
|
|
4841
|
-
|
|
4842
|
-
|
|
4843
|
-
lastCamY;
|
|
4866
|
+
gridCacheDirty = true;
|
|
4867
|
+
// set on recenter/viewport-change; consumed by the grid block
|
|
4844
4868
|
stats = new RenderStats();
|
|
4845
4869
|
layerGroups = /* @__PURE__ */ new Map();
|
|
4846
4870
|
gridCacheCanvas = null;
|
|
4847
4871
|
gridCacheCtx = null;
|
|
4848
|
-
gridCacheZoom = -1;
|
|
4849
|
-
gridCacheCamX = -Infinity;
|
|
4850
|
-
gridCacheCamY = -Infinity;
|
|
4851
|
-
gridCacheWidth = 0;
|
|
4852
|
-
gridCacheHeight = 0;
|
|
4853
4872
|
lastGridRef = null;
|
|
4854
4873
|
constructor(deps) {
|
|
4855
4874
|
this.canvasEl = deps.canvasEl;
|
|
@@ -4861,9 +4880,7 @@ var RenderLoop = class {
|
|
|
4861
4880
|
this.layerManager = deps.layerManager;
|
|
4862
4881
|
this.domNodeManager = deps.domNodeManager;
|
|
4863
4882
|
this.layerCache = deps.layerCache;
|
|
4864
|
-
this.
|
|
4865
|
-
this.lastCamX = deps.camera.position.x;
|
|
4866
|
-
this.lastCamY = deps.camera.position.y;
|
|
4883
|
+
this.marginViewport = deps.marginViewport;
|
|
4867
4884
|
}
|
|
4868
4885
|
requestRender() {
|
|
4869
4886
|
this.needsRender = true;
|
|
@@ -4890,7 +4907,9 @@ var RenderLoop = class {
|
|
|
4890
4907
|
setCanvasSize(width, height) {
|
|
4891
4908
|
this.canvasEl.width = width;
|
|
4892
4909
|
this.canvasEl.height = height;
|
|
4893
|
-
|
|
4910
|
+
const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
|
|
4911
|
+
this.marginViewport.setViewport(width / dpr, height / dpr, dpr);
|
|
4912
|
+
this.layerCache.resize();
|
|
4894
4913
|
}
|
|
4895
4914
|
setActiveDrawingLayer(layerId) {
|
|
4896
4915
|
this.activeDrawingLayerId = layerId;
|
|
@@ -4904,30 +4923,29 @@ var RenderLoop = class {
|
|
|
4904
4923
|
getStats() {
|
|
4905
4924
|
return this.stats.getSnapshot();
|
|
4906
4925
|
}
|
|
4907
|
-
compositeLayerCache(ctx, layerId
|
|
4926
|
+
compositeLayerCache(ctx, layerId) {
|
|
4908
4927
|
const cached = this.layerCache.getCanvas(layerId);
|
|
4928
|
+
const offset = this.marginViewport.compositeOffset(
|
|
4929
|
+
this.camera.position.x,
|
|
4930
|
+
this.camera.position.y
|
|
4931
|
+
);
|
|
4909
4932
|
ctx.save();
|
|
4910
|
-
ctx.
|
|
4911
|
-
ctx.
|
|
4912
|
-
ctx.scale(1 / dpr, 1 / dpr);
|
|
4913
|
-
ctx.drawImage(cached, 0, 0);
|
|
4933
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
4934
|
+
ctx.drawImage(cached, offset.x, offset.y);
|
|
4914
4935
|
ctx.restore();
|
|
4915
4936
|
}
|
|
4916
|
-
ensureGridCache(
|
|
4917
|
-
|
|
4937
|
+
ensureGridCache() {
|
|
4938
|
+
const w = this.marginViewport.physicalWidth();
|
|
4939
|
+
const h = this.marginViewport.physicalHeight();
|
|
4940
|
+
if (this.gridCacheCanvas !== null && this.gridCacheCanvas.width === w && this.gridCacheCanvas.height === h) {
|
|
4918
4941
|
return;
|
|
4919
4942
|
}
|
|
4920
|
-
const physWidth = Math.round(cssWidth * dpr);
|
|
4921
|
-
const physHeight = Math.round(cssHeight * dpr);
|
|
4922
4943
|
if (typeof OffscreenCanvas !== "undefined") {
|
|
4923
|
-
this.gridCacheCanvas = new OffscreenCanvas(
|
|
4924
|
-
physWidth,
|
|
4925
|
-
physHeight
|
|
4926
|
-
);
|
|
4944
|
+
this.gridCacheCanvas = new OffscreenCanvas(w, h);
|
|
4927
4945
|
} else if (typeof document !== "undefined") {
|
|
4928
4946
|
const el = document.createElement("canvas");
|
|
4929
|
-
el.width =
|
|
4930
|
-
el.height =
|
|
4947
|
+
el.width = w;
|
|
4948
|
+
el.height = h;
|
|
4931
4949
|
this.gridCacheCanvas = el;
|
|
4932
4950
|
} else {
|
|
4933
4951
|
this.gridCacheCanvas = null;
|
|
@@ -4946,14 +4964,14 @@ var RenderLoop = class {
|
|
|
4946
4964
|
const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
|
|
4947
4965
|
const cssWidth = this.canvasEl.clientWidth;
|
|
4948
4966
|
const cssHeight = this.canvasEl.clientHeight;
|
|
4967
|
+
this.marginViewport.setViewport(cssWidth, cssHeight, dpr);
|
|
4949
4968
|
const currentZoom = this.camera.zoom;
|
|
4950
4969
|
const currentCamX = this.camera.position.x;
|
|
4951
4970
|
const currentCamY = this.camera.position.y;
|
|
4952
|
-
if (
|
|
4971
|
+
if (this.marginViewport.needsRecenter(currentCamX, currentCamY, currentZoom)) {
|
|
4972
|
+
this.marginViewport.recenter(currentCamX, currentCamY, currentZoom);
|
|
4953
4973
|
this.layerCache.markAllDirty();
|
|
4954
|
-
this.
|
|
4955
|
-
this.lastCamX = currentCamX;
|
|
4956
|
-
this.lastCamY = currentCamY;
|
|
4974
|
+
this.gridCacheDirty = true;
|
|
4957
4975
|
}
|
|
4958
4976
|
ctx.save();
|
|
4959
4977
|
ctx.scale(dpr, dpr);
|
|
@@ -4972,13 +4990,13 @@ var RenderLoop = class {
|
|
|
4972
4990
|
ctx.save();
|
|
4973
4991
|
ctx.translate(this.camera.position.x, this.camera.position.y);
|
|
4974
4992
|
ctx.scale(this.camera.zoom, this.camera.zoom);
|
|
4975
|
-
const
|
|
4976
|
-
const
|
|
4993
|
+
const cullBounds = this.marginViewport.cachedWorldBounds();
|
|
4994
|
+
const cullPad = Math.max(cullBounds.w, cullBounds.h) * 0.05;
|
|
4977
4995
|
const cullingRect = {
|
|
4978
|
-
x:
|
|
4979
|
-
y:
|
|
4980
|
-
w:
|
|
4981
|
-
h:
|
|
4996
|
+
x: cullBounds.x - cullPad,
|
|
4997
|
+
y: cullBounds.y - cullPad,
|
|
4998
|
+
w: cullBounds.w + cullPad * 2,
|
|
4999
|
+
h: cullBounds.h + cullPad * 2
|
|
4982
5000
|
};
|
|
4983
5001
|
const allElements = this.store.getAll();
|
|
4984
5002
|
this.layerGroups.clear();
|
|
@@ -5015,13 +5033,13 @@ var RenderLoop = class {
|
|
|
5015
5033
|
const isActiveDrawingLayer = layerId === this.activeDrawingLayerId;
|
|
5016
5034
|
if (!this.layerCache.isDirty(layerId)) {
|
|
5017
5035
|
const compT0 = performance.now();
|
|
5018
|
-
this.compositeLayerCache(ctx, layerId
|
|
5036
|
+
this.compositeLayerCache(ctx, layerId);
|
|
5019
5037
|
compositeMs += performance.now() - compT0;
|
|
5020
5038
|
continue;
|
|
5021
5039
|
}
|
|
5022
5040
|
if (isActiveDrawingLayer) {
|
|
5023
5041
|
const compT0 = performance.now();
|
|
5024
|
-
this.compositeLayerCache(ctx, layerId
|
|
5042
|
+
this.compositeLayerCache(ctx, layerId);
|
|
5025
5043
|
compositeMs += performance.now() - compT0;
|
|
5026
5044
|
continue;
|
|
5027
5045
|
}
|
|
@@ -5031,9 +5049,7 @@ var RenderLoop = class {
|
|
|
5031
5049
|
const offCanvas = this.layerCache.getCanvas(layerId);
|
|
5032
5050
|
offCtx.clearRect(0, 0, offCanvas.width, offCanvas.height);
|
|
5033
5051
|
offCtx.save();
|
|
5034
|
-
|
|
5035
|
-
offCtx.translate(this.camera.position.x, this.camera.position.y);
|
|
5036
|
-
offCtx.scale(this.camera.zoom, this.camera.zoom);
|
|
5052
|
+
this.marginViewport.applyRenderTransform(offCtx);
|
|
5037
5053
|
for (const element of elements) {
|
|
5038
5054
|
const elBounds = getElementBounds(element);
|
|
5039
5055
|
if (elBounds && !boundsIntersect(elBounds, cullingRect)) continue;
|
|
@@ -5043,48 +5059,54 @@ var RenderLoop = class {
|
|
|
5043
5059
|
this.layerCache.markClean(layerId);
|
|
5044
5060
|
layersMs += performance.now() - layerT0;
|
|
5045
5061
|
const compT0 = performance.now();
|
|
5046
|
-
this.compositeLayerCache(ctx, layerId
|
|
5062
|
+
this.compositeLayerCache(ctx, layerId);
|
|
5047
5063
|
compositeMs += performance.now() - compT0;
|
|
5048
5064
|
}
|
|
5049
5065
|
}
|
|
5050
5066
|
if (gridElements.length > 0) {
|
|
5051
5067
|
const gridT0 = performance.now();
|
|
5052
5068
|
const gridRef = gridElements[0];
|
|
5053
|
-
const
|
|
5054
|
-
if (
|
|
5055
|
-
|
|
5056
|
-
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
5057
|
-
ctx.drawImage(this.gridCacheCanvas, 0, 0);
|
|
5058
|
-
ctx.restore();
|
|
5059
|
-
} else {
|
|
5060
|
-
this.ensureGridCache(cssWidth, cssHeight, dpr);
|
|
5069
|
+
const gridDirty = this.gridCacheDirty || gridRef !== this.lastGridRef;
|
|
5070
|
+
if (gridDirty) {
|
|
5071
|
+
this.ensureGridCache();
|
|
5061
5072
|
if (this.gridCacheCtx && this.gridCacheCanvas) {
|
|
5073
|
+
const cb = this.marginViewport.cachedWorldBounds();
|
|
5074
|
+
this.renderer.setGridBoundsOverride({
|
|
5075
|
+
minX: cb.x,
|
|
5076
|
+
minY: cb.y,
|
|
5077
|
+
maxX: cb.x + cb.w,
|
|
5078
|
+
maxY: cb.y + cb.h
|
|
5079
|
+
});
|
|
5062
5080
|
const gc = this.gridCacheCtx;
|
|
5063
5081
|
gc.clearRect(0, 0, this.gridCacheCanvas.width, this.gridCacheCanvas.height);
|
|
5064
5082
|
gc.save();
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
}
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
5074
|
-
ctx.drawImage(this.gridCacheCanvas, 0, 0);
|
|
5075
|
-
ctx.restore();
|
|
5076
|
-
} else {
|
|
5077
|
-
for (const grid of gridElements) {
|
|
5078
|
-
this.renderer.renderCanvasElement(ctx, grid);
|
|
5083
|
+
this.marginViewport.applyRenderTransform(gc);
|
|
5084
|
+
try {
|
|
5085
|
+
for (const grid of gridElements) {
|
|
5086
|
+
this.renderer.renderCanvasElement(gc, grid);
|
|
5087
|
+
}
|
|
5088
|
+
} finally {
|
|
5089
|
+
gc.restore();
|
|
5090
|
+
this.renderer.setGridBoundsOverride(null);
|
|
5079
5091
|
}
|
|
5080
5092
|
}
|
|
5081
|
-
this.
|
|
5082
|
-
this.gridCacheCamX = currentCamX;
|
|
5083
|
-
this.gridCacheCamY = currentCamY;
|
|
5084
|
-
this.gridCacheWidth = cssWidth;
|
|
5085
|
-
this.gridCacheHeight = cssHeight;
|
|
5093
|
+
this.gridCacheDirty = false;
|
|
5086
5094
|
this.lastGridRef = gridRef;
|
|
5087
5095
|
}
|
|
5096
|
+
if (this.gridCacheCanvas) {
|
|
5097
|
+
const offset = this.marginViewport.compositeOffset(
|
|
5098
|
+
this.camera.position.x,
|
|
5099
|
+
this.camera.position.y
|
|
5100
|
+
);
|
|
5101
|
+
ctx.save();
|
|
5102
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
5103
|
+
ctx.drawImage(this.gridCacheCanvas, offset.x, offset.y);
|
|
5104
|
+
ctx.restore();
|
|
5105
|
+
} else {
|
|
5106
|
+
for (const grid of gridElements) {
|
|
5107
|
+
this.renderer.renderCanvasElement(ctx, grid);
|
|
5108
|
+
}
|
|
5109
|
+
}
|
|
5088
5110
|
gridMs = performance.now() - gridT0;
|
|
5089
5111
|
}
|
|
5090
5112
|
const overlayT0 = performance.now();
|
|
@@ -5116,15 +5138,11 @@ function createOffscreenCanvas(width, height) {
|
|
|
5116
5138
|
return canvas;
|
|
5117
5139
|
}
|
|
5118
5140
|
var LayerCache = class {
|
|
5141
|
+
constructor(viewport) {
|
|
5142
|
+
this.viewport = viewport;
|
|
5143
|
+
}
|
|
5119
5144
|
canvases = /* @__PURE__ */ new Map();
|
|
5120
5145
|
dirtyFlags = /* @__PURE__ */ new Map();
|
|
5121
|
-
width;
|
|
5122
|
-
height;
|
|
5123
|
-
constructor(width, height) {
|
|
5124
|
-
const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
|
|
5125
|
-
this.width = Math.round(width * dpr);
|
|
5126
|
-
this.height = Math.round(height * dpr);
|
|
5127
|
-
}
|
|
5128
5146
|
isDirty(layerId) {
|
|
5129
5147
|
return this.dirtyFlags.get(layerId) !== false;
|
|
5130
5148
|
}
|
|
@@ -5142,7 +5160,7 @@ var LayerCache = class {
|
|
|
5142
5160
|
getCanvas(layerId) {
|
|
5143
5161
|
let canvas = this.canvases.get(layerId);
|
|
5144
5162
|
if (!canvas) {
|
|
5145
|
-
canvas = createOffscreenCanvas(this.
|
|
5163
|
+
canvas = createOffscreenCanvas(this.viewport.physicalWidth(), this.viewport.physicalHeight());
|
|
5146
5164
|
this.canvases.set(layerId, canvas);
|
|
5147
5165
|
this.dirtyFlags.set(layerId, true);
|
|
5148
5166
|
}
|
|
@@ -5152,13 +5170,12 @@ var LayerCache = class {
|
|
|
5152
5170
|
const canvas = this.getCanvas(layerId);
|
|
5153
5171
|
return canvas.getContext("2d");
|
|
5154
5172
|
}
|
|
5155
|
-
resize(
|
|
5156
|
-
const
|
|
5157
|
-
|
|
5158
|
-
this.height = Math.round(height * dpr);
|
|
5173
|
+
resize() {
|
|
5174
|
+
const w = this.viewport.physicalWidth();
|
|
5175
|
+
const h = this.viewport.physicalHeight();
|
|
5159
5176
|
for (const [id, canvas] of this.canvases) {
|
|
5160
|
-
canvas.width =
|
|
5161
|
-
canvas.height =
|
|
5177
|
+
canvas.width = w;
|
|
5178
|
+
canvas.height = h;
|
|
5162
5179
|
this.dirtyFlags.set(id, true);
|
|
5163
5180
|
}
|
|
5164
5181
|
}
|
|
@@ -5168,6 +5185,75 @@ var LayerCache = class {
|
|
|
5168
5185
|
}
|
|
5169
5186
|
};
|
|
5170
5187
|
|
|
5188
|
+
// src/canvas/margin-viewport.ts
|
|
5189
|
+
var MarginViewport = class {
|
|
5190
|
+
constructor(marginPx) {
|
|
5191
|
+
this.marginPx = marginPx;
|
|
5192
|
+
}
|
|
5193
|
+
cssW = 0;
|
|
5194
|
+
cssH = 0;
|
|
5195
|
+
dpr = 1;
|
|
5196
|
+
anchorCamX = 0;
|
|
5197
|
+
anchorCamY = 0;
|
|
5198
|
+
anchorZoom = Number.NaN;
|
|
5199
|
+
// sentinel → first needsRecenter is true
|
|
5200
|
+
viewportDirty = true;
|
|
5201
|
+
setMargin(marginPx) {
|
|
5202
|
+
if (marginPx !== this.marginPx) {
|
|
5203
|
+
this.marginPx = marginPx;
|
|
5204
|
+
this.viewportDirty = true;
|
|
5205
|
+
}
|
|
5206
|
+
}
|
|
5207
|
+
setViewport(cssW, cssH, dpr) {
|
|
5208
|
+
if (cssW !== this.cssW || cssH !== this.cssH || dpr !== this.dpr) {
|
|
5209
|
+
this.cssW = cssW;
|
|
5210
|
+
this.cssH = cssH;
|
|
5211
|
+
this.dpr = dpr;
|
|
5212
|
+
this.viewportDirty = true;
|
|
5213
|
+
}
|
|
5214
|
+
}
|
|
5215
|
+
physicalWidth() {
|
|
5216
|
+
return Math.round((this.cssW + 2 * this.marginPx) * this.dpr);
|
|
5217
|
+
}
|
|
5218
|
+
physicalHeight() {
|
|
5219
|
+
return Math.round((this.cssH + 2 * this.marginPx) * this.dpr);
|
|
5220
|
+
}
|
|
5221
|
+
needsRecenter(camX, camY, zoom) {
|
|
5222
|
+
return this.viewportDirty || zoom !== this.anchorZoom || Math.abs(camX - this.anchorCamX) > this.marginPx || Math.abs(camY - this.anchorCamY) > this.marginPx;
|
|
5223
|
+
}
|
|
5224
|
+
recenter(camX, camY, zoom) {
|
|
5225
|
+
this.anchorCamX = camX;
|
|
5226
|
+
this.anchorCamY = camY;
|
|
5227
|
+
this.anchorZoom = zoom;
|
|
5228
|
+
this.viewportDirty = false;
|
|
5229
|
+
}
|
|
5230
|
+
/** Applies dpr scale + anchor-relative world transform. setViewport must have been called first. */
|
|
5231
|
+
applyRenderTransform(ctx) {
|
|
5232
|
+
ctx.scale(this.dpr, this.dpr);
|
|
5233
|
+
ctx.translate(this.marginPx + this.anchorCamX, this.marginPx + this.anchorCamY);
|
|
5234
|
+
ctx.scale(this.anchorZoom, this.anchorZoom);
|
|
5235
|
+
}
|
|
5236
|
+
// Device-px destination for drawImage(cache, x, y).
|
|
5237
|
+
// A world point P sits in the cache at CSS x `margin + anchorCamX + P*zoom`; it must land on
|
|
5238
|
+
// screen at `camX + P*zoom`; so the blit offset is `camX - anchorCamX - margin` (CSS) * dpr.
|
|
5239
|
+
compositeOffset(camX, camY) {
|
|
5240
|
+
return {
|
|
5241
|
+
x: (camX - this.anchorCamX - this.marginPx) * this.dpr,
|
|
5242
|
+
y: (camY - this.anchorCamY - this.marginPx) * this.dpr
|
|
5243
|
+
};
|
|
5244
|
+
}
|
|
5245
|
+
// World-space bounds of the whole cached region at the anchor (cull rect for re-renders).
|
|
5246
|
+
cachedWorldBounds() {
|
|
5247
|
+
const z = this.anchorZoom;
|
|
5248
|
+
return {
|
|
5249
|
+
x: (-this.marginPx - this.anchorCamX) / z,
|
|
5250
|
+
y: (-this.marginPx - this.anchorCamY) / z,
|
|
5251
|
+
w: (this.cssW + 2 * this.marginPx) / z,
|
|
5252
|
+
h: (this.cssH + 2 * this.marginPx) / z
|
|
5253
|
+
};
|
|
5254
|
+
}
|
|
5255
|
+
};
|
|
5256
|
+
|
|
5171
5257
|
// src/canvas/viewport.ts
|
|
5172
5258
|
var Viewport = class {
|
|
5173
5259
|
constructor(container, options = {}) {
|
|
@@ -5185,13 +5271,13 @@ var Viewport = class {
|
|
|
5185
5271
|
this.renderLoop.markAllLayersDirty();
|
|
5186
5272
|
this.requestRender();
|
|
5187
5273
|
});
|
|
5188
|
-
this.renderer.setOnImageError((src) => {
|
|
5274
|
+
this.renderer.setOnImageError((src, cause) => {
|
|
5189
5275
|
const elementIds = [];
|
|
5190
5276
|
for (const el of this.store.getAll()) {
|
|
5191
5277
|
if (el.type === "image" && el.src === src) elementIds.push(el.id);
|
|
5192
5278
|
}
|
|
5193
5279
|
if (options.onImageError) {
|
|
5194
|
-
options.onImageError({ src, elementIds });
|
|
5280
|
+
options.onImageError({ src, elementIds, cause });
|
|
5195
5281
|
} else {
|
|
5196
5282
|
console.warn(`[fieldnotes] image failed to load: ${src}`);
|
|
5197
5283
|
}
|
|
@@ -5244,10 +5330,13 @@ var Viewport = class {
|
|
|
5244
5330
|
this.interactMode = new InteractMode({
|
|
5245
5331
|
getNode: (id) => this.domNodeManager.getNode(id)
|
|
5246
5332
|
});
|
|
5247
|
-
|
|
5333
|
+
this.marginViewport = new MarginViewport(options.panBufferMargin ?? 256);
|
|
5334
|
+
this.marginViewport.setViewport(
|
|
5248
5335
|
this.canvasEl.clientWidth || 800,
|
|
5249
|
-
this.canvasEl.clientHeight || 600
|
|
5336
|
+
this.canvasEl.clientHeight || 600,
|
|
5337
|
+
typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1
|
|
5250
5338
|
);
|
|
5339
|
+
const layerCache = new LayerCache(this.marginViewport);
|
|
5251
5340
|
this.renderLoop = new RenderLoop({
|
|
5252
5341
|
canvasEl: this.canvasEl,
|
|
5253
5342
|
camera: this.camera,
|
|
@@ -5257,7 +5346,8 @@ var Viewport = class {
|
|
|
5257
5346
|
toolManager: this.toolManager,
|
|
5258
5347
|
layerManager: this.layerManager,
|
|
5259
5348
|
domNodeManager: this.domNodeManager,
|
|
5260
|
-
layerCache
|
|
5349
|
+
layerCache,
|
|
5350
|
+
marginViewport: this.marginViewport
|
|
5261
5351
|
});
|
|
5262
5352
|
this.unsubCamera = this.camera.onChange(() => {
|
|
5263
5353
|
this.applyCameraTransform();
|
|
@@ -5321,6 +5411,7 @@ var Viewport = class {
|
|
|
5321
5411
|
noteEditor;
|
|
5322
5412
|
historyRecorder;
|
|
5323
5413
|
toolContext;
|
|
5414
|
+
marginViewport;
|
|
5324
5415
|
resizeObserver = null;
|
|
5325
5416
|
_snapToGrid = false;
|
|
5326
5417
|
_gridSize;
|
|
@@ -5405,6 +5496,10 @@ var Viewport = class {
|
|
|
5405
5496
|
this.loadState(parseState(json));
|
|
5406
5497
|
}
|
|
5407
5498
|
setTool(name) {
|
|
5499
|
+
if (!this.toolManager.getTool(name)) {
|
|
5500
|
+
console.warn(`[fieldnotes] setTool: no tool registered as "${name}"`);
|
|
5501
|
+
return;
|
|
5502
|
+
}
|
|
5408
5503
|
this.toolManager.setTool(name, this.toolContext);
|
|
5409
5504
|
}
|
|
5410
5505
|
get shortcuts() {
|
|
@@ -5901,20 +5996,6 @@ var PencilTool = class {
|
|
|
5901
5996
|
};
|
|
5902
5997
|
|
|
5903
5998
|
// src/elements/stroke-hit.ts
|
|
5904
|
-
function distSqToSegment(p, a, b) {
|
|
5905
|
-
const abx = b.x - a.x;
|
|
5906
|
-
const aby = b.y - a.y;
|
|
5907
|
-
const apx = p.x - a.x;
|
|
5908
|
-
const apy = p.y - a.y;
|
|
5909
|
-
const lenSq = abx * abx + aby * aby;
|
|
5910
|
-
if (lenSq === 0) {
|
|
5911
|
-
return apx * apx + apy * apy;
|
|
5912
|
-
}
|
|
5913
|
-
const t = Math.max(0, Math.min(1, (apx * abx + apy * aby) / lenSq));
|
|
5914
|
-
const dx = p.x - (a.x + t * abx);
|
|
5915
|
-
const dy = p.y - (a.y + t * aby);
|
|
5916
|
-
return dx * dx + dy * dy;
|
|
5917
|
-
}
|
|
5918
5999
|
function hitTestStroke(stroke, point, radius) {
|
|
5919
6000
|
const bounds = getElementBounds(stroke);
|
|
5920
6001
|
if (!bounds) return false;
|
|
@@ -7511,7 +7592,7 @@ var TemplateTool = class {
|
|
|
7511
7592
|
};
|
|
7512
7593
|
|
|
7513
7594
|
// src/index.ts
|
|
7514
|
-
var VERSION = "0.
|
|
7595
|
+
var VERSION = "0.24.0";
|
|
7515
7596
|
// Annotate the CommonJS export names for ESM import in node:
|
|
7516
7597
|
0 && (module.exports = {
|
|
7517
7598
|
AddElementCommand,
|