@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.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-ops.ts
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") return null;
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
- const hx = bounds.x + bounds.w;
8399
- const hy = bounds.y + bounds.h;
8400
- ctx.fillRect(
8401
- hx - handleWorldSize / 2,
8402
- hy - handleWorldSize / 2,
8403
- handleWorldSize,
8404
- handleWorldSize
8405
- );
8406
- ctx.strokeRect(
8407
- hx - handleWorldSize / 2,
8408
- hy - handleWorldSize / 2,
8409
- handleWorldSize,
8410
- handleWorldSize
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.47.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,