@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 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.preventDefault();
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.preventDefault();
1547
+ e?.preventDefault();
1521
1548
  this.actions.undo();
1522
1549
  return;
1523
1550
  case "redo":
1524
- e.preventDefault();
1551
+ e?.preventDefault();
1525
1552
  this.actions.redo();
1526
1553
  return;
1527
1554
  case "select-all":
1528
- e.preventDefault();
1555
+ e?.preventDefault();
1529
1556
  this.actions.selectAll();
1530
1557
  return;
1531
1558
  case "copy":
1532
- e.preventDefault();
1559
+ e?.preventDefault();
1533
1560
  this.actions.copy();
1534
1561
  return;
1535
1562
  case "paste":
1536
- e.preventDefault();
1563
+ e?.preventDefault();
1537
1564
  this.actions.paste();
1538
1565
  return;
1539
1566
  case "duplicate":
1540
- e.preventDefault();
1567
+ e?.preventDefault();
1541
1568
  this.actions.duplicate();
1542
1569
  return;
1543
1570
  case "z-forward":
1544
- e.preventDefault();
1571
+ e?.preventDefault();
1545
1572
  this.actions.zOrder("forward");
1546
1573
  return;
1547
1574
  case "z-backward":
1548
- e.preventDefault();
1575
+ e?.preventDefault();
1549
1576
  this.actions.zOrder("backward");
1550
1577
  return;
1551
1578
  case "z-front":
1552
- e.preventDefault();
1579
+ e?.preventDefault();
1553
1580
  this.actions.zOrder("front");
1554
1581
  return;
1555
1582
  case "z-back":
1556
- e.preventDefault();
1583
+ e?.preventDefault();
1557
1584
  this.actions.zOrder("back");
1558
1585
  return;
1559
1586
  case "zoom-fit":
1560
- e.preventDefault();
1587
+ e?.preventDefault();
1561
1588
  this.actions.zoomToFit();
1562
1589
  return;
1563
1590
  case "group":
1564
- e.preventDefault();
1591
+ e?.preventDefault();
1565
1592
  this.actions.group();
1566
1593
  return;
1567
1594
  case "ungroup":
1568
- e.preventDefault();
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.preventDefault();
1607
+ e?.preventDefault();
1573
1608
  this.zoomByFactor(ZOOM_STEP);
1574
1609
  return;
1575
1610
  case "zoom-out":
1576
- e.preventDefault();
1611
+ e?.preventDefault();
1577
1612
  this.zoomByFactor(1 / ZOOM_STEP);
1578
1613
  return;
1579
1614
  case "zoom-reset":
1580
- e.preventDefault();
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.shiftKey)) {
1589
- e.preventDefault();
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.preventDefault();
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 ("size" in el) {
7628
- canvasCtx.setLineDash([]);
7629
- canvasCtx.fillStyle = "#ffffff";
7630
- const corners = layout.angle === 0 ? this.getHandlePositions(bounds) : layout.corners;
7631
- for (const [, pos] of corners) {
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
- pos.x - handleWorldSize / 2,
7634
- pos.y - handleWorldSize / 2,
7873
+ hx - handleWorldSize / 2,
7874
+ hy - handleWorldSize / 2,
7635
7875
  handleWorldSize,
7636
7876
  handleWorldSize
7637
7877
  );
7638
7878
  canvasCtx.strokeRect(
7639
- pos.x - handleWorldSize / 2,
7640
- pos.y - handleWorldSize / 2,
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 (this._selectedIds.length === 1 && ROTATABLE_TYPES.has(el.type)) {
7666
- const stemStart = this.topMidpoint(layout);
7667
- const stemEnd = layout.rotateHandle;
7668
- canvasCtx.beginPath();
7669
- canvasCtx.moveTo(stemStart.x, stemStart.y);
7670
- canvasCtx.lineTo(stemEnd.x, stemEnd.y);
7671
- canvasCtx.stroke();
7672
- canvasCtx.setLineDash([]);
7673
- canvasCtx.fillStyle = "#ffffff";
7674
- canvasCtx.beginPath();
7675
- canvasCtx.arc(stemEnd.x, stemEnd.y, handleWorldSize / 2, 0, Math.PI * 2);
7676
- canvasCtx.fill();
7677
- canvasCtx.stroke();
7678
- canvasCtx.setLineDash([4 / zoom, 4 / zoom]);
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.35.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,