@fieldnotes/core 0.8.7 → 0.8.9

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
@@ -542,6 +542,13 @@ var Background = class {
542
542
  color;
543
543
  dotRadius;
544
544
  lineWidth;
545
+ cachedCanvas = null;
546
+ cachedCtx = null;
547
+ lastZoom = -1;
548
+ lastOffsetX = -Infinity;
549
+ lastOffsetY = -Infinity;
550
+ lastWidth = 0;
551
+ lastHeight = 0;
545
552
  constructor(options = {}) {
546
553
  this.pattern = options.pattern ?? DEFAULTS.pattern;
547
554
  this.spacing = options.spacing ?? DEFAULTS.spacing;
@@ -557,13 +564,69 @@ var Background = class {
557
564
  ctx.save();
558
565
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
559
566
  ctx.clearRect(0, 0, cssWidth, cssHeight);
567
+ if (this.pattern === "none") {
568
+ ctx.restore();
569
+ return;
570
+ }
571
+ const spacing = this.adaptSpacing(this.spacing, camera.zoom);
572
+ const keyZoom = camera.zoom;
573
+ const keyX = Math.floor(camera.position.x % spacing);
574
+ const keyY = Math.floor(camera.position.y % spacing);
575
+ if (this.cachedCanvas !== null && keyZoom === this.lastZoom && keyX === this.lastOffsetX && keyY === this.lastOffsetY && cssWidth === this.lastWidth && cssHeight === this.lastHeight) {
576
+ ctx.drawImage(this.cachedCanvas, 0, 0);
577
+ ctx.restore();
578
+ return;
579
+ }
580
+ this.ensureCachedCanvas(cssWidth, cssHeight, dpr);
581
+ if (this.cachedCtx === null) {
582
+ if (this.pattern === "dots") {
583
+ this.renderDots(ctx, camera, cssWidth, cssHeight);
584
+ } else if (this.pattern === "grid") {
585
+ this.renderGrid(ctx, camera, cssWidth, cssHeight);
586
+ }
587
+ ctx.restore();
588
+ return;
589
+ }
590
+ const offCtx = this.cachedCtx;
591
+ offCtx.clearRect(0, 0, cssWidth, cssHeight);
560
592
  if (this.pattern === "dots") {
561
- this.renderDots(ctx, camera, cssWidth, cssHeight);
593
+ this.renderDots(offCtx, camera, cssWidth, cssHeight);
562
594
  } else if (this.pattern === "grid") {
563
- this.renderGrid(ctx, camera, cssWidth, cssHeight);
564
- }
595
+ this.renderGrid(offCtx, camera, cssWidth, cssHeight);
596
+ }
597
+ this.lastZoom = keyZoom;
598
+ this.lastOffsetX = keyX;
599
+ this.lastOffsetY = keyY;
600
+ this.lastWidth = cssWidth;
601
+ this.lastHeight = cssHeight;
602
+ ctx.drawImage(this.cachedCanvas, 0, 0);
565
603
  ctx.restore();
566
604
  }
605
+ ensureCachedCanvas(cssWidth, cssHeight, dpr) {
606
+ if (this.cachedCanvas !== null && this.lastWidth === cssWidth && this.lastHeight === cssHeight) {
607
+ return;
608
+ }
609
+ const physWidth = Math.round(cssWidth * dpr);
610
+ const physHeight = Math.round(cssHeight * dpr);
611
+ if (typeof OffscreenCanvas !== "undefined") {
612
+ this.cachedCanvas = new OffscreenCanvas(physWidth, physHeight);
613
+ } else if (typeof document !== "undefined") {
614
+ const el = document.createElement("canvas");
615
+ el.width = physWidth;
616
+ el.height = physHeight;
617
+ this.cachedCanvas = el;
618
+ } else {
619
+ this.cachedCanvas = null;
620
+ this.cachedCtx = null;
621
+ return;
622
+ }
623
+ const offCtx = this.cachedCanvas.getContext("2d");
624
+ if (offCtx !== null) {
625
+ offCtx.scale(dpr, dpr);
626
+ }
627
+ this.cachedCtx = offCtx;
628
+ this.lastZoom = -1;
629
+ }
567
630
  adaptSpacing(baseSpacing, zoom) {
568
631
  let spacing = baseSpacing * zoom;
569
632
  while (spacing < MIN_PATTERN_SPACING) {
@@ -1046,6 +1109,10 @@ var ElementStore = class {
1046
1109
  const existing = this.elements.get(id);
1047
1110
  if (!existing) return;
1048
1111
  const updated = { ...existing, ...partial, id: existing.id, type: existing.type };
1112
+ if (updated.type === "arrow") {
1113
+ const arrow = updated;
1114
+ arrow.cachedControlPoint = getArrowControlPoint(arrow.from, arrow.to, arrow.bend);
1115
+ }
1049
1116
  this.elements.set(id, updated);
1050
1117
  const newBounds = getElementBounds(updated);
1051
1118
  if (newBounds) {
@@ -1301,6 +1368,25 @@ function smoothToSegments(points) {
1301
1368
  return segments;
1302
1369
  }
1303
1370
 
1371
+ // src/elements/stroke-cache.ts
1372
+ var cache = /* @__PURE__ */ new WeakMap();
1373
+ function computeStrokeSegments(stroke) {
1374
+ const segments = smoothToSegments(stroke.points);
1375
+ const widths = [];
1376
+ for (const seg of segments) {
1377
+ const w = (pressureToWidth(seg.start.pressure, stroke.width) + pressureToWidth(seg.end.pressure, stroke.width)) / 2;
1378
+ widths.push(w);
1379
+ }
1380
+ const data = { segments, widths };
1381
+ cache.set(stroke, data);
1382
+ return data;
1383
+ }
1384
+ function getStrokeRenderData(stroke) {
1385
+ const cached = cache.get(stroke);
1386
+ if (cached) return cached;
1387
+ return computeStrokeSegments(stroke);
1388
+ }
1389
+
1304
1390
  // src/elements/grid-renderer.ts
1305
1391
  function getSquareGridLines(bounds, cellSize) {
1306
1392
  if (cellSize <= 0) return { verticals: [], horizontals: [] };
@@ -1318,58 +1404,6 @@ function getSquareGridLines(bounds, cellSize) {
1318
1404
  }
1319
1405
  return { verticals, horizontals };
1320
1406
  }
1321
- function getHexVertices(cx, cy, circumradius, orientation) {
1322
- const vertices = [];
1323
- const angleOffset = orientation === "pointy" ? -Math.PI / 2 : 0;
1324
- for (let i = 0; i < 6; i++) {
1325
- const angle = Math.PI / 3 * i + angleOffset;
1326
- vertices.push({
1327
- x: cx + circumradius * Math.cos(angle),
1328
- y: cy + circumradius * Math.sin(angle)
1329
- });
1330
- }
1331
- return vertices;
1332
- }
1333
- function getHexCenters(bounds, circumradius, orientation) {
1334
- if (circumradius <= 0) return [];
1335
- const centers = [];
1336
- if (orientation === "pointy") {
1337
- const hexW = Math.sqrt(3) * circumradius;
1338
- const hexH = 2 * circumradius;
1339
- const rowH = hexH * 0.75;
1340
- const startRow = Math.floor((bounds.minY - circumradius) / rowH);
1341
- const endRow = Math.ceil((bounds.maxY + circumradius) / rowH);
1342
- const startCol = Math.floor((bounds.minX - hexW) / hexW);
1343
- const endCol = Math.ceil((bounds.maxX + hexW) / hexW);
1344
- for (let row = startRow; row <= endRow; row++) {
1345
- const offsetX = row % 2 !== 0 ? hexW / 2 : 0;
1346
- for (let col = startCol; col <= endCol; col++) {
1347
- centers.push({
1348
- x: col * hexW + offsetX,
1349
- y: row * rowH
1350
- });
1351
- }
1352
- }
1353
- } else {
1354
- const hexW = 2 * circumradius;
1355
- const hexH = Math.sqrt(3) * circumradius;
1356
- const colW = hexW * 0.75;
1357
- const startCol = Math.floor((bounds.minX - circumradius) / colW);
1358
- const endCol = Math.ceil((bounds.maxX + circumradius) / colW);
1359
- const startRow = Math.floor((bounds.minY - hexH) / hexH);
1360
- const endRow = Math.ceil((bounds.maxY + hexH) / hexH);
1361
- for (let col = startCol; col <= endCol; col++) {
1362
- const offsetY = col % 2 !== 0 ? hexH / 2 : 0;
1363
- for (let row = startRow; row <= endRow; row++) {
1364
- centers.push({
1365
- x: col * colW,
1366
- y: row * hexH + offsetY
1367
- });
1368
- }
1369
- }
1370
- }
1371
- return centers;
1372
- }
1373
1407
  function renderSquareGrid(ctx, bounds, cellSize, strokeColor, strokeWidth, opacity) {
1374
1408
  if (cellSize <= 0) return;
1375
1409
  const { verticals, horizontals } = getSquareGridLines(bounds, cellSize);
@@ -1391,27 +1425,172 @@ function renderSquareGrid(ctx, bounds, cellSize, strokeColor, strokeWidth, opaci
1391
1425
  }
1392
1426
  function renderHexGrid(ctx, bounds, cellSize, orientation, strokeColor, strokeWidth, opacity) {
1393
1427
  if (cellSize <= 0) return;
1394
- const centers = getHexCenters(bounds, cellSize, orientation);
1428
+ const angleOffset = orientation === "pointy" ? -Math.PI / 2 : 0;
1429
+ const ox0 = cellSize * Math.cos(angleOffset);
1430
+ const oy0 = cellSize * Math.sin(angleOffset);
1431
+ const ox1 = cellSize * Math.cos(Math.PI / 3 + angleOffset);
1432
+ const oy1 = cellSize * Math.sin(Math.PI / 3 + angleOffset);
1433
+ const ox2 = cellSize * Math.cos(2 * Math.PI / 3 + angleOffset);
1434
+ const oy2 = cellSize * Math.sin(2 * Math.PI / 3 + angleOffset);
1435
+ const ox3 = cellSize * Math.cos(Math.PI + angleOffset);
1436
+ const oy3 = cellSize * Math.sin(Math.PI + angleOffset);
1437
+ const ox4 = cellSize * Math.cos(4 * Math.PI / 3 + angleOffset);
1438
+ const oy4 = cellSize * Math.sin(4 * Math.PI / 3 + angleOffset);
1439
+ const ox5 = cellSize * Math.cos(5 * Math.PI / 3 + angleOffset);
1440
+ const oy5 = cellSize * Math.sin(5 * Math.PI / 3 + angleOffset);
1395
1441
  ctx.save();
1396
1442
  ctx.strokeStyle = strokeColor;
1397
1443
  ctx.lineWidth = strokeWidth;
1398
1444
  ctx.globalAlpha = opacity;
1399
1445
  ctx.beginPath();
1400
- for (const center of centers) {
1401
- const verts = getHexVertices(center.x, center.y, cellSize, orientation);
1402
- const first = verts[0];
1403
- if (!first) continue;
1404
- ctx.moveTo(first.x, first.y);
1405
- for (let i = 1; i < verts.length; i++) {
1406
- const v = verts[i];
1407
- if (!v) continue;
1408
- ctx.lineTo(v.x, v.y);
1446
+ if (orientation === "pointy") {
1447
+ const hexW = Math.sqrt(3) * cellSize;
1448
+ const rowH = 1.5 * cellSize;
1449
+ const startRow = Math.floor((bounds.minY - cellSize) / rowH);
1450
+ const endRow = Math.ceil((bounds.maxY + cellSize) / rowH);
1451
+ const startCol = Math.floor((bounds.minX - hexW) / hexW);
1452
+ const endCol = Math.ceil((bounds.maxX + hexW) / hexW);
1453
+ for (let row = startRow; row <= endRow; row++) {
1454
+ const offX = row % 2 !== 0 ? hexW / 2 : 0;
1455
+ for (let col = startCol; col <= endCol; col++) {
1456
+ const cx = col * hexW + offX;
1457
+ const cy = row * rowH;
1458
+ ctx.moveTo(cx + ox0, cy + oy0);
1459
+ ctx.lineTo(cx + ox1, cy + oy1);
1460
+ ctx.lineTo(cx + ox2, cy + oy2);
1461
+ ctx.lineTo(cx + ox3, cy + oy3);
1462
+ ctx.lineTo(cx + ox4, cy + oy4);
1463
+ ctx.lineTo(cx + ox5, cy + oy5);
1464
+ ctx.closePath();
1465
+ }
1466
+ }
1467
+ } else {
1468
+ const hexH = Math.sqrt(3) * cellSize;
1469
+ const colW = 1.5 * cellSize;
1470
+ const startCol = Math.floor((bounds.minX - cellSize) / colW);
1471
+ const endCol = Math.ceil((bounds.maxX + cellSize) / colW);
1472
+ const startRow = Math.floor((bounds.minY - hexH) / hexH);
1473
+ const endRow = Math.ceil((bounds.maxY + hexH) / hexH);
1474
+ for (let col = startCol; col <= endCol; col++) {
1475
+ const offY = col % 2 !== 0 ? hexH / 2 : 0;
1476
+ for (let row = startRow; row <= endRow; row++) {
1477
+ const cx = col * colW;
1478
+ const cy = row * hexH + offY;
1479
+ ctx.moveTo(cx + ox0, cy + oy0);
1480
+ ctx.lineTo(cx + ox1, cy + oy1);
1481
+ ctx.lineTo(cx + ox2, cy + oy2);
1482
+ ctx.lineTo(cx + ox3, cy + oy3);
1483
+ ctx.lineTo(cx + ox4, cy + oy4);
1484
+ ctx.lineTo(cx + ox5, cy + oy5);
1485
+ ctx.closePath();
1486
+ }
1409
1487
  }
1410
- ctx.closePath();
1411
1488
  }
1412
1489
  ctx.stroke();
1413
1490
  ctx.restore();
1414
1491
  }
1492
+ function createHexGridTile(cellSize, orientation, strokeColor, strokeWidth, opacity, scale) {
1493
+ let tileW;
1494
+ let tileH;
1495
+ if (orientation === "pointy") {
1496
+ tileW = Math.sqrt(3) * cellSize;
1497
+ tileH = 3 * cellSize;
1498
+ } else {
1499
+ tileW = 3 * cellSize;
1500
+ tileH = Math.sqrt(3) * cellSize;
1501
+ }
1502
+ const pxW = Math.ceil(tileW * scale);
1503
+ const pxH = Math.ceil(tileH * scale);
1504
+ if (pxW <= 0 || pxH <= 0) return null;
1505
+ let canvas;
1506
+ if (typeof OffscreenCanvas !== "undefined") {
1507
+ canvas = new OffscreenCanvas(pxW, pxH);
1508
+ } else if (typeof document !== "undefined") {
1509
+ const el = document.createElement("canvas");
1510
+ el.width = pxW;
1511
+ el.height = pxH;
1512
+ canvas = el;
1513
+ } else {
1514
+ return null;
1515
+ }
1516
+ const tc = canvas.getContext("2d");
1517
+ if (!tc) return null;
1518
+ tc.scale(scale, scale);
1519
+ tc.beginPath();
1520
+ tc.rect(0, 0, tileW, tileH);
1521
+ tc.clip();
1522
+ const angleOffset = orientation === "pointy" ? -Math.PI / 2 : 0;
1523
+ const ox0 = cellSize * Math.cos(angleOffset);
1524
+ const oy0 = cellSize * Math.sin(angleOffset);
1525
+ const ox1 = cellSize * Math.cos(Math.PI / 3 + angleOffset);
1526
+ const oy1 = cellSize * Math.sin(Math.PI / 3 + angleOffset);
1527
+ const ox2 = cellSize * Math.cos(2 * Math.PI / 3 + angleOffset);
1528
+ const oy2 = cellSize * Math.sin(2 * Math.PI / 3 + angleOffset);
1529
+ const ox3 = cellSize * Math.cos(Math.PI + angleOffset);
1530
+ const oy3 = cellSize * Math.sin(Math.PI + angleOffset);
1531
+ const ox4 = cellSize * Math.cos(4 * Math.PI / 3 + angleOffset);
1532
+ const oy4 = cellSize * Math.sin(4 * Math.PI / 3 + angleOffset);
1533
+ const ox5 = cellSize * Math.cos(5 * Math.PI / 3 + angleOffset);
1534
+ const oy5 = cellSize * Math.sin(5 * Math.PI / 3 + angleOffset);
1535
+ tc.strokeStyle = strokeColor;
1536
+ tc.lineWidth = strokeWidth;
1537
+ tc.globalAlpha = opacity;
1538
+ tc.beginPath();
1539
+ if (orientation === "pointy") {
1540
+ const hexW = tileW;
1541
+ const rowH = 1.5 * cellSize;
1542
+ for (let row = -1; row <= 3; row++) {
1543
+ const offX = row % 2 !== 0 ? hexW / 2 : 0;
1544
+ for (let col = -1; col <= 1; col++) {
1545
+ const cx = col * hexW + offX;
1546
+ const cy = row * rowH;
1547
+ tc.moveTo(cx + ox0, cy + oy0);
1548
+ tc.lineTo(cx + ox1, cy + oy1);
1549
+ tc.lineTo(cx + ox2, cy + oy2);
1550
+ tc.lineTo(cx + ox3, cy + oy3);
1551
+ tc.lineTo(cx + ox4, cy + oy4);
1552
+ tc.lineTo(cx + ox5, cy + oy5);
1553
+ tc.closePath();
1554
+ }
1555
+ }
1556
+ } else {
1557
+ const hexH = tileH;
1558
+ const colW = 1.5 * cellSize;
1559
+ for (let col = -1; col <= 3; col++) {
1560
+ const offY = col % 2 !== 0 ? hexH / 2 : 0;
1561
+ for (let row = -1; row <= 1; row++) {
1562
+ const cx = col * colW;
1563
+ const cy = row * hexH + offY;
1564
+ tc.moveTo(cx + ox0, cy + oy0);
1565
+ tc.lineTo(cx + ox1, cy + oy1);
1566
+ tc.lineTo(cx + ox2, cy + oy2);
1567
+ tc.lineTo(cx + ox3, cy + oy3);
1568
+ tc.lineTo(cx + ox4, cy + oy4);
1569
+ tc.lineTo(cx + ox5, cy + oy5);
1570
+ tc.closePath();
1571
+ }
1572
+ }
1573
+ }
1574
+ tc.stroke();
1575
+ return { canvas, tileW, tileH };
1576
+ }
1577
+ function renderHexGridTiled(ctx, bounds, cellSize, tile, scale) {
1578
+ const pattern = ctx.createPattern(tile.canvas, "repeat");
1579
+ if (!pattern) return;
1580
+ const mat = new DOMMatrix();
1581
+ mat.scaleSelf(1 / scale, 1 / scale);
1582
+ pattern.setTransform(mat);
1583
+ ctx.save();
1584
+ ctx.fillStyle = pattern;
1585
+ const pad = cellSize * 2;
1586
+ ctx.fillRect(
1587
+ bounds.minX - pad,
1588
+ bounds.minY - pad,
1589
+ bounds.maxX - bounds.minX + pad * 2,
1590
+ bounds.maxY - bounds.minY + pad * 2
1591
+ );
1592
+ ctx.restore();
1593
+ }
1415
1594
 
1416
1595
  // src/elements/element-renderer.ts
1417
1596
  var DOM_ELEMENT_TYPES = /* @__PURE__ */ new Set(["note", "html", "text"]);
@@ -1423,6 +1602,8 @@ var ElementRenderer = class {
1423
1602
  onImageLoad = null;
1424
1603
  camera = null;
1425
1604
  canvasSize = null;
1605
+ hexTileCache = null;
1606
+ hexTileCacheKey = "";
1426
1607
  setStore(store) {
1427
1608
  this.store = store;
1428
1609
  }
@@ -1465,9 +1646,11 @@ var ElementRenderer = class {
1465
1646
  ctx.lineCap = "round";
1466
1647
  ctx.lineJoin = "round";
1467
1648
  ctx.globalAlpha = stroke.opacity;
1468
- const segments = smoothToSegments(stroke.points);
1469
- for (const seg of segments) {
1470
- const w = (pressureToWidth(seg.start.pressure, stroke.width) + pressureToWidth(seg.end.pressure, stroke.width)) / 2;
1649
+ const { segments, widths } = getStrokeRenderData(stroke);
1650
+ for (let i = 0; i < segments.length; i++) {
1651
+ const seg = segments[i];
1652
+ const w = widths[i];
1653
+ if (!seg || w === void 0) continue;
1471
1654
  ctx.lineWidth = w;
1472
1655
  ctx.beginPath();
1473
1656
  ctx.moveTo(seg.start.x, seg.start.y);
@@ -1488,7 +1671,7 @@ var ElementRenderer = class {
1488
1671
  ctx.beginPath();
1489
1672
  ctx.moveTo(visualFrom.x, visualFrom.y);
1490
1673
  if (arrow.bend !== 0) {
1491
- const cp = getArrowControlPoint(arrow.from, arrow.to, arrow.bend);
1674
+ const cp = arrow.cachedControlPoint ?? getArrowControlPoint(arrow.from, arrow.to, arrow.bend);
1492
1675
  ctx.quadraticCurveTo(cp.x, cp.y, visualTo.x, visualTo.y);
1493
1676
  } else {
1494
1677
  ctx.lineTo(visualTo.x, visualTo.y);
@@ -1606,15 +1789,29 @@ var ElementRenderer = class {
1606
1789
  maxY: bottomRight.y
1607
1790
  };
1608
1791
  if (grid.gridType === "hex") {
1609
- renderHexGrid(
1610
- ctx,
1611
- bounds,
1792
+ const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
1793
+ const scale = cam.zoom * dpr;
1794
+ const tile = this.getHexTile(
1612
1795
  grid.cellSize,
1613
1796
  grid.hexOrientation,
1614
1797
  grid.strokeColor,
1615
1798
  grid.strokeWidth,
1616
- grid.opacity
1799
+ grid.opacity,
1800
+ scale
1617
1801
  );
1802
+ if (tile) {
1803
+ renderHexGridTiled(ctx, bounds, grid.cellSize, tile, scale);
1804
+ } else {
1805
+ renderHexGrid(
1806
+ ctx,
1807
+ bounds,
1808
+ grid.cellSize,
1809
+ grid.hexOrientation,
1810
+ grid.strokeColor,
1811
+ grid.strokeWidth,
1812
+ grid.opacity
1813
+ );
1814
+ }
1618
1815
  } else {
1619
1816
  renderSquareGrid(
1620
1817
  ctx,
@@ -1629,15 +1826,45 @@ var ElementRenderer = class {
1629
1826
  renderImage(ctx, image) {
1630
1827
  const img = this.getImage(image.src);
1631
1828
  if (!img) return;
1632
- ctx.drawImage(img, image.position.x, image.position.y, image.size.w, image.size.h);
1829
+ ctx.drawImage(
1830
+ img,
1831
+ image.position.x,
1832
+ image.position.y,
1833
+ image.size.w,
1834
+ image.size.h
1835
+ );
1836
+ }
1837
+ getHexTile(cellSize, orientation, strokeColor, strokeWidth, opacity, scale) {
1838
+ const key = `${cellSize}:${orientation}:${strokeColor}:${strokeWidth}:${opacity}:${scale}`;
1839
+ if (this.hexTileCacheKey === key && this.hexTileCache) {
1840
+ return this.hexTileCache;
1841
+ }
1842
+ const tile = createHexGridTile(cellSize, orientation, strokeColor, strokeWidth, opacity, scale);
1843
+ if (tile) {
1844
+ this.hexTileCache = tile;
1845
+ this.hexTileCacheKey = key;
1846
+ }
1847
+ return tile;
1633
1848
  }
1634
1849
  getImage(src) {
1635
1850
  const cached = this.imageCache.get(src);
1636
- if (cached) return cached.complete ? cached : null;
1851
+ if (cached) {
1852
+ if (cached instanceof HTMLImageElement) return cached.complete ? cached : null;
1853
+ return cached;
1854
+ }
1637
1855
  const img = new Image();
1638
1856
  img.src = src;
1639
1857
  this.imageCache.set(src, img);
1640
- img.onload = () => this.onImageLoad?.();
1858
+ img.onload = () => {
1859
+ this.onImageLoad?.();
1860
+ if (typeof createImageBitmap !== "undefined") {
1861
+ createImageBitmap(img).then((bitmap) => {
1862
+ this.imageCache.set(src, bitmap);
1863
+ this.onImageLoad?.();
1864
+ }).catch(() => {
1865
+ });
1866
+ }
1867
+ };
1641
1868
  return null;
1642
1869
  }
1643
1870
  };
@@ -2013,6 +2240,7 @@ function createNote(input) {
2013
2240
  };
2014
2241
  }
2015
2242
  function createArrow(input) {
2243
+ const bend = input.bend ?? 0;
2016
2244
  const result = {
2017
2245
  id: createId("arrow"),
2018
2246
  type: "arrow",
@@ -2022,9 +2250,10 @@ function createArrow(input) {
2022
2250
  layerId: input.layerId ?? "",
2023
2251
  from: input.from,
2024
2252
  to: input.to,
2025
- bend: input.bend ?? 0,
2253
+ bend,
2026
2254
  color: input.color ?? "#000000",
2027
- width: input.width ?? 2
2255
+ width: input.width ?? 2,
2256
+ cachedControlPoint: getArrowControlPoint(input.from, input.to, bend)
2028
2257
  };
2029
2258
  if (input.fromBinding) result.fromBinding = input.fromBinding;
2030
2259
  if (input.toBinding) result.toBinding = input.toBinding;
@@ -2275,19 +2504,19 @@ function loadImages(elements) {
2275
2504
  const imageElements = elements.filter(
2276
2505
  (el) => el.type === "image" && "src" in el
2277
2506
  );
2278
- const cache = /* @__PURE__ */ new Map();
2279
- if (imageElements.length === 0) return Promise.resolve(cache);
2507
+ const cache2 = /* @__PURE__ */ new Map();
2508
+ if (imageElements.length === 0) return Promise.resolve(cache2);
2280
2509
  return new Promise((resolve) => {
2281
2510
  let remaining = imageElements.length;
2282
2511
  const done = () => {
2283
2512
  remaining--;
2284
- if (remaining <= 0) resolve(cache);
2513
+ if (remaining <= 0) resolve(cache2);
2285
2514
  };
2286
2515
  for (const el of imageElements) {
2287
2516
  const img = new Image();
2288
2517
  img.crossOrigin = "anonymous";
2289
2518
  img.onload = () => {
2290
- cache.set(el.id, img);
2519
+ cache2.set(el.id, img);
2291
2520
  done();
2292
2521
  };
2293
2522
  img.onerror = done;
@@ -2717,17 +2946,19 @@ var SAMPLE_SIZE = 60;
2717
2946
  var RenderStats = class {
2718
2947
  frameTimes = [];
2719
2948
  frameCount = 0;
2720
- recordFrame(durationMs) {
2949
+ _lastGridMs = 0;
2950
+ recordFrame(durationMs, gridMs) {
2721
2951
  this.frameCount++;
2722
2952
  this.frameTimes.push(durationMs);
2723
2953
  if (this.frameTimes.length > SAMPLE_SIZE) {
2724
2954
  this.frameTimes.shift();
2725
2955
  }
2956
+ if (gridMs !== void 0) this._lastGridMs = gridMs;
2726
2957
  }
2727
2958
  getSnapshot() {
2728
2959
  const times = this.frameTimes;
2729
2960
  if (times.length === 0) {
2730
- return { fps: 0, avgFrameMs: 0, p95FrameMs: 0, lastFrameMs: 0, frameCount: 0 };
2961
+ return { fps: 0, avgFrameMs: 0, p95FrameMs: 0, lastFrameMs: 0, lastGridMs: 0, frameCount: 0 };
2731
2962
  }
2732
2963
  const avg = times.reduce((a, b) => a + b, 0) / times.length;
2733
2964
  const sorted = [...times].sort((a, b) => a - b);
@@ -2738,6 +2969,7 @@ var RenderStats = class {
2738
2969
  avgFrameMs: Math.round(avg * 100) / 100,
2739
2970
  p95FrameMs: Math.round((sorted[p95Index] ?? 0) * 100) / 100,
2740
2971
  lastFrameMs: Math.round(lastFrame * 100) / 100,
2972
+ lastGridMs: Math.round(this._lastGridMs * 100) / 100,
2741
2973
  frameCount: this.frameCount
2742
2974
  };
2743
2975
  }
@@ -2765,6 +2997,15 @@ var RenderLoop = class {
2765
2997
  lastCamX;
2766
2998
  lastCamY;
2767
2999
  stats = new RenderStats();
3000
+ lastGridMs = 0;
3001
+ gridCacheCanvas = null;
3002
+ gridCacheCtx = null;
3003
+ gridCacheZoom = -1;
3004
+ gridCacheCamX = -Infinity;
3005
+ gridCacheCamY = -Infinity;
3006
+ gridCacheWidth = 0;
3007
+ gridCacheHeight = 0;
3008
+ lastGridRef = null;
2768
3009
  constructor(deps) {
2769
3010
  this.canvasEl = deps.canvasEl;
2770
3011
  this.camera = deps.camera;
@@ -2827,6 +3068,29 @@ var RenderLoop = class {
2827
3068
  ctx.drawImage(cached, 0, 0);
2828
3069
  ctx.restore();
2829
3070
  }
3071
+ ensureGridCache(cssWidth, cssHeight, dpr) {
3072
+ if (this.gridCacheCanvas !== null && this.gridCacheWidth === cssWidth && this.gridCacheHeight === cssHeight) {
3073
+ return;
3074
+ }
3075
+ const physWidth = Math.round(cssWidth * dpr);
3076
+ const physHeight = Math.round(cssHeight * dpr);
3077
+ if (typeof OffscreenCanvas !== "undefined") {
3078
+ this.gridCacheCanvas = new OffscreenCanvas(
3079
+ physWidth,
3080
+ physHeight
3081
+ );
3082
+ } else if (typeof document !== "undefined") {
3083
+ const el = document.createElement("canvas");
3084
+ el.width = physWidth;
3085
+ el.height = physHeight;
3086
+ this.gridCacheCanvas = el;
3087
+ } else {
3088
+ this.gridCacheCanvas = null;
3089
+ this.gridCacheCtx = null;
3090
+ return;
3091
+ }
3092
+ this.gridCacheCtx = this.gridCacheCanvas.getContext("2d");
3093
+ }
2830
3094
  render() {
2831
3095
  const t0 = performance.now();
2832
3096
  const ctx = this.canvasEl.getContext("2d");
@@ -2889,9 +3153,6 @@ var RenderLoop = class {
2889
3153
  }
2890
3154
  group.push(element);
2891
3155
  }
2892
- for (const grid of gridElements) {
2893
- this.renderer.renderCanvasElement(ctx, grid);
2894
- }
2895
3156
  for (const [layerId, elements] of layerElements) {
2896
3157
  const isActiveDrawingLayer = layerId === this.activeDrawingLayerId;
2897
3158
  if (!this.layerCache.isDirty(layerId)) {
@@ -2920,13 +3181,53 @@ var RenderLoop = class {
2920
3181
  this.compositeLayerCache(ctx, layerId, dpr);
2921
3182
  }
2922
3183
  }
3184
+ if (gridElements.length > 0) {
3185
+ const gridT0 = performance.now();
3186
+ const gridRef = gridElements[0];
3187
+ const gridCacheHit = this.gridCacheCanvas !== null && currentZoom === this.gridCacheZoom && currentCamX === this.gridCacheCamX && currentCamY === this.gridCacheCamY && cssWidth === this.gridCacheWidth && cssHeight === this.gridCacheHeight && gridRef === this.lastGridRef;
3188
+ if (gridCacheHit) {
3189
+ ctx.save();
3190
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
3191
+ ctx.drawImage(this.gridCacheCanvas, 0, 0);
3192
+ ctx.restore();
3193
+ } else {
3194
+ this.ensureGridCache(cssWidth, cssHeight, dpr);
3195
+ if (this.gridCacheCtx && this.gridCacheCanvas) {
3196
+ const gc = this.gridCacheCtx;
3197
+ gc.clearRect(0, 0, this.gridCacheCanvas.width, this.gridCacheCanvas.height);
3198
+ gc.save();
3199
+ gc.scale(dpr, dpr);
3200
+ gc.translate(currentCamX, currentCamY);
3201
+ gc.scale(currentZoom, currentZoom);
3202
+ for (const grid of gridElements) {
3203
+ this.renderer.renderCanvasElement(gc, grid);
3204
+ }
3205
+ gc.restore();
3206
+ ctx.save();
3207
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
3208
+ ctx.drawImage(this.gridCacheCanvas, 0, 0);
3209
+ ctx.restore();
3210
+ } else {
3211
+ for (const grid of gridElements) {
3212
+ this.renderer.renderCanvasElement(ctx, grid);
3213
+ }
3214
+ }
3215
+ this.gridCacheZoom = currentZoom;
3216
+ this.gridCacheCamX = currentCamX;
3217
+ this.gridCacheCamY = currentCamY;
3218
+ this.gridCacheWidth = cssWidth;
3219
+ this.gridCacheHeight = cssHeight;
3220
+ this.lastGridRef = gridRef;
3221
+ }
3222
+ this.lastGridMs = performance.now() - gridT0;
3223
+ }
2923
3224
  const activeTool = this.toolManager.activeTool;
2924
3225
  if (activeTool?.renderOverlay) {
2925
3226
  activeTool.renderOverlay(ctx);
2926
3227
  }
2927
3228
  ctx.restore();
2928
3229
  ctx.restore();
2929
- this.stats.recordFrame(performance.now() - t0);
3230
+ this.stats.recordFrame(performance.now() - t0, this.lastGridMs);
2930
3231
  }
2931
3232
  };
2932
3233
 
@@ -3226,6 +3527,18 @@ var Viewport = class {
3226
3527
  this.historyRecorder.commit();
3227
3528
  this.requestRender();
3228
3529
  }
3530
+ getRenderStats() {
3531
+ return this.renderLoop.getStats();
3532
+ }
3533
+ logPerformance(intervalMs = 2e3) {
3534
+ const id = setInterval(() => {
3535
+ const s = this.getRenderStats();
3536
+ console.log(
3537
+ `[FieldNotes] fps=${s.fps} frame=${s.avgFrameMs}ms p95=${s.p95FrameMs}ms grid=${s.lastGridMs}ms`
3538
+ );
3539
+ }, intervalMs);
3540
+ return () => clearInterval(id);
3541
+ }
3229
3542
  destroy() {
3230
3543
  this.renderLoop.stop();
3231
3544
  this.interactMode.destroy();
@@ -3535,6 +3848,7 @@ var PencilTool = class {
3535
3848
  layerId: ctx.activeLayerId ?? ""
3536
3849
  });
3537
3850
  ctx.store.add(stroke);
3851
+ computeStrokeSegments(stroke);
3538
3852
  this.points = [];
3539
3853
  ctx.requestRender();
3540
3854
  }
@@ -4609,7 +4923,7 @@ var UpdateLayerCommand = class {
4609
4923
  };
4610
4924
 
4611
4925
  // src/index.ts
4612
- var VERSION = "0.8.7";
4926
+ var VERSION = "0.8.8";
4613
4927
  // Annotate the CommonJS export names for ESM import in node:
4614
4928
  0 && (module.exports = {
4615
4929
  AddElementCommand,