@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.js CHANGED
@@ -457,6 +457,13 @@ var Background = class {
457
457
  color;
458
458
  dotRadius;
459
459
  lineWidth;
460
+ cachedCanvas = null;
461
+ cachedCtx = null;
462
+ lastZoom = -1;
463
+ lastOffsetX = -Infinity;
464
+ lastOffsetY = -Infinity;
465
+ lastWidth = 0;
466
+ lastHeight = 0;
460
467
  constructor(options = {}) {
461
468
  this.pattern = options.pattern ?? DEFAULTS.pattern;
462
469
  this.spacing = options.spacing ?? DEFAULTS.spacing;
@@ -472,13 +479,69 @@ var Background = class {
472
479
  ctx.save();
473
480
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
474
481
  ctx.clearRect(0, 0, cssWidth, cssHeight);
482
+ if (this.pattern === "none") {
483
+ ctx.restore();
484
+ return;
485
+ }
486
+ const spacing = this.adaptSpacing(this.spacing, camera.zoom);
487
+ const keyZoom = camera.zoom;
488
+ const keyX = Math.floor(camera.position.x % spacing);
489
+ const keyY = Math.floor(camera.position.y % spacing);
490
+ if (this.cachedCanvas !== null && keyZoom === this.lastZoom && keyX === this.lastOffsetX && keyY === this.lastOffsetY && cssWidth === this.lastWidth && cssHeight === this.lastHeight) {
491
+ ctx.drawImage(this.cachedCanvas, 0, 0);
492
+ ctx.restore();
493
+ return;
494
+ }
495
+ this.ensureCachedCanvas(cssWidth, cssHeight, dpr);
496
+ if (this.cachedCtx === null) {
497
+ if (this.pattern === "dots") {
498
+ this.renderDots(ctx, camera, cssWidth, cssHeight);
499
+ } else if (this.pattern === "grid") {
500
+ this.renderGrid(ctx, camera, cssWidth, cssHeight);
501
+ }
502
+ ctx.restore();
503
+ return;
504
+ }
505
+ const offCtx = this.cachedCtx;
506
+ offCtx.clearRect(0, 0, cssWidth, cssHeight);
475
507
  if (this.pattern === "dots") {
476
- this.renderDots(ctx, camera, cssWidth, cssHeight);
508
+ this.renderDots(offCtx, camera, cssWidth, cssHeight);
477
509
  } else if (this.pattern === "grid") {
478
- this.renderGrid(ctx, camera, cssWidth, cssHeight);
479
- }
510
+ this.renderGrid(offCtx, camera, cssWidth, cssHeight);
511
+ }
512
+ this.lastZoom = keyZoom;
513
+ this.lastOffsetX = keyX;
514
+ this.lastOffsetY = keyY;
515
+ this.lastWidth = cssWidth;
516
+ this.lastHeight = cssHeight;
517
+ ctx.drawImage(this.cachedCanvas, 0, 0);
480
518
  ctx.restore();
481
519
  }
520
+ ensureCachedCanvas(cssWidth, cssHeight, dpr) {
521
+ if (this.cachedCanvas !== null && this.lastWidth === cssWidth && this.lastHeight === cssHeight) {
522
+ return;
523
+ }
524
+ const physWidth = Math.round(cssWidth * dpr);
525
+ const physHeight = Math.round(cssHeight * dpr);
526
+ if (typeof OffscreenCanvas !== "undefined") {
527
+ this.cachedCanvas = new OffscreenCanvas(physWidth, physHeight);
528
+ } else if (typeof document !== "undefined") {
529
+ const el = document.createElement("canvas");
530
+ el.width = physWidth;
531
+ el.height = physHeight;
532
+ this.cachedCanvas = el;
533
+ } else {
534
+ this.cachedCanvas = null;
535
+ this.cachedCtx = null;
536
+ return;
537
+ }
538
+ const offCtx = this.cachedCanvas.getContext("2d");
539
+ if (offCtx !== null) {
540
+ offCtx.scale(dpr, dpr);
541
+ }
542
+ this.cachedCtx = offCtx;
543
+ this.lastZoom = -1;
544
+ }
482
545
  adaptSpacing(baseSpacing, zoom) {
483
546
  let spacing = baseSpacing * zoom;
484
547
  while (spacing < MIN_PATTERN_SPACING) {
@@ -961,6 +1024,10 @@ var ElementStore = class {
961
1024
  const existing = this.elements.get(id);
962
1025
  if (!existing) return;
963
1026
  const updated = { ...existing, ...partial, id: existing.id, type: existing.type };
1027
+ if (updated.type === "arrow") {
1028
+ const arrow = updated;
1029
+ arrow.cachedControlPoint = getArrowControlPoint(arrow.from, arrow.to, arrow.bend);
1030
+ }
964
1031
  this.elements.set(id, updated);
965
1032
  const newBounds = getElementBounds(updated);
966
1033
  if (newBounds) {
@@ -1216,6 +1283,25 @@ function smoothToSegments(points) {
1216
1283
  return segments;
1217
1284
  }
1218
1285
 
1286
+ // src/elements/stroke-cache.ts
1287
+ var cache = /* @__PURE__ */ new WeakMap();
1288
+ function computeStrokeSegments(stroke) {
1289
+ const segments = smoothToSegments(stroke.points);
1290
+ const widths = [];
1291
+ for (const seg of segments) {
1292
+ const w = (pressureToWidth(seg.start.pressure, stroke.width) + pressureToWidth(seg.end.pressure, stroke.width)) / 2;
1293
+ widths.push(w);
1294
+ }
1295
+ const data = { segments, widths };
1296
+ cache.set(stroke, data);
1297
+ return data;
1298
+ }
1299
+ function getStrokeRenderData(stroke) {
1300
+ const cached = cache.get(stroke);
1301
+ if (cached) return cached;
1302
+ return computeStrokeSegments(stroke);
1303
+ }
1304
+
1219
1305
  // src/elements/grid-renderer.ts
1220
1306
  function getSquareGridLines(bounds, cellSize) {
1221
1307
  if (cellSize <= 0) return { verticals: [], horizontals: [] };
@@ -1233,58 +1319,6 @@ function getSquareGridLines(bounds, cellSize) {
1233
1319
  }
1234
1320
  return { verticals, horizontals };
1235
1321
  }
1236
- function getHexVertices(cx, cy, circumradius, orientation) {
1237
- const vertices = [];
1238
- const angleOffset = orientation === "pointy" ? -Math.PI / 2 : 0;
1239
- for (let i = 0; i < 6; i++) {
1240
- const angle = Math.PI / 3 * i + angleOffset;
1241
- vertices.push({
1242
- x: cx + circumradius * Math.cos(angle),
1243
- y: cy + circumradius * Math.sin(angle)
1244
- });
1245
- }
1246
- return vertices;
1247
- }
1248
- function getHexCenters(bounds, circumradius, orientation) {
1249
- if (circumradius <= 0) return [];
1250
- const centers = [];
1251
- if (orientation === "pointy") {
1252
- const hexW = Math.sqrt(3) * circumradius;
1253
- const hexH = 2 * circumradius;
1254
- const rowH = hexH * 0.75;
1255
- const startRow = Math.floor((bounds.minY - circumradius) / rowH);
1256
- const endRow = Math.ceil((bounds.maxY + circumradius) / rowH);
1257
- const startCol = Math.floor((bounds.minX - hexW) / hexW);
1258
- const endCol = Math.ceil((bounds.maxX + hexW) / hexW);
1259
- for (let row = startRow; row <= endRow; row++) {
1260
- const offsetX = row % 2 !== 0 ? hexW / 2 : 0;
1261
- for (let col = startCol; col <= endCol; col++) {
1262
- centers.push({
1263
- x: col * hexW + offsetX,
1264
- y: row * rowH
1265
- });
1266
- }
1267
- }
1268
- } else {
1269
- const hexW = 2 * circumradius;
1270
- const hexH = Math.sqrt(3) * circumradius;
1271
- const colW = hexW * 0.75;
1272
- const startCol = Math.floor((bounds.minX - circumradius) / colW);
1273
- const endCol = Math.ceil((bounds.maxX + circumradius) / colW);
1274
- const startRow = Math.floor((bounds.minY - hexH) / hexH);
1275
- const endRow = Math.ceil((bounds.maxY + hexH) / hexH);
1276
- for (let col = startCol; col <= endCol; col++) {
1277
- const offsetY = col % 2 !== 0 ? hexH / 2 : 0;
1278
- for (let row = startRow; row <= endRow; row++) {
1279
- centers.push({
1280
- x: col * colW,
1281
- y: row * hexH + offsetY
1282
- });
1283
- }
1284
- }
1285
- }
1286
- return centers;
1287
- }
1288
1322
  function renderSquareGrid(ctx, bounds, cellSize, strokeColor, strokeWidth, opacity) {
1289
1323
  if (cellSize <= 0) return;
1290
1324
  const { verticals, horizontals } = getSquareGridLines(bounds, cellSize);
@@ -1306,27 +1340,172 @@ function renderSquareGrid(ctx, bounds, cellSize, strokeColor, strokeWidth, opaci
1306
1340
  }
1307
1341
  function renderHexGrid(ctx, bounds, cellSize, orientation, strokeColor, strokeWidth, opacity) {
1308
1342
  if (cellSize <= 0) return;
1309
- const centers = getHexCenters(bounds, cellSize, orientation);
1343
+ const angleOffset = orientation === "pointy" ? -Math.PI / 2 : 0;
1344
+ const ox0 = cellSize * Math.cos(angleOffset);
1345
+ const oy0 = cellSize * Math.sin(angleOffset);
1346
+ const ox1 = cellSize * Math.cos(Math.PI / 3 + angleOffset);
1347
+ const oy1 = cellSize * Math.sin(Math.PI / 3 + angleOffset);
1348
+ const ox2 = cellSize * Math.cos(2 * Math.PI / 3 + angleOffset);
1349
+ const oy2 = cellSize * Math.sin(2 * Math.PI / 3 + angleOffset);
1350
+ const ox3 = cellSize * Math.cos(Math.PI + angleOffset);
1351
+ const oy3 = cellSize * Math.sin(Math.PI + angleOffset);
1352
+ const ox4 = cellSize * Math.cos(4 * Math.PI / 3 + angleOffset);
1353
+ const oy4 = cellSize * Math.sin(4 * Math.PI / 3 + angleOffset);
1354
+ const ox5 = cellSize * Math.cos(5 * Math.PI / 3 + angleOffset);
1355
+ const oy5 = cellSize * Math.sin(5 * Math.PI / 3 + angleOffset);
1310
1356
  ctx.save();
1311
1357
  ctx.strokeStyle = strokeColor;
1312
1358
  ctx.lineWidth = strokeWidth;
1313
1359
  ctx.globalAlpha = opacity;
1314
1360
  ctx.beginPath();
1315
- for (const center of centers) {
1316
- const verts = getHexVertices(center.x, center.y, cellSize, orientation);
1317
- const first = verts[0];
1318
- if (!first) continue;
1319
- ctx.moveTo(first.x, first.y);
1320
- for (let i = 1; i < verts.length; i++) {
1321
- const v = verts[i];
1322
- if (!v) continue;
1323
- ctx.lineTo(v.x, v.y);
1361
+ if (orientation === "pointy") {
1362
+ const hexW = Math.sqrt(3) * cellSize;
1363
+ const rowH = 1.5 * cellSize;
1364
+ const startRow = Math.floor((bounds.minY - cellSize) / rowH);
1365
+ const endRow = Math.ceil((bounds.maxY + cellSize) / rowH);
1366
+ const startCol = Math.floor((bounds.minX - hexW) / hexW);
1367
+ const endCol = Math.ceil((bounds.maxX + hexW) / hexW);
1368
+ for (let row = startRow; row <= endRow; row++) {
1369
+ const offX = row % 2 !== 0 ? hexW / 2 : 0;
1370
+ for (let col = startCol; col <= endCol; col++) {
1371
+ const cx = col * hexW + offX;
1372
+ const cy = row * rowH;
1373
+ ctx.moveTo(cx + ox0, cy + oy0);
1374
+ ctx.lineTo(cx + ox1, cy + oy1);
1375
+ ctx.lineTo(cx + ox2, cy + oy2);
1376
+ ctx.lineTo(cx + ox3, cy + oy3);
1377
+ ctx.lineTo(cx + ox4, cy + oy4);
1378
+ ctx.lineTo(cx + ox5, cy + oy5);
1379
+ ctx.closePath();
1380
+ }
1381
+ }
1382
+ } else {
1383
+ const hexH = Math.sqrt(3) * cellSize;
1384
+ const colW = 1.5 * cellSize;
1385
+ const startCol = Math.floor((bounds.minX - cellSize) / colW);
1386
+ const endCol = Math.ceil((bounds.maxX + cellSize) / colW);
1387
+ const startRow = Math.floor((bounds.minY - hexH) / hexH);
1388
+ const endRow = Math.ceil((bounds.maxY + hexH) / hexH);
1389
+ for (let col = startCol; col <= endCol; col++) {
1390
+ const offY = col % 2 !== 0 ? hexH / 2 : 0;
1391
+ for (let row = startRow; row <= endRow; row++) {
1392
+ const cx = col * colW;
1393
+ const cy = row * hexH + offY;
1394
+ ctx.moveTo(cx + ox0, cy + oy0);
1395
+ ctx.lineTo(cx + ox1, cy + oy1);
1396
+ ctx.lineTo(cx + ox2, cy + oy2);
1397
+ ctx.lineTo(cx + ox3, cy + oy3);
1398
+ ctx.lineTo(cx + ox4, cy + oy4);
1399
+ ctx.lineTo(cx + ox5, cy + oy5);
1400
+ ctx.closePath();
1401
+ }
1324
1402
  }
1325
- ctx.closePath();
1326
1403
  }
1327
1404
  ctx.stroke();
1328
1405
  ctx.restore();
1329
1406
  }
1407
+ function createHexGridTile(cellSize, orientation, strokeColor, strokeWidth, opacity, scale) {
1408
+ let tileW;
1409
+ let tileH;
1410
+ if (orientation === "pointy") {
1411
+ tileW = Math.sqrt(3) * cellSize;
1412
+ tileH = 3 * cellSize;
1413
+ } else {
1414
+ tileW = 3 * cellSize;
1415
+ tileH = Math.sqrt(3) * cellSize;
1416
+ }
1417
+ const pxW = Math.ceil(tileW * scale);
1418
+ const pxH = Math.ceil(tileH * scale);
1419
+ if (pxW <= 0 || pxH <= 0) return null;
1420
+ let canvas;
1421
+ if (typeof OffscreenCanvas !== "undefined") {
1422
+ canvas = new OffscreenCanvas(pxW, pxH);
1423
+ } else if (typeof document !== "undefined") {
1424
+ const el = document.createElement("canvas");
1425
+ el.width = pxW;
1426
+ el.height = pxH;
1427
+ canvas = el;
1428
+ } else {
1429
+ return null;
1430
+ }
1431
+ const tc = canvas.getContext("2d");
1432
+ if (!tc) return null;
1433
+ tc.scale(scale, scale);
1434
+ tc.beginPath();
1435
+ tc.rect(0, 0, tileW, tileH);
1436
+ tc.clip();
1437
+ const angleOffset = orientation === "pointy" ? -Math.PI / 2 : 0;
1438
+ const ox0 = cellSize * Math.cos(angleOffset);
1439
+ const oy0 = cellSize * Math.sin(angleOffset);
1440
+ const ox1 = cellSize * Math.cos(Math.PI / 3 + angleOffset);
1441
+ const oy1 = cellSize * Math.sin(Math.PI / 3 + angleOffset);
1442
+ const ox2 = cellSize * Math.cos(2 * Math.PI / 3 + angleOffset);
1443
+ const oy2 = cellSize * Math.sin(2 * Math.PI / 3 + angleOffset);
1444
+ const ox3 = cellSize * Math.cos(Math.PI + angleOffset);
1445
+ const oy3 = cellSize * Math.sin(Math.PI + angleOffset);
1446
+ const ox4 = cellSize * Math.cos(4 * Math.PI / 3 + angleOffset);
1447
+ const oy4 = cellSize * Math.sin(4 * Math.PI / 3 + angleOffset);
1448
+ const ox5 = cellSize * Math.cos(5 * Math.PI / 3 + angleOffset);
1449
+ const oy5 = cellSize * Math.sin(5 * Math.PI / 3 + angleOffset);
1450
+ tc.strokeStyle = strokeColor;
1451
+ tc.lineWidth = strokeWidth;
1452
+ tc.globalAlpha = opacity;
1453
+ tc.beginPath();
1454
+ if (orientation === "pointy") {
1455
+ const hexW = tileW;
1456
+ const rowH = 1.5 * cellSize;
1457
+ for (let row = -1; row <= 3; row++) {
1458
+ const offX = row % 2 !== 0 ? hexW / 2 : 0;
1459
+ for (let col = -1; col <= 1; col++) {
1460
+ const cx = col * hexW + offX;
1461
+ const cy = row * rowH;
1462
+ tc.moveTo(cx + ox0, cy + oy0);
1463
+ tc.lineTo(cx + ox1, cy + oy1);
1464
+ tc.lineTo(cx + ox2, cy + oy2);
1465
+ tc.lineTo(cx + ox3, cy + oy3);
1466
+ tc.lineTo(cx + ox4, cy + oy4);
1467
+ tc.lineTo(cx + ox5, cy + oy5);
1468
+ tc.closePath();
1469
+ }
1470
+ }
1471
+ } else {
1472
+ const hexH = tileH;
1473
+ const colW = 1.5 * cellSize;
1474
+ for (let col = -1; col <= 3; col++) {
1475
+ const offY = col % 2 !== 0 ? hexH / 2 : 0;
1476
+ for (let row = -1; row <= 1; row++) {
1477
+ const cx = col * colW;
1478
+ const cy = row * hexH + offY;
1479
+ tc.moveTo(cx + ox0, cy + oy0);
1480
+ tc.lineTo(cx + ox1, cy + oy1);
1481
+ tc.lineTo(cx + ox2, cy + oy2);
1482
+ tc.lineTo(cx + ox3, cy + oy3);
1483
+ tc.lineTo(cx + ox4, cy + oy4);
1484
+ tc.lineTo(cx + ox5, cy + oy5);
1485
+ tc.closePath();
1486
+ }
1487
+ }
1488
+ }
1489
+ tc.stroke();
1490
+ return { canvas, tileW, tileH };
1491
+ }
1492
+ function renderHexGridTiled(ctx, bounds, cellSize, tile, scale) {
1493
+ const pattern = ctx.createPattern(tile.canvas, "repeat");
1494
+ if (!pattern) return;
1495
+ const mat = new DOMMatrix();
1496
+ mat.scaleSelf(1 / scale, 1 / scale);
1497
+ pattern.setTransform(mat);
1498
+ ctx.save();
1499
+ ctx.fillStyle = pattern;
1500
+ const pad = cellSize * 2;
1501
+ ctx.fillRect(
1502
+ bounds.minX - pad,
1503
+ bounds.minY - pad,
1504
+ bounds.maxX - bounds.minX + pad * 2,
1505
+ bounds.maxY - bounds.minY + pad * 2
1506
+ );
1507
+ ctx.restore();
1508
+ }
1330
1509
 
1331
1510
  // src/elements/element-renderer.ts
1332
1511
  var DOM_ELEMENT_TYPES = /* @__PURE__ */ new Set(["note", "html", "text"]);
@@ -1338,6 +1517,8 @@ var ElementRenderer = class {
1338
1517
  onImageLoad = null;
1339
1518
  camera = null;
1340
1519
  canvasSize = null;
1520
+ hexTileCache = null;
1521
+ hexTileCacheKey = "";
1341
1522
  setStore(store) {
1342
1523
  this.store = store;
1343
1524
  }
@@ -1380,9 +1561,11 @@ var ElementRenderer = class {
1380
1561
  ctx.lineCap = "round";
1381
1562
  ctx.lineJoin = "round";
1382
1563
  ctx.globalAlpha = stroke.opacity;
1383
- const segments = smoothToSegments(stroke.points);
1384
- for (const seg of segments) {
1385
- const w = (pressureToWidth(seg.start.pressure, stroke.width) + pressureToWidth(seg.end.pressure, stroke.width)) / 2;
1564
+ const { segments, widths } = getStrokeRenderData(stroke);
1565
+ for (let i = 0; i < segments.length; i++) {
1566
+ const seg = segments[i];
1567
+ const w = widths[i];
1568
+ if (!seg || w === void 0) continue;
1386
1569
  ctx.lineWidth = w;
1387
1570
  ctx.beginPath();
1388
1571
  ctx.moveTo(seg.start.x, seg.start.y);
@@ -1403,7 +1586,7 @@ var ElementRenderer = class {
1403
1586
  ctx.beginPath();
1404
1587
  ctx.moveTo(visualFrom.x, visualFrom.y);
1405
1588
  if (arrow.bend !== 0) {
1406
- const cp = getArrowControlPoint(arrow.from, arrow.to, arrow.bend);
1589
+ const cp = arrow.cachedControlPoint ?? getArrowControlPoint(arrow.from, arrow.to, arrow.bend);
1407
1590
  ctx.quadraticCurveTo(cp.x, cp.y, visualTo.x, visualTo.y);
1408
1591
  } else {
1409
1592
  ctx.lineTo(visualTo.x, visualTo.y);
@@ -1521,15 +1704,29 @@ var ElementRenderer = class {
1521
1704
  maxY: bottomRight.y
1522
1705
  };
1523
1706
  if (grid.gridType === "hex") {
1524
- renderHexGrid(
1525
- ctx,
1526
- bounds,
1707
+ const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
1708
+ const scale = cam.zoom * dpr;
1709
+ const tile = this.getHexTile(
1527
1710
  grid.cellSize,
1528
1711
  grid.hexOrientation,
1529
1712
  grid.strokeColor,
1530
1713
  grid.strokeWidth,
1531
- grid.opacity
1714
+ grid.opacity,
1715
+ scale
1532
1716
  );
1717
+ if (tile) {
1718
+ renderHexGridTiled(ctx, bounds, grid.cellSize, tile, scale);
1719
+ } else {
1720
+ renderHexGrid(
1721
+ ctx,
1722
+ bounds,
1723
+ grid.cellSize,
1724
+ grid.hexOrientation,
1725
+ grid.strokeColor,
1726
+ grid.strokeWidth,
1727
+ grid.opacity
1728
+ );
1729
+ }
1533
1730
  } else {
1534
1731
  renderSquareGrid(
1535
1732
  ctx,
@@ -1544,15 +1741,45 @@ var ElementRenderer = class {
1544
1741
  renderImage(ctx, image) {
1545
1742
  const img = this.getImage(image.src);
1546
1743
  if (!img) return;
1547
- ctx.drawImage(img, image.position.x, image.position.y, image.size.w, image.size.h);
1744
+ ctx.drawImage(
1745
+ img,
1746
+ image.position.x,
1747
+ image.position.y,
1748
+ image.size.w,
1749
+ image.size.h
1750
+ );
1751
+ }
1752
+ getHexTile(cellSize, orientation, strokeColor, strokeWidth, opacity, scale) {
1753
+ const key = `${cellSize}:${orientation}:${strokeColor}:${strokeWidth}:${opacity}:${scale}`;
1754
+ if (this.hexTileCacheKey === key && this.hexTileCache) {
1755
+ return this.hexTileCache;
1756
+ }
1757
+ const tile = createHexGridTile(cellSize, orientation, strokeColor, strokeWidth, opacity, scale);
1758
+ if (tile) {
1759
+ this.hexTileCache = tile;
1760
+ this.hexTileCacheKey = key;
1761
+ }
1762
+ return tile;
1548
1763
  }
1549
1764
  getImage(src) {
1550
1765
  const cached = this.imageCache.get(src);
1551
- if (cached) return cached.complete ? cached : null;
1766
+ if (cached) {
1767
+ if (cached instanceof HTMLImageElement) return cached.complete ? cached : null;
1768
+ return cached;
1769
+ }
1552
1770
  const img = new Image();
1553
1771
  img.src = src;
1554
1772
  this.imageCache.set(src, img);
1555
- img.onload = () => this.onImageLoad?.();
1773
+ img.onload = () => {
1774
+ this.onImageLoad?.();
1775
+ if (typeof createImageBitmap !== "undefined") {
1776
+ createImageBitmap(img).then((bitmap) => {
1777
+ this.imageCache.set(src, bitmap);
1778
+ this.onImageLoad?.();
1779
+ }).catch(() => {
1780
+ });
1781
+ }
1782
+ };
1556
1783
  return null;
1557
1784
  }
1558
1785
  };
@@ -1928,6 +2155,7 @@ function createNote(input) {
1928
2155
  };
1929
2156
  }
1930
2157
  function createArrow(input) {
2158
+ const bend = input.bend ?? 0;
1931
2159
  const result = {
1932
2160
  id: createId("arrow"),
1933
2161
  type: "arrow",
@@ -1937,9 +2165,10 @@ function createArrow(input) {
1937
2165
  layerId: input.layerId ?? "",
1938
2166
  from: input.from,
1939
2167
  to: input.to,
1940
- bend: input.bend ?? 0,
2168
+ bend,
1941
2169
  color: input.color ?? "#000000",
1942
- width: input.width ?? 2
2170
+ width: input.width ?? 2,
2171
+ cachedControlPoint: getArrowControlPoint(input.from, input.to, bend)
1943
2172
  };
1944
2173
  if (input.fromBinding) result.fromBinding = input.fromBinding;
1945
2174
  if (input.toBinding) result.toBinding = input.toBinding;
@@ -2190,19 +2419,19 @@ function loadImages(elements) {
2190
2419
  const imageElements = elements.filter(
2191
2420
  (el) => el.type === "image" && "src" in el
2192
2421
  );
2193
- const cache = /* @__PURE__ */ new Map();
2194
- if (imageElements.length === 0) return Promise.resolve(cache);
2422
+ const cache2 = /* @__PURE__ */ new Map();
2423
+ if (imageElements.length === 0) return Promise.resolve(cache2);
2195
2424
  return new Promise((resolve) => {
2196
2425
  let remaining = imageElements.length;
2197
2426
  const done = () => {
2198
2427
  remaining--;
2199
- if (remaining <= 0) resolve(cache);
2428
+ if (remaining <= 0) resolve(cache2);
2200
2429
  };
2201
2430
  for (const el of imageElements) {
2202
2431
  const img = new Image();
2203
2432
  img.crossOrigin = "anonymous";
2204
2433
  img.onload = () => {
2205
- cache.set(el.id, img);
2434
+ cache2.set(el.id, img);
2206
2435
  done();
2207
2436
  };
2208
2437
  img.onerror = done;
@@ -2632,17 +2861,19 @@ var SAMPLE_SIZE = 60;
2632
2861
  var RenderStats = class {
2633
2862
  frameTimes = [];
2634
2863
  frameCount = 0;
2635
- recordFrame(durationMs) {
2864
+ _lastGridMs = 0;
2865
+ recordFrame(durationMs, gridMs) {
2636
2866
  this.frameCount++;
2637
2867
  this.frameTimes.push(durationMs);
2638
2868
  if (this.frameTimes.length > SAMPLE_SIZE) {
2639
2869
  this.frameTimes.shift();
2640
2870
  }
2871
+ if (gridMs !== void 0) this._lastGridMs = gridMs;
2641
2872
  }
2642
2873
  getSnapshot() {
2643
2874
  const times = this.frameTimes;
2644
2875
  if (times.length === 0) {
2645
- return { fps: 0, avgFrameMs: 0, p95FrameMs: 0, lastFrameMs: 0, frameCount: 0 };
2876
+ return { fps: 0, avgFrameMs: 0, p95FrameMs: 0, lastFrameMs: 0, lastGridMs: 0, frameCount: 0 };
2646
2877
  }
2647
2878
  const avg = times.reduce((a, b) => a + b, 0) / times.length;
2648
2879
  const sorted = [...times].sort((a, b) => a - b);
@@ -2653,6 +2884,7 @@ var RenderStats = class {
2653
2884
  avgFrameMs: Math.round(avg * 100) / 100,
2654
2885
  p95FrameMs: Math.round((sorted[p95Index] ?? 0) * 100) / 100,
2655
2886
  lastFrameMs: Math.round(lastFrame * 100) / 100,
2887
+ lastGridMs: Math.round(this._lastGridMs * 100) / 100,
2656
2888
  frameCount: this.frameCount
2657
2889
  };
2658
2890
  }
@@ -2680,6 +2912,15 @@ var RenderLoop = class {
2680
2912
  lastCamX;
2681
2913
  lastCamY;
2682
2914
  stats = new RenderStats();
2915
+ lastGridMs = 0;
2916
+ gridCacheCanvas = null;
2917
+ gridCacheCtx = null;
2918
+ gridCacheZoom = -1;
2919
+ gridCacheCamX = -Infinity;
2920
+ gridCacheCamY = -Infinity;
2921
+ gridCacheWidth = 0;
2922
+ gridCacheHeight = 0;
2923
+ lastGridRef = null;
2683
2924
  constructor(deps) {
2684
2925
  this.canvasEl = deps.canvasEl;
2685
2926
  this.camera = deps.camera;
@@ -2742,6 +2983,29 @@ var RenderLoop = class {
2742
2983
  ctx.drawImage(cached, 0, 0);
2743
2984
  ctx.restore();
2744
2985
  }
2986
+ ensureGridCache(cssWidth, cssHeight, dpr) {
2987
+ if (this.gridCacheCanvas !== null && this.gridCacheWidth === cssWidth && this.gridCacheHeight === cssHeight) {
2988
+ return;
2989
+ }
2990
+ const physWidth = Math.round(cssWidth * dpr);
2991
+ const physHeight = Math.round(cssHeight * dpr);
2992
+ if (typeof OffscreenCanvas !== "undefined") {
2993
+ this.gridCacheCanvas = new OffscreenCanvas(
2994
+ physWidth,
2995
+ physHeight
2996
+ );
2997
+ } else if (typeof document !== "undefined") {
2998
+ const el = document.createElement("canvas");
2999
+ el.width = physWidth;
3000
+ el.height = physHeight;
3001
+ this.gridCacheCanvas = el;
3002
+ } else {
3003
+ this.gridCacheCanvas = null;
3004
+ this.gridCacheCtx = null;
3005
+ return;
3006
+ }
3007
+ this.gridCacheCtx = this.gridCacheCanvas.getContext("2d");
3008
+ }
2745
3009
  render() {
2746
3010
  const t0 = performance.now();
2747
3011
  const ctx = this.canvasEl.getContext("2d");
@@ -2804,9 +3068,6 @@ var RenderLoop = class {
2804
3068
  }
2805
3069
  group.push(element);
2806
3070
  }
2807
- for (const grid of gridElements) {
2808
- this.renderer.renderCanvasElement(ctx, grid);
2809
- }
2810
3071
  for (const [layerId, elements] of layerElements) {
2811
3072
  const isActiveDrawingLayer = layerId === this.activeDrawingLayerId;
2812
3073
  if (!this.layerCache.isDirty(layerId)) {
@@ -2835,13 +3096,53 @@ var RenderLoop = class {
2835
3096
  this.compositeLayerCache(ctx, layerId, dpr);
2836
3097
  }
2837
3098
  }
3099
+ if (gridElements.length > 0) {
3100
+ const gridT0 = performance.now();
3101
+ const gridRef = gridElements[0];
3102
+ const gridCacheHit = this.gridCacheCanvas !== null && currentZoom === this.gridCacheZoom && currentCamX === this.gridCacheCamX && currentCamY === this.gridCacheCamY && cssWidth === this.gridCacheWidth && cssHeight === this.gridCacheHeight && gridRef === this.lastGridRef;
3103
+ if (gridCacheHit) {
3104
+ ctx.save();
3105
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
3106
+ ctx.drawImage(this.gridCacheCanvas, 0, 0);
3107
+ ctx.restore();
3108
+ } else {
3109
+ this.ensureGridCache(cssWidth, cssHeight, dpr);
3110
+ if (this.gridCacheCtx && this.gridCacheCanvas) {
3111
+ const gc = this.gridCacheCtx;
3112
+ gc.clearRect(0, 0, this.gridCacheCanvas.width, this.gridCacheCanvas.height);
3113
+ gc.save();
3114
+ gc.scale(dpr, dpr);
3115
+ gc.translate(currentCamX, currentCamY);
3116
+ gc.scale(currentZoom, currentZoom);
3117
+ for (const grid of gridElements) {
3118
+ this.renderer.renderCanvasElement(gc, grid);
3119
+ }
3120
+ gc.restore();
3121
+ ctx.save();
3122
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
3123
+ ctx.drawImage(this.gridCacheCanvas, 0, 0);
3124
+ ctx.restore();
3125
+ } else {
3126
+ for (const grid of gridElements) {
3127
+ this.renderer.renderCanvasElement(ctx, grid);
3128
+ }
3129
+ }
3130
+ this.gridCacheZoom = currentZoom;
3131
+ this.gridCacheCamX = currentCamX;
3132
+ this.gridCacheCamY = currentCamY;
3133
+ this.gridCacheWidth = cssWidth;
3134
+ this.gridCacheHeight = cssHeight;
3135
+ this.lastGridRef = gridRef;
3136
+ }
3137
+ this.lastGridMs = performance.now() - gridT0;
3138
+ }
2838
3139
  const activeTool = this.toolManager.activeTool;
2839
3140
  if (activeTool?.renderOverlay) {
2840
3141
  activeTool.renderOverlay(ctx);
2841
3142
  }
2842
3143
  ctx.restore();
2843
3144
  ctx.restore();
2844
- this.stats.recordFrame(performance.now() - t0);
3145
+ this.stats.recordFrame(performance.now() - t0, this.lastGridMs);
2845
3146
  }
2846
3147
  };
2847
3148
 
@@ -3141,6 +3442,18 @@ var Viewport = class {
3141
3442
  this.historyRecorder.commit();
3142
3443
  this.requestRender();
3143
3444
  }
3445
+ getRenderStats() {
3446
+ return this.renderLoop.getStats();
3447
+ }
3448
+ logPerformance(intervalMs = 2e3) {
3449
+ const id = setInterval(() => {
3450
+ const s = this.getRenderStats();
3451
+ console.log(
3452
+ `[FieldNotes] fps=${s.fps} frame=${s.avgFrameMs}ms p95=${s.p95FrameMs}ms grid=${s.lastGridMs}ms`
3453
+ );
3454
+ }, intervalMs);
3455
+ return () => clearInterval(id);
3456
+ }
3144
3457
  destroy() {
3145
3458
  this.renderLoop.stop();
3146
3459
  this.interactMode.destroy();
@@ -3450,6 +3763,7 @@ var PencilTool = class {
3450
3763
  layerId: ctx.activeLayerId ?? ""
3451
3764
  });
3452
3765
  ctx.store.add(stroke);
3766
+ computeStrokeSegments(stroke);
3453
3767
  this.points = [];
3454
3768
  ctx.requestRender();
3455
3769
  }
@@ -4524,7 +4838,7 @@ var UpdateLayerCommand = class {
4524
4838
  };
4525
4839
 
4526
4840
  // src/index.ts
4527
- var VERSION = "0.8.7";
4841
+ var VERSION = "0.8.8";
4528
4842
  export {
4529
4843
  AddElementCommand,
4530
4844
  ArrowTool,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fieldnotes/core",
3
- "version": "0.8.7",
3
+ "version": "0.8.9",
4
4
  "description": "Vanilla TypeScript infinite canvas engine",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",