@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.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: [] };
|
|
@@ -2719,6 +2775,7 @@ var ElementRenderer = class {
|
|
|
2719
2775
|
store = null;
|
|
2720
2776
|
imageCache = /* @__PURE__ */ new Map();
|
|
2721
2777
|
onImageLoad = null;
|
|
2778
|
+
onImageError = null;
|
|
2722
2779
|
camera = null;
|
|
2723
2780
|
canvasSize = null;
|
|
2724
2781
|
hexTileCache = null;
|
|
@@ -2729,6 +2786,9 @@ var ElementRenderer = class {
|
|
|
2729
2786
|
setOnImageLoad(callback) {
|
|
2730
2787
|
this.onImageLoad = callback;
|
|
2731
2788
|
}
|
|
2789
|
+
setOnImageError(callback) {
|
|
2790
|
+
this.onImageError = callback;
|
|
2791
|
+
}
|
|
2732
2792
|
setCamera(camera) {
|
|
2733
2793
|
this.camera = camera;
|
|
2734
2794
|
}
|
|
@@ -2768,21 +2828,29 @@ var ElementRenderer = class {
|
|
|
2768
2828
|
ctx.lineCap = "round";
|
|
2769
2829
|
ctx.lineJoin = "round";
|
|
2770
2830
|
ctx.globalAlpha = stroke.opacity;
|
|
2771
|
-
const
|
|
2772
|
-
|
|
2773
|
-
const
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2831
|
+
const data = getStrokeRenderData(stroke);
|
|
2832
|
+
if (data.buckets) {
|
|
2833
|
+
for (const bucket of data.buckets) {
|
|
2834
|
+
ctx.lineWidth = bucket.width;
|
|
2835
|
+
ctx.stroke(bucket.path);
|
|
2836
|
+
}
|
|
2837
|
+
} else {
|
|
2838
|
+
for (let i = 0; i < data.segments.length; i++) {
|
|
2839
|
+
const seg = data.segments[i];
|
|
2840
|
+
const w = data.widths[i];
|
|
2841
|
+
if (!seg || w === void 0) continue;
|
|
2842
|
+
ctx.lineWidth = w;
|
|
2843
|
+
ctx.beginPath();
|
|
2844
|
+
ctx.moveTo(seg.start.x, seg.start.y);
|
|
2845
|
+
ctx.bezierCurveTo(seg.cp1.x, seg.cp1.y, seg.cp2.x, seg.cp2.y, seg.end.x, seg.end.y);
|
|
2846
|
+
ctx.stroke();
|
|
2847
|
+
}
|
|
2781
2848
|
}
|
|
2782
2849
|
ctx.restore();
|
|
2783
2850
|
}
|
|
2784
2851
|
renderArrow(ctx, arrow) {
|
|
2785
|
-
const
|
|
2852
|
+
const geometry = getArrowRenderGeometry(arrow);
|
|
2853
|
+
const { visualFrom, visualTo } = this.getVisualEndpoints(arrow, geometry);
|
|
2786
2854
|
ctx.save();
|
|
2787
2855
|
ctx.strokeStyle = arrow.color;
|
|
2788
2856
|
ctx.lineWidth = arrow.width;
|
|
@@ -2793,17 +2861,18 @@ var ElementRenderer = class {
|
|
|
2793
2861
|
ctx.beginPath();
|
|
2794
2862
|
ctx.moveTo(visualFrom.x, visualFrom.y);
|
|
2795
2863
|
if (arrow.bend !== 0) {
|
|
2796
|
-
const cp =
|
|
2797
|
-
|
|
2864
|
+
const cp = geometry.controlPoint;
|
|
2865
|
+
if (cp) {
|
|
2866
|
+
ctx.quadraticCurveTo(cp.x, cp.y, visualTo.x, visualTo.y);
|
|
2867
|
+
}
|
|
2798
2868
|
} else {
|
|
2799
2869
|
ctx.lineTo(visualTo.x, visualTo.y);
|
|
2800
2870
|
}
|
|
2801
2871
|
ctx.stroke();
|
|
2802
|
-
this.renderArrowhead(ctx, arrow, visualTo);
|
|
2872
|
+
this.renderArrowhead(ctx, arrow, visualTo, geometry.tangentEnd);
|
|
2803
2873
|
ctx.restore();
|
|
2804
2874
|
}
|
|
2805
|
-
renderArrowhead(ctx, arrow, tip) {
|
|
2806
|
-
const angle = getArrowTangentAngle(arrow.from, arrow.to, arrow.bend, 1);
|
|
2875
|
+
renderArrowhead(ctx, arrow, tip, angle) {
|
|
2807
2876
|
ctx.beginPath();
|
|
2808
2877
|
ctx.moveTo(tip.x, tip.y);
|
|
2809
2878
|
ctx.lineTo(
|
|
@@ -2818,7 +2887,7 @@ var ElementRenderer = class {
|
|
|
2818
2887
|
ctx.fillStyle = arrow.color;
|
|
2819
2888
|
ctx.fill();
|
|
2820
2889
|
}
|
|
2821
|
-
getVisualEndpoints(arrow) {
|
|
2890
|
+
getVisualEndpoints(arrow, geometry) {
|
|
2822
2891
|
let visualFrom = arrow.from;
|
|
2823
2892
|
let visualTo = arrow.to;
|
|
2824
2893
|
if (!this.store) return { visualFrom, visualTo };
|
|
@@ -2827,7 +2896,7 @@ var ElementRenderer = class {
|
|
|
2827
2896
|
if (el) {
|
|
2828
2897
|
const bounds = getElementBounds(el);
|
|
2829
2898
|
if (bounds) {
|
|
2830
|
-
const tangentAngle =
|
|
2899
|
+
const tangentAngle = geometry.tangentStart;
|
|
2831
2900
|
const rayTarget = {
|
|
2832
2901
|
x: arrow.from.x + Math.cos(tangentAngle) * 1e3,
|
|
2833
2902
|
y: arrow.from.y + Math.sin(tangentAngle) * 1e3
|
|
@@ -2841,7 +2910,7 @@ var ElementRenderer = class {
|
|
|
2841
2910
|
if (el) {
|
|
2842
2911
|
const bounds = getElementBounds(el);
|
|
2843
2912
|
if (bounds) {
|
|
2844
|
-
const tangentAngle =
|
|
2913
|
+
const tangentAngle = geometry.tangentEnd;
|
|
2845
2914
|
const rayTarget = {
|
|
2846
2915
|
x: arrow.to.x - Math.cos(tangentAngle) * 1e3,
|
|
2847
2916
|
y: arrow.to.y - Math.sin(tangentAngle) * 1e3
|
|
@@ -3087,6 +3156,10 @@ var ElementRenderer = class {
|
|
|
3087
3156
|
ctx.restore();
|
|
3088
3157
|
}
|
|
3089
3158
|
renderImage(ctx, image) {
|
|
3159
|
+
if (this.imageCache.get(image.src) === "failed") {
|
|
3160
|
+
this.renderImagePlaceholder(ctx, image);
|
|
3161
|
+
return;
|
|
3162
|
+
}
|
|
3090
3163
|
const img = this.getImage(image.src);
|
|
3091
3164
|
if (!img) return;
|
|
3092
3165
|
ctx.drawImage(
|
|
@@ -3097,6 +3170,27 @@ var ElementRenderer = class {
|
|
|
3097
3170
|
image.size.h
|
|
3098
3171
|
);
|
|
3099
3172
|
}
|
|
3173
|
+
renderImagePlaceholder(ctx, image) {
|
|
3174
|
+
const { x, y } = image.position;
|
|
3175
|
+
const { w, h } = image.size;
|
|
3176
|
+
ctx.save();
|
|
3177
|
+
ctx.fillStyle = "#eeeeee";
|
|
3178
|
+
ctx.fillRect(x, y, w, h);
|
|
3179
|
+
ctx.strokeStyle = "#bdbdbd";
|
|
3180
|
+
ctx.lineWidth = 1;
|
|
3181
|
+
ctx.strokeRect(x, y, w, h);
|
|
3182
|
+
const glyph = Math.min(24, w / 2, h / 2);
|
|
3183
|
+
const cx = x + w / 2;
|
|
3184
|
+
const cy = y + h / 2;
|
|
3185
|
+
ctx.strokeStyle = "#9e9e9e";
|
|
3186
|
+
ctx.lineWidth = 2;
|
|
3187
|
+
ctx.beginPath();
|
|
3188
|
+
ctx.arc(cx, cy, glyph / 2, 0, Math.PI * 2);
|
|
3189
|
+
ctx.moveTo(cx - glyph / 2, cy + glyph / 2);
|
|
3190
|
+
ctx.lineTo(cx + glyph / 2, cy - glyph / 2);
|
|
3191
|
+
ctx.stroke();
|
|
3192
|
+
ctx.restore();
|
|
3193
|
+
}
|
|
3100
3194
|
getHexTile(cellSize, orientation, strokeColor, strokeWidth, opacity, scale) {
|
|
3101
3195
|
const key = `${cellSize}:${orientation}:${strokeColor}:${strokeWidth}:${opacity}:${scale}`;
|
|
3102
3196
|
if (this.hexTileCacheKey === key && this.hexTileCache) {
|
|
@@ -3112,6 +3206,7 @@ var ElementRenderer = class {
|
|
|
3112
3206
|
getImage(src) {
|
|
3113
3207
|
const cached = this.imageCache.get(src);
|
|
3114
3208
|
if (cached) {
|
|
3209
|
+
if (cached === "failed") return null;
|
|
3115
3210
|
if (cached instanceof HTMLImageElement) return cached.complete ? cached : null;
|
|
3116
3211
|
return cached;
|
|
3117
3212
|
}
|
|
@@ -3128,6 +3223,11 @@ var ElementRenderer = class {
|
|
|
3128
3223
|
});
|
|
3129
3224
|
}
|
|
3130
3225
|
};
|
|
3226
|
+
img.onerror = () => {
|
|
3227
|
+
this.imageCache.set(src, "failed");
|
|
3228
|
+
this.onImageError?.(src);
|
|
3229
|
+
this.onImageLoad?.();
|
|
3230
|
+
};
|
|
3131
3231
|
return null;
|
|
3132
3232
|
}
|
|
3133
3233
|
};
|
|
@@ -4174,19 +4274,19 @@ function loadImages(elements) {
|
|
|
4174
4274
|
const imageElements = elements.filter(
|
|
4175
4275
|
(el) => el.type === "image" && "src" in el
|
|
4176
4276
|
);
|
|
4177
|
-
const
|
|
4178
|
-
if (imageElements.length === 0) return Promise.resolve(
|
|
4277
|
+
const cache3 = /* @__PURE__ */ new Map();
|
|
4278
|
+
if (imageElements.length === 0) return Promise.resolve(cache3);
|
|
4179
4279
|
return new Promise((resolve) => {
|
|
4180
4280
|
let remaining = imageElements.length;
|
|
4181
4281
|
const done = () => {
|
|
4182
4282
|
remaining--;
|
|
4183
|
-
if (remaining <= 0) resolve(
|
|
4283
|
+
if (remaining <= 0) resolve(cache3);
|
|
4184
4284
|
};
|
|
4185
4285
|
for (const el of imageElements) {
|
|
4186
4286
|
const img = new Image();
|
|
4187
4287
|
img.crossOrigin = "anonymous";
|
|
4188
4288
|
img.onload = () => {
|
|
4189
|
-
|
|
4289
|
+
cache3.set(el.id, img);
|
|
4190
4290
|
done();
|
|
4191
4291
|
};
|
|
4192
4292
|
img.onerror = done;
|
|
@@ -4667,18 +4767,39 @@ var RenderStats = class {
|
|
|
4667
4767
|
frameTimes = [];
|
|
4668
4768
|
frameCount = 0;
|
|
4669
4769
|
_lastGridMs = 0;
|
|
4670
|
-
|
|
4770
|
+
_lastLayersMs = 0;
|
|
4771
|
+
_lastBackgroundMs = 0;
|
|
4772
|
+
_lastCompositeMs = 0;
|
|
4773
|
+
_lastOverlayMs = 0;
|
|
4774
|
+
recordFrame(durationMs, breakdown) {
|
|
4671
4775
|
this.frameCount++;
|
|
4672
4776
|
this.frameTimes.push(durationMs);
|
|
4673
4777
|
if (this.frameTimes.length > SAMPLE_SIZE) {
|
|
4674
4778
|
this.frameTimes.shift();
|
|
4675
4779
|
}
|
|
4676
|
-
if (
|
|
4780
|
+
if (breakdown !== void 0) {
|
|
4781
|
+
if (breakdown.gridMs !== void 0) this._lastGridMs = breakdown.gridMs;
|
|
4782
|
+
if (breakdown.layersMs !== void 0) this._lastLayersMs = breakdown.layersMs;
|
|
4783
|
+
if (breakdown.backgroundMs !== void 0) this._lastBackgroundMs = breakdown.backgroundMs;
|
|
4784
|
+
if (breakdown.compositeMs !== void 0) this._lastCompositeMs = breakdown.compositeMs;
|
|
4785
|
+
if (breakdown.overlayMs !== void 0) this._lastOverlayMs = breakdown.overlayMs;
|
|
4786
|
+
}
|
|
4677
4787
|
}
|
|
4678
4788
|
getSnapshot() {
|
|
4679
4789
|
const times = this.frameTimes;
|
|
4680
4790
|
if (times.length === 0) {
|
|
4681
|
-
return {
|
|
4791
|
+
return {
|
|
4792
|
+
fps: 0,
|
|
4793
|
+
avgFrameMs: 0,
|
|
4794
|
+
p95FrameMs: 0,
|
|
4795
|
+
lastFrameMs: 0,
|
|
4796
|
+
lastGridMs: 0,
|
|
4797
|
+
layersMs: 0,
|
|
4798
|
+
backgroundMs: 0,
|
|
4799
|
+
compositeMs: 0,
|
|
4800
|
+
overlayMs: 0,
|
|
4801
|
+
frameCount: 0
|
|
4802
|
+
};
|
|
4682
4803
|
}
|
|
4683
4804
|
const avg = times.reduce((a, b) => a + b, 0) / times.length;
|
|
4684
4805
|
const sorted = [...times].sort((a, b) => a - b);
|
|
@@ -4690,6 +4811,10 @@ var RenderStats = class {
|
|
|
4690
4811
|
p95FrameMs: Math.round((sorted[p95Index] ?? 0) * 100) / 100,
|
|
4691
4812
|
lastFrameMs: Math.round(lastFrame * 100) / 100,
|
|
4692
4813
|
lastGridMs: Math.round(this._lastGridMs * 100) / 100,
|
|
4814
|
+
layersMs: Math.round(this._lastLayersMs * 100) / 100,
|
|
4815
|
+
backgroundMs: Math.round(this._lastBackgroundMs * 100) / 100,
|
|
4816
|
+
compositeMs: Math.round(this._lastCompositeMs * 100) / 100,
|
|
4817
|
+
overlayMs: Math.round(this._lastOverlayMs * 100) / 100,
|
|
4693
4818
|
frameCount: this.frameCount
|
|
4694
4819
|
};
|
|
4695
4820
|
}
|
|
@@ -4717,7 +4842,7 @@ var RenderLoop = class {
|
|
|
4717
4842
|
lastCamX;
|
|
4718
4843
|
lastCamY;
|
|
4719
4844
|
stats = new RenderStats();
|
|
4720
|
-
|
|
4845
|
+
layerGroups = /* @__PURE__ */ new Map();
|
|
4721
4846
|
gridCacheCanvas = null;
|
|
4722
4847
|
gridCacheCtx = null;
|
|
4723
4848
|
gridCacheZoom = -1;
|
|
@@ -4815,6 +4940,9 @@ var RenderLoop = class {
|
|
|
4815
4940
|
const t0 = performance.now();
|
|
4816
4941
|
const ctx = this.canvasEl.getContext("2d");
|
|
4817
4942
|
if (!ctx) return;
|
|
4943
|
+
let layersMs = 0;
|
|
4944
|
+
let compositeMs = 0;
|
|
4945
|
+
let gridMs = 0;
|
|
4818
4946
|
const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
|
|
4819
4947
|
const cssWidth = this.canvasEl.clientWidth;
|
|
4820
4948
|
const cssHeight = this.canvasEl.clientHeight;
|
|
@@ -4831,6 +4959,7 @@ var RenderLoop = class {
|
|
|
4831
4959
|
ctx.scale(dpr, dpr);
|
|
4832
4960
|
this.renderer.setCanvasSize(cssWidth, cssHeight);
|
|
4833
4961
|
const hasGridElement = this.store.getElementsByType("grid").length > 0;
|
|
4962
|
+
const bgT0 = performance.now();
|
|
4834
4963
|
if (hasGridElement) {
|
|
4835
4964
|
ctx.save();
|
|
4836
4965
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
@@ -4839,6 +4968,7 @@ var RenderLoop = class {
|
|
|
4839
4968
|
} else {
|
|
4840
4969
|
this.background.render(ctx, this.camera);
|
|
4841
4970
|
}
|
|
4971
|
+
const backgroundMs = performance.now() - bgT0;
|
|
4842
4972
|
ctx.save();
|
|
4843
4973
|
ctx.translate(this.camera.position.x, this.camera.position.y);
|
|
4844
4974
|
ctx.scale(this.camera.zoom, this.camera.zoom);
|
|
@@ -4851,7 +4981,7 @@ var RenderLoop = class {
|
|
|
4851
4981
|
h: visibleRect.h + margin * 2
|
|
4852
4982
|
};
|
|
4853
4983
|
const allElements = this.store.getAll();
|
|
4854
|
-
|
|
4984
|
+
this.layerGroups.clear();
|
|
4855
4985
|
const gridElements = [];
|
|
4856
4986
|
let domZIndex = 0;
|
|
4857
4987
|
for (const element of allElements) {
|
|
@@ -4874,25 +5004,30 @@ var RenderLoop = class {
|
|
|
4874
5004
|
gridElements.push(element);
|
|
4875
5005
|
continue;
|
|
4876
5006
|
}
|
|
4877
|
-
let group =
|
|
5007
|
+
let group = this.layerGroups.get(element.layerId);
|
|
4878
5008
|
if (!group) {
|
|
4879
5009
|
group = [];
|
|
4880
|
-
|
|
5010
|
+
this.layerGroups.set(element.layerId, group);
|
|
4881
5011
|
}
|
|
4882
5012
|
group.push(element);
|
|
4883
5013
|
}
|
|
4884
|
-
for (const [layerId, elements] of
|
|
5014
|
+
for (const [layerId, elements] of this.layerGroups) {
|
|
4885
5015
|
const isActiveDrawingLayer = layerId === this.activeDrawingLayerId;
|
|
4886
5016
|
if (!this.layerCache.isDirty(layerId)) {
|
|
5017
|
+
const compT0 = performance.now();
|
|
4887
5018
|
this.compositeLayerCache(ctx, layerId, dpr);
|
|
5019
|
+
compositeMs += performance.now() - compT0;
|
|
4888
5020
|
continue;
|
|
4889
5021
|
}
|
|
4890
5022
|
if (isActiveDrawingLayer) {
|
|
5023
|
+
const compT0 = performance.now();
|
|
4891
5024
|
this.compositeLayerCache(ctx, layerId, dpr);
|
|
5025
|
+
compositeMs += performance.now() - compT0;
|
|
4892
5026
|
continue;
|
|
4893
5027
|
}
|
|
4894
5028
|
const offCtx = this.layerCache.getContext(layerId);
|
|
4895
5029
|
if (offCtx) {
|
|
5030
|
+
const layerT0 = performance.now();
|
|
4896
5031
|
const offCanvas = this.layerCache.getCanvas(layerId);
|
|
4897
5032
|
offCtx.clearRect(0, 0, offCanvas.width, offCanvas.height);
|
|
4898
5033
|
offCtx.save();
|
|
@@ -4906,7 +5041,10 @@ var RenderLoop = class {
|
|
|
4906
5041
|
}
|
|
4907
5042
|
offCtx.restore();
|
|
4908
5043
|
this.layerCache.markClean(layerId);
|
|
5044
|
+
layersMs += performance.now() - layerT0;
|
|
5045
|
+
const compT0 = performance.now();
|
|
4909
5046
|
this.compositeLayerCache(ctx, layerId, dpr);
|
|
5047
|
+
compositeMs += performance.now() - compT0;
|
|
4910
5048
|
}
|
|
4911
5049
|
}
|
|
4912
5050
|
if (gridElements.length > 0) {
|
|
@@ -4947,15 +5085,23 @@ var RenderLoop = class {
|
|
|
4947
5085
|
this.gridCacheHeight = cssHeight;
|
|
4948
5086
|
this.lastGridRef = gridRef;
|
|
4949
5087
|
}
|
|
4950
|
-
|
|
5088
|
+
gridMs = performance.now() - gridT0;
|
|
4951
5089
|
}
|
|
5090
|
+
const overlayT0 = performance.now();
|
|
4952
5091
|
const activeTool = this.toolManager.activeTool;
|
|
4953
5092
|
if (activeTool?.renderOverlay) {
|
|
4954
5093
|
activeTool.renderOverlay(ctx);
|
|
4955
5094
|
}
|
|
5095
|
+
const overlayMs = performance.now() - overlayT0;
|
|
4956
5096
|
ctx.restore();
|
|
4957
5097
|
ctx.restore();
|
|
4958
|
-
this.stats.recordFrame(performance.now() - t0,
|
|
5098
|
+
this.stats.recordFrame(performance.now() - t0, {
|
|
5099
|
+
gridMs,
|
|
5100
|
+
layersMs,
|
|
5101
|
+
backgroundMs,
|
|
5102
|
+
compositeMs,
|
|
5103
|
+
overlayMs
|
|
5104
|
+
});
|
|
4959
5105
|
}
|
|
4960
5106
|
};
|
|
4961
5107
|
|
|
@@ -5039,6 +5185,17 @@ var Viewport = class {
|
|
|
5039
5185
|
this.renderLoop.markAllLayersDirty();
|
|
5040
5186
|
this.requestRender();
|
|
5041
5187
|
});
|
|
5188
|
+
this.renderer.setOnImageError((src) => {
|
|
5189
|
+
const elementIds = [];
|
|
5190
|
+
for (const el of this.store.getAll()) {
|
|
5191
|
+
if (el.type === "image" && el.src === src) elementIds.push(el.id);
|
|
5192
|
+
}
|
|
5193
|
+
if (options.onImageError) {
|
|
5194
|
+
options.onImageError({ src, elementIds });
|
|
5195
|
+
} else {
|
|
5196
|
+
console.warn(`[fieldnotes] image failed to load: ${src}`);
|
|
5197
|
+
}
|
|
5198
|
+
});
|
|
5042
5199
|
this.noteEditor = new NoteEditor({
|
|
5043
5200
|
fontSizePresets: options.fontSizePresets,
|
|
5044
5201
|
toolbar: options.toolbar,
|
|
@@ -5356,7 +5513,7 @@ var Viewport = class {
|
|
|
5356
5513
|
const id = setInterval(() => {
|
|
5357
5514
|
const s = this.getRenderStats();
|
|
5358
5515
|
console.log(
|
|
5359
|
-
`[FieldNotes] fps=${s.fps} frame=${s.avgFrameMs}ms p95=${s.p95FrameMs}ms grid=${s.lastGridMs}ms`
|
|
5516
|
+
`[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`
|
|
5360
5517
|
);
|
|
5361
5518
|
}, intervalMs);
|
|
5362
5519
|
return () => clearInterval(id);
|
|
@@ -5743,6 +5900,43 @@ var PencilTool = class {
|
|
|
5743
5900
|
}
|
|
5744
5901
|
};
|
|
5745
5902
|
|
|
5903
|
+
// src/elements/stroke-hit.ts
|
|
5904
|
+
function distSqToSegment(p, a, b) {
|
|
5905
|
+
const abx = b.x - a.x;
|
|
5906
|
+
const aby = b.y - a.y;
|
|
5907
|
+
const apx = p.x - a.x;
|
|
5908
|
+
const apy = p.y - a.y;
|
|
5909
|
+
const lenSq = abx * abx + aby * aby;
|
|
5910
|
+
if (lenSq === 0) {
|
|
5911
|
+
return apx * apx + apy * apy;
|
|
5912
|
+
}
|
|
5913
|
+
const t = Math.max(0, Math.min(1, (apx * abx + apy * aby) / lenSq));
|
|
5914
|
+
const dx = p.x - (a.x + t * abx);
|
|
5915
|
+
const dy = p.y - (a.y + t * aby);
|
|
5916
|
+
return dx * dx + dy * dy;
|
|
5917
|
+
}
|
|
5918
|
+
function hitTestStroke(stroke, point, radius) {
|
|
5919
|
+
const bounds = getElementBounds(stroke);
|
|
5920
|
+
if (!bounds) return false;
|
|
5921
|
+
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) {
|
|
5922
|
+
return false;
|
|
5923
|
+
}
|
|
5924
|
+
const radiusSq = radius * radius;
|
|
5925
|
+
const local = { x: point.x - stroke.position.x, y: point.y - stroke.position.y };
|
|
5926
|
+
const { segments } = getStrokeRenderData(stroke);
|
|
5927
|
+
if (segments.length === 0) {
|
|
5928
|
+
const p = stroke.points[0];
|
|
5929
|
+
if (!p) return false;
|
|
5930
|
+
const dx = p.x - local.x;
|
|
5931
|
+
const dy = p.y - local.y;
|
|
5932
|
+
return dx * dx + dy * dy <= radiusSq;
|
|
5933
|
+
}
|
|
5934
|
+
for (const seg of segments) {
|
|
5935
|
+
if (distSqToSegment(local, seg.start, seg.end) <= radiusSq) return true;
|
|
5936
|
+
}
|
|
5937
|
+
return false;
|
|
5938
|
+
}
|
|
5939
|
+
|
|
5746
5940
|
// src/tools/eraser-tool.ts
|
|
5747
5941
|
var DEFAULT_RADIUS = 20;
|
|
5748
5942
|
function makeEraserCursor(radius) {
|
|
@@ -5801,12 +5995,7 @@ var EraserTool = class {
|
|
|
5801
5995
|
if (erased) ctx.requestRender();
|
|
5802
5996
|
}
|
|
5803
5997
|
strokeIntersects(stroke, point) {
|
|
5804
|
-
|
|
5805
|
-
return stroke.points.some((p) => {
|
|
5806
|
-
const dx = p.x + stroke.position.x - point.x;
|
|
5807
|
-
const dy = p.y + stroke.position.y - point.y;
|
|
5808
|
-
return dx * dx + dy * dy <= radiusSq;
|
|
5809
|
-
});
|
|
5998
|
+
return hitTestStroke(stroke, point, this.radius);
|
|
5810
5999
|
}
|
|
5811
6000
|
};
|
|
5812
6001
|
|
|
@@ -6486,12 +6675,7 @@ var SelectTool = class {
|
|
|
6486
6675
|
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;
|
|
6487
6676
|
}
|
|
6488
6677
|
if (el.type === "stroke") {
|
|
6489
|
-
|
|
6490
|
-
return el.points.some((p) => {
|
|
6491
|
-
const dx = p.x + el.position.x - point.x;
|
|
6492
|
-
const dy = p.y + el.position.y - point.y;
|
|
6493
|
-
return dx * dx + dy * dy <= HIT_RADIUS * HIT_RADIUS;
|
|
6494
|
-
});
|
|
6678
|
+
return hitTestStroke(el, point, 10);
|
|
6495
6679
|
}
|
|
6496
6680
|
if (el.type === "arrow") {
|
|
6497
6681
|
return isNearBezier(point, el.from, el.to, el.bend, 10);
|
|
@@ -7327,7 +7511,7 @@ var TemplateTool = class {
|
|
|
7327
7511
|
};
|
|
7328
7512
|
|
|
7329
7513
|
// src/index.ts
|
|
7330
|
-
var VERSION = "0.
|
|
7514
|
+
var VERSION = "0.22.0";
|
|
7331
7515
|
// Annotate the CommonJS export names for ESM import in node:
|
|
7332
7516
|
0 && (module.exports = {
|
|
7333
7517
|
AddElementCommand,
|