@fieldnotes/core 0.21.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/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: [] };
@@ -2772,21 +2828,29 @@ var ElementRenderer = class {
2772
2828
  ctx.lineCap = "round";
2773
2829
  ctx.lineJoin = "round";
2774
2830
  ctx.globalAlpha = stroke.opacity;
2775
- const { segments, widths } = getStrokeRenderData(stroke);
2776
- for (let i = 0; i < segments.length; i++) {
2777
- const seg = segments[i];
2778
- const w = widths[i];
2779
- if (!seg || w === void 0) continue;
2780
- ctx.lineWidth = w;
2781
- ctx.beginPath();
2782
- ctx.moveTo(seg.start.x, seg.start.y);
2783
- ctx.bezierCurveTo(seg.cp1.x, seg.cp1.y, seg.cp2.x, seg.cp2.y, seg.end.x, seg.end.y);
2784
- ctx.stroke();
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
+ }
2785
2848
  }
2786
2849
  ctx.restore();
2787
2850
  }
2788
2851
  renderArrow(ctx, arrow) {
2789
- const { visualFrom, visualTo } = this.getVisualEndpoints(arrow);
2852
+ const geometry = getArrowRenderGeometry(arrow);
2853
+ const { visualFrom, visualTo } = this.getVisualEndpoints(arrow, geometry);
2790
2854
  ctx.save();
2791
2855
  ctx.strokeStyle = arrow.color;
2792
2856
  ctx.lineWidth = arrow.width;
@@ -2797,17 +2861,18 @@ var ElementRenderer = class {
2797
2861
  ctx.beginPath();
2798
2862
  ctx.moveTo(visualFrom.x, visualFrom.y);
2799
2863
  if (arrow.bend !== 0) {
2800
- const cp = arrow.cachedControlPoint ?? getArrowControlPoint(arrow.from, arrow.to, arrow.bend);
2801
- ctx.quadraticCurveTo(cp.x, cp.y, visualTo.x, visualTo.y);
2864
+ const cp = geometry.controlPoint;
2865
+ if (cp) {
2866
+ ctx.quadraticCurveTo(cp.x, cp.y, visualTo.x, visualTo.y);
2867
+ }
2802
2868
  } else {
2803
2869
  ctx.lineTo(visualTo.x, visualTo.y);
2804
2870
  }
2805
2871
  ctx.stroke();
2806
- this.renderArrowhead(ctx, arrow, visualTo);
2872
+ this.renderArrowhead(ctx, arrow, visualTo, geometry.tangentEnd);
2807
2873
  ctx.restore();
2808
2874
  }
2809
- renderArrowhead(ctx, arrow, tip) {
2810
- const angle = getArrowTangentAngle(arrow.from, arrow.to, arrow.bend, 1);
2875
+ renderArrowhead(ctx, arrow, tip, angle) {
2811
2876
  ctx.beginPath();
2812
2877
  ctx.moveTo(tip.x, tip.y);
2813
2878
  ctx.lineTo(
@@ -2822,7 +2887,7 @@ var ElementRenderer = class {
2822
2887
  ctx.fillStyle = arrow.color;
2823
2888
  ctx.fill();
2824
2889
  }
2825
- getVisualEndpoints(arrow) {
2890
+ getVisualEndpoints(arrow, geometry) {
2826
2891
  let visualFrom = arrow.from;
2827
2892
  let visualTo = arrow.to;
2828
2893
  if (!this.store) return { visualFrom, visualTo };
@@ -2831,7 +2896,7 @@ var ElementRenderer = class {
2831
2896
  if (el) {
2832
2897
  const bounds = getElementBounds(el);
2833
2898
  if (bounds) {
2834
- const tangentAngle = getArrowTangentAngle(arrow.from, arrow.to, arrow.bend, 0);
2899
+ const tangentAngle = geometry.tangentStart;
2835
2900
  const rayTarget = {
2836
2901
  x: arrow.from.x + Math.cos(tangentAngle) * 1e3,
2837
2902
  y: arrow.from.y + Math.sin(tangentAngle) * 1e3
@@ -2845,7 +2910,7 @@ var ElementRenderer = class {
2845
2910
  if (el) {
2846
2911
  const bounds = getElementBounds(el);
2847
2912
  if (bounds) {
2848
- const tangentAngle = getArrowTangentAngle(arrow.from, arrow.to, arrow.bend, 1);
2913
+ const tangentAngle = geometry.tangentEnd;
2849
2914
  const rayTarget = {
2850
2915
  x: arrow.to.x - Math.cos(tangentAngle) * 1e3,
2851
2916
  y: arrow.to.y - Math.sin(tangentAngle) * 1e3
@@ -4209,19 +4274,19 @@ function loadImages(elements) {
4209
4274
  const imageElements = elements.filter(
4210
4275
  (el) => el.type === "image" && "src" in el
4211
4276
  );
4212
- const cache2 = /* @__PURE__ */ new Map();
4213
- if (imageElements.length === 0) return Promise.resolve(cache2);
4277
+ const cache3 = /* @__PURE__ */ new Map();
4278
+ if (imageElements.length === 0) return Promise.resolve(cache3);
4214
4279
  return new Promise((resolve) => {
4215
4280
  let remaining = imageElements.length;
4216
4281
  const done = () => {
4217
4282
  remaining--;
4218
- if (remaining <= 0) resolve(cache2);
4283
+ if (remaining <= 0) resolve(cache3);
4219
4284
  };
4220
4285
  for (const el of imageElements) {
4221
4286
  const img = new Image();
4222
4287
  img.crossOrigin = "anonymous";
4223
4288
  img.onload = () => {
4224
- cache2.set(el.id, img);
4289
+ cache3.set(el.id, img);
4225
4290
  done();
4226
4291
  };
4227
4292
  img.onerror = done;
@@ -4702,18 +4767,39 @@ var RenderStats = class {
4702
4767
  frameTimes = [];
4703
4768
  frameCount = 0;
4704
4769
  _lastGridMs = 0;
4705
- recordFrame(durationMs, gridMs) {
4770
+ _lastLayersMs = 0;
4771
+ _lastBackgroundMs = 0;
4772
+ _lastCompositeMs = 0;
4773
+ _lastOverlayMs = 0;
4774
+ recordFrame(durationMs, breakdown) {
4706
4775
  this.frameCount++;
4707
4776
  this.frameTimes.push(durationMs);
4708
4777
  if (this.frameTimes.length > SAMPLE_SIZE) {
4709
4778
  this.frameTimes.shift();
4710
4779
  }
4711
- if (gridMs !== void 0) this._lastGridMs = gridMs;
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
+ }
4712
4787
  }
4713
4788
  getSnapshot() {
4714
4789
  const times = this.frameTimes;
4715
4790
  if (times.length === 0) {
4716
- return { fps: 0, avgFrameMs: 0, p95FrameMs: 0, lastFrameMs: 0, lastGridMs: 0, frameCount: 0 };
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
+ };
4717
4803
  }
4718
4804
  const avg = times.reduce((a, b) => a + b, 0) / times.length;
4719
4805
  const sorted = [...times].sort((a, b) => a - b);
@@ -4725,6 +4811,10 @@ var RenderStats = class {
4725
4811
  p95FrameMs: Math.round((sorted[p95Index] ?? 0) * 100) / 100,
4726
4812
  lastFrameMs: Math.round(lastFrame * 100) / 100,
4727
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,
4728
4818
  frameCount: this.frameCount
4729
4819
  };
4730
4820
  }
@@ -4752,7 +4842,7 @@ var RenderLoop = class {
4752
4842
  lastCamX;
4753
4843
  lastCamY;
4754
4844
  stats = new RenderStats();
4755
- lastGridMs = 0;
4845
+ layerGroups = /* @__PURE__ */ new Map();
4756
4846
  gridCacheCanvas = null;
4757
4847
  gridCacheCtx = null;
4758
4848
  gridCacheZoom = -1;
@@ -4850,6 +4940,9 @@ var RenderLoop = class {
4850
4940
  const t0 = performance.now();
4851
4941
  const ctx = this.canvasEl.getContext("2d");
4852
4942
  if (!ctx) return;
4943
+ let layersMs = 0;
4944
+ let compositeMs = 0;
4945
+ let gridMs = 0;
4853
4946
  const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
4854
4947
  const cssWidth = this.canvasEl.clientWidth;
4855
4948
  const cssHeight = this.canvasEl.clientHeight;
@@ -4866,6 +4959,7 @@ var RenderLoop = class {
4866
4959
  ctx.scale(dpr, dpr);
4867
4960
  this.renderer.setCanvasSize(cssWidth, cssHeight);
4868
4961
  const hasGridElement = this.store.getElementsByType("grid").length > 0;
4962
+ const bgT0 = performance.now();
4869
4963
  if (hasGridElement) {
4870
4964
  ctx.save();
4871
4965
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
@@ -4874,6 +4968,7 @@ var RenderLoop = class {
4874
4968
  } else {
4875
4969
  this.background.render(ctx, this.camera);
4876
4970
  }
4971
+ const backgroundMs = performance.now() - bgT0;
4877
4972
  ctx.save();
4878
4973
  ctx.translate(this.camera.position.x, this.camera.position.y);
4879
4974
  ctx.scale(this.camera.zoom, this.camera.zoom);
@@ -4886,7 +4981,7 @@ var RenderLoop = class {
4886
4981
  h: visibleRect.h + margin * 2
4887
4982
  };
4888
4983
  const allElements = this.store.getAll();
4889
- const layerElements = /* @__PURE__ */ new Map();
4984
+ this.layerGroups.clear();
4890
4985
  const gridElements = [];
4891
4986
  let domZIndex = 0;
4892
4987
  for (const element of allElements) {
@@ -4909,25 +5004,30 @@ var RenderLoop = class {
4909
5004
  gridElements.push(element);
4910
5005
  continue;
4911
5006
  }
4912
- let group = layerElements.get(element.layerId);
5007
+ let group = this.layerGroups.get(element.layerId);
4913
5008
  if (!group) {
4914
5009
  group = [];
4915
- layerElements.set(element.layerId, group);
5010
+ this.layerGroups.set(element.layerId, group);
4916
5011
  }
4917
5012
  group.push(element);
4918
5013
  }
4919
- for (const [layerId, elements] of layerElements) {
5014
+ for (const [layerId, elements] of this.layerGroups) {
4920
5015
  const isActiveDrawingLayer = layerId === this.activeDrawingLayerId;
4921
5016
  if (!this.layerCache.isDirty(layerId)) {
5017
+ const compT0 = performance.now();
4922
5018
  this.compositeLayerCache(ctx, layerId, dpr);
5019
+ compositeMs += performance.now() - compT0;
4923
5020
  continue;
4924
5021
  }
4925
5022
  if (isActiveDrawingLayer) {
5023
+ const compT0 = performance.now();
4926
5024
  this.compositeLayerCache(ctx, layerId, dpr);
5025
+ compositeMs += performance.now() - compT0;
4927
5026
  continue;
4928
5027
  }
4929
5028
  const offCtx = this.layerCache.getContext(layerId);
4930
5029
  if (offCtx) {
5030
+ const layerT0 = performance.now();
4931
5031
  const offCanvas = this.layerCache.getCanvas(layerId);
4932
5032
  offCtx.clearRect(0, 0, offCanvas.width, offCanvas.height);
4933
5033
  offCtx.save();
@@ -4941,7 +5041,10 @@ var RenderLoop = class {
4941
5041
  }
4942
5042
  offCtx.restore();
4943
5043
  this.layerCache.markClean(layerId);
5044
+ layersMs += performance.now() - layerT0;
5045
+ const compT0 = performance.now();
4944
5046
  this.compositeLayerCache(ctx, layerId, dpr);
5047
+ compositeMs += performance.now() - compT0;
4945
5048
  }
4946
5049
  }
4947
5050
  if (gridElements.length > 0) {
@@ -4982,15 +5085,23 @@ var RenderLoop = class {
4982
5085
  this.gridCacheHeight = cssHeight;
4983
5086
  this.lastGridRef = gridRef;
4984
5087
  }
4985
- this.lastGridMs = performance.now() - gridT0;
5088
+ gridMs = performance.now() - gridT0;
4986
5089
  }
5090
+ const overlayT0 = performance.now();
4987
5091
  const activeTool = this.toolManager.activeTool;
4988
5092
  if (activeTool?.renderOverlay) {
4989
5093
  activeTool.renderOverlay(ctx);
4990
5094
  }
5095
+ const overlayMs = performance.now() - overlayT0;
4991
5096
  ctx.restore();
4992
5097
  ctx.restore();
4993
- this.stats.recordFrame(performance.now() - t0, this.lastGridMs);
5098
+ this.stats.recordFrame(performance.now() - t0, {
5099
+ gridMs,
5100
+ layersMs,
5101
+ backgroundMs,
5102
+ compositeMs,
5103
+ overlayMs
5104
+ });
4994
5105
  }
4995
5106
  };
4996
5107
 
@@ -5402,7 +5513,7 @@ var Viewport = class {
5402
5513
  const id = setInterval(() => {
5403
5514
  const s = this.getRenderStats();
5404
5515
  console.log(
5405
- `[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`
5406
5517
  );
5407
5518
  }, intervalMs);
5408
5519
  return () => clearInterval(id);
@@ -7400,7 +7511,7 @@ var TemplateTool = class {
7400
7511
  };
7401
7512
 
7402
7513
  // src/index.ts
7403
- var VERSION = "0.21.0";
7514
+ var VERSION = "0.22.0";
7404
7515
  // Annotate the CommonJS export names for ESM import in node:
7405
7516
  0 && (module.exports = {
7406
7517
  AddElementCommand,