@fieldnotes/core 0.46.1 → 0.48.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +434 -79
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10 -3
- package/dist/index.d.ts +10 -3
- package/dist/index.js +433 -79
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -69,6 +69,7 @@ __export(index_exports, {
|
|
|
69
69
|
getHexCellsInCone: () => getHexCellsInCone,
|
|
70
70
|
getHexCellsInLine: () => getHexCellsInLine,
|
|
71
71
|
getHexCellsInRadius: () => getHexCellsInRadius,
|
|
72
|
+
getHexCellsInRectangle: () => getHexCellsInRectangle,
|
|
72
73
|
getHexCellsInSquare: () => getHexCellsInSquare,
|
|
73
74
|
getHexDistance: () => getHexDistance,
|
|
74
75
|
isNearBezier: () => isNearBezier,
|
|
@@ -983,6 +984,26 @@ function getTemplateBounds(el) {
|
|
|
983
984
|
const maxY = Math.max(y0, y1, y2, y3);
|
|
984
985
|
return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
|
|
985
986
|
}
|
|
987
|
+
case "rectangle": {
|
|
988
|
+
const halfW = (el.width ?? 0) / 2;
|
|
989
|
+
const cos = Math.cos(el.angle);
|
|
990
|
+
const sin = Math.sin(el.angle);
|
|
991
|
+
const perpX = -sin * halfW;
|
|
992
|
+
const perpY = cos * halfW;
|
|
993
|
+
const x0 = cx + perpX;
|
|
994
|
+
const y0 = cy + perpY;
|
|
995
|
+
const x1 = cx + r * cos + perpX;
|
|
996
|
+
const y1 = cy + r * sin + perpY;
|
|
997
|
+
const x2 = cx + r * cos - perpX;
|
|
998
|
+
const y2 = cy + r * sin - perpY;
|
|
999
|
+
const x3 = cx - perpX;
|
|
1000
|
+
const y3 = cy - perpY;
|
|
1001
|
+
const minX = Math.min(x0, x1, x2, x3);
|
|
1002
|
+
const minY = Math.min(y0, y1, y2, y3);
|
|
1003
|
+
const maxX = Math.max(x0, x1, x2, x3);
|
|
1004
|
+
const maxY = Math.max(y0, y1, y2, y3);
|
|
1005
|
+
return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
|
|
1006
|
+
}
|
|
986
1007
|
}
|
|
987
1008
|
}
|
|
988
1009
|
function transferStrokeBounds(prev, next) {
|
|
@@ -3286,6 +3307,46 @@ function getHexCellsInLine(center2, angle, radiusCells, cellSize, orientation) {
|
|
|
3286
3307
|
}
|
|
3287
3308
|
return cells;
|
|
3288
3309
|
}
|
|
3310
|
+
function getHexCellsInRectangle(center2, angle, lengthCells, widthCells, cellSize, orientation) {
|
|
3311
|
+
const nLen = Math.round(lengthCells);
|
|
3312
|
+
const wCells = Math.max(1, Math.round(widthCells));
|
|
3313
|
+
const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
|
|
3314
|
+
const cube = offsetToCube(off.col, off.row, orientation);
|
|
3315
|
+
const centerPixel = offsetToPixel(off.col, off.row, cellSize, orientation);
|
|
3316
|
+
if (nLen <= 0) return [centerPixel];
|
|
3317
|
+
const vertexOffset = orientation === "pointy" ? Math.PI / 6 : 0;
|
|
3318
|
+
const step = Math.PI / 3;
|
|
3319
|
+
const snappedAngle = Math.round((angle - vertexOffset) / step) * step + vertexOffset;
|
|
3320
|
+
const cos = Math.cos(snappedAngle);
|
|
3321
|
+
const sin = Math.sin(snappedAngle);
|
|
3322
|
+
const snapUnit = Math.sqrt(3) * cellSize;
|
|
3323
|
+
const lineLength = nLen * snapUnit;
|
|
3324
|
+
const halfWidth = wCells * snapUnit / 2 + 1e-6;
|
|
3325
|
+
const iterM = nLen + Math.ceil(wCells / 2) + 1;
|
|
3326
|
+
const cells = [];
|
|
3327
|
+
for (let dq = -iterM; dq <= iterM; dq++) {
|
|
3328
|
+
const rMin = Math.max(-iterM, -dq - iterM);
|
|
3329
|
+
const rMax = Math.min(iterM, -dq + iterM);
|
|
3330
|
+
for (let dr = rMin; dr <= rMax; dr++) {
|
|
3331
|
+
const absQ = cube.q + dq;
|
|
3332
|
+
const absR = cube.r + dr;
|
|
3333
|
+
const pixel = offsetToPixel(
|
|
3334
|
+
cubeToOffset(absQ, absR, orientation).col,
|
|
3335
|
+
cubeToOffset(absQ, absR, orientation).row,
|
|
3336
|
+
cellSize,
|
|
3337
|
+
orientation
|
|
3338
|
+
);
|
|
3339
|
+
const dx = pixel.x - centerPixel.x;
|
|
3340
|
+
const dy = pixel.y - centerPixel.y;
|
|
3341
|
+
const along = dx * cos + dy * sin;
|
|
3342
|
+
const perp = Math.abs(-dx * sin + dy * cos);
|
|
3343
|
+
if (along >= -snapUnit * 0.1 && along <= lineLength + snapUnit * 0.1 && perp <= halfWidth) {
|
|
3344
|
+
cells.push(pixel);
|
|
3345
|
+
}
|
|
3346
|
+
}
|
|
3347
|
+
}
|
|
3348
|
+
return cells;
|
|
3349
|
+
}
|
|
3289
3350
|
function getHexCellsInSquare(center2, radiusCells, cellSize, orientation) {
|
|
3290
3351
|
const n2 = Math.round(radiusCells);
|
|
3291
3352
|
const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
|
|
@@ -3324,6 +3385,48 @@ function drawHexPath(ctx, cx, cy, cellSize, orientation) {
|
|
|
3324
3385
|
ctx.closePath();
|
|
3325
3386
|
}
|
|
3326
3387
|
|
|
3388
|
+
// src/elements/renderers/template-measure.ts
|
|
3389
|
+
function renderTemplateFeetLabel(ctx, p) {
|
|
3390
|
+
if (p.feet <= 0) return;
|
|
3391
|
+
const directional = p.templateShape === "cone" || p.templateShape === "line" || p.templateShape === "rectangle";
|
|
3392
|
+
const dir = directional ? p.angle : 0;
|
|
3393
|
+
const length = p.templateShape === "square" ? p.radius / 2 : p.radius;
|
|
3394
|
+
const cos = Math.cos(dir);
|
|
3395
|
+
const sin = Math.sin(dir);
|
|
3396
|
+
const start = p.position;
|
|
3397
|
+
const end = { x: start.x + length * cos, y: start.y + length * sin };
|
|
3398
|
+
const midX = start.x + length / 2 * cos;
|
|
3399
|
+
const midY = start.y + length / 2 * sin;
|
|
3400
|
+
ctx.save();
|
|
3401
|
+
ctx.globalAlpha = 1;
|
|
3402
|
+
ctx.beginPath();
|
|
3403
|
+
ctx.setLineDash([4, 4]);
|
|
3404
|
+
ctx.strokeStyle = p.color;
|
|
3405
|
+
ctx.lineWidth = 1.5;
|
|
3406
|
+
ctx.moveTo(start.x, start.y);
|
|
3407
|
+
ctx.lineTo(end.x, end.y);
|
|
3408
|
+
ctx.stroke();
|
|
3409
|
+
ctx.setLineDash([]);
|
|
3410
|
+
const label = `${Math.round(p.feet)} ft`;
|
|
3411
|
+
const fontSize = Math.max(10, Math.min(14, p.radius * 0.15));
|
|
3412
|
+
ctx.font = `bold ${fontSize}px system-ui, sans-serif`;
|
|
3413
|
+
ctx.textAlign = "center";
|
|
3414
|
+
ctx.textBaseline = "bottom";
|
|
3415
|
+
const textY = midY - 4;
|
|
3416
|
+
const metrics = ctx.measureText(label);
|
|
3417
|
+
const padX = 4;
|
|
3418
|
+
const padY = 2;
|
|
3419
|
+
const textW = metrics.width + padX * 2;
|
|
3420
|
+
const textH = fontSize + padY * 2;
|
|
3421
|
+
ctx.fillStyle = "rgba(255, 255, 255, 0.85)";
|
|
3422
|
+
ctx.beginPath();
|
|
3423
|
+
ctx.roundRect(midX - textW / 2, textY - textH, textW, textH, 3);
|
|
3424
|
+
ctx.fill();
|
|
3425
|
+
ctx.fillStyle = p.color;
|
|
3426
|
+
ctx.fillText(label, midX, textY - padY);
|
|
3427
|
+
ctx.restore();
|
|
3428
|
+
}
|
|
3429
|
+
|
|
3327
3430
|
// src/elements/renderers/template-renderer.ts
|
|
3328
3431
|
function renderTemplate(ctx, template, store) {
|
|
3329
3432
|
const grid = store?.getElementsByType("grid")[0];
|
|
@@ -3347,9 +3450,6 @@ function renderGeometricTemplate(ctx, template) {
|
|
|
3347
3450
|
ctx.arc(cx, cy, r, 0, Math.PI * 2);
|
|
3348
3451
|
ctx.fill();
|
|
3349
3452
|
ctx.stroke();
|
|
3350
|
-
if (template.radiusFeet != null && template.radiusFeet > 0) {
|
|
3351
|
-
renderRadiusMarker(ctx, cx, cy, r, template.radiusFeet);
|
|
3352
|
-
}
|
|
3353
3453
|
break;
|
|
3354
3454
|
case "square":
|
|
3355
3455
|
ctx.fillRect(cx - r / 2, cy - r / 2, r, r);
|
|
@@ -3381,6 +3481,32 @@ function renderGeometricTemplate(ctx, template) {
|
|
|
3381
3481
|
ctx.stroke();
|
|
3382
3482
|
break;
|
|
3383
3483
|
}
|
|
3484
|
+
case "rectangle": {
|
|
3485
|
+
const halfW = (template.width ?? 0) / 2;
|
|
3486
|
+
const cos = Math.cos(template.angle);
|
|
3487
|
+
const sin = Math.sin(template.angle);
|
|
3488
|
+
const perpX = -sin * halfW;
|
|
3489
|
+
const perpY = cos * halfW;
|
|
3490
|
+
ctx.beginPath();
|
|
3491
|
+
ctx.moveTo(cx + perpX, cy + perpY);
|
|
3492
|
+
ctx.lineTo(cx + r * cos + perpX, cy + r * sin + perpY);
|
|
3493
|
+
ctx.lineTo(cx + r * cos - perpX, cy + r * sin - perpY);
|
|
3494
|
+
ctx.lineTo(cx - perpX, cy - perpY);
|
|
3495
|
+
ctx.closePath();
|
|
3496
|
+
ctx.fill();
|
|
3497
|
+
ctx.stroke();
|
|
3498
|
+
break;
|
|
3499
|
+
}
|
|
3500
|
+
}
|
|
3501
|
+
if (template.radiusFeet != null && template.radiusFeet > 0) {
|
|
3502
|
+
renderTemplateFeetLabel(ctx, {
|
|
3503
|
+
position: template.position,
|
|
3504
|
+
radius: template.radius,
|
|
3505
|
+
angle: template.angle,
|
|
3506
|
+
templateShape: template.templateShape,
|
|
3507
|
+
feet: template.radiusFeet,
|
|
3508
|
+
color: template.strokeColor
|
|
3509
|
+
});
|
|
3384
3510
|
}
|
|
3385
3511
|
ctx.restore();
|
|
3386
3512
|
}
|
|
@@ -3402,6 +3528,18 @@ function renderHexTemplate(ctx, template, cellSize, orientation) {
|
|
|
3402
3528
|
case "square":
|
|
3403
3529
|
cells = getHexCellsInSquare(center2, radiusCells, cellSize, orientation);
|
|
3404
3530
|
break;
|
|
3531
|
+
case "rectangle": {
|
|
3532
|
+
const widthCells = (template.width ?? 0) / snapUnit;
|
|
3533
|
+
cells = getHexCellsInRectangle(
|
|
3534
|
+
center2,
|
|
3535
|
+
template.angle,
|
|
3536
|
+
radiusCells,
|
|
3537
|
+
widthCells,
|
|
3538
|
+
cellSize,
|
|
3539
|
+
orientation
|
|
3540
|
+
);
|
|
3541
|
+
break;
|
|
3542
|
+
}
|
|
3405
3543
|
}
|
|
3406
3544
|
ctx.save();
|
|
3407
3545
|
ctx.globalAlpha = template.opacity;
|
|
@@ -3428,44 +3566,18 @@ function renderHexTemplate(ctx, template, cellSize, orientation) {
|
|
|
3428
3566
|
ctx.lineWidth = template.strokeWidth;
|
|
3429
3567
|
ctx.stroke();
|
|
3430
3568
|
}
|
|
3431
|
-
if (template.
|
|
3432
|
-
|
|
3433
|
-
|
|
3569
|
+
if (template.radiusFeet != null && template.radiusFeet > 0) {
|
|
3570
|
+
renderTemplateFeetLabel(ctx, {
|
|
3571
|
+
position: template.position,
|
|
3572
|
+
radius: template.radius,
|
|
3573
|
+
angle: template.angle,
|
|
3574
|
+
templateShape: template.templateShape,
|
|
3575
|
+
feet: template.radiusFeet,
|
|
3576
|
+
color: template.strokeColor
|
|
3577
|
+
});
|
|
3434
3578
|
}
|
|
3435
3579
|
ctx.restore();
|
|
3436
3580
|
}
|
|
3437
|
-
function renderRadiusMarker(ctx, cx, cy, r, feet) {
|
|
3438
|
-
const markerColor = ctx.strokeStyle;
|
|
3439
|
-
ctx.save();
|
|
3440
|
-
ctx.globalAlpha = 1;
|
|
3441
|
-
ctx.beginPath();
|
|
3442
|
-
ctx.setLineDash([4, 4]);
|
|
3443
|
-
ctx.strokeStyle = markerColor;
|
|
3444
|
-
ctx.lineWidth = 1.5;
|
|
3445
|
-
ctx.moveTo(cx, cy);
|
|
3446
|
-
ctx.lineTo(cx + r, cy);
|
|
3447
|
-
ctx.stroke();
|
|
3448
|
-
ctx.setLineDash([]);
|
|
3449
|
-
const label = `${Math.round(feet)} ft`;
|
|
3450
|
-
const fontSize = Math.max(10, Math.min(14, r * 0.15));
|
|
3451
|
-
ctx.font = `bold ${fontSize}px system-ui, sans-serif`;
|
|
3452
|
-
ctx.textAlign = "center";
|
|
3453
|
-
ctx.textBaseline = "bottom";
|
|
3454
|
-
const textX = cx + r / 2;
|
|
3455
|
-
const textY = cy - 4;
|
|
3456
|
-
const metrics = ctx.measureText(label);
|
|
3457
|
-
const padX = 4;
|
|
3458
|
-
const padY = 2;
|
|
3459
|
-
const textW = metrics.width + padX * 2;
|
|
3460
|
-
const textH = fontSize + padY * 2;
|
|
3461
|
-
ctx.fillStyle = "rgba(255, 255, 255, 0.85)";
|
|
3462
|
-
ctx.beginPath();
|
|
3463
|
-
ctx.roundRect(textX - textW / 2, textY - textH, textW, textH, 3);
|
|
3464
|
-
ctx.fill();
|
|
3465
|
-
ctx.fillStyle = markerColor;
|
|
3466
|
-
ctx.fillText(label, textX, textY - padY);
|
|
3467
|
-
ctx.restore();
|
|
3468
|
-
}
|
|
3469
3581
|
|
|
3470
3582
|
// src/elements/grid-renderer.ts
|
|
3471
3583
|
function getSquareGridLines(bounds, cellSize) {
|
|
@@ -4004,7 +4116,8 @@ function createTemplate(input) {
|
|
|
4004
4116
|
opacity: input.opacity ?? 0.6,
|
|
4005
4117
|
feetPerCell: input.feetPerCell,
|
|
4006
4118
|
radiusFeet: input.radiusFeet,
|
|
4007
|
-
...input.renderStyle !== void 0 ? { renderStyle: input.renderStyle } : {}
|
|
4119
|
+
...input.renderStyle !== void 0 ? { renderStyle: input.renderStyle } : {},
|
|
4120
|
+
...input.width !== void 0 ? { width: input.width } : {}
|
|
4008
4121
|
};
|
|
4009
4122
|
}
|
|
4010
4123
|
|
|
@@ -5601,6 +5714,20 @@ function emitGeometricTemplate(t) {
|
|
|
5601
5714
|
].map(([px, py]) => `${n(px ?? 0)},${n(py ?? 0)}`).join(" ");
|
|
5602
5715
|
return `<polygon points="${pts}" ${attrs} />`;
|
|
5603
5716
|
}
|
|
5717
|
+
case "rectangle": {
|
|
5718
|
+
const halfW = (t.width ?? 0) / 2;
|
|
5719
|
+
const cos = Math.cos(t.angle);
|
|
5720
|
+
const sin = Math.sin(t.angle);
|
|
5721
|
+
const perpX = -sin * halfW;
|
|
5722
|
+
const perpY = cos * halfW;
|
|
5723
|
+
const pts = [
|
|
5724
|
+
[cx + perpX, cy + perpY],
|
|
5725
|
+
[cx + r * cos + perpX, cy + r * sin + perpY],
|
|
5726
|
+
[cx + r * cos - perpX, cy + r * sin - perpY],
|
|
5727
|
+
[cx - perpX, cy - perpY]
|
|
5728
|
+
].map(([px, py]) => `${n(px ?? 0)},${n(py ?? 0)}`).join(" ");
|
|
5729
|
+
return `<polygon points="${pts}" ${attrs} />`;
|
|
5730
|
+
}
|
|
5604
5731
|
}
|
|
5605
5732
|
}
|
|
5606
5733
|
function emitHexTemplate(t, grid) {
|
|
@@ -5623,6 +5750,18 @@ function emitHexTemplate(t, grid) {
|
|
|
5623
5750
|
case "square":
|
|
5624
5751
|
cells = getHexCellsInSquare(center2, radiusCells, cellSize, orientation);
|
|
5625
5752
|
break;
|
|
5753
|
+
case "rectangle": {
|
|
5754
|
+
const widthCells = (t.width ?? 0) / snapUnit;
|
|
5755
|
+
cells = getHexCellsInRectangle(
|
|
5756
|
+
center2,
|
|
5757
|
+
t.angle,
|
|
5758
|
+
radiusCells,
|
|
5759
|
+
widthCells,
|
|
5760
|
+
cellSize,
|
|
5761
|
+
orientation
|
|
5762
|
+
);
|
|
5763
|
+
break;
|
|
5764
|
+
}
|
|
5626
5765
|
}
|
|
5627
5766
|
let d = "";
|
|
5628
5767
|
for (const cell of cells) {
|
|
@@ -8311,6 +8450,21 @@ function getOverlayLayout(el, zoom) {
|
|
|
8311
8450
|
const rotateHandle = rotatePoint(topMid, center2, angle);
|
|
8312
8451
|
return { center: center2, corners, rotateHandle, angle };
|
|
8313
8452
|
}
|
|
8453
|
+
function templateAimKnob(el, zoom) {
|
|
8454
|
+
if (el.type !== "template") return null;
|
|
8455
|
+
if (el.templateShape !== "cone" && el.templateShape !== "line" && el.templateShape !== "rectangle")
|
|
8456
|
+
return null;
|
|
8457
|
+
const gap = ROTATE_HANDLE_OFFSET / zoom;
|
|
8458
|
+
const dist = el.radius + gap;
|
|
8459
|
+
const origin = el.position;
|
|
8460
|
+
return {
|
|
8461
|
+
origin,
|
|
8462
|
+
knob: {
|
|
8463
|
+
x: origin.x + dist * Math.cos(el.angle),
|
|
8464
|
+
y: origin.y + dist * Math.sin(el.angle)
|
|
8465
|
+
}
|
|
8466
|
+
};
|
|
8467
|
+
}
|
|
8314
8468
|
function getHandlePositions(bounds) {
|
|
8315
8469
|
return [
|
|
8316
8470
|
["nw", { x: bounds.x, y: bounds.y }],
|
|
@@ -8444,21 +8598,66 @@ function renderSelectionBoxes(ctx, p) {
|
|
|
8444
8598
|
} else if (el.type === "template") {
|
|
8445
8599
|
ctx.setLineDash([]);
|
|
8446
8600
|
ctx.fillStyle = "#ffffff";
|
|
8447
|
-
|
|
8448
|
-
|
|
8449
|
-
|
|
8450
|
-
|
|
8451
|
-
|
|
8452
|
-
|
|
8453
|
-
|
|
8454
|
-
|
|
8455
|
-
|
|
8456
|
-
|
|
8457
|
-
|
|
8458
|
-
|
|
8459
|
-
|
|
8460
|
-
|
|
8601
|
+
if (el.templateShape === "rectangle") {
|
|
8602
|
+
if (p.selectedIds.length === 1) {
|
|
8603
|
+
const cos = Math.cos(el.angle);
|
|
8604
|
+
const sin = Math.sin(el.angle);
|
|
8605
|
+
const halfW = (el.width ?? 0) / 2;
|
|
8606
|
+
const pts = [
|
|
8607
|
+
[el.position.x + el.radius * cos, el.position.y + el.radius * sin],
|
|
8608
|
+
[
|
|
8609
|
+
el.position.x + el.radius / 2 * cos + halfW * -sin,
|
|
8610
|
+
el.position.y + el.radius / 2 * sin + halfW * cos
|
|
8611
|
+
]
|
|
8612
|
+
];
|
|
8613
|
+
for (const [hx, hy] of pts) {
|
|
8614
|
+
ctx.fillRect(
|
|
8615
|
+
hx - handleWorldSize / 2,
|
|
8616
|
+
hy - handleWorldSize / 2,
|
|
8617
|
+
handleWorldSize,
|
|
8618
|
+
handleWorldSize
|
|
8619
|
+
);
|
|
8620
|
+
ctx.strokeRect(
|
|
8621
|
+
hx - handleWorldSize / 2,
|
|
8622
|
+
hy - handleWorldSize / 2,
|
|
8623
|
+
handleWorldSize,
|
|
8624
|
+
handleWorldSize
|
|
8625
|
+
);
|
|
8626
|
+
}
|
|
8627
|
+
}
|
|
8628
|
+
} else {
|
|
8629
|
+
const hx = bounds.x + bounds.w;
|
|
8630
|
+
const hy = bounds.y + bounds.h;
|
|
8631
|
+
ctx.fillRect(
|
|
8632
|
+
hx - handleWorldSize / 2,
|
|
8633
|
+
hy - handleWorldSize / 2,
|
|
8634
|
+
handleWorldSize,
|
|
8635
|
+
handleWorldSize
|
|
8636
|
+
);
|
|
8637
|
+
ctx.strokeRect(
|
|
8638
|
+
hx - handleWorldSize / 2,
|
|
8639
|
+
hy - handleWorldSize / 2,
|
|
8640
|
+
handleWorldSize,
|
|
8641
|
+
handleWorldSize
|
|
8642
|
+
);
|
|
8643
|
+
}
|
|
8461
8644
|
ctx.setLineDash([4 / zoom, 4 / zoom]);
|
|
8645
|
+
if (p.selectedIds.length === 1 && (el.templateShape === "cone" || el.templateShape === "line" || el.templateShape === "rectangle")) {
|
|
8646
|
+
const aim = templateAimKnob(el, zoom);
|
|
8647
|
+
if (aim) {
|
|
8648
|
+
ctx.beginPath();
|
|
8649
|
+
ctx.moveTo(aim.origin.x, aim.origin.y);
|
|
8650
|
+
ctx.lineTo(aim.knob.x, aim.knob.y);
|
|
8651
|
+
ctx.stroke();
|
|
8652
|
+
ctx.setLineDash([]);
|
|
8653
|
+
ctx.fillStyle = "#ffffff";
|
|
8654
|
+
ctx.beginPath();
|
|
8655
|
+
ctx.arc(aim.knob.x, aim.knob.y, handleWorldSize / 2, 0, Math.PI * 2);
|
|
8656
|
+
ctx.fill();
|
|
8657
|
+
ctx.stroke();
|
|
8658
|
+
ctx.setLineDash([4 / zoom, 4 / zoom]);
|
|
8659
|
+
}
|
|
8660
|
+
}
|
|
8462
8661
|
}
|
|
8463
8662
|
if (p.selectedIds.length === 1 && ROTATABLE_TYPES.has(el.type)) {
|
|
8464
8663
|
const stemStart = topMidpoint(layout);
|
|
@@ -8604,6 +8803,7 @@ function hitTestTemplateResizeHandle(world, ctx, selectedIds) {
|
|
|
8604
8803
|
for (const id of selectedIds) {
|
|
8605
8804
|
const el = ctx.store.getById(id);
|
|
8606
8805
|
if (!el || el.type !== "template") continue;
|
|
8806
|
+
if (el.templateShape === "rectangle") continue;
|
|
8607
8807
|
const bounds = getElementBounds(el);
|
|
8608
8808
|
if (!bounds) continue;
|
|
8609
8809
|
const hx = bounds.x + bounds.w;
|
|
@@ -8614,6 +8814,50 @@ function hitTestTemplateResizeHandle(world, ctx, selectedIds) {
|
|
|
8614
8814
|
}
|
|
8615
8815
|
return null;
|
|
8616
8816
|
}
|
|
8817
|
+
function hitTestTemplateAimHandle(world, ctx, selectedIds) {
|
|
8818
|
+
if (selectedIds.length !== 1) return null;
|
|
8819
|
+
const id = selectedIds[0];
|
|
8820
|
+
if (!id) return null;
|
|
8821
|
+
const el = ctx.store.getById(id);
|
|
8822
|
+
if (!el || el.locked) return null;
|
|
8823
|
+
const knob = templateAimKnob(el, ctx.camera.zoom);
|
|
8824
|
+
if (!knob) return null;
|
|
8825
|
+
const r = (HANDLE_SIZE / 2 + HANDLE_HIT_PADDING2) / ctx.camera.zoom;
|
|
8826
|
+
const dx = world.x - knob.knob.x;
|
|
8827
|
+
const dy = world.y - knob.knob.y;
|
|
8828
|
+
return dx * dx + dy * dy <= r * r ? { elementId: id } : null;
|
|
8829
|
+
}
|
|
8830
|
+
function hitTestRectangleLengthHandle(world, ctx, selectedIds) {
|
|
8831
|
+
if (selectedIds.length !== 1) return null;
|
|
8832
|
+
const id = selectedIds[0];
|
|
8833
|
+
if (!id) return null;
|
|
8834
|
+
const el = ctx.store.getById(id);
|
|
8835
|
+
if (!el || el.locked || el.type !== "template" || el.templateShape !== "rectangle") return null;
|
|
8836
|
+
const zoom = ctx.camera.zoom;
|
|
8837
|
+
const r = (HANDLE_SIZE / 2 + HANDLE_HIT_PADDING2) / zoom;
|
|
8838
|
+
const hx = el.position.x + el.radius * Math.cos(el.angle);
|
|
8839
|
+
const hy = el.position.y + el.radius * Math.sin(el.angle);
|
|
8840
|
+
const dx = world.x - hx;
|
|
8841
|
+
const dy = world.y - hy;
|
|
8842
|
+
return dx * dx + dy * dy <= r * r ? { elementId: id } : null;
|
|
8843
|
+
}
|
|
8844
|
+
function hitTestRectangleWidthHandle(world, ctx, selectedIds) {
|
|
8845
|
+
if (selectedIds.length !== 1) return null;
|
|
8846
|
+
const id = selectedIds[0];
|
|
8847
|
+
if (!id) return null;
|
|
8848
|
+
const el = ctx.store.getById(id);
|
|
8849
|
+
if (!el || el.locked || el.type !== "template" || el.templateShape !== "rectangle") return null;
|
|
8850
|
+
const zoom = ctx.camera.zoom;
|
|
8851
|
+
const r = (HANDLE_SIZE / 2 + HANDLE_HIT_PADDING2) / zoom;
|
|
8852
|
+
const cos = Math.cos(el.angle);
|
|
8853
|
+
const sin = Math.sin(el.angle);
|
|
8854
|
+
const halfW = (el.width ?? 0) / 2;
|
|
8855
|
+
const hx = el.position.x + el.radius / 2 * cos + halfW * -sin;
|
|
8856
|
+
const hy = el.position.y + el.radius / 2 * sin + halfW * cos;
|
|
8857
|
+
const dx = world.x - hx;
|
|
8858
|
+
const dy = world.y - hy;
|
|
8859
|
+
return dx * dx + dy * dy <= r * r ? { elementId: id } : null;
|
|
8860
|
+
}
|
|
8617
8861
|
function findElementsInRect(marquee, ctx) {
|
|
8618
8862
|
const candidates = ctx.store.queryRect(marquee);
|
|
8619
8863
|
const ids = [];
|
|
@@ -8768,6 +9012,34 @@ function computeTemplateResize(el, world, opts) {
|
|
|
8768
9012
|
}
|
|
8769
9013
|
return updates;
|
|
8770
9014
|
}
|
|
9015
|
+
function computeRectangleLengthResize(el, world, opts) {
|
|
9016
|
+
const cos = Math.cos(el.angle);
|
|
9017
|
+
const sin = Math.sin(el.angle);
|
|
9018
|
+
let len = (world.x - el.position.x) * cos + (world.y - el.position.y) * sin;
|
|
9019
|
+
if (opts.snapToGrid && opts.gridSize && opts.gridSize > 0) {
|
|
9020
|
+
const snapUnit = opts.gridType === "hex" ? Math.sqrt(3) * opts.gridSize : opts.gridSize;
|
|
9021
|
+
len = Math.max(snapUnit, Math.round(len / snapUnit) * snapUnit);
|
|
9022
|
+
}
|
|
9023
|
+
len = Math.max(MIN_ELEMENT_SIZE, len);
|
|
9024
|
+
const updates = { radius: len };
|
|
9025
|
+
if (el.feetPerCell != null && opts.gridSize && opts.gridSize > 0) {
|
|
9026
|
+
const snapUnit = opts.gridType === "hex" ? Math.sqrt(3) * opts.gridSize : opts.gridSize;
|
|
9027
|
+
updates.radiusFeet = len / snapUnit * el.feetPerCell;
|
|
9028
|
+
}
|
|
9029
|
+
return updates;
|
|
9030
|
+
}
|
|
9031
|
+
function computeRectangleWidthResize(el, world, opts) {
|
|
9032
|
+
const cos = Math.cos(el.angle);
|
|
9033
|
+
const sin = Math.sin(el.angle);
|
|
9034
|
+
const perp = Math.abs(-(world.x - el.position.x) * sin + (world.y - el.position.y) * cos);
|
|
9035
|
+
let width = perp * 2;
|
|
9036
|
+
if (opts.snapToGrid && opts.gridSize && opts.gridSize > 0) {
|
|
9037
|
+
const snapUnit = opts.gridType === "hex" ? Math.sqrt(3) * opts.gridSize : opts.gridSize;
|
|
9038
|
+
width = Math.max(snapUnit, Math.round(width / snapUnit) * snapUnit);
|
|
9039
|
+
}
|
|
9040
|
+
width = Math.max(MIN_ELEMENT_SIZE, width);
|
|
9041
|
+
return { width };
|
|
9042
|
+
}
|
|
8771
9043
|
|
|
8772
9044
|
// src/tools/select-tool.ts
|
|
8773
9045
|
var SNAP_PX = 6;
|
|
@@ -8863,6 +9135,24 @@ var SelectTool = class {
|
|
|
8863
9135
|
ctx.requestRender();
|
|
8864
9136
|
return;
|
|
8865
9137
|
}
|
|
9138
|
+
const rectLengthHit = hitTestRectangleLengthHandle(world, ctx, this._selectedIds);
|
|
9139
|
+
if (rectLengthHit) {
|
|
9140
|
+
this.mode = { type: "resizing-rect-length", elementId: rectLengthHit.elementId };
|
|
9141
|
+
ctx.requestRender();
|
|
9142
|
+
return;
|
|
9143
|
+
}
|
|
9144
|
+
const rectWidthHit = hitTestRectangleWidthHandle(world, ctx, this._selectedIds);
|
|
9145
|
+
if (rectWidthHit) {
|
|
9146
|
+
this.mode = { type: "resizing-rect-width", elementId: rectWidthHit.elementId };
|
|
9147
|
+
ctx.requestRender();
|
|
9148
|
+
return;
|
|
9149
|
+
}
|
|
9150
|
+
const aimHit = hitTestTemplateAimHandle(world, ctx, this._selectedIds);
|
|
9151
|
+
if (aimHit) {
|
|
9152
|
+
this.mode = { type: "aiming-template", elementId: aimHit.elementId };
|
|
9153
|
+
ctx.requestRender();
|
|
9154
|
+
return;
|
|
9155
|
+
}
|
|
8866
9156
|
const rotateHit = hitTestRotateHandle(world, ctx, this._selectedIds);
|
|
8867
9157
|
if (rotateHit) {
|
|
8868
9158
|
const el = ctx.store.getById(rotateHit.elementId);
|
|
@@ -8944,6 +9234,32 @@ var SelectTool = class {
|
|
|
8944
9234
|
this.handleTemplateResize(world, ctx);
|
|
8945
9235
|
return;
|
|
8946
9236
|
}
|
|
9237
|
+
if (this.mode.type === "aiming-template") {
|
|
9238
|
+
const el = ctx.store.getById(this.mode.elementId);
|
|
9239
|
+
if (el && el.type === "template" && !el.locked) {
|
|
9240
|
+
let a = Math.atan2(world.y - el.position.y, world.x - el.position.x);
|
|
9241
|
+
if (state.shiftKey) {
|
|
9242
|
+
const snap = ctx.gridType === "hex" ? Math.PI / 3 : ROTATE_SNAP;
|
|
9243
|
+
a = Math.round(a / snap) * snap;
|
|
9244
|
+
}
|
|
9245
|
+
ctx.store.update(this.mode.elementId, { angle: normalizeAngle(a) });
|
|
9246
|
+
ctx.requestRender();
|
|
9247
|
+
}
|
|
9248
|
+
return;
|
|
9249
|
+
}
|
|
9250
|
+
if (this.mode.type === "resizing-rect-length" || this.mode.type === "resizing-rect-width") {
|
|
9251
|
+
ctx.setCursor?.(this.mode.type === "resizing-rect-length" ? "ew-resize" : "ns-resize");
|
|
9252
|
+
const el = ctx.store.getById(this.mode.elementId);
|
|
9253
|
+
if (el && el.type === "template" && !el.locked) {
|
|
9254
|
+
const opts = { snapToGrid: ctx.snapToGrid, gridSize: ctx.gridSize, gridType: ctx.gridType };
|
|
9255
|
+
const patch = this.mode.type === "resizing-rect-length" ? computeRectangleLengthResize(el, world, opts) : computeRectangleWidthResize(el, world, opts);
|
|
9256
|
+
if (patch) {
|
|
9257
|
+
ctx.store.update(this.mode.elementId, patch);
|
|
9258
|
+
ctx.requestRender();
|
|
9259
|
+
}
|
|
9260
|
+
}
|
|
9261
|
+
return;
|
|
9262
|
+
}
|
|
8947
9263
|
if (this.mode.type === "rotating") {
|
|
8948
9264
|
const { elementId, center: center2, startPointerAngle, startRotation } = this.mode;
|
|
8949
9265
|
const a = Math.atan2(world.y - center2.y, world.x - center2.x);
|
|
@@ -9150,6 +9466,18 @@ var SelectTool = class {
|
|
|
9150
9466
|
ctx.setCursor?.("nwse-resize");
|
|
9151
9467
|
return null;
|
|
9152
9468
|
}
|
|
9469
|
+
if (hitTestRectangleLengthHandle(world, ctx, this._selectedIds)) {
|
|
9470
|
+
ctx.setCursor?.("ew-resize");
|
|
9471
|
+
return null;
|
|
9472
|
+
}
|
|
9473
|
+
if (hitTestRectangleWidthHandle(world, ctx, this._selectedIds)) {
|
|
9474
|
+
ctx.setCursor?.("ns-resize");
|
|
9475
|
+
return null;
|
|
9476
|
+
}
|
|
9477
|
+
if (hitTestTemplateAimHandle(world, ctx, this._selectedIds)) {
|
|
9478
|
+
ctx.setCursor?.("grab");
|
|
9479
|
+
return null;
|
|
9480
|
+
}
|
|
9153
9481
|
if (hitTestRotateHandle(world, ctx, this._selectedIds)) {
|
|
9154
9482
|
ctx.setCursor?.("grab");
|
|
9155
9483
|
return null;
|
|
@@ -9810,6 +10138,11 @@ var MeasureTool = class {
|
|
|
9810
10138
|
};
|
|
9811
10139
|
|
|
9812
10140
|
// src/tools/template-tool.ts
|
|
10141
|
+
var MIN_RECT_WIDTH = 20;
|
|
10142
|
+
function defaultRectWidth(radius, scaleUnit) {
|
|
10143
|
+
if (scaleUnit > 0) return scaleUnit;
|
|
10144
|
+
return Math.max(MIN_RECT_WIDTH, radius * 0.25);
|
|
10145
|
+
}
|
|
9813
10146
|
var TemplateTool = class {
|
|
9814
10147
|
name = "template";
|
|
9815
10148
|
drawing = false;
|
|
@@ -9819,6 +10152,7 @@ var TemplateTool = class {
|
|
|
9819
10152
|
gridType;
|
|
9820
10153
|
hexOrientation;
|
|
9821
10154
|
snapEnabled = false;
|
|
10155
|
+
feetScaleUnit = 0;
|
|
9822
10156
|
templateShape;
|
|
9823
10157
|
fillColor;
|
|
9824
10158
|
strokeColor;
|
|
@@ -9867,6 +10201,7 @@ var TemplateTool = class {
|
|
|
9867
10201
|
this.gridType = ctx.gridType;
|
|
9868
10202
|
this.hexOrientation = ctx.hexOrientation;
|
|
9869
10203
|
this.snapEnabled = !!ctx.gridType || (ctx.snapToGrid ?? false);
|
|
10204
|
+
this.feetScaleUnit = ctx.gridSize && ctx.gridSize > 0 ? ctx.gridType === "hex" ? Math.sqrt(3) * ctx.gridSize : ctx.gridSize : 0;
|
|
9870
10205
|
const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
|
|
9871
10206
|
this.origin = this.snapToGrid(world, ctx);
|
|
9872
10207
|
this.current = { ...this.origin };
|
|
@@ -9886,11 +10221,13 @@ var TemplateTool = class {
|
|
|
9886
10221
|
const snapUnit = gridSize && gridSize > 0 ? ctx.gridType === "hex" ? Math.sqrt(3) * gridSize : gridSize : 0;
|
|
9887
10222
|
const cells = snapUnit > 0 ? radius / snapUnit : 0;
|
|
9888
10223
|
const radiusFeet = cells * this.feetPerCell;
|
|
10224
|
+
const width = this.templateShape === "rectangle" ? defaultRectWidth(radius, this.feetScaleUnit) : void 0;
|
|
9889
10225
|
const element = createTemplate({
|
|
9890
10226
|
position: { ...this.origin },
|
|
9891
10227
|
templateShape: this.templateShape,
|
|
9892
10228
|
radius,
|
|
9893
10229
|
angle,
|
|
10230
|
+
width,
|
|
9894
10231
|
fillColor: this.fillColor,
|
|
9895
10232
|
strokeColor: this.strokeColor,
|
|
9896
10233
|
strokeWidth: this.strokeWidth,
|
|
@@ -9965,7 +10302,24 @@ var TemplateTool = class {
|
|
|
9965
10302
|
ctx.stroke();
|
|
9966
10303
|
break;
|
|
9967
10304
|
}
|
|
10305
|
+
case "rectangle": {
|
|
10306
|
+
const halfW = defaultRectWidth(radius, this.feetScaleUnit) / 2;
|
|
10307
|
+
const cos = Math.cos(angle);
|
|
10308
|
+
const sin = Math.sin(angle);
|
|
10309
|
+
const perpX = -sin * halfW;
|
|
10310
|
+
const perpY = cos * halfW;
|
|
10311
|
+
ctx.beginPath();
|
|
10312
|
+
ctx.moveTo(cx + perpX, cy + perpY);
|
|
10313
|
+
ctx.lineTo(cx + radius * cos + perpX, cy + radius * sin + perpY);
|
|
10314
|
+
ctx.lineTo(cx + radius * cos - perpX, cy + radius * sin - perpY);
|
|
10315
|
+
ctx.lineTo(cx - perpX, cy - perpY);
|
|
10316
|
+
ctx.closePath();
|
|
10317
|
+
ctx.fill();
|
|
10318
|
+
ctx.stroke();
|
|
10319
|
+
break;
|
|
10320
|
+
}
|
|
9968
10321
|
}
|
|
10322
|
+
this.drawFeetLabel(ctx, radius);
|
|
9969
10323
|
ctx.restore();
|
|
9970
10324
|
}
|
|
9971
10325
|
renderHexOverlay(ctx, radius) {
|
|
@@ -9990,6 +10344,18 @@ var TemplateTool = class {
|
|
|
9990
10344
|
case "square":
|
|
9991
10345
|
hexCells = getHexCellsInSquare(center2, radiusCells, cellSize, orientation);
|
|
9992
10346
|
break;
|
|
10347
|
+
case "rectangle": {
|
|
10348
|
+
const widthCells = defaultRectWidth(radius, this.feetScaleUnit) / snapUnit;
|
|
10349
|
+
hexCells = getHexCellsInRectangle(
|
|
10350
|
+
center2,
|
|
10351
|
+
angle,
|
|
10352
|
+
radiusCells,
|
|
10353
|
+
widthCells,
|
|
10354
|
+
cellSize,
|
|
10355
|
+
orientation
|
|
10356
|
+
);
|
|
10357
|
+
break;
|
|
10358
|
+
}
|
|
9993
10359
|
}
|
|
9994
10360
|
ctx.save();
|
|
9995
10361
|
ctx.globalAlpha = 0.4;
|
|
@@ -10006,7 +10372,7 @@ var TemplateTool = class {
|
|
|
10006
10372
|
ctx.strokeStyle = this.strokeColor;
|
|
10007
10373
|
ctx.lineWidth = this.strokeWidth;
|
|
10008
10374
|
ctx.stroke();
|
|
10009
|
-
if (this.templateShape === "cone" || this.templateShape === "line" || this.templateShape === "circle" || this.templateShape === "square") {
|
|
10375
|
+
if (this.templateShape === "cone" || this.templateShape === "line" || this.templateShape === "circle" || this.templateShape === "square" || this.templateShape === "rectangle") {
|
|
10010
10376
|
ctx.globalAlpha = 0.5;
|
|
10011
10377
|
ctx.beginPath();
|
|
10012
10378
|
drawHexPath(ctx, center2.x, center2.y, cellSize, orientation);
|
|
@@ -10016,30 +10382,7 @@ var TemplateTool = class {
|
|
|
10016
10382
|
ctx.lineWidth = this.strokeWidth;
|
|
10017
10383
|
ctx.stroke();
|
|
10018
10384
|
}
|
|
10019
|
-
|
|
10020
|
-
const feet = radiusCells * this.feetPerCell;
|
|
10021
|
-
if (feet > 0) {
|
|
10022
|
-
ctx.globalAlpha = 1;
|
|
10023
|
-
const label = `${Math.round(feet)} ft`;
|
|
10024
|
-
const fontSize = Math.max(10, Math.min(14, radius * 0.15));
|
|
10025
|
-
ctx.font = `bold ${fontSize}px system-ui, sans-serif`;
|
|
10026
|
-
ctx.textAlign = "center";
|
|
10027
|
-
ctx.textBaseline = "bottom";
|
|
10028
|
-
const textX = center2.x;
|
|
10029
|
-
const textY = center2.y - 4;
|
|
10030
|
-
const metrics = ctx.measureText(label);
|
|
10031
|
-
const padX = 4;
|
|
10032
|
-
const padY = 2;
|
|
10033
|
-
const textW = metrics.width + padX * 2;
|
|
10034
|
-
const textH = fontSize + padY * 2;
|
|
10035
|
-
ctx.fillStyle = "rgba(255, 255, 255, 0.85)";
|
|
10036
|
-
ctx.beginPath();
|
|
10037
|
-
ctx.roundRect(textX - textW / 2, textY - textH, textW, textH, 3);
|
|
10038
|
-
ctx.fill();
|
|
10039
|
-
ctx.fillStyle = this.strokeColor;
|
|
10040
|
-
ctx.fillText(label, textX, textY - padY);
|
|
10041
|
-
}
|
|
10042
|
-
}
|
|
10385
|
+
this.drawFeetLabel(ctx, radius);
|
|
10043
10386
|
ctx.restore();
|
|
10044
10387
|
}
|
|
10045
10388
|
computeRadius() {
|
|
@@ -10057,6 +10400,17 @@ var TemplateTool = class {
|
|
|
10057
10400
|
const dy = this.current.y - this.origin.y;
|
|
10058
10401
|
return Math.atan2(dy, dx);
|
|
10059
10402
|
}
|
|
10403
|
+
drawFeetLabel(ctx, radius) {
|
|
10404
|
+
const feet = this.feetScaleUnit > 0 ? radius / this.feetScaleUnit * this.feetPerCell : 0;
|
|
10405
|
+
renderTemplateFeetLabel(ctx, {
|
|
10406
|
+
position: this.origin,
|
|
10407
|
+
radius,
|
|
10408
|
+
angle: this.computeAngle(),
|
|
10409
|
+
templateShape: this.templateShape,
|
|
10410
|
+
feet,
|
|
10411
|
+
color: this.strokeColor
|
|
10412
|
+
});
|
|
10413
|
+
}
|
|
10060
10414
|
snapToGrid(point, ctx) {
|
|
10061
10415
|
if (!ctx.gridSize) return point;
|
|
10062
10416
|
if (ctx.gridType === "hex" && ctx.hexOrientation) {
|
|
@@ -10186,7 +10540,7 @@ var LaserTool = class {
|
|
|
10186
10540
|
};
|
|
10187
10541
|
|
|
10188
10542
|
// src/index.ts
|
|
10189
|
-
var VERSION = "0.
|
|
10543
|
+
var VERSION = "0.48.0";
|
|
10190
10544
|
// Annotate the CommonJS export names for ESM import in node:
|
|
10191
10545
|
0 && (module.exports = {
|
|
10192
10546
|
ArrowTool,
|
|
@@ -10238,6 +10592,7 @@ var VERSION = "0.46.1";
|
|
|
10238
10592
|
getHexCellsInCone,
|
|
10239
10593
|
getHexCellsInLine,
|
|
10240
10594
|
getHexCellsInRadius,
|
|
10595
|
+
getHexCellsInRectangle,
|
|
10241
10596
|
getHexCellsInSquare,
|
|
10242
10597
|
getHexDistance,
|
|
10243
10598
|
isNearBezier,
|