@fieldnotes/core 0.20.0 → 0.22.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 +9 -0
- package/dist/index.cjs +323 -139
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +12 -1
- package/dist/index.d.ts +12 -1
- package/dist/index.js +323 -139
- 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: [] };
|
|
@@ -2609,6 +2665,7 @@ var ElementRenderer = class {
|
|
|
2609
2665
|
store = null;
|
|
2610
2666
|
imageCache = /* @__PURE__ */ new Map();
|
|
2611
2667
|
onImageLoad = null;
|
|
2668
|
+
onImageError = null;
|
|
2612
2669
|
camera = null;
|
|
2613
2670
|
canvasSize = null;
|
|
2614
2671
|
hexTileCache = null;
|
|
@@ -2619,6 +2676,9 @@ var ElementRenderer = class {
|
|
|
2619
2676
|
setOnImageLoad(callback) {
|
|
2620
2677
|
this.onImageLoad = callback;
|
|
2621
2678
|
}
|
|
2679
|
+
setOnImageError(callback) {
|
|
2680
|
+
this.onImageError = callback;
|
|
2681
|
+
}
|
|
2622
2682
|
setCamera(camera) {
|
|
2623
2683
|
this.camera = camera;
|
|
2624
2684
|
}
|
|
@@ -2658,21 +2718,29 @@ var ElementRenderer = class {
|
|
|
2658
2718
|
ctx.lineCap = "round";
|
|
2659
2719
|
ctx.lineJoin = "round";
|
|
2660
2720
|
ctx.globalAlpha = stroke.opacity;
|
|
2661
|
-
const
|
|
2662
|
-
|
|
2663
|
-
const
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2721
|
+
const data = getStrokeRenderData(stroke);
|
|
2722
|
+
if (data.buckets) {
|
|
2723
|
+
for (const bucket of data.buckets) {
|
|
2724
|
+
ctx.lineWidth = bucket.width;
|
|
2725
|
+
ctx.stroke(bucket.path);
|
|
2726
|
+
}
|
|
2727
|
+
} else {
|
|
2728
|
+
for (let i = 0; i < data.segments.length; i++) {
|
|
2729
|
+
const seg = data.segments[i];
|
|
2730
|
+
const w = data.widths[i];
|
|
2731
|
+
if (!seg || w === void 0) continue;
|
|
2732
|
+
ctx.lineWidth = w;
|
|
2733
|
+
ctx.beginPath();
|
|
2734
|
+
ctx.moveTo(seg.start.x, seg.start.y);
|
|
2735
|
+
ctx.bezierCurveTo(seg.cp1.x, seg.cp1.y, seg.cp2.x, seg.cp2.y, seg.end.x, seg.end.y);
|
|
2736
|
+
ctx.stroke();
|
|
2737
|
+
}
|
|
2671
2738
|
}
|
|
2672
2739
|
ctx.restore();
|
|
2673
2740
|
}
|
|
2674
2741
|
renderArrow(ctx, arrow) {
|
|
2675
|
-
const
|
|
2742
|
+
const geometry = getArrowRenderGeometry(arrow);
|
|
2743
|
+
const { visualFrom, visualTo } = this.getVisualEndpoints(arrow, geometry);
|
|
2676
2744
|
ctx.save();
|
|
2677
2745
|
ctx.strokeStyle = arrow.color;
|
|
2678
2746
|
ctx.lineWidth = arrow.width;
|
|
@@ -2683,17 +2751,18 @@ var ElementRenderer = class {
|
|
|
2683
2751
|
ctx.beginPath();
|
|
2684
2752
|
ctx.moveTo(visualFrom.x, visualFrom.y);
|
|
2685
2753
|
if (arrow.bend !== 0) {
|
|
2686
|
-
const cp =
|
|
2687
|
-
|
|
2754
|
+
const cp = geometry.controlPoint;
|
|
2755
|
+
if (cp) {
|
|
2756
|
+
ctx.quadraticCurveTo(cp.x, cp.y, visualTo.x, visualTo.y);
|
|
2757
|
+
}
|
|
2688
2758
|
} else {
|
|
2689
2759
|
ctx.lineTo(visualTo.x, visualTo.y);
|
|
2690
2760
|
}
|
|
2691
2761
|
ctx.stroke();
|
|
2692
|
-
this.renderArrowhead(ctx, arrow, visualTo);
|
|
2762
|
+
this.renderArrowhead(ctx, arrow, visualTo, geometry.tangentEnd);
|
|
2693
2763
|
ctx.restore();
|
|
2694
2764
|
}
|
|
2695
|
-
renderArrowhead(ctx, arrow, tip) {
|
|
2696
|
-
const angle = getArrowTangentAngle(arrow.from, arrow.to, arrow.bend, 1);
|
|
2765
|
+
renderArrowhead(ctx, arrow, tip, angle) {
|
|
2697
2766
|
ctx.beginPath();
|
|
2698
2767
|
ctx.moveTo(tip.x, tip.y);
|
|
2699
2768
|
ctx.lineTo(
|
|
@@ -2708,7 +2777,7 @@ var ElementRenderer = class {
|
|
|
2708
2777
|
ctx.fillStyle = arrow.color;
|
|
2709
2778
|
ctx.fill();
|
|
2710
2779
|
}
|
|
2711
|
-
getVisualEndpoints(arrow) {
|
|
2780
|
+
getVisualEndpoints(arrow, geometry) {
|
|
2712
2781
|
let visualFrom = arrow.from;
|
|
2713
2782
|
let visualTo = arrow.to;
|
|
2714
2783
|
if (!this.store) return { visualFrom, visualTo };
|
|
@@ -2717,7 +2786,7 @@ var ElementRenderer = class {
|
|
|
2717
2786
|
if (el) {
|
|
2718
2787
|
const bounds = getElementBounds(el);
|
|
2719
2788
|
if (bounds) {
|
|
2720
|
-
const tangentAngle =
|
|
2789
|
+
const tangentAngle = geometry.tangentStart;
|
|
2721
2790
|
const rayTarget = {
|
|
2722
2791
|
x: arrow.from.x + Math.cos(tangentAngle) * 1e3,
|
|
2723
2792
|
y: arrow.from.y + Math.sin(tangentAngle) * 1e3
|
|
@@ -2731,7 +2800,7 @@ var ElementRenderer = class {
|
|
|
2731
2800
|
if (el) {
|
|
2732
2801
|
const bounds = getElementBounds(el);
|
|
2733
2802
|
if (bounds) {
|
|
2734
|
-
const tangentAngle =
|
|
2803
|
+
const tangentAngle = geometry.tangentEnd;
|
|
2735
2804
|
const rayTarget = {
|
|
2736
2805
|
x: arrow.to.x - Math.cos(tangentAngle) * 1e3,
|
|
2737
2806
|
y: arrow.to.y - Math.sin(tangentAngle) * 1e3
|
|
@@ -2977,6 +3046,10 @@ var ElementRenderer = class {
|
|
|
2977
3046
|
ctx.restore();
|
|
2978
3047
|
}
|
|
2979
3048
|
renderImage(ctx, image) {
|
|
3049
|
+
if (this.imageCache.get(image.src) === "failed") {
|
|
3050
|
+
this.renderImagePlaceholder(ctx, image);
|
|
3051
|
+
return;
|
|
3052
|
+
}
|
|
2980
3053
|
const img = this.getImage(image.src);
|
|
2981
3054
|
if (!img) return;
|
|
2982
3055
|
ctx.drawImage(
|
|
@@ -2987,6 +3060,27 @@ var ElementRenderer = class {
|
|
|
2987
3060
|
image.size.h
|
|
2988
3061
|
);
|
|
2989
3062
|
}
|
|
3063
|
+
renderImagePlaceholder(ctx, image) {
|
|
3064
|
+
const { x, y } = image.position;
|
|
3065
|
+
const { w, h } = image.size;
|
|
3066
|
+
ctx.save();
|
|
3067
|
+
ctx.fillStyle = "#eeeeee";
|
|
3068
|
+
ctx.fillRect(x, y, w, h);
|
|
3069
|
+
ctx.strokeStyle = "#bdbdbd";
|
|
3070
|
+
ctx.lineWidth = 1;
|
|
3071
|
+
ctx.strokeRect(x, y, w, h);
|
|
3072
|
+
const glyph = Math.min(24, w / 2, h / 2);
|
|
3073
|
+
const cx = x + w / 2;
|
|
3074
|
+
const cy = y + h / 2;
|
|
3075
|
+
ctx.strokeStyle = "#9e9e9e";
|
|
3076
|
+
ctx.lineWidth = 2;
|
|
3077
|
+
ctx.beginPath();
|
|
3078
|
+
ctx.arc(cx, cy, glyph / 2, 0, Math.PI * 2);
|
|
3079
|
+
ctx.moveTo(cx - glyph / 2, cy + glyph / 2);
|
|
3080
|
+
ctx.lineTo(cx + glyph / 2, cy - glyph / 2);
|
|
3081
|
+
ctx.stroke();
|
|
3082
|
+
ctx.restore();
|
|
3083
|
+
}
|
|
2990
3084
|
getHexTile(cellSize, orientation, strokeColor, strokeWidth, opacity, scale) {
|
|
2991
3085
|
const key = `${cellSize}:${orientation}:${strokeColor}:${strokeWidth}:${opacity}:${scale}`;
|
|
2992
3086
|
if (this.hexTileCacheKey === key && this.hexTileCache) {
|
|
@@ -3002,6 +3096,7 @@ var ElementRenderer = class {
|
|
|
3002
3096
|
getImage(src) {
|
|
3003
3097
|
const cached = this.imageCache.get(src);
|
|
3004
3098
|
if (cached) {
|
|
3099
|
+
if (cached === "failed") return null;
|
|
3005
3100
|
if (cached instanceof HTMLImageElement) return cached.complete ? cached : null;
|
|
3006
3101
|
return cached;
|
|
3007
3102
|
}
|
|
@@ -3018,6 +3113,11 @@ var ElementRenderer = class {
|
|
|
3018
3113
|
});
|
|
3019
3114
|
}
|
|
3020
3115
|
};
|
|
3116
|
+
img.onerror = () => {
|
|
3117
|
+
this.imageCache.set(src, "failed");
|
|
3118
|
+
this.onImageError?.(src);
|
|
3119
|
+
this.onImageLoad?.();
|
|
3120
|
+
};
|
|
3021
3121
|
return null;
|
|
3022
3122
|
}
|
|
3023
3123
|
};
|
|
@@ -4064,19 +4164,19 @@ function loadImages(elements) {
|
|
|
4064
4164
|
const imageElements = elements.filter(
|
|
4065
4165
|
(el) => el.type === "image" && "src" in el
|
|
4066
4166
|
);
|
|
4067
|
-
const
|
|
4068
|
-
if (imageElements.length === 0) return Promise.resolve(
|
|
4167
|
+
const cache3 = /* @__PURE__ */ new Map();
|
|
4168
|
+
if (imageElements.length === 0) return Promise.resolve(cache3);
|
|
4069
4169
|
return new Promise((resolve) => {
|
|
4070
4170
|
let remaining = imageElements.length;
|
|
4071
4171
|
const done = () => {
|
|
4072
4172
|
remaining--;
|
|
4073
|
-
if (remaining <= 0) resolve(
|
|
4173
|
+
if (remaining <= 0) resolve(cache3);
|
|
4074
4174
|
};
|
|
4075
4175
|
for (const el of imageElements) {
|
|
4076
4176
|
const img = new Image();
|
|
4077
4177
|
img.crossOrigin = "anonymous";
|
|
4078
4178
|
img.onload = () => {
|
|
4079
|
-
|
|
4179
|
+
cache3.set(el.id, img);
|
|
4080
4180
|
done();
|
|
4081
4181
|
};
|
|
4082
4182
|
img.onerror = done;
|
|
@@ -4557,18 +4657,39 @@ var RenderStats = class {
|
|
|
4557
4657
|
frameTimes = [];
|
|
4558
4658
|
frameCount = 0;
|
|
4559
4659
|
_lastGridMs = 0;
|
|
4560
|
-
|
|
4660
|
+
_lastLayersMs = 0;
|
|
4661
|
+
_lastBackgroundMs = 0;
|
|
4662
|
+
_lastCompositeMs = 0;
|
|
4663
|
+
_lastOverlayMs = 0;
|
|
4664
|
+
recordFrame(durationMs, breakdown) {
|
|
4561
4665
|
this.frameCount++;
|
|
4562
4666
|
this.frameTimes.push(durationMs);
|
|
4563
4667
|
if (this.frameTimes.length > SAMPLE_SIZE) {
|
|
4564
4668
|
this.frameTimes.shift();
|
|
4565
4669
|
}
|
|
4566
|
-
if (
|
|
4670
|
+
if (breakdown !== void 0) {
|
|
4671
|
+
if (breakdown.gridMs !== void 0) this._lastGridMs = breakdown.gridMs;
|
|
4672
|
+
if (breakdown.layersMs !== void 0) this._lastLayersMs = breakdown.layersMs;
|
|
4673
|
+
if (breakdown.backgroundMs !== void 0) this._lastBackgroundMs = breakdown.backgroundMs;
|
|
4674
|
+
if (breakdown.compositeMs !== void 0) this._lastCompositeMs = breakdown.compositeMs;
|
|
4675
|
+
if (breakdown.overlayMs !== void 0) this._lastOverlayMs = breakdown.overlayMs;
|
|
4676
|
+
}
|
|
4567
4677
|
}
|
|
4568
4678
|
getSnapshot() {
|
|
4569
4679
|
const times = this.frameTimes;
|
|
4570
4680
|
if (times.length === 0) {
|
|
4571
|
-
return {
|
|
4681
|
+
return {
|
|
4682
|
+
fps: 0,
|
|
4683
|
+
avgFrameMs: 0,
|
|
4684
|
+
p95FrameMs: 0,
|
|
4685
|
+
lastFrameMs: 0,
|
|
4686
|
+
lastGridMs: 0,
|
|
4687
|
+
layersMs: 0,
|
|
4688
|
+
backgroundMs: 0,
|
|
4689
|
+
compositeMs: 0,
|
|
4690
|
+
overlayMs: 0,
|
|
4691
|
+
frameCount: 0
|
|
4692
|
+
};
|
|
4572
4693
|
}
|
|
4573
4694
|
const avg = times.reduce((a, b) => a + b, 0) / times.length;
|
|
4574
4695
|
const sorted = [...times].sort((a, b) => a - b);
|
|
@@ -4580,6 +4701,10 @@ var RenderStats = class {
|
|
|
4580
4701
|
p95FrameMs: Math.round((sorted[p95Index] ?? 0) * 100) / 100,
|
|
4581
4702
|
lastFrameMs: Math.round(lastFrame * 100) / 100,
|
|
4582
4703
|
lastGridMs: Math.round(this._lastGridMs * 100) / 100,
|
|
4704
|
+
layersMs: Math.round(this._lastLayersMs * 100) / 100,
|
|
4705
|
+
backgroundMs: Math.round(this._lastBackgroundMs * 100) / 100,
|
|
4706
|
+
compositeMs: Math.round(this._lastCompositeMs * 100) / 100,
|
|
4707
|
+
overlayMs: Math.round(this._lastOverlayMs * 100) / 100,
|
|
4583
4708
|
frameCount: this.frameCount
|
|
4584
4709
|
};
|
|
4585
4710
|
}
|
|
@@ -4607,7 +4732,7 @@ var RenderLoop = class {
|
|
|
4607
4732
|
lastCamX;
|
|
4608
4733
|
lastCamY;
|
|
4609
4734
|
stats = new RenderStats();
|
|
4610
|
-
|
|
4735
|
+
layerGroups = /* @__PURE__ */ new Map();
|
|
4611
4736
|
gridCacheCanvas = null;
|
|
4612
4737
|
gridCacheCtx = null;
|
|
4613
4738
|
gridCacheZoom = -1;
|
|
@@ -4705,6 +4830,9 @@ var RenderLoop = class {
|
|
|
4705
4830
|
const t0 = performance.now();
|
|
4706
4831
|
const ctx = this.canvasEl.getContext("2d");
|
|
4707
4832
|
if (!ctx) return;
|
|
4833
|
+
let layersMs = 0;
|
|
4834
|
+
let compositeMs = 0;
|
|
4835
|
+
let gridMs = 0;
|
|
4708
4836
|
const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
|
|
4709
4837
|
const cssWidth = this.canvasEl.clientWidth;
|
|
4710
4838
|
const cssHeight = this.canvasEl.clientHeight;
|
|
@@ -4721,6 +4849,7 @@ var RenderLoop = class {
|
|
|
4721
4849
|
ctx.scale(dpr, dpr);
|
|
4722
4850
|
this.renderer.setCanvasSize(cssWidth, cssHeight);
|
|
4723
4851
|
const hasGridElement = this.store.getElementsByType("grid").length > 0;
|
|
4852
|
+
const bgT0 = performance.now();
|
|
4724
4853
|
if (hasGridElement) {
|
|
4725
4854
|
ctx.save();
|
|
4726
4855
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
@@ -4729,6 +4858,7 @@ var RenderLoop = class {
|
|
|
4729
4858
|
} else {
|
|
4730
4859
|
this.background.render(ctx, this.camera);
|
|
4731
4860
|
}
|
|
4861
|
+
const backgroundMs = performance.now() - bgT0;
|
|
4732
4862
|
ctx.save();
|
|
4733
4863
|
ctx.translate(this.camera.position.x, this.camera.position.y);
|
|
4734
4864
|
ctx.scale(this.camera.zoom, this.camera.zoom);
|
|
@@ -4741,7 +4871,7 @@ var RenderLoop = class {
|
|
|
4741
4871
|
h: visibleRect.h + margin * 2
|
|
4742
4872
|
};
|
|
4743
4873
|
const allElements = this.store.getAll();
|
|
4744
|
-
|
|
4874
|
+
this.layerGroups.clear();
|
|
4745
4875
|
const gridElements = [];
|
|
4746
4876
|
let domZIndex = 0;
|
|
4747
4877
|
for (const element of allElements) {
|
|
@@ -4764,25 +4894,30 @@ var RenderLoop = class {
|
|
|
4764
4894
|
gridElements.push(element);
|
|
4765
4895
|
continue;
|
|
4766
4896
|
}
|
|
4767
|
-
let group =
|
|
4897
|
+
let group = this.layerGroups.get(element.layerId);
|
|
4768
4898
|
if (!group) {
|
|
4769
4899
|
group = [];
|
|
4770
|
-
|
|
4900
|
+
this.layerGroups.set(element.layerId, group);
|
|
4771
4901
|
}
|
|
4772
4902
|
group.push(element);
|
|
4773
4903
|
}
|
|
4774
|
-
for (const [layerId, elements] of
|
|
4904
|
+
for (const [layerId, elements] of this.layerGroups) {
|
|
4775
4905
|
const isActiveDrawingLayer = layerId === this.activeDrawingLayerId;
|
|
4776
4906
|
if (!this.layerCache.isDirty(layerId)) {
|
|
4907
|
+
const compT0 = performance.now();
|
|
4777
4908
|
this.compositeLayerCache(ctx, layerId, dpr);
|
|
4909
|
+
compositeMs += performance.now() - compT0;
|
|
4778
4910
|
continue;
|
|
4779
4911
|
}
|
|
4780
4912
|
if (isActiveDrawingLayer) {
|
|
4913
|
+
const compT0 = performance.now();
|
|
4781
4914
|
this.compositeLayerCache(ctx, layerId, dpr);
|
|
4915
|
+
compositeMs += performance.now() - compT0;
|
|
4782
4916
|
continue;
|
|
4783
4917
|
}
|
|
4784
4918
|
const offCtx = this.layerCache.getContext(layerId);
|
|
4785
4919
|
if (offCtx) {
|
|
4920
|
+
const layerT0 = performance.now();
|
|
4786
4921
|
const offCanvas = this.layerCache.getCanvas(layerId);
|
|
4787
4922
|
offCtx.clearRect(0, 0, offCanvas.width, offCanvas.height);
|
|
4788
4923
|
offCtx.save();
|
|
@@ -4796,7 +4931,10 @@ var RenderLoop = class {
|
|
|
4796
4931
|
}
|
|
4797
4932
|
offCtx.restore();
|
|
4798
4933
|
this.layerCache.markClean(layerId);
|
|
4934
|
+
layersMs += performance.now() - layerT0;
|
|
4935
|
+
const compT0 = performance.now();
|
|
4799
4936
|
this.compositeLayerCache(ctx, layerId, dpr);
|
|
4937
|
+
compositeMs += performance.now() - compT0;
|
|
4800
4938
|
}
|
|
4801
4939
|
}
|
|
4802
4940
|
if (gridElements.length > 0) {
|
|
@@ -4837,15 +4975,23 @@ var RenderLoop = class {
|
|
|
4837
4975
|
this.gridCacheHeight = cssHeight;
|
|
4838
4976
|
this.lastGridRef = gridRef;
|
|
4839
4977
|
}
|
|
4840
|
-
|
|
4978
|
+
gridMs = performance.now() - gridT0;
|
|
4841
4979
|
}
|
|
4980
|
+
const overlayT0 = performance.now();
|
|
4842
4981
|
const activeTool = this.toolManager.activeTool;
|
|
4843
4982
|
if (activeTool?.renderOverlay) {
|
|
4844
4983
|
activeTool.renderOverlay(ctx);
|
|
4845
4984
|
}
|
|
4985
|
+
const overlayMs = performance.now() - overlayT0;
|
|
4846
4986
|
ctx.restore();
|
|
4847
4987
|
ctx.restore();
|
|
4848
|
-
this.stats.recordFrame(performance.now() - t0,
|
|
4988
|
+
this.stats.recordFrame(performance.now() - t0, {
|
|
4989
|
+
gridMs,
|
|
4990
|
+
layersMs,
|
|
4991
|
+
backgroundMs,
|
|
4992
|
+
compositeMs,
|
|
4993
|
+
overlayMs
|
|
4994
|
+
});
|
|
4849
4995
|
}
|
|
4850
4996
|
};
|
|
4851
4997
|
|
|
@@ -4929,6 +5075,17 @@ var Viewport = class {
|
|
|
4929
5075
|
this.renderLoop.markAllLayersDirty();
|
|
4930
5076
|
this.requestRender();
|
|
4931
5077
|
});
|
|
5078
|
+
this.renderer.setOnImageError((src) => {
|
|
5079
|
+
const elementIds = [];
|
|
5080
|
+
for (const el of this.store.getAll()) {
|
|
5081
|
+
if (el.type === "image" && el.src === src) elementIds.push(el.id);
|
|
5082
|
+
}
|
|
5083
|
+
if (options.onImageError) {
|
|
5084
|
+
options.onImageError({ src, elementIds });
|
|
5085
|
+
} else {
|
|
5086
|
+
console.warn(`[fieldnotes] image failed to load: ${src}`);
|
|
5087
|
+
}
|
|
5088
|
+
});
|
|
4932
5089
|
this.noteEditor = new NoteEditor({
|
|
4933
5090
|
fontSizePresets: options.fontSizePresets,
|
|
4934
5091
|
toolbar: options.toolbar,
|
|
@@ -5246,7 +5403,7 @@ var Viewport = class {
|
|
|
5246
5403
|
const id = setInterval(() => {
|
|
5247
5404
|
const s = this.getRenderStats();
|
|
5248
5405
|
console.log(
|
|
5249
|
-
`[FieldNotes] fps=${s.fps} frame=${s.avgFrameMs}ms p95=${s.p95FrameMs}ms grid=${s.lastGridMs}ms`
|
|
5406
|
+
`[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`
|
|
5250
5407
|
);
|
|
5251
5408
|
}, intervalMs);
|
|
5252
5409
|
return () => clearInterval(id);
|
|
@@ -5633,6 +5790,43 @@ var PencilTool = class {
|
|
|
5633
5790
|
}
|
|
5634
5791
|
};
|
|
5635
5792
|
|
|
5793
|
+
// src/elements/stroke-hit.ts
|
|
5794
|
+
function distSqToSegment(p, a, b) {
|
|
5795
|
+
const abx = b.x - a.x;
|
|
5796
|
+
const aby = b.y - a.y;
|
|
5797
|
+
const apx = p.x - a.x;
|
|
5798
|
+
const apy = p.y - a.y;
|
|
5799
|
+
const lenSq = abx * abx + aby * aby;
|
|
5800
|
+
if (lenSq === 0) {
|
|
5801
|
+
return apx * apx + apy * apy;
|
|
5802
|
+
}
|
|
5803
|
+
const t = Math.max(0, Math.min(1, (apx * abx + apy * aby) / lenSq));
|
|
5804
|
+
const dx = p.x - (a.x + t * abx);
|
|
5805
|
+
const dy = p.y - (a.y + t * aby);
|
|
5806
|
+
return dx * dx + dy * dy;
|
|
5807
|
+
}
|
|
5808
|
+
function hitTestStroke(stroke, point, radius) {
|
|
5809
|
+
const bounds = getElementBounds(stroke);
|
|
5810
|
+
if (!bounds) return false;
|
|
5811
|
+
if (point.x < bounds.x - radius || point.x > bounds.x + bounds.w + radius || point.y < bounds.y - radius || point.y > bounds.y + bounds.h + radius) {
|
|
5812
|
+
return false;
|
|
5813
|
+
}
|
|
5814
|
+
const radiusSq = radius * radius;
|
|
5815
|
+
const local = { x: point.x - stroke.position.x, y: point.y - stroke.position.y };
|
|
5816
|
+
const { segments } = getStrokeRenderData(stroke);
|
|
5817
|
+
if (segments.length === 0) {
|
|
5818
|
+
const p = stroke.points[0];
|
|
5819
|
+
if (!p) return false;
|
|
5820
|
+
const dx = p.x - local.x;
|
|
5821
|
+
const dy = p.y - local.y;
|
|
5822
|
+
return dx * dx + dy * dy <= radiusSq;
|
|
5823
|
+
}
|
|
5824
|
+
for (const seg of segments) {
|
|
5825
|
+
if (distSqToSegment(local, seg.start, seg.end) <= radiusSq) return true;
|
|
5826
|
+
}
|
|
5827
|
+
return false;
|
|
5828
|
+
}
|
|
5829
|
+
|
|
5636
5830
|
// src/tools/eraser-tool.ts
|
|
5637
5831
|
var DEFAULT_RADIUS = 20;
|
|
5638
5832
|
function makeEraserCursor(radius) {
|
|
@@ -5691,12 +5885,7 @@ var EraserTool = class {
|
|
|
5691
5885
|
if (erased) ctx.requestRender();
|
|
5692
5886
|
}
|
|
5693
5887
|
strokeIntersects(stroke, point) {
|
|
5694
|
-
|
|
5695
|
-
return stroke.points.some((p) => {
|
|
5696
|
-
const dx = p.x + stroke.position.x - point.x;
|
|
5697
|
-
const dy = p.y + stroke.position.y - point.y;
|
|
5698
|
-
return dx * dx + dy * dy <= radiusSq;
|
|
5699
|
-
});
|
|
5888
|
+
return hitTestStroke(stroke, point, this.radius);
|
|
5700
5889
|
}
|
|
5701
5890
|
};
|
|
5702
5891
|
|
|
@@ -6376,12 +6565,7 @@ var SelectTool = class {
|
|
|
6376
6565
|
return point.x >= el.position.x && point.x <= el.position.x + s.w && point.y >= el.position.y && point.y <= el.position.y + s.h;
|
|
6377
6566
|
}
|
|
6378
6567
|
if (el.type === "stroke") {
|
|
6379
|
-
|
|
6380
|
-
return el.points.some((p) => {
|
|
6381
|
-
const dx = p.x + el.position.x - point.x;
|
|
6382
|
-
const dy = p.y + el.position.y - point.y;
|
|
6383
|
-
return dx * dx + dy * dy <= HIT_RADIUS * HIT_RADIUS;
|
|
6384
|
-
});
|
|
6568
|
+
return hitTestStroke(el, point, 10);
|
|
6385
6569
|
}
|
|
6386
6570
|
if (el.type === "arrow") {
|
|
6387
6571
|
return isNearBezier(point, el.from, el.to, el.bend, 10);
|
|
@@ -7217,7 +7401,7 @@ var TemplateTool = class {
|
|
|
7217
7401
|
};
|
|
7218
7402
|
|
|
7219
7403
|
// src/index.ts
|
|
7220
|
-
var VERSION = "0.
|
|
7404
|
+
var VERSION = "0.22.0";
|
|
7221
7405
|
export {
|
|
7222
7406
|
AddElementCommand,
|
|
7223
7407
|
ArrowTool,
|