@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.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) {
|
|
@@ -940,6 +970,14 @@ var KeyboardActions = class {
|
|
|
940
970
|
if (this.deps.isToolActive()) return;
|
|
941
971
|
this.deps.fitToContent?.();
|
|
942
972
|
}
|
|
973
|
+
group() {
|
|
974
|
+
if (this.deps.isToolActive()) return;
|
|
975
|
+
this.deps.group?.();
|
|
976
|
+
}
|
|
977
|
+
ungroup() {
|
|
978
|
+
if (this.deps.isToolActive()) return;
|
|
979
|
+
this.deps.ungroup?.();
|
|
980
|
+
}
|
|
943
981
|
zOrder(operation) {
|
|
944
982
|
if (this.deps.isToolActive()) return;
|
|
945
983
|
this.flushPendingNudge();
|
|
@@ -973,6 +1011,10 @@ var KeyboardActions = class {
|
|
|
973
1011
|
for (const el of source) {
|
|
974
1012
|
idMap.set(el.id, createId(el.type));
|
|
975
1013
|
}
|
|
1014
|
+
const groupIdMap = /* @__PURE__ */ new Map();
|
|
1015
|
+
for (const el of source) {
|
|
1016
|
+
if (el.groupId && !groupIdMap.has(el.groupId)) groupIdMap.set(el.groupId, createId("group"));
|
|
1017
|
+
}
|
|
976
1018
|
const newIds = [];
|
|
977
1019
|
const recorder = this.deps.getHistoryRecorder();
|
|
978
1020
|
recorder?.begin();
|
|
@@ -981,6 +1023,7 @@ var KeyboardActions = class {
|
|
|
981
1023
|
const newId = idMap.get(el.id);
|
|
982
1024
|
if (!newId) continue;
|
|
983
1025
|
clone.id = newId;
|
|
1026
|
+
if (clone.groupId) clone.groupId = groupIdMap.get(clone.groupId) ?? clone.groupId;
|
|
984
1027
|
clone.position = { x: clone.position.x + offset.x, y: clone.position.y + offset.y };
|
|
985
1028
|
if (clone.type === "arrow") {
|
|
986
1029
|
const arrow = clone;
|
|
@@ -1034,6 +1077,8 @@ var DEFAULT_BINDINGS = [
|
|
|
1034
1077
|
["zoom-in", ["mod+="]],
|
|
1035
1078
|
["zoom-out", ["mod+-"]],
|
|
1036
1079
|
["zoom-reset", ["mod+0"]],
|
|
1080
|
+
["group", ["mod+g"]],
|
|
1081
|
+
["ungroup", ["mod+shift+g"]],
|
|
1037
1082
|
["nudge-left", ["arrowleft"]],
|
|
1038
1083
|
["nudge-right", ["arrowright"]],
|
|
1039
1084
|
["nudge-up", ["arrowup"]],
|
|
@@ -1193,6 +1238,8 @@ var InputHandler = class {
|
|
|
1193
1238
|
getHistoryStack: () => this.historyStack,
|
|
1194
1239
|
isToolActive: () => this.isToolActive,
|
|
1195
1240
|
fitToContent: options.fitToContent,
|
|
1241
|
+
group: options.group,
|
|
1242
|
+
ungroup: options.ungroup,
|
|
1196
1243
|
getLastPointerWorld: () => this.lastPointerWorld()
|
|
1197
1244
|
});
|
|
1198
1245
|
this.shortcutMap = new ShortcutMap(options.shortcuts?.bindings);
|
|
@@ -1432,6 +1479,14 @@ var InputHandler = class {
|
|
|
1432
1479
|
e.preventDefault();
|
|
1433
1480
|
this.actions.zoomToFit();
|
|
1434
1481
|
return;
|
|
1482
|
+
case "group":
|
|
1483
|
+
e.preventDefault();
|
|
1484
|
+
this.actions.group();
|
|
1485
|
+
return;
|
|
1486
|
+
case "ungroup":
|
|
1487
|
+
e.preventDefault();
|
|
1488
|
+
this.actions.ungroup();
|
|
1489
|
+
return;
|
|
1435
1490
|
case "zoom-in":
|
|
1436
1491
|
e.preventDefault();
|
|
1437
1492
|
this.zoomByFactor(ZOOM_STEP);
|
|
@@ -1476,18 +1531,18 @@ var InputHandler = class {
|
|
|
1476
1531
|
handlePinchMove() {
|
|
1477
1532
|
const [a, b] = this.getPinchPoints();
|
|
1478
1533
|
const dist = this.distance(a, b);
|
|
1479
|
-
const
|
|
1534
|
+
const center2 = this.midpoint(a, b);
|
|
1480
1535
|
if (this.lastPinchDistance > 0) {
|
|
1481
1536
|
const scale = dist / this.lastPinchDistance;
|
|
1482
1537
|
const newZoom = this.camera.zoom * scale;
|
|
1483
|
-
this.camera.zoomAt(newZoom,
|
|
1538
|
+
this.camera.zoomAt(newZoom, center2);
|
|
1484
1539
|
}
|
|
1485
|
-
const dx =
|
|
1486
|
-
const dy =
|
|
1540
|
+
const dx = center2.x - this.lastPointer.x;
|
|
1541
|
+
const dy = center2.y - this.lastPointer.y;
|
|
1487
1542
|
this.camera.pan(dx, dy);
|
|
1488
1543
|
this.lastPinchDistance = dist;
|
|
1489
|
-
this.lastPinchCenter =
|
|
1490
|
-
this.lastPointer = { ...
|
|
1544
|
+
this.lastPinchCenter = center2;
|
|
1545
|
+
this.lastPointer = { ...center2 };
|
|
1491
1546
|
}
|
|
1492
1547
|
getPinchPoints() {
|
|
1493
1548
|
const pts = [...this.activePointers.values()];
|
|
@@ -2029,11 +2084,19 @@ var ElementStore = class {
|
|
|
2029
2084
|
(el) => el.type === type
|
|
2030
2085
|
);
|
|
2031
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
|
+
}
|
|
2032
2095
|
add(element) {
|
|
2033
2096
|
this.sortedCache = null;
|
|
2034
2097
|
this._versions.set(element.id, 0);
|
|
2035
2098
|
this.elements.set(element.id, element);
|
|
2036
|
-
const bounds =
|
|
2099
|
+
const bounds = this.indexBounds(element);
|
|
2037
2100
|
if (bounds) this.spatialIndex.insert(element.id, bounds);
|
|
2038
2101
|
this.bus.emit("add", element);
|
|
2039
2102
|
}
|
|
@@ -2055,7 +2118,7 @@ var ElementStore = class {
|
|
|
2055
2118
|
updated.text = sanitizeNoteHtml(updated.text);
|
|
2056
2119
|
}
|
|
2057
2120
|
this.elements.set(id, updated);
|
|
2058
|
-
const newBounds =
|
|
2121
|
+
const newBounds = this.indexBounds(updated);
|
|
2059
2122
|
if (newBounds) {
|
|
2060
2123
|
this.spatialIndex.update(id, newBounds);
|
|
2061
2124
|
}
|
|
@@ -2088,7 +2151,7 @@ var ElementStore = class {
|
|
|
2088
2151
|
for (const el of elements) {
|
|
2089
2152
|
this.elements.set(el.id, el);
|
|
2090
2153
|
this._versions.set(el.id, 0);
|
|
2091
|
-
const bounds =
|
|
2154
|
+
const bounds = this.indexBounds(el);
|
|
2092
2155
|
if (bounds) this.spatialIndex.insert(el.id, bounds);
|
|
2093
2156
|
if (el.type === "stroke") {
|
|
2094
2157
|
computeStrokeSegments(el);
|
|
@@ -2293,9 +2356,9 @@ function updateBoundArrow(arrow, store) {
|
|
|
2293
2356
|
if (arrow.fromBinding) {
|
|
2294
2357
|
const el = store.getById(arrow.fromBinding.elementId);
|
|
2295
2358
|
if (el) {
|
|
2296
|
-
const
|
|
2297
|
-
updates.from =
|
|
2298
|
-
updates.position =
|
|
2359
|
+
const center2 = getElementCenter(el);
|
|
2360
|
+
updates.from = center2;
|
|
2361
|
+
updates.position = center2;
|
|
2299
2362
|
}
|
|
2300
2363
|
}
|
|
2301
2364
|
if (arrow.toBinding) {
|
|
@@ -2307,6 +2370,21 @@ function updateBoundArrow(arrow, store) {
|
|
|
2307
2370
|
return Object.keys(updates).length > 0 ? updates : null;
|
|
2308
2371
|
}
|
|
2309
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
|
+
|
|
2310
2388
|
// src/elements/grid-renderer.ts
|
|
2311
2389
|
function getSquareGridLines(bounds, cellSize) {
|
|
2312
2390
|
if (cellSize <= 0) return { verticals: [], horizontals: [] };
|
|
@@ -2570,18 +2648,18 @@ function getHexDistance(a, b, cellSize, orientation) {
|
|
|
2570
2648
|
const ds = -dq - dr;
|
|
2571
2649
|
return Math.max(Math.abs(dq), Math.abs(dr), Math.abs(ds));
|
|
2572
2650
|
}
|
|
2573
|
-
function getHexCellsInRadius(
|
|
2651
|
+
function getHexCellsInRadius(center2, radiusCells, cellSize, orientation) {
|
|
2574
2652
|
const n = Math.round(radiusCells);
|
|
2575
|
-
const off = pixelToOffset(
|
|
2653
|
+
const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
|
|
2576
2654
|
const cube = offsetToCube(off.col, off.row, orientation);
|
|
2577
2655
|
if (n <= 0) {
|
|
2578
2656
|
return [offsetToPixel(off.col, off.row, cellSize, orientation)];
|
|
2579
2657
|
}
|
|
2580
2658
|
return enumerateHexRing(cube.q, cube.r, n, orientation, cellSize);
|
|
2581
2659
|
}
|
|
2582
|
-
function getHexCellsInCone(
|
|
2660
|
+
function getHexCellsInCone(center2, angle, radiusCells, cellSize, orientation) {
|
|
2583
2661
|
const n = Math.round(radiusCells);
|
|
2584
|
-
const off = pixelToOffset(
|
|
2662
|
+
const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
|
|
2585
2663
|
const cube = offsetToCube(off.col, off.row, orientation);
|
|
2586
2664
|
const centerPixel = offsetToPixel(off.col, off.row, cellSize, orientation);
|
|
2587
2665
|
if (n <= 0) return [centerPixel];
|
|
@@ -2615,9 +2693,9 @@ function getHexCellsInCone(center, angle, radiusCells, cellSize, orientation) {
|
|
|
2615
2693
|
}
|
|
2616
2694
|
return cells;
|
|
2617
2695
|
}
|
|
2618
|
-
function getHexCellsInLine(
|
|
2696
|
+
function getHexCellsInLine(center2, angle, radiusCells, cellSize, orientation) {
|
|
2619
2697
|
const n = Math.round(radiusCells);
|
|
2620
|
-
const off = pixelToOffset(
|
|
2698
|
+
const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
|
|
2621
2699
|
const cube = offsetToCube(off.col, off.row, orientation);
|
|
2622
2700
|
const centerPixel = offsetToPixel(off.col, off.row, cellSize, orientation);
|
|
2623
2701
|
if (n <= 0) return [centerPixel];
|
|
@@ -2653,9 +2731,9 @@ function getHexCellsInLine(center, angle, radiusCells, cellSize, orientation) {
|
|
|
2653
2731
|
}
|
|
2654
2732
|
return cells;
|
|
2655
2733
|
}
|
|
2656
|
-
function getHexCellsInSquare(
|
|
2734
|
+
function getHexCellsInSquare(center2, radiusCells, cellSize, orientation) {
|
|
2657
2735
|
const n = Math.round(radiusCells);
|
|
2658
|
-
const off = pixelToOffset(
|
|
2736
|
+
const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
|
|
2659
2737
|
const cube = offsetToCube(off.col, off.row, orientation);
|
|
2660
2738
|
const centerPixel = offsetToPixel(off.col, off.row, cellSize, orientation);
|
|
2661
2739
|
if (n <= 0) return [centerPixel];
|
|
@@ -2733,18 +2811,27 @@ var ElementRenderer = class {
|
|
|
2733
2811
|
}
|
|
2734
2812
|
renderCanvasElement(ctx, element) {
|
|
2735
2813
|
switch (element.type) {
|
|
2736
|
-
case "stroke":
|
|
2737
|
-
|
|
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));
|
|
2738
2818
|
break;
|
|
2819
|
+
}
|
|
2739
2820
|
case "arrow":
|
|
2740
2821
|
this.renderArrow(ctx, element);
|
|
2741
2822
|
break;
|
|
2742
|
-
case "shape":
|
|
2743
|
-
|
|
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));
|
|
2744
2827
|
break;
|
|
2745
|
-
|
|
2746
|
-
|
|
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));
|
|
2747
2833
|
break;
|
|
2834
|
+
}
|
|
2748
2835
|
case "grid":
|
|
2749
2836
|
this.renderGrid(ctx, element);
|
|
2750
2837
|
break;
|
|
@@ -3041,20 +3128,20 @@ var ElementRenderer = class {
|
|
|
3041
3128
|
renderHexTemplate(ctx, template, cellSize, orientation) {
|
|
3042
3129
|
const snapUnit = Math.sqrt(3) * cellSize;
|
|
3043
3130
|
const radiusCells = template.radius / snapUnit;
|
|
3044
|
-
const
|
|
3131
|
+
const center2 = template.position;
|
|
3045
3132
|
let cells;
|
|
3046
3133
|
switch (template.templateShape) {
|
|
3047
3134
|
case "circle":
|
|
3048
|
-
cells = getHexCellsInRadius(
|
|
3135
|
+
cells = getHexCellsInRadius(center2, radiusCells, cellSize, orientation);
|
|
3049
3136
|
break;
|
|
3050
3137
|
case "cone":
|
|
3051
|
-
cells = getHexCellsInCone(
|
|
3138
|
+
cells = getHexCellsInCone(center2, template.angle, radiusCells, cellSize, orientation);
|
|
3052
3139
|
break;
|
|
3053
3140
|
case "line":
|
|
3054
|
-
cells = getHexCellsInLine(
|
|
3141
|
+
cells = getHexCellsInLine(center2, template.angle, radiusCells, cellSize, orientation);
|
|
3055
3142
|
break;
|
|
3056
3143
|
case "square":
|
|
3057
|
-
cells = getHexCellsInSquare(
|
|
3144
|
+
cells = getHexCellsInSquare(center2, radiusCells, cellSize, orientation);
|
|
3058
3145
|
break;
|
|
3059
3146
|
}
|
|
3060
3147
|
ctx.save();
|
|
@@ -3075,7 +3162,7 @@ var ElementRenderer = class {
|
|
|
3075
3162
|
{
|
|
3076
3163
|
ctx.globalAlpha = Math.min(template.opacity + 0.1, 1);
|
|
3077
3164
|
ctx.beginPath();
|
|
3078
|
-
drawHexPath(ctx,
|
|
3165
|
+
drawHexPath(ctx, center2.x, center2.y, cellSize, orientation);
|
|
3079
3166
|
ctx.fillStyle = template.strokeColor;
|
|
3080
3167
|
ctx.fill();
|
|
3081
3168
|
ctx.strokeStyle = template.strokeColor;
|
|
@@ -3084,7 +3171,7 @@ var ElementRenderer = class {
|
|
|
3084
3171
|
}
|
|
3085
3172
|
if (template.templateShape === "circle" && template.radiusFeet != null && template.radiusFeet > 0) {
|
|
3086
3173
|
const r = template.radius;
|
|
3087
|
-
this.renderRadiusMarker(ctx,
|
|
3174
|
+
this.renderRadiusMarker(ctx, center2.x, center2.y, r, template.radiusFeet);
|
|
3088
3175
|
}
|
|
3089
3176
|
ctx.restore();
|
|
3090
3177
|
}
|
|
@@ -3937,12 +4024,19 @@ var UpdateElementCommand = class {
|
|
|
3937
4024
|
this.current = current;
|
|
3938
4025
|
}
|
|
3939
4026
|
execute(store) {
|
|
3940
|
-
store.update(this.id,
|
|
4027
|
+
store.update(this.id, diffPatch(this.previous, this.current));
|
|
3941
4028
|
}
|
|
3942
4029
|
undo(store) {
|
|
3943
|
-
store.update(this.id,
|
|
4030
|
+
store.update(this.id, diffPatch(this.current, this.previous));
|
|
3944
4031
|
}
|
|
3945
4032
|
};
|
|
4033
|
+
function diffPatch(from, to) {
|
|
4034
|
+
const patch = { ...to };
|
|
4035
|
+
for (const key of Object.keys(from)) {
|
|
4036
|
+
if (!(key in to)) patch[key] = void 0;
|
|
4037
|
+
}
|
|
4038
|
+
return patch;
|
|
4039
|
+
}
|
|
3946
4040
|
var BatchCommand = class {
|
|
3947
4041
|
commands;
|
|
3948
4042
|
constructor(commands) {
|
|
@@ -4196,6 +4290,7 @@ function renderStyledRuns(ctx, runs, startX, startY, maxWidth) {
|
|
|
4196
4290
|
}
|
|
4197
4291
|
|
|
4198
4292
|
// src/canvas/export-image.ts
|
|
4293
|
+
var center = (b) => ({ x: b.x + b.w / 2, y: b.y + b.h / 2 });
|
|
4199
4294
|
function getStrokeBounds(el) {
|
|
4200
4295
|
if (el.type !== "stroke") return null;
|
|
4201
4296
|
if (el.points.length === 0) return null;
|
|
@@ -4221,8 +4316,10 @@ function getStrokeBounds(el) {
|
|
|
4221
4316
|
}
|
|
4222
4317
|
function getElementRect(el) {
|
|
4223
4318
|
switch (el.type) {
|
|
4224
|
-
case "stroke":
|
|
4225
|
-
|
|
4319
|
+
case "stroke": {
|
|
4320
|
+
const r = getStrokeBounds(el);
|
|
4321
|
+
return r ? rotatedAABB(r, el.rotation ?? 0) : r;
|
|
4322
|
+
}
|
|
4226
4323
|
case "arrow": {
|
|
4227
4324
|
const b = getArrowBounds(el.from, el.to, el.bend);
|
|
4228
4325
|
const pad = el.width / 2 + 14;
|
|
@@ -4241,7 +4338,10 @@ function getElementRect(el) {
|
|
|
4241
4338
|
case "text":
|
|
4242
4339
|
case "shape":
|
|
4243
4340
|
if ("size" in el) {
|
|
4244
|
-
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
|
+
);
|
|
4245
4345
|
}
|
|
4246
4346
|
return null;
|
|
4247
4347
|
default:
|
|
@@ -4379,11 +4479,13 @@ async function exportImage(store, options = {}, layerManager) {
|
|
|
4379
4479
|
continue;
|
|
4380
4480
|
}
|
|
4381
4481
|
if (el.type === "note") {
|
|
4382
|
-
|
|
4482
|
+
const b = getElementBounds(el);
|
|
4483
|
+
withRotation(ctx, el, b ? center(b) : el.position, () => renderNoteOnCanvas(ctx, el));
|
|
4383
4484
|
continue;
|
|
4384
4485
|
}
|
|
4385
4486
|
if (el.type === "text") {
|
|
4386
|
-
|
|
4487
|
+
const b = getElementBounds(el);
|
|
4488
|
+
withRotation(ctx, el, b ? center(b) : el.position, () => renderTextOnCanvas(ctx, el));
|
|
4387
4489
|
continue;
|
|
4388
4490
|
}
|
|
4389
4491
|
if (el.type === "html") {
|
|
@@ -4392,7 +4494,13 @@ async function exportImage(store, options = {}, layerManager) {
|
|
|
4392
4494
|
if (el.type === "image") {
|
|
4393
4495
|
const img = imageCache.get(el.id);
|
|
4394
4496
|
if (img) {
|
|
4395
|
-
|
|
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
|
+
);
|
|
4396
4504
|
}
|
|
4397
4505
|
continue;
|
|
4398
4506
|
}
|
|
@@ -4728,7 +4836,9 @@ var DomNodeManager = class {
|
|
|
4728
4836
|
top: `${element.position.y}px`,
|
|
4729
4837
|
width: size ? `${size.w}px` : "auto",
|
|
4730
4838
|
height: size ? `${size.h}px` : "auto",
|
|
4731
|
-
zIndex: String(zIndex)
|
|
4839
|
+
zIndex: String(zIndex),
|
|
4840
|
+
transform: element.rotation ? `rotate(${element.rotation}rad)` : "",
|
|
4841
|
+
transformOrigin: "50% 50%"
|
|
4732
4842
|
});
|
|
4733
4843
|
this.renderDomContent(node, element);
|
|
4734
4844
|
}
|
|
@@ -5505,6 +5615,8 @@ var Viewport = class {
|
|
|
5505
5615
|
historyRecorder: this.historyRecorder,
|
|
5506
5616
|
historyStack: this.history,
|
|
5507
5617
|
fitToContent: () => this.fitToContent(),
|
|
5618
|
+
group: () => this.groupSelection(),
|
|
5619
|
+
ungroup: () => this.ungroupSelection(),
|
|
5508
5620
|
shortcuts: options.shortcuts
|
|
5509
5621
|
});
|
|
5510
5622
|
this.domNodeManager = new DomNodeManager({
|
|
@@ -5842,6 +5954,26 @@ var Viewport = class {
|
|
|
5842
5954
|
}
|
|
5843
5955
|
this.historyRecorder.commit();
|
|
5844
5956
|
}
|
|
5957
|
+
groupSelection() {
|
|
5958
|
+
const ids = this.getSelectedIds();
|
|
5959
|
+
if (ids.length < 2) return;
|
|
5960
|
+
const groupId = createId("group");
|
|
5961
|
+
this.historyRecorder.begin();
|
|
5962
|
+
for (const id of ids) {
|
|
5963
|
+
if (this.store.getById(id)) this.store.update(id, { groupId });
|
|
5964
|
+
}
|
|
5965
|
+
this.historyRecorder.commit();
|
|
5966
|
+
}
|
|
5967
|
+
ungroupSelection() {
|
|
5968
|
+
const ids = this.getSelectedIds();
|
|
5969
|
+
if (ids.length === 0) return;
|
|
5970
|
+
this.historyRecorder.begin();
|
|
5971
|
+
for (const id of ids) {
|
|
5972
|
+
const el = this.store.getById(id);
|
|
5973
|
+
if (el && el.groupId !== void 0) this.store.update(id, { groupId: void 0 });
|
|
5974
|
+
}
|
|
5975
|
+
this.historyRecorder.commit();
|
|
5976
|
+
}
|
|
5845
5977
|
alignSelection(edge) {
|
|
5846
5978
|
const bounded = this.boundedSelection();
|
|
5847
5979
|
if (bounded.length < 2) return;
|
|
@@ -5883,13 +6015,13 @@ var Viewport = class {
|
|
|
5883
6015
|
distributeSelection(axis) {
|
|
5884
6016
|
const bounded = this.boundedSelection();
|
|
5885
6017
|
if (bounded.length < 3) return;
|
|
5886
|
-
const
|
|
5887
|
-
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));
|
|
5888
6020
|
const first = sorted[0];
|
|
5889
6021
|
const last = sorted[sorted.length - 1];
|
|
5890
6022
|
if (!first || !last) return;
|
|
5891
|
-
const c0 =
|
|
5892
|
-
const cN =
|
|
6023
|
+
const c0 = center2(first.bounds);
|
|
6024
|
+
const cN = center2(last.bounds);
|
|
5893
6025
|
const n = sorted.length;
|
|
5894
6026
|
this.historyRecorder.begin();
|
|
5895
6027
|
const moved = [];
|
|
@@ -5897,7 +6029,7 @@ var Viewport = class {
|
|
|
5897
6029
|
const item = sorted[i];
|
|
5898
6030
|
if (!item || !this.isMovable(item.el)) continue;
|
|
5899
6031
|
const target = c0 + i * (cN - c0) / (n - 1);
|
|
5900
|
-
const delta = target -
|
|
6032
|
+
const delta = target - center2(item.bounds);
|
|
5901
6033
|
if (delta === 0) continue;
|
|
5902
6034
|
const [dx, dy] = axis === "horizontal" ? [delta, 0] : [0, delta];
|
|
5903
6035
|
this.store.update(item.id, translateElementPatch(item.el, dx, dy));
|
|
@@ -6551,6 +6683,26 @@ var EraserTool = class {
|
|
|
6551
6683
|
}
|
|
6552
6684
|
};
|
|
6553
6685
|
|
|
6686
|
+
// src/elements/group.ts
|
|
6687
|
+
function expandToGroups(ids, elements) {
|
|
6688
|
+
const byId = new Map(elements.map((e) => [e.id, e]));
|
|
6689
|
+
const groupIds = /* @__PURE__ */ new Set();
|
|
6690
|
+
for (const id of ids) {
|
|
6691
|
+
const g = byId.get(id)?.groupId;
|
|
6692
|
+
if (g) groupIds.add(g);
|
|
6693
|
+
}
|
|
6694
|
+
if (groupIds.size === 0) return ids;
|
|
6695
|
+
const idSet = new Set(ids);
|
|
6696
|
+
const result = [...ids];
|
|
6697
|
+
for (const el of elements) {
|
|
6698
|
+
if (el.groupId && groupIds.has(el.groupId) && !idSet.has(el.id)) {
|
|
6699
|
+
result.push(el.id);
|
|
6700
|
+
idSet.add(el.id);
|
|
6701
|
+
}
|
|
6702
|
+
}
|
|
6703
|
+
return result;
|
|
6704
|
+
}
|
|
6705
|
+
|
|
6554
6706
|
// src/tools/arrow-handles.ts
|
|
6555
6707
|
var BIND_THRESHOLD = 20;
|
|
6556
6708
|
var HANDLE_RADIUS = 5;
|
|
@@ -6600,10 +6752,10 @@ function applyArrowHandleDrag(handle, elementId, world, ctx) {
|
|
|
6600
6752
|
const excludeId = el.toBinding?.elementId;
|
|
6601
6753
|
const target = findBindTarget(world, ctx.store, threshold, excludeId, layerFilter);
|
|
6602
6754
|
if (target) {
|
|
6603
|
-
const
|
|
6755
|
+
const center2 = getElementCenter(target);
|
|
6604
6756
|
ctx.store.update(elementId, {
|
|
6605
|
-
from:
|
|
6606
|
-
position:
|
|
6757
|
+
from: center2,
|
|
6758
|
+
position: center2,
|
|
6607
6759
|
fromBinding: { elementId: target.id }
|
|
6608
6760
|
});
|
|
6609
6761
|
} else {
|
|
@@ -6619,9 +6771,9 @@ function applyArrowHandleDrag(handle, elementId, world, ctx) {
|
|
|
6619
6771
|
const excludeId = el.fromBinding?.elementId;
|
|
6620
6772
|
const target = findBindTarget(world, ctx.store, threshold, excludeId, layerFilter);
|
|
6621
6773
|
if (target) {
|
|
6622
|
-
const
|
|
6774
|
+
const center2 = getElementCenter(target);
|
|
6623
6775
|
ctx.store.update(elementId, {
|
|
6624
|
-
to:
|
|
6776
|
+
to: center2,
|
|
6625
6777
|
toBinding: { elementId: target.id }
|
|
6626
6778
|
});
|
|
6627
6779
|
} else {
|
|
@@ -6710,6 +6862,9 @@ var SNAP_PX = 6;
|
|
|
6710
6862
|
var HANDLE_HIT_PADDING2 = 4;
|
|
6711
6863
|
var SELECTION_PAD = 4;
|
|
6712
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"]);
|
|
6713
6868
|
var HANDLE_CURSORS = {
|
|
6714
6869
|
nw: "nwse-resize",
|
|
6715
6870
|
se: "nwse-resize",
|
|
@@ -6798,6 +6953,22 @@ var SelectTool = class {
|
|
|
6798
6953
|
ctx.requestRender();
|
|
6799
6954
|
return;
|
|
6800
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
|
+
}
|
|
6801
6972
|
const resizeHit = this.hitTestResizeHandle(world, ctx);
|
|
6802
6973
|
if (resizeHit) {
|
|
6803
6974
|
const el = ctx.store.getById(resizeHit.elementId);
|
|
@@ -6816,18 +6987,20 @@ var SelectTool = class {
|
|
|
6816
6987
|
this.hasDragged = false;
|
|
6817
6988
|
const hit = this.hitTest(world, ctx);
|
|
6818
6989
|
if (hit) {
|
|
6990
|
+
const all = ctx.store.getAll();
|
|
6819
6991
|
const alreadySelected = this._selectedIds.includes(hit.id);
|
|
6820
6992
|
if (state.shiftKey) {
|
|
6821
6993
|
if (alreadySelected) {
|
|
6822
|
-
|
|
6994
|
+
const grp = new Set(expandToGroups([hit.id], all));
|
|
6995
|
+
this.setSelectedIds(this._selectedIds.filter((id) => !grp.has(id)));
|
|
6823
6996
|
this.mode = { type: "idle" };
|
|
6824
6997
|
} else {
|
|
6825
|
-
this.setSelectedIds([...this._selectedIds, hit.id]);
|
|
6998
|
+
this.setSelectedIds(expandToGroups([...this._selectedIds, hit.id], all));
|
|
6826
6999
|
this.mode = hit.locked ? { type: "idle" } : { type: "dragging" };
|
|
6827
7000
|
}
|
|
6828
7001
|
} else {
|
|
6829
7002
|
if (!alreadySelected) {
|
|
6830
|
-
this.setSelectedIds([hit.id]);
|
|
7003
|
+
this.setSelectedIds(expandToGroups([hit.id], all));
|
|
6831
7004
|
} else if (this._selectedIds.length > 1) {
|
|
6832
7005
|
this.pendingSingleSelectId = hit.id;
|
|
6833
7006
|
}
|
|
@@ -6861,6 +7034,15 @@ var SelectTool = class {
|
|
|
6861
7034
|
this.handleTemplateResize(world, ctx);
|
|
6862
7035
|
return;
|
|
6863
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
|
+
}
|
|
6864
7046
|
if (this.mode.type === "resizing") {
|
|
6865
7047
|
ctx.setCursor?.(HANDLE_CURSORS[this.mode.handle]);
|
|
6866
7048
|
this.handleResize(world, ctx, state.shiftKey);
|
|
@@ -6941,12 +7123,12 @@ var SelectTool = class {
|
|
|
6941
7123
|
if (this.mode.type === "marquee") {
|
|
6942
7124
|
const rect = this.getMarqueeRect();
|
|
6943
7125
|
if (rect) {
|
|
6944
|
-
this.setSelectedIds(this.findElementsInRect(rect, ctx));
|
|
7126
|
+
this.setSelectedIds(expandToGroups(this.findElementsInRect(rect, ctx), ctx.store.getAll()));
|
|
6945
7127
|
}
|
|
6946
7128
|
ctx.requestRender();
|
|
6947
7129
|
}
|
|
6948
7130
|
if (!this.hasDragged && this.pendingSingleSelectId !== null) {
|
|
6949
|
-
this.setSelectedIds([this.pendingSingleSelectId]);
|
|
7131
|
+
this.setSelectedIds(expandToGroups([this.pendingSingleSelectId], ctx.store.getAll()));
|
|
6950
7132
|
}
|
|
6951
7133
|
this.pendingSingleSelectId = null;
|
|
6952
7134
|
this.hasDragged = false;
|
|
@@ -7063,6 +7245,10 @@ var SelectTool = class {
|
|
|
7063
7245
|
ctx.setCursor?.("nwse-resize");
|
|
7064
7246
|
return null;
|
|
7065
7247
|
}
|
|
7248
|
+
if (this.hitTestRotateHandle(world, ctx)) {
|
|
7249
|
+
ctx.setCursor?.("grab");
|
|
7250
|
+
return null;
|
|
7251
|
+
}
|
|
7066
7252
|
const resizeHit = this.hitTestResizeHandle(world, ctx);
|
|
7067
7253
|
if (resizeHit) {
|
|
7068
7254
|
ctx.setCursor?.(HANDLE_CURSORS[resizeHit.handle]);
|
|
@@ -7081,6 +7267,11 @@ var SelectTool = class {
|
|
|
7081
7267
|
if (this.mode.type !== "resizing") return;
|
|
7082
7268
|
const el = ctx.store.getById(this.mode.elementId);
|
|
7083
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
|
+
}
|
|
7084
7275
|
const { handle } = this.mode;
|
|
7085
7276
|
const dx = world.x - this.lastWorld.x;
|
|
7086
7277
|
const dy = world.y - this.lastWorld.y;
|
|
@@ -7138,6 +7329,78 @@ var SelectTool = class {
|
|
|
7138
7329
|
this.updateArrowsBoundTo([this.mode.elementId], ctx);
|
|
7139
7330
|
ctx.requestRender();
|
|
7140
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
|
+
}
|
|
7141
7404
|
hitTestResizeHandle(world, ctx) {
|
|
7142
7405
|
if (this._selectedIds.length === 0) return null;
|
|
7143
7406
|
const zoom = ctx.camera.zoom;
|
|
@@ -7146,10 +7409,9 @@ var SelectTool = class {
|
|
|
7146
7409
|
const el = ctx.store.getById(id);
|
|
7147
7410
|
if (!el || !("size" in el)) continue;
|
|
7148
7411
|
if (el.type === "shape" && el.shape === "line") continue;
|
|
7149
|
-
const
|
|
7150
|
-
if (!
|
|
7151
|
-
const
|
|
7152
|
-
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) {
|
|
7153
7415
|
if (Math.abs(world.x - pos.x) <= handleHalf && Math.abs(world.y - pos.y) <= handleHalf) {
|
|
7154
7416
|
return { elementId: id, handle };
|
|
7155
7417
|
}
|
|
@@ -7157,6 +7419,19 @@ var SelectTool = class {
|
|
|
7157
7419
|
}
|
|
7158
7420
|
return null;
|
|
7159
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
|
+
}
|
|
7160
7435
|
hitTestLineHandles(world, ctx) {
|
|
7161
7436
|
if (this._selectedIds.length === 0) return null;
|
|
7162
7437
|
const zoom = ctx.camera.zoom;
|
|
@@ -7179,6 +7454,30 @@ var SelectTool = class {
|
|
|
7179
7454
|
["se", { x: bounds.x + bounds.w, y: bounds.y + bounds.h }]
|
|
7180
7455
|
];
|
|
7181
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
|
+
}
|
|
7182
7481
|
renderMarquee(canvasCtx) {
|
|
7183
7482
|
if (this.mode.type !== "marquee") return;
|
|
7184
7483
|
const rect = this.getMarqueeRect();
|
|
@@ -7223,12 +7522,31 @@ var SelectTool = class {
|
|
|
7223
7522
|
}
|
|
7224
7523
|
const bounds = getElementBounds(el);
|
|
7225
7524
|
if (!bounds) continue;
|
|
7525
|
+
const layout = this.getOverlayLayout(el, zoom);
|
|
7526
|
+
if (!layout) continue;
|
|
7226
7527
|
const pad = SELECTION_PAD / zoom;
|
|
7227
|
-
|
|
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
|
+
}
|
|
7228
7546
|
if ("size" in el) {
|
|
7229
7547
|
canvasCtx.setLineDash([]);
|
|
7230
7548
|
canvasCtx.fillStyle = "#ffffff";
|
|
7231
|
-
const corners = this.getHandlePositions(bounds);
|
|
7549
|
+
const corners = layout.angle === 0 ? this.getHandlePositions(bounds) : layout.corners;
|
|
7232
7550
|
for (const [, pos] of corners) {
|
|
7233
7551
|
canvasCtx.fillRect(
|
|
7234
7552
|
pos.x - handleWorldSize / 2,
|
|
@@ -7263,6 +7581,21 @@ var SelectTool = class {
|
|
|
7263
7581
|
);
|
|
7264
7582
|
canvasCtx.setLineDash([4 / zoom, 4 / zoom]);
|
|
7265
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
|
+
}
|
|
7266
7599
|
}
|
|
7267
7600
|
canvasCtx.restore();
|
|
7268
7601
|
}
|
|
@@ -7342,7 +7675,7 @@ var SelectTool = class {
|
|
|
7342
7675
|
if (ctx.isLayerLocked && ctx.isLayerLocked(el.layerId)) continue;
|
|
7343
7676
|
if (el.type === "grid") continue;
|
|
7344
7677
|
const bounds = getElementBounds(el);
|
|
7345
|
-
if (bounds && this.rectsOverlap(marquee, bounds)) {
|
|
7678
|
+
if (bounds && this.rectsOverlap(marquee, rotatedAABB(bounds, el.rotation ?? 0))) {
|
|
7346
7679
|
ids.push(el.id);
|
|
7347
7680
|
}
|
|
7348
7681
|
}
|
|
@@ -7364,6 +7697,13 @@ var SelectTool = class {
|
|
|
7364
7697
|
}
|
|
7365
7698
|
isInsideBounds(point, el) {
|
|
7366
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
|
+
}
|
|
7367
7707
|
if (el.type === "shape" && el.shape === "line") {
|
|
7368
7708
|
const [a, b] = lineEndpoints(el);
|
|
7369
7709
|
const threshold = Math.max(el.strokeWidth / 2, 6);
|
|
@@ -8130,20 +8470,20 @@ var TemplateTool = class {
|
|
|
8130
8470
|
const snapUnit = Math.sqrt(3) * cellSize;
|
|
8131
8471
|
const radiusCells = radius / snapUnit;
|
|
8132
8472
|
const angle = this.computeAngle();
|
|
8133
|
-
const
|
|
8473
|
+
const center2 = this.origin;
|
|
8134
8474
|
let hexCells;
|
|
8135
8475
|
switch (this.templateShape) {
|
|
8136
8476
|
case "circle":
|
|
8137
|
-
hexCells = getHexCellsInRadius(
|
|
8477
|
+
hexCells = getHexCellsInRadius(center2, radiusCells, cellSize, orientation);
|
|
8138
8478
|
break;
|
|
8139
8479
|
case "cone":
|
|
8140
|
-
hexCells = getHexCellsInCone(
|
|
8480
|
+
hexCells = getHexCellsInCone(center2, angle, radiusCells, cellSize, orientation);
|
|
8141
8481
|
break;
|
|
8142
8482
|
case "line":
|
|
8143
|
-
hexCells = getHexCellsInLine(
|
|
8483
|
+
hexCells = getHexCellsInLine(center2, angle, radiusCells, cellSize, orientation);
|
|
8144
8484
|
break;
|
|
8145
8485
|
case "square":
|
|
8146
|
-
hexCells = getHexCellsInSquare(
|
|
8486
|
+
hexCells = getHexCellsInSquare(center2, radiusCells, cellSize, orientation);
|
|
8147
8487
|
break;
|
|
8148
8488
|
}
|
|
8149
8489
|
ctx.save();
|
|
@@ -8164,7 +8504,7 @@ var TemplateTool = class {
|
|
|
8164
8504
|
if (this.templateShape === "cone" || this.templateShape === "line" || this.templateShape === "circle" || this.templateShape === "square") {
|
|
8165
8505
|
ctx.globalAlpha = 0.5;
|
|
8166
8506
|
ctx.beginPath();
|
|
8167
|
-
drawHexPath(ctx,
|
|
8507
|
+
drawHexPath(ctx, center2.x, center2.y, cellSize, orientation);
|
|
8168
8508
|
ctx.fillStyle = this.strokeColor;
|
|
8169
8509
|
ctx.fill();
|
|
8170
8510
|
ctx.strokeStyle = this.strokeColor;
|
|
@@ -8180,8 +8520,8 @@ var TemplateTool = class {
|
|
|
8180
8520
|
ctx.font = `bold ${fontSize}px system-ui, sans-serif`;
|
|
8181
8521
|
ctx.textAlign = "center";
|
|
8182
8522
|
ctx.textBaseline = "bottom";
|
|
8183
|
-
const textX =
|
|
8184
|
-
const textY =
|
|
8523
|
+
const textX = center2.x;
|
|
8524
|
+
const textY = center2.y - 4;
|
|
8185
8525
|
const metrics = ctx.measureText(label);
|
|
8186
8526
|
const padX = 4;
|
|
8187
8527
|
const padY = 2;
|
|
@@ -8231,7 +8571,7 @@ var TemplateTool = class {
|
|
|
8231
8571
|
};
|
|
8232
8572
|
|
|
8233
8573
|
// src/index.ts
|
|
8234
|
-
var VERSION = "0.
|
|
8574
|
+
var VERSION = "0.35.0";
|
|
8235
8575
|
export {
|
|
8236
8576
|
ArrowTool,
|
|
8237
8577
|
AutoSave,
|