@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.js
CHANGED
|
@@ -897,6 +897,26 @@ function getTemplateBounds(el) {
|
|
|
897
897
|
const maxY = Math.max(y0, y1, y2, y3);
|
|
898
898
|
return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
|
|
899
899
|
}
|
|
900
|
+
case "rectangle": {
|
|
901
|
+
const halfW = (el.width ?? 0) / 2;
|
|
902
|
+
const cos = Math.cos(el.angle);
|
|
903
|
+
const sin = Math.sin(el.angle);
|
|
904
|
+
const perpX = -sin * halfW;
|
|
905
|
+
const perpY = cos * halfW;
|
|
906
|
+
const x0 = cx + perpX;
|
|
907
|
+
const y0 = cy + perpY;
|
|
908
|
+
const x1 = cx + r * cos + perpX;
|
|
909
|
+
const y1 = cy + r * sin + perpY;
|
|
910
|
+
const x2 = cx + r * cos - perpX;
|
|
911
|
+
const y2 = cy + r * sin - perpY;
|
|
912
|
+
const x3 = cx - perpX;
|
|
913
|
+
const y3 = cy - perpY;
|
|
914
|
+
const minX = Math.min(x0, x1, x2, x3);
|
|
915
|
+
const minY = Math.min(y0, y1, y2, y3);
|
|
916
|
+
const maxX = Math.max(x0, x1, x2, x3);
|
|
917
|
+
const maxY = Math.max(y0, y1, y2, y3);
|
|
918
|
+
return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
|
|
919
|
+
}
|
|
900
920
|
}
|
|
901
921
|
}
|
|
902
922
|
function transferStrokeBounds(prev, next) {
|
|
@@ -3200,6 +3220,46 @@ function getHexCellsInLine(center2, angle, radiusCells, cellSize, orientation) {
|
|
|
3200
3220
|
}
|
|
3201
3221
|
return cells;
|
|
3202
3222
|
}
|
|
3223
|
+
function getHexCellsInRectangle(center2, angle, lengthCells, widthCells, cellSize, orientation) {
|
|
3224
|
+
const nLen = Math.round(lengthCells);
|
|
3225
|
+
const wCells = Math.max(1, Math.round(widthCells));
|
|
3226
|
+
const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
|
|
3227
|
+
const cube = offsetToCube(off.col, off.row, orientation);
|
|
3228
|
+
const centerPixel = offsetToPixel(off.col, off.row, cellSize, orientation);
|
|
3229
|
+
if (nLen <= 0) return [centerPixel];
|
|
3230
|
+
const vertexOffset = orientation === "pointy" ? Math.PI / 6 : 0;
|
|
3231
|
+
const step = Math.PI / 3;
|
|
3232
|
+
const snappedAngle = Math.round((angle - vertexOffset) / step) * step + vertexOffset;
|
|
3233
|
+
const cos = Math.cos(snappedAngle);
|
|
3234
|
+
const sin = Math.sin(snappedAngle);
|
|
3235
|
+
const snapUnit = Math.sqrt(3) * cellSize;
|
|
3236
|
+
const lineLength = nLen * snapUnit;
|
|
3237
|
+
const halfWidth = wCells * snapUnit / 2 + 1e-6;
|
|
3238
|
+
const iterM = nLen + Math.ceil(wCells / 2) + 1;
|
|
3239
|
+
const cells = [];
|
|
3240
|
+
for (let dq = -iterM; dq <= iterM; dq++) {
|
|
3241
|
+
const rMin = Math.max(-iterM, -dq - iterM);
|
|
3242
|
+
const rMax = Math.min(iterM, -dq + iterM);
|
|
3243
|
+
for (let dr = rMin; dr <= rMax; dr++) {
|
|
3244
|
+
const absQ = cube.q + dq;
|
|
3245
|
+
const absR = cube.r + dr;
|
|
3246
|
+
const pixel = offsetToPixel(
|
|
3247
|
+
cubeToOffset(absQ, absR, orientation).col,
|
|
3248
|
+
cubeToOffset(absQ, absR, orientation).row,
|
|
3249
|
+
cellSize,
|
|
3250
|
+
orientation
|
|
3251
|
+
);
|
|
3252
|
+
const dx = pixel.x - centerPixel.x;
|
|
3253
|
+
const dy = pixel.y - centerPixel.y;
|
|
3254
|
+
const along = dx * cos + dy * sin;
|
|
3255
|
+
const perp = Math.abs(-dx * sin + dy * cos);
|
|
3256
|
+
if (along >= -snapUnit * 0.1 && along <= lineLength + snapUnit * 0.1 && perp <= halfWidth) {
|
|
3257
|
+
cells.push(pixel);
|
|
3258
|
+
}
|
|
3259
|
+
}
|
|
3260
|
+
}
|
|
3261
|
+
return cells;
|
|
3262
|
+
}
|
|
3203
3263
|
function getHexCellsInSquare(center2, radiusCells, cellSize, orientation) {
|
|
3204
3264
|
const n2 = Math.round(radiusCells);
|
|
3205
3265
|
const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
|
|
@@ -3238,6 +3298,48 @@ function drawHexPath(ctx, cx, cy, cellSize, orientation) {
|
|
|
3238
3298
|
ctx.closePath();
|
|
3239
3299
|
}
|
|
3240
3300
|
|
|
3301
|
+
// src/elements/renderers/template-measure.ts
|
|
3302
|
+
function renderTemplateFeetLabel(ctx, p) {
|
|
3303
|
+
if (p.feet <= 0) return;
|
|
3304
|
+
const directional = p.templateShape === "cone" || p.templateShape === "line" || p.templateShape === "rectangle";
|
|
3305
|
+
const dir = directional ? p.angle : 0;
|
|
3306
|
+
const length = p.templateShape === "square" ? p.radius / 2 : p.radius;
|
|
3307
|
+
const cos = Math.cos(dir);
|
|
3308
|
+
const sin = Math.sin(dir);
|
|
3309
|
+
const start = p.position;
|
|
3310
|
+
const end = { x: start.x + length * cos, y: start.y + length * sin };
|
|
3311
|
+
const midX = start.x + length / 2 * cos;
|
|
3312
|
+
const midY = start.y + length / 2 * sin;
|
|
3313
|
+
ctx.save();
|
|
3314
|
+
ctx.globalAlpha = 1;
|
|
3315
|
+
ctx.beginPath();
|
|
3316
|
+
ctx.setLineDash([4, 4]);
|
|
3317
|
+
ctx.strokeStyle = p.color;
|
|
3318
|
+
ctx.lineWidth = 1.5;
|
|
3319
|
+
ctx.moveTo(start.x, start.y);
|
|
3320
|
+
ctx.lineTo(end.x, end.y);
|
|
3321
|
+
ctx.stroke();
|
|
3322
|
+
ctx.setLineDash([]);
|
|
3323
|
+
const label = `${Math.round(p.feet)} ft`;
|
|
3324
|
+
const fontSize = Math.max(10, Math.min(14, p.radius * 0.15));
|
|
3325
|
+
ctx.font = `bold ${fontSize}px system-ui, sans-serif`;
|
|
3326
|
+
ctx.textAlign = "center";
|
|
3327
|
+
ctx.textBaseline = "bottom";
|
|
3328
|
+
const textY = midY - 4;
|
|
3329
|
+
const metrics = ctx.measureText(label);
|
|
3330
|
+
const padX = 4;
|
|
3331
|
+
const padY = 2;
|
|
3332
|
+
const textW = metrics.width + padX * 2;
|
|
3333
|
+
const textH = fontSize + padY * 2;
|
|
3334
|
+
ctx.fillStyle = "rgba(255, 255, 255, 0.85)";
|
|
3335
|
+
ctx.beginPath();
|
|
3336
|
+
ctx.roundRect(midX - textW / 2, textY - textH, textW, textH, 3);
|
|
3337
|
+
ctx.fill();
|
|
3338
|
+
ctx.fillStyle = p.color;
|
|
3339
|
+
ctx.fillText(label, midX, textY - padY);
|
|
3340
|
+
ctx.restore();
|
|
3341
|
+
}
|
|
3342
|
+
|
|
3241
3343
|
// src/elements/renderers/template-renderer.ts
|
|
3242
3344
|
function renderTemplate(ctx, template, store) {
|
|
3243
3345
|
const grid = store?.getElementsByType("grid")[0];
|
|
@@ -3261,9 +3363,6 @@ function renderGeometricTemplate(ctx, template) {
|
|
|
3261
3363
|
ctx.arc(cx, cy, r, 0, Math.PI * 2);
|
|
3262
3364
|
ctx.fill();
|
|
3263
3365
|
ctx.stroke();
|
|
3264
|
-
if (template.radiusFeet != null && template.radiusFeet > 0) {
|
|
3265
|
-
renderRadiusMarker(ctx, cx, cy, r, template.radiusFeet);
|
|
3266
|
-
}
|
|
3267
3366
|
break;
|
|
3268
3367
|
case "square":
|
|
3269
3368
|
ctx.fillRect(cx - r / 2, cy - r / 2, r, r);
|
|
@@ -3295,6 +3394,32 @@ function renderGeometricTemplate(ctx, template) {
|
|
|
3295
3394
|
ctx.stroke();
|
|
3296
3395
|
break;
|
|
3297
3396
|
}
|
|
3397
|
+
case "rectangle": {
|
|
3398
|
+
const halfW = (template.width ?? 0) / 2;
|
|
3399
|
+
const cos = Math.cos(template.angle);
|
|
3400
|
+
const sin = Math.sin(template.angle);
|
|
3401
|
+
const perpX = -sin * halfW;
|
|
3402
|
+
const perpY = cos * halfW;
|
|
3403
|
+
ctx.beginPath();
|
|
3404
|
+
ctx.moveTo(cx + perpX, cy + perpY);
|
|
3405
|
+
ctx.lineTo(cx + r * cos + perpX, cy + r * sin + perpY);
|
|
3406
|
+
ctx.lineTo(cx + r * cos - perpX, cy + r * sin - perpY);
|
|
3407
|
+
ctx.lineTo(cx - perpX, cy - perpY);
|
|
3408
|
+
ctx.closePath();
|
|
3409
|
+
ctx.fill();
|
|
3410
|
+
ctx.stroke();
|
|
3411
|
+
break;
|
|
3412
|
+
}
|
|
3413
|
+
}
|
|
3414
|
+
if (template.radiusFeet != null && template.radiusFeet > 0) {
|
|
3415
|
+
renderTemplateFeetLabel(ctx, {
|
|
3416
|
+
position: template.position,
|
|
3417
|
+
radius: template.radius,
|
|
3418
|
+
angle: template.angle,
|
|
3419
|
+
templateShape: template.templateShape,
|
|
3420
|
+
feet: template.radiusFeet,
|
|
3421
|
+
color: template.strokeColor
|
|
3422
|
+
});
|
|
3298
3423
|
}
|
|
3299
3424
|
ctx.restore();
|
|
3300
3425
|
}
|
|
@@ -3316,6 +3441,18 @@ function renderHexTemplate(ctx, template, cellSize, orientation) {
|
|
|
3316
3441
|
case "square":
|
|
3317
3442
|
cells = getHexCellsInSquare(center2, radiusCells, cellSize, orientation);
|
|
3318
3443
|
break;
|
|
3444
|
+
case "rectangle": {
|
|
3445
|
+
const widthCells = (template.width ?? 0) / snapUnit;
|
|
3446
|
+
cells = getHexCellsInRectangle(
|
|
3447
|
+
center2,
|
|
3448
|
+
template.angle,
|
|
3449
|
+
radiusCells,
|
|
3450
|
+
widthCells,
|
|
3451
|
+
cellSize,
|
|
3452
|
+
orientation
|
|
3453
|
+
);
|
|
3454
|
+
break;
|
|
3455
|
+
}
|
|
3319
3456
|
}
|
|
3320
3457
|
ctx.save();
|
|
3321
3458
|
ctx.globalAlpha = template.opacity;
|
|
@@ -3342,44 +3479,18 @@ function renderHexTemplate(ctx, template, cellSize, orientation) {
|
|
|
3342
3479
|
ctx.lineWidth = template.strokeWidth;
|
|
3343
3480
|
ctx.stroke();
|
|
3344
3481
|
}
|
|
3345
|
-
if (template.
|
|
3346
|
-
|
|
3347
|
-
|
|
3482
|
+
if (template.radiusFeet != null && template.radiusFeet > 0) {
|
|
3483
|
+
renderTemplateFeetLabel(ctx, {
|
|
3484
|
+
position: template.position,
|
|
3485
|
+
radius: template.radius,
|
|
3486
|
+
angle: template.angle,
|
|
3487
|
+
templateShape: template.templateShape,
|
|
3488
|
+
feet: template.radiusFeet,
|
|
3489
|
+
color: template.strokeColor
|
|
3490
|
+
});
|
|
3348
3491
|
}
|
|
3349
3492
|
ctx.restore();
|
|
3350
3493
|
}
|
|
3351
|
-
function renderRadiusMarker(ctx, cx, cy, r, feet) {
|
|
3352
|
-
const markerColor = ctx.strokeStyle;
|
|
3353
|
-
ctx.save();
|
|
3354
|
-
ctx.globalAlpha = 1;
|
|
3355
|
-
ctx.beginPath();
|
|
3356
|
-
ctx.setLineDash([4, 4]);
|
|
3357
|
-
ctx.strokeStyle = markerColor;
|
|
3358
|
-
ctx.lineWidth = 1.5;
|
|
3359
|
-
ctx.moveTo(cx, cy);
|
|
3360
|
-
ctx.lineTo(cx + r, cy);
|
|
3361
|
-
ctx.stroke();
|
|
3362
|
-
ctx.setLineDash([]);
|
|
3363
|
-
const label = `${Math.round(feet)} ft`;
|
|
3364
|
-
const fontSize = Math.max(10, Math.min(14, r * 0.15));
|
|
3365
|
-
ctx.font = `bold ${fontSize}px system-ui, sans-serif`;
|
|
3366
|
-
ctx.textAlign = "center";
|
|
3367
|
-
ctx.textBaseline = "bottom";
|
|
3368
|
-
const textX = cx + r / 2;
|
|
3369
|
-
const textY = cy - 4;
|
|
3370
|
-
const metrics = ctx.measureText(label);
|
|
3371
|
-
const padX = 4;
|
|
3372
|
-
const padY = 2;
|
|
3373
|
-
const textW = metrics.width + padX * 2;
|
|
3374
|
-
const textH = fontSize + padY * 2;
|
|
3375
|
-
ctx.fillStyle = "rgba(255, 255, 255, 0.85)";
|
|
3376
|
-
ctx.beginPath();
|
|
3377
|
-
ctx.roundRect(textX - textW / 2, textY - textH, textW, textH, 3);
|
|
3378
|
-
ctx.fill();
|
|
3379
|
-
ctx.fillStyle = markerColor;
|
|
3380
|
-
ctx.fillText(label, textX, textY - padY);
|
|
3381
|
-
ctx.restore();
|
|
3382
|
-
}
|
|
3383
3494
|
|
|
3384
3495
|
// src/elements/grid-renderer.ts
|
|
3385
3496
|
function getSquareGridLines(bounds, cellSize) {
|
|
@@ -3918,7 +4029,8 @@ function createTemplate(input) {
|
|
|
3918
4029
|
opacity: input.opacity ?? 0.6,
|
|
3919
4030
|
feetPerCell: input.feetPerCell,
|
|
3920
4031
|
radiusFeet: input.radiusFeet,
|
|
3921
|
-
...input.renderStyle !== void 0 ? { renderStyle: input.renderStyle } : {}
|
|
4032
|
+
...input.renderStyle !== void 0 ? { renderStyle: input.renderStyle } : {},
|
|
4033
|
+
...input.width !== void 0 ? { width: input.width } : {}
|
|
3922
4034
|
};
|
|
3923
4035
|
}
|
|
3924
4036
|
|
|
@@ -5515,6 +5627,20 @@ function emitGeometricTemplate(t) {
|
|
|
5515
5627
|
].map(([px, py]) => `${n(px ?? 0)},${n(py ?? 0)}`).join(" ");
|
|
5516
5628
|
return `<polygon points="${pts}" ${attrs} />`;
|
|
5517
5629
|
}
|
|
5630
|
+
case "rectangle": {
|
|
5631
|
+
const halfW = (t.width ?? 0) / 2;
|
|
5632
|
+
const cos = Math.cos(t.angle);
|
|
5633
|
+
const sin = Math.sin(t.angle);
|
|
5634
|
+
const perpX = -sin * halfW;
|
|
5635
|
+
const perpY = cos * halfW;
|
|
5636
|
+
const pts = [
|
|
5637
|
+
[cx + perpX, cy + perpY],
|
|
5638
|
+
[cx + r * cos + perpX, cy + r * sin + perpY],
|
|
5639
|
+
[cx + r * cos - perpX, cy + r * sin - perpY],
|
|
5640
|
+
[cx - perpX, cy - perpY]
|
|
5641
|
+
].map(([px, py]) => `${n(px ?? 0)},${n(py ?? 0)}`).join(" ");
|
|
5642
|
+
return `<polygon points="${pts}" ${attrs} />`;
|
|
5643
|
+
}
|
|
5518
5644
|
}
|
|
5519
5645
|
}
|
|
5520
5646
|
function emitHexTemplate(t, grid) {
|
|
@@ -5537,6 +5663,18 @@ function emitHexTemplate(t, grid) {
|
|
|
5537
5663
|
case "square":
|
|
5538
5664
|
cells = getHexCellsInSquare(center2, radiusCells, cellSize, orientation);
|
|
5539
5665
|
break;
|
|
5666
|
+
case "rectangle": {
|
|
5667
|
+
const widthCells = (t.width ?? 0) / snapUnit;
|
|
5668
|
+
cells = getHexCellsInRectangle(
|
|
5669
|
+
center2,
|
|
5670
|
+
t.angle,
|
|
5671
|
+
radiusCells,
|
|
5672
|
+
widthCells,
|
|
5673
|
+
cellSize,
|
|
5674
|
+
orientation
|
|
5675
|
+
);
|
|
5676
|
+
break;
|
|
5677
|
+
}
|
|
5540
5678
|
}
|
|
5541
5679
|
let d = "";
|
|
5542
5680
|
for (const cell of cells) {
|
|
@@ -8225,6 +8363,21 @@ function getOverlayLayout(el, zoom) {
|
|
|
8225
8363
|
const rotateHandle = rotatePoint(topMid, center2, angle);
|
|
8226
8364
|
return { center: center2, corners, rotateHandle, angle };
|
|
8227
8365
|
}
|
|
8366
|
+
function templateAimKnob(el, zoom) {
|
|
8367
|
+
if (el.type !== "template") return null;
|
|
8368
|
+
if (el.templateShape !== "cone" && el.templateShape !== "line" && el.templateShape !== "rectangle")
|
|
8369
|
+
return null;
|
|
8370
|
+
const gap = ROTATE_HANDLE_OFFSET / zoom;
|
|
8371
|
+
const dist = el.radius + gap;
|
|
8372
|
+
const origin = el.position;
|
|
8373
|
+
return {
|
|
8374
|
+
origin,
|
|
8375
|
+
knob: {
|
|
8376
|
+
x: origin.x + dist * Math.cos(el.angle),
|
|
8377
|
+
y: origin.y + dist * Math.sin(el.angle)
|
|
8378
|
+
}
|
|
8379
|
+
};
|
|
8380
|
+
}
|
|
8228
8381
|
function getHandlePositions(bounds) {
|
|
8229
8382
|
return [
|
|
8230
8383
|
["nw", { x: bounds.x, y: bounds.y }],
|
|
@@ -8358,21 +8511,66 @@ function renderSelectionBoxes(ctx, p) {
|
|
|
8358
8511
|
} else if (el.type === "template") {
|
|
8359
8512
|
ctx.setLineDash([]);
|
|
8360
8513
|
ctx.fillStyle = "#ffffff";
|
|
8361
|
-
|
|
8362
|
-
|
|
8363
|
-
|
|
8364
|
-
|
|
8365
|
-
|
|
8366
|
-
|
|
8367
|
-
|
|
8368
|
-
|
|
8369
|
-
|
|
8370
|
-
|
|
8371
|
-
|
|
8372
|
-
|
|
8373
|
-
|
|
8374
|
-
|
|
8514
|
+
if (el.templateShape === "rectangle") {
|
|
8515
|
+
if (p.selectedIds.length === 1) {
|
|
8516
|
+
const cos = Math.cos(el.angle);
|
|
8517
|
+
const sin = Math.sin(el.angle);
|
|
8518
|
+
const halfW = (el.width ?? 0) / 2;
|
|
8519
|
+
const pts = [
|
|
8520
|
+
[el.position.x + el.radius * cos, el.position.y + el.radius * sin],
|
|
8521
|
+
[
|
|
8522
|
+
el.position.x + el.radius / 2 * cos + halfW * -sin,
|
|
8523
|
+
el.position.y + el.radius / 2 * sin + halfW * cos
|
|
8524
|
+
]
|
|
8525
|
+
];
|
|
8526
|
+
for (const [hx, hy] of pts) {
|
|
8527
|
+
ctx.fillRect(
|
|
8528
|
+
hx - handleWorldSize / 2,
|
|
8529
|
+
hy - handleWorldSize / 2,
|
|
8530
|
+
handleWorldSize,
|
|
8531
|
+
handleWorldSize
|
|
8532
|
+
);
|
|
8533
|
+
ctx.strokeRect(
|
|
8534
|
+
hx - handleWorldSize / 2,
|
|
8535
|
+
hy - handleWorldSize / 2,
|
|
8536
|
+
handleWorldSize,
|
|
8537
|
+
handleWorldSize
|
|
8538
|
+
);
|
|
8539
|
+
}
|
|
8540
|
+
}
|
|
8541
|
+
} else {
|
|
8542
|
+
const hx = bounds.x + bounds.w;
|
|
8543
|
+
const hy = bounds.y + bounds.h;
|
|
8544
|
+
ctx.fillRect(
|
|
8545
|
+
hx - handleWorldSize / 2,
|
|
8546
|
+
hy - handleWorldSize / 2,
|
|
8547
|
+
handleWorldSize,
|
|
8548
|
+
handleWorldSize
|
|
8549
|
+
);
|
|
8550
|
+
ctx.strokeRect(
|
|
8551
|
+
hx - handleWorldSize / 2,
|
|
8552
|
+
hy - handleWorldSize / 2,
|
|
8553
|
+
handleWorldSize,
|
|
8554
|
+
handleWorldSize
|
|
8555
|
+
);
|
|
8556
|
+
}
|
|
8375
8557
|
ctx.setLineDash([4 / zoom, 4 / zoom]);
|
|
8558
|
+
if (p.selectedIds.length === 1 && (el.templateShape === "cone" || el.templateShape === "line" || el.templateShape === "rectangle")) {
|
|
8559
|
+
const aim = templateAimKnob(el, zoom);
|
|
8560
|
+
if (aim) {
|
|
8561
|
+
ctx.beginPath();
|
|
8562
|
+
ctx.moveTo(aim.origin.x, aim.origin.y);
|
|
8563
|
+
ctx.lineTo(aim.knob.x, aim.knob.y);
|
|
8564
|
+
ctx.stroke();
|
|
8565
|
+
ctx.setLineDash([]);
|
|
8566
|
+
ctx.fillStyle = "#ffffff";
|
|
8567
|
+
ctx.beginPath();
|
|
8568
|
+
ctx.arc(aim.knob.x, aim.knob.y, handleWorldSize / 2, 0, Math.PI * 2);
|
|
8569
|
+
ctx.fill();
|
|
8570
|
+
ctx.stroke();
|
|
8571
|
+
ctx.setLineDash([4 / zoom, 4 / zoom]);
|
|
8572
|
+
}
|
|
8573
|
+
}
|
|
8376
8574
|
}
|
|
8377
8575
|
if (p.selectedIds.length === 1 && ROTATABLE_TYPES.has(el.type)) {
|
|
8378
8576
|
const stemStart = topMidpoint(layout);
|
|
@@ -8518,6 +8716,7 @@ function hitTestTemplateResizeHandle(world, ctx, selectedIds) {
|
|
|
8518
8716
|
for (const id of selectedIds) {
|
|
8519
8717
|
const el = ctx.store.getById(id);
|
|
8520
8718
|
if (!el || el.type !== "template") continue;
|
|
8719
|
+
if (el.templateShape === "rectangle") continue;
|
|
8521
8720
|
const bounds = getElementBounds(el);
|
|
8522
8721
|
if (!bounds) continue;
|
|
8523
8722
|
const hx = bounds.x + bounds.w;
|
|
@@ -8528,6 +8727,50 @@ function hitTestTemplateResizeHandle(world, ctx, selectedIds) {
|
|
|
8528
8727
|
}
|
|
8529
8728
|
return null;
|
|
8530
8729
|
}
|
|
8730
|
+
function hitTestTemplateAimHandle(world, ctx, selectedIds) {
|
|
8731
|
+
if (selectedIds.length !== 1) return null;
|
|
8732
|
+
const id = selectedIds[0];
|
|
8733
|
+
if (!id) return null;
|
|
8734
|
+
const el = ctx.store.getById(id);
|
|
8735
|
+
if (!el || el.locked) return null;
|
|
8736
|
+
const knob = templateAimKnob(el, ctx.camera.zoom);
|
|
8737
|
+
if (!knob) return null;
|
|
8738
|
+
const r = (HANDLE_SIZE / 2 + HANDLE_HIT_PADDING2) / ctx.camera.zoom;
|
|
8739
|
+
const dx = world.x - knob.knob.x;
|
|
8740
|
+
const dy = world.y - knob.knob.y;
|
|
8741
|
+
return dx * dx + dy * dy <= r * r ? { elementId: id } : null;
|
|
8742
|
+
}
|
|
8743
|
+
function hitTestRectangleLengthHandle(world, ctx, selectedIds) {
|
|
8744
|
+
if (selectedIds.length !== 1) return null;
|
|
8745
|
+
const id = selectedIds[0];
|
|
8746
|
+
if (!id) return null;
|
|
8747
|
+
const el = ctx.store.getById(id);
|
|
8748
|
+
if (!el || el.locked || el.type !== "template" || el.templateShape !== "rectangle") return null;
|
|
8749
|
+
const zoom = ctx.camera.zoom;
|
|
8750
|
+
const r = (HANDLE_SIZE / 2 + HANDLE_HIT_PADDING2) / zoom;
|
|
8751
|
+
const hx = el.position.x + el.radius * Math.cos(el.angle);
|
|
8752
|
+
const hy = el.position.y + el.radius * Math.sin(el.angle);
|
|
8753
|
+
const dx = world.x - hx;
|
|
8754
|
+
const dy = world.y - hy;
|
|
8755
|
+
return dx * dx + dy * dy <= r * r ? { elementId: id } : null;
|
|
8756
|
+
}
|
|
8757
|
+
function hitTestRectangleWidthHandle(world, ctx, selectedIds) {
|
|
8758
|
+
if (selectedIds.length !== 1) return null;
|
|
8759
|
+
const id = selectedIds[0];
|
|
8760
|
+
if (!id) return null;
|
|
8761
|
+
const el = ctx.store.getById(id);
|
|
8762
|
+
if (!el || el.locked || el.type !== "template" || el.templateShape !== "rectangle") return null;
|
|
8763
|
+
const zoom = ctx.camera.zoom;
|
|
8764
|
+
const r = (HANDLE_SIZE / 2 + HANDLE_HIT_PADDING2) / zoom;
|
|
8765
|
+
const cos = Math.cos(el.angle);
|
|
8766
|
+
const sin = Math.sin(el.angle);
|
|
8767
|
+
const halfW = (el.width ?? 0) / 2;
|
|
8768
|
+
const hx = el.position.x + el.radius / 2 * cos + halfW * -sin;
|
|
8769
|
+
const hy = el.position.y + el.radius / 2 * sin + halfW * cos;
|
|
8770
|
+
const dx = world.x - hx;
|
|
8771
|
+
const dy = world.y - hy;
|
|
8772
|
+
return dx * dx + dy * dy <= r * r ? { elementId: id } : null;
|
|
8773
|
+
}
|
|
8531
8774
|
function findElementsInRect(marquee, ctx) {
|
|
8532
8775
|
const candidates = ctx.store.queryRect(marquee);
|
|
8533
8776
|
const ids = [];
|
|
@@ -8682,6 +8925,34 @@ function computeTemplateResize(el, world, opts) {
|
|
|
8682
8925
|
}
|
|
8683
8926
|
return updates;
|
|
8684
8927
|
}
|
|
8928
|
+
function computeRectangleLengthResize(el, world, opts) {
|
|
8929
|
+
const cos = Math.cos(el.angle);
|
|
8930
|
+
const sin = Math.sin(el.angle);
|
|
8931
|
+
let len = (world.x - el.position.x) * cos + (world.y - el.position.y) * sin;
|
|
8932
|
+
if (opts.snapToGrid && opts.gridSize && opts.gridSize > 0) {
|
|
8933
|
+
const snapUnit = opts.gridType === "hex" ? Math.sqrt(3) * opts.gridSize : opts.gridSize;
|
|
8934
|
+
len = Math.max(snapUnit, Math.round(len / snapUnit) * snapUnit);
|
|
8935
|
+
}
|
|
8936
|
+
len = Math.max(MIN_ELEMENT_SIZE, len);
|
|
8937
|
+
const updates = { radius: len };
|
|
8938
|
+
if (el.feetPerCell != null && opts.gridSize && opts.gridSize > 0) {
|
|
8939
|
+
const snapUnit = opts.gridType === "hex" ? Math.sqrt(3) * opts.gridSize : opts.gridSize;
|
|
8940
|
+
updates.radiusFeet = len / snapUnit * el.feetPerCell;
|
|
8941
|
+
}
|
|
8942
|
+
return updates;
|
|
8943
|
+
}
|
|
8944
|
+
function computeRectangleWidthResize(el, world, opts) {
|
|
8945
|
+
const cos = Math.cos(el.angle);
|
|
8946
|
+
const sin = Math.sin(el.angle);
|
|
8947
|
+
const perp = Math.abs(-(world.x - el.position.x) * sin + (world.y - el.position.y) * cos);
|
|
8948
|
+
let width = perp * 2;
|
|
8949
|
+
if (opts.snapToGrid && opts.gridSize && opts.gridSize > 0) {
|
|
8950
|
+
const snapUnit = opts.gridType === "hex" ? Math.sqrt(3) * opts.gridSize : opts.gridSize;
|
|
8951
|
+
width = Math.max(snapUnit, Math.round(width / snapUnit) * snapUnit);
|
|
8952
|
+
}
|
|
8953
|
+
width = Math.max(MIN_ELEMENT_SIZE, width);
|
|
8954
|
+
return { width };
|
|
8955
|
+
}
|
|
8685
8956
|
|
|
8686
8957
|
// src/tools/select-tool.ts
|
|
8687
8958
|
var SNAP_PX = 6;
|
|
@@ -8777,6 +9048,24 @@ var SelectTool = class {
|
|
|
8777
9048
|
ctx.requestRender();
|
|
8778
9049
|
return;
|
|
8779
9050
|
}
|
|
9051
|
+
const rectLengthHit = hitTestRectangleLengthHandle(world, ctx, this._selectedIds);
|
|
9052
|
+
if (rectLengthHit) {
|
|
9053
|
+
this.mode = { type: "resizing-rect-length", elementId: rectLengthHit.elementId };
|
|
9054
|
+
ctx.requestRender();
|
|
9055
|
+
return;
|
|
9056
|
+
}
|
|
9057
|
+
const rectWidthHit = hitTestRectangleWidthHandle(world, ctx, this._selectedIds);
|
|
9058
|
+
if (rectWidthHit) {
|
|
9059
|
+
this.mode = { type: "resizing-rect-width", elementId: rectWidthHit.elementId };
|
|
9060
|
+
ctx.requestRender();
|
|
9061
|
+
return;
|
|
9062
|
+
}
|
|
9063
|
+
const aimHit = hitTestTemplateAimHandle(world, ctx, this._selectedIds);
|
|
9064
|
+
if (aimHit) {
|
|
9065
|
+
this.mode = { type: "aiming-template", elementId: aimHit.elementId };
|
|
9066
|
+
ctx.requestRender();
|
|
9067
|
+
return;
|
|
9068
|
+
}
|
|
8780
9069
|
const rotateHit = hitTestRotateHandle(world, ctx, this._selectedIds);
|
|
8781
9070
|
if (rotateHit) {
|
|
8782
9071
|
const el = ctx.store.getById(rotateHit.elementId);
|
|
@@ -8858,6 +9147,32 @@ var SelectTool = class {
|
|
|
8858
9147
|
this.handleTemplateResize(world, ctx);
|
|
8859
9148
|
return;
|
|
8860
9149
|
}
|
|
9150
|
+
if (this.mode.type === "aiming-template") {
|
|
9151
|
+
const el = ctx.store.getById(this.mode.elementId);
|
|
9152
|
+
if (el && el.type === "template" && !el.locked) {
|
|
9153
|
+
let a = Math.atan2(world.y - el.position.y, world.x - el.position.x);
|
|
9154
|
+
if (state.shiftKey) {
|
|
9155
|
+
const snap = ctx.gridType === "hex" ? Math.PI / 3 : ROTATE_SNAP;
|
|
9156
|
+
a = Math.round(a / snap) * snap;
|
|
9157
|
+
}
|
|
9158
|
+
ctx.store.update(this.mode.elementId, { angle: normalizeAngle(a) });
|
|
9159
|
+
ctx.requestRender();
|
|
9160
|
+
}
|
|
9161
|
+
return;
|
|
9162
|
+
}
|
|
9163
|
+
if (this.mode.type === "resizing-rect-length" || this.mode.type === "resizing-rect-width") {
|
|
9164
|
+
ctx.setCursor?.(this.mode.type === "resizing-rect-length" ? "ew-resize" : "ns-resize");
|
|
9165
|
+
const el = ctx.store.getById(this.mode.elementId);
|
|
9166
|
+
if (el && el.type === "template" && !el.locked) {
|
|
9167
|
+
const opts = { snapToGrid: ctx.snapToGrid, gridSize: ctx.gridSize, gridType: ctx.gridType };
|
|
9168
|
+
const patch = this.mode.type === "resizing-rect-length" ? computeRectangleLengthResize(el, world, opts) : computeRectangleWidthResize(el, world, opts);
|
|
9169
|
+
if (patch) {
|
|
9170
|
+
ctx.store.update(this.mode.elementId, patch);
|
|
9171
|
+
ctx.requestRender();
|
|
9172
|
+
}
|
|
9173
|
+
}
|
|
9174
|
+
return;
|
|
9175
|
+
}
|
|
8861
9176
|
if (this.mode.type === "rotating") {
|
|
8862
9177
|
const { elementId, center: center2, startPointerAngle, startRotation } = this.mode;
|
|
8863
9178
|
const a = Math.atan2(world.y - center2.y, world.x - center2.x);
|
|
@@ -9064,6 +9379,18 @@ var SelectTool = class {
|
|
|
9064
9379
|
ctx.setCursor?.("nwse-resize");
|
|
9065
9380
|
return null;
|
|
9066
9381
|
}
|
|
9382
|
+
if (hitTestRectangleLengthHandle(world, ctx, this._selectedIds)) {
|
|
9383
|
+
ctx.setCursor?.("ew-resize");
|
|
9384
|
+
return null;
|
|
9385
|
+
}
|
|
9386
|
+
if (hitTestRectangleWidthHandle(world, ctx, this._selectedIds)) {
|
|
9387
|
+
ctx.setCursor?.("ns-resize");
|
|
9388
|
+
return null;
|
|
9389
|
+
}
|
|
9390
|
+
if (hitTestTemplateAimHandle(world, ctx, this._selectedIds)) {
|
|
9391
|
+
ctx.setCursor?.("grab");
|
|
9392
|
+
return null;
|
|
9393
|
+
}
|
|
9067
9394
|
if (hitTestRotateHandle(world, ctx, this._selectedIds)) {
|
|
9068
9395
|
ctx.setCursor?.("grab");
|
|
9069
9396
|
return null;
|
|
@@ -9724,6 +10051,11 @@ var MeasureTool = class {
|
|
|
9724
10051
|
};
|
|
9725
10052
|
|
|
9726
10053
|
// src/tools/template-tool.ts
|
|
10054
|
+
var MIN_RECT_WIDTH = 20;
|
|
10055
|
+
function defaultRectWidth(radius, scaleUnit) {
|
|
10056
|
+
if (scaleUnit > 0) return scaleUnit;
|
|
10057
|
+
return Math.max(MIN_RECT_WIDTH, radius * 0.25);
|
|
10058
|
+
}
|
|
9727
10059
|
var TemplateTool = class {
|
|
9728
10060
|
name = "template";
|
|
9729
10061
|
drawing = false;
|
|
@@ -9733,6 +10065,7 @@ var TemplateTool = class {
|
|
|
9733
10065
|
gridType;
|
|
9734
10066
|
hexOrientation;
|
|
9735
10067
|
snapEnabled = false;
|
|
10068
|
+
feetScaleUnit = 0;
|
|
9736
10069
|
templateShape;
|
|
9737
10070
|
fillColor;
|
|
9738
10071
|
strokeColor;
|
|
@@ -9781,6 +10114,7 @@ var TemplateTool = class {
|
|
|
9781
10114
|
this.gridType = ctx.gridType;
|
|
9782
10115
|
this.hexOrientation = ctx.hexOrientation;
|
|
9783
10116
|
this.snapEnabled = !!ctx.gridType || (ctx.snapToGrid ?? false);
|
|
10117
|
+
this.feetScaleUnit = ctx.gridSize && ctx.gridSize > 0 ? ctx.gridType === "hex" ? Math.sqrt(3) * ctx.gridSize : ctx.gridSize : 0;
|
|
9784
10118
|
const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
|
|
9785
10119
|
this.origin = this.snapToGrid(world, ctx);
|
|
9786
10120
|
this.current = { ...this.origin };
|
|
@@ -9800,11 +10134,13 @@ var TemplateTool = class {
|
|
|
9800
10134
|
const snapUnit = gridSize && gridSize > 0 ? ctx.gridType === "hex" ? Math.sqrt(3) * gridSize : gridSize : 0;
|
|
9801
10135
|
const cells = snapUnit > 0 ? radius / snapUnit : 0;
|
|
9802
10136
|
const radiusFeet = cells * this.feetPerCell;
|
|
10137
|
+
const width = this.templateShape === "rectangle" ? defaultRectWidth(radius, this.feetScaleUnit) : void 0;
|
|
9803
10138
|
const element = createTemplate({
|
|
9804
10139
|
position: { ...this.origin },
|
|
9805
10140
|
templateShape: this.templateShape,
|
|
9806
10141
|
radius,
|
|
9807
10142
|
angle,
|
|
10143
|
+
width,
|
|
9808
10144
|
fillColor: this.fillColor,
|
|
9809
10145
|
strokeColor: this.strokeColor,
|
|
9810
10146
|
strokeWidth: this.strokeWidth,
|
|
@@ -9879,7 +10215,24 @@ var TemplateTool = class {
|
|
|
9879
10215
|
ctx.stroke();
|
|
9880
10216
|
break;
|
|
9881
10217
|
}
|
|
10218
|
+
case "rectangle": {
|
|
10219
|
+
const halfW = defaultRectWidth(radius, this.feetScaleUnit) / 2;
|
|
10220
|
+
const cos = Math.cos(angle);
|
|
10221
|
+
const sin = Math.sin(angle);
|
|
10222
|
+
const perpX = -sin * halfW;
|
|
10223
|
+
const perpY = cos * halfW;
|
|
10224
|
+
ctx.beginPath();
|
|
10225
|
+
ctx.moveTo(cx + perpX, cy + perpY);
|
|
10226
|
+
ctx.lineTo(cx + radius * cos + perpX, cy + radius * sin + perpY);
|
|
10227
|
+
ctx.lineTo(cx + radius * cos - perpX, cy + radius * sin - perpY);
|
|
10228
|
+
ctx.lineTo(cx - perpX, cy - perpY);
|
|
10229
|
+
ctx.closePath();
|
|
10230
|
+
ctx.fill();
|
|
10231
|
+
ctx.stroke();
|
|
10232
|
+
break;
|
|
10233
|
+
}
|
|
9882
10234
|
}
|
|
10235
|
+
this.drawFeetLabel(ctx, radius);
|
|
9883
10236
|
ctx.restore();
|
|
9884
10237
|
}
|
|
9885
10238
|
renderHexOverlay(ctx, radius) {
|
|
@@ -9904,6 +10257,18 @@ var TemplateTool = class {
|
|
|
9904
10257
|
case "square":
|
|
9905
10258
|
hexCells = getHexCellsInSquare(center2, radiusCells, cellSize, orientation);
|
|
9906
10259
|
break;
|
|
10260
|
+
case "rectangle": {
|
|
10261
|
+
const widthCells = defaultRectWidth(radius, this.feetScaleUnit) / snapUnit;
|
|
10262
|
+
hexCells = getHexCellsInRectangle(
|
|
10263
|
+
center2,
|
|
10264
|
+
angle,
|
|
10265
|
+
radiusCells,
|
|
10266
|
+
widthCells,
|
|
10267
|
+
cellSize,
|
|
10268
|
+
orientation
|
|
10269
|
+
);
|
|
10270
|
+
break;
|
|
10271
|
+
}
|
|
9907
10272
|
}
|
|
9908
10273
|
ctx.save();
|
|
9909
10274
|
ctx.globalAlpha = 0.4;
|
|
@@ -9920,7 +10285,7 @@ var TemplateTool = class {
|
|
|
9920
10285
|
ctx.strokeStyle = this.strokeColor;
|
|
9921
10286
|
ctx.lineWidth = this.strokeWidth;
|
|
9922
10287
|
ctx.stroke();
|
|
9923
|
-
if (this.templateShape === "cone" || this.templateShape === "line" || this.templateShape === "circle" || this.templateShape === "square") {
|
|
10288
|
+
if (this.templateShape === "cone" || this.templateShape === "line" || this.templateShape === "circle" || this.templateShape === "square" || this.templateShape === "rectangle") {
|
|
9924
10289
|
ctx.globalAlpha = 0.5;
|
|
9925
10290
|
ctx.beginPath();
|
|
9926
10291
|
drawHexPath(ctx, center2.x, center2.y, cellSize, orientation);
|
|
@@ -9930,30 +10295,7 @@ var TemplateTool = class {
|
|
|
9930
10295
|
ctx.lineWidth = this.strokeWidth;
|
|
9931
10296
|
ctx.stroke();
|
|
9932
10297
|
}
|
|
9933
|
-
|
|
9934
|
-
const feet = radiusCells * this.feetPerCell;
|
|
9935
|
-
if (feet > 0) {
|
|
9936
|
-
ctx.globalAlpha = 1;
|
|
9937
|
-
const label = `${Math.round(feet)} ft`;
|
|
9938
|
-
const fontSize = Math.max(10, Math.min(14, radius * 0.15));
|
|
9939
|
-
ctx.font = `bold ${fontSize}px system-ui, sans-serif`;
|
|
9940
|
-
ctx.textAlign = "center";
|
|
9941
|
-
ctx.textBaseline = "bottom";
|
|
9942
|
-
const textX = center2.x;
|
|
9943
|
-
const textY = center2.y - 4;
|
|
9944
|
-
const metrics = ctx.measureText(label);
|
|
9945
|
-
const padX = 4;
|
|
9946
|
-
const padY = 2;
|
|
9947
|
-
const textW = metrics.width + padX * 2;
|
|
9948
|
-
const textH = fontSize + padY * 2;
|
|
9949
|
-
ctx.fillStyle = "rgba(255, 255, 255, 0.85)";
|
|
9950
|
-
ctx.beginPath();
|
|
9951
|
-
ctx.roundRect(textX - textW / 2, textY - textH, textW, textH, 3);
|
|
9952
|
-
ctx.fill();
|
|
9953
|
-
ctx.fillStyle = this.strokeColor;
|
|
9954
|
-
ctx.fillText(label, textX, textY - padY);
|
|
9955
|
-
}
|
|
9956
|
-
}
|
|
10298
|
+
this.drawFeetLabel(ctx, radius);
|
|
9957
10299
|
ctx.restore();
|
|
9958
10300
|
}
|
|
9959
10301
|
computeRadius() {
|
|
@@ -9971,6 +10313,17 @@ var TemplateTool = class {
|
|
|
9971
10313
|
const dy = this.current.y - this.origin.y;
|
|
9972
10314
|
return Math.atan2(dy, dx);
|
|
9973
10315
|
}
|
|
10316
|
+
drawFeetLabel(ctx, radius) {
|
|
10317
|
+
const feet = this.feetScaleUnit > 0 ? radius / this.feetScaleUnit * this.feetPerCell : 0;
|
|
10318
|
+
renderTemplateFeetLabel(ctx, {
|
|
10319
|
+
position: this.origin,
|
|
10320
|
+
radius,
|
|
10321
|
+
angle: this.computeAngle(),
|
|
10322
|
+
templateShape: this.templateShape,
|
|
10323
|
+
feet,
|
|
10324
|
+
color: this.strokeColor
|
|
10325
|
+
});
|
|
10326
|
+
}
|
|
9974
10327
|
snapToGrid(point, ctx) {
|
|
9975
10328
|
if (!ctx.gridSize) return point;
|
|
9976
10329
|
if (ctx.gridType === "hex" && ctx.hexOrientation) {
|
|
@@ -10100,7 +10453,7 @@ var LaserTool = class {
|
|
|
10100
10453
|
};
|
|
10101
10454
|
|
|
10102
10455
|
// src/index.ts
|
|
10103
|
-
var VERSION = "0.
|
|
10456
|
+
var VERSION = "0.48.0";
|
|
10104
10457
|
export {
|
|
10105
10458
|
ArrowTool,
|
|
10106
10459
|
AutoSave,
|
|
@@ -10151,6 +10504,7 @@ export {
|
|
|
10151
10504
|
getHexCellsInCone,
|
|
10152
10505
|
getHexCellsInLine,
|
|
10153
10506
|
getHexCellsInRadius,
|
|
10507
|
+
getHexCellsInRectangle,
|
|
10154
10508
|
getHexCellsInSquare,
|
|
10155
10509
|
getHexDistance,
|
|
10156
10510
|
isNearBezier,
|