@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.js
CHANGED
|
@@ -1823,10 +1823,134 @@ function getTemplateBounds(el) {
|
|
|
1823
1823
|
}
|
|
1824
1824
|
}
|
|
1825
1825
|
}
|
|
1826
|
+
function transferStrokeBounds(prev, next) {
|
|
1827
|
+
if (prev.type !== "stroke" || next.type !== "stroke") return;
|
|
1828
|
+
if (prev.points !== next.points) return;
|
|
1829
|
+
if (prev.position.x !== next.position.x || prev.position.y !== next.position.y) return;
|
|
1830
|
+
const bounds = strokeBoundsCache.get(prev);
|
|
1831
|
+
if (bounds) strokeBoundsCache.set(next, bounds);
|
|
1832
|
+
}
|
|
1826
1833
|
function boundsIntersect(a, b) {
|
|
1827
1834
|
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;
|
|
1828
1835
|
}
|
|
1829
1836
|
|
|
1837
|
+
// src/elements/stroke-smoothing.ts
|
|
1838
|
+
var MIN_PRESSURE_SCALE = 0.2;
|
|
1839
|
+
function pressureToWidth(pressure, baseWidth) {
|
|
1840
|
+
return baseWidth * (MIN_PRESSURE_SCALE + (1 - MIN_PRESSURE_SCALE) * pressure);
|
|
1841
|
+
}
|
|
1842
|
+
function simplifyPoints(points, tolerance) {
|
|
1843
|
+
if (points.length <= 2) return points.slice();
|
|
1844
|
+
return rdp(points, 0, points.length - 1, tolerance);
|
|
1845
|
+
}
|
|
1846
|
+
function rdp(points, start, end, tolerance) {
|
|
1847
|
+
const first = points[start];
|
|
1848
|
+
const last = points[end];
|
|
1849
|
+
if (!first || !last) return [];
|
|
1850
|
+
if (end - start <= 1) return [first, last];
|
|
1851
|
+
let maxDist = 0;
|
|
1852
|
+
let maxIndex = start;
|
|
1853
|
+
for (let i = start + 1; i < end; i++) {
|
|
1854
|
+
const pt = points[i];
|
|
1855
|
+
if (!pt) continue;
|
|
1856
|
+
const dist = perpendicularDistance(pt, first, last);
|
|
1857
|
+
if (dist > maxDist) {
|
|
1858
|
+
maxDist = dist;
|
|
1859
|
+
maxIndex = i;
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
if (maxDist <= tolerance) return [first, last];
|
|
1863
|
+
const left = rdp(points, start, maxIndex, tolerance);
|
|
1864
|
+
const right = rdp(points, maxIndex, end, tolerance);
|
|
1865
|
+
return left.concat(right.slice(1));
|
|
1866
|
+
}
|
|
1867
|
+
function perpendicularDistance(pt, lineStart, lineEnd) {
|
|
1868
|
+
const dx = lineEnd.x - lineStart.x;
|
|
1869
|
+
const dy = lineEnd.y - lineStart.y;
|
|
1870
|
+
const lenSq = dx * dx + dy * dy;
|
|
1871
|
+
if (lenSq === 0) {
|
|
1872
|
+
const ex = pt.x - lineStart.x;
|
|
1873
|
+
const ey = pt.y - lineStart.y;
|
|
1874
|
+
return Math.sqrt(ex * ex + ey * ey);
|
|
1875
|
+
}
|
|
1876
|
+
const num = Math.abs(dy * pt.x - dx * pt.y + lineEnd.x * lineStart.y - lineEnd.y * lineStart.x);
|
|
1877
|
+
return num / Math.sqrt(lenSq);
|
|
1878
|
+
}
|
|
1879
|
+
function smoothToSegments(points) {
|
|
1880
|
+
if (points.length < 2) return [];
|
|
1881
|
+
if (points.length === 2) {
|
|
1882
|
+
const p0 = points[0];
|
|
1883
|
+
const p1 = points[1];
|
|
1884
|
+
if (!p0 || !p1) return [];
|
|
1885
|
+
const mx = (p0.x + p1.x) / 2;
|
|
1886
|
+
const my = (p0.y + p1.y) / 2;
|
|
1887
|
+
return [{ start: p0, cp1: { x: mx, y: my }, cp2: { x: mx, y: my }, end: p1 }];
|
|
1888
|
+
}
|
|
1889
|
+
const segments = [];
|
|
1890
|
+
const n = points.length;
|
|
1891
|
+
for (let i = 0; i < n - 1; i++) {
|
|
1892
|
+
const p0 = points[Math.max(0, i - 1)];
|
|
1893
|
+
const p1 = points[i];
|
|
1894
|
+
const p2 = points[i + 1];
|
|
1895
|
+
const p3 = points[Math.min(n - 1, i + 2)];
|
|
1896
|
+
if (!p0 || !p1 || !p2 || !p3) continue;
|
|
1897
|
+
const cp1 = {
|
|
1898
|
+
x: p1.x + (p2.x - p0.x) / 6,
|
|
1899
|
+
y: p1.y + (p2.y - p0.y) / 6
|
|
1900
|
+
};
|
|
1901
|
+
const cp2 = {
|
|
1902
|
+
x: p2.x - (p3.x - p1.x) / 6,
|
|
1903
|
+
y: p2.y - (p3.y - p1.y) / 6
|
|
1904
|
+
};
|
|
1905
|
+
segments.push({ start: p1, cp1, cp2, end: p2 });
|
|
1906
|
+
}
|
|
1907
|
+
return segments;
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
// src/elements/stroke-cache.ts
|
|
1911
|
+
var cache = /* @__PURE__ */ new WeakMap();
|
|
1912
|
+
var WIDTH_QUANTUM = 0.25;
|
|
1913
|
+
function buildWidthBuckets(segments, widths) {
|
|
1914
|
+
if (typeof Path2D === "undefined") return null;
|
|
1915
|
+
const byWidth = /* @__PURE__ */ new Map();
|
|
1916
|
+
for (let i = 0; i < segments.length; i++) {
|
|
1917
|
+
const seg = segments[i];
|
|
1918
|
+
const w = widths[i];
|
|
1919
|
+
if (!seg || w === void 0) continue;
|
|
1920
|
+
const q = Math.max(WIDTH_QUANTUM, Math.round(w / WIDTH_QUANTUM) * WIDTH_QUANTUM);
|
|
1921
|
+
let path = byWidth.get(q);
|
|
1922
|
+
if (!path) {
|
|
1923
|
+
path = new Path2D();
|
|
1924
|
+
byWidth.set(q, path);
|
|
1925
|
+
}
|
|
1926
|
+
path.moveTo(seg.start.x, seg.start.y);
|
|
1927
|
+
path.bezierCurveTo(seg.cp1.x, seg.cp1.y, seg.cp2.x, seg.cp2.y, seg.end.x, seg.end.y);
|
|
1928
|
+
}
|
|
1929
|
+
return [...byWidth.entries()].map(([width, path]) => ({ width, path }));
|
|
1930
|
+
}
|
|
1931
|
+
function computeStrokeSegments(stroke) {
|
|
1932
|
+
const segments = smoothToSegments(stroke.points);
|
|
1933
|
+
const widths = [];
|
|
1934
|
+
for (const seg of segments) {
|
|
1935
|
+
const w = (pressureToWidth(seg.start.pressure, stroke.width) + pressureToWidth(seg.end.pressure, stroke.width)) / 2;
|
|
1936
|
+
widths.push(w);
|
|
1937
|
+
}
|
|
1938
|
+
const data = { segments, widths, buckets: buildWidthBuckets(segments, widths) };
|
|
1939
|
+
cache.set(stroke, data);
|
|
1940
|
+
return data;
|
|
1941
|
+
}
|
|
1942
|
+
function getStrokeRenderData(stroke) {
|
|
1943
|
+
const cached = cache.get(stroke);
|
|
1944
|
+
if (cached) return cached;
|
|
1945
|
+
return computeStrokeSegments(stroke);
|
|
1946
|
+
}
|
|
1947
|
+
function transferStrokeRenderData(prev, next) {
|
|
1948
|
+
if (prev.type !== "stroke" || next.type !== "stroke") return;
|
|
1949
|
+
if (prev.points !== next.points) return;
|
|
1950
|
+
const data = cache.get(prev);
|
|
1951
|
+
if (data) cache.set(next, data);
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1830
1954
|
// src/elements/element-store.ts
|
|
1831
1955
|
var ElementStore = class {
|
|
1832
1956
|
elements = /* @__PURE__ */ new Map();
|
|
@@ -1877,6 +2001,10 @@ var ElementStore = class {
|
|
|
1877
2001
|
this.sortedCache = null;
|
|
1878
2002
|
this._versions.set(id, (this._versions.get(id) ?? 0) + 1);
|
|
1879
2003
|
const updated = { ...existing, ...partial, id: existing.id, type: existing.type };
|
|
2004
|
+
if (updated.type === "stroke" && existing.type === "stroke") {
|
|
2005
|
+
transferStrokeRenderData(existing, updated);
|
|
2006
|
+
transferStrokeBounds(existing, updated);
|
|
2007
|
+
}
|
|
1880
2008
|
if (updated.type === "arrow") {
|
|
1881
2009
|
const arrow = updated;
|
|
1882
2010
|
arrow.cachedControlPoint = getArrowControlPoint(arrow.from, arrow.to, arrow.bend);
|
|
@@ -1920,6 +2048,12 @@ var ElementStore = class {
|
|
|
1920
2048
|
this._versions.set(el.id, 0);
|
|
1921
2049
|
const bounds = getElementBounds(el);
|
|
1922
2050
|
if (bounds) this.spatialIndex.insert(el.id, bounds);
|
|
2051
|
+
if (el.type === "stroke") {
|
|
2052
|
+
computeStrokeSegments(el);
|
|
2053
|
+
}
|
|
2054
|
+
if (el.type === "arrow" && el.bend !== 0 && !el.cachedControlPoint) {
|
|
2055
|
+
el.cachedControlPoint = getArrowControlPoint(el.from, el.to, el.bend);
|
|
2056
|
+
}
|
|
1923
2057
|
}
|
|
1924
2058
|
this.bus.emit("clear", null);
|
|
1925
2059
|
for (const el of elements) {
|
|
@@ -2005,6 +2139,20 @@ var ElementStore = class {
|
|
|
2005
2139
|
}
|
|
2006
2140
|
};
|
|
2007
2141
|
|
|
2142
|
+
// src/elements/arrow-render-cache.ts
|
|
2143
|
+
var cache2 = /* @__PURE__ */ new WeakMap();
|
|
2144
|
+
function getArrowRenderGeometry(arrow) {
|
|
2145
|
+
const hit = cache2.get(arrow);
|
|
2146
|
+
if (hit) return hit;
|
|
2147
|
+
const geometry = {
|
|
2148
|
+
controlPoint: arrow.bend !== 0 ? arrow.cachedControlPoint ?? getArrowControlPoint(arrow.from, arrow.to, arrow.bend) : null,
|
|
2149
|
+
tangentStart: getArrowTangentAngle(arrow.from, arrow.to, arrow.bend, 0),
|
|
2150
|
+
tangentEnd: getArrowTangentAngle(arrow.from, arrow.to, arrow.bend, 1)
|
|
2151
|
+
};
|
|
2152
|
+
cache2.set(arrow, geometry);
|
|
2153
|
+
return geometry;
|
|
2154
|
+
}
|
|
2155
|
+
|
|
2008
2156
|
// src/elements/arrow-binding.ts
|
|
2009
2157
|
var BINDABLE_TYPES = /* @__PURE__ */ new Set(["note", "text", "image", "html", "shape"]);
|
|
2010
2158
|
function isBindable(element) {
|
|
@@ -2125,98 +2273,6 @@ function unbindArrow(arrow, store) {
|
|
|
2125
2273
|
return updates;
|
|
2126
2274
|
}
|
|
2127
2275
|
|
|
2128
|
-
// src/elements/stroke-smoothing.ts
|
|
2129
|
-
var MIN_PRESSURE_SCALE = 0.2;
|
|
2130
|
-
function pressureToWidth(pressure, baseWidth) {
|
|
2131
|
-
return baseWidth * (MIN_PRESSURE_SCALE + (1 - MIN_PRESSURE_SCALE) * pressure);
|
|
2132
|
-
}
|
|
2133
|
-
function simplifyPoints(points, tolerance) {
|
|
2134
|
-
if (points.length <= 2) return points.slice();
|
|
2135
|
-
return rdp(points, 0, points.length - 1, tolerance);
|
|
2136
|
-
}
|
|
2137
|
-
function rdp(points, start, end, tolerance) {
|
|
2138
|
-
const first = points[start];
|
|
2139
|
-
const last = points[end];
|
|
2140
|
-
if (!first || !last) return [];
|
|
2141
|
-
if (end - start <= 1) return [first, last];
|
|
2142
|
-
let maxDist = 0;
|
|
2143
|
-
let maxIndex = start;
|
|
2144
|
-
for (let i = start + 1; i < end; i++) {
|
|
2145
|
-
const pt = points[i];
|
|
2146
|
-
if (!pt) continue;
|
|
2147
|
-
const dist = perpendicularDistance(pt, first, last);
|
|
2148
|
-
if (dist > maxDist) {
|
|
2149
|
-
maxDist = dist;
|
|
2150
|
-
maxIndex = i;
|
|
2151
|
-
}
|
|
2152
|
-
}
|
|
2153
|
-
if (maxDist <= tolerance) return [first, last];
|
|
2154
|
-
const left = rdp(points, start, maxIndex, tolerance);
|
|
2155
|
-
const right = rdp(points, maxIndex, end, tolerance);
|
|
2156
|
-
return left.concat(right.slice(1));
|
|
2157
|
-
}
|
|
2158
|
-
function perpendicularDistance(pt, lineStart, lineEnd) {
|
|
2159
|
-
const dx = lineEnd.x - lineStart.x;
|
|
2160
|
-
const dy = lineEnd.y - lineStart.y;
|
|
2161
|
-
const lenSq = dx * dx + dy * dy;
|
|
2162
|
-
if (lenSq === 0) {
|
|
2163
|
-
const ex = pt.x - lineStart.x;
|
|
2164
|
-
const ey = pt.y - lineStart.y;
|
|
2165
|
-
return Math.sqrt(ex * ex + ey * ey);
|
|
2166
|
-
}
|
|
2167
|
-
const num = Math.abs(dy * pt.x - dx * pt.y + lineEnd.x * lineStart.y - lineEnd.y * lineStart.x);
|
|
2168
|
-
return num / Math.sqrt(lenSq);
|
|
2169
|
-
}
|
|
2170
|
-
function smoothToSegments(points) {
|
|
2171
|
-
if (points.length < 2) return [];
|
|
2172
|
-
if (points.length === 2) {
|
|
2173
|
-
const p0 = points[0];
|
|
2174
|
-
const p1 = points[1];
|
|
2175
|
-
if (!p0 || !p1) return [];
|
|
2176
|
-
const mx = (p0.x + p1.x) / 2;
|
|
2177
|
-
const my = (p0.y + p1.y) / 2;
|
|
2178
|
-
return [{ start: p0, cp1: { x: mx, y: my }, cp2: { x: mx, y: my }, end: p1 }];
|
|
2179
|
-
}
|
|
2180
|
-
const segments = [];
|
|
2181
|
-
const n = points.length;
|
|
2182
|
-
for (let i = 0; i < n - 1; i++) {
|
|
2183
|
-
const p0 = points[Math.max(0, i - 1)];
|
|
2184
|
-
const p1 = points[i];
|
|
2185
|
-
const p2 = points[i + 1];
|
|
2186
|
-
const p3 = points[Math.min(n - 1, i + 2)];
|
|
2187
|
-
if (!p0 || !p1 || !p2 || !p3) continue;
|
|
2188
|
-
const cp1 = {
|
|
2189
|
-
x: p1.x + (p2.x - p0.x) / 6,
|
|
2190
|
-
y: p1.y + (p2.y - p0.y) / 6
|
|
2191
|
-
};
|
|
2192
|
-
const cp2 = {
|
|
2193
|
-
x: p2.x - (p3.x - p1.x) / 6,
|
|
2194
|
-
y: p2.y - (p3.y - p1.y) / 6
|
|
2195
|
-
};
|
|
2196
|
-
segments.push({ start: p1, cp1, cp2, end: p2 });
|
|
2197
|
-
}
|
|
2198
|
-
return segments;
|
|
2199
|
-
}
|
|
2200
|
-
|
|
2201
|
-
// src/elements/stroke-cache.ts
|
|
2202
|
-
var cache = /* @__PURE__ */ new WeakMap();
|
|
2203
|
-
function computeStrokeSegments(stroke) {
|
|
2204
|
-
const segments = smoothToSegments(stroke.points);
|
|
2205
|
-
const widths = [];
|
|
2206
|
-
for (const seg of segments) {
|
|
2207
|
-
const w = (pressureToWidth(seg.start.pressure, stroke.width) + pressureToWidth(seg.end.pressure, stroke.width)) / 2;
|
|
2208
|
-
widths.push(w);
|
|
2209
|
-
}
|
|
2210
|
-
const data = { segments, widths };
|
|
2211
|
-
cache.set(stroke, data);
|
|
2212
|
-
return data;
|
|
2213
|
-
}
|
|
2214
|
-
function getStrokeRenderData(stroke) {
|
|
2215
|
-
const cached = cache.get(stroke);
|
|
2216
|
-
if (cached) return cached;
|
|
2217
|
-
return computeStrokeSegments(stroke);
|
|
2218
|
-
}
|
|
2219
|
-
|
|
2220
2276
|
// src/elements/grid-renderer.ts
|
|
2221
2277
|
function getSquareGridLines(bounds, cellSize) {
|
|
2222
2278
|
if (cellSize <= 0) return { verticals: [], horizontals: [] };
|
|
@@ -2614,6 +2670,7 @@ var ElementRenderer = class {
|
|
|
2614
2670
|
canvasSize = null;
|
|
2615
2671
|
hexTileCache = null;
|
|
2616
2672
|
hexTileCacheKey = "";
|
|
2673
|
+
gridBoundsOverride = null;
|
|
2617
2674
|
setStore(store) {
|
|
2618
2675
|
this.store = store;
|
|
2619
2676
|
}
|
|
@@ -2629,6 +2686,9 @@ var ElementRenderer = class {
|
|
|
2629
2686
|
setCanvasSize(w, h) {
|
|
2630
2687
|
this.canvasSize = { w, h };
|
|
2631
2688
|
}
|
|
2689
|
+
setGridBoundsOverride(bounds) {
|
|
2690
|
+
this.gridBoundsOverride = bounds;
|
|
2691
|
+
}
|
|
2632
2692
|
isDomElement(element) {
|
|
2633
2693
|
return DOM_ELEMENT_TYPES.has(element.type);
|
|
2634
2694
|
}
|
|
@@ -2662,21 +2722,29 @@ var ElementRenderer = class {
|
|
|
2662
2722
|
ctx.lineCap = "round";
|
|
2663
2723
|
ctx.lineJoin = "round";
|
|
2664
2724
|
ctx.globalAlpha = stroke.opacity;
|
|
2665
|
-
const
|
|
2666
|
-
|
|
2667
|
-
const
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2725
|
+
const data = getStrokeRenderData(stroke);
|
|
2726
|
+
if (data.buckets) {
|
|
2727
|
+
for (const bucket of data.buckets) {
|
|
2728
|
+
ctx.lineWidth = bucket.width;
|
|
2729
|
+
ctx.stroke(bucket.path);
|
|
2730
|
+
}
|
|
2731
|
+
} else {
|
|
2732
|
+
for (let i = 0; i < data.segments.length; i++) {
|
|
2733
|
+
const seg = data.segments[i];
|
|
2734
|
+
const w = data.widths[i];
|
|
2735
|
+
if (!seg || w === void 0) continue;
|
|
2736
|
+
ctx.lineWidth = w;
|
|
2737
|
+
ctx.beginPath();
|
|
2738
|
+
ctx.moveTo(seg.start.x, seg.start.y);
|
|
2739
|
+
ctx.bezierCurveTo(seg.cp1.x, seg.cp1.y, seg.cp2.x, seg.cp2.y, seg.end.x, seg.end.y);
|
|
2740
|
+
ctx.stroke();
|
|
2741
|
+
}
|
|
2675
2742
|
}
|
|
2676
2743
|
ctx.restore();
|
|
2677
2744
|
}
|
|
2678
2745
|
renderArrow(ctx, arrow) {
|
|
2679
|
-
const
|
|
2746
|
+
const geometry = getArrowRenderGeometry(arrow);
|
|
2747
|
+
const { visualFrom, visualTo } = this.getVisualEndpoints(arrow, geometry);
|
|
2680
2748
|
ctx.save();
|
|
2681
2749
|
ctx.strokeStyle = arrow.color;
|
|
2682
2750
|
ctx.lineWidth = arrow.width;
|
|
@@ -2687,17 +2755,18 @@ var ElementRenderer = class {
|
|
|
2687
2755
|
ctx.beginPath();
|
|
2688
2756
|
ctx.moveTo(visualFrom.x, visualFrom.y);
|
|
2689
2757
|
if (arrow.bend !== 0) {
|
|
2690
|
-
const cp =
|
|
2691
|
-
|
|
2758
|
+
const cp = geometry.controlPoint;
|
|
2759
|
+
if (cp) {
|
|
2760
|
+
ctx.quadraticCurveTo(cp.x, cp.y, visualTo.x, visualTo.y);
|
|
2761
|
+
}
|
|
2692
2762
|
} else {
|
|
2693
2763
|
ctx.lineTo(visualTo.x, visualTo.y);
|
|
2694
2764
|
}
|
|
2695
2765
|
ctx.stroke();
|
|
2696
|
-
this.renderArrowhead(ctx, arrow, visualTo);
|
|
2766
|
+
this.renderArrowhead(ctx, arrow, visualTo, geometry.tangentEnd);
|
|
2697
2767
|
ctx.restore();
|
|
2698
2768
|
}
|
|
2699
|
-
renderArrowhead(ctx, arrow, tip) {
|
|
2700
|
-
const angle = getArrowTangentAngle(arrow.from, arrow.to, arrow.bend, 1);
|
|
2769
|
+
renderArrowhead(ctx, arrow, tip, angle) {
|
|
2701
2770
|
ctx.beginPath();
|
|
2702
2771
|
ctx.moveTo(tip.x, tip.y);
|
|
2703
2772
|
ctx.lineTo(
|
|
@@ -2712,7 +2781,7 @@ var ElementRenderer = class {
|
|
|
2712
2781
|
ctx.fillStyle = arrow.color;
|
|
2713
2782
|
ctx.fill();
|
|
2714
2783
|
}
|
|
2715
|
-
getVisualEndpoints(arrow) {
|
|
2784
|
+
getVisualEndpoints(arrow, geometry) {
|
|
2716
2785
|
let visualFrom = arrow.from;
|
|
2717
2786
|
let visualTo = arrow.to;
|
|
2718
2787
|
if (!this.store) return { visualFrom, visualTo };
|
|
@@ -2721,7 +2790,7 @@ var ElementRenderer = class {
|
|
|
2721
2790
|
if (el) {
|
|
2722
2791
|
const bounds = getElementBounds(el);
|
|
2723
2792
|
if (bounds) {
|
|
2724
|
-
const tangentAngle =
|
|
2793
|
+
const tangentAngle = geometry.tangentStart;
|
|
2725
2794
|
const rayTarget = {
|
|
2726
2795
|
x: arrow.from.x + Math.cos(tangentAngle) * 1e3,
|
|
2727
2796
|
y: arrow.from.y + Math.sin(tangentAngle) * 1e3
|
|
@@ -2735,7 +2804,7 @@ var ElementRenderer = class {
|
|
|
2735
2804
|
if (el) {
|
|
2736
2805
|
const bounds = getElementBounds(el);
|
|
2737
2806
|
if (bounds) {
|
|
2738
|
-
const tangentAngle =
|
|
2807
|
+
const tangentAngle = geometry.tangentEnd;
|
|
2739
2808
|
const rayTarget = {
|
|
2740
2809
|
x: arrow.to.x - Math.cos(tangentAngle) * 1e3,
|
|
2741
2810
|
y: arrow.to.y - Math.sin(tangentAngle) * 1e3
|
|
@@ -2790,20 +2859,20 @@ var ElementRenderer = class {
|
|
|
2790
2859
|
}
|
|
2791
2860
|
}
|
|
2792
2861
|
renderGrid(ctx, grid) {
|
|
2793
|
-
|
|
2862
|
+
const canvasSize = this.canvasSize;
|
|
2863
|
+
if (!canvasSize) return;
|
|
2794
2864
|
const cam = this.camera;
|
|
2795
2865
|
if (!cam) return;
|
|
2796
|
-
const
|
|
2797
|
-
|
|
2798
|
-
x:
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
};
|
|
2866
|
+
const bounds = this.gridBoundsOverride ?? (() => {
|
|
2867
|
+
const topLeft = cam.screenToWorld({ x: 0, y: 0 });
|
|
2868
|
+
const bottomRight = cam.screenToWorld({ x: canvasSize.w, y: canvasSize.h });
|
|
2869
|
+
return {
|
|
2870
|
+
minX: topLeft.x,
|
|
2871
|
+
minY: topLeft.y,
|
|
2872
|
+
maxX: bottomRight.x,
|
|
2873
|
+
maxY: bottomRight.y
|
|
2874
|
+
};
|
|
2875
|
+
})();
|
|
2807
2876
|
if (grid.gridType === "hex") {
|
|
2808
2877
|
const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
|
|
2809
2878
|
const scale = cam.zoom * dpr;
|
|
@@ -4099,19 +4168,19 @@ function loadImages(elements) {
|
|
|
4099
4168
|
const imageElements = elements.filter(
|
|
4100
4169
|
(el) => el.type === "image" && "src" in el
|
|
4101
4170
|
);
|
|
4102
|
-
const
|
|
4103
|
-
if (imageElements.length === 0) return Promise.resolve(
|
|
4171
|
+
const cache3 = /* @__PURE__ */ new Map();
|
|
4172
|
+
if (imageElements.length === 0) return Promise.resolve(cache3);
|
|
4104
4173
|
return new Promise((resolve) => {
|
|
4105
4174
|
let remaining = imageElements.length;
|
|
4106
4175
|
const done = () => {
|
|
4107
4176
|
remaining--;
|
|
4108
|
-
if (remaining <= 0) resolve(
|
|
4177
|
+
if (remaining <= 0) resolve(cache3);
|
|
4109
4178
|
};
|
|
4110
4179
|
for (const el of imageElements) {
|
|
4111
4180
|
const img = new Image();
|
|
4112
4181
|
img.crossOrigin = "anonymous";
|
|
4113
4182
|
img.onload = () => {
|
|
4114
|
-
|
|
4183
|
+
cache3.set(el.id, img);
|
|
4115
4184
|
done();
|
|
4116
4185
|
};
|
|
4117
4186
|
img.onerror = done;
|
|
@@ -4592,18 +4661,39 @@ var RenderStats = class {
|
|
|
4592
4661
|
frameTimes = [];
|
|
4593
4662
|
frameCount = 0;
|
|
4594
4663
|
_lastGridMs = 0;
|
|
4595
|
-
|
|
4664
|
+
_lastLayersMs = 0;
|
|
4665
|
+
_lastBackgroundMs = 0;
|
|
4666
|
+
_lastCompositeMs = 0;
|
|
4667
|
+
_lastOverlayMs = 0;
|
|
4668
|
+
recordFrame(durationMs, breakdown) {
|
|
4596
4669
|
this.frameCount++;
|
|
4597
4670
|
this.frameTimes.push(durationMs);
|
|
4598
4671
|
if (this.frameTimes.length > SAMPLE_SIZE) {
|
|
4599
4672
|
this.frameTimes.shift();
|
|
4600
4673
|
}
|
|
4601
|
-
if (
|
|
4674
|
+
if (breakdown !== void 0) {
|
|
4675
|
+
if (breakdown.gridMs !== void 0) this._lastGridMs = breakdown.gridMs;
|
|
4676
|
+
if (breakdown.layersMs !== void 0) this._lastLayersMs = breakdown.layersMs;
|
|
4677
|
+
if (breakdown.backgroundMs !== void 0) this._lastBackgroundMs = breakdown.backgroundMs;
|
|
4678
|
+
if (breakdown.compositeMs !== void 0) this._lastCompositeMs = breakdown.compositeMs;
|
|
4679
|
+
if (breakdown.overlayMs !== void 0) this._lastOverlayMs = breakdown.overlayMs;
|
|
4680
|
+
}
|
|
4602
4681
|
}
|
|
4603
4682
|
getSnapshot() {
|
|
4604
4683
|
const times = this.frameTimes;
|
|
4605
4684
|
if (times.length === 0) {
|
|
4606
|
-
return {
|
|
4685
|
+
return {
|
|
4686
|
+
fps: 0,
|
|
4687
|
+
avgFrameMs: 0,
|
|
4688
|
+
p95FrameMs: 0,
|
|
4689
|
+
lastFrameMs: 0,
|
|
4690
|
+
lastGridMs: 0,
|
|
4691
|
+
layersMs: 0,
|
|
4692
|
+
backgroundMs: 0,
|
|
4693
|
+
compositeMs: 0,
|
|
4694
|
+
overlayMs: 0,
|
|
4695
|
+
frameCount: 0
|
|
4696
|
+
};
|
|
4607
4697
|
}
|
|
4608
4698
|
const avg = times.reduce((a, b) => a + b, 0) / times.length;
|
|
4609
4699
|
const sorted = [...times].sort((a, b) => a - b);
|
|
@@ -4615,6 +4705,10 @@ var RenderStats = class {
|
|
|
4615
4705
|
p95FrameMs: Math.round((sorted[p95Index] ?? 0) * 100) / 100,
|
|
4616
4706
|
lastFrameMs: Math.round(lastFrame * 100) / 100,
|
|
4617
4707
|
lastGridMs: Math.round(this._lastGridMs * 100) / 100,
|
|
4708
|
+
layersMs: Math.round(this._lastLayersMs * 100) / 100,
|
|
4709
|
+
backgroundMs: Math.round(this._lastBackgroundMs * 100) / 100,
|
|
4710
|
+
compositeMs: Math.round(this._lastCompositeMs * 100) / 100,
|
|
4711
|
+
overlayMs: Math.round(this._lastOverlayMs * 100) / 100,
|
|
4618
4712
|
frameCount: this.frameCount
|
|
4619
4713
|
};
|
|
4620
4714
|
}
|
|
@@ -4637,19 +4731,14 @@ var RenderLoop = class {
|
|
|
4637
4731
|
layerManager;
|
|
4638
4732
|
domNodeManager;
|
|
4639
4733
|
layerCache;
|
|
4734
|
+
marginViewport;
|
|
4640
4735
|
activeDrawingLayerId = null;
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
lastCamY;
|
|
4736
|
+
gridCacheDirty = true;
|
|
4737
|
+
// set on recenter/viewport-change; consumed by the grid block
|
|
4644
4738
|
stats = new RenderStats();
|
|
4645
|
-
|
|
4739
|
+
layerGroups = /* @__PURE__ */ new Map();
|
|
4646
4740
|
gridCacheCanvas = null;
|
|
4647
4741
|
gridCacheCtx = null;
|
|
4648
|
-
gridCacheZoom = -1;
|
|
4649
|
-
gridCacheCamX = -Infinity;
|
|
4650
|
-
gridCacheCamY = -Infinity;
|
|
4651
|
-
gridCacheWidth = 0;
|
|
4652
|
-
gridCacheHeight = 0;
|
|
4653
4742
|
lastGridRef = null;
|
|
4654
4743
|
constructor(deps) {
|
|
4655
4744
|
this.canvasEl = deps.canvasEl;
|
|
@@ -4661,9 +4750,7 @@ var RenderLoop = class {
|
|
|
4661
4750
|
this.layerManager = deps.layerManager;
|
|
4662
4751
|
this.domNodeManager = deps.domNodeManager;
|
|
4663
4752
|
this.layerCache = deps.layerCache;
|
|
4664
|
-
this.
|
|
4665
|
-
this.lastCamX = deps.camera.position.x;
|
|
4666
|
-
this.lastCamY = deps.camera.position.y;
|
|
4753
|
+
this.marginViewport = deps.marginViewport;
|
|
4667
4754
|
}
|
|
4668
4755
|
requestRender() {
|
|
4669
4756
|
this.needsRender = true;
|
|
@@ -4690,7 +4777,9 @@ var RenderLoop = class {
|
|
|
4690
4777
|
setCanvasSize(width, height) {
|
|
4691
4778
|
this.canvasEl.width = width;
|
|
4692
4779
|
this.canvasEl.height = height;
|
|
4693
|
-
|
|
4780
|
+
const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
|
|
4781
|
+
this.marginViewport.setViewport(width / dpr, height / dpr, dpr);
|
|
4782
|
+
this.layerCache.resize();
|
|
4694
4783
|
}
|
|
4695
4784
|
setActiveDrawingLayer(layerId) {
|
|
4696
4785
|
this.activeDrawingLayerId = layerId;
|
|
@@ -4704,30 +4793,29 @@ var RenderLoop = class {
|
|
|
4704
4793
|
getStats() {
|
|
4705
4794
|
return this.stats.getSnapshot();
|
|
4706
4795
|
}
|
|
4707
|
-
compositeLayerCache(ctx, layerId
|
|
4796
|
+
compositeLayerCache(ctx, layerId) {
|
|
4708
4797
|
const cached = this.layerCache.getCanvas(layerId);
|
|
4798
|
+
const offset = this.marginViewport.compositeOffset(
|
|
4799
|
+
this.camera.position.x,
|
|
4800
|
+
this.camera.position.y
|
|
4801
|
+
);
|
|
4709
4802
|
ctx.save();
|
|
4710
|
-
ctx.
|
|
4711
|
-
ctx.
|
|
4712
|
-
ctx.scale(1 / dpr, 1 / dpr);
|
|
4713
|
-
ctx.drawImage(cached, 0, 0);
|
|
4803
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
4804
|
+
ctx.drawImage(cached, offset.x, offset.y);
|
|
4714
4805
|
ctx.restore();
|
|
4715
4806
|
}
|
|
4716
|
-
ensureGridCache(
|
|
4717
|
-
|
|
4807
|
+
ensureGridCache() {
|
|
4808
|
+
const w = this.marginViewport.physicalWidth();
|
|
4809
|
+
const h = this.marginViewport.physicalHeight();
|
|
4810
|
+
if (this.gridCacheCanvas !== null && this.gridCacheCanvas.width === w && this.gridCacheCanvas.height === h) {
|
|
4718
4811
|
return;
|
|
4719
4812
|
}
|
|
4720
|
-
const physWidth = Math.round(cssWidth * dpr);
|
|
4721
|
-
const physHeight = Math.round(cssHeight * dpr);
|
|
4722
4813
|
if (typeof OffscreenCanvas !== "undefined") {
|
|
4723
|
-
this.gridCacheCanvas = new OffscreenCanvas(
|
|
4724
|
-
physWidth,
|
|
4725
|
-
physHeight
|
|
4726
|
-
);
|
|
4814
|
+
this.gridCacheCanvas = new OffscreenCanvas(w, h);
|
|
4727
4815
|
} else if (typeof document !== "undefined") {
|
|
4728
4816
|
const el = document.createElement("canvas");
|
|
4729
|
-
el.width =
|
|
4730
|
-
el.height =
|
|
4817
|
+
el.width = w;
|
|
4818
|
+
el.height = h;
|
|
4731
4819
|
this.gridCacheCanvas = el;
|
|
4732
4820
|
} else {
|
|
4733
4821
|
this.gridCacheCanvas = null;
|
|
@@ -4740,22 +4828,26 @@ var RenderLoop = class {
|
|
|
4740
4828
|
const t0 = performance.now();
|
|
4741
4829
|
const ctx = this.canvasEl.getContext("2d");
|
|
4742
4830
|
if (!ctx) return;
|
|
4831
|
+
let layersMs = 0;
|
|
4832
|
+
let compositeMs = 0;
|
|
4833
|
+
let gridMs = 0;
|
|
4743
4834
|
const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
|
|
4744
4835
|
const cssWidth = this.canvasEl.clientWidth;
|
|
4745
4836
|
const cssHeight = this.canvasEl.clientHeight;
|
|
4837
|
+
this.marginViewport.setViewport(cssWidth, cssHeight, dpr);
|
|
4746
4838
|
const currentZoom = this.camera.zoom;
|
|
4747
4839
|
const currentCamX = this.camera.position.x;
|
|
4748
4840
|
const currentCamY = this.camera.position.y;
|
|
4749
|
-
if (
|
|
4841
|
+
if (this.marginViewport.needsRecenter(currentCamX, currentCamY, currentZoom)) {
|
|
4842
|
+
this.marginViewport.recenter(currentCamX, currentCamY, currentZoom);
|
|
4750
4843
|
this.layerCache.markAllDirty();
|
|
4751
|
-
this.
|
|
4752
|
-
this.lastCamX = currentCamX;
|
|
4753
|
-
this.lastCamY = currentCamY;
|
|
4844
|
+
this.gridCacheDirty = true;
|
|
4754
4845
|
}
|
|
4755
4846
|
ctx.save();
|
|
4756
4847
|
ctx.scale(dpr, dpr);
|
|
4757
4848
|
this.renderer.setCanvasSize(cssWidth, cssHeight);
|
|
4758
4849
|
const hasGridElement = this.store.getElementsByType("grid").length > 0;
|
|
4850
|
+
const bgT0 = performance.now();
|
|
4759
4851
|
if (hasGridElement) {
|
|
4760
4852
|
ctx.save();
|
|
4761
4853
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
@@ -4764,19 +4856,20 @@ var RenderLoop = class {
|
|
|
4764
4856
|
} else {
|
|
4765
4857
|
this.background.render(ctx, this.camera);
|
|
4766
4858
|
}
|
|
4859
|
+
const backgroundMs = performance.now() - bgT0;
|
|
4767
4860
|
ctx.save();
|
|
4768
4861
|
ctx.translate(this.camera.position.x, this.camera.position.y);
|
|
4769
4862
|
ctx.scale(this.camera.zoom, this.camera.zoom);
|
|
4770
|
-
const
|
|
4771
|
-
const
|
|
4863
|
+
const cullBounds = this.marginViewport.cachedWorldBounds();
|
|
4864
|
+
const cullPad = Math.max(cullBounds.w, cullBounds.h) * 0.05;
|
|
4772
4865
|
const cullingRect = {
|
|
4773
|
-
x:
|
|
4774
|
-
y:
|
|
4775
|
-
w:
|
|
4776
|
-
h:
|
|
4866
|
+
x: cullBounds.x - cullPad,
|
|
4867
|
+
y: cullBounds.y - cullPad,
|
|
4868
|
+
w: cullBounds.w + cullPad * 2,
|
|
4869
|
+
h: cullBounds.h + cullPad * 2
|
|
4777
4870
|
};
|
|
4778
4871
|
const allElements = this.store.getAll();
|
|
4779
|
-
|
|
4872
|
+
this.layerGroups.clear();
|
|
4780
4873
|
const gridElements = [];
|
|
4781
4874
|
let domZIndex = 0;
|
|
4782
4875
|
for (const element of allElements) {
|
|
@@ -4799,31 +4892,34 @@ var RenderLoop = class {
|
|
|
4799
4892
|
gridElements.push(element);
|
|
4800
4893
|
continue;
|
|
4801
4894
|
}
|
|
4802
|
-
let group =
|
|
4895
|
+
let group = this.layerGroups.get(element.layerId);
|
|
4803
4896
|
if (!group) {
|
|
4804
4897
|
group = [];
|
|
4805
|
-
|
|
4898
|
+
this.layerGroups.set(element.layerId, group);
|
|
4806
4899
|
}
|
|
4807
4900
|
group.push(element);
|
|
4808
4901
|
}
|
|
4809
|
-
for (const [layerId, elements] of
|
|
4902
|
+
for (const [layerId, elements] of this.layerGroups) {
|
|
4810
4903
|
const isActiveDrawingLayer = layerId === this.activeDrawingLayerId;
|
|
4811
4904
|
if (!this.layerCache.isDirty(layerId)) {
|
|
4812
|
-
|
|
4905
|
+
const compT0 = performance.now();
|
|
4906
|
+
this.compositeLayerCache(ctx, layerId);
|
|
4907
|
+
compositeMs += performance.now() - compT0;
|
|
4813
4908
|
continue;
|
|
4814
4909
|
}
|
|
4815
4910
|
if (isActiveDrawingLayer) {
|
|
4816
|
-
|
|
4911
|
+
const compT0 = performance.now();
|
|
4912
|
+
this.compositeLayerCache(ctx, layerId);
|
|
4913
|
+
compositeMs += performance.now() - compT0;
|
|
4817
4914
|
continue;
|
|
4818
4915
|
}
|
|
4819
4916
|
const offCtx = this.layerCache.getContext(layerId);
|
|
4820
4917
|
if (offCtx) {
|
|
4918
|
+
const layerT0 = performance.now();
|
|
4821
4919
|
const offCanvas = this.layerCache.getCanvas(layerId);
|
|
4822
4920
|
offCtx.clearRect(0, 0, offCanvas.width, offCanvas.height);
|
|
4823
4921
|
offCtx.save();
|
|
4824
|
-
|
|
4825
|
-
offCtx.translate(this.camera.position.x, this.camera.position.y);
|
|
4826
|
-
offCtx.scale(this.camera.zoom, this.camera.zoom);
|
|
4922
|
+
this.marginViewport.applyRenderTransform(offCtx);
|
|
4827
4923
|
for (const element of elements) {
|
|
4828
4924
|
const elBounds = getElementBounds(element);
|
|
4829
4925
|
if (elBounds && !boundsIntersect(elBounds, cullingRect)) continue;
|
|
@@ -4831,56 +4927,73 @@ var RenderLoop = class {
|
|
|
4831
4927
|
}
|
|
4832
4928
|
offCtx.restore();
|
|
4833
4929
|
this.layerCache.markClean(layerId);
|
|
4834
|
-
|
|
4930
|
+
layersMs += performance.now() - layerT0;
|
|
4931
|
+
const compT0 = performance.now();
|
|
4932
|
+
this.compositeLayerCache(ctx, layerId);
|
|
4933
|
+
compositeMs += performance.now() - compT0;
|
|
4835
4934
|
}
|
|
4836
4935
|
}
|
|
4837
4936
|
if (gridElements.length > 0) {
|
|
4838
4937
|
const gridT0 = performance.now();
|
|
4839
4938
|
const gridRef = gridElements[0];
|
|
4840
|
-
const
|
|
4841
|
-
if (
|
|
4842
|
-
|
|
4843
|
-
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
4844
|
-
ctx.drawImage(this.gridCacheCanvas, 0, 0);
|
|
4845
|
-
ctx.restore();
|
|
4846
|
-
} else {
|
|
4847
|
-
this.ensureGridCache(cssWidth, cssHeight, dpr);
|
|
4939
|
+
const gridDirty = this.gridCacheDirty || gridRef !== this.lastGridRef;
|
|
4940
|
+
if (gridDirty) {
|
|
4941
|
+
this.ensureGridCache();
|
|
4848
4942
|
if (this.gridCacheCtx && this.gridCacheCanvas) {
|
|
4943
|
+
const cb = this.marginViewport.cachedWorldBounds();
|
|
4944
|
+
this.renderer.setGridBoundsOverride({
|
|
4945
|
+
minX: cb.x,
|
|
4946
|
+
minY: cb.y,
|
|
4947
|
+
maxX: cb.x + cb.w,
|
|
4948
|
+
maxY: cb.y + cb.h
|
|
4949
|
+
});
|
|
4849
4950
|
const gc = this.gridCacheCtx;
|
|
4850
4951
|
gc.clearRect(0, 0, this.gridCacheCanvas.width, this.gridCacheCanvas.height);
|
|
4851
4952
|
gc.save();
|
|
4852
|
-
|
|
4853
|
-
|
|
4854
|
-
|
|
4855
|
-
|
|
4856
|
-
|
|
4857
|
-
}
|
|
4858
|
-
|
|
4859
|
-
|
|
4860
|
-
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
4861
|
-
ctx.drawImage(this.gridCacheCanvas, 0, 0);
|
|
4862
|
-
ctx.restore();
|
|
4863
|
-
} else {
|
|
4864
|
-
for (const grid of gridElements) {
|
|
4865
|
-
this.renderer.renderCanvasElement(ctx, grid);
|
|
4953
|
+
this.marginViewport.applyRenderTransform(gc);
|
|
4954
|
+
try {
|
|
4955
|
+
for (const grid of gridElements) {
|
|
4956
|
+
this.renderer.renderCanvasElement(gc, grid);
|
|
4957
|
+
}
|
|
4958
|
+
} finally {
|
|
4959
|
+
gc.restore();
|
|
4960
|
+
this.renderer.setGridBoundsOverride(null);
|
|
4866
4961
|
}
|
|
4867
4962
|
}
|
|
4868
|
-
this.
|
|
4869
|
-
this.gridCacheCamX = currentCamX;
|
|
4870
|
-
this.gridCacheCamY = currentCamY;
|
|
4871
|
-
this.gridCacheWidth = cssWidth;
|
|
4872
|
-
this.gridCacheHeight = cssHeight;
|
|
4963
|
+
this.gridCacheDirty = false;
|
|
4873
4964
|
this.lastGridRef = gridRef;
|
|
4874
4965
|
}
|
|
4875
|
-
this.
|
|
4966
|
+
if (this.gridCacheCanvas) {
|
|
4967
|
+
const offset = this.marginViewport.compositeOffset(
|
|
4968
|
+
this.camera.position.x,
|
|
4969
|
+
this.camera.position.y
|
|
4970
|
+
);
|
|
4971
|
+
ctx.save();
|
|
4972
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
4973
|
+
ctx.drawImage(this.gridCacheCanvas, offset.x, offset.y);
|
|
4974
|
+
ctx.restore();
|
|
4975
|
+
} else {
|
|
4976
|
+
for (const grid of gridElements) {
|
|
4977
|
+
this.renderer.renderCanvasElement(ctx, grid);
|
|
4978
|
+
}
|
|
4979
|
+
}
|
|
4980
|
+
gridMs = performance.now() - gridT0;
|
|
4876
4981
|
}
|
|
4982
|
+
const overlayT0 = performance.now();
|
|
4877
4983
|
const activeTool = this.toolManager.activeTool;
|
|
4878
4984
|
if (activeTool?.renderOverlay) {
|
|
4879
4985
|
activeTool.renderOverlay(ctx);
|
|
4880
4986
|
}
|
|
4987
|
+
const overlayMs = performance.now() - overlayT0;
|
|
4881
4988
|
ctx.restore();
|
|
4882
4989
|
ctx.restore();
|
|
4883
|
-
this.stats.recordFrame(performance.now() - t0,
|
|
4990
|
+
this.stats.recordFrame(performance.now() - t0, {
|
|
4991
|
+
gridMs,
|
|
4992
|
+
layersMs,
|
|
4993
|
+
backgroundMs,
|
|
4994
|
+
compositeMs,
|
|
4995
|
+
overlayMs
|
|
4996
|
+
});
|
|
4884
4997
|
}
|
|
4885
4998
|
};
|
|
4886
4999
|
|
|
@@ -4895,15 +5008,11 @@ function createOffscreenCanvas(width, height) {
|
|
|
4895
5008
|
return canvas;
|
|
4896
5009
|
}
|
|
4897
5010
|
var LayerCache = class {
|
|
5011
|
+
constructor(viewport) {
|
|
5012
|
+
this.viewport = viewport;
|
|
5013
|
+
}
|
|
4898
5014
|
canvases = /* @__PURE__ */ new Map();
|
|
4899
5015
|
dirtyFlags = /* @__PURE__ */ new Map();
|
|
4900
|
-
width;
|
|
4901
|
-
height;
|
|
4902
|
-
constructor(width, height) {
|
|
4903
|
-
const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
|
|
4904
|
-
this.width = Math.round(width * dpr);
|
|
4905
|
-
this.height = Math.round(height * dpr);
|
|
4906
|
-
}
|
|
4907
5016
|
isDirty(layerId) {
|
|
4908
5017
|
return this.dirtyFlags.get(layerId) !== false;
|
|
4909
5018
|
}
|
|
@@ -4921,7 +5030,7 @@ var LayerCache = class {
|
|
|
4921
5030
|
getCanvas(layerId) {
|
|
4922
5031
|
let canvas = this.canvases.get(layerId);
|
|
4923
5032
|
if (!canvas) {
|
|
4924
|
-
canvas = createOffscreenCanvas(this.
|
|
5033
|
+
canvas = createOffscreenCanvas(this.viewport.physicalWidth(), this.viewport.physicalHeight());
|
|
4925
5034
|
this.canvases.set(layerId, canvas);
|
|
4926
5035
|
this.dirtyFlags.set(layerId, true);
|
|
4927
5036
|
}
|
|
@@ -4931,13 +5040,12 @@ var LayerCache = class {
|
|
|
4931
5040
|
const canvas = this.getCanvas(layerId);
|
|
4932
5041
|
return canvas.getContext("2d");
|
|
4933
5042
|
}
|
|
4934
|
-
resize(
|
|
4935
|
-
const
|
|
4936
|
-
|
|
4937
|
-
this.height = Math.round(height * dpr);
|
|
5043
|
+
resize() {
|
|
5044
|
+
const w = this.viewport.physicalWidth();
|
|
5045
|
+
const h = this.viewport.physicalHeight();
|
|
4938
5046
|
for (const [id, canvas] of this.canvases) {
|
|
4939
|
-
canvas.width =
|
|
4940
|
-
canvas.height =
|
|
5047
|
+
canvas.width = w;
|
|
5048
|
+
canvas.height = h;
|
|
4941
5049
|
this.dirtyFlags.set(id, true);
|
|
4942
5050
|
}
|
|
4943
5051
|
}
|
|
@@ -4947,6 +5055,75 @@ var LayerCache = class {
|
|
|
4947
5055
|
}
|
|
4948
5056
|
};
|
|
4949
5057
|
|
|
5058
|
+
// src/canvas/margin-viewport.ts
|
|
5059
|
+
var MarginViewport = class {
|
|
5060
|
+
constructor(marginPx) {
|
|
5061
|
+
this.marginPx = marginPx;
|
|
5062
|
+
}
|
|
5063
|
+
cssW = 0;
|
|
5064
|
+
cssH = 0;
|
|
5065
|
+
dpr = 1;
|
|
5066
|
+
anchorCamX = 0;
|
|
5067
|
+
anchorCamY = 0;
|
|
5068
|
+
anchorZoom = Number.NaN;
|
|
5069
|
+
// sentinel → first needsRecenter is true
|
|
5070
|
+
viewportDirty = true;
|
|
5071
|
+
setMargin(marginPx) {
|
|
5072
|
+
if (marginPx !== this.marginPx) {
|
|
5073
|
+
this.marginPx = marginPx;
|
|
5074
|
+
this.viewportDirty = true;
|
|
5075
|
+
}
|
|
5076
|
+
}
|
|
5077
|
+
setViewport(cssW, cssH, dpr) {
|
|
5078
|
+
if (cssW !== this.cssW || cssH !== this.cssH || dpr !== this.dpr) {
|
|
5079
|
+
this.cssW = cssW;
|
|
5080
|
+
this.cssH = cssH;
|
|
5081
|
+
this.dpr = dpr;
|
|
5082
|
+
this.viewportDirty = true;
|
|
5083
|
+
}
|
|
5084
|
+
}
|
|
5085
|
+
physicalWidth() {
|
|
5086
|
+
return Math.round((this.cssW + 2 * this.marginPx) * this.dpr);
|
|
5087
|
+
}
|
|
5088
|
+
physicalHeight() {
|
|
5089
|
+
return Math.round((this.cssH + 2 * this.marginPx) * this.dpr);
|
|
5090
|
+
}
|
|
5091
|
+
needsRecenter(camX, camY, zoom) {
|
|
5092
|
+
return this.viewportDirty || zoom !== this.anchorZoom || Math.abs(camX - this.anchorCamX) > this.marginPx || Math.abs(camY - this.anchorCamY) > this.marginPx;
|
|
5093
|
+
}
|
|
5094
|
+
recenter(camX, camY, zoom) {
|
|
5095
|
+
this.anchorCamX = camX;
|
|
5096
|
+
this.anchorCamY = camY;
|
|
5097
|
+
this.anchorZoom = zoom;
|
|
5098
|
+
this.viewportDirty = false;
|
|
5099
|
+
}
|
|
5100
|
+
/** Applies dpr scale + anchor-relative world transform. setViewport must have been called first. */
|
|
5101
|
+
applyRenderTransform(ctx) {
|
|
5102
|
+
ctx.scale(this.dpr, this.dpr);
|
|
5103
|
+
ctx.translate(this.marginPx + this.anchorCamX, this.marginPx + this.anchorCamY);
|
|
5104
|
+
ctx.scale(this.anchorZoom, this.anchorZoom);
|
|
5105
|
+
}
|
|
5106
|
+
// Device-px destination for drawImage(cache, x, y).
|
|
5107
|
+
// A world point P sits in the cache at CSS x `margin + anchorCamX + P*zoom`; it must land on
|
|
5108
|
+
// screen at `camX + P*zoom`; so the blit offset is `camX - anchorCamX - margin` (CSS) * dpr.
|
|
5109
|
+
compositeOffset(camX, camY) {
|
|
5110
|
+
return {
|
|
5111
|
+
x: (camX - this.anchorCamX - this.marginPx) * this.dpr,
|
|
5112
|
+
y: (camY - this.anchorCamY - this.marginPx) * this.dpr
|
|
5113
|
+
};
|
|
5114
|
+
}
|
|
5115
|
+
// World-space bounds of the whole cached region at the anchor (cull rect for re-renders).
|
|
5116
|
+
cachedWorldBounds() {
|
|
5117
|
+
const z = this.anchorZoom;
|
|
5118
|
+
return {
|
|
5119
|
+
x: (-this.marginPx - this.anchorCamX) / z,
|
|
5120
|
+
y: (-this.marginPx - this.anchorCamY) / z,
|
|
5121
|
+
w: (this.cssW + 2 * this.marginPx) / z,
|
|
5122
|
+
h: (this.cssH + 2 * this.marginPx) / z
|
|
5123
|
+
};
|
|
5124
|
+
}
|
|
5125
|
+
};
|
|
5126
|
+
|
|
4950
5127
|
// src/canvas/viewport.ts
|
|
4951
5128
|
var Viewport = class {
|
|
4952
5129
|
constructor(container, options = {}) {
|
|
@@ -5023,10 +5200,13 @@ var Viewport = class {
|
|
|
5023
5200
|
this.interactMode = new InteractMode({
|
|
5024
5201
|
getNode: (id) => this.domNodeManager.getNode(id)
|
|
5025
5202
|
});
|
|
5026
|
-
|
|
5203
|
+
this.marginViewport = new MarginViewport(options.panBufferMargin ?? 256);
|
|
5204
|
+
this.marginViewport.setViewport(
|
|
5027
5205
|
this.canvasEl.clientWidth || 800,
|
|
5028
|
-
this.canvasEl.clientHeight || 600
|
|
5206
|
+
this.canvasEl.clientHeight || 600,
|
|
5207
|
+
typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1
|
|
5029
5208
|
);
|
|
5209
|
+
const layerCache = new LayerCache(this.marginViewport);
|
|
5030
5210
|
this.renderLoop = new RenderLoop({
|
|
5031
5211
|
canvasEl: this.canvasEl,
|
|
5032
5212
|
camera: this.camera,
|
|
@@ -5036,7 +5216,8 @@ var Viewport = class {
|
|
|
5036
5216
|
toolManager: this.toolManager,
|
|
5037
5217
|
layerManager: this.layerManager,
|
|
5038
5218
|
domNodeManager: this.domNodeManager,
|
|
5039
|
-
layerCache
|
|
5219
|
+
layerCache,
|
|
5220
|
+
marginViewport: this.marginViewport
|
|
5040
5221
|
});
|
|
5041
5222
|
this.unsubCamera = this.camera.onChange(() => {
|
|
5042
5223
|
this.applyCameraTransform();
|
|
@@ -5100,6 +5281,7 @@ var Viewport = class {
|
|
|
5100
5281
|
noteEditor;
|
|
5101
5282
|
historyRecorder;
|
|
5102
5283
|
toolContext;
|
|
5284
|
+
marginViewport;
|
|
5103
5285
|
resizeObserver = null;
|
|
5104
5286
|
_snapToGrid = false;
|
|
5105
5287
|
_gridSize;
|
|
@@ -5292,7 +5474,7 @@ var Viewport = class {
|
|
|
5292
5474
|
const id = setInterval(() => {
|
|
5293
5475
|
const s = this.getRenderStats();
|
|
5294
5476
|
console.log(
|
|
5295
|
-
`[FieldNotes] fps=${s.fps} frame=${s.avgFrameMs}ms p95=${s.p95FrameMs}ms grid=${s.lastGridMs}ms`
|
|
5477
|
+
`[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`
|
|
5296
5478
|
);
|
|
5297
5479
|
}, intervalMs);
|
|
5298
5480
|
return () => clearInterval(id);
|
|
@@ -7290,7 +7472,7 @@ var TemplateTool = class {
|
|
|
7290
7472
|
};
|
|
7291
7473
|
|
|
7292
7474
|
// src/index.ts
|
|
7293
|
-
var VERSION = "0.
|
|
7475
|
+
var VERSION = "0.23.0";
|
|
7294
7476
|
export {
|
|
7295
7477
|
AddElementCommand,
|
|
7296
7478
|
ArrowTool,
|