@fieldnotes/core 0.21.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -0
- package/dist/index.cjs +408 -226
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +15 -1
- package/dist/index.d.ts +15 -1
- package/dist/index.js +408 -226
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1933,10 +1933,134 @@ function getTemplateBounds(el) {
|
|
|
1933
1933
|
}
|
|
1934
1934
|
}
|
|
1935
1935
|
}
|
|
1936
|
+
function transferStrokeBounds(prev, next) {
|
|
1937
|
+
if (prev.type !== "stroke" || next.type !== "stroke") return;
|
|
1938
|
+
if (prev.points !== next.points) return;
|
|
1939
|
+
if (prev.position.x !== next.position.x || prev.position.y !== next.position.y) return;
|
|
1940
|
+
const bounds = strokeBoundsCache.get(prev);
|
|
1941
|
+
if (bounds) strokeBoundsCache.set(next, bounds);
|
|
1942
|
+
}
|
|
1936
1943
|
function boundsIntersect(a, b) {
|
|
1937
1944
|
return a.x <= b.x + b.w && a.x + a.w >= b.x && a.y <= b.y + b.h && a.y + a.h >= b.y;
|
|
1938
1945
|
}
|
|
1939
1946
|
|
|
1947
|
+
// src/elements/stroke-smoothing.ts
|
|
1948
|
+
var MIN_PRESSURE_SCALE = 0.2;
|
|
1949
|
+
function pressureToWidth(pressure, baseWidth) {
|
|
1950
|
+
return baseWidth * (MIN_PRESSURE_SCALE + (1 - MIN_PRESSURE_SCALE) * pressure);
|
|
1951
|
+
}
|
|
1952
|
+
function simplifyPoints(points, tolerance) {
|
|
1953
|
+
if (points.length <= 2) return points.slice();
|
|
1954
|
+
return rdp(points, 0, points.length - 1, tolerance);
|
|
1955
|
+
}
|
|
1956
|
+
function rdp(points, start, end, tolerance) {
|
|
1957
|
+
const first = points[start];
|
|
1958
|
+
const last = points[end];
|
|
1959
|
+
if (!first || !last) return [];
|
|
1960
|
+
if (end - start <= 1) return [first, last];
|
|
1961
|
+
let maxDist = 0;
|
|
1962
|
+
let maxIndex = start;
|
|
1963
|
+
for (let i = start + 1; i < end; i++) {
|
|
1964
|
+
const pt = points[i];
|
|
1965
|
+
if (!pt) continue;
|
|
1966
|
+
const dist = perpendicularDistance(pt, first, last);
|
|
1967
|
+
if (dist > maxDist) {
|
|
1968
|
+
maxDist = dist;
|
|
1969
|
+
maxIndex = i;
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
if (maxDist <= tolerance) return [first, last];
|
|
1973
|
+
const left = rdp(points, start, maxIndex, tolerance);
|
|
1974
|
+
const right = rdp(points, maxIndex, end, tolerance);
|
|
1975
|
+
return left.concat(right.slice(1));
|
|
1976
|
+
}
|
|
1977
|
+
function perpendicularDistance(pt, lineStart, lineEnd) {
|
|
1978
|
+
const dx = lineEnd.x - lineStart.x;
|
|
1979
|
+
const dy = lineEnd.y - lineStart.y;
|
|
1980
|
+
const lenSq = dx * dx + dy * dy;
|
|
1981
|
+
if (lenSq === 0) {
|
|
1982
|
+
const ex = pt.x - lineStart.x;
|
|
1983
|
+
const ey = pt.y - lineStart.y;
|
|
1984
|
+
return Math.sqrt(ex * ex + ey * ey);
|
|
1985
|
+
}
|
|
1986
|
+
const num = Math.abs(dy * pt.x - dx * pt.y + lineEnd.x * lineStart.y - lineEnd.y * lineStart.x);
|
|
1987
|
+
return num / Math.sqrt(lenSq);
|
|
1988
|
+
}
|
|
1989
|
+
function smoothToSegments(points) {
|
|
1990
|
+
if (points.length < 2) return [];
|
|
1991
|
+
if (points.length === 2) {
|
|
1992
|
+
const p0 = points[0];
|
|
1993
|
+
const p1 = points[1];
|
|
1994
|
+
if (!p0 || !p1) return [];
|
|
1995
|
+
const mx = (p0.x + p1.x) / 2;
|
|
1996
|
+
const my = (p0.y + p1.y) / 2;
|
|
1997
|
+
return [{ start: p0, cp1: { x: mx, y: my }, cp2: { x: mx, y: my }, end: p1 }];
|
|
1998
|
+
}
|
|
1999
|
+
const segments = [];
|
|
2000
|
+
const n = points.length;
|
|
2001
|
+
for (let i = 0; i < n - 1; i++) {
|
|
2002
|
+
const p0 = points[Math.max(0, i - 1)];
|
|
2003
|
+
const p1 = points[i];
|
|
2004
|
+
const p2 = points[i + 1];
|
|
2005
|
+
const p3 = points[Math.min(n - 1, i + 2)];
|
|
2006
|
+
if (!p0 || !p1 || !p2 || !p3) continue;
|
|
2007
|
+
const cp1 = {
|
|
2008
|
+
x: p1.x + (p2.x - p0.x) / 6,
|
|
2009
|
+
y: p1.y + (p2.y - p0.y) / 6
|
|
2010
|
+
};
|
|
2011
|
+
const cp2 = {
|
|
2012
|
+
x: p2.x - (p3.x - p1.x) / 6,
|
|
2013
|
+
y: p2.y - (p3.y - p1.y) / 6
|
|
2014
|
+
};
|
|
2015
|
+
segments.push({ start: p1, cp1, cp2, end: p2 });
|
|
2016
|
+
}
|
|
2017
|
+
return segments;
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
// src/elements/stroke-cache.ts
|
|
2021
|
+
var cache = /* @__PURE__ */ new WeakMap();
|
|
2022
|
+
var WIDTH_QUANTUM = 0.25;
|
|
2023
|
+
function buildWidthBuckets(segments, widths) {
|
|
2024
|
+
if (typeof Path2D === "undefined") return null;
|
|
2025
|
+
const byWidth = /* @__PURE__ */ new Map();
|
|
2026
|
+
for (let i = 0; i < segments.length; i++) {
|
|
2027
|
+
const seg = segments[i];
|
|
2028
|
+
const w = widths[i];
|
|
2029
|
+
if (!seg || w === void 0) continue;
|
|
2030
|
+
const q = Math.max(WIDTH_QUANTUM, Math.round(w / WIDTH_QUANTUM) * WIDTH_QUANTUM);
|
|
2031
|
+
let path = byWidth.get(q);
|
|
2032
|
+
if (!path) {
|
|
2033
|
+
path = new Path2D();
|
|
2034
|
+
byWidth.set(q, path);
|
|
2035
|
+
}
|
|
2036
|
+
path.moveTo(seg.start.x, seg.start.y);
|
|
2037
|
+
path.bezierCurveTo(seg.cp1.x, seg.cp1.y, seg.cp2.x, seg.cp2.y, seg.end.x, seg.end.y);
|
|
2038
|
+
}
|
|
2039
|
+
return [...byWidth.entries()].map(([width, path]) => ({ width, path }));
|
|
2040
|
+
}
|
|
2041
|
+
function computeStrokeSegments(stroke) {
|
|
2042
|
+
const segments = smoothToSegments(stroke.points);
|
|
2043
|
+
const widths = [];
|
|
2044
|
+
for (const seg of segments) {
|
|
2045
|
+
const w = (pressureToWidth(seg.start.pressure, stroke.width) + pressureToWidth(seg.end.pressure, stroke.width)) / 2;
|
|
2046
|
+
widths.push(w);
|
|
2047
|
+
}
|
|
2048
|
+
const data = { segments, widths, buckets: buildWidthBuckets(segments, widths) };
|
|
2049
|
+
cache.set(stroke, data);
|
|
2050
|
+
return data;
|
|
2051
|
+
}
|
|
2052
|
+
function getStrokeRenderData(stroke) {
|
|
2053
|
+
const cached = cache.get(stroke);
|
|
2054
|
+
if (cached) return cached;
|
|
2055
|
+
return computeStrokeSegments(stroke);
|
|
2056
|
+
}
|
|
2057
|
+
function transferStrokeRenderData(prev, next) {
|
|
2058
|
+
if (prev.type !== "stroke" || next.type !== "stroke") return;
|
|
2059
|
+
if (prev.points !== next.points) return;
|
|
2060
|
+
const data = cache.get(prev);
|
|
2061
|
+
if (data) cache.set(next, data);
|
|
2062
|
+
}
|
|
2063
|
+
|
|
1940
2064
|
// src/elements/element-store.ts
|
|
1941
2065
|
var ElementStore = class {
|
|
1942
2066
|
elements = /* @__PURE__ */ new Map();
|
|
@@ -1987,6 +2111,10 @@ var ElementStore = class {
|
|
|
1987
2111
|
this.sortedCache = null;
|
|
1988
2112
|
this._versions.set(id, (this._versions.get(id) ?? 0) + 1);
|
|
1989
2113
|
const updated = { ...existing, ...partial, id: existing.id, type: existing.type };
|
|
2114
|
+
if (updated.type === "stroke" && existing.type === "stroke") {
|
|
2115
|
+
transferStrokeRenderData(existing, updated);
|
|
2116
|
+
transferStrokeBounds(existing, updated);
|
|
2117
|
+
}
|
|
1990
2118
|
if (updated.type === "arrow") {
|
|
1991
2119
|
const arrow = updated;
|
|
1992
2120
|
arrow.cachedControlPoint = getArrowControlPoint(arrow.from, arrow.to, arrow.bend);
|
|
@@ -2030,6 +2158,12 @@ var ElementStore = class {
|
|
|
2030
2158
|
this._versions.set(el.id, 0);
|
|
2031
2159
|
const bounds = getElementBounds(el);
|
|
2032
2160
|
if (bounds) this.spatialIndex.insert(el.id, bounds);
|
|
2161
|
+
if (el.type === "stroke") {
|
|
2162
|
+
computeStrokeSegments(el);
|
|
2163
|
+
}
|
|
2164
|
+
if (el.type === "arrow" && el.bend !== 0 && !el.cachedControlPoint) {
|
|
2165
|
+
el.cachedControlPoint = getArrowControlPoint(el.from, el.to, el.bend);
|
|
2166
|
+
}
|
|
2033
2167
|
}
|
|
2034
2168
|
this.bus.emit("clear", null);
|
|
2035
2169
|
for (const el of elements) {
|
|
@@ -2115,6 +2249,20 @@ var ElementStore = class {
|
|
|
2115
2249
|
}
|
|
2116
2250
|
};
|
|
2117
2251
|
|
|
2252
|
+
// src/elements/arrow-render-cache.ts
|
|
2253
|
+
var cache2 = /* @__PURE__ */ new WeakMap();
|
|
2254
|
+
function getArrowRenderGeometry(arrow) {
|
|
2255
|
+
const hit = cache2.get(arrow);
|
|
2256
|
+
if (hit) return hit;
|
|
2257
|
+
const geometry = {
|
|
2258
|
+
controlPoint: arrow.bend !== 0 ? arrow.cachedControlPoint ?? getArrowControlPoint(arrow.from, arrow.to, arrow.bend) : null,
|
|
2259
|
+
tangentStart: getArrowTangentAngle(arrow.from, arrow.to, arrow.bend, 0),
|
|
2260
|
+
tangentEnd: getArrowTangentAngle(arrow.from, arrow.to, arrow.bend, 1)
|
|
2261
|
+
};
|
|
2262
|
+
cache2.set(arrow, geometry);
|
|
2263
|
+
return geometry;
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2118
2266
|
// src/elements/arrow-binding.ts
|
|
2119
2267
|
var BINDABLE_TYPES = /* @__PURE__ */ new Set(["note", "text", "image", "html", "shape"]);
|
|
2120
2268
|
function isBindable(element) {
|
|
@@ -2235,98 +2383,6 @@ function unbindArrow(arrow, store) {
|
|
|
2235
2383
|
return updates;
|
|
2236
2384
|
}
|
|
2237
2385
|
|
|
2238
|
-
// src/elements/stroke-smoothing.ts
|
|
2239
|
-
var MIN_PRESSURE_SCALE = 0.2;
|
|
2240
|
-
function pressureToWidth(pressure, baseWidth) {
|
|
2241
|
-
return baseWidth * (MIN_PRESSURE_SCALE + (1 - MIN_PRESSURE_SCALE) * pressure);
|
|
2242
|
-
}
|
|
2243
|
-
function simplifyPoints(points, tolerance) {
|
|
2244
|
-
if (points.length <= 2) return points.slice();
|
|
2245
|
-
return rdp(points, 0, points.length - 1, tolerance);
|
|
2246
|
-
}
|
|
2247
|
-
function rdp(points, start, end, tolerance) {
|
|
2248
|
-
const first = points[start];
|
|
2249
|
-
const last = points[end];
|
|
2250
|
-
if (!first || !last) return [];
|
|
2251
|
-
if (end - start <= 1) return [first, last];
|
|
2252
|
-
let maxDist = 0;
|
|
2253
|
-
let maxIndex = start;
|
|
2254
|
-
for (let i = start + 1; i < end; i++) {
|
|
2255
|
-
const pt = points[i];
|
|
2256
|
-
if (!pt) continue;
|
|
2257
|
-
const dist = perpendicularDistance(pt, first, last);
|
|
2258
|
-
if (dist > maxDist) {
|
|
2259
|
-
maxDist = dist;
|
|
2260
|
-
maxIndex = i;
|
|
2261
|
-
}
|
|
2262
|
-
}
|
|
2263
|
-
if (maxDist <= tolerance) return [first, last];
|
|
2264
|
-
const left = rdp(points, start, maxIndex, tolerance);
|
|
2265
|
-
const right = rdp(points, maxIndex, end, tolerance);
|
|
2266
|
-
return left.concat(right.slice(1));
|
|
2267
|
-
}
|
|
2268
|
-
function perpendicularDistance(pt, lineStart, lineEnd) {
|
|
2269
|
-
const dx = lineEnd.x - lineStart.x;
|
|
2270
|
-
const dy = lineEnd.y - lineStart.y;
|
|
2271
|
-
const lenSq = dx * dx + dy * dy;
|
|
2272
|
-
if (lenSq === 0) {
|
|
2273
|
-
const ex = pt.x - lineStart.x;
|
|
2274
|
-
const ey = pt.y - lineStart.y;
|
|
2275
|
-
return Math.sqrt(ex * ex + ey * ey);
|
|
2276
|
-
}
|
|
2277
|
-
const num = Math.abs(dy * pt.x - dx * pt.y + lineEnd.x * lineStart.y - lineEnd.y * lineStart.x);
|
|
2278
|
-
return num / Math.sqrt(lenSq);
|
|
2279
|
-
}
|
|
2280
|
-
function smoothToSegments(points) {
|
|
2281
|
-
if (points.length < 2) return [];
|
|
2282
|
-
if (points.length === 2) {
|
|
2283
|
-
const p0 = points[0];
|
|
2284
|
-
const p1 = points[1];
|
|
2285
|
-
if (!p0 || !p1) return [];
|
|
2286
|
-
const mx = (p0.x + p1.x) / 2;
|
|
2287
|
-
const my = (p0.y + p1.y) / 2;
|
|
2288
|
-
return [{ start: p0, cp1: { x: mx, y: my }, cp2: { x: mx, y: my }, end: p1 }];
|
|
2289
|
-
}
|
|
2290
|
-
const segments = [];
|
|
2291
|
-
const n = points.length;
|
|
2292
|
-
for (let i = 0; i < n - 1; i++) {
|
|
2293
|
-
const p0 = points[Math.max(0, i - 1)];
|
|
2294
|
-
const p1 = points[i];
|
|
2295
|
-
const p2 = points[i + 1];
|
|
2296
|
-
const p3 = points[Math.min(n - 1, i + 2)];
|
|
2297
|
-
if (!p0 || !p1 || !p2 || !p3) continue;
|
|
2298
|
-
const cp1 = {
|
|
2299
|
-
x: p1.x + (p2.x - p0.x) / 6,
|
|
2300
|
-
y: p1.y + (p2.y - p0.y) / 6
|
|
2301
|
-
};
|
|
2302
|
-
const cp2 = {
|
|
2303
|
-
x: p2.x - (p3.x - p1.x) / 6,
|
|
2304
|
-
y: p2.y - (p3.y - p1.y) / 6
|
|
2305
|
-
};
|
|
2306
|
-
segments.push({ start: p1, cp1, cp2, end: p2 });
|
|
2307
|
-
}
|
|
2308
|
-
return segments;
|
|
2309
|
-
}
|
|
2310
|
-
|
|
2311
|
-
// src/elements/stroke-cache.ts
|
|
2312
|
-
var cache = /* @__PURE__ */ new WeakMap();
|
|
2313
|
-
function computeStrokeSegments(stroke) {
|
|
2314
|
-
const segments = smoothToSegments(stroke.points);
|
|
2315
|
-
const widths = [];
|
|
2316
|
-
for (const seg of segments) {
|
|
2317
|
-
const w = (pressureToWidth(seg.start.pressure, stroke.width) + pressureToWidth(seg.end.pressure, stroke.width)) / 2;
|
|
2318
|
-
widths.push(w);
|
|
2319
|
-
}
|
|
2320
|
-
const data = { segments, widths };
|
|
2321
|
-
cache.set(stroke, data);
|
|
2322
|
-
return data;
|
|
2323
|
-
}
|
|
2324
|
-
function getStrokeRenderData(stroke) {
|
|
2325
|
-
const cached = cache.get(stroke);
|
|
2326
|
-
if (cached) return cached;
|
|
2327
|
-
return computeStrokeSegments(stroke);
|
|
2328
|
-
}
|
|
2329
|
-
|
|
2330
2386
|
// src/elements/grid-renderer.ts
|
|
2331
2387
|
function getSquareGridLines(bounds, cellSize) {
|
|
2332
2388
|
if (cellSize <= 0) return { verticals: [], horizontals: [] };
|
|
@@ -2724,6 +2780,7 @@ var ElementRenderer = class {
|
|
|
2724
2780
|
canvasSize = null;
|
|
2725
2781
|
hexTileCache = null;
|
|
2726
2782
|
hexTileCacheKey = "";
|
|
2783
|
+
gridBoundsOverride = null;
|
|
2727
2784
|
setStore(store) {
|
|
2728
2785
|
this.store = store;
|
|
2729
2786
|
}
|
|
@@ -2739,6 +2796,9 @@ var ElementRenderer = class {
|
|
|
2739
2796
|
setCanvasSize(w, h) {
|
|
2740
2797
|
this.canvasSize = { w, h };
|
|
2741
2798
|
}
|
|
2799
|
+
setGridBoundsOverride(bounds) {
|
|
2800
|
+
this.gridBoundsOverride = bounds;
|
|
2801
|
+
}
|
|
2742
2802
|
isDomElement(element) {
|
|
2743
2803
|
return DOM_ELEMENT_TYPES.has(element.type);
|
|
2744
2804
|
}
|
|
@@ -2772,21 +2832,29 @@ var ElementRenderer = class {
|
|
|
2772
2832
|
ctx.lineCap = "round";
|
|
2773
2833
|
ctx.lineJoin = "round";
|
|
2774
2834
|
ctx.globalAlpha = stroke.opacity;
|
|
2775
|
-
const
|
|
2776
|
-
|
|
2777
|
-
const
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2835
|
+
const data = getStrokeRenderData(stroke);
|
|
2836
|
+
if (data.buckets) {
|
|
2837
|
+
for (const bucket of data.buckets) {
|
|
2838
|
+
ctx.lineWidth = bucket.width;
|
|
2839
|
+
ctx.stroke(bucket.path);
|
|
2840
|
+
}
|
|
2841
|
+
} else {
|
|
2842
|
+
for (let i = 0; i < data.segments.length; i++) {
|
|
2843
|
+
const seg = data.segments[i];
|
|
2844
|
+
const w = data.widths[i];
|
|
2845
|
+
if (!seg || w === void 0) continue;
|
|
2846
|
+
ctx.lineWidth = w;
|
|
2847
|
+
ctx.beginPath();
|
|
2848
|
+
ctx.moveTo(seg.start.x, seg.start.y);
|
|
2849
|
+
ctx.bezierCurveTo(seg.cp1.x, seg.cp1.y, seg.cp2.x, seg.cp2.y, seg.end.x, seg.end.y);
|
|
2850
|
+
ctx.stroke();
|
|
2851
|
+
}
|
|
2785
2852
|
}
|
|
2786
2853
|
ctx.restore();
|
|
2787
2854
|
}
|
|
2788
2855
|
renderArrow(ctx, arrow) {
|
|
2789
|
-
const
|
|
2856
|
+
const geometry = getArrowRenderGeometry(arrow);
|
|
2857
|
+
const { visualFrom, visualTo } = this.getVisualEndpoints(arrow, geometry);
|
|
2790
2858
|
ctx.save();
|
|
2791
2859
|
ctx.strokeStyle = arrow.color;
|
|
2792
2860
|
ctx.lineWidth = arrow.width;
|
|
@@ -2797,17 +2865,18 @@ var ElementRenderer = class {
|
|
|
2797
2865
|
ctx.beginPath();
|
|
2798
2866
|
ctx.moveTo(visualFrom.x, visualFrom.y);
|
|
2799
2867
|
if (arrow.bend !== 0) {
|
|
2800
|
-
const cp =
|
|
2801
|
-
|
|
2868
|
+
const cp = geometry.controlPoint;
|
|
2869
|
+
if (cp) {
|
|
2870
|
+
ctx.quadraticCurveTo(cp.x, cp.y, visualTo.x, visualTo.y);
|
|
2871
|
+
}
|
|
2802
2872
|
} else {
|
|
2803
2873
|
ctx.lineTo(visualTo.x, visualTo.y);
|
|
2804
2874
|
}
|
|
2805
2875
|
ctx.stroke();
|
|
2806
|
-
this.renderArrowhead(ctx, arrow, visualTo);
|
|
2876
|
+
this.renderArrowhead(ctx, arrow, visualTo, geometry.tangentEnd);
|
|
2807
2877
|
ctx.restore();
|
|
2808
2878
|
}
|
|
2809
|
-
renderArrowhead(ctx, arrow, tip) {
|
|
2810
|
-
const angle = getArrowTangentAngle(arrow.from, arrow.to, arrow.bend, 1);
|
|
2879
|
+
renderArrowhead(ctx, arrow, tip, angle) {
|
|
2811
2880
|
ctx.beginPath();
|
|
2812
2881
|
ctx.moveTo(tip.x, tip.y);
|
|
2813
2882
|
ctx.lineTo(
|
|
@@ -2822,7 +2891,7 @@ var ElementRenderer = class {
|
|
|
2822
2891
|
ctx.fillStyle = arrow.color;
|
|
2823
2892
|
ctx.fill();
|
|
2824
2893
|
}
|
|
2825
|
-
getVisualEndpoints(arrow) {
|
|
2894
|
+
getVisualEndpoints(arrow, geometry) {
|
|
2826
2895
|
let visualFrom = arrow.from;
|
|
2827
2896
|
let visualTo = arrow.to;
|
|
2828
2897
|
if (!this.store) return { visualFrom, visualTo };
|
|
@@ -2831,7 +2900,7 @@ var ElementRenderer = class {
|
|
|
2831
2900
|
if (el) {
|
|
2832
2901
|
const bounds = getElementBounds(el);
|
|
2833
2902
|
if (bounds) {
|
|
2834
|
-
const tangentAngle =
|
|
2903
|
+
const tangentAngle = geometry.tangentStart;
|
|
2835
2904
|
const rayTarget = {
|
|
2836
2905
|
x: arrow.from.x + Math.cos(tangentAngle) * 1e3,
|
|
2837
2906
|
y: arrow.from.y + Math.sin(tangentAngle) * 1e3
|
|
@@ -2845,7 +2914,7 @@ var ElementRenderer = class {
|
|
|
2845
2914
|
if (el) {
|
|
2846
2915
|
const bounds = getElementBounds(el);
|
|
2847
2916
|
if (bounds) {
|
|
2848
|
-
const tangentAngle =
|
|
2917
|
+
const tangentAngle = geometry.tangentEnd;
|
|
2849
2918
|
const rayTarget = {
|
|
2850
2919
|
x: arrow.to.x - Math.cos(tangentAngle) * 1e3,
|
|
2851
2920
|
y: arrow.to.y - Math.sin(tangentAngle) * 1e3
|
|
@@ -2900,20 +2969,20 @@ var ElementRenderer = class {
|
|
|
2900
2969
|
}
|
|
2901
2970
|
}
|
|
2902
2971
|
renderGrid(ctx, grid) {
|
|
2903
|
-
|
|
2972
|
+
const canvasSize = this.canvasSize;
|
|
2973
|
+
if (!canvasSize) return;
|
|
2904
2974
|
const cam = this.camera;
|
|
2905
2975
|
if (!cam) return;
|
|
2906
|
-
const
|
|
2907
|
-
|
|
2908
|
-
x:
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
};
|
|
2976
|
+
const bounds = this.gridBoundsOverride ?? (() => {
|
|
2977
|
+
const topLeft = cam.screenToWorld({ x: 0, y: 0 });
|
|
2978
|
+
const bottomRight = cam.screenToWorld({ x: canvasSize.w, y: canvasSize.h });
|
|
2979
|
+
return {
|
|
2980
|
+
minX: topLeft.x,
|
|
2981
|
+
minY: topLeft.y,
|
|
2982
|
+
maxX: bottomRight.x,
|
|
2983
|
+
maxY: bottomRight.y
|
|
2984
|
+
};
|
|
2985
|
+
})();
|
|
2917
2986
|
if (grid.gridType === "hex") {
|
|
2918
2987
|
const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
|
|
2919
2988
|
const scale = cam.zoom * dpr;
|
|
@@ -4209,19 +4278,19 @@ function loadImages(elements) {
|
|
|
4209
4278
|
const imageElements = elements.filter(
|
|
4210
4279
|
(el) => el.type === "image" && "src" in el
|
|
4211
4280
|
);
|
|
4212
|
-
const
|
|
4213
|
-
if (imageElements.length === 0) return Promise.resolve(
|
|
4281
|
+
const cache3 = /* @__PURE__ */ new Map();
|
|
4282
|
+
if (imageElements.length === 0) return Promise.resolve(cache3);
|
|
4214
4283
|
return new Promise((resolve) => {
|
|
4215
4284
|
let remaining = imageElements.length;
|
|
4216
4285
|
const done = () => {
|
|
4217
4286
|
remaining--;
|
|
4218
|
-
if (remaining <= 0) resolve(
|
|
4287
|
+
if (remaining <= 0) resolve(cache3);
|
|
4219
4288
|
};
|
|
4220
4289
|
for (const el of imageElements) {
|
|
4221
4290
|
const img = new Image();
|
|
4222
4291
|
img.crossOrigin = "anonymous";
|
|
4223
4292
|
img.onload = () => {
|
|
4224
|
-
|
|
4293
|
+
cache3.set(el.id, img);
|
|
4225
4294
|
done();
|
|
4226
4295
|
};
|
|
4227
4296
|
img.onerror = done;
|
|
@@ -4702,18 +4771,39 @@ var RenderStats = class {
|
|
|
4702
4771
|
frameTimes = [];
|
|
4703
4772
|
frameCount = 0;
|
|
4704
4773
|
_lastGridMs = 0;
|
|
4705
|
-
|
|
4774
|
+
_lastLayersMs = 0;
|
|
4775
|
+
_lastBackgroundMs = 0;
|
|
4776
|
+
_lastCompositeMs = 0;
|
|
4777
|
+
_lastOverlayMs = 0;
|
|
4778
|
+
recordFrame(durationMs, breakdown) {
|
|
4706
4779
|
this.frameCount++;
|
|
4707
4780
|
this.frameTimes.push(durationMs);
|
|
4708
4781
|
if (this.frameTimes.length > SAMPLE_SIZE) {
|
|
4709
4782
|
this.frameTimes.shift();
|
|
4710
4783
|
}
|
|
4711
|
-
if (
|
|
4784
|
+
if (breakdown !== void 0) {
|
|
4785
|
+
if (breakdown.gridMs !== void 0) this._lastGridMs = breakdown.gridMs;
|
|
4786
|
+
if (breakdown.layersMs !== void 0) this._lastLayersMs = breakdown.layersMs;
|
|
4787
|
+
if (breakdown.backgroundMs !== void 0) this._lastBackgroundMs = breakdown.backgroundMs;
|
|
4788
|
+
if (breakdown.compositeMs !== void 0) this._lastCompositeMs = breakdown.compositeMs;
|
|
4789
|
+
if (breakdown.overlayMs !== void 0) this._lastOverlayMs = breakdown.overlayMs;
|
|
4790
|
+
}
|
|
4712
4791
|
}
|
|
4713
4792
|
getSnapshot() {
|
|
4714
4793
|
const times = this.frameTimes;
|
|
4715
4794
|
if (times.length === 0) {
|
|
4716
|
-
return {
|
|
4795
|
+
return {
|
|
4796
|
+
fps: 0,
|
|
4797
|
+
avgFrameMs: 0,
|
|
4798
|
+
p95FrameMs: 0,
|
|
4799
|
+
lastFrameMs: 0,
|
|
4800
|
+
lastGridMs: 0,
|
|
4801
|
+
layersMs: 0,
|
|
4802
|
+
backgroundMs: 0,
|
|
4803
|
+
compositeMs: 0,
|
|
4804
|
+
overlayMs: 0,
|
|
4805
|
+
frameCount: 0
|
|
4806
|
+
};
|
|
4717
4807
|
}
|
|
4718
4808
|
const avg = times.reduce((a, b) => a + b, 0) / times.length;
|
|
4719
4809
|
const sorted = [...times].sort((a, b) => a - b);
|
|
@@ -4725,6 +4815,10 @@ var RenderStats = class {
|
|
|
4725
4815
|
p95FrameMs: Math.round((sorted[p95Index] ?? 0) * 100) / 100,
|
|
4726
4816
|
lastFrameMs: Math.round(lastFrame * 100) / 100,
|
|
4727
4817
|
lastGridMs: Math.round(this._lastGridMs * 100) / 100,
|
|
4818
|
+
layersMs: Math.round(this._lastLayersMs * 100) / 100,
|
|
4819
|
+
backgroundMs: Math.round(this._lastBackgroundMs * 100) / 100,
|
|
4820
|
+
compositeMs: Math.round(this._lastCompositeMs * 100) / 100,
|
|
4821
|
+
overlayMs: Math.round(this._lastOverlayMs * 100) / 100,
|
|
4728
4822
|
frameCount: this.frameCount
|
|
4729
4823
|
};
|
|
4730
4824
|
}
|
|
@@ -4747,19 +4841,14 @@ var RenderLoop = class {
|
|
|
4747
4841
|
layerManager;
|
|
4748
4842
|
domNodeManager;
|
|
4749
4843
|
layerCache;
|
|
4844
|
+
marginViewport;
|
|
4750
4845
|
activeDrawingLayerId = null;
|
|
4751
|
-
|
|
4752
|
-
|
|
4753
|
-
lastCamY;
|
|
4846
|
+
gridCacheDirty = true;
|
|
4847
|
+
// set on recenter/viewport-change; consumed by the grid block
|
|
4754
4848
|
stats = new RenderStats();
|
|
4755
|
-
|
|
4849
|
+
layerGroups = /* @__PURE__ */ new Map();
|
|
4756
4850
|
gridCacheCanvas = null;
|
|
4757
4851
|
gridCacheCtx = null;
|
|
4758
|
-
gridCacheZoom = -1;
|
|
4759
|
-
gridCacheCamX = -Infinity;
|
|
4760
|
-
gridCacheCamY = -Infinity;
|
|
4761
|
-
gridCacheWidth = 0;
|
|
4762
|
-
gridCacheHeight = 0;
|
|
4763
4852
|
lastGridRef = null;
|
|
4764
4853
|
constructor(deps) {
|
|
4765
4854
|
this.canvasEl = deps.canvasEl;
|
|
@@ -4771,9 +4860,7 @@ var RenderLoop = class {
|
|
|
4771
4860
|
this.layerManager = deps.layerManager;
|
|
4772
4861
|
this.domNodeManager = deps.domNodeManager;
|
|
4773
4862
|
this.layerCache = deps.layerCache;
|
|
4774
|
-
this.
|
|
4775
|
-
this.lastCamX = deps.camera.position.x;
|
|
4776
|
-
this.lastCamY = deps.camera.position.y;
|
|
4863
|
+
this.marginViewport = deps.marginViewport;
|
|
4777
4864
|
}
|
|
4778
4865
|
requestRender() {
|
|
4779
4866
|
this.needsRender = true;
|
|
@@ -4800,7 +4887,9 @@ var RenderLoop = class {
|
|
|
4800
4887
|
setCanvasSize(width, height) {
|
|
4801
4888
|
this.canvasEl.width = width;
|
|
4802
4889
|
this.canvasEl.height = height;
|
|
4803
|
-
|
|
4890
|
+
const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
|
|
4891
|
+
this.marginViewport.setViewport(width / dpr, height / dpr, dpr);
|
|
4892
|
+
this.layerCache.resize();
|
|
4804
4893
|
}
|
|
4805
4894
|
setActiveDrawingLayer(layerId) {
|
|
4806
4895
|
this.activeDrawingLayerId = layerId;
|
|
@@ -4814,30 +4903,29 @@ var RenderLoop = class {
|
|
|
4814
4903
|
getStats() {
|
|
4815
4904
|
return this.stats.getSnapshot();
|
|
4816
4905
|
}
|
|
4817
|
-
compositeLayerCache(ctx, layerId
|
|
4906
|
+
compositeLayerCache(ctx, layerId) {
|
|
4818
4907
|
const cached = this.layerCache.getCanvas(layerId);
|
|
4908
|
+
const offset = this.marginViewport.compositeOffset(
|
|
4909
|
+
this.camera.position.x,
|
|
4910
|
+
this.camera.position.y
|
|
4911
|
+
);
|
|
4819
4912
|
ctx.save();
|
|
4820
|
-
ctx.
|
|
4821
|
-
ctx.
|
|
4822
|
-
ctx.scale(1 / dpr, 1 / dpr);
|
|
4823
|
-
ctx.drawImage(cached, 0, 0);
|
|
4913
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
4914
|
+
ctx.drawImage(cached, offset.x, offset.y);
|
|
4824
4915
|
ctx.restore();
|
|
4825
4916
|
}
|
|
4826
|
-
ensureGridCache(
|
|
4827
|
-
|
|
4917
|
+
ensureGridCache() {
|
|
4918
|
+
const w = this.marginViewport.physicalWidth();
|
|
4919
|
+
const h = this.marginViewport.physicalHeight();
|
|
4920
|
+
if (this.gridCacheCanvas !== null && this.gridCacheCanvas.width === w && this.gridCacheCanvas.height === h) {
|
|
4828
4921
|
return;
|
|
4829
4922
|
}
|
|
4830
|
-
const physWidth = Math.round(cssWidth * dpr);
|
|
4831
|
-
const physHeight = Math.round(cssHeight * dpr);
|
|
4832
4923
|
if (typeof OffscreenCanvas !== "undefined") {
|
|
4833
|
-
this.gridCacheCanvas = new OffscreenCanvas(
|
|
4834
|
-
physWidth,
|
|
4835
|
-
physHeight
|
|
4836
|
-
);
|
|
4924
|
+
this.gridCacheCanvas = new OffscreenCanvas(w, h);
|
|
4837
4925
|
} else if (typeof document !== "undefined") {
|
|
4838
4926
|
const el = document.createElement("canvas");
|
|
4839
|
-
el.width =
|
|
4840
|
-
el.height =
|
|
4927
|
+
el.width = w;
|
|
4928
|
+
el.height = h;
|
|
4841
4929
|
this.gridCacheCanvas = el;
|
|
4842
4930
|
} else {
|
|
4843
4931
|
this.gridCacheCanvas = null;
|
|
@@ -4850,22 +4938,26 @@ var RenderLoop = class {
|
|
|
4850
4938
|
const t0 = performance.now();
|
|
4851
4939
|
const ctx = this.canvasEl.getContext("2d");
|
|
4852
4940
|
if (!ctx) return;
|
|
4941
|
+
let layersMs = 0;
|
|
4942
|
+
let compositeMs = 0;
|
|
4943
|
+
let gridMs = 0;
|
|
4853
4944
|
const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
|
|
4854
4945
|
const cssWidth = this.canvasEl.clientWidth;
|
|
4855
4946
|
const cssHeight = this.canvasEl.clientHeight;
|
|
4947
|
+
this.marginViewport.setViewport(cssWidth, cssHeight, dpr);
|
|
4856
4948
|
const currentZoom = this.camera.zoom;
|
|
4857
4949
|
const currentCamX = this.camera.position.x;
|
|
4858
4950
|
const currentCamY = this.camera.position.y;
|
|
4859
|
-
if (
|
|
4951
|
+
if (this.marginViewport.needsRecenter(currentCamX, currentCamY, currentZoom)) {
|
|
4952
|
+
this.marginViewport.recenter(currentCamX, currentCamY, currentZoom);
|
|
4860
4953
|
this.layerCache.markAllDirty();
|
|
4861
|
-
this.
|
|
4862
|
-
this.lastCamX = currentCamX;
|
|
4863
|
-
this.lastCamY = currentCamY;
|
|
4954
|
+
this.gridCacheDirty = true;
|
|
4864
4955
|
}
|
|
4865
4956
|
ctx.save();
|
|
4866
4957
|
ctx.scale(dpr, dpr);
|
|
4867
4958
|
this.renderer.setCanvasSize(cssWidth, cssHeight);
|
|
4868
4959
|
const hasGridElement = this.store.getElementsByType("grid").length > 0;
|
|
4960
|
+
const bgT0 = performance.now();
|
|
4869
4961
|
if (hasGridElement) {
|
|
4870
4962
|
ctx.save();
|
|
4871
4963
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
@@ -4874,19 +4966,20 @@ var RenderLoop = class {
|
|
|
4874
4966
|
} else {
|
|
4875
4967
|
this.background.render(ctx, this.camera);
|
|
4876
4968
|
}
|
|
4969
|
+
const backgroundMs = performance.now() - bgT0;
|
|
4877
4970
|
ctx.save();
|
|
4878
4971
|
ctx.translate(this.camera.position.x, this.camera.position.y);
|
|
4879
4972
|
ctx.scale(this.camera.zoom, this.camera.zoom);
|
|
4880
|
-
const
|
|
4881
|
-
const
|
|
4973
|
+
const cullBounds = this.marginViewport.cachedWorldBounds();
|
|
4974
|
+
const cullPad = Math.max(cullBounds.w, cullBounds.h) * 0.05;
|
|
4882
4975
|
const cullingRect = {
|
|
4883
|
-
x:
|
|
4884
|
-
y:
|
|
4885
|
-
w:
|
|
4886
|
-
h:
|
|
4976
|
+
x: cullBounds.x - cullPad,
|
|
4977
|
+
y: cullBounds.y - cullPad,
|
|
4978
|
+
w: cullBounds.w + cullPad * 2,
|
|
4979
|
+
h: cullBounds.h + cullPad * 2
|
|
4887
4980
|
};
|
|
4888
4981
|
const allElements = this.store.getAll();
|
|
4889
|
-
|
|
4982
|
+
this.layerGroups.clear();
|
|
4890
4983
|
const gridElements = [];
|
|
4891
4984
|
let domZIndex = 0;
|
|
4892
4985
|
for (const element of allElements) {
|
|
@@ -4909,31 +5002,34 @@ var RenderLoop = class {
|
|
|
4909
5002
|
gridElements.push(element);
|
|
4910
5003
|
continue;
|
|
4911
5004
|
}
|
|
4912
|
-
let group =
|
|
5005
|
+
let group = this.layerGroups.get(element.layerId);
|
|
4913
5006
|
if (!group) {
|
|
4914
5007
|
group = [];
|
|
4915
|
-
|
|
5008
|
+
this.layerGroups.set(element.layerId, group);
|
|
4916
5009
|
}
|
|
4917
5010
|
group.push(element);
|
|
4918
5011
|
}
|
|
4919
|
-
for (const [layerId, elements] of
|
|
5012
|
+
for (const [layerId, elements] of this.layerGroups) {
|
|
4920
5013
|
const isActiveDrawingLayer = layerId === this.activeDrawingLayerId;
|
|
4921
5014
|
if (!this.layerCache.isDirty(layerId)) {
|
|
4922
|
-
|
|
5015
|
+
const compT0 = performance.now();
|
|
5016
|
+
this.compositeLayerCache(ctx, layerId);
|
|
5017
|
+
compositeMs += performance.now() - compT0;
|
|
4923
5018
|
continue;
|
|
4924
5019
|
}
|
|
4925
5020
|
if (isActiveDrawingLayer) {
|
|
4926
|
-
|
|
5021
|
+
const compT0 = performance.now();
|
|
5022
|
+
this.compositeLayerCache(ctx, layerId);
|
|
5023
|
+
compositeMs += performance.now() - compT0;
|
|
4927
5024
|
continue;
|
|
4928
5025
|
}
|
|
4929
5026
|
const offCtx = this.layerCache.getContext(layerId);
|
|
4930
5027
|
if (offCtx) {
|
|
5028
|
+
const layerT0 = performance.now();
|
|
4931
5029
|
const offCanvas = this.layerCache.getCanvas(layerId);
|
|
4932
5030
|
offCtx.clearRect(0, 0, offCanvas.width, offCanvas.height);
|
|
4933
5031
|
offCtx.save();
|
|
4934
|
-
|
|
4935
|
-
offCtx.translate(this.camera.position.x, this.camera.position.y);
|
|
4936
|
-
offCtx.scale(this.camera.zoom, this.camera.zoom);
|
|
5032
|
+
this.marginViewport.applyRenderTransform(offCtx);
|
|
4937
5033
|
for (const element of elements) {
|
|
4938
5034
|
const elBounds = getElementBounds(element);
|
|
4939
5035
|
if (elBounds && !boundsIntersect(elBounds, cullingRect)) continue;
|
|
@@ -4941,56 +5037,73 @@ var RenderLoop = class {
|
|
|
4941
5037
|
}
|
|
4942
5038
|
offCtx.restore();
|
|
4943
5039
|
this.layerCache.markClean(layerId);
|
|
4944
|
-
|
|
5040
|
+
layersMs += performance.now() - layerT0;
|
|
5041
|
+
const compT0 = performance.now();
|
|
5042
|
+
this.compositeLayerCache(ctx, layerId);
|
|
5043
|
+
compositeMs += performance.now() - compT0;
|
|
4945
5044
|
}
|
|
4946
5045
|
}
|
|
4947
5046
|
if (gridElements.length > 0) {
|
|
4948
5047
|
const gridT0 = performance.now();
|
|
4949
5048
|
const gridRef = gridElements[0];
|
|
4950
|
-
const
|
|
4951
|
-
if (
|
|
4952
|
-
|
|
4953
|
-
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
4954
|
-
ctx.drawImage(this.gridCacheCanvas, 0, 0);
|
|
4955
|
-
ctx.restore();
|
|
4956
|
-
} else {
|
|
4957
|
-
this.ensureGridCache(cssWidth, cssHeight, dpr);
|
|
5049
|
+
const gridDirty = this.gridCacheDirty || gridRef !== this.lastGridRef;
|
|
5050
|
+
if (gridDirty) {
|
|
5051
|
+
this.ensureGridCache();
|
|
4958
5052
|
if (this.gridCacheCtx && this.gridCacheCanvas) {
|
|
5053
|
+
const cb = this.marginViewport.cachedWorldBounds();
|
|
5054
|
+
this.renderer.setGridBoundsOverride({
|
|
5055
|
+
minX: cb.x,
|
|
5056
|
+
minY: cb.y,
|
|
5057
|
+
maxX: cb.x + cb.w,
|
|
5058
|
+
maxY: cb.y + cb.h
|
|
5059
|
+
});
|
|
4959
5060
|
const gc = this.gridCacheCtx;
|
|
4960
5061
|
gc.clearRect(0, 0, this.gridCacheCanvas.width, this.gridCacheCanvas.height);
|
|
4961
5062
|
gc.save();
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
}
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
4971
|
-
ctx.drawImage(this.gridCacheCanvas, 0, 0);
|
|
4972
|
-
ctx.restore();
|
|
4973
|
-
} else {
|
|
4974
|
-
for (const grid of gridElements) {
|
|
4975
|
-
this.renderer.renderCanvasElement(ctx, grid);
|
|
5063
|
+
this.marginViewport.applyRenderTransform(gc);
|
|
5064
|
+
try {
|
|
5065
|
+
for (const grid of gridElements) {
|
|
5066
|
+
this.renderer.renderCanvasElement(gc, grid);
|
|
5067
|
+
}
|
|
5068
|
+
} finally {
|
|
5069
|
+
gc.restore();
|
|
5070
|
+
this.renderer.setGridBoundsOverride(null);
|
|
4976
5071
|
}
|
|
4977
5072
|
}
|
|
4978
|
-
this.
|
|
4979
|
-
this.gridCacheCamX = currentCamX;
|
|
4980
|
-
this.gridCacheCamY = currentCamY;
|
|
4981
|
-
this.gridCacheWidth = cssWidth;
|
|
4982
|
-
this.gridCacheHeight = cssHeight;
|
|
5073
|
+
this.gridCacheDirty = false;
|
|
4983
5074
|
this.lastGridRef = gridRef;
|
|
4984
5075
|
}
|
|
4985
|
-
this.
|
|
5076
|
+
if (this.gridCacheCanvas) {
|
|
5077
|
+
const offset = this.marginViewport.compositeOffset(
|
|
5078
|
+
this.camera.position.x,
|
|
5079
|
+
this.camera.position.y
|
|
5080
|
+
);
|
|
5081
|
+
ctx.save();
|
|
5082
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
5083
|
+
ctx.drawImage(this.gridCacheCanvas, offset.x, offset.y);
|
|
5084
|
+
ctx.restore();
|
|
5085
|
+
} else {
|
|
5086
|
+
for (const grid of gridElements) {
|
|
5087
|
+
this.renderer.renderCanvasElement(ctx, grid);
|
|
5088
|
+
}
|
|
5089
|
+
}
|
|
5090
|
+
gridMs = performance.now() - gridT0;
|
|
4986
5091
|
}
|
|
5092
|
+
const overlayT0 = performance.now();
|
|
4987
5093
|
const activeTool = this.toolManager.activeTool;
|
|
4988
5094
|
if (activeTool?.renderOverlay) {
|
|
4989
5095
|
activeTool.renderOverlay(ctx);
|
|
4990
5096
|
}
|
|
5097
|
+
const overlayMs = performance.now() - overlayT0;
|
|
4991
5098
|
ctx.restore();
|
|
4992
5099
|
ctx.restore();
|
|
4993
|
-
this.stats.recordFrame(performance.now() - t0,
|
|
5100
|
+
this.stats.recordFrame(performance.now() - t0, {
|
|
5101
|
+
gridMs,
|
|
5102
|
+
layersMs,
|
|
5103
|
+
backgroundMs,
|
|
5104
|
+
compositeMs,
|
|
5105
|
+
overlayMs
|
|
5106
|
+
});
|
|
4994
5107
|
}
|
|
4995
5108
|
};
|
|
4996
5109
|
|
|
@@ -5005,15 +5118,11 @@ function createOffscreenCanvas(width, height) {
|
|
|
5005
5118
|
return canvas;
|
|
5006
5119
|
}
|
|
5007
5120
|
var LayerCache = class {
|
|
5121
|
+
constructor(viewport) {
|
|
5122
|
+
this.viewport = viewport;
|
|
5123
|
+
}
|
|
5008
5124
|
canvases = /* @__PURE__ */ new Map();
|
|
5009
5125
|
dirtyFlags = /* @__PURE__ */ new Map();
|
|
5010
|
-
width;
|
|
5011
|
-
height;
|
|
5012
|
-
constructor(width, height) {
|
|
5013
|
-
const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
|
|
5014
|
-
this.width = Math.round(width * dpr);
|
|
5015
|
-
this.height = Math.round(height * dpr);
|
|
5016
|
-
}
|
|
5017
5126
|
isDirty(layerId) {
|
|
5018
5127
|
return this.dirtyFlags.get(layerId) !== false;
|
|
5019
5128
|
}
|
|
@@ -5031,7 +5140,7 @@ var LayerCache = class {
|
|
|
5031
5140
|
getCanvas(layerId) {
|
|
5032
5141
|
let canvas = this.canvases.get(layerId);
|
|
5033
5142
|
if (!canvas) {
|
|
5034
|
-
canvas = createOffscreenCanvas(this.
|
|
5143
|
+
canvas = createOffscreenCanvas(this.viewport.physicalWidth(), this.viewport.physicalHeight());
|
|
5035
5144
|
this.canvases.set(layerId, canvas);
|
|
5036
5145
|
this.dirtyFlags.set(layerId, true);
|
|
5037
5146
|
}
|
|
@@ -5041,13 +5150,12 @@ var LayerCache = class {
|
|
|
5041
5150
|
const canvas = this.getCanvas(layerId);
|
|
5042
5151
|
return canvas.getContext("2d");
|
|
5043
5152
|
}
|
|
5044
|
-
resize(
|
|
5045
|
-
const
|
|
5046
|
-
|
|
5047
|
-
this.height = Math.round(height * dpr);
|
|
5153
|
+
resize() {
|
|
5154
|
+
const w = this.viewport.physicalWidth();
|
|
5155
|
+
const h = this.viewport.physicalHeight();
|
|
5048
5156
|
for (const [id, canvas] of this.canvases) {
|
|
5049
|
-
canvas.width =
|
|
5050
|
-
canvas.height =
|
|
5157
|
+
canvas.width = w;
|
|
5158
|
+
canvas.height = h;
|
|
5051
5159
|
this.dirtyFlags.set(id, true);
|
|
5052
5160
|
}
|
|
5053
5161
|
}
|
|
@@ -5057,6 +5165,75 @@ var LayerCache = class {
|
|
|
5057
5165
|
}
|
|
5058
5166
|
};
|
|
5059
5167
|
|
|
5168
|
+
// src/canvas/margin-viewport.ts
|
|
5169
|
+
var MarginViewport = class {
|
|
5170
|
+
constructor(marginPx) {
|
|
5171
|
+
this.marginPx = marginPx;
|
|
5172
|
+
}
|
|
5173
|
+
cssW = 0;
|
|
5174
|
+
cssH = 0;
|
|
5175
|
+
dpr = 1;
|
|
5176
|
+
anchorCamX = 0;
|
|
5177
|
+
anchorCamY = 0;
|
|
5178
|
+
anchorZoom = Number.NaN;
|
|
5179
|
+
// sentinel → first needsRecenter is true
|
|
5180
|
+
viewportDirty = true;
|
|
5181
|
+
setMargin(marginPx) {
|
|
5182
|
+
if (marginPx !== this.marginPx) {
|
|
5183
|
+
this.marginPx = marginPx;
|
|
5184
|
+
this.viewportDirty = true;
|
|
5185
|
+
}
|
|
5186
|
+
}
|
|
5187
|
+
setViewport(cssW, cssH, dpr) {
|
|
5188
|
+
if (cssW !== this.cssW || cssH !== this.cssH || dpr !== this.dpr) {
|
|
5189
|
+
this.cssW = cssW;
|
|
5190
|
+
this.cssH = cssH;
|
|
5191
|
+
this.dpr = dpr;
|
|
5192
|
+
this.viewportDirty = true;
|
|
5193
|
+
}
|
|
5194
|
+
}
|
|
5195
|
+
physicalWidth() {
|
|
5196
|
+
return Math.round((this.cssW + 2 * this.marginPx) * this.dpr);
|
|
5197
|
+
}
|
|
5198
|
+
physicalHeight() {
|
|
5199
|
+
return Math.round((this.cssH + 2 * this.marginPx) * this.dpr);
|
|
5200
|
+
}
|
|
5201
|
+
needsRecenter(camX, camY, zoom) {
|
|
5202
|
+
return this.viewportDirty || zoom !== this.anchorZoom || Math.abs(camX - this.anchorCamX) > this.marginPx || Math.abs(camY - this.anchorCamY) > this.marginPx;
|
|
5203
|
+
}
|
|
5204
|
+
recenter(camX, camY, zoom) {
|
|
5205
|
+
this.anchorCamX = camX;
|
|
5206
|
+
this.anchorCamY = camY;
|
|
5207
|
+
this.anchorZoom = zoom;
|
|
5208
|
+
this.viewportDirty = false;
|
|
5209
|
+
}
|
|
5210
|
+
/** Applies dpr scale + anchor-relative world transform. setViewport must have been called first. */
|
|
5211
|
+
applyRenderTransform(ctx) {
|
|
5212
|
+
ctx.scale(this.dpr, this.dpr);
|
|
5213
|
+
ctx.translate(this.marginPx + this.anchorCamX, this.marginPx + this.anchorCamY);
|
|
5214
|
+
ctx.scale(this.anchorZoom, this.anchorZoom);
|
|
5215
|
+
}
|
|
5216
|
+
// Device-px destination for drawImage(cache, x, y).
|
|
5217
|
+
// A world point P sits in the cache at CSS x `margin + anchorCamX + P*zoom`; it must land on
|
|
5218
|
+
// screen at `camX + P*zoom`; so the blit offset is `camX - anchorCamX - margin` (CSS) * dpr.
|
|
5219
|
+
compositeOffset(camX, camY) {
|
|
5220
|
+
return {
|
|
5221
|
+
x: (camX - this.anchorCamX - this.marginPx) * this.dpr,
|
|
5222
|
+
y: (camY - this.anchorCamY - this.marginPx) * this.dpr
|
|
5223
|
+
};
|
|
5224
|
+
}
|
|
5225
|
+
// World-space bounds of the whole cached region at the anchor (cull rect for re-renders).
|
|
5226
|
+
cachedWorldBounds() {
|
|
5227
|
+
const z = this.anchorZoom;
|
|
5228
|
+
return {
|
|
5229
|
+
x: (-this.marginPx - this.anchorCamX) / z,
|
|
5230
|
+
y: (-this.marginPx - this.anchorCamY) / z,
|
|
5231
|
+
w: (this.cssW + 2 * this.marginPx) / z,
|
|
5232
|
+
h: (this.cssH + 2 * this.marginPx) / z
|
|
5233
|
+
};
|
|
5234
|
+
}
|
|
5235
|
+
};
|
|
5236
|
+
|
|
5060
5237
|
// src/canvas/viewport.ts
|
|
5061
5238
|
var Viewport = class {
|
|
5062
5239
|
constructor(container, options = {}) {
|
|
@@ -5133,10 +5310,13 @@ var Viewport = class {
|
|
|
5133
5310
|
this.interactMode = new InteractMode({
|
|
5134
5311
|
getNode: (id) => this.domNodeManager.getNode(id)
|
|
5135
5312
|
});
|
|
5136
|
-
|
|
5313
|
+
this.marginViewport = new MarginViewport(options.panBufferMargin ?? 256);
|
|
5314
|
+
this.marginViewport.setViewport(
|
|
5137
5315
|
this.canvasEl.clientWidth || 800,
|
|
5138
|
-
this.canvasEl.clientHeight || 600
|
|
5316
|
+
this.canvasEl.clientHeight || 600,
|
|
5317
|
+
typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1
|
|
5139
5318
|
);
|
|
5319
|
+
const layerCache = new LayerCache(this.marginViewport);
|
|
5140
5320
|
this.renderLoop = new RenderLoop({
|
|
5141
5321
|
canvasEl: this.canvasEl,
|
|
5142
5322
|
camera: this.camera,
|
|
@@ -5146,7 +5326,8 @@ var Viewport = class {
|
|
|
5146
5326
|
toolManager: this.toolManager,
|
|
5147
5327
|
layerManager: this.layerManager,
|
|
5148
5328
|
domNodeManager: this.domNodeManager,
|
|
5149
|
-
layerCache
|
|
5329
|
+
layerCache,
|
|
5330
|
+
marginViewport: this.marginViewport
|
|
5150
5331
|
});
|
|
5151
5332
|
this.unsubCamera = this.camera.onChange(() => {
|
|
5152
5333
|
this.applyCameraTransform();
|
|
@@ -5210,6 +5391,7 @@ var Viewport = class {
|
|
|
5210
5391
|
noteEditor;
|
|
5211
5392
|
historyRecorder;
|
|
5212
5393
|
toolContext;
|
|
5394
|
+
marginViewport;
|
|
5213
5395
|
resizeObserver = null;
|
|
5214
5396
|
_snapToGrid = false;
|
|
5215
5397
|
_gridSize;
|
|
@@ -5402,7 +5584,7 @@ var Viewport = class {
|
|
|
5402
5584
|
const id = setInterval(() => {
|
|
5403
5585
|
const s = this.getRenderStats();
|
|
5404
5586
|
console.log(
|
|
5405
|
-
`[FieldNotes] fps=${s.fps} frame=${s.avgFrameMs}ms p95=${s.p95FrameMs}ms grid=${s.lastGridMs}ms`
|
|
5587
|
+
`[FieldNotes] fps=${s.fps} frame=${s.avgFrameMs}ms p95=${s.p95FrameMs}ms grid=${s.lastGridMs}ms layers=${s.layersMs}ms comp=${s.compositeMs}ms bg=${s.backgroundMs}ms overlay=${s.overlayMs}ms`
|
|
5406
5588
|
);
|
|
5407
5589
|
}, intervalMs);
|
|
5408
5590
|
return () => clearInterval(id);
|
|
@@ -7400,7 +7582,7 @@ var TemplateTool = class {
|
|
|
7400
7582
|
};
|
|
7401
7583
|
|
|
7402
7584
|
// src/index.ts
|
|
7403
|
-
var VERSION = "0.
|
|
7585
|
+
var VERSION = "0.23.0";
|
|
7404
7586
|
// Annotate the CommonJS export names for ESM import in node:
|
|
7405
7587
|
0 && (module.exports = {
|
|
7406
7588
|
AddElementCommand,
|