@fieldnotes/core 0.35.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 +8 -0
- package/dist/index.cjs +310 -63
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -1
- package/dist/index.d.ts +11 -1
- package/dist/index.js +310 -63
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -616,6 +616,14 @@ Select a single element and a rotate handle appears above the selection box. Dra
|
|
|
616
616
|
|
|
617
617
|
Hit-testing, marquee selection, and resize are all rotation-aware: resizing a rotated element keeps the opposite corner fixed in the element's local frame. Rotation is reflected in PNG export and round-trips through serialization (`rotation?` on elements, stored in radians).
|
|
618
618
|
|
|
619
|
+
## Context menu & lock
|
|
620
|
+
|
|
621
|
+
Right-click (desktop) or touch long-press (tablet) opens a context menu over the canvas with Cut/Copy/Paste/Duplicate/Delete, z-order (to front / forward / backward / to back), and Lock/Unlock. The menu is core-provided (plain DOM) and selects the element under the pointer if it isn't already selected. Opt out with `new Viewport(el, { contextMenu: false })`.
|
|
622
|
+
|
|
623
|
+
Lock with **`viewport.toggleLockSelection()`** or **Ctrl/Cmd+Shift+L**; a lock badge appears on the selection. Locked elements stay selectable but can't be moved, resized, or rotated. **Ctrl/Cmd+X** cuts the selection. The shortcuts are rebindable as `toggle-lock` and `cut`.
|
|
624
|
+
|
|
625
|
+
You can drive any menu action programmatically with **`viewport.runAction(name)`** (e.g. `'cut'`, `'paste'`, `'toggle-lock'`), and **`viewport.canPaste()`** reports whether the clipboard has content.
|
|
626
|
+
|
|
619
627
|
## Built-in Interactions
|
|
620
628
|
|
|
621
629
|
| Input | Action |
|
package/dist/index.cjs
CHANGED
|
@@ -991,6 +991,14 @@ var KeyboardActions = class {
|
|
|
991
991
|
}
|
|
992
992
|
this.pasteCount = 0;
|
|
993
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
|
+
}
|
|
994
1002
|
paste() {
|
|
995
1003
|
if (this.deps.isToolActive()) return;
|
|
996
1004
|
this.flushPendingNudge();
|
|
@@ -1059,6 +1067,10 @@ var KeyboardActions = class {
|
|
|
1059
1067
|
if (this.deps.isToolActive()) return;
|
|
1060
1068
|
this.deps.ungroup?.();
|
|
1061
1069
|
}
|
|
1070
|
+
toggleLock() {
|
|
1071
|
+
if (this.deps.isToolActive()) return;
|
|
1072
|
+
this.deps.toggleLock?.();
|
|
1073
|
+
}
|
|
1062
1074
|
zOrder(operation) {
|
|
1063
1075
|
if (this.deps.isToolActive()) return;
|
|
1064
1076
|
this.flushPendingNudge();
|
|
@@ -1160,6 +1172,8 @@ var DEFAULT_BINDINGS = [
|
|
|
1160
1172
|
["zoom-reset", ["mod+0"]],
|
|
1161
1173
|
["group", ["mod+g"]],
|
|
1162
1174
|
["ungroup", ["mod+shift+g"]],
|
|
1175
|
+
["cut", ["mod+x"]],
|
|
1176
|
+
["toggle-lock", ["mod+shift+l"]],
|
|
1163
1177
|
["nudge-left", ["arrowleft"]],
|
|
1164
1178
|
["nudge-right", ["arrowright"]],
|
|
1165
1179
|
["nudge-up", ["arrowup"]],
|
|
@@ -1298,6 +1312,7 @@ var ShortcutMap = class {
|
|
|
1298
1312
|
var ZOOM_SENSITIVITY = 1e-3;
|
|
1299
1313
|
var ZOOM_STEP = 1.2;
|
|
1300
1314
|
var MIDDLE_BUTTON = 1;
|
|
1315
|
+
var LONG_PRESS_MS = 500;
|
|
1301
1316
|
var NUDGE_DELTAS = {
|
|
1302
1317
|
"nudge-left": [-1, 0],
|
|
1303
1318
|
"nudge-right": [1, 0],
|
|
@@ -1321,8 +1336,10 @@ var InputHandler = class {
|
|
|
1321
1336
|
fitToContent: options.fitToContent,
|
|
1322
1337
|
group: options.group,
|
|
1323
1338
|
ungroup: options.ungroup,
|
|
1339
|
+
toggleLock: options.toggleLock,
|
|
1324
1340
|
getLastPointerWorld: () => this.lastPointerWorld()
|
|
1325
1341
|
});
|
|
1342
|
+
this.openContextMenu = options.openContextMenu;
|
|
1326
1343
|
this.shortcutMap = new ShortcutMap(options.shortcuts?.bindings);
|
|
1327
1344
|
this.scope = options.shortcuts?.scope ?? "focus";
|
|
1328
1345
|
this.element.style.touchAction = "none";
|
|
@@ -1346,10 +1363,13 @@ var InputHandler = class {
|
|
|
1346
1363
|
lastPointerEvent = null;
|
|
1347
1364
|
inputFilter = new InputFilter();
|
|
1348
1365
|
deferredDown = null;
|
|
1366
|
+
longPressTimer = null;
|
|
1367
|
+
longPressStart = null;
|
|
1349
1368
|
abortController = new AbortController();
|
|
1350
1369
|
actions;
|
|
1351
1370
|
shortcutMap;
|
|
1352
1371
|
scope;
|
|
1372
|
+
openContextMenu;
|
|
1353
1373
|
setToolManager(toolManager, toolContext) {
|
|
1354
1374
|
this.toolManager = toolManager;
|
|
1355
1375
|
this.toolContext = toolContext;
|
|
@@ -1364,6 +1384,7 @@ var InputHandler = class {
|
|
|
1364
1384
|
this.actions.dispose();
|
|
1365
1385
|
this.abortController.abort();
|
|
1366
1386
|
this.inputFilter.reset();
|
|
1387
|
+
this.cancelLongPress();
|
|
1367
1388
|
this.deferredDown = null;
|
|
1368
1389
|
this.lastPointerEvent = null;
|
|
1369
1390
|
if (this.scope === "focus") {
|
|
@@ -1379,6 +1400,7 @@ var InputHandler = class {
|
|
|
1379
1400
|
this.element.addEventListener("pointerup", this.onPointerUp, opts);
|
|
1380
1401
|
this.element.addEventListener("pointerleave", this.onPointerLeave, opts);
|
|
1381
1402
|
this.element.addEventListener("pointercancel", this.onPointerUp, opts);
|
|
1403
|
+
this.element.addEventListener("contextmenu", this.onContextMenu, opts);
|
|
1382
1404
|
window.addEventListener("keydown", this.onKeyDown, opts);
|
|
1383
1405
|
window.addEventListener("keyup", this.onKeyUp, opts);
|
|
1384
1406
|
}
|
|
@@ -1407,11 +1429,13 @@ var InputHandler = class {
|
|
|
1407
1429
|
this.activePointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
|
1408
1430
|
this.element.setPointerCapture?.(e.pointerId);
|
|
1409
1431
|
if (this.activePointers.size === 2) {
|
|
1432
|
+
this.cancelLongPress();
|
|
1410
1433
|
this.startPinch();
|
|
1411
1434
|
this.cancelToolIfActive(e);
|
|
1412
1435
|
return;
|
|
1413
1436
|
}
|
|
1414
1437
|
if (e.button === MIDDLE_BUTTON || e.button === 0 && this.spaceHeld) {
|
|
1438
|
+
this.cancelLongPress();
|
|
1415
1439
|
this.isPanning = true;
|
|
1416
1440
|
this.lastPointer = { x: e.clientX, y: e.clientY };
|
|
1417
1441
|
return;
|
|
@@ -1421,6 +1445,7 @@ var InputHandler = class {
|
|
|
1421
1445
|
if (result.action === "suppress") return;
|
|
1422
1446
|
if (result.action === "defer") {
|
|
1423
1447
|
this.deferredDown = e;
|
|
1448
|
+
this.startLongPress(e);
|
|
1424
1449
|
return;
|
|
1425
1450
|
}
|
|
1426
1451
|
this.dispatchToolDown(e);
|
|
@@ -1449,6 +1474,7 @@ var InputHandler = class {
|
|
|
1449
1474
|
} else if (this.deferredDown) {
|
|
1450
1475
|
const result = this.inputFilter.filterMove(e);
|
|
1451
1476
|
if (result.action === "dispatch") {
|
|
1477
|
+
this.cancelLongPress();
|
|
1452
1478
|
this.dispatchToolDown(this.deferredDown);
|
|
1453
1479
|
this.deferredDown = null;
|
|
1454
1480
|
this.dispatchToolMove(e);
|
|
@@ -1458,6 +1484,7 @@ var InputHandler = class {
|
|
|
1458
1484
|
}
|
|
1459
1485
|
};
|
|
1460
1486
|
onPointerUp = (e) => {
|
|
1487
|
+
this.cancelLongPress();
|
|
1461
1488
|
try {
|
|
1462
1489
|
this.element.releasePointerCapture(e.pointerId);
|
|
1463
1490
|
} catch {
|
|
@@ -1510,74 +1537,82 @@ var InputHandler = class {
|
|
|
1510
1537
|
runAction(action, e) {
|
|
1511
1538
|
switch (action) {
|
|
1512
1539
|
case "delete":
|
|
1513
|
-
e
|
|
1540
|
+
e?.preventDefault();
|
|
1514
1541
|
this.actions.deleteSelected();
|
|
1515
1542
|
return;
|
|
1516
1543
|
case "deselect":
|
|
1517
1544
|
this.actions.deselect();
|
|
1518
1545
|
return;
|
|
1519
1546
|
case "undo":
|
|
1520
|
-
e
|
|
1547
|
+
e?.preventDefault();
|
|
1521
1548
|
this.actions.undo();
|
|
1522
1549
|
return;
|
|
1523
1550
|
case "redo":
|
|
1524
|
-
e
|
|
1551
|
+
e?.preventDefault();
|
|
1525
1552
|
this.actions.redo();
|
|
1526
1553
|
return;
|
|
1527
1554
|
case "select-all":
|
|
1528
|
-
e
|
|
1555
|
+
e?.preventDefault();
|
|
1529
1556
|
this.actions.selectAll();
|
|
1530
1557
|
return;
|
|
1531
1558
|
case "copy":
|
|
1532
|
-
e
|
|
1559
|
+
e?.preventDefault();
|
|
1533
1560
|
this.actions.copy();
|
|
1534
1561
|
return;
|
|
1535
1562
|
case "paste":
|
|
1536
|
-
e
|
|
1563
|
+
e?.preventDefault();
|
|
1537
1564
|
this.actions.paste();
|
|
1538
1565
|
return;
|
|
1539
1566
|
case "duplicate":
|
|
1540
|
-
e
|
|
1567
|
+
e?.preventDefault();
|
|
1541
1568
|
this.actions.duplicate();
|
|
1542
1569
|
return;
|
|
1543
1570
|
case "z-forward":
|
|
1544
|
-
e
|
|
1571
|
+
e?.preventDefault();
|
|
1545
1572
|
this.actions.zOrder("forward");
|
|
1546
1573
|
return;
|
|
1547
1574
|
case "z-backward":
|
|
1548
|
-
e
|
|
1575
|
+
e?.preventDefault();
|
|
1549
1576
|
this.actions.zOrder("backward");
|
|
1550
1577
|
return;
|
|
1551
1578
|
case "z-front":
|
|
1552
|
-
e
|
|
1579
|
+
e?.preventDefault();
|
|
1553
1580
|
this.actions.zOrder("front");
|
|
1554
1581
|
return;
|
|
1555
1582
|
case "z-back":
|
|
1556
|
-
e
|
|
1583
|
+
e?.preventDefault();
|
|
1557
1584
|
this.actions.zOrder("back");
|
|
1558
1585
|
return;
|
|
1559
1586
|
case "zoom-fit":
|
|
1560
|
-
e
|
|
1587
|
+
e?.preventDefault();
|
|
1561
1588
|
this.actions.zoomToFit();
|
|
1562
1589
|
return;
|
|
1563
1590
|
case "group":
|
|
1564
|
-
e
|
|
1591
|
+
e?.preventDefault();
|
|
1565
1592
|
this.actions.group();
|
|
1566
1593
|
return;
|
|
1567
1594
|
case "ungroup":
|
|
1568
|
-
e
|
|
1595
|
+
e?.preventDefault();
|
|
1569
1596
|
this.actions.ungroup();
|
|
1570
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;
|
|
1571
1606
|
case "zoom-in":
|
|
1572
|
-
e
|
|
1607
|
+
e?.preventDefault();
|
|
1573
1608
|
this.zoomByFactor(ZOOM_STEP);
|
|
1574
1609
|
return;
|
|
1575
1610
|
case "zoom-out":
|
|
1576
|
-
e
|
|
1611
|
+
e?.preventDefault();
|
|
1577
1612
|
this.zoomByFactor(1 / ZOOM_STEP);
|
|
1578
1613
|
return;
|
|
1579
1614
|
case "zoom-reset":
|
|
1580
|
-
e
|
|
1615
|
+
e?.preventDefault();
|
|
1581
1616
|
this.zoomToLevel(1);
|
|
1582
1617
|
return;
|
|
1583
1618
|
case "nudge-left":
|
|
@@ -1585,22 +1620,26 @@ var InputHandler = class {
|
|
|
1585
1620
|
case "nudge-up":
|
|
1586
1621
|
case "nudge-down": {
|
|
1587
1622
|
const delta = NUDGE_DELTAS[action];
|
|
1588
|
-
if (delta && this.actions.nudge(delta[0], delta[1], e
|
|
1589
|
-
e
|
|
1623
|
+
if (delta && this.actions.nudge(delta[0], delta[1], e?.shiftKey ?? false)) {
|
|
1624
|
+
e?.preventDefault();
|
|
1590
1625
|
}
|
|
1591
1626
|
return;
|
|
1592
1627
|
}
|
|
1593
1628
|
default:
|
|
1594
1629
|
if (action.startsWith("tool:")) {
|
|
1595
1630
|
if (this.isToolActive) return;
|
|
1596
|
-
e
|
|
1631
|
+
e?.preventDefault();
|
|
1597
1632
|
this.toolContext?.switchTool?.(action.slice("tool:".length));
|
|
1598
1633
|
return;
|
|
1599
1634
|
}
|
|
1600
1635
|
console.warn(`[fieldnotes] unknown shortcut action "${action}"`);
|
|
1601
1636
|
}
|
|
1602
1637
|
}
|
|
1638
|
+
hasClipboard() {
|
|
1639
|
+
return this.actions.hasClipboard();
|
|
1640
|
+
}
|
|
1603
1641
|
startPinch() {
|
|
1642
|
+
this.cancelLongPress();
|
|
1604
1643
|
this.inputFilter.reset();
|
|
1605
1644
|
this.deferredDown = null;
|
|
1606
1645
|
this.isPanning = true;
|
|
@@ -1643,6 +1682,13 @@ var InputHandler = class {
|
|
|
1643
1682
|
const rect = this.element.getBoundingClientRect();
|
|
1644
1683
|
return this.camera.screenToWorld({ x: e.clientX - rect.left, y: e.clientY - rect.top });
|
|
1645
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
|
+
};
|
|
1646
1692
|
onPointerLeave = (e) => {
|
|
1647
1693
|
this.lastPointerEvent = null;
|
|
1648
1694
|
this.onPointerUp(e);
|
|
@@ -1690,12 +1736,36 @@ var InputHandler = class {
|
|
|
1690
1736
|
this.element.focus({ preventScroll: true });
|
|
1691
1737
|
}
|
|
1692
1738
|
cancelToolIfActive(e) {
|
|
1739
|
+
this.cancelLongPress();
|
|
1693
1740
|
if (this.isToolActive) {
|
|
1694
1741
|
this.dispatchToolUp(e);
|
|
1695
1742
|
this.isToolActive = false;
|
|
1696
1743
|
}
|
|
1697
1744
|
this.deferredDown = null;
|
|
1698
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
|
+
}
|
|
1699
1769
|
};
|
|
1700
1770
|
|
|
1701
1771
|
// src/canvas/background.ts
|
|
@@ -3882,6 +3952,87 @@ var NoteEditor = class {
|
|
|
3882
3952
|
}
|
|
3883
3953
|
};
|
|
3884
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
|
+
|
|
3885
4036
|
// src/elements/translate.ts
|
|
3886
4037
|
function translateElementPatch(el, dx, dy) {
|
|
3887
4038
|
const position = { x: el.position.x + dx, y: el.position.y + dy };
|
|
@@ -5698,8 +5849,20 @@ var Viewport = class {
|
|
|
5698
5849
|
fitToContent: () => this.fitToContent(),
|
|
5699
5850
|
group: () => this.groupSelection(),
|
|
5700
5851
|
ungroup: () => this.ungroupSelection(),
|
|
5852
|
+
toggleLock: () => this.toggleLockSelection(),
|
|
5853
|
+
openContextMenu: (screenPos, world) => {
|
|
5854
|
+
this.getSelectTool()?.selectAtPoint(world, this.toolContext);
|
|
5855
|
+
this.openContextMenu(screenPos);
|
|
5856
|
+
},
|
|
5701
5857
|
shortcuts: options.shortcuts
|
|
5702
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());
|
|
5703
5866
|
this.domNodeManager = new DomNodeManager({
|
|
5704
5867
|
domLayer: this.domLayer,
|
|
5705
5868
|
onEditRequest: (id) => this.startEditingElement(id),
|
|
@@ -5731,6 +5894,7 @@ var Viewport = class {
|
|
|
5731
5894
|
this.unsubCamera = this.camera.onChange(() => {
|
|
5732
5895
|
this.applyCameraTransform();
|
|
5733
5896
|
this.noteEditor.updateToolbarPosition();
|
|
5897
|
+
this.contextMenu?.close();
|
|
5734
5898
|
this.requestRender();
|
|
5735
5899
|
});
|
|
5736
5900
|
this.unsubStore = [
|
|
@@ -5783,6 +5947,7 @@ var Viewport = class {
|
|
|
5783
5947
|
canvasEl;
|
|
5784
5948
|
wrapper;
|
|
5785
5949
|
unsubCamera;
|
|
5950
|
+
unsubToolChange;
|
|
5786
5951
|
unsubStore;
|
|
5787
5952
|
inputHandler;
|
|
5788
5953
|
background;
|
|
@@ -5805,6 +5970,7 @@ var Viewport = class {
|
|
|
5805
5970
|
doubleTapDetector = new DoubleTapDetector();
|
|
5806
5971
|
tapDownX = 0;
|
|
5807
5972
|
tapDownY = 0;
|
|
5973
|
+
contextMenu = null;
|
|
5808
5974
|
get ctx() {
|
|
5809
5975
|
return this.canvasEl.getContext("2d");
|
|
5810
5976
|
}
|
|
@@ -5995,6 +6161,34 @@ var Viewport = class {
|
|
|
5995
6161
|
getSelectedIds() {
|
|
5996
6162
|
return this.getSelectTool()?.selectedIds ?? EMPTY_IDS;
|
|
5997
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
|
+
}
|
|
5998
6192
|
onSelectionChange(listener) {
|
|
5999
6193
|
const tool = this.getSelectTool();
|
|
6000
6194
|
return tool ? tool.onSelectionChange(listener) : noop;
|
|
@@ -6055,6 +6249,20 @@ var Viewport = class {
|
|
|
6055
6249
|
}
|
|
6056
6250
|
this.historyRecorder.commit();
|
|
6057
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
|
+
}
|
|
6058
6266
|
alignSelection(edge) {
|
|
6059
6267
|
const bounded = this.boundedSelection();
|
|
6060
6268
|
if (bounded.length < 2) return;
|
|
@@ -6153,12 +6361,14 @@ var Viewport = class {
|
|
|
6153
6361
|
this.noteEditor.destroy(this.store);
|
|
6154
6362
|
this.arrowLabelEditor.cancel();
|
|
6155
6363
|
this.historyRecorder.destroy();
|
|
6364
|
+
this.contextMenu?.dispose();
|
|
6156
6365
|
this.wrapper.removeEventListener("pointerdown", this.onTapDown);
|
|
6157
6366
|
this.wrapper.removeEventListener("pointerup", this.onDoubleTap);
|
|
6158
6367
|
this.wrapper.removeEventListener("dragover", this.onDragOver);
|
|
6159
6368
|
this.wrapper.removeEventListener("drop", this.onDrop);
|
|
6160
6369
|
this.inputHandler.destroy();
|
|
6161
6370
|
this.unsubCamera();
|
|
6371
|
+
this.unsubToolChange();
|
|
6162
6372
|
this.unsubStore.forEach((fn) => fn());
|
|
6163
6373
|
this.resizeObserver?.disconnect();
|
|
6164
6374
|
this.resizeObserver = null;
|
|
@@ -6986,6 +7196,15 @@ var SelectTool = class {
|
|
|
6986
7196
|
this.setSelectedIds(ids);
|
|
6987
7197
|
this.ctx?.requestRender();
|
|
6988
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
|
+
}
|
|
6989
7208
|
get isMarqueeActive() {
|
|
6990
7209
|
return this.mode.type === "marquee";
|
|
6991
7210
|
}
|
|
@@ -7489,6 +7708,7 @@ var SelectTool = class {
|
|
|
7489
7708
|
for (const id of this._selectedIds) {
|
|
7490
7709
|
const el = ctx.store.getById(id);
|
|
7491
7710
|
if (!el || !("size" in el)) continue;
|
|
7711
|
+
if (el.locked) continue;
|
|
7492
7712
|
if (el.type === "shape" && el.shape === "line") continue;
|
|
7493
7713
|
const layout = this.getOverlayLayout(el, zoom);
|
|
7494
7714
|
if (!layout) continue;
|
|
@@ -7624,62 +7844,89 @@ var SelectTool = class {
|
|
|
7624
7844
|
canvasCtx.stroke();
|
|
7625
7845
|
}
|
|
7626
7846
|
}
|
|
7627
|
-
if (
|
|
7628
|
-
|
|
7629
|
-
|
|
7630
|
-
|
|
7631
|
-
|
|
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;
|
|
7632
7872
|
canvasCtx.fillRect(
|
|
7633
|
-
|
|
7634
|
-
|
|
7873
|
+
hx - handleWorldSize / 2,
|
|
7874
|
+
hy - handleWorldSize / 2,
|
|
7635
7875
|
handleWorldSize,
|
|
7636
7876
|
handleWorldSize
|
|
7637
7877
|
);
|
|
7638
7878
|
canvasCtx.strokeRect(
|
|
7639
|
-
|
|
7640
|
-
|
|
7879
|
+
hx - handleWorldSize / 2,
|
|
7880
|
+
hy - handleWorldSize / 2,
|
|
7641
7881
|
handleWorldSize,
|
|
7642
7882
|
handleWorldSize
|
|
7643
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]);
|
|
7644
7900
|
}
|
|
7645
|
-
canvasCtx.setLineDash([4 / zoom, 4 / zoom]);
|
|
7646
|
-
} else if (el.type === "template") {
|
|
7647
|
-
canvasCtx.setLineDash([]);
|
|
7648
|
-
canvasCtx.fillStyle = "#ffffff";
|
|
7649
|
-
const hx = bounds.x + bounds.w;
|
|
7650
|
-
const hy = bounds.y + bounds.h;
|
|
7651
|
-
canvasCtx.fillRect(
|
|
7652
|
-
hx - handleWorldSize / 2,
|
|
7653
|
-
hy - handleWorldSize / 2,
|
|
7654
|
-
handleWorldSize,
|
|
7655
|
-
handleWorldSize
|
|
7656
|
-
);
|
|
7657
|
-
canvasCtx.strokeRect(
|
|
7658
|
-
hx - handleWorldSize / 2,
|
|
7659
|
-
hy - handleWorldSize / 2,
|
|
7660
|
-
handleWorldSize,
|
|
7661
|
-
handleWorldSize
|
|
7662
|
-
);
|
|
7663
|
-
canvasCtx.setLineDash([4 / zoom, 4 / zoom]);
|
|
7664
7901
|
}
|
|
7665
|
-
if (
|
|
7666
|
-
const
|
|
7667
|
-
|
|
7668
|
-
canvasCtx.beginPath();
|
|
7669
|
-
canvasCtx.moveTo(stemStart.x, stemStart.y);
|
|
7670
|
-
canvasCtx.lineTo(stemEnd.x, stemEnd.y);
|
|
7671
|
-
canvasCtx.stroke();
|
|
7672
|
-
canvasCtx.setLineDash([]);
|
|
7673
|
-
canvasCtx.fillStyle = "#ffffff";
|
|
7674
|
-
canvasCtx.beginPath();
|
|
7675
|
-
canvasCtx.arc(stemEnd.x, stemEnd.y, handleWorldSize / 2, 0, Math.PI * 2);
|
|
7676
|
-
canvasCtx.fill();
|
|
7677
|
-
canvasCtx.stroke();
|
|
7678
|
-
canvasCtx.setLineDash([4 / zoom, 4 / zoom]);
|
|
7902
|
+
if (el.locked) {
|
|
7903
|
+
const ne = layout.corners.find(([h]) => h === "ne")?.[1];
|
|
7904
|
+
if (ne) this.drawLockBadge(canvasCtx, ne, zoom);
|
|
7679
7905
|
}
|
|
7680
7906
|
}
|
|
7681
7907
|
canvasCtx.restore();
|
|
7682
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
|
+
}
|
|
7683
7930
|
renderBindingHighlights(canvasCtx, arrow, zoom) {
|
|
7684
7931
|
if (!this.ctx) return;
|
|
7685
7932
|
if (!arrow.fromBinding && !arrow.toBinding) return;
|
|
@@ -8652,7 +8899,7 @@ var TemplateTool = class {
|
|
|
8652
8899
|
};
|
|
8653
8900
|
|
|
8654
8901
|
// src/index.ts
|
|
8655
|
-
var VERSION = "0.
|
|
8902
|
+
var VERSION = "0.36.0";
|
|
8656
8903
|
// Annotate the CommonJS export names for ESM import in node:
|
|
8657
8904
|
0 && (module.exports = {
|
|
8658
8905
|
ArrowTool,
|