@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 +404 -90
- package/dist/index.d.cts +25 -9
- package/dist/index.d.ts +25 -9
- package/dist/index.js +404 -90
- package/package.json +1 -1
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(
|
|
508
|
+
this.renderDots(offCtx, camera, cssWidth, cssHeight);
|
|
477
509
|
} else if (this.pattern === "grid") {
|
|
478
|
-
this.renderGrid(
|
|
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
|
|
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
|
-
|
|
1316
|
-
const
|
|
1317
|
-
const
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
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 =
|
|
1384
|
-
for (
|
|
1385
|
-
const
|
|
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
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
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(
|
|
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)
|
|
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 = () =>
|
|
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
|
|
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
|
|
2194
|
-
if (imageElements.length === 0) return Promise.resolve(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
4841
|
+
var VERSION = "0.8.8";
|
|
4528
4842
|
export {
|
|
4529
4843
|
AddElementCommand,
|
|
4530
4844
|
ArrowTool,
|