@fieldnotes/core 0.47.0 → 0.49.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 +369 -21
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -3
- package/dist/index.d.ts +11 -3
- package/dist/index.js +368 -21
- 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) {
|
|
@@ -1135,6 +1155,14 @@ var KeyboardActions = class {
|
|
|
1135
1155
|
if (this.deps.isToolActive()) return;
|
|
1136
1156
|
this.deps.toggleLock?.();
|
|
1137
1157
|
}
|
|
1158
|
+
rotate(direction) {
|
|
1159
|
+
if (this.deps.isToolActive()) return;
|
|
1160
|
+
this.flushPendingNudge();
|
|
1161
|
+
const sel = this.selectTool();
|
|
1162
|
+
if (!sel) return;
|
|
1163
|
+
if (sel.tool.selectedIds.length === 0) return;
|
|
1164
|
+
this.deps.rotate?.(direction);
|
|
1165
|
+
}
|
|
1138
1166
|
zOrder(operation) {
|
|
1139
1167
|
if (this.deps.isToolActive()) return;
|
|
1140
1168
|
this.flushPendingNudge();
|
|
@@ -1239,6 +1267,8 @@ var DEFAULT_BINDINGS = [
|
|
|
1239
1267
|
["ungroup", ["mod+shift+g"]],
|
|
1240
1268
|
["cut", ["mod+x"]],
|
|
1241
1269
|
["toggle-lock", ["mod+shift+l"]],
|
|
1270
|
+
["rotate-cw", ["r"]],
|
|
1271
|
+
["rotate-ccw", ["shift+r"]],
|
|
1242
1272
|
["nudge-left", ["arrowleft"]],
|
|
1243
1273
|
["nudge-right", ["arrowright"]],
|
|
1244
1274
|
["nudge-up", ["arrowup"]],
|
|
@@ -1551,6 +1581,14 @@ var KeyboardHandler = class {
|
|
|
1551
1581
|
e?.preventDefault();
|
|
1552
1582
|
this.deps.actions.toggleLock();
|
|
1553
1583
|
return;
|
|
1584
|
+
case "rotate-cw":
|
|
1585
|
+
e?.preventDefault();
|
|
1586
|
+
this.deps.actions.rotate("cw");
|
|
1587
|
+
return;
|
|
1588
|
+
case "rotate-ccw":
|
|
1589
|
+
e?.preventDefault();
|
|
1590
|
+
this.deps.actions.rotate("ccw");
|
|
1591
|
+
return;
|
|
1554
1592
|
case "zoom-in":
|
|
1555
1593
|
e?.preventDefault();
|
|
1556
1594
|
this.zoomByFactor(ZOOM_STEP);
|
|
@@ -1687,6 +1725,7 @@ var InputHandler = class {
|
|
|
1687
1725
|
group: options.group,
|
|
1688
1726
|
ungroup: options.ungroup,
|
|
1689
1727
|
toggleLock: options.toggleLock,
|
|
1728
|
+
rotate: options.rotate,
|
|
1690
1729
|
getLastPointerWorld: () => this.lastPointerWorld()
|
|
1691
1730
|
});
|
|
1692
1731
|
this.openContextMenu = options.openContextMenu;
|
|
@@ -3200,6 +3239,46 @@ function getHexCellsInLine(center2, angle, radiusCells, cellSize, orientation) {
|
|
|
3200
3239
|
}
|
|
3201
3240
|
return cells;
|
|
3202
3241
|
}
|
|
3242
|
+
function getHexCellsInRectangle(center2, angle, lengthCells, widthCells, cellSize, orientation) {
|
|
3243
|
+
const nLen = Math.round(lengthCells);
|
|
3244
|
+
const wCells = Math.max(1, Math.round(widthCells));
|
|
3245
|
+
const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
|
|
3246
|
+
const cube = offsetToCube(off.col, off.row, orientation);
|
|
3247
|
+
const centerPixel = offsetToPixel(off.col, off.row, cellSize, orientation);
|
|
3248
|
+
if (nLen <= 0) return [centerPixel];
|
|
3249
|
+
const vertexOffset = orientation === "pointy" ? Math.PI / 6 : 0;
|
|
3250
|
+
const step = Math.PI / 3;
|
|
3251
|
+
const snappedAngle = Math.round((angle - vertexOffset) / step) * step + vertexOffset;
|
|
3252
|
+
const cos = Math.cos(snappedAngle);
|
|
3253
|
+
const sin = Math.sin(snappedAngle);
|
|
3254
|
+
const snapUnit = Math.sqrt(3) * cellSize;
|
|
3255
|
+
const lineLength = nLen * snapUnit;
|
|
3256
|
+
const halfWidth = wCells * snapUnit / 2 + 1e-6;
|
|
3257
|
+
const iterM = nLen + Math.ceil(wCells / 2) + 1;
|
|
3258
|
+
const cells = [];
|
|
3259
|
+
for (let dq = -iterM; dq <= iterM; dq++) {
|
|
3260
|
+
const rMin = Math.max(-iterM, -dq - iterM);
|
|
3261
|
+
const rMax = Math.min(iterM, -dq + iterM);
|
|
3262
|
+
for (let dr = rMin; dr <= rMax; dr++) {
|
|
3263
|
+
const absQ = cube.q + dq;
|
|
3264
|
+
const absR = cube.r + dr;
|
|
3265
|
+
const pixel = offsetToPixel(
|
|
3266
|
+
cubeToOffset(absQ, absR, orientation).col,
|
|
3267
|
+
cubeToOffset(absQ, absR, orientation).row,
|
|
3268
|
+
cellSize,
|
|
3269
|
+
orientation
|
|
3270
|
+
);
|
|
3271
|
+
const dx = pixel.x - centerPixel.x;
|
|
3272
|
+
const dy = pixel.y - centerPixel.y;
|
|
3273
|
+
const along = dx * cos + dy * sin;
|
|
3274
|
+
const perp = Math.abs(-dx * sin + dy * cos);
|
|
3275
|
+
if (along >= -snapUnit * 0.1 && along <= lineLength + snapUnit * 0.1 && perp <= halfWidth) {
|
|
3276
|
+
cells.push(pixel);
|
|
3277
|
+
}
|
|
3278
|
+
}
|
|
3279
|
+
}
|
|
3280
|
+
return cells;
|
|
3281
|
+
}
|
|
3203
3282
|
function getHexCellsInSquare(center2, radiusCells, cellSize, orientation) {
|
|
3204
3283
|
const n2 = Math.round(radiusCells);
|
|
3205
3284
|
const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
|
|
@@ -3241,7 +3320,7 @@ function drawHexPath(ctx, cx, cy, cellSize, orientation) {
|
|
|
3241
3320
|
// src/elements/renderers/template-measure.ts
|
|
3242
3321
|
function renderTemplateFeetLabel(ctx, p) {
|
|
3243
3322
|
if (p.feet <= 0) return;
|
|
3244
|
-
const directional = p.templateShape === "cone" || p.templateShape === "line";
|
|
3323
|
+
const directional = p.templateShape === "cone" || p.templateShape === "line" || p.templateShape === "rectangle";
|
|
3245
3324
|
const dir = directional ? p.angle : 0;
|
|
3246
3325
|
const length = p.templateShape === "square" ? p.radius / 2 : p.radius;
|
|
3247
3326
|
const cos = Math.cos(dir);
|
|
@@ -3334,6 +3413,22 @@ function renderGeometricTemplate(ctx, template) {
|
|
|
3334
3413
|
ctx.stroke();
|
|
3335
3414
|
break;
|
|
3336
3415
|
}
|
|
3416
|
+
case "rectangle": {
|
|
3417
|
+
const halfW = (template.width ?? 0) / 2;
|
|
3418
|
+
const cos = Math.cos(template.angle);
|
|
3419
|
+
const sin = Math.sin(template.angle);
|
|
3420
|
+
const perpX = -sin * halfW;
|
|
3421
|
+
const perpY = cos * halfW;
|
|
3422
|
+
ctx.beginPath();
|
|
3423
|
+
ctx.moveTo(cx + perpX, cy + perpY);
|
|
3424
|
+
ctx.lineTo(cx + r * cos + perpX, cy + r * sin + perpY);
|
|
3425
|
+
ctx.lineTo(cx + r * cos - perpX, cy + r * sin - perpY);
|
|
3426
|
+
ctx.lineTo(cx - perpX, cy - perpY);
|
|
3427
|
+
ctx.closePath();
|
|
3428
|
+
ctx.fill();
|
|
3429
|
+
ctx.stroke();
|
|
3430
|
+
break;
|
|
3431
|
+
}
|
|
3337
3432
|
}
|
|
3338
3433
|
if (template.radiusFeet != null && template.radiusFeet > 0) {
|
|
3339
3434
|
renderTemplateFeetLabel(ctx, {
|
|
@@ -3365,6 +3460,18 @@ function renderHexTemplate(ctx, template, cellSize, orientation) {
|
|
|
3365
3460
|
case "square":
|
|
3366
3461
|
cells = getHexCellsInSquare(center2, radiusCells, cellSize, orientation);
|
|
3367
3462
|
break;
|
|
3463
|
+
case "rectangle": {
|
|
3464
|
+
const widthCells = (template.width ?? 0) / snapUnit;
|
|
3465
|
+
cells = getHexCellsInRectangle(
|
|
3466
|
+
center2,
|
|
3467
|
+
template.angle,
|
|
3468
|
+
radiusCells,
|
|
3469
|
+
widthCells,
|
|
3470
|
+
cellSize,
|
|
3471
|
+
orientation
|
|
3472
|
+
);
|
|
3473
|
+
break;
|
|
3474
|
+
}
|
|
3368
3475
|
}
|
|
3369
3476
|
ctx.save();
|
|
3370
3477
|
ctx.globalAlpha = template.opacity;
|
|
@@ -3941,7 +4048,8 @@ function createTemplate(input) {
|
|
|
3941
4048
|
opacity: input.opacity ?? 0.6,
|
|
3942
4049
|
feetPerCell: input.feetPerCell,
|
|
3943
4050
|
radiusFeet: input.radiusFeet,
|
|
3944
|
-
...input.renderStyle !== void 0 ? { renderStyle: input.renderStyle } : {}
|
|
4051
|
+
...input.renderStyle !== void 0 ? { renderStyle: input.renderStyle } : {},
|
|
4052
|
+
...input.width !== void 0 ? { width: input.width } : {}
|
|
3945
4053
|
};
|
|
3946
4054
|
}
|
|
3947
4055
|
|
|
@@ -5538,6 +5646,20 @@ function emitGeometricTemplate(t) {
|
|
|
5538
5646
|
].map(([px, py]) => `${n(px ?? 0)},${n(py ?? 0)}`).join(" ");
|
|
5539
5647
|
return `<polygon points="${pts}" ${attrs} />`;
|
|
5540
5648
|
}
|
|
5649
|
+
case "rectangle": {
|
|
5650
|
+
const halfW = (t.width ?? 0) / 2;
|
|
5651
|
+
const cos = Math.cos(t.angle);
|
|
5652
|
+
const sin = Math.sin(t.angle);
|
|
5653
|
+
const perpX = -sin * halfW;
|
|
5654
|
+
const perpY = cos * halfW;
|
|
5655
|
+
const pts = [
|
|
5656
|
+
[cx + perpX, cy + perpY],
|
|
5657
|
+
[cx + r * cos + perpX, cy + r * sin + perpY],
|
|
5658
|
+
[cx + r * cos - perpX, cy + r * sin - perpY],
|
|
5659
|
+
[cx - perpX, cy - perpY]
|
|
5660
|
+
].map(([px, py]) => `${n(px ?? 0)},${n(py ?? 0)}`).join(" ");
|
|
5661
|
+
return `<polygon points="${pts}" ${attrs} />`;
|
|
5662
|
+
}
|
|
5541
5663
|
}
|
|
5542
5664
|
}
|
|
5543
5665
|
function emitHexTemplate(t, grid) {
|
|
@@ -5560,6 +5682,18 @@ function emitHexTemplate(t, grid) {
|
|
|
5560
5682
|
case "square":
|
|
5561
5683
|
cells = getHexCellsInSquare(center2, radiusCells, cellSize, orientation);
|
|
5562
5684
|
break;
|
|
5685
|
+
case "rectangle": {
|
|
5686
|
+
const widthCells = (t.width ?? 0) / snapUnit;
|
|
5687
|
+
cells = getHexCellsInRectangle(
|
|
5688
|
+
center2,
|
|
5689
|
+
t.angle,
|
|
5690
|
+
radiusCells,
|
|
5691
|
+
widthCells,
|
|
5692
|
+
cellSize,
|
|
5693
|
+
orientation
|
|
5694
|
+
);
|
|
5695
|
+
break;
|
|
5696
|
+
}
|
|
5563
5697
|
}
|
|
5564
5698
|
let d = "";
|
|
5565
5699
|
for (const cell of cells) {
|
|
@@ -6680,7 +6814,7 @@ function getElementStyle(element) {
|
|
|
6680
6814
|
}
|
|
6681
6815
|
}
|
|
6682
6816
|
|
|
6683
|
-
// src/canvas/selection-
|
|
6817
|
+
// src/canvas/selection-rotate.ts
|
|
6684
6818
|
function unionBounds2(list) {
|
|
6685
6819
|
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
6686
6820
|
for (const b of list) {
|
|
@@ -6691,6 +6825,39 @@ function unionBounds2(list) {
|
|
|
6691
6825
|
}
|
|
6692
6826
|
return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
|
|
6693
6827
|
}
|
|
6828
|
+
function rotationPivot(items) {
|
|
6829
|
+
const first = items[0];
|
|
6830
|
+
if (items.length === 1 && first) {
|
|
6831
|
+
return { x: first.bounds.x + first.bounds.w / 2, y: first.bounds.y + first.bounds.h / 2 };
|
|
6832
|
+
}
|
|
6833
|
+
const u = unionBounds2(items.map((it) => rotatedAABB(it.bounds, it.el.rotation ?? 0)));
|
|
6834
|
+
return { x: u.x + u.w / 2, y: u.y + u.h / 2 };
|
|
6835
|
+
}
|
|
6836
|
+
function rotateElementPatch(el, bounds, pivot, delta) {
|
|
6837
|
+
if (el.type === "arrow") {
|
|
6838
|
+
const center3 = { x: bounds.x + bounds.w / 2, y: bounds.y + bounds.h / 2 };
|
|
6839
|
+
const moved2 = rotatePoint(center3, pivot, delta);
|
|
6840
|
+
return {
|
|
6841
|
+
position: { x: el.position.x + moved2.x - center3.x, y: el.position.y + moved2.y - center3.y },
|
|
6842
|
+
from: rotatePoint(el.from, pivot, delta),
|
|
6843
|
+
to: rotatePoint(el.to, pivot, delta)
|
|
6844
|
+
};
|
|
6845
|
+
}
|
|
6846
|
+
if (el.type === "template") {
|
|
6847
|
+
return {
|
|
6848
|
+
position: rotatePoint(el.position, pivot, delta),
|
|
6849
|
+
angle: normalizeAngle(el.angle + delta)
|
|
6850
|
+
};
|
|
6851
|
+
}
|
|
6852
|
+
const center2 = { x: bounds.x + bounds.w / 2, y: bounds.y + bounds.h / 2 };
|
|
6853
|
+
const moved = rotatePoint(center2, pivot, delta);
|
|
6854
|
+
return {
|
|
6855
|
+
position: { x: el.position.x + moved.x - center2.x, y: el.position.y + moved.y - center2.y },
|
|
6856
|
+
rotation: normalizeAngle((el.rotation ?? 0) + delta)
|
|
6857
|
+
};
|
|
6858
|
+
}
|
|
6859
|
+
|
|
6860
|
+
// src/canvas/selection-ops.ts
|
|
6694
6861
|
function sharedValue(values) {
|
|
6695
6862
|
const present = values.filter((v) => v !== void 0);
|
|
6696
6863
|
if (present.length === 0) return void 0;
|
|
@@ -6838,6 +7005,21 @@ var SelectionOps = class {
|
|
|
6838
7005
|
this.deps.recorder.commit();
|
|
6839
7006
|
this.deps.requestRender();
|
|
6840
7007
|
}
|
|
7008
|
+
rotateSelection(direction) {
|
|
7009
|
+
const eligible = this.boundedSelection().filter(({ el }) => this.isMovable(el));
|
|
7010
|
+
if (eligible.length === 0) return;
|
|
7011
|
+
const delta = direction === "cw" ? Math.PI / 2 : -Math.PI / 2;
|
|
7012
|
+
const pivot = rotationPivot(eligible);
|
|
7013
|
+
this.deps.recorder.begin();
|
|
7014
|
+
const moved = [];
|
|
7015
|
+
for (const { id, el, bounds } of eligible) {
|
|
7016
|
+
this.deps.store.update(id, rotateElementPatch(el, bounds, pivot, delta));
|
|
7017
|
+
moved.push(id);
|
|
7018
|
+
}
|
|
7019
|
+
updateArrowsBoundToElements(moved, this.deps.store);
|
|
7020
|
+
this.deps.recorder.commit();
|
|
7021
|
+
this.deps.requestRender();
|
|
7022
|
+
}
|
|
6841
7023
|
boundedSelection() {
|
|
6842
7024
|
const out = [];
|
|
6843
7025
|
for (const id of this.deps.getSelectedIds()) {
|
|
@@ -7166,6 +7348,7 @@ var Viewport = class {
|
|
|
7166
7348
|
group: () => this.groupSelection(),
|
|
7167
7349
|
ungroup: () => this.ungroupSelection(),
|
|
7168
7350
|
toggleLock: () => this.toggleLockSelection(),
|
|
7351
|
+
rotate: (direction) => this.rotateSelection(direction),
|
|
7169
7352
|
openContextMenu: (screenPos, world) => {
|
|
7170
7353
|
this.getSelectTool()?.selectAtPoint(world, this.toolContext);
|
|
7171
7354
|
this.openContextMenu(screenPos);
|
|
@@ -7563,6 +7746,8 @@ var Viewport = class {
|
|
|
7563
7746
|
items.push({ label: "Bring Forward", action: "z-forward" });
|
|
7564
7747
|
items.push({ label: "Send Backward", action: "z-backward" });
|
|
7565
7748
|
items.push({ label: "Send to Back", action: "z-back" });
|
|
7749
|
+
items.push({ label: "Rotate 90\xB0 CW", action: "rotate-cw" });
|
|
7750
|
+
items.push({ label: "Rotate 90\xB0 CCW", action: "rotate-ccw" });
|
|
7566
7751
|
const allLocked = ids.every((id) => this.store.getById(id)?.locked);
|
|
7567
7752
|
items.push({ label: allLocked ? "Unlock" : "Lock", action: "toggle-lock" });
|
|
7568
7753
|
} else if (this.canPaste()) {
|
|
@@ -7596,6 +7781,9 @@ var Viewport = class {
|
|
|
7596
7781
|
distributeSelection(axis) {
|
|
7597
7782
|
this.selectionOps.distribute(axis);
|
|
7598
7783
|
}
|
|
7784
|
+
rotateSelection(direction) {
|
|
7785
|
+
this.selectionOps.rotateSelection(direction);
|
|
7786
|
+
}
|
|
7599
7787
|
getRenderStats() {
|
|
7600
7788
|
return this.renderLoop.getStats();
|
|
7601
7789
|
}
|
|
@@ -8250,7 +8438,8 @@ function getOverlayLayout(el, zoom) {
|
|
|
8250
8438
|
}
|
|
8251
8439
|
function templateAimKnob(el, zoom) {
|
|
8252
8440
|
if (el.type !== "template") return null;
|
|
8253
|
-
if (el.templateShape !== "cone" && el.templateShape !== "line"
|
|
8441
|
+
if (el.templateShape !== "cone" && el.templateShape !== "line" && el.templateShape !== "rectangle")
|
|
8442
|
+
return null;
|
|
8254
8443
|
const gap = ROTATE_HANDLE_OFFSET / zoom;
|
|
8255
8444
|
const dist = el.radius + gap;
|
|
8256
8445
|
const origin = el.position;
|
|
@@ -8395,22 +8584,51 @@ function renderSelectionBoxes(ctx, p) {
|
|
|
8395
8584
|
} else if (el.type === "template") {
|
|
8396
8585
|
ctx.setLineDash([]);
|
|
8397
8586
|
ctx.fillStyle = "#ffffff";
|
|
8398
|
-
|
|
8399
|
-
|
|
8400
|
-
|
|
8401
|
-
|
|
8402
|
-
|
|
8403
|
-
|
|
8404
|
-
|
|
8405
|
-
|
|
8406
|
-
|
|
8407
|
-
|
|
8408
|
-
|
|
8409
|
-
|
|
8410
|
-
|
|
8411
|
-
|
|
8587
|
+
if (el.templateShape === "rectangle") {
|
|
8588
|
+
if (p.selectedIds.length === 1) {
|
|
8589
|
+
const cos = Math.cos(el.angle);
|
|
8590
|
+
const sin = Math.sin(el.angle);
|
|
8591
|
+
const halfW = (el.width ?? 0) / 2;
|
|
8592
|
+
const pts = [
|
|
8593
|
+
[el.position.x + el.radius * cos, el.position.y + el.radius * sin],
|
|
8594
|
+
[
|
|
8595
|
+
el.position.x + el.radius / 2 * cos + halfW * -sin,
|
|
8596
|
+
el.position.y + el.radius / 2 * sin + halfW * cos
|
|
8597
|
+
]
|
|
8598
|
+
];
|
|
8599
|
+
for (const [hx, hy] of pts) {
|
|
8600
|
+
ctx.fillRect(
|
|
8601
|
+
hx - handleWorldSize / 2,
|
|
8602
|
+
hy - handleWorldSize / 2,
|
|
8603
|
+
handleWorldSize,
|
|
8604
|
+
handleWorldSize
|
|
8605
|
+
);
|
|
8606
|
+
ctx.strokeRect(
|
|
8607
|
+
hx - handleWorldSize / 2,
|
|
8608
|
+
hy - handleWorldSize / 2,
|
|
8609
|
+
handleWorldSize,
|
|
8610
|
+
handleWorldSize
|
|
8611
|
+
);
|
|
8612
|
+
}
|
|
8613
|
+
}
|
|
8614
|
+
} else {
|
|
8615
|
+
const hx = bounds.x + bounds.w;
|
|
8616
|
+
const hy = bounds.y + bounds.h;
|
|
8617
|
+
ctx.fillRect(
|
|
8618
|
+
hx - handleWorldSize / 2,
|
|
8619
|
+
hy - handleWorldSize / 2,
|
|
8620
|
+
handleWorldSize,
|
|
8621
|
+
handleWorldSize
|
|
8622
|
+
);
|
|
8623
|
+
ctx.strokeRect(
|
|
8624
|
+
hx - handleWorldSize / 2,
|
|
8625
|
+
hy - handleWorldSize / 2,
|
|
8626
|
+
handleWorldSize,
|
|
8627
|
+
handleWorldSize
|
|
8628
|
+
);
|
|
8629
|
+
}
|
|
8412
8630
|
ctx.setLineDash([4 / zoom, 4 / zoom]);
|
|
8413
|
-
if (p.selectedIds.length === 1 && (el.templateShape === "cone" || el.templateShape === "line")) {
|
|
8631
|
+
if (p.selectedIds.length === 1 && (el.templateShape === "cone" || el.templateShape === "line" || el.templateShape === "rectangle")) {
|
|
8414
8632
|
const aim = templateAimKnob(el, zoom);
|
|
8415
8633
|
if (aim) {
|
|
8416
8634
|
ctx.beginPath();
|
|
@@ -8571,6 +8789,7 @@ function hitTestTemplateResizeHandle(world, ctx, selectedIds) {
|
|
|
8571
8789
|
for (const id of selectedIds) {
|
|
8572
8790
|
const el = ctx.store.getById(id);
|
|
8573
8791
|
if (!el || el.type !== "template") continue;
|
|
8792
|
+
if (el.templateShape === "rectangle") continue;
|
|
8574
8793
|
const bounds = getElementBounds(el);
|
|
8575
8794
|
if (!bounds) continue;
|
|
8576
8795
|
const hx = bounds.x + bounds.w;
|
|
@@ -8594,6 +8813,37 @@ function hitTestTemplateAimHandle(world, ctx, selectedIds) {
|
|
|
8594
8813
|
const dy = world.y - knob.knob.y;
|
|
8595
8814
|
return dx * dx + dy * dy <= r * r ? { elementId: id } : null;
|
|
8596
8815
|
}
|
|
8816
|
+
function hitTestRectangleLengthHandle(world, ctx, selectedIds) {
|
|
8817
|
+
if (selectedIds.length !== 1) return null;
|
|
8818
|
+
const id = selectedIds[0];
|
|
8819
|
+
if (!id) return null;
|
|
8820
|
+
const el = ctx.store.getById(id);
|
|
8821
|
+
if (!el || el.locked || el.type !== "template" || el.templateShape !== "rectangle") return null;
|
|
8822
|
+
const zoom = ctx.camera.zoom;
|
|
8823
|
+
const r = (HANDLE_SIZE / 2 + HANDLE_HIT_PADDING2) / zoom;
|
|
8824
|
+
const hx = el.position.x + el.radius * Math.cos(el.angle);
|
|
8825
|
+
const hy = el.position.y + el.radius * Math.sin(el.angle);
|
|
8826
|
+
const dx = world.x - hx;
|
|
8827
|
+
const dy = world.y - hy;
|
|
8828
|
+
return dx * dx + dy * dy <= r * r ? { elementId: id } : null;
|
|
8829
|
+
}
|
|
8830
|
+
function hitTestRectangleWidthHandle(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 cos = Math.cos(el.angle);
|
|
8839
|
+
const sin = Math.sin(el.angle);
|
|
8840
|
+
const halfW = (el.width ?? 0) / 2;
|
|
8841
|
+
const hx = el.position.x + el.radius / 2 * cos + halfW * -sin;
|
|
8842
|
+
const hy = el.position.y + el.radius / 2 * sin + halfW * cos;
|
|
8843
|
+
const dx = world.x - hx;
|
|
8844
|
+
const dy = world.y - hy;
|
|
8845
|
+
return dx * dx + dy * dy <= r * r ? { elementId: id } : null;
|
|
8846
|
+
}
|
|
8597
8847
|
function findElementsInRect(marquee, ctx) {
|
|
8598
8848
|
const candidates = ctx.store.queryRect(marquee);
|
|
8599
8849
|
const ids = [];
|
|
@@ -8748,6 +8998,34 @@ function computeTemplateResize(el, world, opts) {
|
|
|
8748
8998
|
}
|
|
8749
8999
|
return updates;
|
|
8750
9000
|
}
|
|
9001
|
+
function computeRectangleLengthResize(el, world, opts) {
|
|
9002
|
+
const cos = Math.cos(el.angle);
|
|
9003
|
+
const sin = Math.sin(el.angle);
|
|
9004
|
+
let len = (world.x - el.position.x) * cos + (world.y - el.position.y) * sin;
|
|
9005
|
+
if (opts.snapToGrid && opts.gridSize && opts.gridSize > 0) {
|
|
9006
|
+
const snapUnit = opts.gridType === "hex" ? Math.sqrt(3) * opts.gridSize : opts.gridSize;
|
|
9007
|
+
len = Math.max(snapUnit, Math.round(len / snapUnit) * snapUnit);
|
|
9008
|
+
}
|
|
9009
|
+
len = Math.max(MIN_ELEMENT_SIZE, len);
|
|
9010
|
+
const updates = { radius: len };
|
|
9011
|
+
if (el.feetPerCell != null && opts.gridSize && opts.gridSize > 0) {
|
|
9012
|
+
const snapUnit = opts.gridType === "hex" ? Math.sqrt(3) * opts.gridSize : opts.gridSize;
|
|
9013
|
+
updates.radiusFeet = len / snapUnit * el.feetPerCell;
|
|
9014
|
+
}
|
|
9015
|
+
return updates;
|
|
9016
|
+
}
|
|
9017
|
+
function computeRectangleWidthResize(el, world, opts) {
|
|
9018
|
+
const cos = Math.cos(el.angle);
|
|
9019
|
+
const sin = Math.sin(el.angle);
|
|
9020
|
+
const perp = Math.abs(-(world.x - el.position.x) * sin + (world.y - el.position.y) * cos);
|
|
9021
|
+
let width = perp * 2;
|
|
9022
|
+
if (opts.snapToGrid && opts.gridSize && opts.gridSize > 0) {
|
|
9023
|
+
const snapUnit = opts.gridType === "hex" ? Math.sqrt(3) * opts.gridSize : opts.gridSize;
|
|
9024
|
+
width = Math.max(snapUnit, Math.round(width / snapUnit) * snapUnit);
|
|
9025
|
+
}
|
|
9026
|
+
width = Math.max(MIN_ELEMENT_SIZE, width);
|
|
9027
|
+
return { width };
|
|
9028
|
+
}
|
|
8751
9029
|
|
|
8752
9030
|
// src/tools/select-tool.ts
|
|
8753
9031
|
var SNAP_PX = 6;
|
|
@@ -8843,6 +9121,18 @@ var SelectTool = class {
|
|
|
8843
9121
|
ctx.requestRender();
|
|
8844
9122
|
return;
|
|
8845
9123
|
}
|
|
9124
|
+
const rectLengthHit = hitTestRectangleLengthHandle(world, ctx, this._selectedIds);
|
|
9125
|
+
if (rectLengthHit) {
|
|
9126
|
+
this.mode = { type: "resizing-rect-length", elementId: rectLengthHit.elementId };
|
|
9127
|
+
ctx.requestRender();
|
|
9128
|
+
return;
|
|
9129
|
+
}
|
|
9130
|
+
const rectWidthHit = hitTestRectangleWidthHandle(world, ctx, this._selectedIds);
|
|
9131
|
+
if (rectWidthHit) {
|
|
9132
|
+
this.mode = { type: "resizing-rect-width", elementId: rectWidthHit.elementId };
|
|
9133
|
+
ctx.requestRender();
|
|
9134
|
+
return;
|
|
9135
|
+
}
|
|
8846
9136
|
const aimHit = hitTestTemplateAimHandle(world, ctx, this._selectedIds);
|
|
8847
9137
|
if (aimHit) {
|
|
8848
9138
|
this.mode = { type: "aiming-template", elementId: aimHit.elementId };
|
|
@@ -8943,6 +9233,19 @@ var SelectTool = class {
|
|
|
8943
9233
|
}
|
|
8944
9234
|
return;
|
|
8945
9235
|
}
|
|
9236
|
+
if (this.mode.type === "resizing-rect-length" || this.mode.type === "resizing-rect-width") {
|
|
9237
|
+
ctx.setCursor?.(this.mode.type === "resizing-rect-length" ? "ew-resize" : "ns-resize");
|
|
9238
|
+
const el = ctx.store.getById(this.mode.elementId);
|
|
9239
|
+
if (el && el.type === "template" && !el.locked) {
|
|
9240
|
+
const opts = { snapToGrid: ctx.snapToGrid, gridSize: ctx.gridSize, gridType: ctx.gridType };
|
|
9241
|
+
const patch = this.mode.type === "resizing-rect-length" ? computeRectangleLengthResize(el, world, opts) : computeRectangleWidthResize(el, world, opts);
|
|
9242
|
+
if (patch) {
|
|
9243
|
+
ctx.store.update(this.mode.elementId, patch);
|
|
9244
|
+
ctx.requestRender();
|
|
9245
|
+
}
|
|
9246
|
+
}
|
|
9247
|
+
return;
|
|
9248
|
+
}
|
|
8946
9249
|
if (this.mode.type === "rotating") {
|
|
8947
9250
|
const { elementId, center: center2, startPointerAngle, startRotation } = this.mode;
|
|
8948
9251
|
const a = Math.atan2(world.y - center2.y, world.x - center2.x);
|
|
@@ -9149,6 +9452,14 @@ var SelectTool = class {
|
|
|
9149
9452
|
ctx.setCursor?.("nwse-resize");
|
|
9150
9453
|
return null;
|
|
9151
9454
|
}
|
|
9455
|
+
if (hitTestRectangleLengthHandle(world, ctx, this._selectedIds)) {
|
|
9456
|
+
ctx.setCursor?.("ew-resize");
|
|
9457
|
+
return null;
|
|
9458
|
+
}
|
|
9459
|
+
if (hitTestRectangleWidthHandle(world, ctx, this._selectedIds)) {
|
|
9460
|
+
ctx.setCursor?.("ns-resize");
|
|
9461
|
+
return null;
|
|
9462
|
+
}
|
|
9152
9463
|
if (hitTestTemplateAimHandle(world, ctx, this._selectedIds)) {
|
|
9153
9464
|
ctx.setCursor?.("grab");
|
|
9154
9465
|
return null;
|
|
@@ -9813,6 +10124,11 @@ var MeasureTool = class {
|
|
|
9813
10124
|
};
|
|
9814
10125
|
|
|
9815
10126
|
// src/tools/template-tool.ts
|
|
10127
|
+
var MIN_RECT_WIDTH = 20;
|
|
10128
|
+
function defaultRectWidth(radius, scaleUnit) {
|
|
10129
|
+
if (scaleUnit > 0) return scaleUnit;
|
|
10130
|
+
return Math.max(MIN_RECT_WIDTH, radius * 0.25);
|
|
10131
|
+
}
|
|
9816
10132
|
var TemplateTool = class {
|
|
9817
10133
|
name = "template";
|
|
9818
10134
|
drawing = false;
|
|
@@ -9891,11 +10207,13 @@ var TemplateTool = class {
|
|
|
9891
10207
|
const snapUnit = gridSize && gridSize > 0 ? ctx.gridType === "hex" ? Math.sqrt(3) * gridSize : gridSize : 0;
|
|
9892
10208
|
const cells = snapUnit > 0 ? radius / snapUnit : 0;
|
|
9893
10209
|
const radiusFeet = cells * this.feetPerCell;
|
|
10210
|
+
const width = this.templateShape === "rectangle" ? defaultRectWidth(radius, this.feetScaleUnit) : void 0;
|
|
9894
10211
|
const element = createTemplate({
|
|
9895
10212
|
position: { ...this.origin },
|
|
9896
10213
|
templateShape: this.templateShape,
|
|
9897
10214
|
radius,
|
|
9898
10215
|
angle,
|
|
10216
|
+
width,
|
|
9899
10217
|
fillColor: this.fillColor,
|
|
9900
10218
|
strokeColor: this.strokeColor,
|
|
9901
10219
|
strokeWidth: this.strokeWidth,
|
|
@@ -9970,6 +10288,22 @@ var TemplateTool = class {
|
|
|
9970
10288
|
ctx.stroke();
|
|
9971
10289
|
break;
|
|
9972
10290
|
}
|
|
10291
|
+
case "rectangle": {
|
|
10292
|
+
const halfW = defaultRectWidth(radius, this.feetScaleUnit) / 2;
|
|
10293
|
+
const cos = Math.cos(angle);
|
|
10294
|
+
const sin = Math.sin(angle);
|
|
10295
|
+
const perpX = -sin * halfW;
|
|
10296
|
+
const perpY = cos * halfW;
|
|
10297
|
+
ctx.beginPath();
|
|
10298
|
+
ctx.moveTo(cx + perpX, cy + perpY);
|
|
10299
|
+
ctx.lineTo(cx + radius * cos + perpX, cy + radius * sin + perpY);
|
|
10300
|
+
ctx.lineTo(cx + radius * cos - perpX, cy + radius * sin - perpY);
|
|
10301
|
+
ctx.lineTo(cx - perpX, cy - perpY);
|
|
10302
|
+
ctx.closePath();
|
|
10303
|
+
ctx.fill();
|
|
10304
|
+
ctx.stroke();
|
|
10305
|
+
break;
|
|
10306
|
+
}
|
|
9973
10307
|
}
|
|
9974
10308
|
this.drawFeetLabel(ctx, radius);
|
|
9975
10309
|
ctx.restore();
|
|
@@ -9996,6 +10330,18 @@ var TemplateTool = class {
|
|
|
9996
10330
|
case "square":
|
|
9997
10331
|
hexCells = getHexCellsInSquare(center2, radiusCells, cellSize, orientation);
|
|
9998
10332
|
break;
|
|
10333
|
+
case "rectangle": {
|
|
10334
|
+
const widthCells = defaultRectWidth(radius, this.feetScaleUnit) / snapUnit;
|
|
10335
|
+
hexCells = getHexCellsInRectangle(
|
|
10336
|
+
center2,
|
|
10337
|
+
angle,
|
|
10338
|
+
radiusCells,
|
|
10339
|
+
widthCells,
|
|
10340
|
+
cellSize,
|
|
10341
|
+
orientation
|
|
10342
|
+
);
|
|
10343
|
+
break;
|
|
10344
|
+
}
|
|
9999
10345
|
}
|
|
10000
10346
|
ctx.save();
|
|
10001
10347
|
ctx.globalAlpha = 0.4;
|
|
@@ -10012,7 +10358,7 @@ var TemplateTool = class {
|
|
|
10012
10358
|
ctx.strokeStyle = this.strokeColor;
|
|
10013
10359
|
ctx.lineWidth = this.strokeWidth;
|
|
10014
10360
|
ctx.stroke();
|
|
10015
|
-
if (this.templateShape === "cone" || this.templateShape === "line" || this.templateShape === "circle" || this.templateShape === "square") {
|
|
10361
|
+
if (this.templateShape === "cone" || this.templateShape === "line" || this.templateShape === "circle" || this.templateShape === "square" || this.templateShape === "rectangle") {
|
|
10016
10362
|
ctx.globalAlpha = 0.5;
|
|
10017
10363
|
ctx.beginPath();
|
|
10018
10364
|
drawHexPath(ctx, center2.x, center2.y, cellSize, orientation);
|
|
@@ -10180,7 +10526,7 @@ var LaserTool = class {
|
|
|
10180
10526
|
};
|
|
10181
10527
|
|
|
10182
10528
|
// src/index.ts
|
|
10183
|
-
var VERSION = "0.
|
|
10529
|
+
var VERSION = "0.49.0";
|
|
10184
10530
|
export {
|
|
10185
10531
|
ArrowTool,
|
|
10186
10532
|
AutoSave,
|
|
@@ -10231,6 +10577,7 @@ export {
|
|
|
10231
10577
|
getHexCellsInCone,
|
|
10232
10578
|
getHexCellsInLine,
|
|
10233
10579
|
getHexCellsInRadius,
|
|
10580
|
+
getHexCellsInRectangle,
|
|
10234
10581
|
getHexCellsInSquare,
|
|
10235
10582
|
getHexDistance,
|
|
10236
10583
|
isNearBezier,
|