@fieldnotes/core 0.33.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 -620
- package/dist/index.cjs +413 -73
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -1
- package/dist/index.d.ts +14 -1
- package/dist/index.js +413 -73
- 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) {
|
|
@@ -1021,6 +1051,14 @@ var KeyboardActions = class {
|
|
|
1021
1051
|
if (this.deps.isToolActive()) return;
|
|
1022
1052
|
this.deps.fitToContent?.();
|
|
1023
1053
|
}
|
|
1054
|
+
group() {
|
|
1055
|
+
if (this.deps.isToolActive()) return;
|
|
1056
|
+
this.deps.group?.();
|
|
1057
|
+
}
|
|
1058
|
+
ungroup() {
|
|
1059
|
+
if (this.deps.isToolActive()) return;
|
|
1060
|
+
this.deps.ungroup?.();
|
|
1061
|
+
}
|
|
1024
1062
|
zOrder(operation) {
|
|
1025
1063
|
if (this.deps.isToolActive()) return;
|
|
1026
1064
|
this.flushPendingNudge();
|
|
@@ -1054,6 +1092,10 @@ var KeyboardActions = class {
|
|
|
1054
1092
|
for (const el of source) {
|
|
1055
1093
|
idMap.set(el.id, createId(el.type));
|
|
1056
1094
|
}
|
|
1095
|
+
const groupIdMap = /* @__PURE__ */ new Map();
|
|
1096
|
+
for (const el of source) {
|
|
1097
|
+
if (el.groupId && !groupIdMap.has(el.groupId)) groupIdMap.set(el.groupId, createId("group"));
|
|
1098
|
+
}
|
|
1057
1099
|
const newIds = [];
|
|
1058
1100
|
const recorder = this.deps.getHistoryRecorder();
|
|
1059
1101
|
recorder?.begin();
|
|
@@ -1062,6 +1104,7 @@ var KeyboardActions = class {
|
|
|
1062
1104
|
const newId = idMap.get(el.id);
|
|
1063
1105
|
if (!newId) continue;
|
|
1064
1106
|
clone.id = newId;
|
|
1107
|
+
if (clone.groupId) clone.groupId = groupIdMap.get(clone.groupId) ?? clone.groupId;
|
|
1065
1108
|
clone.position = { x: clone.position.x + offset.x, y: clone.position.y + offset.y };
|
|
1066
1109
|
if (clone.type === "arrow") {
|
|
1067
1110
|
const arrow = clone;
|
|
@@ -1115,6 +1158,8 @@ var DEFAULT_BINDINGS = [
|
|
|
1115
1158
|
["zoom-in", ["mod+="]],
|
|
1116
1159
|
["zoom-out", ["mod+-"]],
|
|
1117
1160
|
["zoom-reset", ["mod+0"]],
|
|
1161
|
+
["group", ["mod+g"]],
|
|
1162
|
+
["ungroup", ["mod+shift+g"]],
|
|
1118
1163
|
["nudge-left", ["arrowleft"]],
|
|
1119
1164
|
["nudge-right", ["arrowright"]],
|
|
1120
1165
|
["nudge-up", ["arrowup"]],
|
|
@@ -1274,6 +1319,8 @@ var InputHandler = class {
|
|
|
1274
1319
|
getHistoryStack: () => this.historyStack,
|
|
1275
1320
|
isToolActive: () => this.isToolActive,
|
|
1276
1321
|
fitToContent: options.fitToContent,
|
|
1322
|
+
group: options.group,
|
|
1323
|
+
ungroup: options.ungroup,
|
|
1277
1324
|
getLastPointerWorld: () => this.lastPointerWorld()
|
|
1278
1325
|
});
|
|
1279
1326
|
this.shortcutMap = new ShortcutMap(options.shortcuts?.bindings);
|
|
@@ -1513,6 +1560,14 @@ var InputHandler = class {
|
|
|
1513
1560
|
e.preventDefault();
|
|
1514
1561
|
this.actions.zoomToFit();
|
|
1515
1562
|
return;
|
|
1563
|
+
case "group":
|
|
1564
|
+
e.preventDefault();
|
|
1565
|
+
this.actions.group();
|
|
1566
|
+
return;
|
|
1567
|
+
case "ungroup":
|
|
1568
|
+
e.preventDefault();
|
|
1569
|
+
this.actions.ungroup();
|
|
1570
|
+
return;
|
|
1516
1571
|
case "zoom-in":
|
|
1517
1572
|
e.preventDefault();
|
|
1518
1573
|
this.zoomByFactor(ZOOM_STEP);
|
|
@@ -1557,18 +1612,18 @@ var InputHandler = class {
|
|
|
1557
1612
|
handlePinchMove() {
|
|
1558
1613
|
const [a, b] = this.getPinchPoints();
|
|
1559
1614
|
const dist = this.distance(a, b);
|
|
1560
|
-
const
|
|
1615
|
+
const center2 = this.midpoint(a, b);
|
|
1561
1616
|
if (this.lastPinchDistance > 0) {
|
|
1562
1617
|
const scale = dist / this.lastPinchDistance;
|
|
1563
1618
|
const newZoom = this.camera.zoom * scale;
|
|
1564
|
-
this.camera.zoomAt(newZoom,
|
|
1619
|
+
this.camera.zoomAt(newZoom, center2);
|
|
1565
1620
|
}
|
|
1566
|
-
const dx =
|
|
1567
|
-
const dy =
|
|
1621
|
+
const dx = center2.x - this.lastPointer.x;
|
|
1622
|
+
const dy = center2.y - this.lastPointer.y;
|
|
1568
1623
|
this.camera.pan(dx, dy);
|
|
1569
1624
|
this.lastPinchDistance = dist;
|
|
1570
|
-
this.lastPinchCenter =
|
|
1571
|
-
this.lastPointer = { ...
|
|
1625
|
+
this.lastPinchCenter = center2;
|
|
1626
|
+
this.lastPointer = { ...center2 };
|
|
1572
1627
|
}
|
|
1573
1628
|
getPinchPoints() {
|
|
1574
1629
|
const pts = [...this.activePointers.values()];
|
|
@@ -2110,11 +2165,19 @@ var ElementStore = class {
|
|
|
2110
2165
|
(el) => el.type === type
|
|
2111
2166
|
);
|
|
2112
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
|
+
}
|
|
2113
2176
|
add(element) {
|
|
2114
2177
|
this.sortedCache = null;
|
|
2115
2178
|
this._versions.set(element.id, 0);
|
|
2116
2179
|
this.elements.set(element.id, element);
|
|
2117
|
-
const bounds =
|
|
2180
|
+
const bounds = this.indexBounds(element);
|
|
2118
2181
|
if (bounds) this.spatialIndex.insert(element.id, bounds);
|
|
2119
2182
|
this.bus.emit("add", element);
|
|
2120
2183
|
}
|
|
@@ -2136,7 +2199,7 @@ var ElementStore = class {
|
|
|
2136
2199
|
updated.text = sanitizeNoteHtml(updated.text);
|
|
2137
2200
|
}
|
|
2138
2201
|
this.elements.set(id, updated);
|
|
2139
|
-
const newBounds =
|
|
2202
|
+
const newBounds = this.indexBounds(updated);
|
|
2140
2203
|
if (newBounds) {
|
|
2141
2204
|
this.spatialIndex.update(id, newBounds);
|
|
2142
2205
|
}
|
|
@@ -2169,7 +2232,7 @@ var ElementStore = class {
|
|
|
2169
2232
|
for (const el of elements) {
|
|
2170
2233
|
this.elements.set(el.id, el);
|
|
2171
2234
|
this._versions.set(el.id, 0);
|
|
2172
|
-
const bounds =
|
|
2235
|
+
const bounds = this.indexBounds(el);
|
|
2173
2236
|
if (bounds) this.spatialIndex.insert(el.id, bounds);
|
|
2174
2237
|
if (el.type === "stroke") {
|
|
2175
2238
|
computeStrokeSegments(el);
|
|
@@ -2374,9 +2437,9 @@ function updateBoundArrow(arrow, store) {
|
|
|
2374
2437
|
if (arrow.fromBinding) {
|
|
2375
2438
|
const el = store.getById(arrow.fromBinding.elementId);
|
|
2376
2439
|
if (el) {
|
|
2377
|
-
const
|
|
2378
|
-
updates.from =
|
|
2379
|
-
updates.position =
|
|
2440
|
+
const center2 = getElementCenter(el);
|
|
2441
|
+
updates.from = center2;
|
|
2442
|
+
updates.position = center2;
|
|
2380
2443
|
}
|
|
2381
2444
|
}
|
|
2382
2445
|
if (arrow.toBinding) {
|
|
@@ -2388,6 +2451,21 @@ function updateBoundArrow(arrow, store) {
|
|
|
2388
2451
|
return Object.keys(updates).length > 0 ? updates : null;
|
|
2389
2452
|
}
|
|
2390
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
|
+
|
|
2391
2469
|
// src/elements/grid-renderer.ts
|
|
2392
2470
|
function getSquareGridLines(bounds, cellSize) {
|
|
2393
2471
|
if (cellSize <= 0) return { verticals: [], horizontals: [] };
|
|
@@ -2651,18 +2729,18 @@ function getHexDistance(a, b, cellSize, orientation) {
|
|
|
2651
2729
|
const ds = -dq - dr;
|
|
2652
2730
|
return Math.max(Math.abs(dq), Math.abs(dr), Math.abs(ds));
|
|
2653
2731
|
}
|
|
2654
|
-
function getHexCellsInRadius(
|
|
2732
|
+
function getHexCellsInRadius(center2, radiusCells, cellSize, orientation) {
|
|
2655
2733
|
const n = Math.round(radiusCells);
|
|
2656
|
-
const off = pixelToOffset(
|
|
2734
|
+
const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
|
|
2657
2735
|
const cube = offsetToCube(off.col, off.row, orientation);
|
|
2658
2736
|
if (n <= 0) {
|
|
2659
2737
|
return [offsetToPixel(off.col, off.row, cellSize, orientation)];
|
|
2660
2738
|
}
|
|
2661
2739
|
return enumerateHexRing(cube.q, cube.r, n, orientation, cellSize);
|
|
2662
2740
|
}
|
|
2663
|
-
function getHexCellsInCone(
|
|
2741
|
+
function getHexCellsInCone(center2, angle, radiusCells, cellSize, orientation) {
|
|
2664
2742
|
const n = Math.round(radiusCells);
|
|
2665
|
-
const off = pixelToOffset(
|
|
2743
|
+
const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
|
|
2666
2744
|
const cube = offsetToCube(off.col, off.row, orientation);
|
|
2667
2745
|
const centerPixel = offsetToPixel(off.col, off.row, cellSize, orientation);
|
|
2668
2746
|
if (n <= 0) return [centerPixel];
|
|
@@ -2696,9 +2774,9 @@ function getHexCellsInCone(center, angle, radiusCells, cellSize, orientation) {
|
|
|
2696
2774
|
}
|
|
2697
2775
|
return cells;
|
|
2698
2776
|
}
|
|
2699
|
-
function getHexCellsInLine(
|
|
2777
|
+
function getHexCellsInLine(center2, angle, radiusCells, cellSize, orientation) {
|
|
2700
2778
|
const n = Math.round(radiusCells);
|
|
2701
|
-
const off = pixelToOffset(
|
|
2779
|
+
const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
|
|
2702
2780
|
const cube = offsetToCube(off.col, off.row, orientation);
|
|
2703
2781
|
const centerPixel = offsetToPixel(off.col, off.row, cellSize, orientation);
|
|
2704
2782
|
if (n <= 0) return [centerPixel];
|
|
@@ -2734,9 +2812,9 @@ function getHexCellsInLine(center, angle, radiusCells, cellSize, orientation) {
|
|
|
2734
2812
|
}
|
|
2735
2813
|
return cells;
|
|
2736
2814
|
}
|
|
2737
|
-
function getHexCellsInSquare(
|
|
2815
|
+
function getHexCellsInSquare(center2, radiusCells, cellSize, orientation) {
|
|
2738
2816
|
const n = Math.round(radiusCells);
|
|
2739
|
-
const off = pixelToOffset(
|
|
2817
|
+
const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
|
|
2740
2818
|
const cube = offsetToCube(off.col, off.row, orientation);
|
|
2741
2819
|
const centerPixel = offsetToPixel(off.col, off.row, cellSize, orientation);
|
|
2742
2820
|
if (n <= 0) return [centerPixel];
|
|
@@ -2814,18 +2892,27 @@ var ElementRenderer = class {
|
|
|
2814
2892
|
}
|
|
2815
2893
|
renderCanvasElement(ctx, element) {
|
|
2816
2894
|
switch (element.type) {
|
|
2817
|
-
case "stroke":
|
|
2818
|
-
|
|
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));
|
|
2819
2899
|
break;
|
|
2900
|
+
}
|
|
2820
2901
|
case "arrow":
|
|
2821
2902
|
this.renderArrow(ctx, element);
|
|
2822
2903
|
break;
|
|
2823
|
-
case "shape":
|
|
2824
|
-
|
|
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));
|
|
2825
2908
|
break;
|
|
2826
|
-
|
|
2827
|
-
|
|
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));
|
|
2828
2914
|
break;
|
|
2915
|
+
}
|
|
2829
2916
|
case "grid":
|
|
2830
2917
|
this.renderGrid(ctx, element);
|
|
2831
2918
|
break;
|
|
@@ -3122,20 +3209,20 @@ var ElementRenderer = class {
|
|
|
3122
3209
|
renderHexTemplate(ctx, template, cellSize, orientation) {
|
|
3123
3210
|
const snapUnit = Math.sqrt(3) * cellSize;
|
|
3124
3211
|
const radiusCells = template.radius / snapUnit;
|
|
3125
|
-
const
|
|
3212
|
+
const center2 = template.position;
|
|
3126
3213
|
let cells;
|
|
3127
3214
|
switch (template.templateShape) {
|
|
3128
3215
|
case "circle":
|
|
3129
|
-
cells = getHexCellsInRadius(
|
|
3216
|
+
cells = getHexCellsInRadius(center2, radiusCells, cellSize, orientation);
|
|
3130
3217
|
break;
|
|
3131
3218
|
case "cone":
|
|
3132
|
-
cells = getHexCellsInCone(
|
|
3219
|
+
cells = getHexCellsInCone(center2, template.angle, radiusCells, cellSize, orientation);
|
|
3133
3220
|
break;
|
|
3134
3221
|
case "line":
|
|
3135
|
-
cells = getHexCellsInLine(
|
|
3222
|
+
cells = getHexCellsInLine(center2, template.angle, radiusCells, cellSize, orientation);
|
|
3136
3223
|
break;
|
|
3137
3224
|
case "square":
|
|
3138
|
-
cells = getHexCellsInSquare(
|
|
3225
|
+
cells = getHexCellsInSquare(center2, radiusCells, cellSize, orientation);
|
|
3139
3226
|
break;
|
|
3140
3227
|
}
|
|
3141
3228
|
ctx.save();
|
|
@@ -3156,7 +3243,7 @@ var ElementRenderer = class {
|
|
|
3156
3243
|
{
|
|
3157
3244
|
ctx.globalAlpha = Math.min(template.opacity + 0.1, 1);
|
|
3158
3245
|
ctx.beginPath();
|
|
3159
|
-
drawHexPath(ctx,
|
|
3246
|
+
drawHexPath(ctx, center2.x, center2.y, cellSize, orientation);
|
|
3160
3247
|
ctx.fillStyle = template.strokeColor;
|
|
3161
3248
|
ctx.fill();
|
|
3162
3249
|
ctx.strokeStyle = template.strokeColor;
|
|
@@ -3165,7 +3252,7 @@ var ElementRenderer = class {
|
|
|
3165
3252
|
}
|
|
3166
3253
|
if (template.templateShape === "circle" && template.radiusFeet != null && template.radiusFeet > 0) {
|
|
3167
3254
|
const r = template.radius;
|
|
3168
|
-
this.renderRadiusMarker(ctx,
|
|
3255
|
+
this.renderRadiusMarker(ctx, center2.x, center2.y, r, template.radiusFeet);
|
|
3169
3256
|
}
|
|
3170
3257
|
ctx.restore();
|
|
3171
3258
|
}
|
|
@@ -4018,12 +4105,19 @@ var UpdateElementCommand = class {
|
|
|
4018
4105
|
this.current = current;
|
|
4019
4106
|
}
|
|
4020
4107
|
execute(store) {
|
|
4021
|
-
store.update(this.id,
|
|
4108
|
+
store.update(this.id, diffPatch(this.previous, this.current));
|
|
4022
4109
|
}
|
|
4023
4110
|
undo(store) {
|
|
4024
|
-
store.update(this.id,
|
|
4111
|
+
store.update(this.id, diffPatch(this.current, this.previous));
|
|
4025
4112
|
}
|
|
4026
4113
|
};
|
|
4114
|
+
function diffPatch(from, to) {
|
|
4115
|
+
const patch = { ...to };
|
|
4116
|
+
for (const key of Object.keys(from)) {
|
|
4117
|
+
if (!(key in to)) patch[key] = void 0;
|
|
4118
|
+
}
|
|
4119
|
+
return patch;
|
|
4120
|
+
}
|
|
4027
4121
|
var BatchCommand = class {
|
|
4028
4122
|
commands;
|
|
4029
4123
|
constructor(commands) {
|
|
@@ -4277,6 +4371,7 @@ function renderStyledRuns(ctx, runs, startX, startY, maxWidth) {
|
|
|
4277
4371
|
}
|
|
4278
4372
|
|
|
4279
4373
|
// src/canvas/export-image.ts
|
|
4374
|
+
var center = (b) => ({ x: b.x + b.w / 2, y: b.y + b.h / 2 });
|
|
4280
4375
|
function getStrokeBounds(el) {
|
|
4281
4376
|
if (el.type !== "stroke") return null;
|
|
4282
4377
|
if (el.points.length === 0) return null;
|
|
@@ -4302,8 +4397,10 @@ function getStrokeBounds(el) {
|
|
|
4302
4397
|
}
|
|
4303
4398
|
function getElementRect(el) {
|
|
4304
4399
|
switch (el.type) {
|
|
4305
|
-
case "stroke":
|
|
4306
|
-
|
|
4400
|
+
case "stroke": {
|
|
4401
|
+
const r = getStrokeBounds(el);
|
|
4402
|
+
return r ? rotatedAABB(r, el.rotation ?? 0) : r;
|
|
4403
|
+
}
|
|
4307
4404
|
case "arrow": {
|
|
4308
4405
|
const b = getArrowBounds(el.from, el.to, el.bend);
|
|
4309
4406
|
const pad = el.width / 2 + 14;
|
|
@@ -4322,7 +4419,10 @@ function getElementRect(el) {
|
|
|
4322
4419
|
case "text":
|
|
4323
4420
|
case "shape":
|
|
4324
4421
|
if ("size" in el) {
|
|
4325
|
-
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
|
+
);
|
|
4326
4426
|
}
|
|
4327
4427
|
return null;
|
|
4328
4428
|
default:
|
|
@@ -4460,11 +4560,13 @@ async function exportImage(store, options = {}, layerManager) {
|
|
|
4460
4560
|
continue;
|
|
4461
4561
|
}
|
|
4462
4562
|
if (el.type === "note") {
|
|
4463
|
-
|
|
4563
|
+
const b = getElementBounds(el);
|
|
4564
|
+
withRotation(ctx, el, b ? center(b) : el.position, () => renderNoteOnCanvas(ctx, el));
|
|
4464
4565
|
continue;
|
|
4465
4566
|
}
|
|
4466
4567
|
if (el.type === "text") {
|
|
4467
|
-
|
|
4568
|
+
const b = getElementBounds(el);
|
|
4569
|
+
withRotation(ctx, el, b ? center(b) : el.position, () => renderTextOnCanvas(ctx, el));
|
|
4468
4570
|
continue;
|
|
4469
4571
|
}
|
|
4470
4572
|
if (el.type === "html") {
|
|
@@ -4473,7 +4575,13 @@ async function exportImage(store, options = {}, layerManager) {
|
|
|
4473
4575
|
if (el.type === "image") {
|
|
4474
4576
|
const img = imageCache.get(el.id);
|
|
4475
4577
|
if (img) {
|
|
4476
|
-
|
|
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
|
+
);
|
|
4477
4585
|
}
|
|
4478
4586
|
continue;
|
|
4479
4587
|
}
|
|
@@ -4809,7 +4917,9 @@ var DomNodeManager = class {
|
|
|
4809
4917
|
top: `${element.position.y}px`,
|
|
4810
4918
|
width: size ? `${size.w}px` : "auto",
|
|
4811
4919
|
height: size ? `${size.h}px` : "auto",
|
|
4812
|
-
zIndex: String(zIndex)
|
|
4920
|
+
zIndex: String(zIndex),
|
|
4921
|
+
transform: element.rotation ? `rotate(${element.rotation}rad)` : "",
|
|
4922
|
+
transformOrigin: "50% 50%"
|
|
4813
4923
|
});
|
|
4814
4924
|
this.renderDomContent(node, element);
|
|
4815
4925
|
}
|
|
@@ -5586,6 +5696,8 @@ var Viewport = class {
|
|
|
5586
5696
|
historyRecorder: this.historyRecorder,
|
|
5587
5697
|
historyStack: this.history,
|
|
5588
5698
|
fitToContent: () => this.fitToContent(),
|
|
5699
|
+
group: () => this.groupSelection(),
|
|
5700
|
+
ungroup: () => this.ungroupSelection(),
|
|
5589
5701
|
shortcuts: options.shortcuts
|
|
5590
5702
|
});
|
|
5591
5703
|
this.domNodeManager = new DomNodeManager({
|
|
@@ -5923,6 +6035,26 @@ var Viewport = class {
|
|
|
5923
6035
|
}
|
|
5924
6036
|
this.historyRecorder.commit();
|
|
5925
6037
|
}
|
|
6038
|
+
groupSelection() {
|
|
6039
|
+
const ids = this.getSelectedIds();
|
|
6040
|
+
if (ids.length < 2) return;
|
|
6041
|
+
const groupId = createId("group");
|
|
6042
|
+
this.historyRecorder.begin();
|
|
6043
|
+
for (const id of ids) {
|
|
6044
|
+
if (this.store.getById(id)) this.store.update(id, { groupId });
|
|
6045
|
+
}
|
|
6046
|
+
this.historyRecorder.commit();
|
|
6047
|
+
}
|
|
6048
|
+
ungroupSelection() {
|
|
6049
|
+
const ids = this.getSelectedIds();
|
|
6050
|
+
if (ids.length === 0) return;
|
|
6051
|
+
this.historyRecorder.begin();
|
|
6052
|
+
for (const id of ids) {
|
|
6053
|
+
const el = this.store.getById(id);
|
|
6054
|
+
if (el && el.groupId !== void 0) this.store.update(id, { groupId: void 0 });
|
|
6055
|
+
}
|
|
6056
|
+
this.historyRecorder.commit();
|
|
6057
|
+
}
|
|
5926
6058
|
alignSelection(edge) {
|
|
5927
6059
|
const bounded = this.boundedSelection();
|
|
5928
6060
|
if (bounded.length < 2) return;
|
|
@@ -5964,13 +6096,13 @@ var Viewport = class {
|
|
|
5964
6096
|
distributeSelection(axis) {
|
|
5965
6097
|
const bounded = this.boundedSelection();
|
|
5966
6098
|
if (bounded.length < 3) return;
|
|
5967
|
-
const
|
|
5968
|
-
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));
|
|
5969
6101
|
const first = sorted[0];
|
|
5970
6102
|
const last = sorted[sorted.length - 1];
|
|
5971
6103
|
if (!first || !last) return;
|
|
5972
|
-
const c0 =
|
|
5973
|
-
const cN =
|
|
6104
|
+
const c0 = center2(first.bounds);
|
|
6105
|
+
const cN = center2(last.bounds);
|
|
5974
6106
|
const n = sorted.length;
|
|
5975
6107
|
this.historyRecorder.begin();
|
|
5976
6108
|
const moved = [];
|
|
@@ -5978,7 +6110,7 @@ var Viewport = class {
|
|
|
5978
6110
|
const item = sorted[i];
|
|
5979
6111
|
if (!item || !this.isMovable(item.el)) continue;
|
|
5980
6112
|
const target = c0 + i * (cN - c0) / (n - 1);
|
|
5981
|
-
const delta = target -
|
|
6113
|
+
const delta = target - center2(item.bounds);
|
|
5982
6114
|
if (delta === 0) continue;
|
|
5983
6115
|
const [dx, dy] = axis === "horizontal" ? [delta, 0] : [0, delta];
|
|
5984
6116
|
this.store.update(item.id, translateElementPatch(item.el, dx, dy));
|
|
@@ -6632,6 +6764,26 @@ var EraserTool = class {
|
|
|
6632
6764
|
}
|
|
6633
6765
|
};
|
|
6634
6766
|
|
|
6767
|
+
// src/elements/group.ts
|
|
6768
|
+
function expandToGroups(ids, elements) {
|
|
6769
|
+
const byId = new Map(elements.map((e) => [e.id, e]));
|
|
6770
|
+
const groupIds = /* @__PURE__ */ new Set();
|
|
6771
|
+
for (const id of ids) {
|
|
6772
|
+
const g = byId.get(id)?.groupId;
|
|
6773
|
+
if (g) groupIds.add(g);
|
|
6774
|
+
}
|
|
6775
|
+
if (groupIds.size === 0) return ids;
|
|
6776
|
+
const idSet = new Set(ids);
|
|
6777
|
+
const result = [...ids];
|
|
6778
|
+
for (const el of elements) {
|
|
6779
|
+
if (el.groupId && groupIds.has(el.groupId) && !idSet.has(el.id)) {
|
|
6780
|
+
result.push(el.id);
|
|
6781
|
+
idSet.add(el.id);
|
|
6782
|
+
}
|
|
6783
|
+
}
|
|
6784
|
+
return result;
|
|
6785
|
+
}
|
|
6786
|
+
|
|
6635
6787
|
// src/tools/arrow-handles.ts
|
|
6636
6788
|
var BIND_THRESHOLD = 20;
|
|
6637
6789
|
var HANDLE_RADIUS = 5;
|
|
@@ -6681,10 +6833,10 @@ function applyArrowHandleDrag(handle, elementId, world, ctx) {
|
|
|
6681
6833
|
const excludeId = el.toBinding?.elementId;
|
|
6682
6834
|
const target = findBindTarget(world, ctx.store, threshold, excludeId, layerFilter);
|
|
6683
6835
|
if (target) {
|
|
6684
|
-
const
|
|
6836
|
+
const center2 = getElementCenter(target);
|
|
6685
6837
|
ctx.store.update(elementId, {
|
|
6686
|
-
from:
|
|
6687
|
-
position:
|
|
6838
|
+
from: center2,
|
|
6839
|
+
position: center2,
|
|
6688
6840
|
fromBinding: { elementId: target.id }
|
|
6689
6841
|
});
|
|
6690
6842
|
} else {
|
|
@@ -6700,9 +6852,9 @@ function applyArrowHandleDrag(handle, elementId, world, ctx) {
|
|
|
6700
6852
|
const excludeId = el.fromBinding?.elementId;
|
|
6701
6853
|
const target = findBindTarget(world, ctx.store, threshold, excludeId, layerFilter);
|
|
6702
6854
|
if (target) {
|
|
6703
|
-
const
|
|
6855
|
+
const center2 = getElementCenter(target);
|
|
6704
6856
|
ctx.store.update(elementId, {
|
|
6705
|
-
to:
|
|
6857
|
+
to: center2,
|
|
6706
6858
|
toBinding: { elementId: target.id }
|
|
6707
6859
|
});
|
|
6708
6860
|
} else {
|
|
@@ -6791,6 +6943,9 @@ var SNAP_PX = 6;
|
|
|
6791
6943
|
var HANDLE_HIT_PADDING2 = 4;
|
|
6792
6944
|
var SELECTION_PAD = 4;
|
|
6793
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"]);
|
|
6794
6949
|
var HANDLE_CURSORS = {
|
|
6795
6950
|
nw: "nwse-resize",
|
|
6796
6951
|
se: "nwse-resize",
|
|
@@ -6879,6 +7034,22 @@ var SelectTool = class {
|
|
|
6879
7034
|
ctx.requestRender();
|
|
6880
7035
|
return;
|
|
6881
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
|
+
}
|
|
6882
7053
|
const resizeHit = this.hitTestResizeHandle(world, ctx);
|
|
6883
7054
|
if (resizeHit) {
|
|
6884
7055
|
const el = ctx.store.getById(resizeHit.elementId);
|
|
@@ -6897,18 +7068,20 @@ var SelectTool = class {
|
|
|
6897
7068
|
this.hasDragged = false;
|
|
6898
7069
|
const hit = this.hitTest(world, ctx);
|
|
6899
7070
|
if (hit) {
|
|
7071
|
+
const all = ctx.store.getAll();
|
|
6900
7072
|
const alreadySelected = this._selectedIds.includes(hit.id);
|
|
6901
7073
|
if (state.shiftKey) {
|
|
6902
7074
|
if (alreadySelected) {
|
|
6903
|
-
|
|
7075
|
+
const grp = new Set(expandToGroups([hit.id], all));
|
|
7076
|
+
this.setSelectedIds(this._selectedIds.filter((id) => !grp.has(id)));
|
|
6904
7077
|
this.mode = { type: "idle" };
|
|
6905
7078
|
} else {
|
|
6906
|
-
this.setSelectedIds([...this._selectedIds, hit.id]);
|
|
7079
|
+
this.setSelectedIds(expandToGroups([...this._selectedIds, hit.id], all));
|
|
6907
7080
|
this.mode = hit.locked ? { type: "idle" } : { type: "dragging" };
|
|
6908
7081
|
}
|
|
6909
7082
|
} else {
|
|
6910
7083
|
if (!alreadySelected) {
|
|
6911
|
-
this.setSelectedIds([hit.id]);
|
|
7084
|
+
this.setSelectedIds(expandToGroups([hit.id], all));
|
|
6912
7085
|
} else if (this._selectedIds.length > 1) {
|
|
6913
7086
|
this.pendingSingleSelectId = hit.id;
|
|
6914
7087
|
}
|
|
@@ -6942,6 +7115,15 @@ var SelectTool = class {
|
|
|
6942
7115
|
this.handleTemplateResize(world, ctx);
|
|
6943
7116
|
return;
|
|
6944
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
|
+
}
|
|
6945
7127
|
if (this.mode.type === "resizing") {
|
|
6946
7128
|
ctx.setCursor?.(HANDLE_CURSORS[this.mode.handle]);
|
|
6947
7129
|
this.handleResize(world, ctx, state.shiftKey);
|
|
@@ -7022,12 +7204,12 @@ var SelectTool = class {
|
|
|
7022
7204
|
if (this.mode.type === "marquee") {
|
|
7023
7205
|
const rect = this.getMarqueeRect();
|
|
7024
7206
|
if (rect) {
|
|
7025
|
-
this.setSelectedIds(this.findElementsInRect(rect, ctx));
|
|
7207
|
+
this.setSelectedIds(expandToGroups(this.findElementsInRect(rect, ctx), ctx.store.getAll()));
|
|
7026
7208
|
}
|
|
7027
7209
|
ctx.requestRender();
|
|
7028
7210
|
}
|
|
7029
7211
|
if (!this.hasDragged && this.pendingSingleSelectId !== null) {
|
|
7030
|
-
this.setSelectedIds([this.pendingSingleSelectId]);
|
|
7212
|
+
this.setSelectedIds(expandToGroups([this.pendingSingleSelectId], ctx.store.getAll()));
|
|
7031
7213
|
}
|
|
7032
7214
|
this.pendingSingleSelectId = null;
|
|
7033
7215
|
this.hasDragged = false;
|
|
@@ -7144,6 +7326,10 @@ var SelectTool = class {
|
|
|
7144
7326
|
ctx.setCursor?.("nwse-resize");
|
|
7145
7327
|
return null;
|
|
7146
7328
|
}
|
|
7329
|
+
if (this.hitTestRotateHandle(world, ctx)) {
|
|
7330
|
+
ctx.setCursor?.("grab");
|
|
7331
|
+
return null;
|
|
7332
|
+
}
|
|
7147
7333
|
const resizeHit = this.hitTestResizeHandle(world, ctx);
|
|
7148
7334
|
if (resizeHit) {
|
|
7149
7335
|
ctx.setCursor?.(HANDLE_CURSORS[resizeHit.handle]);
|
|
@@ -7162,6 +7348,11 @@ var SelectTool = class {
|
|
|
7162
7348
|
if (this.mode.type !== "resizing") return;
|
|
7163
7349
|
const el = ctx.store.getById(this.mode.elementId);
|
|
7164
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
|
+
}
|
|
7165
7356
|
const { handle } = this.mode;
|
|
7166
7357
|
const dx = world.x - this.lastWorld.x;
|
|
7167
7358
|
const dy = world.y - this.lastWorld.y;
|
|
@@ -7219,6 +7410,78 @@ var SelectTool = class {
|
|
|
7219
7410
|
this.updateArrowsBoundTo([this.mode.elementId], ctx);
|
|
7220
7411
|
ctx.requestRender();
|
|
7221
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
|
+
}
|
|
7222
7485
|
hitTestResizeHandle(world, ctx) {
|
|
7223
7486
|
if (this._selectedIds.length === 0) return null;
|
|
7224
7487
|
const zoom = ctx.camera.zoom;
|
|
@@ -7227,10 +7490,9 @@ var SelectTool = class {
|
|
|
7227
7490
|
const el = ctx.store.getById(id);
|
|
7228
7491
|
if (!el || !("size" in el)) continue;
|
|
7229
7492
|
if (el.type === "shape" && el.shape === "line") continue;
|
|
7230
|
-
const
|
|
7231
|
-
if (!
|
|
7232
|
-
const
|
|
7233
|
-
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) {
|
|
7234
7496
|
if (Math.abs(world.x - pos.x) <= handleHalf && Math.abs(world.y - pos.y) <= handleHalf) {
|
|
7235
7497
|
return { elementId: id, handle };
|
|
7236
7498
|
}
|
|
@@ -7238,6 +7500,19 @@ var SelectTool = class {
|
|
|
7238
7500
|
}
|
|
7239
7501
|
return null;
|
|
7240
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
|
+
}
|
|
7241
7516
|
hitTestLineHandles(world, ctx) {
|
|
7242
7517
|
if (this._selectedIds.length === 0) return null;
|
|
7243
7518
|
const zoom = ctx.camera.zoom;
|
|
@@ -7260,6 +7535,30 @@ var SelectTool = class {
|
|
|
7260
7535
|
["se", { x: bounds.x + bounds.w, y: bounds.y + bounds.h }]
|
|
7261
7536
|
];
|
|
7262
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
|
+
}
|
|
7263
7562
|
renderMarquee(canvasCtx) {
|
|
7264
7563
|
if (this.mode.type !== "marquee") return;
|
|
7265
7564
|
const rect = this.getMarqueeRect();
|
|
@@ -7304,12 +7603,31 @@ var SelectTool = class {
|
|
|
7304
7603
|
}
|
|
7305
7604
|
const bounds = getElementBounds(el);
|
|
7306
7605
|
if (!bounds) continue;
|
|
7606
|
+
const layout = this.getOverlayLayout(el, zoom);
|
|
7607
|
+
if (!layout) continue;
|
|
7307
7608
|
const pad = SELECTION_PAD / zoom;
|
|
7308
|
-
|
|
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
|
+
}
|
|
7309
7627
|
if ("size" in el) {
|
|
7310
7628
|
canvasCtx.setLineDash([]);
|
|
7311
7629
|
canvasCtx.fillStyle = "#ffffff";
|
|
7312
|
-
const corners = this.getHandlePositions(bounds);
|
|
7630
|
+
const corners = layout.angle === 0 ? this.getHandlePositions(bounds) : layout.corners;
|
|
7313
7631
|
for (const [, pos] of corners) {
|
|
7314
7632
|
canvasCtx.fillRect(
|
|
7315
7633
|
pos.x - handleWorldSize / 2,
|
|
@@ -7344,6 +7662,21 @@ var SelectTool = class {
|
|
|
7344
7662
|
);
|
|
7345
7663
|
canvasCtx.setLineDash([4 / zoom, 4 / zoom]);
|
|
7346
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
|
+
}
|
|
7347
7680
|
}
|
|
7348
7681
|
canvasCtx.restore();
|
|
7349
7682
|
}
|
|
@@ -7423,7 +7756,7 @@ var SelectTool = class {
|
|
|
7423
7756
|
if (ctx.isLayerLocked && ctx.isLayerLocked(el.layerId)) continue;
|
|
7424
7757
|
if (el.type === "grid") continue;
|
|
7425
7758
|
const bounds = getElementBounds(el);
|
|
7426
|
-
if (bounds && this.rectsOverlap(marquee, bounds)) {
|
|
7759
|
+
if (bounds && this.rectsOverlap(marquee, rotatedAABB(bounds, el.rotation ?? 0))) {
|
|
7427
7760
|
ids.push(el.id);
|
|
7428
7761
|
}
|
|
7429
7762
|
}
|
|
@@ -7445,6 +7778,13 @@ var SelectTool = class {
|
|
|
7445
7778
|
}
|
|
7446
7779
|
isInsideBounds(point, el) {
|
|
7447
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
|
+
}
|
|
7448
7788
|
if (el.type === "shape" && el.shape === "line") {
|
|
7449
7789
|
const [a, b] = lineEndpoints(el);
|
|
7450
7790
|
const threshold = Math.max(el.strokeWidth / 2, 6);
|
|
@@ -8211,20 +8551,20 @@ var TemplateTool = class {
|
|
|
8211
8551
|
const snapUnit = Math.sqrt(3) * cellSize;
|
|
8212
8552
|
const radiusCells = radius / snapUnit;
|
|
8213
8553
|
const angle = this.computeAngle();
|
|
8214
|
-
const
|
|
8554
|
+
const center2 = this.origin;
|
|
8215
8555
|
let hexCells;
|
|
8216
8556
|
switch (this.templateShape) {
|
|
8217
8557
|
case "circle":
|
|
8218
|
-
hexCells = getHexCellsInRadius(
|
|
8558
|
+
hexCells = getHexCellsInRadius(center2, radiusCells, cellSize, orientation);
|
|
8219
8559
|
break;
|
|
8220
8560
|
case "cone":
|
|
8221
|
-
hexCells = getHexCellsInCone(
|
|
8561
|
+
hexCells = getHexCellsInCone(center2, angle, radiusCells, cellSize, orientation);
|
|
8222
8562
|
break;
|
|
8223
8563
|
case "line":
|
|
8224
|
-
hexCells = getHexCellsInLine(
|
|
8564
|
+
hexCells = getHexCellsInLine(center2, angle, radiusCells, cellSize, orientation);
|
|
8225
8565
|
break;
|
|
8226
8566
|
case "square":
|
|
8227
|
-
hexCells = getHexCellsInSquare(
|
|
8567
|
+
hexCells = getHexCellsInSquare(center2, radiusCells, cellSize, orientation);
|
|
8228
8568
|
break;
|
|
8229
8569
|
}
|
|
8230
8570
|
ctx.save();
|
|
@@ -8245,7 +8585,7 @@ var TemplateTool = class {
|
|
|
8245
8585
|
if (this.templateShape === "cone" || this.templateShape === "line" || this.templateShape === "circle" || this.templateShape === "square") {
|
|
8246
8586
|
ctx.globalAlpha = 0.5;
|
|
8247
8587
|
ctx.beginPath();
|
|
8248
|
-
drawHexPath(ctx,
|
|
8588
|
+
drawHexPath(ctx, center2.x, center2.y, cellSize, orientation);
|
|
8249
8589
|
ctx.fillStyle = this.strokeColor;
|
|
8250
8590
|
ctx.fill();
|
|
8251
8591
|
ctx.strokeStyle = this.strokeColor;
|
|
@@ -8261,8 +8601,8 @@ var TemplateTool = class {
|
|
|
8261
8601
|
ctx.font = `bold ${fontSize}px system-ui, sans-serif`;
|
|
8262
8602
|
ctx.textAlign = "center";
|
|
8263
8603
|
ctx.textBaseline = "bottom";
|
|
8264
|
-
const textX =
|
|
8265
|
-
const textY =
|
|
8604
|
+
const textX = center2.x;
|
|
8605
|
+
const textY = center2.y - 4;
|
|
8266
8606
|
const metrics = ctx.measureText(label);
|
|
8267
8607
|
const padX = 4;
|
|
8268
8608
|
const padY = 2;
|
|
@@ -8312,7 +8652,7 @@ var TemplateTool = class {
|
|
|
8312
8652
|
};
|
|
8313
8653
|
|
|
8314
8654
|
// src/index.ts
|
|
8315
|
-
var VERSION = "0.
|
|
8655
|
+
var VERSION = "0.35.0";
|
|
8316
8656
|
// Annotate the CommonJS export names for ESM import in node:
|
|
8317
8657
|
0 && (module.exports = {
|
|
8318
8658
|
ArrowTool,
|