@fieldnotes/core 0.34.0 → 0.36.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 +656 -636
- package/dist/index.cjs +624 -113
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +20 -1
- package/dist/index.d.ts +20 -1
- package/dist/index.js +624 -113
- 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) {
|
|
@@ -880,6 +910,14 @@ var KeyboardActions = class {
|
|
|
880
910
|
}
|
|
881
911
|
this.pasteCount = 0;
|
|
882
912
|
}
|
|
913
|
+
cut() {
|
|
914
|
+
if (this.deps.isToolActive()) return;
|
|
915
|
+
this.copy();
|
|
916
|
+
this.deleteSelected();
|
|
917
|
+
}
|
|
918
|
+
hasClipboard() {
|
|
919
|
+
return this.clipboard.length > 0;
|
|
920
|
+
}
|
|
883
921
|
paste() {
|
|
884
922
|
if (this.deps.isToolActive()) return;
|
|
885
923
|
this.flushPendingNudge();
|
|
@@ -948,6 +986,10 @@ var KeyboardActions = class {
|
|
|
948
986
|
if (this.deps.isToolActive()) return;
|
|
949
987
|
this.deps.ungroup?.();
|
|
950
988
|
}
|
|
989
|
+
toggleLock() {
|
|
990
|
+
if (this.deps.isToolActive()) return;
|
|
991
|
+
this.deps.toggleLock?.();
|
|
992
|
+
}
|
|
951
993
|
zOrder(operation) {
|
|
952
994
|
if (this.deps.isToolActive()) return;
|
|
953
995
|
this.flushPendingNudge();
|
|
@@ -1049,6 +1091,8 @@ var DEFAULT_BINDINGS = [
|
|
|
1049
1091
|
["zoom-reset", ["mod+0"]],
|
|
1050
1092
|
["group", ["mod+g"]],
|
|
1051
1093
|
["ungroup", ["mod+shift+g"]],
|
|
1094
|
+
["cut", ["mod+x"]],
|
|
1095
|
+
["toggle-lock", ["mod+shift+l"]],
|
|
1052
1096
|
["nudge-left", ["arrowleft"]],
|
|
1053
1097
|
["nudge-right", ["arrowright"]],
|
|
1054
1098
|
["nudge-up", ["arrowup"]],
|
|
@@ -1187,6 +1231,7 @@ var ShortcutMap = class {
|
|
|
1187
1231
|
var ZOOM_SENSITIVITY = 1e-3;
|
|
1188
1232
|
var ZOOM_STEP = 1.2;
|
|
1189
1233
|
var MIDDLE_BUTTON = 1;
|
|
1234
|
+
var LONG_PRESS_MS = 500;
|
|
1190
1235
|
var NUDGE_DELTAS = {
|
|
1191
1236
|
"nudge-left": [-1, 0],
|
|
1192
1237
|
"nudge-right": [1, 0],
|
|
@@ -1210,8 +1255,10 @@ var InputHandler = class {
|
|
|
1210
1255
|
fitToContent: options.fitToContent,
|
|
1211
1256
|
group: options.group,
|
|
1212
1257
|
ungroup: options.ungroup,
|
|
1258
|
+
toggleLock: options.toggleLock,
|
|
1213
1259
|
getLastPointerWorld: () => this.lastPointerWorld()
|
|
1214
1260
|
});
|
|
1261
|
+
this.openContextMenu = options.openContextMenu;
|
|
1215
1262
|
this.shortcutMap = new ShortcutMap(options.shortcuts?.bindings);
|
|
1216
1263
|
this.scope = options.shortcuts?.scope ?? "focus";
|
|
1217
1264
|
this.element.style.touchAction = "none";
|
|
@@ -1235,10 +1282,13 @@ var InputHandler = class {
|
|
|
1235
1282
|
lastPointerEvent = null;
|
|
1236
1283
|
inputFilter = new InputFilter();
|
|
1237
1284
|
deferredDown = null;
|
|
1285
|
+
longPressTimer = null;
|
|
1286
|
+
longPressStart = null;
|
|
1238
1287
|
abortController = new AbortController();
|
|
1239
1288
|
actions;
|
|
1240
1289
|
shortcutMap;
|
|
1241
1290
|
scope;
|
|
1291
|
+
openContextMenu;
|
|
1242
1292
|
setToolManager(toolManager, toolContext) {
|
|
1243
1293
|
this.toolManager = toolManager;
|
|
1244
1294
|
this.toolContext = toolContext;
|
|
@@ -1253,6 +1303,7 @@ var InputHandler = class {
|
|
|
1253
1303
|
this.actions.dispose();
|
|
1254
1304
|
this.abortController.abort();
|
|
1255
1305
|
this.inputFilter.reset();
|
|
1306
|
+
this.cancelLongPress();
|
|
1256
1307
|
this.deferredDown = null;
|
|
1257
1308
|
this.lastPointerEvent = null;
|
|
1258
1309
|
if (this.scope === "focus") {
|
|
@@ -1268,6 +1319,7 @@ var InputHandler = class {
|
|
|
1268
1319
|
this.element.addEventListener("pointerup", this.onPointerUp, opts);
|
|
1269
1320
|
this.element.addEventListener("pointerleave", this.onPointerLeave, opts);
|
|
1270
1321
|
this.element.addEventListener("pointercancel", this.onPointerUp, opts);
|
|
1322
|
+
this.element.addEventListener("contextmenu", this.onContextMenu, opts);
|
|
1271
1323
|
window.addEventListener("keydown", this.onKeyDown, opts);
|
|
1272
1324
|
window.addEventListener("keyup", this.onKeyUp, opts);
|
|
1273
1325
|
}
|
|
@@ -1296,11 +1348,13 @@ var InputHandler = class {
|
|
|
1296
1348
|
this.activePointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
|
1297
1349
|
this.element.setPointerCapture?.(e.pointerId);
|
|
1298
1350
|
if (this.activePointers.size === 2) {
|
|
1351
|
+
this.cancelLongPress();
|
|
1299
1352
|
this.startPinch();
|
|
1300
1353
|
this.cancelToolIfActive(e);
|
|
1301
1354
|
return;
|
|
1302
1355
|
}
|
|
1303
1356
|
if (e.button === MIDDLE_BUTTON || e.button === 0 && this.spaceHeld) {
|
|
1357
|
+
this.cancelLongPress();
|
|
1304
1358
|
this.isPanning = true;
|
|
1305
1359
|
this.lastPointer = { x: e.clientX, y: e.clientY };
|
|
1306
1360
|
return;
|
|
@@ -1310,6 +1364,7 @@ var InputHandler = class {
|
|
|
1310
1364
|
if (result.action === "suppress") return;
|
|
1311
1365
|
if (result.action === "defer") {
|
|
1312
1366
|
this.deferredDown = e;
|
|
1367
|
+
this.startLongPress(e);
|
|
1313
1368
|
return;
|
|
1314
1369
|
}
|
|
1315
1370
|
this.dispatchToolDown(e);
|
|
@@ -1338,6 +1393,7 @@ var InputHandler = class {
|
|
|
1338
1393
|
} else if (this.deferredDown) {
|
|
1339
1394
|
const result = this.inputFilter.filterMove(e);
|
|
1340
1395
|
if (result.action === "dispatch") {
|
|
1396
|
+
this.cancelLongPress();
|
|
1341
1397
|
this.dispatchToolDown(this.deferredDown);
|
|
1342
1398
|
this.deferredDown = null;
|
|
1343
1399
|
this.dispatchToolMove(e);
|
|
@@ -1347,6 +1403,7 @@ var InputHandler = class {
|
|
|
1347
1403
|
}
|
|
1348
1404
|
};
|
|
1349
1405
|
onPointerUp = (e) => {
|
|
1406
|
+
this.cancelLongPress();
|
|
1350
1407
|
try {
|
|
1351
1408
|
this.element.releasePointerCapture(e.pointerId);
|
|
1352
1409
|
} catch {
|
|
@@ -1399,74 +1456,82 @@ var InputHandler = class {
|
|
|
1399
1456
|
runAction(action, e) {
|
|
1400
1457
|
switch (action) {
|
|
1401
1458
|
case "delete":
|
|
1402
|
-
e
|
|
1459
|
+
e?.preventDefault();
|
|
1403
1460
|
this.actions.deleteSelected();
|
|
1404
1461
|
return;
|
|
1405
1462
|
case "deselect":
|
|
1406
1463
|
this.actions.deselect();
|
|
1407
1464
|
return;
|
|
1408
1465
|
case "undo":
|
|
1409
|
-
e
|
|
1466
|
+
e?.preventDefault();
|
|
1410
1467
|
this.actions.undo();
|
|
1411
1468
|
return;
|
|
1412
1469
|
case "redo":
|
|
1413
|
-
e
|
|
1470
|
+
e?.preventDefault();
|
|
1414
1471
|
this.actions.redo();
|
|
1415
1472
|
return;
|
|
1416
1473
|
case "select-all":
|
|
1417
|
-
e
|
|
1474
|
+
e?.preventDefault();
|
|
1418
1475
|
this.actions.selectAll();
|
|
1419
1476
|
return;
|
|
1420
1477
|
case "copy":
|
|
1421
|
-
e
|
|
1478
|
+
e?.preventDefault();
|
|
1422
1479
|
this.actions.copy();
|
|
1423
1480
|
return;
|
|
1424
1481
|
case "paste":
|
|
1425
|
-
e
|
|
1482
|
+
e?.preventDefault();
|
|
1426
1483
|
this.actions.paste();
|
|
1427
1484
|
return;
|
|
1428
1485
|
case "duplicate":
|
|
1429
|
-
e
|
|
1486
|
+
e?.preventDefault();
|
|
1430
1487
|
this.actions.duplicate();
|
|
1431
1488
|
return;
|
|
1432
1489
|
case "z-forward":
|
|
1433
|
-
e
|
|
1490
|
+
e?.preventDefault();
|
|
1434
1491
|
this.actions.zOrder("forward");
|
|
1435
1492
|
return;
|
|
1436
1493
|
case "z-backward":
|
|
1437
|
-
e
|
|
1494
|
+
e?.preventDefault();
|
|
1438
1495
|
this.actions.zOrder("backward");
|
|
1439
1496
|
return;
|
|
1440
1497
|
case "z-front":
|
|
1441
|
-
e
|
|
1498
|
+
e?.preventDefault();
|
|
1442
1499
|
this.actions.zOrder("front");
|
|
1443
1500
|
return;
|
|
1444
1501
|
case "z-back":
|
|
1445
|
-
e
|
|
1502
|
+
e?.preventDefault();
|
|
1446
1503
|
this.actions.zOrder("back");
|
|
1447
1504
|
return;
|
|
1448
1505
|
case "zoom-fit":
|
|
1449
|
-
e
|
|
1506
|
+
e?.preventDefault();
|
|
1450
1507
|
this.actions.zoomToFit();
|
|
1451
1508
|
return;
|
|
1452
1509
|
case "group":
|
|
1453
|
-
e
|
|
1510
|
+
e?.preventDefault();
|
|
1454
1511
|
this.actions.group();
|
|
1455
1512
|
return;
|
|
1456
1513
|
case "ungroup":
|
|
1457
|
-
e
|
|
1514
|
+
e?.preventDefault();
|
|
1458
1515
|
this.actions.ungroup();
|
|
1459
1516
|
return;
|
|
1517
|
+
case "cut":
|
|
1518
|
+
e?.preventDefault();
|
|
1519
|
+
this.actions.cut();
|
|
1520
|
+
return;
|
|
1521
|
+
case "toggle-lock":
|
|
1522
|
+
e?.preventDefault();
|
|
1523
|
+
this.actions.toggleLock();
|
|
1524
|
+
return;
|
|
1460
1525
|
case "zoom-in":
|
|
1461
|
-
e
|
|
1526
|
+
e?.preventDefault();
|
|
1462
1527
|
this.zoomByFactor(ZOOM_STEP);
|
|
1463
1528
|
return;
|
|
1464
1529
|
case "zoom-out":
|
|
1465
|
-
e
|
|
1530
|
+
e?.preventDefault();
|
|
1466
1531
|
this.zoomByFactor(1 / ZOOM_STEP);
|
|
1467
1532
|
return;
|
|
1468
1533
|
case "zoom-reset":
|
|
1469
|
-
e
|
|
1534
|
+
e?.preventDefault();
|
|
1470
1535
|
this.zoomToLevel(1);
|
|
1471
1536
|
return;
|
|
1472
1537
|
case "nudge-left":
|
|
@@ -1474,22 +1539,26 @@ var InputHandler = class {
|
|
|
1474
1539
|
case "nudge-up":
|
|
1475
1540
|
case "nudge-down": {
|
|
1476
1541
|
const delta = NUDGE_DELTAS[action];
|
|
1477
|
-
if (delta && this.actions.nudge(delta[0], delta[1], e
|
|
1478
|
-
e
|
|
1542
|
+
if (delta && this.actions.nudge(delta[0], delta[1], e?.shiftKey ?? false)) {
|
|
1543
|
+
e?.preventDefault();
|
|
1479
1544
|
}
|
|
1480
1545
|
return;
|
|
1481
1546
|
}
|
|
1482
1547
|
default:
|
|
1483
1548
|
if (action.startsWith("tool:")) {
|
|
1484
1549
|
if (this.isToolActive) return;
|
|
1485
|
-
e
|
|
1550
|
+
e?.preventDefault();
|
|
1486
1551
|
this.toolContext?.switchTool?.(action.slice("tool:".length));
|
|
1487
1552
|
return;
|
|
1488
1553
|
}
|
|
1489
1554
|
console.warn(`[fieldnotes] unknown shortcut action "${action}"`);
|
|
1490
1555
|
}
|
|
1491
1556
|
}
|
|
1557
|
+
hasClipboard() {
|
|
1558
|
+
return this.actions.hasClipboard();
|
|
1559
|
+
}
|
|
1492
1560
|
startPinch() {
|
|
1561
|
+
this.cancelLongPress();
|
|
1493
1562
|
this.inputFilter.reset();
|
|
1494
1563
|
this.deferredDown = null;
|
|
1495
1564
|
this.isPanning = true;
|
|
@@ -1501,18 +1570,18 @@ var InputHandler = class {
|
|
|
1501
1570
|
handlePinchMove() {
|
|
1502
1571
|
const [a, b] = this.getPinchPoints();
|
|
1503
1572
|
const dist = this.distance(a, b);
|
|
1504
|
-
const
|
|
1573
|
+
const center2 = this.midpoint(a, b);
|
|
1505
1574
|
if (this.lastPinchDistance > 0) {
|
|
1506
1575
|
const scale = dist / this.lastPinchDistance;
|
|
1507
1576
|
const newZoom = this.camera.zoom * scale;
|
|
1508
|
-
this.camera.zoomAt(newZoom,
|
|
1577
|
+
this.camera.zoomAt(newZoom, center2);
|
|
1509
1578
|
}
|
|
1510
|
-
const dx =
|
|
1511
|
-
const dy =
|
|
1579
|
+
const dx = center2.x - this.lastPointer.x;
|
|
1580
|
+
const dy = center2.y - this.lastPointer.y;
|
|
1512
1581
|
this.camera.pan(dx, dy);
|
|
1513
1582
|
this.lastPinchDistance = dist;
|
|
1514
|
-
this.lastPinchCenter =
|
|
1515
|
-
this.lastPointer = { ...
|
|
1583
|
+
this.lastPinchCenter = center2;
|
|
1584
|
+
this.lastPointer = { ...center2 };
|
|
1516
1585
|
}
|
|
1517
1586
|
getPinchPoints() {
|
|
1518
1587
|
const pts = [...this.activePointers.values()];
|
|
@@ -1532,6 +1601,13 @@ var InputHandler = class {
|
|
|
1532
1601
|
const rect = this.element.getBoundingClientRect();
|
|
1533
1602
|
return this.camera.screenToWorld({ x: e.clientX - rect.left, y: e.clientY - rect.top });
|
|
1534
1603
|
}
|
|
1604
|
+
onContextMenu = (e) => {
|
|
1605
|
+
e.preventDefault();
|
|
1606
|
+
if (this.toolManager?.activeTool?.name !== "select") return;
|
|
1607
|
+
const rect = this.element.getBoundingClientRect();
|
|
1608
|
+
const world = this.camera.screenToWorld({ x: e.clientX - rect.left, y: e.clientY - rect.top });
|
|
1609
|
+
this.openContextMenu?.({ x: e.clientX, y: e.clientY }, world);
|
|
1610
|
+
};
|
|
1535
1611
|
onPointerLeave = (e) => {
|
|
1536
1612
|
this.lastPointerEvent = null;
|
|
1537
1613
|
this.onPointerUp(e);
|
|
@@ -1579,12 +1655,36 @@ var InputHandler = class {
|
|
|
1579
1655
|
this.element.focus({ preventScroll: true });
|
|
1580
1656
|
}
|
|
1581
1657
|
cancelToolIfActive(e) {
|
|
1658
|
+
this.cancelLongPress();
|
|
1582
1659
|
if (this.isToolActive) {
|
|
1583
1660
|
this.dispatchToolUp(e);
|
|
1584
1661
|
this.isToolActive = false;
|
|
1585
1662
|
}
|
|
1586
1663
|
this.deferredDown = null;
|
|
1587
1664
|
}
|
|
1665
|
+
startLongPress(e) {
|
|
1666
|
+
if (e.pointerType !== "touch") return;
|
|
1667
|
+
if (this.toolManager?.activeTool?.name !== "select") return;
|
|
1668
|
+
this.longPressStart = { x: e.clientX, y: e.clientY };
|
|
1669
|
+
this.longPressTimer = setTimeout(() => this.fireLongPress(), LONG_PRESS_MS);
|
|
1670
|
+
}
|
|
1671
|
+
cancelLongPress() {
|
|
1672
|
+
if (this.longPressTimer !== null) {
|
|
1673
|
+
clearTimeout(this.longPressTimer);
|
|
1674
|
+
this.longPressTimer = null;
|
|
1675
|
+
}
|
|
1676
|
+
this.longPressStart = null;
|
|
1677
|
+
}
|
|
1678
|
+
fireLongPress() {
|
|
1679
|
+
this.longPressTimer = null;
|
|
1680
|
+
if (!this.deferredDown || this.activePointers.size !== 1 || this.isPanning) return;
|
|
1681
|
+
const start = this.longPressStart;
|
|
1682
|
+
if (!start) return;
|
|
1683
|
+
const rect = this.element.getBoundingClientRect();
|
|
1684
|
+
const world = this.camera.screenToWorld({ x: start.x - rect.left, y: start.y - rect.top });
|
|
1685
|
+
this.deferredDown = null;
|
|
1686
|
+
this.openContextMenu?.({ x: start.x, y: start.y }, world);
|
|
1687
|
+
}
|
|
1588
1688
|
};
|
|
1589
1689
|
|
|
1590
1690
|
// src/canvas/background.ts
|
|
@@ -2054,11 +2154,19 @@ var ElementStore = class {
|
|
|
2054
2154
|
(el) => el.type === type
|
|
2055
2155
|
);
|
|
2056
2156
|
}
|
|
2157
|
+
// Spatial index stores the rotation-expanded AABB so rotated elements remain
|
|
2158
|
+
// broad-phase hit-test/marquee candidates; precise tests run against local bounds.
|
|
2159
|
+
indexBounds(element) {
|
|
2160
|
+
const bounds = getElementBounds(element);
|
|
2161
|
+
if (!bounds) return null;
|
|
2162
|
+
const angle = element.rotation ?? 0;
|
|
2163
|
+
return angle === 0 ? bounds : rotatedAABB(bounds, angle);
|
|
2164
|
+
}
|
|
2057
2165
|
add(element) {
|
|
2058
2166
|
this.sortedCache = null;
|
|
2059
2167
|
this._versions.set(element.id, 0);
|
|
2060
2168
|
this.elements.set(element.id, element);
|
|
2061
|
-
const bounds =
|
|
2169
|
+
const bounds = this.indexBounds(element);
|
|
2062
2170
|
if (bounds) this.spatialIndex.insert(element.id, bounds);
|
|
2063
2171
|
this.bus.emit("add", element);
|
|
2064
2172
|
}
|
|
@@ -2080,7 +2188,7 @@ var ElementStore = class {
|
|
|
2080
2188
|
updated.text = sanitizeNoteHtml(updated.text);
|
|
2081
2189
|
}
|
|
2082
2190
|
this.elements.set(id, updated);
|
|
2083
|
-
const newBounds =
|
|
2191
|
+
const newBounds = this.indexBounds(updated);
|
|
2084
2192
|
if (newBounds) {
|
|
2085
2193
|
this.spatialIndex.update(id, newBounds);
|
|
2086
2194
|
}
|
|
@@ -2113,7 +2221,7 @@ var ElementStore = class {
|
|
|
2113
2221
|
for (const el of elements) {
|
|
2114
2222
|
this.elements.set(el.id, el);
|
|
2115
2223
|
this._versions.set(el.id, 0);
|
|
2116
|
-
const bounds =
|
|
2224
|
+
const bounds = this.indexBounds(el);
|
|
2117
2225
|
if (bounds) this.spatialIndex.insert(el.id, bounds);
|
|
2118
2226
|
if (el.type === "stroke") {
|
|
2119
2227
|
computeStrokeSegments(el);
|
|
@@ -2318,9 +2426,9 @@ function updateBoundArrow(arrow, store) {
|
|
|
2318
2426
|
if (arrow.fromBinding) {
|
|
2319
2427
|
const el = store.getById(arrow.fromBinding.elementId);
|
|
2320
2428
|
if (el) {
|
|
2321
|
-
const
|
|
2322
|
-
updates.from =
|
|
2323
|
-
updates.position =
|
|
2429
|
+
const center2 = getElementCenter(el);
|
|
2430
|
+
updates.from = center2;
|
|
2431
|
+
updates.position = center2;
|
|
2324
2432
|
}
|
|
2325
2433
|
}
|
|
2326
2434
|
if (arrow.toBinding) {
|
|
@@ -2332,6 +2440,21 @@ function updateBoundArrow(arrow, store) {
|
|
|
2332
2440
|
return Object.keys(updates).length > 0 ? updates : null;
|
|
2333
2441
|
}
|
|
2334
2442
|
|
|
2443
|
+
// src/elements/rotate-canvas.ts
|
|
2444
|
+
function withRotation(ctx, el, center2, draw) {
|
|
2445
|
+
const angle = el.rotation ?? 0;
|
|
2446
|
+
if (angle === 0) {
|
|
2447
|
+
draw();
|
|
2448
|
+
return;
|
|
2449
|
+
}
|
|
2450
|
+
ctx.save();
|
|
2451
|
+
ctx.translate(center2.x, center2.y);
|
|
2452
|
+
ctx.rotate(angle);
|
|
2453
|
+
ctx.translate(-center2.x, -center2.y);
|
|
2454
|
+
draw();
|
|
2455
|
+
ctx.restore();
|
|
2456
|
+
}
|
|
2457
|
+
|
|
2335
2458
|
// src/elements/grid-renderer.ts
|
|
2336
2459
|
function getSquareGridLines(bounds, cellSize) {
|
|
2337
2460
|
if (cellSize <= 0) return { verticals: [], horizontals: [] };
|
|
@@ -2595,18 +2718,18 @@ function getHexDistance(a, b, cellSize, orientation) {
|
|
|
2595
2718
|
const ds = -dq - dr;
|
|
2596
2719
|
return Math.max(Math.abs(dq), Math.abs(dr), Math.abs(ds));
|
|
2597
2720
|
}
|
|
2598
|
-
function getHexCellsInRadius(
|
|
2721
|
+
function getHexCellsInRadius(center2, radiusCells, cellSize, orientation) {
|
|
2599
2722
|
const n = Math.round(radiusCells);
|
|
2600
|
-
const off = pixelToOffset(
|
|
2723
|
+
const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
|
|
2601
2724
|
const cube = offsetToCube(off.col, off.row, orientation);
|
|
2602
2725
|
if (n <= 0) {
|
|
2603
2726
|
return [offsetToPixel(off.col, off.row, cellSize, orientation)];
|
|
2604
2727
|
}
|
|
2605
2728
|
return enumerateHexRing(cube.q, cube.r, n, orientation, cellSize);
|
|
2606
2729
|
}
|
|
2607
|
-
function getHexCellsInCone(
|
|
2730
|
+
function getHexCellsInCone(center2, angle, radiusCells, cellSize, orientation) {
|
|
2608
2731
|
const n = Math.round(radiusCells);
|
|
2609
|
-
const off = pixelToOffset(
|
|
2732
|
+
const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
|
|
2610
2733
|
const cube = offsetToCube(off.col, off.row, orientation);
|
|
2611
2734
|
const centerPixel = offsetToPixel(off.col, off.row, cellSize, orientation);
|
|
2612
2735
|
if (n <= 0) return [centerPixel];
|
|
@@ -2640,9 +2763,9 @@ function getHexCellsInCone(center, angle, radiusCells, cellSize, orientation) {
|
|
|
2640
2763
|
}
|
|
2641
2764
|
return cells;
|
|
2642
2765
|
}
|
|
2643
|
-
function getHexCellsInLine(
|
|
2766
|
+
function getHexCellsInLine(center2, angle, radiusCells, cellSize, orientation) {
|
|
2644
2767
|
const n = Math.round(radiusCells);
|
|
2645
|
-
const off = pixelToOffset(
|
|
2768
|
+
const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
|
|
2646
2769
|
const cube = offsetToCube(off.col, off.row, orientation);
|
|
2647
2770
|
const centerPixel = offsetToPixel(off.col, off.row, cellSize, orientation);
|
|
2648
2771
|
if (n <= 0) return [centerPixel];
|
|
@@ -2678,9 +2801,9 @@ function getHexCellsInLine(center, angle, radiusCells, cellSize, orientation) {
|
|
|
2678
2801
|
}
|
|
2679
2802
|
return cells;
|
|
2680
2803
|
}
|
|
2681
|
-
function getHexCellsInSquare(
|
|
2804
|
+
function getHexCellsInSquare(center2, radiusCells, cellSize, orientation) {
|
|
2682
2805
|
const n = Math.round(radiusCells);
|
|
2683
|
-
const off = pixelToOffset(
|
|
2806
|
+
const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
|
|
2684
2807
|
const cube = offsetToCube(off.col, off.row, orientation);
|
|
2685
2808
|
const centerPixel = offsetToPixel(off.col, off.row, cellSize, orientation);
|
|
2686
2809
|
if (n <= 0) return [centerPixel];
|
|
@@ -2758,18 +2881,27 @@ var ElementRenderer = class {
|
|
|
2758
2881
|
}
|
|
2759
2882
|
renderCanvasElement(ctx, element) {
|
|
2760
2883
|
switch (element.type) {
|
|
2761
|
-
case "stroke":
|
|
2762
|
-
|
|
2884
|
+
case "stroke": {
|
|
2885
|
+
const b = getElementBounds(element);
|
|
2886
|
+
const c = b ? { x: b.x + b.w / 2, y: b.y + b.h / 2 } : element.position;
|
|
2887
|
+
withRotation(ctx, element, c, () => this.renderStroke(ctx, element));
|
|
2763
2888
|
break;
|
|
2889
|
+
}
|
|
2764
2890
|
case "arrow":
|
|
2765
2891
|
this.renderArrow(ctx, element);
|
|
2766
2892
|
break;
|
|
2767
|
-
case "shape":
|
|
2768
|
-
|
|
2893
|
+
case "shape": {
|
|
2894
|
+
const b = getElementBounds(element);
|
|
2895
|
+
const c = b ? { x: b.x + b.w / 2, y: b.y + b.h / 2 } : element.position;
|
|
2896
|
+
withRotation(ctx, element, c, () => this.renderShape(ctx, element));
|
|
2769
2897
|
break;
|
|
2770
|
-
|
|
2771
|
-
|
|
2898
|
+
}
|
|
2899
|
+
case "image": {
|
|
2900
|
+
const b = getElementBounds(element);
|
|
2901
|
+
const c = b ? { x: b.x + b.w / 2, y: b.y + b.h / 2 } : element.position;
|
|
2902
|
+
withRotation(ctx, element, c, () => this.renderImage(ctx, element));
|
|
2772
2903
|
break;
|
|
2904
|
+
}
|
|
2773
2905
|
case "grid":
|
|
2774
2906
|
this.renderGrid(ctx, element);
|
|
2775
2907
|
break;
|
|
@@ -3066,20 +3198,20 @@ var ElementRenderer = class {
|
|
|
3066
3198
|
renderHexTemplate(ctx, template, cellSize, orientation) {
|
|
3067
3199
|
const snapUnit = Math.sqrt(3) * cellSize;
|
|
3068
3200
|
const radiusCells = template.radius / snapUnit;
|
|
3069
|
-
const
|
|
3201
|
+
const center2 = template.position;
|
|
3070
3202
|
let cells;
|
|
3071
3203
|
switch (template.templateShape) {
|
|
3072
3204
|
case "circle":
|
|
3073
|
-
cells = getHexCellsInRadius(
|
|
3205
|
+
cells = getHexCellsInRadius(center2, radiusCells, cellSize, orientation);
|
|
3074
3206
|
break;
|
|
3075
3207
|
case "cone":
|
|
3076
|
-
cells = getHexCellsInCone(
|
|
3208
|
+
cells = getHexCellsInCone(center2, template.angle, radiusCells, cellSize, orientation);
|
|
3077
3209
|
break;
|
|
3078
3210
|
case "line":
|
|
3079
|
-
cells = getHexCellsInLine(
|
|
3211
|
+
cells = getHexCellsInLine(center2, template.angle, radiusCells, cellSize, orientation);
|
|
3080
3212
|
break;
|
|
3081
3213
|
case "square":
|
|
3082
|
-
cells = getHexCellsInSquare(
|
|
3214
|
+
cells = getHexCellsInSquare(center2, radiusCells, cellSize, orientation);
|
|
3083
3215
|
break;
|
|
3084
3216
|
}
|
|
3085
3217
|
ctx.save();
|
|
@@ -3100,7 +3232,7 @@ var ElementRenderer = class {
|
|
|
3100
3232
|
{
|
|
3101
3233
|
ctx.globalAlpha = Math.min(template.opacity + 0.1, 1);
|
|
3102
3234
|
ctx.beginPath();
|
|
3103
|
-
drawHexPath(ctx,
|
|
3235
|
+
drawHexPath(ctx, center2.x, center2.y, cellSize, orientation);
|
|
3104
3236
|
ctx.fillStyle = template.strokeColor;
|
|
3105
3237
|
ctx.fill();
|
|
3106
3238
|
ctx.strokeStyle = template.strokeColor;
|
|
@@ -3109,7 +3241,7 @@ var ElementRenderer = class {
|
|
|
3109
3241
|
}
|
|
3110
3242
|
if (template.templateShape === "circle" && template.radiusFeet != null && template.radiusFeet > 0) {
|
|
3111
3243
|
const r = template.radius;
|
|
3112
|
-
this.renderRadiusMarker(ctx,
|
|
3244
|
+
this.renderRadiusMarker(ctx, center2.x, center2.y, r, template.radiusFeet);
|
|
3113
3245
|
}
|
|
3114
3246
|
ctx.restore();
|
|
3115
3247
|
}
|
|
@@ -3739,6 +3871,87 @@ var NoteEditor = class {
|
|
|
3739
3871
|
}
|
|
3740
3872
|
};
|
|
3741
3873
|
|
|
3874
|
+
// src/canvas/context-menu.ts
|
|
3875
|
+
var ContextMenu = class {
|
|
3876
|
+
constructor(options) {
|
|
3877
|
+
this.options = options;
|
|
3878
|
+
}
|
|
3879
|
+
el = null;
|
|
3880
|
+
outsideListener = null;
|
|
3881
|
+
keyListener = null;
|
|
3882
|
+
isOpen() {
|
|
3883
|
+
return this.el !== null;
|
|
3884
|
+
}
|
|
3885
|
+
open(items, screenPos) {
|
|
3886
|
+
this.close();
|
|
3887
|
+
const el = document.createElement("div");
|
|
3888
|
+
el.className = "fieldnotes-context-menu";
|
|
3889
|
+
Object.assign(el.style, {
|
|
3890
|
+
position: "fixed",
|
|
3891
|
+
left: `${screenPos.x}px`,
|
|
3892
|
+
top: `${screenPos.y}px`,
|
|
3893
|
+
zIndex: "10000",
|
|
3894
|
+
display: "flex",
|
|
3895
|
+
flexDirection: "column"
|
|
3896
|
+
});
|
|
3897
|
+
for (const item of items) {
|
|
3898
|
+
const btn = document.createElement("button");
|
|
3899
|
+
btn.type = "button";
|
|
3900
|
+
btn.className = "fieldnotes-context-menu-item" + (item.disabled ? " fieldnotes-context-menu-item--disabled" : "");
|
|
3901
|
+
btn.textContent = item.label;
|
|
3902
|
+
if (item.disabled) {
|
|
3903
|
+
btn.disabled = true;
|
|
3904
|
+
} else {
|
|
3905
|
+
btn.addEventListener("click", () => {
|
|
3906
|
+
this.options.onCommand(item.action);
|
|
3907
|
+
this.close();
|
|
3908
|
+
});
|
|
3909
|
+
}
|
|
3910
|
+
el.appendChild(btn);
|
|
3911
|
+
}
|
|
3912
|
+
document.body.appendChild(el);
|
|
3913
|
+
this.el = el;
|
|
3914
|
+
this.clampToViewport(el, screenPos);
|
|
3915
|
+
this.keyListener = (e) => {
|
|
3916
|
+
if (e.key === "Escape") this.close();
|
|
3917
|
+
};
|
|
3918
|
+
document.addEventListener("keydown", this.keyListener);
|
|
3919
|
+
this.outsideListener = (e) => {
|
|
3920
|
+
if (this.el && !this.el.contains(e.target)) this.close();
|
|
3921
|
+
};
|
|
3922
|
+
setTimeout(() => {
|
|
3923
|
+
if (this.outsideListener) document.addEventListener("pointerdown", this.outsideListener);
|
|
3924
|
+
}, 0);
|
|
3925
|
+
}
|
|
3926
|
+
close() {
|
|
3927
|
+
if (this.keyListener) {
|
|
3928
|
+
document.removeEventListener("keydown", this.keyListener);
|
|
3929
|
+
this.keyListener = null;
|
|
3930
|
+
}
|
|
3931
|
+
if (this.outsideListener) {
|
|
3932
|
+
document.removeEventListener("pointerdown", this.outsideListener);
|
|
3933
|
+
this.outsideListener = null;
|
|
3934
|
+
}
|
|
3935
|
+
if (this.el) {
|
|
3936
|
+
this.el.remove();
|
|
3937
|
+
this.el = null;
|
|
3938
|
+
this.options.onClose();
|
|
3939
|
+
}
|
|
3940
|
+
}
|
|
3941
|
+
dispose() {
|
|
3942
|
+
this.close();
|
|
3943
|
+
}
|
|
3944
|
+
clampToViewport(el, screenPos) {
|
|
3945
|
+
const rect = el.getBoundingClientRect();
|
|
3946
|
+
if (rect.width > 0 && screenPos.x + rect.width > window.innerWidth) {
|
|
3947
|
+
el.style.left = `${Math.max(0, screenPos.x - rect.width)}px`;
|
|
3948
|
+
}
|
|
3949
|
+
if (rect.height > 0 && screenPos.y + rect.height > window.innerHeight) {
|
|
3950
|
+
el.style.top = `${Math.max(0, screenPos.y - rect.height)}px`;
|
|
3951
|
+
}
|
|
3952
|
+
}
|
|
3953
|
+
};
|
|
3954
|
+
|
|
3742
3955
|
// src/elements/translate.ts
|
|
3743
3956
|
function translateElementPatch(el, dx, dy) {
|
|
3744
3957
|
const position = { x: el.position.x + dx, y: el.position.y + dy };
|
|
@@ -4228,6 +4441,7 @@ function renderStyledRuns(ctx, runs, startX, startY, maxWidth) {
|
|
|
4228
4441
|
}
|
|
4229
4442
|
|
|
4230
4443
|
// src/canvas/export-image.ts
|
|
4444
|
+
var center = (b) => ({ x: b.x + b.w / 2, y: b.y + b.h / 2 });
|
|
4231
4445
|
function getStrokeBounds(el) {
|
|
4232
4446
|
if (el.type !== "stroke") return null;
|
|
4233
4447
|
if (el.points.length === 0) return null;
|
|
@@ -4253,8 +4467,10 @@ function getStrokeBounds(el) {
|
|
|
4253
4467
|
}
|
|
4254
4468
|
function getElementRect(el) {
|
|
4255
4469
|
switch (el.type) {
|
|
4256
|
-
case "stroke":
|
|
4257
|
-
|
|
4470
|
+
case "stroke": {
|
|
4471
|
+
const r = getStrokeBounds(el);
|
|
4472
|
+
return r ? rotatedAABB(r, el.rotation ?? 0) : r;
|
|
4473
|
+
}
|
|
4258
4474
|
case "arrow": {
|
|
4259
4475
|
const b = getArrowBounds(el.from, el.to, el.bend);
|
|
4260
4476
|
const pad = el.width / 2 + 14;
|
|
@@ -4273,7 +4489,10 @@ function getElementRect(el) {
|
|
|
4273
4489
|
case "text":
|
|
4274
4490
|
case "shape":
|
|
4275
4491
|
if ("size" in el) {
|
|
4276
|
-
return
|
|
4492
|
+
return rotatedAABB(
|
|
4493
|
+
{ x: el.position.x, y: el.position.y, w: el.size.w, h: el.size.h },
|
|
4494
|
+
el.rotation ?? 0
|
|
4495
|
+
);
|
|
4277
4496
|
}
|
|
4278
4497
|
return null;
|
|
4279
4498
|
default:
|
|
@@ -4411,11 +4630,13 @@ async function exportImage(store, options = {}, layerManager) {
|
|
|
4411
4630
|
continue;
|
|
4412
4631
|
}
|
|
4413
4632
|
if (el.type === "note") {
|
|
4414
|
-
|
|
4633
|
+
const b = getElementBounds(el);
|
|
4634
|
+
withRotation(ctx, el, b ? center(b) : el.position, () => renderNoteOnCanvas(ctx, el));
|
|
4415
4635
|
continue;
|
|
4416
4636
|
}
|
|
4417
4637
|
if (el.type === "text") {
|
|
4418
|
-
|
|
4638
|
+
const b = getElementBounds(el);
|
|
4639
|
+
withRotation(ctx, el, b ? center(b) : el.position, () => renderTextOnCanvas(ctx, el));
|
|
4419
4640
|
continue;
|
|
4420
4641
|
}
|
|
4421
4642
|
if (el.type === "html") {
|
|
@@ -4424,7 +4645,13 @@ async function exportImage(store, options = {}, layerManager) {
|
|
|
4424
4645
|
if (el.type === "image") {
|
|
4425
4646
|
const img = imageCache.get(el.id);
|
|
4426
4647
|
if (img) {
|
|
4427
|
-
|
|
4648
|
+
const b = getElementBounds(el);
|
|
4649
|
+
withRotation(
|
|
4650
|
+
ctx,
|
|
4651
|
+
el,
|
|
4652
|
+
b ? center(b) : el.position,
|
|
4653
|
+
() => ctx.drawImage(img, el.position.x, el.position.y, el.size.w, el.size.h)
|
|
4654
|
+
);
|
|
4428
4655
|
}
|
|
4429
4656
|
continue;
|
|
4430
4657
|
}
|
|
@@ -4760,7 +4987,9 @@ var DomNodeManager = class {
|
|
|
4760
4987
|
top: `${element.position.y}px`,
|
|
4761
4988
|
width: size ? `${size.w}px` : "auto",
|
|
4762
4989
|
height: size ? `${size.h}px` : "auto",
|
|
4763
|
-
zIndex: String(zIndex)
|
|
4990
|
+
zIndex: String(zIndex),
|
|
4991
|
+
transform: element.rotation ? `rotate(${element.rotation}rad)` : "",
|
|
4992
|
+
transformOrigin: "50% 50%"
|
|
4764
4993
|
});
|
|
4765
4994
|
this.renderDomContent(node, element);
|
|
4766
4995
|
}
|
|
@@ -5539,8 +5768,20 @@ var Viewport = class {
|
|
|
5539
5768
|
fitToContent: () => this.fitToContent(),
|
|
5540
5769
|
group: () => this.groupSelection(),
|
|
5541
5770
|
ungroup: () => this.ungroupSelection(),
|
|
5771
|
+
toggleLock: () => this.toggleLockSelection(),
|
|
5772
|
+
openContextMenu: (screenPos, world) => {
|
|
5773
|
+
this.getSelectTool()?.selectAtPoint(world, this.toolContext);
|
|
5774
|
+
this.openContextMenu(screenPos);
|
|
5775
|
+
},
|
|
5542
5776
|
shortcuts: options.shortcuts
|
|
5543
5777
|
});
|
|
5778
|
+
if (options.contextMenu !== false) {
|
|
5779
|
+
this.contextMenu = new ContextMenu({
|
|
5780
|
+
onCommand: (action) => this.runAction(action),
|
|
5781
|
+
onClose: noop
|
|
5782
|
+
});
|
|
5783
|
+
}
|
|
5784
|
+
this.unsubToolChange = this.toolManager.onChange(() => this.contextMenu?.close());
|
|
5544
5785
|
this.domNodeManager = new DomNodeManager({
|
|
5545
5786
|
domLayer: this.domLayer,
|
|
5546
5787
|
onEditRequest: (id) => this.startEditingElement(id),
|
|
@@ -5572,6 +5813,7 @@ var Viewport = class {
|
|
|
5572
5813
|
this.unsubCamera = this.camera.onChange(() => {
|
|
5573
5814
|
this.applyCameraTransform();
|
|
5574
5815
|
this.noteEditor.updateToolbarPosition();
|
|
5816
|
+
this.contextMenu?.close();
|
|
5575
5817
|
this.requestRender();
|
|
5576
5818
|
});
|
|
5577
5819
|
this.unsubStore = [
|
|
@@ -5624,6 +5866,7 @@ var Viewport = class {
|
|
|
5624
5866
|
canvasEl;
|
|
5625
5867
|
wrapper;
|
|
5626
5868
|
unsubCamera;
|
|
5869
|
+
unsubToolChange;
|
|
5627
5870
|
unsubStore;
|
|
5628
5871
|
inputHandler;
|
|
5629
5872
|
background;
|
|
@@ -5646,6 +5889,7 @@ var Viewport = class {
|
|
|
5646
5889
|
doubleTapDetector = new DoubleTapDetector();
|
|
5647
5890
|
tapDownX = 0;
|
|
5648
5891
|
tapDownY = 0;
|
|
5892
|
+
contextMenu = null;
|
|
5649
5893
|
get ctx() {
|
|
5650
5894
|
return this.canvasEl.getContext("2d");
|
|
5651
5895
|
}
|
|
@@ -5836,6 +6080,34 @@ var Viewport = class {
|
|
|
5836
6080
|
getSelectedIds() {
|
|
5837
6081
|
return this.getSelectTool()?.selectedIds ?? EMPTY_IDS;
|
|
5838
6082
|
}
|
|
6083
|
+
runAction(action) {
|
|
6084
|
+
this.inputHandler.runAction(action);
|
|
6085
|
+
}
|
|
6086
|
+
canPaste() {
|
|
6087
|
+
return this.inputHandler.hasClipboard();
|
|
6088
|
+
}
|
|
6089
|
+
openContextMenu(screenPos) {
|
|
6090
|
+
if (!this.contextMenu) return;
|
|
6091
|
+
const ids = this.getSelectedIds();
|
|
6092
|
+
const items = [];
|
|
6093
|
+
if (ids.length > 0) {
|
|
6094
|
+
items.push({ label: "Cut", action: "cut" });
|
|
6095
|
+
items.push({ label: "Copy", action: "copy" });
|
|
6096
|
+
if (this.canPaste()) items.push({ label: "Paste", action: "paste" });
|
|
6097
|
+
items.push({ label: "Duplicate", action: "duplicate" });
|
|
6098
|
+
items.push({ label: "Delete", action: "delete" });
|
|
6099
|
+
items.push({ label: "Bring to Front", action: "z-front" });
|
|
6100
|
+
items.push({ label: "Bring Forward", action: "z-forward" });
|
|
6101
|
+
items.push({ label: "Send Backward", action: "z-backward" });
|
|
6102
|
+
items.push({ label: "Send to Back", action: "z-back" });
|
|
6103
|
+
const allLocked = ids.every((id) => this.store.getById(id)?.locked);
|
|
6104
|
+
items.push({ label: allLocked ? "Unlock" : "Lock", action: "toggle-lock" });
|
|
6105
|
+
} else if (this.canPaste()) {
|
|
6106
|
+
items.push({ label: "Paste", action: "paste" });
|
|
6107
|
+
}
|
|
6108
|
+
if (items.length === 0) return;
|
|
6109
|
+
this.contextMenu.open(items, screenPos);
|
|
6110
|
+
}
|
|
5839
6111
|
onSelectionChange(listener) {
|
|
5840
6112
|
const tool = this.getSelectTool();
|
|
5841
6113
|
return tool ? tool.onSelectionChange(listener) : noop;
|
|
@@ -5896,6 +6168,20 @@ var Viewport = class {
|
|
|
5896
6168
|
}
|
|
5897
6169
|
this.historyRecorder.commit();
|
|
5898
6170
|
}
|
|
6171
|
+
toggleLockSelection() {
|
|
6172
|
+
const ids = this.getSelectedIds();
|
|
6173
|
+
if (ids.length === 0) return;
|
|
6174
|
+
const anyUnlocked = ids.some((id) => {
|
|
6175
|
+
const el = this.store.getById(id);
|
|
6176
|
+
return el ? !el.locked : false;
|
|
6177
|
+
});
|
|
6178
|
+
this.historyRecorder.begin();
|
|
6179
|
+
for (const id of ids) {
|
|
6180
|
+
const el = this.store.getById(id);
|
|
6181
|
+
if (el && el.locked !== anyUnlocked) this.store.update(id, { locked: anyUnlocked });
|
|
6182
|
+
}
|
|
6183
|
+
this.historyRecorder.commit();
|
|
6184
|
+
}
|
|
5899
6185
|
alignSelection(edge) {
|
|
5900
6186
|
const bounded = this.boundedSelection();
|
|
5901
6187
|
if (bounded.length < 2) return;
|
|
@@ -5937,13 +6223,13 @@ var Viewport = class {
|
|
|
5937
6223
|
distributeSelection(axis) {
|
|
5938
6224
|
const bounded = this.boundedSelection();
|
|
5939
6225
|
if (bounded.length < 3) return;
|
|
5940
|
-
const
|
|
5941
|
-
const sorted = [...bounded].sort((p, q) =>
|
|
6226
|
+
const center2 = (b) => axis === "horizontal" ? b.x + b.w / 2 : b.y + b.h / 2;
|
|
6227
|
+
const sorted = [...bounded].sort((p, q) => center2(p.bounds) - center2(q.bounds));
|
|
5942
6228
|
const first = sorted[0];
|
|
5943
6229
|
const last = sorted[sorted.length - 1];
|
|
5944
6230
|
if (!first || !last) return;
|
|
5945
|
-
const c0 =
|
|
5946
|
-
const cN =
|
|
6231
|
+
const c0 = center2(first.bounds);
|
|
6232
|
+
const cN = center2(last.bounds);
|
|
5947
6233
|
const n = sorted.length;
|
|
5948
6234
|
this.historyRecorder.begin();
|
|
5949
6235
|
const moved = [];
|
|
@@ -5951,7 +6237,7 @@ var Viewport = class {
|
|
|
5951
6237
|
const item = sorted[i];
|
|
5952
6238
|
if (!item || !this.isMovable(item.el)) continue;
|
|
5953
6239
|
const target = c0 + i * (cN - c0) / (n - 1);
|
|
5954
|
-
const delta = target -
|
|
6240
|
+
const delta = target - center2(item.bounds);
|
|
5955
6241
|
if (delta === 0) continue;
|
|
5956
6242
|
const [dx, dy] = axis === "horizontal" ? [delta, 0] : [0, delta];
|
|
5957
6243
|
this.store.update(item.id, translateElementPatch(item.el, dx, dy));
|
|
@@ -5994,12 +6280,14 @@ var Viewport = class {
|
|
|
5994
6280
|
this.noteEditor.destroy(this.store);
|
|
5995
6281
|
this.arrowLabelEditor.cancel();
|
|
5996
6282
|
this.historyRecorder.destroy();
|
|
6283
|
+
this.contextMenu?.dispose();
|
|
5997
6284
|
this.wrapper.removeEventListener("pointerdown", this.onTapDown);
|
|
5998
6285
|
this.wrapper.removeEventListener("pointerup", this.onDoubleTap);
|
|
5999
6286
|
this.wrapper.removeEventListener("dragover", this.onDragOver);
|
|
6000
6287
|
this.wrapper.removeEventListener("drop", this.onDrop);
|
|
6001
6288
|
this.inputHandler.destroy();
|
|
6002
6289
|
this.unsubCamera();
|
|
6290
|
+
this.unsubToolChange();
|
|
6003
6291
|
this.unsubStore.forEach((fn) => fn());
|
|
6004
6292
|
this.resizeObserver?.disconnect();
|
|
6005
6293
|
this.resizeObserver = null;
|
|
@@ -6674,10 +6962,10 @@ function applyArrowHandleDrag(handle, elementId, world, ctx) {
|
|
|
6674
6962
|
const excludeId = el.toBinding?.elementId;
|
|
6675
6963
|
const target = findBindTarget(world, ctx.store, threshold, excludeId, layerFilter);
|
|
6676
6964
|
if (target) {
|
|
6677
|
-
const
|
|
6965
|
+
const center2 = getElementCenter(target);
|
|
6678
6966
|
ctx.store.update(elementId, {
|
|
6679
|
-
from:
|
|
6680
|
-
position:
|
|
6967
|
+
from: center2,
|
|
6968
|
+
position: center2,
|
|
6681
6969
|
fromBinding: { elementId: target.id }
|
|
6682
6970
|
});
|
|
6683
6971
|
} else {
|
|
@@ -6693,9 +6981,9 @@ function applyArrowHandleDrag(handle, elementId, world, ctx) {
|
|
|
6693
6981
|
const excludeId = el.fromBinding?.elementId;
|
|
6694
6982
|
const target = findBindTarget(world, ctx.store, threshold, excludeId, layerFilter);
|
|
6695
6983
|
if (target) {
|
|
6696
|
-
const
|
|
6984
|
+
const center2 = getElementCenter(target);
|
|
6697
6985
|
ctx.store.update(elementId, {
|
|
6698
|
-
to:
|
|
6986
|
+
to: center2,
|
|
6699
6987
|
toBinding: { elementId: target.id }
|
|
6700
6988
|
});
|
|
6701
6989
|
} else {
|
|
@@ -6784,6 +7072,9 @@ var SNAP_PX = 6;
|
|
|
6784
7072
|
var HANDLE_HIT_PADDING2 = 4;
|
|
6785
7073
|
var SELECTION_PAD = 4;
|
|
6786
7074
|
var MIN_ELEMENT_SIZE = 20;
|
|
7075
|
+
var ROTATE_HANDLE_OFFSET = 24;
|
|
7076
|
+
var ROTATE_SNAP = Math.PI / 12;
|
|
7077
|
+
var ROTATABLE_TYPES = /* @__PURE__ */ new Set(["note", "text", "image", "html", "shape", "stroke"]);
|
|
6787
7078
|
var HANDLE_CURSORS = {
|
|
6788
7079
|
nw: "nwse-resize",
|
|
6789
7080
|
se: "nwse-resize",
|
|
@@ -6824,6 +7115,15 @@ var SelectTool = class {
|
|
|
6824
7115
|
this.setSelectedIds(ids);
|
|
6825
7116
|
this.ctx?.requestRender();
|
|
6826
7117
|
}
|
|
7118
|
+
selectAtPoint(world, ctx) {
|
|
7119
|
+
const hit = this.hitTest(world, ctx);
|
|
7120
|
+
if (!hit) {
|
|
7121
|
+
this.setSelectedIds([]);
|
|
7122
|
+
return;
|
|
7123
|
+
}
|
|
7124
|
+
if (this._selectedIds.includes(hit.id)) return;
|
|
7125
|
+
this.setSelectedIds(expandToGroups([hit.id], ctx.store.getAll()));
|
|
7126
|
+
}
|
|
6827
7127
|
get isMarqueeActive() {
|
|
6828
7128
|
return this.mode.type === "marquee";
|
|
6829
7129
|
}
|
|
@@ -6872,6 +7172,22 @@ var SelectTool = class {
|
|
|
6872
7172
|
ctx.requestRender();
|
|
6873
7173
|
return;
|
|
6874
7174
|
}
|
|
7175
|
+
const rotateHit = this.hitTestRotateHandle(world, ctx);
|
|
7176
|
+
if (rotateHit) {
|
|
7177
|
+
const el = ctx.store.getById(rotateHit.elementId);
|
|
7178
|
+
const layout = el ? this.getOverlayLayout(el, ctx.camera.zoom) : null;
|
|
7179
|
+
if (el && layout) {
|
|
7180
|
+
this.mode = {
|
|
7181
|
+
type: "rotating",
|
|
7182
|
+
elementId: rotateHit.elementId,
|
|
7183
|
+
center: layout.center,
|
|
7184
|
+
startPointerAngle: Math.atan2(world.y - layout.center.y, world.x - layout.center.x),
|
|
7185
|
+
startRotation: el.rotation ?? 0
|
|
7186
|
+
};
|
|
7187
|
+
ctx.requestRender();
|
|
7188
|
+
return;
|
|
7189
|
+
}
|
|
7190
|
+
}
|
|
6875
7191
|
const resizeHit = this.hitTestResizeHandle(world, ctx);
|
|
6876
7192
|
if (resizeHit) {
|
|
6877
7193
|
const el = ctx.store.getById(resizeHit.elementId);
|
|
@@ -6937,6 +7253,15 @@ var SelectTool = class {
|
|
|
6937
7253
|
this.handleTemplateResize(world, ctx);
|
|
6938
7254
|
return;
|
|
6939
7255
|
}
|
|
7256
|
+
if (this.mode.type === "rotating") {
|
|
7257
|
+
const { elementId, center: center2, startPointerAngle, startRotation } = this.mode;
|
|
7258
|
+
const a = Math.atan2(world.y - center2.y, world.x - center2.x);
|
|
7259
|
+
let next = startRotation + (a - startPointerAngle);
|
|
7260
|
+
if (state.shiftKey) next = Math.round(next / ROTATE_SNAP) * ROTATE_SNAP;
|
|
7261
|
+
ctx.store.update(elementId, { rotation: normalizeAngle(next) });
|
|
7262
|
+
ctx.requestRender();
|
|
7263
|
+
return;
|
|
7264
|
+
}
|
|
6940
7265
|
if (this.mode.type === "resizing") {
|
|
6941
7266
|
ctx.setCursor?.(HANDLE_CURSORS[this.mode.handle]);
|
|
6942
7267
|
this.handleResize(world, ctx, state.shiftKey);
|
|
@@ -7139,6 +7464,10 @@ var SelectTool = class {
|
|
|
7139
7464
|
ctx.setCursor?.("nwse-resize");
|
|
7140
7465
|
return null;
|
|
7141
7466
|
}
|
|
7467
|
+
if (this.hitTestRotateHandle(world, ctx)) {
|
|
7468
|
+
ctx.setCursor?.("grab");
|
|
7469
|
+
return null;
|
|
7470
|
+
}
|
|
7142
7471
|
const resizeHit = this.hitTestResizeHandle(world, ctx);
|
|
7143
7472
|
if (resizeHit) {
|
|
7144
7473
|
ctx.setCursor?.(HANDLE_CURSORS[resizeHit.handle]);
|
|
@@ -7157,6 +7486,11 @@ var SelectTool = class {
|
|
|
7157
7486
|
if (this.mode.type !== "resizing") return;
|
|
7158
7487
|
const el = ctx.store.getById(this.mode.elementId);
|
|
7159
7488
|
if (!el || !("size" in el) || el.locked) return;
|
|
7489
|
+
const angle = el.rotation ?? 0;
|
|
7490
|
+
if (angle !== 0) {
|
|
7491
|
+
this.handleRotatedResize(world, el, angle, ctx, shiftKey);
|
|
7492
|
+
return;
|
|
7493
|
+
}
|
|
7160
7494
|
const { handle } = this.mode;
|
|
7161
7495
|
const dx = world.x - this.lastWorld.x;
|
|
7162
7496
|
const dy = world.y - this.lastWorld.y;
|
|
@@ -7214,6 +7548,78 @@ var SelectTool = class {
|
|
|
7214
7548
|
this.updateArrowsBoundTo([this.mode.elementId], ctx);
|
|
7215
7549
|
ctx.requestRender();
|
|
7216
7550
|
}
|
|
7551
|
+
anchorOffset(handle, w, h) {
|
|
7552
|
+
switch (handle) {
|
|
7553
|
+
case "se":
|
|
7554
|
+
return { x: -w / 2, y: -h / 2 };
|
|
7555
|
+
case "sw":
|
|
7556
|
+
return { x: w / 2, y: -h / 2 };
|
|
7557
|
+
case "ne":
|
|
7558
|
+
return { x: -w / 2, y: h / 2 };
|
|
7559
|
+
case "nw":
|
|
7560
|
+
return { x: w / 2, y: h / 2 };
|
|
7561
|
+
default:
|
|
7562
|
+
return { x: 0, y: 0 };
|
|
7563
|
+
}
|
|
7564
|
+
}
|
|
7565
|
+
handleRotatedResize(world, el, angle, ctx, shiftKey) {
|
|
7566
|
+
if (this.mode.type !== "resizing") return;
|
|
7567
|
+
const { handle } = this.mode;
|
|
7568
|
+
const wdx = world.x - this.lastWorld.x;
|
|
7569
|
+
const wdy = world.y - this.lastWorld.y;
|
|
7570
|
+
this.lastWorld = world;
|
|
7571
|
+
const cosN = Math.cos(-angle);
|
|
7572
|
+
const sinN = Math.sin(-angle);
|
|
7573
|
+
const ldx = wdx * cosN - wdy * sinN;
|
|
7574
|
+
const ldy = wdx * sinN + wdy * cosN;
|
|
7575
|
+
let w = el.size.w;
|
|
7576
|
+
let h = el.size.h;
|
|
7577
|
+
switch (handle) {
|
|
7578
|
+
case "se":
|
|
7579
|
+
w += ldx;
|
|
7580
|
+
h += ldy;
|
|
7581
|
+
break;
|
|
7582
|
+
case "sw":
|
|
7583
|
+
w -= ldx;
|
|
7584
|
+
h += ldy;
|
|
7585
|
+
break;
|
|
7586
|
+
case "ne":
|
|
7587
|
+
w += ldx;
|
|
7588
|
+
h -= ldy;
|
|
7589
|
+
break;
|
|
7590
|
+
case "nw":
|
|
7591
|
+
w -= ldx;
|
|
7592
|
+
h -= ldy;
|
|
7593
|
+
break;
|
|
7594
|
+
}
|
|
7595
|
+
if (shiftKey && this.resizeAspectRatio > 0) {
|
|
7596
|
+
const absDw = Math.abs(w - el.size.w);
|
|
7597
|
+
const absDh = Math.abs(h - el.size.h);
|
|
7598
|
+
if (absDw >= absDh) h = w / this.resizeAspectRatio;
|
|
7599
|
+
else w = h * this.resizeAspectRatio;
|
|
7600
|
+
}
|
|
7601
|
+
w = Math.max(w, MIN_ELEMENT_SIZE);
|
|
7602
|
+
h = Math.max(h, MIN_ELEMENT_SIZE);
|
|
7603
|
+
const oldCenter = { x: el.position.x + el.size.w / 2, y: el.position.y + el.size.h / 2 };
|
|
7604
|
+
const oldAnchorLocal = this.anchorOffset(handle, el.size.w, el.size.h);
|
|
7605
|
+
const anchorWorld = rotatePoint(
|
|
7606
|
+
{ x: oldCenter.x + oldAnchorLocal.x, y: oldCenter.y + oldAnchorLocal.y },
|
|
7607
|
+
oldCenter,
|
|
7608
|
+
angle
|
|
7609
|
+
);
|
|
7610
|
+
const newAnchorLocal = this.anchorOffset(handle, w, h);
|
|
7611
|
+
const cos = Math.cos(angle);
|
|
7612
|
+
const sin = Math.sin(angle);
|
|
7613
|
+
const rotatedAnchor = {
|
|
7614
|
+
x: newAnchorLocal.x * cos - newAnchorLocal.y * sin,
|
|
7615
|
+
y: newAnchorLocal.x * sin + newAnchorLocal.y * cos
|
|
7616
|
+
};
|
|
7617
|
+
const newCenter = { x: anchorWorld.x - rotatedAnchor.x, y: anchorWorld.y - rotatedAnchor.y };
|
|
7618
|
+
const position = { x: newCenter.x - w / 2, y: newCenter.y - h / 2 };
|
|
7619
|
+
ctx.store.update(this.mode.elementId, { position, size: { w, h } });
|
|
7620
|
+
this.updateArrowsBoundTo([this.mode.elementId], ctx);
|
|
7621
|
+
ctx.requestRender();
|
|
7622
|
+
}
|
|
7217
7623
|
hitTestResizeHandle(world, ctx) {
|
|
7218
7624
|
if (this._selectedIds.length === 0) return null;
|
|
7219
7625
|
const zoom = ctx.camera.zoom;
|
|
@@ -7221,11 +7627,11 @@ var SelectTool = class {
|
|
|
7221
7627
|
for (const id of this._selectedIds) {
|
|
7222
7628
|
const el = ctx.store.getById(id);
|
|
7223
7629
|
if (!el || !("size" in el)) continue;
|
|
7630
|
+
if (el.locked) continue;
|
|
7224
7631
|
if (el.type === "shape" && el.shape === "line") continue;
|
|
7225
|
-
const
|
|
7226
|
-
if (!
|
|
7227
|
-
const
|
|
7228
|
-
for (const [handle, pos] of corners) {
|
|
7632
|
+
const layout = this.getOverlayLayout(el, zoom);
|
|
7633
|
+
if (!layout) continue;
|
|
7634
|
+
for (const [handle, pos] of layout.corners) {
|
|
7229
7635
|
if (Math.abs(world.x - pos.x) <= handleHalf && Math.abs(world.y - pos.y) <= handleHalf) {
|
|
7230
7636
|
return { elementId: id, handle };
|
|
7231
7637
|
}
|
|
@@ -7233,6 +7639,19 @@ var SelectTool = class {
|
|
|
7233
7639
|
}
|
|
7234
7640
|
return null;
|
|
7235
7641
|
}
|
|
7642
|
+
hitTestRotateHandle(world, ctx) {
|
|
7643
|
+
if (this._selectedIds.length !== 1) return null;
|
|
7644
|
+
const id = this._selectedIds[0];
|
|
7645
|
+
if (!id) return null;
|
|
7646
|
+
const el = ctx.store.getById(id);
|
|
7647
|
+
if (!el || el.locked || !ROTATABLE_TYPES.has(el.type)) return null;
|
|
7648
|
+
const layout = this.getOverlayLayout(el, ctx.camera.zoom);
|
|
7649
|
+
if (!layout) return null;
|
|
7650
|
+
const r = (HANDLE_SIZE / 2 + HANDLE_HIT_PADDING2) / ctx.camera.zoom;
|
|
7651
|
+
const dx = world.x - layout.rotateHandle.x;
|
|
7652
|
+
const dy = world.y - layout.rotateHandle.y;
|
|
7653
|
+
return dx * dx + dy * dy <= r * r ? { elementId: id } : null;
|
|
7654
|
+
}
|
|
7236
7655
|
hitTestLineHandles(world, ctx) {
|
|
7237
7656
|
if (this._selectedIds.length === 0) return null;
|
|
7238
7657
|
const zoom = ctx.camera.zoom;
|
|
@@ -7255,6 +7674,30 @@ var SelectTool = class {
|
|
|
7255
7674
|
["se", { x: bounds.x + bounds.w, y: bounds.y + bounds.h }]
|
|
7256
7675
|
];
|
|
7257
7676
|
}
|
|
7677
|
+
getOverlayLayout(el, zoom) {
|
|
7678
|
+
const bounds = getElementBounds(el);
|
|
7679
|
+
if (!bounds) return null;
|
|
7680
|
+
const angle = el.rotation ?? 0;
|
|
7681
|
+
const pad = SELECTION_PAD / zoom;
|
|
7682
|
+
const center2 = { x: bounds.x + bounds.w / 2, y: bounds.y + bounds.h / 2 };
|
|
7683
|
+
const raw = [
|
|
7684
|
+
["nw", { x: bounds.x - pad, y: bounds.y - pad }],
|
|
7685
|
+
["ne", { x: bounds.x + bounds.w + pad, y: bounds.y - pad }],
|
|
7686
|
+
["sw", { x: bounds.x - pad, y: bounds.y + bounds.h + pad }],
|
|
7687
|
+
["se", { x: bounds.x + bounds.w + pad, y: bounds.y + bounds.h + pad }]
|
|
7688
|
+
];
|
|
7689
|
+
const corners = raw.map(
|
|
7690
|
+
([h, p]) => [h, rotatePoint(p, center2, angle)]
|
|
7691
|
+
);
|
|
7692
|
+
const topMid = { x: center2.x, y: bounds.y - pad - ROTATE_HANDLE_OFFSET / zoom };
|
|
7693
|
+
const rotateHandle = rotatePoint(topMid, center2, angle);
|
|
7694
|
+
return { center: center2, corners, rotateHandle, angle };
|
|
7695
|
+
}
|
|
7696
|
+
topMidpoint(layout) {
|
|
7697
|
+
const nw = layout.corners.find(([h]) => h === "nw")?.[1] ?? { x: 0, y: 0 };
|
|
7698
|
+
const ne = layout.corners.find(([h]) => h === "ne")?.[1] ?? { x: 0, y: 0 };
|
|
7699
|
+
return { x: (nw.x + ne.x) / 2, y: (nw.y + ne.y) / 2 };
|
|
7700
|
+
}
|
|
7258
7701
|
renderMarquee(canvasCtx) {
|
|
7259
7702
|
if (this.mode.type !== "marquee") return;
|
|
7260
7703
|
const rect = this.getMarqueeRect();
|
|
@@ -7299,49 +7742,110 @@ var SelectTool = class {
|
|
|
7299
7742
|
}
|
|
7300
7743
|
const bounds = getElementBounds(el);
|
|
7301
7744
|
if (!bounds) continue;
|
|
7745
|
+
const layout = this.getOverlayLayout(el, zoom);
|
|
7746
|
+
if (!layout) continue;
|
|
7302
7747
|
const pad = SELECTION_PAD / zoom;
|
|
7303
|
-
|
|
7304
|
-
|
|
7305
|
-
|
|
7306
|
-
|
|
7307
|
-
|
|
7308
|
-
|
|
7748
|
+
if (layout.angle === 0) {
|
|
7749
|
+
canvasCtx.strokeRect(
|
|
7750
|
+
bounds.x - pad,
|
|
7751
|
+
bounds.y - pad,
|
|
7752
|
+
bounds.w + pad * 2,
|
|
7753
|
+
bounds.h + pad * 2
|
|
7754
|
+
);
|
|
7755
|
+
} else {
|
|
7756
|
+
const ordered = ["nw", "ne", "se", "sw"].map((h) => layout.corners.find(([c]) => c === h)?.[1]).filter((p) => !!p);
|
|
7757
|
+
const [p0, ...others] = ordered;
|
|
7758
|
+
if (p0) {
|
|
7759
|
+
canvasCtx.beginPath();
|
|
7760
|
+
canvasCtx.moveTo(p0.x, p0.y);
|
|
7761
|
+
for (const p of others) canvasCtx.lineTo(p.x, p.y);
|
|
7762
|
+
canvasCtx.closePath();
|
|
7763
|
+
canvasCtx.stroke();
|
|
7764
|
+
}
|
|
7765
|
+
}
|
|
7766
|
+
if (!el.locked) {
|
|
7767
|
+
if ("size" in el) {
|
|
7768
|
+
canvasCtx.setLineDash([]);
|
|
7769
|
+
canvasCtx.fillStyle = "#ffffff";
|
|
7770
|
+
const corners = layout.angle === 0 ? this.getHandlePositions(bounds) : layout.corners;
|
|
7771
|
+
for (const [, pos] of corners) {
|
|
7772
|
+
canvasCtx.fillRect(
|
|
7773
|
+
pos.x - handleWorldSize / 2,
|
|
7774
|
+
pos.y - handleWorldSize / 2,
|
|
7775
|
+
handleWorldSize,
|
|
7776
|
+
handleWorldSize
|
|
7777
|
+
);
|
|
7778
|
+
canvasCtx.strokeRect(
|
|
7779
|
+
pos.x - handleWorldSize / 2,
|
|
7780
|
+
pos.y - handleWorldSize / 2,
|
|
7781
|
+
handleWorldSize,
|
|
7782
|
+
handleWorldSize
|
|
7783
|
+
);
|
|
7784
|
+
}
|
|
7785
|
+
canvasCtx.setLineDash([4 / zoom, 4 / zoom]);
|
|
7786
|
+
} else if (el.type === "template") {
|
|
7787
|
+
canvasCtx.setLineDash([]);
|
|
7788
|
+
canvasCtx.fillStyle = "#ffffff";
|
|
7789
|
+
const hx = bounds.x + bounds.w;
|
|
7790
|
+
const hy = bounds.y + bounds.h;
|
|
7309
7791
|
canvasCtx.fillRect(
|
|
7310
|
-
|
|
7311
|
-
|
|
7792
|
+
hx - handleWorldSize / 2,
|
|
7793
|
+
hy - handleWorldSize / 2,
|
|
7312
7794
|
handleWorldSize,
|
|
7313
7795
|
handleWorldSize
|
|
7314
7796
|
);
|
|
7315
7797
|
canvasCtx.strokeRect(
|
|
7316
|
-
|
|
7317
|
-
|
|
7798
|
+
hx - handleWorldSize / 2,
|
|
7799
|
+
hy - handleWorldSize / 2,
|
|
7318
7800
|
handleWorldSize,
|
|
7319
7801
|
handleWorldSize
|
|
7320
7802
|
);
|
|
7803
|
+
canvasCtx.setLineDash([4 / zoom, 4 / zoom]);
|
|
7804
|
+
}
|
|
7805
|
+
if (this._selectedIds.length === 1 && ROTATABLE_TYPES.has(el.type)) {
|
|
7806
|
+
const stemStart = this.topMidpoint(layout);
|
|
7807
|
+
const stemEnd = layout.rotateHandle;
|
|
7808
|
+
canvasCtx.beginPath();
|
|
7809
|
+
canvasCtx.moveTo(stemStart.x, stemStart.y);
|
|
7810
|
+
canvasCtx.lineTo(stemEnd.x, stemEnd.y);
|
|
7811
|
+
canvasCtx.stroke();
|
|
7812
|
+
canvasCtx.setLineDash([]);
|
|
7813
|
+
canvasCtx.fillStyle = "#ffffff";
|
|
7814
|
+
canvasCtx.beginPath();
|
|
7815
|
+
canvasCtx.arc(stemEnd.x, stemEnd.y, handleWorldSize / 2, 0, Math.PI * 2);
|
|
7816
|
+
canvasCtx.fill();
|
|
7817
|
+
canvasCtx.stroke();
|
|
7818
|
+
canvasCtx.setLineDash([4 / zoom, 4 / zoom]);
|
|
7321
7819
|
}
|
|
7322
|
-
|
|
7323
|
-
|
|
7324
|
-
|
|
7325
|
-
canvasCtx
|
|
7326
|
-
const hx = bounds.x + bounds.w;
|
|
7327
|
-
const hy = bounds.y + bounds.h;
|
|
7328
|
-
canvasCtx.fillRect(
|
|
7329
|
-
hx - handleWorldSize / 2,
|
|
7330
|
-
hy - handleWorldSize / 2,
|
|
7331
|
-
handleWorldSize,
|
|
7332
|
-
handleWorldSize
|
|
7333
|
-
);
|
|
7334
|
-
canvasCtx.strokeRect(
|
|
7335
|
-
hx - handleWorldSize / 2,
|
|
7336
|
-
hy - handleWorldSize / 2,
|
|
7337
|
-
handleWorldSize,
|
|
7338
|
-
handleWorldSize
|
|
7339
|
-
);
|
|
7340
|
-
canvasCtx.setLineDash([4 / zoom, 4 / zoom]);
|
|
7820
|
+
}
|
|
7821
|
+
if (el.locked) {
|
|
7822
|
+
const ne = layout.corners.find(([h]) => h === "ne")?.[1];
|
|
7823
|
+
if (ne) this.drawLockBadge(canvasCtx, ne, zoom);
|
|
7341
7824
|
}
|
|
7342
7825
|
}
|
|
7343
7826
|
canvasCtx.restore();
|
|
7344
7827
|
}
|
|
7828
|
+
drawLockBadge(ctx, at, zoom) {
|
|
7829
|
+
const r = 9 / zoom;
|
|
7830
|
+
ctx.save();
|
|
7831
|
+
ctx.setLineDash([]);
|
|
7832
|
+
ctx.beginPath();
|
|
7833
|
+
ctx.arc(at.x, at.y, r, 0, Math.PI * 2);
|
|
7834
|
+
ctx.fillStyle = "#ffffff";
|
|
7835
|
+
ctx.fill();
|
|
7836
|
+
ctx.strokeStyle = "#2196F3";
|
|
7837
|
+
ctx.lineWidth = 1.5 / zoom;
|
|
7838
|
+
ctx.stroke();
|
|
7839
|
+
const bw = 8 / zoom;
|
|
7840
|
+
const bh = 6 / zoom;
|
|
7841
|
+
ctx.fillStyle = "#2196F3";
|
|
7842
|
+
ctx.fillRect(at.x - bw / 2, at.y - bh / 2 + 1 / zoom, bw, bh);
|
|
7843
|
+
ctx.beginPath();
|
|
7844
|
+
ctx.arc(at.x, at.y - bh / 2 + 1 / zoom, 2.5 / zoom, Math.PI, 0);
|
|
7845
|
+
ctx.lineWidth = 1.4 / zoom;
|
|
7846
|
+
ctx.stroke();
|
|
7847
|
+
ctx.restore();
|
|
7848
|
+
}
|
|
7345
7849
|
renderBindingHighlights(canvasCtx, arrow, zoom) {
|
|
7346
7850
|
if (!this.ctx) return;
|
|
7347
7851
|
if (!arrow.fromBinding && !arrow.toBinding) return;
|
|
@@ -7418,7 +7922,7 @@ var SelectTool = class {
|
|
|
7418
7922
|
if (ctx.isLayerLocked && ctx.isLayerLocked(el.layerId)) continue;
|
|
7419
7923
|
if (el.type === "grid") continue;
|
|
7420
7924
|
const bounds = getElementBounds(el);
|
|
7421
|
-
if (bounds && this.rectsOverlap(marquee, bounds)) {
|
|
7925
|
+
if (bounds && this.rectsOverlap(marquee, rotatedAABB(bounds, el.rotation ?? 0))) {
|
|
7422
7926
|
ids.push(el.id);
|
|
7423
7927
|
}
|
|
7424
7928
|
}
|
|
@@ -7440,6 +7944,13 @@ var SelectTool = class {
|
|
|
7440
7944
|
}
|
|
7441
7945
|
isInsideBounds(point, el) {
|
|
7442
7946
|
if (el.type === "grid") return false;
|
|
7947
|
+
const angle = el.rotation ?? 0;
|
|
7948
|
+
if (angle !== 0) {
|
|
7949
|
+
const b = getElementBounds(el);
|
|
7950
|
+
if (b) {
|
|
7951
|
+
point = rotatePoint(point, { x: b.x + b.w / 2, y: b.y + b.h / 2 }, -angle);
|
|
7952
|
+
}
|
|
7953
|
+
}
|
|
7443
7954
|
if (el.type === "shape" && el.shape === "line") {
|
|
7444
7955
|
const [a, b] = lineEndpoints(el);
|
|
7445
7956
|
const threshold = Math.max(el.strokeWidth / 2, 6);
|
|
@@ -8206,20 +8717,20 @@ var TemplateTool = class {
|
|
|
8206
8717
|
const snapUnit = Math.sqrt(3) * cellSize;
|
|
8207
8718
|
const radiusCells = radius / snapUnit;
|
|
8208
8719
|
const angle = this.computeAngle();
|
|
8209
|
-
const
|
|
8720
|
+
const center2 = this.origin;
|
|
8210
8721
|
let hexCells;
|
|
8211
8722
|
switch (this.templateShape) {
|
|
8212
8723
|
case "circle":
|
|
8213
|
-
hexCells = getHexCellsInRadius(
|
|
8724
|
+
hexCells = getHexCellsInRadius(center2, radiusCells, cellSize, orientation);
|
|
8214
8725
|
break;
|
|
8215
8726
|
case "cone":
|
|
8216
|
-
hexCells = getHexCellsInCone(
|
|
8727
|
+
hexCells = getHexCellsInCone(center2, angle, radiusCells, cellSize, orientation);
|
|
8217
8728
|
break;
|
|
8218
8729
|
case "line":
|
|
8219
|
-
hexCells = getHexCellsInLine(
|
|
8730
|
+
hexCells = getHexCellsInLine(center2, angle, radiusCells, cellSize, orientation);
|
|
8220
8731
|
break;
|
|
8221
8732
|
case "square":
|
|
8222
|
-
hexCells = getHexCellsInSquare(
|
|
8733
|
+
hexCells = getHexCellsInSquare(center2, radiusCells, cellSize, orientation);
|
|
8223
8734
|
break;
|
|
8224
8735
|
}
|
|
8225
8736
|
ctx.save();
|
|
@@ -8240,7 +8751,7 @@ var TemplateTool = class {
|
|
|
8240
8751
|
if (this.templateShape === "cone" || this.templateShape === "line" || this.templateShape === "circle" || this.templateShape === "square") {
|
|
8241
8752
|
ctx.globalAlpha = 0.5;
|
|
8242
8753
|
ctx.beginPath();
|
|
8243
|
-
drawHexPath(ctx,
|
|
8754
|
+
drawHexPath(ctx, center2.x, center2.y, cellSize, orientation);
|
|
8244
8755
|
ctx.fillStyle = this.strokeColor;
|
|
8245
8756
|
ctx.fill();
|
|
8246
8757
|
ctx.strokeStyle = this.strokeColor;
|
|
@@ -8256,8 +8767,8 @@ var TemplateTool = class {
|
|
|
8256
8767
|
ctx.font = `bold ${fontSize}px system-ui, sans-serif`;
|
|
8257
8768
|
ctx.textAlign = "center";
|
|
8258
8769
|
ctx.textBaseline = "bottom";
|
|
8259
|
-
const textX =
|
|
8260
|
-
const textY =
|
|
8770
|
+
const textX = center2.x;
|
|
8771
|
+
const textY = center2.y - 4;
|
|
8261
8772
|
const metrics = ctx.measureText(label);
|
|
8262
8773
|
const padX = 4;
|
|
8263
8774
|
const padY = 2;
|
|
@@ -8307,7 +8818,7 @@ var TemplateTool = class {
|
|
|
8307
8818
|
};
|
|
8308
8819
|
|
|
8309
8820
|
// src/index.ts
|
|
8310
|
-
var VERSION = "0.
|
|
8821
|
+
var VERSION = "0.36.0";
|
|
8311
8822
|
export {
|
|
8312
8823
|
ArrowTool,
|
|
8313
8824
|
AutoSave,
|