@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.d.cts CHANGED
@@ -574,6 +574,10 @@ interface RenderStatsSnapshot {
574
574
  p95FrameMs: number;
575
575
  lastFrameMs: number;
576
576
  lastGridMs: number;
577
+ layersMs: number;
578
+ backgroundMs: number;
579
+ compositeMs: number;
580
+ overlayMs: number;
577
581
  frameCount: number;
578
582
  }
579
583
 
@@ -1247,6 +1251,6 @@ declare class UpdateLayerCommand implements Command {
1247
1251
  undo(_store: ElementStore): void;
1248
1252
  }
1249
1253
 
1250
- declare const VERSION = "0.21.0";
1254
+ declare const VERSION = "0.22.0";
1251
1255
 
1252
1256
  export { type ActiveFormats, AddElementCommand, type ArrowElement, ArrowTool, type ArrowToolOptions, AutoSave, type AutoSaveOptions, Background, type BackgroundOptions, type BackgroundPattern, BatchCommand, type Binding, type Bounds, Camera, type CameraChangeInfo, type CameraOptions, type CanvasElement, type CanvasState, type Command, CreateLayerCommand, DEFAULT_FONT_SIZE_PRESETS, DEFAULT_NOTE_FONT_SIZE, DoubleTapDetector, type DoubleTapDetectorOptions, ElementRenderer, ElementStore, type ElementType, type ElementUpdateEvent, EraserTool, type EraserToolOptions, EventBus, type ExportImageOptions, type FilterAction, type FilteredEvent, type FilteredUpEvent, type FontSizePreset, type GridElement, type GridInfo, HandTool, type HexOrientation, HistoryRecorder, HistoryStack, type HistoryStackOptions, type HtmlElement, type ImageElement, ImageTool, type ImageToolOptions, InputFilter, InputHandler, type InputHandlerOptions, type Layer, LayerManager, MeasureTool, type MeasureToolOptions, type Measurement, NoteEditor, type NoteEditorOptions, type NoteElement, NoteTool, type NoteToolOptions, NoteToolbar, PencilTool, type PencilToolOptions, type Point, type PointerState, Quadtree, RemoveElementCommand, RemoveLayerCommand, type RenderStatsSnapshot, SelectTool, type ShapeElement, type ShapeKind, ShapeTool, type ShapeToolOptions, type ShortcutBindings, type ShortcutOptions, type ShortcutsApi, type Size, type StrokeElement, type StrokePoint, type StyledRun, type TemplateElement, type TemplateShape, TemplateTool, type TemplateToolOptions, type TextElement, TextTool, type TextToolOptions, type Tool, type ToolContext, ToolManager, type ToolName, UpdateElementCommand, UpdateLayerCommand, VERSION, Viewport, type ViewportOptions, boundsIntersect, clearStaleBindings, createArrow, createGrid, createHtmlElement, createId, createImage, createNote, createShape, createStroke, createTemplate, createText, drawHexPath, exportImage, exportState, findBindTarget, findBoundArrows, getActiveFormats, getArrowBounds, getArrowControlPoint, getArrowMidpoint, getArrowTangentAngle, getBendFromPoint, getEdgeIntersection, getElementBounds, getElementCenter, getElementsBoundingBox, getHexCellsInCone, getHexCellsInLine, getHexCellsInRadius, getHexCellsInSquare, getHexDistance, isBindable, isNearBezier, isNoteContentEmpty, parseState, sanitizeNoteHtml, setFontSize, smartSnap, snapPoint, snapToHexCenter, toggleBold, toggleItalic, toggleStrikethrough, toggleUnderline, unbindArrow, updateBoundArrow };
package/dist/index.d.ts CHANGED
@@ -574,6 +574,10 @@ interface RenderStatsSnapshot {
574
574
  p95FrameMs: number;
575
575
  lastFrameMs: number;
576
576
  lastGridMs: number;
577
+ layersMs: number;
578
+ backgroundMs: number;
579
+ compositeMs: number;
580
+ overlayMs: number;
577
581
  frameCount: number;
578
582
  }
579
583
 
@@ -1247,6 +1251,6 @@ declare class UpdateLayerCommand implements Command {
1247
1251
  undo(_store: ElementStore): void;
1248
1252
  }
1249
1253
 
1250
- declare const VERSION = "0.21.0";
1254
+ declare const VERSION = "0.22.0";
1251
1255
 
1252
1256
  export { type ActiveFormats, AddElementCommand, type ArrowElement, ArrowTool, type ArrowToolOptions, AutoSave, type AutoSaveOptions, Background, type BackgroundOptions, type BackgroundPattern, BatchCommand, type Binding, type Bounds, Camera, type CameraChangeInfo, type CameraOptions, type CanvasElement, type CanvasState, type Command, CreateLayerCommand, DEFAULT_FONT_SIZE_PRESETS, DEFAULT_NOTE_FONT_SIZE, DoubleTapDetector, type DoubleTapDetectorOptions, ElementRenderer, ElementStore, type ElementType, type ElementUpdateEvent, EraserTool, type EraserToolOptions, EventBus, type ExportImageOptions, type FilterAction, type FilteredEvent, type FilteredUpEvent, type FontSizePreset, type GridElement, type GridInfo, HandTool, type HexOrientation, HistoryRecorder, HistoryStack, type HistoryStackOptions, type HtmlElement, type ImageElement, ImageTool, type ImageToolOptions, InputFilter, InputHandler, type InputHandlerOptions, type Layer, LayerManager, MeasureTool, type MeasureToolOptions, type Measurement, NoteEditor, type NoteEditorOptions, type NoteElement, NoteTool, type NoteToolOptions, NoteToolbar, PencilTool, type PencilToolOptions, type Point, type PointerState, Quadtree, RemoveElementCommand, RemoveLayerCommand, type RenderStatsSnapshot, SelectTool, type ShapeElement, type ShapeKind, ShapeTool, type ShapeToolOptions, type ShortcutBindings, type ShortcutOptions, type ShortcutsApi, type Size, type StrokeElement, type StrokePoint, type StyledRun, type TemplateElement, type TemplateShape, TemplateTool, type TemplateToolOptions, type TextElement, TextTool, type TextToolOptions, type Tool, type ToolContext, ToolManager, type ToolName, UpdateElementCommand, UpdateLayerCommand, VERSION, Viewport, type ViewportOptions, boundsIntersect, clearStaleBindings, createArrow, createGrid, createHtmlElement, createId, createImage, createNote, createShape, createStroke, createTemplate, createText, drawHexPath, exportImage, exportState, findBindTarget, findBoundArrows, getActiveFormats, getArrowBounds, getArrowControlPoint, getArrowMidpoint, getArrowTangentAngle, getBendFromPoint, getEdgeIntersection, getElementBounds, getElementCenter, getElementsBoundingBox, getHexCellsInCone, getHexCellsInLine, getHexCellsInRadius, getHexCellsInSquare, getHexDistance, isBindable, isNearBezier, isNoteContentEmpty, parseState, sanitizeNoteHtml, setFontSize, smartSnap, snapPoint, snapToHexCenter, toggleBold, toggleItalic, toggleStrikethrough, toggleUnderline, unbindArrow, updateBoundArrow };
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: [] };
@@ -2662,21 +2718,29 @@ var ElementRenderer = class {
2662
2718
  ctx.lineCap = "round";
2663
2719
  ctx.lineJoin = "round";
2664
2720
  ctx.globalAlpha = stroke.opacity;
2665
- const { segments, widths } = getStrokeRenderData(stroke);
2666
- for (let i = 0; i < segments.length; i++) {
2667
- const seg = segments[i];
2668
- const w = widths[i];
2669
- if (!seg || w === void 0) continue;
2670
- ctx.lineWidth = w;
2671
- ctx.beginPath();
2672
- ctx.moveTo(seg.start.x, seg.start.y);
2673
- ctx.bezierCurveTo(seg.cp1.x, seg.cp1.y, seg.cp2.x, seg.cp2.y, seg.end.x, seg.end.y);
2674
- ctx.stroke();
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
+ }
2675
2738
  }
2676
2739
  ctx.restore();
2677
2740
  }
2678
2741
  renderArrow(ctx, arrow) {
2679
- const { visualFrom, visualTo } = this.getVisualEndpoints(arrow);
2742
+ const geometry = getArrowRenderGeometry(arrow);
2743
+ const { visualFrom, visualTo } = this.getVisualEndpoints(arrow, geometry);
2680
2744
  ctx.save();
2681
2745
  ctx.strokeStyle = arrow.color;
2682
2746
  ctx.lineWidth = arrow.width;
@@ -2687,17 +2751,18 @@ var ElementRenderer = class {
2687
2751
  ctx.beginPath();
2688
2752
  ctx.moveTo(visualFrom.x, visualFrom.y);
2689
2753
  if (arrow.bend !== 0) {
2690
- const cp = arrow.cachedControlPoint ?? getArrowControlPoint(arrow.from, arrow.to, arrow.bend);
2691
- ctx.quadraticCurveTo(cp.x, cp.y, visualTo.x, visualTo.y);
2754
+ const cp = geometry.controlPoint;
2755
+ if (cp) {
2756
+ ctx.quadraticCurveTo(cp.x, cp.y, visualTo.x, visualTo.y);
2757
+ }
2692
2758
  } else {
2693
2759
  ctx.lineTo(visualTo.x, visualTo.y);
2694
2760
  }
2695
2761
  ctx.stroke();
2696
- this.renderArrowhead(ctx, arrow, visualTo);
2762
+ this.renderArrowhead(ctx, arrow, visualTo, geometry.tangentEnd);
2697
2763
  ctx.restore();
2698
2764
  }
2699
- renderArrowhead(ctx, arrow, tip) {
2700
- const angle = getArrowTangentAngle(arrow.from, arrow.to, arrow.bend, 1);
2765
+ renderArrowhead(ctx, arrow, tip, angle) {
2701
2766
  ctx.beginPath();
2702
2767
  ctx.moveTo(tip.x, tip.y);
2703
2768
  ctx.lineTo(
@@ -2712,7 +2777,7 @@ var ElementRenderer = class {
2712
2777
  ctx.fillStyle = arrow.color;
2713
2778
  ctx.fill();
2714
2779
  }
2715
- getVisualEndpoints(arrow) {
2780
+ getVisualEndpoints(arrow, geometry) {
2716
2781
  let visualFrom = arrow.from;
2717
2782
  let visualTo = arrow.to;
2718
2783
  if (!this.store) return { visualFrom, visualTo };
@@ -2721,7 +2786,7 @@ var ElementRenderer = class {
2721
2786
  if (el) {
2722
2787
  const bounds = getElementBounds(el);
2723
2788
  if (bounds) {
2724
- const tangentAngle = getArrowTangentAngle(arrow.from, arrow.to, arrow.bend, 0);
2789
+ const tangentAngle = geometry.tangentStart;
2725
2790
  const rayTarget = {
2726
2791
  x: arrow.from.x + Math.cos(tangentAngle) * 1e3,
2727
2792
  y: arrow.from.y + Math.sin(tangentAngle) * 1e3
@@ -2735,7 +2800,7 @@ var ElementRenderer = class {
2735
2800
  if (el) {
2736
2801
  const bounds = getElementBounds(el);
2737
2802
  if (bounds) {
2738
- const tangentAngle = getArrowTangentAngle(arrow.from, arrow.to, arrow.bend, 1);
2803
+ const tangentAngle = geometry.tangentEnd;
2739
2804
  const rayTarget = {
2740
2805
  x: arrow.to.x - Math.cos(tangentAngle) * 1e3,
2741
2806
  y: arrow.to.y - Math.sin(tangentAngle) * 1e3
@@ -4099,19 +4164,19 @@ function loadImages(elements) {
4099
4164
  const imageElements = elements.filter(
4100
4165
  (el) => el.type === "image" && "src" in el
4101
4166
  );
4102
- const cache2 = /* @__PURE__ */ new Map();
4103
- if (imageElements.length === 0) return Promise.resolve(cache2);
4167
+ const cache3 = /* @__PURE__ */ new Map();
4168
+ if (imageElements.length === 0) return Promise.resolve(cache3);
4104
4169
  return new Promise((resolve) => {
4105
4170
  let remaining = imageElements.length;
4106
4171
  const done = () => {
4107
4172
  remaining--;
4108
- if (remaining <= 0) resolve(cache2);
4173
+ if (remaining <= 0) resolve(cache3);
4109
4174
  };
4110
4175
  for (const el of imageElements) {
4111
4176
  const img = new Image();
4112
4177
  img.crossOrigin = "anonymous";
4113
4178
  img.onload = () => {
4114
- cache2.set(el.id, img);
4179
+ cache3.set(el.id, img);
4115
4180
  done();
4116
4181
  };
4117
4182
  img.onerror = done;
@@ -4592,18 +4657,39 @@ var RenderStats = class {
4592
4657
  frameTimes = [];
4593
4658
  frameCount = 0;
4594
4659
  _lastGridMs = 0;
4595
- recordFrame(durationMs, gridMs) {
4660
+ _lastLayersMs = 0;
4661
+ _lastBackgroundMs = 0;
4662
+ _lastCompositeMs = 0;
4663
+ _lastOverlayMs = 0;
4664
+ recordFrame(durationMs, breakdown) {
4596
4665
  this.frameCount++;
4597
4666
  this.frameTimes.push(durationMs);
4598
4667
  if (this.frameTimes.length > SAMPLE_SIZE) {
4599
4668
  this.frameTimes.shift();
4600
4669
  }
4601
- if (gridMs !== void 0) this._lastGridMs = gridMs;
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
+ }
4602
4677
  }
4603
4678
  getSnapshot() {
4604
4679
  const times = this.frameTimes;
4605
4680
  if (times.length === 0) {
4606
- return { fps: 0, avgFrameMs: 0, p95FrameMs: 0, lastFrameMs: 0, lastGridMs: 0, frameCount: 0 };
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
+ };
4607
4693
  }
4608
4694
  const avg = times.reduce((a, b) => a + b, 0) / times.length;
4609
4695
  const sorted = [...times].sort((a, b) => a - b);
@@ -4615,6 +4701,10 @@ var RenderStats = class {
4615
4701
  p95FrameMs: Math.round((sorted[p95Index] ?? 0) * 100) / 100,
4616
4702
  lastFrameMs: Math.round(lastFrame * 100) / 100,
4617
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,
4618
4708
  frameCount: this.frameCount
4619
4709
  };
4620
4710
  }
@@ -4642,7 +4732,7 @@ var RenderLoop = class {
4642
4732
  lastCamX;
4643
4733
  lastCamY;
4644
4734
  stats = new RenderStats();
4645
- lastGridMs = 0;
4735
+ layerGroups = /* @__PURE__ */ new Map();
4646
4736
  gridCacheCanvas = null;
4647
4737
  gridCacheCtx = null;
4648
4738
  gridCacheZoom = -1;
@@ -4740,6 +4830,9 @@ var RenderLoop = class {
4740
4830
  const t0 = performance.now();
4741
4831
  const ctx = this.canvasEl.getContext("2d");
4742
4832
  if (!ctx) return;
4833
+ let layersMs = 0;
4834
+ let compositeMs = 0;
4835
+ let gridMs = 0;
4743
4836
  const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
4744
4837
  const cssWidth = this.canvasEl.clientWidth;
4745
4838
  const cssHeight = this.canvasEl.clientHeight;
@@ -4756,6 +4849,7 @@ var RenderLoop = class {
4756
4849
  ctx.scale(dpr, dpr);
4757
4850
  this.renderer.setCanvasSize(cssWidth, cssHeight);
4758
4851
  const hasGridElement = this.store.getElementsByType("grid").length > 0;
4852
+ const bgT0 = performance.now();
4759
4853
  if (hasGridElement) {
4760
4854
  ctx.save();
4761
4855
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
@@ -4764,6 +4858,7 @@ var RenderLoop = class {
4764
4858
  } else {
4765
4859
  this.background.render(ctx, this.camera);
4766
4860
  }
4861
+ const backgroundMs = performance.now() - bgT0;
4767
4862
  ctx.save();
4768
4863
  ctx.translate(this.camera.position.x, this.camera.position.y);
4769
4864
  ctx.scale(this.camera.zoom, this.camera.zoom);
@@ -4776,7 +4871,7 @@ var RenderLoop = class {
4776
4871
  h: visibleRect.h + margin * 2
4777
4872
  };
4778
4873
  const allElements = this.store.getAll();
4779
- const layerElements = /* @__PURE__ */ new Map();
4874
+ this.layerGroups.clear();
4780
4875
  const gridElements = [];
4781
4876
  let domZIndex = 0;
4782
4877
  for (const element of allElements) {
@@ -4799,25 +4894,30 @@ var RenderLoop = class {
4799
4894
  gridElements.push(element);
4800
4895
  continue;
4801
4896
  }
4802
- let group = layerElements.get(element.layerId);
4897
+ let group = this.layerGroups.get(element.layerId);
4803
4898
  if (!group) {
4804
4899
  group = [];
4805
- layerElements.set(element.layerId, group);
4900
+ this.layerGroups.set(element.layerId, group);
4806
4901
  }
4807
4902
  group.push(element);
4808
4903
  }
4809
- for (const [layerId, elements] of layerElements) {
4904
+ for (const [layerId, elements] of this.layerGroups) {
4810
4905
  const isActiveDrawingLayer = layerId === this.activeDrawingLayerId;
4811
4906
  if (!this.layerCache.isDirty(layerId)) {
4907
+ const compT0 = performance.now();
4812
4908
  this.compositeLayerCache(ctx, layerId, dpr);
4909
+ compositeMs += performance.now() - compT0;
4813
4910
  continue;
4814
4911
  }
4815
4912
  if (isActiveDrawingLayer) {
4913
+ const compT0 = performance.now();
4816
4914
  this.compositeLayerCache(ctx, layerId, dpr);
4915
+ compositeMs += performance.now() - compT0;
4817
4916
  continue;
4818
4917
  }
4819
4918
  const offCtx = this.layerCache.getContext(layerId);
4820
4919
  if (offCtx) {
4920
+ const layerT0 = performance.now();
4821
4921
  const offCanvas = this.layerCache.getCanvas(layerId);
4822
4922
  offCtx.clearRect(0, 0, offCanvas.width, offCanvas.height);
4823
4923
  offCtx.save();
@@ -4831,7 +4931,10 @@ var RenderLoop = class {
4831
4931
  }
4832
4932
  offCtx.restore();
4833
4933
  this.layerCache.markClean(layerId);
4934
+ layersMs += performance.now() - layerT0;
4935
+ const compT0 = performance.now();
4834
4936
  this.compositeLayerCache(ctx, layerId, dpr);
4937
+ compositeMs += performance.now() - compT0;
4835
4938
  }
4836
4939
  }
4837
4940
  if (gridElements.length > 0) {
@@ -4872,15 +4975,23 @@ var RenderLoop = class {
4872
4975
  this.gridCacheHeight = cssHeight;
4873
4976
  this.lastGridRef = gridRef;
4874
4977
  }
4875
- this.lastGridMs = performance.now() - gridT0;
4978
+ gridMs = performance.now() - gridT0;
4876
4979
  }
4980
+ const overlayT0 = performance.now();
4877
4981
  const activeTool = this.toolManager.activeTool;
4878
4982
  if (activeTool?.renderOverlay) {
4879
4983
  activeTool.renderOverlay(ctx);
4880
4984
  }
4985
+ const overlayMs = performance.now() - overlayT0;
4881
4986
  ctx.restore();
4882
4987
  ctx.restore();
4883
- this.stats.recordFrame(performance.now() - t0, this.lastGridMs);
4988
+ this.stats.recordFrame(performance.now() - t0, {
4989
+ gridMs,
4990
+ layersMs,
4991
+ backgroundMs,
4992
+ compositeMs,
4993
+ overlayMs
4994
+ });
4884
4995
  }
4885
4996
  };
4886
4997
 
@@ -5292,7 +5403,7 @@ var Viewport = class {
5292
5403
  const id = setInterval(() => {
5293
5404
  const s = this.getRenderStats();
5294
5405
  console.log(
5295
- `[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`
5296
5407
  );
5297
5408
  }, intervalMs);
5298
5409
  return () => clearInterval(id);
@@ -7290,7 +7401,7 @@ var TemplateTool = class {
7290
7401
  };
7291
7402
 
7292
7403
  // src/index.ts
7293
- var VERSION = "0.21.0";
7404
+ var VERSION = "0.22.0";
7294
7405
  export {
7295
7406
  AddElementCommand,
7296
7407
  ArrowTool,