@fieldnotes/core 0.47.0 → 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.d.cts CHANGED
@@ -107,13 +107,16 @@ interface GridElement extends BaseElement {
107
107
  strokeWidth: number;
108
108
  opacity: number;
109
109
  }
110
- type TemplateShape = 'circle' | 'cone' | 'line' | 'square';
110
+ type TemplateShape = 'circle' | 'cone' | 'line' | 'square' | 'rectangle';
111
111
  type TemplateRenderStyle = 'cells' | 'geometric';
112
112
  interface TemplateElement extends BaseElement {
113
113
  type: 'template';
114
114
  templateShape: TemplateShape;
115
115
  radius: number;
116
116
  angle: number;
117
+ /** Rectangle-only: full perpendicular extent in world units (centered on the aim axis).
118
+ * Absent for other shapes. Distinct from strokeWidth. */
119
+ width?: number;
117
120
  fillColor: string;
118
121
  strokeColor: string;
119
122
  strokeWidth: number;
@@ -745,6 +748,7 @@ interface TemplateInput extends BaseDefaults {
745
748
  templateShape: TemplateShape;
746
749
  radius: number;
747
750
  angle?: number;
751
+ width?: number;
748
752
  fillColor?: string;
749
753
  strokeColor?: string;
750
754
  strokeWidth?: number;
@@ -771,6 +775,7 @@ declare function getHexDistance(a: Point, b: Point, cellSize: number, orientatio
771
775
  declare function getHexCellsInRadius(center: Point, radiusCells: number, cellSize: number, orientation: HexOrientation): Point[];
772
776
  declare function getHexCellsInCone(center: Point, angle: number, radiusCells: number, cellSize: number, orientation: HexOrientation): Point[];
773
777
  declare function getHexCellsInLine(center: Point, angle: number, radiusCells: number, cellSize: number, orientation: HexOrientation): Point[];
778
+ declare function getHexCellsInRectangle(center: Point, angle: number, lengthCells: number, widthCells: number, cellSize: number, orientation: HexOrientation): Point[];
774
779
  declare function getHexCellsInSquare(center: Point, radiusCells: number, cellSize: number, orientation: HexOrientation): Point[];
775
780
  declare function drawHexPath(ctx: CanvasRenderingContext2D, cx: number, cy: number, cellSize: number, orientation: HexOrientation): void;
776
781
 
@@ -1115,6 +1120,6 @@ declare class LaserTool implements Tool {
1115
1120
  private notifyOptionsChange;
1116
1121
  }
1117
1122
 
1118
- declare const VERSION = "0.47.0";
1123
+ declare const VERSION = "0.48.0";
1119
1124
 
1120
- export { type ActiveFormats, type AlignEdge, type ArrowElement, type ArrowStrokeStyle, ArrowTool, type ArrowToolOptions, AutoSave, type AutoSaveOptions, type BackgroundOptions, type BackgroundPattern, type Binding, type Bounds, Camera, type CameraChangeInfo, type CameraOptions, type CanvasElement, type CanvasState, type Command, DEFAULT_NOTE_FONT_SIZE, type DistributeAxis, type ElementChangeMeta, ElementStore, type ElementStyle, type ElementType, type ElementUpdateEvent, EraserTool, type EraserToolOptions, type ExportImageOptions, type ExportSvgOptions, type FontSizePreset, type GridElement, type GridInfo, HandTool, type HexOrientation, HistoryStack, type HistoryStackOptions, type HtmlElement, type ImageElement, ImageTool, type ImageToolOptions, IndexedDBAdapter, type IndexedDBAdapterOptions, LaserTool, type LaserToolOptions, type Layer, LayerManager, LocalStorageAdapter, MeasureTool, type MeasureToolOptions, type Measurement, MemoryAdapter, type NoteElement, NoteTool, type NoteToolOptions, PencilTool, type PencilToolOptions, type Point, type PointerState, type RenderStatsSnapshot, SelectTool, type ShapeElement, type ShapeKind, ShapeTool, type ShapeToolOptions, type ShortcutBindings, type ShortcutOptions, type ShortcutsApi, type Size, type StorageAdapter, type StrokeElement, type StrokePoint, type TemplateElement, type TemplateRenderStyle, type TemplateShape, TemplateTool, type TemplateToolOptions, type TextElement, TextTool, type TextToolOptions, type Tool, type ToolContext, ToolManager, type ToolName, VERSION, Viewport, type ViewportOptions, boundsIntersect, createArrow, createGrid, createHtmlElement, createImage, createNote, createShape, createStroke, createTemplate, createText, drawHexPath, exportImage, exportSvg, getActiveFormats, getArrowBounds, getArrowControlPoint, getArrowMidpoint, getArrowTangentAngle, getBendFromPoint, getElementBounds, getElementStyle, getElementsBoundingBox, getHexCellsInCone, getHexCellsInLine, getHexCellsInRadius, getHexCellsInSquare, getHexDistance, isNearBezier, setFontSize, smartSnap, snapPoint, snapToHexCenter, styleToPatch, toggleBold, toggleItalic, toggleStrikethrough, toggleUnderline };
1125
+ export { type ActiveFormats, type AlignEdge, type ArrowElement, type ArrowStrokeStyle, ArrowTool, type ArrowToolOptions, AutoSave, type AutoSaveOptions, type BackgroundOptions, type BackgroundPattern, type Binding, type Bounds, Camera, type CameraChangeInfo, type CameraOptions, type CanvasElement, type CanvasState, type Command, DEFAULT_NOTE_FONT_SIZE, type DistributeAxis, type ElementChangeMeta, ElementStore, type ElementStyle, type ElementType, type ElementUpdateEvent, EraserTool, type EraserToolOptions, type ExportImageOptions, type ExportSvgOptions, type FontSizePreset, type GridElement, type GridInfo, HandTool, type HexOrientation, HistoryStack, type HistoryStackOptions, type HtmlElement, type ImageElement, ImageTool, type ImageToolOptions, IndexedDBAdapter, type IndexedDBAdapterOptions, LaserTool, type LaserToolOptions, type Layer, LayerManager, LocalStorageAdapter, MeasureTool, type MeasureToolOptions, type Measurement, MemoryAdapter, type NoteElement, NoteTool, type NoteToolOptions, PencilTool, type PencilToolOptions, type Point, type PointerState, type RenderStatsSnapshot, SelectTool, type ShapeElement, type ShapeKind, ShapeTool, type ShapeToolOptions, type ShortcutBindings, type ShortcutOptions, type ShortcutsApi, type Size, type StorageAdapter, type StrokeElement, type StrokePoint, type TemplateElement, type TemplateRenderStyle, type TemplateShape, TemplateTool, type TemplateToolOptions, type TextElement, TextTool, type TextToolOptions, type Tool, type ToolContext, ToolManager, type ToolName, VERSION, Viewport, type ViewportOptions, boundsIntersect, createArrow, createGrid, createHtmlElement, createImage, createNote, createShape, createStroke, createTemplate, createText, drawHexPath, exportImage, exportSvg, getActiveFormats, getArrowBounds, getArrowControlPoint, getArrowMidpoint, getArrowTangentAngle, getBendFromPoint, getElementBounds, getElementStyle, getElementsBoundingBox, getHexCellsInCone, getHexCellsInLine, getHexCellsInRadius, getHexCellsInRectangle, getHexCellsInSquare, getHexDistance, isNearBezier, setFontSize, smartSnap, snapPoint, snapToHexCenter, styleToPatch, toggleBold, toggleItalic, toggleStrikethrough, toggleUnderline };
package/dist/index.d.ts CHANGED
@@ -107,13 +107,16 @@ interface GridElement extends BaseElement {
107
107
  strokeWidth: number;
108
108
  opacity: number;
109
109
  }
110
- type TemplateShape = 'circle' | 'cone' | 'line' | 'square';
110
+ type TemplateShape = 'circle' | 'cone' | 'line' | 'square' | 'rectangle';
111
111
  type TemplateRenderStyle = 'cells' | 'geometric';
112
112
  interface TemplateElement extends BaseElement {
113
113
  type: 'template';
114
114
  templateShape: TemplateShape;
115
115
  radius: number;
116
116
  angle: number;
117
+ /** Rectangle-only: full perpendicular extent in world units (centered on the aim axis).
118
+ * Absent for other shapes. Distinct from strokeWidth. */
119
+ width?: number;
117
120
  fillColor: string;
118
121
  strokeColor: string;
119
122
  strokeWidth: number;
@@ -745,6 +748,7 @@ interface TemplateInput extends BaseDefaults {
745
748
  templateShape: TemplateShape;
746
749
  radius: number;
747
750
  angle?: number;
751
+ width?: number;
748
752
  fillColor?: string;
749
753
  strokeColor?: string;
750
754
  strokeWidth?: number;
@@ -771,6 +775,7 @@ declare function getHexDistance(a: Point, b: Point, cellSize: number, orientatio
771
775
  declare function getHexCellsInRadius(center: Point, radiusCells: number, cellSize: number, orientation: HexOrientation): Point[];
772
776
  declare function getHexCellsInCone(center: Point, angle: number, radiusCells: number, cellSize: number, orientation: HexOrientation): Point[];
773
777
  declare function getHexCellsInLine(center: Point, angle: number, radiusCells: number, cellSize: number, orientation: HexOrientation): Point[];
778
+ declare function getHexCellsInRectangle(center: Point, angle: number, lengthCells: number, widthCells: number, cellSize: number, orientation: HexOrientation): Point[];
774
779
  declare function getHexCellsInSquare(center: Point, radiusCells: number, cellSize: number, orientation: HexOrientation): Point[];
775
780
  declare function drawHexPath(ctx: CanvasRenderingContext2D, cx: number, cy: number, cellSize: number, orientation: HexOrientation): void;
776
781
 
@@ -1115,6 +1120,6 @@ declare class LaserTool implements Tool {
1115
1120
  private notifyOptionsChange;
1116
1121
  }
1117
1122
 
1118
- declare const VERSION = "0.47.0";
1123
+ declare const VERSION = "0.48.0";
1119
1124
 
1120
- export { type ActiveFormats, type AlignEdge, type ArrowElement, type ArrowStrokeStyle, ArrowTool, type ArrowToolOptions, AutoSave, type AutoSaveOptions, type BackgroundOptions, type BackgroundPattern, type Binding, type Bounds, Camera, type CameraChangeInfo, type CameraOptions, type CanvasElement, type CanvasState, type Command, DEFAULT_NOTE_FONT_SIZE, type DistributeAxis, type ElementChangeMeta, ElementStore, type ElementStyle, type ElementType, type ElementUpdateEvent, EraserTool, type EraserToolOptions, type ExportImageOptions, type ExportSvgOptions, type FontSizePreset, type GridElement, type GridInfo, HandTool, type HexOrientation, HistoryStack, type HistoryStackOptions, type HtmlElement, type ImageElement, ImageTool, type ImageToolOptions, IndexedDBAdapter, type IndexedDBAdapterOptions, LaserTool, type LaserToolOptions, type Layer, LayerManager, LocalStorageAdapter, MeasureTool, type MeasureToolOptions, type Measurement, MemoryAdapter, type NoteElement, NoteTool, type NoteToolOptions, PencilTool, type PencilToolOptions, type Point, type PointerState, type RenderStatsSnapshot, SelectTool, type ShapeElement, type ShapeKind, ShapeTool, type ShapeToolOptions, type ShortcutBindings, type ShortcutOptions, type ShortcutsApi, type Size, type StorageAdapter, type StrokeElement, type StrokePoint, type TemplateElement, type TemplateRenderStyle, type TemplateShape, TemplateTool, type TemplateToolOptions, type TextElement, TextTool, type TextToolOptions, type Tool, type ToolContext, ToolManager, type ToolName, VERSION, Viewport, type ViewportOptions, boundsIntersect, createArrow, createGrid, createHtmlElement, createImage, createNote, createShape, createStroke, createTemplate, createText, drawHexPath, exportImage, exportSvg, getActiveFormats, getArrowBounds, getArrowControlPoint, getArrowMidpoint, getArrowTangentAngle, getBendFromPoint, getElementBounds, getElementStyle, getElementsBoundingBox, getHexCellsInCone, getHexCellsInLine, getHexCellsInRadius, getHexCellsInSquare, getHexDistance, isNearBezier, setFontSize, smartSnap, snapPoint, snapToHexCenter, styleToPatch, toggleBold, toggleItalic, toggleStrikethrough, toggleUnderline };
1125
+ export { type ActiveFormats, type AlignEdge, type ArrowElement, type ArrowStrokeStyle, ArrowTool, type ArrowToolOptions, AutoSave, type AutoSaveOptions, type BackgroundOptions, type BackgroundPattern, type Binding, type Bounds, Camera, type CameraChangeInfo, type CameraOptions, type CanvasElement, type CanvasState, type Command, DEFAULT_NOTE_FONT_SIZE, type DistributeAxis, type ElementChangeMeta, ElementStore, type ElementStyle, type ElementType, type ElementUpdateEvent, EraserTool, type EraserToolOptions, type ExportImageOptions, type ExportSvgOptions, type FontSizePreset, type GridElement, type GridInfo, HandTool, type HexOrientation, HistoryStack, type HistoryStackOptions, type HtmlElement, type ImageElement, ImageTool, type ImageToolOptions, IndexedDBAdapter, type IndexedDBAdapterOptions, LaserTool, type LaserToolOptions, type Layer, LayerManager, LocalStorageAdapter, MeasureTool, type MeasureToolOptions, type Measurement, MemoryAdapter, type NoteElement, NoteTool, type NoteToolOptions, PencilTool, type PencilToolOptions, type Point, type PointerState, type RenderStatsSnapshot, SelectTool, type ShapeElement, type ShapeKind, ShapeTool, type ShapeToolOptions, type ShortcutBindings, type ShortcutOptions, type ShortcutsApi, type Size, type StorageAdapter, type StrokeElement, type StrokePoint, type TemplateElement, type TemplateRenderStyle, type TemplateShape, TemplateTool, type TemplateToolOptions, type TextElement, TextTool, type TextToolOptions, type Tool, type ToolContext, ToolManager, type ToolName, VERSION, Viewport, type ViewportOptions, boundsIntersect, createArrow, createGrid, createHtmlElement, createImage, createNote, createShape, createStroke, createTemplate, createText, drawHexPath, exportImage, exportSvg, getActiveFormats, getArrowBounds, getArrowControlPoint, getArrowMidpoint, getArrowTangentAngle, getBendFromPoint, getElementBounds, getElementStyle, getElementsBoundingBox, getHexCellsInCone, getHexCellsInLine, getHexCellsInRadius, getHexCellsInRectangle, getHexCellsInSquare, getHexDistance, isNearBezier, setFontSize, smartSnap, snapPoint, snapToHexCenter, styleToPatch, toggleBold, toggleItalic, toggleStrikethrough, toggleUnderline };
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);
@@ -3241,7 +3301,7 @@ function drawHexPath(ctx, cx, cy, cellSize, orientation) {
3241
3301
  // src/elements/renderers/template-measure.ts
3242
3302
  function renderTemplateFeetLabel(ctx, p) {
3243
3303
  if (p.feet <= 0) return;
3244
- const directional = p.templateShape === "cone" || p.templateShape === "line";
3304
+ const directional = p.templateShape === "cone" || p.templateShape === "line" || p.templateShape === "rectangle";
3245
3305
  const dir = directional ? p.angle : 0;
3246
3306
  const length = p.templateShape === "square" ? p.radius / 2 : p.radius;
3247
3307
  const cos = Math.cos(dir);
@@ -3334,6 +3394,22 @@ function renderGeometricTemplate(ctx, template) {
3334
3394
  ctx.stroke();
3335
3395
  break;
3336
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
+ }
3337
3413
  }
3338
3414
  if (template.radiusFeet != null && template.radiusFeet > 0) {
3339
3415
  renderTemplateFeetLabel(ctx, {
@@ -3365,6 +3441,18 @@ function renderHexTemplate(ctx, template, cellSize, orientation) {
3365
3441
  case "square":
3366
3442
  cells = getHexCellsInSquare(center2, radiusCells, cellSize, orientation);
3367
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
+ }
3368
3456
  }
3369
3457
  ctx.save();
3370
3458
  ctx.globalAlpha = template.opacity;
@@ -3941,7 +4029,8 @@ function createTemplate(input) {
3941
4029
  opacity: input.opacity ?? 0.6,
3942
4030
  feetPerCell: input.feetPerCell,
3943
4031
  radiusFeet: input.radiusFeet,
3944
- ...input.renderStyle !== void 0 ? { renderStyle: input.renderStyle } : {}
4032
+ ...input.renderStyle !== void 0 ? { renderStyle: input.renderStyle } : {},
4033
+ ...input.width !== void 0 ? { width: input.width } : {}
3945
4034
  };
3946
4035
  }
3947
4036
 
@@ -5538,6 +5627,20 @@ function emitGeometricTemplate(t) {
5538
5627
  ].map(([px, py]) => `${n(px ?? 0)},${n(py ?? 0)}`).join(" ");
5539
5628
  return `<polygon points="${pts}" ${attrs} />`;
5540
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
+ }
5541
5644
  }
5542
5645
  }
5543
5646
  function emitHexTemplate(t, grid) {
@@ -5560,6 +5663,18 @@ function emitHexTemplate(t, grid) {
5560
5663
  case "square":
5561
5664
  cells = getHexCellsInSquare(center2, radiusCells, cellSize, orientation);
5562
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
+ }
5563
5678
  }
5564
5679
  let d = "";
5565
5680
  for (const cell of cells) {
@@ -8250,7 +8365,8 @@ function getOverlayLayout(el, zoom) {
8250
8365
  }
8251
8366
  function templateAimKnob(el, zoom) {
8252
8367
  if (el.type !== "template") return null;
8253
- if (el.templateShape !== "cone" && el.templateShape !== "line") return null;
8368
+ if (el.templateShape !== "cone" && el.templateShape !== "line" && el.templateShape !== "rectangle")
8369
+ return null;
8254
8370
  const gap = ROTATE_HANDLE_OFFSET / zoom;
8255
8371
  const dist = el.radius + gap;
8256
8372
  const origin = el.position;
@@ -8395,22 +8511,51 @@ function renderSelectionBoxes(ctx, p) {
8395
8511
  } else if (el.type === "template") {
8396
8512
  ctx.setLineDash([]);
8397
8513
  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
- );
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
+ }
8412
8557
  ctx.setLineDash([4 / zoom, 4 / zoom]);
8413
- if (p.selectedIds.length === 1 && (el.templateShape === "cone" || el.templateShape === "line")) {
8558
+ if (p.selectedIds.length === 1 && (el.templateShape === "cone" || el.templateShape === "line" || el.templateShape === "rectangle")) {
8414
8559
  const aim = templateAimKnob(el, zoom);
8415
8560
  if (aim) {
8416
8561
  ctx.beginPath();
@@ -8571,6 +8716,7 @@ function hitTestTemplateResizeHandle(world, ctx, selectedIds) {
8571
8716
  for (const id of selectedIds) {
8572
8717
  const el = ctx.store.getById(id);
8573
8718
  if (!el || el.type !== "template") continue;
8719
+ if (el.templateShape === "rectangle") continue;
8574
8720
  const bounds = getElementBounds(el);
8575
8721
  if (!bounds) continue;
8576
8722
  const hx = bounds.x + bounds.w;
@@ -8594,6 +8740,37 @@ function hitTestTemplateAimHandle(world, ctx, selectedIds) {
8594
8740
  const dy = world.y - knob.knob.y;
8595
8741
  return dx * dx + dy * dy <= r * r ? { elementId: id } : null;
8596
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
+ }
8597
8774
  function findElementsInRect(marquee, ctx) {
8598
8775
  const candidates = ctx.store.queryRect(marquee);
8599
8776
  const ids = [];
@@ -8748,6 +8925,34 @@ function computeTemplateResize(el, world, opts) {
8748
8925
  }
8749
8926
  return updates;
8750
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
+ }
8751
8956
 
8752
8957
  // src/tools/select-tool.ts
8753
8958
  var SNAP_PX = 6;
@@ -8843,6 +9048,18 @@ var SelectTool = class {
8843
9048
  ctx.requestRender();
8844
9049
  return;
8845
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
+ }
8846
9063
  const aimHit = hitTestTemplateAimHandle(world, ctx, this._selectedIds);
8847
9064
  if (aimHit) {
8848
9065
  this.mode = { type: "aiming-template", elementId: aimHit.elementId };
@@ -8943,6 +9160,19 @@ var SelectTool = class {
8943
9160
  }
8944
9161
  return;
8945
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
+ }
8946
9176
  if (this.mode.type === "rotating") {
8947
9177
  const { elementId, center: center2, startPointerAngle, startRotation } = this.mode;
8948
9178
  const a = Math.atan2(world.y - center2.y, world.x - center2.x);
@@ -9149,6 +9379,14 @@ var SelectTool = class {
9149
9379
  ctx.setCursor?.("nwse-resize");
9150
9380
  return null;
9151
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
+ }
9152
9390
  if (hitTestTemplateAimHandle(world, ctx, this._selectedIds)) {
9153
9391
  ctx.setCursor?.("grab");
9154
9392
  return null;
@@ -9813,6 +10051,11 @@ var MeasureTool = class {
9813
10051
  };
9814
10052
 
9815
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
+ }
9816
10059
  var TemplateTool = class {
9817
10060
  name = "template";
9818
10061
  drawing = false;
@@ -9891,11 +10134,13 @@ var TemplateTool = class {
9891
10134
  const snapUnit = gridSize && gridSize > 0 ? ctx.gridType === "hex" ? Math.sqrt(3) * gridSize : gridSize : 0;
9892
10135
  const cells = snapUnit > 0 ? radius / snapUnit : 0;
9893
10136
  const radiusFeet = cells * this.feetPerCell;
10137
+ const width = this.templateShape === "rectangle" ? defaultRectWidth(radius, this.feetScaleUnit) : void 0;
9894
10138
  const element = createTemplate({
9895
10139
  position: { ...this.origin },
9896
10140
  templateShape: this.templateShape,
9897
10141
  radius,
9898
10142
  angle,
10143
+ width,
9899
10144
  fillColor: this.fillColor,
9900
10145
  strokeColor: this.strokeColor,
9901
10146
  strokeWidth: this.strokeWidth,
@@ -9970,6 +10215,22 @@ var TemplateTool = class {
9970
10215
  ctx.stroke();
9971
10216
  break;
9972
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
+ }
9973
10234
  }
9974
10235
  this.drawFeetLabel(ctx, radius);
9975
10236
  ctx.restore();
@@ -9996,6 +10257,18 @@ var TemplateTool = class {
9996
10257
  case "square":
9997
10258
  hexCells = getHexCellsInSquare(center2, radiusCells, cellSize, orientation);
9998
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
+ }
9999
10272
  }
10000
10273
  ctx.save();
10001
10274
  ctx.globalAlpha = 0.4;
@@ -10012,7 +10285,7 @@ var TemplateTool = class {
10012
10285
  ctx.strokeStyle = this.strokeColor;
10013
10286
  ctx.lineWidth = this.strokeWidth;
10014
10287
  ctx.stroke();
10015
- 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") {
10016
10289
  ctx.globalAlpha = 0.5;
10017
10290
  ctx.beginPath();
10018
10291
  drawHexPath(ctx, center2.x, center2.y, cellSize, orientation);
@@ -10180,7 +10453,7 @@ var LaserTool = class {
10180
10453
  };
10181
10454
 
10182
10455
  // src/index.ts
10183
- var VERSION = "0.47.0";
10456
+ var VERSION = "0.48.0";
10184
10457
  export {
10185
10458
  ArrowTool,
10186
10459
  AutoSave,
@@ -10231,6 +10504,7 @@ export {
10231
10504
  getHexCellsInCone,
10232
10505
  getHexCellsInLine,
10233
10506
  getHexCellsInRadius,
10507
+ getHexCellsInRectangle,
10234
10508
  getHexCellsInSquare,
10235
10509
  getHexDistance,
10236
10510
  isNearBezier,