@fieldnotes/core 0.21.0 → 0.23.0

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