@fieldnotes/core 0.8.8 → 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 +278 -73
- package/dist/index.d.cts +14 -8
- package/dist/index.d.ts +14 -8
- package/dist/index.js +278 -73
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1404,58 +1404,6 @@ function getSquareGridLines(bounds, cellSize) {
|
|
|
1404
1404
|
}
|
|
1405
1405
|
return { verticals, horizontals };
|
|
1406
1406
|
}
|
|
1407
|
-
function getHexVertices(cx, cy, circumradius, orientation) {
|
|
1408
|
-
const vertices = [];
|
|
1409
|
-
const angleOffset = orientation === "pointy" ? -Math.PI / 2 : 0;
|
|
1410
|
-
for (let i = 0; i < 6; i++) {
|
|
1411
|
-
const angle = Math.PI / 3 * i + angleOffset;
|
|
1412
|
-
vertices.push({
|
|
1413
|
-
x: cx + circumradius * Math.cos(angle),
|
|
1414
|
-
y: cy + circumradius * Math.sin(angle)
|
|
1415
|
-
});
|
|
1416
|
-
}
|
|
1417
|
-
return vertices;
|
|
1418
|
-
}
|
|
1419
|
-
function getHexCenters(bounds, circumradius, orientation) {
|
|
1420
|
-
if (circumradius <= 0) return [];
|
|
1421
|
-
const centers = [];
|
|
1422
|
-
if (orientation === "pointy") {
|
|
1423
|
-
const hexW = Math.sqrt(3) * circumradius;
|
|
1424
|
-
const hexH = 2 * circumradius;
|
|
1425
|
-
const rowH = hexH * 0.75;
|
|
1426
|
-
const startRow = Math.floor((bounds.minY - circumradius) / rowH);
|
|
1427
|
-
const endRow = Math.ceil((bounds.maxY + circumradius) / rowH);
|
|
1428
|
-
const startCol = Math.floor((bounds.minX - hexW) / hexW);
|
|
1429
|
-
const endCol = Math.ceil((bounds.maxX + hexW) / hexW);
|
|
1430
|
-
for (let row = startRow; row <= endRow; row++) {
|
|
1431
|
-
const offsetX = row % 2 !== 0 ? hexW / 2 : 0;
|
|
1432
|
-
for (let col = startCol; col <= endCol; col++) {
|
|
1433
|
-
centers.push({
|
|
1434
|
-
x: col * hexW + offsetX,
|
|
1435
|
-
y: row * rowH
|
|
1436
|
-
});
|
|
1437
|
-
}
|
|
1438
|
-
}
|
|
1439
|
-
} else {
|
|
1440
|
-
const hexW = 2 * circumradius;
|
|
1441
|
-
const hexH = Math.sqrt(3) * circumradius;
|
|
1442
|
-
const colW = hexW * 0.75;
|
|
1443
|
-
const startCol = Math.floor((bounds.minX - circumradius) / colW);
|
|
1444
|
-
const endCol = Math.ceil((bounds.maxX + circumradius) / colW);
|
|
1445
|
-
const startRow = Math.floor((bounds.minY - hexH) / hexH);
|
|
1446
|
-
const endRow = Math.ceil((bounds.maxY + hexH) / hexH);
|
|
1447
|
-
for (let col = startCol; col <= endCol; col++) {
|
|
1448
|
-
const offsetY = col % 2 !== 0 ? hexH / 2 : 0;
|
|
1449
|
-
for (let row = startRow; row <= endRow; row++) {
|
|
1450
|
-
centers.push({
|
|
1451
|
-
x: col * colW,
|
|
1452
|
-
y: row * hexH + offsetY
|
|
1453
|
-
});
|
|
1454
|
-
}
|
|
1455
|
-
}
|
|
1456
|
-
}
|
|
1457
|
-
return centers;
|
|
1458
|
-
}
|
|
1459
1407
|
function renderSquareGrid(ctx, bounds, cellSize, strokeColor, strokeWidth, opacity) {
|
|
1460
1408
|
if (cellSize <= 0) return;
|
|
1461
1409
|
const { verticals, horizontals } = getSquareGridLines(bounds, cellSize);
|
|
@@ -1477,27 +1425,172 @@ function renderSquareGrid(ctx, bounds, cellSize, strokeColor, strokeWidth, opaci
|
|
|
1477
1425
|
}
|
|
1478
1426
|
function renderHexGrid(ctx, bounds, cellSize, orientation, strokeColor, strokeWidth, opacity) {
|
|
1479
1427
|
if (cellSize <= 0) return;
|
|
1480
|
-
const
|
|
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);
|
|
1481
1441
|
ctx.save();
|
|
1482
1442
|
ctx.strokeStyle = strokeColor;
|
|
1483
1443
|
ctx.lineWidth = strokeWidth;
|
|
1484
1444
|
ctx.globalAlpha = opacity;
|
|
1485
1445
|
ctx.beginPath();
|
|
1486
|
-
|
|
1487
|
-
const
|
|
1488
|
-
const
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
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
|
+
}
|
|
1495
1487
|
}
|
|
1496
|
-
ctx.closePath();
|
|
1497
1488
|
}
|
|
1498
1489
|
ctx.stroke();
|
|
1499
1490
|
ctx.restore();
|
|
1500
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
|
+
}
|
|
1501
1594
|
|
|
1502
1595
|
// src/elements/element-renderer.ts
|
|
1503
1596
|
var DOM_ELEMENT_TYPES = /* @__PURE__ */ new Set(["note", "html", "text"]);
|
|
@@ -1509,6 +1602,8 @@ var ElementRenderer = class {
|
|
|
1509
1602
|
onImageLoad = null;
|
|
1510
1603
|
camera = null;
|
|
1511
1604
|
canvasSize = null;
|
|
1605
|
+
hexTileCache = null;
|
|
1606
|
+
hexTileCacheKey = "";
|
|
1512
1607
|
setStore(store) {
|
|
1513
1608
|
this.store = store;
|
|
1514
1609
|
}
|
|
@@ -1694,15 +1789,29 @@ var ElementRenderer = class {
|
|
|
1694
1789
|
maxY: bottomRight.y
|
|
1695
1790
|
};
|
|
1696
1791
|
if (grid.gridType === "hex") {
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1792
|
+
const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
|
|
1793
|
+
const scale = cam.zoom * dpr;
|
|
1794
|
+
const tile = this.getHexTile(
|
|
1700
1795
|
grid.cellSize,
|
|
1701
1796
|
grid.hexOrientation,
|
|
1702
1797
|
grid.strokeColor,
|
|
1703
1798
|
grid.strokeWidth,
|
|
1704
|
-
grid.opacity
|
|
1799
|
+
grid.opacity,
|
|
1800
|
+
scale
|
|
1705
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
|
+
}
|
|
1706
1815
|
} else {
|
|
1707
1816
|
renderSquareGrid(
|
|
1708
1817
|
ctx,
|
|
@@ -1725,6 +1834,18 @@ var ElementRenderer = class {
|
|
|
1725
1834
|
image.size.h
|
|
1726
1835
|
);
|
|
1727
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;
|
|
1848
|
+
}
|
|
1728
1849
|
getImage(src) {
|
|
1729
1850
|
const cached = this.imageCache.get(src);
|
|
1730
1851
|
if (cached) {
|
|
@@ -2825,17 +2946,19 @@ var SAMPLE_SIZE = 60;
|
|
|
2825
2946
|
var RenderStats = class {
|
|
2826
2947
|
frameTimes = [];
|
|
2827
2948
|
frameCount = 0;
|
|
2828
|
-
|
|
2949
|
+
_lastGridMs = 0;
|
|
2950
|
+
recordFrame(durationMs, gridMs) {
|
|
2829
2951
|
this.frameCount++;
|
|
2830
2952
|
this.frameTimes.push(durationMs);
|
|
2831
2953
|
if (this.frameTimes.length > SAMPLE_SIZE) {
|
|
2832
2954
|
this.frameTimes.shift();
|
|
2833
2955
|
}
|
|
2956
|
+
if (gridMs !== void 0) this._lastGridMs = gridMs;
|
|
2834
2957
|
}
|
|
2835
2958
|
getSnapshot() {
|
|
2836
2959
|
const times = this.frameTimes;
|
|
2837
2960
|
if (times.length === 0) {
|
|
2838
|
-
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 };
|
|
2839
2962
|
}
|
|
2840
2963
|
const avg = times.reduce((a, b) => a + b, 0) / times.length;
|
|
2841
2964
|
const sorted = [...times].sort((a, b) => a - b);
|
|
@@ -2846,6 +2969,7 @@ var RenderStats = class {
|
|
|
2846
2969
|
avgFrameMs: Math.round(avg * 100) / 100,
|
|
2847
2970
|
p95FrameMs: Math.round((sorted[p95Index] ?? 0) * 100) / 100,
|
|
2848
2971
|
lastFrameMs: Math.round(lastFrame * 100) / 100,
|
|
2972
|
+
lastGridMs: Math.round(this._lastGridMs * 100) / 100,
|
|
2849
2973
|
frameCount: this.frameCount
|
|
2850
2974
|
};
|
|
2851
2975
|
}
|
|
@@ -2873,6 +2997,15 @@ var RenderLoop = class {
|
|
|
2873
2997
|
lastCamX;
|
|
2874
2998
|
lastCamY;
|
|
2875
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;
|
|
2876
3009
|
constructor(deps) {
|
|
2877
3010
|
this.canvasEl = deps.canvasEl;
|
|
2878
3011
|
this.camera = deps.camera;
|
|
@@ -2935,6 +3068,29 @@ var RenderLoop = class {
|
|
|
2935
3068
|
ctx.drawImage(cached, 0, 0);
|
|
2936
3069
|
ctx.restore();
|
|
2937
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
|
+
}
|
|
2938
3094
|
render() {
|
|
2939
3095
|
const t0 = performance.now();
|
|
2940
3096
|
const ctx = this.canvasEl.getContext("2d");
|
|
@@ -2997,9 +3153,6 @@ var RenderLoop = class {
|
|
|
2997
3153
|
}
|
|
2998
3154
|
group.push(element);
|
|
2999
3155
|
}
|
|
3000
|
-
for (const grid of gridElements) {
|
|
3001
|
-
this.renderer.renderCanvasElement(ctx, grid);
|
|
3002
|
-
}
|
|
3003
3156
|
for (const [layerId, elements] of layerElements) {
|
|
3004
3157
|
const isActiveDrawingLayer = layerId === this.activeDrawingLayerId;
|
|
3005
3158
|
if (!this.layerCache.isDirty(layerId)) {
|
|
@@ -3028,13 +3181,53 @@ var RenderLoop = class {
|
|
|
3028
3181
|
this.compositeLayerCache(ctx, layerId, dpr);
|
|
3029
3182
|
}
|
|
3030
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
|
+
}
|
|
3031
3224
|
const activeTool = this.toolManager.activeTool;
|
|
3032
3225
|
if (activeTool?.renderOverlay) {
|
|
3033
3226
|
activeTool.renderOverlay(ctx);
|
|
3034
3227
|
}
|
|
3035
3228
|
ctx.restore();
|
|
3036
3229
|
ctx.restore();
|
|
3037
|
-
this.stats.recordFrame(performance.now() - t0);
|
|
3230
|
+
this.stats.recordFrame(performance.now() - t0, this.lastGridMs);
|
|
3038
3231
|
}
|
|
3039
3232
|
};
|
|
3040
3233
|
|
|
@@ -3334,6 +3527,18 @@ var Viewport = class {
|
|
|
3334
3527
|
this.historyRecorder.commit();
|
|
3335
3528
|
this.requestRender();
|
|
3336
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
|
+
}
|
|
3337
3542
|
destroy() {
|
|
3338
3543
|
this.renderLoop.stop();
|
|
3339
3544
|
this.interactMode.destroy();
|
package/dist/index.d.cts
CHANGED
|
@@ -440,6 +440,15 @@ interface ExportImageOptions {
|
|
|
440
440
|
}
|
|
441
441
|
declare function exportImage(store: ElementStore, options?: ExportImageOptions, layerManager?: LayerManager): Promise<Blob | null>;
|
|
442
442
|
|
|
443
|
+
interface RenderStatsSnapshot {
|
|
444
|
+
fps: number;
|
|
445
|
+
avgFrameMs: number;
|
|
446
|
+
p95FrameMs: number;
|
|
447
|
+
lastFrameMs: number;
|
|
448
|
+
lastGridMs: number;
|
|
449
|
+
frameCount: number;
|
|
450
|
+
}
|
|
451
|
+
|
|
443
452
|
interface ViewportOptions {
|
|
444
453
|
camera?: CameraOptions;
|
|
445
454
|
background?: BackgroundOptions;
|
|
@@ -504,6 +513,8 @@ declare class Viewport {
|
|
|
504
513
|
}): string;
|
|
505
514
|
updateGrid(updates: Partial<Pick<GridElement, 'gridType' | 'hexOrientation' | 'cellSize' | 'strokeColor' | 'strokeWidth' | 'opacity'>>): void;
|
|
506
515
|
removeGrid(): void;
|
|
516
|
+
getRenderStats(): RenderStatsSnapshot;
|
|
517
|
+
logPerformance(intervalMs?: number): () => void;
|
|
507
518
|
destroy(): void;
|
|
508
519
|
private startEditingElement;
|
|
509
520
|
private onTextEditStop;
|
|
@@ -521,20 +532,14 @@ declare class Viewport {
|
|
|
521
532
|
private observeResize;
|
|
522
533
|
}
|
|
523
534
|
|
|
524
|
-
interface RenderStatsSnapshot {
|
|
525
|
-
fps: number;
|
|
526
|
-
avgFrameMs: number;
|
|
527
|
-
p95FrameMs: number;
|
|
528
|
-
lastFrameMs: number;
|
|
529
|
-
frameCount: number;
|
|
530
|
-
}
|
|
531
|
-
|
|
532
535
|
declare class ElementRenderer {
|
|
533
536
|
private store;
|
|
534
537
|
private imageCache;
|
|
535
538
|
private onImageLoad;
|
|
536
539
|
private camera;
|
|
537
540
|
private canvasSize;
|
|
541
|
+
private hexTileCache;
|
|
542
|
+
private hexTileCacheKey;
|
|
538
543
|
setStore(store: ElementStore): void;
|
|
539
544
|
setOnImageLoad(callback: () => void): void;
|
|
540
545
|
setCamera(camera: Camera): void;
|
|
@@ -550,6 +555,7 @@ declare class ElementRenderer {
|
|
|
550
555
|
private strokeShapePath;
|
|
551
556
|
private renderGrid;
|
|
552
557
|
private renderImage;
|
|
558
|
+
private getHexTile;
|
|
553
559
|
private getImage;
|
|
554
560
|
}
|
|
555
561
|
|
package/dist/index.d.ts
CHANGED
|
@@ -440,6 +440,15 @@ interface ExportImageOptions {
|
|
|
440
440
|
}
|
|
441
441
|
declare function exportImage(store: ElementStore, options?: ExportImageOptions, layerManager?: LayerManager): Promise<Blob | null>;
|
|
442
442
|
|
|
443
|
+
interface RenderStatsSnapshot {
|
|
444
|
+
fps: number;
|
|
445
|
+
avgFrameMs: number;
|
|
446
|
+
p95FrameMs: number;
|
|
447
|
+
lastFrameMs: number;
|
|
448
|
+
lastGridMs: number;
|
|
449
|
+
frameCount: number;
|
|
450
|
+
}
|
|
451
|
+
|
|
443
452
|
interface ViewportOptions {
|
|
444
453
|
camera?: CameraOptions;
|
|
445
454
|
background?: BackgroundOptions;
|
|
@@ -504,6 +513,8 @@ declare class Viewport {
|
|
|
504
513
|
}): string;
|
|
505
514
|
updateGrid(updates: Partial<Pick<GridElement, 'gridType' | 'hexOrientation' | 'cellSize' | 'strokeColor' | 'strokeWidth' | 'opacity'>>): void;
|
|
506
515
|
removeGrid(): void;
|
|
516
|
+
getRenderStats(): RenderStatsSnapshot;
|
|
517
|
+
logPerformance(intervalMs?: number): () => void;
|
|
507
518
|
destroy(): void;
|
|
508
519
|
private startEditingElement;
|
|
509
520
|
private onTextEditStop;
|
|
@@ -521,20 +532,14 @@ declare class Viewport {
|
|
|
521
532
|
private observeResize;
|
|
522
533
|
}
|
|
523
534
|
|
|
524
|
-
interface RenderStatsSnapshot {
|
|
525
|
-
fps: number;
|
|
526
|
-
avgFrameMs: number;
|
|
527
|
-
p95FrameMs: number;
|
|
528
|
-
lastFrameMs: number;
|
|
529
|
-
frameCount: number;
|
|
530
|
-
}
|
|
531
|
-
|
|
532
535
|
declare class ElementRenderer {
|
|
533
536
|
private store;
|
|
534
537
|
private imageCache;
|
|
535
538
|
private onImageLoad;
|
|
536
539
|
private camera;
|
|
537
540
|
private canvasSize;
|
|
541
|
+
private hexTileCache;
|
|
542
|
+
private hexTileCacheKey;
|
|
538
543
|
setStore(store: ElementStore): void;
|
|
539
544
|
setOnImageLoad(callback: () => void): void;
|
|
540
545
|
setCamera(camera: Camera): void;
|
|
@@ -550,6 +555,7 @@ declare class ElementRenderer {
|
|
|
550
555
|
private strokeShapePath;
|
|
551
556
|
private renderGrid;
|
|
552
557
|
private renderImage;
|
|
558
|
+
private getHexTile;
|
|
553
559
|
private getImage;
|
|
554
560
|
}
|
|
555
561
|
|
package/dist/index.js
CHANGED
|
@@ -1319,58 +1319,6 @@ function getSquareGridLines(bounds, cellSize) {
|
|
|
1319
1319
|
}
|
|
1320
1320
|
return { verticals, horizontals };
|
|
1321
1321
|
}
|
|
1322
|
-
function getHexVertices(cx, cy, circumradius, orientation) {
|
|
1323
|
-
const vertices = [];
|
|
1324
|
-
const angleOffset = orientation === "pointy" ? -Math.PI / 2 : 0;
|
|
1325
|
-
for (let i = 0; i < 6; i++) {
|
|
1326
|
-
const angle = Math.PI / 3 * i + angleOffset;
|
|
1327
|
-
vertices.push({
|
|
1328
|
-
x: cx + circumradius * Math.cos(angle),
|
|
1329
|
-
y: cy + circumradius * Math.sin(angle)
|
|
1330
|
-
});
|
|
1331
|
-
}
|
|
1332
|
-
return vertices;
|
|
1333
|
-
}
|
|
1334
|
-
function getHexCenters(bounds, circumradius, orientation) {
|
|
1335
|
-
if (circumradius <= 0) return [];
|
|
1336
|
-
const centers = [];
|
|
1337
|
-
if (orientation === "pointy") {
|
|
1338
|
-
const hexW = Math.sqrt(3) * circumradius;
|
|
1339
|
-
const hexH = 2 * circumradius;
|
|
1340
|
-
const rowH = hexH * 0.75;
|
|
1341
|
-
const startRow = Math.floor((bounds.minY - circumradius) / rowH);
|
|
1342
|
-
const endRow = Math.ceil((bounds.maxY + circumradius) / rowH);
|
|
1343
|
-
const startCol = Math.floor((bounds.minX - hexW) / hexW);
|
|
1344
|
-
const endCol = Math.ceil((bounds.maxX + hexW) / hexW);
|
|
1345
|
-
for (let row = startRow; row <= endRow; row++) {
|
|
1346
|
-
const offsetX = row % 2 !== 0 ? hexW / 2 : 0;
|
|
1347
|
-
for (let col = startCol; col <= endCol; col++) {
|
|
1348
|
-
centers.push({
|
|
1349
|
-
x: col * hexW + offsetX,
|
|
1350
|
-
y: row * rowH
|
|
1351
|
-
});
|
|
1352
|
-
}
|
|
1353
|
-
}
|
|
1354
|
-
} else {
|
|
1355
|
-
const hexW = 2 * circumradius;
|
|
1356
|
-
const hexH = Math.sqrt(3) * circumradius;
|
|
1357
|
-
const colW = hexW * 0.75;
|
|
1358
|
-
const startCol = Math.floor((bounds.minX - circumradius) / colW);
|
|
1359
|
-
const endCol = Math.ceil((bounds.maxX + circumradius) / colW);
|
|
1360
|
-
const startRow = Math.floor((bounds.minY - hexH) / hexH);
|
|
1361
|
-
const endRow = Math.ceil((bounds.maxY + hexH) / hexH);
|
|
1362
|
-
for (let col = startCol; col <= endCol; col++) {
|
|
1363
|
-
const offsetY = col % 2 !== 0 ? hexH / 2 : 0;
|
|
1364
|
-
for (let row = startRow; row <= endRow; row++) {
|
|
1365
|
-
centers.push({
|
|
1366
|
-
x: col * colW,
|
|
1367
|
-
y: row * hexH + offsetY
|
|
1368
|
-
});
|
|
1369
|
-
}
|
|
1370
|
-
}
|
|
1371
|
-
}
|
|
1372
|
-
return centers;
|
|
1373
|
-
}
|
|
1374
1322
|
function renderSquareGrid(ctx, bounds, cellSize, strokeColor, strokeWidth, opacity) {
|
|
1375
1323
|
if (cellSize <= 0) return;
|
|
1376
1324
|
const { verticals, horizontals } = getSquareGridLines(bounds, cellSize);
|
|
@@ -1392,27 +1340,172 @@ function renderSquareGrid(ctx, bounds, cellSize, strokeColor, strokeWidth, opaci
|
|
|
1392
1340
|
}
|
|
1393
1341
|
function renderHexGrid(ctx, bounds, cellSize, orientation, strokeColor, strokeWidth, opacity) {
|
|
1394
1342
|
if (cellSize <= 0) return;
|
|
1395
|
-
const
|
|
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);
|
|
1396
1356
|
ctx.save();
|
|
1397
1357
|
ctx.strokeStyle = strokeColor;
|
|
1398
1358
|
ctx.lineWidth = strokeWidth;
|
|
1399
1359
|
ctx.globalAlpha = opacity;
|
|
1400
1360
|
ctx.beginPath();
|
|
1401
|
-
|
|
1402
|
-
const
|
|
1403
|
-
const
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
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
|
+
}
|
|
1410
1402
|
}
|
|
1411
|
-
ctx.closePath();
|
|
1412
1403
|
}
|
|
1413
1404
|
ctx.stroke();
|
|
1414
1405
|
ctx.restore();
|
|
1415
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
|
+
}
|
|
1416
1509
|
|
|
1417
1510
|
// src/elements/element-renderer.ts
|
|
1418
1511
|
var DOM_ELEMENT_TYPES = /* @__PURE__ */ new Set(["note", "html", "text"]);
|
|
@@ -1424,6 +1517,8 @@ var ElementRenderer = class {
|
|
|
1424
1517
|
onImageLoad = null;
|
|
1425
1518
|
camera = null;
|
|
1426
1519
|
canvasSize = null;
|
|
1520
|
+
hexTileCache = null;
|
|
1521
|
+
hexTileCacheKey = "";
|
|
1427
1522
|
setStore(store) {
|
|
1428
1523
|
this.store = store;
|
|
1429
1524
|
}
|
|
@@ -1609,15 +1704,29 @@ var ElementRenderer = class {
|
|
|
1609
1704
|
maxY: bottomRight.y
|
|
1610
1705
|
};
|
|
1611
1706
|
if (grid.gridType === "hex") {
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1707
|
+
const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
|
|
1708
|
+
const scale = cam.zoom * dpr;
|
|
1709
|
+
const tile = this.getHexTile(
|
|
1615
1710
|
grid.cellSize,
|
|
1616
1711
|
grid.hexOrientation,
|
|
1617
1712
|
grid.strokeColor,
|
|
1618
1713
|
grid.strokeWidth,
|
|
1619
|
-
grid.opacity
|
|
1714
|
+
grid.opacity,
|
|
1715
|
+
scale
|
|
1620
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
|
+
}
|
|
1621
1730
|
} else {
|
|
1622
1731
|
renderSquareGrid(
|
|
1623
1732
|
ctx,
|
|
@@ -1640,6 +1749,18 @@ var ElementRenderer = class {
|
|
|
1640
1749
|
image.size.h
|
|
1641
1750
|
);
|
|
1642
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;
|
|
1763
|
+
}
|
|
1643
1764
|
getImage(src) {
|
|
1644
1765
|
const cached = this.imageCache.get(src);
|
|
1645
1766
|
if (cached) {
|
|
@@ -2740,17 +2861,19 @@ var SAMPLE_SIZE = 60;
|
|
|
2740
2861
|
var RenderStats = class {
|
|
2741
2862
|
frameTimes = [];
|
|
2742
2863
|
frameCount = 0;
|
|
2743
|
-
|
|
2864
|
+
_lastGridMs = 0;
|
|
2865
|
+
recordFrame(durationMs, gridMs) {
|
|
2744
2866
|
this.frameCount++;
|
|
2745
2867
|
this.frameTimes.push(durationMs);
|
|
2746
2868
|
if (this.frameTimes.length > SAMPLE_SIZE) {
|
|
2747
2869
|
this.frameTimes.shift();
|
|
2748
2870
|
}
|
|
2871
|
+
if (gridMs !== void 0) this._lastGridMs = gridMs;
|
|
2749
2872
|
}
|
|
2750
2873
|
getSnapshot() {
|
|
2751
2874
|
const times = this.frameTimes;
|
|
2752
2875
|
if (times.length === 0) {
|
|
2753
|
-
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 };
|
|
2754
2877
|
}
|
|
2755
2878
|
const avg = times.reduce((a, b) => a + b, 0) / times.length;
|
|
2756
2879
|
const sorted = [...times].sort((a, b) => a - b);
|
|
@@ -2761,6 +2884,7 @@ var RenderStats = class {
|
|
|
2761
2884
|
avgFrameMs: Math.round(avg * 100) / 100,
|
|
2762
2885
|
p95FrameMs: Math.round((sorted[p95Index] ?? 0) * 100) / 100,
|
|
2763
2886
|
lastFrameMs: Math.round(lastFrame * 100) / 100,
|
|
2887
|
+
lastGridMs: Math.round(this._lastGridMs * 100) / 100,
|
|
2764
2888
|
frameCount: this.frameCount
|
|
2765
2889
|
};
|
|
2766
2890
|
}
|
|
@@ -2788,6 +2912,15 @@ var RenderLoop = class {
|
|
|
2788
2912
|
lastCamX;
|
|
2789
2913
|
lastCamY;
|
|
2790
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;
|
|
2791
2924
|
constructor(deps) {
|
|
2792
2925
|
this.canvasEl = deps.canvasEl;
|
|
2793
2926
|
this.camera = deps.camera;
|
|
@@ -2850,6 +2983,29 @@ var RenderLoop = class {
|
|
|
2850
2983
|
ctx.drawImage(cached, 0, 0);
|
|
2851
2984
|
ctx.restore();
|
|
2852
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
|
+
}
|
|
2853
3009
|
render() {
|
|
2854
3010
|
const t0 = performance.now();
|
|
2855
3011
|
const ctx = this.canvasEl.getContext("2d");
|
|
@@ -2912,9 +3068,6 @@ var RenderLoop = class {
|
|
|
2912
3068
|
}
|
|
2913
3069
|
group.push(element);
|
|
2914
3070
|
}
|
|
2915
|
-
for (const grid of gridElements) {
|
|
2916
|
-
this.renderer.renderCanvasElement(ctx, grid);
|
|
2917
|
-
}
|
|
2918
3071
|
for (const [layerId, elements] of layerElements) {
|
|
2919
3072
|
const isActiveDrawingLayer = layerId === this.activeDrawingLayerId;
|
|
2920
3073
|
if (!this.layerCache.isDirty(layerId)) {
|
|
@@ -2943,13 +3096,53 @@ var RenderLoop = class {
|
|
|
2943
3096
|
this.compositeLayerCache(ctx, layerId, dpr);
|
|
2944
3097
|
}
|
|
2945
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
|
+
}
|
|
2946
3139
|
const activeTool = this.toolManager.activeTool;
|
|
2947
3140
|
if (activeTool?.renderOverlay) {
|
|
2948
3141
|
activeTool.renderOverlay(ctx);
|
|
2949
3142
|
}
|
|
2950
3143
|
ctx.restore();
|
|
2951
3144
|
ctx.restore();
|
|
2952
|
-
this.stats.recordFrame(performance.now() - t0);
|
|
3145
|
+
this.stats.recordFrame(performance.now() - t0, this.lastGridMs);
|
|
2953
3146
|
}
|
|
2954
3147
|
};
|
|
2955
3148
|
|
|
@@ -3249,6 +3442,18 @@ var Viewport = class {
|
|
|
3249
3442
|
this.historyRecorder.commit();
|
|
3250
3443
|
this.requestRender();
|
|
3251
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
|
+
}
|
|
3252
3457
|
destroy() {
|
|
3253
3458
|
this.renderLoop.stop();
|
|
3254
3459
|
this.interactMode.destroy();
|