@fieldnotes/core 0.34.0 → 0.35.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
@@ -605,6 +605,36 @@ function distSqToSegment(p, a, b) {
605
605
  const dy = p.y - (a.y + t * aby);
606
606
  return dx * dx + dy * dy;
607
607
  }
608
+ function rotatePoint(p, center2, angle) {
609
+ if (angle === 0) return p;
610
+ const cos = Math.cos(angle);
611
+ const sin = Math.sin(angle);
612
+ const dx = p.x - center2.x;
613
+ const dy = p.y - center2.y;
614
+ return { x: center2.x + dx * cos - dy * sin, y: center2.y + dx * sin + dy * cos };
615
+ }
616
+ function rotatedAABB(bounds, angle) {
617
+ if (angle === 0) return bounds;
618
+ const c = { x: bounds.x + bounds.w / 2, y: bounds.y + bounds.h / 2 };
619
+ const corners = [
620
+ { x: bounds.x, y: bounds.y },
621
+ { x: bounds.x + bounds.w, y: bounds.y },
622
+ { x: bounds.x + bounds.w, y: bounds.y + bounds.h },
623
+ { x: bounds.x, y: bounds.y + bounds.h }
624
+ ].map((p) => rotatePoint(p, c, angle));
625
+ const xs = corners.map((p) => p.x);
626
+ const ys = corners.map((p) => p.y);
627
+ const minX = Math.min(...xs);
628
+ const minY = Math.min(...ys);
629
+ return { x: minX, y: minY, w: Math.max(...xs) - minX, h: Math.max(...ys) - minY };
630
+ }
631
+ function normalizeAngle(angle) {
632
+ const twoPi = Math.PI * 2;
633
+ let a = angle % twoPi;
634
+ if (a <= -Math.PI) a += twoPi;
635
+ else if (a > Math.PI) a -= twoPi;
636
+ return a;
637
+ }
608
638
 
609
639
  // src/elements/arrow-geometry.ts
610
640
  function getArrowControlPoint(from, to, bend) {
@@ -1582,18 +1612,18 @@ var InputHandler = class {
1582
1612
  handlePinchMove() {
1583
1613
  const [a, b] = this.getPinchPoints();
1584
1614
  const dist = this.distance(a, b);
1585
- const center = this.midpoint(a, b);
1615
+ const center2 = this.midpoint(a, b);
1586
1616
  if (this.lastPinchDistance > 0) {
1587
1617
  const scale = dist / this.lastPinchDistance;
1588
1618
  const newZoom = this.camera.zoom * scale;
1589
- this.camera.zoomAt(newZoom, center);
1619
+ this.camera.zoomAt(newZoom, center2);
1590
1620
  }
1591
- const dx = center.x - this.lastPointer.x;
1592
- const dy = center.y - this.lastPointer.y;
1621
+ const dx = center2.x - this.lastPointer.x;
1622
+ const dy = center2.y - this.lastPointer.y;
1593
1623
  this.camera.pan(dx, dy);
1594
1624
  this.lastPinchDistance = dist;
1595
- this.lastPinchCenter = center;
1596
- this.lastPointer = { ...center };
1625
+ this.lastPinchCenter = center2;
1626
+ this.lastPointer = { ...center2 };
1597
1627
  }
1598
1628
  getPinchPoints() {
1599
1629
  const pts = [...this.activePointers.values()];
@@ -2135,11 +2165,19 @@ var ElementStore = class {
2135
2165
  (el) => el.type === type
2136
2166
  );
2137
2167
  }
2168
+ // Spatial index stores the rotation-expanded AABB so rotated elements remain
2169
+ // broad-phase hit-test/marquee candidates; precise tests run against local bounds.
2170
+ indexBounds(element) {
2171
+ const bounds = getElementBounds(element);
2172
+ if (!bounds) return null;
2173
+ const angle = element.rotation ?? 0;
2174
+ return angle === 0 ? bounds : rotatedAABB(bounds, angle);
2175
+ }
2138
2176
  add(element) {
2139
2177
  this.sortedCache = null;
2140
2178
  this._versions.set(element.id, 0);
2141
2179
  this.elements.set(element.id, element);
2142
- const bounds = getElementBounds(element);
2180
+ const bounds = this.indexBounds(element);
2143
2181
  if (bounds) this.spatialIndex.insert(element.id, bounds);
2144
2182
  this.bus.emit("add", element);
2145
2183
  }
@@ -2161,7 +2199,7 @@ var ElementStore = class {
2161
2199
  updated.text = sanitizeNoteHtml(updated.text);
2162
2200
  }
2163
2201
  this.elements.set(id, updated);
2164
- const newBounds = getElementBounds(updated);
2202
+ const newBounds = this.indexBounds(updated);
2165
2203
  if (newBounds) {
2166
2204
  this.spatialIndex.update(id, newBounds);
2167
2205
  }
@@ -2194,7 +2232,7 @@ var ElementStore = class {
2194
2232
  for (const el of elements) {
2195
2233
  this.elements.set(el.id, el);
2196
2234
  this._versions.set(el.id, 0);
2197
- const bounds = getElementBounds(el);
2235
+ const bounds = this.indexBounds(el);
2198
2236
  if (bounds) this.spatialIndex.insert(el.id, bounds);
2199
2237
  if (el.type === "stroke") {
2200
2238
  computeStrokeSegments(el);
@@ -2399,9 +2437,9 @@ function updateBoundArrow(arrow, store) {
2399
2437
  if (arrow.fromBinding) {
2400
2438
  const el = store.getById(arrow.fromBinding.elementId);
2401
2439
  if (el) {
2402
- const center = getElementCenter(el);
2403
- updates.from = center;
2404
- updates.position = center;
2440
+ const center2 = getElementCenter(el);
2441
+ updates.from = center2;
2442
+ updates.position = center2;
2405
2443
  }
2406
2444
  }
2407
2445
  if (arrow.toBinding) {
@@ -2413,6 +2451,21 @@ function updateBoundArrow(arrow, store) {
2413
2451
  return Object.keys(updates).length > 0 ? updates : null;
2414
2452
  }
2415
2453
 
2454
+ // src/elements/rotate-canvas.ts
2455
+ function withRotation(ctx, el, center2, draw) {
2456
+ const angle = el.rotation ?? 0;
2457
+ if (angle === 0) {
2458
+ draw();
2459
+ return;
2460
+ }
2461
+ ctx.save();
2462
+ ctx.translate(center2.x, center2.y);
2463
+ ctx.rotate(angle);
2464
+ ctx.translate(-center2.x, -center2.y);
2465
+ draw();
2466
+ ctx.restore();
2467
+ }
2468
+
2416
2469
  // src/elements/grid-renderer.ts
2417
2470
  function getSquareGridLines(bounds, cellSize) {
2418
2471
  if (cellSize <= 0) return { verticals: [], horizontals: [] };
@@ -2676,18 +2729,18 @@ function getHexDistance(a, b, cellSize, orientation) {
2676
2729
  const ds = -dq - dr;
2677
2730
  return Math.max(Math.abs(dq), Math.abs(dr), Math.abs(ds));
2678
2731
  }
2679
- function getHexCellsInRadius(center, radiusCells, cellSize, orientation) {
2732
+ function getHexCellsInRadius(center2, radiusCells, cellSize, orientation) {
2680
2733
  const n = Math.round(radiusCells);
2681
- const off = pixelToOffset(center.x, center.y, cellSize, orientation);
2734
+ const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
2682
2735
  const cube = offsetToCube(off.col, off.row, orientation);
2683
2736
  if (n <= 0) {
2684
2737
  return [offsetToPixel(off.col, off.row, cellSize, orientation)];
2685
2738
  }
2686
2739
  return enumerateHexRing(cube.q, cube.r, n, orientation, cellSize);
2687
2740
  }
2688
- function getHexCellsInCone(center, angle, radiusCells, cellSize, orientation) {
2741
+ function getHexCellsInCone(center2, angle, radiusCells, cellSize, orientation) {
2689
2742
  const n = Math.round(radiusCells);
2690
- const off = pixelToOffset(center.x, center.y, cellSize, orientation);
2743
+ const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
2691
2744
  const cube = offsetToCube(off.col, off.row, orientation);
2692
2745
  const centerPixel = offsetToPixel(off.col, off.row, cellSize, orientation);
2693
2746
  if (n <= 0) return [centerPixel];
@@ -2721,9 +2774,9 @@ function getHexCellsInCone(center, angle, radiusCells, cellSize, orientation) {
2721
2774
  }
2722
2775
  return cells;
2723
2776
  }
2724
- function getHexCellsInLine(center, angle, radiusCells, cellSize, orientation) {
2777
+ function getHexCellsInLine(center2, angle, radiusCells, cellSize, orientation) {
2725
2778
  const n = Math.round(radiusCells);
2726
- const off = pixelToOffset(center.x, center.y, cellSize, orientation);
2779
+ const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
2727
2780
  const cube = offsetToCube(off.col, off.row, orientation);
2728
2781
  const centerPixel = offsetToPixel(off.col, off.row, cellSize, orientation);
2729
2782
  if (n <= 0) return [centerPixel];
@@ -2759,9 +2812,9 @@ function getHexCellsInLine(center, angle, radiusCells, cellSize, orientation) {
2759
2812
  }
2760
2813
  return cells;
2761
2814
  }
2762
- function getHexCellsInSquare(center, radiusCells, cellSize, orientation) {
2815
+ function getHexCellsInSquare(center2, radiusCells, cellSize, orientation) {
2763
2816
  const n = Math.round(radiusCells);
2764
- const off = pixelToOffset(center.x, center.y, cellSize, orientation);
2817
+ const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
2765
2818
  const cube = offsetToCube(off.col, off.row, orientation);
2766
2819
  const centerPixel = offsetToPixel(off.col, off.row, cellSize, orientation);
2767
2820
  if (n <= 0) return [centerPixel];
@@ -2839,18 +2892,27 @@ var ElementRenderer = class {
2839
2892
  }
2840
2893
  renderCanvasElement(ctx, element) {
2841
2894
  switch (element.type) {
2842
- case "stroke":
2843
- this.renderStroke(ctx, element);
2895
+ case "stroke": {
2896
+ const b = getElementBounds(element);
2897
+ const c = b ? { x: b.x + b.w / 2, y: b.y + b.h / 2 } : element.position;
2898
+ withRotation(ctx, element, c, () => this.renderStroke(ctx, element));
2844
2899
  break;
2900
+ }
2845
2901
  case "arrow":
2846
2902
  this.renderArrow(ctx, element);
2847
2903
  break;
2848
- case "shape":
2849
- this.renderShape(ctx, element);
2904
+ case "shape": {
2905
+ const b = getElementBounds(element);
2906
+ const c = b ? { x: b.x + b.w / 2, y: b.y + b.h / 2 } : element.position;
2907
+ withRotation(ctx, element, c, () => this.renderShape(ctx, element));
2850
2908
  break;
2851
- case "image":
2852
- this.renderImage(ctx, element);
2909
+ }
2910
+ case "image": {
2911
+ const b = getElementBounds(element);
2912
+ const c = b ? { x: b.x + b.w / 2, y: b.y + b.h / 2 } : element.position;
2913
+ withRotation(ctx, element, c, () => this.renderImage(ctx, element));
2853
2914
  break;
2915
+ }
2854
2916
  case "grid":
2855
2917
  this.renderGrid(ctx, element);
2856
2918
  break;
@@ -3147,20 +3209,20 @@ var ElementRenderer = class {
3147
3209
  renderHexTemplate(ctx, template, cellSize, orientation) {
3148
3210
  const snapUnit = Math.sqrt(3) * cellSize;
3149
3211
  const radiusCells = template.radius / snapUnit;
3150
- const center = template.position;
3212
+ const center2 = template.position;
3151
3213
  let cells;
3152
3214
  switch (template.templateShape) {
3153
3215
  case "circle":
3154
- cells = getHexCellsInRadius(center, radiusCells, cellSize, orientation);
3216
+ cells = getHexCellsInRadius(center2, radiusCells, cellSize, orientation);
3155
3217
  break;
3156
3218
  case "cone":
3157
- cells = getHexCellsInCone(center, template.angle, radiusCells, cellSize, orientation);
3219
+ cells = getHexCellsInCone(center2, template.angle, radiusCells, cellSize, orientation);
3158
3220
  break;
3159
3221
  case "line":
3160
- cells = getHexCellsInLine(center, template.angle, radiusCells, cellSize, orientation);
3222
+ cells = getHexCellsInLine(center2, template.angle, radiusCells, cellSize, orientation);
3161
3223
  break;
3162
3224
  case "square":
3163
- cells = getHexCellsInSquare(center, radiusCells, cellSize, orientation);
3225
+ cells = getHexCellsInSquare(center2, radiusCells, cellSize, orientation);
3164
3226
  break;
3165
3227
  }
3166
3228
  ctx.save();
@@ -3181,7 +3243,7 @@ var ElementRenderer = class {
3181
3243
  {
3182
3244
  ctx.globalAlpha = Math.min(template.opacity + 0.1, 1);
3183
3245
  ctx.beginPath();
3184
- drawHexPath(ctx, center.x, center.y, cellSize, orientation);
3246
+ drawHexPath(ctx, center2.x, center2.y, cellSize, orientation);
3185
3247
  ctx.fillStyle = template.strokeColor;
3186
3248
  ctx.fill();
3187
3249
  ctx.strokeStyle = template.strokeColor;
@@ -3190,7 +3252,7 @@ var ElementRenderer = class {
3190
3252
  }
3191
3253
  if (template.templateShape === "circle" && template.radiusFeet != null && template.radiusFeet > 0) {
3192
3254
  const r = template.radius;
3193
- this.renderRadiusMarker(ctx, center.x, center.y, r, template.radiusFeet);
3255
+ this.renderRadiusMarker(ctx, center2.x, center2.y, r, template.radiusFeet);
3194
3256
  }
3195
3257
  ctx.restore();
3196
3258
  }
@@ -4309,6 +4371,7 @@ function renderStyledRuns(ctx, runs, startX, startY, maxWidth) {
4309
4371
  }
4310
4372
 
4311
4373
  // src/canvas/export-image.ts
4374
+ var center = (b) => ({ x: b.x + b.w / 2, y: b.y + b.h / 2 });
4312
4375
  function getStrokeBounds(el) {
4313
4376
  if (el.type !== "stroke") return null;
4314
4377
  if (el.points.length === 0) return null;
@@ -4334,8 +4397,10 @@ function getStrokeBounds(el) {
4334
4397
  }
4335
4398
  function getElementRect(el) {
4336
4399
  switch (el.type) {
4337
- case "stroke":
4338
- return getStrokeBounds(el);
4400
+ case "stroke": {
4401
+ const r = getStrokeBounds(el);
4402
+ return r ? rotatedAABB(r, el.rotation ?? 0) : r;
4403
+ }
4339
4404
  case "arrow": {
4340
4405
  const b = getArrowBounds(el.from, el.to, el.bend);
4341
4406
  const pad = el.width / 2 + 14;
@@ -4354,7 +4419,10 @@ function getElementRect(el) {
4354
4419
  case "text":
4355
4420
  case "shape":
4356
4421
  if ("size" in el) {
4357
- return { x: el.position.x, y: el.position.y, w: el.size.w, h: el.size.h };
4422
+ return rotatedAABB(
4423
+ { x: el.position.x, y: el.position.y, w: el.size.w, h: el.size.h },
4424
+ el.rotation ?? 0
4425
+ );
4358
4426
  }
4359
4427
  return null;
4360
4428
  default:
@@ -4492,11 +4560,13 @@ async function exportImage(store, options = {}, layerManager) {
4492
4560
  continue;
4493
4561
  }
4494
4562
  if (el.type === "note") {
4495
- renderNoteOnCanvas(ctx, el);
4563
+ const b = getElementBounds(el);
4564
+ withRotation(ctx, el, b ? center(b) : el.position, () => renderNoteOnCanvas(ctx, el));
4496
4565
  continue;
4497
4566
  }
4498
4567
  if (el.type === "text") {
4499
- renderTextOnCanvas(ctx, el);
4568
+ const b = getElementBounds(el);
4569
+ withRotation(ctx, el, b ? center(b) : el.position, () => renderTextOnCanvas(ctx, el));
4500
4570
  continue;
4501
4571
  }
4502
4572
  if (el.type === "html") {
@@ -4505,7 +4575,13 @@ async function exportImage(store, options = {}, layerManager) {
4505
4575
  if (el.type === "image") {
4506
4576
  const img = imageCache.get(el.id);
4507
4577
  if (img) {
4508
- ctx.drawImage(img, el.position.x, el.position.y, el.size.w, el.size.h);
4578
+ const b = getElementBounds(el);
4579
+ withRotation(
4580
+ ctx,
4581
+ el,
4582
+ b ? center(b) : el.position,
4583
+ () => ctx.drawImage(img, el.position.x, el.position.y, el.size.w, el.size.h)
4584
+ );
4509
4585
  }
4510
4586
  continue;
4511
4587
  }
@@ -4841,7 +4917,9 @@ var DomNodeManager = class {
4841
4917
  top: `${element.position.y}px`,
4842
4918
  width: size ? `${size.w}px` : "auto",
4843
4919
  height: size ? `${size.h}px` : "auto",
4844
- zIndex: String(zIndex)
4920
+ zIndex: String(zIndex),
4921
+ transform: element.rotation ? `rotate(${element.rotation}rad)` : "",
4922
+ transformOrigin: "50% 50%"
4845
4923
  });
4846
4924
  this.renderDomContent(node, element);
4847
4925
  }
@@ -6018,13 +6096,13 @@ var Viewport = class {
6018
6096
  distributeSelection(axis) {
6019
6097
  const bounded = this.boundedSelection();
6020
6098
  if (bounded.length < 3) return;
6021
- const center = (b) => axis === "horizontal" ? b.x + b.w / 2 : b.y + b.h / 2;
6022
- const sorted = [...bounded].sort((p, q) => center(p.bounds) - center(q.bounds));
6099
+ const center2 = (b) => axis === "horizontal" ? b.x + b.w / 2 : b.y + b.h / 2;
6100
+ const sorted = [...bounded].sort((p, q) => center2(p.bounds) - center2(q.bounds));
6023
6101
  const first = sorted[0];
6024
6102
  const last = sorted[sorted.length - 1];
6025
6103
  if (!first || !last) return;
6026
- const c0 = center(first.bounds);
6027
- const cN = center(last.bounds);
6104
+ const c0 = center2(first.bounds);
6105
+ const cN = center2(last.bounds);
6028
6106
  const n = sorted.length;
6029
6107
  this.historyRecorder.begin();
6030
6108
  const moved = [];
@@ -6032,7 +6110,7 @@ var Viewport = class {
6032
6110
  const item = sorted[i];
6033
6111
  if (!item || !this.isMovable(item.el)) continue;
6034
6112
  const target = c0 + i * (cN - c0) / (n - 1);
6035
- const delta = target - center(item.bounds);
6113
+ const delta = target - center2(item.bounds);
6036
6114
  if (delta === 0) continue;
6037
6115
  const [dx, dy] = axis === "horizontal" ? [delta, 0] : [0, delta];
6038
6116
  this.store.update(item.id, translateElementPatch(item.el, dx, dy));
@@ -6755,10 +6833,10 @@ function applyArrowHandleDrag(handle, elementId, world, ctx) {
6755
6833
  const excludeId = el.toBinding?.elementId;
6756
6834
  const target = findBindTarget(world, ctx.store, threshold, excludeId, layerFilter);
6757
6835
  if (target) {
6758
- const center = getElementCenter(target);
6836
+ const center2 = getElementCenter(target);
6759
6837
  ctx.store.update(elementId, {
6760
- from: center,
6761
- position: center,
6838
+ from: center2,
6839
+ position: center2,
6762
6840
  fromBinding: { elementId: target.id }
6763
6841
  });
6764
6842
  } else {
@@ -6774,9 +6852,9 @@ function applyArrowHandleDrag(handle, elementId, world, ctx) {
6774
6852
  const excludeId = el.fromBinding?.elementId;
6775
6853
  const target = findBindTarget(world, ctx.store, threshold, excludeId, layerFilter);
6776
6854
  if (target) {
6777
- const center = getElementCenter(target);
6855
+ const center2 = getElementCenter(target);
6778
6856
  ctx.store.update(elementId, {
6779
- to: center,
6857
+ to: center2,
6780
6858
  toBinding: { elementId: target.id }
6781
6859
  });
6782
6860
  } else {
@@ -6865,6 +6943,9 @@ var SNAP_PX = 6;
6865
6943
  var HANDLE_HIT_PADDING2 = 4;
6866
6944
  var SELECTION_PAD = 4;
6867
6945
  var MIN_ELEMENT_SIZE = 20;
6946
+ var ROTATE_HANDLE_OFFSET = 24;
6947
+ var ROTATE_SNAP = Math.PI / 12;
6948
+ var ROTATABLE_TYPES = /* @__PURE__ */ new Set(["note", "text", "image", "html", "shape", "stroke"]);
6868
6949
  var HANDLE_CURSORS = {
6869
6950
  nw: "nwse-resize",
6870
6951
  se: "nwse-resize",
@@ -6953,6 +7034,22 @@ var SelectTool = class {
6953
7034
  ctx.requestRender();
6954
7035
  return;
6955
7036
  }
7037
+ const rotateHit = this.hitTestRotateHandle(world, ctx);
7038
+ if (rotateHit) {
7039
+ const el = ctx.store.getById(rotateHit.elementId);
7040
+ const layout = el ? this.getOverlayLayout(el, ctx.camera.zoom) : null;
7041
+ if (el && layout) {
7042
+ this.mode = {
7043
+ type: "rotating",
7044
+ elementId: rotateHit.elementId,
7045
+ center: layout.center,
7046
+ startPointerAngle: Math.atan2(world.y - layout.center.y, world.x - layout.center.x),
7047
+ startRotation: el.rotation ?? 0
7048
+ };
7049
+ ctx.requestRender();
7050
+ return;
7051
+ }
7052
+ }
6956
7053
  const resizeHit = this.hitTestResizeHandle(world, ctx);
6957
7054
  if (resizeHit) {
6958
7055
  const el = ctx.store.getById(resizeHit.elementId);
@@ -7018,6 +7115,15 @@ var SelectTool = class {
7018
7115
  this.handleTemplateResize(world, ctx);
7019
7116
  return;
7020
7117
  }
7118
+ if (this.mode.type === "rotating") {
7119
+ const { elementId, center: center2, startPointerAngle, startRotation } = this.mode;
7120
+ const a = Math.atan2(world.y - center2.y, world.x - center2.x);
7121
+ let next = startRotation + (a - startPointerAngle);
7122
+ if (state.shiftKey) next = Math.round(next / ROTATE_SNAP) * ROTATE_SNAP;
7123
+ ctx.store.update(elementId, { rotation: normalizeAngle(next) });
7124
+ ctx.requestRender();
7125
+ return;
7126
+ }
7021
7127
  if (this.mode.type === "resizing") {
7022
7128
  ctx.setCursor?.(HANDLE_CURSORS[this.mode.handle]);
7023
7129
  this.handleResize(world, ctx, state.shiftKey);
@@ -7220,6 +7326,10 @@ var SelectTool = class {
7220
7326
  ctx.setCursor?.("nwse-resize");
7221
7327
  return null;
7222
7328
  }
7329
+ if (this.hitTestRotateHandle(world, ctx)) {
7330
+ ctx.setCursor?.("grab");
7331
+ return null;
7332
+ }
7223
7333
  const resizeHit = this.hitTestResizeHandle(world, ctx);
7224
7334
  if (resizeHit) {
7225
7335
  ctx.setCursor?.(HANDLE_CURSORS[resizeHit.handle]);
@@ -7238,6 +7348,11 @@ var SelectTool = class {
7238
7348
  if (this.mode.type !== "resizing") return;
7239
7349
  const el = ctx.store.getById(this.mode.elementId);
7240
7350
  if (!el || !("size" in el) || el.locked) return;
7351
+ const angle = el.rotation ?? 0;
7352
+ if (angle !== 0) {
7353
+ this.handleRotatedResize(world, el, angle, ctx, shiftKey);
7354
+ return;
7355
+ }
7241
7356
  const { handle } = this.mode;
7242
7357
  const dx = world.x - this.lastWorld.x;
7243
7358
  const dy = world.y - this.lastWorld.y;
@@ -7295,6 +7410,78 @@ var SelectTool = class {
7295
7410
  this.updateArrowsBoundTo([this.mode.elementId], ctx);
7296
7411
  ctx.requestRender();
7297
7412
  }
7413
+ anchorOffset(handle, w, h) {
7414
+ switch (handle) {
7415
+ case "se":
7416
+ return { x: -w / 2, y: -h / 2 };
7417
+ case "sw":
7418
+ return { x: w / 2, y: -h / 2 };
7419
+ case "ne":
7420
+ return { x: -w / 2, y: h / 2 };
7421
+ case "nw":
7422
+ return { x: w / 2, y: h / 2 };
7423
+ default:
7424
+ return { x: 0, y: 0 };
7425
+ }
7426
+ }
7427
+ handleRotatedResize(world, el, angle, ctx, shiftKey) {
7428
+ if (this.mode.type !== "resizing") return;
7429
+ const { handle } = this.mode;
7430
+ const wdx = world.x - this.lastWorld.x;
7431
+ const wdy = world.y - this.lastWorld.y;
7432
+ this.lastWorld = world;
7433
+ const cosN = Math.cos(-angle);
7434
+ const sinN = Math.sin(-angle);
7435
+ const ldx = wdx * cosN - wdy * sinN;
7436
+ const ldy = wdx * sinN + wdy * cosN;
7437
+ let w = el.size.w;
7438
+ let h = el.size.h;
7439
+ switch (handle) {
7440
+ case "se":
7441
+ w += ldx;
7442
+ h += ldy;
7443
+ break;
7444
+ case "sw":
7445
+ w -= ldx;
7446
+ h += ldy;
7447
+ break;
7448
+ case "ne":
7449
+ w += ldx;
7450
+ h -= ldy;
7451
+ break;
7452
+ case "nw":
7453
+ w -= ldx;
7454
+ h -= ldy;
7455
+ break;
7456
+ }
7457
+ if (shiftKey && this.resizeAspectRatio > 0) {
7458
+ const absDw = Math.abs(w - el.size.w);
7459
+ const absDh = Math.abs(h - el.size.h);
7460
+ if (absDw >= absDh) h = w / this.resizeAspectRatio;
7461
+ else w = h * this.resizeAspectRatio;
7462
+ }
7463
+ w = Math.max(w, MIN_ELEMENT_SIZE);
7464
+ h = Math.max(h, MIN_ELEMENT_SIZE);
7465
+ const oldCenter = { x: el.position.x + el.size.w / 2, y: el.position.y + el.size.h / 2 };
7466
+ const oldAnchorLocal = this.anchorOffset(handle, el.size.w, el.size.h);
7467
+ const anchorWorld = rotatePoint(
7468
+ { x: oldCenter.x + oldAnchorLocal.x, y: oldCenter.y + oldAnchorLocal.y },
7469
+ oldCenter,
7470
+ angle
7471
+ );
7472
+ const newAnchorLocal = this.anchorOffset(handle, w, h);
7473
+ const cos = Math.cos(angle);
7474
+ const sin = Math.sin(angle);
7475
+ const rotatedAnchor = {
7476
+ x: newAnchorLocal.x * cos - newAnchorLocal.y * sin,
7477
+ y: newAnchorLocal.x * sin + newAnchorLocal.y * cos
7478
+ };
7479
+ const newCenter = { x: anchorWorld.x - rotatedAnchor.x, y: anchorWorld.y - rotatedAnchor.y };
7480
+ const position = { x: newCenter.x - w / 2, y: newCenter.y - h / 2 };
7481
+ ctx.store.update(this.mode.elementId, { position, size: { w, h } });
7482
+ this.updateArrowsBoundTo([this.mode.elementId], ctx);
7483
+ ctx.requestRender();
7484
+ }
7298
7485
  hitTestResizeHandle(world, ctx) {
7299
7486
  if (this._selectedIds.length === 0) return null;
7300
7487
  const zoom = ctx.camera.zoom;
@@ -7303,10 +7490,9 @@ var SelectTool = class {
7303
7490
  const el = ctx.store.getById(id);
7304
7491
  if (!el || !("size" in el)) continue;
7305
7492
  if (el.type === "shape" && el.shape === "line") continue;
7306
- const bounds = getElementBounds(el);
7307
- if (!bounds) continue;
7308
- const corners = this.getHandlePositions(bounds);
7309
- for (const [handle, pos] of corners) {
7493
+ const layout = this.getOverlayLayout(el, zoom);
7494
+ if (!layout) continue;
7495
+ for (const [handle, pos] of layout.corners) {
7310
7496
  if (Math.abs(world.x - pos.x) <= handleHalf && Math.abs(world.y - pos.y) <= handleHalf) {
7311
7497
  return { elementId: id, handle };
7312
7498
  }
@@ -7314,6 +7500,19 @@ var SelectTool = class {
7314
7500
  }
7315
7501
  return null;
7316
7502
  }
7503
+ hitTestRotateHandle(world, ctx) {
7504
+ if (this._selectedIds.length !== 1) return null;
7505
+ const id = this._selectedIds[0];
7506
+ if (!id) return null;
7507
+ const el = ctx.store.getById(id);
7508
+ if (!el || el.locked || !ROTATABLE_TYPES.has(el.type)) return null;
7509
+ const layout = this.getOverlayLayout(el, ctx.camera.zoom);
7510
+ if (!layout) return null;
7511
+ const r = (HANDLE_SIZE / 2 + HANDLE_HIT_PADDING2) / ctx.camera.zoom;
7512
+ const dx = world.x - layout.rotateHandle.x;
7513
+ const dy = world.y - layout.rotateHandle.y;
7514
+ return dx * dx + dy * dy <= r * r ? { elementId: id } : null;
7515
+ }
7317
7516
  hitTestLineHandles(world, ctx) {
7318
7517
  if (this._selectedIds.length === 0) return null;
7319
7518
  const zoom = ctx.camera.zoom;
@@ -7336,6 +7535,30 @@ var SelectTool = class {
7336
7535
  ["se", { x: bounds.x + bounds.w, y: bounds.y + bounds.h }]
7337
7536
  ];
7338
7537
  }
7538
+ getOverlayLayout(el, zoom) {
7539
+ const bounds = getElementBounds(el);
7540
+ if (!bounds) return null;
7541
+ const angle = el.rotation ?? 0;
7542
+ const pad = SELECTION_PAD / zoom;
7543
+ const center2 = { x: bounds.x + bounds.w / 2, y: bounds.y + bounds.h / 2 };
7544
+ const raw = [
7545
+ ["nw", { x: bounds.x - pad, y: bounds.y - pad }],
7546
+ ["ne", { x: bounds.x + bounds.w + pad, y: bounds.y - pad }],
7547
+ ["sw", { x: bounds.x - pad, y: bounds.y + bounds.h + pad }],
7548
+ ["se", { x: bounds.x + bounds.w + pad, y: bounds.y + bounds.h + pad }]
7549
+ ];
7550
+ const corners = raw.map(
7551
+ ([h, p]) => [h, rotatePoint(p, center2, angle)]
7552
+ );
7553
+ const topMid = { x: center2.x, y: bounds.y - pad - ROTATE_HANDLE_OFFSET / zoom };
7554
+ const rotateHandle = rotatePoint(topMid, center2, angle);
7555
+ return { center: center2, corners, rotateHandle, angle };
7556
+ }
7557
+ topMidpoint(layout) {
7558
+ const nw = layout.corners.find(([h]) => h === "nw")?.[1] ?? { x: 0, y: 0 };
7559
+ const ne = layout.corners.find(([h]) => h === "ne")?.[1] ?? { x: 0, y: 0 };
7560
+ return { x: (nw.x + ne.x) / 2, y: (nw.y + ne.y) / 2 };
7561
+ }
7339
7562
  renderMarquee(canvasCtx) {
7340
7563
  if (this.mode.type !== "marquee") return;
7341
7564
  const rect = this.getMarqueeRect();
@@ -7380,12 +7603,31 @@ var SelectTool = class {
7380
7603
  }
7381
7604
  const bounds = getElementBounds(el);
7382
7605
  if (!bounds) continue;
7606
+ const layout = this.getOverlayLayout(el, zoom);
7607
+ if (!layout) continue;
7383
7608
  const pad = SELECTION_PAD / zoom;
7384
- canvasCtx.strokeRect(bounds.x - pad, bounds.y - pad, bounds.w + pad * 2, bounds.h + pad * 2);
7609
+ if (layout.angle === 0) {
7610
+ canvasCtx.strokeRect(
7611
+ bounds.x - pad,
7612
+ bounds.y - pad,
7613
+ bounds.w + pad * 2,
7614
+ bounds.h + pad * 2
7615
+ );
7616
+ } else {
7617
+ const ordered = ["nw", "ne", "se", "sw"].map((h) => layout.corners.find(([c]) => c === h)?.[1]).filter((p) => !!p);
7618
+ const [p0, ...others] = ordered;
7619
+ if (p0) {
7620
+ canvasCtx.beginPath();
7621
+ canvasCtx.moveTo(p0.x, p0.y);
7622
+ for (const p of others) canvasCtx.lineTo(p.x, p.y);
7623
+ canvasCtx.closePath();
7624
+ canvasCtx.stroke();
7625
+ }
7626
+ }
7385
7627
  if ("size" in el) {
7386
7628
  canvasCtx.setLineDash([]);
7387
7629
  canvasCtx.fillStyle = "#ffffff";
7388
- const corners = this.getHandlePositions(bounds);
7630
+ const corners = layout.angle === 0 ? this.getHandlePositions(bounds) : layout.corners;
7389
7631
  for (const [, pos] of corners) {
7390
7632
  canvasCtx.fillRect(
7391
7633
  pos.x - handleWorldSize / 2,
@@ -7420,6 +7662,21 @@ var SelectTool = class {
7420
7662
  );
7421
7663
  canvasCtx.setLineDash([4 / zoom, 4 / zoom]);
7422
7664
  }
7665
+ if (this._selectedIds.length === 1 && ROTATABLE_TYPES.has(el.type)) {
7666
+ const stemStart = this.topMidpoint(layout);
7667
+ const stemEnd = layout.rotateHandle;
7668
+ canvasCtx.beginPath();
7669
+ canvasCtx.moveTo(stemStart.x, stemStart.y);
7670
+ canvasCtx.lineTo(stemEnd.x, stemEnd.y);
7671
+ canvasCtx.stroke();
7672
+ canvasCtx.setLineDash([]);
7673
+ canvasCtx.fillStyle = "#ffffff";
7674
+ canvasCtx.beginPath();
7675
+ canvasCtx.arc(stemEnd.x, stemEnd.y, handleWorldSize / 2, 0, Math.PI * 2);
7676
+ canvasCtx.fill();
7677
+ canvasCtx.stroke();
7678
+ canvasCtx.setLineDash([4 / zoom, 4 / zoom]);
7679
+ }
7423
7680
  }
7424
7681
  canvasCtx.restore();
7425
7682
  }
@@ -7499,7 +7756,7 @@ var SelectTool = class {
7499
7756
  if (ctx.isLayerLocked && ctx.isLayerLocked(el.layerId)) continue;
7500
7757
  if (el.type === "grid") continue;
7501
7758
  const bounds = getElementBounds(el);
7502
- if (bounds && this.rectsOverlap(marquee, bounds)) {
7759
+ if (bounds && this.rectsOverlap(marquee, rotatedAABB(bounds, el.rotation ?? 0))) {
7503
7760
  ids.push(el.id);
7504
7761
  }
7505
7762
  }
@@ -7521,6 +7778,13 @@ var SelectTool = class {
7521
7778
  }
7522
7779
  isInsideBounds(point, el) {
7523
7780
  if (el.type === "grid") return false;
7781
+ const angle = el.rotation ?? 0;
7782
+ if (angle !== 0) {
7783
+ const b = getElementBounds(el);
7784
+ if (b) {
7785
+ point = rotatePoint(point, { x: b.x + b.w / 2, y: b.y + b.h / 2 }, -angle);
7786
+ }
7787
+ }
7524
7788
  if (el.type === "shape" && el.shape === "line") {
7525
7789
  const [a, b] = lineEndpoints(el);
7526
7790
  const threshold = Math.max(el.strokeWidth / 2, 6);
@@ -8287,20 +8551,20 @@ var TemplateTool = class {
8287
8551
  const snapUnit = Math.sqrt(3) * cellSize;
8288
8552
  const radiusCells = radius / snapUnit;
8289
8553
  const angle = this.computeAngle();
8290
- const center = this.origin;
8554
+ const center2 = this.origin;
8291
8555
  let hexCells;
8292
8556
  switch (this.templateShape) {
8293
8557
  case "circle":
8294
- hexCells = getHexCellsInRadius(center, radiusCells, cellSize, orientation);
8558
+ hexCells = getHexCellsInRadius(center2, radiusCells, cellSize, orientation);
8295
8559
  break;
8296
8560
  case "cone":
8297
- hexCells = getHexCellsInCone(center, angle, radiusCells, cellSize, orientation);
8561
+ hexCells = getHexCellsInCone(center2, angle, radiusCells, cellSize, orientation);
8298
8562
  break;
8299
8563
  case "line":
8300
- hexCells = getHexCellsInLine(center, angle, radiusCells, cellSize, orientation);
8564
+ hexCells = getHexCellsInLine(center2, angle, radiusCells, cellSize, orientation);
8301
8565
  break;
8302
8566
  case "square":
8303
- hexCells = getHexCellsInSquare(center, radiusCells, cellSize, orientation);
8567
+ hexCells = getHexCellsInSquare(center2, radiusCells, cellSize, orientation);
8304
8568
  break;
8305
8569
  }
8306
8570
  ctx.save();
@@ -8321,7 +8585,7 @@ var TemplateTool = class {
8321
8585
  if (this.templateShape === "cone" || this.templateShape === "line" || this.templateShape === "circle" || this.templateShape === "square") {
8322
8586
  ctx.globalAlpha = 0.5;
8323
8587
  ctx.beginPath();
8324
- drawHexPath(ctx, center.x, center.y, cellSize, orientation);
8588
+ drawHexPath(ctx, center2.x, center2.y, cellSize, orientation);
8325
8589
  ctx.fillStyle = this.strokeColor;
8326
8590
  ctx.fill();
8327
8591
  ctx.strokeStyle = this.strokeColor;
@@ -8337,8 +8601,8 @@ var TemplateTool = class {
8337
8601
  ctx.font = `bold ${fontSize}px system-ui, sans-serif`;
8338
8602
  ctx.textAlign = "center";
8339
8603
  ctx.textBaseline = "bottom";
8340
- const textX = center.x;
8341
- const textY = center.y - 4;
8604
+ const textX = center2.x;
8605
+ const textY = center2.y - 4;
8342
8606
  const metrics = ctx.measureText(label);
8343
8607
  const padX = 4;
8344
8608
  const padY = 2;
@@ -8388,7 +8652,7 @@ var TemplateTool = class {
8388
8652
  };
8389
8653
 
8390
8654
  // src/index.ts
8391
- var VERSION = "0.34.0";
8655
+ var VERSION = "0.35.0";
8392
8656
  // Annotate the CommonJS export names for ESM import in node:
8393
8657
  0 && (module.exports = {
8394
8658
  ArrowTool,