@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/README.md +648 -636
- package/dist/index.cjs +330 -66
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10 -1
- package/dist/index.d.ts +10 -1
- package/dist/index.js +330 -66
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
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,
|
|
1538
|
+
this.camera.zoomAt(newZoom, center2);
|
|
1509
1539
|
}
|
|
1510
|
-
const dx =
|
|
1511
|
-
const dy =
|
|
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 =
|
|
1515
|
-
this.lastPointer = { ...
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
2322
|
-
updates.from =
|
|
2323
|
-
updates.position =
|
|
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(
|
|
2651
|
+
function getHexCellsInRadius(center2, radiusCells, cellSize, orientation) {
|
|
2599
2652
|
const n = Math.round(radiusCells);
|
|
2600
|
-
const off = pixelToOffset(
|
|
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(
|
|
2660
|
+
function getHexCellsInCone(center2, angle, radiusCells, cellSize, orientation) {
|
|
2608
2661
|
const n = Math.round(radiusCells);
|
|
2609
|
-
const off = pixelToOffset(
|
|
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(
|
|
2696
|
+
function getHexCellsInLine(center2, angle, radiusCells, cellSize, orientation) {
|
|
2644
2697
|
const n = Math.round(radiusCells);
|
|
2645
|
-
const off = pixelToOffset(
|
|
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(
|
|
2734
|
+
function getHexCellsInSquare(center2, radiusCells, cellSize, orientation) {
|
|
2682
2735
|
const n = Math.round(radiusCells);
|
|
2683
|
-
const off = pixelToOffset(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2771
|
-
|
|
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
|
|
3131
|
+
const center2 = template.position;
|
|
3070
3132
|
let cells;
|
|
3071
3133
|
switch (template.templateShape) {
|
|
3072
3134
|
case "circle":
|
|
3073
|
-
cells = getHexCellsInRadius(
|
|
3135
|
+
cells = getHexCellsInRadius(center2, radiusCells, cellSize, orientation);
|
|
3074
3136
|
break;
|
|
3075
3137
|
case "cone":
|
|
3076
|
-
cells = getHexCellsInCone(
|
|
3138
|
+
cells = getHexCellsInCone(center2, template.angle, radiusCells, cellSize, orientation);
|
|
3077
3139
|
break;
|
|
3078
3140
|
case "line":
|
|
3079
|
-
cells = getHexCellsInLine(
|
|
3141
|
+
cells = getHexCellsInLine(center2, template.angle, radiusCells, cellSize, orientation);
|
|
3080
3142
|
break;
|
|
3081
3143
|
case "square":
|
|
3082
|
-
cells = getHexCellsInSquare(
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
5941
|
-
const sorted = [...bounded].sort((p, q) =>
|
|
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 =
|
|
5946
|
-
const cN =
|
|
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 -
|
|
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
|
|
6755
|
+
const center2 = getElementCenter(target);
|
|
6678
6756
|
ctx.store.update(elementId, {
|
|
6679
|
-
from:
|
|
6680
|
-
position:
|
|
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
|
|
6774
|
+
const center2 = getElementCenter(target);
|
|
6697
6775
|
ctx.store.update(elementId, {
|
|
6698
|
-
to:
|
|
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
|
|
7226
|
-
if (!
|
|
7227
|
-
const
|
|
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
|
-
|
|
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
|
|
8473
|
+
const center2 = this.origin;
|
|
8210
8474
|
let hexCells;
|
|
8211
8475
|
switch (this.templateShape) {
|
|
8212
8476
|
case "circle":
|
|
8213
|
-
hexCells = getHexCellsInRadius(
|
|
8477
|
+
hexCells = getHexCellsInRadius(center2, radiusCells, cellSize, orientation);
|
|
8214
8478
|
break;
|
|
8215
8479
|
case "cone":
|
|
8216
|
-
hexCells = getHexCellsInCone(
|
|
8480
|
+
hexCells = getHexCellsInCone(center2, angle, radiusCells, cellSize, orientation);
|
|
8217
8481
|
break;
|
|
8218
8482
|
case "line":
|
|
8219
|
-
hexCells = getHexCellsInLine(
|
|
8483
|
+
hexCells = getHexCellsInLine(center2, angle, radiusCells, cellSize, orientation);
|
|
8220
8484
|
break;
|
|
8221
8485
|
case "square":
|
|
8222
|
-
hexCells = getHexCellsInSquare(
|
|
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,
|
|
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 =
|
|
8260
|
-
const textY =
|
|
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.
|
|
8574
|
+
var VERSION = "0.35.0";
|
|
8311
8575
|
export {
|
|
8312
8576
|
ArrowTool,
|
|
8313
8577
|
AutoSave,
|