@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 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-ops.ts
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") return null;
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
- const hx = bounds.x + bounds.w;
8485
- const hy = bounds.y + bounds.h;
8486
- ctx.fillRect(
8487
- hx - handleWorldSize / 2,
8488
- hy - handleWorldSize / 2,
8489
- handleWorldSize,
8490
- handleWorldSize
8491
- );
8492
- ctx.strokeRect(
8493
- hx - handleWorldSize / 2,
8494
- hy - handleWorldSize / 2,
8495
- handleWorldSize,
8496
- handleWorldSize
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.47.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,