@fieldnotes/core 0.35.0 → 0.37.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/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
@@ -2013,9 +2083,10 @@ var Quadtree = class {
2013
2083
  };
2014
2084
 
2015
2085
  // src/elements/stroke-smoothing.ts
2016
- var MIN_PRESSURE_SCALE = 0.2;
2086
+ var MIN_PRESSURE_SCALE = 0.4;
2087
+ var MAX_PRESSURE_SCALE = 1.8;
2017
2088
  function pressureToWidth(pressure, baseWidth) {
2018
- return baseWidth * (MIN_PRESSURE_SCALE + (1 - MIN_PRESSURE_SCALE) * pressure);
2089
+ return baseWidth * (MIN_PRESSURE_SCALE + (MAX_PRESSURE_SCALE - MIN_PRESSURE_SCALE) * pressure);
2019
2090
  }
2020
2091
  function simplifyPoints(points, tolerance) {
2021
2092
  if (points.length <= 2) return points.slice();
@@ -3678,12 +3749,33 @@ var NoteToolbar = class {
3678
3749
  e.stopPropagation();
3679
3750
  });
3680
3751
  select.addEventListener("change", () => {
3681
- setFontSize(Number(select.value));
3752
+ this.applyFontSize(Number(select.value));
3682
3753
  this.updateActiveStates();
3683
3754
  this.anchor?.focus();
3684
3755
  });
3685
3756
  return select;
3686
3757
  }
3758
+ applyFontSize(size) {
3759
+ const sel = window.getSelection();
3760
+ const collapsed = !sel || sel.rangeCount === 0 || sel.getRangeAt(0).collapsed;
3761
+ if (collapsed && this.anchor) {
3762
+ const range = document.createRange();
3763
+ range.selectNodeContents(this.anchor);
3764
+ sel?.removeAllRanges();
3765
+ sel?.addRange(range);
3766
+ setFontSize(size);
3767
+ const after = window.getSelection();
3768
+ if (after) {
3769
+ const caret = document.createRange();
3770
+ caret.selectNodeContents(this.anchor);
3771
+ caret.collapse(false);
3772
+ after.removeAllRanges();
3773
+ after.addRange(caret);
3774
+ }
3775
+ return;
3776
+ }
3777
+ setFontSize(size);
3778
+ }
3687
3779
  positionToolbar(anchor) {
3688
3780
  if (!this.el) return;
3689
3781
  const rect = anchor.getBoundingClientRect();
@@ -3882,6 +3974,87 @@ var NoteEditor = class {
3882
3974
  }
3883
3975
  };
3884
3976
 
3977
+ // src/canvas/context-menu.ts
3978
+ var ContextMenu = class {
3979
+ constructor(options) {
3980
+ this.options = options;
3981
+ }
3982
+ el = null;
3983
+ outsideListener = null;
3984
+ keyListener = null;
3985
+ isOpen() {
3986
+ return this.el !== null;
3987
+ }
3988
+ open(items, screenPos) {
3989
+ this.close();
3990
+ const el = document.createElement("div");
3991
+ el.className = "fieldnotes-context-menu";
3992
+ Object.assign(el.style, {
3993
+ position: "fixed",
3994
+ left: `${screenPos.x}px`,
3995
+ top: `${screenPos.y}px`,
3996
+ zIndex: "10000",
3997
+ display: "flex",
3998
+ flexDirection: "column"
3999
+ });
4000
+ for (const item of items) {
4001
+ const btn = document.createElement("button");
4002
+ btn.type = "button";
4003
+ btn.className = "fieldnotes-context-menu-item" + (item.disabled ? " fieldnotes-context-menu-item--disabled" : "");
4004
+ btn.textContent = item.label;
4005
+ if (item.disabled) {
4006
+ btn.disabled = true;
4007
+ } else {
4008
+ btn.addEventListener("click", () => {
4009
+ this.options.onCommand(item.action);
4010
+ this.close();
4011
+ });
4012
+ }
4013
+ el.appendChild(btn);
4014
+ }
4015
+ document.body.appendChild(el);
4016
+ this.el = el;
4017
+ this.clampToViewport(el, screenPos);
4018
+ this.keyListener = (e) => {
4019
+ if (e.key === "Escape") this.close();
4020
+ };
4021
+ document.addEventListener("keydown", this.keyListener);
4022
+ this.outsideListener = (e) => {
4023
+ if (this.el && !this.el.contains(e.target)) this.close();
4024
+ };
4025
+ setTimeout(() => {
4026
+ if (this.outsideListener) document.addEventListener("pointerdown", this.outsideListener);
4027
+ }, 0);
4028
+ }
4029
+ close() {
4030
+ if (this.keyListener) {
4031
+ document.removeEventListener("keydown", this.keyListener);
4032
+ this.keyListener = null;
4033
+ }
4034
+ if (this.outsideListener) {
4035
+ document.removeEventListener("pointerdown", this.outsideListener);
4036
+ this.outsideListener = null;
4037
+ }
4038
+ if (this.el) {
4039
+ this.el.remove();
4040
+ this.el = null;
4041
+ this.options.onClose();
4042
+ }
4043
+ }
4044
+ dispose() {
4045
+ this.close();
4046
+ }
4047
+ clampToViewport(el, screenPos) {
4048
+ const rect = el.getBoundingClientRect();
4049
+ if (rect.width > 0 && screenPos.x + rect.width > window.innerWidth) {
4050
+ el.style.left = `${Math.max(0, screenPos.x - rect.width)}px`;
4051
+ }
4052
+ if (rect.height > 0 && screenPos.y + rect.height > window.innerHeight) {
4053
+ el.style.top = `${Math.max(0, screenPos.y - rect.height)}px`;
4054
+ }
4055
+ }
4056
+ };
4057
+
3885
4058
  // src/elements/translate.ts
3886
4059
  function translateElementPatch(el, dx, dy) {
3887
4060
  const position = { x: el.position.x + dx, y: el.position.y + dy };
@@ -5698,8 +5871,20 @@ var Viewport = class {
5698
5871
  fitToContent: () => this.fitToContent(),
5699
5872
  group: () => this.groupSelection(),
5700
5873
  ungroup: () => this.ungroupSelection(),
5874
+ toggleLock: () => this.toggleLockSelection(),
5875
+ openContextMenu: (screenPos, world) => {
5876
+ this.getSelectTool()?.selectAtPoint(world, this.toolContext);
5877
+ this.openContextMenu(screenPos);
5878
+ },
5701
5879
  shortcuts: options.shortcuts
5702
5880
  });
5881
+ if (options.contextMenu !== false) {
5882
+ this.contextMenu = new ContextMenu({
5883
+ onCommand: (action) => this.runAction(action),
5884
+ onClose: noop
5885
+ });
5886
+ }
5887
+ this.unsubToolChange = this.toolManager.onChange(() => this.contextMenu?.close());
5703
5888
  this.domNodeManager = new DomNodeManager({
5704
5889
  domLayer: this.domLayer,
5705
5890
  onEditRequest: (id) => this.startEditingElement(id),
@@ -5731,6 +5916,7 @@ var Viewport = class {
5731
5916
  this.unsubCamera = this.camera.onChange(() => {
5732
5917
  this.applyCameraTransform();
5733
5918
  this.noteEditor.updateToolbarPosition();
5919
+ this.contextMenu?.close();
5734
5920
  this.requestRender();
5735
5921
  });
5736
5922
  this.unsubStore = [
@@ -5783,6 +5969,7 @@ var Viewport = class {
5783
5969
  canvasEl;
5784
5970
  wrapper;
5785
5971
  unsubCamera;
5972
+ unsubToolChange;
5786
5973
  unsubStore;
5787
5974
  inputHandler;
5788
5975
  background;
@@ -5805,6 +5992,7 @@ var Viewport = class {
5805
5992
  doubleTapDetector = new DoubleTapDetector();
5806
5993
  tapDownX = 0;
5807
5994
  tapDownY = 0;
5995
+ contextMenu = null;
5808
5996
  get ctx() {
5809
5997
  return this.canvasEl.getContext("2d");
5810
5998
  }
@@ -5995,6 +6183,34 @@ var Viewport = class {
5995
6183
  getSelectedIds() {
5996
6184
  return this.getSelectTool()?.selectedIds ?? EMPTY_IDS;
5997
6185
  }
6186
+ runAction(action) {
6187
+ this.inputHandler.runAction(action);
6188
+ }
6189
+ canPaste() {
6190
+ return this.inputHandler.hasClipboard();
6191
+ }
6192
+ openContextMenu(screenPos) {
6193
+ if (!this.contextMenu) return;
6194
+ const ids = this.getSelectedIds();
6195
+ const items = [];
6196
+ if (ids.length > 0) {
6197
+ items.push({ label: "Cut", action: "cut" });
6198
+ items.push({ label: "Copy", action: "copy" });
6199
+ if (this.canPaste()) items.push({ label: "Paste", action: "paste" });
6200
+ items.push({ label: "Duplicate", action: "duplicate" });
6201
+ items.push({ label: "Delete", action: "delete" });
6202
+ items.push({ label: "Bring to Front", action: "z-front" });
6203
+ items.push({ label: "Bring Forward", action: "z-forward" });
6204
+ items.push({ label: "Send Backward", action: "z-backward" });
6205
+ items.push({ label: "Send to Back", action: "z-back" });
6206
+ const allLocked = ids.every((id) => this.store.getById(id)?.locked);
6207
+ items.push({ label: allLocked ? "Unlock" : "Lock", action: "toggle-lock" });
6208
+ } else if (this.canPaste()) {
6209
+ items.push({ label: "Paste", action: "paste" });
6210
+ }
6211
+ if (items.length === 0) return;
6212
+ this.contextMenu.open(items, screenPos);
6213
+ }
5998
6214
  onSelectionChange(listener) {
5999
6215
  const tool = this.getSelectTool();
6000
6216
  return tool ? tool.onSelectionChange(listener) : noop;
@@ -6055,6 +6271,20 @@ var Viewport = class {
6055
6271
  }
6056
6272
  this.historyRecorder.commit();
6057
6273
  }
6274
+ toggleLockSelection() {
6275
+ const ids = this.getSelectedIds();
6276
+ if (ids.length === 0) return;
6277
+ const anyUnlocked = ids.some((id) => {
6278
+ const el = this.store.getById(id);
6279
+ return el ? !el.locked : false;
6280
+ });
6281
+ this.historyRecorder.begin();
6282
+ for (const id of ids) {
6283
+ const el = this.store.getById(id);
6284
+ if (el && el.locked !== anyUnlocked) this.store.update(id, { locked: anyUnlocked });
6285
+ }
6286
+ this.historyRecorder.commit();
6287
+ }
6058
6288
  alignSelection(edge) {
6059
6289
  const bounded = this.boundedSelection();
6060
6290
  if (bounded.length < 2) return;
@@ -6153,12 +6383,14 @@ var Viewport = class {
6153
6383
  this.noteEditor.destroy(this.store);
6154
6384
  this.arrowLabelEditor.cancel();
6155
6385
  this.historyRecorder.destroy();
6386
+ this.contextMenu?.dispose();
6156
6387
  this.wrapper.removeEventListener("pointerdown", this.onTapDown);
6157
6388
  this.wrapper.removeEventListener("pointerup", this.onDoubleTap);
6158
6389
  this.wrapper.removeEventListener("dragover", this.onDragOver);
6159
6390
  this.wrapper.removeEventListener("drop", this.onDrop);
6160
6391
  this.inputHandler.destroy();
6161
6392
  this.unsubCamera();
6393
+ this.unsubToolChange();
6162
6394
  this.unsubStore.forEach((fn) => fn());
6163
6395
  this.resizeObserver?.disconnect();
6164
6396
  this.resizeObserver = null;
@@ -6986,6 +7218,15 @@ var SelectTool = class {
6986
7218
  this.setSelectedIds(ids);
6987
7219
  this.ctx?.requestRender();
6988
7220
  }
7221
+ selectAtPoint(world, ctx) {
7222
+ const hit = this.hitTest(world, ctx);
7223
+ if (!hit) {
7224
+ this.setSelectedIds([]);
7225
+ return;
7226
+ }
7227
+ if (this._selectedIds.includes(hit.id)) return;
7228
+ this.setSelectedIds(expandToGroups([hit.id], ctx.store.getAll()));
7229
+ }
6989
7230
  get isMarqueeActive() {
6990
7231
  return this.mode.type === "marquee";
6991
7232
  }
@@ -7489,6 +7730,7 @@ var SelectTool = class {
7489
7730
  for (const id of this._selectedIds) {
7490
7731
  const el = ctx.store.getById(id);
7491
7732
  if (!el || !("size" in el)) continue;
7733
+ if (el.locked) continue;
7492
7734
  if (el.type === "shape" && el.shape === "line") continue;
7493
7735
  const layout = this.getOverlayLayout(el, zoom);
7494
7736
  if (!layout) continue;
@@ -7624,62 +7866,89 @@ var SelectTool = class {
7624
7866
  canvasCtx.stroke();
7625
7867
  }
7626
7868
  }
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) {
7869
+ if (!el.locked) {
7870
+ if ("size" in el) {
7871
+ canvasCtx.setLineDash([]);
7872
+ canvasCtx.fillStyle = "#ffffff";
7873
+ const corners = layout.angle === 0 ? this.getHandlePositions(bounds) : layout.corners;
7874
+ for (const [, pos] of corners) {
7875
+ canvasCtx.fillRect(
7876
+ pos.x - handleWorldSize / 2,
7877
+ pos.y - handleWorldSize / 2,
7878
+ handleWorldSize,
7879
+ handleWorldSize
7880
+ );
7881
+ canvasCtx.strokeRect(
7882
+ pos.x - handleWorldSize / 2,
7883
+ pos.y - handleWorldSize / 2,
7884
+ handleWorldSize,
7885
+ handleWorldSize
7886
+ );
7887
+ }
7888
+ canvasCtx.setLineDash([4 / zoom, 4 / zoom]);
7889
+ } else if (el.type === "template") {
7890
+ canvasCtx.setLineDash([]);
7891
+ canvasCtx.fillStyle = "#ffffff";
7892
+ const hx = bounds.x + bounds.w;
7893
+ const hy = bounds.y + bounds.h;
7632
7894
  canvasCtx.fillRect(
7633
- pos.x - handleWorldSize / 2,
7634
- pos.y - handleWorldSize / 2,
7895
+ hx - handleWorldSize / 2,
7896
+ hy - handleWorldSize / 2,
7635
7897
  handleWorldSize,
7636
7898
  handleWorldSize
7637
7899
  );
7638
7900
  canvasCtx.strokeRect(
7639
- pos.x - handleWorldSize / 2,
7640
- pos.y - handleWorldSize / 2,
7901
+ hx - handleWorldSize / 2,
7902
+ hy - handleWorldSize / 2,
7641
7903
  handleWorldSize,
7642
7904
  handleWorldSize
7643
7905
  );
7906
+ canvasCtx.setLineDash([4 / zoom, 4 / zoom]);
7907
+ }
7908
+ if (this._selectedIds.length === 1 && ROTATABLE_TYPES.has(el.type)) {
7909
+ const stemStart = this.topMidpoint(layout);
7910
+ const stemEnd = layout.rotateHandle;
7911
+ canvasCtx.beginPath();
7912
+ canvasCtx.moveTo(stemStart.x, stemStart.y);
7913
+ canvasCtx.lineTo(stemEnd.x, stemEnd.y);
7914
+ canvasCtx.stroke();
7915
+ canvasCtx.setLineDash([]);
7916
+ canvasCtx.fillStyle = "#ffffff";
7917
+ canvasCtx.beginPath();
7918
+ canvasCtx.arc(stemEnd.x, stemEnd.y, handleWorldSize / 2, 0, Math.PI * 2);
7919
+ canvasCtx.fill();
7920
+ canvasCtx.stroke();
7921
+ canvasCtx.setLineDash([4 / zoom, 4 / zoom]);
7644
7922
  }
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
7923
  }
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]);
7924
+ if (el.locked) {
7925
+ const ne = layout.corners.find(([h]) => h === "ne")?.[1];
7926
+ if (ne) this.drawLockBadge(canvasCtx, ne, zoom);
7679
7927
  }
7680
7928
  }
7681
7929
  canvasCtx.restore();
7682
7930
  }
7931
+ drawLockBadge(ctx, at, zoom) {
7932
+ const r = 9 / zoom;
7933
+ ctx.save();
7934
+ ctx.setLineDash([]);
7935
+ ctx.beginPath();
7936
+ ctx.arc(at.x, at.y, r, 0, Math.PI * 2);
7937
+ ctx.fillStyle = "#ffffff";
7938
+ ctx.fill();
7939
+ ctx.strokeStyle = "#2196F3";
7940
+ ctx.lineWidth = 1.5 / zoom;
7941
+ ctx.stroke();
7942
+ const bw = 8 / zoom;
7943
+ const bh = 6 / zoom;
7944
+ ctx.fillStyle = "#2196F3";
7945
+ ctx.fillRect(at.x - bw / 2, at.y - bh / 2 + 1 / zoom, bw, bh);
7946
+ ctx.beginPath();
7947
+ ctx.arc(at.x, at.y - bh / 2 + 1 / zoom, 2.5 / zoom, Math.PI, 0);
7948
+ ctx.lineWidth = 1.4 / zoom;
7949
+ ctx.stroke();
7950
+ ctx.restore();
7951
+ }
7683
7952
  renderBindingHighlights(canvasCtx, arrow, zoom) {
7684
7953
  if (!this.ctx) return;
7685
7954
  if (!arrow.fromBinding && !arrow.toBinding) return;
@@ -8652,7 +8921,7 @@ var TemplateTool = class {
8652
8921
  };
8653
8922
 
8654
8923
  // src/index.ts
8655
- var VERSION = "0.35.0";
8924
+ var VERSION = "0.37.0";
8656
8925
  // Annotate the CommonJS export names for ESM import in node:
8657
8926
  0 && (module.exports = {
8658
8927
  ArrowTool,