@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.js CHANGED
@@ -524,6 +524,36 @@ function distSqToSegment(p, a, b) {
524
524
  const dy = p.y - (a.y + t * aby);
525
525
  return dx * dx + dy * dy;
526
526
  }
527
+ function rotatePoint(p, center2, angle) {
528
+ if (angle === 0) return p;
529
+ const cos = Math.cos(angle);
530
+ const sin = Math.sin(angle);
531
+ const dx = p.x - center2.x;
532
+ const dy = p.y - center2.y;
533
+ return { x: center2.x + dx * cos - dy * sin, y: center2.y + dx * sin + dy * cos };
534
+ }
535
+ function rotatedAABB(bounds, angle) {
536
+ if (angle === 0) return bounds;
537
+ const c = { x: bounds.x + bounds.w / 2, y: bounds.y + bounds.h / 2 };
538
+ const corners = [
539
+ { x: bounds.x, y: bounds.y },
540
+ { x: bounds.x + bounds.w, y: bounds.y },
541
+ { x: bounds.x + bounds.w, y: bounds.y + bounds.h },
542
+ { x: bounds.x, y: bounds.y + bounds.h }
543
+ ].map((p) => rotatePoint(p, c, angle));
544
+ const xs = corners.map((p) => p.x);
545
+ const ys = corners.map((p) => p.y);
546
+ const minX = Math.min(...xs);
547
+ const minY = Math.min(...ys);
548
+ return { x: minX, y: minY, w: Math.max(...xs) - minX, h: Math.max(...ys) - minY };
549
+ }
550
+ function normalizeAngle(angle) {
551
+ const twoPi = Math.PI * 2;
552
+ let a = angle % twoPi;
553
+ if (a <= -Math.PI) a += twoPi;
554
+ else if (a > Math.PI) a -= twoPi;
555
+ return a;
556
+ }
527
557
 
528
558
  // src/elements/arrow-geometry.ts
529
559
  function getArrowControlPoint(from, to, bend) {
@@ -1501,18 +1531,18 @@ var InputHandler = class {
1501
1531
  handlePinchMove() {
1502
1532
  const [a, b] = this.getPinchPoints();
1503
1533
  const dist = this.distance(a, b);
1504
- const center = this.midpoint(a, b);
1534
+ const center2 = this.midpoint(a, b);
1505
1535
  if (this.lastPinchDistance > 0) {
1506
1536
  const scale = dist / this.lastPinchDistance;
1507
1537
  const newZoom = this.camera.zoom * scale;
1508
- this.camera.zoomAt(newZoom, center);
1538
+ this.camera.zoomAt(newZoom, center2);
1509
1539
  }
1510
- const dx = center.x - this.lastPointer.x;
1511
- const dy = center.y - this.lastPointer.y;
1540
+ const dx = center2.x - this.lastPointer.x;
1541
+ const dy = center2.y - this.lastPointer.y;
1512
1542
  this.camera.pan(dx, dy);
1513
1543
  this.lastPinchDistance = dist;
1514
- this.lastPinchCenter = center;
1515
- this.lastPointer = { ...center };
1544
+ this.lastPinchCenter = center2;
1545
+ this.lastPointer = { ...center2 };
1516
1546
  }
1517
1547
  getPinchPoints() {
1518
1548
  const pts = [...this.activePointers.values()];
@@ -2054,11 +2084,19 @@ var ElementStore = class {
2054
2084
  (el) => el.type === type
2055
2085
  );
2056
2086
  }
2087
+ // Spatial index stores the rotation-expanded AABB so rotated elements remain
2088
+ // broad-phase hit-test/marquee candidates; precise tests run against local bounds.
2089
+ indexBounds(element) {
2090
+ const bounds = getElementBounds(element);
2091
+ if (!bounds) return null;
2092
+ const angle = element.rotation ?? 0;
2093
+ return angle === 0 ? bounds : rotatedAABB(bounds, angle);
2094
+ }
2057
2095
  add(element) {
2058
2096
  this.sortedCache = null;
2059
2097
  this._versions.set(element.id, 0);
2060
2098
  this.elements.set(element.id, element);
2061
- const bounds = getElementBounds(element);
2099
+ const bounds = this.indexBounds(element);
2062
2100
  if (bounds) this.spatialIndex.insert(element.id, bounds);
2063
2101
  this.bus.emit("add", element);
2064
2102
  }
@@ -2080,7 +2118,7 @@ var ElementStore = class {
2080
2118
  updated.text = sanitizeNoteHtml(updated.text);
2081
2119
  }
2082
2120
  this.elements.set(id, updated);
2083
- const newBounds = getElementBounds(updated);
2121
+ const newBounds = this.indexBounds(updated);
2084
2122
  if (newBounds) {
2085
2123
  this.spatialIndex.update(id, newBounds);
2086
2124
  }
@@ -2113,7 +2151,7 @@ var ElementStore = class {
2113
2151
  for (const el of elements) {
2114
2152
  this.elements.set(el.id, el);
2115
2153
  this._versions.set(el.id, 0);
2116
- const bounds = getElementBounds(el);
2154
+ const bounds = this.indexBounds(el);
2117
2155
  if (bounds) this.spatialIndex.insert(el.id, bounds);
2118
2156
  if (el.type === "stroke") {
2119
2157
  computeStrokeSegments(el);
@@ -2318,9 +2356,9 @@ function updateBoundArrow(arrow, store) {
2318
2356
  if (arrow.fromBinding) {
2319
2357
  const el = store.getById(arrow.fromBinding.elementId);
2320
2358
  if (el) {
2321
- const center = getElementCenter(el);
2322
- updates.from = center;
2323
- updates.position = center;
2359
+ const center2 = getElementCenter(el);
2360
+ updates.from = center2;
2361
+ updates.position = center2;
2324
2362
  }
2325
2363
  }
2326
2364
  if (arrow.toBinding) {
@@ -2332,6 +2370,21 @@ function updateBoundArrow(arrow, store) {
2332
2370
  return Object.keys(updates).length > 0 ? updates : null;
2333
2371
  }
2334
2372
 
2373
+ // src/elements/rotate-canvas.ts
2374
+ function withRotation(ctx, el, center2, draw) {
2375
+ const angle = el.rotation ?? 0;
2376
+ if (angle === 0) {
2377
+ draw();
2378
+ return;
2379
+ }
2380
+ ctx.save();
2381
+ ctx.translate(center2.x, center2.y);
2382
+ ctx.rotate(angle);
2383
+ ctx.translate(-center2.x, -center2.y);
2384
+ draw();
2385
+ ctx.restore();
2386
+ }
2387
+
2335
2388
  // src/elements/grid-renderer.ts
2336
2389
  function getSquareGridLines(bounds, cellSize) {
2337
2390
  if (cellSize <= 0) return { verticals: [], horizontals: [] };
@@ -2595,18 +2648,18 @@ function getHexDistance(a, b, cellSize, orientation) {
2595
2648
  const ds = -dq - dr;
2596
2649
  return Math.max(Math.abs(dq), Math.abs(dr), Math.abs(ds));
2597
2650
  }
2598
- function getHexCellsInRadius(center, radiusCells, cellSize, orientation) {
2651
+ function getHexCellsInRadius(center2, radiusCells, cellSize, orientation) {
2599
2652
  const n = Math.round(radiusCells);
2600
- const off = pixelToOffset(center.x, center.y, cellSize, orientation);
2653
+ const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
2601
2654
  const cube = offsetToCube(off.col, off.row, orientation);
2602
2655
  if (n <= 0) {
2603
2656
  return [offsetToPixel(off.col, off.row, cellSize, orientation)];
2604
2657
  }
2605
2658
  return enumerateHexRing(cube.q, cube.r, n, orientation, cellSize);
2606
2659
  }
2607
- function getHexCellsInCone(center, angle, radiusCells, cellSize, orientation) {
2660
+ function getHexCellsInCone(center2, angle, radiusCells, cellSize, orientation) {
2608
2661
  const n = Math.round(radiusCells);
2609
- const off = pixelToOffset(center.x, center.y, cellSize, orientation);
2662
+ const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
2610
2663
  const cube = offsetToCube(off.col, off.row, orientation);
2611
2664
  const centerPixel = offsetToPixel(off.col, off.row, cellSize, orientation);
2612
2665
  if (n <= 0) return [centerPixel];
@@ -2640,9 +2693,9 @@ function getHexCellsInCone(center, angle, radiusCells, cellSize, orientation) {
2640
2693
  }
2641
2694
  return cells;
2642
2695
  }
2643
- function getHexCellsInLine(center, angle, radiusCells, cellSize, orientation) {
2696
+ function getHexCellsInLine(center2, angle, radiusCells, cellSize, orientation) {
2644
2697
  const n = Math.round(radiusCells);
2645
- const off = pixelToOffset(center.x, center.y, cellSize, orientation);
2698
+ const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
2646
2699
  const cube = offsetToCube(off.col, off.row, orientation);
2647
2700
  const centerPixel = offsetToPixel(off.col, off.row, cellSize, orientation);
2648
2701
  if (n <= 0) return [centerPixel];
@@ -2678,9 +2731,9 @@ function getHexCellsInLine(center, angle, radiusCells, cellSize, orientation) {
2678
2731
  }
2679
2732
  return cells;
2680
2733
  }
2681
- function getHexCellsInSquare(center, radiusCells, cellSize, orientation) {
2734
+ function getHexCellsInSquare(center2, radiusCells, cellSize, orientation) {
2682
2735
  const n = Math.round(radiusCells);
2683
- const off = pixelToOffset(center.x, center.y, cellSize, orientation);
2736
+ const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
2684
2737
  const cube = offsetToCube(off.col, off.row, orientation);
2685
2738
  const centerPixel = offsetToPixel(off.col, off.row, cellSize, orientation);
2686
2739
  if (n <= 0) return [centerPixel];
@@ -2758,18 +2811,27 @@ var ElementRenderer = class {
2758
2811
  }
2759
2812
  renderCanvasElement(ctx, element) {
2760
2813
  switch (element.type) {
2761
- case "stroke":
2762
- this.renderStroke(ctx, element);
2814
+ case "stroke": {
2815
+ const b = getElementBounds(element);
2816
+ const c = b ? { x: b.x + b.w / 2, y: b.y + b.h / 2 } : element.position;
2817
+ withRotation(ctx, element, c, () => this.renderStroke(ctx, element));
2763
2818
  break;
2819
+ }
2764
2820
  case "arrow":
2765
2821
  this.renderArrow(ctx, element);
2766
2822
  break;
2767
- case "shape":
2768
- this.renderShape(ctx, element);
2823
+ case "shape": {
2824
+ const b = getElementBounds(element);
2825
+ const c = b ? { x: b.x + b.w / 2, y: b.y + b.h / 2 } : element.position;
2826
+ withRotation(ctx, element, c, () => this.renderShape(ctx, element));
2769
2827
  break;
2770
- case "image":
2771
- this.renderImage(ctx, element);
2828
+ }
2829
+ case "image": {
2830
+ const b = getElementBounds(element);
2831
+ const c = b ? { x: b.x + b.w / 2, y: b.y + b.h / 2 } : element.position;
2832
+ withRotation(ctx, element, c, () => this.renderImage(ctx, element));
2772
2833
  break;
2834
+ }
2773
2835
  case "grid":
2774
2836
  this.renderGrid(ctx, element);
2775
2837
  break;
@@ -3066,20 +3128,20 @@ var ElementRenderer = class {
3066
3128
  renderHexTemplate(ctx, template, cellSize, orientation) {
3067
3129
  const snapUnit = Math.sqrt(3) * cellSize;
3068
3130
  const radiusCells = template.radius / snapUnit;
3069
- const center = template.position;
3131
+ const center2 = template.position;
3070
3132
  let cells;
3071
3133
  switch (template.templateShape) {
3072
3134
  case "circle":
3073
- cells = getHexCellsInRadius(center, radiusCells, cellSize, orientation);
3135
+ cells = getHexCellsInRadius(center2, radiusCells, cellSize, orientation);
3074
3136
  break;
3075
3137
  case "cone":
3076
- cells = getHexCellsInCone(center, template.angle, radiusCells, cellSize, orientation);
3138
+ cells = getHexCellsInCone(center2, template.angle, radiusCells, cellSize, orientation);
3077
3139
  break;
3078
3140
  case "line":
3079
- cells = getHexCellsInLine(center, template.angle, radiusCells, cellSize, orientation);
3141
+ cells = getHexCellsInLine(center2, template.angle, radiusCells, cellSize, orientation);
3080
3142
  break;
3081
3143
  case "square":
3082
- cells = getHexCellsInSquare(center, radiusCells, cellSize, orientation);
3144
+ cells = getHexCellsInSquare(center2, radiusCells, cellSize, orientation);
3083
3145
  break;
3084
3146
  }
3085
3147
  ctx.save();
@@ -3100,7 +3162,7 @@ var ElementRenderer = class {
3100
3162
  {
3101
3163
  ctx.globalAlpha = Math.min(template.opacity + 0.1, 1);
3102
3164
  ctx.beginPath();
3103
- drawHexPath(ctx, center.x, center.y, cellSize, orientation);
3165
+ drawHexPath(ctx, center2.x, center2.y, cellSize, orientation);
3104
3166
  ctx.fillStyle = template.strokeColor;
3105
3167
  ctx.fill();
3106
3168
  ctx.strokeStyle = template.strokeColor;
@@ -3109,7 +3171,7 @@ var ElementRenderer = class {
3109
3171
  }
3110
3172
  if (template.templateShape === "circle" && template.radiusFeet != null && template.radiusFeet > 0) {
3111
3173
  const r = template.radius;
3112
- this.renderRadiusMarker(ctx, center.x, center.y, r, template.radiusFeet);
3174
+ this.renderRadiusMarker(ctx, center2.x, center2.y, r, template.radiusFeet);
3113
3175
  }
3114
3176
  ctx.restore();
3115
3177
  }
@@ -4228,6 +4290,7 @@ function renderStyledRuns(ctx, runs, startX, startY, maxWidth) {
4228
4290
  }
4229
4291
 
4230
4292
  // src/canvas/export-image.ts
4293
+ var center = (b) => ({ x: b.x + b.w / 2, y: b.y + b.h / 2 });
4231
4294
  function getStrokeBounds(el) {
4232
4295
  if (el.type !== "stroke") return null;
4233
4296
  if (el.points.length === 0) return null;
@@ -4253,8 +4316,10 @@ function getStrokeBounds(el) {
4253
4316
  }
4254
4317
  function getElementRect(el) {
4255
4318
  switch (el.type) {
4256
- case "stroke":
4257
- return getStrokeBounds(el);
4319
+ case "stroke": {
4320
+ const r = getStrokeBounds(el);
4321
+ return r ? rotatedAABB(r, el.rotation ?? 0) : r;
4322
+ }
4258
4323
  case "arrow": {
4259
4324
  const b = getArrowBounds(el.from, el.to, el.bend);
4260
4325
  const pad = el.width / 2 + 14;
@@ -4273,7 +4338,10 @@ function getElementRect(el) {
4273
4338
  case "text":
4274
4339
  case "shape":
4275
4340
  if ("size" in el) {
4276
- return { x: el.position.x, y: el.position.y, w: el.size.w, h: el.size.h };
4341
+ return rotatedAABB(
4342
+ { x: el.position.x, y: el.position.y, w: el.size.w, h: el.size.h },
4343
+ el.rotation ?? 0
4344
+ );
4277
4345
  }
4278
4346
  return null;
4279
4347
  default:
@@ -4411,11 +4479,13 @@ async function exportImage(store, options = {}, layerManager) {
4411
4479
  continue;
4412
4480
  }
4413
4481
  if (el.type === "note") {
4414
- renderNoteOnCanvas(ctx, el);
4482
+ const b = getElementBounds(el);
4483
+ withRotation(ctx, el, b ? center(b) : el.position, () => renderNoteOnCanvas(ctx, el));
4415
4484
  continue;
4416
4485
  }
4417
4486
  if (el.type === "text") {
4418
- renderTextOnCanvas(ctx, el);
4487
+ const b = getElementBounds(el);
4488
+ withRotation(ctx, el, b ? center(b) : el.position, () => renderTextOnCanvas(ctx, el));
4419
4489
  continue;
4420
4490
  }
4421
4491
  if (el.type === "html") {
@@ -4424,7 +4494,13 @@ async function exportImage(store, options = {}, layerManager) {
4424
4494
  if (el.type === "image") {
4425
4495
  const img = imageCache.get(el.id);
4426
4496
  if (img) {
4427
- ctx.drawImage(img, el.position.x, el.position.y, el.size.w, el.size.h);
4497
+ const b = getElementBounds(el);
4498
+ withRotation(
4499
+ ctx,
4500
+ el,
4501
+ b ? center(b) : el.position,
4502
+ () => ctx.drawImage(img, el.position.x, el.position.y, el.size.w, el.size.h)
4503
+ );
4428
4504
  }
4429
4505
  continue;
4430
4506
  }
@@ -4760,7 +4836,9 @@ var DomNodeManager = class {
4760
4836
  top: `${element.position.y}px`,
4761
4837
  width: size ? `${size.w}px` : "auto",
4762
4838
  height: size ? `${size.h}px` : "auto",
4763
- zIndex: String(zIndex)
4839
+ zIndex: String(zIndex),
4840
+ transform: element.rotation ? `rotate(${element.rotation}rad)` : "",
4841
+ transformOrigin: "50% 50%"
4764
4842
  });
4765
4843
  this.renderDomContent(node, element);
4766
4844
  }
@@ -5937,13 +6015,13 @@ var Viewport = class {
5937
6015
  distributeSelection(axis) {
5938
6016
  const bounded = this.boundedSelection();
5939
6017
  if (bounded.length < 3) return;
5940
- const center = (b) => axis === "horizontal" ? b.x + b.w / 2 : b.y + b.h / 2;
5941
- const sorted = [...bounded].sort((p, q) => center(p.bounds) - center(q.bounds));
6018
+ const center2 = (b) => axis === "horizontal" ? b.x + b.w / 2 : b.y + b.h / 2;
6019
+ const sorted = [...bounded].sort((p, q) => center2(p.bounds) - center2(q.bounds));
5942
6020
  const first = sorted[0];
5943
6021
  const last = sorted[sorted.length - 1];
5944
6022
  if (!first || !last) return;
5945
- const c0 = center(first.bounds);
5946
- const cN = center(last.bounds);
6023
+ const c0 = center2(first.bounds);
6024
+ const cN = center2(last.bounds);
5947
6025
  const n = sorted.length;
5948
6026
  this.historyRecorder.begin();
5949
6027
  const moved = [];
@@ -5951,7 +6029,7 @@ var Viewport = class {
5951
6029
  const item = sorted[i];
5952
6030
  if (!item || !this.isMovable(item.el)) continue;
5953
6031
  const target = c0 + i * (cN - c0) / (n - 1);
5954
- const delta = target - center(item.bounds);
6032
+ const delta = target - center2(item.bounds);
5955
6033
  if (delta === 0) continue;
5956
6034
  const [dx, dy] = axis === "horizontal" ? [delta, 0] : [0, delta];
5957
6035
  this.store.update(item.id, translateElementPatch(item.el, dx, dy));
@@ -6674,10 +6752,10 @@ function applyArrowHandleDrag(handle, elementId, world, ctx) {
6674
6752
  const excludeId = el.toBinding?.elementId;
6675
6753
  const target = findBindTarget(world, ctx.store, threshold, excludeId, layerFilter);
6676
6754
  if (target) {
6677
- const center = getElementCenter(target);
6755
+ const center2 = getElementCenter(target);
6678
6756
  ctx.store.update(elementId, {
6679
- from: center,
6680
- position: center,
6757
+ from: center2,
6758
+ position: center2,
6681
6759
  fromBinding: { elementId: target.id }
6682
6760
  });
6683
6761
  } else {
@@ -6693,9 +6771,9 @@ function applyArrowHandleDrag(handle, elementId, world, ctx) {
6693
6771
  const excludeId = el.fromBinding?.elementId;
6694
6772
  const target = findBindTarget(world, ctx.store, threshold, excludeId, layerFilter);
6695
6773
  if (target) {
6696
- const center = getElementCenter(target);
6774
+ const center2 = getElementCenter(target);
6697
6775
  ctx.store.update(elementId, {
6698
- to: center,
6776
+ to: center2,
6699
6777
  toBinding: { elementId: target.id }
6700
6778
  });
6701
6779
  } else {
@@ -6784,6 +6862,9 @@ var SNAP_PX = 6;
6784
6862
  var HANDLE_HIT_PADDING2 = 4;
6785
6863
  var SELECTION_PAD = 4;
6786
6864
  var MIN_ELEMENT_SIZE = 20;
6865
+ var ROTATE_HANDLE_OFFSET = 24;
6866
+ var ROTATE_SNAP = Math.PI / 12;
6867
+ var ROTATABLE_TYPES = /* @__PURE__ */ new Set(["note", "text", "image", "html", "shape", "stroke"]);
6787
6868
  var HANDLE_CURSORS = {
6788
6869
  nw: "nwse-resize",
6789
6870
  se: "nwse-resize",
@@ -6872,6 +6953,22 @@ var SelectTool = class {
6872
6953
  ctx.requestRender();
6873
6954
  return;
6874
6955
  }
6956
+ const rotateHit = this.hitTestRotateHandle(world, ctx);
6957
+ if (rotateHit) {
6958
+ const el = ctx.store.getById(rotateHit.elementId);
6959
+ const layout = el ? this.getOverlayLayout(el, ctx.camera.zoom) : null;
6960
+ if (el && layout) {
6961
+ this.mode = {
6962
+ type: "rotating",
6963
+ elementId: rotateHit.elementId,
6964
+ center: layout.center,
6965
+ startPointerAngle: Math.atan2(world.y - layout.center.y, world.x - layout.center.x),
6966
+ startRotation: el.rotation ?? 0
6967
+ };
6968
+ ctx.requestRender();
6969
+ return;
6970
+ }
6971
+ }
6875
6972
  const resizeHit = this.hitTestResizeHandle(world, ctx);
6876
6973
  if (resizeHit) {
6877
6974
  const el = ctx.store.getById(resizeHit.elementId);
@@ -6937,6 +7034,15 @@ var SelectTool = class {
6937
7034
  this.handleTemplateResize(world, ctx);
6938
7035
  return;
6939
7036
  }
7037
+ if (this.mode.type === "rotating") {
7038
+ const { elementId, center: center2, startPointerAngle, startRotation } = this.mode;
7039
+ const a = Math.atan2(world.y - center2.y, world.x - center2.x);
7040
+ let next = startRotation + (a - startPointerAngle);
7041
+ if (state.shiftKey) next = Math.round(next / ROTATE_SNAP) * ROTATE_SNAP;
7042
+ ctx.store.update(elementId, { rotation: normalizeAngle(next) });
7043
+ ctx.requestRender();
7044
+ return;
7045
+ }
6940
7046
  if (this.mode.type === "resizing") {
6941
7047
  ctx.setCursor?.(HANDLE_CURSORS[this.mode.handle]);
6942
7048
  this.handleResize(world, ctx, state.shiftKey);
@@ -7139,6 +7245,10 @@ var SelectTool = class {
7139
7245
  ctx.setCursor?.("nwse-resize");
7140
7246
  return null;
7141
7247
  }
7248
+ if (this.hitTestRotateHandle(world, ctx)) {
7249
+ ctx.setCursor?.("grab");
7250
+ return null;
7251
+ }
7142
7252
  const resizeHit = this.hitTestResizeHandle(world, ctx);
7143
7253
  if (resizeHit) {
7144
7254
  ctx.setCursor?.(HANDLE_CURSORS[resizeHit.handle]);
@@ -7157,6 +7267,11 @@ var SelectTool = class {
7157
7267
  if (this.mode.type !== "resizing") return;
7158
7268
  const el = ctx.store.getById(this.mode.elementId);
7159
7269
  if (!el || !("size" in el) || el.locked) return;
7270
+ const angle = el.rotation ?? 0;
7271
+ if (angle !== 0) {
7272
+ this.handleRotatedResize(world, el, angle, ctx, shiftKey);
7273
+ return;
7274
+ }
7160
7275
  const { handle } = this.mode;
7161
7276
  const dx = world.x - this.lastWorld.x;
7162
7277
  const dy = world.y - this.lastWorld.y;
@@ -7214,6 +7329,78 @@ var SelectTool = class {
7214
7329
  this.updateArrowsBoundTo([this.mode.elementId], ctx);
7215
7330
  ctx.requestRender();
7216
7331
  }
7332
+ anchorOffset(handle, w, h) {
7333
+ switch (handle) {
7334
+ case "se":
7335
+ return { x: -w / 2, y: -h / 2 };
7336
+ case "sw":
7337
+ return { x: w / 2, y: -h / 2 };
7338
+ case "ne":
7339
+ return { x: -w / 2, y: h / 2 };
7340
+ case "nw":
7341
+ return { x: w / 2, y: h / 2 };
7342
+ default:
7343
+ return { x: 0, y: 0 };
7344
+ }
7345
+ }
7346
+ handleRotatedResize(world, el, angle, ctx, shiftKey) {
7347
+ if (this.mode.type !== "resizing") return;
7348
+ const { handle } = this.mode;
7349
+ const wdx = world.x - this.lastWorld.x;
7350
+ const wdy = world.y - this.lastWorld.y;
7351
+ this.lastWorld = world;
7352
+ const cosN = Math.cos(-angle);
7353
+ const sinN = Math.sin(-angle);
7354
+ const ldx = wdx * cosN - wdy * sinN;
7355
+ const ldy = wdx * sinN + wdy * cosN;
7356
+ let w = el.size.w;
7357
+ let h = el.size.h;
7358
+ switch (handle) {
7359
+ case "se":
7360
+ w += ldx;
7361
+ h += ldy;
7362
+ break;
7363
+ case "sw":
7364
+ w -= ldx;
7365
+ h += ldy;
7366
+ break;
7367
+ case "ne":
7368
+ w += ldx;
7369
+ h -= ldy;
7370
+ break;
7371
+ case "nw":
7372
+ w -= ldx;
7373
+ h -= ldy;
7374
+ break;
7375
+ }
7376
+ if (shiftKey && this.resizeAspectRatio > 0) {
7377
+ const absDw = Math.abs(w - el.size.w);
7378
+ const absDh = Math.abs(h - el.size.h);
7379
+ if (absDw >= absDh) h = w / this.resizeAspectRatio;
7380
+ else w = h * this.resizeAspectRatio;
7381
+ }
7382
+ w = Math.max(w, MIN_ELEMENT_SIZE);
7383
+ h = Math.max(h, MIN_ELEMENT_SIZE);
7384
+ const oldCenter = { x: el.position.x + el.size.w / 2, y: el.position.y + el.size.h / 2 };
7385
+ const oldAnchorLocal = this.anchorOffset(handle, el.size.w, el.size.h);
7386
+ const anchorWorld = rotatePoint(
7387
+ { x: oldCenter.x + oldAnchorLocal.x, y: oldCenter.y + oldAnchorLocal.y },
7388
+ oldCenter,
7389
+ angle
7390
+ );
7391
+ const newAnchorLocal = this.anchorOffset(handle, w, h);
7392
+ const cos = Math.cos(angle);
7393
+ const sin = Math.sin(angle);
7394
+ const rotatedAnchor = {
7395
+ x: newAnchorLocal.x * cos - newAnchorLocal.y * sin,
7396
+ y: newAnchorLocal.x * sin + newAnchorLocal.y * cos
7397
+ };
7398
+ const newCenter = { x: anchorWorld.x - rotatedAnchor.x, y: anchorWorld.y - rotatedAnchor.y };
7399
+ const position = { x: newCenter.x - w / 2, y: newCenter.y - h / 2 };
7400
+ ctx.store.update(this.mode.elementId, { position, size: { w, h } });
7401
+ this.updateArrowsBoundTo([this.mode.elementId], ctx);
7402
+ ctx.requestRender();
7403
+ }
7217
7404
  hitTestResizeHandle(world, ctx) {
7218
7405
  if (this._selectedIds.length === 0) return null;
7219
7406
  const zoom = ctx.camera.zoom;
@@ -7222,10 +7409,9 @@ var SelectTool = class {
7222
7409
  const el = ctx.store.getById(id);
7223
7410
  if (!el || !("size" in el)) continue;
7224
7411
  if (el.type === "shape" && el.shape === "line") continue;
7225
- const bounds = getElementBounds(el);
7226
- if (!bounds) continue;
7227
- const corners = this.getHandlePositions(bounds);
7228
- for (const [handle, pos] of corners) {
7412
+ const layout = this.getOverlayLayout(el, zoom);
7413
+ if (!layout) continue;
7414
+ for (const [handle, pos] of layout.corners) {
7229
7415
  if (Math.abs(world.x - pos.x) <= handleHalf && Math.abs(world.y - pos.y) <= handleHalf) {
7230
7416
  return { elementId: id, handle };
7231
7417
  }
@@ -7233,6 +7419,19 @@ var SelectTool = class {
7233
7419
  }
7234
7420
  return null;
7235
7421
  }
7422
+ hitTestRotateHandle(world, ctx) {
7423
+ if (this._selectedIds.length !== 1) return null;
7424
+ const id = this._selectedIds[0];
7425
+ if (!id) return null;
7426
+ const el = ctx.store.getById(id);
7427
+ if (!el || el.locked || !ROTATABLE_TYPES.has(el.type)) return null;
7428
+ const layout = this.getOverlayLayout(el, ctx.camera.zoom);
7429
+ if (!layout) return null;
7430
+ const r = (HANDLE_SIZE / 2 + HANDLE_HIT_PADDING2) / ctx.camera.zoom;
7431
+ const dx = world.x - layout.rotateHandle.x;
7432
+ const dy = world.y - layout.rotateHandle.y;
7433
+ return dx * dx + dy * dy <= r * r ? { elementId: id } : null;
7434
+ }
7236
7435
  hitTestLineHandles(world, ctx) {
7237
7436
  if (this._selectedIds.length === 0) return null;
7238
7437
  const zoom = ctx.camera.zoom;
@@ -7255,6 +7454,30 @@ var SelectTool = class {
7255
7454
  ["se", { x: bounds.x + bounds.w, y: bounds.y + bounds.h }]
7256
7455
  ];
7257
7456
  }
7457
+ getOverlayLayout(el, zoom) {
7458
+ const bounds = getElementBounds(el);
7459
+ if (!bounds) return null;
7460
+ const angle = el.rotation ?? 0;
7461
+ const pad = SELECTION_PAD / zoom;
7462
+ const center2 = { x: bounds.x + bounds.w / 2, y: bounds.y + bounds.h / 2 };
7463
+ const raw = [
7464
+ ["nw", { x: bounds.x - pad, y: bounds.y - pad }],
7465
+ ["ne", { x: bounds.x + bounds.w + pad, y: bounds.y - pad }],
7466
+ ["sw", { x: bounds.x - pad, y: bounds.y + bounds.h + pad }],
7467
+ ["se", { x: bounds.x + bounds.w + pad, y: bounds.y + bounds.h + pad }]
7468
+ ];
7469
+ const corners = raw.map(
7470
+ ([h, p]) => [h, rotatePoint(p, center2, angle)]
7471
+ );
7472
+ const topMid = { x: center2.x, y: bounds.y - pad - ROTATE_HANDLE_OFFSET / zoom };
7473
+ const rotateHandle = rotatePoint(topMid, center2, angle);
7474
+ return { center: center2, corners, rotateHandle, angle };
7475
+ }
7476
+ topMidpoint(layout) {
7477
+ const nw = layout.corners.find(([h]) => h === "nw")?.[1] ?? { x: 0, y: 0 };
7478
+ const ne = layout.corners.find(([h]) => h === "ne")?.[1] ?? { x: 0, y: 0 };
7479
+ return { x: (nw.x + ne.x) / 2, y: (nw.y + ne.y) / 2 };
7480
+ }
7258
7481
  renderMarquee(canvasCtx) {
7259
7482
  if (this.mode.type !== "marquee") return;
7260
7483
  const rect = this.getMarqueeRect();
@@ -7299,12 +7522,31 @@ var SelectTool = class {
7299
7522
  }
7300
7523
  const bounds = getElementBounds(el);
7301
7524
  if (!bounds) continue;
7525
+ const layout = this.getOverlayLayout(el, zoom);
7526
+ if (!layout) continue;
7302
7527
  const pad = SELECTION_PAD / zoom;
7303
- canvasCtx.strokeRect(bounds.x - pad, bounds.y - pad, bounds.w + pad * 2, bounds.h + pad * 2);
7528
+ if (layout.angle === 0) {
7529
+ canvasCtx.strokeRect(
7530
+ bounds.x - pad,
7531
+ bounds.y - pad,
7532
+ bounds.w + pad * 2,
7533
+ bounds.h + pad * 2
7534
+ );
7535
+ } else {
7536
+ const ordered = ["nw", "ne", "se", "sw"].map((h) => layout.corners.find(([c]) => c === h)?.[1]).filter((p) => !!p);
7537
+ const [p0, ...others] = ordered;
7538
+ if (p0) {
7539
+ canvasCtx.beginPath();
7540
+ canvasCtx.moveTo(p0.x, p0.y);
7541
+ for (const p of others) canvasCtx.lineTo(p.x, p.y);
7542
+ canvasCtx.closePath();
7543
+ canvasCtx.stroke();
7544
+ }
7545
+ }
7304
7546
  if ("size" in el) {
7305
7547
  canvasCtx.setLineDash([]);
7306
7548
  canvasCtx.fillStyle = "#ffffff";
7307
- const corners = this.getHandlePositions(bounds);
7549
+ const corners = layout.angle === 0 ? this.getHandlePositions(bounds) : layout.corners;
7308
7550
  for (const [, pos] of corners) {
7309
7551
  canvasCtx.fillRect(
7310
7552
  pos.x - handleWorldSize / 2,
@@ -7339,6 +7581,21 @@ var SelectTool = class {
7339
7581
  );
7340
7582
  canvasCtx.setLineDash([4 / zoom, 4 / zoom]);
7341
7583
  }
7584
+ if (this._selectedIds.length === 1 && ROTATABLE_TYPES.has(el.type)) {
7585
+ const stemStart = this.topMidpoint(layout);
7586
+ const stemEnd = layout.rotateHandle;
7587
+ canvasCtx.beginPath();
7588
+ canvasCtx.moveTo(stemStart.x, stemStart.y);
7589
+ canvasCtx.lineTo(stemEnd.x, stemEnd.y);
7590
+ canvasCtx.stroke();
7591
+ canvasCtx.setLineDash([]);
7592
+ canvasCtx.fillStyle = "#ffffff";
7593
+ canvasCtx.beginPath();
7594
+ canvasCtx.arc(stemEnd.x, stemEnd.y, handleWorldSize / 2, 0, Math.PI * 2);
7595
+ canvasCtx.fill();
7596
+ canvasCtx.stroke();
7597
+ canvasCtx.setLineDash([4 / zoom, 4 / zoom]);
7598
+ }
7342
7599
  }
7343
7600
  canvasCtx.restore();
7344
7601
  }
@@ -7418,7 +7675,7 @@ var SelectTool = class {
7418
7675
  if (ctx.isLayerLocked && ctx.isLayerLocked(el.layerId)) continue;
7419
7676
  if (el.type === "grid") continue;
7420
7677
  const bounds = getElementBounds(el);
7421
- if (bounds && this.rectsOverlap(marquee, bounds)) {
7678
+ if (bounds && this.rectsOverlap(marquee, rotatedAABB(bounds, el.rotation ?? 0))) {
7422
7679
  ids.push(el.id);
7423
7680
  }
7424
7681
  }
@@ -7440,6 +7697,13 @@ var SelectTool = class {
7440
7697
  }
7441
7698
  isInsideBounds(point, el) {
7442
7699
  if (el.type === "grid") return false;
7700
+ const angle = el.rotation ?? 0;
7701
+ if (angle !== 0) {
7702
+ const b = getElementBounds(el);
7703
+ if (b) {
7704
+ point = rotatePoint(point, { x: b.x + b.w / 2, y: b.y + b.h / 2 }, -angle);
7705
+ }
7706
+ }
7443
7707
  if (el.type === "shape" && el.shape === "line") {
7444
7708
  const [a, b] = lineEndpoints(el);
7445
7709
  const threshold = Math.max(el.strokeWidth / 2, 6);
@@ -8206,20 +8470,20 @@ var TemplateTool = class {
8206
8470
  const snapUnit = Math.sqrt(3) * cellSize;
8207
8471
  const radiusCells = radius / snapUnit;
8208
8472
  const angle = this.computeAngle();
8209
- const center = this.origin;
8473
+ const center2 = this.origin;
8210
8474
  let hexCells;
8211
8475
  switch (this.templateShape) {
8212
8476
  case "circle":
8213
- hexCells = getHexCellsInRadius(center, radiusCells, cellSize, orientation);
8477
+ hexCells = getHexCellsInRadius(center2, radiusCells, cellSize, orientation);
8214
8478
  break;
8215
8479
  case "cone":
8216
- hexCells = getHexCellsInCone(center, angle, radiusCells, cellSize, orientation);
8480
+ hexCells = getHexCellsInCone(center2, angle, radiusCells, cellSize, orientation);
8217
8481
  break;
8218
8482
  case "line":
8219
- hexCells = getHexCellsInLine(center, angle, radiusCells, cellSize, orientation);
8483
+ hexCells = getHexCellsInLine(center2, angle, radiusCells, cellSize, orientation);
8220
8484
  break;
8221
8485
  case "square":
8222
- hexCells = getHexCellsInSquare(center, radiusCells, cellSize, orientation);
8486
+ hexCells = getHexCellsInSquare(center2, radiusCells, cellSize, orientation);
8223
8487
  break;
8224
8488
  }
8225
8489
  ctx.save();
@@ -8240,7 +8504,7 @@ var TemplateTool = class {
8240
8504
  if (this.templateShape === "cone" || this.templateShape === "line" || this.templateShape === "circle" || this.templateShape === "square") {
8241
8505
  ctx.globalAlpha = 0.5;
8242
8506
  ctx.beginPath();
8243
- drawHexPath(ctx, center.x, center.y, cellSize, orientation);
8507
+ drawHexPath(ctx, center2.x, center2.y, cellSize, orientation);
8244
8508
  ctx.fillStyle = this.strokeColor;
8245
8509
  ctx.fill();
8246
8510
  ctx.strokeStyle = this.strokeColor;
@@ -8256,8 +8520,8 @@ var TemplateTool = class {
8256
8520
  ctx.font = `bold ${fontSize}px system-ui, sans-serif`;
8257
8521
  ctx.textAlign = "center";
8258
8522
  ctx.textBaseline = "bottom";
8259
- const textX = center.x;
8260
- const textY = center.y - 4;
8523
+ const textX = center2.x;
8524
+ const textY = center2.y - 4;
8261
8525
  const metrics = ctx.measureText(label);
8262
8526
  const padX = 4;
8263
8527
  const padY = 2;
@@ -8307,7 +8571,7 @@ var TemplateTool = class {
8307
8571
  };
8308
8572
 
8309
8573
  // src/index.ts
8310
- var VERSION = "0.34.0";
8574
+ var VERSION = "0.35.0";
8311
8575
  export {
8312
8576
  ArrowTool,
8313
8577
  AutoSave,