@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.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
|
|
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,
|
|
1619
|
+
this.camera.zoomAt(newZoom, center2);
|
|
1590
1620
|
}
|
|
1591
|
-
const dx =
|
|
1592
|
-
const dy =
|
|
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 =
|
|
1596
|
-
this.lastPointer = { ...
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
2403
|
-
updates.from =
|
|
2404
|
-
updates.position =
|
|
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(
|
|
2732
|
+
function getHexCellsInRadius(center2, radiusCells, cellSize, orientation) {
|
|
2680
2733
|
const n = Math.round(radiusCells);
|
|
2681
|
-
const off = pixelToOffset(
|
|
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(
|
|
2741
|
+
function getHexCellsInCone(center2, angle, radiusCells, cellSize, orientation) {
|
|
2689
2742
|
const n = Math.round(radiusCells);
|
|
2690
|
-
const off = pixelToOffset(
|
|
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(
|
|
2777
|
+
function getHexCellsInLine(center2, angle, radiusCells, cellSize, orientation) {
|
|
2725
2778
|
const n = Math.round(radiusCells);
|
|
2726
|
-
const off = pixelToOffset(
|
|
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(
|
|
2815
|
+
function getHexCellsInSquare(center2, radiusCells, cellSize, orientation) {
|
|
2763
2816
|
const n = Math.round(radiusCells);
|
|
2764
|
-
const off = pixelToOffset(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2852
|
-
|
|
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
|
|
3212
|
+
const center2 = template.position;
|
|
3151
3213
|
let cells;
|
|
3152
3214
|
switch (template.templateShape) {
|
|
3153
3215
|
case "circle":
|
|
3154
|
-
cells = getHexCellsInRadius(
|
|
3216
|
+
cells = getHexCellsInRadius(center2, radiusCells, cellSize, orientation);
|
|
3155
3217
|
break;
|
|
3156
3218
|
case "cone":
|
|
3157
|
-
cells = getHexCellsInCone(
|
|
3219
|
+
cells = getHexCellsInCone(center2, template.angle, radiusCells, cellSize, orientation);
|
|
3158
3220
|
break;
|
|
3159
3221
|
case "line":
|
|
3160
|
-
cells = getHexCellsInLine(
|
|
3222
|
+
cells = getHexCellsInLine(center2, template.angle, radiusCells, cellSize, orientation);
|
|
3161
3223
|
break;
|
|
3162
3224
|
case "square":
|
|
3163
|
-
cells = getHexCellsInSquare(
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
6022
|
-
const sorted = [...bounded].sort((p, q) =>
|
|
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 =
|
|
6027
|
-
const cN =
|
|
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 -
|
|
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
|
|
6836
|
+
const center2 = getElementCenter(target);
|
|
6759
6837
|
ctx.store.update(elementId, {
|
|
6760
|
-
from:
|
|
6761
|
-
position:
|
|
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
|
|
6855
|
+
const center2 = getElementCenter(target);
|
|
6778
6856
|
ctx.store.update(elementId, {
|
|
6779
|
-
to:
|
|
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
|
|
7307
|
-
if (!
|
|
7308
|
-
const
|
|
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
|
-
|
|
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
|
|
8554
|
+
const center2 = this.origin;
|
|
8291
8555
|
let hexCells;
|
|
8292
8556
|
switch (this.templateShape) {
|
|
8293
8557
|
case "circle":
|
|
8294
|
-
hexCells = getHexCellsInRadius(
|
|
8558
|
+
hexCells = getHexCellsInRadius(center2, radiusCells, cellSize, orientation);
|
|
8295
8559
|
break;
|
|
8296
8560
|
case "cone":
|
|
8297
|
-
hexCells = getHexCellsInCone(
|
|
8561
|
+
hexCells = getHexCellsInCone(center2, angle, radiusCells, cellSize, orientation);
|
|
8298
8562
|
break;
|
|
8299
8563
|
case "line":
|
|
8300
|
-
hexCells = getHexCellsInLine(
|
|
8564
|
+
hexCells = getHexCellsInLine(center2, angle, radiusCells, cellSize, orientation);
|
|
8301
8565
|
break;
|
|
8302
8566
|
case "square":
|
|
8303
|
-
hexCells = getHexCellsInSquare(
|
|
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,
|
|
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 =
|
|
8341
|
-
const textY =
|
|
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.
|
|
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,
|