@fieldnotes/core 0.46.1 → 0.48.0

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