@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.cjs
CHANGED
|
@@ -605,6 +605,36 @@ function distSqToSegment(p, a, b) {
|
|
|
605
605
|
const dy = p.y - (a.y + t * aby);
|
|
606
606
|
return dx * dx + dy * dy;
|
|
607
607
|
}
|
|
608
|
+
function rotatePoint(p, center2, angle) {
|
|
609
|
+
if (angle === 0) return p;
|
|
610
|
+
const cos = Math.cos(angle);
|
|
611
|
+
const sin = Math.sin(angle);
|
|
612
|
+
const dx = p.x - center2.x;
|
|
613
|
+
const dy = p.y - center2.y;
|
|
614
|
+
return { x: center2.x + dx * cos - dy * sin, y: center2.y + dx * sin + dy * cos };
|
|
615
|
+
}
|
|
616
|
+
function rotatedAABB(bounds, angle) {
|
|
617
|
+
if (angle === 0) return bounds;
|
|
618
|
+
const c = { x: bounds.x + bounds.w / 2, y: bounds.y + bounds.h / 2 };
|
|
619
|
+
const corners = [
|
|
620
|
+
{ x: bounds.x, y: bounds.y },
|
|
621
|
+
{ x: bounds.x + bounds.w, y: bounds.y },
|
|
622
|
+
{ x: bounds.x + bounds.w, y: bounds.y + bounds.h },
|
|
623
|
+
{ x: bounds.x, y: bounds.y + bounds.h }
|
|
624
|
+
].map((p) => rotatePoint(p, c, angle));
|
|
625
|
+
const xs = corners.map((p) => p.x);
|
|
626
|
+
const ys = corners.map((p) => p.y);
|
|
627
|
+
const minX = Math.min(...xs);
|
|
628
|
+
const minY = Math.min(...ys);
|
|
629
|
+
return { x: minX, y: minY, w: Math.max(...xs) - minX, h: Math.max(...ys) - minY };
|
|
630
|
+
}
|
|
631
|
+
function normalizeAngle(angle) {
|
|
632
|
+
const twoPi = Math.PI * 2;
|
|
633
|
+
let a = angle % twoPi;
|
|
634
|
+
if (a <= -Math.PI) a += twoPi;
|
|
635
|
+
else if (a > Math.PI) a -= twoPi;
|
|
636
|
+
return a;
|
|
637
|
+
}
|
|
608
638
|
|
|
609
639
|
// src/elements/arrow-geometry.ts
|
|
610
640
|
function getArrowControlPoint(from, to, bend) {
|
|
@@ -961,6 +991,14 @@ var KeyboardActions = class {
|
|
|
961
991
|
}
|
|
962
992
|
this.pasteCount = 0;
|
|
963
993
|
}
|
|
994
|
+
cut() {
|
|
995
|
+
if (this.deps.isToolActive()) return;
|
|
996
|
+
this.copy();
|
|
997
|
+
this.deleteSelected();
|
|
998
|
+
}
|
|
999
|
+
hasClipboard() {
|
|
1000
|
+
return this.clipboard.length > 0;
|
|
1001
|
+
}
|
|
964
1002
|
paste() {
|
|
965
1003
|
if (this.deps.isToolActive()) return;
|
|
966
1004
|
this.flushPendingNudge();
|
|
@@ -1029,6 +1067,10 @@ var KeyboardActions = class {
|
|
|
1029
1067
|
if (this.deps.isToolActive()) return;
|
|
1030
1068
|
this.deps.ungroup?.();
|
|
1031
1069
|
}
|
|
1070
|
+
toggleLock() {
|
|
1071
|
+
if (this.deps.isToolActive()) return;
|
|
1072
|
+
this.deps.toggleLock?.();
|
|
1073
|
+
}
|
|
1032
1074
|
zOrder(operation) {
|
|
1033
1075
|
if (this.deps.isToolActive()) return;
|
|
1034
1076
|
this.flushPendingNudge();
|
|
@@ -1130,6 +1172,8 @@ var DEFAULT_BINDINGS = [
|
|
|
1130
1172
|
["zoom-reset", ["mod+0"]],
|
|
1131
1173
|
["group", ["mod+g"]],
|
|
1132
1174
|
["ungroup", ["mod+shift+g"]],
|
|
1175
|
+
["cut", ["mod+x"]],
|
|
1176
|
+
["toggle-lock", ["mod+shift+l"]],
|
|
1133
1177
|
["nudge-left", ["arrowleft"]],
|
|
1134
1178
|
["nudge-right", ["arrowright"]],
|
|
1135
1179
|
["nudge-up", ["arrowup"]],
|
|
@@ -1268,6 +1312,7 @@ var ShortcutMap = class {
|
|
|
1268
1312
|
var ZOOM_SENSITIVITY = 1e-3;
|
|
1269
1313
|
var ZOOM_STEP = 1.2;
|
|
1270
1314
|
var MIDDLE_BUTTON = 1;
|
|
1315
|
+
var LONG_PRESS_MS = 500;
|
|
1271
1316
|
var NUDGE_DELTAS = {
|
|
1272
1317
|
"nudge-left": [-1, 0],
|
|
1273
1318
|
"nudge-right": [1, 0],
|
|
@@ -1291,8 +1336,10 @@ var InputHandler = class {
|
|
|
1291
1336
|
fitToContent: options.fitToContent,
|
|
1292
1337
|
group: options.group,
|
|
1293
1338
|
ungroup: options.ungroup,
|
|
1339
|
+
toggleLock: options.toggleLock,
|
|
1294
1340
|
getLastPointerWorld: () => this.lastPointerWorld()
|
|
1295
1341
|
});
|
|
1342
|
+
this.openContextMenu = options.openContextMenu;
|
|
1296
1343
|
this.shortcutMap = new ShortcutMap(options.shortcuts?.bindings);
|
|
1297
1344
|
this.scope = options.shortcuts?.scope ?? "focus";
|
|
1298
1345
|
this.element.style.touchAction = "none";
|
|
@@ -1316,10 +1363,13 @@ var InputHandler = class {
|
|
|
1316
1363
|
lastPointerEvent = null;
|
|
1317
1364
|
inputFilter = new InputFilter();
|
|
1318
1365
|
deferredDown = null;
|
|
1366
|
+
longPressTimer = null;
|
|
1367
|
+
longPressStart = null;
|
|
1319
1368
|
abortController = new AbortController();
|
|
1320
1369
|
actions;
|
|
1321
1370
|
shortcutMap;
|
|
1322
1371
|
scope;
|
|
1372
|
+
openContextMenu;
|
|
1323
1373
|
setToolManager(toolManager, toolContext) {
|
|
1324
1374
|
this.toolManager = toolManager;
|
|
1325
1375
|
this.toolContext = toolContext;
|
|
@@ -1334,6 +1384,7 @@ var InputHandler = class {
|
|
|
1334
1384
|
this.actions.dispose();
|
|
1335
1385
|
this.abortController.abort();
|
|
1336
1386
|
this.inputFilter.reset();
|
|
1387
|
+
this.cancelLongPress();
|
|
1337
1388
|
this.deferredDown = null;
|
|
1338
1389
|
this.lastPointerEvent = null;
|
|
1339
1390
|
if (this.scope === "focus") {
|
|
@@ -1349,6 +1400,7 @@ var InputHandler = class {
|
|
|
1349
1400
|
this.element.addEventListener("pointerup", this.onPointerUp, opts);
|
|
1350
1401
|
this.element.addEventListener("pointerleave", this.onPointerLeave, opts);
|
|
1351
1402
|
this.element.addEventListener("pointercancel", this.onPointerUp, opts);
|
|
1403
|
+
this.element.addEventListener("contextmenu", this.onContextMenu, opts);
|
|
1352
1404
|
window.addEventListener("keydown", this.onKeyDown, opts);
|
|
1353
1405
|
window.addEventListener("keyup", this.onKeyUp, opts);
|
|
1354
1406
|
}
|
|
@@ -1377,11 +1429,13 @@ var InputHandler = class {
|
|
|
1377
1429
|
this.activePointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
|
1378
1430
|
this.element.setPointerCapture?.(e.pointerId);
|
|
1379
1431
|
if (this.activePointers.size === 2) {
|
|
1432
|
+
this.cancelLongPress();
|
|
1380
1433
|
this.startPinch();
|
|
1381
1434
|
this.cancelToolIfActive(e);
|
|
1382
1435
|
return;
|
|
1383
1436
|
}
|
|
1384
1437
|
if (e.button === MIDDLE_BUTTON || e.button === 0 && this.spaceHeld) {
|
|
1438
|
+
this.cancelLongPress();
|
|
1385
1439
|
this.isPanning = true;
|
|
1386
1440
|
this.lastPointer = { x: e.clientX, y: e.clientY };
|
|
1387
1441
|
return;
|
|
@@ -1391,6 +1445,7 @@ var InputHandler = class {
|
|
|
1391
1445
|
if (result.action === "suppress") return;
|
|
1392
1446
|
if (result.action === "defer") {
|
|
1393
1447
|
this.deferredDown = e;
|
|
1448
|
+
this.startLongPress(e);
|
|
1394
1449
|
return;
|
|
1395
1450
|
}
|
|
1396
1451
|
this.dispatchToolDown(e);
|
|
@@ -1419,6 +1474,7 @@ var InputHandler = class {
|
|
|
1419
1474
|
} else if (this.deferredDown) {
|
|
1420
1475
|
const result = this.inputFilter.filterMove(e);
|
|
1421
1476
|
if (result.action === "dispatch") {
|
|
1477
|
+
this.cancelLongPress();
|
|
1422
1478
|
this.dispatchToolDown(this.deferredDown);
|
|
1423
1479
|
this.deferredDown = null;
|
|
1424
1480
|
this.dispatchToolMove(e);
|
|
@@ -1428,6 +1484,7 @@ var InputHandler = class {
|
|
|
1428
1484
|
}
|
|
1429
1485
|
};
|
|
1430
1486
|
onPointerUp = (e) => {
|
|
1487
|
+
this.cancelLongPress();
|
|
1431
1488
|
try {
|
|
1432
1489
|
this.element.releasePointerCapture(e.pointerId);
|
|
1433
1490
|
} catch {
|
|
@@ -1480,74 +1537,82 @@ var InputHandler = class {
|
|
|
1480
1537
|
runAction(action, e) {
|
|
1481
1538
|
switch (action) {
|
|
1482
1539
|
case "delete":
|
|
1483
|
-
e
|
|
1540
|
+
e?.preventDefault();
|
|
1484
1541
|
this.actions.deleteSelected();
|
|
1485
1542
|
return;
|
|
1486
1543
|
case "deselect":
|
|
1487
1544
|
this.actions.deselect();
|
|
1488
1545
|
return;
|
|
1489
1546
|
case "undo":
|
|
1490
|
-
e
|
|
1547
|
+
e?.preventDefault();
|
|
1491
1548
|
this.actions.undo();
|
|
1492
1549
|
return;
|
|
1493
1550
|
case "redo":
|
|
1494
|
-
e
|
|
1551
|
+
e?.preventDefault();
|
|
1495
1552
|
this.actions.redo();
|
|
1496
1553
|
return;
|
|
1497
1554
|
case "select-all":
|
|
1498
|
-
e
|
|
1555
|
+
e?.preventDefault();
|
|
1499
1556
|
this.actions.selectAll();
|
|
1500
1557
|
return;
|
|
1501
1558
|
case "copy":
|
|
1502
|
-
e
|
|
1559
|
+
e?.preventDefault();
|
|
1503
1560
|
this.actions.copy();
|
|
1504
1561
|
return;
|
|
1505
1562
|
case "paste":
|
|
1506
|
-
e
|
|
1563
|
+
e?.preventDefault();
|
|
1507
1564
|
this.actions.paste();
|
|
1508
1565
|
return;
|
|
1509
1566
|
case "duplicate":
|
|
1510
|
-
e
|
|
1567
|
+
e?.preventDefault();
|
|
1511
1568
|
this.actions.duplicate();
|
|
1512
1569
|
return;
|
|
1513
1570
|
case "z-forward":
|
|
1514
|
-
e
|
|
1571
|
+
e?.preventDefault();
|
|
1515
1572
|
this.actions.zOrder("forward");
|
|
1516
1573
|
return;
|
|
1517
1574
|
case "z-backward":
|
|
1518
|
-
e
|
|
1575
|
+
e?.preventDefault();
|
|
1519
1576
|
this.actions.zOrder("backward");
|
|
1520
1577
|
return;
|
|
1521
1578
|
case "z-front":
|
|
1522
|
-
e
|
|
1579
|
+
e?.preventDefault();
|
|
1523
1580
|
this.actions.zOrder("front");
|
|
1524
1581
|
return;
|
|
1525
1582
|
case "z-back":
|
|
1526
|
-
e
|
|
1583
|
+
e?.preventDefault();
|
|
1527
1584
|
this.actions.zOrder("back");
|
|
1528
1585
|
return;
|
|
1529
1586
|
case "zoom-fit":
|
|
1530
|
-
e
|
|
1587
|
+
e?.preventDefault();
|
|
1531
1588
|
this.actions.zoomToFit();
|
|
1532
1589
|
return;
|
|
1533
1590
|
case "group":
|
|
1534
|
-
e
|
|
1591
|
+
e?.preventDefault();
|
|
1535
1592
|
this.actions.group();
|
|
1536
1593
|
return;
|
|
1537
1594
|
case "ungroup":
|
|
1538
|
-
e
|
|
1595
|
+
e?.preventDefault();
|
|
1539
1596
|
this.actions.ungroup();
|
|
1540
1597
|
return;
|
|
1598
|
+
case "cut":
|
|
1599
|
+
e?.preventDefault();
|
|
1600
|
+
this.actions.cut();
|
|
1601
|
+
return;
|
|
1602
|
+
case "toggle-lock":
|
|
1603
|
+
e?.preventDefault();
|
|
1604
|
+
this.actions.toggleLock();
|
|
1605
|
+
return;
|
|
1541
1606
|
case "zoom-in":
|
|
1542
|
-
e
|
|
1607
|
+
e?.preventDefault();
|
|
1543
1608
|
this.zoomByFactor(ZOOM_STEP);
|
|
1544
1609
|
return;
|
|
1545
1610
|
case "zoom-out":
|
|
1546
|
-
e
|
|
1611
|
+
e?.preventDefault();
|
|
1547
1612
|
this.zoomByFactor(1 / ZOOM_STEP);
|
|
1548
1613
|
return;
|
|
1549
1614
|
case "zoom-reset":
|
|
1550
|
-
e
|
|
1615
|
+
e?.preventDefault();
|
|
1551
1616
|
this.zoomToLevel(1);
|
|
1552
1617
|
return;
|
|
1553
1618
|
case "nudge-left":
|
|
@@ -1555,22 +1620,26 @@ var InputHandler = class {
|
|
|
1555
1620
|
case "nudge-up":
|
|
1556
1621
|
case "nudge-down": {
|
|
1557
1622
|
const delta = NUDGE_DELTAS[action];
|
|
1558
|
-
if (delta && this.actions.nudge(delta[0], delta[1], e
|
|
1559
|
-
e
|
|
1623
|
+
if (delta && this.actions.nudge(delta[0], delta[1], e?.shiftKey ?? false)) {
|
|
1624
|
+
e?.preventDefault();
|
|
1560
1625
|
}
|
|
1561
1626
|
return;
|
|
1562
1627
|
}
|
|
1563
1628
|
default:
|
|
1564
1629
|
if (action.startsWith("tool:")) {
|
|
1565
1630
|
if (this.isToolActive) return;
|
|
1566
|
-
e
|
|
1631
|
+
e?.preventDefault();
|
|
1567
1632
|
this.toolContext?.switchTool?.(action.slice("tool:".length));
|
|
1568
1633
|
return;
|
|
1569
1634
|
}
|
|
1570
1635
|
console.warn(`[fieldnotes] unknown shortcut action "${action}"`);
|
|
1571
1636
|
}
|
|
1572
1637
|
}
|
|
1638
|
+
hasClipboard() {
|
|
1639
|
+
return this.actions.hasClipboard();
|
|
1640
|
+
}
|
|
1573
1641
|
startPinch() {
|
|
1642
|
+
this.cancelLongPress();
|
|
1574
1643
|
this.inputFilter.reset();
|
|
1575
1644
|
this.deferredDown = null;
|
|
1576
1645
|
this.isPanning = true;
|
|
@@ -1582,18 +1651,18 @@ var InputHandler = class {
|
|
|
1582
1651
|
handlePinchMove() {
|
|
1583
1652
|
const [a, b] = this.getPinchPoints();
|
|
1584
1653
|
const dist = this.distance(a, b);
|
|
1585
|
-
const
|
|
1654
|
+
const center2 = this.midpoint(a, b);
|
|
1586
1655
|
if (this.lastPinchDistance > 0) {
|
|
1587
1656
|
const scale = dist / this.lastPinchDistance;
|
|
1588
1657
|
const newZoom = this.camera.zoom * scale;
|
|
1589
|
-
this.camera.zoomAt(newZoom,
|
|
1658
|
+
this.camera.zoomAt(newZoom, center2);
|
|
1590
1659
|
}
|
|
1591
|
-
const dx =
|
|
1592
|
-
const dy =
|
|
1660
|
+
const dx = center2.x - this.lastPointer.x;
|
|
1661
|
+
const dy = center2.y - this.lastPointer.y;
|
|
1593
1662
|
this.camera.pan(dx, dy);
|
|
1594
1663
|
this.lastPinchDistance = dist;
|
|
1595
|
-
this.lastPinchCenter =
|
|
1596
|
-
this.lastPointer = { ...
|
|
1664
|
+
this.lastPinchCenter = center2;
|
|
1665
|
+
this.lastPointer = { ...center2 };
|
|
1597
1666
|
}
|
|
1598
1667
|
getPinchPoints() {
|
|
1599
1668
|
const pts = [...this.activePointers.values()];
|
|
@@ -1613,6 +1682,13 @@ var InputHandler = class {
|
|
|
1613
1682
|
const rect = this.element.getBoundingClientRect();
|
|
1614
1683
|
return this.camera.screenToWorld({ x: e.clientX - rect.left, y: e.clientY - rect.top });
|
|
1615
1684
|
}
|
|
1685
|
+
onContextMenu = (e) => {
|
|
1686
|
+
e.preventDefault();
|
|
1687
|
+
if (this.toolManager?.activeTool?.name !== "select") return;
|
|
1688
|
+
const rect = this.element.getBoundingClientRect();
|
|
1689
|
+
const world = this.camera.screenToWorld({ x: e.clientX - rect.left, y: e.clientY - rect.top });
|
|
1690
|
+
this.openContextMenu?.({ x: e.clientX, y: e.clientY }, world);
|
|
1691
|
+
};
|
|
1616
1692
|
onPointerLeave = (e) => {
|
|
1617
1693
|
this.lastPointerEvent = null;
|
|
1618
1694
|
this.onPointerUp(e);
|
|
@@ -1660,12 +1736,36 @@ var InputHandler = class {
|
|
|
1660
1736
|
this.element.focus({ preventScroll: true });
|
|
1661
1737
|
}
|
|
1662
1738
|
cancelToolIfActive(e) {
|
|
1739
|
+
this.cancelLongPress();
|
|
1663
1740
|
if (this.isToolActive) {
|
|
1664
1741
|
this.dispatchToolUp(e);
|
|
1665
1742
|
this.isToolActive = false;
|
|
1666
1743
|
}
|
|
1667
1744
|
this.deferredDown = null;
|
|
1668
1745
|
}
|
|
1746
|
+
startLongPress(e) {
|
|
1747
|
+
if (e.pointerType !== "touch") return;
|
|
1748
|
+
if (this.toolManager?.activeTool?.name !== "select") return;
|
|
1749
|
+
this.longPressStart = { x: e.clientX, y: e.clientY };
|
|
1750
|
+
this.longPressTimer = setTimeout(() => this.fireLongPress(), LONG_PRESS_MS);
|
|
1751
|
+
}
|
|
1752
|
+
cancelLongPress() {
|
|
1753
|
+
if (this.longPressTimer !== null) {
|
|
1754
|
+
clearTimeout(this.longPressTimer);
|
|
1755
|
+
this.longPressTimer = null;
|
|
1756
|
+
}
|
|
1757
|
+
this.longPressStart = null;
|
|
1758
|
+
}
|
|
1759
|
+
fireLongPress() {
|
|
1760
|
+
this.longPressTimer = null;
|
|
1761
|
+
if (!this.deferredDown || this.activePointers.size !== 1 || this.isPanning) return;
|
|
1762
|
+
const start = this.longPressStart;
|
|
1763
|
+
if (!start) return;
|
|
1764
|
+
const rect = this.element.getBoundingClientRect();
|
|
1765
|
+
const world = this.camera.screenToWorld({ x: start.x - rect.left, y: start.y - rect.top });
|
|
1766
|
+
this.deferredDown = null;
|
|
1767
|
+
this.openContextMenu?.({ x: start.x, y: start.y }, world);
|
|
1768
|
+
}
|
|
1669
1769
|
};
|
|
1670
1770
|
|
|
1671
1771
|
// src/canvas/background.ts
|
|
@@ -2135,11 +2235,19 @@ var ElementStore = class {
|
|
|
2135
2235
|
(el) => el.type === type
|
|
2136
2236
|
);
|
|
2137
2237
|
}
|
|
2238
|
+
// Spatial index stores the rotation-expanded AABB so rotated elements remain
|
|
2239
|
+
// broad-phase hit-test/marquee candidates; precise tests run against local bounds.
|
|
2240
|
+
indexBounds(element) {
|
|
2241
|
+
const bounds = getElementBounds(element);
|
|
2242
|
+
if (!bounds) return null;
|
|
2243
|
+
const angle = element.rotation ?? 0;
|
|
2244
|
+
return angle === 0 ? bounds : rotatedAABB(bounds, angle);
|
|
2245
|
+
}
|
|
2138
2246
|
add(element) {
|
|
2139
2247
|
this.sortedCache = null;
|
|
2140
2248
|
this._versions.set(element.id, 0);
|
|
2141
2249
|
this.elements.set(element.id, element);
|
|
2142
|
-
const bounds =
|
|
2250
|
+
const bounds = this.indexBounds(element);
|
|
2143
2251
|
if (bounds) this.spatialIndex.insert(element.id, bounds);
|
|
2144
2252
|
this.bus.emit("add", element);
|
|
2145
2253
|
}
|
|
@@ -2161,7 +2269,7 @@ var ElementStore = class {
|
|
|
2161
2269
|
updated.text = sanitizeNoteHtml(updated.text);
|
|
2162
2270
|
}
|
|
2163
2271
|
this.elements.set(id, updated);
|
|
2164
|
-
const newBounds =
|
|
2272
|
+
const newBounds = this.indexBounds(updated);
|
|
2165
2273
|
if (newBounds) {
|
|
2166
2274
|
this.spatialIndex.update(id, newBounds);
|
|
2167
2275
|
}
|
|
@@ -2194,7 +2302,7 @@ var ElementStore = class {
|
|
|
2194
2302
|
for (const el of elements) {
|
|
2195
2303
|
this.elements.set(el.id, el);
|
|
2196
2304
|
this._versions.set(el.id, 0);
|
|
2197
|
-
const bounds =
|
|
2305
|
+
const bounds = this.indexBounds(el);
|
|
2198
2306
|
if (bounds) this.spatialIndex.insert(el.id, bounds);
|
|
2199
2307
|
if (el.type === "stroke") {
|
|
2200
2308
|
computeStrokeSegments(el);
|
|
@@ -2399,9 +2507,9 @@ function updateBoundArrow(arrow, store) {
|
|
|
2399
2507
|
if (arrow.fromBinding) {
|
|
2400
2508
|
const el = store.getById(arrow.fromBinding.elementId);
|
|
2401
2509
|
if (el) {
|
|
2402
|
-
const
|
|
2403
|
-
updates.from =
|
|
2404
|
-
updates.position =
|
|
2510
|
+
const center2 = getElementCenter(el);
|
|
2511
|
+
updates.from = center2;
|
|
2512
|
+
updates.position = center2;
|
|
2405
2513
|
}
|
|
2406
2514
|
}
|
|
2407
2515
|
if (arrow.toBinding) {
|
|
@@ -2413,6 +2521,21 @@ function updateBoundArrow(arrow, store) {
|
|
|
2413
2521
|
return Object.keys(updates).length > 0 ? updates : null;
|
|
2414
2522
|
}
|
|
2415
2523
|
|
|
2524
|
+
// src/elements/rotate-canvas.ts
|
|
2525
|
+
function withRotation(ctx, el, center2, draw) {
|
|
2526
|
+
const angle = el.rotation ?? 0;
|
|
2527
|
+
if (angle === 0) {
|
|
2528
|
+
draw();
|
|
2529
|
+
return;
|
|
2530
|
+
}
|
|
2531
|
+
ctx.save();
|
|
2532
|
+
ctx.translate(center2.x, center2.y);
|
|
2533
|
+
ctx.rotate(angle);
|
|
2534
|
+
ctx.translate(-center2.x, -center2.y);
|
|
2535
|
+
draw();
|
|
2536
|
+
ctx.restore();
|
|
2537
|
+
}
|
|
2538
|
+
|
|
2416
2539
|
// src/elements/grid-renderer.ts
|
|
2417
2540
|
function getSquareGridLines(bounds, cellSize) {
|
|
2418
2541
|
if (cellSize <= 0) return { verticals: [], horizontals: [] };
|
|
@@ -2676,18 +2799,18 @@ function getHexDistance(a, b, cellSize, orientation) {
|
|
|
2676
2799
|
const ds = -dq - dr;
|
|
2677
2800
|
return Math.max(Math.abs(dq), Math.abs(dr), Math.abs(ds));
|
|
2678
2801
|
}
|
|
2679
|
-
function getHexCellsInRadius(
|
|
2802
|
+
function getHexCellsInRadius(center2, radiusCells, cellSize, orientation) {
|
|
2680
2803
|
const n = Math.round(radiusCells);
|
|
2681
|
-
const off = pixelToOffset(
|
|
2804
|
+
const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
|
|
2682
2805
|
const cube = offsetToCube(off.col, off.row, orientation);
|
|
2683
2806
|
if (n <= 0) {
|
|
2684
2807
|
return [offsetToPixel(off.col, off.row, cellSize, orientation)];
|
|
2685
2808
|
}
|
|
2686
2809
|
return enumerateHexRing(cube.q, cube.r, n, orientation, cellSize);
|
|
2687
2810
|
}
|
|
2688
|
-
function getHexCellsInCone(
|
|
2811
|
+
function getHexCellsInCone(center2, angle, radiusCells, cellSize, orientation) {
|
|
2689
2812
|
const n = Math.round(radiusCells);
|
|
2690
|
-
const off = pixelToOffset(
|
|
2813
|
+
const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
|
|
2691
2814
|
const cube = offsetToCube(off.col, off.row, orientation);
|
|
2692
2815
|
const centerPixel = offsetToPixel(off.col, off.row, cellSize, orientation);
|
|
2693
2816
|
if (n <= 0) return [centerPixel];
|
|
@@ -2721,9 +2844,9 @@ function getHexCellsInCone(center, angle, radiusCells, cellSize, orientation) {
|
|
|
2721
2844
|
}
|
|
2722
2845
|
return cells;
|
|
2723
2846
|
}
|
|
2724
|
-
function getHexCellsInLine(
|
|
2847
|
+
function getHexCellsInLine(center2, angle, radiusCells, cellSize, orientation) {
|
|
2725
2848
|
const n = Math.round(radiusCells);
|
|
2726
|
-
const off = pixelToOffset(
|
|
2849
|
+
const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
|
|
2727
2850
|
const cube = offsetToCube(off.col, off.row, orientation);
|
|
2728
2851
|
const centerPixel = offsetToPixel(off.col, off.row, cellSize, orientation);
|
|
2729
2852
|
if (n <= 0) return [centerPixel];
|
|
@@ -2759,9 +2882,9 @@ function getHexCellsInLine(center, angle, radiusCells, cellSize, orientation) {
|
|
|
2759
2882
|
}
|
|
2760
2883
|
return cells;
|
|
2761
2884
|
}
|
|
2762
|
-
function getHexCellsInSquare(
|
|
2885
|
+
function getHexCellsInSquare(center2, radiusCells, cellSize, orientation) {
|
|
2763
2886
|
const n = Math.round(radiusCells);
|
|
2764
|
-
const off = pixelToOffset(
|
|
2887
|
+
const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
|
|
2765
2888
|
const cube = offsetToCube(off.col, off.row, orientation);
|
|
2766
2889
|
const centerPixel = offsetToPixel(off.col, off.row, cellSize, orientation);
|
|
2767
2890
|
if (n <= 0) return [centerPixel];
|
|
@@ -2839,18 +2962,27 @@ var ElementRenderer = class {
|
|
|
2839
2962
|
}
|
|
2840
2963
|
renderCanvasElement(ctx, element) {
|
|
2841
2964
|
switch (element.type) {
|
|
2842
|
-
case "stroke":
|
|
2843
|
-
|
|
2965
|
+
case "stroke": {
|
|
2966
|
+
const b = getElementBounds(element);
|
|
2967
|
+
const c = b ? { x: b.x + b.w / 2, y: b.y + b.h / 2 } : element.position;
|
|
2968
|
+
withRotation(ctx, element, c, () => this.renderStroke(ctx, element));
|
|
2844
2969
|
break;
|
|
2970
|
+
}
|
|
2845
2971
|
case "arrow":
|
|
2846
2972
|
this.renderArrow(ctx, element);
|
|
2847
2973
|
break;
|
|
2848
|
-
case "shape":
|
|
2849
|
-
|
|
2974
|
+
case "shape": {
|
|
2975
|
+
const b = getElementBounds(element);
|
|
2976
|
+
const c = b ? { x: b.x + b.w / 2, y: b.y + b.h / 2 } : element.position;
|
|
2977
|
+
withRotation(ctx, element, c, () => this.renderShape(ctx, element));
|
|
2850
2978
|
break;
|
|
2851
|
-
|
|
2852
|
-
|
|
2979
|
+
}
|
|
2980
|
+
case "image": {
|
|
2981
|
+
const b = getElementBounds(element);
|
|
2982
|
+
const c = b ? { x: b.x + b.w / 2, y: b.y + b.h / 2 } : element.position;
|
|
2983
|
+
withRotation(ctx, element, c, () => this.renderImage(ctx, element));
|
|
2853
2984
|
break;
|
|
2985
|
+
}
|
|
2854
2986
|
case "grid":
|
|
2855
2987
|
this.renderGrid(ctx, element);
|
|
2856
2988
|
break;
|
|
@@ -3147,20 +3279,20 @@ var ElementRenderer = class {
|
|
|
3147
3279
|
renderHexTemplate(ctx, template, cellSize, orientation) {
|
|
3148
3280
|
const snapUnit = Math.sqrt(3) * cellSize;
|
|
3149
3281
|
const radiusCells = template.radius / snapUnit;
|
|
3150
|
-
const
|
|
3282
|
+
const center2 = template.position;
|
|
3151
3283
|
let cells;
|
|
3152
3284
|
switch (template.templateShape) {
|
|
3153
3285
|
case "circle":
|
|
3154
|
-
cells = getHexCellsInRadius(
|
|
3286
|
+
cells = getHexCellsInRadius(center2, radiusCells, cellSize, orientation);
|
|
3155
3287
|
break;
|
|
3156
3288
|
case "cone":
|
|
3157
|
-
cells = getHexCellsInCone(
|
|
3289
|
+
cells = getHexCellsInCone(center2, template.angle, radiusCells, cellSize, orientation);
|
|
3158
3290
|
break;
|
|
3159
3291
|
case "line":
|
|
3160
|
-
cells = getHexCellsInLine(
|
|
3292
|
+
cells = getHexCellsInLine(center2, template.angle, radiusCells, cellSize, orientation);
|
|
3161
3293
|
break;
|
|
3162
3294
|
case "square":
|
|
3163
|
-
cells = getHexCellsInSquare(
|
|
3295
|
+
cells = getHexCellsInSquare(center2, radiusCells, cellSize, orientation);
|
|
3164
3296
|
break;
|
|
3165
3297
|
}
|
|
3166
3298
|
ctx.save();
|
|
@@ -3181,7 +3313,7 @@ var ElementRenderer = class {
|
|
|
3181
3313
|
{
|
|
3182
3314
|
ctx.globalAlpha = Math.min(template.opacity + 0.1, 1);
|
|
3183
3315
|
ctx.beginPath();
|
|
3184
|
-
drawHexPath(ctx,
|
|
3316
|
+
drawHexPath(ctx, center2.x, center2.y, cellSize, orientation);
|
|
3185
3317
|
ctx.fillStyle = template.strokeColor;
|
|
3186
3318
|
ctx.fill();
|
|
3187
3319
|
ctx.strokeStyle = template.strokeColor;
|
|
@@ -3190,7 +3322,7 @@ var ElementRenderer = class {
|
|
|
3190
3322
|
}
|
|
3191
3323
|
if (template.templateShape === "circle" && template.radiusFeet != null && template.radiusFeet > 0) {
|
|
3192
3324
|
const r = template.radius;
|
|
3193
|
-
this.renderRadiusMarker(ctx,
|
|
3325
|
+
this.renderRadiusMarker(ctx, center2.x, center2.y, r, template.radiusFeet);
|
|
3194
3326
|
}
|
|
3195
3327
|
ctx.restore();
|
|
3196
3328
|
}
|
|
@@ -3820,6 +3952,87 @@ var NoteEditor = class {
|
|
|
3820
3952
|
}
|
|
3821
3953
|
};
|
|
3822
3954
|
|
|
3955
|
+
// src/canvas/context-menu.ts
|
|
3956
|
+
var ContextMenu = class {
|
|
3957
|
+
constructor(options) {
|
|
3958
|
+
this.options = options;
|
|
3959
|
+
}
|
|
3960
|
+
el = null;
|
|
3961
|
+
outsideListener = null;
|
|
3962
|
+
keyListener = null;
|
|
3963
|
+
isOpen() {
|
|
3964
|
+
return this.el !== null;
|
|
3965
|
+
}
|
|
3966
|
+
open(items, screenPos) {
|
|
3967
|
+
this.close();
|
|
3968
|
+
const el = document.createElement("div");
|
|
3969
|
+
el.className = "fieldnotes-context-menu";
|
|
3970
|
+
Object.assign(el.style, {
|
|
3971
|
+
position: "fixed",
|
|
3972
|
+
left: `${screenPos.x}px`,
|
|
3973
|
+
top: `${screenPos.y}px`,
|
|
3974
|
+
zIndex: "10000",
|
|
3975
|
+
display: "flex",
|
|
3976
|
+
flexDirection: "column"
|
|
3977
|
+
});
|
|
3978
|
+
for (const item of items) {
|
|
3979
|
+
const btn = document.createElement("button");
|
|
3980
|
+
btn.type = "button";
|
|
3981
|
+
btn.className = "fieldnotes-context-menu-item" + (item.disabled ? " fieldnotes-context-menu-item--disabled" : "");
|
|
3982
|
+
btn.textContent = item.label;
|
|
3983
|
+
if (item.disabled) {
|
|
3984
|
+
btn.disabled = true;
|
|
3985
|
+
} else {
|
|
3986
|
+
btn.addEventListener("click", () => {
|
|
3987
|
+
this.options.onCommand(item.action);
|
|
3988
|
+
this.close();
|
|
3989
|
+
});
|
|
3990
|
+
}
|
|
3991
|
+
el.appendChild(btn);
|
|
3992
|
+
}
|
|
3993
|
+
document.body.appendChild(el);
|
|
3994
|
+
this.el = el;
|
|
3995
|
+
this.clampToViewport(el, screenPos);
|
|
3996
|
+
this.keyListener = (e) => {
|
|
3997
|
+
if (e.key === "Escape") this.close();
|
|
3998
|
+
};
|
|
3999
|
+
document.addEventListener("keydown", this.keyListener);
|
|
4000
|
+
this.outsideListener = (e) => {
|
|
4001
|
+
if (this.el && !this.el.contains(e.target)) this.close();
|
|
4002
|
+
};
|
|
4003
|
+
setTimeout(() => {
|
|
4004
|
+
if (this.outsideListener) document.addEventListener("pointerdown", this.outsideListener);
|
|
4005
|
+
}, 0);
|
|
4006
|
+
}
|
|
4007
|
+
close() {
|
|
4008
|
+
if (this.keyListener) {
|
|
4009
|
+
document.removeEventListener("keydown", this.keyListener);
|
|
4010
|
+
this.keyListener = null;
|
|
4011
|
+
}
|
|
4012
|
+
if (this.outsideListener) {
|
|
4013
|
+
document.removeEventListener("pointerdown", this.outsideListener);
|
|
4014
|
+
this.outsideListener = null;
|
|
4015
|
+
}
|
|
4016
|
+
if (this.el) {
|
|
4017
|
+
this.el.remove();
|
|
4018
|
+
this.el = null;
|
|
4019
|
+
this.options.onClose();
|
|
4020
|
+
}
|
|
4021
|
+
}
|
|
4022
|
+
dispose() {
|
|
4023
|
+
this.close();
|
|
4024
|
+
}
|
|
4025
|
+
clampToViewport(el, screenPos) {
|
|
4026
|
+
const rect = el.getBoundingClientRect();
|
|
4027
|
+
if (rect.width > 0 && screenPos.x + rect.width > window.innerWidth) {
|
|
4028
|
+
el.style.left = `${Math.max(0, screenPos.x - rect.width)}px`;
|
|
4029
|
+
}
|
|
4030
|
+
if (rect.height > 0 && screenPos.y + rect.height > window.innerHeight) {
|
|
4031
|
+
el.style.top = `${Math.max(0, screenPos.y - rect.height)}px`;
|
|
4032
|
+
}
|
|
4033
|
+
}
|
|
4034
|
+
};
|
|
4035
|
+
|
|
3823
4036
|
// src/elements/translate.ts
|
|
3824
4037
|
function translateElementPatch(el, dx, dy) {
|
|
3825
4038
|
const position = { x: el.position.x + dx, y: el.position.y + dy };
|
|
@@ -4309,6 +4522,7 @@ function renderStyledRuns(ctx, runs, startX, startY, maxWidth) {
|
|
|
4309
4522
|
}
|
|
4310
4523
|
|
|
4311
4524
|
// src/canvas/export-image.ts
|
|
4525
|
+
var center = (b) => ({ x: b.x + b.w / 2, y: b.y + b.h / 2 });
|
|
4312
4526
|
function getStrokeBounds(el) {
|
|
4313
4527
|
if (el.type !== "stroke") return null;
|
|
4314
4528
|
if (el.points.length === 0) return null;
|
|
@@ -4334,8 +4548,10 @@ function getStrokeBounds(el) {
|
|
|
4334
4548
|
}
|
|
4335
4549
|
function getElementRect(el) {
|
|
4336
4550
|
switch (el.type) {
|
|
4337
|
-
case "stroke":
|
|
4338
|
-
|
|
4551
|
+
case "stroke": {
|
|
4552
|
+
const r = getStrokeBounds(el);
|
|
4553
|
+
return r ? rotatedAABB(r, el.rotation ?? 0) : r;
|
|
4554
|
+
}
|
|
4339
4555
|
case "arrow": {
|
|
4340
4556
|
const b = getArrowBounds(el.from, el.to, el.bend);
|
|
4341
4557
|
const pad = el.width / 2 + 14;
|
|
@@ -4354,7 +4570,10 @@ function getElementRect(el) {
|
|
|
4354
4570
|
case "text":
|
|
4355
4571
|
case "shape":
|
|
4356
4572
|
if ("size" in el) {
|
|
4357
|
-
return
|
|
4573
|
+
return rotatedAABB(
|
|
4574
|
+
{ x: el.position.x, y: el.position.y, w: el.size.w, h: el.size.h },
|
|
4575
|
+
el.rotation ?? 0
|
|
4576
|
+
);
|
|
4358
4577
|
}
|
|
4359
4578
|
return null;
|
|
4360
4579
|
default:
|
|
@@ -4492,11 +4711,13 @@ async function exportImage(store, options = {}, layerManager) {
|
|
|
4492
4711
|
continue;
|
|
4493
4712
|
}
|
|
4494
4713
|
if (el.type === "note") {
|
|
4495
|
-
|
|
4714
|
+
const b = getElementBounds(el);
|
|
4715
|
+
withRotation(ctx, el, b ? center(b) : el.position, () => renderNoteOnCanvas(ctx, el));
|
|
4496
4716
|
continue;
|
|
4497
4717
|
}
|
|
4498
4718
|
if (el.type === "text") {
|
|
4499
|
-
|
|
4719
|
+
const b = getElementBounds(el);
|
|
4720
|
+
withRotation(ctx, el, b ? center(b) : el.position, () => renderTextOnCanvas(ctx, el));
|
|
4500
4721
|
continue;
|
|
4501
4722
|
}
|
|
4502
4723
|
if (el.type === "html") {
|
|
@@ -4505,7 +4726,13 @@ async function exportImage(store, options = {}, layerManager) {
|
|
|
4505
4726
|
if (el.type === "image") {
|
|
4506
4727
|
const img = imageCache.get(el.id);
|
|
4507
4728
|
if (img) {
|
|
4508
|
-
|
|
4729
|
+
const b = getElementBounds(el);
|
|
4730
|
+
withRotation(
|
|
4731
|
+
ctx,
|
|
4732
|
+
el,
|
|
4733
|
+
b ? center(b) : el.position,
|
|
4734
|
+
() => ctx.drawImage(img, el.position.x, el.position.y, el.size.w, el.size.h)
|
|
4735
|
+
);
|
|
4509
4736
|
}
|
|
4510
4737
|
continue;
|
|
4511
4738
|
}
|
|
@@ -4841,7 +5068,9 @@ var DomNodeManager = class {
|
|
|
4841
5068
|
top: `${element.position.y}px`,
|
|
4842
5069
|
width: size ? `${size.w}px` : "auto",
|
|
4843
5070
|
height: size ? `${size.h}px` : "auto",
|
|
4844
|
-
zIndex: String(zIndex)
|
|
5071
|
+
zIndex: String(zIndex),
|
|
5072
|
+
transform: element.rotation ? `rotate(${element.rotation}rad)` : "",
|
|
5073
|
+
transformOrigin: "50% 50%"
|
|
4845
5074
|
});
|
|
4846
5075
|
this.renderDomContent(node, element);
|
|
4847
5076
|
}
|
|
@@ -5620,8 +5849,20 @@ var Viewport = class {
|
|
|
5620
5849
|
fitToContent: () => this.fitToContent(),
|
|
5621
5850
|
group: () => this.groupSelection(),
|
|
5622
5851
|
ungroup: () => this.ungroupSelection(),
|
|
5852
|
+
toggleLock: () => this.toggleLockSelection(),
|
|
5853
|
+
openContextMenu: (screenPos, world) => {
|
|
5854
|
+
this.getSelectTool()?.selectAtPoint(world, this.toolContext);
|
|
5855
|
+
this.openContextMenu(screenPos);
|
|
5856
|
+
},
|
|
5623
5857
|
shortcuts: options.shortcuts
|
|
5624
5858
|
});
|
|
5859
|
+
if (options.contextMenu !== false) {
|
|
5860
|
+
this.contextMenu = new ContextMenu({
|
|
5861
|
+
onCommand: (action) => this.runAction(action),
|
|
5862
|
+
onClose: noop
|
|
5863
|
+
});
|
|
5864
|
+
}
|
|
5865
|
+
this.unsubToolChange = this.toolManager.onChange(() => this.contextMenu?.close());
|
|
5625
5866
|
this.domNodeManager = new DomNodeManager({
|
|
5626
5867
|
domLayer: this.domLayer,
|
|
5627
5868
|
onEditRequest: (id) => this.startEditingElement(id),
|
|
@@ -5653,6 +5894,7 @@ var Viewport = class {
|
|
|
5653
5894
|
this.unsubCamera = this.camera.onChange(() => {
|
|
5654
5895
|
this.applyCameraTransform();
|
|
5655
5896
|
this.noteEditor.updateToolbarPosition();
|
|
5897
|
+
this.contextMenu?.close();
|
|
5656
5898
|
this.requestRender();
|
|
5657
5899
|
});
|
|
5658
5900
|
this.unsubStore = [
|
|
@@ -5705,6 +5947,7 @@ var Viewport = class {
|
|
|
5705
5947
|
canvasEl;
|
|
5706
5948
|
wrapper;
|
|
5707
5949
|
unsubCamera;
|
|
5950
|
+
unsubToolChange;
|
|
5708
5951
|
unsubStore;
|
|
5709
5952
|
inputHandler;
|
|
5710
5953
|
background;
|
|
@@ -5727,6 +5970,7 @@ var Viewport = class {
|
|
|
5727
5970
|
doubleTapDetector = new DoubleTapDetector();
|
|
5728
5971
|
tapDownX = 0;
|
|
5729
5972
|
tapDownY = 0;
|
|
5973
|
+
contextMenu = null;
|
|
5730
5974
|
get ctx() {
|
|
5731
5975
|
return this.canvasEl.getContext("2d");
|
|
5732
5976
|
}
|
|
@@ -5917,6 +6161,34 @@ var Viewport = class {
|
|
|
5917
6161
|
getSelectedIds() {
|
|
5918
6162
|
return this.getSelectTool()?.selectedIds ?? EMPTY_IDS;
|
|
5919
6163
|
}
|
|
6164
|
+
runAction(action) {
|
|
6165
|
+
this.inputHandler.runAction(action);
|
|
6166
|
+
}
|
|
6167
|
+
canPaste() {
|
|
6168
|
+
return this.inputHandler.hasClipboard();
|
|
6169
|
+
}
|
|
6170
|
+
openContextMenu(screenPos) {
|
|
6171
|
+
if (!this.contextMenu) return;
|
|
6172
|
+
const ids = this.getSelectedIds();
|
|
6173
|
+
const items = [];
|
|
6174
|
+
if (ids.length > 0) {
|
|
6175
|
+
items.push({ label: "Cut", action: "cut" });
|
|
6176
|
+
items.push({ label: "Copy", action: "copy" });
|
|
6177
|
+
if (this.canPaste()) items.push({ label: "Paste", action: "paste" });
|
|
6178
|
+
items.push({ label: "Duplicate", action: "duplicate" });
|
|
6179
|
+
items.push({ label: "Delete", action: "delete" });
|
|
6180
|
+
items.push({ label: "Bring to Front", action: "z-front" });
|
|
6181
|
+
items.push({ label: "Bring Forward", action: "z-forward" });
|
|
6182
|
+
items.push({ label: "Send Backward", action: "z-backward" });
|
|
6183
|
+
items.push({ label: "Send to Back", action: "z-back" });
|
|
6184
|
+
const allLocked = ids.every((id) => this.store.getById(id)?.locked);
|
|
6185
|
+
items.push({ label: allLocked ? "Unlock" : "Lock", action: "toggle-lock" });
|
|
6186
|
+
} else if (this.canPaste()) {
|
|
6187
|
+
items.push({ label: "Paste", action: "paste" });
|
|
6188
|
+
}
|
|
6189
|
+
if (items.length === 0) return;
|
|
6190
|
+
this.contextMenu.open(items, screenPos);
|
|
6191
|
+
}
|
|
5920
6192
|
onSelectionChange(listener) {
|
|
5921
6193
|
const tool = this.getSelectTool();
|
|
5922
6194
|
return tool ? tool.onSelectionChange(listener) : noop;
|
|
@@ -5977,6 +6249,20 @@ var Viewport = class {
|
|
|
5977
6249
|
}
|
|
5978
6250
|
this.historyRecorder.commit();
|
|
5979
6251
|
}
|
|
6252
|
+
toggleLockSelection() {
|
|
6253
|
+
const ids = this.getSelectedIds();
|
|
6254
|
+
if (ids.length === 0) return;
|
|
6255
|
+
const anyUnlocked = ids.some((id) => {
|
|
6256
|
+
const el = this.store.getById(id);
|
|
6257
|
+
return el ? !el.locked : false;
|
|
6258
|
+
});
|
|
6259
|
+
this.historyRecorder.begin();
|
|
6260
|
+
for (const id of ids) {
|
|
6261
|
+
const el = this.store.getById(id);
|
|
6262
|
+
if (el && el.locked !== anyUnlocked) this.store.update(id, { locked: anyUnlocked });
|
|
6263
|
+
}
|
|
6264
|
+
this.historyRecorder.commit();
|
|
6265
|
+
}
|
|
5980
6266
|
alignSelection(edge) {
|
|
5981
6267
|
const bounded = this.boundedSelection();
|
|
5982
6268
|
if (bounded.length < 2) return;
|
|
@@ -6018,13 +6304,13 @@ var Viewport = class {
|
|
|
6018
6304
|
distributeSelection(axis) {
|
|
6019
6305
|
const bounded = this.boundedSelection();
|
|
6020
6306
|
if (bounded.length < 3) return;
|
|
6021
|
-
const
|
|
6022
|
-
const sorted = [...bounded].sort((p, q) =>
|
|
6307
|
+
const center2 = (b) => axis === "horizontal" ? b.x + b.w / 2 : b.y + b.h / 2;
|
|
6308
|
+
const sorted = [...bounded].sort((p, q) => center2(p.bounds) - center2(q.bounds));
|
|
6023
6309
|
const first = sorted[0];
|
|
6024
6310
|
const last = sorted[sorted.length - 1];
|
|
6025
6311
|
if (!first || !last) return;
|
|
6026
|
-
const c0 =
|
|
6027
|
-
const cN =
|
|
6312
|
+
const c0 = center2(first.bounds);
|
|
6313
|
+
const cN = center2(last.bounds);
|
|
6028
6314
|
const n = sorted.length;
|
|
6029
6315
|
this.historyRecorder.begin();
|
|
6030
6316
|
const moved = [];
|
|
@@ -6032,7 +6318,7 @@ var Viewport = class {
|
|
|
6032
6318
|
const item = sorted[i];
|
|
6033
6319
|
if (!item || !this.isMovable(item.el)) continue;
|
|
6034
6320
|
const target = c0 + i * (cN - c0) / (n - 1);
|
|
6035
|
-
const delta = target -
|
|
6321
|
+
const delta = target - center2(item.bounds);
|
|
6036
6322
|
if (delta === 0) continue;
|
|
6037
6323
|
const [dx, dy] = axis === "horizontal" ? [delta, 0] : [0, delta];
|
|
6038
6324
|
this.store.update(item.id, translateElementPatch(item.el, dx, dy));
|
|
@@ -6075,12 +6361,14 @@ var Viewport = class {
|
|
|
6075
6361
|
this.noteEditor.destroy(this.store);
|
|
6076
6362
|
this.arrowLabelEditor.cancel();
|
|
6077
6363
|
this.historyRecorder.destroy();
|
|
6364
|
+
this.contextMenu?.dispose();
|
|
6078
6365
|
this.wrapper.removeEventListener("pointerdown", this.onTapDown);
|
|
6079
6366
|
this.wrapper.removeEventListener("pointerup", this.onDoubleTap);
|
|
6080
6367
|
this.wrapper.removeEventListener("dragover", this.onDragOver);
|
|
6081
6368
|
this.wrapper.removeEventListener("drop", this.onDrop);
|
|
6082
6369
|
this.inputHandler.destroy();
|
|
6083
6370
|
this.unsubCamera();
|
|
6371
|
+
this.unsubToolChange();
|
|
6084
6372
|
this.unsubStore.forEach((fn) => fn());
|
|
6085
6373
|
this.resizeObserver?.disconnect();
|
|
6086
6374
|
this.resizeObserver = null;
|
|
@@ -6755,10 +7043,10 @@ function applyArrowHandleDrag(handle, elementId, world, ctx) {
|
|
|
6755
7043
|
const excludeId = el.toBinding?.elementId;
|
|
6756
7044
|
const target = findBindTarget(world, ctx.store, threshold, excludeId, layerFilter);
|
|
6757
7045
|
if (target) {
|
|
6758
|
-
const
|
|
7046
|
+
const center2 = getElementCenter(target);
|
|
6759
7047
|
ctx.store.update(elementId, {
|
|
6760
|
-
from:
|
|
6761
|
-
position:
|
|
7048
|
+
from: center2,
|
|
7049
|
+
position: center2,
|
|
6762
7050
|
fromBinding: { elementId: target.id }
|
|
6763
7051
|
});
|
|
6764
7052
|
} else {
|
|
@@ -6774,9 +7062,9 @@ function applyArrowHandleDrag(handle, elementId, world, ctx) {
|
|
|
6774
7062
|
const excludeId = el.fromBinding?.elementId;
|
|
6775
7063
|
const target = findBindTarget(world, ctx.store, threshold, excludeId, layerFilter);
|
|
6776
7064
|
if (target) {
|
|
6777
|
-
const
|
|
7065
|
+
const center2 = getElementCenter(target);
|
|
6778
7066
|
ctx.store.update(elementId, {
|
|
6779
|
-
to:
|
|
7067
|
+
to: center2,
|
|
6780
7068
|
toBinding: { elementId: target.id }
|
|
6781
7069
|
});
|
|
6782
7070
|
} else {
|
|
@@ -6865,6 +7153,9 @@ var SNAP_PX = 6;
|
|
|
6865
7153
|
var HANDLE_HIT_PADDING2 = 4;
|
|
6866
7154
|
var SELECTION_PAD = 4;
|
|
6867
7155
|
var MIN_ELEMENT_SIZE = 20;
|
|
7156
|
+
var ROTATE_HANDLE_OFFSET = 24;
|
|
7157
|
+
var ROTATE_SNAP = Math.PI / 12;
|
|
7158
|
+
var ROTATABLE_TYPES = /* @__PURE__ */ new Set(["note", "text", "image", "html", "shape", "stroke"]);
|
|
6868
7159
|
var HANDLE_CURSORS = {
|
|
6869
7160
|
nw: "nwse-resize",
|
|
6870
7161
|
se: "nwse-resize",
|
|
@@ -6905,6 +7196,15 @@ var SelectTool = class {
|
|
|
6905
7196
|
this.setSelectedIds(ids);
|
|
6906
7197
|
this.ctx?.requestRender();
|
|
6907
7198
|
}
|
|
7199
|
+
selectAtPoint(world, ctx) {
|
|
7200
|
+
const hit = this.hitTest(world, ctx);
|
|
7201
|
+
if (!hit) {
|
|
7202
|
+
this.setSelectedIds([]);
|
|
7203
|
+
return;
|
|
7204
|
+
}
|
|
7205
|
+
if (this._selectedIds.includes(hit.id)) return;
|
|
7206
|
+
this.setSelectedIds(expandToGroups([hit.id], ctx.store.getAll()));
|
|
7207
|
+
}
|
|
6908
7208
|
get isMarqueeActive() {
|
|
6909
7209
|
return this.mode.type === "marquee";
|
|
6910
7210
|
}
|
|
@@ -6953,6 +7253,22 @@ var SelectTool = class {
|
|
|
6953
7253
|
ctx.requestRender();
|
|
6954
7254
|
return;
|
|
6955
7255
|
}
|
|
7256
|
+
const rotateHit = this.hitTestRotateHandle(world, ctx);
|
|
7257
|
+
if (rotateHit) {
|
|
7258
|
+
const el = ctx.store.getById(rotateHit.elementId);
|
|
7259
|
+
const layout = el ? this.getOverlayLayout(el, ctx.camera.zoom) : null;
|
|
7260
|
+
if (el && layout) {
|
|
7261
|
+
this.mode = {
|
|
7262
|
+
type: "rotating",
|
|
7263
|
+
elementId: rotateHit.elementId,
|
|
7264
|
+
center: layout.center,
|
|
7265
|
+
startPointerAngle: Math.atan2(world.y - layout.center.y, world.x - layout.center.x),
|
|
7266
|
+
startRotation: el.rotation ?? 0
|
|
7267
|
+
};
|
|
7268
|
+
ctx.requestRender();
|
|
7269
|
+
return;
|
|
7270
|
+
}
|
|
7271
|
+
}
|
|
6956
7272
|
const resizeHit = this.hitTestResizeHandle(world, ctx);
|
|
6957
7273
|
if (resizeHit) {
|
|
6958
7274
|
const el = ctx.store.getById(resizeHit.elementId);
|
|
@@ -7018,6 +7334,15 @@ var SelectTool = class {
|
|
|
7018
7334
|
this.handleTemplateResize(world, ctx);
|
|
7019
7335
|
return;
|
|
7020
7336
|
}
|
|
7337
|
+
if (this.mode.type === "rotating") {
|
|
7338
|
+
const { elementId, center: center2, startPointerAngle, startRotation } = this.mode;
|
|
7339
|
+
const a = Math.atan2(world.y - center2.y, world.x - center2.x);
|
|
7340
|
+
let next = startRotation + (a - startPointerAngle);
|
|
7341
|
+
if (state.shiftKey) next = Math.round(next / ROTATE_SNAP) * ROTATE_SNAP;
|
|
7342
|
+
ctx.store.update(elementId, { rotation: normalizeAngle(next) });
|
|
7343
|
+
ctx.requestRender();
|
|
7344
|
+
return;
|
|
7345
|
+
}
|
|
7021
7346
|
if (this.mode.type === "resizing") {
|
|
7022
7347
|
ctx.setCursor?.(HANDLE_CURSORS[this.mode.handle]);
|
|
7023
7348
|
this.handleResize(world, ctx, state.shiftKey);
|
|
@@ -7220,6 +7545,10 @@ var SelectTool = class {
|
|
|
7220
7545
|
ctx.setCursor?.("nwse-resize");
|
|
7221
7546
|
return null;
|
|
7222
7547
|
}
|
|
7548
|
+
if (this.hitTestRotateHandle(world, ctx)) {
|
|
7549
|
+
ctx.setCursor?.("grab");
|
|
7550
|
+
return null;
|
|
7551
|
+
}
|
|
7223
7552
|
const resizeHit = this.hitTestResizeHandle(world, ctx);
|
|
7224
7553
|
if (resizeHit) {
|
|
7225
7554
|
ctx.setCursor?.(HANDLE_CURSORS[resizeHit.handle]);
|
|
@@ -7238,6 +7567,11 @@ var SelectTool = class {
|
|
|
7238
7567
|
if (this.mode.type !== "resizing") return;
|
|
7239
7568
|
const el = ctx.store.getById(this.mode.elementId);
|
|
7240
7569
|
if (!el || !("size" in el) || el.locked) return;
|
|
7570
|
+
const angle = el.rotation ?? 0;
|
|
7571
|
+
if (angle !== 0) {
|
|
7572
|
+
this.handleRotatedResize(world, el, angle, ctx, shiftKey);
|
|
7573
|
+
return;
|
|
7574
|
+
}
|
|
7241
7575
|
const { handle } = this.mode;
|
|
7242
7576
|
const dx = world.x - this.lastWorld.x;
|
|
7243
7577
|
const dy = world.y - this.lastWorld.y;
|
|
@@ -7295,6 +7629,78 @@ var SelectTool = class {
|
|
|
7295
7629
|
this.updateArrowsBoundTo([this.mode.elementId], ctx);
|
|
7296
7630
|
ctx.requestRender();
|
|
7297
7631
|
}
|
|
7632
|
+
anchorOffset(handle, w, h) {
|
|
7633
|
+
switch (handle) {
|
|
7634
|
+
case "se":
|
|
7635
|
+
return { x: -w / 2, y: -h / 2 };
|
|
7636
|
+
case "sw":
|
|
7637
|
+
return { x: w / 2, y: -h / 2 };
|
|
7638
|
+
case "ne":
|
|
7639
|
+
return { x: -w / 2, y: h / 2 };
|
|
7640
|
+
case "nw":
|
|
7641
|
+
return { x: w / 2, y: h / 2 };
|
|
7642
|
+
default:
|
|
7643
|
+
return { x: 0, y: 0 };
|
|
7644
|
+
}
|
|
7645
|
+
}
|
|
7646
|
+
handleRotatedResize(world, el, angle, ctx, shiftKey) {
|
|
7647
|
+
if (this.mode.type !== "resizing") return;
|
|
7648
|
+
const { handle } = this.mode;
|
|
7649
|
+
const wdx = world.x - this.lastWorld.x;
|
|
7650
|
+
const wdy = world.y - this.lastWorld.y;
|
|
7651
|
+
this.lastWorld = world;
|
|
7652
|
+
const cosN = Math.cos(-angle);
|
|
7653
|
+
const sinN = Math.sin(-angle);
|
|
7654
|
+
const ldx = wdx * cosN - wdy * sinN;
|
|
7655
|
+
const ldy = wdx * sinN + wdy * cosN;
|
|
7656
|
+
let w = el.size.w;
|
|
7657
|
+
let h = el.size.h;
|
|
7658
|
+
switch (handle) {
|
|
7659
|
+
case "se":
|
|
7660
|
+
w += ldx;
|
|
7661
|
+
h += ldy;
|
|
7662
|
+
break;
|
|
7663
|
+
case "sw":
|
|
7664
|
+
w -= ldx;
|
|
7665
|
+
h += ldy;
|
|
7666
|
+
break;
|
|
7667
|
+
case "ne":
|
|
7668
|
+
w += ldx;
|
|
7669
|
+
h -= ldy;
|
|
7670
|
+
break;
|
|
7671
|
+
case "nw":
|
|
7672
|
+
w -= ldx;
|
|
7673
|
+
h -= ldy;
|
|
7674
|
+
break;
|
|
7675
|
+
}
|
|
7676
|
+
if (shiftKey && this.resizeAspectRatio > 0) {
|
|
7677
|
+
const absDw = Math.abs(w - el.size.w);
|
|
7678
|
+
const absDh = Math.abs(h - el.size.h);
|
|
7679
|
+
if (absDw >= absDh) h = w / this.resizeAspectRatio;
|
|
7680
|
+
else w = h * this.resizeAspectRatio;
|
|
7681
|
+
}
|
|
7682
|
+
w = Math.max(w, MIN_ELEMENT_SIZE);
|
|
7683
|
+
h = Math.max(h, MIN_ELEMENT_SIZE);
|
|
7684
|
+
const oldCenter = { x: el.position.x + el.size.w / 2, y: el.position.y + el.size.h / 2 };
|
|
7685
|
+
const oldAnchorLocal = this.anchorOffset(handle, el.size.w, el.size.h);
|
|
7686
|
+
const anchorWorld = rotatePoint(
|
|
7687
|
+
{ x: oldCenter.x + oldAnchorLocal.x, y: oldCenter.y + oldAnchorLocal.y },
|
|
7688
|
+
oldCenter,
|
|
7689
|
+
angle
|
|
7690
|
+
);
|
|
7691
|
+
const newAnchorLocal = this.anchorOffset(handle, w, h);
|
|
7692
|
+
const cos = Math.cos(angle);
|
|
7693
|
+
const sin = Math.sin(angle);
|
|
7694
|
+
const rotatedAnchor = {
|
|
7695
|
+
x: newAnchorLocal.x * cos - newAnchorLocal.y * sin,
|
|
7696
|
+
y: newAnchorLocal.x * sin + newAnchorLocal.y * cos
|
|
7697
|
+
};
|
|
7698
|
+
const newCenter = { x: anchorWorld.x - rotatedAnchor.x, y: anchorWorld.y - rotatedAnchor.y };
|
|
7699
|
+
const position = { x: newCenter.x - w / 2, y: newCenter.y - h / 2 };
|
|
7700
|
+
ctx.store.update(this.mode.elementId, { position, size: { w, h } });
|
|
7701
|
+
this.updateArrowsBoundTo([this.mode.elementId], ctx);
|
|
7702
|
+
ctx.requestRender();
|
|
7703
|
+
}
|
|
7298
7704
|
hitTestResizeHandle(world, ctx) {
|
|
7299
7705
|
if (this._selectedIds.length === 0) return null;
|
|
7300
7706
|
const zoom = ctx.camera.zoom;
|
|
@@ -7302,11 +7708,11 @@ var SelectTool = class {
|
|
|
7302
7708
|
for (const id of this._selectedIds) {
|
|
7303
7709
|
const el = ctx.store.getById(id);
|
|
7304
7710
|
if (!el || !("size" in el)) continue;
|
|
7711
|
+
if (el.locked) continue;
|
|
7305
7712
|
if (el.type === "shape" && el.shape === "line") continue;
|
|
7306
|
-
const
|
|
7307
|
-
if (!
|
|
7308
|
-
const
|
|
7309
|
-
for (const [handle, pos] of corners) {
|
|
7713
|
+
const layout = this.getOverlayLayout(el, zoom);
|
|
7714
|
+
if (!layout) continue;
|
|
7715
|
+
for (const [handle, pos] of layout.corners) {
|
|
7310
7716
|
if (Math.abs(world.x - pos.x) <= handleHalf && Math.abs(world.y - pos.y) <= handleHalf) {
|
|
7311
7717
|
return { elementId: id, handle };
|
|
7312
7718
|
}
|
|
@@ -7314,6 +7720,19 @@ var SelectTool = class {
|
|
|
7314
7720
|
}
|
|
7315
7721
|
return null;
|
|
7316
7722
|
}
|
|
7723
|
+
hitTestRotateHandle(world, ctx) {
|
|
7724
|
+
if (this._selectedIds.length !== 1) return null;
|
|
7725
|
+
const id = this._selectedIds[0];
|
|
7726
|
+
if (!id) return null;
|
|
7727
|
+
const el = ctx.store.getById(id);
|
|
7728
|
+
if (!el || el.locked || !ROTATABLE_TYPES.has(el.type)) return null;
|
|
7729
|
+
const layout = this.getOverlayLayout(el, ctx.camera.zoom);
|
|
7730
|
+
if (!layout) return null;
|
|
7731
|
+
const r = (HANDLE_SIZE / 2 + HANDLE_HIT_PADDING2) / ctx.camera.zoom;
|
|
7732
|
+
const dx = world.x - layout.rotateHandle.x;
|
|
7733
|
+
const dy = world.y - layout.rotateHandle.y;
|
|
7734
|
+
return dx * dx + dy * dy <= r * r ? { elementId: id } : null;
|
|
7735
|
+
}
|
|
7317
7736
|
hitTestLineHandles(world, ctx) {
|
|
7318
7737
|
if (this._selectedIds.length === 0) return null;
|
|
7319
7738
|
const zoom = ctx.camera.zoom;
|
|
@@ -7336,6 +7755,30 @@ var SelectTool = class {
|
|
|
7336
7755
|
["se", { x: bounds.x + bounds.w, y: bounds.y + bounds.h }]
|
|
7337
7756
|
];
|
|
7338
7757
|
}
|
|
7758
|
+
getOverlayLayout(el, zoom) {
|
|
7759
|
+
const bounds = getElementBounds(el);
|
|
7760
|
+
if (!bounds) return null;
|
|
7761
|
+
const angle = el.rotation ?? 0;
|
|
7762
|
+
const pad = SELECTION_PAD / zoom;
|
|
7763
|
+
const center2 = { x: bounds.x + bounds.w / 2, y: bounds.y + bounds.h / 2 };
|
|
7764
|
+
const raw = [
|
|
7765
|
+
["nw", { x: bounds.x - pad, y: bounds.y - pad }],
|
|
7766
|
+
["ne", { x: bounds.x + bounds.w + pad, y: bounds.y - pad }],
|
|
7767
|
+
["sw", { x: bounds.x - pad, y: bounds.y + bounds.h + pad }],
|
|
7768
|
+
["se", { x: bounds.x + bounds.w + pad, y: bounds.y + bounds.h + pad }]
|
|
7769
|
+
];
|
|
7770
|
+
const corners = raw.map(
|
|
7771
|
+
([h, p]) => [h, rotatePoint(p, center2, angle)]
|
|
7772
|
+
);
|
|
7773
|
+
const topMid = { x: center2.x, y: bounds.y - pad - ROTATE_HANDLE_OFFSET / zoom };
|
|
7774
|
+
const rotateHandle = rotatePoint(topMid, center2, angle);
|
|
7775
|
+
return { center: center2, corners, rotateHandle, angle };
|
|
7776
|
+
}
|
|
7777
|
+
topMidpoint(layout) {
|
|
7778
|
+
const nw = layout.corners.find(([h]) => h === "nw")?.[1] ?? { x: 0, y: 0 };
|
|
7779
|
+
const ne = layout.corners.find(([h]) => h === "ne")?.[1] ?? { x: 0, y: 0 };
|
|
7780
|
+
return { x: (nw.x + ne.x) / 2, y: (nw.y + ne.y) / 2 };
|
|
7781
|
+
}
|
|
7339
7782
|
renderMarquee(canvasCtx) {
|
|
7340
7783
|
if (this.mode.type !== "marquee") return;
|
|
7341
7784
|
const rect = this.getMarqueeRect();
|
|
@@ -7380,49 +7823,110 @@ var SelectTool = class {
|
|
|
7380
7823
|
}
|
|
7381
7824
|
const bounds = getElementBounds(el);
|
|
7382
7825
|
if (!bounds) continue;
|
|
7826
|
+
const layout = this.getOverlayLayout(el, zoom);
|
|
7827
|
+
if (!layout) continue;
|
|
7383
7828
|
const pad = SELECTION_PAD / zoom;
|
|
7384
|
-
|
|
7385
|
-
|
|
7386
|
-
|
|
7387
|
-
|
|
7388
|
-
|
|
7389
|
-
|
|
7829
|
+
if (layout.angle === 0) {
|
|
7830
|
+
canvasCtx.strokeRect(
|
|
7831
|
+
bounds.x - pad,
|
|
7832
|
+
bounds.y - pad,
|
|
7833
|
+
bounds.w + pad * 2,
|
|
7834
|
+
bounds.h + pad * 2
|
|
7835
|
+
);
|
|
7836
|
+
} else {
|
|
7837
|
+
const ordered = ["nw", "ne", "se", "sw"].map((h) => layout.corners.find(([c]) => c === h)?.[1]).filter((p) => !!p);
|
|
7838
|
+
const [p0, ...others] = ordered;
|
|
7839
|
+
if (p0) {
|
|
7840
|
+
canvasCtx.beginPath();
|
|
7841
|
+
canvasCtx.moveTo(p0.x, p0.y);
|
|
7842
|
+
for (const p of others) canvasCtx.lineTo(p.x, p.y);
|
|
7843
|
+
canvasCtx.closePath();
|
|
7844
|
+
canvasCtx.stroke();
|
|
7845
|
+
}
|
|
7846
|
+
}
|
|
7847
|
+
if (!el.locked) {
|
|
7848
|
+
if ("size" in el) {
|
|
7849
|
+
canvasCtx.setLineDash([]);
|
|
7850
|
+
canvasCtx.fillStyle = "#ffffff";
|
|
7851
|
+
const corners = layout.angle === 0 ? this.getHandlePositions(bounds) : layout.corners;
|
|
7852
|
+
for (const [, pos] of corners) {
|
|
7853
|
+
canvasCtx.fillRect(
|
|
7854
|
+
pos.x - handleWorldSize / 2,
|
|
7855
|
+
pos.y - handleWorldSize / 2,
|
|
7856
|
+
handleWorldSize,
|
|
7857
|
+
handleWorldSize
|
|
7858
|
+
);
|
|
7859
|
+
canvasCtx.strokeRect(
|
|
7860
|
+
pos.x - handleWorldSize / 2,
|
|
7861
|
+
pos.y - handleWorldSize / 2,
|
|
7862
|
+
handleWorldSize,
|
|
7863
|
+
handleWorldSize
|
|
7864
|
+
);
|
|
7865
|
+
}
|
|
7866
|
+
canvasCtx.setLineDash([4 / zoom, 4 / zoom]);
|
|
7867
|
+
} else if (el.type === "template") {
|
|
7868
|
+
canvasCtx.setLineDash([]);
|
|
7869
|
+
canvasCtx.fillStyle = "#ffffff";
|
|
7870
|
+
const hx = bounds.x + bounds.w;
|
|
7871
|
+
const hy = bounds.y + bounds.h;
|
|
7390
7872
|
canvasCtx.fillRect(
|
|
7391
|
-
|
|
7392
|
-
|
|
7873
|
+
hx - handleWorldSize / 2,
|
|
7874
|
+
hy - handleWorldSize / 2,
|
|
7393
7875
|
handleWorldSize,
|
|
7394
7876
|
handleWorldSize
|
|
7395
7877
|
);
|
|
7396
7878
|
canvasCtx.strokeRect(
|
|
7397
|
-
|
|
7398
|
-
|
|
7879
|
+
hx - handleWorldSize / 2,
|
|
7880
|
+
hy - handleWorldSize / 2,
|
|
7399
7881
|
handleWorldSize,
|
|
7400
7882
|
handleWorldSize
|
|
7401
7883
|
);
|
|
7884
|
+
canvasCtx.setLineDash([4 / zoom, 4 / zoom]);
|
|
7885
|
+
}
|
|
7886
|
+
if (this._selectedIds.length === 1 && ROTATABLE_TYPES.has(el.type)) {
|
|
7887
|
+
const stemStart = this.topMidpoint(layout);
|
|
7888
|
+
const stemEnd = layout.rotateHandle;
|
|
7889
|
+
canvasCtx.beginPath();
|
|
7890
|
+
canvasCtx.moveTo(stemStart.x, stemStart.y);
|
|
7891
|
+
canvasCtx.lineTo(stemEnd.x, stemEnd.y);
|
|
7892
|
+
canvasCtx.stroke();
|
|
7893
|
+
canvasCtx.setLineDash([]);
|
|
7894
|
+
canvasCtx.fillStyle = "#ffffff";
|
|
7895
|
+
canvasCtx.beginPath();
|
|
7896
|
+
canvasCtx.arc(stemEnd.x, stemEnd.y, handleWorldSize / 2, 0, Math.PI * 2);
|
|
7897
|
+
canvasCtx.fill();
|
|
7898
|
+
canvasCtx.stroke();
|
|
7899
|
+
canvasCtx.setLineDash([4 / zoom, 4 / zoom]);
|
|
7402
7900
|
}
|
|
7403
|
-
|
|
7404
|
-
|
|
7405
|
-
|
|
7406
|
-
canvasCtx
|
|
7407
|
-
const hx = bounds.x + bounds.w;
|
|
7408
|
-
const hy = bounds.y + bounds.h;
|
|
7409
|
-
canvasCtx.fillRect(
|
|
7410
|
-
hx - handleWorldSize / 2,
|
|
7411
|
-
hy - handleWorldSize / 2,
|
|
7412
|
-
handleWorldSize,
|
|
7413
|
-
handleWorldSize
|
|
7414
|
-
);
|
|
7415
|
-
canvasCtx.strokeRect(
|
|
7416
|
-
hx - handleWorldSize / 2,
|
|
7417
|
-
hy - handleWorldSize / 2,
|
|
7418
|
-
handleWorldSize,
|
|
7419
|
-
handleWorldSize
|
|
7420
|
-
);
|
|
7421
|
-
canvasCtx.setLineDash([4 / zoom, 4 / zoom]);
|
|
7901
|
+
}
|
|
7902
|
+
if (el.locked) {
|
|
7903
|
+
const ne = layout.corners.find(([h]) => h === "ne")?.[1];
|
|
7904
|
+
if (ne) this.drawLockBadge(canvasCtx, ne, zoom);
|
|
7422
7905
|
}
|
|
7423
7906
|
}
|
|
7424
7907
|
canvasCtx.restore();
|
|
7425
7908
|
}
|
|
7909
|
+
drawLockBadge(ctx, at, zoom) {
|
|
7910
|
+
const r = 9 / zoom;
|
|
7911
|
+
ctx.save();
|
|
7912
|
+
ctx.setLineDash([]);
|
|
7913
|
+
ctx.beginPath();
|
|
7914
|
+
ctx.arc(at.x, at.y, r, 0, Math.PI * 2);
|
|
7915
|
+
ctx.fillStyle = "#ffffff";
|
|
7916
|
+
ctx.fill();
|
|
7917
|
+
ctx.strokeStyle = "#2196F3";
|
|
7918
|
+
ctx.lineWidth = 1.5 / zoom;
|
|
7919
|
+
ctx.stroke();
|
|
7920
|
+
const bw = 8 / zoom;
|
|
7921
|
+
const bh = 6 / zoom;
|
|
7922
|
+
ctx.fillStyle = "#2196F3";
|
|
7923
|
+
ctx.fillRect(at.x - bw / 2, at.y - bh / 2 + 1 / zoom, bw, bh);
|
|
7924
|
+
ctx.beginPath();
|
|
7925
|
+
ctx.arc(at.x, at.y - bh / 2 + 1 / zoom, 2.5 / zoom, Math.PI, 0);
|
|
7926
|
+
ctx.lineWidth = 1.4 / zoom;
|
|
7927
|
+
ctx.stroke();
|
|
7928
|
+
ctx.restore();
|
|
7929
|
+
}
|
|
7426
7930
|
renderBindingHighlights(canvasCtx, arrow, zoom) {
|
|
7427
7931
|
if (!this.ctx) return;
|
|
7428
7932
|
if (!arrow.fromBinding && !arrow.toBinding) return;
|
|
@@ -7499,7 +8003,7 @@ var SelectTool = class {
|
|
|
7499
8003
|
if (ctx.isLayerLocked && ctx.isLayerLocked(el.layerId)) continue;
|
|
7500
8004
|
if (el.type === "grid") continue;
|
|
7501
8005
|
const bounds = getElementBounds(el);
|
|
7502
|
-
if (bounds && this.rectsOverlap(marquee, bounds)) {
|
|
8006
|
+
if (bounds && this.rectsOverlap(marquee, rotatedAABB(bounds, el.rotation ?? 0))) {
|
|
7503
8007
|
ids.push(el.id);
|
|
7504
8008
|
}
|
|
7505
8009
|
}
|
|
@@ -7521,6 +8025,13 @@ var SelectTool = class {
|
|
|
7521
8025
|
}
|
|
7522
8026
|
isInsideBounds(point, el) {
|
|
7523
8027
|
if (el.type === "grid") return false;
|
|
8028
|
+
const angle = el.rotation ?? 0;
|
|
8029
|
+
if (angle !== 0) {
|
|
8030
|
+
const b = getElementBounds(el);
|
|
8031
|
+
if (b) {
|
|
8032
|
+
point = rotatePoint(point, { x: b.x + b.w / 2, y: b.y + b.h / 2 }, -angle);
|
|
8033
|
+
}
|
|
8034
|
+
}
|
|
7524
8035
|
if (el.type === "shape" && el.shape === "line") {
|
|
7525
8036
|
const [a, b] = lineEndpoints(el);
|
|
7526
8037
|
const threshold = Math.max(el.strokeWidth / 2, 6);
|
|
@@ -8287,20 +8798,20 @@ var TemplateTool = class {
|
|
|
8287
8798
|
const snapUnit = Math.sqrt(3) * cellSize;
|
|
8288
8799
|
const radiusCells = radius / snapUnit;
|
|
8289
8800
|
const angle = this.computeAngle();
|
|
8290
|
-
const
|
|
8801
|
+
const center2 = this.origin;
|
|
8291
8802
|
let hexCells;
|
|
8292
8803
|
switch (this.templateShape) {
|
|
8293
8804
|
case "circle":
|
|
8294
|
-
hexCells = getHexCellsInRadius(
|
|
8805
|
+
hexCells = getHexCellsInRadius(center2, radiusCells, cellSize, orientation);
|
|
8295
8806
|
break;
|
|
8296
8807
|
case "cone":
|
|
8297
|
-
hexCells = getHexCellsInCone(
|
|
8808
|
+
hexCells = getHexCellsInCone(center2, angle, radiusCells, cellSize, orientation);
|
|
8298
8809
|
break;
|
|
8299
8810
|
case "line":
|
|
8300
|
-
hexCells = getHexCellsInLine(
|
|
8811
|
+
hexCells = getHexCellsInLine(center2, angle, radiusCells, cellSize, orientation);
|
|
8301
8812
|
break;
|
|
8302
8813
|
case "square":
|
|
8303
|
-
hexCells = getHexCellsInSquare(
|
|
8814
|
+
hexCells = getHexCellsInSquare(center2, radiusCells, cellSize, orientation);
|
|
8304
8815
|
break;
|
|
8305
8816
|
}
|
|
8306
8817
|
ctx.save();
|
|
@@ -8321,7 +8832,7 @@ var TemplateTool = class {
|
|
|
8321
8832
|
if (this.templateShape === "cone" || this.templateShape === "line" || this.templateShape === "circle" || this.templateShape === "square") {
|
|
8322
8833
|
ctx.globalAlpha = 0.5;
|
|
8323
8834
|
ctx.beginPath();
|
|
8324
|
-
drawHexPath(ctx,
|
|
8835
|
+
drawHexPath(ctx, center2.x, center2.y, cellSize, orientation);
|
|
8325
8836
|
ctx.fillStyle = this.strokeColor;
|
|
8326
8837
|
ctx.fill();
|
|
8327
8838
|
ctx.strokeStyle = this.strokeColor;
|
|
@@ -8337,8 +8848,8 @@ var TemplateTool = class {
|
|
|
8337
8848
|
ctx.font = `bold ${fontSize}px system-ui, sans-serif`;
|
|
8338
8849
|
ctx.textAlign = "center";
|
|
8339
8850
|
ctx.textBaseline = "bottom";
|
|
8340
|
-
const textX =
|
|
8341
|
-
const textY =
|
|
8851
|
+
const textX = center2.x;
|
|
8852
|
+
const textY = center2.y - 4;
|
|
8342
8853
|
const metrics = ctx.measureText(label);
|
|
8343
8854
|
const padX = 4;
|
|
8344
8855
|
const padY = 2;
|
|
@@ -8388,7 +8899,7 @@ var TemplateTool = class {
|
|
|
8388
8899
|
};
|
|
8389
8900
|
|
|
8390
8901
|
// src/index.ts
|
|
8391
|
-
var VERSION = "0.
|
|
8902
|
+
var VERSION = "0.36.0";
|
|
8392
8903
|
// Annotate the CommonJS export names for ESM import in node:
|
|
8393
8904
|
0 && (module.exports = {
|
|
8394
8905
|
ArrowTool,
|