@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.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) {
|
|
@@ -1221,6 +1242,14 @@ var KeyboardActions = class {
|
|
|
1221
1242
|
if (this.deps.isToolActive()) return;
|
|
1222
1243
|
this.deps.toggleLock?.();
|
|
1223
1244
|
}
|
|
1245
|
+
rotate(direction) {
|
|
1246
|
+
if (this.deps.isToolActive()) return;
|
|
1247
|
+
this.flushPendingNudge();
|
|
1248
|
+
const sel = this.selectTool();
|
|
1249
|
+
if (!sel) return;
|
|
1250
|
+
if (sel.tool.selectedIds.length === 0) return;
|
|
1251
|
+
this.deps.rotate?.(direction);
|
|
1252
|
+
}
|
|
1224
1253
|
zOrder(operation) {
|
|
1225
1254
|
if (this.deps.isToolActive()) return;
|
|
1226
1255
|
this.flushPendingNudge();
|
|
@@ -1325,6 +1354,8 @@ var DEFAULT_BINDINGS = [
|
|
|
1325
1354
|
["ungroup", ["mod+shift+g"]],
|
|
1326
1355
|
["cut", ["mod+x"]],
|
|
1327
1356
|
["toggle-lock", ["mod+shift+l"]],
|
|
1357
|
+
["rotate-cw", ["r"]],
|
|
1358
|
+
["rotate-ccw", ["shift+r"]],
|
|
1328
1359
|
["nudge-left", ["arrowleft"]],
|
|
1329
1360
|
["nudge-right", ["arrowright"]],
|
|
1330
1361
|
["nudge-up", ["arrowup"]],
|
|
@@ -1637,6 +1668,14 @@ var KeyboardHandler = class {
|
|
|
1637
1668
|
e?.preventDefault();
|
|
1638
1669
|
this.deps.actions.toggleLock();
|
|
1639
1670
|
return;
|
|
1671
|
+
case "rotate-cw":
|
|
1672
|
+
e?.preventDefault();
|
|
1673
|
+
this.deps.actions.rotate("cw");
|
|
1674
|
+
return;
|
|
1675
|
+
case "rotate-ccw":
|
|
1676
|
+
e?.preventDefault();
|
|
1677
|
+
this.deps.actions.rotate("ccw");
|
|
1678
|
+
return;
|
|
1640
1679
|
case "zoom-in":
|
|
1641
1680
|
e?.preventDefault();
|
|
1642
1681
|
this.zoomByFactor(ZOOM_STEP);
|
|
@@ -1773,6 +1812,7 @@ var InputHandler = class {
|
|
|
1773
1812
|
group: options.group,
|
|
1774
1813
|
ungroup: options.ungroup,
|
|
1775
1814
|
toggleLock: options.toggleLock,
|
|
1815
|
+
rotate: options.rotate,
|
|
1776
1816
|
getLastPointerWorld: () => this.lastPointerWorld()
|
|
1777
1817
|
});
|
|
1778
1818
|
this.openContextMenu = options.openContextMenu;
|
|
@@ -3286,6 +3326,46 @@ function getHexCellsInLine(center2, angle, radiusCells, cellSize, orientation) {
|
|
|
3286
3326
|
}
|
|
3287
3327
|
return cells;
|
|
3288
3328
|
}
|
|
3329
|
+
function getHexCellsInRectangle(center2, angle, lengthCells, widthCells, cellSize, orientation) {
|
|
3330
|
+
const nLen = Math.round(lengthCells);
|
|
3331
|
+
const wCells = Math.max(1, Math.round(widthCells));
|
|
3332
|
+
const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
|
|
3333
|
+
const cube = offsetToCube(off.col, off.row, orientation);
|
|
3334
|
+
const centerPixel = offsetToPixel(off.col, off.row, cellSize, orientation);
|
|
3335
|
+
if (nLen <= 0) return [centerPixel];
|
|
3336
|
+
const vertexOffset = orientation === "pointy" ? Math.PI / 6 : 0;
|
|
3337
|
+
const step = Math.PI / 3;
|
|
3338
|
+
const snappedAngle = Math.round((angle - vertexOffset) / step) * step + vertexOffset;
|
|
3339
|
+
const cos = Math.cos(snappedAngle);
|
|
3340
|
+
const sin = Math.sin(snappedAngle);
|
|
3341
|
+
const snapUnit = Math.sqrt(3) * cellSize;
|
|
3342
|
+
const lineLength = nLen * snapUnit;
|
|
3343
|
+
const halfWidth = wCells * snapUnit / 2 + 1e-6;
|
|
3344
|
+
const iterM = nLen + Math.ceil(wCells / 2) + 1;
|
|
3345
|
+
const cells = [];
|
|
3346
|
+
for (let dq = -iterM; dq <= iterM; dq++) {
|
|
3347
|
+
const rMin = Math.max(-iterM, -dq - iterM);
|
|
3348
|
+
const rMax = Math.min(iterM, -dq + iterM);
|
|
3349
|
+
for (let dr = rMin; dr <= rMax; dr++) {
|
|
3350
|
+
const absQ = cube.q + dq;
|
|
3351
|
+
const absR = cube.r + dr;
|
|
3352
|
+
const pixel = offsetToPixel(
|
|
3353
|
+
cubeToOffset(absQ, absR, orientation).col,
|
|
3354
|
+
cubeToOffset(absQ, absR, orientation).row,
|
|
3355
|
+
cellSize,
|
|
3356
|
+
orientation
|
|
3357
|
+
);
|
|
3358
|
+
const dx = pixel.x - centerPixel.x;
|
|
3359
|
+
const dy = pixel.y - centerPixel.y;
|
|
3360
|
+
const along = dx * cos + dy * sin;
|
|
3361
|
+
const perp = Math.abs(-dx * sin + dy * cos);
|
|
3362
|
+
if (along >= -snapUnit * 0.1 && along <= lineLength + snapUnit * 0.1 && perp <= halfWidth) {
|
|
3363
|
+
cells.push(pixel);
|
|
3364
|
+
}
|
|
3365
|
+
}
|
|
3366
|
+
}
|
|
3367
|
+
return cells;
|
|
3368
|
+
}
|
|
3289
3369
|
function getHexCellsInSquare(center2, radiusCells, cellSize, orientation) {
|
|
3290
3370
|
const n2 = Math.round(radiusCells);
|
|
3291
3371
|
const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
|
|
@@ -3327,7 +3407,7 @@ function drawHexPath(ctx, cx, cy, cellSize, orientation) {
|
|
|
3327
3407
|
// src/elements/renderers/template-measure.ts
|
|
3328
3408
|
function renderTemplateFeetLabel(ctx, p) {
|
|
3329
3409
|
if (p.feet <= 0) return;
|
|
3330
|
-
const directional = p.templateShape === "cone" || p.templateShape === "line";
|
|
3410
|
+
const directional = p.templateShape === "cone" || p.templateShape === "line" || p.templateShape === "rectangle";
|
|
3331
3411
|
const dir = directional ? p.angle : 0;
|
|
3332
3412
|
const length = p.templateShape === "square" ? p.radius / 2 : p.radius;
|
|
3333
3413
|
const cos = Math.cos(dir);
|
|
@@ -3420,6 +3500,22 @@ function renderGeometricTemplate(ctx, template) {
|
|
|
3420
3500
|
ctx.stroke();
|
|
3421
3501
|
break;
|
|
3422
3502
|
}
|
|
3503
|
+
case "rectangle": {
|
|
3504
|
+
const halfW = (template.width ?? 0) / 2;
|
|
3505
|
+
const cos = Math.cos(template.angle);
|
|
3506
|
+
const sin = Math.sin(template.angle);
|
|
3507
|
+
const perpX = -sin * halfW;
|
|
3508
|
+
const perpY = cos * halfW;
|
|
3509
|
+
ctx.beginPath();
|
|
3510
|
+
ctx.moveTo(cx + perpX, cy + perpY);
|
|
3511
|
+
ctx.lineTo(cx + r * cos + perpX, cy + r * sin + perpY);
|
|
3512
|
+
ctx.lineTo(cx + r * cos - perpX, cy + r * sin - perpY);
|
|
3513
|
+
ctx.lineTo(cx - perpX, cy - perpY);
|
|
3514
|
+
ctx.closePath();
|
|
3515
|
+
ctx.fill();
|
|
3516
|
+
ctx.stroke();
|
|
3517
|
+
break;
|
|
3518
|
+
}
|
|
3423
3519
|
}
|
|
3424
3520
|
if (template.radiusFeet != null && template.radiusFeet > 0) {
|
|
3425
3521
|
renderTemplateFeetLabel(ctx, {
|
|
@@ -3451,6 +3547,18 @@ function renderHexTemplate(ctx, template, cellSize, orientation) {
|
|
|
3451
3547
|
case "square":
|
|
3452
3548
|
cells = getHexCellsInSquare(center2, radiusCells, cellSize, orientation);
|
|
3453
3549
|
break;
|
|
3550
|
+
case "rectangle": {
|
|
3551
|
+
const widthCells = (template.width ?? 0) / snapUnit;
|
|
3552
|
+
cells = getHexCellsInRectangle(
|
|
3553
|
+
center2,
|
|
3554
|
+
template.angle,
|
|
3555
|
+
radiusCells,
|
|
3556
|
+
widthCells,
|
|
3557
|
+
cellSize,
|
|
3558
|
+
orientation
|
|
3559
|
+
);
|
|
3560
|
+
break;
|
|
3561
|
+
}
|
|
3454
3562
|
}
|
|
3455
3563
|
ctx.save();
|
|
3456
3564
|
ctx.globalAlpha = template.opacity;
|
|
@@ -4027,7 +4135,8 @@ function createTemplate(input) {
|
|
|
4027
4135
|
opacity: input.opacity ?? 0.6,
|
|
4028
4136
|
feetPerCell: input.feetPerCell,
|
|
4029
4137
|
radiusFeet: input.radiusFeet,
|
|
4030
|
-
...input.renderStyle !== void 0 ? { renderStyle: input.renderStyle } : {}
|
|
4138
|
+
...input.renderStyle !== void 0 ? { renderStyle: input.renderStyle } : {},
|
|
4139
|
+
...input.width !== void 0 ? { width: input.width } : {}
|
|
4031
4140
|
};
|
|
4032
4141
|
}
|
|
4033
4142
|
|
|
@@ -5624,6 +5733,20 @@ function emitGeometricTemplate(t) {
|
|
|
5624
5733
|
].map(([px, py]) => `${n(px ?? 0)},${n(py ?? 0)}`).join(" ");
|
|
5625
5734
|
return `<polygon points="${pts}" ${attrs} />`;
|
|
5626
5735
|
}
|
|
5736
|
+
case "rectangle": {
|
|
5737
|
+
const halfW = (t.width ?? 0) / 2;
|
|
5738
|
+
const cos = Math.cos(t.angle);
|
|
5739
|
+
const sin = Math.sin(t.angle);
|
|
5740
|
+
const perpX = -sin * halfW;
|
|
5741
|
+
const perpY = cos * halfW;
|
|
5742
|
+
const pts = [
|
|
5743
|
+
[cx + perpX, cy + perpY],
|
|
5744
|
+
[cx + r * cos + perpX, cy + r * sin + perpY],
|
|
5745
|
+
[cx + r * cos - perpX, cy + r * sin - perpY],
|
|
5746
|
+
[cx - perpX, cy - perpY]
|
|
5747
|
+
].map(([px, py]) => `${n(px ?? 0)},${n(py ?? 0)}`).join(" ");
|
|
5748
|
+
return `<polygon points="${pts}" ${attrs} />`;
|
|
5749
|
+
}
|
|
5627
5750
|
}
|
|
5628
5751
|
}
|
|
5629
5752
|
function emitHexTemplate(t, grid) {
|
|
@@ -5646,6 +5769,18 @@ function emitHexTemplate(t, grid) {
|
|
|
5646
5769
|
case "square":
|
|
5647
5770
|
cells = getHexCellsInSquare(center2, radiusCells, cellSize, orientation);
|
|
5648
5771
|
break;
|
|
5772
|
+
case "rectangle": {
|
|
5773
|
+
const widthCells = (t.width ?? 0) / snapUnit;
|
|
5774
|
+
cells = getHexCellsInRectangle(
|
|
5775
|
+
center2,
|
|
5776
|
+
t.angle,
|
|
5777
|
+
radiusCells,
|
|
5778
|
+
widthCells,
|
|
5779
|
+
cellSize,
|
|
5780
|
+
orientation
|
|
5781
|
+
);
|
|
5782
|
+
break;
|
|
5783
|
+
}
|
|
5649
5784
|
}
|
|
5650
5785
|
let d = "";
|
|
5651
5786
|
for (const cell of cells) {
|
|
@@ -6766,7 +6901,7 @@ function getElementStyle(element) {
|
|
|
6766
6901
|
}
|
|
6767
6902
|
}
|
|
6768
6903
|
|
|
6769
|
-
// src/canvas/selection-
|
|
6904
|
+
// src/canvas/selection-rotate.ts
|
|
6770
6905
|
function unionBounds2(list) {
|
|
6771
6906
|
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
6772
6907
|
for (const b of list) {
|
|
@@ -6777,6 +6912,39 @@ function unionBounds2(list) {
|
|
|
6777
6912
|
}
|
|
6778
6913
|
return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
|
|
6779
6914
|
}
|
|
6915
|
+
function rotationPivot(items) {
|
|
6916
|
+
const first = items[0];
|
|
6917
|
+
if (items.length === 1 && first) {
|
|
6918
|
+
return { x: first.bounds.x + first.bounds.w / 2, y: first.bounds.y + first.bounds.h / 2 };
|
|
6919
|
+
}
|
|
6920
|
+
const u = unionBounds2(items.map((it) => rotatedAABB(it.bounds, it.el.rotation ?? 0)));
|
|
6921
|
+
return { x: u.x + u.w / 2, y: u.y + u.h / 2 };
|
|
6922
|
+
}
|
|
6923
|
+
function rotateElementPatch(el, bounds, pivot, delta) {
|
|
6924
|
+
if (el.type === "arrow") {
|
|
6925
|
+
const center3 = { x: bounds.x + bounds.w / 2, y: bounds.y + bounds.h / 2 };
|
|
6926
|
+
const moved2 = rotatePoint(center3, pivot, delta);
|
|
6927
|
+
return {
|
|
6928
|
+
position: { x: el.position.x + moved2.x - center3.x, y: el.position.y + moved2.y - center3.y },
|
|
6929
|
+
from: rotatePoint(el.from, pivot, delta),
|
|
6930
|
+
to: rotatePoint(el.to, pivot, delta)
|
|
6931
|
+
};
|
|
6932
|
+
}
|
|
6933
|
+
if (el.type === "template") {
|
|
6934
|
+
return {
|
|
6935
|
+
position: rotatePoint(el.position, pivot, delta),
|
|
6936
|
+
angle: normalizeAngle(el.angle + delta)
|
|
6937
|
+
};
|
|
6938
|
+
}
|
|
6939
|
+
const center2 = { x: bounds.x + bounds.w / 2, y: bounds.y + bounds.h / 2 };
|
|
6940
|
+
const moved = rotatePoint(center2, pivot, delta);
|
|
6941
|
+
return {
|
|
6942
|
+
position: { x: el.position.x + moved.x - center2.x, y: el.position.y + moved.y - center2.y },
|
|
6943
|
+
rotation: normalizeAngle((el.rotation ?? 0) + delta)
|
|
6944
|
+
};
|
|
6945
|
+
}
|
|
6946
|
+
|
|
6947
|
+
// src/canvas/selection-ops.ts
|
|
6780
6948
|
function sharedValue(values) {
|
|
6781
6949
|
const present = values.filter((v) => v !== void 0);
|
|
6782
6950
|
if (present.length === 0) return void 0;
|
|
@@ -6924,6 +7092,21 @@ var SelectionOps = class {
|
|
|
6924
7092
|
this.deps.recorder.commit();
|
|
6925
7093
|
this.deps.requestRender();
|
|
6926
7094
|
}
|
|
7095
|
+
rotateSelection(direction) {
|
|
7096
|
+
const eligible = this.boundedSelection().filter(({ el }) => this.isMovable(el));
|
|
7097
|
+
if (eligible.length === 0) return;
|
|
7098
|
+
const delta = direction === "cw" ? Math.PI / 2 : -Math.PI / 2;
|
|
7099
|
+
const pivot = rotationPivot(eligible);
|
|
7100
|
+
this.deps.recorder.begin();
|
|
7101
|
+
const moved = [];
|
|
7102
|
+
for (const { id, el, bounds } of eligible) {
|
|
7103
|
+
this.deps.store.update(id, rotateElementPatch(el, bounds, pivot, delta));
|
|
7104
|
+
moved.push(id);
|
|
7105
|
+
}
|
|
7106
|
+
updateArrowsBoundToElements(moved, this.deps.store);
|
|
7107
|
+
this.deps.recorder.commit();
|
|
7108
|
+
this.deps.requestRender();
|
|
7109
|
+
}
|
|
6927
7110
|
boundedSelection() {
|
|
6928
7111
|
const out = [];
|
|
6929
7112
|
for (const id of this.deps.getSelectedIds()) {
|
|
@@ -7252,6 +7435,7 @@ var Viewport = class {
|
|
|
7252
7435
|
group: () => this.groupSelection(),
|
|
7253
7436
|
ungroup: () => this.ungroupSelection(),
|
|
7254
7437
|
toggleLock: () => this.toggleLockSelection(),
|
|
7438
|
+
rotate: (direction) => this.rotateSelection(direction),
|
|
7255
7439
|
openContextMenu: (screenPos, world) => {
|
|
7256
7440
|
this.getSelectTool()?.selectAtPoint(world, this.toolContext);
|
|
7257
7441
|
this.openContextMenu(screenPos);
|
|
@@ -7649,6 +7833,8 @@ var Viewport = class {
|
|
|
7649
7833
|
items.push({ label: "Bring Forward", action: "z-forward" });
|
|
7650
7834
|
items.push({ label: "Send Backward", action: "z-backward" });
|
|
7651
7835
|
items.push({ label: "Send to Back", action: "z-back" });
|
|
7836
|
+
items.push({ label: "Rotate 90\xB0 CW", action: "rotate-cw" });
|
|
7837
|
+
items.push({ label: "Rotate 90\xB0 CCW", action: "rotate-ccw" });
|
|
7652
7838
|
const allLocked = ids.every((id) => this.store.getById(id)?.locked);
|
|
7653
7839
|
items.push({ label: allLocked ? "Unlock" : "Lock", action: "toggle-lock" });
|
|
7654
7840
|
} else if (this.canPaste()) {
|
|
@@ -7682,6 +7868,9 @@ var Viewport = class {
|
|
|
7682
7868
|
distributeSelection(axis) {
|
|
7683
7869
|
this.selectionOps.distribute(axis);
|
|
7684
7870
|
}
|
|
7871
|
+
rotateSelection(direction) {
|
|
7872
|
+
this.selectionOps.rotateSelection(direction);
|
|
7873
|
+
}
|
|
7685
7874
|
getRenderStats() {
|
|
7686
7875
|
return this.renderLoop.getStats();
|
|
7687
7876
|
}
|
|
@@ -8336,7 +8525,8 @@ function getOverlayLayout(el, zoom) {
|
|
|
8336
8525
|
}
|
|
8337
8526
|
function templateAimKnob(el, zoom) {
|
|
8338
8527
|
if (el.type !== "template") return null;
|
|
8339
|
-
if (el.templateShape !== "cone" && el.templateShape !== "line"
|
|
8528
|
+
if (el.templateShape !== "cone" && el.templateShape !== "line" && el.templateShape !== "rectangle")
|
|
8529
|
+
return null;
|
|
8340
8530
|
const gap = ROTATE_HANDLE_OFFSET / zoom;
|
|
8341
8531
|
const dist = el.radius + gap;
|
|
8342
8532
|
const origin = el.position;
|
|
@@ -8481,22 +8671,51 @@ function renderSelectionBoxes(ctx, p) {
|
|
|
8481
8671
|
} else if (el.type === "template") {
|
|
8482
8672
|
ctx.setLineDash([]);
|
|
8483
8673
|
ctx.fillStyle = "#ffffff";
|
|
8484
|
-
|
|
8485
|
-
|
|
8486
|
-
|
|
8487
|
-
|
|
8488
|
-
|
|
8489
|
-
|
|
8490
|
-
|
|
8491
|
-
|
|
8492
|
-
|
|
8493
|
-
|
|
8494
|
-
|
|
8495
|
-
|
|
8496
|
-
|
|
8497
|
-
|
|
8674
|
+
if (el.templateShape === "rectangle") {
|
|
8675
|
+
if (p.selectedIds.length === 1) {
|
|
8676
|
+
const cos = Math.cos(el.angle);
|
|
8677
|
+
const sin = Math.sin(el.angle);
|
|
8678
|
+
const halfW = (el.width ?? 0) / 2;
|
|
8679
|
+
const pts = [
|
|
8680
|
+
[el.position.x + el.radius * cos, el.position.y + el.radius * sin],
|
|
8681
|
+
[
|
|
8682
|
+
el.position.x + el.radius / 2 * cos + halfW * -sin,
|
|
8683
|
+
el.position.y + el.radius / 2 * sin + halfW * cos
|
|
8684
|
+
]
|
|
8685
|
+
];
|
|
8686
|
+
for (const [hx, hy] of pts) {
|
|
8687
|
+
ctx.fillRect(
|
|
8688
|
+
hx - handleWorldSize / 2,
|
|
8689
|
+
hy - handleWorldSize / 2,
|
|
8690
|
+
handleWorldSize,
|
|
8691
|
+
handleWorldSize
|
|
8692
|
+
);
|
|
8693
|
+
ctx.strokeRect(
|
|
8694
|
+
hx - handleWorldSize / 2,
|
|
8695
|
+
hy - handleWorldSize / 2,
|
|
8696
|
+
handleWorldSize,
|
|
8697
|
+
handleWorldSize
|
|
8698
|
+
);
|
|
8699
|
+
}
|
|
8700
|
+
}
|
|
8701
|
+
} else {
|
|
8702
|
+
const hx = bounds.x + bounds.w;
|
|
8703
|
+
const hy = bounds.y + bounds.h;
|
|
8704
|
+
ctx.fillRect(
|
|
8705
|
+
hx - handleWorldSize / 2,
|
|
8706
|
+
hy - handleWorldSize / 2,
|
|
8707
|
+
handleWorldSize,
|
|
8708
|
+
handleWorldSize
|
|
8709
|
+
);
|
|
8710
|
+
ctx.strokeRect(
|
|
8711
|
+
hx - handleWorldSize / 2,
|
|
8712
|
+
hy - handleWorldSize / 2,
|
|
8713
|
+
handleWorldSize,
|
|
8714
|
+
handleWorldSize
|
|
8715
|
+
);
|
|
8716
|
+
}
|
|
8498
8717
|
ctx.setLineDash([4 / zoom, 4 / zoom]);
|
|
8499
|
-
if (p.selectedIds.length === 1 && (el.templateShape === "cone" || el.templateShape === "line")) {
|
|
8718
|
+
if (p.selectedIds.length === 1 && (el.templateShape === "cone" || el.templateShape === "line" || el.templateShape === "rectangle")) {
|
|
8500
8719
|
const aim = templateAimKnob(el, zoom);
|
|
8501
8720
|
if (aim) {
|
|
8502
8721
|
ctx.beginPath();
|
|
@@ -8657,6 +8876,7 @@ function hitTestTemplateResizeHandle(world, ctx, selectedIds) {
|
|
|
8657
8876
|
for (const id of selectedIds) {
|
|
8658
8877
|
const el = ctx.store.getById(id);
|
|
8659
8878
|
if (!el || el.type !== "template") continue;
|
|
8879
|
+
if (el.templateShape === "rectangle") continue;
|
|
8660
8880
|
const bounds = getElementBounds(el);
|
|
8661
8881
|
if (!bounds) continue;
|
|
8662
8882
|
const hx = bounds.x + bounds.w;
|
|
@@ -8680,6 +8900,37 @@ function hitTestTemplateAimHandle(world, ctx, selectedIds) {
|
|
|
8680
8900
|
const dy = world.y - knob.knob.y;
|
|
8681
8901
|
return dx * dx + dy * dy <= r * r ? { elementId: id } : null;
|
|
8682
8902
|
}
|
|
8903
|
+
function hitTestRectangleLengthHandle(world, ctx, selectedIds) {
|
|
8904
|
+
if (selectedIds.length !== 1) return null;
|
|
8905
|
+
const id = selectedIds[0];
|
|
8906
|
+
if (!id) return null;
|
|
8907
|
+
const el = ctx.store.getById(id);
|
|
8908
|
+
if (!el || el.locked || el.type !== "template" || el.templateShape !== "rectangle") return null;
|
|
8909
|
+
const zoom = ctx.camera.zoom;
|
|
8910
|
+
const r = (HANDLE_SIZE / 2 + HANDLE_HIT_PADDING2) / zoom;
|
|
8911
|
+
const hx = el.position.x + el.radius * Math.cos(el.angle);
|
|
8912
|
+
const hy = el.position.y + el.radius * Math.sin(el.angle);
|
|
8913
|
+
const dx = world.x - hx;
|
|
8914
|
+
const dy = world.y - hy;
|
|
8915
|
+
return dx * dx + dy * dy <= r * r ? { elementId: id } : null;
|
|
8916
|
+
}
|
|
8917
|
+
function hitTestRectangleWidthHandle(world, ctx, selectedIds) {
|
|
8918
|
+
if (selectedIds.length !== 1) return null;
|
|
8919
|
+
const id = selectedIds[0];
|
|
8920
|
+
if (!id) return null;
|
|
8921
|
+
const el = ctx.store.getById(id);
|
|
8922
|
+
if (!el || el.locked || el.type !== "template" || el.templateShape !== "rectangle") return null;
|
|
8923
|
+
const zoom = ctx.camera.zoom;
|
|
8924
|
+
const r = (HANDLE_SIZE / 2 + HANDLE_HIT_PADDING2) / zoom;
|
|
8925
|
+
const cos = Math.cos(el.angle);
|
|
8926
|
+
const sin = Math.sin(el.angle);
|
|
8927
|
+
const halfW = (el.width ?? 0) / 2;
|
|
8928
|
+
const hx = el.position.x + el.radius / 2 * cos + halfW * -sin;
|
|
8929
|
+
const hy = el.position.y + el.radius / 2 * sin + halfW * cos;
|
|
8930
|
+
const dx = world.x - hx;
|
|
8931
|
+
const dy = world.y - hy;
|
|
8932
|
+
return dx * dx + dy * dy <= r * r ? { elementId: id } : null;
|
|
8933
|
+
}
|
|
8683
8934
|
function findElementsInRect(marquee, ctx) {
|
|
8684
8935
|
const candidates = ctx.store.queryRect(marquee);
|
|
8685
8936
|
const ids = [];
|
|
@@ -8834,6 +9085,34 @@ function computeTemplateResize(el, world, opts) {
|
|
|
8834
9085
|
}
|
|
8835
9086
|
return updates;
|
|
8836
9087
|
}
|
|
9088
|
+
function computeRectangleLengthResize(el, world, opts) {
|
|
9089
|
+
const cos = Math.cos(el.angle);
|
|
9090
|
+
const sin = Math.sin(el.angle);
|
|
9091
|
+
let len = (world.x - el.position.x) * cos + (world.y - el.position.y) * sin;
|
|
9092
|
+
if (opts.snapToGrid && opts.gridSize && opts.gridSize > 0) {
|
|
9093
|
+
const snapUnit = opts.gridType === "hex" ? Math.sqrt(3) * opts.gridSize : opts.gridSize;
|
|
9094
|
+
len = Math.max(snapUnit, Math.round(len / snapUnit) * snapUnit);
|
|
9095
|
+
}
|
|
9096
|
+
len = Math.max(MIN_ELEMENT_SIZE, len);
|
|
9097
|
+
const updates = { radius: len };
|
|
9098
|
+
if (el.feetPerCell != null && opts.gridSize && opts.gridSize > 0) {
|
|
9099
|
+
const snapUnit = opts.gridType === "hex" ? Math.sqrt(3) * opts.gridSize : opts.gridSize;
|
|
9100
|
+
updates.radiusFeet = len / snapUnit * el.feetPerCell;
|
|
9101
|
+
}
|
|
9102
|
+
return updates;
|
|
9103
|
+
}
|
|
9104
|
+
function computeRectangleWidthResize(el, world, opts) {
|
|
9105
|
+
const cos = Math.cos(el.angle);
|
|
9106
|
+
const sin = Math.sin(el.angle);
|
|
9107
|
+
const perp = Math.abs(-(world.x - el.position.x) * sin + (world.y - el.position.y) * cos);
|
|
9108
|
+
let width = perp * 2;
|
|
9109
|
+
if (opts.snapToGrid && opts.gridSize && opts.gridSize > 0) {
|
|
9110
|
+
const snapUnit = opts.gridType === "hex" ? Math.sqrt(3) * opts.gridSize : opts.gridSize;
|
|
9111
|
+
width = Math.max(snapUnit, Math.round(width / snapUnit) * snapUnit);
|
|
9112
|
+
}
|
|
9113
|
+
width = Math.max(MIN_ELEMENT_SIZE, width);
|
|
9114
|
+
return { width };
|
|
9115
|
+
}
|
|
8837
9116
|
|
|
8838
9117
|
// src/tools/select-tool.ts
|
|
8839
9118
|
var SNAP_PX = 6;
|
|
@@ -8929,6 +9208,18 @@ var SelectTool = class {
|
|
|
8929
9208
|
ctx.requestRender();
|
|
8930
9209
|
return;
|
|
8931
9210
|
}
|
|
9211
|
+
const rectLengthHit = hitTestRectangleLengthHandle(world, ctx, this._selectedIds);
|
|
9212
|
+
if (rectLengthHit) {
|
|
9213
|
+
this.mode = { type: "resizing-rect-length", elementId: rectLengthHit.elementId };
|
|
9214
|
+
ctx.requestRender();
|
|
9215
|
+
return;
|
|
9216
|
+
}
|
|
9217
|
+
const rectWidthHit = hitTestRectangleWidthHandle(world, ctx, this._selectedIds);
|
|
9218
|
+
if (rectWidthHit) {
|
|
9219
|
+
this.mode = { type: "resizing-rect-width", elementId: rectWidthHit.elementId };
|
|
9220
|
+
ctx.requestRender();
|
|
9221
|
+
return;
|
|
9222
|
+
}
|
|
8932
9223
|
const aimHit = hitTestTemplateAimHandle(world, ctx, this._selectedIds);
|
|
8933
9224
|
if (aimHit) {
|
|
8934
9225
|
this.mode = { type: "aiming-template", elementId: aimHit.elementId };
|
|
@@ -9029,6 +9320,19 @@ var SelectTool = class {
|
|
|
9029
9320
|
}
|
|
9030
9321
|
return;
|
|
9031
9322
|
}
|
|
9323
|
+
if (this.mode.type === "resizing-rect-length" || this.mode.type === "resizing-rect-width") {
|
|
9324
|
+
ctx.setCursor?.(this.mode.type === "resizing-rect-length" ? "ew-resize" : "ns-resize");
|
|
9325
|
+
const el = ctx.store.getById(this.mode.elementId);
|
|
9326
|
+
if (el && el.type === "template" && !el.locked) {
|
|
9327
|
+
const opts = { snapToGrid: ctx.snapToGrid, gridSize: ctx.gridSize, gridType: ctx.gridType };
|
|
9328
|
+
const patch = this.mode.type === "resizing-rect-length" ? computeRectangleLengthResize(el, world, opts) : computeRectangleWidthResize(el, world, opts);
|
|
9329
|
+
if (patch) {
|
|
9330
|
+
ctx.store.update(this.mode.elementId, patch);
|
|
9331
|
+
ctx.requestRender();
|
|
9332
|
+
}
|
|
9333
|
+
}
|
|
9334
|
+
return;
|
|
9335
|
+
}
|
|
9032
9336
|
if (this.mode.type === "rotating") {
|
|
9033
9337
|
const { elementId, center: center2, startPointerAngle, startRotation } = this.mode;
|
|
9034
9338
|
const a = Math.atan2(world.y - center2.y, world.x - center2.x);
|
|
@@ -9235,6 +9539,14 @@ var SelectTool = class {
|
|
|
9235
9539
|
ctx.setCursor?.("nwse-resize");
|
|
9236
9540
|
return null;
|
|
9237
9541
|
}
|
|
9542
|
+
if (hitTestRectangleLengthHandle(world, ctx, this._selectedIds)) {
|
|
9543
|
+
ctx.setCursor?.("ew-resize");
|
|
9544
|
+
return null;
|
|
9545
|
+
}
|
|
9546
|
+
if (hitTestRectangleWidthHandle(world, ctx, this._selectedIds)) {
|
|
9547
|
+
ctx.setCursor?.("ns-resize");
|
|
9548
|
+
return null;
|
|
9549
|
+
}
|
|
9238
9550
|
if (hitTestTemplateAimHandle(world, ctx, this._selectedIds)) {
|
|
9239
9551
|
ctx.setCursor?.("grab");
|
|
9240
9552
|
return null;
|
|
@@ -9899,6 +10211,11 @@ var MeasureTool = class {
|
|
|
9899
10211
|
};
|
|
9900
10212
|
|
|
9901
10213
|
// src/tools/template-tool.ts
|
|
10214
|
+
var MIN_RECT_WIDTH = 20;
|
|
10215
|
+
function defaultRectWidth(radius, scaleUnit) {
|
|
10216
|
+
if (scaleUnit > 0) return scaleUnit;
|
|
10217
|
+
return Math.max(MIN_RECT_WIDTH, radius * 0.25);
|
|
10218
|
+
}
|
|
9902
10219
|
var TemplateTool = class {
|
|
9903
10220
|
name = "template";
|
|
9904
10221
|
drawing = false;
|
|
@@ -9977,11 +10294,13 @@ var TemplateTool = class {
|
|
|
9977
10294
|
const snapUnit = gridSize && gridSize > 0 ? ctx.gridType === "hex" ? Math.sqrt(3) * gridSize : gridSize : 0;
|
|
9978
10295
|
const cells = snapUnit > 0 ? radius / snapUnit : 0;
|
|
9979
10296
|
const radiusFeet = cells * this.feetPerCell;
|
|
10297
|
+
const width = this.templateShape === "rectangle" ? defaultRectWidth(radius, this.feetScaleUnit) : void 0;
|
|
9980
10298
|
const element = createTemplate({
|
|
9981
10299
|
position: { ...this.origin },
|
|
9982
10300
|
templateShape: this.templateShape,
|
|
9983
10301
|
radius,
|
|
9984
10302
|
angle,
|
|
10303
|
+
width,
|
|
9985
10304
|
fillColor: this.fillColor,
|
|
9986
10305
|
strokeColor: this.strokeColor,
|
|
9987
10306
|
strokeWidth: this.strokeWidth,
|
|
@@ -10056,6 +10375,22 @@ var TemplateTool = class {
|
|
|
10056
10375
|
ctx.stroke();
|
|
10057
10376
|
break;
|
|
10058
10377
|
}
|
|
10378
|
+
case "rectangle": {
|
|
10379
|
+
const halfW = defaultRectWidth(radius, this.feetScaleUnit) / 2;
|
|
10380
|
+
const cos = Math.cos(angle);
|
|
10381
|
+
const sin = Math.sin(angle);
|
|
10382
|
+
const perpX = -sin * halfW;
|
|
10383
|
+
const perpY = cos * halfW;
|
|
10384
|
+
ctx.beginPath();
|
|
10385
|
+
ctx.moveTo(cx + perpX, cy + perpY);
|
|
10386
|
+
ctx.lineTo(cx + radius * cos + perpX, cy + radius * sin + perpY);
|
|
10387
|
+
ctx.lineTo(cx + radius * cos - perpX, cy + radius * sin - perpY);
|
|
10388
|
+
ctx.lineTo(cx - perpX, cy - perpY);
|
|
10389
|
+
ctx.closePath();
|
|
10390
|
+
ctx.fill();
|
|
10391
|
+
ctx.stroke();
|
|
10392
|
+
break;
|
|
10393
|
+
}
|
|
10059
10394
|
}
|
|
10060
10395
|
this.drawFeetLabel(ctx, radius);
|
|
10061
10396
|
ctx.restore();
|
|
@@ -10082,6 +10417,18 @@ var TemplateTool = class {
|
|
|
10082
10417
|
case "square":
|
|
10083
10418
|
hexCells = getHexCellsInSquare(center2, radiusCells, cellSize, orientation);
|
|
10084
10419
|
break;
|
|
10420
|
+
case "rectangle": {
|
|
10421
|
+
const widthCells = defaultRectWidth(radius, this.feetScaleUnit) / snapUnit;
|
|
10422
|
+
hexCells = getHexCellsInRectangle(
|
|
10423
|
+
center2,
|
|
10424
|
+
angle,
|
|
10425
|
+
radiusCells,
|
|
10426
|
+
widthCells,
|
|
10427
|
+
cellSize,
|
|
10428
|
+
orientation
|
|
10429
|
+
);
|
|
10430
|
+
break;
|
|
10431
|
+
}
|
|
10085
10432
|
}
|
|
10086
10433
|
ctx.save();
|
|
10087
10434
|
ctx.globalAlpha = 0.4;
|
|
@@ -10098,7 +10445,7 @@ var TemplateTool = class {
|
|
|
10098
10445
|
ctx.strokeStyle = this.strokeColor;
|
|
10099
10446
|
ctx.lineWidth = this.strokeWidth;
|
|
10100
10447
|
ctx.stroke();
|
|
10101
|
-
if (this.templateShape === "cone" || this.templateShape === "line" || this.templateShape === "circle" || this.templateShape === "square") {
|
|
10448
|
+
if (this.templateShape === "cone" || this.templateShape === "line" || this.templateShape === "circle" || this.templateShape === "square" || this.templateShape === "rectangle") {
|
|
10102
10449
|
ctx.globalAlpha = 0.5;
|
|
10103
10450
|
ctx.beginPath();
|
|
10104
10451
|
drawHexPath(ctx, center2.x, center2.y, cellSize, orientation);
|
|
@@ -10266,7 +10613,7 @@ var LaserTool = class {
|
|
|
10266
10613
|
};
|
|
10267
10614
|
|
|
10268
10615
|
// src/index.ts
|
|
10269
|
-
var VERSION = "0.
|
|
10616
|
+
var VERSION = "0.49.0";
|
|
10270
10617
|
// Annotate the CommonJS export names for ESM import in node:
|
|
10271
10618
|
0 && (module.exports = {
|
|
10272
10619
|
ArrowTool,
|
|
@@ -10318,6 +10665,7 @@ var VERSION = "0.47.0";
|
|
|
10318
10665
|
getHexCellsInCone,
|
|
10319
10666
|
getHexCellsInLine,
|
|
10320
10667
|
getHexCellsInRadius,
|
|
10668
|
+
getHexCellsInRectangle,
|
|
10321
10669
|
getHexCellsInSquare,
|
|
10322
10670
|
getHexDistance,
|
|
10323
10671
|
isNearBezier,
|